@gemboss/website-ui 0.3.0 → 0.4.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,7 +10,8 @@ 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; no dependencies |
13
+ | `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons; no dependencies; `window.gwWebsite.init(root)` |
14
+ | `js/website.mjs` | the same script as an ES module for apps: `import { init, destroy } from '@gemboss/website-ui/website.mjs'` |
14
15
  | `system.json` | every value of the system: colours, spacing, type, radii, shadows, widths |
15
16
  | `templates/page.html` | the page skeleton: start every new page from it |
16
17
  | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose… |
@@ -119,6 +120,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
119
120
  - Don't ship a page you have not seen at 320px.
120
121
  - Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
121
122
  that is a gap (section 9).
123
+ - Don't wire kit components with your own JS (no click handler on a trigger, no class toggled by hand). After you
124
+ insert kit markup (a route change, a block rendered later), call `gwWebsite.init(root)` on it, or `init(root)` from
125
+ `website.mjs`.
122
126
 
123
127
  ## 5. Accessibility
124
128
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ 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.4.0 (2026-10-25)
8
+
9
+ The website script, standardised like the CSS: it now wires markup that arrives after the page loaded (a Next.js
10
+ route change, a GemPages block rendered later, a CMS preview), and comes as an ES module too. A page that loads
11
+ `js/website.js` with `defer` and changes nothing works exactly as in 0.3.0 (the demo is pixel-identical at 390 and
12
+ 1366px, light and dark).
13
+
14
+ Closes #64
15
+
16
+ - `gwWebsite.init(root = document)`: wires the kit's markup under `root`, `root` included. Idempotent: each element
17
+ is wired once (and marked `data-gw-ready`), so call it after every navigation or insert and only the new elements
18
+ are wired; on a fragment it wires that fragment only. The page's own listeners (Escape, the outside click, the
19
+ busy-button guard, the system theme, the scroll) are installed once. Each call lets go of elements removed from
20
+ the page meanwhile and moves the script's `aria-current` mark to the new path (a mark in the markup stays).
21
+ - `gwWebsite.destroy(root = document)`: removes what `init` wired under `root`, its listeners and its marks, for
22
+ frameworks that unmount. An open mega or mobile menu closes first, so the page is never left locked.
23
+ - ESM entry `@gemboss/website-ui/website.mjs` (`js/website.mjs`): `export { init, destroy, version }`, no side
24
+ effects on import (safe to import on a server), no auto-init. `js/website.js` keeps its path, its export
25
+ `./website.js` and its behaviour: it starts by itself on `DOMContentLoaded` and exposes `window.gwWebsite`.
26
+ - One source: `scripts/build.mjs` writes `js/website.js` from `js/website.mjs` (the code in an IIFE, without the
27
+ comments), and `--check` fails when they differ or when the version is not `package.json`'s.
28
+ - One copy per page: the first copy that runs owns `window.gwWebsite`; another one (the classic script next to a
29
+ bundle) hands its calls to it, so nothing is wired twice.
30
+ - Events, all bubbling, listed in `docs/markup-contract.md`: `gw:mega` on the trigger (`{ id, open }`) and
31
+ `gw:theme` on `<html>` (`{ theme: 'light' | 'dark' }`), next to `gw:segmented`.
32
+ - Budget: `js/website.js` is 11.3 KB unminified, 3.8 KB gzipped (0.3.0: 9.2 KB, 3.3 KB). The test now holds it under
33
+ 12 KB unminified and 4.5 KB gzipped (was 10 KB unminified): the comments live in `website.mjs`, the classic file
34
+ carries none.
35
+ - `AGENTS.md`, the skill and `docs/components.md`: never wire a kit part with your own JS; call `init(root)` after
36
+ inserting kit markup. README: the ESM import and the Next.js recipe (a client effect on each route change).
37
+
7
38
  ## 0.3.0 (2026-10-25)
8
39
 
9
40
  Brand profiles: three knobs so that another GemCommerce brand (GemPages first, Gemians later) can use the kit and
package/README.md CHANGED
@@ -13,7 +13,7 @@ it. Made so that a team working with Claude Code, even without a designer, ships
13
13
  | | |
14
14
  |---|---|
15
15
  | `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 52 KB, 40 KB bản min): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn và đang chờ), link, nhãn, chip, segmented control, badge, card, khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), header + mega menu + menu điện thoại, footer, 12 section, 3 hồ sơ thương hiệu |
16
- | `js/website.js` | 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ờ; không phụ thuộc thư viện nào, khoảng 9 KB |
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ờ; không phụ thuộc thư viện nào, khoảng 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
18
  | `templates/` | khung một trang và 12 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose) |
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ơ |
@@ -40,33 +40,55 @@ 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.3.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.4.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.3.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.4.0/js/website.js" defer></script>
46
46
  ```
47
47
 
48
- **Next.js (App Router):** nạp CSS trong layout gốc, nạp script bằng `next/script`.
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
49
+ sau đó. Đừng tự viết JS cho mega menu, nút đổi giao diện hay segmented control.
50
+
51
+ **Next.js (App Router):** nạp CSS trong layout gốc; script là bản ES module, gọi `init()` trong một effect mỗi lần
52
+ đổi trang (chuyển trang phía client không tải lại trang, nên phần markup mới phải được gắn lại).
53
+
54
+ ```js
55
+ import { init, destroy } from "@gemboss/website-ui/website.mjs";
56
+ ```
57
+
58
+ ```js
59
+ // app/kit-script.jsx
60
+ "use client";
61
+ import { useEffect } from "react";
62
+ import { usePathname } from "next/navigation";
63
+ import { init } from "@gemboss/website-ui/website.mjs";
64
+
65
+ export default function KitScript() {
66
+ const pathname = usePathname();
67
+ useEffect(() => { init(); }, [pathname]);
68
+ return null;
69
+ }
70
+ ```
49
71
 
50
72
  ```js
51
73
  // app/layout.js
52
74
  import "@gemboss/website-ui/website.css";
53
75
  import "./site.css";
54
- import Script from "next/script";
76
+ import KitScript from "./kit-script";
55
77
 
56
78
  export default function RootLayout({ children }) {
57
79
  return (
58
80
  <html lang="vi">
59
81
  <body className="gw-page">
60
82
  {children}
61
- <Script src="/kit/website.js" strategy="afterInteractive" />
83
+ <KitScript />
62
84
  </body>
63
85
  </html>
64
86
  );
65
87
  }
66
88
  ```
67
89
 
68
- Chép `node_modules/@gemboss/website-ui/js/website.js` vào `public/kit/` (hoặc phục vụ nó bằng một route). Font Inter
69
- nạp từ Google Fonts như trong `templates/page.html`.
90
+ `init()` gọi lại bao nhiêu lần cũng được: phần đã gắn thì bỏ qua, chỉ gắn phần mới. Font Inter nạp từ Google Fonts
91
+ như trong `templates/page.html`.
70
92
 
71
93
  **Bắt đầu một trang:** chép `templates/page.html`, điền header và footer một lần (chung cho mọi trang), ghép các
72
94
  section trong `templates/sections/` theo thứ tự trang cần, thay mọi chỗ `[trong ngoặc]`, xoá phần không dùng. Mở
@@ -117,7 +139,7 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
117
139
 
118
140
  ```sh
119
141
  cd packages/website-ui
120
- node scripts/build.mjs # sinh css/tokens.css từ system.json, ghép website.css và website.min.css
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
121
143
  node test/run.mjs # tự kiểm CLI
122
144
  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)
123
145
  ```
@@ -32,6 +32,30 @@ Spacing between the children of text blocks is automatic: inside `.gw-section__h
32
32
 
33
33
  Don't remove the skip link, and don't leave out `id="main"`.
34
34
 
35
+ ## Script
36
+
37
+ `js/website.js` (a `<script defer>`) or `js/website.mjs` (an `import`) wires every part that moves: the mega menu,
38
+ the mobile menu, the theme toggle, reveal, the header's scroll state, the current page, the segmented control and
39
+ the busy button. Markup that arrives after the page loaded (a client-side route change, a builder block, a CMS
40
+ preview) is wired by calling `init` on it, as often as needed: an element is wired once.
41
+
42
+ ```js
43
+ // a page with the classic script, after it inserted kit markup
44
+ slot.innerHTML = html;
45
+ gwWebsite.init(slot);
46
+ ```
47
+
48
+ ```js
49
+ // an app: import the ESM entry, call init() in an effect after each route change
50
+ import { init } from '@gemboss/website-ui/website.mjs';
51
+ ```
52
+
53
+ `destroy(root)` takes it back when a framework unmounts. The Next.js recipe, the `data-gw-ready` mark and the
54
+ events (`gw:mega`, `gw:theme`, `gw:segmented`) are in `docs/markup-contract.md`, "Script".
55
+
56
+ Don't: wire a kit part with your own script (a click handler on a trigger, a class toggled by hand), or load two
57
+ copies of the script and expect both to work (the first owns the page).
58
+
35
59
  ## Type and text
36
60
 
37
61
  | class | for |
@@ -403,6 +427,12 @@ them in order); a page without `.gw-prose` that styles its articles in site CSS
403
427
  Put it on a block (a grid, a list, a window), not on each card in a grid. Don't use it on the hero or anything in
404
428
  the first screen, and don't animate anything but `transform` and `opacity`.
405
429
 
430
+ ## Additions to the markup contract in 0.4.0
431
+
432
+ The script: `init(root)` and `destroy(root)` (`window.gwWebsite`, or `js/website.mjs`), the `data-gw-ready` mark on
433
+ what it wired, and the events `gw:mega` (`{ id, open }`) and `gw:theme` (`{ theme }`) next to `gw:segmented`. No
434
+ markup or class of 0.3.0 changes.
435
+
406
436
  ## Additions to the markup contract in 0.3.0
407
437
 
408
438
  The brand profiles: `data-gw-action="ink|accent"` and `data-gw-radius="tight|soft"` on `<html>`, and
@@ -3,8 +3,8 @@
3
3
  The full class list, including the CSS-only helpers added while building 0.1.0 (`.gw-h1`–`.gw-h4`, `.gw-lead`,
4
4
  `.gw-note`, `.gw-actions`, `.gw-stats__source`, `.gw-plan__price`, the mobile menu parts…), is `docs/components.md`.
5
5
 
6
- The CSS (`css/*.css`), the script (`js/website.js`) and the templates (`templates/`) agree on exactly this. Change it
7
- here first, then in all three.
6
+ The CSS (`css/*.css`), the script (`js/website.mjs`, and `js/website.js` built from it) and the templates
7
+ (`templates/`) agree on exactly this. Change it here first, then in all three.
8
8
 
9
9
  Prefixes: kit classes `gw-`, kit CSS variables `--gw-`, state classes `is-` (set by the script), hooks `data-gw-*`.
10
10
  BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
@@ -21,7 +21,81 @@ BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
21
21
  <header class="gw-header" data-gw-header> … </header>
22
22
  <main id="main"> sections </main>
23
23
  <footer class="gw-footer"> … </footer>
24
- <script src="…/website.js" defer></script>
24
+ <script src="…/website.js" defer></script> <!-- or the ESM entry, see "Script" below -->
25
+ ```
26
+
27
+ ## Script (0.4.0)
28
+
29
+ One script wires every part below: the mega menu, the mobile menu, the theme toggle, reveal, the header's scroll
30
+ state, the current page, the segmented control and the busy button. It comes in two entries built from one source
31
+ (`js/website.mjs`; `scripts/build.mjs` writes `js/website.js` from it and `--check` fails when they differ):
32
+
33
+ | entry | file | how it starts |
34
+ |---|---|---|
35
+ | classic | `js/website.js` (`@gemboss/website-ui/website.js`) | `<script src="…/website.js" defer>`: wires the page on `DOMContentLoaded` by itself, then `window.gwWebsite` |
36
+ | ESM | `js/website.mjs` (`@gemboss/website-ui/website.mjs`) | `import { init, destroy, version } from '@gemboss/website-ui/website.mjs'`: importing does nothing (safe on a server); the app calls `init()` |
37
+
38
+ - **`init(root = document)`** wires the kit markup under `root`, `root` included. It is idempotent: an element is
39
+ wired once, so call it again after every client-side navigation or insert, and only the new elements are wired.
40
+ Called on a fragment, it wires that fragment and nothing else. It also lets go of elements removed from the page
41
+ since the last call, and moves the script's own `aria-current` mark to the new path. The listeners that belong
42
+ to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
43
+ - **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
44
+ mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
45
+ - **`version`**: `'0.4.0'`. The classic script exposes the three as `window.gwWebsite`.
46
+ - **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `theme`, `reveal`, `header`,
47
+ `segmented`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
48
+ is still wired.
49
+ - **One copy per page.** The first copy that runs owns the page (`window.gwWebsite`); another one (the classic
50
+ script next to a bundle, two bundles) hands its calls to it, so nothing is wired twice.
51
+ - Never wire a kit part with the site's own script (a click handler on `.gw-nav__trigger`, a class toggled by hand):
52
+ insert the kit's markup, then call `init` on it.
53
+
54
+ ### After inserting markup
55
+
56
+ A Next.js app (App Router): one client component in the root layout, an effect on each route change. Effects run
57
+ after hydration, so the server's markup is never changed before React reads it.
58
+
59
+ ```jsx
60
+ // app/kit-script.jsx
61
+ 'use client';
62
+ import { useEffect } from 'react';
63
+ import { usePathname } from 'next/navigation';
64
+ import { init } from '@gemboss/website-ui/website.mjs';
65
+
66
+ export default function KitScript() {
67
+ const pathname = usePathname();
68
+ useEffect(() => { init(); }, [pathname]);
69
+ return null;
70
+ }
71
+ // app/layout.js: <body className="gw-page">{children}<KitScript /></body>
72
+ ```
73
+
74
+ A component that mounts and unmounts kit markup on its own (a dialog, a part that streams in later):
75
+ `useEffect(() => { const el = ref.current; init(el); return () => destroy(el); }, [])`, on a wrapper with
76
+ `style={{ display: 'contents' }}` so it does not change the layout.
77
+
78
+ A page that inserts HTML (a GemPages block rendered after load, a CMS preview), with the classic script:
79
+
80
+ ```js
81
+ var slot = document.getElementById('pricing-slot');
82
+ slot.innerHTML = html; // kit markup: a header, a segmented control, sections
83
+ window.gwWebsite.init(slot); // wires what is new in it; the rest of the page is left alone
84
+ ```
85
+
86
+ ### Events
87
+
88
+ The script tells the page's own code what changed. Every event is a `CustomEvent` that bubbles, so one listener on
89
+ `document` hears them all; none is fired by `init`.
90
+
91
+ | event | target | `detail` | fires when |
92
+ |---|---|---|---|
93
+ | `gw:mega` | the trigger (`[data-gw-mega]`) | `{ id, open }`: the panel's `id`, `true` when it opened | a panel opens or closes: click, hover, Escape, a click outside, focus leaving, a link inside, `destroy` of an open one. Moving to another menu fires a close, then an open |
94
+ | `gw:theme` | `<html>` | `{ theme: 'light' \| 'dark' }`: the theme the page now shows | a press on a theme toggle; also a change of the system's scheme while the page follows it (no `data-theme`) |
95
+ | `gw:segmented` | the group (`[data-gw-segmented]`) | `{ name, value }`: the group's name, the chosen option's `value` | the reader chooses an option (click, arrows, Home, End) |
96
+
97
+ ```js
98
+ document.addEventListener('gw:theme', function (e) { chart.setTheme(e.detail.theme); });
25
99
  ```
26
100
 
27
101
  ## Header
@@ -61,24 +135,27 @@ BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
61
135
  Script behaviour:
62
136
  - **Mega menu**: click on a trigger toggles its panel (`hidden` removed, `aria-expanded="true"`, `.is-open` on the
63
137
  `.gw-nav__item`). Hover opens on fine pointers after 80ms and closes 160ms after leaving item and panel. Only one
64
- open at a time. `Escape` closes and returns focus to the trigger. A click outside closes. Arrow keys are not required.
138
+ open at a time on the page, across every header `init` wired. `Escape` closes and returns focus to the trigger. A
139
+ click outside closes. Arrow keys are not required. Each change fires `gw:mega`.
65
140
  - **Mobile menu** (below 1080px the `.gw-nav` and sign-in hide, `.gw-menu-toggle` shows): the toggle flips `hidden`
66
- on `#gw-mobile-menu`, `aria-expanded`, `.is-menu-open` on `<html>` (the CSS locks scroll), and its `aria-label`
67
- (Open menu / Close menu). `Escape` closes. Resizing above 1080px closes it.
141
+ on the menu its `aria-controls` names (`#gw-mobile-menu`), `aria-expanded`, `.is-menu-open` on `<html>` (the CSS
142
+ locks scroll), and its `aria-label` (Open menu / Close menu). `Escape` closes. Resizing above 1080px closes it.
68
143
  - **Labels in other languages**: the script never writes English over a translated page. Give the toggles their
69
144
  words with `data-gw-label-open` / `data-gw-label-close` (menu toggle) and `data-gw-label-dark` /
70
145
  `data-gw-label-light` (theme toggle); without them it keeps the markup's own `aria-label`, then falls back to English.
71
146
  - **Theme toggle**: cycles light ↔ dark by setting `data-theme` on `<html>`, stores the choice in `localStorage`
72
- (`gw-theme`, every access in try/catch), updates `aria-label`. On load, a stored choice is applied before paint by a
73
- one-line inline script in `<head>` that the template carries (`templates/page.html`).
74
- - **Reveal**: elements with `.gw-reveal` get `.is-in` when 15% visible (IntersectionObserver, once). Failsafe: after
75
- 2.5s, or if IntersectionObserver is missing, all get `.is-in`. Under `prefers-reduced-motion` the CSS shows them
76
- without motion.
147
+ (`gw-theme`, every access in try/catch), updates `aria-label` on every toggle, fires `gw:theme`. On load, a stored
148
+ choice is applied before paint by a one-line inline script in `<head>` that the template carries
149
+ (`templates/page.html`).
150
+ - **Reveal**: elements with `.gw-reveal` get `.is-in` when 15% visible (IntersectionObserver, once). Failsafe: 2.5s
151
+ after the `init` that found them, or at once if IntersectionObserver is missing, they get `.is-in`. Under
152
+ `prefers-reduced-motion` the CSS shows them without motion.
77
153
  - **Header**: `.is-scrolled` on `[data-gw-header]` once the page scrolled more than 8px (the CSS may use it for the
78
154
  hairline only; the bar does not change height or colour). The floating header needs nothing more from the script:
79
155
  it is the same element with one more class.
80
- - **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` whose `href`
81
- pathname equals `location.pathname` (when the markup did not already).
156
+ - **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` and the mobile
157
+ menu's links whose `href` pathname equals `location.pathname` (when the markup did not already). Each `init` takes
158
+ its own marks back first, so after a client-side navigation they follow the new path; a mark in the markup stays.
82
159
 
83
160
  ## Brand profiles (0.3.0)
84
161
 
package/js/website.js CHANGED
@@ -1,186 +1,240 @@
1
- /*! @gemboss/website-ui website.js | spec: docs/markup-contract.md | load with defer */
1
+ /*! @gemboss/website-ui 0.4.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
2
2
  (function () {
3
- 'use strict';
4
- if (window.gwWebsite) return;
5
- window.gwWebsite = { version: '0.3.0' };
6
- var doc = document, root = doc.documentElement;
7
- function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
8
- function watch(m, fn) { if (m.addEventListener) m.addEventListener('change', fn); }
9
- function all(sel, ctx) { return [].slice.call((ctx || doc).querySelectorAll(sel)); }
10
- function on(el, type, fn, opt) { el.addEventListener(type, fn, opt || false); }
11
- function attr(el, name, dflt) { return el.getAttribute(name) || dflt; }
12
- function esc(e) { return e.key === 'Escape'; }
13
- function link(e) { return e.target.closest('a[href]'); }
3
+ 'use strict';
4
+ if (window.gwWebsite) return;
14
5
 
15
- // Mega menu. Opened by hover then clicked, a panel stays open: the click meant "open".
16
- function mega() {
17
- var fine = mq('(hover: hover) and (pointer: fine)'), open = null, timer = 0;
18
- var menus = all('[data-gw-mega]').map(function (t) {
19
- var p = doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
20
- return p && { t: t, p: p, item: t.closest('.gw-nav__item') || t.parentNode };
21
- }).filter(Boolean);
22
- if (!menus.length) return;
23
- function set(m, show, mode) {
24
- m.p.hidden = !show;
25
- m.t.setAttribute('aria-expanded', String(show));
26
- m.item.classList.toggle('is-open', show);
27
- m.mode = mode;
28
- open = show ? m : open === m ? null : open;
29
- }
30
- function show(m, mode) {
31
- clearTimeout(timer);
32
- if (open === m) { if (mode === 'click') m.mode = mode; return; }
33
- if (open) set(open, false);
34
- set(m, true, mode);
35
- }
36
- function hide(focus) {
37
- var m = open;
38
- clearTimeout(timer);
39
- if (!m) return;
40
- set(m, false);
41
- if (focus === true) m.t.focus();
42
- }
43
- function inside(m, el) { return !!el && (m.item.contains(el) || m.p.contains(el)); }
44
- menus.forEach(function (m) {
45
- on(m.t, 'click', function () { if (open === m && m.mode !== 'hover') hide(); else show(m, 'click'); });
46
- (m.item.contains(m.p) ? [m.item] : [m.item, m.p]).forEach(function (el) {
47
- on(el, 'mouseenter', function () {
48
- if (fine.matches) { clearTimeout(timer); if (open !== m) timer = setTimeout(function () { show(m, 'hover'); }, 80); }
49
- });
50
- on(el, 'mouseleave', function () {
51
- if (fine.matches) { clearTimeout(timer); if (open === m && m.mode === 'hover') timer = setTimeout(hide, 160); }
52
- });
6
+ var version = '0.4.0';
7
+ var api = { version: version, init: init, destroy: destroy };
8
+ var doc, root, mine, fine, sys, live = [], megaOpen = null, megaTimer = 0, queued = false;
9
+ var READY = 'data-gw-ready';
10
+
11
+ function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
12
+ function watch(m, fn) { if (m.addEventListener) m.addEventListener('change', fn); }
13
+ function all(sel, ctx) { return [].slice.call((ctx || doc).querySelectorAll(sel)); }
14
+ function within(sel, ctx) { var l = all(sel, ctx); if (ctx.matches && ctx.matches(sel)) l.unshift(ctx); return l; }
15
+ function on(el, type, fn, opt) { el.addEventListener(type, fn, opt || false); }
16
+ function attr(el, name, dflt) { return el.getAttribute(name) || dflt; }
17
+ function esc(e) { return e.key === 'Escape'; }
18
+ function link(e) { return e.target.closest('a[href]'); }
19
+ function emit(el, type, detail) { if (window.CustomEvent) el.dispatchEvent(new CustomEvent(type, { bubbles: true, detail: detail })); }
20
+
21
+ function each(ctx, sel, kind, fn) {
22
+ within(sel, ctx).forEach(function (el) {
23
+ if (live.some(function (r) { return r.el === el && r.kind === kind; })) return;
24
+ var offs = [], r = fn(el, function (t, type, f, opt) {
25
+ on(t, type, f, opt);
26
+ offs.push(function () { t.removeEventListener(type, f, opt || false); });
27
+ });
28
+ if (!r) return offs.forEach(function (f) { f(); });
29
+ r.el = el; r.kind = kind; r.offs = offs;
30
+ mark(el, kind, true);
31
+ live.push(r);
32
+ });
33
+ }
34
+ function mark(el, kind, add) {
35
+ var l = attr(el, READY, '').split(' ').filter(function (k) { return k && k !== kind; });
36
+ if (add) l.push(kind);
37
+ if (l.length) el.setAttribute(READY, l.join(' ')); else el.removeAttribute(READY);
38
+ }
39
+ function drop(r) {
40
+ if (r.stop) r.stop();
41
+ r.offs.forEach(function (f) { f(); });
42
+ mark(r.el, r.kind);
43
+ live.splice(live.indexOf(r), 1);
44
+ }
45
+ function records(kind, fn) { live.slice().forEach(function (r) { if (r.kind === kind) fn(r); }); }
46
+
47
+ function setMega(m, show, mode) {
48
+ m.p.hidden = !show;
49
+ m.t.setAttribute('aria-expanded', String(show));
50
+ m.item.classList.toggle('is-open', show);
51
+ m.mode = mode;
52
+ megaOpen = show ? m : megaOpen === m ? null : megaOpen;
53
+ emit(m.t, 'gw:mega', { id: m.p.id, open: show });
54
+ }
55
+ function showMega(m, mode) {
56
+ clearTimeout(megaTimer);
57
+ if (megaOpen === m) { if (mode === 'click') m.mode = mode; return; }
58
+ if (megaOpen) setMega(megaOpen, false);
59
+ setMega(m, true, mode);
60
+ }
61
+ function hideMega(focus) {
62
+ var m = megaOpen;
63
+ clearTimeout(megaTimer);
64
+ if (!m) return;
65
+ setMega(m, false);
66
+ if (focus === true) m.t.focus();
67
+ }
68
+ function inside(m, el) { return !!el && (m.item.contains(el) || m.p.contains(el)); }
69
+ function mega(ctx) {
70
+ each(ctx, '[data-gw-mega]', 'mega', function (t, on) {
71
+ var p = doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
72
+ if (!p) return;
73
+ var m = { t: t, p: p, item: t.closest('.gw-nav__item') || t.parentNode };
74
+ on(t, 'click', function () { if (megaOpen === m && m.mode !== 'hover') hideMega(); else showMega(m, 'click'); });
75
+ (m.item.contains(p) ? [m.item] : [m.item, p]).forEach(function (el) {
76
+ on(el, 'mouseenter', function () {
77
+ if (fine.matches) { clearTimeout(megaTimer); if (megaOpen !== m) megaTimer = setTimeout(function () { showMega(m, 'hover'); }, 80); }
78
+ });
79
+ on(el, 'mouseleave', function () {
80
+ if (fine.matches) { clearTimeout(megaTimer); if (megaOpen === m && m.mode === 'hover') megaTimer = setTimeout(hideMega, 160); }
53
81
  });
54
- on(m.item, 'focusout', function (e) { if (open === m && e.relatedTarget && !inside(m, e.relatedTarget)) hide(); });
55
- on(m.p, 'click', function (e) { if (link(e)) hide(); });
56
82
  });
57
- on(doc, 'click', function (e) { if (open && !inside(open, e.target)) hide(); });
58
- on(doc, 'keydown', function (e) { if (open && esc(e)) hide(inside(open, doc.activeElement) || open.mode === 'click'); });
59
- }
83
+ on(m.item, 'focusout', function (e) { if (megaOpen === m && e.relatedTarget && !inside(m, e.relatedTarget)) hideMega(); });
84
+ on(p, 'click', function (e) { if (link(e)) hideMega(); });
85
+ m.stop = function () { if (megaOpen === m) hideMega(); };
86
+ return m;
87
+ });
88
+ }
60
89
 
61
- // Mobile menu. Labels: data-gw-label-open/-close, else the markup's aria-label, else English.
62
- function mobile() {
63
- var btn = doc.querySelector('[data-gw-menu-toggle]');
64
- var menu = btn && doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
90
+ function mobile(ctx) {
91
+ each(ctx, '[data-gw-menu-toggle]', 'menu', function (btn, on) {
92
+ var menu = doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
65
93
  if (!menu) return;
66
94
  var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
67
- var wide = mq('(min-width: 1080px)');
68
- function isOpen() { return !menu.hidden; }
69
- function set(o, focus) {
70
- menu.hidden = !o;
71
- btn.setAttribute('aria-expanded', String(o));
72
- btn.setAttribute('aria-label', label[+o]);
73
- root.classList.toggle('is-menu-open', o);
74
- if (focus) btn.focus();
75
- }
76
- on(btn, 'click', function () { set(!isOpen()); });
77
- on(doc, 'keydown', function (e) { if (esc(e) && isOpen()) set(false, true); });
78
- on(menu, 'click', function (e) { if (link(e)) set(false); }); // same-page anchors do not reload
79
- watch(wide, function () { if (wide.matches && isOpen()) set(false); });
80
- on(window, 'pageshow', function (e) { if (e.persisted && isOpen()) set(false); });
81
- }
95
+ var r = {
96
+ open: function () { return !menu.hidden; },
97
+ set: function (o, focus) {
98
+ menu.hidden = !o;
99
+ btn.setAttribute('aria-expanded', String(o));
100
+ btn.setAttribute('aria-label', label[+o]);
101
+ root.classList.toggle('is-menu-open', o);
102
+ if (focus) btn.focus();
103
+ },
104
+ stop: function () { if (r.open()) r.set(false); }
105
+ };
106
+ on(btn, 'click', function () { r.set(!r.open()); });
107
+ on(menu, 'click', function (e) { if (link(e)) r.set(false); });
108
+ return r;
109
+ });
110
+ }
111
+ function closeMenus(focus) { records('menu', function (r) { if (r.open()) r.set(false, focus); }); }
82
112
 
83
- // Theme toggle. Labels: data-gw-label-dark/-light, else the markup's aria-label, else English.
84
- function theme() {
85
- var sys = mq('(prefers-color-scheme: dark)'), stored = null;
86
- try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
87
- if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
88
- var btns = all('[data-gw-theme-toggle]'), labels = btns.map(function (b) {
89
- return [attr(b, 'data-gw-label-dark', attr(b, 'aria-label', 'Switch to dark theme')), attr(b, 'data-gw-label-light', 'Switch to light theme')];
113
+ function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
114
+ function relabel() { records('theme', function (r) { r.el.setAttribute('aria-label', r.labels[+dark()]); }); }
115
+ function theme(ctx) {
116
+ each(ctx, '[data-gw-theme-toggle]', 'theme', function (b, on) {
117
+ var r = { labels: [attr(b, 'data-gw-label-dark', attr(b, 'aria-label', 'Switch to dark theme')), attr(b, 'data-gw-label-light', 'Switch to light theme')] };
118
+ on(b, 'click', function () {
119
+ var next = dark() ? 'light' : 'dark';
120
+ root.setAttribute('data-theme', next);
121
+ try { localStorage.setItem('gw-theme', next); } catch (e) { /* blocked */ }
122
+ relabel();
123
+ emit(root, 'gw:theme', { theme: next });
90
124
  });
91
- function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
92
- function relabel() { btns.forEach(function (b, i) { b.setAttribute('aria-label', labels[i][+dark()]); }); }
93
- btns.forEach(function (b) {
94
- on(b, 'click', function () {
95
- var next = dark() ? 'light' : 'dark';
96
- root.setAttribute('data-theme', next);
97
- try { localStorage.setItem('gw-theme', next); } catch (e) { /* blocked */ }
98
- relabel();
99
- });
100
- });
101
- watch(sys, relabel);
102
- relabel();
103
- }
125
+ r.stop = function () { b.setAttribute('aria-label', r.labels[0]); };
126
+ return r;
127
+ });
128
+ relabel();
129
+ }
104
130
 
105
- // Reveal at 15% visible, once. Failsafe: all shown after 2.5s, or at once without IntersectionObserver.
106
- function reveal() {
107
- var els = all('.gw-reveal'), io = null;
108
- function showAll() { els.forEach(function (el) { el.classList.add('is-in'); }); if (io) io.disconnect(); }
109
- if (!els.length) return;
110
- if (!window.IntersectionObserver || mq('(prefers-reduced-motion: reduce)').matches) return showAll();
111
- io = new IntersectionObserver(function (list) {
112
- list.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
113
- }, { threshold: 0.15 });
114
- els.forEach(function (el) { io.observe(el); });
115
- setTimeout(showAll, 2500);
116
- }
131
+ function reveal(ctx) {
132
+ var els = [], io = null;
133
+ each(ctx, '.gw-reveal', 'reveal', function (el) { els.push(el); return { stop: function () { if (io) io.unobserve(el); } }; });
134
+ function showAll() { els.forEach(function (el) { el.classList.add('is-in'); }); if (io) io.disconnect(); }
135
+ if (!els.length) return;
136
+ if (!window.IntersectionObserver || mq('(prefers-reduced-motion: reduce)').matches) return showAll();
137
+ io = new IntersectionObserver(function (list) {
138
+ list.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
139
+ }, { threshold: 0.15 });
140
+ els.forEach(function (el) { io.observe(el); });
141
+ setTimeout(showAll, 2500);
142
+ }
117
143
 
118
- // Header: .is-scrolled past 8px, read once per frame.
119
- function header() {
120
- var h = doc.querySelector('[data-gw-header]'), state = false, queued = false;
121
- if (!h) return;
122
- function update() {
123
- var s = window.scrollY > 8;
124
- queued = false;
125
- if (s !== state) h.classList.toggle('is-scrolled', (state = s));
126
- }
127
- on(window, 'scroll', function () { if (!queued) { queued = true; requestAnimationFrame(update); } }, { passive: true });
128
- update();
129
- }
144
+ function scrolled() {
145
+ var s = window.scrollY > 8;
146
+ queued = false;
147
+ records('header', function (r) { if (r.s !== s) r.el.classList.toggle('is-scrolled', (r.s = s)); });
148
+ }
149
+ function header(ctx) {
150
+ each(ctx, '[data-gw-header]', 'header', function (h) { return { s: false, stop: function () { h.classList.remove('is-scrolled'); } }; });
151
+ scrolled();
152
+ }
130
153
 
131
- // aria-current="page" on nav links to this path, unless the markup marked one.
132
- function current() {
133
- var menu = doc.getElementById('gw-mobile-menu');
134
- var links = all('.gw-nav__link[href], .gw-mega__link[href]').concat(menu ? all('a[href]', menu) : []);
135
- function norm(p) { return p.replace(/\/index\.html?$/, '/').replace(/\.html?$/, '').replace(/\/+$/, '') || '/'; }
136
- if (links.some(function (a) { return a.hasAttribute('aria-current'); })) return;
137
- links.forEach(function (a) {
138
- if (a.classList.contains('gw-btn') || a.hash || a.host !== location.host) return;
139
- if (norm(a.pathname) === norm(location.pathname)) a.setAttribute('aria-current', 'page');
140
- });
141
- }
154
+ function current(ctx) {
155
+ var links = within('.gw-nav__link[href], .gw-mega__link[href]', ctx);
156
+ within('#gw-mobile-menu, .gw-mobile-menu', ctx).forEach(function (m) { links = links.concat(all('a[href]', m)); });
157
+ function norm(p) { return p.replace(/\/index\.html?$/, '/').replace(/\.html?$/, '').replace(/\/+$/, '') || '/'; }
158
+ links.forEach(function (a) { if (mine.has(a)) { mine.delete(a); a.removeAttribute('aria-current'); } });
159
+ if (links.some(function (a) { return a.hasAttribute('aria-current'); })) return;
160
+ links.forEach(function (a) {
161
+ if (a.classList.contains('gw-btn') || a.hash || a.host !== location.host) return;
162
+ if (norm(a.pathname) === norm(location.pathname)) { a.setAttribute('aria-current', 'page'); mine.add(a); }
163
+ });
164
+ }
142
165
 
143
- // Segmented control: a radio group, one Tab stop; arrows (Home, End) move and choose. What one option shows carries
144
- // data-gw-show="group:value" and is hidden unless that option is chosen; without the script the markup's own choice shows.
145
- function segmented() {
146
- all('[data-gw-segmented]').forEach(function (g) {
147
- var name = attr(g, 'data-gw-segmented', ''), opts = all('[role="radio"]', g);
148
- if (!opts.length) return;
149
- function pick(o, focus, said) {
150
- var v = attr(o, 'value', '');
151
- opts.forEach(function (x) { x.setAttribute('aria-checked', String(x === o)); x.setAttribute('tabindex', x === o ? '0' : '-1'); });
152
- if (focus) o.focus();
153
- all('[data-gw-show]').forEach(function (el) {
154
- var mine = attr(el, 'data-gw-show', '').split(/\s+/).filter(function (t) { return t.split(':')[0] === name; });
155
- if (mine.length) el.hidden = mine.indexOf(name + ':' + v) < 0;
156
- });
157
- if (said && window.CustomEvent) g.dispatchEvent(new CustomEvent('gw:segmented', { bubbles: true, detail: { name: name, value: v } }));
158
- }
159
- opts.forEach(function (o, i) {
160
- on(o, 'click', function () { pick(o, false, true); });
161
- on(o, 'keydown', function (e) {
162
- var n = opts.length, k = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
163
- if (k === undefined || e.altKey || e.ctrlKey || e.metaKey) return;
164
- e.preventDefault();
165
- pick(opts[k % n], true, true);
166
- });
166
+ function segmented(ctx) {
167
+ each(ctx, '[data-gw-segmented]', 'segmented', function (g, on) {
168
+ var name = attr(g, 'data-gw-segmented', ''), opts = all('[role="radio"]', g);
169
+ if (!opts.length) return;
170
+ function pick(o, focus, said) {
171
+ var v = attr(o, 'value', '');
172
+ opts.forEach(function (x) { x.setAttribute('aria-checked', String(x === o)); x.setAttribute('tabindex', x === o ? '0' : '-1'); });
173
+ if (focus) o.focus();
174
+ all('[data-gw-show]').forEach(function (el) {
175
+ var own = attr(el, 'data-gw-show', '').split(/\s+/).filter(function (t) { return t.split(':')[0] === name; });
176
+ if (own.length) el.hidden = own.indexOf(name + ':' + v) < 0;
177
+ });
178
+ if (said) emit(g, 'gw:segmented', { name: name, value: v });
179
+ }
180
+ opts.forEach(function (o, i) {
181
+ on(o, 'click', function () { pick(o, false, true); });
182
+ on(o, 'keydown', function (e) {
183
+ var n = opts.length, k = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
184
+ if (k === undefined || e.altKey || e.ctrlKey || e.metaKey) return;
185
+ e.preventDefault();
186
+ pick(opts[k % n], true, true);
167
187
  });
168
- pick(opts.filter(function (o) { return attr(o, 'aria-checked', '') === 'true'; })[0] || opts[0]);
169
188
  });
170
- }
189
+ pick(opts.filter(function (o) { return attr(o, 'aria-checked', '') === 'true'; })[0] || opts[0]);
190
+ return {};
191
+ });
192
+ }
171
193
 
172
- // A busy button (aria-busy="true") ignores presses, by mouse or keyboard: no double submit. It keeps its focus and name.
173
- function busy() {
174
- on(doc, 'click', function (e) {
175
- var b = e.target.closest && e.target.closest('.gw-btn[aria-busy="true"]');
176
- if (b) { e.preventDefault(); e.stopPropagation(); }
177
- }, true);
178
- }
194
+ function install() {
195
+ doc = document; root = doc.documentElement; mine = new WeakSet();
196
+ fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
197
+ var wide = mq('(min-width: 1080px)'), stored = null;
198
+ on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
199
+ on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
200
+ on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
201
+ watch(wide, function () { if (wide.matches) closeMenus(); });
202
+ on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
203
+ try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
204
+ if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
205
+ watch(sys, function () { relabel(); if (!root.hasAttribute('data-theme')) emit(root, 'gw:theme', { theme: dark() ? 'dark' : 'light' }); });
206
+ on(window, 'scroll', function () { if (!queued) { queued = true; requestAnimationFrame(scrolled); } }, { passive: true });
207
+ on(doc, 'click', function (e) {
208
+ var b = e.target.closest && e.target.closest('.gw-btn[aria-busy="true"]');
209
+ if (b) { e.preventDefault(); e.stopPropagation(); }
210
+ }, true);
211
+ }
179
212
 
180
- function init() {
181
- [mega, mobile, theme, reveal, header, current, segmented, busy].forEach(function (f) {
182
- try { f(); } catch (e) { console.error('[gw-website]', e); }
183
- });
184
- }
185
- if (doc.readyState === 'loading') on(doc, 'DOMContentLoaded', init); else init();
213
+ function run(fns, ctx) {
214
+ fns.forEach(function (f) { try { f(ctx); } catch (e) { console.error('[gw-website]', e); } });
215
+ }
216
+ function other() { var g = typeof window != 'undefined' && window.gwWebsite; return g && g !== api && g; }
217
+
218
+ /** Wires the kit's markup under root (default: the whole document). Safe to call again: wires only what is new. */
219
+ function init(ctx) {
220
+ if (typeof document == 'undefined') return;
221
+ var g = other();
222
+ if (g) return g.init && g.init(ctx);
223
+ window.gwWebsite = api;
224
+ if (!doc) run([install]);
225
+ live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
226
+ run([mega, mobile, theme, reveal, header, current, segmented], ctx && ctx.querySelectorAll ? ctx : doc);
227
+ }
228
+
229
+ /** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
230
+ function destroy(ctx) {
231
+ var g = other();
232
+ if (g) return g.destroy && g.destroy(ctx);
233
+ if (!doc) return;
234
+ ctx = ctx && ctx.querySelectorAll ? ctx : doc;
235
+ live.slice().forEach(function (r) { if (ctx === doc || ctx.contains(r.el)) drop(r); });
236
+ }
237
+
238
+ window.gwWebsite = api;
239
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(); }); else init();
186
240
  })();
package/js/website.mjs ADDED
@@ -0,0 +1,269 @@
1
+ /*! @gemboss/website-ui website.mjs | spec: docs/markup-contract.md */
2
+ // The ESM entry: import { init, destroy, version } from '@gemboss/website-ui/website.mjs'. Importing it does nothing;
3
+ // call init() once the markup is in the page, and again after every route change or insert. No dependencies.
4
+ //
5
+ // js/website.js, the classic script, is GENERATED from this file by scripts/build.mjs (this code in an IIFE, without
6
+ // the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
7
+ // their own, so the build can leave them out.
8
+
9
+ var version = '0.4.0';
10
+ var api = { version: version, init: init, destroy: destroy };
11
+ // Page state. Set up by the first init(), never at import: a server can import this file.
12
+ var doc, root, mine, fine, sys, live = [], megaOpen = null, megaTimer = 0, queued = false;
13
+ var READY = 'data-gw-ready';
14
+
15
+ function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
16
+ function watch(m, fn) { if (m.addEventListener) m.addEventListener('change', fn); }
17
+ function all(sel, ctx) { return [].slice.call((ctx || doc).querySelectorAll(sel)); }
18
+ // Like all(), and ctx itself when it matches: init(header) wires the header it is given.
19
+ function within(sel, ctx) { var l = all(sel, ctx); if (ctx.matches && ctx.matches(sel)) l.unshift(ctx); return l; }
20
+ function on(el, type, fn, opt) { el.addEventListener(type, fn, opt || false); }
21
+ function attr(el, name, dflt) { return el.getAttribute(name) || dflt; }
22
+ function esc(e) { return e.key === 'Escape'; }
23
+ function link(e) { return e.target.closest('a[href]'); }
24
+ function emit(el, type, detail) { if (window.CustomEvent) el.dispatchEvent(new CustomEvent(type, { bubbles: true, detail: detail })); }
25
+
26
+ // Wiring. Each element under ctx that matches sel is wired once as `kind`: fn(el, on) adds the element's listeners
27
+ // with the `on` it is given (so destroy can remove them) and returns a record, or nothing when the element cannot be
28
+ // wired yet (the next init tries again). Records live in `live`. data-gw-ready lists the kinds wired on an element;
29
+ // the record, not the attribute, decides, so markup copied from a live page, attribute and all, is still wired.
30
+ function each(ctx, sel, kind, fn) {
31
+ within(sel, ctx).forEach(function (el) {
32
+ if (live.some(function (r) { return r.el === el && r.kind === kind; })) return;
33
+ var offs = [], r = fn(el, function (t, type, f, opt) {
34
+ on(t, type, f, opt);
35
+ offs.push(function () { t.removeEventListener(type, f, opt || false); });
36
+ });
37
+ if (!r) return offs.forEach(function (f) { f(); });
38
+ r.el = el; r.kind = kind; r.offs = offs;
39
+ mark(el, kind, true);
40
+ live.push(r);
41
+ });
42
+ }
43
+ function mark(el, kind, add) {
44
+ var l = attr(el, READY, '').split(' ').filter(function (k) { return k && k !== kind; });
45
+ if (add) l.push(kind);
46
+ if (l.length) el.setAttribute(READY, l.join(' ')); else el.removeAttribute(READY);
47
+ }
48
+ function drop(r) {
49
+ if (r.stop) r.stop();
50
+ r.offs.forEach(function (f) { f(); });
51
+ mark(r.el, r.kind);
52
+ live.splice(live.indexOf(r), 1);
53
+ }
54
+ function records(kind, fn) { live.slice().forEach(function (r) { if (r.kind === kind) fn(r); }); }
55
+
56
+ // Mega menu. Opened by hover then clicked, a panel stays open: the click meant "open". One open on the page, across
57
+ // every header init() wired. Each change fires gw:mega on the trigger.
58
+ function setMega(m, show, mode) {
59
+ m.p.hidden = !show;
60
+ m.t.setAttribute('aria-expanded', String(show));
61
+ m.item.classList.toggle('is-open', show);
62
+ m.mode = mode;
63
+ megaOpen = show ? m : megaOpen === m ? null : megaOpen;
64
+ emit(m.t, 'gw:mega', { id: m.p.id, open: show });
65
+ }
66
+ function showMega(m, mode) {
67
+ clearTimeout(megaTimer);
68
+ if (megaOpen === m) { if (mode === 'click') m.mode = mode; return; }
69
+ if (megaOpen) setMega(megaOpen, false);
70
+ setMega(m, true, mode);
71
+ }
72
+ function hideMega(focus) {
73
+ var m = megaOpen;
74
+ clearTimeout(megaTimer);
75
+ if (!m) return;
76
+ setMega(m, false);
77
+ if (focus === true) m.t.focus();
78
+ }
79
+ function inside(m, el) { return !!el && (m.item.contains(el) || m.p.contains(el)); }
80
+ function mega(ctx) {
81
+ each(ctx, '[data-gw-mega]', 'mega', function (t, on) {
82
+ var p = doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
83
+ if (!p) return;
84
+ var m = { t: t, p: p, item: t.closest('.gw-nav__item') || t.parentNode };
85
+ on(t, 'click', function () { if (megaOpen === m && m.mode !== 'hover') hideMega(); else showMega(m, 'click'); });
86
+ (m.item.contains(p) ? [m.item] : [m.item, p]).forEach(function (el) {
87
+ on(el, 'mouseenter', function () {
88
+ if (fine.matches) { clearTimeout(megaTimer); if (megaOpen !== m) megaTimer = setTimeout(function () { showMega(m, 'hover'); }, 80); }
89
+ });
90
+ on(el, 'mouseleave', function () {
91
+ if (fine.matches) { clearTimeout(megaTimer); if (megaOpen === m && m.mode === 'hover') megaTimer = setTimeout(hideMega, 160); }
92
+ });
93
+ });
94
+ on(m.item, 'focusout', function (e) { if (megaOpen === m && e.relatedTarget && !inside(m, e.relatedTarget)) hideMega(); });
95
+ on(p, 'click', function (e) { if (link(e)) hideMega(); });
96
+ m.stop = function () { if (megaOpen === m) hideMega(); };
97
+ return m;
98
+ });
99
+ }
100
+
101
+ // Mobile menu. Labels: data-gw-label-open/-close, else the markup's aria-label, else English. A link inside closes it
102
+ // (same-page anchors do not reload). Taken back while open, it closes first: it must not keep the page locked.
103
+ function mobile(ctx) {
104
+ each(ctx, '[data-gw-menu-toggle]', 'menu', function (btn, on) {
105
+ var menu = doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
106
+ if (!menu) return;
107
+ var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
108
+ var r = {
109
+ open: function () { return !menu.hidden; },
110
+ set: function (o, focus) {
111
+ menu.hidden = !o;
112
+ btn.setAttribute('aria-expanded', String(o));
113
+ btn.setAttribute('aria-label', label[+o]);
114
+ root.classList.toggle('is-menu-open', o);
115
+ if (focus) btn.focus();
116
+ },
117
+ stop: function () { if (r.open()) r.set(false); }
118
+ };
119
+ on(btn, 'click', function () { r.set(!r.open()); });
120
+ on(menu, 'click', function (e) { if (link(e)) r.set(false); });
121
+ return r;
122
+ });
123
+ }
124
+ function closeMenus(focus) { records('menu', function (r) { if (r.open()) r.set(false, focus); }); }
125
+
126
+ // Theme toggle. Labels: data-gw-label-dark/-light, else the markup's aria-label, else English. A press fires gw:theme
127
+ // on <html>; so does a change of the system's scheme while the page follows it.
128
+ function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
129
+ function relabel() { records('theme', function (r) { r.el.setAttribute('aria-label', r.labels[+dark()]); }); }
130
+ function theme(ctx) {
131
+ each(ctx, '[data-gw-theme-toggle]', 'theme', function (b, on) {
132
+ var r = { labels: [attr(b, 'data-gw-label-dark', attr(b, 'aria-label', 'Switch to dark theme')), attr(b, 'data-gw-label-light', 'Switch to light theme')] };
133
+ on(b, 'click', function () {
134
+ var next = dark() ? 'light' : 'dark';
135
+ root.setAttribute('data-theme', next);
136
+ try { localStorage.setItem('gw-theme', next); } catch (e) { /* blocked */ }
137
+ relabel();
138
+ emit(root, 'gw:theme', { theme: next });
139
+ });
140
+ // taken back: the markup's own label again, so a later init reads the same words
141
+ r.stop = function () { b.setAttribute('aria-label', r.labels[0]); };
142
+ return r;
143
+ });
144
+ relabel();
145
+ }
146
+
147
+ // Reveal at 15% visible, once. Failsafe: shown 2.5s after the init that found them, or at once without
148
+ // IntersectionObserver or under reduced motion.
149
+ function reveal(ctx) {
150
+ var els = [], io = null;
151
+ each(ctx, '.gw-reveal', 'reveal', function (el) { els.push(el); return { stop: function () { if (io) io.unobserve(el); } }; });
152
+ function showAll() { els.forEach(function (el) { el.classList.add('is-in'); }); if (io) io.disconnect(); }
153
+ if (!els.length) return;
154
+ if (!window.IntersectionObserver || mq('(prefers-reduced-motion: reduce)').matches) return showAll();
155
+ io = new IntersectionObserver(function (list) {
156
+ list.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
157
+ }, { threshold: 0.15 });
158
+ els.forEach(function (el) { io.observe(el); });
159
+ setTimeout(showAll, 2500);
160
+ }
161
+
162
+ // Header: .is-scrolled past 8px, read once per frame (one scroll listener for every header).
163
+ function scrolled() {
164
+ var s = window.scrollY > 8;
165
+ queued = false;
166
+ records('header', function (r) { if (r.s !== s) r.el.classList.toggle('is-scrolled', (r.s = s)); });
167
+ }
168
+ function header(ctx) {
169
+ each(ctx, '[data-gw-header]', 'header', function (h) { return { s: false, stop: function () { h.classList.remove('is-scrolled'); } }; });
170
+ scrolled();
171
+ }
172
+
173
+ // aria-current="page" on nav links to this path, unless the markup marked one. The script's own marks are taken
174
+ // back first, so an init() after a client-side navigation moves them.
175
+ function current(ctx) {
176
+ var links = within('.gw-nav__link[href], .gw-mega__link[href]', ctx);
177
+ within('#gw-mobile-menu, .gw-mobile-menu', ctx).forEach(function (m) { links = links.concat(all('a[href]', m)); });
178
+ function norm(p) { return p.replace(/\/index\.html?$/, '/').replace(/\.html?$/, '').replace(/\/+$/, '') || '/'; }
179
+ links.forEach(function (a) { if (mine.has(a)) { mine.delete(a); a.removeAttribute('aria-current'); } });
180
+ if (links.some(function (a) { return a.hasAttribute('aria-current'); })) return;
181
+ links.forEach(function (a) {
182
+ if (a.classList.contains('gw-btn') || a.hash || a.host !== location.host) return;
183
+ if (norm(a.pathname) === norm(location.pathname)) { a.setAttribute('aria-current', 'page'); mine.add(a); }
184
+ });
185
+ }
186
+
187
+ // Segmented control: a radio group, one Tab stop; arrows (Home, End) move and choose. What one option shows carries
188
+ // data-gw-show="group:value" and is hidden unless that option is chosen; without the script the markup's own choice
189
+ // shows. A choice made by the reader fires gw:segmented on the group; wiring it does not.
190
+ function segmented(ctx) {
191
+ each(ctx, '[data-gw-segmented]', 'segmented', function (g, on) {
192
+ var name = attr(g, 'data-gw-segmented', ''), opts = all('[role="radio"]', g);
193
+ if (!opts.length) return;
194
+ function pick(o, focus, said) {
195
+ var v = attr(o, 'value', '');
196
+ opts.forEach(function (x) { x.setAttribute('aria-checked', String(x === o)); x.setAttribute('tabindex', x === o ? '0' : '-1'); });
197
+ if (focus) o.focus();
198
+ all('[data-gw-show]').forEach(function (el) {
199
+ var own = attr(el, 'data-gw-show', '').split(/\s+/).filter(function (t) { return t.split(':')[0] === name; });
200
+ if (own.length) el.hidden = own.indexOf(name + ':' + v) < 0;
201
+ });
202
+ if (said) emit(g, 'gw:segmented', { name: name, value: v });
203
+ }
204
+ opts.forEach(function (o, i) {
205
+ on(o, 'click', function () { pick(o, false, true); });
206
+ on(o, 'keydown', function (e) {
207
+ var n = opts.length, k = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
208
+ if (k === undefined || e.altKey || e.ctrlKey || e.metaKey) return;
209
+ e.preventDefault();
210
+ pick(opts[k % n], true, true);
211
+ });
212
+ });
213
+ pick(opts.filter(function (o) { return attr(o, 'aria-checked', '') === 'true'; })[0] || opts[0]);
214
+ return {};
215
+ });
216
+ }
217
+
218
+ // Once per page: what belongs to the document, the window or a media query rather than to one element.
219
+ function install() {
220
+ doc = document; root = doc.documentElement; mine = new WeakSet();
221
+ fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
222
+ var wide = mq('(min-width: 1080px)'), stored = null;
223
+ on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
224
+ on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
225
+ on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
226
+ watch(wide, function () { if (wide.matches) closeMenus(); });
227
+ on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
228
+ try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
229
+ if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
230
+ watch(sys, function () { relabel(); if (!root.hasAttribute('data-theme')) emit(root, 'gw:theme', { theme: dark() ? 'dark' : 'light' }); });
231
+ on(window, 'scroll', function () { if (!queued) { queued = true; requestAnimationFrame(scrolled); } }, { passive: true });
232
+ // A busy button (aria-busy="true") ignores presses, by mouse or keyboard: no double submit. It keeps its focus and
233
+ // name. One listener on the document, in the capture phase, so it covers buttons inserted later too.
234
+ on(doc, 'click', function (e) {
235
+ var b = e.target.closest && e.target.closest('.gw-btn[aria-busy="true"]');
236
+ if (b) { e.preventDefault(); e.stopPropagation(); }
237
+ }, true);
238
+ }
239
+
240
+ function run(fns, ctx) {
241
+ fns.forEach(function (f) { try { f(ctx); } catch (e) { console.error('[gw-website]', e); } });
242
+ }
243
+ // One copy of the kit owns a page (window.gwWebsite). When another one got there first (the classic script, a second
244
+ // bundle), it does the work, so nothing is wired twice.
245
+ function other() { var g = typeof window != 'undefined' && window.gwWebsite; return g && g !== api && g; }
246
+
247
+ /** Wires the kit's markup under root (default: the whole document). Safe to call again: wires only what is new. */
248
+ function init(ctx) {
249
+ // server rendering: nothing to wire
250
+ if (typeof document == 'undefined') return;
251
+ var g = other();
252
+ if (g) return g.init && g.init(ctx);
253
+ window.gwWebsite = api;
254
+ if (!doc) run([install]);
255
+ // an element removed from the page since the last init is let go
256
+ live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
257
+ run([mega, mobile, theme, reveal, header, current, segmented], ctx && ctx.querySelectorAll ? ctx : doc);
258
+ }
259
+
260
+ /** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
261
+ function destroy(ctx) {
262
+ var g = other();
263
+ if (g) return g.destroy && g.destroy(ctx);
264
+ if (!doc) return;
265
+ ctx = ctx && ctx.querySelectorAll ? ctx : doc;
266
+ live.slice().forEach(function (r) { if (ctx === doc || ctx.contains(r.el)) drop(r); });
267
+ }
268
+
269
+ export { version, init, destroy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/website-ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  "./website.css": "./website.css",
24
24
  "./website.min.css": "./website.min.css",
25
25
  "./website.js": "./js/website.js",
26
+ "./website.mjs": "./js/website.mjs",
26
27
  "./system.json": "./system.json",
27
28
  "./css/*": "./css/*",
28
29
  "./package.json": "./package.json"
package/skill/SKILL.md CHANGED
@@ -20,6 +20,8 @@ the kit and its rules are how the page ends up looking designed. Compose from th
20
20
  - Changed page: keep its kit classes; change words, order and choice of sections.
21
21
  - Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
22
22
  scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
23
+ - No JS of your own for kit parts: markup inserted after load (a route change, a block) gets
24
+ `gwWebsite.init(root)`, or `init(root)` from `website.mjs` (`KIT/docs/markup-contract.md`, "Script").
23
25
  - Another brand: choose only the accent family, wash hues, logo and favicon, and the three named profiles
24
26
  (`data-gw-action` and `data-gw-radius` on `<html>`, `.gw-header--float`), never free values
25
27
  (`KIT/docs/theming.md`).
package/website.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! @gemboss/website-ui 0.3.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
1
+ /*! @gemboss/website-ui 0.4.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
2
2
  /* @gemboss/website-ui tokens. GENERATED from system.json by scripts/build.mjs: do not edit by hand. */
3
3
  :root {
4
4
  color-scheme: light;
package/website.min.css CHANGED
@@ -1 +1 @@
1
- /*! @gemboss/website-ui 0.3.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */ :root{color-scheme: light;--gw-font: Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--gw-font-mono: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--gw-text-11: 11px;--gw-text-12: 12px;--gw-text-13: 13px;--gw-text-14: 14px;--gw-text-16: 16px;--gw-text-18: 18px;--gw-text-20: 20px;--gw-text-22: 22px;--gw-text-24: 24px;--gw-text-30: 30px;--gw-display-h1: clamp(40px,4.6vw,68px);--gw-display-h2: clamp(32px,3.4vw,52px);--gw-display-weight: 600;--gw-display-tracking: -0.03em;--gw-heading-weight: 650;--gw-heading-tracking: -0.022em;--gw-heading-leading: 1.08;--gw-body-size: 18px;--gw-body-leading: 1.6;--gw-space-0: 0;--gw-space-2: 2px;--gw-space-4: 4px;--gw-space-6: 6px;--gw-space-8: 8px;--gw-space-12: 12px;--gw-space-16: 16px;--gw-space-20: 20px;--gw-space-24: 24px;--gw-space-32: 32px;--gw-space-40: 40px;--gw-space-48: 48px;--gw-space-64: 64px;--gw-space-80: 80px;--gw-space-96: 96px;--gw-space-120: 120px;--gw-space-section: clamp(56px,38px + 5vw,110px);--gw-radius-0: 0;--gw-radius-4: 4px;--gw-radius-6: 6px;--gw-radius-8: 8px;--gw-radius-12: 12px;--gw-radius-16: 16px;--gw-radius-20: 20px;--gw-radius-24: 24px;--gw-radius-999: 999px;--gw-radius-action: 8px;--gw-radius-chip: 999px;--gw-radius-card: 12px;--gw-radius-panel: 16px;--gw-radius-popover: 20px;--gw-shadow: 0 1px 2px rgba(31,30,29,.04),0 2px 6px rgba(31,30,29,.06);--gw-shadow-sm: 0 1px 2px rgba(31,30,29,.04);--gw-shadow-raised: 0 4px 16px rgba(31,30,29,.10);--gw-wrap: 1120px;--gw-wrap-narrow: 780px;--gw-gutter: 22px;--gw-header-height: 64px;--gw-header-gap: 16px;--gw-header-surface: 85%;--gw-header-blur: 10px;--gw-button-height: 40px;--gw-tap-min: 44px;--gw-ease: cubic-bezier(.22,1,.36,1);--gw-reveal-ms: 700ms;--gw-spin-ms: 700ms;--gw-wash-pink: #ff6f5e;--gw-wash-violet: #7c8af7;--gw-wash-sky: #43b0ff;--gw-wash-mint: #1fcf9f;--gw-accent-light: #5661f5;--gw-accent-strong-light: #3a44de;--gw-accent-soft-light: #eef0fe;--gw-on-accent-light: #ffffff;--gw-accent-dark: #7c84ff;--gw-accent-strong-dark: #a3a9ff;--gw-accent-soft-dark: #1f2140;--gw-on-accent-dark: #0d0d12;--gw-bg: #ffffff;--gw-bg-soft: #f2f3f5;--gw-surface: #ffffff;--gw-ink: #1b1c1e;--gw-ink-2: #3b3d42;--gw-muted: #686b71;--gw-line: #e5e7ea;--gw-line-strong: #d5d8dd;--gw-action: #1b1c1e;--gw-action-hover: #000000;--gw-on-action: #ffffff;--gw-accent: var(--gw-accent-light);--gw-accent-strong: var(--gw-accent-strong-light);--gw-accent-soft: var(--gw-accent-soft-light);--gw-on-accent: var(--gw-on-accent-light);--gw-success: #007f5f;--gw-success-soft: #e3f1df;--gw-warning: #9a6700;--gw-night: #0c0e14;--gw-night-2: #141722;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){color-scheme: dark;--gw-bg: #0d0d12;--gw-bg-soft: #121318;--gw-surface: #16171d;--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341;--gw-night: #0a0b10;--gw-night-2: #12141c;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}}:root[data-theme="dark"]{color-scheme: dark;--gw-bg: #0d0d12;--gw-bg-soft: #121318;--gw-surface: #16171d;--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341;--gw-night: #0a0b10;--gw-night-2: #12141c;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}:root[data-theme="light"]{color-scheme: light;--gw-bg: #ffffff;--gw-bg-soft: #f2f3f5;--gw-surface: #ffffff;--gw-ink: #1b1c1e;--gw-ink-2: #3b3d42;--gw-muted: #686b71;--gw-line: #e5e7ea;--gw-line-strong: #d5d8dd;--gw-action: #1b1c1e;--gw-action-hover: #000000;--gw-on-action: #ffffff;--gw-accent: var(--gw-accent-light);--gw-accent-strong: var(--gw-accent-strong-light);--gw-accent-soft: var(--gw-accent-soft-light);--gw-on-accent: var(--gw-on-accent-light);--gw-success: #007f5f;--gw-success-soft: #e3f1df;--gw-warning: #9a6700;--gw-night: #0c0e14;--gw-night-2: #141722;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}.gw-band--night,.gw-showcase{color-scheme: dark;--gw-bg: var(--gw-night);--gw-bg-soft: var(--gw-night-2);--gw-surface: var(--gw-night-2);--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341}:root[data-gw-action="accent"],:root[data-gw-action="accent"] :is(.gw-band--night,.gw-showcase){--gw-action: var(--gw-accent);--gw-action-hover: var(--gw-accent-strong);--gw-on-action: var(--gw-on-accent)}:root[data-gw-radius="soft"]{--gw-radius-action: 12px;--gw-radius-card: 20px;--gw-radius-panel: 24px;--gw-radius-popover: 24px}*,*::before,*::after{box-sizing: border-box}[hidden]{display: none !important}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%}body{margin: 0;background: var(--gw-bg);color: var(--gw-ink);font-family: var(--gw-font);font-size: var(--gw-body-size);line-height: var(--gw-body-leading);-webkit-font-smoothing: antialiased;text-rendering: optimizeLegibility}:where(h1,h2,h3,h4,h5,h6,p,ul,ol,dl,dd,figure,blockquote){margin: 0}img,svg,video{display: block;max-width: 100%}img,video{height: auto}button,input,select,textarea{font: inherit;color: inherit}button{cursor: pointer}a{color: inherit;text-decoration: none}b,strong{font-weight: 600}.gw-page{display: flex;flex-direction: column;min-height: 100vh;min-height: 100dvh}.gw-page > main{flex: 1 0 auto}h1,h2,h3,h4,h5,h6,.gw-h1,.gw-h2,.gw-h3,.gw-h4{font-weight: var(--gw-heading-weight);letter-spacing: var(--gw-heading-tracking);line-height: var(--gw-heading-leading);text-wrap: balance}h1,.gw-h1,h2,.gw-h2{font-weight: var(--gw-display-weight);letter-spacing: var(--gw-display-tracking)}h1,.gw-h1{font-size: var(--gw-display-h1)}h2,.gw-h2{font-size: var(--gw-display-h2)}h3,.gw-h3{font-size: 24px}h4,.gw-h4{font-size: 20px;font-weight: 600;letter-spacing: -0.01em}h5,h6{font-size: 16px;font-weight: 600;letter-spacing: 0}.gw-lead{font-size: 18px;line-height: 1.55;color: var(--gw-muted);max-width: 60ch}.gw-note{font-size: 14px;color: var(--gw-muted)}.gw-dim{color: var(--gw-muted)}.gw-label{display: inline-flex;align-items: center;gap: 8px;font-size: 13px;font-weight: 500;line-height: 1.4;color: var(--gw-muted)}.gw-label::before{content: "";flex: none;width: 8px;height: 8px;border-radius: var(--gw-radius-0);background: var(--gw-accent)}:where(p,dd,td,figcaption,.gw-list--check li,.gw-prose) a:not([class]),.gw-link{color: var(--gw-accent-strong);font-weight: 500;text-decoration: underline;text-decoration-thickness: 1px;text-underline-offset: 4px;text-decoration-color: color-mix(in srgb,currentColor 40%,transparent)}:where(p,dd,td,figcaption,.gw-list--check li,.gw-prose) a:not([class]):hover,.gw-link:hover{text-decoration-color: currentColor}:focus-visible{outline: 2px solid var(--gw-accent);outline-offset: 2px}[id]{scroll-margin-top: calc(var(--gw-header-height) + 16px)}.gw-skip{position: absolute;left: -9999px;top: 8px;z-index: 100;padding: 12px 16px;border-radius: var(--gw-radius-action);background: var(--gw-action);color: var(--gw-on-action);font-size: 14px;font-weight: 600}.gw-skip:focus{left: 16px}.gw-reveal{opacity: 0;transform: translateY(24px);transition: opacity var(--gw-reveal-ms) var(--gw-ease),transform var(--gw-reveal-ms) var(--gw-ease);animation: gw-reveal-failsafe 1ms linear 2.5s forwards}.gw-reveal.is-in{opacity: 1;transform: none}@keyframes gw-reveal-failsafe{to{opacity: 1;transform: none}}@media (prefers-reduced-motion: reduce){*,*::before,*::after{scroll-behavior: auto !important}.gw-reveal{opacity: 1;transform: none;transition: none;animation: none}}.gw-wrap,.gw-wrap--narrow{width: min(100% - 2 * var(--gw-gutter),var(--gw-wrap));margin-inline: auto}.gw-wrap--narrow{max-width: var(--gw-wrap-narrow)}.gw-section{padding-block: var(--gw-space-section)}.gw-section--soft{background: var(--gw-bg-soft)}.gw-section--tight{padding-block: 40px}@media (min-width: 768px){.gw-section--tight{padding-block: 64px}}.gw-section__head{max-width: 660px}.gw-section__head--center{margin-inline: auto;text-align: center}.gw-section__head--center .gw-lead{margin-inline: auto}.gw-section__head:not(:last-child){margin-bottom: 40px}@media (min-width: 768px){.gw-section__head:not(:last-child){margin-bottom: 48px}}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-feature,.gw-plan) > * + *{margin-top: 16px}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-plan) > :is(.gw-actions,.gw-segmented){margin-top: 32px}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-plan) > .gw-list--check{margin-top: 24px}.gw-actions{display: flex;flex-wrap: wrap;align-items: center;gap: 12px}.gw-grid,.gw-grid--2,.gw-grid--3,.gw-grid--4{display: grid;grid-template-columns: minmax(0,1fr);gap: 16px}@media (min-width: 480px){.gw-grid--4{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.gw-grid,.gw-grid--2,.gw-grid--3,.gw-grid--4{gap: 24px}.gw-grid--2{grid-template-columns: repeat(2,minmax(0,1fr))}.gw-grid--3{grid-template-columns: repeat(3,minmax(0,1fr))}}@media (min-width: 1080px){.gw-grid--4{grid-template-columns: repeat(4,minmax(0,1fr))}}.gw-band--night,.gw-showcase{position: relative;isolation: isolate;color: var(--gw-ink);background: radial-gradient(800px 360px at 50% 0%,color-mix(in srgb,var(--gw-accent) 22%,transparent),transparent 72%),var(--gw-night)}.gw-btn{position: relative;display: inline-flex;align-items: center;justify-content: center;gap: 8px;max-width: 100%;height: var(--gw-button-height);padding: 0 20px;border: 1px solid transparent;border-radius: var(--gw-radius-action);background: var(--gw-action);color: var(--gw-on-action);font-size: 14px;font-weight: 600;line-height: 1.4;letter-spacing: 0;text-align: center;white-space: nowrap;cursor: pointer}.gw-btn:hover{background: var(--gw-action-hover)}.gw-btn svg{flex: none;width: 16px;height: 16px}.gw-btn--secondary{background: transparent;border-color: var(--gw-line-strong);color: var(--gw-ink)}.gw-btn--secondary:hover{background: transparent;border-color: var(--gw-ink)}.gw-btn--ghost{background: transparent;color: var(--gw-ink)}.gw-btn--ghost:hover{background: var(--gw-bg-soft)}.gw-btn--on-night{background: var(--gw-on-night);color: var(--gw-night)}.gw-btn--on-night:hover{background: var(--gw-on-night);opacity: 0.88}.gw-btn--sm{height: 36px;padding: 0 16px}.gw-btn--sm::after{content: "";position: absolute;top: -6px;right: 0;bottom: -6px;left: 0}.gw-btn--lg{height: 48px;padding: 0 24px;font-size: 16px}.gw-btn--block{display: flex;width: 100%}.gw-btn:disabled,.gw-btn[aria-disabled="true"]{opacity: 0.48;cursor: not-allowed;pointer-events: none}.gw-btn:active{background: var(--gw-action-hover)}.gw-btn--secondary:active{background: var(--gw-bg-soft);border-color: var(--gw-ink)}.gw-btn--ghost:active{background: var(--gw-line)}.gw-btn--on-night:active{background: var(--gw-on-night);opacity: 0.76}.gw-btn[aria-busy="true"]{cursor: progress;-webkit-text-fill-color: transparent}.gw-btn[aria-busy="true"] svg{opacity: 0}.gw-btn[aria-busy="true"]::before{content: "";position: absolute;top: 50%;left: 50%;width: 16px;height: 16px;margin: -8px 0 0 -8px;border: 2px solid color-mix(in srgb,currentColor 18%,transparent);border-top-color: currentColor;border-radius: var(--gw-radius-chip);animation: gw-spin var(--gw-spin-ms) linear infinite}@keyframes gw-spin{to{transform: rotate(360deg)}}@media (prefers-reduced-motion: reduce){.gw-btn[aria-busy="true"]::before{border: 0;border-radius: var(--gw-radius-0);background: radial-gradient(circle at 3px 50%,currentColor 1.5px,transparent 2px),radial-gradient(circle at 8px 50%,currentColor 1.5px,transparent 2px),radial-gradient(circle at 13px 50%,currentColor 1.5px,transparent 2px);animation: none}}.gw-link--arrow{display: inline-flex;align-items: center;gap: 6px;min-height: 24px;font-weight: 600;text-decoration: none}.gw-link--arrow::after{content: "\2192";transition: transform 0.18s var(--gw-ease)}.gw-link--arrow:hover::after{transform: translateX(2px)}.gw-chip{display: inline-flex;align-items: center;gap: 6px;min-height: 32px;padding: 4px 12px;border: 1px solid var(--gw-line-strong);border-radius: var(--gw-radius-chip);background: var(--gw-surface);color: var(--gw-ink-2);font-size: 13px;font-weight: 500;line-height: 1.4;white-space: nowrap}:is(a,button,label).gw-chip{cursor: pointer}:is(a,button,label).gw-chip:hover{border-color: var(--gw-ink);color: var(--gw-ink)}.gw-chip.is-active,.gw-chip[aria-pressed="true"],.gw-chip[aria-selected="true"]{border-color: var(--gw-accent);background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-segmented{display: inline-flex;max-width: 100%;border: 1px solid var(--gw-line-strong);border-radius: var(--gw-radius-action);background: var(--gw-surface);overflow: hidden;overflow: clip;vertical-align: middle}.gw-segmented--block{display: flex;width: 100%}.gw-segmented > button{display: inline-flex;flex: 0 1 auto;align-items: center;justify-content: center;min-width: 0;height: calc(var(--gw-button-height) - 2px);padding: 0 16px;border: 0;background: transparent;color: var(--gw-muted);font-size: 14px;font-weight: 500;line-height: 1.4;white-space: nowrap;cursor: pointer}.gw-segmented--block > button{flex: 1 1 0}.gw-segmented > button + button{border-left: 1px solid var(--gw-line-strong)}.gw-segmented > button:hover{background: var(--gw-bg-soft);color: var(--gw-ink)}.gw-segmented > button:active{background: var(--gw-line)}.gw-segmented > button:focus-visible{outline-offset: -2px}.gw-segmented > button[aria-checked="true"]{background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-badge{display: inline-flex;align-items: center;gap: 6px;min-height: 24px;padding: 2px 8px;border-radius: var(--gw-radius-chip);background: color-mix(in srgb,var(--gw-ink) 8%,transparent);color: var(--gw-ink-2);font-size: 12px;font-weight: 500;line-height: 1.4;white-space: nowrap;vertical-align: middle}.gw-badge--accent{background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-badge--success{background: var(--gw-success-soft);color: var(--gw-ink)}.gw-badge--success::before,.gw-badge--warning::before{content: "";flex: none;width: 6px;height: 6px;border-radius: var(--gw-radius-chip);background: var(--gw-success)}.gw-badge--warning::before{background: var(--gw-warning)}.gw-card{padding: 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);box-shadow: var(--gw-shadow)}@media (min-width: 768px){.gw-card{padding: 32px}}.gw-card--soft{background: var(--gw-bg-soft);box-shadow: none}.gw-card--link{display: block;color: inherit}.gw-card--link:hover{border-color: var(--gw-line-strong)}:where(.gw-card,.gw-feature,.gw-plan) > :is(h3,h4) + p{margin-top: 8px}:where(.gw-card,.gw-feature) h3{font-size: 20px}:where(.gw-card,.gw-feature) p{font-size: 16px;color: var(--gw-muted)}.gw-window{overflow: hidden;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);color: var(--gw-ink);box-shadow: var(--gw-shadow);font-size: 13px;line-height: 1.5}.gw-window__bar{display: flex;align-items: center;gap: 12px;min-height: 40px;padding: 8px 16px;border-bottom: 1px solid var(--gw-line);color: var(--gw-muted);font-size: 12px}.gw-window__bar::before{content: "";flex: none;width: 36px;height: 8px;background: radial-gradient(circle at 4px 4px,var(--gw-line-strong) 4px,transparent 4px),radial-gradient(circle at 18px 4px,var(--gw-line-strong) 4px,transparent 4px),radial-gradient(circle at 32px 4px,var(--gw-line-strong) 4px,transparent 4px)}.gw-window__body{padding: 16px}.gw-window__body:has(> img:only-child){padding: 0}.gw-window__body > img{width: 100%}.gw-window__caption{margin: 0;padding: 0 16px 12px;color: var(--gw-muted);font-size: 12px;line-height: 1.5;text-align: end}.gw-window__body:has(> img:only-child) + .gw-window__caption{padding-top: 12px}.gw-icon{display: inline-flex;align-items: center;justify-content: center;flex: none;width: 20px;height: 20px}.gw-icon svg{width: 20px;height: 20px}.gw-list--check{display: grid;gap: 12px;padding: 0;list-style: none}.gw-list--check > li{position: relative;padding-left: 24px}.gw-list--check > li::before{content: "";position: absolute;left: 0;top: 6px;top: calc((1lh - 16px) / 2);width: 16px;height: 16px;background: var(--gw-accent);-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat}.gw-table{overflow-x: auto;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);-webkit-overflow-scrolling: touch}.gw-table table{width: 100%;border-collapse: collapse;font-size: 14px;line-height: 1.5}.gw-table caption{padding: 16px;text-align: left;font-size: 13px;color: var(--gw-muted)}.gw-table th,.gw-table td{padding: 12px 16px;border-bottom: 1px solid var(--gw-line);text-align: left;vertical-align: top}.gw-table thead th{background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 13px;font-weight: 600;white-space: nowrap}.gw-table tbody th{font-weight: 500}.gw-table tbody tr:last-child > *{border-bottom: 0}.gw-table td{color: var(--gw-ink-2);font-variant-numeric: tabular-nums}.gw-table--compare table{min-width: 640px}.gw-table--compare td{text-align: center}.gw-table--compare thead th:not(:first-child){text-align: center}.gw-table--compare tbody th{position: sticky;left: 0;z-index: 1;background: var(--gw-surface);min-width: 160px}.gw-faq{display: grid;gap: 12px}.gw-faq__item{padding: 0 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface)}.gw-faq__item[open]{border-color: var(--gw-line-strong)}.gw-faq__item > summary{display: flex;align-items: center;justify-content: space-between;gap: 16px;min-height: 44px;padding: 20px 0;font-size: 18px;font-weight: 600;line-height: 1.4;list-style: none;cursor: pointer}.gw-faq__item > summary::-webkit-details-marker{display: none}.gw-faq__item > summary::after{content: "";flex: none;width: 16px;height: 16px;background: linear-gradient(var(--gw-accent),var(--gw-accent)) center / 16px 2px no-repeat,linear-gradient(var(--gw-accent),var(--gw-accent)) center / 2px 16px no-repeat;transition: transform 0.2s var(--gw-ease)}.gw-faq__item[open] > summary::after{transform: rotate(45deg)}.gw-faq__item > :not(summary){margin: 0 0 20px;max-width: 68ch;color: var(--gw-muted);font-size: 16px}@media (max-width: 768px),(pointer: coarse){.gw-btn,.gw-btn--sm,.gw-btn--lg{height: auto;min-height: var(--gw-tap-min);padding-block: 8px;white-space: normal;text-wrap: balance}.gw-btn--lg{min-height: 48px}:is(a,button,label).gw-chip{min-height: var(--gw-tap-min)}.gw-segmented > button{height: auto;min-height: var(--gw-tap-min);padding-block: 8px;white-space: normal;text-wrap: balance}}.gw-announce{padding: 8px var(--gw-gutter);border-bottom: 1px solid var(--gw-line);background: var(--gw-bg-soft);color: var(--gw-ink-2);font-size: 13px;line-height: 1.5;text-align: center}.gw-announce a{color: var(--gw-ink);font-weight: 600;text-decoration: underline;text-underline-offset: 4px}.gw-header{position: sticky;top: 0;z-index: 50;border-bottom: 1px solid var(--gw-line);background: var(--gw-surface)}.gw-header.is-scrolled{border-bottom-color: var(--gw-line-strong)}.gw-header__bar{display: flex;align-items: center;gap: 16px;height: var(--gw-header-height)}.gw-brand{display: inline-flex;align-items: center;gap: 8px;flex: none;min-height: 44px;color: var(--gw-ink);font-size: 18px;font-weight: 700;letter-spacing: -0.01em;white-space: nowrap}.gw-brand__mark{flex: none;width: 26px;height: 26px}.gw-brand__name{line-height: 1.2}.gw-nav{display: none}.gw-nav__list{display: flex;align-items: center;gap: 4px;margin: 0;padding: 0;list-style: none}.gw-nav__trigger,.gw-nav__link{display: inline-flex;align-items: center;gap: 6px;height: 40px;padding: 0 12px;border: 0;border-radius: var(--gw-radius-action);background: transparent;color: var(--gw-ink-2);font-size: 14px;font-weight: 500;line-height: 1.4;white-space: nowrap;cursor: pointer}.gw-nav__trigger:hover,.gw-nav__link:hover,.gw-nav__item.is-open > .gw-nav__trigger{background: var(--gw-bg-soft);color: var(--gw-ink)}.gw-nav__trigger::after,.gw-mobile-menu__group > summary::after{content: "";flex: none;width: 12px;height: 12px;background: currentColor;opacity: 0.6;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;transition: transform 0.18s var(--gw-ease)}.gw-nav__trigger[aria-expanded="true"]::after,.gw-mobile-menu__group[open] > summary::after{transform: rotate(180deg)}.gw-nav__link[aria-current="page"]{color: var(--gw-ink);font-weight: 600;text-decoration: underline;text-decoration-color: var(--gw-accent);text-decoration-thickness: 2px;text-underline-offset: 8px}.gw-mega{position: absolute;top: calc(100% + 8px);left: 50%;width: min(960px,calc(100vw - 40px));max-height: calc(100vh - var(--gw-header-height) - 24px);overflow-y: auto;transform: translateX(-50%);border: 1px solid var(--gw-line);border-radius: var(--gw-radius-popover);background: var(--gw-surface);box-shadow: var(--gw-shadow-raised)}.gw-mega::before{content: "";position: absolute;top: -12px;left: 0;right: 0;height: 12px}.gw-mega:not([hidden]){animation: gw-mega-in 0.16s var(--gw-ease)}@keyframes gw-mega-in{from{opacity: 0;transform: translateX(-50%) translateY(8px)}}@media (prefers-reduced-motion: reduce){.gw-mega:not([hidden]){animation: none}}.gw-mega > .gw-wrap{width: auto;margin: 0}.gw-mega__grid{display: grid;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr);gap: 32px;padding: 32px}.gw-mega__col{display: flex;flex-direction: column;gap: 2px}.gw-mega__heading{padding: 0 8px 8px;color: var(--gw-muted);font-size: 12px;font-weight: 500}.gw-mega__link{display: grid;grid-template-columns: 32px minmax(0,1fr);align-items: start;gap: 12px;padding: 8px;border-radius: var(--gw-radius-action);color: var(--gw-ink)}.gw-mega__link:hover{background: var(--gw-bg-soft)}.gw-mega__link b{display: block;font-size: 14px;font-weight: 500;line-height: 20px}.gw-mega__link i{display: block;margin-top: 2px;color: var(--gw-muted);font-size: 13px;font-style: normal;line-height: 18px}.gw-mega__link[aria-current="page"] b{color: var(--gw-accent-strong);font-weight: 600}.gw-mega__icon{display: grid;place-items: center;width: 32px;height: 32px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-action);background: var(--gw-surface);color: var(--gw-muted)}.gw-mega__icon svg{width: 16px;height: 16px}.gw-mega__link:hover .gw-mega__icon{border-color: var(--gw-accent-soft);background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-mega__feature{display: flex;flex-direction: column;justify-content: flex-end;gap: 4px;min-height: 216px;padding: 24px;border-radius: var(--gw-radius-panel);background: radial-gradient(360px 260px at 12% 112%,color-mix(in srgb,var(--gw-accent) 78%,transparent),transparent 62%),radial-gradient(300px 220px at 92% -14%,color-mix(in srgb,var(--gw-wash-violet) 44%,transparent),transparent 70%),var(--gw-night);color: var(--gw-on-night)}.gw-mega__feature b{font-size: 18px;font-weight: 650;line-height: 1.25}.gw-mega__feature i{color: var(--gw-on-night);font-size: 13px;font-style: normal;line-height: 1.5;opacity: 0.9}.gw-mega__feature .gw-link--arrow{margin-top: 16px;color: var(--gw-on-night);font-size: 13px;font-weight: 600}.gw-header__actions{display: flex;align-items: center;gap: 8px;margin-left: auto}.gw-header__signin{display: none}.gw-header__actions > .gw-btn{display: none}.gw-theme-toggle,.gw-menu-toggle{display: inline-flex;align-items: center;justify-content: center;flex: none;width: 40px;height: 40px;padding: 0;border: 0;border-radius: var(--gw-radius-action);background: transparent;color: var(--gw-ink)}.gw-theme-toggle:hover,.gw-menu-toggle:hover{background: var(--gw-bg-soft)}.gw-theme-toggle svg,.gw-menu-toggle svg{width: 20px;height: 20px}.gw-menu-toggle[aria-expanded="true"] .gw-menu-toggle__open,.gw-menu-toggle:not([aria-expanded="true"]) .gw-menu-toggle__close{display: none}.gw-mobile-menu{position: fixed;top: calc(var(--gw-header-height) + 1px);left: 0;right: 0;bottom: 0;overflow-y: auto;overscroll-behavior: contain;padding-block: 8px 32px;background: var(--gw-bg)}html.is-menu-open{overflow: hidden}html.is-menu-open .gw-header{position: fixed;top: 0;left: 0;right: 0}.gw-mobile-menu__group > summary,.gw-mobile-menu__top{display: flex;align-items: center;justify-content: space-between;min-height: 48px;padding-block: 8px;border-bottom: 1px solid var(--gw-line);color: var(--gw-ink);font-size: 18px;font-weight: 500;list-style: none;cursor: pointer}.gw-mobile-menu__group > summary::-webkit-details-marker{display: none}.gw-mobile-menu__list{margin: 0;padding: 8px 0 16px;list-style: none}.gw-mobile-menu__link{display: flex;align-items: center;min-height: 44px;color: var(--gw-ink-2);font-size: 16px}.gw-mobile-menu__link[aria-current="page"],.gw-mobile-menu__top[aria-current="page"]{color: var(--gw-accent-strong);font-weight: 600}.gw-mobile-menu__actions{display: grid;gap: 8px;margin-top: 24px}@media (min-width: 480px){.gw-header__actions > .gw-btn:not(.gw-btn--secondary){display: inline-flex}}@media (min-width: 768px){.gw-header__actions > .gw-btn--secondary{display: inline-flex}}@media (min-width: 1080px){.gw-header__bar{gap: 32px}.gw-nav{display: block}.gw-header__signin{display: inline-flex}.gw-menu-toggle,.gw-mobile-menu{display: none !important}html.is-menu-open{overflow: visible}html.is-menu-open .gw-header{position: sticky}}@media (max-width: 768px),(pointer: coarse){.gw-theme-toggle,.gw-menu-toggle{width: var(--gw-tap-min);height: var(--gw-tap-min)}}@media (min-width: 1080px){.gw-header--float{top: var(--gw-header-gap);width: min(100% - 32px,var(--gw-wrap) + 50px);margin: var(--gw-header-gap) auto 0;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: color-mix(in srgb,var(--gw-surface) var(--gw-header-surface),transparent);-webkit-backdrop-filter: blur(var(--gw-header-blur));backdrop-filter: blur(var(--gw-header-blur));box-shadow: var(--gw-shadow-raised)}.gw-header--float.is-scrolled{border-color: var(--gw-line-strong)}.gw-header--float > .gw-header__bar{width: auto;padding-inline: 24px}.gw-header--float .gw-mega{max-height: calc(100vh - var(--gw-header-height) - var(--gw-header-gap) - 24px)}:root:has(.gw-header--float) [id]{scroll-margin-top: calc(var(--gw-header-height) + var(--gw-header-gap) + 18px)}}@media (min-width: 1080px) and (prefers-reduced-transparency: reduce){.gw-header--float{background: var(--gw-surface);-webkit-backdrop-filter: none;backdrop-filter: none}}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.gw-header--float{background: var(--gw-surface)}}.gw-footer{padding-block: 64px 40px;border-top: 1px solid var(--gw-line);background: var(--gw-bg);color: var(--gw-ink);font-size: 14px}.gw-footer__grid{display: grid;grid-template-columns: repeat(2,minmax(0,1fr));gap: 32px 24px}.gw-footer__brand{grid-column: 1 / -1;max-width: 320px}.gw-footer__brand p{margin-top: 16px;color: var(--gw-muted)}.gw-footer__heading{margin: 0 0 8px;color: var(--gw-ink);font-size: 13px;font-weight: 600;letter-spacing: 0}.gw-footer__col ul{margin: 0;padding: 0;list-style: none}.gw-footer__col a{display: block;padding-block: 6px;color: var(--gw-muted)}.gw-footer__col a:hover{color: var(--gw-ink)}.gw-footer__col a[aria-current="page"]{color: var(--gw-accent-strong)}.gw-footer__bottom{display: flex;flex-wrap: wrap;align-items: center;justify-content: space-between;gap: 8px 24px;margin-top: 48px;padding-top: 24px;border-top: 1px solid var(--gw-line);color: var(--gw-muted);font-size: 13px}.gw-footer__bottom:first-child{margin-top: 0;padding-top: 0;border-top: 0}.gw-footer__bottom ul{display: flex;flex-wrap: wrap;gap: 0 16px;margin: 0;padding: 0;list-style: none}.gw-footer__bottom a{display: inline-flex;align-items: center;justify-content: center;min-width: 24px;min-height: 24px;color: var(--gw-muted)}.gw-footer__bottom a:hover{color: var(--gw-ink)}@media (min-width: 768px){.gw-footer__grid{grid-template-columns: repeat(3,minmax(0,1fr))}}@media (min-width: 1080px){.gw-footer__grid{grid-auto-flow: column;grid-template-columns: minmax(0,1.6fr);grid-auto-columns: minmax(0,1fr)}.gw-footer__brand{grid-column: auto}}@media (max-width: 768px),(pointer: coarse){.gw-footer__bottom a{min-height: var(--gw-tap-min)}}.gw-hero{position: relative;isolation: isolate;padding-block: 48px 64px}.gw-hero__wash{position: absolute;inset: 0;z-index: -1;pointer-events: none;background: radial-gradient(640px 460px at 88% -10%,color-mix(in srgb,var(--gw-accent) 16%,transparent),transparent 62%),radial-gradient(560px 480px at 6% 108%,color-mix(in srgb,var(--gw-wash-violet) 12%,transparent),transparent 62%),radial-gradient(420px 320px at 52% 118%,color-mix(in srgb,var(--gw-wash-mint) 10%,transparent),transparent 62%)}.gw-hero h1 .gw-dim{display: block}.gw-hero > .gw-wrap > .gw-window{margin-top: 48px}.gw-hero--center{text-align: center}.gw-hero--center > .gw-wrap > *{margin-inline: auto}.gw-hero--center .gw-actions{justify-content: center}.gw-hero--compact{padding-block: 40px 48px}.gw-hero--compact h1{font-size: var(--gw-display-h2)}@media (min-width: 768px){.gw-hero{padding-block: 80px 96px}.gw-hero--compact{padding-block: 64px}.gw-hero .gw-lead{font-size: 20px}.gw-hero > .gw-wrap > .gw-window{margin-top: 64px}}@media (max-width: 479px){:is(.gw-hero,.gw-cta) .gw-actions > .gw-btn{flex: 1 1 100%}}.gw-logos{padding-block: 40px;border-block: 1px solid var(--gw-line)}.gw-logos p{color: var(--gw-muted);font-size: 13px;text-align: center}.gw-logos ul{display: flex;flex-wrap: wrap;align-items: center;justify-content: center;gap: 16px 40px;margin-top: 20px;padding: 0;list-style: none}.gw-logos li{color: var(--gw-muted);font-size: 22px;font-weight: 600;letter-spacing: -0.02em;line-height: 1.2}.gw-logos img{width: auto;height: 28px}.gw-stats{display: grid;grid-template-columns: minmax(0,1fr);gap: 24px;margin: 0;padding: 0;list-style: none}.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}.gw-stats__label{margin-top: 8px;max-width: 28ch;color: var(--gw-muted);font-size: 14px}.gw-stats__source{margin-top: 8px;color: var(--gw-muted);font-size: 12px}@media (min-width: 480px){.gw-stats{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 1080px){.gw-stats{grid-template-columns: none;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr);gap: 0}.gw-stats__item{padding: 4px 24px;border-left: 1px solid var(--gw-line)}.gw-stats__item:first-child{padding-left: 0;border-left: 0}}.gw-features{row-gap: 40px}.gw-feature .gw-icon{width: 40px;height: 40px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-action);background: var(--gw-surface);color: var(--gw-accent)}.gw-split{display: grid;grid-template-columns: minmax(0,1fr);align-items: center;gap: 40px}@media (min-width: 768px){.gw-split{grid-template-columns: repeat(2,minmax(0,1fr))}.gw-split--reverse > :first-child{order: 2}}.gw-split h2{font-size: 30px}@media (min-width: 1080px){.gw-split{gap: 64px}}.gw-showcase .gw-window{max-width: 960px;margin-inline: auto}.gw-showcase .gw-window + *{margin-top: 48px}.gw-steps{display: grid;grid-template-columns: minmax(0,1fr);gap: 32px 24px;margin: 0;padding: 0;list-style: none;counter-reset: gw-step}.gw-steps > li{counter-increment: gw-step}.gw-steps > li::before{content: counter(gw-step,decimal-leading-zero);display: block;margin-bottom: 16px;padding-bottom: 12px;border-bottom: 1px solid var(--gw-line);color: var(--gw-accent-strong);font-size: 13px;font-weight: 600;font-variant-numeric: tabular-nums}.gw-steps h3{font-size: 20px}.gw-steps p{margin-top: 8px;color: var(--gw-muted);font-size: 16px}@media (min-width: 768px){.gw-steps{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 1080px){.gw-steps{grid-template-columns: none;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr)}}.gw-quote{max-width: 880px;margin-inline: auto;padding: 40px 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: var(--gw-surface);text-align: center}.gw-quote::before{content: "";display: block;width: 32px;height: 24px;margin: 0 auto 24px;background: var(--gw-accent);-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M0 24V14C0 6 4 1 12 0l1 4c-4 1-6 4-6 8h5v12zm19 0V14c0-8 4-13 12-14l1 4c-4 1-6 4-6 8h5v12z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M0 24V14C0 6 4 1 12 0l1 4c-4 1-6 4-6 8h5v12zm19 0V14c0-8 4-13 12-14l1 4c-4 1-6 4-6 8h5v12z'/%3E%3C/svg%3E") center / contain no-repeat}.gw-quote blockquote{font-size: 24px;font-weight: 500;line-height: 1.4;letter-spacing: -0.01em;text-wrap: balance}.gw-quote figcaption{margin-top: 24px;color: var(--gw-muted);font-size: 14px}.gw-quote figcaption b{color: var(--gw-ink)}.gw-quote figcaption img{width: 40px;height: 40px;margin: 0 auto 12px;border-radius: var(--gw-radius-chip)}@media (min-width: 768px){.gw-quote{padding: 64px}.gw-quote blockquote{font-size: 30px}}.gw-pricing{display: grid;grid-template-columns: repeat(auto-fit,minmax(min(100%,260px),1fr));align-items: start;gap: 16px}.gw-plan{display: flex;flex-direction: column;padding: 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: var(--gw-surface);box-shadow: var(--gw-shadow)}.gw-plan--featured{border-color: var(--gw-accent)}.gw-plan{position: relative}.gw-plan > .gw-badge{align-self: flex-start}.gw-plan > .gw-badge:first-child{position: absolute;top: -12px;left: 24px}.gw-plan > .gw-badge:first-child + *{margin-top: 0}.gw-plan h3{font-size: 20px}:where(.gw-plan) p{color: var(--gw-muted);font-size: 14px}.gw-plan__price{display: flex;flex-wrap: wrap;align-items: baseline;gap: 6px;color: var(--gw-ink);font-size: var(--gw-display-h2);font-weight: 600;letter-spacing: -0.03em;line-height: 1.1;font-variant-numeric: tabular-nums}.gw-plan__price small{color: var(--gw-muted);font-size: 14px;font-weight: 500;letter-spacing: 0}.gw-plan .gw-list--check{font-size: 14px}@media (min-width: 768px){.gw-plan{padding: 32px}.gw-plan > .gw-badge:first-child{left: 32px}}.gw-cta{max-width: 780px;margin-inline: auto;text-align: center}.gw-cta .gw-lead{margin-inline: auto}.gw-cta .gw-actions{justify-content: center}.gw-prose{max-width: 68ch;min-width: 0;color: var(--gw-ink);font-size: var(--gw-body-size);line-height: var(--gw-body-leading);overflow-wrap: break-word}.gw-prose--sm{font-size: 16px}:where(.gw-prose) > * + *{margin-top: 20px}:where(.gw-prose--sm) > * + *{margin-top: 16px}:where(.gw-prose) > * + :where(figure,img,video,pre,table,hr,.gw-table,.gw-window),:where(.gw-prose) > :where(figure,img,video,pre,table,hr,.gw-table,.gw-window) + *{margin-top: 32px}:where(.gw-prose) > * + h2{margin-top: 48px}:where(.gw-prose) > * + h3{margin-top: 40px}:where(.gw-prose) > * + h4{margin-top: 32px}:where(.gw-prose--sm) > * + h2{margin-top: 40px}:where(.gw-prose--sm) > * + h3{margin-top: 32px}:where(.gw-prose--sm) > * + h4{margin-top: 24px}:where(.gw-prose) > h1 + *{margin-top: 24px}:where(.gw-prose) > :is(h2,h3,h4) + *{margin-top: 12px}:where(.gw-prose) > :is(h1,h2,h3,h4){color: var(--gw-ink);font-weight: var(--gw-heading-weight);letter-spacing: var(--gw-heading-tracking);line-height: 1.25}:where(.gw-prose) > h1{font-size: var(--gw-display-h2);font-weight: var(--gw-display-weight);letter-spacing: var(--gw-display-tracking);line-height: var(--gw-heading-leading)}:where(.gw-prose) > h2{font-size: 30px;line-height: 1.2}:where(.gw-prose) > h3{font-size: 24px}:where(.gw-prose) > h4{font-size: 20px}:where(.gw-prose--sm) > h2{font-size: 24px}:where(.gw-prose--sm) > h3{font-size: 20px}:where(.gw-prose--sm) > h4{font-size: 18px}:where(.gw-prose :is(ul,ol):not([class])){padding-left: 24px}:where(.gw-prose ul:not([class])){list-style: disc}:where(.gw-prose ol:not([class])){list-style: decimal}:where(.gw-prose :is(ul,ol):not([class]) > li + li),:where(.gw-prose :is(ul,ol):not([class]) > li > * + *),:where(.gw-prose :is(ul,ol):not([class]) > li > :is(ul,ol)){margin-top: 8px}:where(.gw-prose :is(ul,ol):not([class]) > li)::marker{color: var(--gw-muted);font-variant-numeric: tabular-nums}:where(.gw-prose) > blockquote{padding-left: 20px;border-left: 3px solid var(--gw-line-strong);color: var(--gw-muted)}:where(.gw-prose) > blockquote > * + *{margin-top: 12px}:where(.gw-prose) > hr{height: 0;border: 0;border-top: 1px solid var(--gw-line)}:where(.gw-prose) :is(sup,sub){line-height: 0}:where(.gw-prose) :is(pre,code,kbd,samp){font-family: var(--gw-font-mono)}:where(.gw-prose) code{padding: 2px 6px;border-radius: var(--gw-radius-6);background: color-mix(in srgb,var(--gw-ink) 8%,transparent);color: var(--gw-ink);font-size: 16px;overflow-wrap: anywhere;-webkit-box-decoration-break: clone;box-decoration-break: clone}:where(.gw-prose--sm) code{font-size: 14px}:where(.gw-prose) :is(h1,h2,h3,h4) code{font-size: inherit}:where(.gw-prose) figcaption code{font-size: 13px}:where(.gw-prose) :is(th,td) code{font-size: 13px;overflow-wrap: normal;white-space: nowrap}:where(.gw-prose) pre{overflow-x: auto;padding: 16px 20px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 14px;line-height: 1.6;tab-size: 2}:where(.gw-prose) pre code{padding: 0;border-radius: var(--gw-radius-0);background: none;font-size: inherit;overflow-wrap: normal}:where(.gw-prose) > :is(img,video),:where(.gw-prose) > :is(p,figure) > :is(img,video){border-radius: var(--gw-radius-card)}:where(.gw-prose) figcaption{margin-top: 12px;color: var(--gw-muted);font-size: 14px;line-height: 1.5}:where(.gw-prose) > table{display: block;width: max-content;max-width: 100%;overflow-x: auto;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);border-collapse: collapse;font-size: 14px;line-height: 1.5;-webkit-overflow-scrolling: touch}:where(.gw-prose) > table :is(th,td){padding: 12px 16px;border-bottom: 1px solid var(--gw-line);text-align: left;vertical-align: top}:where(.gw-prose) > table thead th{background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 13px;font-weight: 600;white-space: nowrap}:where(.gw-prose) > table tbody th{font-weight: 500}:where(.gw-prose) > table td{color: var(--gw-ink-2);font-variant-numeric: tabular-nums}:where(.gw-prose) > table tbody tr:last-child > *{border-bottom: 0}:where(.gw-prose) > table caption{padding: 12px 16px;color: var(--gw-muted);font-size: 13px;text-align: left}@media (max-width: 767px){:where(.gw-prose) > table td{min-width: 160px}}
1
+ /*! @gemboss/website-ui 0.4.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */ :root{color-scheme: light;--gw-font: Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--gw-font-mono: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--gw-text-11: 11px;--gw-text-12: 12px;--gw-text-13: 13px;--gw-text-14: 14px;--gw-text-16: 16px;--gw-text-18: 18px;--gw-text-20: 20px;--gw-text-22: 22px;--gw-text-24: 24px;--gw-text-30: 30px;--gw-display-h1: clamp(40px,4.6vw,68px);--gw-display-h2: clamp(32px,3.4vw,52px);--gw-display-weight: 600;--gw-display-tracking: -0.03em;--gw-heading-weight: 650;--gw-heading-tracking: -0.022em;--gw-heading-leading: 1.08;--gw-body-size: 18px;--gw-body-leading: 1.6;--gw-space-0: 0;--gw-space-2: 2px;--gw-space-4: 4px;--gw-space-6: 6px;--gw-space-8: 8px;--gw-space-12: 12px;--gw-space-16: 16px;--gw-space-20: 20px;--gw-space-24: 24px;--gw-space-32: 32px;--gw-space-40: 40px;--gw-space-48: 48px;--gw-space-64: 64px;--gw-space-80: 80px;--gw-space-96: 96px;--gw-space-120: 120px;--gw-space-section: clamp(56px,38px + 5vw,110px);--gw-radius-0: 0;--gw-radius-4: 4px;--gw-radius-6: 6px;--gw-radius-8: 8px;--gw-radius-12: 12px;--gw-radius-16: 16px;--gw-radius-20: 20px;--gw-radius-24: 24px;--gw-radius-999: 999px;--gw-radius-action: 8px;--gw-radius-chip: 999px;--gw-radius-card: 12px;--gw-radius-panel: 16px;--gw-radius-popover: 20px;--gw-shadow: 0 1px 2px rgba(31,30,29,.04),0 2px 6px rgba(31,30,29,.06);--gw-shadow-sm: 0 1px 2px rgba(31,30,29,.04);--gw-shadow-raised: 0 4px 16px rgba(31,30,29,.10);--gw-wrap: 1120px;--gw-wrap-narrow: 780px;--gw-gutter: 22px;--gw-header-height: 64px;--gw-header-gap: 16px;--gw-header-surface: 85%;--gw-header-blur: 10px;--gw-button-height: 40px;--gw-tap-min: 44px;--gw-ease: cubic-bezier(.22,1,.36,1);--gw-reveal-ms: 700ms;--gw-spin-ms: 700ms;--gw-wash-pink: #ff6f5e;--gw-wash-violet: #7c8af7;--gw-wash-sky: #43b0ff;--gw-wash-mint: #1fcf9f;--gw-accent-light: #5661f5;--gw-accent-strong-light: #3a44de;--gw-accent-soft-light: #eef0fe;--gw-on-accent-light: #ffffff;--gw-accent-dark: #7c84ff;--gw-accent-strong-dark: #a3a9ff;--gw-accent-soft-dark: #1f2140;--gw-on-accent-dark: #0d0d12;--gw-bg: #ffffff;--gw-bg-soft: #f2f3f5;--gw-surface: #ffffff;--gw-ink: #1b1c1e;--gw-ink-2: #3b3d42;--gw-muted: #686b71;--gw-line: #e5e7ea;--gw-line-strong: #d5d8dd;--gw-action: #1b1c1e;--gw-action-hover: #000000;--gw-on-action: #ffffff;--gw-accent: var(--gw-accent-light);--gw-accent-strong: var(--gw-accent-strong-light);--gw-accent-soft: var(--gw-accent-soft-light);--gw-on-accent: var(--gw-on-accent-light);--gw-success: #007f5f;--gw-success-soft: #e3f1df;--gw-warning: #9a6700;--gw-night: #0c0e14;--gw-night-2: #141722;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){color-scheme: dark;--gw-bg: #0d0d12;--gw-bg-soft: #121318;--gw-surface: #16171d;--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341;--gw-night: #0a0b10;--gw-night-2: #12141c;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}}:root[data-theme="dark"]{color-scheme: dark;--gw-bg: #0d0d12;--gw-bg-soft: #121318;--gw-surface: #16171d;--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341;--gw-night: #0a0b10;--gw-night-2: #12141c;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}:root[data-theme="light"]{color-scheme: light;--gw-bg: #ffffff;--gw-bg-soft: #f2f3f5;--gw-surface: #ffffff;--gw-ink: #1b1c1e;--gw-ink-2: #3b3d42;--gw-muted: #686b71;--gw-line: #e5e7ea;--gw-line-strong: #d5d8dd;--gw-action: #1b1c1e;--gw-action-hover: #000000;--gw-on-action: #ffffff;--gw-accent: var(--gw-accent-light);--gw-accent-strong: var(--gw-accent-strong-light);--gw-accent-soft: var(--gw-accent-soft-light);--gw-on-accent: var(--gw-on-accent-light);--gw-success: #007f5f;--gw-success-soft: #e3f1df;--gw-warning: #9a6700;--gw-night: #0c0e14;--gw-night-2: #141722;--gw-on-night: #e3e4e8;--gw-on-night-muted: #9aa3ff}.gw-band--night,.gw-showcase{color-scheme: dark;--gw-bg: var(--gw-night);--gw-bg-soft: var(--gw-night-2);--gw-surface: var(--gw-night-2);--gw-ink: #e3e4e8;--gw-ink-2: #c4c6cc;--gw-muted: #9a9ca6;--gw-line: #2a2b33;--gw-line-strong: #383944;--gw-action: #f2f3f5;--gw-action-hover: #ffffff;--gw-on-action: #0d0d12;--gw-accent: var(--gw-accent-dark);--gw-accent-strong: var(--gw-accent-strong-dark);--gw-accent-soft: var(--gw-accent-soft-dark);--gw-on-accent: var(--gw-on-accent-dark);--gw-success: #34c38f;--gw-success-soft: #132a22;--gw-warning: #e0b341}:root[data-gw-action="accent"],:root[data-gw-action="accent"] :is(.gw-band--night,.gw-showcase){--gw-action: var(--gw-accent);--gw-action-hover: var(--gw-accent-strong);--gw-on-action: var(--gw-on-accent)}:root[data-gw-radius="soft"]{--gw-radius-action: 12px;--gw-radius-card: 20px;--gw-radius-panel: 24px;--gw-radius-popover: 24px}*,*::before,*::after{box-sizing: border-box}[hidden]{display: none !important}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%}body{margin: 0;background: var(--gw-bg);color: var(--gw-ink);font-family: var(--gw-font);font-size: var(--gw-body-size);line-height: var(--gw-body-leading);-webkit-font-smoothing: antialiased;text-rendering: optimizeLegibility}:where(h1,h2,h3,h4,h5,h6,p,ul,ol,dl,dd,figure,blockquote){margin: 0}img,svg,video{display: block;max-width: 100%}img,video{height: auto}button,input,select,textarea{font: inherit;color: inherit}button{cursor: pointer}a{color: inherit;text-decoration: none}b,strong{font-weight: 600}.gw-page{display: flex;flex-direction: column;min-height: 100vh;min-height: 100dvh}.gw-page > main{flex: 1 0 auto}h1,h2,h3,h4,h5,h6,.gw-h1,.gw-h2,.gw-h3,.gw-h4{font-weight: var(--gw-heading-weight);letter-spacing: var(--gw-heading-tracking);line-height: var(--gw-heading-leading);text-wrap: balance}h1,.gw-h1,h2,.gw-h2{font-weight: var(--gw-display-weight);letter-spacing: var(--gw-display-tracking)}h1,.gw-h1{font-size: var(--gw-display-h1)}h2,.gw-h2{font-size: var(--gw-display-h2)}h3,.gw-h3{font-size: 24px}h4,.gw-h4{font-size: 20px;font-weight: 600;letter-spacing: -0.01em}h5,h6{font-size: 16px;font-weight: 600;letter-spacing: 0}.gw-lead{font-size: 18px;line-height: 1.55;color: var(--gw-muted);max-width: 60ch}.gw-note{font-size: 14px;color: var(--gw-muted)}.gw-dim{color: var(--gw-muted)}.gw-label{display: inline-flex;align-items: center;gap: 8px;font-size: 13px;font-weight: 500;line-height: 1.4;color: var(--gw-muted)}.gw-label::before{content: "";flex: none;width: 8px;height: 8px;border-radius: var(--gw-radius-0);background: var(--gw-accent)}:where(p,dd,td,figcaption,.gw-list--check li,.gw-prose) a:not([class]),.gw-link{color: var(--gw-accent-strong);font-weight: 500;text-decoration: underline;text-decoration-thickness: 1px;text-underline-offset: 4px;text-decoration-color: color-mix(in srgb,currentColor 40%,transparent)}:where(p,dd,td,figcaption,.gw-list--check li,.gw-prose) a:not([class]):hover,.gw-link:hover{text-decoration-color: currentColor}:focus-visible{outline: 2px solid var(--gw-accent);outline-offset: 2px}[id]{scroll-margin-top: calc(var(--gw-header-height) + 16px)}.gw-skip{position: absolute;left: -9999px;top: 8px;z-index: 100;padding: 12px 16px;border-radius: var(--gw-radius-action);background: var(--gw-action);color: var(--gw-on-action);font-size: 14px;font-weight: 600}.gw-skip:focus{left: 16px}.gw-reveal{opacity: 0;transform: translateY(24px);transition: opacity var(--gw-reveal-ms) var(--gw-ease),transform var(--gw-reveal-ms) var(--gw-ease);animation: gw-reveal-failsafe 1ms linear 2.5s forwards}.gw-reveal.is-in{opacity: 1;transform: none}@keyframes gw-reveal-failsafe{to{opacity: 1;transform: none}}@media (prefers-reduced-motion: reduce){*,*::before,*::after{scroll-behavior: auto !important}.gw-reveal{opacity: 1;transform: none;transition: none;animation: none}}.gw-wrap,.gw-wrap--narrow{width: min(100% - 2 * var(--gw-gutter),var(--gw-wrap));margin-inline: auto}.gw-wrap--narrow{max-width: var(--gw-wrap-narrow)}.gw-section{padding-block: var(--gw-space-section)}.gw-section--soft{background: var(--gw-bg-soft)}.gw-section--tight{padding-block: 40px}@media (min-width: 768px){.gw-section--tight{padding-block: 64px}}.gw-section__head{max-width: 660px}.gw-section__head--center{margin-inline: auto;text-align: center}.gw-section__head--center .gw-lead{margin-inline: auto}.gw-section__head:not(:last-child){margin-bottom: 40px}@media (min-width: 768px){.gw-section__head:not(:last-child){margin-bottom: 48px}}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-feature,.gw-plan) > * + *{margin-top: 16px}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-plan) > :is(.gw-actions,.gw-segmented){margin-top: 32px}:where(.gw-section__head,.gw-hero > .gw-wrap,.gw-split > :not(.gw-window),.gw-cta,.gw-card,.gw-plan) > .gw-list--check{margin-top: 24px}.gw-actions{display: flex;flex-wrap: wrap;align-items: center;gap: 12px}.gw-grid,.gw-grid--2,.gw-grid--3,.gw-grid--4{display: grid;grid-template-columns: minmax(0,1fr);gap: 16px}@media (min-width: 480px){.gw-grid--4{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.gw-grid,.gw-grid--2,.gw-grid--3,.gw-grid--4{gap: 24px}.gw-grid--2{grid-template-columns: repeat(2,minmax(0,1fr))}.gw-grid--3{grid-template-columns: repeat(3,minmax(0,1fr))}}@media (min-width: 1080px){.gw-grid--4{grid-template-columns: repeat(4,minmax(0,1fr))}}.gw-band--night,.gw-showcase{position: relative;isolation: isolate;color: var(--gw-ink);background: radial-gradient(800px 360px at 50% 0%,color-mix(in srgb,var(--gw-accent) 22%,transparent),transparent 72%),var(--gw-night)}.gw-btn{position: relative;display: inline-flex;align-items: center;justify-content: center;gap: 8px;max-width: 100%;height: var(--gw-button-height);padding: 0 20px;border: 1px solid transparent;border-radius: var(--gw-radius-action);background: var(--gw-action);color: var(--gw-on-action);font-size: 14px;font-weight: 600;line-height: 1.4;letter-spacing: 0;text-align: center;white-space: nowrap;cursor: pointer}.gw-btn:hover{background: var(--gw-action-hover)}.gw-btn svg{flex: none;width: 16px;height: 16px}.gw-btn--secondary{background: transparent;border-color: var(--gw-line-strong);color: var(--gw-ink)}.gw-btn--secondary:hover{background: transparent;border-color: var(--gw-ink)}.gw-btn--ghost{background: transparent;color: var(--gw-ink)}.gw-btn--ghost:hover{background: var(--gw-bg-soft)}.gw-btn--on-night{background: var(--gw-on-night);color: var(--gw-night)}.gw-btn--on-night:hover{background: var(--gw-on-night);opacity: 0.88}.gw-btn--sm{height: 36px;padding: 0 16px}.gw-btn--sm::after{content: "";position: absolute;top: -6px;right: 0;bottom: -6px;left: 0}.gw-btn--lg{height: 48px;padding: 0 24px;font-size: 16px}.gw-btn--block{display: flex;width: 100%}.gw-btn:disabled,.gw-btn[aria-disabled="true"]{opacity: 0.48;cursor: not-allowed;pointer-events: none}.gw-btn:active{background: var(--gw-action-hover)}.gw-btn--secondary:active{background: var(--gw-bg-soft);border-color: var(--gw-ink)}.gw-btn--ghost:active{background: var(--gw-line)}.gw-btn--on-night:active{background: var(--gw-on-night);opacity: 0.76}.gw-btn[aria-busy="true"]{cursor: progress;-webkit-text-fill-color: transparent}.gw-btn[aria-busy="true"] svg{opacity: 0}.gw-btn[aria-busy="true"]::before{content: "";position: absolute;top: 50%;left: 50%;width: 16px;height: 16px;margin: -8px 0 0 -8px;border: 2px solid color-mix(in srgb,currentColor 18%,transparent);border-top-color: currentColor;border-radius: var(--gw-radius-chip);animation: gw-spin var(--gw-spin-ms) linear infinite}@keyframes gw-spin{to{transform: rotate(360deg)}}@media (prefers-reduced-motion: reduce){.gw-btn[aria-busy="true"]::before{border: 0;border-radius: var(--gw-radius-0);background: radial-gradient(circle at 3px 50%,currentColor 1.5px,transparent 2px),radial-gradient(circle at 8px 50%,currentColor 1.5px,transparent 2px),radial-gradient(circle at 13px 50%,currentColor 1.5px,transparent 2px);animation: none}}.gw-link--arrow{display: inline-flex;align-items: center;gap: 6px;min-height: 24px;font-weight: 600;text-decoration: none}.gw-link--arrow::after{content: "\2192";transition: transform 0.18s var(--gw-ease)}.gw-link--arrow:hover::after{transform: translateX(2px)}.gw-chip{display: inline-flex;align-items: center;gap: 6px;min-height: 32px;padding: 4px 12px;border: 1px solid var(--gw-line-strong);border-radius: var(--gw-radius-chip);background: var(--gw-surface);color: var(--gw-ink-2);font-size: 13px;font-weight: 500;line-height: 1.4;white-space: nowrap}:is(a,button,label).gw-chip{cursor: pointer}:is(a,button,label).gw-chip:hover{border-color: var(--gw-ink);color: var(--gw-ink)}.gw-chip.is-active,.gw-chip[aria-pressed="true"],.gw-chip[aria-selected="true"]{border-color: var(--gw-accent);background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-segmented{display: inline-flex;max-width: 100%;border: 1px solid var(--gw-line-strong);border-radius: var(--gw-radius-action);background: var(--gw-surface);overflow: hidden;overflow: clip;vertical-align: middle}.gw-segmented--block{display: flex;width: 100%}.gw-segmented > button{display: inline-flex;flex: 0 1 auto;align-items: center;justify-content: center;min-width: 0;height: calc(var(--gw-button-height) - 2px);padding: 0 16px;border: 0;background: transparent;color: var(--gw-muted);font-size: 14px;font-weight: 500;line-height: 1.4;white-space: nowrap;cursor: pointer}.gw-segmented--block > button{flex: 1 1 0}.gw-segmented > button + button{border-left: 1px solid var(--gw-line-strong)}.gw-segmented > button:hover{background: var(--gw-bg-soft);color: var(--gw-ink)}.gw-segmented > button:active{background: var(--gw-line)}.gw-segmented > button:focus-visible{outline-offset: -2px}.gw-segmented > button[aria-checked="true"]{background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-badge{display: inline-flex;align-items: center;gap: 6px;min-height: 24px;padding: 2px 8px;border-radius: var(--gw-radius-chip);background: color-mix(in srgb,var(--gw-ink) 8%,transparent);color: var(--gw-ink-2);font-size: 12px;font-weight: 500;line-height: 1.4;white-space: nowrap;vertical-align: middle}.gw-badge--accent{background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-badge--success{background: var(--gw-success-soft);color: var(--gw-ink)}.gw-badge--success::before,.gw-badge--warning::before{content: "";flex: none;width: 6px;height: 6px;border-radius: var(--gw-radius-chip);background: var(--gw-success)}.gw-badge--warning::before{background: var(--gw-warning)}.gw-card{padding: 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);box-shadow: var(--gw-shadow)}@media (min-width: 768px){.gw-card{padding: 32px}}.gw-card--soft{background: var(--gw-bg-soft);box-shadow: none}.gw-card--link{display: block;color: inherit}.gw-card--link:hover{border-color: var(--gw-line-strong)}:where(.gw-card,.gw-feature,.gw-plan) > :is(h3,h4) + p{margin-top: 8px}:where(.gw-card,.gw-feature) h3{font-size: 20px}:where(.gw-card,.gw-feature) p{font-size: 16px;color: var(--gw-muted)}.gw-window{overflow: hidden;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);color: var(--gw-ink);box-shadow: var(--gw-shadow);font-size: 13px;line-height: 1.5}.gw-window__bar{display: flex;align-items: center;gap: 12px;min-height: 40px;padding: 8px 16px;border-bottom: 1px solid var(--gw-line);color: var(--gw-muted);font-size: 12px}.gw-window__bar::before{content: "";flex: none;width: 36px;height: 8px;background: radial-gradient(circle at 4px 4px,var(--gw-line-strong) 4px,transparent 4px),radial-gradient(circle at 18px 4px,var(--gw-line-strong) 4px,transparent 4px),radial-gradient(circle at 32px 4px,var(--gw-line-strong) 4px,transparent 4px)}.gw-window__body{padding: 16px}.gw-window__body:has(> img:only-child){padding: 0}.gw-window__body > img{width: 100%}.gw-window__caption{margin: 0;padding: 0 16px 12px;color: var(--gw-muted);font-size: 12px;line-height: 1.5;text-align: end}.gw-window__body:has(> img:only-child) + .gw-window__caption{padding-top: 12px}.gw-icon{display: inline-flex;align-items: center;justify-content: center;flex: none;width: 20px;height: 20px}.gw-icon svg{width: 20px;height: 20px}.gw-list--check{display: grid;gap: 12px;padding: 0;list-style: none}.gw-list--check > li{position: relative;padding-left: 24px}.gw-list--check > li::before{content: "";position: absolute;left: 0;top: 6px;top: calc((1lh - 16px) / 2);width: 16px;height: 16px;background: var(--gw-accent);-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat}.gw-table{overflow-x: auto;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);-webkit-overflow-scrolling: touch}.gw-table table{width: 100%;border-collapse: collapse;font-size: 14px;line-height: 1.5}.gw-table caption{padding: 16px;text-align: left;font-size: 13px;color: var(--gw-muted)}.gw-table th,.gw-table td{padding: 12px 16px;border-bottom: 1px solid var(--gw-line);text-align: left;vertical-align: top}.gw-table thead th{background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 13px;font-weight: 600;white-space: nowrap}.gw-table tbody th{font-weight: 500}.gw-table tbody tr:last-child > *{border-bottom: 0}.gw-table td{color: var(--gw-ink-2);font-variant-numeric: tabular-nums}.gw-table--compare table{min-width: 640px}.gw-table--compare td{text-align: center}.gw-table--compare thead th:not(:first-child){text-align: center}.gw-table--compare tbody th{position: sticky;left: 0;z-index: 1;background: var(--gw-surface);min-width: 160px}.gw-faq{display: grid;gap: 12px}.gw-faq__item{padding: 0 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface)}.gw-faq__item[open]{border-color: var(--gw-line-strong)}.gw-faq__item > summary{display: flex;align-items: center;justify-content: space-between;gap: 16px;min-height: 44px;padding: 20px 0;font-size: 18px;font-weight: 600;line-height: 1.4;list-style: none;cursor: pointer}.gw-faq__item > summary::-webkit-details-marker{display: none}.gw-faq__item > summary::after{content: "";flex: none;width: 16px;height: 16px;background: linear-gradient(var(--gw-accent),var(--gw-accent)) center / 16px 2px no-repeat,linear-gradient(var(--gw-accent),var(--gw-accent)) center / 2px 16px no-repeat;transition: transform 0.2s var(--gw-ease)}.gw-faq__item[open] > summary::after{transform: rotate(45deg)}.gw-faq__item > :not(summary){margin: 0 0 20px;max-width: 68ch;color: var(--gw-muted);font-size: 16px}@media (max-width: 768px),(pointer: coarse){.gw-btn,.gw-btn--sm,.gw-btn--lg{height: auto;min-height: var(--gw-tap-min);padding-block: 8px;white-space: normal;text-wrap: balance}.gw-btn--lg{min-height: 48px}:is(a,button,label).gw-chip{min-height: var(--gw-tap-min)}.gw-segmented > button{height: auto;min-height: var(--gw-tap-min);padding-block: 8px;white-space: normal;text-wrap: balance}}.gw-announce{padding: 8px var(--gw-gutter);border-bottom: 1px solid var(--gw-line);background: var(--gw-bg-soft);color: var(--gw-ink-2);font-size: 13px;line-height: 1.5;text-align: center}.gw-announce a{color: var(--gw-ink);font-weight: 600;text-decoration: underline;text-underline-offset: 4px}.gw-header{position: sticky;top: 0;z-index: 50;border-bottom: 1px solid var(--gw-line);background: var(--gw-surface)}.gw-header.is-scrolled{border-bottom-color: var(--gw-line-strong)}.gw-header__bar{display: flex;align-items: center;gap: 16px;height: var(--gw-header-height)}.gw-brand{display: inline-flex;align-items: center;gap: 8px;flex: none;min-height: 44px;color: var(--gw-ink);font-size: 18px;font-weight: 700;letter-spacing: -0.01em;white-space: nowrap}.gw-brand__mark{flex: none;width: 26px;height: 26px}.gw-brand__name{line-height: 1.2}.gw-nav{display: none}.gw-nav__list{display: flex;align-items: center;gap: 4px;margin: 0;padding: 0;list-style: none}.gw-nav__trigger,.gw-nav__link{display: inline-flex;align-items: center;gap: 6px;height: 40px;padding: 0 12px;border: 0;border-radius: var(--gw-radius-action);background: transparent;color: var(--gw-ink-2);font-size: 14px;font-weight: 500;line-height: 1.4;white-space: nowrap;cursor: pointer}.gw-nav__trigger:hover,.gw-nav__link:hover,.gw-nav__item.is-open > .gw-nav__trigger{background: var(--gw-bg-soft);color: var(--gw-ink)}.gw-nav__trigger::after,.gw-mobile-menu__group > summary::after{content: "";flex: none;width: 12px;height: 12px;background: currentColor;opacity: 0.6;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;transition: transform 0.18s var(--gw-ease)}.gw-nav__trigger[aria-expanded="true"]::after,.gw-mobile-menu__group[open] > summary::after{transform: rotate(180deg)}.gw-nav__link[aria-current="page"]{color: var(--gw-ink);font-weight: 600;text-decoration: underline;text-decoration-color: var(--gw-accent);text-decoration-thickness: 2px;text-underline-offset: 8px}.gw-mega{position: absolute;top: calc(100% + 8px);left: 50%;width: min(960px,calc(100vw - 40px));max-height: calc(100vh - var(--gw-header-height) - 24px);overflow-y: auto;transform: translateX(-50%);border: 1px solid var(--gw-line);border-radius: var(--gw-radius-popover);background: var(--gw-surface);box-shadow: var(--gw-shadow-raised)}.gw-mega::before{content: "";position: absolute;top: -12px;left: 0;right: 0;height: 12px}.gw-mega:not([hidden]){animation: gw-mega-in 0.16s var(--gw-ease)}@keyframes gw-mega-in{from{opacity: 0;transform: translateX(-50%) translateY(8px)}}@media (prefers-reduced-motion: reduce){.gw-mega:not([hidden]){animation: none}}.gw-mega > .gw-wrap{width: auto;margin: 0}.gw-mega__grid{display: grid;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr);gap: 32px;padding: 32px}.gw-mega__col{display: flex;flex-direction: column;gap: 2px}.gw-mega__heading{padding: 0 8px 8px;color: var(--gw-muted);font-size: 12px;font-weight: 500}.gw-mega__link{display: grid;grid-template-columns: 32px minmax(0,1fr);align-items: start;gap: 12px;padding: 8px;border-radius: var(--gw-radius-action);color: var(--gw-ink)}.gw-mega__link:hover{background: var(--gw-bg-soft)}.gw-mega__link b{display: block;font-size: 14px;font-weight: 500;line-height: 20px}.gw-mega__link i{display: block;margin-top: 2px;color: var(--gw-muted);font-size: 13px;font-style: normal;line-height: 18px}.gw-mega__link[aria-current="page"] b{color: var(--gw-accent-strong);font-weight: 600}.gw-mega__icon{display: grid;place-items: center;width: 32px;height: 32px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-action);background: var(--gw-surface);color: var(--gw-muted)}.gw-mega__icon svg{width: 16px;height: 16px}.gw-mega__link:hover .gw-mega__icon{border-color: var(--gw-accent-soft);background: var(--gw-accent-soft);color: var(--gw-accent-strong)}.gw-mega__feature{display: flex;flex-direction: column;justify-content: flex-end;gap: 4px;min-height: 216px;padding: 24px;border-radius: var(--gw-radius-panel);background: radial-gradient(360px 260px at 12% 112%,color-mix(in srgb,var(--gw-accent) 78%,transparent),transparent 62%),radial-gradient(300px 220px at 92% -14%,color-mix(in srgb,var(--gw-wash-violet) 44%,transparent),transparent 70%),var(--gw-night);color: var(--gw-on-night)}.gw-mega__feature b{font-size: 18px;font-weight: 650;line-height: 1.25}.gw-mega__feature i{color: var(--gw-on-night);font-size: 13px;font-style: normal;line-height: 1.5;opacity: 0.9}.gw-mega__feature .gw-link--arrow{margin-top: 16px;color: var(--gw-on-night);font-size: 13px;font-weight: 600}.gw-header__actions{display: flex;align-items: center;gap: 8px;margin-left: auto}.gw-header__signin{display: none}.gw-header__actions > .gw-btn{display: none}.gw-theme-toggle,.gw-menu-toggle{display: inline-flex;align-items: center;justify-content: center;flex: none;width: 40px;height: 40px;padding: 0;border: 0;border-radius: var(--gw-radius-action);background: transparent;color: var(--gw-ink)}.gw-theme-toggle:hover,.gw-menu-toggle:hover{background: var(--gw-bg-soft)}.gw-theme-toggle svg,.gw-menu-toggle svg{width: 20px;height: 20px}.gw-menu-toggle[aria-expanded="true"] .gw-menu-toggle__open,.gw-menu-toggle:not([aria-expanded="true"]) .gw-menu-toggle__close{display: none}.gw-mobile-menu{position: fixed;top: calc(var(--gw-header-height) + 1px);left: 0;right: 0;bottom: 0;overflow-y: auto;overscroll-behavior: contain;padding-block: 8px 32px;background: var(--gw-bg)}html.is-menu-open{overflow: hidden}html.is-menu-open .gw-header{position: fixed;top: 0;left: 0;right: 0}.gw-mobile-menu__group > summary,.gw-mobile-menu__top{display: flex;align-items: center;justify-content: space-between;min-height: 48px;padding-block: 8px;border-bottom: 1px solid var(--gw-line);color: var(--gw-ink);font-size: 18px;font-weight: 500;list-style: none;cursor: pointer}.gw-mobile-menu__group > summary::-webkit-details-marker{display: none}.gw-mobile-menu__list{margin: 0;padding: 8px 0 16px;list-style: none}.gw-mobile-menu__link{display: flex;align-items: center;min-height: 44px;color: var(--gw-ink-2);font-size: 16px}.gw-mobile-menu__link[aria-current="page"],.gw-mobile-menu__top[aria-current="page"]{color: var(--gw-accent-strong);font-weight: 600}.gw-mobile-menu__actions{display: grid;gap: 8px;margin-top: 24px}@media (min-width: 480px){.gw-header__actions > .gw-btn:not(.gw-btn--secondary){display: inline-flex}}@media (min-width: 768px){.gw-header__actions > .gw-btn--secondary{display: inline-flex}}@media (min-width: 1080px){.gw-header__bar{gap: 32px}.gw-nav{display: block}.gw-header__signin{display: inline-flex}.gw-menu-toggle,.gw-mobile-menu{display: none !important}html.is-menu-open{overflow: visible}html.is-menu-open .gw-header{position: sticky}}@media (max-width: 768px),(pointer: coarse){.gw-theme-toggle,.gw-menu-toggle{width: var(--gw-tap-min);height: var(--gw-tap-min)}}@media (min-width: 1080px){.gw-header--float{top: var(--gw-header-gap);width: min(100% - 32px,var(--gw-wrap) + 50px);margin: var(--gw-header-gap) auto 0;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: color-mix(in srgb,var(--gw-surface) var(--gw-header-surface),transparent);-webkit-backdrop-filter: blur(var(--gw-header-blur));backdrop-filter: blur(var(--gw-header-blur));box-shadow: var(--gw-shadow-raised)}.gw-header--float.is-scrolled{border-color: var(--gw-line-strong)}.gw-header--float > .gw-header__bar{width: auto;padding-inline: 24px}.gw-header--float .gw-mega{max-height: calc(100vh - var(--gw-header-height) - var(--gw-header-gap) - 24px)}:root:has(.gw-header--float) [id]{scroll-margin-top: calc(var(--gw-header-height) + var(--gw-header-gap) + 18px)}}@media (min-width: 1080px) and (prefers-reduced-transparency: reduce){.gw-header--float{background: var(--gw-surface);-webkit-backdrop-filter: none;backdrop-filter: none}}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.gw-header--float{background: var(--gw-surface)}}.gw-footer{padding-block: 64px 40px;border-top: 1px solid var(--gw-line);background: var(--gw-bg);color: var(--gw-ink);font-size: 14px}.gw-footer__grid{display: grid;grid-template-columns: repeat(2,minmax(0,1fr));gap: 32px 24px}.gw-footer__brand{grid-column: 1 / -1;max-width: 320px}.gw-footer__brand p{margin-top: 16px;color: var(--gw-muted)}.gw-footer__heading{margin: 0 0 8px;color: var(--gw-ink);font-size: 13px;font-weight: 600;letter-spacing: 0}.gw-footer__col ul{margin: 0;padding: 0;list-style: none}.gw-footer__col a{display: block;padding-block: 6px;color: var(--gw-muted)}.gw-footer__col a:hover{color: var(--gw-ink)}.gw-footer__col a[aria-current="page"]{color: var(--gw-accent-strong)}.gw-footer__bottom{display: flex;flex-wrap: wrap;align-items: center;justify-content: space-between;gap: 8px 24px;margin-top: 48px;padding-top: 24px;border-top: 1px solid var(--gw-line);color: var(--gw-muted);font-size: 13px}.gw-footer__bottom:first-child{margin-top: 0;padding-top: 0;border-top: 0}.gw-footer__bottom ul{display: flex;flex-wrap: wrap;gap: 0 16px;margin: 0;padding: 0;list-style: none}.gw-footer__bottom a{display: inline-flex;align-items: center;justify-content: center;min-width: 24px;min-height: 24px;color: var(--gw-muted)}.gw-footer__bottom a:hover{color: var(--gw-ink)}@media (min-width: 768px){.gw-footer__grid{grid-template-columns: repeat(3,minmax(0,1fr))}}@media (min-width: 1080px){.gw-footer__grid{grid-auto-flow: column;grid-template-columns: minmax(0,1.6fr);grid-auto-columns: minmax(0,1fr)}.gw-footer__brand{grid-column: auto}}@media (max-width: 768px),(pointer: coarse){.gw-footer__bottom a{min-height: var(--gw-tap-min)}}.gw-hero{position: relative;isolation: isolate;padding-block: 48px 64px}.gw-hero__wash{position: absolute;inset: 0;z-index: -1;pointer-events: none;background: radial-gradient(640px 460px at 88% -10%,color-mix(in srgb,var(--gw-accent) 16%,transparent),transparent 62%),radial-gradient(560px 480px at 6% 108%,color-mix(in srgb,var(--gw-wash-violet) 12%,transparent),transparent 62%),radial-gradient(420px 320px at 52% 118%,color-mix(in srgb,var(--gw-wash-mint) 10%,transparent),transparent 62%)}.gw-hero h1 .gw-dim{display: block}.gw-hero > .gw-wrap > .gw-window{margin-top: 48px}.gw-hero--center{text-align: center}.gw-hero--center > .gw-wrap > *{margin-inline: auto}.gw-hero--center .gw-actions{justify-content: center}.gw-hero--compact{padding-block: 40px 48px}.gw-hero--compact h1{font-size: var(--gw-display-h2)}@media (min-width: 768px){.gw-hero{padding-block: 80px 96px}.gw-hero--compact{padding-block: 64px}.gw-hero .gw-lead{font-size: 20px}.gw-hero > .gw-wrap > .gw-window{margin-top: 64px}}@media (max-width: 479px){:is(.gw-hero,.gw-cta) .gw-actions > .gw-btn{flex: 1 1 100%}}.gw-logos{padding-block: 40px;border-block: 1px solid var(--gw-line)}.gw-logos p{color: var(--gw-muted);font-size: 13px;text-align: center}.gw-logos ul{display: flex;flex-wrap: wrap;align-items: center;justify-content: center;gap: 16px 40px;margin-top: 20px;padding: 0;list-style: none}.gw-logos li{color: var(--gw-muted);font-size: 22px;font-weight: 600;letter-spacing: -0.02em;line-height: 1.2}.gw-logos img{width: auto;height: 28px}.gw-stats{display: grid;grid-template-columns: minmax(0,1fr);gap: 24px;margin: 0;padding: 0;list-style: none}.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}.gw-stats__label{margin-top: 8px;max-width: 28ch;color: var(--gw-muted);font-size: 14px}.gw-stats__source{margin-top: 8px;color: var(--gw-muted);font-size: 12px}@media (min-width: 480px){.gw-stats{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 1080px){.gw-stats{grid-template-columns: none;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr);gap: 0}.gw-stats__item{padding: 4px 24px;border-left: 1px solid var(--gw-line)}.gw-stats__item:first-child{padding-left: 0;border-left: 0}}.gw-features{row-gap: 40px}.gw-feature .gw-icon{width: 40px;height: 40px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-action);background: var(--gw-surface);color: var(--gw-accent)}.gw-split{display: grid;grid-template-columns: minmax(0,1fr);align-items: center;gap: 40px}@media (min-width: 768px){.gw-split{grid-template-columns: repeat(2,minmax(0,1fr))}.gw-split--reverse > :first-child{order: 2}}.gw-split h2{font-size: 30px}@media (min-width: 1080px){.gw-split{gap: 64px}}.gw-showcase .gw-window{max-width: 960px;margin-inline: auto}.gw-showcase .gw-window + *{margin-top: 48px}.gw-steps{display: grid;grid-template-columns: minmax(0,1fr);gap: 32px 24px;margin: 0;padding: 0;list-style: none;counter-reset: gw-step}.gw-steps > li{counter-increment: gw-step}.gw-steps > li::before{content: counter(gw-step,decimal-leading-zero);display: block;margin-bottom: 16px;padding-bottom: 12px;border-bottom: 1px solid var(--gw-line);color: var(--gw-accent-strong);font-size: 13px;font-weight: 600;font-variant-numeric: tabular-nums}.gw-steps h3{font-size: 20px}.gw-steps p{margin-top: 8px;color: var(--gw-muted);font-size: 16px}@media (min-width: 768px){.gw-steps{grid-template-columns: repeat(2,minmax(0,1fr))}}@media (min-width: 1080px){.gw-steps{grid-template-columns: none;grid-auto-flow: column;grid-auto-columns: minmax(0,1fr)}}.gw-quote{max-width: 880px;margin-inline: auto;padding: 40px 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: var(--gw-surface);text-align: center}.gw-quote::before{content: "";display: block;width: 32px;height: 24px;margin: 0 auto 24px;background: var(--gw-accent);-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M0 24V14C0 6 4 1 12 0l1 4c-4 1-6 4-6 8h5v12zm19 0V14c0-8 4-13 12-14l1 4c-4 1-6 4-6 8h5v12z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M0 24V14C0 6 4 1 12 0l1 4c-4 1-6 4-6 8h5v12zm19 0V14c0-8 4-13 12-14l1 4c-4 1-6 4-6 8h5v12z'/%3E%3C/svg%3E") center / contain no-repeat}.gw-quote blockquote{font-size: 24px;font-weight: 500;line-height: 1.4;letter-spacing: -0.01em;text-wrap: balance}.gw-quote figcaption{margin-top: 24px;color: var(--gw-muted);font-size: 14px}.gw-quote figcaption b{color: var(--gw-ink)}.gw-quote figcaption img{width: 40px;height: 40px;margin: 0 auto 12px;border-radius: var(--gw-radius-chip)}@media (min-width: 768px){.gw-quote{padding: 64px}.gw-quote blockquote{font-size: 30px}}.gw-pricing{display: grid;grid-template-columns: repeat(auto-fit,minmax(min(100%,260px),1fr));align-items: start;gap: 16px}.gw-plan{display: flex;flex-direction: column;padding: 24px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-panel);background: var(--gw-surface);box-shadow: var(--gw-shadow)}.gw-plan--featured{border-color: var(--gw-accent)}.gw-plan{position: relative}.gw-plan > .gw-badge{align-self: flex-start}.gw-plan > .gw-badge:first-child{position: absolute;top: -12px;left: 24px}.gw-plan > .gw-badge:first-child + *{margin-top: 0}.gw-plan h3{font-size: 20px}:where(.gw-plan) p{color: var(--gw-muted);font-size: 14px}.gw-plan__price{display: flex;flex-wrap: wrap;align-items: baseline;gap: 6px;color: var(--gw-ink);font-size: var(--gw-display-h2);font-weight: 600;letter-spacing: -0.03em;line-height: 1.1;font-variant-numeric: tabular-nums}.gw-plan__price small{color: var(--gw-muted);font-size: 14px;font-weight: 500;letter-spacing: 0}.gw-plan .gw-list--check{font-size: 14px}@media (min-width: 768px){.gw-plan{padding: 32px}.gw-plan > .gw-badge:first-child{left: 32px}}.gw-cta{max-width: 780px;margin-inline: auto;text-align: center}.gw-cta .gw-lead{margin-inline: auto}.gw-cta .gw-actions{justify-content: center}.gw-prose{max-width: 68ch;min-width: 0;color: var(--gw-ink);font-size: var(--gw-body-size);line-height: var(--gw-body-leading);overflow-wrap: break-word}.gw-prose--sm{font-size: 16px}:where(.gw-prose) > * + *{margin-top: 20px}:where(.gw-prose--sm) > * + *{margin-top: 16px}:where(.gw-prose) > * + :where(figure,img,video,pre,table,hr,.gw-table,.gw-window),:where(.gw-prose) > :where(figure,img,video,pre,table,hr,.gw-table,.gw-window) + *{margin-top: 32px}:where(.gw-prose) > * + h2{margin-top: 48px}:where(.gw-prose) > * + h3{margin-top: 40px}:where(.gw-prose) > * + h4{margin-top: 32px}:where(.gw-prose--sm) > * + h2{margin-top: 40px}:where(.gw-prose--sm) > * + h3{margin-top: 32px}:where(.gw-prose--sm) > * + h4{margin-top: 24px}:where(.gw-prose) > h1 + *{margin-top: 24px}:where(.gw-prose) > :is(h2,h3,h4) + *{margin-top: 12px}:where(.gw-prose) > :is(h1,h2,h3,h4){color: var(--gw-ink);font-weight: var(--gw-heading-weight);letter-spacing: var(--gw-heading-tracking);line-height: 1.25}:where(.gw-prose) > h1{font-size: var(--gw-display-h2);font-weight: var(--gw-display-weight);letter-spacing: var(--gw-display-tracking);line-height: var(--gw-heading-leading)}:where(.gw-prose) > h2{font-size: 30px;line-height: 1.2}:where(.gw-prose) > h3{font-size: 24px}:where(.gw-prose) > h4{font-size: 20px}:where(.gw-prose--sm) > h2{font-size: 24px}:where(.gw-prose--sm) > h3{font-size: 20px}:where(.gw-prose--sm) > h4{font-size: 18px}:where(.gw-prose :is(ul,ol):not([class])){padding-left: 24px}:where(.gw-prose ul:not([class])){list-style: disc}:where(.gw-prose ol:not([class])){list-style: decimal}:where(.gw-prose :is(ul,ol):not([class]) > li + li),:where(.gw-prose :is(ul,ol):not([class]) > li > * + *),:where(.gw-prose :is(ul,ol):not([class]) > li > :is(ul,ol)){margin-top: 8px}:where(.gw-prose :is(ul,ol):not([class]) > li)::marker{color: var(--gw-muted);font-variant-numeric: tabular-nums}:where(.gw-prose) > blockquote{padding-left: 20px;border-left: 3px solid var(--gw-line-strong);color: var(--gw-muted)}:where(.gw-prose) > blockquote > * + *{margin-top: 12px}:where(.gw-prose) > hr{height: 0;border: 0;border-top: 1px solid var(--gw-line)}:where(.gw-prose) :is(sup,sub){line-height: 0}:where(.gw-prose) :is(pre,code,kbd,samp){font-family: var(--gw-font-mono)}:where(.gw-prose) code{padding: 2px 6px;border-radius: var(--gw-radius-6);background: color-mix(in srgb,var(--gw-ink) 8%,transparent);color: var(--gw-ink);font-size: 16px;overflow-wrap: anywhere;-webkit-box-decoration-break: clone;box-decoration-break: clone}:where(.gw-prose--sm) code{font-size: 14px}:where(.gw-prose) :is(h1,h2,h3,h4) code{font-size: inherit}:where(.gw-prose) figcaption code{font-size: 13px}:where(.gw-prose) :is(th,td) code{font-size: 13px;overflow-wrap: normal;white-space: nowrap}:where(.gw-prose) pre{overflow-x: auto;padding: 16px 20px;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 14px;line-height: 1.6;tab-size: 2}:where(.gw-prose) pre code{padding: 0;border-radius: var(--gw-radius-0);background: none;font-size: inherit;overflow-wrap: normal}:where(.gw-prose) > :is(img,video),:where(.gw-prose) > :is(p,figure) > :is(img,video){border-radius: var(--gw-radius-card)}:where(.gw-prose) figcaption{margin-top: 12px;color: var(--gw-muted);font-size: 14px;line-height: 1.5}:where(.gw-prose) > table{display: block;width: max-content;max-width: 100%;overflow-x: auto;border: 1px solid var(--gw-line);border-radius: var(--gw-radius-card);background: var(--gw-surface);border-collapse: collapse;font-size: 14px;line-height: 1.5;-webkit-overflow-scrolling: touch}:where(.gw-prose) > table :is(th,td){padding: 12px 16px;border-bottom: 1px solid var(--gw-line);text-align: left;vertical-align: top}:where(.gw-prose) > table thead th{background: var(--gw-bg-soft);color: var(--gw-ink);font-size: 13px;font-weight: 600;white-space: nowrap}:where(.gw-prose) > table tbody th{font-weight: 500}:where(.gw-prose) > table td{color: var(--gw-ink-2);font-variant-numeric: tabular-nums}:where(.gw-prose) > table tbody tr:last-child > *{border-bottom: 0}:where(.gw-prose) > table caption{padding: 12px 16px;color: var(--gw-muted);font-size: 13px;text-align: left}@media (max-width: 767px){:where(.gw-prose) > table td{min-width: 160px}}