From 4bbf6892842990e8021b60bbcc4d27f9acd9ec10 Mon Sep 17 00:00:00 2001 From: ShashiSubramanya Date: Wed, 7 Oct 2026 16:06:50 +0530 Subject: [PATCH 1/5] feat: show hero gradient in nested version site under the public wrapper The nested version site always runs in embedded mode, which hides the radiance blobs and uses the flat in-product banner. VersionIframe now passes _public=1 when the wrapper is the public docs site, and the nested site keeps the public hero for _iframe=1&_public=1. In-product help is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- src/assets/styles/index.scss | 16 ++++++++++++++++ src/components/DevDocTemplate/index.tsx | 14 +++++++++++++- src/components/VersionIframe/index.tsx | 6 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 32e85f14d..9a3572d6d 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -1546,6 +1546,22 @@ a.anchor { color: #fff; } } + + // Nested version site shown inside the public docs wrapper (?_iframe=1&_public=1): keep the + // public hero, i.e. undo the in-product banner overrides above. + &.embedded-public-hero { + .radiance-bg { + display: block; + } + + .header-banner { + background: none; + + .header-banner-text { + color: var(--primary-color); + } + } + } } .introCard, diff --git a/src/components/DevDocTemplate/index.tsx b/src/components/DevDocTemplate/index.tsx index e3760503f..74f0a41c2 100644 --- a/src/components/DevDocTemplate/index.tsx +++ b/src/components/DevDocTemplate/index.tsx @@ -663,8 +663,20 @@ if (isVersionedIframe) { return cName; }; + // Nested version site (?_iframe=1) whose wrapper is the public docs site (VersionIframe adds + // ?_public=1), as opposed to a wrapper embedded in the product. It is still "embedded" for + // layout/nav, but keeps the public hero (gradient) instead of the flat in-product banner. + const isPublicWrapperIframe = () => { + if (!isBrowser()) return false; + const urlParams = new URLSearchParams(location.search); + return urlParams.get('_iframe') === '1' && urlParams.get('_public') === '1'; + }; + const getWrapperClassName = () => { - return isEmbeddedContext() ? 'embedded-mode' : ''; + if (!isEmbeddedContext()) return ''; + return isPublicWrapperIframe() + ? 'embedded-mode embedded-public-hero' + : 'embedded-mode'; }; const getCloudLatestVersion = () => { diff --git a/src/components/VersionIframe/index.tsx b/src/components/VersionIframe/index.tsx index e6e23eaa3..c90de598c 100644 --- a/src/components/VersionIframe/index.tsx +++ b/src/components/VersionIframe/index.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { isPublicSite } from '../../utils/app-utils'; import './index.scss'; interface VersionIframeProps { @@ -38,6 +39,11 @@ const VersionIframe: React.FC = ({ url.hash = location?.hash; } url.searchParams.set('_iframe', '1'); + // Tell the nested site its wrapper is the public docs site (not the product), so it + // can show the public hero gradient instead of the in-product banner. + if (isPublicSite(window.location.search)) { + url.searchParams.set('_public', '1'); + } } return url.toString(); From e755b4f8ab01377be0d9ddbac443dc16bc6b7047 Mon Sep 17 00:00:00 2001 From: ShashiSubramanya <76986173+ShashiSubramanya@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:13:32 +0530 Subject: [PATCH 2/5] Update doc-configs.js --- src/configs/doc-configs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/configs/doc-configs.js b/src/configs/doc-configs.js index 99c3cd8e7..0e414264c 100644 --- a/src/configs/doc-configs.js +++ b/src/configs/doc-configs.js @@ -56,8 +56,8 @@ module.exports = { { label: '26.9.0.cl', link: '26.9.0.cl', - subLabel: 'Cloud (Latest)', - iframeUrl: 'https://developer-docs-26-10-0-cl.vercel.app/docs/', + subLabel: 'Cloud', + iframeUrl: 'https://developer-docs-26-9-0-cl.vercel.app/docs/', }, { label: '26.8.0.cl', From fb224dfd606f342562cfdc8bb97d4ef1c47a29cf Mon Sep 17 00:00:00 2001 From: ShashiSubramanya Date: Thu, 8 Oct 2026 16:10:44 +0530 Subject: [PATCH 3/5] fix: show hero gradient in embedded/in-product mode, drop _public flag Design confirmed the radiance gradient should show everywhere, so remove the .embedded-mode overrides that hid it and forced the flat #232F43 banner. This supersedes the _public=1 approach from 4bbf6892, so that plumbing (VersionIframe flag, embedded-public-hero class) is reverted. The walkthroughs hero keeps its navy override. Co-Authored-By: Claude Sonnet 5.5 --- src/assets/styles/index.scss | 35 +++---------------------- src/components/DevDocTemplate/index.tsx | 14 +--------- src/components/VersionIframe/index.tsx | 6 ----- 3 files changed, 4 insertions(+), 51 deletions(-) diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 9a3572d6d..b05a6e1e8 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -1522,23 +1522,10 @@ a.anchor { } } -// In-product presentation only: override banner color to match product sidebar (#232F43), -// and suppress the page-wide gradient entirely. +// In-product presentation only: the walkthroughs hero keeps the product sidebar color (#232F43). +// The docs header banner (.header-banner) intentionally keeps the same radiance gradient as the +// public site, so it is not overridden here. .embedded-mode { - .radiance-bg { - display: none; - } - - .header-banner { - background: #232F43; - border-bottom: none; - box-shadow: none; - - .header-banner-text { - color: #fff; - } - } - .walkthroughs-page { background: #232F43; @@ -1546,22 +1533,6 @@ a.anchor { color: #fff; } } - - // Nested version site shown inside the public docs wrapper (?_iframe=1&_public=1): keep the - // public hero, i.e. undo the in-product banner overrides above. - &.embedded-public-hero { - .radiance-bg { - display: block; - } - - .header-banner { - background: none; - - .header-banner-text { - color: var(--primary-color); - } - } - } } .introCard, diff --git a/src/components/DevDocTemplate/index.tsx b/src/components/DevDocTemplate/index.tsx index 74f0a41c2..e3760503f 100644 --- a/src/components/DevDocTemplate/index.tsx +++ b/src/components/DevDocTemplate/index.tsx @@ -663,20 +663,8 @@ if (isVersionedIframe) { return cName; }; - // Nested version site (?_iframe=1) whose wrapper is the public docs site (VersionIframe adds - // ?_public=1), as opposed to a wrapper embedded in the product. It is still "embedded" for - // layout/nav, but keeps the public hero (gradient) instead of the flat in-product banner. - const isPublicWrapperIframe = () => { - if (!isBrowser()) return false; - const urlParams = new URLSearchParams(location.search); - return urlParams.get('_iframe') === '1' && urlParams.get('_public') === '1'; - }; - const getWrapperClassName = () => { - if (!isEmbeddedContext()) return ''; - return isPublicWrapperIframe() - ? 'embedded-mode embedded-public-hero' - : 'embedded-mode'; + return isEmbeddedContext() ? 'embedded-mode' : ''; }; const getCloudLatestVersion = () => { diff --git a/src/components/VersionIframe/index.tsx b/src/components/VersionIframe/index.tsx index c90de598c..e6e23eaa3 100644 --- a/src/components/VersionIframe/index.tsx +++ b/src/components/VersionIframe/index.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { isPublicSite } from '../../utils/app-utils'; import './index.scss'; interface VersionIframeProps { @@ -39,11 +38,6 @@ const VersionIframe: React.FC = ({ url.hash = location?.hash; } url.searchParams.set('_iframe', '1'); - // Tell the nested site its wrapper is the public docs site (not the product), so it - // can show the public hero gradient instead of the in-product banner. - if (isPublicSite(window.location.search)) { - url.searchParams.set('_public', '1'); - } } return url.toString(); From 884c092979fded462c5a19fc57a065e2a3aa0b6b Mon Sep 17 00:00:00 2001 From: ShashiSubramanya Date: Fri, 9 Oct 2026 13:17:20 +0530 Subject: [PATCH 4/5] feat: use the Radiance home pattern for the docs hero gradient The hero used hand-written CSS blobs with keyframe loops, so light and dark looked like different gradients and did not match the product home page. Use the product's Radiance 'home' pattern instead: the unmodified v2.0.0 renderer and home recipe CSS (vendored, byte-identical to the Radiance language repo), with the site theme mapped to data-radiance-appearance. The renderer is loaded lazily, only on pages that contain a surface. The walkthroughs hero keeps its existing CSS blobs. Co-Authored-By: Claude Sonnet 5.5 --- modules/ROOT/pages/home.adoc | 14 +- src/assets/styles/radiance-home.css | 142 ++ src/components/DevDocTemplate/index.tsx | 5 + src/index.d.ts | 3 + src/utils/radiance.ts | 64 + static/radiance/radiance-renderer.js | 2514 +++++++++++++++++++++++ 6 files changed, 2731 insertions(+), 11 deletions(-) create mode 100644 src/assets/styles/radiance-home.css create mode 100644 src/utils/radiance.ts create mode 100644 static/radiance/radiance-renderer.js diff --git a/modules/ROOT/pages/home.adoc b/modules/ROOT/pages/home.adoc index f0f150e82..f6e036d4a 100644 --- a/modules/ROOT/pages/home.adoc +++ b/modules/ROOT/pages/home.adoc @@ -7,17 +7,9 @@ ++++
-
-
-
-
-
-
-
-
-
-
-
+
+

ThoughtSpot Embedded Documentation

diff --git a/src/assets/styles/radiance-home.css b/src/assets/styles/radiance-home.css new file mode 100644 index 000000000..1c8c3947b --- /dev/null +++ b/src/assets/styles/radiance-home.css @@ -0,0 +1,142 @@ +/* + * Radiance product pattern "home" - the Home page hero recipe (FIXED, admin-owned). + * + * The finished Home page (Spotter home) draws its hero from two layers: + * .radiance-home the Radiance surface's wrapper - bleeds past the hero, + * fades toward the content, and carries the fine noise + * and tone lift at the language's values (GRAIN-2) + * .radiance-home-stage the hero's container: a faint glow layer (::before) + * The grain is the language's one grain per theme (GRAIN-6): the renderer draws + * it, over Home's colour at the hero's strength (PRODUCT_PATTERNS.home). + * Markup (see README.md): + *
+ * + * ...hero content... + *
+ * The renderer draws the top wash itself (subtle, fast; locked). Nothing here is + * tunable - these values are the product pattern (RULES.md PRODUCT-1..5). The + * glow layer follows the clean-blend order (COLOR-5): pink - violet - blue - mint. + * Theme follows the surface's own data-radiance-appearance (read with :has()). + */ + +.radiance-home-stage { + --radiance-home-base: #ffffff; + position: relative; +} + +.radiance-home-stage:has(.radiance-local[data-radiance-appearance="dark"]) { + --radiance-home-base: #131416; +} + +/* The glow layer under the hero. */ +.radiance-home-stage::before { + content: ""; + position: absolute; + z-index: 0; + top: 0; + left: -16vw; + right: -16vw; + height: 640px; + pointer-events: none; + background: + radial-gradient(ellipse 58% 36% at 22% -4%, rgba(255, 118, 154, 0.072), rgba(255, 118, 154, 0.028) 44%, rgba(255, 118, 154, 0.006) 64%, transparent 88%), + radial-gradient(ellipse 66% 38% at 38% -2%, rgba(165, 102, 255, 0.066), rgba(165, 102, 255, 0.026) 38%, rgba(165, 102, 255, 0.006) 64%, transparent 88%), + radial-gradient(ellipse 72% 42% at 62% 2%, rgba(88, 150, 255, 0.076), rgba(88, 150, 255, 0.032) 42%, rgba(88, 150, 255, 0.006) 66%, transparent 90%), + radial-gradient(ellipse 70% 44% at 84% 4%, rgba(80, 220, 180, 0.048), rgba(80, 220, 180, 0.017) 40%, transparent 78%), + linear-gradient(to bottom, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.16) 42%, rgba(255, 255, 255, 0.56) 78%, var(--radiance-home-base) 100%); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.94) 26%, rgba(0, 0, 0, 0.54) 58%, rgba(0, 0, 0, 0.16) 84%, transparent 100%); + mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.94) 26%, rgba(0, 0, 0, 0.54) 58%, rgba(0, 0, 0, 0.16) 84%, transparent 100%); +} + +.radiance-home-stage:has(.radiance-local[data-radiance-appearance="dark"])::before { + height: 560px; + background: + radial-gradient(ellipse 58% 34% at 20% -4%, rgba(255, 93, 142, 0.06), rgba(255, 93, 142, 0.024) 42%, rgba(255, 93, 142, 0.006) 62%, transparent 86%), + radial-gradient(ellipse 66% 38% at 38% -2%, rgba(138, 93, 255, 0.068), rgba(138, 93, 255, 0.028) 36%, rgba(138, 93, 255, 0.006) 64%, transparent 88%), + radial-gradient(ellipse 72% 42% at 62% 2%, rgba(39, 140, 255, 0.078), rgba(39, 140, 255, 0.032) 38%, rgba(39, 140, 255, 0.007) 66%, transparent 90%), + radial-gradient(ellipse 70% 44% at 84% 4%, rgba(45, 220, 177, 0.026), rgba(45, 220, 177, 0.01) 40%, transparent 78%), + linear-gradient(to bottom, rgba(26, 27, 30, 0.08) 0%, rgba(26, 27, 30, 0.24) 38%, rgba(26, 27, 30, 0.62) 70%, var(--radiance-home-base) 100%); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.96) 24%, rgba(0, 0, 0, 0.5) 54%, rgba(0, 0, 0, 0.14) 78%, transparent 100%); + mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.96) 24%, rgba(0, 0, 0, 0.5) 54%, rgba(0, 0, 0, 0.14) 78%, transparent 100%); +} + +/* The surface's wrapper: bleeds past the hero, fades toward the content, and + carries the tone lift and the 56px fine noise at the language's values. The + renderer draws Home's colour at the hero's strength and the grain on top. */ +.radiance-home { + position: absolute; + z-index: 0; + inset: -96px -12vw -220px; + overflow: hidden; + pointer-events: none; + filter: saturate(1.16) contrast(1.06); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.92) 22%, rgba(0, 0, 0, 0.52) 54%, rgba(0, 0, 0, 0.18) 78%, transparent 100%); + mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.92) 22%, rgba(0, 0, 0, 0.52) 54%, rgba(0, 0, 0, 0.18) 78%, transparent 100%); +} + +.radiance-home:has(> .radiance-local[data-radiance-appearance="dark"]) { + inset: -88px -12vw -150px; + filter: saturate(1.2) contrast(1.08); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.86) 20%, rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0.12) 70%, transparent 90%); + mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.86) 20%, rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0.12) 70%, transparent 90%); +} + +.radiance-home::before { + content: ""; + position: absolute; + z-index: 3; + inset: 0; + pointer-events: none; + opacity: 0.22; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.48' numOctaves='3' seed='21' stitchTiles='stitch'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.58' intercept='-0.18'/%3E%3CfeFuncG type='linear' slope='1.22' intercept='-0.12'/%3E%3CfeFuncB type='linear' slope='1.5' intercept='-0.16'/%3E%3CfeFuncA type='linear' slope='0.64'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='56' height='56' filter='url(%23n)'/%3E%3C/svg%3E"); + background-size: 56px 56px; + mix-blend-mode: multiply; +} + +.radiance-home:has(> .radiance-local[data-radiance-appearance="dark"])::before { + opacity: 0.27; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.48' numOctaves='3' seed='21' stitchTiles='stitch'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.64' intercept='-0.2'/%3E%3CfeFuncG type='linear' slope='1.14' intercept='-0.1'/%3E%3CfeFuncB type='linear' slope='1.58' intercept='-0.18'/%3E%3CfeFuncA type='linear' slope='0.7'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='56' height='56' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode: screen; +} + +.radiance-home::after { + content: ""; + position: absolute; + z-index: 2; + inset: 0; + background: + radial-gradient(ellipse 72% 34% at 50% 8%, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.034) 48%, rgba(255, 255, 255, 0) 78%), + linear-gradient(to bottom, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.16) 38%, rgba(255, 255, 255, 0.48) 70%, #ffffff 100%); +} + +.radiance-home:has(> .radiance-local[data-radiance-appearance="dark"])::after { + background: + radial-gradient(ellipse 72% 34% at 50% 8%, rgba(26, 27, 30, 0.12), rgba(26, 27, 30, 0.052) 46%, rgba(26, 27, 30, 0) 76%), + linear-gradient(to bottom, rgba(26, 27, 30, 0.06) 0%, rgba(26, 27, 30, 0.22) 34%, rgba(26, 27, 30, 0.56) 62%, #131416 100%); +} + +.radiance-home > .radiance-local { + position: absolute; + inset: 0; + overflow: hidden; +} + +.radiance-home > .radiance-local canvas { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; +} + +@media (max-width: 780px) { + .radiance-home { inset: -82px -18vw -176px; } + .radiance-home:has(> .radiance-local[data-radiance-appearance="dark"]) { inset: -76px -18vw -132px; } +} + +@media (max-width: 520px) { + .radiance-home { inset: -72px -28vw -162px; } + .radiance-home:has(> .radiance-local[data-radiance-appearance="dark"]) { inset: -68px -28vw -126px; } +} diff --git a/src/components/DevDocTemplate/index.tsx b/src/components/DevDocTemplate/index.tsx index e3760503f..0af542a6c 100644 --- a/src/components/DevDocTemplate/index.tsx +++ b/src/components/DevDocTemplate/index.tsx @@ -12,6 +12,7 @@ import { BiSearch } from '@react-icons/all-files/bi/BiSearch'; import { Analytics } from '@vercel/analytics/react'; import { Seo } from '../Seo'; import { queryStringParser, isPublicSite } from '../../utils/app-utils'; +import { useRadianceTheme } from '../../utils/radiance'; import { passThroughHandler, fetchChild, getPageIdFromHref } from '../../utils/doc-utils'; import Header from '../Header'; import SecondaryHeader, { DocCategory, CATEGORY_PAGEIDS, CATEGORY_NAV_ID } from '../SecondaryHeader'; @@ -20,6 +21,7 @@ import Docmap from '../Docmap'; import Document from '../Document'; import Search from '../Search'; import '../../assets/styles/index.scss'; +import '../../assets/styles/radiance-home.css'; import { getAlgoliaIndex } from '../../configs/algolia-search-config'; import RenderPlayGround from './playGround/RESTAPI'; import GraphQLPlayGround from './playGround/GraphQL'; @@ -130,6 +132,9 @@ const DevDocTemplate: FC = (props) => { }); const [key, setKey] = useState(''); + // Keeps the Radiance hero surface (home.adoc) in the site's current theme. + useRadianceTheme(isDarkMode); + // Pre-process all category nav HTMLs once ({{navprefix}} substitution applied to each) const processedNavMap = React.useMemo(() => Object.fromEntries( diff --git a/src/index.d.ts b/src/index.d.ts index 44350b3aa..dce9face6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,3 +2,6 @@ declare module '*.svg' { const content: string; export default content; } + +// Injected by Gatsby at build time (the site's pathPrefix, e.g. '/docs'). +declare const __PATH_PREFIX__: string; diff --git a/src/utils/radiance.ts b/src/utils/radiance.ts new file mode 100644 index 000000000..be0bfa25d --- /dev/null +++ b/src/utils/radiance.ts @@ -0,0 +1,64 @@ +import { useEffect } from 'react'; + +/** + * Glue for the Radiance "home" product pattern used by the docs hero (home.adoc). + * + * The renderer (static/radiance/radiance-renderer.js) and the hero recipe + * (src/assets/styles/radiance-home.css) are unmodified copies from the Radiance + * language repo and must stay that way (RULES.md IMPL-1, PRODUCT-3). The only thing the + * page owns is the theme: it sets data-radiance-appearance on the surface, and the + * renderer repaints on change. + */ + +const RENDERER_SRC = '/radiance/radiance-renderer.js'; +const SURFACE_SELECTOR = '.radiance-local'; + +let rendererRequested = false; + +// The renderer scans for .radiance-local elements on load and watches the DOM for new +// ones, so it is loaded once, and only when a page actually contains a surface. +const ensureRendererLoaded = () => { + if (rendererRequested || typeof document === 'undefined') return; + rendererRequested = true; + const script = document.createElement('script'); + // __PATH_PREFIX__ is injected by Gatsby (e.g. '/docs' in production, '' locally). + const prefix = typeof __PATH_PREFIX__ === 'string' ? __PATH_PREFIX__ : ''; + script.src = `${prefix}${RENDERER_SRC}`; + script.async = true; + document.body.appendChild(script); +}; + +const applyAppearance = (isDarkMode: boolean) => { + const appearance = isDarkMode ? 'dark' : 'light'; + const surfaces = document.querySelectorAll(SURFACE_SELECTOR); + surfaces.forEach((surface) => { + if (surface.dataset.radianceAppearance !== appearance) { + surface.dataset.radianceAppearance = appearance; + } + }); + if (surfaces.length) ensureRendererLoaded(); +}; + +/** + * Keeps every Radiance surface on the page in the site's current theme. Page content is + * injected after render, so new surfaces are picked up through a body observer too. + */ +export const useRadianceTheme = (isDarkMode: boolean) => { + useEffect(() => { + if (typeof document === 'undefined') return undefined; + applyAppearance(isDarkMode); + if (!('MutationObserver' in window)) return undefined; + const observer = new MutationObserver((mutations) => { + const added = mutations.some((mutation) => + Array.from(mutation.addedNodes).some( + (node) => + node instanceof Element && + (node.matches(SURFACE_SELECTOR) || node.querySelector(SURFACE_SELECTOR)), + ), + ); + if (added) applyAppearance(isDarkMode); + }); + observer.observe(document.body, { childList: true, subtree: true }); + return () => observer.disconnect(); + }, [isDarkMode]); +}; diff --git a/static/radiance/radiance-renderer.js b/static/radiance/radiance-renderer.js new file mode 100644 index 000000000..81386fffe --- /dev/null +++ b/static/radiance/radiance-renderer.js @@ -0,0 +1,2514 @@ +(() => { + /* + * Radiance Language renderer - v2. + * + * One file for every Radiance surface: the five templates (right-wash, + * left-wash, neutral, top-wash, bottom-wash) and the fixed product + * patterns (signup, home, spotterx, liveboard). v1 is frozen at + * renderer/v1/radiance-renderer.js for reference and comparison. + * + * v2 carries the system-level upgrades first built for SpotterX and + * Liveboard (see RULES.md and CHANGELOG.md): + * F1 film Built on Home's first-launch film: grain that reaches + * through faint colour, a fine second noise layer, and a + * gentle tone lift - all confined to the colour field + * (GRAIN-4), so the bare surface stays clean. + * F2 motion Organic drift: every orbit, size, stretch and tilt is 2-3 + * sines at incommensurate per-blob frequencies, and every blob + * breathes on its own rhythm (MOTION-2). + * F3 blend Clean blends: overlapping colours are wheel neighbours + * (mint - blue - violet - pink - peach); peach and mint stay + * compact accents (COLOR-5). + * Base A surface sits on its host's own background colour, per + * theme (data-radiance-base / --radiance-base, SURFACE-1). + */ + const RADIANCE_VERSION = "2.0.0"; + const LIGHT_SURFACE = "#ffffff"; + const DARK_SURFACE = "#1a1b1e"; + const RADIANCE_FILM_GRAIN_SIZE = 1.0; + const MAX_DPR = 2; + const TIME_ORIGIN_KEY = "radianceLanguageTimeOrigin"; + const MAX_TEXTURE_CACHE_ITEMS = 24; + const TAU = Math.PI * 2; + // GRAIN-4: how hard the colour field is stacked before it masks the film. + // 12 brings the film to ~80% by 12% colour and ~93% by 20% - the texture + // carries through faint colour the way Home's hero does - while the bare + // surface (under ~0.5% colour) keeps under ~6% of it. v1 used 4. + const GRAIN_MASK_SATURATION = 12; + // The mask is the blurred colour field, so it is smooth: it is built at a + // quarter of the canvas resolution and upscaled. The film stays full-res. + const MASK_SCALE = 0.25; + // F1 - the film stack per theme (built on Home's hero, as SpotterX and + // Liveboard carry it): grain strength, the dark dither, the fine noise layer + // and its blend, and the tone lift. The renderer draws all of it, masked to + // the colour field - consumers still add no CSS filter (BLEND-3). This is + // the language's one film per theme: every surface, template or product, + // draws exactly this grain (GRAIN-6). Light is at 1.0's full strength and + // dark at 0.76, so the grain still reads where the colour is subtle (the + // admin's 2.0 review). + const FILM = { + light: { grain: 1, dither: 0, overlay: 0.22, overlayBlend: "multiply", saturate: 1.16, contrast: 1.06 }, + dark: { grain: 0.76, dither: 0.42 * 0.76, overlay: 0.27, overlayBlend: "screen", saturate: 1.2, contrast: 1.08 } + }; + // The fine noise layer: SVG fractal noise rendered by the browser at the + // surface's pixel density and tiled every 56 CSS px - the same layer + // Home's hero draws over its canvas. + const FILM_OVERLAY_TILE = 56; + const FILM_OVERLAY_SVG = { + light: "", + dark: "" + }; + // F2 - every blob breathes: size and brightness swell a few per cent on the + // blob's own rhythm (ms at speed 1; the tempo follows the square root of the + // speed, so a fast pattern breathes a little quicker, never frantically). + const BREATH = { size: 0.05, glow: 0.05, periodMs: [5000, 8000] }; + + // Percentage adjustments. 100% is always the pattern's shipped value, so a + // surface that sets none of these is byte-identical to one that sets 100%. + // Ranges are what the language still reads as itself; outside them the + // renderer warns (RULES.md ADJUST-2). + const ADJUST_RANGES = { + prominence: { min: 0.5, max: 1.5, rule: "ADJUST-3" }, + spread: { min: 0.75, max: 1.25, rule: "ADJUST-4" }, + speed: { min: 0.5, max: 1.5, rule: "ADJUST-5" } + }; + + // Accepts "120%", "120" or "1.2" — all mean the same thing. + function parseAdjustment(raw) { + if (raw === undefined || raw === null || raw === "") return 1; + const text = String(raw).trim(); + const value = Number(text.replace("%", "").trim()); + if (!Number.isFinite(value) || value <= 0) return 1; + if (text.includes("%") || value > 5) return value / 100; + return value; + } + + // ------------------------------------------------------------------ + // Named patterns. These five are the only sanctioned entry points into + // the language; every Radiance surface starts from one of them. + // Canonical values are mirrored in radiance.tokens.json — change both or + // neither. See RULES.md (PLACE-1, MOTION-6). + // ------------------------------------------------------------------ + const RADIANCE_PATTERNS = { + "right-wash": { layout: "right-wash", speeds: { slow: 0.62, fast: 1.02 } }, + "left-wash": { layout: "left-wash", speeds: { slow: 0.62, fast: 1.02 } }, + "neutral": { layout: "neutral", speeds: { slow: 0.86, fast: 1.34 } }, + "top-wash": { layout: "top-wash", speeds: { slow: 0.92, fast: 1.72 } }, + "bottom-wash": { layout: "bottom-wash", speeds: { slow: 0.92, fast: 1.72 } } + }; + + const PLACEMENT_ALIASES = { + right: "right-wash", + left: "left-wash", + top: "top-wash", + bottom: "bottom-wash", + neutral: "neutral", + modal: "neutral", + developer: "neutral" + }; + + // Shorthand fallbacks are the product-safe values, not the legacy + // attribute defaults. Agents still spell the full four-part name out. + const PATTERN_FALLBACKS = { intensity: "subtle", motion: "dynamic", speed: "slow" }; + + // ------------------------------------------------------------------ + // Product patterns (RULES.md section 12). Fixed, admin-owned patterns for + // ThoughtSpot's finished product screens. They take no adjustment, no + // override and no base colour from the page: every value is locked here, + // and only the Radiance admin changes them, in this repository. + // "template" products are a locked template configuration; + // "spotter" products run the Spotter radiance engine (below). + // Mirrored in radiance.tokens.json and products/manifest.json. + // ------------------------------------------------------------------ + const PRODUCT_PATTERNS = { + signup: { + name: "Sign up", + engine: "template", + layout: "right-wash", + intensity: "prominent", + speed: { light: 1.02, dark: 1.02 }, + seed: "signin-v2", + film: "renderer", + // Under reduced motion Sign up shows right-wash's curated static + // frame, as the product always has (MOTION-7). + reducedMotion: "static", + base: { light: "#ffffff", dark: "#1a1b1e" } + }, + home: { + name: "Home page", + engine: "template", + layout: "top-wash", + intensity: "subtle", + speed: { light: 1.86, dark: 1.72 }, + seed: "home-hero", + // Home's page recipe carries the fine noise and the tone lift + // (products/home/radiance-home.css); the renderer draws the grain over + // the whole hero field, as Home was built. Home's colour sits at the + // hero's strength (76% light / 66% dark over the base), drawn here so + // the grain on top is the language's one grain at full value (GRAIN-6). + film: "page", + fieldOpacity: { light: 0.76, dark: 0.66 }, + base: { light: "#ffffff", dark: "#131416" } + }, + spotterx: { + name: "SpotterX", + engine: "spotter", + // The page recipe (products//radiance-.css) carries the film. + film: "recipe", + base: { light: "#ffffff", dark: "#131416" } + }, + liveboard: { + name: "Liveboard", + engine: "spotter", + // The page recipe (products//radiance-.css) carries the film. + film: "recipe", + base: { light: "#ffffff", dark: "#131416" } + } + }; + // data-radiance-state on a spotter product: [reasoning energy, after-answer calm]. + const PRODUCT_STATES = { idle: [0, 0], reasoning: [1, 0], answered: [0, 1] }; + // Everything a product pattern ignores (PRODUCT-2). + const PRODUCT_LOCKED_ATTRIBUTES = [ + ["radianceProminence", "data-radiance-prominence"], + ["radianceSpread", "data-radiance-spread"], + ["radianceSpeedScale", "data-radiance-speed-scale"], + ["radianceSpeed", "data-radiance-speed"], + ["radianceBase", "data-radiance-base"], + ["radianceIntensity", "data-radiance-intensity"], + ["radianceLayout", "data-radiance-layout"], + ["radianceVariant", "data-radiance-variant"], + ["radianceMotion", "data-radiance-motion"], + ["radianceDarkGlow", "data-radiance-dark-glow"], + ["radianceSeed", "data-radiance-seed"] + ]; + // The Spotter engine's locked tuning: the values SpotterX and Liveboard ship. + const SBG_TUNING = { gain: 1, lift: 0, spread: 1, wash: 1, flow: 2.5, breath: 1, calmGain: 1.5, pace: 1 }; + // The Radiance admin's Spotter lab (spotter-lab/) previews other values on + // one surface through RadianceLanguageRenderer.admin; nothing else sets them. + const ADMIN_SPOTTER = new WeakMap(); + + /* + * The Spotter radiance engine - product patterns "spotterx" and "liveboard" + * (formerly the RADIANCE-OVERRIDE proposal "Spotter radiance BG" v5). + * Radiance blobs gathered along the product's top bar, spilling soft light + * below it - drifting organically at rest, breathing while reasoning, and + * retreating to a faint leak at the top after an answer: + * + * - Blobs: ~6.4 across the view (radius 1.6 spacings, hanging 2x taller + * than wide), drifting across the width so no colour owns a region. The + * colour sequence is a random walk along the wheel (see SBG_PATH), so + * neighbours are wheel-neighbours and overlaps stay clean tints; a blob + * that wraps off-screen is reborn, so the sequence never recurs. + * - Motion that never loops (MOTION-2): every blob's position, size, + * stretch and tilt is a sum of 2-3 sines at incommensurate per-blob + * frequencies, on top of a drift whose own speed swells and eases. + * - Breathing: every blob breathes - swells and brightens - on its own + * random rhythm, so many regions inhale and exhale out of step, never in + * one spot. Barely there at rest; while reasoning the swings deepen and + * quicken (1.8-3.4s), the whole glow breathes along (3.2s, starting + * exhaled at ignition), and a second stream of light flows the other way + * along the edge, taking the colour of the light beneath it - so the + * crossings brighten colours instead of muddying them. + * - States: ONE surface; data-radiance-state = idle | reasoning | answered + * (reasoning energy, and the calm after an answer). Both ease + * on critically damped springs; the clock is INTEGRATED (clock += dt * + * speed), so every speed change is live and phase-continuous. Clock rate + * per unit of Motion energy (2.5 by default): default 1.15 (2.88), + * reasoning 1.7 (4.25), after an answer 0.32 (0.8 - a slow, visible + * drift). + * - After an answer the colour drops to a faint leak (x0.5) and the blobs + * rise behind the bar; the page contracts its CSS fade toward the top, so + * colour and grain retreat together. + * - Shape: the vertical fade is the wrapper's CSS mask (the page builds it + * from the rig), exactly as on Home - colour, grain and overlay fade + * together. The renderer adds only reasoning's extra brightness, confined + * to the top band: reasoning never brings colour further down. + * - Grain: the language's one grain per theme (FILM, GRAIN-6), over the + * whole lit canvas, under the page's CSS fine noise and tone filter. + * - Rendering: blobs at quarter resolution with the canonical blur + * (BLEND-2, scaled), padded past every edge so the blur never darkens an + * edge, upscaled onto a dpr-1 canvas shown pixelated, so the 1.0px grain + * stays crisp (GRAIN-1/3). + * + * Sanctioned product exceptions (RULES.md PRODUCT-3): state-bound energy + * (MOTION-1), an integrated per-surface clock (MOTION-4), speeds above 2.0 + * (MOTION-5), per-load randomness (IMPL-4), and the page-level film in + * products//radiance-.css (GRAIN-4/5, BLEND-3). Everything is + * locked: the values below are the only ones these products ever run. + */ + const SBG_PALETTE = { + light: { pink: [255, 120, 160], violet: [140, 100, 255], blue: [40, 140, 255], peach: [255, 180, 100], mint: [80, 220, 180] }, + dark: { pink: [255, 93, 142], violet: [138, 93, 255], blue: [39, 140, 255], peach: [255, 180, 90], mint: [45, 220, 177] } + }; + /* The colour sequence is a random walk with momentum along the wheel - + mint, blue, violet, pink, peach - so neighbours are always + wheel-neighbours (blue never meets peach, pink never meets mint: every + blend a clean tint). Cool colour always leads (COLOR-4): the walk heads + into peach only 40% of the time it reaches pink, must pass back through + violet before any second peach, never runs more than 3-4 warm blobs, + and after a warm run stays cool for at least two - so every six-blob + screenful holds at least two blues/violets and nothing ever sits warm + (the yellow-red cast pink + peach makes in dark). A blob that wraps + off-screen is reborn with the next step of the walk and fresh size, + timing and shape, so the sequence never recurs. Long-run balance + (simulated over 1M steps): blue 29%, violet 35%, pink 24%, peach 7% + (warmth), mint 5% (quiet support). */ + const SBG_PATH = ["mint", "blue", "violet", "pink", "peach"]; + /* Peach and mint stay compact accents so their glow never reaches a hue + two steps away (peach + violet = mauve, the muddiest blend). */ + const SBG_ROLE = { blue: 1.04, violet: 0.84, pink: 0.92, peach: 0.85, mint: 0.5 }; + const SBG_SIZE = { blue: 1.05, violet: 0.96, pink: 0.98, peach: 0.84, mint: 0.78 }; + /* Blob centre opacity at rest and while reasoning (before the page's + saturate/contrast filter, which carries dark a little further); `calm` + is the after-answer leak as a fraction of rest (x the rig's After-answer + intensity, 1.5 by default). + `floor` is what reasoning keeps below its bright top band; light's rest + was raised on its own, so its reasoning keeps the earlier floor and + reads exactly as before. */ + const SBG_STATES = { + // 2.0: reasoning +15% (0.235 -> 0.27, floor 0.14 -> 0.155), calm +10% (0.5 -> 0.55). + light: { rest: 0.165, reason: 0.27, calm: 0.55, floor: 0.155 }, + dark: { rest: 0.12, reason: 0.28, calm: 0.5, floor: 0.12 } + }; + const SBG_MOTION = { + speedDefault: 1.15, // clock rate per unit of Motion energy (x2.5 = 2.88) + speedReason: 1.7, // x2.5 = 4.25 + speedSettled: 0.32, // x2.5 = 0.8 - the calm after an answer: a slow, visible drift + speedCap: 8, + flow: 0.016, // drift, px per clock-ms: ~46 px/s default, ~68 reasoning, ~13 calm + wanderReason: 0.6, // blobs wander 1.6x as far while reasoning + /* Each blob breathes on its own rhythm (ms, random per blob in these + ranges) with these size / brightness swings - rest, reasoning, calm. */ + breathMs: { rest: [5000, 8000], reason: [1800, 3400], calm: [5500, 8500] }, + breathSize: { rest: 0.05, reason: 0.22, calm: 0.06 }, + breathGlow: { rest: 0.05, reason: 0.2, calm: 0.06 }, + /* While reasoning the whole glow breathes along ... */ + chorusMs: 3200, + chorusSize: 0.06, + chorusGlow: 0.08, + /* ... and a stream of light flows the other way along the edge. */ + streamFlow: -1.6, + streamGlow: 0.7, + rise: 3.6, // spring rates (rad/s): ~1.1s into reasoning, + fall: 1.45, // ~2.7s back out, + settle: 1.3 // ~3s between the default and the after-answer calm + }; + const SBG_CALM_RISE = 0.1; // after an answer the blobs rise behind the bar (x radius) + /* The fade (mirrored by the page's CSS mask builder): a short fall - the + edge light, "spread" - plus a long faint tail - "wash coverage". The + reasoning gain lives only in the top `band` px. */ + const SBG_ENVELOPE = { spread: 70, wash: 320, tail: 0.45, band: 80 }; + const SBG_TALL = 2.0; + const SBG_BASE_LIFT = -8; + const SBG_BLUR = { light: 40, dark: 64 }; + const SBG_SCALE = 0.25; + const SBG_PAD = 96; + + + const warnedElements = new WeakMap(); + + function warnOnce(element, code, message) { + let codes = warnedElements.get(element); + if (!codes) { + codes = new Set(); + warnedElements.set(element, codes); + } + if (codes.has(code)) return; + codes.add(code); + console.warn(`[Radiance ${code}] ${message}`); + } + + function resolveRadiancePattern(raw) { + const tokens = String(raw || "").toLowerCase().split(/[^a-z]+/).filter(Boolean); + if (!tokens.length) return null; + + let layout = null; + for (const token of tokens) { + if (PLACEMENT_ALIASES[token]) { + layout = PLACEMENT_ALIASES[token]; + break; + } + } + if (!layout) return null; + + const has = (token) => tokens.includes(token); + const intensity = has("prominent") ? "prominent" : (has("subtle") ? "subtle" : PATTERN_FALLBACKS.intensity); + const motion = (has("static") || has("still")) ? "static" : PATTERN_FALLBACKS.motion; + const speedName = has("fast") ? "fast" : (has("slow") ? "slow" : PATTERN_FALLBACKS.speed); + const appearance = has("dark") ? "dark" : (has("light") ? "light" : null); + + return { + name: `${layout}-${intensity}-${motion}-${speedName}`, + layout, + intensity, + motion, + speedName, + speed: RADIANCE_PATTERNS[layout].speeds[speedName], + appearance + }; + } + + const reducedMotionQuery = typeof window.matchMedia === "function" + ? window.matchMedia("(prefers-reduced-motion: reduce)") + : null; + let prefersReducedMotion = reducedMotionQuery ? reducedMotionQuery.matches : false; + + const LIGHT_BLOBS = [ + { anchorX: 0.9, anchorY: 0.4, color: [255, 120, 160], radius: 0.4, opacity: 0.6, orbitX: 60, orbitY: 80, freqX: 0.0007, freqY: 0.0005, stretchFreq: 0.001 }, + { anchorX: 0.85, anchorY: 0.7, color: [80, 220, 180], radius: 0.36, opacity: 0.32, orbitX: -70, orbitY: -90, freqX: 0.0008, freqY: 0.0006, stretchFreq: 0.0012 }, + { anchorX: 0.95, anchorY: 0.5, color: [140, 100, 255], radius: 0.45, opacity: 0.6, orbitX: 50, orbitY: 100, freqX: 0.0005, freqY: 0.0009, stretchFreq: 0.0008 }, + { anchorX: 0.75, anchorY: 0.3, color: [255, 180, 100], radius: 0.3, opacity: 0.6, orbitX: -60, orbitY: 120, freqX: 0.0009, freqY: 0.0007, stretchFreq: 0.0015 }, + { anchorX: 0.8, anchorY: 0.6, color: [40, 140, 255], radius: 0.4, opacity: 0.6, orbitX: 90, orbitY: -80, freqX: 0.0008, freqY: 0.0011, stretchFreq: 0.0009 }, + { anchorX: 0.3, anchorY: 0.3, color: [200, 220, 255], radius: 0.7, opacity: 0.15, orbitX: 100, orbitY: 50, freqX: 0.0003, freqY: 0.0004, stretchFreq: 0.0005 }, + { anchorX: 0.2, anchorY: 0.7, color: [255, 200, 220], radius: 0.8, opacity: 0.15, orbitX: -80, orbitY: -100, freqX: 0.0004, freqY: 0.0003, stretchFreq: 0.0006 }, + { anchorX: 0.5, anchorY: 0.6, color: [255, 240, 180], radius: 0.6, opacity: 0.15, orbitX: 120, orbitY: -60, freqX: 0.0005, freqY: 0.0004, stretchFreq: 0.0007 } + ]; + + function scaleBlobSet(blobs, opacityScale, orbitScale) { + return blobs.map((blob) => ({ + ...blob, + opacity: blob.opacity * opacityScale, + orbitX: (blob.orbitX || 0) * orbitScale, + orbitY: (blob.orbitY || 0) * orbitScale + })); + } + + function mirrorBlobSet(blobs) { + return blobs.map((blob) => ({ + ...blob, + anchorX: 1 - blob.anchorX, + orbitX: -(blob.orbitX || 0) + })); + } + + function topBlobSet(blobs) { + return blobs.map((blob) => ({ + ...blob, + anchorX: clamp(blob.anchorY, 0.08, 0.94), + anchorY: clamp(1 - blob.anchorX, -0.04, 0.34), + orbitX: (blob.orbitY || 0) * 1.18, + orbitY: -(blob.orbitX || 0) * 0.58, + radius: blob.radius * 1.06 + })); + } + + function bottomBlobSet(blobs) { + return topBlobSet(blobs).map((blob) => ({ + ...blob, + anchorY: 1 - blob.anchorY, + orbitY: -(blob.orbitY || 0) + })); + } + + function lockBlobSet(blobs) { + return blobs.map((blob) => ({ + ...blob, + orbitX: 0, + orbitY: 0, + freqX: 0, + freqY: 0, + stretchFreq: 0, + locked: true + })); + } + + function neutralBlobSet(blobs) { + return blobs.map((blob) => ({ + ...blob, + surfaceScaled: true + })); + } + + function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); + } + + const mix = (a, b, t) => a + (b - a) * t; + const wave = (phase) => 0.5 - 0.5 * Math.cos(phase); + + // SURFACE-1: a base colour is any opaque CSS colour. Parsed once through a + // canvas, so every syntax the browser accepts works. + const colourCache = new Map(); + let colourProbe = null; + + function parseColour(value) { + const text = String(value || "").trim(); + if (!text) return null; + if (colourCache.has(text)) return colourCache.get(text); + colourProbe = colourProbe || document.createElement("canvas").getContext("2d"); + let rgb = null; + for (const sentinel of ["#010203", "#fefdfc"]) { + colourProbe.fillStyle = sentinel; + colourProbe.fillStyle = text; + const normal = String(colourProbe.fillStyle); + if (normal === sentinel) continue; + if (normal.startsWith("#")) { + rgb = [1, 3, 5].map((i) => parseInt(normal.slice(i, i + 2), 16)); + } else { + const parts = (normal.match(/rgba?\(([^)]+)\)/) || [])[1]; + const values = parts ? parts.split(",").map((part) => Number(part.trim())) : []; + if (values.length >= 3 && (values.length < 4 || values[3] >= 0.99)) rgb = values.slice(0, 3).map(Math.round); + } + break; + } + if (!rgb && text.toLowerCase() === "#010203") rgb = [1, 2, 3]; + colourCache.set(text, rgb); + return rgb; + } + + function hashSeed(value) { + let hash = 2166136261; + + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + + return hash >>> 0; + } + + function createSeededRandom(seed) { + let state = hashSeed(seed) || 1; + + return () => { + state += 0x6d2b79f5; + let result = state; + result = Math.imul(result ^ (result >>> 15), result | 1); + result ^= result + Math.imul(result ^ (result >>> 7), result | 61); + return ((result ^ (result >>> 14)) >>> 0) / 4294967296; + }; + } + + function getAnimationTimeOrigin() { + const now = Date.now(); + + try { + const storedOrigin = Number(window.sessionStorage.getItem(TIME_ORIGIN_KEY)); + if (Number.isFinite(storedOrigin) && storedOrigin > 0) return storedOrigin; + + window.sessionStorage.setItem(TIME_ORIGIN_KEY, String(now)); + } catch { + return now; + } + + return now; + } + + function getBlobSurfaceScale(config, width, height) { + if (!config.surfaceScaled) return { radius: 1, orbitX: 1, orbitY: 1 }; + + const minSide = Math.max(1, Math.min(width, height)); + const maxSide = Math.max(width, height); + const aspect = maxSide / minSide; + const compactBoost = clamp((480 - minSide) / 280, 0, 1); + const fieldBoost = clamp((maxSide - 760) / 720, 0, 1); + + return { + radius: 1.06 + clamp(aspect - 1, 0, 1.1) * 0.13 + compactBoost * 0.14 + fieldBoost * 0.05, + orbitX: clamp(width / 920, 0.28, 1.38), + orbitY: clamp(height / 680, 0.28, 1.38) + }; + } + + const LIGHT_SUBTLE_BLOBS = scaleBlobSet(LIGHT_BLOBS, 0.44, 0.62); + + const STATIC_LIGHT_BLOBS = [ + { anchorX: 1.02, anchorY: 0.14, color: [255, 120, 160], radius: 0.48, opacity: 0.58, locked: true }, + { anchorX: 0.96, anchorY: 0.42, color: [140, 100, 255], radius: 0.54, opacity: 0.6, locked: true }, + { anchorX: 0.8, anchorY: 0.62, color: [40, 140, 255], radius: 0.54, opacity: 0.58, locked: true }, + { anchorX: 0.78, anchorY: 0.28, color: [255, 180, 100], radius: 0.36, opacity: 0.44, locked: true }, + { anchorX: 1.02, anchorY: 0.82, color: [80, 220, 180], radius: 0.38, opacity: 0.1, locked: true }, + { anchorX: 0.34, anchorY: 0.34, color: [200, 220, 255], radius: 0.76, opacity: 0.1, locked: true }, + { anchorX: 0.2, anchorY: 0.74, color: [255, 200, 220], radius: 0.84, opacity: 0.075, locked: true }, + { anchorX: 0.52, anchorY: 0.62, color: [255, 240, 180], radius: 0.62, opacity: 0.075, locked: true } + ]; + + const STATIC_LIGHT_SUBTLE_BLOBS = scaleBlobSet(STATIC_LIGHT_BLOBS, 0.48, 1); + + const NEUTRAL_LIGHT_BLOBS = neutralBlobSet([ + { anchorX: 0.12, anchorY: 0.12, color: [255, 180, 100], radius: 0.39, opacity: 0.34, orbitX: 90, orbitY: 68, freqX: 0.00072, freqY: 0.00056, stretchFreq: 0.00108 }, + { anchorX: 0.28, anchorY: 0.28, color: [255, 120, 160], radius: 0.46, opacity: 0.5, orbitX: -118, orbitY: 84, freqX: 0.00086, freqY: 0.00068, stretchFreq: 0.00118 }, + { anchorX: 0.14, anchorY: 0.78, color: [40, 140, 255], radius: 0.54, opacity: 0.56, orbitX: 126, orbitY: -110, freqX: 0.00074, freqY: 0.00092, stretchFreq: 0.00102 }, + { anchorX: 0.44, anchorY: 0.18, color: [140, 100, 255], radius: 0.45, opacity: 0.48, orbitX: 106, orbitY: 92, freqX: 0.00064, freqY: 0.00086, stretchFreq: 0.00112 }, + { anchorX: 0.68, anchorY: 0.32, color: [40, 140, 255], radius: 0.49, opacity: 0.54, orbitX: 108, orbitY: -92, freqX: 0.00084, freqY: 0.00064, stretchFreq: 0.00114 }, + { anchorX: 0.84, anchorY: 0.18, color: [255, 120, 160], radius: 0.42, opacity: 0.45, orbitX: -98, orbitY: 98, freqX: 0.00078, freqY: 0.00088, stretchFreq: 0.00106 }, + { anchorX: 0.88, anchorY: 0.62, color: [140, 100, 255], radius: 0.56, opacity: 0.58, orbitX: -122, orbitY: -104, freqX: 0.00058, freqY: 0.00094, stretchFreq: 0.00098 }, + { anchorX: 0.68, anchorY: 0.86, color: [40, 140, 255], radius: 0.54, opacity: 0.54, orbitX: 108, orbitY: -110, freqX: 0.0009, freqY: 0.00072, stretchFreq: 0.0012 }, + { anchorX: 0.36, anchorY: 0.84, color: [255, 120, 160], radius: 0.42, opacity: 0.38, orbitX: 98, orbitY: -88, freqX: 0.0007, freqY: 0.00082, stretchFreq: 0.00104 }, + { anchorX: 0.48, anchorY: 0.48, color: [80, 220, 180], radius: 0.3, opacity: 0.04, orbitX: 78, orbitY: -72, freqX: 0.00068, freqY: 0.00072, stretchFreq: 0.00092 } + ]); + + const NEUTRAL_LIGHT_SUBTLE_BLOBS = scaleBlobSet(NEUTRAL_LIGHT_BLOBS, 0.52, 0.86); + const STATIC_NEUTRAL_LIGHT_BLOBS = lockBlobSet(NEUTRAL_LIGHT_BLOBS); + const STATIC_NEUTRAL_LIGHT_SUBTLE_BLOBS = lockBlobSet(NEUTRAL_LIGHT_SUBTLE_BLOBS); + + const DARK_SUBTLE_BLOBS = [ + { anchorX: 0.9, anchorY: 0.4, color: [255, 93, 142], radius: 0.42, opacity: 0.34, orbitX: 60, orbitY: 80, freqX: 0.0007, freqY: 0.0005, stretchFreq: 0.001 }, + { anchorX: 0.86, anchorY: 0.72, color: [45, 220, 177], radius: 0.38, opacity: 0.12, orbitX: -70, orbitY: -90, freqX: 0.0008, freqY: 0.0006, stretchFreq: 0.0012 }, + { anchorX: 0.95, anchorY: 0.5, color: [138, 93, 255], radius: 0.48, opacity: 0.36, orbitX: 50, orbitY: 100, freqX: 0.0005, freqY: 0.0009, stretchFreq: 0.0008 }, + { anchorX: 0.74, anchorY: 0.3, color: [255, 180, 90], radius: 0.29, opacity: 0.2, orbitX: -60, orbitY: 120, freqX: 0.0009, freqY: 0.0007, stretchFreq: 0.0015 }, + { anchorX: 0.8, anchorY: 0.62, color: [39, 140, 255], radius: 0.42, opacity: 0.32, orbitX: 90, orbitY: -80, freqX: 0.0008, freqY: 0.0011, stretchFreq: 0.0009 }, + { anchorX: 0.3, anchorY: 0.3, color: [160, 196, 255], radius: 0.62, opacity: 0.055, orbitX: 100, orbitY: 50, freqX: 0.0003, freqY: 0.0004, stretchFreq: 0.0005 }, + { anchorX: 0.2, anchorY: 0.72, color: [255, 170, 210], radius: 0.66, opacity: 0.055, orbitX: -80, orbitY: -100, freqX: 0.0004, freqY: 0.0003, stretchFreq: 0.0006 }, + { anchorX: 0.5, anchorY: 0.6, color: [255, 232, 150], radius: 0.52, opacity: 0.045, orbitX: 120, orbitY: -60, freqX: 0.0005, freqY: 0.0004, stretchFreq: 0.0007 } + ]; + + const DARK_PROMINENT_BLOBS = [ + { anchorX: 0.9, anchorY: 0.4, color: [255, 93, 142], radius: 0.46, opacity: 0.44, orbitX: 60, orbitY: 80, freqX: 0.0007, freqY: 0.0005, stretchFreq: 0.001 }, + { anchorX: 0.86, anchorY: 0.72, color: [45, 220, 177], radius: 0.42, opacity: 0.18, orbitX: -70, orbitY: -90, freqX: 0.0008, freqY: 0.0006, stretchFreq: 0.0012 }, + { anchorX: 0.95, anchorY: 0.5, color: [138, 93, 255], radius: 0.52, opacity: 0.46, orbitX: 50, orbitY: 100, freqX: 0.0005, freqY: 0.0009, stretchFreq: 0.0008 }, + { anchorX: 0.74, anchorY: 0.3, color: [255, 180, 90], radius: 0.32, opacity: 0.28, orbitX: -60, orbitY: 120, freqX: 0.0009, freqY: 0.0007, stretchFreq: 0.0015 }, + { anchorX: 0.8, anchorY: 0.62, color: [39, 140, 255], radius: 0.46, opacity: 0.42, orbitX: 90, orbitY: -80, freqX: 0.0008, freqY: 0.0011, stretchFreq: 0.0009 }, + { anchorX: 0.3, anchorY: 0.3, color: [160, 196, 255], radius: 0.78, opacity: 0.12, orbitX: 100, orbitY: 50, freqX: 0.0003, freqY: 0.0004, stretchFreq: 0.0005 }, + { anchorX: 0.2, anchorY: 0.72, color: [255, 170, 210], radius: 0.82, opacity: 0.12, orbitX: -80, orbitY: -100, freqX: 0.0004, freqY: 0.0003, stretchFreq: 0.0006 }, + { anchorX: 0.5, anchorY: 0.6, color: [255, 232, 150], radius: 0.64, opacity: 0.1, orbitX: 120, orbitY: -60, freqX: 0.0005, freqY: 0.0004, stretchFreq: 0.0007 } + ]; + + const STATIC_DARK_PROMINENT_BLOBS = [ + { anchorX: 1.02, anchorY: 0.14, color: [255, 93, 142], radius: 0.5, opacity: 0.42, locked: true }, + { anchorX: 0.96, anchorY: 0.44, color: [138, 93, 255], radius: 0.56, opacity: 0.46, locked: true }, + { anchorX: 0.8, anchorY: 0.64, color: [39, 140, 255], radius: 0.56, opacity: 0.42, locked: true }, + { anchorX: 0.76, anchorY: 0.28, color: [255, 180, 90], radius: 0.36, opacity: 0.24, locked: true }, + { anchorX: 1.02, anchorY: 0.84, color: [45, 220, 177], radius: 0.38, opacity: 0.07, locked: true }, + { anchorX: 0.34, anchorY: 0.34, color: [160, 196, 255], radius: 0.76, opacity: 0.08, locked: true }, + { anchorX: 0.2, anchorY: 0.74, color: [255, 170, 210], radius: 0.82, opacity: 0.075, locked: true }, + { anchorX: 0.52, anchorY: 0.62, color: [255, 232, 150], radius: 0.62, opacity: 0.055, locked: true } + ]; + + const STATIC_DARK_SUBTLE_BLOBS = scaleBlobSet(STATIC_DARK_PROMINENT_BLOBS, 0.54, 1); + + const NEUTRAL_DARK_PROMINENT_BLOBS = neutralBlobSet([ + { anchorX: 0.12, anchorY: 0.12, color: [255, 180, 90], radius: 0.39, opacity: 0.22, orbitX: 90, orbitY: 68, freqX: 0.00072, freqY: 0.00056, stretchFreq: 0.00108 }, + { anchorX: 0.28, anchorY: 0.28, color: [255, 93, 142], radius: 0.46, opacity: 0.36, orbitX: -118, orbitY: 84, freqX: 0.00086, freqY: 0.00068, stretchFreq: 0.00118 }, + { anchorX: 0.14, anchorY: 0.78, color: [39, 140, 255], radius: 0.54, opacity: 0.4, orbitX: 126, orbitY: -110, freqX: 0.00074, freqY: 0.00092, stretchFreq: 0.00102 }, + { anchorX: 0.44, anchorY: 0.18, color: [138, 93, 255], radius: 0.45, opacity: 0.36, orbitX: 106, orbitY: 92, freqX: 0.00064, freqY: 0.00086, stretchFreq: 0.00112 }, + { anchorX: 0.68, anchorY: 0.32, color: [39, 140, 255], radius: 0.49, opacity: 0.38, orbitX: 108, orbitY: -92, freqX: 0.00084, freqY: 0.00064, stretchFreq: 0.00114 }, + { anchorX: 0.84, anchorY: 0.18, color: [255, 93, 142], radius: 0.42, opacity: 0.32, orbitX: -98, orbitY: 98, freqX: 0.00078, freqY: 0.00088, stretchFreq: 0.00106 }, + { anchorX: 0.88, anchorY: 0.62, color: [138, 93, 255], radius: 0.56, opacity: 0.42, orbitX: -122, orbitY: -104, freqX: 0.00058, freqY: 0.00094, stretchFreq: 0.00098 }, + { anchorX: 0.68, anchorY: 0.86, color: [39, 140, 255], radius: 0.54, opacity: 0.4, orbitX: 108, orbitY: -110, freqX: 0.0009, freqY: 0.00072, stretchFreq: 0.0012 }, + { anchorX: 0.36, anchorY: 0.84, color: [255, 93, 142], radius: 0.42, opacity: 0.28, orbitX: 98, orbitY: -88, freqX: 0.0007, freqY: 0.00082, stretchFreq: 0.00104 }, + { anchorX: 0.48, anchorY: 0.48, color: [45, 220, 177], radius: 0.3, opacity: 0.024, orbitX: 78, orbitY: -72, freqX: 0.00068, freqY: 0.00072, stretchFreq: 0.00092 } + ]); + + const NEUTRAL_DARK_SUBTLE_BLOBS = scaleBlobSet(NEUTRAL_DARK_PROMINENT_BLOBS, 0.52, 0.86); + const STATIC_NEUTRAL_DARK_PROMINENT_BLOBS = lockBlobSet(NEUTRAL_DARK_PROMINENT_BLOBS); + const STATIC_NEUTRAL_DARK_SUBTLE_BLOBS = lockBlobSet(NEUTRAL_DARK_SUBTLE_BLOBS); + + const NEUTRAL_VEIL_BLOBS = [ + { anchorX: 0.08, anchorY: 0.18, radius: 0.56, opacity: 0.28, orbitX: 52, orbitY: 44, freqX: 0.00048, freqY: 0.0004, phase: 0.32 }, + { anchorX: 0.52, anchorY: 0.08, radius: 0.48, opacity: 0.2, orbitX: -56, orbitY: 38, freqX: 0.00038, freqY: 0.00046, phase: 1.18 }, + { anchorX: 0.94, anchorY: 0.26, radius: 0.54, opacity: 0.24, orbitX: -48, orbitY: 54, freqX: 0.00044, freqY: 0.00036, phase: 2.42 }, + { anchorX: 0.22, anchorY: 0.82, radius: 0.6, opacity: 0.22, orbitX: 62, orbitY: -46, freqX: 0.00034, freqY: 0.00042, phase: 3.36 }, + { anchorX: 0.74, anchorY: 0.78, radius: 0.58, opacity: 0.24, orbitX: -64, orbitY: -52, freqX: 0.00042, freqY: 0.00038, phase: 4.72 }, + { anchorX: 0.48, anchorY: 0.5, radius: 0.72, opacity: 0.16, orbitX: 46, orbitY: -42, freqX: 0.0003, freqY: 0.00034, phase: 5.58 } + ]; + + const surfaces = new Set(); + const surfaceByElement = new WeakMap(); + const textureCache = new Map(); + const timeOrigin = getAnimationTimeOrigin(); + let isDocumentVisible = document.visibilityState !== "hidden"; + let mutationObserver; + let raf = 0; + + function getTimelineTime() { + return Math.max(0, Date.now() - timeOrigin); + } + + function rememberTexture(key, texture) { + if (textureCache.has(key)) return textureCache.get(key); + textureCache.set(key, texture); + + if (textureCache.size > MAX_TEXTURE_CACHE_ITEMS) { + const oldestKey = textureCache.keys().next().value; + textureCache.delete(oldestKey); + } + + return texture; + } + + class RadianceBlob { + constructor(config, random) { + this.config = config; + this.phaseX = random() * Math.PI * 2; + this.phaseY = random() * Math.PI * 2; + this.phaseStretch = random() * Math.PI * 2; + this.phaseDrift = random() * Math.PI * 2; + // F2: the extra sines' ratios and phases, a slow tilt, and the blob's + // own breath - all seeded (IMPL-4), so a surface is stable on reload. + this.phases = Array.from({ length: 8 }, () => random() * TAU); + this.ratios = [ + 1.52 + random() * 0.2, + 2.3 + random() * 0.3, + 1.6 + random() * 0.3, + 1.4 + random() * 0.3, + 1.5 + random() * 0.3, + 1.5 + random() * 0.3, + 1.6 + random() * 0.3 + ]; + this.tiltFreq = 0.00022 + random() * 0.00022; + this.breathMs = BREATH.periodMs[0] + random() * (BREATH.periodMs[1] - BREATH.periodMs[0]); + this.breathPhase = random() * TAU; + this.x = 0; + this.y = 0; + this.r = 0; + this.stretchX = 1; + this.stretchY = 1; + this.tilt = 0; + this.hasState = false; + } + + update(t, width, height, mouse, breathT = 0) { + const config = this.config; + const baseX = width * config.anchorX; + const baseY = height * config.anchorY; + const minSide = Math.min(width, height); + const surfaceScale = getBlobSurfaceScale(config, width, height); + const baseRadius = minSide * config.radius * surfaceScale.radius; + + if (config.locked) { + this.x = baseX; + this.y = baseY; + this.r = baseRadius; + this.stretchX = 1; + this.stretchY = 1; + this.tilt = 0; + this.opacity = config.opacity; + this.hasState = true; + return; + } + + const freqX = config.freqX || 0.00022; + const freqY = config.freqY || 0.00022; + const stretchFreq = config.stretchFreq || 0.00032; + let targetX; + let targetY; + let targetR; + let targetStretchX; + let targetStretchY; + + // F2 (MOTION-2): each motion is 2-3 sines at incommensurate per-blob + // ratios - the same reach as v1's single sines, but a path that + // never retraces - and the blob breathes on its own rhythm. + const p = this.phases; + const k = this.ratios; + const orbitX = (config.orbitX || 0) * surfaceScale.orbitX; + const orbitY = (config.orbitY || 0) * surfaceScale.orbitY; + const breath = wave(TAU * breathT / this.breathMs + this.breathPhase); + targetX = baseX + orbitX * ( + 0.72 * Math.sin(t * freqX + this.phaseX) + + 0.2 * Math.sin(t * freqX * k[0] + p[0]) + + 0.08 * Math.sin(t * freqX * k[1] + p[1])); + targetY = baseY + orbitY * ( + 0.78 * Math.cos(t * freqY + this.phaseY) + + 0.22 * Math.sin(t * freqY * k[2] + p[2])); + targetR = baseRadius + * (1 + 0.1 * Math.sin(t * 0.001 + this.phaseX) + 0.05 * Math.sin(t * 0.001 * k[3] + p[3])) + * (1 + BREATH.size * (breath - 0.4)); + targetStretchX = 1 + 0.3 * (0.72 * Math.sin(t * stretchFreq + this.phaseX) + 0.28 * Math.sin(t * stretchFreq * k[4] + p[4])); + targetStretchY = 1 + 0.3 * (0.72 * Math.cos(t * stretchFreq + this.phaseY) + 0.28 * Math.sin(t * stretchFreq * k[5] + p[5])); + this.tilt = 0.16 * Math.sin(t * this.tiltFreq + p[6]) + 0.08 * Math.sin(t * this.tiltFreq * k[6] + p[7]); + this.opacity = config.opacity * (1 + BREATH.glow * (breath - 0.5)); + + const ease = 1; + this.x += (targetX - this.x) * ease; + this.y += (targetY - this.y) * ease; + this.r += (targetR - this.r) * ease; + this.stretchX += (targetStretchX - this.stretchX) * ease; + this.stretchY += (targetStretchY - this.stretchY) * ease; + this.hasState = true; + } + + draw(ctx) { + const [r, g, b] = this.config.color; + const opacity = this.opacity ?? this.config.opacity; + ctx.save(); + ctx.translate(this.x, this.y); + if (this.tilt) ctx.rotate(this.tilt); + ctx.scale(this.stretchX, this.stretchY); + const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.r); + gradient.addColorStop(0, `rgba(${r},${g},${b},${opacity})`); + gradient.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(0, 0, this.r, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + } + + class RadianceSurface { + constructor(element) { + this.element = element; + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d", { alpha: false }); + this.mouse = { x: -9999, y: -9999 }; + this.width = 0; + this.height = 0; + this.dpr = 1; + this.textureKey = ""; + this.ready = false; + this.isInViewport = true; + this.needsResize = true; + this.staticFrameKey = ""; + this.blobCanvas = document.createElement("canvas"); + this.blobCtx = this.blobCanvas.getContext("2d"); + this.fieldCanvas = document.createElement("canvas"); + this.fieldCtx = this.fieldCanvas.getContext("2d"); + this.grainCanvas = document.createElement("canvas"); + this.grainMaskCtx = this.grainCanvas.getContext("2d"); + this.maskCanvas = document.createElement("canvas"); + this.maskCtx = this.maskCanvas.getContext("2d"); + this.liftCanvas = document.createElement("canvas"); + this.liftCtx = this.liftCanvas.getContext("2d"); + this.blobs = []; + this.lastConfigKey = ""; + this.appliedPatternSource = null; + this.product = null; + this.productName = ""; + this.cssBase = ""; + this.baseKey = ""; + this.baseRgb = [255, 255, 255]; + this.baseFill = LIGHT_SURFACE; + this.element.appendChild(this.canvas); + this.element.dataset.radianceReady = "false"; + + if ("ResizeObserver" in window) { + this.resizeObserver = new ResizeObserver(() => { + this.needsResize = true; + }); + this.resizeObserver.observe(this.element); + } + + if ("IntersectionObserver" in window) { + this.intersectionObserver = new IntersectionObserver((entries) => { + this.isInViewport = entries.some((entry) => entry.isIntersecting); + if (this.isInViewport) startLoop(); + }); + this.intersectionObserver.observe(this.element); + } + } + + destroy() { + this.resizeObserver?.disconnect(); + this.intersectionObserver?.disconnect(); + this.canvas.remove(); + } + + get appearance() { + return this.element.dataset.radianceAppearance === "dark" ? "dark" : "light"; + } + + // A template product pattern (signup, home), or null. + get productTemplate() { + return this.product && this.product.engine === "template" ? this.product : null; + } + + get isSpotter() { + return Boolean(this.product && this.product.engine === "spotter"); + } + + get speed() { + const product = this.productTemplate; + if (product) return product.speed[this.appearance]; + const value = Number(this.element.dataset.radianceSpeed || "1"); + const base = Number.isFinite(value) ? value : 1; + return base * this.adjustment("speed", this.element.dataset.radianceSpeedScale); + } + + // ADJUST-1: every adjustment is a percentage of the pattern's own value, + // never an absolute. 100% means "exactly what the pattern ships". + adjustment(kind, raw) { + const scale = parseAdjustment(raw); + const range = ADJUST_RANGES[kind]; + if (range && (scale < range.min || scale > range.max)) { + warnOnce( + this.element, + range.rule, + `${kind} adjusted to ${Math.round(scale * 100)}% — the Radiance range is ` + + `${Math.round(range.min * 100)}%-${Math.round(range.max * 100)}%. ` + + "Outside it this surface stops matching the rest of the product. See RULES.md." + ); + } + return scale; + } + + get prominenceScale() { + if (this.product) return 1; + return this.adjustment("prominence", this.element.dataset.radianceProminence); + } + + get spreadScale() { + if (this.product) return 1; + return this.adjustment("spread", this.element.dataset.radianceSpread); + } + + get variant() { + const product = this.productTemplate; + if (product) { + const motion = prefersReducedMotion && product.reducedMotion === "static" ? "static" : "dynamic"; + return `${motion}-${product.intensity}`; + } + return this.element.dataset.radianceVariant || ""; + } + + get darkGlow() { + const product = this.productTemplate; + if (product) return product.intensity; + return this.element.dataset.radianceDarkGlow || "subtle"; + } + + get seed() { + const product = this.productTemplate; + if (product) return product.seed; + return this.element.dataset.radianceSeed || ""; + } + + // "renderer": the v2 film stack (F1). "page": the product's own page + // recipe carries the film; the renderer draws plain full-surface grain. + get filmMode() { + const product = this.productTemplate; + return product ? product.film : "renderer"; + } + + get layout() { + const product = this.productTemplate; + if (product) return product.layout; + const layout = this.element.dataset.radianceLayout || "right-wash"; + if (layout === "left" || layout === "left-wash") return "left-wash"; + if (layout === "top" || layout === "top-wash") return "top-wash"; + if (layout === "bottom" || layout === "bottom-wash") return "bottom-wash"; + if (layout === "neutral" || layout === "home-corners" || layout === "modal-balanced" || layout === "developer-sides") return "neutral"; + return "right-wash"; + } + + get isSubtle() { + const product = this.productTemplate; + if (product) return product.intensity === "subtle"; + return this.variant.endsWith("subtle") || this.element.dataset.radianceIntensity === "subtle"; + } + + get isDark() { + return this.appearance === "dark"; + } + + get isStill() { + // MOTION-7: a reduced-motion request freezes the surface at its + // resting frame. Same pattern, same composition, no drift. + if (prefersReducedMotion) return true; + if (this.product) return false; + return this.element.dataset.radianceMotion === "still" || this.variant.startsWith("static"); + } + + applyPatternShorthand() { + const raw = (this.element.dataset.radiancePattern || "").trim(); + if (raw === this.appliedPatternSource) { + if (this.product) this.guardProduct(); + return; + } + this.appliedPatternSource = raw; + + // PRODUCT-1: a product name selects that product's fixed pattern. + const productKey = raw.toLowerCase(); + if (Object.prototype.hasOwnProperty.call(PRODUCT_PATTERNS, productKey)) { + this.product = PRODUCT_PATTERNS[productKey]; + this.productName = productKey; + this.needsResize = true; + this.lastConfigKey = ""; + this.staticFrameKey = ""; + this.baseKey = ""; + this.guardProduct(); + return; + } + if (this.product) { + this.product = null; + this.productName = ""; + this.needsResize = true; + this.lastConfigKey = ""; + this.baseKey = ""; + } + if (!raw) return; + + const pattern = resolveRadiancePattern(raw); + if (!pattern) { + warnOnce( + this.element, + "PLACE-1", + `Unknown pattern "${raw}". Templates are: ${Object.keys(RADIANCE_PATTERNS).join(", ")}; ` + + `product patterns are: ${Object.keys(PRODUCT_PATTERNS).join(", ")}. ` + + "Keeping the data-radiance-* attributes already on this element." + ); + return; + } + + const data = this.element.dataset; + data.radianceLayout = pattern.layout; + data.radianceIntensity = pattern.intensity; + data.radianceDarkGlow = pattern.intensity; + data.radianceVariant = `${pattern.motion}-${pattern.intensity}`; + data.radianceSpeed = String(pattern.speed); + if (pattern.appearance) data.radianceAppearance = pattern.appearance; + if (pattern.motion === "static") data.radianceMotion = "still"; + else delete data.radianceMotion; + } + + // PRODUCT-2: a product pattern takes no adjustment or override. The + // attributes stay on the element (the page owns them) but are ignored. + guardProduct() { + const data = this.element.dataset; + const present = PRODUCT_LOCKED_ATTRIBUTES.filter(([key]) => data[key] !== undefined).map(([, attr]) => attr); + if (!present.length) return; + warnOnce( + this.element, + "PRODUCT-2", + `data-radiance-pattern="${this.productName}" is a fixed product pattern, so ${present.join(", ")} ` + + `${present.length === 1 ? "is" : "are"} ignored. Product patterns take no adjustment or override; ` + + "only the Radiance admin changes them. See RULES.md section 12." + ); + } + + // SURFACE-1: the base is the host's own background colour. Product + // patterns lock theirs; templates take data-radiance-base, then the + // --radiance-base custom property, then the language default. + resolveBase() { + const theme = this.appearance; + const raw = this.product + ? this.product.base[theme] + : (this.element.dataset.radianceBase || this.cssBase || ""); + const key = `${theme}|${raw}`; + if (key === this.baseKey) return this.baseRgb; + this.baseKey = key; + let rgb = raw ? parseColour(raw) : null; + if (raw && !rgb) { + warnOnce( + this.element, + "SURFACE-1", + `base colour "${raw}" is not an opaque colour, so the default ${theme} base is used. ` + + "Set data-radiance-base (or --radiance-base) to the exact, opaque background of the area this surface sits in." + ); + } + if (!rgb) rgb = theme === "dark" ? [26, 27, 30] : [255, 255, 255]; + this.baseRgb = rgb; + this.baseFill = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`; + this.staticFrameKey = ""; + return rgb; + } + + get baseTone() { + return `${this.baseRgb[0]},${this.baseRgb[1]},${this.baseRgb[2]}`; + } + + checkConsistency() { + const data = this.element.dataset; + if (this.product) return; + + if (data.radianceIntensity && data.radianceDarkGlow && data.radianceIntensity !== data.radianceDarkGlow) { + warnOnce( + this.element, + "INT-3", + `data-radiance-dark-glow="${data.radianceDarkGlow}" does not match ` + + `data-radiance-intensity="${data.radianceIntensity}". Dark mode draws a different falloff ` + + "when these disagree, so the surface will not match the rest of the product. Set both to the same value." + ); + } + + // MOTION-5 is an absolute guardrail: it applies to the EFFECTIVE speed, + // after any data-radiance-speed-scale percentage has been applied. + const effective = this.speed; + if (Number.isFinite(effective) && (effective < 0.4 || effective > 2)) { + const scale = parseAdjustment(data.radianceSpeedScale); + const via = scale === 1 + ? "" + : ` (${data.radianceSpeed} x ${Math.round(scale * 100)}%)`; + warnOnce( + this.element, + "MOTION-5", + `effective speed ${Math.round(effective * 1000) / 1000}${via} is outside the Radiance ` + + "range 0.4-2.0. Below 0.4 the drift reads as dead, above 2.0 it reads as nervous. " + + "See RULES.md (MOTION-5)." + ); + } + } + + isRenderable() { + if (!isDocumentVisible || !this.element.isConnected || !this.isInViewport) return false; + if (document.documentElement.dataset.radiancePaused === "true" || document.body?.classList.contains("is-proof-paused")) return false; + const slide = this.element.closest(".slide"); + if (slide && !slide.classList.contains("is-active")) return false; + const proofView = this.element.closest(".proof-view"); + if (proofView && !proofView.classList.contains("is-active")) return false; + const style = window.getComputedStyle(this.element); + if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false; + this.cssBase = style.getPropertyValue("--radiance-base").trim(); + const rect = this.element.getBoundingClientRect(); + return rect.width > 2 && rect.height > 2; + } + + getConfig() { + const layout = this.layout; + + if (!this.isDark) { + if (layout === "neutral") { + if (this.variant.startsWith("static")) return this.isSubtle ? STATIC_NEUTRAL_LIGHT_SUBTLE_BLOBS : STATIC_NEUTRAL_LIGHT_BLOBS; + return this.isSubtle ? NEUTRAL_LIGHT_SUBTLE_BLOBS : NEUTRAL_LIGHT_BLOBS; + } + const base = this.variant.startsWith("static") + ? (this.isSubtle ? STATIC_LIGHT_SUBTLE_BLOBS : STATIC_LIGHT_BLOBS) + : (this.isSubtle ? LIGHT_SUBTLE_BLOBS : LIGHT_BLOBS); + if (layout === "top-wash") return topBlobSet(base); + if (layout === "bottom-wash") return bottomBlobSet(base); + return layout === "left-wash" ? mirrorBlobSet(base) : base; + } + + if (layout === "neutral") { + if (this.variant.startsWith("static")) return this.isSubtle ? STATIC_NEUTRAL_DARK_SUBTLE_BLOBS : STATIC_NEUTRAL_DARK_PROMINENT_BLOBS; + return this.isSubtle ? NEUTRAL_DARK_SUBTLE_BLOBS : NEUTRAL_DARK_PROMINENT_BLOBS; + } + const base = this.variant.startsWith("static") + ? (this.isSubtle ? STATIC_DARK_SUBTLE_BLOBS : STATIC_DARK_PROMINENT_BLOBS) + : (this.darkGlow === "subtle" || this.isSubtle ? DARK_SUBTLE_BLOBS : DARK_PROMINENT_BLOBS); + if (layout === "top-wash") return topBlobSet(base); + if (layout === "bottom-wash") return bottomBlobSet(base); + return layout === "left-wash" ? mirrorBlobSet(base) : base; + } + + ensureSize() { + if (this.resizeObserver && !this.needsResize && this.width > 0 && this.height > 0) return; + + const rect = this.element.getBoundingClientRect(); + const nextWidth = Math.max(1, Math.round(this.element.clientWidth || rect.width)); + const nextHeight = Math.max(1, Math.round(this.element.clientHeight || rect.height)); + // The spotter engine renders one canvas pixel per CSS pixel and is + // shown pixelated, so its 1.0px film stays crisp (GRAIN-1/3). + const nextDpr = this.isSpotter ? 1 : Math.min(window.devicePixelRatio || 1, MAX_DPR); + this.canvas.style.imageRendering = this.isSpotter ? "pixelated" : ""; + + if (nextWidth === this.width && nextHeight === this.height && nextDpr === this.dpr) { + this.needsResize = false; + return; + } + + this.width = nextWidth; + this.height = nextHeight; + this.dpr = nextDpr; + this.canvas.width = Math.max(1, Math.floor(this.width * this.dpr)); + this.canvas.height = Math.max(1, Math.floor(this.height * this.dpr)); + this.canvas.style.width = `${this.width}px`; + this.canvas.style.height = `${this.height}px`; + this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.ctx.imageSmoothingEnabled = true; + this.ctx.imageSmoothingQuality = "high"; + this.textureKey = ""; + this.staticFrameKey = ""; + this.needsResize = false; + } + + ensureBlobs() { + const configKey = this.product + ? [this.appearance, this.productName, this.variant].join(":") + : [ + this.appearance, + this.darkGlow, + this.element.dataset.radianceLayout || "default", + this.element.dataset.radianceIntensity || "default", + this.variant || "default", + this.element.dataset.radianceMotion || "dynamic", + this.element.dataset.radianceProminence || "100%", + this.element.dataset.radianceSpread || "100%" + ].join(":"); + if (configKey === this.lastConfigKey && this.blobs.length) return; + this.lastConfigKey = configKey; + this.checkConsistency(); + this.staticFrameKey = ""; + const seed = this.seed || configKey; + const random = createSeededRandom(`radiance-blobs:${seed}`); + const prominence = this.prominenceScale; + const spread = this.spreadScale; + const configs = (prominence === 1 && spread === 1) + ? this.getConfig() + : this.getConfig().map((config) => ({ + ...config, + opacity: config.opacity * prominence, + radius: config.radius * spread + })); + this.blobs = configs.map((config) => new RadianceBlob(config, random)); + } + + createGrain(mode) { + const texture = document.createElement("canvas"); + texture.width = Math.max(1, Math.floor(this.width)); + texture.height = Math.max(1, Math.floor(this.height)); + const grainCtx = texture.getContext("2d"); + const isDark = mode === "dark"; + + const rawTexture = document.createElement("canvas"); + const rawWidth = Math.max(1, Math.ceil(texture.width / RADIANCE_FILM_GRAIN_SIZE)); + const rawHeight = Math.max(1, Math.ceil(texture.height / RADIANCE_FILM_GRAIN_SIZE)); + rawTexture.width = rawWidth; + rawTexture.height = rawHeight; + const rawCtx = rawTexture.getContext("2d"); + const image = rawCtx.createImageData(rawWidth, rawHeight); + const lightFilmBright = [ + [232, 248, 252], + [244, 229, 253], + [253, 228, 240], + [253, 244, 212], + [235, 241, 248] + ]; + const lightFilmChroma = [ + [120, 208, 230], + [194, 132, 232], + [244, 132, 196], + [246, 204, 104], + [116, 154, 246], + [128, 218, 194] + ]; + const lightFilmMid = [ + [174, 214, 220], + [204, 177, 224], + [224, 178, 202], + [224, 206, 150], + [184, 196, 210] + ]; + const lightFilmDark = [ + [122, 150, 156], + [150, 124, 168], + [168, 126, 148], + [150, 140, 116], + [132, 138, 146] + ]; + const darkFilmColors = [ + [76, 116, 164], + [122, 82, 140], + [148, 78, 112], + [118, 112, 92], + [132, 138, 148] + ]; + const darkFilmShadows = [ + [10, 12, 16], + [14, 15, 18], + [18, 16, 23], + [16, 18, 24] + ]; + + const writeFilmGrain = (data, alphaMultiplier, random) => { + for (let i = 0; i < data.length; i += 4) { + let color; + let alpha; + let sharedJitter; + let channelJitter; + + if (isDark) { + const toneRoll = random(); + const useShadowFleck = toneRoll < 0.16; + color = useShadowFleck + ? darkFilmShadows[Math.floor(random() * darkFilmShadows.length)] + : darkFilmColors[Math.floor(random() * darkFilmColors.length)]; + alpha = useShadowFleck + ? 7 + Math.floor(random() * 9) + : 14 + Math.floor(random() * 14); + sharedJitter = Math.floor((random() - 0.5) * (useShadowFleck ? 10 : 38)); + channelJitter = useShadowFleck ? 5 : 12; + } else { + const toneRoll = random(); + const palette = toneRoll < 0.68 + ? lightFilmBright + : toneRoll < 0.9 + ? lightFilmMid + : toneRoll < 0.97 + ? lightFilmChroma + : lightFilmDark; + color = palette[Math.floor(random() * palette.length)]; + alpha = toneRoll < 0.68 + ? 44 + Math.floor(random() * 28) + : toneRoll < 0.9 + ? 28 + Math.floor(random() * 22) + : toneRoll < 0.97 + ? 22 + Math.floor(random() * 20) + : 10 + Math.floor(random() * 14); + sharedJitter = Math.floor((random() - 0.5) * (toneRoll < 0.9 ? 28 : 16)); + channelJitter = toneRoll < 0.97 ? 12 : 8; + } + + data[i] = Math.max(0, Math.min(255, color[0] + sharedJitter + Math.floor((random() - 0.5) * channelJitter))); + data[i + 1] = Math.max(0, Math.min(255, color[1] + sharedJitter + Math.floor((random() - 0.5) * channelJitter))); + data[i + 2] = Math.max(0, Math.min(255, color[2] + sharedJitter + Math.floor((random() - 0.5) * channelJitter))); + data[i + 3] = Math.max(0, Math.min(255, Math.round(alpha * alphaMultiplier))); + } + }; + + writeFilmGrain( + image.data, + isDark ? 1.18 : 1.16, + createSeededRandom(`radiance-grain:${mode}:macro:${texture.width}x${texture.height}`) + ); + rawCtx.putImageData(image, 0, 0); + + grainCtx.save(); + grainCtx.imageSmoothingEnabled = false; + grainCtx.drawImage(rawTexture, 0, 0, texture.width, texture.height); + grainCtx.restore(); + + const microTexture = document.createElement("canvas"); + microTexture.width = texture.width; + microTexture.height = texture.height; + const microCtx = microTexture.getContext("2d"); + const microImage = microCtx.createImageData(texture.width, texture.height); + writeFilmGrain( + microImage.data, + isDark ? 0.36 : 0.3, + createSeededRandom(`radiance-grain:${mode}:micro:${texture.width}x${texture.height}`) + ); + microCtx.putImageData(microImage, 0, 0); + grainCtx.drawImage(microTexture, 0, 0); + + return texture; + } + + createDarkDither() { + const texture = document.createElement("canvas"); + texture.width = Math.max(1, Math.floor(this.width * this.dpr)); + texture.height = Math.max(1, Math.floor(this.height * this.dpr)); + const ditherCtx = texture.getContext("2d"); + const image = ditherCtx.createImageData(texture.width, texture.height); + const data = image.data; + const random = createSeededRandom(`radiance-dither:${texture.width}x${texture.height}`); + + for (let i = 0; i < data.length; i += 4) { + const isLightSpeck = random() > 0.44; + const alpha = isLightSpeck + ? 6 + Math.floor(random() * 8) + : 4 + Math.floor(random() * 5); + + data[i] = isLightSpeck ? 255 : 10; + data[i + 1] = isLightSpeck ? 255 : 12; + data[i + 2] = isLightSpeck ? 255 : 16; + data[i + 3] = alpha; + } + + ditherCtx.putImageData(image, 0, 0); + return texture; + } + + drawLightRightEdgeSupport(t) { + const ctx = this.ctx; + const w = this.width; + const h = this.height; + ctx.save(); + ctx.globalCompositeOperation = "source-over"; + + const intensity = (this.isSubtle ? 0.58 : 1) * this.prominenceScale; + const pulse = (0.92 + 0.08 * Math.sin(t * 0.0005)) * intensity; + const rightWash = ctx.createLinearGradient(w * 0.36, 0, w, 0); + rightWash.addColorStop(0, "rgba(255,255,255,0)"); + rightWash.addColorStop(0.42, `rgba(200,220,255,${0.045 * pulse})`); + rightWash.addColorStop(0.66, `rgba(140,100,255,${0.075 * pulse})`); + rightWash.addColorStop(0.84, `rgba(255,120,160,${0.085 * pulse})`); + rightWash.addColorStop(1, `rgba(40,140,255,${0.11 * pulse})`); + ctx.fillStyle = rightWash; + ctx.fillRect(0, 0, w, h); + + const supportRadius = Math.max(w, h) * 0.42; + const supportGlows = [ + { x: w * 1.02, y: h * 0.12, radius: supportRadius, color: [255, 120, 160], alpha: 0.18 }, + { x: w * 0.96, y: h * 0.42, radius: supportRadius * 1.05, color: [140, 100, 255], alpha: 0.16 }, + { x: w * 1.02, y: h * 0.86, radius: supportRadius * 1.08, color: [40, 140, 255], alpha: 0.18 }, + { x: w * 0.84, y: h * 0.98, radius: supportRadius * 0.9, color: [80, 220, 180], alpha: 0.035 } + ]; + + for (const glow of supportGlows) { + const [r, g, b] = glow.color; + const grd = ctx.createRadialGradient(glow.x, glow.y, 0, glow.x, glow.y, glow.radius); + grd.addColorStop(0, `rgba(${r},${g},${b},${glow.alpha * pulse})`); + grd.addColorStop(0.58, `rgba(${r},${g},${b},${glow.alpha * 0.3 * pulse})`); + grd.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = grd; + ctx.fillRect(0, 0, w, h); + } + + ctx.restore(); + } + + drawDarkRightEdgeSupport(t) { + const ctx = this.ctx; + const w = this.width; + const h = this.height; + ctx.save(); + ctx.globalCompositeOperation = "screen"; + + const intensity = (this.isSubtle ? 0.58 : 1) * this.prominenceScale; + const pulse = (0.86 + 0.14 * Math.sin(t * 0.00062)) * intensity; + const rightWash = ctx.createLinearGradient(w * 0.5, 0, w, 0); + rightWash.addColorStop(0, "rgba(0,0,0,0)"); + rightWash.addColorStop(0.56, `rgba(138,93,255,${0.035 * pulse})`); + rightWash.addColorStop(0.82, `rgba(39,140,255,${0.05 * pulse})`); + rightWash.addColorStop(1, `rgba(255,93,142,${0.055 * pulse})`); + ctx.fillStyle = rightWash; + ctx.fillRect(0, 0, w, h); + + const cornerGlowRadius = Math.max(w, h) * 0.44; + const cornerGlows = [ + { x: w * 1.03, y: h * 0.06, radius: cornerGlowRadius, color: [255, 93, 142], alpha: 0.14 }, + { x: w * 1.04, y: h * 0.92, radius: cornerGlowRadius * 1.05, color: [39, 140, 255], alpha: 0.15 }, + { x: w * 0.9, y: h * 1.02, radius: cornerGlowRadius * 0.82, color: [45, 220, 177], alpha: 0.03 } + ]; + + for (const glow of cornerGlows) { + const [r, g, b] = glow.color; + const grd = ctx.createRadialGradient(glow.x, glow.y, 0, glow.x, glow.y, glow.radius); + grd.addColorStop(0, `rgba(${r},${g},${b},${glow.alpha * pulse})`); + grd.addColorStop(0.58, `rgba(${r},${g},${b},${glow.alpha * 0.28 * pulse})`); + grd.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = grd; + ctx.fillRect(0, 0, w, h); + } + + ctx.restore(); + } + + drawLightTopSupport(t) { + const ctx = this.ctx; + const w = this.width; + const h = this.height; + ctx.save(); + ctx.globalCompositeOperation = "source-over"; + + const intensity = (this.isSubtle ? 0.64 : 1) * this.prominenceScale; + const pulse = (0.9 + 0.1 * Math.sin(t * 0.0007)) * intensity; + const topWash = ctx.createLinearGradient(0, 0, 0, h); + topWash.addColorStop(0, `rgba(200,220,255,${0.105 * pulse})`); + topWash.addColorStop(0.18, `rgba(140,100,255,${0.062 * pulse})`); + topWash.addColorStop(0.34, `rgba(255,120,160,${0.052 * pulse})`); + topWash.addColorStop(0.58, `rgba(255,200,220,${0.024 * pulse})`); + topWash.addColorStop(1, "rgba(255,255,255,0)"); + ctx.fillStyle = topWash; + ctx.fillRect(0, 0, w, h); + + const supportRadius = Math.max(w, h) * 0.42; + const supportGlows = [ + { x: w * 0.16, y: h * -0.04, radius: supportRadius, color: [255, 120, 160], alpha: 0.13 }, + { x: w * 0.36, y: h * -0.05, radius: supportRadius * 1.04, color: [140, 100, 255], alpha: 0.13 }, + { x: w * 0.64, y: h * -0.02, radius: supportRadius * 1.08, color: [40, 140, 255], alpha: 0.15 }, + { x: w * 0.86, y: h * 0.02, radius: supportRadius * 1.12, color: [80, 220, 180], alpha: 0.06 } + ]; + + for (const glow of supportGlows) { + const [r, g, b] = glow.color; + const grd = ctx.createRadialGradient(glow.x, glow.y, 0, glow.x, glow.y, glow.radius); + grd.addColorStop(0, `rgba(${r},${g},${b},${glow.alpha * pulse})`); + grd.addColorStop(0.52, `rgba(${r},${g},${b},${glow.alpha * 0.28 * pulse})`); + grd.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = grd; + ctx.fillRect(0, 0, w, h); + } + + ctx.restore(); + } + + drawDarkTopSupport(t) { + const ctx = this.ctx; + const w = this.width; + const h = this.height; + ctx.save(); + ctx.globalCompositeOperation = "screen"; + + const intensity = (this.isSubtle ? 0.68 : 1) * this.prominenceScale; + const pulse = (0.86 + 0.14 * Math.sin(t * 0.00072)) * intensity; + const topWash = ctx.createLinearGradient(0, 0, 0, h); + topWash.addColorStop(0, `rgba(138,93,255,${0.082 * pulse})`); + topWash.addColorStop(0.18, `rgba(39,140,255,${0.058 * pulse})`); + topWash.addColorStop(0.34, `rgba(255,93,142,${0.043 * pulse})`); + topWash.addColorStop(0.58, `rgba(255,170,210,${0.018 * pulse})`); + topWash.addColorStop(1, "rgba(0,0,0,0)"); + ctx.fillStyle = topWash; + ctx.fillRect(0, 0, w, h); + + const supportRadius = Math.max(w, h) * 0.42; + const supportGlows = [ + { x: w * 0.18, y: h * -0.04, radius: supportRadius, color: [255, 93, 142], alpha: 0.105 }, + { x: w * 0.34, y: h * -0.05, radius: supportRadius * 1.04, color: [138, 93, 255], alpha: 0.12 }, + { x: w * 0.62, y: h * -0.02, radius: supportRadius * 1.08, color: [39, 140, 255], alpha: 0.13 }, + { x: w * 0.86, y: h * 0.02, radius: supportRadius * 1.12, color: [45, 220, 177], alpha: 0.035 } + ]; + + for (const glow of supportGlows) { + const [r, g, b] = glow.color; + const grd = ctx.createRadialGradient(glow.x, glow.y, 0, glow.x, glow.y, glow.radius); + grd.addColorStop(0, `rgba(${r},${g},${b},${glow.alpha * pulse})`); + grd.addColorStop(0.54, `rgba(${r},${g},${b},${glow.alpha * 0.3 * pulse})`); + grd.addColorStop(1, `rgba(${r},${g},${b},0)`); + ctx.fillStyle = grd; + ctx.fillRect(0, 0, w, h); + } + + ctx.restore(); + } + + drawNeutralSupport(t) { + const ctx = this.ctx; + const w = this.width; + const h = this.height; + const cycle = t * 0.00044; + const focusX = w * (0.5 + Math.sin(cycle) * 0.04); + const focusY = h * (0.48 + Math.cos(cycle * 0.86) * 0.035); + const radius = Math.max(w, h) * 0.74; + const washAlpha = (this.isSubtle ? 1.18 : 0.88) / this.prominenceScale; + + ctx.save(); + ctx.globalCompositeOperation = "source-over"; + + const tone = this.baseTone; + const centerWash = ctx.createRadialGradient(focusX, focusY, radius * 0.06, focusX, focusY, radius); + if (this.isDark) { + centerWash.addColorStop(0, `rgba(${tone},${0.18 * washAlpha})`); + centerWash.addColorStop(0.48, `rgba(${tone},${0.08 * washAlpha})`); + centerWash.addColorStop(1, `rgba(${tone},0)`); + } else { + centerWash.addColorStop(0, `rgba(${tone},${0.34 * washAlpha})`); + centerWash.addColorStop(0.46, `rgba(${tone},${0.18 * washAlpha})`); + centerWash.addColorStop(1, `rgba(${tone},0)`); + } + ctx.fillStyle = centerWash; + ctx.fillRect(0, 0, w, h); + + const crossWash = ctx.createLinearGradient(0, h * 0.1, w, h * 0.92); + if (this.isDark) { + crossWash.addColorStop(0, `rgba(${tone},${0.08 * washAlpha})`); + crossWash.addColorStop(0.52, `rgba(${tone},0)`); + crossWash.addColorStop(1, `rgba(${tone},${0.065 * washAlpha})`); + } else { + crossWash.addColorStop(0, `rgba(${tone},${0.08 * washAlpha})`); + crossWash.addColorStop(0.5, `rgba(${tone},0)`); + crossWash.addColorStop(1, `rgba(${tone},${0.07 * washAlpha})`); + } + ctx.fillStyle = crossWash; + ctx.fillRect(0, 0, w, h); + + const minSide = Math.min(w, h); + const veilAlpha = (this.isSubtle ? 1.34 : 0.32) / this.prominenceScale; + const veilTone = tone; + + for (const veil of NEUTRAL_VEIL_BLOBS) { + const x = w * veil.anchorX + Math.sin(t * veil.freqX + veil.phase) * veil.orbitX; + const y = h * veil.anchorY + Math.cos(t * veil.freqY + veil.phase) * veil.orbitY; + const veilRadius = minSide * veil.radius; + const alpha = veil.opacity * veilAlpha; + const neutralBlob = ctx.createRadialGradient(x, y, veilRadius * 0.06, x, y, veilRadius); + neutralBlob.addColorStop(0, `rgba(${veilTone},${alpha})`); + neutralBlob.addColorStop(0.54, `rgba(${veilTone},${alpha * 0.42})`); + neutralBlob.addColorStop(1, `rgba(${veilTone},0)`); + ctx.fillStyle = neutralBlob; + ctx.fillRect(0, 0, w, h); + } + ctx.restore(); + } + + // GRAIN-4 (experiment): keep grain inside the colour field. + // The draw methods all write to this.ctx, so swap it for the offscreen + // field context rather than threading a target through every method. + renderColourField(t) { + const realCtx = this.ctx; + this.fieldCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.fieldCtx.clearRect(0, 0, this.width, this.height); + + this.fieldCtx.save(); + this.fieldCtx.filter = `blur(${this.layout === "neutral" ? 22 : 40}px)`; + for (const blob of this.blobs) { + blob.update(t, this.width, this.height, this.mouse, this.breathT); + blob.draw(this.fieldCtx); + } + this.fieldCtx.restore(); + + this.ctx = this.fieldCtx; + try { + this.drawSupport(t); + } finally { + this.ctx = realCtx; + } + } + + // Dark keeps its own colour compositing (each layer screened onto the + // base separately). This pass exists only to collect the field's ALPHA, + // so the colours it produces here are never shown. + renderDarkColourField(t) { + const realCtx = this.ctx; + this.fieldCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.fieldCtx.globalCompositeOperation = "source-over"; + this.fieldCtx.clearRect(0, 0, this.width, this.height); + + this.fieldCtx.save(); + this.fieldCtx.filter = `blur(${this.layout === "neutral" ? 40 : 64}px)`; + this.fieldCtx.drawImage(this.blobCanvas, 0, 0, this.width, this.height); + this.fieldCtx.restore(); + + this.ctx = this.fieldCtx; + try { + this.drawSupport(t); + } finally { + this.ctx = realCtx; + } + } + + // Stack the colour field onto itself so the mask reads closer to + // present/absent than to the field's own opacity. Without this the + // texture would be thinned everywhere in proportion to colour strength. + buildColourMask(saturation) { + const maskCtx = this.maskCtx; + const mw = this.maskCanvas.width; + const mh = this.maskCanvas.height; + maskCtx.setTransform(1, 0, 0, 1, 0, 0); + maskCtx.globalCompositeOperation = "source-over"; + maskCtx.imageSmoothingEnabled = true; + maskCtx.clearRect(0, 0, mw, mh); + // Stacking n copies gives 1 - (1 - a)^n; drawing the mask onto itself + // squares that, so 3 copies doubled twice is exactly the 12th power. + let power = saturation; + let doublings = 0; + while (power % 2 === 0 && power > 3) { + power /= 2; + doublings += 1; + } + for (let pass = 0; pass < power; pass += 1) { + maskCtx.drawImage(this.fieldCanvas, 0, 0, mw, mh); + } + for (let pass = 0; pass < doublings; pass += 1) { + maskCtx.drawImage(this.maskCanvas, 0, 0); + } + } + + // GRAIN-4: draw a texture through the colour mask, so it never lands on + // the bare surface. Call buildColourMask() first. + drawMaskedTexture(texture, alpha = 1) { + const grainCtx = this.grainMaskCtx; + grainCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + grainCtx.globalCompositeOperation = "source-over"; + grainCtx.clearRect(0, 0, this.width, this.height); + grainCtx.imageSmoothingEnabled = false; + grainCtx.drawImage(texture, 0, 0, this.width, this.height); + grainCtx.globalCompositeOperation = "destination-in"; + grainCtx.imageSmoothingEnabled = true; + grainCtx.drawImage(this.maskCanvas, 0, 0, this.width, this.height); + grainCtx.globalCompositeOperation = "source-over"; + + this.ctx.save(); + this.ctx.globalAlpha = alpha; + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(this.grainCanvas, 0, 0, this.width, this.height); + this.ctx.restore(); + } + + // F1: the fine noise layer, through the colour mask (GRAIN-4). + drawMaskedOverlay(theme) { + const pattern = getOverlayPattern(theme, this.dpr, this.grainMaskCtx); + if (!pattern) return; + const film = FILM[theme]; + const grainCtx = this.grainMaskCtx; + grainCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + grainCtx.globalCompositeOperation = "source-over"; + grainCtx.clearRect(0, 0, this.width, this.height); + grainCtx.fillStyle = pattern; + grainCtx.fillRect(0, 0, this.width, this.height); + grainCtx.globalCompositeOperation = "destination-in"; + grainCtx.imageSmoothingEnabled = true; + grainCtx.drawImage(this.maskCanvas, 0, 0, this.width, this.height); + grainCtx.globalCompositeOperation = "source-over"; + + this.ctx.save(); + this.ctx.globalAlpha = film.overlay; + this.ctx.globalCompositeOperation = film.overlayBlend; + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(this.grainCanvas, 0, 0, this.width, this.height); + this.ctx.restore(); + } + + // F1: the tone lift (Home's saturate/contrast), applied to the colour + // field only - the bare surface keeps the host's exact base colour. + applyToneLift(theme) { + const film = FILM[theme]; + const liftCtx = this.liftCtx; + const w = this.canvas.width; + const h = this.canvas.height; + liftCtx.setTransform(1, 0, 0, 1, 0, 0); + liftCtx.globalCompositeOperation = "copy"; + liftCtx.filter = `saturate(${film.saturate}) contrast(${film.contrast})`; + liftCtx.drawImage(this.canvas, 0, 0); + liftCtx.filter = "none"; + liftCtx.globalCompositeOperation = "destination-in"; + liftCtx.imageSmoothingEnabled = true; + liftCtx.drawImage(this.maskCanvas, 0, 0, w, h); + liftCtx.globalCompositeOperation = "source-over"; + + this.ctx.save(); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.globalAlpha = 1; + this.ctx.globalCompositeOperation = "source-over"; + this.ctx.drawImage(this.liftCanvas, 0, 0); + this.ctx.restore(); + } + + drawSupport(t) { + if (this.layout === "neutral") { + this.drawNeutralSupport(t); + return; + } + + if (this.layout === "top-wash") { + if (this.isDark) this.drawDarkTopSupport(t); + else this.drawLightTopSupport(t); + return; + } + + if (this.layout === "bottom-wash") { + this.ctx.save(); + this.ctx.translate(0, this.height); + this.ctx.scale(1, -1); + if (this.isDark) this.drawDarkTopSupport(t); + else this.drawLightTopSupport(t); + this.ctx.restore(); + return; + } + + const draw = () => { + if (this.isDark) { + this.drawDarkRightEdgeSupport(t); + } else { + this.drawLightRightEdgeSupport(t); + } + }; + + if (this.layout === "left-wash") { + this.ctx.save(); + this.ctx.translate(this.width, 0); + this.ctx.scale(-1, 1); + draw(); + this.ctx.restore(); + } else { + draw(); + } + } + + drawDarkFalloff() { + const ctx = this.ctx; + if (this.layout === "top-wash" || this.layout === "bottom-wash") { + const startY = this.layout === "bottom-wash" ? this.height : 0; + const endY = this.layout === "bottom-wash" ? 0 : this.height; + const verticalFalloff = ctx.createLinearGradient(0, startY, 0, endY); + const tone = this.baseTone; + verticalFalloff.addColorStop(0, `rgba(${tone},0.26)`); + verticalFalloff.addColorStop(0.28, `rgba(${tone},0.16)`); + verticalFalloff.addColorStop(0.58, `rgba(${tone},0.06)`); + verticalFalloff.addColorStop(1, `rgba(${tone},0)`); + ctx.fillStyle = verticalFalloff; + ctx.fillRect(0, 0, this.width, this.height); + return; + } + const startX = this.layout === "left-wash" ? this.width : 0; + const endX = this.layout === "left-wash" ? 0 : this.width; + const falloff = ctx.createLinearGradient(startX, 0, endX, this.height); + const falloffTone = this.baseTone; + falloff.addColorStop(0, `rgba(${falloffTone},0.38)`); + falloff.addColorStop(0.42, `rgba(${falloffTone},0.2)`); + falloff.addColorStop(0.78, `rgba(${falloffTone},0.04)`); + falloff.addColorStop(1, `rgba(${falloffTone},0)`); + ctx.fillStyle = falloff; + ctx.fillRect(0, 0, this.width, this.height); + } + + ensureTextures() { + const nextKey = `${this.width}x${this.height}@${this.dpr}:${this.appearance}`; + if (nextKey === this.textureKey) return; + this.textureKey = nextKey; + + if (this.isDark) { + this.lightGrain = undefined; + this.darkGrain = this.getCachedTexture("dark-grain", () => this.createGrain("dark")); + this.darkDither = this.getCachedTexture("dark-dither", () => this.createDarkDither()); + } else { + this.lightGrain = this.getCachedTexture("light-grain", () => this.createGrain("light")); + this.darkGrain = undefined; + this.darkDither = undefined; + } + + this.blobCanvas.width = Math.max(1, Math.floor(this.width * this.dpr)); + this.blobCanvas.height = Math.max(1, Math.floor(this.height * this.dpr)); + this.blobCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + for (const target of [this.fieldCanvas, this.grainCanvas, this.liftCanvas]) { + target.width = this.blobCanvas.width; + target.height = this.blobCanvas.height; + } + this.maskCanvas.width = Math.max(1, Math.ceil(this.blobCanvas.width * MASK_SCALE)); + this.maskCanvas.height = Math.max(1, Math.ceil(this.blobCanvas.height * MASK_SCALE)); + this.fieldCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.grainMaskCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.staticFrameKey = ""; + } + + getCachedTexture(kind, factory) { + const key = `${kind}:${this.width}x${this.height}@${this.dpr}`; + const cachedTexture = textureCache.get(key); + if (cachedTexture) return cachedTexture; + return rememberTexture(key, factory()); + } + + // "page" film (the Home product): the full-surface grain Home was built + // on; its page recipe fades and tones it. + /* A product whose colour sits below full strength (Home's hero) mixes it + toward the base here, before the grain, so the grain keeps its one + value (GRAIN-6). */ + drawFieldOpacity() { + const opacity = this.product && this.product.fieldOpacity ? this.product.fieldOpacity[this.isDark ? "dark" : "light"] : 1; + if (!(opacity < 1)) return; + this.ctx.save(); + this.ctx.globalCompositeOperation = "source-over"; + this.ctx.globalAlpha = 1 - opacity; + this.ctx.fillStyle = this.baseFill; + this.ctx.fillRect(0, 0, this.width, this.height); + this.ctx.restore(); + } + + drawPlainTexture(texture, alpha) { + if (!texture) return; + this.ctx.save(); + this.ctx.globalAlpha = alpha; + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(texture, 0, 0, this.width, this.height); + this.ctx.restore(); + } + + /* ---- the Spotter radiance engine (product patterns spotterx, liveboard) ---- */ + + /* Locked: the values SpotterX and Liveboard ship (SBG_TUNING). Only the + state comes from the page; the admin lab may preview other values on + one surface (RadianceLanguageRenderer.admin). */ + get sbgTuning() { + const raw = (this.element.dataset.radianceState || "idle").trim().toLowerCase(); + let state = PRODUCT_STATES[raw]; + if (!state) { + warnOnce(this.element, "PRODUCT-4", `data-radiance-state="${raw}" is not a state. Use idle, reasoning or answered.`); + state = PRODUCT_STATES.idle; + } + const tuning = { ...SBG_TUNING }; + const admin = ADMIN_SPOTTER.get(this.element); + if (admin) { + Object.keys(SBG_TUNING).forEach((key) => { + if (Number.isFinite(admin[key])) tuning[key] = admin[key]; + }); + } + return { energy: state[0], settled: state[1], ...tuning }; + } + + /* The state levels for a theme: SBG_STATES, or the admin lab's preview. */ + sbgLevels(theme) { + const admin = ADMIN_SPOTTER.get(this.element); + const levels = admin && admin.levels && admin.levels[theme]; + return levels ? { ...SBG_STATES[theme], ...levels } : SBG_STATES[theme]; + } + + /* The grain strength for a theme: the language's one grain (FILM, + GRAIN-6), or the admin lab's preview of a new one. */ + sbgGrainStrength(theme) { + const admin = ADMIN_SPOTTER.get(this.element); + const grain = admin && admin.grain && admin.grain[theme]; + return Number.isFinite(grain) ? clamp(grain, 0, 1) : FILM[theme].grain; + } + + ensureSbg(tune) { + const key = `${this.width}x${this.height}`; + if (this.sbg && this.sbg.key === key) return this.sbg; + const prev = this.sbg; + const W = this.width; + const H = this.height; + const spacing = clamp(W / 6.4, 170, 300); + const radius = spacing * 1.6; + /* A blob wraps from one edge to the other only when fully out of + view: widest horizontal extent of the tilted, stretched, breathing + ellipse (~2.4 radii) + wander + blur. */ + const reachX = radius * 2.4 + spacing * 0.5 + 80; + const count = Math.max(6, Math.ceil((W + reachX * 2) / spacing)); + const band = count * spacing; + const rand = (lo, hi) => lo + Math.random() * (hi - lo); + const blobs = []; + let walk = { i: 1, d: 1, hold: false, cool: false, warm: 0, run: 1 }; + for (let i = 0; i < count; i += 1) { + const blob = { slot: i, lap: null }; + RadianceSurface.reseedBlob(blob, walk, spacing, band, W); + blobs.push(blob); + walk = RadianceSurface.walkHue(walk); + } + /* The reasoning stream: fewer, smaller lights flowing the other way. */ + const streamSpacing = spacing * 1.7; + const streamCount = Math.max(4, Math.ceil((W + reachX * 2) / streamSpacing)); + const streamBand = streamCount * streamSpacing; + const stream = []; + for (let i = 0; i < streamCount; i += 1) { + stream.push({ + x0: -(streamBand - W) / 2 + (i + 0.5 + rand(-0.25, 0.25)) * streamSpacing, + sig: rand(0.8, 1.2), + f: [rand(0.0006, 0.001), rand(0.0011, 0.0017), rand(0.0005, 0.0011), rand(0.0007, 0.0013)], + p: [rand(0, TAU), rand(0, TAU), rand(0, TAU), rand(0, TAU)], + bp: rand(0, TAU), + bMs: rand(1500, 2800) + }); + } + const lw = Math.ceil((W + SBG_PAD * 2) * SBG_SCALE); + const lh = Math.ceil((H + SBG_PAD) * SBG_SCALE); + const layer = () => { + const canvas = document.createElement("canvas"); + canvas.width = lw; + canvas.height = lh; + return { canvas, ctx: canvas.getContext("2d") }; + }; + this.sbg = { + key, W, H, spacing, radius, band, blobs, lw, lh, + margin: (band - W) / 2, + stream, streamSpacing, streamBand, + streamMargin: (streamBand - W) / 2, + ax: new Float32Array(count), + ah: new Array(count), + blob: layer(), + field: layer(), + grainKey: "", grain: null, + clock: prev ? prev.clock : rand(0, 600000), + chorus: prev ? prev.chorus : 0, + energy: prev ? prev.energy : tune.energy, + vel: prev ? prev.vel : 0, + target: prev ? prev.target : tune.energy, + settled: prev ? prev.settled : tune.settled, + settledVel: prev ? prev.settledVel : 0, + last: 0, + cost: prev ? prev.cost : 0, + drawnH: H, + drawnTheme: "" + }; + return this.sbg; + } + + /* Home's hero grain: the canonical passes at the hero's opacity. */ + sbgGrain(S) { + const strength = this.sbgGrainStrength(this.isDark ? "dark" : "light"); + const key = `${S.W}x${S.H}:${this.appearance}:${strength}`; + if (S.grainKey === key && S.grain) return S.grain; + const texture = document.createElement("canvas"); + texture.width = S.W; + texture.height = S.H; + const tctx = texture.getContext("2d"); + tctx.imageSmoothingEnabled = false; + if (this.isDark) { + tctx.globalAlpha = 0.42 * strength; + tctx.drawImage(this.darkDither, 0, 0, S.W, S.H); + tctx.globalAlpha = strength; + tctx.drawImage(this.darkGrain, 0, 0, S.W, S.H); + } else { + tctx.globalAlpha = strength; + tctx.drawImage(this.lightGrain, 0, 0, S.W, S.H); + } + S.grain = texture; + S.grainKey = key; + return texture; + } + + /* One step of the colour walk (see SBG_PATH). State: i (index on the + path), d (direction), hold (repeat once), cool (peach seen - no second + peach before violet), warm / run (length of the current warm / cool + stretch). */ + static walkHue(state) { + const isWarm = (index) => index >= 3; + let next; + if (SBG_PATH[state.i] === "pink" && state.warm >= 3) { + next = { i: 2, d: -1, hold: false, cool: true }; + } else if (!isWarm(state.i) && state.run >= 5) { + next = { i: state.i + 1, d: 1, hold: false, cool: false }; + } else { + next = RadianceSurface.walkStep(state); + if (!isWarm(state.i) && isWarm(next.i) && state.run < 2) { + next = { i: 1, d: -1, hold: false, cool: next.cool }; + } + } + next.warm = isWarm(next.i) ? (isWarm(state.i) ? state.warm + 1 : 1) : 0; + next.run = isWarm(next.i) ? 0 : (isWarm(state.i) ? 1 : state.run + 1); + return next; + } + + static walkStep(state) { + const { i, hold } = state; + let { d, cool } = state; + if (hold) return { i: i + d, d, hold: false, cool }; + const roll = Math.random(); + const hue = SBG_PATH[i]; + if (hue === "peach") return { i: i - 1, d: -1, hold: false, cool: true }; + if (hue === "mint") return { i: i + 1, d: 1, hold: false, cool }; + if (hue === "blue") { + if (d === -1) { + if (roll < 0.3) return { i: i - 1, d: -1, hold: false, cool }; + if (roll < 0.72) return { i, d: 1, hold: true, cool }; + return { i: i + 1, d: 1, hold: false, cool }; + } + if (roll < 0.1) return { i: i - 1, d: -1, hold: false, cool }; + if (roll < 0.18) return { i, d, hold: false, cool }; + return { i: i + 1, d, hold: false, cool }; + } + if (hue === "violet") { + cool = false; + if (roll < 0.14) d = -d; + else if (roll < 0.22) return { i, d, hold: false, cool }; + return { i: i + d, d, hold: false, cool }; + } + if (d === 1 && !cool) { + if (roll < 0.4) return { i: i + 1, d: 1, hold: false, cool }; + if (roll < 0.5) return { i, d, hold: false, cool }; + return { i: i - 1, d: -1, hold: false, cool }; + } + if (roll < 0.1) return { i, d: -1, hold: false, cool }; + return { i: i - 1, d: -1, hold: false, cool }; + } + + /* A blob's identity: hue, slot jitter, size, and per-blob frequencies + (rad per clock-ms) - every motion is 2-3 sines at incommensurate + ratios, so no blob ever retraces a path. */ + static reseedBlob(blob, walk, spacing, band, W) { + const rand = (lo, hi) => lo + Math.random() * (hi - lo); + const wx = rand(0.00045, 0.0008); + const wy = rand(0.0004, 0.0009); + const wr = rand(0.0006, 0.0011); + const ws = rand(0.0005, 0.0012); + const wt = rand(0.0005, 0.0012); + const wq = rand(0.00025, 0.0005); + blob.walk = walk; + blob.hue = SBG_PATH[walk.i]; + blob.x0 = -(band - W) / 2 + (blob.slot + 0.5 + rand(-0.18, 0.18)) * spacing; + blob.sig = rand(0.92, 1.08); + blob.amp = rand(0.9, 1.1); + blob.f = [ + wx, wx * rand(1.52, 1.72), wx * rand(2.3, 2.6), + wy, wy * rand(1.6, 1.9), + wr, wr * rand(1.4, 1.7), + ws, ws * rand(1.5, 1.8), + wt, wt * rand(1.5, 1.8), + wq, wq * rand(1.6, 1.9) + ]; + blob.p = Array.from({ length: 13 }, () => rand(0, TAU)); + const B = SBG_MOTION.breathMs; + blob.bRest = rand(B.rest[0], B.rest[1]); + blob.bReason = rand(B.reason[0], B.reason[1]); + blob.bCalm = rand(B.calm[0], B.calm[1]); + blob.bp = rand(0, TAU); + } + + /* Exact solution of a critically damped spring step: stable at any dt. */ + static spring(value, velocity, target, omega, dt) { + const e0 = value - target; + const k = velocity + omega * e0; + const decay = Math.exp(-omega * dt); + return [target + (e0 + k * dt) * decay, (velocity - omega * k * dt) * decay]; + } + + renderSbg(timestamp) { + const started = performance.now(); + const tune = this.sbgTuning; + const S = this.ensureSbg(tune); + const theme = this.isDark ? "dark" : "light"; + const still = this.isStill; + const M = SBG_MOTION; + + const dtMs = S.last ? clamp(timestamp - S.last, 0, 250) : 0; + S.last = timestamp; + const dt = dtMs / 1000; + if (tune.energy > 0.5 && S.target <= 0.5) S.chorus = 0; + S.target = tune.energy; + [S.energy, S.vel] = RadianceSurface.spring(S.energy, S.vel, tune.energy, + (tune.energy > S.energy ? M.rise : M.fall) / tune.pace, dt); + [S.settled, S.settledVel] = RadianceSurface.spring(S.settled, S.settledVel, tune.settled, M.settle / tune.pace, dt); + const E = clamp(S.energy, 0, 1); + const calm = clamp(S.settled, 0, 1); + + const m = tune.flow; + const rateRest = mix(M.speedDefault, M.speedSettled, calm) * m; + const rate = Math.min(M.speedCap, mix(rateRest, M.speedReason * m, E)); + const tempo = Math.sqrt(Math.max(m, 0.05) / 2.5); + if (!still) { + S.clock += dtMs * rate; + S.chorus += (TAU * dtMs / M.chorusMs) * tempo * (1 + 0.06 * Math.sin(S.clock * 0.00013)); + } + const t = S.clock; + const breath = still ? 0 : tune.breath; + const sizeSwing = mix(mix(M.breathSize.rest, M.breathSize.calm, calm), M.breathSize.reason, E) * breath; + const glowSwing = mix(mix(M.breathGlow.rest, M.breathGlow.calm, calm), M.breathGlow.reason, E) * breath; + const chorus = wave(S.chorus); + const chorusSize = 1 + M.chorusSize * E * breath * (chorus - 0.3); + const chorusGlow = 1 + M.chorusGlow * E * breath * (chorus - 0.5); + const state = this.sbgLevels(theme); + const restLevel = mix(state.rest, Math.min(1, state.calm * tune.calmGain) * state.rest, calm); + const level = mix(restLevel, state.reason, E); + const alphaNow = level * tune.gain; + const boost = level / mix(restLevel, state.floor, E); + const palette = SBG_PALETTE[theme]; + const W = S.W; + const H = S.H; + const baseLift = SBG_BASE_LIFT + tune.lift; + const lift = baseLift - S.radius * SBG_CALM_RISE * calm; + const wander = S.spacing * (1 + M.wanderReason * E); + + /* Blobs, at quarter resolution, in CSS-pixel coordinates. The drift's + own speed swells and eases on two slow sines (never reversing). */ + const bctx = S.blob.ctx; + bctx.setTransform(1, 0, 0, 1, 0, 0); + bctx.globalCompositeOperation = "source-over"; + bctx.clearRect(0, 0, S.lw, S.lh); + bctx.setTransform(SBG_SCALE, 0, 0, SBG_SCALE, SBG_PAD * SBG_SCALE, SBG_PAD * SBG_SCALE); + const flowX = t * M.flow + S.spacing * (0.5 * Math.sin(t * 0.000061) + 0.3 * Math.sin(t * 0.000097 + 1.3)); + /* A blob that has just wrapped (it is off-screen, entering on the + left) is reborn: the next step of the colour walk from its + right-hand neighbour, fresh size, timing and shape. */ + for (const blob of S.blobs) { + const lap = Math.floor((blob.x0 + flowX + S.margin) / S.band); + if (blob.lap !== null && lap !== blob.lap) { + const right = S.blobs[(blob.slot + 1) % S.blobs.length]; + RadianceSurface.reseedBlob(blob, RadianceSurface.walkHue(right.walk), S.spacing, S.band, W); + blob.lap = Math.floor((blob.x0 + flowX + S.margin) / S.band); + } else { + blob.lap = lap; + } + } + const draw = (x, y, r, sx, sy, tilt, rgb, opacity) => { + bctx.save(); + bctx.translate(x, y); + bctx.rotate(tilt); + bctx.scale(sx, sy); + const gradient = bctx.createRadialGradient(0, 0, 0, 0, 0, r); + gradient.addColorStop(0, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${opacity})`); + gradient.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0)`); + bctx.fillStyle = gradient; + bctx.beginPath(); + bctx.arc(0, 0, r, 0, TAU); + bctx.fill(); + bctx.restore(); + }; + let placed = 0; + for (const blob of S.blobs) { + const f = blob.f; + const q = blob.p; + let x = (blob.x0 + flowX + S.margin) % S.band; + if (x < 0) x += S.band; + x -= S.margin; + x += wander * (0.24 * Math.sin(t * f[0] + q[0]) + 0.13 * Math.sin(t * f[1] + q[1]) + 0.07 * Math.sin(t * f[2] + q[2])); + S.ax[placed] = x; + S.ah[placed] = blob.hue; + placed += 1; + if (!still) blob.bp += (TAU * dtMs / mix(mix(blob.bRest, blob.bCalm, calm), blob.bReason, E)) * tempo; + const own = wave(blob.bp); + const r = S.radius * SBG_SIZE[blob.hue] * blob.sig + * (1 + 0.1 * Math.sin(t * f[5] + q[5]) + 0.05 * Math.sin(t * f[6] + q[6])) + * (1 + sizeSwing * (own - 0.4)) * chorusSize; + const sx = 1 + 0.16 * Math.sin(t * f[7] + q[7]) + 0.06 * Math.sin(t * f[8] + q[8]); + const sy = SBG_TALL * (1 + 0.18 * Math.cos(t * f[9] + q[9]) + 0.07 * Math.sin(t * f[10] + q[10])); + const tilt = 0.3 * Math.sin(t * f[11] + q[11]) + 0.16 * Math.sin(t * f[12] + q[12]); + const cos = Math.cos(tilt); + const sin = Math.sin(tilt); + const extent = r * Math.sqrt(sx * sx * cos * cos + sy * sy * sin * sin); + if (x + extent < -SBG_PAD || x - extent > W + SBG_PAD) continue; + const y = lift + S.radius * (-0.1 + 0.05 * Math.cos(t * f[3] + q[3]) + 0.03 * Math.sin(t * f[4] + q[4])); + const opacity = Math.min(1, alphaNow * SBG_ROLE[blob.hue] * blob.amp * (1 + glowSwing * (own - 0.5)) * chorusGlow); + draw(x, y, r, sx, sy, tilt, palette[blob.hue], opacity); + } + + /* The reasoning stream: lights flowing the other way along the edge, + each taking the colour of the light beneath it (never a new hue, + so crossings brighten instead of muddying). */ + if (E > 0.01) { + const flowS = t * M.flow * M.streamFlow + S.streamSpacing * 0.4 * Math.sin(t * 0.00011 + 0.7); + const reach = S.spacing * 0.9; + for (const light of S.stream) { + const f = light.f; + const q = light.p; + let x = (light.x0 + flowS + S.streamMargin) % S.streamBand; + if (x < 0) x += S.streamBand; + x -= S.streamMargin; + x += S.streamSpacing * (0.2 * Math.sin(t * f[0] + q[0]) + 0.1 * Math.sin(t * f[1] + q[1])); + if (!still) light.bp += (TAU * dtMs / light.bMs) * tempo; + const lb = wave(light.bp); + const r = S.spacing * 0.8 * light.sig * (1 + 0.2 * lb); + const sx = 1.1 + 0.15 * Math.sin(t * f[2] + q[2]); + const sy = 1.4 + 0.2 * Math.cos(t * f[3] + q[3]); + if (x + r * sx < -SBG_PAD || x - r * sx > W + SBG_PAD) continue; + let cr = 0, cg = 0, cb = 0, cw = 0; + for (let k = 0; k < placed; k += 1) { + const u = (x - S.ax[k]) / reach; + if (u > 3 || u < -3) continue; + const w = Math.exp(-u * u); + const rgb = palette[S.ah[k]]; + cr += w * rgb[0]; + cg += w * rgb[1]; + cb += w * rgb[2]; + cw += w; + } + if (cw < 1e-4) continue; + const y = lift + r * (-0.2 + 0.1 * Math.cos(t * f[2] + q[3])); + const opacity = Math.min(1, alphaNow * M.streamGlow * E * (0.55 + 0.45 * lb)); + draw(x, y, r, sx, sy, 0, [Math.round(cr / cw), Math.round(cg / cw), Math.round(cb / cw)], opacity); + } + } + + /* Blur (BLEND-2); while reasoning, fold the extra brightness back to + the resting level below the top band. */ + const fctx = S.field.ctx; + fctx.setTransform(1, 0, 0, 1, 0, 0); + fctx.globalCompositeOperation = "source-over"; + fctx.clearRect(0, 0, S.lw, S.lh); + fctx.filter = `blur(${SBG_BLUR[theme] * SBG_SCALE}px)`; + fctx.drawImage(S.blob.canvas, 0, 0); + fctx.filter = "none"; + if (boost > 1.001) { + fctx.setTransform(SBG_SCALE, 0, 0, SBG_SCALE, SBG_PAD * SBG_SCALE, SBG_PAD * SBG_SCALE); + fctx.globalCompositeOperation = "destination-in"; + const band = fctx.createLinearGradient(0, 0, 0, SBG_ENVELOPE.band * 5); + for (let i = 0; i <= 10; i += 1) { + const y = (i / 10) * SBG_ENVELOPE.band * 5; + band.addColorStop(i / 10, `rgba(0,0,0,${(1 + (boost - 1) * Math.exp(-y / SBG_ENVELOPE.band)) / boost})`); + } + fctx.fillStyle = band; + fctx.fillRect(-SBG_PAD, -SBG_PAD, W + SBG_PAD * 2, H + SBG_PAD); + fctx.globalCompositeOperation = "source-over"; + } + + /* Compose: base -> field (BLEND-1) -> grain. Only the rows the page's + fade reaches change; below them the canvas is bare base, left as + drawn (a theme change repaints it once). */ + const y0 = Math.max(0, baseLift); + const litH = Math.min(H, Math.ceil(y0 + SBG_ENVELOPE.wash * tune.wash * 4.5 + 8)); + const paintKey = `${this.appearance}|${this.baseKey}`; + const drawH = S.drawnTheme === paintKey ? Math.max(litH, S.drawnH) : H; + S.drawnTheme = paintKey; + const inv = 1 / SBG_SCALE; + const ctx = this.ctx; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.globalAlpha = 1; + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, W, drawH); + ctx.clip(); + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = this.baseFill; + ctx.fillRect(0, 0, W, drawH); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "low"; + ctx.globalCompositeOperation = this.isDark ? "screen" : "source-over"; + ctx.drawImage(S.field.canvas, 0, 0, S.lw, S.lh, -SBG_PAD, -SBG_PAD, S.lw * inv, S.lh * inv); + ctx.globalCompositeOperation = "source-over"; + ctx.imageSmoothingEnabled = false; + ctx.drawImage(this.sbgGrain(S), 0, 0, W, drawH, 0, 0, W, drawH); + ctx.restore(); + S.drawnH = litH; + + S.cost = S.cost ? S.cost * 0.92 + (performance.now() - started) * 0.08 : performance.now() - started; + } + + markReady() { + if (this.ready) return; + this.ready = true; + this.element.dataset.radianceReady = "true"; + this.element.dispatchEvent(new CustomEvent("radiance-local-ready", { bubbles: true })); + } + + getFrameKey() { + return [ + this.textureKey, + this.lastConfigKey, + this.baseKey, + overlayGeneration, + this.width, + this.height, + this.dpr, + prefersReducedMotion ? "reduced" : "full" + ].join(":"); + } + + render(timestamp) { + if (!this.isRenderable()) return; + this.applyPatternShorthand(); + this.ensureSize(); + if (this.isSpotter) { + this.ensureTextures(); + this.resolveBase(); + this.renderSbg(timestamp || performance.now()); + this.markReady(); + return; + } + this.ensureBlobs(); + this.ensureTextures(); + this.resolveBase(); + + const frameKey = this.getFrameKey(); + if (this.isStill && this.staticFrameKey === frameKey) return; + + const time = this.isStill ? 0 : getTimelineTime(); + const t = time * this.speed; + // F2: breathing runs on the shared timeline at sqrt(speed). + this.breathT = time * Math.sqrt(Math.max(this.speed, 0.05)); + const ctx = this.ctx; + ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = this.baseFill; + ctx.fillRect(0, 0, this.width, this.height); + const theme = this.isDark ? "dark" : "light"; + const film = FILM[theme]; + const pageFilm = this.filmMode === "page"; + + if (this.isDark) { + this.blobCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + this.blobCtx.clearRect(0, 0, this.width, this.height); + for (const blob of this.blobs) { + blob.update(t, this.width, this.height, this.mouse, this.breathT); + blob.draw(this.blobCtx); + } + + ctx.save(); + ctx.filter = `blur(${this.layout === "neutral" ? 40 : 64}px)`; + ctx.globalCompositeOperation = "screen"; + ctx.drawImage(this.blobCanvas, 0, 0, this.width, this.height); + ctx.restore(); + + this.drawSupport(t); + + if (this.darkGlow === "subtle") { + this.drawDarkFalloff(); + } + + if (pageFilm) { + this.drawFieldOpacity(); + this.drawPlainTexture(this.darkDither, film.dither); + this.drawPlainTexture(this.darkGrain, film.grain); + } else { + this.renderDarkColourField(t); + this.buildColourMask(GRAIN_MASK_SATURATION); + this.drawMaskedTexture(this.darkDither, film.dither); + this.drawMaskedTexture(this.darkGrain, film.grain); + this.drawMaskedOverlay(theme); + this.applyToneLift(theme); + } + } else { + this.renderColourField(t); + ctx.drawImage(this.fieldCanvas, 0, 0, this.width, this.height); + if (pageFilm) { + this.drawFieldOpacity(); + this.drawPlainTexture(this.lightGrain, film.grain); + } else { + this.buildColourMask(GRAIN_MASK_SATURATION); + this.drawMaskedTexture(this.lightGrain, film.grain); + this.drawMaskedOverlay(theme); + this.applyToneLift(theme); + } + } + + this.markReady(); + if (this.isStill) this.staticFrameKey = frameKey; + } + } + + // F1: the fine noise layer, rasterised once per theme and pixel density. + // The SVG loads asynchronously; surfaces repaint once it is ready. + const overlaySources = {}; + const overlayPatterns = new Map(); + let overlayGeneration = 0; + + function getOverlaySource(theme) { + if (overlaySources[theme]) return overlaySources[theme]; + const image = new Image(); + const entry = { image, ready: false }; + image.onload = () => { + entry.ready = true; + overlayGeneration += 1; + surfaces.forEach((surface) => { + surface.staticFrameKey = ""; + }); + }; + image.src = `data:image/svg+xml,${encodeURIComponent(FILM_OVERLAY_SVG[theme])}`; + overlaySources[theme] = entry; + return entry; + } + + function getOverlayPattern(theme, dpr, ctx) { + const key = `${theme}@${dpr}`; + if (overlayPatterns.has(key)) return overlayPatterns.get(key); + const source = getOverlaySource(theme); + if (!source.ready) return null; + const tile = document.createElement("canvas"); + const size = Math.max(1, Math.round(FILM_OVERLAY_TILE * dpr)); + tile.width = size; + tile.height = size; + tile.getContext("2d").drawImage(source.image, 0, 0, size, size); + const pattern = ctx.createPattern(tile, "repeat"); + if (pattern && pattern.setTransform && typeof DOMMatrix === "function") { + pattern.setTransform(new DOMMatrix().scaleSelf(1 / dpr, 1 / dpr)); + } + overlayPatterns.set(key, pattern); + return pattern; + } + + function scan(root = document) { + root.querySelectorAll?.(".radiance-local").forEach((element) => { + if (surfaceByElement.has(element)) return; + const surface = new RadianceSurface(element); + surfaceByElement.set(element, surface); + surfaces.add(surface); + }); + + surfaces.forEach((surface) => { + if (surface.element.isConnected) return; + surface.destroy(); + surfaces.delete(surface); + }); + + startLoop(); + } + + function frame(timestamp) { + if (!isDocumentVisible) { + raf = 0; + return; + } + + surfaces.forEach((surface) => surface.render(timestamp)); + raf = window.requestAnimationFrame(frame); + } + + function startLoop() { + if (raf || !isDocumentVisible) return; + raf = window.requestAnimationFrame(frame); + } + + function stopLoop() { + if (!raf) return; + window.cancelAnimationFrame(raf); + raf = 0; + } + + function handleVisibilityChange() { + isDocumentVisible = document.visibilityState !== "hidden"; + if (isDocumentVisible) { + startLoop(); + } else { + stopLoop(); + } + } + + function observeDomChanges() { + if (mutationObserver || !document.body || !("MutationObserver" in window)) return; + mutationObserver = new MutationObserver((mutations) => { + const needsScan = mutations.some((mutation) => ( + [...mutation.addedNodes].some((node) => node.nodeType === Node.ELEMENT_NODE) + || [...mutation.removedNodes].some((node) => node.nodeType === Node.ELEMENT_NODE) + )); + if (needsScan) scan(); + }); + mutationObserver.observe(document.body, { childList: true, subtree: true }); + } + + function renderGrainPreview(canvas, mode = "light") { + if (!(canvas instanceof HTMLCanvasElement)) return; + const safeMode = mode === "dark" ? "dark" : "light"; + const rect = canvas.getBoundingClientRect(); + const maxWidth = Number(canvas.dataset.grainPreviewMaxWidth || "0"); + const maxHeight = Number(canvas.dataset.grainPreviewMaxHeight || "0"); + const measuredWidth = Math.max(1, Math.round(rect.width || canvas.clientWidth || canvas.width || 280)); + const measuredHeight = Math.max(1, Math.round(rect.height || canvas.clientHeight || canvas.height || 120)); + const width = maxWidth > 0 ? Math.min(measuredWidth, maxWidth) : measuredWidth; + const height = maxHeight > 0 ? Math.min(measuredHeight, maxHeight) : measuredHeight; + const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR); + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + canvas.width = Math.max(1, Math.floor(width * dpr)); + canvas.height = Math.max(1, Math.floor(height * dpr)); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.fillStyle = safeMode === "dark" ? DARK_SURFACE : "#ffffff"; + ctx.fillRect(0, 0, width, height); + + if (safeMode === "dark") { + const dither = RadianceSurface.prototype.createDarkDither.call({ width, height, dpr: 1 }); + ctx.save(); + ctx.globalAlpha = 0.42; + ctx.imageSmoothingEnabled = false; + ctx.drawImage(dither, 0, 0, width, height); + ctx.restore(); + } + + const grain = RadianceSurface.prototype.createGrain.call({ width, height }, safeMode); + ctx.save(); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(grain, 0, 0, width, height); + ctx.restore(); + } + + function handleReducedMotionChange(event) { + prefersReducedMotion = event.matches; + surfaces.forEach((surface) => { + surface.staticFrameKey = ""; + }); + startLoop(); + } + + /* Read-only snapshot of every surface (tests and tooling). */ + function inspect() { + return [...surfaces].map((surface) => ({ + pattern: surface.productName || surface.element.dataset.radiancePattern || surface.layout, + product: surface.productName || null, + engine: surface.product ? surface.product.engine : "template", + appearance: surface.appearance, + base: surface.baseFill, + width: surface.width, + height: surface.height, + energy: surface.sbg ? surface.sbg.energy : null, + settled: surface.sbg ? surface.sbg.settled : null, + clock: surface.sbg ? surface.sbg.clock : null, + costMs: surface.sbg ? surface.sbg.cost : null + })); + } + + window.RadianceLanguageRenderer = { + version: RADIANCE_VERSION, + refresh() { + scan(); + startLoop(); + }, + renderGrainPreview, + grainSize: RADIANCE_FILM_GRAIN_SIZE, + patterns: RADIANCE_PATTERNS, + products: PRODUCT_PATTERNS, + resolvePattern: resolveRadiancePattern, + inspect, + /* For the Radiance admin's Spotter lab (spotter-lab/) and nothing else: + preview a change to the Spotter engine on one surface before it is + made in this file. Product patterns stay fixed (PRODUCT-2), nothing + persists, and the linter fails any use outside spotter-lab/ (PRODUCT-5). */ + admin: Object.freeze({ + spotterDefaults() { + return JSON.parse(JSON.stringify({ ...SBG_TUNING, levels: SBG_STATES, grain: { light: FILM.light.grain, dark: FILM.dark.grain } })); + }, + tuneSpotter(element, values) { + if (element) ADMIN_SPOTTER.set(element, JSON.parse(JSON.stringify(values || {}))); + }, + resetSpotter(element) { + if (element) ADMIN_SPOTTER.delete(element); + } + }), + get prefersReducedMotion() { + return prefersReducedMotion; + } + }; + + scan(); + observeDomChanges(); + document.addEventListener("visibilitychange", handleVisibilityChange); + reducedMotionQuery?.addEventListener?.("change", handleReducedMotionChange); + window.addEventListener("beforeunload", () => { + stopLoop(); + mutationObserver?.disconnect(); + surfaces.forEach((surface) => surface.destroy()); + surfaces.clear(); + textureCache.clear(); + }); +})(); From 6dd0a3a835ca013fe69c57cc95197d0a41225a9b Mon Sep 17 00:00:00 2001 From: ShashiSubramanya Date: Wed, 14 Oct 2026 07:03:16 +0530 Subject: [PATCH 5/5] 404 page updates --- src/assets/fonts/BB-Roller-Mono-Regular.otf | Bin 0 -> 133304 bytes src/assets/svg/not-found-illustration.svg | 110 +++++++++++++++ src/components/Page404/index.scss | 145 ++++++++++++++++++-- src/components/Page404/index.tsx | 71 ++++++++-- src/intl/en.json | 11 +- 5 files changed, 313 insertions(+), 24 deletions(-) create mode 100755 src/assets/fonts/BB-Roller-Mono-Regular.otf create mode 100644 src/assets/svg/not-found-illustration.svg diff --git a/src/assets/fonts/BB-Roller-Mono-Regular.otf b/src/assets/fonts/BB-Roller-Mono-Regular.otf new file mode 100755 index 0000000000000000000000000000000000000000..f9d13b23413804f84ce38326148f098ab1878072 GIT binary patch literal 133304 zcmdSC2Y6J~+V{P7n@MKUCzCQ^CX+^j2uzYmAQUAep$efBib6;NiKLlAQxUOaIfx=y zKm-&N1;m1iT?BhW1r;l(h>G2b2Ic$T>)w+D&++-rdEV!Fuj>u`_J8fQ*IwmbYp*u5 zlhTP3OGTJiDl{>;Xy{Py*s!rPgc!3&C__ITR$Mal;~PG`QYfpo2%&s7Y;5U-;q4#C z3uW~+LKLnaHeu3`8J6{BLV53WA^I#YozN%8|MN|Yg@|7xgqT%PSKj#2Q{}if>1QEw zc2rlDS4P=q;gWb8yj*$`>{ow~VFmuK-JZ zd0kca9)&pw-zJ21qOqa5<>Vs*=VLbEx>ty?!ng%4g*j*Ri=i_k2mT^7?I?W2yCWm+ z4C=`Pe{9mP(s3_Ju9B}1>Ufa?a{n(e2FBH#z^}PIdubqQA#{M{Qq;xI_|j;_Y+nB27g~~|6Cvc z-LQXcqnXIp-$DIJurHzZ6?FekYiA!3>?{6RsohXN^;f9P(6dE-@Y#T#f3i)`P1h)X z>Tjr@p>`Y$)AgC8XK)apdnpXHaca|aZ-c%`jzyE8ln#x%9=I#PAW%WTFBPPBIp2Wu zZD13iv5UqxYWKl;6lnng&3ZUIeWg+nPzopxN{_-5G&T!BWiJLHCS=dvfclSMJ}Ga(`+|ANhaVBlM?KsGg`@i@ zZ*+|q1gPA>@&@Cj_7(Iy8}1a|^qHta9Pw~(*G!evrF@W+z?TON)_=0Vf z`m2$E%0+HUpW+(|$o|26Qk=p31lvEAhY02)ST+h5^b6XS<}!4h(kFuPcC|q;ep2!e zmX(y^Av;o;&IR<$kl#6g!uL0!{w3IlQk$de1ogYYFr?Hbg3regxSK#QeX3J(Q=6l3 zWLrv${DRjgFO)}mCh`G=35KOS!tdl4-~c#=a~*mR8U`pHx+mCXD9%AZ1Eax0Kz6ML z)Sl`2Ne2{;%0_iT`JuEa?-ZWmq_WQibRUH+17!2Ad87FN#StV}XQUJdU8g)yx|AmQ z5ftBIKsKXt)3aLz=o;Nie&nXIQ&@^8=ug+~0gr&~U<;MmdZQx#TH=yeq!3MAs zYz5>`_fx#f0HyO^<$>z$U*&=Pi2td){5$sechmZJ!%=Q@bSn6i~dxU&9Ib{hyG9`zZ~|6ZON? zkCXyx*YO}5(6f;W&H@fVeI-HX)MpT6FS?iFBOL~2o6PUxroM#iNqrEx2?`f~#r zbiGTMU_2C-;tSsYclV?q3~@0S4l1w)rSf+5r*kSZ^@Ed4rTNw8;XV$}yRdXT8NeYNM3@>0zh4<)hadKbMXCvfvjI{f1O$Jq za8Mlh>A0W5m_H}Kf`13d%ZK1ma24|8WEH#2vh2nAO@R9CKLDOju^iVz$xRWeWeTN@ z{GDuPr91}HC6%_6`_sC55*E=nVITSj{VKU*jmyCw>|$$(ZG|u}dt45C30O(0a*oJR zawwc(2KEZ|3T$B0p11rNGQ{f@gGK1clj^-E$19U{Rc!ML<#xKL{Hfjx@d_QsI3=Rv zuHJMJ(9tP+n{HKDMUm;&M7kJhx^>|e^`_ep1I5**+aeOhKGPk7G(Iuip$B`D_y%o4YifMy@j>4UU{Y2 z+f>zD)il4VviFc7-ti5!wN*{t(GB$t-my&$-ic-7tLC=WmN#Yh_W67R5W$@u4-zoX{h&dL}Mp-eLi+pH?&kV)X%32z5DeWP**;$s-b00@7kK# zIlc3G=llEi&HMWlQI_hKmd5^l`Yc$mp!aMlT60TlWlaO>w?QSyaJyK8_S}2>y8mdUhF}u6V_0 zoYey_KCEeyI#HA%OchdZMYwYKX5(I;{5Qai!zo4*A`{`3IyXIiB zEJrhJLb>&xlB2O=0(>ZcUGrIukS(G@hM(VYM{m&&{|1OUlxrU1q%d<(x?03M8#e9@ z3-=cJ!Y}&bU!E-Oe_BSa7pl1y)MS6r2mcnxf4z}Lu-2M!wH38OCF0IQLywgXH~NC` zP39AMIz1KAU!y)1{uTwFwTVy>&<{ZI6I4F?yQ6+UU#oA_Z`ThS;l@aPlfF}bK)*@< zMZZ;lTwkx>rJd0J(Eij;>Oxm^Ro8S~H}qTdJM|O#Z+g3ahcN>AaKnz>VV`1ABF+-y z#4MDj61J-s3&k37i?~&+M_avLyewW7Z;AclkiJ7-t>14<)wdW|C`nSee`pw2DVN8g=sy^CBg_ajLbT5fLp`E? zuI*FGluz^x`Z_~3Z2A|5Fs%C1`ZM}%`g4S~;Tc)*sWK*Z1oC^cVCtV~Cn2 z-qn}tpX<-+&#CXL9~hJMHTsMC9wP$vn2S$13(!wv%odZyG(682qJ)==o#J8fgtE%G z)VRjD%DC9L+_>7f#JJK}ZY&eJeJKWmVwl1T`-j33OT@iOwz34ayh>T2tW;JjYn3~c zyOsNukCiW#ld4x;jyvs?Jbns^w~hTBX*g^V9~lNo`dZs+Xu&sn@A1 z)ivr`b)Wj7rfXqZjFzOOX<1q?EnDlSU9K(HuF+O#cW67b*Yq$wQg`Yuy^o%!_t#7H z8Tw4ULT}Y?KwH{~R<#SZcvgQ!|ILUt+(u@^neKTW!xQOocrrYFJbgW9dWLyMdnR}) zJm+~X^W5NB=lRWR^Tv8zUXM4`+uiH)_V*Th$9ZRY8@-FXS9v#iH)9ys=6%5XkoOVq z6Df|A_!MtSYD)K%-YFAPrlp*da&yX^DVtLsNZFIJH&sowq(-L3q`Fc)sXbGNr_M^P z%GjRqa>nZ!Uvxj(!{HN~uWeWVJbCgYEFOyX>_eN~rud~5uQRQ9JFK`}c~$vB`CUy= zyQ?{{ViBx37FH}%r>p0fR;-2<>$_O-8q8w z(mlOB`JREEV$Vp=SkG)vt!JrcC9JsKt9l*Y1aCL57gp@!Es$262`jdEm%@s7!HQeG zJ3Fix*I~u6DU(vBr>u}x++kYLfEA;=Sg|r=TgFSK6=S3o6|$nS0(LZiPKN!t4jwhu3y(5LH_>!=QEo1;OEG8c|=~0 zTy=;aBYln(B1Hcqy$^qKqzA%eAI3cJ@MVWDJ$%98 z+Qah>qrd*_H}L&uUwrodXODb_Z$f;$3N(H^{i9j<%Edv72|swGl_BWGPN=b(EQNLt zU(mL|n)hhiw70akwU4x;+K)IV@a+xlSB63c{rN7OA2aVisvXmQ)=mWdP2b;1H5|1Q zXn$k%IeIllkw$$Dj_c8TeWZV)f2JSRkLzvv@8}C7&;nzPIOANS)~Kg@jYgx%SZFLZ zE-;p$2fZA<)kYjEz>4%^X=3H0rP?B6l2?$r={sAV z&k3H(vs&B>fu7X>-G4A97-t!m8Kc!B`qLP3#$Y5E zhp{PJ&w;J&#wdM_af$9zKgS4kseZA3nZ5y|kcv^oCCA7ljFJN}I^<%6nP!eKS(trX zE=QSF;u1X|ZV*q%v1S*(_j9xIGse<^7+EG`Je`QKZWcz zc|lyKd_d!~xJ@}G)+yhJwaVAx4&}Jmr2HT@D&LC@%6Aw?+r(BSAns9q6)&lAVz25D z52^yYE>7{h8YA|pvEp?#2|F#R;$5ssdy0?MUg9J5bo54B&>vkOu2kL;PpM(z1re$g ziHXV@alZ1Rct};mZq#jmie)LFG}5P9C)f<_pWr z*}`%>i&q-6jY?y#G0&(oYK(GY7M|C0jX6fOajkKk_Ja1R_L}yC_Kfzd_MEmydtTeC z?bBY@-p~SCoA#Ub9eRv+wFBBg?LF;%?E~$Q_96P7kF`&R0M9 z^;`A0`l}jH+tlCG-_>^Yg!+g2Cq_&~(=-+H4?_#lLN%)vp*gfzZJ0J(8=;NVMr&iV z$=VdHOq;4r)23@Pw6nEyv{_oYR-w(-Dzz%DTC36KYV}&P)}obat=cH9POH@#v_|bb z?Obi1)}&3+W@>Y^IBmWbuPx9Lw1w(7+F4qnwn%eni!tXpU+bn_pt-dRwPfug&8scd zQnZV;RBah%Lzigj7{fEP%d||)jIPvrXjf@hXs2Or)Kj|}WBfIkEnTbi#t7d>`-kSk zd?_1aeU7$L%hhhs{MwC}1FX{WwVSlQ+G@gu(k^x=C&YcqZ{khWja^BXcthN<_Fa46FO$B2pP4!jz#{3Y>xU#$aJp3Pp%AL>S7MVt}$h3{n<}BIRP?R~kjW z(j@vS&7z;uBJz~;#B}UzOjXv3%apgo70TOUiSn{os=O*LR$dd!l-I>Y$}8e3>=azB z92D0m?}=-b_r-GMfOuRD5l^ai@w6H)o>e2obJ#)HqehEo)Ch4<%@7B$HhNF(E_65_5B^Q?>(63Pu6eN-!>xj*YrdB5zN4k>fh;K>K_?pMyY;{ ze!ad@U#4GTbT_<4s*!GF89j_lBgIHFG7QbI8ln2Hv<@(a7=w*S{bzhShuORSq1@pT z8MM!ZAI*sa%@%+`^BgZD*J7C42%xWprZW0QXd0u_&QCg{ABJWy2JM4&XAI1VL?(kB zL?N;O<{0`;=xL086SOCz{{lUo(Qk$JV)VzMm}3$8dT1X;zYFRE1vCqQ_Gk24paU5F zPUt{Jr!a#U9djCi?}QRM?W>*1=yyN|GgvLsOpQ^XLrmPzA`>sP*o4YWvpa(FI~34N zi>?nhISWeh65{~bh?oUNnaqY#IHD4aHmQYSD|HKDtHS_GdTpE&geU!GZ=j}^lV0_IL~1Wx_>64Z-LHY zj4Pn!j6!uqWd;hRQ^6<{AC(^{uu`~zCUC^teW98m6r&SR8& zpkzm&+y|{=lwDA=DNr7RHZaQb&_+i29C{wWEKoTLZDy2jp_nBS3bo-@M)?CuGe@At zLl-b=B9!WE5&UTlu$WO#gPzam+n^UP2G!+-jJ_SZgpoT6>){4i#S1}g7wGGt8yNjl z=tf4Td~Ra&&!A)%p#K1+X9*b8UN$oZwLhveU|a^Jb_0yjP_iRXk3hFF`qR*R8AF9q z8GtbcO8ExHRZwa-z!(RmwgU8%(4F7`xU-=TGR9KqLyTdEKFk=Spj2mo=KSIj#<&Ff zD5LwJRF^>g9J-s)pMgHk=$AsDVDyWjPck}{_bCQ*Qz4!$r4w~EmrS?aVE#Ehx`uqSK0#t7w znot@ancNKhm{ER)e!?i!WJr2HuoAUoXqf|nFU=*tV9~p&g{1c;4 z{Zl!CLUsEKqfq&NWfZD6vMhA}kPP{K`SLL(TOHz<)NRZu)v1kD|kXp_0n7>34m#bL4-8q3gpL2;VY zLaF?K<_-##jhGJ-7@8L-i6-YmT?}^Vg@R{TQU~qE&^WKSO_oDF49x|UWRokQUWUeg zg=|M$1E~Cf<_8MZ6LB>lI|3U26|x<1J?PHR96`x6Spm&rXzW$UcEl!d8bjl@LbfB; zgVPzBD=588RzrI;G`1^!Ozwb^9RbY|6tV?@zC!UcG`1^wCYz!842|tdU(gS11qBR^ z?F!Wau?-Ah;PBB8J9! z>Qf}Up(TtQKM>G1A8ybuh43S#zw3qCD`u}G%u8M!@W?dZ=g~iP|MJq0{b-*713gMLPFy$ z_4@?&N(9z}5*oL$7L;KBL|~05p>bPjX0WFsux6CdxJzROfqfS4N9H4;ci>*YsHxC} z42`Q;gGy*@#NLub?Fl`fp|Mf9fKlnWypW;sQ(3~Or$aAi#7gK@46VJe6CCt+7hLUSmr z?IqY*;%6AoG0k_do|e%16YFY*o=dVLp!Eukg_1SUHyN5EV_nWr-R);+o`|)&L{*@a zCqVN)tkosh#}ZhlOK8rB^}9sXq3|QA0|_we=_1m=t+}Lk>1=JB+y zAg~K3us0&1`8Tyi*&dN5_DdwR)>PdLwGp*DBVwRvHw5)tY8Hb}m4(`aq34FyDulQc ziuNkoEb2$4aDe83YOV>z*OyW5f})NIkqj+nXx^`uFf_MSDQzHbhmK{a4^zj1@yHwb zp?wf!Yqg9~E`q{dgmMXVI+y|X1yHnUf}R_7CPQsjEoYQ#p%sjB1+4QJ#agGRi*aLWbIhN_hp!QYhL7LG`R&V?y;z`6E1l z>Wt_CRx%3J#TthCY;`T8Y=Q1&3<^VO0|V_=J;)drL*HYJ%b`?%z_=QU_D>kgp@$fQ z@`bif7*`6Rp{fXDHWbeuVN^m@#+VC50K$OHG@UW(pcck}T{SCXpiZ<9#+U^SWsF9s zjWN!J+8JXGG>kD&S6U2XoCihwCk)hsmc|&((C#1${w>hcK`*!$K(iTR5i}3ygZlxciM~yH$>e+J%O*cTDIdfyfbv272wpS!75zTyick?hzSkwu zoKVyip?(cTeGytX)L_Wox&?&5KN5;M(rs`99rZ#`J?In;sK|pJ!Ki54I^6@P{`Dx} zK$tI~u>k3--$LVn3vQH4M;+>@Kb7*F4l-~r+MJGhB&coanGCf%9d)bsfIsE^G|&@n zKEZ_o!}kPq5B&~xD)2K9qHxJN_tL4UZ%LI;2%xFY-ymDcsG_ag2s@eLSPJLMJd9;?gHF zS{-x}qt!wwe`ScL0g8T6p9c52(CLhZ=R!w+L1=Uz`ba`UAE}#YbKs^j04)wmWdquL zDBTaVcqru+Xn2nF7Dh{eQeJ_!5W0dH$PFY50w)UWC9 zGSpYn=LCdyE%YFx(KGZOL;aurKBM)4e!x(ls2^f99~5H4WG-@xOFp>9T74@FxfG!GQ*kIRq3q@TL_}o!=U|&Mag~Fx;tyer} zFrp5M@)7uaQh16P(F`qNl&7IX8Tt&sGmOFKn8GuXq0bFGqZoX)DLiK}0&UDQno-bx zJ!2VxHt!k7(7M7io)Igc6Bt^1cxE%SckiiS@L8$w)G}f_l*#~To#Clx1eLLYp*4r6 zkrDSnDX&05JMt`LX#d!AF+=MH&oV~52EBx#HHYU?M!W&NjG;A$hw281PoOt2D%I6G zhCXxftY?G+dIzJBZ8k9Y4!Q7bWN5#{vx%Ya#(VB$X#d7TwgB|KeGkP8lwr^<41IUs zb2p<5hu*`8Na$8Z83DbQ5n)iW3s9(@w=qJ8l6`?f?S*U#gavv(qYQ@bWP}y^07Lsy zo(CCvx4`odqYQyQ%m@Rzi=n+O54Ar)pKW>`Wt0Wb#~Au-)3cjV7C|3pL=p4}Mxi>S z_6P9&VBvX+p*;`J(~O{JsNBc?)MW@zuh^9my_gTBhpy5B=>4u~tDuQRmn_fVSy^!b>F z+AF~4_QFH=0I?LhpP{`658VR<+2kFD_8~m)GJ@Lp0fzP?JO>$Z5%fJqp*+6Nh^wF< zFtp#`ImF;i2I2XTq5THWM~t`z`Y}U$51vmLaV_*yhV~&mpD|)N^e{tv7@i{xeU|0< zoKdMQeZkOYS)MN$)eb$%h^L`nF={yU7(<_BdA?@UNa#0=pgRASQKO*88TyRN^Bto` zL%(O}voX&Pj2Z#`k)h9vJU=mN2J~l!K2!4i!l>!cUm5y5$rE7I?$9=dK2!4i#;93B zcvXhxI9`pRxsO+8Xx`y97@F^REey?Nym%%FniF|pJA&pr-cW|-FJ2o%^CPc=p?R4% zmZABYHvzbC?>;E{JFgq==b;`(hpoI`kb<#u5;T>ed8-#?BlO##eHfa5d3}rl8+r>s ze}q9Dd5al+DRdm8Z-&lfbhKgbEQaP;-bRMjHr^Jn2=U}X>3+0XTAO({F*Nt_-USdJ z&3(LgGcd2+290NoQmB_Pra@st!WavM{RqPX?adfdpmYuB*Fa&bl<{z151jyzj=l_v z`ba^1`sL8+;2gN|45qALXzr6j_W*;^U&$Dh{tXPxnNn^9s}KflGUX%v=M^l$S{S3 z)*qWuXM`Awk3$szyU-ripC=itF_d3Fl?Y?H+&VzulMm6}=0E=^wD+MXitBIRf1<0I zZIPmzWuVf{GDvMp&}X0czMFp4?|Y4I6OtmkmAxOm(=C*YD`NCrs!%ZgM_>-qO{8Hq zn~hQPOfgi9785ZZ&K5PIQ7pj7btOI}x(T1mY!r9n^NJ_&8N|z&dmY5*0H5P?=^w>! z3O;95A{3{RtaMj;;XD5W@V)C1_+I4{(o2dd({Wj$JA%kz3Qv#+v*4EXZQx>uj(K8mLt9wiEkRF;~RuNeEY8m-*Fs=?=)89 z`-=1N{lkm#-NNheeZkxCw!l5w{n{?=NqkrH6>UGhmH8>YefcfEr`e7-bwcqSZnvJU z_muAu4AP7AG5RFDOHipd==1dp^h@;R_|DsE{SJMLejnb=cm{7~9KiRCzr|ZBnh}FH zG_s6b;|ybjF&W<#tu+?lo21tmtBnoDR^w6QIpam+ZR0)Tu<@ zWt3&IrQ9;lGT(BMWw~XQWxZvqxmF8#1;}Ak`R&{(mkYCh(BaN$dHf`A!9?Pgq#yn6;d109I`m%;*hIDR)*XhvOZ*U z$bBIXgghDYLda_&?}U61@>$4NA>W1k5^^F`3$=wthbDw3hxQ2dh4u|SBeW#+tk4Oe z(?ZKbt3w+?=Z9VxdTHp@q1T754&4yCE%cGlr$YCJz7+aS=%LWhLca?AJ~R;er_HcM z*qpX*wlv#mwrtx#Taj&~ZLDpEt=d*^Yq6bgyTW$0ZKdsI+j`q(+cw*Swx?{*+g`C9 zw0&&*!uFNzdt1Qvhh4YZ?J@R5yVss+?`_Ys540EBhucf-lkI2QEA8jm7uqkfUv9tF zzQ%sL{Vw~x_MP@8?R)I6*bmqb+mG3g+kdvVhpAzqVNqf6VV!-j>8 z37Zr)Bdk2EI;{!_MVS%te z!;SE;@R;y!;c4Mn;W^<0!-s{B4?jD+I=nf2N%*zlYr;2%Zwr4kd{6kB;fKP%4F56w zM1(aWDk3qWXGDHPVZ_LYvWVFcbrH=GOCm0bxHe*S#9a{&N9>7sG2)$wk0ZW{_&HLG zjEYQ(%!tn9_eMS#xhL}V$oC_UM1B?dQ{*2} zAyM&BsZqV6`b8B*jft8bH7Dx4sPm&PkNQW{Em3zy?TFePwKwWO)MruOMg^iR(NWP! z(cb8u(RtB@(PN`$MV}kp8ofCB%IKBRw?%J`emMHM=+~m(i#`(lUG(oUMoeUkD<&NeoNJw%oLikcosT-7cJ6b&=6uI_ z$a&a#%=v?}&3Q7;5*Hp98 z$K4cnN8G(}yW*aWdo}LuxDVn!i~B0>`?x^dpYcX~SiB?N6`vBH72hX5KYmbrar~(G z@$pmRXT{Hrua9qyzaajS_~r4}$FGUMJ^rrv`{Eyr-yQ#K{EP8##2<+NDE{;KZ{mN7 z|2;uT2uX-ch)ZxMq$l)D$Vn(j7@ROHVNAlLgc%7H3Fju9m#{G5qJ+y6u1&Zx;nsu= z33n&#NZ6I|WWw_auO#eGct7FOgrf=HCH$K3N1~o+PmD=SO!OvZCiYIuOB|S3lsGbR zQetIdL*nAZ%Mw>4-kNw<;{Az_C+kHqFk}AL|3va z#g*pDaGmbz?aFrLy9!*zu2HTru5qr3uIa8S*Lki*u4S(4T(`LHa_w+E<$A&Omg^JO z*RJ*?kz^!=CPgGUlM<7F1=A-6FfibxZD+*{yfCzTM93c2>8tZk64d zx?R-mnr^pryQ|v+-Ja?8X1BxLes=5bICrMIzk7&#gnO)es=M4><8E{>axZf)cdv49 zaBp)z>fY;q%l(P_m^QF$)_jhC!d)-Jb7$#S@P`UdC4uw7bIVvydwGL0y(d%bDRC(&DScA< zr;JLOoKl`ro3bEfS;{|BZco{k@?^@3DQ~14Nckw`^OSE=eoFa0RY?uOduVZ~8L9r% zLcGm4J#|iMW9mhzD^k~`-k17B>T9WoQom09HBCtiOY@{1?fxDuSmZxeO3DU^v&rz(jQCTm;P4zq4clPe@Xu{ zBQnFC(KBN}#>kAR8FMn4GZtrDoN;x=%8XkxHe_tg*qO0AV-Mavd@JLCuUr*+TnerESk-OIXHc5mvwr2F#jH+A38{qF7$b>H3n`R=cD-{1ZH?q78O zAyZ^pGb1vcncXteGJ9m^XBKCkl{qc5JhM9Uyvz$SFUwq!xjJ)Q=AD`MWH7aX-*3_)ZtcI+Xtc$ajXRXRwpS3mX z;jCw}UdcL;^;y=pSwCl;?2+ALOyTUNs`*u|^)=Zgg(XskZ*xII_Z=!n*Q`riS{$hPe&(Rr3m)YU<~fSG2ZN6_!Ym zG5BO!IsV)t=~q-yjysAfYMLrq>*mx}Ei9^RXeqC#sH$&~DfKHTmLZA}aXEQqm-ski zzLFAIx)OPv^TU}O!bL6O+!b-|inypn{*tZ21KD(r--w4jM#%Dwk++TM5?+S&b9Vfc_v{jXeo6gYxO~|K#ihYEhh~z^eq5wtHe4|q zyqN2wm?J1Q>x!xt?aC}`NojRMQ@u=~n7vBOySNF+W|&=4;w!CgMN?~Pt*b3>Z7J(d3=^Xsl!YqY}^vlS{!{z$)v9KnZ)*))TIFp$(}?H>m=EVDDE78-V|B#DV-&j zi4FBlsjR8OFYKsko-((oe16r`zXtG6mHr%>s@0d1%cdFHZ<>sA+NmwOAg8c*d2LIf zye3`J3gnWJ6qa;2hvauUeVxvnPN&~=Qg24i!p`h<1S$*$qE4>Uw=?d-ex1IYw_uq~ zf9m>zPIQKyp=G8Dr2)v(><~Mz%bc41qYOZ1?{IWlKvsnGmyw$tD0!61Lq|w{+Q^SA zjVDfC{uhNV?zC5NXYqjxv9Knd&&YGF~1!%*fBb z^y~6m(>a+H{ebDrE-By&>D^M(QaeN*i{!Cb9!unLs5}mn$KmogLLNuT<0yGNOCCqd z;~04?mB+F2I8GkN%i~0OoFI>rgQm6IoX`kcI+a;~d#?p(?{ zrU3HRJYRX0G&#;Nj>`<>6!fc*eigE>tmtgI6&>9~Ww0B~F3HZV>@pMJ-1>%A$s$(; zgCUdDU!sg${vytD5ofuG%U_gN)sX-PHt!iy#bv1K>>zXUqzQ8Jcznv^{yL93TVGB; z)1Ewj7fZ=0pf1mslPCM_oc!FmGKX_z4(E23ac-B^Hn(ebd^!1S((K}D3?$W^x5&Ka zIW z@`u*TSnA8^k54o;G*(yDcSh(RQr{&)8JVAp>+f5|Q;(d!Z1TQ&4P8vbBafVjpfBPY zF6Ig|hfg`eVq(Qb%`we3w4q~64Gy~IT@(&cv6V}*8@djy4IL)VF3IEY!6(Dt(AhO{ z{yD+E{hDOCn`B{|x|Fh6`Zt?XkLJ#*Y3>q8MwOr2f;Q3;G#4ftoOWMs@cEolHWw|Q z?cJ}SwTqejhzAWxwu5Xr*TR`>?AhkjtVEi-*sQdYzO5bhDPgM)<-|%jl@c~n$IEVD@bjm?iraUCK(>Kp_QlCoB z^3Gg#1S$*$La!?4Co-18&bSK;I>Qc;u>>t(`cpp~bfPcr3@tNNe(Hc+D6=QaNCD^3 zftXv>REH;bc5QRe$LzQ1)a<*ZGdMlz?8v2e*I^Z%oE(J9*?`=^Qz$vF;eJn^%I3+g zj@2HYb}p0Tc}E@5Sq&ZFQ(M*CEZsF7mBn2kU8kx>Hz>oOYW14Gm@m5|N1CsrSCsdh zYPBM1c(c1SqellJYsTz0gNf$k%ej;^EccOgrIrp|J4@-=k#FuO%_yj|l$TLPsa+oK z&LF{hM&~IrSKBe;X`nz29dUEtXWj^#$f;7psWy>qj%SMO>}+$zo$a}}v#A!#oiGc zPb-6WZLL_V%A{n&mpRXqx!}&0f(JWXcAwHxyLP;MnKm!TC1c9x^3eII4_IqQ^>CkS z-ht}jIrm?x=b!A{FIYjiBRF@5GdQz`vvYo060A1(@*3sTIw|3%M7?)M#!G_nqxbCW z+e*6BRA=>+aPQN#SC{d0sT1zqrEjqR;9j1*7Rf`0LArG1@&+1Phsq)>>S_k=-#bHe zJ`cIUMnrvmX8>8E#dOXczjSrraeEuBHgj|T=j5CqLsmqB! zk~QPx^yGdLLUJa2oIxM$kszd5A}(JJ=gh}(`nX^|j@rk!=a_ln6nvadxx3@@^AyzI zmm}jffWLqvE8tiPIN3sZU!GZ_LerD8E4ONW{vm9TA)JETxP_+-nP(PS?rr+~a$y7E z4DxcoFE`kHe!0O0;Y{!h&(AYFKX2puOE@99x90Qn=90gJZ;%^G@Z=1Za0W}*1Vj10 zp`5{?TE-%wfFJZ@ll!Frb?G`ED{vx;izn9uk-HYt$d;3A)jU_PZje=aAP%Z)_t zE5VZ+iJWlz`~~JU&Q2~@FVDjLa)%Q)aJ}YojpTBB$mJsP63?H@w#?-M=W?^i<#O|i z#qa05_&Gg4TfuMUg+t2iQJ-HfRUn+KpDpC))OnWe_j4xlxK;T%S-HuI`#3MWmh|VD z`7{g0#mwV4^Ed@wnEH9q>i3&@;i%<~sLwA~2xzdJ-M$=I0iP9cECrmU0#2P5`g!KF z$6Eq^xn1D%%WVdIWN5N>6c43 zcycCqaqZ`w2!9EuSHiiL3ly|ePF5~TU^=cixhTOkHi2B5`22Fg0^!?-a%JDR1xV-WJ^b~fz1j-l9}*TuJE=g^unYGOE0>vHYy^ zy4jWGqg%(cmerKj)zF7FV{67&H%zFRTUS1@yj4D;JVTts(G*qJjK%-v8X0?GflRUm zzkj*eOo!bqZZn~d0KxISw61FIDWQWGWExaH&TdCZWejv?#@NN#ajBe4 zGMCIT8wIbd!m_4{OR^Z}_+LX=B%{nDgVaic)XReB4k>D2n=GtptZAxlkVP)aX(r>e zz&IRFMRiRh{^M6lQ~qUiWDar!3((aqb7tP!)g26LUJ9PdAZ9j9chE|*XwuIt5xY%` zoA;1Sy53IbEeqsgp`{u-H*`Lyp|y#$xn?0r75$oSas;Oci>sO%de_v?scNceKntnr za8pDzWYd?9{}_=adA=qif7` zerkl|b4q&TaY{LR%f*BAYG|HArG`*GNTlW9kg1YV389oIi7X==oG2VJEvOj*rPdJ% zodhEyw;2gJJMO2GU>xKQ=BrCCJM-Jz+SpN5K{qAb;g;DCp2?&;PN*Qk6Us=3TNWvJ z#`y}K$&3Y0x+X+9)?5$U~ODBht>I({4P~6)ELJsw^(K zWXjS(#UuwMEmaneT%0($WTNDdiB;A&)KSc&6th$rGr45U(m^qkgJPB{V9&<(2uhi*87t7kZat8+Nz>KqBMktqJZ+KhUSE+&+{N0)O>GPoW`j5JY!(2Ng4 z^A!k9{2(;zfzX^1LbDeLO;8~;VT90Z36j&%th;m~)O7gN4Agnz%gdJCN+o?K1E2g> z_O2*zuA1G7Khw}6ofQpu^rWk5AwCMIt87@nr{}d+HIw~2JZ7Ups%op|@O|=3{sjn* z5f|rG;X&+j8Rwk=8q1sTfkW`JJnOtzRac1*_Bj{z9nR`S^f7|;o!g2J@T=-*BhgHt z!>2PA@@mGWG(vU>Bz-yqk%fG6qMqZEi)4s=RD5#1h%;F=KDofm@yX9);3=!dCs&ra z_)8Xu1tDY&`Q(ZeL1fMP^2u{($xxcC=JUILoVSsGnQX+C-BQoN~H_#kDm} zH1Est73MeMk7;0*A}`bI7oj+5xvWG8**Fkyu*nx>=hip0(1TEm&veSEj>%0MuF`?| zQB%W0>8z@2Y*~cpF+O7>p=D7GS_HP2YAKm)xhBoY@#Q0UWot!Cb1PEh1qbQk$M+<9_Qh^*&JFWL;Pt!ikjs^>n2qRcBSscl${5BjS3Og(IqD;}a2jCYq||)--cZhckSzCgYmj z)G!ZOGS83=UoP(p<@)-{9aiky&t`A*3O>Kw;q3IwCO^I`m$JFMag@vZW4XNHlV}4S<+B^+S9PSx+j+U!L-5JM?5f&^1szv-_b8Xw zOosH&{2 z!nS&SWmQwfA~r%k<+;--yF*w3zH;TwPPsCNr8>is(42^1Eu_-IJ+!)1+LzQQfTGuRf$c zgWs|pi+4zR;CD|KXchRK)$~^;uEp=1-i*J}ak`#^H$X1MU*@<4zuEsq{9gYf`cb14 zzhS=4xZAkj*k^og{9ptuh9$u=+R|dV&GL}tam(*khc&_Kv8Guk$Tv3TSyx$aweGU+ z#`_t2@P5Xd*7vO6g;+zng`|b_z#AF9kbWVP<$D+_L+%NADCFsomqHGNd>J|nZ&#Ft zULLwO^i7-3HVtn`Y_&aMd&Bk)-h0Tl_p{gApTqkMfw0K1*s!E9FWz9N4yz4o30oNU zR@g^j--Z1V9ul4uUK&0vydu0Vd~x`7;jf3kAKn&WkBE+li%5z%BVts<*%6fy)p+;d zhKQRZ?!dbT&qcfx@mj=N5x+#VN1TkbE~8qD!MEM3+UM9eqXgis+l8?~J}bdUy0w z(I3Ppc;~>0Hx5pV=@T<3rYL53%($4jF|~N>U`fnnF;~Z2kM|DN$83(d5APoAig`cg za7k>3r7t zp7Rsu5$9LVpW|$C5qNLF9oIdsC*B*#i|Zd(7&kg@eB9)?d2x&5{t>q^Zfo4mxF_TG z#k~>tPTYrhU*OBQZ{mK83&gAOG4XMDTi~>KUwlD)N&L9@viP&(tK#e8o8uS9FO9zp z?+I*(-x7a+{3G#C#y=PTQv93o@5X-+e=Ppz_!9{#-V%sNa3mxqB;!4S9tpV#gA#@( zOh}l5w*<=Zj=;qU*C*VOa8JU+3A+=XN!XY0YQmcd2NFI`_%h+^gzppD@OFTjXiW@H zbm08}PhwhPPGVu=u*5NmlM|;Vo|8Byu?24kEKR&F@ix36aBt$HiO(m#l=x=ify7S| zze@Zr@n^gvU~z@HqFr%#H=ws`fNQvGl53i4maEz|&(-K!;JUzdiEFv*de<7)I@d8fZ0iP zNv%m2B`wGM0IQQWB;Awr5Z(!RKIx@Ex%;TQpWAa{hJKc1#+>^feRRkDk0l4rEVb0T z?E#nVnVq|yywNkm+Uu<|zgXs(W^HrY%5JFK^^EH=>yyiN&Ya}3pKjZ)=egUDS%K4{ByzZy9B!**Ny9OvEEz)73czIg4XOV=ko_sFBqCa)=7Vr^Zxu(fsR zt?L$e?5`IYUs~G30^!C0i@lF~yTv}YhuiMVcH1o(Hv5^jz+jv4u4P9c->_%73*Gj2 zEm=0Z^P9SkTs|dXA zo;z>tT?;%lB}V<$hZjATG{$}3C7aLR3=@oQn-J*ZZnwYawzsXcwhy0y8ug8J+t0G? zw%9kUTf1Syng!?I=CSYFy?dYML0CNSo%;&QlEqhDacS~(D*{TuYn*B6=QV08E-0Jq znqnRO#MBo(cUWG0;)xfNOP_L1vDUY=)O%Vj^(`B=BtK={vSGs(T;8&wr9L@gip^v9 zWFpQQOJG)@(b#{&QWV%^Z7*+cG>QTV)_uES+j(d+?K6r3XIqYcaOimQ*1(UpERVg- zyTAQlU`arC<8W~Q39rTO%(LXQ3*%F`+J@P#a9?S8vUdEokqd10O&f2#WnJ=kYkf1a zGT(CE{IwgdNFH~-F?IE>>Zg;|2NG?8sn!?Eo*eC&A2{ybSLS@ux@F_WEuQt3EgPHb zlgn^pGu>FT0Md z2H8eVoHce)@*DGwyKmgQerr--yDhnIk#)eRQ3H}MvJQNCLO`ehBR^O7p&ZG9m5V{5>t2X3ur&druP*Dq{d zk=&lxo@fLj+0dKYpT#ZbwXWTCMRFi9kYKb&a%Dw0tK3IWsrL5#wjF_9`%gp^1$v#> zVQ=4hVuG^<9Y|oH)xO$NXbbckX6t#PEO4zm(9c?KTYVzR5||he&Y|v$+M-s&f_OS3 zo`uO#pVizZTP)3J2$!MCirn^`B{usa>un30Ry8C^d*R_HgKgfl>b7;BMf=xU7q8oN z@#cSRwDxpqCm1krsns+e?kCH60<~?iPFWG{=`im_?g(^!_DySV+j5`l2J4Gcp9q}q zo;o`D5^K$^jd#xX*aJ)4r&x>{WcySn8mMfNn`M)1OLxn*+<+#D$7;t@&V9#9EfKd* zOIl>f>TfT!-EJGvelP=_Mj$;f$61%}zS|n%9Jbz=eo=XKS<=|)yZ0uy-PGPx6gcg~ z7;DxC1CD?6=AMJMd+gNHWZ6rnO&{yA!;IT4D{aYdtT*1Y*^2^6?MYVZ{5;bwL)`GU zI|tqV=J=13wp+fV2lG|yS3}3ZCVVqQF*byRUtUQS=@^$^oA{!ucz@ zsCO+d1>ShM{SD(C%dc%KjY7-d6Vc-Xj~Rn3btx2^{Qt?stAKsqH9SztXIEKiafypQ4O>MUr%dS|uXkk)b(Z`nO?tNgxs^q{8 zzbP`8*j!1?14!}`y{J9Fwqte?cY1cPM=olnQJMX{`lUn+;FvJ+w3{=K@1$T+HZ14 zoLGg12mACMd}g0>ZmZZ*m+bg`JtecAl6jhvc^b)hZJ$}%X9e~I#sp>?JuU66?bjNx z>A*lQxBZwc=4fD#+i|EZuWhB%@y&3XDBrmJhUGV)>ddB^=eED_B*vl-+#@XLJWHpelMWmU1l)Gxx%m%NjdTTW z!j&m6U=+%?RGwwr+*Ef%?cMI#H_zEn=NaCv7@t^n>@zO7^X^Nwwq@ELyl`jp-JbTH z?e{p*^Uwpb_SP#dy(PJQO#2?=iq%)Hx!Dz15g6#Kv@Kfnj~gyX4wMF-H&$G9-Gvvp zB9;a6Z1%~vh`@#Jw%GkK0kQwu# z`_Tg!>>pbEXi{L8`&##=%Qq}r?`iLU;?^;?Wr1gGfsp+p_B*~0eDjiXdSIXX-oUlC zANKdOIF62;HqN~sXAfDP3PiY*-?-g)Z$a_%eUr*9fwOJ%t)*KhKUVK?d|y^yGjX)* zTx;f`0mnVNEg!!X7-6HFPDzRgU>w`Bzio&+4&xb~f-1_4{NkrpLZ7wA6_tHW5xF7WXJav&F9UW=IZmY-yt3cX_(mM!+tK#8H4xf<#CXpV2*u!j z;_>~z4en`ePdH(3PiV8_F^C9OX!+XzkFoE7i{e`Qp50}4byiUsS0iM=mKZe}JC>kC z#2S0=7&UgWqp=|t>>}6@QS7LQy`VHJQL&C13)a{*Sc0Be&tks+GqYIo-uu4a@8-(R zDbMN8dHU(0E$C=%L4E$mg07#no)+|KV|yPQwuCK{#;oz-)1A0fSB=57*WEH?c=Xy3 zA2v<>%It1$ncVGEMr!h@KSl4%Vl*?K&y=@~NAOklPC;r}k^UdA0P&O&yY&vS)sXpP+@CBM*9h|#>#w#0FUyTp6D&ysy?=j8hi z|3770cOve8?lYj@XrJ9eMPeEO~5H{i6_jm$GQZ#F;kqYq!k zhk{Vqwg;E*?zrIIn|ykY7#G~ni!CeGcig6fBYX~Sirch@=pnk3ecbIYMHazNV(vlV zi{h!&l+!+Ith*;{8MfNnrkGwnv9rk{vsh}0Hml_9DdLbG5N-2V6Vb#HAIP}xN7)0$ zinvp=~Wva<9KzeVrMmFwRix3Y?TfI4L`1Kg<*>ywQILnj?BG4ul8jXMY`F}@(4Ry#l2XB=-yS7 z-U?{*S!gP_ihr|@?sS{{CEAqV=rVgMW@zVVh8-k`TAo-dcZa`PTVySi#mZu}3Vt}| zRg{6~Spm_O0mMUu$UOEnU7d5~UVKCV8_|U|lpBvT*I#|9?H%B3Fs|;3>UL{hh|Rkr zc2ya)v_Uqobj74@U~goeXk~pw3vcDbe4A8v^FpV)#huk+q1cAvMO!m44C6+P42jza z>MS3cQ%r1QSZP2xv0Aj{8akov-u=c59OtuRXs@+hygprfHdaIqz-?$_Pu26i&@`(n zvxc~*)LY;dzz>2Ew%JRGwyL7^o`I=kU&%pqt^W-w(T+RC&RUskSXcu-mpA3B%=azK z8;p`S4+6Cx!pH63&)&;s+wo-wQZhU@T5U&G>`gvFKOS4oD>imnG~dT0B_BRXjGOw% zelps^M9ei?B!7R^@5u~r_FA?+iM)FKnJ43-4cAX=&><-x5ok_-MHiU_KGkv{aX?V- zfdhN{d~f-6=!V4UK7r%REn-jfxaeimNKQg_`vaVj)OBFH0_<-L<&eS;*?bzunQfAq zCfe>?#06nvGsK6AEsPDu&&Hy3I+S%)bNb2B73>fsPm^hKuj~ zvHo(l5`FgZ*=PlEG48OI4uAVlQF{JRly1?&04bFhpR3hOuaFU7#e}WnjrclqfLcxA zH?1syt>g9CIx{*h>}YQR|tFxeP|H@E!$*6hxj$!u=x4D|9Y z3$WE_99XmFg~0nh2U&h`TY`^mfoQOq--@;!heNTC)b=4ZH5(h#0WsR0PsMAu{`dGIP?!AV6B@VzLuEK-O0aj(MIb-hO?rl7Y z*{&M!$i-?~pEbNzDbm{=0s{ApyKK2=V15})$}llM8=Hi$78xu*z8cvM4c^mxIZ!8? zqS(|~Y?}Rb6Ciln7r+<`Y0H&WY#$Llj%r4A_IC$M8GrLn*6d4WzOlU3)(pqi_B%_? z@W6Uaw6TUxRZ&&j^^m~hzFs!4{pGxE!DH4zXi-J%9$?SGUSZutmT8L?e^VsYc20c8 z%7IUT2nLin4&5d3tlGzP76c^g0Hy<1ZhJ5r{vd3f^>`Jvp4r{-S^>E>3jOo79!uDt zy4-u*1#|uJZ5#P|`dMunp^H_r;Vey;x#0W_7>`(>d9p2oCI4-D3Z)8!`pU}^~kWz0e7K(ja}4`24f#=sTJ6^ zKJNN)xa$+b?^Rw2LqI};4q%0uNLy91P>bCj$)VWh4Y&k$@;MKxm3J;AbBZH{@s zjm$$vLIc@7vpI7^_EON=Tf*e2r^MC)XDzm$gL}1T=*j+&`zR1v+3Z0iGfN{D$C||V}gC0#WjBRpvbPUV1){+zx}=0-=f)OKh%S5W!ubrPzJ+rTC;(mG>zg}R?fq=Lk14;uz6flh9DzB z3{!0Lu=5mF0s9yIR_o$QQS*t}Hg-YeSGdLI$F=~Y7LrZ4ewNENh^<=s*xGdK*2cz! z-(;}elX2+mY`(0P=8f&n(gA8|i@O^Hj3E1PeUj|S7AEo>=YVwd_fHvHhy|$a?W&Be z(v~;HCBxd*#gJ!Py7dYfI>d(s+t}^X_WiUMQFq7s^?MF^-okzwsIVni15Y{4%Vv14 zua=;vKY|)6{3A3D71&6{VAE(gcHP%*16lBkh!s#CD327AjlC2X zu_ZPzT}%LWlmr`S^&x|C&&7iyLmTHn#eg{SvUvyTTTGc`!Tz!-c!d~L(f4#+AHTjv zEg`FBaU6&Z0b2vJP{n%F=7}}7Da(~R!1V1Eh!^;H><0s`Kb6egW4l zhj#YwiSHi2{@O$PzJKAueIHwZvpFxIHqZM5nn7*kXoRzwVI3)w1jM_w)!p$J$V{yv zZ&GX?*(z9XR*)T*ZEc9y3}Z?7*YKBG+Vqz#)3AaqF|Dv2Ju9L+CqVEhtZ2p=#g-1T z!{+finv%E4HpuE)LJsc1{4;F4j>78LRMk$1pa4cLcsx~BgFsoLNay@*C3~uG3}e(D zCiY4_ZnyDotq}8DfjKY9I1^FodF-j~mqlKIRj`GHu=_HW2=@|hdyZQlSCmbyEVHS$ zT+yuy?hhA_{qiPcOg3xRjlFk|^zJuuWbdw?w#?&9P;1F*BOCBSA8ffwvSGcVev2uo zj|IEQt1E1xvy8=Ey*2?;eok(2rk)KVJM27#&tTpG97JtF!!&l;G|h%okVc9Mr3!Fh z!3>kS(8d}$*4QDmwh^oR1fz{oY}!#{tpXH}*K|X~y`~+9<&0CLEE{VQ01hZZ`(tbV zhqcSFemz6ISN{w__U28AiJM0ZAe3QaV?kQ0fikz-GOlJ^WbI_z7||V@^_nf?B2sXO zpJL`gDe$@$o8hlale)tOUb6mpADb^2Y=}8Qklf8ccbkXUf;x2yg7C5{XdlpzwzWgO zrR3<4(Mv{n(Vp&1#2y})wyA#X9`z|S zWH|0z1M>m_z<+4CwK*$%f}~?hWp>HNAigMMQ{F0uztv(Ub&3s)bOYCc!-}b_39gP+AbS35IZskbu{v+z>R}5M>k+@iL+j0mtc0M1(D! zD;QSNd%=bngHRR^Ng0G)ctXm6m;VfF1!1Lt=W`Hg-LT#$d`nM%8a5e)J$OUP4Ph@0 zi3XSz;8k=y`GzR%cp4q=gc}YBLOFxspb>Aa84e2uM2#&k7*b6X8Xg6%2!^A04%A>c zX2b(dhLdhWMJ@XC_X5Reu7sDS%oK3Bvi4lm5A;YBVl*QhGa@M)uAAs>C&Us3*jq-# zQ#Ran6H@5mCc{H|64db6hM>$8S3OOjS1K0@^!{J0fEUgUZ%v4!ZEy%eIv#a0Bj&YH zuuuf;BLbpq3P%k_(TE_(6leLkU@T-bdJ9I6d4~y2*&z?(Z8N?vS6Gf7^j$w(* z!YRQx&kc{FA!;Xrh8m;X@Q@O{2Xt02VnreZBbFmTFk(3x;cXxTLbn^S9DxX$Dj3(g zAr>hfFf(EmE(k`f!bQP|Rk$P=cjh-97L1smCI(}&RcIy{j~k7rv{#D}5LXe583y69 z!H9Oz5W>1%6^v*XzySvU0p}_`ZEU<`5`Gqpe;5$*)Og)W50$kNjQ0$%C^7z-k77V0 zZ0j|__`(Ql65~rZydY(KD;P1`H}T?&O^DLo63WtEc|oAvwgNrtvQ7}%8BBO=wuAPv z%PqVUWD*d9w<8{^G2!hQQxWYomrnHFsHvz?xMMICHw*U!lWfBa0rX%-7r|7)4R3u5 z_XX1r^t34AnBs8=Qw;%OS?M*12ZG7BifItN)M6S!@1~fB8t{^eX}Dk-DVRnGrcp-I zI00|5m_kb6WeC$^Gv0x~Qw^qQBOYeJTMKxG!4xN$HsZ}6lWCI?ul(S_AJeu%rewi% zSTLm+@%E1?T`(PWGyN`@GVr92$#jk$>M@-!DhxB29#|=W_#(mdqM6%ZJU~<3ZAdT#61-#Lw%zFVJ02w|?skfv2yr`&$3e`9 zVT};jh;WSn({5Ld2yczoI^1p=5ThC4aoz5i5ZoE>Xt+HzAhI)pI3or$LPEPeH{+oS zyj$V+x4Tdzzu9O*9A1Q_#)}f>!h+c&(A-r(q-JwBqq&!0?kSjq1aofzky!B*lX-w( z9%MF;5X`}Xd8E-imfl-3k24E>1(*<-CmHB@!u5iAiko?cV4f}*{7vSWZWPnsPY^~4 z^q>Mx3XabNMDrDdAjJI@C{lk(L70d)Cw$-F6_&_{a^!Yr6~ z7=-=;jEoSW*Nhmx{RHzN!Th&iJ}l6y0GqV8k=Gy;xnNG#-ZPU0bBX~EF`Cl^p*OwM zXim=$d?5TLm`{Iaek+)B1oJzS8M?^#g4t;>|09?`Am2^E*+-b>(Sq=+_TJ$}!D1E! z9NV>m&{GiN1Pg$SFj%Sx78GkBSn_=%Ox7aT&lV^?{uDu&E>OVwNrEs-5JI)!_%j5A zs<%`&SZWKFsuoKf!BS7K)D_Gt$(XAUrnRcj9OhiRg+B}?LtTUr9&DIz$TVad^BHRy zTNwu+2=I0s_M67nrm7}CQ!CRjg!N4`9dRq}Hrj0}46-)6ZE;J+6ZW^^E*u2!d9RYV=Tk=^dTN+t{;cEk`WZEibL2^*d`91k#;gon&2aO|o9g zC*`Y!K)AE>?Sk3Wh5QBbm&so*e~bLx^N&FY+r#-U=2!BoqE+-3e-PV?gT=)NM|)p< zWi!}{*s9s;*}5Un>|z9%J!-pJAYTEoz|RFb6&O-rW`X4h7JCLpSZ@o81%D{$Td-Nd zkp;sG?ksq;;N60`?w;=c?)}|oxNmV!hOyO?LPZMIDHK#_cA?}#j>08iHq{A+QjvvM z7v53$0s^!K75Syek|JA*94vCa$YYp8)kWylJ}`gU>~YNFw#REplDwrdFmmb$gQiW= zCFzdz7tEOo7j0H_Owl<-R~Jn#daLLM1Uqej0H?$3bL|`LX$WDOi=d@_i_L_o(b?i9 z5O%Z|42Z@R4=cU|K}M4hT=Zt~$Hh5}g*?Bi3?reQUrqTc_N#3$3cB{y{jXSwd?m`3 zXjo!ki8Uoom2i00L|D(^o>88sJhNe(Q_rir*JQ6{Ua4L;yxx5+eO>?Sv0v}{`uf*5 zWvlEdSCU&Hz-9;nYo3!|cvtZr=e^4NxHrV=K0o^m^I7S0vt+)K4NHcVTwC%;$%my1 zl=`Yvg;M=XZ7r2m>SgHyrOTIYR(eC}^JU7FIa;=8*#HETJoinhZ$^CU`R#~r4}I$_ z*Sg%2a__%u|J}y&CCVSEV5rcw!omuUioGkwR@_!`U&Y@mI=(OeegE$xzCTwCO?E#T|r`-88aZ(rZ3zKeaMeAoLP_I+C0Si3}Rui8J>u3x)L?Gd#jYj3H2v-aaU z#p~3m)22>Posc>w>wKtNtZx0f?dtZaJE88F-H?TGM zxs}*`Kl-7HH_% zuzJIv8xC$bw&9Y7dm7$q=m;np@J&EKK=*(Z0Y@7ZYE-pRk494)#W&j1D6P?xMl7&E zV5z{mft>@#1TGHT9+=*^QsbW*cWFGj@sh>|8eeX#HmT60X_HP(#y45nWOtK&O|Cb2 z*aT9ergGCNP5U*S(sX{)^-YtSDovj^GdA;VR<&8vW|NxDYqqZ0p=M{B-EVGgUZwfO z=EqxjxA>(+($B?z4)}S<&#QmF)v|C)|CS3|o@gbss?@4it68lMwmQ`6c57SfA6pM^ zy`lA!Hr{P&wrSU9e4CgykK24`TcmB%wqx4PZo8!I*0xvLK5P4-U5R!z+BI!Au-)W# ztJ>{s_o012`(N5mZ@<5Ndi#g%9UTgG_`X9xhpruVcevjnr-QR&<&F(Iw(mHsTGi=6XG7--o%?s5+c~{+ZWp>1i~LC>{4_w_v0^I=a%uOhw5_xh>Vv|bUtHup;Hb){E!?}*;# zf(i!B?c>+y;4dA2iTve$-}-&S`hC@}Wxvt=UibgHe~13pe@z%re!#B-mJE13FlgY3 zL0t#^HYj1x)xiY^w;X(E@RK2aLq-nSH{{{aGDAlUy*;efu-IW+hMgFeG3@lPXTz<- zn-1?de9Z8v!oaI4_2fJ#NRihvRri(U58(T|!2MtPEKd5+9Njk~zNc_)6njj&D7_>-g0ZOcRPss4$`F zgzyPb6LwF?oRIUI;kPos`To}7w+X-P`0e6vcYb?2vEW3{i9byAo7j2c^og+(6DMAn zcz=>GspO<)lO|4DJn6(FHrY7YYjVZOl_vi%x$)!?Ao=z<^wer+K zQHDW2n|^Eh!|9G0MQ3!Iv0%pW z8TV#5W=b=w&ulRBr*@oF3v+c9Lo85f&^4ZttRGw3H&iXkU z=lnDG+qtdhcApzOclX@Aa}Uh3%qu!CU|#QeQ|G13yEnhk{Ce}7&i`fpi21YTubqEv z{`&=W7lbZ|TCj1!%Y_XV4qZ59;i83?7QR_jdy)U5Hj5@LnzCr|qNqh{7hPO*cTvuw zcZ<{rOGKdv&xpzqwIci@T12#u=ov9NVn)QOh=hoJ5h)SJBQ8f|E&gV4nV-{~- ze0lNJ#ZMPMU#vzph-@9%D>5kZm&lQkdm?W}K8So7Ws3SLs%%u1s5()BQN5!EMoo`e z6165OK5A#wk*L#4epwQ=ge^U~tnjj4%S$Y8vizSFQ&)tqShr%|isTh1S6od) zL$nwzMVE@M8C^fRWpvl*;OGg_QPHcS|6b|2a?r|{l}BSr#8i%{AJZhJbxg;Y{xOqd z7RE%!tc%$ib2#QyO!g{iRl8O5R~=gQDz;2)li0qoi(}8NF1>pA>fNjVSbbx)y2f|S zpfxkrY*~{TS0!$I+|sx`aS!4gYpbsfUwdY)x~}uO-1Tnj3$CBNe);;u^@r9UTmQ%U zhwJ~@;JKmJhUps?Zn(Q4XJgTg6*o5B*kWVvjbk>3Zk)4m^~OUR&u;vCllLayO>H&} z*)(U<#!V+T-QM(Wv&ZHtn*%m?+B{_Ql+BAbZ`yol^M%d#How{YPrO%rh4}jMt>Syc zkBFZYzdSxZeoOq;_-*kC@!R7Q<9EdGjNcW%JAP06-uQj-`{NJ9AKX%P%YrS}wpQF) zYirEbbK8V%t+z#QQ?~JhDhVSK;u6vm?rkr$z4P|)?XR~x6H6slO!Q6sDY19rki@Zx zVTp?pS0!#qJePPo@$C*_hiymE9mRK)*zxrapB=4sOx!VfN5;-tJFo0?>@w{tw5!A} z-(3N_TJGwxYsjt%yTW&^*|lTWp^G}p0<0}Gq4?y0(`?Vgc)Chgg_eUJ7J+5hZ7u>+?MmO41-;Fg1bCY4C~F3CSBJ}Euv&qIw5B^
oygCa*}|nS3nyV)C8jHz~%HVkwnVYNrIG^iCO>GB;&M%Au5NDJ<2J>Xlk6 zH6XQP>gd!Nsf$vhQ#YjUPyIdhTI!QDW14r`4{0sadZkTHo13;QZC%=~w99D^(%z+; z(|yvbq&H9RmL8lQm3|=o-VygB<&M-p()q}kBV(_t8w?5R22%oix=eJxE*nzSP?I{a zSZVP(gt(&i)yVOtxy*{>&azW#I&(9ymJsNfSc+pxn#>yHPSG+>F}Eb>GxR*Q(fh^C zWk`#(0^umYF3HfAgvu!9!CJEO5WY$u03sYE=H`amzY}-zT4uhNO|jo%PIJt{mCIIn zt%?qvxX9-gcbcUfSa|)?LcUG_c7AM!J$?UmFL|G@JwqHSJ`~ZJ-}5@d%0VL8QuP%j zCrf)DR7sx0huNj~%VBP%nHHcd^-BYI6nZ6yiVwk%3X0eFKSA-D6AIP*i%?CJEIk)q zmi%`UYH4kH*h9y}t}+{tJ5d966*5DbHH<9F{*C;r$foBm(QC^H?_tY`Dm&^v`PS#pwwJ@YK3@OcVq>{}1a*t@qN~Q-ikLl=)q$6bgM@;H;E$^(n-#LjuF8szu$=x}`S zu~WiDeIH>8vshQhIy;OCU`@i-Yg=Hq2kpam=bJvM}D2(r~aS?OR7qeUYu^8SUg%Vhn9v zsBuiJ@=Tfi#*?Wab{mqOBXJ!Y2y=J-x}6Mjhn#_8PO)jSdpO>FQRI`ThZJTfa6Dgb zjvZNQ8ED167Ab>@@{W9YFiLB)Ny!RLsEM)~^Fu9UUMcfW6!*wceq&2kC{hmPNH2Iz zK9-^1C1hqnCoIKan6yB=NXUu4-_f9WW#COMT|;~pWhqdnhWffVvh9S3$OzN>rYx!a zyWDKODD_zb@txLb4|EzJNPF6JE{yXm$_uiObgGds(s7o=3j0;GN*`d2tu;x02q5vP$nu zv9hpyg3*)oE|gc(?YS0U8<(SzMJab!H<@)JVYT1ni(jnCp4tN3VrEifkn9H(n>=c5+`F*sNZ9sk16Kx zca{ub*Z35ED1aTppKGuyV1?1e8-y(^%CCfS1?f2}&wjzg0O>yWlck3PbJtPRY6BQj zc)?&ag~6pGTzBbkXKm!?q_H%qv(zK@>*L7|wYvr8V10St5#(PX;-2<+uEb%tttM5} zKdkMS`;CS3PZcG{=T%eK1DTc5j$bU#_>a(cQ<3sGiW7M9|17}2%~V)-nRK<@nZN&+ zPV(-~4|b@*ZorU8*HWYW3*{NsF_w<=Wd8bpLY~JKi)wuoQPi)SVw@)xb~1!L64iPR z|AYFsNeN(`q1g14)m!QbdsU0-C-Vp^{;eAD&^Z|uGz&*{@7pqq3jwm=^4nh@JfWeY3|hpOJf2hnhoB*+LPPlv)b)l}^!@-RFnc z4q1VL%67TwC+G^OJrGsV${@1idcwGNf>z{C&_Tb_JDin?{`%4R3Cx|F*D+kYuYp40 z6YvoGVZZAP^@c$w&c>&MGf!c&6gKW0Xa&sVES6v4`PsNU{}$Lfm;>M}Fbn=am?2D# zc7(iHG8BU>9n=aA&uu@sUv9U&cG*_k^*D&EfW!ZH5nnWG>i3wnZ@3>49O7Tdyr7o0 zN5Jad9Xr>tIK=Q5j94;1CG!h)Kkn2?GApE9hsL;t{E(1gAyn@}YrH5BP7$zY;pbJ= z>W=Mj^a$lL(~;}qp<*4+So@3`(l5+ArnC7(^noL%Jx^HmYbXWiiy-#T<-sD_&w|O2 zR#D2lE~bKNah(xNO;=bt#f4DjSyVXdtd4C+1b|-Q9@6g7)`Zok%vz#cCG>49<}ujH z0$E^}RPMn&%Io(+u-_wdG^8o+dLan z`A}i&-iYpME$tT?*EfH?zz`YMsv$Qp^;85I`gZo=O_77k*bx?@<-8GtT(dx{fU+es zGvA0wpC$oNV8?=-Z2Zr}qUkDVz()ZJCc-g~>LBNhn57-jp`zxdV$s}GV!N;#vST!M zJCaA^xBBLz|B8jWmko6MP#FzuJc=;pR@zXPv7xpC8}II3p4u1yW&SPMJq8EkzNn8b zCU;S3N+EpvL|FrTyM`{Xf1PUS&DvbhX($Kk<_Vdw=u>bYHy5eE=15zmej_`Ykq*8V z84a&uURD^eJCDL5hIx_E@MRYnL}l-aQMqq4fEr(c-u#_+oog4jp39z#>QR{YoE6cW zext{HC{|i-%@4Ey0i`hvsJh&BnSWMO%cOf9jg`(gg)?i)yV(C-ithL)${5Vs5HekT z7RpM$FLj2Pr{!7~!*CQ>GDaN>)0kMA9L-c&deiJfIc)?Q*MP~>@)gO!{N8uK<@--G zUmY*%g~+z-sDjQsSDxQO_jghE(`XSiJEV86xgbse9rnz)h|Qs4r<%E^wKl>+(131< zAxO#UY8aGCS*>O1+EQ6c{tJJ39p}=802&tZQ1yV>2+`(JDr*vGmu{n67RsffT$Wx= z?+<$~N_TWvFHt5NW$vI%HkDZ>t0k~rVIf%n$25io;MTPIkKm{rYo9?~q`t#k|x7(#V0Dl|c(*XYVr>@eLeWHxO)CVhZAMJ)sBFu)5%VYp09ZyHiyF?oC(TNp@6T z1kM1rOF>l4sm=>y#om7^kdjv*>b=_p6j;bde<}bTK&`Eq9)fgX{b1X80Y=8Yf#eOL z9kJ`Odco0e8y16(nZ6Wk!XQc@1}ksCIHOmXw_{1DD4k+nx!bg^U4-T64H(_%b0TGh zqK6&P!yBR8#yfTurN7AJ{s#DZ%F}Bt5Pycj4h)2(7wUepdmnF=vc_9u$B$e(Lic%e zR1=%A_EaBzsj&$5A%t3378rWf(WZs_6`a!xOqC6h^=8(~RpfRc2;| zEdzRZrFz*}H8v6snKE4yDpr(zBkQ>Mx}1F)o{IZ`n-3~%KIoBsBA-vDk@!3aqMcjV zd|cqF@;P}{nm7GZ_?BEFvdr^ zJFs=4MS68CDe0JID~^=i8fD<9yH9pn@~*>X8o;5M(p}9Bh;OGm(JEc|!}8IL3~gN_ zdcYB|Pv&#!0tDacXoHVOx!1v`hOtl)hJMc-{xwAS+=Ys`?eXopq-zFJs=_uXY)c4| z)w60vdvmrWfN#N7M1L9uT;jDY@SHsWSK>@7mw9b_TeyqCHCqFBOXE0J;RamyA8$ao zO*VA3e1q5rodzhuo4OK-^d_bTw3#3G(|Y4D&~?x5>B2(SwZX&rC|4C^g4AV@tboeJ zDgOs{be6&nq%2%Yd6=+&Byw(RIk***6s;ch02S}JD!xQE|L4OH?0Bk;J4n>;AeIvI zu{Pzfwts;9X`Xp1|e(4CKs(>3#48KzN2hv}Nxnhy6ATr>Opam}z9_HenebE$=V z>u}9{16#=eaI(kK2>{K-E8I2O(EfGT@Hur|3;8M&y_nCIM1FSLH00kR7mUNxOCM)h zts%l~=$>5Zvftww?3BFE`Yhs)8_zvq2T$|lafj^%(WXYy^?r@F4X{X;wO9%K9{e}R zos->z2dDHvu}YVb{kvtz;1IYi^7HH;nVq*Lha4E1;GJR7dL0KNMf55bf7TO0n8sQ; zb|%Qub!=oeSHC}>na}T|KILgm9U|OUOl(21=Lq8fPZj3GYRWRwlhnDUB!~Yf5wtS> zb{)b2fulrFqI43Fnj!OG?x&?ZBY~^7|9KK0r6pm5Q6X5(l7s_t9FzoPk}szgTMIBO zP(Ffr=l}}4mI!Q=*GTU`N^>=%F^~heA0=Io`7ZeHNf+>2Bd0$?U>>xsQ6&ouA$QY@ z)`FrM9CHv>kc_T1idg(TX%4sVx1Y#v=t9k5pab@u@>vc}?r#Lt2mh{;Zp( z^*H2@71-~uYd!Xbe$slln!4&wLk1(KPnL_y7z*V_-eh6#y7Eps4(eKiP}a*elgNbl zQOi$OTYjMg(F$QgH8sc;`EP*1&H}>ts7SdCDSF;az4#w91<(*q>Y!Qn7ut_J&^u%l z0#DZ9}scLY;c?+SP&GZc9rE6-Dh7fe-<oxU6#f267bJW7n&7LN!vJgh6>y{6jH2>b7%unB)Ilw+PjJuh-p; zx;tbIWBAY#C9-aG2kplaC2C8=;;}<8igSHM5JDSt5}9b-twbW<4z%+`3mjVEv(2A< z>C$SGb=DF%5^g?~#f>7ff%!P=;seLFawWN#d}uG%=t5OZ1uDH=-^o=Vg;;G(BY38( zD`__+op7CnJN-VU;v@k67n(pkyZt;2!D5=RqK zZeAl2Uyd71Yr)0vpUhCsXJ{<{bAMf7_?C30&voqKQW^f)4+0J8FaPS4Q333izc#mO zVX$$r5KzYI-S|wGD7~$kw*nsp1A4*tp}JmC`bJ157ugAVIS@fdE`Pax|Jz)FEypm@ zF!?$~bhfi=NgzD%U<>6zV8Ago?{rdVEia(Y&HFFr<{iz=`;T+;PM;f+2c+uqfG=eR z8*>ez2AmNq(K|)F>1PQkU{%KHjCW$Lg&J#nN@z?3YE3_fD z1+g?)IuV5WC;U-=V-V_}@c&q!BoXjv21)zM@c}}YZ1yAM5Fr7sWyupnfQWbw>QL9_ zO7F^i$|qq2^%dPuJmm z%tnG4Kb!%-XTFL*vmdq|nYpiL2hZ--j*I&MEx6BPN#N@XDAyIJP1V)PL|IGH zYC!^=^A&YI#H7r_0^a>PIygEmd~Nt1=`K|CS1lC~BI>(3GW=Y>Tj30XuNl&VpjnCL zURtUb^Rx0r3T`vloGe_qVp+`m68t#IvE$ZNEAnhlFmI)07NIm*JNAl{qxNAp>?u}B zjo-3r@4+v$>~Hq$Sh04Cj~@VJS@$PwNr7Yl48Axb#Tr{ z1QlMEA}Z+y15kLA+VtGc@Qm=x@N2LY_^6cjsgEYEvBe5s4AF{|6JHN={{Chf4?sGD zSC`{>UrO17UzZi`$)>=TAe>LJ^C}2kSH)7zxzpa1O*sb_y#_KuNNsQyMbz~O1&Wbo0 zeXV>vKW|6KE*?IJpSM8BPGQl;u<$K>GD=N;0g#$f&?8$p8y~(goE5}%^^A0lh?9af zI?G330MFQnI5>1;vfr=&S<#(z94CqzY;CJhInKjt*ZHzCAoK zJT_I4PKBz|6}u~ln?<@9ia>5lElB1o3C_h(wsMJ8IuYt@LZ2wz#7anoA4kI{La|Me zwuz6&7eYpiUuu=^g*uHWsv1+BFsQ;Tp9)>gTO$8nC~JxQ6QMnHNQVf}CSFJfRA?iQ zq;x=uWN@Y?W||e)6MlkqB06vkhK!g$PB7Hhb~t~vG!0qWcm2SG(Ixmm)MEXuXfMUbt0f=MM?U!u_C;O;FQ=A3@drdX4PJl}4Ha3=3H>s(`_z!HknT1;s52m@f zvV|w|ci5+{C@p_6cP8uaz$kO*OublPiQ=i)y(vT%(#5a< zImh=W9gFaG9z}lph@hn7o~}?-G=(c!ZT|y*Q(KSGVa=_dt+GftD|^pL3{38&CJ?mq?}yMGcHAF@hY|2=uTj zybb_v=Y_T27N*~@dq4lFXws_xEh?NsI7Uc^(s<93DI29TXjc^prW- z2YbJjW;WZpaM8TE3w@+F!GJ2g-2_aqXJ*2%<^A%NLc&Yyxiw})ariu}>5XCLrd0N6 zrgd3(v`fN$){++xReH<2u!?q_!qg7>(+g32oOQ-Rv-DOEmCD>Wyp}_(($kednn0Ov z*Ya>|7FClAvr?}3#+AXT9U9)pt`6f^XEtT@b1)goF(hZg=W5lK62PH>)OR}OJk$+@rrtG zWnlQd2^&+7X|gyv39KS37JgnzS0U)KL-h5QGjr}{ly*d0rMRd3ym>R9hMkbkrs4aH zri1lhnJdEAgrCsH5{!EFoYy+A3+9@Z4*Wtr^_sb%F(hbU~#J6IZ6qmowE zH1i**A8`nd8Zq3=f2WUhwhMz@-q=j{gcNtvieir}h=gO-PHHME8qNw!aj-w+o!Kiy z+k0U4MIb@GjaNbx1z!uZ@_uF>WtGx-VLs9<#oe*&xoqY~b=bfoaV#JE)yz2(7`L(- ze7~6|u<>>_(|QhpN2wJUJp8rMr0&E}J_}u1*Ya6h)4y1EGao`QMq6WdnE7b>ISDB4 z3;7qjA+l3yOi9^&nuxd|thuQAlWYI5R$3B!0o^tE*)J#B^kpbZV!mW$g+M^rp&68- ziGZM#jp?LSIEDBkNi2t;{0U0*Wx84k23hGS|I(kzr|adj{bO39JcTk)B4t-A(aK`I z9Tudw^F9dmSRCrHAidtZIBKUm>6xdA)YeY41(I?&C~uYygI%;hHoN$sF8arC>0`G@ zj)lQMkhNTqvg6oc$8PlPWe^N_;e0N=qsCrRV=rBeWyfiq)EgqH)Mwi0iGI@ zy9(V}sz`6yq8C^KwTM{0nKmw+8v7auTq7N93>j}3`EQtevCDfz=^%yMQubMg z>T!FXA^;)62UNgS!e7mhYtZQdL-|ly$d-f?TF+7IILd-oe9zqisM8IVC)nm#*PWW= zOZ8-SG5Nq6gs7_I3J7B1b6K5};xrM;6E_029twgbv>I8VYH@WAl=Y!2Vx!ishYZXc zhmUn6t8%PDR_LWZr&`B*<1AKXM#!|tLc)3QBdAme^IXud?ZF~C{=mV!?T;`ofSLXY z=1YHqnN2Hu+dmIxx(jBuzY8X@t8@-}$@)1!N3@7yu9$PW#4MjbZITDny=TO)J_yAo z34GdJ$_<>ehPtwm_c`B7Q}PA zq4jx)LC6Y_QugCITH+DdT+`&}`wC_LR387=LcF%~1k`W}>kq(v)Jh`kYRIbQSzesp zpT()XkDXJ)K(zC*J+SKp)}Wm@OtT!N&m8p{>|?7rD^ViMcC<3zIvT@_xR^7hofs># zNLXr7(aO*_)OAKsL7X5;gOwXwhk%H+oF_DxbwUt4q_^lkOx&#~ie@%=#Ua`&SZ+-W zI91&%z zTo=#lAiiyA83I8l86bRU@>6*Oz|cZT<%o#0@(jiUkjCb+it_hYD0W{BE7ZJX-X101 zh&X#J_6n9B>*}qFavMI#4jC8C53;G3F%M8_5X}zZUfj1x z!z!THo2l1?bn352ryNyWjdpLs@39M?0UdjA2!}fVLZJh^~tA5t$t`f=hmm@$?gG;A>PbaR$~yk^c!{! z5xKvNl!*)ESJ(<~u`~i2bHj!r{Q6oM1pGRf)(&QGB^hqwkLZ<^u4V^5#Z&>hHyQx+ zF#8a~N}qkPhoV6@XlBD<&#toq`C2!u6! z1@X!&GjnrZu}3Y4TomcOD|2^q^FnxfPWK!=K4jGBkTvVQ-JglKSbqq0qi{&q(D)vM znkJI+8PRKxi9~$tA`!3pv#(m(rAPjVMSayjNyM*A`h{@R&%_%l3`VXKDFykie;d1$ zjZ(K!C>v#NYemjek)+l1*`fP?D)591fe>DV_HZ;J zuo9Qln045V?YD^>hDMLA6gpKeK`KQYs=Y(n!P{akR z9Vu+iiHOsvhGOjkDv1=I&OKRvD^n;o=1qwdNcSzNu_#FuwEL)JU|#wuED-xe_oTbc ze$g@LBdkuAhCdOiupvTzN|(oq7C?b$R|8y4yTIId(>yRRh}JUd;tu-`ntiR+ERjXZ zniYAFW<{x!>xlr<`Bqa-S+t3k$(enN3_5oz@lhS;|bYx%LDsXHy}@R8IY@m z3E#k++fGnl=^f3=)GD?kbkiB80$P87ZC)eDFiqdP7a|*)8$h00aKfmgHs?bFSXrl$ z2L`ZbpDf8=iEOHPOl@wjOjrcxsvQ1f8)d5RTp8%B$0{c3GB>EAQ%03&h0Jd{W5oxV|+zeM%aI86M63 zcFXEC-a=um6m~$#0=aOTewz>q_s{QS)-1Oo{J{+DAASJQqxpf#)+#^!RHe$5pB_EB za^;cF6Kg=R%GNHw^y%EC&%V^OeS1^A-3izhh4lc|Kr`xm7G|%+Ht6+x@UH{d*StOg z>_BeTd5)(K|f9%$SH+b7q5v8>j!lCWR>GcSu3gCYE9xko-3Vm85cTBb^*%Shp57cf=phO#k_q9UR+vvzkJ;HY z*ddC?wQ2kahTr3vnr*z|N(bTXYJtz@zvS8b0kh7*x>m(J3{ei|^?^@iEub6KOr;$MomUJN#v>=zi4B6Od}!;@!~QyI`^gtKq$pS=?C$~reP za&F|8Px4Hkv0%|m1P|68kbpvihgcU(pFV#&dn+!9nmsGh8`6?bk4k)+tbf5~{?_d5 zE~(wSo7oHv6yD6JTQ{}KmM!LTpVwEvm$3lgjlRRR)px*z;iG-x#|@6{?-e|9+HWC< zZ5>O@5ZEQPs5No(=EUvs!2`W1=HzFeY^99wZe;#)mOT++dDeua3MX2zLgL3ND05CU zWFQ}bp$RcLi(1s8qO+)#{w-k*INA2no*l9A>%E7^B}~}mwRz*xRcm}E;3TQ-*&|un znJ7vlm=Ql%QBiM={RQ4lZ9{GS+DZ#1Cyl2P2lWzcG&rlwP87;}FZ;pK#eR{O0+HKY zMD)$J_6Zg#0r3c>p;RLw6lE$vH+_-pQZ%^iHC;H#UH=6a8%tfvY@bkgH|MQ?6U4Ol zHxTOs#9uP=5DZYz_hf1(Yob^$x6%<2Xg>^rw_k&`&m!xOzg~zVn&~Y~qH@VJ0uAYF zP_$B?1L;MWANWj7LRXxgEKQ5UttVivuAx0*VoxALx~9(t=~rKCo(Vxji!EjSHBN48 zSy@`D^Kk}$nv`gol&;i?XiUmdObU#Niz2Eg+or}ml0ohN&gP11TjA$^uoCI4ki40w z?CYh7F(D%R5jhHg$WGx6P>A>&a)wga_u$5B=qZ`34V(?WpqO2Guv;h!X9IGeB3^+- z%WHX;NAiSRU!1>4@eEa5{yDw;5he*d(9qWz$8b1JUr@^*%Mh)ctqcdf)!#*Lm3E9i zBgYbz{2~-1ps^}M2_x1h`IfgJBDpmY$#1mrA&V?I$v0^e6u;zN;L3L(pV%COYoaFP zR&5~=;qmM2iF80-O*j>t`(G5^S9p z_&ZB4eKF_rP#j;Q=6~(epjC#T0MW6$DiQH`D-a&5mEK&VRoGxd% zz}zPgnmfV2Kum{iHVhe_Bfcjnsx*?MeDoqf(c6HR26(s8nl@u$?3TERi4^;u^z_Hm zQRsLmN^M9-vE!jAH%wHRP!bK3{igchqpv{A01TcoXN$~8j6sez(Ml#X@*?qsFl+4n z(LF#f^4?Vh3VFRql%o^uZgmHNPerI}EM8Q9V-4(UVBhk2KD!|v?Z^&Rg6_v2t`_nT>N-_8g;N1o7IVsvfIV8eD zh+j>oF^qM=zL^VVK`pE>KV}iZKQFgWWqtwNZz^JZ(o1o*t>0zU&Gufd#a#X6GrKy#S%HHPKijO^OCec^>n})m=w1oi4Ucgv#5WgfsN5R?-O1 z70zpRR|W|~kimLuwB)4R#+0F%2wljMjjc^XaE-avGpVVhqYXo$-Iqa>DGj#KvRIc$y;qGp_+nI-|p> z2+FI0^Y$kw=&Uja%oDm)F!T5!`8YVnJYUrDmdt_FR8>mXLNl9j&q zd}XesqMu(yA7>;;g;#*mofxp@cIF)>CRJq~puVm+;#{3`eglQx7tKHp za)(t%Bz6y!zJ?i*RP`_`V#o6WpEbzh@|udXR?qzD&SdYZdCzy0$1@`rmmN)yJ(rI> z@s4Y|;6DtARn2YbqQPj9(KXOu{~k4)>-_WMJc8n)7_;4p;`$#2w-cK{QY5rTT^|Q) z#8>|=1tCG(+=>t=fl+z{W8lb|qWd|xEzE^jE)c&#rgc$R6U_nmG`nMOq)IaEN~&AB ziGN z)*myQ-}o2C%82!8yv95{Y{b~XUQ`k&3x+qO#-dm9=yA3^3EOsiV-m^aT&D+Axv4fk zSQvOTe7X-|4P@8ml5G|>qj^=0QSZP%KzaqMHC|30u0ovDlt zW+nMpwa2x$o(PF6g6pY`XJp8A#bwBKm7E%+Qv%vO1se=>`_+HE0{N`E+IU~#4wP~N z`M3B2E%)@2zEms>qOtmq!Zgv?qa~NFJ@JI}08^|b3!5h*(b&4rXjT|L| z%>#LFz&E=h@;-rD3hi}D`CjIO;5k5rbRPpEn)n|07*G-|IF(XmZqZ5=VRp2QBq=DZ z*cG&WRV{LyAA&W+adzmcvk2a;E2+I(*MeX88G2jTGoUMq+6hs&K^8re@v>P{IFdDB zbMXd2kZibx_|mu3NMDRBh`10ff&HUh3!MY?Wa=xGV|9SWdviZM6>=Q|>kYvU?jfzy zTlgsCI18|HM>UV=ctwZItRF-Wph09Ut;5+&LWweYfV3T~ARt5`O&XEH?Ro=bB;6MA zV^~`fMU>><>dBe`ay2qQ&SKSG%MHpS8ESaLVf0X4QjB+0+n!?Im{%;0TC&C~OQ~z= zI(S6C-+cHQ=Lho*3#-gjSRATm2pJ4`R0mPd3#gH)qAurgrD&pZ?h~jbYc_dWK~4P4 zO}r`6A)dk2hOtBD%ggXll%%;j7~!j?L4+j-FvS0d`V6Eggf)n*okyA`Uxm|amab__ zhNdk`)3hZwmO;gmRL~XBoet0WP#9xDDtILna;I~lkP4m;)fK=Ap{yXJf|;Q@oP@VW0Qo3R19>&B zG5zjFitFic*w7T$WGPIF>pwKb_4#y7Yy1cGlsd=1{$F~h!yt_|W(^u`3WFa9tV9~^ z^LZNWt3fnq>f!kyy@!c<56`)Jm`FW5r}r=sJ;Xv#*GSdSFfml+w%(IebmX?y57>f!Tv_r3c?#|FU6SCY zI4avIt`?-bZOMv2uXqO)UuqTaVEtaYmJdBUgBICepR3vbw-%@8d5AnE?K{l#?qq{o z(t3DDZzv57-O(CK(e4)~l|ayq|XRRedM# zr=2`qCRz1UO*tP)m%?op1!1QaVM|Jigw*=F-iahsysmX3DX$tlO+HqH9{M}4t%JS|$k`g; z+gO0?Zlojxv+6=DxTcetFMQws%hn){6;oa!=Olk7isC6)}1eH2z30(Ao` z;<9muAtf`5`s1zY9q=#;TL3vAsBp4#V{=J^1b1S_?H~+(6lYUBnC5IKtS4A!aMr;0 zPyZifUjZ1!@%^6=ZZ~Wog&Z5X%id9;Xz{ja2^`+yF2#$x1t+)#cao3M~4!it)@=bWmHyEx0V4sJ{0gvrfQnPs!NbfQbONY7a5Y`Y?_`rxNgm;UN z^|CRHa7w<(IxE)8tSe6IqTzFL57;r^vpwBY-)IMYB*~*Vj|Um|Z}Li{?O?4%3YB_BrEa1WPI!wn>nk6q6Ju_{*DnfLP-AKlE>dpcjmc3aM`Mc`kE2(RRoJ+ELu1?88kb=9G~c@9m=Eg;s zA*fa#$WRetw0R9J34;4A?7H6?BGd#l$9mr=j$1h3a6qsfF-$+kt=FlysHSx7Cz;(y zBgB9i1fk@^Ew-PekmrUI0G{bal2`fE*BfX;eNxecE*j$30=ODkpBwMFc9LEbO43(Z z(T!Sc9K~TW@xEqvaz7vt#(FxbpJ%9dQ4V{#rDEr{*nS;N_(NUdjMb8aW z(erecqUPc!YH=!`A_L$jj0&a(Fey@-czGGh1Q}(mPFSWwtY@aJao_!&q;{IS_&_^8QKk#q5xVT&WPNk2Jy)TroDFq_Pn zK7Q;phnSLTn)CMEsB4vc_im%TZQEMdAk&IBC=J%AOk2!=Vl8PMl zvysZ>w&sPC?4@F(n~6-^%*gHqm-V|j$^|R zPw^sVgM-WdjobFlbTpH{J74zi5soJC&EqOAr)K`OioGK?wx|zZ?}Lh0d2WR48R~Ni z*)yb+e~H<<-WKW65UKYqF!c>3p*M1GsfIB_DX4AI}Sv+o*jX2I02mZfI6yMC6T9IwI#3I!+OJP-e%7%+&k0 zpY{F?_5N*I@86{M9`g*9I^yz3FMk8t?cJ#g_m0X6|Hzp4S_onK~QP61e zC%Spkr5n0=u?i{Wkal^w`m3DU0Q2=`x6DX9dIx^IQFx!!PYp2?_hx^W5=HK!2t0Jz z!~nV;g-7=I;GKJ-9#aIKxA}A+C2jU@Bt!Iu?`lDuFoW-Uy37f{Uig&!yAU~d*NnX*ndptkkanLm{dCuey-PlU z8oaT0>B^9*-&+O;sRlSmrN!QTD>Uy^16>cw{6!EINQIBTnP53i>llrO5pYM{`Y7T~ zi6CM<9+gI{-ncu1Sko)}X?Uzg_P>Z)Q zBpa*Hf6}1P!HLPdoRLmUtfM%J%UZm$9PxmvyvbK=5ZHZ+5DCDb~0XMq9;#XZyr=(>Rj=ga;%x%QwU`grnT z3o(>zzU-ua7ME&!A`O;3i7;O!ucZ8%mmx`+LsIt4A}RSbq~tV@l~NcHQrA49JfX0< zre?n z#}KO;LW))@2lyDXHN>Ax^q6+~CR)faGeXn${Y;tYTfA#OhO?ZgI~Af|0E! z0U$#w&d|I{WPCJv)MFz+_)1CS3uQGwJb;9!RM?9oGv))odVRWNDYCfVCtO6(<|fm% z`!HVg;rTT8Zq(sLeu;r{*2t^8kL}_!R5MFF<%@T+!WnTsyUZGkA~a9wdkEV5JyoF$ z?)$rGpWpSP>Y^@Q{-}q6 zH>o`dHk*|p!CUyHj54nqdQXR`_jJ6_N~P4p^<-aCd=B>GtNBZH&(L4j*S9xq-K{m$ zorx)ar0)EV(eg^hM^cQQV7Vs^JkpoX9117fd-#VhkN1b1H+hwPqy?CxV_fX5G)Jvmsei@=G+P>{cvPL7qeJ#R4( zUqeqbkbO0lLWEDPAX3~MWoj`lMB&)pDB;&Nog5TX^tIe=)z$$!>~oFi;iFM3ceHFh z4{!aJB>*oJ#aqeHcut)XJ=HEWiUv9##T;}gVakFTakKo};Tl4;Hb>2#K5P2Snf|TL z6u=e!45??E(evzAEzaYMs9N87CZJk-xUi5u#_)arD&|3|WBY;u3kNS5>F;Sok2jFV z#Kw(VV2^`Ica6vkPM3EE#R@HylSXa+6>=Ww7ua#Q?js<2owP7v)Ua451t%Vp;|I^{ zJFVB*gwS&^rOWjfHQCtFks7(#YvsnlTkT0?Q?C%oyOXOT{3Y49ORwnGq5ONCGeuXK zwSi{H$hH|L&7U`E@;s+#jhYcdyJE)Dn8XDCRyqP8MU@m^{;h)Ut&?WXm@s~Z(-SC< zA31Wu2>bFWD$gwM7`nPu6I*+&ai>FyV7xj5^C5^=(^og>>D@*8<#>dX zJVppJ)Yc9PP2_&VIivTA-4Lti>F0mTo>~XA!t~27#uJ zE3-ngq;-@D`eeLC1W->h%ZMekK5PtFyYF^M2R3g{Lf-L*3a046{IT)ON@I21Okn<` zmAtBR^94v?F20fTTq*TryrV9vI2bGM-X6blm9vs;B<^l6HR2VtbI5YL&dm9Y=Vwt? zk)G-@D9&6vh_NWPFtS09Q)E!Lx#_!8JO2xuDIw_qbv3&*RDQwa)eOgIRsy(`A^hV9}Btx^&AT zC`_0YkY6)@OFl{T6_YHy6gd_?QDzOcc)r429xlTQ)E_o5#95y_I(FUI`QS-$^YmA9 zVQ(aVNVRf;C7rt=z4Sp8bej|-ekdmz`ylEC1$QqKzu*hv#DAGi!QFyW`(d)=vh_sN zJh2Wp5huP55wRztL?|^ZlpdSssUhXLTpA=&0@tojrHIgN@|CG44=nf{nvwjPdp^|P zyUDWUvwdD!ZredY9lU5oFTu_iS zyN-bF{VUfWK5b*Ak!2bIU5J3j>KrpMdbE=V$X5WGES zOq_0q>&(be(b1!vTp8=hBsZJWVe1|=2ekc>A5GBli%a#babsCUxbU53<<;nZij&35 zlV{F~o`r7h+uFYAEa$ld%jL)lhj;int%5wq~<4aKXm+Y&;G;o*hV%{!@tHx(` z6`xr-UDRY;nmKLe)LE1L#R9x~LiB`aN5`CMOBOG!ja|6}4kQ?5_^|xOW{eM4~1)?(g&geP6PyE0zVWW8nmLnIh8n(uc`IDrW`IDz$GZ;@Tyuc>P>*lUl7PHo0 zSm^=;E_qtaZSd<&NNtEcT7>__ExLv&-7nI z3c#~quz=|pGP8eQTjYrTLxzx|E$HZ_qx!bPh!0*f_7P4}|K{g{LEI+3$4tIv-|rE$ z5#t2Y4qRf6@Hei4{r|H>6vOA243R?r+=C0w1u+1FKH#WGFr084VmI}3Hu_Ri-q+bTxtM&fojq?_*a0(*`{5k6-&zxZqUCeIxfH(W|q>d^>Em`EZt?JEGhHwX!2siD?5m}kvIyG(;esmPqsR2 zCye@a#Lftw%~DQ!gNH6k$JyCgO)Mgb-k88v%Ts$ zj2Y@LXm@ALM9w7Z`MqZ>x=4oM;pj`z6;ucO!aNrG$}+YE8Qj^(DtMk!Uykypkn-l&!^I1L zN;vP9#2j+#!oU<09V~dDiYTD6C9?JQd~DAmG2n*6hEkQ}YUZQ!WC*ngRa7@Cxggs^*5i{}nto@VNiuTE1H8Zt zA^KWp?RCE`TvMNWG#x&y?di{NV%;vOr!rk}oJ}B{0#jJ|){tk8@e&Fl7I%UCy-`=Y z_eostuh6Z7*#eE1sRTo>XS6A?wWY8saFyS&dx*i3b@vA~|0ni^LvYAPr4x~? zoE3#a95s?3N{qsLr`NrEc9|77qUe%iV6AB~Bjqor$>AHkqs$(Pbv(ysvx4C^XHG0k z9qvG$jb;gRj9FTTp--I=nwVKqy19~BxnN^nOi4AD*-(nnM$aRSp6^TF^T#d~;V+Q4 zfYAgig7{rjl+2+s`)Q0Eu*nBgM~gv#u^_Tf|8ewk z(kb{S|8S@=8(q^4(f9}6QEjlbMUrE&w0P0X`EkwxZeUFuYrMC*qaSOk93E&{G?iD; z5_l=4n{#MExk1a84V{N3s|m4jiH>_crEb0Zba%9o`VNdubXs3G7E!=oaEsAcV^Qu) zZrCA$kXXI}8{EK+O`$v=z>X-1(y={zjv<)lXyd)?vC^o9(_KKlFE3lTXt^U%TE4J% zcV_{)Tc1AN9DSv3eHJcvTJJXylThtYB3^&v!#QNZGrQxHLFaxTt@o*ZU< z4s5eaEHHQnQzx*DEUyhIYszB15;ue1Ff9cVPUQp&HAgpc`84t7@)>2O>@k38mj_>y zRs)e5;S}FcfT%`!tDbKoks&+b<05(hX(D)Za>PXf#Jz_&98~uLKWLwam4EA}4ned! zn=Kl`D*H(i55V10QV#l7+WL_Gr^Jkv`*w?nW(F{eFI+Q5NCL)Q_wRFhPl90K8{3>QY*sw zqy&^?nD(MnC(&ChQ0t98!=0SvSKRAhY?Z{FjO?2?TFqlPWts)y>Fmp_z?^?%l4dNB zm02baKSO3f1g6DA`u+(Gw2q_R#Lt*cX-75HU@>wfc6=j00yv?S$}qm`BJ zECw!fhuK&K6T@BwvlYwC!)T2QRKYZzZHgq&cdJpsJ%H<924K|XXHq$3Jw3h21usS` z{zz<*2OID-@8j(*K&_gJT@S=CnwqV!VzQyS7>Yl?D7mhddgjws7P)ix$nI@NFX!KCefy0XI>5;S zz(?6iBO3~#c*C10i@bXIf~8x>cki>-VZAmue(}g9_QZtgbK;yrxj^g?;Mi*jV_Rh{ zffxh??sEL1C2nd$OrkwTA0*Xo-oEZ2r^xG>E-Lb;5LGwoY&s^&3AVG4@~VZ1E0p36 zmp1~YSIlQnFJtLItH_F>48RRV4z?7k(!VLL`Wvy^!gjlGasY4MjVtUY@i!LxQ_D38 zKuAX{K!9UN1(;79vCPCGH1G?+{#Z2ZLi5QpDcNL#Z@~nUSt2rGG(3tB^!T67)wwYt z3@4Qo7<-a|Y4Bj`AD187SOE=m9?mOBFhUd78&%E71sz`4m{!+ z05!xBtbg`^fV6Rx8K_&In>Q(coR_8H#6<;Skwc2~$S89QX%8^z)d7b>@JwlV;K(Yc z^I!%9h&mpe3ktbdInfV!o9J{}2>fTTX5?3#h;|hOeSD6rR72bm4aIZIO(eTvJJ}C# zA)bS_p8+q|*$lW!WB}=1;pn8B6ek~+ib)F-H8RT$04kAFbF_!(F#o#emgyxgcv zQgq0al0F&Fpw>66wz)ZRkqh-AgB!RAwzJFHqvIM&#K#2qEE^4Y??rSU3r)t^e!K_} zx2(ygKGKfyX~Xg%0pxq`go)}Dym%-D5SywHO#5+crDlsquzQB<5nSPJ>NALrJS>Xv zq=D%Z?fDLw9zjnj*i=VwA4Kc+1V4|2hX1ZU9bo&^-m~81F8A4A;#=Bmc|HX)9__Mb zzJ)v37J6MY(vtj_EI#NYE${z3g%O4zEz=LBrAv?en_QR}!+%bHgl91|NFqd`WfQ2E zu=APjk#CJh|CL2!7rp_TMpd$DR81b%TlrRGH}>XBWISky`uteKd@&b~<(E|FySU@HmW(a85 zTtuK>+7VI1pZa2bquxVbOm>KrDDP;$jS|yDuQy7X1#^JGRCr@76-E&KuL@4uFWwKirmo~4Wu)9vJ5h{lg0AzsPH-hD65z^+>om#i< zggb$}8rC}{ar+{e7t-)8Rg7jShW}lDguR(J5F$<`*>EJG5aq=&xJ05o$Jhiz1#yxD z=%OS*-#=-J&rmh6(~zKfO<)upY(@g~eF)GT0yJ{`M!#X2Et(aD3LC}`f{;kX%OOuf zl5}svfd(K+HOMp12xBFz7t$ro1A(Z2uJe&rMW{ z{1wv>01(%ZUeLw~Bid6<=hGo*holMGJdlM#5p`CF2ZFW7*?@sh*4q$)(R&O(>x_M^Gm5!@t1uPtd4>t>L%cJlXDPy1s3<;8HJX7< z{6EB6egp%>tDV;F<5771(PPzshlM9U4OqFCwpWxlMoknK4G|6kL)`xywTbclPvJ2o zbhZQ;?U0#iAlOJwM9z~I5)@``-PK&4YB&Pm4>bSxd3M_Ssio#o08iYYOpoWF3HH>!hReS%(?)xvbg4ijFXpA zS3x<}1qfSfNh0q8#^cL!>a3UUw~W+gnLO244R3jgf&z#c>T(^aZL@W!6H$u^@zw1p_7aS1|j@`wVKjtkY=# z&s@f7APT2}D5gqyz$KdFB+%SA37}m+LFh^gMum7<6k02oP4}`My6CaJJDhUm%|>jY zabr0-;>KLic5By^RAZ8f@bPyl>c_{hFhDhfF#mh1Fdq&RM~U&(tCzCVEH!SkY0vIWoA)>wCKI#DcdP>BfOi$T zjB~Kf6yh<>8G(3!nTA_EW-yU=p&9IwZU!^r7$BOn94?VVWTLr5i7t%has+#Y@Znb^ zSVBxsGwK+UJ=qwqK7$(}GM=?}sr5<1?JSf+Fz3*yv0*+dkH_qg8N6z_{;eV@ZYcCHR&7(YHhSMx*3Laeov zG+&v4)ndx*&9Jw}Luj972<`0d2-V=LkVv*`X$&`+`DlxwMK^?YWH2*o4$@gXY90cq zxDXo*skn8jd5}#4Rj?e&d}9qWC~kYF6X2_P*_%N*2FvqGxc_`4%2)H`NMm$fQ9cex zJgK5QA8+(KzroQ+8p#0ekXJdV^B~Cedy}4VzY(=+dr)89l)EG(|RE# zt~I4I_8tg&IR8)8jl@<4B8BzrJ)kIkws%uIQSzp4gsFJaxvwS97FU3|_9=O#?&soX zNcK&8Qn==)f;~C#Snrok`3Czqn({%gCy@e%kVz>$aEIJR{xfa{0@OQFrgbw68uB`P zQ(Av#~^tw}7G9CI2=D8b)IwZ#!@;ZWx(xml#RWDFP=EuH9 z#Zp5o)44azGJ-m(C}eU3fkGxX5h%Pe`J9pzl}S4#x@SyV`k{LX6tbKOtpTqz9&N6D zEH-linEOq0%+gK3cWlzTn;x_zjvHeHaeM{_2|E+v(EUS8{yC3eU#J~McInm5Zi38h zUAu0Z^C?7_`}q%R#j92+(QHwhb-kU|=WTlTL~^koPgl9=cqpAayz@jHJf6};=Ei|@ zB7*4uKsWu|gZ&e)F_J=Rf%^SeEmI0O zLM$8P$_K`x98Zhs2BI-(S?*w2T>mYO4bKoZt#LHHk&f8@bNJhT4@W9W4uMF8jkDj+ zhCf@jRDU)jez-B-?doRY-}QmWmlIqvU8$NezaK*+5A<0+AN!?~G4I67AHa#;luy z<8@Xu>t%Xf&;O>ZE-8fpO1#1-y=9b zK>5XRAeyi`$^r7gS&sx9ANA+D@K{V;R$-RMg)<DncP9yz*k*WcpsJr8Qn3=ZOkfH0H&Y4fQoEA zWf*S?uyCsK3eF;@5y4%VeMc!2j6njA?8@@|jAmpol$hJu))Dz(91(5!ma7We=;^9f z18!4lLY9Y+QP!|Ltv3hZv=4-FLNZ6P`8%lXj-`bZjf5O^U_0LfJ8mrm&5Kx6N^s(B zcyV-E=RH}TpKyF;NM`6`J$@~@2G0#KG(}QZu8!Zf+lEYq)|F)FgExmJ&d5#TTdeBj zsDXptMpheqXl1P?1n=@Eb7)9>NRC^pVOw2E_X1}q>C^muu2|40`OT-g1)9KvEYFYE zgN`EG*jY9{xjfArJMCFR%7OK5XcNHzRl7iLC&{JYougA#v<$P zkJ2bz!Q-_pIBr2LPCC4A z*_PE#YnS#3n|f`x!x(K{Euu&Kjpne~dcB94@0MOb`@4@HEDu(--SYEopfYZvzpOPs1}xK6W{w(iSl`$ z!5-xU@t+Jce)m{aHP^M$PbWB7rU{m13;T8haxhLz@|k-3D@Qv-rf5s8=q5YewiVMB z!GnOEV{eh>7Ip`yrx%GsmM$FzH);(}y=t(E2fdm{=xi6**Glb%-CK2XVok(p)*i() z&ny^}$vpAD>h{uyG5(w00IcyYAK=GzT^%g-?LTk9D5q$^=UNtx=(w@2J;HTKS}X`jixdIh@xlOk{L|7bc1Ncq`+Ot(zLYtYImXx0x+r~D8z$FW*n$5*o;&Qgaa>Oal18`;p>)*p z<2wv#VY=(SsRF2irN(|%9I^U9$#9LpIwbJumpGE?h9_I@lCXGV*t{-e2Bgxf4$3Q5 zMJ=P;$GZyTfy+isMVIm_*9;V^03M+6HP~TA>|IS1HCwgr0Y#c38ENO$j%N~5d-|xj z1=6flnLIwozxIu007y(jD<>GOEaHNm)@yPr8v|)|7RR3qCNMpJQRoOPF{W$uCygYV{FzFL=xb}5MX&) zd5kKdK6_?VS@NgsN8=2!Hu_q~hc!Z1_R6x{E6bj#a^r(6Yw!=MkyuE7 z)YrQ*U2BE|whCt%6%g6*@ZRIl0A{qPcsVKEkg%bmz5hK${QERc7GriW#*cw)s**uq5t z&iRkn{0#(AKdjhH{D5h78KX7p*Y=x6&VeH>;)~ifD-^Pc+=fK)hrD6_Ar|CzES+n) zxAR2OV_RMY-l|S4T(2<&?j&Eq+FL;Py92v3aJU%3+~iYI>_ zLQ3EaTT%%u%lhD@RRm+F;UEmZ^Vz`Bsz^=OsC)lP^=Wg>7Bb{tG8(|S8 z4U!_QP!eb=nC&rXN<2-ascHsI={$g!Qw*A!EJYt$xav?m3)mcbVbR|Gy6tMU6Qu$1 zCRMA~vep3S*?yK4GnXc;wj%}A#?_tMwC>!iCCt#7TU);G*0N?HblZEJAE%piH4bD& z0Ke(!iY@%Fbj2=#uGpoI>FQJ}S@9SmD`2?slDgk>{yi3fW%gY4QKXwgV#>boJ@mgbAPDSQi&INw4vj*4Ao1i{&7mFxjq|!H zo8T5DVQEfB=6E_pNk&HOR66HdK&7C7opijJOSA2Ci98|7-%h}mvmx_(2f<8RLnERNyBCruiTQ@#xCWetPnmeoYT z$Lfg_NLfuU$}LZwY5_83+qqrWj@{~ScuWLdcnmSchg=b`Ts%fONGc_nVsK~ISwWcs z!L9#*jfFTKEEakUt1^~_x^%FNpW!?1<;2$eOXtp*f`3!PL{9SAeT^uNOWXGDy=}0= zG)4~PRw^rplJPI`6MTB_;|+&!P$Vl6TYDa{szI-hjNOsAU@S7o`5=khFJy_`7{R~f z;VR;85R!HE8N_CPI(9@H5M@!v<_K1H0|h=sAiz5pRSht&?XDoEF*7R@F>C(w_Z}@F;`mPsw=QsrH8(taPAek$1Ps$|BBV zOGICK!^Wt@XK*C{Cn9mFI-sv5QT|mlQlq2wyB1-Q`_Ab#qvs@yFYQ1?L5^iE><9gt|u1)sB(Fo7liM5 zNlN@e!_TTPpGIW4^=@M81;wC2Xh`^MGn3-t#*9Sjl3%I7Yq)N%>Yo6I-4hDcR{j)N z!c+YfVwJ`ob<+S}j{>|KXtajB^QZ->n7z*r{;6QTJu{iID%JSqPgC`BP50g4{8a|+hbK2Y)b7s!=-{lEoleGIVAVeM6vGXv( zR1A;tRxom1i4w~mM?I6#|DnxO=Y!XD0`(f0l{2T;jU2&`!*)9w?5+7qalS{>83m(7X5@c z^k^{XD+Yfa1NO{{LI`<9LdX^fA)`OYAa8bjE`zWf;LvPpa|H;g*e)u;18#|%y^2J8 z*b?j*_~p|0i;v#V;?V&18oPw-X44{RqmZc?xDNdO*G5BFqMH?RGbik6mEj@m$II}F z*flbuS^Nl^h1X%v$c7euwr3PUi}*swMOCmdu}umaBnGhwmE4WoRv#bhTOYOwj<`|S zLEie9l9KQ}reRUGSDVh=){gy#(}>UU-FuHY*UAUmZf`i#F?ou`>hX!D>ut2avTk{A5vxWGW_1NA$$DQ%=#sMq3E^t^=a$p;L7eV`AN)Bv<@3Ph-A*~83j8xpr7q$J0 zx}ta%D)uu9M6nA5g@ml~?|rE5C=&PFtiJmqT3T4E=eF)V3@kHr;cXiYu^15}6i)`U zleWlUq?(}pyX?_A7%8}p%kH}FdPh++9!a5Yzl%tHhzC<^jPcu% zCAtlY?9$$bZ_TJgQwT^QTJWb5>1gcKtlfv5`Uy7bC)5ZKATn5&Rz}}b{;8;p#vaY4 ze%PZyLm?of&jei1K#0Qof2fbkwDZeUBkp16uTUCviJd=dMWWAKAd7Uho^<=3f}`;% z?mq%T3Jy^dgu@-@w!VhCkBfgiTa6dK0io~@5;Z0PgBrxvxuY=+Ru9T1n zPn#(=;U{yZKy~j7s`EskvUlmI&gzLWA5NiZ^Q4(xd%$Ve_7lj;FxC2 zqxChqoHpS#l)46~@9`mO+e=mN@d0^yqCjw|^G=70!tSfIJ|>6}ZkEY-#B65@t&R`v zgxA~#Mt?zS)onj-N!XxY*!c#F~Kf9R^ zt-<#`2v{KLOl)8aKrA1sy~XX=x!l{`iJ)!_?E7y#p>#^&Uurx8h#Oc*XeNmHJ(vy& zl-74ixe$*{yH6q%Sh>wU?M1g2ie7r;}qCm7M5lvB2 zqa|DRXzG$h4Vb+5IcOZ;M^Nu_Q1ARPj;Ix@_LwtmA-tQ4I^X+y>wGsAb-wpE>m010 zMfKgxf|X@`SIcbthn8pkye?W)@3W@XMT_b^78N?je=?S8Z45mNpdP-BfVtG_iTJFJ zWa^_Qbzvq`A3f+JX7Cnfo`$-Ndp|~rhFS=i5WELK>-*{&i&wB9+`DKwfW*aX(TO;? z^&Oavhh-r;Af|EASPBsQ#TV|G#=`lCjlTJei*j(2H{hYEtl3?Y4hr{@F#*WK_E{F&> z!SlfA@@p{sCUds8-)CP4C(#eIjTPZ;XsibSh-J|hPDAL-B5B#8KHa*G82C$9=ZG;F zhp6%$JGZK0D^2H-8u9Y7Da%f;a>UKF99y+^#YvkHdea-u3sL65Uu5v)GZzCd4<=q% z6TOLzH$=Z9=)9D+@uWn3He~ONA&4^jW30mnb5T)e-nbj1GpwX>(8@*2DTO*x+1NWf z8?#f{y^YorF{*dpctR>_sdw353abo)G?x?_VlF9K=H4asF2Z<#RiugLCA8gcKaBeu zF$?=I4*+V}OkCt=7XIKJ1%xI=7XK=Z4}OQ*cfSJey*k4Z%~IP z3Ug;nxX0Vu5ytN5$?#ED7B?(E=Ey=ghb6wfbgsOD{q(UR`$sb($j1MnW?Euw~eB)VERv}D_MsA##_ z4r%$K-re{&n(^}Ph+0Ef+4_B|`(aH)XT?Oa>hQurI>87SBw)Wi#QPAUfCrI2$VANH zq>m7V&MenJlwqBLWs-%JhlINnZoj$cpK*O>Zp~Ajt%H5U7m=5K2uozKr1H$=>xSNi z{`YeOpNRch%lN?^2j6PEmJL#M-qS>HZFnN@x z+y>+gi#xEBEF#6Cgd-$wR5Abf=)woyQ^ue2E_lmUL24yv`Va?M4?&!)MlkMH5%oyG zH`*S|E2Aq89*L$;Al#NaP|u$0XW2D-%esB`jna|bO>4I5+o^eXXYCGc>Q%4}(BtII z39Gj(aU5U2@z6ONtEaS;eymWZ3{-c8p&B-O0qqk)rW}CPHlSdh^XY#myfT}Fn5?rm z&mty^S60umNtIw|LU^C?DUj|5vH52q9VfB*sRdw_h0{|ma;9oeaS`*_hTPCsHWh%M zhq#6f=c6ffc}8L$g^J$z@Ib`t3RUHSx+f66REgf%O2jOdr&)Z6sJ<^gOe6o^JB^qd zV-Co8{OKIPRFl4A$q|1j`JmOH|9y>&b(eS%ObUb3qQqco!7qg9v76~iQGne~?1uX5 zZ|ip=(3V{T$UFDJFLoV8c|nak6xJGK)H2 z@dzNKJbRaTYxNWwT@`J{4;+tf8r~H~TX~?_Rus^xqNP>Q&{Jq9VL}@13^tkxYrfe` zx^6TxI61;-C7JWlO1f^e5@xw{%|z1269%@X(M;T9nayU3OKHuVru&}d&HT=?X)14~ zdzxW7ap?tIEMCy19J(imc?l2>NKw|9T~@_O7dwfIM^Ya>lJ+PrnUCU<_fcH(K8oI^ zKI+RnzR!3?zh`XA0bn0hIRJ{IYe#Vn3Anc$fB}hBIe=lLtL#@c*bl!@)_dW5TtIXY z$^}m3xez%VtLedWacZc5^&1HsMxdbju^GGq)Z56e0;gK+kQ^o92J&%H)|aNxENGzK zn|#J7hb5Ap?XVxyTKy=k)pX6<>PH`1O;=M}jk1Bpqs-fClneayKRA3HHo~?wp4ZPmVOf@;2*Prm9Ve{(gx$p zZ}_rXuPO~aY!MyVJ~h2R%{~Drg7Ok_5(lfG`FV#etTkM8*jL&W{W;ms-tux3wCdTp zV&y)!ZI1lpr_1@KIBvU&iF}Yo@)gr>%htC~qpx0lYFk7O291|Ha>8H0>Oo$ww`{hj z0z)R$yf9?vMPg%M5Cs-vdHDgv&gX?;@*z}t?8b3*u68!Q|HWztOHU26ZodpQy=~)BUa6ueeIfg@;!tTL8dJMjls3Y5j_9=5hT{C9EHP6h4d{Pk<7i zA22Gv2k)u+g#CDtdDuHO|2j*FmHpT5wzX2Wo39ygzQ+D?GsCP~ub}A{%gA}h{Zu-S zt;pUw58FE)w4bL{inLg-#Fsr&=h`@b>PI=CJpNt(JdZ*&{u0!PfItdkr(La!c#cPk;O86t zB_I)m{SF>22P-gQlGr73?H`JyQRoVMPN^>l&>fBVTl4|K#0G`;(+{842w($ox}M^a zXr7Uk_aHX0AN)@79V`mzFks=PHlsDMfu zz5-%6#CZMqhYF1)glnmD}tf�M9?Eqi7M`1a+pT;Y0(~#U59*c15>;}`c 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 Nq(f>^`Qi)v{{yFAbiV)q literal 0 HcmV?d00001 diff --git a/src/assets/svg/not-found-illustration.svg b/src/assets/svg/not-found-illustration.svg new file mode 100644 index 000000000..7340bc549 --- /dev/null +++ b/src/assets/svg/not-found-illustration.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Page404/index.scss b/src/components/Page404/index.scss index 299fcbe01..c42cb9ace 100644 --- a/src/components/Page404/index.scss +++ b/src/components/Page404/index.scss @@ -1,6 +1,19 @@ @import '../../assets/styles/variables.scss'; +// BB Roller Mono is the large "404" numerals in the design. +@font-face { + font-family: 'BB-Roller-Mono'; + src: url('../../assets/fonts/BB-Roller-Mono-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +$not-found-text: var(--rd-sys-color-content-primary, #1d232f); +$not-found-brand: #2770ef; + .pageStyles { + position: relative; display: flex; flex-direction: column; align-items: center; @@ -8,22 +21,136 @@ text-align: center; height: auto; min-height: 100vh; - padding: 96px; - color: var(--rd-sys-color-content-primary); - background-color: var(--rd-sys-color-background-base); + padding: 96px 24px; + overflow: hidden; + color: $not-found-text; + background-color: var(--rd-sys-color-background-base, #fff); font-family: 'Optimo-Plain', sans-serif; } +// Radiance top wash across the top of the page, faded out into the page background. +// Size is the container's (RULES.md ADJUST-6), so only the box is set here. +.radianceTop { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 320px; + pointer-events: none; + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%); + mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%); + + .radiance-local { + position: absolute; + inset: 0; + overflow: hidden; + } +} + +.pageMissing, +.docMissing { + position: relative; + z-index: 1; +} + +.pageMissing { + display: flex; + flex-direction: column; + align-items: center; +} + +.codeStyles { + margin: 0 0 40px; + font-family: 'BB-Roller-Mono', 'Roboto Mono', monospace; + font-size: 96px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; +} + .headingStyles { - margin-top: 0; - margin-bottom: 64px; - font-size: 28px; + margin: 0 0 8px; + font-size: 24px; + font-weight: $font-weight-bold; + line-height: 32px; } .paragraphStyles { - margin: 0 0 16px; + margin: 0; + font-size: 16px; + line-height: 24px; +} + +.homeButton { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 24px; + padding: 4px 12px; + border-radius: 999px; + background: #eaedf2; + color: $not-found-text; + font-size: 13px; + line-height: 16px; + text-decoration: none; + transition: background-color 0.15s ease; + + &:hover { + background: #dde2ea; + text-decoration: none; + } +} + +// In-product presentation: illustration beside the text, primary button. +.pageStyles--embedded { + .docMissing { + display: flex; + align-items: center; + gap: 48px; + text-align: left; + } + + .docMissingIllustration { + flex: none; + width: 140px; + height: 118px; + } + + .docMissingText { + max-width: 440px; + } + + .paragraphStyles { + max-width: 420px; + } + + .homeButton { + padding: 8px 16px; + background: $not-found-brand; + color: #fff; + font-size: 14px; + line-height: 16px; + + &:hover { + background: #1f5fd6; + } + } +} + +@media (max-width: 560px) { + .codeStyles { + font-size: 72px; + } + + .pageStyles--embedded .docMissing { + flex-direction: column; + gap: 24px; + text-align: center; - a { - color: var(--rd-sys-color-content-brand); + .docMissingText { + display: flex; + flex-direction: column; + align-items: center; + } } } diff --git a/src/components/Page404/index.tsx b/src/components/Page404/index.tsx index 40e563eba..7820160d0 100644 --- a/src/components/Page404/index.tsx +++ b/src/components/Page404/index.tsx @@ -2,22 +2,71 @@ import * as React from 'react'; import { Link } from 'gatsby'; import { NOT_FOUND_GO_HOME_PAGE_ID } from '../../configs/doc-configs'; import t from '../../utils/lang-utils'; +import { isPublicSite } from '../../utils/app-utils'; +import { useRadianceTheme } from '../../utils/radiance'; +import notFoundIllustration from '../../assets/svg/not-found-illustration.svg'; import './index.scss'; const Page404 = () => { + // The page is prerendered once (404.html), so it always starts with the public layout and + // switches to the in-product one on mount when it is opened inside the product iframe. + const [isEmbedded, setIsEmbedded] = React.useState(false); + React.useEffect(() => { + setIsEmbedded(!isPublicSite(window.location.search)); + }, []); + + // The 404 designs only have a light variant. + useRadianceTheme(false); + + const homeLink = ( + + {t('404_GO_HOME_LINK_TEXT')} + + ); + return ( -

+
{t('404_PAGE_TITLE')} -

{t('404_PAGE_HEADING')}

-

- {t('404_PAGE_MSG_PRETEXT')} {t('404_PAGE_MSG')} -

-

- - {t('404_GO_HOME_LINK_TEXT')} - - . -

+
); }; diff --git a/src/intl/en.json b/src/intl/en.json index 77f58424b..3354e0ec3 100644 --- a/src/intl/en.json +++ b/src/intl/en.json @@ -3,10 +3,13 @@ "DOCS": "DOCS", "NAV_BACK_BTN_TEXT": "Back", "404_PAGE_TITLE": "Not found", - "404_PAGE_HEADING": "404 - Page Not Found", - "404_PAGE_MSG_PRETEXT": "Sorry,", - "404_PAGE_MSG": "we couldn’t find the page you’re looking for. It may have been removed, renamed, or is temporarily unavailable.", - "404_GO_HOME_LINK_TEXT": "Go to Homepage", + "404_PAGE_CODE": "404", + "404_PAGE_HEADING": "Page not found", + "404_PAGE_MSG": "Sorry, we couldn’t find the page you’re looking for.", + "404_PAGE_MSG_DETAIL": "It may have been removed, renamed, or is temporarily unavailable.", + "404_DOC_HEADING": "This document doesn’t exist", + "404_DOC_MSG": "We couldn’t find this document. It may have been moved, renamed, or the URL is incorrect.", + "404_GO_HOME_LINK_TEXT": "Go to home", "SEARCH_PLACEHOLDER": "Search", "CODE_COPY_BTN_TEXT": "Copy", "CODE_COPY_BTN_HOVER_TEXT": "Copy to clipboard",