@neuravision/construct 2.2.0 → 2.3.1

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,66 @@ All notable changes to `@neuravision/construct` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.3.1] - 2026-09-17
9
+
10
+ ### Fixed
11
+
12
+ - Grid content slots clamp their content track. `ct-card__body`, `ct-modal__body`,
13
+ `ct-drawer__body`, `ct-field`, `ct-alert` and `ct-confirmation` sized their column to the
14
+ widest item's max-content size, so a table, a long identifier or any wide embedded content
15
+ stretched the surface past the width it was given instead of wrapping or scrolling inside it.
16
+ A scroll container one level down — the usual shape when a framework component host sits
17
+ between the slot and `ct-table-wrap` — was stretched with it and stopped scrolling, which
18
+ pushed the overflow out to the page or the application shell. Regression coverage measures
19
+ every slot's track against its own content box at 320px.
20
+ - Documented that `ct-table-wrap` needs `tabindex="0"`, `role="region"` and an accessible name:
21
+ now that wide tables scroll inside the wrapper again, keyboard users must be able to reach
22
+ the columns that are out of view.
23
+
24
+ ## [2.3.0] - 2026-09-16
25
+
26
+ ### Added
27
+
28
+ - Aperture: an additional opt-in signature look with a split oval, proportional offset,
29
+ cobalt surfaces and oversized grotesk typography. Reusable marks, wordmarks, posters,
30
+ offset heading lines, split rules, actions, panels, native radio choices and disclosures ship through the full component
31
+ bundle and the standalone `components/aperture.css` entry. Datum Grammar remains available.
32
+ - Cobalt and graphite primitives, Aperture semantic/component tokens, and contrast gates
33
+ for all four built-in theme scopes. The look includes reduced-motion, forced-colors,
34
+ keyboard-focus and touch-target support.
35
+ - A standalone interactive preview, integration guide, visual polish review, and eleven
36
+ Storybook examples covering the complete look, compact layout, themes, primitives,
37
+ monochrome composition, long action labels, Docs link isolation and right-to-left layout.
38
+ - Aperture's seam and offset carry through shared action hit areas, heading lines,
39
+ composition rules and staggered specimens, with a larger closing wordmark.
40
+
41
+ ### Fixed
42
+
43
+ - Storybook Docs prose link styles no longer override links inside embedded story
44
+ previews. Aperture action labels retain their theme colors instead of becoming
45
+ dark text on dark buttons with orange underlines. Regression coverage checks
46
+ primary, quiet and wordmark links at rest, on hover and with keyboard focus in
47
+ all four theme scopes.
48
+ - Aperture action labels retain their contrast on hover and active links. Logical
49
+ action padding preserves the seam in RTL; disclosure arrows and keyboard focus
50
+ follow the signature's direction and palette.
51
+ - Package export validation accepts Windows CRLF shebangs and checks POSIX executable
52
+ permission bits only on platforms that expose them.
53
+ - Updated the locked development dependencies `nanoid` and `postcss` to patched versions.
54
+
55
+ ## [2.2.1] - 2026-07-28
56
+
57
+ ### Fixed
58
+
59
+ - Datum registration marks (`ct-frame--datum`, `ct-card--datum`, `ct-datum-frame`) no longer leave
60
+ the owner's border showing outside the orange corner. The mark and the border shared one outer
61
+ arc, so both edges antialiased at the same radius and the neutral contour blended through — barely
62
+ visible on `--color-border-subtle` surfaces, a grey double line on stronger borders such as
63
+ `--color-border-default`. The mark now absorbs the owner's hairline: it starts one border width
64
+ further out and grows its stroke and radius to match, so the corner paints as a single orange arc
65
+ at every device pixel ratio. Its inner edge, length, and the datum's own stroke weight are
66
+ unchanged.
67
+
8
68
  ## [2.2.0] - 2026-07-25
9
69
 
10
70
  ### Added
package/README.md CHANGED
@@ -25,11 +25,12 @@
25
25
  Construct is a **single source of design truth** that ships portable CSS, validated design tokens, and opt-in headless behaviors — without framework lock-in. Style values live once as JSON tokens and compile to CSS custom properties, JSON, and typed TypeScript exports. Components use `ct-` classes and ARIA/data state, so they work anywhere that renders DOM.
26
26
 
27
27
  - 🎯 **Three token layers** — primitive, semantic, and component contracts with schema and contrast validation
28
- - 🧩 **Framework-agnostic** — 50 standalone component style entries and a full CSS bundle
28
+ - 🧩 **Framework-agnostic** — 51 standalone component style entries and a full CSS bundle
29
29
  - ♿ **Accessibility first** — WCAG 2.2 AA targets, tested ARIA/keyboard controllers, focus and forced-colors contracts
30
30
  - ⚙️ **Optional behavior** — nine typed, dependency-free ESM controllers; importing CSS never starts JavaScript
31
- - 🎨 **Three themes** — `light`, `dark`, `high-contrast`, with system-preference fallback
31
+ - 🎨 **Four theme scopes** — `light`, `dark`, `high-contrast`, `high-contrast-dark`, with system-preference fallback
32
32
  - 📐 **The "datum" grammar** — a signature orange reference line for focus / active / current state
33
+ - ◐ **Aperture** — an additional signature look with a split oval, cobalt surfaces and expressive typography; [usage and preview](docs/aperture.md)
33
34
  - 🛠️ **Typed tokens** — autocomplete-friendly TypeScript exports
34
35
  - 📖 **Interactive docs** — every component documented & a11y-tested in [Storybook](https://samyssmile.github.io/construct/)
35
36
 
@@ -133,7 +134,7 @@ npx construct-theme --theme node_modules/@neuravision/construct/themes/walnut.th
133
134
 
134
135
  ## 🧩 Components
135
136
 
136
- 50 standalone component style entries, documented and accessibility-tested in [Storybook](https://samyssmile.github.io/construct/).
137
+ 51 standalone component style entries, documented and accessibility-tested in [Storybook](https://samyssmile.github.io/construct/).
137
138
 
138
139
  | Category | Components |
139
140
  |----------|-----------|
@@ -216,6 +217,7 @@ npm test # run behavior, Storybook, touch & forced-colors browse
216
217
 
217
218
  - [Design Guidelines](docs/guidelines.md) — do's, don'ts & accessibility patterns
218
219
  - [Component Usage](components/README.md) — HTML patterns and examples
220
+ - [Aperture](docs/aperture.md) — split-oval signature, integration and theme examples
219
221
  - [Token Reference](tokens/README.md) — token structure & naming
220
222
  - [Architecture](docs/architecture.md) — package layers and ownership boundaries
221
223
  - [Headless Behaviors](docs/behaviors.md) — typed controllers and lifecycle
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <html lang="de" data-theme="light">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="theme-color" content="#F5F5F0">
7
+ <meta name="description" content="Aperture – ein zusätzlicher Signature Look für Construct. Kobaltblau, große Typografie und ein versetztes, geteiltes Oval.">
8
+ <title>Aperture — Construct Signature Look</title>
9
+ <link rel="stylesheet" href="./foundations.css">
10
+ <link rel="stylesheet" href="./standalone/aperture.css">
11
+ <link rel="stylesheet" href="./examples/aperture/showcase.css">
12
+ </head>
13
+ <body>
14
+ <div id="aperture-preview"></div>
15
+ <noscript>Diese interaktive Vorschau benötigt JavaScript. Die Aperture-Komponenten selbst bestehen aus HTML und CSS.</noscript>
16
+ <script type="module">
17
+ import { createApertureShowcase } from './examples/aperture/showcase.js';
18
+ document.querySelector('#aperture-preview').replaceWith(createApertureShowcase({ themeControls: true }));
19
+ </script>
20
+ </body>
21
+ </html>
@@ -214,6 +214,11 @@ Content card with header, body, and footer:
214
214
  </section>
215
215
  ```
216
216
 
217
+ `ct-card__body` clamps its content track, as every grid content slot does (`ct-modal__body`,
218
+ `ct-drawer__body`, `ct-field`, `ct-alert`, `ct-confirmation`). Content wider than the card wraps,
219
+ truncates, or scrolls inside its own scroll container — `ct-table-wrap` for tables — instead of
220
+ stretching the card past the width it was given.
221
+
217
222
  ### Frame
218
223
 
219
224
  Owned surface for figures, code samples, media, and callouts. The outer frame owns its border,
@@ -235,6 +240,38 @@ content is a figure, use the `figure` itself as `ct-frame__content` so its `figc
235
240
  valid direct child. Customize the owned surface with `--ct-frame-background`, `--ct-frame-border`,
236
241
  `--ct-frame-border-width`, `--ct-frame-radius`, and `--ct-frame-shadow`.
237
242
 
243
+ ### Aperture Signature Look
244
+
245
+ An additional opt-in grammar: split ovals, a proportional offset, cobalt and paper,
246
+ and oversized grotesk typography. Import `@neuravision/construct/components/aperture.css`
247
+ or use the full component bundle. Wrap the composition in `.ct-aperture`.
248
+
249
+ ```html
250
+ <section class="ct-aperture">
251
+ <h2 class="ct-aperture-heading">Room for character.</h2>
252
+ <figure class="ct-aperture-poster">
253
+ <div class="ct-aperture-poster__art">
254
+ <span class="ct-aperture-mark" aria-hidden="true"></span>
255
+ </div>
256
+ <figcaption class="ct-aperture-poster__title">Open by design.</figcaption>
257
+ </figure>
258
+ </section>
259
+ ```
260
+
261
+ Primitives: `ct-aperture-mark`, `ct-aperture-wordmark`, `ct-aperture-label`,
262
+ `ct-aperture-display`, `ct-aperture-heading`, `ct-aperture-line`,
263
+ decorative `ct-aperture-rule`, `ct-aperture-poster`,
264
+ `ct-aperture-action` (and `--quiet`), `ct-aperture-panel`, native radio
265
+ `ct-aperture-choices` / `ct-aperture-choice`, and native `ct-aperture-disclosure`.
266
+
267
+ Action text and its displaced arrow disc remain inside one native link or button;
268
+ the visual seam is part of the same hit area. Put `ct-aperture-line` on spans inside
269
+ one heading to offset later lines, and hide decorative rules with `aria-hidden="true"`.
270
+
271
+ All colors use `--component-aperture-*` tokens. Mark size can be overridden with
272
+ `--ct-aperture-mark-size`. See [the design and integration guide](../docs/aperture.md)
273
+ and **Patterns → Aperture Grammar** in Storybook.
274
+
238
275
  ### Datum Primitives
239
276
 
240
277
  The reference-line grammar from `components/datum.css` — Construct's orange signature as reusable
@@ -274,7 +311,7 @@ One datum per surface — see `docs/guidelines.md`.
274
311
  Basic data table with variants:
275
312
 
276
313
  ```html
277
- <div class="ct-table-wrap">
314
+ <div class="ct-table-wrap" tabindex="0" role="region" aria-label="Projects">
278
315
  <table class="ct-table ct-table--striped">
279
316
  <thead>
280
317
  <tr>
@@ -299,6 +336,10 @@ Basic data table with variants:
299
336
  </div>
300
337
  ```
301
338
 
339
+ `ct-table-wrap` scrolls a table that is wider than its container. A scrollable region needs a
340
+ keyboard tab stop and its own accessible name, so give the wrapper `tabindex="0"`, `role="region"`
341
+ and an `aria-label` — otherwise keyboard users cannot reach the columns that are out of view.
342
+
302
343
  Variants:
303
344
  - `ct-table--striped` - Alternating row backgrounds
304
345
  - `ct-table--compact` - Reduced padding
@@ -2,15 +2,24 @@
2
2
  Owning components provide the inset and radius through local properties so
3
3
  every standalone entry renders the same geometry without copying it. */
4
4
  :is(.ct-datum-frame, .ct-card--datum, .ct-frame--datum)::before {
5
+ /* The mark absorbs the owner's hairline instead of merely covering it. Both
6
+ arcs are antialiased at the same radius when they share one outer edge, so
7
+ the owner's border blends through outside the mark — invisible on a subtle
8
+ border, a grey double line on a strong one. Starting the mark one hairline
9
+ further out, and growing its stroke and radius by the same amount, makes
10
+ the corner read as a single orange mark. The inner edge, the mark's length,
11
+ and the datum's own stroke weight are unchanged. */
12
+ --ct-datum-mark-bleed: var(--border-thin);
13
+
5
14
  content: '';
6
15
  position: absolute;
7
- inset-block-start: var(--ct-datum-mark-inset);
8
- inset-inline-start: var(--ct-datum-mark-inset);
9
- width: var(--component-datum-mark-size);
10
- height: var(--component-datum-mark-size);
11
- border-block-start: var(--border-medium) solid var(--color-brand-accent);
12
- border-inline-start: var(--border-medium) solid var(--color-brand-accent);
13
- border-start-start-radius: var(--ct-datum-mark-radius);
16
+ inset-block-start: calc(var(--ct-datum-mark-inset) - var(--ct-datum-mark-bleed));
17
+ inset-inline-start: calc(var(--ct-datum-mark-inset) - var(--ct-datum-mark-bleed));
18
+ width: calc(var(--component-datum-mark-size) + var(--ct-datum-mark-bleed));
19
+ height: calc(var(--component-datum-mark-size) + var(--ct-datum-mark-bleed));
20
+ border-block-start: calc(var(--border-medium) + var(--ct-datum-mark-bleed)) solid var(--color-brand-accent);
21
+ border-inline-start: calc(var(--border-medium) + var(--ct-datum-mark-bleed)) solid var(--color-brand-accent);
22
+ border-start-start-radius: calc(var(--ct-datum-mark-radius) + var(--ct-datum-mark-bleed));
14
23
  z-index: 2;
15
24
  pointer-events: none;
16
25
  }
@@ -4,7 +4,9 @@
4
4
  --ct-alert-accent: var(--color-brand-primary);
5
5
 
6
6
  display: grid;
7
- grid-template-columns: auto 1fr;
7
+ /* The content column is clamped so long words or wide embedded content
8
+ stay inside the alert instead of stretching it. */
9
+ grid-template-columns: auto minmax(0, 1fr);
8
10
  gap: var(--space-4);
9
11
  padding: var(--space-5) var(--space-6);
10
12
  border-radius: var(--radius-md);
@@ -24,6 +26,7 @@
24
26
 
25
27
  .ct-alert__content {
26
28
  display: grid;
29
+ grid-template-columns: minmax(0, 1fr);
27
30
  gap: var(--space-2);
28
31
  }
29
32
 
@@ -0,0 +1,441 @@
1
+ /* Aperture: a split oval, a deliberate offset, generous negative space.
2
+ Opt in with .ct-aperture. No global theme or Datum overrides.
3
+ Marks are decorative: render them with aria-hidden="true". */
4
+ .ct-aperture {
5
+ container-type: inline-size;
6
+ background: var(--component-aperture-paper);
7
+ color: var(--component-aperture-ink);
8
+ font-family: var(--component-aperture-font-family);
9
+ font-size: var(--font-size-md);
10
+ line-height: 1.5;
11
+ accent-color: var(--component-aperture-ink);
12
+ }
13
+
14
+ .ct-aperture,
15
+ .ct-aperture *,
16
+ .ct-aperture *::before,
17
+ .ct-aperture *::after {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ .ct-aperture :is(h1, h2, h3, p, figure) {
22
+ margin: 0;
23
+ font-family: inherit;
24
+ }
25
+
26
+ .ct-aperture :is(a, button, summary, input, [tabindex]):focus-visible,
27
+ .ct-aperture-action:focus-visible,
28
+ .ct-aperture-choice:has(input:focus-visible) {
29
+ outline: 3px solid currentColor;
30
+ outline-offset: 5px;
31
+ }
32
+
33
+ .ct-aperture ::selection {
34
+ color: var(--component-aperture-paper);
35
+ background: var(--component-aperture-ink);
36
+ }
37
+
38
+ /* The same two half-ovals work at wordmark, control, and poster scale.
39
+ Percentages keep the seam and vertical offset proportional at every size. */
40
+ .ct-aperture-mark {
41
+ position: relative;
42
+ display: inline-block;
43
+ flex: none;
44
+ inline-size: var(--ct-aperture-mark-size, var(--component-aperture-mark-size));
45
+ aspect-ratio: 1;
46
+ color: inherit;
47
+ }
48
+
49
+ .ct-aperture-mark::before,
50
+ .ct-aperture-mark::after {
51
+ content: '';
52
+ position: absolute;
53
+ inline-size: 46%;
54
+ block-size: 84%;
55
+ background: currentColor;
56
+ }
57
+
58
+ .ct-aperture-mark::before {
59
+ inset-block-start: 0;
60
+ inset-inline-start: 0;
61
+ border-start-start-radius: 100% 50%;
62
+ border-end-start-radius: 100% 50%;
63
+ }
64
+
65
+ .ct-aperture-mark::after {
66
+ inset-block-end: 0;
67
+ inset-inline-end: 0;
68
+ border-start-end-radius: 100% 50%;
69
+ border-end-end-radius: 100% 50%;
70
+ }
71
+
72
+ .ct-aperture-wordmark {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--space-4);
76
+ color: inherit;
77
+ font-family: var(--component-aperture-font-family);
78
+ font-size: var(--font-size-2xl);
79
+ font-weight: var(--font-weight-bold);
80
+ letter-spacing: -0.065em;
81
+ line-height: 1;
82
+ text-decoration: none;
83
+ min-block-size: var(--component-aperture-action-height);
84
+ }
85
+
86
+ .ct-aperture-wordmark:is(:hover, :active) {
87
+ color: inherit;
88
+ }
89
+
90
+ .ct-aperture-label {
91
+ display: block;
92
+ font-size: var(--font-size-xs);
93
+ font-weight: var(--font-weight-bold);
94
+ letter-spacing: 0.08em;
95
+ line-height: 1.5;
96
+ text-transform: uppercase;
97
+ }
98
+
99
+ .ct-aperture .ct-aperture-display {
100
+ font-size: clamp(3.25rem, 1rem + 9.6cqi, 10.5rem);
101
+ font-weight: var(--font-weight-bold);
102
+ letter-spacing: -0.075em;
103
+ line-height: 0.91;
104
+ overflow-wrap: anywhere;
105
+ }
106
+
107
+ .ct-aperture .ct-aperture-heading {
108
+ font-size: clamp(2.25rem, 1.5rem + 3.5cqi, 5rem);
109
+ font-weight: var(--font-weight-bold);
110
+ letter-spacing: -0.065em;
111
+ line-height: 1.02;
112
+ overflow-wrap: anywhere;
113
+ }
114
+
115
+ /* A second line steps out of the first line's axis, like the two halves
116
+ of the mark. Keep the text in normal flow so zoom and wrapping still work. */
117
+ .ct-aperture-line {
118
+ display: block;
119
+ }
120
+
121
+ .ct-aperture-line + .ct-aperture-line {
122
+ margin-inline-start: 0.24em;
123
+ }
124
+
125
+ /* One interrupted rule, with the same 46 / 8 / 46 proportions as the mark.
126
+ Decorative only: render with aria-hidden="true". */
127
+ .ct-aperture-rule {
128
+ display: grid;
129
+ grid-template-columns: 1fr 1fr;
130
+ column-gap: 8%;
131
+ padding-block: 1px 7px;
132
+ color: var(--component-aperture-border);
133
+ }
134
+
135
+ .ct-aperture-rule::before,
136
+ .ct-aperture-rule::after {
137
+ content: '';
138
+ border-block-start: 1px solid currentColor;
139
+ }
140
+
141
+ .ct-aperture-rule::after {
142
+ transform: translateY(6px);
143
+ }
144
+
145
+ /* The poster is a figure, never a cropped interactive surface. */
146
+ .ct-aperture-poster {
147
+ display: flex;
148
+ flex-direction: column;
149
+ justify-content: space-between;
150
+ gap: var(--space-8);
151
+ min-inline-size: 0;
152
+ padding: clamp(1.5rem, 3cqi, 3rem);
153
+ color: var(--component-aperture-on-accent);
154
+ background: var(--component-aperture-accent);
155
+ border-radius: var(--component-aperture-panel-radius);
156
+ }
157
+
158
+ .ct-aperture-poster__art {
159
+ display: grid;
160
+ place-items: center;
161
+ flex: 1;
162
+ padding-block: var(--space-7);
163
+ }
164
+
165
+ .ct-aperture-poster__art .ct-aperture-mark {
166
+ --ct-aperture-mark-size: 82%;
167
+ }
168
+
169
+ .ct-aperture-poster__caption {
170
+ display: flex;
171
+ flex-wrap: wrap;
172
+ align-items: end;
173
+ justify-content: space-between;
174
+ gap: var(--space-5);
175
+ font-size: var(--font-size-xs);
176
+ }
177
+
178
+ .ct-aperture-poster__title {
179
+ font-size: clamp(2rem, 4cqi, 4rem);
180
+ font-weight: var(--font-weight-bold);
181
+ letter-spacing: -0.075em;
182
+ line-height: 1;
183
+ }
184
+
185
+ /* The label and disc share one hit area. The open seam and lowered disc
186
+ carry the mark's offset into actions without splitting keyboard focus. */
187
+ .ct-aperture-action {
188
+ position: relative;
189
+ isolation: isolate;
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ gap: var(--space-6);
194
+ min-block-size: calc(var(--component-aperture-action-height) + 8px);
195
+ max-inline-size: 100%;
196
+ padding-block: 0 8px;
197
+ padding-inline: var(--space-6) 0;
198
+ border: 0;
199
+ border-radius: 0;
200
+ background: transparent;
201
+ color: var(--component-aperture-paper);
202
+ font-family: var(--component-aperture-font-family);
203
+ font-size: var(--font-size-sm);
204
+ font-weight: var(--font-weight-bold);
205
+ line-height: 1.4;
206
+ text-align: start;
207
+ text-decoration: none;
208
+ overflow-wrap: anywhere;
209
+ cursor: pointer;
210
+ }
211
+
212
+ .ct-aperture-action:is(:hover, :active) {
213
+ color: var(--component-aperture-paper);
214
+ }
215
+
216
+ .ct-aperture-action::before {
217
+ content: '';
218
+ position: absolute;
219
+ z-index: -1;
220
+ inset-block: 0 8px;
221
+ inset-inline: 0 calc(var(--component-aperture-action-height) + 6px);
222
+ border: 1px solid var(--component-aperture-ink);
223
+ border-radius: var(--radius-round);
224
+ background: var(--component-aperture-ink);
225
+ }
226
+
227
+ .ct-aperture-action__icon {
228
+ display: grid;
229
+ place-items: center;
230
+ flex: none;
231
+ inline-size: var(--component-aperture-action-height);
232
+ block-size: var(--component-aperture-action-height);
233
+ border: 1px solid var(--component-aperture-ink);
234
+ border-radius: 50%;
235
+ background: var(--component-aperture-ink);
236
+ color: var(--component-aperture-paper);
237
+ font-size: var(--font-size-xl);
238
+ font-weight: var(--font-weight-regular);
239
+ transform: translateY(8px);
240
+ transition: transform var(--duration-normal) var(--easing-standard);
241
+ }
242
+
243
+ .ct-aperture-action__icon svg {
244
+ inline-size: 20px;
245
+ block-size: 20px;
246
+ }
247
+
248
+ .ct-aperture-action--quiet,
249
+ .ct-aperture-action--quiet:is(:hover, :active) {
250
+ color: var(--component-aperture-ink);
251
+ }
252
+
253
+ .ct-aperture-action--quiet::before,
254
+ .ct-aperture-action--quiet .ct-aperture-action__icon {
255
+ background: transparent;
256
+ color: var(--component-aperture-ink);
257
+ }
258
+
259
+ .ct-aperture-action:focus-visible {
260
+ outline-color: var(--component-aperture-ink);
261
+ }
262
+
263
+ .ct-aperture-action:disabled {
264
+ cursor: not-allowed;
265
+ opacity: var(--opacity-disabled);
266
+ }
267
+
268
+ @media (hover: hover) {
269
+ .ct-aperture-action:where(:not(:disabled)):hover .ct-aperture-action__icon {
270
+ transform: translateY(0);
271
+ }
272
+
273
+ .ct-aperture-action:where(:not(:disabled)):hover {
274
+ text-decoration: underline;
275
+ text-underline-offset: 0.2em;
276
+ }
277
+ }
278
+
279
+ /* Native radio inputs retain keyboard behavior. The text, inset border and
280
+ weight identify selection without depending on the accent color. */
281
+ .ct-aperture-choices {
282
+ display: flex;
283
+ flex-wrap: wrap;
284
+ gap: var(--space-2);
285
+ margin: 0;
286
+ padding: 0;
287
+ border: 0;
288
+ }
289
+
290
+ .ct-aperture-choice {
291
+ position: relative;
292
+ display: inline-flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ min-inline-size: 44px;
296
+ min-block-size: 44px;
297
+ padding: var(--space-3) var(--space-5);
298
+ border: 1px solid transparent;
299
+ border-radius: var(--radius-round);
300
+ font: inherit;
301
+ font-size: var(--font-size-xs);
302
+ cursor: pointer;
303
+ }
304
+
305
+ .ct-aperture-choice input,
306
+ .ct-aperture-choices legend {
307
+ position: absolute;
308
+ inline-size: 1px;
309
+ block-size: 1px;
310
+ padding: 0;
311
+ overflow: hidden;
312
+ clip-path: inset(50%);
313
+ white-space: nowrap;
314
+ }
315
+
316
+ .ct-aperture-choice:has(input:checked) {
317
+ border-color: currentColor;
318
+ font-weight: var(--font-weight-bold);
319
+ }
320
+
321
+ .ct-aperture-panel {
322
+ min-inline-size: 0;
323
+ padding: clamp(1.5rem, 3cqi, 3rem);
324
+ border: 1px solid var(--component-aperture-border);
325
+ border-radius: var(--component-aperture-panel-radius);
326
+ background: var(--component-aperture-surface);
327
+ }
328
+
329
+ .ct-aperture-disclosure {
330
+ border-block-start: 1px solid var(--component-aperture-border);
331
+ }
332
+
333
+ .ct-aperture-disclosure:last-child {
334
+ border-block-end: 1px solid var(--component-aperture-border);
335
+ }
336
+
337
+ .ct-aperture-disclosure summary {
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: space-between;
341
+ gap: var(--space-6);
342
+ min-block-size: var(--component-aperture-action-height);
343
+ padding-block: var(--space-6);
344
+ list-style: none;
345
+ font-size: var(--font-size-lg);
346
+ font-weight: var(--font-weight-bold);
347
+ cursor: pointer;
348
+ }
349
+
350
+ .ct-aperture-disclosure summary::-webkit-details-marker {
351
+ display: none;
352
+ }
353
+
354
+ .ct-aperture-disclosure summary::after {
355
+ content: '';
356
+ flex: none;
357
+ inline-size: 10px;
358
+ block-size: 10px;
359
+ margin-inline-end: var(--space-3);
360
+ border-inline-end: 2px solid currentColor;
361
+ border-block-end: 2px solid currentColor;
362
+ transform: rotate(45deg);
363
+ }
364
+
365
+ .ct-aperture-disclosure[open] summary::after {
366
+ transform: rotate(225deg);
367
+ }
368
+
369
+ .ct-aperture-disclosure summary:dir(rtl)::after {
370
+ transform: rotate(-45deg);
371
+ }
372
+
373
+ .ct-aperture-disclosure[open] summary:dir(rtl)::after {
374
+ transform: rotate(135deg);
375
+ }
376
+
377
+ .ct-aperture-disclosure > :not(summary) {
378
+ max-inline-size: 60ch;
379
+ padding-block-end: var(--space-7);
380
+ color: var(--component-aperture-muted);
381
+ }
382
+
383
+ @media (prefers-reduced-motion: reduce) {
384
+ .ct-aperture-action__icon {
385
+ transition: none;
386
+ }
387
+
388
+ .ct-aperture-action:hover .ct-aperture-action__icon {
389
+ transform: translateY(8px);
390
+ }
391
+ }
392
+
393
+ @media (forced-colors: active) {
394
+ .ct-aperture {
395
+ background: Canvas;
396
+ color: CanvasText;
397
+ }
398
+
399
+ .ct-aperture-mark::before,
400
+ .ct-aperture-mark::after {
401
+ background: CanvasText;
402
+ forced-color-adjust: none;
403
+ }
404
+
405
+ .ct-aperture-poster,
406
+ .ct-aperture-panel {
407
+ background: Canvas;
408
+ color: CanvasText;
409
+ border: 1px solid CanvasText;
410
+ }
411
+
412
+ .ct-aperture-action,
413
+ .ct-aperture-action:is(:hover, :active),
414
+ .ct-aperture-action::before,
415
+ .ct-aperture-action__icon {
416
+ border-color: ButtonText;
417
+ background: ButtonFace;
418
+ color: ButtonText;
419
+ }
420
+
421
+ .ct-aperture-rule {
422
+ color: CanvasText;
423
+ }
424
+
425
+ .ct-aperture-action:focus-visible {
426
+ outline-color: Highlight;
427
+ }
428
+
429
+ .ct-aperture-choice:has(input:checked) {
430
+ border: 2px solid Highlight;
431
+ text-decoration: underline;
432
+ text-underline-offset: 0.25em;
433
+ }
434
+
435
+ .ct-aperture-action:disabled,
436
+ .ct-aperture-action:disabled::before,
437
+ .ct-aperture-action:disabled .ct-aperture-action__icon {
438
+ color: GrayText;
439
+ border-color: GrayText;
440
+ }
441
+ }
@@ -33,6 +33,12 @@
33
33
 
34
34
  .ct-card__body {
35
35
  display: grid;
36
+ /* Content slots hold whatever the product projects into them. An `auto`
37
+ track grows to the widest item's max-content size, so a table, a long
38
+ identifier or a media element pushes the surface past its own width
39
+ instead of scrolling or wrapping inside it. A clamped track keeps the
40
+ slot at the surface width and leaves overflow to the item. */
41
+ grid-template-columns: minmax(0, 1fr);
36
42
  gap: var(--space-4);
37
43
  }
38
44