@gemboss/website-ui 0.12.0 → 0.13.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
@@ -17,6 +17,7 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
17
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
18
  | `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
19
19
  | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
20
+ | `templates/site/` | 0.13.0: what a new site owns from the start (the Next.js wrapper that serves its static pages, its page list, its public allow-list, `site.css`, `kit-gaps.css`): `npx gemboss-website init --site` writes it |
20
21
  | `demo/index.html` | every component on one page: open it to see what exists before you build anything |
21
22
  | `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
22
23
  | `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
@@ -60,6 +61,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
60
61
 
61
62
  ## 2. Build a page
62
63
 
64
+ **A new site** (0.13.0, the way gemboss.ai and gemians.com are built): `npx gemboss-website init --site` in an empty
65
+ folder writes the header and footer partials, `index.html` and `404.html`, `assets/site.css`, `assets/kit-gaps.css` and
66
+ the Next.js wrapper that serves the static pages, and adds the scripts to `package.json`. Then each page is a file with
67
+ `<!-- gw:header -->` and `<!-- gw:footer -->` markers, and `npx gemboss-website build` writes the partials into every
68
+ page (with `aria-current="page"` on its own links) and copies the kit into `assets/kit/`. Never hand-copy a header into
69
+ a page. A page of such a site starts from `index.html`, not from `templates/page.html`.
70
+
63
71
  1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
64
72
  header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
65
73
  **The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
@@ -225,7 +233,9 @@ A brand other than GemBoss chooses **only** these, and nothing else:
225
233
  Do not set `--gw-accent` itself: the kit points it at those per theme, and a direct override loses as soon as the
226
234
  theme toggle is pressed. Accent text uses `--gw-accent-strong`, so it must reach 4.5:1 on `--gw-bg` and
227
235
  `--gw-bg-soft`;
228
- - **the wash hues**, **the logo** in the header and footer, and **the favicon**;
236
+ - **the wash hues**, **the logo** in the header and footer (a mark beside the name, or one drawing of both:
237
+ `.gw-brand__logo`, with a second file for dark grounds when the brand has one), and **the favicon**;
238
+ - **the announcement bar's and the Stage's colours** (`--gw-announce-*`, `--gw-stage-*`), only when it shows them;
229
239
  - **three named profiles**, once for the site, never free values:
230
240
  - action colour: `data-gw-action="accent"` on `<html>` fills the primary button with the accent (default `ink`).
231
241
  Its words (`--gw-on-accent`) must reach 4.5:1 on `--gw-accent` and `--gw-accent-strong`, light and dark, or
@@ -242,7 +252,9 @@ examples: `demo/theme-example.html` (accent only) and `demo/brand-profiles.html`
242
252
  - Link the kit, then the site's own CSS after it: `website.css` → `site.css`.
243
253
  - The site's own CSS is for content the kit cannot know (an illustration, a product mock-up) and uses only the
244
254
  tokens. Its classes start with the site's prefix (for example `gp-` on GemPages), never `gw-`.
245
- - Shared header and footer markup lives in one place in the site (a partial or component), not copied per page.
255
+ - Shared header and footer markup lives in one place in the site (a partial or component), not copied per page:
256
+ `partials/header.html` and `partials/footer.html`, written into the pages by `npx gemboss-website build` (CI runs
257
+ `build --check`, which fails when a page is out of date).
246
258
 
247
259
  ## 9. When the kit is missing something
248
260
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,43 @@ 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.13.0 (2026-10-26)
8
+
9
+ What building a second site on the kit taught it. gemians.com was rebuilt on 0.12.0 from npm, kit sections only (Chris,
10
+ 26/10/2026: inherit from the GemBoss website kit, fix the kit where it gets in the way, then update Gemians and
11
+ GemBoss), and every place the kit fell short became one of these. A minor version: nothing a 0.12.0 site uses changes
12
+ its look, except a list right under a section's head (below), which now gets the head's spacing it was always meant to.
13
+
14
+ - **A new site in one command**: `npx gemboss-website init --site` writes what gemboss.ai and gemians.com both had to
15
+ write by hand: the header and footer partials (from the kit's templates), `index.html` and `404.html` with the
16
+ build's markers, `assets/site.css` (the brand's `:root` block, commented) and `assets/kit-gaps.css`, and the Next.js
17
+ wrapper that serves the static pages byte for byte (`app/[[...path]]/route.js`, `lib/pages.mjs`, `next.config.mjs`
18
+ with clean URLs, `stage-public.mjs` with its allow-list, `vercel.json`, `.gitignore`), from the new
19
+ `templates/site/`. It never overwrites a file and adds the scripts a site needs to `package.json`.
20
+ - **`npx gemboss-website build`** (new): copies the kit into `assets/kit/` as published, writes `partials/header.html`
21
+ and `partials/footer.html` into every page between `<!-- gw:header -->` and `<!-- gw:footer -->` (their leading
22
+ comments, the templates' notes, stay out), puts `aria-current="page"` on the header's links to that page, and the
23
+ kit's version on its two links (`?v=0.13.0`). `--check` writes nothing and fails when a page is out of date (CI).
24
+ One build for every site, so a fix reaches all of them with the kit, instead of a script per site that drifts.
25
+ - **A logo drawn as one file** (`.gw-brand__logo`, new): the mark and the wordmark together, 26px tall, in place of
26
+ `__mark` and `__name`, its `alt` the brand's name. A logo made for dark grounds (Gemians' white wordmark) comes with a
27
+ second file, `.gw-brand__logo--dark`, shown in dark only (the system's setting or the toggle). `check`:
28
+ `brand-markup`.
29
+ - **A photo beside words** (`.gw-figure`, new): a `<figure>` with its picture at the card radius and a 14px muted
30
+ caption 12px under it, in a split or under a hero's actions (48px below, as a window). Until now only the prose had
31
+ one; a split could hold a product window but not a photo of the team.
32
+ - **A list under a section's head gets the head's spacing**: `.gw-steps`, `.gw-stats`, `.gw-post-list` and
33
+ `.gw-testimonials` no longer reset their own margin, which beat 0.12.0's "what follows a head starts 40px below it
34
+ (48px from 768px)", so the steps sat right under the heading. The base still zeroes a list's margins, at no
35
+ specificity.
36
+ - **A brand's announcement bar and Stage**: `--gw-announce-*` and `--gw-stage-*` may be set in the brand's `:root` block
37
+ (`system.json` always said "a brand sets its own bar here", and `check` called it `token-redefined`). `check` computes
38
+ the bar's words and link on its ground and the Stage's words on its gradient (`brand-contrast`, 4.5:1).
39
+ - Docs: `docs/theming.md` asks 4.5:1 only of the accent's text form (`--gw-accent-strong-*`); the plain accent is never
40
+ text in the kit (icons, marks, the focus ring), so 3:1 is its bar (WCAG 1.4.11), and a gold brand keeps its gold. A
41
+ brand mark is 34×34 (the docs and `templates/page.html` still said 26). A contact page's form is its first screen
42
+ with the page's one `<h1>` (`templates/sections/form.html`). The demo's announcement pill says "New", in sentence case.
43
+
7
44
  ## 0.12.0 (2026-10-26)
8
45
 
9
46
  The grammar of every page, 1:1 with gemboss.ai: the announcement bar, the skip link, the button, the type (headings,
package/README.md CHANGED
@@ -19,7 +19,8 @@ it. Made so that a team working with Claude Code, even without a designer, ships
19
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
- | CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
22
+ | CLI `gemboss-website` | `init` (và `init --site`), `build`, `check`, `gap`, `upgrade` |
23
+ | `templates/site/` | 0.13.0: bộ khung một site mới (Next.js phục vụ trang HTML tĩnh, danh sách trang, danh sách tệp công khai, `site.css`, `kit-gaps.css`), `init --site` chép ra |
23
24
 
24
25
  Cần Node 18 trở lên cho CLI. CSS và JS chạy trên mọi trình duyệt hiện đại, không cần bước build.
25
26
 
@@ -40,9 +41,9 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
40
41
  **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
42
 
42
43
  ```html
43
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.12.0/website.min.css">
44
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.13.0/website.min.css">
44
45
  <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.12.0/js/website.js" defer></script>
46
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.13.0/js/website.js" defer></script>
46
47
  ```
47
48
 
48
49
  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,6 +114,8 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
113
114
  | lệnh | làm gì |
114
115
  |---|---|
115
116
  | `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
117
+ | `npx gemboss-website init --site` | 0.13.0: dựng một site mới như gemboss.ai và gemians.com: partial header và footer, `index.html` và `404.html` có dấu `<!-- gw:header -->` / `<!-- gw:footer -->`, `site.css`, `kit-gaps.css`, phần Next.js phục vụ trang tĩnh; không ghi đè tệp có sẵn, thêm script còn thiếu vào `package.json` |
118
+ | `npx gemboss-website build` | 0.13.0: chép kit vào `assets/kit/`, ghi partial header và footer vào mọi trang (đánh dấu `aria-current="page"` cho link của chính trang đó); `--check` cho CI, báo đỏ khi trang chưa cập nhật |
116
119
  | `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
120
  | `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
121
  | `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 |
@@ -8,10 +8,21 @@ export const USAGE = `gemboss-website (${PKG.name} ${PKG.version})
8
8
  Usage: npx gemboss-website <command> [options] (run it in the site's root folder)
9
9
 
10
10
  Commands
11
- init [--force]
11
+ init [--force] [--site]
12
12
  Add or refresh the kit's block in CLAUDE.md (between <!-- ${PKG.name}:start/end --> markers;
13
13
  text outside them is never touched) and install the Claude Code skill
14
14
  .claude/skills/gemboss-website-ui/SKILL.md (replaced only if init wrote it, or with --force).
15
+ --site also start a whole site, as gemboss.ai and gemians.com are built: the header and
16
+ footer partials, index.html and 404.html with their markers, assets/site.css and
17
+ kit-gaps.css, and the Next.js wrapper that serves the static pages (app/, lib/pages.mjs,
18
+ next.config.mjs, stage-public.mjs, vercel.json, .gitignore). Never overwrites a file;
19
+ adds the missing scripts to package.json.
20
+
21
+ build [--check]
22
+ Copy the kit into assets/kit/ and write partials/header.html and partials/footer.html into every
23
+ page between its <!-- gw:header --> / <!-- gw:footer --> markers, with aria-current="page" on
24
+ the header's links to that page. Pages: lib/pages.mjs pages() and 404.html (else every .html in
25
+ the root). --check writes nothing and exits 1 when a file is out of date (for CI).
15
26
 
16
27
  check [paths...] [--json] [--fix-list] [--ignore <path>]... [--include-kit]
17
28
  Check every .html/.htm/.css (and <style>, style="") under the paths (default: here) against
@@ -59,6 +70,7 @@ async function main(argv) {
59
70
  console.log(PKG.version);
60
71
  return 0;
61
72
  case "init": return (await import("./lib/init.mjs")).cmdInit(rest);
73
+ case "build": return (await import("./lib/build.mjs")).cmdBuild(rest);
62
74
  case "check": return (await import("./lib/check.mjs")).cmdCheck(rest);
63
75
  case "gap": return (await import("./lib/gap.mjs")).cmdGap(rest);
64
76
  case "upgrade": return (await import("./lib/upgrade.mjs")).cmdUpgrade(rest);
@@ -0,0 +1,101 @@
1
+ // `gemboss-website build [--check]`: the parts every page of a static site shares, written into each page in place, so
2
+ // the HTML on disk is the HTML served (0.13.0, from gemboss-www and gemians-www):
3
+ // 1. the kit itself: website.min.css and js/website.js → assets/kit/website.css and assets/kit/website.js, as published;
4
+ // 2. partials/header.html and partials/footer.html → between <!-- gw:header --> … <!-- /gw:header --> (and gw:footer)
5
+ // in every page, with aria-current="page" on the header's links to that page;
6
+ // 3. the kit's version on its two links (?v=0.13.0), so a new kit is never served from an old cache.
7
+ // The pages are the site's lib/pages.mjs (`pages()`: Map of URL → file, the same list its Next route serves) plus
8
+ // 404.html; without that file, every .html in the site's root folder.
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { PKG, PKG_DIR, UsageError, c, parseArgs } from "./util.mjs";
13
+
14
+ export const KIT_DIR = "assets/kit";
15
+ const CURRENT = /\b(?:gw-nav__link|gw-nav__trigger|gw-mobile-menu__top|gw-mobile-menu__link|gw-mega__link)\b/;
16
+
17
+ /** file → clean URL, as Vercel's cleanUrls and the site's Next route: index.html → /, a/index.html → /a, a.html → /a. */
18
+ export function routeOf(file) {
19
+ const stem = file.replace(/\.html$/, "");
20
+ if (stem === "index") return "/";
21
+ return "/" + stem.replace(/\/index$/, "");
22
+ }
23
+
24
+ /** The header for one page: aria-current="page" on each of its links (nav, mobile menu, mega rows) to that URL. */
25
+ export function markCurrent(html, route) {
26
+ return html.replace(/<a\b([^>]*)>/g, (tag, attrs) => {
27
+ const cls = (/\bclass\s*=\s*"([^"]*)"/.exec(attrs) || [])[1] || "";
28
+ const href = (/\bhref\s*=\s*"([^"]*)"/.exec(attrs) || [])[1];
29
+ if (!CURRENT.test(cls) || href !== route || /\baria-current\s*=/.test(attrs)) return tag;
30
+ return `<a${attrs} aria-current="page">`;
31
+ });
32
+ }
33
+
34
+ /** Put `body` between the page's <!-- gw:name --> markers; null when the page has none. */
35
+ export function fillRegion(html, name, body) {
36
+ const re = new RegExp(`(<!-- gw:${name} -->)[\\s\\S]*?(<!-- /gw:${name} -->)`);
37
+ return re.test(html) ? html.replace(re, (_, a, b) => `${a}\n${body}\n${b}`) : null;
38
+ }
39
+
40
+ async function sitePages(root) {
41
+ const lib = join(root, "lib", "pages.mjs");
42
+ let files;
43
+ if (existsSync(lib)) {
44
+ const mod = await import(pathToFileURL(lib).href);
45
+ if (typeof mod.pages !== "function") throw new UsageError("lib/pages.mjs must export pages() (a Map of URL → file)");
46
+ files = [...mod.pages().entries()].map(([route, file]) => ({ route, file }));
47
+ } else {
48
+ files = readdirSync(root).filter((f) => f.endsWith(".html") && f !== "404.html").map((file) => ({ route: routeOf(file), file }));
49
+ }
50
+ if (existsSync(join(root, "404.html"))) files.push({ route: "/404", file: "404.html" });
51
+ return files;
52
+ }
53
+
54
+ export async function cmdBuild(argv) {
55
+ const opts = parseArgs(argv, { check: "bool" });
56
+ if (opts._.length) throw new UsageError(`build takes no arguments (got ${opts._.join(" ")}); run it in the site's root folder`);
57
+ const root = process.cwd();
58
+ const stale = [], wrote = [];
59
+ const put = (file, text) => {
60
+ const abs = resolve(root, file);
61
+ if (existsSync(abs) && readFileSync(abs, "utf8") === text) return;
62
+ if (opts.check) { stale.push(file); return; }
63
+ mkdirSync(dirname(abs), { recursive: true });
64
+ writeFileSync(abs, text);
65
+ wrote.push(file);
66
+ };
67
+
68
+ put(`${KIT_DIR}/website.css`, `/* ${PKG.name} ${PKG.version}, copied by gemboss-website build: do not edit */\n` + readFileSync(join(PKG_DIR, "website.min.css"), "utf8"));
69
+ put(`${KIT_DIR}/website.js`, readFileSync(join(PKG_DIR, "js", "website.js"), "utf8"));
70
+
71
+ // a partial's leading comments are notes for its author (the kit templates' instructions), not for every page
72
+ const partial = (name) => { const p = join(root, "partials", `${name}.html`); return existsSync(p) ? readFileSync(p, "utf8").replace(/^(?:\s*<!--[\s\S]*?-->)+/, "").trim() : null; };
73
+ const header = partial("header"), footer = partial("footer");
74
+ const pages = await sitePages(root);
75
+ const problems = [];
76
+ for (const { route, file } of pages) {
77
+ const abs = join(root, file);
78
+ if (!existsSync(abs)) { problems.push(`${file}: listed by lib/pages.mjs, not found`); continue; }
79
+ let html = readFileSync(abs, "utf8");
80
+ for (const [name, body] of [["header", header && markCurrent(header, route)], ["footer", footer]]) {
81
+ if (!html.includes(`<!-- gw:${name} -->`)) continue;
82
+ if (body === null) { problems.push(`${file}: has <!-- gw:${name} --> but partials/${name}.html is missing`); continue; }
83
+ const next = fillRegion(html, name, body);
84
+ if (next === null) problems.push(`${file}: <!-- gw:${name} --> without its closing <!-- /gw:${name} -->`);
85
+ else html = next;
86
+ }
87
+ html = html.replace(/((?:href|src)\s*=\s*"\/assets\/kit\/website\.(?:css|js))(?:\?v=[^"]*)?"/g, `$1?v=${PKG.version}"`);
88
+ put(file, html);
89
+ }
90
+
91
+ for (const p of problems) console.error(` ${c.red(p)}`);
92
+ if (opts.check) {
93
+ for (const f of stale) console.error(` out of date: ${f}`);
94
+ const bad = stale.length + problems.length;
95
+ console.log(bad ? c.red(`build: RED, ${stale.length} file(s) out of date, ${problems.length} problem(s). Run: npx gemboss-website build`) : c.green(`build: green, ${pages.length} pages, kit ${PKG.version}`));
96
+ return bad ? 1 : 0;
97
+ }
98
+ for (const f of wrote) console.log(` wrote ${f}`);
99
+ console.log(`build: ${pages.length} pages, kit ${PKG.version}${wrote.length ? "" : ", nothing to write"}`);
100
+ return problems.length ? 1 : 0;
101
+ }
package/bin/lib/check.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { basename, resolve } from "node:path";
5
5
  import { PKG, SYSTEM, UsageError, blank, c, isKitPath, lineIndex, parseArgs, rel, walkFiles } from "./util.mjs";
6
- import { MIN_CONTRAST, actionPairs, ratio } from "./contrast.mjs";
6
+ import { MIN_CONTRAST, actionPairs, contrast, ratio } from "./contrast.mjs";
7
7
 
8
8
  const RADII = new Set(SYSTEM.radius.scale);
9
9
  // A brand's named profiles (system.json `profiles`): one attribute on <html> each, and the floating header's class.
@@ -31,7 +31,7 @@ export const RULES = {
31
31
  },
32
32
  "token-redefined": {
33
33
  level: "error", title: "Kit token redefined",
34
- why: "A brand chooses its accent family and wash hues (variables), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
34
+ why: "A brand chooses its accent family and wash hues (variables), the colours of its announcement bar and of its Stage (--gw-announce-*, --gw-stage-*), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
35
35
  fix: `Remove it. The action colour and the corners are profiles, not tokens: ${profileList()} on <html>; the floating header is the class ${FLOAT_CLASS} (docs/theming.md). If the brand needs something else, report a gap: npx gemboss-website gap --kind token.`,
36
36
  },
37
37
  "radius-off-scale": {
@@ -129,6 +129,16 @@ export const RULES = {
129
129
  why: `With data-gw-action="accent" the primary button is filled with the brand's accent. Its words (--gw-on-accent) must reach ${MIN_CONTRAST}:1 on --gw-accent and on --gw-accent-strong (hover and pressed), in light and in dark, or the main action of every page is hard to read.`,
130
130
  fix: "Darken --gw-accent-light and --gw-accent-strong-light (or change --gw-on-accent-light), lighten the -dark ones (dark words on them), until every pair passes; write them as #rrggbb. Or keep the ink button: remove data-gw-action.",
131
131
  },
132
+ "brand-contrast": {
133
+ level: "error", title: "Words on the brand's announcement bar or Stage below 4.5:1",
134
+ why: `A brand that sets its own announcement bar (--gw-announce-*) or Stage (--gw-stage-*) picks colours the kit cannot check for it: the bar's words and its link on its ground, and the Stage's words on both ends of its gradient, must reach ${MIN_CONTRAST}:1, or the sentence over every page and the mega menu's pictures are hard to read.`,
135
+ fix: "Darken the ground (--gw-announce-bg, --gw-stage-top, --gw-stage-bottom) or lighten the words (--gw-announce-ink, --gw-announce-link, --gw-stage-ink) until every pair passes; write them as #rrggbb.",
136
+ },
137
+ "brand-markup": {
138
+ level: "error", title: "Brand logo without its markup",
139
+ why: "A logo drawn as one file (.gw-brand__logo, the mark and the wordmark together) stands in for the brand's name: its alt is that name, or the home link has no name for a screen reader. Its width and height keep the bar from jumping while it loads. A file for dark grounds (.gw-brand__logo--dark) is the same logo in another colour and needs the first one right before it.",
140
+ fix: "Write <a class=\"gw-brand\" href=\"/\"><img class=\"gw-brand__logo\" src=\"/logo.png\" alt=\"Brand\" width=\"150\" height=\"26\"><img class=\"gw-brand__logo gw-brand__logo--dark\" src=\"/logo-on-dark.png\" alt=\"Brand\" width=\"150\" height=\"26\"></a> (docs/theming.md, the logo).",
141
+ },
132
142
  "toc-target": {
133
143
  level: "error", title: "Contents link to a heading that is not on the page",
134
144
  why: "A .gw-toc link whose #id names no element on the page jumps nowhere, and website.js cannot mark its section. It happens when a heading is renamed or removed and the list is not.",
@@ -339,7 +349,9 @@ const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
339
349
  // its pictures' width and its columns (0.11.0: style="--gw-mega-w: 1120px; --gw-mega-pv: 380px; --gw-mega-cols: 2"); the colour of
340
350
  // a label's square (0.12.0: --gw-label-mark, on the label or around it)
341
351
  const INPUTS = new Set(["--gw-stars", "--gw-mega-w", "--gw-mega-pv", "--gw-mega-cols", "--gw-label-mark"]);
342
- const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
352
+ // docs/theming.md: the eight accent variables (-light/-dark), the wash, and (0.13.0) the announcement bar's and the
353
+ // Stage's colours, which system.json always said a brand sets
354
+ const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?|announce-[\w-]+|stage-[\w-]+)$/;
343
355
 
344
356
  const COLOR_PROPS = /^(?:color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)?(?:-color)?|outline(?:-color)?|fill|stroke|text-decoration(?:-color)?|caret-color|accent-color|column-rule(?:-color)?|text-emphasis(?:-color)?|-webkit-text-fill-color|-webkit-text-stroke(?:-color)?|scrollbar-color|stop-color|flood-color|lighting-color)$/;
345
357
  const NAMED = new Set(("white black red green blue yellow orange purple pink gray grey silver gold navy teal maroon olive lime aqua " +
@@ -480,6 +492,28 @@ function brandAccent(cssRules, page) {
480
492
  return vals;
481
493
  }
482
494
 
495
+ /** The brand's announcement bar and Stage, when its CSS sets them: their words on their grounds (brand-contrast). */
496
+ function checkBrandContrast(out, add) {
497
+ const vals = {};
498
+ for (const { file, rules } of out.cssRules.filter((x) => !x.inline)) {
499
+ for (const r of rules) {
500
+ if (r.atBlock || r.at.length || !r.selector || !isRootish(r.selector)) continue;
501
+ for (const d of r.decls) if (/^--gw-(?:announce|stage)-/.test(d.prop)) vals[d.prop] = { value: d.value.replace(/!important\s*$/i, "").trim(), at: `${file}:${d.line}` };
502
+ }
503
+ }
504
+ const pairs = [["announce", "ink", "bg"], ["announce", "link", "bg"], ["stage", "ink", "top"], ["stage", "ink", "bottom"]];
505
+ for (const [part, text, ground] of pairs) {
506
+ const t = vals[`--gw-${part}-${text}`], g = vals[`--gw-${part}-${ground}`];
507
+ if (!t && !g) continue; // the kit's own pair, checked by the kit
508
+ const kit = (k) => SYSTEM.color[part][k];
509
+ const tv = t ? t.value : kit(text), gv = g ? g.value : kit(ground);
510
+ const r = contrast(tv, gv);
511
+ if (r !== null && r >= MIN_CONTRAST) continue;
512
+ const at = (t || g).at, [file, line] = [at.slice(0, at.lastIndexOf(":")), Number(at.slice(at.lastIndexOf(":") + 1))];
513
+ add("brand-contrast", { file, line, excerpt: `--gw-${part}-${text} ${tv} on --gw-${part}-${ground} ${gv} ${r === null ? "cannot be read as a colour (write #rrggbb)" : `is ${ratio(r)}, needs ${MIN_CONTRAST}:1`}` });
514
+ }
515
+ }
516
+
483
517
  /** Pages with data-gw-action="accent": the words of the primary button on its fills, light and dark (action-contrast). */
484
518
  function checkActionContrast(out, add) {
485
519
  for (const pg of out.pageProfiles.filter((p) => p.action === "accent")) {
@@ -659,6 +693,15 @@ function analyzeHtml(file, text, out) {
659
693
  ].filter(Boolean);
660
694
  if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
661
695
  }
696
+ if (tag === "img" && cls.includes("gw-brand__logo")) {
697
+ const before = scanText.slice(0, m.index).replace(/\s+$/, "");
698
+ const missing = [
699
+ !(a.alt || "").trim() && "alt with the brand's name (the logo stands in for it)",
700
+ !(a.width && a.height) && "width and height",
701
+ cls.includes("gw-brand__logo--dark") && !/<img\b[^>]*\bclass\s*=\s*["'][^"']*\bgw-brand__logo\b(?![\w-])[^"']*["'][^>]*>$/i.test(before) && "the light file right before it (<img class=\"gw-brand__logo\">)",
702
+ ].filter(Boolean);
703
+ if (missing.length) add("brand-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
704
+ }
662
705
  if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
663
706
  add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
664
707
  }
@@ -825,6 +868,7 @@ export function runCheck(paths, { ignore = [], includeKit = false } = {}) {
825
868
  }
826
869
 
827
870
  checkActionContrast(out, add);
871
+ checkBrandContrast(out, add);
828
872
 
829
873
  // Classes styled in site CSS: first definition of each.
830
874
  const styled = new Map();
package/bin/lib/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // `gemboss-website init`: connect the kit's rules to the project's Claude Code (CLAUDE.md block + skill).
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { PKG_DIR, PKG_NAME, UsageError, parseArgs, rel } from "./util.mjs";
5
5
 
@@ -59,8 +59,87 @@ export function upsertBlock(text, block) {
59
59
  return { text: text + sep + b + eol, action: "added" };
60
60
  }
61
61
 
62
+ // ------------------------------------------------------------------------------------------- init --site
63
+
64
+ /** The files a new site owns from the start, from templates/site/ (gitignore → .gitignore: npm drops dotfiles). */
65
+ function siteTemplateFiles(dir = join(PKG_DIR, "templates", "site"), base = "") {
66
+ const out = [];
67
+ for (const name of readdirSync(dir)) {
68
+ const abs = join(dir, name), r = base ? `${base}/${name}` : name;
69
+ if (statSync(abs).isDirectory()) out.push(...siteTemplateFiles(abs, r));
70
+ else out.push({ from: abs, to: r === "gitignore" ? ".gitignore" : r });
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * A page of the site from templates/page.html: the header and footer become the build's markers, the kit's paths the
77
+ * site's (assets/kit/), and kit-gaps.css follows site.css. `main` replaces the template's <main> when given.
78
+ */
79
+ export function sitePage(main) {
80
+ let html = readFileSync(join(PKG_DIR, "templates", "page.html"), "utf8");
81
+ const cut = (re, what) => { if (!re.test(html)) throw new Error(`templates/page.html: no ${what} to replace`); html = html.replace(re, ""); };
82
+ html = html.replace(/<!--\s*\n\s*Page skeleton:[\s\S]*?-->\n/, `<!-- A page of this site. Its header and footer are partials/header.html and partials/footer.html, written between the
83
+ markers by \`npx gemboss-website build\`. Compose <main> from node_modules/${PKG_NAME}/templates/sections/. -->\n`);
84
+ cut(/\n\s*<div class="gw-announce"[\s\S]*?<\/header>\n/, "header");
85
+ html = html.replace(/(<a class="gw-skip"[^\n]*\n)/, "$1 <!-- gw:header -->\n <!-- /gw:header -->\n\n");
86
+ cut(/\n\s*<!-- the short footer[^\n]*-->\s*<footer class="gw-footer">[\s\S]*?<\/footer>\n/, "footer");
87
+ html = html.replace(/(\s*<\/main>\n)/, "$1\n <!-- gw:footer -->\n <!-- /gw:footer -->\n");
88
+ html = html.split("[/kit]").join("/assets/kit").replace("[/assets]/site.css", "/assets/site.css")
89
+ .replace(/(<link rel="stylesheet" href="\/assets\/site\.css">)/, '$1\n <link rel="stylesheet" href="/assets/kit-gaps.css">')
90
+ .replace("[/favicon.svg]", "/favicon.svg");
91
+ if (main) html = html.replace(/<main id="main">[\s\S]*?<\/main>/, `<main id="main">\n${main}\n </main>`);
92
+ return html;
93
+ }
94
+
95
+ const NOT_FOUND_MAIN = ` <section class="gw-hero gw-hero--compact">
96
+ <div class="gw-wrap">
97
+ <p class="gw-label">404</p>
98
+ <h1><span class="gw-dim">This page is not here.</span> The rest of the site is.</h1>
99
+ <div class="gw-actions">
100
+ <a class="gw-btn" href="/">Go to the home page</a>
101
+ </div>
102
+ </div>
103
+ </section>`;
104
+
105
+ export const SITE_SCRIPTS = {
106
+ "build:site": "gemboss-website build",
107
+ build: "gemboss-website build && node stage-public.mjs && next build",
108
+ start: "next start",
109
+ check: "gemboss-website build --check && node stage-public.mjs --check && gemboss-website check . --ignore node_modules --ignore public",
110
+ };
111
+ export const SITE_DEPS = "next@16.4.2 react@19.2.8 react-dom@19.2.8";
112
+
113
+ function initSite(cwd, done) {
114
+ const write = (to, text) => {
115
+ const abs = resolve(cwd, to);
116
+ if (existsSync(abs)) { done.push([to, "kept (exists)"]); return; }
117
+ mkdirSync(dirname(abs), { recursive: true });
118
+ writeFileSync(abs, text);
119
+ done.push([to, "created"]);
120
+ };
121
+ for (const { from, to } of siteTemplateFiles()) write(to, readFileSync(from, "utf8"));
122
+ write("partials/header.html", readFileSync(join(PKG_DIR, "templates", "header.html"), "utf8"));
123
+ write("partials/footer.html", readFileSync(join(PKG_DIR, "templates", "footer.html"), "utf8"));
124
+ write("index.html", sitePage());
125
+ write("404.html", sitePage(NOT_FOUND_MAIN).replace("<title>[Page title] · [Brand]</title>", "<title>Page not found · [Brand]</title>"));
126
+
127
+ const pkgPath = resolve(cwd, "package.json");
128
+ const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")) : { name: "[site]-www", private: true, type: "module" };
129
+ pkg.scripts = pkg.scripts || {};
130
+ const added = Object.keys(SITE_SCRIPTS).filter((k) => !(k in pkg.scripts));
131
+ for (const k of added) pkg.scripts[k] = SITE_SCRIPTS[k];
132
+ const typed = pkg.type === "module";
133
+ if (!typed && !pkg.type) pkg.type = "module";
134
+ if (added.length || !existsSync(pkgPath) || !typed) {
135
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
136
+ done.push(["package.json", added.length ? `scripts added: ${added.join(", ")}` : "written"]);
137
+ } else done.push(["package.json", "scripts already there"]);
138
+ if (pkg.type !== "module") done.push(["package.json", `"type" is "${pkg.type}": the .mjs files work, next.config.mjs too`]);
139
+ }
140
+
62
141
  export function cmdInit(argv) {
63
- const opts = parseArgs(argv, { force: "bool" });
142
+ const opts = parseArgs(argv, { force: "bool", site: "bool" });
64
143
  if (opts._.length) throw new UsageError(`init takes no arguments (got ${opts._.join(" ")}); run it in the project's root folder`);
65
144
  const cwd = process.cwd();
66
145
  const done = [];
@@ -89,11 +168,22 @@ export function cmdInit(argv) {
89
168
  }
90
169
  }
91
170
 
171
+ if (opts.site) initSite(cwd, done);
172
+
92
173
  const w = Math.max(...done.map(([p]) => p.length));
93
174
  console.log(`gemboss-website init (${PKG_NAME})`);
94
175
  for (const [p, what] of done) console.log(` ${p.padEnd(w)} ${what}`);
95
176
  console.log("");
96
177
  console.log(`Claude Code now reads ${KIT}/AGENTS.md before website work in this project.`);
97
- console.log("Next: start a page from the kit's templates/page.html, then run: npx gemboss-website check");
178
+ if (opts.site) {
179
+ console.log("Next:");
180
+ console.log(` 1. npm install ${SITE_DEPS}`);
181
+ console.log(" 2. Fill partials/header.html and partials/footer.html once (delete what the site does not use),");
182
+ console.log(" the brand's :root block in assets/site.css, and add favicon.svg");
183
+ console.log(" 3. npm run build:site, then compose index.html from the kit's templates/sections/");
184
+ console.log(" 4. npm run check, and look at every page at 320px and 1366px");
185
+ } else {
186
+ console.log("Next: start a page from the kit's templates/page.html, then run: npx gemboss-website check");
187
+ }
98
188
  return 0;
99
189
  }
package/css/article.css CHANGED
@@ -203,7 +203,7 @@
203
203
  .gw-card__media first, a .gw-label for the category, the title (its link covers the card, so the card is one target
204
204
  and the link's name is the title alone), one or two lines of excerpt and a .gw-article-meta row at the foot. One
205
205
  column on phones, two from 768px, three from 1080px. --compact: one column of rows, no card and no picture. */
206
- .gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin: 0; padding: 0; list-style: none; }
206
+ .gw-post-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 0; list-style: none; }
207
207
  .gw-post-list > li { display: flex; min-width: 0; }
208
208
  .gw-post { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; }
209
209
  .gw-post:hover { border-color: var(--gw-line-strong); }
@@ -266,6 +266,13 @@
266
266
  }
267
267
  .gw-window__body:has(> img:only-child) + .gw-window__caption { padding-top: 12px; }
268
268
 
269
+ /* figure (0.13.0, Gemians): a photo and its caption where a section shows one outside the prose (beside words in a
270
+ split, under a hero's actions): the picture at the card radius, the caption 14px, muted, 12px under it, as a figure
271
+ in .gw-prose. A screen of the product goes in .gw-window instead. */
272
+ .gw-figure > :is(img, video, picture) { display: block; width: 100%; height: auto; border-radius: var(--gw-radius-card); }
273
+ .gw-figure > picture > img { display: block; width: 100%; height: auto; border-radius: inherit; }
274
+ .gw-figure > figcaption { margin-top: 12px; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
275
+
269
276
  /* icon: 20px inline stroke SVG (1.7 to 2px stroke, currentColor) */
270
277
  .gw-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 20px; height: 20px; }
271
278
  .gw-icon svg { width: 20px; height: 20px; }
package/css/header.css CHANGED
@@ -703,3 +703,16 @@ html.is-menu-open .gw-header:has(> .gw-mobile-menu.is-drill) { position: sticky;
703
703
  :root:not([data-theme="light"]) .gw-theme-toggle__sun { display: block; }
704
704
  :root:not([data-theme="light"]) .gw-theme-toggle__moon { display: none; }
705
705
  }
706
+
707
+ /* ---- 0.13.0: the brand's logo as one drawing (Gemians): the mark and the wordmark in one file, in place of the mark and
708
+ the name, 26px tall, its width the file's. Its alt is the brand's name. A logo drawn for dark grounds only (a white
709
+ wordmark) comes as a second file, .gw-brand__logo--dark, right after the first: it shows in dark (the system's or the
710
+ toggle's) and the first one hides, the toggle icons' rule. One file that reads on both: no --dark. */
711
+ .gw-brand__logo { display: block; flex: none; width: auto; height: 26px; }
712
+ .gw-brand__logo--dark { display: none; }
713
+ :root[data-theme="dark"] .gw-brand__logo--dark { display: block; }
714
+ :root[data-theme="dark"] .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
715
+ @media (prefers-color-scheme: dark) {
716
+ :root:not([data-theme="light"]) .gw-brand__logo--dark { display: block; }
717
+ :root:not([data-theme="light"]) .gw-brand__logo:has(+ .gw-brand__logo--dark) { display: none; }
718
+ }
package/css/ratings.css CHANGED
@@ -26,7 +26,6 @@
26
26
  display: grid;
27
27
  grid-template-columns: minmax(0, 1fr);
28
28
  gap: 16px;
29
- margin: 0;
30
29
  padding: 0;
31
30
  list-style: none;
32
31
  }
package/css/sections.css CHANGED
@@ -13,7 +13,7 @@
13
13
  radial-gradient(420px 320px at 52% 118%, color-mix(in srgb, var(--gw-wash-mint) 10%, transparent), transparent 62%);
14
14
  }
15
15
  .gw-hero h1 .gw-dim { display: block; }
16
- .gw-hero > .gw-wrap > .gw-window { margin-top: 48px; }
16
+ .gw-hero > .gw-wrap > :is(.gw-window, .gw-figure) { margin-top: 48px; }
17
17
  .gw-hero--center { text-align: center; }
18
18
  .gw-hero--center > .gw-wrap > * { margin-inline: auto; }
19
19
  .gw-hero--center .gw-actions { justify-content: center; }
@@ -46,7 +46,7 @@
46
46
  .gw-logos img { width: auto; height: 28px; }
47
47
 
48
48
  /* stats: three or four facts, each with its source */
49
- .gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; margin: 0; padding: 0; list-style: none; }
49
+ .gw-stats { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; padding: 0; list-style: none; }
50
50
  .gw-stats__value { font-size: var(--gw-display-h2); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
51
51
  .gw-stats__label { margin-top: 8px; max-width: 28ch; color: var(--gw-muted); font-size: 14px; }
52
52
  .gw-stats__source { margin-top: 8px; color: var(--gw-muted); font-size: 12px; }
@@ -79,7 +79,6 @@
79
79
  display: grid;
80
80
  grid-template-columns: minmax(0, 1fr);
81
81
  gap: 32px 24px;
82
- margin: 0;
83
82
  padding: 0;
84
83
  list-style: none;
85
84
  counter-reset: gw-step;
package/demo/header.html CHANGED
@@ -44,7 +44,7 @@
44
44
  <body class="gw-page">
45
45
  <a class="gw-skip" href="#main">Skip to content</a>
46
46
 
47
- <div class="gw-announce" role="region" aria-label="Announcement"><span class="gw-announce__tag">NEW</span> The GemBoss community is live. <a href="https://community.gemboss.ai">Join founders building brand-true stores &rarr;</a></div>
47
+ <div class="gw-announce" role="region" aria-label="Announcement"><span class="gw-announce__tag">New</span> The GemBoss community is live. <a href="https://community.gemboss.ai">Join founders building brand-true stores &rarr;</a></div>
48
48
  <header class="gw-header" data-gw-header>
49
49
  <div class="gw-header__bar">
50
50
  <a class="gw-brand" href="https://gemboss.ai/" data-gw-show="brand:gemboss"><svg class="gw-brand__mark" viewBox="0 0 100 116" aria-hidden="true"><polygon points="26,14 74,14 66,50 34,50" style="fill:var(--gw-accent)"/><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="9" stroke-linejoin="round"/><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round"/></svg><span class="gw-brand__name">GemBoss</span></a><a class="gw-brand" href="header.html#gempages" data-gw-show="brand:gempages" hidden><svg class="gw-brand__mark" viewBox="0 0 26 26" aria-hidden="true"><rect width="26" height="26" rx="7" style="fill:var(--gw-accent)"/><path d="M8 9h10M8 13h10M8 17h6" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/></svg><span class="gw-brand__name">Example brand</span></a>
@@ -274,6 +274,24 @@ the mock-up shows a demo brand ("Vital Glow is a GemBoss demo brand."). Under re
274
274
  Don't: put interactive controls inside a mock-up; show made-up numbers without the caption; write the caption in
275
275
  other words ("Sample", "Demo numbers") or leave it in English on a translated page.
276
276
 
277
+ ### Figure `.gw-figure`
278
+
279
+ 0.13.0 (Gemians): a photo and its caption outside the prose, where a section shows one: beside the words of a split,
280
+ under a hero's actions (48px below, as a window). The picture takes the card radius; the `<figcaption>` is 14px,
281
+ muted, 12px under it, the look of a figure inside `.gw-prose`. A screen of the product goes in `.gw-window`; a photo
282
+ of people, a place or a product goes here.
283
+
284
+ ```html
285
+ <figure class="gw-figure">
286
+ <img src="/img/team-2025.webp" alt="The team together in the office, 2025" width="1000" height="471" loading="lazy">
287
+ <figcaption>The GemCommerce team, 2025.</figcaption>
288
+ </figure>
289
+ ```
290
+
291
+ Don't: a photo of people without their permission, or of another brand without its; a caption that repeats the alt
292
+ word for word (the alt says what is in the picture, the caption says what it is); a frame, a shadow or a radius of
293
+ your own.
294
+
277
295
  ### Icon `.gw-icon`
278
296
 
279
297
  A 20px box for an inline stroke SVG (1.7–2px stroke, `currentColor`, `aria-hidden="true"`). Inside `.gw-feature` it
@@ -341,6 +359,7 @@ level. A brand changes what it fills in and the tokens (colours, the Stage, the
341
359
  | `.gw-header--float` | with `.gw-header`, a brand's header profile: from 1081px the same bar as an inset card (panel radius, the surface at 85% over a blur, one hairline, the raised shadow, 16px from the top); the flat bar up to 1080px; solid under `prefers-reduced-transparency` |
342
360
  | `.gw-header__bar` | the row inside: the page's wrap + 12px (1132px on GemBoss), 24px inside (6px up to 640px), 32px between its parts |
343
361
  | `.gw-brand`, `__mark`, `__name` | logo (34px, its corners clipped at 8px) and name, linking home; a 44px row up to 768px |
362
+ | `.gw-brand__logo`, `--dark` | 0.13.0: a brand's logo drawn as one file (mark and wordmark), 26px tall, in place of `__mark` and `__name`, `alt` the brand's name; `--dark` a second file shown in dark only, right after the first (docs/theming.md) |
344
363
  | `.gw-nav`, `__list`, `__item`, `__trigger`, `__chev`, `__link` | the desktop nav (from 1081px), 14px weight 500, 0.005em tighter; `__trigger` is a section's link that opens its mega panel (its 12px `__chev` turns while open), `__link` a plain link; the current page (`aria-current="page"`) or section (`aria-current` on a trigger) in the accent's strong ink, weight 600 |
345
364
  | `.gw-mega` (`--gw-mega-w`, `--gw-mega-pv`, `--gw-mega-cols`), `__body`, `__grid`, `__col`, `__heading`, `__link`, `__aside`, `__card`, `__art`, `__card-text`, `__go`, `__foot`, `__foot-start`, `__foot-end`, `__cta`, `__new` | 0.11.0, gemboss.ai's mega menu: a panel on one surface that morphs between panels (the popover radius, raised shadow), a caret at the open name, a pill under the hovered name, a highlight under the hovered row; columns of rows (a name and one line), the pictures beside them (`__aside`: the panel's own `__card`, a row's preview from its `<template>`), each picture on the Stage (`__art`: the deep iris ground, `--gw-stage-*`); an optional strip under it. The page behind dims and blurs (`.gw-scrim`). Script: `docs/markup-contract.md` |
346
365
  | `.gw-header__actions` | Sign in, a second text action, Start free, the menu button (4px apart; the button 8px further) |