@ambientcss/components 1.2.0 → 2.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.
@@ -2,6 +2,73 @@ import { useId, useRef } from "react";
2
2
  import type { HTMLAttributes, KeyboardEvent, PointerEvent } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
 
5
+ /* Straight-knurled silhouettes for the rotating face, in
6
+ objectBoundingBox units so the clip scales with the component. Each
7
+ variant family mirrors its referent's rib section
8
+ (ambient3d/ground_components.py): trapezoid teeth between the outer
9
+ radius and the tooth-root radius. The circular body underneath keeps
10
+ the smooth drop shadow. */
11
+ type KnurlSpec = {
12
+ teeth: number; // rib count
13
+ root: number; // tooth-root radius (bounding-box units; outer is 0.5)
14
+ rise: number; // fraction of the pitch spent climbing to the crest
15
+ fall: [number, number]; // crest-end and root-return pitch fractions
16
+ };
17
+
18
+ const KNURLS: Record<"standard" | "flute" | "fine", KnurlSpec> = {
19
+ /* 36 ribs — the grounded referent knob (dot and line variants) */
20
+ standard: { teeth: 36, root: 0.468, rise: 0.12, fall: [0.5, 0.62] },
21
+ /* 14 broad flutes with deeper roots (OP-Z-style flute variant) */
22
+ flute: { teeth: 14, root: 0.44, rise: 0.08, fall: [0.72, 0.8] },
23
+ /* 48-rib fine knurl, shallower (cap and wheel variants) */
24
+ fine: { teeth: 48, root: 0.476, rise: 0.12, fall: [0.5, 0.62] }
25
+ };
26
+
27
+ function knurlPath({ teeth, root, rise, fall }: KnurlSpec): string {
28
+ const outer = 0.5;
29
+ const pitch = (Math.PI * 2) / teeth;
30
+ const pts: string[] = [];
31
+ for (let i = 0; i < teeth; i++) {
32
+ const a = i * pitch;
33
+ const tooth: Array<[number, number]> = [
34
+ [0, root],
35
+ [rise, outer],
36
+ [fall[0], outer],
37
+ [fall[1], root]
38
+ ];
39
+ for (const [frac, radius] of tooth) {
40
+ const t = a + frac * pitch;
41
+ pts.push(
42
+ `${(0.5 + radius * Math.cos(t)).toFixed(4)} ${(0.5 + radius * Math.sin(t)).toFixed(4)}`
43
+ );
44
+ }
45
+ }
46
+ return `M${pts.join(" L")} Z`;
47
+ }
48
+
49
+ const KNURL_PATHS = {
50
+ standard: knurlPath(KNURLS.standard),
51
+ flute: knurlPath(KNURLS.flute),
52
+ fine: knurlPath(KNURLS.fine)
53
+ };
54
+
55
+ /* Knob types from the referent lineup (ambient3d/generate.py):
56
+ - "dot": the grounded referent — 36-rib knurl, offset indicator dot
57
+ - "line": same body with a radial indicator line (classic pot)
58
+ - "flute": 14 broad flutes, centered dot (OP-Z-style encoder)
59
+ - "cap": fine knurl under a smooth accent top disc (OP-1-style)
60
+ - "wheel": bare fine knurl, no indicator (machined wheel — pair with
61
+ material="shiny") */
62
+ export type AmbientKnobVariant = "dot" | "line" | "flute" | "cap" | "wheel";
63
+
64
+ const VARIANT_FAMILY: Record<AmbientKnobVariant, keyof typeof KNURL_PATHS> = {
65
+ dot: "standard",
66
+ line: "standard",
67
+ flute: "flute",
68
+ cap: "fine",
69
+ wheel: "fine"
70
+ };
71
+
5
72
  export type AmbientKnobProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
6
73
  value: number;
7
74
  min?: number;
@@ -9,6 +76,7 @@ export type AmbientKnobProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange">
9
76
  step?: number;
10
77
  label?: string;
11
78
  material?: "matte" | "shiny" | "glass";
79
+ variant?: AmbientKnobVariant;
12
80
  onChange?: (nextValue: number) => void;
13
81
  };
14
82
 
@@ -23,14 +91,17 @@ export function AmbientKnob({
23
91
  step = 1,
24
92
  label,
25
93
  material,
94
+ variant = "dot",
26
95
  onChange,
27
96
  className,
28
97
  ...props
29
98
  }: AmbientKnobProps) {
30
99
  const id = useId();
100
+ const clipId = `amb-knurl-${id.replace(/:/g, "")}`;
31
101
  const draggingRef = useRef(false);
32
102
  const knobRef = useRef<HTMLDivElement>(null);
33
103
  const safeStep = step > 0 ? step : 1;
104
+ const family = VARIANT_FAMILY[variant];
34
105
 
35
106
  const percent = (value - min) / (max - min || 1);
36
107
  const rotation = percent * 270 - 135;
@@ -102,7 +173,7 @@ export function AmbientKnob({
102
173
  <div className={cn("ambx-stack", className)} {...props}>
103
174
  <div
104
175
  ref={knobRef}
105
- className={cn("ambient amb-knob amb-chamfer amb-elevation-2 amb-surface ambx-knob", material && `amb-mat-${material}`)}
176
+ className="amb-knob ambx-knob"
106
177
  role="slider"
107
178
  aria-label={label}
108
179
  aria-valuemin={min}
@@ -127,14 +198,29 @@ export function AmbientKnob({
127
198
  }}
128
199
  onKeyDown={onKeyDown}
129
200
  >
130
- <div className="ambx-knob-rotation" style={{ transform: `rotate(${rotation}deg)` }}>
131
- <div className="amb-knob-indicator">
132
- <div className="amb-knob-grip">
133
- <span className="amb-fader-dot" />
134
- <span className="amb-fader-dot" />
135
- <span className="amb-fader-dot" />
136
- </div>
137
- </div>
201
+ <svg width={0} height={0} style={{ position: "absolute" }} aria-hidden focusable={false}>
202
+ <defs>
203
+ <clipPath id={clipId} clipPathUnits="objectBoundingBox">
204
+ <path d={KNURL_PATHS[family]} />
205
+ </clipPath>
206
+ </defs>
207
+ </svg>
208
+ <span className="amb-knob-body ambient amb-thickness-2 amb-surface" />
209
+ <div
210
+ className={cn(
211
+ "ambx-knob-rotation amb-knob-face",
212
+ family === "flute" && "amb-knob-face-flute",
213
+ family === "fine" && "amb-knob-face-fine",
214
+ variant === "cap" && "amb-knob-face-cap",
215
+ material && `amb-mat-${material}`
216
+ )}
217
+ style={{ transform: `rotate(${rotation}deg)`, clipPath: `url(#${clipId})` }}
218
+ >
219
+ {variant === "dot" ? <span className="amb-knob-indicator-dot" /> : null}
220
+ {variant === "line" ? <span className="amb-knob-indicator-line" /> : null}
221
+ {variant === "flute" ? (
222
+ <span className="amb-knob-indicator-dot amb-knob-indicator-dot-center" />
223
+ ) : null}
138
224
  </div>
139
225
  </div>
140
226
  {label ? (
@@ -91,7 +91,7 @@ export function AmbientSlider({
91
91
  return (
92
92
  <div className={cn("ambx-stack", className)} {...props}>
93
93
  <div
94
- className="amb-slider ambx-slider"
94
+ className="amb-slider amb-groove ambx-slider"
95
95
  ref={trackRef}
96
96
  role="slider"
97
97
  aria-label={label}
@@ -109,15 +109,9 @@ export function AmbientSlider({
109
109
  onKeyDown={onKeyDown}
110
110
  >
111
111
  <div
112
- className={cn("amb-slider-thumb ambient amb-fillet amb-elevation-1 ambx-slider-thumb", material !== "glass" && "amb-surface-concave-h", material && `amb-mat-${material}`)}
112
+ className={cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`)}
113
113
  style={{ left: `${percent}%` }}
114
- >
115
- <div className="amb-slider-grip">
116
- <span className="amb-fader-dot" />
117
- <span className="amb-fader-dot" />
118
- <span className="amb-fader-dot" />
119
- </div>
120
- </div>
114
+ />
121
115
  </div>
122
116
  {label ? (
123
117
  <span id={id} className="ambx-label">
@@ -38,8 +38,8 @@ export function AmbientSwitch({
38
38
  aria-checked={active}
39
39
  aria-labelledby={label ? labelId : props["aria-labelledby"]}
40
40
  className={cn(
41
- "ambient amb-switch amb-chamfer amb-elevation-1 amb-heading-3",
42
- active ? "amb-surface-convex amb-switch-on" : "amb-surface-concave",
41
+ "amb-switch",
42
+ active && "amb-switch-on",
43
43
  "ambx-switch",
44
44
  `ambx-switch-${size}`,
45
45
  className
@@ -58,6 +58,9 @@ export function AmbientSwitch({
58
58
  style={typeof led === "string" ? { "--amb-led-color": led } as React.CSSProperties : undefined}
59
59
  />
60
60
  ) : null}
61
+ <span className="amb-switch-track amb-groove">
62
+ <span className="amb-switch-pill ambient amb-fillet amb-surface-convex" />
63
+ </span>
61
64
  {children}
62
65
  </button>
63
66
  );
package/src/index.ts CHANGED
@@ -5,13 +5,13 @@ export { AmbientPanel } from "./components/AmbientPanel";
5
5
  export type { AmbientPanelProps } from "./components/AmbientPanel";
6
6
 
7
7
  export { AmbientButton } from "./components/AmbientButton";
8
- export type { AmbientButtonProps } from "./components/AmbientButton";
8
+ export type { AmbientButtonProps, AmbientButtonShape } from "./components/AmbientButton";
9
9
 
10
10
  export { AmbientSwitch } from "./components/AmbientSwitch";
11
11
  export type { AmbientSwitchProps, AmbientSwitchSize } from "./components/AmbientSwitch";
12
12
 
13
13
  export { AmbientKnob } from "./components/AmbientKnob";
14
- export type { AmbientKnobProps } from "./components/AmbientKnob";
14
+ export type { AmbientKnobProps, AmbientKnobVariant } from "./components/AmbientKnob";
15
15
 
16
16
  export { AmbientFader } from "./components/AmbientFader";
17
17
  export type { AmbientFaderProps } from "./components/AmbientFader";
package/src/styles.css CHANGED
@@ -35,51 +35,264 @@
35
35
  letter-spacing: var(--ambx-type-tracking);
36
36
  }
37
37
 
38
+ /* Button: a chamfered key cap seated in a clearance well — the referent
39
+ is components/button.py's "flush" style (cap seated into a hole with a
40
+ shadow gap ring around it). The button element IS the well: a shallow
41
+ groove (seat depth ~1.2mm = thickness 0.27) whose lume interior shows
42
+ as the gap ring around the cap. Pressing sinks the cap by the
43
+ referent's 0.7mm travel (thickness 1 -> 0.84): the chamfer bands and
44
+ the swept shadow shrink with it for free. */
38
45
  .amb-button {
39
46
  cursor: pointer;
40
- border: medium solid var(--amb-lume);
41
- background-color: inherit;
42
- padding: calc(var(--ambx-grid) * 2);
47
+ border: none;
48
+ padding: var(--ambx-grid-half); /* the clearance gap ring */
49
+ --amb-thickness: 0.27; /* seat depth of the well */
50
+ background-color: var(--amb-lume);
51
+ border-radius: calc(var(--ambx-grid) * 3 + var(--ambx-grid-half));
52
+ min-width: calc(var(--ambx-grid) * 16);
53
+ }
54
+
55
+ .amb-button-cap {
56
+ display: grid;
57
+ place-items: center;
58
+ width: 100%;
59
+ padding: calc(var(--ambx-grid) * 1.5) calc(var(--ambx-grid) * 2);
43
60
  border-radius: calc(var(--ambx-grid) * 3);
44
61
  text-transform: uppercase;
45
- min-width: calc(var(--ambx-grid) * 16);
46
62
  font-weight: 600;
47
- color: var(--amb-lume);
63
+ color: var(--amb-label);
48
64
  }
49
65
 
50
66
  .amb-button:focus {
51
67
  outline-offset: var(--ambx-grid);
52
68
  }
53
69
 
54
- .amb-button:hover,
55
- .amb-knob:hover {
56
- border-color: var(--amb-highlight-color);
70
+ .amb-button:hover .amb-button-cap {
71
+ color: var(--amb-highlight-color);
72
+ }
73
+
74
+ .amb-button:active .amb-button-cap {
75
+ --amb-thickness: 0.84;
76
+ }
77
+
78
+ /* Button shapes, matching the referent lineup (ambient3d/generate.py):
79
+ the default stadium key above, a round key (superellipse exponent 2 —
80
+ pair with amb-mat-shiny for the machined metal-button look), and a
81
+ squarer, flatter EP-133-style pad (exponent 6: tighter corners and a
82
+ 3.6mm cap instead of the key's 4.5mm — thickness 0.8 — with the same
83
+ 0.7mm press travel). */
84
+ .amb-button.amb-button-round {
85
+ min-width: calc(var(--ambx-grid) * 12);
86
+ aspect-ratio: 1;
87
+ border-radius: 50%;
88
+ }
89
+
90
+ .amb-button-round .amb-button-cap {
91
+ height: 100%;
92
+ padding: var(--ambx-grid);
93
+ border-radius: 50%;
94
+ }
95
+
96
+ .amb-button.amb-button-square {
97
+ min-width: calc(var(--ambx-grid) * 14);
98
+ aspect-ratio: 1;
99
+ border-radius: calc(var(--ambx-grid) * 2);
100
+ }
101
+
102
+ .amb-button-square .amb-button-cap {
103
+ height: 100%;
104
+ padding: var(--ambx-grid);
105
+ border-radius: calc(var(--ambx-grid) * 1.5);
106
+ --amb-thickness: 0.8;
57
107
  }
58
108
 
59
- .amb-button:active {
60
- --amb-elevation: 0;
109
+ .amb-button.amb-button-square:active .amb-button-cap {
110
+ --amb-thickness: 0.64;
61
111
  }
62
112
 
113
+ /* Knob: a knob-scale body (thickness 2 = the referent knob.py's 9mm
114
+ height) resting on the panel. The rotating face carries the knurl — a
115
+ repeating-conic rib pattern, physically the referent's ribs catching
116
+ key light on one flank and shading on the other — under a smooth
117
+ opaque top disc (the OP-1-style cap), with an accent indicator dot.
118
+ The whole face rotates with the value, so the ribs move under the
119
+ fingers like the real thing. */
63
120
  .amb-knob {
64
121
  border-radius: 50%;
65
122
  height: calc(var(--ambx-grid) * 16);
66
123
  width: calc(var(--ambx-grid) * 16);
67
- border: medium solid var(--amb-lume);
68
124
  }
69
125
 
70
- .amb-knob-indicator {
126
+ /* The body is the smooth circle at the tooth-root radius: it carries
127
+ the ambient drop shadow (kept circular — the teeth are too fine to
128
+ read in a penumbra) while the clipped face above it supplies the
129
+ knurled silhouette. */
130
+ .amb-knob-body {
71
131
  position: absolute;
72
- top: 6px;
132
+ inset: calc(var(--ambx-grid) / 2);
133
+ border-radius: 50%;
134
+ }
135
+
136
+ /* The rotating face: clipped to the straight-knurl silhouette (the
137
+ inline SVG clipPath in AmbientKnob), opaque at the knob's surface
138
+ color, with per-tooth flank shading — a lit leading flank and a
139
+ shaded trailing flank every 10deg, phase-aligned with the clip's
140
+ teeth (conic 0 is rotated to the +x axis where the tooth path
141
+ starts) — under the smooth top disc. */
142
+ .amb-knob-face {
143
+ position: absolute;
144
+ inset: 0;
145
+ background-color: hsl(
146
+ var(--amb-light-hue)
147
+ var(--amb-light-saturation)
148
+ calc(
149
+ var(--amb-key-light-intensity) * 31.5% +
150
+ var(--amb-fill-light-intensity) * 26.4% + 43.6%
151
+ )
152
+ );
153
+ background-image:
154
+ radial-gradient(
155
+ circle,
156
+ hsl(
157
+ var(--amb-light-hue)
158
+ var(--amb-light-saturation)
159
+ calc(
160
+ var(--amb-key-light-intensity) * 31.5% +
161
+ var(--amb-fill-light-intensity) * 26.4% + 43.6%
162
+ )
163
+ ) 0 60%,
164
+ transparent 61%
165
+ ),
166
+ repeating-conic-gradient(
167
+ from 90deg,
168
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
169
+ calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.4deg,
170
+ transparent 2.2deg 4.6deg,
171
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
172
+ calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
173
+ * 0.3 + 0.16)) 5deg 6.4deg,
174
+ transparent 7.2deg 10deg
175
+ );
176
+ }
177
+
178
+ /* Fluted face (flute variant — the OP-Z-style referent: 14 broad
179
+ ridges split by narrow grooves, pitch 25.714deg). Flank bands sit on
180
+ the clip's rising (0–2.1deg) and falling (18.5–20.6deg) flanks;
181
+ deeper flutes catch more key light, so both bands run stronger than
182
+ the 36-rib knurl's. */
183
+ .amb-knob-face-flute {
184
+ background-image:
185
+ radial-gradient(
186
+ circle,
187
+ hsl(
188
+ var(--amb-light-hue)
189
+ var(--amb-light-saturation)
190
+ calc(
191
+ var(--amb-key-light-intensity) * 31.5% +
192
+ var(--amb-fill-light-intensity) * 26.4% + 43.6%
193
+ )
194
+ ) 0 60%,
195
+ transparent 61%
196
+ ),
197
+ repeating-conic-gradient(
198
+ from 90deg,
199
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
200
+ calc(var(--amb-key-light-intensity) * 0.45)) 0deg 2.1deg,
201
+ transparent 3.2deg 17.4deg,
202
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
203
+ calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
204
+ * 0.35 + 0.2)) 18.5deg 20.6deg,
205
+ transparent 21.6deg 25.7143deg
206
+ );
207
+ }
208
+
209
+ /* Fine knurl (cap and wheel variants: 48 ribs, pitch 7.5deg) — the
210
+ 36-rib pattern's angles scaled by 3/4, same flank alphas. */
211
+ .amb-knob-face-fine {
212
+ background-image:
213
+ radial-gradient(
214
+ circle,
215
+ hsl(
216
+ var(--amb-light-hue)
217
+ var(--amb-light-saturation)
218
+ calc(
219
+ var(--amb-key-light-intensity) * 31.5% +
220
+ var(--amb-fill-light-intensity) * 26.4% + 43.6%
221
+ )
222
+ ) 0 60%,
223
+ transparent 61%
224
+ ),
225
+ repeating-conic-gradient(
226
+ from 90deg,
227
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
228
+ calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.05deg,
229
+ transparent 1.65deg 3.45deg,
230
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
231
+ calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
232
+ * 0.3 + 0.16)) 3.75deg 4.8deg,
233
+ transparent 5.4deg 7.5deg
234
+ );
235
+ }
236
+
237
+ /* Encoder cap (cap variant — the OP-1-style referent's smooth
238
+ contrasting top disc, 0.35mm proud of the knurl): the accent disc
239
+ replaces the surface-colored center disc over the fine knurl. */
240
+ .amb-knob-face-cap {
241
+ background-image:
242
+ radial-gradient(circle, var(--amb-highlight-color) 0 65%, transparent 66%),
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.35)) 0deg 1.05deg,
247
+ transparent 1.65deg 3.45deg,
248
+ hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
249
+ calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
250
+ * 0.3 + 0.16)) 3.75deg 4.8deg,
251
+ transparent 5.4deg 7.5deg
252
+ );
253
+ }
254
+
255
+ .amb-knob-indicator-dot {
256
+ position: absolute;
257
+ top: 16%;
73
258
  left: 50%;
74
259
  transform: translateX(-50%);
260
+ width: calc(var(--ambx-grid) * 2);
261
+ height: calc(var(--ambx-grid) * 2);
262
+ border-radius: 50%;
263
+ background: var(--amb-highlight-color);
75
264
  }
76
265
 
77
- .amb-knob-grip {
78
- display: flex;
79
- flex-direction: column;
80
- gap: 2px;
266
+ /* Centered dot (flute variant): the OP-Z referent's dot sits in the
267
+ middle of the cap (dot_frac 0.32), not at the rim. */
268
+ .amb-knob-indicator-dot-center {
269
+ top: 50%;
270
+ transform: translate(-50%, -50%);
271
+ width: calc(var(--ambx-grid) * 4.5);
272
+ height: calc(var(--ambx-grid) * 4.5);
81
273
  }
82
274
 
275
+ /* Radial indicator line (line variant): the referent's accent bar,
276
+ center to ~85% of the cap radius, pointing at 12 o'clock. */
277
+ .amb-knob-indicator-line {
278
+ position: absolute;
279
+ top: 12%;
280
+ left: 50%;
281
+ transform: translateX(-50%);
282
+ width: calc(var(--ambx-grid) * 0.75);
283
+ height: 34%;
284
+ border-radius: calc(var(--ambx-grid) * 0.375);
285
+ background: var(--amb-highlight-color);
286
+ }
287
+
288
+ .amb-knob:hover .amb-knob-indicator-dot,
289
+ .amb-knob:hover .amb-knob-indicator-line {
290
+ background: color-mix(in oklab, var(--amb-highlight-color), white 25%);
291
+ }
292
+
293
+ /* Tracks are grounded grooves (.amb-groove supplies the recess: lit-wall
294
+ shadow band + far-wall bounce) with a lume interior — dark in bright
295
+ light, glowing in low light — overriding the groove's neutral floor. */
83
296
  .amb-fader {
84
297
  position: relative;
85
298
  width: var(--ambx-grid);
@@ -88,6 +301,9 @@
88
301
  background-color: var(--amb-lume);
89
302
  }
90
303
 
304
+ /* Fader thumb: the referent (fader.py) is a pill cap on a stem — 7mm
305
+ tall (thickness 1.5) riding 2.2mm above the plate (elevation 0.28) —
306
+ with a single grip line across the top. */
91
307
  .amb-fader-thumb {
92
308
  position: absolute;
93
309
  width: calc(var(--ambx-grid) * 6);
@@ -95,55 +311,41 @@
95
311
  left: 50%;
96
312
  top: 50%;
97
313
  transform: translate(-50%, -50%);
98
- border-radius: calc(var(--ambx-grid) * 3);
314
+ border-radius: calc(var(--ambx-grid) * 2);
99
315
  display: grid;
100
316
  place-items: center;
317
+ --amb-thickness: 1.5;
318
+ --amb-elevation: 0.28;
101
319
  }
102
320
 
103
- .amb-fader-grip {
104
- display: flex;
105
- gap: 2px;
106
- }
107
-
108
- .amb-fader-dot {
109
- width: 3px;
110
- height: 3px;
111
- border-radius: 50%;
112
- background: radial-gradient(
113
- circle at 30% 30%,
114
- color-mix(in oklab, var(--amb-lume), white 30%) 0%,
115
- var(--amb-lume) 55%,
116
- color-mix(in oklab, var(--amb-lume), black 35%) 100%
117
- );
321
+ .amb-fader-gripline {
322
+ width: 70%;
323
+ height: 2px;
324
+ border-radius: 1px;
325
+ background: var(--amb-lume);
118
326
  }
119
327
 
328
+ /* Slider: the referent (slider.py) is a shallow concave channel (1mm
329
+ deep = thickness 0.22), not a through-slot, with a domed disc thumb
330
+ gliding over it. */
120
331
  .amb-slider {
121
332
  position: relative;
122
- height: var(--ambx-grid);
333
+ height: calc(var(--ambx-grid) * 1.5);
123
334
  width: calc(var(--ambx-grid) * 30);
124
335
  border-radius: var(--ambx-grid);
125
336
  background-color: var(--amb-lume);
337
+ --amb-thickness: 0.22;
126
338
  }
127
339
 
128
340
  .amb-slider-thumb {
129
341
  position: absolute;
130
- width: calc(var(--ambx-grid) * 9);
342
+ width: calc(var(--ambx-grid) * 6);
131
343
  height: calc(var(--ambx-grid) * 6);
132
344
  top: 50%;
133
345
  left: 50%;
134
346
  transform: translate(-50%, -50%);
135
- border-radius: calc(var(--ambx-grid) * 3);
136
- display: grid;
137
- place-items: center;
138
- }
139
-
140
- .amb-slider-grip {
141
- display: flex;
142
- flex-direction: column;
143
- gap: 2px;
144
- box-shadow:
145
- inset 0 var(--ambx-grid-quarter) var(--ambx-grid-quarter) rgb(255 255 255 / 0.28),
146
- inset 0 calc(var(--ambx-grid-quarter) * -1) var(--ambx-grid-quarter) rgb(0 0 0 / 0.22);
347
+ border-radius: 50%;
348
+ --amb-thickness: 0.7;
147
349
  }
148
350
 
149
351
  .amb-led {
@@ -164,16 +366,65 @@
164
366
  box-shadow: none;
165
367
  }
166
368
 
369
+ /* Switch: a slide switch like the referent (switch.py) — a pill riding
370
+ in a dark stadium recess (well 1.5mm deep = thickness 0.33; the pill
371
+ stands 2.6mm above the recess floor = thickness 0.58), with an
372
+ optional LED above. The pill slides, nothing presses. */
167
373
  .amb-switch {
168
- position: relative;
169
- width: calc(var(--ambx-grid) * 6);
170
- height: calc(var(--ambx-grid) * 6);
171
- border-radius: 50%;
172
374
  cursor: pointer;
173
- border: medium solid var(--amb-lume);
375
+ border: none;
376
+ padding: 0;
377
+ background: none;
378
+ display: inline-grid;
379
+ justify-items: center;
380
+ gap: var(--ambx-grid);
174
381
  margin: 0;
175
382
  }
176
383
 
384
+ .amb-switch-track {
385
+ position: relative;
386
+ display: block;
387
+ width: var(--ambx-switch-w);
388
+ height: var(--ambx-switch-h);
389
+ border-radius: calc(var(--ambx-switch-h) / 2);
390
+ background-color: var(--amb-lume);
391
+ --amb-thickness: 0.33;
392
+ }
393
+
394
+ .amb-switch-pill {
395
+ position: absolute;
396
+ top: 50%;
397
+ left: 0;
398
+ width: calc(var(--ambx-switch-w) * 0.52);
399
+ height: calc(var(--ambx-switch-h) * 0.78);
400
+ border-radius: calc(var(--ambx-switch-h) * 0.39);
401
+ transform: translate(calc(var(--ambx-switch-h) * 0.11), -50%);
402
+ transition: transform 140ms ease;
403
+ --amb-thickness: 0.58;
404
+ }
405
+
406
+ .amb-switch-on .amb-switch-pill {
407
+ transform: translate(
408
+ calc(var(--ambx-switch-w) - 100% - var(--ambx-switch-h) * 0.11),
409
+ -50%
410
+ );
411
+ }
412
+
413
+ .amb-switch:hover .amb-switch-pill {
414
+ background-color: color-mix(
415
+ in oklab,
416
+ hsl(
417
+ var(--amb-light-hue)
418
+ var(--amb-light-saturation)
419
+ calc(
420
+ var(--amb-key-light-intensity) * 31.5% +
421
+ var(--amb-fill-light-intensity) * 26.4% + 43.6%
422
+ )
423
+ ),
424
+ var(--amb-highlight-color) 12%
425
+ );
426
+ }
427
+
177
428
  .ambx-panel {
178
429
  border-radius: calc(var(--ambx-grid) * 4);
179
430
  padding: calc(var(--ambx-grid) * 4);
@@ -185,29 +436,23 @@
185
436
  }
186
437
 
187
438
  .ambx-switch {
188
- --ambx-switch-size: calc(var(--ambx-grid) * 6);
189
- width: var(--ambx-switch-size);
190
- height: var(--ambx-switch-size);
191
- display: inline-grid;
192
- place-items: center;
193
- padding: 0;
439
+ --ambx-switch-w: calc(var(--ambx-grid) * 12);
440
+ --ambx-switch-h: calc(var(--ambx-grid) * 6);
194
441
  }
195
442
 
196
443
  .ambx-switch-sm {
197
- --ambx-switch-size: calc(var(--ambx-grid) * 5);
444
+ --ambx-switch-w: calc(var(--ambx-grid) * 10);
445
+ --ambx-switch-h: calc(var(--ambx-grid) * 5);
198
446
  }
199
447
 
200
448
  .ambx-switch-md {
201
- --ambx-switch-size: calc(var(--ambx-grid) * 6);
449
+ --ambx-switch-w: calc(var(--ambx-grid) * 12);
450
+ --ambx-switch-h: calc(var(--ambx-grid) * 6);
202
451
  }
203
452
 
204
453
  .ambx-switch-lg {
205
- --ambx-switch-size: calc(var(--ambx-grid) * 8);
206
- }
207
-
208
- .amb-switch-on {
209
- box-shadow:
210
- inset 0 0 0 var(--ambx-grid-quarter) rgb(255 255 255 / 0.25);
454
+ --ambx-switch-w: calc(var(--ambx-grid) * 16);
455
+ --ambx-switch-h: calc(var(--ambx-grid) * 8);
211
456
  }
212
457
 
213
458
  .ambx-stack {