@signal9/era-ui 32.0.1 → 33.0.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 (142) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/dev/audit-overlay.svelte +2 -2
  3. package/dist/era-ui.css +1 -1
  4. package/dist/generated-docs/utilities.json +99 -99
  5. package/dist/styles/index.css +2 -956
  6. package/dist/styles/typography.css +1 -1
  7. package/dist/styles/utilities/geometry.css +188 -0
  8. package/dist/styles/utilities/interaction.css +334 -0
  9. package/dist/styles/utilities/materials.css +39 -0
  10. package/dist/styles/utilities/public.css +391 -0
  11. package/dist/styles/utilities.css +5 -0
  12. package/dist/ui/card/card.svelte +2 -52
  13. package/dist/ui/card/card.svelte.d.ts +0 -43
  14. package/dist/ui/card/variants.d.ts +44 -0
  15. package/dist/ui/card/variants.js +20 -0
  16. package/dist/ui/context-menu/context-menu-checkbox-item.svelte +2 -4
  17. package/dist/ui/context-menu/context-menu-group-heading.svelte +2 -5
  18. package/dist/ui/context-menu/context-menu-item.svelte +2 -8
  19. package/dist/ui/context-menu/context-menu-radio-item.svelte +2 -8
  20. package/dist/ui/context-menu/context-menu-separator.svelte +2 -5
  21. package/dist/ui/context-menu/context-menu-sub-trigger.svelte +2 -8
  22. package/dist/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +2 -4
  23. package/dist/ui/dropdown-menu/dropdown-menu-group-heading.svelte +2 -5
  24. package/dist/ui/dropdown-menu/dropdown-menu-item.svelte +2 -8
  25. package/dist/ui/dropdown-menu/dropdown-menu-radio-item.svelte +2 -8
  26. package/dist/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -5
  27. package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -8
  28. package/dist/ui/menu-recipes.d.ts +6 -0
  29. package/dist/ui/menu-recipes.js +6 -0
  30. package/dist/ui/menubar/menubar-checkbox-item.svelte +2 -9
  31. package/dist/ui/menubar/menubar-group-heading.svelte +2 -5
  32. package/dist/ui/menubar/menubar-item.svelte +2 -8
  33. package/dist/ui/menubar/menubar-radio-item.svelte +2 -8
  34. package/dist/ui/menubar/menubar-separator.svelte +2 -5
  35. package/dist/ui/menubar/menubar-sub-trigger.svelte +2 -8
  36. package/dist/ui/video-player/video-player.svelte +8 -8
  37. package/package.json +48 -12
  38. package/dist/docs/badge.md +0 -18
  39. package/dist/docs/bits-ui-audit.md +0 -58
  40. package/dist/docs/button.md +0 -19
  41. package/dist/docs/chip.md +0 -20
  42. package/dist/docs/component-docs-core.d.ts +0 -8
  43. package/dist/docs/component-docs-core.js +0 -73
  44. package/dist/docs/component-docs.d.ts +0 -2
  45. package/dist/docs/component-docs.js +0 -25
  46. package/dist/docs/component-source-scanners.d.ts +0 -18
  47. package/dist/docs/component-source-scanners.js +0 -46
  48. package/dist/docs/consumer-import.d.ts +0 -65
  49. package/dist/docs/consumer-import.js +0 -70
  50. package/dist/docs/css-utilities.d.ts +0 -58
  51. package/dist/docs/css-utilities.js +0 -317
  52. package/dist/docs/extract-props.d.ts +0 -13
  53. package/dist/docs/extract-props.js +0 -324
  54. package/dist/docs/generated-docs.d.ts +0 -6
  55. package/dist/docs/generated-docs.js +0 -30
  56. package/dist/docs/index.d.ts +0 -5
  57. package/dist/docs/index.js +0 -66
  58. package/dist/docs/notes.md +0 -83
  59. package/dist/docs/scroll-area.md +0 -7
  60. package/dist/docs/separator.md +0 -20
  61. package/dist/docs/table.md +0 -35
  62. package/dist/docs/utilities.d.ts +0 -4
  63. package/dist/docs/utilities.js +0 -15
  64. package/dist/generated-docs/accordion.md +0 -57
  65. package/dist/generated-docs/alert-dialog.md +0 -91
  66. package/dist/generated-docs/aspect-ratio.md +0 -17
  67. package/dist/generated-docs/avatar.md +0 -39
  68. package/dist/generated-docs/badge.md +0 -41
  69. package/dist/generated-docs/bar.md +0 -41
  70. package/dist/generated-docs/button-group.md +0 -19
  71. package/dist/generated-docs/button.md +0 -117
  72. package/dist/generated-docs/calendar.md +0 -126
  73. package/dist/generated-docs/card.md +0 -58
  74. package/dist/generated-docs/checkbox.md +0 -42
  75. package/dist/generated-docs/chip.md +0 -61
  76. package/dist/generated-docs/code-block.md +0 -20
  77. package/dist/generated-docs/collapsible.md +0 -30
  78. package/dist/generated-docs/combobox.md +0 -74
  79. package/dist/generated-docs/command-bar.md +0 -3
  80. package/dist/generated-docs/command.md +0 -126
  81. package/dist/generated-docs/context-menu.md +0 -104
  82. package/dist/generated-docs/copy-button.md +0 -29
  83. package/dist/generated-docs/cycle.md +0 -29
  84. package/dist/generated-docs/date-field.md +0 -49
  85. package/dist/generated-docs/date-picker.md +0 -180
  86. package/dist/generated-docs/date-range-field.md +0 -50
  87. package/dist/generated-docs/date-range-picker.md +0 -180
  88. package/dist/generated-docs/dialog.md +0 -94
  89. package/dist/generated-docs/draggable.md +0 -3
  90. package/dist/generated-docs/dropdown-menu.md +0 -104
  91. package/dist/generated-docs/file-upload.md +0 -18
  92. package/dist/generated-docs/getting-started.md +0 -3
  93. package/dist/generated-docs/input.md +0 -23
  94. package/dist/generated-docs/kv.md +0 -20
  95. package/dist/generated-docs/label.md +0 -17
  96. package/dist/generated-docs/link-preview.md +0 -40
  97. package/dist/generated-docs/llm-shell.md +0 -3
  98. package/dist/generated-docs/llms-full.txt +0 -5201
  99. package/dist/generated-docs/llms.txt +0 -138
  100. package/dist/generated-docs/logo.md +0 -24
  101. package/dist/generated-docs/manifest.json +0 -1685
  102. package/dist/generated-docs/measurements.md +0 -3
  103. package/dist/generated-docs/menu.md +0 -11
  104. package/dist/generated-docs/menubar.md +0 -104
  105. package/dist/generated-docs/meter.md +0 -101
  106. package/dist/generated-docs/mode.md +0 -24
  107. package/dist/generated-docs/nav.md +0 -73
  108. package/dist/generated-docs/navigation-menu.md +0 -11
  109. package/dist/generated-docs/notes.md +0 -87
  110. package/dist/generated-docs/os.md +0 -3
  111. package/dist/generated-docs/pagination.md +0 -55
  112. package/dist/generated-docs/pane.md +0 -34
  113. package/dist/generated-docs/pin-input.md +0 -31
  114. package/dist/generated-docs/popover.md +0 -43
  115. package/dist/generated-docs/progress.md +0 -17
  116. package/dist/generated-docs/radio-group.md +0 -31
  117. package/dist/generated-docs/range-calendar.md +0 -11
  118. package/dist/generated-docs/rating-group.md +0 -31
  119. package/dist/generated-docs/scroll-area.md +0 -65
  120. package/dist/generated-docs/select.md +0 -67
  121. package/dist/generated-docs/separator.md +0 -57
  122. package/dist/generated-docs/sheet.md +0 -11
  123. package/dist/generated-docs/skeleton.md +0 -18
  124. package/dist/generated-docs/slider.md +0 -71
  125. package/dist/generated-docs/spacing.md +0 -3
  126. package/dist/generated-docs/step.md +0 -51
  127. package/dist/generated-docs/surfaces.md +0 -3
  128. package/dist/generated-docs/switch.md +0 -18
  129. package/dist/generated-docs/table.md +0 -113
  130. package/dist/generated-docs/tabs.md +0 -48
  131. package/dist/generated-docs/text.md +0 -3
  132. package/dist/generated-docs/time-field.md +0 -49
  133. package/dist/generated-docs/time-range-field.md +0 -50
  134. package/dist/generated-docs/timeline.md +0 -32
  135. package/dist/generated-docs/todo.md +0 -3
  136. package/dist/generated-docs/toggle-group.md +0 -31
  137. package/dist/generated-docs/toggle.md +0 -20
  138. package/dist/generated-docs/toolbar.md +0 -11
  139. package/dist/generated-docs/tooltip.md +0 -40
  140. package/dist/generated-docs/tree.md +0 -36
  141. package/dist/generated-docs/utilities.md +0 -1236
  142. package/dist/generated-docs/video-player.md +0 -73
@@ -43,6 +43,8 @@
43
43
  @import './surfaces/bevel.css' layer(era.tokens);
44
44
  /* UNLAYERED: carries the font-axis !important — see above. */
45
45
  @import './typography.css';
46
+ /* Semantic utilities and the public Tailwind vocabulary. */
47
+ @import './utilities.css';
46
48
 
47
49
  /* WHAT IS NOT HERE: vidstack's stylesheets, which the VideoPlayer needs and
48
50
  * nothing else does. They were imported right here, so every consumer of this
@@ -106,959 +108,3 @@
106
108
  -webkit-text-security: var(--era-mask-glyph, disc);
107
109
  }
108
110
  }
109
-
110
- /* Shared interactive-state recipe: quiet focus, disabled affordance.
111
- * Equivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.
112
- *
113
- * @use the focus/disabled recipe every era control carries — put it on a custom
114
- * interactive element instead of hand-writing focus/disabled variants. */
115
- @utility era-interactive {
116
- &:focus {
117
- outline: none;
118
- }
119
- &:disabled {
120
- cursor: not-allowed;
121
- opacity: 0.5;
122
- }
123
- }
124
-
125
- /* Inline text link. An understated, permanent underline that darkens from
126
- * muted to the text colour on hover — no layout shift, no appearing/vanishing
127
- * underline. The colour fade rides the MOTION axis (--era-duration/-ease), so
128
- * it snaps at data-motion="instant", eases at "normal", and sweeps longer at
129
- * "extra" — matching every other era transition. Font-agnostic: it styles
130
- * decoration + colour only, so it reads correctly in mono, sans, or serif, and
131
- * the 0.2em offset scales with the type size. Keyboard focus mirrors hover so
132
- * the affordance is reachable without a pointer.
133
- *
134
- * @use understated, motion-aware inline text link — the canonical style for a
135
- * link in body copy or prose; prefer it over a hand-rolled underline or
136
- * `Button variant="link"` (which is a control, not running text). */
137
- @utility era-link {
138
- /* The text is bright at rest and never changes — the link reads as a link
139
- * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —
140
- * the link's own ink knocked back to 30% so it barely whispers — that
141
- * resolves to the full ink on hover.
142
- *
143
- * currentColor, not --color-muted/--color-fg: the underline belongs to the
144
- * TEXT it underlines. On themes whose link ink is a colour (signalnine's
145
- * navy, an accent-tinted bright), the neutral tokens drew a whitish line
146
- * under coloured text — a second hue where there should be one. Derived
147
- * from currentColor, the underline tracks whatever the link resolves to,
148
- * in every theme, including a caller overriding `color` locally. */
149
- color: var(--color-link);
150
- text-decoration-line: underline;
151
- text-decoration-color: color-mix(in oklch, currentColor 30%, transparent);
152
- text-underline-offset: 0.2em;
153
- transition: text-decoration-color var(--era-duration) var(--era-ease);
154
- &:hover,
155
- &:focus-visible {
156
- text-decoration-color: currentColor;
157
- }
158
- }
159
-
160
- /* Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /
161
- * drag scrolling (e.g. an overflowing tab strip in a small pane handle).
162
- *
163
- * @use hide the scrollbar on an overflowing strip or rail while keeping it
164
- * scrollable — for a scroll region that should show its bar, use ScrollArea. */
165
- @utility scrollbar-none {
166
- scrollbar-width: none;
167
- &::-webkit-scrollbar {
168
- display: none;
169
- }
170
- }
171
-
172
- /* Font-agnostic vertical ink-centering for single-line control text.
173
- * Measured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):
174
- * the offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in
175
- * a 24px row places ink on a half pixel, ~1px error) and font ascent/descent
176
- * asymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and
177
- * is the Firefox fallback; text-box ex/alphabetic fixes the second by
178
- * centering the x-height band — the visual mass of MIXED-CASE text (caps and
179
- * descenders cancel above/below). cap alphabetic is the spec-example recipe
180
- * but is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.
181
- * Where text-box is supported it fully determines the box, so the two
182
- * declarations never fight. Worst-case residual: 0.44px (measured at 14px; the
183
- * mechanism is font-relative, so dense's 12px inherits the same guarantee with
184
- * a proportionally smaller residual).
185
- *
186
- * The hard rules concern overflow clipping, and there are TWO of them.
187
- *
188
- * 1. NEVER on a NATIVE EDITABLE — <input>, <textarea>, <select>. These clip to
189
- * a UA-owned inner editor box that IS the trimmed line box, so a fixed
190
- * height does NOT hold it open: whatever you type gets its ascenders and
191
- * descenders sliced off (a "d" renders as an "o"). This rule used to read
192
- * the other way round — it claimed a fixed-height <input> was the trim's
193
- * intended home — and the Input component shipped chopped text as a result.
194
- * A tier-height input centres its own text; it needs no trim.
195
- *
196
- * 2. NEVER on an AUTO-height element that clips — a `truncate` child, an
197
- * auto-height box with overflow-hidden. With no height to hold it open the
198
- * box collapses to the x-height band (~7.5px at 14px) and the clip chops the
199
- * glyphs into a middle stripe. Put the trim on the fixed-height ROW instead
200
- * and let the child centre via the row's items-center.
201
- *
202
- * A fixed-height NON-editable element is the safe home: the ink overflows the
203
- * trimmed LINE box but stays inside the padding box, which is what actually
204
- * clips. Both rules are enforced at runtime by typography/collapsed-text-trim.
205
- *
206
- * @use vertically center single-line control text (a label beside an icon, a
207
- * button's own text) without a translateY nudge. Fixed-height, non-editable
208
- * elements ONLY: never on an <input>/<textarea>, and never on the same element
209
- * as `truncate` or any overflow clip — put it on the fixed-height ROW and let
210
- * the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS.
211
- */
212
- @utility era-text-trim {
213
- line-height: 1;
214
- text-box: trim-both ex alphabetic;
215
- }
216
-
217
- /* Tailwind's `truncate`, with the vertical clip fixed at the source.
218
- *
219
- * truncate is overflow:hidden, and overflow clips in BOTH axes — so whatever
220
- * line box the element ends up with is also its ink clip. Era rows put
221
- * era-text-trim on the fixed-height row (a Bar, a list row), and its
222
- * `line-height: 1` INHERITS into the truncating child, which then has no fixed
223
- * height of its own: the child's box collapses to 1em while the font's real ink
224
- * extent is ~1.15-1.2em, and the descenders (the "g" hook, the "y" tail) get
225
- * sliced off at the box edge. Same bug on any `leading-none` + `truncate` pair.
226
- *
227
- * The fix belongs here rather than at each call site: a horizontally-truncating
228
- * element must never be the thing that clips vertically. `line-height: normal`
229
- * restores the font's own ascent+descent box — exactly the ink extent — and
230
- * `text-box: normal` cancels a trim if one is set alongside. Rows stay the same
231
- * height (they are tier-fixed and centre the child), and the child now has room
232
- * for its own glyphs.
233
- *
234
- * @use single-line horizontal truncation with an ellipsis — put it on the
235
- * truncating CHILD inside a trimmed or leading-none row, never on the same
236
- * element as era-text-trim; it will not clip ascenders or descenders. */
237
- @utility truncate {
238
- overflow: hidden;
239
- text-overflow: ellipsis;
240
- white-space: nowrap;
241
- line-height: normal;
242
- text-box: normal;
243
- }
244
-
245
- /* THE COLLISION GUARD — `truncate` and `era-text-trim` on the SAME element.
246
- *
247
- * Two utilities, same specificity, opposite intents: the trim collapses the box
248
- * to the x-height band, and truncate clips to whatever box it lands on. Class
249
- * ORDER in the attribute cannot break the tie — CSS resolves same-specificity
250
- * conflicts by stylesheet order, and Tailwind emits era-text-trim last, so the
251
- * trim silently won and the clip chopped the glyphs into a middle stripe
252
- * (measured: an 8.58px box holding 16px of ink). Writing the classes the other
253
- * way round changed nothing, which is what made it so hard to see.
254
- *
255
- * A real consumer hit exactly this. So the pairing is now DEFINED rather than
256
- * accidental, with a compound selector that outranks both single-class
257
- * utilities: truncate wins the vertical, the glyphs stay whole, and the
258
- * element centres the way the docs say it should anyway — via its
259
- * fixed-height parent's items-center.
260
- *
261
- * This is a safety net, not a blessing: the correct authoring is still trim on
262
- * the ROW, truncate on the CHILD (typography/collapsed-text-trim still reports
263
- * the pair so the runtime audit can point at it). It exists so the failure mode
264
- * is a no-op instead of unreadable text. */
265
- .era-text-trim.truncate,
266
- .era-text-trim-caps.truncate,
267
- .era-label-ink.truncate,
268
- .era-token-ink.truncate {
269
- line-height: normal;
270
- text-box: normal;
271
- }
272
-
273
- /* ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
274
- * misjudges caps-only strings — cap/alphabetic edges are the right box.
275
- *
276
- * @use the era-text-trim variant for ALL-CAPS single-line strings (kbd hints,
277
- * acronym badges) — never for mixed-case text (a label like "System" reads
278
- * high; use era-text-trim), and never on the same element as `truncate`. */
279
- @utility era-text-trim-caps {
280
- line-height: 1;
281
- text-box: trim-both cap alphabetic;
282
- }
283
-
284
- /* The EDITABLE counterpart of era-text-trim — for native <input>s, which the
285
- * trim is forbidden on (it collapses the UA's inner editor box and slices the
286
- * glyphs; see typography/collapsed-text-trim).
287
- *
288
- * Without help, a tier-height input centres its text by FONT BOX, and the ui
289
- * stacks' font box is deeply asymmetric: ascent ≈1.07em against descent
290
- * ≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em
291
- * LOW — so a field's text sat visibly below the trimmed label beside it.
292
- *
293
- * The value is OPTICAL, not the derivation's: pure x-band centring computes
294
- * to ~0.23em of padding, but pixel-measured against real Title-Case text it
295
- * over-lifts — the eye weighs the cap-to-baseline mass, which x-band
296
- * centring parks ~2px high (caps 2.75px off the top against 5.7px of air
297
- * under the baseline, measured at dense; the general form of that offset,
298
- * and why era-label-ink's note about "System" is not a contradiction of it,
299
- * is derived at era-label-ink below). 0.08em splits the difference
300
- * between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
301
- * 18px field with ~4.7px under the baseline — the reading where neither
302
- * band's partisans win and neither complains.
303
- *
304
- * The lift is padding, not a translateY: an icon-less Input paints the field
305
- * chrome itself, and a transform would drag the field along with the ink.
306
- * padding-bottom shrinks the content box from below, and a single-line input
307
- * re-centres its line box in what remains — the ink rises by exactly half the
308
- * padding, the chrome doesn't move, and the caret and selection follow the
309
- * text. In em on purpose: the correction is a fraction of the type, so it
310
- * holds across every density and text size with one value.
311
- *
312
- * WHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS "TOO HIGH", check the
313
- * STRING before the geometry. A placeholder like "Filter…" has no descender and
314
- * no cap, so its ink occupies only the x-height band — and a box that centres
315
- * that band exactly still reads high, because the eye centres on the mass it
316
- * can see and expects the space a descender would have filled. Measured at the
317
- * pill tier on the shipped mono stack, "Filter…" sits within half a pixel of
318
- * centre while looking a little high; the pill suite documents the same effect
319
- * from the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where "soon"
320
- * cannot be symmetric and is bounded by sign instead of magnitude).
321
- *
322
- * That is not a licence to ignore the report — era-ink-center IS wrong by
323
- * 1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a
324
- * reason to measure the ink before touching the constant.
325
- *
326
- * @use on a native single-line <input> (or the text layer that must mirror
327
- * one) inside a fixed-height field — never alongside era-text-trim. */
328
- @utility era-ink-center {
329
- padding-bottom: 0.08em;
330
- }
331
-
332
- /* TOKEN INK — the ink centring for a pill whose content is a TOKEN: an id, a
333
- * count, a status, a tag. Font-independent, unlike everything it replaces.
334
- *
335
- * THE BUG IT FIXES, which had been shipping library-wide and invisibly:
336
- * `text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all
337
- * flex, and a flex container's bare text becomes an ANONYMOUS flex item — a box
338
- * no selector can reach and no non-inherited property can touch. So
339
- * `era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one
340
- * thing: `line-height: 1`. The ink was then centred by `align-items: center`
341
- * on the LINE BOX, which is precisely the font-metric-dependent centring the
342
- * trim exists to eliminate. Measured on the shipped mono stack: a badge reading
343
- * "AC-2.1" sat 2px high in an 18px pill, and no amount of tuning the trim's
344
- * BAND changed it, because the trim was never running.
345
- *
346
- * The mechanism here restores it: a BLOCK box (where text-box-trim does apply)
347
- * whose trimmed line box is then centred by `align-content` — block-container
348
- * alignment, so no flex formatting context and no anonymous item. The browser
349
- * computes the band from the font's own metrics, which is what makes it hold
350
- * across ui-monospace, system-ui and ui-serif instead of needing a constant
351
- * per stack.
352
- *
353
- * CAP band, not the x-height band the general trim uses, because the content
354
- * class is different: a token is caps and digits (both cap-height), and
355
- * centring the x-band parks them high by (cap − x)/2. Measured on the shipped
356
- * stacks, cap-band centring takes an id/count/status from −2.0px to 0.0.
357
- * Prose and sentence-case labels keep era-text-trim; this is for tokens.
358
- *
359
- * @use the ink centring for a token pill — a badge, a KV value, any short
360
- * caps-or-digits label in a fixed-height box, CENTRED horizontally and with no
361
- * icon sibling. For a left-aligned sentence-case label use era-label-ink; when
362
- * an icon really must sit beside the text, see the note there. */
363
- @utility era-token-ink {
364
- display: block;
365
- align-content: center;
366
- text-align: center;
367
- line-height: 1;
368
- text-box: trim-both cap alphabetic;
369
- }
370
-
371
- /* LABEL INK — era-token-ink's mixed-case sibling, and the general answer to
372
- * "this fixed-height control holds one line of text".
373
- *
374
- * Same mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row
375
- * becomes one and `align-content` (block-container alignment) does the
376
- * centring. What differs is the band and the alignment, because the content
377
- * class differs — a label is sentence case, so the x-height band is the visual
378
- * mass to centre (cap-band reads low on "System"), and it is laid out against
379
- * the row's padding rather than centred in it.
380
- *
381
- * THAT PARENTHESIS AND era-ink-center's "x-band centring parks ~2px high" READ
382
- * AS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,
383
- * and each was generalised one step too far. Under an ex band the ink offset is
384
- *
385
- * (xh − ascent + descent) / 2
386
- *
387
- * which predicts a controlled A/B — one real button, one class set, only the
388
- * STRING changing — within 0.3px:
389
- *
390
- * Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high
391
- * Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low
392
- *
393
- * "System" has a descender and reads low under the cap band; "Save" has none
394
- * and reads high under the ex band. Case is not the variable — "Save" and
395
- * "SAVE" are identical to the hundredth of a pixel. No fixed band centres both
396
- * a descender-less and a descender-bearing label, because they differ by ~1.5px
397
- * in the same box.
398
- *
399
- * WHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.
400
- * A band gives every label the SAME baseline whatever it spells — measured
401
- * spread across Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px —
402
- * while ink-extent centring would move each label independently and leave a bar
403
- * of buttons visibly ragged. A uniform offset the eye cannot see beats a local
404
- * misalignment it can. So the offset is accepted, not corrected, and the only
405
- * escape is per-label and opt-in (Button's `caps`), which buys optical centring
406
- * for a lone label at the cost of ~1.06px against its neighbours.
407
- *
408
- * AND THE GUARANTEE SPANS BOTH INK PATHS. This utility and era-text-trim are
409
- * two mechanisms — block+align-content here, a blockified flex item there — and
410
- * every measurement above was taken on the trim path. Measured since: the
411
- * text-only path holds the same 0.00px spread AND lands on the same baseline,
412
- * 15.22px from the button top at control tier. So an icon button and a text
413
- * button put their labels on one line, and `not-has-[svg]` costs nothing but
414
- * which element carries the trim.
415
- *
416
- * Measured by signalnine.org and reproduced here; see label-band.spec.ts, which
417
- * pins the 0.00px spread, the cost of breaking it, and the agreement between
418
- * the two paths.
419
- *
420
- * WHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare
421
- * text is an ANONYMOUS flex item — a box no selector reaches and no
422
- * non-inherited property touches. So `era-text-trim` on a flex row is inert,
423
- * contributing only its `line-height: 1`, and the ink falls back to line-box
424
- * centring by font metrics. era-token-ink already fixed that for centred
425
- * tokens; this fixes it for everything else.
426
- *
427
- * THE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text
428
- * needs the box to stay flex, which re-creates the anonymous item. A component
429
- * whose children are an opaque slot therefore guards it —
430
- * `not-has-[svg]:era-label-ink` — and ink-centres the text-only case while
431
- * leaving the icon case on line-box centring. Splitting icon and label into
432
- * separate slots is what would close it; typography/inert-trim reports every
433
- * site still on the fallback.
434
- *
435
- * @use the ink centring for a fixed-height control holding ONE line of
436
- * sentence-case text and no icon — a menu item, a nav trigger, a button label.
437
- * Never alongside `truncate` (the trimmed box becomes the clip). */
438
- @utility era-label-ink {
439
- display: block;
440
- align-content: center;
441
- line-height: 1;
442
- text-box: trim-both ex alphabetic;
443
- }
444
-
445
- /* MATERIAL ROLES — components choose meaning; surfaces choose appearance. */
446
- /* @use the surface-aware backdrop treatment without choosing a fill or relief.
447
- * Prefer material-popover/material-panel when the element is a complete surface. */
448
- @utility surface-backdrop {
449
- backdrop-filter: var(--era-backdrop-filter);
450
- -webkit-backdrop-filter: var(--era-backdrop-filter);
451
- }
452
-
453
- /* @use a floating transient surface: menus, popovers, tooltips, pickers, toasts.
454
- * Binds its raised fill, floating relief, and backdrop treatment atomically. */
455
- @utility material-popover {
456
- background-color: var(--era-surface-bg-elevated);
457
- box-shadow: var(--era-shadow-lg);
458
- backdrop-filter: var(--era-backdrop-filter);
459
- -webkit-backdrop-filter: var(--era-backdrop-filter);
460
- }
461
-
462
- /* @use a floating persistent surface: dialogs, sheets, panes, inspectors.
463
- * Binds its resting fill, floating relief, and backdrop treatment atomically. */
464
- @utility material-panel {
465
- background-color: var(--era-surface-bg);
466
- box-shadow: var(--era-shadow-lg);
467
- backdrop-filter: var(--era-backdrop-filter);
468
- -webkit-backdrop-filter: var(--era-backdrop-filter);
469
- }
470
-
471
- /* @use the physical press contract on a control whose direct content plane is
472
- * marked data-era-press-content. The surface owns relief/travel; compact
473
- * components select the compact travel primitives as their effective values. */
474
- @utility pressable {
475
- &:active {
476
- box-shadow: var(--era-shadow-pressed);
477
- }
478
-
479
- &:active > [data-era-press-content] {
480
- translate: var(--era-press-effective-x, var(--era-press-sink-x))
481
- var(--era-press-effective-y, var(--era-press-sink-y));
482
- }
483
- }
484
-
485
- /* BOUNDED GEOMETRY — height and radius are one decision.
486
- *
487
- * A tier is a geometric type, not two classes a caller must remember to keep in
488
- * sync. `tier-control` establishes h-control AND rd-control; the radius ladder
489
- * then guarantees rd_outer − rd_inner = (h_outer − h_inner)/2 for every pair.
490
- * Width remains the component's decision: a row stretches, a button adds
491
- * min-w-control, and a square control adds w-control. */
492
- @utility tier-icon {
493
- height: var(--era-h-icon);
494
- border-radius: var(--era-rd-icon);
495
- }
496
-
497
- @utility tier-pill {
498
- height: var(--era-h-pill);
499
- border-radius: var(--era-rd-pill);
500
- }
501
-
502
- @utility tier-chip {
503
- height: var(--era-h-chip);
504
- border-radius: var(--era-rd-chip);
505
- }
506
-
507
- @utility tier-control {
508
- height: var(--era-h-control);
509
- border-radius: var(--era-rd-control);
510
- }
511
-
512
- @utility tier-bar {
513
- height: var(--era-h-bar);
514
- border-radius: var(--era-rd-bar);
515
- }
516
-
517
- /* One LINE BOX tall — the height of the text this stands in for.
518
- *
519
- * Skeleton's `line` used h-(--era-text), which is the body FONT SIZE: never the
520
- * height of a rendered line (that is font-size x leading), and wrong again by
521
- * the ladder's ratio for any rung other than body. `1lh` resolves against the
522
- * element's OWN computed line-height, so putting a text-* class on the skeleton
523
- * makes it match that tier exactly, at every density, and it follows a leading
524
- * change for free.
525
- *
526
- * @use the height of one line of the element's own text — a skeleton standing in
527
- * for a line, a spacer that must match a line box. */
528
- @utility h-line {
529
- height: 1lh;
530
- }
531
-
532
- /* A WELL THAT SURVIVES BEING FILLED.
533
- *
534
- * `shadow-well` is an INSET shadow, and an inset shadow belongs to the
535
- * element's background layer — it paints under the element's own descendants.
536
- * That is invisible on a well holding text (a checkbox, a code block: the
537
- * chrome runs around the content) and wrong the moment something paints the
538
- * whole box. Meter, Progress and the Slider track all do exactly that: an
539
- * opaque child, `translateX`-ed to the value. So the well survived only where
540
- * the track was EMPTY — a half-full meter on bevel had a chisel to the right
541
- * of the value and none to the left, which reads as a hole with a sticker
542
- * over it rather than a level inside a well.
543
- *
544
- * The fix is painting order, not geometry: the same `--era-shadow-well` on a
545
- * pseudo-element that is the element's LAST child, so it lands above the fill.
546
- * The fill still spans the full box — nothing is inset, no radius is
547
- * re-derived, and 0% and 100% keep their exact ends. The `position: relative`
548
- * is load-bearing (the overlay resolves against this box) and `inherit` on the
549
- * radius keeps it honest under `data-corners="square"`.
550
- *
551
- * Surface-agnostic on purpose. It reads the same token every well reads, so
552
- * glass's hairline rim is restored by the same line that restores bevel's
553
- * chisel, and flat — where the token is `none` — renders exactly as before.
554
- * Reported against bevel by a consumer; it was never a bevel bug.
555
- *
556
- * @use instead of `shadow-well` on a track whose value is painted by a child
557
- * that covers the box — a progress/meter/slider track, a custom gauge. For a
558
- * well holding ordinary content, `shadow-well` is still right. */
559
- @utility era-track-well {
560
- position: relative;
561
- &::after {
562
- content: '';
563
- position: absolute;
564
- inset: 0;
565
- border-radius: inherit;
566
- box-shadow: var(--era-shadow-well);
567
- pointer-events: none;
568
- }
569
- }
570
-
571
- /* Skeleton's pulse, on the MOTION AXIS.
572
- *
573
- * It used to be a literal `2s ease-in-out` on the component, so it neither died
574
- * at data-motion="instant" nor honoured prefers-reduced-motion — on the one
575
- * component most likely to be on screen in bulk, and animating. That is an
576
- * accessibility defect rather than a preference: a page of pulsing blocks is
577
- * exactly what someone with a vestibular disorder turns motion off to avoid.
578
- *
579
- * Same derivation as era-shimmer: 10x the axis duration, so it is a slow pulse
580
- * at normal, slower at extra, and at instant the duration resolves to 0s and it
581
- * freezes on its base frame — full opacity, a plain filled block. */
582
- @utility era-skeleton-pulse {
583
- animation: era-skeleton-pulse calc(var(--era-duration) * 10) var(--era-ease) infinite;
584
- }
585
-
586
- @keyframes era-skeleton-pulse {
587
- 0%,
588
- 100% {
589
- opacity: 1;
590
- }
591
- 50% {
592
- opacity: 0.4;
593
- }
594
- }
595
-
596
- /* In-progress text shimmer — a bright sweep through muted text (the standard
597
- * "working…" label treatment in streaming UIs). Timing derives from the motion
598
- * axis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the
599
- * duration is 0s and the sweep freezes on its base frame — plain muted text —
600
- * so reduced-motion and data-motion="instant" both kill it for free.
601
- *
602
- * @use the in-progress text treatment — put it on a "working…" / "thinking…"
603
- * label while a task streams, instead of a spinner beside the text. */
604
- @utility era-shimmer {
605
- background: linear-gradient(
606
- 110deg,
607
- var(--color-muted) 0% 43%,
608
- var(--color-bright) 50%,
609
- var(--color-muted) 57% 100%
610
- );
611
- background-size: 200% 100%;
612
- background-clip: text;
613
- -webkit-text-fill-color: transparent;
614
- animation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;
615
- }
616
- @keyframes era-shimmer {
617
- 0% {
618
- background-position: 100% 0;
619
- }
620
- 100% {
621
- background-position: -100% 0;
622
- }
623
- }
624
-
625
- /* The open/close animation for a bits-ui Collapsible.Content — the height
626
- * sweep every disclosure in the library uses (a Step's body, an error panel's
627
- * detail, a swarm node's children).
628
- *
629
- * It has to be a keyframe animation rather than a transition because the
630
- * natural height is only knowable at open time: bits measures the content and
631
- * publishes `--bits-collapsible-content-height`, which the `to` frame reads.
632
- * The class also owns `overflow: hidden` — animating height without it just
633
- * spills the content out of the shrinking box.
634
- *
635
- * Timing rides the motion axis, so `instant` collapses it to a cut (0s) and
636
- * `extra` stretches it, for free. Two components carried a byte-identical copy of
637
- * these keyframes under two different names before this existed (a Step's body
638
- * and the error panel's detail), and the swarm tree would have made a third; the
639
- * panel that needs different motion should override `animation`, not re-declare
640
- * it.
641
- *
642
- * @use the height sweep for a bits-ui Collapsible.Content — put it on the
643
- * Content element itself (it carries its own overflow: hidden). */
644
- @utility era-collapse {
645
- overflow: hidden;
646
- &[data-state='open'] {
647
- animation: era-collapse-open var(--era-duration) var(--era-ease);
648
- }
649
- &[data-state='closed'] {
650
- animation: era-collapse-close var(--era-duration) var(--era-ease);
651
- }
652
- }
653
- @keyframes era-collapse-open {
654
- from {
655
- height: 0;
656
- opacity: 0;
657
- }
658
- to {
659
- height: var(--bits-collapsible-content-height);
660
- opacity: 1;
661
- }
662
- }
663
- @keyframes era-collapse-close {
664
- from {
665
- height: var(--bits-collapsible-content-height);
666
- opacity: 1;
667
- }
668
- to {
669
- height: 0;
670
- opacity: 0;
671
- }
672
- }
673
-
674
- /* ------------------------------------------------------------------ */
675
- /* THE PUBLIC CLASS API */
676
- /* */
677
- /* Era's consumer-facing styling surface is NAMED utilities — a user */
678
- /* of the library writes `tier-control px-gutter shadow`, never */
679
- /* `h-(--era-h-control)`-style var classes. Where Tailwind already has a */
680
- /* name for the concept (rounded-control, shadow, ease) era OVERLOADS it */
681
- /* so stock-looking markup renders on-design; where it doesn't, era */
682
- /* registers its own (h-control, px-gutter, gap-gutter, p-card). */
683
- /* */
684
- /* The --era-* custom properties remain the internal contract (and */
685
- /* the escape hatch for anything without a name — e.g. min-w-* tier */
686
- /* names are NOT registered because Tailwind's container scale */
687
- /* already owns min-w-md/lg; use min-w-(--era-h-control) there). */
688
- /* */
689
- /* Everything here is a thin name over a token, so the full axis */
690
- /* system — density, surface, corners, motion — flows through */
691
- /* untouched. @theme inline is load-bearing: it inlines the var() */
692
- /* reference instead of snapshotting a value, so each USE re-resolves */
693
- /* against the element's own inherited axes. */
694
- /* ------------------------------------------------------------------ */
695
- @theme inline {
696
- /* THE TYPE LADDER — the one axis that was not on the density system.
697
- *
698
- * era had exactly one size, `text-body`, so a heading had nowhere to go and
699
- * consumers reached for Tailwind's fixed `text-2xl` or a bare <h1>. Both are
700
- * rem-fixed while `--era-text` re-tiers, so heading prominence DEGRADED as
701
- * density increased: measured in a consumer app, an <h1> against body text
702
- * was 1.33x at dense and 1.14x at touch. The page title gets quieter the
703
- * tighter you set the page, which is backwards, and it is the only place in
704
- * era where a size relationship does not hold across the axis.
705
- *
706
- * Named by ROLE, like the tier ladder and for the same reason: size names
707
- * invert (this library already shipped an `xxs` bigger than its `xs`), and a
708
- * role tells you where the rung belongs. Ratios are simple fractions of the
709
- * atom rather than a geometric scale — again like the tier ladder, whose
710
- * rungs are sp+6, 2sp+6, 3sp, 4sp rather than a constant multiplier. They
711
- * land on whole pixels at dense and hold their ratio at every mode.
712
- *
713
- * micro 5/6 10 / 11.7 / 11.7 / 11.7 an eyebrow, a legend, a tag
714
- * body 1 12 / 14 / 14 / 14 control and UI text
715
- * lede 5/4 15 / 17.5 the paragraph under a title
716
- * heading 3/2 18 / 21 a section heading
717
- * title 2 24 / 28 a page or card title
718
- * display 3 36 / 42 a hero
719
- *
720
- * `micro` is not invented: the library hand-writes text-[0.625rem] ten times
721
- * for exactly this role, and 5/6 of dense's 12px is that value.
722
- *
723
- * Leading and tracking ride with the size, so a rung is one class. Both
724
- * tighten as the rung grows, which is ordinary optical setting and matters
725
- * more here than usual — era's default stack is monospace, whose display
726
- * sizes read loose without it.
727
- *
728
- * @use the type ladder — micro (eyebrow/legend) < body (controls) < lede
729
- * (the paragraph under a title) < heading < title < display. Derived from
730
- * --era-text, so the ratio between any two rungs holds at every density;
731
- * leading and tracking ride with the size, so a rung is one class.
732
- *
733
- * body and micro deliberately carry NO line-height: `text-body` is worn by
734
- * every control in the library, where leading is owned by era-text-trim and
735
- * leading-none. Giving it a default would re-space every control in era.
736
- * Use `leading-body` for running text, as before. */
737
- --text-micro: calc(var(--era-text) * 5 / 6);
738
- --text-micro--letter-spacing: 0.04em;
739
- --text-lede: calc(var(--era-text) * 5 / 4);
740
- --text-lede--line-height: 1.5;
741
- --text-heading: calc(var(--era-text) * 3 / 2);
742
- --text-heading--line-height: 1.3;
743
- --text-heading--letter-spacing: -0.01em;
744
- --text-title: calc(var(--era-text) * 2);
745
- --text-title--line-height: 1.15;
746
- --text-title--letter-spacing: -0.02em;
747
- --text-display: calc(var(--era-text) * 3);
748
- --text-display--line-height: 1.05;
749
- --text-display--letter-spacing: -0.03em;
750
- /* Density-derived body size: `text-body` replaces text-[length:var(--era-text)] */
751
- --text-body: var(--era-text);
752
- /* Prose leading — `leading-body` is the pair to `text-body`, and every piece
753
- * of text in the library that can wrap to a second line carries it. Enforced
754
- * at runtime by typography/prose-leading. */
755
- --leading-body: var(--era-leading);
756
- /* Radius ladder — Tailwind's own rounded-* names now walk era's concentric
757
- * ladder (rd-T = rd-xs + (h-T − h-icon)/2), ride the corners toggle, and
758
- * collapse to 0 on square. rounded-menu-item is the menu-row radius
759
- * (rd-md − panel-inset) that keeps rows concentric inside a rounded-control
760
- * panel on every surface. */
761
- --radius-icon: var(--era-rd-icon);
762
- --radius-pill: var(--era-rd-pill);
763
- --radius-chip: var(--era-rd-chip);
764
- --radius-control: var(--era-rd-control);
765
- --radius-bar: var(--era-rd-bar);
766
- --radius-menu-item: var(--era-item-rd);
767
- /* Surface chrome — bare `shadow` is the resting control edge, shadow-lg the
768
- * floating panel, shadow-well the recessed container, shadow-pressed the
769
- * active/latched face, shadow-highlight the material half of hover feedback.
770
- * Flat renders them as none, bevel as chisels, glass as frosted rims — the
771
- * class stays the same. */
772
- --shadow: var(--era-shadow);
773
- /* v4 marks bare `shadow` a deprecated spelling of shadow-sm, and lints
774
- * auto-fix one to the other — alias both to the control edge so either
775
- * spelling renders era chrome. */
776
- --shadow-sm: var(--era-shadow);
777
- --shadow-lg: var(--era-shadow-lg);
778
- --shadow-well: var(--era-shadow-well);
779
- --shadow-pressed: var(--era-shadow-pressed);
780
- --shadow-highlight: var(--era-highlight-shadow);
781
- /* Surface fills — bg-well is the surface's own resting fill (pairs with
782
- * shadow-well for recessed chrome), bg-elevated the raised tier,
783
- * bg-highlight the transient
784
- * hover/focus/keyboard wash (pairs with shadow-highlight; latched states
785
- * stay on opaque bg-hover), bg-overlay the modal scrim. */
786
- --color-well: var(--era-surface-bg);
787
- --color-elevated: var(--era-surface-bg-elevated);
788
- --color-highlight: var(--era-highlight);
789
- --color-overlay: var(--era-overlay-bg);
790
- /* THE SPACING SCALE — one entry per token, from which Tailwind derives the
791
- * WHOLE directional family: p/px/py/pt/pr/pb/pl/ps/pe, every margin, gap and
792
- * gap-x/y, inset/top/right/bottom/left, translate, and the sizing prefixes.
793
- *
794
- * These were hand-written `@utility` blocks, which could only ever cover the
795
- * spellings someone remembered to write: `px-gutter` existed and `mt-gutter`
796
- * did not, `p-inset-control` existed and `pl-inset-control` did not, so the
797
- * library fell back to `mt-(--era-gap)` in exactly the places the named API
798
- * was supposed to cover. One line here replaces twenty blocks and leaves no
799
- * spelling missing.
800
- *
801
- * ONLY genuine spacing belongs here. The namespace is shared across
802
- * h/w/p/m/gap, so a name registered here means the same length in ALL of
803
- * them — true of a gap or an inset, false of a TIER HEIGHT. Tiers stay
804
- * hand-written per prefix below, and the two vocabularies deliberately share
805
- * no word: icon/pill/chip/control/bar are heights; gutter, card, panel,
806
- * content, menu, field and the inset-* names are spacing. Registering a tier
807
- * name here would silently make `px-control` mean 24px of side padding. */
808
- --spacing-gutter: var(--era-gap);
809
- --spacing-panel: var(--era-pad-panel);
810
- --spacing-card: var(--era-pad-card);
811
- --spacing-content: var(--era-pad-content);
812
- --spacing-menu: var(--era-panel-inset);
813
- --spacing-field: var(--era-field-px);
814
- /* @use text-symmetric content padding — (tier − text)/2, the inset that
815
- * vertically centres body text in its tier; px-inset-control is a text
816
- * button's side padding, p-inset-control a code block's. */
817
- --spacing-inset-pill: var(--era-inset-pill);
818
- --spacing-inset-chip: var(--era-inset-chip);
819
- --spacing-inset-control: var(--era-inset-control);
820
- /* @use the concentric inset for a nested pair — (h-container − h-child)/2,
821
- * the gap that makes a child control sit evenly inside its container, and
822
- * the radius step that keeps their corners concentric. Named for the pair it
823
- * joins: gap-icon-inset-control seats an icon in a control. */
824
- --spacing-icon-inset-pill: var(--era-icon-inset-pill);
825
- --spacing-icon-inset-chip: var(--era-icon-inset-chip);
826
- --spacing-icon-inset-control: var(--era-icon-inset-control);
827
- --spacing-icon-inset-bar: var(--era-icon-inset-bar);
828
- --spacing-pill-inset-chip: var(--era-pill-inset-chip);
829
- --spacing-pill-inset-control: var(--era-pill-inset-control);
830
- --spacing-pill-inset-bar: var(--era-pill-inset-bar);
831
- --spacing-chip-inset-control: var(--era-chip-inset-control);
832
- --spacing-chip-inset-bar: var(--era-chip-inset-bar);
833
- --spacing-control-inset-bar: var(--era-control-inset-bar);
834
- /* @use the glyph size for a tier — the INK inside the box, not the box
835
- * (size-glyph-pill is the dismiss × in a pill, size-pill is the pill). */
836
- --spacing-glyph-icon: var(--era-glyph-icon);
837
- --spacing-glyph-pill: var(--era-glyph-pill);
838
- /* @use the recessed field fill — bg-field is a text input's resting well,
839
- * bg-field-highlight its focused/hovered face. */
840
- --color-field: var(--era-field-bg);
841
- --color-field-highlight: var(--era-field-highlight);
842
- /* @use the stacking tier for chrome that renders at the document root —
843
- * z-chrome (taskbar) < z-drawer (slide-over) < z-overlay (every dialog,
844
- * menu, popover and scrim) < z-toast < z-debug. A component ordering its
845
- * OWN parts inside an isolated context keeps small integers instead; see
846
- * styles/layers.css for why the two are different things. */
847
- --z-index-chrome: var(--era-z-chrome);
848
- --z-index-drawer: var(--era-z-drawer);
849
- --z-index-overlay: var(--era-z-overlay);
850
- --z-index-toast: var(--era-z-toast);
851
- --z-index-debug: var(--era-z-debug);
852
- /* Motion — bare transition utilities default to the motion axis, so
853
- * `transition-colors` alone is on-design (0s at instant, longer at extra).
854
- * ease-base/duration-base name the axis explicitly for overrides. */
855
- --default-transition-duration: var(--era-duration);
856
- --default-transition-timing-function: var(--era-ease);
857
- --ease-base: var(--era-ease);
858
- /* Surface divider colors: border-divider, bg-divider-faded, … */
859
- --color-divider: var(--era-divider-color);
860
- --color-divider-faded: var(--era-divider-faded);
861
- --color-background: var(--color-1);
862
- --color-foreground: var(--color-fg);
863
- --color-1: var(--color-1);
864
- --color-2: var(--color-2);
865
- --color-3: var(--color-3);
866
- --color-4: var(--color-4);
867
- --color-5: var(--color-5);
868
- --color-6: var(--color-6);
869
- --color-7: var(--color-7);
870
- --color-8: var(--color-8);
871
- --color-9: var(--color-9);
872
- --color-10: var(--color-10);
873
- --color-11: var(--color-11);
874
- --color-12: var(--color-12);
875
- --color-border: var(--color-border);
876
- --color-surface: var(--color-surface);
877
- --color-hover: var(--color-hover);
878
- --color-fg: var(--color-fg);
879
- --color-bright: var(--color-bright);
880
- --color-muted: var(--color-muted);
881
- /* The standing pill fill — --era-fill, the latched counterpart of the
882
- * transient highlight. Surface-owned: a composing wash on flat/bevel, the
883
- * opaque swatch on glass. */
884
- --color-fill: var(--era-fill);
885
- --color-link: var(--color-link);
886
- --color-primary: var(--color-primary);
887
- --color-primary-fg: var(--color-primary-fg);
888
- --color-primary-hover: var(--color-primary-hover);
889
- --color-destructive: var(--color-destructive);
890
- --color-destructive-fg: var(--color-destructive-fg);
891
- --color-success: var(--color-success);
892
- --color-success-fg: var(--color-success-fg);
893
- --color-warning: var(--color-warning);
894
- --color-warning-fg: var(--color-warning-fg);
895
- --color-info: var(--color-info);
896
- --color-info-fg: var(--color-info-fg);
897
- }
898
-
899
- /* ------------------------------------------------------------------ */
900
- /* Named tier + spacing utilities */
901
- /* */
902
- /* What is left here is what the SPACING SCALE above cannot express: */
903
- /* a tier is a HEIGHT, and the spacing namespace is shared across */
904
- /* h/w/p/m/gap, so registering `control` there would silently make */
905
- /* px-control mean 24px of side padding. Tier names therefore get one */
906
- /* hand-written block per prefix that needs them, and the two */
907
- /* vocabularies share no word. Kept terse; the ladder itself is */
908
- /* documented in density.css and on the /spacing page. */
909
- /* ------------------------------------------------------------------ */
910
-
911
- /* @use the tier-height ladder — h-icon (icon) · h-pill (nested pill) · h-chip
912
- * (standalone pill) · h-control (control) · h-bar (bar); the named form of
913
- * h-(--era-h-*). */
914
- @utility h-icon {
915
- height: var(--era-h-icon);
916
- }
917
- @utility h-pill {
918
- height: var(--era-h-pill);
919
- }
920
- @utility h-chip {
921
- height: var(--era-h-chip);
922
- }
923
- @utility h-control {
924
- height: var(--era-h-control);
925
- }
926
- @utility h-bar {
927
- height: var(--era-h-bar);
928
- }
929
-
930
- /* @use minimum tier height for containers that may grow — a bar that is at
931
- * least lg-tier but can wrap (min-h-bar). */
932
- @utility min-h-icon {
933
- min-height: var(--era-h-icon);
934
- }
935
- @utility min-h-pill {
936
- min-height: var(--era-h-pill);
937
- }
938
- @utility min-h-chip {
939
- min-height: var(--era-h-chip);
940
- }
941
- @utility min-h-control {
942
- min-height: var(--era-h-control);
943
- }
944
- @utility min-h-bar {
945
- min-height: var(--era-h-bar);
946
- }
947
-
948
- /* @use a tier-sized WIDTH — the horizontal twin of the h-* ladder, for a rail or
949
- * a column measured in tiers rather than in content. */
950
- @utility w-icon {
951
- width: var(--era-h-icon);
952
- }
953
- @utility w-pill {
954
- width: var(--era-h-pill);
955
- }
956
- @utility w-chip {
957
- width: var(--era-h-chip);
958
- }
959
- @utility w-control {
960
- width: var(--era-h-control);
961
- }
962
- @utility w-bar {
963
- width: var(--era-h-bar);
964
- }
965
-
966
- /* @use a minimum tier WIDTH — the floor that makes a single-glyph control render
967
- * as a square rather than a skinny pill (min-w-control on an icon button,
968
- * min-w-pill on a badge). These could not exist before the ladder was renamed:
969
- * the old rungs were sm/md/lg, and Tailwind's container scale already owns
970
- * min-w-md and min-w-lg, so the names collided and every call site had to write
971
- * min-w-(--era-h-control) by hand. icon/pill/chip/control/bar collide with nothing. */
972
- @utility min-w-icon {
973
- min-width: var(--era-h-icon);
974
- }
975
- @utility min-w-pill {
976
- min-width: var(--era-h-pill);
977
- }
978
- @utility min-w-chip {
979
- min-width: var(--era-h-chip);
980
- }
981
- @utility min-w-control {
982
- min-width: var(--era-h-control);
983
- }
984
- @utility min-w-bar {
985
- min-width: var(--era-h-bar);
986
- }
987
-
988
- /* @use a tier-sized square — size-icon is THE icon size (a Lucide glyph beside
989
- * body text), size-control a square control face. */
990
- @utility size-icon {
991
- width: var(--era-h-icon);
992
- height: var(--era-h-icon);
993
- }
994
- @utility size-pill {
995
- width: var(--era-h-pill);
996
- height: var(--era-h-pill);
997
- }
998
- @utility size-chip {
999
- width: var(--era-h-chip);
1000
- height: var(--era-h-chip);
1001
- }
1002
- @utility size-control {
1003
- width: var(--era-h-control);
1004
- height: var(--era-h-control);
1005
- }
1006
- @utility size-bar {
1007
- width: var(--era-h-bar);
1008
- height: var(--era-h-bar);
1009
- }
1010
-
1011
- /* @use pill side padding — the wider-than-vertical horizontal breathing room
1012
- * badge/chip text wants (glyph-advance derived, ~2× the vertical inset). */
1013
- @utility px-pill {
1014
- padding-inline: var(--era-pill-px);
1015
- }
1016
- @utility px-chip {
1017
- padding-inline: var(--era-chip-px);
1018
- }
1019
- @utility pl-pill {
1020
- padding-left: var(--era-pill-px);
1021
- }
1022
- @utility pr-pill {
1023
- padding-right: var(--era-pill-px);
1024
- }
1025
- @utility pl-chip {
1026
- padding-left: var(--era-chip-px);
1027
- }
1028
- @utility pr-chip {
1029
- padding-right: var(--era-chip-px);
1030
- }
1031
-
1032
- /* @use padding for a content panel that sits UNDER a section header row — the
1033
- * even-padding exception, and the reason it is not called p-section: it
1034
- * deliberately under-pays the block start. For an evenly padded content panel
1035
- * use p-section, which is the plain token.
1036
- *
1037
- * A content panel always follows a section header ROW, and that row already
1038
- * contributes its own slack: the heading's text sits in an h-bar bar, so there
1039
- * is (h-bar − text)/2 of empty bar under its glyphs before the panel even
1040
- * starts. Padding the panel evenly on top of that spends the space twice — the
1041
- * header ends up floating between the section above and the paragraph it
1042
- * introduces rather than binding to it.
1043
- *
1044
- * So the block-start pays only the REMAINDER. The target is the indent: the
1045
- * space under a heading should measure the same as the space beside the text,
1046
- * which is --era-pad-content. The header row already supplies (h-bar − text)/2 of
1047
- * it, so the panel adds what's left, and heading-to-paragraph reads equal to
1048
- * the paragraph's own inset. Subtracting one --era-gap (the previous rule) left
1049
- * the header's slack unaccounted for and the heading still detached.
1050
- *
1051
- * Both terms are density-derived, so the equality holds at every mode; the
1052
- * max() is the floor for a hypothetical mode where the row's slack would
1053
- * exceed the inset outright. */
1054
- @utility p-section {
1055
- padding: var(--era-pad-content);
1056
- padding-top: max(0px, calc(var(--era-pad-content) - (var(--era-h-bar) - var(--era-text)) / 2));
1057
- }
1058
-
1059
- /* @use the motion axis as an explicit transition-duration — for the rare case
1060
- * a utility (not the transition-* default) needs to name it; 0s at
1061
- * data-motion="instant", longer at "extra". */
1062
- @utility duration-base {
1063
- transition-duration: var(--era-duration);
1064
- }