studio-engine 0.129.0 → 0.131.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +192 -0
- data/README.md +48 -6
- data/app/assets/tailwind/studio_engine/engine-motion.css +322 -56
- data/app/assets/tailwind/studio_engine/engine.css +737 -19
- data/app/assets/tailwind/studio_engine/theme.css +4 -0
- data/app/controllers/concerns/studio/error_handling.rb +53 -15
- data/app/controllers/concerns/studio/magic_link_issuing.rb +4 -0
- data/app/controllers/magic_links_controller.rb +25 -2
- data/app/controllers/registrations_controller.rb +58 -22
- data/app/controllers/style_controller.rb +6 -0
- data/app/helpers/studio/agent_chat_helper.rb +14 -1
- data/app/helpers/studio/crumbs_helper.rb +34 -0
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/helpers/studio/loader_helper.rb +79 -2
- data/app/javascript/studio/agent_chat.js +294 -6
- data/app/javascript/studio/carousel.js +99 -0
- data/app/javascript/studio/controllers/carousel_controller.js +138 -0
- data/app/javascript/studio/controllers/infinite_scroll_controller.js +17 -0
- data/app/javascript/studio/controllers/pull_to_refresh_controller.js +18 -0
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/controllers/talk_to_agent_controller.js +20 -0
- data/app/javascript/studio/infinite_scroll.js +142 -0
- data/app/javascript/studio/loader.js +6 -1
- data/app/javascript/studio/page_transitions.js +65 -8
- data/app/javascript/studio/pinned_stack.js +112 -4
- data/app/javascript/studio/pull_to_refresh.js +147 -0
- data/app/javascript/studio/stimulus.js +12 -1
- data/app/javascript/studio/talk_to_agent.js +116 -0
- data/app/views/components/_geo_signpost.html.erb +2 -2
- data/app/views/components/_link_sidebar.html.erb +2 -2
- data/app/views/components/_link_sidebar_trigger.html.erb +1 -1
- data/app/views/components/_sidebar_panel.html.erb +1 -1
- data/app/views/error_logs/index.html.erb +2 -8
- data/app/views/studio/_carousel.html.erb +97 -0
- data/app/views/studio/_fizz.html.erb +26 -0
- data/app/views/studio/_fizz_layer.html.erb +4 -1
- data/app/views/studio/_hold_button.html.erb +1 -2
- data/app/views/studio/_page_header.html.erb +55 -13
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/studio/agent_chat/_chat.html.erb +7 -4
- data/app/views/studio/agent_chat/_talk_modal.html.erb +58 -0
- data/app/views/studio/agent_chat/_turn.html.erb +1 -1
- data/app/views/studio/loader/_infinite_scroll.html.erb +42 -0
- data/app/views/studio/loader/_loader.html.erb +20 -3
- data/app/views/studio/loader/_pull_to_refresh.html.erb +20 -0
- data/app/views/studio/loader/_skeleton.html.erb +37 -0
- data/app/views/style/_components.html.erb +3 -5
- data/app/views/style/_host.html.erb +3 -6
- data/app/views/style/_loaders.html.erb +118 -0
- data/app/views/style/_loaders_feed_page.html.erb +16 -0
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +15 -5
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_specimen.html.erb +21 -10
- data/app/views/style/_tasks.html.erb +3 -5
- data/app/views/style/_theme.html.erb +2 -2
- data/app/views/style/_tricks.html.erb +218 -217
- data/app/views/style/index.html.erb +21 -11
- data/app/views/style/loaders_feed.html.erb +7 -0
- data/lib/studio/crumbs.rb +150 -0
- data/lib/studio/loader.rb +30 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +3 -0
- metadata +22 -2
|
@@ -308,6 +308,13 @@ body.modal-open .studio-app-banner {
|
|
|
308
308
|
font-size: calc(var(--type-base, 16px) * 1.5);
|
|
309
309
|
font-weight: var(--type-display-weight, 800);
|
|
310
310
|
line-height: 1.25;
|
|
311
|
+
|
|
312
|
+
/* From 40rem, the h2 of the type scale (type-h2), Alex McRitchie's "H2 header"
|
|
313
|
+
(2026-11-05). A phone keeps 1.5x, so a long title leaves the tabs room. */
|
|
314
|
+
@media (width >= 40rem) {
|
|
315
|
+
font-size: 30px;
|
|
316
|
+
line-height: 1.2;
|
|
317
|
+
}
|
|
311
318
|
min-width: 0;
|
|
312
319
|
overflow-wrap: anywhere;
|
|
313
320
|
display: -webkit-box;
|
|
@@ -340,6 +347,11 @@ body.modal-open .studio-app-banner {
|
|
|
340
347
|
min-width: 0;
|
|
341
348
|
overflow-x: auto;
|
|
342
349
|
scrollbar-width: none;
|
|
350
|
+
/* A sideways scroller clips both axes, so the pills get room inside it: their contact
|
|
351
|
+
shadow (3px below), the lit edge (1px) and a focus ring (2px). The negative margin
|
|
352
|
+
gives the room back, so the row's box does not move. */
|
|
353
|
+
padding: 0.25rem;
|
|
354
|
+
margin: -0.25rem;
|
|
343
355
|
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--page-header-fade-start), #000 calc(100% - var(--page-header-fade-end)), transparent 100%);
|
|
344
356
|
mask-image: linear-gradient(to right, transparent 0, #000 var(--page-header-fade-start), #000 calc(100% - var(--page-header-fade-end)), transparent 100%);
|
|
345
357
|
|
|
@@ -365,9 +377,652 @@ body.modal-open .studio-app-banner {
|
|
|
365
377
|
|
|
366
378
|
@utility page-header-actions {
|
|
367
379
|
flex-shrink: 0;
|
|
380
|
+
/* Never wider than the row: a group of controls wraps inside it on a phone, and the
|
|
381
|
+
page does not scroll sideways. */
|
|
382
|
+
max-width: 100%;
|
|
383
|
+
margin-left: auto;
|
|
384
|
+
|
|
385
|
+
/* After the tabs, the tabs' own auto margin already pushes both right. */
|
|
386
|
+
.page-header-tabs + & {
|
|
387
|
+
margin-left: 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* THE TRAIL: the page's ancestors above the title (docs/PAGE_HEADER.md, "Crumbs"). It takes
|
|
392
|
+
the row's whole first line, so the title, the tabs and the actions share the next one.
|
|
393
|
+
Ancestors are muted links, the page itself is bold, and a chevron sits between. Below
|
|
394
|
+
40rem the trail gives way to one link to the parent. The negative margin brings the
|
|
395
|
+
title up to the trail: the row's gap is for the title and the tabs. */
|
|
396
|
+
@utility page-header-trail {
|
|
397
|
+
flex: 1 0 100%;
|
|
398
|
+
min-width: 0;
|
|
399
|
+
margin-bottom: -0.5rem;
|
|
400
|
+
font-size: calc(var(--type-base, 16px) * 0.8125);
|
|
401
|
+
line-height: 1.25;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@utility page-header-crumbs {
|
|
405
|
+
min-width: 0;
|
|
406
|
+
|
|
407
|
+
& ol {
|
|
408
|
+
display: flex;
|
|
409
|
+
flex-wrap: wrap;
|
|
410
|
+
align-items: center;
|
|
411
|
+
column-gap: 0.5rem;
|
|
412
|
+
row-gap: 0.125rem;
|
|
413
|
+
margin: 0;
|
|
414
|
+
padding: 0;
|
|
415
|
+
list-style: none;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
& li {
|
|
419
|
+
display: inline-flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: 0.5rem;
|
|
422
|
+
min-width: 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* The chevron is CSS, with empty alt text, so a reader hears the names alone. */
|
|
426
|
+
& li + li::before {
|
|
427
|
+
content: "\203A";
|
|
428
|
+
content: "\203A" / "";
|
|
429
|
+
@apply text-muted;
|
|
430
|
+
opacity: 0.6;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@media (width < 40rem) {
|
|
434
|
+
display: none;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@utility page-header-crumb {
|
|
439
|
+
@apply text-muted;
|
|
440
|
+
text-decoration: none;
|
|
441
|
+
transition: color 150ms ease;
|
|
442
|
+
|
|
443
|
+
&:hover,
|
|
444
|
+
&:focus-visible {
|
|
445
|
+
@apply text-heading;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
@utility page-header-crumb-current {
|
|
450
|
+
@apply text-heading;
|
|
451
|
+
font-weight: 600;
|
|
452
|
+
overflow-wrap: anywhere;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@utility page-header-parent {
|
|
456
|
+
display: none;
|
|
457
|
+
@apply text-muted;
|
|
458
|
+
font-size: calc(var(--type-base, 16px) * 0.875);
|
|
459
|
+
text-decoration: none;
|
|
460
|
+
|
|
461
|
+
&:hover,
|
|
462
|
+
&:focus-visible {
|
|
463
|
+
@apply text-heading;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@media (width < 40rem) {
|
|
467
|
+
display: inline-flex;
|
|
468
|
+
gap: 0.25rem;
|
|
469
|
+
align-items: baseline;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* THE LINKS LINE: the page's other links, on a line of their own at the foot of the row, so
|
|
474
|
+
they stick with it. Muted, small, one line; on a phone it scrolls sideways inside the row. */
|
|
475
|
+
@utility page-header-links {
|
|
476
|
+
flex: 1 0 100%;
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
gap: 1rem;
|
|
480
|
+
min-width: 0;
|
|
481
|
+
margin-top: -0.25rem;
|
|
482
|
+
overflow-x: auto;
|
|
483
|
+
scrollbar-width: none;
|
|
484
|
+
white-space: nowrap;
|
|
485
|
+
font-size: calc(var(--type-base, 16px) * 0.8125);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@utility page-header-link {
|
|
489
|
+
@apply text-muted;
|
|
490
|
+
text-decoration: none;
|
|
491
|
+
transition: color 150ms ease;
|
|
492
|
+
|
|
493
|
+
&:hover,
|
|
494
|
+
&:focus-visible,
|
|
495
|
+
&[aria-current] {
|
|
496
|
+
@apply text-heading;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* More than colour marks the current link (WCAG 1.4.1). */
|
|
500
|
+
&[aria-current] {
|
|
501
|
+
font-weight: 600;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* The tab's label sits above the gliding pill. */
|
|
506
|
+
@utility page-header-tab-label {
|
|
507
|
+
position: relative;
|
|
508
|
+
z-index: 2;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* The pill is hidden until the browser can anchor it (below). */
|
|
512
|
+
@utility page-header-glide {
|
|
513
|
+
display: none;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* -- The gliding pill -------------------------------------------------------
|
|
517
|
+
Alex McRitchie, 2026-11-05: the current tab's pill glides to the next current tab, as
|
|
518
|
+
one highlight, instead of each tab switching its own fill. One pill per row
|
|
519
|
+
(.page-header-glide, the lit primary button's own classes, so it keeps that look in
|
|
520
|
+
light, dark and glass) is anchored with CSS anchor positioning to whichever tab carries
|
|
521
|
+
aria-current. aria-current stays the one source of truth: the scroll spy and a page's
|
|
522
|
+
own markup set it, and the pill follows with no script measuring anything. Its insets
|
|
523
|
+
transition, so a change of anchor slides it. Reduced motion jumps.
|
|
524
|
+
|
|
525
|
+
The tab with aria-current gives up its own fill so the pill shows through, and the
|
|
526
|
+
labels sit above the pill. !important on the fill: the glass lit button's selectors are
|
|
527
|
+
heavier than any a row should repeat.
|
|
528
|
+
|
|
529
|
+
Where anchor positioning or :has is missing, nothing in this block applies and each tab
|
|
530
|
+
keeps its own fill, as before. */
|
|
531
|
+
@supports (anchor-name: --a) and (position-anchor: --a) and selector(:has(a)) {
|
|
532
|
+
.page-header-track {
|
|
533
|
+
position: relative;
|
|
534
|
+
/* Each row's pill anchors to its own current tab, not the last one on the page. */
|
|
535
|
+
anchor-scope: --page-header-current;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.page-header-track > [aria-current] {
|
|
539
|
+
anchor-name: --page-header-current;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.page-header-track:has(> .page-header-tab[aria-current]) > .page-header-glide {
|
|
543
|
+
display: block;
|
|
544
|
+
position: absolute;
|
|
545
|
+
position-anchor: --page-header-current;
|
|
546
|
+
top: anchor(top);
|
|
547
|
+
right: anchor(right);
|
|
548
|
+
bottom: anchor(bottom);
|
|
549
|
+
left: anchor(left);
|
|
550
|
+
z-index: 1;
|
|
551
|
+
margin: 0;
|
|
552
|
+
padding: 0;
|
|
553
|
+
pointer-events: none;
|
|
554
|
+
transition: top 320ms cubic-bezier(0.2, 0.8, 0.2, 1), right 320ms cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
555
|
+
bottom 320ms cubic-bezier(0.2, 0.8, 0.2, 1), left 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.page-header-track:has(> .page-header-glide) > .page-header-tab[aria-current] {
|
|
559
|
+
background-color: transparent !important;
|
|
560
|
+
background-image: none !important;
|
|
561
|
+
border-color: transparent !important;
|
|
562
|
+
box-shadow: none !important;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@media (prefers-reduced-motion: reduce) {
|
|
566
|
+
.page-header-track > .page-header-glide {
|
|
567
|
+
transition: none !important;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* -- The clear border glow (.studio-border-glow--clear) ------------------------
|
|
573
|
+
Alex McRitchie, 2026-11-05: "a clear background and the glowing circumference". A
|
|
574
|
+
modifier on .studio-border-glow (engine-motion.css), named as .sheen--loop is: the
|
|
575
|
+
button has no fill, so the page shows through, and only the moving rainbow ring and
|
|
576
|
+
its soft halo draw. The label is the heading ink, which clears 4.5:1 on every surface
|
|
577
|
+
(e2e/border_glow_clear.spec.js). Pair it with .btn alone, not a filled variant.
|
|
578
|
+
|
|
579
|
+
<button class="btn studio-border-glow studio-border-glow--clear">Save</button> */
|
|
580
|
+
.btn.studio-border-glow--clear {
|
|
581
|
+
@apply text-heading;
|
|
582
|
+
background-color: transparent;
|
|
583
|
+
background-image: none;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.btn.studio-border-glow--clear:not(:disabled, [aria-disabled="true"]):hover {
|
|
587
|
+
--studio-border-glow-halo-opacity: 0.4;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* Reduced motion: the ring and the halo stay, still. */
|
|
591
|
+
@media (prefers-reduced-motion: reduce) {
|
|
592
|
+
.studio-border-glow::before,
|
|
593
|
+
.studio-border-glow::after {
|
|
594
|
+
animation: none;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* -- The carousel (studio/_carousel) -----------------------------------------
|
|
599
|
+
One sideways row of slides on a native scroll-snap track: a swipe, a trackpad and
|
|
600
|
+
a focused link move it with no script. The carousel controller adds the edge fade,
|
|
601
|
+
the arrows, the dots and the keys (studio/controllers/carousel_controller).
|
|
602
|
+
|
|
603
|
+
WIDTHS COME FROM THE REGION, NOT THE WINDOW. The region is an inline-size container,
|
|
604
|
+
so a carousel in a narrow column lays out as a phone does. A phone row shows one
|
|
605
|
+
slide, a medium row up to two, a wide row --carousel-per-view (the partial's
|
|
606
|
+
per_view). Every row adds --carousel-peek of the next slide, so a visitor sees
|
|
607
|
+
there is more. n slides and their n gaps plus the peek fill the row:
|
|
608
|
+
n * w + n * gap + peek * w = 100 percent.
|
|
609
|
+
|
|
610
|
+
BEFORE THE CONTROLLER CONNECTS, or when it failed to load, the controls are hidden
|
|
611
|
+
(they would do nothing) and the track keeps a thin scrollbar, so the row still
|
|
612
|
+
scrolls with a mouse. */
|
|
613
|
+
@utility carousel {
|
|
614
|
+
--carousel-gap: 1rem;
|
|
615
|
+
--carousel-peek: 0.2;
|
|
616
|
+
--carousel-fade: 2.5rem;
|
|
617
|
+
position: relative;
|
|
618
|
+
min-width: 0;
|
|
619
|
+
container-type: inline-size;
|
|
620
|
+
|
|
621
|
+
/* The track's focus ring is drawn here: the track's edge mask clips anything
|
|
622
|
+
outside its own box, an outline included. */
|
|
623
|
+
&:has(> .carousel-track:focus-visible) {
|
|
624
|
+
outline: 2px solid color-mix(in srgb, var(--color-cta) 70%, transparent);
|
|
625
|
+
outline-offset: 2px;
|
|
626
|
+
border-radius: 0.75rem;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
@utility carousel-track {
|
|
631
|
+
--carousel-n: min(var(--carousel-per-view, 3), 1);
|
|
632
|
+
--carousel-fade-start: 0px;
|
|
633
|
+
--carousel-fade-end: 0px;
|
|
634
|
+
position: relative;
|
|
635
|
+
display: flex;
|
|
636
|
+
gap: var(--carousel-gap);
|
|
637
|
+
overflow-x: auto;
|
|
638
|
+
overscroll-behavior-x: contain;
|
|
639
|
+
scroll-snap-type: x mandatory;
|
|
640
|
+
scroll-behavior: smooth;
|
|
641
|
+
scrollbar-width: none;
|
|
642
|
+
/* Room for a slide's hover shadow and focus ring: the scroll box clips both ways. */
|
|
643
|
+
padding-block: 0.25rem 0.5rem;
|
|
644
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--carousel-fade-start), #000 calc(100% - var(--carousel-fade-end)), transparent 100%);
|
|
645
|
+
mask-image: linear-gradient(to right, transparent 0, #000 var(--carousel-fade-start), #000 calc(100% - var(--carousel-fade-end)), transparent 100%);
|
|
646
|
+
|
|
647
|
+
&::-webkit-scrollbar {
|
|
648
|
+
display: none;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
&[data-fade-start] {
|
|
652
|
+
--carousel-fade-start: var(--carousel-fade);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
&[data-fade-end] {
|
|
656
|
+
--carousel-fade-end: var(--carousel-fade);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
&:focus-visible {
|
|
660
|
+
outline: none;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.carousel:not([data-carousel-ready]) & {
|
|
664
|
+
scrollbar-width: thin;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.carousel:not([data-carousel-ready]) &::-webkit-scrollbar {
|
|
668
|
+
display: initial;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@container (width >= 36rem) {
|
|
672
|
+
--carousel-n: min(var(--carousel-per-view, 3), 2);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
@container (width >= 56rem) {
|
|
676
|
+
--carousel-n: var(--carousel-per-view, 3);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@media (prefers-reduced-motion: reduce) {
|
|
680
|
+
scroll-behavior: auto;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
@utility carousel-slide {
|
|
685
|
+
flex: 0 0 calc((100% - var(--carousel-n) * var(--carousel-gap)) / (var(--carousel-n) + var(--carousel-peek)));
|
|
686
|
+
min-width: 0;
|
|
687
|
+
scroll-snap-align: start;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@utility carousel-controls {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
justify-content: space-between;
|
|
694
|
+
gap: 0.75rem;
|
|
695
|
+
margin-top: 0.75rem;
|
|
696
|
+
|
|
697
|
+
/* Until the controller connects the row is hidden but keeps its room, so nothing
|
|
698
|
+
below moves when the arrows arrive (a hash landing, CLS). A row that fits, or a
|
|
699
|
+
controller that failed to load, gives the room back. */
|
|
700
|
+
.carousel:not([data-carousel-ready]) & {
|
|
701
|
+
visibility: hidden;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.carousel[data-carousel-static] &,
|
|
705
|
+
.carousel[data-studio-controller-failed] & {
|
|
706
|
+
display: none;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
@utility carousel-dots {
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-wrap: wrap;
|
|
713
|
+
align-items: center;
|
|
714
|
+
min-width: 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* A 1.5rem tap target around a small pill. The open stop's pill is wider and wears
|
|
718
|
+
the primary, so the dots read as a progress line. */
|
|
719
|
+
@utility carousel-dot {
|
|
720
|
+
display: inline-flex;
|
|
721
|
+
align-items: center;
|
|
722
|
+
justify-content: center;
|
|
723
|
+
height: 1.5rem;
|
|
724
|
+
padding-inline: 0.1875rem;
|
|
725
|
+
border-radius: 9999px;
|
|
726
|
+
cursor: pointer;
|
|
727
|
+
|
|
728
|
+
&::before {
|
|
729
|
+
content: "";
|
|
730
|
+
display: block;
|
|
731
|
+
width: 0.5rem;
|
|
732
|
+
height: 0.5rem;
|
|
733
|
+
border-radius: 9999px;
|
|
734
|
+
background-color: color-mix(in srgb, var(--color-text-muted) 45%, transparent);
|
|
735
|
+
transition: width 200ms ease, background-color 200ms ease;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
&:hover::before {
|
|
739
|
+
background-color: var(--color-text-muted);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
&[aria-current="true"]::before {
|
|
743
|
+
width: 1.5rem;
|
|
744
|
+
background-color: var(--color-primary);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
&[hidden] {
|
|
748
|
+
display: none;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
&:focus-visible {
|
|
752
|
+
outline: 2px solid color-mix(in srgb, var(--color-cta) 70%, transparent);
|
|
753
|
+
outline-offset: 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@media (prefers-reduced-motion: reduce) {
|
|
757
|
+
&::before {
|
|
758
|
+
transition: none;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
@utility carousel-arrows {
|
|
764
|
+
display: flex;
|
|
765
|
+
flex-shrink: 0;
|
|
766
|
+
gap: 0.5rem;
|
|
368
767
|
margin-left: auto;
|
|
369
768
|
}
|
|
370
769
|
|
|
770
|
+
/* Round arrows on the card fill, so under the glass surfaces they are glass too. */
|
|
771
|
+
@utility carousel-button {
|
|
772
|
+
@apply border border-subtle text-heading;
|
|
773
|
+
display: inline-flex;
|
|
774
|
+
align-items: center;
|
|
775
|
+
justify-content: center;
|
|
776
|
+
width: 2.5rem;
|
|
777
|
+
height: 2.5rem;
|
|
778
|
+
border-radius: 9999px;
|
|
779
|
+
background-color: var(--surface-card-fill, var(--color-surface));
|
|
780
|
+
-webkit-backdrop-filter: var(--surface-glass-filter, none);
|
|
781
|
+
backdrop-filter: var(--surface-glass-filter, none);
|
|
782
|
+
box-shadow: var(--surface-glass-highlight, 0 0 #0000), var(--surface-glass-shadow, 0 0 #0000);
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
transition: border-color 150ms ease, opacity 150ms ease, transform 150ms ease;
|
|
785
|
+
|
|
786
|
+
& svg {
|
|
787
|
+
width: 1.125rem;
|
|
788
|
+
height: 1.125rem;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
&:hover:not([aria-disabled="true"]) {
|
|
792
|
+
@apply border-primary;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
&:active:not([aria-disabled="true"]) {
|
|
796
|
+
transform: scale(0.94);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
&[aria-disabled="true"] {
|
|
800
|
+
opacity: 0.4;
|
|
801
|
+
cursor: default;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
&:focus-visible {
|
|
805
|
+
outline: 2px solid color-mix(in srgb, var(--color-cta) 70%, transparent);
|
|
806
|
+
outline-offset: 2px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
@media (prefers-reduced-motion: reduce) {
|
|
810
|
+
transition: none;
|
|
811
|
+
|
|
812
|
+
&:active:not([aria-disabled="true"]) {
|
|
813
|
+
transform: none;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/* -- The spotlight carousel (studio/_carousel, style: :spotlight) --------------
|
|
819
|
+
The same region, track, arrows, dots and keys, but each slide snaps to the centre.
|
|
820
|
+
The centred slide is full size and full colour, with its caption under it; its
|
|
821
|
+
neighbours are smaller, faded and grey, and the next ones peek at the edges.
|
|
822
|
+
|
|
823
|
+
WIDTHS. A slide is --carousel-spot-w wide, in units of the region (cqi, since the
|
|
824
|
+
region is the inline-size container). n slides and a 0.6 peek fill the row:
|
|
825
|
+
n * w + (n - 1) * gap + 0.6 * w = 100cqi. The track pads each end by half the
|
|
826
|
+
rest, so the first and the last slide can centre too.
|
|
827
|
+
|
|
828
|
+
THE EFFECT FOLLOWS THE SCROLL where the browser has scroll-driven animations:
|
|
829
|
+
each slide is its own inline view timeline, and its body and caption run one
|
|
830
|
+
animation across the stretch from one neighbour's place to the other's (a step,
|
|
831
|
+
a slide and a gap, either side of the centre), full at the middle. Outside that
|
|
832
|
+
stretch the fill holds the neighbour look.
|
|
833
|
+
|
|
834
|
+
ELSEWHERE, the controller marks the centred slide (data-current) and the body
|
|
835
|
+
transitions to it. Until the controller connects every slide stays plain.
|
|
836
|
+
|
|
837
|
+
UNDER REDUCED MOTION nothing scales, greys or animates: the neighbours of the
|
|
838
|
+
marked slide are only dimmed. */
|
|
839
|
+
@keyframes carousel-spotlight {
|
|
840
|
+
0%, 100% {
|
|
841
|
+
scale: var(--carousel-spot-scale);
|
|
842
|
+
opacity: var(--carousel-spot-opacity);
|
|
843
|
+
filter: grayscale(1);
|
|
844
|
+
}
|
|
845
|
+
50% {
|
|
846
|
+
scale: 1;
|
|
847
|
+
opacity: 1;
|
|
848
|
+
filter: grayscale(0);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
@keyframes carousel-spotlight-caption {
|
|
853
|
+
0%, 30%, 70%, 100% {
|
|
854
|
+
opacity: 0;
|
|
855
|
+
translate: 0 0.25rem;
|
|
856
|
+
}
|
|
857
|
+
50% {
|
|
858
|
+
opacity: 1;
|
|
859
|
+
translate: 0 0;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
@utility carousel-spotlight {
|
|
864
|
+
--carousel-spot-scale: 0.75;
|
|
865
|
+
--carousel-spot-opacity: 0.45;
|
|
866
|
+
|
|
867
|
+
/* On the track, not the region: the track's container queries set --carousel-n,
|
|
868
|
+
and a container query cannot read the element it sits on. */
|
|
869
|
+
& > .carousel-track {
|
|
870
|
+
/* A smaller gap: the neighbours' scale already opens space around the centre. */
|
|
871
|
+
--carousel-gap: 0.5rem;
|
|
872
|
+
--carousel-spot-w: calc((100cqi - (var(--carousel-n) - 1) * var(--carousel-gap)) / (var(--carousel-n) + 0.6));
|
|
873
|
+
--carousel-spot-step: calc(var(--carousel-spot-w) + var(--carousel-gap));
|
|
874
|
+
padding-inline: calc((100cqi - var(--carousel-spot-w)) / 2);
|
|
875
|
+
/* The neighbours are already dimmed; an edge fade over them reads as a smear. */
|
|
876
|
+
-webkit-mask-image: none;
|
|
877
|
+
mask-image: none;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
& > .carousel-track > .carousel-slide {
|
|
881
|
+
flex: 0 0 var(--carousel-spot-w);
|
|
882
|
+
scroll-snap-align: center;
|
|
883
|
+
display: flex;
|
|
884
|
+
flex-direction: column;
|
|
885
|
+
view-timeline: --carousel-slide inline;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
& .carousel-spotlight-body {
|
|
889
|
+
flex: 1 1 auto;
|
|
890
|
+
min-height: 0;
|
|
891
|
+
transform-origin: 50% 50%;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
& .carousel-caption {
|
|
895
|
+
margin-top: 0.75rem;
|
|
896
|
+
text-align: center;
|
|
897
|
+
@apply text-sm text-body;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/* The fallback: the controller's mark, with a transition. */
|
|
901
|
+
&[data-carousel-ready] .carousel-slide .carousel-spotlight-body {
|
|
902
|
+
transition: scale 300ms ease, opacity 300ms ease, filter 300ms ease;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
&[data-carousel-ready] .carousel-slide:not([data-current]) .carousel-spotlight-body {
|
|
906
|
+
scale: var(--carousel-spot-scale);
|
|
907
|
+
opacity: var(--carousel-spot-opacity);
|
|
908
|
+
filter: grayscale(1);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
&[data-carousel-ready] .carousel-caption {
|
|
912
|
+
transition: opacity 300ms ease;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
&[data-carousel-ready] .carousel-slide:not([data-current]) .carousel-caption {
|
|
916
|
+
opacity: 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* A neighbour holding focus drops the neighbour look on every path, so its focus
|
|
920
|
+
ring shows at full strength (WCAG 2.4.7). It outranks the animation and the mark. */
|
|
921
|
+
& .carousel-slide:focus-within .carousel-spotlight-body,
|
|
922
|
+
&[data-carousel-ready] .carousel-slide:focus-within .carousel-spotlight-body {
|
|
923
|
+
animation: none;
|
|
924
|
+
scale: 1;
|
|
925
|
+
opacity: 1;
|
|
926
|
+
filter: none;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* The scroll-driven path. It needs no controller, so it runs from the first paint. */
|
|
930
|
+
@supports (animation-timeline: view()) and (animation-range: cover 0% cover 100%) {
|
|
931
|
+
& .carousel-spotlight-body,
|
|
932
|
+
& .carousel-caption {
|
|
933
|
+
transition: none;
|
|
934
|
+
animation: carousel-spotlight linear both;
|
|
935
|
+
animation-timeline: --carousel-slide;
|
|
936
|
+
animation-range: cover calc(50% - var(--carousel-spot-step)) cover calc(50% + var(--carousel-spot-step));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
& .carousel-caption {
|
|
940
|
+
animation-name: carousel-spotlight-caption;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
@media (prefers-reduced-motion: reduce) {
|
|
945
|
+
& .carousel-spotlight-body,
|
|
946
|
+
& .carousel-caption {
|
|
947
|
+
animation: none !important;
|
|
948
|
+
transition: none !important;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
&[data-carousel-ready] .carousel-slide:not([data-current]) .carousel-spotlight-body {
|
|
952
|
+
scale: none;
|
|
953
|
+
filter: none;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/* -- The section header (studio/_section_header, docs/PAGE_HEADER.md) --------
|
|
959
|
+
A section's title that sticks under the rest of the stack, inside its own <section>,
|
|
960
|
+
so the next section's header takes its place with no scroll script. At rest it reads as
|
|
961
|
+
a normal title. Stuck it takes the page header's bar, so content does not show through
|
|
962
|
+
it. studio/pinned_stack says which one is stuck. Its box never changes between the two,
|
|
963
|
+
so the change moves nothing. One line, always: a long title ends in an ellipsis on the
|
|
964
|
+
bar, the height is known, and the whole text stays in the heading.
|
|
965
|
+
|
|
966
|
+
IT STICKS ONLY ONCE THE PUBLISHER RUNS (html[data-pin-live]). With no publisher no bar
|
|
967
|
+
ever comes, and a clear title would lie over the page; in the flow it is a plain title.
|
|
968
|
+
|
|
969
|
+
A SHORT SCREEN KEEPS ITS ROOM. Under 36rem tall, a small phone or one on its side, the
|
|
970
|
+
header stays in the flow: the navbar and the page row already take a quarter of the height.
|
|
971
|
+
|
|
972
|
+
z-index is one tier under the page row, set by the partial, so a header being pushed out
|
|
973
|
+
passes under the row and over the content. --section-header-h is what a target inside a
|
|
974
|
+
section adds to its scroll margin, so it lands below the bar. */
|
|
975
|
+
:root {
|
|
976
|
+
--section-header-h: 2.25rem;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
@utility section-header {
|
|
980
|
+
@apply text-heading;
|
|
981
|
+
position: relative;
|
|
982
|
+
min-width: 0;
|
|
983
|
+
min-height: var(--section-header-h);
|
|
984
|
+
padding-block: 0.25rem;
|
|
985
|
+
margin-block-end: 0.25rem;
|
|
986
|
+
border-block: 1px solid transparent;
|
|
987
|
+
background-color: transparent;
|
|
988
|
+
font-size: calc(var(--type-base, 16px) * 1.25);
|
|
989
|
+
font-weight: 700;
|
|
990
|
+
line-height: 1.3;
|
|
991
|
+
overflow: hidden;
|
|
992
|
+
white-space: nowrap;
|
|
993
|
+
text-overflow: ellipsis;
|
|
994
|
+
transition: background-color 150ms ease, border-color 150ms ease;
|
|
995
|
+
|
|
996
|
+
html[data-pin-live] & {
|
|
997
|
+
@media (height >= 36rem) {
|
|
998
|
+
position: sticky;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
&[data-stuck] {
|
|
1003
|
+
@apply bg-surface border-subtle;
|
|
1004
|
+
border-top-color: transparent;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/* Where a jump lands. A jump goes deep, so what counts is the stack once PINNED, with the
|
|
1009
|
+
navbar compact and the page row stuck: heights, not --pin-section-top, whose value at the
|
|
1010
|
+
top of the page holds the row's rest place and whatever scrolls away above it. So this one
|
|
1011
|
+
composes, which a thing placed on screen must not: a margin is read once, at the jump. Less
|
|
1012
|
+
two pixels, so the header has left its place on a fractional height and its title shows on
|
|
1013
|
+
the bar.
|
|
1014
|
+
|
|
1015
|
+
A JUMP FROM THE TOP OF THE PAGE reads the navbar still tall, and lands by the navbar's
|
|
1016
|
+
collapse below the line. Landing short is safe, the title shows. Landing past is safe too,
|
|
1017
|
+
since the bar holds the title. A target inside a section: a bar's height lower. */
|
|
1018
|
+
@utility section-target {
|
|
1019
|
+
scroll-margin-top: calc(var(--nav-h, 5rem) + var(--pin-page-h, 0px) - 2px);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
@utility section-subtarget {
|
|
1023
|
+
scroll-margin-top: calc(var(--nav-h, 5rem) + var(--pin-page-h, 0px) + var(--section-header-h));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
371
1026
|
/* -- Scrims: tints and shades ----------------------------------------------
|
|
372
1027
|
A scrim is the wash laid over media (a photo, a video) so text reads on it.
|
|
373
1028
|
It comes in two TONES, and the tone is a design decision, not a colour pick:
|
|
@@ -1514,14 +2169,36 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1514
2169
|
}
|
|
1515
2170
|
}
|
|
1516
2171
|
|
|
2172
|
+
/* THE SECTION HEADER UNDER GLASS, the same bar one layer down. While it is stuck the bar
|
|
2173
|
+
under the row owns the round corners and the shadow: the row goes square, or the page
|
|
2174
|
+
would show in the notches between the two. */
|
|
2175
|
+
html[data-surface-style="glass"] .section-header[data-stuck] {
|
|
2176
|
+
background-color: var(--color-page);
|
|
2177
|
+
border-top-color: transparent;
|
|
2178
|
+
border-bottom-color: var(--surface-glass-line);
|
|
2179
|
+
border-radius: 0 0 0.875rem 0.875rem;
|
|
2180
|
+
box-shadow: var(--surface-glass-shadow);
|
|
2181
|
+
}
|
|
2182
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
2183
|
+
html[data-surface-style="glass"] .section-header[data-stuck] {
|
|
2184
|
+
background-color: color-mix(in srgb, var(--color-page) 62%, transparent);
|
|
2185
|
+
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
2186
|
+
backdrop-filter: blur(18px) saturate(140%);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
html[data-surface-style="glass"][data-pin-section-stuck] .page-header[data-stuck] {
|
|
2190
|
+
border-radius: 0;
|
|
2191
|
+
box-shadow: var(--surface-glass-highlight);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1517
2194
|
/* LIT FROM ABOVE, under the same opt-in: one light for buttons, badges and inputs, from the
|
|
1518
2195
|
direction of the cards' top highlight. Four shadows: top light, bottom shade, contact, and
|
|
1519
2196
|
a glow in the button's own color.
|
|
1520
2197
|
|
|
1521
|
-
THE GRADIENT KEEPS ITS CHROMA. Both stops are built in oklch from the token,
|
|
1522
|
-
and
|
|
1523
|
-
|
|
1524
|
-
|
|
2198
|
+
THE GRADIENT KEEPS ITS CHROMA. Both stops are built in oklch from the token, the same step
|
|
2199
|
+
lighter and darker at the token's own chroma; mixing toward black or grey is what mutes a
|
|
2200
|
+
button. The midpoint is the token, so white text holds 4.5:1 there on the semantic fills
|
|
2201
|
+
and about 3:1 on the default primary, which Alex McRitchie accepted to match the brand word.
|
|
1525
2202
|
|
|
1526
2203
|
WHO GETS WHAT. Every full-size filled button: gradient, top, bottom, contact and a glow in
|
|
1527
2204
|
its own color, on the same 0.15 lightness range. A .btn-sm has no glow, since a toolbar or
|
|
@@ -1569,16 +2246,17 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1569
2246
|
--lit-badge: inset 0 1px 0 rgb(255 255 255 / 0.14), 0 1px 1px rgb(0 0 0 / 0.35);
|
|
1570
2247
|
--lit-input: inset 0 1px 2px rgb(0 0 0 / 0.4), inset 0 -1px 0 rgb(255 255 255 / 0.06);
|
|
1571
2248
|
}
|
|
1572
|
-
/* Stops:
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
html[data-surface-style="glass"] :is(.btn-secondary, .btn-danger, .btn-warning, .btn-success) {
|
|
1579
|
-
--lit-top-stop: oklch(from var(--lit-fill) calc(l + 0.
|
|
1580
|
-
--lit-low-stop: oklch(from var(--lit-fill) calc(l - 0.
|
|
1581
|
-
}
|
|
2249
|
+
/* Stops: CENTERED ON THE FILL. Each stop moves lightness the same step up and down, with
|
|
2250
|
+
chroma and hue unchanged, so the gradient's midpoint (oklab interpolation) is the token
|
|
2251
|
+
itself. The primary centers on --color-cta, the brand violet a text-primary word paints,
|
|
2252
|
+
so the button and the word read as one purple. Alex McRitchie accepted white near 3:1 on
|
|
2253
|
+
the default primary for that; the semantic fills hold 4.5:1 at the midpoint. A top stop
|
|
2254
|
+
past sRGB clips only the top edge: the midpoint is interpolated before it is clipped. */
|
|
2255
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success) {
|
|
2256
|
+
--lit-top-stop: oklch(from var(--lit-fill) calc(l + 0.06) c h / var(--lit-fill-alpha, 1));
|
|
2257
|
+
--lit-low-stop: oklch(from var(--lit-fill) calc(l - 0.06) c h / var(--lit-fill-alpha, 1));
|
|
2258
|
+
}
|
|
2259
|
+
html[data-surface-style="glass"] .btn-primary { --lit-fill: var(--color-cta); }
|
|
1582
2260
|
html[data-surface-style="glass"] .btn-secondary { --lit-fill: var(--btn-secondary-bg, var(--color-success)); }
|
|
1583
2261
|
html[data-surface-style="glass"] .btn-danger { --lit-fill: var(--color-danger-fill, var(--color-danger)); }
|
|
1584
2262
|
html[data-surface-style="glass"] .btn-warning { --lit-fill: var(--color-warning-fill, var(--color-warning)); }
|
|
@@ -1598,16 +2276,16 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1598
2276
|
var(--lit-glow) oklch(from var(--lit-glow-stop, var(--lit-low-stop)) l c h / var(--lit-glow-alpha));
|
|
1599
2277
|
}
|
|
1600
2278
|
/* On a dark page a deep green, red or orange glow disappears, so these glow from the top
|
|
1601
|
-
stop. The violet is
|
|
2279
|
+
stop. The violet, centered on the brand colour, is bright enough at its low stop. */
|
|
1602
2280
|
html.dark[data-surface-style="glass"] :is(.btn-secondary, .btn-danger, .btn-warning, .btn-success) { --lit-glow-stop: var(--lit-top-stop); }
|
|
1603
2281
|
/* A TOUCH OF TRANSPARENCY on a full-size filled button, dark theme only: a photo behind it
|
|
1604
|
-
shows subtly, and white text holds
|
|
2282
|
+
shows subtly, and white text holds its contrast at the midpoint. An outer shadow is not painted
|
|
1605
2283
|
under its own box, so the glow never brightens the fill. On a light page the backdrop would
|
|
1606
2284
|
lift the fill and cost contrast, so it stays solid there. Without a blur, or without relative
|
|
1607
2285
|
oklch (the gradient would drop out and leave white text on nothing), it stays solid. */
|
|
1608
2286
|
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and (color: oklch(from red l c h)) {
|
|
1609
2287
|
html.dark[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"], [class*=" bg-"], [class^="bg-"]) {
|
|
1610
|
-
--lit-fill-alpha: 0.
|
|
2288
|
+
--lit-fill-alpha: 0.85;
|
|
1611
2289
|
background-color: transparent;
|
|
1612
2290
|
-webkit-backdrop-filter: blur(6px);
|
|
1613
2291
|
backdrop-filter: blur(6px);
|
|
@@ -1655,13 +2333,53 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1655
2333
|
html[data-surface-style="glass"] .badge { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-badge); }
|
|
1656
2334
|
html[data-surface-style="glass"] .input-field { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--lit-input); }
|
|
1657
2335
|
|
|
2336
|
+
/* BUTTON TRICKS ON A LIT BUTTON (the lit rules above).
|
|
2337
|
+
A lit button already uses its box-shadow for its contact shadow and glow, and
|
|
2338
|
+
its ::after for its inset light. These rules sit in the utilities layer and the
|
|
2339
|
+
tricks in engine-motion.css sit in components, so the lit rules win, and each trick
|
|
2340
|
+
that shares one of them makes room here. Keep a trick on a real .btn lit.
|
|
2341
|
+
|
|
2342
|
+
--btn-lit-rest is the lit button's outer shadow at rest. The pulse's keyframes
|
|
2343
|
+
replace box-shadow for the whole animation, so they put it back themselves. */
|
|
2344
|
+
html[data-surface-style="glass"] .btn:not(:disabled, [aria-disabled="true"]) {
|
|
2345
|
+
--btn-lit-rest: var(--lit-contact);
|
|
2346
|
+
}
|
|
2347
|
+
html[data-surface-style="glass"] .btn:is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"]) {
|
|
2348
|
+
--btn-lit-rest: var(--lit-contact),
|
|
2349
|
+
var(--lit-glow) oklch(from var(--lit-glow-stop, var(--lit-low-stop)) l c h / var(--lit-glow-alpha));
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/* The glows need ::after for their bloom, so the inset light moves to the host and
|
|
2353
|
+
the bloom gets its own box back. Without this the lit rule's inset: -1px closes
|
|
2354
|
+
the bloom's hole and it washes the whole face. .btn-outline has no lit ::after. */
|
|
2355
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow, .studio-border-glow):not(.btn-outline, :disabled, [aria-disabled="true"]) {
|
|
2356
|
+
box-shadow: var(--lit-inset), var(--btn-lit-rest);
|
|
2357
|
+
}
|
|
2358
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow, .studio-border-glow):not(.btn-outline, :disabled, [aria-disabled="true"]):active {
|
|
2359
|
+
box-shadow: var(--lit-inset-pressed);
|
|
2360
|
+
}
|
|
2361
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow):not(.btn-outline, :disabled, [aria-disabled="true"])::after {
|
|
2362
|
+
inset: calc(-1 * (var(--edge-rotate-glow-thickness) + 4px + var(--edge-rotate-glow-bloom) * 2));
|
|
2363
|
+
box-shadow: none;
|
|
2364
|
+
}
|
|
2365
|
+
html[data-surface-style="glass"] .btn.studio-border-glow:not(.btn-outline, :disabled, [aria-disabled="true"])::after {
|
|
2366
|
+
inset: -46px;
|
|
2367
|
+
box-shadow: none;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/* The sheen's band already stops at its own rounded box, and overflow: hidden would
|
|
2371
|
+
cut the lit ::after's outer pixel, the top highlight. */
|
|
2372
|
+
html[data-surface-style="glass"] .btn.sheen:not(:disabled, [aria-disabled="true"]) {
|
|
2373
|
+
overflow: visible;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
1658
2376
|
/* LESS TRANSPARENCY: every surface above goes solid and drops its blur. Last, to win on order;
|
|
1659
2377
|
both spellings of the root, since html.dark outranks a bare html[...] on specificity. */
|
|
1660
2378
|
@media (prefers-reduced-transparency: reduce) {
|
|
1661
2379
|
html[data-surface-style="glass"], html.dark[data-surface-style="glass"] { --surface-glass-fill: var(--color-surface); --surface-glass-fill-hover: var(--color-surface-alt); }
|
|
1662
|
-
html[data-surface-style="glass"] :is(.surface-glass-blur, .nav-shell, .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)),
|
|
2380
|
+
html[data-surface-style="glass"] :is(.surface-glass-blur, .nav-shell, .section-header[data-stuck], .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)),
|
|
1663
2381
|
html.dark[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"]) { --lit-fill-alpha: 1; -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
1664
|
-
html[data-surface-style="glass"] :is(.nav-shell, .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)) { background-color: var(--color-page); }
|
|
2382
|
+
html[data-surface-style="glass"] :is(.nav-shell, .section-header[data-stuck], .page-header:is([data-stuck], [data-studio-controller-failed~="page-header"] + .page-header)) { background-color: var(--color-page); }
|
|
1665
2383
|
}
|
|
1666
2384
|
|
|
1667
2385
|
/* THE SIDEBAR ROW'S DESCRIPTION (components/_link_sidebar, _geo_signpost). At rest
|