@ambientcss/components 2.0.1 → 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 +1563 -384
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +763 -48
- package/dist/index.d.ts +763 -48
- package/dist/index.js +1524 -385
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1102 -256
- package/package.json +2 -2
- package/src/components/AmbientButton.tsx +25 -25
- package/src/components/AmbientFader.tsx +33 -115
- package/src/components/AmbientKnob.tsx +64 -222
- package/src/components/AmbientPanel.tsx +8 -2
- package/src/components/AmbientProvider.tsx +3 -0
- package/src/components/AmbientRack.tsx +32 -0
- package/src/components/AmbientSelect.tsx +40 -0
- package/src/components/AmbientSlider.tsx +32 -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 +127 -4
- 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 +1102 -256
package/src/styles.css
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
--ambx-grid: 4px;
|
|
3
3
|
--ambx-grid-half: calc(var(--ambx-grid) / 2);
|
|
4
4
|
--ambx-grid-quarter: calc(var(--ambx-grid) / 4);
|
|
5
|
+
/* Border-radius scale: deliberately identical in px to
|
|
6
|
+
@ambientcss/css's frozen .amb-rounded/-md/-lg/-xl/-full utilities
|
|
7
|
+
(4/8/12/16/9999) — cross-layer coherence, not coincidence. Radius
|
|
8
|
+
stays fixed across size variants; only footprint scales with size. */
|
|
9
|
+
--ambx-radius-sm: calc(var(--ambx-grid) * 1);
|
|
10
|
+
--ambx-radius-md: calc(var(--ambx-grid) * 2);
|
|
11
|
+
--ambx-radius-lg: calc(var(--ambx-grid) * 3);
|
|
12
|
+
--ambx-radius-xl: calc(var(--ambx-grid) * 4);
|
|
13
|
+
--ambx-radius-full: 9999px;
|
|
14
|
+
/* Spacing/clearance scale for composing controls into a device face
|
|
15
|
+
(hardware-panel convention: a single modular pitch, tighter within a
|
|
16
|
+
functional cluster, looser between clusters/at the panel edge). Each
|
|
17
|
+
tier ~1.6-1.7x the last so they stay visually distinct at a glance. */
|
|
18
|
+
--ambx-gap-tight: calc(var(--ambx-grid) * 3);
|
|
19
|
+
--ambx-gap-normal: calc(var(--ambx-grid) * 5);
|
|
20
|
+
--ambx-gap-loose: calc(var(--ambx-grid) * 8);
|
|
5
21
|
--ambx-type-1-size: calc(var(--ambx-grid) * 8);
|
|
6
22
|
--ambx-type-1-line: calc(var(--ambx-grid) * 10);
|
|
7
23
|
--ambx-type-2-size: calc(var(--ambx-grid) * 6);
|
|
@@ -12,6 +28,10 @@
|
|
|
12
28
|
--ambx-label-size: calc(var(--ambx-grid) * 2 + var(--ambx-grid-half));
|
|
13
29
|
--ambx-label-line: calc(var(--ambx-grid) * 3);
|
|
14
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);
|
|
15
35
|
--ambx-knob-label-clearance: calc(var(--ambx-grid) * 2);
|
|
16
36
|
--ambx-slider-label-clearance: calc(var(--ambx-grid) * 2);
|
|
17
37
|
--ambx-fader-label-clearance: calc(var(--ambx-grid) * 4);
|
|
@@ -25,19 +45,11 @@
|
|
|
25
45
|
rather than shipped as a global reset: apps that already reset globally
|
|
26
46
|
(apps/demo does) see no change, and apps that do not are no longer
|
|
27
47
|
silently broken. */
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.amb-switch,
|
|
31
|
-
.amb-fader,
|
|
32
|
-
.amb-slider,
|
|
48
|
+
.ambx-control,
|
|
49
|
+
.ambx-control *,
|
|
33
50
|
.ambx-panel,
|
|
34
|
-
.ambx-stack,
|
|
35
|
-
.amb-button *,
|
|
36
|
-
.amb-knob *,
|
|
37
|
-
.amb-switch *,
|
|
38
|
-
.amb-fader *,
|
|
39
|
-
.amb-slider *,
|
|
40
51
|
.ambx-panel *,
|
|
52
|
+
.ambx-stack,
|
|
41
53
|
.ambx-stack * {
|
|
42
54
|
box-sizing: border-box;
|
|
43
55
|
}
|
|
@@ -60,6 +72,256 @@
|
|
|
60
72
|
letter-spacing: var(--ambx-type-tracking);
|
|
61
73
|
}
|
|
62
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
|
+
|
|
63
325
|
/* Button: a chamfered key cap seated in a clearance well — the referent
|
|
64
326
|
is components/button.py's "flush" style (cap seated into a hole with a
|
|
65
327
|
shadow gap ring around it). The button element IS the well: a shallow
|
|
@@ -73,19 +335,154 @@
|
|
|
73
335
|
padding: var(--ambx-grid-half); /* the clearance gap ring */
|
|
74
336
|
--amb-thickness: 0.27; /* seat depth of the well */
|
|
75
337
|
background-color: var(--amb-lume);
|
|
76
|
-
|
|
77
|
-
|
|
338
|
+
/* Well radius = cap radius + its own clearance padding (concentric
|
|
339
|
+
shapes nest by adding the gap, not by picking a second number). */
|
|
340
|
+
border-radius: calc(var(--ambx-radius-lg) + var(--ambx-grid-half));
|
|
341
|
+
min-width: var(--ambx-button-size);
|
|
78
342
|
}
|
|
79
343
|
|
|
80
344
|
.amb-button-cap {
|
|
81
345
|
display: grid;
|
|
82
346
|
place-items: center;
|
|
83
347
|
width: 100%;
|
|
84
|
-
padding:
|
|
85
|
-
border-radius:
|
|
348
|
+
padding: var(--ambx-button-cap-pad-y) var(--ambx-button-cap-pad-x);
|
|
349
|
+
border-radius: var(--ambx-radius-lg);
|
|
86
350
|
text-transform: uppercase;
|
|
87
351
|
font-weight: 600;
|
|
88
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;
|
|
89
486
|
}
|
|
90
487
|
|
|
91
488
|
.amb-button:focus {
|
|
@@ -96,6 +493,13 @@
|
|
|
96
493
|
color: var(--amb-highlight-color);
|
|
97
494
|
}
|
|
98
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. */
|
|
99
503
|
.amb-button:active .amb-button-cap {
|
|
100
504
|
--amb-thickness: 0.84;
|
|
101
505
|
}
|
|
@@ -107,27 +511,30 @@
|
|
|
107
511
|
3.6mm cap instead of the key's 4.5mm — thickness 0.8 — with the same
|
|
108
512
|
0.7mm press travel). */
|
|
109
513
|
.amb-button.amb-button-round {
|
|
110
|
-
min-width:
|
|
514
|
+
min-width: var(--ambx-button-size);
|
|
111
515
|
aspect-ratio: 1;
|
|
112
516
|
border-radius: 50%;
|
|
113
517
|
}
|
|
114
518
|
|
|
115
519
|
.amb-button-round .amb-button-cap {
|
|
116
520
|
height: 100%;
|
|
117
|
-
padding: var(--ambx-
|
|
521
|
+
padding: var(--ambx-button-cap-inset);
|
|
118
522
|
border-radius: 50%;
|
|
119
523
|
}
|
|
120
524
|
|
|
121
525
|
.amb-button.amb-button-square {
|
|
122
|
-
min-width:
|
|
526
|
+
min-width: var(--ambx-button-size);
|
|
123
527
|
aspect-ratio: 1;
|
|
124
|
-
border-radius:
|
|
528
|
+
border-radius: var(--ambx-radius-md);
|
|
125
529
|
}
|
|
126
530
|
|
|
127
531
|
.amb-button-square .amb-button-cap {
|
|
128
532
|
height: 100%;
|
|
129
|
-
padding: var(--ambx-
|
|
130
|
-
|
|
533
|
+
padding: var(--ambx-button-cap-inset);
|
|
534
|
+
/* Cap radius = well radius (var(--ambx-radius-md)) minus its own inset
|
|
535
|
+
clearance — the same concentric-nesting rule as the pill, solved the
|
|
536
|
+
other way because here the well, not the cap, sits on the scale. */
|
|
537
|
+
border-radius: calc(var(--ambx-radius-md) - var(--ambx-grid-half));
|
|
131
538
|
--amb-thickness: 0.8;
|
|
132
539
|
}
|
|
133
540
|
|
|
@@ -136,207 +543,192 @@
|
|
|
136
543
|
}
|
|
137
544
|
|
|
138
545
|
/* Knob: a knob-scale body (thickness 2 = the referent knob.py's 9mm
|
|
139
|
-
height) resting on the panel
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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. */
|
|
145
562
|
.amb-knob {
|
|
146
563
|
border-radius: 50%;
|
|
147
|
-
|
|
148
|
-
width: calc(var(--ambx-
|
|
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);
|
|
149
568
|
}
|
|
150
569
|
|
|
151
|
-
/* The
|
|
152
|
-
the ambient drop shadow (kept circular — the
|
|
153
|
-
read in a penumbra)
|
|
154
|
-
|
|
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. */
|
|
155
585
|
.amb-knob-body {
|
|
156
586
|
position: absolute;
|
|
157
|
-
inset:
|
|
587
|
+
inset: 0;
|
|
158
588
|
border-radius: 50%;
|
|
589
|
+
--amb-chamfer: 1;
|
|
590
|
+
--amb-chamfer-width: 2;
|
|
159
591
|
}
|
|
160
592
|
|
|
161
|
-
/* The rotating
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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. */
|
|
167
611
|
.amb-knob-face {
|
|
168
612
|
position: absolute;
|
|
169
613
|
inset: 0;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
var(--amb-light-saturation)
|
|
173
|
-
calc(
|
|
174
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
175
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
176
|
-
)
|
|
177
|
-
);
|
|
614
|
+
--amb-shade: 0.88;
|
|
615
|
+
background-color: var(--amb-lit);
|
|
178
616
|
background-image:
|
|
179
617
|
radial-gradient(
|
|
180
|
-
circle,
|
|
181
|
-
hsl(
|
|
182
|
-
var(--amb-light-hue)
|
|
183
|
-
var(--amb-light-saturation)
|
|
184
|
-
calc(
|
|
185
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
186
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
187
|
-
)
|
|
188
|
-
) 0 60%,
|
|
189
|
-
transparent 61%
|
|
190
|
-
),
|
|
191
|
-
repeating-conic-gradient(
|
|
192
|
-
from 90deg,
|
|
193
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
194
|
-
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.4deg,
|
|
195
|
-
transparent 2.2deg 4.6deg,
|
|
618
|
+
circle closest-side,
|
|
196
619
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
197
620
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
198
|
-
* 0.
|
|
199
|
-
transparent 7.2deg 10deg
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* Fluted face (flute variant — the OP-Z-style referent: 14 broad
|
|
204
|
-
ridges split by narrow grooves, pitch 25.714deg). Flank bands sit on
|
|
205
|
-
the clip's rising (0–2.1deg) and falling (18.5–20.6deg) flanks;
|
|
206
|
-
deeper flutes catch more key light, so both bands run stronger than
|
|
207
|
-
the 36-rib knurl's. */
|
|
208
|
-
.amb-knob-face-flute {
|
|
209
|
-
background-image:
|
|
210
|
-
radial-gradient(
|
|
211
|
-
circle,
|
|
212
|
-
hsl(
|
|
213
|
-
var(--amb-light-hue)
|
|
214
|
-
var(--amb-light-saturation)
|
|
215
|
-
calc(
|
|
216
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
217
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
218
|
-
)
|
|
219
|
-
) 0 60%,
|
|
220
|
-
transparent 61%
|
|
221
|
-
),
|
|
222
|
-
repeating-conic-gradient(
|
|
223
|
-
from 90deg,
|
|
224
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
225
|
-
calc(var(--amb-key-light-intensity) * 0.45)) 0deg 2.1deg,
|
|
226
|
-
transparent 3.2deg 17.4deg,
|
|
621
|
+
* 0.07 + 0.09)) 89%,
|
|
227
622
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
228
623
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
229
|
-
* 0.
|
|
230
|
-
transparent
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/* Fine knurl (cap and wheel variants: 48 ribs, pitch 7.5deg) — the
|
|
235
|
-
36-rib pattern's angles scaled by 3/4, same flank alphas. */
|
|
236
|
-
.amb-knob-face-fine {
|
|
237
|
-
background-image:
|
|
238
|
-
radial-gradient(
|
|
239
|
-
circle,
|
|
240
|
-
hsl(
|
|
241
|
-
var(--amb-light-hue)
|
|
242
|
-
var(--amb-light-saturation)
|
|
243
|
-
calc(
|
|
244
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
245
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
246
|
-
)
|
|
247
|
-
) 0 60%,
|
|
248
|
-
transparent 61%
|
|
624
|
+
* 0.02 + 0.03)) 95%,
|
|
625
|
+
transparent 100%
|
|
249
626
|
),
|
|
250
627
|
repeating-conic-gradient(
|
|
251
628
|
from 90deg,
|
|
629
|
+
transparent 0deg,
|
|
252
630
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
253
|
-
calc(var(--amb-key-light-intensity) * 0.
|
|
254
|
-
transparent
|
|
631
|
+
calc(var(--amb-key-light-intensity) * 0.26)) 1.875deg,
|
|
632
|
+
transparent 3.75deg,
|
|
255
633
|
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
256
634
|
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
257
|
-
* 0.
|
|
258
|
-
transparent
|
|
635
|
+
* 0.24 + 0.13)) 5.625deg,
|
|
636
|
+
transparent 7.5deg
|
|
259
637
|
);
|
|
260
638
|
}
|
|
261
639
|
|
|
262
|
-
/*
|
|
263
|
-
|
|
264
|
-
replaces the surface-colored center disc over the fine knurl. */
|
|
265
|
-
.amb-knob-face-cap {
|
|
266
|
-
background-image:
|
|
267
|
-
radial-gradient(circle, var(--amb-highlight-color) 0 65%, transparent 66%),
|
|
268
|
-
repeating-conic-gradient(
|
|
269
|
-
from 90deg,
|
|
270
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
271
|
-
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.05deg,
|
|
272
|
-
transparent 1.65deg 3.45deg,
|
|
273
|
-
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
274
|
-
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
275
|
-
* 0.3 + 0.16)) 3.75deg 4.8deg,
|
|
276
|
-
transparent 5.4deg 7.5deg
|
|
277
|
-
);
|
|
278
|
-
}
|
|
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.
|
|
279
642
|
|
|
280
|
-
|
|
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 {
|
|
281
646
|
position: absolute;
|
|
282
647
|
top: 16%;
|
|
283
648
|
left: 50%;
|
|
284
649
|
transform: translateX(-50%);
|
|
285
|
-
width: calc(var(--ambx-
|
|
286
|
-
height: calc(var(--ambx-
|
|
650
|
+
width: calc(var(--ambx-knob-size) * 0.125);
|
|
651
|
+
height: calc(var(--ambx-knob-size) * 0.125);
|
|
287
652
|
border-radius: 50%;
|
|
288
653
|
background: var(--amb-highlight-color);
|
|
289
654
|
}
|
|
290
655
|
|
|
291
|
-
/*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/* Radial indicator line (line variant): the referent's accent bar,
|
|
301
|
-
center to ~85% of the cap radius, pointing at 12 o'clock. */
|
|
302
|
-
.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 {
|
|
303
663
|
position: absolute;
|
|
304
|
-
top:
|
|
664
|
+
top: 8%;
|
|
305
665
|
left: 50%;
|
|
306
666
|
transform: translateX(-50%);
|
|
307
|
-
width:
|
|
308
|
-
height:
|
|
309
|
-
border-radius:
|
|
667
|
+
width: var(--ambx-knob-rect-width);
|
|
668
|
+
height: 17%;
|
|
669
|
+
border-radius: var(--ambx-grid-quarter);
|
|
310
670
|
background: var(--amb-highlight-color);
|
|
311
671
|
}
|
|
312
672
|
|
|
313
|
-
.amb-knob:hover .amb-knob-indicator-
|
|
314
|
-
.amb-knob:hover .amb-knob-indicator-
|
|
673
|
+
.amb-knob:hover .amb-knob-indicator-circle,
|
|
674
|
+
.amb-knob:hover .amb-knob-indicator-rectangle {
|
|
315
675
|
background: color-mix(in oklab, var(--amb-highlight-color), white 25%);
|
|
316
676
|
}
|
|
317
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
|
+
|
|
318
709
|
/* Tracks are grounded grooves (.amb-groove supplies the recess: lit-wall
|
|
319
710
|
shadow band + far-wall bounce) with a lume interior — dark in bright
|
|
320
711
|
light, glowing in low light — overriding the groove's neutral floor. */
|
|
321
|
-
.amb-
|
|
322
|
-
position:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
border-radius: var(--ambx-grid);
|
|
712
|
+
.amb-travel-track {
|
|
713
|
+
position: absolute;
|
|
714
|
+
inset: 0;
|
|
715
|
+
border-radius: var(--ambx-radius-full);
|
|
326
716
|
background-color: var(--amb-lume);
|
|
327
717
|
}
|
|
328
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
|
+
|
|
329
725
|
/* Fader thumb: the referent (fader.py) is a pill cap on a stem — 7mm
|
|
330
726
|
tall (thickness 1.5) riding 2.2mm above the plate (elevation 0.28) —
|
|
331
727
|
with a single grip line across the top. */
|
|
332
728
|
.amb-fader-thumb {
|
|
333
729
|
position: absolute;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
left: 50%;
|
|
337
|
-
top: 50%;
|
|
338
|
-
transform: translate(-50%, -50%);
|
|
339
|
-
border-radius: calc(var(--ambx-grid) * 2);
|
|
730
|
+
inset: 0;
|
|
731
|
+
border-radius: var(--ambx-radius-md);
|
|
340
732
|
display: grid;
|
|
341
733
|
place-items: center;
|
|
342
734
|
--amb-thickness: 1.5;
|
|
@@ -350,29 +742,20 @@
|
|
|
350
742
|
background: var(--amb-lume);
|
|
351
743
|
}
|
|
352
744
|
|
|
353
|
-
/*
|
|
354
|
-
deep = thickness 0.22), not a through-slot, with a domed disc thumb
|
|
355
|
-
gliding over it. */
|
|
356
|
-
.amb-slider {
|
|
357
|
-
position: relative;
|
|
358
|
-
height: calc(var(--ambx-grid) * 1.5);
|
|
359
|
-
width: calc(var(--ambx-grid) * 30);
|
|
360
|
-
border-radius: var(--ambx-grid);
|
|
361
|
-
background-color: var(--amb-lume);
|
|
362
|
-
--amb-thickness: 0.22;
|
|
363
|
-
}
|
|
364
|
-
|
|
745
|
+
/* A domed disc gliding over the channel. */
|
|
365
746
|
.amb-slider-thumb {
|
|
366
747
|
position: absolute;
|
|
367
|
-
|
|
368
|
-
height: calc(var(--ambx-grid) * 6);
|
|
369
|
-
top: 50%;
|
|
370
|
-
left: 50%;
|
|
371
|
-
transform: translate(-50%, -50%);
|
|
748
|
+
inset: 0;
|
|
372
749
|
border-radius: 50%;
|
|
373
750
|
--amb-thickness: 0.7;
|
|
374
751
|
}
|
|
375
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
|
+
|
|
376
759
|
.amb-led {
|
|
377
760
|
width: calc(var(--ambx-grid) * 2);
|
|
378
761
|
height: calc(var(--ambx-grid) * 2);
|
|
@@ -395,22 +778,9 @@
|
|
|
395
778
|
in a dark stadium recess (well 1.5mm deep = thickness 0.33; the pill
|
|
396
779
|
stands 2.6mm above the recess floor = thickness 0.58), with an
|
|
397
780
|
optional LED above. The pill slides, nothing presses. */
|
|
398
|
-
.amb-switch {
|
|
399
|
-
cursor: pointer;
|
|
400
|
-
border: none;
|
|
401
|
-
padding: 0;
|
|
402
|
-
background: none;
|
|
403
|
-
display: inline-grid;
|
|
404
|
-
justify-items: center;
|
|
405
|
-
gap: var(--ambx-grid);
|
|
406
|
-
margin: 0;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
781
|
.amb-switch-track {
|
|
410
|
-
position:
|
|
411
|
-
|
|
412
|
-
width: var(--ambx-switch-w);
|
|
413
|
-
height: var(--ambx-switch-h);
|
|
782
|
+
position: absolute;
|
|
783
|
+
inset: 0;
|
|
414
784
|
border-radius: calc(var(--ambx-switch-h) / 2);
|
|
415
785
|
background-color: var(--amb-lume);
|
|
416
786
|
--amb-thickness: 0.33;
|
|
@@ -418,66 +788,321 @@
|
|
|
418
788
|
|
|
419
789
|
.amb-switch-pill {
|
|
420
790
|
position: absolute;
|
|
421
|
-
|
|
422
|
-
left: 0;
|
|
423
|
-
width: calc(var(--ambx-switch-w) * 0.52);
|
|
424
|
-
height: calc(var(--ambx-switch-h) * 0.78);
|
|
791
|
+
inset: 0;
|
|
425
792
|
border-radius: calc(var(--ambx-switch-h) * 0.39);
|
|
426
|
-
transform: translate(calc(var(--ambx-switch-h) * 0.11), -50%);
|
|
427
|
-
transition: transform 140ms ease;
|
|
428
793
|
--amb-thickness: 0.58;
|
|
429
794
|
}
|
|
430
795
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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);
|
|
436
802
|
}
|
|
437
803
|
|
|
438
804
|
.amb-switch:hover .amb-switch-pill {
|
|
439
805
|
background-color: color-mix(
|
|
440
806
|
in oklab,
|
|
441
|
-
|
|
442
|
-
var(--amb-light-hue)
|
|
443
|
-
var(--amb-light-saturation)
|
|
444
|
-
calc(
|
|
445
|
-
var(--amb-key-light-intensity) * 31.5% +
|
|
446
|
-
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
447
|
-
)
|
|
448
|
-
),
|
|
807
|
+
var(--amb-lit),
|
|
449
808
|
var(--amb-highlight-color) 12%
|
|
450
809
|
);
|
|
451
810
|
}
|
|
452
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
|
+
|
|
453
1020
|
.ambx-panel {
|
|
454
|
-
border-radius:
|
|
1021
|
+
border-radius: var(--ambx-radius-xl);
|
|
455
1022
|
padding: calc(var(--ambx-grid) * 4);
|
|
456
1023
|
}
|
|
457
1024
|
|
|
458
|
-
|
|
459
|
-
|
|
1025
|
+
/* Opt-in "device face" mode: edge margin = the loose gap tier, so a
|
|
1026
|
+
panel composing a .ambx-rack of controls reads its own border as one
|
|
1027
|
+
more slot in the same lattice (Rams/Braun, Teenage Engineering
|
|
1028
|
+
convention). Not the default padding above, which serves general
|
|
1029
|
+
containers (cards, settings panels) that aren't device faces. */
|
|
1030
|
+
.ambx-panel-device {
|
|
1031
|
+
padding: var(--ambx-gap-loose);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/* Layout primitive for composing controls into a device face: pick a
|
|
1035
|
+
gap tier by how tightly the controls belong together (tight = one
|
|
1036
|
+
functional unit/repeated row, normal = a few related controls, loose
|
|
1037
|
+
= separating distinct zones or the panel edge, see .ambx-panel-device
|
|
1038
|
+
above) rather than picking an arbitrary gap per layout. */
|
|
1039
|
+
.ambx-rack {
|
|
1040
|
+
display: flex;
|
|
1041
|
+
align-items: center;
|
|
1042
|
+
gap: var(--ambx-gap-normal);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.ambx-rack-tight {
|
|
1046
|
+
gap: var(--ambx-gap-tight);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.ambx-rack-loose {
|
|
1050
|
+
gap: var(--ambx-gap-loose);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.ambx-rack-column {
|
|
1054
|
+
flex-direction: column;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.ambx-press {
|
|
460
1058
|
line-height: var(--ambx-type-3-line);
|
|
1059
|
+
--ambx-button-size: calc(var(--ambx-grid) * 18); /* 72px pill */
|
|
1060
|
+
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2); /* 8px */
|
|
1061
|
+
--ambx-button-cap-pad-y: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
1062
|
+
--ambx-button-cap-inset: var(--ambx-grid); /* 4px */
|
|
461
1063
|
}
|
|
462
1064
|
|
|
463
|
-
.ambx-
|
|
464
|
-
--ambx-
|
|
465
|
-
--ambx-
|
|
1065
|
+
.ambx-press-sm {
|
|
1066
|
+
--ambx-button-size: calc(var(--ambx-grid) * 16); /* 64px pill */
|
|
1067
|
+
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
1068
|
+
--ambx-button-cap-pad-y: var(--ambx-grid); /* 4px */
|
|
1069
|
+
--ambx-button-cap-inset: calc(var(--ambx-grid) * 0.75); /* 3px */
|
|
466
1070
|
}
|
|
467
1071
|
|
|
468
|
-
.ambx-
|
|
469
|
-
--ambx-
|
|
470
|
-
--ambx-
|
|
1072
|
+
.ambx-press-md {
|
|
1073
|
+
--ambx-button-size: calc(var(--ambx-grid) * 18); /* 72px pill */
|
|
1074
|
+
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2); /* 8px */
|
|
1075
|
+
--ambx-button-cap-pad-y: calc(var(--ambx-grid) * 1.5); /* 6px */
|
|
1076
|
+
--ambx-button-cap-inset: var(--ambx-grid); /* 4px */
|
|
471
1077
|
}
|
|
472
1078
|
|
|
473
|
-
.ambx-
|
|
474
|
-
--ambx-
|
|
475
|
-
--ambx-
|
|
1079
|
+
.ambx-press-lg {
|
|
1080
|
+
--ambx-button-size: calc(var(--ambx-grid) * 20); /* 80px pill */
|
|
1081
|
+
--ambx-button-cap-pad-x: calc(var(--ambx-grid) * 2.5); /* 10px */
|
|
1082
|
+
--ambx-button-cap-pad-y: calc(var(--ambx-grid) * 2); /* 8px */
|
|
1083
|
+
--ambx-button-cap-inset: calc(var(--ambx-grid) * 1.25); /* 5px */
|
|
476
1084
|
}
|
|
477
1085
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1086
|
+
/* Round/square shapes have their own min-width table per size (not a
|
|
1087
|
+
ratio of the pill's — the three shapes' true md values, 72/48/56,
|
|
1088
|
+
share no common factor), overridden via compound selectors so they
|
|
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 */
|
|
1093
|
+
|
|
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 */
|
|
1097
|
+
|
|
1098
|
+
.ambx-press-sm .amb-button-cap {
|
|
1099
|
+
font-size: var(--ambx-label-size);
|
|
1100
|
+
line-height: var(--ambx-label-line);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.ambx-press-lg .amb-button-cap {
|
|
1104
|
+
font-size: calc(var(--ambx-grid) * 3.5);
|
|
1105
|
+
line-height: calc(var(--ambx-grid) * 5);
|
|
481
1106
|
}
|
|
482
1107
|
|
|
483
1108
|
.ambx-stack {
|
|
@@ -495,49 +1120,270 @@
|
|
|
495
1120
|
color: var(--amb-label);
|
|
496
1121
|
}
|
|
497
1122
|
|
|
498
|
-
.ambx-
|
|
1123
|
+
.ambx-travel-horizontal + .ambx-label {
|
|
499
1124
|
margin-top: var(--ambx-slider-label-clearance);
|
|
500
1125
|
}
|
|
501
1126
|
|
|
502
|
-
.ambx-
|
|
1127
|
+
.ambx-travel-vertical + .ambx-label {
|
|
503
1128
|
margin-top: var(--ambx-fader-label-clearance);
|
|
504
1129
|
}
|
|
505
1130
|
|
|
506
|
-
.ambx-
|
|
1131
|
+
.ambx-rotary + .ambx-label {
|
|
507
1132
|
margin-top: var(--ambx-knob-label-clearance);
|
|
508
1133
|
}
|
|
509
1134
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
|
|
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);
|
|
516
1194
|
}
|
|
517
1195
|
|
|
518
|
-
|
|
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 {
|
|
519
1202
|
position: absolute;
|
|
520
1203
|
inset: 0;
|
|
1204
|
+
border-radius: 50%;
|
|
1205
|
+
--amb-thickness: 0.27;
|
|
1206
|
+
background-color: var(--amb-lume);
|
|
521
1207
|
}
|
|
522
1208
|
|
|
523
|
-
.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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%;
|
|
528
1216
|
}
|
|
529
1217
|
|
|
530
|
-
|
|
531
|
-
|
|
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);
|
|
532
1235
|
}
|
|
533
1236
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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;
|
|
539
1268
|
}
|
|
540
1269
|
|
|
541
|
-
.
|
|
542
|
-
|
|
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%);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
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;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
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);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.amb-console-legend-min {
|
|
1314
|
+
right: 100%;
|
|
1315
|
+
margin-right: calc(var(--ambx-size) * 0.1);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.amb-console-legend-max {
|
|
1319
|
+
left: 100%;
|
|
1320
|
+
margin-left: calc(var(--ambx-size) * 0.1);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
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);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
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
|
+
}
|
|
543
1389
|
}
|