@atelic-action/ui 0.13.0 → 0.14.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/README.md +22 -3
- package/package.json +3 -1
- package/src/primitives/Button.tsx +87 -0
- package/src/primitives/Chip.tsx +30 -0
- package/src/primitives/Eyebrow.tsx +25 -0
- package/src/primitives/Lead.tsx +17 -0
- package/src/primitives/SectionHeading.tsx +42 -0
- package/src/primitives/index.ts +5 -0
- package/src/styles/primitives.css +153 -0
package/README.md
CHANGED
|
@@ -31,10 +31,12 @@ Import the stylesheets in this order, from the root route or the site's base she
|
|
|
31
31
|
2. `@atelic-action/ui/styles/base.css`
|
|
32
32
|
3. `@atelic-action/ui/styles/chrome.css`
|
|
33
33
|
4. `@atelic-action/ui/styles/components.css`
|
|
34
|
-
5.
|
|
35
|
-
6.
|
|
34
|
+
5. `@atelic-action/ui/styles/primitives.css`
|
|
35
|
+
6. `@atelic-action/ui/styles/gate.css`, on a site that mounts the access gate
|
|
36
|
+
7. The site's own CSS
|
|
37
|
+
8. The site's `theme.css`, last
|
|
36
38
|
|
|
37
|
-
The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outside a layer wins over them whatever its specificity. The package reads the theme tokens (`--ink`, `--surface`, `--primary`, `--nav-height`, and the rest) and defines none, so `theme.css` stays the one file a site edits to rebrand. The chrome's buttons wear the
|
|
39
|
+
The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outside a layer wins over them whatever its specificity. The package reads the theme tokens (`--ink`, `--surface`, `--primary`, `--nav-height`, and the rest) and defines none, so `theme.css` stays the one file a site edits to rebrand. The chrome's buttons wear the `.btn` classes, which `primitives.css` defines and a site may restyle.
|
|
38
40
|
|
|
39
41
|
## What Is Inside
|
|
40
42
|
|
|
@@ -42,10 +44,12 @@ The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outs
|
|
|
42
44
|
|---|---|
|
|
43
45
|
| `@atelic-action/ui/chrome` | `SiteHeader`, `SiteMenu`, `Footer`, `CreditBar`, `StickyCTABar`, `BrandLockup`, `SkipLink`, and their prop types |
|
|
44
46
|
| `@atelic-action/ui/components` | `NotFound`, the access gate's `GateLogin` and `Watermark` (with `readCookie`), and the page components (`Checklist`, `DataTable`, `StatRow`, `Callout`, `Collapsible`, `StackedBar`, `ColumnChart`, `ThresholdScale`, `StarRating`, `Disclosure`), with their prop types (see [The Not Found Page](#the-not-found-page) and [Page Components](#page-components)) |
|
|
47
|
+
| `@atelic-action/ui/primitives` | `Button`, `Chip`, `Eyebrow`, `Lead`, `SectionHeading`, and their prop types (see [Primitives](#primitives)) |
|
|
45
48
|
| `@atelic-action/ui/email` | The email components, the theme provider, the plain text helpers, and their prop types (see [Email](#email)) |
|
|
46
49
|
| `@atelic-action/ui/email/render` | `renderEmail` and `renderFailureEmail`, the only entry that imports `react-dom/server` |
|
|
47
50
|
| `@atelic-action/ui/tokens` | `Palette`, `atelicPalette`, `Fonts`, `atelicFonts`, `toThemeCSS`, `themeTokenMap` |
|
|
48
51
|
| `@atelic-action/ui/hooks` | `useScrollSpy` and its `PageStop` type |
|
|
52
|
+
| `@atelic-action/ui/styles/primitives.css` | Styles for everything under `primitives`, the `.btn` classes included |
|
|
49
53
|
| `@atelic-action/ui/styles/gate.css` | Styles for `GateLogin` and `Watermark` |
|
|
50
54
|
| `@atelic-action/ui/gate` | `signToken`, `verifyToken`, `safeNext`, `needsSession`, and the token types |
|
|
51
55
|
| `@atelic-action/ui/gate/request-link` | `POST`, the handler that emails a sign in link, and `createRequestLink` |
|
|
@@ -121,6 +125,21 @@ export function NotFoundPage() {
|
|
|
121
125
|
|
|
122
126
|
`staticNotFoundRouting` is for prerendered sites only. On a live app the pending state is a real loading moment and the page would flash through every slow load, so an app sets `defaultNotFoundComponent` alone. A browser test is the only proof any of this works; template-marketing's `e2e/not-found.spec.ts` is the one to copy.
|
|
123
127
|
|
|
128
|
+
## Primitives
|
|
129
|
+
|
|
130
|
+
`Button`, `Chip`, `Eyebrow`, `Lead`, and `SectionHeading` are the small pieces a page is set in, the same in every template. Each renders from props and wears a class of the same name (`.btn`, `.chip`, `.eyebrow`, `.lead`, `.section-head`), styled by `primitives.css` from the theme tokens.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { Button, SectionHeading } from "@atelic-action/ui/primitives";
|
|
134
|
+
|
|
135
|
+
<SectionHeading eyebrow="Pricing" title="One number" lead="Flat and posted." />;
|
|
136
|
+
<Button href="/contact" size="lg" arrow>
|
|
137
|
+
Book a visit
|
|
138
|
+
</Button>;
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Button` is an anchor when it has an `href` and a real button otherwise. It renders the `href` as given: a site served under a base path wraps it once and applies its own prefix there. Its arrow is hidden from the name a screen reader hears.
|
|
142
|
+
|
|
124
143
|
## The Access Gate
|
|
125
144
|
|
|
126
145
|
The gate puts a whole host behind an email sign in with no database: a reader on the allowlist asks for a link, the link starts a signed session cookie, and the middleware holds every page and file back from anyone without one. It is opt in and fails open, so a host with no `GATE_SESSION_SECRET` is fully public.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, and the access gate they install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
"./email": "./src/email/index.ts",
|
|
25
25
|
"./email/render": "./src/email/render.tsx",
|
|
26
26
|
"./hooks": "./src/hooks/index.ts",
|
|
27
|
+
"./primitives": "./src/primitives/index.ts",
|
|
27
28
|
"./routing": "./src/routing/index.ts",
|
|
28
29
|
"./tokens": "./src/tokens/index.ts",
|
|
29
30
|
"./styles/base.css": "./src/styles/base.css",
|
|
30
31
|
"./styles/chrome.css": "./src/styles/chrome.css",
|
|
31
32
|
"./styles/components.css": "./src/styles/components.css",
|
|
32
33
|
"./styles/gate.css": "./src/styles/gate.css",
|
|
34
|
+
"./styles/primitives.css": "./src/styles/primitives.css",
|
|
33
35
|
"./styles/star-rating.css": "./src/styles/star-rating.css",
|
|
34
36
|
"./gate": {
|
|
35
37
|
"types": "./dist/gate/index.d.ts",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { newTabProps } from "../lib/newTabProps";
|
|
3
|
+
|
|
4
|
+
export interface ButtonProps {
|
|
5
|
+
variant?: "primary" | "dark" | "ghost";
|
|
6
|
+
size?: "md" | "lg";
|
|
7
|
+
/** Inverts ghost styling for use on dark surfaces. */
|
|
8
|
+
onDark?: boolean;
|
|
9
|
+
/** Appends the animated arrow glyph. */
|
|
10
|
+
arrow?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Link target: an internal path ("/services") or protocol link
|
|
13
|
+
* (https, tel, mailto). Marketing pages navigate as an MPA, so plain
|
|
14
|
+
* anchors are intentional here.
|
|
15
|
+
*/
|
|
16
|
+
href?: string;
|
|
17
|
+
/** Open href in a new tab with rel="noopener". */
|
|
18
|
+
newTab?: boolean;
|
|
19
|
+
/** Renders a real button element (e.g. form submit) when set. */
|
|
20
|
+
type?: "submit" | "button";
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
ariaLabel?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Button({
|
|
29
|
+
variant = "primary",
|
|
30
|
+
size = "md",
|
|
31
|
+
onDark = false,
|
|
32
|
+
arrow = false,
|
|
33
|
+
href,
|
|
34
|
+
newTab = false,
|
|
35
|
+
type,
|
|
36
|
+
onClick,
|
|
37
|
+
ariaLabel,
|
|
38
|
+
className,
|
|
39
|
+
style,
|
|
40
|
+
children,
|
|
41
|
+
}: ButtonProps) {
|
|
42
|
+
const classes = [
|
|
43
|
+
"btn",
|
|
44
|
+
`btn-${variant}`,
|
|
45
|
+
size === "lg" && "btn-lg",
|
|
46
|
+
onDark && "on-dark",
|
|
47
|
+
className,
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" ");
|
|
51
|
+
|
|
52
|
+
const content = (
|
|
53
|
+
<>
|
|
54
|
+
{children}
|
|
55
|
+
{arrow && (
|
|
56
|
+
<span className="arrow" aria-hidden="true">
|
|
57
|
+
→
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (href) {
|
|
64
|
+
return (
|
|
65
|
+
<a
|
|
66
|
+
href={href}
|
|
67
|
+
className={classes}
|
|
68
|
+
style={style}
|
|
69
|
+
aria-label={ariaLabel}
|
|
70
|
+
{...newTabProps(href, newTab)}
|
|
71
|
+
>
|
|
72
|
+
{content}
|
|
73
|
+
</a>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
type={type ?? "button"}
|
|
79
|
+
className={classes}
|
|
80
|
+
style={style}
|
|
81
|
+
aria-label={ariaLabel}
|
|
82
|
+
onClick={onClick}
|
|
83
|
+
>
|
|
84
|
+
{content}
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ChipProps {
|
|
4
|
+
/** Shows the small brand-colored dot before the label. */
|
|
5
|
+
dot?: boolean;
|
|
6
|
+
/** Inverts styling for dark surfaces. */
|
|
7
|
+
onDark?: boolean;
|
|
8
|
+
/** Renders the chip as an anchor (e.g. jump links on a page hero). */
|
|
9
|
+
href?: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Small mono tag/badge, used for credentials, tags, and service areas. */
|
|
14
|
+
export function Chip({ dot = false, onDark = false, href, children }: ChipProps) {
|
|
15
|
+
const classes = ["chip", onDark && "on-dark"].filter(Boolean).join(" ");
|
|
16
|
+
const content = (
|
|
17
|
+
<>
|
|
18
|
+
{dot && <span className="dot" />}
|
|
19
|
+
{children}
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
if (href) {
|
|
23
|
+
return (
|
|
24
|
+
<a className={classes} href={href}>
|
|
25
|
+
{content}
|
|
26
|
+
</a>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return <span className={classes}>{content}</span>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface EyebrowProps {
|
|
4
|
+
/** Lifts the color for dark surfaces. */
|
|
5
|
+
onDark?: boolean;
|
|
6
|
+
/** Centers the eyebrow (its trailing fade included). */
|
|
7
|
+
center?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Deprecated no-op. The trailing fade (the wordmark's signature) is now
|
|
10
|
+
* the default and only rendering; the old leading-dash mode is retired.
|
|
11
|
+
* Accepted so existing call sites keep compiling.
|
|
12
|
+
*/
|
|
13
|
+
run?: boolean;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The small mono, uppercase kicker line above a heading, trailed by a bar
|
|
19
|
+
* that fades out to the right (the practice wordmark's signature), rendered
|
|
20
|
+
* from the primary token so it carries each site's brand color.
|
|
21
|
+
*/
|
|
22
|
+
export function Eyebrow({ onDark = false, center = false, children }: EyebrowProps) {
|
|
23
|
+
const classes = ["eyebrow", onDark && "on-dark", center && "center"].filter(Boolean).join(" ");
|
|
24
|
+
return <p className={classes}>{children}</p>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface LeadProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Lead paragraph: the larger intro copy under a heading. */
|
|
10
|
+
export function Lead({ className, style, children }: LeadProps) {
|
|
11
|
+
const classes = ["lead", className].filter(Boolean).join(" ");
|
|
12
|
+
return (
|
|
13
|
+
<p className={classes} style={style}>
|
|
14
|
+
{children}
|
|
15
|
+
</p>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Eyebrow } from "./Eyebrow";
|
|
3
|
+
import { Lead } from "./Lead";
|
|
4
|
+
|
|
5
|
+
export interface SectionHeadingProps {
|
|
6
|
+
eyebrow?: string;
|
|
7
|
+
/** Heading content; may include serif emphasis spans. */
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
lead?: ReactNode;
|
|
10
|
+
center?: boolean;
|
|
11
|
+
/** Lifts colors for dark surfaces. */
|
|
12
|
+
onDark?: boolean;
|
|
13
|
+
/** Deprecated no-op: the eyebrow's trailing fade is now its only rendering. */
|
|
14
|
+
eyebrowRun?: boolean;
|
|
15
|
+
/** Heading level; sections default to h2. */
|
|
16
|
+
as?: "h1" | "h2" | "h3";
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Standard section opener: eyebrow, title, optional lead. */
|
|
21
|
+
export function SectionHeading({
|
|
22
|
+
eyebrow,
|
|
23
|
+
title,
|
|
24
|
+
lead,
|
|
25
|
+
center = false,
|
|
26
|
+
onDark = false,
|
|
27
|
+
as: Heading = "h2",
|
|
28
|
+
className,
|
|
29
|
+
}: SectionHeadingProps) {
|
|
30
|
+
const classes = ["section-head", center && "center", className].filter(Boolean).join(" ");
|
|
31
|
+
return (
|
|
32
|
+
<div className={classes}>
|
|
33
|
+
{eyebrow && (
|
|
34
|
+
<Eyebrow onDark={onDark} center={center}>
|
|
35
|
+
{eyebrow}
|
|
36
|
+
</Eyebrow>
|
|
37
|
+
)}
|
|
38
|
+
<Heading>{title}</Heading>
|
|
39
|
+
{lead && <Lead className="section-head-lead">{lead}</Lead>}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Button, type ButtonProps } from "./Button";
|
|
2
|
+
export { Chip, type ChipProps } from "./Chip";
|
|
3
|
+
export { Eyebrow, type EyebrowProps } from "./Eyebrow";
|
|
4
|
+
export { Lead, type LeadProps } from "./Lead";
|
|
5
|
+
export { SectionHeading, type SectionHeadingProps } from "./SectionHeading";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* The primitives under src/primitives: the button, the chip, the eyebrow,
|
|
2
|
+
the lead paragraph, and the section heading. Every value is a theme token.
|
|
3
|
+
They sit in the package's layer like every sheet here, so any rule a site
|
|
4
|
+
writes outside a layer still wins over them whatever its specificity. */
|
|
5
|
+
@layer atelic-ui {
|
|
6
|
+
/* CTA buttons: primary (brand), dark (ink), ghost (outline). */
|
|
7
|
+
:is(.mkt, .atelic-ui) .btn {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
gap: 0.6em;
|
|
12
|
+
padding: 0.95em 1.6em;
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
font-size: 1rem;
|
|
15
|
+
letter-spacing: -0.01em;
|
|
16
|
+
border-radius: var(--radius-pill);
|
|
17
|
+
transition:
|
|
18
|
+
transform 0.18s ease,
|
|
19
|
+
background 0.2s ease,
|
|
20
|
+
box-shadow 0.2s ease,
|
|
21
|
+
color 0.2s ease;
|
|
22
|
+
text-align: center;
|
|
23
|
+
line-height: 1;
|
|
24
|
+
}
|
|
25
|
+
:is(.mkt, .atelic-ui) .btn:active {
|
|
26
|
+
transform: translateY(1px) scale(0.99);
|
|
27
|
+
}
|
|
28
|
+
:is(.mkt, .atelic-ui) .btn-primary {
|
|
29
|
+
background: var(--primary);
|
|
30
|
+
color: #fff;
|
|
31
|
+
box-shadow: var(--primary-shadow);
|
|
32
|
+
}
|
|
33
|
+
:is(.mkt, .atelic-ui) .btn-primary:hover {
|
|
34
|
+
background: var(--primary-deep);
|
|
35
|
+
box-shadow: var(--primary-shadow-hover);
|
|
36
|
+
transform: translateY(-1px);
|
|
37
|
+
}
|
|
38
|
+
:is(.mkt, .atelic-ui) .btn-dark {
|
|
39
|
+
background: var(--ink);
|
|
40
|
+
color: var(--surface);
|
|
41
|
+
}
|
|
42
|
+
:is(.mkt, .atelic-ui) .btn-dark:hover {
|
|
43
|
+
background: #000;
|
|
44
|
+
transform: translateY(-1px);
|
|
45
|
+
}
|
|
46
|
+
:is(.mkt, .atelic-ui) .btn-ghost {
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: var(--ink);
|
|
49
|
+
box-shadow: inset 0 0 0 1.5px var(--neutral-300);
|
|
50
|
+
}
|
|
51
|
+
:is(.mkt, .atelic-ui) .btn-ghost:hover {
|
|
52
|
+
box-shadow: inset 0 0 0 1.5px var(--ink);
|
|
53
|
+
}
|
|
54
|
+
:is(.mkt, .atelic-ui) .btn-ghost.on-dark {
|
|
55
|
+
color: #fff;
|
|
56
|
+
box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.45);
|
|
57
|
+
}
|
|
58
|
+
:is(.mkt, .atelic-ui) .btn-ghost.on-dark:hover {
|
|
59
|
+
box-shadow: inset 0 0 0 1.5px #fff;
|
|
60
|
+
background: rgba(255, 255, 255, 0.08);
|
|
61
|
+
}
|
|
62
|
+
:is(.mkt, .atelic-ui) .btn-lg {
|
|
63
|
+
padding: 1.1em 2em;
|
|
64
|
+
font-size: 1.08rem;
|
|
65
|
+
}
|
|
66
|
+
:is(.mkt, .atelic-ui) .btn .arrow {
|
|
67
|
+
transition: transform 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
:is(.mkt, .atelic-ui) .btn:hover .arrow {
|
|
70
|
+
transform: translateX(3px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Chip: small mono tag for credentials, feature tags, service areas. */
|
|
74
|
+
:is(.mkt, .atelic-ui) .chip {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.5em;
|
|
78
|
+
font-family: var(--font-mono);
|
|
79
|
+
font-size: 0.72rem;
|
|
80
|
+
letter-spacing: 0.06em;
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
padding: 0.5em 0.85em;
|
|
83
|
+
border-radius: var(--radius-pill);
|
|
84
|
+
background: var(--card);
|
|
85
|
+
color: var(--ink-soft);
|
|
86
|
+
box-shadow: inset 0 0 0 1px var(--neutral-200);
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
:is(.mkt, .atelic-ui) .chip .dot {
|
|
90
|
+
width: 6px;
|
|
91
|
+
height: 6px;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
background: var(--primary);
|
|
94
|
+
}
|
|
95
|
+
:is(.mkt, .atelic-ui) .chip svg {
|
|
96
|
+
flex: none;
|
|
97
|
+
}
|
|
98
|
+
:is(.mkt, .atelic-ui) .chip.on-dark {
|
|
99
|
+
background: rgba(255, 255, 255, 0.08);
|
|
100
|
+
color: #efe7da;
|
|
101
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Eyebrow: mono uppercase kicker trailed by a bar that fades off to the
|
|
105
|
+
right — the wordmark's signature, in the brand's primary. This fade is
|
|
106
|
+
the only rendering; the old leading-dash mode is retired. */
|
|
107
|
+
:is(.mkt, .atelic-ui) .eyebrow {
|
|
108
|
+
font-family: var(--font-mono);
|
|
109
|
+
font-size: 0.74rem;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
letter-spacing: 0.18em;
|
|
112
|
+
text-transform: uppercase;
|
|
113
|
+
color: var(--primary);
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 0.6em;
|
|
117
|
+
}
|
|
118
|
+
:is(.mkt, .atelic-ui) .eyebrow.on-dark {
|
|
119
|
+
color: var(--primary-bright);
|
|
120
|
+
}
|
|
121
|
+
:is(.mkt, .atelic-ui) .eyebrow.center {
|
|
122
|
+
justify-content: center;
|
|
123
|
+
}
|
|
124
|
+
:is(.mkt, .atelic-ui) .eyebrow::after {
|
|
125
|
+
content: "";
|
|
126
|
+
width: 40px;
|
|
127
|
+
height: 2px;
|
|
128
|
+
display: inline-block;
|
|
129
|
+
/* Themes with a dedicated ruling accent (a brand color reserved for
|
|
130
|
+
rules, never text) set --rule-accent; default stays the text color. */
|
|
131
|
+
background: linear-gradient(90deg, var(--rule-accent, currentColor), transparent);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:is(.mkt, .atelic-ui) .lead {
|
|
135
|
+
font-size: clamp(1.05rem, 2vw, 1.3rem);
|
|
136
|
+
color: var(--ink-soft);
|
|
137
|
+
line-height: 1.55;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:is(.mkt, .atelic-ui) .section-head {
|
|
141
|
+
/* Width is constrained on the lead only: titles size to their section so
|
|
142
|
+
they never wrap early into dead space (the 60ch cap here was measured
|
|
143
|
+
at the body font size, boxing every title into ~510px). */
|
|
144
|
+
max-width: none;
|
|
145
|
+
}
|
|
146
|
+
:is(.mkt, .atelic-ui) .section-head.center {
|
|
147
|
+
margin-inline: auto;
|
|
148
|
+
}
|
|
149
|
+
:is(.mkt, .atelic-ui) .section-head .section-head-lead {
|
|
150
|
+
margin-top: 16px;
|
|
151
|
+
max-width: 60ch;
|
|
152
|
+
}
|
|
153
|
+
}
|