@tonycodes-ui/mortar 0.11.0 → 0.13.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/AGENTS.md +1 -1
- package/CHANGELOG.md +16 -47
- package/CONTRIBUTING.md +14 -2
- package/DESIGN.md +1 -1
- package/README.md +1 -1
- package/catalog.json +610 -377
- package/cli/audit.mjs +152 -0
- package/dist/agent-data.json +1 -1
- package/dist/application.d.ts +30 -5
- package/dist/application.js +20 -8
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/inspection.js +17 -1
- package/dist/internal.d.ts +39 -3
- package/dist/internal.js +74 -4
- package/dist/marketing.d.ts +33 -9
- package/dist/marketing.js +23 -14
- package/dist/primitives.js +20 -11
- package/docs/agents.md +21 -18
- package/docs/theming.md +72 -1
- package/package.json +1 -1
- package/skills/mortar/SKILL.md +1 -1
- package/src/application.css +95 -1
- package/src/application.tsx +88 -19
- package/src/data.css +58 -2
- package/src/foundation.css +10 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +6 -0
- package/src/inspection.tsx +15 -0
- package/src/internal.tsx +180 -3
- package/src/marketing.css +181 -2
- package/src/marketing.tsx +103 -94
- package/src/primitives.tsx +38 -19
package/src/internal.tsx
CHANGED
|
@@ -5,14 +5,26 @@ import {
|
|
|
5
5
|
createContext,
|
|
6
6
|
createElement,
|
|
7
7
|
useContext,
|
|
8
|
+
useEffect,
|
|
8
9
|
useId,
|
|
10
|
+
useState,
|
|
9
11
|
type HTMLAttributes,
|
|
12
|
+
type RefObject,
|
|
10
13
|
type ReactNode,
|
|
11
14
|
cloneElement,
|
|
12
15
|
isValidElement,
|
|
13
16
|
} from "react";
|
|
14
|
-
import {
|
|
15
|
-
|
|
17
|
+
import {
|
|
18
|
+
CloseButton,
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogPanel,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
Popover,
|
|
23
|
+
PopoverButton,
|
|
24
|
+
PopoverPanel,
|
|
25
|
+
} from "@headlessui/react";
|
|
26
|
+
import { KitLink, type ItemGroup, type LinkItem } from "./navigation.js";
|
|
27
|
+
import { ChevronDown } from "./icons.js";
|
|
16
28
|
import { ActionLink, Button, type LinkAction } from "./primitives.js";
|
|
17
29
|
import { headingTag, type HeadingLevel } from "./heading.js";
|
|
18
30
|
|
|
@@ -71,6 +83,8 @@ export interface IntroProps {
|
|
|
71
83
|
eyebrow?: string;
|
|
72
84
|
/** Omit to render no heading. */
|
|
73
85
|
title?: string;
|
|
86
|
+
/** A phrase in `title` to emphasise; its first occurrence renders as `<em>`. */
|
|
87
|
+
emphasis?: string;
|
|
74
88
|
description?: string;
|
|
75
89
|
/** Id for the heading, e.g. the target of the section's aria-labelledby. */
|
|
76
90
|
id?: string;
|
|
@@ -87,6 +101,7 @@ export interface IntroProps {
|
|
|
87
101
|
export function introSlots({
|
|
88
102
|
eyebrow,
|
|
89
103
|
title,
|
|
104
|
+
emphasis,
|
|
90
105
|
description,
|
|
91
106
|
id,
|
|
92
107
|
headingLevel = 2,
|
|
@@ -95,12 +110,26 @@ export function introSlots({
|
|
|
95
110
|
const Heading = headingTag(headingLevel);
|
|
96
111
|
return [
|
|
97
112
|
eyebrow && <p className="m-eyebrow">{eyebrow}</p>,
|
|
98
|
-
title !== undefined &&
|
|
113
|
+
title !== undefined && (
|
|
114
|
+
<Heading id={id}>{emphasise(title, emphasis)}</Heading>
|
|
115
|
+
),
|
|
99
116
|
description && (
|
|
100
117
|
<p className={descriptionClassName ?? undefined}>{description}</p>
|
|
101
118
|
),
|
|
102
119
|
];
|
|
103
120
|
}
|
|
121
|
+
/** `title` with the first occurrence of `phrase` in `<em>`; unchanged when absent. */
|
|
122
|
+
function emphasise(title: string, phrase?: string): ReactNode {
|
|
123
|
+
const at = phrase ? title.indexOf(phrase) : -1;
|
|
124
|
+
if (!phrase || at < 0) return title;
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{title.slice(0, at)}
|
|
128
|
+
<em className="m-emphasis">{phrase}</em>
|
|
129
|
+
{title.slice(at + phrase.length)}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
104
133
|
/** The section intro as one element, for parents where nothing user-supplied follows it. */
|
|
105
134
|
export function SectionIntro(props: IntroProps) {
|
|
106
135
|
return <>{...introSlots(props)}</>;
|
|
@@ -210,6 +239,72 @@ export function useDomId(prefix: string) {
|
|
|
210
239
|
return prefix + useId().replace(/[^a-zA-Z0-9-]/g, "");
|
|
211
240
|
}
|
|
212
241
|
|
|
242
|
+
/** Why an animation is not running; null while it plays. */
|
|
243
|
+
export type PauseReason = "user" | "motion" | "hidden" | "offscreen" | null;
|
|
244
|
+
/**
|
|
245
|
+
* Shared pause and resume for decorative motion. It plays only while the
|
|
246
|
+
* user has not paused it, reduced motion is not requested, the document is
|
|
247
|
+
* visible and `ref` is on screen. The user's pause is kept apart from the
|
|
248
|
+
* automatic reasons, so returning to the page never overrides it. `paused`
|
|
249
|
+
* and `onPausedChange` make the user's choice controlled, for example one
|
|
250
|
+
* state shared by several animations. Every listener is removed on unmount.
|
|
251
|
+
*/
|
|
252
|
+
export function usePlayback(
|
|
253
|
+
ref: RefObject<Element | null>,
|
|
254
|
+
{
|
|
255
|
+
paused,
|
|
256
|
+
onPausedChange,
|
|
257
|
+
enabled = true,
|
|
258
|
+
}: {
|
|
259
|
+
paused?: boolean;
|
|
260
|
+
onPausedChange?: (paused: boolean) => void;
|
|
261
|
+
enabled?: boolean;
|
|
262
|
+
},
|
|
263
|
+
) {
|
|
264
|
+
const [ownPaused, setOwnPaused] = useState(false);
|
|
265
|
+
const [reduced, setReduced] = useState(false);
|
|
266
|
+
const [hidden, setHidden] = useState(false);
|
|
267
|
+
const [offscreen, setOffscreen] = useState(false);
|
|
268
|
+
const userPaused = paused ?? ownPaused;
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (!enabled) return;
|
|
271
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
272
|
+
const onMedia = () => setReduced(media.matches);
|
|
273
|
+
const onVisibility = () => setHidden(document.visibilityState === "hidden");
|
|
274
|
+
onMedia();
|
|
275
|
+
onVisibility();
|
|
276
|
+
media.addEventListener("change", onMedia);
|
|
277
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
278
|
+
const observer =
|
|
279
|
+
ref.current && typeof IntersectionObserver !== "undefined"
|
|
280
|
+
? new IntersectionObserver(([entry]) =>
|
|
281
|
+
setOffscreen(!entry.isIntersecting),
|
|
282
|
+
)
|
|
283
|
+
: null;
|
|
284
|
+
if (observer && ref.current) observer.observe(ref.current);
|
|
285
|
+
return () => {
|
|
286
|
+
media.removeEventListener("change", onMedia);
|
|
287
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
288
|
+
observer?.disconnect();
|
|
289
|
+
};
|
|
290
|
+
}, [enabled, ref]);
|
|
291
|
+
const reason: PauseReason = userPaused
|
|
292
|
+
? "user"
|
|
293
|
+
: reduced
|
|
294
|
+
? "motion"
|
|
295
|
+
: hidden
|
|
296
|
+
? "hidden"
|
|
297
|
+
: offscreen
|
|
298
|
+
? "offscreen"
|
|
299
|
+
: null;
|
|
300
|
+
const toggle = () => {
|
|
301
|
+
const next = !userPaused;
|
|
302
|
+
if (paused === undefined) setOwnPaused(next);
|
|
303
|
+
onPausedChange?.(next);
|
|
304
|
+
};
|
|
305
|
+
return { reason, reduced, userPaused, toggle };
|
|
306
|
+
}
|
|
307
|
+
|
|
213
308
|
/**
|
|
214
309
|
* Next index for arrow, Home and End keys in a horizontal set (wrapping), or
|
|
215
310
|
* null for any other key.
|
|
@@ -612,3 +707,85 @@ export function focusAfterRemoval(element: HTMLElement) {
|
|
|
612
707
|
}
|
|
613
708
|
});
|
|
614
709
|
}
|
|
710
|
+
|
|
711
|
+
/** An item in a flyout panel; `current` marks the page you are on. */
|
|
712
|
+
export type FlyoutItem = Pick<
|
|
713
|
+
LinkItem,
|
|
714
|
+
"label" | "href" | "description" | "icon" | "current"
|
|
715
|
+
>;
|
|
716
|
+
/**
|
|
717
|
+
* A labelled group of links in a header that opens an anchored panel
|
|
718
|
+
* (MarketingHeader and Navbar). The group is marked current when one of its
|
|
719
|
+
* items is the current page.
|
|
720
|
+
*/
|
|
721
|
+
export function Flyout({
|
|
722
|
+
label,
|
|
723
|
+
items,
|
|
724
|
+
footer = [],
|
|
725
|
+
layout = "list",
|
|
726
|
+
onNavigate,
|
|
727
|
+
}: {
|
|
728
|
+
label: string;
|
|
729
|
+
items: FlyoutItem[];
|
|
730
|
+
footer?: LinkAction[];
|
|
731
|
+
layout?: "list" | "columns" | "full";
|
|
732
|
+
onNavigate: () => void;
|
|
733
|
+
}) {
|
|
734
|
+
const dataLayout = layout === "list" ? undefined : layout;
|
|
735
|
+
const current = items.some((item) => item.current);
|
|
736
|
+
return (
|
|
737
|
+
<Popover
|
|
738
|
+
className="m-flyout"
|
|
739
|
+
data-current={current || undefined}
|
|
740
|
+
data-layout={dataLayout}
|
|
741
|
+
>
|
|
742
|
+
<PopoverButton className="m-flyout-trigger">
|
|
743
|
+
{label}
|
|
744
|
+
<ChevronDown />
|
|
745
|
+
</PopoverButton>
|
|
746
|
+
{/* Anchored panels render in a portal, so headers inside clipped containers never cut them off. */}
|
|
747
|
+
<PopoverPanel
|
|
748
|
+
anchor="bottom"
|
|
749
|
+
className="m-flyout-panel"
|
|
750
|
+
data-layout={dataLayout}
|
|
751
|
+
>
|
|
752
|
+
<ul>
|
|
753
|
+
{items.map((item, index) => (
|
|
754
|
+
<li key={`${item.label}:${item.href}:${index}`}>
|
|
755
|
+
<CloseButton
|
|
756
|
+
as={KitLink}
|
|
757
|
+
href={item.href}
|
|
758
|
+
aria-current={item.current ? "page" : undefined}
|
|
759
|
+
onClick={onNavigate}
|
|
760
|
+
>
|
|
761
|
+
{item.icon && (
|
|
762
|
+
<span className="m-flyout-icon" aria-hidden="true">
|
|
763
|
+
{item.icon}
|
|
764
|
+
</span>
|
|
765
|
+
)}
|
|
766
|
+
<span className="m-flyout-text">
|
|
767
|
+
<strong>{item.label}</strong>
|
|
768
|
+
{item.description && <span>{item.description}</span>}
|
|
769
|
+
</span>
|
|
770
|
+
</CloseButton>
|
|
771
|
+
</li>
|
|
772
|
+
))}
|
|
773
|
+
</ul>
|
|
774
|
+
{footer.length > 0 && (
|
|
775
|
+
<div className="m-flyout-footer">
|
|
776
|
+
{footer.map((link, index) => (
|
|
777
|
+
<CloseButton
|
|
778
|
+
as={KitLink}
|
|
779
|
+
key={`${link.label}:${link.href}:${index}`}
|
|
780
|
+
href={link.href}
|
|
781
|
+
onClick={onNavigate}
|
|
782
|
+
>
|
|
783
|
+
{link.label}
|
|
784
|
+
</CloseButton>
|
|
785
|
+
))}
|
|
786
|
+
</div>
|
|
787
|
+
)}
|
|
788
|
+
</PopoverPanel>
|
|
789
|
+
</Popover>
|
|
790
|
+
);
|
|
791
|
+
}
|
package/src/marketing.css
CHANGED
|
@@ -24,6 +24,21 @@
|
|
|
24
24
|
font-weight: var(--m-weight-bold);
|
|
25
25
|
letter-spacing: var(--m-tracking-heading);
|
|
26
26
|
}
|
|
27
|
+
/* A short descriptor beside the wordmark, outside the home link. */
|
|
28
|
+
.m-brand-lockup {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 12px;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
.m-brand-tagline {
|
|
35
|
+
max-width: 28ch;
|
|
36
|
+
padding-left: 12px;
|
|
37
|
+
border-left: 1px solid var(--m-line);
|
|
38
|
+
color: var(--m-muted);
|
|
39
|
+
font-size: 13px;
|
|
40
|
+
line-height: 1.3;
|
|
41
|
+
}
|
|
27
42
|
.m-marketing-header nav {
|
|
28
43
|
display: flex;
|
|
29
44
|
gap: 22px;
|
|
@@ -192,7 +207,8 @@
|
|
|
192
207
|
grid-column: 1;
|
|
193
208
|
grid-row: 1;
|
|
194
209
|
}
|
|
195
|
-
.m-marketing-header[data-layout="center"] > .m-brand
|
|
210
|
+
.m-marketing-header[data-layout="center"] > .m-brand,
|
|
211
|
+
.m-marketing-header[data-layout="center"] > .m-brand-lockup {
|
|
196
212
|
grid-column: 2;
|
|
197
213
|
grid-row: 1;
|
|
198
214
|
}
|
|
@@ -253,7 +269,8 @@
|
|
|
253
269
|
.m-marketing-header[data-layout="center"] {
|
|
254
270
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
255
271
|
}
|
|
256
|
-
.m-marketing-header[data-layout="center"] > .m-brand
|
|
272
|
+
.m-marketing-header[data-layout="center"] > .m-brand,
|
|
273
|
+
.m-marketing-header[data-layout="center"] > .m-brand-lockup {
|
|
257
274
|
justify-self: center;
|
|
258
275
|
}
|
|
259
276
|
.m-marketing-header[data-layout="center"] > .m-actions .m-button {
|
|
@@ -274,6 +291,32 @@
|
|
|
274
291
|
.m-flyout-panel[data-layout="columns"] ul {
|
|
275
292
|
grid-template-columns: minmax(0, 1fr);
|
|
276
293
|
}
|
|
294
|
+
.m-brand-tagline {
|
|
295
|
+
display: none;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* == MarketingPage == */
|
|
300
|
+
/* Header, main and footer fill the viewport; main grows, nothing is clipped. */
|
|
301
|
+
.m-marketing-page {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
min-height: 100vh;
|
|
305
|
+
min-height: 100dvh;
|
|
306
|
+
}
|
|
307
|
+
/* Header, sections and footer centre themselves with auto margins, which in a
|
|
308
|
+
flex column would shrink them to their content; keep them full width. */
|
|
309
|
+
.m-marketing-page > *,
|
|
310
|
+
.m-marketing-page[data-align="center"] > .m-marketing-main > * {
|
|
311
|
+
width: 100%;
|
|
312
|
+
}
|
|
313
|
+
.m-marketing-main {
|
|
314
|
+
flex: 1 0 auto;
|
|
315
|
+
}
|
|
316
|
+
.m-marketing-page[data-align="center"] > .m-marketing-main {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
justify-content: center;
|
|
277
320
|
}
|
|
278
321
|
|
|
279
322
|
/* == Banner == */
|
|
@@ -591,6 +634,30 @@
|
|
|
591
634
|
.m-hero-split .m-section-title {
|
|
592
635
|
font-size: clamp(40px, 5vw, 64px);
|
|
593
636
|
}
|
|
637
|
+
/* Beside a preview the copy is start-aligned: no auto margins, which would
|
|
638
|
+
centre the description inside a copy column wider than its max width. */
|
|
639
|
+
.m-hero-split .m-hero-description {
|
|
640
|
+
margin-inline: 0;
|
|
641
|
+
}
|
|
642
|
+
.m-hero-split[data-wide="copy"] {
|
|
643
|
+
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
|
644
|
+
}
|
|
645
|
+
.m-hero-split[data-wide="preview"] {
|
|
646
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
|
647
|
+
}
|
|
648
|
+
/* An emphasised phrase in the title: the brand's text-safe link colour. */
|
|
649
|
+
.m-hero .m-emphasis {
|
|
650
|
+
font-style: normal;
|
|
651
|
+
color: var(--m-link);
|
|
652
|
+
}
|
|
653
|
+
.m-hero:is([data-background], [data-tone="accent"]) .m-emphasis {
|
|
654
|
+
color: inherit;
|
|
655
|
+
}
|
|
656
|
+
.m-hero-note {
|
|
657
|
+
margin-top: 20px;
|
|
658
|
+
color: var(--m-muted);
|
|
659
|
+
font-size: 14px;
|
|
660
|
+
}
|
|
594
661
|
.m-hero-preview {
|
|
595
662
|
margin-top: 48px;
|
|
596
663
|
text-align: left;
|
|
@@ -694,6 +761,7 @@
|
|
|
694
761
|
}
|
|
695
762
|
@media (max-width: 900px) {
|
|
696
763
|
.m-hero-split,
|
|
764
|
+
.m-hero-split[data-wide],
|
|
697
765
|
.m-feature-section,
|
|
698
766
|
.m-faq {
|
|
699
767
|
grid-template-columns: minmax(0, 1fr);
|
|
@@ -1893,6 +1961,41 @@
|
|
|
1893
1961
|
text-align: center;
|
|
1894
1962
|
margin-top: 40px;
|
|
1895
1963
|
}
|
|
1964
|
+
/* Footer: one compact row that wraps in reading order. */
|
|
1965
|
+
.m-footer[data-layout="inline"] {
|
|
1966
|
+
padding-block: 24px;
|
|
1967
|
+
border-top: 1px solid var(--m-line);
|
|
1968
|
+
}
|
|
1969
|
+
.m-footer-inline {
|
|
1970
|
+
display: flex;
|
|
1971
|
+
flex-wrap: wrap;
|
|
1972
|
+
align-items: center;
|
|
1973
|
+
gap: 8px 24px;
|
|
1974
|
+
color: var(--m-muted);
|
|
1975
|
+
font-size: 13px;
|
|
1976
|
+
}
|
|
1977
|
+
.m-footer-inline .m-brand {
|
|
1978
|
+
font-size: 14px;
|
|
1979
|
+
color: var(--m-ink);
|
|
1980
|
+
}
|
|
1981
|
+
.m-footer-inline p {
|
|
1982
|
+
margin: 0;
|
|
1983
|
+
}
|
|
1984
|
+
.m-footer-inline > :is(nav, div) {
|
|
1985
|
+
display: flex;
|
|
1986
|
+
flex-wrap: wrap;
|
|
1987
|
+
gap: 8px 20px;
|
|
1988
|
+
}
|
|
1989
|
+
.m-footer-inline a {
|
|
1990
|
+
color: var(--m-muted);
|
|
1991
|
+
text-decoration: none;
|
|
1992
|
+
}
|
|
1993
|
+
.m-footer-inline a:hover {
|
|
1994
|
+
color: var(--m-ink);
|
|
1995
|
+
}
|
|
1996
|
+
.m-footer-inline > .m-icon-links {
|
|
1997
|
+
margin: 0;
|
|
1998
|
+
}
|
|
1896
1999
|
@media (max-width: 900px) {
|
|
1897
2000
|
.m-footer-grid {
|
|
1898
2001
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -2385,6 +2488,82 @@
|
|
|
2385
2488
|
background: var(--m-primary);
|
|
2386
2489
|
color: var(--m-on-primary);
|
|
2387
2490
|
}
|
|
2491
|
+
|
|
2492
|
+
/* == NetworkIllustration == */
|
|
2493
|
+
/* Nodes and connections from data; signals travel along connections. Every
|
|
2494
|
+
colour is a token, so the drawing follows the brand and appearance. */
|
|
2495
|
+
.m-network {
|
|
2496
|
+
display: flex;
|
|
2497
|
+
flex-direction: column;
|
|
2498
|
+
gap: 8px;
|
|
2499
|
+
}
|
|
2500
|
+
.m-network svg {
|
|
2501
|
+
display: block;
|
|
2502
|
+
width: 100%;
|
|
2503
|
+
height: auto;
|
|
2504
|
+
overflow: visible;
|
|
2505
|
+
}
|
|
2506
|
+
.m-network-link {
|
|
2507
|
+
stroke: var(--m-line-strong);
|
|
2508
|
+
stroke-width: 1.5;
|
|
2509
|
+
/* Lines keep their width however large the drawing is shown. */
|
|
2510
|
+
vector-effect: non-scaling-stroke;
|
|
2511
|
+
}
|
|
2512
|
+
/* The outline is the strong line (3:1) whatever the fill, so nodes stay visible. */
|
|
2513
|
+
.m-network-node {
|
|
2514
|
+
fill: var(--m-surface);
|
|
2515
|
+
stroke: var(--m-line-strong);
|
|
2516
|
+
stroke-width: 1.5;
|
|
2517
|
+
vector-effect: non-scaling-stroke;
|
|
2518
|
+
}
|
|
2519
|
+
.m-network-node[data-tone="accent"] {
|
|
2520
|
+
fill: var(--m-accent);
|
|
2521
|
+
}
|
|
2522
|
+
.m-network-label {
|
|
2523
|
+
fill: var(--m-secondary);
|
|
2524
|
+
font-size: 12px;
|
|
2525
|
+
}
|
|
2526
|
+
.m-network-signal {
|
|
2527
|
+
fill: var(--m-accent);
|
|
2528
|
+
translate: var(--m-signal-from);
|
|
2529
|
+
animation: m-network-signal 3s linear infinite;
|
|
2530
|
+
}
|
|
2531
|
+
.m-network[data-paused] .m-network-signal {
|
|
2532
|
+
animation-play-state: paused;
|
|
2533
|
+
}
|
|
2534
|
+
/* Below the drawing, never over it, so no node is covered at any size. */
|
|
2535
|
+
.m-network-toggle {
|
|
2536
|
+
align-self: flex-end;
|
|
2537
|
+
padding: 4px 10px;
|
|
2538
|
+
border: 1px solid var(--m-line);
|
|
2539
|
+
border-radius: var(--m-radius-sm);
|
|
2540
|
+
background: var(--m-surface);
|
|
2541
|
+
color: var(--m-secondary);
|
|
2542
|
+
font: inherit;
|
|
2543
|
+
font-size: 12px;
|
|
2544
|
+
cursor: pointer;
|
|
2545
|
+
}
|
|
2546
|
+
.m-network-toggle:hover {
|
|
2547
|
+
color: var(--m-ink);
|
|
2548
|
+
}
|
|
2549
|
+
/* Before scripts run, and whenever reduced motion is requested, show the
|
|
2550
|
+
static drawing; the script then stops rendering the signals. */
|
|
2551
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2552
|
+
.m-network-signal {
|
|
2553
|
+
display: none;
|
|
2554
|
+
}
|
|
2555
|
+
.m-network-toggle {
|
|
2556
|
+
display: none;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
@keyframes m-network-signal {
|
|
2561
|
+
from {
|
|
2562
|
+
translate: var(--m-signal-from);
|
|
2563
|
+
}
|
|
2564
|
+
to {
|
|
2565
|
+
translate: var(--m-signal-to);
|
|
2566
|
+
}
|
|
2388
2567
|
}
|
|
2389
2568
|
@keyframes m-chat-dot {
|
|
2390
2569
|
0%,
|