@gemboss/website-ui 0.8.0 → 0.10.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 +8 -3
- package/CHANGELOG.md +110 -0
- package/README.md +5 -5
- package/bin/lib/check.mjs +19 -0
- package/css/effects.css +71 -0
- package/css/footer.css +74 -58
- package/css/header.css +12 -0
- package/css/tokens.css +9 -2
- package/demo/footer.html +1 -1
- package/demo/index.html +1 -0
- package/demo/spotlight.html +291 -0
- package/docs/components.md +62 -10
- package/docs/markup-contract.md +45 -7
- package/docs/theming.md +6 -0
- package/js/website.js +54 -4
- package/js/website.mjs +70 -7
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
- package/system.json +16 -4
- package/templates/footer.html +11 -9
- package/website.css +168 -61
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
10
10
|
| file | what it is |
|
|
11
11
|
|---|---|
|
|
12
12
|
| `website.css` | the whole kit, one file (tokens, base, layout, components, sections) |
|
|
13
|
-
| `js/website.js` | header, mega menu, mobile menu (and its drill-down), language switcher, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box, the footer's folds; no dependencies; `window.gwWebsite.init(root)` |
|
|
13
|
+
| `js/website.js` | header, mega menu, mobile menu (and its drill-down), language switcher, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box, the footer's folds, the spotlight; no dependencies; `window.gwWebsite.init(root)` |
|
|
14
14
|
| `js/website.mjs` | the same script as an ES module for apps: `import { init, destroy } from '@gemboss/website-ui/website.mjs'` |
|
|
15
15
|
| `system.json` | every value of the system: colours, spacing, type, radii, shadows, widths |
|
|
16
16
|
| `templates/page.html` | the page skeleton: start every new page from it |
|
|
@@ -23,6 +23,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
23
23
|
| `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
|
|
24
24
|
| `demo/footer.html` | the full footer (Ask AI, columns that fold on phones, family rows, theme toggle, wordmark), and the same for a GemPages-style brand |
|
|
25
25
|
| `demo/header.html` | the header of gemboss.ai: the mobile menu that drills down with its bar of actions, and the language switcher, and the same for a GemPages-style brand |
|
|
26
|
+
| `demo/spotlight.html` | the spotlight on plan cards, link cards, the feature card and a night band's button, and the same for a GemPages-style brand |
|
|
26
27
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
27
28
|
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
28
29
|
|
|
@@ -122,6 +123,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
122
123
|
else on white or `--gw-bg-soft`.
|
|
123
124
|
- **Motion**: only `transform` and `opacity`, the kit's ease, nothing moves under `prefers-reduced-motion`. Scroll
|
|
124
125
|
reveal is `.gw-reveal` (the JS adds `.is-in`, and a failsafe shows it anyway).
|
|
126
|
+
- **Spotlight** (0.9.0): `data-gw-spot` only on a link, a button or a card with exactly one action (a plan, a link
|
|
127
|
+
card, the mega menu's feature card, a night band's primary button). The kit draws the light in the accent; never on a
|
|
128
|
+
heading or text, never in another colour, never your own script.
|
|
125
129
|
|
|
126
130
|
## 4. Don't (each of these has happened, and each one made a page look cheap or broke it)
|
|
127
131
|
|
|
@@ -272,8 +276,9 @@ with numbers and no caption, an unknown brand profile or one set off `<html>`, `
|
|
|
272
276
|
words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
|
|
273
277
|
the page, a callout without its label, stars without their value in words, a rating or a testimonial without a link to
|
|
274
278
|
its source, a footer fold without its heading in the summary or a wordmark a screen reader reads, a drill-down level
|
|
275
|
-
without its Back or its title, a language switcher shown by the markup or a translation without `hreflang`,
|
|
276
|
-
|
|
279
|
+
without its Back or its title, a language switcher shown by the markup or a translation without `hreflang`, a
|
|
280
|
+
spotlight on a heading or on something that is not one action, and patterns that repeat across pages (likely gaps). It
|
|
281
|
+
does not replace looking at the page at 320px and 1366px.
|
|
277
282
|
|
|
278
283
|
## 11. Upgrading the kit
|
|
279
284
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,116 @@ Every release lists what changed for a site that uses the kit. A release that ad
|
|
|
4
4
|
says so with `Closes gap #N` (the GitHub issue number), so `npx gemboss-website upgrade` can tell that site which
|
|
5
5
|
stopgap (`kitgap-*`) it can now delete.
|
|
6
6
|
|
|
7
|
+
## 0.10.0 (2026-10-26)
|
|
8
|
+
|
|
9
|
+
The footer, 1:1 with gemboss.ai. 0.7.0 drew the footer "as gemboss.ai has it", but not to the pixel: when gemboss.ai
|
|
10
|
+
moved onto the kit (site PR #76) its footer changed in about a dozen places (the gaps, the bottom row, the logo, the
|
|
11
|
+
wordmark, the widths where the columns fold). Chris asked for it identical, with nothing missed, and for the values to
|
|
12
|
+
live in the kit, so every site that uses it inherits them (26/10/2026). 0.10.0 takes every value from the site's own
|
|
13
|
+
footer before #76 and was measured against it in Chromium: part by part (position, size, type, colour of every word)
|
|
14
|
+
and pixel by pixel, at 14 widths from 320 to 1920px, light and dark, folded and open, every part hovered and focused
|
|
15
|
+
from the keyboard, on every page of the site. All of it is pixel-identical but one thing, on purpose: at 320px
|
|
16
|
+
gemboss.ai squeezed the chevron of its longest row to 7.3px, and the kit keeps it 8px.
|
|
17
|
+
|
|
18
|
+
A minor version: a footer that already uses the kit changes to these values. Nothing to change in its markup.
|
|
19
|
+
|
|
20
|
+
- Widths: the columns fold up to 620px and stand open from 621px (were 767 and 768), in the CSS and in `website.js`
|
|
21
|
+
(`matchMedia('(min-width: 621px)')`); the footer is one row from 1081px (was 1080px); two columns between (was three
|
|
22
|
+
from 768px). The wordmark shows from 621px (was 480px), its gap 16px up to 640px.
|
|
23
|
+
- The footer: 64px above and 32px below (was 40px), no ground of its own (`background: transparent`, was `--gw-bg`), 32px
|
|
24
|
+
between columns (24px x 20px up to 620px).
|
|
25
|
+
- The brand block: the mark 34px with 8px corners (`--gw-radius-8`), the row 34px tall, as wide as the block (44px on
|
|
26
|
+
touch); its sentence 30 characters a line at most, 22px lines (was 320px wide). The block keeps its width beside the
|
|
27
|
+
Ask AI row; the 0.7.0 rules that widened it are gone.
|
|
28
|
+
- Headings: 12px over their links (was 8px), 1.6 line height, balanced, whatever the page's `h2` rule says (a site's
|
|
29
|
+
`h2` gave them its display face, and balance on some pages, not on others).
|
|
30
|
+
- The face: the brand's name, the headings and the wordmark are set in `--gw-font` whatever the page's body says (a
|
|
31
|
+
page in Chinese puts PingFang first; gemboss.ai keeps Inter for these three). A brand sets its face with the token.
|
|
32
|
+
- Links in a column: 44px rows up to 768px and on touch (gemboss.ai's), 20px lines.
|
|
33
|
+
- The bottom row: 32px under the columns (was 48px), 12px x 24px between its parts, 16px between its links (8px x 16px on
|
|
34
|
+
touch); no 24px minimum size on a desktop link; a social link at least 44px wide on touch.
|
|
35
|
+
- Folds up to 620px: each column's hairline on top, the last column's and each family row's below; a 52px row without
|
|
36
|
+
padding (was 44px padded to 52px); the chevron is two 2px borders, 8px, turned 45deg and 225deg open, at once (it
|
|
37
|
+
eased 0.18s); a focused row draws 2px of accent 2px out with 6px corners.
|
|
38
|
+
- Family rows: four items a line from 621px, 44px items from 621 to 768px, the list pulled 12px left only from 1081px;
|
|
39
|
+
each item fills its cell.
|
|
40
|
+
- `.gw-footer__ext` takes the glyph: `<span class="gw-footer__ext" aria-hidden="true">↗</span>`, set muted at weight
|
|
41
|
+
400, as gemboss.ai writes it. An empty one is still drawn (the 12px mask).
|
|
42
|
+
- The Ask AI label has 22px lines; a hovered Ask AI link takes the browser's own underline (was 1px, 4px under).
|
|
43
|
+
- The theme toggle: on hover its hairline turns ink and its icon the accent, its ground stays (colour only, 0.15s; was
|
|
44
|
+
the soft ground). Two icons: `.gw-theme-toggle__moon` shows in light, `.gw-theme-toggle__sun` in dark (a new section
|
|
45
|
+
at the end of `header.css`, for any `.gw-theme-toggle`); a toggle with one plain `<svg>` stays as it was.
|
|
46
|
+
- The wordmark: weight 800 (added to `type.weights`, its one use), 48px above and nothing under but the footer's 32px
|
|
47
|
+
(was 24px), the mark 7vw wide (40 to 96px; was 0.9em tall). Its `<svg>` may carry `width` and `height` for its shape:
|
|
48
|
+
gemboss.ai's gem says `width="86" height="100"`.
|
|
49
|
+
- Token: light `--gw-muted` is `#6a6d73` (was `#686b71`), gemboss.ai's: 5.18:1 on white, 4.67:1 on the soft ground
|
|
50
|
+
(were 5.34 and 4.81), both above 4.5:1.
|
|
51
|
+
- `templates/footer.html` follows: the ↗ glyph, the two toggle icons, a 34px mark, the new widths in its notes.
|
|
52
|
+
- Docs: `components.md` (Footer) and `markup-contract.md` (the folds' widths, the ↗, the toggle's icons, the
|
|
53
|
+
wordmark's mark).
|
|
54
|
+
- Tests: `test/run.mjs` holds the base rules byte for byte again from this version (their hash, renewed once on purpose)
|
|
55
|
+
and pins the values above; the fold tests read 620/621px.
|
|
56
|
+
|
|
57
|
+
## 0.9.0 (2026-10-25)
|
|
58
|
+
|
|
59
|
+
The spotlight, a named hover effect for the things you click (Chris, 25/10/2026, chosen over a heading that glows
|
|
60
|
+
under the pointer): a soft light in the brand's accent follows the pointer over a plan card, a link card, the mega menu's
|
|
61
|
+
feature card or the primary button of a night band. Opt-in per element: a page without `data-gw-spot` looks exactly
|
|
62
|
+
as before. The demo index (without its new link), `brand-profiles.html`, `article.html`, `blog.html`, `forms.html`,
|
|
63
|
+
`footer.html`, `header.html` and `theme-example.html` are pixel-identical to 0.8.0 at 390 and 1366px, light and dark.
|
|
64
|
+
|
|
65
|
+
Closes #74
|
|
66
|
+
|
|
67
|
+
- `data-gw-spot` on an `<a href>`, a `<button>`, or a card (`<div>`, `<article>`, `<li>`) that holds exactly one of
|
|
68
|
+
them: a `.gw-plan` with its one `.gw-btn`, a `.gw-card--link`, the `.gw-mega__feature`, the `.gw-btn` of a
|
|
69
|
+
`.gw-band--night`. `data-gw-spot="<selector>"` lights the first element the selector names inside it instead (the art
|
|
70
|
+
of a card). Never on a heading or running text: the script refuses a heading, and the kit still forbids gradient or
|
|
71
|
+
accent-coloured words in a heading, and lights of more than one colour.
|
|
72
|
+
- The light: a radial gradient on the `::before` of the element that carries it, under its words, in its own corners
|
|
73
|
+
(`border-radius: inherit`, so both radius profiles hold), no layout change. One colour, `--gw-accent` mixed with
|
|
74
|
+
`color-mix()`: 22% on a card, 38% on a button, 40% on the Stage's night ground; the `-dark` family in dark mode and in
|
|
75
|
+
night bands, the brand's own accent everywhere (a GemPages-style page lights blue). Its radius, a registered
|
|
76
|
+
`@property --gw-spot-r` that inherits, grows to 260px (120px on a button) in 0.55s with `cubic-bezier(.16,1,.3,1)`:
|
|
77
|
+
the one animated property that is neither transform nor opacity (`system.json` `motion` says so).
|
|
78
|
+
- The motion, as Chris asked after the prototype: the light starts where the pointer comes in, then trails it, going
|
|
79
|
+
18% of the way that is left on each frame (one `requestAnimationFrame` loop per element, stopped within 0.3px), so a
|
|
80
|
+
jump of the pointer is followed over several frames, never snapped.
|
|
81
|
+
- Pointer devices only: the CSS is all inside `(hover: hover) and (pointer: fine)`, and `website.js` adds no listener
|
|
82
|
+
without a fine pointer (again on every change). Keyboard focus shows the same light, centred and as large, on the
|
|
83
|
+
focused host or on a card whose one action has the focus (`:has(:focus-visible)`, in a rule of its own). Reduced
|
|
84
|
+
motion: no listener, a still light in the middle that appears without a transition.
|
|
85
|
+
- Fallback: without `@property` or `color-mix()` the script wires nothing, so nothing is drawn, not half a light.
|
|
86
|
+
- Tokens (`system.json` `motion.spot`): `--gw-spot-radius`, `--gw-spot-radius-action`, `--gw-spot-strength`,
|
|
87
|
+
`--gw-spot-strength-action`, `--gw-spot-strength-stage`, `--gw-spot-ms`, `--gw-spot-ease`; the trail and the settle
|
|
88
|
+
distance are the script's, held equal to `motion.spot.trail` and `.settle` by the tests.
|
|
89
|
+
- CSS: `css/effects.css`, last in `website.css`. Every rule of it selects a wired spot (`data-gw-ready~="spot"`) or the
|
|
90
|
+
element carrying its light (`.gw-spot`, set by the script), so no page without the attribute can change.
|
|
91
|
+
- `website.js`: the `spot` part in the `init(root)`/`destroy(root)` registry (`data-gw-ready="spot"`), a
|
|
92
|
+
`pointerenter` and a `pointermove` listener per element, `--gw-spot-x` and `--gw-spot-y` on the element that carries
|
|
93
|
+
the light, nothing else and no event. `destroy` takes the listeners, the frame, the class and the place back.
|
|
94
|
+
- `check`: `spot-target` (an error): `data-gw-spot` on a heading, with a selector that names a heading, on running text
|
|
95
|
+
or a section, on an `<a>` without `href`, or on a card with no link or button or with more than one.
|
|
96
|
+
- Words under the light: while a thing is lit (hover, keyboard focus), its muted words read `--gw-ink-2`: the host sets
|
|
97
|
+
`--gw-muted: var(--gw-ink-2)`, so every part that uses the muted token follows, with no class of its own. Muted would
|
|
98
|
+
dip to 4.0:1 at the light's middle on a white card (5.3:1 at rest); ink-2 there is 8.2:1 measured in Chromium (7.6:1
|
|
99
|
+
in dark), and 4.5:1 or more on every ground by the tokens (a card, a soft card, a night band). The words turn ink-2 at
|
|
100
|
+
once and turn back only once the light has gone (a colour that waits 0.55s, no movement; at once under reduced
|
|
101
|
+
motion). Ink keeps 10:1 or more and a button's words 4.5:1 in every profile, light and dark.
|
|
102
|
+
- Demo: `demo/spotlight.html`, plan cards, link cards, the feature card (also in the Product menu) and the night band's
|
|
103
|
+
CTA, with a switch to a GemPages-style brand (`data-gw-action="accent"`, `data-gw-radius="soft"`, `#3c67ff`), linked
|
|
104
|
+
from `demo/index.html`. No other demo page opts in.
|
|
105
|
+
- Budget: `js/website.js` is 23.1 KB unminified, 7.1 KB gzipped (0.8.0: 20.8 KB, 6.4 KB); the test's budget is now
|
|
106
|
+
24 KB and 8 KB. `website.min.css` is 73.8 KB, 12.7 KB gzipped (0.8.0: 71.8 KB, 12.3 KB).
|
|
107
|
+
- Tests: `test/run.mjs` holds the CSS (every rule opt-in, fine pointer only, the accent only, the reduced-motion and
|
|
108
|
+
focus rules, the tokens), the contrast under the light, the script in its fake DOM (entry point, trailing frame by
|
|
109
|
+
frame, settling, touch, inner element, init twice, destroy, media changes, no `@property`, no `color-mix()`) and the
|
|
110
|
+
check rule; `test/browser.mjs` drives `demo/spotlight.html` in Chromium (the radius read on the `::before` while
|
|
111
|
+
hovering, the light following a jump over several frames, no layout shift, the Stage and the night band, Tab to a
|
|
112
|
+
card and to a plan's button, reduced motion, a touch screen with no listener counted by DevTools and the same
|
|
113
|
+
pixels after a tap, no `@property` or `color-mix()`, init twice and destroy counted by DevTools, a heading refused
|
|
114
|
+
by the script and flagged by `check`, the GemPages-style accent and corners, light and dark, and a muted line at the
|
|
115
|
+
light's middle measured on the drawn pixel: 4.5:1 or more, light and dark, ink-2 while the light fades, muted after).
|
|
116
|
+
|
|
7
117
|
## 0.8.0 (2026-10-25)
|
|
8
118
|
|
|
9
119
|
The header of gemboss.ai on a phone, so the site's header can move onto the kit: a mobile menu that drills down into
|
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@ it. Made so that a team working with Claude Code, even without a designer, ships
|
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng
|
|
16
|
-
| `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại (kể cả bản đi sâu từng mục: Back, giữ focus trong menu, sự kiện `gw:menu`), nút đổi ngôn ngữ (đọc `<link rel="alternate" hreflang>` của trang, ẩn khi trang chỉ có một ngôn ngữ), đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ, đánh dấu mục đang đọc trong mục lục bài viết, khung cuộn cho bảng trong bài, cột footer gập/mở theo khổ màn
|
|
15
|
+
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 107 KB, 74 KB bản min, 12,7 KB khi nén gzip): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn, đang chờ và nút xoá), link, nhãn, chip, segmented control, badge, card (kèm ảnh trên đầu thẻ), khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), bộ khung bài blog (breadcrumb, dòng tác giả và ngày, mục lục dính, hộp lưu ý, danh sách bài, phân trang), ô nhập và trạng thái lỗi (ô chữ, danh sách chọn, ô nhiều dòng, checkbox, radio, hàng một ô một nút cho newsletter), sao đánh giá, tường review, huy hiệu điểm App Store, header + mega menu + menu điện thoại (tuỳ chọn đi sâu từng mục kèm thanh nút dính đáy, như gemboss.ai) + nút đổi ngôn ngữ, footer như gemboss.ai (cột gập lại trên điện thoại, hàng Ask AI, hàng sản phẩm cùng nhà, nút sáng/tối, wordmark; ô newsletter tuỳ chọn), đốm sáng theo con trỏ trên thứ bấm được (`data-gw-spot`, bật từng chỗ), 16 section, 3 hồ sơ thương hiệu |
|
|
16
|
+
| `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại (kể cả bản đi sâu từng mục: Back, giữ focus trong menu, sự kiện `gw:menu`), nút đổi ngôn ngữ (đọc `<link rel="alternate" hreflang>` của trang, ẩn khi trang chỉ có một ngôn ngữ), đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ, đánh dấu mục đang đọc trong mục lục bài viết, khung cuộn cho bảng trong bài, cột footer gập/mở theo khổ màn, đốm sáng theo con trỏ; không phụ thuộc thư viện nào, khoảng 7,1 KB khi nén gzip. `website.js` là script thường (tự chạy khi trang tải xong), `website.mjs` là bản ES module cho app; cả hai có `init(root)` để gắn lại phần markup chèn sau (`docs/markup-contract.md`, mục "Script") |
|
|
17
17
|
| `AGENTS.md` | luật mà Claude Code (và người) phải theo: 10 luật, cách dựng trang, điều cấm, chữ, khả năng truy cập, đổi thương hiệu, khi kit thiếu |
|
|
18
18
|
| `templates/` | khung một trang, header (`header.html`: hai tuỳ chọn, menu điện thoại đi sâu và nút ngôn ngữ), footer (`footer.html`: footer của gemboss.ai, mọi phần tuỳ chọn đều ghi rõ) và 16 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose, article, post-list, form, testimonials) |
|
|
19
|
-
| `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài; `forms.html` là mọi trạng thái của ô nhập, newsletter ở footer, tường review và huy hiệu điểm (dữ liệu mẫu); `footer.html` là footer đầy đủ như gemboss.ai, kèm nút chuyển sang bản kiểu GemPages; `header.html` là header của gemboss.ai trên điện thoại (menu đi sâu từng mục, thanh nút dính đáy, nút đổi ngôn ngữ với ba ngôn ngữ), cũng có bản kiểu GemPages |
|
|
19
|
+
| `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài; `forms.html` là mọi trạng thái của ô nhập, newsletter ở footer, tường review và huy hiệu điểm (dữ liệu mẫu); `footer.html` là footer đầy đủ như gemboss.ai, kèm nút chuyển sang bản kiểu GemPages; `header.html` là header của gemboss.ai trên điện thoại (menu đi sâu từng mục, thanh nút dính đáy, nút đổi ngôn ngữ với ba ngôn ngữ), cũng có bản kiểu GemPages; `spotlight.html` là đốm sáng trên thẻ gói giá, thẻ link, thẻ nổi bật của mega menu và nút trong dải tối, cũng có bản kiểu GemPages |
|
|
20
20
|
| `docs/` | danh mục class (`components.md`), đổi thương hiệu (`theming.md`), triết lý (`philosophy.md`), hợp đồng markup |
|
|
21
21
|
| `system.json` | mọi giá trị của hệ thống (màu, khoảng cách, chữ, bo góc, bóng, khung): nguồn duy nhất |
|
|
22
22
|
| CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
|
|
@@ -40,9 +40,9 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
|
|
|
40
40
|
**Site HTML tĩnh hoặc không có bước build:** dùng CDN của npm, ghim đúng phiên bản.
|
|
41
41
|
|
|
42
42
|
```html
|
|
43
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
43
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.10.0/website.min.css">
|
|
44
44
|
<link rel="stylesheet" href="/site.css"> <!-- CSS riêng của site, luôn SAU kit -->
|
|
45
|
-
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.10.0/js/website.js" defer></script>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Chèn markup của kit sau khi trang đã tải (block của builder, bản xem trước CMS): gọi `gwWebsite.init(phầnMới)` ngay
|
package/bin/lib/check.mjs
CHANGED
|
@@ -159,6 +159,11 @@ export const RULES = {
|
|
|
159
159
|
why: "website.js fills .gw-lang from the page's <link rel=\"alternate\" hreflang href> and shows it only when there is another language: it starts hidden, its button (.gw-lang__btn) names its list with aria-controls and says aria-expanded, and the list is empty in the markup. A switcher shown by the markup is an empty button on a page with one language; links typed into the list are replaced; a translation's <link rel=\"alternate\"> without hreflang is one the switcher (and search engines) cannot list.",
|
|
160
160
|
fix: "Copy the switcher from templates/header.html: <div class=\"gw-lang\" data-gw-lang hidden><button class=\"gw-lang__btn\" type=\"button\" aria-expanded=\"false\" aria-controls=\"gw-lang-menu\">…</button><ul class=\"gw-lang__menu\" id=\"gw-lang-menu\" hidden></ul></div>, and give each translation <link rel=\"alternate\" hreflang=\"vi\" href=\"…\"> in <head>.",
|
|
161
161
|
},
|
|
162
|
+
"spot-target": {
|
|
163
|
+
level: "error", title: "Spotlight on something that is not one action",
|
|
164
|
+
why: "data-gw-spot lights what you can click: the light says \"this goes somewhere\". On a heading or running text it promises a link that is not there (and a heading stays ink: no gradient, no accent words, no light); on a box with no action, or several, the light cannot say which one it means.",
|
|
165
|
+
fix: "Put data-gw-spot on the <a href> or the <button> itself, or on a card (a <div>, <article> or <li>) that holds exactly one of them, such as a .gw-plan with its one .gw-btn. Never on a heading, a paragraph or a section, and never with a selector that names a heading (docs/components.md, Spotlight).",
|
|
166
|
+
},
|
|
162
167
|
"uppercase-text": {
|
|
163
168
|
level: "warning", title: "Uppercase text",
|
|
164
169
|
why: "Headings, buttons, labels and menu items are sentence case.",
|
|
@@ -509,6 +514,9 @@ function elementEnd(text, start, tag) {
|
|
|
509
514
|
}
|
|
510
515
|
return -1;
|
|
511
516
|
}
|
|
517
|
+
// The spotlight (0.9.0): a card lit as a whole is one of these, holding exactly one link or button.
|
|
518
|
+
const SPOT_BOX = new Set(["div", "article", "li"]);
|
|
519
|
+
const HEADING_SEL = /(?:^|[\s>+~,(])h[1-6](?![\w-])/i;
|
|
512
520
|
const LOADING_CLASS = /^(?:is-)?(?:loading|busy)$|--(?:loading|busy)$/;
|
|
513
521
|
const SPINNER_CLASS = /\bclass\s*=\s*["'][^"']*(?:spinner|loader|loading)/i;
|
|
514
522
|
|
|
@@ -692,6 +700,17 @@ function analyzeHtml(file, text, out) {
|
|
|
692
700
|
if (missing.length) add("lang-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
693
701
|
}
|
|
694
702
|
if (tag === "link" && /(?:^|\s)alternate(?:\s|$)/i.test(a.rel || "") && !a.type) alts.push({ a, where: where() });
|
|
703
|
+
if ("data-gw-spot" in a) {
|
|
704
|
+
const sel = (a["data-gw-spot"] || "").trim();
|
|
705
|
+
const acts = (inner().match(/<a\b[^>]*\bhref\s*=|<button\b/gi) || []).length;
|
|
706
|
+
const why = /^h[1-6]$/.test(tag) ? `on a heading (<${tag}>)`
|
|
707
|
+
: HEADING_SEL.test(sel) ? `its selector names a heading (data-gw-spot="${sel}")`
|
|
708
|
+
: (tag === "a" && "href" in a) || tag === "button" ? ""
|
|
709
|
+
: tag === "a" ? "on an <a> without href"
|
|
710
|
+
: !SPOT_BOX.has(tag) ? `on a <${tag}>, which is not a link, a button or a card`
|
|
711
|
+
: acts !== 1 ? `on a <${tag}> that holds ${acts} links or buttons, not exactly one` : "";
|
|
712
|
+
if (why) add("spot-target", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${why}`));
|
|
713
|
+
}
|
|
695
714
|
if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
|
|
696
715
|
const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
|
|
697
716
|
if (!linked) add("rating-source", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${decode(visible.slice(m.index, elementEnd(scanText, m.index, tag))).replace(/\s+/g, " ").trim().slice(0, 60)}`));
|
package/css/effects.css
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* effects: the spotlight (0.9.0, Chris 25/10/2026, issue #74), a named hover effect for the things you click.
|
|
2
|
+
data-gw-spot on an <a href>, a <button>, or a card that holds exactly one of them (a plan card, the mega menu's
|
|
3
|
+
feature card, the primary button of a night band, a .gw-card--link): a soft light in the brand's accent follows the
|
|
4
|
+
pointer over it. Opt-in per element: a page without the attribute does not change.
|
|
5
|
+
website.js wires the host (data-gw-ready="spot") and puts .gw-spot on the element that carries the light: the host,
|
|
6
|
+
or the one its data-gw-spot="<selector>" names inside it. It writes the pointer's place there (--gw-spot-x,
|
|
7
|
+
--gw-spot-y, trailing it a little every frame); this file draws the light on its ::before, under its words, in its
|
|
8
|
+
own corners (border-radius: inherit, so the radius profile holds), and grows the light's radius on hover.
|
|
9
|
+
- Pointer devices only: nothing here applies without (hover: hover) and (pointer: fine).
|
|
10
|
+
- Keyboard focus (on the host, or on the one action of a card) shows the same light, centred.
|
|
11
|
+
- Reduced motion: the light stands still, centred, and appears without a transition.
|
|
12
|
+
- Words under the light: while a thing is lit, its muted words read --gw-ink-2 (the token, set as --gw-muted on the
|
|
13
|
+
host), so they keep 4.5:1 at the light's middle; they turn back once the light has gone. A colour, no movement.
|
|
14
|
+
- Without @property or color-mix(), website.js puts .gw-spot nowhere: no light at all, nothing half drawn.
|
|
15
|
+
- One colour, the accent family, as the page has it (the -dark family in dark mode and in night bands). Never on a
|
|
16
|
+
heading or running text: a heading stays ink, with no gradient and no accent words (check: spot-target).
|
|
17
|
+
The radius is the one animated property that is neither transform nor opacity: it repaints one layer of one
|
|
18
|
+
element. Registered, so it can transition; inherited, so the ::before (and an inner element) reads the host's. */
|
|
19
|
+
@property --gw-spot-r {
|
|
20
|
+
syntax: "<length>";
|
|
21
|
+
inherits: true;
|
|
22
|
+
initial-value: 0px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (hover: hover) and (pointer: fine) {
|
|
26
|
+
/* the host: no light at rest (a lit card does not light a lit button inside it), the light's size on hover and focus */
|
|
27
|
+
[data-gw-ready~="spot"] { --gw-spot-r: 0px; }
|
|
28
|
+
[data-gw-ready~="spot"]:is(:hover, :focus-visible) { --gw-spot-r: var(--gw-spot-radius); --gw-muted: var(--gw-ink-2); }
|
|
29
|
+
/* a card whose one action has the keyboard focus; a rule of its own, so a browser without :has() drops only it */
|
|
30
|
+
[data-gw-ready~="spot"]:has(:focus-visible) { --gw-spot-r: var(--gw-spot-radius); --gw-muted: var(--gw-ink-2); }
|
|
31
|
+
.gw-btn[data-gw-ready~="spot"]:is(:hover, :focus-visible) { --gw-spot-r: var(--gw-spot-radius-action); }
|
|
32
|
+
|
|
33
|
+
/* the element that carries the light: positioned for its layer, isolated so the layer sits under its words.
|
|
34
|
+
:where(), so a site's own position for an inner element wins. */
|
|
35
|
+
:where(.gw-spot) { position: relative; isolation: isolate; }
|
|
36
|
+
/* the light. Not on a busy button: its ::before is the ring. */
|
|
37
|
+
.gw-spot:not([aria-busy="true"])::before {
|
|
38
|
+
--gw-spot-mix: var(--gw-spot-strength);
|
|
39
|
+
content: "";
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset: 0;
|
|
42
|
+
z-index: -1;
|
|
43
|
+
border-radius: inherit;
|
|
44
|
+
background: radial-gradient(circle var(--gw-spot-r) at var(--gw-spot-x, 50%) var(--gw-spot-y, 50%), color-mix(in srgb, var(--gw-accent) var(--gw-spot-mix), transparent), transparent 70%);
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
transition: --gw-spot-r var(--gw-spot-ms) var(--gw-spot-ease);
|
|
47
|
+
}
|
|
48
|
+
/* a button is small and filled: a stronger light. Its words keep their contrast: the light is the accent over the
|
|
49
|
+
button's fill, so under the accent action profile it lies between the accent and the accent-strong, the two fills
|
|
50
|
+
the build checks the words on. */
|
|
51
|
+
.gw-btn.gw-spot:not([aria-busy="true"])::before { --gw-spot-mix: var(--gw-spot-strength-action); }
|
|
52
|
+
/* the mega menu's feature card is the Stage, a night ground: a stronger light, or it would not show */
|
|
53
|
+
.gw-mega__feature.gw-spot:not([aria-busy="true"])::before { --gw-spot-mix: var(--gw-spot-strength-stage); }
|
|
54
|
+
|
|
55
|
+
/* words under the light: lit, muted words are ink-2 at once (rules above); back to muted only once the light has gone
|
|
56
|
+
out (a colour that waits the light's time, no movement). :where(), so a part's own transition wins. */
|
|
57
|
+
:where([data-gw-ready~="spot"] *) { transition: color 0s linear var(--gw-spot-ms); }
|
|
58
|
+
:where([data-gw-ready~="spot"]:is(:hover, :focus-visible) *) { transition-delay: 0s; }
|
|
59
|
+
:where([data-gw-ready~="spot"]:has(:focus-visible) *) { transition-delay: 0s; }
|
|
60
|
+
|
|
61
|
+
/* keyboard focus: the light in the middle, so focus is never weaker than hover */
|
|
62
|
+
:is([data-gw-ready~="spot"]:focus-visible, [data-gw-ready~="spot"]:focus-visible *).gw-spot::before { --gw-spot-x: 50%; --gw-spot-y: 50%; }
|
|
63
|
+
:is([data-gw-ready~="spot"]:has(:focus-visible), [data-gw-ready~="spot"]:has(:focus-visible) *).gw-spot::before { --gw-spot-x: 50%; --gw-spot-y: 50%; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* reduced motion: a still light in the middle, there at once and gone at once, the words with it (website.js follows
|
|
67
|
+
no pointer then) */
|
|
68
|
+
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
|
|
69
|
+
.gw-spot:not([aria-busy="true"])::before { --gw-spot-x: 50%; --gw-spot-y: 50%; transition: none; }
|
|
70
|
+
:where([data-gw-ready~="spot"] *) { transition: none; }
|
|
71
|
+
}
|
package/css/footer.css
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/* footer: brand block, link columns, bottom row. One hairline on top, white like the page. */
|
|
2
2
|
.gw-footer {
|
|
3
|
-
padding-block: 64px
|
|
3
|
+
padding-block: 64px 32px;
|
|
4
4
|
border-top: 1px solid var(--gw-line);
|
|
5
|
-
background:
|
|
5
|
+
background: transparent;
|
|
6
6
|
color: var(--gw-ink);
|
|
7
7
|
font-size: 14px;
|
|
8
8
|
}
|
|
9
9
|
.gw-footer__grid {
|
|
10
10
|
display: grid;
|
|
11
11
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
12
|
-
gap: 32px
|
|
13
|
-
}
|
|
14
|
-
.gw-footer__brand {
|
|
15
|
-
.gw-footer__brand
|
|
16
|
-
.gw-
|
|
12
|
+
gap: 32px;
|
|
13
|
+
}
|
|
14
|
+
.gw-footer__brand p { max-width: 30ch; margin-top: 16px; color: var(--gw-muted); line-height: 22px; }
|
|
15
|
+
.gw-footer__brand .gw-brand { display: flex; min-height: 34px; font-family: var(--gw-font); line-height: 1.6; }
|
|
16
|
+
.gw-footer__brand .gw-brand__mark { width: 34px; height: 34px; border-radius: var(--gw-radius-8); }
|
|
17
|
+
/* the brand's name, the column headings and the wordmark are set in the kit's face (--gw-font), whatever the page
|
|
18
|
+
does to its body or its h2 (a page in Chinese puts PingFang first; gemboss.ai keeps Inter for these three); a
|
|
19
|
+
heading is balanced, like gemboss.ai's main pages */
|
|
20
|
+
.gw-footer__heading { margin: 0 0 12px; color: var(--gw-ink); font-family: var(--gw-font); font-size: 13px; font-weight: 600; letter-spacing: 0; line-height: 1.6; text-wrap: balance; }
|
|
17
21
|
/* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
|
|
18
22
|
reader agrees to; only when the form is wired to a list that keeps the address */
|
|
19
23
|
.gw-footer__newsletter { margin-top: 24px; }
|
|
@@ -26,33 +30,35 @@
|
|
|
26
30
|
flex-wrap: wrap;
|
|
27
31
|
align-items: center;
|
|
28
32
|
justify-content: space-between;
|
|
29
|
-
gap:
|
|
30
|
-
margin-top:
|
|
33
|
+
gap: 12px 24px;
|
|
34
|
+
margin-top: 32px;
|
|
31
35
|
padding-top: 24px;
|
|
32
36
|
border-top: 1px solid var(--gw-line);
|
|
33
37
|
color: var(--gw-muted);
|
|
34
38
|
font-size: 13px;
|
|
35
39
|
}
|
|
36
40
|
.gw-footer__bottom:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
37
|
-
.gw-footer__bottom ul { display: flex; flex-wrap: wrap; gap:
|
|
41
|
+
.gw-footer__bottom ul { display: flex; flex-wrap: wrap; gap: 16px; margin: 0; padding: 0; list-style: none; }
|
|
38
42
|
.gw-footer__bottom a {
|
|
39
43
|
display: inline-flex;
|
|
40
44
|
align-items: center;
|
|
41
45
|
justify-content: center;
|
|
42
|
-
min-width: 24px;
|
|
43
|
-
min-height: 24px;
|
|
44
46
|
color: var(--gw-muted);
|
|
45
47
|
}
|
|
46
48
|
.gw-footer__bottom a:hover { color: var(--gw-ink); }
|
|
47
|
-
@media (
|
|
48
|
-
.gw-footer__grid {
|
|
49
|
+
@media (max-width: 620px) {
|
|
50
|
+
.gw-footer__grid { gap: 24px 20px; }
|
|
51
|
+
.gw-footer__brand { grid-column: 1 / -1; }
|
|
49
52
|
}
|
|
50
|
-
@media (min-width:
|
|
51
|
-
.gw-footer__grid { grid-auto-flow: column; grid-template-columns: minmax(0, 1.6fr); grid-auto-columns: minmax(0, 1fr); }
|
|
52
|
-
.gw-footer__brand { grid-column: auto; }
|
|
53
|
+
@media (min-width: 1081px) {
|
|
54
|
+
.gw-footer__grid { grid-auto-flow: column; grid-template-columns: minmax(0, 1.6fr); grid-auto-columns: minmax(0, 1fr); gap: 32px 24px; }
|
|
53
55
|
}
|
|
54
56
|
@media (max-width: 768px), (pointer: coarse) {
|
|
57
|
+
.gw-footer__brand .gw-brand { min-height: var(--gw-tap-min); }
|
|
58
|
+
.gw-footer__col a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
|
|
55
59
|
.gw-footer__bottom a { min-height: var(--gw-tap-min); }
|
|
60
|
+
.gw-footer__bottom ul:last-child a { min-width: var(--gw-tap-min); }
|
|
61
|
+
.gw-footer__bottom ul:not(:last-child) { gap: 8px 16px; }
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
/* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
|
|
@@ -66,16 +72,14 @@
|
|
|
66
72
|
touch. */
|
|
67
73
|
.gw-footer__ask { margin-top: 24px; }
|
|
68
74
|
/* the row may run wider than the brand's sentence (four names on one line at 390px) */
|
|
69
|
-
.gw-
|
|
70
|
-
.gw-footer__brand:has(> .gw-footer__ask) > :not(.gw-footer__ask) { max-width: 320px; }
|
|
71
|
-
.gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; }
|
|
75
|
+
.gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; line-height: 22px; }
|
|
72
76
|
.gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
|
|
73
77
|
.gw-footer__ask a { display: block; padding-block: 4px; color: var(--gw-ink); line-height: 20px; }
|
|
74
|
-
.gw-footer__ask a:hover { text-decoration: underline;
|
|
78
|
+
.gw-footer__ask a:hover { text-decoration: underline; }
|
|
75
79
|
|
|
76
80
|
/* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
|
|
77
81
|
.gw-visually-hidden span, only when the link has target="_blank" */
|
|
78
|
-
.gw-footer__ext {
|
|
82
|
+
.gw-footer__ext:empty {
|
|
79
83
|
display: inline-block;
|
|
80
84
|
width: 12px;
|
|
81
85
|
height: 12px;
|
|
@@ -85,6 +89,7 @@
|
|
|
85
89
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
86
90
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
87
91
|
}
|
|
92
|
+
.gw-footer__ext:not(:empty) { color: var(--gw-muted); font-weight: 400; }
|
|
88
93
|
|
|
89
94
|
/* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
|
|
90
95
|
.gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
|
|
@@ -93,48 +98,52 @@
|
|
|
93
98
|
(::details-content; website.js also sets `open` there, for browsers without it). */
|
|
94
99
|
.gw-footer__fold > summary { list-style: none; }
|
|
95
100
|
.gw-footer__fold > summary::-webkit-details-marker { display: none; }
|
|
96
|
-
.gw-footer__fold .gw-footer__heading { margin: 0;
|
|
101
|
+
.gw-footer__fold .gw-footer__heading { margin: 0; }
|
|
97
102
|
.gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
|
|
98
103
|
.gw-footer__col > .gw-footer__fold a { line-height: 20px; }
|
|
99
|
-
@media (min-width:
|
|
104
|
+
@media (min-width: 621px) {
|
|
100
105
|
.gw-footer__fold > summary { display: contents; }
|
|
101
106
|
.gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
|
|
102
107
|
}
|
|
103
|
-
@media (min-width:
|
|
108
|
+
@media (min-width: 621px) {
|
|
104
109
|
.gw-footer__fold::details-content { content-visibility: visible; }
|
|
105
110
|
}
|
|
106
|
-
@media (max-width:
|
|
111
|
+
@media (max-width: 620px) {
|
|
107
112
|
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
|
|
108
113
|
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
|
|
109
|
-
.
|
|
114
|
+
/* a column's hairline on top, the last column's below too, a family row's below (gemboss.ai's order: a focused
|
|
115
|
+
row's ring passes under the next row's line) */
|
|
116
|
+
.gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
|
|
117
|
+
.gw-footer__col:has(> .gw-footer__fold):last-child,
|
|
110
118
|
.gw-footer__family { border-bottom: 1px solid var(--gw-line); }
|
|
111
|
-
.gw-footer__brand + .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
|
|
112
119
|
.gw-footer__fold > summary {
|
|
113
120
|
display: flex;
|
|
114
121
|
align-items: center;
|
|
115
122
|
justify-content: space-between;
|
|
116
123
|
gap: 16px;
|
|
117
|
-
min-height:
|
|
118
|
-
padding
|
|
124
|
+
min-height: 52px;
|
|
125
|
+
padding: 0;
|
|
119
126
|
cursor: pointer;
|
|
120
127
|
}
|
|
121
128
|
.gw-footer__fold > summary::after {
|
|
122
129
|
content: "";
|
|
123
130
|
flex: none;
|
|
124
|
-
width:
|
|
125
|
-
height:
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
width: 8px;
|
|
132
|
+
height: 8px;
|
|
133
|
+
margin-right: 4px;
|
|
134
|
+
border-right: 2px solid var(--gw-muted);
|
|
135
|
+
border-bottom: 2px solid var(--gw-muted);
|
|
136
|
+
rotate: 45deg;
|
|
137
|
+
translate: 0 -2px;
|
|
130
138
|
}
|
|
131
|
-
.gw-footer__fold[open] > summary::after {
|
|
139
|
+
.gw-footer__fold[open] > summary::after { rotate: 225deg; translate: 0 2px; }
|
|
140
|
+
.gw-footer__fold > summary:focus-visible { outline: 2px solid var(--gw-accent); outline-offset: 2px; border-radius: var(--gw-radius-6); }
|
|
132
141
|
.gw-footer__fold > ul { padding-bottom: 12px; }
|
|
133
142
|
.gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
|
|
134
143
|
}
|
|
135
144
|
/* desktop: the brand's track is as wide as gemboss.ai draws it beside five columns (1.2 parts of 6.2, after five 24px
|
|
136
145
|
gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
|
|
137
|
-
@media (min-width:
|
|
146
|
+
@media (min-width: 1081px) {
|
|
138
147
|
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -144,6 +153,8 @@
|
|
|
144
153
|
the items start where the first column starts. */
|
|
145
154
|
.gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
|
|
146
155
|
.gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
156
|
+
.gw-footer__family .gw-footer__fold > ul > li { display: flex; }
|
|
157
|
+
.gw-footer__family .gw-footer__fold > ul > li > a { flex: 1 1 auto; }
|
|
147
158
|
.gw-footer__family a {
|
|
148
159
|
display: flex;
|
|
149
160
|
flex-direction: column;
|
|
@@ -157,14 +168,18 @@
|
|
|
157
168
|
.gw-footer__family a:hover { background: var(--gw-bg-soft); }
|
|
158
169
|
.gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
|
|
159
170
|
.gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
|
|
160
|
-
@media (min-width:
|
|
171
|
+
@media (min-width: 621px) {
|
|
161
172
|
.gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
|
|
162
173
|
.gw-footer__family .gw-footer__heading { margin-bottom: 12px; }
|
|
163
|
-
.gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px;
|
|
174
|
+
.gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; }
|
|
164
175
|
.gw-footer__family a { min-height: 0; padding: 8px 12px; }
|
|
165
176
|
.gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
|
|
166
177
|
}
|
|
167
|
-
@media (min-width:
|
|
178
|
+
@media (min-width: 621px) and (max-width: 768px) {
|
|
179
|
+
.gw-footer__family a { min-height: var(--gw-tap-min); }
|
|
180
|
+
}
|
|
181
|
+
@media (min-width: 1081px) {
|
|
182
|
+
.gw-footer__family .gw-footer__fold > ul { margin-left: -12px; }
|
|
168
183
|
.gw-footer__family { display: flow-root; }
|
|
169
184
|
.gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
|
|
170
185
|
.gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
|
|
@@ -172,7 +187,7 @@
|
|
|
172
187
|
|
|
173
188
|
/* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
|
|
174
189
|
square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
|
|
175
|
-
|
|
190
|
+
Up to 620px the copyright and the toggle share the first line, legal and social links the second. */
|
|
176
191
|
.gw-footer__toggle {
|
|
177
192
|
position: relative;
|
|
178
193
|
width: 32px;
|
|
@@ -183,12 +198,15 @@
|
|
|
183
198
|
}
|
|
184
199
|
.gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
|
|
185
200
|
.gw-footer__toggle svg { width: 18px; height: 18px; }
|
|
201
|
+
/* on hover the hairline turns ink and the icon the accent, the ground stays (gemboss.ai's); colour only, 0.15s */
|
|
202
|
+
.gw-footer__toggle { transition: border-color 0.15s, color 0.15s; }
|
|
203
|
+
.gw-footer__toggle:hover { border-color: var(--gw-ink); background: var(--gw-surface); color: var(--gw-accent); }
|
|
186
204
|
@media (max-width: 768px), (pointer: coarse) {
|
|
187
205
|
.gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
|
|
188
206
|
}
|
|
189
|
-
@media (max-width:
|
|
190
|
-
.gw-footer__bottom:has(> .gw-footer__toggle) {
|
|
191
|
-
.gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% -
|
|
207
|
+
@media (max-width: 620px) {
|
|
208
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) { gap: 8px 16px; }
|
|
209
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - 64px); }
|
|
192
210
|
.gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
|
|
193
211
|
.gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
|
|
194
212
|
.gw-footer__family + .gw-footer__bottom,
|
|
@@ -198,37 +216,35 @@
|
|
|
198
216
|
/* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
|
|
199
217
|
the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
|
|
200
218
|
page scroll sideways: a name too long for the line is cut. The mark is drawn in currentColor (an inline SVG); an
|
|
201
|
-
<img> mark keeps its own colours, so leave it out and keep the name.
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
<img> mark keeps its own colours, so leave it out and keep the name. Values are gemboss.ai's, 1:1 (Chris 26/10/2026):
|
|
220
|
+
weight 800, 48px above, nothing below but the footer's own 32px, the mark 7vw wide (40–96px), hidden up to 620px.
|
|
221
|
+
The footer is not painted (transparent on the page), so the breathing gate reads it as part of the page. */
|
|
204
222
|
.gw-footer__wordmark {
|
|
205
223
|
display: flex;
|
|
206
224
|
align-items: center;
|
|
207
225
|
justify-content: center;
|
|
208
226
|
gap: 24px;
|
|
209
|
-
padding-block: 48px
|
|
227
|
+
padding-block: 48px 0;
|
|
210
228
|
color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
|
|
229
|
+
font-family: var(--gw-font);
|
|
211
230
|
font-size: var(--gw-display-wordmark);
|
|
212
|
-
font-weight:
|
|
231
|
+
font-weight: 800;
|
|
213
232
|
letter-spacing: var(--gw-display-tracking);
|
|
214
233
|
line-height: 1;
|
|
215
234
|
white-space: nowrap;
|
|
216
235
|
user-select: none;
|
|
217
236
|
}
|
|
218
237
|
.gw-footer__wordmark > svg,
|
|
219
|
-
.gw-footer__wordmark > img { flex: none; width:
|
|
238
|
+
.gw-footer__wordmark > img { flex: none; width: clamp(40px, 7vw, 96px); height: auto; }
|
|
220
239
|
.gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
|
|
221
|
-
@media (max-width:
|
|
222
|
-
@media (max-width:
|
|
240
|
+
@media (max-width: 640px) { .gw-footer__wordmark { gap: 16px; } }
|
|
241
|
+
@media (max-width: 620px) { .gw-footer__wordmark { display: none; } }
|
|
223
242
|
|
|
224
243
|
@media (max-width: 768px), (pointer: coarse) {
|
|
225
244
|
.gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
|
|
226
245
|
}
|
|
227
|
-
@media (prefers-reduced-motion: reduce) {
|
|
228
|
-
.gw-footer__fold > summary::after { transition: none; }
|
|
229
|
-
}
|
|
230
246
|
/* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
|
|
231
247
|
@media (forced-colors: active) {
|
|
232
|
-
.gw-footer__ext
|
|
233
|
-
.gw-footer__fold > summary::after {
|
|
248
|
+
.gw-footer__ext:empty { forced-color-adjust: none; background: CanvasText; }
|
|
249
|
+
.gw-footer__fold > summary::after { border-color: CanvasText; }
|
|
234
250
|
}
|
package/css/header.css
CHANGED
|
@@ -437,3 +437,15 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
|
|
|
437
437
|
@media (min-width: 1080px) {
|
|
438
438
|
.gw-header__actions > .gw-lang { display: block; }
|
|
439
439
|
}
|
|
440
|
+
|
|
441
|
+
/* ---- 0.10.0: the theme toggle's two icons ---------------------------------------------------------------------------
|
|
442
|
+
A toggle may hold two icons: .gw-theme-toggle__moon, shown in light, and .gw-theme-toggle__sun, shown in dark, as
|
|
443
|
+
gemboss.ai draws them. The theme in force picks one: data-theme on <html>, else the system's. A toggle with one plain
|
|
444
|
+
<svg> stays as it is. */
|
|
445
|
+
.gw-theme-toggle__sun { display: none; }
|
|
446
|
+
:root[data-theme="dark"] .gw-theme-toggle__sun { display: block; }
|
|
447
|
+
:root[data-theme="dark"] .gw-theme-toggle__moon { display: none; }
|
|
448
|
+
@media (prefers-color-scheme: dark) {
|
|
449
|
+
:root:not([data-theme="light"]) .gw-theme-toggle__sun { display: block; }
|
|
450
|
+
:root:not([data-theme="light"]) .gw-theme-toggle__moon { display: none; }
|
|
451
|
+
}
|