studio-engine 0.129.0 → 0.130.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 +41 -0
- data/README.md +5 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +148 -56
- data/app/assets/tailwind/studio_engine/engine.css +150 -19
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/pinned_stack.js +112 -4
- 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 +3 -2
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/style/_components.html.erb +3 -5
- data/app/views/style/_host.html.erb +3 -6
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +3 -5
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_specimen.html.erb +4 -0
- 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 +155 -216
- data/app/views/style/index.html.erb +7 -3
- data/lib/studio/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ce81b9c4f82a619f86521fd4495d07e8a2a81f9f1eb5f15963e26f0cb038aa
|
|
4
|
+
data.tar.gz: ed8a7169b6322c577ca796c974f4561bd3c4a2e29fa7da917cd28aec84f401b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f0f640580ed2f77fdc6093e9a90f2edfc12bf2ba6925fc8ad6b24cd5587668c84d465090be866dcf19c5f536cb38ccbff123f078a52dbee52f8b2f7d73d302d
|
|
7
|
+
data.tar.gz: c02e3417b8f8eb1bb4b6b38706ddf79561959b61cacb7317a9465825c1724097c3b77fc5c4cf131f0011dff852b0c430c90f660277fd0067100a611cec10ec55
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.130.0 — 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Section headers that stack under the page header: `studio/_section_header`.** Render it
|
|
12
|
+
as the first child of a `<section>`. The title sticks directly under the page row, takes a
|
|
13
|
+
bar (solid in light and dark, glass under glass), and the next section's header takes its
|
|
14
|
+
place, by CSS sticky and no scroll script. `/admin/style` uses it on every section,
|
|
15
|
+
including a host's own. It joins the pinned stack as one layer, `section`: a new
|
|
16
|
+
`data-pin-section`, and `--pin-section-top`, `-h` and `-bottom`. Only the stuck header
|
|
17
|
+
counts, so `--pin-stack-bottom` and a table's sticky header sit under it. New classes:
|
|
18
|
+
`section-header`, `section-target` and `section-subtarget`, which
|
|
19
|
+
replace a hand-written `scroll-margin-top`. The publisher now reads the layers once more after
|
|
20
|
+
a settle and after a layer's top moves, so a layer under a layer does not keep a stale line.
|
|
21
|
+
Detail: `docs/PAGE_HEADER.md`.
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **A lit button's gradient centers on its own colour.** Every lit variant steps lightness 0.06 up
|
|
25
|
+
and 0.06 down from its fill, with chroma and hue unchanged, so the painted midpoint is the token.
|
|
26
|
+
The lit primary now centers on `--color-cta`, the violet a `text-primary` word paints, instead of
|
|
27
|
+
sitting darker and more saturated. White on the default primary is about 3.1:1 at the midpoint;
|
|
28
|
+
Alex McRitchie accepted that so the button and the brand word read as one purple. The semantic
|
|
29
|
+
fills still hold 4.5:1 there. `/lab/hero_button` shows a `text-primary` word beside the button.
|
|
30
|
+
|
|
31
|
+
- **Tricks starts with the buttons, then what you can add to them.** A new Button tricks group
|
|
32
|
+
sits directly under Buttons: rotating edge glow, border glow, sheen, pulse, fizz, hold to confirm,
|
|
33
|
+
confetti burst and cannons. Each sits on a real `.btn` or fires from one, with its code to copy.
|
|
34
|
+
The other groups keep their order, with one short sentence each at most.
|
|
35
|
+
- **`.studio-team-glow` is now `.edge-rotate-glow`, with `--edge-rotate-glow-*` knobs.** The old class
|
|
36
|
+
and knobs are a deprecated alias and paint the same, so no host has to change.
|
|
37
|
+
- **Fizz works around any button.** `render layout: "studio/fizz", locals: { seed: "save" }` wraps a
|
|
38
|
+
button in a `.fizz-stack`. The hold button's markup and selectors are unchanged. The only tie was
|
|
39
|
+
the `.hold-stack` selector.
|
|
40
|
+
- **Trial: a dark, full-size lit button's fill drops from 0.94 to 0.85 alpha** (`--lit-fill-alpha`).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **The button tricks keep a lit button lit.** A lit button's `::after` closed the hole in the glow's
|
|
45
|
+
bloom, which washed its whole face. The sheen clipped the top highlight, and the pulse dropped the
|
|
46
|
+
lit shadow. `.studio-border-glow` on a button no longer ends its halo on a hard box edge.
|
|
47
|
+
|
|
7
48
|
## 0.129.0 — 2026-11-05
|
|
8
49
|
|
|
9
50
|
### Changed
|
data/README.md
CHANGED
|
@@ -294,6 +294,11 @@ An admin page's title: a plain header at rest, a bar once stuck under the navbar
|
|
|
294
294
|
in the pinned stack so a table's sticky header sits under it
|
|
295
295
|
([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md)).
|
|
296
296
|
|
|
297
|
+
### The section header — `studio/_section_header`
|
|
298
|
+
|
|
299
|
+
A section's title that sticks under the page row until the next section's takes its place,
|
|
300
|
+
with a bar once stuck ([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md#the-section-header)).
|
|
301
|
+
|
|
297
302
|
```erb
|
|
298
303
|
<%= render "studio/page_header", title: "Models", meta: "v1.2.3", label: "Models",
|
|
299
304
|
tabs: [{ label: "Users", href: users_path, current: "page" }, { label: "Teams", href: teams_path }] %>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Effect:
|
|
20
20
|
.text-gradient gradient-clipped text fill (authored fresh)
|
|
21
21
|
.studio-glow soft themed glow halo (authored fresh)
|
|
22
|
+
.edge-rotate-glow a glow rotating around the edge (ported, TM)
|
|
22
23
|
.surface-glass frosted translucent glass panel (authored fresh)
|
|
23
24
|
.conic-surface slow rotating conic-gradient wash (authored fresh)
|
|
24
25
|
.pulse-cta attention pulse/glow for a CTA (ported, TM)
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
Action:
|
|
27
28
|
.hold-btn press-and-hold confirm button (ported, TM)
|
|
28
29
|
.hold-stack/.fizz-bit the carbonation behind it (ported, TM)
|
|
30
|
+
.fizz-stack the same bubbles around any button (studio/_fizz)
|
|
29
31
|
.email-reject email field rejection shake + fade (extracted, TM)
|
|
30
32
|
|
|
31
33
|
Loader:
|
|
@@ -167,6 +169,16 @@
|
|
|
167
169
|
transform: translateZ(0);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/* On a button the halo needs room, as .edge-rotate-glow's bloom does. Mask applies
|
|
173
|
+
after filter, so the gradient stops at the padding box and the transparent border
|
|
174
|
+
is empty room for the blur to fade into. Without it the halo ends on a hard box. */
|
|
175
|
+
.btn.studio-border-glow::after {
|
|
176
|
+
inset: -46px;
|
|
177
|
+
padding: 10px;
|
|
178
|
+
border: 36px solid transparent;
|
|
179
|
+
background-clip: padding-box;
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
@keyframes studioBorderGlowSteam {
|
|
171
183
|
0% {
|
|
172
184
|
background-position: var(--studio-border-glow-offset, 0%) 0;
|
|
@@ -521,37 +533,32 @@
|
|
|
521
533
|
}
|
|
522
534
|
|
|
523
535
|
/* =============================================================================
|
|
524
|
-
9b.
|
|
525
|
-
Ported from turf-monster's
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
slides it between step cards as a modal's step machine advances).
|
|
542
|
-
|
|
543
|
-
THE HOST MAY CARRY ITS OWN BACKGROUND — the ring cuts the host's box out of
|
|
544
|
-
both wedge layers, so only the band around the edge paints. That hole is what
|
|
545
|
-
makes the primitive work in the two shapes its consumers need:
|
|
536
|
+
9b. edge-rotate-glow — a glow that rotates around an element's edge
|
|
537
|
+
Ported from turf-monster's selected-card glow (.holo-border-glow): a conic
|
|
538
|
+
gradient rotates around the element's edge, with a blurred bloom twin behind it.
|
|
539
|
+
DISTINCT from .studio-border-glow (an animated RAINBOW border) and .studio-glow
|
|
540
|
+
(a static soft box-shadow halo).
|
|
541
|
+
|
|
542
|
+
The color is a CSS var, default the CTA role. TWO opposed wedges travel the
|
|
543
|
+
edge: --edge-rotate-glow-color-b defaults to the first color, so a one-color
|
|
544
|
+
caller sees one color and a caller with a pair gets one per wedge. The wedges
|
|
545
|
+
are swept by animating a registered @property angle (rotating the GRADIENT, not
|
|
546
|
+
the element, so no oversized-child clipping trick is needed). Opacity is a var
|
|
547
|
+
with a transition, so a caller can fade the glow in and out (the living style
|
|
548
|
+
guide slides it between step cards as a modal's step machine advances).
|
|
549
|
+
|
|
550
|
+
THE HOST MAY CARRY ITS OWN BACKGROUND. The ring cuts the host's box out of both
|
|
551
|
+
wedge layers, so only the band around the edge paints. That hole is what makes
|
|
552
|
+
the primitive work in the two shapes its consumers need:
|
|
546
553
|
|
|
547
554
|
· the WRAPPER shape (TM's holo-wrap / holo-card split) — a bare host with an
|
|
548
555
|
opaque child in front. The engine's style/_modal_specimen and the hub's
|
|
549
556
|
release-phase-meter both ship this. Unchanged by the hole: the child was
|
|
550
557
|
already covering that interior.
|
|
551
|
-
· the CARD shape — the host IS the opaque surface.
|
|
552
|
-
card needs, because it cannot take a wrapper at all: Turbo targets
|
|
553
|
-
element for both replace and remove, so a wrapper is orphaned every
|
|
554
|
-
card leaves the board.
|
|
558
|
+
· the CARD shape — the host IS the opaque surface, as a .btn is. A live board
|
|
559
|
+
card needs this too, because it cannot take a wrapper at all: Turbo targets
|
|
560
|
+
the card element for both replace and remove, so a wrapper is orphaned every
|
|
561
|
+
time a card leaves the board.
|
|
555
562
|
|
|
556
563
|
Without the hole the card shape is impossible, and not for a tunable reason:
|
|
557
564
|
both layers are pseudo-elements at z-index -1 / -2, and CSS paints an element's
|
|
@@ -563,59 +570,87 @@
|
|
|
563
570
|
The host must still NOT be clipped by an overflow-hidden ancestor near its
|
|
564
571
|
border, or the ring is cut off with it.
|
|
565
572
|
|
|
566
|
-
Usage: <
|
|
573
|
+
Usage: <button class="btn btn-primary edge-rotate-glow">Save</button>
|
|
567
574
|
wrapper shape, when the face is its own element:
|
|
568
|
-
<div class="
|
|
575
|
+
<div class="edge-rotate-glow rounded-xl">
|
|
569
576
|
<div class="rounded-xl bg-surface p-5">Selected</div>
|
|
570
577
|
</div>
|
|
571
|
-
|
|
572
|
-
two
|
|
573
|
-
--
|
|
574
|
-
Tune: --
|
|
578
|
+
one color: style="--edge-rotate-glow-color: #E11D48"
|
|
579
|
+
two colors: style="--edge-rotate-glow-color: #F08030;
|
|
580
|
+
--edge-rotate-glow-color-b: #6890F0"
|
|
581
|
+
Tune: --edge-rotate-glow-color / -color-b / -opacity / -speed / -thickness /
|
|
575
582
|
-bloom
|
|
583
|
+
|
|
584
|
+
DEPRECATED ALIAS: .studio-team-glow and its --studio-team-glow-* knobs. Every
|
|
585
|
+
old name keeps working, because hub and Turf views, scripts and e2e use them on
|
|
586
|
+
their main branches. Remove the alias only after both have moved. An old knob
|
|
587
|
+
on the new class also still applies; a new knob on the old class is not
|
|
588
|
+
supported, so move the class and its knobs together.
|
|
576
589
|
========================================================================== */
|
|
577
590
|
}
|
|
578
|
-
@property --
|
|
591
|
+
@property --edge-rotate-glow-angle {
|
|
579
592
|
syntax: "<angle>";
|
|
580
593
|
inherits: false;
|
|
581
594
|
initial-value: 0deg;
|
|
582
595
|
}
|
|
583
596
|
@layer components {
|
|
597
|
+
.edge-rotate-glow {
|
|
598
|
+
/* Each default reads the old knob first, so a host that moves the class name
|
|
599
|
+
and leaves an old knob in its inline style keeps its value. */
|
|
600
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color, var(--color-cta));
|
|
601
|
+
/* The SECOND wedge's color. Defaults to the first. Resolved on this element, so
|
|
602
|
+
an inline --edge-rotate-glow-color override feeds this default too. */
|
|
603
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b, var(--edge-rotate-glow-color));
|
|
604
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity, 1);
|
|
605
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed, 4s);
|
|
606
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness, 4px);
|
|
607
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom, 10px);
|
|
608
|
+
}
|
|
609
|
+
/* DEPRECATED ALIAS. See the note above. The old knobs keep their old defaults on
|
|
610
|
+
the old class, at the old specificity, so a consumer's own rule or inline style
|
|
611
|
+
wins exactly as it did; the new knobs then read them. */
|
|
584
612
|
.studio-team-glow {
|
|
585
613
|
--studio-team-glow-color: var(--color-cta);
|
|
586
|
-
/* The SECOND wedge's color. Defaults to the first, so a one-color caller sees
|
|
587
|
-
no change; a caller with two colors to show (a Pokémon's two types, a pair of
|
|
588
|
-
brand colors) gets one per wedge. Resolved on this element, so an inline
|
|
589
|
-
--studio-team-glow-color override feeds this default too. */
|
|
590
614
|
--studio-team-glow-color-b: var(--studio-team-glow-color);
|
|
591
615
|
--studio-team-glow-opacity: 1;
|
|
592
616
|
--studio-team-glow-speed: 4s;
|
|
593
617
|
--studio-team-glow-thickness: 4px;
|
|
594
618
|
--studio-team-glow-bloom: 10px;
|
|
619
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color);
|
|
620
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b);
|
|
621
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity);
|
|
622
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed);
|
|
623
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness);
|
|
624
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom);
|
|
625
|
+
}
|
|
626
|
+
.edge-rotate-glow,
|
|
627
|
+
.studio-team-glow {
|
|
595
628
|
position: relative;
|
|
596
629
|
isolation: isolate;
|
|
597
630
|
}
|
|
631
|
+
.edge-rotate-glow::before,
|
|
632
|
+
.edge-rotate-glow::after,
|
|
598
633
|
.studio-team-glow::before,
|
|
599
634
|
.studio-team-glow::after {
|
|
600
635
|
content: "";
|
|
601
636
|
position: absolute;
|
|
602
|
-
inset: calc(-1 * var(--
|
|
637
|
+
inset: calc(-1 * var(--edge-rotate-glow-thickness));
|
|
603
638
|
border-radius: inherit;
|
|
604
639
|
z-index: -1;
|
|
605
640
|
pointer-events: none;
|
|
606
|
-
opacity: var(--
|
|
641
|
+
opacity: var(--edge-rotate-glow-opacity);
|
|
607
642
|
transition: opacity 0.4s ease;
|
|
608
643
|
/* Two opposed wedges so two stretches of the ring glow at once. */
|
|
609
644
|
background: conic-gradient(
|
|
610
|
-
from var(--
|
|
645
|
+
from var(--edge-rotate-glow-angle),
|
|
611
646
|
transparent 0%,
|
|
612
|
-
var(--
|
|
647
|
+
var(--edge-rotate-glow-color) 12%,
|
|
613
648
|
transparent 33%,
|
|
614
649
|
transparent 50%,
|
|
615
|
-
var(--
|
|
650
|
+
var(--edge-rotate-glow-color-b) 62%,
|
|
616
651
|
transparent 83%
|
|
617
652
|
);
|
|
618
|
-
animation:
|
|
653
|
+
animation: edge-rotate-glow-spin var(--edge-rotate-glow-speed) linear infinite;
|
|
619
654
|
/* THE HOST'S OWN BOX IS PUNCHED OUT of both wedge layers, so what paints is the
|
|
620
655
|
band around the edge and nothing else. `padding` makes each layer's CONTENT
|
|
621
656
|
box exactly the host's box; mask-composite then removes that box from the
|
|
@@ -625,7 +660,7 @@
|
|
|
625
660
|
itself for replace and remove, so a wrapper is orphaned on every remove. With
|
|
626
661
|
the hole cut here the ring rides the card directly. For a wrapper host this
|
|
627
662
|
changes nothing visible: the child was already covering that interior. */
|
|
628
|
-
padding: var(--
|
|
663
|
+
padding: var(--edge-rotate-glow-thickness);
|
|
629
664
|
-webkit-mask:
|
|
630
665
|
linear-gradient(#000 0 0) content-box,
|
|
631
666
|
linear-gradient(#000 0 0);
|
|
@@ -635,6 +670,7 @@
|
|
|
635
670
|
linear-gradient(#000 0 0);
|
|
636
671
|
mask-composite: exclude;
|
|
637
672
|
}
|
|
673
|
+
.edge-rotate-glow::after,
|
|
638
674
|
.studio-team-glow::after {
|
|
639
675
|
/* Blurred bloom twin, a touch larger: its wedges reach 4px PAST the ring, which
|
|
640
676
|
is what makes the bloom read as a halo, not a soft edge on the ring. Drop the
|
|
@@ -647,19 +683,21 @@
|
|
|
647
683
|
card's own or the wrapper child's). Mask applies AFTER filter, which is why
|
|
648
684
|
the reach has to out-measure the blur — without it the halo ends on a hard
|
|
649
685
|
line at the layer's own box. */
|
|
650
|
-
inset: calc(-1 * (var(--
|
|
651
|
-
padding: calc(var(--
|
|
652
|
-
border: calc(var(--
|
|
686
|
+
inset: calc(-1 * (var(--edge-rotate-glow-thickness) + 4px + var(--edge-rotate-glow-bloom) * 2));
|
|
687
|
+
padding: calc(var(--edge-rotate-glow-thickness) + 4px);
|
|
688
|
+
border: calc(var(--edge-rotate-glow-bloom) * 2) solid transparent;
|
|
653
689
|
/* The wedges stop at the padding box (host + ring + the 4px); the border is empty
|
|
654
690
|
room the blur is allowed to spread into. */
|
|
655
691
|
background-clip: padding-box;
|
|
656
692
|
z-index: -2;
|
|
657
|
-
filter: blur(var(--
|
|
693
|
+
filter: blur(var(--edge-rotate-glow-bloom)) saturate(1.35);
|
|
658
694
|
}
|
|
659
|
-
@keyframes
|
|
660
|
-
to { --
|
|
695
|
+
@keyframes edge-rotate-glow-spin {
|
|
696
|
+
to { --edge-rotate-glow-angle: 360deg; }
|
|
661
697
|
}
|
|
662
698
|
@media (prefers-reduced-motion: reduce) {
|
|
699
|
+
.edge-rotate-glow::before,
|
|
700
|
+
.edge-rotate-glow::after,
|
|
663
701
|
.studio-team-glow::before,
|
|
664
702
|
.studio-team-glow::after { animation: none; }
|
|
665
703
|
}
|
|
@@ -1262,7 +1300,7 @@ body.modal-open { overflow: visible; }
|
|
|
1262
1300
|
Named pulse-cta (not btn-pulse) so it stays a plain motion-layer effect, not a
|
|
1263
1301
|
.btn component variant the engine.css component contract would demand.
|
|
1264
1302
|
|
|
1265
|
-
Usage: <button class="btn btn-primary pulse-cta">
|
|
1303
|
+
Usage: <button class="btn btn-primary pulse-cta">Save</button>
|
|
1266
1304
|
green pulse: style="--pulse-cta-color: var(--color-success)"
|
|
1267
1305
|
Tune: --pulse-cta-color / --pulse-cta-speed / --pulse-cta-scale /
|
|
1268
1306
|
--pulse-cta-ring / --pulse-cta-strength
|
|
@@ -1278,19 +1316,19 @@ body.modal-open { overflow: visible; }
|
|
|
1278
1316
|
@keyframes pulse-cta {
|
|
1279
1317
|
0% {
|
|
1280
1318
|
transform: scale(1);
|
|
1281
|
-
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 100%), transparent);
|
|
1319
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 100%), transparent);
|
|
1282
1320
|
}
|
|
1283
1321
|
10% {
|
|
1284
1322
|
transform: scale(var(--pulse-cta-scale));
|
|
1285
|
-
box-shadow: 0 0 0 calc(var(--pulse-cta-ring) * 0.6) color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 50%), transparent);
|
|
1323
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 calc(var(--pulse-cta-ring) * 0.6) color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 50%), transparent);
|
|
1286
1324
|
}
|
|
1287
1325
|
22% {
|
|
1288
1326
|
transform: scale(1);
|
|
1289
|
-
box-shadow: 0 0 0 var(--pulse-cta-ring) color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1327
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 var(--pulse-cta-ring) color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1290
1328
|
}
|
|
1291
1329
|
100% {
|
|
1292
1330
|
transform: scale(1);
|
|
1293
|
-
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1331
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1294
1332
|
}
|
|
1295
1333
|
}
|
|
1296
1334
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1866,6 +1904,58 @@ body.modal-open { overflow: visible; }
|
|
|
1866
1904
|
opacity: 0;
|
|
1867
1905
|
}
|
|
1868
1906
|
|
|
1907
|
+
/* FIZZ AROUND ANY BUTTON (studio/_fizz). The bubbles have no tie to the hold: the
|
|
1908
|
+
only thing that held them to it was this section's .hold-stack selector, and the
|
|
1909
|
+
hold's own states. So .fizz-stack gives them the same stack without a hold, with
|
|
1910
|
+
a neutral name, and it rests and answers hover as a hold button does. */
|
|
1911
|
+
.fizz-stack {
|
|
1912
|
+
position: relative;
|
|
1913
|
+
display: inline-flex;
|
|
1914
|
+
isolation: isolate;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.fizz-stack > .fizz-layer {
|
|
1918
|
+
position: absolute;
|
|
1919
|
+
inset: 0;
|
|
1920
|
+
z-index: 0;
|
|
1921
|
+
pointer-events: none;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
.fizz-stack > :not(.fizz-layer) {
|
|
1925
|
+
position: relative;
|
|
1926
|
+
z-index: 1;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
.fizz-stack .fizz-bit {
|
|
1930
|
+
position: absolute;
|
|
1931
|
+
width: var(--fs, 4px);
|
|
1932
|
+
height: var(--fs, 4px);
|
|
1933
|
+
margin-top: calc(var(--fs, 4px) / -2);
|
|
1934
|
+
margin-left: calc(var(--fs, 4px) / -2);
|
|
1935
|
+
border-radius: 100px;
|
|
1936
|
+
background: var(--fc, hsl(292 92% 70%));
|
|
1937
|
+
box-shadow:
|
|
1938
|
+
0 0 6px var(--fc, hsl(292 92% 70%)),
|
|
1939
|
+
0 0 0 0.5px rgb(255 255 255 / 0.35);
|
|
1940
|
+
opacity: 0;
|
|
1941
|
+
will-change: transform, opacity;
|
|
1942
|
+
animation: fizz-simmer var(--ft, 1.8s) linear var(--fd, 0s) infinite;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
.fizz-stack:not(.fizz-lively):hover .fizz-bit,
|
|
1946
|
+
.fizz-stack.fizz-lively .fizz-bit {
|
|
1947
|
+
animation: fizz-boil calc(var(--ft, 1.8s) * 0.62) linear calc(var(--fd, 0s) * 0.35) infinite;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
.fizz-stack > .fizz-layer-extra {
|
|
1951
|
+
opacity: 0;
|
|
1952
|
+
transition: opacity 0.45s ease;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
.fizz-stack.fizz-lively:hover > .fizz-layer-extra {
|
|
1956
|
+
opacity: 1;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1869
1959
|
/* THE PORTAL (studio/hold_button, fizz_portal: true). A button inside an
|
|
1870
1960
|
overflow-hidden card has its bubbles cut off at the card's edge, because the
|
|
1871
1961
|
layer is absolutely positioned inside the card's clip. With the option set,
|
|
@@ -1967,7 +2057,9 @@ body.modal-open { overflow: visible; }
|
|
|
1967
2057
|
.hold-stack .fizz-bit,
|
|
1968
2058
|
.hold-stack .hold-fizz-extra,
|
|
1969
2059
|
.hold-fizz-portal .fizz-bit,
|
|
1970
|
-
.hold-fizz-portal .hold-fizz-extra
|
|
2060
|
+
.hold-fizz-portal .hold-fizz-extra,
|
|
2061
|
+
.fizz-stack .fizz-bit,
|
|
2062
|
+
.fizz-stack .fizz-layer-extra {
|
|
1971
2063
|
animation: none !important;
|
|
1972
2064
|
opacity: 0 !important;
|
|
1973
2065
|
transition: none !important;
|
|
@@ -368,6 +368,74 @@ body.modal-open .studio-app-banner {
|
|
|
368
368
|
margin-left: auto;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
/* -- The section header (studio/_section_header, docs/PAGE_HEADER.md) --------
|
|
372
|
+
A section's title that sticks under the rest of the stack, inside its own <section>,
|
|
373
|
+
so the next section's header takes its place with no scroll script. At rest it reads as
|
|
374
|
+
a normal title. Stuck it takes the page header's bar, so content does not show through
|
|
375
|
+
it. studio/pinned_stack says which one is stuck. Its box never changes between the two,
|
|
376
|
+
so the change moves nothing. One line, always: a long title ends in an ellipsis on the
|
|
377
|
+
bar, the height is known, and the whole text stays in the heading.
|
|
378
|
+
|
|
379
|
+
IT STICKS ONLY ONCE THE PUBLISHER RUNS (html[data-pin-live]). With no publisher no bar
|
|
380
|
+
ever comes, and a clear title would lie over the page; in the flow it is a plain title.
|
|
381
|
+
|
|
382
|
+
A SHORT SCREEN KEEPS ITS ROOM. Under 36rem tall, a small phone or one on its side, the
|
|
383
|
+
header stays in the flow: the navbar and the page row already take a quarter of the height.
|
|
384
|
+
|
|
385
|
+
z-index is one tier under the page row, set by the partial, so a header being pushed out
|
|
386
|
+
passes under the row and over the content. --section-header-h is what a target inside a
|
|
387
|
+
section adds to its scroll margin, so it lands below the bar. */
|
|
388
|
+
:root {
|
|
389
|
+
--section-header-h: 2.25rem;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@utility section-header {
|
|
393
|
+
@apply text-heading;
|
|
394
|
+
position: relative;
|
|
395
|
+
min-width: 0;
|
|
396
|
+
min-height: var(--section-header-h);
|
|
397
|
+
padding-block: 0.25rem;
|
|
398
|
+
margin-block-end: 0.25rem;
|
|
399
|
+
border-block: 1px solid transparent;
|
|
400
|
+
background-color: transparent;
|
|
401
|
+
font-size: calc(var(--type-base, 16px) * 1.25);
|
|
402
|
+
font-weight: 700;
|
|
403
|
+
line-height: 1.3;
|
|
404
|
+
overflow: hidden;
|
|
405
|
+
white-space: nowrap;
|
|
406
|
+
text-overflow: ellipsis;
|
|
407
|
+
transition: background-color 150ms ease, border-color 150ms ease;
|
|
408
|
+
|
|
409
|
+
html[data-pin-live] & {
|
|
410
|
+
@media (height >= 36rem) {
|
|
411
|
+
position: sticky;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
&[data-stuck] {
|
|
416
|
+
@apply bg-surface border-subtle;
|
|
417
|
+
border-top-color: transparent;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Where a jump lands. A jump goes deep, so what counts is the stack once PINNED, with the
|
|
422
|
+
navbar compact and the page row stuck: heights, not --pin-section-top, whose value at the
|
|
423
|
+
top of the page holds the row's rest place and whatever scrolls away above it. So this one
|
|
424
|
+
composes, which a thing placed on screen must not: a margin is read once, at the jump. Less
|
|
425
|
+
two pixels, so the header has left its place on a fractional height and its title shows on
|
|
426
|
+
the bar.
|
|
427
|
+
|
|
428
|
+
A JUMP FROM THE TOP OF THE PAGE reads the navbar still tall, and lands by the navbar's
|
|
429
|
+
collapse below the line. Landing short is safe, the title shows. Landing past is safe too,
|
|
430
|
+
since the bar holds the title. A target inside a section: a bar's height lower. */
|
|
431
|
+
@utility section-target {
|
|
432
|
+
scroll-margin-top: calc(var(--nav-h, 5rem) + var(--pin-page-h, 0px) - 2px);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@utility section-subtarget {
|
|
436
|
+
scroll-margin-top: calc(var(--nav-h, 5rem) + var(--pin-page-h, 0px) + var(--section-header-h));
|
|
437
|
+
}
|
|
438
|
+
|
|
371
439
|
/* -- Scrims: tints and shades ----------------------------------------------
|
|
372
440
|
A scrim is the wash laid over media (a photo, a video) so text reads on it.
|
|
373
441
|
It comes in two TONES, and the tone is a design decision, not a colour pick:
|
|
@@ -1514,14 +1582,36 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1514
1582
|
}
|
|
1515
1583
|
}
|
|
1516
1584
|
|
|
1585
|
+
/* THE SECTION HEADER UNDER GLASS, the same bar one layer down. While it is stuck the bar
|
|
1586
|
+
under the row owns the round corners and the shadow: the row goes square, or the page
|
|
1587
|
+
would show in the notches between the two. */
|
|
1588
|
+
html[data-surface-style="glass"] .section-header[data-stuck] {
|
|
1589
|
+
background-color: var(--color-page);
|
|
1590
|
+
border-top-color: transparent;
|
|
1591
|
+
border-bottom-color: var(--surface-glass-line);
|
|
1592
|
+
border-radius: 0 0 0.875rem 0.875rem;
|
|
1593
|
+
box-shadow: var(--surface-glass-shadow);
|
|
1594
|
+
}
|
|
1595
|
+
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
1596
|
+
html[data-surface-style="glass"] .section-header[data-stuck] {
|
|
1597
|
+
background-color: color-mix(in srgb, var(--color-page) 62%, transparent);
|
|
1598
|
+
-webkit-backdrop-filter: blur(18px) saturate(140%);
|
|
1599
|
+
backdrop-filter: blur(18px) saturate(140%);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
html[data-surface-style="glass"][data-pin-section-stuck] .page-header[data-stuck] {
|
|
1603
|
+
border-radius: 0;
|
|
1604
|
+
box-shadow: var(--surface-glass-highlight);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1517
1607
|
/* LIT FROM ABOVE, under the same opt-in: one light for buttons, badges and inputs, from the
|
|
1518
1608
|
direction of the cards' top highlight. Four shadows: top light, bottom shade, contact, and
|
|
1519
1609
|
a glow in the button's own color.
|
|
1520
1610
|
|
|
1521
|
-
THE GRADIENT KEEPS ITS CHROMA. Both stops are built in oklch from the token,
|
|
1522
|
-
and
|
|
1523
|
-
|
|
1524
|
-
|
|
1611
|
+
THE GRADIENT KEEPS ITS CHROMA. Both stops are built in oklch from the token, the same step
|
|
1612
|
+
lighter and darker at the token's own chroma; mixing toward black or grey is what mutes a
|
|
1613
|
+
button. The midpoint is the token, so white text holds 4.5:1 there on the semantic fills
|
|
1614
|
+
and about 3:1 on the default primary, which Alex McRitchie accepted to match the brand word.
|
|
1525
1615
|
|
|
1526
1616
|
WHO GETS WHAT. Every full-size filled button: gradient, top, bottom, contact and a glow in
|
|
1527
1617
|
its own color, on the same 0.15 lightness range. A .btn-sm has no glow, since a toolbar or
|
|
@@ -1569,16 +1659,17 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1569
1659
|
--lit-badge: inset 0 1px 0 rgb(255 255 255 / 0.14), 0 1px 1px rgb(0 0 0 / 0.35);
|
|
1570
1660
|
--lit-input: inset 0 1px 2px rgb(0 0 0 / 0.4), inset 0 -1px 0 rgb(255 255 255 / 0.06);
|
|
1571
1661
|
}
|
|
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
|
-
}
|
|
1662
|
+
/* Stops: CENTERED ON THE FILL. Each stop moves lightness the same step up and down, with
|
|
1663
|
+
chroma and hue unchanged, so the gradient's midpoint (oklab interpolation) is the token
|
|
1664
|
+
itself. The primary centers on --color-cta, the brand violet a text-primary word paints,
|
|
1665
|
+
so the button and the word read as one purple. Alex McRitchie accepted white near 3:1 on
|
|
1666
|
+
the default primary for that; the semantic fills hold 4.5:1 at the midpoint. A top stop
|
|
1667
|
+
past sRGB clips only the top edge: the midpoint is interpolated before it is clipped. */
|
|
1668
|
+
html[data-surface-style="glass"] :is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success) {
|
|
1669
|
+
--lit-top-stop: oklch(from var(--lit-fill) calc(l + 0.06) c h / var(--lit-fill-alpha, 1));
|
|
1670
|
+
--lit-low-stop: oklch(from var(--lit-fill) calc(l - 0.06) c h / var(--lit-fill-alpha, 1));
|
|
1671
|
+
}
|
|
1672
|
+
html[data-surface-style="glass"] .btn-primary { --lit-fill: var(--color-cta); }
|
|
1582
1673
|
html[data-surface-style="glass"] .btn-secondary { --lit-fill: var(--btn-secondary-bg, var(--color-success)); }
|
|
1583
1674
|
html[data-surface-style="glass"] .btn-danger { --lit-fill: var(--color-danger-fill, var(--color-danger)); }
|
|
1584
1675
|
html[data-surface-style="glass"] .btn-warning { --lit-fill: var(--color-warning-fill, var(--color-warning)); }
|
|
@@ -1598,16 +1689,16 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1598
1689
|
var(--lit-glow) oklch(from var(--lit-glow-stop, var(--lit-low-stop)) l c h / var(--lit-glow-alpha));
|
|
1599
1690
|
}
|
|
1600
1691
|
/* On a dark page a deep green, red or orange glow disappears, so these glow from the top
|
|
1601
|
-
stop. The violet is
|
|
1692
|
+
stop. The violet, centered on the brand colour, is bright enough at its low stop. */
|
|
1602
1693
|
html.dark[data-surface-style="glass"] :is(.btn-secondary, .btn-danger, .btn-warning, .btn-success) { --lit-glow-stop: var(--lit-top-stop); }
|
|
1603
1694
|
/* A TOUCH OF TRANSPARENCY on a full-size filled button, dark theme only: a photo behind it
|
|
1604
|
-
shows subtly, and white text holds
|
|
1695
|
+
shows subtly, and white text holds its contrast at the midpoint. An outer shadow is not painted
|
|
1605
1696
|
under its own box, so the glow never brightens the fill. On a light page the backdrop would
|
|
1606
1697
|
lift the fill and cost contrast, so it stays solid there. Without a blur, or without relative
|
|
1607
1698
|
oklch (the gradient would drop out and leave white text on nothing), it stays solid. */
|
|
1608
1699
|
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and (color: oklch(from red l c h)) {
|
|
1609
1700
|
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.
|
|
1701
|
+
--lit-fill-alpha: 0.85;
|
|
1611
1702
|
background-color: transparent;
|
|
1612
1703
|
-webkit-backdrop-filter: blur(6px);
|
|
1613
1704
|
backdrop-filter: blur(6px);
|
|
@@ -1655,13 +1746,53 @@ html[data-theme-flip]::view-transition-new(*) {
|
|
|
1655
1746
|
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
1747
|
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
1748
|
|
|
1749
|
+
/* BUTTON TRICKS ON A LIT BUTTON (the lit rules above).
|
|
1750
|
+
A lit button already uses its box-shadow for its contact shadow and glow, and
|
|
1751
|
+
its ::after for its inset light. These rules sit in the utilities layer and the
|
|
1752
|
+
tricks in engine-motion.css sit in components, so the lit rules win, and each trick
|
|
1753
|
+
that shares one of them makes room here. Keep a trick on a real .btn lit.
|
|
1754
|
+
|
|
1755
|
+
--btn-lit-rest is the lit button's outer shadow at rest. The pulse's keyframes
|
|
1756
|
+
replace box-shadow for the whole animation, so they put it back themselves. */
|
|
1757
|
+
html[data-surface-style="glass"] .btn:not(:disabled, [aria-disabled="true"]) {
|
|
1758
|
+
--btn-lit-rest: var(--lit-contact);
|
|
1759
|
+
}
|
|
1760
|
+
html[data-surface-style="glass"] .btn:is(.btn-primary, .btn-secondary, .btn-danger, .btn-warning, .btn-success):not(.btn-sm, :disabled, [aria-disabled="true"]) {
|
|
1761
|
+
--btn-lit-rest: var(--lit-contact),
|
|
1762
|
+
var(--lit-glow) oklch(from var(--lit-glow-stop, var(--lit-low-stop)) l c h / var(--lit-glow-alpha));
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/* The glows need ::after for their bloom, so the inset light moves to the host and
|
|
1766
|
+
the bloom gets its own box back. Without this the lit rule's inset: -1px closes
|
|
1767
|
+
the bloom's hole and it washes the whole face. .btn-outline has no lit ::after. */
|
|
1768
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow, .studio-border-glow):not(.btn-outline, :disabled, [aria-disabled="true"]) {
|
|
1769
|
+
box-shadow: var(--lit-inset), var(--btn-lit-rest);
|
|
1770
|
+
}
|
|
1771
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow, .studio-border-glow):not(.btn-outline, :disabled, [aria-disabled="true"]):active {
|
|
1772
|
+
box-shadow: var(--lit-inset-pressed);
|
|
1773
|
+
}
|
|
1774
|
+
html[data-surface-style="glass"] .btn:is(.edge-rotate-glow, .studio-team-glow):not(.btn-outline, :disabled, [aria-disabled="true"])::after {
|
|
1775
|
+
inset: calc(-1 * (var(--edge-rotate-glow-thickness) + 4px + var(--edge-rotate-glow-bloom) * 2));
|
|
1776
|
+
box-shadow: none;
|
|
1777
|
+
}
|
|
1778
|
+
html[data-surface-style="glass"] .btn.studio-border-glow:not(.btn-outline, :disabled, [aria-disabled="true"])::after {
|
|
1779
|
+
inset: -46px;
|
|
1780
|
+
box-shadow: none;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/* The sheen's band already stops at its own rounded box, and overflow: hidden would
|
|
1784
|
+
cut the lit ::after's outer pixel, the top highlight. */
|
|
1785
|
+
html[data-surface-style="glass"] .btn.sheen:not(:disabled, [aria-disabled="true"]) {
|
|
1786
|
+
overflow: visible;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1658
1789
|
/* LESS TRANSPARENCY: every surface above goes solid and drops its blur. Last, to win on order;
|
|
1659
1790
|
both spellings of the root, since html.dark outranks a bare html[...] on specificity. */
|
|
1660
1791
|
@media (prefers-reduced-transparency: reduce) {
|
|
1661
1792
|
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)),
|
|
1793
|
+
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
1794
|
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); }
|
|
1795
|
+
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
1796
|
}
|
|
1666
1797
|
|
|
1667
1798
|
/* THE SIDEBAR ROW'S DESCRIPTION (components/_link_sidebar, _geo_signpost). At rest
|