@cawalch/porchlight 0.1.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.
Files changed (47) hide show
  1. package/README.md +28 -0
  2. package/dist/porchlight.css +3765 -0
  3. package/dist/porchlight.min.css +1 -0
  4. package/package.json +59 -0
  5. package/porchlight.css +62 -0
  6. package/src/00-layer-order.css +22 -0
  7. package/src/01-reset.css +53 -0
  8. package/src/02-tokens.css +254 -0
  9. package/src/03-themes.css +79 -0
  10. package/src/04-base.css +78 -0
  11. package/src/05-layout.css +209 -0
  12. package/src/06-components/accordion.css +161 -0
  13. package/src/06-components/alert.css +102 -0
  14. package/src/06-components/avatar.css +112 -0
  15. package/src/06-components/badge.css +73 -0
  16. package/src/06-components/breadcrumb.css +111 -0
  17. package/src/06-components/button.css +180 -0
  18. package/src/06-components/card.css +186 -0
  19. package/src/06-components/chip.css +146 -0
  20. package/src/06-components/command-palette.css +201 -0
  21. package/src/06-components/data-table.css +380 -0
  22. package/src/06-components/dialog.css +148 -0
  23. package/src/06-components/drawer.css +137 -0
  24. package/src/06-components/dropdown.css +180 -0
  25. package/src/06-components/empty-state.css +85 -0
  26. package/src/06-components/field.css +125 -0
  27. package/src/06-components/file-upload.css +104 -0
  28. package/src/06-components/nav.css +185 -0
  29. package/src/06-components/pagination.css +106 -0
  30. package/src/06-components/popover-menu.css +146 -0
  31. package/src/06-components/progress.css +77 -0
  32. package/src/06-components/reveal.css +73 -0
  33. package/src/06-components/scroll-progress.css +73 -0
  34. package/src/06-components/segmented.css +113 -0
  35. package/src/06-components/skeleton.css +73 -0
  36. package/src/06-components/stat.css +107 -0
  37. package/src/06-components/stepper.css +172 -0
  38. package/src/06-components/switch.css +138 -0
  39. package/src/06-components/tabs.css +164 -0
  40. package/src/06-components/tag-input.css +77 -0
  41. package/src/06-components/textarea-auto.css +77 -0
  42. package/src/06-components/timeline.css +129 -0
  43. package/src/06-components/toast.css +175 -0
  44. package/src/06-components/toolbar.css +87 -0
  45. package/src/06-components/tooltip.css +104 -0
  46. package/src/07-utilities.css +77 -0
  47. package/src/08-enhancements.css +129 -0
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Porchlight - themes
3
+ * ===========================================================================
4
+ * Theme, density, and accessibility-media overrides. These do NOT redefine the
5
+ * palette - they steer it:
6
+ *
7
+ * - [data-theme] sets color-scheme, which drives light-dark() in tokens.
8
+ * - [data-density] resizes controls via the control tokens.
9
+ * - prefers-reduced-motion zeroes the motion scale (and any transitions).
10
+ * - forced-colors remaps semantic tokens to Windows system colors so the
11
+ * UI stays legible under High Contrast mode.
12
+ *
13
+ * `:where()` keeps every selector at zero specificity, so an app can override
14
+ * any of this without a fight. The `!important` rules under reduced-motion are
15
+ * accessibility-critical (PLAN's allow-list): motion must stop regardless of
16
+ * component-level transition declarations.
17
+ */
18
+ @layer porchlight.themes {
19
+ /* Explicit theme. Omit the attribute to inherit the system color-scheme. */
20
+ :where([data-theme="light"]) {
21
+ color-scheme: light;
22
+ }
23
+
24
+ :where([data-theme="dark"]) {
25
+ color-scheme: dark;
26
+ }
27
+
28
+ /* Density - control geometry only; spacing/type/radius are unchanged. */
29
+ :where([data-density="compact"]) {
30
+ --pl-control-block-size: 2rem;
31
+ --pl-control-padding-inline: var(--pl-space-3);
32
+ --pl-control-gap: var(--pl-space-1);
33
+ }
34
+
35
+ :where([data-density="comfortable"]) {
36
+ --pl-control-block-size: 2.5rem;
37
+ }
38
+
39
+ :where([data-density="touch"]) {
40
+ --pl-control-block-size: 2.75rem;
41
+ --pl-control-padding-inline: var(--pl-space-5);
42
+ }
43
+
44
+ /* Motion: respect the OS preference. Scale token kills framework motion;
45
+ the universal rule guarantees no stray transition outlasts it. */
46
+ @media (prefers-reduced-motion: reduce) {
47
+ :root {
48
+ --pl-motion-scale: 0;
49
+
50
+ scroll-behavior: auto;
51
+ }
52
+
53
+ *,
54
+ *::before,
55
+ *::after {
56
+ animation-duration: 1ms !important;
57
+ animation-iteration-count: 1 !important;
58
+ transition-duration: 1ms !important;
59
+ }
60
+ }
61
+
62
+ /* Forced colors (Windows High Contrast): remap to system colors. */
63
+ @media (forced-colors: active) {
64
+ :root {
65
+ --pl-color-bg: Canvas;
66
+ --pl-color-surface: Canvas;
67
+ --pl-color-surface-2: Canvas;
68
+ --pl-color-text: CanvasText;
69
+ --pl-color-text-muted: CanvasText;
70
+ --pl-color-border: ButtonBorder;
71
+ --pl-color-accent: Highlight;
72
+ --pl-color-accent-text: HighlightText;
73
+ --pl-focus-color: Highlight;
74
+ --pl-shadow-1: none;
75
+ --pl-shadow-2: none;
76
+ --pl-shadow-3: none;
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Porchlight - base layer
3
+ * ===========================================================================
4
+ * Element defaults. These give bare HTML a sensible, on-token appearance so
5
+ * pages are usable before any component class is applied.
6
+ *
7
+ * Everything is `:where()` (zero specificity) so every rule is trivially
8
+ * overridable by a component or an application. Logical properties throughout.
9
+ * No `!important` here - the reset already handles the only a11y-critical case
10
+ * (`[hidden]`).
11
+ *
12
+ * Color and type come from the token layers; theme/density/forced-colors are
13
+ * handled by `03-themes`. This layer only assembles them onto elements.
14
+ */
15
+ @layer porchlight.base {
16
+ :where(html) {
17
+ font-family: var(--pl-font-sans);
18
+ background: var(--pl-color-bg);
19
+ color: var(--pl-color-text);
20
+
21
+ /* Theme every native checkbox/radio/range/color/date input app-wide so the
22
+ brand accent is consistent without per-control styling. Inherits, so a
23
+ single root declaration covers the whole document. */
24
+ accent-color: var(--pl-color-accent);
25
+ }
26
+
27
+ :where(body) {
28
+ font-size: var(--pl-text-md);
29
+ line-height: var(--pl-leading-normal);
30
+ background: var(--pl-color-bg);
31
+ color: var(--pl-color-text);
32
+ text-rendering: optimizeLegibility;
33
+ }
34
+
35
+ :where(a) {
36
+ color: var(--pl-color-accent);
37
+ text-underline-offset: 0.18em;
38
+ text-decoration-thickness: 0.08em;
39
+ }
40
+
41
+ :where(a:hover) {
42
+ color: var(--pl-color-accent-hover);
43
+ }
44
+
45
+ :where(:focus-visible) {
46
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
47
+ outline-offset: var(--pl-focus-offset);
48
+ }
49
+
50
+ :where(h1, h2, h3, h4) {
51
+ line-height: var(--pl-leading-tight);
52
+ text-wrap: balance;
53
+ margin-block: 0;
54
+ letter-spacing: -0.02em;
55
+ }
56
+
57
+ :where(h1) {
58
+ font-weight: var(--pl-font-weight-black, 900);
59
+ letter-spacing: -0.03em;
60
+ }
61
+
62
+ :where(h2) {
63
+ font-weight: var(--pl-font-weight-bold, 700);
64
+ letter-spacing: -0.025em;
65
+ }
66
+
67
+ :where(p, ul, ol, dl, figure, blockquote) {
68
+ margin-block: 0;
69
+ }
70
+
71
+ :where(code, kbd, samp, pre) {
72
+ font-family: var(--pl-font-mono);
73
+ }
74
+
75
+ :where(::selection) {
76
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 60%);
77
+ }
78
+ }
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Porchlight - layout primitives
3
+ * ===========================================================================
4
+ * Small, composable layout helpers for the regions AROUND components - not
5
+ * opinionated page designs. They are deliberately quiet: low specificity
6
+ * (:where), token-driven gaps, and local --l-* config tokens every primitive
7
+ * exposes for instance tuning (e.g. --l-stack-gap), each with a sensible
8
+ * framework default.
9
+ *
10
+ * Built for desktop SaaS / web-app surfaces: the default states assume
11
+ * comfortable desktop density; container queries adapt a layout to its PANEL
12
+ * width (a nested app region), not only to the viewport. Viewport media logic
13
+ * is reserved for the app shell (05 app-shell block, next PR).
14
+ *
15
+ * Naming: .l-* primitives; --l-* are instance-config tokens consumed only by
16
+ * the layout layer.
17
+ */
18
+ @layer porchlight.layout {
19
+ /* Vertical flow - forms, settings sections, stacked panels. */
20
+ :where(.l-stack) {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--l-stack-gap, var(--pl-space-4));
24
+ }
25
+
26
+ /* Horizontal wrapping group - action bars, button rows, toolbar items. */
27
+ :where(.l-cluster) {
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ align-items: var(--l-cluster-align, center);
31
+ justify-content: var(--l-cluster-justify, flex-start);
32
+ gap: var(--l-cluster-gap, var(--pl-space-3));
33
+ }
34
+
35
+ /* Auto-fitting grid - dashboard widgets, KPI cards, result tiles. Each
36
+ track is at least --l-grid-min wide (clamped to 100% so a single item
37
+ never overflows) and grows to fill. */
38
+ :where(.l-grid) {
39
+ display: grid;
40
+ gap: var(--l-grid-gap, var(--pl-space-4));
41
+ grid-template-columns: repeat(
42
+ auto-fit,
43
+ minmax(min(var(--l-grid-min, 16rem), 100%), 1fr)
44
+ );
45
+ }
46
+
47
+ /* Sidebar + main - settings/filter panels, master-detail regions. The
48
+ default is the desktop two-column layout. It collapses to a single
49
+ column when its nearest ANCESTOR query container is narrower than 48rem
50
+ (an element cannot query its own size). Note: .l-sidebar does NOT declare
51
+ container-type itself - that would be misleading (self-querying is
52
+ impossible) and adds containment side-effects. Wrap it in a
53
+ container-type: inline-size element, or nest it inside
54
+ .l-app-shell__main (which is a container) to get the collapse. */
55
+ :where(.l-sidebar) {
56
+ display: grid;
57
+ gap: var(--l-sidebar-gap, var(--pl-space-4));
58
+ grid-template-columns:
59
+ minmax(0, var(--l-sidebar-size, 16rem))
60
+ minmax(0, 1fr);
61
+ }
62
+
63
+ @media (width < 48rem) {
64
+ :where(.l-sidebar) {
65
+ grid-template-columns: 1fr;
66
+ }
67
+ }
68
+
69
+ @container (inline-size < 48rem) {
70
+ :where(.l-sidebar) {
71
+ grid-template-columns: 1fr;
72
+ }
73
+ }
74
+
75
+ /* Readable content column centered within a wide app surface. Default max
76
+ is generous (88rem) for dashboards; tighten via --l-page-max for prose. */
77
+ :where(.l-page) {
78
+ inline-size: min(100% - 2 * var(--pl-space-4), var(--l-page-max, 88rem));
79
+ margin-inline: auto;
80
+ }
81
+
82
+ /* Independent scroll region - data panes, log viewers, long lists. Stable
83
+ gutter prevents layout shift when scrollbars appear/disappear; contain
84
+ stops scroll chaining to the page. */
85
+ :where(.l-scroll-area) {
86
+ overflow: auto;
87
+ scrollbar-gutter: stable;
88
+ overscroll-behavior: contain;
89
+ }
90
+
91
+ /* --------------------------------------------------------------------- */
92
+
93
+ /* App shell - the one place viewport logic belongs. A desktop SaaS app
94
+ is typically: a sticky topbar, a persistent sidebar, and a scrolling
95
+ main work area. Below 60rem (a phone or a narrow split pane) the sidebar
96
+ drops out so the work surface stays usable.
97
+
98
+ .l-app-shell__main is a query container so any .l-sidebar nested inside
99
+ it collapses correctly at narrow app widths WITHOUT a manual wrapper
100
+ (closes the container-ancestor story from the audit fix). */
101
+ :where(.l-app-shell) {
102
+ min-block-size: 100dvb;
103
+ display: grid;
104
+ grid-template:
105
+ "topbar topbar" auto
106
+ "sidebar main" minmax(0, 1fr)
107
+ / auto minmax(0, 1fr);
108
+ }
109
+
110
+ :where(.l-app-shell__topbar) {
111
+ grid-area: topbar;
112
+ position: sticky;
113
+ inset-block-start: 0;
114
+ z-index: var(--pl-z-sticky);
115
+ }
116
+
117
+ /* Glass topbar - opt in via [data-glass]. Frosted backdrop replaces an
118
+ opaque fill; a hairline border-bottom anchors the bar visually. */
119
+ :where(.l-app-shell__topbar[data-glass]) {
120
+ background: color-mix(in oklab, var(--pl-color-surface), transparent 18%);
121
+ backdrop-filter: blur(16px) saturate(180%);
122
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
123
+ border-block-end: 1px solid color-mix(
124
+ in oklab,
125
+ var(--pl-color-border),
126
+ transparent 50%
127
+ );
128
+ }
129
+
130
+ :where(.l-app-shell__sidebar) {
131
+ grid-area: sidebar;
132
+
133
+ /* Manual collapse: apps set data-sidebar="collapsed" to shrink the rail
134
+ to an icon strip without hiding it (PLAN only hid on narrow viewports).
135
+ --app-sidebar-size tunes the expanded width; --app-sidebar-collapsed
136
+ the icon width. */
137
+ inline-size: var(--app-sidebar-size, 16rem);
138
+ overflow-inline: clip;
139
+ transition: inline-size var(--pl-duration-2) var(--pl-ease-standard);
140
+ }
141
+
142
+ :where(.l-app-shell__sidebar[data-sidebar="collapsed"]) {
143
+ inline-size: var(--app-sidebar-collapsed, 3.5rem);
144
+ }
145
+
146
+ :where(.l-app-shell__main) {
147
+ grid-area: main;
148
+ min-inline-size: 0;
149
+
150
+ /* Main is the container .l-sidebar collapses against (see header note). */
151
+ container-type: inline-size;
152
+ overflow: auto;
153
+ scrollbar-gutter: stable;
154
+ overscroll-behavior: contain;
155
+ }
156
+
157
+ /* Viewport fallback: on a genuinely narrow viewport the sidebar is hidden
158
+ (the work surface takes priority). This is the ONLY viewport media query
159
+ in the layout layer - components adapt via container queries. */
160
+ @media (width < 60rem) {
161
+ :where(.l-app-shell) {
162
+ grid-template:
163
+ "topbar" auto
164
+ "main" minmax(0, 1fr)
165
+ / minmax(0, 1fr);
166
+ }
167
+
168
+ :where(.l-app-shell__sidebar) {
169
+ display: none;
170
+ }
171
+ }
172
+
173
+ /* --------------------------------------------------------------------- */
174
+
175
+ /* Multi-column masonry - card walls, search results, image grids. Uses
176
+ CSS multi-column layout (simpler than grid masonry and works in all
177
+ browsers). Items break-inside avoid so cards don't split across
178
+ columns. Tune column count via --l-columns-count or let it auto. */
179
+ :where(.l-columns) {
180
+ --l-columns-width: 16rem;
181
+
182
+ columns: var(--l-columns-width) var(--l-columns-count, auto);
183
+ column-gap: var(--l-columns-gap, var(--pl-space-4));
184
+ }
185
+
186
+ :where(.l-columns > *) {
187
+ break-inside: avoid;
188
+ margin-block-end: var(--l-columns-gap, var(--pl-space-4));
189
+ }
190
+
191
+ /* Centered max-width container - general-purpose wrapper for app
192
+ surfaces. Distinct from .l-page (which targets prose at 88rem).
193
+ Use for dashboards, settings, lists. */
194
+ :where(.l-container) {
195
+ inline-size: min(
196
+ 100% - 2 * var(--l-container-pad, var(--pl-space-4)),
197
+ var(--l-container-max, 80rem)
198
+ );
199
+ margin-inline: auto;
200
+ }
201
+
202
+ /* Inset - constrained content inside a full-bleed parent. For captions,
203
+ forms, or text within a full-width hero/banner. Centers and constrains
204
+ without adding its own vertical rhythm. */
205
+ :where(.l-inset) {
206
+ inline-size: min(100%, var(--l-inset-max, 48rem));
207
+ margin-inline: auto;
208
+ }
209
+ }
@@ -0,0 +1,161 @@
1
+ /*
2
+ * Porchlight - accordion component
3
+ * ===========================================================================
4
+ * A collapsible disclosure built on native <details>/<summary>. Animates
5
+ * open/close to/from height: auto using interpolate-size: allow-keywords
6
+ * (declared globally in 08-enhancements). As a universal fallback, uses the
7
+ * grid-template-rows: 0fr -> 1fr technique which works without it.
8
+ *
9
+ * Structure:
10
+ * <details class="c-accordion__item">
11
+ * <summary class="c-accordion__header">
12
+ * <span>Title</span>
13
+ * <svg class="c-accordion__icon" ...>
14
+ * </summary>
15
+ * <div class="c-accordion__panel">
16
+ * <div class="c-accordion__content">...</div>
17
+ * </div>
18
+ * </details>
19
+ *
20
+ * Wrap multiple items in .c-accordion. For single-open behavior, use
21
+ * [data-variant="single"] (implemented via :has()).
22
+ *
23
+ * The native <summary> marker is hidden and replaced with a custom chevron.
24
+ */
25
+ @layer porchlight.components {
26
+ @scope (.c-accordion) {
27
+ :scope {
28
+ --c-accordion-border: var(--pl-color-border);
29
+ --c-accordion-bg: var(--pl-color-surface);
30
+ --c-accordion-radius: var(--pl-radius-xl);
31
+ --c-accordion-header-pad: var(--pl-space-4);
32
+ --c-accordion-content-pad: 0 var(--pl-space-4) var(--pl-space-4);
33
+ --c-accordion-gap: var(--pl-space-2);
34
+
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--c-accordion-gap);
38
+ }
39
+ }
40
+
41
+ @scope (.c-accordion__item) {
42
+ :scope {
43
+ border: 1px solid var(--c-accordion-border, var(--pl-color-border));
44
+ border-radius: var(--c-accordion-radius, var(--pl-radius-lg));
45
+ background: var(--c-accordion-bg, var(--pl-color-surface));
46
+ overflow: hidden;
47
+ }
48
+
49
+ .c-accordion__header {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ gap: var(--pl-space-3);
54
+ padding: var(--c-accordion-header-pad, var(--pl-space-4));
55
+ font-weight: var(--pl-font-weight-semibold);
56
+ cursor: pointer;
57
+ list-style: none;
58
+ user-select: none;
59
+
60
+ /* Hide the native disclosure marker. */
61
+ &::-webkit-details-marker {
62
+ display: none;
63
+ }
64
+
65
+ &::marker {
66
+ content: "";
67
+ }
68
+ }
69
+
70
+ .c-accordion__header:hover {
71
+ background: var(--pl-color-surface-2);
72
+ }
73
+
74
+ .c-accordion__header:focus-visible {
75
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
76
+ outline-offset: calc(var(--pl-focus-offset) * -1);
77
+ }
78
+
79
+ .c-accordion__icon {
80
+ flex-shrink: 0;
81
+ inline-size: 1.25rem;
82
+ block-size: 1.25rem;
83
+ color: var(--pl-color-text-muted);
84
+ transition: rotate var(--pl-duration-2) var(--pl-ease-standard);
85
+ }
86
+
87
+ /* Rotate the chevron when open. */
88
+ :scope[open] .c-accordion__icon {
89
+ rotate: 180deg;
90
+ }
91
+
92
+ .c-accordion__panel {
93
+ /* Universal fallback: grid-row animation technique. Works in ALL
94
+ browsers (no interpolate-size needed). The wrapper collapses to
95
+ 0fr (hidden) then expands to 1fr (visible). */
96
+ display: grid;
97
+ grid-template-rows: 0fr;
98
+ transition: grid-template-rows var(--pl-duration-3)
99
+ var(--pl-ease-standard);
100
+ }
101
+
102
+ :scope[open] .c-accordion__panel {
103
+ grid-template-rows: 1fr;
104
+ }
105
+
106
+ .c-accordion__content {
107
+ overflow: hidden;
108
+ padding: var(
109
+ --c-accordion-content-pad,
110
+ 0 var(--pl-space-4) var(--pl-space-4)
111
+ );
112
+ color: var(--pl-color-text-muted);
113
+ line-height: var(--pl-leading-normal);
114
+ }
115
+
116
+ /* Where interpolate-size is supported: animate height: auto directly.
117
+ This produces smoother results than the grid technique. */
118
+ @supports (interpolate-size: allow-keywords) {
119
+ .c-accordion__panel {
120
+ display: block;
121
+ overflow: hidden;
122
+ grid-template-rows: initial;
123
+ block-size: 0;
124
+ transition: block-size var(--pl-duration-3) var(--pl-ease-standard);
125
+ }
126
+
127
+ :scope[open] .c-accordion__panel {
128
+ block-size: auto;
129
+ }
130
+
131
+ /* Starting state for the open transition. */
132
+ @starting-style {
133
+ :scope[open] .c-accordion__panel {
134
+ block-size: 0;
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ /* Single-open variant: close other items when one opens. Uses :has() to
141
+ detect which item is open and collapse siblings. */
142
+ :where(.c-accordion[data-variant="single"])
143
+ > .c-accordion__item:not([open]):has(~ .c-accordion__item[open]),
144
+ :where(.c-accordion[data-variant="single"])
145
+ > .c-accordion__item[open]
146
+ ~ .c-accordion__item:not([open]) {
147
+ /* These remain closed (default state). The :has() ensures only one
148
+ [open] item survives visually; JS should manage the actual [open]
149
+ attribute removal for full single-open behavior. */
150
+ }
151
+
152
+ @media (forced-colors: active) {
153
+ :where(.c-accordion__item) {
154
+ border-color: ButtonBorder;
155
+ }
156
+
157
+ :where(.c-accordion__header:focus-visible) {
158
+ outline-color: Highlight;
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,102 @@
1
+ /*
2
+ * Porchlight - alert component
3
+ * ===========================================================================
4
+ * An inline notification - contextual feedback within the page flow (not an
5
+ * overlay like toast). Four tones via [data-tone]: info (default), success,
6
+ * warning, danger. Each consumes the semantic -bg/-text pairs for WCAG-AA.
7
+ *
8
+ * Structure: .c-alert > .c-alert__icon (optional) + .c-alert__title +
9
+ * .c-alert__body. A leading icon slot anchors the tone visually; the tone
10
+ * color on the icon + a tinted background carry the semantic without relying
11
+ * on color alone (the icon shape does the heavy lifting for colorblind users).
12
+ *
13
+ * Density-aware: [data-density] on an ancestor tightens the padding.
14
+ */
15
+ @layer porchlight.components {
16
+ @scope (.c-alert) {
17
+ :scope {
18
+ --c-alert-tone: var(--pl-color-accent);
19
+ --c-alert-tone-bg: var(--pl-color-accent);
20
+ --c-alert-pad: var(--pl-space-4);
21
+ --c-alert-gap: var(--pl-space-3);
22
+
23
+ display: grid;
24
+ grid-template-columns: auto 1fr;
25
+ gap: var(--c-alert-gap);
26
+ padding: var(--c-alert-pad);
27
+ border: 1px solid var(--pl-color-border);
28
+ border-inline-start: var(--pl-accent-bar-width) solid var(--c-alert-tone);
29
+ border-radius: var(--pl-radius-xl);
30
+ background: color-mix(
31
+ in oklab,
32
+ var(--c-alert-tone-bg),
33
+ var(--pl-color-surface) 92%
34
+ );
35
+ color: var(--pl-color-text);
36
+ }
37
+
38
+ .c-alert__icon {
39
+ display: flex;
40
+ align-items: flex-start;
41
+ color: var(--c-alert-tone);
42
+ flex-shrink: 0;
43
+ }
44
+
45
+ .c-alert__icon svg {
46
+ inline-size: 1.25rem;
47
+ block-size: 1.25rem;
48
+ }
49
+
50
+ .c-alert__content {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--pl-space-1);
54
+ min-inline-size: 0;
55
+ }
56
+
57
+ .c-alert__title {
58
+ font-weight: var(--pl-font-weight-semibold);
59
+ font-size: var(--pl-text-sm);
60
+ color: var(--pl-color-text);
61
+ }
62
+
63
+ .c-alert__body {
64
+ font-size: var(--pl-text-sm);
65
+ color: var(--pl-color-text-muted);
66
+ }
67
+
68
+ /* Tone variants. */
69
+ :scope[data-tone="success"] {
70
+ --c-alert-tone: var(--pl-color-success);
71
+ --c-alert-tone-bg: var(--pl-color-success-bg);
72
+ }
73
+
74
+ :scope[data-tone="warning"] {
75
+ --c-alert-tone: var(--pl-color-warning);
76
+ --c-alert-tone-bg: var(--pl-color-warning-bg);
77
+ }
78
+
79
+ :scope[data-tone="danger"] {
80
+ --c-alert-tone: var(--pl-color-danger);
81
+ --c-alert-tone-bg: var(--pl-color-danger-bg);
82
+ }
83
+
84
+ /* A dismiss button in the top-right corner. */
85
+ .c-alert__close {
86
+ position: absolute;
87
+ inset-block-start: var(--pl-space-2);
88
+ inset-inline-end: var(--pl-space-2);
89
+ }
90
+ }
91
+
92
+ /* Make the alert position-relative so the close button can anchor. */
93
+ :where(.c-alert) {
94
+ position: relative;
95
+ }
96
+
97
+ @media (forced-colors: active) {
98
+ :where(.c-alert) {
99
+ border-color: CanvasText;
100
+ }
101
+ }
102
+ }