@agent-ui-kit/components 0.0.3

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 (271) hide show
  1. package/dist/base-styles.css +8 -0
  2. package/dist/component-styles.css +147 -0
  3. package/dist/controls/_base/index.d.ts +3 -0
  4. package/dist/controls/_base/index.js +9 -0
  5. package/dist/controls/_base/indicator-element.d.ts +132 -0
  6. package/dist/controls/_base/indicator-element.js +158 -0
  7. package/dist/controls/_base/listbox-element.d.ts +70 -0
  8. package/dist/controls/_base/listbox-element.js +74 -0
  9. package/dist/controls/_base/range-element.d.ts +120 -0
  10. package/dist/controls/_base/range-element.js +158 -0
  11. package/dist/controls/_surface/container-box.css +301 -0
  12. package/dist/controls/_surface/container.css +156 -0
  13. package/dist/controls/_token-surface/token-surface.d.ts +35 -0
  14. package/dist/controls/_token-surface/token-surface.js +97 -0
  15. package/dist/controls/attachment/attachment-meta.d.ts +8 -0
  16. package/dist/controls/attachment/attachment-meta.js +84 -0
  17. package/dist/controls/attachment/attachment.css +91 -0
  18. package/dist/controls/attachment/attachment.d.ts +40 -0
  19. package/dist/controls/attachment/attachment.js +70 -0
  20. package/dist/controls/avatar/avatar-initials.d.ts +4 -0
  21. package/dist/controls/avatar/avatar-initials.js +30 -0
  22. package/dist/controls/avatar/avatar.css +87 -0
  23. package/dist/controls/avatar/avatar.d.ts +40 -0
  24. package/dist/controls/avatar/avatar.js +77 -0
  25. package/dist/controls/badge/badge.css +175 -0
  26. package/dist/controls/badge/badge.d.ts +36 -0
  27. package/dist/controls/badge/badge.js +71 -0
  28. package/dist/controls/bar-chart/bar-chart.css +110 -0
  29. package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
  30. package/dist/controls/bar-chart/bar-chart.js +80 -0
  31. package/dist/controls/bar-chart/bar-math.d.ts +31 -0
  32. package/dist/controls/bar-chart/bar-math.js +106 -0
  33. package/dist/controls/button/button.css +263 -0
  34. package/dist/controls/button/button.d.ts +61 -0
  35. package/dist/controls/button/button.js +115 -0
  36. package/dist/controls/calendar/calendar.css +495 -0
  37. package/dist/controls/calendar/calendar.d.ts +161 -0
  38. package/dist/controls/calendar/calendar.js +1004 -0
  39. package/dist/controls/card/card-content.d.ts +24 -0
  40. package/dist/controls/card/card-content.js +133 -0
  41. package/dist/controls/card/card-footer.d.ts +3 -0
  42. package/dist/controls/card/card-footer.js +16 -0
  43. package/dist/controls/card/card-header.d.ts +3 -0
  44. package/dist/controls/card/card-header.js +18 -0
  45. package/dist/controls/card/card.css +445 -0
  46. package/dist/controls/card/card.d.ts +51 -0
  47. package/dist/controls/card/card.js +61 -0
  48. package/dist/controls/checkbox/checkbox.css +204 -0
  49. package/dist/controls/checkbox/checkbox.d.ts +114 -0
  50. package/dist/controls/checkbox/checkbox.js +130 -0
  51. package/dist/controls/code/code.css +72 -0
  52. package/dist/controls/code/code.d.ts +23 -0
  53. package/dist/controls/code/code.js +40 -0
  54. package/dist/controls/color-picker/color-picker.css +209 -0
  55. package/dist/controls/color-picker/color-picker.d.ts +84 -0
  56. package/dist/controls/color-picker/color-picker.js +545 -0
  57. package/dist/controls/color-picker/color.d.ts +40 -0
  58. package/dist/controls/color-picker/color.js +159 -0
  59. package/dist/controls/column/column.css +151 -0
  60. package/dist/controls/column/column.d.ts +107 -0
  61. package/dist/controls/column/column.js +57 -0
  62. package/dist/controls/combo-box/combo-box.css +397 -0
  63. package/dist/controls/combo-box/combo-box.d.ts +133 -0
  64. package/dist/controls/combo-box/combo-box.js +680 -0
  65. package/dist/controls/command-modal/command-modal.css +226 -0
  66. package/dist/controls/command-modal/command-modal.d.ts +42 -0
  67. package/dist/controls/command-modal/command-modal.js +315 -0
  68. package/dist/controls/disclosure/disclosure.css +162 -0
  69. package/dist/controls/disclosure/disclosure.d.ts +37 -0
  70. package/dist/controls/disclosure/disclosure.js +207 -0
  71. package/dist/controls/field/field.css +140 -0
  72. package/dist/controls/field/field.d.ts +27 -0
  73. package/dist/controls/field/field.js +228 -0
  74. package/dist/controls/form-provider/form-provider.css +40 -0
  75. package/dist/controls/form-provider/form-provider.d.ts +42 -0
  76. package/dist/controls/form-provider/form-provider.js +105 -0
  77. package/dist/controls/grid/grid.css +72 -0
  78. package/dist/controls/grid/grid.d.ts +60 -0
  79. package/dist/controls/grid/grid.js +56 -0
  80. package/dist/controls/icon/icon.css +37 -0
  81. package/dist/controls/icon/icon.d.ts +25 -0
  82. package/dist/controls/icon/icon.js +55 -0
  83. package/dist/controls/index.d.ts +57 -0
  84. package/dist/controls/index.js +136 -0
  85. package/dist/controls/ladder/ladder.css +89 -0
  86. package/dist/controls/ladder/ladder.d.ts +27 -0
  87. package/dist/controls/ladder/ladder.js +78 -0
  88. package/dist/controls/list/list.css +118 -0
  89. package/dist/controls/list/list.d.ts +84 -0
  90. package/dist/controls/list/list.js +48 -0
  91. package/dist/controls/menu/menu.css +195 -0
  92. package/dist/controls/menu/menu.d.ts +44 -0
  93. package/dist/controls/menu/menu.js +248 -0
  94. package/dist/controls/modal/modal.css +99 -0
  95. package/dist/controls/modal/modal.d.ts +61 -0
  96. package/dist/controls/modal/modal.js +155 -0
  97. package/dist/controls/popover/popover.css +83 -0
  98. package/dist/controls/popover/popover.d.ts +44 -0
  99. package/dist/controls/popover/popover.js +164 -0
  100. package/dist/controls/progress/progress.css +106 -0
  101. package/dist/controls/progress/progress.d.ts +28 -0
  102. package/dist/controls/progress/progress.js +80 -0
  103. package/dist/controls/radio/radio-group.css +84 -0
  104. package/dist/controls/radio/radio-group.d.ts +152 -0
  105. package/dist/controls/radio/radio-group.js +350 -0
  106. package/dist/controls/radio/radio.css +219 -0
  107. package/dist/controls/radio/radio.d.ts +19 -0
  108. package/dist/controls/radio/radio.js +75 -0
  109. package/dist/controls/ramp/ramp.css +75 -0
  110. package/dist/controls/ramp/ramp.d.ts +29 -0
  111. package/dist/controls/ramp/ramp.js +72 -0
  112. package/dist/controls/row/row.css +152 -0
  113. package/dist/controls/row/row.d.ts +95 -0
  114. package/dist/controls/row/row.js +45 -0
  115. package/dist/controls/segment/segment.css +66 -0
  116. package/dist/controls/segment/segment.d.ts +3 -0
  117. package/dist/controls/segment/segment.js +26 -0
  118. package/dist/controls/segmented-control/segmented-control.css +189 -0
  119. package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
  120. package/dist/controls/segmented-control/segmented-control.js +64 -0
  121. package/dist/controls/select/select.css +467 -0
  122. package/dist/controls/select/select.d.ts +147 -0
  123. package/dist/controls/select/select.js +538 -0
  124. package/dist/controls/slider/slider.css +231 -0
  125. package/dist/controls/slider/slider.d.ts +111 -0
  126. package/dist/controls/slider/slider.js +60 -0
  127. package/dist/controls/slider-multi/slider-multi.css +197 -0
  128. package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
  129. package/dist/controls/slider-multi/slider-multi.js +380 -0
  130. package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
  131. package/dist/controls/sparkline/sparkline-math.js +79 -0
  132. package/dist/controls/sparkline/sparkline.css +61 -0
  133. package/dist/controls/sparkline/sparkline.d.ts +27 -0
  134. package/dist/controls/sparkline/sparkline.js +74 -0
  135. package/dist/controls/split/constrain.d.ts +44 -0
  136. package/dist/controls/split/constrain.js +126 -0
  137. package/dist/controls/split/split-pane.css +63 -0
  138. package/dist/controls/split/split-pane.d.ts +49 -0
  139. package/dist/controls/split/split-pane.js +42 -0
  140. package/dist/controls/split/split.css +127 -0
  141. package/dist/controls/split/split.d.ts +42 -0
  142. package/dist/controls/split/split.js +364 -0
  143. package/dist/controls/stat/stat-model.d.ts +22 -0
  144. package/dist/controls/stat/stat-model.js +69 -0
  145. package/dist/controls/stat/stat.css +101 -0
  146. package/dist/controls/stat/stat.d.ts +30 -0
  147. package/dist/controls/stat/stat.js +73 -0
  148. package/dist/controls/status-stream/status-stream.css +151 -0
  149. package/dist/controls/status-stream/status-stream.d.ts +108 -0
  150. package/dist/controls/status-stream/status-stream.js +417 -0
  151. package/dist/controls/swatch/swatch.css +66 -0
  152. package/dist/controls/swatch/swatch.d.ts +28 -0
  153. package/dist/controls/swatch/swatch.js +74 -0
  154. package/dist/controls/swiper/swiper-item.css +17 -0
  155. package/dist/controls/swiper/swiper-item.d.ts +31 -0
  156. package/dist/controls/swiper/swiper-item.js +47 -0
  157. package/dist/controls/swiper/swiper-label.css +10 -0
  158. package/dist/controls/swiper/swiper-label.d.ts +4 -0
  159. package/dist/controls/swiper/swiper-label.js +20 -0
  160. package/dist/controls/swiper/swiper-paddles.css +26 -0
  161. package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
  162. package/dist/controls/swiper/swiper-paddles.js +62 -0
  163. package/dist/controls/swiper/swiper-pagination.css +60 -0
  164. package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
  165. package/dist/controls/swiper/swiper-pagination.js +78 -0
  166. package/dist/controls/swiper/swiper.css +153 -0
  167. package/dist/controls/swiper/swiper.d.ts +158 -0
  168. package/dist/controls/swiper/swiper.js +659 -0
  169. package/dist/controls/switch/switch.css +194 -0
  170. package/dist/controls/switch/switch.d.ts +5 -0
  171. package/dist/controls/switch/switch.js +18 -0
  172. package/dist/controls/table/table-model.d.ts +43 -0
  173. package/dist/controls/table/table-model.js +124 -0
  174. package/dist/controls/table/table.css +90 -0
  175. package/dist/controls/table/table.d.ts +29 -0
  176. package/dist/controls/table/table.js +178 -0
  177. package/dist/controls/tabs/tab-panel.d.ts +10 -0
  178. package/dist/controls/tabs/tab-panel.js +89 -0
  179. package/dist/controls/tabs/tab.d.ts +26 -0
  180. package/dist/controls/tabs/tab.js +67 -0
  181. package/dist/controls/tabs/tabs.css +181 -0
  182. package/dist/controls/tabs/tabs.d.ts +61 -0
  183. package/dist/controls/tabs/tabs.js +176 -0
  184. package/dist/controls/text/href.d.ts +26 -0
  185. package/dist/controls/text/href.js +48 -0
  186. package/dist/controls/text/text.css +328 -0
  187. package/dist/controls/text/text.d.ts +85 -0
  188. package/dist/controls/text/text.js +273 -0
  189. package/dist/controls/text-field/text-field.css +498 -0
  190. package/dist/controls/text-field/text-field.d.ts +226 -0
  191. package/dist/controls/text-field/text-field.js +905 -0
  192. package/dist/controls/textarea/textarea.css +251 -0
  193. package/dist/controls/textarea/textarea.d.ts +136 -0
  194. package/dist/controls/textarea/textarea.js +312 -0
  195. package/dist/controls/theme-provider/theme-provider.css +48 -0
  196. package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
  197. package/dist/controls/theme-provider/theme-provider.js +40 -0
  198. package/dist/controls/timeline/timeline.css +33 -0
  199. package/dist/controls/timeline/timeline.d.ts +39 -0
  200. package/dist/controls/timeline/timeline.js +52 -0
  201. package/dist/controls/timeline-item/timeline-item.css +495 -0
  202. package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
  203. package/dist/controls/timeline-item/timeline-item.js +360 -0
  204. package/dist/controls/toast/toast-region.css +56 -0
  205. package/dist/controls/toast/toast-region.d.ts +20 -0
  206. package/dist/controls/toast/toast-region.js +113 -0
  207. package/dist/controls/toast/toast.css +96 -0
  208. package/dist/controls/toast/toast.d.ts +28 -0
  209. package/dist/controls/toast/toast.js +191 -0
  210. package/dist/controls/toolbar/toolbar.css +125 -0
  211. package/dist/controls/toolbar/toolbar.d.ts +108 -0
  212. package/dist/controls/toolbar/toolbar.js +68 -0
  213. package/dist/controls/tooltip/tooltip.css +80 -0
  214. package/dist/controls/tooltip/tooltip.d.ts +57 -0
  215. package/dist/controls/tooltip/tooltip.js +205 -0
  216. package/dist/descriptor/component-descriptor.d.ts +112 -0
  217. package/dist/descriptor/component-descriptor.js +492 -0
  218. package/dist/descriptor/index.d.ts +1 -0
  219. package/dist/descriptor/index.js +5 -0
  220. package/dist/dom/container.d.ts +45 -0
  221. package/dist/dom/container.js +60 -0
  222. package/dist/dom/element.d.ts +98 -0
  223. package/dist/dom/element.js +221 -0
  224. package/dist/dom/form.d.ts +240 -0
  225. package/dist/dom/form.js +399 -0
  226. package/dist/dom/index.d.ts +10 -0
  227. package/dist/dom/index.js +31 -0
  228. package/dist/dom/props.d.ts +56 -0
  229. package/dist/dom/props.js +231 -0
  230. package/dist/dom/repeat.d.ts +10 -0
  231. package/dist/dom/repeat.js +202 -0
  232. package/dist/dom/template.d.ts +161 -0
  233. package/dist/dom/template.js +634 -0
  234. package/dist/dom/watch.d.ts +11 -0
  235. package/dist/dom/watch.js +82 -0
  236. package/dist/foundation-styles.css +9 -0
  237. package/dist/index.d.ts +2 -0
  238. package/dist/index.js +3 -0
  239. package/dist/reactive/graph.d.ts +65 -0
  240. package/dist/reactive/graph.js +310 -0
  241. package/dist/reactive/index.d.ts +3 -0
  242. package/dist/reactive/index.js +4 -0
  243. package/dist/reactive/scheduler.d.ts +13 -0
  244. package/dist/reactive/scheduler.js +49 -0
  245. package/dist/traits/area-drag.d.ts +12 -0
  246. package/dist/traits/area-drag.js +70 -0
  247. package/dist/traits/form-registry.d.ts +36 -0
  248. package/dist/traits/form-registry.js +108 -0
  249. package/dist/traits/index.d.ts +24 -0
  250. package/dist/traits/index.js +19 -0
  251. package/dist/traits/overlay.d.ts +50 -0
  252. package/dist/traits/overlay.js +277 -0
  253. package/dist/traits/pane-resize.d.ts +26 -0
  254. package/dist/traits/pane-resize.js +103 -0
  255. package/dist/traits/press-activation.d.ts +11 -0
  256. package/dist/traits/press-activation.js +55 -0
  257. package/dist/traits/roving-focus.d.ts +52 -0
  258. package/dist/traits/roving-focus.js +214 -0
  259. package/dist/traits/scroll-fade.d.ts +28 -0
  260. package/dist/traits/scroll-fade.js +120 -0
  261. package/dist/traits/selection-commit.d.ts +30 -0
  262. package/dist/traits/selection-commit.js +146 -0
  263. package/dist/traits/tabbable.d.ts +12 -0
  264. package/dist/traits/tabbable.js +59 -0
  265. package/dist/traits/track-user-invalid.d.ts +36 -0
  266. package/dist/traits/track-user-invalid.js +52 -0
  267. package/dist/traits/value-codec.d.ts +120 -0
  268. package/dist/traits/value-codec.js +497 -0
  269. package/dist/traits/value-drag.d.ts +18 -0
  270. package/dist/traits/value-drag.js +84 -0
  271. package/package.json +263 -0
@@ -0,0 +1,445 @@
1
+ /* card.css — the SINGLE-file stylesheet for the whole ui-card family (ADR-0003 one-sheet-per-folder):
2
+ * ui-card + ui-card-header / ui-card-content / ui-card-footer. Sectioned, one TOKEN `:where()` block + one
3
+ * STYLES block per element, mirroring button.css / text-field.css (declaration vs consumption split).
4
+ *
5
+ * Role-purity (ADR-0008 / tokens.md): the `--md-sys-color-*` colour roles and the shared `--md-sys-space-*` / `--md-sys-shape-corner-base`
6
+ * ramps enter ONLY in the `:where()` TOKEN blocks (the variable-definition side); every STYLES block (`@scope` /
7
+ * the region anatomy) consumes ONLY the element's own `--ui-card-*` chain plus the role-pure container surface
8
+ * seam `--ui-container-bg` (ADR-0015 cl.2 — the cross-family surface seam, not a raw role). A card holds zero
9
+ * colour opinion; forced-colors is handled by the role layer + the block at the foot.
10
+ *
11
+ * Surface (ADR-0015): ui-card sits on the shared container seam (controls/_surface/container.css paints
12
+ * `background-color: var(--ui-container-bg)` and maps [elevation]/[brightness] for the family). The base default
13
+ * is `transparent`; a card seeds its OWN default `--ui-container-bg: var(--md-sys-color-neutral-surface)` below so an
14
+ * UN-elevated card still reads as a surface (card.css is `@import`-ed AFTER container.css in s12 — later source,
15
+ * equal `:where()` specificity, wins).
16
+ *
17
+ * Nested radius (ADR-0018, one level): the concentric-corner law is `r_child = max(0, r_parent − pad_parent)`.
18
+ * The literal ADR formula (a card both reads AND declares `--ui-card-child-radius`) is a SELF-REFERENTIAL custom
19
+ * property — a CSS cycle (guaranteed-invalid), the exact failure ADR-0018 records. The cycle-free realization of
20
+ * the SAME seam: a card computes its own inner radius (`--ui-card-inner-radius`, NOT read back into its radius)
21
+ * and PUBLISHES it to its DESCENDANTS as `--ui-card-child-radius` (the `:where(ui-card) > *` rule — set on the
22
+ * children, never on self, so the publishing card never reads what it writes). A nested card (inside a region)
23
+ * reads the INHERITED `--ui-card-child-radius` for its own radius → one-level decrement, no cycle. The normal
24
+ * structure is card › ui-card-content › card; a card nested DIRECTLY inside another card (no region wrapper
25
+ * between, regardless of whether the outer card is bare or region-bearing) is the depth-≥2 manual case (set
26
+ * an explicit border-radius to reseed) — documented in card.md. The direct-child card matches BOTH
27
+ * `:where(ui-card)` (declaring its OWN `--ui-card-inner-radius`) and its parent's `:where(ui-card) > *` publish
28
+ * rule (setting `--ui-card-child-radius` directly ON it) — a genuine multi-property CYCLE
29
+ * (radius → child-radius → inner-radius → radius) collapses its radius to 0, independent of any padding value.
30
+ * ADR-0056's fallback below only ever sets padding/gap — it does not touch this chain, so it neither fixes nor
31
+ * worsens the pre-existing manual case (re-verified, card.browser.test.ts).
32
+ *
33
+ * REVISED 2026-07-04 (Kim, the region-inset pass): the card-only 6px inline-padding override is RESCINDED — a
34
+ * card now reads container-box.css's shared region defaults straight (inline 12px / block 6px), same as
35
+ * modal/select/menu/combo-box (one spacing vocabulary, restoring ADR-0046's original intent). Region padding
36
+ * going from 6px inline to 12px means `--ui-card-inner-radius` (below) now decrements against a LARGER value —
37
+ * with the default `--md-sys-shape-corner-base` of 12px, the inner radius floors at EXACTLY 0 for an unreseeded root card
38
+ * (12 − 12 = 0, a knife-edge, not the old 6px-positive headroom). This is a real visual consequence of removing
39
+ * the override, not a bug: an author who wants a visibly rounded nested corner now needs to reseed
40
+ * `--ui-card-radius` larger than 12px. Flagged for Kim; not silently patched around.
41
+ *
42
+ * Region-less humane default (ADR-0056): a card with NO region child (no header/content/footer) applies the
43
+ * region-equivalent padding + content rhythm to its OWN box (the `:not(:has(...))` leg in the STYLES block
44
+ * below) — a bare-children card, the wild-payload DEFAULT an LLM emits against a catalog whose Card has no
45
+ * region sub-types, reads as padded instead of the box-model's zero-padding law. `:has()` re-evaluation is
46
+ * streaming-safe by construction: a region child arriving late flips the fallback off and the presence-driven
47
+ * row template on in the same reflow, no double padding, no synthetic DOM (payload tree ≡ DOM tree). Mixed
48
+ * composition (a region PLUS loose siblings) does NOT match — regions present means the author owns the
49
+ * structure; the loose siblings render at the box-model default (documented, not repaired).
50
+ *
51
+ * Standalone sheet — linked via the component-styles barrel (s12), NEVER injected from card.ts (behaviour-only
52
+ * `.ts`, no runtime style; plan §2). */
53
+
54
+ /* ════════════════ [ui-card] TOKEN BLOCK — :where(ui-card) declares the chain ════════════════ */
55
+
56
+ :where(ui-card) {
57
+ /* Surface: the card's OWN default plane (the base seam defaults transparent — ADR-0015 cl.1). [elevation]/
58
+ [brightness] still repoint this via container.css; this is only the un-elevated default. */
59
+ --ui-container-bg: var(--md-sys-color-neutral-surface);
60
+
61
+ /* Layout spacing — the container BOX-MODEL (container-box.css, REVISED 2026-07-04): the card holds NO
62
+ padding (the shell law). A region (header/content/footer) carries the SAME uniform inset margin every
63
+ other [data-box] child gets, PLUS its own region padding. The card-only 6px-inline override is RESCINDED:
64
+ card reads container-box.css's shared region defaults straight (inline 12px / block 6px), matching
65
+ modal/select/menu/combo-box exactly (one spacing vocabulary). The DEFAULT (non-scroll) shell is plain BLOCK
66
+ FLOW (a flow-root BFC, like every other [data-box]) — NOT grid/flex (Kim: "containers should not use grid
67
+ or flex; let contents space themselves"): regions stack in normal flow (an absent region leaves no box →
68
+ presence-driven for free) and their uniform 6px margins COLLAPSE to clean 6px gutters, the 1px frame border
69
+ blocking collapse-through at the edges. No grid gap, no first/last margin split, no :has() row templates
70
+ needed.
71
+ SCROLL MODE (@scope, `[scrollable]`) is DIFFERENT (REVISED 2026-07-07, Kim, verbatim: "<ui-card-content>
72
+ should have the mask and manage overflow ... it should be set to use 100% of its parent height when
73
+ [scrollable]" — SUPERSEDES the short-lived WRAPPER MODEL, 2026-07-06): `ui-card-content` itself becomes the
74
+ scroll viewport (no author-written wrapper child any more), and the header/footer become OVERLAID PEERS
75
+ (`position: absolute`) rather than in-flow items — see the @scope STYLES block below for the full mechanism
76
+ and why the shell still needs `display: flex` even though header/footer no longer participate in it.
77
+ THE SCROLLBAR IS HIDDEN (Kim, 2026-07-08, resolving the mask-fades-the-scrollbar tension with a third
78
+ option: "keep native, but hide it" — ADR-0046 Amendment 6): no visible bar means nothing for the mask to
79
+ fade in the first place; native wheel/trackpad/keyboard scrolling is unaffected. Losing the visible bar
80
+ as a scroll affordance/keyboard entry point is why card-content.ts gives the region an explicit
81
+ tabindex=0 in scroll mode (WCAG 2.1.1) — see that file and the STYLES block below. */
82
+ --ui-card-padding: 0;
83
+ --ui-card-region-margin: var(--ui-box-inset, 0.375rem); /* 6px — the uniform inset margin around each region */
84
+ --ui-card-region-pad-inline: var(--ui-box-pad-inline, 0.75rem); /* 12px — shared region inline padding */
85
+ --ui-card-region-pad-block: var(--ui-box-pad-block, 0.375rem); /* 6px — shared region block padding */
86
+ --ui-card-content-gap: var(--ui-box-gap, 0.5rem); /* 8px region rhythm between content children */
87
+
88
+ /* Frame: the neutral hairline border + the corner radius. The radius reads the inherited
89
+ --ui-card-child-radius a PARENT card published (one-level nesting, ADR-0018), else the shared
90
+ --md-sys-shape-corner-base (a root card — the channel is unset → the fallback). */
91
+ --ui-card-border: var(--md-sys-color-neutral-outline-variant);
92
+ --ui-card-radius: var(--ui-card-child-radius, var(--md-sys-shape-corner-base));
93
+
94
+ /* The card's INNER radius (the concentric-corner law) — the value handed down to a nested card. Under the
95
+ box-model the inset is the CONTENT region's inline padding (a nested card lives inside ui-card-content),
96
+ so the chain re-bases off that, NOT the (now-zero) card padding. Not read back into --ui-card-radius → no
97
+ self-referential cycle (ADR-0018). */
98
+ --ui-card-inner-radius: max(0px, calc(var(--ui-card-radius) - var(--ui-card-region-pad-inline)));
99
+ }
100
+
101
+ /* PUBLISH the child radius ONE level (ADR-0018): hand every direct child the card's inner radius as the
102
+ inherited --ui-card-child-radius. Set on the CHILDREN (`> *`), never on the card itself, so the publishing
103
+ card never reads the property it writes (the cycle-free realization of the published seam). A nested card —
104
+ normally inside ui-card-content — inherits this through the region wrapper and reads it for its own radius. */
105
+ :where(ui-card) > * {
106
+ --ui-card-child-radius: var(--ui-card-inner-radius);
107
+ }
108
+
109
+ /* ════════════════ [ui-card] STYLES — @scope (ui-card) consumes ONLY --ui-card-* ════════════════ */
110
+
111
+ @scope (ui-card) {
112
+ /* The surface container: regions stack in plain BLOCK FLOW (a flow-root BFC — NOT grid/flex, per Kim's
113
+ "containers should not use grid or flex; let contents space themselves"). The surface PAINT rides the
114
+ shared container seam (container.css); here we own the frame (border/radius) and the region inset. */
115
+ :scope {
116
+ display: flow-root;
117
+ box-sizing: border-box;
118
+
119
+ padding: var(--ui-card-padding);
120
+ border: 1px solid var(--ui-card-border);
121
+ border-radius: var(--ui-card-radius);
122
+ /* min-inline-size: 0 — RETAINED, ADR-0100 cl.3 (the audited "Defect B" is DISPOSITIONED, not patched): the
123
+ * tile-crush the visual audit attributed to this floor is actually ADR-0100 cl.1's containment defect
124
+ * end-to-end (measured: a `min-content` floor renders the IDENTICAL crushed width under containment, and
125
+ * is equally irrelevant once containment is fixed — the wrap-row tiles land at their real content size,
126
+ * ~176–180px, with or without this floor). The declaration's own scenario IS real and measured: in a
127
+ * `ui-grid` track, a card with unbreakable content held the exact 200px track at `0` and BLEW OUT to
128
+ * 484.2px with a `min-content` floor — the "composes under any parent (a grid cell)" behavior ADR-0030
129
+ * already leans on. So `0` stays: it is NOT binding in the crush (nothing to fix) and IS protective in
130
+ * grid tracks (a floor would break them). Residual, accepted: a `reflow="locked"` nowrap row narrower than
131
+ * the sum of its cards' min-contents still crushes them below min-content — the tiles regression leg
132
+ * (card.browser.test.ts) watches for the crush class returning, it does not chase this named exotic case
133
+ * (the default row stacks below 24rem before that window opens). */
134
+ min-inline-size: 0;
135
+ }
136
+
137
+ /* Region INSET (REVISED 2026-07-04, block-flow): every region floats inside the frame with a uniform 6px
138
+ gutter, the same as every other [data-box] consumer (container-box.css). In the card's block-flow BFC the
139
+ adjacent region margins COLLAPSE (6px between siblings) and the 1px frame border blocks collapse-through
140
+ at the top/bottom edges (6px there too) — so ONE uniform margin gives clean 6px gutters everywhere; no
141
+ grid, no first/last split, no row templates. An ABSENT region simply contributes no box (presence-driven
142
+ for free — block flow, not a :has() grid template). */
143
+ :scope > :where(ui-card-header, ui-card-content, ui-card-footer) {
144
+ margin: var(--ui-card-region-margin);
145
+ }
146
+
147
+ /* Region-less humane default (ADR-0056): NO region child present → apply the region-equivalent PADDING +
148
+ content rhythm to the card's own box (the same OWN-ink-padding a real region would carry: inline 12 · block
149
+ 6 · gap 8 — REVISED 2026-07-04, was 6·6·8 under the rescinded card-only override). This mirrors a region's
150
+ own padding, not its total frame-to-ink distance (a real region ALSO sits behind a 6px margin the bare
151
+ card's single flattened box has no equivalent layer for) — deliberately the simpler of the two readings;
152
+ flag to Kim if full margin+padding parity is wanted instead. Higher selector specificity than the base
153
+ `:scope` rule above wins outright (an extra :not(:has()) clause,
154
+ the same pattern the row legs already use), so this leg overrides the box-model's zero padding/gap ONLY
155
+ when it applies. The moment any region child exists, `:has()` re-evaluates false and this leg drops —
156
+ no double padding, no phantom transition state (streaming-safe). Mixed composition (a region present PLUS
157
+ loose siblings) is excluded by the same :has() test — regions present means the author owns the layout. */
158
+ :scope:not(:has(> ui-card-header, > ui-card-content, > ui-card-footer)) {
159
+ padding-inline: var(--ui-card-region-pad-inline);
160
+ padding-block: var(--ui-card-region-pad-block);
161
+ }
162
+ /* loose-child rhythm = adjacent-sibling margin (the shell is block flow now, not a grid/flex `gap`). */
163
+ :scope:not(:has(> ui-card-header, > ui-card-content, > ui-card-footer)) > * + * {
164
+ margin-block-start: var(--ui-card-content-gap);
165
+ }
166
+
167
+ /* SCROLL MODE — HEADER/FOOTER AS OVERLAID PEERS (REVISED 2026-07-07, Kim, verbatim: "<ui-card-content> should
168
+ have the mask and manage overflow, and adjust block-padding and linear gradient coordinates based on
169
+ presence of the peer footer and header. it should be set to use 100% of its parent height when
170
+ [scrollable]"). SUPERSEDES the short-lived WRAPPER MODEL (2026-07-06): no author-written `[scroll-wrapper]`
171
+ child any more — `ui-card-content` itself is now the ONE box that scrolls AND carries the mask.
172
+ `position: relative` anchors the header/footer (below, now `position: absolute` — removed from flow
173
+ entirely, no longer flex items). The shell still needs `display: flex; flex-direction: column` — NOT
174
+ dropped, despite the brackets leaving flex — because it is the SIZING MECHANISM that realizes "100% of
175
+ parent height": plain block layout's `height: 100%` on an auto-height parent bounded only by
176
+ `max-block-size` computes to 'auto', not 100% (the classic CSS percentage-height gap — a `max-height` alone
177
+ is never a "specified" height for a child's percentage resolution); flexbox explicitly carves out this case
178
+ for a flex item's used size. With header/footer no longer flex items, `ui-card-content` is the shell's SOLE
179
+ item — `flex: 1 1 auto` (below) is what actually fills the whole column; `display: flex` here is the
180
+ necessary plumbing, not new visual structure. No `gap`/`padding` needed on the shell any more (those existed
181
+ only to space THREE flex items without doubling the block-flow default's margin-collapse; one item needs
182
+ neither). */
183
+ :scope[scrollable],
184
+ :scope:has(> ui-card-content[scrollable]) {
185
+ position: relative;
186
+ isolation: isolate;
187
+ display: flex;
188
+ flex-direction: column;
189
+ }
190
+
191
+ /* Content's own 6px region margin (the block-flow default, above) — split by AXIS in scroll mode (Kim,
192
+ verbatim: "for contents of <ui-card-content ...> do not lose the default margins" — a screenshot caught
193
+ content's text sitting ~6px LEFT of the header/footer text once this went to `margin: 0` wholesale).
194
+ BLOCK (top/bottom) stays zeroed: it would otherwise shrink content away from "100% of parent" (the flex
195
+ item's margin box, not just its border box, fills the column), AND double-stack against the block-padding
196
+ bracket-clearance formula below (`max(band, plain-pad)` already reserves the exact vertical gutter a
197
+ present header/footer needs — `scroll-fade.ts`'s `bandOf()` measures the BRACKET's own rendered size, not
198
+ content's, so it does not shrink back if content also gained a margin here). INLINE (left/right) is KEPT at
199
+ the shared region margin — content is the flex item's CROSS-axis (column direction), so a left/right margin
200
+ only insets its stretched width, it does not touch the block/main-axis sizing at all — and it is what makes
201
+ content's text align with the header/footer text (which keep their own margin unchanged, below), both
202
+ reading at the SAME 6px+12px = 18px inset from the card's inner edge, matching the non-scroll shell. Header/
203
+ footer, below, were never zeroed (see their own rule for why). Higher specificity (an extra
204
+ `[scrollable]`/`:has()` clause) wins outright over the generic `> :where(ui-card-header, ui-card-content,
205
+ ui-card-footer) { margin: ... }` rule above. */
206
+ :scope[scrollable] > :where(ui-card-content),
207
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-content) {
208
+ margin-block: 0;
209
+ margin-inline: var(--ui-card-region-margin);
210
+ }
211
+
212
+ /* Header/footer become OVERLAID PEERS — `position: absolute`, pinned to the card's inline edges and ONE
213
+ block edge each — removed from flow entirely (so `ui-card-content`, above, becomes the shell's sole flex
214
+ item and can fill the whole column). They KEEP their existing 6px region margin (the generic `> *` rule
215
+ earlier in this file, unaffected here): for an absolutely positioned box, `margin` still offsets it INWARD
216
+ from its resolved inset edges, so `inset-inline: 0` + a 6px margin nets the SAME floating 6px-inset look
217
+ the family's regions always carry, just out-of-flow now. NO background (Kim: the brackets stay see-
218
+ through) — `ui-card-content`'s own mask (below) fades content to transparent as it passes beneath a
219
+ bracket, so a transparently-backed header/footer shows the card's own surface through faded content, never
220
+ a hard bleed ("the gradient carries the occlusion"). `z-index: 1` is a defensive no-op here (a positioned
221
+ box already paints after a static in-flow sibling regardless of z-index) but matches the family's standing
222
+ bracket convention. */
223
+ :scope[scrollable] > :where(ui-card-header),
224
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-header) {
225
+ position: absolute;
226
+ inset-inline: 0;
227
+ inset-block-start: 0;
228
+ z-index: 1;
229
+ }
230
+ :scope[scrollable] > :where(ui-card-footer),
231
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-footer) {
232
+ position: absolute;
233
+ inset-inline: 0;
234
+ inset-block-end: 0;
235
+ z-index: 1;
236
+ }
237
+
238
+ /* `ui-card-content` IS the scroll viewport now (no separate wrapper, no separate paint target) — `flex: 1 1
239
+ auto` is the mechanism realizing "100% of parent height" (see the shell banner above); `min-block-size: 0`
240
+ overrides the flex-shrink floor so genuinely tall content can compress below its natural height and become
241
+ internally scrollable, rather than forcing the card to grow (an item whose `overflow` is non-`visible`
242
+ already gets an automatic min-size of 0 per spec, but the explicit override matches this family's
243
+ belt-and-suspenders style). The block-PADDING — NOT the outer flex size — is what clears the overlaid
244
+ brackets: `padding-block-start`/`-end` read `--ui-card-box-head`/`--ui-card-box-foot` (the TOKEN block
245
+ above — the SAME measured bracket bands the gradient offset, container-box.css, already consumes via their
246
+ un-namespaced `--ui-box-*` source) — ONE measured source drives BOTH, per Kim's ask. `max(band, plain-pad)`
247
+ collapses correctly either way: a PRESENT bracket's band is always well over the plain 6px region pad, so
248
+ the band wins (content clears the bracket exactly); an ABSENT bracket publishes 0px, so the plain 6px
249
+ region pad wins instead — "clears a present header/footer; else the plain region padding". */
250
+ :scope[scrollable] > :where(ui-card-content),
251
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-content) {
252
+ flex: 1 1 auto;
253
+ min-block-size: 0;
254
+ overflow-y: auto;
255
+ padding-block-start: max(var(--ui-card-box-head), var(--ui-card-region-pad-block));
256
+ padding-block-end: max(var(--ui-card-box-foot), var(--ui-card-region-pad-block));
257
+ scrollbar-width: none; /* Firefox / the standard property — HIDE the native scrollbar (Kim's third
258
+ option, 2026-07-08: "keep native, but hide it" — no visible bar for the mask to
259
+ fade at all; native wheel/trackpad/keyboard scrolling is UNCHANGED, `overflow-y:
260
+ auto` above still does the scrolling. The fade becomes the sole scroll
261
+ affordance — its intended job all along. Keyboard operability moves to
262
+ card-content.ts's own tabindex=0 wiring (a hidden-scrollbar region needs an
263
+ explicit tab stop — see that file). */
264
+ }
265
+ /* Chromium/WebKit's own scrollbar-hiding hook — a separate rule (pseudo-elements cannot join a normal
266
+ declaration block). Scoped identically to the rule above (both scroll-mode triggers). */
267
+ :scope[scrollable] > :where(ui-card-content)::-webkit-scrollbar,
268
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-content)::-webkit-scrollbar {
269
+ display: none;
270
+ }
271
+
272
+ /* Focus ring (ADR-0009, component-review, 2026-07-08 — the GO-blocker on the scrollbar-hide pass): hiding
273
+ the scrollbar made ui-card-content a genuine keyboard tab stop (card-content.ts's tabindex=0), and every
274
+ fleet control that can take keyboard focus draws the SAME shared ring — this region is no exception. The
275
+ colour/width read the identical fleet constants every sibling reads (checkbox.css is the fleet
276
+ reference) — `--md-sys-color-focus-ring` / `--md-sys-state-focus-ring-width`, never a per-control repoint.
277
+ THE RING PAINTS ON THE PARENT CARD, not on ui-card-content itself (Kim, 2026-07-08, superseding an
278
+ earlier inset/negative-offset draft): `:has(> ui-card-content:focus-visible)` lets the card react to its
279
+ content region's focus state and draw the ring around the WHOLE card instead. This sidesteps the clipping
280
+ problem entirely rather than working around it — ui-card-content is an `overflow-y: auto` scroll
281
+ container, so a ring on IT gets clipped by its own overflow (measured: a positive-offset ring there
282
+ painted nothing); ui-card itself has no such overflow, so its ring uses the STANDARD fleet-positive
283
+ (outward) offset like every other consumer — no sign flip, no divergence. `:focus-visible` (not `:focus`)
284
+ keeps it keyboard-only, matching the rest of the fleet. */
285
+ :scope[scrollable]:has(> ui-card-content:focus-visible),
286
+ :scope:has(> ui-card-content[scrollable]:focus-visible) {
287
+ outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
288
+ outline-offset: var(--md-sys-state-focus-ring-offset);
289
+ }
290
+ }
291
+
292
+ /* ════════════════ [ui-card-header / -footer] TOKEN + anatomy — the host-as-grid ════════════════ */
293
+
294
+ /* The header/footer reuse the family leading/label/trailing anatomy (anatomy.md / ADR-0006): a presence-driven
295
+ `:has()` host-as-grid where the `slot` PLACES (leading start cell · the default/label `1fr` centre · trailing
296
+ end cell). A Container-class region has NO control height, so the adornment cells size INTRINSICALLY (`auto`)
297
+ — the structure is reused, not the control-frame glyph sizing. A region that paints a fill now rounds ALL
298
+ FOUR of its own corners to the card's INNER radius (below) — REVISED 2026-07-04: a region is inset, not
299
+ full-bleed, so it no longer meets the card's outer edge at all. */
300
+ :where(ui-card-header, ui-card-footer) {
301
+ --ui-card-region-gap: var(--md-sys-space-sm); /* adornment↔label column-gap (off the --md-sys-space ladder) */
302
+
303
+ display: grid;
304
+ grid-template-columns: 1fr; /* [label] — slotless */
305
+ align-items: center;
306
+ column-gap: var(--ui-card-region-gap);
307
+ border-radius: var(--ui-card-inner-radius); /* a region floats inset inside the frame (@scope margin rules
308
+ above) — round it like the small concentric surface it now is,
309
+ not clipped to the outer card radius (REVISED 2026-07-04) */
310
+ /* Box-model region padding — inline 12 / block 6, the shared container-box.css default (no card override
311
+ anymore); this is the region's OWN ink inset, separate from its 6px positional margin set in @scope above. */
312
+ padding-inline: var(--ui-card-region-pad-inline);
313
+ padding-block: var(--ui-card-region-pad-block);
314
+ }
315
+
316
+ /* [leading | label] — auto 1fr */
317
+ :where(ui-card-header, ui-card-footer):has(> [slot='leading']):not(:has(> [slot='trailing'])) {
318
+ grid-template-columns: auto 1fr;
319
+ }
320
+ /* [label | trailing] — 1fr auto */
321
+ :where(ui-card-header, ui-card-footer):has(> [slot='trailing']):not(:has(> [slot='leading'])) {
322
+ grid-template-columns: 1fr auto;
323
+ }
324
+ /* [leading | label | trailing] — auto 1fr auto */
325
+ :where(ui-card-header, ui-card-footer):has(> [slot='leading']):has(> [slot='trailing']) {
326
+ grid-template-columns: auto 1fr auto;
327
+ }
328
+
329
+ /* ════════════════ [ui-card-content] TOKEN + STYLES — the body + scroll ════════════════ */
330
+
331
+ :where(ui-card-content) {
332
+ /* Box-model content region — inset (its 6px positional margin is set in @scope (ui-card) above), with the
333
+ region's OWN inline 12 / block 6 padding — REVISED 2026-07-04: the card-only 6/6 override is gone; content
334
+ reads the shared container-box.css 12/6 default like every other family member. `display: block` is the
335
+ DEFAULT (non-scroll) shape — a normal block region that stacks in the card's flow. SCROLL MODE (REVISED
336
+ 2026-07-07 — supersedes the short-lived WRAPPER MODEL) repoints `display`/`overflow`/the block-padding per
337
+ @scope (ui-card) above: content becomes the shell's sole flex item (`flex: 1 1 auto`, filling the whole
338
+ column) AND directly `overflow-y: auto` — it IS the scroll viewport now, one box, no wrapper, no separate
339
+ paint target. The 8px region rhythm between children comes from an adjacent-sibling margin (gap-without-
340
+ flex), applied in the @scope block below. NOTE: unlike the shared container-box.css `[data-region='content']`/
341
+ `main` model, a ui-card-content has NO nested-content padding-STEPPING law — it is not marked `[data-region]`,
342
+ so the shared L1/L2/L3 descendant-level rules never match it; this region padding stays FLAT (12/6) at every
343
+ nesting depth (a card-in-card-content-in-card sees 12/6 inside 12/6, not a decrement). Only the RADIUS chain
344
+ steps one level (ADR-0018, above). */
345
+ display: block;
346
+ padding-inline: var(--ui-card-region-pad-inline);
347
+ padding-block: var(--ui-card-region-pad-block);
348
+ min-inline-size: 0; /* may shrink in a constrained (flex/grid) parent */
349
+
350
+ /* Edge-fade depth for the scroll mask painted HERE (Kim: "the mask should not be placed on the container
351
+ element — it should be on ui-card-content", REVISED 2026-07-07: content now manages its own overflow AND
352
+ carries the mask directly — no split at all). Density-linked; the 1rem fallback keeps it self-contained
353
+ where only this sheet is loaded. */
354
+ --ui-box-fade: var(--md-sys-space-lg, 1rem);
355
+
356
+ /* The bracket bands `traits/scroll-fade.ts` publishes as INLINE `--ui-box-head`/`--ui-box-foot` on this very
357
+ element (card-content.ts's `brackets` points at the card) — re-declared under the card's own `--ui-card-*`
358
+ namespace, the same pattern `--ui-card-region-margin` etc. above already use for the shared `--ui-box-*`
359
+ seam, so the STYLES block below stays role-pure (consumes only `--ui-card-*`). 0px when that edge has no
360
+ bracket. Drives BOTH this region's own block-padding (below) AND (unchanged) container-box.css's gradient
361
+ offset — one measured source, two consumers, per Kim's ask. */
362
+ --ui-card-box-head: var(--ui-box-head, 0px);
363
+ --ui-card-box-foot: var(--ui-box-foot, 0px);
364
+
365
+ /* Opt INTO the shared HOLD (container-box.css's `--ui-box-head-hold`/`-foot-hold`, both default 0px — a
366
+ coincident-stop no-op every OTHER consumer never touches): ui-card-content's own brackets are backgroundless
367
+ overlays (no fill of their own), so the gradient must hold fully transparent/opaque THROUGH each bracket's
368
+ entire band, not just ramp continuously across it — else a crisp bracket sits over PARTIALLY visible
369
+ scrolled content (measured, card.browser.test.ts). Declared under the SHARED (un-namespaced) name — this is
370
+ the one deliberate exception to the role-purity split above, since container-box.css's generic rule expects
371
+ this literal name, exactly like `--ui-box-fade` already is. */
372
+ --ui-box-head-hold: var(--ui-box-head, 0px);
373
+ --ui-box-foot-hold: var(--ui-box-foot, 0px);
374
+ }
375
+
376
+ @scope (ui-card-content) {
377
+ /* The 8px region rhythm between content children — an adjacent-sibling margin (gap-without-flex). The FIRST
378
+ child has no top margin (the block padding provides the inset). */
379
+ :scope > * + * {
380
+ margin-block-start: var(--ui-card-content-gap);
381
+ }
382
+
383
+ /* The `scrollable` prop is a SIGNAL that this region should become the scroll viewport (REVISED 2026-07-07 —
384
+ supersedes the short-lived WRAPPER MODEL). `<ui-card-content scrollable>` (the A2UI-mapped signal) triggers
385
+ the PARENT card's scroll mode through `@scope (ui-card)`'s `:has(> ui-card-content[scrollable])`, which is
386
+ where `overflow-y: auto` + the block-padding formula actually live (this element's OWN box is the thing
387
+ that scrolls now — no wrapper, no separate viewport). (Named `scrollable`, not `scroll`, to avoid shadowing
388
+ the native Element.scroll() method — see card-content.ts.)
389
+
390
+ The edge-fade mask paints HERE, on ui-card-content — Kim (2026-07-05): "the mask should not be placed on
391
+ the container element — it should be on ui-card-content"; REVISED 2026-07-07: content now measures AND
392
+ paints itself directly (`traits/scroll-fade.ts`'s `viewport` stays its default, `this`) — the sticky→
393
+ overlaid ui-card-header/ui-card-footer siblings never carry the mask at all (the MUST-PASS: they stay fully
394
+ crisp, proven cross-engine, card.browser.test.ts). AUTOMATIC in scroll mode (no opt-in), self-gates on real
395
+ overflow. card.md documents the contract; card-css.test.ts / card.browser.test.ts prove the mechanism.
396
+
397
+ WHY THIS SHAPE FIXES THE FLAGGED CONSEQUENCE the 2026-07-05 revision measured (cross-engine, Chromium +
398
+ WebKit — not silently patched around): `mask-image`'s gradient paints relative to the MASKED element's OWN
399
+ border-box (0%–100% of its full height). Under the 2026-07-05 shape, ui-card-content was masked while the
400
+ CARD scrolled — content's own box was a plain block, growing to its full natural (un-clipped) height, so
401
+ for content genuinely taller than roughly 2×(bracket band + fade depth) the fade zone was only ever visible
402
+ within the first/last ~60–90px of the TOTAL scroll range; through the middle of a long scroll the visible
403
+ slice sat in the gradient's opaque middle, text cut hard at the header/footer boundary with NO visible
404
+ fade. Under THIS shape, ui-card-content's own box is bounded (`flex: 1 1 auto` off a `max-block-size`-only
405
+ card — the @scope (ui-card) banner above) and it is ALSO the element that scrolls (`overflow-y: auto`) — so
406
+ its "full height" IS the small, fixed, currently-visible frame, and the SAME 0%–100% gradient is genuinely
407
+ viewport-registered and visible throughout the WHOLE scroll, at any content length (measured cross-engine,
408
+ card.browser.test.ts), without a separate wrapper element at all. The header/footer crispness (unchanged
409
+ across every revision of this mechanism) holds regardless.
410
+
411
+ THE HOLD (2026-07-07, a second measured finding on the SAME shape): a bare ramp from the true edge lets
412
+ content go partially visible WITHIN the bracket's own band (a backgroundless bracket has no occlusion of
413
+ its own) — content behind bracket text could still read as faintly layered underneath. `--ui-box-head-hold`/
414
+ `--ui-box-foot-hold` (declared in the TOKEN block above, off the SAME measured band) tell container-box.css's
415
+ shared mask to hold fully transparent/opaque THROUGH the whole bracket band before ramping — see that file's
416
+ own banner for the coincident-stop no-op that keeps every other `--ui-box-fade` consumer untouched. */
417
+ }
418
+
419
+ /* ════════════════ forced-colors (WHCM) ════════════════ */
420
+
421
+ /* Keep the card frame visible as a system colour. The edge-fade mask drop is now handled generically by the
422
+ shared `[data-fade-top], [data-fade-bottom]` forced-colors block (container-box.css) — which the scrollFade
423
+ trait targets on ui-card-content directly (REVISED 2026-07-07: content measures + paints itself, no split).
424
+ The surface itself survives via container.css's forced-colors block (the role layer maps it to Canvas). */
425
+ @media (forced-colors: active) {
426
+ :where(ui-card) {
427
+ border-color: CanvasText;
428
+ }
429
+
430
+ /* Scroll-mode bracket bleed-through (component-review, 2026-07-07): the shared `[data-fade-top]`/
431
+ `[data-fade-bottom]` block above drops container-box.css's mask entirely under forced-colors — but in
432
+ THIS revision the gradient is the SOLE occluder (the overlaid brackets are deliberately backgroundless,
433
+ "the gradient carries the occlusion"). With no mask AND no bracket fill, scrolled content bleeds straight
434
+ through the header/footer text in WHCM — text over text, illegible. Give the scroll-mode brackets an
435
+ opaque Canvas fallback, WHCM-only, so Kim's no-background normal-mode look is untouched. Both scroll-mode
436
+ triggers (`<ui-card scrollable>` and `<ui-card-content scrollable>` via `:has()`) are covered. (A ~6px
437
+ top/bottom gutter sliver above/below each bracket stays unmasked in WHCM — the same minor, pre-existing
438
+ exposure every other [data-box] consumer has; not chased here.) */
439
+ @scope (ui-card) {
440
+ :scope[scrollable] > :where(ui-card-header, ui-card-footer),
441
+ :scope:has(> ui-card-content[scrollable]) > :where(ui-card-header, ui-card-footer) {
442
+ background: Canvas;
443
+ }
444
+ }
445
+ }
@@ -0,0 +1,51 @@
1
+ import { type ReactiveProps } from '../../dom/index.js';
2
+ import { UIContainerElement } from '../../dom/container.js';
3
+ import './card-header.js';
4
+ import './card-content.js';
5
+ import './card-footer.js';
6
+ declare const props: {
7
+ scrollable: {
8
+ reflect: true;
9
+ type: import("../../index.js").PropType<boolean>;
10
+ default: boolean;
11
+ attribute?: string | false;
12
+ };
13
+ elevation: {
14
+ reflect: true;
15
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
16
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
17
+ attribute?: string | false;
18
+ };
19
+ brightness: {
20
+ reflect: true;
21
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
22
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
23
+ attribute?: string | false;
24
+ };
25
+ };
26
+ export interface UICardElement extends ReactiveProps<typeof props> {
27
+ }
28
+ export declare class UICardElement extends UIContainerElement {
29
+ static props: {
30
+ scrollable: {
31
+ reflect: true;
32
+ type: import("../../index.js").PropType<boolean>;
33
+ default: boolean;
34
+ attribute?: string | false;
35
+ };
36
+ elevation: {
37
+ reflect: true;
38
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
39
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
40
+ attribute?: string | false;
41
+ };
42
+ brightness: {
43
+ reflect: true;
44
+ type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
45
+ default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
46
+ attribute?: string | false;
47
+ };
48
+ };
49
+ protected connected(): void;
50
+ }
51
+ export {};
@@ -0,0 +1,61 @@
1
+ // card.ts — UICardElement, the surface container of the G9 card family (goals.md §G9 / decomp
2
+ // g9-containers slice s7). BEHAVIOUR + props + the opt-in ARIA role + self-define ONLY; the surface, the
3
+ // presence-driven region grid, the padding, the border and the one-level nested radius all live in card.css.
4
+ //
5
+ // `ui-card` is the FIRST non-form container with a default plane: it extends `UIContainerElement` (NOT
6
+ // form-associated — a container contributes nothing to a form) and folds the spreadable `surfaceProps`
7
+ // (elevation/brightness, ADR-0015) into its OWN `static props` — NOT `flexProps` (a card lays its regions out
8
+ // as a presence-driven grid, not a flex line; ADR-0016 governs row/column, not card). The two surface axes
9
+ // repoint the role-pure `--ui-container-bg`/`-tint` seam in controls/_surface/container.css; card.css adds the
10
+ // one card-specific surface decision the base does NOT make — an un-elevated card still reads as a SURFACE
11
+ // (the base default is `transparent`), so card.css seeds its own `--ui-container-bg: var(--md-sys-color-neutral-surface)`.
12
+ //
13
+ // Content model — host-as-grid over the region SUB-ELEMENTS (`ui-card-header`/`-content`/`-footer`, the
14
+ // ratified "regions = sub-elements"): the card does NOT `render()` a wrapper, so `render()` stays the
15
+ // inherited void and the agent's light-DOM regions are never clobbered. The presence-driven grid (card.css
16
+ // `:has()`) collapses the row of an absent region — no phantom gap.
17
+ //
18
+ // ARIA is minimal + opt-in (ADR-0014 widgets-not-elements posture): a card with an author-supplied accessible
19
+ // name (`aria-label`/`aria-labelledby`) reads as a `group` THROUGH `internals` (never a host `role` attribute);
20
+ // an unnamed card stays a generic container with no role. `controls → dom` is the allowed import direction.
21
+ import { prop } from "../../dom/index.js";
22
+ import { UIContainerElement } from "../../dom/container.js";
23
+ // Self-define the region sub-elements as a side effect of importing the family entry, so `ui-card` is a
24
+ // self-contained compound (the `ui-tabs` → `ui-tab`/`ui-tab-panel` precedent): importing `card.ts` — directly
25
+ // or via the controls barrel (decomp s12) — registers all four card tags, and a consumer who imports only
26
+ // `ui-card` still gets its regions. Side-effect imports (no binding needed): each module self-defines its tag.
27
+ import "./card-header.js";
28
+ import "./card-content.js";
29
+ import "./card-footer.js";
30
+ // `static props = { ...surfaceProps, scrollable }` — the elevation/brightness axes (no flexProps; a card lays
31
+ // its regions out in block flow, not a flex line) PLUS the ergonomic scroll signal. `<ui-card scrollable>`
32
+ // (Kim, 2026-07-07: "<ui-card-content> should have the mask and manage overflow ... it should be set to use
33
+ // 100% of its parent height when [scrollable]") puts the card into scroll mode: card.css makes ui-card-content
34
+ // ITSELF the scroll viewport (`overflow-y:auto`, filling 100% of the card via the flex carve-out — the shell's
35
+ // SOLE flex item), while the header/footer become OVERLAID PEERS (`position: absolute`, no background —
36
+ // removed from flow entirely) rather than sticky/flex siblings. The edge-fade mask paints directly on
37
+ // ui-card-content (wired in card-content.ts) — the brackets never carry it and stay crisp. It is the ergonomic
38
+ // convenience over marking the content region itself —
39
+ // `<ui-card-content scrollable>` (the A2UI-mapped signal); card.css triggers scroll mode off EITHER. Note the
40
+ // two are not identical at RUNTIME: the content signal is fully reactive, while this parent `scrollable` is read
41
+ // once at the content region's connect for the fade-mask arming — toggle scroll at runtime via the content
42
+ // region's `[scrollable]` (CSS scroll engages either way; only the JS fade differs). Spread, not inherited (props.ts has no static-props prototype merge — the ADR-0013 pattern the
43
+ // container base exposes `surfaceProps` for).
44
+ const props = {
45
+ ...UIContainerElement.surfaceProps,
46
+ scrollable: { ...prop.boolean(false), reflect: true },
47
+ };
48
+ export class UICardElement extends UIContainerElement {
49
+ static props = props;
50
+ connected() {
51
+ // Opt-in landmark: a NAMED card reads as an ARIA `group` (set THROUGH internals — never a host attribute,
52
+ // the family discipline). An unnamed card stays a generic container (no role). The accessible name is
53
+ // author-supplied (`aria-label` / `aria-labelledby`); read once at connect — dynamic re-naming is out of
54
+ // this slice's scope (documented in card.md). `role` is the only ARIA a card opts into.
55
+ if (this.hasAttribute('aria-label') || this.hasAttribute('aria-labelledby')) {
56
+ this.internals.role = 'group';
57
+ }
58
+ }
59
+ }
60
+ if (!customElements.get('ui-card'))
61
+ customElements.define('ui-card', UICardElement);