diff --git a/AGENTS.md b/AGENTS.md index 68f2a5f..d03c6e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,157 @@ # ChatYX Agent Instructions +These rules apply to every coding agent working in this repository. They are the +short form of the engineering contract; `documents/ARCHITECTURE.md` and +`documents/DESIGN.md` hold the detail. + +## Required reading + +Before non-trivial work, read what the change touches: + +- `documents/ARCHITECTURE.md` — layers, ownership, the setup feature, icons, tokens +- `documents/DESIGN.md` — the token contract and the overlay's visual model +- relevant tests for the area being changed + +Repository-wide refactors additionally require: + +- `documents/REFACTORING.md` — the full engineering rules and refactoring workflow +- `documents/REFACTORING_FOLLOWUP.md` — the current completion supplement + +Do not start a repository-wide cleanup after reading one or two files. + +## Architecture direction + +```text +routes -> features -> services/config/utils + -> platform adapters +``` + +- `routes/` bind Solid signals and JSX to a feature's public API. A route owns + visibility and composition, not infrastructure, network clients or timers. +- `features/*/application` coordinate use cases and own lifecycle. +- `features/*/model` hold pure state transformations and calculations. +- `services/` implement network, browser, provider, storage and platform + capabilities. +- `config/` holds schemas, normalization, parsing, serialization and defaults. +- `utils/` holds small genuinely reusable helpers, never domain logic. + +A service must not import a feature, a route or a component. A feature must not +import a component: shared contracts live in `features/*/model`, `config/` or the +`services/` layer, and components import them, never the reverse. The one +recorded exception is that a `config/` module may depend on a dependency-free +`services/` leaf (`config/setupTemplates` on `services/storage/setupStorage`). + +Do not add a service locator or a DI container. Pass dependencies through +constructors, factories or parameters. + +## Composition roots and lifecycle + +Every long-lived resource — socket, timer, interval, subscription, observer, +listener, mutable cache, reconnect loop, runtime — has exactly one owner, and +that owner releases it. + +- `createChatOverlayApplication` is the chat route's composition root: one live + or preview runtime plus one predictions controller. +- Construction must not create an owned resource that `destroy()` refuses to + release. `destroy()` is safe before `start()`, safe when called twice, and + safe while `start()` is still awaiting. +- Teardown is an ownership boundary, not a reset call: asynchronous work started + under a runtime must not repopulate shared state after that runtime is + destroyed. Singleton services guard their commits with a generation they + capture when the work starts. +- Preview and live runtime own different things in the same document. Reset a + process-wide singleton only from the owner that installed it. +- Browser-global events are for real browser integration only, never as an + internal event bus. + +## UI primitives + +`src/components/ui/` holds generic primitives with no ChatYX domain knowledge — +no Twitch, YouTube, Kick, OBS, TTS, RTE, setup-section or chat-config concepts. + +```text +components/ui generic primitive +components/ feature presentation +routes/* page composition +``` + +Check for an existing primitive before writing a control. Do not push a bespoke +control through a generic primitive when that changes its design; extract only +genuine duplication, and put feature wrappers in `components//`. + +## Icons + +- Generic UI icons: `@hugeicons/core-free-icons`, rendered through + `src/components/ui/icon.tsx`, which is a thin wrapper over the vendor's + `HugeiconsIcon` from `@hugeicons/solid-js`. The wrapper owns `currentColor`, + the `1em` default size and decorative-by-default accessibility; the vendor + owns SVG rendering; the icon payload owns its stroke width. +- Import icons by subpath (`@hugeicons/core-free-icons/TextIcon`). Never + `import * as Icons`, never a runtime string lookup, never an icon font. +- Brand glyphs (Twitch, YouTube, Kick, GitHub, 7TV) have one owner: + `src/components/brand/PlatformGlyph.tsx`. Do not replace them with generic + icons and do not duplicate them as a public SVG. +- Bespoke graphics — sparklines, event, reply and role artwork, custom + visualizations — stay custom. Consistency means one system per + responsibility, not one renderer for every drawing. +- Decorative icons stay hidden from assistive technology; icon-only controls + carry an accessible name. + +## Styling + +Tailwind, CVA, `cn()` and Kobalte are the current stack. Do not add another CSS +framework, and do not remove Tailwind to prepare for a future one. + +- Static CSS owns rule shape. The runtime publishes values as CSS custom + properties: `getOverlayStyleVariables()` → `OverlayStyleManager` → document + root → `chat.css`. Do not go back to one generated stylesheet per preset. +- Generated CSS is valid only where the rule shape is provider data: 7TV paint + and emote modifiers. Ask whether stable CSS plus a custom property expresses + the behaviour before generating CSS. +- A reusable design decision becomes a token; a value that changes while the + overlay runs stays a custom property. Do not turn every literal into a token. +- Inline styles are for genuinely dynamic values, not for static design. +- Classify `!important` before touching it. Keep the OBS transparency override + and provider/emote overrides; fix specificity problems in CSS instead. +- Do not migrate overlay, provider or runtime-generated markup to Tailwind. +- Lifecycle modules control state and ownership, not visual design values. Put a + product colour in CSS, not in a document helper. + +## Accessibility, OBS and public contracts + +- Refactors preserve labels, ARIA relationships, keyboard navigation, focus + visibility, semantic buttons, disabled state and screen-reader text. +- The overlay is an OBS browser source: transparent page and container + background, stable source order, wrapping contracts, emote modifiers, + provider cosmetics and the user's font/colour/stroke/shadow choices. +- Public contracts are frozen unless the task is explicitly a breaking change: + chat URL parameter names and defaults, overlay URL shape, the preview message + protocol, setup import/export meaning, platform source configuration, and the + stored setup keys (`chatyx.setup.*`). `buildOverlayUrl` is the single producer + of the overlay link; `tests/setupConfig.test.ts` and `tests/formValues.test.ts` + pin the round trip. +- No visual redesign during structural work. + +## Dependencies + +One clear responsibility per library. Before adding a dependency, answer what it +owns, whether that is already covered, and what it costs. Remove an old library +only after every real usage is migrated and the build passes. + +## Tests + +Prefer characterization tests at the layer that owns the behaviour: injected +factories, deferred promises, minimal `window`/`document` shims, pure projection +tests, service-level tests. Do not add a component-render environment for its +own sake, and do not snapshot generated class names. Never delete a valid test +to make a refactor easier, and never claim green without running the commands. + +Full validation before declaring work done: + +```bash +bun run check # lint + typecheck + tests + build +``` + ## Commit Messages Use [Conventional Commits](https://www.conventionalcommits.org/) for every commit. @@ -10,4 +162,5 @@ Follow the project rules in [CONTRIBUTING.md](./CONTRIBUTING.md) and the - Use an imperative, lower-case description without a trailing period. - Use a logical project area as the scope; do not use issue IDs as scopes. - Use `!` and a `BREAKING CHANGE:` footer for breaking changes. +- One concern per commit. Do not combine unrelated migrations. - Run `bun run check` before committing when practical. diff --git a/bun.lock b/bun.lock index 6ddc735..69b3af4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,14 @@ "": { "name": "chatyx", "dependencies": { + "@hugeicons/core-free-icons": "^4.3.5", + "@hugeicons/solid-js": "^1.0.0", "@kobalte/core": "^0.13.13", "@solid-primitives/i18n": "^2.2.1", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-solid": "^1.41.0", "solid-js": "^1.9.15", "tailwind-merge": "^3.6.0", }, @@ -87,6 +88,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@4.3.5", "", {}, "sha512-Sv+NjHRPnQk+yZsGCMcGznCHdTfJR9PMMOEKcJYAQU4gy90dmlc9PwdtvYOImBZIjiheMsTLL5eMn2DmHxUiUg=="], + + "@hugeicons/solid-js": ["@hugeicons/solid-js@1.0.0", "", { "peerDependencies": { "solid-js": "^1.8.0" } }, "sha512-GrcS7YlZVELwXljQZIB5LvcvVQevy1f4rUn3tP/UvB8qj1Wu9IP3aEQH9Y4OAyhStZTXA3T1mtvzeaHnaTH5dg=="], + "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -367,8 +372,6 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-solid": ["lucide-solid@1.41.0", "", { "peerDependencies": { "solid-js": "^1.4.7" } }, "sha512-kqAHyabxyoI9badC2nTvvW88d90E4NYyXtBEFdHI5mKQYegw4NW6vXook7UU69tjBzO578bYVwbNqLT1Q5VRLw=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], diff --git a/documents/ARCHITECTURE.md b/documents/ARCHITECTURE.md index b278536..b44a18d 100644 --- a/documents/ARCHITECTURE.md +++ b/documents/ARCHITECTURE.md @@ -12,14 +12,20 @@ routes -> features -> services/config/utils - `routes` bind Solid signals and JSX to a feature's public API. - `features/*/application` coordinate use cases and own lifecycle. -- `features/*/model` contains pure state transformations and view calculations. +- `features/*/model` contains pure state transformations and contracts shared + with other layers. - `services` implement platform and browser capabilities. - A service must not import a feature or route. +- A feature must not import a component. A contract that both a feature and a + component need lives in the feature's `model`, in `config/` or in `services/`, + and the component imports it — never the reverse. `SetupSectionId` is the + worked example: it lives in `features/setup/model/setupSections.ts` because the + route, the workspace layout and the settings search all need it. Feature internals use direct imports. A feature-level `index.ts` is a public API for consumers outside that feature. -## Chat overlay +## Chat overlay lifecycle and asset ownership `createChatOverlayApplication` is the composition root for the chat route. It constructs exactly one live or preview runtime and one predictions controller. @@ -31,6 +37,30 @@ channel.tsx -> PredictionController ``` +Construction and teardown agree about ownership: `ChatOverlayApplication.destroy()` +releases what its constructor built, is idempotent, and is terminal, so it is +also safe while `start()` is still awaiting `initialize()`. A runtime that is +destroyed stops publishing: the live runtime guards every step of its +initialization with a generation, and the preview runtime ignores configuration +and timer callbacks after destruction. + +Asset state has one owner per document. `emoteService`, `badgeService`, +`sevenTVCosmeticsService`, `mentionStyleService` and `chatFeatureIntegration` are +process-wide singletons, and the runtime that filled them is the runtime that +resets them in `destroy()`. Resetting alone is not enough, because a request +that is already in flight can still resolve: each of those services carries a +generation that `reset()` (or `clearAllCaches()`) bumps, and a load captures it +on entry so a late response cannot commit into the store the next runtime owns. + +`ChatAssetLoader` needs none of that: it is constructed per live runtime, and its +shared-channel map and channel-scoped caches die with the instance. + +The preview runtime is the only runtime in its document — the setup page loads it +in an iframe, which has its own module instances — so its teardown clears the +process-wide state it claimed. For the same reason the RTE proxy flag is a plain +boolean rather than a lease or a reference count: exactly one chat runtime exists +per document, and only the runtime that set the flag clears it. + Each runtime owns the resources it starts and releases them in `destroy()`. Cross-module events use typed callbacks; browser-global custom events are reserved for actual browser integration boundaries, not communication between services in the same object graph. @@ -52,9 +82,89 @@ src/services/chat/ twitch/ # IRC, GQL, and recent-message clients ``` +`runtime/` also owns the layout and fade managers. They are chat overlay +infrastructure, not general helpers — the stylesheets they publish are written +against `#chat_container`, `.chat_line` and `.message-fade-out` — so they sit +next to the pipeline that owns them instead of in `utils/`. + Provider folders do not expose internal barrels. Cross-feature consumers use `src/services/chat/index.ts`; chat internals import concrete sibling modules directly. +## Setup feature + +`src/routes/setup.tsx` is a UI composition root: it binds signals, wires feature +APIs into effects, composes sections and renders JSX, and nothing else. +Everything with a contract of its own lives outside it. + +```text +src/features/setup/model/setupSections.ts # setup section identity +src/features/setup/previewSync.ts # the setup side of the preview iframe protocol +src/features/setup/settingsSearch.ts # searchable elements, highlights, counter, reveal +src/features/setup/setupDocument.ts # document scroll lock and reduced-motion watch +src/config/setupConfig.ts # form state -> ChatConfig, ChatConfig -> overlay URL +src/config/formValues.ts # form string -> config value, mirroring chatUrlParams kinds +src/config/setupTemplates.ts # built-in and user templates, validation, serialization +src/services/setup/botProfiles.ts # Twitch/Kick bot lookups (profiles) +src/services/setup/channelSearch.ts # Twitch/Kick channel search and its normalizers +src/services/setup/logins.ts # pasted-login normalization shared by the three lists +src/services/setup/localFonts.ts # Local Font Access capability +src/services/storage/setupStorage.ts # every localStorage access the setup page makes +src/components/setup/* # section layout, rows, chips and the feature's controls +src/components/setup/setupText.ts # label resolution, kept free of JSX so it stays testable +``` + +The row descriptors under `components/setup/sections/` are pure descriptor +factories: they take one source object of signals and callbacks and return the +rows. They import translations, UI primitives and types only, so they cannot +grow into a second route. + +The overlay URL is a public contract: deployed OBS browser sources depend on it. +It is therefore produced in one place, `buildOverlayUrl`, and covered by tests +that round-trip through `parseChatConfigFromSearchParams`. Renaming or dropping a +query parameter is a breaking change, not a rename. + +`config` may depend on a dependency-free `services` leaf (as `setupTemplates` +does on `setupStorage`), because services are the lower layer for capabilities. +The reverse — a service importing a route or a feature — remains forbidden. + +## Icons and brand assets + +UI icons come from `@hugeicons/core-free-icons` and are rendered through +`src/components/ui/icon.tsx`, which is a thin wrapper over the vendor's +`HugeiconsIcon` from `@hugeicons/solid-js`. The vendor owns SVG rendering; the +wrapper owns only the project convention — `currentColor`, `1em` as the default +size, and decorative-by-default accessibility — and the icon payload keeps its +own stroke width. + +There is no icon font, no `hgi-*` class string, and no inline SVG standing in for +a UI icon. Icons are imported by subpath (`@hugeicons/core-free-icons/TextIcon`) +because the package barrel is 675 KB while a single icon is under a kilobyte. + +Two kinds of drawing are deliberately not UI icons and stay inline: monochrome +platform glyphs, which have exactly one owner in +`src/components/brand/PlatformGlyph.tsx`, and bespoke visuals such as the +sparkline and the event, reply and role graphics. The public brand chips under +`public/img/` are a different rendering for a different surface, not a duplicate +of the glyph components. + +## Plain and generated CSS + +Design tokens are consumed as CSS custom properties. `src/styles/chat.css` owns +the overlay's rules and `overlayStyleManager` writes only property values and two +root attributes, so the rule shape is fixed at build time. + +Two boundaries are still allowed to generate CSS, because there the rule shape is +data supplied from outside rather than a design decision: + +- provider cosmetics (7TV paint), where colours and gradients arrive as protocol + data; +- emote modifiers, where geometry depends on the emote's own dimensions. + +Everything else prefers a stable rule plus a custom property. The same split +applies away from the overlay: the setup page's document lock is a `data-*` +attribute on the root and `SetupWorkspace.css` owns what that means, so the +lifecycle helper holds state rather than a colour. + ## Refactoring rules 1. Extract ownership before extracting files: every timer, socket, listener, and mutable cache has diff --git a/documents/DESIGN.md b/documents/DESIGN.md index faffb9d..637e970 100644 --- a/documents/DESIGN.md +++ b/documents/DESIGN.md @@ -12,6 +12,75 @@ Setup is a compact operational interface for producing an OBS overlay URL. It pr - The RTE proxy routes only allowlisted public emote and badge hosts; Twitch authentication and user URLs are never routed through it. - Reyohoho badges and RTE paints are soft optional cosmetics; unavailable data leaves the existing chat rendering unchanged. +## Token Contract + +Tokens are the interface between the stylesheets and the runtime. Two rules make +that interface stable: + +1. `src/styles/chat.css` owns the rules. A rule's shape is fixed at build time. +2. The runtime writes only custom properties. `overlayStyleManager` sets values + on the document; it does not emit a stylesheet whose shape depends on which + preset the user picked. + +A value that changes while the overlay runs — a user-selected color, emote +geometry, a measured layout shift, 7TV paint, provider cosmetics — is passed as a +custom property. A value that is a reusable design decision belongs in the +stylesheet. + +`SIZE_CONFIGS` in `src/styles/chatStyles.ts` stays the single source of truth for +message size, weight, line height and the emote scale, because +`renderMessageContent.ts` reads the same table for emote geometry. The runtime +publishes it through `getOverlayStyleVariables()` instead of generating rules, so +the table drives custom properties rather than rule text. `OVERLAY_STYLE_PROPERTIES` +and `OVERLAY_ATTRIBUTES` list exactly what the manager may write; the boolean +variants (`.user_info`, the name separator) are keyed on +`:root[data-hide-names]` and `:root[data-nl-after-name]`. + +Two boundaries are allowed to generate CSS, because there the rule shape is data +supplied from outside rather than a design decision: + +- provider cosmetics (7TV paint), where colors and gradients arrive as protocol + data; +- emote modifiers, where geometry depends on the emote's own dimensions. + +The setup page's document lock follows the same rule in the other direction: the +lifecycle helper sets `data-setup-document` on the root and the setup stylesheet +owns the scrolling and height it implies. Product colors stay in CSS. + +## Styling Stack + +```text +Tailwind application utility styling, as it already exists +components/ui reusable primitives, with CVA where a real variant contract exists +CSS custom props overlay runtime values and component contracts +plain CSS overlay, provider and browser-integration boundaries +@hugeicons/solid-js generic UI icon rendering +PlatformGlyph brand-owned glyphs +``` + +Tailwind is not scheduled for removal and is not being extended: no new utility +spaghetti, no new abstraction layer, no Tailwind in provider- or +runtime-generated markup. + +## Iconography + +Generic UI icons are Hugeicons payloads rendered by the project's thin `Icon` +wrapper over the vendor's Solid renderer. The wrapper fixes `currentColor`, the +`1em` default size and the decorative default; the payload keeps its own stroke +width. Icons stay hidden from assistive technology unless they carry a label, and +icon-only controls always have an accessible name. + +Brand marks are not generic icons: `PlatformGlyph` is their only owner, and the +bespoke overlay graphics — sparkline, event, reply and role artwork — stay custom +drawings. + +## Surface Ownership + +`.setup-root` paints the setup workspace surface; the document itself stays +transparent because `app.css` forces that for OBS. `html`, `body` and `#root` +therefore never need a background write at runtime, and a module that needs to +change document behaviour changes state (an attribute) rather than presentation. + ## Reusable Primitives - `SetupNav`: desktop section navigation and its compact mobile equivalent. diff --git a/documents/REFACTORING.md b/documents/REFACTORING.md new file mode 100644 index 0000000..06343c0 --- /dev/null +++ b/documents/REFACTORING.md @@ -0,0 +1,1029 @@ +# ChatYX Agent Instructions + +These instructions apply to every coding agent working in this repository. + +The goal is not only to make code pass. Changes should preserve and improve the architecture of ChatYX over time. + +Before making non-trivial changes, understand the relevant feature, its dependencies, its lifecycle, and the existing project conventions. + +--- + +## 1. Core Engineering Principles + +Prefer: + +- clear ownership over shared mutable behavior; +- feature boundaries over generic technical buckets; +- explicit dependencies over hidden globals; +- reusable UI primitives over repeated markup; +- semantic design tokens over repeated visual literals; +- one clear library responsibility over overlapping solutions; +- small, behavior-preserving refactors over rewrites; +- characterization tests before changing risky code; +- direct, readable code over abstraction for abstraction's sake. + +Do not optimize primarily for fewer lines or smaller files. + +A large cohesive file may be acceptable. +A small file with mixed responsibilities may still be poorly designed. + +Split code by **reason to change**, not by line count. + +--- + +## 2. Required Project Context + +Before any substantial refactor, read: + +- `README.md` +- `CONTRIBUTING.md` +- `documents/ARCHITECTURE.md` +- `documents/DESIGN.md` +- `package.json` +- `solidcn.json` +- relevant tests for the feature being changed + +For repository-wide architectural work, inspect the full project structure first: + +```text +src/ +services/ +tests/ +scripts/ +public/ +documents/ +``` + +Do not start a repository-wide cleanup after inspecting only one or two files. + +--- + +## 3. Architecture Direction + +The intended dependency direction is: + +```text +routes -> features -> services/config/utils + -> platform adapters +``` + +General responsibilities: + +```text +routes/ + Routing and UI composition. + Bind Solid signals/JSX to feature APIs. + Avoid owning infrastructure and domain behavior. + +features/ + Application behavior and feature-level coordination. + Lifecycle ownership. + Feature model/state transformations. + +components/ui/ + Generic reusable visual primitives. + No ChatYX-specific domain knowledge. + +components// + Reusable presentation components for a specific feature. + +services/ + Network, browser, provider, storage, platform, and integration capabilities. + +config/ + Configuration schemas, normalization, parsing, serialization, and defaults. + +utils/ + Small genuinely reusable helpers. + Do not use `utils` as a dumping ground for domain logic. + +styles/ + Shared styling contracts and runtime style helpers where appropriate. +``` + +A service must not import a route or feature. + +Routes should not become service/application layers. + +--- + +## 4. Composition Roots and Lifecycle Ownership + +Every long-lived resource must have one clear owner. + +This includes: + +- sockets; +- timers; +- intervals; +- subscriptions; +- observers; +- DOM listeners; +- browser-global listeners; +- mutable caches; +- reconnect loops; +- preview runtimes; +- external provider sessions. + +If code starts a resource, ownership of cleanup must be obvious. + +Prefer: + +```text +create feature/application + -> create runtime/resources + -> expose API + -> destroy()/cleanup() +``` + +Avoid orphaned side effects or cleanup spread across unrelated modules. + +Browser-global custom events should be used only at actual browser/integration boundaries, not as an internal event bus between modules that can communicate directly. + +--- + +## 5. Refactoring Workflow + +For non-trivial refactors, do not immediately rewrite code. + +Use this order: + +1. Inspect. +2. Identify current responsibility boundaries. +3. Identify concrete problems. +4. Verify behavior with existing tests. +5. Add characterization tests when behavior is insufficiently protected. +6. Define the target responsibility boundary. +7. Refactor one concern at a time. +8. Run checks. +9. Remove dead compatibility code only after migration is complete. +10. Update architecture/design documentation when the contract changed. + +For repository-wide cleanup, create or update: + +- `documents/REFACTOR_AUDIT.md` +- `documents/REFACTOR_PLAN.md` + +The audit must use concrete file references and examples rather than subjective statements such as "this is spaghetti." + +--- + +## 6. How to Identify Spaghetti Code + +Do not label a file as spaghetti only because it is large. + +Evidence of architectural debt includes: + +- unrelated responsibilities changing for unrelated reasons; +- UI code performing network/storage/infrastructure work; +- repeated orchestration logic; +- unclear lifecycle ownership; +- hidden global state; +- duplicated normalization or parsing; +- cross-layer imports against the documented dependency direction; +- one component knowing about many unrelated providers/services; +- generic helpers containing feature/domain behavior; +- arbitrary string protocols where typed structures would be clearer; +- repeated visual patterns that bypass existing primitives; +- generated styling logic spread across unrelated layers; +- multiple libraries solving the same responsibility without a documented reason. + +When reporting architectural debt, always identify: + +```text +file +responsibility currently owned +responsibility that does not belong there +proposed owner +regression risk +tests protecting the change +``` + +--- + +## 7. Known High-Risk Areas + +Treat the following areas as architecture-sensitive: + +```text +src/routes/setup.tsx +src/components/chat/ChatMessage.tsx +src/components/chat/renderMessageContent.ts +src/components/setup/ +src/features/chat-overlay/ +src/services/chat/ +src/styles/ +src/utils/chat/ +src/utils/ui/ +``` + +This does not mean they must always be split. + +Inspect them carefully for responsibility mixing before modifying them. + +### Setup route + +`src/routes/setup.tsx` should trend toward being a UI composition root rather than an infrastructure implementation. + +When appropriate, move responsibilities such as: + +- persistence; +- browser capability detection; +- Twitch lookup; +- remote fetch orchestration; +- setup model/state operations; +- preview synchronization; +- serialization/mapping + +into explicit feature/application/service modules. + +Do not move code merely to reduce the size of the route. + +--- + +## 8. UI Component Architecture + +The project already has a reusable primitive layer: + +```text +src/components/ui/ +``` + +Use it. + +Before implementing a new control manually, check whether the project already has an appropriate primitive. + +Typical generic primitives include: + +- Button; +- Input; +- Textarea; +- Select; +- Switch; +- Slider; +- Label; +- Badge; +- Card; +- Separator; +- Collapsible; +- icon button patterns. + +The intended layering is: + +```text +generic primitive + ↓ +feature-specific wrapper/component + ↓ +route/page composition +``` + +Example: + +```text +components/ui/Button + ↓ +components/setup/SetupAction + ↓ +routes/setup +``` + +Avoid repeatedly implementing the same button/input/field structure directly inside routes. + +### `components/ui` + +Components in `components/ui` must: + +- be generic; +- not know about Twitch, YouTube, Kick, chat configuration, OBS, TTS, RTE, or setup sections; +- expose predictable variants; +- use the shared token system; +- preserve accessibility behavior; +- avoid feature-specific side effects. + +### Feature components + +Feature-specific presentation belongs in directories such as: + +```text +components/setup/ +components/chat/ +components/predictions/ +``` + +If a component contains application behavior instead of presentation, consider moving the behavior into the corresponding feature layer. + +--- + +## 9. Icon Policy + +Do not introduce arbitrary new icon systems. + +The project should converge on one general-purpose UI/system icon strategy. + +Current code may contain a mixture of: + +- `lucide-solid`; +- Hugeicons CSS/font classes; +- inline SVG; +- SVG files under `public/`; +- platform/brand logos. + +When touching icon-related code, classify the icon first. + +### UI/system icons + +Examples: + +- arrows; +- close; +- play/pause; +- settings; +- search; +- chevrons; +- status symbols. + +These should use one consistent general-purpose icon library unless there is a documented reason otherwise. + +Prefer tree-shakeable typed components over stringly-typed icon class names. + +Avoid patterns such as: + +```ts +icon: "some-icon-class-name" +``` + +when a typed icon component/key can provide compile-time safety. + +### Brand icons + +Brand assets are different from generic UI icons. + +Examples: + +- Twitch; +- YouTube; +- Kick; +- GitHub. + +Do not replace brand logos with visually similar generic icons. + +Centralize brand icon ownership so the same logo is not duplicated as: + +- inline SVG; +- a second inline SVG; +- a public SVG; +- an icon-font glyph + +at the same time without a real reason. + +### Inline SVG + +Inline SVG is acceptable when: + +- the graphic is custom; +- it is not provided by the approved UI icon library; +- it needs runtime geometry; +- it is a visualization rather than an icon; +- centralizing it would make the code less clear. + +Do not mechanically replace charts, sparklines, or custom graphics with an icon library. + +--- + +## 10. CSS and Design Tokens + +The project uses Tailwind and CSS custom properties. + +New styling should strengthen one coherent token system rather than create parallel systems. + +### Token categories + +Use design tokens for repeated design decisions. + +Examples: + +```text +surface colors +text colors +semantic action colors +borders +radii +spacing scale +font sizes +font weights +shadows +focus rings +motion durations +easing +control dimensions +z-index layers +``` + +Prefer semantic tokens. + +Good: + +```css +--surface-card +--text-muted +--border-default +--action-primary +--radius-md +--duration-fast +``` + +Avoid meaningless token names whose semantic role is unclear. + +### Component tokens + +Component-level custom properties are appropriate when the value is part of a stable component contract. + +Examples: + +```css +--chat-event-gap +--chat-event-radius +--setup-sidebar-width +--prediction-label-color +``` + +### Runtime values + +Do not turn runtime values into global static design tokens. + +Examples of valid runtime values: + +- user-selected colors; +- measured sizes; +- prediction percentages; +- emote dimensions; +- dynamic transforms; +- provider cosmetics; +- runtime layout calculations. + +Prefer passing runtime presentation values through a small set of CSS custom properties when that makes the relationship clearer. + +--- + +## 11. Hardcoded Visual Values + +Do not introduce repeated hardcoded presentation values when a shared semantic token exists. + +When touching CSS, inspect repeated: + +- hex/rgb/hsl colors; +- border radii; +- spacing; +- control heights; +- shadows; +- transitions; +- opacity; +- typography; +- z-index values. + +Move repeated design decisions to the shared token system when appropriate. + +Do not convert every numeric literal into a CSS variable. + +A value should become a token because it represents a reusable design decision, not because it is a number. + +--- + +## 12. Inline Styles + +Inline styles are not universally forbidden. + +They are appropriate for genuinely dynamic values such as: + +```tsx +style={{ transform: dynamicTransform() }} +style={{ color: userSelectedColor() }} +style={{ width: `${runtimeWidth()}px` }} +``` + +Static presentation values should normally live in reusable classes, variants, or tokens. + +Bad reason for inline style: + +> "It was faster to put the static design value here." + +Good reason: + +> "The value is produced dynamically at runtime and is part of the component API." + +--- + +## 13. `!important` + +Do not mechanically remove every `!important`. + +Classify existing usages as one of: + +```text +necessary boundary override +third-party/provider override +OBS/runtime compatibility requirement +legacy specificity workaround +unnecessary +``` + +`!important` may be justified for: + +- externally generated markup/styles; +- provider cosmetics; +- emote modifiers; +- OBS integration constraints; +- runtime user-selected presentation contracts. + +When it is only compensating for poor local specificity, prefer fixing the CSS architecture. + +Document non-obvious necessary overrides. + +--- + +## 14. Generated CSS + +Be cautious with CSS assembled as TypeScript strings. + +Before adding more generated CSS, ask whether the behavior can be represented as: + +```text +stable CSS rule ++ +runtime CSS custom property +``` + +Prefer: + +```ts +element.style.setProperty("--runtime-value", value); +``` + +with: + +```css +.some-class { + property: var(--runtime-value); +} +``` + +when this accurately represents the behavior. + +However, do not force third-party/provider cosmetics or genuinely dynamic external style payloads into static CSS when the generated form is the correct integration boundary. + +--- + +## 15. Library Responsibilities + +Every library should have one clear role. + +Current core responsibilities include: + +### SolidJS + +Use for: + +- components; +- signals; +- memos; +- effects; +- lifecycle; +- reactive composition. + +### Kobalte + +Use for accessible headless UI behavior where appropriate. + +Do not reimplement complex accessible primitive behavior manually when the existing library already provides it well. + +### SolidCN/project UI primitives + +Use for shared styled application primitives under `src/components/ui`. + +### Tailwind + +Use for: + +- layout; +- utility composition; +- reusable variants; +- integration with the token system. + +Do not add another general CSS framework. + +### class-variance-authority + +Use for meaningful reusable component variants. + +Do not create variant abstractions for one-off styling. + +### `clsx` / `tailwind-merge` + +Use through the project's shared `cn()` helper. + +### Icon library + +Use for general-purpose UI/system icons only. + +Brand assets and custom visualizations are separate concerns. + +--- + +## 16. Dependency Rules + +Before adding a dependency, answer: + +1. What responsibility does it own? +2. Is that responsibility already covered by an existing dependency? +3. Is the added runtime/bundle cost justified? +4. Does it work naturally with SolidJS? +5. Can the requirement be solved clearly without a new dependency? + +Do not add overlapping libraries without a strong reason. + +When refactoring, remove an old dependency only after all real usages are migrated and tests/builds pass. + +--- + +## 17. Utility Modules + +Do not create generic dumping grounds. + +Names such as: + +```text +utils +helpers +common +manager +misc +service +handler +``` + +should be treated with suspicion when they hide the real responsibility. + +Before putting code in `utils`, ask: + +- Is it a pure general helper? +- Is it chat domain logic? +- Is it setup feature logic? +- Is it configuration logic? +- Is it a browser/platform adapter? +- Is it presentation calculation? +- Is it parsing/normalization? + +Place the code with the responsibility it actually belongs to. + +Avoid moving cohesive code into many tiny files only to satisfy a directory pattern. + +--- + +## 18. Duplication + +Do not look only for identical text. + +Also inspect duplication of: + +- UI field structures; +- icon patterns; +- fetch wrappers; +- localStorage access; +- provider maps; +- normalization; +- error handling; +- section layouts; +- repeated CSS declarations; +- platform logos; +- configuration transformations. + +Distinguish: + +```text +accidental duplication +``` + +from: + +```text +coincidental similarity +``` + +Do not introduce an abstraction after seeing the same-looking code only once or twice unless the shared responsibility is real. + +--- + +## 19. Type Safety + +Prefer typed boundaries over string protocols. + +Examples: + +- typed icon identifiers/components; +- typed provider identifiers; +- discriminated unions for state; +- explicit feature APIs; +- validated configuration objects. + +Avoid widening types simply to make a refactor compile. + +Do not replace known domain types with `string`, `unknown`, or `any` unless the boundary truly requires it. + +Keep parsing/validation at external boundaries. + +--- + +## 20. Accessibility + +Refactors must preserve or improve accessibility. + +Do not regress: + +- labels; +- ARIA relationships; +- keyboard navigation; +- focus visibility; +- semantic buttons/links; +- disabled state behavior; +- readable contrast; +- screen-reader descriptions. + +Use accessible primitives from existing libraries rather than rebuilding them casually. + +Icons used purely for decoration should remain hidden from assistive technology. + +Icon-only actions must have an accessible name. + +--- + +## 21. OBS and Overlay Constraints + +The overlay is not a normal application page. + +Preserve: + +- transparent page/background behavior; +- source ordering; +- message wrapping contracts; +- emote modifier behavior; +- provider cosmetics; +- user-selected fonts/colors/strokes/shadows; +- overlay URL/config compatibility; +- horizontal/vertical layout behavior; +- event rendering semantics. + +Do not "clean up" CSS in a way that breaks browser-source rendering. + +Runtime presentation contracts documented in `documents/DESIGN.md` take precedence over aesthetic simplification. + +--- + +## 22. Tests and Regression Safety + +Before changing risky behavior, inspect the existing tests. + +Important coverage areas include: + +- URL/config parsing and serialization; +- setup import/export; +- setup templates; +- chat runtime lifecycle; +- message rendering; +- event rendering; +- emote modifiers; +- sender identity; +- provider adapters; +- preview behavior; +- RTE; +- TTS; +- SevenTV cosmetics; +- animation/layout calculations. + +If behavior is important but not clearly protected, add a characterization test before refactoring it. + +Never delete a valid test merely to make a refactor easier. + +--- + +## 23. Repository-Wide Refactor Audits + +When explicitly asked to assess architecture/technical debt, inspect the full repository and create evidence-based reports. + +Recommended scoring dimensions: + +```text +architecture coupling +responsibility mixing +UI duplication +CSS inconsistency +icon inconsistency +dependency overlap +global state / side effects +testability +naming / discoverability +overall technical debt +``` + +Use a 0–10 scale where: + +```text +0 = no meaningful problem found +10 = severe systemic problem +``` + +Every score must cite concrete examples. + +Reports should include: + +```text +top architectural hotspots +quick wins +highest-risk refactors +dependency responsibility map +icon inventory +CSS/token inventory +migration phases +regression risks +``` + +Do not invent quantitative precision that the code does not support. + +--- + +## 24. Preferred Refactor Order + +When doing a broad cleanup, prefer this order unless evidence supports a different sequence: + +### Phase 1 — Guard rails + +- inspect architecture; +- add missing characterization tests; +- document current behavior; +- make no unnecessary visual changes. + +### Phase 2 — Design tokens + +- consolidate semantic tokens; +- remove repeated static design literals; +- keep runtime values dynamic. + +### Phase 3 — Icon normalization + +- establish one UI/system icon strategy; +- centralize brand icons; +- remove actual duplication; +- remove unused icon infrastructure only after migration. + +### Phase 4 — UI primitives + +- reuse `components/ui`; +- extract genuine repeated controls/layout primitives; +- keep feature-specific wrappers outside `components/ui`. + +### Phase 5 — Setup decomposition + +- reduce infrastructure and application logic inside `src/routes/setup.tsx`; +- extract responsibilities by reason to change; +- preserve setup behavior and URL/config contracts. + +### Phase 6 — Chat presentation cleanup + +- review `ChatMessage`; +- review `renderMessageContent`; +- simplify presentation responsibilities without breaking emotes/events/cosmetics. + +### Phase 7 — Dependency/dead-code cleanup + +- remove obsolete libraries; +- remove duplicate assets; +- remove dead adapters/styles; +- verify the production build. + +### Phase 8 — Documentation and enforcement + +- update architecture/design documents; +- tighten boundaries where useful; +- run the full verification suite. + +The repository must remain usable after every phase. + +--- + +## 25. Definition of Done + +A refactor is not complete merely because TypeScript compiles. + +At minimum, run the relevant checks. + +For broad changes, run: + +```bash +bun run lint +bun run typecheck +bun test ./tests +bun run build +``` + +Prefer the complete project check: + +```bash +bun run check +``` + +Before declaring completion, verify that the change did not introduce: + +- new cyclic dependencies; +- duplicate UI primitives; +- a second icon system; +- unnecessary dependencies; +- new giant generic `utils`; +- hidden side effects; +- broken lifecycle cleanup; +- duplicated platform SVGs; +- static design literals that bypass established tokens; +- accessibility regressions; +- OBS regressions; +- config/URL contract changes. + +--- + +## 26. Commit Messages + +Use [Conventional Commits](https://www.conventionalcommits.org/) for every commit. + +Follow the project rules in `CONTRIBUTING.md`. + +Format: + +```text +(): +``` + +Use: + +- an imperative description; +- lower-case text; +- no trailing period; +- a logical project area as scope; +- `!` and a `BREAKING CHANGE:` footer for actual breaking changes. + +Examples: + +```text +refactor(ui): consolidate icon usage +refactor(styles): introduce semantic design tokens +refactor(setup): extract persistence adapter +refactor(setup): extract twitch channel lookup +refactor(chat): separate event presentation +chore(deps): remove unused icon assets +docs(architecture): document ui dependency boundaries +test(setup): characterize preview synchronization +``` + +Do not combine unrelated architectural migrations in one commit. + +Run `bun run check` before committing when practical. + +--- + +## 27. Things Agents Must Not Do + +Do not: + +- rewrite the application from scratch; +- replace SolidJS without an explicit project decision; +- add a state-management framework merely to make code look structured; +- add another CSS framework; +- split every large file into tiny files; +- create abstractions only to reduce duplication counts; +- replace all inline styles mechanically; +- remove all `!important` mechanically; +- convert every numeric value into a CSS variable; +- replace brand assets with generic icons; +- break public URL/config behavior; +- break OBS compatibility; +- hide infrastructure inside generic helpers; +- add a service locator or DI container; +- silence type errors with broad `any`; +- delete tests to simplify a refactor; +- change visual behavior during a structural refactor unless that change is explicitly part of the task. + +--- + +## 28. Final Rule + +Leave the codebase easier to understand than you found it. + +A good change should make at least one of these clearer: + +- who owns a behavior; +- where a dependency belongs; +- which layer is responsible; +- which component should be reused; +- which token represents a design decision; +- which library owns a capability; +- how the behavior is tested; +- how the resource is cleaned up. + +If a refactor only moves code around without improving one of those, reconsider whether it is useful. diff --git a/documents/REFACTORING_FOLLOWUP.md b/documents/REFACTORING_FOLLOWUP.md new file mode 100644 index 0000000..e3a79df --- /dev/null +++ b/documents/REFACTORING_FOLLOWUP.md @@ -0,0 +1,2188 @@ +# ChatYX Refactoring Follow-up + +> Status: required completion supplement +> Applies after: PR #15 / repository-wide cleanup work +> Scope: finish the architectural cleanup that remains after the first refactor pass +> Explicit exclusion: **DO NOT migrate to Panda CSS in this follow-up** + +--- + +# 1. Purpose + +This document is an **additive follow-up** to the existing ChatYX refactoring documents. + +It does not replace them. + +The previous refactor already completed a large amount of useful cleanup: + +- setup responsibilities were extracted from the route; +- the overlay URL projection became testable; +- runtime styling was moved toward stable CSS + custom properties; +- the Hugeicons font was removed; +- Lucide was removed; +- typed icon data replaced `hgi-*` string classes; +- lifecycle ownership improved; +- tests were added around setup projection, preview synchronization, style teardown and singleton reset; +- dead code and duplicated helpers were removed. + +The next agent must **not restart the refactor from scratch**. + +The job now is to finish the remaining architectural issues, remove inconsistencies introduced or left behind by the first pass, and leave the repository in a stable state for a future styling-system migration. + +Panda CSS is intentionally deferred. + +--- + +# 2. Mandatory Read Order + +Before changing code, read these files in this order: + +```text +1. AGENTS.md +2. documents/ARCHITECTURE.md +3. documents/DESIGN.md +4. documents/REFACTORING.md +5. documents/REFACTOR_AUDIT.md +6. documents/REFACTOR_PLAN.md +7. documents/REFACTORING_FOLLOWUP.md +8. relevant tests for every area being changed +``` + +Then inspect the current repository tree. + +Do not assume the plan documents are perfectly current. + +The source code and tests may have moved forward since a plan outcome note was written. + +--- + +# 3. Precedence Rules + +When guidance conflicts, use this order: + +```text +1. current externally observable behavior and public contracts +2. passing characterization tests that intentionally pin those contracts +3. AGENTS.md +4. documents/ARCHITECTURE.md +5. documents/DESIGN.md +6. this REFACTORING_FOLLOWUP.md for explicitly updated follow-up items +7. documents/REFACTOR_PLAN.md +8. older audit observations +``` + +This document overrides older refactor notes **only where it explicitly says that the previous information is stale or incomplete**. + +Do not reinterpret this as permission to ignore architecture documentation. + +--- + +# 4. Hard Constraint: No Panda CSS Yet + +This follow-up must not introduce Panda CSS. + +Do not add: + +```text +@pandacss/dev +panda.config.ts +styled-system/ +Panda recipes +Panda semantic-token configuration +``` + +Do not remove Tailwind merely to prepare for Panda. + +Do not migrate Tailwind classes into a temporary second styling abstraction. + +For this phase, keep the existing styling stack operational: + +```text +Tailwind CSS +CVA where currently justified +cn() +Kobalte +existing CSS custom properties +plain CSS where already appropriate +``` + +The purpose of this follow-up is to make the architecture ready for a later styling-system migration without combining two large migrations into one review surface. + +--- + +# 5. Primary Completion Goals + +The follow-up is complete when all of these are addressed: + +```text +A. root AGENTS.md reflects the real architecture and mandatory rules +B. Hugeicons uses the official SolidJS renderer +C. feature -> component dependency leaks are removed +D. async asset loading cannot repopulate singleton state after runtime teardown +E. application/runtime destruction is safe and idempotent +F. preview teardown owns the global/shared state it enables +G. static setup document styling no longer hardcodes design decisions in lifecycle code +H. architecture/design/refactor docs match the final source tree +I. no stale claims remain in the refactor documentation +J. all checks pass +``` + +Do these incrementally. + +Do not combine all items into one giant commit. + +--- + +# 6. First Task: Fix the Agent Documentation + +## Problem + +The repository root `AGENTS.md` still contains only the older commit-message instructions. + +The detailed architectural rules currently live under `documents/REFACTORING.md`. + +That is not enough. + +Agents commonly discover root-level instructions automatically. If the real rules are only in a document that must be discovered manually, future automated changes can regress the architecture. + +## Required outcome + +Rewrite root: + +```text +AGENTS.md +``` + +so it contains the current engineering rules. + +It should cover at minimum: + +```text +architecture direction +route responsibilities +feature responsibilities +service boundaries +lifecycle ownership +UI primitive boundaries +Hugeicons policy +brand icon policy +CSS custom-property/runtime-value rules +Tailwind status +dependency rules +utility-module rules +accessibility +OBS constraints +testing requirements +Conventional Commits +``` + +The root file must also explicitly require repository-wide refactor agents to read: + +```text +documents/REFACTORING.md +documents/REFACTORING_FOLLOWUP.md +``` + +Do not make `AGENTS.md` a one-line pointer. + +The important rules must remain visible directly in the root file. + +## Preserve + +Keep the existing Conventional Commit rules. + +Keep: + +```text +bun run check +``` + +as the preferred full validation command. + +--- + +# 7. Update: Hugeicons Official SolidJS Renderer Now Exists + +## Previous state + +The first cleanup pass introduced: + +```text +@hugeicons/core-free-icons +src/components/ui/icon.tsx +``` + +and implemented a custom renderer for Hugeicons `IconSvgObject` data. + +That decision was based on the statement that no official SolidJS renderer existed. + +That statement is now stale. + +## Current official package + +Hugeicons now provides: + +```text +@hugeicons/solid-js +``` + +The official SolidJS documentation uses: + +```text +@hugeicons/solid-js ++ +@hugeicons/core-free-icons +``` + +with the `HugeiconsIcon` component. + +Official reference: + +```text +https://hugeicons.com/docs/integrations/solid-js/overview +https://hugeicons.com/docs/integrations/solid-js/wrapper +https://hugeicons.com/docs/integrations/solid-js/best-practices +``` + +## Required change + +Add: + +```text +@hugeicons/solid-js +``` + +Keep: + +```text +@hugeicons/core-free-icons +``` + +Remove the custom SVG payload rendering logic from: + +```text +src/components/ui/icon.tsx +``` + +The local `Icon` primitive may remain, but it should become a **thin project wrapper** around `HugeiconsIcon`. + +Target responsibility: + +```text +HugeiconsIcon + = vendor SVG rendering + +ChatYX Icon + = ChatYX defaults and accessibility convention +``` + +The project wrapper may own: + +```text +default size +default stroke width +currentColor +class forwarding +decorative accessibility defaults +``` + +It must NOT continue owning: + +```text +manual SVG tag creation +manual attribute kebab-casing +manual Hugeicons payload traversal +vendor-specific SVG normalization +``` + +## Suggested conceptual implementation + +Do not copy this blindly; verify the current official API first. + +```tsx +import { HugeiconsIcon } from "@hugeicons/solid-js"; +import type { IconSvgObject } from "@hugeicons/core-free-icons"; + +export function Icon(props: IconProps) { + return ( + + ); +} +``` + +Preserve existing accessible behavior. + +## Import policy + +Prefer specific icon imports that keep bundling predictable. + +Do not use: + +```ts +import * as Icons from "@hugeicons/core-free-icons"; +``` + +Do not introduce runtime string lookup for icon names. + +Typed icon values remain the contract. + +## Exit criteria + +Repository contains: + +```text +@hugeicons/solid-js +@hugeicons/core-free-icons +``` + +and no project-owned Hugeicons SVG renderer logic. + +All icon call sites still use typed icons. + +No icon font returns. + +No Lucide dependency returns. + +--- + +# 8. Preserve the Current Icon Architecture + +The following decisions from the first refactor remain valid. + +## General UI icons + +Use Hugeicons. + +Examples: + +```text +search +close +play +pause +arrows +navigation +settings +database +colors +bot +voice +status +``` + +## Brand icons + +Do not force streaming-platform brands through generic Hugeicons icons. + +Keep canonical ownership for: + +```text +Twitch +YouTube +Kick +7TV +GitHub +``` + +If `PlatformGlyph.tsx` is the canonical monochrome owner, preserve that model unless there is a concrete reason to improve it. + +## Bespoke graphics + +Do not replace: + +```text +sparklines +chat event artwork +reply visuals +role artwork +custom visualizations +``` + +with generic UI icons merely for consistency. + +Consistency means one system **per responsibility**, not one renderer for every drawing in the application. + +--- + +# 9. Fix the Feature → Component Dependency Leak + +## Current issue + +The setup search feature currently imports a type from a presentation component: + +```text +src/features/setup/settingsSearch.ts + ↓ +src/components/setup/SetupLayout.tsx +``` + +Specifically: + +```text +SetupSectionId +``` + +This reverses the intended dependency direction. + +A feature-level module should not depend on a component module only to access a domain/UI-contract type. + +## Required outcome + +Move setup section identity/configuration to a neutral setup contract. + +Possible targets: + +```text +src/features/setup/model/setupSections.ts +``` + +or: + +```text +src/config/setupSections.ts +``` + +Choose based on actual responsibility. + +A good target should allow: + +```text +route +component +feature search +``` + +to import the same contract without importing each other. + +Example responsibility: + +```ts +export type SetupSectionId = + | "import" + | "appearance" + | ... +``` + +Potentially the navigation descriptors may also belong there if they are not presentation-specific. + +Do not move icon components into a model/config module if doing so would mix visual dependencies into the domain contract. + +Separate: + +```text +section identity +``` + +from: + +```text +section presentation/icon metadata +``` + +when appropriate. + +## Exit criteria + +No file under: + +```text +src/features/ +``` + +imports: + +```text +src/components/ +``` + +unless the architecture explicitly documents and justifies that dependency. + +Search the full repository, not only setup. + +--- + +# 10. Audit All Dependency Direction Violations + +Do a fresh import-boundary audit. + +At minimum search for: + +```text +services -> features +services -> routes +features -> routes +features -> components +config -> components +utils -> routes +``` + +Do not mechanically forbid every `config -> services` edge if the current architecture intentionally permits a dependency-free capability leaf. + +The first refactor explicitly allows: + +```text +config/setupTemplates + ↓ +services/storage/setupStorage +``` + +because storage is a leaf capability. + +Evaluate dependency direction by responsibility and cycle risk. + +Record any intentional exceptions in `ARCHITECTURE.md`. + +--- + +# 11. Critical Lifecycle Follow-up: Async Loads After Teardown + +## Why this matters + +The current live runtime now calls: + +```text +badgeService.reset() +emoteService.reset() +``` + +during teardown. + +That is useful, but reset alone does not cancel requests that are already in flight. + +The runtime also starts asynchronous background work such as: + +```text +assetLoader.loadEmotes(...) +assetLoader.loadDeferredAssets(...) +shared-channel loads +badge loads +cosmetic loads +``` + +Some of these operations mutate singleton services after their promises resolve. + +A possible race is: + +```text +runtime A starts async load +↓ +runtime A is destroyed +↓ +singleton reset runs +↓ +runtime B starts +↓ +old runtime A request resolves +↓ +old result mutates the shared singleton again +``` + +If this can happen, teardown is not a true ownership boundary. + +## Required work + +Do not assume this bug exists in every service. + +Prove or disprove it. + +Audit: + +```text +src/services/chat/assets/emoteService.ts +src/services/badges/badgeService.ts +src/services/chat/runtime/chatAssetLoader.ts +src/services/chat/seven-tv/* +src/services/chat/rte/* +``` + +Identify every async operation that: + +1. begins under a runtime; +2. can survive runtime destruction; +3. mutates shared/global state after awaiting. + +## Acceptable solutions + +Choose the smallest correct ownership model. + +Possible patterns: + +### Generation token + +```text +service generation increments on reset +async load captures generation +mutation is allowed only if generation still matches +``` + +### AbortController + +Use when the underlying request path supports true cancellation cleanly. + +### Runtime-owned loader instance + +Prefer instance ownership if a singleton is not actually needed. + +### Scoped commit token + +Load data into a local structure, then commit only if the owning runtime is still current. + +Do not add cancellation abstractions where the existing call cannot outlive a runtime. + +## Tests required + +Add deferred-promise regression tests. + +A useful test shape: + +```text +start load for old channel +hold promise unresolved +reset/destroy +start new runtime or leave store empty +resolve old promise +assert old data was NOT committed +``` + +Test actual mutation boundaries, not only runtime callbacks. + +`initializationGeneration` in `LiveChatRuntime` is not enough if a service mutates its own singleton before the runtime sees the promise completion. + +## Exit criteria + +Old asynchronous work cannot repopulate shared asset state after its owner is destroyed/reset. + +--- + +# 12. Verify Badge Reset Semantics + +The existing tests verify: + +```text +runtime-added badge data is removed +built-in fallback badges survive reset +``` + +Keep that behavior. + +But also inspect: + +```text +in-flight Twitch badge loads +in-flight third-party badge loads +in-flight user badge loads +``` + +If old requests can commit after reset, add the same generation/cancellation protection described above. + +Do not remove fallback badges during reset unless the service is redesigned so they are immutable constants outside the mutable store. + +--- + +# 13. Verify Emote Reset Semantics + +The current `emoteService.reset()` clears: + +```text +global emotes +channel emotes +personal emotes +current channel +load bookkeeping +7TV visibility bookkeeping +``` + +That is a reasonable clean slate. + +But verify in-flight behavior for: + +```text +global emotes +channel emotes +shared-channel emotes +cheer emotes +personal emotes +7TV reload +``` + +A cleared promise registry does not automatically prevent the original promise body from mutating the store later. + +Add a regression test for at least one representative old-runtime load. + +If different mutation paths use different commit mechanisms, test more than one. + +--- + +# 14. ChatOverlayApplication Destruction Must Be Safe Before Start Completes + +## Current issue to review + +`ChatOverlayApplication` constructs: + +```text +runtime +predictions runtime +``` + +in its constructor. + +Its current destroy logic returns immediately when: + +```text +started === false +``` + +Conceptually: + +```ts +destroy() { + if (!this.started) return; + ... +} +``` + +This means an object can own constructed resources even though `destroy()` refuses to destroy them when `start()` was never called. + +The earlier refactor plan explicitly identified this lifecycle case but did not fully characterize it. + +## Required outcome + +Define the ownership contract. + +Preferred contract: + +```text +if construction creates an owned disposable resource, +destroy() must be safe whether or not start() was called +``` + +`destroy()` should also be idempotent. + +## Required tests + +Add tests covering: + +```text +construct -> destroy +construct -> start -> destroy +construct -> start -> destroy -> destroy +start called twice +destroy while initialize is still pending +``` + +Do not require a full browser DOM if dependencies can be injected. + +The application already has injectable factories; use them. + +## Important + +If the final correct design is instead: + +```text +do not construct runtime/predictions until start() +``` + +that is also acceptable, but do not perform a broad lifecycle rewrite without tests. + +--- + +# 15. Destroy During Pending Initialization + +`application.start()` awaits: + +```text +runtime.initialize() +``` + +while the route cleanup may run before initialization finishes. + +The live runtime already has an initialization generation mechanism. + +Audit the complete path: + +```text +ChatOverlayApplication +LiveChatRuntime +PreviewRuntime +PredictionController +``` + +Ensure: + +- no callback publishes stale state after destroy; +- no reconnect starts after destroy; +- no delayed timer is installed after destroy; +- no asset result from the old runtime leaks into the next one; +- predictions do not continue after application destruction. + +Add focused tests at the narrowest responsible layer. + +--- + +# 16. Preview Runtime Teardown Audit + +The preview runtime currently owns: + +```text +message interval +render timer +preview styles +presentation service +proxy state configuration +preview-loaded asset work +``` + +Audit whether `destroy()` fully releases everything `initialize()` enabled. + +Explicitly review: + +```text +setProxyEnabled(...) +badge state +emote state +7TV cosmetics state +mention style state +preview stylesheet/root attributes +presentation layout +``` + +Do not automatically reset a process-wide singleton if it is intentionally shared by another live owner in the same document. + +Instead, define the ownership model. + +If preview is the only owner in its document, teardown should leave the document in a clean state. + +Add tests where practical. + +--- + +# 17. RTE Proxy Global State Ownership + +The RTE proxy uses shared mutable state. + +Live runtime teardown now calls: + +```text +setRteProxyEnabled(false) +``` + +Audit all owners: + +```text +LiveChatRuntime +PreviewRuntime +tests +any other route/runtime +``` + +Answer: + +```text +Can two owners coexist in one document? +Who is allowed to disable the flag? +Can an old runtime disable it after a new runtime enabled it? +``` + +If concurrent ownership is impossible by design, document that. + +If concurrent ownership is possible, a plain process-wide boolean is not a sufficient ownership model. + +Do not introduce reference counting unless concurrency actually exists. + +--- + +# 18. Setup Document Styling: Remove Static Design Decisions From Lifecycle Code + +## Current issue + +`src/features/setup/setupDocument.ts` contains: + +```ts +const SETUP_BACKGROUND = "#09090b"; +``` + +The module's responsibility is document lifecycle: + +```text +capture styles +lock scrolling +restore styles +watch reduced motion +``` + +A product design color does not belong in that lifecycle module. + +## Required outcome + +Make the document helper control state/ownership, not visual design values. + +Preferred approaches: + +### Class or data attribute + +The lifecycle helper applies something like: + +```text +data-setup-document +``` + +or a class, and CSS owns: + +```text +background +overflow behavior where appropriate +``` + +### Existing semantic variable + +If an inline value is genuinely required, use the existing semantic token/custom property rather than a raw color literal. + +Avoid reading a CSS variable and copying its computed value back into inline styles unless there is a concrete reason. + +## Preserve + +The helper must still restore previous document state exactly when released. + +Do not introduce route-change background flashes. + +--- + +# 19. Setup Route Completion + +The first pass reduced `setup.tsx` substantially. + +Do not chase an arbitrary line-count target. + +Review the remaining route and ask only: + +```text +Does this code represent route composition? +Or is it still an independently testable responsibility? +``` + +Reasonable route responsibilities include: + +```text +signals +effects wiring feature APIs +section composition +binding callbacks +route-level visibility +``` + +Potential remaining extraction candidates should only move if they have a clear separate contract. + +Do not create: + +```text +setupHelpers.ts +setupManager.ts +setupUtils.ts +``` + +as dumping grounds. + +Do not split JSX into components solely because the route remains long. + +--- + +# 20. Setup Section Descriptors + +The first pass extracted several section row descriptors. + +Review whether these descriptors now have a clear dependency structure. + +Preferred: + +```text +pure/typed descriptor definitions ++ +feature-level callbacks/state supplied explicitly ++ +presentation rendered by setup components +``` + +Avoid descriptor modules becoming hidden mini-routes that import many unrelated services. + +No new service access should be added to section descriptor files. + +--- + +# 21. `renderMessageContent.ts`: Do Not Split Just for Organization + +The previous plan left a physical split of `renderMessageContent.ts` outstanding. + +Do not treat that as mandatory. + +The important architectural result already achieved is that rendering no longer requires a network client directly. + +Only split the file if the current code demonstrates real independent responsibilities such as: + +```text +tokenization +assembly +DOM binding +provider-specific transformation +``` + +and the split improves: + +```text +testability +dependency direction +change isolation +``` + +Do not perform a wide mechanical file split merely to mark an old plan item complete. + +If no strong benefit is found, explicitly mark the plan item as intentionally closed without further code movement. + +--- + +# 22. Layout Manager Location + +The previous plan notes that moving the layout manager under: + +```text +features/chat-overlay +``` + +would create the wrong dependency direction because a service currently owns/uses it. + +Do not force that move. + +Instead, determine the actual responsibility. + +Questions: + +```text +Is LayoutManager generic presentation infrastructure? +Is it chat-specific runtime infrastructure? +Does it need feature knowledge? +``` + +Move only if the dependency graph becomes better. + +If its current location is merely imperfect naming but dependency direction is correct, documentation may be the correct fix. + +--- + +# 23. Documentation Consistency Pass + +The first refactor changed code faster than some prose was updated. + +Do a final documentation audit after code changes. + +## Known stale item: Hugeicons + +Remove claims such as: + +```text +"There is no official Hugeicons package for Solid" +``` + +Update to the official: + +```text +@hugeicons/solid-js +``` + +architecture. + +## Known stale item: generated chat preset CSS + +`DESIGN.md` currently contains language suggesting `chatStyles.ts` still generates preset-dependent style rules. + +But the current branch already moved those presets to: + +```text +getOverlayStyleVariables() ++ +chat.css +``` + +Update the document to describe the actual final implementation. + +Do not document completed work as "remaining." + +## Audit all status words + +Search docs for: + +```text +remaining +outstanding +TODO +in progress +partly done +blocked +known remainder +still emits +does not exist +``` + +Verify each against the actual branch. + +--- + +# 24. REFACTOR_PLAN Finalization + +`documents/REFACTOR_PLAN.md` should remain useful as a historical execution record. + +Do not delete useful outcomes. + +But update it so a future agent cannot misread stale implementation notes as current truth. + +For every remaining open item: + +```text +complete it +or +mark it intentionally deferred with a precise reason +or +mark it no longer necessary +``` + +No ambiguous "in progress" should remain when this follow-up closes. + +--- + +# 25. REFACTOR_AUDIT Finalization + +Do not rewrite the audit to pretend the original problems never existed. + +Instead add a closing section: + +```text +## Post-refactor state +``` + +For each high-impact original issue, record: + +```text +original problem +final owner +status +remaining debt +``` + +This preserves the value of the audit as history. + +If scores are updated, keep both: + +```text +before +after +``` + +rather than replacing the original score. + +--- + +# 26. ARCHITECTURE.md Final State + +After the code is final, `ARCHITECTURE.md` must describe reality. + +Ensure it covers: + +```text +setup feature boundaries +overlay lifecycle ownership +asset loading ownership +storage boundary +URL/config projection +icon system +brand assets +runtime style publication +plain/generated CSS boundaries +``` + +Do not describe Panda. + +Do not describe Ark as a chosen standard unless it has actually been adopted. + +Kobalte remains valid in this follow-up. + +--- + +# 27. DESIGN.md Final State + +Document the current styling model accurately. + +The target **for this follow-up** is: + +```text +Tailwind for existing application utility styling +components/ui for reusable primitives +CSS custom properties for overlay runtime values +plain CSS for overlay/provider/browser integration boundaries +Hugeicons Solid renderer for generic UI icons +brand-owned SVG/glyph components for brands +``` + +Do not claim Panda tokens exist. + +Do not write aspirational future-stack documentation as current state. + +A future Panda migration may replace part of this model later. + +--- + +# 28. Tailwind: Stabilize, Do Not Expand + +Panda is deferred, but this does not mean new Tailwind spaghetti is acceptable. + +During this follow-up: + +- reuse existing primitives; +- avoid giant repeated utility strings; +- extract a component/variant only when repetition is real; +- do not introduce a new Tailwind abstraction layer; +- do not migrate existing plain overlay CSS to Tailwind; +- do not add Tailwind to provider/runtime-generated markup. + +No Tailwind removal is required. + +--- + +# 29. CVA / `tailwind-merge` / `clsx` + +Keep them if they still have real usages. + +Do not remove a dependency just because a future Panda migration may replace it. + +Do not add new CVA recipes unless a reusable variant contract actually exists. + +Use the project's `cn()` helper where class composition is appropriate. + +--- + +# 30. Kobalte + +Do not migrate to Ark UI in this follow-up. + +Kobalte migration is a separate behavioral migration. + +Keep existing accessible primitives stable. + +Do not reimplement Kobalte behavior manually. + +If you encounter a Kobalte issue while finishing this work, fix the smallest issue necessary and document it. + +--- + +# 31. CSS Custom Property Contract + +Preserve the useful direction established by PR #15: + +```text +static CSS owns rule shape +runtime publishes values +``` + +For overlay configuration: + +```text +getOverlayStyleVariables() + ↓ +OverlayStyleManager + ↓ +document root custom properties + ↓ +chat.css +``` + +This is the preferred model. + +Do not revert to one generated stylesheet per preset. + +--- + +# 32. Generated CSS Boundary + +Generated CSS remains valid only where the rule shape genuinely comes from runtime/provider data. + +Examples that may remain: + +```text +7TV paint/cosmetics +emote modifiers +provider-controlled visual data +animation vocabulary if static CSS cannot represent the required contract cleanly +``` + +Before keeping generated CSS, answer: + +```text +Could stable CSS + custom properties express this? +``` + +If yes, prefer stable CSS. + +Do not force dynamic provider data into an unsuitable token system. + +--- + +# 33. Animation Stylesheet Ownership + +The first refactor fixed duplicate animation stylesheets and later fixed a teardown regression. + +Keep tests that ensure: + +```text +one #chat-animations stylesheet +repeated apply does not accumulate duplicates +teardown removes it +``` + +Audit whether animation CSS itself can safely remain in the current runtime module. + +Do not move it solely for file organization. + +--- + +# 34. Overlay Style Manager Contract + +Preserve: + +```text +apply + → publish all overlay properties/attributes +cleanup + → remove every property/attribute/style resource it owns +``` + +`apply()` should be safe when called repeatedly. + +`cleanup()` should be safe when called repeatedly. + +A later `apply()` after cleanup should reconstruct a correct state. + +Add or retain tests for these properties. + +--- + +# 35. Browser Global State Audit + +Search for mutable browser-global state: + +```text +documentElement attributes +root inline custom properties +global style elements +window listeners +matchMedia listeners +localStorage +postMessage listeners +singleton proxy flags +``` + +For each, identify: + +```text +owner +installation point +cleanup point +whether cleanup is idempotent +whether an old owner can affect a new owner +``` + +Do not create a generic global-state manager. + +Fix ownership at the narrowest responsible layer. + +--- + +# 36. Singleton Audit + +Search for exported singleton service instances. + +Examples include: + +```text +emoteService +badgeService +mentionStyleService +sevenTVCosmeticsService +chatFeatureIntegration +network/shared proxy state +``` + +For each singleton answer: + +```text +Why is this global? +What state is channel-specific? +What resets on runtime teardown? +Can in-flight work mutate it after reset? +Can live and preview owners coexist? +``` + +Do not convert every singleton into dependency injection as a style exercise. + +Change only where lifecycle correctness requires it. + +--- + +# 37. Test Strategy + +Prefer characterization tests at the layer that owns the behavior. + +Do not require a full DOM environment when a dependency seam already exists. + +Use: + +```text +injected factories +deferred promises +minimal window/document shims +pure projection tests +service-level tests +``` + +Avoid brittle snapshots of generated class names. + +Test: + +```text +observable behavior +lifecycle +public URL contracts +mutation ownership +cleanup +``` + +--- + +# 38. Required New Regression Tests + +At minimum, add tests for the following if they are not already present after the agent re-scans the branch. + +## Application lifecycle + +```text +destroy before start +double destroy +double start +destroy while runtime initialize is pending +``` + +## Async singleton mutation + +```text +old emote load cannot commit after reset +old badge load cannot commit after reset +``` + +or equivalent tests at the actual mutation boundary. + +## Icon wrapper + +Only test project behavior if meaningful. + +Do not test Hugeicons vendor internals. + +## Dependency direction + +A custom architecture test is optional. + +Prefer simple import cleanup over introducing a complex static-analysis framework for one issue. + +--- + +# 39. Browser Verification + +After structural tests pass, perform browser checks for: + +```text +setup page +preview +live/demo switching +settings search +channel persistence +overlay URL generation +chat preview navigation +navigation icons +brand glyphs +reduced motion +route cleanup +``` + +Overlay checks: + +```text +normal message +reply +badges +events +gigantified emotes +emote modifiers +custom font/stroke/shadow +horizontal layout +transparent background +animations +``` + +Use automated browser tooling if available outside the project. + +Do not add a browser-test dependency merely for this follow-up unless the project explicitly wants one. + +--- + +# 40. Bundle Verification + +After switching to `@hugeicons/solid-js`, compare production output. + +Do not assume the official renderer is automatically smaller. + +Verify: + +```text +build succeeds +icons tree-shake as expected +no icon font returns +no full icon barrel is accidentally bundled +``` + +If importing through package subpaths is still materially better and supported, keep that pattern. + +If official Hugeicons best practices recommend named imports and the bundler tree-shakes them correctly, use the simplest verified approach. + +Measure rather than speculate. + +--- + +# 41. Accessibility Verification + +Icon migration must preserve: + +```text +decorative icons hidden from assistive technology +icon-only controls have labels +buttons remain semantic buttons +focus remains visible +radiogroups remain keyboard/AT understandable +disabled state is preserved +``` + +Do not expose duplicate names from both an icon and its surrounding labelled control. + +--- + +# 42. No Visual Redesign + +This follow-up is structural. + +Do not change: + +```text +spacing +colors +typography +component density +icon meaning +preview layout +overlay visual language +``` + +unless necessary to fix a bug. + +If a tiny visual difference is unavoidable due to switching to the official Hugeicons renderer, identify and verify it explicitly. + +Prefer matching the existing Hugeicons glyph appearance. + +--- + +# 43. Public Contracts Must Not Change + +Do not change: + +```text +chat URL parameter names +URL defaults +setup import/export meaning +OBS source compatibility +preview message protocol +platform source configuration +stored setup keys +``` + +without an explicit breaking-change task. + +Run the existing round-trip tests. + +--- + +# 44. Storage Contract + +Keep: + +```text +chatyx.setup.config.v1 +chatyx.setup.twitchChannel +chatyx.setup.previewStageBackdrop +chatyx.setup.previewStageColor +chatyx.setup.templates.v1 +``` + +unless a deliberate migration is added. + +Do not rename storage keys as cleanup. + +If storage schema changes, add migration and tests. + +--- + +# 45. Commit Strategy + +Suggested order: + +```text +docs(agents): publish refactor rules at repository root + +refactor(icons): use official hugeicons solid renderer + +refactor(setup): move section identity out of presentation +refactor(architecture): remove remaining layer inversions + +test(chat): characterize application destruction +fix(chat): make application teardown idempotent + +test(chat): reproduce stale emote load after reset +fix(chat): prevent stale emote commits after teardown + +test(chat): reproduce stale badge load after reset +fix(chat): prevent stale badge commits after teardown + +refactor(setup): move document appearance back to css + +docs(refactor): reconcile final architecture and outcomes +``` + +Actual commits should follow actual findings. + +If an investigated race is proven impossible, do not create fake production work. + +Document the proof and add a test if it is valuable. + +--- + +# 46. Do Not Reopen Completed Work Without Evidence + +The previous cleanup already completed many changes. + +Do not redo: + +```text +setup storage extraction +form coercion extraction +setup config projection +preview synchronizer extraction +settings search extraction +icon-font deletion +Lucide removal +chat style-variable conversion +animation stylesheet de-duplication +dead legacy token removal +``` + +unless a concrete bug is discovered. + +Refactoring the refactor is not a goal. + +--- + +# 47. Completion Report + +When the follow-up is done, append a final section to this document: + +```md +# Completion Report + +## Completed items + +## Items investigated and intentionally unchanged + +## Deferred items + +## New tests + +## Dependency changes + +## Bundle/build impact + +## Final verification +``` + +For every intentionally unchanged item, explain why no code change was necessary. + +--- + +# 48. Required Final Searches + +Before declaring completion, search the full repository for: + +```text +lucide-solid +hgi- +hgi-stroke +@hugeicons/solid-js +HugeiconsIcon +IconSvgObject +TODO +HACK +in progress +partly done +remaining exception +no official Hugeicons +SETUP_BACKGROUND +from "~/components +``` + +Interpret results, do not mechanically force zero results for generic searches. + +Specific expectations: + +```text +lucide-solid → zero +hgi-* UI classes → zero +"no official Hugeicons..." stale docs → zero +@hugeicons/solid-js → expected usage +``` + +Also audit imports under `src/features` manually for presentation-layer dependencies. + +--- + +# 49. Required Final Verification + +Run: + +```bash +bun run lint +bun run typecheck +bun test ./tests +bun run build +``` + +Then: + +```bash +bun run check +``` + +If `bun run check` duplicates the individual commands, that is fine. + +Record exact results in the completion report. + +Do not claim green status without running the commands on the final tree. + +--- + +# 50. Definition of Done + +This follow-up is complete only when: + +## Agent guidance + +- root `AGENTS.md` contains the real engineering rules; +- it requires reading this follow-up for repository-wide refactor work. + +## Icons + +- `@hugeicons/solid-js` is used as the vendor renderer; +- the ChatYX `Icon` wrapper is thin; +- no custom Hugeicons payload renderer remains; +- no Lucide remains; +- no icon font remains; +- brand assets retain separate ownership. + +## Architecture + +- features do not depend on components for shared contracts; +- known layer inversions are removed or explicitly justified; +- setup remains a composition-oriented route; +- no generic helper dumping ground was introduced. + +## Lifecycle + +- application destruction is safe before/after start; +- destroy is idempotent; +- pending initialization cannot publish stale ownership state; +- old async asset work cannot repopulate reset singleton state; +- preview/global proxy ownership is explicitly correct. + +## Styling + +- no Panda CSS is introduced; +- no broad Tailwind rewrite occurs; +- lifecycle modules do not own static design colors unnecessarily; +- overlay runtime styling remains stable CSS + custom property values. + +## Documentation + +- `AGENTS.md` is current; +- `ARCHITECTURE.md` matches code; +- `DESIGN.md` matches code; +- `REFACTOR_PLAN.md` has no misleading open/stale status; +- outdated Hugeicons claims are removed; +- this document contains a completion report. + +## Verification + +```text +lint: pass +typecheck: pass +tests: pass +build: pass +bun run check: pass +``` + +--- + +# 51. What Comes After This + +A future PR may evaluate: + +```text +Panda CSS +semantic design tokens +Panda recipes +possible Ark UI migration +Tailwind removal +CVA/tailwind-merge cleanup +``` + +That future work is intentionally outside this follow-up. + +The repository should first have: + +```text +correct ownership +stable lifecycle +correct docs +one icon renderer +clean layer direction +``` + +Only then should the styling engine be replaced. + +--- + +# 52. Final Instruction to the Agent + +Do not optimize for the number of refactor tasks marked complete. + +Optimize for correctness and clear ownership. + +For every change ask: + +```text +What problem exists? +Which layer owns the solution? +What observable behavior must remain? +What test proves it? +Can old asynchronous work outlive this owner? +Does this make the next contributor's choice more obvious? +``` + +If there is no concrete problem, do not move code. + +If a previous plan item is no longer useful, close it explicitly instead of performing a ceremonial refactor. + +The desired end state of this follow-up is: + +```text +current architecture ++ finished lifecycle ownership ++ correct dependency direction ++ official Hugeicons Solid integration ++ current agent/documentation rules +- stale assumptions +- refactor leftovers +``` + +**Do not add Panda CSS in this phase.** + +--- + +# Completion Report + +Written after finishing the follow-up on top of PR #15. Every claim below was +checked against the tree that the last commit in the series produces. + +## Completed items + +### A. Root agent documentation (section 6) — `3baf16a` + +`AGENTS.md` now carries the engineering rules directly: architecture direction, +route/feature/service responsibilities, lifecycle ownership, UI primitive +boundaries, the icon policy (Hugeicons plus brand-glyph ownership), the styling +and custom-property contract, dependency and utility-module rules, accessibility, +OBS constraints, public contracts, testing requirements and Conventional +Commits. It also requires repository-wide refactor work to read +`documents/REFACTORING.md` and `documents/REFACTORING_FOLLOWUP.md`. The +Conventional Commit rules and `bun run check` were preserved. + +### B. Official Hugeicons Solid renderer (sections 7, 40) — `cd17aae` + +`@hugeicons/solid-js` was added. `src/components/ui/icon.tsx` went from 68 lines +of payload traversal, attribute kebab-casing and `createDynamic` to a 40-line +wrapper over the vendor's `HugeiconsIcon`. The wrapper owns `currentColor`, the +`1em` default size and decorative-by-default accessibility; the icon payload +keeps its own stroke width, because all 25 icons in use carry `strokeWidth: 1.5` +and a forced root default would give a stroke to a fill-only icon. + +Evidence: every `` on the setup page was captured before and after — +65/65 identical geometry, font size, stroke width and colour, and identical +attribute sets per element except the vendor's explicit `color="currentColor"`, +which inherits to the same value. Subpath imports are unchanged, and a search of +the built output for an unused icon's path data finds nothing, so the 675 KB +barrel is not bundled. + +### C. Dependency direction (sections 9, 10) — `5121bf4` + +- `SetupSectionId` moved to `features/setup/model/setupSections.ts`, so the route, + the workspace layout and the settings search share the identity without + importing each other. The translation and icon metadata stayed with the + presentation that renders it. +- Two pure helpers were unreachable for tests once the icon module imported the + client-only vendor renderer, because their tests imported JSX modules for a + function: the Twitch/Kick channel search moved to + `services/setup/channelSearch.ts`, and `resolveSetupText`/`SetupText` moved to + `components/setup/setupText.ts`. +- The move collapsed a real duplication: `normalizeLogin`, the avatar fallback + letter and the login-list merge existed twice; they now have one owner in + `services/setup/logins.ts`. + +Fresh audit results: `features` → `components` zero, `features` → `routes` zero, +`services`/`config`/`utils` → `features`/`routes`/`components` zero, and no new +lateral edge. The only non-vertical edge remains the recorded +`config/setupTemplates` → `services/storage/setupStorage`, whose target is a +dependency-free leaf. + +### D. Async work after teardown (sections 11, 12, 13) — `bfd8102` + +The race exists. `emoteService`, `badgeService`, `sevenTVCosmeticsService` and +`chatFeatureIntegration` all committed into their own stores after an `await`, +so a response that arrived after `reset()` repopulated the store the next runtime +had just cleaned. `chatFeatureIntegration` was worse: it could mark itself +initialized after `destroy()` and then refuse the next runtime's initialization. + +Each service now carries a generation that `reset()` (or `clearAllCaches()`) +bumps; a load captures it on entry and skips its commit when the store moved on. +The 7TV visibility toggle bumps it too, because a response loaded under the +previous filter is equally stale. + +Evidence: `tests/assetTeardownRace.test.ts` holds each response open, resets, and +only then resolves it. All seven tests fail against the previous code and pass +against the new one (verified by stashing the four services and re-running). + +`ChatAssetLoader` needed nothing: it is constructed per live runtime, so its +shared-channel map and caches die with the instance. + +### E. Application and runtime destruction (sections 14, 15) — `a7f6fce`, `96200bd` + +`ChatOverlayApplication.destroy()` used to return immediately when `start()` had +never run, although the constructor had already built both runtimes. It now +releases what the constructor built, is idempotent, and is terminal, which is +what also makes it safe while `start()` is still awaiting `initialize()`. + +The preview runtime enabled the RTE proxy flag and filled the shared emote, +badge and mention stores without releasing either. Its teardown now clears both, +and only when `initialize()`/`updateConfig()` actually claimed them. + +### F. Preview and proxy ownership (sections 16, 17) — `96200bd` + +`PreviewRuntime` teardown releases the message interval, the render timer, the +preview styles, the presentation service and the shared state it claimed. +A destroyed runtime also ignores further configuration instead of re-injecting +the styles it just removed. + +Concurrency does not exist by design, and this was checked rather than assumed: +the setup page loads the preview in an iframe, which has its own module +instances, and the chat route renders exactly one application. The proxy flag is +therefore a plain boolean, not a lease or a reference count; the ownership +argument is recorded in `ARCHITECTURE.md` and at the teardown itself. + +### G. Setup document styling (section 18) — `0890919` + +The literal colour is gone. Measured before the change: the inline +`#09090b` resolved to `rgba(0, 0, 0, 0)` because `app.css` forces +`background: transparent !important` for OBS, while `.setup-root` painted +`rgb(9, 9, 11)` and covered the viewport. The helper now sets +`data-setup-document` and the setup stylesheet owns the scrolling and height. + +Verified by a DOM-level comparison, not a screenshot: the page is not +pixel-stable (two shots of an unchanged state differ, even in a region with no +animation), so every element's rect plus the lock-relevant computed styles and +every scroll height were compared between the two implementations — zero +changes. In the browser the lock is applied on setup, removed when navigating +in-app to `/status`, and re-applied when navigating between two setup routes. + +### H. Layout and fade ownership (sections 21, 22) — `1f5c508` + +`utils/ui/layoutUtils.ts` → `services/chat/runtime/layoutManager.ts` and +`utils/ui/fadeUtils.ts` → `services/chat/runtime/messageFade.ts`. These are chat +overlay runtime infrastructure, not general helpers: their stylesheets are +written against `#chat_container`, `.chat_line` and `.message-fade-out`, and +`ChatPresentationService` is their only owner. The correct home is the service +layer, which is why the earlier "blocked by layering" note is resolved rather +than worked around. + +The duplicate `LayoutManager` in `utils/ui/layoutManager.ts` is deleted. Its only +caller was `chatFeatureIntegration.setOptions`, which wired two options into a +class that was a no-op: `setContainer` was never called, so `applyLayout` +returned before touching anything, and `shouldShowMessage`, `getContainerClasses`, +`getLayoutCSS` and `injectCSS` had no callers. `utils/ui/` no longer exists. + +### I. Remaining global state (sections 33, 34, 35) — `27b6096`, `adabb32`, `feb4e11` + +- The 7TV paint stylesheet was created on demand and never removed, and the + cached sheet reference would have kept writing into the detached element. + `disposeStylesheet()` now removes it and drops the reference; it is called from + `ChatPresentationService.cleanup()` next to `clearAllCaches()`. +- The setup route created the preview synchronizer with a `dispose()` that + nothing called, so a pending navigation timer could fire after the route was + left. `onCleanup` now disposes it. +- The overlay style manager contract is covered end to end: apply, apply twice, + cleanup, and apply-after-cleanup. +- `LayoutManager.cleanup()` cancelled only the stylesheet; a smooth follow it had + started kept its `requestAnimationFrame` scheduled. Cleanup now cancels the + frame and drops the follow state. +- `PreviewRuntime.initialize()` used to reset its `destroyed` flag, so a destroyed + runtime could be re-initialized and re-claim the proxy flag and the shared + stores. Destruction is now terminal there too, which matches the application's + contract. +- Animation CSS stays in `services/chat/runtime/animationStyles.ts`. It is the + module the runtime already owns, and moving it would be organisation without a + contract change. + +### J. Documentation (sections 23–27) — this commit + +- `ARCHITECTURE.md`: new dependency rule (a feature must not import a component), + new "Chat overlay lifecycle and asset ownership" section (construction and + teardown contract, generation-guarded loads, the single-owner proxy argument), + the layout/fade relocation, the setup module map including the new modules, the + descriptor-factory rule, the official icon renderer, and a renamed + "Plain and generated CSS" section that describes the actual boundaries. +- `DESIGN.md`: the stale claim that `chatStyles.ts` still generates + preset-dependent rules is gone; the token contract now describes what the code + does, the allowed generated-CSS boundaries are unchanged, and new sections + record the styling stack, iconography and surface ownership. No Panda CSS and + no Ark UI is described, because neither exists here. +- `REFACTOR_PLAN.md`: no phase is left in progress, the phase 6 items are closed + with reasons, the phase 2 outcome no longer says the conversion is open, and + the stale "no official Hugeicons package for Solid" claim is struck through + with the correction. +- `REFACTOR_AUDIT.md`: a new "Post-refactor state" section keeps every original + score and adds the after-score, records the final owner and remaining debt per + issue, and lists the two problems the audit did not predict. + +## Items investigated and intentionally unchanged + +| Item | Why no change | +| --- | --- | +| Reconnect suppression (phase 1, item 3) | Still browser-verified. It is not on the follow-up's required-test list, and a unit test would need a WebSocket shim around the connection manager rather than a behaviour seam. | +| `channelRolesService`, `twitchGqlService`, `bitsService` caches | Keyed by channel or by user/channel id, so an in-flight write cannot serve another channel's data. They are caches of provider facts, not runtime state, and no cross-channel leak was found. | +| `ffzapBadgeService`, `bttvBadgeService`, `chatterinoBadgeService`, `chatisBadgeService` | All four load global, channel-independent badge directories keyed by user id or name, and none of them takes a channel. A response that resolves after teardown writes the same directory the next load would fetch, so it cannot serve another channel's badges. | +| `sevenTVCosmeticsService` paint catalogue and user→paint assignments | Provider facts about users, not channel state. `clearAllCaches()` deliberately clears the channel-scoped caches and the generated CSS only; dropping the catalogue and the assignments would refetch both for the very users the next runtime renders. The `refresh` command's `reloadCosmetics()` clears them when a real refresh is wanted. | +| `mentionStyleService` | Synchronous only: `registerMessageAuthor` and `reset`. No in-flight work exists to guard. | +| `chatStyles.ts` `SIZE_CONFIGS` | It is the single source of truth for message size, weight, line height and the emote scale, and JS reads it for emote geometry. Deleting it would duplicate the table; it now publishes custom properties instead of generating rules. | +| `renderMessageContent.ts` physical split | Section 21 explicitly allows closing it. The file has one cohesive pipeline (tokenize, then assemble an HTML string), no DOM binding and no provider-specific transformation, so a split buys organisation only. | +| The route's chip keydown handlers | Plain signal binding, which sections 18–19 list as a legitimate route responsibility. A factory would add indirection for line count. | +| Tailwind, CVA, `tailwind-merge`/`clsx`, Kobalte | Kept as they are. No new abstraction layer, no removal in preparation for a different styling engine. | +| `tests/chatRuntimeLifecycle.test.ts` private-field probes | They test orchestration that has no public seam; the follow-up's new tests cover the ownership boundaries instead. | +| `buildOverlayUrl` and the stored setup keys | Public contract. Untouched, and still pinned by the round-trip tests. | + +## Deferred items + +- **Panda CSS migration** (explicitly excluded by section 4). +- **Ark UI migration**, **Tailwind removal**, **CVA/`tailwind-merge` cleanup** — + section 51 assigns these to a future PR. +- **`ChatConfig`'s two-hop barrel** (`~/utils/chat` re-exporting a `config/` + type) — naming debt that nothing depends on; moving 15 import sites is churn + without a behaviour or ownership change. +- **Component-render characterization tests** — deferred by the decision to keep + `happy-dom` out of the project; those behaviours are covered by browser checks. + +## New tests + +| File | Covers | +| --- | --- | +| `tests/chatOverlayApplication.test.ts` | destroy before start, double destroy, double start, start after destroy, destroy during a pending initialization | +| `tests/assetTeardownRace.test.ts` | stale commits after reset for emote (global + channel), badge (channel, third-party, per-user), 7TV cosmetics, and the paint stylesheet; plus the feature-integration initialization race | +| `tests/previewRuntimeTeardown.test.ts` | preview destroy releases the flag and the shared stores, does not release what it never claimed, ignores post-destroy configuration, and cannot be initialized again | +| `tests/overlayStyleManager.test.ts` (added case) | apply after cleanup reconstructs properties, attributes and the stylesheet | +| `tests/layoutUtils.test.ts` (added case) | cleanup cancels the smooth-scroll frame and removes the stylesheet | +| `tests/channelSearch.test.ts`, `tests/setupTranslations.test.ts` (moved imports) | the two extracted pure helpers now test without a DOM | + +Suite size: 375 tests before the follow-up, 394 after. + +## Dependency changes + +Added `@hugeicons/solid-js` (MIT, one peer dependency on `solid-js`). No +dependency was removed in this follow-up; `lucide-solid` and the Hugeicons font +were already gone. `package.json` carries ten runtime dependencies, each with one +responsibility. + +## Bundle/build impact + +| Chunk | Before | After | +| --- | --- | --- | +| `ui` | 133.30 kB | 136.07 kB | +| `setup` | 155.75 kB | 154.50 kB | +| JS + CSS assets | 0.68 MB | 0.69 MB | +| `dist` (with public assets) | 2.15 MB | 2.15 MB | + +The vendor renderer costs about 2.8 kB in the shared UI chunk and the removed +payload renderer saves about 0.5 kB in the setup chunk, for a net increase of +roughly 2.3 kB. Subpath imports still keep the icon barrel out of the bundle: +searching the built assets for the path data of an icon the project does not use +returns nothing. + +## Final verification + +```text +bun run lint 0 warnings, 0 errors (164 app files, 9 service files) +bun run typecheck exit 0 +bun test ./tests 394 pass, 0 fail (51 files, 1048 assertions) +bun run build ok, 292 modules, dist 2.15 MB +bun run check ok (all four above, in order) +``` + +Browser checks (Playwright, installed outside the repository): + +- **Setup page** — 8 navigation icons, 8 section-heading icons, 2 platform + glyphs, 9 sections; settings search finds a row and marks its navigation item; + the generated overlay URL carries the channel; the channel survives a reload + through `chatyx.setup.twitchChannel`; the preview iframe renders and the + live/demo switch flips state and keeps rendering. +- **Channel search** — the Twitch field resolves a login with its avatar and + commits it into the overlay URL as `c=linaryx`; the Kick field returns three + suggestions; no page error. The 404s the preview then produces are the + third-party emote/badge endpoints answering for a channel with no data. +- **Reduced motion** — the demo is paused on mount. +- **Route cleanup** — after an in-app navigation away from setup: the lock + attribute is gone, `#chat-animations` is gone, the preview attribute is gone, + no `.setup-root` remains, and the document did not reload. +- **Overlay** — transparent `html`, `body`, `#root` and `#chat_container`; + 9 messages, 3 badges, 11 event elements and a gigantified emote in the demo; + exactly one animation stylesheet; `hr=true` switches the container to + `layout-horizontal`/`row`; `st=3&sh=3` publishes `--chat-stroke: 3px black` + and the 0.5rem shadow preset, and the computed `-webkit-text-stroke-width` + becomes `3px`. +- **Dev fixture** — 28 messages, 71 badges, 4 emotes, 2 emoji, 25 `user_info` + entries, 1 reply, 1 gigantified emote, 6 emote-modified images (2 wide, 1 + rotated, 3 zero-width) with their transform, filter and animation layers, and + the wide modifier resolving to 88px; no page error. +- **Accessibility of the icon migration** — 65 icons, all decorative; 36 + icon-only buttons, none without an accessible name; 6 radios in 3 radiogroups; + 34 switches; the section navigation carries a label. diff --git a/documents/REFACTOR_AUDIT.md b/documents/REFACTOR_AUDIT.md new file mode 100644 index 0000000..4dfc46c --- /dev/null +++ b/documents/REFACTOR_AUDIT.md @@ -0,0 +1,328 @@ +# ChatYX Refactor Audit + +Evidence-based architecture and technical-debt audit of the ChatYX repository. + +Scope: `src/`, `services/`, `tests/`, `scripts/`, `public/`, `documents/`. +Method: module-graph greps across the whole tree, targeted deep reads of the +architecture-sensitive subtrees listed in `documents/REFACTORING.md` section 7, +then independent re-verification of every load-bearing claim. Claims marked +**verified** were re-checked directly; the rest carry a `file:line` citation for +follow-up confirmation before action. + +Scores use the scale from `documents/REFACTORING.md` section 23: +`0 = no meaningful problem found`, `10 = severe systemic problem`. + +--- + +## 1. Scores + +| Dimension | Score | Evidence | +| --- | --- | --- | +| Architecture coupling | 3 | **verified** No `services/` → `routes/`/`features/` import exists, and no `components/` → `services/`/`config/` import exists. The documented dependency direction holds at the module-graph level. Remaining coupling is UI reaching into mutable singletons: `src/components/chat/ChatBadges.tsx:4,133,193-197` → `badgeService`; `src/components/chat/ChatMessage.tsx:13,167` → `sevenTVCosmeticsService`; `src/components/chat/renderMessageContent.ts:4` → `bitsService`. | +| Responsibility mixing | 7 | `src/routes/setup.tsx` (2779 lines) owns persistence (`:133-156`), browser capability sniffing (`:158-175`), two remote fetchers (`:212-318`), font enumeration (`:1375-1398`), document style locking (`:634-684`), DOM search (`:686-731`), overlay URL construction (`:1012-1028`) and the full config projection (`:885-992`). `src/components/chat/renderMessageContent.ts` performs network resolution (`:203`, `:244`) and DOM construction (`:557-558`) inside a renderer. | +| UI duplication | 3 | **corrected during phase 4** Raw-element counts alone overstated this. Case-sensitive counts: `src/routes/setup.tsx` has 14 raw ` - ChatYX diff --git a/package.json b/package.json index b7e7aa4..352b55c 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,14 @@ "vite-plugin-solid": "^2.11.14" }, "dependencies": { + "@hugeicons/core-free-icons": "^4.3.5", + "@hugeicons/solid-js": "^1.0.0", "@kobalte/core": "^0.13.13", "@solid-primitives/i18n": "^2.2.1", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-solid": "^1.41.0", "solid-js": "^1.9.15", "tailwind-merge": "^3.6.0" }, diff --git a/public/hugeicons/hgi-stroke-rounded.woff2 b/public/hugeicons/hgi-stroke-rounded.woff2 deleted file mode 100644 index 5d8cb1c..0000000 Binary files a/public/hugeicons/hgi-stroke-rounded.woff2 and /dev/null differ diff --git a/public/hugeicons/icons.css b/public/hugeicons/icons.css deleted file mode 100644 index 3560c92..0000000 --- a/public/hugeicons/icons.css +++ /dev/null @@ -1,24935 +0,0 @@ -@font-face { - font-family: "hugeicons-stroke-rounded"; - src: url("hgi-stroke-rounded.woff2") format("woff2"); - font-weight: normal; - font-style: normal; - font-display: swap; -} - -.hgi-stroke { - font-family: "hugeicons-stroke-rounded" !important; - font-style: normal; - font-weight: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - position: relative; - display: inline-block; - font-variant: normal; - font-variant-ligatures: discretionary-ligatures; - font-feature-settings: "liga" 1; - line-height: 1; - text-rendering: auto; - font-display: swap; -} - -.hgi-stroke.hgi-24-hours-clock::before { - content: "\f14e0"; -} - -.hgi-stroke.hgi-a-arrow-down::before { - content: "\f14f3"; -} - -.hgi-stroke.hgi-a-arrow-up::before { - content: "\f14f4"; -} - -.hgi-stroke.hgi-a-large-small::before { - content: "\f2f8d"; -} - -.hgi-stroke.hgi-abacus::before { - content: "\f14fb"; -} - -.hgi-stroke.hgi-absolute::before { - content: "\f14fc"; -} - -.hgi-stroke.hgi-acceleration::before { - content: "\f14fd"; -} - -.hgi-stroke.hgi-access::before { - content: "\f14fe"; -} - -.hgi-stroke.hgi-accessibility::before { - content: "\f2f8e"; -} - -.hgi-stroke.hgi-accident::before { - content: "\f14ff"; -} - -.hgi-stroke.hgi-account-recovery::before { - content: "\f1500"; -} - -.hgi-stroke.hgi-account-setting-01::before { - content: "\f1501"; -} - -.hgi-stroke.hgi-account-setting-02::before { - content: "\f1502"; -} - -.hgi-stroke.hgi-account-setting-03::before { - content: "\f1503"; -} - -.hgi-stroke.hgi-activity-01::before { - content: "\f1504"; -} - -.hgi-stroke.hgi-activity-02::before { - content: "\f1505"; -} - -.hgi-stroke.hgi-activity-03::before { - content: "\f1506"; -} - -.hgi-stroke.hgi-activity-04::before { - content: "\f1507"; -} - -.hgi-stroke.hgi-activity-05::before { - content: "\f1508"; -} - -.hgi-stroke.hgi-activity-circle::before { - content: "\f1509"; -} - -.hgi-stroke.hgi-activity-spark::before { - content: "\f150a"; -} - -.hgi-stroke.hgi-acute::before { - content: "\f150b"; -} - -.hgi-stroke.hgi-add-01::before { - content: "\f150c"; -} - -.hgi-stroke.hgi-add-02::before { - content: "\f150d"; -} - -.hgi-stroke.hgi-add-circle-half-dot::before { - content: "\f150e"; -} - -.hgi-stroke.hgi-add-circle::before { - content: "\f150f"; -} - -.hgi-stroke.hgi-add-female::before { - content: "\f1510"; -} - -.hgi-stroke.hgi-add-invoice::before { - content: "\f2f92"; -} - -.hgi-stroke.hgi-add-male::before { - content: "\f1511"; -} - -.hgi-stroke.hgi-add-money-circle::before { - content: "\f2f93"; -} - -.hgi-stroke.hgi-add-square::before { - content: "\f1512"; -} - -.hgi-stroke.hgi-add-team-02::before { - content: "\f1513"; -} - -.hgi-stroke.hgi-add-team::before { - content: "\f1514"; -} - -.hgi-stroke.hgi-add-to-list::before { - content: "\f1515"; -} - -.hgi-stroke.hgi-address-book::before { - content: "\f1516"; -} - -.hgi-stroke.hgi-adjust-position::before { - content: "\f1517"; -} - -.hgi-stroke.hgi-adobe-after-effect::before { - content: "\f1518"; -} - -.hgi-stroke.hgi-adobe-illustrator::before { - content: "\f1519"; -} - -.hgi-stroke.hgi-adobe-indesign::before { - content: "\f151a"; -} - -.hgi-stroke.hgi-adobe-photoshop::before { - content: "\f151b"; -} - -.hgi-stroke.hgi-adobe-premier::before { - content: "\f151c"; -} - -.hgi-stroke.hgi-adobe-xd::before { - content: "\f151d"; -} - -.hgi-stroke.hgi-adventure::before { - content: "\f151e"; -} - -.hgi-stroke.hgi-advertisiment::before { - content: "\f151f"; -} - -.hgi-stroke.hgi-adzan::before { - content: "\f1520"; -} - -.hgi-stroke.hgi-affiliate::before { - content: "\f1521"; -} - -.hgi-stroke.hgi-agreement-01::before { - content: "\f1522"; -} - -.hgi-stroke.hgi-agreement-02::before { - content: "\f1523"; -} - -.hgi-stroke.hgi-agreement-03::before { - content: "\f1524"; -} - -.hgi-stroke.hgi-ai-art-2::before { - content: "\f2f95"; -} - -.hgi-stroke.hgi-ai-art::before { - content: "\f2f94"; -} - -.hgi-stroke.hgi-ai-audio::before { - content: "\f1525"; -} - -.hgi-stroke.hgi-ai-auto-rotate-2::before { - content: "\f2f97"; -} - -.hgi-stroke.hgi-ai-auto-rotate::before { - content: "\f2f96"; -} - -.hgi-stroke.hgi-ai-background-2::before { - content: "\f2f99"; -} - -.hgi-stroke.hgi-ai-background-eraser-2::before { - content: "\f2f9b"; -} - -.hgi-stroke.hgi-ai-background-eraser::before { - content: "\f2f9a"; -} - -.hgi-stroke.hgi-ai-background::before { - content: "\f2f98"; -} - -.hgi-stroke.hgi-ai-beautify::before { - content: "\f1526"; -} - -.hgi-stroke.hgi-ai-book::before { - content: "\f1527"; -} - -.hgi-stroke.hgi-ai-brain-01::before { - content: "\f1528"; -} - -.hgi-stroke.hgi-ai-brain-02::before { - content: "\f1529"; -} - -.hgi-stroke.hgi-ai-brain-03::before { - content: "\f152a"; -} - -.hgi-stroke.hgi-ai-brain-04::before { - content: "\f152b"; -} - -.hgi-stroke.hgi-ai-brain-05::before { - content: "\f152c"; -} - -.hgi-stroke.hgi-ai-browser::before { - content: "\f152d"; -} - -.hgi-stroke.hgi-ai-camera-2::before { - content: "\f2f9d"; -} - -.hgi-stroke.hgi-ai-camera::before { - content: "\f2f9c"; -} - -.hgi-stroke.hgi-ai-chat-01::before { - content: "\f152e"; -} - -.hgi-stroke.hgi-ai-chat-02::before { - content: "\f152f"; -} - -.hgi-stroke.hgi-ai-chemistry-01::before { - content: "\f1530"; -} - -.hgi-stroke.hgi-ai-chemistry-02::before { - content: "\f1531"; -} - -.hgi-stroke.hgi-ai-chemistry-03::before { - content: "\f1532"; -} - -.hgi-stroke.hgi-ai-chip::before { - content: "\f1533"; -} - -.hgi-stroke.hgi-ai-clothes-2::before { - content: "\f2f9f"; -} - -.hgi-stroke.hgi-ai-clothes::before { - content: "\f2f9e"; -} - -.hgi-stroke.hgi-ai-cloud-01::before { - content: "\f1534"; -} - -.hgi-stroke.hgi-ai-cloud-02::before { - content: "\f1535"; -} - -.hgi-stroke.hgi-ai-cloud::before { - content: "\f1536"; -} - -.hgi-stroke.hgi-ai-co-editing-2::before { - content: "\f2fa1"; -} - -.hgi-stroke.hgi-ai-co-editing::before { - content: "\f2fa0"; -} - -.hgi-stroke.hgi-ai-collage-template-2::before { - content: "\f2fa3"; -} - -.hgi-stroke.hgi-ai-collage-template::before { - content: "\f2fa2"; -} - -.hgi-stroke.hgi-ai-computer::before { - content: "\f1537"; -} - -.hgi-stroke.hgi-ai-concert-2::before { - content: "\f2fa5"; -} - -.hgi-stroke.hgi-ai-concert::before { - content: "\f2fa4"; -} - -.hgi-stroke.hgi-ai-content-generator-01::before { - content: "\f1538"; -} - -.hgi-stroke.hgi-ai-content-generator-02::before { - content: "\f1539"; -} - -.hgi-stroke.hgi-ai-crop-2::before { - content: "\f2fa7"; -} - -.hgi-stroke.hgi-ai-crop::before { - content: "\f2fa6"; -} - -.hgi-stroke.hgi-ai-dna::before { - content: "\f153a"; -} - -.hgi-stroke.hgi-ai-drawing-2::before { - content: "\f2fa9"; -} - -.hgi-stroke.hgi-ai-drawing::before { - content: "\f2fa8"; -} - -.hgi-stroke.hgi-ai-editing::before { - content: "\f153b"; -} - -.hgi-stroke.hgi-ai-elements-2::before { - content: "\f2fab"; -} - -.hgi-stroke.hgi-ai-elements::before { - content: "\f2faa"; -} - -.hgi-stroke.hgi-ai-eraser::before { - content: "\f153c"; -} - -.hgi-stroke.hgi-ai-expand-2::before { - content: "\f2fad"; -} - -.hgi-stroke.hgi-ai-expand::before { - content: "\f2fac"; -} - -.hgi-stroke.hgi-ai-file-01-2::before { - content: "\f2faf"; -} - -.hgi-stroke.hgi-ai-file-01::before { - content: "\f2fae"; -} - -.hgi-stroke.hgi-ai-file::before { - content: "\f153d"; -} - -.hgi-stroke.hgi-ai-folder-01::before { - content: "\f153e"; -} - -.hgi-stroke.hgi-ai-folder-02::before { - content: "\f153f"; -} - -.hgi-stroke.hgi-ai-game::before { - content: "\f1540"; -} - -.hgi-stroke.hgi-ai-generate-2::before { - content: "\f2fb1"; -} - -.hgi-stroke.hgi-ai-generate::before { - content: "\f2fb0"; -} - -.hgi-stroke.hgi-ai-generative::before { - content: "\f1541"; -} - -.hgi-stroke.hgi-ai-hd-resolution-2::before { - content: "\f2fb3"; -} - -.hgi-stroke.hgi-ai-hd-resolution::before { - content: "\f2fb2"; -} - -.hgi-stroke.hgi-ai-idea::before { - content: "\f1542"; -} - -.hgi-stroke.hgi-ai-image-01-2::before { - content: "\f2fb5"; -} - -.hgi-stroke.hgi-ai-image-01::before { - content: "\f2fb4"; -} - -.hgi-stroke.hgi-ai-image-edit-2::before { - content: "\f2fb7"; -} - -.hgi-stroke.hgi-ai-image-edit::before { - content: "\f2fb6"; -} - -.hgi-stroke.hgi-ai-image::before { - content: "\f1543"; -} - -.hgi-stroke.hgi-ai-imagine-2::before { - content: "\f2fb9"; -} - -.hgi-stroke.hgi-ai-imagine::before { - content: "\f2fb8"; -} - -.hgi-stroke.hgi-ai-innovation-01::before { - content: "\f1544"; -} - -.hgi-stroke.hgi-ai-innovation-02::before { - content: "\f1545"; -} - -.hgi-stroke.hgi-ai-innovation-03::before { - content: "\f1546"; -} - -.hgi-stroke.hgi-ai-laptop::before { - content: "\f1547"; -} - -.hgi-stroke.hgi-ai-learning::before { - content: "\f1548"; -} - -.hgi-stroke.hgi-ai-lock::before { - content: "\f1549"; -} - -.hgi-stroke.hgi-ai-magic::before { - content: "\f154a"; -} - -.hgi-stroke.hgi-ai-mail-01::before { - content: "\f154b"; -} - -.hgi-stroke.hgi-ai-mail-02::before { - content: "\f154c"; -} - -.hgi-stroke.hgi-ai-mail::before { - content: "\f154d"; -} - -.hgi-stroke.hgi-ai-mic::before { - content: "\f154e"; -} - -.hgi-stroke.hgi-ai-network::before { - content: "\f154f"; -} - -.hgi-stroke.hgi-ai-paintbrush-2::before { - content: "\f2fbb"; -} - -.hgi-stroke.hgi-ai-paintbrush::before { - content: "\f2fba"; -} - -.hgi-stroke.hgi-ai-phone-01::before { - content: "\f1550"; -} - -.hgi-stroke.hgi-ai-phone-02::before { - content: "\f1551"; -} - -.hgi-stroke.hgi-ai-programming::before { - content: "\f1552"; -} - -.hgi-stroke.hgi-ai-replace-2::before { - content: "\f2fbd"; -} - -.hgi-stroke.hgi-ai-replace::before { - content: "\f2fbc"; -} - -.hgi-stroke.hgi-ai-scan-text-2::before { - content: "\f2fbf"; -} - -.hgi-stroke.hgi-ai-scan-text::before { - content: "\f2fbe"; -} - -.hgi-stroke.hgi-ai-scan::before { - content: "\f1553"; -} - -.hgi-stroke.hgi-ai-scheduling::before { - content: "\f1554"; -} - -.hgi-stroke.hgi-ai-search-01-2::before { - content: "\f2fc1"; -} - -.hgi-stroke.hgi-ai-search-01::before { - content: "\f2fc0"; -} - -.hgi-stroke.hgi-ai-search-02::before { - content: "\f1555"; -} - -.hgi-stroke.hgi-ai-search-lines-2::before { - content: "\f2fc3"; -} - -.hgi-stroke.hgi-ai-search-lines::before { - content: "\f2fc2"; -} - -.hgi-stroke.hgi-ai-search::before { - content: "\f1556"; -} - -.hgi-stroke.hgi-ai-security-01::before { - content: "\f1557"; -} - -.hgi-stroke.hgi-ai-security-02::before { - content: "\f1558"; -} - -.hgi-stroke.hgi-ai-security-03::before { - content: "\f1559"; -} - -.hgi-stroke.hgi-ai-setting::before { - content: "\f155a"; -} - -.hgi-stroke.hgi-ai-sheets::before { - content: "\f155b"; -} - -.hgi-stroke.hgi-ai-sketch-2::before { - content: "\f2fc5"; -} - -.hgi-stroke.hgi-ai-sketch::before { - content: "\f2fc4"; -} - -.hgi-stroke.hgi-ai-smartwatch::before { - content: "\f155c"; -} - -.hgi-stroke.hgi-ai-sparkles-2::before { - content: "\f2fc7"; -} - -.hgi-stroke.hgi-ai-sparkles::before { - content: "\f2fc6"; -} - -.hgi-stroke.hgi-ai-speech-2::before { - content: "\f2fc9"; -} - -.hgi-stroke.hgi-ai-speech::before { - content: "\f2fc8"; -} - -.hgi-stroke.hgi-ai-swap-2::before { - content: "\f2fcb"; -} - -.hgi-stroke.hgi-ai-swap::before { - content: "\f2fca"; -} - -.hgi-stroke.hgi-ai-template-2::before { - content: "\f2fcd"; -} - -.hgi-stroke.hgi-ai-template::before { - content: "\f2fcc"; -} - -.hgi-stroke.hgi-ai-transcribe-audio-2::before { - content: "\f2fcf"; -} - -.hgi-stroke.hgi-ai-transcribe-audio::before { - content: "\f2fce"; -} - -.hgi-stroke.hgi-ai-translate-2::before { - content: "\f2fd1"; -} - -.hgi-stroke.hgi-ai-translate::before { - content: "\f2fd0"; -} - -.hgi-stroke.hgi-ai-user::before { - content: "\f155d"; -} - -.hgi-stroke.hgi-ai-video-01-2::before { - content: "\f2fd3"; -} - -.hgi-stroke.hgi-ai-video-01::before { - content: "\f2fd2"; -} - -.hgi-stroke.hgi-ai-video::before { - content: "\f155e"; -} - -.hgi-stroke.hgi-ai-view::before { - content: "\f155f"; -} - -.hgi-stroke.hgi-ai-vision-recognition::before { - content: "\f1560"; -} - -.hgi-stroke.hgi-ai-voice-01-2::before { - content: "\f2fd5"; -} - -.hgi-stroke.hgi-ai-voice-01::before { - content: "\f2fd4"; -} - -.hgi-stroke.hgi-ai-voice-generator::before { - content: "\f1561"; -} - -.hgi-stroke.hgi-ai-voice::before { - content: "\f1562"; -} - -.hgi-stroke.hgi-ai-watermark-2::before { - content: "\f2fd7"; -} - -.hgi-stroke.hgi-ai-watermark::before { - content: "\f2fd6"; -} - -.hgi-stroke.hgi-ai-web-browsing::before { - content: "\f1563"; -} - -.hgi-stroke.hgi-ai-wiper-2::before { - content: "\f2fd9"; -} - -.hgi-stroke.hgi-ai-wiper::before { - content: "\f2fd8"; -} - -.hgi-stroke.hgi-aids::before { - content: "\f1564"; -} - -.hgi-stroke.hgi-air-vent::before { - content: "\f2fda"; -} - -.hgi-stroke.hgi-airbnb::before { - content: "\f1565"; -} - -.hgi-stroke.hgi-aircraft-game::before { - content: "\f1566"; -} - -.hgi-stroke.hgi-airdrop::before { - content: "\f1567"; -} - -.hgi-stroke.hgi-airplane-01::before { - content: "\f1568"; -} - -.hgi-stroke.hgi-airplane-02::before { - content: "\f1569"; -} - -.hgi-stroke.hgi-airplane-landing-01::before { - content: "\f156a"; -} - -.hgi-stroke.hgi-airplane-landing-02::before { - content: "\f156b"; -} - -.hgi-stroke.hgi-airplane-mode-off::before { - content: "\f156c"; -} - -.hgi-stroke.hgi-airplane-mode::before { - content: "\f156d"; -} - -.hgi-stroke.hgi-airplane-seat-02::before { - content: "\f156e"; -} - -.hgi-stroke.hgi-airplane-seat::before { - content: "\f156f"; -} - -.hgi-stroke.hgi-airplane-take-off-01::before { - content: "\f1570"; -} - -.hgi-stroke.hgi-airplane-take-off-02::before { - content: "\f1571"; -} - -.hgi-stroke.hgi-airplay-line::before { - content: "\f1572"; -} - -.hgi-stroke.hgi-airplay::before { - content: "\f2fdb"; -} - -.hgi-stroke.hgi-airpod-01::before { - content: "\f1573"; -} - -.hgi-stroke.hgi-airpod-02::before { - content: "\f1574"; -} - -.hgi-stroke.hgi-airpod-03::before { - content: "\f1575"; -} - -.hgi-stroke.hgi-airport-02::before { - content: "\f1576"; -} - -.hgi-stroke.hgi-airport-tower::before { - content: "\f1577"; -} - -.hgi-stroke.hgi-airport::before { - content: "\f1578"; -} - -.hgi-stroke.hgi-al-aqsa-mosque::before { - content: "\f1579"; -} - -.hgi-stroke.hgi-alarm-clock-check::before { - content: "\f2fdc"; -} - -.hgi-stroke.hgi-alarm-clock-minus::before { - content: "\f2fdd"; -} - -.hgi-stroke.hgi-alarm-clock-off::before { - content: "\f157a"; -} - -.hgi-stroke.hgi-alarm-clock-plus::before { - content: "\f2fde"; -} - -.hgi-stroke.hgi-alarm-clock::before { - content: "\f157b"; -} - -.hgi-stroke.hgi-alarm-smoke::before { - content: "\f157c"; -} - -.hgi-stroke.hgi-album-01::before { - content: "\f157d"; -} - -.hgi-stroke.hgi-album-02::before { - content: "\f157e"; -} - -.hgi-stroke.hgi-album-not-found-01::before { - content: "\f157f"; -} - -.hgi-stroke.hgi-album-not-found-02::before { - content: "\f1580"; -} - -.hgi-stroke.hgi-album::before { - content: "\f1581"; -} - -.hgi-stroke.hgi-alert-01::before { - content: "\f1582"; -} - -.hgi-stroke.hgi-alert-02::before { - content: "\f1583"; -} - -.hgi-stroke.hgi-alert-circle::before { - content: "\f1584"; -} - -.hgi-stroke.hgi-alert-diamond::before { - content: "\f1585"; -} - -.hgi-stroke.hgi-alert-square::before { - content: "\f1586"; -} - -.hgi-stroke.hgi-algorithm::before { - content: "\f1587"; -} - -.hgi-stroke.hgi-alien-01-2::before { - content: "\f2fdf"; -} - -.hgi-stroke.hgi-alien-01::before { - content: "\f1588"; -} - -.hgi-stroke.hgi-alien-02-2::before { - content: "\f2fe0"; -} - -.hgi-stroke.hgi-alien-02::before { - content: "\f1589"; -} - -.hgi-stroke.hgi-align-bottom::before { - content: "\f158a"; -} - -.hgi-stroke.hgi-align-box-bottom-center::before { - content: "\f158b"; -} - -.hgi-stroke.hgi-align-box-bottom-left::before { - content: "\f158c"; -} - -.hgi-stroke.hgi-align-box-bottom-right::before { - content: "\f158d"; -} - -.hgi-stroke.hgi-align-box-middle-center::before { - content: "\f158e"; -} - -.hgi-stroke.hgi-align-box-middle-left::before { - content: "\f158f"; -} - -.hgi-stroke.hgi-align-box-middle-right::before { - content: "\f1590"; -} - -.hgi-stroke.hgi-align-box-top-center::before { - content: "\f1591"; -} - -.hgi-stroke.hgi-align-box-top-left::before { - content: "\f1592"; -} - -.hgi-stroke.hgi-align-box-top-right::before { - content: "\f1593"; -} - -.hgi-stroke.hgi-align-end-horizontal::before { - content: "\f2fe1"; -} - -.hgi-stroke.hgi-align-end-vertical::before { - content: "\f2fe2"; -} - -.hgi-stroke.hgi-align-horizontal-center::before { - content: "\f1594"; -} - -.hgi-stroke.hgi-align-horizontal-distribute-center::before { - content: "\f2fe3"; -} - -.hgi-stroke.hgi-align-horizontal-distribute-end::before { - content: "\f1595"; -} - -.hgi-stroke.hgi-align-horizontal-distribute-start::before { - content: "\f1596"; -} - -.hgi-stroke.hgi-align-horizontal-justify-center::before { - content: "\f2fe4"; -} - -.hgi-stroke.hgi-align-horizontal-justify-end::before { - content: "\f1597"; -} - -.hgi-stroke.hgi-align-horizontal-justify-start::before { - content: "\f1598"; -} - -.hgi-stroke.hgi-align-horizontal-space-around::before { - content: "\f1599"; -} - -.hgi-stroke.hgi-align-horizontal-space-between::before { - content: "\f159a"; -} - -.hgi-stroke.hgi-align-key-object::before { - content: "\f159b"; -} - -.hgi-stroke.hgi-align-left::before { - content: "\f159c"; -} - -.hgi-stroke.hgi-align-right::before { - content: "\f159d"; -} - -.hgi-stroke.hgi-align-selection::before { - content: "\f159e"; -} - -.hgi-stroke.hgi-align-start-horizontal::before { - content: "\f2fe5"; -} - -.hgi-stroke.hgi-align-start-vertical::before { - content: "\f2fe6"; -} - -.hgi-stroke.hgi-align-top::before { - content: "\f159f"; -} - -.hgi-stroke.hgi-align-vertical-center::before { - content: "\f15a0"; -} - -.hgi-stroke.hgi-align-vertical-distribute-center::before { - content: "\f15a1"; -} - -.hgi-stroke.hgi-align-vertical-distribute-end::before { - content: "\f15a2"; -} - -.hgi-stroke.hgi-align-vertical-distribute-start::before { - content: "\f15a3"; -} - -.hgi-stroke.hgi-align-vertical-justify-center::before { - content: "\f2fe7"; -} - -.hgi-stroke.hgi-align-vertical-justify-end::before { - content: "\f2fe8"; -} - -.hgi-stroke.hgi-align-vertical-justify-start::before { - content: "\f15a4"; -} - -.hgi-stroke.hgi-align-vertical-space-around::before { - content: "\f2fe9"; -} - -.hgi-stroke.hgi-align-vertical-space-between::before { - content: "\f15a5"; -} - -.hgi-stroke.hgi-all-bookmark::before { - content: "\f15a6"; -} - -.hgi-stroke.hgi-allah::before { - content: "\f15a7"; -} - -.hgi-stroke.hgi-alms::before { - content: "\f15a8"; -} - -.hgi-stroke.hgi-alpha-circle::before { - content: "\f15a9"; -} - -.hgi-stroke.hgi-alpha-square::before { - content: "\f15aa"; -} - -.hgi-stroke.hgi-alpha::before { - content: "\f15ab"; -} - -.hgi-stroke.hgi-alphabet-arabic::before { - content: "\f15ac"; -} - -.hgi-stroke.hgi-alphabet-bangla::before { - content: "\f15ad"; -} - -.hgi-stroke.hgi-alphabet-chinese::before { - content: "\f15ae"; -} - -.hgi-stroke.hgi-alphabet-greek::before { - content: "\f15af"; -} - -.hgi-stroke.hgi-alphabet-hebrew::before { - content: "\f15b0"; -} - -.hgi-stroke.hgi-alphabet-hindi::before { - content: "\f15b1"; -} - -.hgi-stroke.hgi-alphabet-japanese::before { - content: "\f15b2"; -} - -.hgi-stroke.hgi-alphabet-korean::before { - content: "\f15b3"; -} - -.hgi-stroke.hgi-alphabet-thai::before { - content: "\f15b4"; -} - -.hgi-stroke.hgi-amazon::before { - content: "\f15b5"; -} - -.hgi-stroke.hgi-ambulance::before { - content: "\f15b6"; -} - -.hgi-stroke.hgi-american-football::before { - content: "\f15b7"; -} - -.hgi-stroke.hgi-amie::before { - content: "\f15b8"; -} - -.hgi-stroke.hgi-ammo::before { - content: "\f15b9"; -} - -.hgi-stroke.hgi-ampersand::before { - content: "\f2fea"; -} - -.hgi-stroke.hgi-ampersands::before { - content: "\f2feb"; -} - -.hgi-stroke.hgi-amphora::before { - content: "\f15ba"; -} - -.hgi-stroke.hgi-ampoule::before { - content: "\f15bb"; -} - -.hgi-stroke.hgi-analysis-text-link::before { - content: "\f15bc"; -} - -.hgi-stroke.hgi-analytics-01::before { - content: "\f15bd"; -} - -.hgi-stroke.hgi-analytics-02::before { - content: "\f15be"; -} - -.hgi-stroke.hgi-analytics-03::before { - content: "\f15bf"; -} - -.hgi-stroke.hgi-analytics-down::before { - content: "\f15c0"; -} - -.hgi-stroke.hgi-analytics-up::before { - content: "\f15c1"; -} - -.hgi-stroke.hgi-anchor-point::before { - content: "\f15c2"; -} - -.hgi-stroke.hgi-anchor::before { - content: "\f15c3"; -} - -.hgi-stroke.hgi-android::before { - content: "\f15c4"; -} - -.hgi-stroke.hgi-angel::before { - content: "\f15c5"; -} - -.hgi-stroke.hgi-angle-01::before { - content: "\f15c6"; -} - -.hgi-stroke.hgi-angle::before { - content: "\f15c7"; -} - -.hgi-stroke.hgi-angry-bird::before { - content: "\f15c8"; -} - -.hgi-stroke.hgi-angry::before { - content: "\f15c9"; -} - -.hgi-stroke.hgi-annoyed::before { - content: "\f2fec"; -} - -.hgi-stroke.hgi-anonymous::before { - content: "\f15ca"; -} - -.hgi-stroke.hgi-antenna::before { - content: "\f2fed"; -} - -.hgi-stroke.hgi-anvil::before { - content: "\f15cb"; -} - -.hgi-stroke.hgi-apartment::before { - content: "\f15cc"; -} - -.hgi-stroke.hgi-aperture::before { - content: "\f2fee"; -} - -.hgi-stroke.hgi-api-gateway::before { - content: "\f15cd"; -} - -.hgi-stroke.hgi-api::before { - content: "\f15ce"; -} - -.hgi-stroke.hgi-app-store::before { - content: "\f15cf"; -} - -.hgi-stroke.hgi-app-window-mac::before { - content: "\f2ff0"; -} - -.hgi-stroke.hgi-app-window::before { - content: "\f2fef"; -} - -.hgi-stroke.hgi-apple-01::before { - content: "\f15d0"; -} - -.hgi-stroke.hgi-apple-finder::before { - content: "\f15d1"; -} - -.hgi-stroke.hgi-apple-intelligence::before { - content: "\f15d2"; -} - -.hgi-stroke.hgi-apple-music::before { - content: "\f15d3"; -} - -.hgi-stroke.hgi-apple-news::before { - content: "\f15d4"; -} - -.hgi-stroke.hgi-apple-pie::before { - content: "\f15d5"; -} - -.hgi-stroke.hgi-apple-reminder::before { - content: "\f15d6"; -} - -.hgi-stroke.hgi-apple-stocks::before { - content: "\f15d7"; -} - -.hgi-stroke.hgi-apple-vision-pro::before { - content: "\f15d8"; -} - -.hgi-stroke.hgi-apple::before { - content: "\f15d9"; -} - -.hgi-stroke.hgi-appointment-01::before { - content: "\f15da"; -} - -.hgi-stroke.hgi-appointment-02::before { - content: "\f15db"; -} - -.hgi-stroke.hgi-approximately-equal-circle::before { - content: "\f15dc"; -} - -.hgi-stroke.hgi-approximately-equal-square::before { - content: "\f15dd"; -} - -.hgi-stroke.hgi-approximately-equal::before { - content: "\f15de"; -} - -.hgi-stroke.hgi-apricot::before { - content: "\f15df"; -} - -.hgi-stroke.hgi-apron::before { - content: "\f15e0"; -} - -.hgi-stroke.hgi-arc-browser::before { - content: "\f15e1"; -} - -.hgi-stroke.hgi-archer::before { - content: "\f15e2"; -} - -.hgi-stroke.hgi-archive-01::before { - content: "\f15e3"; -} - -.hgi-stroke.hgi-archive-02::before { - content: "\f15e4"; -} - -.hgi-stroke.hgi-archive-03-2::before { - content: "\f2ff1"; -} - -.hgi-stroke.hgi-archive-03::before { - content: "\f15e5"; -} - -.hgi-stroke.hgi-archive-04::before { - content: "\f15e6"; -} - -.hgi-stroke.hgi-archive-arrow-down::before { - content: "\f15e7"; -} - -.hgi-stroke.hgi-archive-arrow-up::before { - content: "\f15e8"; -} - -.hgi-stroke.hgi-archive-off-03::before { - content: "\f15e9"; -} - -.hgi-stroke.hgi-archive-off-04::before { - content: "\f15ea"; -} - -.hgi-stroke.hgi-archive-restore::before { - content: "\f15eb"; -} - -.hgi-stroke.hgi-archive-x::before { - content: "\f2ff2"; -} - -.hgi-stroke.hgi-archive::before { - content: "\f15ec"; -} - -.hgi-stroke.hgi-armchair::before { - content: "\f2ff3"; -} - -.hgi-stroke.hgi-armored-boot::before { - content: "\f15ed"; -} - -.hgi-stroke.hgi-arrange-by-letters-a-z::before { - content: "\f15ee"; -} - -.hgi-stroke.hgi-arrange-by-letters-z-a::before { - content: "\f15ef"; -} - -.hgi-stroke.hgi-arrange-by-numbers-nine-1::before { - content: "\f15f0"; -} - -.hgi-stroke.hgi-arrange-by-numbers-one-9::before { - content: "\f15f1"; -} - -.hgi-stroke.hgi-arrange::before { - content: "\f15f2"; -} - -.hgi-stroke.hgi-arrow-all-direction::before { - content: "\f15f3"; -} - -.hgi-stroke.hgi-arrow-big-down-dash::before { - content: "\f15f4"; -} - -.hgi-stroke.hgi-arrow-big-left-dash::before { - content: "\f15f5"; -} - -.hgi-stroke.hgi-arrow-big-right-dash::before { - content: "\f15f6"; -} - -.hgi-stroke.hgi-arrow-big-up-dash::before { - content: "\f15f7"; -} - -.hgi-stroke.hgi-arrow-data-transfer-diagonal::before { - content: "\f15f8"; -} - -.hgi-stroke.hgi-arrow-data-transfer-horizontal::before { - content: "\f15f9"; -} - -.hgi-stroke.hgi-arrow-data-transfer-vertical::before { - content: "\f15fa"; -} - -.hgi-stroke.hgi-arrow-diagonal::before { - content: "\f15fb"; -} - -.hgi-stroke.hgi-arrow-down-01::before { - content: "\f15fc"; -} - -.hgi-stroke.hgi-arrow-down-02::before { - content: "\f15fd"; -} - -.hgi-stroke.hgi-arrow-down-03::before { - content: "\f15fe"; -} - -.hgi-stroke.hgi-arrow-down-04::before { - content: "\f15ff"; -} - -.hgi-stroke.hgi-arrow-down-05::before { - content: "\f1600"; -} - -.hgi-stroke.hgi-arrow-down-a-z::before { - content: "\f1601"; -} - -.hgi-stroke.hgi-arrow-down-az::before { - content: "\f2ff4"; -} - -.hgi-stroke.hgi-arrow-down-big::before { - content: "\f1602"; -} - -.hgi-stroke.hgi-arrow-down-double::before { - content: "\f1603"; -} - -.hgi-stroke.hgi-arrow-down-from-line::before { - content: "\f1604"; -} - -.hgi-stroke.hgi-arrow-down-left-01::before { - content: "\f1605"; -} - -.hgi-stroke.hgi-arrow-down-left-02::before { - content: "\f1606"; -} - -.hgi-stroke.hgi-arrow-down-narrow-wide::before { - content: "\f2ff5"; -} - -.hgi-stroke.hgi-arrow-down-one-0::before { - content: "\f1607"; -} - -.hgi-stroke.hgi-arrow-down-right-01::before { - content: "\f1608"; -} - -.hgi-stroke.hgi-arrow-down-right-02::before { - content: "\f1609"; -} - -.hgi-stroke.hgi-arrow-down-to-dot::before { - content: "\f160a"; -} - -.hgi-stroke.hgi-arrow-down-to-line::before { - content: "\f2ff6"; -} - -.hgi-stroke.hgi-arrow-down-wide-narrow::before { - content: "\f2ff7"; -} - -.hgi-stroke.hgi-arrow-down-za::before { - content: "\f2ff8"; -} - -.hgi-stroke.hgi-arrow-down-zero-1::before { - content: "\f160b"; -} - -.hgi-stroke.hgi-arrow-down01::before { - content: "\f2ff9"; -} - -.hgi-stroke.hgi-arrow-down10::before { - content: "\f2ffa"; -} - -.hgi-stroke.hgi-arrow-expand-01::before { - content: "\f160c"; -} - -.hgi-stroke.hgi-arrow-expand-02::before { - content: "\f160d"; -} - -.hgi-stroke.hgi-arrow-expand-diagonal-01::before { - content: "\f160e"; -} - -.hgi-stroke.hgi-arrow-expand-diagonal-02::before { - content: "\f160f"; -} - -.hgi-stroke.hgi-arrow-expand::before { - content: "\f1610"; -} - -.hgi-stroke.hgi-arrow-horizontal::before { - content: "\f1611"; -} - -.hgi-stroke.hgi-arrow-left-01::before { - content: "\f1612"; -} - -.hgi-stroke.hgi-arrow-left-02::before { - content: "\f1613"; -} - -.hgi-stroke.hgi-arrow-left-03::before { - content: "\f1614"; -} - -.hgi-stroke.hgi-arrow-left-04::before { - content: "\f1615"; -} - -.hgi-stroke.hgi-arrow-left-05::before { - content: "\f1616"; -} - -.hgi-stroke.hgi-arrow-left-big::before { - content: "\f1617"; -} - -.hgi-stroke.hgi-arrow-left-double::before { - content: "\f1618"; -} - -.hgi-stroke.hgi-arrow-left-from-line::before { - content: "\f1619"; -} - -.hgi-stroke.hgi-arrow-left-right::before { - content: "\f161a"; -} - -.hgi-stroke.hgi-arrow-left-to-line::before { - content: "\f2ffb"; -} - -.hgi-stroke.hgi-arrow-move-down-left::before { - content: "\f161b"; -} - -.hgi-stroke.hgi-arrow-move-down-right::before { - content: "\f161c"; -} - -.hgi-stroke.hgi-arrow-move-left-down::before { - content: "\f161d"; -} - -.hgi-stroke.hgi-arrow-move-right-down::before { - content: "\f161e"; -} - -.hgi-stroke.hgi-arrow-move-up-left::before { - content: "\f161f"; -} - -.hgi-stroke.hgi-arrow-move-up-right::before { - content: "\f1620"; -} - -.hgi-stroke.hgi-arrow-reload-horizontal::before { - content: "\f1621"; -} - -.hgi-stroke.hgi-arrow-reload-vertical::before { - content: "\f1622"; -} - -.hgi-stroke.hgi-arrow-right-01::before { - content: "\f1623"; -} - -.hgi-stroke.hgi-arrow-right-02::before { - content: "\f1624"; -} - -.hgi-stroke.hgi-arrow-right-03::before { - content: "\f1625"; -} - -.hgi-stroke.hgi-arrow-right-04::before { - content: "\f1626"; -} - -.hgi-stroke.hgi-arrow-right-05::before { - content: "\f1627"; -} - -.hgi-stroke.hgi-arrow-right-big::before { - content: "\f1628"; -} - -.hgi-stroke.hgi-arrow-right-double::before { - content: "\f1629"; -} - -.hgi-stroke.hgi-arrow-right-from-line::before { - content: "\f162a"; -} - -.hgi-stroke.hgi-arrow-right-to-line::before { - content: "\f2ffc"; -} - -.hgi-stroke.hgi-arrow-shrink-01::before { - content: "\f162b"; -} - -.hgi-stroke.hgi-arrow-shrink-02::before { - content: "\f162c"; -} - -.hgi-stroke.hgi-arrow-shrink::before { - content: "\f162d"; -} - -.hgi-stroke.hgi-arrow-turn-backward::before { - content: "\f162e"; -} - -.hgi-stroke.hgi-arrow-turn-down::before { - content: "\f162f"; -} - -.hgi-stroke.hgi-arrow-turn-forward::before { - content: "\f1630"; -} - -.hgi-stroke.hgi-arrow-turn-up::before { - content: "\f1631"; -} - -.hgi-stroke.hgi-arrow-up-01::before { - content: "\f1632"; -} - -.hgi-stroke.hgi-arrow-up-02::before { - content: "\f1633"; -} - -.hgi-stroke.hgi-arrow-up-03::before { - content: "\f1634"; -} - -.hgi-stroke.hgi-arrow-up-04::before { - content: "\f1635"; -} - -.hgi-stroke.hgi-arrow-up-05::before { - content: "\f1636"; -} - -.hgi-stroke.hgi-arrow-up-az::before { - content: "\f2ffd"; -} - -.hgi-stroke.hgi-arrow-up-big::before { - content: "\f1637"; -} - -.hgi-stroke.hgi-arrow-up-double::before { - content: "\f1638"; -} - -.hgi-stroke.hgi-arrow-up-down::before { - content: "\f1639"; -} - -.hgi-stroke.hgi-arrow-up-from-dot::before { - content: "\f163a"; -} - -.hgi-stroke.hgi-arrow-up-from-line-2::before { - content: "\f2ffe"; -} - -.hgi-stroke.hgi-arrow-up-from-line::before { - content: "\f163b"; -} - -.hgi-stroke.hgi-arrow-up-left-01::before { - content: "\f163c"; -} - -.hgi-stroke.hgi-arrow-up-left-02::before { - content: "\f163d"; -} - -.hgi-stroke.hgi-arrow-up-narrow-wide::before { - content: "\f163e"; -} - -.hgi-stroke.hgi-arrow-up-one-0::before { - content: "\f163f"; -} - -.hgi-stroke.hgi-arrow-up-right-01::before { - content: "\f1640"; -} - -.hgi-stroke.hgi-arrow-up-right-02::before { - content: "\f1641"; -} - -.hgi-stroke.hgi-arrow-up-right-03::before { - content: "\f2fff"; -} - -.hgi-stroke.hgi-arrow-up-right-stack::before { - content: "\f1642"; -} - -.hgi-stroke.hgi-arrow-up-to-line::before { - content: "\f3000"; -} - -.hgi-stroke.hgi-arrow-up-wide-narrow::before { - content: "\f1643"; -} - -.hgi-stroke.hgi-arrow-up-z-a::before { - content: "\f1644"; -} - -.hgi-stroke.hgi-arrow-up-za::before { - content: "\f3001"; -} - -.hgi-stroke.hgi-arrow-up-zero-1::before { - content: "\f1645"; -} - -.hgi-stroke.hgi-arrow-up01::before { - content: "\f3002"; -} - -.hgi-stroke.hgi-arrow-up10::before { - content: "\f3003"; -} - -.hgi-stroke.hgi-arrow-vertical::before { - content: "\f1646"; -} - -.hgi-stroke.hgi-arrows-up-from-line::before { - content: "\f3004"; -} - -.hgi-stroke.hgi-artboard-tool::before { - content: "\f1647"; -} - -.hgi-stroke.hgi-artboard::before { - content: "\f1648"; -} - -.hgi-stroke.hgi-artificial-intelligence-01::before { - content: "\f1649"; -} - -.hgi-stroke.hgi-artificial-intelligence-02::before { - content: "\f164a"; -} - -.hgi-stroke.hgi-artificial-intelligence-03::before { - content: "\f164b"; -} - -.hgi-stroke.hgi-artificial-intelligence-04::before { - content: "\f164c"; -} - -.hgi-stroke.hgi-artificial-intelligence-05::before { - content: "\f164d"; -} - -.hgi-stroke.hgi-artificial-intelligence-06::before { - content: "\f164e"; -} - -.hgi-stroke.hgi-artificial-intelligence-07::before { - content: "\f164f"; -} - -.hgi-stroke.hgi-artificial-intelligence-08::before { - content: "\f1650"; -} - -.hgi-stroke.hgi-aspect-ratio::before { - content: "\f1651"; -} - -.hgi-stroke.hgi-assignments::before { - content: "\f1652"; -} - -.hgi-stroke.hgi-asterisk-02::before { - content: "\f1653"; -} - -.hgi-stroke.hgi-asterisk::before { - content: "\f1654"; -} - -.hgi-stroke.hgi-asteroid-01-2::before { - content: "\f3005"; -} - -.hgi-stroke.hgi-asteroid-01::before { - content: "\f1655"; -} - -.hgi-stroke.hgi-asteroid-02-2::before { - content: "\f3006"; -} - -.hgi-stroke.hgi-asteroid-02::before { - content: "\f1656"; -} - -.hgi-stroke.hgi-astronaut-01-2::before { - content: "\f3007"; -} - -.hgi-stroke.hgi-astronaut-01::before { - content: "\f1657"; -} - -.hgi-stroke.hgi-astronaut-02-2::before { - content: "\f3008"; -} - -.hgi-stroke.hgi-astronaut-02::before { - content: "\f1658"; -} - -.hgi-stroke.hgi-at-sign::before { - content: "\f3009"; -} - -.hgi-stroke.hgi-at::before { - content: "\f1659"; -} - -.hgi-stroke.hgi-atm-01::before { - content: "\f165a"; -} - -.hgi-stroke.hgi-atm-02::before { - content: "\f165b"; -} - -.hgi-stroke.hgi-atom-01::before { - content: "\f165c"; -} - -.hgi-stroke.hgi-atom-02::before { - content: "\f165d"; -} - -.hgi-stroke.hgi-atomic-power::before { - content: "\f165e"; -} - -.hgi-stroke.hgi-attachment-01::before { - content: "\f165f"; -} - -.hgi-stroke.hgi-attachment-02::before { - content: "\f1660"; -} - -.hgi-stroke.hgi-attachment-circle::before { - content: "\f1661"; -} - -.hgi-stroke.hgi-attachment-square::before { - content: "\f1662"; -} - -.hgi-stroke.hgi-attachment::before { - content: "\f1663"; -} - -.hgi-stroke.hgi-auction::before { - content: "\f1664"; -} - -.hgi-stroke.hgi-audio-book-01::before { - content: "\f1665"; -} - -.hgi-stroke.hgi-audio-book-02::before { - content: "\f1666"; -} - -.hgi-stroke.hgi-audio-book-03::before { - content: "\f1667"; -} - -.hgi-stroke.hgi-audio-book-04::before { - content: "\f1668"; -} - -.hgi-stroke.hgi-audio-lines::before { - content: "\f300a"; -} - -.hgi-stroke.hgi-audio-wave-01::before { - content: "\f1669"; -} - -.hgi-stroke.hgi-audio-wave-02::before { - content: "\f166a"; -} - -.hgi-stroke.hgi-audio-waveform::before { - content: "\f300b"; -} - -.hgi-stroke.hgi-audit-01::before { - content: "\f166b"; -} - -.hgi-stroke.hgi-audit-02::before { - content: "\f166c"; -} - -.hgi-stroke.hgi-augmented-reality-ar::before { - content: "\f166d"; -} - -.hgi-stroke.hgi-authorized::before { - content: "\f166e"; -} - -.hgi-stroke.hgi-auto-conversations::before { - content: "\f166f"; -} - -.hgi-stroke.hgi-automotive-battery-01::before { - content: "\f1670"; -} - -.hgi-stroke.hgi-automotive-battery-02::before { - content: "\f1671"; -} - -.hgi-stroke.hgi-avalanche::before { - content: "\f1672"; -} - -.hgi-stroke.hgi-avocado::before { - content: "\f1673"; -} - -.hgi-stroke.hgi-award-01::before { - content: "\f1674"; -} - -.hgi-stroke.hgi-award-02::before { - content: "\f1675"; -} - -.hgi-stroke.hgi-award-03::before { - content: "\f1676"; -} - -.hgi-stroke.hgi-award-04::before { - content: "\f1677"; -} - -.hgi-stroke.hgi-award-05::before { - content: "\f1678"; -} - -.hgi-stroke.hgi-aws-lambda::before { - content: "\f1679"; -} - -.hgi-stroke.hgi-axe::before { - content: "\f167a"; -} - -.hgi-stroke.hgi-axis-three-d::before { - content: "\f167b"; -} - -.hgi-stroke.hgi-axis3d::before { - content: "\f300e"; -} - -.hgi-stroke.hgi-baby-01::before { - content: "\f169a"; -} - -.hgi-stroke.hgi-baby-02::before { - content: "\f169b"; -} - -.hgi-stroke.hgi-baby-bed-01::before { - content: "\f169c"; -} - -.hgi-stroke.hgi-baby-bed-02::before { - content: "\f169d"; -} - -.hgi-stroke.hgi-baby-bottle::before { - content: "\f169e"; -} - -.hgi-stroke.hgi-baby-boy-dress::before { - content: "\f169f"; -} - -.hgi-stroke.hgi-baby-girl-dress::before { - content: "\f16a0"; -} - -.hgi-stroke.hgi-back-muscle-body::before { - content: "\f16a1"; -} - -.hgi-stroke.hgi-background::before { - content: "\f16a2"; -} - -.hgi-stroke.hgi-backpack-01::before { - content: "\f16a3"; -} - -.hgi-stroke.hgi-backpack-02::before { - content: "\f16a4"; -} - -.hgi-stroke.hgi-backpack-03::before { - content: "\f16a5"; -} - -.hgi-stroke.hgi-backward-01::before { - content: "\f16a6"; -} - -.hgi-stroke.hgi-backward-02::before { - content: "\f16a7"; -} - -.hgi-stroke.hgi-bacteria::before { - content: "\f16a8"; -} - -.hgi-stroke.hgi-badge-alert::before { - content: "\f16a9"; -} - -.hgi-stroke.hgi-badge-cent::before { - content: "\f16aa"; -} - -.hgi-stroke.hgi-badge-check::before { - content: "\f300f"; -} - -.hgi-stroke.hgi-badge-dollar-sign::before { - content: "\f16ab"; -} - -.hgi-stroke.hgi-badge-euro::before { - content: "\f16ac"; -} - -.hgi-stroke.hgi-badge-indian-rupee::before { - content: "\f16ad"; -} - -.hgi-stroke.hgi-badge-info::before { - content: "\f16ae"; -} - -.hgi-stroke.hgi-badge-japanese-yen::before { - content: "\f16af"; -} - -.hgi-stroke.hgi-badge-minus::before { - content: "\f3010"; -} - -.hgi-stroke.hgi-badge-percent::before { - content: "\f16b0"; -} - -.hgi-stroke.hgi-badge-plus::before { - content: "\f3011"; -} - -.hgi-stroke.hgi-badge-pound-sterling::before { - content: "\f16b1"; -} - -.hgi-stroke.hgi-badge-question-mark::before { - content: "\f3012"; -} - -.hgi-stroke.hgi-badge-russian-ruble::before { - content: "\f16b2"; -} - -.hgi-stroke.hgi-badge-swiss-franc::before { - content: "\f16b3"; -} - -.hgi-stroke.hgi-badge-turkish-lira::before { - content: "\f16b4"; -} - -.hgi-stroke.hgi-badge-x::before { - content: "\f3013"; -} - -.hgi-stroke.hgi-badge::before { - content: "\f16b5"; -} - -.hgi-stroke.hgi-badminton-shuttle::before { - content: "\f16b6"; -} - -.hgi-stroke.hgi-badminton::before { - content: "\f16b7"; -} - -.hgi-stroke.hgi-baggage-claim::before { - content: "\f3014"; -} - -.hgi-stroke.hgi-balance-scale::before { - content: "\f16b8"; -} - -.hgi-stroke.hgi-balloon::before { - content: "\f3015"; -} - -.hgi-stroke.hgi-balloons::before { - content: "\f16b9"; -} - -.hgi-stroke.hgi-ban::before { - content: "\f3016"; -} - -.hgi-stroke.hgi-banana::before { - content: "\f16ba"; -} - -.hgi-stroke.hgi-bandage-2::before { - content: "\f3017"; -} - -.hgi-stroke.hgi-bandage::before { - content: "\f16bb"; -} - -.hgi-stroke.hgi-bank::before { - content: "\f16bc"; -} - -.hgi-stroke.hgi-banknote-arrow-down::before { - content: "\f3019"; -} - -.hgi-stroke.hgi-banknote-arrow-up::before { - content: "\f301a"; -} - -.hgi-stroke.hgi-banknote-x::before { - content: "\f301b"; -} - -.hgi-stroke.hgi-banknote::before { - content: "\f3018"; -} - -.hgi-stroke.hgi-bar-chart-horizontal::before { - content: "\f16bd"; -} - -.hgi-stroke.hgi-bar-chart::before { - content: "\f16be"; -} - -.hgi-stroke.hgi-bar-code-01::before { - content: "\f16bf"; -} - -.hgi-stroke.hgi-bar-code-02::before { - content: "\f16c0"; -} - -.hgi-stroke.hgi-barcode-scan::before { - content: "\f16c1"; -} - -.hgi-stroke.hgi-barcode::before { - content: "\f301c"; -} - -.hgi-stroke.hgi-barns::before { - content: "\f16c2"; -} - -.hgi-stroke.hgi-barrel::before { - content: "\f16c3"; -} - -.hgi-stroke.hgi-baseball-bat::before { - content: "\f16c4"; -} - -.hgi-stroke.hgi-baseball-helmet::before { - content: "\f16c5"; -} - -.hgi-stroke.hgi-baseball::before { - content: "\f16c6"; -} - -.hgi-stroke.hgi-baseline::before { - content: "\f301d"; -} - -.hgi-stroke.hgi-bash-2::before { - content: "\f301e"; -} - -.hgi-stroke.hgi-bash::before { - content: "\f16c7"; -} - -.hgi-stroke.hgi-basketball-01::before { - content: "\f16c8"; -} - -.hgi-stroke.hgi-basketball-02::before { - content: "\f16c9"; -} - -.hgi-stroke.hgi-basketball-hoop::before { - content: "\f16ca"; -} - -.hgi-stroke.hgi-bath::before { - content: "\f301f"; -} - -.hgi-stroke.hgi-bathtub-01::before { - content: "\f16cb"; -} - -.hgi-stroke.hgi-bathtub-02::before { - content: "\f16cc"; -} - -.hgi-stroke.hgi-batteries-energy::before { - content: "\f16cd"; -} - -.hgi-stroke.hgi-battery-charging-01::before { - content: "\f16ce"; -} - -.hgi-stroke.hgi-battery-charging-02::before { - content: "\f16cf"; -} - -.hgi-stroke.hgi-battery-eco-charging::before { - content: "\f16d0"; -} - -.hgi-stroke.hgi-battery-empty::before { - content: "\f16d1"; -} - -.hgi-stroke.hgi-battery-full::before { - content: "\f16d2"; -} - -.hgi-stroke.hgi-battery-low::before { - content: "\f16d3"; -} - -.hgi-stroke.hgi-battery-medium-01::before { - content: "\f16d4"; -} - -.hgi-stroke.hgi-battery-medium-02::before { - content: "\f16d5"; -} - -.hgi-stroke.hgi-battery-plus::before { - content: "\f16d6"; -} - -.hgi-stroke.hgi-battery-warning::before { - content: "\f16d7"; -} - -.hgi-stroke.hgi-battery::before { - content: "\f3020"; -} - -.hgi-stroke.hgi-bbq-grill::before { - content: "\f16d8"; -} - -.hgi-stroke.hgi-beach-02::before { - content: "\f16d9"; -} - -.hgi-stroke.hgi-beach::before { - content: "\f16da"; -} - -.hgi-stroke.hgi-beaker::before { - content: "\f3021"; -} - -.hgi-stroke.hgi-bean-off::before { - content: "\f16db"; -} - -.hgi-stroke.hgi-bean::before { - content: "\f16dc"; -} - -.hgi-stroke.hgi-beater::before { - content: "\f16dd"; -} - -.hgi-stroke.hgi-bebo::before { - content: "\f16de"; -} - -.hgi-stroke.hgi-bed-bunk::before { - content: "\f16df"; -} - -.hgi-stroke.hgi-bed-double::before { - content: "\f16e0"; -} - -.hgi-stroke.hgi-bed-single-01::before { - content: "\f16e1"; -} - -.hgi-stroke.hgi-bed-single-02::before { - content: "\f16e2"; -} - -.hgi-stroke.hgi-bed::before { - content: "\f16e3"; -} - -.hgi-stroke.hgi-bedug-01::before { - content: "\f16e4"; -} - -.hgi-stroke.hgi-bedug-02::before { - content: "\f16e5"; -} - -.hgi-stroke.hgi-beef-off::before { - content: "\f3022"; -} - -.hgi-stroke.hgi-beef::before { - content: "\f16e6"; -} - -.hgi-stroke.hgi-beer-off::before { - content: "\f3024"; -} - -.hgi-stroke.hgi-beer::before { - content: "\f3023"; -} - -.hgi-stroke.hgi-behance-01::before { - content: "\f16e7"; -} - -.hgi-stroke.hgi-behance-02::before { - content: "\f16e8"; -} - -.hgi-stroke.hgi-bell-dot::before { - content: "\f16e9"; -} - -.hgi-stroke.hgi-bell-electric::before { - content: "\f16ea"; -} - -.hgi-stroke.hgi-bell-minus::before { - content: "\f3026"; -} - -.hgi-stroke.hgi-bell-off::before { - content: "\f3027"; -} - -.hgi-stroke.hgi-bell-plus::before { - content: "\f3028"; -} - -.hgi-stroke.hgi-bell-ring::before { - content: "\f3029"; -} - -.hgi-stroke.hgi-bell::before { - content: "\f3025"; -} - -.hgi-stroke.hgi-belt::before { - content: "\f16eb"; -} - -.hgi-stroke.hgi-bend-tool::before { - content: "\f16ec"; -} - -.hgi-stroke.hgi-berlin-tower::before { - content: "\f16ed"; -} - -.hgi-stroke.hgi-berlin::before { - content: "\f16ee"; -} - -.hgi-stroke.hgi-beta::before { - content: "\f16ef"; -} - -.hgi-stroke.hgi-between-horizontal-end::before { - content: "\f16f0"; -} - -.hgi-stroke.hgi-between-horizontal-start::before { - content: "\f302a"; -} - -.hgi-stroke.hgi-between-vertical-end::before { - content: "\f16f1"; -} - -.hgi-stroke.hgi-between-vertical-start::before { - content: "\f302b"; -} - -.hgi-stroke.hgi-biceps-flexed::before { - content: "\f302c"; -} - -.hgi-stroke.hgi-bicycle-01::before { - content: "\f16f2"; -} - -.hgi-stroke.hgi-bicycle::before { - content: "\f16f3"; -} - -.hgi-stroke.hgi-bike::before { - content: "\f302d"; -} - -.hgi-stroke.hgi-billiard-01::before { - content: "\f16f4"; -} - -.hgi-stroke.hgi-billiard-02::before { - content: "\f16f5"; -} - -.hgi-stroke.hgi-binary-code::before { - content: "\f16f6"; -} - -.hgi-stroke.hgi-binary::before { - content: "\f302e"; -} - -.hgi-stroke.hgi-bing::before { - content: "\f16f7"; -} - -.hgi-stroke.hgi-binoculars::before { - content: "\f16f8"; -} - -.hgi-stroke.hgi-bio-energy::before { - content: "\f16f9"; -} - -.hgi-stroke.hgi-biohazard::before { - content: "\f16fa"; -} - -.hgi-stroke.hgi-biomass-energy::before { - content: "\f16fb"; -} - -.hgi-stroke.hgi-biometric-access::before { - content: "\f16fc"; -} - -.hgi-stroke.hgi-biometric-device::before { - content: "\f16fd"; -} - -.hgi-stroke.hgi-bird::before { - content: "\f16fe"; -} - -.hgi-stroke.hgi-birdhouse::before { - content: "\f16ff"; -} - -.hgi-stroke.hgi-birthday-cake::before { - content: "\f1700"; -} - -.hgi-stroke.hgi-biscuit::before { - content: "\f1701"; -} - -.hgi-stroke.hgi-bitcoin-01::before { - content: "\f1702"; -} - -.hgi-stroke.hgi-bitcoin-02::before { - content: "\f1703"; -} - -.hgi-stroke.hgi-bitcoin-03::before { - content: "\f1704"; -} - -.hgi-stroke.hgi-bitcoin-04::before { - content: "\f1705"; -} - -.hgi-stroke.hgi-bitcoin-bag::before { - content: "\f1706"; -} - -.hgi-stroke.hgi-bitcoin-circle::before { - content: "\f1707"; -} - -.hgi-stroke.hgi-bitcoin-cloud::before { - content: "\f1708"; -} - -.hgi-stroke.hgi-bitcoin-cpu::before { - content: "\f1709"; -} - -.hgi-stroke.hgi-bitcoin-credit-card::before { - content: "\f170a"; -} - -.hgi-stroke.hgi-bitcoin-database::before { - content: "\f170b"; -} - -.hgi-stroke.hgi-bitcoin-down-01::before { - content: "\f170c"; -} - -.hgi-stroke.hgi-bitcoin-down-02::before { - content: "\f170d"; -} - -.hgi-stroke.hgi-bitcoin-ellipse::before { - content: "\f170e"; -} - -.hgi-stroke.hgi-bitcoin-eye::before { - content: "\f170f"; -} - -.hgi-stroke.hgi-bitcoin-filter::before { - content: "\f1710"; -} - -.hgi-stroke.hgi-bitcoin-flashdisk::before { - content: "\f1711"; -} - -.hgi-stroke.hgi-bitcoin-graph::before { - content: "\f1712"; -} - -.hgi-stroke.hgi-bitcoin-invoice::before { - content: "\f1713"; -} - -.hgi-stroke.hgi-bitcoin-key::before { - content: "\f1714"; -} - -.hgi-stroke.hgi-bitcoin-location::before { - content: "\f1715"; -} - -.hgi-stroke.hgi-bitcoin-lock::before { - content: "\f1716"; -} - -.hgi-stroke.hgi-bitcoin-mail::before { - content: "\f1717"; -} - -.hgi-stroke.hgi-bitcoin-mind::before { - content: "\f1718"; -} - -.hgi-stroke.hgi-bitcoin-money-01::before { - content: "\f1719"; -} - -.hgi-stroke.hgi-bitcoin-money-02::before { - content: "\f171a"; -} - -.hgi-stroke.hgi-bitcoin-pie-chart::before { - content: "\f171b"; -} - -.hgi-stroke.hgi-bitcoin-piggy-bank::before { - content: "\f171c"; -} - -.hgi-stroke.hgi-bitcoin-presentation::before { - content: "\f171d"; -} - -.hgi-stroke.hgi-bitcoin-receipt::before { - content: "\f171e"; -} - -.hgi-stroke.hgi-bitcoin-receive::before { - content: "\f171f"; -} - -.hgi-stroke.hgi-bitcoin-rectangle::before { - content: "\f1720"; -} - -.hgi-stroke.hgi-bitcoin-safe::before { - content: "\f1721"; -} - -.hgi-stroke.hgi-bitcoin-search::before { - content: "\f1722"; -} - -.hgi-stroke.hgi-bitcoin-send::before { - content: "\f1723"; -} - -.hgi-stroke.hgi-bitcoin-setting::before { - content: "\f1724"; -} - -.hgi-stroke.hgi-bitcoin-shield::before { - content: "\f1725"; -} - -.hgi-stroke.hgi-bitcoin-shopping::before { - content: "\f1726"; -} - -.hgi-stroke.hgi-bitcoin-smartphone-01::before { - content: "\f1727"; -} - -.hgi-stroke.hgi-bitcoin-smartphone-02::before { - content: "\f1728"; -} - -.hgi-stroke.hgi-bitcoin-square::before { - content: "\f1729"; -} - -.hgi-stroke.hgi-bitcoin-store::before { - content: "\f172a"; -} - -.hgi-stroke.hgi-bitcoin-tag::before { - content: "\f172b"; -} - -.hgi-stroke.hgi-bitcoin-target::before { - content: "\f172c"; -} - -.hgi-stroke.hgi-bitcoin-transaction::before { - content: "\f172d"; -} - -.hgi-stroke.hgi-bitcoin-up-01::before { - content: "\f172e"; -} - -.hgi-stroke.hgi-bitcoin-up-02::before { - content: "\f172f"; -} - -.hgi-stroke.hgi-bitcoin-wallet::before { - content: "\f1730"; -} - -.hgi-stroke.hgi-bitcoin-withdraw::before { - content: "\f1731"; -} - -.hgi-stroke.hgi-bitcoin::before { - content: "\f1732"; -} - -.hgi-stroke.hgi-black-hole-01::before { - content: "\f1733"; -} - -.hgi-stroke.hgi-black-hole-2::before { - content: "\f302f"; -} - -.hgi-stroke.hgi-black-hole::before { - content: "\f1734"; -} - -.hgi-stroke.hgi-blend::before { - content: "\f1735"; -} - -.hgi-stroke.hgi-blender::before { - content: "\f1736"; -} - -.hgi-stroke.hgi-blinds::before { - content: "\f1737"; -} - -.hgi-stroke.hgi-block-game::before { - content: "\f1738"; -} - -.hgi-stroke.hgi-blockchain-01::before { - content: "\f1739"; -} - -.hgi-stroke.hgi-blockchain-02::before { - content: "\f173a"; -} - -.hgi-stroke.hgi-blockchain-03::before { - content: "\f173b"; -} - -.hgi-stroke.hgi-blockchain-04::before { - content: "\f173c"; -} - -.hgi-stroke.hgi-blockchain-05::before { - content: "\f173d"; -} - -.hgi-stroke.hgi-blockchain-06::before { - content: "\f173e"; -} - -.hgi-stroke.hgi-blockchain-07::before { - content: "\f173f"; -} - -.hgi-stroke.hgi-blocked::before { - content: "\f1740"; -} - -.hgi-stroke.hgi-blocks::before { - content: "\f3030"; -} - -.hgi-stroke.hgi-blogger::before { - content: "\f1741"; -} - -.hgi-stroke.hgi-blood-bag::before { - content: "\f1742"; -} - -.hgi-stroke.hgi-blood-bottle::before { - content: "\f1743"; -} - -.hgi-stroke.hgi-blood-pressure::before { - content: "\f1744"; -} - -.hgi-stroke.hgi-blood-type::before { - content: "\f1745"; -} - -.hgi-stroke.hgi-blood::before { - content: "\f1746"; -} - -.hgi-stroke.hgi-blueprint::before { - content: "\f3031"; -} - -.hgi-stroke.hgi-bluesky::before { - content: "\f1747"; -} - -.hgi-stroke.hgi-bluetooth-circle::before { - content: "\f1748"; -} - -.hgi-stroke.hgi-bluetooth-connected::before { - content: "\f3032"; -} - -.hgi-stroke.hgi-bluetooth-not-connected::before { - content: "\f1749"; -} - -.hgi-stroke.hgi-bluetooth-off::before { - content: "\f3033"; -} - -.hgi-stroke.hgi-bluetooth-search::before { - content: "\f174a"; -} - -.hgi-stroke.hgi-bluetooth-searching::before { - content: "\f3034"; -} - -.hgi-stroke.hgi-bluetooth-square::before { - content: "\f174b"; -} - -.hgi-stroke.hgi-bluetooth::before { - content: "\f174c"; -} - -.hgi-stroke.hgi-blur::before { - content: "\f174d"; -} - -.hgi-stroke.hgi-blush-brush-01::before { - content: "\f174e"; -} - -.hgi-stroke.hgi-blush-brush-02::before { - content: "\f174f"; -} - -.hgi-stroke.hgi-board-math::before { - content: "\f1750"; -} - -.hgi-stroke.hgi-boat::before { - content: "\f1751"; -} - -.hgi-stroke.hgi-body-armor::before { - content: "\f1752"; -} - -.hgi-stroke.hgi-body-part-leg::before { - content: "\f1753"; -} - -.hgi-stroke.hgi-body-part-muscle::before { - content: "\f1754"; -} - -.hgi-stroke.hgi-body-part-six-pack-2::before { - content: "\f3035"; -} - -.hgi-stroke.hgi-body-part-six-pack::before { - content: "\f1755"; -} - -.hgi-stroke.hgi-body-soap::before { - content: "\f1756"; -} - -.hgi-stroke.hgi-body-weight::before { - content: "\f1757"; -} - -.hgi-stroke.hgi-bold::before { - content: "\f3036"; -} - -.hgi-stroke.hgi-bolt::before { - content: "\f3037"; -} - -.hgi-stroke.hgi-bomb::before { - content: "\f1758"; -} - -.hgi-stroke.hgi-bone-01::before { - content: "\f1759"; -} - -.hgi-stroke.hgi-bone-02::before { - content: "\f175a"; -} - -.hgi-stroke.hgi-book-01::before { - content: "\f175b"; -} - -.hgi-stroke.hgi-book-02::before { - content: "\f175c"; -} - -.hgi-stroke.hgi-book-03::before { - content: "\f175d"; -} - -.hgi-stroke.hgi-book-04::before { - content: "\f175e"; -} - -.hgi-stroke.hgi-book-a::before { - content: "\f175f"; -} - -.hgi-stroke.hgi-book-alert::before { - content: "\f1760"; -} - -.hgi-stroke.hgi-book-bookmark-01::before { - content: "\f1761"; -} - -.hgi-stroke.hgi-book-bookmark-02::before { - content: "\f1762"; -} - -.hgi-stroke.hgi-book-check::before { - content: "\f1763"; -} - -.hgi-stroke.hgi-book-copy::before { - content: "\f1764"; -} - -.hgi-stroke.hgi-book-dashed::before { - content: "\f1765"; -} - -.hgi-stroke.hgi-book-down::before { - content: "\f3038"; -} - -.hgi-stroke.hgi-book-download::before { - content: "\f1766"; -} - -.hgi-stroke.hgi-book-edit::before { - content: "\f1767"; -} - -.hgi-stroke.hgi-book-headphones::before { - content: "\f3039"; -} - -.hgi-stroke.hgi-book-heart::before { - content: "\f1768"; -} - -.hgi-stroke.hgi-book-image::before { - content: "\f1769"; -} - -.hgi-stroke.hgi-book-key::before { - content: "\f176a"; -} - -.hgi-stroke.hgi-book-lock::before { - content: "\f176b"; -} - -.hgi-stroke.hgi-book-marked::before { - content: "\f303a"; -} - -.hgi-stroke.hgi-book-minus::before { - content: "\f176c"; -} - -.hgi-stroke.hgi-book-open-01::before { - content: "\f176d"; -} - -.hgi-stroke.hgi-book-open-02::before { - content: "\f176e"; -} - -.hgi-stroke.hgi-book-open-check::before { - content: "\f176f"; -} - -.hgi-stroke.hgi-book-open-text::before { - content: "\f1770"; -} - -.hgi-stroke.hgi-book-plus::before { - content: "\f1771"; -} - -.hgi-stroke.hgi-book-search::before { - content: "\f1772"; -} - -.hgi-stroke.hgi-book-text::before { - content: "\f303b"; -} - -.hgi-stroke.hgi-book-type::before { - content: "\f1773"; -} - -.hgi-stroke.hgi-book-up-2::before { - content: "\f1774"; -} - -.hgi-stroke.hgi-book-up2::before { - content: "\f303c"; -} - -.hgi-stroke.hgi-book-upload::before { - content: "\f1775"; -} - -.hgi-stroke.hgi-book-user::before { - content: "\f1776"; -} - -.hgi-stroke.hgi-book-x::before { - content: "\f1777"; -} - -.hgi-stroke.hgi-bookmark-01::before { - content: "\f1778"; -} - -.hgi-stroke.hgi-bookmark-02::before { - content: "\f1779"; -} - -.hgi-stroke.hgi-bookmark-03::before { - content: "\f177a"; -} - -.hgi-stroke.hgi-bookmark-add-01::before { - content: "\f177b"; -} - -.hgi-stroke.hgi-bookmark-add-02::before { - content: "\f177c"; -} - -.hgi-stroke.hgi-bookmark-block-01::before { - content: "\f177d"; -} - -.hgi-stroke.hgi-bookmark-block-02::before { - content: "\f177e"; -} - -.hgi-stroke.hgi-bookmark-check-01::before { - content: "\f177f"; -} - -.hgi-stroke.hgi-bookmark-check-02::before { - content: "\f1780"; -} - -.hgi-stroke.hgi-bookmark-minus-01::before { - content: "\f1781"; -} - -.hgi-stroke.hgi-bookmark-minus-02::before { - content: "\f1782"; -} - -.hgi-stroke.hgi-bookmark-off-01::before { - content: "\f1783"; -} - -.hgi-stroke.hgi-bookmark-off-02::before { - content: "\f1784"; -} - -.hgi-stroke.hgi-bookmark-plus::before { - content: "\f303d"; -} - -.hgi-stroke.hgi-bookmark-remove-01::before { - content: "\f1785"; -} - -.hgi-stroke.hgi-bookmark-remove-02::before { - content: "\f1786"; -} - -.hgi-stroke.hgi-bookmark-x::before { - content: "\f303e"; -} - -.hgi-stroke.hgi-books-01::before { - content: "\f1787"; -} - -.hgi-stroke.hgi-books-02::before { - content: "\f1788"; -} - -.hgi-stroke.hgi-bookshelf-01::before { - content: "\f1789"; -} - -.hgi-stroke.hgi-bookshelf-02::before { - content: "\f178a"; -} - -.hgi-stroke.hgi-bookshelf-03::before { - content: "\f178b"; -} - -.hgi-stroke.hgi-boom-box::before { - content: "\f178c"; -} - -.hgi-stroke.hgi-bootstrap::before { - content: "\f178d"; -} - -.hgi-stroke.hgi-border-all-01::before { - content: "\f178e"; -} - -.hgi-stroke.hgi-border-all-02::before { - content: "\f178f"; -} - -.hgi-stroke.hgi-border-bottom-01::before { - content: "\f1790"; -} - -.hgi-stroke.hgi-border-bottom-02::before { - content: "\f1791"; -} - -.hgi-stroke.hgi-border-full::before { - content: "\f1792"; -} - -.hgi-stroke.hgi-border-horizontal::before { - content: "\f1793"; -} - -.hgi-stroke.hgi-border-inner::before { - content: "\f1794"; -} - -.hgi-stroke.hgi-border-left-01::before { - content: "\f1795"; -} - -.hgi-stroke.hgi-border-left-02::before { - content: "\f1796"; -} - -.hgi-stroke.hgi-border-none-01::before { - content: "\f1797"; -} - -.hgi-stroke.hgi-border-none-02::before { - content: "\f1798"; -} - -.hgi-stroke.hgi-border-right-01::before { - content: "\f1799"; -} - -.hgi-stroke.hgi-border-right-02::before { - content: "\f179a"; -} - -.hgi-stroke.hgi-border-top-01::before { - content: "\f179b"; -} - -.hgi-stroke.hgi-border-top-02::before { - content: "\f179c"; -} - -.hgi-stroke.hgi-border-vertical::before { - content: "\f179d"; -} - -.hgi-stroke.hgi-borobudur::before { - content: "\f179e"; -} - -.hgi-stroke.hgi-bot-message-square::before { - content: "\f303f"; -} - -.hgi-stroke.hgi-bot-off::before { - content: "\f3040"; -} - -.hgi-stroke.hgi-bot::before { - content: "\f179f"; -} - -.hgi-stroke.hgi-bottle-wine::before { - content: "\f3041"; -} - -.hgi-stroke.hgi-bounce-left::before { - content: "\f17a0"; -} - -.hgi-stroke.hgi-bounce-right::before { - content: "\f17a1"; -} - -.hgi-stroke.hgi-bounding-box::before { - content: "\f17a2"; -} - -.hgi-stroke.hgi-bow-arrow::before { - content: "\f3042"; -} - -.hgi-stroke.hgi-bow-tie::before { - content: "\f17a3"; -} - -.hgi-stroke.hgi-bowling-ball::before { - content: "\f17a4"; -} - -.hgi-stroke.hgi-bowling-pins::before { - content: "\f17a5"; -} - -.hgi-stroke.hgi-bowling::before { - content: "\f17a6"; -} - -.hgi-stroke.hgi-box::before { - content: "\f3043"; -} - -.hgi-stroke.hgi-boxer::before { - content: "\f17a7"; -} - -.hgi-stroke.hgi-boxes::before { - content: "\f3044"; -} - -.hgi-stroke.hgi-boxing-bag::before { - content: "\f17a8"; -} - -.hgi-stroke.hgi-boxing-glove-01::before { - content: "\f17a9"; -} - -.hgi-stroke.hgi-boxing-glove::before { - content: "\f17aa"; -} - -.hgi-stroke.hgi-braces::before { - content: "\f3045"; -} - -.hgi-stroke.hgi-brackets::before { - content: "\f3046"; -} - -.hgi-stroke.hgi-brain-01::before { - content: "\f17ab"; -} - -.hgi-stroke.hgi-brain-02::before { - content: "\f17ac"; -} - -.hgi-stroke.hgi-brain-03::before { - content: "\f17ad"; -} - -.hgi-stroke.hgi-brain-circuit::before { - content: "\f3047"; -} - -.hgi-stroke.hgi-brain-cog::before { - content: "\f17ae"; -} - -.hgi-stroke.hgi-brain::before { - content: "\f17af"; -} - -.hgi-stroke.hgi-brandfetch::before { - content: "\f17b0"; -} - -.hgi-stroke.hgi-bread-01::before { - content: "\f17b1"; -} - -.hgi-stroke.hgi-bread-02::before { - content: "\f17b2"; -} - -.hgi-stroke.hgi-bread-03::before { - content: "\f17b3"; -} - -.hgi-stroke.hgi-bread-04::before { - content: "\f17b4"; -} - -.hgi-stroke.hgi-breast-pump::before { - content: "\f17b5"; -} - -.hgi-stroke.hgi-brick-wall-fire::before { - content: "\f3048"; -} - -.hgi-stroke.hgi-brick-wall-shield::before { - content: "\f17b6"; -} - -.hgi-stroke.hgi-brick-wall::before { - content: "\f17b7"; -} - -.hgi-stroke.hgi-bridge::before { - content: "\f17b8"; -} - -.hgi-stroke.hgi-briefcase-01::before { - content: "\f17b9"; -} - -.hgi-stroke.hgi-briefcase-02::before { - content: "\f17ba"; -} - -.hgi-stroke.hgi-briefcase-03::before { - content: "\f17bb"; -} - -.hgi-stroke.hgi-briefcase-04::before { - content: "\f17bc"; -} - -.hgi-stroke.hgi-briefcase-05::before { - content: "\f17bd"; -} - -.hgi-stroke.hgi-briefcase-06::before { - content: "\f17be"; -} - -.hgi-stroke.hgi-briefcase-07::before { - content: "\f17bf"; -} - -.hgi-stroke.hgi-briefcase-08::before { - content: "\f17c0"; -} - -.hgi-stroke.hgi-briefcase-09::before { - content: "\f17c1"; -} - -.hgi-stroke.hgi-briefcase-business::before { - content: "\f3049"; -} - -.hgi-stroke.hgi-briefcase-conveyor-belt::before { - content: "\f17c2"; -} - -.hgi-stroke.hgi-briefcase-dollar::before { - content: "\f17c3"; -} - -.hgi-stroke.hgi-briefcase-medical::before { - content: "\f304a"; -} - -.hgi-stroke.hgi-bring-to-front::before { - content: "\f17c4"; -} - -.hgi-stroke.hgi-broccoli::before { - content: "\f17c5"; -} - -.hgi-stroke.hgi-brochure::before { - content: "\f17c6"; -} - -.hgi-stroke.hgi-broken-bone::before { - content: "\f17c7"; -} - -.hgi-stroke.hgi-browser::before { - content: "\f17c8"; -} - -.hgi-stroke.hgi-brush-cleaning::before { - content: "\f304b"; -} - -.hgi-stroke.hgi-brush::before { - content: "\f17c9"; -} - -.hgi-stroke.hgi-bubble-chat-add::before { - content: "\f17ca"; -} - -.hgi-stroke.hgi-bubble-chat-blocked::before { - content: "\f17cb"; -} - -.hgi-stroke.hgi-bubble-chat-cancel::before { - content: "\f17cc"; -} - -.hgi-stroke.hgi-bubble-chat-delay-2::before { - content: "\f304c"; -} - -.hgi-stroke.hgi-bubble-chat-delay::before { - content: "\f17cd"; -} - -.hgi-stroke.hgi-bubble-chat-done::before { - content: "\f17ce"; -} - -.hgi-stroke.hgi-bubble-chat-download-01::before { - content: "\f17cf"; -} - -.hgi-stroke.hgi-bubble-chat-download-02::before { - content: "\f17d0"; -} - -.hgi-stroke.hgi-bubble-chat-edit::before { - content: "\f17d1"; -} - -.hgi-stroke.hgi-bubble-chat-favourite::before { - content: "\f17d2"; -} - -.hgi-stroke.hgi-bubble-chat-income::before { - content: "\f17d3"; -} - -.hgi-stroke.hgi-bubble-chat-lock::before { - content: "\f17d4"; -} - -.hgi-stroke.hgi-bubble-chat-notification::before { - content: "\f17d5"; -} - -.hgi-stroke.hgi-bubble-chat-outcome::before { - content: "\f17d6"; -} - -.hgi-stroke.hgi-bubble-chat-preview::before { - content: "\f17d7"; -} - -.hgi-stroke.hgi-bubble-chat-question::before { - content: "\f17d8"; -} - -.hgi-stroke.hgi-bubble-chat-search::before { - content: "\f17d9"; -} - -.hgi-stroke.hgi-bubble-chat-secure::before { - content: "\f17da"; -} - -.hgi-stroke.hgi-bubble-chat-spark-01::before { - content: "\f17db"; -} - -.hgi-stroke.hgi-bubble-chat-spark::before { - content: "\f17dc"; -} - -.hgi-stroke.hgi-bubble-chat-temporary::before { - content: "\f17dd"; -} - -.hgi-stroke.hgi-bubble-chat-translate::before { - content: "\f17de"; -} - -.hgi-stroke.hgi-bubble-chat-unlock::before { - content: "\f17df"; -} - -.hgi-stroke.hgi-bubble-chat-upload::before { - content: "\f17e0"; -} - -.hgi-stroke.hgi-bubble-chat-user::before { - content: "\f17e1"; -} - -.hgi-stroke.hgi-bubble-chat::before { - content: "\f17e2"; -} - -.hgi-stroke.hgi-bubble-tea-01::before { - content: "\f17e3"; -} - -.hgi-stroke.hgi-bubble-tea-02::before { - content: "\f17e4"; -} - -.hgi-stroke.hgi-bubbles::before { - content: "\f304d"; -} - -.hgi-stroke.hgi-bucket::before { - content: "\f17e5"; -} - -.hgi-stroke.hgi-bug-01::before { - content: "\f17e6"; -} - -.hgi-stroke.hgi-bug-02::before { - content: "\f17e7"; -} - -.hgi-stroke.hgi-bug-off::before { - content: "\f17e8"; -} - -.hgi-stroke.hgi-bug-play::before { - content: "\f17e9"; -} - -.hgi-stroke.hgi-building-01::before { - content: "\f17ea"; -} - -.hgi-stroke.hgi-building-02::before { - content: "\f17eb"; -} - -.hgi-stroke.hgi-building-03::before { - content: "\f17ec"; -} - -.hgi-stroke.hgi-building-04::before { - content: "\f17ed"; -} - -.hgi-stroke.hgi-building-05::before { - content: "\f17ee"; -} - -.hgi-stroke.hgi-building-06::before { - content: "\f17ef"; -} - -.hgi-stroke.hgi-building::before { - content: "\f304e"; -} - -.hgi-stroke.hgi-building2-2::before { - content: "\f3050"; -} - -.hgi-stroke.hgi-building2::before { - content: "\f304f"; -} - -.hgi-stroke.hgi-bulb-charging::before { - content: "\f17f0"; -} - -.hgi-stroke.hgi-bulb::before { - content: "\f17f1"; -} - -.hgi-stroke.hgi-bullet::before { - content: "\f17f2"; -} - -.hgi-stroke.hgi-bulletproof-vest::before { - content: "\f17f3"; -} - -.hgi-stroke.hgi-burj-al-arab::before { - content: "\f17f4"; -} - -.hgi-stroke.hgi-burning-cd::before { - content: "\f17f5"; -} - -.hgi-stroke.hgi-bus-01::before { - content: "\f17f6"; -} - -.hgi-stroke.hgi-bus-02::before { - content: "\f17f7"; -} - -.hgi-stroke.hgi-bus-03::before { - content: "\f17f8"; -} - -.hgi-stroke.hgi-bus-front::before { - content: "\f3051"; -} - -.hgi-stroke.hgi-c-programming::before { - content: "\f1843"; -} - -.hgi-stroke.hgi-c::before { - content: "\f3052"; -} - -.hgi-stroke.hgi-cabinet-01::before { - content: "\f1844"; -} - -.hgi-stroke.hgi-cabinet-02::before { - content: "\f1845"; -} - -.hgi-stroke.hgi-cabinet-03::before { - content: "\f1846"; -} - -.hgi-stroke.hgi-cabinet-04::before { - content: "\f1847"; -} - -.hgi-stroke.hgi-cable-car::before { - content: "\f1848"; -} - -.hgi-stroke.hgi-cable::before { - content: "\f1849"; -} - -.hgi-stroke.hgi-cactus::before { - content: "\f184a"; -} - -.hgi-stroke.hgi-caduceus::before { - content: "\f184b"; -} - -.hgi-stroke.hgi-cafe::before { - content: "\f184c"; -} - -.hgi-stroke.hgi-cake-slice::before { - content: "\f184d"; -} - -.hgi-stroke.hgi-cake::before { - content: "\f3053"; -} - -.hgi-stroke.hgi-calculate::before { - content: "\f184e"; -} - -.hgi-stroke.hgi-calculator-01::before { - content: "\f184f"; -} - -.hgi-stroke.hgi-calculator::before { - content: "\f1850"; -} - -.hgi-stroke.hgi-calendar-01::before { - content: "\f1851"; -} - -.hgi-stroke.hgi-calendar-02::before { - content: "\f1852"; -} - -.hgi-stroke.hgi-calendar-03::before { - content: "\f1853"; -} - -.hgi-stroke.hgi-calendar-04::before { - content: "\f1854"; -} - -.hgi-stroke.hgi-calendar-05::before { - content: "\f1855"; -} - -.hgi-stroke.hgi-calendar-add-01::before { - content: "\f1856"; -} - -.hgi-stroke.hgi-calendar-add-02::before { - content: "\f1857"; -} - -.hgi-stroke.hgi-calendar-analysis::before { - content: "\f1858"; -} - -.hgi-stroke.hgi-calendar-arrow-down::before { - content: "\f3054"; -} - -.hgi-stroke.hgi-calendar-arrow-up::before { - content: "\f3055"; -} - -.hgi-stroke.hgi-calendar-block-01::before { - content: "\f1859"; -} - -.hgi-stroke.hgi-calendar-block-02::before { - content: "\f185a"; -} - -.hgi-stroke.hgi-calendar-check-01::before { - content: "\f3057"; -} - -.hgi-stroke.hgi-calendar-check-in-01::before { - content: "\f185b"; -} - -.hgi-stroke.hgi-calendar-check-in-02::before { - content: "\f185c"; -} - -.hgi-stroke.hgi-calendar-check-out-01::before { - content: "\f185d"; -} - -.hgi-stroke.hgi-calendar-check-out-02::before { - content: "\f185e"; -} - -.hgi-stroke.hgi-calendar-check::before { - content: "\f3056"; -} - -.hgi-stroke.hgi-calendar-check2::before { - content: "\f3058"; -} - -.hgi-stroke.hgi-calendar-clock::before { - content: "\f3059"; -} - -.hgi-stroke.hgi-calendar-cog::before { - content: "\f305a"; -} - -.hgi-stroke.hgi-calendar-date-1::before { - content: "\f305b"; -} - -.hgi-stroke.hgi-calendar-days::before { - content: "\f305c"; -} - -.hgi-stroke.hgi-calendar-download-01::before { - content: "\f185f"; -} - -.hgi-stroke.hgi-calendar-download-02::before { - content: "\f1860"; -} - -.hgi-stroke.hgi-calendar-favorite-01::before { - content: "\f1861"; -} - -.hgi-stroke.hgi-calendar-favorite-02::before { - content: "\f1862"; -} - -.hgi-stroke.hgi-calendar-fold::before { - content: "\f1863"; -} - -.hgi-stroke.hgi-calendar-heart::before { - content: "\f305d"; -} - -.hgi-stroke.hgi-calendar-lock-01::before { - content: "\f1864"; -} - -.hgi-stroke.hgi-calendar-lock-02::before { - content: "\f1865"; -} - -.hgi-stroke.hgi-calendar-love-01::before { - content: "\f1866"; -} - -.hgi-stroke.hgi-calendar-love-02::before { - content: "\f1867"; -} - -.hgi-stroke.hgi-calendar-minus-01::before { - content: "\f1868"; -} - -.hgi-stroke.hgi-calendar-minus-02::before { - content: "\f1869"; -} - -.hgi-stroke.hgi-calendar-minus::before { - content: "\f305e"; -} - -.hgi-stroke.hgi-calendar-minus2::before { - content: "\f305f"; -} - -.hgi-stroke.hgi-calendar-mortarboard::before { - content: "\f186a"; -} - -.hgi-stroke.hgi-calendar-off::before { - content: "\f186b"; -} - -.hgi-stroke.hgi-calendar-plus-01::before { - content: "\f3061"; -} - -.hgi-stroke.hgi-calendar-plus::before { - content: "\f3060"; -} - -.hgi-stroke.hgi-calendar-plus2::before { - content: "\f3062"; -} - -.hgi-stroke.hgi-calendar-range::before { - content: "\f3063"; -} - -.hgi-stroke.hgi-calendar-remove-01::before { - content: "\f186c"; -} - -.hgi-stroke.hgi-calendar-remove-02::before { - content: "\f186d"; -} - -.hgi-stroke.hgi-calendar-search::before { - content: "\f3064"; -} - -.hgi-stroke.hgi-calendar-setting-01::before { - content: "\f186e"; -} - -.hgi-stroke.hgi-calendar-setting-02::before { - content: "\f186f"; -} - -.hgi-stroke.hgi-calendar-sync::before { - content: "\f1870"; -} - -.hgi-stroke.hgi-calendar-upload-01::before { - content: "\f1871"; -} - -.hgi-stroke.hgi-calendar-upload-02::before { - content: "\f1872"; -} - -.hgi-stroke.hgi-calendar-user::before { - content: "\f1873"; -} - -.hgi-stroke.hgi-calendar-x-01::before { - content: "\f3066"; -} - -.hgi-stroke.hgi-calendar-x::before { - content: "\f3065"; -} - -.hgi-stroke.hgi-calendar-x2::before { - content: "\f3067"; -} - -.hgi-stroke.hgi-calendar1::before { - content: "\f3068"; -} - -.hgi-stroke.hgi-calendars::before { - content: "\f1874"; -} - -.hgi-stroke.hgi-call-02::before { - content: "\f1875"; -} - -.hgi-stroke.hgi-call-add-02::before { - content: "\f1876"; -} - -.hgi-stroke.hgi-call-add::before { - content: "\f1877"; -} - -.hgi-stroke.hgi-call-blocked-02::before { - content: "\f1878"; -} - -.hgi-stroke.hgi-call-blocked::before { - content: "\f1879"; -} - -.hgi-stroke.hgi-call-disabled-02::before { - content: "\f187a"; -} - -.hgi-stroke.hgi-call-disabled::before { - content: "\f187b"; -} - -.hgi-stroke.hgi-call-done-02::before { - content: "\f187c"; -} - -.hgi-stroke.hgi-call-done::before { - content: "\f187d"; -} - -.hgi-stroke.hgi-call-end-01::before { - content: "\f187e"; -} - -.hgi-stroke.hgi-call-end-02::before { - content: "\f187f"; -} - -.hgi-stroke.hgi-call-end-03::before { - content: "\f1880"; -} - -.hgi-stroke.hgi-call-end-04::before { - content: "\f1881"; -} - -.hgi-stroke.hgi-call-incoming-01::before { - content: "\f1882"; -} - -.hgi-stroke.hgi-call-incoming-02::before { - content: "\f1883"; -} - -.hgi-stroke.hgi-call-incoming-03::before { - content: "\f1884"; -} - -.hgi-stroke.hgi-call-incoming-04::before { - content: "\f1885"; -} - -.hgi-stroke.hgi-call-internal-02::before { - content: "\f1886"; -} - -.hgi-stroke.hgi-call-internal::before { - content: "\f1887"; -} - -.hgi-stroke.hgi-call-locked-02::before { - content: "\f1888"; -} - -.hgi-stroke.hgi-call-locked::before { - content: "\f1889"; -} - -.hgi-stroke.hgi-call-minus-02::before { - content: "\f188a"; -} - -.hgi-stroke.hgi-call-minus::before { - content: "\f188b"; -} - -.hgi-stroke.hgi-call-missed-01::before { - content: "\f188c"; -} - -.hgi-stroke.hgi-call-missed-02::before { - content: "\f188d"; -} - -.hgi-stroke.hgi-call-missed-03::before { - content: "\f188e"; -} - -.hgi-stroke.hgi-call-missed-04::before { - content: "\f188f"; -} - -.hgi-stroke.hgi-call-outgoing-01::before { - content: "\f1890"; -} - -.hgi-stroke.hgi-call-outgoing-02::before { - content: "\f1891"; -} - -.hgi-stroke.hgi-call-outgoing-03::before { - content: "\f1892"; -} - -.hgi-stroke.hgi-call-outgoing-04::before { - content: "\f1893"; -} - -.hgi-stroke.hgi-call-paused-02::before { - content: "\f1894"; -} - -.hgi-stroke.hgi-call-paused::before { - content: "\f1895"; -} - -.hgi-stroke.hgi-call-received-02::before { - content: "\f1896"; -} - -.hgi-stroke.hgi-call-received::before { - content: "\f1897"; -} - -.hgi-stroke.hgi-call-ringing-01::before { - content: "\f1898"; -} - -.hgi-stroke.hgi-call-ringing-02::before { - content: "\f1899"; -} - -.hgi-stroke.hgi-call-ringing-03::before { - content: "\f189a"; -} - -.hgi-stroke.hgi-call-ringing-04::before { - content: "\f189b"; -} - -.hgi-stroke.hgi-call-spark-01::before { - content: "\f189c"; -} - -.hgi-stroke.hgi-call-spark-02::before { - content: "\f189d"; -} - -.hgi-stroke.hgi-call-unlocked-02::before { - content: "\f189e"; -} - -.hgi-stroke.hgi-call-unlocked::before { - content: "\f189f"; -} - -.hgi-stroke.hgi-call::before { - content: "\f18a0"; -} - -.hgi-stroke.hgi-calling-02::before { - content: "\f18a1"; -} - -.hgi-stroke.hgi-calling::before { - content: "\f18a2"; -} - -.hgi-stroke.hgi-camel::before { - content: "\f18a3"; -} - -.hgi-stroke.hgi-camera-01::before { - content: "\f18a4"; -} - -.hgi-stroke.hgi-camera-02::before { - content: "\f18a5"; -} - -.hgi-stroke.hgi-camera-03::before { - content: "\f18a6"; -} - -.hgi-stroke.hgi-camera-add-01::before { - content: "\f18a7"; -} - -.hgi-stroke.hgi-camera-add-02::before { - content: "\f18a8"; -} - -.hgi-stroke.hgi-camera-add-03::before { - content: "\f18a9"; -} - -.hgi-stroke.hgi-camera-ai::before { - content: "\f18aa"; -} - -.hgi-stroke.hgi-camera-automatically-01::before { - content: "\f18ab"; -} - -.hgi-stroke.hgi-camera-automatically-02::before { - content: "\f18ac"; -} - -.hgi-stroke.hgi-camera-lens::before { - content: "\f18ad"; -} - -.hgi-stroke.hgi-camera-microphone-01::before { - content: "\f18ae"; -} - -.hgi-stroke.hgi-camera-microphone-02::before { - content: "\f18af"; -} - -.hgi-stroke.hgi-camera-night-mode-01::before { - content: "\f18b0"; -} - -.hgi-stroke.hgi-camera-night-mode-02::before { - content: "\f18b1"; -} - -.hgi-stroke.hgi-camera-off-01::before { - content: "\f18b2"; -} - -.hgi-stroke.hgi-camera-off-02::before { - content: "\f18b3"; -} - -.hgi-stroke.hgi-camera-rotated-01::before { - content: "\f18b4"; -} - -.hgi-stroke.hgi-camera-rotated-02::before { - content: "\f18b5"; -} - -.hgi-stroke.hgi-camera-smile-01::before { - content: "\f18b6"; -} - -.hgi-stroke.hgi-camera-smile-02::before { - content: "\f18b7"; -} - -.hgi-stroke.hgi-camera-tripod::before { - content: "\f18b8"; -} - -.hgi-stroke.hgi-camera-video::before { - content: "\f18b9"; -} - -.hgi-stroke.hgi-camper::before { - content: "\f18ba"; -} - -.hgi-stroke.hgi-campfire::before { - content: "\f18bb"; -} - -.hgi-stroke.hgi-cancel-01::before { - content: "\f18bc"; -} - -.hgi-stroke.hgi-cancel-02::before { - content: "\f18bd"; -} - -.hgi-stroke.hgi-cancel-circle-half-dot::before { - content: "\f18be"; -} - -.hgi-stroke.hgi-cancel-circle::before { - content: "\f18bf"; -} - -.hgi-stroke.hgi-cancel-square::before { - content: "\f18c0"; -} - -.hgi-stroke.hgi-candelier-01::before { - content: "\f18c1"; -} - -.hgi-stroke.hgi-candelier-02::before { - content: "\f18c2"; -} - -.hgi-stroke.hgi-candy-cane::before { - content: "\f18c3"; -} - -.hgi-stroke.hgi-candy-off::before { - content: "\f18c4"; -} - -.hgi-stroke.hgi-candy::before { - content: "\f18c5"; -} - -.hgi-stroke.hgi-cannabis-off::before { - content: "\f18c6"; -} - -.hgi-stroke.hgi-cannabis::before { - content: "\f18c7"; -} - -.hgi-stroke.hgi-canvas::before { - content: "\f18c8"; -} - -.hgi-stroke.hgi-cap-projecting::before { - content: "\f18c9"; -} - -.hgi-stroke.hgi-cap-round::before { - content: "\f18ca"; -} - -.hgi-stroke.hgi-cap-straight::before { - content: "\f18cb"; -} - -.hgi-stroke.hgi-cap::before { - content: "\f18cc"; -} - -.hgi-stroke.hgi-capcut-rectangle::before { - content: "\f18cd"; -} - -.hgi-stroke.hgi-capcut::before { - content: "\f18ce"; -} - -.hgi-stroke.hgi-captions-off::before { - content: "\f306a"; -} - -.hgi-stroke.hgi-captions::before { - content: "\f3069"; -} - -.hgi-stroke.hgi-car-01::before { - content: "\f18cf"; -} - -.hgi-stroke.hgi-car-02::before { - content: "\f18d0"; -} - -.hgi-stroke.hgi-car-03::before { - content: "\f18d1"; -} - -.hgi-stroke.hgi-car-04::before { - content: "\f18d2"; -} - -.hgi-stroke.hgi-car-05::before { - content: "\f18d3"; -} - -.hgi-stroke.hgi-car-alert::before { - content: "\f18d4"; -} - -.hgi-stroke.hgi-car-front::before { - content: "\f306b"; -} - -.hgi-stroke.hgi-car-parking-01::before { - content: "\f18d5"; -} - -.hgi-stroke.hgi-car-parking-02::before { - content: "\f18d6"; -} - -.hgi-stroke.hgi-car-signal::before { - content: "\f18d7"; -} - -.hgi-stroke.hgi-car-taxi-front::before { - content: "\f306c"; -} - -.hgi-stroke.hgi-car-time::before { - content: "\f18d8"; -} - -.hgi-stroke.hgi-caravan::before { - content: "\f18d9"; -} - -.hgi-stroke.hgi-card-exchange-01::before { - content: "\f18da"; -} - -.hgi-stroke.hgi-card-exchange-02::before { - content: "\f18db"; -} - -.hgi-stroke.hgi-card-sim::before { - content: "\f306d"; -} - -.hgi-stroke.hgi-cardigan::before { - content: "\f18dc"; -} - -.hgi-stroke.hgi-cardiogram-01::before { - content: "\f18dd"; -} - -.hgi-stroke.hgi-cardiogram-02::before { - content: "\f18de"; -} - -.hgi-stroke.hgi-cards-01::before { - content: "\f18df"; -} - -.hgi-stroke.hgi-cards-02::before { - content: "\f18e0"; -} - -.hgi-stroke.hgi-cargo-ship::before { - content: "\f18e1"; -} - -.hgi-stroke.hgi-caring::before { - content: "\f306e"; -} - -.hgi-stroke.hgi-carousel-horizontal-02::before { - content: "\f18e2"; -} - -.hgi-stroke.hgi-carousel-horizontal::before { - content: "\f18e3"; -} - -.hgi-stroke.hgi-carousel-vertical::before { - content: "\f18e4"; -} - -.hgi-stroke.hgi-carrot::before { - content: "\f18e5"; -} - -.hgi-stroke.hgi-case-lower::before { - content: "\f306f"; -} - -.hgi-stroke.hgi-case-sensitive::before { - content: "\f3070"; -} - -.hgi-stroke.hgi-case-upper::before { - content: "\f3071"; -} - -.hgi-stroke.hgi-cash-01::before { - content: "\f18e6"; -} - -.hgi-stroke.hgi-cash-02::before { - content: "\f18e7"; -} - -.hgi-stroke.hgi-cashback-bitcoin::before { - content: "\f18e8"; -} - -.hgi-stroke.hgi-cashback-euro::before { - content: "\f18e9"; -} - -.hgi-stroke.hgi-cashback-pound::before { - content: "\f18ea"; -} - -.hgi-stroke.hgi-cashback-yen::before { - content: "\f18eb"; -} - -.hgi-stroke.hgi-cashback::before { - content: "\f18ec"; -} - -.hgi-stroke.hgi-cashier-02::before { - content: "\f18ed"; -} - -.hgi-stroke.hgi-cashier::before { - content: "\f18ee"; -} - -.hgi-stroke.hgi-cassette-tape::before { - content: "\f3072"; -} - -.hgi-stroke.hgi-cast::before { - content: "\f3073"; -} - -.hgi-stroke.hgi-castbox::before { - content: "\f18ef"; -} - -.hgi-stroke.hgi-castle-01::before { - content: "\f18f0"; -} - -.hgi-stroke.hgi-castle-02::before { - content: "\f18f1"; -} - -.hgi-stroke.hgi-castle::before { - content: "\f18f2"; -} - -.hgi-stroke.hgi-cat::before { - content: "\f3074"; -} - -.hgi-stroke.hgi-catalogue::before { - content: "\f18f3"; -} - -.hgi-stroke.hgi-cayan-tower::before { - content: "\f18f4"; -} - -.hgi-stroke.hgi-cctv-camera::before { - content: "\f18f5"; -} - -.hgi-stroke.hgi-cctv-off::before { - content: "\f3076"; -} - -.hgi-stroke.hgi-cctv::before { - content: "\f3075"; -} - -.hgi-stroke.hgi-cd::before { - content: "\f18f6"; -} - -.hgi-stroke.hgi-cells::before { - content: "\f18f7"; -} - -.hgi-stroke.hgi-cellular-network-offline::before { - content: "\f18f8"; -} - -.hgi-stroke.hgi-cellular-network::before { - content: "\f18f9"; -} - -.hgi-stroke.hgi-celsius::before { - content: "\f18fa"; -} - -.hgi-stroke.hgi-center-focus::before { - content: "\f18fb"; -} - -.hgi-stroke.hgi-central-shaheed-minar::before { - content: "\f18fc"; -} - -.hgi-stroke.hgi-centralized::before { - content: "\f18fd"; -} - -.hgi-stroke.hgi-certificate-01::before { - content: "\f18fe"; -} - -.hgi-stroke.hgi-certificate-02::before { - content: "\f18ff"; -} - -.hgi-stroke.hgi-chair-01::before { - content: "\f1900"; -} - -.hgi-stroke.hgi-chair-02::before { - content: "\f1901"; -} - -.hgi-stroke.hgi-chair-03::before { - content: "\f1902"; -} - -.hgi-stroke.hgi-chair-04::before { - content: "\f1903"; -} - -.hgi-stroke.hgi-chair-05::before { - content: "\f1904"; -} - -.hgi-stroke.hgi-chair-barber::before { - content: "\f1905"; -} - -.hgi-stroke.hgi-champion::before { - content: "\f1906"; -} - -.hgi-stroke.hgi-change-screen-mode::before { - content: "\f1907"; -} - -.hgi-stroke.hgi-character-phonetic::before { - content: "\f1908"; -} - -.hgi-stroke.hgi-charity::before { - content: "\f1909"; -} - -.hgi-stroke.hgi-chart-01-2::before { - content: "\f3077"; -} - -.hgi-stroke.hgi-chart-01::before { - content: "\f190a"; -} - -.hgi-stroke.hgi-chart-02-2::before { - content: "\f3078"; -} - -.hgi-stroke.hgi-chart-02::before { - content: "\f190b"; -} - -.hgi-stroke.hgi-chart-03::before { - content: "\f190c"; -} - -.hgi-stroke.hgi-chart-analysis::before { - content: "\f190d"; -} - -.hgi-stroke.hgi-chart-area::before { - content: "\f190e"; -} - -.hgi-stroke.hgi-chart-average::before { - content: "\f190f"; -} - -.hgi-stroke.hgi-chart-bar-big::before { - content: "\f1910"; -} - -.hgi-stroke.hgi-chart-bar-decreasing::before { - content: "\f1911"; -} - -.hgi-stroke.hgi-chart-bar-increasing::before { - content: "\f1912"; -} - -.hgi-stroke.hgi-chart-bar-line::before { - content: "\f1913"; -} - -.hgi-stroke.hgi-chart-bar-stacked::before { - content: "\f1914"; -} - -.hgi-stroke.hgi-chart-breakout-circle::before { - content: "\f1915"; -} - -.hgi-stroke.hgi-chart-breakout-square::before { - content: "\f1916"; -} - -.hgi-stroke.hgi-chart-bubble-01::before { - content: "\f1917"; -} - -.hgi-stroke.hgi-chart-bubble-02::before { - content: "\f1918"; -} - -.hgi-stroke.hgi-chart-candle::before { - content: "\f1919"; -} - -.hgi-stroke.hgi-chart-candlestick::before { - content: "\f191a"; -} - -.hgi-stroke.hgi-chart-column-2::before { - content: "\f3079"; -} - -.hgi-stroke.hgi-chart-column-big::before { - content: "\f191b"; -} - -.hgi-stroke.hgi-chart-column-decreasing::before { - content: "\f191c"; -} - -.hgi-stroke.hgi-chart-column-increasing::before { - content: "\f307a"; -} - -.hgi-stroke.hgi-chart-column-stacked::before { - content: "\f191d"; -} - -.hgi-stroke.hgi-chart-column::before { - content: "\f191e"; -} - -.hgi-stroke.hgi-chart-decrease::before { - content: "\f191f"; -} - -.hgi-stroke.hgi-chart-down-2::before { - content: "\f307b"; -} - -.hgi-stroke.hgi-chart-down::before { - content: "\f1920"; -} - -.hgi-stroke.hgi-chart-evaluation::before { - content: "\f1921"; -} - -.hgi-stroke.hgi-chart-gantt::before { - content: "\f1922"; -} - -.hgi-stroke.hgi-chart-high-low::before { - content: "\f1923"; -} - -.hgi-stroke.hgi-chart-histogram::before { - content: "\f1924"; -} - -.hgi-stroke.hgi-chart-increase::before { - content: "\f1925"; -} - -.hgi-stroke.hgi-chart-line-data-01::before { - content: "\f1926"; -} - -.hgi-stroke.hgi-chart-line-data-02::before { - content: "\f1927"; -} - -.hgi-stroke.hgi-chart-line-data-03::before { - content: "\f1928"; -} - -.hgi-stroke.hgi-chart-line::before { - content: "\f307c"; -} - -.hgi-stroke.hgi-chart-maximum::before { - content: "\f1929"; -} - -.hgi-stroke.hgi-chart-medium::before { - content: "\f192a"; -} - -.hgi-stroke.hgi-chart-minimum::before { - content: "\f192b"; -} - -.hgi-stroke.hgi-chart-network::before { - content: "\f307d"; -} - -.hgi-stroke.hgi-chart-no-axes-column-decreasing::before { - content: "\f307f"; -} - -.hgi-stroke.hgi-chart-no-axes-column-increasing::before { - content: "\f192c"; -} - -.hgi-stroke.hgi-chart-no-axes-column::before { - content: "\f307e"; -} - -.hgi-stroke.hgi-chart-no-axes-combined::before { - content: "\f192d"; -} - -.hgi-stroke.hgi-chart-no-axes-gantt::before { - content: "\f192e"; -} - -.hgi-stroke.hgi-chart-radar::before { - content: "\f192f"; -} - -.hgi-stroke.hgi-chart-relationship::before { - content: "\f1930"; -} - -.hgi-stroke.hgi-chart-ring::before { - content: "\f1931"; -} - -.hgi-stroke.hgi-chart-rose::before { - content: "\f1932"; -} - -.hgi-stroke.hgi-chart-scatter::before { - content: "\f1933"; -} - -.hgi-stroke.hgi-chart-spline::before { - content: "\f3080"; -} - -.hgi-stroke.hgi-chart-up-2::before { - content: "\f3081"; -} - -.hgi-stroke.hgi-chart-up::before { - content: "\f1934"; -} - -.hgi-stroke.hgi-chart::before { - content: "\f1935"; -} - -.hgi-stroke.hgi-chat-01::before { - content: "\f1936"; -} - -.hgi-stroke.hgi-chat-add-01::before { - content: "\f1937"; -} - -.hgi-stroke.hgi-chat-add::before { - content: "\f1938"; -} - -.hgi-stroke.hgi-chat-blocked-01::before { - content: "\f1939"; -} - -.hgi-stroke.hgi-chat-blocked::before { - content: "\f193a"; -} - -.hgi-stroke.hgi-chat-bot::before { - content: "\f193b"; -} - -.hgi-stroke.hgi-chat-cancel-01::before { - content: "\f193c"; -} - -.hgi-stroke.hgi-chat-cancel::before { - content: "\f193d"; -} - -.hgi-stroke.hgi-chat-delay-01::before { - content: "\f193e"; -} - -.hgi-stroke.hgi-chat-delay::before { - content: "\f193f"; -} - -.hgi-stroke.hgi-chat-done-01::before { - content: "\f1940"; -} - -.hgi-stroke.hgi-chat-done::before { - content: "\f1941"; -} - -.hgi-stroke.hgi-chat-download-01::before { - content: "\f1942"; -} - -.hgi-stroke.hgi-chat-download::before { - content: "\f1943"; -} - -.hgi-stroke.hgi-chat-edit-01::before { - content: "\f1944"; -} - -.hgi-stroke.hgi-chat-edit::before { - content: "\f1945"; -} - -.hgi-stroke.hgi-chat-favourite-01::before { - content: "\f1946"; -} - -.hgi-stroke.hgi-chat-favourite::before { - content: "\f1947"; -} - -.hgi-stroke.hgi-chat-feedback-01::before { - content: "\f1948"; -} - -.hgi-stroke.hgi-chat-feedback::before { - content: "\f1949"; -} - -.hgi-stroke.hgi-chat-gpt::before { - content: "\f194a"; -} - -.hgi-stroke.hgi-chat-income-01::before { - content: "\f194b"; -} - -.hgi-stroke.hgi-chat-income::before { - content: "\f194c"; -} - -.hgi-stroke.hgi-chat-lock-01::before { - content: "\f194d"; -} - -.hgi-stroke.hgi-chat-lock::before { - content: "\f194e"; -} - -.hgi-stroke.hgi-chat-notification-01::before { - content: "\f194f"; -} - -.hgi-stroke.hgi-chat-notification::before { - content: "\f1950"; -} - -.hgi-stroke.hgi-chat-outcome-01::before { - content: "\f1951"; -} - -.hgi-stroke.hgi-chat-outcome::before { - content: "\f1952"; -} - -.hgi-stroke.hgi-chat-preview-01::before { - content: "\f1953"; -} - -.hgi-stroke.hgi-chat-preview::before { - content: "\f1954"; -} - -.hgi-stroke.hgi-chat-question-01::before { - content: "\f1955"; -} - -.hgi-stroke.hgi-chat-question::before { - content: "\f1956"; -} - -.hgi-stroke.hgi-chat-search-01::before { - content: "\f1957"; -} - -.hgi-stroke.hgi-chat-search::before { - content: "\f1958"; -} - -.hgi-stroke.hgi-chat-secure-01::before { - content: "\f1959"; -} - -.hgi-stroke.hgi-chat-secure::before { - content: "\f195a"; -} - -.hgi-stroke.hgi-chat-spark-01::before { - content: "\f195b"; -} - -.hgi-stroke.hgi-chat-spark::before { - content: "\f195c"; -} - -.hgi-stroke.hgi-chat-translate-01::before { - content: "\f195d"; -} - -.hgi-stroke.hgi-chat-translate::before { - content: "\f195e"; -} - -.hgi-stroke.hgi-chat-unlock-01::before { - content: "\f195f"; -} - -.hgi-stroke.hgi-chat-unlock::before { - content: "\f1960"; -} - -.hgi-stroke.hgi-chat-upload-01::before { - content: "\f1961"; -} - -.hgi-stroke.hgi-chat-upload::before { - content: "\f1962"; -} - -.hgi-stroke.hgi-chat-user-01::before { - content: "\f1963"; -} - -.hgi-stroke.hgi-chat-user::before { - content: "\f1964"; -} - -.hgi-stroke.hgi-chat::before { - content: "\f1965"; -} - -.hgi-stroke.hgi-chatting-01::before { - content: "\f1966"; -} - -.hgi-stroke.hgi-check-check::before { - content: "\f3083"; -} - -.hgi-stroke.hgi-check-line::before { - content: "\f3084"; -} - -.hgi-stroke.hgi-check-list::before { - content: "\f1967"; -} - -.hgi-stroke.hgi-check-unread-01::before { - content: "\f1968"; -} - -.hgi-stroke.hgi-check-unread-02::before { - content: "\f1969"; -} - -.hgi-stroke.hgi-check-unread-03::before { - content: "\f196a"; -} - -.hgi-stroke.hgi-check-unread-04::before { - content: "\f196b"; -} - -.hgi-stroke.hgi-check::before { - content: "\f3082"; -} - -.hgi-stroke.hgi-checkmark-badge-01::before { - content: "\f196c"; -} - -.hgi-stroke.hgi-checkmark-badge-02::before { - content: "\f196d"; -} - -.hgi-stroke.hgi-checkmark-badge-03::before { - content: "\f196e"; -} - -.hgi-stroke.hgi-checkmark-badge-04::before { - content: "\f196f"; -} - -.hgi-stroke.hgi-checkmark-circle-01::before { - content: "\f1970"; -} - -.hgi-stroke.hgi-checkmark-circle-02::before { - content: "\f1971"; -} - -.hgi-stroke.hgi-checkmark-circle-03::before { - content: "\f1972"; -} - -.hgi-stroke.hgi-checkmark-circle-04::before { - content: "\f1973"; -} - -.hgi-stroke.hgi-checkmark-square-01::before { - content: "\f1974"; -} - -.hgi-stroke.hgi-checkmark-square-02::before { - content: "\f1975"; -} - -.hgi-stroke.hgi-checkmark-square-03::before { - content: "\f1976"; -} - -.hgi-stroke.hgi-checkmark-square-04::before { - content: "\f1977"; -} - -.hgi-stroke.hgi-cheese-cake-01::before { - content: "\f1978"; -} - -.hgi-stroke.hgi-cheese-cake-02::before { - content: "\f1979"; -} - -.hgi-stroke.hgi-cheese::before { - content: "\f197a"; -} - -.hgi-stroke.hgi-chef-hat::before { - content: "\f197b"; -} - -.hgi-stroke.hgi-chef::before { - content: "\f197c"; -} - -.hgi-stroke.hgi-chemistry-01::before { - content: "\f197d"; -} - -.hgi-stroke.hgi-chemistry-02::before { - content: "\f197e"; -} - -.hgi-stroke.hgi-chemistry-03::before { - content: "\f197f"; -} - -.hgi-stroke.hgi-cherry::before { - content: "\f1980"; -} - -.hgi-stroke.hgi-chess-01::before { - content: "\f1981"; -} - -.hgi-stroke.hgi-chess-02::before { - content: "\f1982"; -} - -.hgi-stroke.hgi-chess-bishop::before { - content: "\f1983"; -} - -.hgi-stroke.hgi-chess-king::before { - content: "\f1984"; -} - -.hgi-stroke.hgi-chess-knight::before { - content: "\f3085"; -} - -.hgi-stroke.hgi-chess-pawn::before { - content: "\f1985"; -} - -.hgi-stroke.hgi-chess-queen::before { - content: "\f3086"; -} - -.hgi-stroke.hgi-chess-rook::before { - content: "\f3087"; -} - -.hgi-stroke.hgi-chevron-double-close::before { - content: "\f1986"; -} - -.hgi-stroke.hgi-chevron-down::before { - content: "\f3088"; -} - -.hgi-stroke.hgi-chevron-first::before { - content: "\f3089"; -} - -.hgi-stroke.hgi-chevron-last::before { - content: "\f308a"; -} - -.hgi-stroke.hgi-chevron-left::before { - content: "\f308b"; -} - -.hgi-stroke.hgi-chevron-right::before { - content: "\f308c"; -} - -.hgi-stroke.hgi-chevron-up::before { - content: "\f308d"; -} - -.hgi-stroke.hgi-chevrons-down-up::before { - content: "\f1987"; -} - -.hgi-stroke.hgi-chevrons-down::before { - content: "\f308e"; -} - -.hgi-stroke.hgi-chevrons-left-right-ellipsis::before { - content: "\f3091"; -} - -.hgi-stroke.hgi-chevrons-left-right::before { - content: "\f3090"; -} - -.hgi-stroke.hgi-chevrons-left::before { - content: "\f308f"; -} - -.hgi-stroke.hgi-chevrons-right-left::before { - content: "\f3093"; -} - -.hgi-stroke.hgi-chevrons-right::before { - content: "\f3092"; -} - -.hgi-stroke.hgi-chevrons-up::before { - content: "\f3094"; -} - -.hgi-stroke.hgi-chicken-thighs::before { - content: "\f1988"; -} - -.hgi-stroke.hgi-child::before { - content: "\f1989"; -} - -.hgi-stroke.hgi-chimney::before { - content: "\f198a"; -} - -.hgi-stroke.hgi-china-temple::before { - content: "\f198b"; -} - -.hgi-stroke.hgi-chip-02::before { - content: "\f198c"; -} - -.hgi-stroke.hgi-chip::before { - content: "\f198d"; -} - -.hgi-stroke.hgi-chocolate::before { - content: "\f198e"; -} - -.hgi-stroke.hgi-chopsticks::before { - content: "\f198f"; -} - -.hgi-stroke.hgi-chrome::before { - content: "\f1990"; -} - -.hgi-stroke.hgi-chrysler::before { - content: "\f1991"; -} - -.hgi-stroke.hgi-church::before { - content: "\f1992"; -} - -.hgi-stroke.hgi-cigarette-off::before { - content: "\f1993"; -} - -.hgi-stroke.hgi-cigarette::before { - content: "\f1994"; -} - -.hgi-stroke.hgi-cinnamon-roll::before { - content: "\f1995"; -} - -.hgi-stroke.hgi-circle-arrow-data-transfer-diagonal::before { - content: "\f1996"; -} - -.hgi-stroke.hgi-circle-arrow-data-transfer-horizontal::before { - content: "\f1997"; -} - -.hgi-stroke.hgi-circle-arrow-data-transfer-vertical::before { - content: "\f1998"; -} - -.hgi-stroke.hgi-circle-arrow-diagonal-01::before { - content: "\f1999"; -} - -.hgi-stroke.hgi-circle-arrow-diagonal-02::before { - content: "\f199a"; -} - -.hgi-stroke.hgi-circle-arrow-down-01::before { - content: "\f199b"; -} - -.hgi-stroke.hgi-circle-arrow-down-02::before { - content: "\f199c"; -} - -.hgi-stroke.hgi-circle-arrow-down-03::before { - content: "\f199d"; -} - -.hgi-stroke.hgi-circle-arrow-down-double::before { - content: "\f199e"; -} - -.hgi-stroke.hgi-circle-arrow-down-left::before { - content: "\f199f"; -} - -.hgi-stroke.hgi-circle-arrow-down-right::before { - content: "\f19a0"; -} - -.hgi-stroke.hgi-circle-arrow-expand-01::before { - content: "\f19a1"; -} - -.hgi-stroke.hgi-circle-arrow-expand-02::before { - content: "\f19a2"; -} - -.hgi-stroke.hgi-circle-arrow-horizontal::before { - content: "\f19a3"; -} - -.hgi-stroke.hgi-circle-arrow-left-01::before { - content: "\f19a4"; -} - -.hgi-stroke.hgi-circle-arrow-left-02::before { - content: "\f19a5"; -} - -.hgi-stroke.hgi-circle-arrow-left-03::before { - content: "\f19a6"; -} - -.hgi-stroke.hgi-circle-arrow-left-double::before { - content: "\f19a7"; -} - -.hgi-stroke.hgi-circle-arrow-left-right::before { - content: "\f19a8"; -} - -.hgi-stroke.hgi-circle-arrow-move-down-left::before { - content: "\f19a9"; -} - -.hgi-stroke.hgi-circle-arrow-move-down-right::before { - content: "\f19aa"; -} - -.hgi-stroke.hgi-circle-arrow-move-left-down::before { - content: "\f19ab"; -} - -.hgi-stroke.hgi-circle-arrow-move-right-down::before { - content: "\f19ac"; -} - -.hgi-stroke.hgi-circle-arrow-move-up-left::before { - content: "\f19ad"; -} - -.hgi-stroke.hgi-circle-arrow-move-up-right::before { - content: "\f19ae"; -} - -.hgi-stroke.hgi-circle-arrow-out-down-left::before { - content: "\f3096"; -} - -.hgi-stroke.hgi-circle-arrow-out-down-right::before { - content: "\f3097"; -} - -.hgi-stroke.hgi-circle-arrow-out-up-left::before { - content: "\f3098"; -} - -.hgi-stroke.hgi-circle-arrow-out-up-right::before { - content: "\f19af"; -} - -.hgi-stroke.hgi-circle-arrow-reload-01::before { - content: "\f19b0"; -} - -.hgi-stroke.hgi-circle-arrow-reload-02::before { - content: "\f19b1"; -} - -.hgi-stroke.hgi-circle-arrow-right-01::before { - content: "\f19b2"; -} - -.hgi-stroke.hgi-circle-arrow-right-02::before { - content: "\f19b3"; -} - -.hgi-stroke.hgi-circle-arrow-right-03::before { - content: "\f19b4"; -} - -.hgi-stroke.hgi-circle-arrow-right-double::before { - content: "\f19b5"; -} - -.hgi-stroke.hgi-circle-arrow-shrink-01::before { - content: "\f19b6"; -} - -.hgi-stroke.hgi-circle-arrow-shrink-02::before { - content: "\f19b7"; -} - -.hgi-stroke.hgi-circle-arrow-up-01::before { - content: "\f19b8"; -} - -.hgi-stroke.hgi-circle-arrow-up-02::before { - content: "\f19b9"; -} - -.hgi-stroke.hgi-circle-arrow-up-03::before { - content: "\f19ba"; -} - -.hgi-stroke.hgi-circle-arrow-up-double::before { - content: "\f19bb"; -} - -.hgi-stroke.hgi-circle-arrow-up-down::before { - content: "\f19bc"; -} - -.hgi-stroke.hgi-circle-arrow-up-left::before { - content: "\f19bd"; -} - -.hgi-stroke.hgi-circle-arrow-up-right-02::before { - content: "\f3099"; -} - -.hgi-stroke.hgi-circle-arrow-up-right::before { - content: "\f19be"; -} - -.hgi-stroke.hgi-circle-arrow-vertical::before { - content: "\f19bf"; -} - -.hgi-stroke.hgi-circle-check-big::before { - content: "\f309b"; -} - -.hgi-stroke.hgi-circle-check::before { - content: "\f309a"; -} - -.hgi-stroke.hgi-circle-chevron-down::before { - content: "\f309c"; -} - -.hgi-stroke.hgi-circle-chevron-left::before { - content: "\f309d"; -} - -.hgi-stroke.hgi-circle-chevron-right::before { - content: "\f309e"; -} - -.hgi-stroke.hgi-circle-chevron-up::before { - content: "\f309f"; -} - -.hgi-stroke.hgi-circle-dashed::before { - content: "\f30a0"; -} - -.hgi-stroke.hgi-circle-divide::before { - content: "\f30a1"; -} - -.hgi-stroke.hgi-circle-dollar-sign-2::before { - content: "\f30a3"; -} - -.hgi-stroke.hgi-circle-dollar-sign::before { - content: "\f30a2"; -} - -.hgi-stroke.hgi-circle-dot-dashed::before { - content: "\f30a5"; -} - -.hgi-stroke.hgi-circle-dot::before { - content: "\f30a4"; -} - -.hgi-stroke.hgi-circle-ellipsis::before { - content: "\f30a6"; -} - -.hgi-stroke.hgi-circle-equal::before { - content: "\f30a7"; -} - -.hgi-stroke.hgi-circle-fading-arrow-up::before { - content: "\f30a8"; -} - -.hgi-stroke.hgi-circle-fading-plus::before { - content: "\f30a9"; -} - -.hgi-stroke.hgi-circle-gauge::before { - content: "\f30aa"; -} - -.hgi-stroke.hgi-circle-lock-01::before { - content: "\f19c0"; -} - -.hgi-stroke.hgi-circle-lock-02::before { - content: "\f19c1"; -} - -.hgi-stroke.hgi-circle-lock-add-01::before { - content: "\f19c2"; -} - -.hgi-stroke.hgi-circle-lock-add-02::before { - content: "\f19c3"; -} - -.hgi-stroke.hgi-circle-lock-check-01::before { - content: "\f19c4"; -} - -.hgi-stroke.hgi-circle-lock-check-02::before { - content: "\f19c5"; -} - -.hgi-stroke.hgi-circle-lock-minus-01::before { - content: "\f19c6"; -} - -.hgi-stroke.hgi-circle-lock-minus-02::before { - content: "\f19c7"; -} - -.hgi-stroke.hgi-circle-lock-remove-01::before { - content: "\f19c8"; -} - -.hgi-stroke.hgi-circle-lock-remove-02::before { - content: "\f19c9"; -} - -.hgi-stroke.hgi-circle-minus::before { - content: "\f30ab"; -} - -.hgi-stroke.hgi-circle-off::before { - content: "\f30ac"; -} - -.hgi-stroke.hgi-circle-parking-off::before { - content: "\f19ca"; -} - -.hgi-stroke.hgi-circle-parking::before { - content: "\f30ad"; -} - -.hgi-stroke.hgi-circle-password::before { - content: "\f19cb"; -} - -.hgi-stroke.hgi-circle-pile::before { - content: "\f19cc"; -} - -.hgi-stroke.hgi-circle-plus::before { - content: "\f30ae"; -} - -.hgi-stroke.hgi-circle-pound-sterling::before { - content: "\f30af"; -} - -.hgi-stroke.hgi-circle-power::before { - content: "\f30b0"; -} - -.hgi-stroke.hgi-circle-question-mark::before { - content: "\f30b1"; -} - -.hgi-stroke.hgi-circle-slash-2::before { - content: "\f19cd"; -} - -.hgi-stroke.hgi-circle-slash::before { - content: "\f30b2"; -} - -.hgi-stroke.hgi-circle-slash2::before { - content: "\f30b3"; -} - -.hgi-stroke.hgi-circle-small::before { - content: "\f19ce"; -} - -.hgi-stroke.hgi-circle-unlock-01::before { - content: "\f19cf"; -} - -.hgi-stroke.hgi-circle-unlock-02::before { - content: "\f19d0"; -} - -.hgi-stroke.hgi-circle-user-round::before { - content: "\f30b4"; -} - -.hgi-stroke.hgi-circle-x::before { - content: "\f30b5"; -} - -.hgi-stroke.hgi-circle::before { - content: "\f19d1"; -} - -.hgi-stroke.hgi-circuit-board::before { - content: "\f30b6"; -} - -.hgi-stroke.hgi-citrus::before { - content: "\f19d2"; -} - -.hgi-stroke.hgi-city-01::before { - content: "\f19d3"; -} - -.hgi-stroke.hgi-city-02::before { - content: "\f19d4"; -} - -.hgi-stroke.hgi-city-03::before { - content: "\f19d5"; -} - -.hgi-stroke.hgi-clapperboard::before { - content: "\f30b7"; -} - -.hgi-stroke.hgi-clapping-01::before { - content: "\f19d6"; -} - -.hgi-stroke.hgi-clapping-02::before { - content: "\f19d7"; -} - -.hgi-stroke.hgi-claude::before { - content: "\f19d8"; -} - -.hgi-stroke.hgi-clean::before { - content: "\f19d9"; -} - -.hgi-stroke.hgi-cleaning-bucket::before { - content: "\f19da"; -} - -.hgi-stroke.hgi-clinic::before { - content: "\f19db"; -} - -.hgi-stroke.hgi-clip::before { - content: "\f19dc"; -} - -.hgi-stroke.hgi-cliparts::before { - content: "\f19dd"; -} - -.hgi-stroke.hgi-clipboard-check::before { - content: "\f30b8"; -} - -.hgi-stroke.hgi-clipboard-clock::before { - content: "\f19de"; -} - -.hgi-stroke.hgi-clipboard-copy::before { - content: "\f19df"; -} - -.hgi-stroke.hgi-clipboard-list::before { - content: "\f30b9"; -} - -.hgi-stroke.hgi-clipboard-minus::before { - content: "\f19e0"; -} - -.hgi-stroke.hgi-clipboard-paste::before { - content: "\f19e1"; -} - -.hgi-stroke.hgi-clipboard-pen-line::before { - content: "\f30bb"; -} - -.hgi-stroke.hgi-clipboard-pen::before { - content: "\f30ba"; -} - -.hgi-stroke.hgi-clipboard-plus::before { - content: "\f30bc"; -} - -.hgi-stroke.hgi-clipboard-type::before { - content: "\f30bd"; -} - -.hgi-stroke.hgi-clipboard-x::before { - content: "\f30be"; -} - -.hgi-stroke.hgi-clipboard::before { - content: "\f19e2"; -} - -.hgi-stroke.hgi-clock-01::before { - content: "\f19e3"; -} - -.hgi-stroke.hgi-clock-02::before { - content: "\f19e4"; -} - -.hgi-stroke.hgi-clock-03::before { - content: "\f19e5"; -} - -.hgi-stroke.hgi-clock-04::before { - content: "\f19e6"; -} - -.hgi-stroke.hgi-clock-05::before { - content: "\f19e7"; -} - -.hgi-stroke.hgi-clock-add::before { - content: "\f19e8"; -} - -.hgi-stroke.hgi-clock-alert::before { - content: "\f19e9"; -} - -.hgi-stroke.hgi-clock-arrow-down::before { - content: "\f19ea"; -} - -.hgi-stroke.hgi-clock-arrow-up::before { - content: "\f19eb"; -} - -.hgi-stroke.hgi-clock-check::before { - content: "\f19ec"; -} - -.hgi-stroke.hgi-clock-fading::before { - content: "\f30bf"; -} - -.hgi-stroke.hgi-clock-hour-1::before { - content: "\f30c0"; -} - -.hgi-stroke.hgi-clock-hour-10::before { - content: "\f30c1"; -} - -.hgi-stroke.hgi-clock-hour-11::before { - content: "\f30c2"; -} - -.hgi-stroke.hgi-clock-hour-12::before { - content: "\f30c3"; -} - -.hgi-stroke.hgi-clock-hour-2::before { - content: "\f30c4"; -} - -.hgi-stroke.hgi-clock-hour-3::before { - content: "\f30c5"; -} - -.hgi-stroke.hgi-clock-hour-4::before { - content: "\f30c6"; -} - -.hgi-stroke.hgi-clock-hour-5::before { - content: "\f30c7"; -} - -.hgi-stroke.hgi-clock-hour-6::before { - content: "\f30c8"; -} - -.hgi-stroke.hgi-clock-hour-7::before { - content: "\f30c9"; -} - -.hgi-stroke.hgi-clock-hour-8::before { - content: "\f30ca"; -} - -.hgi-stroke.hgi-clock-hour-9::before { - content: "\f30cb"; -} - -.hgi-stroke.hgi-clock-plus::before { - content: "\f19ed"; -} - -.hgi-stroke.hgi-clock1::before { - content: "\f30cc"; -} - -.hgi-stroke.hgi-clock10::before { - content: "\f30cd"; -} - -.hgi-stroke.hgi-clock11::before { - content: "\f30ce"; -} - -.hgi-stroke.hgi-clock12::before { - content: "\f30cf"; -} - -.hgi-stroke.hgi-clock2::before { - content: "\f30d0"; -} - -.hgi-stroke.hgi-clock3::before { - content: "\f30d1"; -} - -.hgi-stroke.hgi-clock4::before { - content: "\f30d2"; -} - -.hgi-stroke.hgi-clock5::before { - content: "\f30d3"; -} - -.hgi-stroke.hgi-clock6::before { - content: "\f30d4"; -} - -.hgi-stroke.hgi-clock7::before { - content: "\f30d5"; -} - -.hgi-stroke.hgi-clock8::before { - content: "\f30d6"; -} - -.hgi-stroke.hgi-clock9::before { - content: "\f30d7"; -} - -.hgi-stroke.hgi-closed-caption-alt::before { - content: "\f19ee"; -} - -.hgi-stroke.hgi-closed-caption::before { - content: "\f19ef"; -} - -.hgi-stroke.hgi-clothes::before { - content: "\f19f0"; -} - -.hgi-stroke.hgi-cloud-alert::before { - content: "\f19f1"; -} - -.hgi-stroke.hgi-cloud-angled-rain-zap::before { - content: "\f19f2"; -} - -.hgi-stroke.hgi-cloud-angled-rain::before { - content: "\f19f3"; -} - -.hgi-stroke.hgi-cloud-angled-zap::before { - content: "\f19f4"; -} - -.hgi-stroke.hgi-cloud-backup::before { - content: "\f30d8"; -} - -.hgi-stroke.hgi-cloud-big-rain::before { - content: "\f19f5"; -} - -.hgi-stroke.hgi-cloud-check::before { - content: "\f30d9"; -} - -.hgi-stroke.hgi-cloud-cog::before { - content: "\f30da"; -} - -.hgi-stroke.hgi-cloud-download::before { - content: "\f19f6"; -} - -.hgi-stroke.hgi-cloud-drizzle::before { - content: "\f30db"; -} - -.hgi-stroke.hgi-cloud-fast-wind::before { - content: "\f19f7"; -} - -.hgi-stroke.hgi-cloud-fog::before { - content: "\f30dc"; -} - -.hgi-stroke.hgi-cloud-hail::before { - content: "\f30dd"; -} - -.hgi-stroke.hgi-cloud-hailstone::before { - content: "\f19f8"; -} - -.hgi-stroke.hgi-cloud-lightning::before { - content: "\f30de"; -} - -.hgi-stroke.hgi-cloud-little-rain::before { - content: "\f19f9"; -} - -.hgi-stroke.hgi-cloud-little-snow::before { - content: "\f19fa"; -} - -.hgi-stroke.hgi-cloud-loading::before { - content: "\f19fb"; -} - -.hgi-stroke.hgi-cloud-mid-rain::before { - content: "\f19fc"; -} - -.hgi-stroke.hgi-cloud-mid-snow::before { - content: "\f19fd"; -} - -.hgi-stroke.hgi-cloud-moon-rain::before { - content: "\f30df"; -} - -.hgi-stroke.hgi-cloud-off::before { - content: "\f19fe"; -} - -.hgi-stroke.hgi-cloud-rain-wind::before { - content: "\f30e1"; -} - -.hgi-stroke.hgi-cloud-rain::before { - content: "\f30e0"; -} - -.hgi-stroke.hgi-cloud-saving-done-01::before { - content: "\f19ff"; -} - -.hgi-stroke.hgi-cloud-saving-done-02::before { - content: "\f1a00"; -} - -.hgi-stroke.hgi-cloud-server::before { - content: "\f1a01"; -} - -.hgi-stroke.hgi-cloud-slow-wind::before { - content: "\f1a02"; -} - -.hgi-stroke.hgi-cloud-snow::before { - content: "\f1a03"; -} - -.hgi-stroke.hgi-cloud-sun-rain::before { - content: "\f30e2"; -} - -.hgi-stroke.hgi-cloud-sync-2::before { - content: "\f30e3"; -} - -.hgi-stroke.hgi-cloud-sync::before { - content: "\f1a04"; -} - -.hgi-stroke.hgi-cloud-upload::before { - content: "\f1a05"; -} - -.hgi-stroke.hgi-cloud::before { - content: "\f1a06"; -} - -.hgi-stroke.hgi-cloudy::before { - content: "\f30e4"; -} - -.hgi-stroke.hgi-clover::before { - content: "\f30e5"; -} - -.hgi-stroke.hgi-club::before { - content: "\f30e6"; -} - -.hgi-stroke.hgi-clubs-01::before { - content: "\f1a07"; -} - -.hgi-stroke.hgi-clubs-02::before { - content: "\f1a08"; -} - -.hgi-stroke.hgi-co-present::before { - content: "\f1a09"; -} - -.hgi-stroke.hgi-code-circle::before { - content: "\f1a0a"; -} - -.hgi-stroke.hgi-code-folder::before { - content: "\f1a0b"; -} - -.hgi-stroke.hgi-code-simple::before { - content: "\f1a0c"; -} - -.hgi-stroke.hgi-code-square::before { - content: "\f1a0d"; -} - -.hgi-stroke.hgi-code-xml::before { - content: "\f30e7"; -} - -.hgi-stroke.hgi-code::before { - content: "\f1a0e"; -} - -.hgi-stroke.hgi-codepen::before { - content: "\f1a0f"; -} - -.hgi-stroke.hgi-codesandbox::before { - content: "\f1a10"; -} - -.hgi-stroke.hgi-coffee-01::before { - content: "\f1a11"; -} - -.hgi-stroke.hgi-coffee-02::before { - content: "\f1a12"; -} - -.hgi-stroke.hgi-coffee-03::before { - content: "\f1a13"; -} - -.hgi-stroke.hgi-coffee-04::before { - content: "\f1a14"; -} - -.hgi-stroke.hgi-coffee-beans::before { - content: "\f1a15"; -} - -.hgi-stroke.hgi-cog::before { - content: "\f1a16"; -} - -.hgi-stroke.hgi-coinbase::before { - content: "\f1a17"; -} - -.hgi-stroke.hgi-coins-01::before { - content: "\f1a18"; -} - -.hgi-stroke.hgi-coins-02::before { - content: "\f1a19"; -} - -.hgi-stroke.hgi-coins-bitcoin::before { - content: "\f1a1a"; -} - -.hgi-stroke.hgi-coins-dollar::before { - content: "\f1a1b"; -} - -.hgi-stroke.hgi-coins-euro::before { - content: "\f1a1c"; -} - -.hgi-stroke.hgi-coins-pound::before { - content: "\f1a1d"; -} - -.hgi-stroke.hgi-coins-swap::before { - content: "\f1a1e"; -} - -.hgi-stroke.hgi-coins-yen::before { - content: "\f1a1f"; -} - -.hgi-stroke.hgi-collapse::before { - content: "\f1a20"; -} - -.hgi-stroke.hgi-collections-bookmark::before { - content: "\f1a21"; -} - -.hgi-stroke.hgi-color-picker::before { - content: "\f1a22"; -} - -.hgi-stroke.hgi-colors::before { - content: "\f1a23"; -} - -.hgi-stroke.hgi-colosseum::before { - content: "\f1a24"; -} - -.hgi-stroke.hgi-column-delete::before { - content: "\f1a25"; -} - -.hgi-stroke.hgi-column-insert::before { - content: "\f1a26"; -} - -.hgi-stroke.hgi-columns-three-cog::before { - content: "\f1a27"; -} - -.hgi-stroke.hgi-combine::before { - content: "\f1a28"; -} - -.hgi-stroke.hgi-comet-01-2::before { - content: "\f30e8"; -} - -.hgi-stroke.hgi-comet-01::before { - content: "\f1a29"; -} - -.hgi-stroke.hgi-comet-02-2::before { - content: "\f30ea"; -} - -.hgi-stroke.hgi-comet-02::before { - content: "\f1a2a"; -} - -.hgi-stroke.hgi-coming-soon-01::before { - content: "\f1a2b"; -} - -.hgi-stroke.hgi-coming-soon-02::before { - content: "\f1a2c"; -} - -.hgi-stroke.hgi-command-line::before { - content: "\f1a2d"; -} - -.hgi-stroke.hgi-command::before { - content: "\f1a2e"; -} - -.hgi-stroke.hgi-comment-01::before { - content: "\f1a2f"; -} - -.hgi-stroke.hgi-comment-02::before { - content: "\f1a30"; -} - -.hgi-stroke.hgi-comment-03::before { - content: "\f1a31"; -} - -.hgi-stroke.hgi-comment-add-01::before { - content: "\f1a32"; -} - -.hgi-stroke.hgi-comment-add-02::before { - content: "\f1a33"; -} - -.hgi-stroke.hgi-comment-add-03::before { - content: "\f1a34"; -} - -.hgi-stroke.hgi-comment-block-01::before { - content: "\f1a35"; -} - -.hgi-stroke.hgi-comment-block-02::before { - content: "\f1a36"; -} - -.hgi-stroke.hgi-comment-block-03::before { - content: "\f1a37"; -} - -.hgi-stroke.hgi-comment-remove-01::before { - content: "\f1a38"; -} - -.hgi-stroke.hgi-comment-remove-02::before { - content: "\f1a39"; -} - -.hgi-stroke.hgi-comment-remove-03::before { - content: "\f1a3a"; -} - -.hgi-stroke.hgi-compass-01::before { - content: "\f1a3b"; -} - -.hgi-stroke.hgi-compass::before { - content: "\f1a3c"; -} - -.hgi-stroke.hgi-complaint::before { - content: "\f1a3d"; -} - -.hgi-stroke.hgi-component::before { - content: "\f1a3f"; -} - -.hgi-stroke.hgi-computer-activity::before { - content: "\f1a40"; -} - -.hgi-stroke.hgi-computer-add::before { - content: "\f1a41"; -} - -.hgi-stroke.hgi-computer-arrow-down::before { - content: "\f1a42"; -} - -.hgi-stroke.hgi-computer-arrow-up::before { - content: "\f1a43"; -} - -.hgi-stroke.hgi-computer-chart-down::before { - content: "\f1a44"; -} - -.hgi-stroke.hgi-computer-chart-up::before { - content: "\f1a45"; -} - -.hgi-stroke.hgi-computer-check::before { - content: "\f1a46"; -} - -.hgi-stroke.hgi-computer-cloud::before { - content: "\f1a47"; -} - -.hgi-stroke.hgi-computer-desk-01::before { - content: "\f1a48"; -} - -.hgi-stroke.hgi-computer-desk-02::before { - content: "\f1a49"; -} - -.hgi-stroke.hgi-computer-desk-03::before { - content: "\f1a4a"; -} - -.hgi-stroke.hgi-computer-dollar::before { - content: "\f1a4b"; -} - -.hgi-stroke.hgi-computer-ethernet::before { - content: "\f1a4c"; -} - -.hgi-stroke.hgi-computer-phone-sync::before { - content: "\f1a4d"; -} - -.hgi-stroke.hgi-computer-programming-01::before { - content: "\f1a4e"; -} - -.hgi-stroke.hgi-computer-programming-02::before { - content: "\f1a4f"; -} - -.hgi-stroke.hgi-computer-protection::before { - content: "\f1a50"; -} - -.hgi-stroke.hgi-computer-remove::before { - content: "\f1a51"; -} - -.hgi-stroke.hgi-computer-screen-share::before { - content: "\f1a52"; -} - -.hgi-stroke.hgi-computer-settings::before { - content: "\f1a53"; -} - -.hgi-stroke.hgi-computer-terminal-01::before { - content: "\f1a54"; -} - -.hgi-stroke.hgi-computer-terminal-02::before { - content: "\f1a55"; -} - -.hgi-stroke.hgi-computer-user::before { - content: "\f1a56"; -} - -.hgi-stroke.hgi-computer-video-call::before { - content: "\f1a57"; -} - -.hgi-stroke.hgi-computer-video::before { - content: "\f1a58"; -} - -.hgi-stroke.hgi-computer::before { - content: "\f1a59"; -} - -.hgi-stroke.hgi-concierge-bell::before { - content: "\f30eb"; -} - -.hgi-stroke.hgi-cone-01::before { - content: "\f1a5a"; -} - -.hgi-stroke.hgi-cone-02::before { - content: "\f1a5b"; -} - -.hgi-stroke.hgi-conference::before { - content: "\f1a5c"; -} - -.hgi-stroke.hgi-configuration-01::before { - content: "\f1a5d"; -} - -.hgi-stroke.hgi-configuration-02::before { - content: "\f1a5e"; -} - -.hgi-stroke.hgi-confused::before { - content: "\f1a5f"; -} - -.hgi-stroke.hgi-congruent-to-circle::before { - content: "\f1a60"; -} - -.hgi-stroke.hgi-congruent-to-square::before { - content: "\f1a61"; -} - -.hgi-stroke.hgi-congruent-to::before { - content: "\f1a62"; -} - -.hgi-stroke.hgi-connect::before { - content: "\f1a63"; -} - -.hgi-stroke.hgi-console::before { - content: "\f1a64"; -} - -.hgi-stroke.hgi-constellation-2::before { - content: "\f30ee"; -} - -.hgi-stroke.hgi-constellation::before { - content: "\f1a65"; -} - -.hgi-stroke.hgi-construction::before { - content: "\f30ef"; -} - -.hgi-stroke.hgi-contact-01::before { - content: "\f1a66"; -} - -.hgi-stroke.hgi-contact-02::before { - content: "\f1a67"; -} - -.hgi-stroke.hgi-contact-book::before { - content: "\f1a68"; -} - -.hgi-stroke.hgi-contact-round::before { - content: "\f30f0"; -} - -.hgi-stroke.hgi-contact::before { - content: "\f1a69"; -} - -.hgi-stroke.hgi-container-truck-01::before { - content: "\f1a6a"; -} - -.hgi-stroke.hgi-container-truck-02::before { - content: "\f1a6b"; -} - -.hgi-stroke.hgi-container-truck::before { - content: "\f1a6c"; -} - -.hgi-stroke.hgi-container::before { - content: "\f30f1"; -} - -.hgi-stroke.hgi-content-writing::before { - content: "\f1a6d"; -} - -.hgi-stroke.hgi-contracts::before { - content: "\f1a6e"; -} - -.hgi-stroke.hgi-contrast::before { - content: "\f30f2"; -} - -.hgi-stroke.hgi-conversation::before { - content: "\f1a6f"; -} - -.hgi-stroke.hgi-cook-book::before { - content: "\f1a70"; -} - -.hgi-stroke.hgi-cookie::before { - content: "\f1a71"; -} - -.hgi-stroke.hgi-cooking-pot::before { - content: "\f30f3"; -} - -.hgi-stroke.hgi-coordinate-01::before { - content: "\f1a72"; -} - -.hgi-stroke.hgi-coordinate-02::before { - content: "\f1a73"; -} - -.hgi-stroke.hgi-copilot::before { - content: "\f1a74"; -} - -.hgi-stroke.hgi-copy-01::before { - content: "\f1a75"; -} - -.hgi-stroke.hgi-copy-02::before { - content: "\f1a76"; -} - -.hgi-stroke.hgi-copy-check::before { - content: "\f1a77"; -} - -.hgi-stroke.hgi-copy-link::before { - content: "\f1a78"; -} - -.hgi-stroke.hgi-copy-minus::before { - content: "\f1a79"; -} - -.hgi-stroke.hgi-copy-plus::before { - content: "\f1a7a"; -} - -.hgi-stroke.hgi-copy-slash-2::before { - content: "\f30f4"; -} - -.hgi-stroke.hgi-copy-slash::before { - content: "\f1a7b"; -} - -.hgi-stroke.hgi-copy-x::before { - content: "\f30f5"; -} - -.hgi-stroke.hgi-copy::before { - content: "\f1a7c"; -} - -.hgi-stroke.hgi-copyleft::before { - content: "\f1a7d"; -} - -.hgi-stroke.hgi-copyright::before { - content: "\f1a7e"; -} - -.hgi-stroke.hgi-corn::before { - content: "\f1a7f"; -} - -.hgi-stroke.hgi-corner-down-left::before { - content: "\f30f6"; -} - -.hgi-stroke.hgi-corner-down-right::before { - content: "\f30f7"; -} - -.hgi-stroke.hgi-corner-left-down::before { - content: "\f30f8"; -} - -.hgi-stroke.hgi-corner-left-up::before { - content: "\f30f9"; -} - -.hgi-stroke.hgi-corner-right-down::before { - content: "\f30fa"; -} - -.hgi-stroke.hgi-corner-right-up::before { - content: "\f30fb"; -} - -.hgi-stroke.hgi-corner-up-left::before { - content: "\f30fc"; -} - -.hgi-stroke.hgi-corner-up-right::before { - content: "\f30fd"; -} - -.hgi-stroke.hgi-corporate::before { - content: "\f1a80"; -} - -.hgi-stroke.hgi-cos::before { - content: "\f1a81"; -} - -.hgi-stroke.hgi-cosine-01::before { - content: "\f1a82"; -} - -.hgi-stroke.hgi-cosine-02::before { - content: "\f1a83"; -} - -.hgi-stroke.hgi-cottage::before { - content: "\f1a84"; -} - -.hgi-stroke.hgi-cotton-candy::before { - content: "\f1a85"; -} - -.hgi-stroke.hgi-coupon-01::before { - content: "\f1a86"; -} - -.hgi-stroke.hgi-coupon-02::before { - content: "\f1a87"; -} - -.hgi-stroke.hgi-coupon-03::before { - content: "\f1a88"; -} - -.hgi-stroke.hgi-coupon-percent::before { - content: "\f1a89"; -} - -.hgi-stroke.hgi-course::before { - content: "\f1a8a"; -} - -.hgi-stroke.hgi-court-house::before { - content: "\f1a8b"; -} - -.hgi-stroke.hgi-court-law::before { - content: "\f1a8c"; -} - -.hgi-stroke.hgi-covariate::before { - content: "\f1a8d"; -} - -.hgi-stroke.hgi-covid-info::before { - content: "\f1a8e"; -} - -.hgi-stroke.hgi-cowboy-hat::before { - content: "\f1a8f"; -} - -.hgi-stroke.hgi-cpp::before { - content: "\f1a90"; -} - -.hgi-stroke.hgi-cpu-charge::before { - content: "\f1a91"; -} - -.hgi-stroke.hgi-cpu-settings::before { - content: "\f1a92"; -} - -.hgi-stroke.hgi-cpu::before { - content: "\f1a93"; -} - -.hgi-stroke.hgi-crab::before { - content: "\f1a94"; -} - -.hgi-stroke.hgi-crane-01::before { - content: "\f30fe"; -} - -.hgi-stroke.hgi-crane-tower::before { - content: "\f30ff"; -} - -.hgi-stroke.hgi-crane::before { - content: "\f1a95"; -} - -.hgi-stroke.hgi-crazy::before { - content: "\f1a96"; -} - -.hgi-stroke.hgi-creative-commons::before { - content: "\f1a97"; -} - -.hgi-stroke.hgi-creative-market::before { - content: "\f1a98"; -} - -.hgi-stroke.hgi-credit-card-accept::before { - content: "\f1a99"; -} - -.hgi-stroke.hgi-credit-card-add::before { - content: "\f1a9a"; -} - -.hgi-stroke.hgi-credit-card-change::before { - content: "\f1a9b"; -} - -.hgi-stroke.hgi-credit-card-defrost::before { - content: "\f1a9c"; -} - -.hgi-stroke.hgi-credit-card-freeze::before { - content: "\f1a9d"; -} - -.hgi-stroke.hgi-credit-card-not-accept::before { - content: "\f1a9e"; -} - -.hgi-stroke.hgi-credit-card-not-found::before { - content: "\f1a9f"; -} - -.hgi-stroke.hgi-credit-card-pos::before { - content: "\f1aa0"; -} - -.hgi-stroke.hgi-credit-card-validation::before { - content: "\f1aa1"; -} - -.hgi-stroke.hgi-credit-card::before { - content: "\f1aa2"; -} - -.hgi-stroke.hgi-cricket-bat::before { - content: "\f1aa3"; -} - -.hgi-stroke.hgi-cricket-helmet::before { - content: "\f1aa4"; -} - -.hgi-stroke.hgi-croissant::before { - content: "\f1aa5"; -} - -.hgi-stroke.hgi-crop::before { - content: "\f1aa6"; -} - -.hgi-stroke.hgi-cross::before { - content: "\f3100"; -} - -.hgi-stroke.hgi-crosshair::before { - content: "\f3101"; -} - -.hgi-stroke.hgi-crowdfunding::before { - content: "\f1aa7"; -} - -.hgi-stroke.hgi-crown-02::before { - content: "\f1aa8"; -} - -.hgi-stroke.hgi-crown-03::before { - content: "\f1aa9"; -} - -.hgi-stroke.hgi-crown-minus::before { - content: "\f1aaa"; -} - -.hgi-stroke.hgi-crown-plus::before { - content: "\f1aab"; -} - -.hgi-stroke.hgi-crown::before { - content: "\f1aac"; -} - -.hgi-stroke.hgi-crying::before { - content: "\f1aad"; -} - -.hgi-stroke.hgi-css-3::before { - content: "\f1aae"; -} - -.hgi-stroke.hgi-css-file-01::before { - content: "\f1aaf"; -} - -.hgi-stroke.hgi-css-file-02::before { - content: "\f1ab0"; -} - -.hgi-stroke.hgi-csv-01::before { - content: "\f1ab1"; -} - -.hgi-stroke.hgi-csv-02::before { - content: "\f1ab2"; -} - -.hgi-stroke.hgi-cube::before { - content: "\f1ab3"; -} - -.hgi-stroke.hgi-cuboid::before { - content: "\f3102"; -} - -.hgi-stroke.hgi-cup-soda::before { - content: "\f3103"; -} - -.hgi-stroke.hgi-cupcake-01::before { - content: "\f1ab4"; -} - -.hgi-stroke.hgi-cupcake-02::before { - content: "\f1ab5"; -} - -.hgi-stroke.hgi-cupcake-03::before { - content: "\f1ab6"; -} - -.hgi-stroke.hgi-curling::before { - content: "\f1ab7"; -} - -.hgi-stroke.hgi-currency::before { - content: "\f1ab8"; -} - -.hgi-stroke.hgi-cursor-01::before { - content: "\f1ab9"; -} - -.hgi-stroke.hgi-cursor-02::before { - content: "\f1aba"; -} - -.hgi-stroke.hgi-cursor-add-selection-01::before { - content: "\f1abb"; -} - -.hgi-stroke.hgi-cursor-add-selection-02::before { - content: "\f1abc"; -} - -.hgi-stroke.hgi-cursor-circle-selection-01::before { - content: "\f1abd"; -} - -.hgi-stroke.hgi-cursor-circle-selection-02::before { - content: "\f1abe"; -} - -.hgi-stroke.hgi-cursor-disabled-01::before { - content: "\f1abf"; -} - -.hgi-stroke.hgi-cursor-disabled-02::before { - content: "\f1ac0"; -} - -.hgi-stroke.hgi-cursor-edit-01::before { - content: "\f1ac1"; -} - -.hgi-stroke.hgi-cursor-edit-02::before { - content: "\f1ac2"; -} - -.hgi-stroke.hgi-cursor-hold-01::before { - content: "\f1ac3"; -} - -.hgi-stroke.hgi-cursor-hold-02::before { - content: "\f1ac4"; -} - -.hgi-stroke.hgi-cursor-in-window::before { - content: "\f1ac5"; -} - -.hgi-stroke.hgi-cursor-info-01::before { - content: "\f1ac6"; -} - -.hgi-stroke.hgi-cursor-info-02::before { - content: "\f1ac7"; -} - -.hgi-stroke.hgi-cursor-loading-01::before { - content: "\f1ac8"; -} - -.hgi-stroke.hgi-cursor-loading-02::before { - content: "\f1ac9"; -} - -.hgi-stroke.hgi-cursor-magic-selection-01::before { - content: "\f1aca"; -} - -.hgi-stroke.hgi-cursor-magic-selection-02::before { - content: "\f1acb"; -} - -.hgi-stroke.hgi-cursor-magic-selection-03::before { - content: "\f3104"; -} - -.hgi-stroke.hgi-cursor-magic-selection-04::before { - content: "\f3105"; -} - -.hgi-stroke.hgi-cursor-move-01::before { - content: "\f1acc"; -} - -.hgi-stroke.hgi-cursor-move-02::before { - content: "\f1acd"; -} - -.hgi-stroke.hgi-cursor-pointer-01::before { - content: "\f1ace"; -} - -.hgi-stroke.hgi-cursor-pointer-02::before { - content: "\f1acf"; -} - -.hgi-stroke.hgi-cursor-progress-01::before { - content: "\f1ad0"; -} - -.hgi-stroke.hgi-cursor-progress-02::before { - content: "\f1ad1"; -} - -.hgi-stroke.hgi-cursor-progress-03::before { - content: "\f1ad2"; -} - -.hgi-stroke.hgi-cursor-progress-04::before { - content: "\f1ad3"; -} - -.hgi-stroke.hgi-cursor-rectangle-selection-01::before { - content: "\f1ad4"; -} - -.hgi-stroke.hgi-cursor-rectangle-selection-02::before { - content: "\f1ad5"; -} - -.hgi-stroke.hgi-cursor-remove-selection-01::before { - content: "\f1ad6"; -} - -.hgi-stroke.hgi-cursor-remove-selection-02::before { - content: "\f1ad7"; -} - -.hgi-stroke.hgi-cursor-text::before { - content: "\f1ad8"; -} - -.hgi-stroke.hgi-curtains::before { - content: "\f1ad9"; -} - -.hgi-stroke.hgi-curvy-left-direction::before { - content: "\f1ada"; -} - -.hgi-stroke.hgi-curvy-left-right-direction::before { - content: "\f1adb"; -} - -.hgi-stroke.hgi-curvy-right-direction::before { - content: "\f1adc"; -} - -.hgi-stroke.hgi-curvy-up-down-direction::before { - content: "\f1add"; -} - -.hgi-stroke.hgi-custom-field::before { - content: "\f1ade"; -} - -.hgi-stroke.hgi-customer-service-01::before { - content: "\f1adf"; -} - -.hgi-stroke.hgi-customer-service-02::before { - content: "\f1ae0"; -} - -.hgi-stroke.hgi-customer-service::before { - content: "\f1ae1"; -} - -.hgi-stroke.hgi-customer-support::before { - content: "\f1ae2"; -} - -.hgi-stroke.hgi-customize::before { - content: "\f1ae3"; -} - -.hgi-stroke.hgi-cylinder-01::before { - content: "\f1ae4"; -} - -.hgi-stroke.hgi-cylinder-02::before { - content: "\f1ae5"; -} - -.hgi-stroke.hgi-cylinder-03::before { - content: "\f1ae6"; -} - -.hgi-stroke.hgi-cylinder-04::before { - content: "\f1ae7"; -} - -.hgi-stroke.hgi-dam::before { - content: "\f1b09"; -} - -.hgi-stroke.hgi-danger::before { - content: "\f1b0a"; -} - -.hgi-stroke.hgi-dark-mode::before { - content: "\f1b0b"; -} - -.hgi-stroke.hgi-dart::before { - content: "\f1b0c"; -} - -.hgi-stroke.hgi-dashboard-browsing::before { - content: "\f1b0d"; -} - -.hgi-stroke.hgi-dashboard-circle-add::before { - content: "\f1b0e"; -} - -.hgi-stroke.hgi-dashboard-circle-edit::before { - content: "\f1b0f"; -} - -.hgi-stroke.hgi-dashboard-circle-remove::before { - content: "\f1b10"; -} - -.hgi-stroke.hgi-dashboard-circle-settings::before { - content: "\f1b11"; -} - -.hgi-stroke.hgi-dashboard-circle::before { - content: "\f1b12"; -} - -.hgi-stroke.hgi-dashboard-speed-01::before { - content: "\f1b13"; -} - -.hgi-stroke.hgi-dashboard-speed-02::before { - content: "\f1b14"; -} - -.hgi-stroke.hgi-dashboard-square-01::before { - content: "\f1b15"; -} - -.hgi-stroke.hgi-dashboard-square-02::before { - content: "\f1b16"; -} - -.hgi-stroke.hgi-dashboard-square-03::before { - content: "\f1b17"; -} - -.hgi-stroke.hgi-dashboard-square-add::before { - content: "\f1b18"; -} - -.hgi-stroke.hgi-dashboard-square-edit::before { - content: "\f1b19"; -} - -.hgi-stroke.hgi-dashboard-square-remove::before { - content: "\f1b1a"; -} - -.hgi-stroke.hgi-dashboard-square-setting::before { - content: "\f1b1b"; -} - -.hgi-stroke.hgi-dashed-line-01::before { - content: "\f1b1c"; -} - -.hgi-stroke.hgi-dashed-line-02::before { - content: "\f1b1d"; -} - -.hgi-stroke.hgi-dashed-line-circle::before { - content: "\f1b1e"; -} - -.hgi-stroke.hgi-data-recovery::before { - content: "\f1b1f"; -} - -.hgi-stroke.hgi-database-01::before { - content: "\f1b20"; -} - -.hgi-stroke.hgi-database-02::before { - content: "\f1b21"; -} - -.hgi-stroke.hgi-database-add::before { - content: "\f1b22"; -} - -.hgi-stroke.hgi-database-backup-2::before { - content: "\f3107"; -} - -.hgi-stroke.hgi-database-backup::before { - content: "\f1b23"; -} - -.hgi-stroke.hgi-database-expand::before { - content: "\f1b24"; -} - -.hgi-stroke.hgi-database-export::before { - content: "\f1b25"; -} - -.hgi-stroke.hgi-database-import::before { - content: "\f1b26"; -} - -.hgi-stroke.hgi-database-lightning::before { - content: "\f1b27"; -} - -.hgi-stroke.hgi-database-locked::before { - content: "\f1b28"; -} - -.hgi-stroke.hgi-database-restore::before { - content: "\f1b29"; -} - -.hgi-stroke.hgi-database-search::before { - content: "\f3108"; -} - -.hgi-stroke.hgi-database-setting::before { - content: "\f1b2a"; -} - -.hgi-stroke.hgi-database-sync-01::before { - content: "\f1b2b"; -} - -.hgi-stroke.hgi-database-sync::before { - content: "\f1b2c"; -} - -.hgi-stroke.hgi-database-zap::before { - content: "\f3109"; -} - -.hgi-stroke.hgi-database::before { - content: "\f1b2d"; -} - -.hgi-stroke.hgi-date-time-2::before { - content: "\f310a"; -} - -.hgi-stroke.hgi-date-time::before { - content: "\f1b2e"; -} - -.hgi-stroke.hgi-dates::before { - content: "\f1b2f"; -} - -.hgi-stroke.hgi-datev::before { - content: "\f1b30"; -} - -.hgi-stroke.hgi-dead::before { - content: "\f1b31"; -} - -.hgi-stroke.hgi-decimals-arrow-left-2::before { - content: "\f310b"; -} - -.hgi-stroke.hgi-decimals-arrow-left-3::before { - content: "\f310c"; -} - -.hgi-stroke.hgi-decimals-arrow-left::before { - content: "\f1b32"; -} - -.hgi-stroke.hgi-decimals-arrow-right::before { - content: "\f310d"; -} - -.hgi-stroke.hgi-deepseek::before { - content: "\f1b33"; -} - -.hgi-stroke.hgi-delete-01::before { - content: "\f1b34"; -} - -.hgi-stroke.hgi-delete-02::before { - content: "\f1b35"; -} - -.hgi-stroke.hgi-delete-03::before { - content: "\f1b36"; -} - -.hgi-stroke.hgi-delete-04::before { - content: "\f1b37"; -} - -.hgi-stroke.hgi-delete-column::before { - content: "\f1b38"; -} - -.hgi-stroke.hgi-delete-put-back::before { - content: "\f1b39"; -} - -.hgi-stroke.hgi-delete-row::before { - content: "\f1b3a"; -} - -.hgi-stroke.hgi-delete-throw::before { - content: "\f1b3b"; -} - -.hgi-stroke.hgi-delivered-sent::before { - content: "\f1b3c"; -} - -.hgi-stroke.hgi-delivery-box-01::before { - content: "\f1b3d"; -} - -.hgi-stroke.hgi-delivery-box-02::before { - content: "\f1b3e"; -} - -.hgi-stroke.hgi-delivery-delay-01::before { - content: "\f1b3f"; -} - -.hgi-stroke.hgi-delivery-delay-02::before { - content: "\f1b40"; -} - -.hgi-stroke.hgi-delivery-return-01::before { - content: "\f1b41"; -} - -.hgi-stroke.hgi-delivery-return-02::before { - content: "\f1b42"; -} - -.hgi-stroke.hgi-delivery-secure-01::before { - content: "\f1b43"; -} - -.hgi-stroke.hgi-delivery-secure-02::before { - content: "\f1b44"; -} - -.hgi-stroke.hgi-delivery-sent-01::before { - content: "\f1b45"; -} - -.hgi-stroke.hgi-delivery-sent-02::before { - content: "\f1b46"; -} - -.hgi-stroke.hgi-delivery-tracking-01::before { - content: "\f1b47"; -} - -.hgi-stroke.hgi-delivery-tracking-02::before { - content: "\f1b48"; -} - -.hgi-stroke.hgi-delivery-truck-01::before { - content: "\f1b49"; -} - -.hgi-stroke.hgi-delivery-truck-02::before { - content: "\f1b4a"; -} - -.hgi-stroke.hgi-delivery-view-01::before { - content: "\f1b4b"; -} - -.hgi-stroke.hgi-delivery-view-02::before { - content: "\f1b4c"; -} - -.hgi-stroke.hgi-dental-braces::before { - content: "\f1b4d"; -} - -.hgi-stroke.hgi-dental-broken-tooth::before { - content: "\f1b4e"; -} - -.hgi-stroke.hgi-dental-care::before { - content: "\f1b4f"; -} - -.hgi-stroke.hgi-dental-tooth::before { - content: "\f1b50"; -} - -.hgi-stroke.hgi-departement::before { - content: "\f1b51"; -} - -.hgi-stroke.hgi-desert::before { - content: "\f1b52"; -} - -.hgi-stroke.hgi-desk-01::before { - content: "\f1b53"; -} - -.hgi-stroke.hgi-desk-02::before { - content: "\f1b54"; -} - -.hgi-stroke.hgi-desk::before { - content: "\f1b55"; -} - -.hgi-stroke.hgi-dessert::before { - content: "\f310e"; -} - -.hgi-stroke.hgi-detergent::before { - content: "\f1b56"; -} - -.hgi-stroke.hgi-developer::before { - content: "\f1b57"; -} - -.hgi-stroke.hgi-deviantart::before { - content: "\f1b58"; -} - -.hgi-stroke.hgi-device-access::before { - content: "\f1b59"; -} - -.hgi-stroke.hgi-diagonal-scroll-point-01::before { - content: "\f1b5a"; -} - -.hgi-stroke.hgi-diagonal-scroll-point-02::before { - content: "\f1b5b"; -} - -.hgi-stroke.hgi-dialpad-circle-01::before { - content: "\f1b5c"; -} - -.hgi-stroke.hgi-dialpad-circle-02::before { - content: "\f1b5d"; -} - -.hgi-stroke.hgi-dialpad-square-01::before { - content: "\f1b5e"; -} - -.hgi-stroke.hgi-dialpad-square-02::before { - content: "\f1b5f"; -} - -.hgi-stroke.hgi-diameter::before { - content: "\f1b60"; -} - -.hgi-stroke.hgi-diamond-01::before { - content: "\f1b61"; -} - -.hgi-stroke.hgi-diamond-02::before { - content: "\f1b62"; -} - -.hgi-stroke.hgi-diamond-minus::before { - content: "\f1b63"; -} - -.hgi-stroke.hgi-diamond-percent::before { - content: "\f1b64"; -} - -.hgi-stroke.hgi-diamond-plus::before { - content: "\f1b65"; -} - -.hgi-stroke.hgi-diamond::before { - content: "\f1b66"; -} - -.hgi-stroke.hgi-diaper::before { - content: "\f1b67"; -} - -.hgi-stroke.hgi-dice-faces-01::before { - content: "\f1b68"; -} - -.hgi-stroke.hgi-dice-faces-02::before { - content: "\f1b69"; -} - -.hgi-stroke.hgi-dice-faces-03::before { - content: "\f1b6a"; -} - -.hgi-stroke.hgi-dice-faces-04::before { - content: "\f1b6b"; -} - -.hgi-stroke.hgi-dice-faces-05::before { - content: "\f1b6c"; -} - -.hgi-stroke.hgi-dice-faces-06::before { - content: "\f1b6d"; -} - -.hgi-stroke.hgi-dice::before { - content: "\f1b6e"; -} - -.hgi-stroke.hgi-dices::before { - content: "\f310f"; -} - -.hgi-stroke.hgi-diff::before { - content: "\f3110"; -} - -.hgi-stroke.hgi-digestion::before { - content: "\f1b6f"; -} - -.hgi-stroke.hgi-digg::before { - content: "\f1b70"; -} - -.hgi-stroke.hgi-digital-clock::before { - content: "\f1b71"; -} - -.hgi-stroke.hgi-dim-sum-01::before { - content: "\f1b72"; -} - -.hgi-stroke.hgi-dim-sum-02::before { - content: "\f1b73"; -} - -.hgi-stroke.hgi-dining-table::before { - content: "\f1b74"; -} - -.hgi-stroke.hgi-diploma::before { - content: "\f1b75"; -} - -.hgi-stroke.hgi-direction-left-01::before { - content: "\f1b76"; -} - -.hgi-stroke.hgi-direction-left-02::before { - content: "\f1b77"; -} - -.hgi-stroke.hgi-direction-right-01::before { - content: "\f1b78"; -} - -.hgi-stroke.hgi-direction-right-02::before { - content: "\f1b79"; -} - -.hgi-stroke.hgi-directions-01::before { - content: "\f1b7a"; -} - -.hgi-stroke.hgi-directions-02::before { - content: "\f1b7b"; -} - -.hgi-stroke.hgi-dirham::before { - content: "\f1b7c"; -} - -.hgi-stroke.hgi-disability-01::before { - content: "\f1b7d"; -} - -.hgi-stroke.hgi-disability-02::before { - content: "\f1b7e"; -} - -.hgi-stroke.hgi-disc-2::before { - content: "\f1b7f"; -} - -.hgi-stroke.hgi-disc-3::before { - content: "\f1b80"; -} - -.hgi-stroke.hgi-disc-album::before { - content: "\f1b81"; -} - -.hgi-stroke.hgi-disc::before { - content: "\f1b82"; -} - -.hgi-stroke.hgi-discord::before { - content: "\f1b83"; -} - -.hgi-stroke.hgi-discount-01::before { - content: "\f1b84"; -} - -.hgi-stroke.hgi-discount-tag-01::before { - content: "\f1b85"; -} - -.hgi-stroke.hgi-discount-tag-02::before { - content: "\f1b86"; -} - -.hgi-stroke.hgi-discount::before { - content: "\f1b87"; -} - -.hgi-stroke.hgi-discover-circle::before { - content: "\f1b88"; -} - -.hgi-stroke.hgi-discover-square::before { - content: "\f1b89"; -} - -.hgi-stroke.hgi-dish-01::before { - content: "\f1b8a"; -} - -.hgi-stroke.hgi-dish-02::before { - content: "\f1b8b"; -} - -.hgi-stroke.hgi-dish-washer::before { - content: "\f1b8c"; -} - -.hgi-stroke.hgi-displeased::before { - content: "\f1b8d"; -} - -.hgi-stroke.hgi-distribute-horizontal-center::before { - content: "\f1b8e"; -} - -.hgi-stroke.hgi-distribute-horizontal-left::before { - content: "\f1b8f"; -} - -.hgi-stroke.hgi-distribute-horizontal-right::before { - content: "\f1b90"; -} - -.hgi-stroke.hgi-distribute-vertical-bottom::before { - content: "\f1b91"; -} - -.hgi-stroke.hgi-distribute-vertical-center::before { - content: "\f1b92"; -} - -.hgi-stroke.hgi-distribute-vertical-top::before { - content: "\f1b93"; -} - -.hgi-stroke.hgi-distribution::before { - content: "\f1b94"; -} - -.hgi-stroke.hgi-divide-sign-circle::before { - content: "\f1b95"; -} - -.hgi-stroke.hgi-divide-sign-square::before { - content: "\f1b96"; -} - -.hgi-stroke.hgi-divide-sign::before { - content: "\f1b97"; -} - -.hgi-stroke.hgi-divide::before { - content: "\f3111"; -} - -.hgi-stroke.hgi-dna-01::before { - content: "\f1b98"; -} - -.hgi-stroke.hgi-dna-off::before { - content: "\f3112"; -} - -.hgi-stroke.hgi-dna::before { - content: "\f1b99"; -} - -.hgi-stroke.hgi-do-not-touch-01::before { - content: "\f1b9a"; -} - -.hgi-stroke.hgi-do-not-touch-02::before { - content: "\f1b9b"; -} - -.hgi-stroke.hgi-doc-01::before { - content: "\f1b9c"; -} - -.hgi-stroke.hgi-doc-02::before { - content: "\f1b9d"; -} - -.hgi-stroke.hgi-dock::before { - content: "\f3113"; -} - -.hgi-stroke.hgi-doctor-01::before { - content: "\f1b9e"; -} - -.hgi-stroke.hgi-doctor-02::before { - content: "\f1b9f"; -} - -.hgi-stroke.hgi-doctor-03::before { - content: "\f1ba0"; -} - -.hgi-stroke.hgi-document-attachment::before { - content: "\f1ba1"; -} - -.hgi-stroke.hgi-document-code::before { - content: "\f1ba2"; -} - -.hgi-stroke.hgi-document-validation::before { - content: "\f1ba3"; -} - -.hgi-stroke.hgi-dollar-01::before { - content: "\f1ba4"; -} - -.hgi-stroke.hgi-dollar-02::before { - content: "\f1ba5"; -} - -.hgi-stroke.hgi-dollar-circle::before { - content: "\f1ba6"; -} - -.hgi-stroke.hgi-dollar-receive-01::before { - content: "\f1ba7"; -} - -.hgi-stroke.hgi-dollar-receive-02::before { - content: "\f1ba8"; -} - -.hgi-stroke.hgi-dollar-send-01::before { - content: "\f1ba9"; -} - -.hgi-stroke.hgi-dollar-send-02::before { - content: "\f1baa"; -} - -.hgi-stroke.hgi-dollar-sign::before { - content: "\f3114"; -} - -.hgi-stroke.hgi-dollar-square::before { - content: "\f1bab"; -} - -.hgi-stroke.hgi-dome::before { - content: "\f1bac"; -} - -.hgi-stroke.hgi-domino::before { - content: "\f1bad"; -} - -.hgi-stroke.hgi-donut::before { - content: "\f3115"; -} - -.hgi-stroke.hgi-door-01::before { - content: "\f1bae"; -} - -.hgi-stroke.hgi-door-02::before { - content: "\f1baf"; -} - -.hgi-stroke.hgi-door-closed-locked::before { - content: "\f3117"; -} - -.hgi-stroke.hgi-door-closed::before { - content: "\f3116"; -} - -.hgi-stroke.hgi-door-lock::before { - content: "\f1bb0"; -} - -.hgi-stroke.hgi-door-open::before { - content: "\f1bb1"; -} - -.hgi-stroke.hgi-door::before { - content: "\f1bb2"; -} - -.hgi-stroke.hgi-dot::before { - content: "\f3118"; -} - -.hgi-stroke.hgi-doughnut::before { - content: "\f1bb3"; -} - -.hgi-stroke.hgi-download-01::before { - content: "\f1bb4"; -} - -.hgi-stroke.hgi-download-02::before { - content: "\f1bb5"; -} - -.hgi-stroke.hgi-download-03::before { - content: "\f1bb6"; -} - -.hgi-stroke.hgi-download-04::before { - content: "\f1bb7"; -} - -.hgi-stroke.hgi-download-05::before { - content: "\f1bb8"; -} - -.hgi-stroke.hgi-download-06::before { - content: "\f1bb9"; -} - -.hgi-stroke.hgi-download-circle-01::before { - content: "\f1bba"; -} - -.hgi-stroke.hgi-download-circle-02::before { - content: "\f1bbb"; -} - -.hgi-stroke.hgi-download-square-01::before { - content: "\f1bbc"; -} - -.hgi-stroke.hgi-download-square-02::before { - content: "\f1bbd"; -} - -.hgi-stroke.hgi-drafting-compass::before { - content: "\f3119"; -} - -.hgi-stroke.hgi-drag-01::before { - content: "\f1bbe"; -} - -.hgi-stroke.hgi-drag-02::before { - content: "\f1bbf"; -} - -.hgi-stroke.hgi-drag-03::before { - content: "\f1bc0"; -} - -.hgi-stroke.hgi-drag-04::before { - content: "\f1bc1"; -} - -.hgi-stroke.hgi-drag-drop-horizontal::before { - content: "\f1bc2"; -} - -.hgi-stroke.hgi-drag-drop-vertical::before { - content: "\f1bc3"; -} - -.hgi-stroke.hgi-drag-drop::before { - content: "\f1bc4"; -} - -.hgi-stroke.hgi-drag-left-01::before { - content: "\f1bc5"; -} - -.hgi-stroke.hgi-drag-left-02::before { - content: "\f1bc6"; -} - -.hgi-stroke.hgi-drag-left-03::before { - content: "\f1bc7"; -} - -.hgi-stroke.hgi-drag-left-04::before { - content: "\f1bc8"; -} - -.hgi-stroke.hgi-drag-right-01::before { - content: "\f1bc9"; -} - -.hgi-stroke.hgi-drag-right-02::before { - content: "\f1bca"; -} - -.hgi-stroke.hgi-drag-right-03::before { - content: "\f1bcb"; -} - -.hgi-stroke.hgi-drag-right-04::before { - content: "\f1bcc"; -} - -.hgi-stroke.hgi-drama::before { - content: "\f311a"; -} - -.hgi-stroke.hgi-drawing-compass::before { - content: "\f1bcd"; -} - -.hgi-stroke.hgi-drawing-mode::before { - content: "\f1bce"; -} - -.hgi-stroke.hgi-dress-01::before { - content: "\f1bcf"; -} - -.hgi-stroke.hgi-dress-02::before { - content: "\f1bd0"; -} - -.hgi-stroke.hgi-dress-03::before { - content: "\f1bd1"; -} - -.hgi-stroke.hgi-dress-04::before { - content: "\f1bd2"; -} - -.hgi-stroke.hgi-dress-05::before { - content: "\f1bd3"; -} - -.hgi-stroke.hgi-dress-06::before { - content: "\f1bd4"; -} - -.hgi-stroke.hgi-dress-07::before { - content: "\f1bd5"; -} - -.hgi-stroke.hgi-dressing-table-01::before { - content: "\f1bd6"; -} - -.hgi-stroke.hgi-dressing-table-02::before { - content: "\f1bd7"; -} - -.hgi-stroke.hgi-dressing-table-03::before { - content: "\f1bd8"; -} - -.hgi-stroke.hgi-dribbble::before { - content: "\f1bd9"; -} - -.hgi-stroke.hgi-drill::before { - content: "\f1bda"; -} - -.hgi-stroke.hgi-drink::before { - content: "\f1bdb"; -} - -.hgi-stroke.hgi-drive::before { - content: "\f1bdc"; -} - -.hgi-stroke.hgi-drone::before { - content: "\f1bdd"; -} - -.hgi-stroke.hgi-drooling::before { - content: "\f1bde"; -} - -.hgi-stroke.hgi-dropbox::before { - content: "\f1bdf"; -} - -.hgi-stroke.hgi-dropdown-field-type::before { - content: "\f1be0"; -} - -.hgi-stroke.hgi-droplet-off::before { - content: "\f311b"; -} - -.hgi-stroke.hgi-droplet::before { - content: "\f1be1"; -} - -.hgi-stroke.hgi-droplets::before { - content: "\f311c"; -} - -.hgi-stroke.hgi-dropper::before { - content: "\f1be2"; -} - -.hgi-stroke.hgi-drum::before { - content: "\f311d"; -} - -.hgi-stroke.hgi-drumstick::before { - content: "\f311e"; -} - -.hgi-stroke.hgi-ds3-tool::before { - content: "\f1be3"; -} - -.hgi-stroke.hgi-dua::before { - content: "\f1be4"; -} - -.hgi-stroke.hgi-dumbbell-01::before { - content: "\f1be5"; -} - -.hgi-stroke.hgi-dumbbell-02::before { - content: "\f1be6"; -} - -.hgi-stroke.hgi-dumbbell-03::before { - content: "\f1be7"; -} - -.hgi-stroke.hgi-dump-truck::before { - content: "\f311f"; -} - -.hgi-stroke.hgi-duplex::before { - content: "\f1be8"; -} - -.hgi-stroke.hgi-ear-off::before { - content: "\f1bee"; -} - -.hgi-stroke.hgi-ear-rings-01::before { - content: "\f1bef"; -} - -.hgi-stroke.hgi-ear-rings-02::before { - content: "\f1bf0"; -} - -.hgi-stroke.hgi-ear-rings-03::before { - content: "\f1bf1"; -} - -.hgi-stroke.hgi-ear::before { - content: "\f1bf2"; -} - -.hgi-stroke.hgi-earth-2::before { - content: "\f3120"; -} - -.hgi-stroke.hgi-earth-lock::before { - content: "\f1bf3"; -} - -.hgi-stroke.hgi-earth::before { - content: "\f1bf4"; -} - -.hgi-stroke.hgi-ease-curve-control-points::before { - content: "\f1bf5"; -} - -.hgi-stroke.hgi-ease-in-control-point::before { - content: "\f1bf6"; -} - -.hgi-stroke.hgi-ease-in-out::before { - content: "\f1bf7"; -} - -.hgi-stroke.hgi-ease-in::before { - content: "\f1bf8"; -} - -.hgi-stroke.hgi-ease-out-control-point::before { - content: "\f1bf9"; -} - -.hgi-stroke.hgi-ease-out::before { - content: "\f1bfa"; -} - -.hgi-stroke.hgi-eclipse::before { - content: "\f3121"; -} - -.hgi-stroke.hgi-eco-energy::before { - content: "\f1bfb"; -} - -.hgi-stroke.hgi-eco-lab-01::before { - content: "\f1bfc"; -} - -.hgi-stroke.hgi-eco-lab-02::before { - content: "\f1bfd"; -} - -.hgi-stroke.hgi-eco-lab::before { - content: "\f1bfe"; -} - -.hgi-stroke.hgi-eco-power::before { - content: "\f1bff"; -} - -.hgi-stroke.hgi-edge-style::before { - content: "\f1c00"; -} - -.hgi-stroke.hgi-edit-01::before { - content: "\f1c01"; -} - -.hgi-stroke.hgi-edit-02::before { - content: "\f1c02"; -} - -.hgi-stroke.hgi-edit-03::before { - content: "\f1c03"; -} - -.hgi-stroke.hgi-edit-04::before { - content: "\f1c04"; -} - -.hgi-stroke.hgi-edit-off-03::before { - content: "\f1c05"; -} - -.hgi-stroke.hgi-edit-off-04::before { - content: "\f1c06"; -} - -.hgi-stroke.hgi-edit-off::before { - content: "\f1c07"; -} - -.hgi-stroke.hgi-edit-road::before { - content: "\f1c08"; -} - -.hgi-stroke.hgi-edit-table::before { - content: "\f1c09"; -} - -.hgi-stroke.hgi-edit-user-02::before { - content: "\f1c0a"; -} - -.hgi-stroke.hgi-egg-fried::before { - content: "\f1c0b"; -} - -.hgi-stroke.hgi-egg-off::before { - content: "\f1c0c"; -} - -.hgi-stroke.hgi-egg::before { - content: "\f1c0d"; -} - -.hgi-stroke.hgi-eggs::before { - content: "\f1c0e"; -} - -.hgi-stroke.hgi-eid-mubarak::before { - content: "\f1c0f"; -} - -.hgi-stroke.hgi-eiffel-tower::before { - content: "\f1c10"; -} - -.hgi-stroke.hgi-eight-circle::before { - content: "\f1c11"; -} - -.hgi-stroke.hgi-eight-square::before { - content: "\f1c12"; -} - -.hgi-stroke.hgi-elder-01::before { - content: "\f3123"; -} - -.hgi-stroke.hgi-elder::before { - content: "\f3122"; -} - -.hgi-stroke.hgi-elearning-exchange::before { - content: "\f1c13"; -} - -.hgi-stroke.hgi-electric-home-01::before { - content: "\f1c14"; -} - -.hgi-stroke.hgi-electric-home-02::before { - content: "\f1c15"; -} - -.hgi-stroke.hgi-electric-plugs::before { - content: "\f1c16"; -} - -.hgi-stroke.hgi-electric-tower-01::before { - content: "\f1c17"; -} - -.hgi-stroke.hgi-electric-tower-02::before { - content: "\f1c18"; -} - -.hgi-stroke.hgi-electric-wire::before { - content: "\f1c19"; -} - -.hgi-stroke.hgi-electricity-stack::before { - content: "\f1c1a"; -} - -.hgi-stroke.hgi-ellipse-selection::before { - content: "\f1c1b"; -} - -.hgi-stroke.hgi-ellipse::before { - content: "\f3124"; -} - -.hgi-stroke.hgi-ellipsis-vertical::before { - content: "\f3126"; -} - -.hgi-stroke.hgi-ellipsis::before { - content: "\f3125"; -} - -.hgi-stroke.hgi-encrypt::before { - content: "\f1c1c"; -} - -.hgi-stroke.hgi-energy-ellipse::before { - content: "\f1c1d"; -} - -.hgi-stroke.hgi-energy-rectangle::before { - content: "\f1c1e"; -} - -.hgi-stroke.hgi-energy::before { - content: "\f1c1f"; -} - -.hgi-stroke.hgi-engine-slash::before { - content: "\f1c20"; -} - -.hgi-stroke.hgi-engine::before { - content: "\f1c21"; -} - -.hgi-stroke.hgi-entering-geo-fence::before { - content: "\f1c22"; -} - -.hgi-stroke.hgi-entrance-stairs::before { - content: "\f1c23"; -} - -.hgi-stroke.hgi-envato::before { - content: "\f1c24"; -} - -.hgi-stroke.hgi-equal-not::before { - content: "\f3128"; -} - -.hgi-stroke.hgi-equal-sign-circle::before { - content: "\f1c25"; -} - -.hgi-stroke.hgi-equal-sign-square::before { - content: "\f1c26"; -} - -.hgi-stroke.hgi-equal-sign::before { - content: "\f1c27"; -} - -.hgi-stroke.hgi-equal::before { - content: "\f3127"; -} - -.hgi-stroke.hgi-equipment-bench-press::before { - content: "\f1c28"; -} - -.hgi-stroke.hgi-equipment-chest-press::before { - content: "\f1c29"; -} - -.hgi-stroke.hgi-equipment-gym-01::before { - content: "\f1c2a"; -} - -.hgi-stroke.hgi-equipment-gym-02::before { - content: "\f1c2b"; -} - -.hgi-stroke.hgi-equipment-gym-03::before { - content: "\f1c2c"; -} - -.hgi-stroke.hgi-equipment-weightlifting::before { - content: "\f1c2d"; -} - -.hgi-stroke.hgi-eraser-01::before { - content: "\f1c2e"; -} - -.hgi-stroke.hgi-eraser-add::before { - content: "\f1c2f"; -} - -.hgi-stroke.hgi-eraser-auto::before { - content: "\f1c30"; -} - -.hgi-stroke.hgi-eraser::before { - content: "\f1c31"; -} - -.hgi-stroke.hgi-estimate-01::before { - content: "\f1c32"; -} - -.hgi-stroke.hgi-estimate-02::before { - content: "\f1c33"; -} - -.hgi-stroke.hgi-ethereum-ellipse::before { - content: "\f1c34"; -} - -.hgi-stroke.hgi-ethereum-rectangle::before { - content: "\f1c35"; -} - -.hgi-stroke.hgi-ethereum::before { - content: "\f1c36"; -} - -.hgi-stroke.hgi-euro-circle::before { - content: "\f1c37"; -} - -.hgi-stroke.hgi-euro-receive::before { - content: "\f1c38"; -} - -.hgi-stroke.hgi-euro-send::before { - content: "\f1c39"; -} - -.hgi-stroke.hgi-euro-square::before { - content: "\f1c3a"; -} - -.hgi-stroke.hgi-euro::before { - content: "\f1c3b"; -} - -.hgi-stroke.hgi-ev-charger::before { - content: "\f3129"; -} - -.hgi-stroke.hgi-ev-charging::before { - content: "\f1c3c"; -} - -.hgi-stroke.hgi-evil::before { - content: "\f1c3d"; -} - -.hgi-stroke.hgi-exchange-01::before { - content: "\f1c3e"; -} - -.hgi-stroke.hgi-exchange-02::before { - content: "\f1c3f"; -} - -.hgi-stroke.hgi-exchange-03::before { - content: "\f1c40"; -} - -.hgi-stroke.hgi-exchange-bitcoin::before { - content: "\f1c41"; -} - -.hgi-stroke.hgi-exchange-dollar::before { - content: "\f312a"; -} - -.hgi-stroke.hgi-exclamation-mark-big-slash::before { - content: "\f1c42"; -} - -.hgi-stroke.hgi-exclamation-mark-big::before { - content: "\f1c43"; -} - -.hgi-stroke.hgi-exclamation-mark-slash::before { - content: "\f1c44"; -} - -.hgi-stroke.hgi-exclamation-mark::before { - content: "\f1c45"; -} - -.hgi-stroke.hgi-expand-paragraph::before { - content: "\f1c46"; -} - -.hgi-stroke.hgi-expand::before { - content: "\f1c47"; -} - -.hgi-stroke.hgi-expander::before { - content: "\f1c48"; -} - -.hgi-stroke.hgi-external-drive::before { - content: "\f1c49"; -} - -.hgi-stroke.hgi-external-link::before { - content: "\f312b"; -} - -.hgi-stroke.hgi-eye-closed::before { - content: "\f312c"; -} - -.hgi-stroke.hgi-eye-off::before { - content: "\f312d"; -} - -.hgi-stroke.hgi-eye::before { - content: "\f1c4a"; -} - -.hgi-stroke.hgi-face-id::before { - content: "\f1c53"; -} - -.hgi-stroke.hgi-face-mimic-2::before { - content: "\f312f"; -} - -.hgi-stroke.hgi-face-mimic::before { - content: "\f312e"; -} - -.hgi-stroke.hgi-facebook-01::before { - content: "\f1c54"; -} - -.hgi-stroke.hgi-facebook-02::before { - content: "\f1c55"; -} - -.hgi-stroke.hgi-factory-01::before { - content: "\f1c56"; -} - -.hgi-stroke.hgi-factory-02::before { - content: "\f1c57"; -} - -.hgi-stroke.hgi-factory::before { - content: "\f1c58"; -} - -.hgi-stroke.hgi-fahrenheit::before { - content: "\f1c59"; -} - -.hgi-stroke.hgi-falling-star-2::before { - content: "\f3130"; -} - -.hgi-stroke.hgi-falling-star::before { - content: "\f1c5a"; -} - -.hgi-stroke.hgi-fan-01::before { - content: "\f1c5b"; -} - -.hgi-stroke.hgi-fan-02::before { - content: "\f1c5c"; -} - -.hgi-stroke.hgi-fast-forward::before { - content: "\f3131"; -} - -.hgi-stroke.hgi-fast-wind::before { - content: "\f1c5d"; -} - -.hgi-stroke.hgi-favourite-circle::before { - content: "\f1c5e"; -} - -.hgi-stroke.hgi-favourite-square::before { - content: "\f1c5f"; -} - -.hgi-stroke.hgi-favourite::before { - content: "\f1c60"; -} - -.hgi-stroke.hgi-feather::before { - content: "\f1c61"; -} - -.hgi-stroke.hgi-female-02::before { - content: "\f1c62"; -} - -.hgi-stroke.hgi-female-symbol::before { - content: "\f1c63"; -} - -.hgi-stroke.hgi-fence::before { - content: "\f1c64"; -} - -.hgi-stroke.hgi-fencing-mask::before { - content: "\f1c65"; -} - -.hgi-stroke.hgi-fencing::before { - content: "\f1c66"; -} - -.hgi-stroke.hgi-ferris-wheel::before { - content: "\f1c67"; -} - -.hgi-stroke.hgi-ferry-boat::before { - content: "\f1c68"; -} - -.hgi-stroke.hgi-figma::before { - content: "\f1c69"; -} - -.hgi-stroke.hgi-file-01::before { - content: "\f1c6a"; -} - -.hgi-stroke.hgi-file-02::before { - content: "\f1c6b"; -} - -.hgi-stroke.hgi-file-add::before { - content: "\f1c6c"; -} - -.hgi-stroke.hgi-file-archive::before { - content: "\f1c6d"; -} - -.hgi-stroke.hgi-file-attachment::before { - content: "\f1c6e"; -} - -.hgi-stroke.hgi-file-audio::before { - content: "\f1c6f"; -} - -.hgi-stroke.hgi-file-axis-three-d::before { - content: "\f1c70"; -} - -.hgi-stroke.hgi-file-axis3d::before { - content: "\f3132"; -} - -.hgi-stroke.hgi-file-badge::before { - content: "\f1c71"; -} - -.hgi-stroke.hgi-file-bitcoin::before { - content: "\f1c72"; -} - -.hgi-stroke.hgi-file-block::before { - content: "\f1c73"; -} - -.hgi-stroke.hgi-file-bookmark::before { - content: "\f1c74"; -} - -.hgi-stroke.hgi-file-box::before { - content: "\f1c75"; -} - -.hgi-stroke.hgi-file-braces-corner::before { - content: "\f3133"; -} - -.hgi-stroke.hgi-file-braces::before { - content: "\f1c76"; -} - -.hgi-stroke.hgi-file-chart-column-increasing::before { - content: "\f1c77"; -} - -.hgi-stroke.hgi-file-chart-column::before { - content: "\f1c78"; -} - -.hgi-stroke.hgi-file-chart-line::before { - content: "\f3134"; -} - -.hgi-stroke.hgi-file-chart-pie::before { - content: "\f1c79"; -} - -.hgi-stroke.hgi-file-check-corner::before { - content: "\f3136"; -} - -.hgi-stroke.hgi-file-check::before { - content: "\f3135"; -} - -.hgi-stroke.hgi-file-clock::before { - content: "\f1c7a"; -} - -.hgi-stroke.hgi-file-cloud::before { - content: "\f1c7b"; -} - -.hgi-stroke.hgi-file-code-corner::before { - content: "\f1c7c"; -} - -.hgi-stroke.hgi-file-code::before { - content: "\f1c7d"; -} - -.hgi-stroke.hgi-file-cog::before { - content: "\f3137"; -} - -.hgi-stroke.hgi-file-corrupt::before { - content: "\f1c7e"; -} - -.hgi-stroke.hgi-file-database::before { - content: "\f1c7f"; -} - -.hgi-stroke.hgi-file-diff::before { - content: "\f1c80"; -} - -.hgi-stroke.hgi-file-digit::before { - content: "\f1c81"; -} - -.hgi-stroke.hgi-file-dollar::before { - content: "\f1c82"; -} - -.hgi-stroke.hgi-file-down::before { - content: "\f1c83"; -} - -.hgi-stroke.hgi-file-download::before { - content: "\f1c84"; -} - -.hgi-stroke.hgi-file-edit::before { - content: "\f1c85"; -} - -.hgi-stroke.hgi-file-empty-01::before { - content: "\f1c86"; -} - -.hgi-stroke.hgi-file-empty-02::before { - content: "\f1c87"; -} - -.hgi-stroke.hgi-file-euro::before { - content: "\f1c88"; -} - -.hgi-stroke.hgi-file-exclamation-point::before { - content: "\f1c89"; -} - -.hgi-stroke.hgi-file-export::before { - content: "\f1c8a"; -} - -.hgi-stroke.hgi-file-favourite::before { - content: "\f1c8b"; -} - -.hgi-stroke.hgi-file-headphone::before { - content: "\f1c8c"; -} - -.hgi-stroke.hgi-file-heart::before { - content: "\f1c8d"; -} - -.hgi-stroke.hgi-file-image::before { - content: "\f1c8e"; -} - -.hgi-stroke.hgi-file-import::before { - content: "\f1c8f"; -} - -.hgi-stroke.hgi-file-input::before { - content: "\f1c90"; -} - -.hgi-stroke.hgi-file-key::before { - content: "\f1c91"; -} - -.hgi-stroke.hgi-file-link::before { - content: "\f1c92"; -} - -.hgi-stroke.hgi-file-lock::before { - content: "\f1c93"; -} - -.hgi-stroke.hgi-file-locked::before { - content: "\f1c94"; -} - -.hgi-stroke.hgi-file-management::before { - content: "\f1c95"; -} - -.hgi-stroke.hgi-file-minus-corner::before { - content: "\f3138"; -} - -.hgi-stroke.hgi-file-minus::before { - content: "\f1c96"; -} - -.hgi-stroke.hgi-file-music::before { - content: "\f1c97"; -} - -.hgi-stroke.hgi-file-not-found::before { - content: "\f1c98"; -} - -.hgi-stroke.hgi-file-output::before { - content: "\f3139"; -} - -.hgi-stroke.hgi-file-paste::before { - content: "\f1c99"; -} - -.hgi-stroke.hgi-file-pen-line::before { - content: "\f313a"; -} - -.hgi-stroke.hgi-file-pen::before { - content: "\f1c9a"; -} - -.hgi-stroke.hgi-file-pin::before { - content: "\f1c9b"; -} - -.hgi-stroke.hgi-file-play::before { - content: "\f1c9c"; -} - -.hgi-stroke.hgi-file-plus-corner::before { - content: "\f313b"; -} - -.hgi-stroke.hgi-file-plus::before { - content: "\f1c9d"; -} - -.hgi-stroke.hgi-file-pound::before { - content: "\f1c9e"; -} - -.hgi-stroke.hgi-file-question-mark::before { - content: "\f1c9f"; -} - -.hgi-stroke.hgi-file-remove::before { - content: "\f1ca0"; -} - -.hgi-stroke.hgi-file-scan::before { - content: "\f1ca1"; -} - -.hgi-stroke.hgi-file-script::before { - content: "\f1ca2"; -} - -.hgi-stroke.hgi-file-search-2::before { - content: "\f313c"; -} - -.hgi-stroke.hgi-file-search-corner::before { - content: "\f313d"; -} - -.hgi-stroke.hgi-file-search::before { - content: "\f1ca3"; -} - -.hgi-stroke.hgi-file-security::before { - content: "\f1ca4"; -} - -.hgi-stroke.hgi-file-shredder::before { - content: "\f1ca5"; -} - -.hgi-stroke.hgi-file-signal::before { - content: "\f1ca6"; -} - -.hgi-stroke.hgi-file-sliders::before { - content: "\f1ca7"; -} - -.hgi-stroke.hgi-file-spreadsheet::before { - content: "\f1ca8"; -} - -.hgi-stroke.hgi-file-stack::before { - content: "\f1ca9"; -} - -.hgi-stroke.hgi-file-star::before { - content: "\f1caa"; -} - -.hgi-stroke.hgi-file-symlink::before { - content: "\f313e"; -} - -.hgi-stroke.hgi-file-sync::before { - content: "\f1cab"; -} - -.hgi-stroke.hgi-file-terminal::before { - content: "\f313f"; -} - -.hgi-stroke.hgi-file-text::before { - content: "\f3140"; -} - -.hgi-stroke.hgi-file-type-corner::before { - content: "\f3142"; -} - -.hgi-stroke.hgi-file-type::before { - content: "\f3141"; -} - -.hgi-stroke.hgi-file-unknown::before { - content: "\f1cac"; -} - -.hgi-stroke.hgi-file-unlocked::before { - content: "\f1cad"; -} - -.hgi-stroke.hgi-file-up::before { - content: "\f3143"; -} - -.hgi-stroke.hgi-file-upload::before { - content: "\f1cae"; -} - -.hgi-stroke.hgi-file-user::before { - content: "\f3144"; -} - -.hgi-stroke.hgi-file-validation::before { - content: "\f1caf"; -} - -.hgi-stroke.hgi-file-verified::before { - content: "\f1cb0"; -} - -.hgi-stroke.hgi-file-video-camera::before { - content: "\f3145"; -} - -.hgi-stroke.hgi-file-video::before { - content: "\f1cb1"; -} - -.hgi-stroke.hgi-file-view::before { - content: "\f1cb2"; -} - -.hgi-stroke.hgi-file-volume::before { - content: "\f3146"; -} - -.hgi-stroke.hgi-file-x::before { - content: "\f3147"; -} - -.hgi-stroke.hgi-file-yen::before { - content: "\f1cb3"; -} - -.hgi-stroke.hgi-file-zip::before { - content: "\f1cb4"; -} - -.hgi-stroke.hgi-files-01::before { - content: "\f1cb5"; -} - -.hgi-stroke.hgi-files-02::before { - content: "\f1cb6"; -} - -.hgi-stroke.hgi-film-01::before { - content: "\f1cb7"; -} - -.hgi-stroke.hgi-film-02::before { - content: "\f1cb8"; -} - -.hgi-stroke.hgi-film-roll-01::before { - content: "\f1cb9"; -} - -.hgi-stroke.hgi-film-roll-02::before { - content: "\f1cba"; -} - -.hgi-stroke.hgi-filter-add::before { - content: "\f1cbb"; -} - -.hgi-stroke.hgi-filter-edit::before { - content: "\f1cbc"; -} - -.hgi-stroke.hgi-filter-horizontal::before { - content: "\f1cbd"; -} - -.hgi-stroke.hgi-filter-mail-circle::before { - content: "\f1cbe"; -} - -.hgi-stroke.hgi-filter-mail-edit::before { - content: "\f1cbf"; -} - -.hgi-stroke.hgi-filter-mail-remove::before { - content: "\f1cc0"; -} - -.hgi-stroke.hgi-filter-mail-square::before { - content: "\f1cc1"; -} - -.hgi-stroke.hgi-filter-mail::before { - content: "\f1cc2"; -} - -.hgi-stroke.hgi-filter-remove::before { - content: "\f1cc3"; -} - -.hgi-stroke.hgi-filter-reset::before { - content: "\f1cc4"; -} - -.hgi-stroke.hgi-filter-vertical::before { - content: "\f1cc5"; -} - -.hgi-stroke.hgi-filter::before { - content: "\f1cc6"; -} - -.hgi-stroke.hgi-finger-access::before { - content: "\f1cc7"; -} - -.hgi-stroke.hgi-finger-print-add::before { - content: "\f1cc8"; -} - -.hgi-stroke.hgi-finger-print-check::before { - content: "\f1cc9"; -} - -.hgi-stroke.hgi-finger-print-minus::before { - content: "\f1cca"; -} - -.hgi-stroke.hgi-finger-print-remove::before { - content: "\f1ccb"; -} - -.hgi-stroke.hgi-finger-print-scan::before { - content: "\f1ccc"; -} - -.hgi-stroke.hgi-finger-print::before { - content: "\f1ccd"; -} - -.hgi-stroke.hgi-fingerprint-pattern::before { - content: "\f3148"; -} - -.hgi-stroke.hgi-fingerprint-scan::before { - content: "\f1cce"; -} - -.hgi-stroke.hgi-fins::before { - content: "\f1ccf"; -} - -.hgi-stroke.hgi-fire-02::before { - content: "\f1cd0"; -} - -.hgi-stroke.hgi-fire-03::before { - content: "\f1cd1"; -} - -.hgi-stroke.hgi-fire-extinguisher::before { - content: "\f1cd2"; -} - -.hgi-stroke.hgi-fire-pit::before { - content: "\f1cd3"; -} - -.hgi-stroke.hgi-fire-security::before { - content: "\f1cd4"; -} - -.hgi-stroke.hgi-fire::before { - content: "\f1cd5"; -} - -.hgi-stroke.hgi-firewall::before { - content: "\f1cd6"; -} - -.hgi-stroke.hgi-fireworks::before { - content: "\f1cd7"; -} - -.hgi-stroke.hgi-first-aid-kit::before { - content: "\f1cd8"; -} - -.hgi-stroke.hgi-first-bracket-circle::before { - content: "\f1cd9"; -} - -.hgi-stroke.hgi-first-bracket-square::before { - content: "\f1cda"; -} - -.hgi-stroke.hgi-first-bracket::before { - content: "\f1cdb"; -} - -.hgi-stroke.hgi-fish-food::before { - content: "\f1cdc"; -} - -.hgi-stroke.hgi-fish-off::before { - content: "\f1cdd"; -} - -.hgi-stroke.hgi-fish-symbol::before { - content: "\f1cde"; -} - -.hgi-stroke.hgi-fish::before { - content: "\f3149"; -} - -.hgi-stroke.hgi-fishing-hook::before { - content: "\f314a"; -} - -.hgi-stroke.hgi-fishing-rod::before { - content: "\f314b"; -} - -.hgi-stroke.hgi-fit-to-screen::before { - content: "\f1cdf"; -} - -.hgi-stroke.hgi-five-circle::before { - content: "\f1ce0"; -} - -.hgi-stroke.hgi-five-g-signal::before { - content: "\f314c"; -} - -.hgi-stroke.hgi-five-square::before { - content: "\f1ce1"; -} - -.hgi-stroke.hgi-fiverr::before { - content: "\f1ce2"; -} - -.hgi-stroke.hgi-flag-01::before { - content: "\f1ce3"; -} - -.hgi-stroke.hgi-flag-02::before { - content: "\f1ce4"; -} - -.hgi-stroke.hgi-flag-03::before { - content: "\f1ce5"; -} - -.hgi-stroke.hgi-flag-off::before { - content: "\f314d"; -} - -.hgi-stroke.hgi-flag-triangle-left::before { - content: "\f314e"; -} - -.hgi-stroke.hgi-flag-triangle-right::before { - content: "\f314f"; -} - -.hgi-stroke.hgi-flame-kindling::before { - content: "\f1ce6"; -} - -.hgi-stroke.hgi-flame::before { - content: "\f3150"; -} - -.hgi-stroke.hgi-flash-off::before { - content: "\f1ce7"; -} - -.hgi-stroke.hgi-flash::before { - content: "\f1ce8"; -} - -.hgi-stroke.hgi-flashlight-off::before { - content: "\f3151"; -} - -.hgi-stroke.hgi-flashlight::before { - content: "\f1ce9"; -} - -.hgi-stroke.hgi-flask-conical-off::before { - content: "\f3153"; -} - -.hgi-stroke.hgi-flask-conical::before { - content: "\f3152"; -} - -.hgi-stroke.hgi-flask-round::before { - content: "\f3154"; -} - -.hgi-stroke.hgi-flaticon::before { - content: "\f1cea"; -} - -.hgi-stroke.hgi-flickr::before { - content: "\f1ceb"; -} - -.hgi-stroke.hgi-flim-slate::before { - content: "\f1cec"; -} - -.hgi-stroke.hgi-flip-bottom::before { - content: "\f1ced"; -} - -.hgi-stroke.hgi-flip-horizontal-01::before { - content: "\f3155"; -} - -.hgi-stroke.hgi-flip-horizontal::before { - content: "\f1cee"; -} - -.hgi-stroke.hgi-flip-horizontal2::before { - content: "\f3156"; -} - -.hgi-stroke.hgi-flip-left::before { - content: "\f1cef"; -} - -.hgi-stroke.hgi-flip-phone::before { - content: "\f1cf0"; -} - -.hgi-stroke.hgi-flip-right::before { - content: "\f1cf1"; -} - -.hgi-stroke.hgi-flip-top::before { - content: "\f1cf2"; -} - -.hgi-stroke.hgi-flip-vertical-01::before { - content: "\f3157"; -} - -.hgi-stroke.hgi-flip-vertical::before { - content: "\f1cf3"; -} - -.hgi-stroke.hgi-flip-vertical2::before { - content: "\f3158"; -} - -.hgi-stroke.hgi-floor-plan::before { - content: "\f1cf4"; -} - -.hgi-stroke.hgi-floppy-disk::before { - content: "\f1cf5"; -} - -.hgi-stroke.hgi-flow-circle::before { - content: "\f1cf6"; -} - -.hgi-stroke.hgi-flow-connection::before { - content: "\f1cf7"; -} - -.hgi-stroke.hgi-flow-square::before { - content: "\f1cf8"; -} - -.hgi-stroke.hgi-flow::before { - content: "\f1cf9"; -} - -.hgi-stroke.hgi-flowchart-01::before { - content: "\f1cfa"; -} - -.hgi-stroke.hgi-flowchart-02::before { - content: "\f1cfb"; -} - -.hgi-stroke.hgi-flower-01::before { - content: "\f3159"; -} - -.hgi-stroke.hgi-flower-pot::before { - content: "\f1cfc"; -} - -.hgi-stroke.hgi-flower::before { - content: "\f1cfd"; -} - -.hgi-stroke.hgi-flower2::before { - content: "\f315a"; -} - -.hgi-stroke.hgi-flushed::before { - content: "\f1cfe"; -} - -.hgi-stroke.hgi-flying-human::before { - content: "\f1cff"; -} - -.hgi-stroke.hgi-focus-point::before { - content: "\f1d00"; -} - -.hgi-stroke.hgi-focus::before { - content: "\f315b"; -} - -.hgi-stroke.hgi-fold-horizontal::before { - content: "\f315c"; -} - -.hgi-stroke.hgi-fold-vertical::before { - content: "\f315d"; -} - -.hgi-stroke.hgi-folder-01::before { - content: "\f1d01"; -} - -.hgi-stroke.hgi-folder-02::before { - content: "\f1d02"; -} - -.hgi-stroke.hgi-folder-03::before { - content: "\f1d03"; -} - -.hgi-stroke.hgi-folder-add::before { - content: "\f1d04"; -} - -.hgi-stroke.hgi-folder-archive::before { - content: "\f315e"; -} - -.hgi-stroke.hgi-folder-attachment::before { - content: "\f1d05"; -} - -.hgi-stroke.hgi-folder-audio::before { - content: "\f1d06"; -} - -.hgi-stroke.hgi-folder-block::before { - content: "\f1d07"; -} - -.hgi-stroke.hgi-folder-check::before { - content: "\f1d08"; -} - -.hgi-stroke.hgi-folder-clock::before { - content: "\f1d09"; -} - -.hgi-stroke.hgi-folder-closed::before { - content: "\f315f"; -} - -.hgi-stroke.hgi-folder-cloud::before { - content: "\f1d0a"; -} - -.hgi-stroke.hgi-folder-code::before { - content: "\f3160"; -} - -.hgi-stroke.hgi-folder-cog::before { - content: "\f3161"; -} - -.hgi-stroke.hgi-folder-details-reference::before { - content: "\f1d0b"; -} - -.hgi-stroke.hgi-folder-details::before { - content: "\f1d0c"; -} - -.hgi-stroke.hgi-folder-down::before { - content: "\f3162"; -} - -.hgi-stroke.hgi-folder-download::before { - content: "\f1d0d"; -} - -.hgi-stroke.hgi-folder-edit::before { - content: "\f1d0e"; -} - -.hgi-stroke.hgi-folder-export::before { - content: "\f1d0f"; -} - -.hgi-stroke.hgi-folder-favourite::before { - content: "\f1d10"; -} - -.hgi-stroke.hgi-folder-file-storage::before { - content: "\f1d11"; -} - -.hgi-stroke.hgi-folder-git-2::before { - content: "\f3164"; -} - -.hgi-stroke.hgi-folder-git::before { - content: "\f3163"; -} - -.hgi-stroke.hgi-folder-git2::before { - content: "\f3165"; -} - -.hgi-stroke.hgi-folder-heart::before { - content: "\f3166"; -} - -.hgi-stroke.hgi-folder-import::before { - content: "\f1d12"; -} - -.hgi-stroke.hgi-folder-input::before { - content: "\f3167"; -} - -.hgi-stroke.hgi-folder-kanban::before { - content: "\f1d13"; -} - -.hgi-stroke.hgi-folder-key::before { - content: "\f3168"; -} - -.hgi-stroke.hgi-folder-library::before { - content: "\f1d14"; -} - -.hgi-stroke.hgi-folder-links::before { - content: "\f1d15"; -} - -.hgi-stroke.hgi-folder-lock::before { - content: "\f3169"; -} - -.hgi-stroke.hgi-folder-locked::before { - content: "\f1d16"; -} - -.hgi-stroke.hgi-folder-management::before { - content: "\f1d17"; -} - -.hgi-stroke.hgi-folder-minus::before { - content: "\f1d18"; -} - -.hgi-stroke.hgi-folder-music::before { - content: "\f1d19"; -} - -.hgi-stroke.hgi-folder-off::before { - content: "\f1d1a"; -} - -.hgi-stroke.hgi-folder-open-dot::before { - content: "\f316a"; -} - -.hgi-stroke.hgi-folder-open::before { - content: "\f1d1b"; -} - -.hgi-stroke.hgi-folder-output::before { - content: "\f316b"; -} - -.hgi-stroke.hgi-folder-pen::before { - content: "\f316c"; -} - -.hgi-stroke.hgi-folder-pin::before { - content: "\f1d1c"; -} - -.hgi-stroke.hgi-folder-plus::before { - content: "\f316d"; -} - -.hgi-stroke.hgi-folder-remove::before { - content: "\f1d1d"; -} - -.hgi-stroke.hgi-folder-root::before { - content: "\f316e"; -} - -.hgi-stroke.hgi-folder-search-2::before { - content: "\f316f"; -} - -.hgi-stroke.hgi-folder-search::before { - content: "\f1d1e"; -} - -.hgi-stroke.hgi-folder-search2::before { - content: "\f3170"; -} - -.hgi-stroke.hgi-folder-security::before { - content: "\f1d1f"; -} - -.hgi-stroke.hgi-folder-shared-01::before { - content: "\f1d20"; -} - -.hgi-stroke.hgi-folder-shared-02::before { - content: "\f1d21"; -} - -.hgi-stroke.hgi-folder-shared-03::before { - content: "\f1d22"; -} - -.hgi-stroke.hgi-folder-symlink::before { - content: "\f3171"; -} - -.hgi-stroke.hgi-folder-sync::before { - content: "\f1d23"; -} - -.hgi-stroke.hgi-folder-transfer::before { - content: "\f1d24"; -} - -.hgi-stroke.hgi-folder-tree::before { - content: "\f3172"; -} - -.hgi-stroke.hgi-folder-unknown::before { - content: "\f1d25"; -} - -.hgi-stroke.hgi-folder-unlocked::before { - content: "\f1d26"; -} - -.hgi-stroke.hgi-folder-up::before { - content: "\f3173"; -} - -.hgi-stroke.hgi-folder-upload::before { - content: "\f1d27"; -} - -.hgi-stroke.hgi-folder-video::before { - content: "\f1d28"; -} - -.hgi-stroke.hgi-folder-view::before { - content: "\f1d29"; -} - -.hgi-stroke.hgi-folder-x::before { - content: "\f3174"; -} - -.hgi-stroke.hgi-folder-zip::before { - content: "\f1d2a"; -} - -.hgi-stroke.hgi-folders::before { - content: "\f1d2b"; -} - -.hgi-stroke.hgi-football-pitch::before { - content: "\f1d2c"; -} - -.hgi-stroke.hgi-football::before { - content: "\f1d2d"; -} - -.hgi-stroke.hgi-footprints::before { - content: "\f3175"; -} - -.hgi-stroke.hgi-forgot-password::before { - content: "\f1d2e"; -} - -.hgi-stroke.hgi-fork::before { - content: "\f1d2f"; -} - -.hgi-stroke.hgi-forklift::before { - content: "\f3176"; -} - -.hgi-stroke.hgi-form::before { - content: "\f3177"; -} - -.hgi-stroke.hgi-forrst::before { - content: "\f1d30"; -} - -.hgi-stroke.hgi-fortress::before { - content: "\f1d31"; -} - -.hgi-stroke.hgi-forward-01::before { - content: "\f1d32"; -} - -.hgi-stroke.hgi-forward-02::before { - content: "\f1d33"; -} - -.hgi-stroke.hgi-four-circle::before { - content: "\f1d34"; -} - -.hgi-stroke.hgi-four-finger-02::before { - content: "\f1d35"; -} - -.hgi-stroke.hgi-four-finger-03::before { - content: "\f1d36"; -} - -.hgi-stroke.hgi-four-g-signal::before { - content: "\f3178"; -} - -.hgi-stroke.hgi-four-k::before { - content: "\f1d37"; -} - -.hgi-stroke.hgi-four-square::before { - content: "\f1d38"; -} - -.hgi-stroke.hgi-foursquare::before { - content: "\f1d39"; -} - -.hgi-stroke.hgi-frame::before { - content: "\f3179"; -} - -.hgi-stroke.hgi-framer::before { - content: "\f1d3a"; -} - -.hgi-stroke.hgi-frameworks::before { - content: "\f1d3b"; -} - -.hgi-stroke.hgi-french-fries-01::before { - content: "\f1d3c"; -} - -.hgi-stroke.hgi-french-fries-02::before { - content: "\f1d3d"; -} - -.hgi-stroke.hgi-fridge::before { - content: "\f1d3e"; -} - -.hgi-stroke.hgi-frisbee::before { - content: "\f317a"; -} - -.hgi-stroke.hgi-frown::before { - content: "\f317b"; -} - -.hgi-stroke.hgi-fry::before { - content: "\f1d3f"; -} - -.hgi-stroke.hgi-fuel-01::before { - content: "\f1d40"; -} - -.hgi-stroke.hgi-fuel-02::before { - content: "\f1d41"; -} - -.hgi-stroke.hgi-fuel-station::before { - content: "\f1d42"; -} - -.hgi-stroke.hgi-fuel::before { - content: "\f317c"; -} - -.hgi-stroke.hgi-full-screen::before { - content: "\f1d43"; -} - -.hgi-stroke.hgi-full-signal::before { - content: "\f1d44"; -} - -.hgi-stroke.hgi-fullscreen::before { - content: "\f1d45"; -} - -.hgi-stroke.hgi-function-circle::before { - content: "\f1d46"; -} - -.hgi-stroke.hgi-function-of-x::before { - content: "\f1d47"; -} - -.hgi-stroke.hgi-function-square::before { - content: "\f1d48"; -} - -.hgi-stroke.hgi-function::before { - content: "\f1d49"; -} - -.hgi-stroke.hgi-funnel-plus::before { - content: "\f317e"; -} - -.hgi-stroke.hgi-funnel-x::before { - content: "\f317f"; -} - -.hgi-stroke.hgi-funnel::before { - content: "\f317d"; -} - -.hgi-stroke.hgi-galaxy-2::before { - content: "\f3180"; -} - -.hgi-stroke.hgi-galaxy::before { - content: "\f1d52"; -} - -.hgi-stroke.hgi-gallery-horizontal-end::before { - content: "\f1d53"; -} - -.hgi-stroke.hgi-gallery-horizontal::before { - content: "\f3181"; -} - -.hgi-stroke.hgi-gallery-thumbnails::before { - content: "\f3182"; -} - -.hgi-stroke.hgi-gallery-vertical-end::before { - content: "\f3184"; -} - -.hgi-stroke.hgi-gallery-vertical::before { - content: "\f3183"; -} - -.hgi-stroke.hgi-game-controller-01::before { - content: "\f1d54"; -} - -.hgi-stroke.hgi-game-controller-02::before { - content: "\f1d55"; -} - -.hgi-stroke.hgi-game-controller-03::before { - content: "\f1d56"; -} - -.hgi-stroke.hgi-game::before { - content: "\f1d57"; -} - -.hgi-stroke.hgi-gameboy::before { - content: "\f1d58"; -} - -.hgi-stroke.hgi-gamepad-01::before { - content: "\f3186"; -} - -.hgi-stroke.hgi-gamepad-directional::before { - content: "\f1d59"; -} - -.hgi-stroke.hgi-gamepad::before { - content: "\f3185"; -} - -.hgi-stroke.hgi-gamepad2::before { - content: "\f3187"; -} - -.hgi-stroke.hgi-garage::before { - content: "\f1d5a"; -} - -.hgi-stroke.hgi-garbage-truck::before { - content: "\f1d5b"; -} - -.hgi-stroke.hgi-garlands::before { - content: "\f1d5c"; -} - -.hgi-stroke.hgi-gas-pipe::before { - content: "\f1d5d"; -} - -.hgi-stroke.hgi-gas-stove::before { - content: "\f1d5e"; -} - -.hgi-stroke.hgi-gauge::before { - content: "\f3188"; -} - -.hgi-stroke.hgi-gavel::before { - content: "\f3189"; -} - -.hgi-stroke.hgi-gears::before { - content: "\f1d5f"; -} - -.hgi-stroke.hgi-gem::before { - content: "\f1d60"; -} - -.hgi-stroke.hgi-geology-crust::before { - content: "\f1d61"; -} - -.hgi-stroke.hgi-geometric-shapes-01::before { - content: "\f1d62"; -} - -.hgi-stroke.hgi-geometric-shapes-02::before { - content: "\f1d63"; -} - -.hgi-stroke.hgi-georgian-lari::before { - content: "\f1d64"; -} - -.hgi-stroke.hgi-ghost::before { - content: "\f318a"; -} - -.hgi-stroke.hgi-gibbous-moon::before { - content: "\f1d65"; -} - -.hgi-stroke.hgi-gif-01::before { - content: "\f1d66"; -} - -.hgi-stroke.hgi-gif-02::before { - content: "\f1d67"; -} - -.hgi-stroke.hgi-gift-card-02::before { - content: "\f1d68"; -} - -.hgi-stroke.hgi-gift-card::before { - content: "\f1d69"; -} - -.hgi-stroke.hgi-gift::before { - content: "\f1d6a"; -} - -.hgi-stroke.hgi-git-branch-minus::before { - content: "\f318b"; -} - -.hgi-stroke.hgi-git-branch-plus::before { - content: "\f318c"; -} - -.hgi-stroke.hgi-git-branch::before { - content: "\f1d6b"; -} - -.hgi-stroke.hgi-git-commit-horizontal::before { - content: "\f1d6c"; -} - -.hgi-stroke.hgi-git-commit-vertical::before { - content: "\f318d"; -} - -.hgi-stroke.hgi-git-commit::before { - content: "\f1d6d"; -} - -.hgi-stroke.hgi-git-compare-arrows::before { - content: "\f318e"; -} - -.hgi-stroke.hgi-git-compare::before { - content: "\f1d6e"; -} - -.hgi-stroke.hgi-git-fork::before { - content: "\f1d6f"; -} - -.hgi-stroke.hgi-git-graph::before { - content: "\f318f"; -} - -.hgi-stroke.hgi-git-merge-conflict::before { - content: "\f3190"; -} - -.hgi-stroke.hgi-git-merge::before { - content: "\f1d70"; -} - -.hgi-stroke.hgi-git-pull-request-arrow::before { - content: "\f3191"; -} - -.hgi-stroke.hgi-git-pull-request-closed::before { - content: "\f1d71"; -} - -.hgi-stroke.hgi-git-pull-request-create-arrow::before { - content: "\f3193"; -} - -.hgi-stroke.hgi-git-pull-request-create::before { - content: "\f3192"; -} - -.hgi-stroke.hgi-git-pull-request-draft-2::before { - content: "\f3194"; -} - -.hgi-stroke.hgi-git-pull-request-draft::before { - content: "\f1d72"; -} - -.hgi-stroke.hgi-git-pull-request::before { - content: "\f1d73"; -} - -.hgi-stroke.hgi-gitbook::before { - content: "\f1d74"; -} - -.hgi-stroke.hgi-github-01::before { - content: "\f1d75"; -} - -.hgi-stroke.hgi-github::before { - content: "\f1d76"; -} - -.hgi-stroke.hgi-gitlab::before { - content: "\f1d77"; -} - -.hgi-stroke.hgi-give-blood::before { - content: "\f1d78"; -} - -.hgi-stroke.hgi-give-pill::before { - content: "\f1d79"; -} - -.hgi-stroke.hgi-glass-water::before { - content: "\f3195"; -} - -.hgi-stroke.hgi-glasses::before { - content: "\f1d7a"; -} - -.hgi-stroke.hgi-global-editing::before { - content: "\f1d7b"; -} - -.hgi-stroke.hgi-global-education::before { - content: "\f1d7c"; -} - -.hgi-stroke.hgi-global-refresh::before { - content: "\f1d7d"; -} - -.hgi-stroke.hgi-global-search::before { - content: "\f1d7e"; -} - -.hgi-stroke.hgi-global::before { - content: "\f1d7f"; -} - -.hgi-stroke.hgi-globe-02::before { - content: "\f1d80"; -} - -.hgi-stroke.hgi-globe-lock::before { - content: "\f3196"; -} - -.hgi-stroke.hgi-globe-off::before { - content: "\f3197"; -} - -.hgi-stroke.hgi-globe-x::before { - content: "\f1d81"; -} - -.hgi-stroke.hgi-globe::before { - content: "\f1d82"; -} - -.hgi-stroke.hgi-glove::before { - content: "\f1d83"; -} - -.hgi-stroke.hgi-gnome::before { - content: "\f1d84"; -} - -.hgi-stroke.hgi-go-backward-10-sec::before { - content: "\f1d85"; -} - -.hgi-stroke.hgi-go-backward-15-sec::before { - content: "\f1d86"; -} - -.hgi-stroke.hgi-go-backward-30-sec::before { - content: "\f1d87"; -} - -.hgi-stroke.hgi-go-backward-60-sec::before { - content: "\f1d88"; -} - -.hgi-stroke.hgi-go-backward-five-sec::before { - content: "\f1d89"; -} - -.hgi-stroke.hgi-go-forward-10-sec::before { - content: "\f1d8a"; -} - -.hgi-stroke.hgi-go-forward-15-sec::before { - content: "\f1d8b"; -} - -.hgi-stroke.hgi-go-forward-30-sec::before { - content: "\f1d8c"; -} - -.hgi-stroke.hgi-go-forward-60-sec::before { - content: "\f1d8d"; -} - -.hgi-stroke.hgi-go-forward-five-sec::before { - content: "\f1d8e"; -} - -.hgi-stroke.hgi-goal::before { - content: "\f3198"; -} - -.hgi-stroke.hgi-gold-buy::before { - content: "\f1d8f"; -} - -.hgi-stroke.hgi-gold-ingots::before { - content: "\f1d90"; -} - -.hgi-stroke.hgi-gold-sell::before { - content: "\f1d91"; -} - -.hgi-stroke.hgi-gold::before { - content: "\f1d92"; -} - -.hgi-stroke.hgi-golf-ball::before { - content: "\f1d93"; -} - -.hgi-stroke.hgi-golf-bat::before { - content: "\f1d94"; -} - -.hgi-stroke.hgi-golf-cart::before { - content: "\f1d95"; -} - -.hgi-stroke.hgi-golf-hole::before { - content: "\f1d96"; -} - -.hgi-stroke.hgi-google-doc::before { - content: "\f1d97"; -} - -.hgi-stroke.hgi-google-drive::before { - content: "\f1d98"; -} - -.hgi-stroke.hgi-google-gemini::before { - content: "\f1d99"; -} - -.hgi-stroke.hgi-google-home::before { - content: "\f1d9a"; -} - -.hgi-stroke.hgi-google-lens::before { - content: "\f1d9b"; -} - -.hgi-stroke.hgi-google-maps::before { - content: "\f1d9c"; -} - -.hgi-stroke.hgi-google-photos::before { - content: "\f1d9d"; -} - -.hgi-stroke.hgi-google-sheet::before { - content: "\f1d9e"; -} - -.hgi-stroke.hgi-google::before { - content: "\f1d9f"; -} - -.hgi-stroke.hgi-gps-01::before { - content: "\f1da0"; -} - -.hgi-stroke.hgi-gps-02::before { - content: "\f1da1"; -} - -.hgi-stroke.hgi-gps-disconnected::before { - content: "\f1da2"; -} - -.hgi-stroke.hgi-gps-off-01::before { - content: "\f1da3"; -} - -.hgi-stroke.hgi-gps-off-02::before { - content: "\f1da4"; -} - -.hgi-stroke.hgi-gps-signal-01::before { - content: "\f1da5"; -} - -.hgi-stroke.hgi-gps-signal-02::before { - content: "\f1da6"; -} - -.hgi-stroke.hgi-gpu::before { - content: "\f1da7"; -} - -.hgi-stroke.hgi-graduate-female::before { - content: "\f1da8"; -} - -.hgi-stroke.hgi-graduate-male::before { - content: "\f1da9"; -} - -.hgi-stroke.hgi-graduation-cap::before { - content: "\f3199"; -} - -.hgi-stroke.hgi-graduation-scroll::before { - content: "\f1daa"; -} - -.hgi-stroke.hgi-grape::before { - content: "\f319a"; -} - -.hgi-stroke.hgi-grapes::before { - content: "\f1dab"; -} - -.hgi-stroke.hgi-graphic-card::before { - content: "\f1dac"; -} - -.hgi-stroke.hgi-gravity::before { - content: "\f1dad"; -} - -.hgi-stroke.hgi-greater-than-01::before { - content: "\f319b"; -} - -.hgi-stroke.hgi-greater-than-circle::before { - content: "\f1dae"; -} - -.hgi-stroke.hgi-greater-than-or-equal::before { - content: "\f319c"; -} - -.hgi-stroke.hgi-greater-than-square::before { - content: "\f1daf"; -} - -.hgi-stroke.hgi-greater-than::before { - content: "\f1db0"; -} - -.hgi-stroke.hgi-greek-helmet::before { - content: "\f1db1"; -} - -.hgi-stroke.hgi-green-house::before { - content: "\f1db2"; -} - -.hgi-stroke.hgi-grid-02::before { - content: "\f1db3"; -} - -.hgi-stroke.hgi-grid-off::before { - content: "\f1db4"; -} - -.hgi-stroke.hgi-grid-table::before { - content: "\f1db5"; -} - -.hgi-stroke.hgi-grid-view::before { - content: "\f1db6"; -} - -.hgi-stroke.hgi-grid::before { - content: "\f1db7"; -} - -.hgi-stroke.hgi-grid2x2-check::before { - content: "\f319e"; -} - -.hgi-stroke.hgi-grid2x2-plus::before { - content: "\f319f"; -} - -.hgi-stroke.hgi-grid2x2-x::before { - content: "\f31a0"; -} - -.hgi-stroke.hgi-grid2x2::before { - content: "\f319d"; -} - -.hgi-stroke.hgi-grid3x2::before { - content: "\f31a1"; -} - -.hgi-stroke.hgi-grid3x3::before { - content: "\f31a2"; -} - -.hgi-stroke.hgi-grimacing::before { - content: "\f1db8"; -} - -.hgi-stroke.hgi-grinning::before { - content: "\f1db9"; -} - -.hgi-stroke.hgi-grip-horizontal::before { - content: "\f31a4"; -} - -.hgi-stroke.hgi-grip-vertical::before { - content: "\f31a5"; -} - -.hgi-stroke.hgi-grip::before { - content: "\f31a3"; -} - -.hgi-stroke.hgi-grok-02::before { - content: "\f1dba"; -} - -.hgi-stroke.hgi-grok::before { - content: "\f1dbb"; -} - -.hgi-stroke.hgi-group-01::before { - content: "\f1dbc"; -} - -.hgi-stroke.hgi-group-items::before { - content: "\f1dbd"; -} - -.hgi-stroke.hgi-group-layers::before { - content: "\f1dbe"; -} - -.hgi-stroke.hgi-group::before { - content: "\f1dbf"; -} - -.hgi-stroke.hgi-groww::before { - content: "\f31a6"; -} - -.hgi-stroke.hgi-guest-house::before { - content: "\f1dc0"; -} - -.hgi-stroke.hgi-guitar::before { - content: "\f31a7"; -} - -.hgi-stroke.hgi-gun::before { - content: "\f1dc1"; -} - -.hgi-stroke.hgi-gymnastic-rings::before { - content: "\f1dc2"; -} - -.hgi-stroke.hgi-gymnastic::before { - content: "\f1dc3"; -} - -.hgi-stroke.hgi-hackerrank::before { - content: "\f1dd0"; -} - -.hgi-stroke.hgi-hair-clips::before { - content: "\f1dd1"; -} - -.hgi-stroke.hgi-hair-dryer::before { - content: "\f1dd2"; -} - -.hgi-stroke.hgi-haji::before { - content: "\f1dd3"; -} - -.hgi-stroke.hgi-halal-lab::before { - content: "\f1dd4"; -} - -.hgi-stroke.hgi-halal::before { - content: "\f1dd5"; -} - -.hgi-stroke.hgi-ham::before { - content: "\f1dd6"; -} - -.hgi-stroke.hgi-hamburger-01::before { - content: "\f1dd7"; -} - -.hgi-stroke.hgi-hamburger-02::before { - content: "\f1dd8"; -} - -.hgi-stroke.hgi-hammer::before { - content: "\f31a9"; -} - -.hgi-stroke.hgi-hand-bag-01::before { - content: "\f1dd9"; -} - -.hgi-stroke.hgi-hand-bag-02::before { - content: "\f1dda"; -} - -.hgi-stroke.hgi-hand-beater::before { - content: "\f1ddb"; -} - -.hgi-stroke.hgi-hand-coins::before { - content: "\f1ddc"; -} - -.hgi-stroke.hgi-hand-fist::before { - content: "\f31ab"; -} - -.hgi-stroke.hgi-hand-grab::before { - content: "\f1ddd"; -} - -.hgi-stroke.hgi-hand-grip::before { - content: "\f1dde"; -} - -.hgi-stroke.hgi-hand-heart::before { - content: "\f31ac"; -} - -.hgi-stroke.hgi-hand-helping::before { - content: "\f1ddf"; -} - -.hgi-stroke.hgi-hand-metal::before { - content: "\f31ad"; -} - -.hgi-stroke.hgi-hand-platter::before { - content: "\f31ae"; -} - -.hgi-stroke.hgi-hand-pointing-down-01::before { - content: "\f1de0"; -} - -.hgi-stroke.hgi-hand-pointing-down-02::before { - content: "\f1de1"; -} - -.hgi-stroke.hgi-hand-pointing-down-03::before { - content: "\f1de2"; -} - -.hgi-stroke.hgi-hand-pointing-down-04::before { - content: "\f1de3"; -} - -.hgi-stroke.hgi-hand-pointing-left-01::before { - content: "\f1de4"; -} - -.hgi-stroke.hgi-hand-pointing-left-02::before { - content: "\f1de5"; -} - -.hgi-stroke.hgi-hand-pointing-left-03::before { - content: "\f1de6"; -} - -.hgi-stroke.hgi-hand-pointing-left-04::before { - content: "\f1de7"; -} - -.hgi-stroke.hgi-hand-pointing-right-01::before { - content: "\f1de8"; -} - -.hgi-stroke.hgi-hand-pointing-right-02::before { - content: "\f1de9"; -} - -.hgi-stroke.hgi-hand-pointing-right-03::before { - content: "\f1dea"; -} - -.hgi-stroke.hgi-hand-pointing-right-04::before { - content: "\f1deb"; -} - -.hgi-stroke.hgi-hand-prayer::before { - content: "\f1dec"; -} - -.hgi-stroke.hgi-hand-sanitizer::before { - content: "\f1ded"; -} - -.hgi-stroke.hgi-hand::before { - content: "\f31aa"; -} - -.hgi-stroke.hgi-handbag::before { - content: "\f31af"; -} - -.hgi-stroke.hgi-handcuffs::before { - content: "\f1dee"; -} - -.hgi-stroke.hgi-hands-clapping::before { - content: "\f31b0"; -} - -.hgi-stroke.hgi-handshake::before { - content: "\f31b1"; -} - -.hgi-stroke.hgi-hanger::before { - content: "\f1def"; -} - -.hgi-stroke.hgi-hanging-clock::before { - content: "\f1df0"; -} - -.hgi-stroke.hgi-hangout::before { - content: "\f1df1"; -} - -.hgi-stroke.hgi-happy-01::before { - content: "\f1df2"; -} - -.hgi-stroke.hgi-happy::before { - content: "\f1df3"; -} - -.hgi-stroke.hgi-hard-drive-download::before { - content: "\f1df4"; -} - -.hgi-stroke.hgi-hard-drive-upload::before { - content: "\f1df5"; -} - -.hgi-stroke.hgi-hard-drive::before { - content: "\f1df6"; -} - -.hgi-stroke.hgi-hard-hat::before { - content: "\f31b2"; -} - -.hgi-stroke.hgi-hash::before { - content: "\f31b3"; -} - -.hgi-stroke.hgi-hashtag::before { - content: "\f1df7"; -} - -.hgi-stroke.hgi-hat-glasses::before { - content: "\f31b4"; -} - -.hgi-stroke.hgi-hat::before { - content: "\f1df8"; -} - -.hgi-stroke.hgi-haze::before { - content: "\f31b5"; -} - -.hgi-stroke.hgi-hd::before { - content: "\f31b6"; -} - -.hgi-stroke.hgi-hdd::before { - content: "\f1df9"; -} - -.hgi-stroke.hgi-hdmi-port::before { - content: "\f31b7"; -} - -.hgi-stroke.hgi-hdr-01::before { - content: "\f1dfa"; -} - -.hgi-stroke.hgi-hdr-02::before { - content: "\f1dfb"; -} - -.hgi-stroke.hgi-heading-01::before { - content: "\f1dfc"; -} - -.hgi-stroke.hgi-heading-02::before { - content: "\f1dfd"; -} - -.hgi-stroke.hgi-heading-03::before { - content: "\f1dfe"; -} - -.hgi-stroke.hgi-heading-04::before { - content: "\f1dff"; -} - -.hgi-stroke.hgi-heading-05::before { - content: "\f1e00"; -} - -.hgi-stroke.hgi-heading-06::before { - content: "\f1e01"; -} - -.hgi-stroke.hgi-heading::before { - content: "\f1e02"; -} - -.hgi-stroke.hgi-heading1::before { - content: "\f31b8"; -} - -.hgi-stroke.hgi-heading2::before { - content: "\f31b9"; -} - -.hgi-stroke.hgi-heading3::before { - content: "\f31ba"; -} - -.hgi-stroke.hgi-heading4::before { - content: "\f31bb"; -} - -.hgi-stroke.hgi-heading5::before { - content: "\f31bc"; -} - -.hgi-stroke.hgi-heading6::before { - content: "\f31bd"; -} - -.hgi-stroke.hgi-headphone-mute::before { - content: "\f1e03"; -} - -.hgi-stroke.hgi-headphone-off::before { - content: "\f31be"; -} - -.hgi-stroke.hgi-headphones::before { - content: "\f1e04"; -} - -.hgi-stroke.hgi-headset-connected::before { - content: "\f1e05"; -} - -.hgi-stroke.hgi-headset-off::before { - content: "\f1e06"; -} - -.hgi-stroke.hgi-headset::before { - content: "\f1e07"; -} - -.hgi-stroke.hgi-healtcare::before { - content: "\f1e08"; -} - -.hgi-stroke.hgi-health::before { - content: "\f1e09"; -} - -.hgi-stroke.hgi-heart-add::before { - content: "\f1e0a"; -} - -.hgi-stroke.hgi-heart-check::before { - content: "\f1e0b"; -} - -.hgi-stroke.hgi-heart-crack::before { - content: "\f31c0"; -} - -.hgi-stroke.hgi-heart-handshake::before { - content: "\f31c1"; -} - -.hgi-stroke.hgi-heart-minus::before { - content: "\f1e0c"; -} - -.hgi-stroke.hgi-heart-off::before { - content: "\f31c2"; -} - -.hgi-stroke.hgi-heart-plus::before { - content: "\f31c3"; -} - -.hgi-stroke.hgi-heart-pulse::before { - content: "\f31c4"; -} - -.hgi-stroke.hgi-heart-remove::before { - content: "\f1e0d"; -} - -.hgi-stroke.hgi-heart::before { - content: "\f31bf"; -} - -.hgi-stroke.hgi-heartbreak::before { - content: "\f1e0e"; -} - -.hgi-stroke.hgi-heater::before { - content: "\f1e0f"; -} - -.hgi-stroke.hgi-helicopter::before { - content: "\f1e10"; -} - -.hgi-stroke.hgi-help-circle::before { - content: "\f1e11"; -} - -.hgi-stroke.hgi-help-square::before { - content: "\f1e12"; -} - -.hgi-stroke.hgi-hexagon-01::before { - content: "\f1e13"; -} - -.hgi-stroke.hgi-hexagon::before { - content: "\f1e14"; -} - -.hgi-stroke.hgi-hierarchy-circle-01::before { - content: "\f1e15"; -} - -.hgi-stroke.hgi-hierarchy-circle-02::before { - content: "\f1e16"; -} - -.hgi-stroke.hgi-hierarchy-circle-03::before { - content: "\f1e17"; -} - -.hgi-stroke.hgi-hierarchy-files::before { - content: "\f1e18"; -} - -.hgi-stroke.hgi-hierarchy-square-01::before { - content: "\f1e19"; -} - -.hgi-stroke.hgi-hierarchy-square-02::before { - content: "\f1e1a"; -} - -.hgi-stroke.hgi-hierarchy-square-03::before { - content: "\f1e1b"; -} - -.hgi-stroke.hgi-hierarchy-square-04::before { - content: "\f1e1c"; -} - -.hgi-stroke.hgi-hierarchy-square-05::before { - content: "\f1e1d"; -} - -.hgi-stroke.hgi-hierarchy-square-06::before { - content: "\f1e1e"; -} - -.hgi-stroke.hgi-hierarchy-square-07::before { - content: "\f1e1f"; -} - -.hgi-stroke.hgi-hierarchy-square-08::before { - content: "\f1e20"; -} - -.hgi-stroke.hgi-hierarchy-square-10::before { - content: "\f1e21"; -} - -.hgi-stroke.hgi-hierarchy::before { - content: "\f1e22"; -} - -.hgi-stroke.hgi-high-heels-01::before { - content: "\f1e23"; -} - -.hgi-stroke.hgi-high-heels-02::before { - content: "\f1e24"; -} - -.hgi-stroke.hgi-highlighter::before { - content: "\f1e25"; -} - -.hgi-stroke.hgi-hijab::before { - content: "\f1e26"; -} - -.hgi-stroke.hgi-hiking::before { - content: "\f31c5"; -} - -.hgi-stroke.hgi-history::before { - content: "\f31c6"; -} - -.hgi-stroke.hgi-hockey::before { - content: "\f1e27"; -} - -.hgi-stroke.hgi-hold-01::before { - content: "\f1e28"; -} - -.hgi-stroke.hgi-hold-02::before { - content: "\f1e29"; -} - -.hgi-stroke.hgi-hold-03::before { - content: "\f1e2a"; -} - -.hgi-stroke.hgi-hold-04::before { - content: "\f1e2b"; -} - -.hgi-stroke.hgi-hold-05::before { - content: "\f1e2c"; -} - -.hgi-stroke.hgi-hold-locked-01::before { - content: "\f1e2d"; -} - -.hgi-stroke.hgi-hold-locked-02::before { - content: "\f1e2e"; -} - -.hgi-stroke.hgi-hold-phone::before { - content: "\f1e2f"; -} - -.hgi-stroke.hgi-hologram::before { - content: "\f1e30"; -} - -.hgi-stroke.hgi-home-01::before { - content: "\f1e31"; -} - -.hgi-stroke.hgi-home-02::before { - content: "\f1e32"; -} - -.hgi-stroke.hgi-home-03::before { - content: "\f1e33"; -} - -.hgi-stroke.hgi-home-04::before { - content: "\f1e34"; -} - -.hgi-stroke.hgi-home-05::before { - content: "\f1e35"; -} - -.hgi-stroke.hgi-home-06::before { - content: "\f1e36"; -} - -.hgi-stroke.hgi-home-07::before { - content: "\f1e37"; -} - -.hgi-stroke.hgi-home-08::before { - content: "\f1e38"; -} - -.hgi-stroke.hgi-home-09::before { - content: "\f1e39"; -} - -.hgi-stroke.hgi-home-10::before { - content: "\f1e3a"; -} - -.hgi-stroke.hgi-home-11::before { - content: "\f1e3b"; -} - -.hgi-stroke.hgi-home-12::before { - content: "\f1e3c"; -} - -.hgi-stroke.hgi-home-13::before { - content: "\f1e3d"; -} - -.hgi-stroke.hgi-home-wifi::before { - content: "\f1e3e"; -} - -.hgi-stroke.hgi-honey-01::before { - content: "\f1e3f"; -} - -.hgi-stroke.hgi-honey-02::before { - content: "\f1e40"; -} - -.hgi-stroke.hgi-honor::before { - content: "\f1e41"; -} - -.hgi-stroke.hgi-honour-star::before { - content: "\f1e42"; -} - -.hgi-stroke.hgi-hoodie::before { - content: "\f1e43"; -} - -.hgi-stroke.hgi-hop-off::before { - content: "\f1e44"; -} - -.hgi-stroke.hgi-hop::before { - content: "\f1e45"; -} - -.hgi-stroke.hgi-horizonal-scroll-point::before { - content: "\f1e46"; -} - -.hgi-stroke.hgi-horizontal-resize::before { - content: "\f1e47"; -} - -.hgi-stroke.hgi-horse-head::before { - content: "\f1e48"; -} - -.hgi-stroke.hgi-horse-saddle::before { - content: "\f1e49"; -} - -.hgi-stroke.hgi-horse::before { - content: "\f1e4a"; -} - -.hgi-stroke.hgi-hospital-01::before { - content: "\f1e4b"; -} - -.hgi-stroke.hgi-hospital-02::before { - content: "\f1e4c"; -} - -.hgi-stroke.hgi-hospital-bed-01::before { - content: "\f1e4d"; -} - -.hgi-stroke.hgi-hospital-bed-02::before { - content: "\f1e4e"; -} - -.hgi-stroke.hgi-hospital-location::before { - content: "\f1e4f"; -} - -.hgi-stroke.hgi-hot-air-balloon::before { - content: "\f1e50"; -} - -.hgi-stroke.hgi-hot-price::before { - content: "\f1e51"; -} - -.hgi-stroke.hgi-hot-tube::before { - content: "\f1e52"; -} - -.hgi-stroke.hgi-hotdog::before { - content: "\f1e53"; -} - -.hgi-stroke.hgi-hotel-01::before { - content: "\f1e54"; -} - -.hgi-stroke.hgi-hotel-02::before { - content: "\f1e55"; -} - -.hgi-stroke.hgi-hotel-bell::before { - content: "\f1e56"; -} - -.hgi-stroke.hgi-hotspot-offline::before { - content: "\f1e57"; -} - -.hgi-stroke.hgi-hotspot::before { - content: "\f1e58"; -} - -.hgi-stroke.hgi-hourglass-off::before { - content: "\f1e59"; -} - -.hgi-stroke.hgi-hourglass::before { - content: "\f1e5a"; -} - -.hgi-stroke.hgi-house-01::before { - content: "\f1e5b"; -} - -.hgi-stroke.hgi-house-02::before { - content: "\f1e5c"; -} - -.hgi-stroke.hgi-house-03::before { - content: "\f1e5d"; -} - -.hgi-stroke.hgi-house-04::before { - content: "\f1e5e"; -} - -.hgi-stroke.hgi-house-05::before { - content: "\f1e5f"; -} - -.hgi-stroke.hgi-house-heart::before { - content: "\f1e60"; -} - -.hgi-stroke.hgi-house-plug::before { - content: "\f31c7"; -} - -.hgi-stroke.hgi-house-plus::before { - content: "\f1e61"; -} - -.hgi-stroke.hgi-house-solar-panel::before { - content: "\f1e62"; -} - -.hgi-stroke.hgi-house-wifi::before { - content: "\f1e63"; -} - -.hgi-stroke.hgi-html-5::before { - content: "\f1e64"; -} - -.hgi-stroke.hgi-html-file-01::before { - content: "\f1e65"; -} - -.hgi-stroke.hgi-html-file-02::before { - content: "\f1e66"; -} - -.hgi-stroke.hgi-hugeicons::before { - content: "\f1e67"; -} - -.hgi-stroke.hgi-humidity::before { - content: "\f1e68"; -} - -.hgi-stroke.hgi-hut::before { - content: "\f1e69"; -} - -.hgi-stroke.hgi-hydro-power::before { - content: "\f1e6a"; -} - -.hgi-stroke.hgi-hyperbole::before { - content: "\f1e6b"; -} - -.hgi-stroke.hgi-ice-cream-01::before { - content: "\f1e74"; -} - -.hgi-stroke.hgi-ice-cream-02::before { - content: "\f1e75"; -} - -.hgi-stroke.hgi-ice-cream-03::before { - content: "\f1e76"; -} - -.hgi-stroke.hgi-ice-cream-04::before { - content: "\f1e77"; -} - -.hgi-stroke.hgi-ice-cream-bowl::before { - content: "\f31c8"; -} - -.hgi-stroke.hgi-ice-cream-cone::before { - content: "\f31c9"; -} - -.hgi-stroke.hgi-ice-cubes::before { - content: "\f1e78"; -} - -.hgi-stroke.hgi-ice-hockey::before { - content: "\f1e79"; -} - -.hgi-stroke.hgi-ice-skating::before { - content: "\f1e7a"; -} - -.hgi-stroke.hgi-ico::before { - content: "\f1e7b"; -} - -.hgi-stroke.hgi-iconjar::before { - content: "\f1e7c"; -} - -.hgi-stroke.hgi-id-card-lanyard::before { - content: "\f1e7d"; -} - -.hgi-stroke.hgi-id-card::before { - content: "\f31ca"; -} - -.hgi-stroke.hgi-id-not-verified::before { - content: "\f1e7e"; -} - -.hgi-stroke.hgi-id-verified::before { - content: "\f1e7f"; -} - -.hgi-stroke.hgi-id::before { - content: "\f1e80"; -} - -.hgi-stroke.hgi-idea-01::before { - content: "\f1e81"; -} - -.hgi-stroke.hgi-idea::before { - content: "\f1e82"; -} - -.hgi-stroke.hgi-identification::before { - content: "\f1e83"; -} - -.hgi-stroke.hgi-identity-card-check::before { - content: "\f31cb"; -} - -.hgi-stroke.hgi-identity-card::before { - content: "\f1e84"; -} - -.hgi-stroke.hgi-image-01::before { - content: "\f1e85"; -} - -.hgi-stroke.hgi-image-02::before { - content: "\f1e86"; -} - -.hgi-stroke.hgi-image-03::before { - content: "\f1e87"; -} - -.hgi-stroke.hgi-image-actual-size::before { - content: "\f1e88"; -} - -.hgi-stroke.hgi-image-add-01::before { - content: "\f1e89"; -} - -.hgi-stroke.hgi-image-add-02::before { - content: "\f1e8a"; -} - -.hgi-stroke.hgi-image-composition-oval::before { - content: "\f1e8b"; -} - -.hgi-stroke.hgi-image-composition::before { - content: "\f1e8c"; -} - -.hgi-stroke.hgi-image-counter-clockwise::before { - content: "\f1e8d"; -} - -.hgi-stroke.hgi-image-crop::before { - content: "\f1e8e"; -} - -.hgi-stroke.hgi-image-delete-01::before { - content: "\f1e8f"; -} - -.hgi-stroke.hgi-image-delete-02::before { - content: "\f1e90"; -} - -.hgi-stroke.hgi-image-done-01::before { - content: "\f1e91"; -} - -.hgi-stroke.hgi-image-done-02::before { - content: "\f1e92"; -} - -.hgi-stroke.hgi-image-down::before { - content: "\f31cc"; -} - -.hgi-stroke.hgi-image-download-02::before { - content: "\f1e93"; -} - -.hgi-stroke.hgi-image-download::before { - content: "\f1e94"; -} - -.hgi-stroke.hgi-image-flip-horizontal::before { - content: "\f1e95"; -} - -.hgi-stroke.hgi-image-flip-vertical::before { - content: "\f1e96"; -} - -.hgi-stroke.hgi-image-minus::before { - content: "\f31cd"; -} - -.hgi-stroke.hgi-image-not-found-01::before { - content: "\f1e97"; -} - -.hgi-stroke.hgi-image-not-found-02::before { - content: "\f1e98"; -} - -.hgi-stroke.hgi-image-off::before { - content: "\f31ce"; -} - -.hgi-stroke.hgi-image-play::before { - content: "\f1e99"; -} - -.hgi-stroke.hgi-image-plus::before { - content: "\f31cf"; -} - -.hgi-stroke.hgi-image-remove-01::before { - content: "\f1e9a"; -} - -.hgi-stroke.hgi-image-remove-02::before { - content: "\f1e9b"; -} - -.hgi-stroke.hgi-image-rotation-clockwise::before { - content: "\f1e9c"; -} - -.hgi-stroke.hgi-image-to-video-2::before { - content: "\f31d1"; -} - -.hgi-stroke.hgi-image-to-video::before { - content: "\f31d0"; -} - -.hgi-stroke.hgi-image-up::before { - content: "\f31d2"; -} - -.hgi-stroke.hgi-image-upload-01::before { - content: "\f1e9d"; -} - -.hgi-stroke.hgi-image-upload::before { - content: "\f1e9e"; -} - -.hgi-stroke.hgi-image-upscale::before { - content: "\f31d3"; -} - -.hgi-stroke.hgi-images::before { - content: "\f31d4"; -} - -.hgi-stroke.hgi-imo::before { - content: "\f1e9f"; -} - -.hgi-stroke.hgi-import::before { - content: "\f31d5"; -} - -.hgi-stroke.hgi-important-book::before { - content: "\f1ea0"; -} - -.hgi-stroke.hgi-in-love::before { - content: "\f1ea1"; -} - -.hgi-stroke.hgi-inbox-check::before { - content: "\f1ea2"; -} - -.hgi-stroke.hgi-inbox-download::before { - content: "\f1ea3"; -} - -.hgi-stroke.hgi-inbox-unread::before { - content: "\f1ea4"; -} - -.hgi-stroke.hgi-inbox-upload::before { - content: "\f1ea5"; -} - -.hgi-stroke.hgi-inbox::before { - content: "\f1ea6"; -} - -.hgi-stroke.hgi-incognito::before { - content: "\f1ea7"; -} - -.hgi-stroke.hgi-india-gate::before { - content: "\f1ea8"; -} - -.hgi-stroke.hgi-indian-rupee::before { - content: "\f31d6"; -} - -.hgi-stroke.hgi-inequality-01::before { - content: "\f1ea9"; -} - -.hgi-stroke.hgi-inequality-02::before { - content: "\f1eaa"; -} - -.hgi-stroke.hgi-inequality-circle-01::before { - content: "\f1eab"; -} - -.hgi-stroke.hgi-inequality-circle-02::before { - content: "\f1eac"; -} - -.hgi-stroke.hgi-inequality-square-01::before { - content: "\f1ead"; -} - -.hgi-stroke.hgi-inequality-square-02::before { - content: "\f1eae"; -} - -.hgi-stroke.hgi-infant::before { - content: "\f1eaf"; -} - -.hgi-stroke.hgi-infinity-01::before { - content: "\f1eb0"; -} - -.hgi-stroke.hgi-infinity-02::before { - content: "\f1eb1"; -} - -.hgi-stroke.hgi-infinity-circle::before { - content: "\f1eb2"; -} - -.hgi-stroke.hgi-infinity-square::before { - content: "\f1eb3"; -} - -.hgi-stroke.hgi-info::before { - content: "\f31d7"; -} - -.hgi-stroke.hgi-information-circle::before { - content: "\f1eb4"; -} - -.hgi-stroke.hgi-information-diamond::before { - content: "\f1eb5"; -} - -.hgi-stroke.hgi-information-square::before { - content: "\f1eb6"; -} - -.hgi-stroke.hgi-injection::before { - content: "\f1eb7"; -} - -.hgi-stroke.hgi-input-cursor-text::before { - content: "\f1eb8"; -} - -.hgi-stroke.hgi-input-long-text::before { - content: "\f1eb9"; -} - -.hgi-stroke.hgi-input-numeric::before { - content: "\f1eba"; -} - -.hgi-stroke.hgi-input-short-text::before { - content: "\f1ebb"; -} - -.hgi-stroke.hgi-input-text::before { - content: "\f1ebc"; -} - -.hgi-stroke.hgi-insert-bottom-image::before { - content: "\f1ebd"; -} - -.hgi-stroke.hgi-insert-center-image::before { - content: "\f1ebe"; -} - -.hgi-stroke.hgi-insert-column-left-2::before { - content: "\f31d8"; -} - -.hgi-stroke.hgi-insert-column-left::before { - content: "\f1ebf"; -} - -.hgi-stroke.hgi-insert-column-right::before { - content: "\f31d9"; -} - -.hgi-stroke.hgi-insert-column::before { - content: "\f1ec0"; -} - -.hgi-stroke.hgi-insert-pi::before { - content: "\f1ec1"; -} - -.hgi-stroke.hgi-insert-row-down::before { - content: "\f1ec2"; -} - -.hgi-stroke.hgi-insert-row-up::before { - content: "\f1ec3"; -} - -.hgi-stroke.hgi-insert-row::before { - content: "\f1ec4"; -} - -.hgi-stroke.hgi-insert-top-image::before { - content: "\f1ec5"; -} - -.hgi-stroke.hgi-inspect-code::before { - content: "\f1ec6"; -} - -.hgi-stroke.hgi-inspection-panel::before { - content: "\f31da"; -} - -.hgi-stroke.hgi-instagram::before { - content: "\f1ec7"; -} - -.hgi-stroke.hgi-installing-updates-01::before { - content: "\f1ec8"; -} - -.hgi-stroke.hgi-installing-updates-02::before { - content: "\f1ec9"; -} - -.hgi-stroke.hgi-internet-antenna-01::before { - content: "\f1eca"; -} - -.hgi-stroke.hgi-internet-antenna-02::before { - content: "\f1ecb"; -} - -.hgi-stroke.hgi-internet-antenna-03::before { - content: "\f1ecc"; -} - -.hgi-stroke.hgi-internet-antenna-04::before { - content: "\f1ecd"; -} - -.hgi-stroke.hgi-internet::before { - content: "\f1ece"; -} - -.hgi-stroke.hgi-investigation::before { - content: "\f1ecf"; -} - -.hgi-stroke.hgi-invoice-01::before { - content: "\f1ed0"; -} - -.hgi-stroke.hgi-invoice-02::before { - content: "\f1ed1"; -} - -.hgi-stroke.hgi-invoice-03::before { - content: "\f1ed2"; -} - -.hgi-stroke.hgi-invoice-04::before { - content: "\f1ed3"; -} - -.hgi-stroke.hgi-invoice::before { - content: "\f1ed4"; -} - -.hgi-stroke.hgi-ipod::before { - content: "\f1ed5"; -} - -.hgi-stroke.hgi-iris-scan::before { - content: "\f1ed6"; -} - -.hgi-stroke.hgi-island::before { - content: "\f1ed7"; -} - -.hgi-stroke.hgi-italic::before { - content: "\f31dd"; -} - -.hgi-stroke.hgi-iteration-ccw::before { - content: "\f1ed8"; -} - -.hgi-stroke.hgi-iteration-cw::before { - content: "\f1ed9"; -} - -.hgi-stroke.hgi-japanese-yen::before { - content: "\f31de"; -} - -.hgi-stroke.hgi-jar::before { - content: "\f1eda"; -} - -.hgi-stroke.hgi-java-script::before { - content: "\f1edb"; -} - -.hgi-stroke.hgi-java::before { - content: "\f1edc"; -} - -.hgi-stroke.hgi-job-link::before { - content: "\f1edd"; -} - -.hgi-stroke.hgi-job-search::before { - content: "\f1ede"; -} - -.hgi-stroke.hgi-job-share::before { - content: "\f1edf"; -} - -.hgi-stroke.hgi-jogger-pants::before { - content: "\f1ee0"; -} - -.hgi-stroke.hgi-join-bevel::before { - content: "\f1ee1"; -} - -.hgi-stroke.hgi-join-round::before { - content: "\f1ee2"; -} - -.hgi-stroke.hgi-join-straight::before { - content: "\f1ee3"; -} - -.hgi-stroke.hgi-joker::before { - content: "\f1ee4"; -} - -.hgi-stroke.hgi-joystick-01::before { - content: "\f1ee5"; -} - -.hgi-stroke.hgi-joystick-02::before { - content: "\f1ee6"; -} - -.hgi-stroke.hgi-joystick-03::before { - content: "\f1ee7"; -} - -.hgi-stroke.hgi-joystick-04::before { - content: "\f1ee8"; -} - -.hgi-stroke.hgi-joystick-05::before { - content: "\f1ee9"; -} - -.hgi-stroke.hgi-jpg-01::before { - content: "\f1eea"; -} - -.hgi-stroke.hgi-jpg-02::before { - content: "\f1eeb"; -} - -.hgi-stroke.hgi-jsx-01::before { - content: "\f1eec"; -} - -.hgi-stroke.hgi-jsx-02::before { - content: "\f1eed"; -} - -.hgi-stroke.hgi-jsx-03::before { - content: "\f1eee"; -} - -.hgi-stroke.hgi-judge::before { - content: "\f1eef"; -} - -.hgi-stroke.hgi-jumpers::before { - content: "\f1ef0"; -} - -.hgi-stroke.hgi-jupiter-2::before { - content: "\f31df"; -} - -.hgi-stroke.hgi-jupiter::before { - content: "\f1ef1"; -} - -.hgi-stroke.hgi-justice-scale-01::before { - content: "\f1ef2"; -} - -.hgi-stroke.hgi-justice-scale-02::before { - content: "\f1ef3"; -} - -.hgi-stroke.hgi-kaaba-01::before { - content: "\f1ef6"; -} - -.hgi-stroke.hgi-kaaba-02::before { - content: "\f1ef7"; -} - -.hgi-stroke.hgi-kanban-2::before { - content: "\f31e0"; -} - -.hgi-stroke.hgi-kanban::before { - content: "\f1ef8"; -} - -.hgi-stroke.hgi-kayak::before { - content: "\f1ef9"; -} - -.hgi-stroke.hgi-keffiyeh::before { - content: "\f1efa"; -} - -.hgi-stroke.hgi-kettle-01::before { - content: "\f1efb"; -} - -.hgi-stroke.hgi-kettle::before { - content: "\f1efc"; -} - -.hgi-stroke.hgi-kettlebell::before { - content: "\f1efd"; -} - -.hgi-stroke.hgi-ketupat::before { - content: "\f1efe"; -} - -.hgi-stroke.hgi-key-01::before { - content: "\f1eff"; -} - -.hgi-stroke.hgi-key-02::before { - content: "\f1f00"; -} - -.hgi-stroke.hgi-key-generator-fob::before { - content: "\f1f01"; -} - -.hgi-stroke.hgi-key-round::before { - content: "\f31e1"; -} - -.hgi-stroke.hgi-key-square::before { - content: "\f31e2"; -} - -.hgi-stroke.hgi-keyboard-off::before { - content: "\f31e3"; -} - -.hgi-stroke.hgi-keyboard::before { - content: "\f1f02"; -} - -.hgi-stroke.hgi-keyframe-add::before { - content: "\f1f03"; -} - -.hgi-stroke.hgi-keyframe-align-center::before { - content: "\f1f04"; -} - -.hgi-stroke.hgi-keyframe-align-horizontal::before { - content: "\f1f05"; -} - -.hgi-stroke.hgi-keyframe-align-vertical::before { - content: "\f1f06"; -} - -.hgi-stroke.hgi-keyframe-bottom::before { - content: "\f1f07"; -} - -.hgi-stroke.hgi-keyframe-left::before { - content: "\f1f08"; -} - -.hgi-stroke.hgi-keyframe-remove::before { - content: "\f1f09"; -} - -.hgi-stroke.hgi-keyframe-right::before { - content: "\f1f0a"; -} - -.hgi-stroke.hgi-keyframe-top::before { - content: "\f1f0b"; -} - -.hgi-stroke.hgi-keyframe::before { - content: "\f1f0c"; -} - -.hgi-stroke.hgi-keyframes-double-add::before { - content: "\f1f0d"; -} - -.hgi-stroke.hgi-keyframes-double-remove::before { - content: "\f1f0e"; -} - -.hgi-stroke.hgi-keyframes-double::before { - content: "\f1f0f"; -} - -.hgi-stroke.hgi-keyframes-multiple-add::before { - content: "\f1f10"; -} - -.hgi-stroke.hgi-keyframes-multiple-remove::before { - content: "\f1f11"; -} - -.hgi-stroke.hgi-keyframes-multiple::before { - content: "\f1f12"; -} - -.hgi-stroke.hgi-kickstarter-01::before { - content: "\f1f13"; -} - -.hgi-stroke.hgi-kickstarter-02::before { - content: "\f1f14"; -} - -.hgi-stroke.hgi-kid::before { - content: "\f1f15"; -} - -.hgi-stroke.hgi-kidneys::before { - content: "\f1f16"; -} - -.hgi-stroke.hgi-kimi-ai::before { - content: "\f1f17"; -} - -.hgi-stroke.hgi-kissing::before { - content: "\f1f18"; -} - -.hgi-stroke.hgi-kitchen-utensils::before { - content: "\f1f19"; -} - -.hgi-stroke.hgi-kite::before { - content: "\f1f1a"; -} - -.hgi-stroke.hgi-klarna::before { - content: "\f1f1b"; -} - -.hgi-stroke.hgi-knife-01::before { - content: "\f1f1c"; -} - -.hgi-stroke.hgi-knife-02::before { - content: "\f1f1d"; -} - -.hgi-stroke.hgi-knife-bread::before { - content: "\f1f1e"; -} - -.hgi-stroke.hgi-knight-shield::before { - content: "\f1f1f"; -} - -.hgi-stroke.hgi-knives::before { - content: "\f1f20"; -} - -.hgi-stroke.hgi-knowledge-01::before { - content: "\f1f21"; -} - -.hgi-stroke.hgi-knowledge-02::before { - content: "\f1f22"; -} - -.hgi-stroke.hgi-ko-fi::before { - content: "\f1f23"; -} - -.hgi-stroke.hgi-kurta-01::before { - content: "\f1f24"; -} - -.hgi-stroke.hgi-kurta::before { - content: "\f1f25"; -} - -.hgi-stroke.hgi-label-important::before { - content: "\f1f2d"; -} - -.hgi-stroke.hgi-label::before { - content: "\f1f2e"; -} - -.hgi-stroke.hgi-labor::before { - content: "\f1f2f"; -} - -.hgi-stroke.hgi-labs::before { - content: "\f1f30"; -} - -.hgi-stroke.hgi-lake::before { - content: "\f1f31"; -} - -.hgi-stroke.hgi-lamp-01::before { - content: "\f1f32"; -} - -.hgi-stroke.hgi-lamp-02::before { - content: "\f1f33"; -} - -.hgi-stroke.hgi-lamp-03::before { - content: "\f1f34"; -} - -.hgi-stroke.hgi-lamp-04::before { - content: "\f1f35"; -} - -.hgi-stroke.hgi-lamp-05::before { - content: "\f1f36"; -} - -.hgi-stroke.hgi-lamp-ceiling::before { - content: "\f31e4"; -} - -.hgi-stroke.hgi-lamp-desk::before { - content: "\f31e5"; -} - -.hgi-stroke.hgi-lamp-floor::before { - content: "\f31e6"; -} - -.hgi-stroke.hgi-lamp-wall-down::before { - content: "\f1f37"; -} - -.hgi-stroke.hgi-lamp-wall-up::before { - content: "\f1f38"; -} - -.hgi-stroke.hgi-lamp::before { - content: "\f1f39"; -} - -.hgi-stroke.hgi-land-plot::before { - content: "\f1f3a"; -} - -.hgi-stroke.hgi-landmark::before { - content: "\f1f3b"; -} - -.hgi-stroke.hgi-language-circle::before { - content: "\f1f3c"; -} - -.hgi-stroke.hgi-language-skill::before { - content: "\f1f3d"; -} - -.hgi-stroke.hgi-language-square::before { - content: "\f1f3e"; -} - -.hgi-stroke.hgi-languages::before { - content: "\f31e7"; -} - -.hgi-stroke.hgi-lantern::before { - content: "\f1f3f"; -} - -.hgi-stroke.hgi-laptop-add::before { - content: "\f1f40"; -} - -.hgi-stroke.hgi-laptop-charging::before { - content: "\f1f41"; -} - -.hgi-stroke.hgi-laptop-check::before { - content: "\f1f42"; -} - -.hgi-stroke.hgi-laptop-cloud::before { - content: "\f1f43"; -} - -.hgi-stroke.hgi-laptop-issue::before { - content: "\f1f44"; -} - -.hgi-stroke.hgi-laptop-minimal-check::before { - content: "\f31e9"; -} - -.hgi-stroke.hgi-laptop-minimal::before { - content: "\f31e8"; -} - -.hgi-stroke.hgi-laptop-performance::before { - content: "\f1f45"; -} - -.hgi-stroke.hgi-laptop-phone-sync::before { - content: "\f1f46"; -} - -.hgi-stroke.hgi-laptop-programming::before { - content: "\f1f47"; -} - -.hgi-stroke.hgi-laptop-remove::before { - content: "\f1f48"; -} - -.hgi-stroke.hgi-laptop-settings::before { - content: "\f1f49"; -} - -.hgi-stroke.hgi-laptop-video::before { - content: "\f1f4a"; -} - -.hgi-stroke.hgi-laptop::before { - content: "\f1f4b"; -} - -.hgi-stroke.hgi-lasso-2::before { - content: "\f31eb"; -} - -.hgi-stroke.hgi-lasso-select::before { - content: "\f1f4c"; -} - -.hgi-stroke.hgi-lasso-tool-01::before { - content: "\f1f4d"; -} - -.hgi-stroke.hgi-lasso-tool-02::before { - content: "\f1f4e"; -} - -.hgi-stroke.hgi-lasso::before { - content: "\f31ea"; -} - -.hgi-stroke.hgi-last-fm::before { - content: "\f1f4f"; -} - -.hgi-stroke.hgi-latitude::before { - content: "\f1f50"; -} - -.hgi-stroke.hgi-laugh::before { - content: "\f31ec"; -} - -.hgi-stroke.hgi-laughing::before { - content: "\f1f51"; -} - -.hgi-stroke.hgi-laurel-wreath-01::before { - content: "\f1f52"; -} - -.hgi-stroke.hgi-laurel-wreath-02::before { - content: "\f1f53"; -} - -.hgi-stroke.hgi-laurel-wreath-first-01::before { - content: "\f1f54"; -} - -.hgi-stroke.hgi-laurel-wreath-first-02::before { - content: "\f1f55"; -} - -.hgi-stroke.hgi-laurel-wreath-left-01::before { - content: "\f1f56"; -} - -.hgi-stroke.hgi-laurel-wreath-left-02::before { - content: "\f1f57"; -} - -.hgi-stroke.hgi-laurel-wreath-left-03::before { - content: "\f1f58"; -} - -.hgi-stroke.hgi-laurel-wreath-right-01::before { - content: "\f1f59"; -} - -.hgi-stroke.hgi-laurel-wreath-right-02::before { - content: "\f1f5a"; -} - -.hgi-stroke.hgi-laurel-wreath-right-03::before { - content: "\f1f5b"; -} - -.hgi-stroke.hgi-layer-add::before { - content: "\f1f5c"; -} - -.hgi-stroke.hgi-layer-bring-forward::before { - content: "\f1f5d"; -} - -.hgi-stroke.hgi-layer-bring-to-front::before { - content: "\f1f5e"; -} - -.hgi-stroke.hgi-layer-mask-01::before { - content: "\f1f5f"; -} - -.hgi-stroke.hgi-layer-mask-02::before { - content: "\f1f60"; -} - -.hgi-stroke.hgi-layer-send-backward::before { - content: "\f1f61"; -} - -.hgi-stroke.hgi-layer-send-to-back::before { - content: "\f1f62"; -} - -.hgi-stroke.hgi-layer::before { - content: "\f1f63"; -} - -.hgi-stroke.hgi-layers-01::before { - content: "\f1f64"; -} - -.hgi-stroke.hgi-layers-02::before { - content: "\f1f65"; -} - -.hgi-stroke.hgi-layers-logo::before { - content: "\f1f66"; -} - -.hgi-stroke.hgi-layers-plus::before { - content: "\f31ee"; -} - -.hgi-stroke.hgi-layers::before { - content: "\f31ed"; -} - -.hgi-stroke.hgi-layers2::before { - content: "\f31ef"; -} - -.hgi-stroke.hgi-layout-01::before { - content: "\f1f67"; -} - -.hgi-stroke.hgi-layout-02::before { - content: "\f1f68"; -} - -.hgi-stroke.hgi-layout-03::before { - content: "\f1f69"; -} - -.hgi-stroke.hgi-layout-04::before { - content: "\f1f6a"; -} - -.hgi-stroke.hgi-layout-05::before { - content: "\f1f6b"; -} - -.hgi-stroke.hgi-layout-06::before { - content: "\f1f6c"; -} - -.hgi-stroke.hgi-layout-07::before { - content: "\f1f6d"; -} - -.hgi-stroke.hgi-layout-align-bottom::before { - content: "\f1f6e"; -} - -.hgi-stroke.hgi-layout-align-left::before { - content: "\f1f6f"; -} - -.hgi-stroke.hgi-layout-align-right::before { - content: "\f1f70"; -} - -.hgi-stroke.hgi-layout-align-top::before { - content: "\f1f71"; -} - -.hgi-stroke.hgi-layout-bottom::before { - content: "\f1f72"; -} - -.hgi-stroke.hgi-layout-dashboard::before { - content: "\f31f0"; -} - -.hgi-stroke.hgi-layout-grid::before { - content: "\f1f73"; -} - -.hgi-stroke.hgi-layout-left::before { - content: "\f1f74"; -} - -.hgi-stroke.hgi-layout-list::before { - content: "\f31f1"; -} - -.hgi-stroke.hgi-layout-panel-left::before { - content: "\f31f2"; -} - -.hgi-stroke.hgi-layout-panel-top::before { - content: "\f31f3"; -} - -.hgi-stroke.hgi-layout-right::before { - content: "\f1f75"; -} - -.hgi-stroke.hgi-layout-table-01::before { - content: "\f1f76"; -} - -.hgi-stroke.hgi-layout-table-02::before { - content: "\f1f77"; -} - -.hgi-stroke.hgi-layout-template::before { - content: "\f31f4"; -} - -.hgi-stroke.hgi-layout-three-column::before { - content: "\f1f78"; -} - -.hgi-stroke.hgi-layout-three-row::before { - content: "\f1f79"; -} - -.hgi-stroke.hgi-layout-top::before { - content: "\f1f7a"; -} - -.hgi-stroke.hgi-layout-two-column::before { - content: "\f1f7b"; -} - -.hgi-stroke.hgi-layout-two-row::before { - content: "\f1f7c"; -} - -.hgi-stroke.hgi-leaf-01::before { - content: "\f1f7d"; -} - -.hgi-stroke.hgi-leaf-02::before { - content: "\f1f7e"; -} - -.hgi-stroke.hgi-leaf-03::before { - content: "\f1f7f"; -} - -.hgi-stroke.hgi-leaf-04::before { - content: "\f1f80"; -} - -.hgi-stroke.hgi-leafy-green::before { - content: "\f1f81"; -} - -.hgi-stroke.hgi-leaving-geo-fence::before { - content: "\f1f82"; -} - -.hgi-stroke.hgi-lectern::before { - content: "\f31f5"; -} - -.hgi-stroke.hgi-leetcode::before { - content: "\f1f83"; -} - -.hgi-stroke.hgi-left-angle::before { - content: "\f1f84"; -} - -.hgi-stroke.hgi-left-to-right-block-quote::before { - content: "\f1f85"; -} - -.hgi-stroke.hgi-left-to-right-list-bullet::before { - content: "\f1f86"; -} - -.hgi-stroke.hgi-left-to-right-list-dash::before { - content: "\f1f87"; -} - -.hgi-stroke.hgi-left-to-right-list-number::before { - content: "\f1f88"; -} - -.hgi-stroke.hgi-left-to-right-list-star-01::before { - content: "\f1f89"; -} - -.hgi-stroke.hgi-left-to-right-list-star::before { - content: "\f1f8a"; -} - -.hgi-stroke.hgi-left-to-right-list-triangle::before { - content: "\f1f8b"; -} - -.hgi-stroke.hgi-left-triangle::before { - content: "\f1f8c"; -} - -.hgi-stroke.hgi-legal-01::before { - content: "\f1f8d"; -} - -.hgi-stroke.hgi-legal-02::before { - content: "\f1f8e"; -} - -.hgi-stroke.hgi-legal-document-01::before { - content: "\f1f8f"; -} - -.hgi-stroke.hgi-legal-document-02::before { - content: "\f1f90"; -} - -.hgi-stroke.hgi-legal-hammer::before { - content: "\f1f91"; -} - -.hgi-stroke.hgi-lens-concave::before { - content: "\f31f6"; -} - -.hgi-stroke.hgi-lens-convex::before { - content: "\f31f7"; -} - -.hgi-stroke.hgi-less-than-01::before { - content: "\f31f8"; -} - -.hgi-stroke.hgi-less-than-circle::before { - content: "\f1f92"; -} - -.hgi-stroke.hgi-less-than-or-equal::before { - content: "\f31f9"; -} - -.hgi-stroke.hgi-less-than-square::before { - content: "\f1f93"; -} - -.hgi-stroke.hgi-less-than::before { - content: "\f1f94"; -} - -.hgi-stroke.hgi-letter-spacing::before { - content: "\f1f95"; -} - -.hgi-stroke.hgi-libraries::before { - content: "\f1f96"; -} - -.hgi-stroke.hgi-library-big::before { - content: "\f31fa"; -} - -.hgi-stroke.hgi-library::before { - content: "\f1f97"; -} - -.hgi-stroke.hgi-license-draft::before { - content: "\f1f98"; -} - -.hgi-stroke.hgi-license-maintenance::before { - content: "\f1f99"; -} - -.hgi-stroke.hgi-license-no::before { - content: "\f1f9a"; -} - -.hgi-stroke.hgi-license-pin::before { - content: "\f1f9b"; -} - -.hgi-stroke.hgi-license-third-party::before { - content: "\f1f9c"; -} - -.hgi-stroke.hgi-license::before { - content: "\f1f9d"; -} - -.hgi-stroke.hgi-life-buoy::before { - content: "\f31fb"; -} - -.hgi-stroke.hgi-lifebuoy::before { - content: "\f31fc"; -} - -.hgi-stroke.hgi-lift-truck::before { - content: "\f1f9e"; -} - -.hgi-stroke.hgi-ligature::before { - content: "\f31fd"; -} - -.hgi-stroke.hgi-lightbulb-off::before { - content: "\f1f9f"; -} - -.hgi-stroke.hgi-lightbulb::before { - content: "\f31fe"; -} - -.hgi-stroke.hgi-lighthouse::before { - content: "\f1fa0"; -} - -.hgi-stroke.hgi-limit-order::before { - content: "\f1fa1"; -} - -.hgi-stroke.hgi-limitation::before { - content: "\f1fa2"; -} - -.hgi-stroke.hgi-line-dot-right-horizontal::before { - content: "\f31ff"; -} - -.hgi-stroke.hgi-line-squiggle::before { - content: "\f3200"; -} - -.hgi-stroke.hgi-line-style::before { - content: "\f3201"; -} - -.hgi-stroke.hgi-line::before { - content: "\f1fa3"; -} - -.hgi-stroke.hgi-liner::before { - content: "\f1fa4"; -} - -.hgi-stroke.hgi-link-01::before { - content: "\f1fa5"; -} - -.hgi-stroke.hgi-link-02-2::before { - content: "\f3203"; -} - -.hgi-stroke.hgi-link-02::before { - content: "\f1fa6"; -} - -.hgi-stroke.hgi-link-03::before { - content: "\f1fa7"; -} - -.hgi-stroke.hgi-link-04::before { - content: "\f1fa8"; -} - -.hgi-stroke.hgi-link-05::before { - content: "\f1fa9"; -} - -.hgi-stroke.hgi-link-06::before { - content: "\f1faa"; -} - -.hgi-stroke.hgi-link-backward::before { - content: "\f1fab"; -} - -.hgi-stroke.hgi-link-circle-02::before { - content: "\f1fac"; -} - -.hgi-stroke.hgi-link-circle::before { - content: "\f1fad"; -} - -.hgi-stroke.hgi-link-forward::before { - content: "\f1fae"; -} - -.hgi-stroke.hgi-link-off-02::before { - content: "\f3205"; -} - -.hgi-stroke.hgi-link-off::before { - content: "\f3204"; -} - -.hgi-stroke.hgi-link-square-01::before { - content: "\f1faf"; -} - -.hgi-stroke.hgi-link-square-02::before { - content: "\f1fb0"; -} - -.hgi-stroke.hgi-link::before { - content: "\f3202"; -} - -.hgi-stroke.hgi-linkedin-01::before { - content: "\f1fb1"; -} - -.hgi-stroke.hgi-linkedin-02::before { - content: "\f1fb2"; -} - -.hgi-stroke.hgi-list-checks::before { - content: "\f3207"; -} - -.hgi-stroke.hgi-list-chevrons-down-up::before { - content: "\f1fb3"; -} - -.hgi-stroke.hgi-list-collapse::before { - content: "\f3208"; -} - -.hgi-stroke.hgi-list-end::before { - content: "\f3209"; -} - -.hgi-stroke.hgi-list-filter-plus::before { - content: "\f1fb4"; -} - -.hgi-stroke.hgi-list-filter::before { - content: "\f320a"; -} - -.hgi-stroke.hgi-list-indent-decrease::before { - content: "\f1fb5"; -} - -.hgi-stroke.hgi-list-indent-increase::before { - content: "\f1fb6"; -} - -.hgi-stroke.hgi-list-minus::before { - content: "\f320b"; -} - -.hgi-stroke.hgi-list-music::before { - content: "\f1fb7"; -} - -.hgi-stroke.hgi-list-ordered::before { - content: "\f320c"; -} - -.hgi-stroke.hgi-list-plus::before { - content: "\f1fb8"; -} - -.hgi-stroke.hgi-list-restart::before { - content: "\f1fb9"; -} - -.hgi-stroke.hgi-list-setting::before { - content: "\f1fba"; -} - -.hgi-stroke.hgi-list-start::before { - content: "\f1fbb"; -} - -.hgi-stroke.hgi-list-todo::before { - content: "\f320e"; -} - -.hgi-stroke.hgi-list-tree::before { - content: "\f1fbc"; -} - -.hgi-stroke.hgi-list-video::before { - content: "\f1fbd"; -} - -.hgi-stroke.hgi-list-view::before { - content: "\f1fbe"; -} - -.hgi-stroke.hgi-list-x::before { - content: "\f1fbf"; -} - -.hgi-stroke.hgi-list::before { - content: "\f3206"; -} - -.hgi-stroke.hgi-litecoin::before { - content: "\f1fc0"; -} - -.hgi-stroke.hgi-live-streaming-01::before { - content: "\f1fc1"; -} - -.hgi-stroke.hgi-live-streaming-02::before { - content: "\f1fc2"; -} - -.hgi-stroke.hgi-live-streaming-03::before { - content: "\f1fc3"; -} - -.hgi-stroke.hgi-liver::before { - content: "\f1fc4"; -} - -.hgi-stroke.hgi-loader-circle::before { - content: "\f3210"; -} - -.hgi-stroke.hgi-loader-pinwheel::before { - content: "\f1fc5"; -} - -.hgi-stroke.hgi-loader::before { - content: "\f320f"; -} - -.hgi-stroke.hgi-loading-01::before { - content: "\f1fc6"; -} - -.hgi-stroke.hgi-loading-02::before { - content: "\f1fc7"; -} - -.hgi-stroke.hgi-loading-03::before { - content: "\f1fc8"; -} - -.hgi-stroke.hgi-loading-04::before { - content: "\f1fc9"; -} - -.hgi-stroke.hgi-locate-fixed::before { - content: "\f3212"; -} - -.hgi-stroke.hgi-locate-off::before { - content: "\f3213"; -} - -.hgi-stroke.hgi-locate::before { - content: "\f3211"; -} - -.hgi-stroke.hgi-location-01::before { - content: "\f1fca"; -} - -.hgi-stroke.hgi-location-02::before { - content: "\f1fcb"; -} - -.hgi-stroke.hgi-location-03::before { - content: "\f1fcc"; -} - -.hgi-stroke.hgi-location-04::before { - content: "\f1fcd"; -} - -.hgi-stroke.hgi-location-05::before { - content: "\f1fce"; -} - -.hgi-stroke.hgi-location-06::before { - content: "\f1fcf"; -} - -.hgi-stroke.hgi-location-07::before { - content: "\f1fd0"; -} - -.hgi-stroke.hgi-location-08::before { - content: "\f1fd1"; -} - -.hgi-stroke.hgi-location-09::before { - content: "\f1fd2"; -} - -.hgi-stroke.hgi-location-10::before { - content: "\f1fd3"; -} - -.hgi-stroke.hgi-location-add-01::before { - content: "\f1fd4"; -} - -.hgi-stroke.hgi-location-add-02::before { - content: "\f1fd5"; -} - -.hgi-stroke.hgi-location-check-01::before { - content: "\f1fd6"; -} - -.hgi-stroke.hgi-location-check-02::before { - content: "\f1fd7"; -} - -.hgi-stroke.hgi-location-favourite-01::before { - content: "\f1fd8"; -} - -.hgi-stroke.hgi-location-favourite-02::before { - content: "\f1fd9"; -} - -.hgi-stroke.hgi-location-offline-01::before { - content: "\f1fda"; -} - -.hgi-stroke.hgi-location-offline-02::before { - content: "\f1fdb"; -} - -.hgi-stroke.hgi-location-offline-03::before { - content: "\f1fdc"; -} - -.hgi-stroke.hgi-location-offline-04::before { - content: "\f1fdd"; -} - -.hgi-stroke.hgi-location-remove-01::before { - content: "\f1fde"; -} - -.hgi-stroke.hgi-location-remove-02::before { - content: "\f1fdf"; -} - -.hgi-stroke.hgi-location-share-01::before { - content: "\f1fe0"; -} - -.hgi-stroke.hgi-location-share-02::before { - content: "\f1fe1"; -} - -.hgi-stroke.hgi-location-star-01::before { - content: "\f1fe2"; -} - -.hgi-stroke.hgi-location-star-02::before { - content: "\f1fe3"; -} - -.hgi-stroke.hgi-location-update-01::before { - content: "\f1fe4"; -} - -.hgi-stroke.hgi-location-update-02::before { - content: "\f1fe5"; -} - -.hgi-stroke.hgi-location-user-01::before { - content: "\f1fe6"; -} - -.hgi-stroke.hgi-location-user-02::before { - content: "\f1fe7"; -} - -.hgi-stroke.hgi-location-user-03::before { - content: "\f1fe8"; -} - -.hgi-stroke.hgi-location-user-04::before { - content: "\f1fe9"; -} - -.hgi-stroke.hgi-lock-computer::before { - content: "\f1fea"; -} - -.hgi-stroke.hgi-lock-key::before { - content: "\f1feb"; -} - -.hgi-stroke.hgi-lock-keyhole-open::before { - content: "\f3215"; -} - -.hgi-stroke.hgi-lock-keyhole::before { - content: "\f3214"; -} - -.hgi-stroke.hgi-lock-open::before { - content: "\f3216"; -} - -.hgi-stroke.hgi-lock-password::before { - content: "\f1fec"; -} - -.hgi-stroke.hgi-lock-sync-01::before { - content: "\f1fed"; -} - -.hgi-stroke.hgi-lock-sync-02::before { - content: "\f1fee"; -} - -.hgi-stroke.hgi-lock::before { - content: "\f1fef"; -} - -.hgi-stroke.hgi-locked::before { - content: "\f1ff0"; -} - -.hgi-stroke.hgi-locker-01::before { - content: "\f1ff1"; -} - -.hgi-stroke.hgi-locker::before { - content: "\f1ff2"; -} - -.hgi-stroke.hgi-log-in::before { - content: "\f3217"; -} - -.hgi-stroke.hgi-log-out::before { - content: "\f3218"; -} - -.hgi-stroke.hgi-login-01::before { - content: "\f1ff3"; -} - -.hgi-stroke.hgi-login-02::before { - content: "\f1ff4"; -} - -.hgi-stroke.hgi-login-03::before { - content: "\f1ff5"; -} - -.hgi-stroke.hgi-login-circle-01::before { - content: "\f1ff6"; -} - -.hgi-stroke.hgi-login-circle-02::before { - content: "\f1ff7"; -} - -.hgi-stroke.hgi-login-method::before { - content: "\f1ff8"; -} - -.hgi-stroke.hgi-login-square-01::before { - content: "\f1ff9"; -} - -.hgi-stroke.hgi-login-square-02::before { - content: "\f1ffa"; -} - -.hgi-stroke.hgi-logout-01::before { - content: "\f1ffb"; -} - -.hgi-stroke.hgi-logout-02::before { - content: "\f1ffc"; -} - -.hgi-stroke.hgi-logout-03::before { - content: "\f1ffd"; -} - -.hgi-stroke.hgi-logout-04::before { - content: "\f1ffe"; -} - -.hgi-stroke.hgi-logout-05::before { - content: "\f1fff"; -} - -.hgi-stroke.hgi-logout-circle-01::before { - content: "\f2000"; -} - -.hgi-stroke.hgi-logout-circle-02::before { - content: "\f2001"; -} - -.hgi-stroke.hgi-logout-square-01::before { - content: "\f2002"; -} - -.hgi-stroke.hgi-logout-square-02::before { - content: "\f2003"; -} - -.hgi-stroke.hgi-logs::before { - content: "\f3219"; -} - -.hgi-stroke.hgi-lollipop::before { - content: "\f2004"; -} - -.hgi-stroke.hgi-long-sleeve-shirt::before { - content: "\f2005"; -} - -.hgi-stroke.hgi-longitude::before { - content: "\f2006"; -} - -.hgi-stroke.hgi-look-bottom::before { - content: "\f2007"; -} - -.hgi-stroke.hgi-look-left::before { - content: "\f2008"; -} - -.hgi-stroke.hgi-look-right::before { - content: "\f2009"; -} - -.hgi-stroke.hgi-look-top::before { - content: "\f200a"; -} - -.hgi-stroke.hgi-loom::before { - content: "\f200b"; -} - -.hgi-stroke.hgi-lottiefiles::before { - content: "\f200c"; -} - -.hgi-stroke.hgi-love-korean-finger::before { - content: "\f200d"; -} - -.hgi-stroke.hgi-low-signal::before { - content: "\f200e"; -} - -.hgi-stroke.hgi-loyalty-card::before { - content: "\f200f"; -} - -.hgi-stroke.hgi-luggage-01::before { - content: "\f2010"; -} - -.hgi-stroke.hgi-luggage-02::before { - content: "\f2011"; -} - -.hgi-stroke.hgi-lungs::before { - content: "\f2012"; -} - -.hgi-stroke.hgi-machine-robot::before { - content: "\f2053"; -} - -.hgi-stroke.hgi-magic-book-2::before { - content: "\f321b"; -} - -.hgi-stroke.hgi-magic-book::before { - content: "\f321a"; -} - -.hgi-stroke.hgi-magic-wand-01::before { - content: "\f2054"; -} - -.hgi-stroke.hgi-magic-wand-02::before { - content: "\f2055"; -} - -.hgi-stroke.hgi-magic-wand-03::before { - content: "\f2056"; -} - -.hgi-stroke.hgi-magic-wand-04::before { - content: "\f2057"; -} - -.hgi-stroke.hgi-magic-wand-05::before { - content: "\f2058"; -} - -.hgi-stroke.hgi-magnet-01::before { - content: "\f2059"; -} - -.hgi-stroke.hgi-magnet-02::before { - content: "\f205a"; -} - -.hgi-stroke.hgi-magnet::before { - content: "\f205b"; -} - -.hgi-stroke.hgi-mail-01::before { - content: "\f205c"; -} - -.hgi-stroke.hgi-mail-02::before { - content: "\f205d"; -} - -.hgi-stroke.hgi-mail-account-01::before { - content: "\f205e"; -} - -.hgi-stroke.hgi-mail-account-02::before { - content: "\f205f"; -} - -.hgi-stroke.hgi-mail-add-01::before { - content: "\f2060"; -} - -.hgi-stroke.hgi-mail-add-02::before { - content: "\f2061"; -} - -.hgi-stroke.hgi-mail-at-sign-01::before { - content: "\f2062"; -} - -.hgi-stroke.hgi-mail-at-sign-02::before { - content: "\f2063"; -} - -.hgi-stroke.hgi-mail-block-01::before { - content: "\f2064"; -} - -.hgi-stroke.hgi-mail-block-02::before { - content: "\f2065"; -} - -.hgi-stroke.hgi-mail-check::before { - content: "\f321c"; -} - -.hgi-stroke.hgi-mail-download-01::before { - content: "\f2066"; -} - -.hgi-stroke.hgi-mail-download-02::before { - content: "\f2067"; -} - -.hgi-stroke.hgi-mail-edit-01::before { - content: "\f2068"; -} - -.hgi-stroke.hgi-mail-edit-02::before { - content: "\f2069"; -} - -.hgi-stroke.hgi-mail-lock-01::before { - content: "\f206a"; -} - -.hgi-stroke.hgi-mail-lock-02::before { - content: "\f206b"; -} - -.hgi-stroke.hgi-mail-love-01::before { - content: "\f206c"; -} - -.hgi-stroke.hgi-mail-love-02::before { - content: "\f206d"; -} - -.hgi-stroke.hgi-mail-minus-01::before { - content: "\f206e"; -} - -.hgi-stroke.hgi-mail-minus-02::before { - content: "\f206f"; -} - -.hgi-stroke.hgi-mail-open-01::before { - content: "\f2070"; -} - -.hgi-stroke.hgi-mail-open-02::before { - content: "\f2071"; -} - -.hgi-stroke.hgi-mail-open-love::before { - content: "\f2072"; -} - -.hgi-stroke.hgi-mail-open::before { - content: "\f2073"; -} - -.hgi-stroke.hgi-mail-plus::before { - content: "\f321d"; -} - -.hgi-stroke.hgi-mail-question-mark::before { - content: "\f2074"; -} - -.hgi-stroke.hgi-mail-receive-01::before { - content: "\f2075"; -} - -.hgi-stroke.hgi-mail-receive-02::before { - content: "\f2076"; -} - -.hgi-stroke.hgi-mail-remove-01::before { - content: "\f2077"; -} - -.hgi-stroke.hgi-mail-remove-02::before { - content: "\f2078"; -} - -.hgi-stroke.hgi-mail-reply-01::before { - content: "\f2079"; -} - -.hgi-stroke.hgi-mail-reply-02::before { - content: "\f207a"; -} - -.hgi-stroke.hgi-mail-reply-all-01::before { - content: "\f207b"; -} - -.hgi-stroke.hgi-mail-reply-all-02::before { - content: "\f207c"; -} - -.hgi-stroke.hgi-mail-search-01::before { - content: "\f207d"; -} - -.hgi-stroke.hgi-mail-search-02::before { - content: "\f207e"; -} - -.hgi-stroke.hgi-mail-secure-01::before { - content: "\f207f"; -} - -.hgi-stroke.hgi-mail-secure-02::before { - content: "\f2080"; -} - -.hgi-stroke.hgi-mail-send-01::before { - content: "\f2081"; -} - -.hgi-stroke.hgi-mail-send-02::before { - content: "\f2082"; -} - -.hgi-stroke.hgi-mail-setting-01::before { - content: "\f2083"; -} - -.hgi-stroke.hgi-mail-setting-02::before { - content: "\f2084"; -} - -.hgi-stroke.hgi-mail-unlock-01::before { - content: "\f2085"; -} - -.hgi-stroke.hgi-mail-unlock-02::before { - content: "\f2086"; -} - -.hgi-stroke.hgi-mail-upload-01::before { - content: "\f2087"; -} - -.hgi-stroke.hgi-mail-upload-02::before { - content: "\f2088"; -} - -.hgi-stroke.hgi-mail-validation-01::before { - content: "\f2089"; -} - -.hgi-stroke.hgi-mail-validation-02::before { - content: "\f208a"; -} - -.hgi-stroke.hgi-mail-voice-01::before { - content: "\f208b"; -} - -.hgi-stroke.hgi-mail-voice-02::before { - content: "\f208c"; -} - -.hgi-stroke.hgi-mail-warning::before { - content: "\f208d"; -} - -.hgi-stroke.hgi-mail-x::before { - content: "\f321e"; -} - -.hgi-stroke.hgi-mailbox-01::before { - content: "\f208e"; -} - -.hgi-stroke.hgi-mailbox::before { - content: "\f208f"; -} - -.hgi-stroke.hgi-mails::before { - content: "\f2090"; -} - -.hgi-stroke.hgi-male-02::before { - content: "\f2091"; -} - -.hgi-stroke.hgi-male-symbol::before { - content: "\f2092"; -} - -.hgi-stroke.hgi-man-woman::before { - content: "\f2093"; -} - -.hgi-stroke.hgi-man::before { - content: "\f2094"; -} - -.hgi-stroke.hgi-manager::before { - content: "\f2095"; -} - -.hgi-stroke.hgi-map-minus::before { - content: "\f3221"; -} - -.hgi-stroke.hgi-map-pin-check-inside::before { - content: "\f3223"; -} - -.hgi-stroke.hgi-map-pin-check::before { - content: "\f3222"; -} - -.hgi-stroke.hgi-map-pin-house::before { - content: "\f3224"; -} - -.hgi-stroke.hgi-map-pin-minus-inside::before { - content: "\f3226"; -} - -.hgi-stroke.hgi-map-pin-minus::before { - content: "\f3225"; -} - -.hgi-stroke.hgi-map-pin-off::before { - content: "\f3227"; -} - -.hgi-stroke.hgi-map-pin-pen::before { - content: "\f3228"; -} - -.hgi-stroke.hgi-map-pin-plus-inside::before { - content: "\f322a"; -} - -.hgi-stroke.hgi-map-pin-plus::before { - content: "\f3229"; -} - -.hgi-stroke.hgi-map-pin-search::before { - content: "\f322b"; -} - -.hgi-stroke.hgi-map-pin-x-inside::before { - content: "\f322d"; -} - -.hgi-stroke.hgi-map-pin-x::before { - content: "\f322c"; -} - -.hgi-stroke.hgi-map-pin::before { - content: "\f2096"; -} - -.hgi-stroke.hgi-map-pinned::before { - content: "\f322e"; -} - -.hgi-stroke.hgi-map-pinpoint-01::before { - content: "\f2097"; -} - -.hgi-stroke.hgi-map-pinpoint-02::before { - content: "\f2098"; -} - -.hgi-stroke.hgi-map-plus::before { - content: "\f322f"; -} - -.hgi-stroke.hgi-map::before { - content: "\f3220"; -} - -.hgi-stroke.hgi-maping::before { - content: "\f2099"; -} - -.hgi-stroke.hgi-maps-circle-01::before { - content: "\f209a"; -} - -.hgi-stroke.hgi-maps-circle-02::before { - content: "\f209b"; -} - -.hgi-stroke.hgi-maps-editing::before { - content: "\f209c"; -} - -.hgi-stroke.hgi-maps-global-01::before { - content: "\f209d"; -} - -.hgi-stroke.hgi-maps-global-02::before { - content: "\f209e"; -} - -.hgi-stroke.hgi-maps-location-01::before { - content: "\f209f"; -} - -.hgi-stroke.hgi-maps-location-02::before { - content: "\f20a0"; -} - -.hgi-stroke.hgi-maps-off::before { - content: "\f20a1"; -} - -.hgi-stroke.hgi-maps-refresh::before { - content: "\f20a2"; -} - -.hgi-stroke.hgi-maps-search::before { - content: "\f20a3"; -} - -.hgi-stroke.hgi-maps-square-01::before { - content: "\f20a4"; -} - -.hgi-stroke.hgi-maps-square-02::before { - content: "\f20a5"; -} - -.hgi-stroke.hgi-maps::before { - content: "\f20a6"; -} - -.hgi-stroke.hgi-market-analysis::before { - content: "\f20a7"; -} - -.hgi-stroke.hgi-market-order::before { - content: "\f20a8"; -} - -.hgi-stroke.hgi-marketing::before { - content: "\f20a9"; -} - -.hgi-stroke.hgi-mars-stroke::before { - content: "\f3231"; -} - -.hgi-stroke.hgi-mars::before { - content: "\f3230"; -} - -.hgi-stroke.hgi-martini::before { - content: "\f3232"; -} - -.hgi-stroke.hgi-mask-love::before { - content: "\f20aa"; -} - -.hgi-stroke.hgi-mask-theater-01::before { - content: "\f20ab"; -} - -.hgi-stroke.hgi-mask-theater-02::before { - content: "\f20ac"; -} - -.hgi-stroke.hgi-mask::before { - content: "\f20ad"; -} - -.hgi-stroke.hgi-master-card::before { - content: "\f20ae"; -} - -.hgi-stroke.hgi-mastodon::before { - content: "\f20af"; -} - -.hgi-stroke.hgi-matches::before { - content: "\f20b0"; -} - -.hgi-stroke.hgi-material-and-texture::before { - content: "\f20b1"; -} - -.hgi-stroke.hgi-math::before { - content: "\f20b2"; -} - -.hgi-stroke.hgi-matrix::before { - content: "\f20b3"; -} - -.hgi-stroke.hgi-maximize-01::before { - content: "\f20b4"; -} - -.hgi-stroke.hgi-maximize-02::before { - content: "\f20b5"; -} - -.hgi-stroke.hgi-maximize-03::before { - content: "\f20b6"; -} - -.hgi-stroke.hgi-maximize-04::before { - content: "\f20b7"; -} - -.hgi-stroke.hgi-maximize-screen::before { - content: "\f20b8"; -} - -.hgi-stroke.hgi-maximize2::before { - content: "\f3236"; -} - -.hgi-stroke.hgi-mayan-pyramid::before { - content: "\f20b9"; -} - -.hgi-stroke.hgi-maze::before { - content: "\f20ba"; -} - -.hgi-stroke.hgi-mcp-server::before { - content: "\f20bb"; -} - -.hgi-stroke.hgi-meal-scan::before { - content: "\f20bc"; -} - -.hgi-stroke.hgi-medal-01::before { - content: "\f20bd"; -} - -.hgi-stroke.hgi-medal-02::before { - content: "\f20be"; -} - -.hgi-stroke.hgi-medal-03::before { - content: "\f20bf"; -} - -.hgi-stroke.hgi-medal-04::before { - content: "\f20c0"; -} - -.hgi-stroke.hgi-medal-05::before { - content: "\f20c1"; -} - -.hgi-stroke.hgi-medal-06::before { - content: "\f20c2"; -} - -.hgi-stroke.hgi-medal-07::before { - content: "\f20c3"; -} - -.hgi-stroke.hgi-medal-first-place::before { - content: "\f20c4"; -} - -.hgi-stroke.hgi-medal-second-place::before { - content: "\f20c5"; -} - -.hgi-stroke.hgi-medal-third-place::before { - content: "\f20c6"; -} - -.hgi-stroke.hgi-medical-file::before { - content: "\f20c7"; -} - -.hgi-stroke.hgi-medical-mask::before { - content: "\f20c8"; -} - -.hgi-stroke.hgi-medicine-01::before { - content: "\f20c9"; -} - -.hgi-stroke.hgi-medicine-02::before { - content: "\f20ca"; -} - -.hgi-stroke.hgi-medicine-bottle-01::before { - content: "\f20cb"; -} - -.hgi-stroke.hgi-medicine-bottle-02::before { - content: "\f20cc"; -} - -.hgi-stroke.hgi-medicine-syrup::before { - content: "\f20cd"; -} - -.hgi-stroke.hgi-medium-signal::before { - content: "\f20ce"; -} - -.hgi-stroke.hgi-medium-square::before { - content: "\f20cf"; -} - -.hgi-stroke.hgi-medium::before { - content: "\f20d0"; -} - -.hgi-stroke.hgi-meeting-room::before { - content: "\f20d1"; -} - -.hgi-stroke.hgi-megaphone-01::before { - content: "\f20d2"; -} - -.hgi-stroke.hgi-megaphone-02::before { - content: "\f20d3"; -} - -.hgi-stroke.hgi-megaphone-03::before { - content: "\f20d4"; -} - -.hgi-stroke.hgi-megaphone-off::before { - content: "\f3237"; -} - -.hgi-stroke.hgi-meh::before { - content: "\f20d5"; -} - -.hgi-stroke.hgi-memory-stick::before { - content: "\f20d6"; -} - -.hgi-stroke.hgi-mentor::before { - content: "\f20d7"; -} - -.hgi-stroke.hgi-mentoring::before { - content: "\f20d8"; -} - -.hgi-stroke.hgi-menu-01::before { - content: "\f20d9"; -} - -.hgi-stroke.hgi-menu-02::before { - content: "\f20da"; -} - -.hgi-stroke.hgi-menu-03::before { - content: "\f20db"; -} - -.hgi-stroke.hgi-menu-04::before { - content: "\f20dc"; -} - -.hgi-stroke.hgi-menu-05::before { - content: "\f20dd"; -} - -.hgi-stroke.hgi-menu-06::before { - content: "\f20de"; -} - -.hgi-stroke.hgi-menu-07::before { - content: "\f20df"; -} - -.hgi-stroke.hgi-menu-08::before { - content: "\f20e0"; -} - -.hgi-stroke.hgi-menu-09::before { - content: "\f20e1"; -} - -.hgi-stroke.hgi-menu-10::before { - content: "\f20e2"; -} - -.hgi-stroke.hgi-menu-11::before { - content: "\f20e3"; -} - -.hgi-stroke.hgi-menu-circle::before { - content: "\f20e4"; -} - -.hgi-stroke.hgi-menu-collapse::before { - content: "\f20e5"; -} - -.hgi-stroke.hgi-menu-restaurant::before { - content: "\f20e6"; -} - -.hgi-stroke.hgi-menu-square::before { - content: "\f20e7"; -} - -.hgi-stroke.hgi-menu-two-line::before { - content: "\f20e8"; -} - -.hgi-stroke.hgi-menu::before { - content: "\f3238"; -} - -.hgi-stroke.hgi-merge::before { - content: "\f3239"; -} - -.hgi-stroke.hgi-message-01::before { - content: "\f20e9"; -} - -.hgi-stroke.hgi-message-02::before { - content: "\f20ea"; -} - -.hgi-stroke.hgi-message-add-01::before { - content: "\f20eb"; -} - -.hgi-stroke.hgi-message-add-02::before { - content: "\f20ec"; -} - -.hgi-stroke.hgi-message-blocked::before { - content: "\f20ed"; -} - -.hgi-stroke.hgi-message-cancel-01::before { - content: "\f20ee"; -} - -.hgi-stroke.hgi-message-cancel-02::before { - content: "\f20ef"; -} - -.hgi-stroke.hgi-message-circle-check::before { - content: "\f323b"; -} - -.hgi-stroke.hgi-message-circle-code::before { - content: "\f20f0"; -} - -.hgi-stroke.hgi-message-circle-dashed::before { - content: "\f323c"; -} - -.hgi-stroke.hgi-message-circle-heart::before { - content: "\f323d"; -} - -.hgi-stroke.hgi-message-circle-more::before { - content: "\f323e"; -} - -.hgi-stroke.hgi-message-circle-off::before { - content: "\f323f"; -} - -.hgi-stroke.hgi-message-circle-plus::before { - content: "\f3240"; -} - -.hgi-stroke.hgi-message-circle-question-mark::before { - content: "\f3241"; -} - -.hgi-stroke.hgi-message-circle-reply::before { - content: "\f20f1"; -} - -.hgi-stroke.hgi-message-circle-warning::before { - content: "\f3242"; -} - -.hgi-stroke.hgi-message-circle-x::before { - content: "\f3243"; -} - -.hgi-stroke.hgi-message-circle::before { - content: "\f323a"; -} - -.hgi-stroke.hgi-message-delay-01::before { - content: "\f20f2"; -} - -.hgi-stroke.hgi-message-delay-02::before { - content: "\f20f3"; -} - -.hgi-stroke.hgi-message-done-01::before { - content: "\f20f4"; -} - -.hgi-stroke.hgi-message-done-02::before { - content: "\f20f5"; -} - -.hgi-stroke.hgi-message-download-01::before { - content: "\f20f6"; -} - -.hgi-stroke.hgi-message-download-02::before { - content: "\f20f7"; -} - -.hgi-stroke.hgi-message-edit-01::before { - content: "\f20f8"; -} - -.hgi-stroke.hgi-message-edit-02::before { - content: "\f20f9"; -} - -.hgi-stroke.hgi-message-favourite-01::before { - content: "\f20fa"; -} - -.hgi-stroke.hgi-message-favourite-02::before { - content: "\f20fb"; -} - -.hgi-stroke.hgi-message-incoming-01::before { - content: "\f20fc"; -} - -.hgi-stroke.hgi-message-incoming-02::before { - content: "\f20fd"; -} - -.hgi-stroke.hgi-message-lock-01::before { - content: "\f20fe"; -} - -.hgi-stroke.hgi-message-lock-02::before { - content: "\f20ff"; -} - -.hgi-stroke.hgi-message-multiple-01::before { - content: "\f2100"; -} - -.hgi-stroke.hgi-message-multiple-02::before { - content: "\f2101"; -} - -.hgi-stroke.hgi-message-notification-01::before { - content: "\f2102"; -} - -.hgi-stroke.hgi-message-notification-02::before { - content: "\f2103"; -} - -.hgi-stroke.hgi-message-outgoing-01::before { - content: "\f2104"; -} - -.hgi-stroke.hgi-message-outgoing-02::before { - content: "\f2105"; -} - -.hgi-stroke.hgi-message-preview-01::before { - content: "\f2106"; -} - -.hgi-stroke.hgi-message-preview-02::before { - content: "\f2107"; -} - -.hgi-stroke.hgi-message-programming::before { - content: "\f2108"; -} - -.hgi-stroke.hgi-message-question::before { - content: "\f2109"; -} - -.hgi-stroke.hgi-message-search-01::before { - content: "\f210a"; -} - -.hgi-stroke.hgi-message-search-02::before { - content: "\f210b"; -} - -.hgi-stroke.hgi-message-secure-01::before { - content: "\f210c"; -} - -.hgi-stroke.hgi-message-secure-02::before { - content: "\f210d"; -} - -.hgi-stroke.hgi-message-square-check::before { - content: "\f3245"; -} - -.hgi-stroke.hgi-message-square-code::before { - content: "\f210e"; -} - -.hgi-stroke.hgi-message-square-dashed::before { - content: "\f210f"; -} - -.hgi-stroke.hgi-message-square-diff::before { - content: "\f2110"; -} - -.hgi-stroke.hgi-message-square-dot-2::before { - content: "\f3247"; -} - -.hgi-stroke.hgi-message-square-dot::before { - content: "\f3246"; -} - -.hgi-stroke.hgi-message-square-heart-2::before { - content: "\f3249"; -} - -.hgi-stroke.hgi-message-square-heart::before { - content: "\f3248"; -} - -.hgi-stroke.hgi-message-square-lock-2::before { - content: "\f324b"; -} - -.hgi-stroke.hgi-message-square-lock::before { - content: "\f324a"; -} - -.hgi-stroke.hgi-message-square-more-2::before { - content: "\f324d"; -} - -.hgi-stroke.hgi-message-square-more::before { - content: "\f324c"; -} - -.hgi-stroke.hgi-message-square-off-2::before { - content: "\f324f"; -} - -.hgi-stroke.hgi-message-square-off::before { - content: "\f324e"; -} - -.hgi-stroke.hgi-message-square-plus-2::before { - content: "\f3251"; -} - -.hgi-stroke.hgi-message-square-plus::before { - content: "\f3250"; -} - -.hgi-stroke.hgi-message-square-quote-2::before { - content: "\f3253"; -} - -.hgi-stroke.hgi-message-square-quote::before { - content: "\f3252"; -} - -.hgi-stroke.hgi-message-square-reply-2::before { - content: "\f3255"; -} - -.hgi-stroke.hgi-message-square-reply::before { - content: "\f3254"; -} - -.hgi-stroke.hgi-message-square-share::before { - content: "\f2111"; -} - -.hgi-stroke.hgi-message-square-text-2::before { - content: "\f3257"; -} - -.hgi-stroke.hgi-message-square-text::before { - content: "\f3256"; -} - -.hgi-stroke.hgi-message-square-warning-2::before { - content: "\f3259"; -} - -.hgi-stroke.hgi-message-square-warning::before { - content: "\f3258"; -} - -.hgi-stroke.hgi-message-square-x::before { - content: "\f325a"; -} - -.hgi-stroke.hgi-message-square::before { - content: "\f3244"; -} - -.hgi-stroke.hgi-message-translate::before { - content: "\f2112"; -} - -.hgi-stroke.hgi-message-unlock-01::before { - content: "\f2113"; -} - -.hgi-stroke.hgi-message-unlock-02::before { - content: "\f2114"; -} - -.hgi-stroke.hgi-message-upload-01::before { - content: "\f2115"; -} - -.hgi-stroke.hgi-message-upload-02::before { - content: "\f2116"; -} - -.hgi-stroke.hgi-message-user-01::before { - content: "\f2117"; -} - -.hgi-stroke.hgi-message-user-02::before { - content: "\f2118"; -} - -.hgi-stroke.hgi-messages-square::before { - content: "\f325b"; -} - -.hgi-stroke.hgi-messenger::before { - content: "\f2119"; -} - -.hgi-stroke.hgi-meta::before { - content: "\f211a"; -} - -.hgi-stroke.hgi-metro::before { - content: "\f211b"; -} - -.hgi-stroke.hgi-metronome::before { - content: "\f325c"; -} - -.hgi-stroke.hgi-mic-01::before { - content: "\f211c"; -} - -.hgi-stroke.hgi-mic-02::before { - content: "\f211d"; -} - -.hgi-stroke.hgi-mic-off-01::before { - content: "\f211e"; -} - -.hgi-stroke.hgi-mic-off-02::before { - content: "\f211f"; -} - -.hgi-stroke.hgi-mic-vocal::before { - content: "\f325d"; -} - -.hgi-stroke.hgi-microchip::before { - content: "\f2120"; -} - -.hgi-stroke.hgi-microscope::before { - content: "\f2121"; -} - -.hgi-stroke.hgi-microsoft-admin::before { - content: "\f2122"; -} - -.hgi-stroke.hgi-microsoft::before { - content: "\f2123"; -} - -.hgi-stroke.hgi-microwave::before { - content: "\f2124"; -} - -.hgi-stroke.hgi-milestone::before { - content: "\f325e"; -} - -.hgi-stroke.hgi-milk-bottle::before { - content: "\f2125"; -} - -.hgi-stroke.hgi-milk-carton::before { - content: "\f2126"; -} - -.hgi-stroke.hgi-milk-coconut::before { - content: "\f2127"; -} - -.hgi-stroke.hgi-milk-oat::before { - content: "\f2128"; -} - -.hgi-stroke.hgi-milk-off::before { - content: "\f3260"; -} - -.hgi-stroke.hgi-milk::before { - content: "\f325f"; -} - -.hgi-stroke.hgi-minimize-01::before { - content: "\f2129"; -} - -.hgi-stroke.hgi-minimize-02::before { - content: "\f212a"; -} - -.hgi-stroke.hgi-minimize-03::before { - content: "\f212b"; -} - -.hgi-stroke.hgi-minimize-04::before { - content: "\f212c"; -} - -.hgi-stroke.hgi-minimize-screen::before { - content: "\f212d"; -} - -.hgi-stroke.hgi-minimize::before { - content: "\f3261"; -} - -.hgi-stroke.hgi-minimize2::before { - content: "\f3262"; -} - -.hgi-stroke.hgi-mining-01::before { - content: "\f212e"; -} - -.hgi-stroke.hgi-mining-02::before { - content: "\f212f"; -} - -.hgi-stroke.hgi-mining-03::before { - content: "\f2130"; -} - -.hgi-stroke.hgi-minus-plus-01::before { - content: "\f2131"; -} - -.hgi-stroke.hgi-minus-plus-02::before { - content: "\f2132"; -} - -.hgi-stroke.hgi-minus-plus-circle-01::before { - content: "\f2133"; -} - -.hgi-stroke.hgi-minus-plus-circle-02::before { - content: "\f2134"; -} - -.hgi-stroke.hgi-minus-plus-square-01::before { - content: "\f2135"; -} - -.hgi-stroke.hgi-minus-plus-square-02::before { - content: "\f2136"; -} - -.hgi-stroke.hgi-minus-sign-circle::before { - content: "\f2137"; -} - -.hgi-stroke.hgi-minus-sign-square::before { - content: "\f2138"; -} - -.hgi-stroke.hgi-minus-sign::before { - content: "\f2139"; -} - -.hgi-stroke.hgi-minus::before { - content: "\f3263"; -} - -.hgi-stroke.hgi-mirror-rectangular::before { - content: "\f3264"; -} - -.hgi-stroke.hgi-mirror-round::before { - content: "\f3265"; -} - -.hgi-stroke.hgi-mirror::before { - content: "\f213a"; -} - -.hgi-stroke.hgi-mirroring-screen::before { - content: "\f213b"; -} - -.hgi-stroke.hgi-mistral::before { - content: "\f213c"; -} - -.hgi-stroke.hgi-mixer::before { - content: "\f213d"; -} - -.hgi-stroke.hgi-mobile-navigator-01::before { - content: "\f213e"; -} - -.hgi-stroke.hgi-mobile-navigator-02::before { - content: "\f213f"; -} - -.hgi-stroke.hgi-mobile-programming-01::before { - content: "\f2140"; -} - -.hgi-stroke.hgi-mobile-programming-02::before { - content: "\f2141"; -} - -.hgi-stroke.hgi-mobile-protection::before { - content: "\f2142"; -} - -.hgi-stroke.hgi-mobile-security::before { - content: "\f2143"; -} - -.hgi-stroke.hgi-mochi::before { - content: "\f2144"; -} - -.hgi-stroke.hgi-modern-tv-four-k::before { - content: "\f2145"; -} - -.hgi-stroke.hgi-modern-tv-issue::before { - content: "\f2146"; -} - -.hgi-stroke.hgi-modern-tv::before { - content: "\f2147"; -} - -.hgi-stroke.hgi-molecules::before { - content: "\f2148"; -} - -.hgi-stroke.hgi-mollie::before { - content: "\f2149"; -} - -.hgi-stroke.hgi-monas::before { - content: "\f214a"; -} - -.hgi-stroke.hgi-money-01::before { - content: "\f214b"; -} - -.hgi-stroke.hgi-money-02::before { - content: "\f214c"; -} - -.hgi-stroke.hgi-money-03::before { - content: "\f214d"; -} - -.hgi-stroke.hgi-money-04::before { - content: "\f214e"; -} - -.hgi-stroke.hgi-money-add-01::before { - content: "\f214f"; -} - -.hgi-stroke.hgi-money-add-02::before { - content: "\f2150"; -} - -.hgi-stroke.hgi-money-bag-01::before { - content: "\f2151"; -} - -.hgi-stroke.hgi-money-bag-02::before { - content: "\f2152"; -} - -.hgi-stroke.hgi-money-exchange-01::before { - content: "\f2153"; -} - -.hgi-stroke.hgi-money-exchange-02::before { - content: "\f2154"; -} - -.hgi-stroke.hgi-money-exchange-03::before { - content: "\f2155"; -} - -.hgi-stroke.hgi-money-lock::before { - content: "\f2156"; -} - -.hgi-stroke.hgi-money-not-found-01::before { - content: "\f2157"; -} - -.hgi-stroke.hgi-money-not-found-02::before { - content: "\f2158"; -} - -.hgi-stroke.hgi-money-not-found-03::before { - content: "\f2159"; -} - -.hgi-stroke.hgi-money-not-found-04::before { - content: "\f215a"; -} - -.hgi-stroke.hgi-money-receive-01::before { - content: "\f215b"; -} - -.hgi-stroke.hgi-money-receive-02::before { - content: "\f215c"; -} - -.hgi-stroke.hgi-money-receive-circle::before { - content: "\f215d"; -} - -.hgi-stroke.hgi-money-receive-flow-01::before { - content: "\f215e"; -} - -.hgi-stroke.hgi-money-receive-flow-02::before { - content: "\f215f"; -} - -.hgi-stroke.hgi-money-receive-square::before { - content: "\f2160"; -} - -.hgi-stroke.hgi-money-remove-01::before { - content: "\f2161"; -} - -.hgi-stroke.hgi-money-remove-02::before { - content: "\f2162"; -} - -.hgi-stroke.hgi-money-safe::before { - content: "\f2163"; -} - -.hgi-stroke.hgi-money-saving-jar::before { - content: "\f2164"; -} - -.hgi-stroke.hgi-money-security::before { - content: "\f2165"; -} - -.hgi-stroke.hgi-money-send-01::before { - content: "\f2166"; -} - -.hgi-stroke.hgi-money-send-02::before { - content: "\f2167"; -} - -.hgi-stroke.hgi-money-send-circle::before { - content: "\f2168"; -} - -.hgi-stroke.hgi-money-send-flow-01::before { - content: "\f2169"; -} - -.hgi-stroke.hgi-money-send-flow-02::before { - content: "\f216a"; -} - -.hgi-stroke.hgi-money-send-square::before { - content: "\f216b"; -} - -.hgi-stroke.hgi-monitor-check::before { - content: "\f3267"; -} - -.hgi-stroke.hgi-monitor-cloud::before { - content: "\f3268"; -} - -.hgi-stroke.hgi-monitor-cog::before { - content: "\f3269"; -} - -.hgi-stroke.hgi-monitor-dot::before { - content: "\f216c"; -} - -.hgi-stroke.hgi-monitor-down::before { - content: "\f326a"; -} - -.hgi-stroke.hgi-monitor-off::before { - content: "\f326b"; -} - -.hgi-stroke.hgi-monitor-pause::before { - content: "\f216d"; -} - -.hgi-stroke.hgi-monitor-play::before { - content: "\f326c"; -} - -.hgi-stroke.hgi-monitor-smartphone::before { - content: "\f326d"; -} - -.hgi-stroke.hgi-monitor-speaker::before { - content: "\f216e"; -} - -.hgi-stroke.hgi-monitor-stop::before { - content: "\f216f"; -} - -.hgi-stroke.hgi-monitor-up::before { - content: "\f326e"; -} - -.hgi-stroke.hgi-monitor-x::before { - content: "\f326f"; -} - -.hgi-stroke.hgi-monitor::before { - content: "\f3266"; -} - -.hgi-stroke.hgi-monocle-01::before { - content: "\f2170"; -} - -.hgi-stroke.hgi-monocle::before { - content: "\f2171"; -} - -.hgi-stroke.hgi-monster-2::before { - content: "\f3270"; -} - -.hgi-stroke.hgi-monster::before { - content: "\f2172"; -} - -.hgi-stroke.hgi-moon-01::before { - content: "\f2173"; -} - -.hgi-stroke.hgi-moon-02::before { - content: "\f2174"; -} - -.hgi-stroke.hgi-moon-angled-rain-zap::before { - content: "\f2175"; -} - -.hgi-stroke.hgi-moon-cloud-angled-rain::before { - content: "\f2176"; -} - -.hgi-stroke.hgi-moon-cloud-angled-zap::before { - content: "\f2177"; -} - -.hgi-stroke.hgi-moon-cloud-big-rain::before { - content: "\f2178"; -} - -.hgi-stroke.hgi-moon-cloud-fast-wind::before { - content: "\f2179"; -} - -.hgi-stroke.hgi-moon-cloud-hailstone::before { - content: "\f217a"; -} - -.hgi-stroke.hgi-moon-cloud-little-rain::before { - content: "\f217b"; -} - -.hgi-stroke.hgi-moon-cloud-little-snow::before { - content: "\f217c"; -} - -.hgi-stroke.hgi-moon-cloud-mid-rain::before { - content: "\f217d"; -} - -.hgi-stroke.hgi-moon-cloud-mid-snow::before { - content: "\f217e"; -} - -.hgi-stroke.hgi-moon-cloud-slow-wind::before { - content: "\f217f"; -} - -.hgi-stroke.hgi-moon-cloud-snow::before { - content: "\f2180"; -} - -.hgi-stroke.hgi-moon-cloud::before { - content: "\f2181"; -} - -.hgi-stroke.hgi-moon-eclipse::before { - content: "\f2182"; -} - -.hgi-stroke.hgi-moon-fast-wind::before { - content: "\f2183"; -} - -.hgi-stroke.hgi-moon-landing-2::before { - content: "\f3271"; -} - -.hgi-stroke.hgi-moon-landing::before { - content: "\f2184"; -} - -.hgi-stroke.hgi-moon-slow-wind::before { - content: "\f2185"; -} - -.hgi-stroke.hgi-moon-star::before { - content: "\f3272"; -} - -.hgi-stroke.hgi-moon::before { - content: "\f2186"; -} - -.hgi-stroke.hgi-moonset::before { - content: "\f2187"; -} - -.hgi-stroke.hgi-more-01::before { - content: "\f2188"; -} - -.hgi-stroke.hgi-more-02::before { - content: "\f2189"; -} - -.hgi-stroke.hgi-more-03::before { - content: "\f218a"; -} - -.hgi-stroke.hgi-more-horizontal-circle-01::before { - content: "\f218b"; -} - -.hgi-stroke.hgi-more-horizontal-circle-02::before { - content: "\f218c"; -} - -.hgi-stroke.hgi-more-horizontal-square-01::before { - content: "\f218d"; -} - -.hgi-stroke.hgi-more-horizontal-square-02::before { - content: "\f218e"; -} - -.hgi-stroke.hgi-more-horizontal::before { - content: "\f218f"; -} - -.hgi-stroke.hgi-more-or-less-circle::before { - content: "\f2190"; -} - -.hgi-stroke.hgi-more-or-less-square::before { - content: "\f2191"; -} - -.hgi-stroke.hgi-more-or-less::before { - content: "\f2192"; -} - -.hgi-stroke.hgi-more-vertical-circle-01::before { - content: "\f2193"; -} - -.hgi-stroke.hgi-more-vertical-circle-02::before { - content: "\f2194"; -} - -.hgi-stroke.hgi-more-vertical-square-01::before { - content: "\f2195"; -} - -.hgi-stroke.hgi-more-vertical-square-02::before { - content: "\f2196"; -} - -.hgi-stroke.hgi-more-vertical::before { - content: "\f2197"; -} - -.hgi-stroke.hgi-more::before { - content: "\f2198"; -} - -.hgi-stroke.hgi-mortar::before { - content: "\f2199"; -} - -.hgi-stroke.hgi-mortarboard-01::before { - content: "\f219a"; -} - -.hgi-stroke.hgi-mortarboard-02::before { - content: "\f219b"; -} - -.hgi-stroke.hgi-mosque-01::before { - content: "\f219c"; -} - -.hgi-stroke.hgi-mosque-02::before { - content: "\f219d"; -} - -.hgi-stroke.hgi-mosque-03::before { - content: "\f219e"; -} - -.hgi-stroke.hgi-mosque-04::before { - content: "\f219f"; -} - -.hgi-stroke.hgi-mosque-05::before { - content: "\f21a0"; -} - -.hgi-stroke.hgi-mosque-location::before { - content: "\f21a1"; -} - -.hgi-stroke.hgi-motion-01::before { - content: "\f21a2"; -} - -.hgi-stroke.hgi-motion-02::before { - content: "\f21a3"; -} - -.hgi-stroke.hgi-motorbike-01::before { - content: "\f21a4"; -} - -.hgi-stroke.hgi-motorbike-02::before { - content: "\f21a5"; -} - -.hgi-stroke.hgi-mountain-snow::before { - content: "\f3274"; -} - -.hgi-stroke.hgi-mountain::before { - content: "\f21a6"; -} - -.hgi-stroke.hgi-mouse-01::before { - content: "\f21a7"; -} - -.hgi-stroke.hgi-mouse-02::before { - content: "\f21a8"; -} - -.hgi-stroke.hgi-mouse-03::before { - content: "\f21a9"; -} - -.hgi-stroke.hgi-mouse-04::before { - content: "\f21aa"; -} - -.hgi-stroke.hgi-mouse-05::before { - content: "\f21ab"; -} - -.hgi-stroke.hgi-mouse-06::before { - content: "\f21ac"; -} - -.hgi-stroke.hgi-mouse-07::before { - content: "\f21ad"; -} - -.hgi-stroke.hgi-mouse-08::before { - content: "\f21ae"; -} - -.hgi-stroke.hgi-mouse-09::before { - content: "\f21af"; -} - -.hgi-stroke.hgi-mouse-10::before { - content: "\f21b0"; -} - -.hgi-stroke.hgi-mouse-11::before { - content: "\f21b1"; -} - -.hgi-stroke.hgi-mouse-12::before { - content: "\f21b2"; -} - -.hgi-stroke.hgi-mouse-13::before { - content: "\f21b3"; -} - -.hgi-stroke.hgi-mouse-14::before { - content: "\f21b4"; -} - -.hgi-stroke.hgi-mouse-15::before { - content: "\f21b5"; -} - -.hgi-stroke.hgi-mouse-16::before { - content: "\f21b6"; -} - -.hgi-stroke.hgi-mouse-17::before { - content: "\f21b7"; -} - -.hgi-stroke.hgi-mouse-18::before { - content: "\f21b8"; -} - -.hgi-stroke.hgi-mouse-19::before { - content: "\f21b9"; -} - -.hgi-stroke.hgi-mouse-20::before { - content: "\f21ba"; -} - -.hgi-stroke.hgi-mouse-21::before { - content: "\f21bb"; -} - -.hgi-stroke.hgi-mouse-22::before { - content: "\f21bc"; -} - -.hgi-stroke.hgi-mouse-23::before { - content: "\f21bd"; -} - -.hgi-stroke.hgi-mouse-left-click-01::before { - content: "\f21be"; -} - -.hgi-stroke.hgi-mouse-left-click-02::before { - content: "\f21bf"; -} - -.hgi-stroke.hgi-mouse-left-click-03::before { - content: "\f21c0"; -} - -.hgi-stroke.hgi-mouse-left-click-04::before { - content: "\f21c1"; -} - -.hgi-stroke.hgi-mouse-left-click-05::before { - content: "\f21c2"; -} - -.hgi-stroke.hgi-mouse-left-click-06::before { - content: "\f21c3"; -} - -.hgi-stroke.hgi-mouse-left::before { - content: "\f3275"; -} - -.hgi-stroke.hgi-mouse-off::before { - content: "\f3276"; -} - -.hgi-stroke.hgi-mouse-pointer-01::before { - content: "\f3278"; -} - -.hgi-stroke.hgi-mouse-pointer-ban::before { - content: "\f3279"; -} - -.hgi-stroke.hgi-mouse-pointer-click::before { - content: "\f327a"; -} - -.hgi-stroke.hgi-mouse-pointer-off-01::before { - content: "\f327b"; -} - -.hgi-stroke.hgi-mouse-pointer-off::before { - content: "\f21c4"; -} - -.hgi-stroke.hgi-mouse-pointer::before { - content: "\f3277"; -} - -.hgi-stroke.hgi-mouse-pointer2-off::before { - content: "\f327d"; -} - -.hgi-stroke.hgi-mouse-pointer2::before { - content: "\f327c"; -} - -.hgi-stroke.hgi-mouse-right-click-01::before { - content: "\f21c5"; -} - -.hgi-stroke.hgi-mouse-right-click-02::before { - content: "\f21c6"; -} - -.hgi-stroke.hgi-mouse-right-click-03::before { - content: "\f21c7"; -} - -.hgi-stroke.hgi-mouse-right-click-04::before { - content: "\f21c8"; -} - -.hgi-stroke.hgi-mouse-right-click-05::before { - content: "\f21c9"; -} - -.hgi-stroke.hgi-mouse-right-click-06::before { - content: "\f21ca"; -} - -.hgi-stroke.hgi-mouse-right::before { - content: "\f327e"; -} - -.hgi-stroke.hgi-mouse-scroll-01::before { - content: "\f21cb"; -} - -.hgi-stroke.hgi-mouse-scroll-02::before { - content: "\f21cc"; -} - -.hgi-stroke.hgi-move-01::before { - content: "\f21cd"; -} - -.hgi-stroke.hgi-move-02::before { - content: "\f21ce"; -} - -.hgi-stroke.hgi-move-bottom::before { - content: "\f21cf"; -} - -.hgi-stroke.hgi-move-diagonal-01::before { - content: "\f3280"; -} - -.hgi-stroke.hgi-move-diagonal::before { - content: "\f327f"; -} - -.hgi-stroke.hgi-move-diagonal2::before { - content: "\f3281"; -} - -.hgi-stroke.hgi-move-down-left::before { - content: "\f3283"; -} - -.hgi-stroke.hgi-move-down-right::before { - content: "\f3284"; -} - -.hgi-stroke.hgi-move-down::before { - content: "\f3282"; -} - -.hgi-stroke.hgi-move-horizontal::before { - content: "\f3285"; -} - -.hgi-stroke.hgi-move-left::before { - content: "\f21d0"; -} - -.hgi-stroke.hgi-move-right::before { - content: "\f21d1"; -} - -.hgi-stroke.hgi-move-three-d::before { - content: "\f3286"; -} - -.hgi-stroke.hgi-move-to::before { - content: "\f21d2"; -} - -.hgi-stroke.hgi-move-top::before { - content: "\f21d3"; -} - -.hgi-stroke.hgi-move-up-left::before { - content: "\f3288"; -} - -.hgi-stroke.hgi-move-up-right::before { - content: "\f3289"; -} - -.hgi-stroke.hgi-move-up::before { - content: "\f3287"; -} - -.hgi-stroke.hgi-move-vertical::before { - content: "\f328a"; -} - -.hgi-stroke.hgi-move::before { - content: "\f21d4"; -} - -.hgi-stroke.hgi-move3d::before { - content: "\f328b"; -} - -.hgi-stroke.hgi-mp-four-01::before { - content: "\f21d5"; -} - -.hgi-stroke.hgi-mp-four-02::before { - content: "\f21d6"; -} - -.hgi-stroke.hgi-mp-three-01::before { - content: "\f21d7"; -} - -.hgi-stroke.hgi-mp-three-02::before { - content: "\f21d8"; -} - -.hgi-stroke.hgi-mp3-01::before { - content: "\f328c"; -} - -.hgi-stroke.hgi-muhammad::before { - content: "\f21d9"; -} - -.hgi-stroke.hgi-multiplication-sign-circle::before { - content: "\f21da"; -} - -.hgi-stroke.hgi-multiplication-sign-square::before { - content: "\f21db"; -} - -.hgi-stroke.hgi-multiplication-sign::before { - content: "\f21dc"; -} - -.hgi-stroke.hgi-mushroom-01::before { - content: "\f21dd"; -} - -.hgi-stroke.hgi-mushroom::before { - content: "\f21de"; -} - -.hgi-stroke.hgi-music-01::before { - content: "\f328e"; -} - -.hgi-stroke.hgi-music-02::before { - content: "\f328f"; -} - -.hgi-stroke.hgi-music-3::before { - content: "\f21df"; -} - -.hgi-stroke.hgi-music-note-01::before { - content: "\f21e0"; -} - -.hgi-stroke.hgi-music-note-02::before { - content: "\f21e1"; -} - -.hgi-stroke.hgi-music-note-03::before { - content: "\f21e2"; -} - -.hgi-stroke.hgi-music-note-04::before { - content: "\f21e3"; -} - -.hgi-stroke.hgi-music-note-square-01::before { - content: "\f21e4"; -} - -.hgi-stroke.hgi-music-note-square-02::before { - content: "\f21e5"; -} - -.hgi-stroke.hgi-music::before { - content: "\f328d"; -} - -.hgi-stroke.hgi-music2::before { - content: "\f3290"; -} - -.hgi-stroke.hgi-music3::before { - content: "\f3291"; -} - -.hgi-stroke.hgi-music4::before { - content: "\f3292"; -} - -.hgi-stroke.hgi-muslim::before { - content: "\f21e6"; -} - -.hgi-stroke.hgi-mute::before { - content: "\f21e7"; -} - -.hgi-stroke.hgi-mymind::before { - content: "\f21e8"; -} - -.hgi-stroke.hgi-n-th-root-circle::before { - content: "\f21ed"; -} - -.hgi-stroke.hgi-n-th-root-square::before { - content: "\f21ee"; -} - -.hgi-stroke.hgi-n-th-root::before { - content: "\f21ef"; -} - -.hgi-stroke.hgi-nano-technology::before { - content: "\f21f0"; -} - -.hgi-stroke.hgi-napkins-01::before { - content: "\f21f1"; -} - -.hgi-stroke.hgi-napkins-02::before { - content: "\f21f2"; -} - -.hgi-stroke.hgi-natural-food::before { - content: "\f21f3"; -} - -.hgi-stroke.hgi-navigation-01::before { - content: "\f21f4"; -} - -.hgi-stroke.hgi-navigation-02::before { - content: "\f21f5"; -} - -.hgi-stroke.hgi-navigation-03::before { - content: "\f21f6"; -} - -.hgi-stroke.hgi-navigation-04::before { - content: "\f21f7"; -} - -.hgi-stroke.hgi-navigation-05::before { - content: "\f21f8"; -} - -.hgi-stroke.hgi-navigation-06::before { - content: "\f21f9"; -} - -.hgi-stroke.hgi-navigation-07::before { - content: "\f3293"; -} - -.hgi-stroke.hgi-navigation-2::before { - content: "\f21fa"; -} - -.hgi-stroke.hgi-navigation-off-01::before { - content: "\f3294"; -} - -.hgi-stroke.hgi-navigation-off::before { - content: "\f21fb"; -} - -.hgi-stroke.hgi-navigation-two-off::before { - content: "\f21fc"; -} - -.hgi-stroke.hgi-navigation::before { - content: "\f21fd"; -} - -.hgi-stroke.hgi-navigation2-off::before { - content: "\f3296"; -} - -.hgi-stroke.hgi-navigation2::before { - content: "\f3295"; -} - -.hgi-stroke.hgi-navigator-01::before { - content: "\f21fe"; -} - -.hgi-stroke.hgi-navigator-02::before { - content: "\f21ff"; -} - -.hgi-stroke.hgi-necklace::before { - content: "\f2200"; -} - -.hgi-stroke.hgi-nerd::before { - content: "\f2201"; -} - -.hgi-stroke.hgi-network::before { - content: "\f3297"; -} - -.hgi-stroke.hgi-neural-network::before { - content: "\f2202"; -} - -.hgi-stroke.hgi-neutral::before { - content: "\f2203"; -} - -.hgi-stroke.hgi-new-job::before { - content: "\f2204"; -} - -.hgi-stroke.hgi-new-office::before { - content: "\f2205"; -} - -.hgi-stroke.hgi-new-releases::before { - content: "\f2206"; -} - -.hgi-stroke.hgi-new-twitter-ellipse::before { - content: "\f2207"; -} - -.hgi-stroke.hgi-new-twitter-rectangle::before { - content: "\f2208"; -} - -.hgi-stroke.hgi-new-twitter::before { - content: "\f2209"; -} - -.hgi-stroke.hgi-news-01::before { - content: "\f220a"; -} - -.hgi-stroke.hgi-news::before { - content: "\f220b"; -} - -.hgi-stroke.hgi-newspaper::before { - content: "\f3298"; -} - -.hgi-stroke.hgi-next-week::before { - content: "\f220c"; -} - -.hgi-stroke.hgi-next::before { - content: "\f220d"; -} - -.hgi-stroke.hgi-nfc::before { - content: "\f3299"; -} - -.hgi-stroke.hgi-nike::before { - content: "\f220e"; -} - -.hgi-stroke.hgi-nine-circle::before { - content: "\f220f"; -} - -.hgi-stroke.hgi-nine-square::before { - content: "\f2210"; -} - -.hgi-stroke.hgi-nintendo-switch::before { - content: "\f2211"; -} - -.hgi-stroke.hgi-nintendo::before { - content: "\f2212"; -} - -.hgi-stroke.hgi-niqab::before { - content: "\f2213"; -} - -.hgi-stroke.hgi-no-internet::before { - content: "\f2214"; -} - -.hgi-stroke.hgi-no-meeting-room::before { - content: "\f2215"; -} - -.hgi-stroke.hgi-no-signal::before { - content: "\f2216"; -} - -.hgi-stroke.hgi-node-add::before { - content: "\f2217"; -} - -.hgi-stroke.hgi-node-edit::before { - content: "\f2218"; -} - -.hgi-stroke.hgi-node-move-down::before { - content: "\f2219"; -} - -.hgi-stroke.hgi-node-move-up::before { - content: "\f221a"; -} - -.hgi-stroke.hgi-node-remove::before { - content: "\f221b"; -} - -.hgi-stroke.hgi-non-binary::before { - content: "\f329a"; -} - -.hgi-stroke.hgi-noodles::before { - content: "\f221c"; -} - -.hgi-stroke.hgi-nose::before { - content: "\f221d"; -} - -.hgi-stroke.hgi-not-equal-sign-circle::before { - content: "\f221e"; -} - -.hgi-stroke.hgi-not-equal-sign-square::before { - content: "\f221f"; -} - -.hgi-stroke.hgi-not-equal-sign::before { - content: "\f2220"; -} - -.hgi-stroke.hgi-note-01::before { - content: "\f2221"; -} - -.hgi-stroke.hgi-note-02::before { - content: "\f2222"; -} - -.hgi-stroke.hgi-note-03::before { - content: "\f2223"; -} - -.hgi-stroke.hgi-note-04::before { - content: "\f2224"; -} - -.hgi-stroke.hgi-note-05::before { - content: "\f2225"; -} - -.hgi-stroke.hgi-note-add::before { - content: "\f2226"; -} - -.hgi-stroke.hgi-note-done::before { - content: "\f2227"; -} - -.hgi-stroke.hgi-note-edit::before { - content: "\f2228"; -} - -.hgi-stroke.hgi-note-remove::before { - content: "\f2229"; -} - -.hgi-stroke.hgi-note::before { - content: "\f222a"; -} - -.hgi-stroke.hgi-notebook-01::before { - content: "\f222b"; -} - -.hgi-stroke.hgi-notebook-02::before { - content: "\f222c"; -} - -.hgi-stroke.hgi-notebook-pen::before { - content: "\f329c"; -} - -.hgi-stroke.hgi-notebook-tabs::before { - content: "\f329d"; -} - -.hgi-stroke.hgi-notebook-text::before { - content: "\f329e"; -} - -.hgi-stroke.hgi-notebook::before { - content: "\f222d"; -} - -.hgi-stroke.hgi-notepad-text-dashed::before { - content: "\f222e"; -} - -.hgi-stroke.hgi-notepad-text::before { - content: "\f329f"; -} - -.hgi-stroke.hgi-notification-01::before { - content: "\f222f"; -} - -.hgi-stroke.hgi-notification-02::before { - content: "\f2230"; -} - -.hgi-stroke.hgi-notification-03::before { - content: "\f2231"; -} - -.hgi-stroke.hgi-notification-block-01::before { - content: "\f2232"; -} - -.hgi-stroke.hgi-notification-block-02::before { - content: "\f2233"; -} - -.hgi-stroke.hgi-notification-block-03::before { - content: "\f2234"; -} - -.hgi-stroke.hgi-notification-bubble::before { - content: "\f2235"; -} - -.hgi-stroke.hgi-notification-circle::before { - content: "\f2236"; -} - -.hgi-stroke.hgi-notification-off-01::before { - content: "\f2237"; -} - -.hgi-stroke.hgi-notification-off-02::before { - content: "\f2238"; -} - -.hgi-stroke.hgi-notification-off-03::before { - content: "\f2239"; -} - -.hgi-stroke.hgi-notification-snooze-01::before { - content: "\f223a"; -} - -.hgi-stroke.hgi-notification-snooze-02::before { - content: "\f223b"; -} - -.hgi-stroke.hgi-notification-snooze-03::before { - content: "\f223c"; -} - -.hgi-stroke.hgi-notification-square::before { - content: "\f223d"; -} - -.hgi-stroke.hgi-notion-01::before { - content: "\f223e"; -} - -.hgi-stroke.hgi-notion-02::before { - content: "\f223f"; -} - -.hgi-stroke.hgi-npm::before { - content: "\f2240"; -} - -.hgi-stroke.hgi-nuclear-power::before { - content: "\f2241"; -} - -.hgi-stroke.hgi-nut-off::before { - content: "\f2242"; -} - -.hgi-stroke.hgi-nut::before { - content: "\f2243"; -} - -.hgi-stroke.hgi-obtuse::before { - content: "\f224a"; -} - -.hgi-stroke.hgi-octagon-alert::before { - content: "\f32a0"; -} - -.hgi-stroke.hgi-octagon-minus::before { - content: "\f32a1"; -} - -.hgi-stroke.hgi-octagon-pause::before { - content: "\f32a2"; -} - -.hgi-stroke.hgi-octagon-x::before { - content: "\f224b"; -} - -.hgi-stroke.hgi-octagon::before { - content: "\f224c"; -} - -.hgi-stroke.hgi-octopus::before { - content: "\f224d"; -} - -.hgi-stroke.hgi-office-365::before { - content: "\f224e"; -} - -.hgi-stroke.hgi-office-chair::before { - content: "\f224f"; -} - -.hgi-stroke.hgi-office::before { - content: "\f2250"; -} - -.hgi-stroke.hgi-oil-barrel::before { - content: "\f2251"; -} - -.hgi-stroke.hgi-ok-finger::before { - content: "\f2252"; -} - -.hgi-stroke.hgi-olympic-torch::before { - content: "\f2253"; -} - -.hgi-stroke.hgi-omega::before { - content: "\f32a3"; -} - -.hgi-stroke.hgi-one-circle::before { - content: "\f2254"; -} - -.hgi-stroke.hgi-one-square::before { - content: "\f2255"; -} - -.hgi-stroke.hgi-online-learning-01::before { - content: "\f2256"; -} - -.hgi-stroke.hgi-online-learning-02::before { - content: "\f2257"; -} - -.hgi-stroke.hgi-online-learning-03::before { - content: "\f2258"; -} - -.hgi-stroke.hgi-online-learning-04::before { - content: "\f2259"; -} - -.hgi-stroke.hgi-open-caption::before { - content: "\f225a"; -} - -.hgi-stroke.hgi-open-source::before { - content: "\f225b"; -} - -.hgi-stroke.hgi-option::before { - content: "\f225c"; -} - -.hgi-stroke.hgi-options-call::before { - content: "\f225d"; -} - -.hgi-stroke.hgi-orange::before { - content: "\f225e"; -} - -.hgi-stroke.hgi-orbit-01-2::before { - content: "\f32a4"; -} - -.hgi-stroke.hgi-orbit-01::before { - content: "\f225f"; -} - -.hgi-stroke.hgi-orbit-02-2::before { - content: "\f32a5"; -} - -.hgi-stroke.hgi-orbit-02::before { - content: "\f2260"; -} - -.hgi-stroke.hgi-organic-food::before { - content: "\f2261"; -} - -.hgi-stroke.hgi-orientation-image-landscape-to-potrait::before { - content: "\f2262"; -} - -.hgi-stroke.hgi-orientation-image-potrait-to-landscape::before { - content: "\f2263"; -} - -.hgi-stroke.hgi-orientation-landscape-to-potrait::before { - content: "\f2264"; -} - -.hgi-stroke.hgi-orientation-potrait-to-landscape::before { - content: "\f2265"; -} - -.hgi-stroke.hgi-origami::before { - content: "\f2266"; -} - -.hgi-stroke.hgi-orthogonal-edge::before { - content: "\f2267"; -} - -.hgi-stroke.hgi-oval::before { - content: "\f2268"; -} - -.hgi-stroke.hgi-oven::before { - content: "\f2269"; -} - -.hgi-stroke.hgi-package-01::before { - content: "\f2289"; -} - -.hgi-stroke.hgi-package-02::before { - content: "\f228a"; -} - -.hgi-stroke.hgi-package-03::before { - content: "\f228b"; -} - -.hgi-stroke.hgi-package-04::before { - content: "\f32a6"; -} - -.hgi-stroke.hgi-package-add-01::before { - content: "\f228c"; -} - -.hgi-stroke.hgi-package-add::before { - content: "\f228d"; -} - -.hgi-stroke.hgi-package-check::before { - content: "\f32a7"; -} - -.hgi-stroke.hgi-package-delivered-01::before { - content: "\f32a8"; -} - -.hgi-stroke.hgi-package-delivered::before { - content: "\f228e"; -} - -.hgi-stroke.hgi-package-dimensions-01::before { - content: "\f228f"; -} - -.hgi-stroke.hgi-package-dimensions-02::before { - content: "\f2290"; -} - -.hgi-stroke.hgi-package-minus::before { - content: "\f32a9"; -} - -.hgi-stroke.hgi-package-moving-01::before { - content: "\f2291"; -} - -.hgi-stroke.hgi-package-moving::before { - content: "\f2292"; -} - -.hgi-stroke.hgi-package-open::before { - content: "\f2293"; -} - -.hgi-stroke.hgi-package-out-of-stock::before { - content: "\f2294"; -} - -.hgi-stroke.hgi-package-plus::before { - content: "\f32aa"; -} - -.hgi-stroke.hgi-package-process-01::before { - content: "\f2295"; -} - -.hgi-stroke.hgi-package-process::before { - content: "\f2296"; -} - -.hgi-stroke.hgi-package-receive-01::before { - content: "\f2297"; -} - -.hgi-stroke.hgi-package-receive::before { - content: "\f2298"; -} - -.hgi-stroke.hgi-package-remove-01::before { - content: "\f2299"; -} - -.hgi-stroke.hgi-package-remove::before { - content: "\f229a"; -} - -.hgi-stroke.hgi-package-search-01::before { - content: "\f229b"; -} - -.hgi-stroke.hgi-package-search::before { - content: "\f229c"; -} - -.hgi-stroke.hgi-package-sent-01::before { - content: "\f229d"; -} - -.hgi-stroke.hgi-package-sent::before { - content: "\f229e"; -} - -.hgi-stroke.hgi-package-x::before { - content: "\f32ab"; -} - -.hgi-stroke.hgi-package::before { - content: "\f229f"; -} - -.hgi-stroke.hgi-package2::before { - content: "\f32ac"; -} - -.hgi-stroke.hgi-packaging::before { - content: "\f22a0"; -} - -.hgi-stroke.hgi-pacman-01::before { - content: "\f22a1"; -} - -.hgi-stroke.hgi-pacman-02::before { - content: "\f22a2"; -} - -.hgi-stroke.hgi-paella::before { - content: "\f22a3"; -} - -.hgi-stroke.hgi-paint-board::before { - content: "\f22a4"; -} - -.hgi-stroke.hgi-paint-brush-01::before { - content: "\f22a5"; -} - -.hgi-stroke.hgi-paint-brush-02::before { - content: "\f22a6"; -} - -.hgi-stroke.hgi-paint-brush-03::before { - content: "\f22a7"; -} - -.hgi-stroke.hgi-paint-brush-04::before { - content: "\f22a8"; -} - -.hgi-stroke.hgi-paint-bucket::before { - content: "\f22a9"; -} - -.hgi-stroke.hgi-paint-roller::before { - content: "\f32ad"; -} - -.hgi-stroke.hgi-paintbrush-vertical::before { - content: "\f32af"; -} - -.hgi-stroke.hgi-paintbrush::before { - content: "\f32ae"; -} - -.hgi-stroke.hgi-palette::before { - content: "\f32b0"; -} - -.hgi-stroke.hgi-pan-01::before { - content: "\f22aa"; -} - -.hgi-stroke.hgi-pan-02::before { - content: "\f22ab"; -} - -.hgi-stroke.hgi-pan-03::before { - content: "\f22ac"; -} - -.hgi-stroke.hgi-panda::before { - content: "\f32b1"; -} - -.hgi-stroke.hgi-panel-bottom-close::before { - content: "\f22ad"; -} - -.hgi-stroke.hgi-panel-bottom-dashed::before { - content: "\f32b3"; -} - -.hgi-stroke.hgi-panel-bottom-open::before { - content: "\f32b4"; -} - -.hgi-stroke.hgi-panel-bottom::before { - content: "\f32b2"; -} - -.hgi-stroke.hgi-panel-left-close::before { - content: "\f22ae"; -} - -.hgi-stroke.hgi-panel-left-dashed::before { - content: "\f22af"; -} - -.hgi-stroke.hgi-panel-left-open::before { - content: "\f22b0"; -} - -.hgi-stroke.hgi-panel-left-right-dashed::before { - content: "\f32b5"; -} - -.hgi-stroke.hgi-panel-left::before { - content: "\f22b1"; -} - -.hgi-stroke.hgi-panel-right-close::before { - content: "\f22b2"; -} - -.hgi-stroke.hgi-panel-right-dashed::before { - content: "\f22b3"; -} - -.hgi-stroke.hgi-panel-right-open::before { - content: "\f22b4"; -} - -.hgi-stroke.hgi-panel-right::before { - content: "\f22b5"; -} - -.hgi-stroke.hgi-panel-top-bottom-dashed::before { - content: "\f22b6"; -} - -.hgi-stroke.hgi-panel-top-close::before { - content: "\f32b7"; -} - -.hgi-stroke.hgi-panel-top-dashed::before { - content: "\f32b8"; -} - -.hgi-stroke.hgi-panel-top-open::before { - content: "\f32b9"; -} - -.hgi-stroke.hgi-panel-top::before { - content: "\f32b6"; -} - -.hgi-stroke.hgi-panels-left-bottom::before { - content: "\f32ba"; -} - -.hgi-stroke.hgi-panels-right-bottom::before { - content: "\f32bb"; -} - -.hgi-stroke.hgi-panels-top-left::before { - content: "\f32bc"; -} - -.hgi-stroke.hgi-paperclip::before { - content: "\f32bd"; -} - -.hgi-stroke.hgi-parabola-01::before { - content: "\f22b7"; -} - -.hgi-stroke.hgi-parabola-02::before { - content: "\f22b8"; -} - -.hgi-stroke.hgi-parabola-03::before { - content: "\f22b9"; -} - -.hgi-stroke.hgi-paragliding::before { - content: "\f22ba"; -} - -.hgi-stroke.hgi-paragraph-bullets-point-01::before { - content: "\f22bb"; -} - -.hgi-stroke.hgi-paragraph-bullets-point-02::before { - content: "\f22bc"; -} - -.hgi-stroke.hgi-paragraph-spacing::before { - content: "\f22bd"; -} - -.hgi-stroke.hgi-paragraph::before { - content: "\f22be"; -} - -.hgi-stroke.hgi-parallelogram::before { - content: "\f22bf"; -} - -.hgi-stroke.hgi-parentheses::before { - content: "\f32be"; -} - -.hgi-stroke.hgi-parking-area-circle::before { - content: "\f22c0"; -} - -.hgi-stroke.hgi-parking-area-square::before { - content: "\f22c1"; -} - -.hgi-stroke.hgi-parking-meter::before { - content: "\f22c2"; -} - -.hgi-stroke.hgi-party-popper::before { - content: "\f32bf"; -} - -.hgi-stroke.hgi-party::before { - content: "\f22c3"; -} - -.hgi-stroke.hgi-passport-01::before { - content: "\f22c4"; -} - -.hgi-stroke.hgi-passport-expired::before { - content: "\f22c5"; -} - -.hgi-stroke.hgi-passport-valid::before { - content: "\f22c6"; -} - -.hgi-stroke.hgi-passport::before { - content: "\f22c7"; -} - -.hgi-stroke.hgi-password-validation::before { - content: "\f22c8"; -} - -.hgi-stroke.hgi-path::before { - content: "\f22c9"; -} - -.hgi-stroke.hgi-pathfinder-crop::before { - content: "\f22ca"; -} - -.hgi-stroke.hgi-pathfinder-divide::before { - content: "\f22cb"; -} - -.hgi-stroke.hgi-pathfinder-exclude::before { - content: "\f22cc"; -} - -.hgi-stroke.hgi-pathfinder-intersect::before { - content: "\f22cd"; -} - -.hgi-stroke.hgi-pathfinder-merge::before { - content: "\f22ce"; -} - -.hgi-stroke.hgi-pathfinder-minus-back::before { - content: "\f22cf"; -} - -.hgi-stroke.hgi-pathfinder-minus-front::before { - content: "\f22d0"; -} - -.hgi-stroke.hgi-pathfinder-outline::before { - content: "\f22d1"; -} - -.hgi-stroke.hgi-pathfinder-trim::before { - content: "\f22d2"; -} - -.hgi-stroke.hgi-pathfinder-unite::before { - content: "\f22d3"; -} - -.hgi-stroke.hgi-patient::before { - content: "\f22d4"; -} - -.hgi-stroke.hgi-patio::before { - content: "\f22d5"; -} - -.hgi-stroke.hgi-pause-circle::before { - content: "\f22d6"; -} - -.hgi-stroke.hgi-pause::before { - content: "\f22d7"; -} - -.hgi-stroke.hgi-pavilon::before { - content: "\f22d8"; -} - -.hgi-stroke.hgi-paw-print::before { - content: "\f32c0"; -} - -.hgi-stroke.hgi-pay-by-check::before { - content: "\f22d9"; -} - -.hgi-stroke.hgi-payment-01::before { - content: "\f22da"; -} - -.hgi-stroke.hgi-payment-02::before { - content: "\f22db"; -} - -.hgi-stroke.hgi-payment-success-01::before { - content: "\f22dc"; -} - -.hgi-stroke.hgi-payment-success-02::before { - content: "\f22dd"; -} - -.hgi-stroke.hgi-payoneer::before { - content: "\f22de"; -} - -.hgi-stroke.hgi-paypal::before { - content: "\f22df"; -} - -.hgi-stroke.hgi-pc-case::before { - content: "\f22e0"; -} - -.hgi-stroke.hgi-pdf-01::before { - content: "\f22e1"; -} - -.hgi-stroke.hgi-pdf-02::before { - content: "\f22e2"; -} - -.hgi-stroke.hgi-peer-to-peer-01::before { - content: "\f22e3"; -} - -.hgi-stroke.hgi-peer-to-peer-02::before { - content: "\f22e4"; -} - -.hgi-stroke.hgi-peer-to-peer-03::before { - content: "\f22e5"; -} - -.hgi-stroke.hgi-pen-01::before { - content: "\f22e6"; -} - -.hgi-stroke.hgi-pen-02::before { - content: "\f22e7"; -} - -.hgi-stroke.hgi-pen-connect-bluetooth::before { - content: "\f22e8"; -} - -.hgi-stroke.hgi-pen-connect-usb::before { - content: "\f22e9"; -} - -.hgi-stroke.hgi-pen-connect-wifi::before { - content: "\f22ea"; -} - -.hgi-stroke.hgi-pen-line::before { - content: "\f32c1"; -} - -.hgi-stroke.hgi-pen-off::before { - content: "\f32c2"; -} - -.hgi-stroke.hgi-pen-tool-01::before { - content: "\f22eb"; -} - -.hgi-stroke.hgi-pen-tool-02::before { - content: "\f22ec"; -} - -.hgi-stroke.hgi-pen-tool-03::before { - content: "\f22ed"; -} - -.hgi-stroke.hgi-pen-tool-add::before { - content: "\f22ee"; -} - -.hgi-stroke.hgi-pen-tool-minus::before { - content: "\f22ef"; -} - -.hgi-stroke.hgi-pencil-edit-01::before { - content: "\f22f0"; -} - -.hgi-stroke.hgi-pencil-edit-02::before { - content: "\f22f1"; -} - -.hgi-stroke.hgi-pencil-line::before { - content: "\f32c3"; -} - -.hgi-stroke.hgi-pencil-off::before { - content: "\f32c4"; -} - -.hgi-stroke.hgi-pencil-ruler::before { - content: "\f22f2"; -} - -.hgi-stroke.hgi-pencil::before { - content: "\f22f3"; -} - -.hgi-stroke.hgi-pendulum::before { - content: "\f22f4"; -} - -.hgi-stroke.hgi-pensive::before { - content: "\f22f5"; -} - -.hgi-stroke.hgi-pentagon-01::before { - content: "\f22f6"; -} - -.hgi-stroke.hgi-pentagon::before { - content: "\f22f7"; -} - -.hgi-stroke.hgi-pepper::before { - content: "\f32c5"; -} - -.hgi-stroke.hgi-percent-circle::before { - content: "\f22f8"; -} - -.hgi-stroke.hgi-percent-square::before { - content: "\f22f9"; -} - -.hgi-stroke.hgi-percent::before { - content: "\f22fa"; -} - -.hgi-stroke.hgi-perfume::before { - content: "\f22fb"; -} - -.hgi-stroke.hgi-periscope::before { - content: "\f22fc"; -} - -.hgi-stroke.hgi-permanent-job::before { - content: "\f22fd"; -} - -.hgi-stroke.hgi-perplexity-ai::before { - content: "\f22fe"; -} - -.hgi-stroke.hgi-person-standing::before { - content: "\f32c6"; -} - -.hgi-stroke.hgi-perspective::before { - content: "\f22ff"; -} - -.hgi-stroke.hgi-petrol-pump::before { - content: "\f2300"; -} - -.hgi-stroke.hgi-pexels::before { - content: "\f2301"; -} - -.hgi-stroke.hgi-philippine-peso::before { - content: "\f2302"; -} - -.hgi-stroke.hgi-phone-arrow-down::before { - content: "\f2303"; -} - -.hgi-stroke.hgi-phone-arrow-up::before { - content: "\f2304"; -} - -.hgi-stroke.hgi-phone-call::before { - content: "\f32c8"; -} - -.hgi-stroke.hgi-phone-check::before { - content: "\f2305"; -} - -.hgi-stroke.hgi-phone-developer-mode::before { - content: "\f2306"; -} - -.hgi-stroke.hgi-phone-erase::before { - content: "\f2307"; -} - -.hgi-stroke.hgi-phone-forwarded::before { - content: "\f32c9"; -} - -.hgi-stroke.hgi-phone-incoming::before { - content: "\f32ca"; -} - -.hgi-stroke.hgi-phone-lock::before { - content: "\f2308"; -} - -.hgi-stroke.hgi-phone-missed::before { - content: "\f32cb"; -} - -.hgi-stroke.hgi-phone-off-01::before { - content: "\f2309"; -} - -.hgi-stroke.hgi-phone-off-02::before { - content: "\f230a"; -} - -.hgi-stroke.hgi-phone-outgoing::before { - content: "\f32cc"; -} - -.hgi-stroke.hgi-phone-shake::before { - content: "\f32cd"; -} - -.hgi-stroke.hgi-phone::before { - content: "\f32c7"; -} - -.hgi-stroke.hgi-php::before { - content: "\f230b"; -} - -.hgi-stroke.hgi-physics::before { - content: "\f230c"; -} - -.hgi-stroke.hgi-pi-circle::before { - content: "\f230d"; -} - -.hgi-stroke.hgi-pi-square::before { - content: "\f230e"; -} - -.hgi-stroke.hgi-pi::before { - content: "\f230f"; -} - -.hgi-stroke.hgi-picasa::before { - content: "\f2310"; -} - -.hgi-stroke.hgi-pickaxe::before { - content: "\f32ce"; -} - -.hgi-stroke.hgi-pickup-01::before { - content: "\f2311"; -} - -.hgi-stroke.hgi-pickup-02::before { - content: "\f2312"; -} - -.hgi-stroke.hgi-picture-in-picture-01::before { - content: "\f32d0"; -} - -.hgi-stroke.hgi-picture-in-picture-exit::before { - content: "\f2313"; -} - -.hgi-stroke.hgi-picture-in-picture-on::before { - content: "\f2314"; -} - -.hgi-stroke.hgi-picture-in-picture::before { - content: "\f32cf"; -} - -.hgi-stroke.hgi-picture-in-picture2::before { - content: "\f32d3"; -} - -.hgi-stroke.hgi-pie-chart-01::before { - content: "\f2315"; -} - -.hgi-stroke.hgi-pie-chart-02::before { - content: "\f2316"; -} - -.hgi-stroke.hgi-pie-chart-03::before { - content: "\f2317"; -} - -.hgi-stroke.hgi-pie-chart-04::before { - content: "\f2318"; -} - -.hgi-stroke.hgi-pie-chart-05-2::before { - content: "\f32d4"; -} - -.hgi-stroke.hgi-pie-chart-05::before { - content: "\f2319"; -} - -.hgi-stroke.hgi-pie-chart-06-2::before { - content: "\f32d5"; -} - -.hgi-stroke.hgi-pie-chart-06::before { - content: "\f231a"; -} - -.hgi-stroke.hgi-pie-chart-07-2::before { - content: "\f32d6"; -} - -.hgi-stroke.hgi-pie-chart-07::before { - content: "\f231b"; -} - -.hgi-stroke.hgi-pie-chart-08-2::before { - content: "\f32d7"; -} - -.hgi-stroke.hgi-pie-chart-08::before { - content: "\f231c"; -} - -.hgi-stroke.hgi-pie-chart-09::before { - content: "\f231d"; -} - -.hgi-stroke.hgi-pie-chart-square::before { - content: "\f231e"; -} - -.hgi-stroke.hgi-pie-chart::before { - content: "\f231f"; -} - -.hgi-stroke.hgi-pie::before { - content: "\f2320"; -} - -.hgi-stroke.hgi-piggy-bank::before { - content: "\f2321"; -} - -.hgi-stroke.hgi-pilcrow-left::before { - content: "\f32d9"; -} - -.hgi-stroke.hgi-pilcrow-right::before { - content: "\f32da"; -} - -.hgi-stroke.hgi-pilcrow::before { - content: "\f32d8"; -} - -.hgi-stroke.hgi-pill-bottle::before { - content: "\f2322"; -} - -.hgi-stroke.hgi-pill-off::before { - content: "\f2323"; -} - -.hgi-stroke.hgi-pill::before { - content: "\f2324"; -} - -.hgi-stroke.hgi-pillow::before { - content: "\f32db"; -} - -.hgi-stroke.hgi-pills-tablet::before { - content: "\f2325"; -} - -.hgi-stroke.hgi-pin-02::before { - content: "\f2326"; -} - -.hgi-stroke.hgi-pin-code::before { - content: "\f2327"; -} - -.hgi-stroke.hgi-pin-location-01::before { - content: "\f2328"; -} - -.hgi-stroke.hgi-pin-location-02::before { - content: "\f2329"; -} - -.hgi-stroke.hgi-pin-location-03::before { - content: "\f232a"; -} - -.hgi-stroke.hgi-pin-off::before { - content: "\f232b"; -} - -.hgi-stroke.hgi-pin::before { - content: "\f232c"; -} - -.hgi-stroke.hgi-pine-tree::before { - content: "\f232d"; -} - -.hgi-stroke.hgi-pinterest::before { - content: "\f232e"; -} - -.hgi-stroke.hgi-pipeline::before { - content: "\f232f"; -} - -.hgi-stroke.hgi-pipette::before { - content: "\f32dc"; -} - -.hgi-stroke.hgi-pisa-tower::before { - content: "\f2330"; -} - -.hgi-stroke.hgi-pivot::before { - content: "\f2331"; -} - -.hgi-stroke.hgi-pizza-01::before { - content: "\f2332"; -} - -.hgi-stroke.hgi-pizza-02-2::before { - content: "\f32dd"; -} - -.hgi-stroke.hgi-pizza-02::before { - content: "\f2333"; -} - -.hgi-stroke.hgi-pizza-03::before { - content: "\f2334"; -} - -.hgi-stroke.hgi-pizza-04::before { - content: "\f2335"; -} - -.hgi-stroke.hgi-pizza-cutter::before { - content: "\f2336"; -} - -.hgi-stroke.hgi-plane-landing::before { - content: "\f32de"; -} - -.hgi-stroke.hgi-plane-takeoff::before { - content: "\f32df"; -} - -.hgi-stroke.hgi-plane::before { - content: "\f2337"; -} - -.hgi-stroke.hgi-plant-01::before { - content: "\f2338"; -} - -.hgi-stroke.hgi-plant-02::before { - content: "\f2339"; -} - -.hgi-stroke.hgi-plant-03::before { - content: "\f233a"; -} - -.hgi-stroke.hgi-plant-04::before { - content: "\f233b"; -} - -.hgi-stroke.hgi-plate::before { - content: "\f233c"; -} - -.hgi-stroke.hgi-plaxo::before { - content: "\f233d"; -} - -.hgi-stroke.hgi-play-circle-02::before { - content: "\f233e"; -} - -.hgi-stroke.hgi-play-circle::before { - content: "\f233f"; -} - -.hgi-stroke.hgi-play-list-add::before { - content: "\f2340"; -} - -.hgi-stroke.hgi-play-list-favourite-01::before { - content: "\f2341"; -} - -.hgi-stroke.hgi-play-list-favourite-02::before { - content: "\f2342"; -} - -.hgi-stroke.hgi-play-list-minus::before { - content: "\f2343"; -} - -.hgi-stroke.hgi-play-list-remove::before { - content: "\f2344"; -} - -.hgi-stroke.hgi-play-list::before { - content: "\f2345"; -} - -.hgi-stroke.hgi-play-square::before { - content: "\f2346"; -} - -.hgi-stroke.hgi-play-store::before { - content: "\f2347"; -} - -.hgi-stroke.hgi-play::before { - content: "\f2348"; -} - -.hgi-stroke.hgi-playlist-01::before { - content: "\f2349"; -} - -.hgi-stroke.hgi-playlist-02::before { - content: "\f234a"; -} - -.hgi-stroke.hgi-playlist-03::before { - content: "\f234b"; -} - -.hgi-stroke.hgi-plaza::before { - content: "\f234c"; -} - -.hgi-stroke.hgi-plug-01::before { - content: "\f234d"; -} - -.hgi-stroke.hgi-plug-02::before { - content: "\f234e"; -} - -.hgi-stroke.hgi-plug-03::before { - content: "\f32e1"; -} - -.hgi-stroke.hgi-plug-socket::before { - content: "\f234f"; -} - -.hgi-stroke.hgi-plug-zap::before { - content: "\f32e2"; -} - -.hgi-stroke.hgi-plug::before { - content: "\f32e0"; -} - -.hgi-stroke.hgi-plug2::before { - content: "\f32e3"; -} - -.hgi-stroke.hgi-plus-minus-01::before { - content: "\f2350"; -} - -.hgi-stroke.hgi-plus-minus-02::before { - content: "\f2351"; -} - -.hgi-stroke.hgi-plus-minus-03::before { - content: "\f2352"; -} - -.hgi-stroke.hgi-plus-minus-circle-01::before { - content: "\f2353"; -} - -.hgi-stroke.hgi-plus-minus-circle-02::before { - content: "\f2354"; -} - -.hgi-stroke.hgi-plus-minus-square-01::before { - content: "\f2355"; -} - -.hgi-stroke.hgi-plus-minus-square-02::before { - content: "\f2356"; -} - -.hgi-stroke.hgi-plus-minus::before { - content: "\f2357"; -} - -.hgi-stroke.hgi-plus-sign-circle::before { - content: "\f2358"; -} - -.hgi-stroke.hgi-plus-sign-square::before { - content: "\f2359"; -} - -.hgi-stroke.hgi-plus-sign::before { - content: "\f235a"; -} - -.hgi-stroke.hgi-plus::before { - content: "\f32e4"; -} - -.hgi-stroke.hgi-png-01::before { - content: "\f235b"; -} - -.hgi-stroke.hgi-png-02::before { - content: "\f235c"; -} - -.hgi-stroke.hgi-pocket-knife::before { - content: "\f235d"; -} - -.hgi-stroke.hgi-pocket::before { - content: "\f235e"; -} - -.hgi-stroke.hgi-podcast::before { - content: "\f235f"; -} - -.hgi-stroke.hgi-podium::before { - content: "\f2360"; -} - -.hgi-stroke.hgi-pointer-off::before { - content: "\f32e6"; -} - -.hgi-stroke.hgi-pointer::before { - content: "\f32e5"; -} - -.hgi-stroke.hgi-pointing-left-01::before { - content: "\f2361"; -} - -.hgi-stroke.hgi-pointing-left-02::before { - content: "\f2362"; -} - -.hgi-stroke.hgi-pointing-left-03::before { - content: "\f2363"; -} - -.hgi-stroke.hgi-pointing-left-04::before { - content: "\f2364"; -} - -.hgi-stroke.hgi-pointing-left-05::before { - content: "\f2365"; -} - -.hgi-stroke.hgi-pointing-left-06::before { - content: "\f2366"; -} - -.hgi-stroke.hgi-pointing-left-07::before { - content: "\f2367"; -} - -.hgi-stroke.hgi-pointing-left-08::before { - content: "\f2368"; -} - -.hgi-stroke.hgi-pointing-right-01::before { - content: "\f2369"; -} - -.hgi-stroke.hgi-pointing-right-02::before { - content: "\f236a"; -} - -.hgi-stroke.hgi-pointing-right-03::before { - content: "\f236b"; -} - -.hgi-stroke.hgi-pointing-right-04::before { - content: "\f236c"; -} - -.hgi-stroke.hgi-pointing-right-05::before { - content: "\f236d"; -} - -.hgi-stroke.hgi-pointing-right-06::before { - content: "\f236e"; -} - -.hgi-stroke.hgi-pointing-right-07::before { - content: "\f236f"; -} - -.hgi-stroke.hgi-pointing-right-08::before { - content: "\f2370"; -} - -.hgi-stroke.hgi-pokeball::before { - content: "\f2371"; -} - -.hgi-stroke.hgi-pokemon::before { - content: "\f2372"; -} - -.hgi-stroke.hgi-police-badge::before { - content: "\f2373"; -} - -.hgi-stroke.hgi-police-cap::before { - content: "\f2374"; -} - -.hgi-stroke.hgi-police-car::before { - content: "\f2375"; -} - -.hgi-stroke.hgi-police-station::before { - content: "\f2376"; -} - -.hgi-stroke.hgi-policy::before { - content: "\f2377"; -} - -.hgi-stroke.hgi-poly-tank::before { - content: "\f2378"; -} - -.hgi-stroke.hgi-polygon::before { - content: "\f2379"; -} - -.hgi-stroke.hgi-pool-table::before { - content: "\f237a"; -} - -.hgi-stroke.hgi-pool::before { - content: "\f237b"; -} - -.hgi-stroke.hgi-poop::before { - content: "\f237c"; -} - -.hgi-stroke.hgi-popcorn::before { - content: "\f237d"; -} - -.hgi-stroke.hgi-popsicle::before { - content: "\f237e"; -} - -.hgi-stroke.hgi-pot-01::before { - content: "\f237f"; -} - -.hgi-stroke.hgi-pot-02::before { - content: "\f2380"; -} - -.hgi-stroke.hgi-potion::before { - content: "\f2381"; -} - -.hgi-stroke.hgi-pound-circle::before { - content: "\f2382"; -} - -.hgi-stroke.hgi-pound-receive::before { - content: "\f2383"; -} - -.hgi-stroke.hgi-pound-send::before { - content: "\f2384"; -} - -.hgi-stroke.hgi-pound-square::before { - content: "\f2385"; -} - -.hgi-stroke.hgi-pound-sterling::before { - content: "\f32e7"; -} - -.hgi-stroke.hgi-pound::before { - content: "\f2386"; -} - -.hgi-stroke.hgi-power-off::before { - content: "\f2387"; -} - -.hgi-stroke.hgi-power-service::before { - content: "\f2388"; -} - -.hgi-stroke.hgi-power-socket-01::before { - content: "\f2389"; -} - -.hgi-stroke.hgi-power-socket-02::before { - content: "\f238a"; -} - -.hgi-stroke.hgi-power::before { - content: "\f238b"; -} - -.hgi-stroke.hgi-ppt-01::before { - content: "\f238c"; -} - -.hgi-stroke.hgi-ppt-02::before { - content: "\f238d"; -} - -.hgi-stroke.hgi-prawn::before { - content: "\f238e"; -} - -.hgi-stroke.hgi-prayer-rug-01::before { - content: "\f238f"; -} - -.hgi-stroke.hgi-prayer-rug-02::before { - content: "\f2390"; -} - -.hgi-stroke.hgi-preference-horizontal::before { - content: "\f2391"; -} - -.hgi-stroke.hgi-preference-vertical::before { - content: "\f2392"; -} - -.hgi-stroke.hgi-prescription::before { - content: "\f2393"; -} - -.hgi-stroke.hgi-prescriptions::before { - content: "\f2394"; -} - -.hgi-stroke.hgi-presentation-01::before { - content: "\f2395"; -} - -.hgi-stroke.hgi-presentation-02::before { - content: "\f2396"; -} - -.hgi-stroke.hgi-presentation-03::before { - content: "\f2397"; -} - -.hgi-stroke.hgi-presentation-04::before { - content: "\f2398"; -} - -.hgi-stroke.hgi-presentation-05::before { - content: "\f2399"; -} - -.hgi-stroke.hgi-presentation-06::before { - content: "\f239a"; -} - -.hgi-stroke.hgi-presentation-07::before { - content: "\f239b"; -} - -.hgi-stroke.hgi-presentation-bar-chart-01::before { - content: "\f239c"; -} - -.hgi-stroke.hgi-presentation-bar-chart-02::before { - content: "\f239d"; -} - -.hgi-stroke.hgi-presentation-line-chart-01::before { - content: "\f239e"; -} - -.hgi-stroke.hgi-presentation-line-chart-02::before { - content: "\f239f"; -} - -.hgi-stroke.hgi-presentation-online::before { - content: "\f23a0"; -} - -.hgi-stroke.hgi-presentation-podium::before { - content: "\f23a1"; -} - -.hgi-stroke.hgi-previous::before { - content: "\f23a2"; -} - -.hgi-stroke.hgi-printer-check::before { - content: "\f23a3"; -} - -.hgi-stroke.hgi-printer-off::before { - content: "\f23a4"; -} - -.hgi-stroke.hgi-printer-three-d::before { - content: "\f23a5"; -} - -.hgi-stroke.hgi-printer-x::before { - content: "\f23a6"; -} - -.hgi-stroke.hgi-printer::before { - content: "\f23a7"; -} - -.hgi-stroke.hgi-prism-01::before { - content: "\f23a8"; -} - -.hgi-stroke.hgi-prism::before { - content: "\f23a9"; -} - -.hgi-stroke.hgi-prison-guard::before { - content: "\f23aa"; -} - -.hgi-stroke.hgi-prison::before { - content: "\f23ab"; -} - -.hgi-stroke.hgi-prisoner::before { - content: "\f23ac"; -} - -.hgi-stroke.hgi-product-loading::before { - content: "\f23ad"; -} - -.hgi-stroke.hgi-profile-02::before { - content: "\f23ae"; -} - -.hgi-stroke.hgi-profile::before { - content: "\f23af"; -} - -.hgi-stroke.hgi-profit::before { - content: "\f23b0"; -} - -.hgi-stroke.hgi-programming-flag::before { - content: "\f23b1"; -} - -.hgi-stroke.hgi-progress-01::before { - content: "\f23b2"; -} - -.hgi-stroke.hgi-progress-02::before { - content: "\f23b3"; -} - -.hgi-stroke.hgi-progress-03::before { - content: "\f23b4"; -} - -.hgi-stroke.hgi-progress-04::before { - content: "\f23b5"; -} - -.hgi-stroke.hgi-progress::before { - content: "\f23b6"; -} - -.hgi-stroke.hgi-projector-01::before { - content: "\f23b7"; -} - -.hgi-stroke.hgi-projector::before { - content: "\f23b8"; -} - -.hgi-stroke.hgi-promotion::before { - content: "\f23b9"; -} - -.hgi-stroke.hgi-property-add::before { - content: "\f23ba"; -} - -.hgi-stroke.hgi-property-delete::before { - content: "\f23bb"; -} - -.hgi-stroke.hgi-property-edit::before { - content: "\f23bc"; -} - -.hgi-stroke.hgi-property-new::before { - content: "\f23bd"; -} - -.hgi-stroke.hgi-property-search::before { - content: "\f23be"; -} - -.hgi-stroke.hgi-property-view::before { - content: "\f23bf"; -} - -.hgi-stroke.hgi-proportions::before { - content: "\f23c0"; -} - -.hgi-stroke.hgi-protection-mask::before { - content: "\f23c1"; -} - -.hgi-stroke.hgi-pulley::before { - content: "\f23c2"; -} - -.hgi-stroke.hgi-pulse-01::before { - content: "\f23c3"; -} - -.hgi-stroke.hgi-pulse-02::before { - content: "\f23c4"; -} - -.hgi-stroke.hgi-pulse-rectangle-01::before { - content: "\f23c5"; -} - -.hgi-stroke.hgi-pulse-rectangle-02::before { - content: "\f23c6"; -} - -.hgi-stroke.hgi-pumpkin::before { - content: "\f23c7"; -} - -.hgi-stroke.hgi-punch::before { - content: "\f23c8"; -} - -.hgi-stroke.hgi-punching-ball-01::before { - content: "\f23c9"; -} - -.hgi-stroke.hgi-punching-ball-02::before { - content: "\f23ca"; -} - -.hgi-stroke.hgi-purse-01::before { - content: "\f23cb"; -} - -.hgi-stroke.hgi-purse::before { - content: "\f23cc"; -} - -.hgi-stroke.hgi-push-up-bar::before { - content: "\f23cd"; -} - -.hgi-stroke.hgi-puzzle::before { - content: "\f23ce"; -} - -.hgi-stroke.hgi-pyramid-maslowo::before { - content: "\f23cf"; -} - -.hgi-stroke.hgi-pyramid-structure-01::before { - content: "\f23d0"; -} - -.hgi-stroke.hgi-pyramid-structure-02::before { - content: "\f23d1"; -} - -.hgi-stroke.hgi-pyramid::before { - content: "\f23d2"; -} - -.hgi-stroke.hgi-python::before { - content: "\f23d3"; -} - -.hgi-stroke.hgi-qq-plot::before { - content: "\f23d9"; -} - -.hgi-stroke.hgi-qr-code-01::before { - content: "\f23da"; -} - -.hgi-stroke.hgi-qr-code-scan::before { - content: "\f23db"; -} - -.hgi-stroke.hgi-qr-code::before { - content: "\f23dc"; -} - -.hgi-stroke.hgi-question::before { - content: "\f23dd"; -} - -.hgi-stroke.hgi-queue-01::before { - content: "\f23de"; -} - -.hgi-stroke.hgi-queue-02::before { - content: "\f23df"; -} - -.hgi-stroke.hgi-quill-write-01::before { - content: "\f23e0"; -} - -.hgi-stroke.hgi-quill-write-02::before { - content: "\f23e1"; -} - -.hgi-stroke.hgi-quiz-01::before { - content: "\f23e2"; -} - -.hgi-stroke.hgi-quiz-02::before { - content: "\f23e3"; -} - -.hgi-stroke.hgi-quiz-03::before { - content: "\f23e4"; -} - -.hgi-stroke.hgi-quiz-04::before { - content: "\f23e5"; -} - -.hgi-stroke.hgi-quiz-05::before { - content: "\f23e6"; -} - -.hgi-stroke.hgi-quora::before { - content: "\f23e7"; -} - -.hgi-stroke.hgi-quote-down-circle::before { - content: "\f23e8"; -} - -.hgi-stroke.hgi-quote-down-square::before { - content: "\f23e9"; -} - -.hgi-stroke.hgi-quote-down::before { - content: "\f23ea"; -} - -.hgi-stroke.hgi-quote-up-circle::before { - content: "\f23eb"; -} - -.hgi-stroke.hgi-quote-up-square::before { - content: "\f23ec"; -} - -.hgi-stroke.hgi-quote-up::before { - content: "\f23ed"; -} - -.hgi-stroke.hgi-quote::before { - content: "\f32e8"; -} - -.hgi-stroke.hgi-quotes::before { - content: "\f23ee"; -} - -.hgi-stroke.hgi-quran-01::before { - content: "\f23ef"; -} - -.hgi-stroke.hgi-quran-02::before { - content: "\f23f0"; -} - -.hgi-stroke.hgi-quran-03::before { - content: "\f23f1"; -} - -.hgi-stroke.hgi-qwen::before { - content: "\f23f2"; -} - -.hgi-stroke.hgi-rabbit::before { - content: "\f32e9"; -} - -.hgi-stroke.hgi-racing-flag::before { - content: "\f2411"; -} - -.hgi-stroke.hgi-radar-01::before { - content: "\f2412"; -} - -.hgi-stroke.hgi-radar-02::before { - content: "\f2413"; -} - -.hgi-stroke.hgi-radar-03::before { - content: "\f2414"; -} - -.hgi-stroke.hgi-radial::before { - content: "\f2415"; -} - -.hgi-stroke.hgi-radiation::before { - content: "\f2416"; -} - -.hgi-stroke.hgi-radical::before { - content: "\f32ea"; -} - -.hgi-stroke.hgi-radio-01::before { - content: "\f2417"; -} - -.hgi-stroke.hgi-radio-02::before { - content: "\f2418"; -} - -.hgi-stroke.hgi-radio-button::before { - content: "\f2419"; -} - -.hgi-stroke.hgi-radio-off::before { - content: "\f32eb"; -} - -.hgi-stroke.hgi-radio-receiver::before { - content: "\f32ec"; -} - -.hgi-stroke.hgi-radio-tower::before { - content: "\f32ed"; -} - -.hgi-stroke.hgi-radio::before { - content: "\f241a"; -} - -.hgi-stroke.hgi-radioactive-alert::before { - content: "\f241b"; -} - -.hgi-stroke.hgi-radius::before { - content: "\f241c"; -} - -.hgi-stroke.hgi-rail-symbol::before { - content: "\f241d"; -} - -.hgi-stroke.hgi-rain-double-drop::before { - content: "\f241e"; -} - -.hgi-stroke.hgi-rain-drop::before { - content: "\f241f"; -} - -.hgi-stroke.hgi-rain::before { - content: "\f2420"; -} - -.hgi-stroke.hgi-rainbow::before { - content: "\f2421"; -} - -.hgi-stroke.hgi-ram-memory::before { - content: "\f2422"; -} - -.hgi-stroke.hgi-ramadhan-01::before { - content: "\f2423"; -} - -.hgi-stroke.hgi-ramadhan-02::before { - content: "\f2424"; -} - -.hgi-stroke.hgi-ramadhan-month::before { - content: "\f2425"; -} - -.hgi-stroke.hgi-ranking::before { - content: "\f2426"; -} - -.hgi-stroke.hgi-rar-01::before { - content: "\f2427"; -} - -.hgi-stroke.hgi-rar-02::before { - content: "\f2428"; -} - -.hgi-stroke.hgi-rat::before { - content: "\f32ee"; -} - -.hgi-stroke.hgi-ratio::before { - content: "\f2429"; -} - -.hgi-stroke.hgi-raw-01::before { - content: "\f242a"; -} - -.hgi-stroke.hgi-raw-02::before { - content: "\f242b"; -} - -.hgi-stroke.hgi-re::before { - content: "\f242c"; -} - -.hgi-stroke.hgi-react::before { - content: "\f242d"; -} - -.hgi-stroke.hgi-real-estate-01::before { - content: "\f242e"; -} - -.hgi-stroke.hgi-real-estate-02::before { - content: "\f242f"; -} - -.hgi-stroke.hgi-receipt-cent::before { - content: "\f2430"; -} - -.hgi-stroke.hgi-receipt-dollar::before { - content: "\f2431"; -} - -.hgi-stroke.hgi-receipt-euro::before { - content: "\f2432"; -} - -.hgi-stroke.hgi-receipt-indian-rupee::before { - content: "\f2433"; -} - -.hgi-stroke.hgi-receipt-japanese-yen::before { - content: "\f2434"; -} - -.hgi-stroke.hgi-receipt-pound-sterling::before { - content: "\f2435"; -} - -.hgi-stroke.hgi-receipt-russian-ruble::before { - content: "\f2436"; -} - -.hgi-stroke.hgi-receipt-swiss-franc::before { - content: "\f2437"; -} - -.hgi-stroke.hgi-receipt-text::before { - content: "\f2438"; -} - -.hgi-stroke.hgi-receipt-turkish-lira::before { - content: "\f2439"; -} - -.hgi-stroke.hgi-receipt::before { - content: "\f32ef"; -} - -.hgi-stroke.hgi-record::before { - content: "\f243a"; -} - -.hgi-stroke.hgi-recovery-mail::before { - content: "\f243b"; -} - -.hgi-stroke.hgi-rectangle-circle::before { - content: "\f243c"; -} - -.hgi-stroke.hgi-rectangle-ellipsis::before { - content: "\f32f0"; -} - -.hgi-stroke.hgi-rectangle-goggles::before { - content: "\f32f1"; -} - -.hgi-stroke.hgi-rectangle-horizontal::before { - content: "\f32f2"; -} - -.hgi-stroke.hgi-rectangle-vertical::before { - content: "\f32f3"; -} - -.hgi-stroke.hgi-rectangular-01::before { - content: "\f243d"; -} - -.hgi-stroke.hgi-rectangular::before { - content: "\f243e"; -} - -.hgi-stroke.hgi-recycle-01::before { - content: "\f243f"; -} - -.hgi-stroke.hgi-recycle-02::before { - content: "\f2440"; -} - -.hgi-stroke.hgi-recycle-03::before { - content: "\f2441"; -} - -.hgi-stroke.hgi-reddit::before { - content: "\f2442"; -} - -.hgi-stroke.hgi-redo-01::before { - content: "\f32f4"; -} - -.hgi-stroke.hgi-redo-02::before { - content: "\f2443"; -} - -.hgi-stroke.hgi-redo-03::before { - content: "\f2444"; -} - -.hgi-stroke.hgi-redo-dot::before { - content: "\f2445"; -} - -.hgi-stroke.hgi-redo::before { - content: "\f2446"; -} - -.hgi-stroke.hgi-redo2::before { - content: "\f32f5"; -} - -.hgi-stroke.hgi-reduce-paragraph::before { - content: "\f2447"; -} - -.hgi-stroke.hgi-reflex::before { - content: "\f2448"; -} - -.hgi-stroke.hgi-refresh-01::before { - content: "\f2449"; -} - -.hgi-stroke.hgi-refresh-03::before { - content: "\f244a"; -} - -.hgi-stroke.hgi-refresh-04::before { - content: "\f244b"; -} - -.hgi-stroke.hgi-refresh-ccw-dot::before { - content: "\f32f7"; -} - -.hgi-stroke.hgi-refresh-ccw::before { - content: "\f32f6"; -} - -.hgi-stroke.hgi-refresh-cw-off::before { - content: "\f244c"; -} - -.hgi-stroke.hgi-refresh-cw::before { - content: "\f32f8"; -} - -.hgi-stroke.hgi-refresh-dot::before { - content: "\f244d"; -} - -.hgi-stroke.hgi-refresh::before { - content: "\f244e"; -} - -.hgi-stroke.hgi-refrigerator::before { - content: "\f244f"; -} - -.hgi-stroke.hgi-regex::before { - content: "\f2450"; -} - -.hgi-stroke.hgi-register::before { - content: "\f2451"; -} - -.hgi-stroke.hgi-registered::before { - content: "\f2452"; -} - -.hgi-stroke.hgi-relieved-01::before { - content: "\f2453"; -} - -.hgi-stroke.hgi-relieved-02::before { - content: "\f2454"; -} - -.hgi-stroke.hgi-reload::before { - content: "\f2455"; -} - -.hgi-stroke.hgi-reminder::before { - content: "\f2456"; -} - -.hgi-stroke.hgi-remote-control::before { - content: "\f2457"; -} - -.hgi-stroke.hgi-remove-01::before { - content: "\f2458"; -} - -.hgi-stroke.hgi-remove-02::before { - content: "\f2459"; -} - -.hgi-stroke.hgi-remove-circle-half-dot::before { - content: "\f245a"; -} - -.hgi-stroke.hgi-remove-circle::before { - content: "\f245b"; -} - -.hgi-stroke.hgi-remove-female::before { - content: "\f245c"; -} - -.hgi-stroke.hgi-remove-formatting::before { - content: "\f32f9"; -} - -.hgi-stroke.hgi-remove-male::before { - content: "\f245d"; -} - -.hgi-stroke.hgi-remove-pi::before { - content: "\f245e"; -} - -.hgi-stroke.hgi-remove-square::before { - content: "\f245f"; -} - -.hgi-stroke.hgi-renewable-energy-01::before { - content: "\f2460"; -} - -.hgi-stroke.hgi-renewable-energy::before { - content: "\f2461"; -} - -.hgi-stroke.hgi-repair::before { - content: "\f2462"; -} - -.hgi-stroke.hgi-repeat-off::before { - content: "\f2463"; -} - -.hgi-stroke.hgi-repeat-one-01::before { - content: "\f2464"; -} - -.hgi-stroke.hgi-repeat-one-02::before { - content: "\f2465"; -} - -.hgi-stroke.hgi-repeat::before { - content: "\f2466"; -} - -.hgi-stroke.hgi-replace-all::before { - content: "\f32fb"; -} - -.hgi-stroke.hgi-replace::before { - content: "\f32fa"; -} - -.hgi-stroke.hgi-replay::before { - content: "\f2467"; -} - -.hgi-stroke.hgi-replit::before { - content: "\f2468"; -} - -.hgi-stroke.hgi-reply-all::before { - content: "\f32fd"; -} - -.hgi-stroke.hgi-reply::before { - content: "\f32fc"; -} - -.hgi-stroke.hgi-repository::before { - content: "\f2469"; -} - -.hgi-stroke.hgi-repost::before { - content: "\f246a"; -} - -.hgi-stroke.hgi-reset-password::before { - content: "\f246b"; -} - -.hgi-stroke.hgi-resize-01::before { - content: "\f246c"; -} - -.hgi-stroke.hgi-resize-02::before { - content: "\f246d"; -} - -.hgi-stroke.hgi-resize-field-rectangle::before { - content: "\f246e"; -} - -.hgi-stroke.hgi-resize-field::before { - content: "\f246f"; -} - -.hgi-stroke.hgi-resources-add::before { - content: "\f2470"; -} - -.hgi-stroke.hgi-resources-remove::before { - content: "\f2471"; -} - -.hgi-stroke.hgi-restaurant-01::before { - content: "\f2472"; -} - -.hgi-stroke.hgi-restaurant-02::before { - content: "\f2473"; -} - -.hgi-stroke.hgi-restaurant-03::before { - content: "\f2474"; -} - -.hgi-stroke.hgi-restaurant-table::before { - content: "\f2475"; -} - -.hgi-stroke.hgi-restaurant::before { - content: "\f32fe"; -} - -.hgi-stroke.hgi-restore-bin::before { - content: "\f2476"; -} - -.hgi-stroke.hgi-return-request::before { - content: "\f2477"; -} - -.hgi-stroke.hgi-reverse-withdrawal-01::before { - content: "\f2478"; -} - -.hgi-stroke.hgi-reverse-withdrawal-02::before { - content: "\f2479"; -} - -.hgi-stroke.hgi-rewind::before { - content: "\f32ff"; -} - -.hgi-stroke.hgi-rhombus-01::before { - content: "\f247a"; -} - -.hgi-stroke.hgi-rhombus::before { - content: "\f247b"; -} - -.hgi-stroke.hgi-ribbon::before { - content: "\f247c"; -} - -.hgi-stroke.hgi-rice-bowl-01::before { - content: "\f247d"; -} - -.hgi-stroke.hgi-rice-bowl-02::before { - content: "\f247e"; -} - -.hgi-stroke.hgi-right-angle::before { - content: "\f247f"; -} - -.hgi-stroke.hgi-right-to-left-block-quote::before { - content: "\f2480"; -} - -.hgi-stroke.hgi-right-to-left-list-bullet::before { - content: "\f2481"; -} - -.hgi-stroke.hgi-right-to-left-list-dash::before { - content: "\f2482"; -} - -.hgi-stroke.hgi-right-to-left-list-number::before { - content: "\f2483"; -} - -.hgi-stroke.hgi-right-to-left-list-triangle::before { - content: "\f2484"; -} - -.hgi-stroke.hgi-right-triangle::before { - content: "\f2485"; -} - -.hgi-stroke.hgi-ripple::before { - content: "\f2486"; -} - -.hgi-stroke.hgi-riyal-rectangle::before { - content: "\f2487"; -} - -.hgi-stroke.hgi-riyal::before { - content: "\f2488"; -} - -.hgi-stroke.hgi-road-01::before { - content: "\f2489"; -} - -.hgi-stroke.hgi-road-02::before { - content: "\f248a"; -} - -.hgi-stroke.hgi-road-location-01::before { - content: "\f248b"; -} - -.hgi-stroke.hgi-road-location-02::before { - content: "\f248c"; -} - -.hgi-stroke.hgi-road-wayside::before { - content: "\f248d"; -} - -.hgi-stroke.hgi-road::before { - content: "\f248e"; -} - -.hgi-stroke.hgi-roblox::before { - content: "\f3301"; -} - -.hgi-stroke.hgi-robot-01::before { - content: "\f248f"; -} - -.hgi-stroke.hgi-robot-02::before { - content: "\f2490"; -} - -.hgi-stroke.hgi-robotic::before { - content: "\f2491"; -} - -.hgi-stroke.hgi-rocket-01-2::before { - content: "\f3302"; -} - -.hgi-stroke.hgi-rocket-01::before { - content: "\f2492"; -} - -.hgi-stroke.hgi-rocket-02-2::before { - content: "\f3303"; -} - -.hgi-stroke.hgi-rocket-02::before { - content: "\f2493"; -} - -.hgi-stroke.hgi-rocket::before { - content: "\f2494"; -} - -.hgi-stroke.hgi-rocking-chair::before { - content: "\f2495"; -} - -.hgi-stroke.hgi-rocking-horse::before { - content: "\f2496"; -} - -.hgi-stroke.hgi-roller-coaster::before { - content: "\f2497"; -} - -.hgi-stroke.hgi-roller-skate::before { - content: "\f2498"; -} - -.hgi-stroke.hgi-rolling-pin::before { - content: "\f2499"; -} - -.hgi-stroke.hgi-root-01::before { - content: "\f249a"; -} - -.hgi-stroke.hgi-root-02::before { - content: "\f249b"; -} - -.hgi-stroke.hgi-root-circle::before { - content: "\f249c"; -} - -.hgi-stroke.hgi-root-first-bracket::before { - content: "\f249d"; -} - -.hgi-stroke.hgi-root-second-bracket::before { - content: "\f249e"; -} - -.hgi-stroke.hgi-root-third-bracket::before { - content: "\f249f"; -} - -.hgi-stroke.hgi-rose::before { - content: "\f24a0"; -} - -.hgi-stroke.hgi-rotate-01::before { - content: "\f24a1"; -} - -.hgi-stroke.hgi-rotate-02::before { - content: "\f24a2"; -} - -.hgi-stroke.hgi-rotate-360::before { - content: "\f24a3"; -} - -.hgi-stroke.hgi-rotate-bottom-left::before { - content: "\f24a4"; -} - -.hgi-stroke.hgi-rotate-bottom-right::before { - content: "\f24a5"; -} - -.hgi-stroke.hgi-rotate-ccw-badge::before { - content: "\f24a6"; -} - -.hgi-stroke.hgi-rotate-ccw-key::before { - content: "\f3305"; -} - -.hgi-stroke.hgi-rotate-ccw-square::before { - content: "\f24a7"; -} - -.hgi-stroke.hgi-rotate-ccw::before { - content: "\f3304"; -} - -.hgi-stroke.hgi-rotate-clockwise::before { - content: "\f24a8"; -} - -.hgi-stroke.hgi-rotate-crop::before { - content: "\f24a9"; -} - -.hgi-stroke.hgi-rotate-cw-square::before { - content: "\f24aa"; -} - -.hgi-stroke.hgi-rotate-cw::before { - content: "\f3306"; -} - -.hgi-stroke.hgi-rotate-left-01::before { - content: "\f24ab"; -} - -.hgi-stroke.hgi-rotate-left-02::before { - content: "\f24ac"; -} - -.hgi-stroke.hgi-rotate-left-03::before { - content: "\f24ad"; -} - -.hgi-stroke.hgi-rotate-left-04::before { - content: "\f24ae"; -} - -.hgi-stroke.hgi-rotate-left-05::before { - content: "\f24af"; -} - -.hgi-stroke.hgi-rotate-left-06::before { - content: "\f24b0"; -} - -.hgi-stroke.hgi-rotate-right-01::before { - content: "\f24b1"; -} - -.hgi-stroke.hgi-rotate-right-02::before { - content: "\f24b2"; -} - -.hgi-stroke.hgi-rotate-right-03::before { - content: "\f24b3"; -} - -.hgi-stroke.hgi-rotate-right-04::before { - content: "\f24b4"; -} - -.hgi-stroke.hgi-rotate-right-05::before { - content: "\f24b5"; -} - -.hgi-stroke.hgi-rotate-right-06::before { - content: "\f24b6"; -} - -.hgi-stroke.hgi-rotate-square::before { - content: "\f24b7"; -} - -.hgi-stroke.hgi-rotate-three-d::before { - content: "\f3307"; -} - -.hgi-stroke.hgi-rotate-top-left::before { - content: "\f24b8"; -} - -.hgi-stroke.hgi-rotate-top-right::before { - content: "\f24b9"; -} - -.hgi-stroke.hgi-rotate3d::before { - content: "\f3308"; -} - -.hgi-stroke.hgi-route-01::before { - content: "\f24ba"; -} - -.hgi-stroke.hgi-route-02::before { - content: "\f24bb"; -} - -.hgi-stroke.hgi-route-03::before { - content: "\f24bc"; -} - -.hgi-stroke.hgi-route-block::before { - content: "\f24bd"; -} - -.hgi-stroke.hgi-route-off::before { - content: "\f3309"; -} - -.hgi-stroke.hgi-router-01::before { - content: "\f24be"; -} - -.hgi-stroke.hgi-router-02::before { - content: "\f24bf"; -} - -.hgi-stroke.hgi-router::before { - content: "\f24c0"; -} - -.hgi-stroke.hgi-row-delete::before { - content: "\f24c1"; -} - -.hgi-stroke.hgi-row-insert::before { - content: "\f24c2"; -} - -.hgi-stroke.hgi-rows-2::before { - content: "\f330a"; -} - -.hgi-stroke.hgi-rows-3::before { - content: "\f330b"; -} - -.hgi-stroke.hgi-rows-4::before { - content: "\f330c"; -} - -.hgi-stroke.hgi-rows2::before { - content: "\f330d"; -} - -.hgi-stroke.hgi-rows3::before { - content: "\f330e"; -} - -.hgi-stroke.hgi-rows4::before { - content: "\f330f"; -} - -.hgi-stroke.hgi-rss-connected-01::before { - content: "\f24c3"; -} - -.hgi-stroke.hgi-rss-connected-02::before { - content: "\f24c4"; -} - -.hgi-stroke.hgi-rss-error::before { - content: "\f24c5"; -} - -.hgi-stroke.hgi-rss-locked::before { - content: "\f24c6"; -} - -.hgi-stroke.hgi-rss-unlocked::before { - content: "\f24c7"; -} - -.hgi-stroke.hgi-rss::before { - content: "\f24c8"; -} - -.hgi-stroke.hgi-rub-el-hizb::before { - content: "\f24c9"; -} - -.hgi-stroke.hgi-rubber-duck::before { - content: "\f24ca"; -} - -.hgi-stroke.hgi-rubiks-cube::before { - content: "\f24cb"; -} - -.hgi-stroke.hgi-ruku::before { - content: "\f24cc"; -} - -.hgi-stroke.hgi-ruler-dimension-line::before { - content: "\f3310"; -} - -.hgi-stroke.hgi-ruler::before { - content: "\f24cd"; -} - -.hgi-stroke.hgi-running-shoes::before { - content: "\f24ce"; -} - -.hgi-stroke.hgi-rupee-circle::before { - content: "\f3311"; -} - -.hgi-stroke.hgi-rupee-shield::before { - content: "\f24cf"; -} - -.hgi-stroke.hgi-rupee-square::before { - content: "\f3312"; -} - -.hgi-stroke.hgi-rupee::before { - content: "\f24d0"; -} - -.hgi-stroke.hgi-russian-ruble::before { - content: "\f24d1"; -} - -.hgi-stroke.hgi-sad-01::before { - content: "\f2500"; -} - -.hgi-stroke.hgi-sad-02::before { - content: "\f2501"; -} - -.hgi-stroke.hgi-sad-dizzy::before { - content: "\f2502"; -} - -.hgi-stroke.hgi-safari::before { - content: "\f2503"; -} - -.hgi-stroke.hgi-safe-box::before { - content: "\f2504"; -} - -.hgi-stroke.hgi-safe-delivery-01::before { - content: "\f2505"; -} - -.hgi-stroke.hgi-safe-delivery-02::before { - content: "\f2506"; -} - -.hgi-stroke.hgi-safe::before { - content: "\f2507"; -} - -.hgi-stroke.hgi-safety-pin-01::before { - content: "\f2508"; -} - -.hgi-stroke.hgi-safety-pin-02::before { - content: "\f2509"; -} - -.hgi-stroke.hgi-sailboat-coastal::before { - content: "\f250a"; -} - -.hgi-stroke.hgi-sailboat-offshore::before { - content: "\f250b"; -} - -.hgi-stroke.hgi-sailboat::before { - content: "\f3313"; -} - -.hgi-stroke.hgi-sakura::before { - content: "\f250c"; -} - -.hgi-stroke.hgi-salad::before { - content: "\f250d"; -} - -.hgi-stroke.hgi-salah-time::before { - content: "\f250e"; -} - -.hgi-stroke.hgi-salah::before { - content: "\f250f"; -} - -.hgi-stroke.hgi-sale-tag-01::before { - content: "\f2510"; -} - -.hgi-stroke.hgi-sale-tag-02::before { - content: "\f2511"; -} - -.hgi-stroke.hgi-sandals::before { - content: "\f2512"; -} - -.hgi-stroke.hgi-sandwich::before { - content: "\f3314"; -} - -.hgi-stroke.hgi-satellite-01-2::before { - content: "\f3315"; -} - -.hgi-stroke.hgi-satellite-01::before { - content: "\f2513"; -} - -.hgi-stroke.hgi-satellite-02-2::before { - content: "\f3316"; -} - -.hgi-stroke.hgi-satellite-02::before { - content: "\f2514"; -} - -.hgi-stroke.hgi-satellite-03::before { - content: "\f2515"; -} - -.hgi-stroke.hgi-satellite-dish::before { - content: "\f3317"; -} - -.hgi-stroke.hgi-satellite::before { - content: "\f2516"; -} - -.hgi-stroke.hgi-saturn-01-2::before { - content: "\f3318"; -} - -.hgi-stroke.hgi-saturn-01::before { - content: "\f2517"; -} - -.hgi-stroke.hgi-saturn-02-2::before { - content: "\f3319"; -} - -.hgi-stroke.hgi-saturn-02::before { - content: "\f2518"; -} - -.hgi-stroke.hgi-saturn::before { - content: "\f2519"; -} - -.hgi-stroke.hgi-saudi-riyal::before { - content: "\f251a"; -} - -.hgi-stroke.hgi-sausage::before { - content: "\f251b"; -} - -.hgi-stroke.hgi-save-all::before { - content: "\f251c"; -} - -.hgi-stroke.hgi-save-energy-01::before { - content: "\f251d"; -} - -.hgi-stroke.hgi-save-energy-02::before { - content: "\f251e"; -} - -.hgi-stroke.hgi-save-money-dollar::before { - content: "\f251f"; -} - -.hgi-stroke.hgi-save-money-euro::before { - content: "\f2520"; -} - -.hgi-stroke.hgi-save-money-pound::before { - content: "\f2521"; -} - -.hgi-stroke.hgi-save-money-yen::before { - content: "\f2522"; -} - -.hgi-stroke.hgi-save-off::before { - content: "\f2523"; -} - -.hgi-stroke.hgi-save::before { - content: "\f2524"; -} - -.hgi-stroke.hgi-savings::before { - content: "\f2525"; -} - -.hgi-stroke.hgi-scale-three-d::before { - content: "\f331b"; -} - -.hgi-stroke.hgi-scale::before { - content: "\f331a"; -} - -.hgi-stroke.hgi-scale3d::before { - content: "\f331c"; -} - -.hgi-stroke.hgi-scaling::before { - content: "\f331d"; -} - -.hgi-stroke.hgi-scan-eye::before { - content: "\f2526"; -} - -.hgi-stroke.hgi-scan-face::before { - content: "\f331e"; -} - -.hgi-stroke.hgi-scan-heart::before { - content: "\f2527"; -} - -.hgi-stroke.hgi-scan-image::before { - content: "\f2528"; -} - -.hgi-stroke.hgi-scan-line::before { - content: "\f331f"; -} - -.hgi-stroke.hgi-scan-search::before { - content: "\f3320"; -} - -.hgi-stroke.hgi-scan-text::before { - content: "\f3321"; -} - -.hgi-stroke.hgi-scan::before { - content: "\f2529"; -} - -.hgi-stroke.hgi-scheme::before { - content: "\f252a"; -} - -.hgi-stroke.hgi-school-01::before { - content: "\f252b"; -} - -.hgi-stroke.hgi-school-bell-01::before { - content: "\f252c"; -} - -.hgi-stroke.hgi-school-bell-02::before { - content: "\f252d"; -} - -.hgi-stroke.hgi-school-bus::before { - content: "\f252e"; -} - -.hgi-stroke.hgi-school-report-card::before { - content: "\f252f"; -} - -.hgi-stroke.hgi-school-tie::before { - content: "\f2530"; -} - -.hgi-stroke.hgi-school::before { - content: "\f2531"; -} - -.hgi-stroke.hgi-scissor-01::before { - content: "\f2532"; -} - -.hgi-stroke.hgi-scissor-rectangle::before { - content: "\f2533"; -} - -.hgi-stroke.hgi-scissor::before { - content: "\f2534"; -} - -.hgi-stroke.hgi-scissors-line-dashed::before { - content: "\f3323"; -} - -.hgi-stroke.hgi-scissors::before { - content: "\f3322"; -} - -.hgi-stroke.hgi-scooter-01::before { - content: "\f2535"; -} - -.hgi-stroke.hgi-scooter-02::before { - content: "\f2536"; -} - -.hgi-stroke.hgi-scooter-03::before { - content: "\f2537"; -} - -.hgi-stroke.hgi-scooter-04::before { - content: "\f2538"; -} - -.hgi-stroke.hgi-scooter-electric::before { - content: "\f2539"; -} - -.hgi-stroke.hgi-scratch-card::before { - content: "\f253a"; -} - -.hgi-stroke.hgi-screen-add-to-home-02::before { - content: "\f253b"; -} - -.hgi-stroke.hgi-screen-add-to-home::before { - content: "\f253c"; -} - -.hgi-stroke.hgi-screen-lock-rotation::before { - content: "\f253d"; -} - -.hgi-stroke.hgi-screen-rotation::before { - content: "\f253e"; -} - -.hgi-stroke.hgi-screen-share-off::before { - content: "\f3325"; -} - -.hgi-stroke.hgi-screen-share::before { - content: "\f3324"; -} - -.hgi-stroke.hgi-scribd::before { - content: "\f253f"; -} - -.hgi-stroke.hgi-scroll-01::before { - content: "\f2540"; -} - -.hgi-stroke.hgi-scroll-horizontal::before { - content: "\f2541"; -} - -.hgi-stroke.hgi-scroll-text::before { - content: "\f3326"; -} - -.hgi-stroke.hgi-scroll-vertical::before { - content: "\f2542"; -} - -.hgi-stroke.hgi-scroll::before { - content: "\f2543"; -} - -.hgi-stroke.hgi-sd-card::before { - content: "\f2544"; -} - -.hgi-stroke.hgi-seal::before { - content: "\f2545"; -} - -.hgi-stroke.hgi-search-01::before { - content: "\f2546"; -} - -.hgi-stroke.hgi-search-02::before { - content: "\f2547"; -} - -.hgi-stroke.hgi-search-add::before { - content: "\f2548"; -} - -.hgi-stroke.hgi-search-alert::before { - content: "\f3327"; -} - -.hgi-stroke.hgi-search-area::before { - content: "\f2549"; -} - -.hgi-stroke.hgi-search-check::before { - content: "\f3328"; -} - -.hgi-stroke.hgi-search-circle::before { - content: "\f254a"; -} - -.hgi-stroke.hgi-search-code::before { - content: "\f254b"; -} - -.hgi-stroke.hgi-search-dollar::before { - content: "\f254c"; -} - -.hgi-stroke.hgi-search-focus::before { - content: "\f254d"; -} - -.hgi-stroke.hgi-search-list-01::before { - content: "\f254e"; -} - -.hgi-stroke.hgi-search-list-02::before { - content: "\f254f"; -} - -.hgi-stroke.hgi-search-minus::before { - content: "\f2550"; -} - -.hgi-stroke.hgi-search-remove::before { - content: "\f2551"; -} - -.hgi-stroke.hgi-search-replace::before { - content: "\f2552"; -} - -.hgi-stroke.hgi-search-slash::before { - content: "\f3329"; -} - -.hgi-stroke.hgi-search-square::before { - content: "\f2553"; -} - -.hgi-stroke.hgi-search-visual::before { - content: "\f2554"; -} - -.hgi-stroke.hgi-search-x::before { - content: "\f332a"; -} - -.hgi-stroke.hgi-searching::before { - content: "\f2555"; -} - -.hgi-stroke.hgi-seat-selector::before { - content: "\f2556"; -} - -.hgi-stroke.hgi-second-bracket-circle::before { - content: "\f2557"; -} - -.hgi-stroke.hgi-second-bracket-square::before { - content: "\f2558"; -} - -.hgi-stroke.hgi-second-bracket::before { - content: "\f2559"; -} - -.hgi-stroke.hgi-section::before { - content: "\f332b"; -} - -.hgi-stroke.hgi-secured-network::before { - content: "\f255a"; -} - -.hgi-stroke.hgi-security-block::before { - content: "\f255b"; -} - -.hgi-stroke.hgi-security-check::before { - content: "\f255c"; -} - -.hgi-stroke.hgi-security-key-usb::before { - content: "\f255d"; -} - -.hgi-stroke.hgi-security-lock::before { - content: "\f255e"; -} - -.hgi-stroke.hgi-security-password::before { - content: "\f255f"; -} - -.hgi-stroke.hgi-security-validation::before { - content: "\f2560"; -} - -.hgi-stroke.hgi-security-warning::before { - content: "\f2561"; -} - -.hgi-stroke.hgi-security-wifi::before { - content: "\f2562"; -} - -.hgi-stroke.hgi-security::before { - content: "\f2563"; -} - -.hgi-stroke.hgi-seesaw::before { - content: "\f2564"; -} - -.hgi-stroke.hgi-segment::before { - content: "\f2565"; -} - -.hgi-stroke.hgi-select-01::before { - content: "\f2566"; -} - -.hgi-stroke.hgi-select-02::before { - content: "\f2567"; -} - -.hgi-stroke.hgi-self-transfer-2::before { - content: "\f332c"; -} - -.hgi-stroke.hgi-self-transfer::before { - content: "\f2568"; -} - -.hgi-stroke.hgi-semi-truck::before { - content: "\f332d"; -} - -.hgi-stroke.hgi-send-horizontal::before { - content: "\f332f"; -} - -.hgi-stroke.hgi-send-to-back::before { - content: "\f2569"; -} - -.hgi-stroke.hgi-send-to-mobile-02::before { - content: "\f256a"; -} - -.hgi-stroke.hgi-send-to-mobile::before { - content: "\f256b"; -} - -.hgi-stroke.hgi-send::before { - content: "\f332e"; -} - -.hgi-stroke.hgi-sending-order::before { - content: "\f256c"; -} - -.hgi-stroke.hgi-senseless::before { - content: "\f256d"; -} - -.hgi-stroke.hgi-sent-02::before { - content: "\f256e"; -} - -.hgi-stroke.hgi-sent::before { - content: "\f256f"; -} - -.hgi-stroke.hgi-seo::before { - content: "\f2570"; -} - -.hgi-stroke.hgi-separator-horizontal::before { - content: "\f2571"; -} - -.hgi-stroke.hgi-separator-vertical::before { - content: "\f3330"; -} - -.hgi-stroke.hgi-server-cog::before { - content: "\f3332"; -} - -.hgi-stroke.hgi-server-crash::before { - content: "\f3333"; -} - -.hgi-stroke.hgi-server-off::before { - content: "\f3334"; -} - -.hgi-stroke.hgi-server-stack-01::before { - content: "\f2572"; -} - -.hgi-stroke.hgi-server-stack-02::before { - content: "\f2573"; -} - -.hgi-stroke.hgi-server-stack-03::before { - content: "\f2574"; -} - -.hgi-stroke.hgi-server::before { - content: "\f3331"; -} - -.hgi-stroke.hgi-service::before { - content: "\f2575"; -} - -.hgi-stroke.hgi-serving-food-2::before { - content: "\f3335"; -} - -.hgi-stroke.hgi-serving-food::before { - content: "\f2576"; -} - -.hgi-stroke.hgi-setting-06::before { - content: "\f2577"; -} - -.hgi-stroke.hgi-setting-07::before { - content: "\f2578"; -} - -.hgi-stroke.hgi-setting-done-01::before { - content: "\f2579"; -} - -.hgi-stroke.hgi-setting-done-02::before { - content: "\f257a"; -} - -.hgi-stroke.hgi-setting-done-03::before { - content: "\f257b"; -} - -.hgi-stroke.hgi-setting-done-04::before { - content: "\f257c"; -} - -.hgi-stroke.hgi-setting-error-03::before { - content: "\f257d"; -} - -.hgi-stroke.hgi-setting-error-04::before { - content: "\f257e"; -} - -.hgi-stroke.hgi-settings-01::before { - content: "\f257f"; -} - -.hgi-stroke.hgi-settings-02::before { - content: "\f2580"; -} - -.hgi-stroke.hgi-settings-03::before { - content: "\f2581"; -} - -.hgi-stroke.hgi-settings-04::before { - content: "\f2582"; -} - -.hgi-stroke.hgi-settings-05::before { - content: "\f2583"; -} - -.hgi-stroke.hgi-settings-error-01::before { - content: "\f2584"; -} - -.hgi-stroke.hgi-settings-error-02::before { - content: "\f2585"; -} - -.hgi-stroke.hgi-setup-01::before { - content: "\f2586"; -} - -.hgi-stroke.hgi-setup-02::before { - content: "\f2587"; -} - -.hgi-stroke.hgi-seven-circle::before { - content: "\f2588"; -} - -.hgi-stroke.hgi-seven-square::before { - content: "\f2589"; -} - -.hgi-stroke.hgi-seven-z-01::before { - content: "\f258a"; -} - -.hgi-stroke.hgi-seven-z-02::before { - content: "\f258b"; -} - -.hgi-stroke.hgi-shadcn-square::before { - content: "\f258c"; -} - -.hgi-stroke.hgi-shadcn::before { - content: "\f258d"; -} - -.hgi-stroke.hgi-shaka-01::before { - content: "\f258e"; -} - -.hgi-stroke.hgi-shaka-02::before { - content: "\f258f"; -} - -.hgi-stroke.hgi-shaka-03::before { - content: "\f2590"; -} - -.hgi-stroke.hgi-shaka-04::before { - content: "\f2591"; -} - -.hgi-stroke.hgi-shampoo::before { - content: "\f2592"; -} - -.hgi-stroke.hgi-shape-collection::before { - content: "\f2593"; -} - -.hgi-stroke.hgi-shapes-01::before { - content: "\f2594"; -} - -.hgi-stroke.hgi-shapes::before { - content: "\f2595"; -} - -.hgi-stroke.hgi-share-01::before { - content: "\f2596"; -} - -.hgi-stroke.hgi-share-02::before { - content: "\f2597"; -} - -.hgi-stroke.hgi-share-03::before { - content: "\f2598"; -} - -.hgi-stroke.hgi-share-04::before { - content: "\f2599"; -} - -.hgi-stroke.hgi-share-05::before { - content: "\f259a"; -} - -.hgi-stroke.hgi-share-06::before { - content: "\f259b"; -} - -.hgi-stroke.hgi-share-07::before { - content: "\f259c"; -} - -.hgi-stroke.hgi-share-08::before { - content: "\f259d"; -} - -.hgi-stroke.hgi-share-knowledge::before { - content: "\f259e"; -} - -.hgi-stroke.hgi-share-location-01::before { - content: "\f259f"; -} - -.hgi-stroke.hgi-share-location-02::before { - content: "\f25a0"; -} - -.hgi-stroke.hgi-shared-drive::before { - content: "\f25a1"; -} - -.hgi-stroke.hgi-shared-wifi::before { - content: "\f25a2"; -} - -.hgi-stroke.hgi-sharing::before { - content: "\f25a3"; -} - -.hgi-stroke.hgi-sheet::before { - content: "\f3337"; -} - -.hgi-stroke.hgi-shell::before { - content: "\f25a4"; -} - -.hgi-stroke.hgi-shellfish::before { - content: "\f25a5"; -} - -.hgi-stroke.hgi-shelving-unit::before { - content: "\f3338"; -} - -.hgi-stroke.hgi-sheriff-01::before { - content: "\f25a6"; -} - -.hgi-stroke.hgi-sheriff-02::before { - content: "\f25a7"; -} - -.hgi-stroke.hgi-shield-01::before { - content: "\f25a8"; -} - -.hgi-stroke.hgi-shield-02::before { - content: "\f25a9"; -} - -.hgi-stroke.hgi-shield-alert::before { - content: "\f333a"; -} - -.hgi-stroke.hgi-shield-ban::before { - content: "\f25aa"; -} - -.hgi-stroke.hgi-shield-blockchain::before { - content: "\f25ab"; -} - -.hgi-stroke.hgi-shield-check::before { - content: "\f333b"; -} - -.hgi-stroke.hgi-shield-cog-corner::before { - content: "\f333d"; -} - -.hgi-stroke.hgi-shield-cog::before { - content: "\f333c"; -} - -.hgi-stroke.hgi-shield-ellipsis::before { - content: "\f333e"; -} - -.hgi-stroke.hgi-shield-energy::before { - content: "\f25ac"; -} - -.hgi-stroke.hgi-shield-half::before { - content: "\f25ad"; -} - -.hgi-stroke.hgi-shield-key::before { - content: "\f25ae"; -} - -.hgi-stroke.hgi-shield-minus::before { - content: "\f25af"; -} - -.hgi-stroke.hgi-shield-off::before { - content: "\f333f"; -} - -.hgi-stroke.hgi-shield-plus::before { - content: "\f25b0"; -} - -.hgi-stroke.hgi-shield-question-mark::before { - content: "\f25b1"; -} - -.hgi-stroke.hgi-shield-user::before { - content: "\f25b2"; -} - -.hgi-stroke.hgi-shield-x::before { - content: "\f3340"; -} - -.hgi-stroke.hgi-ship-wheel::before { - content: "\f3342"; -} - -.hgi-stroke.hgi-ship::before { - content: "\f3341"; -} - -.hgi-stroke.hgi-shipment-tracking::before { - content: "\f25b3"; -} - -.hgi-stroke.hgi-shipping-center::before { - content: "\f25b4"; -} - -.hgi-stroke.hgi-shipping-loading::before { - content: "\f25b5"; -} - -.hgi-stroke.hgi-shipping-truck-01::before { - content: "\f25b6"; -} - -.hgi-stroke.hgi-shipping-truck-02::before { - content: "\f25b7"; -} - -.hgi-stroke.hgi-shirt-01::before { - content: "\f25b8"; -} - -.hgi-stroke.hgi-shocked::before { - content: "\f25b9"; -} - -.hgi-stroke.hgi-shoes::before { - content: "\f3343"; -} - -.hgi-stroke.hgi-shop-sign::before { - content: "\f25ba"; -} - -.hgi-stroke.hgi-shopify::before { - content: "\f25bb"; -} - -.hgi-stroke.hgi-shopping-bag-01::before { - content: "\f25bc"; -} - -.hgi-stroke.hgi-shopping-bag-02::before { - content: "\f25bd"; -} - -.hgi-stroke.hgi-shopping-bag-03::before { - content: "\f25be"; -} - -.hgi-stroke.hgi-shopping-bag-add::before { - content: "\f25bf"; -} - -.hgi-stroke.hgi-shopping-bag-check::before { - content: "\f25c0"; -} - -.hgi-stroke.hgi-shopping-bag-favorite::before { - content: "\f25c1"; -} - -.hgi-stroke.hgi-shopping-bag-remove::before { - content: "\f25c2"; -} - -.hgi-stroke.hgi-shopping-basket-01::before { - content: "\f25c3"; -} - -.hgi-stroke.hgi-shopping-basket-02::before { - content: "\f25c4"; -} - -.hgi-stroke.hgi-shopping-basket-03::before { - content: "\f25c5"; -} - -.hgi-stroke.hgi-shopping-basket-add-01::before { - content: "\f25c6"; -} - -.hgi-stroke.hgi-shopping-basket-add-02::before { - content: "\f25c7"; -} - -.hgi-stroke.hgi-shopping-basket-add-03::before { - content: "\f25c8"; -} - -.hgi-stroke.hgi-shopping-basket-check-in-01::before { - content: "\f25c9"; -} - -.hgi-stroke.hgi-shopping-basket-check-in-02::before { - content: "\f25ca"; -} - -.hgi-stroke.hgi-shopping-basket-check-in-03::before { - content: "\f25cb"; -} - -.hgi-stroke.hgi-shopping-basket-check-out-01::before { - content: "\f25cc"; -} - -.hgi-stroke.hgi-shopping-basket-check-out-02::before { - content: "\f25cd"; -} - -.hgi-stroke.hgi-shopping-basket-check-out-03::before { - content: "\f25ce"; -} - -.hgi-stroke.hgi-shopping-basket-done-01::before { - content: "\f25cf"; -} - -.hgi-stroke.hgi-shopping-basket-done-02::before { - content: "\f25d0"; -} - -.hgi-stroke.hgi-shopping-basket-done-03::before { - content: "\f25d1"; -} - -.hgi-stroke.hgi-shopping-basket-favorite-01::before { - content: "\f25d2"; -} - -.hgi-stroke.hgi-shopping-basket-favorite-02::before { - content: "\f25d3"; -} - -.hgi-stroke.hgi-shopping-basket-favorite-03::before { - content: "\f25d4"; -} - -.hgi-stroke.hgi-shopping-basket-remove-01::before { - content: "\f25d5"; -} - -.hgi-stroke.hgi-shopping-basket-remove-02::before { - content: "\f25d6"; -} - -.hgi-stroke.hgi-shopping-basket-remove-03::before { - content: "\f25d7"; -} - -.hgi-stroke.hgi-shopping-basket-secure-01::before { - content: "\f25d8"; -} - -.hgi-stroke.hgi-shopping-basket-secure-02::before { - content: "\f25d9"; -} - -.hgi-stroke.hgi-shopping-basket-secure-03::before { - content: "\f25da"; -} - -.hgi-stroke.hgi-shopping-cart-01::before { - content: "\f25db"; -} - -.hgi-stroke.hgi-shopping-cart-02::before { - content: "\f25dc"; -} - -.hgi-stroke.hgi-shopping-cart-add-01::before { - content: "\f25dd"; -} - -.hgi-stroke.hgi-shopping-cart-add-02::before { - content: "\f25de"; -} - -.hgi-stroke.hgi-shopping-cart-check-01::before { - content: "\f25df"; -} - -.hgi-stroke.hgi-shopping-cart-check-02::before { - content: "\f25e0"; -} - -.hgi-stroke.hgi-shopping-cart-check-in-01::before { - content: "\f25e1"; -} - -.hgi-stroke.hgi-shopping-cart-check-in-02::before { - content: "\f25e2"; -} - -.hgi-stroke.hgi-shopping-cart-check-out-01::before { - content: "\f25e3"; -} - -.hgi-stroke.hgi-shopping-cart-check-out-02::before { - content: "\f25e4"; -} - -.hgi-stroke.hgi-shopping-cart-favorite-01::before { - content: "\f25e5"; -} - -.hgi-stroke.hgi-shopping-cart-favorite-02::before { - content: "\f25e6"; -} - -.hgi-stroke.hgi-shopping-cart-remove-01::before { - content: "\f25e7"; -} - -.hgi-stroke.hgi-shopping-cart-remove-02::before { - content: "\f25e8"; -} - -.hgi-stroke.hgi-shorts-pants::before { - content: "\f25e9"; -} - -.hgi-stroke.hgi-shoulder::before { - content: "\f25ea"; -} - -.hgi-stroke.hgi-shovel::before { - content: "\f25eb"; -} - -.hgi-stroke.hgi-shower-head::before { - content: "\f25ec"; -} - -.hgi-stroke.hgi-shredder::before { - content: "\f3344"; -} - -.hgi-stroke.hgi-shrimp::before { - content: "\f3345"; -} - -.hgi-stroke.hgi-shrink-dot::before { - content: "\f25ed"; -} - -.hgi-stroke.hgi-shrink::before { - content: "\f3346"; -} - -.hgi-stroke.hgi-shrub::before { - content: "\f3347"; -} - -.hgi-stroke.hgi-shuffle-square::before { - content: "\f25ee"; -} - -.hgi-stroke.hgi-shuffle::before { - content: "\f25ef"; -} - -.hgi-stroke.hgi-shut-down::before { - content: "\f25f0"; -} - -.hgi-stroke.hgi-shutterstock::before { - content: "\f25f1"; -} - -.hgi-stroke.hgi-sidebar-bottom::before { - content: "\f25f2"; -} - -.hgi-stroke.hgi-sidebar-left-01::before { - content: "\f25f3"; -} - -.hgi-stroke.hgi-sidebar-left::before { - content: "\f25f4"; -} - -.hgi-stroke.hgi-sidebar-right-01::before { - content: "\f25f5"; -} - -.hgi-stroke.hgi-sidebar-right::before { - content: "\f25f6"; -} - -.hgi-stroke.hgi-sidebar-top::before { - content: "\f25f7"; -} - -.hgi-stroke.hgi-sigma::before { - content: "\f3349"; -} - -.hgi-stroke.hgi-sign-language-c::before { - content: "\f25f8"; -} - -.hgi-stroke.hgi-signal-full-01::before { - content: "\f25f9"; -} - -.hgi-stroke.hgi-signal-full-02::before { - content: "\f25fa"; -} - -.hgi-stroke.hgi-signal-high::before { - content: "\f334a"; -} - -.hgi-stroke.hgi-signal-low-01::before { - content: "\f25fb"; -} - -.hgi-stroke.hgi-signal-low-02::before { - content: "\f25fc"; -} - -.hgi-stroke.hgi-signal-low-medium::before { - content: "\f25fd"; -} - -.hgi-stroke.hgi-signal-medium-01::before { - content: "\f25fe"; -} - -.hgi-stroke.hgi-signal-medium-02::before { - content: "\f25ff"; -} - -.hgi-stroke.hgi-signal-no-01::before { - content: "\f2600"; -} - -.hgi-stroke.hgi-signal-no-02::before { - content: "\f2601"; -} - -.hgi-stroke.hgi-signal-zero::before { - content: "\f334b"; -} - -.hgi-stroke.hgi-signal::before { - content: "\f2602"; -} - -.hgi-stroke.hgi-signature::before { - content: "\f2603"; -} - -.hgi-stroke.hgi-signpost-big::before { - content: "\f334d"; -} - -.hgi-stroke.hgi-signpost::before { - content: "\f334c"; -} - -.hgi-stroke.hgi-silence::before { - content: "\f2604"; -} - -.hgi-stroke.hgi-simcard-01::before { - content: "\f2605"; -} - -.hgi-stroke.hgi-simcard-02::before { - content: "\f2606"; -} - -.hgi-stroke.hgi-simcard-dual::before { - content: "\f2607"; -} - -.hgi-stroke.hgi-sin::before { - content: "\f2608"; -} - -.hgi-stroke.hgi-sine-01::before { - content: "\f2609"; -} - -.hgi-stroke.hgi-sine-02::before { - content: "\f260a"; -} - -.hgi-stroke.hgi-sing-left::before { - content: "\f260b"; -} - -.hgi-stroke.hgi-sing-right::before { - content: "\f260c"; -} - -.hgi-stroke.hgi-sink-01::before { - content: "\f260d"; -} - -.hgi-stroke.hgi-sink-02::before { - content: "\f260e"; -} - -.hgi-stroke.hgi-siren::before { - content: "\f334e"; -} - -.hgi-stroke.hgi-siri-new::before { - content: "\f260f"; -} - -.hgi-stroke.hgi-siri::before { - content: "\f2610"; -} - -.hgi-stroke.hgi-six-circle::before { - content: "\f2611"; -} - -.hgi-stroke.hgi-six-square::before { - content: "\f2612"; -} - -.hgi-stroke.hgi-sketch::before { - content: "\f2613"; -} - -.hgi-stroke.hgi-skew::before { - content: "\f2614"; -} - -.hgi-stroke.hgi-ski::before { - content: "\f334f"; -} - -.hgi-stroke.hgi-skip-back::before { - content: "\f3350"; -} - -.hgi-stroke.hgi-skip-forward::before { - content: "\f3351"; -} - -.hgi-stroke.hgi-skipping-rope::before { - content: "\f2616"; -} - -.hgi-stroke.hgi-skool::before { - content: "\f2617"; -} - -.hgi-stroke.hgi-skull::before { - content: "\f2618"; -} - -.hgi-stroke.hgi-skype::before { - content: "\f2619"; -} - -.hgi-stroke.hgi-slack::before { - content: "\f261a"; -} - -.hgi-stroke.hgi-slash::before { - content: "\f3352"; -} - -.hgi-stroke.hgi-sleeping::before { - content: "\f261b"; -} - -.hgi-stroke.hgi-sleeveless::before { - content: "\f261c"; -} - -.hgi-stroke.hgi-slice::before { - content: "\f3353"; -} - -.hgi-stroke.hgi-slide::before { - content: "\f261d"; -} - -.hgi-stroke.hgi-sliders-horizontal::before { - content: "\f261e"; -} - -.hgi-stroke.hgi-sliders-vertical::before { - content: "\f261f"; -} - -.hgi-stroke.hgi-slideshare::before { - content: "\f2620"; -} - -.hgi-stroke.hgi-slow-winds::before { - content: "\f2621"; -} - -.hgi-stroke.hgi-smart-ac::before { - content: "\f2622"; -} - -.hgi-stroke.hgi-smart-phone-01::before { - content: "\f2623"; -} - -.hgi-stroke.hgi-smart-phone-02::before { - content: "\f2624"; -} - -.hgi-stroke.hgi-smart-phone-03::before { - content: "\f2625"; -} - -.hgi-stroke.hgi-smart-phone-04::before { - content: "\f2626"; -} - -.hgi-stroke.hgi-smart-phone-landscape-02::before { - content: "\f2627"; -} - -.hgi-stroke.hgi-smart-phone-landscape-2::before { - content: "\f3354"; -} - -.hgi-stroke.hgi-smart-phone-landscape::before { - content: "\f2628"; -} - -.hgi-stroke.hgi-smart-watch-01::before { - content: "\f2629"; -} - -.hgi-stroke.hgi-smart-watch-02::before { - content: "\f262a"; -} - -.hgi-stroke.hgi-smart-watch-03::before { - content: "\f262b"; -} - -.hgi-stroke.hgi-smart-watch-04::before { - content: "\f262c"; -} - -.hgi-stroke.hgi-smart::before { - content: "\f262d"; -} - -.hgi-stroke.hgi-smartphone-charging::before { - content: "\f262e"; -} - -.hgi-stroke.hgi-smartphone-lost-wifi::before { - content: "\f262f"; -} - -.hgi-stroke.hgi-smartphone-nfc::before { - content: "\f3356"; -} - -.hgi-stroke.hgi-smartphone-wifi::before { - content: "\f2630"; -} - -.hgi-stroke.hgi-smartphone::before { - content: "\f3355"; -} - -.hgi-stroke.hgi-smile-dizzy::before { - content: "\f2631"; -} - -.hgi-stroke.hgi-smile-plus::before { - content: "\f2632"; -} - -.hgi-stroke.hgi-smile::before { - content: "\f2633"; -} - -.hgi-stroke.hgi-sms-code::before { - content: "\f2634"; -} - -.hgi-stroke.hgi-snail::before { - content: "\f2635"; -} - -.hgi-stroke.hgi-snapchat::before { - content: "\f2636"; -} - -.hgi-stroke.hgi-sneaker::before { - content: "\f3357"; -} - -.hgi-stroke.hgi-snow::before { - content: "\f2637"; -} - -.hgi-stroke.hgi-snowflake::before { - content: "\f3358"; -} - -.hgi-stroke.hgi-soap-dispenser-droplet::before { - content: "\f3359"; -} - -.hgi-stroke.hgi-socks::before { - content: "\f2638"; -} - -.hgi-stroke.hgi-soda-can::before { - content: "\f2639"; -} - -.hgi-stroke.hgi-sofa-01::before { - content: "\f263a"; -} - -.hgi-stroke.hgi-sofa-02::before { - content: "\f263b"; -} - -.hgi-stroke.hgi-sofa-03::before { - content: "\f263c"; -} - -.hgi-stroke.hgi-sofa-single::before { - content: "\f263d"; -} - -.hgi-stroke.hgi-soft-drink-01::before { - content: "\f263e"; -} - -.hgi-stroke.hgi-soft-drink-02::before { - content: "\f263f"; -} - -.hgi-stroke.hgi-software-license::before { - content: "\f2640"; -} - -.hgi-stroke.hgi-software-uninstall::before { - content: "\f2641"; -} - -.hgi-stroke.hgi-software::before { - content: "\f2642"; -} - -.hgi-stroke.hgi-soil-moisture-field::before { - content: "\f2643"; -} - -.hgi-stroke.hgi-soil-moisture-global::before { - content: "\f2644"; -} - -.hgi-stroke.hgi-soil-temperature-field::before { - content: "\f2645"; -} - -.hgi-stroke.hgi-soil-temperature-global::before { - content: "\f2646"; -} - -.hgi-stroke.hgi-solar-energy::before { - content: "\f2647"; -} - -.hgi-stroke.hgi-solar-panel-01::before { - content: "\f2648"; -} - -.hgi-stroke.hgi-solar-panel-02::before { - content: "\f2649"; -} - -.hgi-stroke.hgi-solar-panel-03::before { - content: "\f264a"; -} - -.hgi-stroke.hgi-solar-panel-04::before { - content: "\f264b"; -} - -.hgi-stroke.hgi-solar-panel-05::before { - content: "\f264c"; -} - -.hgi-stroke.hgi-solar-power::before { - content: "\f264d"; -} - -.hgi-stroke.hgi-solar-system-01::before { - content: "\f264e"; -} - -.hgi-stroke.hgi-solar-system-2::before { - content: "\f335a"; -} - -.hgi-stroke.hgi-solar-system::before { - content: "\f264f"; -} - -.hgi-stroke.hgi-solid-line-01::before { - content: "\f2650"; -} - -.hgi-stroke.hgi-solid-line-02::before { - content: "\f2651"; -} - -.hgi-stroke.hgi-sort-by-down-01::before { - content: "\f2652"; -} - -.hgi-stroke.hgi-sort-by-down-02::before { - content: "\f2653"; -} - -.hgi-stroke.hgi-sort-by-up-01::before { - content: "\f2654"; -} - -.hgi-stroke.hgi-sort-by-up-02::before { - content: "\f2655"; -} - -.hgi-stroke.hgi-sort-descending::before { - content: "\f2656"; -} - -.hgi-stroke.hgi-sorting-01::before { - content: "\f2657"; -} - -.hgi-stroke.hgi-sorting-02::before { - content: "\f2658"; -} - -.hgi-stroke.hgi-sorting-03::before { - content: "\f2659"; -} - -.hgi-stroke.hgi-sorting-04::before { - content: "\f265a"; -} - -.hgi-stroke.hgi-sorting-05::before { - content: "\f265b"; -} - -.hgi-stroke.hgi-sorting-a-z-01::before { - content: "\f265c"; -} - -.hgi-stroke.hgi-sorting-a-z-02::before { - content: "\f265d"; -} - -.hgi-stroke.hgi-sorting-down::before { - content: "\f265e"; -} - -.hgi-stroke.hgi-sorting-nine-1::before { - content: "\f265f"; -} - -.hgi-stroke.hgi-sorting-one-9::before { - content: "\f2660"; -} - -.hgi-stroke.hgi-sorting-up::before { - content: "\f2661"; -} - -.hgi-stroke.hgi-sorting-z-a-01::before { - content: "\f2662"; -} - -.hgi-stroke.hgi-soundcloud::before { - content: "\f2663"; -} - -.hgi-stroke.hgi-soup::before { - content: "\f335b"; -} - -.hgi-stroke.hgi-source-code-circle::before { - content: "\f2664"; -} - -.hgi-stroke.hgi-source-code-square::before { - content: "\f2665"; -} - -.hgi-stroke.hgi-source-code::before { - content: "\f2666"; -} - -.hgi-stroke.hgi-space::before { - content: "\f335c"; -} - -.hgi-stroke.hgi-spaceship-2::before { - content: "\f335d"; -} - -.hgi-stroke.hgi-spaceship::before { - content: "\f2667"; -} - -.hgi-stroke.hgi-spade::before { - content: "\f335e"; -} - -.hgi-stroke.hgi-spades::before { - content: "\f2668"; -} - -.hgi-stroke.hgi-spaghetti::before { - content: "\f2669"; -} - -.hgi-stroke.hgi-spam::before { - content: "\f266a"; -} - -.hgi-stroke.hgi-sparkle::before { - content: "\f335f"; -} - -.hgi-stroke.hgi-sparkles::before { - content: "\f266b"; -} - -.hgi-stroke.hgi-spartan-helmet::before { - content: "\f266c"; -} - -.hgi-stroke.hgi-spatula::before { - content: "\f266d"; -} - -.hgi-stroke.hgi-speach-to-text-2::before { - content: "\f3361"; -} - -.hgi-stroke.hgi-speach-to-text::before { - content: "\f3360"; -} - -.hgi-stroke.hgi-speaker-01::before { - content: "\f266e"; -} - -.hgi-stroke.hgi-speaker::before { - content: "\f266f"; -} - -.hgi-stroke.hgi-speech-to-text::before { - content: "\f2670"; -} - -.hgi-stroke.hgi-speech::before { - content: "\f3362"; -} - -.hgi-stroke.hgi-speed-train-01::before { - content: "\f2671"; -} - -.hgi-stroke.hgi-speed-train-02::before { - content: "\f2672"; -} - -.hgi-stroke.hgi-spell-check::before { - content: "\f3363"; -} - -.hgi-stroke.hgi-sperm::before { - content: "\f2673"; -} - -.hgi-stroke.hgi-sphere::before { - content: "\f2674"; -} - -.hgi-stroke.hgi-spirals::before { - content: "\f2675"; -} - -.hgi-stroke.hgi-spline-pointer::before { - content: "\f2676"; -} - -.hgi-stroke.hgi-spline::before { - content: "\f3364"; -} - -.hgi-stroke.hgi-split::before { - content: "\f2677"; -} - -.hgi-stroke.hgi-spool::before { - content: "\f3365"; -} - -.hgi-stroke.hgi-spoon-and-fork::before { - content: "\f2678"; -} - -.hgi-stroke.hgi-spoon-and-knife::before { - content: "\f2679"; -} - -.hgi-stroke.hgi-spoon::before { - content: "\f267a"; -} - -.hgi-stroke.hgi-sport-shoe::before { - content: "\f3366"; -} - -.hgi-stroke.hgi-spotify::before { - content: "\f267b"; -} - -.hgi-stroke.hgi-spotlight::before { - content: "\f267c"; -} - -.hgi-stroke.hgi-spray-can::before { - content: "\f267d"; -} - -.hgi-stroke.hgi-sprout::before { - content: "\f3367"; -} - -.hgi-stroke.hgi-sql::before { - content: "\f267e"; -} - -.hgi-stroke.hgi-square-01::before { - content: "\f267f"; -} - -.hgi-stroke.hgi-square-activity::before { - content: "\f3368"; -} - -.hgi-stroke.hgi-square-arrow-data-transfer-diagonal::before { - content: "\f2680"; -} - -.hgi-stroke.hgi-square-arrow-data-transfer-horizontal::before { - content: "\f2681"; -} - -.hgi-stroke.hgi-square-arrow-data-transfer-vertical::before { - content: "\f2682"; -} - -.hgi-stroke.hgi-square-arrow-diagonal-01::before { - content: "\f2683"; -} - -.hgi-stroke.hgi-square-arrow-diagonal-02::before { - content: "\f2684"; -} - -.hgi-stroke.hgi-square-arrow-down-01::before { - content: "\f2685"; -} - -.hgi-stroke.hgi-square-arrow-down-02::before { - content: "\f2686"; -} - -.hgi-stroke.hgi-square-arrow-down-03::before { - content: "\f2687"; -} - -.hgi-stroke.hgi-square-arrow-down-double::before { - content: "\f2688"; -} - -.hgi-stroke.hgi-square-arrow-down-left::before { - content: "\f2689"; -} - -.hgi-stroke.hgi-square-arrow-down-right::before { - content: "\f268a"; -} - -.hgi-stroke.hgi-square-arrow-expand-01::before { - content: "\f268b"; -} - -.hgi-stroke.hgi-square-arrow-expand-02::before { - content: "\f268c"; -} - -.hgi-stroke.hgi-square-arrow-horizontal::before { - content: "\f268d"; -} - -.hgi-stroke.hgi-square-arrow-left-01::before { - content: "\f268e"; -} - -.hgi-stroke.hgi-square-arrow-left-02::before { - content: "\f268f"; -} - -.hgi-stroke.hgi-square-arrow-left-03::before { - content: "\f2690"; -} - -.hgi-stroke.hgi-square-arrow-left-double::before { - content: "\f2691"; -} - -.hgi-stroke.hgi-square-arrow-left-right::before { - content: "\f2692"; -} - -.hgi-stroke.hgi-square-arrow-move-down-left::before { - content: "\f2693"; -} - -.hgi-stroke.hgi-square-arrow-move-down-right::before { - content: "\f2694"; -} - -.hgi-stroke.hgi-square-arrow-move-left-down::before { - content: "\f2695"; -} - -.hgi-stroke.hgi-square-arrow-move-left-up::before { - content: "\f2696"; -} - -.hgi-stroke.hgi-square-arrow-move-right-down::before { - content: "\f2697"; -} - -.hgi-stroke.hgi-square-arrow-move-right-up::before { - content: "\f2698"; -} - -.hgi-stroke.hgi-square-arrow-out-down-left::before { - content: "\f3369"; -} - -.hgi-stroke.hgi-square-arrow-out-down-right::before { - content: "\f336a"; -} - -.hgi-stroke.hgi-square-arrow-out-up-left::before { - content: "\f336b"; -} - -.hgi-stroke.hgi-square-arrow-out-up-right::before { - content: "\f336c"; -} - -.hgi-stroke.hgi-square-arrow-reload-01::before { - content: "\f2699"; -} - -.hgi-stroke.hgi-square-arrow-reload-02::before { - content: "\f269a"; -} - -.hgi-stroke.hgi-square-arrow-right-01::before { - content: "\f269b"; -} - -.hgi-stroke.hgi-square-arrow-right-02::before { - content: "\f269c"; -} - -.hgi-stroke.hgi-square-arrow-right-03::before { - content: "\f269d"; -} - -.hgi-stroke.hgi-square-arrow-right-double::before { - content: "\f269e"; -} - -.hgi-stroke.hgi-square-arrow-right-enter::before { - content: "\f336d"; -} - -.hgi-stroke.hgi-square-arrow-right-exit::before { - content: "\f336e"; -} - -.hgi-stroke.hgi-square-arrow-shrink-01::before { - content: "\f269f"; -} - -.hgi-stroke.hgi-square-arrow-shrink-02::before { - content: "\f26a0"; -} - -.hgi-stroke.hgi-square-arrow-up-01::before { - content: "\f26a1"; -} - -.hgi-stroke.hgi-square-arrow-up-02::before { - content: "\f26a2"; -} - -.hgi-stroke.hgi-square-arrow-up-03::before { - content: "\f26a3"; -} - -.hgi-stroke.hgi-square-arrow-up-double::before { - content: "\f26a4"; -} - -.hgi-stroke.hgi-square-arrow-up-down::before { - content: "\f26a5"; -} - -.hgi-stroke.hgi-square-arrow-up-left::before { - content: "\f26a6"; -} - -.hgi-stroke.hgi-square-arrow-up-right-02::before { - content: "\f336f"; -} - -.hgi-stroke.hgi-square-arrow-up-right::before { - content: "\f26a7"; -} - -.hgi-stroke.hgi-square-arrow-vertical::before { - content: "\f26a8"; -} - -.hgi-stroke.hgi-square-asterisk::before { - content: "\f26a9"; -} - -.hgi-stroke.hgi-square-bottom-dashed-scissors::before { - content: "\f26aa"; -} - -.hgi-stroke.hgi-square-centerline-dashed-horizontal::before { - content: "\f3370"; -} - -.hgi-stroke.hgi-square-centerline-dashed-vertical::before { - content: "\f3371"; -} - -.hgi-stroke.hgi-square-chart-gantt::before { - content: "\f26ab"; -} - -.hgi-stroke.hgi-square-check-big::before { - content: "\f3373"; -} - -.hgi-stroke.hgi-square-check::before { - content: "\f3372"; -} - -.hgi-stroke.hgi-square-chevron-down::before { - content: "\f3374"; -} - -.hgi-stroke.hgi-square-chevron-left::before { - content: "\f3375"; -} - -.hgi-stroke.hgi-square-chevron-right::before { - content: "\f3376"; -} - -.hgi-stroke.hgi-square-chevron-up::before { - content: "\f3377"; -} - -.hgi-stroke.hgi-square-circle::before { - content: "\f26ac"; -} - -.hgi-stroke.hgi-square-dashed-bottom-code::before { - content: "\f337a"; -} - -.hgi-stroke.hgi-square-dashed-bottom::before { - content: "\f3379"; -} - -.hgi-stroke.hgi-square-dashed-kanban::before { - content: "\f26ad"; -} - -.hgi-stroke.hgi-square-dashed-mouse-pointer::before { - content: "\f337b"; -} - -.hgi-stroke.hgi-square-dashed-top-solid::before { - content: "\f26ae"; -} - -.hgi-stroke.hgi-square-dashed::before { - content: "\f3378"; -} - -.hgi-stroke.hgi-square-divide::before { - content: "\f337c"; -} - -.hgi-stroke.hgi-square-dot::before { - content: "\f337d"; -} - -.hgi-stroke.hgi-square-equal::before { - content: "\f337e"; -} - -.hgi-stroke.hgi-square-f::before { - content: "\f26af"; -} - -.hgi-stroke.hgi-square-kanban::before { - content: "\f337f"; -} - -.hgi-stroke.hgi-square-library::before { - content: "\f3380"; -} - -.hgi-stroke.hgi-square-lock-01::before { - content: "\f26b0"; -} - -.hgi-stroke.hgi-square-lock-02::before { - content: "\f26b1"; -} - -.hgi-stroke.hgi-square-lock-add-01::before { - content: "\f26b2"; -} - -.hgi-stroke.hgi-square-lock-add-02::before { - content: "\f26b3"; -} - -.hgi-stroke.hgi-square-lock-check-01::before { - content: "\f26b4"; -} - -.hgi-stroke.hgi-square-lock-check-02::before { - content: "\f26b5"; -} - -.hgi-stroke.hgi-square-lock-minus-01::before { - content: "\f26b6"; -} - -.hgi-stroke.hgi-square-lock-minus-02::before { - content: "\f26b7"; -} - -.hgi-stroke.hgi-square-lock-password::before { - content: "\f26b8"; -} - -.hgi-stroke.hgi-square-lock-remove-01::before { - content: "\f26b9"; -} - -.hgi-stroke.hgi-square-lock-remove-02::before { - content: "\f26ba"; -} - -.hgi-stroke.hgi-square-m::before { - content: "\f26bb"; -} - -.hgi-stroke.hgi-square-minus::before { - content: "\f3381"; -} - -.hgi-stroke.hgi-square-mouse-pointer::before { - content: "\f26bc"; -} - -.hgi-stroke.hgi-square-parking-off::before { - content: "\f26bd"; -} - -.hgi-stroke.hgi-square-parking::before { - content: "\f3382"; -} - -.hgi-stroke.hgi-square-pause::before { - content: "\f3383"; -} - -.hgi-stroke.hgi-square-pen::before { - content: "\f3384"; -} - -.hgi-stroke.hgi-square-pilcrow::before { - content: "\f3385"; -} - -.hgi-stroke.hgi-square-plus::before { - content: "\f3386"; -} - -.hgi-stroke.hgi-square-power::before { - content: "\f26be"; -} - -.hgi-stroke.hgi-square-radical::before { - content: "\f3387"; -} - -.hgi-stroke.hgi-square-root-square::before { - content: "\f26bf"; -} - -.hgi-stroke.hgi-square-round-corner::before { - content: "\f26c0"; -} - -.hgi-stroke.hgi-square-scissors::before { - content: "\f3388"; -} - -.hgi-stroke.hgi-square-sigma::before { - content: "\f3389"; -} - -.hgi-stroke.hgi-square-slash::before { - content: "\f338a"; -} - -.hgi-stroke.hgi-square-split-horizontal::before { - content: "\f338b"; -} - -.hgi-stroke.hgi-square-split-vertical::before { - content: "\f338c"; -} - -.hgi-stroke.hgi-square-square::before { - content: "\f26c1"; -} - -.hgi-stroke.hgi-square-stack::before { - content: "\f26c2"; -} - -.hgi-stroke.hgi-square-stop::before { - content: "\f338d"; -} - -.hgi-stroke.hgi-square-terminal::before { - content: "\f338e"; -} - -.hgi-stroke.hgi-square-unlock-01::before { - content: "\f26c3"; -} - -.hgi-stroke.hgi-square-unlock-02::before { - content: "\f26c4"; -} - -.hgi-stroke.hgi-square-user-round::before { - content: "\f338f"; -} - -.hgi-stroke.hgi-square-x::before { - content: "\f3390"; -} - -.hgi-stroke.hgi-square::before { - content: "\f26c5"; -} - -.hgi-stroke.hgi-squares-exclude::before { - content: "\f26c6"; -} - -.hgi-stroke.hgi-squares-intersect::before { - content: "\f3391"; -} - -.hgi-stroke.hgi-squares-subtract::before { - content: "\f3392"; -} - -.hgi-stroke.hgi-squares-unite::before { - content: "\f3393"; -} - -.hgi-stroke.hgi-squircle-dashed::before { - content: "\f3395"; -} - -.hgi-stroke.hgi-squircle::before { - content: "\f3394"; -} - -.hgi-stroke.hgi-squirrel::before { - content: "\f3396"; -} - -.hgi-stroke.hgi-stack-star::before { - content: "\f26c7"; -} - -.hgi-stroke.hgi-stairs-01::before { - content: "\f26c8"; -} - -.hgi-stroke.hgi-stairs-02::before { - content: "\f26c9"; -} - -.hgi-stroke.hgi-stairs-03::before { - content: "\f26ca"; -} - -.hgi-stroke.hgi-stairs-04::before { - content: "\f26cb"; -} - -.hgi-stroke.hgi-stake::before { - content: "\f26cc"; -} - -.hgi-stroke.hgi-stamp-01::before { - content: "\f26cd"; -} - -.hgi-stroke.hgi-stamp-02::before { - content: "\f26ce"; -} - -.hgi-stroke.hgi-stamp::before { - content: "\f26cf"; -} - -.hgi-stroke.hgi-star-award-01::before { - content: "\f26d0"; -} - -.hgi-stroke.hgi-star-award-02::before { - content: "\f26d1"; -} - -.hgi-stroke.hgi-star-circle::before { - content: "\f26d2"; -} - -.hgi-stroke.hgi-star-face::before { - content: "\f26d3"; -} - -.hgi-stroke.hgi-star-half::before { - content: "\f26d4"; -} - -.hgi-stroke.hgi-star-off::before { - content: "\f26d5"; -} - -.hgi-stroke.hgi-star-square::before { - content: "\f26d6"; -} - -.hgi-stroke.hgi-star::before { - content: "\f26d7"; -} - -.hgi-stroke.hgi-stars::before { - content: "\f26d8"; -} - -.hgi-stroke.hgi-start-up-01::before { - content: "\f26d9"; -} - -.hgi-stroke.hgi-start-up-02::before { - content: "\f26da"; -} - -.hgi-stroke.hgi-stationery::before { - content: "\f26db"; -} - -.hgi-stroke.hgi-status::before { - content: "\f26dc"; -} - -.hgi-stroke.hgi-steak::before { - content: "\f26dd"; -} - -.hgi-stroke.hgi-steering::before { - content: "\f26de"; -} - -.hgi-stroke.hgi-step-back::before { - content: "\f3397"; -} - -.hgi-stroke.hgi-step-forward::before { - content: "\f3398"; -} - -.hgi-stroke.hgi-step-into::before { - content: "\f26df"; -} - -.hgi-stroke.hgi-step-out::before { - content: "\f26e0"; -} - -.hgi-stroke.hgi-step-over::before { - content: "\f26e1"; -} - -.hgi-stroke.hgi-stethoscope-02::before { - content: "\f26e2"; -} - -.hgi-stroke.hgi-stethoscope::before { - content: "\f26e3"; -} - -.hgi-stroke.hgi-sticker::before { - content: "\f26e4"; -} - -.hgi-stroke.hgi-sticky-note-01::before { - content: "\f26e5"; -} - -.hgi-stroke.hgi-sticky-note-02::before { - content: "\f26e6"; -} - -.hgi-stroke.hgi-sticky-note-03::before { - content: "\f26e7"; -} - -.hgi-stroke.hgi-stone::before { - content: "\f3399"; -} - -.hgi-stroke.hgi-stop-circle::before { - content: "\f26e8"; -} - -.hgi-stroke.hgi-stop-loss-order::before { - content: "\f26e9"; -} - -.hgi-stroke.hgi-stop-watch::before { - content: "\f26ea"; -} - -.hgi-stroke.hgi-stop::before { - content: "\f26eb"; -} - -.hgi-stroke.hgi-store-01::before { - content: "\f26ec"; -} - -.hgi-stroke.hgi-store-02::before { - content: "\f26ed"; -} - -.hgi-stroke.hgi-store-03::before { - content: "\f26ee"; -} - -.hgi-stroke.hgi-store-04::before { - content: "\f26ef"; -} - -.hgi-stroke.hgi-store-add-01::before { - content: "\f26f0"; -} - -.hgi-stroke.hgi-store-add-02::before { - content: "\f26f1"; -} - -.hgi-stroke.hgi-store-location-01::before { - content: "\f26f2"; -} - -.hgi-stroke.hgi-store-location-02::before { - content: "\f26f3"; -} - -.hgi-stroke.hgi-store-management-01::before { - content: "\f26f4"; -} - -.hgi-stroke.hgi-store-management-02::before { - content: "\f26f5"; -} - -.hgi-stroke.hgi-store-remove-01::before { - content: "\f26f6"; -} - -.hgi-stroke.hgi-store-remove-02::before { - content: "\f26f7"; -} - -.hgi-stroke.hgi-store-verified-01::before { - content: "\f26f8"; -} - -.hgi-stroke.hgi-store-verified-02::before { - content: "\f26f9"; -} - -.hgi-stroke.hgi-straight-edge::before { - content: "\f26fa"; -} - -.hgi-stroke.hgi-strategy::before { - content: "\f26fb"; -} - -.hgi-stroke.hgi-streering-wheel::before { - content: "\f26fc"; -} - -.hgi-stroke.hgi-street-food::before { - content: "\f26fd"; -} - -.hgi-stroke.hgi-stretch-horizontal::before { - content: "\f339a"; -} - -.hgi-stroke.hgi-stretch-vertical::before { - content: "\f339b"; -} - -.hgi-stroke.hgi-strikethrough::before { - content: "\f339c"; -} - -.hgi-stroke.hgi-stripe::before { - content: "\f26fe"; -} - -.hgi-stroke.hgi-stroke-bottom::before { - content: "\f26ff"; -} - -.hgi-stroke.hgi-stroke-center::before { - content: "\f2700"; -} - -.hgi-stroke.hgi-stroke-inside::before { - content: "\f2701"; -} - -.hgi-stroke.hgi-stroke-left::before { - content: "\f2702"; -} - -.hgi-stroke.hgi-stroke-outside::before { - content: "\f2703"; -} - -.hgi-stroke.hgi-stroke-right::before { - content: "\f2704"; -} - -.hgi-stroke.hgi-stroke-top::before { - content: "\f2705"; -} - -.hgi-stroke.hgi-structure-01::before { - content: "\f2706"; -} - -.hgi-stroke.hgi-structure-02::before { - content: "\f2707"; -} - -.hgi-stroke.hgi-structure-03::before { - content: "\f2708"; -} - -.hgi-stroke.hgi-structure-04::before { - content: "\f2709"; -} - -.hgi-stroke.hgi-structure-05::before { - content: "\f270a"; -} - -.hgi-stroke.hgi-structure-add::before { - content: "\f270b"; -} - -.hgi-stroke.hgi-structure-check::before { - content: "\f270c"; -} - -.hgi-stroke.hgi-structure-fail::before { - content: "\f270d"; -} - -.hgi-stroke.hgi-structure-folder-circle::before { - content: "\f270e"; -} - -.hgi-stroke.hgi-structure-folder::before { - content: "\f270f"; -} - -.hgi-stroke.hgi-student-card::before { - content: "\f2710"; -} - -.hgi-stroke.hgi-student::before { - content: "\f2711"; -} - -.hgi-stroke.hgi-students::before { - content: "\f2712"; -} - -.hgi-stroke.hgi-study-desk::before { - content: "\f2713"; -} - -.hgi-stroke.hgi-study-lamp::before { - content: "\f2714"; -} - -.hgi-stroke.hgi-stumbleupon::before { - content: "\f2715"; -} - -.hgi-stroke.hgi-submarine::before { - content: "\f2716"; -} - -.hgi-stroke.hgi-submerge::before { - content: "\f2717"; -} - -.hgi-stroke.hgi-subnode-add::before { - content: "\f2718"; -} - -.hgi-stroke.hgi-subnode-delete::before { - content: "\f2719"; -} - -.hgi-stroke.hgi-subpoena::before { - content: "\f271a"; -} - -.hgi-stroke.hgi-subscript::before { - content: "\f339d"; -} - -.hgi-stroke.hgi-subtitle::before { - content: "\f271b"; -} - -.hgi-stroke.hgi-suit-01::before { - content: "\f271c"; -} - -.hgi-stroke.hgi-suit-02::before { - content: "\f271d"; -} - -.hgi-stroke.hgi-sujood::before { - content: "\f271e"; -} - -.hgi-stroke.hgi-summation-01::before { - content: "\f271f"; -} - -.hgi-stroke.hgi-summation-02::before { - content: "\f2720"; -} - -.hgi-stroke.hgi-summation-circle::before { - content: "\f2721"; -} - -.hgi-stroke.hgi-summation-square::before { - content: "\f2722"; -} - -.hgi-stroke.hgi-sun-01::before { - content: "\f2723"; -} - -.hgi-stroke.hgi-sun-02::before { - content: "\f2724"; -} - -.hgi-stroke.hgi-sun-03::before { - content: "\f2725"; -} - -.hgi-stroke.hgi-sun-cloud-01::before { - content: "\f2726"; -} - -.hgi-stroke.hgi-sun-cloud-02::before { - content: "\f2727"; -} - -.hgi-stroke.hgi-sun-cloud-angled-rain-01::before { - content: "\f2728"; -} - -.hgi-stroke.hgi-sun-cloud-angled-rain-02::before { - content: "\f2729"; -} - -.hgi-stroke.hgi-sun-cloud-angled-rain-zap-01::before { - content: "\f272a"; -} - -.hgi-stroke.hgi-sun-cloud-angled-rain-zap-02::before { - content: "\f272b"; -} - -.hgi-stroke.hgi-sun-cloud-angled-zap-01::before { - content: "\f272c"; -} - -.hgi-stroke.hgi-sun-cloud-angled-zap-02::before { - content: "\f272d"; -} - -.hgi-stroke.hgi-sun-cloud-big-rain-01::before { - content: "\f272e"; -} - -.hgi-stroke.hgi-sun-cloud-big-rain-02::before { - content: "\f272f"; -} - -.hgi-stroke.hgi-sun-cloud-fast-wind-01::before { - content: "\f2730"; -} - -.hgi-stroke.hgi-sun-cloud-fast-wind-02::before { - content: "\f2731"; -} - -.hgi-stroke.hgi-sun-cloud-hailstone-01::before { - content: "\f2732"; -} - -.hgi-stroke.hgi-sun-cloud-hailstone-02::before { - content: "\f2733"; -} - -.hgi-stroke.hgi-sun-cloud-little-rain-01::before { - content: "\f2734"; -} - -.hgi-stroke.hgi-sun-cloud-little-rain-02::before { - content: "\f2735"; -} - -.hgi-stroke.hgi-sun-cloud-little-snow-01::before { - content: "\f2736"; -} - -.hgi-stroke.hgi-sun-cloud-little-snow-02::before { - content: "\f2737"; -} - -.hgi-stroke.hgi-sun-cloud-mid-rain-01::before { - content: "\f2738"; -} - -.hgi-stroke.hgi-sun-cloud-mid-rain-02::before { - content: "\f2739"; -} - -.hgi-stroke.hgi-sun-cloud-mid-snow-01::before { - content: "\f273a"; -} - -.hgi-stroke.hgi-sun-cloud-mid-snow-02::before { - content: "\f273b"; -} - -.hgi-stroke.hgi-sun-cloud-slow-wind-01::before { - content: "\f273c"; -} - -.hgi-stroke.hgi-sun-cloud-slow-wind-02::before { - content: "\f273d"; -} - -.hgi-stroke.hgi-sun-cloud-snow-01::before { - content: "\f273e"; -} - -.hgi-stroke.hgi-sun-cloud-snow-02::before { - content: "\f273f"; -} - -.hgi-stroke.hgi-sun-dim::before { - content: "\f339e"; -} - -.hgi-stroke.hgi-sun-medium::before { - content: "\f339f"; -} - -.hgi-stroke.hgi-sun-moon::before { - content: "\f33a0"; -} - -.hgi-stroke.hgi-sun-snow::before { - content: "\f33a1"; -} - -.hgi-stroke.hgi-sunglasses::before { - content: "\f2740"; -} - -.hgi-stroke.hgi-sunrise::before { - content: "\f2741"; -} - -.hgi-stroke.hgi-sunset::before { - content: "\f2742"; -} - -.hgi-stroke.hgi-super-mario-toad::before { - content: "\f2743"; -} - -.hgi-stroke.hgi-super-mario::before { - content: "\f2744"; -} - -.hgi-stroke.hgi-superscript::before { - content: "\f33a2"; -} - -.hgi-stroke.hgi-surfboard::before { - content: "\f2745"; -} - -.hgi-stroke.hgi-surprise::before { - content: "\f2746"; -} - -.hgi-stroke.hgi-sushi-01::before { - content: "\f2747"; -} - -.hgi-stroke.hgi-sushi-02::before { - content: "\f2748"; -} - -.hgi-stroke.hgi-sushi-03::before { - content: "\f2749"; -} - -.hgi-stroke.hgi-suspicious::before { - content: "\f274a"; -} - -.hgi-stroke.hgi-sustainable-energy::before { - content: "\f274b"; -} - -.hgi-stroke.hgi-svg-01::before { - content: "\f274c"; -} - -.hgi-stroke.hgi-svg-02::before { - content: "\f274d"; -} - -.hgi-stroke.hgi-swarm::before { - content: "\f274e"; -} - -.hgi-stroke.hgi-swatch-book::before { - content: "\f33a3"; -} - -.hgi-stroke.hgi-swatch::before { - content: "\f274f"; -} - -.hgi-stroke.hgi-swimming-cap::before { - content: "\f2750"; -} - -.hgi-stroke.hgi-swimming::before { - content: "\f2751"; -} - -.hgi-stroke.hgi-swipe-down-01::before { - content: "\f2752"; -} - -.hgi-stroke.hgi-swipe-down-02::before { - content: "\f2753"; -} - -.hgi-stroke.hgi-swipe-down-03::before { - content: "\f2754"; -} - -.hgi-stroke.hgi-swipe-down-04::before { - content: "\f2755"; -} - -.hgi-stroke.hgi-swipe-down-05::before { - content: "\f2756"; -} - -.hgi-stroke.hgi-swipe-down-06::before { - content: "\f2757"; -} - -.hgi-stroke.hgi-swipe-down-07::before { - content: "\f2758"; -} - -.hgi-stroke.hgi-swipe-down-08::before { - content: "\f2759"; -} - -.hgi-stroke.hgi-swipe-left-01::before { - content: "\f275a"; -} - -.hgi-stroke.hgi-swipe-left-02::before { - content: "\f275b"; -} - -.hgi-stroke.hgi-swipe-left-03::before { - content: "\f275c"; -} - -.hgi-stroke.hgi-swipe-left-04::before { - content: "\f275d"; -} - -.hgi-stroke.hgi-swipe-left-05::before { - content: "\f275e"; -} - -.hgi-stroke.hgi-swipe-left-06::before { - content: "\f275f"; -} - -.hgi-stroke.hgi-swipe-left-07::before { - content: "\f2760"; -} - -.hgi-stroke.hgi-swipe-left-08::before { - content: "\f2761"; -} - -.hgi-stroke.hgi-swipe-left-09::before { - content: "\f2762"; -} - -.hgi-stroke.hgi-swipe-right-01::before { - content: "\f2763"; -} - -.hgi-stroke.hgi-swipe-right-02::before { - content: "\f2764"; -} - -.hgi-stroke.hgi-swipe-right-03::before { - content: "\f2765"; -} - -.hgi-stroke.hgi-swipe-right-04::before { - content: "\f2766"; -} - -.hgi-stroke.hgi-swipe-right-05::before { - content: "\f2767"; -} - -.hgi-stroke.hgi-swipe-right-06::before { - content: "\f2768"; -} - -.hgi-stroke.hgi-swipe-right-07::before { - content: "\f2769"; -} - -.hgi-stroke.hgi-swipe-right-08::before { - content: "\f276a"; -} - -.hgi-stroke.hgi-swipe-right-09::before { - content: "\f276b"; -} - -.hgi-stroke.hgi-swipe-up-01::before { - content: "\f276c"; -} - -.hgi-stroke.hgi-swipe-up-02::before { - content: "\f276d"; -} - -.hgi-stroke.hgi-swipe-up-03::before { - content: "\f276e"; -} - -.hgi-stroke.hgi-swipe-up-04::before { - content: "\f276f"; -} - -.hgi-stroke.hgi-swipe-up-05::before { - content: "\f2770"; -} - -.hgi-stroke.hgi-swipe-up-06::before { - content: "\f2771"; -} - -.hgi-stroke.hgi-swipe-up-07::before { - content: "\f2772"; -} - -.hgi-stroke.hgi-swipe-up-08::before { - content: "\f2773"; -} - -.hgi-stroke.hgi-swiss-franc::before { - content: "\f2774"; -} - -.hgi-stroke.hgi-switch-camera::before { - content: "\f2775"; -} - -.hgi-stroke.hgi-sword-01::before { - content: "\f2776"; -} - -.hgi-stroke.hgi-sword-02::before { - content: "\f2777"; -} - -.hgi-stroke.hgi-sword-03::before { - content: "\f2778"; -} - -.hgi-stroke.hgi-swords::before { - content: "\f33a4"; -} - -.hgi-stroke.hgi-syringe::before { - content: "\f33a5"; -} - -.hgi-stroke.hgi-system-update-01::before { - content: "\f2779"; -} - -.hgi-stroke.hgi-system-update-02::before { - content: "\f277a"; -} - -.hgi-stroke.hgi-t-shirt::before { - content: "\f277b"; -} - -.hgi-stroke.hgi-table-01::before { - content: "\f278c"; -} - -.hgi-stroke.hgi-table-02::before { - content: "\f278d"; -} - -.hgi-stroke.hgi-table-03::before { - content: "\f278e"; -} - -.hgi-stroke.hgi-table-cells-merge::before { - content: "\f33a6"; -} - -.hgi-stroke.hgi-table-cells-split::before { - content: "\f33a7"; -} - -.hgi-stroke.hgi-table-columns-split::before { - content: "\f278f"; -} - -.hgi-stroke.hgi-table-lamp-01::before { - content: "\f2790"; -} - -.hgi-stroke.hgi-table-lamp-02::before { - content: "\f2791"; -} - -.hgi-stroke.hgi-table-of-contents::before { - content: "\f33a8"; -} - -.hgi-stroke.hgi-table-properties::before { - content: "\f33a9"; -} - -.hgi-stroke.hgi-table-round::before { - content: "\f2792"; -} - -.hgi-stroke.hgi-table-rows-split::before { - content: "\f2793"; -} - -.hgi-stroke.hgi-table-tennis-bat::before { - content: "\f2794"; -} - -.hgi-stroke.hgi-table::before { - content: "\f2795"; -} - -.hgi-stroke.hgi-tablet-01::before { - content: "\f2796"; -} - -.hgi-stroke.hgi-tablet-02::before { - content: "\f2797"; -} - -.hgi-stroke.hgi-tablet-connected-bluetooth::before { - content: "\f2798"; -} - -.hgi-stroke.hgi-tablet-connected-usb::before { - content: "\f2799"; -} - -.hgi-stroke.hgi-tablet-connected-wifi::before { - content: "\f279a"; -} - -.hgi-stroke.hgi-tablet-pen::before { - content: "\f279b"; -} - -.hgi-stroke.hgi-tablet-smartphone::before { - content: "\f33aa"; -} - -.hgi-stroke.hgi-tablets::before { - content: "\f279c"; -} - -.hgi-stroke.hgi-taco-01::before { - content: "\f279d"; -} - -.hgi-stroke.hgi-taco-02::before { - content: "\f279e"; -} - -.hgi-stroke.hgi-tag-01::before { - content: "\f279f"; -} - -.hgi-stroke.hgi-tag-02::before { - content: "\f27a0"; -} - -.hgi-stroke.hgi-tags::before { - content: "\f27a1"; -} - -.hgi-stroke.hgi-tailwindcss::before { - content: "\f27a2"; -} - -.hgi-stroke.hgi-taj-mahal::before { - content: "\f27a3"; -} - -.hgi-stroke.hgi-tally-1::before { - content: "\f27a4"; -} - -.hgi-stroke.hgi-tally-2::before { - content: "\f27a5"; -} - -.hgi-stroke.hgi-tally-3::before { - content: "\f27a6"; -} - -.hgi-stroke.hgi-tally-4::before { - content: "\f27a7"; -} - -.hgi-stroke.hgi-tally-5::before { - content: "\f27a8"; -} - -.hgi-stroke.hgi-tally1::before { - content: "\f33ab"; -} - -.hgi-stroke.hgi-tally2::before { - content: "\f33ac"; -} - -.hgi-stroke.hgi-tally3::before { - content: "\f33ad"; -} - -.hgi-stroke.hgi-tally4::before { - content: "\f33ae"; -} - -.hgi-stroke.hgi-tally5::before { - content: "\f33af"; -} - -.hgi-stroke.hgi-tan::before { - content: "\f27a9"; -} - -.hgi-stroke.hgi-tangent::before { - content: "\f27aa"; -} - -.hgi-stroke.hgi-tank-top::before { - content: "\f27ab"; -} - -.hgi-stroke.hgi-tanker-truck::before { - content: "\f33b0"; -} - -.hgi-stroke.hgi-tap-01::before { - content: "\f27ac"; -} - -.hgi-stroke.hgi-tap-02::before { - content: "\f27ad"; -} - -.hgi-stroke.hgi-tap-03::before { - content: "\f27ae"; -} - -.hgi-stroke.hgi-tap-04::before { - content: "\f27af"; -} - -.hgi-stroke.hgi-tap-05::before { - content: "\f27b0"; -} - -.hgi-stroke.hgi-tap-06::before { - content: "\f27b1"; -} - -.hgi-stroke.hgi-tap-07::before { - content: "\f27b2"; -} - -.hgi-stroke.hgi-tap-08::before { - content: "\f27b3"; -} - -.hgi-stroke.hgi-tape-measure::before { - content: "\f27b4"; -} - -.hgi-stroke.hgi-target-01::before { - content: "\f27b5"; -} - -.hgi-stroke.hgi-target-02::before { - content: "\f27b6"; -} - -.hgi-stroke.hgi-target-03::before { - content: "\f27b7"; -} - -.hgi-stroke.hgi-target-dollar::before { - content: "\f27b8"; -} - -.hgi-stroke.hgi-tasbih::before { - content: "\f27b9"; -} - -.hgi-stroke.hgi-task-01::before { - content: "\f27ba"; -} - -.hgi-stroke.hgi-task-02::before { - content: "\f27bb"; -} - -.hgi-stroke.hgi-task-add-01::before { - content: "\f27bc"; -} - -.hgi-stroke.hgi-task-add-02::before { - content: "\f27bd"; -} - -.hgi-stroke.hgi-task-daily-01-2::before { - content: "\f33b1"; -} - -.hgi-stroke.hgi-task-daily-01::before { - content: "\f27be"; -} - -.hgi-stroke.hgi-task-daily-02::before { - content: "\f27bf"; -} - -.hgi-stroke.hgi-task-done-01::before { - content: "\f27c0"; -} - -.hgi-stroke.hgi-task-done-02::before { - content: "\f27c1"; -} - -.hgi-stroke.hgi-task-edit-01::before { - content: "\f27c2"; -} - -.hgi-stroke.hgi-task-edit-02::before { - content: "\f27c3"; -} - -.hgi-stroke.hgi-task-remove-01::before { - content: "\f27c4"; -} - -.hgi-stroke.hgi-task-remove-02::before { - content: "\f27c5"; -} - -.hgi-stroke.hgi-taxes::before { - content: "\f27c6"; -} - -.hgi-stroke.hgi-taxi-02::before { - content: "\f27c7"; -} - -.hgi-stroke.hgi-taxi::before { - content: "\f27c8"; -} - -.hgi-stroke.hgi-tea-pod::before { - content: "\f27c9"; -} - -.hgi-stroke.hgi-tea::before { - content: "\f27ca"; -} - -.hgi-stroke.hgi-teacher::before { - content: "\f27cb"; -} - -.hgi-stroke.hgi-teaching::before { - content: "\f27cc"; -} - -.hgi-stroke.hgi-team-work::before { - content: "\f33b2"; -} - -.hgi-stroke.hgi-teamviewer::before { - content: "\f27cd"; -} - -.hgi-stroke.hgi-technician::before { - content: "\f33b3"; -} - -.hgi-stroke.hgi-telegram::before { - content: "\f27ce"; -} - -.hgi-stroke.hgi-telephone::before { - content: "\f27cf"; -} - -.hgi-stroke.hgi-telescope-01::before { - content: "\f27d0"; -} - -.hgi-stroke.hgi-telescope-02::before { - content: "\f27d1"; -} - -.hgi-stroke.hgi-television-table::before { - content: "\f27d2"; -} - -.hgi-stroke.hgi-temperature::before { - content: "\f27d3"; -} - -.hgi-stroke.hgi-tennis-ball::before { - content: "\f27d4"; -} - -.hgi-stroke.hgi-tennis-racket::before { - content: "\f27d5"; -} - -.hgi-stroke.hgi-tent-tree::before { - content: "\f27d6"; -} - -.hgi-stroke.hgi-tent::before { - content: "\f27d7"; -} - -.hgi-stroke.hgi-terminal::before { - content: "\f33b4"; -} - -.hgi-stroke.hgi-terrace::before { - content: "\f27d8"; -} - -.hgi-stroke.hgi-test-tube-01::before { - content: "\f27d9"; -} - -.hgi-stroke.hgi-test-tube-02::before { - content: "\f27da"; -} - -.hgi-stroke.hgi-test-tube-03::before { - content: "\f27db"; -} - -.hgi-stroke.hgi-test-tube-diagonal::before { - content: "\f27dc"; -} - -.hgi-stroke.hgi-test-tube::before { - content: "\f27dd"; -} - -.hgi-stroke.hgi-test-tubes::before { - content: "\f27de"; -} - -.hgi-stroke.hgi-tetris::before { - content: "\f27df"; -} - -.hgi-stroke.hgi-text-align-center::before { - content: "\f27e0"; -} - -.hgi-stroke.hgi-text-align-end::before { - content: "\f33b5"; -} - -.hgi-stroke.hgi-text-align-justify-center::before { - content: "\f27e1"; -} - -.hgi-stroke.hgi-text-align-justify-left::before { - content: "\f27e2"; -} - -.hgi-stroke.hgi-text-align-justify-right::before { - content: "\f27e3"; -} - -.hgi-stroke.hgi-text-align-justify::before { - content: "\f33b6"; -} - -.hgi-stroke.hgi-text-align-left-01::before { - content: "\f27e4"; -} - -.hgi-stroke.hgi-text-align-left::before { - content: "\f27e5"; -} - -.hgi-stroke.hgi-text-align-right-01::before { - content: "\f27e6"; -} - -.hgi-stroke.hgi-text-align-right::before { - content: "\f27e7"; -} - -.hgi-stroke.hgi-text-align-start::before { - content: "\f33b7"; -} - -.hgi-stroke.hgi-text-all-caps::before { - content: "\f27e8"; -} - -.hgi-stroke.hgi-text-bold::before { - content: "\f27e9"; -} - -.hgi-stroke.hgi-text-centerline-center-top::before { - content: "\f27ea"; -} - -.hgi-stroke.hgi-text-centerline-left::before { - content: "\f27eb"; -} - -.hgi-stroke.hgi-text-centerline-middle::before { - content: "\f27ec"; -} - -.hgi-stroke.hgi-text-centerline-right::before { - content: "\f27ed"; -} - -.hgi-stroke.hgi-text-check::before { - content: "\f27ee"; -} - -.hgi-stroke.hgi-text-circle::before { - content: "\f27ef"; -} - -.hgi-stroke.hgi-text-clear::before { - content: "\f27f0"; -} - -.hgi-stroke.hgi-text-color::before { - content: "\f27f1"; -} - -.hgi-stroke.hgi-text-creation::before { - content: "\f27f2"; -} - -.hgi-stroke.hgi-text-firstline-left::before { - content: "\f27f3"; -} - -.hgi-stroke.hgi-text-firstline-right::before { - content: "\f27f4"; -} - -.hgi-stroke.hgi-text-font::before { - content: "\f27f5"; -} - -.hgi-stroke.hgi-text-footnote::before { - content: "\f27f6"; -} - -.hgi-stroke.hgi-text-indent-01::before { - content: "\f27f7"; -} - -.hgi-stroke.hgi-text-indent-less::before { - content: "\f27f8"; -} - -.hgi-stroke.hgi-text-indent-more::before { - content: "\f27f9"; -} - -.hgi-stroke.hgi-text-indent::before { - content: "\f27fa"; -} - -.hgi-stroke.hgi-text-initial::before { - content: "\f33b8"; -} - -.hgi-stroke.hgi-text-italic-slash::before { - content: "\f27fb"; -} - -.hgi-stroke.hgi-text-italic::before { - content: "\f27fc"; -} - -.hgi-stroke.hgi-text-kerning::before { - content: "\f27fd"; -} - -.hgi-stroke.hgi-text-number-sign::before { - content: "\f27fe"; -} - -.hgi-stroke.hgi-text-quote::before { - content: "\f33b9"; -} - -.hgi-stroke.hgi-text-search::before { - content: "\f33ba"; -} - -.hgi-stroke.hgi-text-select::before { - content: "\f27ff"; -} - -.hgi-stroke.hgi-text-selection::before { - content: "\f2800"; -} - -.hgi-stroke.hgi-text-smallcaps::before { - content: "\f2801"; -} - -.hgi-stroke.hgi-text-square::before { - content: "\f2802"; -} - -.hgi-stroke.hgi-text-strikethrough::before { - content: "\f2803"; -} - -.hgi-stroke.hgi-text-subscript::before { - content: "\f2804"; -} - -.hgi-stroke.hgi-text-superscript::before { - content: "\f2805"; -} - -.hgi-stroke.hgi-text-to-image-2::before { - content: "\f33bc"; -} - -.hgi-stroke.hgi-text-to-image::before { - content: "\f33bb"; -} - -.hgi-stroke.hgi-text-to-speach-2::before { - content: "\f33be"; -} - -.hgi-stroke.hgi-text-to-speach::before { - content: "\f33bd"; -} - -.hgi-stroke.hgi-text-tracking::before { - content: "\f2806"; -} - -.hgi-stroke.hgi-text-underline::before { - content: "\f2807"; -} - -.hgi-stroke.hgi-text-variable-front::before { - content: "\f2808"; -} - -.hgi-stroke.hgi-text-vertical-alignment::before { - content: "\f2809"; -} - -.hgi-stroke.hgi-text-wrap::before { - content: "\f280a"; -} - -.hgi-stroke.hgi-text::before { - content: "\f280b"; -} - -.hgi-stroke.hgi-the-prophets-mosque::before { - content: "\f280c"; -} - -.hgi-stroke.hgi-theater::before { - content: "\f280d"; -} - -.hgi-stroke.hgi-thermometer-cold::before { - content: "\f280e"; -} - -.hgi-stroke.hgi-thermometer-snowflake::before { - content: "\f33bf"; -} - -.hgi-stroke.hgi-thermometer-sun::before { - content: "\f33c0"; -} - -.hgi-stroke.hgi-thermometer-warm::before { - content: "\f280f"; -} - -.hgi-stroke.hgi-thermometer::before { - content: "\f2810"; -} - -.hgi-stroke.hgi-third-bracket-circle::before { - content: "\f2811"; -} - -.hgi-stroke.hgi-third-bracket-square::before { - content: "\f2812"; -} - -.hgi-stroke.hgi-third-bracket::before { - content: "\f2813"; -} - -.hgi-stroke.hgi-thought-bubble::before { - content: "\f33c1"; -} - -.hgi-stroke.hgi-thread::before { - content: "\f2814"; -} - -.hgi-stroke.hgi-threads-ellipse::before { - content: "\f2815"; -} - -.hgi-stroke.hgi-threads-rectangle::before { - content: "\f2816"; -} - -.hgi-stroke.hgi-threads::before { - content: "\f2817"; -} - -.hgi-stroke.hgi-three-circle::before { - content: "\f2818"; -} - -.hgi-stroke.hgi-three-d-move::before { - content: "\f2819"; -} - -.hgi-stroke.hgi-three-d-rotate::before { - content: "\f281a"; -} - -.hgi-stroke.hgi-three-d-scale::before { - content: "\f281b"; -} - -.hgi-stroke.hgi-three-d-view::before { - content: "\f281c"; -} - -.hgi-stroke.hgi-three-finger-01::before { - content: "\f281d"; -} - -.hgi-stroke.hgi-three-finger-02::before { - content: "\f281e"; -} - -.hgi-stroke.hgi-three-finger-03::before { - content: "\f281f"; -} - -.hgi-stroke.hgi-three-finger-04::before { - content: "\f2820"; -} - -.hgi-stroke.hgi-three-finger-05::before { - content: "\f2821"; -} - -.hgi-stroke.hgi-three-g-signal::before { - content: "\f33c2"; -} - -.hgi-stroke.hgi-three-square::before { - content: "\f2822"; -} - -.hgi-stroke.hgi-thumbs-down-ellipse::before { - content: "\f2823"; -} - -.hgi-stroke.hgi-thumbs-down-rectangle::before { - content: "\f2824"; -} - -.hgi-stroke.hgi-thumbs-down::before { - content: "\f2825"; -} - -.hgi-stroke.hgi-thumbs-up-down::before { - content: "\f2826"; -} - -.hgi-stroke.hgi-thumbs-up-ellipse::before { - content: "\f2827"; -} - -.hgi-stroke.hgi-thumbs-up-rectangle::before { - content: "\f2828"; -} - -.hgi-stroke.hgi-thumbs-up::before { - content: "\f2829"; -} - -.hgi-stroke.hgi-tic-tac-toe::before { - content: "\f282a"; -} - -.hgi-stroke.hgi-tick-01::before { - content: "\f282b"; -} - -.hgi-stroke.hgi-tick-02::before { - content: "\f282c"; -} - -.hgi-stroke.hgi-tick-03::before { - content: "\f282d"; -} - -.hgi-stroke.hgi-tick-04::before { - content: "\f282e"; -} - -.hgi-stroke.hgi-tick-double-01::before { - content: "\f282f"; -} - -.hgi-stroke.hgi-tick-double-02::before { - content: "\f2830"; -} - -.hgi-stroke.hgi-tick-double-03::before { - content: "\f2831"; -} - -.hgi-stroke.hgi-tick-double-04::before { - content: "\f2832"; -} - -.hgi-stroke.hgi-ticket-01::before { - content: "\f2833"; -} - -.hgi-stroke.hgi-ticket-02::before { - content: "\f2834"; -} - -.hgi-stroke.hgi-ticket-03::before { - content: "\f2835"; -} - -.hgi-stroke.hgi-ticket-check::before { - content: "\f33c3"; -} - -.hgi-stroke.hgi-ticket-minus::before { - content: "\f33c4"; -} - -.hgi-stroke.hgi-ticket-percent::before { - content: "\f33c5"; -} - -.hgi-stroke.hgi-ticket-plus::before { - content: "\f33c6"; -} - -.hgi-stroke.hgi-ticket-slash::before { - content: "\f33c7"; -} - -.hgi-stroke.hgi-ticket-star::before { - content: "\f2836"; -} - -.hgi-stroke.hgi-ticket-x::before { - content: "\f33c8"; -} - -.hgi-stroke.hgi-tickets-plane::before { - content: "\f33ca"; -} - -.hgi-stroke.hgi-tickets::before { - content: "\f33c9"; -} - -.hgi-stroke.hgi-tie::before { - content: "\f2837"; -} - -.hgi-stroke.hgi-tiff-01::before { - content: "\f2838"; -} - -.hgi-stroke.hgi-tiff-02::before { - content: "\f2839"; -} - -.hgi-stroke.hgi-tiktok-2::before { - content: "\f33cb"; -} - -.hgi-stroke.hgi-tiktok::before { - content: "\f283a"; -} - -.hgi-stroke.hgi-tiltify::before { - content: "\f283b"; -} - -.hgi-stroke.hgi-time-01::before { - content: "\f283c"; -} - -.hgi-stroke.hgi-time-02::before { - content: "\f283d"; -} - -.hgi-stroke.hgi-time-03::before { - content: "\f283e"; -} - -.hgi-stroke.hgi-time-04::before { - content: "\f283f"; -} - -.hgi-stroke.hgi-time-half-pass::before { - content: "\f2840"; -} - -.hgi-stroke.hgi-time-management-circle::before { - content: "\f2841"; -} - -.hgi-stroke.hgi-time-management::before { - content: "\f2842"; -} - -.hgi-stroke.hgi-time-quarter-02::before { - content: "\f2843"; -} - -.hgi-stroke.hgi-time-quarter-pass::before { - content: "\f2844"; -} - -.hgi-stroke.hgi-time-quarter::before { - content: "\f2845"; -} - -.hgi-stroke.hgi-time-schedule::before { - content: "\f2846"; -} - -.hgi-stroke.hgi-time-setting-01::before { - content: "\f2847"; -} - -.hgi-stroke.hgi-time-setting-02::before { - content: "\f2848"; -} - -.hgi-stroke.hgi-time-setting-03::before { - content: "\f2849"; -} - -.hgi-stroke.hgi-time-zone::before { - content: "\f284a"; -} - -.hgi-stroke.hgi-timeline-event::before { - content: "\f284b"; -} - -.hgi-stroke.hgi-timeline-list::before { - content: "\f284c"; -} - -.hgi-stroke.hgi-timeline::before { - content: "\f284d"; -} - -.hgi-stroke.hgi-timer-01::before { - content: "\f284e"; -} - -.hgi-stroke.hgi-timer-02::before { - content: "\f284f"; -} - -.hgi-stroke.hgi-timer-off::before { - content: "\f33cd"; -} - -.hgi-stroke.hgi-timer-reset::before { - content: "\f33ce"; -} - -.hgi-stroke.hgi-tips::before { - content: "\f2850"; -} - -.hgi-stroke.hgi-tire::before { - content: "\f2851"; -} - -.hgi-stroke.hgi-tired-01::before { - content: "\f2852"; -} - -.hgi-stroke.hgi-tired-02::before { - content: "\f2853"; -} - -.hgi-stroke.hgi-tissue-paper::before { - content: "\f2854"; -} - -.hgi-stroke.hgi-toggle-left::before { - content: "\f33cf"; -} - -.hgi-stroke.hgi-toggle-off::before { - content: "\f2855"; -} - -.hgi-stroke.hgi-toggle-on::before { - content: "\f2856"; -} - -.hgi-stroke.hgi-toggle-right::before { - content: "\f33d0"; -} - -.hgi-stroke.hgi-toilet-01::before { - content: "\f2857"; -} - -.hgi-stroke.hgi-toilet-02::before { - content: "\f2858"; -} - -.hgi-stroke.hgi-token-circle::before { - content: "\f2859"; -} - -.hgi-stroke.hgi-token-square::before { - content: "\f285a"; -} - -.hgi-stroke.hgi-tongue-01::before { - content: "\f285b"; -} - -.hgi-stroke.hgi-tongue-wink-left::before { - content: "\f285c"; -} - -.hgi-stroke.hgi-tongue-wink-right::before { - content: "\f285d"; -} - -.hgi-stroke.hgi-tongue::before { - content: "\f285e"; -} - -.hgi-stroke.hgi-tool-case::before { - content: "\f285f"; -} - -.hgi-stroke.hgi-toolbox::before { - content: "\f2860"; -} - -.hgi-stroke.hgi-tools::before { - content: "\f2861"; -} - -.hgi-stroke.hgi-tornado-01::before { - content: "\f2862"; -} - -.hgi-stroke.hgi-tornado-02::before { - content: "\f2863"; -} - -.hgi-stroke.hgi-torri-gate::before { - content: "\f2864"; -} - -.hgi-stroke.hgi-torus::before { - content: "\f2865"; -} - -.hgi-stroke.hgi-touch-01::before { - content: "\f2866"; -} - -.hgi-stroke.hgi-touch-02::before { - content: "\f2867"; -} - -.hgi-stroke.hgi-touch-03::before { - content: "\f2868"; -} - -.hgi-stroke.hgi-touch-04::before { - content: "\f2869"; -} - -.hgi-stroke.hgi-touch-05::before { - content: "\f286a"; -} - -.hgi-stroke.hgi-touch-06::before { - content: "\f286b"; -} - -.hgi-stroke.hgi-touch-07::before { - content: "\f286c"; -} - -.hgi-stroke.hgi-touch-08::before { - content: "\f286d"; -} - -.hgi-stroke.hgi-touch-09::before { - content: "\f286e"; -} - -.hgi-stroke.hgi-touch-10::before { - content: "\f286f"; -} - -.hgi-stroke.hgi-touch-interaction-01::before { - content: "\f2870"; -} - -.hgi-stroke.hgi-touch-interaction-02::before { - content: "\f2871"; -} - -.hgi-stroke.hgi-touch-interaction-03::before { - content: "\f2872"; -} - -.hgi-stroke.hgi-touch-interaction-04::before { - content: "\f2873"; -} - -.hgi-stroke.hgi-touch-locked-01::before { - content: "\f2874"; -} - -.hgi-stroke.hgi-touch-locked-02::before { - content: "\f2875"; -} - -.hgi-stroke.hgi-touch-locked-03::before { - content: "\f2876"; -} - -.hgi-stroke.hgi-touch-locked-04::before { - content: "\f2877"; -} - -.hgi-stroke.hgi-touchpad-01::before { - content: "\f2878"; -} - -.hgi-stroke.hgi-touchpad-02::before { - content: "\f2879"; -} - -.hgi-stroke.hgi-touchpad-03::before { - content: "\f287a"; -} - -.hgi-stroke.hgi-touchpad-04::before { - content: "\f287b"; -} - -.hgi-stroke.hgi-touchpad-off::before { - content: "\f287c"; -} - -.hgi-stroke.hgi-tow-truck::before { - content: "\f287d"; -} - -.hgi-stroke.hgi-towel-rack::before { - content: "\f33d1"; -} - -.hgi-stroke.hgi-towels::before { - content: "\f287e"; -} - -.hgi-stroke.hgi-tower-control::before { - content: "\f33d2"; -} - -.hgi-stroke.hgi-toy-brick::before { - content: "\f287f"; -} - -.hgi-stroke.hgi-toy-train::before { - content: "\f2880"; -} - -.hgi-stroke.hgi-tractor::before { - content: "\f2881"; -} - -.hgi-stroke.hgi-trade-down::before { - content: "\f2882"; -} - -.hgi-stroke.hgi-trade-mark::before { - content: "\f2883"; -} - -.hgi-stroke.hgi-trade-up::before { - content: "\f2884"; -} - -.hgi-stroke.hgi-trademark::before { - content: "\f2885"; -} - -.hgi-stroke.hgi-traffic-cone-01::before { - content: "\f33d4"; -} - -.hgi-stroke.hgi-traffic-cone::before { - content: "\f33d3"; -} - -.hgi-stroke.hgi-traffic-incident::before { - content: "\f2886"; -} - -.hgi-stroke.hgi-traffic-jam-01::before { - content: "\f2887"; -} - -.hgi-stroke.hgi-traffic-jam-02::before { - content: "\f2888"; -} - -.hgi-stroke.hgi-traffic-light::before { - content: "\f2889"; -} - -.hgi-stroke.hgi-train-01::before { - content: "\f288a"; -} - -.hgi-stroke.hgi-train-02::before { - content: "\f288b"; -} - -.hgi-stroke.hgi-train-front-tunnel::before { - content: "\f33d6"; -} - -.hgi-stroke.hgi-train-front::before { - content: "\f33d5"; -} - -.hgi-stroke.hgi-train-track::before { - content: "\f288c"; -} - -.hgi-stroke.hgi-tram-front::before { - content: "\f33d7"; -} - -.hgi-stroke.hgi-tram::before { - content: "\f288d"; -} - -.hgi-stroke.hgi-trampoline::before { - content: "\f288e"; -} - -.hgi-stroke.hgi-transaction-history::before { - content: "\f288f"; -} - -.hgi-stroke.hgi-transaction::before { - content: "\f2890"; -} - -.hgi-stroke.hgi-transition-bottom::before { - content: "\f2891"; -} - -.hgi-stroke.hgi-transition-left::before { - content: "\f2892"; -} - -.hgi-stroke.hgi-transition-right::before { - content: "\f2893"; -} - -.hgi-stroke.hgi-transition-top::before { - content: "\f2894"; -} - -.hgi-stroke.hgi-translate::before { - content: "\f2895"; -} - -.hgi-stroke.hgi-translation::before { - content: "\f2896"; -} - -.hgi-stroke.hgi-transmission::before { - content: "\f2897"; -} - -.hgi-stroke.hgi-transparency::before { - content: "\f2898"; -} - -.hgi-stroke.hgi-trapezoid-line-horizontal::before { - content: "\f2899"; -} - -.hgi-stroke.hgi-trapezoid-line-vertical::before { - content: "\f289a"; -} - -.hgi-stroke.hgi-trash::before { - content: "\f33d8"; -} - -.hgi-stroke.hgi-travel-bag::before { - content: "\f289b"; -} - -.hgi-stroke.hgi-treadmill-01::before { - content: "\f289c"; -} - -.hgi-stroke.hgi-treadmill-02::before { - content: "\f289d"; -} - -.hgi-stroke.hgi-treatment::before { - content: "\f289e"; -} - -.hgi-stroke.hgi-tree-01::before { - content: "\f289f"; -} - -.hgi-stroke.hgi-tree-02::before { - content: "\f28a0"; -} - -.hgi-stroke.hgi-tree-03::before { - content: "\f28a1"; -} - -.hgi-stroke.hgi-tree-04::before { - content: "\f28a2"; -} - -.hgi-stroke.hgi-tree-05::before { - content: "\f28a3"; -} - -.hgi-stroke.hgi-tree-06::before { - content: "\f28a4"; -} - -.hgi-stroke.hgi-tree-07::before { - content: "\f28a5"; -} - -.hgi-stroke.hgi-tree-deciduous::before { - content: "\f33d9"; -} - -.hgi-stroke.hgi-tree-palm::before { - content: "\f28a6"; -} - -.hgi-stroke.hgi-trees::before { - content: "\f28a7"; -} - -.hgi-stroke.hgi-trello::before { - content: "\f28a8"; -} - -.hgi-stroke.hgi-trending-down::before { - content: "\f33da"; -} - -.hgi-stroke.hgi-trending-up-down::before { - content: "\f28a9"; -} - -.hgi-stroke.hgi-trending-up::before { - content: "\f33db"; -} - -.hgi-stroke.hgi-triangle-01::before { - content: "\f28aa"; -} - -.hgi-stroke.hgi-triangle-02::before { - content: "\f28ab"; -} - -.hgi-stroke.hgi-triangle-03::before { - content: "\f28ac"; -} - -.hgi-stroke.hgi-triangle-alert::before { - content: "\f33dc"; -} - -.hgi-stroke.hgi-triangle-dash::before { - content: "\f28ad"; -} - -.hgi-stroke.hgi-triangle-dashed::before { - content: "\f33dd"; -} - -.hgi-stroke.hgi-triangle-right::before { - content: "\f28ae"; -} - -.hgi-stroke.hgi-triangle::before { - content: "\f28af"; -} - -.hgi-stroke.hgi-trolley-01::before { - content: "\f28b0"; -} - -.hgi-stroke.hgi-trolley-02::before { - content: "\f28b1"; -} - -.hgi-stroke.hgi-trophy::before { - content: "\f33de"; -} - -.hgi-stroke.hgi-tropical-storm-tracks-01::before { - content: "\f28b2"; -} - -.hgi-stroke.hgi-tropical-storm-tracks-02::before { - content: "\f28b3"; -} - -.hgi-stroke.hgi-tropical-storm::before { - content: "\f28b4"; -} - -.hgi-stroke.hgi-truck-delivery::before { - content: "\f28b5"; -} - -.hgi-stroke.hgi-truck-electric::before { - content: "\f33df"; -} - -.hgi-stroke.hgi-truck-monster::before { - content: "\f28b6"; -} - -.hgi-stroke.hgi-truck-return::before { - content: "\f28b7"; -} - -.hgi-stroke.hgi-truck::before { - content: "\f28b8"; -} - -.hgi-stroke.hgi-trulli::before { - content: "\f28b9"; -} - -.hgi-stroke.hgi-tsunami::before { - content: "\f28ba"; -} - -.hgi-stroke.hgi-tulip::before { - content: "\f28bb"; -} - -.hgi-stroke.hgi-tumblr::before { - content: "\f28bc"; -} - -.hgi-stroke.hgi-turkish-lira::before { - content: "\f28bd"; -} - -.hgi-stroke.hgi-turntable::before { - content: "\f33e0"; -} - -.hgi-stroke.hgi-turtle-neck::before { - content: "\f28be"; -} - -.hgi-stroke.hgi-turtle::before { - content: "\f33e1"; -} - -.hgi-stroke.hgi-tv-01::before { - content: "\f28bf"; -} - -.hgi-stroke.hgi-tv-02::before { - content: "\f28c0"; -} - -.hgi-stroke.hgi-tv-fix::before { - content: "\f28c1"; -} - -.hgi-stroke.hgi-tv-issue::before { - content: "\f28c2"; -} - -.hgi-stroke.hgi-tv-minimal-play::before { - content: "\f33e3"; -} - -.hgi-stroke.hgi-tv-minimal::before { - content: "\f33e2"; -} - -.hgi-stroke.hgi-tv-smart::before { - content: "\f28c3"; -} - -.hgi-stroke.hgi-twin-tower::before { - content: "\f28c4"; -} - -.hgi-stroke.hgi-twitch::before { - content: "\f28c5"; -} - -.hgi-stroke.hgi-twitter-square::before { - content: "\f28c6"; -} - -.hgi-stroke.hgi-twitter::before { - content: "\f28c7"; -} - -.hgi-stroke.hgi-two-circle::before { - content: "\f28c8"; -} - -.hgi-stroke.hgi-two-factor-access::before { - content: "\f28c9"; -} - -.hgi-stroke.hgi-two-finger-01::before { - content: "\f28ca"; -} - -.hgi-stroke.hgi-two-finger-02::before { - content: "\f28cb"; -} - -.hgi-stroke.hgi-two-finger-03::before { - content: "\f28cc"; -} - -.hgi-stroke.hgi-two-finger-04::before { - content: "\f28cd"; -} - -.hgi-stroke.hgi-two-finger-05::before { - content: "\f28ce"; -} - -.hgi-stroke.hgi-two-square::before { - content: "\f28cf"; -} - -.hgi-stroke.hgi-txt-01::before { - content: "\f28d0"; -} - -.hgi-stroke.hgi-txt-02::before { - content: "\f28d1"; -} - -.hgi-stroke.hgi-type-cursor::before { - content: "\f28d2"; -} - -.hgi-stroke.hgi-type-outline::before { - content: "\f33e5"; -} - -.hgi-stroke.hgi-type::before { - content: "\f33e4"; -} - -.hgi-stroke.hgi-typescript-01::before { - content: "\f28d3"; -} - -.hgi-stroke.hgi-typescript-02::before { - content: "\f28d4"; -} - -.hgi-stroke.hgi-typescript-03::before { - content: "\f28d5"; -} - -.hgi-stroke.hgi-uber::before { - content: "\f28da"; -} - -.hgi-stroke.hgi-ufo-01::before { - content: "\f28db"; -} - -.hgi-stroke.hgi-ufo::before { - content: "\f28dc"; -} - -.hgi-stroke.hgi-umbrella-dollar::before { - content: "\f28dd"; -} - -.hgi-stroke.hgi-umbrella-off::before { - content: "\f28de"; -} - -.hgi-stroke.hgi-umbrella::before { - content: "\f28df"; -} - -.hgi-stroke.hgi-unamused::before { - content: "\f28e0"; -} - -.hgi-stroke.hgi-unarchive-03::before { - content: "\f28e1"; -} - -.hgi-stroke.hgi-unavailable::before { - content: "\f28e2"; -} - -.hgi-stroke.hgi-underline::before { - content: "\f33e6"; -} - -.hgi-stroke.hgi-underpants-01::before { - content: "\f28e3"; -} - -.hgi-stroke.hgi-underpants-02::before { - content: "\f28e4"; -} - -.hgi-stroke.hgi-underpants-03::before { - content: "\f28e5"; -} - -.hgi-stroke.hgi-undo-02::before { - content: "\f28e6"; -} - -.hgi-stroke.hgi-undo-03::before { - content: "\f28e7"; -} - -.hgi-stroke.hgi-undo-dot::before { - content: "\f33e7"; -} - -.hgi-stroke.hgi-undo::before { - content: "\f28e8"; -} - -.hgi-stroke.hgi-unfold-horizontal::before { - content: "\f33e8"; -} - -.hgi-stroke.hgi-unfold-less::before { - content: "\f28e9"; -} - -.hgi-stroke.hgi-unfold-more-2::before { - content: "\f33e9"; -} - -.hgi-stroke.hgi-unfold-more-down::before { - content: "\f28ea"; -} - -.hgi-stroke.hgi-unfold-more-up::before { - content: "\f28eb"; -} - -.hgi-stroke.hgi-unfold-more::before { - content: "\f28ec"; -} - -.hgi-stroke.hgi-unfold-vertical::before { - content: "\f33ea"; -} - -.hgi-stroke.hgi-ungroup-items::before { - content: "\f28ed"; -} - -.hgi-stroke.hgi-ungroup-layers::before { - content: "\f28ee"; -} - -.hgi-stroke.hgi-ungroup::before { - content: "\f28ef"; -} - -.hgi-stroke.hgi-unhappy::before { - content: "\f28f0"; -} - -.hgi-stroke.hgi-universal-access-circle-2::before { - content: "\f33eb"; -} - -.hgi-stroke.hgi-universal-access-circle::before { - content: "\f28f1"; -} - -.hgi-stroke.hgi-universal-access::before { - content: "\f28f2"; -} - -.hgi-stroke.hgi-university::before { - content: "\f28f3"; -} - -.hgi-stroke.hgi-unlink-01::before { - content: "\f28f4"; -} - -.hgi-stroke.hgi-unlink-02::before { - content: "\f28f5"; -} - -.hgi-stroke.hgi-unlink-03::before { - content: "\f28f6"; -} - -.hgi-stroke.hgi-unlink-04::before { - content: "\f28f7"; -} - -.hgi-stroke.hgi-unlink-05::before { - content: "\f28f8"; -} - -.hgi-stroke.hgi-unlink-06::before { - content: "\f28f9"; -} - -.hgi-stroke.hgi-unplug::before { - content: "\f33ec"; -} - -.hgi-stroke.hgi-unsplash::before { - content: "\f28fa"; -} - -.hgi-stroke.hgi-upload-01::before { - content: "\f28fb"; -} - -.hgi-stroke.hgi-upload-02::before { - content: "\f28fc"; -} - -.hgi-stroke.hgi-upload-03::before { - content: "\f28fd"; -} - -.hgi-stroke.hgi-upload-04::before { - content: "\f28fe"; -} - -.hgi-stroke.hgi-upload-05::before { - content: "\f28ff"; -} - -.hgi-stroke.hgi-upload-06::before { - content: "\f2900"; -} - -.hgi-stroke.hgi-upload-circle-01::before { - content: "\f2901"; -} - -.hgi-stroke.hgi-upload-circle-02::before { - content: "\f2902"; -} - -.hgi-stroke.hgi-upload-square-01::before { - content: "\f2903"; -} - -.hgi-stroke.hgi-upload-square-02::before { - content: "\f2904"; -} - -.hgi-stroke.hgi-upwork::before { - content: "\f2905"; -} - -.hgi-stroke.hgi-usb-bugs::before { - content: "\f2906"; -} - -.hgi-stroke.hgi-usb-connected-01::before { - content: "\f2907"; -} - -.hgi-stroke.hgi-usb-connected-02::before { - content: "\f2908"; -} - -.hgi-stroke.hgi-usb-connected-03::before { - content: "\f2909"; -} - -.hgi-stroke.hgi-usb-error::before { - content: "\f290a"; -} - -.hgi-stroke.hgi-usb-memory-01::before { - content: "\f290b"; -} - -.hgi-stroke.hgi-usb-memory-02::before { - content: "\f290c"; -} - -.hgi-stroke.hgi-usb-not-connected-01::before { - content: "\f290d"; -} - -.hgi-stroke.hgi-usb-not-connected-02::before { - content: "\f290e"; -} - -.hgi-stroke.hgi-usb::before { - content: "\f290f"; -} - -.hgi-stroke.hgi-usdt::before { - content: "\f2910"; -} - -.hgi-stroke.hgi-user-02::before { - content: "\f2911"; -} - -.hgi-stroke.hgi-user-03::before { - content: "\f2912"; -} - -.hgi-stroke.hgi-user-account::before { - content: "\f2913"; -} - -.hgi-stroke.hgi-user-add-01::before { - content: "\f2914"; -} - -.hgi-stroke.hgi-user-add-02::before { - content: "\f2915"; -} - -.hgi-stroke.hgi-user-ai::before { - content: "\f2916"; -} - -.hgi-stroke.hgi-user-arrow-left-right::before { - content: "\f2917"; -} - -.hgi-stroke.hgi-user-block-01::before { - content: "\f2918"; -} - -.hgi-stroke.hgi-user-block-02::before { - content: "\f2919"; -} - -.hgi-stroke.hgi-user-check-01::before { - content: "\f291a"; -} - -.hgi-stroke.hgi-user-check-02::before { - content: "\f291b"; -} - -.hgi-stroke.hgi-user-circle-02::before { - content: "\f291c"; -} - -.hgi-stroke.hgi-user-circle::before { - content: "\f291d"; -} - -.hgi-stroke.hgi-user-cog::before { - content: "\f33ed"; -} - -.hgi-stroke.hgi-user-dollar::before { - content: "\f291e"; -} - -.hgi-stroke.hgi-user-edit-01::before { - content: "\f291f"; -} - -.hgi-stroke.hgi-user-full-view::before { - content: "\f2920"; -} - -.hgi-stroke.hgi-user-group-02::before { - content: "\f2921"; -} - -.hgi-stroke.hgi-user-group-03::before { - content: "\f2922"; -} - -.hgi-stroke.hgi-user-group::before { - content: "\f2923"; -} - -.hgi-stroke.hgi-user-id-verification::before { - content: "\f2924"; -} - -.hgi-stroke.hgi-user-key::before { - content: "\f33ee"; -} - -.hgi-stroke.hgi-user-list::before { - content: "\f2925"; -} - -.hgi-stroke.hgi-user-lock-01::before { - content: "\f2926"; -} - -.hgi-stroke.hgi-user-lock-02::before { - content: "\f2927"; -} - -.hgi-stroke.hgi-user-love-01::before { - content: "\f2928"; -} - -.hgi-stroke.hgi-user-love-02::before { - content: "\f2929"; -} - -.hgi-stroke.hgi-user-minus-01::before { - content: "\f292a"; -} - -.hgi-stroke.hgi-user-minus-02::before { - content: "\f292b"; -} - -.hgi-stroke.hgi-user-multiple-02::before { - content: "\f292c"; -} - -.hgi-stroke.hgi-user-multiple-03::before { - content: "\f292d"; -} - -.hgi-stroke.hgi-user-multiple::before { - content: "\f292e"; -} - -.hgi-stroke.hgi-user-pen::before { - content: "\f33ef"; -} - -.hgi-stroke.hgi-user-plus::before { - content: "\f33f0"; -} - -.hgi-stroke.hgi-user-question-01::before { - content: "\f292f"; -} - -.hgi-stroke.hgi-user-question-02::before { - content: "\f2930"; -} - -.hgi-stroke.hgi-user-remove-01::before { - content: "\f2931"; -} - -.hgi-stroke.hgi-user-remove-02::before { - content: "\f2932"; -} - -.hgi-stroke.hgi-user-roadside::before { - content: "\f2933"; -} - -.hgi-stroke.hgi-user-round-check::before { - content: "\f33f2"; -} - -.hgi-stroke.hgi-user-round-cog::before { - content: "\f33f3"; -} - -.hgi-stroke.hgi-user-round-key::before { - content: "\f33f4"; -} - -.hgi-stroke.hgi-user-round-minus::before { - content: "\f33f5"; -} - -.hgi-stroke.hgi-user-round-pen::before { - content: "\f33f6"; -} - -.hgi-stroke.hgi-user-round-plus::before { - content: "\f33f7"; -} - -.hgi-stroke.hgi-user-round-search::before { - content: "\f33f8"; -} - -.hgi-stroke.hgi-user-round-x::before { - content: "\f33f9"; -} - -.hgi-stroke.hgi-user-round::before { - content: "\f33f1"; -} - -.hgi-stroke.hgi-user-search-01::before { - content: "\f2934"; -} - -.hgi-stroke.hgi-user-search-02::before { - content: "\f2935"; -} - -.hgi-stroke.hgi-user-settings-01::before { - content: "\f2936"; -} - -.hgi-stroke.hgi-user-settings-02::before { - content: "\f2937"; -} - -.hgi-stroke.hgi-user-sharing::before { - content: "\f2938"; -} - -.hgi-stroke.hgi-user-shield-01::before { - content: "\f2939"; -} - -.hgi-stroke.hgi-user-shield-02::before { - content: "\f293a"; -} - -.hgi-stroke.hgi-user-square::before { - content: "\f293b"; -} - -.hgi-stroke.hgi-user-star-01::before { - content: "\f293c"; -} - -.hgi-stroke.hgi-user-star-02::before { - content: "\f293d"; -} - -.hgi-stroke.hgi-user-status::before { - content: "\f293e"; -} - -.hgi-stroke.hgi-user-story::before { - content: "\f293f"; -} - -.hgi-stroke.hgi-user-switch::before { - content: "\f2940"; -} - -.hgi-stroke.hgi-user-time-01::before { - content: "\f2941"; -} - -.hgi-stroke.hgi-user-time-02::before { - content: "\f2942"; -} - -.hgi-stroke.hgi-user-time-03::before { - content: "\f2943"; -} - -.hgi-stroke.hgi-user-unlock-01::before { - content: "\f2944"; -} - -.hgi-stroke.hgi-user-unlock-02::before { - content: "\f2945"; -} - -.hgi-stroke.hgi-user-warning-01::before { - content: "\f2946"; -} - -.hgi-stroke.hgi-user-warning-02::before { - content: "\f2947"; -} - -.hgi-stroke.hgi-user-warning-03::before { - content: "\f2948"; -} - -.hgi-stroke.hgi-user-x::before { - content: "\f33fa"; -} - -.hgi-stroke.hgi-user::before { - content: "\f2949"; -} - -.hgi-stroke.hgi-users-round::before { - content: "\f33fc"; -} - -.hgi-stroke.hgi-users::before { - content: "\f33fb"; -} - -.hgi-stroke.hgi-utensils-crossed::before { - content: "\f33fe"; -} - -.hgi-stroke.hgi-utensils::before { - content: "\f33fd"; -} - -.hgi-stroke.hgi-utility-pole::before { - content: "\f294a"; -} - -.hgi-stroke.hgi-uv-01::before { - content: "\f294b"; -} - -.hgi-stroke.hgi-uv-02::before { - content: "\f294c"; -} - -.hgi-stroke.hgi-uv-03::before { - content: "\f294d"; -} - -.hgi-stroke.hgi-vaccine::before { - content: "\f2952"; -} - -.hgi-stroke.hgi-vacuum-cleaner::before { - content: "\f2953"; -} - -.hgi-stroke.hgi-validation-approval::before { - content: "\f2954"; -} - -.hgi-stroke.hgi-validation::before { - content: "\f2955"; -} - -.hgi-stroke.hgi-van::before { - content: "\f2956"; -} - -.hgi-stroke.hgi-variable::before { - content: "\f2957"; -} - -.hgi-stroke.hgi-vault::before { - content: "\f33ff"; -} - -.hgi-stroke.hgi-vector-square::before { - content: "\f2958"; -} - -.hgi-stroke.hgi-vegan::before { - content: "\f3400"; -} - -.hgi-stroke.hgi-vegetarian-food::before { - content: "\f2959"; -} - -.hgi-stroke.hgi-venetian-mask::before { - content: "\f295a"; -} - -.hgi-stroke.hgi-venus-and-mars::before { - content: "\f3402"; -} - -.hgi-stroke.hgi-venus::before { - content: "\f3401"; -} - -.hgi-stroke.hgi-versus::before { - content: "\f295b"; -} - -.hgi-stroke.hgi-vertical-resize::before { - content: "\f295c"; -} - -.hgi-stroke.hgi-vertical-scroll-point::before { - content: "\f295d"; -} - -.hgi-stroke.hgi-vest::before { - content: "\f295e"; -} - -.hgi-stroke.hgi-viber::before { - content: "\f295f"; -} - -.hgi-stroke.hgi-vibrate-off::before { - content: "\f3403"; -} - -.hgi-stroke.hgi-vibrate::before { - content: "\f2960"; -} - -.hgi-stroke.hgi-victory-finger-01::before { - content: "\f2961"; -} - -.hgi-stroke.hgi-victory-finger-02::before { - content: "\f2962"; -} - -.hgi-stroke.hgi-victory-finger-03::before { - content: "\f2963"; -} - -.hgi-stroke.hgi-video-01::before { - content: "\f2964"; -} - -.hgi-stroke.hgi-video-02::before { - content: "\f2965"; -} - -.hgi-stroke.hgi-video-ai::before { - content: "\f2966"; -} - -.hgi-stroke.hgi-video-camera-ai::before { - content: "\f2967"; -} - -.hgi-stroke.hgi-video-console::before { - content: "\f2968"; -} - -.hgi-stroke.hgi-video-off::before { - content: "\f2969"; -} - -.hgi-stroke.hgi-video-replay::before { - content: "\f296a"; -} - -.hgi-stroke.hgi-videotape::before { - content: "\f296b"; -} - -.hgi-stroke.hgi-view-agenda::before { - content: "\f296c"; -} - -.hgi-stroke.hgi-view-off-slash::before { - content: "\f296d"; -} - -.hgi-stroke.hgi-view-off::before { - content: "\f296e"; -} - -.hgi-stroke.hgi-view-sidebar-left::before { - content: "\f296f"; -} - -.hgi-stroke.hgi-view-sidebar-right::before { - content: "\f2970"; -} - -.hgi-stroke.hgi-view::before { - content: "\f2971"; -} - -.hgi-stroke.hgi-villa-01::before { - content: "\f2972"; -} - -.hgi-stroke.hgi-villa-02::before { - content: "\f2973"; -} - -.hgi-stroke.hgi-vimeo::before { - content: "\f2974"; -} - -.hgi-stroke.hgi-vine-square::before { - content: "\f2975"; -} - -.hgi-stroke.hgi-vine::before { - content: "\f2976"; -} - -.hgi-stroke.hgi-vintage-clock::before { - content: "\f2977"; -} - -.hgi-stroke.hgi-virtual-reality-vr-01::before { - content: "\f2978"; -} - -.hgi-stroke.hgi-virtual-reality-vr-02::before { - content: "\f2979"; -} - -.hgi-stroke.hgi-vision::before { - content: "\f297a"; -} - -.hgi-stroke.hgi-visual-studio-code::before { - content: "\f297b"; -} - -.hgi-stroke.hgi-vk-square::before { - content: "\f297c"; -} - -.hgi-stroke.hgi-vk::before { - content: "\f297d"; -} - -.hgi-stroke.hgi-voice-comment-2::before { - content: "\f3405"; -} - -.hgi-stroke.hgi-voice-comment::before { - content: "\f3404"; -} - -.hgi-stroke.hgi-voice-id::before { - content: "\f297e"; -} - -.hgi-stroke.hgi-voice-to-text-2::before { - content: "\f3407"; -} - -.hgi-stroke.hgi-voice-to-text::before { - content: "\f3406"; -} - -.hgi-stroke.hgi-voice::before { - content: "\f297f"; -} - -.hgi-stroke.hgi-voicemail::before { - content: "\f3408"; -} - -.hgi-stroke.hgi-volleyball::before { - content: "\f2980"; -} - -.hgi-stroke.hgi-volume-01::before { - content: "\f340a"; -} - -.hgi-stroke.hgi-volume-02::before { - content: "\f340b"; -} - -.hgi-stroke.hgi-volume-high::before { - content: "\f2981"; -} - -.hgi-stroke.hgi-volume-low::before { - content: "\f2982"; -} - -.hgi-stroke.hgi-volume-minus::before { - content: "\f2983"; -} - -.hgi-stroke.hgi-volume-mute-01::before { - content: "\f2984"; -} - -.hgi-stroke.hgi-volume-mute-02::before { - content: "\f2985"; -} - -.hgi-stroke.hgi-volume-off::before { - content: "\f2986"; -} - -.hgi-stroke.hgi-volume-up::before { - content: "\f2987"; -} - -.hgi-stroke.hgi-volume-x::before { - content: "\f340c"; -} - -.hgi-stroke.hgi-volume::before { - content: "\f3409"; -} - -.hgi-stroke.hgi-volume1::before { - content: "\f340d"; -} - -.hgi-stroke.hgi-volume2::before { - content: "\f340e"; -} - -.hgi-stroke.hgi-vomiting::before { - content: "\f2988"; -} - -.hgi-stroke.hgi-vote::before { - content: "\f340f"; -} - -.hgi-stroke.hgi-voucher::before { - content: "\f2989"; -} - -.hgi-stroke.hgi-vr-glasses::before { - content: "\f298a"; -} - -.hgi-stroke.hgi-vr::before { - content: "\f298b"; -} - -.hgi-stroke.hgi-vynil-01::before { - content: "\f298c"; -} - -.hgi-stroke.hgi-vynil-02::before { - content: "\f298d"; -} - -.hgi-stroke.hgi-vynil-03::before { - content: "\f298e"; -} - -.hgi-stroke.hgi-w-three-schools::before { - content: "\f298f"; -} - -.hgi-stroke.hgi-waiter::before { - content: "\f29a5"; -} - -.hgi-stroke.hgi-waiters::before { - content: "\f29a6"; -} - -.hgi-stroke.hgi-walking::before { - content: "\f29a7"; -} - -.hgi-stroke.hgi-wall-lamp::before { - content: "\f29a8"; -} - -.hgi-stroke.hgi-wallet-01::before { - content: "\f29a9"; -} - -.hgi-stroke.hgi-wallet-02::before { - content: "\f29aa"; -} - -.hgi-stroke.hgi-wallet-03::before { - content: "\f29ab"; -} - -.hgi-stroke.hgi-wallet-04::before { - content: "\f29ac"; -} - -.hgi-stroke.hgi-wallet-05::before { - content: "\f29ad"; -} - -.hgi-stroke.hgi-wallet-add-01::before { - content: "\f29ae"; -} - -.hgi-stroke.hgi-wallet-add-02::before { - content: "\f29af"; -} - -.hgi-stroke.hgi-wallet-cards::before { - content: "\f29b0"; -} - -.hgi-stroke.hgi-wallet-done-01::before { - content: "\f29b1"; -} - -.hgi-stroke.hgi-wallet-done-02::before { - content: "\f29b2"; -} - -.hgi-stroke.hgi-wallet-minimal::before { - content: "\f3410"; -} - -.hgi-stroke.hgi-wallet-not-found-01::before { - content: "\f29b3"; -} - -.hgi-stroke.hgi-wallet-not-found-02::before { - content: "\f29b4"; -} - -.hgi-stroke.hgi-wallet-remove-01::before { - content: "\f29b5"; -} - -.hgi-stroke.hgi-wallet-remove-02::before { - content: "\f29b6"; -} - -.hgi-stroke.hgi-wallpaper::before { - content: "\f3411"; -} - -.hgi-stroke.hgi-walpaper::before { - content: "\f29b7"; -} - -.hgi-stroke.hgi-wand-sparkles::before { - content: "\f3413"; -} - -.hgi-stroke.hgi-wand::before { - content: "\f3412"; -} - -.hgi-stroke.hgi-wanted::before { - content: "\f29b8"; -} - -.hgi-stroke.hgi-wardrobe-01::before { - content: "\f29b9"; -} - -.hgi-stroke.hgi-wardrobe-02::before { - content: "\f29ba"; -} - -.hgi-stroke.hgi-wardrobe-03::before { - content: "\f29bb"; -} - -.hgi-stroke.hgi-wardrobe-04::before { - content: "\f29bc"; -} - -.hgi-stroke.hgi-warehouse::before { - content: "\f29bd"; -} - -.hgi-stroke.hgi-washing-machine::before { - content: "\f29be"; -} - -.hgi-stroke.hgi-washington-monument::before { - content: "\f29bf"; -} - -.hgi-stroke.hgi-waste-restore::before { - content: "\f29c0"; -} - -.hgi-stroke.hgi-waste::before { - content: "\f29c1"; -} - -.hgi-stroke.hgi-watch-01::before { - content: "\f29c2"; -} - -.hgi-stroke.hgi-watch-02::before { - content: "\f29c3"; -} - -.hgi-stroke.hgi-water-energy::before { - content: "\f29c4"; -} - -.hgi-stroke.hgi-water-polo::before { - content: "\f29c5"; -} - -.hgi-stroke.hgi-water-pump::before { - content: "\f29c6"; -} - -.hgi-stroke.hgi-waterfall-down-01::before { - content: "\f29c7"; -} - -.hgi-stroke.hgi-waterfall-down-03::before { - content: "\f29c8"; -} - -.hgi-stroke.hgi-waterfall-up-01::before { - content: "\f29c9"; -} - -.hgi-stroke.hgi-waterfall-up-02::before { - content: "\f29ca"; -} - -.hgi-stroke.hgi-watermelon::before { - content: "\f29cb"; -} - -.hgi-stroke.hgi-wattpad-square::before { - content: "\f29cc"; -} - -.hgi-stroke.hgi-wattpad::before { - content: "\f29cd"; -} - -.hgi-stroke.hgi-wav-01::before { - content: "\f29ce"; -} - -.hgi-stroke.hgi-wav-02::before { - content: "\f29cf"; -} - -.hgi-stroke.hgi-wave-square::before { - content: "\f29d0"; -} - -.hgi-stroke.hgi-wave-triangle::before { - content: "\f29d1"; -} - -.hgi-stroke.hgi-wave::before { - content: "\f29d2"; -} - -.hgi-stroke.hgi-waves-arrow-down::before { - content: "\f3415"; -} - -.hgi-stroke.hgi-waves-arrow-up::before { - content: "\f3416"; -} - -.hgi-stroke.hgi-waves-ladder::before { - content: "\f3417"; -} - -.hgi-stroke.hgi-waves::before { - content: "\f3414"; -} - -.hgi-stroke.hgi-waving-hand-01::before { - content: "\f29d3"; -} - -.hgi-stroke.hgi-waving-hand-02::before { - content: "\f29d4"; -} - -.hgi-stroke.hgi-waypoints::before { - content: "\f3418"; -} - -.hgi-stroke.hgi-waze::before { - content: "\f29d5"; -} - -.hgi-stroke.hgi-web-design-01::before { - content: "\f29d6"; -} - -.hgi-stroke.hgi-web-design-02::before { - content: "\f29d7"; -} - -.hgi-stroke.hgi-web-programming::before { - content: "\f29d8"; -} - -.hgi-stroke.hgi-web-protection::before { - content: "\f29d9"; -} - -.hgi-stroke.hgi-web-security::before { - content: "\f29da"; -} - -.hgi-stroke.hgi-web-validation::before { - content: "\f29db"; -} - -.hgi-stroke.hgi-webcam::before { - content: "\f3419"; -} - -.hgi-stroke.hgi-webflow-ellipse::before { - content: "\f341a"; -} - -.hgi-stroke.hgi-webflow-rectangle::before { - content: "\f341b"; -} - -.hgi-stroke.hgi-webflow::before { - content: "\f29dc"; -} - -.hgi-stroke.hgi-webhook-off::before { - content: "\f29dd"; -} - -.hgi-stroke.hgi-webhook::before { - content: "\f29de"; -} - -.hgi-stroke.hgi-wechat::before { - content: "\f29df"; -} - -.hgi-stroke.hgi-wedding::before { - content: "\f29e0"; -} - -.hgi-stroke.hgi-weight-scale-01::before { - content: "\f29e1"; -} - -.hgi-stroke.hgi-weight-scale::before { - content: "\f29e2"; -} - -.hgi-stroke.hgi-weight-tilde::before { - content: "\f341d"; -} - -.hgi-stroke.hgi-weight::before { - content: "\f341c"; -} - -.hgi-stroke.hgi-wellness::before { - content: "\f29e3"; -} - -.hgi-stroke.hgi-whatsapp-business::before { - content: "\f29e4"; -} - -.hgi-stroke.hgi-whatsapp::before { - content: "\f29e5"; -} - -.hgi-stroke.hgi-wheat-off::before { - content: "\f29e6"; -} - -.hgi-stroke.hgi-wheat::before { - content: "\f29e7"; -} - -.hgi-stroke.hgi-wheelchair::before { - content: "\f29e8"; -} - -.hgi-stroke.hgi-whisk::before { - content: "\f29e9"; -} - -.hgi-stroke.hgi-whistle::before { - content: "\f29ea"; -} - -.hgi-stroke.hgi-whiteboard::before { - content: "\f29eb"; -} - -.hgi-stroke.hgi-whole-word::before { - content: "\f341e"; -} - -.hgi-stroke.hgi-wifi-01::before { - content: "\f29ec"; -} - -.hgi-stroke.hgi-wifi-02::before { - content: "\f29ed"; -} - -.hgi-stroke.hgi-wifi-circle::before { - content: "\f29ee"; -} - -.hgi-stroke.hgi-wifi-cog::before { - content: "\f341f"; -} - -.hgi-stroke.hgi-wifi-connected-01::before { - content: "\f29ef"; -} - -.hgi-stroke.hgi-wifi-connected-02::before { - content: "\f29f0"; -} - -.hgi-stroke.hgi-wifi-connected-03-2::before { - content: "\f3420"; -} - -.hgi-stroke.hgi-wifi-connected-03::before { - content: "\f29f1"; -} - -.hgi-stroke.hgi-wifi-disconnected-01::before { - content: "\f29f2"; -} - -.hgi-stroke.hgi-wifi-disconnected-02::before { - content: "\f29f3"; -} - -.hgi-stroke.hgi-wifi-disconnected-03::before { - content: "\f29f4"; -} - -.hgi-stroke.hgi-wifi-disconnected-04::before { - content: "\f29f5"; -} - -.hgi-stroke.hgi-wifi-error-01::before { - content: "\f29f6"; -} - -.hgi-stroke.hgi-wifi-error-02::before { - content: "\f29f7"; -} - -.hgi-stroke.hgi-wifi-full-signal::before { - content: "\f29f8"; -} - -.hgi-stroke.hgi-wifi-high::before { - content: "\f3421"; -} - -.hgi-stroke.hgi-wifi-location::before { - content: "\f29f9"; -} - -.hgi-stroke.hgi-wifi-lock::before { - content: "\f29fa"; -} - -.hgi-stroke.hgi-wifi-low-signal::before { - content: "\f29fb"; -} - -.hgi-stroke.hgi-wifi-low::before { - content: "\f3422"; -} - -.hgi-stroke.hgi-wifi-medium-signal::before { - content: "\f29fc"; -} - -.hgi-stroke.hgi-wifi-no-signal::before { - content: "\f29fd"; -} - -.hgi-stroke.hgi-wifi-off-01::before { - content: "\f29fe"; -} - -.hgi-stroke.hgi-wifi-off-02::before { - content: "\f29ff"; -} - -.hgi-stroke.hgi-wifi-pen::before { - content: "\f2a00"; -} - -.hgi-stroke.hgi-wifi-square::before { - content: "\f2a01"; -} - -.hgi-stroke.hgi-wifi-sync::before { - content: "\f2a02"; -} - -.hgi-stroke.hgi-wifi-unlock::before { - content: "\f2a03"; -} - -.hgi-stroke.hgi-wifi-zero::before { - content: "\f3423"; -} - -.hgi-stroke.hgi-wikipedia::before { - content: "\f2a04"; -} - -.hgi-stroke.hgi-wind-arrow-down::before { - content: "\f3425"; -} - -.hgi-stroke.hgi-wind-power-01::before { - content: "\f2a05"; -} - -.hgi-stroke.hgi-wind-power-02::before { - content: "\f2a06"; -} - -.hgi-stroke.hgi-wind-power-03::before { - content: "\f2a07"; -} - -.hgi-stroke.hgi-wind-power::before { - content: "\f2a08"; -} - -.hgi-stroke.hgi-wind-surf::before { - content: "\f2a09"; -} - -.hgi-stroke.hgi-wind-turbine::before { - content: "\f2a0a"; -} - -.hgi-stroke.hgi-wind::before { - content: "\f3424"; -} - -.hgi-stroke.hgi-windows-new::before { - content: "\f2a0b"; -} - -.hgi-stroke.hgi-windows-old::before { - content: "\f2a0c"; -} - -.hgi-stroke.hgi-wink::before { - content: "\f2a0d"; -} - -.hgi-stroke.hgi-winking::before { - content: "\f2a0e"; -} - -.hgi-stroke.hgi-wireless-cloud-access::before { - content: "\f2a0f"; -} - -.hgi-stroke.hgi-wireless::before { - content: "\f2a10"; -} - -.hgi-stroke.hgi-wise::before { - content: "\f2a11"; -} - -.hgi-stroke.hgi-woman::before { - content: "\f2a12"; -} - -.hgi-stroke.hgi-wordpress::before { - content: "\f2a13"; -} - -.hgi-stroke.hgi-work-alert::before { - content: "\f2a14"; -} - -.hgi-stroke.hgi-work-history::before { - content: "\f2a15"; -} - -.hgi-stroke.hgi-work-update::before { - content: "\f2a16"; -} - -.hgi-stroke.hgi-work::before { - content: "\f2a17"; -} - -.hgi-stroke.hgi-workflow-circle-01::before { - content: "\f2a18"; -} - -.hgi-stroke.hgi-workflow-circle-02::before { - content: "\f2a19"; -} - -.hgi-stroke.hgi-workflow-circle-03::before { - content: "\f2a1a"; -} - -.hgi-stroke.hgi-workflow-circle-04::before { - content: "\f2a1b"; -} - -.hgi-stroke.hgi-workflow-circle-05::before { - content: "\f2a1c"; -} - -.hgi-stroke.hgi-workflow-circle-06::before { - content: "\f2a1d"; -} - -.hgi-stroke.hgi-workflow-square-01::before { - content: "\f2a1e"; -} - -.hgi-stroke.hgi-workflow-square-02::before { - content: "\f2a1f"; -} - -.hgi-stroke.hgi-workflow-square-03::before { - content: "\f2a20"; -} - -.hgi-stroke.hgi-workflow-square-04::before { - content: "\f2a21"; -} - -.hgi-stroke.hgi-workflow-square-05::before { - content: "\f2a22"; -} - -.hgi-stroke.hgi-workflow-square-06::before { - content: "\f2a23"; -} - -.hgi-stroke.hgi-workflow-square-07::before { - content: "\f2a24"; -} - -.hgi-stroke.hgi-workflow-square-08::before { - content: "\f2a25"; -} - -.hgi-stroke.hgi-workflow-square-09::before { - content: "\f2a26"; -} - -.hgi-stroke.hgi-workflow-square-10::before { - content: "\f2a27"; -} - -.hgi-stroke.hgi-workflow::before { - content: "\f3426"; -} - -.hgi-stroke.hgi-workout-battle-ropes::before { - content: "\f2a28"; -} - -.hgi-stroke.hgi-workout-gymnastics::before { - content: "\f2a29"; -} - -.hgi-stroke.hgi-workout-kicking::before { - content: "\f2a2a"; -} - -.hgi-stroke.hgi-workout-run::before { - content: "\f2a2b"; -} - -.hgi-stroke.hgi-workout-sport::before { - content: "\f2a2c"; -} - -.hgi-stroke.hgi-workout-squats::before { - content: "\f2a2d"; -} - -.hgi-stroke.hgi-workout-stretching::before { - content: "\f2a2e"; -} - -.hgi-stroke.hgi-workout-warm-up::before { - content: "\f2a2f"; -} - -.hgi-stroke.hgi-worm::before { - content: "\f3427"; -} - -.hgi-stroke.hgi-worry::before { - content: "\f2a30"; -} - -.hgi-stroke.hgi-wps-office-rectangle::before { - content: "\f2a31"; -} - -.hgi-stroke.hgi-wps-office::before { - content: "\f2a32"; -} - -.hgi-stroke.hgi-wrench-01::before { - content: "\f2a33"; -} - -.hgi-stroke.hgi-wrench-02::before { - content: "\f2a34"; -} - -.hgi-stroke.hgi-wudu::before { - content: "\f2a35"; -} - -.hgi-stroke.hgi-x-line-top::before { - content: "\f3429"; -} - -.hgi-stroke.hgi-x-ray::before { - content: "\f2a36"; -} - -.hgi-stroke.hgi-x-variable-circle::before { - content: "\f2a37"; -} - -.hgi-stroke.hgi-x-variable-square::before { - content: "\f2a38"; -} - -.hgi-stroke.hgi-x-variable::before { - content: "\f2a39"; -} - -.hgi-stroke.hgi-x::before { - content: "\f3428"; -} - -.hgi-stroke.hgi-xing::before { - content: "\f2a3a"; -} - -.hgi-stroke.hgi-xls-01::before { - content: "\f2a3b"; -} - -.hgi-stroke.hgi-xls-02::before { - content: "\f2a3c"; -} - -.hgi-stroke.hgi-xml-01::before { - content: "\f2a3d"; -} - -.hgi-stroke.hgi-xml-02::before { - content: "\f2a3e"; -} - -.hgi-stroke.hgi-xsl-01::before { - content: "\f2a3f"; -} - -.hgi-stroke.hgi-xsl-02::before { - content: "\f2a40"; -} - -.hgi-stroke.hgi-yelp::before { - content: "\f2a42"; -} - -.hgi-stroke.hgi-yen-circle::before { - content: "\f2a43"; -} - -.hgi-stroke.hgi-yen-receive::before { - content: "\f2a44"; -} - -.hgi-stroke.hgi-yen-send::before { - content: "\f2a45"; -} - -.hgi-stroke.hgi-yen-square::before { - content: "\f2a46"; -} - -.hgi-stroke.hgi-yen::before { - content: "\f2a47"; -} - -.hgi-stroke.hgi-yoga-01::before { - content: "\f2a48"; -} - -.hgi-stroke.hgi-yoga-02::before { - content: "\f2a49"; -} - -.hgi-stroke.hgi-yoga-03::before { - content: "\f2a4a"; -} - -.hgi-stroke.hgi-yoga-ball::before { - content: "\f2a4b"; -} - -.hgi-stroke.hgi-yoga-mat::before { - content: "\f2a4c"; -} - -.hgi-stroke.hgi-yogurt::before { - content: "\f2a4d"; -} - -.hgi-stroke.hgi-youtube::before { - content: "\f2a4e"; -} - -.hgi-stroke.hgi-yurt::before { - content: "\f2a4f"; -} - -.hgi-stroke.hgi-zakat::before { - content: "\f2a50"; -} - -.hgi-stroke.hgi-zap-off::before { - content: "\f342a"; -} - -.hgi-stroke.hgi-zap::before { - content: "\f2a51"; -} - -.hgi-stroke.hgi-zeppelin::before { - content: "\f2a52"; -} - -.hgi-stroke.hgi-zero-circle::before { - content: "\f2a53"; -} - -.hgi-stroke.hgi-zero-square::before { - content: "\f2a54"; -} - -.hgi-stroke.hgi-zip-01::before { - content: "\f2a55"; -} - -.hgi-stroke.hgi-zip-02::before { - content: "\f2a56"; -} - -.hgi-stroke.hgi-zodiac-aquarius::before { - content: "\f342b"; -} - -.hgi-stroke.hgi-zodiac-aries::before { - content: "\f342c"; -} - -.hgi-stroke.hgi-zodiac-cancer::before { - content: "\f342d"; -} - -.hgi-stroke.hgi-zodiac-capricorn::before { - content: "\f342e"; -} - -.hgi-stroke.hgi-zodiac-gemini::before { - content: "\f342f"; -} - -.hgi-stroke.hgi-zodiac-leo::before { - content: "\f3430"; -} - -.hgi-stroke.hgi-zodiac-libra::before { - content: "\f3431"; -} - -.hgi-stroke.hgi-zodiac-ophiuchus::before { - content: "\f3432"; -} - -.hgi-stroke.hgi-zodiac-pisces::before { - content: "\f3433"; -} - -.hgi-stroke.hgi-zodiac-sagittarius::before { - content: "\f3434"; -} - -.hgi-stroke.hgi-zodiac-scorpio::before { - content: "\f3435"; -} - -.hgi-stroke.hgi-zodiac-taurus::before { - content: "\f3436"; -} - -.hgi-stroke.hgi-zodiac-virgo::before { - content: "\f3437"; -} - -.hgi-stroke.hgi-zoom-circle::before { - content: "\f2a57"; -} - -.hgi-stroke.hgi-zoom-in-area::before { - content: "\f2a58"; -} - -.hgi-stroke.hgi-zoom-in::before { - content: "\f3438"; -} - -.hgi-stroke.hgi-zoom-out-area::before { - content: "\f2a59"; -} - -.hgi-stroke.hgi-zoom-out::before { - content: "\f3439"; -} - -.hgi-stroke.hgi-zoom-square::before { - content: "\f2a5a"; -} - -.hgi-stroke.hgi-zoom::before { - content: "\f2a5b"; -} - -.hgi-stroke.hgi-zsh-2::before { - content: "\f343a"; -} - -.hgi-stroke.hgi-zsh::before { - content: "\f2a5c"; -} - -.hgi-stroke.hgi-zzz::before { - content: "\f2a5d"; -} \ No newline at end of file diff --git a/src/components/brand/PlatformGlyph.tsx b/src/components/brand/PlatformGlyph.tsx new file mode 100644 index 0000000..008989f --- /dev/null +++ b/src/components/brand/PlatformGlyph.tsx @@ -0,0 +1,58 @@ +import { For, type JSX } from "solid-js"; + +export type BrandGlyphName = "twitch" | "youtube" | "seven-tv"; + +type GlyphDefinition = { + viewBox: string; + paths: readonly string[]; +}; + +/** + * Monochrome brand marks, tinted through `currentColor`. + * + * Deliberately not part of `components/ui`: the generic primitive layer must + * not know about streaming providers, and these are brand assets rather than + * UI icons (see `documents/REFACTORING.md` section 9). + * + * The filled brand chips under `public/img/platform-*.svg` are a different + * rendering of the same brands for the overlay badges and stay there. This + * module owns only the monochrome glyph form so the same path data is not + * re-declared at each call site. + */ +const BRAND_GLYPHS: Record = { + twitch: { + viewBox: "0 0 24 24", + paths: [ + "M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0 1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z", + ], + }, + youtube: { + viewBox: "0 0 24 24", + paths: [ + "M23.5 6.19a3.02 3.02 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.51A3.02 3.02 0 0 0 .5 6.19C0 8.07 0 12 0 12s0 3.93.5 5.81a3.02 3.02 0 0 0 2.123 2.136c1.872.509 9.377.509 9.377.509s7.505 0 9.377-.51a3.02 3.02 0 0 0 2.122-2.135C24 15.93 24 12 24 12s0-3.93-.5-5.81zM9.545 15.568V8.432L15.818 12l-6.273 3.568z", + ], + }, + "seven-tv": { + viewBox: "0 0 28 20", + paths: [ + "M20.7465 5.48825 21.9799 3.33745 22.646 2.20024 21.4125 0.0494437V0H14.8259L17.2928 4.3016 17.9836 5.48825H20.7465Z", + "M7.15395 19.9258 14.5546 7.02104 15.4673 5.43884 13.0004 1.13724 12.3097 0.0247596H1.8995L0.666057 2.17556 0 3.31276 1.23344 5.46356V5.51301H9.12745L2.96025 16.267 2.09685 17.7998 3.33029 19.9506V20H7.15395", + "M17.4655 19.9257H21.2398L26.1736 11.3225 27.037 9.83924 25.8036 7.68844V7.63899H22.0046L19.5377 11.9406 19.365 12.262 16.8981 7.96038 16.7255 7.63899 14.2586 11.9406 13.5679 13.1272 17.2682 19.5796 17.4655 19.9257Z", + ], + }, +}; + +export const PlatformGlyph = (props: { + name: BrandGlyphName; + class?: string; +}): JSX.Element => ( + +); diff --git a/src/components/chat/ChatBadges.tsx b/src/components/chat/ChatBadges.tsx index 337e202..5614dba 100644 --- a/src/components/chat/ChatBadges.tsx +++ b/src/components/chat/ChatBadges.tsx @@ -11,7 +11,7 @@ import { orderThirdPartyBadges, ROLE_BADGE_ORDER, SUB_BADGE_ORDER, -} from "~/utils/chat/badgePriority"; +} from "./badgePriority"; type ChatBadgesProps = { message: TwitchMessage; @@ -20,9 +20,9 @@ type ChatBadgesProps = { }; export const ChatBadges = (props: ChatBadgesProps): JSX.Element => { - if (props.config.hideAllBadges) return null; const renderedBadges = createMemo(() => { const { message, config, service } = props; + if (config.hideAllBadges) return [] as JSX.Element[]; const showPlatformIcon = config.platformMarker === "icon" && hasMultipleChatSources(config) && diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index a4fe98e..e1f9196 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -7,7 +7,7 @@ import { Switch, type JSX, } from "solid-js"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import { hasMultipleChatSources, normalizeFontWeight } from "~/config/chatUrlParams"; import { sevenTVCosmeticsService, @@ -17,7 +17,7 @@ import { import { getFontFamily } from "~/styles/chatStyles"; import { getChatEventStyleVariables } from "~/styles/chatEventStyles"; import { networkClient } from "~/services/network/networkClient"; -import { hasMessageEntryAnimation } from "~/utils/ui/animationUtils"; +import { hasMessageEntryAnimation } from "~/config/chatAnimation"; import { ChatBadges } from "~/components/chat/ChatBadges"; import { ChatNick } from "~/components/chat/ChatNick"; import { ChatText } from "~/components/chat/ChatText"; @@ -352,31 +352,20 @@ export const ChatMessage = (props: ChatMessageProps) => { // Smooth "throw from bottom": a CSS @keyframes animation (chatFlowEnter) // slides the new row up from one row-height below its natural position. // Measure synchronously (forces layout) so the animation starts with the - // correct shift and never jumps; animationend/timer clean up the class. + // correct shift and never jumps. const height = rootRef.getBoundingClientRect().height || 18; rootRef.style.setProperty("--chat-flow-entry-shift", `${height}px`); - const clearEntryAnimation = () => { - const r = rootRef; - if (!r) return; - r.classList.remove("message-enter"); - r.style.removeProperty("--chat-flow-entry-shift"); - rootRef?.removeEventListener("animationend", onEntryAnimationEnd!); - animationTimer = undefined; - }; - onEntryAnimationEnd = (event) => { - if (event.target === rootRef) clearEntryAnimation(); - }; - rootRef.addEventListener("animationend", onEntryAnimationEnd); - animationTimer = window.setTimeout( - clearEntryAnimation, - props.animationDurationMs + 100, - ); - return; } + // One cleanup for both entry animations. Only the flow variant sets the + // shift property, and removing a property that was never set is a no-op, + // so the two branches do not need their own copies of this. const clearEntryAnimation = () => { - rootRef?.classList.remove("message-enter"); - rootRef?.removeEventListener("animationend", onEntryAnimationEnd!); + const element = rootRef; + if (!element) return; + element.classList.remove("message-enter"); + element.style.removeProperty("--chat-flow-entry-shift"); + element.removeEventListener("animationend", onEntryAnimationEnd!); animationTimer = undefined; }; onEntryAnimationEnd = (event) => { diff --git a/src/components/chat/ChatMessageList.tsx b/src/components/chat/ChatMessageList.tsx index 9ff5414..7066714 100644 --- a/src/components/chat/ChatMessageList.tsx +++ b/src/components/chat/ChatMessageList.tsx @@ -1,5 +1,5 @@ import { createMemo, For, Show, onCleanup, onMount } from "solid-js"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { TwitchMessage, ChatPresentationService } from "~/services/chat"; import { ChatMessage } from "~/components/chat/ChatMessage"; import { installMessageImageFallback } from "~/utils/chat/messageImageFallback"; diff --git a/src/components/chat/ChatText.tsx b/src/components/chat/ChatText.tsx index 19df478..0c0ad73 100644 --- a/src/components/chat/ChatText.tsx +++ b/src/components/chat/ChatText.tsx @@ -1,7 +1,8 @@ import type { JSX } from "solid-js"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { TwitchMessage, ChatPresentationService } from "~/services/chat"; import { renderMessageWithEmotes } from "./renderMessageContent"; +import { networkClient } from "~/services/network/networkClient"; type ChatTextProps = { message: TwitchMessage; @@ -21,7 +22,10 @@ export const ChatText = (props: ChatTextProps): JSX.Element => { "font-weight": props.fontWeight ?? "800", }} > - {renderMessageWithEmotes(props.message, props.config, props.service, props.displayText)} + {renderMessageWithEmotes(props.message, props.config, props.service, { + displayText: props.displayText, + resolveUrl: networkClient.resolveHttpUrl, + })} ); }; diff --git a/src/utils/chat/badgePriority.ts b/src/components/chat/badgePriority.ts similarity index 100% rename from src/utils/chat/badgePriority.ts rename to src/components/chat/badgePriority.ts diff --git a/src/components/chat/renderMessageContent.ts b/src/components/chat/renderMessageContent.ts index da1ff34..b4c6398 100644 --- a/src/components/chat/renderMessageContent.ts +++ b/src/components/chat/renderMessageContent.ts @@ -1,9 +1,9 @@ import type { JSX } from "solid-js"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { TwitchMessage, ChatPresentationService } from "~/services/chat"; import { bitsService } from "~/services/chat"; import { mentionStyleService } from "~/services/chat"; -import { networkClient } from "~/services/network/networkClient"; +import type { NetworkRoute } from "~/services/network/networkClient"; import { createMessageTokenSnapshot, parseGoogleEmoji, @@ -15,6 +15,7 @@ import { attachZeroWidthOverlay, bindWideEmoteSizes, buildGigantifiedLine, + escapeAttr, getEmoteModifier, resolveEmoteModifiers, wrapEmoteModifiers, @@ -29,13 +30,19 @@ export function escapeHtml(message: string): string { .replace(/(>)(?!\()/g, ">"); } -function escapeAttr(value: string): string { - return value - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); +/** + * Twitch reports emote and GIF ranges in Unicode code points, while string + * indices are UTF-16 code units. + */ +function codePointToCodeUnit(text: string, codePointIndex: number): number { + let currentCodePoint = 0; + for (let i = 0; i < text.length; i++) { + if (currentCodePoint === codePointIndex) return i; + const charCode = text.charCodeAt(i); + if (charCode >= 0xd800 && charCode <= 0xdbff) i += 1; + currentCodePoint += 1; + } + return text.length; } function sanitizeImageUrl(value: string): string { @@ -154,6 +161,18 @@ function twitchGifUrl(rawUrl: string): string { } } +/** + * Resolves an asset URL for the renderer. Injected rather than imported so this + * module owns no network policy and a caller can render without infrastructure. + */ +export type MessageUrlResolver = (url: string, route?: NetworkRoute) => string; + +export type RenderMessageOptions = { + /** Text to render instead of `message.message`, used when a reply mention is hidden. */ + displayText?: string; + resolveUrl: MessageUrlResolver; +}; + /** * Render message content with emotes/emoji/cheers to JSX (innerHTML) */ @@ -161,8 +180,9 @@ export function renderMessageWithEmotes( message: TwitchMessage, config: ChatConfig, service: ChatPresentationService, - displayText: string = message.message, + options: RenderMessageOptions, ): JSX.Element { + const { resolveUrl, displayText = message.message } = options; const size = SIZE_CONFIGS[config.size as keyof typeof SIZE_CONFIGS] || SIZE_CONFIGS[2]; const rawMessage = displayText; @@ -181,16 +201,6 @@ export function renderMessageWithEmotes( const replacements: Record = {}; if (config.showGifs && message.gifs) { - const codePointToCodeUnit = (text: string, codePointIndex: number): number => { - let currentCodePoint = 0; - for (let i = 0; i < text.length; i++) { - if (currentCodePoint === codePointIndex) return i; - const charCode = text.charCodeAt(i); - if (charCode >= 0xd800 && charCode <= 0xdbff) i += 1; - currentCodePoint += 1; - } - return text.length; - }; const gifScale = Number.isFinite(config.gifScale) ? Math.min(Math.max(config.gifScale, 0.25), 3) : 1; @@ -200,7 +210,7 @@ export function renderMessageWithEmotes( const token = message.message.substring(start, end); const url = twitchGifUrl(gif.url); if (!token || !url) continue; - const resolvedUrl = networkClient.resolveHttpUrl(url, "rte"); + const resolvedUrl = resolveUrl(url, "rte"); replacements[token] = { kind: "html", html: `GIF`, @@ -210,20 +220,6 @@ export function renderMessageWithEmotes( } if (message.emotes && typeof message.emotes === "object") { - const codePointToCodeUnit = ( - text: string, - codePointIndex: number, - ): number => { - let currentCodePoint = 0; - for (let i = 0; i < text.length; i++) { - if (currentCodePoint === codePointIndex) return i; - const charCode = text.charCodeAt(i); - if (charCode >= 0xd800 && charCode <= 0xdbff) i += 1; - currentCodePoint += 1; - } - return text.length; - }; - Object.entries(message.emotes as Record).forEach( ([emoteId, positions]) => { if (!Array.isArray(positions)) return; @@ -241,7 +237,7 @@ export function renderMessageWithEmotes( replacements[emoteCode] = { kind: "html", - html: ``, + html: ``, isOverlayTarget: true, }; }); diff --git a/src/components/debug/PerfMonitor.tsx b/src/components/debug/PerfMonitor.tsx index 83e0c0b..950a6cf 100644 --- a/src/components/debug/PerfMonitor.tsx +++ b/src/components/debug/PerfMonitor.tsx @@ -4,7 +4,7 @@ import { start as collectorStart, stop as collectorStop, subscribe } from "./per import { PerfPanel } from "./PerfPanel"; export interface PerfMonitorProps { - /** Connection state from the chat service (optional — wired in Todo 5) */ + /** Connection state from the chat service; omitted when the route has none. */ readonly isConnected?: boolean; } diff --git a/src/components/debug/PerfPanel.tsx b/src/components/debug/PerfPanel.tsx index df5f925..e1f57d1 100644 --- a/src/components/debug/PerfPanel.tsx +++ b/src/components/debug/PerfPanel.tsx @@ -16,7 +16,7 @@ import "./PerfPanel.css"; export interface PerfPanelProps { snapshot: CollectorSnapshot; snapshots: readonly CollectorSnapshot[]; - /** Optional connection state prop (for Todo 5 integration) */ + /** Connection state passed by the monitor when the route can report it. */ isConnected?: boolean; } diff --git a/src/components/ColorPickerField.tsx b/src/components/setup/ColorPickerField.tsx similarity index 100% rename from src/components/ColorPickerField.tsx rename to src/components/setup/ColorPickerField.tsx diff --git a/src/components/setup/FfzBadgeMergeBlock.tsx b/src/components/setup/FfzBadgeMergeBlock.tsx new file mode 100644 index 0000000..073cb7a --- /dev/null +++ b/src/components/setup/FfzBadgeMergeBlock.tsx @@ -0,0 +1,89 @@ +import { For, type JSX } from "solid-js"; +import { cn } from "~/lib/utils"; +import { t } from "~/i18n"; +import { getPublicAssetUrl } from "~/utils/appBase"; + +export type FfzBadgeMergeOption = { + label: () => string; + /** Role colour shown behind the badge preview. */ + badgeColor: string; + checked: () => boolean; + onChange: (checked: boolean) => void; +}; + +export type FfzBadgeMergeBlockProps = { + options: readonly FfzBadgeMergeOption[]; + /** + * Role badges cannot be merged while FFZ badges are off or every badge is + * hidden, so the pills are disabled rather than hidden. + */ + disabled: boolean; +}; + +const BADGE_PREVIEW_URL = getPublicAssetUrl("img/ffz-bot-badge.png"); + +/** + * Chooses which role badge the FFZ bot badge is drawn next to. + * + * The pills are bespoke controls rather than the generic `Button`: each one + * carries a coloured swatch, a pressed state and a desaturated disabled tint, + * all of which the shared button base styles would fight. + */ +export function FfzBadgeMergeBlock(props: FfzBadgeMergeBlockProps): JSX.Element { + const disabled = () => props.disabled; + + return ( +
+
+
+ {t("setup.botBadgeNearRole")} +
+
+ {t("setup.botBadgeNearRoleHint")} +
+
+
+ + {(option) => { + const active = () => option.checked(); + return ( + + ); + }} + +
+
+ ); +} diff --git a/src/components/setup/PreviewControls.tsx b/src/components/setup/PreviewControls.tsx new file mode 100644 index 0000000..7100208 --- /dev/null +++ b/src/components/setup/PreviewControls.tsx @@ -0,0 +1,321 @@ +import { createEffect, createSignal, onCleanup, onMount, Show, type JSX, type Setter } from "solid-js"; +import LiveStreaming02Icon from "@hugeicons/core-free-icons/LiveStreaming02Icon"; +import MessageSquareMoreIcon from "@hugeicons/core-free-icons/MessageSquareMoreIcon"; +import PauseIcon from "@hugeicons/core-free-icons/PauseIcon"; +import PlayIcon from "@hugeicons/core-free-icons/PlayIcon"; +import RubberDuckIcon from "@hugeicons/core-free-icons/RubberDuckIcon"; +import TestTube01Icon from "@hugeicons/core-free-icons/TestTube01Icon"; +import { t } from "~/i18n"; +import { cn } from "~/lib/utils"; +import { Icon } from "~/components/ui/icon"; +import { Slider } from "~/components/ui/slider"; +import { MAX_MESSAGE_SPEED, MIN_MESSAGE_SPEED } from "~/config/chatAnimation"; +import type { PreviewDemoKind } from "~/services/chat/preview/messages"; +import { ColorPickerField } from "./ColorPickerField"; + +type StageBackdrop = "dark" | "light" | "checker" | "custom"; +type PreviewMode = "live" | "demo"; + +export type PreviewControlsProps = { + stageBackdrop: () => StageBackdrop; + setStageBackdrop: (value: StageBackdrop) => void; + stageColor: () => string; + setStageColor: (value: string) => void; + previewMode: () => PreviewMode; + setPreviewMode: (value: PreviewMode) => void; + previewDemoKind: () => PreviewDemoKind; + setPreviewDemoKind: (value: PreviewDemoKind) => void; + demoPaused: () => boolean; + setDemoPaused: Setter; + messageSpeedValue: () => number; + messageSpeedLabel: () => string; + setMessageSpeed: (value: string) => void; + hasPreviewChannel: () => boolean; + /** True when only an external platform is configured, which hides the demo controls. */ + isExternalOnly: () => boolean; +}; + +/** + * The preview pane's controls: the backdrop swatches, the chat-mode and + * demo-scenario selectors, and the demo playback controls. + * + * It owns its own selection thumbs, including the refs and the resize observer + * that measure them, because the measurement is the only reason those refs exist + * and splitting them across a prop boundary would spread the coordination + * instead of reducing it. + */ +export function PreviewControls(props: PreviewControlsProps): JSX.Element { + const { + stageBackdrop, + setStageBackdrop, + stageColor, + setStageColor, + previewMode, + setPreviewMode, + previewDemoKind, + setPreviewDemoKind, + demoPaused, + setDemoPaused, + messageSpeedValue, + messageSpeedLabel, + setMessageSpeed, + hasPreviewChannel, + isExternalOnly, + } = props; + + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewControlsRef: HTMLDivElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewModeSelectorRef: HTMLDivElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewLiveOptionRef: HTMLButtonElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewDemoOptionRef: HTMLButtonElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewDemoSelectorRef: HTMLDivElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewPastaOptionRef: HTMLButtonElement | undefined; + // eslint-disable-next-line no-unassigned-vars -- assigned by SolidJS ref={} + let previewEmoteOptionRef: HTMLButtonElement | undefined; + + const [previewModeThumbStyle, setPreviewModeThumbStyle] = createSignal({ + opacity: "0", + }); + const [previewDemoThumbStyle, setPreviewDemoThumbStyle] = createSignal({ + opacity: "0", + }); + + const getPreviewSelectorThumbStyle = ( + selector: HTMLDivElement | undefined, + option: HTMLButtonElement | undefined, + ): JSX.CSSProperties => { + if (!selector || !option) return { opacity: "0" }; + + const selectorRect = selector.getBoundingClientRect(); + const optionRect = option.getBoundingClientRect(); + return { + height: `${optionRect.height}px`, + opacity: "1", + transform: `translateY(${optionRect.top - selectorRect.top}px)`, + }; + }; + + const updatePreviewSelectorThumbs = () => { + setPreviewModeThumbStyle( + getPreviewSelectorThumbStyle( + previewModeSelectorRef, + previewMode() === "live" ? previewLiveOptionRef : previewDemoOptionRef, + ), + ); + setPreviewDemoThumbStyle( + isExternalOnly() + ? { opacity: "0" } + : getPreviewSelectorThumbStyle( + previewDemoSelectorRef, + previewDemoKind() === "pasta" + ? previewPastaOptionRef + : previewEmoteOptionRef, + ), + ); + }; + + createEffect(updatePreviewSelectorThumbs); + onMount(() => { + const observer = new ResizeObserver(updatePreviewSelectorThumbs); + if (previewControlsRef) observer.observe(previewControlsRef); + onCleanup(() => observer.disconnect()); + }); + + return ( +
+
(previewControlsRef = element)}> +
+
{t("setup.demoBackgroundColor")}
+
+ + + + +
+ +
+ setStageColor(value.color)} + /> +
+
+
+
+
{t("setup.previewSettings")}
+
+
+
{t("setup.chatMode")}
+
(previewModeSelectorRef = element)} + role="radiogroup" + aria-label={t("setup.previewChatMode")} + > + +
+ +
+
{t("setup.demoScenario")}
+
(previewDemoSelectorRef = element)} + role="radiogroup" + aria-label={t("setup.demoScenario")} + > + +
+ +
+
+
+ + +
+
+ + {t("setup.speed")} + + { + const next = values[0]; + if (next !== undefined) + setMessageSpeed(String(next)); + }} + class="min-w-0 flex-1 px-1" + /> +
+ {messageSpeedLabel()} +
+
+ +
+
+
+ ); +} diff --git a/src/components/setup/SetupChipInput.tsx b/src/components/setup/SetupChipInput.tsx new file mode 100644 index 0000000..2d73b67 --- /dev/null +++ b/src/components/setup/SetupChipInput.tsx @@ -0,0 +1,34 @@ +import type { JSX } from "solid-js"; + +export type SetupChipInputProps = { + /** Current draft value, owned by the caller. */ + value: string; + onInput: (value: string) => void; + /** Commits the draft, for example on blur or Enter. */ + onCommit: () => void; + onKeyDown: (event: KeyboardEvent) => void; + label: string; + placeholder: string; +}; + +/** + * Borderless text field that adds entries to a chip list. + * + * The bot, YouTube-bot, Kick-bot and viewer allowlist rows all embed the same + * control inside their chip container, so it lives here rather than being + * repeated as raw markup in the route. It is intentionally not the generic + * `Input` primitive: the field sits inside an existing bordered container and + * must stay borderless and transparent. + */ +export const SetupChipInput = (props: SetupChipInputProps): JSX.Element => ( + props.onInput(event.currentTarget.value)} + onKeyDown={(event) => props.onKeyDown(event)} + onBlur={props.onCommit} + placeholder={props.placeholder} + class="h-[34px] min-w-[150px] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground" + /> +); diff --git a/src/components/setup/SetupImportCard.tsx b/src/components/setup/SetupImportCard.tsx index dd7104c..66ab825 100644 --- a/src/components/setup/SetupImportCard.tsx +++ b/src/components/setup/SetupImportCard.tsx @@ -14,6 +14,7 @@ import { writeUserSetupTemplates, type TemplateSource, } from "~/config/setupTemplates"; +import DatabaseImportIcon from "@hugeicons/core-free-icons/DatabaseImportIcon"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Textarea } from "~/components/ui/textarea"; @@ -172,7 +173,7 @@ export function SetupImportCard(props: SetupImportCardProps) { id="setup-section-import" title={t("setup.import.title")} description={t("setup.import.description")} - icon="hgi-database-import" + icon={DatabaseImportIcon} hidden={props.hidden} >
diff --git a/src/components/setup/SetupLayout.tsx b/src/components/setup/SetupLayout.tsx index a1e982e..49ddbf3 100644 --- a/src/components/setup/SetupLayout.tsx +++ b/src/components/setup/SetupLayout.tsx @@ -8,26 +8,22 @@ import { onMount, Show, } from "solid-js"; +import type { IconSvgObject } from "@hugeicons/core-free-icons/types"; +import ArrowUpRightStackIcon from "@hugeicons/core-free-icons/ArrowUpRightStackIcon"; +import Blockchain05Icon from "@hugeicons/core-free-icons/Blockchain05Icon"; +import BotMessageSquareIcon from "@hugeicons/core-free-icons/BotMessageSquareIcon"; +import ColorsIcon from "@hugeicons/core-free-icons/ColorsIcon"; +import DashboardSquare03Icon from "@hugeicons/core-free-icons/DashboardSquare03Icon"; +import DatabaseImportIcon from "@hugeicons/core-free-icons/DatabaseImportIcon"; +import TextIcon from "@hugeicons/core-free-icons/TextIcon"; +import VoiceCommentIcon from "@hugeicons/core-free-icons/VoiceCommentIcon"; import { cn } from "~/lib/utils"; import { t } from "~/i18n"; +import { Icon } from "~/components/ui/icon"; +import type { SetupSectionId } from "~/features/setup/model/setupSections"; +import { resolveSetupText, type SetupText } from "./setupText"; import { SetupSwitch } from "./SetupSwitch"; -export type SetupSectionId = - | "import" - | "appearance" - | "styling" - | "behavior" - | "content" - | "bots" - | "tts" - | "rte"; - -export type SetupText = string | (() => string); - -export function resolveSetupText(value: SetupText): string { - return typeof value === "function" ? value() : value; -} - export type ControlRow = { label: SetupText; control: (labelId: string) => JSX.Element; @@ -46,55 +42,55 @@ export const SETUP_NAV: { id: SetupSectionId; labelKey: "navigation.import.label" | "navigation.appearance.label" | "navigation.styling.label" | "navigation.behavior.label" | "navigation.content.label" | "navigation.bots.label" | "navigation.tts.label" | "navigation.rte.label"; descriptionKey: "navigation.import.description" | "navigation.appearance.description" | "navigation.styling.description" | "navigation.behavior.description" | "navigation.content.description" | "navigation.bots.description" | "navigation.tts.description" | "navigation.rte.description"; - icon: string; + icon: IconSvgObject; }[] = [ { id: "import", labelKey: "navigation.import.label", descriptionKey: "navigation.import.description", - icon: "hgi-database-import", + icon: DatabaseImportIcon, }, { id: "appearance", labelKey: "navigation.appearance.label", descriptionKey: "navigation.appearance.description", - icon: "hgi-text", + icon: TextIcon, }, { id: "styling", labelKey: "navigation.styling.label", descriptionKey: "navigation.styling.description", - icon: "hgi-colors", + icon: ColorsIcon, }, { id: "behavior", labelKey: "navigation.behavior.label", descriptionKey: "navigation.behavior.description", - icon: "hgi-arrow-up-right-stack", + icon: ArrowUpRightStackIcon, }, { id: "content", labelKey: "navigation.content.label", descriptionKey: "navigation.content.description", - icon: "hgi-dashboard-square-03", + icon: DashboardSquare03Icon, }, { id: "bots", labelKey: "navigation.bots.label", descriptionKey: "navigation.bots.description", - icon: "hgi-bot-message-square", + icon: BotMessageSquareIcon, }, { id: "tts", labelKey: "navigation.tts.label", descriptionKey: "navigation.tts.description", - icon: "hgi-voice-comment", + icon: VoiceCommentIcon, }, { id: "rte", labelKey: "navigation.rte.label", descriptionKey: "navigation.rte.description", - icon: "hgi-blockchain-05", + icon: Blockchain05Icon, }, ]; @@ -146,7 +142,7 @@ export function ToggleRows(props: { rows: ToggleRow[] }) { export function SectionCard(props: { title: string; description?: string; - icon?: string; + icon?: IconSvgObject; children: JSX.Element; class?: string; id?: string; @@ -164,8 +160,9 @@ export function SectionCard(props: {
{(icon) => ( -
) : ( @@ -820,7 +685,7 @@ export function TwitchChannelField(props: TwitchChannelFieldProps) { ) : ( - {fallbackName(suggestion.login)} + {loginFallbackName(suggestion.login)} )} diff --git a/src/components/setup/UserChip.tsx b/src/components/setup/UserChip.tsx new file mode 100644 index 0000000..4980aba --- /dev/null +++ b/src/components/setup/UserChip.tsx @@ -0,0 +1,76 @@ +import { Show, type JSX } from "solid-js"; +import XIcon from "@hugeicons/core-free-icons/XIcon"; +import { Icon } from "~/components/ui/icon"; +import { type BotProfile } from "~/services/setup/botProfiles"; +import { loginFallbackName } from "~/services/setup/logins"; + +export type UserChipProps = { + login: string; + /** + * The profile map is passed as a getter rather than as one resolved profile, + * because bot profiles arrive after the chip has rendered and the chip has to + * pick them up. + */ + profiles: () => Record; + onRemove: (login: string) => void; + /** Localized action, combined with the display name for the remove label. */ + removeLabel: () => string; +}; + +/** + * A removable entry in one of the bot or viewer lists: the resolved avatar or a + * single-letter fallback, the display name, the raw login when the two differ, + * and a remove button. Backspace and Delete remove it while it has focus. + */ +export function UserChip(props: UserChipProps): JSX.Element { + const profile = () => props.profiles()[props.login]; + const displayName = () => profile()?.displayName || props.login; + const avatarUrl = () => profile()?.avatarUrl || ""; + + return ( +
{ + if (event.key === "Backspace" || event.key === "Delete") { + event.preventDefault(); + props.onRemove(props.login); + } + }} + > + + {loginFallbackName(props.login)} + + } + > + + + + + {displayName()} + + + + @{props.login} + + + + +
+ ); +} diff --git a/src/components/setup/sections/appearanceRows.tsx b/src/components/setup/sections/appearanceRows.tsx new file mode 100644 index 0000000..7fe8c6a --- /dev/null +++ b/src/components/setup/sections/appearanceRows.tsx @@ -0,0 +1,238 @@ +import { For, Show } from "solid-js"; +import { t } from "~/i18n"; +import { cn } from "~/lib/utils"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import type { LocalFontOption } from "~/services/setup/localFonts"; +import { SetupNumberField } from "../SetupNumberField"; +import { SetupSelect } from "../SetupSelect"; +import type { ControlRow } from "../SetupLayout"; + +/** Form state and callbacks the appearance rows read. */ +export type AppearanceRowsSource = { + size: () => string; + setSize: (value: string) => void; + font: () => string; + setFont: (value: string) => void; + fontCustom: () => string; + setFontCustom: (value: string) => void; + localFontBrowser: () => string; + localFonts: () => LocalFontOption[]; + localFontStatusText: () => string; + isLoadingLocalFonts: () => boolean; + loadLocalFonts: () => void; + lineHeight: () => string; + setLineHeight: (value: string) => void; + fontWeight: () => string; + setFontWeight: (value: string) => void; + nickFontWeight: () => string; + setNickFontWeight: (value: string) => void; + emoteScale: () => string; + setEmoteScale: (value: string) => void; + gifScale: () => string; + setGifScale: (value: string) => void; +}; + +/** + * The appearance section's rows: message size and font, the local-font picker, + * and the weight and emote scales. + * + * The source is destructured up front rather than threaded through each row, so + * the row bodies are exactly what the route declared before the extraction. + */ +export function createAppearanceRows(source: AppearanceRowsSource): ControlRow[] { + const { + size, + setSize, + font, + setFont, + fontCustom, + setFontCustom, + localFontBrowser, + localFonts, + localFontStatusText, + isLoadingLocalFonts, + loadLocalFonts, + lineHeight, + setLineHeight, + fontWeight, + setFontWeight, + nickFontWeight, + setNickFontWeight, + emoteScale, + setEmoteScale, + gifScale, + setGifScale, + } = source; + + return [ + { + label: () => t("setup.messageSize"), + control: (labelId) => ( + setSize(e.currentTarget.value)} + > + + + + + ), + }, + { + label: () => t("setup.font"), + control: (labelId) => ( + setFont(e.currentTarget.value)} + > + + + + + + + + + + + + + + + + ), + }, + { + label: () => t("setup.customFontName"), + hint: () => t("setup.customFontHint"), + control: (labelId) => ( +
+ setFontCustom(e.currentTarget.value)} + placeholder={t("setup.customFontPlaceholder")} + disabled={font() !== "0"} + class={cn(font() !== "0" && "opacity-50")} + /> + +
+ + { + const selectedFont = e.currentTarget.value; + if (selectedFont) setFontCustom(selectedFont); + }} + disabled={font() !== "0" || localFonts().length === 0} + class={cn( + !(font() === "0" && localFonts().length > 0) && "opacity-50", + )} + > + + + {(localFont) => ( + + )} + + +
+
+
{localFontStatusText()}
+
+ ), + }, + { + label: () => t("setup.lineHeight"), + hint: () => t("setup.lineHeightHint"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.textWeight"), + hint: () => t("setup.textWeightHint"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.nicknameWeight"), + hint: () => t("setup.nicknameWeightHint"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.emoteSize"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.gifSize"), + hint: () => t("setup.gifSizeHint"), + control: (_labelId) => ( + + ), + }, + ]; +} diff --git a/src/components/setup/sections/behaviorRows.tsx b/src/components/setup/sections/behaviorRows.tsx new file mode 100644 index 0000000..30199c4 --- /dev/null +++ b/src/components/setup/sections/behaviorRows.tsx @@ -0,0 +1,136 @@ +import { t } from "~/i18n"; +import type { ChatAnimationMode, LinkDisplayMode } from "~/config/chatUrlParams"; +import { SetupSelect } from "../SetupSelect"; +import type { ControlRow, ToggleRow } from "../SetupLayout"; + +/** Form state and callbacks the behaviour section reads. */ +export type BehaviorRowsSource = { + animation: () => ChatAnimationMode; + setAnimation: (value: ChatAnimationMode) => void; + linkMode: () => LinkDisplayMode; + setLinkMode: (value: LinkDisplayMode) => void; +}; + +export type BehaviorTogglesSource = { + twitchEventBold: () => boolean; + setTwitchEventBold: (value: boolean) => void; + twitchEventItalic: () => boolean; + setTwitchEventItalic: (value: boolean) => void; + recentMessages: () => boolean; + setRecentMessages: (value: boolean) => void; + smallCaps: () => boolean; + setSmallCaps: (value: boolean) => void; + nlAfterName: () => boolean; + setNlAfterName: (value: boolean) => void; + hideNames: () => boolean; + setHideNames: (value: boolean) => void; + reverseLineOrder: () => boolean; + setReverseLineOrder: (value: boolean) => void; + horizontal: () => boolean; + setHorizontal: (value: boolean) => void; +}; + +/** The behaviour section's select rows. */ +export function createBehaviorRows(source: BehaviorRowsSource): ControlRow[] { + const { animation, setAnimation, linkMode, setLinkMode } = source; + + return [ + { + label: () => t("setup.messageAnimation"), + hint: () => t("setup.messageAnimationHint"), + control: (labelId) => ( + + setAnimation(event.currentTarget.value as ChatAnimationMode) + } + > + + + + + + ), + }, + { + label: () => t("setup.messageLinks"), + control: (labelId) => ( + + setLinkMode(event.currentTarget.value as LinkDisplayMode) + } + > + + + + + ), + }, + ]; +} + +/** The behaviour section's switches. */ +export function createBehaviorToggles(source: BehaviorTogglesSource): ToggleRow[] { + const { + twitchEventBold, + setTwitchEventBold, + twitchEventItalic, + setTwitchEventItalic, + recentMessages, + setRecentMessages, + smallCaps, + setSmallCaps, + nlAfterName, + setNlAfterName, + hideNames, + setHideNames, + reverseLineOrder, + setReverseLineOrder, + horizontal, + setHorizontal, + } = source; + + return [ + { + label: () => t("setup.emphasizeEventText"), + checked: twitchEventBold, + onChange: setTwitchEventBold, + hint: () => t("setup.emphasizeEventTextHint"), + }, + { + label: () => t("setup.italicEvents"), + checked: twitchEventItalic, + onChange: setTwitchEventItalic, + }, + { + label: () => t("setup.loadRecentMessages"), + checked: recentMessages, + onChange: setRecentMessages, + hint: () => t("setup.loadRecentMessagesHint"), + }, + { + label: () => t("setup.uppercaseNicknames"), + checked: smallCaps, + onChange: setSmallCaps, + }, + { + label: () => t("setup.lineBreakAfterNickname"), + checked: nlAfterName, + onChange: setNlAfterName, + }, + { label: () => t("setup.hideNicknames"), checked: hideNames, onChange: setHideNames }, + { + label: () => t("setup.reverseMessageOrder"), + checked: reverseLineOrder, + onChange: setReverseLineOrder, + }, + { + label: () => t("setup.horizontalMessageFeed"), + checked: horizontal, + onChange: setHorizontal, + }, + ]; +} diff --git a/src/components/setup/sections/stylingRows.tsx b/src/components/setup/sections/stylingRows.tsx new file mode 100644 index 0000000..08d3d09 --- /dev/null +++ b/src/components/setup/sections/stylingRows.tsx @@ -0,0 +1,156 @@ +import { t } from "~/i18n"; +import { DEFAULT_CHAT_CONFIG } from "~/config/chatUrlParams"; +import { toInt } from "~/config/formValues"; +import { ColorPickerField } from "../ColorPickerField"; +import { SetupNumberField } from "../SetupNumberField"; +import { SetupSelect } from "../SetupSelect"; +import type { ControlRow } from "../SetupLayout"; + +/** Form state and callbacks the styling rows read. */ +export type StylingRowsSource = { + shadow: () => string; + setShadow: (value: string) => void; + stroke: () => string; + setStroke: (value: string) => void; + fade: () => string; + setFade: (value: string) => void; + overlayBackgroundColor: () => string; + setOverlayBackgroundColor: (value: string) => void; + overlayBackgroundOpacity: () => string; + setOverlayBackgroundOpacity: (value: string) => void; + overlayBackgroundRadius: () => string; + setOverlayBackgroundRadius: (value: string) => void; + overlayPadding: () => string; + setOverlayPadding: (value: string) => void; + overlayBorderWidth: () => string; + setOverlayBorderWidth: (value: string) => void; +}; + +/** + * The appearance/styling section's rows: text shadow and stroke, message + * lifetime, and the message backing geometry. + */ +export function createStylingRows(source: StylingRowsSource): ControlRow[] { + const { + shadow, + setShadow, + stroke, + setStroke, + fade, + setFade, + overlayBackgroundColor, + setOverlayBackgroundColor, + overlayBackgroundOpacity, + setOverlayBackgroundOpacity, + overlayBackgroundRadius, + setOverlayBackgroundRadius, + overlayPadding, + setOverlayPadding, + overlayBorderWidth, + setOverlayBorderWidth, + } = source; + + return [ + { + label: () => t("setup.textShadow"), + control: (labelId) => ( + setShadow(e.currentTarget.value)} + > + + + + + + ), + }, + { + label: () => t("setup.textStroke"), + control: (labelId) => ( + setStroke(e.currentTarget.value)} + > + + + + + + + ), + }, + { + label: () => t("setup.hideMessagesAfter"), + hint: () => t("setup.hideMessagesAfterHint"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.messageBackground"), + control: (_labelId) => ( + { + setOverlayBackgroundColor(color); + setOverlayBackgroundOpacity(String(opacity)); + }} + /> + ), + }, + { + label: () => t("setup.backgroundRadius"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.padding"), + hint: () => t("setup.paddingHint"), + control: (_labelId) => ( + + ), + }, + { + label: () => t("setup.borderThickness"), + control: (_labelId) => ( + + ), + }, + ]; +} diff --git a/src/components/setup/sections/toggleRows.tsx b/src/components/setup/sections/toggleRows.tsx new file mode 100644 index 0000000..c62e884 --- /dev/null +++ b/src/components/setup/sections/toggleRows.tsx @@ -0,0 +1,143 @@ +import { t } from "~/i18n"; +import type { ToggleRow } from "../SetupLayout"; + +/** Switches for the content and badges section. */ +export type ContentTogglesSource = { + showHighlightedMessages: () => boolean; + setShowHighlightedMessages: (value: boolean) => void; + showChannelPointRewards: () => boolean; + setShowChannelPointRewards: (value: boolean) => void; + hideLinkRewards: () => boolean; + setHideLinkRewards: (value: boolean) => void; + showGigantifiedEmotes: () => boolean; + setShowGigantifiedEmotes: (value: boolean) => void; + showGifs: () => boolean; + setShowGifs: (value: boolean) => void; + showPredictions: () => boolean; + setShowPredictions: (value: boolean) => void; + commands: () => boolean; + setCommands: (value: boolean) => void; + show7tvUnlisted: () => boolean; + setShow7tvUnlisted: (value: boolean) => void; +}; + +/** Switches for the narration section. */ +export type TtsTogglesSource = { + rteChatIsTts: () => boolean; + setRteChatIsTts: (value: boolean) => void; + rteAzureTts: () => boolean; + setRteAzureTts: (value: boolean) => void; +}; + +/** Switches for the RTE section. */ +export type RteTogglesSource = { + rteProxy: () => boolean; + setRteProxy: (value: boolean) => void; + rteCustomCosmetics: () => boolean; + setRteCustomCosmetics: (value: boolean) => void; +}; + +export function createContentToggles(source: ContentTogglesSource): ToggleRow[] { + const { + showHighlightedMessages, + setShowHighlightedMessages, + showChannelPointRewards, + setShowChannelPointRewards, + hideLinkRewards, + setHideLinkRewards, + showGigantifiedEmotes, + setShowGigantifiedEmotes, + showGifs, + setShowGifs, + showPredictions, + setShowPredictions, + commands, + setCommands, + show7tvUnlisted, + setShow7tvUnlisted, + } = source; + + return [ + { + label: () => t("setup.showHighlightedMessages"), + checked: showHighlightedMessages, + onChange: setShowHighlightedMessages, + }, + { + label: () => t("setup.showPointRewards"), + checked: showChannelPointRewards, + onChange: setShowChannelPointRewards, + }, + { + label: () => t("setup.hideLinkRewards"), + checked: hideLinkRewards, + onChange: setHideLinkRewards, + hint: () => t("setup.hideLinkRewardsHint"), + }, + { + label: () => t("setup.showGigantifiedEmotes"), + checked: showGigantifiedEmotes, + onChange: setShowGigantifiedEmotes, + }, + { + label: () => t("setup.showGifs"), + checked: showGifs, + onChange: setShowGifs, + hint: () => t("setup.showGifsHint"), + }, + { + label: () => t("setup.showPredictions"), + checked: showPredictions, + onChange: setShowPredictions, + hint: () => t("setup.showPredictionsHint"), + }, + { + label: () => t("setup.showCommands"), + checked: commands, + onChange: setCommands, + }, + { + label: () => t("setup.showUnlistedEmotes"), + checked: show7tvUnlisted, + onChange: setShow7tvUnlisted, + }, + ]; +} + +export function createTtsToggles(source: TtsTogglesSource): ToggleRow[] { + const { rteChatIsTts, setRteChatIsTts, rteAzureTts, setRteAzureTts } = source; + + return [ + { + label: () => t("setup.chatIsTts"), + checked: rteChatIsTts, + onChange: setRteChatIsTts, + hint: () => t("setup.chatIsTtsHint"), + }, + { + label: () => t("setup.azureTts"), + checked: rteAzureTts, + onChange: setRteAzureTts, + hint: () => t("setup.azureTtsHint"), + }, + ]; +} + +export function createRteToggles(source: RteTogglesSource): ToggleRow[] { + const { rteProxy, setRteProxy, rteCustomCosmetics, setRteCustomCosmetics } = source; + + return [ + { + label: () => t("setup.rteProxy"), + checked: rteProxy, + onChange: setRteProxy, + hint: () => t("setup.rteProxyHint"), + }, + { + label: () => t("setup.rteCosmetics"), + checked: rteCustomCosmetics, + onChange: setRteCustomCosmetics, + hint: () => t("setup.rteCosmeticsHint"), + }, + ]; +} diff --git a/src/components/setup/setupText.ts b/src/components/setup/setupText.ts new file mode 100644 index 0000000..5e0340a --- /dev/null +++ b/src/components/setup/setupText.ts @@ -0,0 +1,12 @@ +/** + * Setup label text. + * + * A label is either a resolved string or a getter, so a row descriptor can be + * built once and still read the active locale when it renders. This module has + * no JSX so the resolver stays testable without a DOM. + */ +export type SetupText = string | (() => string); + +export function resolveSetupText(value: SetupText): string { + return typeof value === "function" ? value() : value; +} diff --git a/src/components/ui/icon.tsx b/src/components/ui/icon.tsx new file mode 100644 index 0000000..ed9ad63 --- /dev/null +++ b/src/components/ui/icon.tsx @@ -0,0 +1,41 @@ +import { HugeiconsIcon } from "@hugeicons/solid-js"; +import type { IconSvgObject } from "@hugeicons/core-free-icons/types"; +import type { JSX } from "solid-js"; + +export type IconProps = { + /** Icon payload from `@hugeicons/core-free-icons`. */ + icon: IconSvgObject; + class?: string; + /** + * Any CSS length. Defaults to `1em` so an icon keeps scaling with the + * `font-size` of the element that owns it; project stylesheets set that size + * per control instead of passing pixels per call site. + */ + size?: string | number; + "aria-hidden"?: boolean | "true" | "false"; + "aria-label"?: string; +}; + +/** + * Renders a Hugeicons icon through the vendor's Solid renderer. + * + * The wrapper owns only the project convention: `currentColor`, `1em` as the + * default size, and decorative-by-default accessibility. Stroke width is left + * to the icon payload, which carries its own value; forcing one here would give + * a stroke to a fill-only icon. Pass `aria-label` to expose the icon as a + * labelled image. + */ +export const Icon = (props: IconProps): JSX.Element => { + const label = () => props["aria-label"]; + return ( + + ); +}; diff --git a/src/config/chatAnimation.ts b/src/config/chatAnimation.ts new file mode 100644 index 0000000..ee69a6f --- /dev/null +++ b/src/config/chatAnimation.ts @@ -0,0 +1,86 @@ +/** + * Chat animation vocabulary: the modes, the speed bounds and the pure mappings + * between them. + * + * Kept free of DOM access so the URL contract, the setup form and the runtime + * queue can all depend on it; the stylesheet generation lives with the overlay + * runtime in `services/chat/runtime/animationStyles`. + */ + +export const CHAT_ANIMATION_MODES = [ + "fade", + "flow", + "scroll", + "none", +] as const; +export type ChatAnimationMode = (typeof CHAT_ANIMATION_MODES)[number]; + +export interface AnimationOptions { + enabled: boolean; + duration: number; + easing: string; + type: ChatAnimationMode; +} + +export const MIN_MESSAGE_SPEED = 0; +export const MAX_MESSAGE_SPEED = 100; +export const DEFAULT_MESSAGE_SPEED = 31; +export const MIN_MESSAGE_INTERVAL_MS = 80; +export const MAX_MESSAGE_INTERVAL_MS = 3000; + +export const DEFAULT_ANIMATION_OPTIONS: AnimationOptions = { + enabled: true, + duration: 200, + easing: "ease-in-out", + type: "fade", +}; +export const FLOW_ANIMATION_DURATION = 380; + +export function getMessageEntryAnimationDuration( + mode: ChatAnimationMode, +): number { + return mode === "flow" + ? FLOW_ANIMATION_DURATION + : DEFAULT_ANIMATION_OPTIONS.duration; +} + +export function normalizeChatAnimationMode( + value: unknown, + fallback: ChatAnimationMode = "fade", +): ChatAnimationMode { + return CHAT_ANIMATION_MODES.includes(value as ChatAnimationMode) + ? (value as ChatAnimationMode) + : fallback; +} + +export function hasMessageEntryAnimation(mode: ChatAnimationMode): boolean { + return mode === "fade" || mode === "flow"; +} + +export function getAnimationScrollBehavior( + mode: ChatAnimationMode, +): ScrollBehavior { + return mode === "scroll" ? "smooth" : "auto"; +} + +export function clampMessageSpeed(speed: number): number { + if (!Number.isFinite(speed)) return DEFAULT_MESSAGE_SPEED; + return Math.min( + Math.max(Math.round(speed), MIN_MESSAGE_SPEED), + MAX_MESSAGE_SPEED, + ); +} + +/** + * Maps the 1..100 speed control onto a message interval, or null when the speed + * is at its minimum, which means "no automatic pacing". + */ +export function messageSpeedToIntervalMs(speed: number): number | null { + const clamped = clampMessageSpeed(speed); + if (clamped <= MIN_MESSAGE_SPEED) return null; + + const normalized = (clamped - 1) / (MAX_MESSAGE_SPEED - 1); + const ratio = MIN_MESSAGE_INTERVAL_MS / MAX_MESSAGE_INTERVAL_MS; + + return Math.round(MAX_MESSAGE_INTERVAL_MS * ratio ** normalized); +} diff --git a/src/config/chatUrlParams.ts b/src/config/chatUrlParams.ts index 365dcb7..9840a67 100644 --- a/src/config/chatUrlParams.ts +++ b/src/config/chatUrlParams.ts @@ -12,9 +12,9 @@ import { clampMessageSpeed, normalizeChatAnimationMode, type ChatAnimationMode, -} from "../utils/ui/animationUtils"; +} from "./chatAnimation"; -export type { ChatAnimationMode } from "../utils/ui/animationUtils"; +export type { ChatAnimationMode } from "./chatAnimation"; export type LinkDisplayMode = "normal" | "hide" | "highlight"; export type PlatformMarkerMode = "none" | "stripe" | "icon"; diff --git a/src/config/formValues.ts b/src/config/formValues.ts new file mode 100644 index 0000000..8058885 --- /dev/null +++ b/src/config/formValues.ts @@ -0,0 +1,55 @@ +/** + * Coercion from raw form input to configuration values. + * + * The setup form keeps every field as a string so the inputs stay controlled, + * and the URL contract in `chatUrlParams` describes the same value shapes + * through its `ParamKind` vocabulary (`int`, `float`, `intOrFalse`, + * `secondsOrFalse`). These helpers are the form-side counterpart of that + * vocabulary, so a value typed into the form and the same value arriving in a + * URL land on identical types. + */ + +/** Accepts `#rrggbb`, or `#rrggbbaa` when `allowAlpha` is set. */ +export function normalizeHexColor( + raw: string, + fallback: string, + allowAlpha = false, +): string { + const value = raw.trim(); + const withHash = value.startsWith("#") ? value : `#${value}`; + const pattern = allowAlpha ? /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/ : /^#[0-9a-fA-F]{6}$/; + return pattern.test(withHash) ? withHash : fallback; +} + +export function toInt(raw: string, fallback: number): number { + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) ? n : fallback; +} + +export function toClampedInt( + raw: string, + fallback: number, + min: number, + max: number, +): number { + const n = Number.parseInt(raw, 10); + const value = Number.isFinite(n) ? n : fallback; + return Math.min(Math.max(value, min), max); +} + +export function toFloat(raw: string, fallback: number): number { + const n = Number.parseFloat(raw); + return Number.isFinite(n) ? n : fallback; +} + +/** + * Positive integer, or `false` when the input is not a positive number. + * + * This covers both the `intOrFalse` and `secondsOrFalse` URL kinds, which + * `chatUrlParams` already parses and serializes identically, and it replaces two + * byte-identical route helpers that differed only in name. + */ +export function toPositiveIntOrFalse(raw: string): number | false { + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : false; +} diff --git a/src/config/setupConfig.ts b/src/config/setupConfig.ts new file mode 100644 index 0000000..9931d72 --- /dev/null +++ b/src/config/setupConfig.ts @@ -0,0 +1,251 @@ +/** + * Projection from the setup form onto the overlay configuration. + * + * The setup form keeps every control as its own signal and the values stay in + * their raw, user-editable shape — mostly strings — so the inputs can be + * cleared while typing. This module owns the single step that turns that form + * state into a `ChatConfig`, and the single step that turns a `ChatConfig` into + * an overlay URL. + * + * Both are the setup page's half of the URL contract in `chatUrlParams`: a + * value the user typed and the same value arriving back in a URL must land on + * identical types, which is why the coercion helpers from `formValues` are + * applied here rather than at the input sites. + */ +import { + DEFAULT_CHAT_CONFIG, + chatConfigToSearchParams, + normalizeBotNames, + type ChatAnimationMode, + type ChatConfig, + type LinkDisplayMode, + type PlatformMarkerMode, +} from "./chatUrlParams"; +import type { EventColorConfig } from "./eventColors"; +import { + normalizeHexColor, + toClampedInt, + toFloat, + toInt, + toPositiveIntOrFalse, +} from "./formValues"; +import { MAX_MESSAGE_SPEED, MIN_MESSAGE_SPEED } from "./chatAnimation"; +import { getAppBaseUrl } from "../utils/appBase"; + +/** Raw setup-form values, before any coercion. */ +export type SetupFormState = { + youtubeChannel: string; + kickChannel: string; + size: string; + font: string; + lineHeight: string; + fontWeight: string; + nickFontWeight: string; + fontCustom: string; + shadow: string; + stroke: string; + fade: string; + animation: ChatAnimationMode; + messageSpeed: string; + showHomies: boolean; + show7tvBadges: boolean; + showFfzBadges: boolean; + showBttvBadges: boolean; + showChatterinoBadges: boolean; + showChatisBadges: boolean; + showTwitchBadges: boolean; + showYouTubeBadges: boolean; + showKickBadges: boolean; + recentMessages: boolean; + bots: boolean; + commands: boolean; + hideAllBadges: boolean; + emoteScale: string; + showGifs: boolean; + gifScale: string; + botNames: readonly string[]; + kickBotNames: readonly string[]; + youtubeBotNames: readonly string[]; + allowedChatters: readonly string[]; + show7tvUnlisted: boolean; + smallCaps: boolean; + nlAfterName: boolean; + hideNames: boolean; + reverseLineOrder: boolean; + horizontal: boolean; + platformMarker: PlatformMarkerMode; + ffzBotMixBroadcaster: boolean; + ffzBotMixModerator: boolean; + ffzBotMixVip: boolean; + overlayBackgroundColor: string; + overlayBackgroundOpacity: string; + overlayBackgroundRadius: string; + overlayPadding: string; + overlayBorderWidth: string; + overlayBorderColor: string; + highlightTwitchEvents: boolean; + eventColorOpacity: string; + eventColors: EventColorConfig; + twitchEventBold: boolean; + twitchEventItalic: boolean; + showHighlightedMessages: boolean; + showChannelPointRewards: boolean; + showGigantifiedEmotes: boolean; + showPredictions: boolean; + linkMode: LinkDisplayMode; + linkColor: string; + usersColorEnabled: boolean; + usersColor: string; + hideLinkRewards: boolean; + rteProxy: boolean; + rteAzureTts: boolean; + rteChatIsTts: boolean; + rteReyohohoBadge: boolean; + rteCustomCosmetics: boolean; +}; + +/** + * `selectedChannel` stays a separate argument because the preview and the + * exported link deliberately project the same form onto different channels. + */ +export function buildSetupConfig( + form: SetupFormState, + selectedChannel: string, +): ChatConfig { + return { + ...DEFAULT_CHAT_CONFIG, + channel: selectedChannel, + youtubeChannel: form.youtubeChannel.trim().replace(/^@/, ""), + kickChannel: form.kickChannel.trim().replace(/^@/, ""), + size: toInt(form.size, DEFAULT_CHAT_CONFIG.size), + font: toInt(form.font, DEFAULT_CHAT_CONFIG.font), + lineHeight: toClampedInt( + form.lineHeight, + DEFAULT_CHAT_CONFIG.lineHeight, + 80, + 200, + ), + fontWeight: toClampedInt( + form.fontWeight, + DEFAULT_CHAT_CONFIG.fontWeight, + 100, + 1000, + ), + nickFontWeight: toClampedInt( + form.nickFontWeight, + DEFAULT_CHAT_CONFIG.nickFontWeight, + 100, + 1000, + ), + fontCustom: form.fontCustom, + shadow: toPositiveIntOrFalse(form.shadow), + stroke: toPositiveIntOrFalse(form.stroke), + fade: toPositiveIntOrFalse(form.fade), + animation: form.animation, + messageSpeed: toClampedInt( + form.messageSpeed, + DEFAULT_CHAT_CONFIG.messageSpeed, + MIN_MESSAGE_SPEED, + MAX_MESSAGE_SPEED, + ), + showHomies: form.showHomies, + show7tvBadges: form.show7tvBadges, + showFfzBadges: form.showFfzBadges, + showBttvBadges: form.showBttvBadges, + showChatterinoBadges: form.showChatterinoBadges, + showChatisBadges: form.showChatisBadges, + showTwitchBadges: form.showTwitchBadges, + showYouTubeBadges: form.showYouTubeBadges, + showKickBadges: form.showKickBadges, + recentMessages: form.recentMessages, + bots: form.bots, + commands: form.commands, + hideAllBadges: form.hideAllBadges, + emoteScale: toFloat(form.emoteScale, DEFAULT_CHAT_CONFIG.emoteScale), + showGifs: form.showGifs, + gifScale: toFloat(form.gifScale, DEFAULT_CHAT_CONFIG.gifScale), + botNames: normalizeBotNames(form.botNames.join(",")), + kickBotNames: normalizeBotNames(form.kickBotNames.join(",")), + youtubeBotNames: normalizeBotNames(form.youtubeBotNames.join(",")), + singleChatter: normalizeBotNames(form.allowedChatters.join(",")), + show7tvUnlisted: form.show7tvUnlisted, + smallCaps: form.smallCaps, + nlAfterName: form.nlAfterName, + hideNames: form.hideNames, + reverseLineOrder: form.reverseLineOrder, + horizontal: form.horizontal, + platformMarker: form.platformMarker, + ffzBotMixCustom: true, + ffzBotMixBroadcaster: form.ffzBotMixBroadcaster, + ffzBotMixModerator: form.ffzBotMixModerator, + ffzBotMixVip: form.ffzBotMixVip, + overlayBackgroundColor: normalizeHexColor( + form.overlayBackgroundColor, + DEFAULT_CHAT_CONFIG.overlayBackgroundColor, + ), + overlayBackgroundOpacity: toInt( + form.overlayBackgroundOpacity, + DEFAULT_CHAT_CONFIG.overlayBackgroundOpacity, + ), + overlayBackgroundRadius: toInt( + form.overlayBackgroundRadius, + DEFAULT_CHAT_CONFIG.overlayBackgroundRadius, + ), + overlayPadding: toInt(form.overlayPadding, DEFAULT_CHAT_CONFIG.overlayPadding), + overlayBorderWidth: toInt( + form.overlayBorderWidth, + DEFAULT_CHAT_CONFIG.overlayBorderWidth, + ), + overlayBorderColor: normalizeHexColor( + form.overlayBorderColor, + DEFAULT_CHAT_CONFIG.overlayBorderColor, + true, + ), + highlightTwitchEvents: form.highlightTwitchEvents, + eventColorOpacity: toInt( + form.eventColorOpacity, + DEFAULT_CHAT_CONFIG.eventColorOpacity, + ), + ...form.eventColors, + twitchEventBold: form.twitchEventBold, + twitchEventItalic: form.twitchEventItalic, + showHighlightedMessages: form.showHighlightedMessages, + showChannelPointRewards: form.showChannelPointRewards, + showGigantifiedEmotes: form.showGigantifiedEmotes, + showPredictions: form.showPredictions, + linkMode: form.linkMode, + linkColor: normalizeHexColor(form.linkColor, DEFAULT_CHAT_CONFIG.linkColor), + usersColor: form.usersColorEnabled + ? normalizeHexColor(form.usersColor, "#ffffff") + : "", + hideLinkRewards: form.hideLinkRewards, + rteProxy: form.rteProxy, + rteAzureTts: form.rteAzureTts, + rteChatIsTts: form.rteChatIsTts, + rteReyohohoBadge: form.rteReyohohoBadge, + rteCustomCosmetics: form.rteCustomCosmetics, + }; +} + +/** + * Builds the overlay link. `includeMessageSpeed: false` drops the `ms` parameter + * for the embedded preview, which must follow the preview's own pacing rather + * than the exported message speed. `baseUrl` exists so the URL contract can be + * asserted in tests without a document to derive the app base from. + */ +export function buildOverlayUrl( + cfg: ChatConfig, + extraParams?: Record, + options?: { includeMessageSpeed?: boolean; baseUrl?: string }, +): string { + const params = chatConfigToSearchParams(cfg); + if (options?.includeMessageSpeed === false) { + params.delete("ms"); + } + if (extraParams) { + Object.entries(extraParams).forEach(([key, value]) => params.set(key, value)); + } + const query = params.toString(); + const baseUrl = options?.baseUrl ?? getAppBaseUrl(); + return `${baseUrl}/chat/${query ? `?${query}` : ""}`; +} diff --git a/src/config/setupTemplates.ts b/src/config/setupTemplates.ts index 56e0caa..53ada9c 100644 --- a/src/config/setupTemplates.ts +++ b/src/config/setupTemplates.ts @@ -1,6 +1,9 @@ import { DEFAULT_CHAT_CONFIG } from "./chatUrlParams"; import { DEFAULT_EVENT_COLORS } from "./eventColors"; import type { SetupImportPatch } from "./setupImport"; +import { + getSetupStorage, +} from "../services/storage/setupStorage"; const STORAGE_KEY = "chatyx.setup.templates.v1"; const STORAGE_VERSION = 1; @@ -65,8 +68,6 @@ export type UserSetupTemplate = { readonly settings: VisualSetupPatch; }; -type StorageLike = Pick; - export type BuiltInSetupTemplate = { readonly id: "standard" | "atom"; readonly settings: VisualSetupPatch; @@ -149,19 +150,6 @@ export const BUILT_IN_SETUP_TEMPLATES: readonly BuiltInSetupTemplate[] = [ }, ]; -function hasStorage(): boolean { - return typeof window !== "undefined" && "localStorage" in window; -} - -function getStorage(): StorageLike | null { - if (!hasStorage()) return null; - try { - return window.localStorage; - } catch { - return null; - } -} - function isTemplateSource(value: unknown): value is TemplateSource { return value === "manual" || value === "chatyx" || value === "chatis" || value === "cyan" || value === "davii"; } @@ -207,7 +195,7 @@ function isUserTemplate(value: unknown): value is UserSetupTemplate { && typeof value.createdAt === "string" && typeof value.updatedAt === "string"; } -export function readUserSetupTemplates(storage = getStorage()): UserSetupTemplate[] { +export function readUserSetupTemplates(storage = getSetupStorage()): UserSetupTemplate[] { if (!storage) return []; try { const raw = storage.getItem(STORAGE_KEY); @@ -223,7 +211,7 @@ export function readUserSetupTemplates(storage = getStorage()): UserSetupTemplat } } -export function writeUserSetupTemplates(templates: readonly UserSetupTemplate[], storage = getStorage()): boolean { +export function writeUserSetupTemplates(templates: readonly UserSetupTemplate[], storage = getSetupStorage()): boolean { if (!storage) return false; try { storage.setItem(STORAGE_KEY, JSON.stringify({ version: STORAGE_VERSION, templates })); diff --git a/src/config/twitch.ts b/src/config/twitch.ts index ba3de43..c537be4 100644 --- a/src/config/twitch.ts +++ b/src/config/twitch.ts @@ -10,6 +10,15 @@ export const TWITCH_CONFIG = { TWITCH_OAUTH_URL: "https://id.twitch.tv/oauth2/token", }; +/** + * Public anonymous GQL endpoint and its well-known web client id. Shared by the + * setup bot-profile lookups and the channel summary field so the constants are + * not redeclared per caller. + */ +export const TWITCH_GQL_ENDPOINT = "https://gql.twitch.tv/gql"; +export const TWITCH_WEB_CLIENT_ID = + import.meta.env.VITE_TWITCH_GQL_CLIENT_ID || "kimne78kx3ncx6brgo4mv6wki5h1ko"; + export const FALLBACK_APIS = { badges_global: "https://api.ivr.fi/v2/twitch/badges/global", badges_channel: (channelName: string) => diff --git a/src/features/chat-overlay/application/chatOverlayApplication.ts b/src/features/chat-overlay/application/chatOverlayApplication.ts index cdeaff7..906967b 100644 --- a/src/features/chat-overlay/application/chatOverlayApplication.ts +++ b/src/features/chat-overlay/application/chatOverlayApplication.ts @@ -3,7 +3,7 @@ import { type PreviewDemoKind, } from "~/services/chat/preview"; import type { TwitchPredictionEvent } from "~/services/predictions/twitchPredictions"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { PredictionControllerHooks } from "./predictionController"; import type { ChatRuntimeHooks } from "./runtimeHooks"; import { @@ -67,6 +67,7 @@ export class ChatOverlayApplication { private readonly predictions: PredictionsRuntime; private config: ChatConfig; private started = false; + private destroyed = false; constructor( private readonly options: ChatOverlayApplicationOptions, @@ -106,14 +107,21 @@ export class ChatOverlayApplication { } async start() { - if (this.started) return; + if (this.started || this.destroyed) return; this.started = true; window.addEventListener("message", this.handleConfigMessage); await this.runtime.initialize(); } + /** + * Releases both runtimes. The constructor builds them, so teardown is not + * conditional on `start()` having run: an application that never started still + * owns what it constructed. Destruction is terminal and idempotent, which also + * makes it safe while `start()` is still awaiting `initialize()`. + */ destroy() { - if (!this.started) return; + if (this.destroyed) return; + this.destroyed = true; this.started = false; window.removeEventListener("message", this.handleConfigMessage); this.predictions.destroy(); diff --git a/src/features/chat-overlay/application/liveRuntime.ts b/src/features/chat-overlay/application/liveRuntime.ts index 0e3bda0..d5aab51 100644 --- a/src/features/chat-overlay/application/liveRuntime.ts +++ b/src/features/chat-overlay/application/liveRuntime.ts @@ -8,6 +8,7 @@ import { createChatPresentationConfig, } from "~/services/chat/chatPresentationService"; import { emoteService } from "~/services/chat/assets/emoteService"; +import { badgeService } from "~/services/badges/badgeService"; import { mentionStyleService } from "~/services/chat/mentionStyleService"; import { chatFeatureIntegration } from "~/services/chat/chatFeatureIntegration"; import type { TwitchMessage } from "~/services/chat/twitch/twitchService"; @@ -16,9 +17,9 @@ import { fetchRecentMessages } from "~/services/chat/twitch/recentMessagesServic import { getAnimationScrollBehavior, hasMessageEntryAnimation, -} from "~/utils/ui/animationUtils"; +} from "~/config/chatAnimation"; import { log, LOG_CATEGORIES } from "~/utils/logger"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import { ChatCommandFeedback, CHATYX_DEVELOPER_CHANNEL, @@ -318,6 +319,12 @@ export class LiveChatRuntime { chatFeatureIntegration.destroy(); this.chatService?.cleanup(); this.chatService = null; + // Teardown owns everything it installed: the generated style elements, the + // shared proxy flag and the channel-scoped asset caches. + this.styleManager.cleanup(); + setRteProxyEnabled(false); + badgeService.reset(); + emoteService.reset(); this.initialized = false; } diff --git a/src/features/chat-overlay/application/predictionController.ts b/src/features/chat-overlay/application/predictionController.ts index cd7e547..1f92f0c 100644 --- a/src/features/chat-overlay/application/predictionController.ts +++ b/src/features/chat-overlay/application/predictionController.ts @@ -4,7 +4,7 @@ import { type TwitchPredictionEvent, type TwitchPredictionsClient, } from "~/services/predictions/twitchPredictions"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; export type PredictionControllerHooks = { onPredictionChange: (prediction: TwitchPredictionEvent | null) => void; diff --git a/src/features/chat-overlay/application/previewRuntime.ts b/src/features/chat-overlay/application/previewRuntime.ts index d594cd6..a5fbb52 100644 --- a/src/features/chat-overlay/application/previewRuntime.ts +++ b/src/features/chat-overlay/application/previewRuntime.ts @@ -20,8 +20,8 @@ import { getAnimationScrollBehavior, hasMessageEntryAnimation, messageSpeedToIntervalMs, -} from "~/utils/ui/animationUtils"; -import type { ChatConfig } from "~/utils/chat"; +} from "~/config/chatAnimation"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { ChatRuntimeHooks } from "./runtimeHooks"; const CHANNEL_RESOLUTION_TIMEOUT_MS = 8_000; @@ -54,6 +54,13 @@ export type PreviewRuntimeDependencies = { ) => Promise; loadBadges: (channel: string, channelId: string) => Promise; loadCosmetics: (channelId: string) => Promise; + /** + * Releases the process-wide stores this document's preview filled. The + * preview runtime is the only runtime in its document, so its teardown leaves + * that document's singletons empty instead of leaving the demo's emotes and + * badges behind. + */ + resetSharedAssetState: () => void; }; const browserDependencies: PreviewRuntimeDependencies = { @@ -66,6 +73,11 @@ const browserDependencies: PreviewRuntimeDependencies = { emoteService.loadEmotes(channelId, channel, { show7tvUnlisted }), loadBadges: (channel, channelId) => badgeService.loadBadges(channel, channelId), loadCosmetics: (channelId) => sevenTVCosmeticsService.loadCosmetics(channelId), + resetSharedAssetState: () => { + mentionStyleService.reset(); + badgeService.reset(); + emoteService.reset(); + }, }; function isTwitchUserId(value: string): boolean { @@ -94,6 +106,12 @@ export class PreviewRuntime { private renderTimer: number | null = null; private ready = false; private destroyed = false; + /** + * Set by `initialize()`, which is what claims the shared proxy flag and fills + * the shared asset stores. Teardown releases them only for a runtime that + * actually took them. + */ + private holdsSharedState = false; constructor( private readonly options: PreviewRuntimeOptions, @@ -104,11 +122,14 @@ export class PreviewRuntime { } async initialize() { - this.destroyed = false; + // Destruction is terminal: a runtime that released the shared state it + // claimed must not claim it again. + if (this.destroyed) return; const { channel } = this.options; const isRealChannel = Boolean(channel && channel !== "chatyxpreview"); this.dependencies.setProxyEnabled(this.config.rteProxy); + this.holdsSharedState = true; this.service = this.dependencies.createPresentationService(this.config); mentionStyleService.reset(); this.service.updateConfig({ userId: "0" }); @@ -191,9 +212,11 @@ export class PreviewRuntime { } updateConfig(config: ChatConfig) { + if (this.destroyed) return; const speedChanged = this.config.messageSpeed !== config.messageSpeed; this.config = config; this.dependencies.setProxyEnabled(config.rteProxy); + this.holdsSharedState = true; this.hooks.onConfigResolved(config); injectPreviewStyles(config); @@ -225,6 +248,16 @@ export class PreviewRuntime { cleanupPreviewStyles(); this.service?.cleanup(); this.service = null; + + // Release the document-wide state this runtime claimed. Concurrency is not + // possible by design: a document hosts exactly one chat runtime, so the + // runtime that enabled the proxy flag and filled the asset stores is the + // one that clears them. + if (this.holdsSharedState) { + this.holdsSharedState = false; + this.dependencies.setProxyEnabled(false); + this.dependencies.resetSharedAssetState(); + } } private finishInitialization() { diff --git a/src/features/chat-overlay/application/runtimeHooks.ts b/src/features/chat-overlay/application/runtimeHooks.ts index 77fd689..93dd34f 100644 --- a/src/features/chat-overlay/application/runtimeHooks.ts +++ b/src/features/chat-overlay/application/runtimeHooks.ts @@ -1,7 +1,7 @@ import type { ChatPresentationService } from "~/services/chat/chatPresentationService"; import type { MessageUpdater } from "~/services/chat/runtime/messageQueueManager"; import type { ChannelIdentity } from "~/services/chat/runtime/channelIdentityResolver"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; export type ChatCommandStatus = { text: string; diff --git a/src/features/chat-overlay/model/overlayStyles.ts b/src/features/chat-overlay/model/overlayStyles.ts index d2f7a62..69144c0 100644 --- a/src/features/chat-overlay/model/overlayStyles.ts +++ b/src/features/chat-overlay/model/overlayStyles.ts @@ -1,4 +1,4 @@ -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); diff --git a/src/features/setup/model/setupSections.ts b/src/features/setup/model/setupSections.ts new file mode 100644 index 0000000..dd5a055 --- /dev/null +++ b/src/features/setup/model/setupSections.ts @@ -0,0 +1,18 @@ +/** + * Setup section identity. + * + * A setup section is a domain concept: the route composes sections, the + * workspace navigation presents them and the settings search points at them. + * The identity therefore lives in the feature's model so none of those three + * has to import another, and the presentation layer keeps only the icon and + * translation metadata that belongs to it. + */ +export type SetupSectionId = + | "import" + | "appearance" + | "styling" + | "behavior" + | "content" + | "bots" + | "tts" + | "rte"; diff --git a/src/features/setup/previewSync.ts b/src/features/setup/previewSync.ts new file mode 100644 index 0000000..6edf9e4 --- /dev/null +++ b/src/features/setup/previewSync.ts @@ -0,0 +1,94 @@ +/** + * The setup page's side of the preview protocol. + * + * The embedded overlay is an iframe that receives its configuration over + * `postMessage` and its navigation through a direct `src` assignment. Both + * mechanics are here rather than in the route: the route keeps the reactive + * effects, this module keeps the iframe protocol, the debounce and the session + * bookkeeping. + */ +import type { ChatConfig } from "~/config/chatUrlParams"; +import { createChatPreviewConfigMessage } from "~/services/chat/preview/configMessage"; + +/** + * Long enough that typing in a text field does not reload the overlay on every + * keystroke, short enough that the preview still feels immediate. + */ +const PREVIEW_NAVIGATION_DEBOUNCE_MS = 180; + +export type PreviewSynchronizerOptions = { + getIframe: () => HTMLIFrameElement | undefined; + /** + * Used when the iframe has not been created yet — the first URL has to arrive + * through the signal so the element mounts with it. + */ + setFallbackUrl: (url: string) => void; + debounceMs?: number; +}; + +export type PreviewSynchronizer = { + /** Pushes a config to the overlay without reloading it. */ + postConfig: (config: ChatConfig) => void; + /** + * Navigates only when `sessionKey` differs from the last navigation, and + * debounces the swap. The URL is built lazily, so an unchanged session costs + * nothing. Returns whether a navigation was scheduled. + */ + scheduleNavigation: (sessionKey: string, buildUrl: () => string) => boolean; + /** Cancels a pending navigation. */ + dispose: () => void; +}; + +export function createPreviewSynchronizer( + options: PreviewSynchronizerOptions, +): PreviewSynchronizer { + let activeSessionKey = ""; + let navigationTimer: number | undefined; + + const navigate = (url: string) => { + const iframe = options.getIframe(); + if (iframe) { + // Assigning through the element swaps the document without the + // about:blank flash a signal-driven src update would produce. + iframe.src = url; + } else { + options.setFallbackUrl(url); + } + }; + + const postConfig = (config: ChatConfig) => { + const target = options.getIframe()?.contentWindow; + if (!target) return; + target.postMessage( + createChatPreviewConfigMessage(config), + window.location.origin, + ); + }; + + const scheduleNavigation = ( + sessionKey: string, + buildUrl: () => string, + ): boolean => { + if (sessionKey === activeSessionKey) return false; + activeSessionKey = sessionKey; + + if (navigationTimer !== undefined) { + window.clearTimeout(navigationTimer); + } + navigationTimer = window.setTimeout(() => { + navigationTimer = undefined; + navigate(buildUrl()); + }, options.debounceMs ?? PREVIEW_NAVIGATION_DEBOUNCE_MS); + + return true; + }; + + const dispose = () => { + if (navigationTimer !== undefined) { + window.clearTimeout(navigationTimer); + navigationTimer = undefined; + } + }; + + return { postConfig, scheduleNavigation, dispose }; +} diff --git a/src/features/setup/settingsSearch.ts b/src/features/setup/settingsSearch.ts new file mode 100644 index 0000000..d72f979 --- /dev/null +++ b/src/features/setup/settingsSearch.ts @@ -0,0 +1,78 @@ +/** + * Settings search for the setup workspace. + * + * The matcher itself is pure and lives in `utils/setupSearch`. This module owns + * the DOM half: which elements are searchable, which section each hit belongs + * to, the highlight class, the counter format and the reveal scroll. The scroll + * callback is passed in because the workspace's scroll state belongs to the + * route. + */ +import type { SetupSectionId } from "~/features/setup/model/setupSections"; +import { isSetupSearchMatch } from "~/utils/setupSearch"; + +/** + * Rows and field headings are searchable. The selector is a layout contract, so + * it stays next to the highlight class that the workspace stylesheet expects. + */ +const SEARCHABLE_SELECTOR = + ".setup-section .setup-control-row, .setup-section .setup-switch-row, .setup-section .setup-field-group > h3"; + +const SECTION_SELECTOR = ".setup-section"; +const SECTION_ID_PREFIX = "setup-section-"; +const HIGHLIGHT_CLASS = "setup-search-match"; + +/** Shortest query the search acts on; below it every query matches everything. */ +export const MIN_SETUP_SEARCH_LENGTH = 3; + +export type SetupSearchHit = { + element: HTMLElement; + /** Undefined when a matched element sits outside any section. */ + sectionId: SetupSectionId | undefined; +}; + +function sectionIdOf(element: HTMLElement): SetupSectionId | undefined { + const id = element + .closest(SECTION_SELECTOR) + ?.id.replace(SECTION_ID_PREFIX, ""); + return id ? (id as SetupSectionId) : undefined; +} + +/** Collects the searchable elements that match `query`, in document order. */ +export function collectSetupSearchHits(query: string): SetupSearchHit[] { + if (typeof document === "undefined") return []; + + return Array.from(document.querySelectorAll(SEARCHABLE_SELECTOR)) + .filter((element) => isSetupSearchMatch(query, element.textContent ?? "")) + .map((element) => ({ element, sectionId: sectionIdOf(element) })); +} + +/** Formats the "current / total" counter, which reads "0" with no hits. */ +export function formatSetupSearchCounter(index: number, total: number): string { + if (total === 0) return "0"; + return `${Math.min(index, total - 1) + 1} / ${total}`; +} + +/** Marks exactly `hits`, clearing the class from every previous hit. */ +export function highlightSetupSearchHits(hits: readonly SetupSearchHit[]): void { + document + .querySelectorAll(`.${HIGHLIGHT_CLASS}`) + .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS)); + for (const hit of hits) hit.element.classList.add(HIGHLIGHT_CLASS); +} + +/** + * Brings a hit into view: the owning section is scrolled first so the element + * exists in the visible column, then the element itself. The double frame waits + * for that section switch to lay out. + */ +export function revealSetupSearchHit( + hit: SetupSearchHit, + scrollToSection: (section: SetupSectionId) => void, +): void { + if (hit.sectionId) scrollToSection(hit.sectionId); + requestAnimationFrame(() => + requestAnimationFrame(() => { + hit.element.scrollIntoView({ behavior: "smooth", block: "start" }); + }), + ); +} diff --git a/src/features/setup/setupDocument.ts b/src/features/setup/setupDocument.ts new file mode 100644 index 0000000..81d8315 --- /dev/null +++ b/src/features/setup/setupDocument.ts @@ -0,0 +1,48 @@ +/** + * Document-level lifecycle for the setup workspace. + * + * Setup lays itself out in fixed columns and therefore takes over document + * scrolling, and it pauses the demo when the user prefers reduced motion. Both + * are concerns of the document rather than of a component, so they live here and + * the route only wires the results into its signals. + * + * The lock is a state marker, not a style: it puts `data-setup-document` on the + * root and the setup stylesheet owns what that means. The workspace paints its + * own surface on `.setup-root`, and the app keeps the document background + * transparent for OBS, so this module owns no colour at all. + */ + +/** Marks the setup document lock; the setup stylesheet owns the rules. */ +const SETUP_DOCUMENT_ATTRIBUTE = "data-setup-document"; + +/** + * Locks document scrolling while the setup workspace is mounted. Returns the + * release function, which restores exactly the attribute state that was in place + * before the lock. + */ +export function lockSetupDocument(): () => void { + const root = document.documentElement; + const previous = root.getAttribute(SETUP_DOCUMENT_ATTRIBUTE); + root.setAttribute(SETUP_DOCUMENT_ATTRIBUTE, ""); + + return () => { + if (previous === null) root.removeAttribute(SETUP_DOCUMENT_ATTRIBUTE); + else root.setAttribute(SETUP_DOCUMENT_ATTRIBUTE, previous); + }; +} + +/** + * Reports the reduced-motion preference immediately and again on every change, + * so callers do not need a separate initial read. Returns the unsubscribe + * function. + */ +export function watchReducedMotion( + onChange: (matches: boolean) => void, +): () => void { + const query = window.matchMedia("(prefers-reduced-motion: reduce)"); + const sync = () => onChange(query.matches); + + sync(); + query.addEventListener("change", sync); + return () => query.removeEventListener("change", sync); +} diff --git a/src/root.css b/src/root.css index 43084ac..a3873a1 100644 --- a/src/root.css +++ b/src/root.css @@ -5,16 +5,13 @@ src: url(/v3/font/Alsina_Ultrajada.ttf); } -/* Minimalist grayscale theme with white text */ +/* Default text color for every surface. + The overlay renders on a transparent OBS background rather than on the + themed app surface, so it must not inherit the SolidCN foreground token, + which is near-black in the light theme. This is the only token that stays + outside `app.css` (see documents/REFACTORING.md section 10). */ :root { - --bg: #1a1a1a; - --panel: #2a2a2a; --text: #ffffff; - --text-secondary: #b3b3b3; - --border: #3a3a3a; - --input-bg: #0f0f0f; - --button-bg: #ffffff; - --button-text: #000000; } html, @@ -77,11 +74,3 @@ button:focus { outline: none; box-shadow: none; } - -/* Utility classes */ -.bg-panel { - background: var(--panel); -} -.text-secondary { - color: var(--text-secondary); -} diff --git a/src/routes/chat/channel.tsx b/src/routes/chat/channel.tsx index 266d68b..2bd0abf 100644 --- a/src/routes/chat/channel.tsx +++ b/src/routes/chat/channel.tsx @@ -17,9 +17,9 @@ import { } from "~/services/chat"; import type { TwitchPredictionEvent } from "~/services/predictions/twitchPredictions"; import "~/styles/chat.css"; -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { PreviewDemoKind } from "~/services/chat/preview"; -import { DEFAULT_ANIMATION_OPTIONS } from "~/utils/ui/animationUtils"; +import { DEFAULT_ANIMATION_OPTIONS } from "~/config/chatAnimation"; import { createChatOverlayApplication, type ChatCommandStatus, diff --git a/src/routes/dev/messages.tsx b/src/routes/dev/messages.tsx index 3d3f7bd..e5fc5f6 100644 --- a/src/routes/dev/messages.tsx +++ b/src/routes/dev/messages.tsx @@ -14,10 +14,9 @@ import { type ChatConfig, } from "~/config/chatUrlParams"; import { - generateShadowStyles, - generateSizeStyles, - generateStrokeStyles, - generateVariantStyles, + getOverlayStyleVariables, + OVERLAY_ATTRIBUTES, + OVERLAY_STYLE_PROPERTIES, } from "~/styles/chatStyles"; import "~/styles/chat.css"; import "./messages.css"; @@ -28,7 +27,6 @@ type DevCase = { companionMessage?: TwitchMessage; }; -const DEV_STYLE_ID = "chat-dev-message-style-overrides"; const DEV_SCROLL_CLASS = "message-style-dev-scroll"; const DEV_THEME_CLASS = "dark"; const DEV_EMOTE_URL = @@ -348,19 +346,32 @@ function createDevCases(): DevCase[] { } function installDevChatStyles(config: ChatConfig) { - let styleEl = document.getElementById(DEV_STYLE_ID) as HTMLStyleElement | null; - if (!styleEl) { - styleEl = document.createElement("style"); - styleEl.id = DEV_STYLE_ID; - document.head.appendChild(styleEl); + // The dev fixture publishes the same custom properties the overlay does, so the + // shared stylesheet renders it; it deliberately does not touch the animation + // styles, which this page never generated. + const root = document.documentElement; + for (const name of OVERLAY_STYLE_PROPERTIES) { + root.style.removeProperty(name); } + for (const [name, value] of Object.entries(getOverlayStyleVariables(config))) { + root.style.setProperty(name, value); + } + for (const [attribute, enabled] of [ + [OVERLAY_ATTRIBUTES.hideNames, config.hideNames], + [OVERLAY_ATTRIBUTES.nlAfterName, config.nlAfterName], + ] as const) { + if (enabled) root.setAttribute(attribute, ""); + else root.removeAttribute(attribute); + } +} - styleEl.textContent = [ - generateSizeStyles(config.size as 1 | 2 | 3, config.lineHeight), - config.shadow ? generateShadowStyles(config.shadow as 1 | 2 | 3) : "", - config.stroke ? generateStrokeStyles(config.stroke as 1 | 2 | 3 | 4) : "", - generateVariantStyles(config), - ].join("\n"); +function clearDevChatStyles() { + const root = document.documentElement; + for (const name of OVERLAY_STYLE_PROPERTIES) { + root.style.removeProperty(name); + } + root.removeAttribute(OVERLAY_ATTRIBUTES.hideNames); + root.removeAttribute(OVERLAY_ATTRIBUTES.nlAfterName); } export default function MessageStylesDevPage() { @@ -418,7 +429,7 @@ export default function MessageStylesDevPage() { document.documentElement.classList.remove(DEV_SCROLL_CLASS); document.body.classList.remove(DEV_SCROLL_CLASS); if (!hadThemeClass) document.documentElement.classList.remove(DEV_THEME_CLASS); - document.getElementById(DEV_STYLE_ID)?.remove(); + clearDevChatStyles(); void service.cleanup(); }); diff --git a/src/routes/setup.tsx b/src/routes/setup.tsx index f771fbc..7ea18f4 100644 --- a/src/routes/setup.tsx +++ b/src/routes/setup.tsx @@ -9,7 +9,8 @@ import { type JSX, } from "solid-js"; import { Title } from "@solidjs/meta"; -import { ColorPickerField } from "~/components/ColorPickerField"; +import { PlatformGlyph } from "~/components/brand/PlatformGlyph"; +import { ColorPickerField } from "~/components/setup/ColorPickerField"; import { LanguageSwitcher } from "~/components/setup/LanguageSwitcher"; import { locale, t } from "~/i18n"; import { @@ -19,19 +20,56 @@ import { SetupNav, ToggleRows, type ControlRow, - type SetupSectionId, type ToggleRow, } from "~/components/setup/SetupLayout"; +import type { SetupSectionId } from "~/features/setup/model/setupSections"; import { VoiceCatalog } from "~/components/setup/VoiceCatalog"; import { SetupImportCard } from "~/components/setup/SetupImportCard"; +import { SetupChipInput } from "~/components/setup/SetupChipInput"; +import { UserChip } from "~/components/setup/UserChip"; +import { FfzBadgeMergeBlock } from "~/components/setup/FfzBadgeMergeBlock"; +import { PreviewControls } from "~/components/setup/PreviewControls"; +import { createAppearanceRows } from "~/components/setup/sections/appearanceRows"; +import { createStylingRows } from "~/components/setup/sections/stylingRows"; +import { + createBehaviorRows, + createBehaviorToggles, +} from "~/components/setup/sections/behaviorRows"; +import { + createContentToggles, + createRteToggles, + createTtsToggles, +} from "~/components/setup/sections/toggleRows"; import { parseSetupImport } from "~/config/setupImport"; import { toVisualSetupPatch } from "~/config/setupTemplates"; import { applySetupImport } from "~/components/setup/setupImportAdapter"; -import { SetupNumberField } from "~/components/setup/SetupNumberField"; +import { + SETUP_STORAGE_KEYS, + readStoredSetupValue, + writeStoredSetupValue, +} from "~/services/storage/setupStorage"; +import { + loadKickBotProfiles, + loadTwitchBotProfiles, + type BotProfile, +} from "~/services/setup/botProfiles"; +import { mergeUniqueLogins } from "~/services/setup/logins"; +import { + detectLocalFontBrowser, + loadLocalFontOptions, + type LocalFontOption, +} from "~/services/setup/localFonts"; +import { toClampedInt, toInt } from "~/config/formValues"; +import { + buildOverlayUrl, + buildSetupConfig, + type SetupFormState, +} from "~/config/setupConfig"; import { SetupSelect } from "~/components/setup/SetupSelect"; import { SetupSwitch } from "~/components/setup/SetupSwitch"; import { TwitchChannelField } from "~/components/setup/TwitchChannelField"; import { Button } from "~/components/ui/button"; +import { Icon } from "~/components/ui/icon"; import { Input } from "~/components/ui/input"; import { Slider } from "~/components/ui/slider"; import { @@ -43,28 +81,50 @@ import { import { DEFAULT_CHAT_CONFIG, chatConfigToSearchParams, - normalizeBotNames, parseBotNames, type ChatAnimationMode, - type ChatConfig, type LinkDisplayMode, type PlatformMarkerMode, } from "~/config/chatUrlParams"; -import { getAppBaseUrl, getPublicAssetUrl } from "~/utils/appBase"; +import { getPublicAssetUrl } from "~/utils/appBase"; import { MAX_MESSAGE_SPEED, MIN_MESSAGE_SPEED, messageSpeedToIntervalMs, -} from "~/utils/ui/animationUtils"; +} from "~/config/chatAnimation"; +import { getChatPreviewSessionKey } from "~/services/chat/preview"; +import { createPreviewSynchronizer } from "~/features/setup/previewSync"; import { - createChatPreviewConfigMessage, - getChatPreviewSessionKey, -} from "~/services/chat/preview"; + lockSetupDocument, + watchReducedMotion, +} from "~/features/setup/setupDocument"; import { cn } from "~/lib/utils"; -import { isSetupSearchMatch } from "~/utils/setupSearch"; -import Monitor from "lucide-solid/icons/monitor"; -import Pause from "lucide-solid/icons/pause"; -import Play from "lucide-solid/icons/play"; +import { + collectSetupSearchHits, + formatSetupSearchCounter, + highlightSetupSearchHits, + revealSetupSearchHit, + MIN_SETUP_SEARCH_LENGTH, + type SetupSearchHit, +} from "~/features/setup/settingsSearch"; +import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon"; +import ArrowLeft01Icon from "@hugeicons/core-free-icons/ArrowLeft01Icon"; +import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon"; +import ArrowUpRightStackIcon from "@hugeicons/core-free-icons/ArrowUpRightStackIcon"; +import Blockchain05Icon from "@hugeicons/core-free-icons/Blockchain05Icon"; +import BotMessageSquareIcon from "@hugeicons/core-free-icons/BotMessageSquareIcon"; +import ColorsIcon from "@hugeicons/core-free-icons/ColorsIcon"; +import Copy01Icon from "@hugeicons/core-free-icons/Copy01Icon"; +import DashboardSquare03Icon from "@hugeicons/core-free-icons/DashboardSquare03Icon"; +import GithubIcon from "@hugeicons/core-free-icons/GithubIcon"; +import LinkSquare01Icon from "@hugeicons/core-free-icons/LinkSquare01Icon"; +import MonitorIcon from "@hugeicons/core-free-icons/MonitorIcon"; +import SearchingIcon from "@hugeicons/core-free-icons/SearchingIcon"; +import SlidersHorizontalIcon from "@hugeicons/core-free-icons/SlidersHorizontalIcon"; +import TextIcon from "@hugeicons/core-free-icons/TextIcon"; +import TrashIcon from "@hugeicons/core-free-icons/TrashIcon"; +import VoiceCommentIcon from "@hugeicons/core-free-icons/VoiceCommentIcon"; +import XIcon from "@hugeicons/core-free-icons/XIcon"; const eventColorPalette: ReadonlyArray<{ readonly field: EventColorField; @@ -85,32 +145,8 @@ const eventColorPalette: ReadonlyArray<{ { field: "eventColorAnnOrange", label: () => t("setup.eventColorAnnOrange") }, ]; -import SlidersHorizontal from "lucide-solid/icons/sliders-horizontal"; -import X from "lucide-solid/icons/x"; import "~/components/setup/SetupWorkspace.css"; -type BotProfile = { - login: string; - displayName: string; - avatarUrl: string; -}; - -type LocalFontData = { - family: string; - fullName?: string; - postscriptName?: string; - style?: string; -}; - -type LocalFontOption = { - family: string; - styles: string[]; -}; - -type LocalFontWindow = Window & { - queryLocalFonts?: () => Promise; -}; - type LocalFontStatus = | { kind: "idle" } | { kind: "available"; browser: string } @@ -120,218 +156,8 @@ type LocalFontStatus = | { kind: "empty" } | { kind: "error" }; -const TWITCH_GQL_ENDPOINT = "https://gql.twitch.tv/gql"; -const TWITCH_WEB_CLIENT_ID = - import.meta.env.VITE_TWITCH_GQL_CLIENT_ID || "kimne78kx3ncx6brgo4mv6wki5h1ko"; -const SETUP_STORAGE_KEYS = { - config: "chatyx.setup.config.v1", - twitchChannel: "chatyx.setup.twitchChannel", - previewStageBackdrop: "chatyx.setup.previewStageBackdrop", - previewStageColor: "chatyx.setup.previewStageColor", -} as const; - -function readStoredSetupValue(key: string): string { - if (typeof window === "undefined") return ""; - - try { - return window.localStorage.getItem(key) || ""; - } catch { - return ""; - } -} - -function writeStoredSetupValue(key: string, value: string) { - if (typeof window === "undefined") return; - - try { - const normalized = value.trim(); - if (normalized) { - window.localStorage.setItem(key, normalized); - } else { - window.localStorage.removeItem(key); - } - } catch { - // Storage can be blocked in private windows; setup must still work. - } -} - -function detectLocalFontBrowser(): string | null { - if (typeof window === "undefined" || typeof navigator === "undefined") { - return null; - } - - const hasApi = - typeof (window as LocalFontWindow).queryLocalFonts === "function"; - if (!hasApi) return null; - - const ua = navigator.userAgent; - const vendor = navigator.vendor || ""; - - if (/Edg\//.test(ua)) return "Edge"; - if (/(OPR|Opera)\//.test(ua)) return "Opera"; - if (/Chrome\//.test(ua) && vendor.includes("Google")) return "Chrome"; - - return null; -} - -function normalizeLocalFonts(fonts: LocalFontData[]): LocalFontOption[] { - const families = new Map>(); - - for (const font of fonts) { - const family = font.family?.trim(); - if (!family) continue; - - const styles = families.get(family) ?? new Set(); - if (font.style) styles.add(font.style); - families.set(family, styles); - } - - return Array.from(families.entries()) - .map(([family, styles]) => ({ - family, - styles: Array.from(styles).sort((a, b) => a.localeCompare(b)), - })) - .sort((a, b) => a.family.localeCompare(b.family)); -} - -function normalizeBotLogin(raw: string): string { - return raw.trim().replace(/^@/, "").toLowerCase(); -} - -function splitBotLogins(raw: string): string[] { - return raw - .split(/[\s,]+/) - .map(normalizeBotLogin) - .filter(Boolean); -} - -function botFallbackName(login: string): string { - return login.slice(0, 1).toUpperCase(); -} - -async function fetchJsonWithTimeout( - url: string, - init: RequestInit, - timeoutMs: number, -): Promise { - const controller = new AbortController(); - const timeout = window.setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetch(url, { ...init, signal: controller.signal }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return await response.json(); - } finally { - window.clearTimeout(timeout); - } -} - -async function loadBotProfiles(logins: string[]): Promise { - if (logins.length === 0) return []; - - try { - const payload = await fetchJsonWithTimeout( - TWITCH_GQL_ENDPOINT, - { - method: "POST", - headers: { - "Client-ID": TWITCH_WEB_CLIENT_ID, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - operationName: "ChatYXSetupBotProfiles", - query: ` - query ChatYXSetupBotProfiles($logins: [String!]!) { - users(logins: $logins) { - login - displayName - profileImageURL(width: 70) - } - } - `, - variables: { logins }, - }), - }, - 3500, - ); - - const users = (payload as { data?: { users?: unknown[] } })?.data?.users; - if (!Array.isArray(users)) return []; - - return users - .map((user) => { - if (!user || typeof user !== "object") return null; - - const entry = user as { - login?: unknown; - displayName?: unknown; - profileImageURL?: unknown; - }; - const login = String(entry.login || "").toLowerCase(); - if (!login) return null; - - return { - login, - displayName: String(entry.displayName || entry.login || login), - avatarUrl: String(entry.profileImageURL || ""), - }; - }) - .filter((profile): profile is BotProfile => profile !== null); - } catch { - return []; - } -} - -async function loadKickBotProfiles(logins: string[]): Promise { - const profiles = await Promise.all( - logins.map(async (login) => { - try { - const payload = await fetchJsonWithTimeout( - `https://kick.com/api/v2/channels/${encodeURIComponent(login)}/info`, - {}, - 3500, - ); - const channel = payload as { - slug?: unknown; - user?: { username?: unknown; profile_pic?: unknown }; - }; - if (typeof channel.slug !== "string") return null; - const avatarUrl = channel.user?.profile_pic; - - return { - login: channel.slug.toLowerCase(), - displayName: - typeof channel.user?.username === "string" && channel.user.username.trim() - ? channel.user.username.trim() - : channel.slug, - avatarUrl: - typeof avatarUrl === "string" && avatarUrl.startsWith("https://") - ? avatarUrl - : "", - }; - } catch { - return null; - } - }), - ); - return profiles.filter((profile): profile is BotProfile => profile !== null); -} - -function mergeUniqueLogins(current: string[], raw: string): string[] { - const nextLogins = splitBotLogins(raw); - if (nextLogins.length === 0) return current; - - const seen = new Set(current); - const merged = [...current]; - - for (const login of nextLogins) { - if (seen.has(login)) continue; - seen.add(login); - merged.push(login); - } - - return merged; -} +/** YouTube bot entries have no profile lookup, so their chips show the login. */ +const EMPTY_BOT_PROFILES = (): Record => ({}); export default function ChatSetup() { const [channel, setChannel] = createSignal( @@ -436,17 +262,6 @@ export default function ChatSetup() { const [previewDemoKind, setPreviewDemoKind] = createSignal< "pasta" | "emote" >("pasta"); - const [previewModeThumbStyle, setPreviewModeThumbStyle] = - createSignal({ opacity: "0" }); - const [previewDemoThumbStyle, setPreviewDemoThumbStyle] = - createSignal({ opacity: "0" }); - let previewControlsRef: HTMLDivElement | undefined; - let previewModeSelectorRef: HTMLDivElement | undefined; - let previewLiveOptionRef: HTMLButtonElement | undefined; - let previewDemoOptionRef: HTMLButtonElement | undefined; - let previewDemoSelectorRef: HTMLDivElement | undefined; - let previewPastaOptionRef: HTMLButtonElement | undefined; - let previewEmoteOptionRef: HTMLButtonElement | undefined; const [showHomies, setShowHomies] = createSignal( DEFAULT_CHAT_CONFIG.showHomies, ); @@ -620,66 +435,30 @@ export default function ChatSetup() { let bodyScrollRef: HTMLDivElement | undefined; const sectionScrollPositions = new Map(); const viewScrollPositions = { settings: 0, preview: 0 }; - let activePreviewSessionKey = ""; - let previewNavigationTimer: number | undefined; let copyResetTimer: number | undefined; - const postPreviewConfig = (config = previewConfig()) => { - iframeRef?.contentWindow?.postMessage( - createChatPreviewConfigMessage(config), - window.location.origin, - ); - }; + const previewSync = createPreviewSynchronizer({ + getIframe: () => iframeRef, + setFallbackUrl: setPreviewUrl, + }); onMount(() => { - const html = document.documentElement; - const body = document.body; - const root = document.getElementById("root"); - const prev = { - htmlBg: html.style.background, - bodyBg: body.style.background, - htmlOverflow: html.style.overflow, - bodyOverflow: body.style.overflow, - bodyHeight: body.style.height, - rootOverflow: root?.style.overflow ?? "", - rootHeight: root?.style.height ?? "", - }; - const supportedBrowser = detectLocalFontBrowser(); - const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); - const syncMotionPreference = () => { - setReducedMotion(motionQuery.matches); - if (motionQuery.matches) setDemoPaused(true); - }; - syncMotionPreference(); - motionQuery.addEventListener("change", syncMotionPreference); - - // Lock document scroll — setup owns scrolling in fixed columns - html.style.background = "#09090b"; - html.style.overflow = "hidden"; - body.style.background = "#09090b"; - body.style.overflow = "hidden"; - body.style.height = "100%"; - if (root) { - root.style.overflow = "hidden"; - root.style.height = "100%"; - } + const releaseDocumentLock = lockSetupDocument(); + const stopMotionWatch = watchReducedMotion((matches) => { + setReducedMotion(matches); + if (matches) setDemoPaused(true); + }); + const supportedBrowser = detectLocalFontBrowser(); if (supportedBrowser) { setLocalFontBrowser(supportedBrowser); setLocalFontStatus({ kind: "available", browser: supportedBrowser }); } onCleanup(() => { - motionQuery.removeEventListener("change", syncMotionPreference); - html.style.background = prev.htmlBg; - html.style.overflow = prev.htmlOverflow; - body.style.background = prev.bodyBg; - body.style.overflow = prev.bodyOverflow; - body.style.height = prev.bodyHeight; - if (root) { - root.style.overflow = prev.rootOverflow; - root.style.height = prev.rootHeight; - } + previewSync.dispose(); + stopMotionWatch(); + releaseDocumentLock(); }); }); @@ -688,46 +467,33 @@ const [activeSection, setActiveSection] = const [setupSearch, setSetupSearch] = createSignal(""); const [setupSearchIndex, setSetupSearchIndex] = createSignal(0); const setupSearchQuery = createMemo(() => setupSearch().trim()); - const setupSearchResults = createMemo(() => { + const setupSearchHits = createMemo(() => { const query = setupSearchQuery(); - if (query.length < 3 || typeof document === "undefined") return [] as HTMLElement[]; - return Array.from(document.querySelectorAll( - ".setup-section .setup-control-row, .setup-section .setup-switch-row, .setup-section .setup-field-group > h3", - )).filter((element) => isSetupSearchMatch(query, element.textContent ?? "")); + if (query.length < MIN_SETUP_SEARCH_LENGTH) return [] as SetupSearchHit[]; + return collectSetupSearchHits(query); }); const matchedSearchSections = createMemo(() => new Set( - setupSearchResults().flatMap((element) => { - const id = element.closest(".setup-section")?.id.replace("setup-section-", "") as SetupSectionId | undefined; - return id ? [id] : []; - }), + setupSearchHits().flatMap((hit) => (hit.sectionId ? [hit.sectionId] : [])), )); - const setupSearchCounter = createMemo(() => { - const total = setupSearchResults().length; - if (total === 0) return "0"; - return `${Math.min(setupSearchIndex(), total - 1) + 1} / ${total}`; - }); + const setupSearchCounter = createMemo(() => + formatSetupSearchCounter(setupSearchIndex(), setupSearchHits().length), + ); const focusSearchResult = (index: number) => { - const results = setupSearchResults(); - if (results.length === 0) return; - const next = (index + results.length) % results.length; + const hits = setupSearchHits(); + if (hits.length === 0) return; + const next = (index + hits.length) % hits.length; setSetupSearchIndex(next); - const target = results[next]!; - const section = target.closest(".setup-section")?.id.replace("setup-section-", "") as SetupSectionId | undefined; - if (section) scrollToSection(section); - requestAnimationFrame(() => requestAnimationFrame(() => { - target.scrollIntoView({ behavior: "smooth", block: "start" }); - })); + revealSetupSearchHit(hits[next]!, scrollToSection); }; let previousSearchQuery = ""; createEffect(() => { - document.querySelectorAll(".setup-search-match").forEach((element) => element.classList.remove("setup-search-match")); - const results = setupSearchResults(); - results.forEach((element) => element.classList.add("setup-search-match")); + const hits = setupSearchHits(); + highlightSetupSearchHits(hits); const query = setupSearchQuery(); if (query === previousSearchQuery) return; previousSearchQuery = query; setSetupSearchIndex(0); - if (results.length > 0) focusSearchResult(0); + if (hits.length > 0) focusSearchResult(0); }); const importSettings = (patch: Parameters[0]) => { @@ -834,39 +600,6 @@ const [activeSection, setActiveSection] = if (bodyScrollRef) bodyScrollRef.scrollTop = viewScrollPositions[view]; }; - const normalizeHexColor = (raw: string, fallback: string, allowAlpha = false): string => { - const value = raw.trim(); - const withHash = value.startsWith("#") ? value : `#${value}`; - const pattern = allowAlpha ? /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/ : /^#[0-9a-fA-F]{6}$/; - return pattern.test(withHash) ? withHash : fallback; - }; - - const toIntOrFalse = (raw: string): number | false => { - const n = Number.parseInt(raw, 10); - return Number.isFinite(n) && n > 0 ? n : false; - }; - - const toSecondsOrFalse = (raw: string): number | false => { - const n = Number.parseInt(raw, 10); - return Number.isFinite(n) && n > 0 ? n : false; - }; - - const toInt = (raw: string, fallback: number): number => { - const n = Number.parseInt(raw, 10); - return Number.isFinite(n) ? n : fallback; - }; - - const toClampedInt = ( - raw: string, - fallback: number, - min: number, - max: number, - ): number => { - const n = Number.parseInt(raw, 10); - const value = Number.isFinite(n) ? n : fallback; - return Math.min(Math.max(value, min), max); - }; - const previewFrameStyle = createMemo(() => { const radius = toClampedInt( overlayBackgroundRadius(), @@ -877,42 +610,20 @@ const [activeSection, setActiveSection] = return `${previewStageStyle()} border-radius: ${radius}px;`; }); - const toFloat = (raw: string, fallback: number): number => { - const n = Number.parseFloat(raw); - return Number.isFinite(n) ? n : fallback; - }; - - const buildConfig = (selectedChannel: string): ChatConfig => ({ - ...DEFAULT_CHAT_CONFIG, - channel: selectedChannel, - youtubeChannel: youtubeChannel().trim().replace(/^@/, ""), - kickChannel: kickChannel().trim().replace(/^@/, ""), - size: toInt(size(), DEFAULT_CHAT_CONFIG.size), - font: toInt(font(), DEFAULT_CHAT_CONFIG.font), - lineHeight: toClampedInt(lineHeight(), DEFAULT_CHAT_CONFIG.lineHeight, 80, 200), - fontWeight: toClampedInt( - fontWeight(), - DEFAULT_CHAT_CONFIG.fontWeight, - 100, - 1000, - ), - nickFontWeight: toClampedInt( - nickFontWeight(), - DEFAULT_CHAT_CONFIG.nickFontWeight, - 100, - 1000, - ), + const formState = (): SetupFormState => ({ + youtubeChannel: youtubeChannel(), + kickChannel: kickChannel(), + size: size(), + font: font(), + lineHeight: lineHeight(), + fontWeight: fontWeight(), + nickFontWeight: nickFontWeight(), fontCustom: fontCustom(), - shadow: toIntOrFalse(shadow()), - stroke: toIntOrFalse(stroke()), - fade: toSecondsOrFalse(fade()), + shadow: shadow(), + stroke: stroke(), + fade: fade(), animation: animation(), - messageSpeed: toClampedInt( - messageSpeed(), - DEFAULT_CHAT_CONFIG.messageSpeed, - MIN_MESSAGE_SPEED, - MAX_MESSAGE_SPEED, - ), + messageSpeed: messageSpeed(), showHomies: showHomies(), show7tvBadges: show7tvBadges(), showFfzBadges: showFfzBadges(), @@ -926,13 +637,13 @@ const [activeSection, setActiveSection] = bots: bots(), commands: commands(), hideAllBadges: hideAllBadges(), - emoteScale: toFloat(emoteScale(), DEFAULT_CHAT_CONFIG.emoteScale), + emoteScale: emoteScale(), showGifs: showGifs(), - gifScale: toFloat(gifScale(), DEFAULT_CHAT_CONFIG.gifScale), - botNames: normalizeBotNames(botNames().join(",")), - kickBotNames: normalizeBotNames(kickBotNames().join(",")), - youtubeBotNames: normalizeBotNames(youtubeBotNames().join(",")), - singleChatter: normalizeBotNames(allowedChatters().join(",")), + gifScale: gifScale(), + botNames: botNames(), + kickBotNames: kickBotNames(), + youtubeBotNames: youtubeBotNames(), + allowedChatters: allowedChatters(), show7tvUnlisted: show7tvUnlisted(), smallCaps: smallCaps(), nlAfterName: nlAfterName(), @@ -940,38 +651,18 @@ const [activeSection, setActiveSection] = reverseLineOrder: reverseLineOrder(), horizontal: horizontal(), platformMarker: platformMarker(), - ffzBotMixCustom: true, ffzBotMixBroadcaster: ffzBotMixBroadcaster(), ffzBotMixModerator: ffzBotMixModerator(), ffzBotMixVip: ffzBotMixVip(), - overlayBackgroundColor: normalizeHexColor( - overlayBackgroundColor(), - DEFAULT_CHAT_CONFIG.overlayBackgroundColor, - ), - overlayBackgroundOpacity: toInt( - overlayBackgroundOpacity(), - DEFAULT_CHAT_CONFIG.overlayBackgroundOpacity, - ), - overlayBackgroundRadius: toInt( - overlayBackgroundRadius(), - DEFAULT_CHAT_CONFIG.overlayBackgroundRadius, - ), - overlayPadding: toInt( - overlayPadding(), - DEFAULT_CHAT_CONFIG.overlayPadding, - ), - overlayBorderWidth: toInt( - overlayBorderWidth(), - DEFAULT_CHAT_CONFIG.overlayBorderWidth, - ), - overlayBorderColor: normalizeHexColor( - overlayBorderColor(), - DEFAULT_CHAT_CONFIG.overlayBorderColor, - true, - ), + overlayBackgroundColor: overlayBackgroundColor(), + overlayBackgroundOpacity: overlayBackgroundOpacity(), + overlayBackgroundRadius: overlayBackgroundRadius(), + overlayPadding: overlayPadding(), + overlayBorderWidth: overlayBorderWidth(), + overlayBorderColor: overlayBorderColor(), highlightTwitchEvents: highlightTwitchEvents(), - eventColorOpacity: toInt(eventColorOpacity(), DEFAULT_CHAT_CONFIG.eventColorOpacity), - ...eventColors(), + eventColorOpacity: eventColorOpacity(), + eventColors: eventColors(), twitchEventBold: twitchEventBold(), twitchEventItalic: twitchEventItalic(), showHighlightedMessages: showHighlightedMessages(), @@ -979,10 +670,9 @@ const [activeSection, setActiveSection] = showGigantifiedEmotes: showGigantifiedEmotes(), showPredictions: showPredictions(), linkMode: linkMode(), - linkColor: normalizeHexColor(linkColor(), DEFAULT_CHAT_CONFIG.linkColor), - usersColor: usersColorEnabled() - ? normalizeHexColor(usersColor(), "#ffffff") - : "", + linkColor: linkColor(), + usersColorEnabled: usersColorEnabled(), + usersColor: usersColor(), hideLinkRewards: hideLinkRewards(), rteProxy: rteProxy(), rteAzureTts: rteAzureTts(), @@ -991,6 +681,13 @@ const [activeSection, setActiveSection] = rteCustomCosmetics: rteCustomCosmetics(), }); + /** + * The preview, the persisted config and the exported link all project the same + * form; only the channel differs between them. + */ + const buildConfig = (selectedChannel: string) => + buildSetupConfig(formState(), selectedChannel); + let canPersistSetupConfig = false; onMount(() => { const savedConfig = readStoredSetupValue(SETUP_STORAGE_KEYS.config); @@ -1009,24 +706,6 @@ const [activeSection, setActiveSection] = } }); - const buildChatUrl = ( - cfg: ChatConfig, - extraParams?: Record, - options?: { includeMessageSpeed?: boolean }, - ) => { - const params = chatConfigToSearchParams(cfg); - if (options?.includeMessageSpeed === false) { - params.delete("ms"); - } - if (extraParams) { - Object.entries(extraParams).forEach(([key, value]) => - params.set(key, value), - ); - } - const query = params.toString(); - return `${getAppBaseUrl()}/chat/${query ? `?${query}` : ""}`; - }; - const hasTwitchChannel = createMemo(() => Boolean(channel().trim())); const hasYouTubeChannel = createMemo(() => Boolean(youtubeChannel().trim())); const hasKickChannel = createMemo(() => Boolean(kickChannel().trim())); @@ -1036,44 +715,6 @@ const [activeSection, setActiveSection] = const hasPreviewChannel = createMemo( () => hasTwitchChannel() || hasYouTubeChannel() || hasKickChannel(), ); - const getPreviewSelectorThumbStyle = ( - selector: HTMLDivElement | undefined, - option: HTMLButtonElement | undefined, - ): JSX.CSSProperties => { - if (!selector || !option) return { opacity: "0" }; - - const selectorRect = selector.getBoundingClientRect(); - const optionRect = option.getBoundingClientRect(); - return { - height: `${optionRect.height}px`, - opacity: "1", - transform: `translateY(${optionRect.top - selectorRect.top}px)`, - }; - }; - const updatePreviewSelectorThumbs = () => { - setPreviewModeThumbStyle( - getPreviewSelectorThumbStyle( - previewModeSelectorRef, - previewMode() === "live" ? previewLiveOptionRef : previewDemoOptionRef, - ), - ); - setPreviewDemoThumbStyle( - isExternalOnly() - ? { opacity: "0" } - : getPreviewSelectorThumbStyle( - previewDemoSelectorRef, - previewDemoKind() === "pasta" - ? previewPastaOptionRef - : previewEmoteOptionRef, - ), - ); - }; - createEffect(updatePreviewSelectorThumbs); - onMount(() => { - const observer = new ResizeObserver(updatePreviewSelectorThumbs); - if (previewControlsRef) observer.observe(previewControlsRef); - onCleanup(() => observer.disconnect()); - }); const previewChannel = createMemo(() => channel().trim() || (hasYouTubeChannel() || hasKickChannel() ? "" : "chatyxpreview"), ); @@ -1099,7 +740,6 @@ const [activeSection, setActiveSection] = ? t("setup.speedStopped") : `${messageIntervalMs()} ${t("setup.milliseconds")}`, ); - const ffzBotBadgePreviewUrl = getPublicAssetUrl("img/ffz-bot-badge.png"); const requestedBotProfiles = new Set(); const requestedKickBotProfiles = new Set(); @@ -1199,7 +839,7 @@ const [activeSection, setActiveSection] = requestedBotProfiles.add(login); } - void loadBotProfiles(missing).then((profiles) => { + void loadTwitchBotProfiles(missing).then((profiles) => { if (profiles.length === 0) return; setBotProfiles((current) => { @@ -1270,7 +910,7 @@ const [activeSection, setActiveSection] = } setGeneratedUrl( - buildChatUrl(buildConfig(currentChannel), undefined, { + buildOverlayUrl(buildConfig(currentChannel), undefined, { includeMessageSpeed: false, }), ); @@ -1281,15 +921,9 @@ const [activeSection, setActiveSection] = const mode = previewMode(); const demoKind = previewDemoKind(); const sessionKey = getChatPreviewSessionKey(cfg, mode, demoKind); - if (sessionKey === activePreviewSessionKey) return; - activePreviewSessionKey = sessionKey; - if (previewNavigationTimer !== undefined) { - window.clearTimeout(previewNavigationTimer); - } - previewNavigationTimer = window.setTimeout(() => { - previewNavigationTimer = undefined; - const nextPreviewUrl = buildChatUrl( + previewSync.scheduleNavigation(sessionKey, () => + buildOverlayUrl( cfg, mode === "demo" ? { @@ -1299,28 +933,19 @@ const [activeSection, setActiveSection] = : { preview: "false", }, - ); - - // Set via ref to avoid about:blank flash — just swap src directly - if (iframeRef) { - iframeRef.src = nextPreviewUrl; - } else { - setPreviewUrl(nextPreviewUrl); - } - }, 180); + ), + ); }); onCleanup(() => { - if (previewNavigationTimer !== undefined) { - window.clearTimeout(previewNavigationTimer); - } + previewSync.dispose(); if (copyResetTimer !== undefined) { window.clearTimeout(copyResetTimer); } }); createEffect(() => { - postPreviewConfig(previewConfig()); + previewSync.postConfig(previewConfig()); }); const copyToClipboard = async () => { @@ -1373,8 +998,7 @@ const [activeSection, setActiveSection] = }); const loadLocalFonts = async () => { - const queryLocalFonts = (window as LocalFontWindow).queryLocalFonts; - if (!localFontBrowser() || typeof queryLocalFonts !== "function") { + if (!localFontBrowser()) { setLocalFontStatus({ kind: "unsupported" }); return; } @@ -1382,295 +1006,63 @@ const [activeSection, setActiveSection] = setIsLoadingLocalFonts(true); setLocalFontStatus({ kind: "loading" }); - try { - const fonts = normalizeLocalFonts(await queryLocalFonts()); - setLocalFonts(fonts); - setLocalFontStatus( - fonts.length > 0 - ? { kind: "found", count: fonts.length } - : { kind: "empty" }, - ); - } catch { + const result = await loadLocalFontOptions(); + if (result.kind === "found") { + setLocalFonts(result.fonts); + setLocalFontStatus({ kind: "found", count: result.fonts.length }); + } else if (result.kind === "empty") { + setLocalFontStatus({ kind: "empty" }); + } else if (result.kind === "unsupported") { + setLocalFontStatus({ kind: "unsupported" }); + } else { setLocalFontStatus({ kind: "error" }); - } finally { - setIsLoadingLocalFonts(false); } + + setIsLoadingLocalFonts(false); }; - const appearanceRows: ControlRow[] = [ - { - label: () => t("setup.messageSize"), - control: (labelId) => ( - setSize(e.currentTarget.value)} - > - - - - - ), - }, - { - label: () => t("setup.font"), - control: (labelId) => ( - setFont(e.currentTarget.value)} - > - - - - - - - - - - - - - - - - ), - }, - { - label: () => t("setup.customFontName"), - hint: () => t("setup.customFontHint"), - control: (labelId) => ( -
- setFontCustom(e.currentTarget.value)} - placeholder={t("setup.customFontPlaceholder")} - disabled={font() !== "0"} - class={cn(font() !== "0" && "opacity-50")} - /> - -
- - { - const selectedFont = e.currentTarget.value; - if (selectedFont) setFontCustom(selectedFont); - }} - disabled={font() !== "0" || localFonts().length === 0} - class={cn( - !(font() === "0" && localFonts().length > 0) && "opacity-50", - )} - > - - - {(localFont) => ( - - )} - - -
-
-
{localFontStatusText()}
-
- ), - }, - { - label: () => t("setup.lineHeight"), - hint: () => t("setup.lineHeightHint"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.textWeight"), - hint: () => t("setup.textWeightHint"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.nicknameWeight"), - hint: () => t("setup.nicknameWeightHint"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.emoteSize"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.gifSize"), - hint: () => t("setup.gifSizeHint"), - control: (_labelId) => ( - - ), - }, - ]; + const appearanceRows = createAppearanceRows({ + size, + setSize, + font, + setFont, + fontCustom, + setFontCustom, + localFontBrowser, + localFonts, + localFontStatusText, + isLoadingLocalFonts, + loadLocalFonts, + lineHeight, + setLineHeight, + fontWeight, + setFontWeight, + nickFontWeight, + setNickFontWeight, + emoteScale, + setEmoteScale, + gifScale, + setGifScale, + }); - const stylingRows: ControlRow[] = [ - { - label: () => t("setup.textShadow"), - control: (labelId) => ( - setShadow(e.currentTarget.value)} - > - - - - - - ), - }, - { - label: () => t("setup.textStroke"), - control: (labelId) => ( - setStroke(e.currentTarget.value)} - > - - - - - - - ), - }, - { - label: () => t("setup.hideMessagesAfter"), - hint: () => t("setup.hideMessagesAfterHint"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.messageBackground"), - control: (_labelId) => ( - { - setOverlayBackgroundColor(color); - setOverlayBackgroundOpacity(String(opacity)); - }} - /> - ), - }, - { - label: () => t("setup.backgroundRadius"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.padding"), - hint: () => t("setup.paddingHint"), - control: (_labelId) => ( - - ), - }, - { - label: () => t("setup.borderThickness"), - control: (_labelId) => ( - - ), - }, - ]; + const stylingRows = createStylingRows({ + shadow, + setShadow, + stroke, + setStroke, + fade, + setFade, + overlayBackgroundColor, + setOverlayBackgroundColor, + overlayBackgroundOpacity, + setOverlayBackgroundOpacity, + overlayBackgroundRadius, + setOverlayBackgroundRadius, + overlayPadding, + setOverlayPadding, + overlayBorderWidth, + setOverlayBorderWidth, + }); const linkColorRow: ControlRow = { label: () => t("setup.linkColor"), @@ -1707,160 +1099,60 @@ const [activeSection, setActiveSection] = ), }; - const behaviorRows: ControlRow[] = [ - { - label: () => t("setup.messageAnimation"), - hint: () => t("setup.messageAnimationHint"), - control: (labelId) => ( - - setAnimation(event.currentTarget.value as ChatAnimationMode) - } - > - - - - - - ), - }, - { - label: () => t("setup.messageLinks"), - control: (labelId) => ( - - setLinkMode(event.currentTarget.value as LinkDisplayMode) - } - > - - - - - ), - }, - ]; - - const behaviorToggles: ToggleRow[] = [ - { - label: () => t("setup.emphasizeEventText"), - checked: twitchEventBold, - onChange: setTwitchEventBold, - hint: () => t("setup.emphasizeEventTextHint"), - }, - { - label: () => t("setup.italicEvents"), - checked: twitchEventItalic, - onChange: setTwitchEventItalic, - }, - { - label: () => t("setup.loadRecentMessages"), - checked: recentMessages, - onChange: setRecentMessages, - hint: () => t("setup.loadRecentMessagesHint"), - }, - { - label: () => t("setup.uppercaseNicknames"), - checked: smallCaps, - onChange: setSmallCaps, - }, - { - label: () => t("setup.lineBreakAfterNickname"), - checked: nlAfterName, - onChange: setNlAfterName, - }, - { label: () => t("setup.hideNicknames"), checked: hideNames, onChange: setHideNames }, - { - label: () => t("setup.reverseMessageOrder"), - checked: reverseLineOrder, - onChange: setReverseLineOrder, - }, - { - label: () => t("setup.horizontalMessageFeed"), - checked: horizontal, - onChange: setHorizontal, - }, - ]; + const behaviorRows = createBehaviorRows({ animation, setAnimation, linkMode, setLinkMode }); + + const behaviorToggles = createBehaviorToggles({ + twitchEventBold, + setTwitchEventBold, + twitchEventItalic, + setTwitchEventItalic, + recentMessages, + setRecentMessages, + smallCaps, + setSmallCaps, + nlAfterName, + setNlAfterName, + hideNames, + setHideNames, + reverseLineOrder, + setReverseLineOrder, + horizontal, + setHorizontal, + }); - const contentToggles: ToggleRow[] = [ - { - label: () => t("setup.showHighlightedMessages"), - checked: showHighlightedMessages, - onChange: setShowHighlightedMessages, - }, - { - label: () => t("setup.showPointRewards"), - checked: showChannelPointRewards, - onChange: setShowChannelPointRewards, - }, - { - label: () => t("setup.hideLinkRewards"), - checked: hideLinkRewards, - onChange: setHideLinkRewards, - hint: () => t("setup.hideLinkRewardsHint"), - }, - { - label: () => t("setup.showGigantifiedEmotes"), - checked: showGigantifiedEmotes, - onChange: setShowGigantifiedEmotes, - }, - { - label: () => t("setup.showGifs"), - checked: showGifs, - onChange: setShowGifs, - hint: () => t("setup.showGifsHint"), - }, - { - label: () => t("setup.showPredictions"), - checked: showPredictions, - onChange: setShowPredictions, - hint: () => t("setup.showPredictionsHint"), - }, - { - label: () => t("setup.showCommands"), - checked: commands, - onChange: setCommands, - }, - { - label: () => t("setup.showUnlistedEmotes"), - checked: show7tvUnlisted, - onChange: setShow7tvUnlisted, - }, - ]; + const contentToggles = createContentToggles({ + showHighlightedMessages, + setShowHighlightedMessages, + showChannelPointRewards, + setShowChannelPointRewards, + hideLinkRewards, + setHideLinkRewards, + showGigantifiedEmotes, + setShowGigantifiedEmotes, + showGifs, + setShowGifs, + showPredictions, + setShowPredictions, + commands, + setCommands, + show7tvUnlisted, + setShow7tvUnlisted, + }); - const ttsToggles: ToggleRow[] = [ - { - label: () => t("setup.chatIsTts"), - checked: rteChatIsTts, - onChange: setRteChatIsTts, - hint: () => t("setup.chatIsTtsHint"), - }, - { - label: () => t("setup.azureTts"), - checked: rteAzureTts, - onChange: setRteAzureTts, - hint: () => t("setup.azureTtsHint"), - }, - ]; + const ttsToggles = createTtsToggles({ + rteChatIsTts, + setRteChatIsTts, + rteAzureTts, + setRteAzureTts, + }); - const rteToggles: ToggleRow[] = [ - { - label: () => t("setup.rteProxy"), - checked: rteProxy, - onChange: setRteProxy, - hint: () => t("setup.rteProxyHint"), - }, - { - label: () => t("setup.rteCosmetics"), - checked: rteCustomCosmetics, - onChange: setRteCustomCosmetics, - hint: () => t("setup.rteCosmeticsHint"), - }, - ]; + const rteToggles = createRteToggles({ + rteProxy, + setRteProxy, + rteCustomCosmetics, + setRteCustomCosmetics, + }); const roleBadgeMergeOptions = [ { @@ -1883,61 +1175,6 @@ const [activeSection, setActiveSection] = }, ]; - const ffzBadgeMergeBlock = ( -
-
-
- {t("setup.botBadgeNearRole")} -
-
- {t("setup.botBadgeNearRoleHint")} -
-
-
- - {(option) => { - const active = () => option.checked(); - const disabled = () => !showFfzBadges() || hideAllBadges(); - return ( - - ); - }} - -
-
- ); const badgeProviderRows: Array<{ toggle: ToggleRow; extra?: JSX.Element; @@ -1989,7 +1226,12 @@ const [activeSection, setActiveSection] = onChange: setShowFfzBadges, disabled: hideAllBadges, }, - extra: ffzBadgeMergeBlock, + extra: ( + + ), }, { toggle: { @@ -2026,64 +1268,6 @@ const [activeSection, setActiveSection] = }, ]; - const renderUserChip = ( - login: string, - remove: (login: string) => void, - ariaLabel: () => string, - profiles: () => Record = botProfiles, - ) => { - const profile = () => profiles()[login]; - const displayName = () => profile()?.displayName || login; - const avatarUrl = () => profile()?.avatarUrl || ""; - - return ( -
{ - if (event.key === "Backspace" || event.key === "Delete") { - event.preventDefault(); - remove(login); - } - }} - > - - {botFallbackName(login)} - - } - > - - - - - {displayName()} - - - - @{login} - - - - -
- ); - }; - const chipFieldClass = "flex min-h-[72px] w-full flex-wrap content-start items-center gap-1.5 rounded-lg border border-input bg-background p-2"; @@ -2107,7 +1291,7 @@ const [activeSection, setActiveSection] = target="_blank" rel="noreferrer" > -
@@ -2278,7 +1464,7 @@ const [activeSection, setActiveSection] = id="setup-section-appearance" title={t("setup.appearanceTitle")} description={t("setup.appearanceDescription")} - icon="hgi-text" + icon={TextIcon} hidden={activeSection() !== "appearance"} >

{t("setup.messageFont")}

@@ -2289,7 +1475,7 @@ const [activeSection, setActiveSection] = id="setup-section-styling" title={t("setup.stylingTitle")} description={t("setup.stylingDescription")} - icon="hgi-colors" + icon={ColorsIcon} hidden={activeSection() !== "styling"} >

{t("setup.textReadability")}

@@ -2336,7 +1522,7 @@ const [activeSection, setActiveSection] = id="setup-section-behavior" title={t("setup.behaviorTitle")} description={t("setup.behaviorDescription")} - icon="hgi-arrow-up-right-stack" + icon={ArrowUpRightStackIcon} hidden={activeSection() !== "behavior"} >

{t("setup.animationAndLinks")}

@@ -2348,7 +1534,7 @@ const [activeSection, setActiveSection] = id="setup-section-content" title={t("setup.contentTitle")} description={t("setup.contentDescription")} - icon="hgi-dashboard-square-03" + icon={DashboardSquare03Icon} hidden={activeSection() !== "content"} >

{t("setup.messagesAndEvents")}

@@ -2376,7 +1562,7 @@ const [activeSection, setActiveSection] = id="setup-section-bots" title={t("setup.botsTitle")} description={t("setup.botsDescription")} - icon="hgi-bot-message-square" + icon={BotMessageSquareIcon} hidden={activeSection() !== "bots"} >
@@ -2392,28 +1578,25 @@ const [activeSection, setActiveSection] =
- {(login) => - renderUserChip( - login, - removeBotName, - () => t("setup.removeBot"), - ) - } + {(login) => ( + t("setup.removeBot")} + /> + )} - - setBotInput(event.currentTarget.value) - } + onInput={setBotInput} onKeyDown={handleBotInputKeyDown} - onBlur={() => { + onCommit={() => { addBotNames(botInput()); setBotInput(""); }} - placeholder={t("setup.nicknamePlaceholder")} - class="h-[34px] min-w-[150px] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground" + label={t("setup.addBot")} + placeholder={t("setup.nicknamePlaceholder")} />
@@ -2426,29 +1609,25 @@ const [activeSection, setActiveSection] =
- {(login) => - renderUserChip( - login, - removeYouTubeBotName, - () => t("setup.removeYouTubeBot"), - () => ({}), - ) - } + {(login) => ( + t("setup.removeYouTubeBot")} + /> + )} - - setYoutubeBotInput(event.currentTarget.value) - } + onInput={setYoutubeBotInput} onKeyDown={handleYouTubeBotInputKeyDown} - onBlur={() => { + onCommit={() => { addYouTubeBotNames(youtubeBotInput()); setYoutubeBotInput(""); }} + label={t("setup.addYouTubeBot")} placeholder={t("setup.nicknamePlaceholder")} - class="h-[34px] min-w-[150px] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground" />
@@ -2461,29 +1640,25 @@ const [activeSection, setActiveSection] =
- {(login) => - renderUserChip( - login, - removeKickBotName, - () => t("setup.removeKickBot"), - kickBotProfiles, - ) - } + {(login) => ( + t("setup.removeKickBot")} + /> + )} - - setKickBotInput(event.currentTarget.value) - } + onInput={setKickBotInput} onKeyDown={handleKickBotInputKeyDown} - onBlur={() => { + onCommit={() => { addKickBotNames(kickBotInput()); setKickBotInput(""); }} + label={t("setup.addKickBot")} placeholder={t("setup.nicknamePlaceholder")} - class="h-[34px] min-w-[150px] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground" />
@@ -2499,28 +1674,25 @@ const [activeSection, setActiveSection] =
- {(login) => - renderUserChip( - login, - removeAllowedChatter, - () => t("setup.removeViewer"), - ) - } + {(login) => ( + t("setup.removeViewer")} + /> + )} - - setAllowedChatterInput(event.currentTarget.value) - } + onInput={setAllowedChatterInput} onKeyDown={handleAllowedChatterInputKeyDown} - onBlur={() => { + onCommit={() => { addAllowedChatters(allowedChatterInput()); setAllowedChatterInput(""); }} - placeholder={t("setup.nicknamePlaceholder")} - class="h-[34px] min-w-[150px] flex-1 border-0 bg-transparent px-1 text-sm text-foreground outline-none placeholder:text-muted-foreground" + label={t("setup.addViewer")} + placeholder={t("setup.nicknamePlaceholder")} />
@@ -2530,7 +1702,7 @@ const [activeSection, setActiveSection] = id="setup-section-tts" title={t("setup.ttsTitle")} description={t("setup.ttsDescription")} - icon="hgi-voice-comment" + icon={VoiceCommentIcon} hidden={activeSection() !== "tts"} > @@ -2541,7 +1713,7 @@ const [activeSection, setActiveSection] = id="setup-section-rte" title={t("setup.rteTitle")} description={t("setup.rteDescription")} - icon="hgi-blockchain-05" + icon={Blockchain05Icon} hidden={activeSection() !== "rte"} > @@ -2558,199 +1730,23 @@ const [activeSection, setActiveSection] = class="setup-preview-section min-[1100px]:flex min-[1100px]:min-h-0 min-[1100px]:flex-1 min-[1100px]:flex-col" >
-
-
(previewControlsRef = element)} - > -
-
{t("setup.demoBackgroundColor")}
-
- - - - -
- -
- setStageColor(value.color)} - /> -
-
-
-
-
{t("setup.previewSettings")}
-
-
-
{t("setup.chatMode")}
-
(previewModeSelectorRef = element)} - role="radiogroup" - aria-label={t("setup.previewChatMode")} - > - -
- -
-
{t("setup.demoScenario")}
-
(previewDemoSelectorRef = element)} - role="radiogroup" - aria-label={t("setup.demoScenario")} - > - -
- -
-
-
-
- - -
-
- - {t("setup.speed")} - - { - const next = values[0]; - if (next !== undefined) - setMessageSpeed(String(next)); - }} - class="min-w-0 flex-1 px-1" - /> -
- {messageSpeedLabel()} -
-
- -
-
+
postPreviewConfig()} + onLoad={() => previewSync.postConfig(previewConfig())} class="pointer-events-none block h-full w-full border-0 bg-transparent" title={t("setup.chatPreview")} scrolling="no" diff --git a/src/services/badges/badgeService.ts b/src/services/badges/badgeService.ts index 99be64b..603a1f1 100644 --- a/src/services/badges/badgeService.ts +++ b/src/services/badges/badgeService.ts @@ -324,6 +324,36 @@ class BadgeService { seventvBadges: {}, }; + /** + * Snapshot of the state a fresh service starts from. `reset()` restores this + * rather than repeating the fallback literal above, which would be free to + * drift from it. + */ + private readonly initialBadgeData = structuredClone(this.badgeData); + + /** + * Bumped by `reset()`. A load captures it on entry and skips its commit when + * the store moved on, so a badge response that arrives after the owning + * runtime was destroyed cannot repopulate the next runtime's store. + */ + private generation = 0; + + /** + * Drops the channel-scoped badge state, so the next runtime loads badges for + * its own channel instead of inheriting the previous one's. + */ + reset(): void { + this.generation += 1; + this.currentChannelId = ""; + this.thirdPartyBadgesReady = null; + this.thirdPartyBadgeIndex = { byUserId: new Map(), byUsername: new Map() }; + this.badgeData = structuredClone(this.initialBadgeData); + } + + private isCurrentGeneration(generation: number): boolean { + return generation === this.generation; + } + async loadBadges(channel: string, channelId: string): Promise { this.currentChannelId = channelId; @@ -345,15 +375,18 @@ class BadgeService { channelId: string, channelName: string, ): Promise { + const generation = this.generation; try { // Глобальные баджи с автоматическим fallback const globalResponse = await fetchWithFallback( `${TWITCH_CONFIG.API_BASE_URL}/badges/global`, FALLBACK_APIS.badges_global, ); + if (!this.isCurrentGeneration(generation)) return; if (globalResponse.ok) { const globalData = await globalResponse.json(); + if (!this.isCurrentGeneration(generation)) return; const badges = Array.isArray(globalData) ? globalData : globalData.data; if (Array.isArray(badges)) { @@ -378,9 +411,11 @@ class BadgeService { `${TWITCH_CONFIG.API_BASE_URL}/badges/channel?broadcaster_id=${encodeURIComponent(channelId)}`, FALLBACK_APIS.badges_channel(channelName), ); + if (!this.isCurrentGeneration(generation)) return; if (channelResponse.ok) { const channelData = await channelResponse.json(); + if (!this.isCurrentGeneration(generation)) return; const badges = Array.isArray(channelData) ? channelData : channelData.data; @@ -407,8 +442,10 @@ class BadgeService { `https://api.frankerfacez.com/v1/_room/id/${encodeURIComponent(channelId)}`, { route: "rte" }, ); + if (!this.isCurrentGeneration(generation)) return; if (ffzRoomResponse.ok) { const ffzRoomData = await ffzRoomResponse.json(); + if (!this.isCurrentGeneration(generation)) return; if (ffzRoomData.room?.moderator_badge) { this.badgeData.badges["moderator:1"] = @@ -433,6 +470,7 @@ class BadgeService { } private async loadThirdPartyBadges(): Promise { + const generation = this.generation; // Загружаем все баджи параллельно для оптимизации const results = await Promise.allSettled([ // FFZ badges (for bot/other global user badges) @@ -465,6 +503,10 @@ class BadgeService { ), ]); + // Everything below commits the fetched payload into the store, so it must + // not run for a store that was reset while the requests were in flight. + if (!this.isCurrentGeneration(generation)) return; + const ffzBadgeData = results[0].status === "fulfilled" ? results[0].value : null; if (ffzBadgeData?.badges) { @@ -536,6 +578,7 @@ class BadgeService { userId: string, includeTwitchSender = false, ): Promise { + const generation = this.generation; // Проверяем кэш if (this.badgeData.userBadges[username]) { return this.badgeData.userBadges[username]; @@ -553,6 +596,7 @@ class BadgeService { this.currentChannelId, userId, ); + if (!this.isCurrentGeneration(generation)) return []; if (gqlSender) { this.applyGqlBadgesToCache(gqlSender.displayBadges); } @@ -625,7 +669,9 @@ class BadgeService { ); } - this.badgeData.userBadges[username] = userBadges; + if (this.isCurrentGeneration(generation)) { + this.badgeData.userBadges[username] = userBadges; + } return userBadges; } @@ -640,7 +686,9 @@ class BadgeService { } private async loadTwitchGqlBadgeSets(channelId: string): Promise { + const generation = this.generation; const badges = await twitchGqlService.loadBadgeSets(channelId); + if (!this.isCurrentGeneration(generation)) return; this.applyGqlBadgesToCache(badges); } @@ -687,6 +735,7 @@ class BadgeService { } async addUserSevenTVBadge(username: string, badgeId: string): Promise { + const generation = this.generation; if (!this.badgeData.userBadges[username]) { this.badgeData.userBadges[username] = []; } @@ -699,6 +748,7 @@ class BadgeService { // Попробуем загрузить бадж через API await this.loadMissingBadge(badgeId); + if (!this.isCurrentGeneration(generation)) return; // Проверяем еще раз после загрузки const newBadgeUrl = this.badgeData.badges[`7tv:${badgeId}`]; @@ -743,6 +793,7 @@ class BadgeService { } private async loadMissingBadge(badgeId: string): Promise { + const generation = this.generation; try { const response = await networkClient.request("https://7tv.io/v3/gql", { route: "rte", @@ -772,6 +823,7 @@ class BadgeService { if (response.ok) { const data = await response.json(); + if (!this.isCurrentGeneration(generation)) return; if (data.data?.badge) { const badge = data.data.badge; diff --git a/src/services/chat/assets/emoteService.ts b/src/services/chat/assets/emoteService.ts index 7c74a3b..f84797d 100644 --- a/src/services/chat/assets/emoteService.ts +++ b/src/services/chat/assets/emoteService.ts @@ -48,6 +48,36 @@ class EmoteService { private show7tvUnlisted = true; private loaded7tvVisibility: boolean | null = null; + /** + * Bumped whenever the cache is dropped. An async load captures the value on + * entry and skips its commit when the store moved on, so a response that + * arrives after `reset()` (or after the 7TV visibility changed) cannot + * repopulate a store the next runtime owns. + */ + private generation = 0; + + /** + * Drops every cached emote and the load bookkeeping, so a runtime that starts + * after this one refetches instead of serving the previous channel's emotes. + * The global caches are deliberately included: keeping them is exactly what + * lets a stale channel survive a restart. + */ + reset(): void { + this.generation += 1; + this.emoteData = { emotes: {}, channelEmotes: {}, personalEmotes: {} }; + this.currentChannelId = ""; + this.currentChannelName = ""; + this.globalEmotesLoaded = false; + this.globalEmotesPromise = null; + this.channelLoadPromises.clear(); + this.show7tvUnlisted = true; + this.loaded7tvVisibility = null; + } + + private isCurrentGeneration(generation: number): boolean { + return generation === this.generation; + } + async loadEmotes( channelId: string, channelName: string, @@ -173,6 +203,7 @@ class EmoteService { private async loadGlobalEmotes(): Promise { if (this.globalEmotesLoaded) return; + const generation = this.generation; await Promise.all([ this.load7TVGlobalEmotes(), @@ -180,10 +211,14 @@ class EmoteService { this.loadBTTVGlobalEmotes(), ]); + if (!this.isCurrentGeneration(generation)) return; this.globalEmotesLoaded = true; } private clearSevenTVEmotes(): void { + // The 7TV visibility filter changed, so responses already in flight carry + // the wrong filter and must not be committed. + this.generation += 1; for (const [name, emote] of Object.entries(this.emoteData.emotes)) { if (emote.source === "7tv") { delete this.emoteData.emotes[name]; @@ -215,6 +250,7 @@ class EmoteService { channelId: string, emoteSetId?: string, ): Promise { + const generation = this.generation; try { // Канальные 7TV эмодзи - use specific set ID if provided to avoid race conditions if (emoteSetId) { @@ -222,8 +258,10 @@ class EmoteService { `https://7tv.io/v3/emote-sets/${emoteSetId}`, { route: "rte" }, ); + if (!this.isCurrentGeneration(generation)) return; if (setResponse.ok) { const setData = await setResponse.json(); + if (!this.isCurrentGeneration(generation)) return; if (setData.emotes) { setData.emotes.forEach((emoteWithMeta: any) => { @@ -252,8 +290,10 @@ class EmoteService { `https://7tv.io/v3/users/twitch/${channelId}`, { route: "rte" }, ); + if (!this.isCurrentGeneration(generation)) return; if (channelResponse.ok) { const channelData = await channelResponse.json(); + if (!this.isCurrentGeneration(generation)) return; if (channelData.emote_set && channelData.emote_set.emotes) { channelData.emote_set.emotes.forEach((emoteWithMeta: any) => { @@ -279,10 +319,13 @@ class EmoteService { } private async load7TVGlobalEmotes(): Promise { + const generation = this.generation; try { const globalResponse = await networkClient.request("https://7tv.io/v3/emote-sets/global", { route: "rte" }); + if (!this.isCurrentGeneration(generation)) return; if (!globalResponse.ok) return; const globalData = await globalResponse.json(); + if (!this.isCurrentGeneration(generation)) return; if (globalData.emotes) { globalData.emotes.forEach((emoteWithMeta: any) => { @@ -319,13 +362,16 @@ class EmoteService { channelId?: string, isGlobal = false, ): Promise { + const generation = this.generation; try { const response = await networkClient.request( "https://api.betterttv.net/3/cached/frankerfacez/" + endpoint, { route: "rte" }, ); + if (!this.isCurrentGeneration(generation)) return; if (!response.ok) return; const data = await response.json(); + if (!this.isCurrentGeneration(generation)) return; data.forEach((emote: any) => { const imageUrl = @@ -381,13 +427,16 @@ class EmoteService { channelId?: string, isGlobal = false, ): Promise { + const generation = this.generation; try { const response = await networkClient.request( "https://api.betterttv.net/3/cached/" + endpoint, { route: "rte" }, ); + if (!this.isCurrentGeneration(generation)) return; if (!response.ok) return; let data = await response.json(); + if (!this.isCurrentGeneration(generation)) return; if (!Array.isArray(data)) { data = data.channelEmotes.concat(data.sharedEmotes); @@ -451,15 +500,18 @@ class EmoteService { } private async loadCheerEmotes(channelId: string): Promise { + const generation = this.generation; try { // Cheermotes доступны только через локальный API (требуется OAuth) const response = await fetchWithFallback( `${TWITCH_CONFIG.API_BASE_URL}/bits/cheermotes?broadcaster_id=${encodeURIComponent(channelId)}`, FALLBACK_APIS.cheermotes, // null - нет публичного API ); + if (!this.isCurrentGeneration(generation)) return; if (response.ok) { const data = await response.json(); + if (!this.isCurrentGeneration(generation)) return; if (data.data) { data.data.forEach((action: any) => { @@ -481,6 +533,7 @@ class EmoteService { } } catch (error) { log.error(LOG_CATEGORIES.EMOTES, "Failed to load cheer emotes", error); + if (!this.isCurrentGeneration(generation)) return; // Fallback: добавляем несколько базовых Cheer эмоутов для тестирования const fallbackEmotes = [ { diff --git a/src/services/chat/chatFeatureIntegration.ts b/src/services/chat/chatFeatureIntegration.ts index 2050c19..7fed0d3 100644 --- a/src/services/chat/chatFeatureIntegration.ts +++ b/src/services/chat/chatFeatureIntegration.ts @@ -13,7 +13,6 @@ import { chatterinoBadgeService } from "../badges/chatterinoBadgeService"; import { chatisBadgeService } from "../badges/chatisBadgeService"; import { sevenTVEventApi } from "./seven-tv/eventApi"; -import { layoutManager } from "../../utils/ui/layoutManager"; import type { SevenTVEventDispatch } from "./seven-tv/eventApi"; const SEVENTV_RETRY_DELAY_MS = 5 * 60 * 1000; @@ -43,10 +42,6 @@ export interface ChatFeatureIntegrationOptions { enableBits: boolean; - // Layout options - reverseLineOrder: boolean; - singleChatter?: string; - // 7TV EventAPI enable7TVEventAPI: boolean; } @@ -55,6 +50,12 @@ export class ChatFeatureIntegrationService { private options: ChatFeatureIntegrationOptions; private initialized: boolean = false; private sevenTvRetryTimer: ReturnType | null = null; + /** + * Bumped by `destroy()`. Initialization captures it on entry and stops after + * its awaits when the service was destroyed in the meantime, so a torn-down + * integration cannot mark itself initialized and block the next runtime. + */ + private generation = 0; constructor(options: Partial = {}) { this.options = { @@ -63,7 +64,6 @@ export class ChatFeatureIntegrationService { showChatterinoBadges: true, showChatisBadges: true, enableBits: true, - reverseLineOrder: false, enable7TVEventAPI: true, ...options, }; @@ -83,6 +83,7 @@ export class ChatFeatureIntegrationService { log.info(LOG_CATEGORIES.INTEGRATION, "Initializing Chat feature integration..."); + const generation = this.generation; try { await Promise.all([ this.options.showFFZAPBadges @@ -110,18 +111,15 @@ export class ChatFeatureIntegrationService { log.error(LOG_CATEGORIES.INTEGRATION, "Failed to load bits service", err), ); } + if (!this.isCurrentGeneration(generation)) return; if (this.options.enable7TVEventAPI) { await this.connect7TVEventAPI(channelId, onSevenTvEvent, { scheduleRetry: true, }); + if (!this.isCurrentGeneration(generation)) return; } - layoutManager.setOptions({ - reverseLineOrder: this.options.reverseLineOrder, - singleChatter: this.options.singleChatter, - }); - this.initialized = true; log.info( LOG_CATEGORIES.INTEGRATION, @@ -197,16 +195,16 @@ export class ChatFeatureIntegrationService { this.sevenTvRetryTimer = null; } + private isCurrentGeneration(generation: number): boolean { + return generation === this.generation; + } + setOptions(options: Partial): void { this.options = { ...this.options, ...options }; - - layoutManager.setOptions({ - reverseLineOrder: this.options.reverseLineOrder, - singleChatter: this.options.singleChatter, - }); } destroy(): void { + this.generation += 1; this.clearSevenTvRetryTimer(); sevenTVEventApi.disconnect(); this.initialized = false; diff --git a/src/services/chat/chatPresentationService.ts b/src/services/chat/chatPresentationService.ts index a9a892f..e9f5d19 100644 --- a/src/services/chat/chatPresentationService.ts +++ b/src/services/chat/chatPresentationService.ts @@ -6,24 +6,25 @@ import { badgeService, type Badge } from "../badges/badgeService"; import { SevenTVPaintService, type Paint } from "./seven-tv/paintService"; import { sevenTVEventApi } from "./seven-tv/eventApi"; import type { SevenTVEventApiService } from "./seven-tv/eventApi"; +import { sevenTVCosmeticsService } from "./seven-tv/cosmeticsService"; import { BotFilterService } from "../../utils/botFilter"; import { MessageFadeManager, DEFAULT_FADE_OPTIONS, -} from "../../utils/ui/fadeUtils"; -import type { FadeOptions } from "../../utils/ui/fadeUtils"; +} from "./runtime/messageFade"; +import type { FadeOptions } from "./runtime/messageFade"; import { LayoutManager, DEFAULT_LAYOUT_OPTIONS, -} from "../../utils/ui/layoutUtils"; -import type { LayoutOptions } from "../../utils/ui/layoutUtils"; +} from "./runtime/layoutManager"; +import type { LayoutOptions } from "./runtime/layoutManager"; import { DEFAULT_ANIMATION_OPTIONS, getMessageEntryAnimationDuration, hasMessageEntryAnimation, - injectAnimationStyles, -} from "../../utils/ui/animationUtils"; -import type { AnimationOptions } from "../../utils/ui/animationUtils"; +} from "../../config/chatAnimation"; +import { injectAnimationStyles } from "./runtime/animationStyles"; +import type { AnimationOptions } from "../../config/chatAnimation"; import { parseBotNames, type ChatConfig } from "../../config/chatUrlParams"; import { log, LOG_CATEGORIES } from "../../utils/logger"; @@ -635,11 +636,18 @@ export class ChatPresentationService { await this.eventApiService.disconnect(); } - // Clear fade timers + // Clear fade timers and the stylesheets both managers injected this.fadeManager.clear(); + this.fadeManager.cleanup(); + this.layoutManager?.cleanup(); this.clearPaintCache(); this.rteBadges.clear(); this.rtePaints.clear(); + // The 7TV cosmetics caches are keyed by channel and user and would otherwise + // outlive the runtime that filled them, as would the paint stylesheet the + // service generated from provider data. + sevenTVCosmeticsService.clearAllCaches(); + sevenTVCosmeticsService.disposeStylesheet(); this.initialized = false; log.info(LOG_CATEGORIES.INTEGRATION, "Cleanup complete"); diff --git a/src/services/chat/external/youtubeChatService.ts b/src/services/chat/external/youtubeChatService.ts deleted file mode 100644 index f5f11d0..0000000 --- a/src/services/chat/external/youtubeChatService.ts +++ /dev/null @@ -1,276 +0,0 @@ -import type { TwitchMessage } from "../twitch/twitchService"; - -type YouTubeAuthor = { - name?: string; - id?: string; - moderator?: boolean; - badges?: Array<{ - url?: string; - tooltip?: string; - }>; -}; - -type YouTubeRun = - | { text?: string } - | { emoji?: { image?: Array<{ url?: string; width?: number; height?: number }> } }; - -type YouTubeChatEvent = - | { - type: "message"; - id?: string; - message?: string; - runs?: YouTubeRun[]; - author?: YouTubeAuthor; - unix?: number; - } - | { - type: "superchat"; - id?: string; - purchase_amount?: string; - hasMessage?: boolean; - message?: string; - runs?: YouTubeRun[]; - author?: YouTubeAuthor; - unix?: number; - } - | { info: "deleted"; message?: string } - | { info: "banned"; externalChannelId?: string } - | { info: string }; - -type YouTubeChatCallbacks = { - onMessage: (message: TwitchMessage) => void; - onDelete: (messageId: string) => void; - onBan: (userId: string) => void; - onConnectionChange: (connected: boolean) => void; -}; - -const FALLBACK_COLORS = [ - "#ff0033", - "#ff4d8d", - "#d946ef", - "#a855f7", - "#f97316", -] as const; - -function normalizeHandle(value: string) { - return value.trim().replace(/^@/, ""); -} - -function normalizeWebSocketBase(value: string) { - const trimmed = value.trim().replace(/\/+$/, ""); - if (trimmed) return trimmed; - return "wss://ytwss.ruina.team"; -} - -function fallbackColor(seed: string) { - let hash = 0; - for (let index = 0; index < seed.length; index += 1) { - hash = (hash + seed.charCodeAt(index) * (index + 1)) % 997; - } - return FALLBACK_COLORS[hash % FALLBACK_COLORS.length]; -} - -function sanitizeMessageId(value: string) { - return value.replace(/[^\w-]/g, ""); -} - -function getAuthorLogin(author?: YouTubeAuthor) { - const raw = author?.name || "youtube"; - return raw.replace(/^@/, "").toLowerCase().trim() || "youtube"; -} - -function normalizeAuthorBadges(author?: YouTubeAuthor) { - return (author?.badges || []) - .map((badge) => { - const url = String(badge.url || ""); - if (!url) return null; - - return { - url, - title: String(badge.tooltip || "YouTube badge"), - }; - }) - .filter((badge): badge is { url: string; title: string } => badge !== null); -} - -function buildMessageFromRuns( - eventId: string, - fallbackMessage: string, - runs?: YouTubeRun[], -) { - const snapshot = new Map(); - - if (!Array.isArray(runs) || runs.length === 0) { - return { message: fallbackMessage, snapshot }; - } - - let emojiIndex = 0; - const parts = runs.map((run) => { - if ("text" in run && run.text) return run.text; - - if ("emoji" in run) { - const image = run.emoji?.image?.find((image) => image.url); - const url = image?.url || ""; - if (!url) return ""; - - const token = `yt_emoji_${sanitizeMessageId(eventId)}_${emojiIndex}`; - emojiIndex += 1; - snapshot.set(token, { - id: token, - name: token, - url, - source: "youtube", - zero_width: false, - width: image?.width, - height: image?.height, - }); - return token; - } - - return ""; - }); - - const message = parts.join("").trim() || fallbackMessage; - return { message, snapshot }; -} - -function eventToMessage(event: Extract) { - const author = event.author; - const username = getAuthorLogin(author); - const rawId = event.id || `${username}-${event.unix || Date.now()}`; - const id = `youtube-${sanitizeMessageId(rawId)}`; - const baseText = - event.type === "superchat" - ? [event.purchase_amount, event.hasMessage ? event.message : ""] - .filter(Boolean) - .join(" ") - : event.message || ""; - const { message, snapshot } = buildMessageFromRuns( - id, - baseText, - event.runs, - ); - - return { - id, - username, - displayName: (author?.name || username).replace(/^@/, ""), - message, - color: fallbackColor(author?.id || username), - badges: [], - emotes: {}, - userType: "", - isModerator: Boolean(author?.moderator), - isSubscriber: false, - timestamp: new Date(event.unix || Date.now()), - userId: author?.id || undefined, - platform: "youtube" as const, - platformBadges: normalizeAuthorBadges(author), - emoteSnapshot: snapshot, - } satisfies TwitchMessage; -} - -export class YouTubeChatService { - private socket: WebSocket | null = null; - private reconnectTimer: number | null = null; - private reconnectAttempts = 0; - private intentionallyDisconnected = false; - - connect( - channel: string, - wsBaseUrl: string, - callbacks: YouTubeChatCallbacks, - ) { - const normalizedChannel = normalizeHandle(channel); - if (!normalizedChannel || typeof window === "undefined") return; - - this.disconnect(); - this.intentionallyDisconnected = false; - - const baseUrl = normalizeWebSocketBase(wsBaseUrl); - const url = `${baseUrl}/c/${encodeURIComponent(normalizedChannel)}`; - this.open(url, callbacks); - } - - disconnect() { - this.intentionallyDisconnected = true; - if (this.reconnectTimer !== null) { - window.clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - if (this.socket) { - this.socket.close(); - this.socket = null; - } - } - - private open(url: string, callbacks: YouTubeChatCallbacks) { - const socket = new WebSocket(url); - this.socket = socket; - - socket.onopen = () => { - this.reconnectAttempts = 0; - callbacks.onConnectionChange(true); - }; - - socket.onmessage = (event) => { - const payload = JSON.parse(String(event.data)) as YouTubeChatEvent; - - if ("info" in payload) { - if ( - payload.info === "deleted" && - "message" in payload && - payload.message - ) { - callbacks.onDelete(`youtube-${sanitizeMessageId(payload.message)}`); - } else if ( - payload.info === "banned" && - "externalChannelId" in payload && - payload.externalChannelId - ) { - callbacks.onBan(payload.externalChannelId); - } - return; - } - - if (payload.type === "message" || payload.type === "superchat") { - callbacks.onMessage(eventToMessage(payload)); - } - }; - - socket.onclose = (event) => { - if (this.socket === socket) this.socket = null; - callbacks.onConnectionChange(false); - if (!this.intentionallyDisconnected && this.shouldReconnect(event)) { - this.scheduleReconnect(url, callbacks); - } - }; - - socket.onerror = () => { - socket.close(); - }; - } - - private scheduleReconnect(url: string, callbacks: YouTubeChatCallbacks) { - if (this.reconnectTimer !== null) return; - - const delay = Math.min(30000, 1000 * 2 ** this.reconnectAttempts); - this.reconnectAttempts += 1; - this.reconnectTimer = window.setTimeout(() => { - this.reconnectTimer = null; - if (!this.intentionallyDisconnected) { - this.open(url, callbacks); - } - }, delay); - } - - private shouldReconnect(event: CloseEvent) { - if (event.code === 1000) return false; - - const reason = event.reason.toLowerCase(); - if (reason.includes("could not find a live stream")) return false; - if (reason.includes("no available live chat")) return false; - - return true; - } -} diff --git a/src/services/chat/index.ts b/src/services/chat/index.ts index e2db46a..7534524 100644 --- a/src/services/chat/index.ts +++ b/src/services/chat/index.ts @@ -25,6 +25,5 @@ export type { TwitchGqlSender, } from "./twitch/twitchGqlService"; export { chatFeatureIntegration } from "./chatFeatureIntegration"; -export { YouTubeChatService } from "./external/youtubeChatService"; export { ExternalChatService } from "./external/externalChatService"; export type { ExternalChatEvent } from "./external/externalChatService"; diff --git a/src/services/chat/preview/configMessage.ts b/src/services/chat/preview/configMessage.ts index c471898..f11f86a 100644 --- a/src/services/chat/preview/configMessage.ts +++ b/src/services/chat/preview/configMessage.ts @@ -1,4 +1,4 @@ -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import type { PreviewDemoKind } from "./messages"; export const CHAT_PREVIEW_CONFIG_MESSAGE = "chatyx:preview-config"; diff --git a/src/services/chat/preview/styles.ts b/src/services/chat/preview/styles.ts index 707f1d5..f33e7dc 100644 --- a/src/services/chat/preview/styles.ts +++ b/src/services/chat/preview/styles.ts @@ -1,80 +1,21 @@ -import { - generateShadowStyles, - generateSizeStyles, - generateStrokeStyles, - generateVariantStyles, -} from "~/styles/chatStyles"; -import { - getMessageEntryAnimationDuration, - hasMessageEntryAnimation, - injectAnimationStyles, -} from "~/utils/ui/animationUtils"; -import type { ChatConfig } from "~/utils/chat"; - -const PREVIEW_STYLE_IDS = [ - "chat-size-styles", - "chat-shadow-styles", - "chat-stroke-styles", - "chat-variant-styles", - "chat-animations", - "chat-preview-layout-styles", -]; - -export function cleanupPreviewStyles() { - PREVIEW_STYLE_IDS.forEach((id) => document.getElementById(id)?.remove()); -} - +import type { ChatConfig } from "~/config/chatUrlParams"; +import { applyOverlayStyles, clearOverlayStyles } from "~/services/chat/runtime/overlayStyleManager"; +import { OVERLAY_ATTRIBUTES } from "~/styles/chatStyles"; + +/** + * Publishes the overlay styles for the embedded preview. + * + * The preview wants exactly what the live overlay wants, plus a marker that stops + * the container from scrolling on its own, so this delegates rather than + * duplicating the variable set. + */ export function injectPreviewStyles(config: ChatConfig) { cleanupPreviewStyles(); - - const sizeEl = document.createElement("style"); - sizeEl.id = "chat-size-styles"; - sizeEl.innerHTML = generateSizeStyles(config.size as 1 | 2 | 3, config.lineHeight); - document.head.appendChild(sizeEl); - - if (config.shadow) { - const el = document.createElement("style"); - el.id = "chat-shadow-styles"; - el.innerHTML = generateShadowStyles(config.shadow as 1 | 2 | 3); - document.head.appendChild(el); - } - - if (config.stroke) { - const el = document.createElement("style"); - el.id = "chat-stroke-styles"; - el.innerHTML = generateStrokeStyles(config.stroke as 1 | 2 | 3 | 4); - document.head.appendChild(el); - } - - const variantStyles = generateVariantStyles(config); - if (variantStyles) { - const el = document.createElement("style"); - el.id = "chat-variant-styles"; - el.innerHTML = variantStyles; - document.head.appendChild(el); - } - - if (hasMessageEntryAnimation(config.animation)) { - injectAnimationStyles({ - enabled: true, - duration: getMessageEntryAnimationDuration(config.animation), - easing: "ease-out", - type: config.animation, - }); - } - - const layoutEl = document.createElement("style"); - layoutEl.id = "chat-preview-layout-styles"; - layoutEl.innerHTML = ` -#chat_container { - overflow: hidden !important; - scrollbar-width: none; - -ms-overflow-style: none; + applyOverlayStyles(config); + document.documentElement.setAttribute(OVERLAY_ATTRIBUTES.preview, ""); } -#chat_container::-webkit-scrollbar { - display: none; -} -`; - document.head.appendChild(layoutEl); +export function cleanupPreviewStyles() { + clearOverlayStyles(); + document.documentElement.removeAttribute(OVERLAY_ATTRIBUTES.preview); } diff --git a/src/utils/ui/animationUtils.ts b/src/services/chat/runtime/animationStyles.ts similarity index 51% rename from src/utils/ui/animationUtils.ts rename to src/services/chat/runtime/animationStyles.ts index b2bec5f..53ab4f8 100644 --- a/src/utils/ui/animationUtils.ts +++ b/src/services/chat/runtime/animationStyles.ts @@ -1,78 +1,6 @@ -// Animation utilities for chat messages +import type { AnimationOptions } from "~/config/chatAnimation"; -export const CHAT_ANIMATION_MODES = [ - "fade", - "flow", - "scroll", - "none", -] as const; -export type ChatAnimationMode = (typeof CHAT_ANIMATION_MODES)[number]; - -export interface AnimationOptions { - enabled: boolean; - duration: number; - easing: string; - type: ChatAnimationMode; -} - -export const MIN_MESSAGE_SPEED = 0; -export const MAX_MESSAGE_SPEED = 100; -export const DEFAULT_MESSAGE_SPEED = 31; -export const MIN_MESSAGE_INTERVAL_MS = 80; -export const MAX_MESSAGE_INTERVAL_MS = 3000; - -export const DEFAULT_ANIMATION_OPTIONS: AnimationOptions = { - enabled: true, - duration: 200, - easing: "ease-in-out", - type: "fade", -}; -export const FLOW_ANIMATION_DURATION = 380; - -export function getMessageEntryAnimationDuration( - mode: ChatAnimationMode, -): number { - return mode === "flow" - ? FLOW_ANIMATION_DURATION - : DEFAULT_ANIMATION_OPTIONS.duration; -} - -export function normalizeChatAnimationMode( - value: unknown, - fallback: ChatAnimationMode = "fade", -): ChatAnimationMode { - return CHAT_ANIMATION_MODES.includes(value as ChatAnimationMode) - ? (value as ChatAnimationMode) - : fallback; -} - -export function hasMessageEntryAnimation(mode: ChatAnimationMode): boolean { - return mode === "fade" || mode === "flow"; -} - -export function getAnimationScrollBehavior( - mode: ChatAnimationMode, -): ScrollBehavior { - return mode === "scroll" ? "smooth" : "auto"; -} - -export function clampMessageSpeed(speed: number): number { - if (!Number.isFinite(speed)) return DEFAULT_MESSAGE_SPEED; - return Math.min( - Math.max(Math.round(speed), MIN_MESSAGE_SPEED), - MAX_MESSAGE_SPEED, - ); -} - -export function messageSpeedToIntervalMs(speed: number): number | null { - const clamped = clampMessageSpeed(speed); - if (clamped <= MIN_MESSAGE_SPEED) return null; - - const normalized = (clamped - 1) / (MAX_MESSAGE_SPEED - 1); - const ratio = MIN_MESSAGE_INTERVAL_MS / MAX_MESSAGE_INTERVAL_MS; - - return Math.round(MAX_MESSAGE_INTERVAL_MS * ratio ** normalized); -} +const ANIMATION_STYLE_ID = "chat-animations"; /** * Generate CSS animation classes based on options @@ -154,13 +82,24 @@ export function applyAnimation( } /** - * Create style element for animations + * Writes the animation styles into the document, reusing the existing element + * when there is one. + * + * Reuse is what keeps the element identifiable: callers that appended + * unconditionally left duplicate elements behind, and a teardown that removes + * "the" element by id can only ever remove one of them. */ export function injectAnimationStyles( options: AnimationOptions, ): HTMLStyleElement { + const existing = document.getElementById(ANIMATION_STYLE_ID); + if (existing?.tagName === "STYLE") { + existing.textContent = getAnimationStyles(options); + return existing as HTMLStyleElement; + } + const styleEl = document.createElement("style"); - styleEl.id = "chat-animations"; + styleEl.id = ANIMATION_STYLE_ID; styleEl.textContent = getAnimationStyles(options); document.head.appendChild(styleEl); return styleEl; @@ -170,12 +109,15 @@ export function injectAnimationStyles( * Update existing animation styles */ export function updateAnimationStyles(options: AnimationOptions): void { - let styleEl = document.getElementById("chat-animations") as HTMLStyleElement; - - if (!styleEl) { - injectAnimationStyles(options); - return; - } + injectAnimationStyles(options); +} - styleEl.textContent = getAnimationStyles(options); +/** + * Removes the animation stylesheet this module injects, so the runtime that + * published it can take it down again. + */ +export function clearAnimationStyles(): void { + document + .querySelectorAll(`style[id="${ANIMATION_STYLE_ID}"]`) + .forEach((element) => element.remove()); } diff --git a/src/utils/ui/layoutUtils.ts b/src/services/chat/runtime/layoutManager.ts similarity index 84% rename from src/utils/ui/layoutUtils.ts rename to src/services/chat/runtime/layoutManager.ts index b18e64b..b9edcb6 100644 --- a/src/utils/ui/layoutUtils.ts +++ b/src/services/chat/runtime/layoutManager.ts @@ -1,4 +1,11 @@ -// Layout utilities for chat display +/** + * Layout runtime for the chat overlay. + * + * Owns the container's layout stylesheet, its layout classes and its scroll + * behaviour. It is chat overlay infrastructure rather than a general helper: + * the stylesheet it publishes is written against `#chat_container` and + * `.chat_line`, and `ChatPresentationService` is its only owner. + */ export interface LayoutOptions { horizontal: boolean; @@ -59,6 +66,12 @@ export function getLayoutStyles(options: LayoutOptions): string { * Inject layout styles into document */ export function injectLayoutStyles(options: LayoutOptions): HTMLStyleElement { + const existing = document.getElementById("chat-layout"); + if (existing?.tagName === "STYLE") { + existing.textContent = getLayoutStyles(options); + return existing as HTMLStyleElement; + } + const styleEl = document.createElement("style"); styleEl.id = "chat-layout"; styleEl.textContent = getLayoutStyles(options); @@ -70,14 +83,7 @@ export function injectLayoutStyles(options: LayoutOptions): HTMLStyleElement { * Update existing layout styles */ export function updateLayoutStyles(options: LayoutOptions): void { - let styleEl = document.getElementById("chat-layout") as HTMLStyleElement; - - if (!styleEl) { - injectLayoutStyles(options); - return; - } - - styleEl.textContent = getLayoutStyles(options); + injectLayoutStyles(options); } /** @@ -159,7 +165,9 @@ export function isScrolledToEnd( } /** - * Layout manager class + * Owns one overlay container's layout: the published stylesheet, the layout + * classes and the scroll behaviour, including the smooth follow that keeps + * velocity continuous while messages arrive. */ export class LayoutManager { private container: HTMLElement; @@ -195,6 +203,23 @@ export class LayoutManager { this.applyLayout(); } + /** + * Removes the layout stylesheet this manager injected and stops the smooth + * follow it may have running. The container classes are left alone because the + * container leaves with the runtime that owned it. + */ + cleanup(): void { + if (this.smoothScrollFrame !== undefined) { + window.cancelAnimationFrame(this.smoothScrollFrame); + this.smoothScrollFrame = undefined; + } + this.smoothScrollTarget = undefined; + this.smoothScrollLastTime = undefined; + document + .querySelectorAll('style[id="chat-layout"]') + .forEach((element) => element.remove()); + } + /** * Get current options */ diff --git a/src/utils/ui/fadeUtils.ts b/src/services/chat/runtime/messageFade.ts similarity index 85% rename from src/utils/ui/fadeUtils.ts rename to src/services/chat/runtime/messageFade.ts index ea2a927..a03c9c4 100644 --- a/src/utils/ui/fadeUtils.ts +++ b/src/services/chat/runtime/messageFade.ts @@ -1,4 +1,9 @@ -// Message fade utilities +/** + * Message fade runtime for the chat overlay. + * + * Owns the fade timers and the `#chat-fade` stylesheet; `ChatPresentationService` + * is its only owner. + */ export interface FadeOptions { enabled: boolean; @@ -78,6 +83,12 @@ export function getFadeStyles(options: FadeOptions): string { * Inject fade styles into document */ export function injectFadeStyles(options: FadeOptions): HTMLStyleElement { + const existing = document.getElementById("chat-fade"); + if (existing?.tagName === "STYLE") { + existing.textContent = getFadeStyles(options); + return existing as HTMLStyleElement; + } + const styleEl = document.createElement('style'); styleEl.id = 'chat-fade'; styleEl.textContent = getFadeStyles(options); @@ -89,14 +100,7 @@ export function injectFadeStyles(options: FadeOptions): HTMLStyleElement { * Update existing fade styles */ export function updateFadeStyles(options: FadeOptions): void { - let styleEl = document.getElementById('chat-fade') as HTMLStyleElement; - - if (!styleEl) { - injectFadeStyles(options); - return; - } - - styleEl.textContent = getFadeStyles(options); + injectFadeStyles(options); } /** @@ -177,6 +181,16 @@ export class MessageFadeManager { updateFadeStyles(this.options); } + /** + * Removes the fade stylesheet this manager injected. Timers are `clear()`'s + * job, so a caller can stop the fades and keep the styles if it wants to. + */ + cleanup(): void { + document + .querySelectorAll('style[id="chat-fade"]') + .forEach((element) => element.remove()); + } + /** * Get current options */ diff --git a/src/services/chat/runtime/messageQueueManager.ts b/src/services/chat/runtime/messageQueueManager.ts index 4150b34..973a932 100644 --- a/src/services/chat/runtime/messageQueueManager.ts +++ b/src/services/chat/runtime/messageQueueManager.ts @@ -1,5 +1,5 @@ import type { ChatConfig } from "~/config/chatUrlParams"; -import { getAnimationScrollBehavior } from "~/utils/ui/animationUtils"; +import { getAnimationScrollBehavior } from "~/config/chatAnimation"; import type { ChatPresentationService } from "../chatPresentationService"; import type { TwitchMessage } from "../twitch/twitchService"; diff --git a/src/services/chat/runtime/overlayStyleManager.ts b/src/services/chat/runtime/overlayStyleManager.ts index 62b671d..7b6d1c3 100644 --- a/src/services/chat/runtime/overlayStyleManager.ts +++ b/src/services/chat/runtime/overlayStyleManager.ts @@ -1,70 +1,74 @@ -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import { - generateShadowStyles, - generateSizeStyles, - generateStrokeStyles, - generateVariantStyles, + getOverlayStyleVariables, + OVERLAY_ATTRIBUTES, + OVERLAY_STYLE_PROPERTIES, } from "~/styles/chatStyles"; import { getMessageEntryAnimationDuration, hasMessageEntryAnimation, - updateAnimationStyles, -} from "~/utils/ui/animationUtils"; +} from "~/config/chatAnimation"; +import { clearAnimationStyles, updateAnimationStyles } from "./animationStyles"; -const STYLE_ELEMENT_IDS = [ - "chat-size-styles", - "chat-shadow-styles", - "chat-stroke-styles", - "chat-variant-styles", - "chat-animations", -]; - -function appendStyleElement(id: string, content: string) { - const element = document.createElement("style"); - element.id = id; - element.innerHTML = content; - document.head.appendChild(element); +function setRootAttribute(name: string, enabled: boolean) { + const root = document.documentElement; + if (enabled) root.setAttribute(name, ""); + else root.removeAttribute(name); } -export class OverlayStyleManager { - apply(config: ChatConfig) { - this.cleanup(); +/** + * Publishes the overlay's presentation onto the document root. + * + * `chat.css` owns every rule; this writes only the values it needs, as custom + * properties, and flips the boolean variants as attributes. Nothing is generated + * and nothing is appended to `document.head`, so the teardown has exactly one + * thing to undo: what this wrote. + */ +export function applyOverlayStyles(config: ChatConfig): void { + clearOverlayStyles(); - appendStyleElement( - "chat-size-styles", - generateSizeStyles(config.size as 1 | 2 | 3, config.lineHeight), - ); + const root = document.documentElement; + for (const [name, value] of Object.entries(getOverlayStyleVariables(config))) { + root.style.setProperty(name, value); + } - if (config.shadow) { - appendStyleElement( - "chat-shadow-styles", - generateShadowStyles(config.shadow as 1 | 2 | 3), - ); - } + setRootAttribute(OVERLAY_ATTRIBUTES.hideNames, config.hideNames); + setRootAttribute(OVERLAY_ATTRIBUTES.nlAfterName, config.nlAfterName); - if (config.stroke) { - appendStyleElement( - "chat-stroke-styles", - generateStrokeStyles(config.stroke as 1 | 2 | 3 | 4), - ); - } + if (hasMessageEntryAnimation(config.animation)) { + updateAnimationStyles({ + enabled: true, + duration: getMessageEntryAnimationDuration(config.animation), + easing: "ease-out", + type: config.animation, + }); + } +} - const variantStyles = generateVariantStyles(config); - if (variantStyles) { - appendStyleElement("chat-variant-styles", variantStyles); - } +/** Removes every property and attribute `applyOverlayStyles` can have written. */ +export function clearOverlayStyles(): void { + const root = document.documentElement; + for (const name of OVERLAY_STYLE_PROPERTIES) { + root.style.removeProperty(name); + } + root.removeAttribute(OVERLAY_ATTRIBUTES.hideNames); + root.removeAttribute(OVERLAY_ATTRIBUTES.nlAfterName); + // applyOverlayStyles injects the animation stylesheet, so the teardown owns it + // too. Nothing else generates a stylesheet for the overlay. + clearAnimationStyles(); +} - if (hasMessageEntryAnimation(config.animation)) { - updateAnimationStyles({ - enabled: true, - duration: getMessageEntryAnimationDuration(config.animation), - easing: "ease-out", - type: config.animation, - }); - } +/** + * Lifecycle owner for the overlay's published styles. It holds no state of its + * own — the document is the state — so it exists to give the runtime a single + * object to create and tear down. + */ +export class OverlayStyleManager { + apply(config: ChatConfig) { + applyOverlayStyles(config); } cleanup() { - STYLE_ELEMENT_IDS.forEach((id) => document.getElementById(id)?.remove()); + clearOverlayStyles(); } } diff --git a/src/services/chat/seven-tv/cosmeticsService.ts b/src/services/chat/seven-tv/cosmeticsService.ts index fb48505..1b2ca54 100644 --- a/src/services/chat/seven-tv/cosmeticsService.ts +++ b/src/services/chat/seven-tv/cosmeticsService.ts @@ -75,12 +75,26 @@ export class SevenTVCosmeticsService { private paintStylesheet: CSSStyleSheet | null = null; private channelCache: Set = new Set(); // Кэш загруженных каналов + /** + * Bumped by `clearAllCaches()`. A load captures it on entry and skips its + * commit when the caches were cleared while it was in flight, so a response + * that arrives after its runtime was torn down cannot re-add the channel to + * the cache or assign paints to a user nobody is watching any more. + */ + private generation = 0; + + private isCurrentGeneration(generation: number): boolean { + return generation === this.generation; + } + async loadCosmetics(channelId: string): Promise { // Избегаем повторной загрузки if (this.channelCache.has(channelId)) return; - + const generation = this.generation; + try { await this.loadChannelUserPaints(channelId); + if (!this.isCurrentGeneration(generation)) return; this.channelCache.add(channelId); } catch (error) { log.error(LOG_CATEGORIES.PAINTS, "Failed to load cosmetics", error); @@ -88,12 +102,14 @@ export class SevenTVCosmeticsService { } private async loadChannelUserPaints(channelId: string): Promise { + const generation = this.generation; try { const response = await networkClient.request(`https://7tv.io/v3/users/twitch/${channelId}`, { route: "rte" }); - + if (response.ok) { const data = await response.json(); - + if (!this.isCurrentGeneration(generation)) return; + // Проверяем style.paint_id для канала if (data.user?.style?.paint_id) { const channelUsername = data.username || data.display_name; @@ -133,13 +149,15 @@ export class SevenTVCosmeticsService { async loadUserPaints(username: string, userId?: string): Promise { if (!userId) return; username = username.toLowerCase(); - + const generation = this.generation; + try { const response = await networkClient.request(`https://7tv.io/v3/users/twitch/${userId}`, { route: "rte" }); - + if (response.ok) { const data = await response.json(); - + if (!this.isCurrentGeneration(generation)) return; + if (data.user?.style?.paint_id) { this.addUserCosmetic(username, data.user.style.paint_id); } else if (data.cosmetics?.length) { @@ -179,6 +197,7 @@ export class SevenTVCosmeticsService { } private async loadPaintCatalog(): Promise { + const generation = this.generation; const response = await networkClient.request("https://7tv.io/v3/gql", { route: "rte", init: { @@ -211,6 +230,7 @@ export class SevenTVCosmeticsService { } const payload = await response.json(); + if (!this.isCurrentGeneration(generation)) return; const paints = payload.data?.cosmetics?.paints; if (!Array.isArray(paints)) { throw new Error("7TV paint catalog response is invalid"); @@ -284,6 +304,19 @@ export class SevenTVCosmeticsService { return (this.paintStylesheet = style.sheet ?? null); } + /** + * Removes the paint stylesheet this service generated. The rules are keyed by + * provider paint id, so the runtime that filled them owns their removal; the + * next runtime rebuilds the element instead of writing into a detached sheet. + */ + disposeStylesheet(): void { + this.paintStylesheet = null; + if (typeof document === "undefined") return; + document + .querySelectorAll('style[id="chatyx-seventv-paint-styles"]') + .forEach((element) => element.remove()); + } + calculatePaintCSS(username: string): any { // Проверяем кэш if (this.paintCSSCache.has(username)) { @@ -472,8 +505,15 @@ export class SevenTVCosmeticsService { return this.getCSSColorFromInt(paint.color); } - // Очистка всех кэшей + // Очистка кэшей, которые привязаны к загруженному каналу: paint CSS и список + // загруженных каналов. Каталог красок и назначения красок пользователям + // остаются: это провайдерские факты о пользователях, не зависящие от канала, + // и сбрасывать их на teardown значило бы перезагружать их заново ради + // сообщений тех же пользователей. `reloadCosmetics` сбрасывает и их. + // `reloadCosmetics` also calls this before it re-fetches, so the bump happens + // first and the reload's own loads capture the new generation. clearAllCaches(): void { + this.generation += 1; this.paintCSSCache.clear(); this.channelCache.clear(); } diff --git a/src/services/network/fetchJsonWithTimeout.ts b/src/services/network/fetchJsonWithTimeout.ts new file mode 100644 index 0000000..bbac508 --- /dev/null +++ b/src/services/network/fetchJsonWithTimeout.ts @@ -0,0 +1,22 @@ +/** + * Fetches a URL, parses the JSON body, and aborts after `timeoutMs`. + * + * Non-2xx responses throw, so callers can treat every failure — network, + * timeout and HTTP status — through the same catch path. + */ +export async function fetchJsonWithTimeout( + url: string, + init?: RequestInit, + timeoutMs = 8000, +): Promise { + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { ...init, signal: controller.signal }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return await response.json(); + } finally { + window.clearTimeout(timeout); + } +} diff --git a/src/services/setup/botProfiles.ts b/src/services/setup/botProfiles.ts new file mode 100644 index 0000000..7351bd2 --- /dev/null +++ b/src/services/setup/botProfiles.ts @@ -0,0 +1,113 @@ +/** + * Bot profile lookup for the setup page. + * + * Owns the Twitch/Kick lookups that resolve a login into a display name and + * avatar, so the route stays presentation and the GQL request shape has a + * single definition. Login normalization itself lives in `./logins`. + */ +import { TWITCH_GQL_ENDPOINT, TWITCH_WEB_CLIENT_ID } from "~/config/twitch"; +import { fetchJsonWithTimeout } from "~/services/network/fetchJsonWithTimeout"; + +export type BotProfile = { + login: string; + displayName: string; + avatarUrl: string; +}; + +const BOT_PROFILE_TIMEOUT_MS = 3500; + +/** Resolves logins through the public Twitch GQL endpoint in a single request. */ +export async function loadTwitchBotProfiles(logins: string[]): Promise { + if (logins.length === 0) return []; + + try { + const payload = await fetchJsonWithTimeout( + TWITCH_GQL_ENDPOINT, + { + method: "POST", + headers: { + "Client-ID": TWITCH_WEB_CLIENT_ID, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + operationName: "ChatYXSetupBotProfiles", + query: ` + query ChatYXSetupBotProfiles($logins: [String!]!) { + users(logins: $logins) { + login + displayName + profileImageURL(width: 70) + } + } + `, + variables: { logins }, + }), + }, + BOT_PROFILE_TIMEOUT_MS, + ); + + const users = (payload as { data?: { users?: unknown[] } })?.data?.users; + if (!Array.isArray(users)) return []; + + return users + .map((user) => { + if (!user || typeof user !== "object") return null; + + const entry = user as { + login?: unknown; + displayName?: unknown; + profileImageURL?: unknown; + }; + const login = String(entry.login || "").toLowerCase(); + if (!login) return null; + + return { + login, + displayName: String(entry.displayName || entry.login || login), + avatarUrl: String(entry.profileImageURL || ""), + }; + }) + .filter((profile): profile is BotProfile => profile !== null); + } catch { + return []; + } +} + +/** + * Kick has no batch endpoint, so each login is looked up on its own and a + * failure only drops that one entry. + */ +export async function loadKickBotProfiles(logins: string[]): Promise { + const profiles = await Promise.all( + logins.map(async (login) => { + try { + const payload = await fetchJsonWithTimeout( + `https://kick.com/api/v2/channels/${encodeURIComponent(login)}/info`, + {}, + BOT_PROFILE_TIMEOUT_MS, + ); + const channel = payload as { + slug?: unknown; + user?: { username?: unknown; profile_pic?: unknown }; + }; + if (typeof channel.slug !== "string") return null; + const avatarUrl = channel.user?.profile_pic; + + return { + login: channel.slug.toLowerCase(), + displayName: + typeof channel.user?.username === "string" && channel.user.username.trim() + ? channel.user.username.trim() + : channel.slug, + avatarUrl: + typeof avatarUrl === "string" && avatarUrl.startsWith("https://") + ? avatarUrl + : "", + }; + } catch { + return null; + } + }), + ); + return profiles.filter((profile): profile is BotProfile => profile !== null); +} diff --git a/src/services/setup/channelSearch.ts b/src/services/setup/channelSearch.ts new file mode 100644 index 0000000..425535a --- /dev/null +++ b/src/services/setup/channelSearch.ts @@ -0,0 +1,110 @@ +/** + * Channel search for the setup page's channel field. + * + * Twitch answers through its public GQL search-suggestion query and Kick + * through the project's source bridge. Both normalize to the same shape, and + * the normalizers are pure so they can be tested without a DOM. + */ +import { TWITCH_GQL_ENDPOINT, TWITCH_WEB_CLIENT_ID } from "~/config/twitch"; +import { normalizeLogin } from "./logins"; + +export type ChannelSuggestion = { + login: string; + displayName: string; + avatarUrl: string; +}; + +const KICK_SEARCH_ENDPOINT = "https://ytwss.ruina.team/api/kick/channels"; + +function isSafeHttpsUrl(value: unknown): value is string { + if (typeof value !== "string") return false; + try { + return new URL(value).protocol === "https:"; + } catch { + return false; + } +} + +export function normalizeTwitchSearchSuggestions( + value: unknown, +): ChannelSuggestion[] { + const payload = Array.isArray(value) ? value[0] : null; + const edges = (payload as { + data?: { searchSuggestions?: { edges?: unknown } }; + })?.data?.searchSuggestions?.edges; + if (!Array.isArray(edges)) return []; + + return edges.flatMap((edge) => { + if (!edge || typeof edge !== "object") return []; + const entry = edge as Record; + const content = (entry.node as { content?: unknown } | undefined)?.content; + if (!content || typeof content !== "object") return []; + const channel = content as Record; + const login = normalizeLogin(String(channel.login || "")); + const displayName = String(entry.text || login).trim(); + const avatarUrl = isSafeHttpsUrl(channel.profileImageURL) + ? channel.profileImageURL + : ""; + return channel.__typename === "SearchSuggestionChannel" && login + ? [{ login, displayName: displayName.slice(0, 64), avatarUrl }] + : []; + }).slice(0, 8); +} + +function normalizeKickSuggestions(value: unknown): ChannelSuggestion[] { + const channels = (value as { channels?: unknown })?.channels; + if (!Array.isArray(channels)) return []; + + return channels.flatMap((channel) => { + if (!channel || typeof channel !== "object") return []; + const entry = channel as Record; + const login = normalizeLogin(String(entry.slug || "")); + const displayName = String(entry.username || "").trim(); + const avatarUrl = isSafeHttpsUrl(entry.avatarUrl) ? entry.avatarUrl : ""; + return /^[a-z0-9_-]{1,64}$/i.test(login) && displayName + ? [{ login, displayName: displayName.slice(0, 64), avatarUrl }] + : []; + }).slice(0, 8); +} + +export async function searchTwitchChannels( + query: string, + signal: AbortSignal, +): Promise { + const payload = [{ + operationName: "SearchTray_SearchSuggestions", + variables: { + requestID: crypto.randomUUID(), + queryFragment: query, + withOfflineChannelContent: true, + }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: "1d2cd6ae289d7baa682ef4437ab010c8ea42749ebb81c052f87a8a857ea93378", + }, + }, + }]; + const response = await fetch(TWITCH_GQL_ENDPOINT, { + method: "POST", + headers: { + "Client-ID": TWITCH_WEB_CLIENT_ID, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + signal, + }); + if (!response.ok) throw new Error(`Twitch search failed (${response.status})`); + return normalizeTwitchSearchSuggestions(await response.json()); +} + +export async function searchKickChannels( + query: string, + signal: AbortSignal, +): Promise { + const url = new URL(KICK_SEARCH_ENDPOINT); + url.searchParams.set("query", query); + const response = await fetch(url, { signal }); + if (!response.ok) throw new Error(`Kick search failed (${response.status})`); + return normalizeKickSuggestions(await response.json()); +} diff --git a/src/services/setup/localFonts.ts b/src/services/setup/localFonts.ts new file mode 100644 index 0000000..fd7a163 --- /dev/null +++ b/src/services/setup/localFonts.ts @@ -0,0 +1,96 @@ +/** + * Local font capability for the setup page. + * + * The Local Font Access API is Chromium-only *and* additionally gated behind a + * permission prompt, so both the browser sniff and the enumeration live here. + * The route only maps the typed result onto its status signal. + */ + +type LocalFontData = { + family: string; + fullName?: string; + postscriptName?: string; + style?: string; +}; + +type LocalFontWindow = Window & { + queryLocalFonts?: () => Promise; +}; + +export type LocalFontOption = { + family: string; + styles: string[]; +}; + +export type LocalFontQueryResult = + | { kind: "unsupported" } + | { kind: "found"; fonts: LocalFontOption[] } + | { kind: "empty" } + | { kind: "error" }; + +/** + * Names the browser when it exposes the Local Font Access API, or null when the + * API is missing. Callers use the name in the UI copy, so a non-null result also + * serves as the "this browser can enumerate fonts" answer. + */ +export function detectLocalFontBrowser(): string | null { + if (typeof window === "undefined" || typeof navigator === "undefined") { + return null; + } + + const hasApi = + typeof (window as LocalFontWindow).queryLocalFonts === "function"; + if (!hasApi) return null; + + const ua = navigator.userAgent; + const vendor = navigator.vendor || ""; + + if (/Edg\//.test(ua)) return "Edge"; + if (/(OPR|Opera)\//.test(ua)) return "Opera"; + if (/Chrome\//.test(ua) && vendor.includes("Google")) return "Chrome"; + + return null; +} + +/** + * Collapses the raw per-face font records into one entry per family with the + * distinct styles it provides, both sorted for a stable list order. + */ +export function normalizeLocalFonts(fonts: LocalFontData[]): LocalFontOption[] { + const families = new Map>(); + + for (const font of fonts) { + const family = font.family?.trim(); + if (!family) continue; + + const styles = families.get(family) ?? new Set(); + if (font.style) styles.add(font.style); + families.set(family, styles); + } + + return Array.from(families.entries()) + .map(([family, styles]) => ({ + family, + styles: Array.from(styles).sort((a, b) => a.localeCompare(b)), + })) + .sort((a, b) => a.family.localeCompare(b.family)); +} + +/** + * Enumerates installed fonts. Never rejects: a denied permission prompt and a + * call made without user activation both surface as `error`, and an absent API + * as `unsupported`. + */ +export async function loadLocalFontOptions(): Promise { + if (!detectLocalFontBrowser()) return { kind: "unsupported" }; + + const query = (window as LocalFontWindow).queryLocalFonts; + if (typeof query !== "function") return { kind: "unsupported" }; + + try { + const fonts = normalizeLocalFonts(await query()); + return fonts.length > 0 ? { kind: "found", fonts } : { kind: "empty" }; + } catch { + return { kind: "error" }; + } +} diff --git a/src/services/setup/logins.ts b/src/services/setup/logins.ts new file mode 100644 index 0000000..6e96802 --- /dev/null +++ b/src/services/setup/logins.ts @@ -0,0 +1,43 @@ +/** + * Login vocabulary for the setup page. + * + * Bot lists, the viewer allowlist and the channel search all accept the same + * kind of value — a Twitch/Kick login a user pasted, possibly with a leading + * `@`. Keeping the normalization in one place is what lets those three inputs + * behave identically. + * + * `config/chatUrlParams` deliberately does *not* strip the `@` when it parses + * URL values, so URL parsing and input normalization stay separate. + */ + +/** Normalizes one pasted login: trims, strips one leading `@`, lowercases. */ +export function normalizeLogin(raw: string): string { + return raw.trim().replace(/^@/, "").toLowerCase(); +} + +/** The single-letter avatar fallback used before a profile arrives. */ +export function loginFallbackName(login: string): string { + return login.slice(0, 1).toUpperCase(); +} + +/** Splits a pasted comma/space separated login list. */ +export function splitLogins(raw: string): string[] { + return raw.split(/[\s,]+/).map(normalizeLogin).filter(Boolean); +} + +/** Appends the logins in `raw` that `current` does not already contain. */ +export function mergeUniqueLogins(current: string[], raw: string): string[] { + const nextLogins = splitLogins(raw); + if (nextLogins.length === 0) return current; + + const seen = new Set(current); + const merged = [...current]; + + for (const login of nextLogins) { + if (seen.has(login)) continue; + seen.add(login); + merged.push(login); + } + + return merged; +} diff --git a/src/services/storage/setupStorage.ts b/src/services/storage/setupStorage.ts new file mode 100644 index 0000000..7148ee3 --- /dev/null +++ b/src/services/storage/setupStorage.ts @@ -0,0 +1,65 @@ +/** + * Browser storage adapter for the setup page. + * + * Owns every `localStorage` access the setup feature performs, so the route + * does not talk to the platform API directly and the templates module does not + * carry its own second copy of the same guards. + */ + +/** Minimal storage surface, so tests can inject an in-memory implementation. */ +export type StorageLike = Pick; + +/** + * Storage addresses for the persisted setup state. + * + * The `config` key is versioned because older payloads are migrated by + * `config/setupImport`, not by this adapter. + */ +export const SETUP_STORAGE_KEYS = { + config: "chatyx.setup.config.v1", + twitchChannel: "chatyx.setup.twitchChannel", + previewStageBackdrop: "chatyx.setup.previewStageBackdrop", + previewStageColor: "chatyx.setup.previewStageColor", +} as const; + +/** Returns the browser storage, or null when it is missing or blocked. */ +export function getSetupStorage(): StorageLike | null { + if (typeof window === "undefined" || !("localStorage" in window)) return null; + try { + return window.localStorage; + } catch { + return null; + } +} + +/** Reads a stored setup value, returning `""` when storage is unavailable. */ +export function readStoredSetupValue( + key: string, + storage: StorageLike | null = getSetupStorage(), +): string { + if (!storage) return ""; + try { + return storage.getItem(key) || ""; + } catch { + return ""; + } +} + +/** Writes a trimmed value, or removes the key when the value is empty. */ +export function writeStoredSetupValue( + key: string, + value: string, + storage: StorageLike | null = getSetupStorage(), +): void { + if (!storage) return; + try { + const normalized = value.trim(); + if (normalized) { + storage.setItem(key, normalized); + } else { + storage.removeItem(key); + } + } catch { + // Storage can be blocked in private windows; setup must still work. + } +} diff --git a/src/styles/chat.css b/src/styles/chat.css index 2f2c435..b0cac0f 100644 --- a/src/styles/chat.css +++ b/src/styles/chat.css @@ -44,7 +44,15 @@ body { overflow-x: hidden; background-color: transparent; color: white; - font-weight: 800; + /* Overlay values arrive as custom properties from the runtime. An absent + property leaves its declaration invalid at computed-value time, which + resolves to the initial value — the same result the old generated rules + produced by not existing. */ + font-size: var(--chat-size-font-size); + font-weight: var(--chat-font-weight, 800); + filter: var(--chat-shadow-filter); + -webkit-text-stroke: var(--chat-stroke); + paint-order: var(--chat-paint-order); word-break: break-word; user-select: text; -webkit-user-select: text; @@ -62,6 +70,7 @@ body { var(--chat-event-color, #9146ff) var(--chat-event-opacity, 22%), transparent ); + line-height: var(--chat-line-height); box-sizing: border-box; inline-size: fit-content; min-inline-size: 0; @@ -315,10 +324,30 @@ body { display: inline-block; } +/* The author block is hidden rather than removed so the message keeps its + badge/author rhythm when the setting is switched back on. */ +:root[data-hide-names] .user_info { + display: none; +} + +.message { + font-weight: var(--chat-font-weight, 800); +} + +/* Breaks the line after the author name. */ +:root[data-nl-after-name] .message::before { + content: '\A'; + white-space: pre; +} + .badge { display: inline-block; vertical-align: middle; border-radius: 10%; + width: var(--chat-badge-size); + height: var(--chat-badge-size); + margin-right: var(--chat-badge-margin-right); + margin-bottom: var(--chat-badge-margin-bottom); } img.badge { @@ -331,7 +360,7 @@ img.badge { } .badge:last-of-type { - margin-right: 3px; + margin-right: var(--chat-badge-last-margin-right); } .badge.ffz-bot-mix { @@ -351,26 +380,35 @@ img.badge { } .nick { - font-weight: 800; + font-weight: var(--chat-nick-font-weight, 800); } .colon { - margin-right: 8px; + margin-right: var(--chat-colon-margin-right); + font-weight: var(--chat-nick-font-weight, 800); } .cheer_bits { - font-weight: 700; - margin-left: 2px; - margin-right: 4px; + font-weight: var(--chat-cheer-bits-font-weight); + margin-left: var(--chat-cheer-bits-margin-left); + margin-right: var(--chat-cheer-bits-margin-right); -webkit-text-stroke: 1px black; } .cheer_emote { vertical-align: middle; + max-height: var(--chat-cheer-emote-max-height); + margin-bottom: var(--chat-cheer-emote-margin-bottom); } .emote { vertical-align: middle; + max-width: var(--chat-emote-max-width); + max-height: var(--chat-emote-max-height); +} + +.emote-container { + margin-right: var(--chat-emote-margin-right); } .youtube-emote { @@ -584,6 +622,11 @@ img.badge { .emoji { vertical-align: middle; + /* The emoji image carries its own inline size, so the scale-aware size has to + win over it; at the default scale this resolves to the same value the inline + style already sets. */ + width: var(--chat-emoji-size) !important; + height: var(--chat-emoji-size) !important; } .mention { @@ -612,13 +655,19 @@ img.badge { } .upscale { - /* Emotes that need upscaling */ + height: var(--chat-upscale-height); +} + +/* The embedded preview scrolls the page around the overlay, so the container + must not scroll on its own. */ +:root[data-preview] #chat_container { + overflow: hidden; } .gigantified-emote { position: relative; align-self: flex-start; - inline-size: min(var(--gigantified-emote-width), 100%); + inline-size: min(var(--chat-gigantified-emote-width), 100%); max-inline-size: 100%; white-space: normal; } @@ -636,14 +685,14 @@ img.badge { .gigantified-emote-line { display: block; - inline-size: min(var(--gigantified-emote-width), 100%); + inline-size: min(var(--chat-gigantified-emote-width), 100%); max-inline-size: 100%; line-height: 1; overflow: visible; } .gigantified-emote-line:has(.emote-modifier-wide) { - inline-size: min(var(--gigantified-emote-width), 100%); + inline-size: min(var(--chat-gigantified-emote-width), 100%); } .gigantified-emote-line img.gigantified { diff --git a/src/styles/chatStyles.ts b/src/styles/chatStyles.ts index 4286e31..1be9f16 100644 --- a/src/styles/chatStyles.ts +++ b/src/styles/chatStyles.ts @@ -1,4 +1,4 @@ -import type { ChatConfig } from "~/utils/chat"; +import type { ChatConfig } from "~/config/chatUrlParams"; import { normalizeFontWeight } from "~/config/chatUrlParams"; // Size presets (v2 parity) @@ -65,150 +65,138 @@ export const SIZE_CONFIGS = { }, } as const; -export const generateSizeStyles = (size: 1 | 2 | 3, lineHeight = 100) => { - const config = SIZE_CONFIGS[size]; - const lineHeightPx = Number.parseInt(config.lineHeight, 10) * Math.min(Math.max(lineHeight, 80), 200) / 100; - return ` -#chat_container { - font-size: ${config.fontSize}; -} - -.chat_line { - line-height: ${lineHeightPx}px; -} - -.badge { - width: ${config.badgeSize}px; - height: ${config.badgeSize}px; - margin-right: ${config.badgeMarginRight}; - margin-bottom: ${config.badgeMarginBottom}; -} - -.badge:last-of-type { - margin-right: ${config.badgeLastMarginRight}; -} - -.colon { - margin-right: ${config.colonMarginRight}; -} - -.cheer_bits { - font-weight: ${config.cheerBitsFontWeight}; - margin-left: ${config.cheerBitsMarginLeft}; - margin-right: ${config.cheerBitsMarginRight}; -} - -.cheer_emote { - max-height: ${config.cheerEmoteMaxHeight}px; - margin-bottom: ${config.cheerEmoteMarginBottom}; -} - -.emote { - max-width: ${config.emoteMaxWidth}; - max-height: ${config.emoteMaxHeight}px; -} - -.emote-container { - margin-right: ${config.emoteMarginRight}; -} - -.upscale { - height: ${config.upscaleHeight}px; -} - -.gigantified-emote { - --gigantified-emote-width: ${config.gigantifiedEmoteWidth}; -} - -.emoji { - height: ${config.emojiHeight}px; -} -`; -}; - -export const generateShadowStyles = (shadow: 1 | 2 | 3) => { - const shadows = { - 1: "drop-shadow(2px 2px 0.2rem black)", - 2: "drop-shadow(2px 2px 0.35rem black)", - 3: "drop-shadow(2px 2px 0.5rem black)", - }; - return ` -#chat_container { - filter: ${shadows[shadow]}; -} -`; -}; +/** Shadow filters by preset, indexed the way the config stores them. */ +const SHADOW_FILTERS = { + 1: "drop-shadow(2px 2px 0.2rem black)", + 2: "drop-shadow(2px 2px 0.35rem black)", + 3: "drop-shadow(2px 2px 0.5rem black)", +} as const; -export const generateStrokeStyles = (stroke: 1 | 2 | 3 | 4) => { - const strokes = { - 1: "1px", - 2: "2px", - 3: "3px", - 4: "4px", - }; - return ` -#chat_container { - -webkit-text-stroke: ${strokes[stroke]} black; - paint-order: stroke fill; -} -`; -}; +const STROKE_WIDTHS = { + 1: "1px", + 2: "2px", + 3: "3px", + 4: "4px", +} as const; -export const generateVariantStyles = (config: ChatConfig) => { - let styles = ""; +/** Line-height percentages outside this range are clamped, as they always were. */ +const MIN_LINE_HEIGHT_PERCENT = 80; +const MAX_LINE_HEIGHT_PERCENT = 200; + +/** The emote scale is clamped rather than rejected, as it always was. */ +const MIN_EMOTE_SCALE = 0.25; +const MAX_EMOTE_SCALE = 3; + +/** + * Custom properties the overlay stylesheet consumes, derived from one config. + * + * `chat.css` owns every rule; this function only decides the values, the same way + * `getChatEventStyleVariables` does for event colours. A property that is absent + * falls back to the stylesheet's own default, which is how the disabled states + * are expressed — so a shadow or stroke that is switched off simply publishes + * nothing. + * + * The emote and emoji sizes fold in the user's emote scale, because a stylesheet + * cannot multiply a preset by a runtime value. + */ +export function getOverlayStyleVariables( + config: ChatConfig, +): Record { const size = SIZE_CONFIGS[config.size as keyof typeof SIZE_CONFIGS] || SIZE_CONFIGS[2]; - const fontWeight = normalizeFontWeight(config.fontWeight); - const nickFontWeight = normalizeFontWeight(config.nickFontWeight); + const lineHeightPercent = Math.min( + Math.max(config.lineHeight, MIN_LINE_HEIGHT_PERCENT), + MAX_LINE_HEIGHT_PERCENT, + ); const emoteScale = Number.isFinite(config.emoteScale) - ? Math.min(Math.max(config.emoteScale, 0.25), 3) + ? Math.min(Math.max(config.emoteScale, MIN_EMOTE_SCALE), MAX_EMOTE_SCALE) : 1; - styles += ` -#chat_container, -.message { - font-weight: ${fontWeight}; -} - -.nick, -.colon { - font-weight: ${nickFontWeight}; -} -`; + const variables: Record = { + "--chat-size-font-size": size.fontSize, + "--chat-line-height": `${ + (Number.parseInt(size.lineHeight, 10) * lineHeightPercent) / 100 + }px`, + "--chat-font-weight": String(normalizeFontWeight(config.fontWeight)), + "--chat-nick-font-weight": String(normalizeFontWeight(config.nickFontWeight)), + "--chat-badge-size": `${size.badgeSize}px`, + "--chat-badge-margin-right": size.badgeMarginRight, + "--chat-badge-margin-bottom": size.badgeMarginBottom, + "--chat-badge-last-margin-right": size.badgeLastMarginRight, + "--chat-colon-margin-right": size.colonMarginRight, + "--chat-cheer-bits-font-weight": String(size.cheerBitsFontWeight), + "--chat-cheer-bits-margin-left": size.cheerBitsMarginLeft, + "--chat-cheer-bits-margin-right": size.cheerBitsMarginRight, + "--chat-cheer-emote-max-height": `${size.cheerEmoteMaxHeight}px`, + "--chat-cheer-emote-margin-bottom": size.cheerEmoteMarginBottom, + "--chat-emote-max-width": `${ + Number.parseFloat(size.emoteMaxWidth) * emoteScale + }px`, + "--chat-emote-max-height": `${size.emoteMaxHeight * emoteScale}px`, + "--chat-emoji-size": `${size.emojiHeight * emoteScale}px`, + "--chat-emote-margin-right": size.emoteMarginRight, + "--chat-upscale-height": `${size.upscaleHeight}px`, + "--chat-gigantified-emote-width": size.gigantifiedEmoteWidth, + }; - if (config.hideNames) { - styles += ` -.user_info { - display: none; -} -`; + if (config.shadow) { + variables["--chat-shadow-filter"] = + SHADOW_FILTERS[config.shadow as keyof typeof SHADOW_FILTERS]; } - - if (config.nlAfterName) { - styles += ` -.message::before { - content: '\\A'; - white-space: pre; -} -`; + if (config.stroke) { + variables["--chat-stroke"] = + `${STROKE_WIDTHS[config.stroke as keyof typeof STROKE_WIDTHS]} black`; + variables["--chat-paint-order"] = "stroke fill"; } - if (emoteScale !== 1) { - styles += ` -.emote { - max-width: ${Number.parseFloat(size.emoteMaxWidth) * emoteScale}px; - max-height: ${size.emoteMaxHeight * emoteScale}px; -} - -.emoji { - width: ${size.emojiHeight * emoteScale}px !important; - height: ${size.emojiHeight * emoteScale}px !important; -} -`; - } + return variables; +} + +/** + * Every property `getOverlayStyleVariables` can publish, so a teardown can + * remove all of them without re-deriving the config. + * `tests/overlayStylesheet.test.ts` fails if this list and the function drift + * apart. + */ +export const OVERLAY_STYLE_PROPERTIES = [ + "--chat-size-font-size", + "--chat-line-height", + "--chat-font-weight", + "--chat-nick-font-weight", + "--chat-badge-size", + "--chat-badge-margin-right", + "--chat-badge-margin-bottom", + "--chat-badge-last-margin-right", + "--chat-colon-margin-right", + "--chat-cheer-bits-font-weight", + "--chat-cheer-bits-margin-left", + "--chat-cheer-bits-margin-right", + "--chat-cheer-emote-max-height", + "--chat-cheer-emote-margin-bottom", + "--chat-emote-max-width", + "--chat-emote-max-height", + "--chat-emoji-size", + "--chat-emote-margin-right", + "--chat-upscale-height", + "--chat-gigantified-emote-width", + "--chat-shadow-filter", + "--chat-stroke", + "--chat-paint-order", +] as const; - return styles; -}; +/** + * Attributes the overlay stylesheet keys its boolean variants on, so those rules + * stay static instead of being generated. `chat.css` spells them literally, and + * `tests/overlayStylesheet.test.ts` fails if the two ever drift apart. + */ +export const OVERLAY_ATTRIBUTES = { + /** Hides the author block (`.user_info`). */ + hideNames: "data-hide-names", + /** Breaks the line after the author name (`.message::before`). */ + nlAfterName: "data-nl-after-name", + /** Suppresses the container's own scrolling, for the embedded preview. */ + preview: "data-preview", +} as const; export const FONTS = [ "'Baloo Tammudu 2', cursive", diff --git a/src/types/userNotice.ts b/src/types/userNotice.ts deleted file mode 100644 index 055dced..0000000 --- a/src/types/userNotice.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * USERNOTICE Types - * TypeScript interfaces for Twitch USERNOTICE IRC messages - */ - -/** - * USERNOTICE message types - */ -export type UserNoticeType = - | 'sub' // New subscription - | 'resub' // Re-subscription - | 'subgift' // Gifted subscription - | 'submysterygift' // Mystery gift subs - | 'giftpaidupgrade' // Gifted sub converted to paid - | 'rewardgift' // Reward gift - | 'anongiftpaidupgrade' // Anonymous gift upgrade - | 'raid' // Incoming raid - | 'unraid' // Cancelled raid - | 'ritual' // New chatter ritual - | 'bitsbadgetier' // Bits badge tier achieved - | 'announcement' // Announcement - | 'charity'; // Charity donation - -/** - * Base USERNOTICE event interface - */ -export interface UserNoticeEvent { - /** Type of notice */ - type: UserNoticeType; - - /** User who triggered the notice */ - username: string; - - /** Display name */ - displayName: string; - - /** User ID */ - userId: string; - - /** System message text */ - systemMsg: string; - - /** Optional user message */ - message?: string; - - /** IRC tags */ - tags: Map; -} - -/** - * Subscription notice - */ -export interface SubNotice extends UserNoticeEvent { - type: 'sub' | 'resub'; - plan: string; // 1000, 2000, 3000, Prime - planName: string; - months?: number; - cumulativeMonths?: number; - streak?: number; - shouldShareStreak?: boolean; -} - -/** - * Gifted subscription notice - */ -export interface SubGiftNotice extends UserNoticeEvent { - type: 'subgift'; - plan: string; - planName: string; - months: number; - recipientUsername: string; - recipientDisplayName: string; - recipientId: string; - giftMonths: number; - senderCount?: number; -} - -/** - * Mystery gift subs notice - */ -export interface MysteryGiftNotice extends UserNoticeEvent { - type: 'submysterygift'; - plan: string; - giftCount: number; - senderCount?: number; -} - -/** - * Raid notice - */ -export interface RaidNotice extends UserNoticeEvent { - type: 'raid'; - viewerCount: number; - displayName: string; -} - -/** - * Ritual notice (new chatter) - */ -export interface RitualNotice extends UserNoticeEvent { - type: 'ritual'; - ritualName: string; // 'new_chatter' -} - -/** - * Bits badge tier notice - */ -export interface BitsBadgeTierNotice extends UserNoticeEvent { - type: 'bitsbadgetier'; - threshold: number; -} - -/** - * Announcement notice - */ -export interface AnnouncementNotice extends UserNoticeEvent { - type: 'announcement'; - color: string; // blue, green, orange, purple, primary -} - -/** - * Union type for all USERNOTICE events - */ -export type UserNotice = - | SubNotice - | SubGiftNotice - | MysteryGiftNotice - | RaidNotice - | RitualNotice - | BitsBadgeTierNotice - | AnnouncementNotice - | UserNoticeEvent; - -/** - * USERNOTICE display options - */ -export interface UserNoticeOptions { - /** Show subscription notices */ - showSubs: boolean; - - /** Show gifted subs */ - showGiftSubs: boolean; - - /** Show raids */ - showRaids: boolean; - - /** Show rituals */ - showRituals: boolean; - - /** Show bits badge tiers */ - showBitsBadges: boolean; - - /** Show announcements */ - showAnnouncements: boolean; - - /** Custom styling */ - customStyle?: string; -} diff --git a/src/utils/chat/actionMessages.ts b/src/utils/chat/actionMessages.ts deleted file mode 100644 index 02d97ca..0000000 --- a/src/utils/chat/actionMessages.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Action Messages (/me) Utilities - * Handles IRC ACTION messages with italic styling - */ - -import { log, LOG_CATEGORIES } from "../logger"; - -/** - * Check if message is an action (/me) message - * ACTION messages start with \x01ACTION and end with \x01 - */ -export function isActionMessage(messageText: string): boolean { - return messageText.startsWith('\x01ACTION') && messageText.endsWith('\x01'); -} - -/** - * Parse action message and remove IRC control characters - */ -export function parseActionMessage(messageText: string): string { - if (!isActionMessage(messageText)) { - return messageText; - } - - // Remove \x01ACTION prefix and \x01 suffix - const text = messageText.slice(8, -1).trim(); - - log.debug(LOG_CATEGORIES.IRC, `Parsed action message: "${text}"`); - - return text; -} - -/** - * Check if raw IRC message is an ACTION - */ -export function isActionFromIRC(rawMessage: string): boolean { - // Check PRIVMSG content for ACTION - const match = /PRIVMSG #\S+ :(.+)/.exec(rawMessage); - if (!match) return false; - - return isActionMessage(match[1]); -} - -/** - * Extract action message from raw IRC line - */ -export function extractActionFromIRC(rawMessage: string): string | null { - const match = /PRIVMSG #\S+ :(.+)/.exec(rawMessage); - if (!match) return null; - - const messageText = match[1]; - if (!isActionMessage(messageText)) return null; - - return parseActionMessage(messageText); -} - -/** - * Format action message for display - * Returns HTML with italic styling - */ -export function formatActionMessage(username: string, actionText: string, userColor: string): string { - const color = userColor || '#999999'; - - return `${username} ${actionText}`; -} - -/** - * Create action message CSS class - */ -export function getActionMessageClass(): string { - return 'chat-message-action'; -} - -/** - * Generate action message styles - */ -export function getActionMessageStyles(): string { - return ` -.chat-message-action { - font-style: italic; -} - -.chat-message-action .chat-username, -.chat-message-action .chat-message-text { - font-style: italic; -} -`; -} diff --git a/src/utils/chat/chatUtils.ts b/src/utils/chat/chatUtils.ts deleted file mode 100644 index f777f25..0000000 --- a/src/utils/chat/chatUtils.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Utilities for working with chat parameters. - -export type { ChatConfig } from "../../config/chatUrlParams"; - -export function escapeHtml(message: string): string { - const div = document.createElement("div"); - div.textContent = message; - return div.innerHTML; -} - -export function parseEmotes(message: string, emotes: any): string { - if (!emotes || Object.keys(emotes).length === 0) { - return escapeHtml(message); - } - - // Создаем объект замен как в v2 - const replacements: { [key: string]: string } = {}; - - Object.entries(emotes).forEach(([emoteId, positions]: [string, any]) => { - if (Array.isArray(positions)) { - positions.forEach((pos: string) => { - const [start, end] = pos.split("-").map(Number); - // Убираем эмодзи из сообщения для поиска кода - const emojis = new RegExp("[\u1000-\uFFFF]+", "g"); - const aux = message.replace(emojis, " "); - const emoteCode = aux.substr(start, end - start + 1); - - // Создаем плейсхолдер для замены - replacements[emoteCode] = `{{EMOTE:${emoteId}:${start}:${end}}}`; - }); - } - }); - - // Сортируем ключи по длине (как в v2) - const replacementKeys = Object.keys(replacements); - replacementKeys.sort((a, b) => b.length - a.length); - - // Заменяем эмодзи - let result = message; - replacementKeys.forEach((replacementKey) => { - const regex = new RegExp( - "(? length ? str.substr(0, length - 3) + "..." : str; -} - -export function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/src/utils/chat/emojiRenderer.ts b/src/utils/chat/emojiRenderer.ts deleted file mode 100644 index f9deedb..0000000 --- a/src/utils/chat/emojiRenderer.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Emoji Renderer - * Converts Unicode emojis to images using Twemoji CDN - */ - -// Twemoji CDN base URL -const TWEMOJI_CDN = "https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets"; - -// Emoji regex (simplified - for full support use twemoji library) -const EMOJI_REGEX = - /(?:[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{2934}\u{2935}]|\u{FE0F})/gu; - -/** - * Convert emoji codepoint to hex string - */ -export function emojiToCodepoint(emoji: string): string { - const codepoints: string[] = []; - - for (let i = 0; i < emoji.length; i++) { - const code = emoji.codePointAt(i); - if (code) { - codepoints.push(code.toString(16)); - // Skip low surrogate if high surrogate was processed - if (code > 0xffff) { - i++; - } - } - } - - return codepoints.join("-"); -} - -/** - * Get Twemoji image URL for an emoji - */ -export function getTwemojiURL( - emoji: string, - size: "72x72" | "36x36" = "72x72", -): string { - const codepoint = emojiToCodepoint(emoji); - return `${TWEMOJI_CDN}/${size}/${codepoint}.png`; -} - -/** - * Render emoji as an image tag - */ -export function renderEmoji(emoji: string, height: number = 24): string { - const url = getTwemojiURL(emoji); - return ``; -} - -/** - * Replace all emojis in text with images - */ -export function emojifyText(text: string, height: number = 24): string { - return text.replace(EMOJI_REGEX, (emoji) => { - return renderEmoji(emoji, height); - }); -} - -/** - * Check if text contains emojis - */ -export function hasEmojis(text: string): boolean { - EMOJI_REGEX.lastIndex = 0; - return EMOJI_REGEX.test(text); -} - -/** - * Extract all emojis from text - */ -export function extractEmojis(text: string): string[] { - const matches = text.match(EMOJI_REGEX); - return matches || []; -} - -/** - * Remove emojis from text - */ -export function removeEmojis(text: string): string { - return text.replace(EMOJI_REGEX, ""); -} - -/** - * Count emojis in text - */ -export function countEmojis(text: string): number { - const matches = text.match(EMOJI_REGEX); - return matches ? matches.length : 0; -} diff --git a/src/utils/chat/emoteModifiers/html.ts b/src/utils/chat/emoteModifiers/html.ts index ae2faee..bd31ce6 100644 --- a/src/utils/chat/emoteModifiers/html.ts +++ b/src/utils/chat/emoteModifiers/html.ts @@ -1,7 +1,7 @@ import type { EmoteModifierEffect, EmoteSizeBox } from "./types"; import { buildModifierStyleVars } from "./layout"; -function escapeAttr(value: string): string { +export function escapeAttr(value: string): string { return value .replace(/&/g, "&") .replace(/$1') - .replace(/__(.+?)__/g, '$1'); -} - -/** - * Parse italic text (*text* or _text_) - */ -function parseItalic(text: string): string { - // *text* or _text_ (but not ** or __) - return text - .replace(/(?$1') - .replace(/(?$1'); -} - -/** - * Parse strikethrough text (~~text~~) - */ -function parseStrikethrough(text: string): string { - return text.replace(/~~(.+?)~~/g, '$1'); -} - -/** - * Parse inline code (`code`) - */ -function parseCode(text: string): string { - return text.replace(/`(.+?)`/g, '$1'); -} - -/** - * Parse markdown links [text](url) - */ -function parseLinks(text: string): string { - return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { - // Sanitize URL - const sanitized = sanitizeMarkdownURL(url); - if (!sanitized) { - return match; // Return original if URL is dangerous - } - return `${linkText}`; - }); -} - -/** - * Sanitize URL in markdown - */ -function sanitizeMarkdownURL(url: string): string { - const trimmed = url.trim().toLowerCase(); - - if (trimmed.startsWith('javascript:') || - trimmed.startsWith('data:') || - trimmed.startsWith('vbscript:') || - trimmed.startsWith('file:')) { - return ''; - } - - return url; -} - -/** - * Parse markdown image ![alt](url) - */ -export function parseMarkdownImage(text: string, maxWidth: number = 400, maxHeight: number = 300): string { - return text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, url) => { - // Sanitize URL - const sanitized = sanitizeMarkdownURL(url); - if (!sanitized) { - return match; - } - - // Check if URL is an image - if (!isImageURL(url)) { - return match; - } - - return `${alt}`; - }); -} - -/** - * Check if URL points to an image - */ -function isImageURL(url: string): boolean { - const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']; - const lowerURL = url.toLowerCase(); - return imageExtensions.some(ext => lowerURL.endsWith(ext)); -} - -/** - * Parse markdown with options - */ -export function parseMarkdown(text: string, options: MarkdownOptions = DEFAULT_OPTIONS): string { - let result = text; - - const opts = { ...DEFAULT_OPTIONS, ...options }; - - // Parse in specific order to avoid conflicts - if (opts.code) { - result = parseCode(result); - } - - if (opts.bold) { - result = parseBold(result); - } - - if (opts.italic) { - result = parseItalic(result); - } - - if (opts.strikethrough) { - result = parseStrikethrough(result); - } - - if (opts.links) { - result = parseLinks(result); - } - - return result; -} - -/** - * Check if text contains markdown syntax - */ -export function hasMarkdown(text: string): boolean { - const patterns = [ - /\*\*.+?\*\*/, // Bold **text** - /__.+?__/, // Bold __text__ - /(? pattern.test(text)); -} - -/** - * Strip markdown formatting (keep plain text) - */ -export function stripMarkdown(text: string): string { - return text - // Remove bold - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/__(.+?)__/g, '$1') - // Remove italic - .replace(/\*(.+?)\*/g, '$1') - .replace(/_(.+?)_/g, '$1') - // Remove strikethrough - .replace(/~~(.+?)~~/g, '$1') - // Remove code - .replace(/`(.+?)`/g, '$1') - // Remove links - .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') - // Remove images - .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1'); -} diff --git a/src/utils/chat/sanitize.ts b/src/utils/chat/sanitize.ts deleted file mode 100644 index 43d44af..0000000 --- a/src/utils/chat/sanitize.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * HTML Sanitizer - * Provides XSS protection for user-generated content - */ - -/** - * Escape HTML special characters - */ -export function escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Unescape HTML entities - */ -export function unescapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'"); -} - -/** - * Preserve emoticons while escaping HTML - * <3 should stay <3, >( should stay >( - */ -export function escapeHtmlPreserveEmoticons(text: string): string { - return text - .replace(/&/g, '&') - .replace(/(<)(?!3)/g, '<') // Don't escape < if followed by 3 - .replace(/(>)(?!\()/g, '>') // Don't escape > if followed by ( - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Strip all HTML tags from text - */ -export function stripHtmlTags(text: string): string { - return text.replace(/<[^>]*>/g, ''); -} - -/** - * Sanitize HTML with allowed tags - */ -export function sanitizeHtml(html: string, allowedTags: string[] = []): string { - if (allowedTags.length === 0) { - return stripHtmlTags(html); - } - - // This is a simple sanitizer - for production use DOMPurify - const div = document.createElement('div'); - div.innerHTML = html; - - const traverse = (node: Node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element; - const tagName = element.tagName.toLowerCase(); - - if (!allowedTags.includes(tagName)) { - // Remove disallowed tag but keep content - const fragment = document.createDocumentFragment(); - while (element.firstChild) { - fragment.appendChild(element.firstChild); - } - element.parentNode?.replaceChild(fragment, element); - return; - } - - // Remove dangerous attributes - const attrs = element.attributes; - for (let i = attrs.length - 1; i >= 0; i--) { - const attr = attrs[i]; - if (attr.name.startsWith('on') || attr.name === 'style') { - element.removeAttribute(attr.name); - } - } - } - - // Traverse children - const children = Array.from(node.childNodes); - children.forEach(traverse); - }; - - traverse(div); - return div.innerHTML; -} - -/** - * Safe innerHTML setter with automatic sanitization - */ -export function setInnerHTML(element: HTMLElement, html: string, allowedTags: string[] = []): void { - element.innerHTML = sanitizeHtml(html, allowedTags); -} - -/** - * Create safe text node - */ -export function createTextNode(text: string): Text { - return document.createTextNode(text); -} - -/** - * Sanitize URL to prevent XSS - */ -export function sanitizeURL(url: string): string { - const trimmed = url.trim().toLowerCase(); - - if (trimmed.startsWith('javascript:') || - trimmed.startsWith('data:') || - trimmed.startsWith('vbscript:') || - trimmed.startsWith('file:')) { - return ''; - } - - return url; -} diff --git a/src/utils/chat/urlParser.ts b/src/utils/chat/urlParser.ts deleted file mode 100644 index cb24954..0000000 --- a/src/utils/chat/urlParser.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * URL Parser and Linker - * Detects URLs in text and creates clickable links - */ - -// URL regex pattern (from https://urlregex.com/) -const URL_REGEX = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi; - -export interface DetectedURL { - url: string; - start: number; - end: number; - display: string; -} - -/** - * Detect all URLs in text - */ -export function detectURLs(text: string): DetectedURL[] { - const urls: DetectedURL[] = []; - let match; - - // Reset regex lastIndex - URL_REGEX.lastIndex = 0; - - while ((match = URL_REGEX.exec(text)) !== null) { - const url = match[0]; - urls.push({ - url, - start: match.index, - end: match.index + url.length, - display: url.length > 50 ? url.substring(0, 47) + "..." : url, - }); - } - - return urls; -} - -/** - * Sanitize URL to prevent javascript: and data: schemes - */ -export function sanitizeURL(url: string): string { - const trimmed = url.trim().toLowerCase(); - - // Block dangerous protocols - if ( - trimmed.startsWith("javascript:") || - trimmed.startsWith("data:") || - trimmed.startsWith("vbscript:") || - trimmed.startsWith("file:") - ) { - return ""; - } - - return url; -} - -/** - * Create a clickable link element - */ -export function createLink(url: string, display?: string): string { - const sanitized = sanitizeURL(url); - if (!sanitized) { - return display || url; // Return plain text if URL is dangerous - } - - const safeHref = sanitized.replace(/"/g, "%22"); - const displayText = display || url; - return `${displayText}`; -} - -/** - * Replace all URLs in text with clickable links - */ -export function linkifyURLs(text: string): string { - const urls = detectURLs(text); - - if (urls.length === 0) { - return text; - } - - // Replace URLs from end to start to maintain indices - let result = text; - for (let i = urls.length - 1; i >= 0; i--) { - const detected = urls[i]; - const link = createLink(detected.url, detected.display); - result = - result.substring(0, detected.start) + - link + - result.substring(detected.end); - } - - return result; -} - -/** - * Check if text contains URLs - */ -export function hasURLs(text: string): boolean { - URL_REGEX.lastIndex = 0; - return URL_REGEX.test(text); -} diff --git a/src/utils/chat/userNoticeParser.ts b/src/utils/chat/userNoticeParser.ts deleted file mode 100644 index 26fbe10..0000000 --- a/src/utils/chat/userNoticeParser.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * USERNOTICE Parser - * Handles Twitch USERNOTICE IRC messages (subs, raids, gifts, etc.) - */ - -import { log, LOG_CATEGORIES } from "../logger"; -import type { - UserNoticeEvent, - UserNotice, - UserNoticeType, -} from "../../types/userNotice"; - -/** - * Parse USERNOTICE IRC message - * Format: @tags :tmi.twitch.tv USERNOTICE #channel :optional message - */ -export function parseUserNotice(ircMessage: string): UserNotice | null { - // Extract tags - const tagsMatch = /^@([^ ]+)/.exec(ircMessage); - if (!tagsMatch) return null; - - const tagsString = tagsMatch[1]; - const tags = new Map(); - - // Parse tags - tagsString.split(';').forEach(tag => { - const [key, value] = tag.split('='); - if (key && value) { - tags.set(key, decodeIRCTag(value)); - } - }); - - // Extract message if present - const messageMatch = /:([^:\s]+)$/.exec(ircMessage); - const message = messageMatch ? messageMatch[1].trim() : undefined; - - // Get notice type - const msgId = tags.get('msg-id') as UserNoticeType; - if (!msgId) { - log.warn(LOG_CATEGORIES.IRC, 'USERNOTICE missing msg-id tag'); - return null; - } - - // Base notice data - const base: UserNoticeEvent = { - type: msgId, - username: tags.get('login') || '', - displayName: tags.get('display-name') || '', - userId: tags.get('user-id') || '', - systemMsg: tags.get('system-msg') || '', - message, - tags - }; - - // Parse specific notice types - return parseSpecificNotice(base, tags); -} - -/** - * Parse specific USERNOTICE type with additional data - */ -function parseSpecificNotice(base: UserNoticeEvent, tags: Map): UserNotice { - switch (base.type) { - case 'sub': - case 'resub': - return { - ...base, - plan: tags.get('msg-param-sub-plan') || '1000', - planName: tags.get('msg-param-sub-plan-name') || 'Tier 1', - months: parseInt(tags.get('msg-param-months') || '0'), - cumulativeMonths: parseInt(tags.get('msg-param-cumulative-months') || '0'), - streak: parseInt(tags.get('msg-param-streak-months') || '0'), - shouldShareStreak: tags.get('msg-param-should-share-streak') === '1' - }; - - case 'subgift': - return { - ...base, - plan: tags.get('msg-param-sub-plan') || '1000', - planName: tags.get('msg-param-sub-plan-name') || 'Tier 1', - months: parseInt(tags.get('msg-param-gift-months') || '1'), - recipientUsername: tags.get('msg-param-recipient-user-name') || '', - recipientDisplayName: tags.get('msg-param-recipient-display-name') || '', - recipientId: tags.get('msg-param-recipient-id') || '', - giftMonths: parseInt(tags.get('msg-param-gift-months') || '1'), - senderCount: parseInt(tags.get('msg-param-sender-count') || '0') - }; - - case 'submysterygift': - return { - ...base, - plan: tags.get('msg-param-sub-plan') || '1000', - giftCount: parseInt(tags.get('msg-param-mass-gift-count') || '1'), - senderCount: parseInt(tags.get('msg-param-sender-count') || '0') - }; - - case 'raid': - return { - ...base, - viewerCount: parseInt(tags.get('msg-param-viewerCount') || '0'), - displayName: tags.get('msg-param-displayName') || base.displayName - }; - - case 'ritual': - return { - ...base, - ritualName: tags.get('msg-param-ritual-name') || 'new_chatter' - }; - - case 'bitsbadgetier': - return { - ...base, - threshold: parseInt(tags.get('msg-param-threshold') || '0') - }; - - case 'announcement': - return { - ...base, - color: tags.get('msg-param-color') || 'primary' - }; - - default: - return base; - } -} - -/** - * Decode IRC tag value - * IRC tags escape special characters - */ -function decodeIRCTag(value: string): string { - return value - .replace(/\\s/g, ' ') - .replace(/\\:/g, ';') - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\\\/g, '\\'); -} - -/** - * Format USERNOTICE for display - */ -export function formatUserNotice(notice: UserNotice): string { - // Use system message if available - if (notice.systemMsg) { - return notice.systemMsg; - } - - // Fallback formatting based on type - switch (notice.type) { - case 'sub': - return `${notice.displayName} subscribed!`; - - case 'resub': - return `${notice.displayName} resubscribed!`; - - case 'subgift': { - const gift = notice as any; - return `${notice.displayName} gifted a sub to ${gift.recipientDisplayName}!`; - } - - case 'submysterygift': { - const mystery = notice as any; - return `${notice.displayName} is gifting ${mystery.giftCount} subs!`; - } - - case 'raid': { - const raid = notice as any; - return `${notice.displayName} is raiding with ${raid.viewerCount} viewers!`; - } - - case 'ritual': - return `${notice.displayName} is new to chat!`; - - case 'bitsbadgetier': { - const bits = notice as any; - return `${notice.displayName} achieved ${bits.threshold} bits badge!`; - } - - case 'announcement': - return `Announcement: ${notice.displayName}: ${notice.message || ''}`; - - default: - return `${notice.displayName}: ${notice.message || ''}`; - } -} - -/** - * Get color for USERNOTICE type - */ -export function getUserNoticeColor(type: UserNoticeType): string { - const colors: Record = { - 'sub': '#9147ff', - 'resub': '#9147ff', - 'subgift': '#ff69b4', - 'submysterygift': '#ff69b4', - 'giftpaidupgrade': '#ff69b4', - 'rewardgift': '#ff69b4', - 'anongiftpaidupgrade': '#ff69b4', - 'raid': '#ff0000', - 'unraid': '#ff0000', - 'ritual': '#00ff00', - 'bitsbadgetier': '#9c27b0', - 'announcement': '#0099ff', - 'charity': '#00ff00' - }; - - return colors[type] || '#999999'; -} - -/** - * Check if USERNOTICE should be displayed - */ -export function shouldDisplayUserNotice(notice: UserNotice, options: any): boolean { - if (!options) return true; - - switch (notice.type) { - case 'sub': - case 'resub': - return options.showSubs !== false; - - case 'subgift': - case 'submysterygift': - return options.showGiftSubs !== false; - - case 'raid': - return options.showRaids !== false; - - case 'ritual': - return options.showRituals !== false; - - case 'bitsbadgetier': - return options.showBitsBadges !== false; - - case 'announcement': - return options.showAnnouncements !== false; - - default: - return true; - } -} diff --git a/src/utils/ui/index.ts b/src/utils/ui/index.ts deleted file mode 100644 index f669c9c..0000000 --- a/src/utils/ui/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./animationUtils"; -export * from "./fadeUtils"; diff --git a/src/utils/ui/layoutManager.ts b/src/utils/ui/layoutManager.ts deleted file mode 100644 index a7ea331..0000000 --- a/src/utils/ui/layoutManager.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Layout Manager - * Handles advanced layout options like reverse order, horizontal mode, etc. - */ - -export interface LayoutOptions { - reverseLineOrder?: boolean; - horizontal?: boolean; - singleChatter?: string; -} - -function parseAllowedChatters(raw: string): string[] { - return raw - .split(/[\s,]+/) - .map((entry) => entry.trim().toLowerCase()) - .filter(Boolean); -} - -export class LayoutManager { - private options: LayoutOptions = {}; - private container: HTMLElement | null = null; - - /** - * Set layout options - */ - setOptions(options: LayoutOptions): void { - this.options = { ...this.options, ...options }; - this.applyLayout(); - } - - /** - * Set container element - */ - setContainer(container: HTMLElement): void { - this.container = container; - this.applyLayout(); - } - - /** - * Apply layout based on current options - */ - private applyLayout(): void { - if (!this.container) return; - - // Apply reverse line order - if (this.options.reverseLineOrder) { - this.container.style.display = 'flex'; - this.container.style.flexDirection = 'column-reverse'; - } else { - this.container.style.display = 'block'; - this.container.style.flexDirection = 'row'; - } - - // Apply horizontal mode - if (this.options.horizontal) { - this.container.style.display = 'flex'; - this.container.style.flexDirection = 'row'; - this.container.style.flexWrap = 'wrap'; - this.container.style.alignItems = 'flex-start'; - } - } - - /** - * Check if message should be shown based on single chatter mode - */ - shouldShowMessage(username: string): boolean { - if (!this.options.singleChatter) { - return true; - } - - const allowedChatters = parseAllowedChatters(this.options.singleChatter); - if (allowedChatters.length === 0) { - return true; - } - - return allowedChatters.includes(username.toLowerCase()); - } - - /** - * Get layout classes for container - */ - getContainerClasses(): string[] { - const classes: string[] = []; - - if (this.options.reverseLineOrder) { - classes.push('reverse-order'); - } - - if (this.options.horizontal) { - classes.push('horizontal'); - } - - return classes; - } - - /** - * Get CSS for layout - */ - getLayoutCSS(): string { - let css = ''; - - if (this.options.reverseLineOrder) { - css += ` - #chat_container.reverse-order { - display: flex; - flex-direction: column-reverse; - } - `; - } - - if (this.options.horizontal) { - css += ` - #chat_container.horizontal { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: flex-start; - } - - #chat_container.horizontal .chat_line { - margin-right: 15px; - margin-bottom: 5px; - } - `; - } - - return css; - } - - /** - * Inject layout CSS into document - */ - injectCSS(): void { - const css = this.getLayoutCSS(); - if (!css) return; - - const styleId = 'chatyx-layout-styles'; - let style = document.getElementById(styleId); - - if (!style) { - style = document.createElement('style'); - style.id = styleId; - document.head.appendChild(style); - } - - style.textContent = css; - } -} - -// Singleton instance -export const layoutManager = new LayoutManager(); diff --git a/tests/assetTeardownRace.test.ts b/tests/assetTeardownRace.test.ts new file mode 100644 index 0000000..c16b689 --- /dev/null +++ b/tests/assetTeardownRace.test.ts @@ -0,0 +1,346 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { badgeService } from "../src/services/badges/badgeService"; +import { emoteService } from "../src/services/chat/assets/emoteService"; +import { sevenTVCosmeticsService } from "../src/services/chat/seven-tv/cosmeticsService"; +import { chatFeatureIntegration } from "../src/services/chat/chatFeatureIntegration"; +import { ffzapBadgeService } from "../src/services/badges/ffzapBadgeService"; +import { twitchGqlService } from "../src/services/chat/twitch/twitchGqlService"; +import { networkClient } from "../src/services/network/networkClient"; + +/** + * Teardown is only an ownership boundary if work that started under the old + * runtime cannot commit into shared state after the new one has started. These + * tests hold a response open, reset the store, and only then let it resolve. + */ + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +function jsonResponse(payload: unknown): Response { + return new Response(JSON.stringify(payload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +const originalRequest = networkClient.request; +let requests: string[] = []; + +function stubRequests(handler: (target: string) => Promise) { + requests = []; + networkClient.request = ((target: string) => { + requests.push(target); + return handler(target); + }) as typeof networkClient.request; +} + +afterEach(() => { + networkClient.request = originalRequest; + emoteService.reset(); + badgeService.reset(); + sevenTVCosmeticsService.clearAllCaches(); + for (const username of Object.keys(sevenTVCosmeticsService.getUserCosmetics())) { + delete sevenTVCosmeticsService.getUserCosmetics()[username]; + } + for (const id of Object.keys(sevenTVCosmeticsService.getCosmetics())) { + delete sevenTVCosmeticsService.getCosmetics()[id]; + } +}); + +describe("emote store teardown", () => { + test("a channel load that resolves after reset does not repopulate the store", async () => { + const sevenTvUser = deferred(); + stubRequests((target) => + target.includes("/v3/users/twitch/123") + ? sevenTvUser.promise + : Promise.resolve(jsonResponse({})), + ); + + const load = emoteService.loadEmotes("123", "channel"); + await Promise.resolve(); + + emoteService.reset(); + sevenTvUser.resolve( + jsonResponse({ + emote_set: { + emotes: [ + { + name: "ProbeEmote", + data: { + id: "emote-1", + name: "ProbeEmote", + flags: 0, + host: { + url: "//cdn.7tv.app/emote/emote-1", + files: [{ format: "WEBP", name: "4x.webp", width: 32, height: 32 }], + }, + }, + }, + ], + }, + }), + ); + await load; + + expect(emoteService.getEmote("ProbeEmote", "123")).toBeUndefined(); + expect(emoteService.getEmoteData().channelEmotes).toEqual({}); + expect(emoteService.getEmoteData().emotes).toEqual({}); + }); + + test("a global load that resolves after reset does not mark the store loaded", async () => { + const sevenTvGlobal = deferred(); + stubRequests((target) => + target.includes("/emote-sets/global") + ? sevenTvGlobal.promise + : Promise.resolve(jsonResponse({})), + ); + + const load = emoteService.loadEmotes("", "channel"); + await Promise.resolve(); + + emoteService.reset(); + sevenTvGlobal.resolve( + jsonResponse({ + emotes: [ + { + name: "GlobalProbe", + data: { + id: "global-1", + name: "GlobalProbe", + flags: 0, + host: { + url: "//cdn.7tv.app/emote/global-1", + files: [{ format: "WEBP", name: "4x.webp", width: 32, height: 32 }], + }, + }, + }, + ], + }), + ); + await load; + + expect(emoteService.getEmoteData().emotes).toEqual({}); + }); +}); + +describe("badge store teardown", () => { + test("a channel badge load that resolves after reset does not repopulate the store", async () => { + const channelBadges = deferred(); + let channelRequested = false; + stubRequests((target) => { + if (target.includes("badges/channel")) { + channelRequested = true; + return channelBadges.promise; + } + return Promise.resolve(jsonResponse({})); + }); + + const load = badgeService.loadBadges("channel", "123"); + while (!channelRequested) await new Promise((resolve) => setTimeout(resolve, 0)); + + badgeService.reset(); + channelBadges.resolve( + jsonResponse({ + data: [ + { + set_id: "probe", + versions: [ + { id: "9", image_url_4x: "https://example.test/probe-9.png" }, + ], + }, + ], + }), + ); + await load; + + expect(badgeService.getTwitchBadge("probe", "9")).toBeUndefined(); + }); + + test("a third-party badge load that resolves after reset does not repopulate the store", async () => { + const thirdParty = deferred(); + let thirdPartyRequested = false; + stubRequests((target) => { + if (target.includes("api.ffzap.com/v1/supporters")) { + thirdPartyRequested = true; + return thirdParty.promise; + } + return Promise.resolve(jsonResponse({})); + }); + + const load = badgeService.loadBadges("channel", "123"); + while (!thirdPartyRequested) await new Promise((resolve) => setTimeout(resolve, 0)); + + badgeService.reset(); + thirdParty.resolve( + jsonResponse([ + { + id: "42", + tier: "2", + badge_color: "#fff", + }, + ]), + ); + await load; + // `loadBadges` fires the third-party load without awaiting it, so the test + // has to let the stale promise chain drain before asserting. + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(badgeService.getBadgeData().ffzapBadges).toEqual([]); + }); + + test("a user badge lookup that resolves after reset does not commit into the store", async () => { + const sender = deferred<{ displayBadges: Array<{ setID: string; version: string; image4x: string }> }>(); + const originalLoadSender = twitchGqlService.loadSender; + let senderRequested = false; + (twitchGqlService as unknown as { loadSender: unknown }).loadSender = () => { + senderRequested = true; + return sender.promise; + }; + stubRequests(() => Promise.resolve(jsonResponse({}))); + + try { + const lookup = badgeService.loadUserBadges("viewer", "7", true); + while (!senderRequested) await new Promise((resolve) => setTimeout(resolve, 0)); + + badgeService.reset(); + sender.resolve({ + displayBadges: [ + { + setID: "probe", + version: "9", + image4x: "https://example.test/probe-9.png", + }, + ], + }); + await lookup; + + expect(badgeService.getTwitchBadge("probe", "9")).toBeUndefined(); + expect(badgeService.getUserBadges("viewer")).toEqual([]); + } finally { + (twitchGqlService as unknown as { loadSender: unknown }).loadSender = originalLoadSender; + } + }); +}); + +describe("7TV cosmetics teardown", () => { + test("the generated paint stylesheet is removed with the runtime", () => { + const head: Array<{ id: string; remove: () => void; sheet: unknown }> = []; + const previousDocument = (globalThis as { document?: unknown }).document; + + const makeElement = () => { + const element = { + id: "", + sheet: { cssRules: [], insertRule: () => {}, deleteRule: () => {} }, + remove: () => { + const index = head.indexOf(element); + if (index >= 0) head.splice(index, 1); + }, + }; + return element; + }; + + (globalThis as unknown as { document: unknown }).document = { + createElement: makeElement, + head: { appendChild: (element: unknown) => head.push(element as never) }, + querySelectorAll: (selector: string) => + selector.includes("chatyx-seventv-paint-styles") + ? head.filter((element) => element.id === "chatyx-seventv-paint-styles") + : [], + }; + + try { + sevenTVCosmeticsService.addCosmetic("paint-1", { + function: "LINEAR_GRADIENT", + color: 0xffffffff, + stops: [ + { at: 0, color: 0xff0000ff }, + { at: 1, color: 0x00ff00ff }, + ], + }); + sevenTVCosmeticsService.addUserCosmetic("probeuser", "paint-1"); + + expect(sevenTVCosmeticsService.calculatePaintCSS("probeuser")).toBeTruthy(); + expect(head).toHaveLength(1); + + // What `ChatPresentationService.cleanup()` does on runtime teardown. + sevenTVCosmeticsService.clearAllCaches(); + sevenTVCosmeticsService.disposeStylesheet(); + expect(head).toHaveLength(0); + + // The reference was dropped as well, so the next render rebuilds the + // element instead of writing into the detached sheet. + expect(sevenTVCosmeticsService.calculatePaintCSS("probeuser")).toBeTruthy(); + expect(head).toHaveLength(1); + } finally { + if (previousDocument === undefined) { + Reflect.deleteProperty(globalThis, "document"); + } else { + (globalThis as unknown as { document: unknown }).document = previousDocument; + } + } + }); + + test("a channel cosmetics load that resolves after the caches were cleared is dropped", async () => { + const channelUser = deferred(); + stubRequests(() => channelUser.promise); + + const load = sevenTVCosmeticsService.loadCosmetics("123"); + await Promise.resolve(); + + // What `ChatPresentationService.cleanup()` does on runtime teardown. + sevenTVCosmeticsService.clearAllCaches(); + channelUser.resolve( + jsonResponse({ + username: "probeuser", + user: { style: { paint_id: "paint-1" } }, + }), + ); + await load; + + expect(sevenTVCosmeticsService.getPaintsForUser("probeuser")).toEqual([]); + + // The channel is not marked as loaded either, so the next runtime refetches. + stubRequests(() => Promise.resolve(jsonResponse({}))); + await sevenTVCosmeticsService.loadCosmetics("123"); + expect(requests).toHaveLength(1); + }); +}); + +describe("chat feature integration teardown", () => { + test("an initialization that outlives destroy does not claim the integration", async () => { + const originalLoadBadges = ffzapBadgeService.loadBadges; + const badges = deferred(); + let loads = 0; + ffzapBadgeService.loadBadges = () => { + loads += 1; + return badges.promise; + }; + stubRequests(() => Promise.resolve(jsonResponse({}))); + + try { + // A non-numeric channel keeps the 7TV EventAPI out of the test. + const initialization = chatFeatureIntegration.initialize("channel"); + await Promise.resolve(); + + chatFeatureIntegration.destroy(); + badges.resolve(); + await initialization; + + // The stale initialization must not have marked the integration as + // initialized, so a second one runs its loaders again. + const second = chatFeatureIntegration.initialize("channel"); + await Promise.resolve(); + expect(loads).toBe(2); + + chatFeatureIntegration.destroy(); + await second; + } finally { + ffzapBadgeService.loadBadges = originalLoadBadges; + } + }); +}); diff --git a/tests/channelSearch.test.ts b/tests/channelSearch.test.ts index e3ed4f6..fa434e4 100644 --- a/tests/channelSearch.test.ts +++ b/tests/channelSearch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { normalizeTwitchSearchSuggestions } from "../src/components/setup/TwitchChannelField"; +import { normalizeTwitchSearchSuggestions } from "../src/services/setup/channelSearch"; describe("Twitch search suggestions", () => { test("keeps channel suggestions with HTTPS avatars", () => { diff --git a/tests/animationUtils.test.ts b/tests/chatAnimation.test.ts similarity index 93% rename from tests/animationUtils.test.ts rename to tests/chatAnimation.test.ts index 5e1a9af..da35f9a 100644 --- a/tests/animationUtils.test.ts +++ b/tests/chatAnimation.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from "bun:test"; import { getAnimationScrollBehavior, - getAnimationStyles, getMessageEntryAnimationDuration, normalizeChatAnimationMode, -} from "../src/utils/ui/animationUtils"; +} from "../src/config/chatAnimation"; +import { getAnimationStyles } from "../src/services/chat/runtime/animationStyles"; const options = { enabled: true, diff --git a/tests/chatEventStyles.test.ts b/tests/chatEventStyles.test.ts index 32043c5..6dada5c 100644 --- a/tests/chatEventStyles.test.ts +++ b/tests/chatEventStyles.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { getChatEventStyleVariables } from "../src/styles/chatEventStyles"; -import { generateSizeStyles, SIZE_CONFIGS } from "../src/styles/chatStyles"; +import { getOverlayStyleVariables, SIZE_CONFIGS } from "../src/styles/chatStyles"; +import { DEFAULT_CHAT_CONFIG } from "../src/config/chatUrlParams"; import { DEFAULT_EVENT_COLORS } from "../src/config/eventColors"; describe("chat event style variables", () => { @@ -37,13 +38,65 @@ describe("chat event style variables", () => { test("publishes the configured gigantified emote width for every size preset", () => { for (const size of [1, 2, 3] as const) { - expect(generateSizeStyles(size)).toContain( - `--gigantified-emote-width: ${SIZE_CONFIGS[size].gigantifiedEmoteWidth}`, + const variables = getOverlayStyleVariables({ ...DEFAULT_CHAT_CONFIG, size }); + expect(variables["--chat-gigantified-emote-width"]).toBe( + SIZE_CONFIGS[size].gigantifiedEmoteWidth, ); } }); test("scales the message line height", () => { - expect(generateSizeStyles(1, 150)).toContain("line-height: 45px"); + const variables = getOverlayStyleVariables({ + ...DEFAULT_CHAT_CONFIG, + size: 1, + lineHeight: 150, + }); + expect(variables["--chat-line-height"]).toBe("45px"); + }); + + test("clamps the line height percentage", () => { + expect( + getOverlayStyleVariables({ ...DEFAULT_CHAT_CONFIG, size: 1, lineHeight: 900 })[ + "--chat-line-height" + ], + ).toBe("60px"); + expect( + getOverlayStyleVariables({ ...DEFAULT_CHAT_CONFIG, size: 1, lineHeight: 0 })[ + "--chat-line-height" + ], + ).toBe("24px"); + }); + + test("folds the emote scale into the emote and emoji sizes", () => { + const scaled = getOverlayStyleVariables({ + ...DEFAULT_CHAT_CONFIG, + size: 2, + emoteScale: 1.5, + }); + expect(scaled["--chat-emote-max-width"]).toBe("192px"); + expect(scaled["--chat-emote-max-height"]).toBe("63px"); + expect(scaled["--chat-emoji-size"]).toBe("58.5px"); + }); + + test("omits the shadow and stroke properties when they are switched off", () => { + const off = getOverlayStyleVariables({ + ...DEFAULT_CHAT_CONFIG, + shadow: false, + stroke: false, + }); + expect(off["--chat-shadow-filter"]).toBeUndefined(); + expect(off["--chat-stroke"]).toBeUndefined(); + expect(off["--chat-paint-order"]).toBeUndefined(); + }); + + test("publishes the shadow and stroke properties when they are on", () => { + const on = getOverlayStyleVariables({ + ...DEFAULT_CHAT_CONFIG, + shadow: 3, + stroke: 4, + }); + expect(on["--chat-shadow-filter"]).toBe("drop-shadow(2px 2px 0.5rem black)"); + expect(on["--chat-stroke"]).toBe("4px black"); + expect(on["--chat-paint-order"]).toBe("stroke fill"); }); }); diff --git a/tests/chatOverlayApplication.test.ts b/tests/chatOverlayApplication.test.ts new file mode 100644 index 0000000..c4e997c --- /dev/null +++ b/tests/chatOverlayApplication.test.ts @@ -0,0 +1,166 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { DEFAULT_CHAT_CONFIG, type ChatConfig } from "../src/config/chatUrlParams"; +import { + ChatOverlayApplication, + type ChatOverlayApplicationDependencies, + type ChatOverlayApplicationHooks, +} from "../src/features/chat-overlay/application/chatOverlayApplication"; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +const initialConfig: ChatConfig = { ...DEFAULT_CHAT_CONFIG, channel: "streamer" }; + +function createHooks(): ChatOverlayApplicationHooks { + return { + onConfigResolved: () => {}, + onServiceReady: () => {}, + onLoadingChange: () => {}, + onCommandStatusChange: () => {}, + onConnectionChange: () => {}, + onMessagesChange: () => {}, + onAnimationDurationChange: () => {}, + onChannelResolved: () => {}, + onPredictionChange: () => {}, + onPredictionTimeChange: () => {}, + onLoadingComplete: () => {}, + }; +} + +/** + * The application constructs both runtimes in its constructor, so the harness + * reports what the constructor built separately from what has been released. + */ +function createHarness(initialize?: () => Promise) { + const calls: string[] = []; + const dependencies: ChatOverlayApplicationDependencies = { + createLiveRuntime: () => { + calls.push("runtime.create"); + return { + initialize: () => { + calls.push("runtime.initialize"); + return initialize ? initialize() : Promise.resolve(); + }, + updateConfig: () => calls.push("runtime.updateConfig"), + destroy: () => calls.push("runtime.destroy"), + }; + }, + createPreviewRuntime: () => { + throw new Error("the preview runtime should not be built for a live application"); + }, + createPredictionsRuntime: () => { + calls.push("predictions.create"); + return { + update: () => calls.push("predictions.update"), + destroy: () => calls.push("predictions.destroy"), + }; + }, + }; + + const application = new ChatOverlayApplication( + { + channel: "streamer", + initialConfig, + mode: "live", + previewDemoKind: "pasta", + }, + createHooks(), + dependencies, + ); + + return { application, calls }; +} + +const originalWindow = (globalThis as { window?: unknown }).window; + +function installWindowShim() { + const listeners = new Set(); + (globalThis as unknown as { window: unknown }).window = { + addEventListener: (_type: string, listener: unknown) => listeners.add(listener), + removeEventListener: (_type: string, listener: unknown) => listeners.delete(listener), + }; + return listeners; +} + +afterEach(() => { + if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window; + else (globalThis as unknown as { window: unknown }).window = originalWindow; +}); + +describe("chat overlay application lifecycle", () => { + test("destroy before start releases the runtimes the constructor built", () => { + installWindowShim(); + const { application, calls } = createHarness(); + + expect(calls).toEqual(["predictions.create", "runtime.create"]); + + application.destroy(); + + expect(calls).toEqual([ + "predictions.create", + "runtime.create", + "predictions.destroy", + "runtime.destroy", + ]); + }); + + test("destroy does not release an already released application twice", () => { + installWindowShim(); + const { application, calls } = createHarness(); + + application.destroy(); + application.destroy(); + + expect(calls.filter((call) => call === "runtime.destroy")).toHaveLength(1); + expect(calls.filter((call) => call === "predictions.destroy")).toHaveLength(1); + }); + + test("start initializes once and detaches its message listener on destroy", async () => { + const listeners = installWindowShim(); + const { application, calls } = createHarness(); + + await application.start(); + await application.start(); + + expect(calls.filter((call) => call === "runtime.initialize")).toHaveLength(1); + expect(listeners.size).toBe(1); + + application.destroy(); + expect(listeners.size).toBe(0); + }); + + test("start after destroy does not resurrect the application", async () => { + installWindowShim(); + const { application, calls } = createHarness(); + + application.destroy(); + await application.start(); + + expect(calls.filter((call) => call === "runtime.initialize")).toHaveLength(0); + expect(calls.filter((call) => call === "runtime.destroy")).toHaveLength(1); + }); + + test("destroy during a pending initialization leaves no owner behind", async () => { + const listeners = installWindowShim(); + const initialization = deferred(); + const { application, calls } = createHarness(() => initialization.promise); + + const started = application.start(); + expect(calls).toContain("runtime.initialize"); + + application.destroy(); + initialization.resolve(); + await started; + + // The runtime was released while its initialize was still awaiting, and the + // resolved initialization did not re-attach the listener or restart it. + expect(calls.filter((call) => call === "runtime.destroy")).toHaveLength(1); + expect(listeners.size).toBe(0); + expect(calls.filter((call) => call === "runtime.initialize")).toHaveLength(1); + }); +}); diff --git a/tests/chatRuntimeLifecycle.test.ts b/tests/chatRuntimeLifecycle.test.ts index 7cfcf35..81f519a 100644 --- a/tests/chatRuntimeLifecycle.test.ts +++ b/tests/chatRuntimeLifecycle.test.ts @@ -386,6 +386,11 @@ describe("overlay runtime lifecycle", () => { style: {}, remove: () => {}, }), + documentElement: { + style: { setProperty: () => {}, removeProperty: () => {} }, + setAttribute: () => {}, + removeAttribute: () => {}, + }, getElementById: () => null, querySelectorAll: () => [], }; @@ -434,6 +439,11 @@ describe("overlay runtime lifecycle", () => { style: {}, remove: () => {}, }), + documentElement: { + style: { setProperty: () => {}, removeProperty: () => {} }, + setAttribute: () => {}, + removeAttribute: () => {}, + }, getElementById: () => null, querySelectorAll: () => [], }; diff --git a/tests/formValues.test.ts b/tests/formValues.test.ts new file mode 100644 index 0000000..1b760f6 --- /dev/null +++ b/tests/formValues.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test"; +import { + DEFAULT_CHAT_CONFIG, + chatConfigToSearchParams, + parseChatConfigFromSearchParams, + type ChatConfig, +} from "../src/config/chatUrlParams"; +import { + normalizeHexColor, + toClampedInt, + toFloat, + toInt, + toPositiveIntOrFalse, +} from "../src/config/formValues"; + +test("normalizeHexColor accepts a plain six-digit colour with or without the hash", () => { + expect(normalizeHexColor("#AbCdEf", "#000000")).toBe("#AbCdEf"); + expect(normalizeHexColor("AbCdEf", "#000000")).toBe("#AbCdEf"); + expect(normalizeHexColor(" #abcdef ", "#000000")).toBe("#abcdef"); +}); + +test("normalizeHexColor rejects a value of the wrong length or alphabet", () => { + expect(normalizeHexColor("#abc", "#ffffff")).toBe("#ffffff"); + expect(normalizeHexColor("#gggggg", "#ffffff")).toBe("#ffffff"); + expect(normalizeHexColor("", "#ffffff")).toBe("#ffffff"); +}); + +test("normalizeHexColor only accepts the alpha suffix when asked", () => { + expect(normalizeHexColor("#11223380", "#ffffff", true)).toBe("#11223380"); + expect(normalizeHexColor("#11223380", "#ffffff", false)).toBe("#ffffff"); + expect(normalizeHexColor("#1122338", "#ffffff", true)).toBe("#ffffff"); +}); + +test("numeric coercion falls back instead of producing NaN", () => { + expect(toInt("42", 7)).toBe(42); + expect(toInt("abc", 7)).toBe(7); + expect(toInt("", 7)).toBe(7); + + expect(toFloat("1.35", 1)).toBe(1.35); + expect(toFloat("nope", 1)).toBe(1); + + expect(toClampedInt("150", 100, 80, 200)).toBe(150); + expect(toClampedInt("500", 100, 80, 200)).toBe(200); + expect(toClampedInt("1", 100, 80, 200)).toBe(80); + expect(toClampedInt("abc", 100, 80, 200)).toBe(100); +}); + +test("toPositiveIntOrFalse treats zero and negatives as off", () => { + expect(toPositiveIntOrFalse("4")).toBe(4); + expect(toPositiveIntOrFalse("0")).toBe(false); + expect(toPositiveIntOrFalse("-3")).toBe(false); + expect(toPositiveIntOrFalse("abc")).toBe(false); +}); + +test("form coercion and url parsing agree on the shared value shapes", () => { + const config: ChatConfig = { + ...DEFAULT_CHAT_CONFIG, + size: toInt("2", DEFAULT_CHAT_CONFIG.size), + lineHeight: toClampedInt("150", DEFAULT_CHAT_CONFIG.lineHeight, 80, 200), + emoteScale: toFloat("1.35", DEFAULT_CHAT_CONFIG.emoteScale), + shadow: toPositiveIntOrFalse("4"), + stroke: toPositiveIntOrFalse("0"), + fade: toPositiveIntOrFalse("7"), + overlayBorderColor: normalizeHexColor("#AbCdEf", "#ffffff"), + overlayBackgroundColor: normalizeHexColor( + "#11223380", + DEFAULT_CHAT_CONFIG.overlayBackgroundColor, + true, + ), + }; + + const parsed = parseChatConfigFromSearchParams(chatConfigToSearchParams(config)); + + expect(parsed.size).toBe(2); + expect(parsed.lineHeight).toBe(150); + expect(parsed.emoteScale).toBe(1.35); + expect(parsed.shadow).toBe(4); + expect(parsed.stroke).toBe(false); + expect(parsed.fade).toBe(7); + expect(parsed.overlayBorderColor).toBe("#AbCdEf"); + expect(parsed.overlayBackgroundColor).toBe("#11223380"); +}); + +test("the intOrFalse and secondsOrFalse url kinds parse identically", () => { + // toPositiveIntOrFalse replaces two byte-identical helpers, so pin the + // premise: the two URL kinds really are the same shape. + const params = new URLSearchParams({ sh: "9", fd: "9" }); + const parsed = parseChatConfigFromSearchParams(params); + expect(parsed.shadow).toBe(parsed.fade); + + const offParams = new URLSearchParams({ sh: "0", fd: "0" }); + const offParsed = parseChatConfigFromSearchParams(offParams); + expect(offParsed.shadow).toBe(false); + expect(offParsed.fade).toBe(false); +}); diff --git a/tests/layoutUtils.test.ts b/tests/layoutUtils.test.ts index bbc5d38..7592758 100644 --- a/tests/layoutUtils.test.ts +++ b/tests/layoutUtils.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from "bun:test"; import { + LayoutManager, getLayoutStyles, getScrollPosition, isScrolledToEnd, scrollToLatest, -} from "../src/utils/ui/layoutUtils"; +} from "../src/services/chat/runtime/layoutManager"; function createContainer( values: Partial = {}, @@ -80,3 +81,70 @@ describe("chat layout scrolling", () => { expect(container.lastScroll).toEqual({ top: 600, behavior: "smooth" }); }); }); + +describe("layout manager cleanup", () => { + test("cancels the smooth follow it started", () => { + const previousWindow = (globalThis as { window?: unknown }).window; + const previousDocument = (globalThis as { document?: unknown }).document; + const frames: FrameRequestCallback[] = []; + const head: Array<{ remove: () => void }> = []; + let cancelled: number | undefined; + + (globalThis as unknown as { window: unknown }).window = { + requestAnimationFrame: (callback: FrameRequestCallback) => { + frames.push(callback); + return 42; + }, + cancelAnimationFrame: (id: number) => { + cancelled = id; + }, + }; + (globalThis as unknown as { document: unknown }).document = { + createElement: () => { + const element = { + remove: () => { + const index = head.indexOf(element); + if (index >= 0) head.splice(index, 1); + }, + }; + head.push(element); + return element; + }, + head: { appendChild: () => {} }, + getElementById: () => null, + querySelectorAll: () => head, + }; + + try { + const container = createContainer({ + classList: { remove: () => {}, add: () => {} }, + } as unknown as Partial); + const manager = new LayoutManager(container, { + horizontal: false, + reverse: false, + }); + + manager.scrollIfNeeded("smooth", true); + expect(frames).toHaveLength(1); + expect(head).toHaveLength(1); + + manager.cleanup(); + + // The stylesheet and the pending animation frame both belong to the + // manager, so teardown must release both. + expect(cancelled).toBe(42); + expect(head).toHaveLength(0); + } finally { + if (previousWindow === undefined) { + Reflect.deleteProperty(globalThis, "window"); + } else { + (globalThis as unknown as { window: unknown }).window = previousWindow; + } + if (previousDocument === undefined) { + Reflect.deleteProperty(globalThis, "document"); + } else { + (globalThis as unknown as { document: unknown }).document = previousDocument; + } + } + }); +}); diff --git a/tests/overlayStyleManager.test.ts b/tests/overlayStyleManager.test.ts new file mode 100644 index 0000000..07e3b96 --- /dev/null +++ b/tests/overlayStyleManager.test.ts @@ -0,0 +1,127 @@ +import { expect, test } from "bun:test"; +import { DEFAULT_CHAT_CONFIG } from "../src/config/chatUrlParams"; +import { + applyOverlayStyles, + clearOverlayStyles, +} from "../src/services/chat/runtime/overlayStyleManager"; +import { SIZE_CONFIGS } from "../src/styles/chatStyles"; + +/** + * The overlay stylesheet teardown is exercised through a minimal document shim: + * the manager writes custom properties and attributes on the root, and it owns + * the animation stylesheet it injects. This is not a DOM implementation — only + * the surface the manager touches. + */ +type FakeStyleElement = { id: string; tagName: string; textContent: string; remove: () => void }; + +function installDocument() { + const properties = new Map(); + const attributes = new Map(); + const head: FakeStyleElement[] = []; + + const makeStyle = (): FakeStyleElement => { + const element: FakeStyleElement = { + id: "", + tagName: "STYLE", + textContent: "", + remove: () => { + const index = head.indexOf(element); + if (index >= 0) head.splice(index, 1); + }, + }; + return element; + }; + + const document = { + documentElement: { + style: { + setProperty: (name: string, value: string) => properties.set(name, value), + removeProperty: (name: string) => properties.delete(name), + }, + setAttribute: (name: string, value: string) => attributes.set(name, value), + removeAttribute: (name: string) => attributes.delete(name), + }, + head: { appendChild: (element: FakeStyleElement) => head.push(element) }, + createElement: () => makeStyle(), + getElementById: (id: string) => head.find((element) => element.id === id) ?? null, + querySelectorAll: (selector: string) => { + const match = /^style\[id="(.+)"\]$/.exec(selector); + if (!match) return []; + return head.filter((element) => element.id === match[1]); + }, + }; + + const previous = Object.getOwnPropertyDescriptor(globalThis, "document"); + Object.defineProperty(globalThis, "document", { configurable: true, value: document }); + return { + properties, + attributes, + head, + restore: () => { + if (previous) Object.defineProperty(globalThis, "document", previous); + else Reflect.deleteProperty(globalThis, "document"); + }, + }; +} + +test("applying publishes properties and the boolean attributes", () => { + const shim = installDocument(); + try { + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, hideNames: true, nlAfterName: true }); + + expect(shim.properties.get("--chat-size-font-size")).toBe( + SIZE_CONFIGS[DEFAULT_CHAT_CONFIG.size as 1 | 2 | 3].fontSize, + ); + expect(shim.attributes.has("data-hide-names")).toBe(true); + expect(shim.attributes.has("data-nl-after-name")).toBe(true); + } finally { + shim.restore(); + } +}); + +test("clearing removes the properties, the attributes and the animation stylesheet", () => { + const shim = installDocument(); + try { + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, animation: "fade", hideNames: true }); + expect(shim.head.map((element) => element.id)).toContain("chat-animations"); + + clearOverlayStyles(); + + expect([...shim.properties.keys()]).toEqual([]); + expect([...shim.attributes.keys()]).toEqual([]); + // The manager injects the animation stylesheet, so it must take it down too; + // an earlier revision of the refactor left this one behind. + expect(shim.head).toEqual([]); + } finally { + shim.restore(); + } +}); + +test("applying twice does not accumulate animation stylesheets", () => { + const shim = installDocument(); + try { + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, animation: "fade" }); + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, animation: "fade" }); + + expect(shim.head).toHaveLength(1); + } finally { + shim.restore(); + } +}); + +test("applying after clearing reconstructs the published state", () => { + const shim = installDocument(); + try { + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, hideNames: true, animation: "fade" }); + clearOverlayStyles(); + applyOverlayStyles({ ...DEFAULT_CHAT_CONFIG, hideNames: true, animation: "fade" }); + + expect(shim.properties.get("--chat-size-font-size")).toBe( + SIZE_CONFIGS[DEFAULT_CHAT_CONFIG.size as 1 | 2 | 3].fontSize, + ); + expect(shim.attributes.has("data-hide-names")).toBe(true); + expect(shim.head.map((element) => element.id)).toEqual(["chat-animations"]); + } finally { + shim.restore(); + } +}); diff --git a/tests/overlayStylesheet.test.ts b/tests/overlayStylesheet.test.ts new file mode 100644 index 0000000..0b46888 --- /dev/null +++ b/tests/overlayStylesheet.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "bun:test"; +import { DEFAULT_CHAT_CONFIG } from "../src/config/chatUrlParams"; +import { + getOverlayStyleVariables, + OVERLAY_ATTRIBUTES, + OVERLAY_STYLE_PROPERTIES, +} from "../src/styles/chatStyles"; + +const chatCss = await Bun.file( + new URL("../src/styles/chat.css", import.meta.url), +).text(); + +test("the property list covers exactly what the variables function publishes", () => { + // A maximal config, so the conditional shadow and stroke properties are present + // and the two sets have to match in both directions. + const variables = getOverlayStyleVariables({ + ...DEFAULT_CHAT_CONFIG, + shadow: 3, + stroke: 4, + }); + const known = new Set(OVERLAY_STYLE_PROPERTIES); + + expect(Object.keys(variables).filter((name) => !known.has(name))).toEqual([]); + expect([...OVERLAY_STYLE_PROPERTIES].filter((name) => !(name in variables))).toEqual( + [], + ); +}); + +test("every published property is consumed by the stylesheet", () => { + const unused = [...OVERLAY_STYLE_PROPERTIES].filter( + (name) => !chatCss.includes(`var(${name}`), + ); + expect(unused).toEqual([]); +}); + +test("every boolean variant is keyed on an attribute the stylesheet knows", () => { + for (const attribute of [ + OVERLAY_ATTRIBUTES.hideNames, + OVERLAY_ATTRIBUTES.nlAfterName, + OVERLAY_ATTRIBUTES.preview, + ]) { + expect(chatCss).toContain(`:root[${attribute}]`); + } +}); + +test("the stylesheet no longer depends on generated style elements", () => { + // The runtime publishes properties and attributes only; nothing appends a + // stylesheet for the overlay any more. + for (const id of [ + "chat-size-styles", + "chat-shadow-styles", + "chat-stroke-styles", + "chat-variant-styles", + ]) { + expect(chatCss.includes(id)).toBe(false); + } +}); diff --git a/tests/previewRuntimeTeardown.test.ts b/tests/previewRuntimeTeardown.test.ts new file mode 100644 index 0000000..6a5e2e0 --- /dev/null +++ b/tests/previewRuntimeTeardown.test.ts @@ -0,0 +1,149 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { DEFAULT_CHAT_CONFIG, type ChatConfig } from "../src/config/chatUrlParams"; +import { + PreviewRuntime, + type PreviewRuntimeDependencies, + type PreviewRuntimeHooks, +} from "../src/features/chat-overlay/application/previewRuntime"; + +const initialConfig: ChatConfig = { ...DEFAULT_CHAT_CONFIG, rteProxy: true }; + +const originalWindow = (globalThis as { window?: unknown }).window; +const originalDocument = (globalThis as { document?: unknown }).document; + +afterEach(() => { + if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window; + else (globalThis as unknown as { window: unknown }).window = originalWindow; + if (originalDocument === undefined) delete (globalThis as { document?: unknown }).document; + else (globalThis as unknown as { document: unknown }).document = originalDocument; +}); + +function createHooks(): PreviewRuntimeHooks { + return { + onConfigResolved: () => {}, + onServiceReady: () => {}, + onLoadingChange: () => {}, + onConnectionChange: () => {}, + onMessagesChange: () => {}, + onAnimationDurationChange: () => {}, + onChannelResolved: () => {}, + onLoadingComplete: () => {}, + }; +} + +function createHarness() { + const calls: string[] = []; + const timeouts: Array<() => void> = []; + let nextTimer = 1; + + (globalThis as unknown as { window: unknown }).window = { + setTimeout: (callback: () => void) => { + timeouts.push(callback); + return nextTimer++; + }, + clearTimeout: (id: number) => calls.push(`clear-timeout:${id}`), + setInterval: () => 77, + clearInterval: (id: number) => calls.push(`clear-interval:${id}`), + requestAnimationFrame: () => 1, + }; + (globalThis as unknown as { document: unknown }).document = { + documentElement: { + style: { setProperty: () => {}, removeProperty: () => {} }, + setAttribute: () => {}, + removeAttribute: () => {}, + }, + head: { appendChild: () => {} }, + getElementById: () => null, + createElement: () => ({ id: "", textContent: "", remove: () => {}, style: {} }), + querySelectorAll: () => [], + }; + + const dependencies: PreviewRuntimeDependencies = { + createPresentationService: () => ({ + updateConfig: () => {}, + cleanup: () => calls.push("service.cleanup"), + getConfig: () => ({ animation: { duration: 0 } }), + scrollToLatest: () => {}, + initializeLayout: () => {}, + }) as never, + setProxyEnabled: (enabled) => calls.push(`proxy:${enabled}`), + resolveChannelId: async () => "0", + fetchChannelUsers: async () => undefined, + loadEmotes: async () => undefined, + loadBadges: async () => undefined, + loadCosmetics: async () => undefined, + resetSharedAssetState: () => calls.push("reset-shared-state"), + }; + + const runtime = new PreviewRuntime( + { channel: "chatyxpreview", initialConfig, demoKind: "pasta" }, + createHooks(), + dependencies, + ); + + /** Runs the render timer that `initialize()` installs. */ + const finishRendering = () => { + const last = timeouts.at(-1); + if (!last) throw new Error("the preview runtime installed no render timer"); + timeouts.length = 0; + last(); + }; + + return { runtime, calls, finishRendering }; +} + +describe("preview runtime teardown", () => { + test("destroy releases the document state the runtime claimed", async () => { + const { runtime, calls, finishRendering } = createHarness(); + + await runtime.initialize(); + finishRendering(); + runtime.destroy(); + + expect(calls).toContain("proxy:true"); + expect(calls).toContain("proxy:false"); + expect(calls).toContain("reset-shared-state"); + expect(calls).toContain("service.cleanup"); + // The message interval `finishInitialization` started is stopped. + expect(calls).toContain("clear-interval:77"); + expect(calls.indexOf("proxy:true")).toBeLessThan(calls.indexOf("proxy:false")); + expect(calls.indexOf("reset-shared-state")).toBeGreaterThan( + calls.indexOf("service.cleanup"), + ); + }); + + test("destroy before initialize does not release state it never claimed", () => { + const { runtime, calls } = createHarness(); + + runtime.destroy(); + + expect(calls).not.toContain("proxy:false"); + expect(calls).not.toContain("reset-shared-state"); + expect(calls).not.toContain("service.cleanup"); + }); + + test("a destroyed preview runtime ignores further configuration", async () => { + const { runtime, calls, finishRendering } = createHarness(); + + await runtime.initialize(); + finishRendering(); + runtime.destroy(); + + const afterDestroy = calls.length; + runtime.updateConfig({ ...initialConfig, size: 52 }); + + expect(calls).toHaveLength(afterDestroy); + }); + + test("a destroyed preview runtime cannot be initialized again", async () => { + const { runtime, calls } = createHarness(); + + runtime.destroy(); + const afterDestroy = calls.length; + await runtime.initialize(); + + // Destruction is terminal: nothing was claimed again. + expect(calls).toHaveLength(afterDestroy); + expect(calls).not.toContain("reset-shared-state"); + }); +}); diff --git a/tests/previewSync.test.ts b/tests/previewSync.test.ts new file mode 100644 index 0000000..8a37016 --- /dev/null +++ b/tests/previewSync.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, expect, test } from "bun:test"; +import { DEFAULT_CHAT_CONFIG } from "../src/config/chatUrlParams"; +import { createPreviewSynchronizer } from "../src/features/setup/previewSync"; + +/** + * The synchronizer reads `window` lazily, inside its functions, so a minimal + * shim is enough to exercise it outside a browser. This is deliberately not a + * DOM implementation: only timers and `location.origin` are needed. + */ +type PendingTimer = { id: number; run: () => void; cancelled: boolean }; + +let pending: PendingTimer[] = []; +let nextTimerId = 1; + +(globalThis as { window?: unknown }).window = { + setTimeout: (run: () => void) => { + const id = nextTimerId++; + pending.push({ id, run, cancelled: false }); + return id; + }, + clearTimeout: (id: number) => { + const timer = pending.find((entry) => entry.id === id); + if (timer) timer.cancelled = true; + }, + location: { origin: "https://setup.test" }, +}; + +/** Runs every timer that was not cancelled, as a real event loop would. */ +function flushTimers() { + const due = pending.filter((entry) => !entry.cancelled); + pending = []; + for (const entry of due) entry.run(); +} + +type FakeIframe = { + src: string; + contentWindow: { postMessage: (message: unknown, origin: string) => void }; +}; + +function createIframe(): FakeIframe & { posted: Array<{ message: unknown; origin: string }> } { + const posted: Array<{ message: unknown; origin: string }> = []; + return { + src: "", + posted, + contentWindow: { + postMessage: (message, origin) => posted.push({ message, origin }), + }, + }; +} + +beforeEach(() => { + pending = []; +}); + +test("the first session key navigates and a repeat of it does not", () => { + const iframe = createIframe(); + const sync = createPreviewSynchronizer({ + getIframe: () => iframe as unknown as HTMLIFrameElement, + setFallbackUrl: () => {}, + }); + + expect(sync.scheduleNavigation("a", () => "https://app.test/chat/?c=a")).toBe(true); + expect(sync.scheduleNavigation("a", () => "https://app.test/chat/?c=ignored")).toBe(false); + + flushTimers(); + expect(iframe.src).toBe("https://app.test/chat/?c=a"); +}); + +test("a burst of session keys collapses into one navigation with the last url", () => { + const iframe = createIframe(); + const sync = createPreviewSynchronizer({ + getIframe: () => iframe as unknown as HTMLIFrameElement, + setFallbackUrl: () => {}, + }); + const built: string[] = []; + + const build = (key: string) => () => { + built.push(key); + return `https://app.test/chat/?c=${key}`; + }; + + expect(sync.scheduleNavigation("a", build("a"))).toBe(true); + expect(sync.scheduleNavigation("b", build("b"))).toBe(true); + expect(sync.scheduleNavigation("c", build("c"))).toBe(true); + + // Nothing is built or navigated until the debounce elapses. + expect(built).toEqual([]); + expect(iframe.src).toBe(""); + + flushTimers(); + expect(built).toEqual(["c"]); + expect(iframe.src).toBe("https://app.test/chat/?c=c"); +}); + +test("dispose cancels a pending navigation", () => { + const iframe = createIframe(); + const sync = createPreviewSynchronizer({ + getIframe: () => iframe as unknown as HTMLIFrameElement, + setFallbackUrl: () => {}, + }); + + sync.scheduleNavigation("a", () => "https://app.test/chat/?c=a"); + sync.dispose(); + flushTimers(); + + expect(iframe.src).toBe(""); +}); + +test("the fallback url setter is used before the iframe exists", () => { + const fallbacks: string[] = []; + const sync = createPreviewSynchronizer({ + getIframe: () => undefined, + setFallbackUrl: (url) => fallbacks.push(url), + }); + + sync.scheduleNavigation("a", () => "https://app.test/chat/?c=a"); + flushTimers(); + + expect(fallbacks).toEqual(["https://app.test/chat/?c=a"]); +}); + +test("postConfig posts the preview message to the app origin", () => { + const iframe = createIframe(); + const sync = createPreviewSynchronizer({ + getIframe: () => iframe as unknown as HTMLIFrameElement, + setFallbackUrl: () => {}, + }); + + sync.postConfig(DEFAULT_CHAT_CONFIG); + + expect(iframe.posted).toHaveLength(1); + expect(iframe.posted[0]?.origin).toBe("https://setup.test"); + expect(iframe.posted[0]?.message).toMatchObject({ + type: "chatyx:preview-config", + config: DEFAULT_CHAT_CONFIG, + }); +}); + +test("postConfig is a no-op without an iframe", () => { + const sync = createPreviewSynchronizer({ + getIframe: () => undefined, + setFallbackUrl: () => {}, + }); + + expect(() => sync.postConfig(DEFAULT_CHAT_CONFIG)).not.toThrow(); +}); + +test("a custom debounce delay is honoured", () => { + const iframe = createIframe(); + const sync = createPreviewSynchronizer({ + getIframe: () => iframe as unknown as HTMLIFrameElement, + setFallbackUrl: () => {}, + debounceMs: 0, + }); + + sync.scheduleNavigation("a", () => "https://app.test/chat/?c=a"); + flushTimers(); + + expect(iframe.src).toBe("https://app.test/chat/?c=a"); +}); diff --git a/tests/renderMessageContent.test.ts b/tests/renderMessageContent.test.ts index 6ab4fb8..74d68d8 100644 --- a/tests/renderMessageContent.test.ts +++ b/tests/renderMessageContent.test.ts @@ -5,6 +5,7 @@ import type { ChatPresentationService } from "../src/services/chat/chatPresentat import { mentionStyleService } from "../src/services/chat"; import type { TwitchMessage } from "../src/services/chat/twitch/twitchService"; import { createMessageTokenSnapshot } from "../src/utils/chat/emojiUtils"; +import { networkClient } from "../src/services/network/networkClient"; import { setRteProxyEnabled } from "../src/services/network/rteProxyTransport"; function message(text: string, positions: string[]): TwitchMessage { @@ -28,6 +29,7 @@ function render( message: TwitchMessage, displayText?: string, config = DEFAULT_CHAT_CONFIG, + resolveUrl = networkClient.resolveHttpUrl, ): string { const previousDocument = Object.getOwnPropertyDescriptor(globalThis, "document"); const element = { @@ -50,9 +52,12 @@ function render( }, }, }); - const rendered = displayText === undefined - ? renderMessageWithEmotes(message, config, service) - : renderMessageWithEmotes(message, config, service, displayText); + const rendered = renderMessageWithEmotes(message, config, service, { + displayText, + // The renderer no longer imports a network client, so the test passes the + // real resolver explicitly and keeps exercising the RTE rewrite. + resolveUrl, + }); expect(rendered).toBe(element); return element.innerHTML; } finally { @@ -247,3 +252,29 @@ describe("renderMessageWithEmotes display text", () => { expect(html).toContain("@viewer"); }); }); + +describe("renderMessageWithEmotes url resolution", () => { + test("routes every asset url through the injected resolver", () => { + const seen: string[] = []; + const html = render(message(":D", ["0-1"]), undefined, DEFAULT_CHAT_CONFIG, (url) => { + seen.push(url); + return `https://stub.test/${seen.length}`; + }); + + expect(seen).toHaveLength(1); + expect(seen[0]).toContain("/emoticons/v2/25/"); + expect(html).toContain("https://stub.test/1"); + expect(html).not.toContain("static-cdn.jtvnw.net"); + }); + + test("renders without any resolver side effects when nothing needs resolving", () => { + const calls: string[] = []; + const html = render(message("plain text", []), undefined, DEFAULT_CHAT_CONFIG, (url) => { + calls.push(url); + return url; + }); + + expect(calls).toEqual([]); + expect(html).toContain("plain text"); + }); +}); diff --git a/tests/serviceReset.test.ts b/tests/serviceReset.test.ts new file mode 100644 index 0000000..93819f5 --- /dev/null +++ b/tests/serviceReset.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from "bun:test"; +import { badgeService } from "../src/services/badges/badgeService"; +import { emoteService } from "../src/services/chat/assets/emoteService"; + +const emote = (name: string) => ({ + id: `id-${name}`, + name, + url: `https://example.test/${name}.png`, + source: "bttv" as const, +}); + +test("emoteService.reset clears channel and personal emotes", () => { + emoteService.addChannelEmote("channel-a", "Kappa", emote("Kappa")); + emoteService.addPersonalEmote("viewer", "Mine", emote("Mine")); + + expect(emoteService.getEmote("Kappa", "channel-a")).toBeDefined(); + expect(emoteService.getPersonalEmotes("viewer").Mine).toBeDefined(); + + emoteService.reset(); + + expect(emoteService.getEmote("Kappa", "channel-a")).toBeUndefined(); + expect(emoteService.getPersonalEmotes("viewer").Mine).toBeUndefined(); + expect(emoteService.getEmoteData().emotes).toEqual({}); + expect(emoteService.getEmoteData().channelEmotes).toEqual({}); + expect(emoteService.getEmoteData().personalEmotes).toEqual({}); +}); + +test("badgeService.reset keeps the built-in fallback badges", () => { + // The fallbacks are the reason reset cannot simply empty the store: a fresh + // service starts with them, so a reset service must too. + const before = badgeService.getTwitchBadge("moderator", "1"); + expect(before).toBeTruthy(); + + badgeService.reset(); + + expect(badgeService.getTwitchBadge("moderator", "1")).toBe(before); +}); + +test("badgeService.reset drops badges added at runtime", () => { + badgeService.addSevenTVBadge("probe-badge", { host: { url: "//cdn.example.test" } }); + expect(Object.keys(badgeService.getBadgeData().seventvBadges)).toContain("probe-badge"); + expect(badgeService.getBadgeData().badges["7tv:probe-badge"]).toBeTruthy(); + + badgeService.reset(); + + expect(Object.keys(badgeService.getBadgeData().seventvBadges)).toHaveLength(0); + expect(badgeService.getBadgeData().badges["7tv:probe-badge"]).toBeUndefined(); +}); + +test("a reset badge store does not leak state into the next reset", () => { + badgeService.addSevenTVBadge("first", { host: { url: "//cdn.example.test" } }); + badgeService.reset(); + badgeService.addSevenTVBadge("second", { host: { url: "//cdn.example.test" } }); + badgeService.reset(); + + // If reset snapshotted the live store instead of the initial one, the second + // reset would restore "first" as well. + expect(Object.keys(badgeService.getBadgeData().seventvBadges)).toHaveLength(0); +}); diff --git a/tests/setupConfig.test.ts b/tests/setupConfig.test.ts new file mode 100644 index 0000000..54ad6fa --- /dev/null +++ b/tests/setupConfig.test.ts @@ -0,0 +1,235 @@ +import { expect, test } from "bun:test"; +import { + DEFAULT_CHAT_CONFIG, + parseBotNames, + parseChatConfigFromSearchParams, +} from "../src/config/chatUrlParams"; +import { DEFAULT_EVENT_COLORS } from "../src/config/eventColors"; +import { + buildOverlayUrl, + buildSetupConfig, + type SetupFormState, +} from "../src/config/setupConfig"; +import { MIN_MESSAGE_SPEED } from "../src/config/chatAnimation"; + +/** Mirrors how the route seeds an `intOrFalse` field as a form string. */ +const optionalIntToForm = (value: number | false): string => + value === false ? "0" : String(value); + +/** A form at its defaults, so each test only states what it changes. */ +function createForm(overrides: Partial = {}): SetupFormState { + return { + youtubeChannel: "", + kickChannel: "", + size: String(DEFAULT_CHAT_CONFIG.size), + font: String(DEFAULT_CHAT_CONFIG.font), + lineHeight: String(DEFAULT_CHAT_CONFIG.lineHeight), + fontWeight: String(DEFAULT_CHAT_CONFIG.fontWeight), + nickFontWeight: String(DEFAULT_CHAT_CONFIG.nickFontWeight), + fontCustom: "", + shadow: optionalIntToForm(DEFAULT_CHAT_CONFIG.shadow), + stroke: optionalIntToForm(DEFAULT_CHAT_CONFIG.stroke), + fade: optionalIntToForm(DEFAULT_CHAT_CONFIG.fade), + animation: DEFAULT_CHAT_CONFIG.animation, + messageSpeed: String(DEFAULT_CHAT_CONFIG.messageSpeed), + showHomies: DEFAULT_CHAT_CONFIG.showHomies, + show7tvBadges: DEFAULT_CHAT_CONFIG.show7tvBadges, + showFfzBadges: DEFAULT_CHAT_CONFIG.showFfzBadges, + showBttvBadges: DEFAULT_CHAT_CONFIG.showBttvBadges, + showChatterinoBadges: DEFAULT_CHAT_CONFIG.showChatterinoBadges, + showChatisBadges: DEFAULT_CHAT_CONFIG.showChatisBadges, + showTwitchBadges: DEFAULT_CHAT_CONFIG.showTwitchBadges, + showYouTubeBadges: DEFAULT_CHAT_CONFIG.showYouTubeBadges, + showKickBadges: DEFAULT_CHAT_CONFIG.showKickBadges, + recentMessages: DEFAULT_CHAT_CONFIG.recentMessages, + bots: DEFAULT_CHAT_CONFIG.bots, + commands: DEFAULT_CHAT_CONFIG.commands, + hideAllBadges: DEFAULT_CHAT_CONFIG.hideAllBadges, + emoteScale: String(DEFAULT_CHAT_CONFIG.emoteScale), + showGifs: DEFAULT_CHAT_CONFIG.showGifs, + gifScale: String(DEFAULT_CHAT_CONFIG.gifScale), + botNames: parseBotNames(DEFAULT_CHAT_CONFIG.botNames), + kickBotNames: parseBotNames(DEFAULT_CHAT_CONFIG.kickBotNames), + youtubeBotNames: parseBotNames(DEFAULT_CHAT_CONFIG.youtubeBotNames), + allowedChatters: [], + show7tvUnlisted: DEFAULT_CHAT_CONFIG.show7tvUnlisted, + smallCaps: DEFAULT_CHAT_CONFIG.smallCaps, + nlAfterName: DEFAULT_CHAT_CONFIG.nlAfterName, + hideNames: DEFAULT_CHAT_CONFIG.hideNames, + reverseLineOrder: DEFAULT_CHAT_CONFIG.reverseLineOrder, + horizontal: DEFAULT_CHAT_CONFIG.horizontal, + platformMarker: DEFAULT_CHAT_CONFIG.platformMarker, + ffzBotMixBroadcaster: DEFAULT_CHAT_CONFIG.ffzBotMixBroadcaster, + ffzBotMixModerator: DEFAULT_CHAT_CONFIG.ffzBotMixModerator, + ffzBotMixVip: DEFAULT_CHAT_CONFIG.ffzBotMixVip, + overlayBackgroundColor: DEFAULT_CHAT_CONFIG.overlayBackgroundColor, + overlayBackgroundOpacity: String(DEFAULT_CHAT_CONFIG.overlayBackgroundOpacity), + overlayBackgroundRadius: String(DEFAULT_CHAT_CONFIG.overlayBackgroundRadius), + overlayPadding: String(DEFAULT_CHAT_CONFIG.overlayPadding), + overlayBorderWidth: String(DEFAULT_CHAT_CONFIG.overlayBorderWidth), + overlayBorderColor: DEFAULT_CHAT_CONFIG.overlayBorderColor, + highlightTwitchEvents: DEFAULT_CHAT_CONFIG.highlightTwitchEvents, + eventColorOpacity: String(DEFAULT_CHAT_CONFIG.eventColorOpacity), + eventColors: { ...DEFAULT_EVENT_COLORS }, + twitchEventBold: DEFAULT_CHAT_CONFIG.twitchEventBold, + twitchEventItalic: DEFAULT_CHAT_CONFIG.twitchEventItalic, + showHighlightedMessages: DEFAULT_CHAT_CONFIG.showHighlightedMessages, + showChannelPointRewards: DEFAULT_CHAT_CONFIG.showChannelPointRewards, + showGigantifiedEmotes: DEFAULT_CHAT_CONFIG.showGigantifiedEmotes, + showPredictions: DEFAULT_CHAT_CONFIG.showPredictions, + linkMode: DEFAULT_CHAT_CONFIG.linkMode, + linkColor: DEFAULT_CHAT_CONFIG.linkColor, + usersColorEnabled: false, + usersColor: "#ffffff", + hideLinkRewards: DEFAULT_CHAT_CONFIG.hideLinkRewards, + rteProxy: DEFAULT_CHAT_CONFIG.rteProxy, + rteAzureTts: DEFAULT_CHAT_CONFIG.rteAzureTts, + rteChatIsTts: DEFAULT_CHAT_CONFIG.rteChatIsTts, + rteReyohohoBadge: DEFAULT_CHAT_CONFIG.rteReyohohoBadge, + rteCustomCosmetics: DEFAULT_CHAT_CONFIG.rteCustomCosmetics, + ...overrides, + }; +} + +test("a default form projects the default config", () => { + expect(buildSetupConfig(createForm(), "somechannel")).toEqual({ + ...DEFAULT_CHAT_CONFIG, + channel: "somechannel", + }); +}); + +test("buildSetupConfig trims and de-channels the secondary platforms", () => { + const config = buildSetupConfig( + createForm({ youtubeChannel: " @SomeYouTube ", kickChannel: "@SomeKick" }), + "twitchchannel", + ); + + expect(config.youtubeChannel).toBe("SomeYouTube"); + expect(config.kickChannel).toBe("SomeKick"); + expect(config.channel).toBe("twitchchannel"); +}); + +test("buildSetupConfig coerces out-of-range and malformed form input", () => { + const config = buildSetupConfig( + createForm({ + lineHeight: "900", + fontWeight: "abc", + messageSpeed: "-40", + shadow: "0", + stroke: "6", + fade: "-1", + emoteScale: "oops", + overlayBackgroundColor: "not-a-colour", + overlayBorderColor: "#11223380", + linkColor: "#AABBCC", + usersColorEnabled: true, + usersColor: "#00ff00", + botNames: [" Alpha ", "BETA"], + allowedChatters: ["Viewer"], + }), + "c", + ); + + expect(config.lineHeight).toBe(200); + expect(config.fontWeight).toBe(DEFAULT_CHAT_CONFIG.fontWeight); + expect(config.messageSpeed).toBe(MIN_MESSAGE_SPEED); + expect(config.shadow).toBe(false); + expect(config.stroke).toBe(6); + expect(config.fade).toBe(false); + expect(config.emoteScale).toBe(DEFAULT_CHAT_CONFIG.emoteScale); + expect(config.overlayBackgroundColor).toBe( + DEFAULT_CHAT_CONFIG.overlayBackgroundColor, + ); + expect(config.overlayBorderColor).toBe("#11223380"); + expect(config.linkColor).toBe("#AABBCC"); + expect(config.usersColor).toBe("#00ff00"); + expect(config.botNames).toBe("alpha,beta"); + expect(config.singleChatter).toBe("viewer"); +}); + +test("bot lists are normalized but not de-duplicated by the projection", () => { + // De-duplication is the chip list's job (mergeUniqueLogins); the projection + // only normalizes, which is what the URL round trip relies on. + const config = buildSetupConfig( + createForm({ botNames: [" Alpha ", "BETA", "alpha"] }), + "c", + ); + expect(config.botNames).toBe("alpha,beta,alpha"); +}); + +test("usersColor is cleared when the uniform nickname colour is off", () => { + const config = buildSetupConfig( + createForm({ usersColorEnabled: false, usersColor: "#00ff00" }), + "c", + ); + expect(config.usersColor).toBe(""); +}); + +test("buildOverlayUrl keeps the message speed unless the preview asks otherwise", () => { + const config = buildSetupConfig(createForm({ messageSpeed: "5" }), "chan"); + const base = { baseUrl: "https://example.test" }; + + const exported = buildOverlayUrl(config, undefined, base); + expect(exported.startsWith("https://example.test/chat/?")).toBe(true); + expect(new URL(exported).searchParams.get("ms")).toBe("5"); + + const preview = buildOverlayUrl(config, undefined, { + ...base, + includeMessageSpeed: false, + }); + expect(new URL(preview).searchParams.get("ms")).toBeNull(); +}); + +test("buildOverlayUrl lets extra params win over the projected config", () => { + const config = buildSetupConfig(createForm(), "chan"); + const url = buildOverlayUrl( + config, + { preview: "true", c: "override" }, + { baseUrl: "https://example.test" }, + ); + const params = new URL(url).searchParams; + + expect(params.get("preview")).toBe("true"); + expect(params.get("c")).toBe("override"); +}); + +test("the exported url round-trips through the overlay parser", () => { + const config = buildSetupConfig( + createForm({ + size: "3", + font: "7", + lineHeight: "170", + fontWeight: "700", + emoteScale: "1.4", + gifScale: "0.8", + fade: "12", + shadow: "3", + showGifs: true, + horizontal: true, + botNames: ["alpha"], + kickBotNames: ["kickbot"], + youtubeBotNames: ["ytbot"], + allowedChatters: ["viewer"], + }), + "roundtrip", + ); + + const url = buildOverlayUrl(config, undefined, { baseUrl: "https://example.test" }); + const parsed = parseChatConfigFromSearchParams(new URL(url).searchParams); + + expect(parsed.channel).toBe("roundtrip"); + expect(parsed.size).toBe(3); + expect(parsed.font).toBe(7); + expect(parsed.lineHeight).toBe(170); + expect(parsed.fontWeight).toBe(700); + expect(parsed.emoteScale).toBe(1.4); + expect(parsed.gifScale).toBe(0.8); + expect(parsed.fade).toBe(12); + expect(parsed.shadow).toBe(3); + expect(parsed.showGifs).toBe(true); + expect(parsed.horizontal).toBe(true); + expect(parsed.botNames).toBe("alpha"); + expect(parsed.kickBotNames).toBe("kickbot"); + expect(parsed.youtubeBotNames).toBe("ytbot"); + expect(parsed.singleChatter).toBe("viewer"); +}); diff --git a/tests/setupTranslations.test.ts b/tests/setupTranslations.test.ts index b073d9f..6d4afe4 100644 --- a/tests/setupTranslations.test.ts +++ b/tests/setupTranslations.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { resolveSetupText, type SetupText } from "~/components/setup/SetupLayout"; +import { resolveSetupText, type SetupText } from "../src/components/setup/setupText"; import { setLocale, t } from "~/i18n"; test("resolves setup labels with the active locale", () => { diff --git a/vite.config.ts b/vite.config.ts index f78d6b5..5194205 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -47,7 +47,7 @@ export default defineConfig({ // than the framework, so give the UI stack its own cache boundary. { name: "ui", - test: /[\\/]node_modules[\\/](?:@kobalte|@corvu|@floating-ui|@internationalized|@solid-primitives|lucide-solid|solid-presence|solid-prevent-scroll)[\\/]/, + test: /[\\/]node_modules[\\/](?:@kobalte|@corvu|@floating-ui|@internationalized|@solid-primitives|@hugeicons|solid-presence|solid-prevent-scroll)[\\/]/, priority: 20, }, // Tiny utility packages are shared broadly but are independent of