@frontpanel/css 0.1.0 → 0.2.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/base.css CHANGED
@@ -12,21 +12,13 @@
12
12
  */
13
13
  /* The page wears the skin. Native controls and scrollbars follow it, forced or not. */
14
14
  :root {
15
- color-scheme: light dark;
15
+ color-scheme: var(--fp-scheme);
16
16
  background-color: var(--fp-color-backdrop);
17
17
  color: var(--fp-color-ink);
18
18
  font-family: var(--fp-font-ui);
19
19
  -webkit-text-size-adjust: 100%;
20
20
  }
21
21
 
22
- :root[data-fp-skin="silver"] {
23
- color-scheme: light;
24
- }
25
-
26
- :root[data-fp-skin="gunmetal"] {
27
- color-scheme: dark;
28
- }
29
-
30
22
  body {
31
23
  margin: 0;
32
24
  min-height: 100dvh;
package/components.css CHANGED
@@ -49,6 +49,7 @@
49
49
 
50
50
  /** The page behind the device: quiet blue-grey with a faint Y2K grid. */
51
51
  .fp-device-backdrop {
52
+ color-scheme: var(--fp-scheme);
52
53
  background-color: var(--fp-color-backdrop);
53
54
  background-image:
54
55
  linear-gradient(var(--fp-color-grid) 1px, transparent 1px),
@@ -64,6 +65,7 @@
64
65
 
65
66
  /** Liquid-chrome ring around the device body. Diagonal so the reflection band crosses the corners. */
66
67
  .fp-device-rim {
68
+ color-scheme: var(--fp-scheme);
67
69
  border-radius: 36px;
68
70
  padding: 10px;
69
71
  background-image: linear-gradient(
@@ -93,6 +95,7 @@
93
95
 
94
96
  /** Molded body plastic the modules sit on. */
95
97
  .fp-device-body {
98
+ color-scheme: var(--fp-scheme);
96
99
  background-image: linear-gradient(180deg, var(--fp-color-shell-hi), var(--fp-color-shell) 240px);
97
100
  box-shadow:
98
101
  inset 1px 1px 0 var(--fp-color-bevel-hi),
@@ -353,8 +356,510 @@
353
356
  --fp-cap-ink: var(--fp-color-cap-berry-ink);
354
357
  }
355
358
 
359
+ /**
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.
362
+ */
363
+ .fp-unit {
364
+ display: inline-flex;
365
+ flex-direction: column;
366
+ align-items: center;
367
+ gap: 0.375rem;
368
+ }
369
+
370
+ /**
371
+ * Rotary knob: a knurled chrome skirt round a domed cap. The cap is lit from the top left and stays
372
+ * still; only its fp-knob-mark turns, by `rotate`, through 270 degrees. 48 px, 40 px with a mouse,
373
+ * trackpad or pen.
374
+ */
375
+ .fp-knob {
376
+ position: relative;
377
+ display: inline-block;
378
+ flex-shrink: 0;
379
+ width: 3rem;
380
+ height: 3rem;
381
+ border-radius: 50%;
382
+ background-image: repeating-conic-gradient(var(--fp-color-chrome-100) 0deg 5deg, var(--fp-color-chrome-300) 5deg 10deg);
383
+ box-shadow:
384
+ 0 0 0 1px var(--fp-color-edge),
385
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
386
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
387
+ 1px 2px 0 var(--fp-color-cast);
388
+ cursor: ns-resize;
389
+ touch-action: none;
390
+ user-select: none;
391
+
392
+ @media (pointer: fine) {
393
+ width: 2.5rem;
394
+ height: 2.5rem;
395
+ }
396
+
397
+ &::before {
398
+ content: "";
399
+ position: absolute;
400
+ inset: 16%;
401
+ border-radius: 50%;
402
+ background-image: radial-gradient(
403
+ circle at 30% 25%,
404
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
405
+ var(--fp-color-chrome-100) 45%,
406
+ var(--fp-color-chrome-300) 100%
407
+ );
408
+ box-shadow:
409
+ 0 0 0 1px var(--fp-color-edge),
410
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
411
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
412
+ 1px 1px 2px var(--fp-color-cast);
413
+ }
414
+
415
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
416
+ &:focus-visible {
417
+ outline: 2px solid var(--fp-color-focus-ring);
418
+ outline-offset: 2px;
419
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
420
+ }
421
+
422
+ &[aria-disabled="true"] {
423
+ opacity: 0.45;
424
+ cursor: not-allowed;
425
+ }
426
+ }
427
+
428
+ /** The pointer line on a knob's cap. Turn it with `rotate`: -135deg at the minimum, 135deg at the maximum. */
429
+ .fp-knob-mark {
430
+ position: absolute;
431
+ inset: 0;
432
+ pointer-events: none;
433
+
434
+ &::after {
435
+ content: "";
436
+ position: absolute;
437
+ top: 21%;
438
+ left: 50%;
439
+ width: 3px;
440
+ height: 24%;
441
+ translate: -50% 0;
442
+ border-radius: 2px;
443
+ background-color: var(--fp-color-ink);
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Fader: a chrome cap on a recessed slot. It stands up by default and lies on its side with
449
+ * `data-orientation="horizontal"`; `--fp-fader-length` sets how far it travels (8rem). 44 px
450
+ * across, 36 px with a mouse, trackpad or pen. Pressing the slot moves the cap there.
451
+ */
452
+ .fp-fader {
453
+ position: relative;
454
+ display: inline-block;
455
+ flex-shrink: 0;
456
+ width: 2.75rem;
457
+ height: var(--fp-fader-length, 8rem);
458
+ cursor: ns-resize;
459
+ touch-action: none;
460
+ user-select: none;
461
+
462
+ @media (pointer: fine) {
463
+ width: 2.25rem;
464
+ }
465
+
466
+ &[data-orientation="horizontal"] {
467
+ width: var(--fp-fader-length, 8rem);
468
+ height: 2.75rem;
469
+ cursor: ew-resize;
470
+
471
+ @media (pointer: fine) {
472
+ height: 2.25rem;
473
+ }
474
+ }
475
+
476
+ &[data-disabled] {
477
+ opacity: 0.45;
478
+ cursor: not-allowed;
479
+ }
480
+ }
481
+
482
+ /** The slot a fader's cap rides in, cut into the panel: shaded on its top and left inside edges. */
483
+ .fp-fader-slot {
484
+ position: absolute;
485
+ inset: 0.5rem auto;
486
+ left: 50%;
487
+ width: 6px;
488
+ translate: -50% 0;
489
+ border-radius: 3px;
490
+ background-color: var(--fp-color-housing);
491
+ box-shadow:
492
+ inset 1px 1px 2px rgb(0 0 0 / 0.5),
493
+ 1px 1px 0 var(--fp-color-bevel-hi);
494
+
495
+ &[data-orientation="horizontal"] {
496
+ inset: 50% 0.5rem auto;
497
+ width: auto;
498
+ height: 6px;
499
+ translate: 0 -50%;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * A fader's cap: brushed chrome lit from the top left, with an ink grip line that marks the value.
505
+ * Place it along the slot with `bottom` (or `left` on its side, with `data-orientation="horizontal"`
506
+ * on the slot and cap as well as the fader).
507
+ */
508
+ .fp-fader-cap {
509
+ position: absolute;
510
+ inset-inline: 0;
511
+ height: 1.5rem;
512
+ border-radius: 4px;
513
+ background-image: linear-gradient(
514
+ 180deg,
515
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
516
+ var(--fp-color-chrome-100) 45%,
517
+ var(--fp-color-chrome-300) 100%
518
+ );
519
+ box-shadow:
520
+ 0 0 0 1px var(--fp-color-edge),
521
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
522
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
523
+ 1px 2px 0 var(--fp-color-cast);
524
+
525
+ @media (pointer: fine) {
526
+ height: 1.25rem;
527
+ }
528
+
529
+ &::after {
530
+ content: "";
531
+ position: absolute;
532
+ inset: 50% 20% auto;
533
+ height: 2px;
534
+ translate: 0 -50%;
535
+ border-radius: 1px;
536
+ background-color: var(--fp-color-ink);
537
+ }
538
+
539
+ &[data-orientation="horizontal"] {
540
+ inset-inline: auto;
541
+ inset-block: 0;
542
+ width: 1.5rem;
543
+ height: auto;
544
+
545
+ @media (pointer: fine) {
546
+ width: 1.25rem;
547
+ }
548
+
549
+ &::after {
550
+ inset: 20% auto 20% 50%;
551
+ width: 2px;
552
+ height: auto;
553
+ translate: -50% 0;
554
+ }
555
+ }
556
+
557
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
558
+ &:focus-visible {
559
+ outline: 2px solid var(--fp-color-focus-ring);
560
+ outline-offset: 2px;
561
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Level meter: a strip of LED segments in a dark housing, lit green, then amber, then red. Set
567
+ * `--fp-meter-level` (0 to 1, in whole segments), `--fp-meter-segments` (12), and where amber and
568
+ * red begin, `--fp-meter-amber` (8/12) and `--fp-meter-red` (11/12), as shares of the strip on
569
+ * segment edges, or a segment lights in two colours. It stands
570
+ * up by default and lies on its side with `data-orientation="horizontal"`; `--fp-meter-length`
571
+ * sets how long it is (8rem). Give it `role="meter"` and its values, or `aria-hidden` beside a
572
+ * readout that says the same.
573
+ */
574
+ .fp-meter {
575
+ --fp-meter-zones: linear-gradient(
576
+ to top,
577
+ var(--fp-color-led-on) 0 calc(var(--fp-meter-amber, calc(8 / 12)) * 100%),
578
+ var(--fp-color-led-amber) 0 calc(var(--fp-meter-red, calc(11 / 12)) * 100%),
579
+ var(--fp-color-led-red) 0
580
+ );
581
+ --fp-meter-gaps: repeating-linear-gradient(
582
+ to top,
583
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
584
+ transparent 0 calc(100% / var(--fp-meter-segments, 12))
585
+ );
586
+
587
+ /* Lit segments spill a little light into the gaps beside them: the lens's glow. */
588
+ --fp-meter-lit-gaps: repeating-linear-gradient(
589
+ to top,
590
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
591
+ rgb(0 0 0 / calc(0.18 * var(--fp-led-glow, 1))) 0 calc(100% / var(--fp-meter-segments, 12))
592
+ );
593
+ --fp-meter-cell: 100% calc(100% / var(--fp-meter-segments, 12));
594
+ position: relative;
595
+ display: inline-block;
596
+ flex-shrink: 0;
597
+ width: 0.75rem;
598
+ height: var(--fp-meter-length, 8rem);
599
+ border-radius: 3px;
600
+ background-color: var(--fp-color-housing);
601
+ box-shadow:
602
+ inset 1px 1px 2px rgb(0 0 0 / 0.5),
603
+ 1px 1px 0 var(--fp-color-bevel-hi);
604
+
605
+ /* Each segment's gap sits at its far end, so the strip's own inset at that end is the gap. */
606
+ &::before,
607
+ &::after {
608
+ content: "";
609
+ position: absolute;
610
+ inset: 0 2px 2px;
611
+ background-image: var(--fp-meter-zones);
612
+ mask-image: var(--fp-meter-gaps);
613
+ }
614
+
615
+ /* Unlit segments are the LED's off colour with a trace of their zone's, so the zones show. */
616
+ &::before {
617
+ background-image:
618
+ linear-gradient(color-mix(in srgb, var(--fp-color-led-off) 75%, transparent) 0 0),
619
+ var(--fp-meter-zones);
620
+ }
621
+
622
+ /*
623
+ * A lit segment is brightest over its chip, in the middle, by the lens. The cut sits 1 px into the
624
+ * gap below the next segment, so rounding never lights a sliver of it.
625
+ */
626
+ &::after {
627
+ background-image:
628
+ radial-gradient(closest-side, rgb(255 255 255 / var(--fp-led-core, 0.3)), transparent),
629
+ var(--fp-meter-zones);
630
+ background-position: left bottom;
631
+ background-size: var(--fp-meter-cell), auto;
632
+ mask-image: var(--fp-meter-lit-gaps);
633
+ clip-path: inset(calc((1 - var(--fp-meter-level, 0)) * 100% + 1px) 0 0 0);
634
+ }
635
+
636
+ /* Forced colours: the strip as an outline and the lit segments in CanvasText, so the level shows. */
637
+ @media (forced-colors: active) {
638
+ outline: 1px solid CanvasText;
639
+
640
+ &::before {
641
+ content: none;
642
+ }
643
+
644
+ &::after {
645
+ forced-color-adjust: none;
646
+ background: CanvasText;
647
+ mask-image: var(--fp-meter-gaps);
648
+ }
649
+ }
650
+
651
+ &[data-orientation="horizontal"] {
652
+ --fp-meter-zones: linear-gradient(
653
+ to right,
654
+ var(--fp-color-led-on) 0 calc(var(--fp-meter-amber, calc(8 / 12)) * 100%),
655
+ var(--fp-color-led-amber) 0 calc(var(--fp-meter-red, calc(11 / 12)) * 100%),
656
+ var(--fp-color-led-red) 0
657
+ );
658
+ --fp-meter-gaps: repeating-linear-gradient(
659
+ to right,
660
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
661
+ transparent 0 calc(100% / var(--fp-meter-segments, 12))
662
+ );
663
+ --fp-meter-lit-gaps: repeating-linear-gradient(
664
+ to right,
665
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
666
+ rgb(0 0 0 / calc(0.18 * var(--fp-led-glow, 1))) 0 calc(100% / var(--fp-meter-segments, 12))
667
+ );
668
+ --fp-meter-cell: calc(100% / var(--fp-meter-segments, 12)) 100%;
669
+ width: var(--fp-meter-length, 8rem);
670
+ height: 0.75rem;
671
+
672
+ &::before,
673
+ &::after {
674
+ inset: 2px 0 2px 2px;
675
+ }
676
+
677
+ &::after {
678
+ clip-path: inset(0 calc((1 - var(--fp-meter-level, 0)) * 100% + 1px) 0 0);
679
+ }
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Seven-segment readout: LED digits on a near-black face, set in the housing. Each character is an
685
+ * fp-readout-cell, and the value is said in an fp-readout-text inside. Green by default; colour it
686
+ * with fp-led-amber or fp-led-red. Lit digits pass as text on the face at any size.
687
+ */
688
+ .fp-readout {
689
+ position: relative;
690
+ display: inline-flex;
691
+ align-items: center;
692
+ gap: 0.1875rem;
693
+ padding: 0.3125rem 0.4375rem;
694
+ border: 3px solid var(--fp-color-housing);
695
+ border-radius: 5px;
696
+ background-color: color-mix(in srgb, var(--fp-color-led-off) 70%, black);
697
+ box-shadow:
698
+ inset 1px 1px 3px rgb(0 0 0 / 0.8),
699
+ 0 1px 0 var(--fp-color-bevel-hi);
700
+ }
701
+
702
+ /**
703
+ * One character of an fp-readout, leaning a little like a real display. It shows its unlit
704
+ * segments itself, a figure eight and a point, faintly in the lit colour; put the lit ones in an
705
+ * fp-readout-lit inside. With `data-colon` it is a narrow colon cell. `--fp-readout-height` sets
706
+ * its height (2rem). Give it aria-hidden="true".
707
+ */
708
+ .fp-readout-cell {
709
+ position: relative;
710
+ flex-shrink: 0;
711
+ height: var(--fp-readout-height, 2rem);
712
+ aspect-ratio: 15 / 22;
713
+ transform: skewX(-6deg);
714
+
715
+ &::before {
716
+ content: "";
717
+ position: absolute;
718
+ inset: 0;
719
+ background-color: color-mix(in srgb, var(--fp-color-led-off) 75%, var(--fp-led, var(--fp-color-led-on)));
720
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 22'%3E%3Cpolygon points='1.5,1 2.5,0 9.5,0 10.5,1 9.5,2 2.5,2'/%3E%3Cpolygon points='11,1.5 12,2.5 12,9.5 11,10.5 10,9.5 10,2.5'/%3E%3Cpolygon points='11,11.5 12,12.5 12,19.5 11,20.5 10,19.5 10,12.5'/%3E%3Cpolygon points='1.5,21 2.5,20 9.5,20 10.5,21 9.5,22 2.5,22'/%3E%3Cpolygon points='1,11.5 2,12.5 2,19.5 1,20.5 0,19.5 0,12.5'/%3E%3Cpolygon points='1,1.5 2,2.5 2,9.5 1,10.5 0,9.5 0,2.5'/%3E%3Cpolygon points='1.5,11 2.5,10 9.5,10 10.5,11 9.5,12 2.5,12'/%3E%3Ccircle cx='13.5' cy='21' r='1.2'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
721
+ }
722
+
723
+ &[data-colon] {
724
+ aspect-ratio: 5 / 22;
725
+
726
+ &::before {
727
+ content: none;
728
+ }
729
+ }
730
+
731
+ /* With forced colours, only the lit digits show: a ghost in a system colour blurs into them. */
732
+ @media (forced-colors: active) {
733
+ &::before {
734
+ content: none;
735
+ }
736
+ }
737
+ }
738
+
739
+ /**
740
+ * The lit segments of an fp-readout-cell: an element for each, named by `data-segment`, `a` to `g`
741
+ * (top, then clockwise, then the middle), `dp`, or a colon's `colon-upper` and `colon-lower`. Each
742
+ * is brightest over its chip, in the middle, and they glow onto the face, both by the lens.
743
+ */
744
+ .fp-readout-lit {
745
+ position: absolute;
746
+ inset: 0;
747
+ filter: drop-shadow(
748
+ 0 0 calc(var(--fp-readout-height, 2rem) * 0.06 * var(--fp-led-glow, 1))
749
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent)
750
+ );
751
+
752
+ & > [data-segment] {
753
+ position: absolute;
754
+ background-color: var(--fp-led, var(--fp-color-led-on));
755
+ background-image: radial-gradient(
756
+ closest-side,
757
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) calc((1 - var(--fp-led-core, 0.3)) * 100%), white),
758
+ var(--fp-led, var(--fp-color-led-on))
759
+ );
760
+ }
761
+
762
+ /* The bars, on the cell's 15 x 22 grid: 9 long and 2 thick, pointed at both ends. */
763
+ & > [data-segment="a"],
764
+ & > [data-segment="d"],
765
+ & > [data-segment="g"] {
766
+ left: 10%;
767
+ width: 60%;
768
+ height: 9.0909%;
769
+ clip-path: polygon(0 50%, 11.1111% 0, 88.8889% 0, 100% 50%, 88.8889% 100%, 11.1111% 100%);
770
+ }
771
+
772
+ & > [data-segment="b"],
773
+ & > [data-segment="c"],
774
+ & > [data-segment="e"],
775
+ & > [data-segment="f"] {
776
+ width: 13.3333%;
777
+ height: 40.9091%;
778
+ clip-path: polygon(50% 0, 100% 11.1111%, 100% 88.8889%, 50% 100%, 0 88.8889%, 0 11.1111%);
779
+ }
780
+
781
+ & > [data-segment="a"] {
782
+ top: 0;
783
+ }
784
+
785
+ & > [data-segment="g"] {
786
+ top: 45.4545%;
787
+ }
788
+
789
+ & > [data-segment="d"] {
790
+ top: 90.9091%;
791
+ }
792
+
793
+ & > [data-segment="b"],
794
+ & > [data-segment="c"] {
795
+ left: 66.6667%;
796
+ }
797
+
798
+ & > [data-segment="e"],
799
+ & > [data-segment="f"] {
800
+ left: 0;
801
+ }
802
+
803
+ & > [data-segment="b"],
804
+ & > [data-segment="f"] {
805
+ top: 6.8182%;
806
+ }
807
+
808
+ & > [data-segment="c"],
809
+ & > [data-segment="e"] {
810
+ top: 52.2727%;
811
+ }
812
+
813
+ & > [data-segment="dp"],
814
+ & > [data-segment="colon-upper"],
815
+ & > [data-segment="colon-lower"] {
816
+ aspect-ratio: 1;
817
+ border-radius: 50%;
818
+ }
819
+
820
+ & > [data-segment="dp"] {
821
+ top: 90%;
822
+ left: 82%;
823
+ width: 16%;
824
+ }
825
+
826
+ & > [data-segment="colon-upper"],
827
+ & > [data-segment="colon-lower"] {
828
+ left: 26%;
829
+ width: 48%;
830
+ }
831
+
832
+ & > [data-segment="colon-upper"] {
833
+ top: 26.3636%;
834
+ }
835
+
836
+ & > [data-segment="colon-lower"] {
837
+ top: 62.7273%;
838
+ }
839
+
840
+ @media (forced-colors: active) {
841
+ filter: none;
842
+
843
+ & > [data-segment] {
844
+ forced-color-adjust: none;
845
+ background: CanvasText;
846
+ }
847
+ }
848
+ }
849
+
850
+ /** An fp-readout's value as text, for screen readers: hidden from view, read in place of the digits. */
851
+ .fp-readout-text {
852
+ position: absolute;
853
+ width: 1px;
854
+ height: 1px;
855
+ overflow: hidden;
856
+ clip-path: inset(50%);
857
+ white-space: nowrap;
858
+ }
859
+
356
860
  /** Matte content panel. This is where reading happens: no gloss, a 1 px bevel and a short hard shadow. */
357
861
  .fp-module {
862
+ color-scheme: var(--fp-scheme);
358
863
  border-radius: 10px;
359
864
  background-color: var(--fp-color-shell-matte);
360
865
  box-shadow:
@@ -418,6 +923,7 @@
418
923
 
419
924
  /** Recessed well inside a matte panel, for secondary content or an empty slot. */
420
925
  .fp-well {
926
+ color-scheme: var(--fp-scheme);
421
927
  border-radius: 6px;
422
928
  background-color: var(--fp-color-well);
423
929
  box-shadow:
@@ -491,30 +997,76 @@
491
997
  box-shadow:
492
998
  0 0 0 2px var(--fp-color-housing),
493
999
  inset 0 -1px 1px rgb(0 0 0 / 0.45);
1000
+
1001
+ /* Forced colours drop the fill and the ring: an outline keeps an unlit LED in view. */
1002
+ @media (forced-colors: active) {
1003
+ outline: 1px solid CanvasText;
1004
+ outline-offset: 1px;
1005
+ }
494
1006
  }
495
1007
 
496
- /** Green for an fp-led, the default. */
1008
+ /** Green for an fp-led or fp-readout, the default. */
497
1009
  .fp-led-green {
498
1010
  --fp-led: var(--fp-color-led-on);
499
1011
  }
500
1012
 
501
- /** Amber for an fp-led. */
1013
+ /** Amber for an fp-led or fp-readout. */
502
1014
  .fp-led-amber {
503
1015
  --fp-led: var(--fp-color-led-amber);
504
1016
  }
505
1017
 
506
- /** Red for an fp-led. */
1018
+ /** Red for an fp-led or fp-readout. */
507
1019
  .fp-led-red {
508
1020
  --fp-led: var(--fp-color-led-red);
509
1021
  }
510
1022
 
511
- /** Lights an fp-led in its colour, with a glow. Say what it means in text nearby, never by colour alone. */
1023
+ /**
1024
+ * Lights an fp-led in its colour, brightest over the chip in its middle, with a glow. The lens sets
1025
+ * how strong both are. Say what it means in text nearby, never by colour alone.
1026
+ */
512
1027
  .fp-led-lit {
513
1028
  background-color: var(--fp-led, var(--fp-color-led-on));
1029
+ background-image: radial-gradient(
1030
+ closest-side,
1031
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) calc((1 - var(--fp-led-core, 0.3)) * 100%), white),
1032
+ var(--fp-led, var(--fp-color-led-on))
1033
+ );
514
1034
  box-shadow:
515
1035
  0 0 0 2px var(--fp-color-housing),
516
- 0 0 7px 2px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, transparent),
1036
+ 0 0 calc(7px * var(--fp-led-glow, 1)) calc(2px * var(--fp-led-glow, 1))
1037
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, transparent),
517
1038
  inset 0 -1px 1px rgb(0 0 0 / 0.2);
1039
+
1040
+ /* Lit is filled, so it reads apart from unlit by shape as well as colour. */
1041
+ @media (forced-colors: active) {
1042
+ forced-color-adjust: none;
1043
+ background: CanvasText;
1044
+ box-shadow: none;
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * LED lens, flat: an even fill with no hot spot. A lens class works on an fp-led, fp-meter or
1050
+ * fp-readout, or on any element around them, such as a device body, to set every one inside.
1051
+ */
1052
+ .fp-lens-flat {
1053
+ --fp-led-core: 0;
1054
+ --fp-led-glow: 1;
1055
+ }
1056
+
1057
+ /**
1058
+ * LED lens, diffused, the default: like most real displays, each LED is a little brighter over its
1059
+ * chip, in the middle, with a soft glow.
1060
+ */
1061
+ .fp-lens-diffused {
1062
+ --fp-led-core: 0.3;
1063
+ --fp-led-glow: 1;
1064
+ }
1065
+
1066
+ /** LED lens, clear: a bright, pale core over each chip and a wider glow, like a water-clear lens. */
1067
+ .fp-lens-clear {
1068
+ --fp-led-core: 0.5;
1069
+ --fp-led-glow: 1.5;
518
1070
  }
519
1071
 
520
1072
  /** Decorative screw head. Give it aria-hidden="true". */
@@ -532,6 +1084,7 @@
532
1084
 
533
1085
  /** A scrolling panel, such as a list window, with a thin scrollbar that never pushes the layout. */
534
1086
  .fp-scroll-thin {
1087
+ color-scheme: var(--fp-scheme);
535
1088
  scrollbar-width: thin;
536
1089
  scrollbar-color: var(--fp-color-scroll-thumb) transparent;
537
1090
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frontpanel/css",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.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
@@ -63,6 +63,7 @@
63
63
  */
64
64
  /** A matte panel lifted over the device as a bottom sheet on phones; dock it as an fp-module on wide screens. */
65
65
  .fp-sheet-panel {
66
+ color-scheme: var(--fp-scheme);
66
67
  border-radius: 14px 14px 0 0;
67
68
  background-color: var(--fp-color-shell-matte);
68
69
  box-shadow:
@@ -73,6 +74,7 @@
73
74
 
74
75
  /** Inset text field or select on a matte panel. Its border keeps 3:1 against the panel in both skins. */
75
76
  .fp-field {
77
+ color-scheme: var(--fp-scheme);
76
78
  min-height: 2.75rem;
77
79
  width: 100%;
78
80
  border-radius: 6px;
package/tailwind-base.css CHANGED
@@ -12,21 +12,13 @@
12
12
  */
13
13
  /* The page wears the skin. Native controls and scrollbars follow it, forced or not. */
14
14
  :root {
15
- color-scheme: light dark;
15
+ color-scheme: var(--fp-scheme);
16
16
  background-color: var(--fp-color-backdrop);
17
17
  color: var(--fp-color-ink);
18
18
  font-family: var(--fp-font-ui);
19
19
  -webkit-text-size-adjust: 100%;
20
20
  }
21
21
 
22
- :root[data-fp-skin="silver"] {
23
- color-scheme: light;
24
- }
25
-
26
- :root[data-fp-skin="gunmetal"] {
27
- color-scheme: dark;
28
- }
29
-
30
22
  body {
31
23
  margin: 0;
32
24
  min-height: 100dvh;
package/tailwind-page.css CHANGED
@@ -64,6 +64,7 @@
64
64
  */
65
65
  /** A matte panel lifted over the device as a bottom sheet on phones; dock it as an fp-module on wide screens. */
66
66
  @utility fp-sheet-panel {
67
+ color-scheme: var(--fp-scheme);
67
68
  border-radius: 14px 14px 0 0;
68
69
  background-color: var(--fp-color-shell-matte);
69
70
  box-shadow:
@@ -74,6 +75,7 @@
74
75
 
75
76
  /** Inset text field or select on a matte panel. Its border keeps 3:1 against the panel in both skins. */
76
77
  @utility fp-field {
78
+ color-scheme: var(--fp-scheme);
77
79
  min-height: 2.75rem;
78
80
  width: 100%;
79
81
  border-radius: 6px;
package/tailwind.css CHANGED
@@ -108,6 +108,7 @@
108
108
 
109
109
  /** The page behind the device: quiet blue-grey with a faint Y2K grid. */
110
110
  @utility fp-device-backdrop {
111
+ color-scheme: var(--fp-scheme);
111
112
  background-color: var(--fp-color-backdrop);
112
113
  background-image:
113
114
  linear-gradient(var(--fp-color-grid) 1px, transparent 1px),
@@ -123,6 +124,7 @@
123
124
 
124
125
  /** Liquid-chrome ring around the device body. Diagonal so the reflection band crosses the corners. */
125
126
  @utility fp-device-rim {
127
+ color-scheme: var(--fp-scheme);
126
128
  border-radius: 36px;
127
129
  padding: 10px;
128
130
  background-image: linear-gradient(
@@ -152,6 +154,7 @@
152
154
 
153
155
  /** Molded body plastic the modules sit on. */
154
156
  @utility fp-device-body {
157
+ color-scheme: var(--fp-scheme);
155
158
  background-image: linear-gradient(180deg, var(--fp-color-shell-hi), var(--fp-color-shell) 240px);
156
159
  box-shadow:
157
160
  inset 1px 1px 0 var(--fp-color-bevel-hi),
@@ -412,8 +415,510 @@
412
415
  --fp-cap-ink: var(--fp-color-cap-berry-ink);
413
416
  }
414
417
 
418
+ /**
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.
421
+ */
422
+ @utility fp-unit {
423
+ display: inline-flex;
424
+ flex-direction: column;
425
+ align-items: center;
426
+ gap: 0.375rem;
427
+ }
428
+
429
+ /**
430
+ * Rotary knob: a knurled chrome skirt round a domed cap. The cap is lit from the top left and stays
431
+ * still; only its fp-knob-mark turns, by `rotate`, through 270 degrees. 48 px, 40 px with a mouse,
432
+ * trackpad or pen.
433
+ */
434
+ @utility fp-knob {
435
+ position: relative;
436
+ display: inline-block;
437
+ flex-shrink: 0;
438
+ width: 3rem;
439
+ height: 3rem;
440
+ border-radius: 50%;
441
+ background-image: repeating-conic-gradient(var(--fp-color-chrome-100) 0deg 5deg, var(--fp-color-chrome-300) 5deg 10deg);
442
+ box-shadow:
443
+ 0 0 0 1px var(--fp-color-edge),
444
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
445
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
446
+ 1px 2px 0 var(--fp-color-cast);
447
+ cursor: ns-resize;
448
+ touch-action: none;
449
+ user-select: none;
450
+
451
+ @media (pointer: fine) {
452
+ width: 2.5rem;
453
+ height: 2.5rem;
454
+ }
455
+
456
+ &::before {
457
+ content: "";
458
+ position: absolute;
459
+ inset: 16%;
460
+ border-radius: 50%;
461
+ background-image: radial-gradient(
462
+ circle at 30% 25%,
463
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
464
+ var(--fp-color-chrome-100) 45%,
465
+ var(--fp-color-chrome-300) 100%
466
+ );
467
+ box-shadow:
468
+ 0 0 0 1px var(--fp-color-edge),
469
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
470
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
471
+ 1px 1px 2px var(--fp-color-cast);
472
+ }
473
+
474
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
475
+ &:focus-visible {
476
+ outline: 2px solid var(--fp-color-focus-ring);
477
+ outline-offset: 2px;
478
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
479
+ }
480
+
481
+ &[aria-disabled="true"] {
482
+ opacity: 0.45;
483
+ cursor: not-allowed;
484
+ }
485
+ }
486
+
487
+ /** The pointer line on a knob's cap. Turn it with `rotate`: -135deg at the minimum, 135deg at the maximum. */
488
+ @utility fp-knob-mark {
489
+ position: absolute;
490
+ inset: 0;
491
+ pointer-events: none;
492
+
493
+ &::after {
494
+ content: "";
495
+ position: absolute;
496
+ top: 21%;
497
+ left: 50%;
498
+ width: 3px;
499
+ height: 24%;
500
+ translate: -50% 0;
501
+ border-radius: 2px;
502
+ background-color: var(--fp-color-ink);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Fader: a chrome cap on a recessed slot. It stands up by default and lies on its side with
508
+ * `data-orientation="horizontal"`; `--fp-fader-length` sets how far it travels (8rem). 44 px
509
+ * across, 36 px with a mouse, trackpad or pen. Pressing the slot moves the cap there.
510
+ */
511
+ @utility fp-fader {
512
+ position: relative;
513
+ display: inline-block;
514
+ flex-shrink: 0;
515
+ width: 2.75rem;
516
+ height: var(--fp-fader-length, 8rem);
517
+ cursor: ns-resize;
518
+ touch-action: none;
519
+ user-select: none;
520
+
521
+ @media (pointer: fine) {
522
+ width: 2.25rem;
523
+ }
524
+
525
+ &[data-orientation="horizontal"] {
526
+ width: var(--fp-fader-length, 8rem);
527
+ height: 2.75rem;
528
+ cursor: ew-resize;
529
+
530
+ @media (pointer: fine) {
531
+ height: 2.25rem;
532
+ }
533
+ }
534
+
535
+ &[data-disabled] {
536
+ opacity: 0.45;
537
+ cursor: not-allowed;
538
+ }
539
+ }
540
+
541
+ /** The slot a fader's cap rides in, cut into the panel: shaded on its top and left inside edges. */
542
+ @utility fp-fader-slot {
543
+ position: absolute;
544
+ inset: 0.5rem auto;
545
+ left: 50%;
546
+ width: 6px;
547
+ translate: -50% 0;
548
+ border-radius: 3px;
549
+ background-color: var(--fp-color-housing);
550
+ box-shadow:
551
+ inset 1px 1px 2px rgb(0 0 0 / 0.5),
552
+ 1px 1px 0 var(--fp-color-bevel-hi);
553
+
554
+ &[data-orientation="horizontal"] {
555
+ inset: 50% 0.5rem auto;
556
+ width: auto;
557
+ height: 6px;
558
+ translate: 0 -50%;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * A fader's cap: brushed chrome lit from the top left, with an ink grip line that marks the value.
564
+ * Place it along the slot with `bottom` (or `left` on its side, with `data-orientation="horizontal"`
565
+ * on the slot and cap as well as the fader).
566
+ */
567
+ @utility fp-fader-cap {
568
+ position: absolute;
569
+ inset-inline: 0;
570
+ height: 1.5rem;
571
+ border-radius: 4px;
572
+ background-image: linear-gradient(
573
+ 180deg,
574
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
575
+ var(--fp-color-chrome-100) 45%,
576
+ var(--fp-color-chrome-300) 100%
577
+ );
578
+ box-shadow:
579
+ 0 0 0 1px var(--fp-color-edge),
580
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
581
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
582
+ 1px 2px 0 var(--fp-color-cast);
583
+
584
+ @media (pointer: fine) {
585
+ height: 1.25rem;
586
+ }
587
+
588
+ &::after {
589
+ content: "";
590
+ position: absolute;
591
+ inset: 50% 20% auto;
592
+ height: 2px;
593
+ translate: 0 -50%;
594
+ border-radius: 1px;
595
+ background-color: var(--fp-color-ink);
596
+ }
597
+
598
+ &[data-orientation="horizontal"] {
599
+ inset-inline: auto;
600
+ inset-block: 0;
601
+ width: 1.5rem;
602
+ height: auto;
603
+
604
+ @media (pointer: fine) {
605
+ width: 1.25rem;
606
+ }
607
+
608
+ &::after {
609
+ inset: 20% auto 20% 50%;
610
+ width: 2px;
611
+ height: auto;
612
+ translate: -50% 0;
613
+ }
614
+ }
615
+
616
+ /* A solid ring with a glow outside it: the glow alone fails non-text contrast on light chrome. */
617
+ &:focus-visible {
618
+ outline: 2px solid var(--fp-color-focus-ring);
619
+ outline-offset: 2px;
620
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Level meter: a strip of LED segments in a dark housing, lit green, then amber, then red. Set
626
+ * `--fp-meter-level` (0 to 1, in whole segments), `--fp-meter-segments` (12), and where amber and
627
+ * red begin, `--fp-meter-amber` (8/12) and `--fp-meter-red` (11/12), as shares of the strip on
628
+ * segment edges, or a segment lights in two colours. It stands
629
+ * up by default and lies on its side with `data-orientation="horizontal"`; `--fp-meter-length`
630
+ * sets how long it is (8rem). Give it `role="meter"` and its values, or `aria-hidden` beside a
631
+ * readout that says the same.
632
+ */
633
+ @utility fp-meter {
634
+ --fp-meter-zones: linear-gradient(
635
+ to top,
636
+ var(--fp-color-led-on) 0 calc(var(--fp-meter-amber, calc(8 / 12)) * 100%),
637
+ var(--fp-color-led-amber) 0 calc(var(--fp-meter-red, calc(11 / 12)) * 100%),
638
+ var(--fp-color-led-red) 0
639
+ );
640
+ --fp-meter-gaps: repeating-linear-gradient(
641
+ to top,
642
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
643
+ transparent 0 calc(100% / var(--fp-meter-segments, 12))
644
+ );
645
+
646
+ /* Lit segments spill a little light into the gaps beside them: the lens's glow. */
647
+ --fp-meter-lit-gaps: repeating-linear-gradient(
648
+ to top,
649
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
650
+ rgb(0 0 0 / calc(0.18 * var(--fp-led-glow, 1))) 0 calc(100% / var(--fp-meter-segments, 12))
651
+ );
652
+ --fp-meter-cell: 100% calc(100% / var(--fp-meter-segments, 12));
653
+ position: relative;
654
+ display: inline-block;
655
+ flex-shrink: 0;
656
+ width: 0.75rem;
657
+ height: var(--fp-meter-length, 8rem);
658
+ border-radius: 3px;
659
+ background-color: var(--fp-color-housing);
660
+ box-shadow:
661
+ inset 1px 1px 2px rgb(0 0 0 / 0.5),
662
+ 1px 1px 0 var(--fp-color-bevel-hi);
663
+
664
+ /* Each segment's gap sits at its far end, so the strip's own inset at that end is the gap. */
665
+ &::before,
666
+ &::after {
667
+ content: "";
668
+ position: absolute;
669
+ inset: 0 2px 2px;
670
+ background-image: var(--fp-meter-zones);
671
+ mask-image: var(--fp-meter-gaps);
672
+ }
673
+
674
+ /* Unlit segments are the LED's off colour with a trace of their zone's, so the zones show. */
675
+ &::before {
676
+ background-image:
677
+ linear-gradient(color-mix(in srgb, var(--fp-color-led-off) 75%, transparent) 0 0),
678
+ var(--fp-meter-zones);
679
+ }
680
+
681
+ /*
682
+ * A lit segment is brightest over its chip, in the middle, by the lens. The cut sits 1 px into the
683
+ * gap below the next segment, so rounding never lights a sliver of it.
684
+ */
685
+ &::after {
686
+ background-image:
687
+ radial-gradient(closest-side, rgb(255 255 255 / var(--fp-led-core, 0.3)), transparent),
688
+ var(--fp-meter-zones);
689
+ background-position: left bottom;
690
+ background-size: var(--fp-meter-cell), auto;
691
+ mask-image: var(--fp-meter-lit-gaps);
692
+ clip-path: inset(calc((1 - var(--fp-meter-level, 0)) * 100% + 1px) 0 0 0);
693
+ }
694
+
695
+ /* Forced colours: the strip as an outline and the lit segments in CanvasText, so the level shows. */
696
+ @media (forced-colors: active) {
697
+ outline: 1px solid CanvasText;
698
+
699
+ &::before {
700
+ content: none;
701
+ }
702
+
703
+ &::after {
704
+ forced-color-adjust: none;
705
+ background: CanvasText;
706
+ mask-image: var(--fp-meter-gaps);
707
+ }
708
+ }
709
+
710
+ &[data-orientation="horizontal"] {
711
+ --fp-meter-zones: linear-gradient(
712
+ to right,
713
+ var(--fp-color-led-on) 0 calc(var(--fp-meter-amber, calc(8 / 12)) * 100%),
714
+ var(--fp-color-led-amber) 0 calc(var(--fp-meter-red, calc(11 / 12)) * 100%),
715
+ var(--fp-color-led-red) 0
716
+ );
717
+ --fp-meter-gaps: repeating-linear-gradient(
718
+ to right,
719
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
720
+ transparent 0 calc(100% / var(--fp-meter-segments, 12))
721
+ );
722
+ --fp-meter-lit-gaps: repeating-linear-gradient(
723
+ to right,
724
+ #000 0 calc(100% / var(--fp-meter-segments, 12) - 2px),
725
+ rgb(0 0 0 / calc(0.18 * var(--fp-led-glow, 1))) 0 calc(100% / var(--fp-meter-segments, 12))
726
+ );
727
+ --fp-meter-cell: calc(100% / var(--fp-meter-segments, 12)) 100%;
728
+ width: var(--fp-meter-length, 8rem);
729
+ height: 0.75rem;
730
+
731
+ &::before,
732
+ &::after {
733
+ inset: 2px 0 2px 2px;
734
+ }
735
+
736
+ &::after {
737
+ clip-path: inset(0 calc((1 - var(--fp-meter-level, 0)) * 100% + 1px) 0 0);
738
+ }
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Seven-segment readout: LED digits on a near-black face, set in the housing. Each character is an
744
+ * fp-readout-cell, and the value is said in an fp-readout-text inside. Green by default; colour it
745
+ * with fp-led-amber or fp-led-red. Lit digits pass as text on the face at any size.
746
+ */
747
+ @utility fp-readout {
748
+ position: relative;
749
+ display: inline-flex;
750
+ align-items: center;
751
+ gap: 0.1875rem;
752
+ padding: 0.3125rem 0.4375rem;
753
+ border: 3px solid var(--fp-color-housing);
754
+ border-radius: 5px;
755
+ background-color: color-mix(in srgb, var(--fp-color-led-off) 70%, black);
756
+ box-shadow:
757
+ inset 1px 1px 3px rgb(0 0 0 / 0.8),
758
+ 0 1px 0 var(--fp-color-bevel-hi);
759
+ }
760
+
761
+ /**
762
+ * One character of an fp-readout, leaning a little like a real display. It shows its unlit
763
+ * segments itself, a figure eight and a point, faintly in the lit colour; put the lit ones in an
764
+ * fp-readout-lit inside. With `data-colon` it is a narrow colon cell. `--fp-readout-height` sets
765
+ * its height (2rem). Give it aria-hidden="true".
766
+ */
767
+ @utility fp-readout-cell {
768
+ position: relative;
769
+ flex-shrink: 0;
770
+ height: var(--fp-readout-height, 2rem);
771
+ aspect-ratio: 15 / 22;
772
+ transform: skewX(-6deg);
773
+
774
+ &::before {
775
+ content: "";
776
+ position: absolute;
777
+ inset: 0;
778
+ background-color: color-mix(in srgb, var(--fp-color-led-off) 75%, var(--fp-led, var(--fp-color-led-on)));
779
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 22'%3E%3Cpolygon points='1.5,1 2.5,0 9.5,0 10.5,1 9.5,2 2.5,2'/%3E%3Cpolygon points='11,1.5 12,2.5 12,9.5 11,10.5 10,9.5 10,2.5'/%3E%3Cpolygon points='11,11.5 12,12.5 12,19.5 11,20.5 10,19.5 10,12.5'/%3E%3Cpolygon points='1.5,21 2.5,20 9.5,20 10.5,21 9.5,22 2.5,22'/%3E%3Cpolygon points='1,11.5 2,12.5 2,19.5 1,20.5 0,19.5 0,12.5'/%3E%3Cpolygon points='1,1.5 2,2.5 2,9.5 1,10.5 0,9.5 0,2.5'/%3E%3Cpolygon points='1.5,11 2.5,10 9.5,10 10.5,11 9.5,12 2.5,12'/%3E%3Ccircle cx='13.5' cy='21' r='1.2'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
780
+ }
781
+
782
+ &[data-colon] {
783
+ aspect-ratio: 5 / 22;
784
+
785
+ &::before {
786
+ content: none;
787
+ }
788
+ }
789
+
790
+ /* With forced colours, only the lit digits show: a ghost in a system colour blurs into them. */
791
+ @media (forced-colors: active) {
792
+ &::before {
793
+ content: none;
794
+ }
795
+ }
796
+ }
797
+
798
+ /**
799
+ * The lit segments of an fp-readout-cell: an element for each, named by `data-segment`, `a` to `g`
800
+ * (top, then clockwise, then the middle), `dp`, or a colon's `colon-upper` and `colon-lower`. Each
801
+ * is brightest over its chip, in the middle, and they glow onto the face, both by the lens.
802
+ */
803
+ @utility fp-readout-lit {
804
+ position: absolute;
805
+ inset: 0;
806
+ filter: drop-shadow(
807
+ 0 0 calc(var(--fp-readout-height, 2rem) * 0.06 * var(--fp-led-glow, 1))
808
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent)
809
+ );
810
+
811
+ & > [data-segment] {
812
+ position: absolute;
813
+ background-color: var(--fp-led, var(--fp-color-led-on));
814
+ background-image: radial-gradient(
815
+ closest-side,
816
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) calc((1 - var(--fp-led-core, 0.3)) * 100%), white),
817
+ var(--fp-led, var(--fp-color-led-on))
818
+ );
819
+ }
820
+
821
+ /* The bars, on the cell's 15 x 22 grid: 9 long and 2 thick, pointed at both ends. */
822
+ & > [data-segment="a"],
823
+ & > [data-segment="d"],
824
+ & > [data-segment="g"] {
825
+ left: 10%;
826
+ width: 60%;
827
+ height: 9.0909%;
828
+ clip-path: polygon(0 50%, 11.1111% 0, 88.8889% 0, 100% 50%, 88.8889% 100%, 11.1111% 100%);
829
+ }
830
+
831
+ & > [data-segment="b"],
832
+ & > [data-segment="c"],
833
+ & > [data-segment="e"],
834
+ & > [data-segment="f"] {
835
+ width: 13.3333%;
836
+ height: 40.9091%;
837
+ clip-path: polygon(50% 0, 100% 11.1111%, 100% 88.8889%, 50% 100%, 0 88.8889%, 0 11.1111%);
838
+ }
839
+
840
+ & > [data-segment="a"] {
841
+ top: 0;
842
+ }
843
+
844
+ & > [data-segment="g"] {
845
+ top: 45.4545%;
846
+ }
847
+
848
+ & > [data-segment="d"] {
849
+ top: 90.9091%;
850
+ }
851
+
852
+ & > [data-segment="b"],
853
+ & > [data-segment="c"] {
854
+ left: 66.6667%;
855
+ }
856
+
857
+ & > [data-segment="e"],
858
+ & > [data-segment="f"] {
859
+ left: 0;
860
+ }
861
+
862
+ & > [data-segment="b"],
863
+ & > [data-segment="f"] {
864
+ top: 6.8182%;
865
+ }
866
+
867
+ & > [data-segment="c"],
868
+ & > [data-segment="e"] {
869
+ top: 52.2727%;
870
+ }
871
+
872
+ & > [data-segment="dp"],
873
+ & > [data-segment="colon-upper"],
874
+ & > [data-segment="colon-lower"] {
875
+ aspect-ratio: 1;
876
+ border-radius: 50%;
877
+ }
878
+
879
+ & > [data-segment="dp"] {
880
+ top: 90%;
881
+ left: 82%;
882
+ width: 16%;
883
+ }
884
+
885
+ & > [data-segment="colon-upper"],
886
+ & > [data-segment="colon-lower"] {
887
+ left: 26%;
888
+ width: 48%;
889
+ }
890
+
891
+ & > [data-segment="colon-upper"] {
892
+ top: 26.3636%;
893
+ }
894
+
895
+ & > [data-segment="colon-lower"] {
896
+ top: 62.7273%;
897
+ }
898
+
899
+ @media (forced-colors: active) {
900
+ filter: none;
901
+
902
+ & > [data-segment] {
903
+ forced-color-adjust: none;
904
+ background: CanvasText;
905
+ }
906
+ }
907
+ }
908
+
909
+ /** An fp-readout's value as text, for screen readers: hidden from view, read in place of the digits. */
910
+ @utility fp-readout-text {
911
+ position: absolute;
912
+ width: 1px;
913
+ height: 1px;
914
+ overflow: hidden;
915
+ clip-path: inset(50%);
916
+ white-space: nowrap;
917
+ }
918
+
415
919
  /** Matte content panel. This is where reading happens: no gloss, a 1 px bevel and a short hard shadow. */
416
920
  @utility fp-module {
921
+ color-scheme: var(--fp-scheme);
417
922
  border-radius: 10px;
418
923
  background-color: var(--fp-color-shell-matte);
419
924
  box-shadow:
@@ -477,6 +982,7 @@
477
982
 
478
983
  /** Recessed well inside a matte panel, for secondary content or an empty slot. */
479
984
  @utility fp-well {
985
+ color-scheme: var(--fp-scheme);
480
986
  border-radius: 6px;
481
987
  background-color: var(--fp-color-well);
482
988
  box-shadow:
@@ -550,30 +1056,76 @@
550
1056
  box-shadow:
551
1057
  0 0 0 2px var(--fp-color-housing),
552
1058
  inset 0 -1px 1px rgb(0 0 0 / 0.45);
1059
+
1060
+ /* Forced colours drop the fill and the ring: an outline keeps an unlit LED in view. */
1061
+ @media (forced-colors: active) {
1062
+ outline: 1px solid CanvasText;
1063
+ outline-offset: 1px;
1064
+ }
553
1065
  }
554
1066
 
555
- /** Green for an fp-led, the default. */
1067
+ /** Green for an fp-led or fp-readout, the default. */
556
1068
  @utility fp-led-green {
557
1069
  --fp-led: var(--fp-color-led-on);
558
1070
  }
559
1071
 
560
- /** Amber for an fp-led. */
1072
+ /** Amber for an fp-led or fp-readout. */
561
1073
  @utility fp-led-amber {
562
1074
  --fp-led: var(--fp-color-led-amber);
563
1075
  }
564
1076
 
565
- /** Red for an fp-led. */
1077
+ /** Red for an fp-led or fp-readout. */
566
1078
  @utility fp-led-red {
567
1079
  --fp-led: var(--fp-color-led-red);
568
1080
  }
569
1081
 
570
- /** Lights an fp-led in its colour, with a glow. Say what it means in text nearby, never by colour alone. */
1082
+ /**
1083
+ * Lights an fp-led in its colour, brightest over the chip in its middle, with a glow. The lens sets
1084
+ * how strong both are. Say what it means in text nearby, never by colour alone.
1085
+ */
571
1086
  @utility fp-led-lit {
572
1087
  background-color: var(--fp-led, var(--fp-color-led-on));
1088
+ background-image: radial-gradient(
1089
+ closest-side,
1090
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) calc((1 - var(--fp-led-core, 0.3)) * 100%), white),
1091
+ var(--fp-led, var(--fp-color-led-on))
1092
+ );
573
1093
  box-shadow:
574
1094
  0 0 0 2px var(--fp-color-housing),
575
- 0 0 7px 2px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, transparent),
1095
+ 0 0 calc(7px * var(--fp-led-glow, 1)) calc(2px * var(--fp-led-glow, 1))
1096
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, transparent),
576
1097
  inset 0 -1px 1px rgb(0 0 0 / 0.2);
1098
+
1099
+ /* Lit is filled, so it reads apart from unlit by shape as well as colour. */
1100
+ @media (forced-colors: active) {
1101
+ forced-color-adjust: none;
1102
+ background: CanvasText;
1103
+ box-shadow: none;
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * LED lens, flat: an even fill with no hot spot. A lens class works on an fp-led, fp-meter or
1109
+ * fp-readout, or on any element around them, such as a device body, to set every one inside.
1110
+ */
1111
+ @utility fp-lens-flat {
1112
+ --fp-led-core: 0;
1113
+ --fp-led-glow: 1;
1114
+ }
1115
+
1116
+ /**
1117
+ * LED lens, diffused, the default: like most real displays, each LED is a little brighter over its
1118
+ * chip, in the middle, with a soft glow.
1119
+ */
1120
+ @utility fp-lens-diffused {
1121
+ --fp-led-core: 0.3;
1122
+ --fp-led-glow: 1;
1123
+ }
1124
+
1125
+ /** LED lens, clear: a bright, pale core over each chip and a wider glow, like a water-clear lens. */
1126
+ @utility fp-lens-clear {
1127
+ --fp-led-core: 0.5;
1128
+ --fp-led-glow: 1.5;
577
1129
  }
578
1130
 
579
1131
  /** Decorative screw head. Give it aria-hidden="true". */
@@ -591,6 +1143,7 @@
591
1143
 
592
1144
  /** A scrolling panel, such as a list window, with a thin scrollbar that never pushes the layout. */
593
1145
  @utility fp-scroll-thin {
1146
+ color-scheme: var(--fp-scheme);
594
1147
  scrollbar-width: thin;
595
1148
  scrollbar-color: var(--fp-color-scroll-thumb) transparent;
596
1149
  }
package/tokens.css CHANGED
@@ -1,6 +1,7 @@
1
1
  /* Generated by scripts/build.ts from src/. Do not edit; edit the source and run `pnpm build`. */
2
2
  @layer frontpanel {
3
3
  :root, [data-fp-skin="silver"] {
4
+ --fp-scheme: light;
4
5
  --fp-color-chrome-hi: #ffffff;
5
6
  --fp-color-chrome-100: #e8ecf2;
6
7
  --fp-color-chrome-300: #b9c2cf;
@@ -46,7 +47,7 @@
46
47
  --fp-color-gel-lo: #113c80;
47
48
  --fp-color-gel-ink: #ffffff;
48
49
  --fp-color-focus: #4fd2ff;
49
- --fp-color-focus-ring: #0a3d62;
50
+ --fp-color-focus-ring: #1a56b0;
50
51
  --fp-color-select: #1a1f29;
51
52
  --fp-color-error-text: #b3261e;
52
53
  --fp-color-backdrop: #a3b0c3;
@@ -58,6 +59,7 @@
58
59
 
59
60
  @media (prefers-color-scheme: dark) {
60
61
  :root:not([data-fp-skin="silver"]) {
62
+ --fp-scheme: dark;
61
63
  --fp-color-chrome-hi: #a3adbc;
62
64
  --fp-color-chrome-100: #4b5361;
63
65
  --fp-color-chrome-300: #363c47;
@@ -87,7 +89,7 @@
87
89
  --fp-color-cap: #d5dae1;
88
90
  --fp-color-cap-graphite: #5a6373;
89
91
  --fp-color-focus-ring: #4fd2ff;
90
- --fp-color-select: #ffb547;
92
+ --fp-color-select: #e7eaf0;
91
93
  --fp-color-error-text: #ff8a80;
92
94
  --fp-color-backdrop: #0d1016;
93
95
  --fp-color-backdrop-hi: #1c2230;
@@ -95,6 +97,7 @@
95
97
  }
96
98
 
97
99
  [data-fp-skin="gunmetal"] {
100
+ --fp-scheme: dark;
98
101
  --fp-color-chrome-hi: #a3adbc;
99
102
  --fp-color-chrome-100: #4b5361;
100
103
  --fp-color-chrome-300: #363c47;
@@ -124,7 +127,7 @@
124
127
  --fp-color-cap: #d5dae1;
125
128
  --fp-color-cap-graphite: #5a6373;
126
129
  --fp-color-focus-ring: #4fd2ff;
127
- --fp-color-select: #ffb547;
130
+ --fp-color-select: #e7eaf0;
128
131
  --fp-color-error-text: #ff8a80;
129
132
  --fp-color-backdrop: #0d1016;
130
133
  --fp-color-backdrop-hi: #1c2230;