@frontpanel/css 0.2.0 → 0.3.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/components.css CHANGED
@@ -91,6 +91,11 @@
91
91
  overflow: clip;
92
92
  border-radius: 26px;
93
93
  }
94
+
95
+ /* Forced colours drop the chrome: an outline keeps the device's edge. */
96
+ @media (forced-colors: active) {
97
+ outline: 1px solid CanvasText;
98
+ }
94
99
  }
95
100
 
96
101
  /** Molded body plastic the modules sit on. */
@@ -195,6 +200,35 @@
195
200
  &:disabled {
196
201
  opacity: 0.45;
197
202
  }
203
+
204
+ /*
205
+ * Forced colours drop the chrome and its shadows: an outline keeps the button's edge, the system's
206
+ * selected colours show it pressed, and GrayText, not a fade, shows it disabled.
207
+ */
208
+ @media (forced-colors: active) {
209
+ outline: 1px solid ButtonText;
210
+ outline-offset: -1px;
211
+
212
+ &[aria-pressed="true"],
213
+ &[aria-expanded="true"],
214
+ &[aria-current="page"] {
215
+ forced-color-adjust: none;
216
+ background: SelectedItem;
217
+ box-shadow: none;
218
+ color: SelectedItemText;
219
+
220
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
221
+ &:focus-visible {
222
+ outline-color: CanvasText;
223
+ }
224
+ }
225
+
226
+ &:disabled {
227
+ opacity: 1;
228
+ color: GrayText;
229
+ outline-color: GrayText;
230
+ }
231
+ }
198
232
  }
199
233
 
200
234
  /**
@@ -258,6 +292,18 @@
258
292
  &:disabled {
259
293
  opacity: 0.45;
260
294
  }
295
+
296
+ /* Forced colours drop the gel and its shadows: an outline keeps its edge, and GrayText, not a fade, shows it disabled. */
297
+ @media (forced-colors: active) {
298
+ outline: 1px solid ButtonText;
299
+ outline-offset: -1px;
300
+
301
+ &:disabled {
302
+ opacity: 1;
303
+ color: GrayText;
304
+ outline-color: GrayText;
305
+ }
306
+ }
261
307
  }
262
308
 
263
309
  /**
@@ -330,6 +376,34 @@
330
376
  &:disabled {
331
377
  opacity: 0.5;
332
378
  }
379
+
380
+ /*
381
+ * Forced colours drop the plastic and its ring: an outline keeps the cap's edge, the system's
382
+ * selected colours show it selected, and GrayText, not a fade, shows it disabled.
383
+ */
384
+ @media (forced-colors: active) {
385
+ outline: 1px solid ButtonText;
386
+ outline-offset: -1px;
387
+
388
+ &[aria-pressed="true"],
389
+ &[aria-checked="true"] {
390
+ forced-color-adjust: none;
391
+ background: SelectedItem;
392
+ box-shadow: none;
393
+ color: SelectedItemText;
394
+
395
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
396
+ &:focus-visible {
397
+ outline-color: CanvasText;
398
+ }
399
+ }
400
+
401
+ &:disabled {
402
+ opacity: 1;
403
+ color: GrayText;
404
+ outline-color: GrayText;
405
+ }
406
+ }
333
407
  }
334
408
 
335
409
  /** Blue plastic for an fp-cap, with its own label colour. */
@@ -357,14 +431,21 @@
357
431
  }
358
432
 
359
433
  /**
360
- * A control with its silkscreen label under it: a knob, fader or meter and an fp-silk. A length
361
- * set here, such as `--fp-fader-length`, reaches the control inside.
434
+ * A control with its silkscreen label under it: a knob, fader, meter or switch and then an fp-silk.
435
+ * With `data-label-placement="start"` the label sits level with the control, before it in the
436
+ * line. A length set here, such as `--fp-fader-length`, reaches the control inside.
362
437
  */
363
438
  .fp-unit {
364
439
  display: inline-flex;
365
440
  flex-direction: column;
366
441
  align-items: center;
367
442
  gap: 0.375rem;
443
+
444
+ /* Reversed, so the markup stays control then label either way. */
445
+ &[data-label-placement="start"] {
446
+ flex-direction: row-reverse;
447
+ gap: 0.5rem;
448
+ }
368
449
  }
369
450
 
370
451
  /**
@@ -423,6 +504,15 @@
423
504
  opacity: 0.45;
424
505
  cursor: not-allowed;
425
506
  }
507
+
508
+ /* Forced colours drop the knurling, the dome and their shadows: outlines draw the skirt and the cap. */
509
+ @media (forced-colors: active) {
510
+ outline: 1px solid CanvasText;
511
+
512
+ &::before {
513
+ outline: 1px solid CanvasText;
514
+ }
515
+ }
426
516
  }
427
517
 
428
518
  /** The pointer line on a knob's cap. Turn it with `rotate`: -135deg at the minimum, 135deg at the maximum. */
@@ -442,6 +532,14 @@
442
532
  border-radius: 2px;
443
533
  background-color: var(--fp-color-ink);
444
534
  }
535
+
536
+ /* Forced colours would paint the line in the page colour, on the page colour. */
537
+ @media (forced-colors: active) {
538
+ &::after {
539
+ forced-color-adjust: none;
540
+ background-color: CanvasText;
541
+ }
542
+ }
445
543
  }
446
544
 
447
545
  /**
@@ -498,6 +596,13 @@
498
596
  height: 6px;
499
597
  translate: 0 -50%;
500
598
  }
599
+
600
+ /* Forced colours: the slot as a solid line, so the cap's travel shows. */
601
+ @media (forced-colors: active) {
602
+ forced-color-adjust: none;
603
+ background-color: CanvasText;
604
+ box-shadow: none;
605
+ }
501
606
  }
502
607
 
503
608
  /**
@@ -560,6 +665,344 @@
560
665
  outline-offset: 2px;
561
666
  filter: drop-shadow(0 0 3px var(--fp-color-focus));
562
667
  }
668
+
669
+ /* Forced colours: a cap in the page colour hides the slot under it; an outline and the grip draw it. */
670
+ @media (forced-colors: active) {
671
+ background-color: Canvas;
672
+ outline: 1px solid CanvasText;
673
+ outline-offset: -1px;
674
+
675
+ &::after {
676
+ forced-color-adjust: none;
677
+ background-color: CanvasText;
678
+ }
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Illuminated rocker switch: a lamp-lit paddle in a dark bezel, marked I at the top and O at the
684
+ * bottom. Off, the O half is pressed in; on, the I half is, and the lamp behind the lens lights.
685
+ * Put it on a button with `role="switch"` and `aria-checked`, holding an fp-switch-rocker. The
686
+ * lamp is red; colour it with fp-led-green or fp-led-amber. 44 x 56 px, 36 x 46 px with a mouse,
687
+ * trackpad or pen. With `data-orientation="horizontal"` it lies on its side, 56 x 44 px (46 x 36),
688
+ * O on the left and I on the right. After an illuminated rocker pen by ykadosh (CodePen, MIT), rebuilt on the kit's
689
+ * tokens.
690
+ */
691
+ .fp-switch {
692
+ --fp-switch-lamp: var(--fp-led, var(--fp-color-led-red));
693
+ position: relative;
694
+ display: inline-block;
695
+ flex-shrink: 0;
696
+ width: 2.75rem;
697
+ height: 3.5rem;
698
+ padding: 5px;
699
+ border: 0;
700
+ border-radius: 4px;
701
+ background-color: color-mix(in srgb, var(--fp-color-housing) 40%, black);
702
+ box-shadow:
703
+ 0 0 0 1px var(--fp-color-edge),
704
+ inset 1px 1px 0 rgb(255 255 255 / 0.25),
705
+ inset 0 0 1px 3px var(--fp-color-housing),
706
+ inset 0 0 1px 5px color-mix(in srgb, var(--fp-color-housing) 40%, black),
707
+ 1px 2px 0 var(--fp-color-cast);
708
+ perspective: 216px;
709
+ cursor: pointer;
710
+
711
+ @media (pointer: fine) {
712
+ width: 2.25rem;
713
+ height: 2.875rem;
714
+ padding: 4px;
715
+ }
716
+
717
+ /* On: the I half goes in, the shine comes up, the shading goes, and the lamp lights as it lands. */
718
+ &[aria-checked="true"] .fp-switch-rocker {
719
+ transform: translateZ(6px) rotateX(25deg);
720
+ box-shadow: 0 -3px 6px var(--fp-switch-lamp);
721
+ }
722
+
723
+ &[aria-checked="true"] .fp-switch-face::before {
724
+ opacity: 1;
725
+ }
726
+
727
+ &[aria-checked="true"] .fp-switch-face::after {
728
+ opacity: 0;
729
+ }
730
+
731
+ &[aria-checked="true"] .fp-switch-lamp {
732
+ opacity: 1;
733
+ transition-duration: 60ms;
734
+ transition-timing-function: linear;
735
+ transition-delay: 250ms;
736
+ }
737
+
738
+ /*
739
+ * On its side the paddle tips across instead, and the marks stay upright. With the light at the
740
+ * top left, the right end is now the one in shade that casts the shadow, and the left end is lit.
741
+ */
742
+ &[data-orientation="horizontal"] {
743
+ width: 3.5rem;
744
+ height: 2.75rem;
745
+
746
+ @media (pointer: fine) {
747
+ width: 2.875rem;
748
+ height: 2.25rem;
749
+ }
750
+
751
+ & .fp-switch-rocker {
752
+ transform: translateZ(6px) rotateY(-25deg);
753
+ background: linear-gradient(
754
+ 90deg,
755
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 0%,
756
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 30%,
757
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 70%,
758
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 100%
759
+ );
760
+ }
761
+
762
+ /* The right end, in shade, with the shadow the paddle casts into the bezel. */
763
+ & .fp-switch-rocker::before {
764
+ left: auto;
765
+ right: 0;
766
+ width: 15px;
767
+ height: 100%;
768
+ transform-origin: right;
769
+ transform: rotateY(-90deg);
770
+ background: linear-gradient(
771
+ 270deg,
772
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black),
773
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
774
+ );
775
+ box-shadow:
776
+ -15px 0 3px 0 black,
777
+ -24px 0 6px 0 rgb(0 0 0 / 0.5);
778
+ }
779
+
780
+ /* The left end, lit from the left while it stands proud. */
781
+ & .fp-switch-rocker::after {
782
+ top: 0;
783
+ bottom: auto;
784
+ width: 15px;
785
+ height: 100%;
786
+ transform-origin: right;
787
+ transform: translateX(-15px) rotateY(-90deg);
788
+ background:
789
+ linear-gradient(
790
+ 270deg,
791
+ rgb(255 255 255 / 0.8) 10%,
792
+ rgb(255 255 255 / 0.3) 30%,
793
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 75%,
794
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
795
+ )
796
+ 50% 50% / 97% 97% no-repeat,
797
+ color-mix(in srgb, var(--fp-switch-lamp) 70%, black);
798
+ box-shadow: none;
799
+ }
800
+
801
+ & .fp-switch-face {
802
+ background:
803
+ linear-gradient(#fff, #fff) 78% 50% / max(2px, 4%) 28% no-repeat,
804
+ radial-gradient(circle, transparent 50%, #fff 52%, #fff 70%, transparent 72%) 22% 50% / 25% 33% no-repeat,
805
+ radial-gradient(transparent 30%, color-mix(in srgb, color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 70%, transparent) 70%) 0 0 / 3px 3px;
806
+ }
807
+
808
+ & .fp-switch-face::before {
809
+ background:
810
+ linear-gradient(#fff, transparent 3%) 50% 50% / 97% 97% no-repeat,
811
+ linear-gradient(270deg, rgb(255 255 255 / 0.5), transparent 50%, transparent 80%, rgb(255 255 255 / 0.5)) 50% 50% / 97% 97% no-repeat;
812
+ }
813
+
814
+ & .fp-switch-face::after {
815
+ background-image: linear-gradient(270deg, transparent 70%, rgb(0 0 0 / 0.8));
816
+ }
817
+
818
+ &[aria-checked="true"] .fp-switch-rocker {
819
+ transform: translateZ(6px) rotateY(25deg);
820
+ box-shadow: 3px 0 6px var(--fp-switch-lamp);
821
+ }
822
+ }
823
+
824
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
825
+ &:focus-visible {
826
+ outline: 2px solid var(--fp-color-focus-ring);
827
+ outline-offset: 2px;
828
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
829
+ }
830
+
831
+ &:disabled,
832
+ &[aria-disabled="true"] {
833
+ opacity: 0.45;
834
+ cursor: not-allowed;
835
+ }
836
+
837
+ /*
838
+ * Forced colours drop the paddle's paint: an outline keeps the bezel's edge, and the rocker fills
839
+ * when on, as a lit LED does, so the state reads by shape as well as by the tilt.
840
+ */
841
+ @media (forced-colors: active) {
842
+ outline: 1px solid ButtonText;
843
+
844
+ &[data-orientation="horizontal"] .fp-switch-rocker {
845
+ background: Canvas;
846
+ }
847
+
848
+ &[aria-checked="true"] .fp-switch-rocker,
849
+ &[data-orientation="horizontal"][aria-checked="true"] .fp-switch-rocker {
850
+ background: CanvasText;
851
+ box-shadow: none;
852
+ }
853
+
854
+ &:disabled,
855
+ &[aria-disabled="true"] {
856
+ opacity: 1;
857
+ outline-color: GrayText;
858
+ }
859
+ }
860
+ }
861
+
862
+ /**
863
+ * The paddle of an fp-switch, tilted in 3D. It holds an fp-switch-lamp and then an fp-switch-face;
864
+ * its top and bottom ends show as it tips.
865
+ */
866
+ .fp-switch-rocker {
867
+ position: relative;
868
+ display: block;
869
+ width: 100%;
870
+ height: 100%;
871
+ transform-origin: 50% 50% -6px;
872
+ transform: translateZ(6px) rotateX(-25deg);
873
+ transform-style: preserve-3d;
874
+ background: linear-gradient(
875
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 0%,
876
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 30%,
877
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 70%,
878
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 100%
879
+ );
880
+
881
+ /* Snaps over at the end of its travel, as a rocker's spring does. */
882
+ transition: transform 300ms cubic-bezier(1, 0, 1, 1), box-shadow 300ms cubic-bezier(1, 0, 1, 1);
883
+
884
+ @media (prefers-reduced-motion: reduce) {
885
+ transition-duration: 0ms;
886
+ }
887
+
888
+ /* The top end, lit from above while it stands proud. */
889
+ &::before {
890
+ content: "";
891
+ position: absolute;
892
+ top: 0;
893
+ left: 0;
894
+ width: 100%;
895
+ height: 15px;
896
+ transform-origin: top;
897
+ transform: rotateX(-90deg);
898
+ background:
899
+ linear-gradient(
900
+ rgb(255 255 255 / 0.8) 10%,
901
+ rgb(255 255 255 / 0.3) 30%,
902
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 75%,
903
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
904
+ )
905
+ 50% 50% / 97% 97% no-repeat,
906
+ color-mix(in srgb, var(--fp-switch-lamp) 70%, black);
907
+ }
908
+
909
+ /* The bottom end, in shadow, with the shadow the paddle casts into the bezel. */
910
+ &::after {
911
+ content: "";
912
+ position: absolute;
913
+ bottom: 0;
914
+ left: 0;
915
+ width: 100%;
916
+ height: 15px;
917
+ transform-origin: top;
918
+ transform: translateY(15px) rotateX(-90deg);
919
+ background-image: linear-gradient(
920
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black),
921
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
922
+ );
923
+ box-shadow:
924
+ 0 15px 3px 0 black,
925
+ 0 24px 6px 0 rgb(0 0 0 / 0.5);
926
+ }
927
+
928
+ /* Forced colours: a plain paddle in the system's colours; fp-switch fills it when on. */
929
+ @media (forced-colors: active) {
930
+ forced-color-adjust: none;
931
+ background: Canvas;
932
+ outline: 1px solid CanvasText;
933
+ outline-offset: -1px;
934
+
935
+ &::before,
936
+ &::after {
937
+ content: none;
938
+ }
939
+ }
940
+ }
941
+
942
+ /** The lamp behind an fp-switch's lens: lit when the switch is on, and slow to fade, like a filament cooling. */
943
+ .fp-switch-lamp {
944
+ position: absolute;
945
+ inset: 0;
946
+ opacity: 0;
947
+ background-image: radial-gradient(
948
+ color-mix(in srgb, var(--fp-switch-lamp) 35%, #fff2c8),
949
+ var(--fp-switch-lamp) 40%,
950
+ transparent 70%
951
+ );
952
+ transition: opacity 600ms ease-out;
953
+
954
+ @media (prefers-reduced-motion: reduce) {
955
+ transition-duration: 0ms;
956
+ }
957
+
958
+ /* Forced colours: the rocker's fill shows the state instead. */
959
+ @media (forced-colors: active) {
960
+ display: none;
961
+ }
962
+ }
963
+
964
+ /**
965
+ * The front of an fp-switch's paddle: the dotted lens over the lamp, the I and O marks, and the
966
+ * shine that comes up on the pressed-in half. Decorative; the switch says its state.
967
+ */
968
+ .fp-switch-face {
969
+ position: absolute;
970
+ inset: 0;
971
+ background:
972
+ linear-gradient(#fff, #fff) 50% 20% / max(2px, 5%) 20% no-repeat,
973
+ radial-gradient(circle, transparent 50%, #fff 52%, #fff 70%, transparent 72%) 50% 80% / 33% 25% no-repeat,
974
+ radial-gradient(transparent 30%, color-mix(in srgb, color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 70%, transparent) 70%) 0 0 / 3px 3px;
975
+
976
+ /* The shine: faint while the I half stands out, full once it is pressed in. */
977
+ &::before,
978
+ &::after {
979
+ content: "";
980
+ position: absolute;
981
+ inset: 0;
982
+ transition: opacity 300ms cubic-bezier(1, 0, 1, 1);
983
+
984
+ @media (prefers-reduced-motion: reduce) {
985
+ transition-duration: 0ms;
986
+ }
987
+ }
988
+
989
+ &::before {
990
+ opacity: 0.3;
991
+ background:
992
+ linear-gradient(#fff, transparent 3%) 50% 50% / 97% 97% no-repeat,
993
+ linear-gradient(rgb(255 255 255 / 0.5), transparent 50%, transparent 80%, rgb(255 255 255 / 0.5)) 50% 50% / 97% 97% no-repeat;
994
+ }
995
+
996
+ /* The shading on the O half while it is pressed in. */
997
+ &::after {
998
+ opacity: 1;
999
+ background-image: linear-gradient(transparent 70%, rgb(0 0 0 / 0.8));
1000
+ }
1001
+
1002
+ /* Forced colours: no lens or marks; the rocker's fill shows the state. */
1003
+ @media (forced-colors: active) {
1004
+ display: none;
1005
+ }
563
1006
  }
564
1007
 
565
1008
  /**
@@ -867,6 +1310,11 @@
867
1310
  inset 1px 1px 0 var(--fp-color-bevel-hi),
868
1311
  inset -1px -1px 0 var(--fp-color-bevel-lo),
869
1312
  2px 3px 0 var(--fp-color-cast);
1313
+
1314
+ /* Forced colours drop the shadow that draws its edge: an outline keeps it. */
1315
+ @media (forced-colors: active) {
1316
+ outline: 1px solid CanvasText;
1317
+ }
870
1318
  }
871
1319
 
872
1320
  /** A module's own little title bar, in brushed chrome. */
@@ -929,6 +1377,11 @@
929
1377
  box-shadow:
930
1378
  inset 1px 1px 2px var(--fp-color-bevel-lo),
931
1379
  inset -1px -1px 0 var(--fp-color-bevel-hi);
1380
+
1381
+ /* Forced colours drop the shading that recesses it: an outline keeps an empty well in view. */
1382
+ @media (forced-colors: active) {
1383
+ outline: 1px solid CanvasText;
1384
+ }
932
1385
  }
933
1386
 
934
1387
  /** Silkscreened hardware label. */
@@ -951,6 +1404,11 @@
951
1404
  0 0 0 2px var(--fp-color-chrome-800),
952
1405
  inset 2px 2px 4px rgb(0 0 0 / 0.35),
953
1406
  inset -1px -1px 0 rgb(255 255 255 / 0.2);
1407
+
1408
+ /* Forced colours drop the chrome bezel: an outline frames the glass. */
1409
+ @media (forced-colors: active) {
1410
+ outline: 2px solid CanvasText;
1411
+ }
954
1412
  }
955
1413
 
956
1414
  /** One line of LCD text. Cells of fp-lcd-ghost stacked behind it show the unlit segments, as on real glass. */
@@ -968,6 +1426,11 @@
968
1426
  /** The unlit segments behind an LCD line. Decorative: give it aria-hidden="true". */
969
1427
  .fp-lcd-ghost {
970
1428
  color: var(--fp-color-lcd-ghost);
1429
+
1430
+ /* Forced colours would draw the unlit segments as solid as the text over them. */
1431
+ @media (forced-colors: active) {
1432
+ visibility: hidden;
1433
+ }
971
1434
  }
972
1435
 
973
1436
  /** Dark housing that LEDs and small readouts sit in. */
@@ -984,6 +1447,11 @@
984
1447
  box-shadow:
985
1448
  inset 1px 1px 2px rgb(0 0 0 / 0.5),
986
1449
  0 1px 0 var(--fp-color-bevel-hi);
1450
+
1451
+ /* Forced colours turn the dark housing into the page colour: an outline keeps it in view. */
1452
+ @media (forced-colors: active) {
1453
+ outline: 1px solid CanvasText;
1454
+ }
987
1455
  }
988
1456
 
989
1457
  /** An LED with its own dark bezel ring, so a lit one reads on light chrome too. Unlit by default. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frontpanel/css",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Early-2000s hardware skin as plain CSS: liquid-chrome bezels, gel buttons, LCDs and LEDs. Tokens, classes and an optional Tailwind 4 entry.",
6
6
  "author": "Dulan Katukurundage",
7
7
  "license": "MIT",
package/page.css CHANGED
@@ -70,6 +70,11 @@
70
70
  0 0 0 1px var(--fp-color-edge),
71
71
  inset 1px 1px 0 var(--fp-color-bevel-hi),
72
72
  0 -10px 28px var(--fp-color-cast);
73
+
74
+ /* Forced colours drop the shadow that lifts it off the device: an outline keeps its edge. */
75
+ @media (forced-colors: active) {
76
+ outline: 1px solid CanvasText;
77
+ }
73
78
  }
74
79
 
75
80
  /** Inset text field or select on a matte panel. Its border keeps 3:1 against the panel in both skins. */
package/tailwind-page.css CHANGED
@@ -71,6 +71,11 @@
71
71
  0 0 0 1px var(--fp-color-edge),
72
72
  inset 1px 1px 0 var(--fp-color-bevel-hi),
73
73
  0 -10px 28px var(--fp-color-cast);
74
+
75
+ /* Forced colours drop the shadow that lifts it off the device: an outline keeps its edge. */
76
+ @media (forced-colors: active) {
77
+ outline: 1px solid CanvasText;
78
+ }
74
79
  }
75
80
 
76
81
  /** Inset text field or select on a matte panel. Its border keeps 3:1 against the panel in both skins. */
package/tailwind.css CHANGED
@@ -150,6 +150,11 @@
150
150
  overflow: clip;
151
151
  border-radius: 26px;
152
152
  }
153
+
154
+ /* Forced colours drop the chrome: an outline keeps the device's edge. */
155
+ @media (forced-colors: active) {
156
+ outline: 1px solid CanvasText;
157
+ }
153
158
  }
154
159
 
155
160
  /** Molded body plastic the modules sit on. */
@@ -254,6 +259,35 @@
254
259
  &:disabled {
255
260
  opacity: 0.45;
256
261
  }
262
+
263
+ /*
264
+ * Forced colours drop the chrome and its shadows: an outline keeps the button's edge, the system's
265
+ * selected colours show it pressed, and GrayText, not a fade, shows it disabled.
266
+ */
267
+ @media (forced-colors: active) {
268
+ outline: 1px solid ButtonText;
269
+ outline-offset: -1px;
270
+
271
+ &[aria-pressed="true"],
272
+ &[aria-expanded="true"],
273
+ &[aria-current="page"] {
274
+ forced-color-adjust: none;
275
+ background: SelectedItem;
276
+ box-shadow: none;
277
+ color: SelectedItemText;
278
+
279
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
280
+ &:focus-visible {
281
+ outline-color: CanvasText;
282
+ }
283
+ }
284
+
285
+ &:disabled {
286
+ opacity: 1;
287
+ color: GrayText;
288
+ outline-color: GrayText;
289
+ }
290
+ }
257
291
  }
258
292
 
259
293
  /**
@@ -317,6 +351,18 @@
317
351
  &:disabled {
318
352
  opacity: 0.45;
319
353
  }
354
+
355
+ /* Forced colours drop the gel and its shadows: an outline keeps its edge, and GrayText, not a fade, shows it disabled. */
356
+ @media (forced-colors: active) {
357
+ outline: 1px solid ButtonText;
358
+ outline-offset: -1px;
359
+
360
+ &:disabled {
361
+ opacity: 1;
362
+ color: GrayText;
363
+ outline-color: GrayText;
364
+ }
365
+ }
320
366
  }
321
367
 
322
368
  /**
@@ -389,6 +435,34 @@
389
435
  &:disabled {
390
436
  opacity: 0.5;
391
437
  }
438
+
439
+ /*
440
+ * Forced colours drop the plastic and its ring: an outline keeps the cap's edge, the system's
441
+ * selected colours show it selected, and GrayText, not a fade, shows it disabled.
442
+ */
443
+ @media (forced-colors: active) {
444
+ outline: 1px solid ButtonText;
445
+ outline-offset: -1px;
446
+
447
+ &[aria-pressed="true"],
448
+ &[aria-checked="true"] {
449
+ forced-color-adjust: none;
450
+ background: SelectedItem;
451
+ box-shadow: none;
452
+ color: SelectedItemText;
453
+
454
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
455
+ &:focus-visible {
456
+ outline-color: CanvasText;
457
+ }
458
+ }
459
+
460
+ &:disabled {
461
+ opacity: 1;
462
+ color: GrayText;
463
+ outline-color: GrayText;
464
+ }
465
+ }
392
466
  }
393
467
 
394
468
  /** Blue plastic for an fp-cap, with its own label colour. */
@@ -416,14 +490,21 @@
416
490
  }
417
491
 
418
492
  /**
419
- * A control with its silkscreen label under it: a knob, fader or meter and an fp-silk. A length
420
- * set here, such as `--fp-fader-length`, reaches the control inside.
493
+ * A control with its silkscreen label under it: a knob, fader, meter or switch and then an fp-silk.
494
+ * With `data-label-placement="start"` the label sits level with the control, before it in the
495
+ * line. A length set here, such as `--fp-fader-length`, reaches the control inside.
421
496
  */
422
497
  @utility fp-unit {
423
498
  display: inline-flex;
424
499
  flex-direction: column;
425
500
  align-items: center;
426
501
  gap: 0.375rem;
502
+
503
+ /* Reversed, so the markup stays control then label either way. */
504
+ &[data-label-placement="start"] {
505
+ flex-direction: row-reverse;
506
+ gap: 0.5rem;
507
+ }
427
508
  }
428
509
 
429
510
  /**
@@ -482,6 +563,15 @@
482
563
  opacity: 0.45;
483
564
  cursor: not-allowed;
484
565
  }
566
+
567
+ /* Forced colours drop the knurling, the dome and their shadows: outlines draw the skirt and the cap. */
568
+ @media (forced-colors: active) {
569
+ outline: 1px solid CanvasText;
570
+
571
+ &::before {
572
+ outline: 1px solid CanvasText;
573
+ }
574
+ }
485
575
  }
486
576
 
487
577
  /** The pointer line on a knob's cap. Turn it with `rotate`: -135deg at the minimum, 135deg at the maximum. */
@@ -501,6 +591,14 @@
501
591
  border-radius: 2px;
502
592
  background-color: var(--fp-color-ink);
503
593
  }
594
+
595
+ /* Forced colours would paint the line in the page colour, on the page colour. */
596
+ @media (forced-colors: active) {
597
+ &::after {
598
+ forced-color-adjust: none;
599
+ background-color: CanvasText;
600
+ }
601
+ }
504
602
  }
505
603
 
506
604
  /**
@@ -557,6 +655,13 @@
557
655
  height: 6px;
558
656
  translate: 0 -50%;
559
657
  }
658
+
659
+ /* Forced colours: the slot as a solid line, so the cap's travel shows. */
660
+ @media (forced-colors: active) {
661
+ forced-color-adjust: none;
662
+ background-color: CanvasText;
663
+ box-shadow: none;
664
+ }
560
665
  }
561
666
 
562
667
  /**
@@ -619,6 +724,344 @@
619
724
  outline-offset: 2px;
620
725
  filter: drop-shadow(0 0 3px var(--fp-color-focus));
621
726
  }
727
+
728
+ /* Forced colours: a cap in the page colour hides the slot under it; an outline and the grip draw it. */
729
+ @media (forced-colors: active) {
730
+ background-color: Canvas;
731
+ outline: 1px solid CanvasText;
732
+ outline-offset: -1px;
733
+
734
+ &::after {
735
+ forced-color-adjust: none;
736
+ background-color: CanvasText;
737
+ }
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Illuminated rocker switch: a lamp-lit paddle in a dark bezel, marked I at the top and O at the
743
+ * bottom. Off, the O half is pressed in; on, the I half is, and the lamp behind the lens lights.
744
+ * Put it on a button with `role="switch"` and `aria-checked`, holding an fp-switch-rocker. The
745
+ * lamp is red; colour it with fp-led-green or fp-led-amber. 44 x 56 px, 36 x 46 px with a mouse,
746
+ * trackpad or pen. With `data-orientation="horizontal"` it lies on its side, 56 x 44 px (46 x 36),
747
+ * O on the left and I on the right. After an illuminated rocker pen by ykadosh (CodePen, MIT), rebuilt on the kit's
748
+ * tokens.
749
+ */
750
+ @utility fp-switch {
751
+ --fp-switch-lamp: var(--fp-led, var(--fp-color-led-red));
752
+ position: relative;
753
+ display: inline-block;
754
+ flex-shrink: 0;
755
+ width: 2.75rem;
756
+ height: 3.5rem;
757
+ padding: 5px;
758
+ border: 0;
759
+ border-radius: 4px;
760
+ background-color: color-mix(in srgb, var(--fp-color-housing) 40%, black);
761
+ box-shadow:
762
+ 0 0 0 1px var(--fp-color-edge),
763
+ inset 1px 1px 0 rgb(255 255 255 / 0.25),
764
+ inset 0 0 1px 3px var(--fp-color-housing),
765
+ inset 0 0 1px 5px color-mix(in srgb, var(--fp-color-housing) 40%, black),
766
+ 1px 2px 0 var(--fp-color-cast);
767
+ perspective: 216px;
768
+ cursor: pointer;
769
+
770
+ @media (pointer: fine) {
771
+ width: 2.25rem;
772
+ height: 2.875rem;
773
+ padding: 4px;
774
+ }
775
+
776
+ /* On: the I half goes in, the shine comes up, the shading goes, and the lamp lights as it lands. */
777
+ &[aria-checked="true"] .fp-switch-rocker {
778
+ transform: translateZ(6px) rotateX(25deg);
779
+ box-shadow: 0 -3px 6px var(--fp-switch-lamp);
780
+ }
781
+
782
+ &[aria-checked="true"] .fp-switch-face::before {
783
+ opacity: 1;
784
+ }
785
+
786
+ &[aria-checked="true"] .fp-switch-face::after {
787
+ opacity: 0;
788
+ }
789
+
790
+ &[aria-checked="true"] .fp-switch-lamp {
791
+ opacity: 1;
792
+ transition-duration: 60ms;
793
+ transition-timing-function: linear;
794
+ transition-delay: 250ms;
795
+ }
796
+
797
+ /*
798
+ * On its side the paddle tips across instead, and the marks stay upright. With the light at the
799
+ * top left, the right end is now the one in shade that casts the shadow, and the left end is lit.
800
+ */
801
+ &[data-orientation="horizontal"] {
802
+ width: 3.5rem;
803
+ height: 2.75rem;
804
+
805
+ @media (pointer: fine) {
806
+ width: 2.875rem;
807
+ height: 2.25rem;
808
+ }
809
+
810
+ & .fp-switch-rocker {
811
+ transform: translateZ(6px) rotateY(-25deg);
812
+ background: linear-gradient(
813
+ 90deg,
814
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 0%,
815
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 30%,
816
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 70%,
817
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 100%
818
+ );
819
+ }
820
+
821
+ /* The right end, in shade, with the shadow the paddle casts into the bezel. */
822
+ & .fp-switch-rocker::before {
823
+ left: auto;
824
+ right: 0;
825
+ width: 15px;
826
+ height: 100%;
827
+ transform-origin: right;
828
+ transform: rotateY(-90deg);
829
+ background: linear-gradient(
830
+ 270deg,
831
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black),
832
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
833
+ );
834
+ box-shadow:
835
+ -15px 0 3px 0 black,
836
+ -24px 0 6px 0 rgb(0 0 0 / 0.5);
837
+ }
838
+
839
+ /* The left end, lit from the left while it stands proud. */
840
+ & .fp-switch-rocker::after {
841
+ top: 0;
842
+ bottom: auto;
843
+ width: 15px;
844
+ height: 100%;
845
+ transform-origin: right;
846
+ transform: translateX(-15px) rotateY(-90deg);
847
+ background:
848
+ linear-gradient(
849
+ 270deg,
850
+ rgb(255 255 255 / 0.8) 10%,
851
+ rgb(255 255 255 / 0.3) 30%,
852
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 75%,
853
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
854
+ )
855
+ 50% 50% / 97% 97% no-repeat,
856
+ color-mix(in srgb, var(--fp-switch-lamp) 70%, black);
857
+ box-shadow: none;
858
+ }
859
+
860
+ & .fp-switch-face {
861
+ background:
862
+ linear-gradient(#fff, #fff) 78% 50% / max(2px, 4%) 28% no-repeat,
863
+ radial-gradient(circle, transparent 50%, #fff 52%, #fff 70%, transparent 72%) 22% 50% / 25% 33% no-repeat,
864
+ radial-gradient(transparent 30%, color-mix(in srgb, color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 70%, transparent) 70%) 0 0 / 3px 3px;
865
+ }
866
+
867
+ & .fp-switch-face::before {
868
+ background:
869
+ linear-gradient(#fff, transparent 3%) 50% 50% / 97% 97% no-repeat,
870
+ linear-gradient(270deg, rgb(255 255 255 / 0.5), transparent 50%, transparent 80%, rgb(255 255 255 / 0.5)) 50% 50% / 97% 97% no-repeat;
871
+ }
872
+
873
+ & .fp-switch-face::after {
874
+ background-image: linear-gradient(270deg, transparent 70%, rgb(0 0 0 / 0.8));
875
+ }
876
+
877
+ &[aria-checked="true"] .fp-switch-rocker {
878
+ transform: translateZ(6px) rotateY(25deg);
879
+ box-shadow: 3px 0 6px var(--fp-switch-lamp);
880
+ }
881
+ }
882
+
883
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
884
+ &:focus-visible {
885
+ outline: 2px solid var(--fp-color-focus-ring);
886
+ outline-offset: 2px;
887
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
888
+ }
889
+
890
+ &:disabled,
891
+ &[aria-disabled="true"] {
892
+ opacity: 0.45;
893
+ cursor: not-allowed;
894
+ }
895
+
896
+ /*
897
+ * Forced colours drop the paddle's paint: an outline keeps the bezel's edge, and the rocker fills
898
+ * when on, as a lit LED does, so the state reads by shape as well as by the tilt.
899
+ */
900
+ @media (forced-colors: active) {
901
+ outline: 1px solid ButtonText;
902
+
903
+ &[data-orientation="horizontal"] .fp-switch-rocker {
904
+ background: Canvas;
905
+ }
906
+
907
+ &[aria-checked="true"] .fp-switch-rocker,
908
+ &[data-orientation="horizontal"][aria-checked="true"] .fp-switch-rocker {
909
+ background: CanvasText;
910
+ box-shadow: none;
911
+ }
912
+
913
+ &:disabled,
914
+ &[aria-disabled="true"] {
915
+ opacity: 1;
916
+ outline-color: GrayText;
917
+ }
918
+ }
919
+ }
920
+
921
+ /**
922
+ * The paddle of an fp-switch, tilted in 3D. It holds an fp-switch-lamp and then an fp-switch-face;
923
+ * its top and bottom ends show as it tips.
924
+ */
925
+ @utility fp-switch-rocker {
926
+ position: relative;
927
+ display: block;
928
+ width: 100%;
929
+ height: 100%;
930
+ transform-origin: 50% 50% -6px;
931
+ transform: translateZ(6px) rotateX(-25deg);
932
+ transform-style: preserve-3d;
933
+ background: linear-gradient(
934
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 0%,
935
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 30%,
936
+ color-mix(in srgb, var(--fp-switch-lamp) 44%, black) 70%,
937
+ color-mix(in srgb, var(--fp-switch-lamp) 60%, black) 100%
938
+ );
939
+
940
+ /* Snaps over at the end of its travel, as a rocker's spring does. */
941
+ transition: transform 300ms cubic-bezier(1, 0, 1, 1), box-shadow 300ms cubic-bezier(1, 0, 1, 1);
942
+
943
+ @media (prefers-reduced-motion: reduce) {
944
+ transition-duration: 0ms;
945
+ }
946
+
947
+ /* The top end, lit from above while it stands proud. */
948
+ &::before {
949
+ content: "";
950
+ position: absolute;
951
+ top: 0;
952
+ left: 0;
953
+ width: 100%;
954
+ height: 15px;
955
+ transform-origin: top;
956
+ transform: rotateX(-90deg);
957
+ background:
958
+ linear-gradient(
959
+ rgb(255 255 255 / 0.8) 10%,
960
+ rgb(255 255 255 / 0.3) 30%,
961
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 75%,
962
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
963
+ )
964
+ 50% 50% / 97% 97% no-repeat,
965
+ color-mix(in srgb, var(--fp-switch-lamp) 70%, black);
966
+ }
967
+
968
+ /* The bottom end, in shadow, with the shadow the paddle casts into the bezel. */
969
+ &::after {
970
+ content: "";
971
+ position: absolute;
972
+ bottom: 0;
973
+ left: 0;
974
+ width: 100%;
975
+ height: 15px;
976
+ transform-origin: top;
977
+ transform: translateY(15px) rotateX(-90deg);
978
+ background-image: linear-gradient(
979
+ color-mix(in srgb, var(--fp-switch-lamp) 40%, black),
980
+ color-mix(in srgb, var(--fp-switch-lamp) 20%, black)
981
+ );
982
+ box-shadow:
983
+ 0 15px 3px 0 black,
984
+ 0 24px 6px 0 rgb(0 0 0 / 0.5);
985
+ }
986
+
987
+ /* Forced colours: a plain paddle in the system's colours; fp-switch fills it when on. */
988
+ @media (forced-colors: active) {
989
+ forced-color-adjust: none;
990
+ background: Canvas;
991
+ outline: 1px solid CanvasText;
992
+ outline-offset: -1px;
993
+
994
+ &::before,
995
+ &::after {
996
+ content: none;
997
+ }
998
+ }
999
+ }
1000
+
1001
+ /** The lamp behind an fp-switch's lens: lit when the switch is on, and slow to fade, like a filament cooling. */
1002
+ @utility fp-switch-lamp {
1003
+ position: absolute;
1004
+ inset: 0;
1005
+ opacity: 0;
1006
+ background-image: radial-gradient(
1007
+ color-mix(in srgb, var(--fp-switch-lamp) 35%, #fff2c8),
1008
+ var(--fp-switch-lamp) 40%,
1009
+ transparent 70%
1010
+ );
1011
+ transition: opacity 600ms ease-out;
1012
+
1013
+ @media (prefers-reduced-motion: reduce) {
1014
+ transition-duration: 0ms;
1015
+ }
1016
+
1017
+ /* Forced colours: the rocker's fill shows the state instead. */
1018
+ @media (forced-colors: active) {
1019
+ display: none;
1020
+ }
1021
+ }
1022
+
1023
+ /**
1024
+ * The front of an fp-switch's paddle: the dotted lens over the lamp, the I and O marks, and the
1025
+ * shine that comes up on the pressed-in half. Decorative; the switch says its state.
1026
+ */
1027
+ @utility fp-switch-face {
1028
+ position: absolute;
1029
+ inset: 0;
1030
+ background:
1031
+ linear-gradient(#fff, #fff) 50% 20% / max(2px, 5%) 20% no-repeat,
1032
+ radial-gradient(circle, transparent 50%, #fff 52%, #fff 70%, transparent 72%) 50% 80% / 33% 25% no-repeat,
1033
+ radial-gradient(transparent 30%, color-mix(in srgb, color-mix(in srgb, var(--fp-switch-lamp) 40%, black) 70%, transparent) 70%) 0 0 / 3px 3px;
1034
+
1035
+ /* The shine: faint while the I half stands out, full once it is pressed in. */
1036
+ &::before,
1037
+ &::after {
1038
+ content: "";
1039
+ position: absolute;
1040
+ inset: 0;
1041
+ transition: opacity 300ms cubic-bezier(1, 0, 1, 1);
1042
+
1043
+ @media (prefers-reduced-motion: reduce) {
1044
+ transition-duration: 0ms;
1045
+ }
1046
+ }
1047
+
1048
+ &::before {
1049
+ opacity: 0.3;
1050
+ background:
1051
+ linear-gradient(#fff, transparent 3%) 50% 50% / 97% 97% no-repeat,
1052
+ linear-gradient(rgb(255 255 255 / 0.5), transparent 50%, transparent 80%, rgb(255 255 255 / 0.5)) 50% 50% / 97% 97% no-repeat;
1053
+ }
1054
+
1055
+ /* The shading on the O half while it is pressed in. */
1056
+ &::after {
1057
+ opacity: 1;
1058
+ background-image: linear-gradient(transparent 70%, rgb(0 0 0 / 0.8));
1059
+ }
1060
+
1061
+ /* Forced colours: no lens or marks; the rocker's fill shows the state. */
1062
+ @media (forced-colors: active) {
1063
+ display: none;
1064
+ }
622
1065
  }
623
1066
 
624
1067
  /**
@@ -926,6 +1369,11 @@
926
1369
  inset 1px 1px 0 var(--fp-color-bevel-hi),
927
1370
  inset -1px -1px 0 var(--fp-color-bevel-lo),
928
1371
  2px 3px 0 var(--fp-color-cast);
1372
+
1373
+ /* Forced colours drop the shadow that draws its edge: an outline keeps it. */
1374
+ @media (forced-colors: active) {
1375
+ outline: 1px solid CanvasText;
1376
+ }
929
1377
  }
930
1378
 
931
1379
  /** A module's own little title bar, in brushed chrome. */
@@ -988,6 +1436,11 @@
988
1436
  box-shadow:
989
1437
  inset 1px 1px 2px var(--fp-color-bevel-lo),
990
1438
  inset -1px -1px 0 var(--fp-color-bevel-hi);
1439
+
1440
+ /* Forced colours drop the shading that recesses it: an outline keeps an empty well in view. */
1441
+ @media (forced-colors: active) {
1442
+ outline: 1px solid CanvasText;
1443
+ }
991
1444
  }
992
1445
 
993
1446
  /** Silkscreened hardware label. */
@@ -1010,6 +1463,11 @@
1010
1463
  0 0 0 2px var(--fp-color-chrome-800),
1011
1464
  inset 2px 2px 4px rgb(0 0 0 / 0.35),
1012
1465
  inset -1px -1px 0 rgb(255 255 255 / 0.2);
1466
+
1467
+ /* Forced colours drop the chrome bezel: an outline frames the glass. */
1468
+ @media (forced-colors: active) {
1469
+ outline: 2px solid CanvasText;
1470
+ }
1013
1471
  }
1014
1472
 
1015
1473
  /** One line of LCD text. Cells of fp-lcd-ghost stacked behind it show the unlit segments, as on real glass. */
@@ -1027,6 +1485,11 @@
1027
1485
  /** The unlit segments behind an LCD line. Decorative: give it aria-hidden="true". */
1028
1486
  @utility fp-lcd-ghost {
1029
1487
  color: var(--fp-color-lcd-ghost);
1488
+
1489
+ /* Forced colours would draw the unlit segments as solid as the text over them. */
1490
+ @media (forced-colors: active) {
1491
+ visibility: hidden;
1492
+ }
1030
1493
  }
1031
1494
 
1032
1495
  /** Dark housing that LEDs and small readouts sit in. */
@@ -1043,6 +1506,11 @@
1043
1506
  box-shadow:
1044
1507
  inset 1px 1px 2px rgb(0 0 0 / 0.5),
1045
1508
  0 1px 0 var(--fp-color-bevel-hi);
1509
+
1510
+ /* Forced colours turn the dark housing into the page colour: an outline keeps it in view. */
1511
+ @media (forced-colors: active) {
1512
+ outline: 1px solid CanvasText;
1513
+ }
1046
1514
  }
1047
1515
 
1048
1516
  /** An LED with its own dark bezel ring, so a lit one reads on light chrome too. Unlit by default. */