@godxjp/ui 18.5.0 → 18.7.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 (101) hide show
  1. package/dist/components/charts/compact-bar-trend.d.ts +2 -1
  2. package/dist/components/charts/index.d.ts +4 -1
  3. package/dist/components/data-display/data-table.d.ts +25 -4
  4. package/dist/components/data-display/data-table.js +13 -2
  5. package/dist/components/data-display/empty-state.js +1 -1
  6. package/dist/components/data-display/index.d.ts +1 -1
  7. package/dist/components/data-display/list-row.d.ts +19 -0
  8. package/dist/components/data-display/list-row.js +2 -0
  9. package/dist/components/data-display/qr-code.d.ts +1 -1
  10. package/dist/components/data-display/table.d.ts +47 -2
  11. package/dist/components/data-display/table.js +29 -4
  12. package/dist/components/data-entry/command.d.ts +10 -10
  13. package/dist/components/data-entry/index.d.ts +1 -0
  14. package/dist/components/data-entry/input-otp.d.ts +1 -0
  15. package/dist/components/feedback/alert.d.ts +20 -2
  16. package/dist/components/feedback/alert.js +11 -1
  17. package/dist/components/feedback/dialog.d.ts +14 -1
  18. package/dist/components/feedback/dialog.js +4 -0
  19. package/dist/components/feedback/index.d.ts +3 -3
  20. package/dist/components/feedback/index.js +5 -1
  21. package/dist/components/general/logo.d.ts +7 -1
  22. package/dist/components/general/logo.js +1 -1
  23. package/dist/components/layout/app-shell.d.ts +1 -1
  24. package/dist/components/layout/app-shell.js +55 -46
  25. package/dist/components/layout/auth-account-summary.d.ts +5 -0
  26. package/dist/components/layout/auth-account-summary.js +31 -0
  27. package/dist/components/layout/auth-shell.d.ts +5 -2
  28. package/dist/components/layout/centered-shell.d.ts +1 -1
  29. package/dist/components/layout/centered-shell.js +28 -19
  30. package/dist/components/layout/error-surface.d.ts +35 -0
  31. package/dist/components/layout/error-surface.js +155 -0
  32. package/dist/components/layout/flex.d.ts +1 -1
  33. package/dist/components/layout/flex.js +4 -0
  34. package/dist/components/layout/index.d.ts +7 -0
  35. package/dist/components/layout/index.js +6 -0
  36. package/dist/components/layout/page-container.d.ts +1 -1
  37. package/dist/components/layout/page-container.js +23 -41
  38. package/dist/components/layout/page-header.d.ts +22 -0
  39. package/dist/components/layout/page-header.js +89 -0
  40. package/dist/components/navigation/filter-bar.d.ts +2 -2
  41. package/dist/components/navigation/filter-bar.js +44 -1
  42. package/dist/components/navigation/index.d.ts +1 -1
  43. package/dist/components/navigation/steps.js +38 -19
  44. package/dist/components/navigation/tabs.js +12 -3
  45. package/dist/components/ui/avatar.d.ts +2 -1
  46. package/dist/components/ui/avatar.js +2 -1
  47. package/dist/components/ui/input-otp.d.ts +4 -1
  48. package/dist/components/ui/input-otp.js +2 -1
  49. package/dist/components/ui/toggle-group.d.ts +7 -4
  50. package/dist/components/ui/toggle-group.js +32 -21
  51. package/dist/email/brand-mark.d.ts +21 -5
  52. package/dist/email/brand-mark.js +5 -0
  53. package/dist/email/color.d.ts +2 -1
  54. package/dist/email/geometry.d.ts +9 -1
  55. package/dist/email/geometry.js +1 -0
  56. package/dist/email/index.d.ts +6 -1
  57. package/dist/email/index.js +6 -0
  58. package/dist/email/inline-style.d.ts +7 -5
  59. package/dist/email/inline-style.js +5 -0
  60. package/dist/email/tokens.generated.d.ts +40 -20
  61. package/dist/email/tokens.generated.js +40 -20
  62. package/dist/email/urgency.d.ts +22 -0
  63. package/dist/email/urgency.js +14 -0
  64. package/dist/i18n/messages/en.json +26 -1
  65. package/dist/i18n/messages/ja.json +23 -1
  66. package/dist/i18n/messages/vi.json +23 -1
  67. package/dist/props/components/data-display.prop.d.ts +63 -1
  68. package/dist/props/components/data-entry.prop.d.ts +6 -0
  69. package/dist/props/components/feedback.prop.d.ts +6 -0
  70. package/dist/props/components/index.d.ts +1 -1
  71. package/dist/props/components/layout.prop.d.ts +244 -4
  72. package/dist/props/components/navigation.prop.d.ts +57 -1
  73. package/dist/props/registry.d.ts +205 -5
  74. package/dist/props/registry.js +270 -6
  75. package/dist/props/vocabulary/data.prop.d.ts +27 -0
  76. package/dist/props/vocabulary/index.d.ts +3 -3
  77. package/dist/props/vocabulary/interaction.prop.d.ts +19 -2
  78. package/dist/props/vocabulary/layout.prop.d.ts +41 -7
  79. package/dist/styles/alert-layout.css +36 -0
  80. package/dist/styles/control.css +28 -0
  81. package/dist/styles/data-display-layout.css +26 -0
  82. package/dist/styles/layout.css +212 -0
  83. package/dist/styles/logo-layout.css +42 -9
  84. package/dist/styles/navigation-layout.css +109 -0
  85. package/dist/styles/shell-layout.css +336 -0
  86. package/dist/styles/table-layout.css +164 -1
  87. package/dist/theme/dxs.canonical.css +5 -4
  88. package/dist/tokens/axes.css +4 -3
  89. package/dist/tokens/base.css +1 -0
  90. package/dist/tokens/components/data-display.css +13 -0
  91. package/dist/tokens/components/email.css +43 -16
  92. package/dist/tokens/components/error-surface.css +36 -0
  93. package/dist/tokens/components/feedback.css +20 -0
  94. package/dist/tokens/components/list-row.css +20 -0
  95. package/dist/tokens/components/logo.css +52 -10
  96. package/dist/tokens/components/navigation.css +41 -0
  97. package/dist/tokens/components/shell.css +109 -0
  98. package/dist/tokens/components/table.css +45 -0
  99. package/dist/tokens/foundation.css +26 -0
  100. package/dist/tokens/semantic/layout.css +38 -0
  101. package/package.json +8 -2
@@ -49,6 +49,45 @@
49
49
  scrollbar-width: thin;
50
50
  }
51
51
 
52
+ /* Tabs `line` — the ACTIVE decoration is a token-owned bar and nothing else (gh#248). It is
53
+ * deliberately painted here (not as an `after:*` utility) so it stays independent of the
54
+ * `:focus-visible` ring utilities: "selected" and "keyboard-focused" are two different states and
55
+ * must remain visually distinct (WCAG 2.4.7). Logical insets keep the vertical rail on the
56
+ * inline-END edge under dir="rtl" too. */
57
+ [data-slot="tabs-list"][data-variant="line"] [data-slot="tabs-trigger"]::after {
58
+ content: "";
59
+ position: absolute;
60
+ background: var(--tabs-indicator-background, hsl(var(--primary)));
61
+ opacity: 0;
62
+ transition: opacity var(--duration-fast) var(--ease-standard);
63
+ pointer-events: none;
64
+ }
65
+
66
+ [data-slot="tabs-list"][data-variant="line"][data-orientation="horizontal"]
67
+ [data-slot="tabs-trigger"]::after {
68
+ inset-inline: 0;
69
+ inset-block-end: calc(-1 * var(--tabs-indicator-offset));
70
+ block-size: var(--tabs-indicator-size);
71
+ }
72
+
73
+ [data-slot="tabs-list"][data-variant="line"][data-orientation="vertical"]
74
+ [data-slot="tabs-trigger"]::after {
75
+ inset-block: 0;
76
+ inset-inline-end: calc(-1 * var(--tabs-indicator-offset));
77
+ inline-size: var(--tabs-indicator-size);
78
+ }
79
+
80
+ [data-slot="tabs-list"][data-variant="line"]
81
+ [data-slot="tabs-trigger"][data-state="active"]::after {
82
+ opacity: 1;
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ [data-slot="tabs-list"][data-variant="line"] [data-slot="tabs-trigger"]::after {
87
+ transition: none;
88
+ }
89
+ }
90
+
52
91
  .ui-context-menu-content {
53
92
  z-index: 50;
54
93
  min-width: 10rem;
@@ -320,3 +359,73 @@
320
359
  transform-origin: center;
321
360
  }
322
361
  }
362
+
363
+ @layer components {
364
+ .ui-steps-inline {
365
+ display: flex;
366
+ width: 100%;
367
+ flex-wrap: wrap;
368
+ align-items: center;
369
+ justify-content: center;
370
+ gap: var(--steps-inline-gap);
371
+ font-size: var(--steps-inline-font-size);
372
+ line-height: var(--line-height-tight);
373
+ }
374
+
375
+ .ui-steps-inline-item {
376
+ display: inline-flex;
377
+ min-width: 0;
378
+ flex: 0 1 auto;
379
+ align-items: center;
380
+ gap: var(--steps-inline-gap);
381
+ text-align: start;
382
+ }
383
+
384
+ .ui-steps-inline-control {
385
+ display: inline-flex;
386
+ min-width: 0;
387
+ align-items: center;
388
+ gap: var(--steps-inline-item-gap);
389
+ border-radius: var(--radius-sm);
390
+ outline: none;
391
+ }
392
+
393
+ button.ui-steps-inline-control:focus-visible {
394
+ box-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--focus-ring-color, var(--ring)));
395
+ }
396
+
397
+ .ui-steps-inline-index {
398
+ flex: none;
399
+ font-variant-numeric: tabular-nums;
400
+ font-weight: var(--font-weight-bold);
401
+ }
402
+
403
+ .ui-steps-inline-title {
404
+ min-width: 0;
405
+ overflow-wrap: anywhere;
406
+ }
407
+
408
+ .ui-steps-inline-item[data-status="finish"] .ui-steps-inline-control {
409
+ color: hsl(var(--primary));
410
+ }
411
+
412
+ .ui-steps-inline-item[data-status="process"] .ui-steps-inline-control {
413
+ color: hsl(var(--foreground));
414
+ font-weight: var(--font-weight-medium);
415
+ }
416
+
417
+ .ui-steps-inline-item[data-status="error"] .ui-steps-inline-control {
418
+ color: hsl(var(--destructive));
419
+ }
420
+
421
+ .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control,
422
+ .ui-steps-inline-separator {
423
+ color: hsl(var(--muted-foreground));
424
+ }
425
+
426
+ .ui-steps-inline-separator {
427
+ width: var(--steps-inline-separator-size);
428
+ height: var(--steps-inline-separator-size);
429
+ flex: none;
430
+ }
431
+ }
@@ -147,6 +147,51 @@
147
147
  gap: var(--auth-shell-card-stack-gap);
148
148
  }
149
149
 
150
+ /* Login is an anchored three-part composition: AuthIdentity · Card · AuthFooter. The fixed
151
+ * identity track absorbs absent, one-line and wrapped two-line requester states while its
152
+ * content remains visually adjacent to the Card. This keeps real requester data intact without
153
+ * allowing its line count to move the canonical SCR-001 card anchor (gh#237). */
154
+ .ui-auth-shell[data-preset="login"] {
155
+ --auth-shell-card-max-width: var(--auth-shell-login-card-max-width);
156
+ --auth-shell-main-align: flex-start;
157
+ --auth-shell-main-padding: var(--auth-shell-login-flow-offset-block)
158
+ var(--auth-shell-login-main-padding-inline) var(--auth-shell-login-main-padding-block-end);
159
+ --auth-shell-card-stack-gap: var(--auth-shell-login-card-stack-gap);
160
+ --auth-shell-card-padding-block-compact: var(--auth-shell-login-card-padding-block-compact);
161
+ }
162
+
163
+ .ui-auth-shell[data-preset="login"] .ui-auth-shell-card {
164
+ display: grid;
165
+ grid-template-rows: var(--auth-shell-login-identity-slot-block-size) auto auto;
166
+ }
167
+
168
+ .ui-auth-shell[data-preset="login"] .ui-auth-shell-card > .ui-auth-identity,
169
+ .ui-auth-shell[data-preset="login"] .ui-auth-shell-card > :has(> .ui-auth-identity) {
170
+ min-width: 0;
171
+ min-height: 0;
172
+ block-size: var(--auth-shell-login-identity-slot-block-size);
173
+ }
174
+
175
+ .ui-auth-shell[data-preset="login"] .ui-auth-shell-card > .ui-auth-identity,
176
+ .ui-auth-shell[data-preset="login"]
177
+ .ui-auth-shell-card
178
+ > :has(> .ui-auth-identity)
179
+ > .ui-auth-identity {
180
+ height: 100%;
181
+ justify-content: flex-end;
182
+ }
183
+
184
+ .ui-auth-shell[data-preset="login"] .ui-auth-requester {
185
+ max-width: 100%;
186
+ justify-content: center;
187
+ }
188
+
189
+ .ui-auth-shell[data-preset="login"] .ui-auth-requester > :last-child {
190
+ min-width: 0;
191
+ overflow-wrap: anywhere;
192
+ text-align: center;
193
+ }
194
+
150
195
  .ui-auth-shell[data-preset="device-authorization"] {
151
196
  --auth-shell-card-max-width: var(--auth-shell-device-card-max-width);
152
197
  --auth-shell-main-padding: var(--auth-shell-device-main-padding);
@@ -167,6 +212,30 @@
167
212
  --auth-shell-main-padding: var(--auth-shell-recovery-main-padding);
168
213
  }
169
214
 
215
+ /* Registration preset (gh#256) — the sign-up measure. `flex-start` is the load-bearing line: a
216
+ * registration card is the tallest surface in the hosted-identity set, and a vertically CENTRED
217
+ * tall card overflows ABOVE the scroll origin on a short viewport, putting its first field out of
218
+ * reach. Start-aligned, the block-start inset is ordinary page padding the user scrolls past, and
219
+ * the block-end inset is the footer clearance the form keeps at the end of a long scroll. */
220
+ .ui-auth-shell[data-preset="registration"] {
221
+ --auth-shell-card-max-width: var(--auth-shell-registration-card-max-width);
222
+ --auth-shell-main-align: flex-start;
223
+ --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start)
224
+ var(--auth-shell-registration-main-padding-inline)
225
+ var(--auth-shell-registration-main-padding-block-end);
226
+ --auth-shell-card-stack-gap: var(--auth-shell-registration-card-stack-gap);
227
+ }
228
+
229
+ /* Fixed identity track — the mechanism `login` proved (gh#237), applied to the identity block
230
+ * wherever it sits in the column rather than through a grid row, so the preset does not constrain
231
+ * how many sections the page stacks. Content aligns to the slot END, so absent / one-line /
232
+ * wrapped two-line identity copy changes only the empty space ABOVE it and never moves the card
233
+ * off its canonical anchor. */
234
+ .ui-auth-shell[data-preset="registration"] .ui-auth-shell-card > .ui-auth-identity {
235
+ block-size: var(--auth-shell-registration-identity-slot-block-size);
236
+ justify-content: flex-end;
237
+ }
238
+
170
239
  .ui-auth-identity {
171
240
  display: flex;
172
241
  flex-direction: column;
@@ -197,6 +266,52 @@
197
266
  height: var(--auth-requester-glyph-size);
198
267
  }
199
268
 
269
+ .ui-auth-account-summary {
270
+ display: flex;
271
+ min-height: var(--auth-account-summary-min-height);
272
+ flex-wrap: wrap;
273
+ align-items: center;
274
+ gap: var(--auth-account-summary-gap);
275
+ padding: var(--auth-account-summary-padding);
276
+ border-radius: var(--radius-md);
277
+ background: hsl(var(--muted));
278
+ }
279
+
280
+ .ui-auth-account-identity {
281
+ display: flex;
282
+ min-width: min(100%, var(--auth-account-summary-identity-min-width));
283
+ flex: 1 1 var(--auth-account-summary-identity-min-width);
284
+ align-items: center;
285
+ gap: var(--auth-account-summary-gap);
286
+ }
287
+
288
+ .ui-auth-account-avatar {
289
+ width: var(--auth-account-summary-avatar-size);
290
+ height: var(--auth-account-summary-avatar-size);
291
+ flex: none;
292
+ }
293
+
294
+ .ui-auth-account-fallback-icon {
295
+ width: var(--auth-account-summary-avatar-glyph-size);
296
+ height: var(--auth-account-summary-avatar-glyph-size);
297
+ }
298
+
299
+ .ui-auth-account-email {
300
+ min-width: 0;
301
+ overflow: hidden;
302
+ color: hsl(var(--foreground));
303
+ font-size: var(--auth-account-summary-email-font-size);
304
+ /* Declares its own line box because it CLIPS — see gh#254. */
305
+ line-height: var(--auth-account-summary-email-line-height);
306
+ font-weight: var(--font-weight-medium);
307
+ text-overflow: ellipsis;
308
+ white-space: nowrap;
309
+ }
310
+
311
+ .ui-auth-account-summary > [data-slot="button"] {
312
+ margin-inline-start: auto;
313
+ }
314
+
200
315
  .ui-auth-legal-footer {
201
316
  display: flex;
202
317
  flex-wrap: wrap;
@@ -277,6 +392,12 @@
277
392
  --auth-shell-main-padding: var(--auth-shell-canonical-main-padding-mobile);
278
393
  }
279
394
 
395
+ .ui-auth-shell[data-preset="login"] {
396
+ --auth-shell-main-padding: var(--auth-shell-login-flow-offset-block-mobile)
397
+ var(--auth-shell-login-main-padding-inline-mobile)
398
+ var(--auth-shell-login-main-padding-block-end);
399
+ }
400
+
280
401
  /* Declared after the canonical mobile inset so a preset's mobile gutter wins at equal
281
402
  * specificity: device-authorization = 5px inline (card x=5, width=380 at 390px);
282
403
  * context-selection = edge-to-edge (0 inline). */
@@ -294,6 +415,16 @@
294
415
  .ui-auth-shell[data-preset="account-recovery"] {
295
416
  --auth-shell-main-padding: var(--auth-shell-recovery-main-padding-mobile);
296
417
  }
418
+
419
+ /* registration = 15px inline at 390 (card x=15, width=360 — the same page rhythm as Login, so
420
+ * sign-in → sign-up never jumps on a phone). The block insets tighten but the footer clearance
421
+ * stays deliberately larger than the block-start inset: at 390 the form is at its tallest and
422
+ * the consent/legal footer must not sit flush against the submit button. */
423
+ .ui-auth-shell[data-preset="registration"] {
424
+ --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start-mobile)
425
+ var(--auth-shell-registration-main-padding-inline-mobile)
426
+ var(--auth-shell-registration-main-padding-block-end-mobile);
427
+ }
297
428
  }
298
429
 
299
430
  /*
@@ -479,6 +610,33 @@
479
610
  .app-mobile-nav-trigger {
480
611
  display: inline-flex;
481
612
  }
613
+
614
+ /* Some approved application contracts retain their navigation rail at narrow widths. This is
615
+ * an explicit AppShell mode, never a consumer media-query fork: the same sidebar node, footer,
616
+ * active items and token-owned rail width remain in the canonical two-track grid. */
617
+ .app-root[data-responsive-navigation="docked"] {
618
+ grid-template-areas:
619
+ "sidebar topbar"
620
+ "sidebar main"
621
+ "sidebar footer";
622
+ grid-template-columns: var(--app-shell-sidebar-width) minmax(0, 1fr);
623
+ }
624
+
625
+ .app-root[data-responsive-navigation="docked"][data-collapsed="true"] {
626
+ grid-template-columns: var(--app-shell-rail-width) minmax(0, 1fr);
627
+ }
628
+
629
+ .app-root[data-responsive-navigation="docked"] > .app-sidebar {
630
+ display: flex;
631
+ }
632
+
633
+ .app-root[data-responsive-navigation="docked"] > .app-topbar {
634
+ padding-inline: var(--app-shell-bar-inset);
635
+ }
636
+
637
+ .app-root[data-responsive-navigation="docked"] .app-mobile-nav-trigger {
638
+ display: none;
639
+ }
482
640
  }
483
641
 
484
642
  .sb-root {
@@ -781,6 +939,10 @@
781
939
  min-width: 0;
782
940
  flex: 1;
783
941
  overflow: hidden;
942
+ /* gh#254 — MUST NOT inherit the row's `line-height: 1`. This element clips, so its line box is
943
+ * its clip box, and a 1em box is shorter than the font's ascent+descent: every descender and
944
+ * Vietnamese tone mark below the baseline gets sheared off. Knob, not constant (rule #45). */
945
+ line-height: var(--sidebar-nav-item-line-height);
784
946
  text-overflow: ellipsis;
785
947
  white-space: nowrap;
786
948
  }
@@ -1093,6 +1255,16 @@
1093
1255
  .ui-topbar-start {
1094
1256
  flex: 0 1 auto;
1095
1257
  }
1258
+ .ui-topbar-start > :last-child {
1259
+ max-width: 100%;
1260
+ min-width: 0;
1261
+ flex: 0 1 auto;
1262
+ overflow: hidden;
1263
+ /* Declares its own line box because it CLIPS — see gh#254. */
1264
+ line-height: var(--topbar-start-truncate-line-height);
1265
+ text-overflow: ellipsis;
1266
+ white-space: nowrap;
1267
+ }
1096
1268
  .ui-topbar-center {
1097
1269
  flex: 1 1 0%;
1098
1270
  justify-content: center;
@@ -1103,6 +1275,17 @@
1103
1275
  margin-inline-start: auto;
1104
1276
  }
1105
1277
 
1278
+ /* COLLISION CONTRACT (gh#244) — with a docked 16rem sidebar, 901–1100px viewports do not have
1279
+ * enough room for a breadcrumb/title, full search trigger and end utilities. The center slot is
1280
+ * optional by definition, so remove it before it can cover either owned edge. This also gives
1281
+ * phones the same deterministic allocation. A consumer with a real compact center presentation
1282
+ * can opt it back in through the public token instead of a page-local media query. */
1283
+ @media (width <= 68.75rem) {
1284
+ .ui-topbar-center {
1285
+ display: var(--topbar-center-compact-display);
1286
+ }
1287
+ }
1288
+
1106
1289
  .tb-chip {
1107
1290
  display: inline-flex;
1108
1291
  max-width: 15rem;
@@ -1156,6 +1339,10 @@
1156
1339
  min-width: 0;
1157
1340
  overflow: hidden;
1158
1341
  font-weight: 500;
1342
+ /* gh#254 — same trap as `.sb-label`: this clips, so it must own a line box tall enough for the
1343
+ * font's descenders. `.tb-chip` uses `font: inherit`, so without this the chip would shear its
1344
+ * glyphs the moment a service set a tight line-height on the bar. */
1345
+ line-height: 1.5;
1159
1346
  text-overflow: ellipsis;
1160
1347
  white-space: nowrap;
1161
1348
  }
@@ -1286,3 +1473,152 @@
1286
1473
  transition: none;
1287
1474
  }
1288
1475
  }
1476
+
1477
+ /*
1478
+ * ERROR SURFACE — the 403 / 404 / 500 / 503 exception surface (gh#221, gh#251).
1479
+ *
1480
+ * Geometry is PACKAGE-OWNED at 1440 / 1024 / 390: a consumer never writes `min-h-dvh`, a
1481
+ * flex-centring class or a media query for an error page. In `system` mode the viewport centring
1482
+ * comes from `CenteredShell align="center"` (which the component renders itself); this block owns
1483
+ * the surface block that is identical in BOTH modes — measure, rhythm, metadata list.
1484
+ *
1485
+ * The narrow step is a CONTAINER query, not a viewport one (the SplitPane precedent, gh#165): the
1486
+ * surface reads its OWN width, so it compacts correctly whether the shrinking came from a 390px
1487
+ * phone or from an application shell whose sidebar is expanded on a laptop.
1488
+ */
1489
+ @layer components {
1490
+ .ui-error-surface {
1491
+ container: error-surface / inline-size;
1492
+ display: flex;
1493
+ width: 100%;
1494
+ max-width: var(--error-surface-max-width);
1495
+ flex-direction: column;
1496
+ align-items: center;
1497
+ /* Logical, so the surface stays centred and its text start-aligns correctly under `dir="rtl"`. */
1498
+ margin-inline: auto;
1499
+ gap: var(--error-surface-gap);
1500
+ padding-block: var(--error-surface-padding-block);
1501
+ text-align: center;
1502
+ }
1503
+
1504
+ .ui-error-surface-brand {
1505
+ display: flex;
1506
+ justify-content: center;
1507
+ padding-block-end: var(--error-surface-brand-gap);
1508
+ }
1509
+
1510
+ /* The EmptyState body already owns its own vertical rhythm and description measure
1511
+ * (--empty-state-*); the surface only removes the duplicated block padding so the two spacing
1512
+ * systems do not stack into a hole between the status code and the icon. */
1513
+ .ui-error-surface .ui-error-surface-body {
1514
+ width: 100%;
1515
+ padding-block: 0;
1516
+ }
1517
+
1518
+ .ui-error-surface-status {
1519
+ margin: 0;
1520
+ }
1521
+
1522
+ .ui-error-surface-meta {
1523
+ display: flex;
1524
+ width: 100%;
1525
+ flex-direction: column;
1526
+ gap: var(--error-surface-meta-gap);
1527
+ /* Chrome is a token, default quiet (rule #44) — `none` unless a service opts in. */
1528
+ border-block-start: var(--error-surface-meta-border);
1529
+ padding-block-start: var(--error-surface-meta-padding-block);
1530
+ margin: 0;
1531
+ }
1532
+
1533
+ .ui-error-surface-meta-row {
1534
+ display: flex;
1535
+ flex-wrap: wrap;
1536
+ justify-content: center;
1537
+ gap: var(--error-surface-meta-row-gap);
1538
+ }
1539
+
1540
+ .ui-error-surface-meta-row > dd {
1541
+ margin-inline-start: 0;
1542
+ }
1543
+
1544
+ .ui-error-surface-progress {
1545
+ width: 100%;
1546
+ max-width: var(--error-surface-progress-max-width);
1547
+ }
1548
+
1549
+ /*
1550
+ * NARROW STEP (the 390 case). Reclaim vertical space and stop a long ja / vi metadata row from
1551
+ * wrapping into a ragged two-column shape: each label stacks over its value, centred.
1552
+ */
1553
+ @container error-surface (width < 30rem) {
1554
+ .ui-error-surface {
1555
+ padding-block: var(--error-surface-padding-block-compact);
1556
+ }
1557
+
1558
+ .ui-error-surface-meta-row {
1559
+ flex-direction: column;
1560
+ gap: 0;
1561
+ }
1562
+ }
1563
+ }
1564
+
1565
+ @layer components {
1566
+ /* ── CenteredShell · `preset="public-landing"` (gh#252) ──────────────────────────────────────
1567
+ * A PUBLIC landing page (marketing header · hero · sections · legal footer) is a COMPOSITION,
1568
+ * not a framework component (docs/COMPOSITION-VS-COMPONENT.md: Hero / Navbar / Footer / Pricing
1569
+ * all FAIL the Framework-Component Test). What the consumer must NOT own is the SHELL geometry —
1570
+ * the content measure, the header/footer measure alignment, the section rhythm and the flat
1571
+ * public-surface chrome. This preset owns all of it through `--centered-shell-landing-*` knobs,
1572
+ * so a landing composes real primitives with ZERO page-local CSS and zero descendant selectors.
1573
+ *
1574
+ * `preset="default"` emits NO `data-preset` attribute at all, so none of this can match and every
1575
+ * existing CenteredShell keeps its exact box (the gh#231 inert-default contract). */
1576
+ .ui-centered-shell[data-preset="public-landing"] {
1577
+ --centered-shell-main-padding: var(--centered-shell-landing-main-padding-block)
1578
+ var(--centered-shell-landing-inset-inline);
1579
+ /* The hero title is a real `Heading level={1}`; the preset re-points the h1 tier the same way
1580
+ * `.ui-auth-shell` re-points it — never a consumer font-size. */
1581
+ --heading-h1: var(--centered-shell-landing-heading-size);
1582
+ /* Flat public surface: every Card in the subtree loses its elevation from ONE knob instead of
1583
+ * a consumer `.landing .card { box-shadow: none }` descendant selector. */
1584
+ --card-shadow: var(--centered-shell-landing-card-shadow);
1585
+ background: hsl(var(--centered-shell-landing-background, var(--background)));
1586
+ }
1587
+
1588
+ .ui-centered-shell[data-preset="public-landing"] .ui-centered-shell-column {
1589
+ display: flex;
1590
+ max-width: var(--centered-shell-landing-max-width);
1591
+ flex-direction: column;
1592
+ /* Section rhythm — the consumer stacks plain `<section>` layout elements and never sets a gap. */
1593
+ gap: var(--centered-shell-landing-section-gap);
1594
+ }
1595
+
1596
+ /* Header and footer share the column's measure WITHOUT a consumer max-width wrapper: the inline
1597
+ * padding is the page gutter until the viewport is wider than the measure, then it grows so the
1598
+ * bar content starts on exactly the same inline edge as the centred column. */
1599
+ .ui-centered-shell[data-preset="public-landing"] .ui-centered-shell-bar,
1600
+ .ui-centered-shell[data-preset="public-landing"] .ui-centered-shell-footer {
1601
+ padding-inline: max(
1602
+ var(--centered-shell-landing-inset-inline),
1603
+ calc((100% - var(--centered-shell-landing-max-width)) / 2)
1604
+ );
1605
+ }
1606
+
1607
+ .ui-centered-shell[data-preset="public-landing"] .ui-centered-shell-footer {
1608
+ padding-block: var(--centered-shell-landing-footer-padding-block);
1609
+ text-align: start;
1610
+ }
1611
+
1612
+ /* Compact step (the 390px acceptance width sits here): the gutter, the section rhythm and the
1613
+ * hero tier switch to their compact knobs. One media query, owned by the package. */
1614
+ @media (width < 40rem) {
1615
+ .ui-centered-shell[data-preset="public-landing"] {
1616
+ --centered-shell-landing-inset-inline: var(--centered-shell-landing-inset-inline-compact);
1617
+ --centered-shell-landing-main-padding-block: var(
1618
+ --centered-shell-landing-main-padding-block-compact
1619
+ );
1620
+ --centered-shell-landing-section-gap: var(--centered-shell-landing-section-gap-compact);
1621
+ --centered-shell-landing-heading-size: var(--centered-shell-landing-heading-size-compact);
1622
+ }
1623
+ }
1624
+ }