openvibers/openvibe.shared Commits
Font Awesome's features on the glyphs, and the glyphs as fonts and a sprite (4.62.0) (#214)
Changes
@@@ -1,3 +1,5 @@* text=auto eol=lf*.png binary*.woff2 binary++*.ttf binary++*.otf binary@@@ -6,6 +6,35 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.62.0 — 2026-10-31++++- **Font Awesome's features, kept, on the glyphs** (plan L32 I6; the owner: "basically want to keep Font Awesome, so all++ their features"). `ov-glyphs.js` now carries its own rules for every documented Font Awesome class, with Font++ Awesome's CSS variables, so a site can drop Font Awesome's stylesheet and keep its markup:++ - sizes `fa-2xs` … `fa-2xl` and `fa-1x` … `fa-10x`, `fa-fw`, `fa-border`, `fa-pull-left` / `-right`;++ - `fa-rotate-90/180/270`, `fa-rotate-by` (`--fa-rotate-angle`), `fa-flip-horizontal/vertical/both`;++ - `fa-ul` / `fa-li`, `fa-stack` (`-1x`, `-2x`, `fa-inverse`), `fa-layers` with `fa-layers-text` and++ `fa-layers-counter`;++ - the animations `fa-beat`, `fa-bounce`, `fa-fade`, `fa-beat-fade`, `fa-flip` and `fa-shake`. `fa-spin` and++ `fa-spin-pulse` breathe instead, since nothing on the network spins (L27). Reduced motion stops all of them;++ - `fa-thin` and `fa-light` as thinner strokes of the same drawing (`--ovg-stroke`, `ovg-bold`), and++ `--fa-primary-color` on duotone icons;++ - `data-fa-transform` (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), in Font Awesome's sixteenths;++ - a `title` names the icon to screen readers (`role="img"`).++ - A page that resets an icon's class list but keeps its name keeps the glyph. The drop-in is 9.1 KB brotli and loads++ after first paint.++- **The glyphs as fonts** (plan L32 I5; the owner: "exporting like web fonts, regular font files"):++ - `ov-glyphs.woff2` (12 KB, served at `/shared/ov-glyphs.woff2`), plus `glyph-fonts/ov-glyphs.ttf` and++ `glyph-fonts/ov-glyphs.otf` for desktop apps and design tools. Each glyph's strokes are expanded to outlines++ (picosvg, skia-pathops) by `scripts/build-glyph-fonts.py`, so the font draws what the SVG does.++ - `glyph-codepoints.json`: each glyph's Private Use Area code point from U+E001, given once and never reused.++ - `ov-glyphs-font.css`: `<i class="ovi ovi-eye">`. `ov-glyphs-fa.css`: Font Awesome's class names on the font, for a++ page whose every name is covered.++ - `test/glyph-fonts.test.js` fails when the fonts no longer match the registry. Builds are reproducible++ (`SOURCE_DATE_EPOCH`).++- **The sprite:** `ov-glyphs.svg`, every glyph as a `<symbol id="ovg-<name>">`, served at `/shared/ov-glyphs.svg`++ (`files.IMAGES`; `serve.js` answers `image/svg+xml`).++## 4.61.0 — 2026-10-31- **The network's own icons: `openvibe-shared/glyphs` and `ov-glyphs.js`** (plan L32; the owner: "our own custom@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.61.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.62.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -21,9 +21,9 @@ were reconciled first.|---|---|| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `notification-live.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `player.js`, `release-watch.js`, `release-update.js`, `web-runtime.js`, `boost.js`, `openvibe-sw.js` || Fonts, served at `/shared/<file>` (`files.FONTS`) | `ov-brand.woff2`: the brand face the names are set in (Nunito, SIL OFL 1.1, `OFL-Nunito.txt`; Latin, 700–900), declared by `navbar.js` |--| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,pg,retention,event}`), `app-icon`, `glyphs` (the network's icons, one registry: `NAMES`, `path(name)`, `svg(name, { size, title })`, `fromFa(name)`, `FA`; the browser drop-in `ov-glyphs.js` shows them in place of Font Awesome's), `mark` (the OpenVibe mark, the Pair, as static SVG: `svg({ form, tone, wide, size, title })`, `art()`, `geo()`, `shapes()`, `COLORS`; the browser drop-in `ov-mark.js` draws the same numbers), `app-icons` (every site's installable app icon set: `has(service)`, `url(service, size, { maskable })`, `isIcon(name)`, `path(name)`; the pre-rendered PNGs are served at `/shared/icons/<service>[-maskable]-<size>.png` and named by the web app manifest `serve.js` serves at `/shared/manifest/<service>.webmanifest`), `assets` (content-addressed image/asset URLs and responsive `<picture>` markup: `hash(bytes)`, `url(src, hash)`, `srcset(entries)`, `picture({ src, hash?, alt, width, height, sizes, sources })` — AVIF before WebP before the fallback `<img>`, an unhashed path kept exactly as given), `auth-client`, `brand`, `builtin-themes`, `cache-policy` (the one cache rule: a content-addressed asset — `?v=<hash>` or a hashed filename — is `public, max-age=31536000, immutable`, anything else is `public, max-age=300, stale-while-revalidate=86400`, HTML is `public, max-age=120, stale-while-revalidate=3600` and a personalised page is `private, no-store`; `assetHeaders`/`htmlHeaders`/`isHashed` plus an `applyHtml()` middleware, used by `serve.js`), `indexnow` (tell engines a page changed: `createIndexNow({ host, key, keyLocation?, endpoint?, fetch, log })` → `keyFile` for `/<key>.txt`, `ping(urls)` for one POST of a deduped, https-only, own-host batch of ≤10,000 URLs that never throws, and `pingSoon(urls)` debounced into one batch in 30s; an unset key sends nothing), `frame` (the OpenVibe Frame on the server), `legal`, `middleware`, `notifications`, `seo`, `social` (every site's 1200×630 share card: `url(service)`, served by `serve.js` at `/shared/social/<service>.jpg` and used by `shell.page()` for `og:image` when a page names none), `shell` (the server-rendered page shell: `page(o)` composes the doctype and `<head>` from `seo.headTags`/`seo.pageSummary`, the theme-loader before the first paint, `web-runtime`/`navbar`/`footer` through `serve.url`, the `frame.noscriptNav` no-JS nav, the body and `frame.footer`; `scripts(o)` is just the head scripts and the escaped runtime boot JSON), `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`, `config` (the configuration model: revisioned, validated, classified settings with last-known-good and `/api/admin/config`), `test-runner` (the `npm test` runner of every service's `test/run.js`: each `*.test.js` in its own process; a line `<label>: skipped (<why>)` makes a file skipped, listed with ○ and never counted as passed, and `--strict` fails on skips), `perf-budget` (size budgets for a page's first load, measured from the running server: HTML, same-origin scripts and stylesheets, raw and brotli; for `npm test`), `browser-harness` (real-Chrome checks of a running site: status, console errors, overflow, duplicate scripts, no-JS text, canonical, JSON-LD against visible text, axe-core, repeated-navigation growth and idle work; Node 22, Chrome); `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |++| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,pg,retention,event}`), `app-icon`, `glyphs` (the network's icons, one registry: `NAMES`, `path(name)`, `svg(name, { size, title })`, `fromFa(name)`, `FA`; the browser drop-in `ov-glyphs.js` shows them in place of Font Awesome's and keeps Font Awesome's classes, animations and `data-fa-transform`; also as fonts, `/shared/ov-glyphs.woff2` with `ov-glyphs-font.css` (`ovi ovi-<name>`) or `ov-glyphs-fa.css` (Font Awesome's names), `glyph-fonts/ov-glyphs.{ttf,otf}` and `glyph-codepoints.json`, and as the sprite `/shared/ov-glyphs.svg#ovg-<name>`), `mark` (the OpenVibe mark, the Pair, as static SVG: `svg({ form, tone, wide, size, title })`, `art()`, `geo()`, `shapes()`, `COLORS`; the browser drop-in `ov-mark.js` draws the same numbers), `app-icons` (every site's installable app icon set: `has(service)`, `url(service, size, { maskable })`, `isIcon(name)`, `path(name)`; the pre-rendered PNGs are served at `/shared/icons/<service>[-maskable]-<size>.png` and named by the web app manifest `serve.js` serves at `/shared/manifest/<service>.webmanifest`), `assets` (content-addressed image/asset URLs and responsive `<picture>` markup: `hash(bytes)`, `url(src, hash)`, `srcset(entries)`, `picture({ src, hash?, alt, width, height, sizes, sources })` — AVIF before WebP before the fallback `<img>`, an unhashed path kept exactly as given), `auth-client`, `brand`, `builtin-themes`, `cache-policy` (the one cache rule: a content-addressed asset — `?v=<hash>` or a hashed filename — is `public, max-age=31536000, immutable`, anything else is `public, max-age=300, stale-while-revalidate=86400`, HTML is `public, max-age=120, stale-while-revalidate=3600` and a personalised page is `private, no-store`; `assetHeaders`/`htmlHeaders`/`isHashed` plus an `applyHtml()` middleware, used by `serve.js`), `indexnow` (tell engines a page changed: `createIndexNow({ host, key, keyLocation?, endpoint?, fetch, log })` → `keyFile` for `/<key>.txt`, `ping(urls)` for one POST of a deduped, https-only, own-host batch of ≤10,000 URLs that never throws, and `pingSoon(urls)` debounced into one batch in 30s; an unset key sends nothing), `frame` (the OpenVibe Frame on the server), `legal`, `middleware`, `notifications`, `seo`, `social` (every site's 1200×630 share card: `url(service)`, served by `serve.js` at `/shared/social/<service>.jpg` and used by `shell.page()` for `og:image` when a page names none), `shell` (the server-rendered page shell: `page(o)` composes the doctype and `<head>` from `seo.headTags`/`seo.pageSummary`, the theme-loader before the first paint, `web-runtime`/`navbar`/`footer` through `serve.url`, the `frame.noscriptNav` no-JS nav, the body and `frame.footer`; `scripts(o)` is just the head scripts and the escaped runtime boot JSON), `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`, `config` (the configuration model: revisioned, validated, classified settings with last-known-good and `/api/admin/config`), `test-runner` (the `npm test` runner of every service's `test/run.js`: each `*.test.js` in its own process; a line `<label>: skipped (<why>)` makes a file skipped, listed with ○ and never counted as passed, and `--strict` fails on skips), `perf-budget` (size budgets for a page's first load, measured from the running server: HTML, same-origin scripts and stylesheets, raw and brotli; for `npm test`), `browser-harness` (real-Chrome checks of a running site: status, console errors, overflow, duplicate scripts, no-JS text, canonical, JSON-LD against visible text, axe-core, repeated-navigation growth and idle work; Node 22, Chrome); `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` || Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |--| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js`, `scripts/build-icons.js`, `scripts/build-social-cards.js` (share cards from the Contracts manifests; Chrome + ImageMagick) |++| Generators | `scripts/build-glyphs.js` (`ov-glyphs.js` and `ov-glyphs.svg` from `glyphs.js`), `scripts/build-glyph-fonts.py` (the glyph fonts and their CSS; Python with picosvg, fonttools and brotli), `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js`, `scripts/build-icons.js`, `scripts/build-social-cards.js` (share cards from the Contracts manifests; Chrome + ImageMagick) || Pin bot | `scripts/pin-bump.js` (`node scripts/pin-bump.js <library> <tag> [--repos A,B] [--apply]`): after a release, one PR per repository that pins the library below the tag, with the lockfile refreshed and the docs-status fields rewritten; a dry run unless `--apply`; CI tests each PR || Checks (CI) | `scripts/pin-drift.js` (every openvibe-* pin is a published tag and not more than one minor behind, and openvibe-shared is installed once), `scripts/docs-status.js` (`node scripts/docs-status.js <dir> [--check]`: STATUS.json's contracts/sdk/shared fields and the README `<!-- versions:start/end -->` block must name the pinned releases; `--check` exits 1 on drift) and `scripts/floating-promises.js` (`node scripts/floating-promises.js <dir> [--json]`: an async call whose result nobody awaits — a failure is an unhandled rejection and a transaction commits without it — unless awaited, returned, assigned, `void`-ed, chained with `.then(`/`.catch(`/`.finally(` or opted out with `// floating-ok: <reason>`; exit 1 when found). The native code is compiled too: `scripts/native-check.js` writes the generated Compose and SwiftUI sources out, and `.github/workflows/native.yml` compiles the Kotlin against the Compose artifacts and type-checks the Swift against SwiftUI |@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.61.0",++ "version": "4.62.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -65,18 +65,27 @@ const STYLES = Object.freeze(['room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)'kit-data.css', // Kit 2's look: steps, tables, charts, rankings, meters, drop zones, receipts (4.37.0; after kit.css)'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)++ 'ov-glyphs-font.css', // the glyphs as a font: <i class="ovi ovi-eye"> (4.62.0; built with the font, scripts/build-glyph-fonts.py)++ 'ov-glyphs-fa.css', // Font Awesome's class names on the glyph font, for a site that drops Font Awesome's own (4.62.0)]);--// Fonts sites serve at /shared/<file>: the brand face the names are set in (navbar.js declares it from beside itself).++// Fonts sites serve at /shared/<file>: the brand face the names are set in (navbar.js declares it from beside itself),++// and the glyph font.const FONTS = Object.freeze(['ov-brand.woff2', // Nunito (SIL OFL 1.1, OFL-Nunito.txt), Latin, weights 700-900: the OpenVibe names (4.59.0)++ 'ov-glyphs.woff2', // the OpenVibe glyphs as a font (4.62.0; TTF and OTF for desktop apps are in glyph-fonts/)]);--const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name)) || FONTS.includes(String(name));++// Pictures sites serve at /shared/<file>.++const IMAGES = Object.freeze([++ 'ov-glyphs.svg', // every glyph as a <symbol>: <svg><use href="/shared/ov-glyphs.svg#ovg-eye"/></svg> (4.62.0)++]);++const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name)) || FONTS.includes(String(name)) || IMAGES.includes(String(name));module.exports = {dir: __dirname,BROWSER,STYLES,FONTS,++ IMAGES,isBrowserFile: served,path: (name) => {if (!served(name)) throw new Error(`openvibe-shared: ${name} is not a browser file`);nnew file mode 100644@@@ -0,0 +1,160 @@++{++ "about": "Each OpenVibe glyph's code point in the OV Glyphs font (U+E001 up). Given once, never changed or reused.",++ "glyphs": {++ "alert": "E001",++ "alert-circle": "E002",++ "archive": "E003",++ "arrow-down": "E004",++ "arrow-left": "E005",++ "arrow-right": "E006",++ "arrow-up": "E007",++ "back": "E008",++ "ban": "E009",++ "bell": "E00A",++ "book": "E00B",++ "brain": "E00C",++ "branch": "E00D",++ "bridge": "E00E",++ "busy": "E00F",++ "calendar": "E010",++ "camera": "E011",++ "care": "E012",++ "cart": "E013",++ "chart": "E014",++ "chat": "E015",++ "check": "E016",++ "check-circle": "E017",++ "chevron": "E018",++ "chip": "E019",++ "circle": "E01A",++ "city": "E01B",++ "clipboard": "E01C",++ "clock": "E01D",++ "close": "E01E",++ "cloud": "E01F",++ "code": "E020",++ "coin": "E021",++ "compress": "E022",++ "computer": "E023",++ "copy": "E024",++ "crown": "E025",++ "database": "E026",++ "dish": "E027",++ "dollar": "E028",++ "down": "E029",++ "download": "E02A",++ "drop": "E02B",++ "enter": "E02C",++ "expand": "E02D",++ "eye": "E02E",++ "eye-off": "E02F",++ "file": "E030",++ "film": "E031",++ "filter": "E032",++ "fire": "E033",++ "flag": "E034",++ "folder": "E035",++ "food": "E036",++ "forward": "E037",++ "game": "E038",++ "gauge": "E039",++ "gavel": "E03A",++ "gear": "E03B",++ "gift": "E03C",++ "globe": "E03D",++ "grid": "E03E",++ "hand": "E03F",++ "hash": "E040",++ "heart": "E041",++ "help": "E042",++ "history": "E043",++ "home": "E044",++ "id": "E045",++ "idea": "E046",++ "image": "E047",++ "info": "E048",++ "invite": "E049",++ "key": "E04A",++ "keyboard": "E04B",++ "language": "E04C",++ "layers": "E04D",++ "leaf": "E04E",++ "leave": "E04F",++ "link": "E050",++ "list": "E051",++ "live": "E052",++ "lock": "E053",++ "mail": "E054",++ "map": "E055",++ "members": "E056",++ "menu": "E057",++ "mic": "E058",++ "mic-off": "E059",++ "minus": "E05A",++ "moon": "E05B",++ "more": "E05C",++ "mountain": "E05D",++ "music": "E05E",++ "mute": "E05F",++ "news": "E060",++ "nodes": "E061",++ "open": "E062",++ "palette": "E063",++ "pause": "E064",++ "pencil": "E065",++ "phone": "E066",++ "pin": "E067",++ "plane": "E068",++ "play": "E069",++ "plug": "E06A",++ "plus": "E06B",++ "reply": "E06C",++ "retry": "E06D",++ "robot": "E06E",++ "route": "E06F",++ "save": "E070",++ "scissors": "E071",++ "search": "E072",++ "send": "E073",++ "server": "E074",++ "shield": "E075",++ "shirt": "E076",++ "shoe": "E077",++ "sidebar": "E078",++ "sliders": "E079",++ "smile": "E07A",++ "snow": "E07B",++ "spark": "E07C",++ "star": "E07D",++ "steps": "E07E",++ "stop": "E07F",++ "store": "E080",++ "sun": "E081",++ "swap": "E082",++ "tack": "E083",++ "tag": "E084",++ "target": "E085",++ "task": "E086",++ "tent": "E087",++ "terminal": "E088",++ "text": "E089",++ "ticket": "E08A",++ "train": "E08B",++ "trash": "E08C",++ "tree": "E08D",++ "trophy": "E08E",++ "up": "E08F",++ "upload": "E090",++ "user": "E091",++ "user-check": "E092",++ "video": "E093",++ "volume": "E094",++ "wallet": "E095",++ "water": "E096",++ "wave": "E097",++ "wifi": "E098",++ "wrench": "E099",++ "x-circle": "E09A",++ "zap": "E09B"++ }++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "family": "OV Glyphs",++ "glyphs": 155,++ "source_sha256": "fb052064fcb5279c7c291230e2e5389d164613d278969ad437f975b26ab40bdc",++ "files": {++ "ov-glyphs.woff2": 12320,++ "glyph-fonts/ov-glyphs.ttf": 28500,++ "glyph-fonts/ov-glyphs.otf": 35816++ }++}nnew file mode 100644BBinary files /dev/null and b/glyph-fonts/ov-glyphs.otf differnnew file mode 100644BBinary files /dev/null and b/glyph-fonts/ov-glyphs.ttf differ@@@ -475,7 +475,7 @@const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': '2e3590333ae6', 'ov-brand.woff2': '3b4e6a8a5878' };++ const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': 'ba57d6e1f417', 'ov-brand.woff2': '3b4e6a8a5878' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.nnew file mode 100644@@@ -0,0 +1,243 @@++/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */++/* Font Awesome's class names on the OV Glyphs font, for a site that drops Font Awesome's stylesheet and its++ font. A name with no glyph shows nothing, so load it only where every name a page uses is covered++ (openvibe.design/icons lists them); brand logos keep Font Awesome Brands. */++@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}++.fa,.fas,.far,.fal,.fat,.fa-solid,.fa-regular,.fa-light,.fa-thin,.fa-duotone{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}++.fa-fw{width:1.25em;text-align:center}++.fa-triangle-exclamation::before{content:"\e001"}++.fa-circle-exclamation::before{content:"\e002"}++.fa-box-archive::before{content:"\e003"}++.fa-arrow-down::before{content:"\e004"}++.fa-arrow-left::before{content:"\e005"}++.fa-arrow-right::before{content:"\e006"}++.fa-arrow-up::before{content:"\e007"}++.fa-chevron-left::before{content:"\e008"}++.fa-ban::before{content:"\e009"}++.fa-bell::before{content:"\e00a"}++.fa-book::before{content:"\e00b"}++.fa-book-open::before{content:"\e00b"}++.fa-brain::before{content:"\e00c"}++.fa-code-branch::before{content:"\e00d"}++.fa-code-fork::before{content:"\e00d"}++.fa-bridge::before{content:"\e00e"}++.fa-circle-notch::before{content:"\e00f"}++.fa-spinner::before{content:"\e00f"}++.fa-calendar::before{content:"\e010"}++.fa-calendar-days::before{content:"\e010"}++.fa-camera::before{content:"\e011"}++.fa-hand-holding-heart::before{content:"\e012"}++.fa-cart::before{content:"\e013"}++.fa-cart-shopping::before{content:"\e013"}++.fa-chart-bar::before{content:"\e014"}++.fa-chart-line::before{content:"\e014"}++.fa-chart-simple::before{content:"\e014"}++.fa-comment::before{content:"\e015"}++.fa-comment-dots::before{content:"\e015"}++.fa-comments::before{content:"\e015"}++.fa-message::before{content:"\e015"}++.fa-check::before{content:"\e016"}++.fa-check-circle::before{content:"\e017"}++.fa-circle-check::before{content:"\e017"}++.fa-angle-right::before{content:"\e018"}++.fa-chevron-right::before{content:"\e018"}++.fa-microchip::before{content:"\e019"}++.fa-circle::before{content:"\e01a"}++.fa-city::before{content:"\e01b"}++.fa-clipboard::before{content:"\e01c"}++.fa-clipboard-list::before{content:"\e01c"}++.fa-paste::before{content:"\e01c"}++.fa-clock::before{content:"\e01d"}++.fa-times::before{content:"\e01e"}++.fa-xmark::before{content:"\e01e"}++.fa-cloud::before{content:"\e01f"}++.fa-cloud-rain::before{content:"\e01f"}++.fa-cloud-showers-heavy::before{content:"\e01f"}++.fa-cloud-sun::before{content:"\e01f"}++.fa-code::before{content:"\e020"}++.fa-coin::before{content:"\e021"}++.fa-coins::before{content:"\e021"}++.fa-hand-holding-dollar::before{content:"\e021"}++.fa-compress::before{content:"\e022"}++.fa-desktop::before{content:"\e023"}++.fa-display::before{content:"\e023"}++.fa-laptop::before{content:"\e023"}++.fa-copy::before{content:"\e024"}++.fa-crown::before{content:"\e025"}++.fa-database::before{content:"\e026"}++.fa-satellite-dish::before{content:"\e027"}++.fa-dollar-sign::before{content:"\e028"}++.fa-angle-down::before{content:"\e029"}++.fa-chevron-down::before{content:"\e029"}++.fa-download::before{content:"\e02a"}++.fa-droplet::before{content:"\e02b"}++.fa-arrow-right-to-bracket::before{content:"\e02c"}++.fa-right-to-bracket::before{content:"\e02c"}++.fa-expand::before{content:"\e02d"}++.fa-up-right-and-down-left-from-center::before{content:"\e02d"}++.fa-eye::before{content:"\e02e"}++.fa-eye-slash::before{content:"\e02f"}++.fa-file::before{content:"\e030"}++.fa-file-audio::before{content:"\e030"}++.fa-file-lines::before{content:"\e030"}++.fa-file-pdf::before{content:"\e030"}++.fa-film::before{content:"\e031"}++.fa-photo-film::before{content:"\e031"}++.fa-filter::before{content:"\e032"}++.fa-fire::before{content:"\e033"}++.fa-flag::before{content:"\e034"}++.fa-folder::before{content:"\e035"}++.fa-utensils::before{content:"\e036"}++.fa-gamepad::before{content:"\e038"}++.fa-gauge::before{content:"\e039"}++.fa-gauge-high::before{content:"\e039"}++.fa-gavel::before{content:"\e03a"}++.fa-gear::before{content:"\e03b"}++.fa-gears::before{content:"\e03b"}++.fa-gift::before{content:"\e03c"}++.fa-earth-americas::before{content:"\e03d"}++.fa-earth-europe::before{content:"\e03d"}++.fa-globe::before{content:"\e03d"}++.fa-grid::before{content:"\e03e"}++.fa-table-cells::before{content:"\e03e"}++.fa-hand::before{content:"\e03f"}++.fa-hand-pointer::before{content:"\e03f"}++.fa-hashtag::before{content:"\e040"}++.fa-heart::before{content:"\e041"}++.fa-circle-question::before{content:"\e042"}++.fa-question::before{content:"\e042"}++.fa-clock-rotate-left::before{content:"\e043"}++.fa-house::before{content:"\e044"}++.fa-house-chimney::before{content:"\e044"}++.fa-id-card::before{content:"\e045"}++.fa-lightbulb::before{content:"\e046"}++.fa-file-image::before{content:"\e047"}++.fa-image::before{content:"\e047"}++.fa-images::before{content:"\e047"}++.fa-circle-info::before{content:"\e048"}++.fa-user-plus::before{content:"\e049"}++.fa-key::before{content:"\e04a"}++.fa-keyboard::before{content:"\e04b"}++.fa-language::before{content:"\e04c"}++.fa-layer-group::before{content:"\e04d"}++.fa-leaf::before{content:"\e04e"}++.fa-arrow-right-from-bracket::before{content:"\e04f"}++.fa-right-from-bracket::before{content:"\e04f"}++.fa-link::before{content:"\e050"}++.fa-list::before{content:"\e051"}++.fa-list-ul::before{content:"\e051"}++.fa-broadcast-tower::before{content:"\e052"}++.fa-tower-broadcast::before{content:"\e052"}++.fa-tower-cell::before{content:"\e052"}++.fa-lock::before{content:"\e053"}++.fa-unlock::before{content:"\e053"}++.fa-envelope::before{content:"\e054"}++.fa-map::before{content:"\e055"}++.fa-people-group::before{content:"\e056"}++.fa-user-group::before{content:"\e056"}++.fa-users::before{content:"\e056"}++.fa-bars::before{content:"\e057"}++.fa-microphone::before{content:"\e058"}++.fa-microphone-slash::before{content:"\e059"}++.fa-minus::before{content:"\e05a"}++.fa-moon::before{content:"\e05b"}++.fa-ellipsis::before{content:"\e05c"}++.fa-ellipsis-vertical::before{content:"\e05c"}++.fa-mountain::before{content:"\e05d"}++.fa-mountain-sun::before{content:"\e05d"}++.fa-music::before{content:"\e05e"}++.fa-volume-off::before{content:"\e05f"}++.fa-volume-xmark::before{content:"\e05f"}++.fa-newspaper::before{content:"\e060"}++.fa-circle-nodes::before{content:"\e061"}++.fa-diagram-project::before{content:"\e061"}++.fa-network-wired::before{content:"\e061"}++.fa-share-nodes::before{content:"\e061"}++.fa-sitemap::before{content:"\e061"}++.fa-arrow-up-right-from-square::before{content:"\e062"}++.fa-external-link::before{content:"\e062"}++.fa-up-right-from-square::before{content:"\e062"}++.fa-palette::before{content:"\e063"}++.fa-pause::before{content:"\e064"}++.fa-pen::before{content:"\e065"}++.fa-pen-nib::before{content:"\e065"}++.fa-pen-to-square::before{content:"\e065"}++.fa-pencil::before{content:"\e065"}++.fa-mobile-screen::before{content:"\e066"}++.fa-phone::before{content:"\e066"}++.fa-location-dot::before{content:"\e067"}++.fa-location-pin::before{content:"\e067"}++.fa-map-pin::before{content:"\e067"}++.fa-paper-plane::before{content:"\e068"}++.fa-circle-play::before{content:"\e069"}++.fa-play::before{content:"\e069"}++.fa-plug::before{content:"\e06a"}++.fa-plus::before{content:"\e06b"}++.fa-reply::before{content:"\e06c"}++.fa-arrow-rotate-right::before{content:"\e06d"}++.fa-arrows-rotate::before{content:"\e06d"}++.fa-rotate::before{content:"\e06d"}++.fa-rotate-left::before{content:"\e06d"}++.fa-rotate-right::before{content:"\e06d"}++.fa-robot::before{content:"\e06e"}++.fa-route::before{content:"\e06f"}++.fa-floppy-disk::before{content:"\e070"}++.fa-cut::before{content:"\e071"}++.fa-scissors::before{content:"\e071"}++.fa-magnifying-glass::before{content:"\e072"}++.fa-search::before{content:"\e072"}++.fa-server::before{content:"\e074"}++.fa-shield::before{content:"\e075"}++.fa-shield-halved::before{content:"\e075"}++.fa-user-shield::before{content:"\e075"}++.fa-shirt::before{content:"\e076"}++.fa-sliders::before{content:"\e079"}++.fa-sliders-h::before{content:"\e079"}++.fa-face-smile::before{content:"\e07a"}++.fa-snowflake::before{content:"\e07b"}++.fa-sparkles::before{content:"\e07c"}++.fa-star-of-life::before{content:"\e07c"}++.fa-wand-magic::before{content:"\e07c"}++.fa-wand-magic-sparkles::before{content:"\e07c"}++.fa-star::before{content:"\e07d"}++.fa-list-check::before{content:"\e07e"}++.fa-stop::before{content:"\e07f"}++.fa-store::before{content:"\e080"}++.fa-sun::before{content:"\e081"}++.fa-thumbtack::before{content:"\e083"}++.fa-tag::before{content:"\e084"}++.fa-tags::before{content:"\e084"}++.fa-bullseye::before{content:"\e085"}++.fa-campground::before{content:"\e087"}++.fa-terminal::before{content:"\e088"}++.fa-font::before{content:"\e089"}++.fa-text-height::before{content:"\e089"}++.fa-ticket::before{content:"\e08a"}++.fa-train::before{content:"\e08b"}++.fa-trash::before{content:"\e08c"}++.fa-trash-can::before{content:"\e08c"}++.fa-tree::before{content:"\e08d"}++.fa-trophy::before{content:"\e08e"}++.fa-chevron-up::before{content:"\e08f"}++.fa-cloud-arrow-up::before{content:"\e090"}++.fa-upload::before{content:"\e090"}++.fa-user::before{content:"\e091"}++.fa-user-astronaut::before{content:"\e091"}++.fa-user-secret::before{content:"\e091"}++.fa-user-check::before{content:"\e092"}++.fa-video::before{content:"\e093"}++.fa-video-slash::before{content:"\e093"}++.fa-volume-high::before{content:"\e094"}++.fa-volume-low::before{content:"\e094"}++.fa-wallet::before{content:"\e095"}++.fa-person-swimming::before{content:"\e096"}++.fa-water::before{content:"\e096"}++.fa-wave-square::before{content:"\e097"}++.fa-signal::before{content:"\e098"}++.fa-wifi::before{content:"\e098"}++.fa-screwdriver-wrench::before{content:"\e099"}++.fa-wrench::before{content:"\e099"}++.fa-circle-xmark::before{content:"\e09a"}++.fa-xmark-circle::before{content:"\e09a"}++.fa-bolt::before{content:"\e09b"}++.fa-bolt-lightning::before{content:"\e09b"}nnew file mode 100644@@@ -0,0 +1,160 @@++/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */++/* <i class="ovi ovi-eye"></i>: the OpenVibe glyphs as a font. Sizes follow the text. */++@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}++.ovi{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}++.ovi-fw{width:1.25em;text-align:center}++.ovi-alert::before{content:"\e001"}++.ovi-alert-circle::before{content:"\e002"}++.ovi-archive::before{content:"\e003"}++.ovi-arrow-down::before{content:"\e004"}++.ovi-arrow-left::before{content:"\e005"}++.ovi-arrow-right::before{content:"\e006"}++.ovi-arrow-up::before{content:"\e007"}++.ovi-back::before{content:"\e008"}++.ovi-ban::before{content:"\e009"}++.ovi-bell::before{content:"\e00a"}++.ovi-book::before{content:"\e00b"}++.ovi-brain::before{content:"\e00c"}++.ovi-branch::before{content:"\e00d"}++.ovi-bridge::before{content:"\e00e"}++.ovi-busy::before{content:"\e00f"}++.ovi-calendar::before{content:"\e010"}++.ovi-camera::before{content:"\e011"}++.ovi-care::before{content:"\e012"}++.ovi-cart::before{content:"\e013"}++.ovi-chart::before{content:"\e014"}++.ovi-chat::before{content:"\e015"}++.ovi-check::before{content:"\e016"}++.ovi-check-circle::before{content:"\e017"}++.ovi-chevron::before{content:"\e018"}++.ovi-chip::before{content:"\e019"}++.ovi-circle::before{content:"\e01a"}++.ovi-city::before{content:"\e01b"}++.ovi-clipboard::before{content:"\e01c"}++.ovi-clock::before{content:"\e01d"}++.ovi-close::before{content:"\e01e"}++.ovi-cloud::before{content:"\e01f"}++.ovi-code::before{content:"\e020"}++.ovi-coin::before{content:"\e021"}++.ovi-compress::before{content:"\e022"}++.ovi-computer::before{content:"\e023"}++.ovi-copy::before{content:"\e024"}++.ovi-crown::before{content:"\e025"}++.ovi-database::before{content:"\e026"}++.ovi-dish::before{content:"\e027"}++.ovi-dollar::before{content:"\e028"}++.ovi-down::before{content:"\e029"}++.ovi-download::before{content:"\e02a"}++.ovi-drop::before{content:"\e02b"}++.ovi-enter::before{content:"\e02c"}++.ovi-expand::before{content:"\e02d"}++.ovi-eye::before{content:"\e02e"}++.ovi-eye-off::before{content:"\e02f"}++.ovi-file::before{content:"\e030"}++.ovi-film::before{content:"\e031"}++.ovi-filter::before{content:"\e032"}++.ovi-fire::before{content:"\e033"}++.ovi-flag::before{content:"\e034"}++.ovi-folder::before{content:"\e035"}++.ovi-food::before{content:"\e036"}++.ovi-forward::before{content:"\e037"}++.ovi-game::before{content:"\e038"}++.ovi-gauge::before{content:"\e039"}++.ovi-gavel::before{content:"\e03a"}++.ovi-gear::before{content:"\e03b"}++.ovi-gift::before{content:"\e03c"}++.ovi-globe::before{content:"\e03d"}++.ovi-grid::before{content:"\e03e"}++.ovi-hand::before{content:"\e03f"}++.ovi-hash::before{content:"\e040"}++.ovi-heart::before{content:"\e041"}++.ovi-help::before{content:"\e042"}++.ovi-history::before{content:"\e043"}++.ovi-home::before{content:"\e044"}++.ovi-id::before{content:"\e045"}++.ovi-idea::before{content:"\e046"}++.ovi-image::before{content:"\e047"}++.ovi-info::before{content:"\e048"}++.ovi-invite::before{content:"\e049"}++.ovi-key::before{content:"\e04a"}++.ovi-keyboard::before{content:"\e04b"}++.ovi-language::before{content:"\e04c"}++.ovi-layers::before{content:"\e04d"}++.ovi-leaf::before{content:"\e04e"}++.ovi-leave::before{content:"\e04f"}++.ovi-link::before{content:"\e050"}++.ovi-list::before{content:"\e051"}++.ovi-live::before{content:"\e052"}++.ovi-lock::before{content:"\e053"}++.ovi-mail::before{content:"\e054"}++.ovi-map::before{content:"\e055"}++.ovi-members::before{content:"\e056"}++.ovi-menu::before{content:"\e057"}++.ovi-mic::before{content:"\e058"}++.ovi-mic-off::before{content:"\e059"}++.ovi-minus::before{content:"\e05a"}++.ovi-moon::before{content:"\e05b"}++.ovi-more::before{content:"\e05c"}++.ovi-mountain::before{content:"\e05d"}++.ovi-music::before{content:"\e05e"}++.ovi-mute::before{content:"\e05f"}++.ovi-news::before{content:"\e060"}++.ovi-nodes::before{content:"\e061"}++.ovi-open::before{content:"\e062"}++.ovi-palette::before{content:"\e063"}++.ovi-pause::before{content:"\e064"}++.ovi-pencil::before{content:"\e065"}++.ovi-phone::before{content:"\e066"}++.ovi-pin::before{content:"\e067"}++.ovi-plane::before{content:"\e068"}++.ovi-play::before{content:"\e069"}++.ovi-plug::before{content:"\e06a"}++.ovi-plus::before{content:"\e06b"}++.ovi-reply::before{content:"\e06c"}++.ovi-retry::before{content:"\e06d"}++.ovi-robot::before{content:"\e06e"}++.ovi-route::before{content:"\e06f"}++.ovi-save::before{content:"\e070"}++.ovi-scissors::before{content:"\e071"}++.ovi-search::before{content:"\e072"}++.ovi-send::before{content:"\e073"}++.ovi-server::before{content:"\e074"}++.ovi-shield::before{content:"\e075"}++.ovi-shirt::before{content:"\e076"}++.ovi-shoe::before{content:"\e077"}++.ovi-sidebar::before{content:"\e078"}++.ovi-sliders::before{content:"\e079"}++.ovi-smile::before{content:"\e07a"}++.ovi-snow::before{content:"\e07b"}++.ovi-spark::before{content:"\e07c"}++.ovi-star::before{content:"\e07d"}++.ovi-steps::before{content:"\e07e"}++.ovi-stop::before{content:"\e07f"}++.ovi-store::before{content:"\e080"}++.ovi-sun::before{content:"\e081"}++.ovi-swap::before{content:"\e082"}++.ovi-tack::before{content:"\e083"}++.ovi-tag::before{content:"\e084"}++.ovi-target::before{content:"\e085"}++.ovi-task::before{content:"\e086"}++.ovi-tent::before{content:"\e087"}++.ovi-terminal::before{content:"\e088"}++.ovi-text::before{content:"\e089"}++.ovi-ticket::before{content:"\e08a"}++.ovi-train::before{content:"\e08b"}++.ovi-trash::before{content:"\e08c"}++.ovi-tree::before{content:"\e08d"}++.ovi-trophy::before{content:"\e08e"}++.ovi-up::before{content:"\e08f"}++.ovi-upload::before{content:"\e090"}++.ovi-user::before{content:"\e091"}++.ovi-user-check::before{content:"\e092"}++.ovi-video::before{content:"\e093"}++.ovi-volume::before{content:"\e094"}++.ovi-wallet::before{content:"\e095"}++.ovi-water::before{content:"\e096"}++.ovi-wave::before{content:"\e097"}++.ovi-wifi::before{content:"\e098"}++.ovi-wrench::before{content:"\e099"}++.ovi-x-circle::before{content:"\e09a"}++.ovi-zap::before{content:"\e09b"}@@@ -5,6 +5,12 @@* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.*++ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,++ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the++ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light++ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that++ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured.++ ** <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later* OVGlyphs.swap(rootEl) swaps inside one element now* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null@@@ -16,30 +22,44 @@if (typeof window === 'undefined' || window.OVGlyphs) return;const G = {"alert":"<path d=\"M12 4 2.8 19.5h18.4Z\"/><path d=\"M12 10v4M12 17h.01\"/>","alert-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5v5.5M12 16.5h.01\"/>","archive":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"5\" rx=\"1\"/><path d=\"M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4\"/>","arrow-down":"<path d=\"M12 4v16M6 14l6 6 6-6\"/>","arrow-left":"<path d=\"M20 12H4M10 6l-6 6 6 6\"/>","arrow-right":"<path d=\"M4 12h16M14 6l6 6-6 6\"/>","arrow-up":"<path d=\"M12 20V4M6 10l6-6 6 6\"/>","back":"<path d=\"M15 5l-7 7 7 7\"/>","ban":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m6 6 12 12\"/>","bell":"<path d=\"M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20a2 2 0 0 0 4 0\"/>","book":"<path d=\"M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Z\"/><path d=\"M4 19V5\"/>","brain":"<path d=\"M9 4a3 3 0 0 0-3 3 3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 6 1.2V5.5A2 2 0 0 0 9 4ZM15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-6 1.2\"/>","branch":"<circle cx=\"6\" cy=\"5\" r=\"2\"/><circle cx=\"6\" cy=\"19\" r=\"2\"/><circle cx=\"18\" cy=\"7\" r=\"2\"/><path d=\"M6 7v10M18 9c0 5.5-6.5 4-11 8.5\"/>","bridge":"<path d=\"M2 15.5h20M4.5 15.5V10M19.5 15.5V10M2 9.5c4 0 6-3 10-3s6 3 10 3M8.5 15.5V12M15.5 15.5V12M12 15.5V8\"/>","busy":"<path d=\"M6 12h.01M12 12h.01M18 12h.01\" stroke-width=\"3.4\"/>","calendar":"<rect x=\"4\" y=\"5\" width=\"16\" height=\"15\" rx=\"2\"/><path d=\"M4 10h16M9 3v4M15 3v4\"/>","camera":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><circle cx=\"12\" cy=\"13.3\" r=\"3.4\"/>","care":"<path d=\"M3 15.5h3l4 3h5.5a2 2 0 0 0 0-4H12M7 15.5l2.2-2h3.3\"/><path d=\"M15.5 10s-3.2-2-3.2-4.4a1.9 1.9 0 0 1 3.2-1.3 1.9 1.9 0 0 1 3.2 1.3c0 2.4-3.2 4.4-3.2 4.4Z\"/>","cart":"<path d=\"M3 4h2l2.4 11h11L21 7H6.2\"/><circle cx=\"9\" cy=\"19.5\" r=\"1.3\"/><circle cx=\"17\" cy=\"19.5\" r=\"1.3\"/>","chart":"<path d=\"M4 4v16h16\"/><path d=\"m7.5 14.5 3.5-4 3 2.5 5-6\"/>","chat":"<path d=\"M4 5h16v11H8l-4 4Z\"/>","check":"<path d=\"m5 12.5 4.5 4.5L19 7.5\"/>","check-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8 12.4 2.8 2.8L16 9.6\"/>","chevron":"<path d=\"m9 6 6 6-6 6\"/>","chip":"<rect x=\"7\" y=\"7\" width=\"10\" height=\"10\" rx=\"1.5\"/><path d=\"M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/>","city":"<path d=\"M3 21h18M5 21V9h5v12M10 21V4h6v17M16 21v-8h3.5v8M7.5 12h.01M7.5 15.5h.01M13 7.5h.01M13 11h.01M13 14.5h.01\"/>","clipboard":"<rect x=\"5.5\" y=\"5\" width=\"13\" height=\"16\" rx=\"2\"/><path d=\"M9 5V3.5h6V5M9 11h6M9 15h4\"/>","clock":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5V12l3 2\"/>","close":"<path d=\"M6 6l12 12M18 6 6 18\"/>","cloud":"<path d=\"M7 18h10a4 4 0 0 0 .5-8A6 6 0 0 0 6 9.5 4.3 4.3 0 0 0 7 18Z\"/>","code":"<path d=\"m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14\"/>","coin":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11\"/>","compress":"<path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/>","computer":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\"/><path d=\"M8 20h8M12 16v4\"/>","copy":"<rect x=\"8\" y=\"8\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2\"/>","crown":"<path d=\"M3 8.5 7.5 12.5 12 5.5l4.5 7 4.5-4L19 19H5Z\"/>","database":"<ellipse cx=\"12\" cy=\"6\" rx=\"7\" ry=\"3\"/><path d=\"M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3\"/>","dish":"<path d=\"M5 9.5A9.5 9.5 0 0 0 14.5 19Z\"/><path d=\"m9.5 14.5 4.5-4.5M14.5 4.5a5 5 0 0 1 5 5M14.5 8a1.5 1.5 0 0 1 1.5 1.5\"/>","dollar":"<path d=\"M16 7.5C15.3 6 13.8 5 12 5 9.5 5 8 6.3 8 8s1.7 2.6 4 3 4 1.4 4 3.3-1.6 3.2-4 3.2c-1.9 0-3.4-1-4-2.5M12 3v18\"/>","down":"<path d=\"m6 9 6 6 6-6\"/>","download":"<path d=\"M12 4v11M7 10l5 5 5-5M5 20h14\"/>","drop":"<path d=\"M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z\"/>","enter":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 8l4 4-4 4M14 12H4\"/>","expand":"<path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/>","eye":"<path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>","eye-off":"<path d=\"M3 12s3.5-6.5 9-6.5c1.6 0 3 .5 4.3 1.2M21 12s-3.5 6.5-9 6.5c-1.6 0-3-.5-4.3-1.2\"/><path d=\"M9.9 9.9a3 3 0 0 0 4.2 4.2M4 4l16 16\"/>","file":"<path d=\"M6 3h8l4 4v14H6Z\"/><path d=\"M14 3v4h4\"/>","film":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M7.5 4v16M16.5 4v16M3 9h4.5M3 15h4.5M16.5 9H21M16.5 15H21\"/>","filter":"<path d=\"M4 6h16M7 12h10M10 18h4\"/>","fire":"<path d=\"M12 21a6 6 0 0 0 6-6c0-3.8-2.6-6-3.7-9-1.8 2-2.3 4-2.3 4S10.8 8 9.8 6.2C7.6 8.7 6 11.6 6 15a6 6 0 0 0 6 6Z\"/>","flag":"<path d=\"M5 21V4M5 4h11l-2 4 2 4H5\"/>","folder":"<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\"/>","food":"<path d=\"M7 3v7M5 3v4.5a2 2 0 0 0 4 0V3M7 10v11M17 21V3c-2 1.2-3.2 3.6-3.2 7v3H17\"/>","game":"<rect x=\"2.5\" y=\"7\" width=\"19\" height=\"11\" rx=\"5\"/><path d=\"M7 11v3M5.5 12.5h3M15.5 12h.01M18 13.5h.01\"/>","gauge":"<path d=\"M4 18a8 8 0 1 1 16 0\"/><path d=\"m12 14.5 4-5.5\"/><circle cx=\"12\" cy=\"14.5\" r=\"1.3\"/>","gavel":"<path d=\"m14 4.5 5.5 5.5M10.5 8l5.5 5.5M12.3 6.3l-6 6 5.4 5.4 6-6M3 21l6.3-6.3M14 21h7\"/>","gear":"<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z\"/>","gift":"<rect x=\"4\" y=\"9\" width=\"16\" height=\"11\" rx=\"1\"/><path d=\"M3 9h18M12 9v11M12 9c-1.5-3-5-4-5-1.5S12 9 12 9s5-1 5-3.5S13.5 6 12 9\"/>","globe":"<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\"/>","grid":"<rect x=\"4\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"4\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/>","hand":"<path d=\"M8 13V5.5a1.5 1.5 0 0 1 3 0V11M11 10V4.5a1.5 1.5 0 0 1 3 0V11M14 10.5V6a1.5 1.5 0 0 1 3 0v7c0 4-2.5 7-6.5 7-2.6 0-4.2-1.3-5.7-3.6L3 13.6a1.5 1.5 0 0 1 2.4-1.8L8 15\"/>","hash":"<path d=\"M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17\"/>","heart":"<path d=\"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z\"/>","help":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01\"/>","history":"<path d=\"M4 12a8 8 0 1 0 2.4-5.7L4 8.6M4 4.5v4.1h4.1\"/><path d=\"M12 8v4.2l3 1.8\"/>","home":"<path d=\"M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1Z\"/>","id":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><circle cx=\"9\" cy=\"11\" r=\"2\"/><path d=\"M6 16c.5-1.5 1.7-2.3 3-2.3s2.5.8 3 2.3M15 10h3M15 13.5h3\"/>","idea":"<path d=\"M9 18h6M10 21h4M8.6 14.6A6 6 0 1 1 15.4 14.6c-.7.6-1.4 1.5-1.4 2.4h-4c0-.9-.7-1.8-1.4-2.4Z\"/>","image":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"3\"/><circle cx=\"9\" cy=\"10\" r=\"1.6\"/><path d=\"m21 16-5-5-8 8\"/>","info":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 11v5M12 8h.01\"/>","invite":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M19 8v6M16 11h6\"/>","key":"<circle cx=\"8\" cy=\"15\" r=\"4\"/><path d=\"m10.8 12.2 8.7-8.7M16 7l3 3M14 9l2 2\"/>","keyboard":"<rect x=\"2.5\" y=\"6\" width=\"19\" height=\"12\" rx=\"2\"/><path d=\"M6 10h.01M9 10h.01M12 10h.01M15 10h.01M18 10h.01M8 14h8\"/>","language":"<path d=\"M4 5h9M8.5 3v2M6 5c.5 3 2.5 6 6 7.5M11 5c-.6 3.4-2.8 6.8-6.5 8.5M12 21l4-9 4 9M13.4 18h5.2\"/>","layers":"<path d=\"m12 3.5 9 5-9 5-9-5Z\"/><path d=\"m3 13 9 5 9-5\"/>","leaf":"<path d=\"M5 19c0-8.5 5-13.6 15-15-.9 9.9-6 15-15 15ZM5 19l8-8\"/>","leave":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 16l-4-4 4-4M6 12h10\"/>","link":"<path d=\"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1\"/>","list":"<path d=\"M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01\"/>","live":"<circle cx=\"12\" cy=\"12\" r=\"2.5\"/><path d=\"M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14\"/>","lock":"<rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\"/><path d=\"M8 11V8a4 4 0 0 1 8 0v3\"/>","mail":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"m4 7 8 6 8-6\"/>","map":"<path d=\"M9 4 3 6v14l6-2 6 2 6-2V4l-6 2Z\"/><path d=\"M9 4v14M15 6v14\"/>","members":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 4.8a3.2 3.2 0 0 1 0 6.4M18 14.8c1.9.6 3 2.4 3 5.2\"/>","menu":"<path d=\"M4 7h16M4 12h16M4 17h16\"/>","mic":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 14 0M12 18v3\"/>","mic-off":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 11.6 5.3M19 11c0 .8-.1 1.6-.4 2.3M12 18v3M4 4l16 16\"/>","minus":"<path d=\"M5 12h14\"/>","moon":"<path d=\"M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\"/>","more":"<circle cx=\"5\" cy=\"12\" r=\"1.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\"/>","mountain":"<path d=\"M3 20 9.5 8.5l4 6.5 2.5-3.5L21 20Z\"/>","music":"<path d=\"M9 18V6l11-2v12\"/><circle cx=\"6.5\" cy=\"18\" r=\"2.5\"/><circle cx=\"17.5\" cy=\"16\" r=\"2.5\"/>","mute":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"m16 10 4 4M20 10l-4 4\"/>","news":"<path d=\"M4 5h13v14a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2Z\"/><path d=\"M17 9h3v10a2 2 0 0 1-2 2M7.5 9h6M7.5 12.5h6M7.5 16h3.5\"/>","nodes":"<circle cx=\"12\" cy=\"5\" r=\"2.2\"/><circle cx=\"5.5\" cy=\"18\" r=\"2.2\"/><circle cx=\"18.5\" cy=\"18\" r=\"2.2\"/><path d=\"M11 7 6.5 16M13 7l4.5 9M7.7 18h8.6\"/>","open":"<path d=\"M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\"/>","palette":"<path d=\"M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6s.8-1.7 2-1.7H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3Z\"/><circle cx=\"7.5\" cy=\"11\" r=\"1.2\"/><circle cx=\"10\" cy=\"7.3\" r=\"1.2\"/><circle cx=\"14.5\" cy=\"7.3\" r=\"1.2\"/>","pause":"<path d=\"M8 5v14M16 5v14\"/>","pencil":"<path d=\"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"/>","phone":"<path d=\"M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2\"/>","pin":"<path d=\"M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z\"/><circle cx=\"12\" cy=\"10\" r=\"2.4\"/>","plane":"<path d=\"M20.5 3.5 3.5 10.5l6.8 3.2 3.2 6.8Z\"/><path d=\"m10.3 13.7 10.2-10.2\"/>","play":"<path d=\"M8 5v14l11-7Z\"/>","plug":"<path d=\"M9 3v5M15 3v5M6.5 8h11v3a5.5 5.5 0 0 1-11 0ZM12 16.5V21\"/>","plus":"<path d=\"M12 5v14M5 12h14\"/>","reply":"<path d=\"M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6v1\"/>","retry":"<path d=\"M4 12a8 8 0 0 1 13.7-5.7L20 8.5M20 4v4.5h-4.5M20 12a8 8 0 0 1-13.7 5.7L4 15.5M4 20v-4.5h4.5\"/>","robot":"<rect x=\"5\" y=\"8\" width=\"14\" height=\"11\" rx=\"3\"/><path d=\"M12 4v4M9 13h.01M15 13h.01M9.5 16.5h5M3 13v2M21 13v2\"/>","route":"<circle cx=\"6\" cy=\"18\" r=\"2.4\"/><circle cx=\"18\" cy=\"6\" r=\"2.4\"/><path d=\"M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6\"/>","save":"<path d=\"M5 4h11l3 3v13H5Z\"/><path d=\"M8 4v5h7V4M8 20v-6h8v6\"/>","scissors":"<circle cx=\"6\" cy=\"7\" r=\"2.6\"/><circle cx=\"6\" cy=\"17\" r=\"2.6\"/><path d=\"M8.2 8.6 20 18M8.2 15.4 20 6\"/>","search":"<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m20 20-4.2-4.2\"/>","server":"<rect x=\"4\" y=\"4\" width=\"16\" height=\"7\" rx=\"2\"/><rect x=\"4\" y=\"13\" width=\"16\" height=\"7\" rx=\"2\"/><path d=\"M8 7.5h.01M8 16.5h.01\"/>","shield":"<path d=\"M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6Z\"/>","shirt":"<path d=\"M8 3 3 6l2 4 2-1v12h10V9l2 1 2-4-5-3c-.5 1.5-2 2.5-4 2.5S8.5 4.5 8 3Z\"/>","sliders":"<path d=\"M4 7h9.5M18.5 7H20M4 17h3.5M12.5 17H20\"/><circle cx=\"16\" cy=\"7\" r=\"2.3\"/><circle cx=\"10\" cy=\"17\" r=\"2.3\"/>","smile":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01\"/>","snow":"<path d=\"M12 3v18M4.2 7.5l15.6 9M4.2 16.5l15.6-9M9.5 4.5 12 7l2.5-2.5M9.5 19.5 12 17l2.5 2.5\"/>","spark":"<path d=\"M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8ZM19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z\"/>","star":"<path d=\"m12 4 2.4 5 5.4.6-4 3.7 1.1 5.4L12 16l-4.9 2.7 1.1-5.4-4-3.7 5.4-.6Z\"/>","steps":"<path d=\"M9 6h11M9 12h11M9 18h11\"/><path d=\"m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17\"/>","stop":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8.5 8.5 7 7\"/>","store":"<path d=\"M4 9 5.5 4h13L20 9M4 9h16v1.5a2.7 2.7 0 0 1-5.3 0 2.7 2.7 0 0 1-5.4 0A2.7 2.7 0 0 1 4 10.5ZM5 13v7h14v-7M10 20v-4h4v4\"/>","sun":"<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/>","tack":"<path d=\"M9 3h6l-1 5 3 3H7l3-3ZM12 11v10\"/>","tag":"<path d=\"M3 12V4h8l10 10-8 8Z\"/><circle cx=\"7.5\" cy=\"8.5\" r=\"1.3\"/>","target":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/>","tent":"<path d=\"M2.5 20h19M12 5 4 20M12 5l8 15M12 13.5 9 20M12 13.5l3 6.5\"/>","terminal":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"m7 9 3 3-3 3M13 15h4\"/>","text":"<path d=\"M5.5 20 11 4h2l5.5 16M7.6 14h8.8\"/>","ticket":"<path d=\"M3 7h18v3a2 2 0 0 0 0 4v3H3v-3a2 2 0 0 0 0-4Z\"/><path d=\"M14 7v10\"/>","train":"<rect x=\"6\" y=\"3\" width=\"12\" height=\"13\" rx=\"3\"/><path d=\"M6 10h12M9 13h.01M15 13h.01M8.5 21l2-3.5M15.5 21l-2-3.5\"/>","trash":"<path d=\"M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3\"/>","tree":"<path d=\"M12 3 6.5 10.5h3L5 17h14l-4.5-6.5h3ZM12 17v4\"/>","trophy":"<path d=\"M8 4h8v5a4 4 0 0 1-8 0ZM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 20h8\"/>","up":"<path d=\"m6 15 6-6 6 6\"/>","upload":"<path d=\"M12 20V9M7 14l5-5 5 5M5 4h14\"/>","user":"<circle cx=\"12\" cy=\"8\" r=\"3.5\"/><path d=\"M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6\"/>","user-check":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 11l2 2 4-4\"/>","video":"<rect x=\"3\" y=\"6\" width=\"13\" height=\"12\" rx=\"2\"/><path d=\"m16 10 5-3v10l-5-3\"/>","volume":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10\"/>","wallet":"<path d=\"M4 7.5A2.5 2.5 0 0 1 6.5 5H18v4M4 7.5V18a2 2 0 0 0 2 2h14V9H6.5A2.5 2.5 0 0 1 4 7.5Z\"/><path d=\"M16 14.5h.01\"/>","water":"<path d=\"M2.5 9c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2M2.5 15c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2\"/>","wave":"<path d=\"M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2\"/><path d=\"M5.5 11.5A7 7 0 0 1 18 8.6c-2.3-.5-4.2.4-4.8 2.4\"/>","wifi":"<path d=\"M2.5 9a14 14 0 0 1 19 0M5.5 12.5a9.5 9.5 0 0 1 13 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01\"/>","wrench":"<path d=\"M14.5 4a5 5 0 0 0-4.4 7L4 17.1V20h2.9l6.1-6.1a5 5 0 0 0 7-4.4l-3 1.1-2.4-2.4 1.1-3Z\"/>","x-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m9 9 6 6M15 9l-6 6\"/>","zap":"<path d=\"M13 3 5 13h6l-1 8 8-10h-6Z\"/>"};const FA = {"angle-down":"down","angle-right":"chevron","arrow-down":"arrow-down","arrow-left":"arrow-left","arrow-right":"arrow-right","arrow-right-from-bracket":"leave","arrow-right-to-bracket":"enter","arrow-rotate-right":"retry","arrow-up":"arrow-up","arrow-up-right-from-square":"open","arrows-rotate":"retry","ban":"ban","bars":"menu","bell":"bell","bolt":"zap","bolt-lightning":"zap","book":"book","book-open":"book","box-archive":"archive","brain":"brain","bridge":"bridge","broadcast-tower":"live","bullseye":"target","calendar":"calendar","calendar-days":"calendar","camera":"camera","campground":"tent","cart":"cart","cart-shopping":"cart","chart-bar":"chart","chart-line":"chart","chart-simple":"chart","check":"check","check-circle":"check-circle","chevron-down":"down","chevron-left":"back","chevron-right":"chevron","chevron-up":"up","circle":"circle","circle-check":"check-circle","circle-exclamation":"alert-circle","circle-info":"info","circle-nodes":"nodes","circle-notch":"busy","circle-play":"play","circle-question":"help","circle-xmark":"x-circle","city":"city","clipboard":"clipboard","clipboard-list":"clipboard","clock":"clock","clock-rotate-left":"history","cloud":"cloud","cloud-arrow-up":"upload","cloud-rain":"cloud","cloud-showers-heavy":"cloud","cloud-sun":"cloud","code":"code","code-branch":"branch","code-fork":"branch","coin":"coin","coins":"coin","comment":"chat","comment-dots":"chat","comments":"chat","compress":"compress","copy":"copy","crown":"crown","cut":"scissors","database":"database","desktop":"computer","diagram-project":"nodes","display":"computer","dollar-sign":"dollar","download":"download","droplet":"drop","earth-americas":"globe","earth-europe":"globe","ellipsis":"more","ellipsis-vertical":"more","envelope":"mail","expand":"expand","external-link":"open","eye":"eye","eye-slash":"eye-off","face-smile":"smile","file":"file","file-audio":"file","file-image":"image","file-lines":"file","file-pdf":"file","film":"film","filter":"filter","fire":"fire","flag":"flag","floppy-disk":"save","folder":"folder","font":"text","gamepad":"game","gauge":"gauge","gauge-high":"gauge","gavel":"gavel","gear":"gear","gears":"gear","gift":"gift","globe":"globe","grid":"grid","hand":"hand","hand-holding-dollar":"coin","hand-holding-heart":"care","hand-pointer":"hand","hashtag":"hash","heart":"heart","house":"home","house-chimney":"home","id-card":"id","image":"image","images":"image","key":"key","keyboard":"keyboard","language":"language","laptop":"computer","layer-group":"layers","leaf":"leaf","lightbulb":"idea","link":"link","list":"list","list-check":"steps","list-ul":"list","location-dot":"pin","location-pin":"pin","lock":"lock","magnifying-glass":"search","map":"map","map-pin":"pin","message":"chat","microchip":"chip","microphone":"mic","microphone-slash":"mic-off","minus":"minus","mobile-screen":"phone","moon":"moon","mountain":"mountain","mountain-sun":"mountain","music":"music","network-wired":"nodes","newspaper":"news","palette":"palette","paper-plane":"plane","paste":"clipboard","pause":"pause","pen":"pencil","pen-nib":"pencil","pen-to-square":"pencil","pencil":"pencil","people-group":"members","person-swimming":"water","phone":"phone","photo-film":"film","play":"play","plug":"plug","plus":"plus","question":"help","reply":"reply","right-from-bracket":"leave","right-to-bracket":"enter","robot":"robot","rotate":"retry","rotate-left":"retry","rotate-right":"retry","route":"route","satellite-dish":"dish","scissors":"scissors","screwdriver-wrench":"wrench","search":"search","server":"server","share-nodes":"nodes","shield":"shield","shield-halved":"shield","shirt":"shirt","signal":"wifi","sitemap":"nodes","sliders":"sliders","sliders-h":"sliders","snowflake":"snow","sparkles":"spark","spinner":"busy","star":"star","star-of-life":"spark","stop":"stop","store":"store","sun":"sun","table-cells":"grid","tag":"tag","tags":"tag","terminal":"terminal","text-height":"text","thumbtack":"tack","ticket":"ticket","times":"close","tower-broadcast":"live","tower-cell":"live","train":"train","trash":"trash","trash-can":"trash","tree":"tree","triangle-exclamation":"alert","trophy":"trophy","unlock":"lock","up-right-and-down-left-from-center":"expand","up-right-from-square":"open","upload":"upload","user":"user","user-astronaut":"user","user-check":"user-check","user-group":"members","user-plus":"invite","user-secret":"user","user-shield":"shield","users":"members","utensils":"food","video":"video","video-slash":"video","volume-high":"volume","volume-low":"volume","volume-off":"mute","volume-xmark":"mute","wallet":"wallet","wand-magic":"spark","wand-magic-sparkles":"spark","water":"water","wave-square":"wave","wifi":"wifi","wrench":"wrench","xmark":"close","xmark-circle":"x-circle"};-- // Font Awesome's style and size classes, and its animations: they say how, not what.-- const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity)$/;++ // Font Awesome's style, size, layout and animation classes: they say how, not what.++ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';-- const CSS = '.ovg::before,.ovg::after{content:none!important;display:none!important}'-- + '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}'-- + '.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'-- + '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;overflow:visible}'-- + '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}'-- + '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}'-- + '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}'-- + '@media (prefers-reduced-motion:reduce){.ovg[data-ovg=busy] circle{animation:none}}html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none}';++ const CSS = ".ovg::before,.ovg::after{content:none!important;display:none!important}.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:var(--ovg-stroke,2);stroke-linecap:round;stroke-linejoin:round;overflow:visible}.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}.ovg.fa-1x{font-size:1em}.ovg.fa-2x{font-size:2em}.ovg.fa-3x{font-size:3em}.ovg.fa-4x{font-size:4em}.ovg.fa-5x{font-size:5em}.ovg.fa-6x{font-size:6em}.ovg.fa-7x{font-size:7em}.ovg.fa-8x{font-size:8em}.ovg.fa-9x{font-size:9em}.ovg.fa-10x{font-size:10em}.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}.ovg.fa-beat{animation:ovgBeat var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-bounce{animation:ovgBounce var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-fade{animation:ovgFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-beat-fade{animation:ovgBeatFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-flip{animation:ovgFlip var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-shake{animation:ovgShake var(--fa-animation-duration,1s) var(--fa-animation-timing,linear) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}html[data-ov-motion=reduced] .ovg,html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none!important}";const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };++ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).++ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;++ function transformOf(spec) {++ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;++ for (const p of String(spec || '').toLowerCase().split(/\s+/)) {++ const m = TF.exec(p);++ if (!m) continue;++ any = true;++ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }++ const n = Number(m[2]);++ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;++ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;++ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;++ }++ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';++ }function draw(el) {if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;const fa = nameOf(el);const g = fa && fromFa(fa);-- if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; el.textContent = ''; } return; }-- if (el.dataset.ovgFa === fa) return;++ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }++ const tf = el.getAttribute('data-fa-transform') || '';++ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself foreverif (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an iconel.classList.add('ovg');el.dataset.ovg = g; el.dataset.ovgFa = fa;-- if (!el.hasAttribute('aria-label') && !el.hasAttribute('title')) el.setAttribute('aria-hidden', 'true');-- el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + (g === 'busy' ? BUSY : G[g]) + '</svg>';++ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;++ const title = el.getAttribute('title');++ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }++ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');++ const t = transformOf(tf);++ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g]) + '</svg>';}const SEL = 'i[class*="fa-"],span[class*="fa-"]';function swap(root) {@@@ -56,7 +76,7 @@if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });}-- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });++ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });} catch (e) { /* no observer: what was there is swapped */ }}window.OVGlyphs = { swap, fromFa };nnew file mode 100644@@@ -0,0 +1,158 @@++<svg xmlns="http://www.w3.org/2000/svg">++<!-- The OpenVibe glyphs (openvibe-shared, MIT): <svg><use href="ov-glyphs.svg#ovg-eye"/></svg>. Generated by scripts/build-glyphs.js. -->++<symbol id="ovg-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 2.8 19.5h18.4Z"/><path d="M12 10v4M12 17h.01"/></symbol>++<symbol id="ovg-alert-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v5.5M12 16.5h.01"/></symbol>++<symbol id="ovg-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="5" rx="1"/><path d="M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4"/></symbol>++<symbol id="ovg-arrow-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16M6 14l6 6 6-6"/></symbol>++<symbol id="ovg-arrow-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12H4M10 6l-6 6 6 6"/></symbol>++<symbol id="ovg-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16M14 6l6 6-6 6"/></symbol>++<symbol id="ovg-arrow-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V4M6 10l6-6 6 6"/></symbol>++<symbol id="ovg-back" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></symbol>++<symbol id="ovg-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m6 6 12 12"/></symbol>++<symbol id="ovg-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20a2 2 0 0 0 4 0"/></symbol>++<symbol id="ovg-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Z"/><path d="M4 19V5"/></symbol>++<symbol id="ovg-brain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4a3 3 0 0 0-3 3 3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 6 1.2V5.5A2 2 0 0 0 9 4ZM15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-6 1.2"/></symbol>++<symbol id="ovg-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5" r="2"/><circle cx="6" cy="19" r="2"/><circle cx="18" cy="7" r="2"/><path d="M6 7v10M18 9c0 5.5-6.5 4-11 8.5"/></symbol>++<symbol id="ovg-bridge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15.5h20M4.5 15.5V10M19.5 15.5V10M2 9.5c4 0 6-3 10-3s6 3 10 3M8.5 15.5V12M15.5 15.5V12M12 15.5V8"/></symbol>++<symbol id="ovg-busy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3.4"/></symbol>++<symbol id="ovg-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M9 3v4M15 3v4"/></symbol>++<symbol id="ovg-camera" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z"/><circle cx="12" cy="13.3" r="3.4"/></symbol>++<symbol id="ovg-care" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15.5h3l4 3h5.5a2 2 0 0 0 0-4H12M7 15.5l2.2-2h3.3"/><path d="M15.5 10s-3.2-2-3.2-4.4a1.9 1.9 0 0 1 3.2-1.3 1.9 1.9 0 0 1 3.2 1.3c0 2.4-3.2 4.4-3.2 4.4Z"/></symbol>++<symbol id="ovg-cart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h2l2.4 11h11L21 7H6.2"/><circle cx="9" cy="19.5" r="1.3"/><circle cx="17" cy="19.5" r="1.3"/></symbol>++<symbol id="ovg-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v16h16"/><path d="m7.5 14.5 3.5-4 3 2.5 5-6"/></symbol>++<symbol id="ovg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4Z"/></symbol>++<symbol id="ovg-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>++<symbol id="ovg-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m8 12.4 2.8 2.8L16 9.6"/></symbol>++<symbol id="ovg-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></symbol>++<symbol id="ovg-chip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="10" rx="1.5"/><path d="M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4"/></symbol>++<symbol id="ovg-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/></symbol>++<symbol id="ovg-city" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M5 21V9h5v12M10 21V4h6v17M16 21v-8h3.5v8M7.5 12h.01M7.5 15.5h.01M13 7.5h.01M13 11h.01M13 14.5h.01"/></symbol>++<symbol id="ovg-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="5" width="13" height="16" rx="2"/><path d="M9 5V3.5h6V5M9 11h6M9 15h4"/></symbol>++<symbol id="ovg-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>++<symbol id="ovg-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>++<symbol id="ovg-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18h10a4 4 0 0 0 .5-8A6 6 0 0 0 6 9.5 4.3 4.3 0 0 0 7 18Z"/></symbol>++<symbol id="ovg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14"/></symbol>++<symbol id="ovg-coin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11"/></symbol>++<symbol id="ovg-compress" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></symbol>++<symbol id="ovg-computer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></symbol>++<symbol id="ovg-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></symbol>++<symbol id="ovg-crown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5 7.5 12.5 12 5.5l4.5 7 4.5-4L19 19H5Z"/></symbol>++<symbol id="ovg-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6" rx="7" ry="3"/><path d="M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3"/></symbol>++<symbol id="ovg-dish" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9.5A9.5 9.5 0 0 0 14.5 19Z"/><path d="m9.5 14.5 4.5-4.5M14.5 4.5a5 5 0 0 1 5 5M14.5 8a1.5 1.5 0 0 1 1.5 1.5"/></symbol>++<symbol id="ovg-dollar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7.5C15.3 6 13.8 5 12 5 9.5 5 8 6.3 8 8s1.7 2.6 4 3 4 1.4 4 3.3-1.6 3.2-4 3.2c-1.9 0-3.4-1-4-2.5M12 3v18"/></symbol>++<symbol id="ovg-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>++<symbol id="ovg-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></symbol>++<symbol id="ovg-drop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/></symbol>++<symbol id="ovg-enter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 8l4 4-4 4M14 12H4"/></symbol>++<symbol id="ovg-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></symbol>++<symbol id="ovg-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="3"/></symbol>++<symbol id="ovg-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12s3.5-6.5 9-6.5c1.6 0 3 .5 4.3 1.2M21 12s-3.5 6.5-9 6.5c-1.6 0-3-.5-4.3-1.2"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M4 4l16 16"/></symbol>++<symbol id="ovg-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14H6Z"/><path d="M14 3v4h4"/></symbol>++<symbol id="ovg-film" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7.5 4v16M16.5 4v16M3 9h4.5M3 15h4.5M16.5 9H21M16.5 15H21"/></symbol>++<symbol id="ovg-filter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M7 12h10M10 18h4"/></symbol>++<symbol id="ovg-fire" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21a6 6 0 0 0 6-6c0-3.8-2.6-6-3.7-9-1.8 2-2.3 4-2.3 4S10.8 8 9.8 6.2C7.6 8.7 6 11.6 6 15a6 6 0 0 0 6 6Z"/></symbol>++<symbol id="ovg-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></symbol>++<symbol id="ovg-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></symbol>++<symbol id="ovg-food" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3v7M5 3v4.5a2 2 0 0 0 4 0V3M7 10v11M17 21V3c-2 1.2-3.2 3.6-3.2 7v3H17"/></symbol>++<symbol id="ovg-forward" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 5 7 7-7 7"/></symbol>++<symbol id="ovg-game" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="7" width="19" height="11" rx="5"/><path d="M7 11v3M5.5 12.5h3M15.5 12h.01M18 13.5h.01"/></symbol>++<symbol id="ovg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 18a8 8 0 1 1 16 0"/><path d="m12 14.5 4-5.5"/><circle cx="12" cy="14.5" r="1.3"/></symbol>++<symbol id="ovg-gavel" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 4.5 5.5 5.5M10.5 8l5.5 5.5M12.3 6.3l-6 6 5.4 5.4 6-6M3 21l6.3-6.3M14 21h7"/></symbol>++<symbol id="ovg-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></symbol>++<symbol id="ovg-gift" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="9" width="16" height="11" rx="1"/><path d="M3 9h18M12 9v11M12 9c-1.5-3-5-4-5-1.5S12 9 12 9s5-1 5-3.5S13.5 6 12 9"/></symbol>++<symbol id="ovg-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18"/></symbol>++<symbol id="ovg-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></symbol>++<symbol id="ovg-hand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V5.5a1.5 1.5 0 0 1 3 0V11M11 10V4.5a1.5 1.5 0 0 1 3 0V11M14 10.5V6a1.5 1.5 0 0 1 3 0v7c0 4-2.5 7-6.5 7-2.6 0-4.2-1.3-5.7-3.6L3 13.6a1.5 1.5 0 0 1 2.4-1.8L8 15"/></symbol>++<symbol id="ovg-hash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17"/></symbol>++<symbol id="ovg-heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></symbol>++<symbol id="ovg-help" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01"/></symbol>++<symbol id="ovg-history" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 1 0 2.4-5.7L4 8.6M4 4.5v4.1h4.1"/><path d="M12 8v4.2l3 1.8"/></symbol>++<symbol id="ovg-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1Z"/></symbol>++<symbol id="ovg-id" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.5-1.5 1.7-2.3 3-2.3s2.5.8 3 2.3M15 10h3M15 13.5h3"/></symbol>++<symbol id="ovg-idea" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 21h4M8.6 14.6A6 6 0 1 1 15.4 14.6c-.7.6-1.4 1.5-1.4 2.4h-4c0-.9-.7-1.8-1.4-2.4Z"/></symbol>++<symbol id="ovg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></symbol>++<symbol id="ovg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/></symbol>++<symbol id="ovg-invite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M19 8v6M16 11h6"/></symbol>++<symbol id="ovg-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m10.8 12.2 8.7-8.7M16 7l3 3M14 9l2 2"/></symbol>++<symbol id="ovg-keyboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="6" width="19" height="12" rx="2"/><path d="M6 10h.01M9 10h.01M12 10h.01M15 10h.01M18 10h.01M8 14h8"/></symbol>++<symbol id="ovg-language" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h9M8.5 3v2M6 5c.5 3 2.5 6 6 7.5M11 5c-.6 3.4-2.8 6.8-6.5 8.5M12 21l4-9 4 9M13.4 18h5.2"/></symbol>++<symbol id="ovg-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3.5 9 5-9 5-9-5Z"/><path d="m3 13 9 5 9-5"/></symbol>++<symbol id="ovg-leaf" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19c0-8.5 5-13.6 15-15-.9 9.9-6 15-15 15ZM5 19l8-8"/></symbol>++<symbol id="ovg-leave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 16l-4-4 4-4M6 12h10"/></symbol>++<symbol id="ovg-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></symbol>++<symbol id="ovg-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></symbol>++<symbol id="ovg-live" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.5"/><path d="M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14"/></symbol>++<symbol id="ovg-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></symbol>++<symbol id="ovg-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>++<symbol id="ovg-map" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4 3 6v14l6-2 6 2 6-2V4l-6 2Z"/><path d="M9 4v14M15 6v14"/></symbol>++<symbol id="ovg-members" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 4.8a3.2 3.2 0 0 1 0 6.4M18 14.8c1.9.6 3 2.4 3 5.2"/></symbol>++<symbol id="ovg-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>++<symbol id="ovg-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></symbol>++<symbol id="ovg-mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 11.6 5.3M19 11c0 .8-.1 1.6-.4 2.3M12 18v3M4 4l16 16"/></symbol>++<symbol id="ovg-minus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></symbol>++<symbol id="ovg-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></symbol>++<symbol id="ovg-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></symbol>++<symbol id="ovg-mountain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 20 9.5 8.5l4 6.5 2.5-3.5L21 20Z"/></symbol>++<symbol id="ovg-music" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V6l11-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="17.5" cy="16" r="2.5"/></symbol>++<symbol id="ovg-mute" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5h3.5L12 6v12l-4.5-3.5H4Z"/><path d="m16 10 4 4M20 10l-4 4"/></symbol>++<symbol id="ovg-news" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h13v14a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2Z"/><path d="M17 9h3v10a2 2 0 0 1-2 2M7.5 9h6M7.5 12.5h6M7.5 16h3.5"/></symbol>++<symbol id="ovg-nodes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="2.2"/><circle cx="5.5" cy="18" r="2.2"/><circle cx="18.5" cy="18" r="2.2"/><path d="M11 7 6.5 16M13 7l4.5 9M7.7 18h8.6"/></symbol>++<symbol id="ovg-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></symbol>++<symbol id="ovg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6s.8-1.7 2-1.7H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3Z"/><circle cx="7.5" cy="11" r="1.2"/><circle cx="10" cy="7.3" r="1.2"/><circle cx="14.5" cy="7.3" r="1.2"/></symbol>++<symbol id="ovg-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5v14M16 5v14"/></symbol>++<symbol id="ovg-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>++<symbol id="ovg-phone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></symbol>++<symbol id="ovg-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z"/><circle cx="12" cy="10" r="2.4"/></symbol>++<symbol id="ovg-plane" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 3.5 3.5 10.5l6.8 3.2 3.2 6.8Z"/><path d="m10.3 13.7 10.2-10.2"/></symbol>++<symbol id="ovg-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5v14l11-7Z"/></symbol>++<symbol id="ovg-plug" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3v5M15 3v5M6.5 8h11v3a5.5 5.5 0 0 1-11 0ZM12 16.5V21"/></symbol>++<symbol id="ovg-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>++<symbol id="ovg-reply" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6v1"/></symbol>++<symbol id="ovg-retry" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 0 1 13.7-5.7L20 8.5M20 4v4.5h-4.5M20 12a8 8 0 0 1-13.7 5.7L4 15.5M4 20v-4.5h4.5"/></symbol>++<symbol id="ovg-robot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="8" width="14" height="11" rx="3"/><path d="M12 4v4M9 13h.01M15 13h.01M9.5 16.5h5M3 13v2M21 13v2"/></symbol>++<symbol id="ovg-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="18" r="2.4"/><circle cx="18" cy="6" r="2.4"/><path d="M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6"/></symbol>++<symbol id="ovg-save" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h11l3 3v13H5Z"/><path d="M8 4v5h7V4M8 20v-6h8v6"/></symbol>++<symbol id="ovg-scissors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="7" r="2.6"/><circle cx="6" cy="17" r="2.6"/><path d="M8.2 8.6 20 18M8.2 15.4 20 6"/></symbol>++<symbol id="ovg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></symbol>++<symbol id="ovg-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></symbol>++<symbol id="ovg-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="7" rx="2"/><rect x="4" y="13" width="16" height="7" rx="2"/><path d="M8 7.5h.01M8 16.5h.01"/></symbol>++<symbol id="ovg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6Z"/></symbol>++<symbol id="ovg-shirt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 3 6l2 4 2-1v12h10V9l2 1 2-4-5-3c-.5 1.5-2 2.5-4 2.5S8.5 4.5 8 3Z"/></symbol>++<symbol id="ovg-shoe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17v-5.5l4-.5 3-4 3.2 1.4-.9 2.6 4 2.2 4.7 1a2 2 0 0 1 1.5 1.9V17Z"/><path d="M2.5 20h19"/></symbol>++<symbol id="ovg-sidebar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M9.5 4.5v15"/></symbol>++<symbol id="ovg-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h9.5M18.5 7H20M4 17h3.5M12.5 17H20"/><circle cx="16" cy="7" r="2.3"/><circle cx="10" cy="17" r="2.3"/></symbol>++<symbol id="ovg-smile" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01"/></symbol>++<symbol id="ovg-snow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M4.2 7.5l15.6 9M4.2 16.5l15.6-9M9.5 4.5 12 7l2.5-2.5M9.5 19.5 12 17l2.5 2.5"/></symbol>++<symbol id="ovg-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8ZM19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z"/></symbol>++<symbol id="ovg-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 4 2.4 5 5.4.6-4 3.7 1.1 5.4L12 16l-4.9 2.7 1.1-5.4-4-3.7 5.4-.6Z"/></symbol>++<symbol id="ovg-steps" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11M9 12h11M9 18h11"/><path d="m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17"/></symbol>++<symbol id="ovg-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m8.5 8.5 7 7"/></symbol>++<symbol id="ovg-store" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9 5.5 4h13L20 9M4 9h16v1.5a2.7 2.7 0 0 1-5.3 0 2.7 2.7 0 0 1-5.4 0A2.7 2.7 0 0 1 4 10.5ZM5 13v7h14v-7M10 20v-4h4v4"/></symbol>++<symbol id="ovg-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>++<symbol id="ovg-swap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h14l-3.5-3.5M20 16H6l3.5 3.5"/></symbol>++<symbol id="ovg-tack" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6l-1 5 3 3H7l3-3ZM12 11v10"/></symbol>++<symbol id="ovg-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12V4h8l10 10-8 8Z"/><circle cx="7.5" cy="8.5" r="1.3"/></symbol>++<symbol id="ovg-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5"/></symbol>++<symbol id="ovg-task" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="m8 12 3 3 5-6"/></symbol>++<symbol id="ovg-tent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 20h19M12 5 4 20M12 5l8 15M12 13.5 9 20M12 13.5l3 6.5"/></symbol>++<symbol id="ovg-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="m7 9 3 3-3 3M13 15h4"/></symbol>++<symbol id="ovg-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 20 11 4h2l5.5 16M7.6 14h8.8"/></symbol>++<symbol id="ovg-ticket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18v3a2 2 0 0 0 0 4v3H3v-3a2 2 0 0 0 0-4Z"/><path d="M14 7v10"/></symbol>++<symbol id="ovg-train" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="3" width="12" height="13" rx="3"/><path d="M6 10h12M9 13h.01M15 13h.01M8.5 21l2-3.5M15.5 21l-2-3.5"/></symbol>++<symbol id="ovg-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></symbol>++<symbol id="ovg-tree" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 6.5 10.5h3L5 17h14l-4.5-6.5h3ZM12 17v4"/></symbol>++<symbol id="ovg-trophy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 4h8v5a4 4 0 0 1-8 0ZM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 20h8"/></symbol>++<symbol id="ovg-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></symbol>++<symbol id="ovg-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></symbol>++<symbol id="ovg-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6"/></symbol>++<symbol id="ovg-user-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 11l2 2 4-4"/></symbol>++<symbol id="ovg-video" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 10 5-3v10l-5-3"/></symbol>++<symbol id="ovg-volume" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5h3.5L12 6v12l-4.5-3.5H4Z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10"/></symbol>++<symbol id="ovg-wallet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7.5A2.5 2.5 0 0 1 6.5 5H18v4M4 7.5V18a2 2 0 0 0 2 2h14V9H6.5A2.5 2.5 0 0 1 4 7.5Z"/><path d="M16 14.5h.01"/></symbol>++<symbol id="ovg-water" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2M2.5 15c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2"/></symbol>++<symbol id="ovg-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"/><path d="M5.5 11.5A7 7 0 0 1 18 8.6c-2.3-.5-4.2.4-4.8 2.4"/></symbol>++<symbol id="ovg-wifi" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9a14 14 0 0 1 19 0M5.5 12.5a9.5 9.5 0 0 1 13 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01"/></symbol>++<symbol id="ovg-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4a5 5 0 0 0-4.4 7L4 17.1V20h2.9l6.1-6.1a5 5 0 0 0 7-4.4l-3 1.1-2.4-2.4 1.1-3Z"/></symbol>++<symbol id="ovg-x-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6M15 9l-6 6"/></symbol>++<symbol id="ovg-zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3 5 13h6l-1 8 8-10h-6Z"/></symbol>++</svg>nnew file mode 100644BBinary files /dev/null and b/ov-glyphs.woff2 differ@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.61.0",++ "version": "4.62.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -97,6 +97,9 @@"*.js","*.css","*.woff2",++ "*.svg",++ "glyph-codepoints.json",++ "glyph-fonts/","OFL-Nunito.txt","analytics/","docs/schemas/",nnew file mode 100644@@@ -0,0 +1,168 @@++#!/usr/bin/env python3++"""++The OpenVibe glyphs as fonts (plan L32 I5, 4.62.0), built from glyphs.js (the one registry):++++ ov-glyphs.woff2 the web font, served at /shared/ov-glyphs.woff2++ glyph-fonts/ov-glyphs.ttf TrueType, for desktop apps and design tools++ glyph-fonts/ov-glyphs.otf OpenType (CFF)++ ov-glyphs-font.css @font-face plus .ovi .ovi-<name> classes++ ov-glyphs-fa.css Font Awesome's class names on the font, for a site that drops Font Awesome's own++ glyph-codepoints.json each glyph's Private Use Area code point: given once, never changed or reused++ glyph-fonts/manifest.json what the fonts were built from (test/glyph-fonts.test.js checks it is current)++++A glyph is drawn in strokes; a font holds outlines. Each glyph's strokes (2 units, round caps and joins) are expanded++to outlines by picosvg and merged into one shape by skia-pathops, so the font draws exactly what the SVG does.++++Needs Python 3 with picosvg, fonttools and brotli (picosvg brings skia-pathops):++ python3 -m venv .venv && .venv/bin/pip install picosvg fonttools brotli++ .venv/bin/python scripts/build-glyph-fonts.py++"""++import hashlib++import json++import os++import subprocess++import sys++++import pathops++from fontTools.fontBuilder import FontBuilder++from fontTools.pens.cu2quPen import Cu2QuPen++from fontTools.pens.t2CharStringPen import T2CharStringPen++from fontTools.pens.transformPen import TransformPen++from fontTools.pens.ttGlyphPen import TTGlyphPen++from fontTools.svgLib.path import parse_path++from fontTools.ttLib import TTFont++from picosvg.svg import SVG++++ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))++# A fixed build date in the fonts' head table, so building twice gives the same bytes (fontTools reads it).++os.environ.setdefault('SOURCE_DATE_EPOCH', '1793404800')++FAMILY = 'OV Glyphs'++UPM = 1024++DESCENT = 128 # the icon sits like Font Awesome's: an eighth below the baseline++SCALE = UPM / 24++FIRST = 0xE001 # the Private Use Area, from U+E001++VERSION = json.load(open(os.path.join(ROOT, 'package.json')))['version']++++++def registry():++ """Every glyph's drawing, and the Font Awesome names each stands for, from glyphs.js."""++ js = ("const g=require('./glyphs');const fa={};for(const[k,v]of Object.entries(g.FA))(fa[v]=fa[v]||[]).push(k);"++ "console.log(JSON.stringify({names:g.NAMES,paths:Object.fromEntries(g.NAMES.map(n=>[n,g.path(n)])),fa}))")++ return json.loads(subprocess.check_output(['node', '-e', js], cwd=ROOT))++++++def codepoints(names):++ """glyph-codepoints.json: a new glyph gets the next free code point; a code point once given is never reused."""++ p = os.path.join(ROOT, 'glyph-codepoints.json')++ cps = json.load(open(p))['glyphs'] if os.path.exists(p) else {}++ nxt = max([int(v, 16) for v in cps.values()] + [FIRST - 1]) + 1++ for n in sorted(names):++ if n not in cps:++ cps[n] = '%04X' % nxt++ nxt += 1++ data = {'about': 'Each OpenVibe glyph\'s code point in the OV Glyphs font (U+E001 up). Given once, never changed or reused.',++ 'glyphs': dict(sorted(cps.items(), key=lambda kv: int(kv[1], 16)))}++ with open(p, 'w') as f:++ f.write(json.dumps(data, indent=2) + '\n')++ return {n: int(v, 16) for n, v in cps.items()}++++++def outline(inner):++ """The glyph's strokes as one merged outline (a skia-pathops Path), in the 24-unit grid."""++ svg = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-width="2" '++ 'stroke-linecap="round" stroke-linejoin="round">' + inner + '</g></svg>')++ shapes = []++ for shape in SVG.fromstring(svg).topicosvg().shapes():++ p = pathops.Path()++ parse_path(shape.d, p.getPen())++ shapes.append(p)++ out = pathops.Path()++ pathops.union(shapes, out.getPen())++ return out++++++def to_font(path, pen):++ """Draws a 24-unit outline into a font pen: y up, the em square, an eighth below the baseline."""++ path.draw(TransformPen(pen, (SCALE, 0, 0, -SCALE, 0, 24 * SCALE - DESCENT)))++++++def build(flavour, order, cmap, outlines):++ fb = FontBuilder(UPM, isTTF=(flavour == 'ttf'))++ fb.setupGlyphOrder(order)++ fb.setupCharacterMap(cmap)++ if flavour == 'ttf':++ glyfs = {'.notdef': TTGlyphPen(None).glyph(), 'space': TTGlyphPen(None).glyph()}++ for name, path in outlines.items():++ pen = TTGlyphPen(None)++ to_font(path, Cu2QuPen(pen, max_err=1, reverse_direction=True))++ glyfs[name] = pen.glyph()++ fb.setupGlyf(glyfs)++ else:++ charstrings = {}++ for name in order:++ pen = T2CharStringPen(UPM, None)++ if name in outlines:++ to_font(outlines[name], pen)++ charstrings[name] = pen.getCharString()++ fb.setupCFF(FAMILY.replace(' ', ''), {'FullName': FAMILY}, charstrings, {})++ fb.setupHorizontalMetrics({n: (UPM, 0) for n in order})++ fb.setupHorizontalHeader(ascent=UPM - DESCENT, descent=-DESCENT)++ fb.setupNameTable({'familyName': FAMILY, 'styleName': 'Regular', 'uniqueFontIdentifier': f'OpenVibe:{FAMILY}:{VERSION}',++ 'fullName': FAMILY, 'psName': FAMILY.replace(' ', '') + '-Regular', 'version': f'Version {VERSION}',++ 'copyright': 'OpenVibe glyphs, MIT licence. https://openvibe.design/icons',++ 'licenseDescription': 'MIT', 'licenseInfoURL': 'https://opensource.org/license/mit'})++ fb.setupOS2(sTypoAscender=UPM - DESCENT, sTypoDescender=-DESCENT, sTypoLineGap=0, usWinAscent=UPM - DESCENT,++ usWinDescent=DESCENT, achVendID='OVIB', fsType=0)++ fb.setupPost(keepGlyphNames=True)++ return fb.font++++++def main():++ reg = registry()++ names = reg['names']++ cps = codepoints(names)++ outlines = {n: outline(reg['paths'][n]) for n in names}++ order = ['.notdef', 'space'] + names++ cmap = {0x20: 'space', **{cps[n]: n for n in names}}++++ os.makedirs(os.path.join(ROOT, 'glyph-fonts'), exist_ok=True)++ ttf = build('ttf', order, cmap, outlines)++ ttf.save(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.ttf'))++ build('otf', order, cmap, outlines).save(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.otf'))++ web = TTFont(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.ttf'))++ web.flavor = 'woff2'++ web.save(os.path.join(ROOT, 'ov-glyphs.woff2'))++++ esc = lambda n: '\\' + ('%04x' % cps[n])++ head = '/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */\n'++ face = ("@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}\n")++ base = ("{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;"++ "text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}\n")++ font_css = (head + '/* <i class="ovi ovi-eye"></i>: the OpenVibe glyphs as a font. Sizes follow the text. */\n' + face++ + '.ovi' + base + '.ovi-fw{width:1.25em;text-align:center}\n'++ + ''.join(f'.ovi-{n}::before{{content:"{esc(n)}"}}\n' for n in names))++ with open(os.path.join(ROOT, 'ov-glyphs-font.css'), 'w') as f:++ f.write(font_css)++ fa_rules = []++ for n in names:++ for fa in sorted(reg['fa'].get(n, [])):++ fa_rules.append(f'.fa-{fa}::before{{content:"{esc(n)}"}}')++ fa_css = (head + "/* Font Awesome's class names on the OV Glyphs font, for a site that drops Font Awesome's stylesheet and its\n"++ " font. A name with no glyph shows nothing, so load it only where every name a page uses is covered\n"++ " (openvibe.design/icons lists them); brand logos keep Font Awesome Brands. */\n" + face++ + '.fa,.fas,.far,.fal,.fat,.fa-solid,.fa-regular,.fa-light,.fa-thin,.fa-duotone' + base++ + '.fa-fw{width:1.25em;text-align:center}\n' + '\n'.join(fa_rules) + '\n')++ with open(os.path.join(ROOT, 'ov-glyphs-fa.css'), 'w') as f:++ f.write(fa_css)++++ source = json.dumps([[n, reg['paths'][n], '%04X' % cps[n]] for n in names], separators=(',', ':'), ensure_ascii=False)++ manifest = {'family': FAMILY, 'glyphs': len(names), 'source_sha256': hashlib.sha256(source.encode('utf8')).hexdigest(),++ 'files': {p: os.path.getsize(os.path.join(ROOT, p)) for p in++ ['ov-glyphs.woff2', 'glyph-fonts/ov-glyphs.ttf', 'glyph-fonts/ov-glyphs.otf']}}++ with open(os.path.join(ROOT, 'glyph-fonts', 'manifest.json'), 'w') as f:++ f.write(json.dumps(manifest, indent=2) + '\n')++ print(f"{len(names)} glyphs: " + ', '.join(f'{k} {v / 1024:.1f} KB' for k, v in manifest['files'].items()))++++++if __name__ == '__main__':++ sys.exit(main())@@@ -1,8 +1,9 @@#!/usr/bin/env node'use strict';--// Writes ov-glyphs.js, the page drop-in that swaps Font Awesome icons for the OpenVibe glyphs, from glyphs.js (the one--// registry). Re-run after a glyph or an alias changes: node scripts/build-glyphs.js (test/glyphs.test.js checks the file--// is current). Only the glyphs some Font Awesome name stands for go in, so the drop-in carries no drawing it cannot use.++// Writes ov-glyphs.js, the page drop-in that swaps Font Awesome icons for the OpenVibe glyphs, and ov-glyphs.svg, the++// sprite (<symbol id="ovg-eye">), from glyphs.js (the one registry). Re-run after a glyph or an alias changes:++// node scripts/build-glyphs.js (test/glyphs.test.js checks both files are current). Only the glyphs some Font Awesome++// name stands for go in the drop-in, so it carries no drawing it cannot use; the sprite has every glyph.const fs = require('fs');const path = require('path');const glyphs = require('../glyphs');@@@ -11,6 +12,55 @@ const used = [...new Set(Object.values(glyphs.FA))].sort();const G = Object.fromEntries(used.map((n) => [n, glyphs.path(n)]));const FA = Object.fromEntries(Object.entries(glyphs.FA).sort(([a], [b]) => (a < b ? -1 : 1)));++// Font Awesome's features on a swapped icon (4.62.0), with Font Awesome's own class names and CSS variables, so a site++// can drop Font Awesome's stylesheet and keep its markup. Rules about the icon itself are scoped to .ovg; the wrappers++// (fa-ul, fa-stack, fa-layers) carry Font Awesome's own values. Nothing spins (plan L27): fa-spin breathes instead.++const anim = (cls, name, timing, dur = '1s') => `.ovg.${cls}{animation:${name} var(--fa-animation-duration,${dur}) var(--fa-animation-timing,${timing}) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}`;++const CSS = [++ // The icon: Font Awesome's glyph off, ours in the text's colour and size, one drawing in four weights.++ '.ovg::before,.ovg::after{content:none!important;display:none!important}',++ '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}',++ '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:var(--ovg-stroke,2);stroke-linecap:round;stroke-linejoin:round;overflow:visible}',++ '.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}',++ // Sizes, fixed width, borders and pulls.++ '.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}',++ Array.from({ length: 10 }, (_, i) => `.ovg.fa-${i + 1}x{font-size:${i + 1}em}`).join(''),++ '.ovg.fa-fw{width:1.25em;text-align:center}',++ '.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}',++ '.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}',++ // Turns and flips act on the drawing, so they combine with an animation on the icon.++ '.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}',++ '.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}',++ // Lists, stacks and layers (Font Awesome's wrappers).++ '.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}',++ '.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}',++ '.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}',++ '.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}',++ '.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}',++ // Animations, with Font Awesome's variables and timings.++ anim('fa-beat', 'ovgBeat', 'ease-in-out'),++ anim('fa-bounce', 'ovgBounce', 'cubic-bezier(.28,.84,.42,1)'),++ anim('fa-fade', 'ovgFade', 'cubic-bezier(.4,0,.6,1)'),++ anim('fa-beat-fade', 'ovgBeatFade', 'cubic-bezier(.4,0,.6,1)'),++ anim('fa-flip', 'ovgFlip', 'ease-in-out'),++ anim('fa-shake', 'ovgShake', 'linear'),++ '.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}',++ '.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}',++ '@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}',++ '@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}',++ '@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}',++ '@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}',++ '@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}',++ '@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}',++ '@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}',++ // The busy dots take turns.++ '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}',++ '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}',++ '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}',++ // Reduced motion: nothing moves.++ '@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}html[data-ov-motion=reduced] .ovg,html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none!important}',++].join('');++const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. *//*** openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its@@@ -18,6 +68,12 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.*++ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,++ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the++ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light++ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that++ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured.++ ** <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later* OVGlyphs.swap(rootEl) swaps inside one element now* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null@@@ -29,30 +85,44 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,if (typeof window === 'undefined' || window.OVGlyphs) return;const G = ${JSON.stringify(G)};const FA = ${JSON.stringify(FA)};-- // Font Awesome's style and size classes, and its animations: they say how, not what.-- const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity)$/;++ // Font Awesome's style, size, layout and animation classes: they say how, not what.++ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';-- const CSS = '.ovg::before,.ovg::after{content:none!important;display:none!important}'-- + '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}'-- + '.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'-- + '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;overflow:visible}'-- + '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}'-- + '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}'-- + '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}'-- + '@media (prefers-reduced-motion:reduce){.ovg[data-ovg=busy] circle{animation:none}}html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none}';++ const CSS = ${JSON.stringify(CSS)};const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };++ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).++ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;++ function transformOf(spec) {++ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;++ for (const p of String(spec || '').toLowerCase().split(/\\s+/)) {++ const m = TF.exec(p);++ if (!m) continue;++ any = true;++ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }++ const n = Number(m[2]);++ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;++ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;++ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;++ }++ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';++ }function draw(el) {if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;const fa = nameOf(el);const g = fa && fromFa(fa);-- if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; el.textContent = ''; } return; }-- if (el.dataset.ovgFa === fa) return;++ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }++ const tf = el.getAttribute('data-fa-transform') || '';++ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself foreverif (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an iconel.classList.add('ovg');el.dataset.ovg = g; el.dataset.ovgFa = fa;-- if (!el.hasAttribute('aria-label') && !el.hasAttribute('title')) el.setAttribute('aria-hidden', 'true');-- el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + (g === 'busy' ? BUSY : G[g]) + '</svg>';++ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;++ const title = el.getAttribute('title');++ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }++ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');++ const t = transformOf(tf);++ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g]) + '</svg>';}const SEL = 'i[class*="fa-"],span[class*="fa-"]';function swap(root) {@@@ -69,13 +139,20 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });}-- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });++ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });} catch (e) { /* no observer: what was there is swapped */ }}window.OVGlyphs = { swap, fromFa };if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();})();`;--fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), out);--if (require.main === module) console.log(`ov-glyphs.js: ${used.length} glyphs for ${Object.keys(FA).length} Font Awesome names, ${(out.length / 1024).toFixed(1)} KB`);--module.exports = { build: () => out };++++// The sprite: every glyph as a <symbol>, for <svg><use href="/shared/ov-glyphs.svg#ovg-eye"/></svg>.++const sprite = `<svg xmlns="http://www.w3.org/2000/svg">\n<!-- The OpenVibe glyphs (openvibe-shared, MIT): <svg><use href="ov-glyphs.svg#ovg-eye"/></svg>. Generated by scripts/build-glyphs.js. -->\n${glyphs.NAMES.map((n) => `<symbol id="ovg-${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${glyphs.path(n)}</symbol>`).join('\n')}\n</svg>\n`;++++if (require.main === module) {++ fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), out);++ fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.svg'), sprite);++ console.log(`ov-glyphs.js: ${used.length} glyphs for ${Object.keys(FA).length} Font Awesome names, ${(out.length / 1024).toFixed(1)} KB; ov-glyphs.svg: ${glyphs.NAMES.length} symbols, ${(sprite.length / 1024).toFixed(1)} KB`);++}++module.exports = { build: () => out, sprite: () => sprite, CSS };@@@ -80,6 +80,7 @@ function contentType(name) {if (name.endsWith('.jpg')) return 'image/jpeg';if (name.endsWith('.png')) return 'image/png';if (name.endsWith('.woff2')) return 'font/woff2';++ if (name.endsWith('.svg')) return 'image/svg+xml; charset=utf-8';return 'application/javascript; charset=utf-8';}nnew file mode 100644@@@ -0,0 +1,45 @@++'use strict';++// The glyphs as fonts (4.62.0, plan L32 I5): built by scripts/build-glyph-fonts.py from glyphs.js. The fonts are++// current (the manifest names exactly what they were drawn from), every glyph has its own code point in the Private++// Use Area that never changes, the CSS names every glyph and every Font Awesome name, and sites serve the web font,++// both style sheets and the sprite.++const assert = require('assert');++const crypto = require('crypto');++const fs = require('fs');++const path = require('path');++const glyphs = require('../glyphs');++const files = require('../files');++++const ROOT = path.join(__dirname, '..');++const read = (f) => fs.readFileSync(path.join(ROOT, f));++const cps = JSON.parse(read('glyph-codepoints.json')).glyphs;++const manifest = JSON.parse(read('glyph-fonts/manifest.json'));++++// Every glyph has a code point; none is shared; all sit in the Private Use Area from U+E001.++for (const n of glyphs.NAMES) assert.ok(/^[0-9A-F]{4}$/.test(cps[n] || ''), `${n} has a code point`);++const values = Object.values(cps);++assert.strictEqual(new Set(values).size, values.length, 'no code point is given twice');++values.forEach((v) => assert.ok(parseInt(v, 16) >= 0xE001 && parseInt(v, 16) <= 0xF8FF, `U+${v} is in the Private Use Area`));++++// The fonts were built from today's registry (run scripts/build-glyph-fonts.py after a glyph changes).++const source = JSON.stringify(glyphs.NAMES.map((n) => [n, glyphs.path(n), cps[n]]));++assert.strictEqual(manifest.source_sha256, crypto.createHash('sha256').update(source).digest('hex'), 'the fonts are current: run scripts/build-glyph-fonts.py');++assert.strictEqual(manifest.glyphs, glyphs.NAMES.length);++for (const [f, size] of Object.entries(manifest.files)) assert.strictEqual(read(f).length, size, `${f} is the file the build wrote`);++assert.strictEqual(read('ov-glyphs.woff2').subarray(0, 4).toString('latin1'), 'wOF2', 'a WOFF2 file');++assert.strictEqual(read('glyph-fonts/ov-glyphs.ttf').readUInt32BE(0), 0x00010000, 'a TrueType file');++assert.strictEqual(read('glyph-fonts/ov-glyphs.otf').subarray(0, 4).toString('latin1'), 'OTTO', 'an OpenType (CFF) file');++++// The style sheets.++const font = read('ov-glyphs-font.css').toString();++assert.ok(font.includes("@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2')"));++for (const n of glyphs.NAMES) assert.ok(font.includes(`.ovi-${n}::before{content:"\\${cps[n].toLowerCase()}"}`), `.ovi-${n}`);++const fa = read('ov-glyphs-fa.css').toString();++for (const [name, g] of Object.entries(glyphs.FA)) assert.ok(fa.includes(`.fa-${name}::before{content:"\\${cps[g].toLowerCase()}"}`), `.fa-${name} → ${g}`);++assert.ok(!/fa-brands|\.fab\b/.test(fa), 'brand logos stay Font Awesome Brands');++++// Sites serve them.++for (const f of ['ov-glyphs.woff2', 'ov-glyphs-font.css', 'ov-glyphs-fa.css', 'ov-glyphs.svg']) assert.ok(files.isBrowserFile(f), `/shared/${f}`);++assert.ok(files.FONTS.includes('ov-glyphs.woff2') && files.IMAGES.includes('ov-glyphs.svg'));++++console.log(`glyph fonts: ${glyphs.NAMES.length} glyphs at U+${values.reduce((a, b) => (a < b ? a : b))}–U+${values.reduce((a, b) => (a > b ? a : b))}; WOFF2, TTF and OTF current; both style sheets; served`);@@@ -34,9 +34,25 @@ for (const fa of ['wand-magic-sparkles', 'eye', 'tower-broadcast', 'xmark', 'vid}// ── The drop-in is the registry's, current ──--const built = require('../scripts/build-glyphs').build();++const builder = require('../scripts/build-glyphs');const SRC = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');--assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-glyphs.js');++assert.strictEqual(SRC, builder.build(), 'ov-glyphs.js is current: run node scripts/build-glyphs.js');++const SPRITE = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.svg'), 'utf8');++assert.strictEqual(SPRITE, builder.sprite(), 'ov-glyphs.svg is current: run node scripts/build-glyphs.js');++assert.strictEqual((SPRITE.match(/<symbol id="ovg-/g) || []).length, glyphs.NAMES.length, 'the sprite has every glyph');++assert.ok(SPRITE.includes(`<symbol id="ovg-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${glyphs.path('eye')}</symbol>`));++++// Font Awesome's features, kept (4.62.0): every documented class has a rule, with Font Awesome's own variables.++const CSS = builder.CSS;++const ON_ICON = ['fa-2xs', 'fa-xs', 'fa-sm', 'fa-lg', 'fa-xl', 'fa-2xl', 'fa-1x', 'fa-10x', 'fa-fw', 'fa-border', 'fa-pull-left', 'fa-pull-right',++ 'fa-rotate-90', 'fa-rotate-180', 'fa-rotate-270', 'fa-rotate-by', 'fa-flip-horizontal', 'fa-flip-vertical', 'fa-flip-both', 'fa-inverse',++ 'fa-beat', 'fa-bounce', 'fa-fade', 'fa-beat-fade', 'fa-flip', 'fa-shake', 'fa-spin', 'fa-thin', 'fa-light', 'fa-duotone'];++ON_ICON.forEach((cls) => assert.ok(CSS.includes(`.ovg.${cls}{`) || CSS.includes(`.ovg.${cls}>svg{`) || CSS.includes(`.ovg.${cls},`), `.${cls} on a swapped icon`));++['fa-ul', 'fa-li', 'fa-stack', 'fa-layers', 'fa-layers-text', 'fa-layers-counter'].forEach((cls) => assert.ok(CSS.includes(`.${cls}{`), `.${cls}, Font Awesome's wrapper`));++['--fa-beat-scale', '--fa-bounce-height', '--fa-fade-opacity', '--fa-flip-angle', '--fa-rotate-angle', '--fa-animation-duration', '--fa-li-width',++ '--fa-border-color', '--fa-counter-background-color', '--fa-primary-color', '--fa-inverse'].forEach((v) => assert.ok(CSS.includes(v), `Font Awesome's ${v}`));++assert.ok(!CSS.includes('360deg') && /\.ovg\.fa-spin[^{]*\{animation:ovgBreathe/.test(CSS), 'fa-spin breathes: nothing spins (L27)');++assert.ok(CSS.includes('@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}'), 'reduced motion stops every animation');// ── In a page ──(async () => {@@@ -47,6 +63,9 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-<i class="fa-solid fa-couch" id="unknown"></i><i class="fa-solid fa-eye" aria-label="Seen" id="labelled"></i><span class="fa-solid fa-play" id="toggle"></span>++<i class="fa-solid fa-heart fa-2x fa-beat" data-fa-transform="grow-4 up-2 rotate-90 flip-h" id="tf"></i>++<i class="fa-solid fa-bell" title="Notifications" id="titled"></i>++<span class="fa-layers"><i class="fa-solid fa-envelope"></i><span class="fa-layers-counter" id="counter">12</span></span></body></html>`);window.MutationObserver = MutationObserver;new Function('window', 'document', 'MutationObserver', SRC)(window, document, MutationObserver);@@@ -58,13 +77,26 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-assert.ok($('#spin').dataset.ovg === 'busy' && $('#spin').querySelectorAll('circle').length === 3, 'a spinner becomes three dots');assert.ok(!$('#gh').classList.contains('ovg') && !$('#unknown').classList.contains('ovg'), 'brands and unknown names stay Font Awesome');const css = document.getElementById('ov-glyphs-css').textContent;-- assert.ok(css.includes('.ovg::before,.ovg::after{content:none!important') && css.includes('.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'), "Font Awesome's own glyph and spin are turned off on a swapped icon");++ assert.ok(css.includes('.ovg::before,.ovg::after{content:none!important') && css.includes('.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe'), "Font Awesome's own glyph is off on a swapped icon, and its spin breathes");++ // data-fa-transform: Font Awesome's sixteenths, on the drawing; size and animation classes stay on the icon.++ const tf = $('#tf');++ assert.ok(tf.classList.contains('ovg') && tf.classList.contains('fa-2x') && tf.classList.contains('fa-beat'));++ assert.strictEqual(tf.querySelector('svg').getAttribute('style'), 'transform:translate(0em,-0.125em) rotate(90deg) scale(-1.25,1.25)');++ // A title names the icon to screen readers.++ assert.ok($('#titled').getAttribute('role') === 'img' && $('#titled').getAttribute('aria-label') === 'Notifications' && !$('#titled').hasAttribute('aria-hidden'));++ // A layer's counter is text, not an icon.++ assert.ok(!$('#counter').classList.contains('ovg') && $('#counter').textContent === '12');++ assert.ok($('.fa-layers > i').dataset.ovg, 'the layer\'s icon is swapped');// A page that changes an icon by its class (play ↔ pause) gets the new glyph.const t = $('#toggle');assert.strictEqual(t.dataset.ovg, 'play');t.className = 'fa-solid fa-pause';await new Promise((r) => setImmediate(r));assert.ok(t.dataset.ovg === 'pause' && t.classList.contains('ovg'), 'follows a class change');++ // A page that resets the class list but keeps the name keeps the glyph (the swap class comes back).++ t.className = 'fa-solid fa-pause is-on';++ await new Promise((r) => setImmediate(r));++ assert.ok(t.classList.contains('ovg') && t.dataset.ovg === 'pause', 'the swap class comes back after a reset');t.className = 'fa-solid fa-couch';await new Promise((r) => setImmediate(r));assert.ok(!t.classList.contains('ovg') && !t.dataset.ovg && !t.querySelector('svg'), 'and hands back a name it does not know');
@@ -1,3 +1,5 @@
@@ -1,3 +1,5 @@
* text=auto eol=lf
* text=auto eol=lf
*.png binary
*.png binary
*.woff2 binary
*.woff2 binary
+*.ttf binary
+*.otf binary
@@ -6,6 +6,35 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,35 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.62.0 — 2026-10-31
+
+- **Font Awesome's features, kept, on the glyphs** (plan L32 I6; the owner: "basically want to keep Font Awesome, so all
+ their features"). `ov-glyphs.js` now carries its own rules for every documented Font Awesome class, with Font
+ Awesome's CSS variables, so a site can drop Font Awesome's stylesheet and keep its markup:
+ - sizes `fa-2xs` … `fa-2xl` and `fa-1x` … `fa-10x`, `fa-fw`, `fa-border`, `fa-pull-left` / `-right`;
+ - `fa-rotate-90/180/270`, `fa-rotate-by` (`--fa-rotate-angle`), `fa-flip-horizontal/vertical/both`;
+ - `fa-ul` / `fa-li`, `fa-stack` (`-1x`, `-2x`, `fa-inverse`), `fa-layers` with `fa-layers-text` and
+ `fa-layers-counter`;
+ - the animations `fa-beat`, `fa-bounce`, `fa-fade`, `fa-beat-fade`, `fa-flip` and `fa-shake`. `fa-spin` and
+ `fa-spin-pulse` breathe instead, since nothing on the network spins (L27). Reduced motion stops all of them;
+ - `fa-thin` and `fa-light` as thinner strokes of the same drawing (`--ovg-stroke`, `ovg-bold`), and
+ `--fa-primary-color` on duotone icons;
+ - `data-fa-transform` (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), in Font Awesome's sixteenths;
+ - a `title` names the icon to screen readers (`role="img"`).
+ - A page that resets an icon's class list but keeps its name keeps the glyph. The drop-in is 9.1 KB brotli and loads
+ after first paint.
+- **The glyphs as fonts** (plan L32 I5; the owner: "exporting like web fonts, regular font files"):
+ - `ov-glyphs.woff2` (12 KB, served at `/shared/ov-glyphs.woff2`), plus `glyph-fonts/ov-glyphs.ttf` and
+ `glyph-fonts/ov-glyphs.otf` for desktop apps and design tools. Each glyph's strokes are expanded to outlines
+ (picosvg, skia-pathops) by `scripts/build-glyph-fonts.py`, so the font draws what the SVG does.
+ - `glyph-codepoints.json`: each glyph's Private Use Area code point from U+E001, given once and never reused.
+ - `ov-glyphs-font.css`: `<i class="ovi ovi-eye">`. `ov-glyphs-fa.css`: Font Awesome's class names on the font, for a
+ page whose every name is covered.
+ - `test/glyph-fonts.test.js` fails when the fonts no longer match the registry. Builds are reproducible
+ (`SOURCE_DATE_EPOCH`).
+- **The sprite:** `ov-glyphs.svg`, every glyph as a `<symbol id="ovg-<name>">`, served at `/shared/ov-glyphs.svg`
+ (`files.IMAGES`; `serve.js` answers `image/svg+xml`).
+
## 4.61.0 — 2026-10-31
## 4.61.0 — 2026-10-31
- **The network's own icons: `openvibe-shared/glyphs` and `ov-glyphs.js`** (plan L32; the owner: "our own custom
- **The network's own icons: `openvibe-shared/glyphs` and `ov-glyphs.js`** (plan L32; the owner: "our own custom
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.61.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.62.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -21,9 +21,9 @@ were reconciled first.
@@ -21,9 +21,9 @@ were reconciled first.
|---|---|
|---|---|
| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `notification-live.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `player.js`, `release-watch.js`, `release-update.js`, `web-runtime.js`, `boost.js`, `openvibe-sw.js` |
| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `notification-live.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `player.js`, `release-watch.js`, `release-update.js`, `web-runtime.js`, `boost.js`, `openvibe-sw.js` |
| Fonts, served at `/shared/<file>` (`files.FONTS`) | `ov-brand.woff2`: the brand face the names are set in (Nunito, SIL OFL 1.1, `OFL-Nunito.txt`; Latin, 700–900), declared by `navbar.js` |
| Fonts, served at `/shared/<file>` (`files.FONTS`) | `ov-brand.woff2`: the brand face the names are set in (Nunito, SIL OFL 1.1, `OFL-Nunito.txt`; Latin, 700–900), declared by `navbar.js` |
-| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,pg,retention,event}`), `app-icon`, `glyphs` (the network's icons, one registry: `NAMES`, `path(name)`, `svg(name, { size, title })`, `fromFa(name)`, `FA`; the browser drop-in `ov-glyphs.js` shows them in place of Font Awesome's), `mark` (the OpenVibe mark, the Pair, as static SVG: `svg({ form, tone, wide, size, title })`, `art()`, `geo()`, `shapes()`, `COLORS`; the browser drop-in `ov-mark.js` draws the same numbers), `app-icons` (every site's installable app icon set: `has(service)`, `url(service, size, { maskable })`, `isIcon(name)`, `path(name)`; the pre-rendered PNGs are served at `/shared/icons/<service>[-maskable]-<size>.png` and named by the web app manifest `serve.js` serves at `/shared/manifest/<service>.webmanifest`), `assets` (content-addressed image/asset URLs and responsive `<picture>` markup: `hash(bytes)`, `url(src, hash)`, `srcset(entries)`, `picture({ src, hash?, alt, width, height, sizes, sources })` — AVIF before WebP before the fallback `<img>`, an unhashed path kept exactly as given), `auth-client`, `brand`, `builtin-themes`, `cache-policy` (the one cache rule: a content-addressed asset — `?v=<hash>` or a hashed filename — is `public, max-age=31536000, immutable`, anything else is `public, max-age=300, stale-while-revalidate=86400`, HTML is `public, max-age=120, stale-while-revalidate=3600` and a personalised page is `private, no-store`; `assetHeaders`/`htmlHeaders`/`isHashed` plus an `applyHtml()` middleware, used by `serve.js`), `indexnow` (tell engines a page changed: `createIndexNow({ host, key, keyLocation?, endpoint?, fetch, log })` → `keyFile` for `/<key>.txt`, `ping(urls)` for one POST of a deduped, https-only, own-host batch of ≤10,000 URLs that never throws, and `pingSoon(urls)` debounced into one batch in 30s; an unset key sends nothing), `frame` (the OpenVibe Frame on the server), `legal`, `middleware`, `notifications`, `seo`, `social` (every site's 1200×630 share card: `url(service)`, served by `serve.js` at `/shared/social/<service>.jpg` and used by `shell.page()` for `og:image` when a page names none), `shell` (the server-rendered page shell: `page(o)` composes the doctype and `<head>` from `seo.headTags`/`seo.pageSummary`, the theme-loader before the first paint, `web-runtime`/`navbar`/`footer` through `serve.url`, the `frame.noscriptNav` no-JS nav, the body and `frame.footer`; `scripts(o)` is just the head scripts and the escaped runtime boot JSON), `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`, `config` (the configuration model: revisioned, validated, classified settings with last-known-good and `/api/admin/config`), `test-runner` (the `npm test` runner of every service's `test/run.js`: each `*.test.js` in its own process; a line `<label>: skipped (<why>)` makes a file skipped, listed with ○ and never counted as passed, and `--strict` fails on skips), `perf-budget` (size budgets for a page's first load, measured from the running server: HTML, same-origin scripts and stylesheets, raw and brotli; for `npm test`), `browser-harness` (real-Chrome checks of a running site: status, console errors, overflow, duplicate scripts, no-JS text, canonical, JSON-LD against visible text, axe-core, repeated-navigation growth and idle work; Node 22, Chrome); `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |
+| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,pg,retention,event}`), `app-icon`, `glyphs` (the network's icons, one registry: `NAMES`, `path(name)`, `svg(name, { size, title })`, `fromFa(name)`, `FA`; the browser drop-in `ov-glyphs.js` shows them in place of Font Awesome's and keeps Font Awesome's classes, animations and `data-fa-transform`; also as fonts, `/shared/ov-glyphs.woff2` with `ov-glyphs-font.css` (`ovi ovi-<name>`) or `ov-glyphs-fa.css` (Font Awesome's names), `glyph-fonts/ov-glyphs.{ttf,otf}` and `glyph-codepoints.json`, and as the sprite `/shared/ov-glyphs.svg#ovg-<name>`), `mark` (the OpenVibe mark, the Pair, as static SVG: `svg({ form, tone, wide, size, title })`, `art()`, `geo()`, `shapes()`, `COLORS`; the browser drop-in `ov-mark.js` draws the same numbers), `app-icons` (every site's installable app icon set: `has(service)`, `url(service, size, { maskable })`, `isIcon(name)`, `path(name)`; the pre-rendered PNGs are served at `/shared/icons/<service>[-maskable]-<size>.png` and named by the web app manifest `serve.js` serves at `/shared/manifest/<service>.webmanifest`), `assets` (content-addressed image/asset URLs and responsive `<picture>` markup: `hash(bytes)`, `url(src, hash)`, `srcset(entries)`, `picture({ src, hash?, alt, width, height, sizes, sources })` — AVIF before WebP before the fallback `<img>`, an unhashed path kept exactly as given), `auth-client`, `brand`, `builtin-themes`, `cache-policy` (the one cache rule: a content-addressed asset — `?v=<hash>` or a hashed filename — is `public, max-age=31536000, immutable`, anything else is `public, max-age=300, stale-while-revalidate=86400`, HTML is `public, max-age=120, stale-while-revalidate=3600` and a personalised page is `private, no-store`; `assetHeaders`/`htmlHeaders`/`isHashed` plus an `applyHtml()` middleware, used by `serve.js`), `indexnow` (tell engines a page changed: `createIndexNow({ host, key, keyLocation?, endpoint?, fetch, log })` → `keyFile` for `/<key>.txt`, `ping(urls)` for one POST of a deduped, https-only, own-host batch of ≤10,000 URLs that never throws, and `pingSoon(urls)` debounced into one batch in 30s; an unset key sends nothing), `frame` (the OpenVibe Frame on the server), `legal`, `middleware`, `notifications`, `seo`, `social` (every site's 1200×630 share card: `url(service)`, served by `serve.js` at `/shared/social/<service>.jpg` and used by `shell.page()` for `og:image` when a page names none), `shell` (the server-rendered page shell: `page(o)` composes the doctype and `<head>` from `seo.headTags`/`seo.pageSummary`, the theme-loader before the first paint, `web-runtime`/`navbar`/`footer` through `serve.url`, the `frame.noscriptNav` no-JS nav, the body and `frame.footer`; `scripts(o)` is just the head scripts and the escaped runtime boot JSON), `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`, `config` (the configuration model: revisioned, validated, classified settings with last-known-good and `/api/admin/config`), `test-runner` (the `npm test` runner of every service's `test/run.js`: each `*.test.js` in its own process; a line `<label>: skipped (<why>)` makes a file skipped, listed with ○ and never counted as passed, and `--strict` fails on skips), `perf-budget` (size budgets for a page's first load, measured from the running server: HTML, same-origin scripts and stylesheets, raw and brotli; for `npm test`), `browser-harness` (real-Chrome checks of a running site: status, console errors, overflow, duplicate scripts, no-JS text, canonical, JSON-LD against visible text, axe-core, repeated-navigation growth and idle work; Node 22, Chrome); `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |
| Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |
| Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |
-| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js`, `scripts/build-icons.js`, `scripts/build-social-cards.js` (share cards from the Contracts manifests; Chrome + ImageMagick) |
+| Generators | `scripts/build-glyphs.js` (`ov-glyphs.js` and `ov-glyphs.svg` from `glyphs.js`), `scripts/build-glyph-fonts.py` (the glyph fonts and their CSS; Python with picosvg, fonttools and brotli), `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js`, `scripts/build-icons.js`, `scripts/build-social-cards.js` (share cards from the Contracts manifests; Chrome + ImageMagick) |
| Pin bot | `scripts/pin-bump.js` (`node scripts/pin-bump.js <library> <tag> [--repos A,B] [--apply]`): after a release, one PR per repository that pins the library below the tag, with the lockfile refreshed and the docs-status fields rewritten; a dry run unless `--apply`; CI tests each PR |
| Pin bot | `scripts/pin-bump.js` (`node scripts/pin-bump.js <library> <tag> [--repos A,B] [--apply]`): after a release, one PR per repository that pins the library below the tag, with the lockfile refreshed and the docs-status fields rewritten; a dry run unless `--apply`; CI tests each PR |
| Checks (CI) | `scripts/pin-drift.js` (every openvibe-* pin is a published tag and not more than one minor behind, and openvibe-shared is installed once), `scripts/docs-status.js` (`node scripts/docs-status.js <dir> [--check]`: STATUS.json's contracts/sdk/shared fields and the README `<!-- versions:start/end -->` block must name the pinned releases; `--check` exits 1 on drift) and `scripts/floating-promises.js` (`node scripts/floating-promises.js <dir> [--json]`: an async call whose result nobody awaits — a failure is an unhandled rejection and a transaction commits without it — unless awaited, returned, assigned, `void`-ed, chained with `.then(`/`.catch(`/`.finally(` or opted out with `// floating-ok: <reason>`; exit 1 when found). The native code is compiled too: `scripts/native-check.js` writes the generated Compose and SwiftUI sources out, and `.github/workflows/native.yml` compiles the Kotlin against the Compose artifacts and type-checks the Swift against SwiftUI |
| Checks (CI) | `scripts/pin-drift.js` (every openvibe-* pin is a published tag and not more than one minor behind, and openvibe-shared is installed once), `scripts/docs-status.js` (`node scripts/docs-status.js <dir> [--check]`: STATUS.json's contracts/sdk/shared fields and the README `<!-- versions:start/end -->` block must name the pinned releases; `--check` exits 1 on drift) and `scripts/floating-promises.js` (`node scripts/floating-promises.js <dir> [--json]`: an async call whose result nobody awaits — a failure is an unhandled rejection and a transaction commits without it — unless awaited, returned, assigned, `void`-ed, chained with `.then(`/`.catch(`/`.finally(` or opted out with `// floating-ok: <reason>`; exit 1 when found). The native code is compiled too: `scripts/native-check.js` writes the generated Compose and SwiftUI sources out, and `.github/workflows/native.yml` compiles the Kotlin against the Compose artifacts and type-checks the Swift against SwiftUI |
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.61.0",
+ "version": "4.62.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -65,18 +65,27 @@ const STYLES = Object.freeze([
@@ -65,18 +65,27 @@ const STYLES = Object.freeze([
'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)
'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)
'kit-data.css', // Kit 2's look: steps, tables, charts, rankings, meters, drop zones, receipts (4.37.0; after kit.css)
'kit-data.css', // Kit 2's look: steps, tables, charts, rankings, meters, drop zones, receipts (4.37.0; after kit.css)
'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)
'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)
+ 'ov-glyphs-font.css', // the glyphs as a font: <i class="ovi ovi-eye"> (4.62.0; built with the font, scripts/build-glyph-fonts.py)
+ 'ov-glyphs-fa.css', // Font Awesome's class names on the glyph font, for a site that drops Font Awesome's own (4.62.0)
]);
]);
-// Fonts sites serve at /shared/<file>: the brand face the names are set in (navbar.js declares it from beside itself).
+// Fonts sites serve at /shared/<file>: the brand face the names are set in (navbar.js declares it from beside itself),
+// and the glyph font.
const FONTS = Object.freeze([
const FONTS = Object.freeze([
'ov-brand.woff2', // Nunito (SIL OFL 1.1, OFL-Nunito.txt), Latin, weights 700-900: the OpenVibe names (4.59.0)
'ov-brand.woff2', // Nunito (SIL OFL 1.1, OFL-Nunito.txt), Latin, weights 700-900: the OpenVibe names (4.59.0)
+ 'ov-glyphs.woff2', // the OpenVibe glyphs as a font (4.62.0; TTF and OTF for desktop apps are in glyph-fonts/)
]);
]);
-const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name)) || FONTS.includes(String(name));
+// Pictures sites serve at /shared/<file>.
+const IMAGES = Object.freeze([
+ 'ov-glyphs.svg', // every glyph as a <symbol>: <svg><use href="/shared/ov-glyphs.svg#ovg-eye"/></svg> (4.62.0)
+]);
+const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name)) || FONTS.includes(String(name)) || IMAGES.includes(String(name));
module.exports = {
module.exports = {
dir: __dirname,
dir: __dirname,
BROWSER,
BROWSER,
STYLES,
STYLES,
FONTS,
FONTS,
+ IMAGES,
isBrowserFile: served,
isBrowserFile: served,
path: (name) => {
path: (name) => {
if (!served(name)) throw new Error(`openvibe-shared: ${name} is not a browser file`);
if (!served(name)) throw new Error(`openvibe-shared: ${name} is not a browser file`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,160 @@
@@ -0,0 +1,160 @@
+{
+ "about": "Each OpenVibe glyph's code point in the OV Glyphs font (U+E001 up). Given once, never changed or reused.",
+ "glyphs": {
+ "alert": "E001",
+ "alert-circle": "E002",
+ "archive": "E003",
+ "arrow-down": "E004",
+ "arrow-left": "E005",
+ "arrow-right": "E006",
+ "arrow-up": "E007",
+ "back": "E008",
+ "ban": "E009",
+ "bell": "E00A",
+ "book": "E00B",
+ "brain": "E00C",
+ "branch": "E00D",
+ "bridge": "E00E",
+ "busy": "E00F",
+ "calendar": "E010",
+ "camera": "E011",
+ "care": "E012",
+ "cart": "E013",
+ "chart": "E014",
+ "chat": "E015",
+ "check": "E016",
+ "check-circle": "E017",
+ "chevron": "E018",
+ "chip": "E019",
+ "circle": "E01A",
+ "city": "E01B",
+ "clipboard": "E01C",
+ "clock": "E01D",
+ "close": "E01E",
+ "cloud": "E01F",
+ "code": "E020",
+ "coin": "E021",
+ "compress": "E022",
+ "computer": "E023",
+ "copy": "E024",
+ "crown": "E025",
+ "database": "E026",
+ "dish": "E027",
+ "dollar": "E028",
+ "down": "E029",
+ "download": "E02A",
+ "drop": "E02B",
+ "enter": "E02C",
+ "expand": "E02D",
+ "eye": "E02E",
+ "eye-off": "E02F",
+ "file": "E030",
+ "film": "E031",
+ "filter": "E032",
+ "fire": "E033",
+ "flag": "E034",
+ "folder": "E035",
+ "food": "E036",
+ "forward": "E037",
+ "game": "E038",
+ "gauge": "E039",
+ "gavel": "E03A",
+ "gear": "E03B",
+ "gift": "E03C",
+ "globe": "E03D",
+ "grid": "E03E",
+ "hand": "E03F",
+ "hash": "E040",
+ "heart": "E041",
+ "help": "E042",
+ "history": "E043",
+ "home": "E044",
+ "id": "E045",
+ "idea": "E046",
+ "image": "E047",
+ "info": "E048",
+ "invite": "E049",
+ "key": "E04A",
+ "keyboard": "E04B",
+ "language": "E04C",
+ "layers": "E04D",
+ "leaf": "E04E",
+ "leave": "E04F",
+ "link": "E050",
+ "list": "E051",
+ "live": "E052",
+ "lock": "E053",
+ "mail": "E054",
+ "map": "E055",
+ "members": "E056",
+ "menu": "E057",
+ "mic": "E058",
+ "mic-off": "E059",
+ "minus": "E05A",
+ "moon": "E05B",
+ "more": "E05C",
+ "mountain": "E05D",
+ "music": "E05E",
+ "mute": "E05F",
+ "news": "E060",
+ "nodes": "E061",
+ "open": "E062",
+ "palette": "E063",
+ "pause": "E064",
+ "pencil": "E065",
+ "phone": "E066",
+ "pin": "E067",
+ "plane": "E068",
+ "play": "E069",
+ "plug": "E06A",
+ "plus": "E06B",
+ "reply": "E06C",
+ "retry": "E06D",
+ "robot": "E06E",
+ "route": "E06F",
+ "save": "E070",
+ "scissors": "E071",
+ "search": "E072",
+ "send": "E073",
+ "server": "E074",
+ "shield": "E075",
+ "shirt": "E076",
+ "shoe": "E077",
+ "sidebar": "E078",
+ "sliders": "E079",
+ "smile": "E07A",
+ "snow": "E07B",
+ "spark": "E07C",
+ "star": "E07D",
+ "steps": "E07E",
+ "stop": "E07F",
+ "store": "E080",
+ "sun": "E081",
+ "swap": "E082",
+ "tack": "E083",
+ "tag": "E084",
+ "target": "E085",
+ "task": "E086",
+ "tent": "E087",
+ "terminal": "E088",
+ "text": "E089",
+ "ticket": "E08A",
+ "train": "E08B",
+ "trash": "E08C",
+ "tree": "E08D",
+ "trophy": "E08E",
+ "up": "E08F",
+ "upload": "E090",
+ "user": "E091",
+ "user-check": "E092",
+ "video": "E093",
+ "volume": "E094",
+ "wallet": "E095",
+ "water": "E096",
+ "wave": "E097",
+ "wifi": "E098",
+ "wrench": "E099",
+ "x-circle": "E09A",
+ "zap": "E09B"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "family": "OV Glyphs",
+ "glyphs": 155,
+ "source_sha256": "fb052064fcb5279c7c291230e2e5389d164613d278969ad437f975b26ab40bdc",
+ "files": {
+ "ov-glyphs.woff2": 12320,
+ "glyph-fonts/ov-glyphs.ttf": 28500,
+ "glyph-fonts/ov-glyphs.otf": 35816
+ }
+}
new file mode 100644
new file mode 100644
Binary files /dev/null and b/glyph-fonts/ov-glyphs.otf differ
Binary files /dev/null and b/glyph-fonts/ov-glyphs.otf differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/glyph-fonts/ov-glyphs.ttf differ
Binary files /dev/null and b/glyph-fonts/ov-glyphs.ttf differ
@@ -475,7 +475,7 @@
@@ -475,7 +475,7 @@
const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': '2e3590333ae6', 'ov-brand.woff2': '3b4e6a8a5878' };
+ const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': 'ba57d6e1f417', 'ov-brand.woff2': '3b4e6a8a5878' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
new file mode 100644
new file mode 100644
@@ -0,0 +1,243 @@
@@ -0,0 +1,243 @@
+/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */
+/* Font Awesome's class names on the OV Glyphs font, for a site that drops Font Awesome's stylesheet and its
+ font. A name with no glyph shows nothing, so load it only where every name a page uses is covered
+ (openvibe.design/icons lists them); brand logos keep Font Awesome Brands. */
+@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}
+.fa,.fas,.far,.fal,.fat,.fa-solid,.fa-regular,.fa-light,.fa-thin,.fa-duotone{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}
+.fa-fw{width:1.25em;text-align:center}
+.fa-triangle-exclamation::before{content:"\e001"}
+.fa-circle-exclamation::before{content:"\e002"}
+.fa-box-archive::before{content:"\e003"}
+.fa-arrow-down::before{content:"\e004"}
+.fa-arrow-left::before{content:"\e005"}
+.fa-arrow-right::before{content:"\e006"}
+.fa-arrow-up::before{content:"\e007"}
+.fa-chevron-left::before{content:"\e008"}
+.fa-ban::before{content:"\e009"}
+.fa-bell::before{content:"\e00a"}
+.fa-book::before{content:"\e00b"}
+.fa-book-open::before{content:"\e00b"}
+.fa-brain::before{content:"\e00c"}
+.fa-code-branch::before{content:"\e00d"}
+.fa-code-fork::before{content:"\e00d"}
+.fa-bridge::before{content:"\e00e"}
+.fa-circle-notch::before{content:"\e00f"}
+.fa-spinner::before{content:"\e00f"}
+.fa-calendar::before{content:"\e010"}
+.fa-calendar-days::before{content:"\e010"}
+.fa-camera::before{content:"\e011"}
+.fa-hand-holding-heart::before{content:"\e012"}
+.fa-cart::before{content:"\e013"}
+.fa-cart-shopping::before{content:"\e013"}
+.fa-chart-bar::before{content:"\e014"}
+.fa-chart-line::before{content:"\e014"}
+.fa-chart-simple::before{content:"\e014"}
+.fa-comment::before{content:"\e015"}
+.fa-comment-dots::before{content:"\e015"}
+.fa-comments::before{content:"\e015"}
+.fa-message::before{content:"\e015"}
+.fa-check::before{content:"\e016"}
+.fa-check-circle::before{content:"\e017"}
+.fa-circle-check::before{content:"\e017"}
+.fa-angle-right::before{content:"\e018"}
+.fa-chevron-right::before{content:"\e018"}
+.fa-microchip::before{content:"\e019"}
+.fa-circle::before{content:"\e01a"}
+.fa-city::before{content:"\e01b"}
+.fa-clipboard::before{content:"\e01c"}
+.fa-clipboard-list::before{content:"\e01c"}
+.fa-paste::before{content:"\e01c"}
+.fa-clock::before{content:"\e01d"}
+.fa-times::before{content:"\e01e"}
+.fa-xmark::before{content:"\e01e"}
+.fa-cloud::before{content:"\e01f"}
+.fa-cloud-rain::before{content:"\e01f"}
+.fa-cloud-showers-heavy::before{content:"\e01f"}
+.fa-cloud-sun::before{content:"\e01f"}
+.fa-code::before{content:"\e020"}
+.fa-coin::before{content:"\e021"}
+.fa-coins::before{content:"\e021"}
+.fa-hand-holding-dollar::before{content:"\e021"}
+.fa-compress::before{content:"\e022"}
+.fa-desktop::before{content:"\e023"}
+.fa-display::before{content:"\e023"}
+.fa-laptop::before{content:"\e023"}
+.fa-copy::before{content:"\e024"}
+.fa-crown::before{content:"\e025"}
+.fa-database::before{content:"\e026"}
+.fa-satellite-dish::before{content:"\e027"}
+.fa-dollar-sign::before{content:"\e028"}
+.fa-angle-down::before{content:"\e029"}
+.fa-chevron-down::before{content:"\e029"}
+.fa-download::before{content:"\e02a"}
+.fa-droplet::before{content:"\e02b"}
+.fa-arrow-right-to-bracket::before{content:"\e02c"}
+.fa-right-to-bracket::before{content:"\e02c"}
+.fa-expand::before{content:"\e02d"}
+.fa-up-right-and-down-left-from-center::before{content:"\e02d"}
+.fa-eye::before{content:"\e02e"}
+.fa-eye-slash::before{content:"\e02f"}
+.fa-file::before{content:"\e030"}
+.fa-file-audio::before{content:"\e030"}
+.fa-file-lines::before{content:"\e030"}
+.fa-file-pdf::before{content:"\e030"}
+.fa-film::before{content:"\e031"}
+.fa-photo-film::before{content:"\e031"}
+.fa-filter::before{content:"\e032"}
+.fa-fire::before{content:"\e033"}
+.fa-flag::before{content:"\e034"}
+.fa-folder::before{content:"\e035"}
+.fa-utensils::before{content:"\e036"}
+.fa-gamepad::before{content:"\e038"}
+.fa-gauge::before{content:"\e039"}
+.fa-gauge-high::before{content:"\e039"}
+.fa-gavel::before{content:"\e03a"}
+.fa-gear::before{content:"\e03b"}
+.fa-gears::before{content:"\e03b"}
+.fa-gift::before{content:"\e03c"}
+.fa-earth-americas::before{content:"\e03d"}
+.fa-earth-europe::before{content:"\e03d"}
+.fa-globe::before{content:"\e03d"}
+.fa-grid::before{content:"\e03e"}
+.fa-table-cells::before{content:"\e03e"}
+.fa-hand::before{content:"\e03f"}
+.fa-hand-pointer::before{content:"\e03f"}
+.fa-hashtag::before{content:"\e040"}
+.fa-heart::before{content:"\e041"}
+.fa-circle-question::before{content:"\e042"}
+.fa-question::before{content:"\e042"}
+.fa-clock-rotate-left::before{content:"\e043"}
+.fa-house::before{content:"\e044"}
+.fa-house-chimney::before{content:"\e044"}
+.fa-id-card::before{content:"\e045"}
+.fa-lightbulb::before{content:"\e046"}
+.fa-file-image::before{content:"\e047"}
+.fa-image::before{content:"\e047"}
+.fa-images::before{content:"\e047"}
+.fa-circle-info::before{content:"\e048"}
+.fa-user-plus::before{content:"\e049"}
+.fa-key::before{content:"\e04a"}
+.fa-keyboard::before{content:"\e04b"}
+.fa-language::before{content:"\e04c"}
+.fa-layer-group::before{content:"\e04d"}
+.fa-leaf::before{content:"\e04e"}
+.fa-arrow-right-from-bracket::before{content:"\e04f"}
+.fa-right-from-bracket::before{content:"\e04f"}
+.fa-link::before{content:"\e050"}
+.fa-list::before{content:"\e051"}
+.fa-list-ul::before{content:"\e051"}
+.fa-broadcast-tower::before{content:"\e052"}
+.fa-tower-broadcast::before{content:"\e052"}
+.fa-tower-cell::before{content:"\e052"}
+.fa-lock::before{content:"\e053"}
+.fa-unlock::before{content:"\e053"}
+.fa-envelope::before{content:"\e054"}
+.fa-map::before{content:"\e055"}
+.fa-people-group::before{content:"\e056"}
+.fa-user-group::before{content:"\e056"}
+.fa-users::before{content:"\e056"}
+.fa-bars::before{content:"\e057"}
+.fa-microphone::before{content:"\e058"}
+.fa-microphone-slash::before{content:"\e059"}
+.fa-minus::before{content:"\e05a"}
+.fa-moon::before{content:"\e05b"}
+.fa-ellipsis::before{content:"\e05c"}
+.fa-ellipsis-vertical::before{content:"\e05c"}
+.fa-mountain::before{content:"\e05d"}
+.fa-mountain-sun::before{content:"\e05d"}
+.fa-music::before{content:"\e05e"}
+.fa-volume-off::before{content:"\e05f"}
+.fa-volume-xmark::before{content:"\e05f"}
+.fa-newspaper::before{content:"\e060"}
+.fa-circle-nodes::before{content:"\e061"}
+.fa-diagram-project::before{content:"\e061"}
+.fa-network-wired::before{content:"\e061"}
+.fa-share-nodes::before{content:"\e061"}
+.fa-sitemap::before{content:"\e061"}
+.fa-arrow-up-right-from-square::before{content:"\e062"}
+.fa-external-link::before{content:"\e062"}
+.fa-up-right-from-square::before{content:"\e062"}
+.fa-palette::before{content:"\e063"}
+.fa-pause::before{content:"\e064"}
+.fa-pen::before{content:"\e065"}
+.fa-pen-nib::before{content:"\e065"}
+.fa-pen-to-square::before{content:"\e065"}
+.fa-pencil::before{content:"\e065"}
+.fa-mobile-screen::before{content:"\e066"}
+.fa-phone::before{content:"\e066"}
+.fa-location-dot::before{content:"\e067"}
+.fa-location-pin::before{content:"\e067"}
+.fa-map-pin::before{content:"\e067"}
+.fa-paper-plane::before{content:"\e068"}
+.fa-circle-play::before{content:"\e069"}
+.fa-play::before{content:"\e069"}
+.fa-plug::before{content:"\e06a"}
+.fa-plus::before{content:"\e06b"}
+.fa-reply::before{content:"\e06c"}
+.fa-arrow-rotate-right::before{content:"\e06d"}
+.fa-arrows-rotate::before{content:"\e06d"}
+.fa-rotate::before{content:"\e06d"}
+.fa-rotate-left::before{content:"\e06d"}
+.fa-rotate-right::before{content:"\e06d"}
+.fa-robot::before{content:"\e06e"}
+.fa-route::before{content:"\e06f"}
+.fa-floppy-disk::before{content:"\e070"}
+.fa-cut::before{content:"\e071"}
+.fa-scissors::before{content:"\e071"}
+.fa-magnifying-glass::before{content:"\e072"}
+.fa-search::before{content:"\e072"}
+.fa-server::before{content:"\e074"}
+.fa-shield::before{content:"\e075"}
+.fa-shield-halved::before{content:"\e075"}
+.fa-user-shield::before{content:"\e075"}
+.fa-shirt::before{content:"\e076"}
+.fa-sliders::before{content:"\e079"}
+.fa-sliders-h::before{content:"\e079"}
+.fa-face-smile::before{content:"\e07a"}
+.fa-snowflake::before{content:"\e07b"}
+.fa-sparkles::before{content:"\e07c"}
+.fa-star-of-life::before{content:"\e07c"}
+.fa-wand-magic::before{content:"\e07c"}
+.fa-wand-magic-sparkles::before{content:"\e07c"}
+.fa-star::before{content:"\e07d"}
+.fa-list-check::before{content:"\e07e"}
+.fa-stop::before{content:"\e07f"}
+.fa-store::before{content:"\e080"}
+.fa-sun::before{content:"\e081"}
+.fa-thumbtack::before{content:"\e083"}
+.fa-tag::before{content:"\e084"}
+.fa-tags::before{content:"\e084"}
+.fa-bullseye::before{content:"\e085"}
+.fa-campground::before{content:"\e087"}
+.fa-terminal::before{content:"\e088"}
+.fa-font::before{content:"\e089"}
+.fa-text-height::before{content:"\e089"}
+.fa-ticket::before{content:"\e08a"}
+.fa-train::before{content:"\e08b"}
+.fa-trash::before{content:"\e08c"}
+.fa-trash-can::before{content:"\e08c"}
+.fa-tree::before{content:"\e08d"}
+.fa-trophy::before{content:"\e08e"}
+.fa-chevron-up::before{content:"\e08f"}
+.fa-cloud-arrow-up::before{content:"\e090"}
+.fa-upload::before{content:"\e090"}
+.fa-user::before{content:"\e091"}
+.fa-user-astronaut::before{content:"\e091"}
+.fa-user-secret::before{content:"\e091"}
+.fa-user-check::before{content:"\e092"}
+.fa-video::before{content:"\e093"}
+.fa-video-slash::before{content:"\e093"}
+.fa-volume-high::before{content:"\e094"}
+.fa-volume-low::before{content:"\e094"}
+.fa-wallet::before{content:"\e095"}
+.fa-person-swimming::before{content:"\e096"}
+.fa-water::before{content:"\e096"}
+.fa-wave-square::before{content:"\e097"}
+.fa-signal::before{content:"\e098"}
+.fa-wifi::before{content:"\e098"}
+.fa-screwdriver-wrench::before{content:"\e099"}
+.fa-wrench::before{content:"\e099"}
+.fa-circle-xmark::before{content:"\e09a"}
+.fa-xmark-circle::before{content:"\e09a"}
+.fa-bolt::before{content:"\e09b"}
+.fa-bolt-lightning::before{content:"\e09b"}
new file mode 100644
new file mode 100644
@@ -0,0 +1,160 @@
@@ -0,0 +1,160 @@
+/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */
+/* <i class="ovi ovi-eye"></i>: the OpenVibe glyphs as a font. Sizes follow the text. */
+@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}
+.ovi{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}
+.ovi-fw{width:1.25em;text-align:center}
+.ovi-alert::before{content:"\e001"}
+.ovi-alert-circle::before{content:"\e002"}
+.ovi-archive::before{content:"\e003"}
+.ovi-arrow-down::before{content:"\e004"}
+.ovi-arrow-left::before{content:"\e005"}
+.ovi-arrow-right::before{content:"\e006"}
+.ovi-arrow-up::before{content:"\e007"}
+.ovi-back::before{content:"\e008"}
+.ovi-ban::before{content:"\e009"}
+.ovi-bell::before{content:"\e00a"}
+.ovi-book::before{content:"\e00b"}
+.ovi-brain::before{content:"\e00c"}
+.ovi-branch::before{content:"\e00d"}
+.ovi-bridge::before{content:"\e00e"}
+.ovi-busy::before{content:"\e00f"}
+.ovi-calendar::before{content:"\e010"}
+.ovi-camera::before{content:"\e011"}
+.ovi-care::before{content:"\e012"}
+.ovi-cart::before{content:"\e013"}
+.ovi-chart::before{content:"\e014"}
+.ovi-chat::before{content:"\e015"}
+.ovi-check::before{content:"\e016"}
+.ovi-check-circle::before{content:"\e017"}
+.ovi-chevron::before{content:"\e018"}
+.ovi-chip::before{content:"\e019"}
+.ovi-circle::before{content:"\e01a"}
+.ovi-city::before{content:"\e01b"}
+.ovi-clipboard::before{content:"\e01c"}
+.ovi-clock::before{content:"\e01d"}
+.ovi-close::before{content:"\e01e"}
+.ovi-cloud::before{content:"\e01f"}
+.ovi-code::before{content:"\e020"}
+.ovi-coin::before{content:"\e021"}
+.ovi-compress::before{content:"\e022"}
+.ovi-computer::before{content:"\e023"}
+.ovi-copy::before{content:"\e024"}
+.ovi-crown::before{content:"\e025"}
+.ovi-database::before{content:"\e026"}
+.ovi-dish::before{content:"\e027"}
+.ovi-dollar::before{content:"\e028"}
+.ovi-down::before{content:"\e029"}
+.ovi-download::before{content:"\e02a"}
+.ovi-drop::before{content:"\e02b"}
+.ovi-enter::before{content:"\e02c"}
+.ovi-expand::before{content:"\e02d"}
+.ovi-eye::before{content:"\e02e"}
+.ovi-eye-off::before{content:"\e02f"}
+.ovi-file::before{content:"\e030"}
+.ovi-film::before{content:"\e031"}
+.ovi-filter::before{content:"\e032"}
+.ovi-fire::before{content:"\e033"}
+.ovi-flag::before{content:"\e034"}
+.ovi-folder::before{content:"\e035"}
+.ovi-food::before{content:"\e036"}
+.ovi-forward::before{content:"\e037"}
+.ovi-game::before{content:"\e038"}
+.ovi-gauge::before{content:"\e039"}
+.ovi-gavel::before{content:"\e03a"}
+.ovi-gear::before{content:"\e03b"}
+.ovi-gift::before{content:"\e03c"}
+.ovi-globe::before{content:"\e03d"}
+.ovi-grid::before{content:"\e03e"}
+.ovi-hand::before{content:"\e03f"}
+.ovi-hash::before{content:"\e040"}
+.ovi-heart::before{content:"\e041"}
+.ovi-help::before{content:"\e042"}
+.ovi-history::before{content:"\e043"}
+.ovi-home::before{content:"\e044"}
+.ovi-id::before{content:"\e045"}
+.ovi-idea::before{content:"\e046"}
+.ovi-image::before{content:"\e047"}
+.ovi-info::before{content:"\e048"}
+.ovi-invite::before{content:"\e049"}
+.ovi-key::before{content:"\e04a"}
+.ovi-keyboard::before{content:"\e04b"}
+.ovi-language::before{content:"\e04c"}
+.ovi-layers::before{content:"\e04d"}
+.ovi-leaf::before{content:"\e04e"}
+.ovi-leave::before{content:"\e04f"}
+.ovi-link::before{content:"\e050"}
+.ovi-list::before{content:"\e051"}
+.ovi-live::before{content:"\e052"}
+.ovi-lock::before{content:"\e053"}
+.ovi-mail::before{content:"\e054"}
+.ovi-map::before{content:"\e055"}
+.ovi-members::before{content:"\e056"}
+.ovi-menu::before{content:"\e057"}
+.ovi-mic::before{content:"\e058"}
+.ovi-mic-off::before{content:"\e059"}
+.ovi-minus::before{content:"\e05a"}
+.ovi-moon::before{content:"\e05b"}
+.ovi-more::before{content:"\e05c"}
+.ovi-mountain::before{content:"\e05d"}
+.ovi-music::before{content:"\e05e"}
+.ovi-mute::before{content:"\e05f"}
+.ovi-news::before{content:"\e060"}
+.ovi-nodes::before{content:"\e061"}
+.ovi-open::before{content:"\e062"}
+.ovi-palette::before{content:"\e063"}
+.ovi-pause::before{content:"\e064"}
+.ovi-pencil::before{content:"\e065"}
+.ovi-phone::before{content:"\e066"}
+.ovi-pin::before{content:"\e067"}
+.ovi-plane::before{content:"\e068"}
+.ovi-play::before{content:"\e069"}
+.ovi-plug::before{content:"\e06a"}
+.ovi-plus::before{content:"\e06b"}
+.ovi-reply::before{content:"\e06c"}
+.ovi-retry::before{content:"\e06d"}
+.ovi-robot::before{content:"\e06e"}
+.ovi-route::before{content:"\e06f"}
+.ovi-save::before{content:"\e070"}
+.ovi-scissors::before{content:"\e071"}
+.ovi-search::before{content:"\e072"}
+.ovi-send::before{content:"\e073"}
+.ovi-server::before{content:"\e074"}
+.ovi-shield::before{content:"\e075"}
+.ovi-shirt::before{content:"\e076"}
+.ovi-shoe::before{content:"\e077"}
+.ovi-sidebar::before{content:"\e078"}
+.ovi-sliders::before{content:"\e079"}
+.ovi-smile::before{content:"\e07a"}
+.ovi-snow::before{content:"\e07b"}
+.ovi-spark::before{content:"\e07c"}
+.ovi-star::before{content:"\e07d"}
+.ovi-steps::before{content:"\e07e"}
+.ovi-stop::before{content:"\e07f"}
+.ovi-store::before{content:"\e080"}
+.ovi-sun::before{content:"\e081"}
+.ovi-swap::before{content:"\e082"}
+.ovi-tack::before{content:"\e083"}
+.ovi-tag::before{content:"\e084"}
+.ovi-target::before{content:"\e085"}
+.ovi-task::before{content:"\e086"}
+.ovi-tent::before{content:"\e087"}
+.ovi-terminal::before{content:"\e088"}
+.ovi-text::before{content:"\e089"}
+.ovi-ticket::before{content:"\e08a"}
+.ovi-train::before{content:"\e08b"}
+.ovi-trash::before{content:"\e08c"}
+.ovi-tree::before{content:"\e08d"}
+.ovi-trophy::before{content:"\e08e"}
+.ovi-up::before{content:"\e08f"}
+.ovi-upload::before{content:"\e090"}
+.ovi-user::before{content:"\e091"}
+.ovi-user-check::before{content:"\e092"}
+.ovi-video::before{content:"\e093"}
+.ovi-volume::before{content:"\e094"}
+.ovi-wallet::before{content:"\e095"}
+.ovi-water::before{content:"\e096"}
+.ovi-wave::before{content:"\e097"}
+.ovi-wifi::before{content:"\e098"}
+.ovi-wrench::before{content:"\e099"}
+.ovi-x-circle::before{content:"\e09a"}
+.ovi-zap::before{content:"\e09b"}
@@ -5,6 +5,12 @@
@@ -5,6 +5,12 @@
* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every
* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every
* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.
* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.
*
*
+ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,
+ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the
+ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light
+ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that
+ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured.
+ *
* <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later
* <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later
* OVGlyphs.swap(rootEl) swaps inside one element now
* OVGlyphs.swap(rootEl) swaps inside one element now
* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null
* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null
@@ -16,30 +22,44 @@
@@ -16,30 +22,44 @@
if (typeof window === 'undefined' || window.OVGlyphs) return;
if (typeof window === 'undefined' || window.OVGlyphs) return;
const G = {"alert":"<path d=\"M12 4 2.8 19.5h18.4Z\"/><path d=\"M12 10v4M12 17h.01\"/>","alert-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5v5.5M12 16.5h.01\"/>","archive":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"5\" rx=\"1\"/><path d=\"M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4\"/>","arrow-down":"<path d=\"M12 4v16M6 14l6 6 6-6\"/>","arrow-left":"<path d=\"M20 12H4M10 6l-6 6 6 6\"/>","arrow-right":"<path d=\"M4 12h16M14 6l6 6-6 6\"/>","arrow-up":"<path d=\"M12 20V4M6 10l6-6 6 6\"/>","back":"<path d=\"M15 5l-7 7 7 7\"/>","ban":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m6 6 12 12\"/>","bell":"<path d=\"M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20a2 2 0 0 0 4 0\"/>","book":"<path d=\"M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Z\"/><path d=\"M4 19V5\"/>","brain":"<path d=\"M9 4a3 3 0 0 0-3 3 3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 6 1.2V5.5A2 2 0 0 0 9 4ZM15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-6 1.2\"/>","branch":"<circle cx=\"6\" cy=\"5\" r=\"2\"/><circle cx=\"6\" cy=\"19\" r=\"2\"/><circle cx=\"18\" cy=\"7\" r=\"2\"/><path d=\"M6 7v10M18 9c0 5.5-6.5 4-11 8.5\"/>","bridge":"<path d=\"M2 15.5h20M4.5 15.5V10M19.5 15.5V10M2 9.5c4 0 6-3 10-3s6 3 10 3M8.5 15.5V12M15.5 15.5V12M12 15.5V8\"/>","busy":"<path d=\"M6 12h.01M12 12h.01M18 12h.01\" stroke-width=\"3.4\"/>","calendar":"<rect x=\"4\" y=\"5\" width=\"16\" height=\"15\" rx=\"2\"/><path d=\"M4 10h16M9 3v4M15 3v4\"/>","camera":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><circle cx=\"12\" cy=\"13.3\" r=\"3.4\"/>","care":"<path d=\"M3 15.5h3l4 3h5.5a2 2 0 0 0 0-4H12M7 15.5l2.2-2h3.3\"/><path d=\"M15.5 10s-3.2-2-3.2-4.4a1.9 1.9 0 0 1 3.2-1.3 1.9 1.9 0 0 1 3.2 1.3c0 2.4-3.2 4.4-3.2 4.4Z\"/>","cart":"<path d=\"M3 4h2l2.4 11h11L21 7H6.2\"/><circle cx=\"9\" cy=\"19.5\" r=\"1.3\"/><circle cx=\"17\" cy=\"19.5\" r=\"1.3\"/>","chart":"<path d=\"M4 4v16h16\"/><path d=\"m7.5 14.5 3.5-4 3 2.5 5-6\"/>","chat":"<path d=\"M4 5h16v11H8l-4 4Z\"/>","check":"<path d=\"m5 12.5 4.5 4.5L19 7.5\"/>","check-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8 12.4 2.8 2.8L16 9.6\"/>","chevron":"<path d=\"m9 6 6 6-6 6\"/>","chip":"<rect x=\"7\" y=\"7\" width=\"10\" height=\"10\" rx=\"1.5\"/><path d=\"M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/>","city":"<path d=\"M3 21h18M5 21V9h5v12M10 21V4h6v17M16 21v-8h3.5v8M7.5 12h.01M7.5 15.5h.01M13 7.5h.01M13 11h.01M13 14.5h.01\"/>","clipboard":"<rect x=\"5.5\" y=\"5\" width=\"13\" height=\"16\" rx=\"2\"/><path d=\"M9 5V3.5h6V5M9 11h6M9 15h4\"/>","clock":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5V12l3 2\"/>","close":"<path d=\"M6 6l12 12M18 6 6 18\"/>","cloud":"<path d=\"M7 18h10a4 4 0 0 0 .5-8A6 6 0 0 0 6 9.5 4.3 4.3 0 0 0 7 18Z\"/>","code":"<path d=\"m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14\"/>","coin":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11\"/>","compress":"<path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/>","computer":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\"/><path d=\"M8 20h8M12 16v4\"/>","copy":"<rect x=\"8\" y=\"8\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2\"/>","crown":"<path d=\"M3 8.5 7.5 12.5 12 5.5l4.5 7 4.5-4L19 19H5Z\"/>","database":"<ellipse cx=\"12\" cy=\"6\" rx=\"7\" ry=\"3\"/><path d=\"M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3\"/>","dish":"<path d=\"M5 9.5A9.5 9.5 0 0 0 14.5 19Z\"/><path d=\"m9.5 14.5 4.5-4.5M14.5 4.5a5 5 0 0 1 5 5M14.5 8a1.5 1.5 0 0 1 1.5 1.5\"/>","dollar":"<path d=\"M16 7.5C15.3 6 13.8 5 12 5 9.5 5 8 6.3 8 8s1.7 2.6 4 3 4 1.4 4 3.3-1.6 3.2-4 3.2c-1.9 0-3.4-1-4-2.5M12 3v18\"/>","down":"<path d=\"m6 9 6 6 6-6\"/>","download":"<path d=\"M12 4v11M7 10l5 5 5-5M5 20h14\"/>","drop":"<path d=\"M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z\"/>","enter":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 8l4 4-4 4M14 12H4\"/>","expand":"<path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/>","eye":"<path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>","eye-off":"<path d=\"M3 12s3.5-6.5 9-6.5c1.6 0 3 .5 4.3 1.2M21 12s-3.5 6.5-9 6.5c-1.6 0-3-.5-4.3-1.2\"/><path d=\"M9.9 9.9a3 3 0 0 0 4.2 4.2M4 4l16 16\"/>","file":"<path d=\"M6 3h8l4 4v14H6Z\"/><path d=\"M14 3v4h4\"/>","film":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M7.5 4v16M16.5 4v16M3 9h4.5M3 15h4.5M16.5 9H21M16.5 15H21\"/>","filter":"<path d=\"M4 6h16M7 12h10M10 18h4\"/>","fire":"<path d=\"M12 21a6 6 0 0 0 6-6c0-3.8-2.6-6-3.7-9-1.8 2-2.3 4-2.3 4S10.8 8 9.8 6.2C7.6 8.7 6 11.6 6 15a6 6 0 0 0 6 6Z\"/>","flag":"<path d=\"M5 21V4M5 4h11l-2 4 2 4H5\"/>","folder":"<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\"/>","food":"<path d=\"M7 3v7M5 3v4.5a2 2 0 0 0 4 0V3M7 10v11M17 21V3c-2 1.2-3.2 3.6-3.2 7v3H17\"/>","game":"<rect x=\"2.5\" y=\"7\" width=\"19\" height=\"11\" rx=\"5\"/><path d=\"M7 11v3M5.5 12.5h3M15.5 12h.01M18 13.5h.01\"/>","gauge":"<path d=\"M4 18a8 8 0 1 1 16 0\"/><path d=\"m12 14.5 4-5.5\"/><circle cx=\"12\" cy=\"14.5\" r=\"1.3\"/>","gavel":"<path d=\"m14 4.5 5.5 5.5M10.5 8l5.5 5.5M12.3 6.3l-6 6 5.4 5.4 6-6M3 21l6.3-6.3M14 21h7\"/>","gear":"<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z\"/>","gift":"<rect x=\"4\" y=\"9\" width=\"16\" height=\"11\" rx=\"1\"/><path d=\"M3 9h18M12 9v11M12 9c-1.5-3-5-4-5-1.5S12 9 12 9s5-1 5-3.5S13.5 6 12 9\"/>","globe":"<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\"/>","grid":"<rect x=\"4\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"4\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/>","hand":"<path d=\"M8 13V5.5a1.5 1.5 0 0 1 3 0V11M11 10V4.5a1.5 1.5 0 0 1 3 0V11M14 10.5V6a1.5 1.5 0 0 1 3 0v7c0 4-2.5 7-6.5 7-2.6 0-4.2-1.3-5.7-3.6L3 13.6a1.5 1.5 0 0 1 2.4-1.8L8 15\"/>","hash":"<path d=\"M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17\"/>","heart":"<path d=\"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z\"/>","help":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01\"/>","history":"<path d=\"M4 12a8 8 0 1 0 2.4-5.7L4 8.6M4 4.5v4.1h4.1\"/><path d=\"M12 8v4.2l3 1.8\"/>","home":"<path d=\"M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1Z\"/>","id":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><circle cx=\"9\" cy=\"11\" r=\"2\"/><path d=\"M6 16c.5-1.5 1.7-2.3 3-2.3s2.5.8 3 2.3M15 10h3M15 13.5h3\"/>","idea":"<path d=\"M9 18h6M10 21h4M8.6 14.6A6 6 0 1 1 15.4 14.6c-.7.6-1.4 1.5-1.4 2.4h-4c0-.9-.7-1.8-1.4-2.4Z\"/>","image":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"3\"/><circle cx=\"9\" cy=\"10\" r=\"1.6\"/><path d=\"m21 16-5-5-8 8\"/>","info":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 11v5M12 8h.01\"/>","invite":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M19 8v6M16 11h6\"/>","key":"<circle cx=\"8\" cy=\"15\" r=\"4\"/><path d=\"m10.8 12.2 8.7-8.7M16 7l3 3M14 9l2 2\"/>","keyboard":"<rect x=\"2.5\" y=\"6\" width=\"19\" height=\"12\" rx=\"2\"/><path d=\"M6 10h.01M9 10h.01M12 10h.01M15 10h.01M18 10h.01M8 14h8\"/>","language":"<path d=\"M4 5h9M8.5 3v2M6 5c.5 3 2.5 6 6 7.5M11 5c-.6 3.4-2.8 6.8-6.5 8.5M12 21l4-9 4 9M13.4 18h5.2\"/>","layers":"<path d=\"m12 3.5 9 5-9 5-9-5Z\"/><path d=\"m3 13 9 5 9-5\"/>","leaf":"<path d=\"M5 19c0-8.5 5-13.6 15-15-.9 9.9-6 15-15 15ZM5 19l8-8\"/>","leave":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 16l-4-4 4-4M6 12h10\"/>","link":"<path d=\"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1\"/>","list":"<path d=\"M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01\"/>","live":"<circle cx=\"12\" cy=\"12\" r=\"2.5\"/><path d=\"M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14\"/>","lock":"<rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\"/><path d=\"M8 11V8a4 4 0 0 1 8 0v3\"/>","mail":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"m4 7 8 6 8-6\"/>","map":"<path d=\"M9 4 3 6v14l6-2 6 2 6-2V4l-6 2Z\"/><path d=\"M9 4v14M15 6v14\"/>","members":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 4.8a3.2 3.2 0 0 1 0 6.4M18 14.8c1.9.6 3 2.4 3 5.2\"/>","menu":"<path d=\"M4 7h16M4 12h16M4 17h16\"/>","mic":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 14 0M12 18v3\"/>","mic-off":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 11.6 5.3M19 11c0 .8-.1 1.6-.4 2.3M12 18v3M4 4l16 16\"/>","minus":"<path d=\"M5 12h14\"/>","moon":"<path d=\"M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\"/>","more":"<circle cx=\"5\" cy=\"12\" r=\"1.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\"/>","mountain":"<path d=\"M3 20 9.5 8.5l4 6.5 2.5-3.5L21 20Z\"/>","music":"<path d=\"M9 18V6l11-2v12\"/><circle cx=\"6.5\" cy=\"18\" r=\"2.5\"/><circle cx=\"17.5\" cy=\"16\" r=\"2.5\"/>","mute":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"m16 10 4 4M20 10l-4 4\"/>","news":"<path d=\"M4 5h13v14a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2Z\"/><path d=\"M17 9h3v10a2 2 0 0 1-2 2M7.5 9h6M7.5 12.5h6M7.5 16h3.5\"/>","nodes":"<circle cx=\"12\" cy=\"5\" r=\"2.2\"/><circle cx=\"5.5\" cy=\"18\" r=\"2.2\"/><circle cx=\"18.5\" cy=\"18\" r=\"2.2\"/><path d=\"M11 7 6.5 16M13 7l4.5 9M7.7 18h8.6\"/>","open":"<path d=\"M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\"/>","palette":"<path d=\"M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6s.8-1.7 2-1.7H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3Z\"/><circle cx=\"7.5\" cy=\"11\" r=\"1.2\"/><circle cx=\"10\" cy=\"7.3\" r=\"1.2\"/><circle cx=\"14.5\" cy=\"7.3\" r=\"1.2\"/>","pause":"<path d=\"M8 5v14M16 5v14\"/>","pencil":"<path d=\"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"/>","phone":"<path d=\"M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2\"/>","pin":"<path d=\"M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z\"/><circle cx=\"12\" cy=\"10\" r=\"2.4\"/>","plane":"<path d=\"M20.5 3.5 3.5 10.5l6.8 3.2 3.2 6.8Z\"/><path d=\"m10.3 13.7 10.2-10.2\"/>","play":"<path d=\"M8 5v14l11-7Z\"/>","plug":"<path d=\"M9 3v5M15 3v5M6.5 8h11v3a5.5 5.5 0 0 1-11 0ZM12 16.5V21\"/>","plus":"<path d=\"M12 5v14M5 12h14\"/>","reply":"<path d=\"M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6v1\"/>","retry":"<path d=\"M4 12a8 8 0 0 1 13.7-5.7L20 8.5M20 4v4.5h-4.5M20 12a8 8 0 0 1-13.7 5.7L4 15.5M4 20v-4.5h4.5\"/>","robot":"<rect x=\"5\" y=\"8\" width=\"14\" height=\"11\" rx=\"3\"/><path d=\"M12 4v4M9 13h.01M15 13h.01M9.5 16.5h5M3 13v2M21 13v2\"/>","route":"<circle cx=\"6\" cy=\"18\" r=\"2.4\"/><circle cx=\"18\" cy=\"6\" r=\"2.4\"/><path d=\"M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6\"/>","save":"<path d=\"M5 4h11l3 3v13H5Z\"/><path d=\"M8 4v5h7V4M8 20v-6h8v6\"/>","scissors":"<circle cx=\"6\" cy=\"7\" r=\"2.6\"/><circle cx=\"6\" cy=\"17\" r=\"2.6\"/><path d=\"M8.2 8.6 20 18M8.2 15.4 20 6\"/>","search":"<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m20 20-4.2-4.2\"/>","server":"<rect x=\"4\" y=\"4\" width=\"16\" height=\"7\" rx=\"2\"/><rect x=\"4\" y=\"13\" width=\"16\" height=\"7\" rx=\"2\"/><path d=\"M8 7.5h.01M8 16.5h.01\"/>","shield":"<path d=\"M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6Z\"/>","shirt":"<path d=\"M8 3 3 6l2 4 2-1v12h10V9l2 1 2-4-5-3c-.5 1.5-2 2.5-4 2.5S8.5 4.5 8 3Z\"/>","sliders":"<path d=\"M4 7h9.5M18.5 7H20M4 17h3.5M12.5 17H20\"/><circle cx=\"16\" cy=\"7\" r=\"2.3\"/><circle cx=\"10\" cy=\"17\" r=\"2.3\"/>","smile":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01\"/>","snow":"<path d=\"M12 3v18M4.2 7.5l15.6 9M4.2 16.5l15.6-9M9.5 4.5 12 7l2.5-2.5M9.5 19.5 12 17l2.5 2.5\"/>","spark":"<path d=\"M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8ZM19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z\"/>","star":"<path d=\"m12 4 2.4 5 5.4.6-4 3.7 1.1 5.4L12 16l-4.9 2.7 1.1-5.4-4-3.7 5.4-.6Z\"/>","steps":"<path d=\"M9 6h11M9 12h11M9 18h11\"/><path d=\"m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17\"/>","stop":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8.5 8.5 7 7\"/>","store":"<path d=\"M4 9 5.5 4h13L20 9M4 9h16v1.5a2.7 2.7 0 0 1-5.3 0 2.7 2.7 0 0 1-5.4 0A2.7 2.7 0 0 1 4 10.5ZM5 13v7h14v-7M10 20v-4h4v4\"/>","sun":"<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/>","tack":"<path d=\"M9 3h6l-1 5 3 3H7l3-3ZM12 11v10\"/>","tag":"<path d=\"M3 12V4h8l10 10-8 8Z\"/><circle cx=\"7.5\" cy=\"8.5\" r=\"1.3\"/>","target":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/>","tent":"<path d=\"M2.5 20h19M12 5 4 20M12 5l8 15M12 13.5 9 20M12 13.5l3 6.5\"/>","terminal":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"m7 9 3 3-3 3M13 15h4\"/>","text":"<path d=\"M5.5 20 11 4h2l5.5 16M7.6 14h8.8\"/>","ticket":"<path d=\"M3 7h18v3a2 2 0 0 0 0 4v3H3v-3a2 2 0 0 0 0-4Z\"/><path d=\"M14 7v10\"/>","train":"<rect x=\"6\" y=\"3\" width=\"12\" height=\"13\" rx=\"3\"/><path d=\"M6 10h12M9 13h.01M15 13h.01M8.5 21l2-3.5M15.5 21l-2-3.5\"/>","trash":"<path d=\"M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3\"/>","tree":"<path d=\"M12 3 6.5 10.5h3L5 17h14l-4.5-6.5h3ZM12 17v4\"/>","trophy":"<path d=\"M8 4h8v5a4 4 0 0 1-8 0ZM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 20h8\"/>","up":"<path d=\"m6 15 6-6 6 6\"/>","upload":"<path d=\"M12 20V9M7 14l5-5 5 5M5 4h14\"/>","user":"<circle cx=\"12\" cy=\"8\" r=\"3.5\"/><path d=\"M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6\"/>","user-check":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 11l2 2 4-4\"/>","video":"<rect x=\"3\" y=\"6\" width=\"13\" height=\"12\" rx=\"2\"/><path d=\"m16 10 5-3v10l-5-3\"/>","volume":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10\"/>","wallet":"<path d=\"M4 7.5A2.5 2.5 0 0 1 6.5 5H18v4M4 7.5V18a2 2 0 0 0 2 2h14V9H6.5A2.5 2.5 0 0 1 4 7.5Z\"/><path d=\"M16 14.5h.01\"/>","water":"<path d=\"M2.5 9c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2M2.5 15c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2\"/>","wave":"<path d=\"M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2\"/><path d=\"M5.5 11.5A7 7 0 0 1 18 8.6c-2.3-.5-4.2.4-4.8 2.4\"/>","wifi":"<path d=\"M2.5 9a14 14 0 0 1 19 0M5.5 12.5a9.5 9.5 0 0 1 13 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01\"/>","wrench":"<path d=\"M14.5 4a5 5 0 0 0-4.4 7L4 17.1V20h2.9l6.1-6.1a5 5 0 0 0 7-4.4l-3 1.1-2.4-2.4 1.1-3Z\"/>","x-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m9 9 6 6M15 9l-6 6\"/>","zap":"<path d=\"M13 3 5 13h6l-1 8 8-10h-6Z\"/>"};
const G = {"alert":"<path d=\"M12 4 2.8 19.5h18.4Z\"/><path d=\"M12 10v4M12 17h.01\"/>","alert-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5v5.5M12 16.5h.01\"/>","archive":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"5\" rx=\"1\"/><path d=\"M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4\"/>","arrow-down":"<path d=\"M12 4v16M6 14l6 6 6-6\"/>","arrow-left":"<path d=\"M20 12H4M10 6l-6 6 6 6\"/>","arrow-right":"<path d=\"M4 12h16M14 6l6 6-6 6\"/>","arrow-up":"<path d=\"M12 20V4M6 10l6-6 6 6\"/>","back":"<path d=\"M15 5l-7 7 7 7\"/>","ban":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m6 6 12 12\"/>","bell":"<path d=\"M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20a2 2 0 0 0 4 0\"/>","book":"<path d=\"M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Z\"/><path d=\"M4 19V5\"/>","brain":"<path d=\"M9 4a3 3 0 0 0-3 3 3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 6 1.2V5.5A2 2 0 0 0 9 4ZM15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-6 1.2\"/>","branch":"<circle cx=\"6\" cy=\"5\" r=\"2\"/><circle cx=\"6\" cy=\"19\" r=\"2\"/><circle cx=\"18\" cy=\"7\" r=\"2\"/><path d=\"M6 7v10M18 9c0 5.5-6.5 4-11 8.5\"/>","bridge":"<path d=\"M2 15.5h20M4.5 15.5V10M19.5 15.5V10M2 9.5c4 0 6-3 10-3s6 3 10 3M8.5 15.5V12M15.5 15.5V12M12 15.5V8\"/>","busy":"<path d=\"M6 12h.01M12 12h.01M18 12h.01\" stroke-width=\"3.4\"/>","calendar":"<rect x=\"4\" y=\"5\" width=\"16\" height=\"15\" rx=\"2\"/><path d=\"M4 10h16M9 3v4M15 3v4\"/>","camera":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><circle cx=\"12\" cy=\"13.3\" r=\"3.4\"/>","care":"<path d=\"M3 15.5h3l4 3h5.5a2 2 0 0 0 0-4H12M7 15.5l2.2-2h3.3\"/><path d=\"M15.5 10s-3.2-2-3.2-4.4a1.9 1.9 0 0 1 3.2-1.3 1.9 1.9 0 0 1 3.2 1.3c0 2.4-3.2 4.4-3.2 4.4Z\"/>","cart":"<path d=\"M3 4h2l2.4 11h11L21 7H6.2\"/><circle cx=\"9\" cy=\"19.5\" r=\"1.3\"/><circle cx=\"17\" cy=\"19.5\" r=\"1.3\"/>","chart":"<path d=\"M4 4v16h16\"/><path d=\"m7.5 14.5 3.5-4 3 2.5 5-6\"/>","chat":"<path d=\"M4 5h16v11H8l-4 4Z\"/>","check":"<path d=\"m5 12.5 4.5 4.5L19 7.5\"/>","check-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8 12.4 2.8 2.8L16 9.6\"/>","chevron":"<path d=\"m9 6 6 6-6 6\"/>","chip":"<rect x=\"7\" y=\"7\" width=\"10\" height=\"10\" rx=\"1.5\"/><path d=\"M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/>","city":"<path d=\"M3 21h18M5 21V9h5v12M10 21V4h6v17M16 21v-8h3.5v8M7.5 12h.01M7.5 15.5h.01M13 7.5h.01M13 11h.01M13 14.5h.01\"/>","clipboard":"<rect x=\"5.5\" y=\"5\" width=\"13\" height=\"16\" rx=\"2\"/><path d=\"M9 5V3.5h6V5M9 11h6M9 15h4\"/>","clock":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5V12l3 2\"/>","close":"<path d=\"M6 6l12 12M18 6 6 18\"/>","cloud":"<path d=\"M7 18h10a4 4 0 0 0 .5-8A6 6 0 0 0 6 9.5 4.3 4.3 0 0 0 7 18Z\"/>","code":"<path d=\"m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14\"/>","coin":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11\"/>","compress":"<path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/>","computer":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\"/><path d=\"M8 20h8M12 16v4\"/>","copy":"<rect x=\"8\" y=\"8\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2\"/>","crown":"<path d=\"M3 8.5 7.5 12.5 12 5.5l4.5 7 4.5-4L19 19H5Z\"/>","database":"<ellipse cx=\"12\" cy=\"6\" rx=\"7\" ry=\"3\"/><path d=\"M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3\"/>","dish":"<path d=\"M5 9.5A9.5 9.5 0 0 0 14.5 19Z\"/><path d=\"m9.5 14.5 4.5-4.5M14.5 4.5a5 5 0 0 1 5 5M14.5 8a1.5 1.5 0 0 1 1.5 1.5\"/>","dollar":"<path d=\"M16 7.5C15.3 6 13.8 5 12 5 9.5 5 8 6.3 8 8s1.7 2.6 4 3 4 1.4 4 3.3-1.6 3.2-4 3.2c-1.9 0-3.4-1-4-2.5M12 3v18\"/>","down":"<path d=\"m6 9 6 6 6-6\"/>","download":"<path d=\"M12 4v11M7 10l5 5 5-5M5 20h14\"/>","drop":"<path d=\"M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z\"/>","enter":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 8l4 4-4 4M14 12H4\"/>","expand":"<path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/>","eye":"<path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>","eye-off":"<path d=\"M3 12s3.5-6.5 9-6.5c1.6 0 3 .5 4.3 1.2M21 12s-3.5 6.5-9 6.5c-1.6 0-3-.5-4.3-1.2\"/><path d=\"M9.9 9.9a3 3 0 0 0 4.2 4.2M4 4l16 16\"/>","file":"<path d=\"M6 3h8l4 4v14H6Z\"/><path d=\"M14 3v4h4\"/>","film":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M7.5 4v16M16.5 4v16M3 9h4.5M3 15h4.5M16.5 9H21M16.5 15H21\"/>","filter":"<path d=\"M4 6h16M7 12h10M10 18h4\"/>","fire":"<path d=\"M12 21a6 6 0 0 0 6-6c0-3.8-2.6-6-3.7-9-1.8 2-2.3 4-2.3 4S10.8 8 9.8 6.2C7.6 8.7 6 11.6 6 15a6 6 0 0 0 6 6Z\"/>","flag":"<path d=\"M5 21V4M5 4h11l-2 4 2 4H5\"/>","folder":"<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\"/>","food":"<path d=\"M7 3v7M5 3v4.5a2 2 0 0 0 4 0V3M7 10v11M17 21V3c-2 1.2-3.2 3.6-3.2 7v3H17\"/>","game":"<rect x=\"2.5\" y=\"7\" width=\"19\" height=\"11\" rx=\"5\"/><path d=\"M7 11v3M5.5 12.5h3M15.5 12h.01M18 13.5h.01\"/>","gauge":"<path d=\"M4 18a8 8 0 1 1 16 0\"/><path d=\"m12 14.5 4-5.5\"/><circle cx=\"12\" cy=\"14.5\" r=\"1.3\"/>","gavel":"<path d=\"m14 4.5 5.5 5.5M10.5 8l5.5 5.5M12.3 6.3l-6 6 5.4 5.4 6-6M3 21l6.3-6.3M14 21h7\"/>","gear":"<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z\"/>","gift":"<rect x=\"4\" y=\"9\" width=\"16\" height=\"11\" rx=\"1\"/><path d=\"M3 9h18M12 9v11M12 9c-1.5-3-5-4-5-1.5S12 9 12 9s5-1 5-3.5S13.5 6 12 9\"/>","globe":"<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\"/>","grid":"<rect x=\"4\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"4\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"4\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/><rect x=\"13\" y=\"13\" width=\"7\" height=\"7\" rx=\"1.5\"/>","hand":"<path d=\"M8 13V5.5a1.5 1.5 0 0 1 3 0V11M11 10V4.5a1.5 1.5 0 0 1 3 0V11M14 10.5V6a1.5 1.5 0 0 1 3 0v7c0 4-2.5 7-6.5 7-2.6 0-4.2-1.3-5.7-3.6L3 13.6a1.5 1.5 0 0 1 2.4-1.8L8 15\"/>","hash":"<path d=\"M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17\"/>","heart":"<path d=\"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z\"/>","help":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01\"/>","history":"<path d=\"M4 12a8 8 0 1 0 2.4-5.7L4 8.6M4 4.5v4.1h4.1\"/><path d=\"M12 8v4.2l3 1.8\"/>","home":"<path d=\"M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1Z\"/>","id":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><circle cx=\"9\" cy=\"11\" r=\"2\"/><path d=\"M6 16c.5-1.5 1.7-2.3 3-2.3s2.5.8 3 2.3M15 10h3M15 13.5h3\"/>","idea":"<path d=\"M9 18h6M10 21h4M8.6 14.6A6 6 0 1 1 15.4 14.6c-.7.6-1.4 1.5-1.4 2.4h-4c0-.9-.7-1.8-1.4-2.4Z\"/>","image":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"3\"/><circle cx=\"9\" cy=\"10\" r=\"1.6\"/><path d=\"m21 16-5-5-8 8\"/>","info":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 11v5M12 8h.01\"/>","invite":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M19 8v6M16 11h6\"/>","key":"<circle cx=\"8\" cy=\"15\" r=\"4\"/><path d=\"m10.8 12.2 8.7-8.7M16 7l3 3M14 9l2 2\"/>","keyboard":"<rect x=\"2.5\" y=\"6\" width=\"19\" height=\"12\" rx=\"2\"/><path d=\"M6 10h.01M9 10h.01M12 10h.01M15 10h.01M18 10h.01M8 14h8\"/>","language":"<path d=\"M4 5h9M8.5 3v2M6 5c.5 3 2.5 6 6 7.5M11 5c-.6 3.4-2.8 6.8-6.5 8.5M12 21l4-9 4 9M13.4 18h5.2\"/>","layers":"<path d=\"m12 3.5 9 5-9 5-9-5Z\"/><path d=\"m3 13 9 5 9-5\"/>","leaf":"<path d=\"M5 19c0-8.5 5-13.6 15-15-.9 9.9-6 15-15 15ZM5 19l8-8\"/>","leave":"<path d=\"M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 16l-4-4 4-4M6 12h10\"/>","link":"<path d=\"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1\"/>","list":"<path d=\"M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01\"/>","live":"<circle cx=\"12\" cy=\"12\" r=\"2.5\"/><path d=\"M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14\"/>","lock":"<rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\"/><path d=\"M8 11V8a4 4 0 0 1 8 0v3\"/>","mail":"<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"m4 7 8 6 8-6\"/>","map":"<path d=\"M9 4 3 6v14l6-2 6 2 6-2V4l-6 2Z\"/><path d=\"M9 4v14M15 6v14\"/>","members":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 4.8a3.2 3.2 0 0 1 0 6.4M18 14.8c1.9.6 3 2.4 3 5.2\"/>","menu":"<path d=\"M4 7h16M4 12h16M4 17h16\"/>","mic":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 14 0M12 18v3\"/>","mic-off":"<rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 11.6 5.3M19 11c0 .8-.1 1.6-.4 2.3M12 18v3M4 4l16 16\"/>","minus":"<path d=\"M5 12h14\"/>","moon":"<path d=\"M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z\"/>","more":"<circle cx=\"5\" cy=\"12\" r=\"1.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\"/>","mountain":"<path d=\"M3 20 9.5 8.5l4 6.5 2.5-3.5L21 20Z\"/>","music":"<path d=\"M9 18V6l11-2v12\"/><circle cx=\"6.5\" cy=\"18\" r=\"2.5\"/><circle cx=\"17.5\" cy=\"16\" r=\"2.5\"/>","mute":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"m16 10 4 4M20 10l-4 4\"/>","news":"<path d=\"M4 5h13v14a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2Z\"/><path d=\"M17 9h3v10a2 2 0 0 1-2 2M7.5 9h6M7.5 12.5h6M7.5 16h3.5\"/>","nodes":"<circle cx=\"12\" cy=\"5\" r=\"2.2\"/><circle cx=\"5.5\" cy=\"18\" r=\"2.2\"/><circle cx=\"18.5\" cy=\"18\" r=\"2.2\"/><path d=\"M11 7 6.5 16M13 7l4.5 9M7.7 18h8.6\"/>","open":"<path d=\"M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\"/>","palette":"<path d=\"M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6s.8-1.7 2-1.7H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3Z\"/><circle cx=\"7.5\" cy=\"11\" r=\"1.2\"/><circle cx=\"10\" cy=\"7.3\" r=\"1.2\"/><circle cx=\"14.5\" cy=\"7.3\" r=\"1.2\"/>","pause":"<path d=\"M8 5v14M16 5v14\"/>","pencil":"<path d=\"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"/>","phone":"<path d=\"M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2\"/>","pin":"<path d=\"M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z\"/><circle cx=\"12\" cy=\"10\" r=\"2.4\"/>","plane":"<path d=\"M20.5 3.5 3.5 10.5l6.8 3.2 3.2 6.8Z\"/><path d=\"m10.3 13.7 10.2-10.2\"/>","play":"<path d=\"M8 5v14l11-7Z\"/>","plug":"<path d=\"M9 3v5M15 3v5M6.5 8h11v3a5.5 5.5 0 0 1-11 0ZM12 16.5V21\"/>","plus":"<path d=\"M12 5v14M5 12h14\"/>","reply":"<path d=\"M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6v1\"/>","retry":"<path d=\"M4 12a8 8 0 0 1 13.7-5.7L20 8.5M20 4v4.5h-4.5M20 12a8 8 0 0 1-13.7 5.7L4 15.5M4 20v-4.5h4.5\"/>","robot":"<rect x=\"5\" y=\"8\" width=\"14\" height=\"11\" rx=\"3\"/><path d=\"M12 4v4M9 13h.01M15 13h.01M9.5 16.5h5M3 13v2M21 13v2\"/>","route":"<circle cx=\"6\" cy=\"18\" r=\"2.4\"/><circle cx=\"18\" cy=\"6\" r=\"2.4\"/><path d=\"M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6\"/>","save":"<path d=\"M5 4h11l3 3v13H5Z\"/><path d=\"M8 4v5h7V4M8 20v-6h8v6\"/>","scissors":"<circle cx=\"6\" cy=\"7\" r=\"2.6\"/><circle cx=\"6\" cy=\"17\" r=\"2.6\"/><path d=\"M8.2 8.6 20 18M8.2 15.4 20 6\"/>","search":"<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m20 20-4.2-4.2\"/>","server":"<rect x=\"4\" y=\"4\" width=\"16\" height=\"7\" rx=\"2\"/><rect x=\"4\" y=\"13\" width=\"16\" height=\"7\" rx=\"2\"/><path d=\"M8 7.5h.01M8 16.5h.01\"/>","shield":"<path d=\"M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6Z\"/>","shirt":"<path d=\"M8 3 3 6l2 4 2-1v12h10V9l2 1 2-4-5-3c-.5 1.5-2 2.5-4 2.5S8.5 4.5 8 3Z\"/>","sliders":"<path d=\"M4 7h9.5M18.5 7H20M4 17h3.5M12.5 17H20\"/><circle cx=\"16\" cy=\"7\" r=\"2.3\"/><circle cx=\"10\" cy=\"17\" r=\"2.3\"/>","smile":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01\"/>","snow":"<path d=\"M12 3v18M4.2 7.5l15.6 9M4.2 16.5l15.6-9M9.5 4.5 12 7l2.5-2.5M9.5 19.5 12 17l2.5 2.5\"/>","spark":"<path d=\"M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8ZM19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z\"/>","star":"<path d=\"m12 4 2.4 5 5.4.6-4 3.7 1.1 5.4L12 16l-4.9 2.7 1.1-5.4-4-3.7 5.4-.6Z\"/>","steps":"<path d=\"M9 6h11M9 12h11M9 18h11\"/><path d=\"m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17\"/>","stop":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8.5 8.5 7 7\"/>","store":"<path d=\"M4 9 5.5 4h13L20 9M4 9h16v1.5a2.7 2.7 0 0 1-5.3 0 2.7 2.7 0 0 1-5.4 0A2.7 2.7 0 0 1 4 10.5ZM5 13v7h14v-7M10 20v-4h4v4\"/>","sun":"<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/>","tack":"<path d=\"M9 3h6l-1 5 3 3H7l3-3ZM12 11v10\"/>","tag":"<path d=\"M3 12V4h8l10 10-8 8Z\"/><circle cx=\"7.5\" cy=\"8.5\" r=\"1.3\"/>","target":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/>","tent":"<path d=\"M2.5 20h19M12 5 4 20M12 5l8 15M12 13.5 9 20M12 13.5l3 6.5\"/>","terminal":"<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"m7 9 3 3-3 3M13 15h4\"/>","text":"<path d=\"M5.5 20 11 4h2l5.5 16M7.6 14h8.8\"/>","ticket":"<path d=\"M3 7h18v3a2 2 0 0 0 0 4v3H3v-3a2 2 0 0 0 0-4Z\"/><path d=\"M14 7v10\"/>","train":"<rect x=\"6\" y=\"3\" width=\"12\" height=\"13\" rx=\"3\"/><path d=\"M6 10h12M9 13h.01M15 13h.01M8.5 21l2-3.5M15.5 21l-2-3.5\"/>","trash":"<path d=\"M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3\"/>","tree":"<path d=\"M12 3 6.5 10.5h3L5 17h14l-4.5-6.5h3ZM12 17v4\"/>","trophy":"<path d=\"M8 4h8v5a4 4 0 0 1-8 0ZM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 20h8\"/>","up":"<path d=\"m6 15 6-6 6 6\"/>","upload":"<path d=\"M12 20V9M7 14l5-5 5 5M5 4h14\"/>","user":"<circle cx=\"12\" cy=\"8\" r=\"3.5\"/><path d=\"M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6\"/>","user-check":"<circle cx=\"9\" cy=\"8\" r=\"3.2\"/><path d=\"M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 11l2 2 4-4\"/>","video":"<rect x=\"3\" y=\"6\" width=\"13\" height=\"12\" rx=\"2\"/><path d=\"m16 10 5-3v10l-5-3\"/>","volume":"<path d=\"M4 9.5h3.5L12 6v12l-4.5-3.5H4Z\"/><path d=\"M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10\"/>","wallet":"<path d=\"M4 7.5A2.5 2.5 0 0 1 6.5 5H18v4M4 7.5V18a2 2 0 0 0 2 2h14V9H6.5A2.5 2.5 0 0 1 4 7.5Z\"/><path d=\"M16 14.5h.01\"/>","water":"<path d=\"M2.5 9c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2M2.5 15c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2\"/>","wave":"<path d=\"M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2\"/><path d=\"M5.5 11.5A7 7 0 0 1 18 8.6c-2.3-.5-4.2.4-4.8 2.4\"/>","wifi":"<path d=\"M2.5 9a14 14 0 0 1 19 0M5.5 12.5a9.5 9.5 0 0 1 13 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01\"/>","wrench":"<path d=\"M14.5 4a5 5 0 0 0-4.4 7L4 17.1V20h2.9l6.1-6.1a5 5 0 0 0 7-4.4l-3 1.1-2.4-2.4 1.1-3Z\"/>","x-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m9 9 6 6M15 9l-6 6\"/>","zap":"<path d=\"M13 3 5 13h6l-1 8 8-10h-6Z\"/>"};
const FA = {"angle-down":"down","angle-right":"chevron","arrow-down":"arrow-down","arrow-left":"arrow-left","arrow-right":"arrow-right","arrow-right-from-bracket":"leave","arrow-right-to-bracket":"enter","arrow-rotate-right":"retry","arrow-up":"arrow-up","arrow-up-right-from-square":"open","arrows-rotate":"retry","ban":"ban","bars":"menu","bell":"bell","bolt":"zap","bolt-lightning":"zap","book":"book","book-open":"book","box-archive":"archive","brain":"brain","bridge":"bridge","broadcast-tower":"live","bullseye":"target","calendar":"calendar","calendar-days":"calendar","camera":"camera","campground":"tent","cart":"cart","cart-shopping":"cart","chart-bar":"chart","chart-line":"chart","chart-simple":"chart","check":"check","check-circle":"check-circle","chevron-down":"down","chevron-left":"back","chevron-right":"chevron","chevron-up":"up","circle":"circle","circle-check":"check-circle","circle-exclamation":"alert-circle","circle-info":"info","circle-nodes":"nodes","circle-notch":"busy","circle-play":"play","circle-question":"help","circle-xmark":"x-circle","city":"city","clipboard":"clipboard","clipboard-list":"clipboard","clock":"clock","clock-rotate-left":"history","cloud":"cloud","cloud-arrow-up":"upload","cloud-rain":"cloud","cloud-showers-heavy":"cloud","cloud-sun":"cloud","code":"code","code-branch":"branch","code-fork":"branch","coin":"coin","coins":"coin","comment":"chat","comment-dots":"chat","comments":"chat","compress":"compress","copy":"copy","crown":"crown","cut":"scissors","database":"database","desktop":"computer","diagram-project":"nodes","display":"computer","dollar-sign":"dollar","download":"download","droplet":"drop","earth-americas":"globe","earth-europe":"globe","ellipsis":"more","ellipsis-vertical":"more","envelope":"mail","expand":"expand","external-link":"open","eye":"eye","eye-slash":"eye-off","face-smile":"smile","file":"file","file-audio":"file","file-image":"image","file-lines":"file","file-pdf":"file","film":"film","filter":"filter","fire":"fire","flag":"flag","floppy-disk":"save","folder":"folder","font":"text","gamepad":"game","gauge":"gauge","gauge-high":"gauge","gavel":"gavel","gear":"gear","gears":"gear","gift":"gift","globe":"globe","grid":"grid","hand":"hand","hand-holding-dollar":"coin","hand-holding-heart":"care","hand-pointer":"hand","hashtag":"hash","heart":"heart","house":"home","house-chimney":"home","id-card":"id","image":"image","images":"image","key":"key","keyboard":"keyboard","language":"language","laptop":"computer","layer-group":"layers","leaf":"leaf","lightbulb":"idea","link":"link","list":"list","list-check":"steps","list-ul":"list","location-dot":"pin","location-pin":"pin","lock":"lock","magnifying-glass":"search","map":"map","map-pin":"pin","message":"chat","microchip":"chip","microphone":"mic","microphone-slash":"mic-off","minus":"minus","mobile-screen":"phone","moon":"moon","mountain":"mountain","mountain-sun":"mountain","music":"music","network-wired":"nodes","newspaper":"news","palette":"palette","paper-plane":"plane","paste":"clipboard","pause":"pause","pen":"pencil","pen-nib":"pencil","pen-to-square":"pencil","pencil":"pencil","people-group":"members","person-swimming":"water","phone":"phone","photo-film":"film","play":"play","plug":"plug","plus":"plus","question":"help","reply":"reply","right-from-bracket":"leave","right-to-bracket":"enter","robot":"robot","rotate":"retry","rotate-left":"retry","rotate-right":"retry","route":"route","satellite-dish":"dish","scissors":"scissors","screwdriver-wrench":"wrench","search":"search","server":"server","share-nodes":"nodes","shield":"shield","shield-halved":"shield","shirt":"shirt","signal":"wifi","sitemap":"nodes","sliders":"sliders","sliders-h":"sliders","snowflake":"snow","sparkles":"spark","spinner":"busy","star":"star","star-of-life":"spark","stop":"stop","store":"store","sun":"sun","table-cells":"grid","tag":"tag","tags":"tag","terminal":"terminal","text-height":"text","thumbtack":"tack","ticket":"ticket","times":"close","tower-broadcast":"live","tower-cell":"live","train":"train","trash":"trash","trash-can":"trash","tree":"tree","triangle-exclamation":"alert","trophy":"trophy","unlock":"lock","up-right-and-down-left-from-center":"expand","up-right-from-square":"open","upload":"upload","user":"user","user-astronaut":"user","user-check":"user-check","user-group":"members","user-plus":"invite","user-secret":"user","user-shield":"shield","users":"members","utensils":"food","video":"video","video-slash":"video","volume-high":"volume","volume-low":"volume","volume-off":"mute","volume-xmark":"mute","wallet":"wallet","wand-magic":"spark","wand-magic-sparkles":"spark","water":"water","wave-square":"wave","wifi":"wifi","wrench":"wrench","xmark":"close","xmark-circle":"x-circle"};
const FA = {"angle-down":"down","angle-right":"chevron","arrow-down":"arrow-down","arrow-left":"arrow-left","arrow-right":"arrow-right","arrow-right-from-bracket":"leave","arrow-right-to-bracket":"enter","arrow-rotate-right":"retry","arrow-up":"arrow-up","arrow-up-right-from-square":"open","arrows-rotate":"retry","ban":"ban","bars":"menu","bell":"bell","bolt":"zap","bolt-lightning":"zap","book":"book","book-open":"book","box-archive":"archive","brain":"brain","bridge":"bridge","broadcast-tower":"live","bullseye":"target","calendar":"calendar","calendar-days":"calendar","camera":"camera","campground":"tent","cart":"cart","cart-shopping":"cart","chart-bar":"chart","chart-line":"chart","chart-simple":"chart","check":"check","check-circle":"check-circle","chevron-down":"down","chevron-left":"back","chevron-right":"chevron","chevron-up":"up","circle":"circle","circle-check":"check-circle","circle-exclamation":"alert-circle","circle-info":"info","circle-nodes":"nodes","circle-notch":"busy","circle-play":"play","circle-question":"help","circle-xmark":"x-circle","city":"city","clipboard":"clipboard","clipboard-list":"clipboard","clock":"clock","clock-rotate-left":"history","cloud":"cloud","cloud-arrow-up":"upload","cloud-rain":"cloud","cloud-showers-heavy":"cloud","cloud-sun":"cloud","code":"code","code-branch":"branch","code-fork":"branch","coin":"coin","coins":"coin","comment":"chat","comment-dots":"chat","comments":"chat","compress":"compress","copy":"copy","crown":"crown","cut":"scissors","database":"database","desktop":"computer","diagram-project":"nodes","display":"computer","dollar-sign":"dollar","download":"download","droplet":"drop","earth-americas":"globe","earth-europe":"globe","ellipsis":"more","ellipsis-vertical":"more","envelope":"mail","expand":"expand","external-link":"open","eye":"eye","eye-slash":"eye-off","face-smile":"smile","file":"file","file-audio":"file","file-image":"image","file-lines":"file","file-pdf":"file","film":"film","filter":"filter","fire":"fire","flag":"flag","floppy-disk":"save","folder":"folder","font":"text","gamepad":"game","gauge":"gauge","gauge-high":"gauge","gavel":"gavel","gear":"gear","gears":"gear","gift":"gift","globe":"globe","grid":"grid","hand":"hand","hand-holding-dollar":"coin","hand-holding-heart":"care","hand-pointer":"hand","hashtag":"hash","heart":"heart","house":"home","house-chimney":"home","id-card":"id","image":"image","images":"image","key":"key","keyboard":"keyboard","language":"language","laptop":"computer","layer-group":"layers","leaf":"leaf","lightbulb":"idea","link":"link","list":"list","list-check":"steps","list-ul":"list","location-dot":"pin","location-pin":"pin","lock":"lock","magnifying-glass":"search","map":"map","map-pin":"pin","message":"chat","microchip":"chip","microphone":"mic","microphone-slash":"mic-off","minus":"minus","mobile-screen":"phone","moon":"moon","mountain":"mountain","mountain-sun":"mountain","music":"music","network-wired":"nodes","newspaper":"news","palette":"palette","paper-plane":"plane","paste":"clipboard","pause":"pause","pen":"pencil","pen-nib":"pencil","pen-to-square":"pencil","pencil":"pencil","people-group":"members","person-swimming":"water","phone":"phone","photo-film":"film","play":"play","plug":"plug","plus":"plus","question":"help","reply":"reply","right-from-bracket":"leave","right-to-bracket":"enter","robot":"robot","rotate":"retry","rotate-left":"retry","rotate-right":"retry","route":"route","satellite-dish":"dish","scissors":"scissors","screwdriver-wrench":"wrench","search":"search","server":"server","share-nodes":"nodes","shield":"shield","shield-halved":"shield","shirt":"shirt","signal":"wifi","sitemap":"nodes","sliders":"sliders","sliders-h":"sliders","snowflake":"snow","sparkles":"spark","spinner":"busy","star":"star","star-of-life":"spark","stop":"stop","store":"store","sun":"sun","table-cells":"grid","tag":"tag","tags":"tag","terminal":"terminal","text-height":"text","thumbtack":"tack","ticket":"ticket","times":"close","tower-broadcast":"live","tower-cell":"live","train":"train","trash":"trash","trash-can":"trash","tree":"tree","triangle-exclamation":"alert","trophy":"trophy","unlock":"lock","up-right-and-down-left-from-center":"expand","up-right-from-square":"open","upload":"upload","user":"user","user-astronaut":"user","user-check":"user-check","user-group":"members","user-plus":"invite","user-secret":"user","user-shield":"shield","users":"members","utensils":"food","video":"video","video-slash":"video","volume-high":"volume","volume-low":"volume","volume-off":"mute","volume-xmark":"mute","wallet":"wallet","wand-magic":"spark","wand-magic-sparkles":"spark","water":"water","wave-square":"wave","wifi":"wifi","wrench":"wrench","xmark":"close","xmark-circle":"x-circle"};
- // Font Awesome's style and size classes, and its animations: they say how, not what.
- const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity)$/;
+ // Font Awesome's style, size, layout and animation classes: they say how, not what.
+ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;
const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';
const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';
- const CSS = '.ovg::before,.ovg::after{content:none!important;display:none!important}'
- + '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}'
- + '.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'
- + '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;overflow:visible}'
- + '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}'
- + '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}'
- + '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}'
- + '@media (prefers-reduced-motion:reduce){.ovg[data-ovg=busy] circle{animation:none}}html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none}';
+ const CSS = ".ovg::before,.ovg::after{content:none!important;display:none!important}.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:var(--ovg-stroke,2);stroke-linecap:round;stroke-linejoin:round;overflow:visible}.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}.ovg.fa-1x{font-size:1em}.ovg.fa-2x{font-size:2em}.ovg.fa-3x{font-size:3em}.ovg.fa-4x{font-size:4em}.ovg.fa-5x{font-size:5em}.ovg.fa-6x{font-size:6em}.ovg.fa-7x{font-size:7em}.ovg.fa-8x{font-size:8em}.ovg.fa-9x{font-size:9em}.ovg.fa-10x{font-size:10em}.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}.ovg.fa-beat{animation:ovgBeat var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-bounce{animation:ovgBounce var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-fade{animation:ovgFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-beat-fade{animation:ovgBeatFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-flip{animation:ovgFlip var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-shake{animation:ovgShake var(--fa-animation-duration,1s) var(--fa-animation-timing,linear) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}html[data-ov-motion=reduced] .ovg,html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none!important}";
const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };
const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };
const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };
const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };
+ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).
+ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;
+ function transformOf(spec) {
+ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;
+ for (const p of String(spec || '').toLowerCase().split(/\s+/)) {
+ const m = TF.exec(p);
+ if (!m) continue;
+ any = true;
+ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }
+ const n = Number(m[2]);
+ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;
+ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;
+ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;
+ }
+ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';
+ }
function draw(el) {
function draw(el) {
if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;
if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;
const fa = nameOf(el);
const fa = nameOf(el);
const g = fa && fromFa(fa);
const g = fa && fromFa(fa);
- if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; el.textContent = ''; } return; }
- if (el.dataset.ovgFa === fa) return;
+ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }
+ const tf = el.getAttribute('data-fa-transform') || '';
+ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself forever
if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon
if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon
el.classList.add('ovg');
el.classList.add('ovg');
el.dataset.ovg = g; el.dataset.ovgFa = fa;
el.dataset.ovg = g; el.dataset.ovgFa = fa;
- if (!el.hasAttribute('aria-label') && !el.hasAttribute('title')) el.setAttribute('aria-hidden', 'true');
- el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + (g === 'busy' ? BUSY : G[g]) + '</svg>';
+ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;
+ const title = el.getAttribute('title');
+ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }
+ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');
+ const t = transformOf(tf);
+ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g]) + '</svg>';
}
}
const SEL = 'i[class*="fa-"],span[class*="fa-"]';
const SEL = 'i[class*="fa-"],span[class*="fa-"]';
function swap(root) {
function swap(root) {
@@ -56,7 +76,7 @@
@@ -56,7 +76,7 @@
if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }
if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }
r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });
r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });
}
}
- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
+ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });
} catch (e) { /* no observer: what was there is swapped */ }
} catch (e) { /* no observer: what was there is swapped */ }
}
}
window.OVGlyphs = { swap, fromFa };
window.OVGlyphs = { swap, fromFa };
new file mode 100644
new file mode 100644
@@ -0,0 +1,158 @@
@@ -0,0 +1,158 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+<!-- The OpenVibe glyphs (openvibe-shared, MIT): <svg><use href="ov-glyphs.svg#ovg-eye"/></svg>. Generated by scripts/build-glyphs.js. -->
+<symbol id="ovg-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 2.8 19.5h18.4Z"/><path d="M12 10v4M12 17h.01"/></symbol>
+<symbol id="ovg-alert-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v5.5M12 16.5h.01"/></symbol>
+<symbol id="ovg-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="5" rx="1"/><path d="M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4"/></symbol>
+<symbol id="ovg-arrow-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16M6 14l6 6 6-6"/></symbol>
+<symbol id="ovg-arrow-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12H4M10 6l-6 6 6 6"/></symbol>
+<symbol id="ovg-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16M14 6l6 6-6 6"/></symbol>
+<symbol id="ovg-arrow-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V4M6 10l6-6 6 6"/></symbol>
+<symbol id="ovg-back" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></symbol>
+<symbol id="ovg-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m6 6 12 12"/></symbol>
+<symbol id="ovg-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15ZM10 20a2 2 0 0 0 4 0"/></symbol>
+<symbol id="ovg-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Z"/><path d="M4 19V5"/></symbol>
+<symbol id="ovg-brain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4a3 3 0 0 0-3 3 3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 6 1.2V5.5A2 2 0 0 0 9 4ZM15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-6 1.2"/></symbol>
+<symbol id="ovg-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5" r="2"/><circle cx="6" cy="19" r="2"/><circle cx="18" cy="7" r="2"/><path d="M6 7v10M18 9c0 5.5-6.5 4-11 8.5"/></symbol>
+<symbol id="ovg-bridge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15.5h20M4.5 15.5V10M19.5 15.5V10M2 9.5c4 0 6-3 10-3s6 3 10 3M8.5 15.5V12M15.5 15.5V12M12 15.5V8"/></symbol>
+<symbol id="ovg-busy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3.4"/></symbol>
+<symbol id="ovg-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M9 3v4M15 3v4"/></symbol>
+<symbol id="ovg-camera" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z"/><circle cx="12" cy="13.3" r="3.4"/></symbol>
+<symbol id="ovg-care" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15.5h3l4 3h5.5a2 2 0 0 0 0-4H12M7 15.5l2.2-2h3.3"/><path d="M15.5 10s-3.2-2-3.2-4.4a1.9 1.9 0 0 1 3.2-1.3 1.9 1.9 0 0 1 3.2 1.3c0 2.4-3.2 4.4-3.2 4.4Z"/></symbol>
+<symbol id="ovg-cart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h2l2.4 11h11L21 7H6.2"/><circle cx="9" cy="19.5" r="1.3"/><circle cx="17" cy="19.5" r="1.3"/></symbol>
+<symbol id="ovg-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v16h16"/><path d="m7.5 14.5 3.5-4 3 2.5 5-6"/></symbol>
+<symbol id="ovg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4Z"/></symbol>
+<symbol id="ovg-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
+<symbol id="ovg-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m8 12.4 2.8 2.8L16 9.6"/></symbol>
+<symbol id="ovg-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></symbol>
+<symbol id="ovg-chip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="10" rx="1.5"/><path d="M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4"/></symbol>
+<symbol id="ovg-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/></symbol>
+<symbol id="ovg-city" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M5 21V9h5v12M10 21V4h6v17M16 21v-8h3.5v8M7.5 12h.01M7.5 15.5h.01M13 7.5h.01M13 11h.01M13 14.5h.01"/></symbol>
+<symbol id="ovg-clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="5" width="13" height="16" rx="2"/><path d="M9 5V3.5h6V5M9 11h6M9 15h4"/></symbol>
+<symbol id="ovg-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
+<symbol id="ovg-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
+<symbol id="ovg-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18h10a4 4 0 0 0 .5-8A6 6 0 0 0 6 9.5 4.3 4.3 0 0 0 7 18Z"/></symbol>
+<symbol id="ovg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14"/></symbol>
+<symbol id="ovg-coin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11"/></symbol>
+<symbol id="ovg-compress" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></symbol>
+<symbol id="ovg-computer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></symbol>
+<symbol id="ovg-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></symbol>
+<symbol id="ovg-crown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5 7.5 12.5 12 5.5l4.5 7 4.5-4L19 19H5Z"/></symbol>
+<symbol id="ovg-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6" rx="7" ry="3"/><path d="M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3"/></symbol>
+<symbol id="ovg-dish" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9.5A9.5 9.5 0 0 0 14.5 19Z"/><path d="m9.5 14.5 4.5-4.5M14.5 4.5a5 5 0 0 1 5 5M14.5 8a1.5 1.5 0 0 1 1.5 1.5"/></symbol>
+<symbol id="ovg-dollar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7.5C15.3 6 13.8 5 12 5 9.5 5 8 6.3 8 8s1.7 2.6 4 3 4 1.4 4 3.3-1.6 3.2-4 3.2c-1.9 0-3.4-1-4-2.5M12 3v18"/></symbol>
+<symbol id="ovg-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
+<symbol id="ovg-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></symbol>
+<symbol id="ovg-drop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/></symbol>
+<symbol id="ovg-enter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 8l4 4-4 4M14 12H4"/></symbol>
+<symbol id="ovg-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></symbol>
+<symbol id="ovg-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="3"/></symbol>
+<symbol id="ovg-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12s3.5-6.5 9-6.5c1.6 0 3 .5 4.3 1.2M21 12s-3.5 6.5-9 6.5c-1.6 0-3-.5-4.3-1.2"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M4 4l16 16"/></symbol>
+<symbol id="ovg-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14H6Z"/><path d="M14 3v4h4"/></symbol>
+<symbol id="ovg-film" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7.5 4v16M16.5 4v16M3 9h4.5M3 15h4.5M16.5 9H21M16.5 15H21"/></symbol>
+<symbol id="ovg-filter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M7 12h10M10 18h4"/></symbol>
+<symbol id="ovg-fire" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21a6 6 0 0 0 6-6c0-3.8-2.6-6-3.7-9-1.8 2-2.3 4-2.3 4S10.8 8 9.8 6.2C7.6 8.7 6 11.6 6 15a6 6 0 0 0 6 6Z"/></symbol>
+<symbol id="ovg-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></symbol>
+<symbol id="ovg-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></symbol>
+<symbol id="ovg-food" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3v7M5 3v4.5a2 2 0 0 0 4 0V3M7 10v11M17 21V3c-2 1.2-3.2 3.6-3.2 7v3H17"/></symbol>
+<symbol id="ovg-forward" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 5 7 7-7 7"/></symbol>
+<symbol id="ovg-game" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="7" width="19" height="11" rx="5"/><path d="M7 11v3M5.5 12.5h3M15.5 12h.01M18 13.5h.01"/></symbol>
+<symbol id="ovg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 18a8 8 0 1 1 16 0"/><path d="m12 14.5 4-5.5"/><circle cx="12" cy="14.5" r="1.3"/></symbol>
+<symbol id="ovg-gavel" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 4.5 5.5 5.5M10.5 8l5.5 5.5M12.3 6.3l-6 6 5.4 5.4 6-6M3 21l6.3-6.3M14 21h7"/></symbol>
+<symbol id="ovg-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></symbol>
+<symbol id="ovg-gift" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="9" width="16" height="11" rx="1"/><path d="M3 9h18M12 9v11M12 9c-1.5-3-5-4-5-1.5S12 9 12 9s5-1 5-3.5S13.5 6 12 9"/></symbol>
+<symbol id="ovg-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18"/></symbol>
+<symbol id="ovg-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></symbol>
+<symbol id="ovg-hand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V5.5a1.5 1.5 0 0 1 3 0V11M11 10V4.5a1.5 1.5 0 0 1 3 0V11M14 10.5V6a1.5 1.5 0 0 1 3 0v7c0 4-2.5 7-6.5 7-2.6 0-4.2-1.3-5.7-3.6L3 13.6a1.5 1.5 0 0 1 2.4-1.8L8 15"/></symbol>
+<symbol id="ovg-hash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17"/></symbol>
+<symbol id="ovg-heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></symbol>
+<symbol id="ovg-help" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01"/></symbol>
+<symbol id="ovg-history" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 1 0 2.4-5.7L4 8.6M4 4.5v4.1h4.1"/><path d="M12 8v4.2l3 1.8"/></symbol>
+<symbol id="ovg-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11 12 4l8 7v8a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1Z"/></symbol>
+<symbol id="ovg-id" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.5-1.5 1.7-2.3 3-2.3s2.5.8 3 2.3M15 10h3M15 13.5h3"/></symbol>
+<symbol id="ovg-idea" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 21h4M8.6 14.6A6 6 0 1 1 15.4 14.6c-.7.6-1.4 1.5-1.4 2.4h-4c0-.9-.7-1.8-1.4-2.4Z"/></symbol>
+<symbol id="ovg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></symbol>
+<symbol id="ovg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/></symbol>
+<symbol id="ovg-invite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M19 8v6M16 11h6"/></symbol>
+<symbol id="ovg-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m10.8 12.2 8.7-8.7M16 7l3 3M14 9l2 2"/></symbol>
+<symbol id="ovg-keyboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="6" width="19" height="12" rx="2"/><path d="M6 10h.01M9 10h.01M12 10h.01M15 10h.01M18 10h.01M8 14h8"/></symbol>
+<symbol id="ovg-language" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h9M8.5 3v2M6 5c.5 3 2.5 6 6 7.5M11 5c-.6 3.4-2.8 6.8-6.5 8.5M12 21l4-9 4 9M13.4 18h5.2"/></symbol>
+<symbol id="ovg-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3.5 9 5-9 5-9-5Z"/><path d="m3 13 9 5 9-5"/></symbol>
+<symbol id="ovg-leaf" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19c0-8.5 5-13.6 15-15-.9 9.9-6 15-15 15ZM5 19l8-8"/></symbol>
+<symbol id="ovg-leave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4M10 16l-4-4 4-4M6 12h10"/></symbol>
+<symbol id="ovg-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></symbol>
+<symbol id="ovg-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></symbol>
+<symbol id="ovg-live" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.5"/><path d="M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14"/></symbol>
+<symbol id="ovg-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></symbol>
+<symbol id="ovg-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>
+<symbol id="ovg-map" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4 3 6v14l6-2 6 2 6-2V4l-6 2Z"/><path d="M9 4v14M15 6v14"/></symbol>
+<symbol id="ovg-members" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 4.8a3.2 3.2 0 0 1 0 6.4M18 14.8c1.9.6 3 2.4 3 5.2"/></symbol>
+<symbol id="ovg-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
+<symbol id="ovg-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></symbol>
+<symbol id="ovg-mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 11.6 5.3M19 11c0 .8-.1 1.6-.4 2.3M12 18v3M4 4l16 16"/></symbol>
+<symbol id="ovg-minus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></symbol>
+<symbol id="ovg-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></symbol>
+<symbol id="ovg-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></symbol>
+<symbol id="ovg-mountain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 20 9.5 8.5l4 6.5 2.5-3.5L21 20Z"/></symbol>
+<symbol id="ovg-music" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V6l11-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="17.5" cy="16" r="2.5"/></symbol>
+<symbol id="ovg-mute" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5h3.5L12 6v12l-4.5-3.5H4Z"/><path d="m16 10 4 4M20 10l-4 4"/></symbol>
+<symbol id="ovg-news" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h13v14a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2Z"/><path d="M17 9h3v10a2 2 0 0 1-2 2M7.5 9h6M7.5 12.5h6M7.5 16h3.5"/></symbol>
+<symbol id="ovg-nodes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="2.2"/><circle cx="5.5" cy="18" r="2.2"/><circle cx="18.5" cy="18" r="2.2"/><path d="M11 7 6.5 16M13 7l4.5 9M7.7 18h8.6"/></symbol>
+<symbol id="ovg-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></symbol>
+<symbol id="ovg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6s.8-1.7 2-1.7H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3Z"/><circle cx="7.5" cy="11" r="1.2"/><circle cx="10" cy="7.3" r="1.2"/><circle cx="14.5" cy="7.3" r="1.2"/></symbol>
+<symbol id="ovg-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5v14M16 5v14"/></symbol>
+<symbol id="ovg-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
+<symbol id="ovg-phone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></symbol>
+<symbol id="ovg-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z"/><circle cx="12" cy="10" r="2.4"/></symbol>
+<symbol id="ovg-plane" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 3.5 3.5 10.5l6.8 3.2 3.2 6.8Z"/><path d="m10.3 13.7 10.2-10.2"/></symbol>
+<symbol id="ovg-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5v14l11-7Z"/></symbol>
+<symbol id="ovg-plug" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3v5M15 3v5M6.5 8h11v3a5.5 5.5 0 0 1-11 0ZM12 16.5V21"/></symbol>
+<symbol id="ovg-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
+<symbol id="ovg-reply" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6v1"/></symbol>
+<symbol id="ovg-retry" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 0 1 13.7-5.7L20 8.5M20 4v4.5h-4.5M20 12a8 8 0 0 1-13.7 5.7L4 15.5M4 20v-4.5h4.5"/></symbol>
+<symbol id="ovg-robot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="8" width="14" height="11" rx="3"/><path d="M12 4v4M9 13h.01M15 13h.01M9.5 16.5h5M3 13v2M21 13v2"/></symbol>
+<symbol id="ovg-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="18" r="2.4"/><circle cx="18" cy="6" r="2.4"/><path d="M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6"/></symbol>
+<symbol id="ovg-save" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h11l3 3v13H5Z"/><path d="M8 4v5h7V4M8 20v-6h8v6"/></symbol>
+<symbol id="ovg-scissors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="7" r="2.6"/><circle cx="6" cy="17" r="2.6"/><path d="M8.2 8.6 20 18M8.2 15.4 20 6"/></symbol>
+<symbol id="ovg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></symbol>
+<symbol id="ovg-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></symbol>
+<symbol id="ovg-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="7" rx="2"/><rect x="4" y="13" width="16" height="7" rx="2"/><path d="M8 7.5h.01M8 16.5h.01"/></symbol>
+<symbol id="ovg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6Z"/></symbol>
+<symbol id="ovg-shirt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 3 6l2 4 2-1v12h10V9l2 1 2-4-5-3c-.5 1.5-2 2.5-4 2.5S8.5 4.5 8 3Z"/></symbol>
+<symbol id="ovg-shoe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17v-5.5l4-.5 3-4 3.2 1.4-.9 2.6 4 2.2 4.7 1a2 2 0 0 1 1.5 1.9V17Z"/><path d="M2.5 20h19"/></symbol>
+<symbol id="ovg-sidebar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M9.5 4.5v15"/></symbol>
+<symbol id="ovg-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h9.5M18.5 7H20M4 17h3.5M12.5 17H20"/><circle cx="16" cy="7" r="2.3"/><circle cx="10" cy="17" r="2.3"/></symbol>
+<symbol id="ovg-smile" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01"/></symbol>
+<symbol id="ovg-snow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M4.2 7.5l15.6 9M4.2 16.5l15.6-9M9.5 4.5 12 7l2.5-2.5M9.5 19.5 12 17l2.5 2.5"/></symbol>
+<symbol id="ovg-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8ZM19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8Z"/></symbol>
+<symbol id="ovg-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 4 2.4 5 5.4.6-4 3.7 1.1 5.4L12 16l-4.9 2.7 1.1-5.4-4-3.7 5.4-.6Z"/></symbol>
+<symbol id="ovg-steps" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11M9 12h11M9 18h11"/><path d="m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17"/></symbol>
+<symbol id="ovg-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m8.5 8.5 7 7"/></symbol>
+<symbol id="ovg-store" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9 5.5 4h13L20 9M4 9h16v1.5a2.7 2.7 0 0 1-5.3 0 2.7 2.7 0 0 1-5.4 0A2.7 2.7 0 0 1 4 10.5ZM5 13v7h14v-7M10 20v-4h4v4"/></symbol>
+<symbol id="ovg-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
+<symbol id="ovg-swap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h14l-3.5-3.5M20 16H6l3.5 3.5"/></symbol>
+<symbol id="ovg-tack" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6l-1 5 3 3H7l3-3ZM12 11v10"/></symbol>
+<symbol id="ovg-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12V4h8l10 10-8 8Z"/><circle cx="7.5" cy="8.5" r="1.3"/></symbol>
+<symbol id="ovg-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5"/></symbol>
+<symbol id="ovg-task" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="m8 12 3 3 5-6"/></symbol>
+<symbol id="ovg-tent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 20h19M12 5 4 20M12 5l8 15M12 13.5 9 20M12 13.5l3 6.5"/></symbol>
+<symbol id="ovg-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="m7 9 3 3-3 3M13 15h4"/></symbol>
+<symbol id="ovg-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 20 11 4h2l5.5 16M7.6 14h8.8"/></symbol>
+<symbol id="ovg-ticket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18v3a2 2 0 0 0 0 4v3H3v-3a2 2 0 0 0 0-4Z"/><path d="M14 7v10"/></symbol>
+<symbol id="ovg-train" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="3" width="12" height="13" rx="3"/><path d="M6 10h12M9 13h.01M15 13h.01M8.5 21l2-3.5M15.5 21l-2-3.5"/></symbol>
+<symbol id="ovg-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></symbol>
+<symbol id="ovg-tree" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 6.5 10.5h3L5 17h14l-4.5-6.5h3ZM12 17v4"/></symbol>
+<symbol id="ovg-trophy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 4h8v5a4 4 0 0 1-8 0ZM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 20h8"/></symbol>
+<symbol id="ovg-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></symbol>
+<symbol id="ovg-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></symbol>
+<symbol id="ovg-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6"/></symbol>
+<symbol id="ovg-user-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5M16 11l2 2 4-4"/></symbol>
+<symbol id="ovg-video" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 10 5-3v10l-5-3"/></symbol>
+<symbol id="ovg-volume" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5h3.5L12 6v12l-4.5-3.5H4Z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10"/></symbol>
+<symbol id="ovg-wallet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7.5A2.5 2.5 0 0 1 6.5 5H18v4M4 7.5V18a2 2 0 0 0 2 2h14V9H6.5A2.5 2.5 0 0 1 4 7.5Z"/><path d="M16 14.5h.01"/></symbol>
+<symbol id="ovg-water" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2M2.5 15c2.4 0 2.4-2 4.8-2s2.4 2 4.7 2 2.4-2 4.8-2 2.4 2 4.7 2"/></symbol>
+<symbol id="ovg-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"/><path d="M5.5 11.5A7 7 0 0 1 18 8.6c-2.3-.5-4.2.4-4.8 2.4"/></symbol>
+<symbol id="ovg-wifi" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 9a14 14 0 0 1 19 0M5.5 12.5a9.5 9.5 0 0 1 13 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01"/></symbol>
+<symbol id="ovg-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4a5 5 0 0 0-4.4 7L4 17.1V20h2.9l6.1-6.1a5 5 0 0 0 7-4.4l-3 1.1-2.4-2.4 1.1-3Z"/></symbol>
+<symbol id="ovg-x-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6M15 9l-6 6"/></symbol>
+<symbol id="ovg-zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3 5 13h6l-1 8 8-10h-6Z"/></symbol>
+</svg>
new file mode 100644
new file mode 100644
Binary files /dev/null and b/ov-glyphs.woff2 differ
Binary files /dev/null and b/ov-glyphs.woff2 differ
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.61.0",
+ "version": "4.62.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -97,6 +97,9 @@
@@ -97,6 +97,9 @@
"*.js",
"*.js",
"*.css",
"*.css",
"*.woff2",
"*.woff2",
+ "*.svg",
+ "glyph-codepoints.json",
+ "glyph-fonts/",
"OFL-Nunito.txt",
"OFL-Nunito.txt",
"analytics/",
"analytics/",
"docs/schemas/",
"docs/schemas/",
new file mode 100644
new file mode 100644
@@ -0,0 +1,168 @@
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+"""
+The OpenVibe glyphs as fonts (plan L32 I5, 4.62.0), built from glyphs.js (the one registry):
+
+ ov-glyphs.woff2 the web font, served at /shared/ov-glyphs.woff2
+ glyph-fonts/ov-glyphs.ttf TrueType, for desktop apps and design tools
+ glyph-fonts/ov-glyphs.otf OpenType (CFF)
+ ov-glyphs-font.css @font-face plus .ovi .ovi-<name> classes
+ ov-glyphs-fa.css Font Awesome's class names on the font, for a site that drops Font Awesome's own
+ glyph-codepoints.json each glyph's Private Use Area code point: given once, never changed or reused
+ glyph-fonts/manifest.json what the fonts were built from (test/glyph-fonts.test.js checks it is current)
+
+A glyph is drawn in strokes; a font holds outlines. Each glyph's strokes (2 units, round caps and joins) are expanded
+to outlines by picosvg and merged into one shape by skia-pathops, so the font draws exactly what the SVG does.
+
+Needs Python 3 with picosvg, fonttools and brotli (picosvg brings skia-pathops):
+ python3 -m venv .venv && .venv/bin/pip install picosvg fonttools brotli
+ .venv/bin/python scripts/build-glyph-fonts.py
+"""
+import hashlib
+import json
+import os
+import subprocess
+import sys
+
+import pathops
+from fontTools.fontBuilder import FontBuilder
+from fontTools.pens.cu2quPen import Cu2QuPen
+from fontTools.pens.t2CharStringPen import T2CharStringPen
+from fontTools.pens.transformPen import TransformPen
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+from fontTools.svgLib.path import parse_path
+from fontTools.ttLib import TTFont
+from picosvg.svg import SVG
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+# A fixed build date in the fonts' head table, so building twice gives the same bytes (fontTools reads it).
+os.environ.setdefault('SOURCE_DATE_EPOCH', '1793404800')
+FAMILY = 'OV Glyphs'
+UPM = 1024
+DESCENT = 128 # the icon sits like Font Awesome's: an eighth below the baseline
+SCALE = UPM / 24
+FIRST = 0xE001 # the Private Use Area, from U+E001
+VERSION = json.load(open(os.path.join(ROOT, 'package.json')))['version']
+
+
+def registry():
+ """Every glyph's drawing, and the Font Awesome names each stands for, from glyphs.js."""
+ js = ("const g=require('./glyphs');const fa={};for(const[k,v]of Object.entries(g.FA))(fa[v]=fa[v]||[]).push(k);"
+ "console.log(JSON.stringify({names:g.NAMES,paths:Object.fromEntries(g.NAMES.map(n=>[n,g.path(n)])),fa}))")
+ return json.loads(subprocess.check_output(['node', '-e', js], cwd=ROOT))
+
+
+def codepoints(names):
+ """glyph-codepoints.json: a new glyph gets the next free code point; a code point once given is never reused."""
+ p = os.path.join(ROOT, 'glyph-codepoints.json')
+ cps = json.load(open(p))['glyphs'] if os.path.exists(p) else {}
+ nxt = max([int(v, 16) for v in cps.values()] + [FIRST - 1]) + 1
+ for n in sorted(names):
+ if n not in cps:
+ cps[n] = '%04X' % nxt
+ nxt += 1
+ data = {'about': 'Each OpenVibe glyph\'s code point in the OV Glyphs font (U+E001 up). Given once, never changed or reused.',
+ 'glyphs': dict(sorted(cps.items(), key=lambda kv: int(kv[1], 16)))}
+ with open(p, 'w') as f:
+ f.write(json.dumps(data, indent=2) + '\n')
+ return {n: int(v, 16) for n, v in cps.items()}
+
+
+def outline(inner):
+ """The glyph's strokes as one merged outline (a skia-pathops Path), in the 24-unit grid."""
+ svg = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-width="2" '
+ 'stroke-linecap="round" stroke-linejoin="round">' + inner + '</g></svg>')
+ shapes = []
+ for shape in SVG.fromstring(svg).topicosvg().shapes():
+ p = pathops.Path()
+ parse_path(shape.d, p.getPen())
+ shapes.append(p)
+ out = pathops.Path()
+ pathops.union(shapes, out.getPen())
+ return out
+
+
+def to_font(path, pen):
+ """Draws a 24-unit outline into a font pen: y up, the em square, an eighth below the baseline."""
+ path.draw(TransformPen(pen, (SCALE, 0, 0, -SCALE, 0, 24 * SCALE - DESCENT)))
+
+
+def build(flavour, order, cmap, outlines):
+ fb = FontBuilder(UPM, isTTF=(flavour == 'ttf'))
+ fb.setupGlyphOrder(order)
+ fb.setupCharacterMap(cmap)
+ if flavour == 'ttf':
+ glyfs = {'.notdef': TTGlyphPen(None).glyph(), 'space': TTGlyphPen(None).glyph()}
+ for name, path in outlines.items():
+ pen = TTGlyphPen(None)
+ to_font(path, Cu2QuPen(pen, max_err=1, reverse_direction=True))
+ glyfs[name] = pen.glyph()
+ fb.setupGlyf(glyfs)
+ else:
+ charstrings = {}
+ for name in order:
+ pen = T2CharStringPen(UPM, None)
+ if name in outlines:
+ to_font(outlines[name], pen)
+ charstrings[name] = pen.getCharString()
+ fb.setupCFF(FAMILY.replace(' ', ''), {'FullName': FAMILY}, charstrings, {})
+ fb.setupHorizontalMetrics({n: (UPM, 0) for n in order})
+ fb.setupHorizontalHeader(ascent=UPM - DESCENT, descent=-DESCENT)
+ fb.setupNameTable({'familyName': FAMILY, 'styleName': 'Regular', 'uniqueFontIdentifier': f'OpenVibe:{FAMILY}:{VERSION}',
+ 'fullName': FAMILY, 'psName': FAMILY.replace(' ', '') + '-Regular', 'version': f'Version {VERSION}',
+ 'copyright': 'OpenVibe glyphs, MIT licence. https://openvibe.design/icons',
+ 'licenseDescription': 'MIT', 'licenseInfoURL': 'https://opensource.org/license/mit'})
+ fb.setupOS2(sTypoAscender=UPM - DESCENT, sTypoDescender=-DESCENT, sTypoLineGap=0, usWinAscent=UPM - DESCENT,
+ usWinDescent=DESCENT, achVendID='OVIB', fsType=0)
+ fb.setupPost(keepGlyphNames=True)
+ return fb.font
+
+
+def main():
+ reg = registry()
+ names = reg['names']
+ cps = codepoints(names)
+ outlines = {n: outline(reg['paths'][n]) for n in names}
+ order = ['.notdef', 'space'] + names
+ cmap = {0x20: 'space', **{cps[n]: n for n in names}}
+
+ os.makedirs(os.path.join(ROOT, 'glyph-fonts'), exist_ok=True)
+ ttf = build('ttf', order, cmap, outlines)
+ ttf.save(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.ttf'))
+ build('otf', order, cmap, outlines).save(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.otf'))
+ web = TTFont(os.path.join(ROOT, 'glyph-fonts', 'ov-glyphs.ttf'))
+ web.flavor = 'woff2'
+ web.save(os.path.join(ROOT, 'ov-glyphs.woff2'))
+
+ esc = lambda n: '\\' + ('%04x' % cps[n])
+ head = '/* Generated by scripts/build-glyph-fonts.py from glyphs.js (openvibe-shared, MIT): edit those, then run it. */\n'
+ face = ("@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2');font-weight:400;font-style:normal;font-display:block}\n")
+ base = ("{display:inline-block;font-family:'OV Glyphs';font-style:normal;font-weight:400;font-variant:normal;line-height:1;"
+ "text-rendering:auto;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:never}\n")
+ font_css = (head + '/* <i class="ovi ovi-eye"></i>: the OpenVibe glyphs as a font. Sizes follow the text. */\n' + face
+ + '.ovi' + base + '.ovi-fw{width:1.25em;text-align:center}\n'
+ + ''.join(f'.ovi-{n}::before{{content:"{esc(n)}"}}\n' for n in names))
+ with open(os.path.join(ROOT, 'ov-glyphs-font.css'), 'w') as f:
+ f.write(font_css)
+ fa_rules = []
+ for n in names:
+ for fa in sorted(reg['fa'].get(n, [])):
+ fa_rules.append(f'.fa-{fa}::before{{content:"{esc(n)}"}}')
+ fa_css = (head + "/* Font Awesome's class names on the OV Glyphs font, for a site that drops Font Awesome's stylesheet and its\n"
+ " font. A name with no glyph shows nothing, so load it only where every name a page uses is covered\n"
+ " (openvibe.design/icons lists them); brand logos keep Font Awesome Brands. */\n" + face
+ + '.fa,.fas,.far,.fal,.fat,.fa-solid,.fa-regular,.fa-light,.fa-thin,.fa-duotone' + base
+ + '.fa-fw{width:1.25em;text-align:center}\n' + '\n'.join(fa_rules) + '\n')
+ with open(os.path.join(ROOT, 'ov-glyphs-fa.css'), 'w') as f:
+ f.write(fa_css)
+
+ source = json.dumps([[n, reg['paths'][n], '%04X' % cps[n]] for n in names], separators=(',', ':'), ensure_ascii=False)
+ manifest = {'family': FAMILY, 'glyphs': len(names), 'source_sha256': hashlib.sha256(source.encode('utf8')).hexdigest(),
+ 'files': {p: os.path.getsize(os.path.join(ROOT, p)) for p in
+ ['ov-glyphs.woff2', 'glyph-fonts/ov-glyphs.ttf', 'glyph-fonts/ov-glyphs.otf']}}
+ with open(os.path.join(ROOT, 'glyph-fonts', 'manifest.json'), 'w') as f:
+ f.write(json.dumps(manifest, indent=2) + '\n')
+ print(f"{len(names)} glyphs: " + ', '.join(f'{k} {v / 1024:.1f} KB' for k, v in manifest['files'].items()))
+
+
+if __name__ == '__main__':
+ sys.exit(main())
@@ -1,8 +1,9 @@
@@ -1,8 +1,9 @@
#!/usr/bin/env node
#!/usr/bin/env node
'use strict';
'use strict';
-// Writes ov-glyphs.js, the page drop-in that swaps Font Awesome icons for the OpenVibe glyphs, from glyphs.js (the one
-// registry). Re-run after a glyph or an alias changes: node scripts/build-glyphs.js (test/glyphs.test.js checks the file
-// is current). Only the glyphs some Font Awesome name stands for go in, so the drop-in carries no drawing it cannot use.
+// Writes ov-glyphs.js, the page drop-in that swaps Font Awesome icons for the OpenVibe glyphs, and ov-glyphs.svg, the
+// sprite (<symbol id="ovg-eye">), from glyphs.js (the one registry). Re-run after a glyph or an alias changes:
+// node scripts/build-glyphs.js (test/glyphs.test.js checks both files are current). Only the glyphs some Font Awesome
+// name stands for go in the drop-in, so it carries no drawing it cannot use; the sprite has every glyph.
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const glyphs = require('../glyphs');
const glyphs = require('../glyphs');
@@ -11,6 +12,55 @@ const used = [...new Set(Object.values(glyphs.FA))].sort();
@@ -11,6 +12,55 @@ const used = [...new Set(Object.values(glyphs.FA))].sort();
const G = Object.fromEntries(used.map((n) => [n, glyphs.path(n)]));
const G = Object.fromEntries(used.map((n) => [n, glyphs.path(n)]));
const FA = Object.fromEntries(Object.entries(glyphs.FA).sort(([a], [b]) => (a < b ? -1 : 1)));
const FA = Object.fromEntries(Object.entries(glyphs.FA).sort(([a], [b]) => (a < b ? -1 : 1)));
+// Font Awesome's features on a swapped icon (4.62.0), with Font Awesome's own class names and CSS variables, so a site
+// can drop Font Awesome's stylesheet and keep its markup. Rules about the icon itself are scoped to .ovg; the wrappers
+// (fa-ul, fa-stack, fa-layers) carry Font Awesome's own values. Nothing spins (plan L27): fa-spin breathes instead.
+const anim = (cls, name, timing, dur = '1s') => `.ovg.${cls}{animation:${name} var(--fa-animation-duration,${dur}) var(--fa-animation-timing,${timing}) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}`;
+const CSS = [
+ // The icon: Font Awesome's glyph off, ours in the text's colour and size, one drawing in four weights.
+ '.ovg::before,.ovg::after{content:none!important;display:none!important}',
+ '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}',
+ '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:var(--ovg-stroke,2);stroke-linecap:round;stroke-linejoin:round;overflow:visible}',
+ '.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}',
+ // Sizes, fixed width, borders and pulls.
+ '.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}',
+ Array.from({ length: 10 }, (_, i) => `.ovg.fa-${i + 1}x{font-size:${i + 1}em}`).join(''),
+ '.ovg.fa-fw{width:1.25em;text-align:center}',
+ '.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}',
+ '.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}',
+ // Turns and flips act on the drawing, so they combine with an animation on the icon.
+ '.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}',
+ '.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}',
+ // Lists, stacks and layers (Font Awesome's wrappers).
+ '.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}',
+ '.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}',
+ '.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}',
+ '.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}',
+ '.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}',
+ // Animations, with Font Awesome's variables and timings.
+ anim('fa-beat', 'ovgBeat', 'ease-in-out'),
+ anim('fa-bounce', 'ovgBounce', 'cubic-bezier(.28,.84,.42,1)'),
+ anim('fa-fade', 'ovgFade', 'cubic-bezier(.4,0,.6,1)'),
+ anim('fa-beat-fade', 'ovgBeatFade', 'cubic-bezier(.4,0,.6,1)'),
+ anim('fa-flip', 'ovgFlip', 'ease-in-out'),
+ anim('fa-shake', 'ovgShake', 'linear'),
+ '.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}',
+ '.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}',
+ '@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}',
+ '@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}',
+ '@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}',
+ '@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}',
+ '@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}',
+ '@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}',
+ '@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}',
+ // The busy dots take turns.
+ '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}',
+ '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}',
+ '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}',
+ // Reduced motion: nothing moves.
+ '@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}html[data-ov-motion=reduced] .ovg,html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none!important}',
+].join('');
+
const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
/**
/**
* openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
* openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
@@ -18,6 +68,12 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -18,6 +68,12 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every
* (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet, and every
* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.
* brand logo (fa-brands), stays Font Awesome's. A spinner is three dots that take turns, never a spin.
*
*
+ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,
+ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the
+ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light
+ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that
+ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured.
+ *
* <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later
* <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later
* OVGlyphs.swap(rootEl) swaps inside one element now
* OVGlyphs.swap(rootEl) swaps inside one element now
* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null
* OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null
@@ -29,30 +85,44 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -29,30 +85,44 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
if (typeof window === 'undefined' || window.OVGlyphs) return;
if (typeof window === 'undefined' || window.OVGlyphs) return;
const G = ${JSON.stringify(G)};
const G = ${JSON.stringify(G)};
const FA = ${JSON.stringify(FA)};
const FA = ${JSON.stringify(FA)};
- // Font Awesome's style and size classes, and its animations: they say how, not what.
- const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity)$/;
+ // Font Awesome's style, size, layout and animation classes: they say how, not what.
+ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;
const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';
const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';
- const CSS = '.ovg::before,.ovg::after{content:none!important;display:none!important}'
- + '.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}'
- + '.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'
- + '.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;overflow:visible}'
- + '.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}'
- + '.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}'
- + '@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}'
- + '@media (prefers-reduced-motion:reduce){.ovg[data-ovg=busy] circle{animation:none}}html[data-ov-motion=reduced] .ovg[data-ovg=busy] circle{animation:none}';
+ const CSS = ${JSON.stringify(CSS)};
const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };
const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };
const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };
const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };
+ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).
+ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;
+ function transformOf(spec) {
+ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;
+ for (const p of String(spec || '').toLowerCase().split(/\\s+/)) {
+ const m = TF.exec(p);
+ if (!m) continue;
+ any = true;
+ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }
+ const n = Number(m[2]);
+ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;
+ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;
+ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;
+ }
+ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';
+ }
function draw(el) {
function draw(el) {
if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;
if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return;
const fa = nameOf(el);
const fa = nameOf(el);
const g = fa && fromFa(fa);
const g = fa && fromFa(fa);
- if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; el.textContent = ''; } return; }
- if (el.dataset.ovgFa === fa) return;
+ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }
+ const tf = el.getAttribute('data-fa-transform') || '';
+ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself forever
if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon
if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon
el.classList.add('ovg');
el.classList.add('ovg');
el.dataset.ovg = g; el.dataset.ovgFa = fa;
el.dataset.ovg = g; el.dataset.ovgFa = fa;
- if (!el.hasAttribute('aria-label') && !el.hasAttribute('title')) el.setAttribute('aria-hidden', 'true');
- el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + (g === 'busy' ? BUSY : G[g]) + '</svg>';
+ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;
+ const title = el.getAttribute('title');
+ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }
+ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');
+ const t = transformOf(tf);
+ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g]) + '</svg>';
}
}
const SEL = 'i[class*="fa-"],span[class*="fa-"]';
const SEL = 'i[class*="fa-"],span[class*="fa-"]';
function swap(root) {
function swap(root) {
@@ -69,13 +139,20 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
@@ -69,13 +139,20 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }
if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }
r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });
r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });
}
}
- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
+ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });
} catch (e) { /* no observer: what was there is swapped */ }
} catch (e) { /* no observer: what was there is swapped */ }
}
}
window.OVGlyphs = { swap, fromFa };
window.OVGlyphs = { swap, fromFa };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
})();
})();
`;
`;
-fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), out);
-if (require.main === module) console.log(`ov-glyphs.js: ${used.length} glyphs for ${Object.keys(FA).length} Font Awesome names, ${(out.length / 1024).toFixed(1)} KB`);
-module.exports = { build: () => out };
+
+// The sprite: every glyph as a <symbol>, for <svg><use href="/shared/ov-glyphs.svg#ovg-eye"/></svg>.
+const sprite = `<svg xmlns="http://www.w3.org/2000/svg">\n<!-- The OpenVibe glyphs (openvibe-shared, MIT): <svg><use href="ov-glyphs.svg#ovg-eye"/></svg>. Generated by scripts/build-glyphs.js. -->\n${glyphs.NAMES.map((n) => `<symbol id="ovg-${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${glyphs.path(n)}</symbol>`).join('\n')}\n</svg>\n`;
+
+if (require.main === module) {
+ fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), out);
+ fs.writeFileSync(path.join(__dirname, '..', 'ov-glyphs.svg'), sprite);
+ console.log(`ov-glyphs.js: ${used.length} glyphs for ${Object.keys(FA).length} Font Awesome names, ${(out.length / 1024).toFixed(1)} KB; ov-glyphs.svg: ${glyphs.NAMES.length} symbols, ${(sprite.length / 1024).toFixed(1)} KB`);
+}
+module.exports = { build: () => out, sprite: () => sprite, CSS };
@@ -80,6 +80,7 @@ function contentType(name) {
@@ -80,6 +80,7 @@ function contentType(name) {
if (name.endsWith('.jpg')) return 'image/jpeg';
if (name.endsWith('.jpg')) return 'image/jpeg';
if (name.endsWith('.png')) return 'image/png';
if (name.endsWith('.png')) return 'image/png';
if (name.endsWith('.woff2')) return 'font/woff2';
if (name.endsWith('.woff2')) return 'font/woff2';
+ if (name.endsWith('.svg')) return 'image/svg+xml; charset=utf-8';
return 'application/javascript; charset=utf-8';
return 'application/javascript; charset=utf-8';
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+'use strict';
+// The glyphs as fonts (4.62.0, plan L32 I5): built by scripts/build-glyph-fonts.py from glyphs.js. The fonts are
+// current (the manifest names exactly what they were drawn from), every glyph has its own code point in the Private
+// Use Area that never changes, the CSS names every glyph and every Font Awesome name, and sites serve the web font,
+// both style sheets and the sprite.
+const assert = require('assert');
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const glyphs = require('../glyphs');
+const files = require('../files');
+
+const ROOT = path.join(__dirname, '..');
+const read = (f) => fs.readFileSync(path.join(ROOT, f));
+const cps = JSON.parse(read('glyph-codepoints.json')).glyphs;
+const manifest = JSON.parse(read('glyph-fonts/manifest.json'));
+
+// Every glyph has a code point; none is shared; all sit in the Private Use Area from U+E001.
+for (const n of glyphs.NAMES) assert.ok(/^[0-9A-F]{4}$/.test(cps[n] || ''), `${n} has a code point`);
+const values = Object.values(cps);
+assert.strictEqual(new Set(values).size, values.length, 'no code point is given twice');
+values.forEach((v) => assert.ok(parseInt(v, 16) >= 0xE001 && parseInt(v, 16) <= 0xF8FF, `U+${v} is in the Private Use Area`));
+
+// The fonts were built from today's registry (run scripts/build-glyph-fonts.py after a glyph changes).
+const source = JSON.stringify(glyphs.NAMES.map((n) => [n, glyphs.path(n), cps[n]]));
+assert.strictEqual(manifest.source_sha256, crypto.createHash('sha256').update(source).digest('hex'), 'the fonts are current: run scripts/build-glyph-fonts.py');
+assert.strictEqual(manifest.glyphs, glyphs.NAMES.length);
+for (const [f, size] of Object.entries(manifest.files)) assert.strictEqual(read(f).length, size, `${f} is the file the build wrote`);
+assert.strictEqual(read('ov-glyphs.woff2').subarray(0, 4).toString('latin1'), 'wOF2', 'a WOFF2 file');
+assert.strictEqual(read('glyph-fonts/ov-glyphs.ttf').readUInt32BE(0), 0x00010000, 'a TrueType file');
+assert.strictEqual(read('glyph-fonts/ov-glyphs.otf').subarray(0, 4).toString('latin1'), 'OTTO', 'an OpenType (CFF) file');
+
+// The style sheets.
+const font = read('ov-glyphs-font.css').toString();
+assert.ok(font.includes("@font-face{font-family:'OV Glyphs';src:url(ov-glyphs.woff2) format('woff2')"));
+for (const n of glyphs.NAMES) assert.ok(font.includes(`.ovi-${n}::before{content:"\\${cps[n].toLowerCase()}"}`), `.ovi-${n}`);
+const fa = read('ov-glyphs-fa.css').toString();
+for (const [name, g] of Object.entries(glyphs.FA)) assert.ok(fa.includes(`.fa-${name}::before{content:"\\${cps[g].toLowerCase()}"}`), `.fa-${name} → ${g}`);
+assert.ok(!/fa-brands|\.fab\b/.test(fa), 'brand logos stay Font Awesome Brands');
+
+// Sites serve them.
+for (const f of ['ov-glyphs.woff2', 'ov-glyphs-font.css', 'ov-glyphs-fa.css', 'ov-glyphs.svg']) assert.ok(files.isBrowserFile(f), `/shared/${f}`);
+assert.ok(files.FONTS.includes('ov-glyphs.woff2') && files.IMAGES.includes('ov-glyphs.svg'));
+
+console.log(`glyph fonts: ${glyphs.NAMES.length} glyphs at U+${values.reduce((a, b) => (a < b ? a : b))}–U+${values.reduce((a, b) => (a > b ? a : b))}; WOFF2, TTF and OTF current; both style sheets; served`);
@@ -34,9 +34,25 @@ for (const fa of ['wand-magic-sparkles', 'eye', 'tower-broadcast', 'xmark', 'vid
@@ -34,9 +34,25 @@ for (const fa of ['wand-magic-sparkles', 'eye', 'tower-broadcast', 'xmark', 'vid
}
}
// ── The drop-in is the registry's, current ──
// ── The drop-in is the registry's, current ──
-const built = require('../scripts/build-glyphs').build();
+const builder = require('../scripts/build-glyphs');
const SRC = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');
const SRC = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');
-assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-glyphs.js');
+assert.strictEqual(SRC, builder.build(), 'ov-glyphs.js is current: run node scripts/build-glyphs.js');
+const SPRITE = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.svg'), 'utf8');
+assert.strictEqual(SPRITE, builder.sprite(), 'ov-glyphs.svg is current: run node scripts/build-glyphs.js');
+assert.strictEqual((SPRITE.match(/<symbol id="ovg-/g) || []).length, glyphs.NAMES.length, 'the sprite has every glyph');
+assert.ok(SPRITE.includes(`<symbol id="ovg-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${glyphs.path('eye')}</symbol>`));
+
+// Font Awesome's features, kept (4.62.0): every documented class has a rule, with Font Awesome's own variables.
+const CSS = builder.CSS;
+const ON_ICON = ['fa-2xs', 'fa-xs', 'fa-sm', 'fa-lg', 'fa-xl', 'fa-2xl', 'fa-1x', 'fa-10x', 'fa-fw', 'fa-border', 'fa-pull-left', 'fa-pull-right',
+ 'fa-rotate-90', 'fa-rotate-180', 'fa-rotate-270', 'fa-rotate-by', 'fa-flip-horizontal', 'fa-flip-vertical', 'fa-flip-both', 'fa-inverse',
+ 'fa-beat', 'fa-bounce', 'fa-fade', 'fa-beat-fade', 'fa-flip', 'fa-shake', 'fa-spin', 'fa-thin', 'fa-light', 'fa-duotone'];
+ON_ICON.forEach((cls) => assert.ok(CSS.includes(`.ovg.${cls}{`) || CSS.includes(`.ovg.${cls}>svg{`) || CSS.includes(`.ovg.${cls},`), `.${cls} on a swapped icon`));
+['fa-ul', 'fa-li', 'fa-stack', 'fa-layers', 'fa-layers-text', 'fa-layers-counter'].forEach((cls) => assert.ok(CSS.includes(`.${cls}{`), `.${cls}, Font Awesome's wrapper`));
+['--fa-beat-scale', '--fa-bounce-height', '--fa-fade-opacity', '--fa-flip-angle', '--fa-rotate-angle', '--fa-animation-duration', '--fa-li-width',
+ '--fa-border-color', '--fa-counter-background-color', '--fa-primary-color', '--fa-inverse'].forEach((v) => assert.ok(CSS.includes(v), `Font Awesome's ${v}`));
+assert.ok(!CSS.includes('360deg') && /\.ovg\.fa-spin[^{]*\{animation:ovgBreathe/.test(CSS), 'fa-spin breathes: nothing spins (L27)');
+assert.ok(CSS.includes('@media (prefers-reduced-motion:reduce){.ovg,.ovg[data-ovg=busy] circle{animation:none!important}}'), 'reduced motion stops every animation');
// ── In a page ──
// ── In a page ──
(async () => {
(async () => {
@@ -47,6 +63,9 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-
@@ -47,6 +63,9 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-
<i class="fa-solid fa-couch" id="unknown"></i>
<i class="fa-solid fa-couch" id="unknown"></i>
<i class="fa-solid fa-eye" aria-label="Seen" id="labelled"></i>
<i class="fa-solid fa-eye" aria-label="Seen" id="labelled"></i>
<span class="fa-solid fa-play" id="toggle"></span>
<span class="fa-solid fa-play" id="toggle"></span>
+<i class="fa-solid fa-heart fa-2x fa-beat" data-fa-transform="grow-4 up-2 rotate-90 flip-h" id="tf"></i>
+<i class="fa-solid fa-bell" title="Notifications" id="titled"></i>
+<span class="fa-layers"><i class="fa-solid fa-envelope"></i><span class="fa-layers-counter" id="counter">12</span></span>
</body></html>`);
</body></html>`);
window.MutationObserver = MutationObserver;
window.MutationObserver = MutationObserver;
new Function('window', 'document', 'MutationObserver', SRC)(window, document, MutationObserver);
new Function('window', 'document', 'MutationObserver', SRC)(window, document, MutationObserver);
@@ -58,13 +77,26 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-
@@ -58,13 +77,26 @@ assert.strictEqual(SRC, built, 'ov-glyphs.js is current: run node scripts/build-
assert.ok($('#spin').dataset.ovg === 'busy' && $('#spin').querySelectorAll('circle').length === 3, 'a spinner becomes three dots');
assert.ok($('#spin').dataset.ovg === 'busy' && $('#spin').querySelectorAll('circle').length === 3, 'a spinner becomes three dots');
assert.ok(!$('#gh').classList.contains('ovg') && !$('#unknown').classList.contains('ovg'), 'brands and unknown names stay Font Awesome');
assert.ok(!$('#gh').classList.contains('ovg') && !$('#unknown').classList.contains('ovg'), 'brands and unknown names stay Font Awesome');
const css = document.getElementById('ov-glyphs-css').textContent;
const css = document.getElementById('ov-glyphs-css').textContent;
- assert.ok(css.includes('.ovg::before,.ovg::after{content:none!important') && css.includes('.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:none!important}'), "Font Awesome's own glyph and spin are turned off on a swapped icon");
+ assert.ok(css.includes('.ovg::before,.ovg::after{content:none!important') && css.includes('.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe'), "Font Awesome's own glyph is off on a swapped icon, and its spin breathes");
+ // data-fa-transform: Font Awesome's sixteenths, on the drawing; size and animation classes stay on the icon.
+ const tf = $('#tf');
+ assert.ok(tf.classList.contains('ovg') && tf.classList.contains('fa-2x') && tf.classList.contains('fa-beat'));
+ assert.strictEqual(tf.querySelector('svg').getAttribute('style'), 'transform:translate(0em,-0.125em) rotate(90deg) scale(-1.25,1.25)');
+ // A title names the icon to screen readers.
+ assert.ok($('#titled').getAttribute('role') === 'img' && $('#titled').getAttribute('aria-label') === 'Notifications' && !$('#titled').hasAttribute('aria-hidden'));
+ // A layer's counter is text, not an icon.
+ assert.ok(!$('#counter').classList.contains('ovg') && $('#counter').textContent === '12');
+ assert.ok($('.fa-layers > i').dataset.ovg, 'the layer\'s icon is swapped');
// A page that changes an icon by its class (play ↔ pause) gets the new glyph.
// A page that changes an icon by its class (play ↔ pause) gets the new glyph.
const t = $('#toggle');
const t = $('#toggle');
assert.strictEqual(t.dataset.ovg, 'play');
assert.strictEqual(t.dataset.ovg, 'play');
t.className = 'fa-solid fa-pause';
t.className = 'fa-solid fa-pause';
await new Promise((r) => setImmediate(r));
await new Promise((r) => setImmediate(r));
assert.ok(t.dataset.ovg === 'pause' && t.classList.contains('ovg'), 'follows a class change');
assert.ok(t.dataset.ovg === 'pause' && t.classList.contains('ovg'), 'follows a class change');
+ // A page that resets the class list but keeps the name keeps the glyph (the swap class comes back).
+ t.className = 'fa-solid fa-pause is-on';
+ await new Promise((r) => setImmediate(r));
+ assert.ok(t.classList.contains('ovg') && t.dataset.ovg === 'pause', 'the swap class comes back after a reset');
t.className = 'fa-solid fa-couch';
t.className = 'fa-solid fa-couch';
await new Promise((r) => setImmediate(r));
await new Promise((r) => setImmediate(r));
assert.ok(!t.classList.contains('ovg') && !t.dataset.ovg && !t.querySelector('svg'), 'and hands back a name it does not know');
assert.ok(!t.classList.contains('ovg') && !t.dataset.ovg && !t.querySelector('svg'), 'and hands back a name it does not know');