@ps1ui/core 0.0.0 → 0.1.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/base.css +378 -0
  4. package/dist/components/Anchor/Anchor.d.mts +14 -0
  5. package/dist/components/Anchor/Anchor.mjs +15 -0
  6. package/dist/components/Anchor/Anchor.mjs.map +1 -0
  7. package/dist/components/Button/Button.d.mts +30 -0
  8. package/dist/components/Button/Button.mjs +17 -0
  9. package/dist/components/Button/Button.mjs.map +1 -0
  10. package/dist/components/Card/Card.d.mts +7 -0
  11. package/dist/components/Card/Card.mjs +14 -0
  12. package/dist/components/Card/Card.mjs.map +1 -0
  13. package/dist/components/Checkbox/Checkbox.d.mts +10 -0
  14. package/dist/components/Checkbox/Checkbox.mjs +23 -0
  15. package/dist/components/Checkbox/Checkbox.mjs.map +1 -0
  16. package/dist/components/Code/Code.d.mts +7 -0
  17. package/dist/components/Code/Code.mjs +14 -0
  18. package/dist/components/Code/Code.mjs.map +1 -0
  19. package/dist/components/CodeBlock/CodeBlock.d.mts +15 -0
  20. package/dist/components/CodeBlock/CodeBlock.mjs +44 -0
  21. package/dist/components/CodeBlock/CodeBlock.mjs.map +1 -0
  22. package/dist/components/CodeBlock/refractor.d.mts +7 -0
  23. package/dist/components/CodeBlock/refractor.mjs +26 -0
  24. package/dist/components/CodeBlock/refractor.mjs.map +1 -0
  25. package/dist/components/Container/Container.d.mts +21 -0
  26. package/dist/components/Container/Container.mjs +24 -0
  27. package/dist/components/Container/Container.mjs.map +1 -0
  28. package/dist/components/Details/Details.d.mts +10 -0
  29. package/dist/components/Details/Details.mjs +20 -0
  30. package/dist/components/Details/Details.mjs.map +1 -0
  31. package/dist/components/Grid/Grid.d.mts +21 -0
  32. package/dist/components/Grid/Grid.mjs +24 -0
  33. package/dist/components/Grid/Grid.mjs.map +1 -0
  34. package/dist/components/GridItem/GridItem.d.mts +14 -0
  35. package/dist/components/GridItem/GridItem.mjs +21 -0
  36. package/dist/components/GridItem/GridItem.mjs.map +1 -0
  37. package/dist/components/Heading/Heading.d.mts +26 -0
  38. package/dist/components/Heading/Heading.mjs +53 -0
  39. package/dist/components/Heading/Heading.mjs.map +1 -0
  40. package/dist/components/Input/Input.d.mts +7 -0
  41. package/dist/components/Input/Input.mjs +15 -0
  42. package/dist/components/Input/Input.mjs.map +1 -0
  43. package/dist/components/Label/Label.d.mts +7 -0
  44. package/dist/components/Label/Label.mjs +14 -0
  45. package/dist/components/Label/Label.mjs.map +1 -0
  46. package/dist/components/List/List.d.mts +13 -0
  47. package/dist/components/List/List.mjs +15 -0
  48. package/dist/components/List/List.mjs.map +1 -0
  49. package/dist/components/ListItem/ListItem.d.mts +7 -0
  50. package/dist/components/ListItem/ListItem.mjs +13 -0
  51. package/dist/components/ListItem/ListItem.mjs.map +1 -0
  52. package/dist/components/PS1Root/PS1Root.d.mts +7 -0
  53. package/dist/components/PS1Root/PS1Root.mjs +13 -0
  54. package/dist/components/PS1Root/PS1Root.mjs.map +1 -0
  55. package/dist/components/Stack/Stack.d.mts +33 -0
  56. package/dist/components/Stack/Stack.mjs +48 -0
  57. package/dist/components/Stack/Stack.mjs.map +1 -0
  58. package/dist/components/Table/Table.d.mts +7 -0
  59. package/dist/components/Table/Table.mjs +25 -0
  60. package/dist/components/Table/Table.mjs.map +1 -0
  61. package/dist/components/Tbody/Tbody.d.mts +7 -0
  62. package/dist/components/Tbody/Tbody.mjs +13 -0
  63. package/dist/components/Tbody/Tbody.mjs.map +1 -0
  64. package/dist/components/Td/Td.d.mts +7 -0
  65. package/dist/components/Td/Td.mjs +13 -0
  66. package/dist/components/Td/Td.mjs.map +1 -0
  67. package/dist/components/Text/Text.d.mts +31 -0
  68. package/dist/components/Text/Text.mjs +34 -0
  69. package/dist/components/Text/Text.mjs.map +1 -0
  70. package/dist/components/Th/Th.d.mts +7 -0
  71. package/dist/components/Th/Th.mjs +13 -0
  72. package/dist/components/Th/Th.mjs.map +1 -0
  73. package/dist/components/Thead/Thead.d.mts +7 -0
  74. package/dist/components/Thead/Thead.mjs +13 -0
  75. package/dist/components/Thead/Thead.mjs.map +1 -0
  76. package/dist/components/Tr/Tr.d.mts +7 -0
  77. package/dist/components/Tr/Tr.mjs +13 -0
  78. package/dist/components/Tr/Tr.mjs.map +1 -0
  79. package/dist/components.css +1870 -0
  80. package/dist/index.d.mts +28 -0
  81. package/dist/index.mjs +25 -0
  82. package/dist/styles.css +1930 -0
  83. package/dist/utils/cx.mjs +8 -0
  84. package/dist/utils/cx.mjs.map +1 -0
  85. package/dist/utils/numbers.mjs +8 -0
  86. package/dist/utils/numbers.mjs.map +1 -0
  87. package/dist/utils/responsive.d.mts +6 -0
  88. package/dist/utils/responsive.mjs +39 -0
  89. package/dist/utils/responsive.mjs.map +1 -0
  90. package/dist/utils/spacing.d.mts +5 -0
  91. package/dist/utils/spacing.mjs +8 -0
  92. package/dist/utils/spacing.mjs.map +1 -0
  93. package/dist/utils/typography.d.mts +5 -0
  94. package/dist/utils/typography.mjs +16 -0
  95. package/dist/utils/typography.mjs.map +1 -0
  96. package/dist/utils/useMergedRef.mjs +30 -0
  97. package/dist/utils/useMergedRef.mjs.map +1 -0
  98. package/dist/utils/useScrollableFocus.mjs +29 -0
  99. package/dist/utils/useScrollableFocus.mjs.map +1 -0
  100. package/package.json +84 -3
@@ -0,0 +1,1930 @@
1
+ @import "@fontsource-variable/jetbrains-mono";
2
+ /* Palette hexes are mirrored in .storybook/ps1uiTheme.ts — see note there. */
3
+ :root {
4
+ --ps1ui-font-mono:
5
+ "JetBrains Mono Variable", ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", Consolas,
6
+ monospace;
7
+
8
+ --ps1ui-color-bg: #0b0f14;
9
+ --ps1ui-color-surface: #0f1520;
10
+ --ps1ui-color-border: #21262d;
11
+ --ps1ui-color-border-strong: #30363d;
12
+ --ps1ui-color-fg: #c7d5df;
13
+ --ps1ui-color-fg-muted: #8b98a5;
14
+ /* fg-subtle must maintain ≥ 4.5:1 (WCAG AA) vs BOTH --ps1ui-color-bg and --ps1ui-color-surface.
15
+ Surface is the tighter case; verified by Text.contrast.test.tsx. */
16
+ --ps1ui-color-fg-subtle: #7a8593;
17
+ --ps1ui-color-primary: #7ee787;
18
+ --ps1ui-color-primary-hover: #56d364;
19
+ --ps1ui-color-primary-active: #3fb950;
20
+ --ps1ui-color-primary-fg: #0b0f14;
21
+ --ps1ui-color-accent: #ffa657;
22
+ --ps1ui-color-focus-ring-soft: color-mix(in srgb, var(--ps1ui-color-primary) 14%, transparent);
23
+ --ps1ui-shadow-focus-ring: 0 0 0 3px var(--ps1ui-color-focus-ring-soft);
24
+
25
+ /* Syntax-highlight palette for CodeBlock. Every value is verified ≥ 4.5:1 vs
26
+ BOTH --ps1ui-color-bg and --ps1ui-color-surface by CodeBlock.contrast.test.tsx. */
27
+ --ps1ui-code-comment: #7a8593;
28
+ --ps1ui-code-string: #f0c880;
29
+ --ps1ui-code-keyword: #ff7b95;
30
+ --ps1ui-code-function: #79c0ff;
31
+ --ps1ui-code-constant: #ffa657;
32
+ --ps1ui-code-type: #5eead4;
33
+ --ps1ui-code-tag: #ff9587;
34
+ --ps1ui-code-attr: #ffdc99;
35
+ --ps1ui-code-punctuation: #7a8593;
36
+ /* Diff-specific: green = added, rose = removed. Kept separate from tag/string so
37
+ the semantic overlap doesn't leak between grammars (a removed diff line has
38
+ nothing to do with an HTML <tag>, and an added line isn't a string literal). */
39
+ --ps1ui-code-inserted: #7ee787;
40
+ --ps1ui-code-deleted: #ff8896;
41
+
42
+ --ps1ui-radius-md: 3px;
43
+ --ps1ui-radius-lg: 4px;
44
+
45
+ --ps1ui-space-xs: 4px;
46
+ --ps1ui-space-sm: 8px;
47
+ --ps1ui-space-md: 12px;
48
+ --ps1ui-space-lg: 16px;
49
+ --ps1ui-space-xl: 24px;
50
+ --ps1ui-space-2xl: 32px;
51
+
52
+ /* Vertical padding for 1px-bordered controls (Button, Input): border +
53
+ padding = --ps1ui-space-sm (8px), so this is a derived value, not a new
54
+ spacing step. */
55
+ --ps1ui-control-padding-y: 7px;
56
+
57
+ /* Container max-widths, in rem so they scale with the user's browser
58
+ font-size preference (WCAG-friendly). Numeric equivalents at the
59
+ browser-default 16px root: 40 = 640, 48 = 768, 64 = 1024, 80 = 1280.
60
+ Doubles as the breakpoint scale — see --ps1ui-breakpoint-* below. */
61
+ --ps1ui-container-sm: 40rem;
62
+ --ps1ui-container-md: 48rem;
63
+ --ps1ui-container-lg: 64rem;
64
+ --ps1ui-container-xl: 80rem;
65
+
66
+ /* Responsive breakpoints, in rem for the same accessibility reason.
67
+ Values are identical to --ps1ui-container-* by design: a Container
68
+ of size=lg (64rem) fills at exactly the `lg` breakpoint. These are
69
+ documentation tokens — @container / @media conditions cannot use
70
+ `var()` per the CSS spec, so the same numeric values are also
71
+ hardcoded in each component's CSS. Keep in sync. */
72
+ --ps1ui-breakpoint-sm: 40rem;
73
+ --ps1ui-breakpoint-md: 48rem;
74
+ --ps1ui-breakpoint-lg: 64rem;
75
+ --ps1ui-breakpoint-xl: 80rem;
76
+
77
+ /* Type scale, in rem so text tracks the user's browser font-size preference
78
+ (same rationale as the container/breakpoint tokens above). All values sit
79
+ on the 0.125rem (2px) grid; px equivalents at the default 16px root:
80
+ 12 / 14 / 16 / 18 / 22 / 26 / 32. */
81
+ --ps1ui-font-size-xs: 0.75rem;
82
+ --ps1ui-font-size-sm: 0.875rem;
83
+ --ps1ui-font-size-md: 1rem;
84
+ --ps1ui-font-size-lg: 1.125rem;
85
+ --ps1ui-font-size-xl: 1.375rem;
86
+ --ps1ui-font-size-2xl: 1.625rem;
87
+ --ps1ui-font-size-3xl: 2rem;
88
+
89
+ --ps1ui-transition-fast: 120ms ease;
90
+ }
91
+ /* ==========================================================================
92
+ * Reset
93
+ * --------------------------------------------------------------------------
94
+ * Included in base.css (and therefore in styles.css) so consumers who commit
95
+ * fully to the PS1 UI look get a consistent baseline across browsers.
96
+ *
97
+ * NEVER duplicate any of these rules in components.css — that entry is for
98
+ * consumers embedding PS1 UI into a foreign design system, where a global
99
+ * reset would collide with the host system. The invariant is enforced by
100
+ * scripts/check-css-split.mjs at build time.
101
+ *
102
+ * Consumer heads-up: raw semantic tags rendered outside PS1 UI's own
103
+ * components lose their UA appearance under this reset — a bare <a> paints
104
+ * with inherited color/no underline (route through <Anchor>), a bare <h1>
105
+ * collapses to body text (route through <Heading>), a bare <button> has no
106
+ * chrome (route through <Button>), <img> becomes block (breaks inline avatar
107
+ * layouts unless you set display:inline explicitly), <ul>/<ol> lose their
108
+ * bullets. Locked in by src/styles/reset.test.tsx.
109
+ * ========================================================================== */
110
+ /* Box model + border baseline. Applied to `*` only — NOT to ::before/::after
111
+ /::backdrop/::file-selector-button. Pseudo-elements with border-drawn
112
+ glyphs (the classic rotated-L checkmark, tooltip carets, radio-dot rings)
113
+ need the initial `box-sizing: content-box` so their declared width/height
114
+ sit INSIDE the border. Josh Comeau's reset excludes pseudos for this
115
+ exact reason; Tailwind Preflight includes them but then has to document
116
+ the "authors doing border tricks must override" caveat. We pick the
117
+ Comeau side because ps1ui components (Checkbox) already use the trick.
118
+ Locked in by reset.test.tsx's "reset does not touch pseudos" block. */
119
+ * {
120
+ box-sizing: border-box;
121
+ border: 0 solid;
122
+ }
123
+ body,
124
+ blockquote,
125
+ dl,
126
+ dd,
127
+ figure,
128
+ fieldset,
129
+ h1,
130
+ h2,
131
+ h3,
132
+ h4,
133
+ h5,
134
+ h6,
135
+ hr,
136
+ menu,
137
+ ol,
138
+ p,
139
+ pre,
140
+ ul {
141
+ margin: 0;
142
+ }
143
+ fieldset,
144
+ legend,
145
+ menu,
146
+ ol,
147
+ ul {
148
+ padding: 0;
149
+ }
150
+ html {
151
+ color-scheme: dark;
152
+ line-height: 1.5;
153
+ -webkit-text-size-adjust: 100%;
154
+ -webkit-tap-highlight-color: transparent;
155
+ -moz-tab-size: 4;
156
+ -o-tab-size: 4;
157
+ tab-size: 4;
158
+ text-rendering: optimizeLegibility;
159
+ }
160
+ body {
161
+ min-height: 100dvh;
162
+ -webkit-font-smoothing: antialiased;
163
+ -moz-osx-font-smoothing: grayscale;
164
+ }
165
+ /* Heading responsibility belongs to <Heading>; raw h1..h6 inherit so a bare
166
+ <h1> outside <Heading> doesn't blow up to the UA default. */
167
+ h1,
168
+ h2,
169
+ h3,
170
+ h4,
171
+ h5,
172
+ h6 {
173
+ font-size: inherit;
174
+ font-weight: inherit;
175
+ text-wrap: balance;
176
+ overflow-wrap: break-word;
177
+ }
178
+ p {
179
+ text-wrap: pretty;
180
+ overflow-wrap: break-word;
181
+ }
182
+ ol,
183
+ ul,
184
+ menu {
185
+ list-style: none;
186
+ }
187
+ img,
188
+ svg,
189
+ video,
190
+ canvas,
191
+ audio,
192
+ iframe,
193
+ embed,
194
+ object {
195
+ display: block;
196
+ vertical-align: middle;
197
+ }
198
+ img,
199
+ video {
200
+ max-width: 100%;
201
+ height: auto;
202
+ }
203
+ /* Anchor responsibility belongs to <Anchor>; raw <a> inherits color/decoration
204
+ so it doesn't paint as UA blue underline against the dark canvas. */
205
+ a {
206
+ color: inherit;
207
+ text-decoration: inherit;
208
+ }
209
+ /* UA still refuses to inherit font/color into form controls in 2026.
210
+ Inherit font-feature-settings / font-variation-settings too so JetBrains
211
+ Mono Variable's axes stay live inside <input> / <textarea>. */
212
+ button,
213
+ input,
214
+ optgroup,
215
+ select,
216
+ textarea,
217
+ ::file-selector-button {
218
+ font: inherit;
219
+ font-feature-settings: inherit;
220
+ font-variation-settings: inherit;
221
+ letter-spacing: inherit;
222
+ color: inherit;
223
+ background-color: transparent;
224
+ border-radius: 0;
225
+ opacity: 1;
226
+ }
227
+ :where(select:is([multiple], [size])) optgroup {
228
+ font-weight: bolder;
229
+ }
230
+ button,
231
+ select {
232
+ text-transform: none;
233
+ }
234
+ button,
235
+ input:where([type="button"], [type="reset"], [type="submit"]),
236
+ ::file-selector-button {
237
+ -moz-appearance: button;
238
+ appearance: button;
239
+ -webkit-appearance: button;
240
+ }
241
+ button {
242
+ cursor: pointer;
243
+ }
244
+ textarea {
245
+ resize: vertical;
246
+ }
247
+ /* `in srgb` for consistency with the library's other color-mix calls. The
248
+ space is irrelevant when mixing with `transparent` (premultiplied-alpha
249
+ interpolation, CSS Color 4), so diverging from Tailwind Preflight's oklab
250
+ here is visually a no-op. */
251
+ ::-moz-placeholder {
252
+ opacity: 1;
253
+ color: color-mix(in srgb, currentcolor 55%, transparent);
254
+ }
255
+ ::placeholder {
256
+ opacity: 1;
257
+ color: color-mix(in srgb, currentcolor 55%, transparent);
258
+ }
259
+ ::-webkit-date-and-time-value {
260
+ min-height: 1lh;
261
+ text-align: inherit;
262
+ }
263
+ ::-webkit-datetime-edit {
264
+ display: inline-flex;
265
+ padding: 0;
266
+ }
267
+ ::-webkit-datetime-edit-fields-wrapper {
268
+ padding: 0;
269
+ }
270
+ ::-webkit-inner-spin-button,
271
+ ::-webkit-outer-spin-button {
272
+ -webkit-appearance: none;
273
+ margin: 0;
274
+ height: auto;
275
+ }
276
+ /* number spinner: Chromium is already handled by ::-webkit-inner/outer-spin-button
277
+ { appearance: none } above. Firefox needs its own vendor prefix — the unprefixed
278
+ `appearance: textfield` is deprecated per CSS UI Level 4 (moved to compatibility
279
+ appendix) and dropped by Tailwind v4 Preflight, so we omit it. */
280
+ input[type="number"] {
281
+ -moz-appearance: textfield;
282
+ }
283
+ [type="search"] {
284
+ -webkit-appearance: textfield;
285
+ outline-offset: -2px;
286
+ }
287
+ ::-webkit-search-decoration,
288
+ ::-webkit-search-cancel-button {
289
+ -webkit-appearance: none;
290
+ }
291
+ :-moz-ui-invalid {
292
+ box-shadow: none;
293
+ }
294
+ :-moz-focusring:where(:not(iframe)) {
295
+ outline: auto;
296
+ }
297
+ b,
298
+ strong {
299
+ font-weight: bolder;
300
+ }
301
+ small {
302
+ font-size: 80%;
303
+ }
304
+ code,
305
+ kbd,
306
+ samp,
307
+ pre {
308
+ font-family: var(--ps1ui-font-mono);
309
+ font-size: 1em;
310
+ }
311
+ abbr:where([title]) {
312
+ -webkit-text-decoration: underline dotted;
313
+ text-decoration: underline dotted;
314
+ }
315
+ q::before,
316
+ q::after {
317
+ content: "";
318
+ }
319
+ /* Keeps sub/sup from expanding the parent line-height. */
320
+ sub,
321
+ sup {
322
+ font-size: 75%;
323
+ line-height: 0;
324
+ position: relative;
325
+ vertical-align: baseline;
326
+ }
327
+ sub {
328
+ bottom: -0.25em;
329
+ }
330
+ sup {
331
+ top: -0.5em;
332
+ }
333
+ table {
334
+ text-indent: 0;
335
+ border-color: inherit;
336
+ border-collapse: collapse;
337
+ }
338
+ hr {
339
+ height: 0;
340
+ color: inherit;
341
+ border-top: 1px solid;
342
+ }
343
+ summary {
344
+ display: list-item;
345
+ }
346
+ /* [hidden] would lose to `display: flex` etc. without this. */
347
+ [hidden]:where(:not([hidden="until-found"])) {
348
+ display: none !important;
349
+ }
350
+ @media (prefers-reduced-motion: no-preference) {
351
+ html {
352
+ interpolate-size: allow-keywords;
353
+ }
354
+ }
355
+ /* ==========================================================================
356
+ * Ambient environment
357
+ * --------------------------------------------------------------------------
358
+ * `background`/`color`/`font-family` intentionally live on `html`, not `body`.
359
+ * Rationale: the reset above zeros `body { margin }`, but the browser still
360
+ * paints the area beyond `body` (macOS/iOS rubber-band overscroll region,
361
+ * horizontal-scroll overflow, viewport slack) using the canvas root's
362
+ * background — which is `html`. If these rules were on `body`, that overscroll
363
+ * area would reveal the light UA default and flash white on scroll. `color`
364
+ * and `font-family` cascade down to `body` and descendants naturally.
365
+ * ========================================================================== */
366
+ html {
367
+ background: var(--ps1ui-color-bg);
368
+ color: var(--ps1ui-color-fg);
369
+ font-family: var(--ps1ui-font-mono);
370
+ }
371
+ ::-moz-selection {
372
+ background: var(--ps1ui-color-primary);
373
+ color: var(--ps1ui-color-primary-fg);
374
+ }
375
+ ::selection {
376
+ background: var(--ps1ui-color-primary);
377
+ color: var(--ps1ui-color-primary-fg);
378
+ }
379
+ /* ==========================================================================
380
+ * Cascade & overrides — why this file does NOT use @layer
381
+ * --------------------------------------------------------------------------
382
+ * Wrapping these styles in `@layer ps1ui { … }` was considered and rejected:
383
+ * unlayered styles beat layered ones regardless of specificity, and host
384
+ * design systems (this entry's target audience) invariably ship unlayered
385
+ * element-level rules (`button { … }`, `input { … }`, `a { … }`). Layering
386
+ * ps1ui would let every such host rule silently override component classes,
387
+ * breaking components by default exactly where this entry is meant to be
388
+ * embedded. So component rules stay unlayered, and selectors like
389
+ * `.ps1ui-checkbox:checked:hover:not(:disabled)` keep real specificity.
390
+ *
391
+ * Overriding ps1ui styles, in preference order:
392
+ * 1. Redefine `--ps1ui-*` design tokens (the supported theming surface) —
393
+ * colors, spacing, radii, fonts all flow from tokens.css.
394
+ * 2. Match or exceed the component selector's specificity, e.g.
395
+ * `.my-app .ps1ui-button { … }`.
396
+ * 3. If YOUR system is fully layered, import this entry into a low layer
397
+ * yourself — `@import "@ps1ui/core/components.css" layer(vendor);` —
398
+ * and your layered rules above it win without extra specificity.
399
+ * ========================================================================== */
400
+ /* PS1Root establishes the app-level responsive containment context.
401
+ `container-type: inline-size` turns this element into a query context for
402
+ `@container` rules used by responsive ps1ui components. Named `ps1ui-root`
403
+ for future targeted queries; unnamed queries in component CSS match this
404
+ via nearest-ancestor lookup. */
405
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
406
+ in components.css's shared selector list — see the block header there. */
407
+ .ps1ui-root {
408
+ container: ps1ui-root / inline-size;
409
+ }
410
+ .ps1ui-anchor {
411
+ font-family: var(--ps1ui-font-mono);
412
+ text-decoration: underline;
413
+ text-underline-offset: 2px;
414
+ text-decoration-thickness: 1px;
415
+ border-radius: var(--ps1ui-radius-md);
416
+ cursor: pointer;
417
+ transition:
418
+ color var(--ps1ui-transition-fast),
419
+ text-decoration-color var(--ps1ui-transition-fast);
420
+ }
421
+ /* outline (not box-shadow) so a wrapping inline anchor renders as connected fragments
422
+ per line-box instead of two disjoint rectangles. Modern browsers follow border-radius. */
423
+ .ps1ui-anchor:focus-visible {
424
+ outline: 2px solid var(--ps1ui-color-primary);
425
+ outline-offset: 2px;
426
+ }
427
+ /* text-decoration-color defaults to currentColor, so setting only `color` per state
428
+ also shifts the underline. subtle's default intentionally decouples the two — the
429
+ underline is dimmer than the text — hence the explicit override there. */
430
+ .ps1ui-anchor--primary {
431
+ color: var(--ps1ui-color-primary);
432
+ }
433
+ .ps1ui-anchor--primary:hover {
434
+ color: var(--ps1ui-color-primary-hover);
435
+ }
436
+ .ps1ui-anchor--primary:active {
437
+ color: var(--ps1ui-color-primary-active);
438
+ }
439
+ .ps1ui-anchor--subtle {
440
+ color: inherit;
441
+ text-decoration-color: var(--ps1ui-color-fg-subtle);
442
+ }
443
+ /* Reset text-decoration-color to currentColor so the underline follows the
444
+ hover/active color shift; without this it would stay pinned to fg-subtle
445
+ from the base rule. */
446
+ .ps1ui-anchor--subtle:hover {
447
+ color: var(--ps1ui-color-primary);
448
+ text-decoration-color: currentColor;
449
+ }
450
+ .ps1ui-anchor--subtle:active {
451
+ color: var(--ps1ui-color-primary-active);
452
+ text-decoration-color: currentColor;
453
+ }
454
+ .ps1ui-button {
455
+ display: inline-flex;
456
+ align-items: center;
457
+ justify-content: center;
458
+ gap: var(--ps1ui-space-sm);
459
+ font-family: var(--ps1ui-font-mono);
460
+ font-size: var(--ps1ui-font-size-sm);
461
+ /* Applied at base rather than per-variant so primary and secondary read as
462
+ equal-weight siblings — variant should encode affordance (fill vs outline),
463
+ not typography weight. A per-variant weight difference used to make
464
+ secondary feel "one size smaller" next to primary. */
465
+ font-weight: 600;
466
+ line-height: 1;
467
+ /* 14px: 2× the 7px vertical rhythm; deliberately off the space scale. */
468
+ padding: var(--ps1ui-control-padding-y) 14px;
469
+ border-radius: var(--ps1ui-radius-md);
470
+ border: 1px solid transparent;
471
+ cursor: pointer;
472
+ -webkit-user-select: none;
473
+ -moz-user-select: none;
474
+ user-select: none;
475
+ transition:
476
+ background var(--ps1ui-transition-fast),
477
+ border-color var(--ps1ui-transition-fast),
478
+ color var(--ps1ui-transition-fast);
479
+ }
480
+ .ps1ui-button:focus-visible {
481
+ outline: none;
482
+ border-color: var(--ps1ui-color-primary);
483
+ box-shadow: var(--ps1ui-shadow-focus-ring);
484
+ }
485
+ .ps1ui-button:disabled {
486
+ cursor: not-allowed;
487
+ }
488
+ .ps1ui-button--primary {
489
+ background: var(--ps1ui-color-primary);
490
+ color: var(--ps1ui-color-primary-fg);
491
+ border-color: var(--ps1ui-color-primary);
492
+ }
493
+ .ps1ui-button--primary:hover:not(:disabled) {
494
+ background: var(--ps1ui-color-primary-hover);
495
+ border-color: var(--ps1ui-color-primary-hover);
496
+ }
497
+ .ps1ui-button--primary:active:not(:disabled) {
498
+ background: var(--ps1ui-color-primary-active);
499
+ border-color: var(--ps1ui-color-primary-active);
500
+ }
501
+ .ps1ui-button--primary:disabled {
502
+ background: transparent;
503
+ color: var(--ps1ui-color-fg-subtle);
504
+ border-color: var(--ps1ui-color-border);
505
+ }
506
+ .ps1ui-button--secondary {
507
+ background: transparent;
508
+ color: var(--ps1ui-color-fg);
509
+ /* fg-subtle (~5:1 vs --ps1ui-color-bg) instead of border-strong (~1.7:1) so
510
+ the 1px outline actually defines the click target on the dark canvas —
511
+ otherwise the boundary reads as a ghost and the button visually shrinks
512
+ down to its text glyphs next to a solid primary. */
513
+ border-color: var(--ps1ui-color-fg-subtle);
514
+ }
515
+ .ps1ui-button--secondary:hover:not(:disabled) {
516
+ border-color: var(--ps1ui-color-primary);
517
+ background: color-mix(in srgb, var(--ps1ui-color-primary) 6%, transparent);
518
+ }
519
+ .ps1ui-button--secondary:active:not(:disabled) {
520
+ border-color: var(--ps1ui-color-primary-active);
521
+ background: color-mix(in srgb, var(--ps1ui-color-primary-active) 10%, transparent);
522
+ }
523
+ .ps1ui-button--secondary:disabled {
524
+ color: var(--ps1ui-color-fg-subtle);
525
+ border-color: var(--ps1ui-color-border);
526
+ }
527
+ .ps1ui-checkbox {
528
+ -moz-appearance: none;
529
+ appearance: none;
530
+ -webkit-appearance: none;
531
+ width: 16px;
532
+ height: 16px;
533
+ margin: 0;
534
+ padding: 0;
535
+ box-sizing: border-box;
536
+ display: inline-block;
537
+ position: relative;
538
+ vertical-align: middle;
539
+ flex-shrink: 0;
540
+ font-family: var(--ps1ui-font-mono);
541
+ color: var(--ps1ui-color-primary-fg);
542
+ background: var(--ps1ui-color-bg);
543
+ border: 1px solid var(--ps1ui-color-border-strong);
544
+ border-radius: var(--ps1ui-radius-md);
545
+ cursor: pointer;
546
+ transition:
547
+ background var(--ps1ui-transition-fast),
548
+ border-color var(--ps1ui-transition-fast),
549
+ box-shadow var(--ps1ui-transition-fast);
550
+ }
551
+ .ps1ui-checkbox:hover:not(:disabled) {
552
+ border-color: var(--ps1ui-color-primary);
553
+ }
554
+ .ps1ui-checkbox:focus-visible {
555
+ outline: none;
556
+ border-color: var(--ps1ui-color-primary);
557
+ box-shadow: var(--ps1ui-shadow-focus-ring);
558
+ }
559
+ .ps1ui-checkbox:checked,
560
+ .ps1ui-checkbox:indeterminate {
561
+ background: var(--ps1ui-color-primary);
562
+ border-color: var(--ps1ui-color-primary);
563
+ }
564
+ .ps1ui-checkbox:checked:hover:not(:disabled),
565
+ .ps1ui-checkbox:indeterminate:hover:not(:disabled) {
566
+ background: var(--ps1ui-color-primary-hover);
567
+ border-color: var(--ps1ui-color-primary-hover);
568
+ }
569
+ .ps1ui-checkbox:checked:active:not(:disabled),
570
+ .ps1ui-checkbox:indeterminate:active:not(:disabled) {
571
+ background: var(--ps1ui-color-primary-active);
572
+ border-color: var(--ps1ui-color-primary-active);
573
+ }
574
+ /* Checkmark: classic rotated-L trick — draws the right + bottom border of a small
575
+ rectangle, then rotates 45deg so it reads as a ✓. `:not(:indeterminate)` makes
576
+ precedence explicit for the mixed DOM state (both .checked and .indeterminate
577
+ true) so the indeterminate bar wins without relying on source order. Uses
578
+ `currentColor` so the disabled state can dim the mark by changing `color`.
579
+ Depends on the pseudo's initial `box-sizing: content-box` so the 2px borders
580
+ sit OUTSIDE the 4×8 content — base.css's reset intentionally excludes pseudo
581
+ elements from its `box-sizing: border-box` block for exactly this pattern.
582
+ Rendered geometry locked in by Checkbox.checkmark-geometry.test.tsx. */
583
+ .ps1ui-checkbox:checked:not(:indeterminate)::after {
584
+ content: "";
585
+ position: absolute;
586
+ left: 4px;
587
+ top: 1px;
588
+ width: 4px;
589
+ height: 8px;
590
+ border-style: solid;
591
+ border-color: currentColor;
592
+ border-width: 0 2px 2px 0;
593
+ transform: rotate(45deg);
594
+ }
595
+ .ps1ui-checkbox:indeterminate::after {
596
+ content: "";
597
+ position: absolute;
598
+ left: 2px;
599
+ top: 6px;
600
+ width: 10px;
601
+ height: 2px;
602
+ background: currentColor;
603
+ border-radius: 1px;
604
+ }
605
+ /* forced-colors force-replaces author backgrounds, so the background-drawn
606
+ indeterminate bar would vanish — redraw it as a border, which survives as
607
+ system ink (same 6–8px band; the checked ✓ is already border-drawn and
608
+ needs nothing). The focus-outline counterpart lives in the grouped
609
+ forced-colors rule in styles/components.css. Tested in
610
+ src/styles/forced-colors.test.tsx. */
611
+ @media (forced-colors: active) {
612
+ .ps1ui-checkbox:indeterminate::after {
613
+ background: none;
614
+ height: 0;
615
+ border-top: 2px solid currentColor;
616
+ border-radius: 0;
617
+ }
618
+ }
619
+ .ps1ui-checkbox:disabled {
620
+ cursor: not-allowed;
621
+ color: var(--ps1ui-color-fg-subtle);
622
+ background: var(--ps1ui-color-surface);
623
+ border-color: var(--ps1ui-color-border);
624
+ }
625
+ .ps1ui-checkbox:disabled:checked,
626
+ .ps1ui-checkbox:disabled:indeterminate {
627
+ background: var(--ps1ui-color-surface);
628
+ border-color: var(--ps1ui-color-border);
629
+ }
630
+ /* Input custom properties emitted by Container.tsx per set responsive-prop
631
+ breakpoint (`--_container-<axis>-<bp>`). Registered as non-inheriting for
632
+ the same reason as Stack — see Stack.css's @property block for the full
633
+ rationale (CSS Custom Properties Level 1 §2 inheritance default;
634
+ `unset`/`revert` won't block; only `initial` or `@property inherits:false`
635
+ works). Any new responsive axis added to Container.tsx must add matching
636
+ @property blocks here. */
637
+ @property --_container-size-base {
638
+ syntax: "*";
639
+ inherits: false;
640
+ }
641
+ @property --_container-size-sm {
642
+ syntax: "*";
643
+ inherits: false;
644
+ }
645
+ @property --_container-size-md {
646
+ syntax: "*";
647
+ inherits: false;
648
+ }
649
+ @property --_container-size-lg {
650
+ syntax: "*";
651
+ inherits: false;
652
+ }
653
+ @property --_container-size-xl {
654
+ syntax: "*";
655
+ inherits: false;
656
+ }
657
+ @property --_container-px-base {
658
+ syntax: "*";
659
+ inherits: false;
660
+ }
661
+ @property --_container-px-sm {
662
+ syntax: "*";
663
+ inherits: false;
664
+ }
665
+ @property --_container-px-md {
666
+ syntax: "*";
667
+ inherits: false;
668
+ }
669
+ @property --_container-px-lg {
670
+ syntax: "*";
671
+ inherits: false;
672
+ }
673
+ @property --_container-px-xl {
674
+ syntax: "*";
675
+ inherits: false;
676
+ }
677
+ /* Container — max-width + horizontal padding + auto centering.
678
+ Establishes an inline-size containment context so descendants respond to
679
+ Container's width via `@container` queries. Named `ps1ui-container` for
680
+ future targeted queries.
681
+
682
+ Responsive values propagate via a CSS custom-property cascade. The
683
+ component emits `--_container-size-{bp}` / `--_container-px-{bp}` via
684
+ inline style (see Container.tsx). Each `@container (min-width: X)` block
685
+ below overrides `--_size` / `--_px` with the value for the matching
686
+ breakpoint — the fallback chain in `--_size-{bp}` inherits the previous
687
+ breakpoint's effective value so a partial responsive object cascades
688
+ naturally.
689
+
690
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
691
+ inline as literal values because `@container` conditions cannot resolve
692
+ `var()`. Keep them in sync. */
693
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
694
+ in components.css's shared selector list — see the block header there. */
695
+ .ps1ui-container {
696
+ container: ps1ui-container / inline-size;
697
+ width: 100%;
698
+ margin-inline: auto;
699
+
700
+ --_size-base: var(--_container-size-base, var(--ps1ui-container-lg));
701
+ --_size-sm: var(--_container-size-sm, var(--_size-base));
702
+ --_size-md: var(--_container-size-md, var(--_size-sm));
703
+ --_size-lg: var(--_container-size-lg, var(--_size-md));
704
+ --_size-xl: var(--_container-size-xl, var(--_size-lg));
705
+
706
+ --_px-base: var(--_container-px-base, var(--ps1ui-space-lg));
707
+ --_px-sm: var(--_container-px-sm, var(--_px-base));
708
+ --_px-md: var(--_container-px-md, var(--_px-sm));
709
+ --_px-lg: var(--_container-px-lg, var(--_px-md));
710
+ --_px-xl: var(--_container-px-xl, var(--_px-lg));
711
+
712
+ --_size: var(--_size-base);
713
+ --_px: var(--_px-base);
714
+
715
+ max-width: var(--_size);
716
+ padding-inline: var(--_px);
717
+ }
718
+ @container (min-width: 40rem) {
719
+ .ps1ui-container {
720
+ --_size: var(--_size-sm);
721
+ --_px: var(--_px-sm);
722
+ }
723
+ }
724
+ @container (min-width: 48rem) {
725
+ .ps1ui-container {
726
+ --_size: var(--_size-md);
727
+ --_px: var(--_px-md);
728
+ }
729
+ }
730
+ @container (min-width: 64rem) {
731
+ .ps1ui-container {
732
+ --_size: var(--_size-lg);
733
+ --_px: var(--_px-lg);
734
+ }
735
+ }
736
+ @container (min-width: 80rem) {
737
+ .ps1ui-container {
738
+ --_size: var(--_size-xl);
739
+ --_px: var(--_px-xl);
740
+ }
741
+ }
742
+ /* Input custom properties emitted by Grid.tsx per set responsive-prop
743
+ breakpoint (`--_grid-<axis>-<bp>`). Registered as non-inheriting for the
744
+ same reason as Stack — see Stack.css's @property block for the full
745
+ rationale. Any new responsive axis added to Grid.tsx must add matching
746
+ @property blocks here. */
747
+ @property --_grid-columns-base {
748
+ syntax: "*";
749
+ inherits: false;
750
+ }
751
+ @property --_grid-columns-sm {
752
+ syntax: "*";
753
+ inherits: false;
754
+ }
755
+ @property --_grid-columns-md {
756
+ syntax: "*";
757
+ inherits: false;
758
+ }
759
+ @property --_grid-columns-lg {
760
+ syntax: "*";
761
+ inherits: false;
762
+ }
763
+ @property --_grid-columns-xl {
764
+ syntax: "*";
765
+ inherits: false;
766
+ }
767
+ @property --_grid-gap-base {
768
+ syntax: "*";
769
+ inherits: false;
770
+ }
771
+ @property --_grid-gap-sm {
772
+ syntax: "*";
773
+ inherits: false;
774
+ }
775
+ @property --_grid-gap-md {
776
+ syntax: "*";
777
+ inherits: false;
778
+ }
779
+ @property --_grid-gap-lg {
780
+ syntax: "*";
781
+ inherits: false;
782
+ }
783
+ @property --_grid-gap-xl {
784
+ syntax: "*";
785
+ inherits: false;
786
+ }
787
+ /* Grid — display:grid + N-column tracks + gap.
788
+ Establishes an inline-size containment context so descendants respond to
789
+ Grid's width via `@container` queries. Named `ps1ui-grid` for future
790
+ targeted queries.
791
+
792
+ Responsive values propagate via a CSS custom-property cascade. The
793
+ component emits `--_grid-columns-{bp}` / `--_grid-gap-{bp}` via inline
794
+ style (see Grid.tsx). Each `@container (min-width: X)` block below
795
+ overrides `--_columns` / `--_gap` with the value for the matching
796
+ breakpoint; the fallback chain in `--_columns-{bp}` inherits the
797
+ previous breakpoint's effective value so a partial responsive object
798
+ cascades naturally.
799
+
800
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
801
+ inline as literal values because `@container` conditions cannot resolve
802
+ `var()`. Keep them in sync. */
803
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
804
+ in components.css's shared selector list — see the block header there. */
805
+ .ps1ui-grid {
806
+ container: ps1ui-grid / inline-size;
807
+ display: grid;
808
+
809
+ --_columns-base: var(--_grid-columns-base, 1);
810
+ --_columns-sm: var(--_grid-columns-sm, var(--_columns-base));
811
+ --_columns-md: var(--_grid-columns-md, var(--_columns-sm));
812
+ --_columns-lg: var(--_grid-columns-lg, var(--_columns-md));
813
+ --_columns-xl: var(--_grid-columns-xl, var(--_columns-lg));
814
+
815
+ --_gap-base: var(--_grid-gap-base, var(--ps1ui-space-md));
816
+ --_gap-sm: var(--_grid-gap-sm, var(--_gap-base));
817
+ --_gap-md: var(--_grid-gap-md, var(--_gap-sm));
818
+ --_gap-lg: var(--_grid-gap-lg, var(--_gap-md));
819
+ --_gap-xl: var(--_grid-gap-xl, var(--_gap-lg));
820
+
821
+ --_columns: var(--_columns-base);
822
+ --_gap: var(--_gap-base);
823
+
824
+ grid-template-columns: repeat(var(--_columns), minmax(0, 1fr));
825
+ gap: var(--_gap);
826
+ }
827
+ @container (min-width: 40rem) {
828
+ .ps1ui-grid {
829
+ --_columns: var(--_columns-sm);
830
+ --_gap: var(--_gap-sm);
831
+ }
832
+ }
833
+ @container (min-width: 48rem) {
834
+ .ps1ui-grid {
835
+ --_columns: var(--_columns-md);
836
+ --_gap: var(--_gap-md);
837
+ }
838
+ }
839
+ @container (min-width: 64rem) {
840
+ .ps1ui-grid {
841
+ --_columns: var(--_columns-lg);
842
+ --_gap: var(--_gap-lg);
843
+ }
844
+ }
845
+ @container (min-width: 80rem) {
846
+ .ps1ui-grid {
847
+ --_columns: var(--_columns-xl);
848
+ --_gap: var(--_gap-xl);
849
+ }
850
+ }
851
+ /* Input custom properties emitted by GridItem.tsx per set responsive-prop
852
+ breakpoint (`--_griditem-col-span-<bp>`). Registered as non-inheriting for
853
+ the same reason as Stack — see Stack.css's @property block for the full
854
+ rationale. Any new responsive axis added to GridItem.tsx must add matching
855
+ @property blocks here. */
856
+ @property --_griditem-col-span-base {
857
+ syntax: "*";
858
+ inherits: false;
859
+ }
860
+ @property --_griditem-col-span-sm {
861
+ syntax: "*";
862
+ inherits: false;
863
+ }
864
+ @property --_griditem-col-span-md {
865
+ syntax: "*";
866
+ inherits: false;
867
+ }
868
+ @property --_griditem-col-span-lg {
869
+ syntax: "*";
870
+ inherits: false;
871
+ }
872
+ @property --_griditem-col-span-xl {
873
+ syntax: "*";
874
+ inherits: false;
875
+ }
876
+ /* GridItem — child of Grid that spans multiple column tracks.
877
+ GridItem is a leaf: it queries an ancestor containment context (typically
878
+ the parent Grid, which establishes `container: ps1ui-grid / inline-size`),
879
+ but does NOT establish one itself. Establishing containment on every
880
+ GridItem would fragment the query surface for descendants and impose a
881
+ `contain: layout` side effect on every cell — needless overhead for a
882
+ component whose only job is to declare its own column span.
883
+
884
+ Responsive values propagate via a CSS custom-property cascade. The
885
+ component emits `--_griditem-col-span-{bp}` via inline style (see
886
+ GridItem.tsx). Each `@container (min-width: X)` block below overrides
887
+ `--_col-span` with the value for the matching breakpoint; the fallback
888
+ chain in `--_col-span-{bp}` inherits the previous breakpoint's effective
889
+ value so a partial responsive object cascades naturally.
890
+
891
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
892
+ inline as literal values because `@container` conditions cannot resolve
893
+ `var()`. Keep them in sync.
894
+
895
+ Fallback default: `1` — a bare `<GridItem>` with no colSpan behaves
896
+ exactly like a plain grid child (`grid-column: span 1`), so wrapping
897
+ existing Grid children in `<GridItem>` never changes layout by itself.
898
+
899
+ Used outside a display:grid parent, `grid-column` is an inert property —
900
+ the item still renders, the declaration is simply ignored. That matches
901
+ the "no runtime enforcement of parent type" convention used across the
902
+ library. */
903
+ .ps1ui-griditem {
904
+ --_col-span-base: var(--_griditem-col-span-base, 1);
905
+ --_col-span-sm: var(--_griditem-col-span-sm, var(--_col-span-base));
906
+ --_col-span-md: var(--_griditem-col-span-md, var(--_col-span-sm));
907
+ --_col-span-lg: var(--_griditem-col-span-lg, var(--_col-span-md));
908
+ --_col-span-xl: var(--_griditem-col-span-xl, var(--_col-span-lg));
909
+
910
+ --_col-span: var(--_col-span-base);
911
+
912
+ grid-column: span var(--_col-span);
913
+ }
914
+ @container (min-width: 40rem) {
915
+ .ps1ui-griditem {
916
+ --_col-span: var(--_col-span-sm);
917
+ }
918
+ }
919
+ @container (min-width: 48rem) {
920
+ .ps1ui-griditem {
921
+ --_col-span: var(--_col-span-md);
922
+ }
923
+ }
924
+ @container (min-width: 64rem) {
925
+ .ps1ui-griditem {
926
+ --_col-span: var(--_col-span-lg);
927
+ }
928
+ }
929
+ @container (min-width: 80rem) {
930
+ .ps1ui-griditem {
931
+ --_col-span: var(--_col-span-xl);
932
+ }
933
+ }
934
+ .ps1ui-input {
935
+ display: block;
936
+ width: 100%;
937
+ box-sizing: border-box;
938
+ font-family: var(--ps1ui-font-mono);
939
+ font-size: var(--ps1ui-font-size-sm);
940
+ line-height: 1.4;
941
+ color: var(--ps1ui-color-fg);
942
+ background: var(--ps1ui-color-bg);
943
+ border: 1px solid var(--ps1ui-color-border-strong);
944
+ border-radius: var(--ps1ui-radius-md);
945
+ padding: var(--ps1ui-control-padding-y) var(--ps1ui-space-md);
946
+ transition:
947
+ border-color var(--ps1ui-transition-fast),
948
+ box-shadow var(--ps1ui-transition-fast);
949
+ }
950
+ .ps1ui-input::-moz-placeholder {
951
+ color: var(--ps1ui-color-fg-subtle);
952
+ }
953
+ .ps1ui-input::placeholder {
954
+ color: var(--ps1ui-color-fg-subtle);
955
+ }
956
+ .ps1ui-input:hover:not(:disabled) {
957
+ border-color: var(--ps1ui-color-primary);
958
+ }
959
+ .ps1ui-input:focus {
960
+ outline: none;
961
+ border-color: var(--ps1ui-color-primary);
962
+ box-shadow: var(--ps1ui-shadow-focus-ring);
963
+ }
964
+ .ps1ui-input:disabled {
965
+ cursor: not-allowed;
966
+ color: var(--ps1ui-color-fg-subtle);
967
+ border-color: var(--ps1ui-color-border);
968
+ background: var(--ps1ui-color-surface);
969
+ }
970
+ .ps1ui-label {
971
+ display: inline-block;
972
+ font-family: var(--ps1ui-font-mono);
973
+ font-size: var(--ps1ui-font-size-sm);
974
+ font-weight: 500;
975
+ line-height: 1.6;
976
+ color: var(--ps1ui-color-fg);
977
+ }
978
+ /* List — semantic <ul>/<ol> wrapper with plain-text markers.
979
+
980
+ The global reset in base.css sets `list-style: none` on every <ul>/<ol>,
981
+ which strips the UA bullet/number glyph. List re-introduces a marker via
982
+ `::before` on each direct <li> — `-` for unordered, `NN.` for ordered —
983
+ so the "reset removes bullets" side effect turns into an on-brand
984
+ plain-text glyph rather than a hole.
985
+
986
+ The ordered marker uses `counter(list-item)` — the browser-maintained
987
+ list-item counter that already honours `<ol start>`, `<ol reversed>`,
988
+ and per-item `<li value>`. Using a hand-rolled `counter-reset` /
989
+ `counter-increment` would silently ignore all three attributes and
990
+ diverge from what assistive tech announcing the native counter reads.
991
+
992
+ `::before` (not `::marker`) is used because `::marker` styleability
993
+ lands late in some engines (custom `content` on `::marker` only reached
994
+ Safari 17) and because the inline-block `::before` + negative margin
995
+ pattern composes cleanly with block-flow content — a nested <List>
996
+ inside a <li> flows below the item text, with its own indent stacking
997
+ on top of the outer's. An earlier `display: grid` layout collided here
998
+ (nested list fell into the marker column and shoved the outer text
999
+ right); block flow avoids all of it.
1000
+
1001
+ Forced-colors (Windows High Contrast) repaints the `::before` glyph
1002
+ with `CanvasText` — the marker survives as system-foreground ink; no
1003
+ forced-colors-specific defense is needed. */
1004
+ .ps1ui-list {
1005
+ display: flex;
1006
+ flex-direction: column;
1007
+ gap: var(--ps1ui-space-xs);
1008
+ padding: 0;
1009
+ margin: 0;
1010
+ color: var(--ps1ui-color-fg);
1011
+ font-family: var(--ps1ui-font-mono);
1012
+ font-size: var(--ps1ui-font-size-sm);
1013
+ line-height: 1.6;
1014
+ }
1015
+ /* Hanging indent — `padding-left` reserves the marker column; the ::before
1016
+ is inline-block with a negative `margin-left` that pulls it into that
1017
+ reservation. Wrapped item text continues from the padding-left position
1018
+ on line 2+, so long items don't slip back under the marker. Block-flow
1019
+ content nested inside the <li> (a sub <List>, a paragraph, etc.) also
1020
+ respects the padding-left, so it renders indented under the item's text
1021
+ rather than under the marker.
1022
+
1023
+ Marker column widths are hard-coded per variant (not lifted to a custom
1024
+ property) because the same numeric value shows up in `padding-left` on
1025
+ the <li> and in `width` / `margin-left` on the ::before; a shared var
1026
+ would need to be either non-inheriting (breaking pseudo-element access)
1027
+ or inheriting (leaking into descendants and violating the codebase's
1028
+ `--_*` = non-inheriting convention). Duplicating a `1ch` / `3ch`
1029
+ literal is the smaller cost.
1030
+
1031
+ Ordered's `3ch` marker column fits `1.` through `99.`; lists past 99
1032
+ items visually break the right-align column — an acceptable trade for
1033
+ the common < 100 case. */
1034
+ .ps1ui-list--unordered > li {
1035
+ padding-left: calc(1ch + var(--ps1ui-space-sm));
1036
+ }
1037
+ .ps1ui-list--ordered > li {
1038
+ padding-left: calc(3ch + var(--ps1ui-space-sm));
1039
+ }
1040
+ .ps1ui-list > li::before {
1041
+ display: inline-block;
1042
+ padding-right: var(--ps1ui-space-sm);
1043
+ color: var(--ps1ui-color-fg-muted);
1044
+ font-variant-numeric: tabular-nums;
1045
+ text-align: right;
1046
+ }
1047
+ .ps1ui-list--unordered > li::before {
1048
+ content: "-";
1049
+ width: 1ch;
1050
+ margin-left: calc((1ch + var(--ps1ui-space-sm)) * -1);
1051
+ }
1052
+ .ps1ui-list--ordered > li::before {
1053
+ content: counter(list-item) ".";
1054
+ width: 3ch;
1055
+ margin-left: calc((3ch + var(--ps1ui-space-sm)) * -1);
1056
+ }
1057
+ /* Breathing room above a nested <List>. Descendant combinator (not child)
1058
+ so the rule still applies when a consumer wraps the sublist in a layout
1059
+ primitive — `<li><Stack><List>…</List></Stack></li>` gets the same gap
1060
+ as `<li><List>…</List></li>`. The outer flex `gap` on `.ps1ui-list`
1061
+ only reaches direct children, so without this rule any nested list
1062
+ loses breathing room. */
1063
+ .ps1ui-list li .ps1ui-list {
1064
+ margin-top: var(--ps1ui-space-xs);
1065
+ }
1066
+ /* ListItem — no rules of its own.
1067
+
1068
+ The `.ps1ui-listitem` class (naming parallels `.ps1ui-griditem`) is a
1069
+ stable API hook for consumers. All marker / padding / hanging-indent
1070
+ rules live on `.ps1ui-list > li` in List.css so a bare `<li>` inside
1071
+ `<List>` continues to render identically to `<ListItem>`. Keeping this
1072
+ file present (and imported from components.css) matches the
1073
+ per-component file layout the `new-component` skill enforces. */
1074
+ .ps1ui-card {
1075
+ background: var(--ps1ui-color-surface);
1076
+ border: 1px solid var(--ps1ui-color-border);
1077
+ border-radius: var(--ps1ui-radius-lg);
1078
+ padding: var(--ps1ui-space-xl);
1079
+ color: var(--ps1ui-color-fg);
1080
+ font-family: var(--ps1ui-font-mono);
1081
+ font-size: var(--ps1ui-font-size-sm);
1082
+ line-height: 1.6;
1083
+ }
1084
+ /* Code — inline code chip.
1085
+
1086
+ Companion to <CodeBlock>, but for inline fragments that live inside body
1087
+ text: `<Text>call <Code>useState()</Code> to add state.</Text>`.
1088
+
1089
+ Font-size is `0.9em` (relative) so the chip auto-scales with the surrounding
1090
+ text. Inside a default `<Text size="sm">` (14 px) the chip renders at
1091
+ ~12.6 px, matching a small-caps rhythm without overriding the parent's
1092
+ size axis. Consumers who want an absolute size still can — an inline
1093
+ `style={{ fontSize: ... }}` outranks this rule.
1094
+
1095
+ `overflow-wrap: anywhere` lets long identifiers (`veryLongIdentifier`,
1096
+ `a.b.c.d`, file paths) break inside a narrow container instead of forcing
1097
+ a horizontal scroll on the parent. `break-word` was rejected because it
1098
+ only fires when NO other soft-wrap opportunity exists on the line — that
1099
+ fails the common "long path inside a short label" case. `anywhere` is
1100
+ the correct choice for inline code fragments where preserving whole-word
1101
+ integrity has no value (code has no natural word breaks).
1102
+
1103
+ Font-family is set explicitly (rather than relying on base.css's `code {
1104
+ font-family: var(--ps1ui-font-mono); }` reset) because Code must render
1105
+ the same way when consumed via the components-only CSS entry, which
1106
+ ships no reset. Same reason Card/Text/etc. redeclare font-family.
1107
+
1108
+ No variant axis: inline code is overwhelmingly used at a single visual
1109
+ weight. Consumers who need a colored fragment can override via inline
1110
+ style (`style={{ color: "var(--ps1ui-color-primary)" }}`) or a caller
1111
+ className — the base chip stays a single API surface.
1112
+
1113
+ `padding-block: 0.1em` assumes the parent's line-height leaves at least
1114
+ ~0.1em of vertical slack per side. Inline padding does NOT extend the
1115
+ line box (per CSS 2.1 §10.8), so an outer `line-height: 1` container
1116
+ would paint the chip's background/border into the adjacent line. All
1117
+ in-repo callers (Text = 1.6, Heading = 1.3) leave ample slack;
1118
+ consumers who tighten line-height below ~1.2 need to widen it around
1119
+ inline `<Code>` themselves. */
1120
+ .ps1ui-code {
1121
+ font-family: var(--ps1ui-font-mono);
1122
+ font-size: 0.9em;
1123
+ padding: 0.1em 0.35em;
1124
+ background: var(--ps1ui-color-surface);
1125
+ border: 1px solid var(--ps1ui-color-border);
1126
+ border-radius: var(--ps1ui-radius-md);
1127
+ color: var(--ps1ui-color-fg);
1128
+ overflow-wrap: anywhere;
1129
+ }
1130
+ .ps1ui-codeblock {
1131
+ margin: 0;
1132
+ padding: var(--ps1ui-space-md) var(--ps1ui-space-lg);
1133
+ background: var(--ps1ui-color-surface);
1134
+ border: 1px solid var(--ps1ui-color-border);
1135
+ border-radius: var(--ps1ui-radius-lg);
1136
+ color: var(--ps1ui-color-fg);
1137
+ font-family: var(--ps1ui-font-mono);
1138
+ font-size: var(--ps1ui-font-size-sm);
1139
+ line-height: 1.6;
1140
+ overflow-x: auto;
1141
+ -moz-tab-size: 2;
1142
+ -o-tab-size: 2;
1143
+ tab-size: 2;
1144
+ /* Explicit `left` (not `start`) — <pre> defaults to text-align: inherit, so a
1145
+ consumer that centers text on an ancestor would center each code line too.
1146
+ `left` (rather than `start`) is deliberate: source code is inherently LTR
1147
+ and should stay left-aligned even inside an RTL context. */
1148
+ text-align: left;
1149
+ /* Force LTR text flow so code renders identically inside `<html dir="rtl">` /
1150
+ `<div dir="rtl">` ancestors — the character order and the horizontal scroll
1151
+ direction both stay consistent with what a developer expects from code. */
1152
+ direction: ltr;
1153
+ /* Explicit `pre` overrides consumer reset CSS that globally sets
1154
+ `pre { white-space: pre-wrap; }` — wrapping would defeat overflow-x: auto
1155
+ and collapse the scrollable-region focus semantics. Per-instance opt-in
1156
+ to wrapping is still possible via `style={{ whiteSpace: "pre-wrap" }}`. */
1157
+ white-space: pre;
1158
+ }
1159
+ .ps1ui-codeblock:focus-visible {
1160
+ outline: none;
1161
+ border-color: var(--ps1ui-color-primary);
1162
+ box-shadow: var(--ps1ui-shadow-focus-ring);
1163
+ }
1164
+ .ps1ui-codeblock code {
1165
+ font-family: inherit;
1166
+ font-size: inherit;
1167
+ color: inherit;
1168
+ background: none;
1169
+ }
1170
+ /* Prism token classes, mapped to ps1ui code palette (see tokens.css).
1171
+ Refractor emits hast <span class="token <type>"> nodes; keep this list
1172
+ in sync with the token types produced by Prism grammars. */
1173
+ .ps1ui-codeblock .token.comment,
1174
+ .ps1ui-codeblock .token.prolog,
1175
+ .ps1ui-codeblock .token.doctype,
1176
+ .ps1ui-codeblock .token.cdata {
1177
+ color: var(--ps1ui-code-comment);
1178
+ font-style: italic;
1179
+ }
1180
+ .ps1ui-codeblock .token.punctuation,
1181
+ .ps1ui-codeblock .token.operator {
1182
+ color: var(--ps1ui-code-punctuation);
1183
+ }
1184
+ .ps1ui-codeblock .token.property,
1185
+ .ps1ui-codeblock .token.tag,
1186
+ .ps1ui-codeblock .token.symbol {
1187
+ color: var(--ps1ui-code-tag);
1188
+ }
1189
+ .ps1ui-codeblock .token.attr-name {
1190
+ color: var(--ps1ui-code-attr);
1191
+ }
1192
+ .ps1ui-codeblock .token.string,
1193
+ .ps1ui-codeblock .token.char,
1194
+ .ps1ui-codeblock .token.url,
1195
+ .ps1ui-codeblock .token.attr-value,
1196
+ .ps1ui-codeblock .token.regex {
1197
+ color: var(--ps1ui-code-string);
1198
+ }
1199
+ /* diff: line-level markers get their own hue so "+" reads as added (green) and
1200
+ "-" reads as removed (rose). Distinct from tag/string groups on purpose. */
1201
+ .ps1ui-codeblock .token.inserted {
1202
+ color: var(--ps1ui-code-inserted);
1203
+ }
1204
+ .ps1ui-codeblock .token.deleted {
1205
+ color: var(--ps1ui-code-deleted);
1206
+ }
1207
+ .ps1ui-codeblock .token.number,
1208
+ .ps1ui-codeblock .token.boolean,
1209
+ .ps1ui-codeblock .token.constant,
1210
+ .ps1ui-codeblock .token.entity {
1211
+ color: var(--ps1ui-code-constant);
1212
+ }
1213
+ .ps1ui-codeblock .token.class-name,
1214
+ .ps1ui-codeblock .token.builtin {
1215
+ color: var(--ps1ui-code-type);
1216
+ }
1217
+ .ps1ui-codeblock .token.function,
1218
+ .ps1ui-codeblock .token.method {
1219
+ color: var(--ps1ui-code-function);
1220
+ }
1221
+ .ps1ui-codeblock .token.keyword,
1222
+ .ps1ui-codeblock .token.selector,
1223
+ .ps1ui-codeblock .token.atrule,
1224
+ .ps1ui-codeblock .token.important {
1225
+ color: var(--ps1ui-code-keyword);
1226
+ }
1227
+ .ps1ui-codeblock .token.important,
1228
+ .ps1ui-codeblock .token.bold {
1229
+ font-weight: 600;
1230
+ }
1231
+ .ps1ui-codeblock .token.italic {
1232
+ font-style: italic;
1233
+ }
1234
+ .ps1ui-details {
1235
+ background: var(--ps1ui-color-surface);
1236
+ border: 1px solid var(--ps1ui-color-border);
1237
+ border-radius: var(--ps1ui-radius-md);
1238
+ color: var(--ps1ui-color-fg);
1239
+ font-family: var(--ps1ui-font-mono);
1240
+ font-size: var(--ps1ui-font-size-sm);
1241
+ line-height: 1.6;
1242
+ }
1243
+ .ps1ui-details__summary {
1244
+ padding: var(--ps1ui-control-padding-y) var(--ps1ui-space-md);
1245
+ cursor: pointer;
1246
+ -webkit-user-select: none;
1247
+ -moz-user-select: none;
1248
+ user-select: none;
1249
+ color: var(--ps1ui-color-fg);
1250
+ border-radius: var(--ps1ui-radius-md);
1251
+ transition:
1252
+ color var(--ps1ui-transition-fast),
1253
+ box-shadow var(--ps1ui-transition-fast);
1254
+ /* base.css's reset keeps summary's default `display: list-item`; kill the
1255
+ marker so our ::before glyph is the only disclosure indicator. */
1256
+ list-style: none;
1257
+ }
1258
+ /* Older WebKit (Safari < 17) renders a native disclosure triangle via its
1259
+ private ::-webkit-details-marker pseudo — `list-style: none` alone doesn't
1260
+ suppress it there. Explicitly hide it so ::before is the only glyph. */
1261
+ .ps1ui-details__summary::-webkit-details-marker {
1262
+ display: none;
1263
+ }
1264
+ /* Glyph disclosure marker: `▸` when closed, `▾` when open. Fixed
1265
+ inline-block width so the summary text baseline stays put across states —
1266
+ the two glyphs don't measure identically in the JetBrains Mono fallback
1267
+ monospace stack. Colored with fg-muted so the arrow reads as chrome, not
1268
+ content; ::before is not exposed to the a11y tree so this is safe. */
1269
+ .ps1ui-details__summary::before {
1270
+ content: "▸";
1271
+ display: inline-block;
1272
+ width: 1.25ch;
1273
+ margin-right: var(--ps1ui-space-sm);
1274
+ color: var(--ps1ui-color-fg-muted);
1275
+ }
1276
+ .ps1ui-details[open] > .ps1ui-details__summary::before {
1277
+ content: "▾";
1278
+ }
1279
+ .ps1ui-details__summary:hover {
1280
+ color: var(--ps1ui-color-primary);
1281
+ }
1282
+ /* Focus ring lives on the summary (the interactive element), not on details
1283
+ itself — .ps1ui-details__summary:focus-visible is registered in
1284
+ styles/components.css's forced-colors grouped selector so the box-shadow
1285
+ ring falls back to a real outline in high-contrast mode. */
1286
+ .ps1ui-details__summary:focus-visible {
1287
+ outline: none;
1288
+ box-shadow: var(--ps1ui-shadow-focus-ring);
1289
+ }
1290
+ .ps1ui-details__body {
1291
+ padding: var(--ps1ui-space-md);
1292
+ border-top: 1px solid var(--ps1ui-color-border);
1293
+ }
1294
+ /* Input custom properties emitted by Stack.tsx per set responsive-prop
1295
+ breakpoint (`--_stack-<axis>-<bp>`). Registered as non-inheriting so a
1296
+ parent Stack's `--_stack-gap-md` (etc.) does NOT cascade into a nested
1297
+ Stack — CSS custom properties inherit by default (css-variables §2), and
1298
+ without this guard the `var(--_stack-<axis>-<bp>, fallback)` chain below
1299
+ picks up the ancestor's value on every descendant and silently overrides
1300
+ the child's own responsive resolution when the matching @container query
1301
+ fires. `syntax: "*"` lets us omit `initial-value` — the default value is
1302
+ the guaranteed-invalid value, which makes the fallback branch of each
1303
+ `var()` in the intermediate chain fire exactly as intended. Only inline
1304
+ styles emitted by Stack.tsx (author-origin, inline specificity) set these
1305
+ per-element, which beats the @property registration in the cascade. If a
1306
+ new responsive axis is added to Stack.tsx, a matching @property block MUST
1307
+ be added here; the nested-Stack regression tests in Stack.test.tsx cover
1308
+ every axis so a forgotten one trips CI immediately. `unset` / `revert` /
1309
+ `revert-layer` are NOT interchangeable with this approach for custom
1310
+ properties — they resolve to the inherited value, defeating the block. */
1311
+ @property --_stack-direction-base {
1312
+ syntax: "*";
1313
+ inherits: false;
1314
+ }
1315
+ @property --_stack-direction-sm {
1316
+ syntax: "*";
1317
+ inherits: false;
1318
+ }
1319
+ @property --_stack-direction-md {
1320
+ syntax: "*";
1321
+ inherits: false;
1322
+ }
1323
+ @property --_stack-direction-lg {
1324
+ syntax: "*";
1325
+ inherits: false;
1326
+ }
1327
+ @property --_stack-direction-xl {
1328
+ syntax: "*";
1329
+ inherits: false;
1330
+ }
1331
+ @property --_stack-gap-base {
1332
+ syntax: "*";
1333
+ inherits: false;
1334
+ }
1335
+ @property --_stack-gap-sm {
1336
+ syntax: "*";
1337
+ inherits: false;
1338
+ }
1339
+ @property --_stack-gap-md {
1340
+ syntax: "*";
1341
+ inherits: false;
1342
+ }
1343
+ @property --_stack-gap-lg {
1344
+ syntax: "*";
1345
+ inherits: false;
1346
+ }
1347
+ @property --_stack-gap-xl {
1348
+ syntax: "*";
1349
+ inherits: false;
1350
+ }
1351
+ @property --_stack-align-base {
1352
+ syntax: "*";
1353
+ inherits: false;
1354
+ }
1355
+ @property --_stack-align-sm {
1356
+ syntax: "*";
1357
+ inherits: false;
1358
+ }
1359
+ @property --_stack-align-md {
1360
+ syntax: "*";
1361
+ inherits: false;
1362
+ }
1363
+ @property --_stack-align-lg {
1364
+ syntax: "*";
1365
+ inherits: false;
1366
+ }
1367
+ @property --_stack-align-xl {
1368
+ syntax: "*";
1369
+ inherits: false;
1370
+ }
1371
+ @property --_stack-justify-base {
1372
+ syntax: "*";
1373
+ inherits: false;
1374
+ }
1375
+ @property --_stack-justify-sm {
1376
+ syntax: "*";
1377
+ inherits: false;
1378
+ }
1379
+ @property --_stack-justify-md {
1380
+ syntax: "*";
1381
+ inherits: false;
1382
+ }
1383
+ @property --_stack-justify-lg {
1384
+ syntax: "*";
1385
+ inherits: false;
1386
+ }
1387
+ @property --_stack-justify-xl {
1388
+ syntax: "*";
1389
+ inherits: false;
1390
+ }
1391
+ @property --_stack-wrap-base {
1392
+ syntax: "*";
1393
+ inherits: false;
1394
+ }
1395
+ @property --_stack-wrap-sm {
1396
+ syntax: "*";
1397
+ inherits: false;
1398
+ }
1399
+ @property --_stack-wrap-md {
1400
+ syntax: "*";
1401
+ inherits: false;
1402
+ }
1403
+ @property --_stack-wrap-lg {
1404
+ syntax: "*";
1405
+ inherits: false;
1406
+ }
1407
+ @property --_stack-wrap-xl {
1408
+ syntax: "*";
1409
+ inherits: false;
1410
+ }
1411
+ /* Stack — flexbox composition of children along one axis.
1412
+ Establishes an inline-size containment context so descendants respond to
1413
+ Stack's width via `@container` queries. Named `ps1ui-stack` for future
1414
+ targeted queries.
1415
+
1416
+ Responsive values propagate via a CSS custom-property cascade. The
1417
+ component emits `--_stack-{axis}-{bp}` via inline style (see Stack.tsx).
1418
+ Each `@container (min-width: X)` block below overrides `--_{axis}` with
1419
+ the value for the matching breakpoint; the fallback chain in
1420
+ `--_{axis}-{bp}` inherits the previous breakpoint's effective value so a
1421
+ partial responsive object cascades naturally.
1422
+
1423
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
1424
+ inline as literal values because `@container` conditions cannot resolve
1425
+ `var()`. Keep them in sync.
1426
+
1427
+ Fallback defaults:
1428
+ - direction: `column` — vertical stacks are the mobile-first default
1429
+ for a column-major layout primitive
1430
+ - gap: `--ps1ui-space-md` — matches Container's default padding scale
1431
+ - align: `normal` — the CSS box-alignment initial value; in a flex
1432
+ context, `normal` behaves as `stretch`, matching Flexbox's default
1433
+ - justify: `normal` — behaves as `flex-start` in a flex context
1434
+ - wrap: `nowrap` — the flex-wrap CSS initial value
1435
+ Each default matches the property's own initial-value behaviour so
1436
+ omitting the corresponding prop is equivalent to not writing the
1437
+ declaration at all. */
1438
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
1439
+ in components.css's shared selector list — see the block header there. */
1440
+ .ps1ui-stack {
1441
+ container: ps1ui-stack / inline-size;
1442
+ display: flex;
1443
+
1444
+ --_direction-base: var(--_stack-direction-base, column);
1445
+ --_direction-sm: var(--_stack-direction-sm, var(--_direction-base));
1446
+ --_direction-md: var(--_stack-direction-md, var(--_direction-sm));
1447
+ --_direction-lg: var(--_stack-direction-lg, var(--_direction-md));
1448
+ --_direction-xl: var(--_stack-direction-xl, var(--_direction-lg));
1449
+
1450
+ --_gap-base: var(--_stack-gap-base, var(--ps1ui-space-md));
1451
+ --_gap-sm: var(--_stack-gap-sm, var(--_gap-base));
1452
+ --_gap-md: var(--_stack-gap-md, var(--_gap-sm));
1453
+ --_gap-lg: var(--_stack-gap-lg, var(--_gap-md));
1454
+ --_gap-xl: var(--_stack-gap-xl, var(--_gap-lg));
1455
+
1456
+ --_align-base: var(--_stack-align-base, normal);
1457
+ --_align-sm: var(--_stack-align-sm, var(--_align-base));
1458
+ --_align-md: var(--_stack-align-md, var(--_align-sm));
1459
+ --_align-lg: var(--_stack-align-lg, var(--_align-md));
1460
+ --_align-xl: var(--_stack-align-xl, var(--_align-lg));
1461
+
1462
+ --_justify-base: var(--_stack-justify-base, normal);
1463
+ --_justify-sm: var(--_stack-justify-sm, var(--_justify-base));
1464
+ --_justify-md: var(--_stack-justify-md, var(--_justify-sm));
1465
+ --_justify-lg: var(--_stack-justify-lg, var(--_justify-md));
1466
+ --_justify-xl: var(--_stack-justify-xl, var(--_justify-lg));
1467
+
1468
+ --_wrap-base: var(--_stack-wrap-base, nowrap);
1469
+ --_wrap-sm: var(--_stack-wrap-sm, var(--_wrap-base));
1470
+ --_wrap-md: var(--_stack-wrap-md, var(--_wrap-sm));
1471
+ --_wrap-lg: var(--_stack-wrap-lg, var(--_wrap-md));
1472
+ --_wrap-xl: var(--_stack-wrap-xl, var(--_wrap-lg));
1473
+
1474
+ --_direction: var(--_direction-base);
1475
+ --_gap: var(--_gap-base);
1476
+ --_align: var(--_align-base);
1477
+ --_justify: var(--_justify-base);
1478
+ --_wrap: var(--_wrap-base);
1479
+
1480
+ flex-direction: var(--_direction);
1481
+ gap: var(--_gap);
1482
+ align-items: var(--_align);
1483
+ justify-content: var(--_justify);
1484
+ flex-wrap: var(--_wrap);
1485
+ }
1486
+ @container (min-width: 40rem) {
1487
+ .ps1ui-stack {
1488
+ --_direction: var(--_direction-sm);
1489
+ --_gap: var(--_gap-sm);
1490
+ --_align: var(--_align-sm);
1491
+ --_justify: var(--_justify-sm);
1492
+ --_wrap: var(--_wrap-sm);
1493
+ }
1494
+ }
1495
+ @container (min-width: 48rem) {
1496
+ .ps1ui-stack {
1497
+ --_direction: var(--_direction-md);
1498
+ --_gap: var(--_gap-md);
1499
+ --_align: var(--_align-md);
1500
+ --_justify: var(--_justify-md);
1501
+ --_wrap: var(--_wrap-md);
1502
+ }
1503
+ }
1504
+ @container (min-width: 64rem) {
1505
+ .ps1ui-stack {
1506
+ --_direction: var(--_direction-lg);
1507
+ --_gap: var(--_gap-lg);
1508
+ --_align: var(--_align-lg);
1509
+ --_justify: var(--_justify-lg);
1510
+ --_wrap: var(--_wrap-lg);
1511
+ }
1512
+ }
1513
+ @container (min-width: 80rem) {
1514
+ .ps1ui-stack {
1515
+ --_direction: var(--_direction-xl);
1516
+ --_gap: var(--_gap-xl);
1517
+ --_align: var(--_align-xl);
1518
+ --_justify: var(--_justify-xl);
1519
+ --_wrap: var(--_wrap-xl);
1520
+ }
1521
+ }
1522
+ /* Table — semantic <table> with box-drawing-style grid borders, wrapped in a
1523
+ horizontal-scroll container.
1524
+
1525
+ Scroll wrapper: `display: table` boxes don't honor overflow, so Table.tsx
1526
+ renders a dedicated `__scroller` div with `overflow-x: auto`. It follows
1527
+ CodeBlock's keyboard contract — tabIndex=0 only while the table actually
1528
+ overflows — and reuses the shared box-shadow focus ring, so
1529
+ `.ps1ui-table__scroller:focus-visible` is registered in the forced-colors
1530
+ grouped selector in styles/components.css (box-shadow is force-stripped in
1531
+ high-contrast mode; the grouped rule restores a real outline).
1532
+
1533
+ Grid borders: `border-collapse: collapse` + a uniform 1px cell border in
1534
+ --ps1ui-color-border draw the closed single-stroke grid a box-drawing
1535
+ table (┌─┬─┐) renders — collapsing is what merges adjacent
1536
+ cell edges into one shared rule. The header row separates on two axes:
1537
+ a surface background and a border-strong bottom rule. That rule is
1538
+ declared on the <th> (cell origin) deliberately — in border-collapse
1539
+ conflict resolution, equal-width equal-style borders from two adjacent
1540
+ cells resolve to the topmost cell's color, so the th's strong bottom edge
1541
+ wins over the first body row's plain top edge. Declaring it on the row or
1542
+ row group instead would lose (cell origin outranks both).
1543
+
1544
+ All rules target `.ps1ui-table <element>` selectors, not the
1545
+ ps1ui-thead/tbody/tr/th/td wrapper classes: a bare <thead>/<tr>/<th>/<td>
1546
+ inside <Table> must render identically to the wrapper components (same
1547
+ contract as `.ps1ui-list > li` vs ListItem). The wrapper classes are
1548
+ stable hooks only.
1549
+
1550
+ Self-sufficiency: `border-collapse` and the <th> font-weight/text-align
1551
+ neutralizations are declared here even though base.css's reset also
1552
+ collapses table borders — the components.css entry ships without the
1553
+ reset, and UA <th> defaults (bold, center) would leak through. */
1554
+ .ps1ui-table__scroller {
1555
+ overflow-x: auto;
1556
+ }
1557
+ .ps1ui-table__scroller:focus-visible {
1558
+ outline: none;
1559
+ box-shadow: var(--ps1ui-shadow-focus-ring);
1560
+ }
1561
+ /* No width override — the UA's shrink-to-fit keeps the grid hugging its
1562
+ content like a box-drawing table. Call sites that want a full-bleed table
1563
+ (e.g. a props table filling a docs column) opt in with `width: 100%`. */
1564
+ .ps1ui-table {
1565
+ border-collapse: collapse;
1566
+ color: var(--ps1ui-color-fg);
1567
+ font-family: var(--ps1ui-font-mono);
1568
+ font-size: var(--ps1ui-font-size-sm);
1569
+ line-height: 1.6;
1570
+ }
1571
+ .ps1ui-table th,
1572
+ .ps1ui-table td {
1573
+ border: 1px solid var(--ps1ui-color-border);
1574
+ padding: var(--ps1ui-space-sm) var(--ps1ui-space-md);
1575
+ /* `start` (not `left`): tables carry natural-language content and should
1576
+ mirror in RTL contexts — unlike CodeBlock, which pins `left` because
1577
+ source code stays LTR. Explicit on both cell kinds because the UA
1578
+ default for <th> is center. */
1579
+ text-align: start;
1580
+ /* Multi-line cells align their first lines instead of drifting toward
1581
+ vertical centers of unequal-height neighbors. */
1582
+ vertical-align: top;
1583
+ }
1584
+ .ps1ui-table th {
1585
+ background: var(--ps1ui-color-surface);
1586
+ font-weight: 600;
1587
+ }
1588
+ .ps1ui-table thead th {
1589
+ border-bottom-color: var(--ps1ui-color-border-strong);
1590
+ }
1591
+ /* Thead — no rules of its own.
1592
+
1593
+ The `.ps1ui-thead` class is a stable API hook for consumers. All header
1594
+ rendering (surface background, strong bottom rule) lives on
1595
+ `.ps1ui-table thead th` in Table.css so a bare <thead> inside <Table>
1596
+ continues to render identically to <Thead>. Keeping this file present
1597
+ (and imported from components.css) matches the per-component file layout
1598
+ the `new-component` skill enforces — same precedent as ListItem.css. */
1599
+ /* Tbody — no rules of its own.
1600
+
1601
+ The `.ps1ui-tbody` class is a stable API hook for consumers. All cell
1602
+ rendering lives on `.ps1ui-table th / td` in Table.css so a bare <tbody>
1603
+ inside <Table> continues to render identically to <Tbody>. Keeping this
1604
+ file present (and imported from components.css) matches the per-component
1605
+ file layout the `new-component` skill enforces — same precedent as
1606
+ ListItem.css. */
1607
+ /* Tr — no rules of its own.
1608
+
1609
+ The `.ps1ui-tr` class is a stable API hook for consumers. All cell
1610
+ rendering lives on `.ps1ui-table th / td` in Table.css so a bare <tr>
1611
+ inside <Table> continues to render identically to <Tr>. Keeping this file
1612
+ present (and imported from components.css) matches the per-component file
1613
+ layout the `new-component` skill enforces — same precedent as
1614
+ ListItem.css. */
1615
+ /* Th — no rules of its own.
1616
+
1617
+ The `.ps1ui-th` class is a stable API hook for consumers. All header-cell
1618
+ rendering (grid border, padding, surface background, weight) lives on
1619
+ `.ps1ui-table th` in Table.css so a bare <th> inside <Table> continues to
1620
+ render identically to <Th>. Keeping this file present (and imported from
1621
+ components.css) matches the per-component file layout the `new-component`
1622
+ skill enforces — same precedent as ListItem.css. */
1623
+ /* Td — no rules of its own.
1624
+
1625
+ The `.ps1ui-td` class is a stable API hook for consumers. All data-cell
1626
+ rendering (grid border, padding, alignment) lives on `.ps1ui-table td` in
1627
+ Table.css so a bare <td> inside <Table> continues to render identically to
1628
+ <Td>. Keeping this file present (and imported from components.css) matches
1629
+ the per-component file layout the `new-component` skill enforces — same
1630
+ precedent as ListItem.css. */
1631
+ /* Input custom properties emitted by Text.tsx per set responsive-prop
1632
+ breakpoint (`--_text-<axis>-<bp>`). Registered as non-inheriting for the
1633
+ same reason as Stack — see Stack.css's @property block for the full
1634
+ rationale. */
1635
+ @property --_text-size-base {
1636
+ syntax: "*";
1637
+ inherits: false;
1638
+ }
1639
+ @property --_text-size-sm {
1640
+ syntax: "*";
1641
+ inherits: false;
1642
+ }
1643
+ @property --_text-size-md {
1644
+ syntax: "*";
1645
+ inherits: false;
1646
+ }
1647
+ @property --_text-size-lg {
1648
+ syntax: "*";
1649
+ inherits: false;
1650
+ }
1651
+ @property --_text-size-xl {
1652
+ syntax: "*";
1653
+ inherits: false;
1654
+ }
1655
+ @property --_text-weight-base {
1656
+ syntax: "*";
1657
+ inherits: false;
1658
+ }
1659
+ @property --_text-weight-sm {
1660
+ syntax: "*";
1661
+ inherits: false;
1662
+ }
1663
+ @property --_text-weight-md {
1664
+ syntax: "*";
1665
+ inherits: false;
1666
+ }
1667
+ @property --_text-weight-lg {
1668
+ syntax: "*";
1669
+ inherits: false;
1670
+ }
1671
+ @property --_text-weight-xl {
1672
+ syntax: "*";
1673
+ inherits: false;
1674
+ }
1675
+ /* Text — typography primitive.
1676
+ Text is a leaf component: it queries an ancestor containment context (a
1677
+ PS1Root / Container / Grid / Stack), but does NOT establish one itself.
1678
+ Establishing containment on every Text would fragment the query surface
1679
+ for descendants and impose a `contain: layout` side effect on every
1680
+ paragraph — needless overhead for a component that carries no children
1681
+ worth measuring.
1682
+
1683
+ Responsive values propagate via a CSS custom-property cascade. The
1684
+ component emits `--_text-size-{bp}` / `--_text-weight-{bp}` via inline
1685
+ style. Each `@container (min-width: X)` block below overrides `--_size`
1686
+ / `--_weight` with the value for the matching breakpoint; the fallback
1687
+ chain in `--_size-{bp}` inherits the previous breakpoint's effective
1688
+ value so a partial responsive object cascades naturally.
1689
+
1690
+ `weight` fallback is `initial` (= normal / 400). Consumers who want bold
1691
+ text pass `weight="bold"` explicitly; a bare `<Text as="strong">` renders
1692
+ as normal weight (Text is the source of truth for its own typography,
1693
+ NOT the UA `<strong>` style). Users who need the UA-strong appearance
1694
+ should pair `as="strong"` with `weight="bold"` for consistency across
1695
+ engines. */
1696
+ .ps1ui-text {
1697
+ margin: 0;
1698
+ font-family: var(--ps1ui-font-mono);
1699
+ line-height: 1.6;
1700
+
1701
+ --_size-base: var(--_text-size-base, var(--ps1ui-font-size-sm));
1702
+ --_size-sm: var(--_text-size-sm, var(--_size-base));
1703
+ --_size-md: var(--_text-size-md, var(--_size-sm));
1704
+ --_size-lg: var(--_text-size-lg, var(--_size-md));
1705
+ --_size-xl: var(--_text-size-xl, var(--_size-lg));
1706
+
1707
+ --_weight-base: var(--_text-weight-base, initial);
1708
+ --_weight-sm: var(--_text-weight-sm, var(--_weight-base));
1709
+ --_weight-md: var(--_text-weight-md, var(--_weight-sm));
1710
+ --_weight-lg: var(--_text-weight-lg, var(--_weight-md));
1711
+ --_weight-xl: var(--_text-weight-xl, var(--_weight-lg));
1712
+
1713
+ --_size: var(--_size-base);
1714
+ --_weight: var(--_weight-base);
1715
+
1716
+ font-size: var(--_size);
1717
+ font-weight: var(--_weight);
1718
+ }
1719
+ @container (min-width: 40rem) {
1720
+ .ps1ui-text {
1721
+ --_size: var(--_size-sm);
1722
+ --_weight: var(--_weight-sm);
1723
+ }
1724
+ }
1725
+ @container (min-width: 48rem) {
1726
+ .ps1ui-text {
1727
+ --_size: var(--_size-md);
1728
+ --_weight: var(--_weight-md);
1729
+ }
1730
+ }
1731
+ @container (min-width: 64rem) {
1732
+ .ps1ui-text {
1733
+ --_size: var(--_size-lg);
1734
+ --_weight: var(--_weight-lg);
1735
+ }
1736
+ }
1737
+ @container (min-width: 80rem) {
1738
+ .ps1ui-text {
1739
+ --_size: var(--_size-xl);
1740
+ --_weight: var(--_weight-xl);
1741
+ }
1742
+ }
1743
+ .ps1ui-text--truncate {
1744
+ overflow: hidden;
1745
+ white-space: nowrap;
1746
+ text-overflow: ellipsis;
1747
+ max-width: 100%;
1748
+ }
1749
+ .ps1ui-text--truncate-inline {
1750
+ display: inline-block;
1751
+ }
1752
+ .ps1ui-text--body {
1753
+ color: var(--ps1ui-color-fg);
1754
+ }
1755
+ .ps1ui-text--muted {
1756
+ color: var(--ps1ui-color-fg-muted);
1757
+ }
1758
+ .ps1ui-text--subtle {
1759
+ color: var(--ps1ui-color-fg-subtle);
1760
+ }
1761
+ .ps1ui-text--primary {
1762
+ color: var(--ps1ui-color-primary);
1763
+ }
1764
+ .ps1ui-text--accent {
1765
+ color: var(--ps1ui-color-accent);
1766
+ }
1767
+ /* Input custom properties emitted by Heading.tsx per set responsive-prop
1768
+ breakpoint (`--_heading-<axis>-<bp>`). Registered as non-inheriting for
1769
+ the same reason as Stack — see Stack.css's @property block for the full
1770
+ rationale. */
1771
+ @property --_heading-size-base {
1772
+ syntax: "*";
1773
+ inherits: false;
1774
+ }
1775
+ @property --_heading-size-sm {
1776
+ syntax: "*";
1777
+ inherits: false;
1778
+ }
1779
+ @property --_heading-size-md {
1780
+ syntax: "*";
1781
+ inherits: false;
1782
+ }
1783
+ @property --_heading-size-lg {
1784
+ syntax: "*";
1785
+ inherits: false;
1786
+ }
1787
+ @property --_heading-size-xl {
1788
+ syntax: "*";
1789
+ inherits: false;
1790
+ }
1791
+ @property --_heading-weight-base {
1792
+ syntax: "*";
1793
+ inherits: false;
1794
+ }
1795
+ @property --_heading-weight-sm {
1796
+ syntax: "*";
1797
+ inherits: false;
1798
+ }
1799
+ @property --_heading-weight-md {
1800
+ syntax: "*";
1801
+ inherits: false;
1802
+ }
1803
+ @property --_heading-weight-lg {
1804
+ syntax: "*";
1805
+ inherits: false;
1806
+ }
1807
+ @property --_heading-weight-xl {
1808
+ syntax: "*";
1809
+ inherits: false;
1810
+ }
1811
+ /* Heading — semantic heading primitive with level-driven typography.
1812
+ Like Text, Heading is a leaf: it queries an ancestor containment context
1813
+ but does not establish one itself. See Text.css for the rationale.
1814
+
1815
+ Responsive values propagate via a CSS custom-property cascade. The
1816
+ component emits `--_heading-size-{bp}` / `--_heading-weight-{bp}` via
1817
+ inline style, always at least at `-base` (Heading.tsx normalizes the
1818
+ level's default into the base breakpoint if the caller didn't supply
1819
+ one). Each `@container (min-width: X)` block below overrides `--_size`
1820
+ / `--_weight` for the matching breakpoint; the fallback chain inherits
1821
+ the previous breakpoint's effective value so a partial responsive object
1822
+ cascades naturally.
1823
+
1824
+ Heading.tsx contract: `--_heading-size-base` and `--_heading-weight-base`
1825
+ are ALWAYS emitted via inline style (see Heading.tsx — `size` and
1826
+ `weight` both pass through `withResponsiveBase(..., LEVEL_DEFAULTS[level].
1827
+ size|weight)`, which guarantees the `base` breakpoint is populated for
1828
+ every render). The `--_size-base` / `--_weight-base` fallback branches
1829
+ below are therefore defensive floors only — they should never be reached
1830
+ in normal rendering. If Heading.tsx ever changes to NOT normalize the
1831
+ base breakpoint, these floors keep the component in a legible default
1832
+ (font-size sm, font-weight normal). Text.css uses a live fallback path
1833
+ because Text.tsx does not normalize its size/weight props. */
1834
+ .ps1ui-heading {
1835
+ margin: 0;
1836
+ font-family: var(--ps1ui-font-mono);
1837
+ color: var(--ps1ui-color-fg);
1838
+ line-height: 1.3;
1839
+
1840
+ /* Defensive-floor fallbacks. Never reached under normal render — see
1841
+ the comment above. Kept as font-size sm / weight normal so a
1842
+ bypassed Heading.tsx contract still yields legible output. */
1843
+ --_size-base: var(--_heading-size-base, var(--ps1ui-font-size-sm));
1844
+ --_size-sm: var(--_heading-size-sm, var(--_size-base));
1845
+ --_size-md: var(--_heading-size-md, var(--_size-sm));
1846
+ --_size-lg: var(--_heading-size-lg, var(--_size-md));
1847
+ --_size-xl: var(--_heading-size-xl, var(--_size-lg));
1848
+
1849
+ --_weight-base: var(--_heading-weight-base, normal);
1850
+ --_weight-sm: var(--_heading-weight-sm, var(--_weight-base));
1851
+ --_weight-md: var(--_heading-weight-md, var(--_weight-sm));
1852
+ --_weight-lg: var(--_heading-weight-lg, var(--_weight-md));
1853
+ --_weight-xl: var(--_heading-weight-xl, var(--_weight-lg));
1854
+
1855
+ --_size: var(--_size-base);
1856
+ --_weight: var(--_weight-base);
1857
+
1858
+ font-size: var(--_size);
1859
+ font-weight: var(--_weight);
1860
+ }
1861
+ @container (min-width: 40rem) {
1862
+ .ps1ui-heading {
1863
+ --_size: var(--_size-sm);
1864
+ --_weight: var(--_weight-sm);
1865
+ }
1866
+ }
1867
+ @container (min-width: 48rem) {
1868
+ .ps1ui-heading {
1869
+ --_size: var(--_size-md);
1870
+ --_weight: var(--_weight-md);
1871
+ }
1872
+ }
1873
+ @container (min-width: 64rem) {
1874
+ .ps1ui-heading {
1875
+ --_size: var(--_size-lg);
1876
+ --_weight: var(--_weight-lg);
1877
+ }
1878
+ }
1879
+ @container (min-width: 80rem) {
1880
+ .ps1ui-heading {
1881
+ --_size: var(--_size-xl);
1882
+ --_weight: var(--_weight-xl);
1883
+ }
1884
+ }
1885
+ /* Containment side-effect defense — applied uniformly to every primitive that
1886
+ establishes a `container-type: inline-size` context (i.e. every layout
1887
+ primitive whose responsive props drive `@container` queries).
1888
+
1889
+ `container-type: inline-size` implies `contain: inline-size`, which treats
1890
+ the element's intrinsic inline-size as 0 for parent sizing. Placing any of
1891
+ these primitives inside a shrink-to-fit flex/grid parent (`align-items:
1892
+ flex-start`, `justify-items: start`, etc.) would otherwise collapse them to
1893
+ 0 wide, silently degrading every `@container`-driven descendant.
1894
+
1895
+ `align-self: stretch` and `justify-self: stretch` force fill-cross-axis /
1896
+ fill-inline-track in flex and grid contexts respectively (both no-ops in
1897
+ block context). `min-width: 0` lets the primitive shrink below content-min
1898
+ inside a narrow row parent so inner overflow-scroll surfaces (CodeBlock)
1899
+ take over instead of bursting the row open.
1900
+
1901
+ Adding a new responsive-container primitive means adding its class to this
1902
+ selector list — that's the load-bearing invariant. Per-primitive regression
1903
+ tests (Stack.test.tsx / Container.test.tsx / Grid.test.tsx / PS1Root.test.tsx's
1904
+ "resists collapse via align-self: stretch in shrink-wrap flex parent") catch
1905
+ accidental removal. */
1906
+ .ps1ui-root,
1907
+ .ps1ui-container,
1908
+ .ps1ui-grid,
1909
+ .ps1ui-stack {
1910
+ align-self: stretch;
1911
+ justify-self: stretch;
1912
+ min-width: 0;
1913
+ }
1914
+ /* forced-colors (Windows High Contrast) strips box-shadow, so every component
1915
+ whose focus ring is box-shadow-based needs a real outline there — one rule,
1916
+ one owner, same precedent as the containment grouped selector above. Add new
1917
+ box-shadow-focus components to this list. Component-specific forced-colors
1918
+ adjustments (e.g. Checkbox's indeterminate bar) stay in their own CSS.
1919
+ Tested in src/styles/forced-colors.test.tsx. */
1920
+ @media (forced-colors: active) {
1921
+ .ps1ui-button:focus-visible,
1922
+ .ps1ui-checkbox:focus-visible,
1923
+ .ps1ui-codeblock:focus-visible,
1924
+ .ps1ui-details__summary:focus-visible,
1925
+ .ps1ui-input:focus,
1926
+ .ps1ui-table__scroller:focus-visible {
1927
+ outline: 2px solid CanvasText;
1928
+ outline-offset: 2px;
1929
+ }
1930
+ }