@gemboss/website-ui 0.6.0 → 0.8.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 CHANGED
@@ -10,15 +10,19 @@ 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, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box; 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; 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 |
17
+ | `templates/header.html` | the header of every site, with its two options (the drill-down mobile menu, the language switcher): fill it once, in the site's shared partial |
18
+ | `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
17
19
  | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
18
20
  | `demo/index.html` | every component on one page: open it to see what exists before you build anything |
19
21
  | `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
20
22
  | `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
21
23
  | `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
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
+ | `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 |
22
26
  | `docs/components.md` | every class, what it is for, its variants |
23
27
  | `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
24
28
 
@@ -57,6 +61,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
57
61
 
58
62
  1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
59
63
  header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
64
+ **The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
65
+ words) and deletes the optional parts it does not use (Ask AI, newsletter, family rows, theme toggle, wordmark);
66
+ it never invents a footer part (a logo strip, badges, a map, a second bottom row). Something missing is a gap.
60
67
  2. Pick sections from `templates/sections/` in the order the page needs. A typical page: hero → proof (logos or
61
68
  stats) → 2 to 4 feature sections → one showcase or split → FAQ → final CTA. Not every page needs every section;
62
69
  fewer, clearer sections beat more. A page that is read rather than scanned (a blog post, docs, terms, privacy) is
@@ -92,6 +99,15 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
92
99
  button in a row `.gw-form-inline`, a short form `.gw-form`. Ask as little as you can: one field beats a form, and a
93
100
  form keeps two or three fields (`templates/sections/form.html`). A field in error is `aria-invalid="true"` plus its
94
101
  `.gw-field__error` in words; the page decides when, the kit draws it.
102
+ - **Footer** (0.7.0): `templates/footer.html`. Columns are `<details class="gw-footer__fold">` with the heading in the
103
+ `<summary>`: rows that fold on phones, open columns from 768px, no script needed. A link that leaves the site gets the
104
+ ↗ (`.gw-footer__ext`); one with `target="_blank"` also `<span class="gw-visually-hidden"> (opens in a new tab)</span>`.
105
+ The wordmark is `aria-hidden="true"`.
106
+ - **Header** (0.8.0): `templates/header.html`. Two options. The mobile menu drills down (`data-gw-menu="drill"`): each
107
+ group carries its head, `<button class="gw-mobile-menu__back">` and `<h2 class="gw-mobile-menu__title" tabindex="-1">`
108
+ in the page's language, and the actions sit in a bar at the bottom. The language switcher (`.gw-lang`, kept `hidden`,
109
+ its list empty) lists the page's `<link rel="alternate" hreflang>` from `<head>` and shows itself when there is
110
+ another language; its word for "Language" goes in `data-gw-label-language`.
95
111
  - **Ratings** (0.6.0): stars `.gw-stars` (`role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"`), the
96
112
  wall `.gw-testimonials` of `.gw-card.gw-testimonial`, the badge `.gw-rating-badge` (`templates/sections/testimonials.html`).
97
113
  - **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
@@ -138,6 +154,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
138
154
  - Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
139
155
  - Don't type separators (›, /, >) between breadcrumbs, and don't build a contents list whose links point at ids the
140
156
  page does not have: build it from the article's own h2s.
157
+ - Don't build your own language menu or phone menu, don't type the switcher's links (it reads `<link rel="alternate"
158
+ hreflang>`), and don't show it on a page that has one language. Don't leave Back in English on a translated page.
159
+ - Don't add a footer part the template does not have, and don't build footer columns that fold with your own script:
160
+ fill `templates/footer.html` (its family rows take another brand's siblings: GemBoss, GemX, GemUpsells on a
161
+ GemPages site). Don't put the assistants' logos in the Ask AI row.
141
162
  - Don't animate layout properties (width, height, top, margin), and don't autoplay motion that cannot be stopped.
142
163
  - Don't ship a page you have not seen at 320px.
143
164
  - Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
@@ -250,7 +271,9 @@ a waiting button not marked with `aria-busy`, a segmented control without its ra
250
271
  with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
251
272
  words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
252
273
  the page, a callout without its label, stars without their value in words, a rating or a testimonial without a link to
253
- its source, and patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
274
+ 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`, and
276
+ patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
254
277
 
255
278
  ## 11. Upgrading the kit
256
279
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,136 @@ 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.8.0 (2026-10-25)
8
+
9
+ 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
10
+ each section with its actions in a bar at the bottom, and the language switcher. Both are options: a header without them
11
+ looks and works exactly as before. The demo index (without its new link), `brand-profiles.html`, `article.html`,
12
+ `blog.html`, `forms.html`, `footer.html` and `theme-example.html` are pixel-identical to 0.7.0 at 390 and 1366px, light
13
+ and dark.
14
+
15
+ - Drill-down mobile menu, opt-in: `data-gw-menu="drill"` on `.gw-mobile-menu`, as gemboss.ai's phone menu (v4, approved
16
+ 15/10/2026). The markup is the 0.7.0 menu, plus a head in each group: `<div class="gw-mobile-menu__head">` holding
17
+ `<button class="gw-mobile-menu__back" type="button">Back</button>` and `<h2 class="gw-mobile-menu__title"
18
+ tabindex="-1">`, in the page's language. The top level lists the sections in 64px rows (24px words, a chevron that
19
+ points right); a press on one shows its level alone, with Back on the left and its title in the middle; a level may
20
+ carry headings over runs of links (`.gw-mobile-menu__heading`), a 32px icon tile and a line under a link's name
21
+ (`.gw-mobile-menu__icon`, `<b>`, `<i>`), and a last link to the section's own page (`.gw-mobile-menu__all`, in the
22
+ accent, with its arrow). The actions sit in a bar that stays at the bottom of the sheet while the list scrolls: what is
23
+ not a button on its first line (the language on the left, Sign in on the right), the two buttons side by side under it
24
+ (`gw-btn--lg`: 48px), one hairline above, the safe area below.
25
+ Closes gap #41
26
+ - Its script, wired like every other part (one record, `init` once per element, `destroy` takes it back): a press on a
27
+ section (tap, click, Enter, Space) opens its level and moves the focus to the level's title; Back shows the top level
28
+ and puts the focus back on the section; Escape closes the sheet from anywhere in it, back on the top level, the focus on
29
+ the toggle; while it is open, Tab and Shift+Tab go round the toggle and the sheet (the focus trap); the page under it
30
+ is locked (as in 0.7.0); the sheet starts where the header ends (measured as it opens, `--gw-menu-top`) and the header
31
+ stays in place, so an announcement bar above it no longer jumps away. A level slides in from the right (0.24s, the
32
+ kit's ease, transform and opacity), the sections slide back from the left; under `prefers-reduced-motion` they switch
33
+ at once. Without the script (or before `init`) the groups are the 0.7.0 `<details>` that open in place. `.is-drill`
34
+ (wired), `.is-drilled` (a level in view) and `.is-back` (the slide back) are the script's states.
35
+ - `gw:menu`, a new event on the menu (`.gw-mobile-menu`), `detail: { open, level }`: `level` is the title of the level
36
+ in view, `''` for the top level. It fires when a menu opens or closes (every mobile menu, the 0.7.0 one too) and when
37
+ a drill-down menu moves to a level or back; closing from a level says only `{ open: false, level: '' }`.
38
+ - Language switcher `.gw-lang` (`data-gw-lang`, `hidden` in the markup): a ghost button with the globe and the current
39
+ language's short code (`.gw-lang__btn`, `.gw-lang__short`), and the list of the other languages (`.gw-lang__menu`), a
40
+ small popover card with the card radius of the site's profile. `website.js` fills the list from the page's own
41
+ `<link rel="alternate" hreflang href>` (not `x-default`): the links search engines read, so the switcher offers
42
+ exactly the translations they are told about, and a page with no other language keeps the switcher hidden (gemboss.ai
43
+ today, while `liveLangs` is empty). The current language is the one whose `hreflang` is `<html lang>` (else the same
44
+ base language: `zh` for `zh-Hans`). Each other one is a real link (`.gw-lang__link`, with `hreflang` and `lang`), named
45
+ in its own language: the `<link>`'s `title`, else the kit's name (English, Tiếng Việt, 中文), else the browser's. A link
46
+ to the host of `<link rel="canonical">` keeps only its path, so a preview deployment stays on itself. Every `init`
47
+ reads the links again (a route change). Keyboard: the button opens and closes it; ArrowDown and ArrowUp on it open the
48
+ list on its first or last link, move in it (wrapping), Home and End; Escape closes it with the focus back on the
49
+ button, and goes no further; focus leaving it or a click outside closes it. Its words come from the markup:
50
+ `data-gw-label-language` (else the button's `aria-label`, else "Language") names the button "Language: English" and
51
+ the list "Language". In the header's actions it shows from 1080px; below, a second one sits in the mobile menu's bar
52
+ and its list opens upward. No new dependency: it replaces loading `@gemboss/ui`'s plain switcher next to the kit.
53
+ Closes gap #29
54
+ - Templates: `templates/header.html` shows both options, with comments: the drill-down menu (and how to leave it out),
55
+ the switcher in the actions and in the bar. `templates/page.html` keeps the 0.7.0 menu.
56
+ - Demo: `demo/header.html`, the header of gemboss.ai with its words, sections, links and icons, the drill-down menu, an
57
+ announcement bar, and the switcher with three languages (en, vi, zh-Hans) whose alternates point at the demo itself
58
+ (`?hl=`), with a switch to the same header for a GemPages-style brand (soft corners, the accent button, the floating
59
+ header from 1080px, GemPages' blue). Linked from `demo/index.html`.
60
+ - `check`: `menu-markup` (an error): in a `data-gw-menu="drill"` menu, a group that is not a `<details>`, has no
61
+ `<summary>`, no back button, no title, or a title without `tabindex="-1"`; `data-gw-menu` with another value.
62
+ `lang-markup` (an error): a `.gw-lang` not `hidden` in the markup, without `data-gw-lang`, without its button, a button
63
+ naming no list inside it or without `aria-expanded`, links typed into its list; and, on a page with a switcher, a
64
+ translation's `<link rel="alternate">` (no `type`) without `hreflang` or `href`.
65
+ - `AGENTS.md`, the skill and the docs (`components.md`, `markup-contract.md`): the two options, their markup, the
66
+ script's behaviour and `gw:menu`.
67
+ - Budget: `website.min.css` is 71.8 KB, 12.2 KB gzipped (0.7.0: 65.8 KB, 11.4 KB). `js/website.js` is 20.8 KB
68
+ unminified, 6.4 KB gzipped (0.7.0: 14.5 KB, 4.7 KB): the test's budget is now 22 KB and 7 KB.
69
+ - Tests: `test/run.mjs` holds the 0.7.0 rules of `header.css` byte for byte and every 0.8.0 rule to a 0.8.0 part, and
70
+ covers the drill-down menu, the trap, `gw:menu` and the switcher in its fake DOM (which now sets text, has
71
+ `getClientRects`, style properties, `URL` and `Intl`) and the two rules; `test/browser.mjs` drives `demo/header.html`
72
+ in Chromium (by touch and by keyboard, the focus, Escape, the trap, the scroll lock, the bar at the bottom of a long
73
+ level, reduced motion, without the script, the switcher shown and hidden by the head's links, its keys and a real
74
+ switch to the Vietnamese page, init twice, destroy then init, 320 and 390px light and dark, the GemPages-style switch).
75
+
76
+ ## 0.7.0 (2026-10-25)
77
+
78
+ The footer as gemboss.ai has it today (gap #71): the kit's footer, built on 24/10 from gemboss.ai, had fallen behind
79
+ the site's. Every new part is optional, per site, and selects only its own markup: a page with the 0.6.0 footer looks
80
+ exactly as before. The demo index (without its new link), `brand-profiles.html`, `article.html`, `blog.html`,
81
+ `forms.html` and `theme-example.html` are pixel-identical to 0.6.0 at 390 and 1366px, light and dark.
82
+
83
+ Closes #71
84
+
85
+ - Ask AI `.gw-footer__ask` (+ `__ask-label`) in the brand block: "Ask AI about [Brand]" and four links (ChatGPT,
86
+ Claude, Perplexity, Google AI Mode) that open the assistant with one neutral question already typed, URL-encoded
87
+ after `?q=` (Google: `search?udm=50&q=`), as gemboss.ai builds them. Words, not the assistants' logos. 28px rows,
88
+ 44px on touch; the row may run wider than the brand's sentence (four names on one line at 390px).
89
+ - Columns that fold: each `.gw-footer__col` holds a `<details class="gw-footer__fold">` whose `<summary>` is the
90
+ column's `<h2 class="gw-footer__heading">`. Below 768px the columns are one track of 52px rows with a chevron,
91
+ folded, and the browser opens and closes them (no script needed); the open list's links are 44px. From 768px they
92
+ are the columns as before: the summary is only its heading (`display: contents`: no box, no Tab stop) and the list
93
+ shows even while the `<details>` is closed (`::details-content`, in a rule of its own so an older browser drops only
94
+ it). `website.js` sets `open` from 768px, takes the summary out of the Tab order there and ignores a click on it,
95
+ gives back the markup's state below 768px (again on every change of width), and is wired like every other part
96
+ (`data-gw-ready="fold"`, `init` once per element, `destroy` takes it back). From 1080px the brand's track is as
97
+ wide as gemboss.ai draws it beside five columns (193.5px at 1120px), whatever the number of columns, and the columns
98
+ share the rest: three to six fit. With the fold markup only; the 0.6.0 columns keep their 1.6fr.
99
+ Closes gap #40 (footer accordion on phones)
100
+ - Product family `.gw-footer__family`: a `<nav aria-label>` holding a fold whose heading is the row's label, and a
101
+ list of products, name and one line each (`<b>` and `<i>`, a space between). Repeat it for a second family ("More
102
+ from GemBoss", "From the same team, on the Shopify App Store"). Two items a line on phones (one more folded row),
103
+ four from 768px, and from 1080px the label in the brand's track and the items starting where the first column
104
+ starts. Items take the action radius (12px in `soft`) and the soft ground on hover.
105
+ - External links: `.gw-footer__ext`, a drawn ↗ after a link that leaves the site (aria-hidden). A link with
106
+ `target="_blank"` also carries `<span class="gw-visually-hidden"> (opens in a new tab)</span>`; one without it does
107
+ not. `.gw-visually-hidden` is new in `base.css`: words a screen reader hears and the page does not show.
108
+ - Theme toggle in the bottom row: `.gw-theme-toggle.gw-footer__toggle` with `data-gw-theme-toggle`, wired by
109
+ `website.js` like the header's (both relabel together). A 32px square with one hairline and the action radius, a
110
+ 40px hit area, 44px on touch; it pushes itself and the social links to the right. Below 768px the copyright and the
111
+ toggle share the first line, the legal and social links the second.
112
+ - Wordmark `.gw-footer__wordmark` (`aria-hidden="true"`): the brand's mark (an inline SVG in `currentColor`) and name,
113
+ ink at 10%, at `--gw-display-wordmark` (`clamp(40px, 9vw, 128px)`, new in `system.json` `type.display`), the
114
+ display tracking, weight 700, centred, 48px under the bottom row. A name too long for the line is cut, never a
115
+ sideways scroll. Not shown below 480px, as gemboss.ai hides it on phones.
116
+ - Spacing as measured on gemboss.ai and on the kit's scale: family rows 48px apart with 32px under their hairline,
117
+ 32px from the last family row to the bottom row (24px on phones), headings 12px above their links, 32px link rows.
118
+ - Templates: `templates/footer.html` is now the full footer, every optional part marked (Ask AI, the 0.6.0
119
+ newsletter, the family row, the toggle, the wordmark), five columns; `templates/page.html` uses the folding columns.
120
+ Demo: `demo/footer.html`, the gemboss.ai footer with gemboss.ai's words, and a switch to the same footer for a
121
+ GemPages-style brand (`data-gw-radius="soft"`, `data-gw-action="accent"`, the GemPages accent of
122
+ `docs/theming.md`, a family row listing GemBoss, GemX and GemUpsells, the newsletter instead of Ask AI), linked
123
+ from `demo/index.html`.
124
+ - `check`: `footer-markup` (an error): a `.gw-footer__fold` that is not a `<details>`, has no `<summary>`, or whose
125
+ summary does not hold the `.gw-footer__heading`; a `.gw-footer__wordmark` without `aria-hidden="true"`.
126
+ - `AGENTS.md`, the skill and the docs: the footer comes from `templates/footer.html`; a brand fills its rows and
127
+ never invents a footer part.
128
+ - Budget: `website.min.css` is 65.8 KB, 11.4 KB gzipped (0.6.0: 59.3 KB, 10.5 KB), most of it the three drawn icons'
129
+ masks. `js/website.js` is 14.5 KB unminified, 4.7 KB gzipped (0.6.0: 13.8 KB, 4.5 KB), under the test's 15 KB and
130
+ 5 KB.
131
+ - Tests: `test/run.mjs` holds the 0.6.0 rules of `footer.css` byte for byte and every 0.7.0 rule to a 0.7.0 part, and
132
+ covers the folds in its fake DOM and the new rule; `test/browser.mjs` drives `demo/footer.html` in Chromium (folded
133
+ at 390px, open at 1366px, Tab, Enter and Space, a click on a desktop heading, resizing both ways, without the
134
+ script, the wordmark out of the accessibility tree, no sideways scroll at 320 and 390px, the toggle, the
135
+ GemPages-style switch light and dark, and the column widths measured on gemboss.ai).
136
+
7
137
  ## 0.6.0 (2026-10-25)
8
138
 
9
139
  Forms and ratings: the fields a contact page, a sign-up or a footer newsletter needs, with their error state, and the
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 82 KB, 59 KB bản min, 10,5 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, footer (kèm ô newsletter tuỳ chọn), 16 section, 3 hồ sơ thương hiệu |
16
- | `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại, đổ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; không phụ thuộc thư viện nào, khoảng 4,5 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") |
15
+ | `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 101 KB, 72 KB bản min, 12,2 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), 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; không phụ thuộc thư viện nào, khoảng 6,4 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
- | `templates/` | khung một trang 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) |
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 |
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.6.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.8.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.6.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.8.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
@@ -113,7 +113,7 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
113
113
  | lệnh | làm gì |
114
114
  |---|---|
115
115
  | `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
116
- | `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ, sao thiếu giá trị bằng chữ, điểm hay review không có link nguồn… và gợi ý chỗ kit có thể đang thiếu |
116
+ | `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ, sao thiếu giá trị bằng chữ, điểm hay review không có link nguồn, cột footer gập thiếu tiêu đề trong summary, wordmark không ẩn với trình đọc màn hình, mục của menu đi sâu thiếu nút Back hay tiêu đề, nút ngôn ngữ không ẩn sẵn hay bản dịch thiếu `hreflang`… và gợi ý chỗ kit có thể đang thiếu |
117
117
  | `npx gemboss-website gap` | báo một chỗ kit còn thiếu: mở GitHub Issue ở repo của kit (nếu `gh` đã đăng nhập và có quyền), không thì ghi `kit-gaps/<tên>.md` để gửi |
118
118
  | `npx gemboss-website upgrade` | so bản đang dùng với bản mới nhất, in thay đổi, gap đã có trong kit, và lệnh nâng cấp |
119
119
 
@@ -141,7 +141,7 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
141
141
  cd packages/website-ui
142
142
  node scripts/build.mjs # sinh css/tokens.css từ system.json, ghép website.css và website.min.css, sinh js/website.js từ js/website.mjs
143
143
  node test/run.mjs # tự kiểm CLI, CSS và website.js (DOM giả)
144
- node test/browser.mjs # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng) và form, sao, review (cần .ci của repo)
144
+ node test/browser.mjs # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng), form, sao, review, footer (gập, Tab, nút sáng/tối) và header (menu đi sâu bằng chạm và bàn phím, giữ focus, nút ngôn ngữ) (cần .ci của repo)
145
145
  node scripts/gate-demo.mjs # cổng thị giác và khoảng thở của gemboss.ai trên demo (cần .ci của repo)
146
146
  ```
147
147
 
package/bin/lib/check.mjs CHANGED
@@ -144,6 +144,21 @@ export const RULES = {
144
144
  why: "The kit draws .gw-stars from style=\"--gw-stars: N\" (a number from 0 to 5), and a screen reader hears only its name: role=\"img\" and aria-label=\"N out of 5\". Without them the stars are empty or silent, and a label that names another number than the drawing says two things at once.",
145
145
  fix: "Write <span class=\"gw-stars\" role=\"img\" aria-label=\"4.9 out of 5\" style=\"--gw-stars: 4.9\"></span>: the same number in the style and in the label, from 0 to 5 (docs/components.md, Ratings).",
146
146
  },
147
+ "footer-markup": {
148
+ level: "error", title: "Footer fold or wordmark without its markup",
149
+ why: "A footer column folds on phones as a <details class=\"gw-footer__fold\"> whose <summary> holds the column's heading (<h2 class=\"gw-footer__heading\">): the summary is the row a reader taps, and a screen reader names the row by that heading. The wordmark (.gw-footer__wordmark) repeats the brand's name as decoration; without aria-hidden=\"true\" a screen reader reads the name a second time.",
150
+ fix: "Copy the markup from templates/footer.html: <details class=\"gw-footer__fold\"><summary><h2 class=\"gw-footer__heading\">Product</h2></summary><ul>…</ul></details>, and <div class=\"gw-footer__wordmark\" aria-hidden=\"true\">.",
151
+ },
152
+ "menu-markup": {
153
+ level: "error", title: "Drill-down menu level without its head",
154
+ why: "In a mobile menu with data-gw-menu=\"drill\" each <details class=\"gw-mobile-menu__group\"> is a level the reader opens on its own, from its <summary>. Its head is the only way back (a <button class=\"gw-mobile-menu__back\">) and its title (.gw-mobile-menu__title with tabindex=\"-1\") is where website.js puts the focus. Without them a phone visitor is stuck in the level, or a keyboard and a screen reader lose their place. data-gw-menu has one value: drill.",
155
+ fix: "Copy the group from templates/header.html: <details class=\"gw-mobile-menu__group\"><summary>Product</summary><div class=\"gw-mobile-menu__head\"><button class=\"gw-mobile-menu__back\" type=\"button\">Back</button><h2 class=\"gw-mobile-menu__title\" tabindex=\"-1\">Product</h2></div><ul class=\"gw-mobile-menu__list\">…</ul></details>, with Back in the page's language.",
156
+ },
157
+ "lang-markup": {
158
+ level: "error", title: "Language switcher without its markup",
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
+ 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
+ },
147
162
  "uppercase-text": {
148
163
  level: "warning", title: "Uppercase text",
149
164
  why: "Headings, buttons, labels and menu items are sentence case.",
@@ -547,7 +562,8 @@ function analyzeHtml(file, text, out) {
547
562
  }
548
563
 
549
564
  const classes = new Set();
550
- const ids = new Set(), tocs = [];
565
+ const ids = new Set(), tocs = [], drills = [], alts = [];
566
+ let langs = 0;
551
567
  let h1 = 0;
552
568
  for (const m of scanText.matchAll(TAG_RE)) {
553
569
  const tag = m[1].toLowerCase();
@@ -630,6 +646,52 @@ function analyzeHtml(file, text, out) {
630
646
  ].filter(Boolean);
631
647
  if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
632
648
  }
649
+ if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
650
+ add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
651
+ }
652
+ if (cls.includes("gw-footer__fold")) {
653
+ const sum = /<summary\b[^>]*>([\s\S]*?)<\/summary\s*>/i.exec(inner());
654
+ const missing = [
655
+ tag !== "details" && `a <details> (the class is on <${tag}>)`,
656
+ !sum && "a <summary> as its first child",
657
+ sum && !/\bclass\s*=\s*["'][^"']*\bgw-footer__heading\b/i.test(sum[1]) && "the column's heading inside the <summary> (<h2 class=\"gw-footer__heading\">)",
658
+ ].filter(Boolean);
659
+ if (missing.length) add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
660
+ }
661
+ if (cls.includes("gw-mobile-menu") && "data-gw-menu" in a) {
662
+ if (a["data-gw-menu"] !== "drill") add("menu-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: data-gw-menu="drill" (the one option; leave the attribute out for the 0.7.0 menu)`));
663
+ else drills.push([m.index, elementEnd(scanText, m.index, tag)]);
664
+ }
665
+ if (cls.includes("gw-mobile-menu__group") && drills.some(([s0, e0]) => m.index > s0 && (e0 < 0 || m.index < e0))) {
666
+ const body = inner();
667
+ const t = /<[a-z][\w-]*\b([^>]*\bclass\s*=\s*["'][^"']*\bgw-mobile-menu__title\b[^>]*)>/i.exec(body);
668
+ const missing = [
669
+ tag !== "details" && `a <details> (the class is on <${tag}>)`,
670
+ !/<summary\b/i.test(body) && "a <summary> (the row that opens the level)",
671
+ !/<button\b[^>]*\bclass\s*=\s*["'][^"']*\bgw-mobile-menu__back\b/i.test(body) && 'a back button (<button class="gw-mobile-menu__back" type="button">)',
672
+ !t && 'the level\'s title (<h2 class="gw-mobile-menu__title" tabindex="-1">)',
673
+ t && parseAttrs(t[1]).tabindex !== "-1" && 'tabindex="-1" on the title (the script puts the focus there)',
674
+ ].filter(Boolean);
675
+ if (missing.length) add("menu-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
676
+ }
677
+ if (cls.includes("gw-lang")) {
678
+ langs++;
679
+ const body = inner();
680
+ const b = /<button\b([^>]*\bclass\s*=\s*["'][^"']*\bgw-lang__btn\b[^>]*)>/i.exec(body);
681
+ const ba = b ? parseAttrs(b[1]) : {};
682
+ const list = ba["aria-controls"] && new RegExp(`<([a-z]+)\\b[^>]*\\bid\\s*=\\s*["']${ba["aria-controls"].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'][^>]*>`, "i").exec(body);
683
+ const missing = [
684
+ !("hidden" in a) && "hidden (the script shows it when the page has another language)",
685
+ !("data-gw-lang" in a) && "data-gw-lang",
686
+ !b && 'a <button class="gw-lang__btn" type="button">',
687
+ b && !ba["aria-controls"] && "aria-controls on the button, naming its list",
688
+ b && ba["aria-controls"] && !list && `the list the button names (id="${ba["aria-controls"]}") inside the switcher`,
689
+ b && !("aria-expanded" in ba) && 'aria-expanded="false" on the button',
690
+ /<a\b/i.test(body) && "an empty list (the script writes the links from <link rel=\"alternate\" hreflang>)",
691
+ ].filter(Boolean);
692
+ if (missing.length) add("lang-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
693
+ }
694
+ if (tag === "link" && /(?:^|\s)alternate(?:\s|$)/i.test(a.rel || "") && !a.type) alts.push({ a, where: where() });
633
695
  if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
634
696
  const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
635
697
  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)}`));
@@ -671,6 +733,13 @@ function analyzeHtml(file, text, out) {
671
733
  if (!ids.has(id)) add("toc-target", { file: relFile, line: lineAt(t.start + x.index), excerpt: `href="#${x[1]}": no element on the page has id="${id}"` });
672
734
  }
673
735
  }
736
+ // A page with a switcher: each translation it links in <head> needs its hreflang and its href.
737
+ if (langs) {
738
+ for (const x of alts) {
739
+ const missing = [!(x.a.hreflang || "").trim() && "hreflang (the switcher lists a translation by it)", !(x.a.href || "").trim() && "href"].filter(Boolean);
740
+ if (missing.length) add("lang-markup", { ...x.where, excerpt: `${x.where.excerpt} missing: ${missing.join(", ")}` });
741
+ }
742
+ }
674
743
  out.pageClasses.push({ file: relFile, classes });
675
744
  }
676
745
 
package/css/base.css CHANGED
@@ -91,6 +91,18 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
91
91
  }
92
92
  .gw-skip:focus { left: 16px; }
93
93
 
94
+ /* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
95
+ .gw-visually-hidden {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
99
+ padding: 0;
100
+ overflow: hidden;
101
+ clip-path: inset(50%);
102
+ white-space: nowrap;
103
+ border: 0;
104
+ }
105
+
94
106
  /* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
95
107
  .gw-reveal {
96
108
  opacity: 0;
package/css/footer.css CHANGED
@@ -54,3 +54,181 @@
54
54
  @media (max-width: 768px), (pointer: coarse) {
55
55
  .gw-footer__bottom a { min-height: var(--gw-tap-min); }
56
56
  }
57
+
58
+ /* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
59
+ Every part below is optional, per site, and styles only its own markup: a 0.6.0 footer looks exactly as before.
60
+ In page order: the Ask AI row in the brand block, columns that fold on phones (gap #40), product-family rows, the
61
+ theme toggle in the bottom row, and the wordmark at the very end. Values measured on gemboss.ai on 25/10/2026 and
62
+ put on the kit's scales; the full markup is templates/footer.html. */
63
+
64
+ /* Ask AI: a label and links that open an assistant with one neutral question already typed (a URL-encoded ?q=), so
65
+ the answer comes from what the web says, not from a claim the brand wrote. Words, not logos. 28px rows, 44px on
66
+ touch. */
67
+ .gw-footer__ask { margin-top: 24px; }
68
+ /* the row may run wider than the brand's sentence (four names on one line at 390px) */
69
+ .gw-footer__brand:has(> .gw-footer__ask) { max-width: none; }
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; }
72
+ .gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
73
+ .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; text-decoration-thickness: 1px; text-underline-offset: 4px; }
75
+
76
+ /* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
77
+ .gw-visually-hidden span, only when the link has target="_blank" */
78
+ .gw-footer__ext {
79
+ display: inline-block;
80
+ width: 12px;
81
+ height: 12px;
82
+ margin-left: 4px;
83
+ vertical-align: -1px;
84
+ background: var(--gw-muted);
85
+ -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
+ 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
+ }
88
+
89
+ /* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
90
+ .gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
91
+ `open`; the browser opens and closes it, no script needed. From 768px it is the column as before: the summary is
92
+ only the heading (display: contents: no box, no Tab stop), and the list shows even while the <details> is closed
93
+ (::details-content; website.js also sets `open` there, for browsers without it). */
94
+ .gw-footer__fold > summary { list-style: none; }
95
+ .gw-footer__fold > summary::-webkit-details-marker { display: none; }
96
+ .gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
97
+ .gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
98
+ .gw-footer__col > .gw-footer__fold a { line-height: 20px; }
99
+ @media (min-width: 768px) {
100
+ .gw-footer__fold > summary { display: contents; }
101
+ .gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
102
+ }
103
+ @media (min-width: 768px) {
104
+ .gw-footer__fold::details-content { content-visibility: visible; }
105
+ }
106
+ @media (max-width: 767px) {
107
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
108
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
109
+ .gw-footer__col:has(> .gw-footer__fold),
110
+ .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
+ .gw-footer__fold > summary {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ gap: 16px;
117
+ min-height: var(--gw-tap-min);
118
+ padding-block: 16px;
119
+ cursor: pointer;
120
+ }
121
+ .gw-footer__fold > summary::after {
122
+ content: "";
123
+ flex: none;
124
+ width: 16px;
125
+ height: 16px;
126
+ background: var(--gw-muted);
127
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
128
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
129
+ transition: transform 0.18s var(--gw-ease);
130
+ }
131
+ .gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
132
+ .gw-footer__fold > ul { padding-bottom: 12px; }
133
+ .gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
134
+ }
135
+ /* 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
+ gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
137
+ @media (min-width: 1080px) {
138
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
139
+ }
140
+
141
+ /* Product family: a labelled row of products, name and one line each (<b> and <i>), as many rows as the brand needs
142
+ ("More from GemBoss", "From the same team, on the Shopify App Store"). A <nav aria-label> holding a fold whose
143
+ heading is the label. Two items a row on phones, four from 768px; from 1080px the label sits in the brand's track and
144
+ the items start where the first column starts. */
145
+ .gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
146
+ .gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
147
+ .gw-footer__family a {
148
+ display: flex;
149
+ flex-direction: column;
150
+ justify-content: center;
151
+ gap: 2px;
152
+ min-height: var(--gw-tap-min);
153
+ padding: 8px 12px 8px 0;
154
+ border-radius: var(--gw-radius-action);
155
+ color: var(--gw-ink);
156
+ }
157
+ .gw-footer__family a:hover { background: var(--gw-bg-soft); }
158
+ .gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
159
+ .gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
160
+ @media (min-width: 768px) {
161
+ .gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
162
+ .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; margin-left: -12px; }
164
+ .gw-footer__family a { min-height: 0; padding: 8px 12px; }
165
+ .gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
166
+ }
167
+ @media (min-width: 1080px) {
168
+ .gw-footer__family { display: flow-root; }
169
+ .gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
170
+ .gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
171
+ }
172
+
173
+ /* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
174
+ square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
175
+ Below 768px the copyright and the toggle share the first line, legal and social links the second. */
176
+ .gw-footer__toggle {
177
+ position: relative;
178
+ width: 32px;
179
+ height: 32px;
180
+ margin-inline: auto 16px;
181
+ border: 1px solid var(--gw-line-strong);
182
+ background: var(--gw-surface);
183
+ }
184
+ .gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
185
+ .gw-footer__toggle svg { width: 18px; height: 18px; }
186
+ @media (max-width: 768px), (pointer: coarse) {
187
+ .gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
188
+ }
189
+ @media (max-width: 767px) {
190
+ .gw-footer__bottom:has(> .gw-footer__toggle) { column-gap: 16px; }
191
+ .gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - var(--gw-tap-min) - 16px); }
192
+ .gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
193
+ .gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
194
+ .gw-footer__family + .gw-footer__bottom,
195
+ .gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) + .gw-footer__bottom { margin-top: 24px; }
196
+ }
197
+
198
+ /* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
199
+ the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
200
+ 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. Not on phones, as on gemboss.ai. 24px more room
202
+ under it than gemboss.ai gives: the kit's footer is a painted band (white like the page), and the breathing gate
203
+ wants half the letters' size between them and its edge (67px under 123px letters at 1366px). */
204
+ .gw-footer__wordmark {
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ gap: 24px;
209
+ padding-block: 48px 24px;
210
+ color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
211
+ font-size: var(--gw-display-wordmark);
212
+ font-weight: 700;
213
+ letter-spacing: var(--gw-display-tracking);
214
+ line-height: 1;
215
+ white-space: nowrap;
216
+ user-select: none;
217
+ }
218
+ .gw-footer__wordmark > svg,
219
+ .gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
220
+ .gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
221
+ @media (max-width: 767px) { .gw-footer__wordmark { gap: 16px; } }
222
+ @media (max-width: 479px) { .gw-footer__wordmark { display: none; } }
223
+
224
+ @media (max-width: 768px), (pointer: coarse) {
225
+ .gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
226
+ }
227
+ @media (prefers-reduced-motion: reduce) {
228
+ .gw-footer__fold > summary::after { transition: none; }
229
+ }
230
+ /* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
231
+ @media (forced-colors: active) {
232
+ .gw-footer__ext,
233
+ .gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
234
+ }