@gemboss/website-ui 0.4.0 → 0.6.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 +43 -5
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/bin/lib/check.mjs +77 -0
- package/css/article.css +237 -0
- package/css/components.css +23 -0
- package/css/footer.css +3 -0
- package/css/forms.css +154 -0
- package/css/prose.css +29 -16
- package/css/ratings.css +78 -0
- package/css/tokens.css +25 -0
- package/demo/article.html +359 -0
- package/demo/assets/article-hero.svg +1 -0
- package/demo/assets/avatar-gemboss.svg +1 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/assets/post-headless.svg +1 -0
- package/demo/assets/post-motion.svg +1 -0
- package/demo/assets/post-output.svg +1 -0
- package/demo/assets/post-speed.svg +1 -0
- package/demo/assets/post-theme.svg +1 -0
- package/demo/assets/post-voice.svg +1 -0
- package/demo/blog.html +314 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +2 -0
- package/docs/components.md +334 -12
- package/docs/markup-contract.md +123 -7
- package/docs/theming.md +4 -3
- package/js/website.js +68 -8
- package/js/website.mjs +80 -7
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/system.json +12 -1
- package/templates/footer.html +15 -0
- package/templates/sections/article.html +93 -0
- package/templates/sections/form.html +42 -0
- package/templates/sections/post-list.html +65 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +553 -17
- package/website.min.css +1 -1
package/css/article.css
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/* article (0.5.0): the parts around a long read and the blog index. Breadcrumbs, the meta row, the article layout with
|
|
2
|
+
its contents list (.gw-toc), callouts, the post list and prev / next pagination. The article's own HTML stays in
|
|
3
|
+
.gw-prose (prose.css); .gw-card__media is in components.css. */
|
|
4
|
+
|
|
5
|
+
/* breadcrumbs: a nav with an <ol>, the last item aria-current="page". The separator is drawn here (a chevron mask,
|
|
6
|
+
no character), so screen readers read the trail and not the punctuation. The current item stays on one line and
|
|
7
|
+
is cut with an ellipsis on a phone: the h1 right under it says it in full. */
|
|
8
|
+
.gw-breadcrumbs { color: var(--gw-muted); font-size: 13px; line-height: 1.5; }
|
|
9
|
+
.gw-breadcrumbs > ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0 8px; margin: 0; padding: 0; list-style: none; }
|
|
10
|
+
.gw-breadcrumbs li { display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; }
|
|
11
|
+
.gw-breadcrumbs li + li::before {
|
|
12
|
+
content: "";
|
|
13
|
+
flex: none;
|
|
14
|
+
width: 12px;
|
|
15
|
+
height: 12px;
|
|
16
|
+
background: currentColor;
|
|
17
|
+
opacity: 0.6;
|
|
18
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
19
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
20
|
+
}
|
|
21
|
+
.gw-breadcrumbs a { display: inline-flex; align-items: center; min-height: 24px; color: var(--gw-muted); }
|
|
22
|
+
.gw-breadcrumbs a:hover { color: var(--gw-ink); text-decoration: underline; text-underline-offset: 4px; }
|
|
23
|
+
.gw-breadcrumbs [aria-current="page"] {
|
|
24
|
+
display: block;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
color: var(--gw-ink);
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
line-height: 24px;
|
|
30
|
+
text-overflow: ellipsis;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* meta row: author (an optional 24px avatar), category, published or updated <time datetime>, read time, each item a
|
|
35
|
+
<span> (or a <time>, an <a>) straight in the row. Small muted text on 24px lines that wraps. A dot sits before every
|
|
36
|
+
item; the row is pulled 16px to the left and clipped there, so the dot before the first item of each line is hidden
|
|
37
|
+
and a wrapped line never starts with a separator. */
|
|
38
|
+
.gw-article-meta {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
align-items: center;
|
|
42
|
+
row-gap: 4px;
|
|
43
|
+
margin-inline-start: -16px;
|
|
44
|
+
clip-path: inset(-8px -8px -8px 12px);
|
|
45
|
+
color: var(--gw-muted);
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
line-height: 24px;
|
|
48
|
+
}
|
|
49
|
+
.gw-article-meta > *::before {
|
|
50
|
+
content: "";
|
|
51
|
+
display: inline-block;
|
|
52
|
+
width: 4px;
|
|
53
|
+
height: 4px;
|
|
54
|
+
margin-inline: 6px;
|
|
55
|
+
border-radius: var(--gw-radius-chip);
|
|
56
|
+
background: var(--gw-line-strong);
|
|
57
|
+
vertical-align: middle;
|
|
58
|
+
}
|
|
59
|
+
.gw-article-meta a {
|
|
60
|
+
display: inline-block;
|
|
61
|
+
color: var(--gw-accent-strong);
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
text-decoration: underline;
|
|
64
|
+
text-decoration-thickness: 1px;
|
|
65
|
+
text-underline-offset: 4px;
|
|
66
|
+
text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
|
|
67
|
+
}
|
|
68
|
+
.gw-article-meta a:hover { text-decoration-color: currentColor; }
|
|
69
|
+
.gw-article-meta__avatar { display: inline-block; width: 24px; height: 24px; margin-inline-end: 8px; border-radius: var(--gw-radius-chip); object-fit: cover; vertical-align: top; }
|
|
70
|
+
|
|
71
|
+
/* the article page (templates/sections/article.html): <article class="gw-wrap gw-article"> holding __head
|
|
72
|
+
(breadcrumbs, h1, meta), an optional __hero image, the .gw-toc and the .gw-prose, in that order. On phones one
|
|
73
|
+
column, the contents folded above the prose. From 1080px (the kit's desktop width, where the header shows its nav)
|
|
74
|
+
two columns: the reading column (68ch, the prose measure) and the contents beside it, sticky under the header. */
|
|
75
|
+
.gw-article { padding-block: 32px 64px; }
|
|
76
|
+
.gw-article__head > * + * { margin-top: 16px; }
|
|
77
|
+
.gw-article__head h1 { font-size: var(--gw-display-h2); }
|
|
78
|
+
.gw-article__hero { margin-top: 32px; }
|
|
79
|
+
.gw-article__hero img { width: 100%; border-radius: var(--gw-radius-card); background: var(--gw-bg-soft); }
|
|
80
|
+
.gw-article__hero figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
81
|
+
.gw-article > .gw-toc { margin-top: 32px; }
|
|
82
|
+
.gw-article > .gw-prose { margin-top: 40px; }
|
|
83
|
+
@media (min-width: 768px) {
|
|
84
|
+
.gw-article { padding-block: 48px 96px; }
|
|
85
|
+
.gw-article__hero { margin-top: 40px; }
|
|
86
|
+
}
|
|
87
|
+
@media (min-width: 1080px) {
|
|
88
|
+
.gw-article {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: minmax(0, 68ch) minmax(200px, 240px);
|
|
91
|
+
grid-template-rows: auto auto 1fr;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
column-gap: 64px;
|
|
94
|
+
}
|
|
95
|
+
.gw-article > * { grid-column: 1; }
|
|
96
|
+
.gw-article > .gw-article__head { grid-row: 1; }
|
|
97
|
+
.gw-article > .gw-article__hero { grid-row: 2; }
|
|
98
|
+
.gw-article > .gw-prose { grid-row: 3; }
|
|
99
|
+
.gw-article > .gw-toc { grid-column: 2; grid-row: 2 / 4; margin-top: 40px; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* contents: <nav class="gw-toc" aria-label="On this page" data-gw-toc> holding a <details>, its <summary> and an <ol>
|
|
103
|
+
of links to the article's h2s (a nested <ol> for h3s). Plain links: they work without the script. website.js marks
|
|
104
|
+
the section in view (aria-current="true", the accent bar) and opens the <details> from 1080px, where the list sits
|
|
105
|
+
beside the article and sticks under the header; below 1080px it is folded above the article and a link closes it.
|
|
106
|
+
The weight never changes with the mark, so the list does not move. */
|
|
107
|
+
.gw-toc { font-size: 14px; line-height: 1.45; }
|
|
108
|
+
.gw-toc > details { border: 1px solid var(--gw-line); border-radius: var(--gw-radius-card); background: var(--gw-surface); }
|
|
109
|
+
.gw-toc summary {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
gap: 12px;
|
|
114
|
+
min-height: var(--gw-tap-min);
|
|
115
|
+
padding: 8px 16px;
|
|
116
|
+
color: var(--gw-ink);
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
list-style: none;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
.gw-toc summary::-webkit-details-marker { display: none; }
|
|
122
|
+
.gw-toc summary::after {
|
|
123
|
+
content: "";
|
|
124
|
+
flex: none;
|
|
125
|
+
width: 12px;
|
|
126
|
+
height: 12px;
|
|
127
|
+
background: currentColor;
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
130
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
131
|
+
transition: transform 0.18s var(--gw-ease);
|
|
132
|
+
}
|
|
133
|
+
.gw-toc details[open] > summary::after { transform: rotate(180deg); }
|
|
134
|
+
.gw-toc ol { margin: 0; padding: 0; list-style: none; }
|
|
135
|
+
/* the rail is a background, so the list can scroll inside itself without clipping the bar of the marked link */
|
|
136
|
+
.gw-toc details > ol { margin: 0 16px 16px; background: linear-gradient(var(--gw-line), var(--gw-line)) 0 0 / 1px 100% no-repeat; }
|
|
137
|
+
.gw-toc a {
|
|
138
|
+
display: block;
|
|
139
|
+
padding: 6px 8px 6px 16px;
|
|
140
|
+
border-left: 2px solid transparent;
|
|
141
|
+
color: var(--gw-muted);
|
|
142
|
+
}
|
|
143
|
+
.gw-toc ol ol a { padding-left: 32px; }
|
|
144
|
+
.gw-toc a:hover { color: var(--gw-ink); }
|
|
145
|
+
.gw-toc a:focus-visible { outline-offset: -2px; }
|
|
146
|
+
.gw-toc a[aria-current="true"] { border-left-color: var(--gw-accent); color: var(--gw-ink); }
|
|
147
|
+
/* from 1080px: sticky beside the article; a long list scrolls inside itself, under the summary. Under the floating
|
|
148
|
+
header it sticks below the card. */
|
|
149
|
+
@media (min-width: 1080px) {
|
|
150
|
+
.gw-toc { position: sticky; top: calc(var(--gw-header-height) + 32px); align-self: start; }
|
|
151
|
+
.gw-toc > details { border: 0; border-radius: var(--gw-radius-0); background: transparent; }
|
|
152
|
+
.gw-toc summary { min-height: 32px; padding: 0; color: var(--gw-muted); font-size: 13px; }
|
|
153
|
+
.gw-toc details > ol {
|
|
154
|
+
max-height: calc(100vh - var(--gw-header-height) - 120px);
|
|
155
|
+
margin: 8px 0 0;
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
overscroll-behavior: contain;
|
|
158
|
+
}
|
|
159
|
+
:root:has(.gw-header--float) .gw-toc { top: calc(var(--gw-header-height) + var(--gw-header-gap) + 32px); }
|
|
160
|
+
:root:has(.gw-header--float) .gw-toc details > ol { max-height: calc(100vh - var(--gw-header-height) - var(--gw-header-gap) - 120px); }
|
|
161
|
+
}
|
|
162
|
+
/* a contents link glides to its section, except for a visitor who asked for less motion (base.css stops it too) */
|
|
163
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
164
|
+
:root:has(.gw-toc) { scroll-behavior: smooth; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* callout: a note, a tip or a warning inside an article. The kind is said in words by __label (Note, Tip, Warning,
|
|
168
|
+
or the reader's language), with a 16px icon before it; the tint only repeats it. Tints are the kit's: the accent's,
|
|
169
|
+
success's and warning's -soft grounds; the icon takes the solid hue. role="note" on the box. */
|
|
170
|
+
.gw-callout {
|
|
171
|
+
padding: 16px 20px;
|
|
172
|
+
border-radius: var(--gw-radius-card);
|
|
173
|
+
background: var(--gw-accent-soft);
|
|
174
|
+
color: var(--gw-ink);
|
|
175
|
+
font-size: 16px;
|
|
176
|
+
line-height: 1.6;
|
|
177
|
+
}
|
|
178
|
+
.gw-callout > * + * { margin-top: 8px; }
|
|
179
|
+
.gw-callout--tip { background: var(--gw-success-soft); }
|
|
180
|
+
.gw-callout--warning { background: var(--gw-warning-soft); }
|
|
181
|
+
.gw-callout__label { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; line-height: 1.5; }
|
|
182
|
+
.gw-callout__label::before {
|
|
183
|
+
content: "";
|
|
184
|
+
flex: none;
|
|
185
|
+
width: 16px;
|
|
186
|
+
height: 16px;
|
|
187
|
+
background: var(--gw-accent);
|
|
188
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11v5M12 7.5v.5'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
189
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11v5M12 7.5v.5'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
190
|
+
}
|
|
191
|
+
.gw-callout--tip > .gw-callout__label::before {
|
|
192
|
+
background: var(--gw-success);
|
|
193
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.2h5c0-.9.4-1.7 1.1-2.2A6 6 0 0 0 12 3z'/%3E%3C/svg%3E");
|
|
194
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.2h5c0-.9.4-1.7 1.1-2.2A6 6 0 0 0 12 3z'/%3E%3C/svg%3E");
|
|
195
|
+
}
|
|
196
|
+
.gw-callout--warning > .gw-callout__label::before {
|
|
197
|
+
background: var(--gw-warning);
|
|
198
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5 2.8 19.5h18.4z'/%3E%3Cpath d='M12 10v4M12 16.8v.2'/%3E%3C/svg%3E");
|
|
199
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5 2.8 19.5h18.4z'/%3E%3Cpath d='M12 10v4M12 16.8v.2'/%3E%3C/svg%3E");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* post list: the blog index and "more from the blog". A <ul role="list"> of post cards: .gw-card with a
|
|
203
|
+
.gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
|
|
204
|
+
and the link's name is the title alone), one or two lines of excerpt and a .gw-article-meta row at the foot. One
|
|
205
|
+
column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
|
|
206
|
+
.gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin: 0; padding: 0; list-style: none; }
|
|
207
|
+
.gw-post-list > li { display: flex; min-width: 0; }
|
|
208
|
+
.gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
|
|
209
|
+
.gw-post:hover { border-color: var(--gw-line-strong); }
|
|
210
|
+
.gw-post > .gw-card__media + * { margin-top: 20px; }
|
|
211
|
+
.gw-post > .gw-label + .gw-post__title { margin-top: 8px; }
|
|
212
|
+
.gw-post__title { font-size: 20px; line-height: 1.3; }
|
|
213
|
+
.gw-post__title a { color: var(--gw-ink); }
|
|
214
|
+
.gw-post__title a::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--gw-radius-card); }
|
|
215
|
+
.gw-post:hover .gw-post__title a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
|
|
216
|
+
.gw-post__title a:focus-visible { outline: 0; }
|
|
217
|
+
.gw-post__title a:focus-visible::after { outline: 2px solid var(--gw-accent); outline-offset: 2px; }
|
|
218
|
+
.gw-post__title + p { margin-top: 8px; }
|
|
219
|
+
.gw-post > .gw-article-meta { margin-top: auto; padding-top: 16px; }
|
|
220
|
+
@media (min-width: 768px) {
|
|
221
|
+
.gw-post-list { gap: 24px; }
|
|
222
|
+
.gw-post-list:not(.gw-post-list--compact) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
223
|
+
}
|
|
224
|
+
@media (min-width: 1080px) {
|
|
225
|
+
.gw-post-list:not(.gw-post-list--compact) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
226
|
+
}
|
|
227
|
+
.gw-post-list--compact { gap: 0; border-top: 1px solid var(--gw-line); }
|
|
228
|
+
.gw-post-list--compact .gw-post { padding: 20px 0; border: 0; border-bottom: 1px solid var(--gw-line); border-radius: var(--gw-radius-0); background: transparent; box-shadow: none; }
|
|
229
|
+
.gw-post-list--compact .gw-post__title a::after { border-radius: var(--gw-radius-0); }
|
|
230
|
+
.gw-post-list--compact .gw-card__media { display: none; }
|
|
231
|
+
.gw-post-list--compact .gw-post > .gw-article-meta { padding-top: 8px; }
|
|
232
|
+
|
|
233
|
+
/* pagination: previous and next as links, a muted "Page 2 of 12" between them. Markup only, no script. */
|
|
234
|
+
.gw-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 16px; }
|
|
235
|
+
* + .gw-pagination { margin-top: 40px; }
|
|
236
|
+
.gw-pagination__status { color: var(--gw-muted); font-size: 14px; }
|
|
237
|
+
.gw-pagination > [rel="next"] { margin-inline-start: auto; }
|
package/css/components.css
CHANGED
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
.gw-btn--secondary:active { background: var(--gw-bg-soft); border-color: var(--gw-ink); }
|
|
44
44
|
.gw-btn--ghost:active { background: var(--gw-line); }
|
|
45
45
|
.gw-btn--on-night:active { background: var(--gw-on-night); opacity: 0.76; }
|
|
46
|
+
/* destructive (0.6.0, gap #28): delete, cancel a plan, erase my data. The danger fill with on-danger words (6.5:1 or
|
|
47
|
+
more, light and dark); hover and pressed one step stronger (towards the ink: darker in light, lighter in dark). It is
|
|
48
|
+
the action of a page that destroys something, never a second primary beside .gw-btn. */
|
|
49
|
+
.gw-btn--danger { background: var(--gw-danger); color: var(--gw-on-danger); }
|
|
50
|
+
.gw-btn--danger:hover, .gw-btn--danger:active { background: color-mix(in srgb, var(--gw-danger) 84%, var(--gw-ink)); }
|
|
46
51
|
/* loading: aria-busy="true" on the <button>. The label stays (it keeps the width and the accessible name) but is not
|
|
47
52
|
painted; a 16px ring in the button's own ink turns in its place, the dashboard's spinner. ::before, because
|
|
48
53
|
.gw-btn--sm draws its hit area with ::after. website.js ignores presses on a busy button (no double submit). */
|
|
@@ -171,6 +176,9 @@
|
|
|
171
176
|
.gw-badge--success { background: var(--gw-success-soft); color: var(--gw-ink); }
|
|
172
177
|
.gw-badge--success::before, .gw-badge--warning::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-success); }
|
|
173
178
|
.gw-badge--warning::before { background: var(--gw-warning); }
|
|
179
|
+
/* an error (0.6.0, gap #28): the danger dot, ink words on the danger tint, as success's */
|
|
180
|
+
.gw-badge--danger { background: var(--gw-danger-soft); color: var(--gw-ink); }
|
|
181
|
+
.gw-badge--danger::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-danger); }
|
|
174
182
|
|
|
175
183
|
/* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
|
|
176
184
|
shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
|
|
@@ -185,6 +193,21 @@
|
|
|
185
193
|
.gw-card--soft { background: var(--gw-bg-soft); box-shadow: none; }
|
|
186
194
|
.gw-card--link { display: block; color: inherit; }
|
|
187
195
|
.gw-card--link:hover { border-color: var(--gw-line-strong); }
|
|
196
|
+
/* a card's picture (0.5.0, gap #33): the first child of the card, edge to edge across its top, its corners the card's
|
|
197
|
+
radius inside the hairline. An aspect-ratio box (16:9; --wide 1.91:1, the share-image shape, so a post's own
|
|
198
|
+
1200 x 630 image fits uncut; --square 1:1) that the image covers, on the soft ground while it loads. */
|
|
199
|
+
.gw-card__media {
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
aspect-ratio: 16 / 9;
|
|
202
|
+
margin: -24px -24px 0;
|
|
203
|
+
border-top-left-radius: calc(var(--gw-radius-card) - 1px);
|
|
204
|
+
border-top-right-radius: calc(var(--gw-radius-card) - 1px);
|
|
205
|
+
background: var(--gw-bg-soft);
|
|
206
|
+
}
|
|
207
|
+
.gw-card__media--wide { aspect-ratio: 1200 / 630; }
|
|
208
|
+
.gw-card__media--square { aspect-ratio: 1 / 1; }
|
|
209
|
+
.gw-card__media :is(img, video) { width: 100%; height: 100%; object-fit: cover; }
|
|
210
|
+
@media (min-width: 768px) { .gw-card__media { margin: -32px -32px 0; } }
|
|
188
211
|
:where(.gw-card, .gw-feature, .gw-plan) > :is(h3, h4) + p { margin-top: 8px; }
|
|
189
212
|
:where(.gw-card, .gw-feature) h3 { font-size: 20px; }
|
|
190
213
|
:where(.gw-card, .gw-feature) p { font-size: 16px; color: var(--gw-muted); }
|
package/css/footer.css
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
.gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
|
|
15
15
|
.gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
|
|
16
16
|
.gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
|
|
17
|
+
/* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
|
|
18
|
+
reader agrees to; only when the form is wired to a list that keeps the address */
|
|
19
|
+
.gw-footer__newsletter { margin-top: 24px; }
|
|
17
20
|
.gw-footer__col ul { margin: 0; padding: 0; list-style: none; }
|
|
18
21
|
.gw-footer__col a { display: block; padding-block: 6px; color: var(--gw-muted); }
|
|
19
22
|
.gw-footer__col a:hover { color: var(--gw-ink); }
|
package/css/forms.css
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* forms (0.6.0, gap #28): a field (label, control, error, hint), the controls, a group of choices, the inline row and
|
|
2
|
+
the form stack. A public site asks as little as it can: one field beats a form, and a form keeps two or three fields
|
|
3
|
+
(AGENTS.md section 5). The look is the dashboard's field family (@gemboss/ui gemboss-field, ui-select, ui-textarea,
|
|
4
|
+
ui-check, ui-help, ui-error) at the website's sizes: as tall as a button (40px, 44px on touch), 16px words (a phone
|
|
5
|
+
does not zoom the page), the action radius of the site's profile, and a 1px border that reaches 3:1
|
|
6
|
+
(--gw-line-control). The page owns validation: it sets aria-invalid="true" and writes the message; the kit draws it. */
|
|
7
|
+
|
|
8
|
+
/* a field: the label above the control, the error and then the hint under it, 8px apart */
|
|
9
|
+
.gw-field { display: grid; gap: 8px; min-width: 0; }
|
|
10
|
+
.gw-field__label { display: block; color: var(--gw-ink); font-size: 14px; font-weight: 600; line-height: 1.5; }
|
|
11
|
+
/* two classes, so a context that styles its paragraphs (the footer's brand block) does not move or recolour them */
|
|
12
|
+
:is(.gw-field, .gw-choices) > :is(.gw-field__hint, .gw-field__error) { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.5; }
|
|
13
|
+
:is(.gw-field, .gw-choices) > .gw-field__hint { color: var(--gw-muted); }
|
|
14
|
+
/* the error: an icon and words, in the danger colour; never the colour alone. Empty, it takes no room, so a page can
|
|
15
|
+
keep it in the markup (named by aria-describedby) and fill it when the visitor sends the form. */
|
|
16
|
+
:is(.gw-field, .gw-choices) > .gw-field__error { display: flex; gap: 6px; color: var(--gw-danger); font-weight: 500; }
|
|
17
|
+
:is(.gw-field, .gw-choices) > .gw-field__error:empty { display: none; }
|
|
18
|
+
.gw-field__error::before {
|
|
19
|
+
content: "";
|
|
20
|
+
flex: none;
|
|
21
|
+
width: 16px;
|
|
22
|
+
height: 16px;
|
|
23
|
+
margin-top: 2px;
|
|
24
|
+
margin-top: calc((1lh - 16px) / 2);
|
|
25
|
+
background: currentColor;
|
|
26
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
27
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* the controls: one box for a line of text, a choice from a list and a few lines. Full width of their field. */
|
|
31
|
+
.gw-input,
|
|
32
|
+
.gw-select,
|
|
33
|
+
.gw-textarea {
|
|
34
|
+
display: block;
|
|
35
|
+
width: 100%;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
height: var(--gw-button-height);
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0 12px;
|
|
40
|
+
border: 1px solid var(--gw-line-control);
|
|
41
|
+
border-radius: var(--gw-radius-action);
|
|
42
|
+
background-color: var(--gw-surface);
|
|
43
|
+
color: var(--gw-ink);
|
|
44
|
+
font-size: 16px;
|
|
45
|
+
line-height: 1.5;
|
|
46
|
+
-webkit-appearance: none;
|
|
47
|
+
appearance: none;
|
|
48
|
+
}
|
|
49
|
+
:is(.gw-input, .gw-textarea)::placeholder { color: var(--gw-muted); opacity: 1; }
|
|
50
|
+
/* a list: the caret is drawn with the muted ink (two gradients, so it follows dark mode), with room for it */
|
|
51
|
+
.gw-select {
|
|
52
|
+
padding-right: 40px;
|
|
53
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--gw-muted) 50%), linear-gradient(135deg, var(--gw-muted) 50%, transparent 50%);
|
|
54
|
+
background-position: calc(100% - 20px) 50%, calc(100% - 15px) 50%;
|
|
55
|
+
background-size: 5px 5px;
|
|
56
|
+
background-repeat: no-repeat;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
.gw-textarea { height: auto; min-height: 96px; padding-block: 8px; resize: vertical; }
|
|
60
|
+
/* the states, in this order: hover, focus, read-only, invalid, disabled. Focus is the kit's accent ring. */
|
|
61
|
+
:is(.gw-input, .gw-select, .gw-textarea):hover { border-color: var(--gw-muted); }
|
|
62
|
+
:is(.gw-input, .gw-select, .gw-textarea):focus-visible { border-color: var(--gw-ink); outline: 2px solid var(--gw-accent); outline-offset: 1px; }
|
|
63
|
+
/* read-only: still focusable and readable, visibly not editable (the dashboard's) */
|
|
64
|
+
:is(.gw-input, .gw-textarea)[readonly] { border-color: var(--gw-line-strong); background-color: var(--gw-bg-soft); cursor: default; }
|
|
65
|
+
:is(.gw-input, .gw-select, .gw-textarea)[aria-invalid="true"] { border-color: var(--gw-danger); background-color: var(--gw-danger-soft); }
|
|
66
|
+
:is(.gw-input, .gw-select, .gw-textarea):disabled { opacity: 0.48; cursor: not-allowed; }
|
|
67
|
+
|
|
68
|
+
/* a group of choices: a <fieldset> whose <legend> is the question, one choice per row */
|
|
69
|
+
.gw-choices { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
70
|
+
.gw-choices > legend { margin-bottom: 4px; padding: 0; }
|
|
71
|
+
.gw-choices > :is(.gw-check, .gw-radio) { display: flex; }
|
|
72
|
+
.gw-choices > :is(.gw-field__hint, .gw-field__error) { margin-top: 4px; }
|
|
73
|
+
|
|
74
|
+
/* a checkbox or a radio: the <label> is the target (32px, 44px on touch), the box sits on the first line of words */
|
|
75
|
+
.gw-check,
|
|
76
|
+
.gw-radio {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: flex-start;
|
|
79
|
+
gap: 8px;
|
|
80
|
+
max-width: 100%;
|
|
81
|
+
padding-block: 4px;
|
|
82
|
+
color: var(--gw-ink);
|
|
83
|
+
font-size: 16px;
|
|
84
|
+
line-height: 1.5;
|
|
85
|
+
overflow-wrap: break-word;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
}
|
|
88
|
+
:is(.gw-check, .gw-radio) > :not(input) { min-width: 0; }
|
|
89
|
+
:is(.gw-check, .gw-radio) > input {
|
|
90
|
+
display: inline-grid;
|
|
91
|
+
flex: none;
|
|
92
|
+
place-content: center;
|
|
93
|
+
width: 18px;
|
|
94
|
+
height: 18px;
|
|
95
|
+
margin: 3px 0 0;
|
|
96
|
+
border: 1px solid var(--gw-line-control);
|
|
97
|
+
border-radius: var(--gw-radius-4);
|
|
98
|
+
background: var(--gw-surface);
|
|
99
|
+
-webkit-appearance: none;
|
|
100
|
+
appearance: none;
|
|
101
|
+
cursor: inherit;
|
|
102
|
+
}
|
|
103
|
+
.gw-radio > input { border-radius: var(--gw-radius-chip); }
|
|
104
|
+
:is(.gw-check, .gw-radio):hover > input { border-color: var(--gw-muted); }
|
|
105
|
+
/* chosen: the accent, as every check mark of the kit; the mark in the words-on-accent colour */
|
|
106
|
+
.gw-check > input:checked { border-color: var(--gw-accent); background: var(--gw-accent); }
|
|
107
|
+
.gw-check > input:checked::before {
|
|
108
|
+
content: "";
|
|
109
|
+
width: 14px;
|
|
110
|
+
height: 14px;
|
|
111
|
+
background: var(--gw-on-accent);
|
|
112
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
|
|
113
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
|
|
114
|
+
}
|
|
115
|
+
.gw-radio > input:checked { border: 5px solid var(--gw-accent); }
|
|
116
|
+
:is(.gw-check, .gw-radio) > input[aria-invalid="true"]:not(:checked) { border-color: var(--gw-danger); }
|
|
117
|
+
:is(.gw-check, .gw-radio):has(> input:disabled) { opacity: 0.48; cursor: not-allowed; }
|
|
118
|
+
/* a filter's count: a small pill at the end of the row, the neutral badge's colours */
|
|
119
|
+
.gw-check__count {
|
|
120
|
+
flex: none;
|
|
121
|
+
min-width: 24px;
|
|
122
|
+
margin: 1px 0 0 auto;
|
|
123
|
+
padding: 2px 8px;
|
|
124
|
+
border-radius: var(--gw-radius-chip);
|
|
125
|
+
background: color-mix(in srgb, var(--gw-ink) 8%, transparent);
|
|
126
|
+
color: var(--gw-ink-2);
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
text-align: center;
|
|
130
|
+
font-variant-numeric: tabular-nums;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* the inline row: one field and its button (a newsletter, "get the guide"). One line from 480px, the field taking the
|
|
134
|
+
room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
|
|
135
|
+
.gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
136
|
+
.gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
|
|
137
|
+
.gw-form-inline > .gw-btn { flex: 1 0 auto; }
|
|
138
|
+
@media (max-width: 479px) {
|
|
139
|
+
.gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* the form stack: fields 24px apart, the actions 32px below the last one, 560px at most (a field's width hints at its
|
|
143
|
+
answer: no field runs across a wide screen) */
|
|
144
|
+
.gw-form { display: grid; gap: 24px; max-width: 560px; }
|
|
145
|
+
/* the gap is the rhythm, also when the form is the card itself (a card spaces its own children otherwise) */
|
|
146
|
+
.gw-form > * { margin: 0; }
|
|
147
|
+
.gw-form > :is(.gw-actions, .gw-btn) { margin-top: 8px; }
|
|
148
|
+
.gw-form > .gw-btn { justify-self: start; }
|
|
149
|
+
|
|
150
|
+
/* touch: every control is at least 44px tall, a choice's row too */
|
|
151
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
152
|
+
.gw-input, .gw-select { height: var(--gw-tap-min); }
|
|
153
|
+
.gw-check, .gw-radio { padding-block: 10px; }
|
|
154
|
+
}
|
package/css/prose.css
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
/* the flow: each block 20px below the one before it (16px in --sm); more air around headings and large blocks */
|
|
17
17
|
:where(.gw-prose) > * + * { margin-top: 20px; }
|
|
18
18
|
:where(.gw-prose--sm) > * + * { margin-top: 16px; }
|
|
19
|
-
:where(.gw-prose) > * + :where(figure, img, video, pre, table, hr, .gw-table, .gw-window),
|
|
20
|
-
:where(.gw-prose) > :where(figure, img, video, pre, table, hr, .gw-table, .gw-window) + * { margin-top: 32px; }
|
|
19
|
+
:where(.gw-prose) > * + :where(figure, img, video, pre, table, hr, .gw-table, .gw-window, .gw-prose__table, .gw-callout),
|
|
20
|
+
:where(.gw-prose) > :where(figure, img, video, pre, table, hr, .gw-table, .gw-window, .gw-prose__table, .gw-callout) + * { margin-top: 32px; }
|
|
21
21
|
:where(.gw-prose) > * + h2 { margin-top: 48px; }
|
|
22
22
|
:where(.gw-prose) > * + h3 { margin-top: 40px; }
|
|
23
23
|
:where(.gw-prose) > * + h4 { margin-top: 32px; }
|
|
@@ -95,26 +95,39 @@
|
|
|
95
95
|
:where(.gw-prose) > :is(p, figure) > :is(img, video) { border-radius: var(--gw-radius-card); }
|
|
96
96
|
:where(.gw-prose) figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
97
97
|
|
|
98
|
-
/* a table straight from
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
/* a table straight from the CMS or markdown (gap #63, 0.5.0). website.js puts each bare table in a scroll box of its
|
|
99
|
+
own, .gw-prose__table: a region named after the table's caption or first header cell, with tabindex="0" so a
|
|
100
|
+
keyboard can scroll it. The table is marked .is-wrapped and keeps display: table, because some Safari versions stop
|
|
101
|
+
announcing a table as a table once its display changes. Without the script, or before it runs, the table itself
|
|
102
|
+
scrolls inside its box, as in 0.2.0. The box looks the same either way. When you write the markup, the .gw-table
|
|
103
|
+
wrapper (a region with a name and tabindex="0") is still the better choice. */
|
|
104
|
+
:where(.gw-prose) > table:where(:not(.is-wrapped)),
|
|
105
|
+
:where(.gw-prose) > :where(.gw-prose__table) {
|
|
102
106
|
width: max-content;
|
|
103
107
|
max-width: 100%;
|
|
104
108
|
overflow-x: auto;
|
|
105
109
|
border: 1px solid var(--gw-line);
|
|
106
110
|
border-radius: var(--gw-radius-card);
|
|
107
111
|
background: var(--gw-surface);
|
|
108
|
-
border-collapse: collapse;
|
|
109
|
-
font-size: 14px;
|
|
110
|
-
line-height: 1.5;
|
|
111
112
|
-webkit-overflow-scrolling: touch;
|
|
112
113
|
}
|
|
113
|
-
:where(.gw-prose) > table
|
|
114
|
-
:where(.gw-prose) > table
|
|
115
|
-
:where(.gw-prose) >
|
|
116
|
-
:where(.gw-prose) > table
|
|
117
|
-
:where(.gw-prose) >
|
|
118
|
-
:where(.gw-prose) > table
|
|
114
|
+
:where(.gw-prose) > table:where(:not(.is-wrapped)) { display: block; }
|
|
115
|
+
:where(.gw-prose) > table,
|
|
116
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table { border-collapse: collapse; font-size: 14px; line-height: 1.5; }
|
|
117
|
+
:where(.gw-prose) > table :is(th, td),
|
|
118
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table :is(th, td) { padding: 12px 16px; border-bottom: 1px solid var(--gw-line); text-align: left; vertical-align: top; }
|
|
119
|
+
:where(.gw-prose) > table thead th,
|
|
120
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table thead th { background: var(--gw-bg-soft); color: var(--gw-ink); font-size: 13px; font-weight: 600; white-space: nowrap; }
|
|
121
|
+
:where(.gw-prose) > table tbody th,
|
|
122
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table tbody th { font-weight: 500; }
|
|
123
|
+
:where(.gw-prose) > table td,
|
|
124
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table td { color: var(--gw-ink-2); font-variant-numeric: tabular-nums; }
|
|
125
|
+
:where(.gw-prose) > table tbody tr:last-child > *,
|
|
126
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table tbody tr:last-child > * { border-bottom: 0; }
|
|
127
|
+
:where(.gw-prose) > table caption,
|
|
128
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table caption { padding: 12px 16px; color: var(--gw-muted); font-size: 13px; text-align: left; }
|
|
119
129
|
/* on a phone a cell keeps room for a few words a line: the table scrolls further instead of squeezing its text */
|
|
120
|
-
@media (max-width: 767px) {
|
|
130
|
+
@media (max-width: 767px) {
|
|
131
|
+
:where(.gw-prose) > table td,
|
|
132
|
+
:where(.gw-prose) > :where(.gw-prose__table) > table td { min-width: 160px; }
|
|
133
|
+
}
|
package/css/ratings.css
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* ratings (0.6.0, gap #56): stars, the testimonial wall and the rating badge. Every rating and every testimonial
|
|
2
|
+
links to the page it comes from, and says only what that page says (AGENTS.md section 6); `check` warns about one
|
|
3
|
+
without its link (rating-source). */
|
|
4
|
+
|
|
5
|
+
/* stars: a value from 0 to 5 (style="--gw-stars: 4.9") drawn as five 16px stars, the last one cut where the value
|
|
6
|
+
ends. A screen reader hears the value in words: role="img" and aria-label="4.9 out of 5" (the drawing itself is a
|
|
7
|
+
background, nothing to read). Ink on the strong hairline: a rating is proof, not a state or an action. */
|
|
8
|
+
.gw-stars {
|
|
9
|
+
display: inline-block;
|
|
10
|
+
flex: none;
|
|
11
|
+
width: 80px;
|
|
12
|
+
height: 16px;
|
|
13
|
+
vertical-align: middle;
|
|
14
|
+
background: linear-gradient(90deg, var(--gw-ink) calc(clamp(0, var(--gw-stars, 0), 5) * 20%), var(--gw-line-strong) 0);
|
|
15
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
|
|
16
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
|
|
17
|
+
}
|
|
18
|
+
/* a forced-colours screen drops backgrounds: draw the stars in its own text colours */
|
|
19
|
+
@media (forced-colors: active) {
|
|
20
|
+
.gw-stars { forced-color-adjust: none; background: linear-gradient(90deg, CanvasText calc(clamp(0, var(--gw-stars, 0), 5) * 20%), GrayText 0); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* the wall: short quotes in cards, one column on phones, two from 768px, three from 1080px. No masonry: the cards of
|
|
24
|
+
a row share its height, and each card's byline sits at its foot. */
|
|
25
|
+
.gw-testimonials {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: minmax(0, 1fr);
|
|
28
|
+
gap: 16px;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
list-style: none;
|
|
32
|
+
}
|
|
33
|
+
.gw-testimonials > li { display: flex; min-width: 0; }
|
|
34
|
+
@media (min-width: 768px) { .gw-testimonials { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
|
|
35
|
+
@media (min-width: 1080px) { .gw-testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
|
36
|
+
/* a quote card (with .gw-card): stars, the reviewer's own words, who wrote them and where */
|
|
37
|
+
.gw-testimonial { display: flex; flex-direction: column; gap: 16px; width: 100%; margin: 0; }
|
|
38
|
+
.gw-testimonial > * { margin: 0; }
|
|
39
|
+
.gw-testimonial__quote,
|
|
40
|
+
.gw-testimonial__quote p { color: var(--gw-ink); font-size: 16px; line-height: 1.6; }
|
|
41
|
+
.gw-testimonial__quote > * + * { margin-top: 8px; }
|
|
42
|
+
.gw-testimonial > .gw-testimonial__by { display: flex; align-items: center; gap: 12px; margin-top: auto; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
|
|
43
|
+
.gw-testimonial__by b { display: block; color: var(--gw-ink); font-weight: 600; }
|
|
44
|
+
.gw-testimonial__by > :not(img) { min-width: 0; }
|
|
45
|
+
.gw-testimonial__avatar { flex: none; width: 40px; height: 40px; border-radius: var(--gw-radius-chip); background: var(--gw-bg-soft); object-fit: cover; }
|
|
46
|
+
|
|
47
|
+
/* the rating badge: one link to the store's review page, with the store's mark, the score, the stars and the count.
|
|
48
|
+
A small card that sits; the border darkens on hover, as a linked card's. */
|
|
49
|
+
.gw-rating-badge {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
max-width: 100%;
|
|
54
|
+
padding: 8px 16px 8px 8px;
|
|
55
|
+
border: 1px solid var(--gw-line);
|
|
56
|
+
border-radius: var(--gw-radius-card);
|
|
57
|
+
background: var(--gw-surface);
|
|
58
|
+
color: var(--gw-ink);
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
line-height: 1.4;
|
|
61
|
+
vertical-align: middle;
|
|
62
|
+
}
|
|
63
|
+
a.gw-rating-badge:hover { border-color: var(--gw-line-strong); }
|
|
64
|
+
.gw-rating-badge__icon {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
flex: none;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
width: 40px;
|
|
70
|
+
height: 40px;
|
|
71
|
+
border-radius: var(--gw-radius-action);
|
|
72
|
+
background: var(--gw-bg-soft);
|
|
73
|
+
color: var(--gw-ink);
|
|
74
|
+
}
|
|
75
|
+
.gw-rating-badge__icon :is(svg, img) { width: 24px; height: 24px; }
|
|
76
|
+
.gw-rating-badge__body { display: grid; gap: 2px; min-width: 0; }
|
|
77
|
+
.gw-rating-badge__score { display: flex; align-items: center; gap: 8px; color: var(--gw-ink); font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
78
|
+
.gw-rating-badge__count { color: var(--gw-muted); }
|