@ambientcss/components 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/index.cjs +1540 -391
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +749 -52
- package/dist/index.d.ts +749 -52
- package/dist/index.js +1501 -391
- package/dist/index.js.map +1 -1
- package/dist/styles.css +998 -291
- package/package.json +2 -2
- package/src/components/AmbientButton.tsx +23 -27
- package/src/components/AmbientFader.tsx +29 -115
- package/src/components/AmbientKnob.tsx +58 -220
- package/src/components/AmbientPanel.tsx +8 -2
- package/src/components/AmbientProvider.tsx +3 -0
- package/src/components/AmbientSelect.tsx +40 -0
- package/src/components/AmbientSlider.tsx +28 -113
- package/src/components/AmbientSwitch.tsx +58 -58
- package/src/controls/AmbientBank.tsx +138 -0
- package/src/controls/AmbientLatch.tsx +78 -0
- package/src/controls/AmbientPress.tsx +74 -0
- package/src/controls/AmbientRotary.tsx +94 -0
- package/src/controls/AmbientTravel.tsx +83 -0
- package/src/core/context.tsx +46 -0
- package/src/core/controllable.ts +33 -0
- package/src/core/dev.ts +15 -0
- package/src/core/frames.tsx +63 -0
- package/src/core/kit.tsx +107 -0
- package/src/core/material.ts +27 -0
- package/src/core/numeric.ts +88 -0
- package/src/core/types.ts +116 -0
- package/src/core/useBank.ts +163 -0
- package/src/core/useLatch.ts +54 -0
- package/src/core/usePress.ts +120 -0
- package/src/core/useRotary.ts +253 -0
- package/src/core/useTravel.ts +141 -0
- package/src/index.ts +122 -2
- package/src/kits/console.tsx +80 -0
- package/src/kits/grounded.tsx +113 -0
- package/src/parts/bank.tsx +32 -0
- package/src/parts/console.tsx +99 -0
- package/src/parts/knob.tsx +203 -0
- package/src/parts/latch.tsx +33 -0
- package/src/parts/press.tsx +56 -0
- package/src/parts/travel.tsx +70 -0
- package/src/styles.css +998 -291
package/dist/styles.css
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
--ambx-label-size: calc(var(--ambx-grid) * 2 + var(--ambx-grid-half));
|
|
29
29
|
--ambx-label-line: calc(var(--ambx-grid) * 3);
|
|
30
30
|
--ambx-control-label-gap: calc(var(--ambx-grid) * 2);
|
|
31
|
+
/* The panel accent a kit paints its lit states with. Defaults to the
|
|
32
|
+
scene's own highlight so a kit inherits the app's colour rather than
|
|
33
|
+
imposing one. */
|
|
34
|
+
--ambx-accent: var(--amb-highlight-color);
|
|
31
35
|
--ambx-knob-label-clearance: calc(var(--ambx-grid) * 2);
|
|
32
36
|
--ambx-slider-label-clearance: calc(var(--ambx-grid) * 2);
|
|
33
37
|
--ambx-fader-label-clearance: calc(var(--ambx-grid) * 4);
|
|
@@ -41,19 +45,11 @@
|
|
|
41
45
|
rather than shipped as a global reset: apps that already reset globally
|
|
42
46
|
(apps/demo does) see no change, and apps that do not are no longer
|
|
43
47
|
silently broken. */
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.amb-switch,
|
|
47
|
-
.amb-fader,
|
|
48
|
-
.amb-slider,
|
|
48
|
+
.ambx-control,
|
|
49
|
+
.ambx-control *,
|
|
49
50
|
.ambx-panel,
|
|
50
|
-
.ambx-stack,
|
|
51
|
-
.amb-button *,
|
|
52
|
-
.amb-knob *,
|
|
53
|
-
.amb-switch *,
|
|
54
|
-
.amb-fader *,
|
|
55
|
-
.amb-slider *,
|
|
56
51
|
.ambx-panel *,
|
|
52
|
+
.ambx-stack,
|
|
57
53
|
.ambx-stack * {
|
|
58
54
|
box-sizing: border-box;
|
|
59
55
|
}
|
|
@@ -76,6 +72,256 @@
|
|
|
76
72
|
letter-spacing: var(--ambx-type-tracking);
|
|
77
73
|
}
|
|
78
74
|
|
|
75
|
+
/* ------------------------------------------------------------------ *\
|
|
76
|
+
The control base: state channel, frames, motion.
|
|
77
|
+
|
|
78
|
+
Every control publishes its state on its own root — as custom properties
|
|
79
|
+
for parts that are pure CSS, and as data attributes for the discrete
|
|
80
|
+
conditions a part cannot detect for itself. The properties INHERIT, which
|
|
81
|
+
is the whole point (any frame can read --ambx-angle without being handed
|
|
82
|
+
it), and is also why every one of them is reset here: a control nested
|
|
83
|
+
inside another control's frame would otherwise sweep along with its
|
|
84
|
+
ancestor.
|
|
85
|
+
\* ------------------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
/* Registered so they interpolate. That is what turns "smooth or stepped"
|
|
88
|
+
into a CSS question rather than a JS animation loop — the same reason
|
|
89
|
+
@ambientcss/css registers --amb-elevation. */
|
|
90
|
+
@property --ambx-percent {
|
|
91
|
+
syntax: "<number>";
|
|
92
|
+
inherits: true;
|
|
93
|
+
initial-value: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@property --ambx-angle {
|
|
97
|
+
syntax: "<angle>";
|
|
98
|
+
inherits: true;
|
|
99
|
+
initial-value: 0deg;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ambx-control {
|
|
103
|
+
position: relative;
|
|
104
|
+
--ambx-value: 0;
|
|
105
|
+
--ambx-percent: 0;
|
|
106
|
+
--ambx-angle: 0deg;
|
|
107
|
+
--ambx-travel-start: 0deg;
|
|
108
|
+
--ambx-travel-sweep: 0deg;
|
|
109
|
+
--ambx-detents: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ambx-control:focus-visible {
|
|
113
|
+
outline-offset: var(--ambx-grid-half);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A frame is a MARKER by default, not a box: it contributes paint order and
|
|
117
|
+
the [data-frame] hook and nothing to layout. That default is load-bearing
|
|
118
|
+
rather than lazy — a button takes its height from its cap in normal flow
|
|
119
|
+
and its width from min-width PLUS the cap's legend, so wrapping the cap in
|
|
120
|
+
a positioned box would collapse the control to nothing. */
|
|
121
|
+
.ambx-frame {
|
|
122
|
+
display: contents;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Where the control has a size of its own, frames become boxes and hand
|
|
126
|
+
their part a correct one. A part that means to fill its frame does it at
|
|
127
|
+
`position: absolute; inset: 0` — which also blockifies it, where
|
|
128
|
+
`width/height: 100%` on a bare <span> would silently measure zero. */
|
|
129
|
+
.ambx-rotary > .ambx-frame,
|
|
130
|
+
.ambx-travel > .ambx-frame,
|
|
131
|
+
.ambx-latch > .ambx-frame {
|
|
132
|
+
display: block;
|
|
133
|
+
position: absolute;
|
|
134
|
+
inset: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The panel frame is the one allowed past the control's box — a scale ring
|
|
138
|
+
sits outside the knob it belongs to. It takes no pointer events, so
|
|
139
|
+
printing a ring never enlarges the grab area. */
|
|
140
|
+
.ambx-rotary > .ambx-frame-panel,
|
|
141
|
+
.ambx-travel > .ambx-frame-panel {
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Motion. The actuator is the only frame that moves, and it moves by
|
|
146
|
+
transform — which is what lets the two transition rules below cover every
|
|
147
|
+
family whose actuator is a box. */
|
|
148
|
+
.ambx-rotary > .ambx-frame-actuator {
|
|
149
|
+
transform: rotate(var(--ambx-angle));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Follow the pointer 1:1 while dragging, ease otherwise. No JS: the root
|
|
153
|
+
already carries data-dragging. */
|
|
154
|
+
.ambx-control[data-animate="auto"] > .ambx-frame-actuator,
|
|
155
|
+
.ambx-control[data-animate="ease"] > .ambx-frame-actuator {
|
|
156
|
+
transition: transform 120ms ease-out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.ambx-control[data-animate="auto"][data-dragging] > .ambx-frame-actuator,
|
|
160
|
+
.ambx-control[data-animate="follow"] > .ambx-frame-actuator,
|
|
161
|
+
.ambx-control[data-animate="snap"] > .ambx-frame-actuator {
|
|
162
|
+
transition: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@media (prefers-reduced-motion: reduce) {
|
|
166
|
+
.ambx-control > .ambx-frame-actuator {
|
|
167
|
+
transition: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.ambx-control[data-disabled] {
|
|
172
|
+
cursor: not-allowed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Rotary geometry. Square, sized off one property so a user-supplied part
|
|
176
|
+
can scale with the control — a part cannot match .ambx-rotary-lg, it does
|
|
177
|
+
not know the class exists, but it can read --ambx-size. */
|
|
178
|
+
.ambx-rotary {
|
|
179
|
+
display: grid;
|
|
180
|
+
place-items: center;
|
|
181
|
+
touch-action: none;
|
|
182
|
+
user-select: none;
|
|
183
|
+
width: var(--ambx-size);
|
|
184
|
+
height: var(--ambx-size);
|
|
185
|
+
--ambx-size: calc(var(--ambx-grid) * 16); /* 64px */
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.ambx-rotary-sm { --ambx-size: calc(var(--ambx-grid) * 12); } /* 48px */
|
|
189
|
+
.ambx-rotary-md { --ambx-size: calc(var(--ambx-grid) * 16); } /* 64px */
|
|
190
|
+
.ambx-rotary-lg { --ambx-size: calc(var(--ambx-grid) * 20); } /* 80px */
|
|
191
|
+
|
|
192
|
+
/* Travel geometry. Track length, track width and thumb all come off one
|
|
193
|
+
size step, and the slider's and fader's measured tables turn out to be
|
|
194
|
+
the same table — 6/8/12 track against a 20/24/32 thumb — so one set of
|
|
195
|
+
steps serves both, with the fader's pill taking the extra 1.5x height its
|
|
196
|
+
referent has. */
|
|
197
|
+
.ambx-travel {
|
|
198
|
+
position: relative;
|
|
199
|
+
touch-action: none;
|
|
200
|
+
user-select: none;
|
|
201
|
+
--ambx-size: calc(var(--ambx-grid) * 6); /* 24px thumb */
|
|
202
|
+
--ambx-travel-track: calc(var(--ambx-grid) * 2); /* 8px */
|
|
203
|
+
--ambx-travel-length: calc(var(--ambx-grid) * 30); /* 120px */
|
|
204
|
+
--ambx-travel-thumb-w: var(--ambx-size);
|
|
205
|
+
--ambx-travel-thumb-h: var(--ambx-size);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ambx-travel-sm {
|
|
209
|
+
--ambx-size: calc(var(--ambx-grid) * 5); /* 20px */
|
|
210
|
+
--ambx-travel-track: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.ambx-travel-md {
|
|
214
|
+
--ambx-size: calc(var(--ambx-grid) * 6); /* 24px */
|
|
215
|
+
--ambx-travel-track: calc(var(--ambx-grid) * 2); /* 8px */
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.ambx-travel-lg {
|
|
219
|
+
--ambx-size: calc(var(--ambx-grid) * 8); /* 32px */
|
|
220
|
+
--ambx-travel-track: calc(var(--ambx-grid) * 3); /* 12px */
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ambx-travel-horizontal {
|
|
224
|
+
width: var(--ambx-travel-length);
|
|
225
|
+
height: var(--ambx-travel-track);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ambx-travel-vertical {
|
|
229
|
+
width: var(--ambx-travel-track);
|
|
230
|
+
height: var(--ambx-travel-length);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* The thumb frame is a thumb-sized box riding the track — the one case
|
|
234
|
+
where "inset: 0" would be the wrong box for the part. A percentage
|
|
235
|
+
translate resolves against the ELEMENT's size, not the track's, which is
|
|
236
|
+
why the length has to be a property the transform can multiply. */
|
|
237
|
+
.ambx-travel > .ambx-frame-actuator {
|
|
238
|
+
inset: auto;
|
|
239
|
+
width: var(--ambx-travel-thumb-w);
|
|
240
|
+
height: var(--ambx-travel-thumb-h);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.ambx-travel-horizontal > .ambx-frame-actuator {
|
|
244
|
+
top: 50%;
|
|
245
|
+
left: 0;
|
|
246
|
+
transform: translate(-50%, -50%)
|
|
247
|
+
translateX(calc(var(--ambx-percent) * var(--ambx-travel-length)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.ambx-travel-vertical > .ambx-frame-actuator {
|
|
251
|
+
top: 100%;
|
|
252
|
+
left: 50%;
|
|
253
|
+
transform: translate(-50%, -50%)
|
|
254
|
+
translateY(calc(var(--ambx-percent) * var(--ambx-travel-length) * -1));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Latch: the control IS the track, so the pill can be positioned against
|
|
258
|
+
it directly. Its travel is the track less the pill and the two end
|
|
259
|
+
clearances — stated once here rather than as an on/off pair of offsets,
|
|
260
|
+
so --ambx-percent drives it like every other actuator. */
|
|
261
|
+
.ambx-latch {
|
|
262
|
+
position: relative;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
border: none;
|
|
265
|
+
padding: 0;
|
|
266
|
+
margin: 0;
|
|
267
|
+
background: none;
|
|
268
|
+
display: block;
|
|
269
|
+
width: var(--ambx-switch-w);
|
|
270
|
+
height: var(--ambx-switch-h);
|
|
271
|
+
--ambx-size: var(--ambx-switch-h);
|
|
272
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
273
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
274
|
+
--ambx-latch-thumb-w: calc(var(--ambx-switch-w) * 0.52);
|
|
275
|
+
--ambx-latch-thumb-h: calc(var(--ambx-switch-h) * 0.78);
|
|
276
|
+
--ambx-latch-inset: calc(var(--ambx-switch-h) * 0.11);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.ambx-latch-sm {
|
|
280
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 10);
|
|
281
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 5);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.ambx-latch-md {
|
|
285
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
286
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.ambx-latch-lg {
|
|
290
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 16);
|
|
291
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 8);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Stated as tokens rather than as literals in the transform, so a kit that
|
|
295
|
+
wants a round thumb on a wider track restates three lengths instead of
|
|
296
|
+
re-writing this selector. */
|
|
297
|
+
.ambx-latch > .ambx-frame-actuator {
|
|
298
|
+
inset: auto;
|
|
299
|
+
top: 50%;
|
|
300
|
+
left: 0;
|
|
301
|
+
width: var(--ambx-latch-thumb-w);
|
|
302
|
+
height: var(--ambx-latch-thumb-h);
|
|
303
|
+
transform: translateY(-50%)
|
|
304
|
+
translateX(
|
|
305
|
+
calc(
|
|
306
|
+
var(--ambx-latch-inset) + var(--ambx-percent) *
|
|
307
|
+
(
|
|
308
|
+
var(--ambx-switch-w) - var(--ambx-latch-thumb-w) - 2 *
|
|
309
|
+
var(--ambx-latch-inset)
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Press and bank size their control from their cap's legend, so their
|
|
316
|
+
frames stay markers and only the well needs geometry. */
|
|
317
|
+
.ambx-press {
|
|
318
|
+
--ambx-size: var(--ambx-button-size);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.ambx-bank {
|
|
322
|
+
--ambx-size: var(--ambx-select-size);
|
|
323
|
+
}
|
|
324
|
+
|
|
79
325
|
/* Button: a chamfered key cap seated in a clearance well — the referent
|
|
80
326
|
is components/button.py's "flush" style (cap seated into a hole with a
|
|
81
327
|
shadow gap ring around it). The button element IS the well: a shallow
|
|
@@ -104,6 +350,139 @@
|
|
|
104
350
|
text-transform: uppercase;
|
|
105
351
|
font-weight: 600;
|
|
106
352
|
color: var(--amb-label);
|
|
353
|
+
/* Containing block + stacking context for the dish overlay below, so it
|
|
354
|
+
can sit above the cap's own background and below the label. */
|
|
355
|
+
position: relative;
|
|
356
|
+
isolation: isolate;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Cap dish (subtle concavity).
|
|
360
|
+
|
|
361
|
+
Real key caps are not flat: the top is scooped, so the wall on the far
|
|
362
|
+
side of the dish tilts INTO the light and the near wall tilts away.
|
|
363
|
+
Photographed reference (a dished ON key lit from above): along the light
|
|
364
|
+
axis the face starts a little off its floor at the lit rim, bottoms out
|
|
365
|
+
~20% across, then climbs — accelerating, not linearly — to its brightest
|
|
366
|
+
at the far rim. Sampling that face and normalizing (0 = lit rim, 1 = far
|
|
367
|
+
rim) gives v = u^1.37 past the minimum; subtracting the profile's own
|
|
368
|
+
mean so the dish redistributes light rather than darkening the cap
|
|
369
|
+
overall leaves the signed stop table used below, in units of the end
|
|
370
|
+
delta: -0.42 at the lit rim, -0.73 at the floor, 0 at 62.5%, +1 at the
|
|
371
|
+
far rim. The far side overshoots more than the near side dips — a
|
|
372
|
+
grazing wall gains more than a tilted-away one loses.
|
|
373
|
+
|
|
374
|
+
AMPLITUDE is not taken from that photo (a near-black cap; absolute
|
|
375
|
+
deltas there mean nothing at .amb-surface's ~90% lightness). It reads
|
|
376
|
+
--amb-curve-delta, the grounded end delta .amb-surface-concave and
|
|
377
|
+
-convex ride (ambient3d/derived/notes/curved.md), so a concave cap and
|
|
378
|
+
a concave plate curve by the same measured amount and cannot drift
|
|
379
|
+
apart if that fit is ever re-derived. Only the AXIS and the PROFILE are
|
|
380
|
+
this file's own: the curved classes are single-axis (-concave reads
|
|
381
|
+
only --amb-light-y) and symmetric, and a cap has to follow whichever
|
|
382
|
+
light an app has set.
|
|
383
|
+
|
|
384
|
+
The overlay is painted as white/black alpha rather than as a background
|
|
385
|
+
gradient because the cap's background is already spoken for: the
|
|
386
|
+
material classes own background-image, and .amb-surface-concave's
|
|
387
|
+
`background` shorthand would wipe the shiny specular. An alpha over a
|
|
388
|
+
base of lightness L lands exactly on L + a(100 - L) for white and
|
|
389
|
+
L(1 - a) for black (the hue/saturation terms cancel in HSL lightness),
|
|
390
|
+
so the two alphas below invert the wanted delta EXACTLY for a matte cap
|
|
391
|
+
and approximately for shiny/glass, whose base is no longer plain
|
|
392
|
+
.amb-surface — the same residual .amb-mat-shiny's own comment carries. */
|
|
393
|
+
.amb-button-cap::after {
|
|
394
|
+
content: "";
|
|
395
|
+
position: absolute;
|
|
396
|
+
inset: 0;
|
|
397
|
+
z-index: -1; /* over the cap face, under the label:
|
|
398
|
+
the alphas are calibrated for the
|
|
399
|
+
surface's lightness, and washing them
|
|
400
|
+
over the much darker label would fade
|
|
401
|
+
the glyph out at the bright rim. */
|
|
402
|
+
border-radius: inherit;
|
|
403
|
+
pointer-events: none;
|
|
404
|
+
/* Same axis convention as .amb-mat-shiny's --_shiny-angle: 0% of the
|
|
405
|
+
gradient lands on the lit edge, so the dish reorients itself with all
|
|
406
|
+
eight .amb-light-* positions instead of assuming a light from above. */
|
|
407
|
+
--_ambx-dish-angle: calc(atan2(var(--amb-light-y), var(--amb-light-x)) + 270deg);
|
|
408
|
+
/* Unitless twins of the cap's own surface lightness and dish end delta
|
|
409
|
+
(percentage points), so they can divide into alphas.
|
|
410
|
+
|
|
411
|
+
The lightness is rebuilt here rather than read off --amb-lit because
|
|
412
|
+
it is needed as a NUMBER, and relative colour syntax only exposes `l`
|
|
413
|
+
inside a colour function. So: the grounded exposure law (albedo x
|
|
414
|
+
exposure) with the sRGB transfer applied by hand.
|
|
415
|
+
|
|
416
|
+
That tracks --amb-shade EXACTLY — a dished cap on a darkened panel
|
|
417
|
+
gets alphas scaled for the tone it actually sits at — but it has to
|
|
418
|
+
assume the reference reflectance for the CHROMATIC part, because an
|
|
419
|
+
--amb-albedo's luminance cannot be reached as a number. On a dark
|
|
420
|
+
coloured albedo the two halves then miss in opposite directions: the
|
|
421
|
+
black half divides by too large an l and under-shades, while the
|
|
422
|
+
white half divides by too small a (100 - l) and over-lifts, so the
|
|
423
|
+
dish reads as a bright sheen rather than a curve. Set luminance with
|
|
424
|
+
--amb-shade and hue with --amb-albedo and both halves stay exact;
|
|
425
|
+
that pairing is the documented way to colour a dished component.
|
|
426
|
+
|
|
427
|
+
--_ambx-cap-tone is an escape hatch for a future material that ships
|
|
428
|
+
its own fixed albedo again: set it to that albedo's ratio against the
|
|
429
|
+
reference so the alphas derive at the lightness they actually wash
|
|
430
|
+
over. Nothing applies it today — the micro-relief finishes carry no
|
|
431
|
+
--amb-albedo of their own (@ambientcss/css), so a relief-faced cap's
|
|
432
|
+
.ambx-cap-face is already at the reference tone by default and takes
|
|
433
|
+
--amb-shade the same way any other dished surface does; it multiplies
|
|
434
|
+
--amb-shade rather than replacing it, so a consumer's own shade still
|
|
435
|
+
composes. */
|
|
436
|
+
--_ambx-dish-l: calc(
|
|
437
|
+
100 * (
|
|
438
|
+
1.055 * pow(
|
|
439
|
+
0.82 * var(--_ambx-cap-tone, 1) * var(--amb-shade) * var(--amb-exposure),
|
|
440
|
+
0.41667
|
|
441
|
+
) - 0.055
|
|
442
|
+
)
|
|
443
|
+
);
|
|
444
|
+
/* One curvature for all three cap silhouettes: a dish is a property of
|
|
445
|
+
the tooling, not of how big or how round the key is. Tune it — for
|
|
446
|
+
one button, or a whole panel — with @ambientcss/css's
|
|
447
|
+
--amb-curve-scale, which is the same knob the curved surface classes
|
|
448
|
+
answer to and inherits like any other variable. */
|
|
449
|
+
--_ambx-dish-d: var(--amb-curve-delta);
|
|
450
|
+
/* Alpha per unit of the stop table. The max() floors keep the divisor
|
|
451
|
+
off zero: at Ik = If = 1 the grounded surface computes past 100%
|
|
452
|
+
lightness (already clipped white), where a white overlay is a no-op
|
|
453
|
+
anyway. */
|
|
454
|
+
--_ambx-dish-up: calc(var(--_ambx-dish-d) / max(6, 100 - var(--_ambx-dish-l)));
|
|
455
|
+
--_ambx-dish-dn: calc(var(--_ambx-dish-d) / max(6, var(--_ambx-dish-l)));
|
|
456
|
+
background: linear-gradient(
|
|
457
|
+
var(--_ambx-dish-angle),
|
|
458
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% / calc(var(--_ambx-dish-dn) * 0.42)) 0%,
|
|
459
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% / calc(var(--_ambx-dish-dn) * 0.73)) 20%,
|
|
460
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% / calc(var(--_ambx-dish-dn) * 0.47)) 40%,
|
|
461
|
+
/* Doubled stop at the neutral crossing: a zero-length interval, so the
|
|
462
|
+
gradient never interpolates between a black and a white stop while
|
|
463
|
+
either still carries alpha. */
|
|
464
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% / 0) 62.5%,
|
|
465
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% / 0) 62.5%,
|
|
466
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% / calc(var(--_ambx-dish-up) * 0.44)) 80%,
|
|
467
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% / var(--_ambx-dish-up)) 100%
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* The micro-relief cap face: the layer .amb-mat-brushed / .amb-mat-blasted
|
|
472
|
+
ride on when the cap itself cannot carry them, because its own ::after is
|
|
473
|
+
already the dish. Sits under the dish (same negative index, earlier in
|
|
474
|
+
paint order) and under the legend, so the grain textures the cap's face
|
|
475
|
+
without washing over either. It paints .amb-surface in its own material's
|
|
476
|
+
tone, covering the cap's, and wears the cap's chamfer itself — an inset
|
|
477
|
+
shadow belongs to the background layer of the element that declares it, so
|
|
478
|
+
an opaque face would otherwise hide the cut. The cap keeps its shadow and
|
|
479
|
+
its sink-on-press, neither of which the face touches. */
|
|
480
|
+
.ambx-cap-face {
|
|
481
|
+
position: absolute;
|
|
482
|
+
inset: 0;
|
|
483
|
+
z-index: -1;
|
|
484
|
+
border-radius: inherit;
|
|
485
|
+
pointer-events: none;
|
|
107
486
|
}
|
|
108
487
|
|
|
109
488
|
.amb-button:focus {
|
|
@@ -114,6 +493,13 @@
|
|
|
114
493
|
color: var(--amb-highlight-color);
|
|
115
494
|
}
|
|
116
495
|
|
|
496
|
+
/* Pressed: the cap keeps its tone and its dish, and only SINKS — thickness
|
|
497
|
+
1 -> 0.84, the referent's 0.7mm travel. The cue is entirely geometric:
|
|
498
|
+
the swept drop shadow shortens and tightens as the cap's silhouette
|
|
499
|
+
drops toward the well floor, and the chamfer bands narrow with it. A
|
|
500
|
+
key that is the same key still lights the same face, so nothing about
|
|
501
|
+
the cap's colour moves — the button reads as travel, not as a state
|
|
502
|
+
repaint. */
|
|
117
503
|
.amb-button:active .amb-button-cap {
|
|
118
504
|
--amb-thickness: 0.84;
|
|
119
505
|
}
|
|
@@ -157,148 +543,106 @@
|
|
|
157
543
|
}
|
|
158
544
|
|
|
159
545
|
/* Knob: a knob-scale body (thickness 2 = the referent knob.py's 9mm
|
|
160
|
-
height) resting on the panel
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
546
|
+
height) resting on the panel — a smooth chamfered cap, and around it
|
|
547
|
+
three independent axes, each its own prop. The cap may be ringed by a
|
|
548
|
+
knurl — a rim band of ribs standing proud of its edge and a step below
|
|
549
|
+
it, physically the referent's ribs catching key light on one flank and
|
|
550
|
+
shading on the other — the panel around it may carry printed scale
|
|
551
|
+
markers, and the face carries one accent indicator. The knurl and the
|
|
552
|
+
indicator rotate with the value, so the ribs move under the fingers like
|
|
553
|
+
the real thing; the markers do not, because they are printed on the
|
|
554
|
+
panel, and neither does the cap, whose chamfer is lit from a fixed
|
|
555
|
+
direction and so cannot ride a turning frame. */
|
|
556
|
+
/* Every knob metric is a fraction of the control's own size, so the three
|
|
557
|
+
size steps only restate that one length. Marker radius and dot size are
|
|
558
|
+
fractions of the DIAMETER: the ring sits at 1.33R and the dots measure
|
|
559
|
+
0.14R, both taken off the reference panels. --ambx-knob-size is a family
|
|
560
|
+
alias of --ambx-size, kept because these fractions read better against a
|
|
561
|
+
name that says which control they belong to. */
|
|
166
562
|
.amb-knob {
|
|
167
563
|
border-radius: 50%;
|
|
168
|
-
|
|
169
|
-
width: var(--ambx-knob-size);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
564
|
+
--ambx-knob-size: var(--ambx-size);
|
|
565
|
+
--ambx-knob-rect-width: calc(var(--ambx-knob-size) * 0.075);
|
|
566
|
+
--ambx-knob-marker-radius: calc(var(--ambx-knob-size) * 0.665);
|
|
567
|
+
--ambx-knob-marker-size: calc(var(--ambx-knob-size) * 0.07);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* The cap: the smooth chamfered disc that is most of the knob, and the
|
|
571
|
+
element that carries the ambient drop shadow (kept circular — the knurl's
|
|
572
|
+
teeth are too fine to read in a penumbra).
|
|
573
|
+
|
|
574
|
+
Both variants take the small base chamfer the referent cuts on every knob
|
|
575
|
+
(knob.py's `chamfer=0.35`, regardless of rib count) — width 2,
|
|
576
|
+
level-for-level with the body's knob-scale thickness (.amb-chamfer-2's
|
|
577
|
+
pairing). A rim of ribs is a grip, not an edge treatment: it tells you
|
|
578
|
+
the knob turns, not where its top face stops, so the knurled knob needs
|
|
579
|
+
the chamfer just as much as the smooth one does.
|
|
580
|
+
|
|
581
|
+
knurling={false} leaves the cap flush at the full width and it becomes the
|
|
582
|
+
whole visible knob — a turned cylinder. Otherwise KnobBody insets it by the
|
|
583
|
+
knurl band (inline, from the same constant the clip path uses) so the ribs
|
|
584
|
+
ring it. */
|
|
176
585
|
.amb-knob-body {
|
|
177
586
|
position: absolute;
|
|
178
|
-
inset:
|
|
587
|
+
inset: 0;
|
|
179
588
|
border-radius: 50%;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
589
|
+
--amb-chamfer: 1;
|
|
590
|
+
--amb-chamfer-width: 2;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* The rotating knurl: clipped to the toothed ANNULUS (the inline SVG
|
|
594
|
+
clipPath in KnurledFace), so it paints only the rim band outside the cap
|
|
595
|
+
and the cap's own chamfer bands survive underneath.
|
|
596
|
+
|
|
597
|
+
Two things paint it. Per-tooth flank shading — a lit flank climbing to
|
|
598
|
+
each ridge and a shaded one falling away, one pair every 7.5deg (360/48,
|
|
599
|
+
the clip's rib count), phase-aligned with the clip: conic 0 is rotated to
|
|
600
|
+
the +x axis, where the tooth curve starts in a groove, so the ridge lands
|
|
601
|
+
at 3.75deg and the lit and shaded stops at 1.875 and 5.625. And a
|
|
602
|
+
contact-occlusion band along the inner edge, where the cap overhangs the
|
|
603
|
+
band it steps down to: radial, so it survives the frame's rotation
|
|
604
|
+
unchanged, which a directional cue in a turning element could not.
|
|
605
|
+
|
|
606
|
+
The step down itself is --amb-shade, not a black wash: the rim is the same
|
|
607
|
+
material as the cap sitting lower and facing less of the sky, and shading
|
|
608
|
+
the albedo darkens it the way less light would, where an overlay would
|
|
609
|
+
drain a coloured knob toward grey. Uniform round the circumference, so it
|
|
610
|
+
too is rotation-safe. */
|
|
188
611
|
.amb-knob-face {
|
|
189
612
|
position: absolute;
|
|
190
613
|
inset: 0;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
var(--amb-light-saturation)
|
|
194
|
-
calc(
|
|
195
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
196
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
197
|
-
)
|
|
198
|
-
);
|
|
614
|
+
--amb-shade: 0.88;
|
|
615
|
+
background-color: var(--amb-lit);
|
|
199
616
|
background-image:
|
|
200
617
|
radial-gradient(
|
|
201
|
-
circle,
|
|
202
|
-
hsl(
|
|
203
|
-
var(--amb-light-hue)
|
|
204
|
-
var(--amb-light-saturation)
|
|
205
|
-
calc(
|
|
206
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
207
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
208
|
-
)
|
|
209
|
-
) 0 60%,
|
|
210
|
-
transparent 61%
|
|
211
|
-
),
|
|
212
|
-
repeating-conic-gradient(
|
|
213
|
-
from 90deg,
|
|
214
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
215
|
-
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.4deg,
|
|
216
|
-
transparent 2.2deg 4.6deg,
|
|
618
|
+
circle closest-side,
|
|
217
619
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
218
620
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
219
|
-
* 0.
|
|
220
|
-
transparent 7.2deg 10deg
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/* Fluted face (flute variant — the OP-Z-style referent: 14 broad
|
|
225
|
-
ridges split by narrow grooves, pitch 25.714deg). Flank bands sit on
|
|
226
|
-
the clip's rising (0–2.1deg) and falling (18.5–20.6deg) flanks;
|
|
227
|
-
deeper flutes catch more key light, so both bands run stronger than
|
|
228
|
-
the 36-rib knurl's. */
|
|
229
|
-
.amb-knob-face-flute {
|
|
230
|
-
background-image:
|
|
231
|
-
radial-gradient(
|
|
232
|
-
circle,
|
|
233
|
-
hsl(
|
|
234
|
-
var(--amb-light-hue)
|
|
235
|
-
var(--amb-light-saturation)
|
|
236
|
-
calc(
|
|
237
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
238
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
239
|
-
)
|
|
240
|
-
) 0 60%,
|
|
241
|
-
transparent 61%
|
|
242
|
-
),
|
|
243
|
-
repeating-conic-gradient(
|
|
244
|
-
from 90deg,
|
|
245
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
246
|
-
calc(var(--amb-key-light-intensity) * 0.45)) 0deg 2.1deg,
|
|
247
|
-
transparent 3.2deg 17.4deg,
|
|
621
|
+
* 0.07 + 0.09)) 89%,
|
|
248
622
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
249
623
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
250
|
-
* 0.
|
|
251
|
-
transparent
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/* Fine knurl (cap and wheel variants: 48 ribs, pitch 7.5deg) — the
|
|
256
|
-
36-rib pattern's angles scaled by 3/4, same flank alphas. */
|
|
257
|
-
.amb-knob-face-fine {
|
|
258
|
-
background-image:
|
|
259
|
-
radial-gradient(
|
|
260
|
-
circle,
|
|
261
|
-
hsl(
|
|
262
|
-
var(--amb-light-hue)
|
|
263
|
-
var(--amb-light-saturation)
|
|
264
|
-
calc(
|
|
265
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
266
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
267
|
-
)
|
|
268
|
-
) 0 60%,
|
|
269
|
-
transparent 61%
|
|
624
|
+
* 0.02 + 0.03)) 95%,
|
|
625
|
+
transparent 100%
|
|
270
626
|
),
|
|
271
627
|
repeating-conic-gradient(
|
|
272
628
|
from 90deg,
|
|
629
|
+
transparent 0deg,
|
|
273
630
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
274
|
-
calc(var(--amb-key-light-intensity) * 0.
|
|
275
|
-
transparent
|
|
631
|
+
calc(var(--amb-key-light-intensity) * 0.26)) 1.875deg,
|
|
632
|
+
transparent 3.75deg,
|
|
276
633
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
277
634
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
278
|
-
* 0.
|
|
279
|
-
transparent
|
|
635
|
+
* 0.24 + 0.13)) 5.625deg,
|
|
636
|
+
transparent 7.5deg
|
|
280
637
|
);
|
|
281
638
|
}
|
|
282
639
|
|
|
283
|
-
/*
|
|
284
|
-
|
|
285
|
-
replaces the surface-colored center disc over the fine knurl. */
|
|
286
|
-
.amb-knob-face-cap {
|
|
287
|
-
background-image:
|
|
288
|
-
radial-gradient(circle, var(--amb-highlight-color) 0 65%, transparent 66%),
|
|
289
|
-
repeating-conic-gradient(
|
|
290
|
-
from 90deg,
|
|
291
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
292
|
-
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.05deg,
|
|
293
|
-
transparent 1.65deg 3.45deg,
|
|
294
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
295
|
-
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
296
|
-
* 0.3 + 0.16)) 3.75deg 4.8deg,
|
|
297
|
-
transparent 5.4deg 7.5deg
|
|
298
|
-
);
|
|
299
|
-
}
|
|
640
|
+
/* Indicators. Both ride in the actuator frame, so they point at 12 o'clock
|
|
641
|
+
here and the frame's rotation carries them round to the value.
|
|
300
642
|
|
|
301
|
-
|
|
643
|
+
The circle is the grounded referent's offset dot (knob() dot_frac 0.12,
|
|
644
|
+
dot_offset 0.68). */
|
|
645
|
+
.amb-knob-indicator-circle {
|
|
302
646
|
position: absolute;
|
|
303
647
|
top: 16%;
|
|
304
648
|
left: 50%;
|
|
@@ -309,54 +653,81 @@
|
|
|
309
653
|
background: var(--amb-highlight-color);
|
|
310
654
|
}
|
|
311
655
|
|
|
312
|
-
/*
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/* Radial indicator line (line variant): the referent's accent bar,
|
|
322
|
-
center to ~85% of the cap radius, pointing at 12 o'clock. */
|
|
323
|
-
.amb-knob-indicator-line {
|
|
656
|
+
/* The rectangle is a short radial bar out near the rim, not a spoke from
|
|
657
|
+
the centre: it runs 0.50R to 0.84R, the proportions measured off the
|
|
658
|
+
reference panels, which is why it is stated as a top offset plus a
|
|
659
|
+
height rather than the full-radius line it replaces. Corners take a
|
|
660
|
+
quarter-grid round-over — enough to read as machined rather than
|
|
661
|
+
aliased, and far short of the pill a 50% radius would give. */
|
|
662
|
+
.amb-knob-indicator-rectangle {
|
|
324
663
|
position: absolute;
|
|
325
|
-
top:
|
|
664
|
+
top: 8%;
|
|
326
665
|
left: 50%;
|
|
327
666
|
transform: translateX(-50%);
|
|
328
|
-
width: var(--ambx-knob-
|
|
329
|
-
height:
|
|
330
|
-
border-radius:
|
|
667
|
+
width: var(--ambx-knob-rect-width);
|
|
668
|
+
height: 17%;
|
|
669
|
+
border-radius: var(--ambx-grid-quarter);
|
|
331
670
|
background: var(--amb-highlight-color);
|
|
332
671
|
}
|
|
333
672
|
|
|
334
|
-
.amb-knob:hover .amb-knob-indicator-
|
|
335
|
-
.amb-knob:hover .amb-knob-indicator-
|
|
673
|
+
.amb-knob:hover .amb-knob-indicator-circle,
|
|
674
|
+
.amb-knob:hover .amb-knob-indicator-rectangle {
|
|
336
675
|
background: color-mix(in oklab, var(--amb-highlight-color), white 25%);
|
|
337
676
|
}
|
|
338
677
|
|
|
678
|
+
/* Printed scale markers. The ring is a zero-size point at the knob's
|
|
679
|
+
centre and each dot is placed by rotating about that centre and pushing
|
|
680
|
+
out along the rotated axis, so one angle per dot is the whole geometry —
|
|
681
|
+
and the angles come from the same sweep the value uses (ScaleRing reads
|
|
682
|
+
--ambx-travel-start/-sweep off the control), so a dot always lands where
|
|
683
|
+
its value would, whatever `travel` is set to.
|
|
684
|
+
|
|
685
|
+
Ink is --amb-label, not --amb-highlight-color: these are printed panel
|
|
686
|
+
graphics like the control's legend, not part of the control. */
|
|
687
|
+
.amb-knob-marker-ring {
|
|
688
|
+
position: absolute;
|
|
689
|
+
top: 50%;
|
|
690
|
+
left: 50%;
|
|
691
|
+
width: 0;
|
|
692
|
+
height: 0;
|
|
693
|
+
pointer-events: none;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.amb-knob-marker {
|
|
697
|
+
position: absolute;
|
|
698
|
+
top: 0;
|
|
699
|
+
left: 0;
|
|
700
|
+
width: var(--ambx-knob-marker-size);
|
|
701
|
+
height: var(--ambx-knob-marker-size);
|
|
702
|
+
margin: calc(var(--ambx-knob-marker-size) / -2);
|
|
703
|
+
border-radius: 50%;
|
|
704
|
+
background: var(--amb-label);
|
|
705
|
+
transform: rotate(var(--amb-marker-angle))
|
|
706
|
+
translateY(calc(-1 * var(--ambx-knob-marker-radius)));
|
|
707
|
+
}
|
|
708
|
+
|
|
339
709
|
/* Tracks are grounded grooves (.amb-groove supplies the recess: lit-wall
|
|
340
710
|
shadow band + far-wall bounce) with a lume interior — dark in bright
|
|
341
711
|
light, glowing in low light — overriding the groove's neutral floor. */
|
|
342
|
-
.amb-
|
|
343
|
-
position:
|
|
344
|
-
|
|
345
|
-
height: calc(var(--ambx-grid) * 30);
|
|
712
|
+
.amb-travel-track {
|
|
713
|
+
position: absolute;
|
|
714
|
+
inset: 0;
|
|
346
715
|
border-radius: var(--ambx-radius-full);
|
|
347
716
|
background-color: var(--amb-lume);
|
|
348
717
|
}
|
|
349
718
|
|
|
719
|
+
/* The slider's referent (slider.py) is a shallow concave channel 1mm deep
|
|
720
|
+
rather than the fader's through-slot. */
|
|
721
|
+
.amb-travel-track-channel {
|
|
722
|
+
--amb-thickness: 0.22;
|
|
723
|
+
}
|
|
724
|
+
|
|
350
725
|
/* Fader thumb: the referent (fader.py) is a pill cap on a stem — 7mm
|
|
351
726
|
tall (thickness 1.5) riding 2.2mm above the plate (elevation 0.28) —
|
|
352
727
|
with a single grip line across the top. */
|
|
353
728
|
.amb-fader-thumb {
|
|
354
729
|
position: absolute;
|
|
355
|
-
|
|
356
|
-
height: var(--ambx-fader-thumb-h);
|
|
357
|
-
left: 50%;
|
|
358
|
-
top: 50%;
|
|
359
|
-
transform: translate(-50%, -50%);
|
|
730
|
+
inset: 0;
|
|
360
731
|
border-radius: var(--ambx-radius-md);
|
|
361
732
|
display: grid;
|
|
362
733
|
place-items: center;
|
|
@@ -371,29 +742,20 @@
|
|
|
371
742
|
background: var(--amb-lume);
|
|
372
743
|
}
|
|
373
744
|
|
|
374
|
-
/*
|
|
375
|
-
deep = thickness 0.22), not a through-slot, with a domed disc thumb
|
|
376
|
-
gliding over it. */
|
|
377
|
-
.amb-slider {
|
|
378
|
-
position: relative;
|
|
379
|
-
height: var(--ambx-slider-track);
|
|
380
|
-
width: calc(var(--ambx-grid) * 30);
|
|
381
|
-
border-radius: var(--ambx-radius-full);
|
|
382
|
-
background-color: var(--amb-lume);
|
|
383
|
-
--amb-thickness: 0.22;
|
|
384
|
-
}
|
|
385
|
-
|
|
745
|
+
/* A domed disc gliding over the channel. */
|
|
386
746
|
.amb-slider-thumb {
|
|
387
747
|
position: absolute;
|
|
388
|
-
|
|
389
|
-
height: var(--ambx-slider-thumb);
|
|
390
|
-
top: 50%;
|
|
391
|
-
left: 50%;
|
|
392
|
-
transform: translate(-50%, -50%);
|
|
748
|
+
inset: 0;
|
|
393
749
|
border-radius: 50%;
|
|
394
750
|
--amb-thickness: 0.7;
|
|
395
751
|
}
|
|
396
752
|
|
|
753
|
+
/* The fader's pill is 1.5x its width; the slider's disc is round. Both take
|
|
754
|
+
their width from the shared travel size step. */
|
|
755
|
+
.amb-fader {
|
|
756
|
+
--ambx-travel-thumb-h: calc(var(--ambx-size) * 1.5);
|
|
757
|
+
}
|
|
758
|
+
|
|
397
759
|
.amb-led {
|
|
398
760
|
width: calc(var(--ambx-grid) * 2);
|
|
399
761
|
height: calc(var(--ambx-grid) * 2);
|
|
@@ -416,22 +778,9 @@
|
|
|
416
778
|
in a dark stadium recess (well 1.5mm deep = thickness 0.33; the pill
|
|
417
779
|
stands 2.6mm above the recess floor = thickness 0.58), with an
|
|
418
780
|
optional LED above. The pill slides, nothing presses. */
|
|
419
|
-
.amb-switch {
|
|
420
|
-
cursor: pointer;
|
|
421
|
-
border: none;
|
|
422
|
-
padding: 0;
|
|
423
|
-
background: none;
|
|
424
|
-
display: inline-grid;
|
|
425
|
-
justify-items: center;
|
|
426
|
-
gap: var(--ambx-grid);
|
|
427
|
-
margin: 0;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
781
|
.amb-switch-track {
|
|
431
|
-
position:
|
|
432
|
-
|
|
433
|
-
width: var(--ambx-switch-w);
|
|
434
|
-
height: var(--ambx-switch-h);
|
|
782
|
+
position: absolute;
|
|
783
|
+
inset: 0;
|
|
435
784
|
border-radius: calc(var(--ambx-switch-h) / 2);
|
|
436
785
|
background-color: var(--amb-lume);
|
|
437
786
|
--amb-thickness: 0.33;
|
|
@@ -439,38 +788,235 @@
|
|
|
439
788
|
|
|
440
789
|
.amb-switch-pill {
|
|
441
790
|
position: absolute;
|
|
442
|
-
|
|
443
|
-
left: 0;
|
|
444
|
-
width: calc(var(--ambx-switch-w) * 0.52);
|
|
445
|
-
height: calc(var(--ambx-switch-h) * 0.78);
|
|
791
|
+
inset: 0;
|
|
446
792
|
border-radius: calc(var(--ambx-switch-h) * 0.39);
|
|
447
|
-
transform: translate(calc(var(--ambx-switch-h) * 0.11), -50%);
|
|
448
|
-
transition: transform 140ms ease;
|
|
449
793
|
--amb-thickness: 0.58;
|
|
450
794
|
}
|
|
451
795
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
796
|
+
/* The lamp sits above the track on the tight grid pitch, one tier in from
|
|
797
|
+
the label's own clearance. */
|
|
798
|
+
.ambx-switch-mount {
|
|
799
|
+
display: inline-grid;
|
|
800
|
+
justify-items: center;
|
|
801
|
+
gap: var(--ambx-grid);
|
|
457
802
|
}
|
|
458
803
|
|
|
459
804
|
.amb-switch:hover .amb-switch-pill {
|
|
460
805
|
background-color: color-mix(
|
|
461
806
|
in oklab,
|
|
462
|
-
|
|
463
|
-
var(--amb-light-hue)
|
|
464
|
-
var(--amb-light-saturation)
|
|
465
|
-
calc(
|
|
466
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
467
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
468
|
-
)
|
|
469
|
-
),
|
|
807
|
+
var(--amb-lit),
|
|
470
808
|
var(--amb-highlight-color) 12%
|
|
471
809
|
);
|
|
472
810
|
}
|
|
473
811
|
|
|
812
|
+
/* Select: a bank of lamp-lit keys in a shared rail — the hardware idiom
|
|
813
|
+
for a mode/bank selector, where the state is a lamp rather than a mark.
|
|
814
|
+
Three physical layers per option, and the ORDER is the whole trick:
|
|
815
|
+
|
|
816
|
+
1. the option itself is the pocket floor, in the grounded surface tone
|
|
817
|
+
2. .amb-select-lens is the LED's lens, a big disc lying ON that floor
|
|
818
|
+
3. .amb-select-cap is a translucent diffuser sitting OVER both
|
|
819
|
+
|
|
820
|
+
The cap is .amb-mat-glass, so its backdrop-filter blurs whatever is
|
|
821
|
+
behind it — which is the lens. That single fact produces both states
|
|
822
|
+
without a second gradient anywhere: unlit, the lens body reads as a soft
|
|
823
|
+
dark circle through the frost ("the LED you can see is there"); lit, the
|
|
824
|
+
same disc blooms into a broad radial glow with a hot centre, exactly as
|
|
825
|
+
an LED does behind a diffuser. Confirmed in the browser: backdrop-filter
|
|
826
|
+
samples a positioned SIBLING, not just an ancestor's background, so the
|
|
827
|
+
lens must paint before the cap — hence the lens is absolute and the cap
|
|
828
|
+
is position: relative rather than the other way round. */
|
|
829
|
+
.amb-select {
|
|
830
|
+
display: inline-flex;
|
|
831
|
+
flex-direction: column;
|
|
832
|
+
padding: var(--ambx-grid-half); /* the clearance gap ring */
|
|
833
|
+
gap: var(--ambx-grid-half); /* the dark rail between keys */
|
|
834
|
+
--amb-thickness: 0.27; /* seat depth, as .amb-button */
|
|
835
|
+
background-color: var(--amb-lume);
|
|
836
|
+
border-radius: calc(var(--ambx-radius-md) + var(--ambx-grid-half));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.ambx-bank-horizontal.amb-select {
|
|
840
|
+
flex-direction: row;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.amb-select .ambx-key {
|
|
844
|
+
/* The lamp colour. Same variable .amb-led and AmbientSwitch's led prop
|
|
845
|
+
use, so one declaration lights every indicator on a panel; unset, it
|
|
846
|
+
falls back to the scene's own accent rather than to a hardcoded hue.
|
|
847
|
+
|
|
848
|
+
Resolved HERE, on the key, not on the bank: a custom property has its
|
|
849
|
+
var()s substituted on the element that declares it, so reading
|
|
850
|
+
--amb-led-color one level up would bake in the bank's colour (or the
|
|
851
|
+
fallback) and inherit that down, and a per-key colour would never be
|
|
852
|
+
seen. Same trap --amb-curve-delta is declared per element to avoid. */
|
|
853
|
+
--_ambx-sel-color: var(--amb-led-color, var(--amb-highlight-color));
|
|
854
|
+
position: relative;
|
|
855
|
+
cursor: pointer;
|
|
856
|
+
border: none;
|
|
857
|
+
padding: 0;
|
|
858
|
+
display: grid;
|
|
859
|
+
/* Square by default, but a WORD is allowed to widen it: min-width floors
|
|
860
|
+
the key at the hardware square, and the cap (a stretched grid item, so
|
|
861
|
+
it still covers the whole face) contributes its legend's width above
|
|
862
|
+
that. A bank of numerals stays a bank of numerals; a bank of mode names
|
|
863
|
+
grows to fit and, in a column, all keys stretch to the widest. */
|
|
864
|
+
min-width: var(--ambx-select-size);
|
|
865
|
+
height: var(--ambx-select-size);
|
|
866
|
+
border-radius: var(--ambx-radius-md);
|
|
867
|
+
/* The pocket floor the lens lies on: the grounded .amb-surface tone, so
|
|
868
|
+
an unlit key reads as a lit white diffuser rather than as a hole. */
|
|
869
|
+
background-color: var(--amb-lit);
|
|
870
|
+
transition: box-shadow 140ms ease;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* Disabled dims the LAMP and the legend, not the key. Fading the whole
|
|
874
|
+
option would sink it into the dark rail, and opacity on the option makes
|
|
875
|
+
it a backdrop root — the one ancestor property that can cut the cap off
|
|
876
|
+
from the lens it is supposed to be diffusing. Fading the lens instead
|
|
877
|
+
leaves the diffuser doing its job on a lamp that simply cannot light. */
|
|
878
|
+
.amb-select .ambx-key:disabled {
|
|
879
|
+
cursor: not-allowed;
|
|
880
|
+
box-shadow: none; /* a dead lamp spills nothing onto the rail */
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.amb-select .ambx-key:disabled .amb-select-lens {
|
|
884
|
+
opacity: 0.4;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.amb-select .ambx-key:disabled .amb-select-cap {
|
|
888
|
+
color: color-mix(in oklab, var(--amb-label), transparent 55%);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.amb-select .ambx-key:focus-visible {
|
|
892
|
+
outline-offset: var(--ambx-grid-half);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* The LED's lens: a big disc, not the pinprick .amb-led is — this is the
|
|
896
|
+
lamp UNDER the key, sized so the diffused circle is legible through the
|
|
897
|
+
cap (the referent's lens reads at roughly three fifths of the key). */
|
|
898
|
+
.amb-select-lens {
|
|
899
|
+
position: absolute;
|
|
900
|
+
left: 50%;
|
|
901
|
+
top: 50%;
|
|
902
|
+
transform: translate(-50%, -50%);
|
|
903
|
+
height: 62%;
|
|
904
|
+
aspect-ratio: 1; /* off the HEIGHT, so a widened key keeps a
|
|
905
|
+
round lamp rather than an overflowing one */
|
|
906
|
+
border-radius: 50%;
|
|
907
|
+
/* Unlit, the lens is a tinted lens body, not an off pixel: it sits
|
|
908
|
+
between the floor's lit tone and the same material at 0.38 of its
|
|
909
|
+
reflectance (the mid-tone plate the rig calls DARKER_ALBEDO, taken
|
|
910
|
+
off --amb-lit in LINEAR light so it darkens the panel's own colour
|
|
911
|
+
rather than replacing it with a grey), and carries a trace of its
|
|
912
|
+
own colour so an amber lamp reads
|
|
913
|
+
faintly warm while dark — 6%, enough to tell an amber lamp from a
|
|
914
|
+
cyan one up close and not enough to tint the key. Measured against
|
|
915
|
+
the referent photo, whose unlit disc dips about 5% below the
|
|
916
|
+
surrounding cap. */
|
|
917
|
+
background-color: color-mix(
|
|
918
|
+
in oklab,
|
|
919
|
+
color-mix(
|
|
920
|
+
in oklab,
|
|
921
|
+
var(--amb-lit) 45%,
|
|
922
|
+
color-mix(in srgb-linear, var(--amb-lit) 37.8%, black)
|
|
923
|
+
),
|
|
924
|
+
var(--_ambx-sel-color) 6%
|
|
925
|
+
);
|
|
926
|
+
transition: background-color 140ms ease;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* The diffuser cap. --_glass-blur is dialled down from the material's own
|
|
930
|
+
8 + 8*Ik (15.2px at the defaults): at that radius the lens washes out to
|
|
931
|
+
a flat tint and both states lose their circle. It is also tied to the
|
|
932
|
+
key rather than fixed — diffusion is a ratio of the lens it spreads, so
|
|
933
|
+
a flat radius would leave a small key soft and a large one hard-edged.
|
|
934
|
+
0.18 of the key puts md back at the 8px that read correctly against the
|
|
935
|
+
referent. */
|
|
936
|
+
.amb-select-cap {
|
|
937
|
+
position: relative; /* paints above the absolutely-placed lens */
|
|
938
|
+
display: grid;
|
|
939
|
+
place-items: center;
|
|
940
|
+
padding-inline: var(--ambx-select-pad);
|
|
941
|
+
height: 100%;
|
|
942
|
+
/* Flush with the pocket: the cap IS the key's whole visible face, so
|
|
943
|
+
the floor beneath it never shows as a rim — the dark gap between keys
|
|
944
|
+
is the rail, not a ring around each one. */
|
|
945
|
+
border-radius: var(--ambx-radius-md);
|
|
946
|
+
font-weight: 600;
|
|
947
|
+
font-size: var(--ambx-select-label-size);
|
|
948
|
+
color: var(--amb-label);
|
|
949
|
+
--_glass-blur: calc(var(--ambx-select-size) * 0.18);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.amb-select .ambx-key:hover .amb-select-cap {
|
|
953
|
+
color: var(--amb-highlight-color);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* Pressing sinks the key, and only sinks it — same rule the button follows:
|
|
957
|
+
the drop shadow shortens and the chamfer bands narrow, nothing repaints. */
|
|
958
|
+
.amb-select .ambx-key:active .amb-select-cap {
|
|
959
|
+
--amb-thickness: 0.84;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/* Lit. The lens goes to full lamp colour and the diffuser saturates with
|
|
963
|
+
it: the cap's own tint is mixed to 35% of the lamp over the glass wash,
|
|
964
|
+
which is what the referent measures — a flooded cap that still shows the
|
|
965
|
+
lens's hot centre through it. Past ~55% the radial disappears into a
|
|
966
|
+
flat swatch and the key stops reading as lit-from-behind. */
|
|
967
|
+
.amb-select .ambx-key[data-on] .amb-select-lens {
|
|
968
|
+
background-color: var(--_ambx-sel-color);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.amb-select .ambx-key[data-on] .amb-select-cap {
|
|
972
|
+
background-color: color-mix(
|
|
973
|
+
in srgb,
|
|
974
|
+
var(--_ambx-sel-color) 35%,
|
|
975
|
+
hsl(
|
|
976
|
+
var(--amb-light-hue) var(--amb-light-saturation)
|
|
977
|
+
calc(var(--amb-key-light-intensity) * 100%) /
|
|
978
|
+
calc(0.05 + var(--amb-key-light-intensity) * 0.1)
|
|
979
|
+
)
|
|
980
|
+
);
|
|
981
|
+
/* The legend is printed ink: it does not light up, it silhouettes
|
|
982
|
+
against the lamp — so it darkens toward the lamp's own hue rather
|
|
983
|
+
than staying --amb-label, which inverts to pale in a dark scene and
|
|
984
|
+
would vanish against a bright cap. */
|
|
985
|
+
color: color-mix(in oklab, var(--_ambx-sel-color), black 68%);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/* Light escaping around the key onto the rail. Radius is the grounded
|
|
989
|
+
emissive bloom (ambient3d/derived/notes/glow.md: 6.2px = 2 sigma of the
|
|
990
|
+
measured falloff), the same figure .amb-glow uses — only the colour is
|
|
991
|
+
the lamp's rather than --amb-lume's. */
|
|
992
|
+
.amb-select .ambx-key[data-on] {
|
|
993
|
+
box-shadow: 0 0 6.2px var(--_ambx-sel-color);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.ambx-bank {
|
|
997
|
+
--ambx-select-size: calc(var(--ambx-grid) * 11); /* 44px key */
|
|
998
|
+
--ambx-select-pad: calc(var(--ambx-grid) * 2);
|
|
999
|
+
--ambx-select-label-size: calc(var(--ambx-grid) * 3.5);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.ambx-bank-sm {
|
|
1003
|
+
--ambx-select-size: calc(var(--ambx-grid) * 8); /* 32px */
|
|
1004
|
+
--ambx-select-pad: calc(var(--ambx-grid) * 1.5);
|
|
1005
|
+
--ambx-select-label-size: calc(var(--ambx-grid) * 2.5);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.ambx-bank-md {
|
|
1009
|
+
--ambx-select-size: calc(var(--ambx-grid) * 11); /* 44px */
|
|
1010
|
+
--ambx-select-pad: calc(var(--ambx-grid) * 2);
|
|
1011
|
+
--ambx-select-label-size: calc(var(--ambx-grid) * 3.5);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.ambx-bank-lg {
|
|
1015
|
+
--ambx-select-size: calc(var(--ambx-grid) * 14); /* 56px */
|
|
1016
|
+
--ambx-select-pad: calc(var(--ambx-grid) * 2.5);
|
|
1017
|
+
--ambx-select-label-size: calc(var(--ambx-grid) * 4.5);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
474
1020
|
.ambx-panel {
|
|
475
1021
|
border-radius: var(--ambx-radius-xl);
|
|
476
1022
|
padding: calc(var(--ambx-grid) * 4);
|
|
@@ -508,7 +1054,7 @@
|
|
|
508
1054
|
flex-direction: column;
|
|
509
1055
|
}
|
|
510
1056
|
|
|
511
|
-
.ambx-
|
|
1057
|
+
.ambx-press {
|
|
512
1058
|
line-height: var(--ambx-type-3-line);
|
|
513
1059
|
--ambx-button-size: calc(var(--ambx-grid) * 18); /* 72px pill */
|
|
514
1060
|
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2); /* 8px */
|
|
@@ -516,21 +1062,21 @@
|
|
|
516
1062
|
--ambx-button-cap-inset: var(--ambx-grid); /* 4px */
|
|
517
1063
|
}
|
|
518
1064
|
|
|
519
|
-
.ambx-
|
|
1065
|
+
.ambx-press-sm {
|
|
520
1066
|
--ambx-button-size: calc(var(--ambx-grid) * 16); /* 64px pill */
|
|
521
1067
|
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
522
1068
|
--ambx-button-cap-pad-y: var(--ambx-grid); /* 4px */
|
|
523
1069
|
--ambx-button-cap-inset: calc(var(--ambx-grid) * 0.75); /* 3px */
|
|
524
1070
|
}
|
|
525
1071
|
|
|
526
|
-
.ambx-
|
|
1072
|
+
.ambx-press-md {
|
|
527
1073
|
--ambx-button-size: calc(var(--ambx-grid) * 18); /* 72px pill */
|
|
528
1074
|
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2); /* 8px */
|
|
529
1075
|
--ambx-button-cap-pad-y: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
530
1076
|
--ambx-button-cap-inset: var(--ambx-grid); /* 4px */
|
|
531
1077
|
}
|
|
532
1078
|
|
|
533
|
-
.ambx-
|
|
1079
|
+
.ambx-press-lg {
|
|
534
1080
|
--ambx-button-size: calc(var(--ambx-grid) * 20); /* 80px pill */
|
|
535
1081
|
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2.5); /* 10px */
|
|
536
1082
|
--ambx-button-cap-pad-y: calc(var(--ambx-grid) * 2); /* 8px */
|
|
@@ -540,45 +1086,25 @@
|
|
|
540
1086
|
/* Round/square shapes have their own min-width table per size (not a
|
|
541
1087
|
ratio of the pill's — the three shapes' true md values, 72/48/56,
|
|
542
1088
|
share no common factor), overridden via compound selectors so they
|
|
543
|
-
win regardless of source order relative to .ambx-
|
|
544
|
-
.amb-button-round.ambx-
|
|
545
|
-
.amb-button-round.ambx-
|
|
546
|
-
.amb-button-round.ambx-
|
|
1089
|
+
win regardless of source order relative to .ambx-press-sm/-md/-lg. */
|
|
1090
|
+
.amb-button-round.ambx-press-sm { --ambx-button-size: calc(var(--ambx-grid) * 10); } /* 40px */
|
|
1091
|
+
.amb-button-round.ambx-press-md { --ambx-button-size: calc(var(--ambx-grid) * 12); } /* 48px */
|
|
1092
|
+
.amb-button-round.ambx-press-lg { --ambx-button-size: calc(var(--ambx-grid) * 14); } /* 56px */
|
|
547
1093
|
|
|
548
|
-
.amb-button-square.ambx-
|
|
549
|
-
.amb-button-square.ambx-
|
|
550
|
-
.amb-button-square.ambx-
|
|
1094
|
+
.amb-button-square.ambx-press-sm { --ambx-button-size: calc(var(--ambx-grid) * 12); } /* 48px */
|
|
1095
|
+
.amb-button-square.ambx-press-md { --ambx-button-size: calc(var(--ambx-grid) * 14); } /* 56px */
|
|
1096
|
+
.amb-button-square.ambx-press-lg { --ambx-button-size: calc(var(--ambx-grid) * 16); } /* 64px */
|
|
551
1097
|
|
|
552
|
-
.ambx-
|
|
1098
|
+
.ambx-press-sm .amb-button-cap {
|
|
553
1099
|
font-size: var(--ambx-label-size);
|
|
554
1100
|
line-height: var(--ambx-label-line);
|
|
555
1101
|
}
|
|
556
1102
|
|
|
557
|
-
.ambx-
|
|
1103
|
+
.ambx-press-lg .amb-button-cap {
|
|
558
1104
|
font-size: calc(var(--ambx-grid) * 3.5);
|
|
559
1105
|
line-height: calc(var(--ambx-grid) * 5);
|
|
560
1106
|
}
|
|
561
1107
|
|
|
562
|
-
.ambx-switch {
|
|
563
|
-
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
564
|
-
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
.ambx-switch-sm {
|
|
568
|
-
--ambx-switch-w: calc(var(--ambx-grid) * 10);
|
|
569
|
-
--ambx-switch-h: calc(var(--ambx-grid) * 5);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.ambx-switch-md {
|
|
573
|
-
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
574
|
-
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
.ambx-switch-lg {
|
|
578
|
-
--ambx-switch-w: calc(var(--ambx-grid) * 16);
|
|
579
|
-
--ambx-switch-h: calc(var(--ambx-grid) * 8);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
1108
|
.ambx-stack {
|
|
583
1109
|
display: inline-flex;
|
|
584
1110
|
flex-direction: column;
|
|
@@ -594,89 +1120,270 @@
|
|
|
594
1120
|
color: var(--amb-label);
|
|
595
1121
|
}
|
|
596
1122
|
|
|
597
|
-
.ambx-
|
|
1123
|
+
.ambx-travel-horizontal + .ambx-label {
|
|
598
1124
|
margin-top: var(--ambx-slider-label-clearance);
|
|
599
1125
|
}
|
|
600
1126
|
|
|
601
|
-
.ambx-
|
|
1127
|
+
.ambx-travel-vertical + .ambx-label {
|
|
602
1128
|
margin-top: var(--ambx-fader-label-clearance);
|
|
603
1129
|
}
|
|
604
1130
|
|
|
605
|
-
.ambx-
|
|
1131
|
+
.ambx-rotary + .ambx-label {
|
|
606
1132
|
margin-top: var(--ambx-knob-label-clearance);
|
|
607
1133
|
}
|
|
608
1134
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1135
|
+
/* The full ring reaches past the knob's own box, so it has to be paid for in
|
|
1136
|
+
layout or it collides with whatever sits beside the knob. Geometrically only
|
|
1137
|
+
three sides need it — the widest dots are the pair at ±90deg (1.33R out,
|
|
1138
|
+
left and right) and the one at 12 o'clock, while the arc's bottom ends land
|
|
1139
|
+
at 0.94R, inside the box — but the clearance is applied on all four sides
|
|
1140
|
+
anyway, and that is deliberate: margin on three sides moves the knob off the
|
|
1141
|
+
centre of its own box, so turning markers on would shift the knob down from
|
|
1142
|
+
wherever a caller positioned it. Measured against the referent, the
|
|
1143
|
+
three-sided version put the knob 6.4px low at the md size. The price is a
|
|
1144
|
+
band of unused space under the arc; keeping the knob concentric with the box
|
|
1145
|
+
it occupies is worth it.
|
|
1146
|
+
|
|
1147
|
+
markers="ends" gets no clearance: its two dots sit at 0.94R, so only the
|
|
1148
|
+
1.3px their edges reach past the box (at md) falls outside, well inside the
|
|
1149
|
+
drop shadow the knob already casts there. */
|
|
1150
|
+
.amb-knob-markers-full {
|
|
1151
|
+
margin: var(--ambx-knob-marker-clearance);
|
|
1152
|
+
--ambx-knob-marker-clearance: calc(
|
|
1153
|
+
var(--ambx-knob-marker-radius) + var(--ambx-knob-marker-size) / 2 -
|
|
1154
|
+
var(--ambx-knob-size) / 2
|
|
1155
|
+
);
|
|
617
1156
|
}
|
|
618
1157
|
|
|
619
|
-
.ambx-knob-sm { --ambx-knob-size: calc(var(--ambx-grid) * 12); } /* 48px */
|
|
620
|
-
.ambx-knob-md { --ambx-knob-size: calc(var(--ambx-grid) * 16); } /* 64px */
|
|
621
|
-
.ambx-knob-lg { --ambx-knob-size: calc(var(--ambx-grid) * 20); } /* 80px */
|
|
622
1158
|
|
|
623
|
-
|
|
1159
|
+
/* ==========================================================================
|
|
1160
|
+
KIT: console
|
|
1161
|
+
|
|
1162
|
+
A mixer-desk visual language, measured off photographs of the real
|
|
1163
|
+
controls. It dresses knobs and switches and leaves the rest to `grounded`,
|
|
1164
|
+
which is what a partial third-party kit looks like.
|
|
1165
|
+
|
|
1166
|
+
Everything here is paint. The kit adds no behaviour and touches no
|
|
1167
|
+
mechanism: the toggle's thumb travels because the latch already moves its
|
|
1168
|
+
actuator frame by --ambx-percent, and the track lights because a stylesheet
|
|
1169
|
+
can read that same number. If a genuinely different visual language had
|
|
1170
|
+
needed new mechanism code, the v3 split would not have been clean enough.
|
|
1171
|
+
========================================================================== */
|
|
1172
|
+
|
|
1173
|
+
/* -- console knob ---------------------------------------------------------
|
|
1174
|
+
A cuboid bar standing on a flat face, housed in a circular groove cut into
|
|
1175
|
+
the panel — the same construction the button uses, a groove for the
|
|
1176
|
+
housing and the flat part seated in it. The bar IS the pointer, and a
|
|
1177
|
+
short mark printed near one end of it says which end reads. Measured off
|
|
1178
|
+
the reference: the housing ring is the button's own clearance gap and the
|
|
1179
|
+
bar is 0.24 of the face it stands on. */
|
|
1180
|
+
.amb-console-knob {
|
|
1181
|
+
border-radius: 50%;
|
|
1182
|
+
/* The same gap the button leaves around its cap, and a fixed length for
|
|
1183
|
+
the same reason: it is a clearance between two parts, not a feature
|
|
1184
|
+
that scales with the control. */
|
|
1185
|
+
--ambx-console-housing: var(--ambx-grid-half);
|
|
1186
|
+
--ambx-console-face: calc(var(--ambx-size) - 2 * var(--ambx-console-housing));
|
|
1187
|
+
--ambx-console-bar-width: calc(var(--ambx-console-face) * 0.24);
|
|
1188
|
+
/* Just short of the face. The bar swings through every angle inside a
|
|
1189
|
+
circle, so anything longer would cross the housing at the diagonals —
|
|
1190
|
+
and unlike the panel graphics, which pay for their overhang in the two
|
|
1191
|
+
clearance classes at the end of this block, the actuator has no margin
|
|
1192
|
+
of its own to spend. */
|
|
1193
|
+
--ambx-console-bar-length: calc(var(--ambx-console-face) * 0.94);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/* The housing: a circular groove, showing as a thin dark ring around the
|
|
1197
|
+
face — the button's well, cut round. It takes the button's seat depth and
|
|
1198
|
+
the button's floor colour: --amb-lume is near-black under a bright key,
|
|
1199
|
+
which is what makes a clearance gap read as a gap rather than as a paler
|
|
1200
|
+
ring of the same panel. */
|
|
1201
|
+
.amb-console-housing {
|
|
624
1202
|
position: absolute;
|
|
625
1203
|
inset: 0;
|
|
1204
|
+
border-radius: 50%;
|
|
1205
|
+
--amb-thickness: 0.27;
|
|
1206
|
+
background-color: var(--amb-lume);
|
|
626
1207
|
}
|
|
627
1208
|
|
|
628
|
-
.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1209
|
+
/* The face: flat, and the panel's own surface colour. No thickness, so it
|
|
1210
|
+
has no edge to catch light and casts nothing — what draws the knob's
|
|
1211
|
+
outline is the dark ring of housing around it. */
|
|
1212
|
+
.amb-console-face {
|
|
1213
|
+
position: absolute;
|
|
1214
|
+
inset: var(--ambx-console-housing);
|
|
1215
|
+
border-radius: 50%;
|
|
634
1216
|
}
|
|
635
1217
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1218
|
+
/* The bar rides the rotating actuator frame, so its lit edge would turn with
|
|
1219
|
+
it: at 180deg the chamfer highlight would sit on the shadow side of the
|
|
1220
|
+
screen and the drop shadow would fall towards the light. Rotating the
|
|
1221
|
+
LIGHT the other way fixes both at once, and it is the same trick the
|
|
1222
|
+
scale ring plays in reverse — the geometry is already published on the
|
|
1223
|
+
root, so a stylesheet can do the trigonometry.
|
|
1224
|
+
|
|
1225
|
+
This outer span exists only to capture the scene's light vector: a custom
|
|
1226
|
+
property cannot read itself, and `--amb-light-x: calc(var(--amb-light-x)
|
|
1227
|
+
...)` on one element is a cycle, which resolves to invalid at
|
|
1228
|
+
computed-value time and takes the whole .ambient box-shadow composite
|
|
1229
|
+
down with it — silently, and looking exactly like a flat rectangle. */
|
|
1230
|
+
.amb-console-bar {
|
|
1231
|
+
position: absolute;
|
|
1232
|
+
inset: 0;
|
|
1233
|
+
--_ambx-console-light-x: var(--amb-light-x);
|
|
1234
|
+
--_ambx-console-light-y: var(--amb-light-y);
|
|
640
1235
|
}
|
|
641
1236
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1237
|
+
/* The world light, restated in the frame's own turned coordinates: the
|
|
1238
|
+
inverse rotation, [cos, sin; -sin, cos] applied to the captured vector.
|
|
1239
|
+
--ambx-angle is an <angle>, which is what cos()/sin() want, so the whole
|
|
1240
|
+
correction is these two lines. */
|
|
1241
|
+
.amb-console-bar-body {
|
|
1242
|
+
position: absolute;
|
|
1243
|
+
top: 50%;
|
|
1244
|
+
left: 50%;
|
|
1245
|
+
width: var(--ambx-console-bar-width);
|
|
1246
|
+
height: var(--ambx-console-bar-length);
|
|
1247
|
+
margin-top: calc(var(--ambx-console-bar-length) / -2);
|
|
1248
|
+
margin-left: calc(var(--ambx-console-bar-width) / -2);
|
|
1249
|
+
/* A machined round-over, not a pill: the reference bar's corners are
|
|
1250
|
+
barely broken. */
|
|
1251
|
+
border-radius: calc(var(--ambx-size) * 0.028);
|
|
1252
|
+
/* The bar is the panel's own surface colour, the same material as the
|
|
1253
|
+
face it stands on, so it is not told apart by tone at all: the chamfer
|
|
1254
|
+
down its long edges and the shadow it drops on the face are what read
|
|
1255
|
+
as a block standing proud. */
|
|
1256
|
+
--amb-light-x: calc(
|
|
1257
|
+
var(--_ambx-console-light-x) * cos(var(--ambx-angle)) +
|
|
1258
|
+
var(--_ambx-console-light-y) * sin(var(--ambx-angle))
|
|
1259
|
+
);
|
|
1260
|
+
--amb-light-y: calc(
|
|
1261
|
+
var(--_ambx-console-light-y) * cos(var(--ambx-angle)) -
|
|
1262
|
+
var(--_ambx-console-light-x) * sin(var(--ambx-angle))
|
|
1263
|
+
);
|
|
1264
|
+
/* Standing on the face, not floating above it: the block's own body casts
|
|
1265
|
+
the shadow (the part carries the thickness), and elevation stays at zero
|
|
1266
|
+
so the shadow hugs its shadow-side edges the way a body at rest does. */
|
|
1267
|
+
--amb-elevation: 0;
|
|
646
1268
|
}
|
|
647
1269
|
|
|
648
|
-
.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1270
|
+
/* The indicator: a short mark printed across the bar near one end. It is
|
|
1271
|
+
ink, not geometry — no edges, no shadow — and it is inside the bar rather
|
|
1272
|
+
than beside it because the bar already occupies the whole diameter it
|
|
1273
|
+
would otherwise sit on. */
|
|
1274
|
+
.amb-console-indicator {
|
|
1275
|
+
position: absolute;
|
|
1276
|
+
left: 50%;
|
|
1277
|
+
top: 7%;
|
|
1278
|
+
width: 62%;
|
|
1279
|
+
height: 7.5%;
|
|
1280
|
+
transform: translateX(-50%);
|
|
1281
|
+
border-radius: calc(var(--ambx-size) * 0.012);
|
|
1282
|
+
background-color: color-mix(in oklab, var(--amb-lit), black 82%);
|
|
652
1283
|
}
|
|
653
1284
|
|
|
654
|
-
|
|
655
|
-
|
|
1285
|
+
/* Panel graphics: the accent centre mark above the knob and the -/+ legends
|
|
1286
|
+
beside it. All three sit outside the knob's own box, which is what the
|
|
1287
|
+
panel frame allows — and what the two clearance classes below pay for. */
|
|
1288
|
+
.amb-console-marks {
|
|
1289
|
+
position: absolute;
|
|
1290
|
+
inset: 0;
|
|
656
1291
|
}
|
|
657
1292
|
|
|
658
|
-
.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1293
|
+
.amb-console-mark {
|
|
1294
|
+
position: absolute;
|
|
1295
|
+
left: 50%;
|
|
1296
|
+
bottom: 100%;
|
|
1297
|
+
width: calc(var(--ambx-size) * 0.085);
|
|
1298
|
+
height: calc(var(--ambx-size) * 0.2);
|
|
1299
|
+
margin-bottom: calc(var(--ambx-size) * 0.07);
|
|
1300
|
+
border-radius: var(--ambx-radius-sm);
|
|
1301
|
+
background-color: var(--ambx-accent);
|
|
1302
|
+
transform: translateX(-50%);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.amb-console-legend {
|
|
1306
|
+
position: absolute;
|
|
1307
|
+
top: 58%;
|
|
1308
|
+
font-size: calc(var(--ambx-size) * 0.26);
|
|
1309
|
+
line-height: 1;
|
|
1310
|
+
color: var(--amb-label);
|
|
663
1311
|
}
|
|
664
1312
|
|
|
665
|
-
.
|
|
666
|
-
|
|
667
|
-
|
|
1313
|
+
.amb-console-legend-min {
|
|
1314
|
+
right: 100%;
|
|
1315
|
+
margin-right: calc(var(--ambx-size) * 0.1);
|
|
668
1316
|
}
|
|
669
1317
|
|
|
670
|
-
.
|
|
671
|
-
|
|
672
|
-
|
|
1318
|
+
.amb-console-legend-max {
|
|
1319
|
+
left: 100%;
|
|
1320
|
+
margin-left: calc(var(--ambx-size) * 0.1);
|
|
673
1321
|
}
|
|
674
1322
|
|
|
675
|
-
.
|
|
676
|
-
|
|
677
|
-
|
|
1323
|
+
/* Clearance for graphics that overflow the control's box. The centre mark
|
|
1324
|
+
only reaches upward, so unlike the grounded marker ring — which is paid
|
|
1325
|
+
for on all four sides to keep the knob concentric — this one is a top
|
|
1326
|
+
margin, and the knob deliberately sits low in its box because that is
|
|
1327
|
+
where the mark puts it. */
|
|
1328
|
+
.amb-console-knob-marked {
|
|
1329
|
+
margin-top: calc(var(--ambx-size) * 0.3);
|
|
678
1330
|
}
|
|
679
1331
|
|
|
680
|
-
.
|
|
681
|
-
|
|
1332
|
+
.amb-console-knob-legended {
|
|
1333
|
+
margin-inline: calc(var(--ambx-size) * 0.34);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/* -- console toggle -------------------------------------------------------
|
|
1337
|
+
A pill track that fills with the accent as the switch travels, carrying a
|
|
1338
|
+
round accent thumb inside a white ring. Track 2.06:1; the thumb takes 0.9
|
|
1339
|
+
of the track height, which is a thumb that nearly fills its groove — the
|
|
1340
|
+
clearance has to come down with it or a thumb that size runs off the ends
|
|
1341
|
+
of its own travel. */
|
|
1342
|
+
.amb-console-toggle {
|
|
1343
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 13);
|
|
1344
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 6.3);
|
|
1345
|
+
--ambx-latch-thumb-w: calc(var(--ambx-switch-h) * 0.9);
|
|
1346
|
+
--ambx-latch-thumb-h: calc(var(--ambx-switch-h) * 0.9);
|
|
1347
|
+
--ambx-latch-inset: calc(var(--ambx-switch-h) * 0.05);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/* The track is a pure-CSS part: it reads --ambx-percent off the control root
|
|
1351
|
+
and mixes its own colour from it, so it lights and dims with the travel
|
|
1352
|
+
without the mechanism knowing this element exists. */
|
|
1353
|
+
.amb-console-track {
|
|
1354
|
+
position: absolute;
|
|
1355
|
+
inset: 0;
|
|
1356
|
+
border-radius: var(--ambx-radius-full);
|
|
1357
|
+
background-color: color-mix(
|
|
1358
|
+
in oklab,
|
|
1359
|
+
color-mix(in oklab, var(--amb-lit), black 55%),
|
|
1360
|
+
var(--ambx-accent) calc(var(--ambx-percent) * 100%)
|
|
1361
|
+
);
|
|
1362
|
+
--amb-thickness: 0.4;
|
|
1363
|
+
transition: background-color 160ms ease;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/* Flat-topped, but a body: `.ambient` with a thickness and NO edge
|
|
1367
|
+
treatment, which is the combination that casts a drop shadow while
|
|
1368
|
+
leaving the top face unbroken — the chamfer and fillet layers of the
|
|
1369
|
+
composite are gated on their own properties, and neither is set here. */
|
|
1370
|
+
.amb-console-thumb {
|
|
1371
|
+
position: absolute;
|
|
1372
|
+
inset: 0;
|
|
1373
|
+
border-radius: 50%;
|
|
1374
|
+
background-color: color-mix(in oklab, var(--ambx-accent), white 12%);
|
|
1375
|
+
/* The white ring is a BORDER, not a spread shadow. Two reasons, and the
|
|
1376
|
+
second is the load-bearing one: with the package's border-box reset the
|
|
1377
|
+
ring grows inward, so the thumb keeps the outer diameter measured off
|
|
1378
|
+
the reference — and `box-shadow` is the property `.ambient` composites
|
|
1379
|
+
the whole lighting model into, so setting it here would silently wipe
|
|
1380
|
+
the drop shadow the thickness above is there to cast. */
|
|
1381
|
+
border: calc(var(--ambx-latch-thumb-h) * 0.09) solid
|
|
1382
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 99%);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1386
|
+
.amb-console-track {
|
|
1387
|
+
transition: none;
|
|
1388
|
+
}
|
|
682
1389
|
}
|