From 8eaebe9bfbc99afef045cff2c6af135b4b49af1d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 12:11:30 +0200 Subject: [PATCH 1/5] fix: update devtools trigger logo --- .../devtools/src/components/tanstack-logo.png | Bin 14643 -> 8649 bytes packages/devtools/src/components/trigger.tsx | 1 + packages/devtools/src/styles/use-styles.ts | 39 +++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/devtools/src/components/tanstack-logo.png b/packages/devtools/src/components/tanstack-logo.png index 367cbcffcb8a6f3dd2104fad6eb2cf8c9ebd36ed..0fd9af58edb7ab3b0a75ccd4ca51ddd84d1f3d55 100644 GIT binary patch literal 8649 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 B(mVhF literal 14643 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 diff --git a/packages/devtools/src/components/trigger.tsx b/packages/devtools/src/components/trigger.tsx index df7e0763..b4bac0a9 100644 --- a/packages/devtools/src/components/trigger.tsx +++ b/packages/devtools/src/components/trigger.tsx @@ -67,6 +67,7 @@ export const Trigger = (props: { // static-flow position) and the hand-off to inline left/top is seamless. (!isFloating() || !coords()) && styles().mainCloseBtnPosition(settings().position), + !settings().customTrigger && styles().mainCloseBtnDefault, styles().mainCloseBtnAnimation(props.isOpen(), settings().hideUntilHover), isFloating() && styles().mainCloseBtnFloating, ) diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index e01337fe..da41c33c 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -80,7 +80,7 @@ const sparkle = goober.keyframes` ` const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { - const { colors, font, size, border } = tokens + const { colors, font, size } = tokens const { fontFamily, size: fontSize } = font const css = goober.css const t = (light: string, dark: string) => (theme === 'light' ? light : dark) @@ -452,15 +452,7 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { padding: 0; font-size: ${font.size.xs}; cursor: pointer; - transition: all 0.25s ease-out; - & > img { - width: 56px; - height: 56px; - transition: all 0.3s ease; - outline-offset: 2px; - border-radius: ${border.radius.full}; - outline: 2px solid transparent; - } + transition: opacity 0.25s ease-out; &:hide-until-hover { opacity: 0; pointer-events: none; @@ -471,8 +463,33 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { pointer-events: auto; visibility: visible; } + `, + mainCloseBtnDefault: css` + background: #eeebd4; + width: 56px; + height: 56px; + justify-content: center; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(17, 17, 17, 0.14); + transition: + opacity 0.25s ease-out, + background-color 0.2s ease-out, + box-shadow 0.2s ease-out; + & > img { + width: 48px; + height: 48px; + object-fit: contain; + transition: all 0.3s ease; + outline-offset: 2px; + border-radius: 8px; + outline: 2px solid transparent; + } + &:hover { + background: #e5e0c5; + box-shadow: 0 3px 10px rgba(17, 17, 17, 0.18); + } & > img:focus-visible, - img:hover { + & > img:hover { outline: 2px solid ${t(colors.black, colors.black)}; } `, From e3c8f897552d5ed3e6f02d73d99fb6e43a04b34b Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 16:13:52 +0200 Subject: [PATCH 2/5] docs: add TanStack Devtools branding design --- ...07-31-tanstack-devtools-branding-design.md | 385 ++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md diff --git a/docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md b/docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md new file mode 100644 index 00000000..9c1a5cc7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md @@ -0,0 +1,385 @@ +# TanStack Devtools Branding and Compact Workbench Design + +- **Date:** 2026-07-31 +- **Status:** Approved for implementation +- **Scope:** `@tanstack/devtools`, `@tanstack/devtools-ui`, `@tanstack/devtools-a11y`, and the React basic example + +## Context + +Core Devtools and shared UI duplicate raw color ramps in `packages/devtools/src/styles/tokens.ts` and `packages/devtools-ui/src/styles/tokens.ts`. The accessibility plugin has a third styling layer in `packages/devtools-a11y/src/core/styles/styles.ts`. The resulting UI uses unrelated gray/blue values, inconsistent light/dark states, and a plugin drawer that consumes panel width. + +The replacement is a compact Workbench with app-first work surfaces, TanStack brand chrome, and a horizontal Plugins strip. This is a presentation and accessibility change. It must retain the existing core navigation, plugin lifecycle, equal-width multi-plugin layout, whole-panel resizing, persistence, trigger, hide/reveal, and picture-in-picture (PiP) behavior. + +This specification serves shared-UI contributors who need an exact token/build contract and core/accessibility contributors who need state, lifecycle, and verification contracts. Each contributor can follow the relevant ownership section, migration step, named test paths, and acceptance criteria without consulting a separate design artifact. + +## Goals + +- Resolve one private semantic light/dark theme from `@tanstack/devtools-ui` for core, shared components, and the accessibility plugin. +- Use exact TanStack color, Bricolage Grotesque, and Inter assets. +- Replace the 0/192px horizontal plugin drawer with a horizontal Plugins strip that expands vertically from 32px to 48px. +- Put Settings and Detach in the 36px TanStack Devtools banner. +- Keep up to three active plugin panes mounted at equal width with static separators. +- Register the accessibility plugin in `examples/react/basic` and provide deterministic audit content for browser verification. +- Meet the contrast, keyboard, forced-colors, reduced-motion, and constrained-size requirements in this specification. + +## Non-goals and exemptions + +- Query, Router, and all user-provided plugin internals remain owner-styled. The example's `examples/react/basic/src/package-json-panel.tsx` is a user-plugin fixture and is out of scope. +- No per-pane splitter or per-pane resizing is added. Plugin panes remain equal-width. Only the existing whole-panel height resize remains. +- No public CSS-variable theme API, new theme value, or supported consumer token contract is introduced. +- The native expanded operating-system select popup remains platform-owned. The closed control is in scope. +- Vendor marks, plugin logos, screenshots, and SEO preview colors that simulate Google or social platforms may retain their source colors. Repository-owned chrome around them uses semantic roles. +- The implementation does not change audit rules, plugin registration APIs, storage keys, state shapes, shortcuts, placement, or PiP capability. +- Gradients, gradient text, glow, glass effects, neon effects, bounce/spring motion, and decorative card grids are prohibited in in-scope surfaces. + +## Chosen approach + +Add a private semantic theme entry point to `@tanstack/devtools-ui`, migrate shared primitives, consume that theme from core and accessibility, and then replace the drawer layout. This keeps one internal semantic source without converting the existing raw-token surface into a public compatibility promise. + +Rejected alternatives: + +1. Restyling each package independently retains three theme implementations. +2. Publishing CSS variables as a customization API creates a new public compatibility contract. +3. Keeping the vertical rail or horizontal drawer does not recover panel width. +4. A single-active-pane tab canvas removes simultaneous plugin panes. + +## Package ownership and build contract + +### Shared UI + +`packages/devtools-ui/src/styles/semantic-theme.ts` is the new source for semantic colors, fonts, spacing, radii, shadows, and motion. `packages/devtools-ui/src/internal.ts` exports the resolved theme types and helpers for monorepo packages. The root `packages/devtools-ui/src/index.ts` does not re-export them. + +Add `./src/internal.ts` to the `entry` array in `packages/devtools-ui/vite.config.ts` and add this package export in `packages/devtools-ui/package.json`: + +```json +"./internal": { + "import": { + "types": "./dist/esm/internal.d.ts", + "default": "./dist/esm/internal.js" + } +} +``` + +Core and accessibility import from `@tanstack/devtools-ui/internal`. Publint must verify the subpath. The `/internal` name declares that it is package plumbing, not a supported application theming API. + +Keep every existing raw key in `packages/devtools-ui/src/styles/tokens.ts`. In particular, `Tag` accepts `keyof typeof tokens.colors`; removing or renaming a color key is a source/type compatibility break. Shared primitives migrate to semantic roles, but the legacy raw map and `Tag` keys remain available. Core's duplicated raw palette may be reduced only after no core style references it. + +### Core and accessibility + +- `packages/devtools/src/` owns the Workbench grid, banner, navigation, Plugins strip, Marketplace, whole-panel separator, and Devtools-owned plugin frames. +- `packages/devtools-a11y/src/core/` consumes `/internal` semantic roles while retaining audit/config/export behavior. +- Plugin content continues to receive only the existing `{ theme, devtoolsOpen }` props. No semantic theme object crosses the plugin boundary. + +## Semantic theme + +### Surfaces, text, borders, and controls + +All listed pairs are minimum verified contrast pairs for the named use. Text may be placed only on the backgrounds listed in its row. + +| Role | Light | Dark | Allowed pairing and ratio | +|---|---|---|---| +| `surface.app` | `#ffffff` | `#111111` | panel root | +| `surface.workspace` | `#ffffff` | `#1f1f1f` | pane content | +| `surface.subtle` | `#fafafa` | `#1b1b1b` | secondary region | +| `surface.elevated` | `#ffffff` | `#2b2b2b` | popover/dialog | +| `surface.brand` | `#eeebd4` | `#111111` | banner/brand-only region | +| `text.primary` | `#111111` | `#ffffff` | app/ workspace: at least 16.48:1 | +| `text.secondary` | `#3e3529` | `#aea691` | workspace: 12.03:1 / 6.80:1 | +| `text.muted` | `#756c5b` | `#aea691` | subtle/workspace: 4.96:1 / 6.80:1 | +| `text.mutedOnBrand` | `#3e3529` | `#aea691` | brand: 10.00:1 / 7.79:1 | +| `text.inverse` | `#ffffff` | `#111111` | inverse/control fill | +| `text.link` | `#003e53` | `#9cd5e2` | workspace: 11.58:1 / 10.23:1 | +| `border.decorative` | `#eeebd4` | `#2d2d2d` | non-informational division only | +| `border.control` | `#756c5b` | `#aea691` | workspace: 5.18:1 / 6.80:1 | +| `border.focus` | `#003e53` | `#61adbf` | adjacent app surface: 11.58:1 / 7.41:1 | +| `state.hover` | `#1111110f` | `#ffffff14` | overlay plus non-color cue | +| `state.pressed` | `#1111111f` | `#ffffff1f` | overlay plus non-color cue | +| `selection.fill` | `#3e3529` | `#c5c3bf` | white text 12.03:1 / `#111111` text 10.73:1 | + +`border.decorative` may be below 3:1 only when it carries no control boundary, state, focus, or separation needed to understand content. Inputs, checkboxes, buttons without a fill, whole-panel resize separator, selected indicators, and error boundaries use `border.control`, focus, or status borders and meet 3:1. Focus is a 2px outline with a 2px offset and cannot be clipped. + +Do not use `#756c5b` on cream; its ratio is 4.31:1. Muted banner copy uses `text.mutedOnBrand`. Blue is limited to focus, links, and information. Tooling selection uses charcoal/tint. Green, amber, and terracotta indicate status only. + +Light neutral and blue text ratios are recorded separately by surface: + +| Foreground | On `#ffffff` | On `#fafafa` | +|---|---:|---:| +| `#3e3529` | 12.03:1 | 11.52:1 | +| `#003e53` | 11.58:1 | 11.09:1 | + +### Status roles + +Five roles cover component feedback and accessibility summaries. Each subtle treatment uses all three listed values. The contrast ratio is text against background; borders are also tested against the adjacent background. + +| Role | Light background | Light border | Light text | Text / border ratio | +|---|---|---|---|---:| +| success | `#d8f0da` | `#1d4226` | `#1d4226` | 9.36:1 / 9.36:1 | +| warning | `#fef6cc` | `#624a00` | `#624a00` | 7.71:1 / 7.71:1 | +| error | `#f9d8c4` | `#5f1a06` | `#5f1a06` | 9.56:1 / 9.56:1 | +| info | `#d8f0f3` | `#003e53` | `#003e53` | 9.75:1 / 9.75:1 | +| neutral | `#eeebd4` | `#756c5b` | `#3e3529` | 10.00:1 / 4.31:1 | + +| Role | Dark subtle background | Dark border | Dark text | Text / border ratio | +|---|---|---|---|---:| +| success | `#1d4226` | `#69bc75` | `#a2e1a9` | 7.46:1 / 4.87:1 | +| warning | `#624a00` | `#f4d648` | `#fae884` | 6.78:1 / 5.83:1 | +| error | `#5f1a06` | `#e06e49` | `#edaa8d` | 6.56:1 / 3.97:1 | +| info | `#003e53` | `#61adbf` | `#9cd5e2` | 7.18:1 / 4.54:1 | +| neutral | `#2b2b2b` | `#aea691` | `#c5c3bf` | 8.05:1 / 5.84:1 | + +Each status role also defines a solid treatment. `solidFill` is the control/badge background and `onFill` is its only text/icon foreground: + +| Role | Light `solidFill` | Light `onFill` | Ratio | Dark `solidFill` | Dark `onFill` | Ratio | +|---|---|---|---:|---|---|---:| +| success | `#1d4226` | `#ffffff` | 11.29:1 | `#69bc75` | `#111111` | 8.14:1 | +| warning | `#624a00` | `#ffffff` | 8.40:1 | `#f4d648` | `#111111` | 13.09:1 | +| error | `#5f1a06` | `#ffffff` | 12.83:1 | `#e06e49` | `#111111` | 5.84:1 | +| info | `#003e53` | `#ffffff` | 11.58:1 | `#61adbf` | `#111111` | 7.41:1 | +| neutral | `#3e3529` | `#ffffff` | 12.03:1 | `#c5c3bf` | `#111111` | 10.73:1 | + +Status on an untinted white/`#fafafa` surface uses light text `#1d4226`, `#624a00`, `#5f1a06`, `#003e53`, or `#3e3529`; each is at least 8.05:1 on `#fafafa`. Status on an untinted `#1f1f1f` surface uses dark text `#69bc75`, `#f4d648`, `#e06e49`, `#9cd5e2`, or `#c5c3bf`; each is at least 5.10:1. Every status also includes an icon and text label. A colored dot alone is insufficient. + +### Syntax roles + +Code surfaces are `#fafafa` in light mode and `#1f1f1f` in dark mode. Use these exact foregrounds: + +| Syntax role | Light | Ratio | Dark | Ratio | +|---|---|---:|---|---:| +| keyword | `#5f1a06` | 12.29:1 | `#e06e49` | 5.10:1 | +| string | `#1d4226` | 10.82:1 | `#69bc75` | 7.10:1 | +| number/constant | `#541f5d` | 11.62:1 | `#c56dcf` | 5.07:1 | +| comment | `#756c5b` | 4.96:1 | `#aea691` | 6.80:1 | +| property/link | `#003e53` | 11.09:1 | `#61adbf` | 6.47:1 | +| punctuation/plain | `#3e3529` | 11.52:1 | `#c5c3bf` | 9.37:1 | +| selection | `#003e53` on `#d8f0f3` | 9.75:1 | `#ffffff` on `#003e53` | 11.58:1 | + +The contrast tests label white and `#fafafa` cases separately; a passing white pair is not assumed to pass on `#fafafa`. `::selection` switches every syntax token to the selection foreground shown above so a token's original foreground never remains on the selection background. + +### Type, spacing, radius, depth, and motion + +- Bundle exactly `BricolageGrotesque-Bold.ttf` and the variable `Inter-latin.woff2` under `packages/devtools-ui/src/assets/fonts/`. Add `OFL-Bricolage-Grotesque.txt` and `OFL-Inter.txt` beside them. Do not bundle a mono font. +- Use Bricolage only for the banner title, pane titles, first-level empty-state headings, and numeric audit totals. Use Inter elsewhere. Code uses the existing system monospace stack. +- UI sizes are 12/17, 14/20, and 16/24px. Labels are 12/14px at weight 500 and 0.5px tracking. Body is weight 300 at 16px and weight 400 at 14px and 12px. +- Spacing uses 4px increments. Compact controls use 4 or 8px gaps and 6px vertical/8px horizontal padding. Section gaps are 8, 12, or 16px. +- Radius is 4px for controls, 6px for grouped surfaces, and 8px for dialogs/popovers. +- Daily shadows are exactly `0 1px 2px rgba(0,0,0,0.03)` or `0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03)`. A modal/popover may use `0 25px 50px -12px rgba(0,0,0,0.20)`. +- Plugins-strip CSS interpolation is 120ms ease-out. The existing drawer leave grace period remains 400ms. `prefers-reduced-motion: reduce` sets CSS interpolation duration to 0ms but retains the 400ms leave grace period to prevent accidental collapse. + +Register these exact faces once per target document: + +```css +@font-face { + font-family: 'Bricolage Grotesque'; + src: url() format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'Inter'; + src: url() format('woff2'); + font-style: normal; + font-weight: 100 900; + font-display: swap; +} +``` + +`semantic-theme.ts` imports both asset URLs so Vite emits them. An idempotent `/internal` helper inserts a style element with a fixed ID into the active `Document`. `ThemeContextProvider` installs it in its owner document; `PiPProvider` installs it again after clearing/copying the popup head so PiP does not depend on opener timing. On unmount, a shared document-level registration remains usable by another Devtools instance. If either request fails, Bricolage falls back to `ui-sans-serif, system-ui, sans-serif`, Inter falls back to `ui-sans-serif, system-ui, sans-serif`, and code retains the existing system monospace stack. + +The Bricolage license is copied with its bundled source asset. Before adding `OFL-Inter.txt`, retrieve `LICENSE.txt` from the official `rsms/inter` repository revision that supplied `Inter-latin.woff2`, verify that it declares SIL Open Font License 1.1 and the Inter Project Authors, record that revision and both SHA-256 hashes in the implementation PR, then copy it without modification. If provenance or license verification fails, do not ship the Inter binary and use the system-sans fallback. + +The two font binaries must total at most 160 KiB uncompressed and no third font binary may be emitted. Add a package-build assertion for emitted font count and total bytes; retain the existing 60 KB core JavaScript size limit through `pnpm size`. + +## Concrete component coverage + +Shared UI coverage is `Button` (`components/button.tsx`), `Checkbox` (`checkbox.tsx`), `Header`/`HeaderLogo` (`header.tsx`), `Input` (`input.tsx`), `TanStackLogo` (`logo.tsx`), `MainPanel` (`main-panel.tsx`), `Section`/`SectionTitle`/`SectionDescription`/`SectionIcon` (`section.tsx`), `Select` (`select.tsx`), `Tag` (`tag.tsx`), `ThemeContextProvider` (`theme.tsx`), `JsonTree` (`tree.tsx`), and the icons exported by `packages/devtools-ui/src/icons.ts`. + +Core coverage is `MainPanel` (`packages/devtools/src/components/main-panel.tsx`), `ContentPanel` (`content-panel.tsx`), `Tabs`/the replacement Workbench navigation (`tabs.tsx`), `TabContent` (`tab-content.tsx`), `Trigger` (`trigger.tsx`), `SourceInspector` (`source-inspector.tsx`), `SettingsTab` (`tabs/settings-tab.tsx`), `HotkeyConfig` (`tabs/hotkey-config.tsx`), `PluginsTab` (`tabs/plugins-tab.tsx`), `PluginMarketplace` (`tabs/plugin-marketplace.tsx`), all components in `tabs/marketplace/`, and repository-owned wrappers in `tabs/seo-tab/`. + +Accessibility coverage is `Shell`, `Settings`, `IssueList`, and `IssueCard` in `packages/devtools-a11y/src/core/components/`, `packages/devtools-a11y/src/core/styles/styles.ts`, and the severity highlight/tooltip styles created by `packages/devtools-a11y/src/core/utils/ui.utils.ts`. + +Accessibility severity mapping is explicit: critical uses error with a 3px solid outline, serious uses error with a 2px solid outline, moderate uses warning with a 2px solid outline, and minor uses info with a 2px dashed outline. Each issue and tooltip includes the written severity label, so critical and serious do not depend on outline thickness alone. + +Repository-owned hover, focus, pressed, selected, disabled, loading, empty, validation, error, severity, scrollbar, dialog, popover, and source-inspector states in those files use semantic roles. PackageJsonPanel and all external plugin descendants are excluded. SEO simulated result text/link colors and vendor assets use the exemption above. + +## Workbench geometry and constrained height + +The core open panel uses: + +```css +grid-template-rows: 36px var(--plugins-strip-height, 32px) minmax(0, 1fr); +``` + +- Row 1 is a fixed 36px banner. It contains the emblem and “TanStack Devtools” on the left; explicit “Plugins” and “SEO” destination controls; and Settings, Detach, and Close controls on the right. Plugins, SEO, and Settings expose the current destination with `aria-current="page"`. Controls are 28×28px or wider and have accessible names. +- Row 2 is a single-line horizontal Plugins strip. It is 32px idle and 48px while hovered, `:focus-within`, Marketplace-open, or zero-active-plugin forced expansion applies. It never wraps and uses horizontal scrolling when content exceeds width. +- Row 3 contains zero to three equal-width plugin panes or the selected core view. Active plugin panes use `display:flex`; each mount is `flex: 1 1 0; min-width: 0`. Adjacent panes have a static 1px `border.decorative` separator. There are no pane resize handles. + +The 32→48px strip grows downward and its tab content translates vertically within the 16px change, matching the current drawer's reveal concept on the new axis. Pointer leave schedules contraction after the existing 400ms grace period. Focus leaving the strip uses the same grace period. Focus never moves during contraction. + +At widths `<=360px`, hide the “TanStack Devtools” text and retain the emblem with accessible name “TanStack Devtools”. The strip remains one row. At whole-panel heights 84px and above, all three grid rows operate normally. At 70–83px, compact-height mode fixes the banner at 36px and strip at 32px, disables the 48px expansion, and leaves `minmax(0,1fr)` for any remaining content; all banner and strip controls remain operable. Dragging the existing whole-panel separator to a computed height `<70px` keeps the existing result: set `isOpen` false. The stored height, close threshold, and `persistOpen` semantics are not changed. + +PiP uses `100vh`, displays the same grid, and omits Detach and Close as it does today. As an intentional compatibility exception, PiP also omits the resize separator: popup height is window-owned, and the current handle can only mutate the hidden future attached height. Detaching snapshots the stored attached height without changing it; closing PiP/reattaching restores that exact height. The trigger is not interactive in PiP. The component is always described as a **horizontal Plugins strip with vertical expansion**; “vertical strip” is not used. + +## Whole-panel resize separator + +Preserve the current left-button pointer drag calculation for top/bottom placement in `packages/devtools/src/devtools.tsx`. Add keyboard semantics to the same drag handle: + +- `role="separator"`, `aria-orientation="horizontal"`, `tabIndex="0"`, and accessible name “Resize TanStack Devtools panel”. +- The separator exists only while attached. Its maximum is `Math.floor(window.innerHeight * 0.9)`. `aria-valuemax` is that integer, `aria-valuenow` is the stored height clamped to `[70, aria-valuemax]`, and `aria-valuemin="70"`. +- For a bottom-attached panel, ArrowUp increases height by 10px and ArrowDown decreases it by 10px. For a top-attached panel, ArrowDown increases height by 10px and ArrowUp decreases it by 10px. Shift changes the step to 50px. This matches the physical separator direction. +- Home sets 70px. End sets the computed maximum above. Every keyboard change uses the same `setHeight` and `<70px` close paths as pointer resizing. PiP has no pointer or keyboard resize path. +- The separator has a visible focus outline and a control-boundary contrast of at least 3:1. + +## Navigation and state transitions + +The strip is rendered independently of the selected core view so plugin choices remain available from SEO and Settings. The following transitions are normative: + +| Action/state | State change | Rendered result and lifecycle | +|---|---|---| +| Select SEO | `activeTab = 'seo'`; `activePlugins` unchanged; Marketplace closed | SEO view replaces plugin mounts; no plugin `destroy` call | +| Select Settings | `activeTab = 'settings'`; `activePlugins` unchanged; Marketplace closed | Settings view replaces plugin mounts; no plugin `destroy` call | +| Select banner Plugins destination | `activeTab = 'plugins'`; Marketplace closed; `activePlugins` unchanged | Existing active mounts return in activation order; the Plugins control has `aria-current="page"` | +| Select Marketplace | `activeTab = 'plugins'`; Marketplace open; `activePlugins` unchanged; strip forced to 48px except compact-height mode | Marketplace replaces plugin mounts; no plugin `destroy` call | +| Select inactive plugin | `activeTab = 'plugins'`; Marketplace closed; append its ID when fewer than three are active | New mount is appended; all panes become equal width; `render` receives current props | +| Select active plugin | `activeTab = 'plugins'`; Marketplace closed; remove its ID | Call `destroy(pluginId)` once before removing its mount; remaining panes become equal width | +| Select a fourth inactive plugin from Plugins, SEO, or Settings | `activeTab = 'plugins'`; Marketplace closed; `activePlugins` unchanged | The existing three mounts render in activation order; no `render`/`destroy` call is added for the rejected plugin | +| Zero registered plugins | `plugins = []`; `activeTab = 'plugins'` | Render the existing Marketplace fallback; strip contains the Marketplace destination and does not render plugin controls | +| Registered plugins with zero active | `plugins.length > 0`; `activePlugins = []`; `activeTab = 'plugins'` | Render the zero-active workspace state; strip forced to 48px, or 32px at 70–83px height, so registered plugins remain selectable | +| Three active plugins | Three IDs in activation order | Three equal-width mounts with two static 1px separators; no resize controls | +| Close whole panel or press Escape | Existing `isOpen`/`persistOpen` toggle | Plugin instances are not destroyed; active `render` calls receive `devtoolsOpen: false` through the existing effect | +| Theme change | Existing `theme` value changes | Active plugin `render` and custom name callbacks receive the new `light | dark` value | +| Enter PiP | Existing PiP request and window state; stored attached height unchanged | Same active/core view; panel uses `100vh`; Detach, Close, and resize separator omitted; reattachment restores the exact pre-detach attached height | + +## Plugin compatibility contract + +- Preserve `PLUGIN_CONTAINER_ID = 'plugin-container'` and `PLUGIN_TITLE_CONTAINER_ID = 'plugin-title-container'`. Mount and title IDs remain `${PLUGIN_CONTAINER_ID}-${plugin.id}` and `${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`. +- Preserve explicit IDs and generated ID behavior, registration order in the strip, activation order in `activePlugins`, `MAX_ACTIVE_PLUGINS = 3`, default-open behavior, and stored ID filtering. +- Preserve `name: string | ((el: HTMLHeadingElement, props) => void)`. The function still receives the same live `HTMLHeadingElement`, theme, and `devtoolsOpen` values. +- Preserve `render(el: HTMLDivElement, props)` calls when an active mount exists and reactive theme/open inputs change. Moving between Plugins, Marketplace, SEO, or Settings may unmount/remount DOM but does not call plugin `destroy`. +- Preserve `destroy(pluginId)` only when an active plugin is toggled inactive. Whole-panel close, core-view navigation, Marketplace navigation, and PiP do not add destroy calls. +- Do not apply core typography, colors, or descendant selectors inside plugin mount elements. Devtools owns only mount sizing, background frame, title control, and inter-pane separator. +- Replace each click-only plugin title container with a focusable wrapper using `role="button"`, `tabIndex="0"`, `aria-labelledby` pointing to the existing live `h3` ID, and `aria-pressed` for active state. Click, Enter, and Space invoke the same toggle; Space prevents page scrolling. Keep the live `h3` so the custom `HTMLHeadingElement` callback is unchanged. A string name receives a core-owned text class. A callback-owned `h3` and all of its descendants receive no core font family, size, weight, text-transform, or foreground-color rule; only the wrapper's layout, background, border, and focus state are core-owned. + +## Accessibility and forced colors + +- Normal text is at least 4.5:1; large text and required non-text boundaries are at least 3:1. Tests use the exact pair tables above. +- Selected/open/severity/validation states use text or an accessible name plus an icon/border; color is not the only cue. +- Hover and `:focus-within` expand the Plugins strip. Horizontal overflow is operable with keyboard focus and browser scrolling. +- Icon buttons are actual buttons with names: “SEO”, “Settings”, “Detach TanStack Devtools”, “Close TanStack Devtools”, and plugin-derived names. New names are asserted; the implementation does not assume the current icon-only controls already have names. +- At 200% browser zoom, banner controls remain reachable, the strip scrolls, and plugin/core content scrolls without page-level horizontal overflow. +- Under `@media (forced-colors: active)`, surfaces use `Canvas`/`CanvasText`, controls use `ButtonFace`/`ButtonText`, and selected plugins use `Highlight`/`HighlightText`. Retain `forced-color-adjust: auto` on controls. Focused controls, inputs, checkboxes, pane separators, and the attached-only resize separator retain a `ButtonText` or `CanvasText` border at least 1 CSS px wide. The emblem uses `currentColor` or remains hidden behind the accessible brand name; no state depends on raster/SVG color. + +## React basic accessibility fixture + +Add `@tanstack/devtools-a11y: workspace:*` to `examples/react/basic/package.json`. In `examples/react/basic/src/setup.tsx`, use the exact development API: + +```ts +import { a11yDevtoolsPlugin } from '@tanstack/devtools-a11y/react' + +plugins={[a11yDevtoolsPlugin(), /* existing plugins */]} +``` + +Add `examples/react/basic/src/a11y-audit-fixture.tsx` exporting `A11yAuditFixture`. Render it only when `import.meta.env.DEV` is true. The fixture contains an inline-data image without `alt` (`image-alt`), an empty button (`button-name`), and an input without a label (`label`), each with a fixed test ID. Do not randomize content, fetch data, or use timers. Browser audit assertions require those three axe rule IDs and may allow additional host/browser findings. + +The React entry returns a no-op accessibility plugin outside development. Therefore accessibility-plugin browser verification must run against the one-shot Vite development server from `pnpm --filter @tanstack/devtools-example-react-basic dev`, not `vite preview` or a production build. Start it natively in Windows, await the browser checks, and terminate the complete process tree. + +Query, Router, duplicate Router, and Package.json panels remain useful multi-pane fixtures but are not recolored. + +## Migration sequence + +1. Add `semantic-theme.ts`, `internal.ts`, font assets/licenses, Vite entry, package export, and semantic/contrast tests in `packages/devtools-ui` without removing legacy token keys. +2. Migrate the concrete shared components to semantic roles. +3. Migrate core-owned styles while retaining the current drawer geometry. +4. Migrate accessibility-owned components/styles without changing audit output. +5. Add `a11yDevtoolsPlugin()` and `A11yAuditFixture` to the React basic development example. +6. Implement the 36/32/minmax Workbench grid, banner navigation, horizontal strip, and additive resize-separator keyboard behavior. +7. Remove only unused core literals/raw ramps. Retain shared legacy raw keys and exempt external/vendor/simulated SEO colors. +8. Apply the documentation impact below. No public theme API documentation is added. + +## Documentation Impact + +- Update `docs/architecture.md` in its Core Layer description and diagram labels: document the 36px banner, explicit Plugins/SEO/Settings destinations, horizontal Plugins strip with vertical expansion, equal-width multi-plugin mounts, and private `@tanstack/devtools-ui/internal` dependency. This serves contributors tracing shell ownership and plugin mounting. +- Update `docs/overview.md` in Core and Key Features: describe the banner navigation, horizontal Plugins strip, simultaneous plugin panes, Marketplace destination, and light/dark TanStack styling. This serves evaluators learning what the shell exposes. +- Do not change `docs/plugins/a11y.md`: the package install, exact `a11yDevtoolsPlugin()` call, audit behavior, severity model, and public configuration remain unchanged. The deterministic basic-example fixture is test-only example content, not an accessibility-plugin user workflow. A visual-theme-only edit would add no user action to this page. +- Update `examples/react/basic/README.md`: use the repository's pnpm filter command, state that browser audit testing requires Vite development mode, identify `A11yAuditFixture` as intentionally invalid development-only content, and list the three expected axe rule IDs. This serves contributors reproducing the browser matrix. + +## Test Strategy + +Tests are grouped by unit, integration, and browser scope. Each category includes happy, edge, and failure coverage. Existing test paths are extended instead of creating a second test framework. + +### Unit + +| Case | Path | Assertions | +|---|---|---| +| Happy | New `packages/devtools-ui/tests/semantic-theme.test.ts` | Exact role values, light/dark resolution, 120ms motion, raw `tokens.colors`/`Tag` key retention, `/internal` types and exports, two emitted font files, and <=160 KiB font total | +| Happy | New `packages/devtools-ui/tests/contrast.test.ts` | Every white and `#fafafa` text case separately, including the exact `#3e3529` and `#003e53` ratios; dark workspace; all five subtle and `solidFill`/`onFill` roles in both modes; syntax; selection foreground switching; focus; and control borders | +| Edge | Existing `packages/devtools-ui/tests/index.test.ts` and `tree.tsx` | All exported component states, string/custom plugin-name typography boundary helpers, theme propagation, disabled/focus states, and system-monospace retention | +| Failure | New `packages/devtools-ui/tests/fonts.test.ts` | Missing font request leaves declared system-sans fallbacks, duplicate registration is idempotent, popup registration targets the popup document, and license files/provenance metadata exist | +| Happy | Existing `packages/devtools-a11y/tests/index.test.ts` and `export.test.ts` | Semantic theme consumption and unchanged React/Solid/Angular plugin exports | +| Edge/error | Existing `packages/devtools-a11y/tests/config.test.ts` | Four audit severities map to labeled semantic status treatments; invalid/missing persisted config retains current fallback behavior | + +### Integration + +| Case | Path | Assertions | +|---|---|---| +| Happy | New `packages/devtools/tests/workbench.test.tsx` | Every normative navigation row; explicit Plugins destination; 1–3 equal-width mounts; static separators; Settings/SEO/Marketplace/PiP; live IDs and callback heading | +| Edge | `packages/devtools/tests/workbench.test.tsx` | Zero registered versus registered-zero-active; fourth inactive selection from Plugins/SEO/Settings; 32/48px strip; 400ms grace; focus leave/re-entry; 69/70/83/84px; `<=360px`; top/bottom placement | +| Failure | `packages/devtools/tests/workbench.test.tsx` | Rejected fourth plugin leaves three IDs/order; blocked `window.open` preserves current thrown error; callback/render exceptions retain current fail-loud behavior; no extra destroy calls; timers clean up on unmount | +| Happy | Existing `packages/devtools/tests/index.test.ts` | Default/custom trigger, URL flag, open hotkey, Escape, source inspector, theme, close/reopen persistence, pointer whole-panel resize, and additive keyboard separator | +| Edge/error | Existing `packages/devtools/tests/index.test.ts` | Malformed storage fallback, storage write failure, `<70px` close, 90%-viewport attached maximum, separator omission in PiP, exact pre-detach height after reattachment, mount aborted before dynamic import, and existing repeated mount/unmount errors | +| Happy | Existing `packages/devtools/src/context/devtools-context.test.ts` | Explicit/generated IDs, registration and activation order, default-open, stored ID filtering, reactive plugin replacement, mount/remount, render input changes, and destroy-only-on-toggle | +| Failure | Existing `packages/devtools/src/context/devtools-context.test.ts` | Removed plugin IDs are filtered, duplicate/generated IDs retain existing behavior, malformed stored state falls back through the existing parser, and a plugin replacement cannot reorder retained active IDs | +| Happy/error | Existing `packages/devtools-a11y/tests/index.test.ts` plus example type/build | Exact React factory call works in development; production remains no-op; deterministic rule IDs are declared without timers/network | + +### Browser + +Use `pnpm --filter @tanstack/devtools-example-react-basic dev` and browser automation. Before every independent scenario, run the following in the page, then reload and wait for the app and Devtools event bus: + +```js +localStorage.removeItem('tanstack_devtools_settings') +localStorage.removeItem('tanstack_devtools_state') +localStorage.removeItem('pip_open') +localStorage.removeItem('tanstack-devtools-a11y-config') +location.reload() +``` + +| Case | Matrix and assertions | +|---|---| +| Happy | Light and dark at 1280×720: trigger/reveal, explicit Plugins/SEO/Settings destinations, Marketplace, one and three active plugins, equal widths/static separators, A11y audit with `image-alt`/`button-name`/`label`, Detach/PiP with no resize separator, exact pre-detach height after return, close/reopen, and zero console errors | +| Edge | Light and dark at 360×480 and 320×400; panel heights 70, 83, 84, and 400px; 200% zoom; keyboard-only strip overflow; 400ms focus/pointer grace; reduced motion; top/bottom panel placement; three panes; owner colors unchanged in Query/Router/PackageJsonPanel | +| Failure | 69px pointer/keyboard resize closes the attached panel; a blocked popup follows the current error path without changing active state; failed font requests render system-sans controls without clipping; production build exposes the no-op accessibility plugin; native expanded select remains platform-styled | +| Forced colors | With `forced-colors: active`, create hidden reference elements authored with each required system-color pair. Compare computed surface, control, and selected-plugin colors to the corresponding reference element, because browsers may resolve keywords to RGB values. If the automation environment cannot expose forced-color computed values, assert the authored forced-colors CSS plus keyboard/state behavior. Never compare computed values to literal keyword strings. Also assert that text/state remains present when authored colors/background images are suppressed and that focused controls, inputs, checkboxes, static pane separators, and the attached resize separator have a system-color border at least 1 CSS px wide. | + +Run focused tests first. Then run Prettier check, affected ESLint, typecheck, package builds, publint, example build, `pnpm test:docs`, font asset-count/byte check, and `pnpm size`. The final repository gate is the root command `pnpm run test`. All commands are native Windows one-shots; no watch, Vite, Node, esbuild, or Vitest process remains afterward. + +## Risks and controls + +- Mixed raw/semantic styles: search only the in-scope concrete files and fail review on non-exempt color literals. +- Plugin selector bleed: prohibit descendant typography/color rules below plugin mount IDs and compare external plugin screenshots before/after. +- Font size increase: include only listed weights/formats and enforce `pnpm size` plus package build output review. +- Height regression: test exact 69/70/83/84px boundaries and both top/bottom placement. +- Lifecycle regression: assert callback counts and DOM IDs across every navigation row. +- Development-only a11y behavior: require Vite development mode for audit browser tests and separately verify the production build remains no-op. + +## Acceptance criteria + +- [ ] `@tanstack/devtools-ui/internal` is emitted, typed, publint-valid, and not re-exported from the package root. +- [ ] Core and accessibility resolve the exact semantic tables through `/internal`; all legacy `tokens.colors` and `Tag` color keys remain. +- [ ] Exactly `BricolageGrotesque-Bold.ttf` and `Inter-latin.woff2` plus the two verified OFL files are packaged; both font faces use `font-display: swap`, opener and PiP registration passes, fallback rendering passes, binary count is two, and raw binary total is <=160 KiB. +- [ ] `#3e3529` measures 12.03:1 on white and 11.52:1 on `#fafafa`; `#003e53` measures 11.58:1 on white and 11.09:1 on `#fafafa`; all five subtle and `solidFill`/`onFill` roles in both modes, syntax selection foregrounds, borders, focus, and muted-on-brand roles pass automated checks. +- [ ] Shared components, named core files, and named accessibility files have no non-exempt raw color literals after migration. +- [ ] External plugin descendants, PackageJsonPanel, vendor marks, and simulated SEO colors retain owner/source styling. +- [ ] Grid rows are 36px, 32/48px, and `minmax(0,1fr)`; CSS motion is 120ms and leave grace is 400ms. +- [ ] Heights 70–83px use 36/32px compact mode; 84px enables expansion; a drag result below 70px closes the whole panel with existing persistence semantics. +- [ ] At widths `<=360px`, the visible wordmark is removed and the accessible “TanStack Devtools” name remains. +- [ ] Zero to three plugins follow the navigation table, render at equal width with static 1px separators, and expose no per-pane resizing. +- [ ] Zero registered plugins renders Marketplace; registered-zero-active renders its separate workspace state and forces strip expansion outside compact-height mode. +- [ ] Selecting a fourth inactive plugin from Plugins, SEO, or Settings enters Plugins, closes Marketplace, retains the same three IDs/order, and renders the same three mounts. IDs, callback element type, render/destroy behavior, mount behavior, and `MAX_ACTIVE_PLUGINS` remain compatible. +- [ ] Explicit Plugins, SEO, Settings, Marketplace, close, theme, and PiP transitions match the normative table; Plugins/SEO/Settings expose `aria-current="page"`. +- [ ] Each plugin control is a `role="button"`/`tabIndex="0"` wrapper with Enter/Space, `aria-labelledby`, and `aria-pressed`; callback-owned heading subtrees have no core typography or foreground override. +- [ ] Attached whole-panel pointer resizing is unchanged; keyboard directions match top/bottom attachment and max/now/End use 90% attached height. PiP has no resize separator, stays `100vh`, and reattachment restores the exact pre-detach stored height. +- [ ] Forced-colors tests compare computed values with hidden system-color reference elements or verify authored CSS plus behavior; reduced-motion, keyboard-only, 200% zoom, horizontal overflow, focus, font-failure, and blocked-popup tests pass. +- [ ] React basic calls `a11yDevtoolsPlugin()` in development and the deterministic fixture yields `image-alt`, `button-name`, and `label`. +- [ ] Each browser scenario clears `tanstack_devtools_settings`, `tanstack_devtools_state`, `pip_open`, and `tanstack-devtools-a11y-config`, reloads, and then begins assertions. +- [ ] `docs/architecture.md`, `docs/overview.md`, and `examples/react/basic/README.md` contain the specified updates; `docs/plugins/a11y.md` remains unchanged for the recorded rationale. +- [ ] Unit, integration, browser, formatting, ESLint, typecheck, build, publint, docs links, asset-size, `pnpm size`, and final root `pnpm run test` checks pass with no remaining test/server processes. From 0bf4bf2e028f4eff44051c7af24afde71da497b8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 3 Aug 2026 19:38:39 +0200 Subject: [PATCH 3/5] feat: redesign TanStack Devtools workbench --- .changeset/tanstack-devtools-branding.md | 7 + docs/architecture.md | 18 +- docs/overview.md | 7 +- examples/react/basic/README.md | 17 +- examples/react/basic/package.json | 2 + .../basic/scripts/check-a11y-fixture.mjs | 52 + .../react/basic/src/a11y-audit-fixture.tsx | 14 + examples/react/basic/src/example.css | 248 ++++ examples/react/basic/src/index.tsx | 71 +- examples/react/basic/src/setup.tsx | 6 +- examples/react/basic/src/vite-env.d.ts | 1 + .../src/core/components/IssueCard.tsx | 42 +- .../src/core/components/IssueList.tsx | 11 +- .../src/core/components/Settings.tsx | 14 +- .../src/core/components/Shell.tsx | 13 +- .../src/core/contexts/allyContext.tsx | 6 +- .../src/core/styles/severity-theme.ts | 26 + .../devtools-a11y/src/core/styles/styles.ts | 458 +++---- .../devtools-a11y/src/core/utils/ui.utils.ts | 100 +- packages/devtools-a11y/tests/index.test.ts | 28 +- packages/devtools-a11y/tests/theme.test.ts | 215 ++++ packages/devtools-a11y/vite.config.ts | 11 +- packages/devtools-ui/package.json | 13 +- .../devtools-ui/scripts/check-font-assets.mjs | 317 +++++ .../assets/fonts/BricolageGrotesque-Bold.ttf | Bin 0 -> 82204 bytes .../src/assets/fonts/Inter-latin.woff2 | Bin 0 -> 72920 bytes .../assets/fonts/OFL-Bricolage-Grotesque.txt | 93 ++ .../src/assets/fonts/OFL-Inter.txt | 92 ++ .../devtools-ui/src/components/button.tsx | 2 +- .../devtools-ui/src/components/checkbox.tsx | 8 +- .../devtools-ui/src/components/header.tsx | 8 +- packages/devtools-ui/src/components/icons.tsx | 10 +- packages/devtools-ui/src/components/input.tsx | 15 +- packages/devtools-ui/src/components/logo.tsx | 1 + .../devtools-ui/src/components/main-panel.tsx | 1 + .../devtools-ui/src/components/section.tsx | 12 +- .../devtools-ui/src/components/select.tsx | 15 +- packages/devtools-ui/src/components/tag.tsx | 6 +- packages/devtools-ui/src/components/theme.tsx | 10 +- packages/devtools-ui/src/components/tree.tsx | 113 +- packages/devtools-ui/src/internal.ts | 11 + .../devtools-ui/src/styles/semantic-theme.ts | 345 +++++ packages/devtools-ui/src/styles/use-styles.ts | 589 +++++---- packages/devtools-ui/tests/contrast.test.ts | 82 ++ packages/devtools-ui/tests/fonts.test.ts | 96 ++ packages/devtools-ui/tests/index.test.ts | 357 +++++- .../devtools-ui/tests/semantic-theme.test.ts | 274 ++++ packages/devtools-ui/tests/tree.tsx | 80 +- packages/devtools-ui/tsconfig.json | 1 + packages/devtools-ui/vite.config.ts | 10 +- .../devtools/src/components/content-panel.tsx | 62 +- .../devtools/src/components/main-panel.tsx | 113 +- .../devtools/src/components/plugins-strip.tsx | 93 ++ .../src/components/source-inspector.tsx | 4 +- .../devtools/src/components/tab-content.tsx | 26 +- packages/devtools/src/components/tabs.tsx | 72 -- packages/devtools/src/components/trigger.tsx | 1 + .../src/components/workbench-header.tsx | 122 ++ .../components/workbench-secondary-tabs.tsx | 57 + .../src/context/devtools-context.test.ts | 73 +- .../devtools/src/context/devtools-context.tsx | 15 +- .../devtools/src/context/draw-context.tsx | 69 - packages/devtools/src/context/pip-context.tsx | 2 + .../src/context/use-devtools-context.ts | 54 +- packages/devtools/src/devtools.tsx | 174 +-- packages/devtools/src/styles/tokens.ts | 305 ----- packages/devtools/src/styles/use-styles.ts | 818 ++++++++---- packages/devtools/src/tabs/index.tsx | 4 - .../tabs/marketplace/marketplace-header.tsx | 10 +- .../src/tabs/marketplace/plugin-card.tsx | 3 +- .../src/tabs/marketplace/plugin-section.tsx | 79 +- .../src/tabs/marketplace/settings-panel.tsx | 10 +- .../src/tabs/marketplace/tag-filters.tsx | 4 + .../devtools/src/tabs/plugin-marketplace.tsx | 6 +- packages/devtools/src/tabs/plugins-tab.tsx | 177 +-- packages/devtools/src/tabs/seo-tab/index.tsx | 46 +- .../src/tabs/seo-tab/serp-preview.tsx | 11 +- .../src/tabs/seo-tab/social-previews.tsx | 25 +- packages/devtools/src/tabs/settings-tab.tsx | 238 +--- packages/devtools/src/utils/constants.ts | 5 + packages/devtools/src/utils/storage.ts | 10 +- packages/devtools/tests/index.test.ts | 159 ++- .../tests/semantic-color-usage.test.ts | 94 ++ .../devtools/tests/seo-workbench.test.tsx | 102 ++ .../tests/workbench-secondary-tabs.test.tsx | 169 +++ packages/devtools/tests/workbench.test.tsx | 1124 +++++++++++++++++ pnpm-lock.yaml | 3 + 87 files changed, 6297 insertions(+), 1970 deletions(-) create mode 100644 .changeset/tanstack-devtools-branding.md create mode 100644 examples/react/basic/scripts/check-a11y-fixture.mjs create mode 100644 examples/react/basic/src/a11y-audit-fixture.tsx create mode 100644 examples/react/basic/src/example.css create mode 100644 examples/react/basic/src/vite-env.d.ts create mode 100644 packages/devtools-a11y/src/core/styles/severity-theme.ts create mode 100644 packages/devtools-a11y/tests/theme.test.ts create mode 100644 packages/devtools-ui/scripts/check-font-assets.mjs create mode 100644 packages/devtools-ui/src/assets/fonts/BricolageGrotesque-Bold.ttf create mode 100644 packages/devtools-ui/src/assets/fonts/Inter-latin.woff2 create mode 100644 packages/devtools-ui/src/assets/fonts/OFL-Bricolage-Grotesque.txt create mode 100644 packages/devtools-ui/src/assets/fonts/OFL-Inter.txt create mode 100644 packages/devtools-ui/src/internal.ts create mode 100644 packages/devtools-ui/src/styles/semantic-theme.ts create mode 100644 packages/devtools-ui/tests/contrast.test.ts create mode 100644 packages/devtools-ui/tests/fonts.test.ts create mode 100644 packages/devtools-ui/tests/semantic-theme.test.ts create mode 100644 packages/devtools/src/components/plugins-strip.tsx delete mode 100644 packages/devtools/src/components/tabs.tsx create mode 100644 packages/devtools/src/components/workbench-header.tsx create mode 100644 packages/devtools/src/components/workbench-secondary-tabs.tsx delete mode 100644 packages/devtools/src/context/draw-context.tsx delete mode 100644 packages/devtools/src/styles/tokens.ts create mode 100644 packages/devtools/tests/semantic-color-usage.test.ts create mode 100644 packages/devtools/tests/seo-workbench.test.tsx create mode 100644 packages/devtools/tests/workbench-secondary-tabs.test.tsx create mode 100644 packages/devtools/tests/workbench.test.tsx diff --git a/.changeset/tanstack-devtools-branding.md b/.changeset/tanstack-devtools-branding.md new file mode 100644 index 00000000..cb64ac83 --- /dev/null +++ b/.changeset/tanstack-devtools-branding.md @@ -0,0 +1,7 @@ +--- +'@tanstack/devtools': minor +'@tanstack/devtools-ui': minor +'@tanstack/devtools-a11y': minor +--- + +feat: apply TanStack branding and the compact Workbench layout across core, shared UI, and accessibility Devtools diff --git a/docs/architecture.md b/docs/architecture.md index e9a79802..2b033798 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -14,7 +14,7 @@ block-beta end block:core["Core Layer"] columns 3 - Shell["Devtools Shell"] UI["UI Components"] Client["Event Client"] + Workbench["Devtools Workbench"] UI["UI components + private semantic theme"] Client["Event Client"] end block:transport["Transport Layer"] columns 3 @@ -37,10 +37,10 @@ graph TD end subgraph Core["Core Layer"] - shell["@tanstack/devtools
Core shell (Solid.js)"] + shell["@tanstack/devtools
Core Workbench (Solid.js)"] client["@tanstack/devtools-client
Core devtools events"] eventClient["@tanstack/devtools-event-client
Generic EventClient"] - ui["@tanstack/devtools-ui
Shared UI components"] + ui["@tanstack/devtools-ui
Shared UI + private semantic-theme owner"] clientBus["@tanstack/devtools-event-bus/client
ClientEventBus"] end @@ -147,17 +147,23 @@ The devtools shell is a Solid.js application that renders the entire devtools UI The shell renders: - A **trigger button** (the floating devtools toggle, customizable or replaceable) -- A **resizable panel** (docked to the bottom of the viewport, resizable via drag) -- **Tab navigation** for switching between plugins, settings, SEO inspector, and the plugin marketplace +- A **resizable Workbench panel** (docked to the top or bottom of the viewport, resizable via pointer or keyboard) +- A compact **36px TanStack Devtools header** with explicit Plugins, SEO, and Settings destinations +- A horizontal **Plugins strip** that expands vertically on hover or focus while keeping the panel footprint compact +- The **Plugin Marketplace**, reached from the Plugins destination without disturbing mounted plugin panes - A **settings panel** for theme, hotkeys, position, and other preferences -- **Plugin containers** -- DOM elements where each plugin's UI is mounted +- Up to three simultaneous **plugin mount frames**, divided into equal widths by static separators Settings and UI state (panel size, position, active tab, theme) are persisted in `localStorage` so they survive page reloads. +The core shell owns the Workbench header, navigation, mount-frame geometry, separators, and surrounding light/dark surfaces. Each external plugin owns everything inside its mount target; core styling deliberately does not reach into plugin descendants. Detaching the Workbench uses a fixed `100vh` Picture-in-Picture layout and restores the stored docked height when reattached. + ### @tanstack/devtools-ui -- Component Library A shared Solid.js component library used by the core shell and available for use in Solid.js plugins. Provides buttons, inputs, checkboxes, a JSON tree viewer, section layouts, and other UI primitives. The `@tanstack/devtools-utils` package also depends on it to provide framework-specific plugin helpers. +Core packages share a private semantic resolver through `@tanstack/devtools-ui/internal`. It supplies TanStack light/dark colors, typography, spacing, status roles, and focus treatment to core-owned UI and the accessibility plugin. This internal subpath is an implementation boundary, not a public theming API for application plugins. + ### @tanstack/devtools-client -- Core Event Client A specialized `EventClient` pre-configured with `pluginId: 'tanstack-devtools-core'` and a fixed event map for devtools-internal operations: diff --git a/docs/overview.md b/docs/overview.md index 15d2716c..bfd14652 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -83,12 +83,15 @@ Your application loads a **Framework Adapter**, which mounts the **Core Shell**. ## Key Features +- **Compact TanStack Workbench**: The trigger reveals a branded light/dark panel with a 36px banner and explicit Plugins, SEO, and Settings navigation. The whole panel keeps its existing hide-and-reveal lifecycle. - **Framework Agnostic**: Works with React, Vue, Solid, and Preact out of the box. -- **Plugin System & Marketplace**: Build, share, and install devtools plugins with a simple API. +- **Plugin System & Marketplace**: Build, share, discover, and install devtools plugins with a simple API. The horizontal Plugins strip expands on hover or keyboard focus. +- **Simultaneous Plugin Panes**: Open up to three plugins at once in equal-width panes while each plugin retains ownership of its own UI. +- **Accessibility Auditing**: Add the accessibility plugin during development to surface issues alongside the other plugin panes; the basic React example includes an intentional audit fixture. - **Type-Safe Event System**: Communicate between plugins and the shell using fully typed events. - **Source Inspector**: Click any element in your app to jump straight to its source code (go-to-source). - **Console Piping**: Route devtools output to your browser console for a familiar debugging workflow. -- **Picture-in-Picture Mode**: Pop the devtools panel out into its own window so it never covers your app. +- **Picture-in-Picture Mode**: Pop the Workbench into a fixed-`100vh` window; reattaching restores its stored docked height. - **Customizable Hotkeys**: Rebind keyboard shortcuts to match your workflow. ## Next Steps diff --git a/examples/react/basic/README.md b/examples/react/basic/README.md index 1cf88926..3f99d7bc 100644 --- a/examples/react/basic/README.md +++ b/examples/react/basic/README.md @@ -1,6 +1,15 @@ -# Example +# Basic React example -To run this example: +Run the example from the repository root: -- `npm install` -- `npm run dev` +```sh +pnpm --filter @tanstack/devtools-example-react-basic dev +``` + +The accessibility plugin is registered only in development builds. The page's `A11yAuditFixture` deliberately includes three violations so the plugin has deterministic findings to display: + +- `image-alt`: an image without alternative text +- `button-name`: an icon-only button without an accessible name +- `label`: an input without an associated label + +The fixture is intentionally invalid test content, not an accessibility pattern to copy. Browser extensions, environment differences, or future audit rules may report additional findings. diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 70918cd2..2450f411 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -6,9 +6,11 @@ "dev": "vite --port=3005", "build": "vite build", "preview": "vite preview", + "test:fixture": "node ./scripts/check-a11y-fixture.mjs", "test:types": "tsc" }, "dependencies": { + "@tanstack/devtools-a11y": "workspace:*", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-client": "0.5.0", "@tanstack/react-devtools": "^0.10.9", diff --git a/examples/react/basic/scripts/check-a11y-fixture.mjs b/examples/react/basic/scripts/check-a11y-fixture.mjs new file mode 100644 index 00000000..fc0804fd --- /dev/null +++ b/examples/react/basic/scripts/check-a11y-fixture.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const setup = readFileSync(join(root, 'src/setup.tsx'), 'utf8') +const fixture = readFileSync(join(root, 'src/a11y-audit-fixture.tsx'), 'utf8') +const example = readFileSync(join(root, 'src/index.tsx'), 'utf8') +const exampleCss = readFileSync(join(root, 'src/example.css'), 'utf8') +assert.match( + setup, + /import\s+\{\s*a11yDevtoolsPlugin\s*\}\s+from\s+['"]@tanstack\/devtools-a11y\/react['"]/, +) +assert.equal(setup.match(/a11yDevtoolsPlugin\(\)/g)?.length, 1) +assert.match(setup, /position:\s*['"]bottom-left['"]/) +assert.match(setup, /import\.meta\.env\.DEV\s*&&\s*/) +assert.match(example, /import ['"]\.\/example\.css['"]/) +assert.match(fixture, /data-testid="a11y-image-alt"[^>]*src=[^>]*\/>/) +assert.doesNotMatch(fixture, /data-testid="a11y-image-alt"[^>]*\balt=/) +assert.match( + fixture, + //, +) +assert.match(fixture, //) +assert.doesNotMatch(fixture, /setTimeout|setInterval|fetch\(|https?:\/\//) +const darkStyles = exampleCss.slice( + exampleCss.indexOf('@media (prefers-color-scheme: dark)'), +) +assert.match( + darkStyles, + /\.example-actions p\s*\{[^}]*color:\s*#aea691;/, + 'dark action copy must override the light foreground on #111111', +) + +const luminance = (hex) => { + const channels = hex + .slice(1) + .match(/.{2}/g) + .map((channel) => Number.parseInt(channel, 16) / 255) + .map((channel) => + channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4, + ) + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2] +} +const contrast = (foreground, background) => { + const values = [luminance(foreground), luminance(background)].sort( + (a, b) => b - a, + ) + return (values[0] + 0.05) / (values[1] + 0.05) +} +assert.ok(contrast('#aea691', '#111111') >= 4.5) diff --git a/examples/react/basic/src/a11y-audit-fixture.tsx b/examples/react/basic/src/a11y-audit-fixture.tsx new file mode 100644 index 00000000..1365a8bd --- /dev/null +++ b/examples/react/basic/src/a11y-audit-fixture.tsx @@ -0,0 +1,14 @@ +export function A11yAuditFixture() { + return ( +
+ {/* prettier-ignore */} + +
+ ) +} diff --git a/examples/react/basic/src/example.css b/examples/react/basic/src/example.css new file mode 100644 index 00000000..11521b4a --- /dev/null +++ b/examples/react/basic/src/example.css @@ -0,0 +1,248 @@ +:root { + color: #3e3529; + background: #f7f5ef; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 20rem; + min-height: 100vh; + background: + radial-gradient(circle at top right, #eeebd4, transparent 36rem), #f7f5ef; +} + +button, +input { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +.example-shell { + width: min(100% - 2rem, 48rem); + margin: 0 auto; + padding: clamp(2rem, 7vw, 5rem) 0 7rem; +} + +.example-header { + max-width: 40rem; + margin-bottom: 2rem; +} + +.example-eyebrow { + margin: 0 0 0.5rem; + color: #624a00; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.example-header h1, +.example-content h2, +.example-content h1 { + color: #111111; + letter-spacing: -0.035em; +} + +.example-header h1 { + margin: 0; + font-size: clamp(2rem, 6vw, 3.25rem); + line-height: 1.05; +} + +.example-header p:not(.example-eyebrow) { + max-width: 36rem; + margin: 1rem 0 0; + color: #756c5b; + font-size: 1rem; + line-height: 1.6; +} + +.example-card { + margin-top: 1rem; + padding: clamp(1rem, 3vw, 1.5rem); + border: 1px solid #eeebd4; + border-radius: 0.75rem; + background: #ffffff; + box-shadow: + 0 1px 2px rgb(17 17 17 / 0.03), + 0 10px 24px rgb(62 53 41 / 0.05); +} + +.example-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.example-actions p { + margin: 0; + color: #756c5b; +} + +.example-actions strong { + color: #111111; + font-variant-numeric: tabular-nums; +} + +.example-button-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.example-shell button:not([data-testid^='a11y-']) { + min-height: 2.5rem; + padding: 0.55rem 0.875rem; + border: 1px solid #3e3529; + border-radius: 0.5rem; + background: #3e3529; + color: #ffffff; + cursor: pointer; + font-weight: 650; + transition: + background-color 120ms ease, + border-color 120ms ease, + transform 120ms ease; +} + +.example-shell button:not([data-testid^='a11y-']):hover { + border-color: #624a00; + background: #624a00; +} + +.example-shell button:not([data-testid^='a11y-']):active { + transform: scale(0.96); +} + +.example-shell :focus-visible { + outline: 3px solid #61adbf; + outline-offset: 3px; +} + +.example-content { + color: #3e3529; + line-height: 1.65; +} + +.example-content h2, +.example-content h1 { + margin: 0 0 0.75rem; + font-size: 1.25rem; + line-height: 1.25; +} + +.example-content p { + max-width: 66ch; +} + +.example-content a { + color: #003e53; + font-weight: 650; + text-decoration-thickness: 0.1em; + text-underline-offset: 0.18em; +} + +.a11y-audit-fixture { + position: fixed; + top: 1rem; + right: 1rem; + display: grid; + gap: 0.5rem; + padding: 0.5rem; + border: 1px dashed #756c5b; + border-radius: 0.5rem; + background: #ffffff; +} + +.a11y-audit-fixture button, +.a11y-audit-fixture input { + min-height: 2rem; +} + +@media (max-width: 36rem) { + .example-shell { + width: min(100% - 1.5rem, 48rem); + padding-top: 1.5rem; + } + + .example-actions { + align-items: flex-start; + flex-direction: column; + } + + .example-button-row { + width: 100%; + } + + .example-button-row button { + flex: 1 1 12rem; + } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #c5c3bf; + background: #111111; + } + + body { + background: + radial-gradient(circle at top right, #003e53, transparent 36rem), #111111; + } + + .example-eyebrow { + color: #fae884; + } + + .example-header h1, + .example-content h2, + .example-content h1, + .example-actions strong { + color: #ffffff; + } + + .example-header p:not(.example-eyebrow), + .example-actions p { + color: #aea691; + } + + .example-card, + .a11y-audit-fixture { + border-color: #2d2d2d; + background: #1f1f1f; + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.2), + 0 10px 24px rgb(0 0 0 / 0.3); + } + + .example-content { + color: #c5c3bf; + } + + .example-content a { + color: #9cd5e2; + } + + .example-shell button:not([data-testid^='a11y-']) { + border-color: #aea691; + background: #aea691; + color: #111111; + } + + .example-shell button:not([data-testid^='a11y-']):hover { + border-color: #ffffff; + background: #ffffff; + } +} diff --git a/examples/react/basic/src/index.tsx b/examples/react/basic/src/index.tsx index 7fe7eeab..00ca1317 100644 --- a/examples/react/basic/src/index.tsx +++ b/examples/react/basic/src/index.tsx @@ -11,6 +11,7 @@ import Devtools from './setup' import { queryPlugin } from './plugin' import { Button } from './button' import { Feature } from './feature' +import './example.css' const queryClient = new QueryClient({ defaultOptions: { @@ -168,37 +169,57 @@ function App() { const [win, setWin] = useState(null) const [postId, setPostId] = useState(-1) return ( -
+
-

TanStack Devtools React Basic Example

- current count: {state} - - +
+

TanStack Devtools

+

React basic example

+

+ Explore cached posts, plugin panels, and the development-only + accessibility fixture in one focused sandbox. +

+
+
+

+ Current count {state} +

+
+ + +
+
{win && createPortal(, win.document.body)} - -

- As you visit the posts below, you will notice them in a loading - state the first time you load them. However, after you return to - this list and click on any posts you have already visited again, you - will see them load instantly and background refresh right before - your eyes!{' '} - - (You may need to throttle your network speed to simulate longer - loading sequences) - -

- {postId > -1 ? ( - - ) : ( - - )} +
+ +

+ As you visit the posts below, you will notice them in a loading + state the first time you load them. However, after you return to + this list and click on any posts you have already visited again, + you will see them load instantly and background refresh right + before your eyes!{' '} + + (You may need to throttle your network speed to simulate longer + loading sequences) + +

+ {postId > -1 ? ( + + ) : ( + + )} +
-
+ ) } diff --git a/examples/react/basic/src/setup.tsx b/examples/react/basic/src/setup.tsx index c246fcea..f5f918ca 100644 --- a/examples/react/basic/src/setup.tsx +++ b/examples/react/basic/src/setup.tsx @@ -10,6 +10,8 @@ import { createRouter, } from '@tanstack/react-router' import { TanStackDevtools } from '@tanstack/react-devtools' +import { a11yDevtoolsPlugin } from '@tanstack/devtools-a11y/react' +import { A11yAuditFixture } from './a11y-audit-fixture' import { PackageJsonPanel } from './package-json-panel' const rootRoute = createRootRoute({ @@ -62,8 +64,9 @@ export default function DevtoolsExample() { eventBusConfig={{ connectToServerBus: true, }} - config={{ sourceAction: 'copy-path' }} + config={{ sourceAction: 'copy-path', position: 'bottom-left' }} plugins={[ + a11yDevtoolsPlugin(), { name: 'TanStack Query', render: , @@ -88,6 +91,7 @@ export default function DevtoolsExample() { ]} /> + {import.meta.env.DEV && } ) } diff --git a/examples/react/basic/src/vite-env.d.ts b/examples/react/basic/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/react/basic/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/devtools-a11y/src/core/components/IssueCard.tsx b/packages/devtools-a11y/src/core/components/IssueCard.tsx index 4420616d..a7fe1da5 100644 --- a/packages/devtools-a11y/src/core/components/IssueCard.tsx +++ b/packages/devtools-a11y/src/core/components/IssueCard.tsx @@ -1,8 +1,9 @@ /** @jsxImportSource solid-js */ import { For, Show } from 'solid-js' -import { Button } from '@tanstack/devtools-ui' +import { Button, createTheme } from '@tanstack/devtools-ui' import { createStyles } from '../styles/styles' +import { getSeverityStyle } from '../styles/severity-theme' // types import type { A11yIssue, SeverityThreshold } from '../types/types' @@ -18,34 +19,55 @@ interface A11yIssueCardProps { export function A11yIssueCard(props: A11yIssueCardProps) { const selector = () => props.issue.nodes[0]?.selector || 'unknown' const styles = createStyles() + const { theme } = createTheme() + const severity = () => getSeverityStyle(props.impact, theme()) return ( -
-
+
+
event.stopPropagation()} > Learn more @@ -53,8 +75,10 @@ export function A11yIssueCard(props: A11yIssueCardProps) {
-
+ ) } diff --git a/packages/devtools-a11y/src/core/components/IssueList.tsx b/packages/devtools-a11y/src/core/components/IssueList.tsx index 5552785a..082466d6 100644 --- a/packages/devtools-a11y/src/core/components/IssueList.tsx +++ b/packages/devtools-a11y/src/core/components/IssueList.tsx @@ -38,7 +38,7 @@ export function A11yIssueList(props: A11yIssueListProps) { ally.allyResult.audit && ally.filteredIssues().length > 0 ) { - highlightAllIssues(ally.filteredIssues()) + highlightAllIssues(ally.filteredIssues(), ally.theme()) } return @@ -66,6 +66,7 @@ export function A11yIssueList(props: A11yIssueListProps) { highlightElement(selector, issue.impact, { showTooltip: true, ruleId: issue.ruleId, + theme: ally.theme(), }) } } catch (error) { @@ -75,8 +76,8 @@ export function A11yIssueList(props: A11yIssueListProps) { } return ( -
-
+
+
{(impact) => { // Count issues from the reactive filteredIssues memo so counts update when config.threshold changes @@ -88,6 +89,8 @@ export function A11yIssueList(props: A11yIssueListProps) { return (
-
-
+
+

General

@@ -142,7 +142,7 @@ export function A11ySettingsOverlay(props: A11ySettingsOverlayProps) { />
-
+
{(rule, idx) => { const isDisabled = () => disabledRulesSet().has(rule.id) @@ -158,6 +158,8 @@ export function A11ySettingsOverlay(props: A11ySettingsOverlayProps) { return (
-
+
{`${SEVERITY_LABELS[config.threshold]}+ | ${RULE_SET_LABELS[config.ruleSet]}`} @@ -110,10 +111,10 @@ export function Shell() {
-
+
-
+

No audit results yet

@@ -129,7 +130,7 @@ export function Shell() { allyResult.audit.issues.length === 0 } > -

+

No accessibility issues found!

diff --git a/packages/devtools-a11y/src/core/contexts/allyContext.tsx b/packages/devtools-a11y/src/core/contexts/allyContext.tsx index 920eae9c..37860154 100644 --- a/packages/devtools-a11y/src/core/contexts/allyContext.tsx +++ b/packages/devtools-a11y/src/core/contexts/allyContext.tsx @@ -8,6 +8,7 @@ import { useContext as getContext, } from 'solid-js' import { createStore } from 'solid-js/store' +import { createTheme } from '@tanstack/devtools-ui' import { filterIssuesAboveThreshold, runAudit } from '../utils/ally-audit.utils' import { mergeConfig, saveConfig } from '../utils/config.utils' import { clearHighlights, highlightAllIssues } from '../utils/ui.utils' @@ -25,6 +26,7 @@ import type { ParentComponent } from 'solid-js' // function createAllyValue() { + const { theme } = createTheme() const [config, setConfig] = createStore>(mergeConfig()) @@ -67,7 +69,8 @@ function createAllyValue() { return } - if (allyResult.state === 'done') highlightAllIssues(filteredIssues()) + if (allyResult.state === 'done') + highlightAllIssues(filteredIssues(), theme()) }) createEffect(() => { @@ -86,6 +89,7 @@ function createAllyValue() { config, allyResult, + theme, } } diff --git a/packages/devtools-a11y/src/core/styles/severity-theme.ts b/packages/devtools-a11y/src/core/styles/severity-theme.ts new file mode 100644 index 00000000..46784e30 --- /dev/null +++ b/packages/devtools-a11y/src/core/styles/severity-theme.ts @@ -0,0 +1,26 @@ +import { resolveSemanticTheme } from '@tanstack/devtools-ui/internal' + +import type { StatusRole } from '@tanstack/devtools-ui/internal' +import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui' +import type { SeverityThreshold } from '../types/types' + +const severity = { + critical: { role: 'error', label: 'Critical', outline: '3px solid' }, + serious: { role: 'error', label: 'Serious', outline: '2px solid' }, + moderate: { role: 'warning', label: 'Moderate', outline: '2px solid' }, + minor: { role: 'info', label: 'Minor', outline: '2px dashed' }, +} as const satisfies Record< + SeverityThreshold, + { role: StatusRole; label: string; outline: string } +> + +export function getSeverityStyle( + impact: SeverityThreshold, + theme: TanStackDevtoolsTheme, +) { + const definition = severity[impact] + return { + ...definition, + colors: resolveSemanticTheme(theme).color.status[definition.role], + } +} diff --git a/packages/devtools-a11y/src/core/styles/styles.ts b/packages/devtools-a11y/src/core/styles/styles.ts index d7d41e46..b96f713f 100644 --- a/packages/devtools-a11y/src/core/styles/styles.ts +++ b/packages/devtools-a11y/src/core/styles/styles.ts @@ -1,16 +1,11 @@ import * as goober from 'goober' import { createTheme } from '@tanstack/devtools-ui' import { createMemo } from 'solid-js' +import { resolveSemanticTheme } from '@tanstack/devtools-ui/internal' -import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui' -import type { RuleCategory, SeverityThreshold } from '../types/types' +import { getSeverityStyle } from './severity-theme' -const SEVERITY_COLORS: Record = { - critical: '#dc2626', - serious: '#ea580c', - moderate: '#ca8a04', - minor: '#2563eb', -} +import type { RuleCategory, SeverityThreshold } from '../types/types' export const CATEGORY_LABELS: Record = { all: 'All Categories', @@ -47,129 +42,118 @@ export const CATEGORIES: Array = [ ] const css = goober.css -const FONT_SCALE = 1.1 -const fontPx = (size: number) => `calc(${size}px * ${FONT_SCALE})` - -function createA11yPanelStyles(theme: TanStackDevtoolsTheme) { - const t = (light: string, dark: string) => (theme === 'light' ? light : dark) - const bg = t('#f9fafb;', '#191c24') - const fg = t('#1e293b', '#e2e8f0') - const border = t('#e2e8f0', '#292e3d') - const muted = t('#64748b', '#94a3b8') - const muted2 = t('#727c8b', '#818386') +function createA11yPanelStyles(themeName: 'light' | 'dark') { + const theme = resolveSemanticTheme(themeName) + const { color, font, gap, radius, shadow, space, type } = theme return { - colors: { bg, fg, border, muted, muted2, theme }, - + colors: { theme: themeName }, root: css` height: 100%; display: flex; flex-direction: column; overflow: hidden; position: relative; + background: ${color.surface.workspace}; + color: ${color.text.primary}; + font-family: ${font.body}; + font-weight: 300; `, - header: css` - padding: 16px; - border-bottom: 1px solid ${border}; + padding: ${space[3]} ${space[4]}; + border-bottom: 1px solid ${color.border.decorative}; display: flex; justify-content: space-between; align-items: center; + gap: ${gap.section}; flex-shrink: 0; anchor-name: --a11y-toast-anchor; + background: ${color.surface.brand}; `, headerTitleRow: css` display: flex; align-items: center; - gap: 12px; + gap: ${gap.control}; min-width: 0; `, headerTitle: css` margin: 0; - font-size: ${fontPx(16)}; - font-weight: 600; + font-family: ${font.display}; + font-size: ${type.headingPane.size}; + line-height: ${type.headingPane.lineHeight}; + font-weight: ${type.headingPane.weight}; `, headerSub: css` - font-size: ${fontPx(12)}; - color: ${muted}; + font-size: ${type.bodyXs.size}; + line-height: ${type.bodyXs.lineHeight}; + color: ${color.text.mutedOnBrand}; white-space: nowrap; `, headerActions: css` display: flex; - gap: 8px; + gap: ${gap.control}; align-items: center; flex-wrap: wrap; justify-content: flex-end; `, primaryButton: css` - padding: 8px 16px; - color: #fff; - border-radius: 6px; + padding: ${theme.padding.controlBlock} ${theme.padding.controlInline}; + border-radius: ${radius.control}; cursor: pointer; - font-weight: 500; - font-size: ${fontPx(13)}; - opacity: 1; `, primaryButtonDisabled: css` cursor: not-allowed; opacity: 0.7; `, button: css` - padding: 8px 12px; - color: ${fg}; - border: 1px solid ${border}; - border-radius: 6px; + padding: ${theme.padding.controlBlock} ${theme.padding.controlInline}; + border-radius: ${radius.control}; cursor: pointer; - font-size: ${fontPx(11)}; `, buttonRow: css` display: flex; - gap: 6px; + gap: ${gap.tight}; align-items: center; `, toggleOverlay: css` - padding: 8px 12px; - color: ${fg}; - border: 1px solid ${border}; - border-radius: 6px; + padding: ${theme.padding.controlBlock} ${theme.padding.controlInline}; + border-radius: ${radius.control}; cursor: pointer; - font-size: ${fontPx(13)}; `, toggleOverlayOn: css` - background: #10b981; - color: #fff; - border-color: #10b981; + background: ${color.status.success.subtleFill}; + color: ${color.status.success.text}; + border-color: ${color.status.success.border}; `, - statusBar: css` - padding: 8px 16px; - border-bottom: 1px solid ${border}; + padding: ${space[2]} ${space[4]}; + border-bottom: 1px solid ${color.border.decorative}; display: flex; - gap: 12px; + gap: ${gap.section}; align-items: center; flex-shrink: 0; - font-size: ${fontPx(11)}; - color: ${muted}; + font-size: ${type.bodyXs.size}; + line-height: ${type.bodyXs.lineHeight}; + color: ${color.text.muted}; + background: ${color.surface.subtle}; `, statusSpacer: css` flex: 1; `, smallLinkButton: css` - padding: 4px 10px; + padding: ${space[1]} ${space[2]}; background: transparent; - color: #0ea5e9; - border: 1px solid ${border}; - border-radius: 4px; + color: ${color.text.link}; + border: 1px solid ${color.border.control}; + border-radius: ${radius.control}; cursor: pointer; - font-size: ${fontPx(11)}; - font-weight: 500; `, - content: css` flex: 1; overflow-y: auto; - padding: 16px; + padding: ${space[4]}; + background: ${color.surface.workspace}; `, emptyState: css` display: flex; @@ -178,14 +162,17 @@ function createA11yPanelStyles(theme: TanStackDevtoolsTheme) { justify-content: center; height: 100%; text-align: center; - color: ${muted}; + color: ${color.text.muted}; `, emptyPrimary: css` - font-size: ${fontPx(14)}; - margin: 0 0 8px 0; + font-size: ${type.bodySm.size}; + line-height: ${type.bodySm.lineHeight}; + margin: 0 0 ${space[2]} 0; + color: ${color.text.primary}; `, emptySecondary: css` - font-size: ${fontPx(12)}; + font-size: ${type.bodyXs.size}; + line-height: ${type.bodyXs.lineHeight}; margin: 0; `, successState: css` @@ -195,308 +182,335 @@ function createA11yPanelStyles(theme: TanStackDevtoolsTheme) { justify-content: center; height: 100%; text-align: center; + background: ${color.status.success.subtleFill}; + border-radius: ${radius.group}; `, successTitle: css` - font-size: ${fontPx(16)}; - color: #10b981; - font-weight: 600; + font-family: ${font.display}; + font-size: ${type.headingPane.size}; + line-height: ${type.headingPane.lineHeight}; + color: ${color.status.success.text}; + font-weight: 700; margin: 0; `, successSub: css` - font-size: ${fontPx(12)}; - color: ${muted}; - margin-top: 8px; - margin-bottom: 0; + font-size: ${type.bodyXs.size}; + color: ${color.status.success.text}; + margin: ${space[2]} 0 0; `, - summaryGrid: css` display: grid; grid-template-columns: repeat(4, 1fr); - gap: 12px; - margin-bottom: 20px; - + gap: ${gap.control}; + margin-bottom: ${space[4]}; @media (max-width: 520px) { grid-template-columns: repeat(2, 1fr); } `, summaryButton: css` - padding: 12px; - color: ${fg}; - background: ${bg}; - border-radius: 8px; - border: 1px solid ${border}; + padding: ${space[3]}; + color: ${color.text.primary}; + background: ${color.surface.subtle}; + border-radius: ${radius.group}; + border: 1px solid ${color.border.decorative}; text-align: left; cursor: pointer; - box-shadow: none; - + box-shadow: ${shadow.xs}; &:hover { - background: ${t('#f0f2f5', '#111318')}; + background: ${color.state.hover}; + } + &:focus-visible { + outline: 2px solid ${color.border.focus}; + outline-offset: 2px; } `, summaryButtonActive: (impact: SeverityThreshold) => css` - box-shadow: 0 0 0 2px ${SEVERITY_COLORS[impact]}; + background: ${getSeverityStyle(impact, themeName).colors.subtleFill}; + border-color: ${getSeverityStyle(impact, themeName).colors.border}; `, summaryCount: (impact: SeverityThreshold) => css` - font-size: ${fontPx(24)}; + font-family: ${font.display}; + font-size: 24px; + line-height: 28px; font-weight: 700; - color: ${SEVERITY_COLORS[impact]}; + color: ${getSeverityStyle(impact, themeName).colors.text}; `, summaryLabel: css` - font-size: ${fontPx(11)}; - color: ${muted}; + font-size: ${type.labelSm.size}; + line-height: ${type.labelSm.lineHeight}; + font-weight: ${type.labelSm.weight}; + letter-spacing: ${type.labelSm.tracking}; + color: ${color.text.muted}; text-transform: uppercase; `, - section: css` - margin-bottom: 16px; + margin-bottom: ${space[4]}; `, sectionTitle: (impact: SeverityThreshold) => css` - color: ${SEVERITY_COLORS[impact]}; - font-size: ${fontPx(13)}; - font-weight: 600; - margin: 0 0 8px 0; - text-transform: uppercase; - letter-spacing: 0.5px; + color: ${getSeverityStyle(impact, themeName).colors.text}; + font-family: ${font.display}; + font-size: ${type.headingCompact.size}; + line-height: ${type.headingCompact.lineHeight}; + font-weight: 700; + margin: 0 0 ${space[2]}; `, - issueCard: css` - padding: 12px; - margin-bottom: 8px; - border: 1px solid ${border}; - border-radius: 6px; + padding: ${space[3]}; + margin-bottom: ${space[2]}; + border: 1px solid ${color.border.decorative}; + border-radius: ${radius.group}; cursor: pointer; + background: ${color.surface.elevated}; + box-shadow: ${shadow.xs}; `, issueCardSelected: css` - background: ${t('#e0f2fe', '#1e3a5f')}; - border-color: #0ea5e9; + background: ${color.state.selectionFill}; + color: ${color.state.selectionText}; + border-color: ${color.border.focus}; `, issueRow: css` display: flex; justify-content: space-between; align-items: flex-start; - gap: 12px; + gap: ${gap.section}; `, issueMain: css` flex: 1; min-width: 0; `, + issueSelectButton: css` + flex: 1; + min-width: 0; + padding: 0; + color: inherit; + background: transparent; + border: 0; + font: inherit; + text-align: left; + cursor: pointer; + &:focus-visible { + outline: 2px solid ${color.border.focus}; + outline-offset: 2px; + border-radius: ${radius.control}; + } + `, + issueSelectButtonSelected: css` + &:focus-visible { + outline-color: ${color.state.selectionText}; + } + `, issueTitleRow: css` font-weight: 600; - font-size: ${fontPx(13)}; - margin-bottom: 4px; + font-size: ${type.bodySm.size}; + line-height: ${type.bodySm.lineHeight}; + margin-bottom: ${space[1]}; display: flex; align-items: center; - gap: 8px; + gap: ${gap.control}; `, dot: (impact: SeverityThreshold) => css` - width: 8px; - height: 8px; + width: ${space[2]}; + height: ${space[2]}; border-radius: 50%; - background: ${SEVERITY_COLORS[impact]}; + background: ${getSeverityStyle(impact, themeName).colors.solidFill}; flex-shrink: 0; `, + severityLabel: (impact: SeverityThreshold) => css` + font-size: ${type.labelSm.size}; + line-height: ${type.labelSm.lineHeight}; + color: ${getSeverityStyle(impact, themeName).colors.text}; + background: ${getSeverityStyle(impact, themeName).colors.subtleFill}; + border: 1px solid ${getSeverityStyle(impact, themeName).colors.border}; + padding: 2px ${space[1]}; + border-radius: ${radius.control}; + `, issueMessage: css` - font-size: ${fontPx(12)}; - color: ${t('#475569', '#cbd5e1')}; - margin: 0 0 8px 0; - line-height: 1.4; + font-size: ${type.bodyXs.size}; + line-height: ${type.bodyXs.lineHeight}; + color: inherit; + margin: 0 0 ${space[2]}; `, selector: css` - font-size: ${fontPx(10)}; - color: ${muted2}; - font-family: - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; + font-size: ${type.bodyXs.size}; + line-height: ${type.bodyXs.lineHeight}; + color: inherit; + font-family: ${font.mono}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + opacity: 0.8; `, issueAside: css` display: flex; flex-direction: column; align-items: flex-end; - gap: 4px; + gap: ${gap.tight}; flex-shrink: 0; `, helpLink: css` - font-size: ${fontPx(12)}; - color: #0ea5e9; - padding: 0 12px; + font-size: ${type.bodyXs.size}; + color: ${color.text.link}; + padding: 0 ${space[3]}; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; - - &:hover { - color: #0284c7; - } - &:focus-visible { - outline: 2px solid #0ea5e9; + outline: 2px solid ${color.border.focus}; outline-offset: 2px; - border-radius: 4px; + border-radius: ${radius.control}; + } + `, + helpLinkSelected: css` + color: ${color.state.selectionText}; + `, + issueGhostControlSelected: css` + && { + color: ${color.state.selectionText}; + border-color: ${color.state.selectionText}; } `, disableRule: css` - font-size: ${fontPx(10)}; - color: ${muted}; + font-size: ${type.bodyXs.size}; + color: ${color.text.muted}; background: none; border: none; cursor: pointer; padding: 0; font-weight: 600; - - &:hover { - color: #000000; - } - - &:focus-visible { - outline: 2px solid #0ea5e9; - outline-offset: 2px; - border-radius: 4px; - } `, tags: css` display: flex; - gap: 4px; - margin-top: 8px; + gap: ${gap.tight}; + margin-top: ${space[2]}; flex-wrap: wrap; `, tag: css` - font-size: ${fontPx(10)}; - padding: 2px 6px; - border: 1px solid ${border}; - border-radius: 4px; - color: ${muted}; + font-size: ${type.bodyXs.size}; + padding: 2px ${space[1]}; + border: 1px solid currentColor; + border-radius: ${radius.control}; + color: inherit; + opacity: 0.8; `, - settingsOverlay: css` position: absolute; inset: 0; - background: ${bg}; + background: ${color.surface.workspace}; display: flex; flex-direction: column; z-index: 10; `, settingsHeader: css` - padding: 12px 16px; - border-bottom: 1px solid ${border}; + padding: ${space[3]} ${space[4]}; + border-bottom: 1px solid ${color.border.decorative}; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; + background: ${color.surface.brand}; `, settingsTitle: css` margin: 0; - font-size: ${fontPx(14)}; - font-weight: 600; + font-family: ${font.display}; + font-size: ${type.headingPane.size}; + line-height: ${type.headingPane.lineHeight}; + font-weight: 700; `, doneButton: css` - padding: 6px 12px; - background: ${bg}; - color: ${bg}; - border: none; - border-radius: 4px; + padding: ${theme.padding.controlBlock} ${theme.padding.controlInline}; + border-radius: ${radius.control}; cursor: pointer; - font-size: ${fontPx(12)}; - font-weight: 500; `, settingsContent: css` flex: 1; overflow-y: auto; - padding: 16px; + padding: ${space[4]}; `, settingsSection: css` margin-bottom: 24px; `, settingsRowStack: css` display: grid; - gap: 12px; + gap: ${gap.section}; `, settingsSectionLabel: css` - margin: 0 0 12px 0; - font-size: ${fontPx(12)}; - font-weight: 600; + margin: 0 0 ${space[3]}; + font-size: ${type.labelSm.size}; + line-height: ${type.labelSm.lineHeight}; + font-weight: ${type.labelSm.weight}; + letter-spacing: ${type.labelSm.tracking}; text-transform: uppercase; - letter-spacing: 0.5px; - color: ${muted}; + color: ${color.text.muted}; `, settingsRow: css` display: flex; align-items: center; justify-content: space-between; - padding: 10px 0; - border-bottom: 1px solid ${border}; - gap: 12px; + padding: ${space[2]} 0; + border-bottom: 1px solid ${color.border.decorative}; + gap: ${gap.section}; `, settingsRowTitle: css` - font-size: ${fontPx(13)}; + font-size: ${type.bodySm.size}; font-weight: 500; `, settingsRowDesc: css` - font-size: ${fontPx(11)}; - color: ${muted}; + font-size: ${type.bodyXs.size}; + color: ${color.text.muted}; margin-top: 2px; `, select: css` - padding: 6px 10px; - border: 1px solid ${border}; - border-radius: 4px; - background: ${bg}; - color: ${fg}; - font-size: ${fontPx(12)}; + padding: ${theme.padding.controlBlock} ${theme.padding.controlInline}; + border: 1px solid ${color.border.control}; + border-radius: ${radius.control}; + background: ${color.surface.elevated}; + color: ${color.text.primary}; + font-size: ${type.bodyXs.size}; `, rulesHeaderRow: css` display: flex; align-items: center; justify-content: space-between; - margin-bottom: 12px; - gap: 12px; + margin-bottom: ${space[3]}; + gap: ${gap.section}; flex-wrap: wrap; `, rulesHeaderActions: css` display: flex; - gap: 6px; + gap: ${gap.tight}; `, filtersRow: css` display: flex; - gap: 8px; - margin-bottom: 12px; + gap: ${gap.control}; + margin-bottom: ${space[3]}; flex-wrap: wrap; `, search: css` flex: 1; min-width: 180px; - padding: 8px 10px; - border: 1px solid ${border}; - border-radius: 4px; - background: ${bg}; - color: ${fg}; - font-size: ${fontPx(12)}; - box-sizing: border-box; `, rulesList: css` - border: 1px solid ${border}; - border-radius: 6px; + border: 1px solid ${color.border.decorative}; + border-radius: ${radius.group}; overflow-y: auto; + background: ${color.surface.elevated}; `, ruleRow: css` display: flex; align-items: flex-start; - gap: 8px; - padding: 8px 10px; + gap: ${gap.control}; + padding: ${space[2]} ${space[3]}; cursor: pointer; - opacity: 1; background: transparent; - &:hover { - background: ${t('#f0f2f5', '#111318')}; + background: ${color.state.hover}; } `, ruleRowDisabled: css` opacity: 0.6; `, ruleRowBorder: css` - border-bottom: 1px solid ${border}; + border-bottom: 1px solid ${color.border.decorative}; `, ruleCheckbox: css` margin-top: 2px; @@ -509,41 +523,41 @@ function createA11yPanelStyles(theme: TanStackDevtoolsTheme) { ruleTop: css` display: flex; align-items: center; - gap: 6px; + gap: ${gap.tight}; margin-bottom: 2px; `, ruleId: css` font-weight: 500; - font-size: ${fontPx(12)}; - text-decoration: none; + font-size: ${type.bodyXs.size}; `, ruleIdDisabled: css` text-decoration: line-through; `, bpBadge: css` - font-size: ${fontPx(9)}; - padding: 1px 4px; - background: #f59e0b; - color: #fff; - border-radius: 3px; + font-size: ${type.bodyXs.size}; + padding: 1px ${space[1]}; + background: ${color.status.warning.subtleFill}; + color: ${color.status.warning.text}; + border: 1px solid ${color.status.warning.border}; + border-radius: ${radius.control}; font-weight: 500; `, ruleDesc: css` - font-size: ${fontPx(11)}; - color: ${muted}; - line-height: 2; + font-size: ${type.bodyXs.size}; + color: ${color.text.muted}; + line-height: ${type.bodyXs.lineHeight}; `, catTagRow: css` display: flex; - gap: 4px; - margin-top: 4px; + gap: ${gap.tight}; + margin-top: ${space[1]}; `, catTag: css` - font-size: ${fontPx(9)}; - padding: 1px 4px; - border: 1px solid ${muted}; - border-radius: 3px; - color: ${muted}; + font-size: ${type.bodyXs.size}; + padding: 1px ${space[1]}; + border: 1px solid ${color.border.control}; + border-radius: ${radius.control}; + color: ${color.text.muted}; `, } } diff --git a/packages/devtools-a11y/src/core/utils/ui.utils.ts b/packages/devtools-a11y/src/core/utils/ui.utils.ts index 880a514e..c623ba49 100644 --- a/packages/devtools-a11y/src/core/utils/ui.utils.ts +++ b/packages/devtools-a11y/src/core/utils/ui.utils.ts @@ -1,4 +1,8 @@ +import { resolveSemanticTheme } from '@tanstack/devtools-ui/internal' +import { getSeverityStyle } from '../styles/severity-theme' + // types +import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui' import type { A11yIssue, RuleSetPreset, @@ -82,38 +86,15 @@ export const RULE_SET_LABELS: Record = { all: 'All Rules', } -/** - * Color scheme for different severity levels - */ -const SEVERITY_COLORS: Record< - SeverityThreshold, - { border: string; bg: string; text: string } -> = { - critical: { - border: '#dc2626', - bg: 'rgba(220, 38, 38, 0.15)', - text: '#dc2626', - }, - serious: { - border: '#ea580c', - bg: 'rgba(234, 88, 12, 0.15)', - text: '#ea580c', - }, - moderate: { - border: '#ca8a04', - bg: 'rgba(202, 138, 4, 0.15)', - text: '#ca8a04', - }, - minor: { border: '#2563eb', bg: 'rgba(37, 99, 235, 0.15)', text: '#2563eb' }, -} - /** * Inject overlay styles into the document */ -function injectStyles(): void { - if (document.getElementById(HIGHLIGHT_STYLE_ID)) { - return - } +function injectStyles(theme: TanStackDevtoolsTheme): void { + document.getElementById(HIGHLIGHT_STYLE_ID)?.remove() + + const semantic = resolveSemanticTheme(theme) + const styleFor = (impact: SeverityThreshold) => + getSeverityStyle(impact, theme) const style = document.createElement('style') style.id = HIGHLIGHT_STYLE_ID @@ -121,63 +102,61 @@ function injectStyles(): void { // Tooltips use fixed positioning to avoid layout shifts style.textContent = ` .${HIGHLIGHT_CLASS}--critical { - outline: 3px solid ${SEVERITY_COLORS.critical.border} !important; + outline: ${styleFor('critical').outline} ${styleFor('critical').colors.border} !important; outline-offset: 2px !important; - background-color: ${SEVERITY_COLORS.critical.bg} !important; + background-color: ${styleFor('critical').colors.subtleFill} !important; } .${HIGHLIGHT_CLASS}--serious { - outline: 3px solid ${SEVERITY_COLORS.serious.border} !important; + outline: ${styleFor('serious').outline} ${styleFor('serious').colors.border} !important; outline-offset: 2px !important; - background-color: ${SEVERITY_COLORS.serious.bg} !important; + background-color: ${styleFor('serious').colors.subtleFill} !important; } .${HIGHLIGHT_CLASS}--moderate { - outline: 2px solid ${SEVERITY_COLORS.moderate.border} !important; + outline: ${styleFor('moderate').outline} ${styleFor('moderate').colors.border} !important; outline-offset: 2px !important; - background-color: ${SEVERITY_COLORS.moderate.bg} !important; + background-color: ${styleFor('moderate').colors.subtleFill} !important; } .${HIGHLIGHT_CLASS}--minor { - outline: 2px dashed ${SEVERITY_COLORS.minor.border} !important; + outline: ${styleFor('minor').outline} ${styleFor('minor').colors.border} !important; outline-offset: 2px !important; - background-color: ${SEVERITY_COLORS.minor.bg} !important; + background-color: ${styleFor('minor').colors.subtleFill} !important; } .${TOOLTIP_CLASS} { position: fixed; padding: 4px 8px; border-radius: 4px; - font-size: 11px; - font-weight: 600; - font-family: system-ui, -apple-system, sans-serif; + font: ${semantic.type.bodyXs.weight} ${semantic.type.bodyXs.size} / ${semantic.type.bodyXs.lineHeight} ${semantic.font.body}; white-space: nowrap; z-index: 99990; pointer-events: none; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: ${semantic.shadow.sm}; max-width: 90vw; overflow: hidden; text-overflow: ellipsis; } .${TOOLTIP_CLASS}--critical { - background: ${SEVERITY_COLORS.critical.border}; - color: white; + background: ${styleFor('critical').colors.solidFill}; + color: ${styleFor('critical').colors.onFill}; } .${TOOLTIP_CLASS}--serious { - background: ${SEVERITY_COLORS.serious.border}; - color: white; + background: ${styleFor('serious').colors.solidFill}; + color: ${styleFor('serious').colors.onFill}; } .${TOOLTIP_CLASS}--moderate { - background: ${SEVERITY_COLORS.moderate.border}; - color: white; + background: ${styleFor('moderate').colors.solidFill}; + color: ${styleFor('moderate').colors.onFill}; } .${TOOLTIP_CLASS}--minor { - background: ${SEVERITY_COLORS.minor.border}; - color: white; + background: ${styleFor('minor').colors.solidFill}; + color: ${styleFor('minor').colors.onFill}; } ` document.head.appendChild(style) @@ -306,13 +285,11 @@ function createTooltip( // Build tooltip content showing all issues if (sortedIssues.length === 1) { - tooltip.textContent = `${mostSevere.toUpperCase()}: ${firstIssue.ruleId}` + tooltip.textContent = `${SEVERITY_LABELS[mostSevere]}: ${firstIssue.ruleId}` } else { // Multiple issues - show count and list const issueList = sortedIssues - .map( - (issue) => `${issue.impact.charAt(0).toUpperCase()}: ${issue.ruleId}`, - ) + .map((issue) => `${SEVERITY_LABELS[issue.impact]}: ${issue.ruleId}`) .join(' | ') tooltip.textContent = `${sortedIssues.length} issues: ${issueList}` } @@ -342,12 +319,16 @@ function createTooltip( export function highlightElement( selector: string, impact: SeverityThreshold = 'serious', - options: { showTooltip?: boolean; ruleId?: string } = {}, + options: { + showTooltip?: boolean + ruleId?: string + theme?: TanStackDevtoolsTheme + } = {}, ): void { - const { showTooltip = true, ruleId } = options + const { showTooltip = true, ruleId, theme = 'dark' } = options try { - injectStyles() + injectStyles(theme) const elements = document.querySelectorAll(selector) if (elements.length === 0) { @@ -389,8 +370,11 @@ export function highlightElement( * Highlight all elements with issues. * Shows all issues per element in the tooltip, using the most severe for highlighting. */ -export function highlightAllIssues(issues: Array): void { - injectStyles() +export function highlightAllIssues( + issues: Array, + theme: TanStackDevtoolsTheme = 'dark', +): void { + injectStyles(theme) clearHighlights() // Track ALL issues for each selector diff --git a/packages/devtools-a11y/tests/index.test.ts b/packages/devtools-a11y/tests/index.test.ts index 9ca0bdfc..06aef4b8 100644 --- a/packages/devtools-a11y/tests/index.test.ts +++ b/packages/devtools-a11y/tests/index.test.ts @@ -1,7 +1,33 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@tanstack/devtools-utils/react', () => ({ + createReactPanel: () => [() => null, () => null], + createReactPlugin: ({ name }: { name: string }) => [ + () => ({ name, render: () => ({ type: Symbol.for('development') }) }), + () => ({ name, render: () => ({ type: Symbol.for('react.fragment') }) }), + ], +})) + +afterEach(() => { + vi.unstubAllEnvs() + vi.resetModules() +}) describe('devtools', () => { it('should pass', () => { expect(true).toBe(true) }) }) + +it('keeps the React accessibility plugin a no-op in production', async () => { + vi.stubEnv('NODE_ENV', 'production') + vi.resetModules() + const { a11yDevtoolsPlugin } = await import('../src/react/index') + const definition = a11yDevtoolsPlugin() + expect(definition.name).toBe('TanStack A11y') + const rendered = definition.render(document.createElement('div'), { + theme: 'dark', + devtoolsOpen: true, + }) + expect(rendered).toMatchObject({ type: Symbol.for('react.fragment') }) +}) diff --git a/packages/devtools-a11y/tests/theme.test.ts b/packages/devtools-a11y/tests/theme.test.ts new file mode 100644 index 00000000..0f4e870b --- /dev/null +++ b/packages/devtools-a11y/tests/theme.test.ts @@ -0,0 +1,215 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { extname, join, relative } from 'node:path' +import { createComponent } from 'solid-js' +import { render } from 'solid-js/web' +import { afterEach, describe, expect, it } from 'vitest' +import { ThemeContextProvider } from '@tanstack/devtools-ui' +import { resolveSemanticTheme } from '@tanstack/devtools-ui/internal' +import { A11yIssueCard } from '../src/core/components/IssueCard' +import { getSeverityStyle } from '../src/core/styles/severity-theme' +import { + clearHighlights, + highlightAllIssues, + highlightElement, +} from '../src/core/utils/ui.utils' +import type { A11yIssue } from '../src/core/types/types' + +const packageRoot = process.cwd() +const coreRoot = join(packageRoot, 'src/core') +const files = (path: string): Array => + readdirSync(path, { withFileTypes: true }).flatMap((entry) => { + const child = join(path, entry.name) + return entry.isDirectory() + ? files(child) + : ['.ts', '.tsx'].includes(extname(child)) + ? [child] + : [] + }) +const rawColor = /#[\da-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch)\(/i +const gradient = /\b(?:linear|radial|conic)-gradient\(/i +const namedExemptions = new Map([ + [ + 'semantic-color-exempt: transparent-overlay', + /^src[\\/]core[\\/]utils[\\/]ui\.utils\.ts$/, + ], +]) + +afterEach(() => { + clearHighlights() + document.body.replaceChildren() +}) + +describe.each(['light', 'dark'] as const)('%s accessibility theme', (theme) => { + it('maps severity to exact shared roles and distinguishable outlines', () => { + expect(getSeverityStyle('critical', theme)).toMatchObject({ + role: 'error', + label: 'Critical', + outline: '3px solid', + }) + expect(getSeverityStyle('serious', theme)).toMatchObject({ + role: 'error', + label: 'Serious', + outline: '2px solid', + }) + expect(getSeverityStyle('moderate', theme)).toMatchObject({ + role: 'warning', + label: 'Moderate', + outline: '2px solid', + }) + expect(getSeverityStyle('minor', theme)).toMatchObject({ + role: 'info', + label: 'Minor', + outline: '2px dashed', + }) + const semantic = resolveSemanticTheme(theme) + expect(getSeverityStyle('critical', theme).colors).toEqual( + semantic.color.status.error, + ) + expect(getSeverityStyle('moderate', theme).colors).toEqual( + semantic.color.status.warning, + ) + }) + + it('renders selected IssueCard surface/control markers and visible severity text', () => { + const issue: A11yIssue = { + id: 'issue-1', + ruleId: 'image-alt', + impact: 'critical', + message: 'Add alt text', + help: 'Images need alt text', + helpUrl: 'https://example.test/image-alt', + wcagTags: ['wcag111'], + nodes: [ + { + selector: '#bad-image', + html: '', + failureSummary: 'Missing alt', + }, + ], + meetsThreshold: true, + timestamp: 1, + } + const host = document.body.appendChild(document.createElement('div')) + const dispose = render( + () => + createComponent(ThemeContextProvider, { + theme, + get children() { + return createComponent(A11yIssueCard, { + issue, + impact: 'critical', + selected: true, + onSelect: () => {}, + onDisableRule: () => {}, + }) + }, + }), + host, + ) + expect(host.querySelector('[data-tsd-surface]')).toHaveAttribute( + 'data-tsd-selected', + 'true', + ) + expect(host.querySelector('[data-severity="critical"]')).toHaveTextContent( + 'Critical', + ) + const selectButton = host.querySelector('button[aria-pressed]') + expect(selectButton).toHaveAttribute('type', 'button') + expect(selectButton).toHaveAttribute('aria-pressed', 'true') + expect(selectButton).toHaveAttribute('data-tsd-control') + expect(host.querySelector('a')?.className).not.toBe('') + expect( + host.querySelector('button:not([aria-pressed])')?.className, + ).not.toBe('') + expect(document.head.textContent).toContain( + resolveSemanticTheme(theme).color.state.selectionText, + ) + dispose() + }) + + it('injects theme-aware overlay CSS and written tooltip severity', () => { + const target = document.body.appendChild(document.createElement('img')) + target.id = 'bad-image' + highlightElement('#bad-image', 'critical', { + showTooltip: true, + ruleId: 'image-alt', + theme, + }) + const style = document.getElementById('tsd-a11y-highlight-styles')! + expect(style.textContent).toContain( + getSeverityStyle('critical', theme).colors.border, + ) + expect(document.querySelector('[data-a11y-overlay]')).toHaveTextContent( + 'Critical: image-alt', + ) + expect(style.textContent).toContain( + `font: ${resolveSemanticTheme(theme).type.bodyXs.weight} ${resolveSemanticTheme(theme).type.bodyXs.size}`, + ) + }) + + it('uses written severity labels for every multi-issue tooltip entry', () => { + const target = document.body.appendChild(document.createElement('img')) + target.id = 'bad-image' + highlightAllIssues( + [ + { + id: 'issue-critical', + ruleId: 'image-alt', + impact: 'critical', + message: 'Add alt text', + help: 'Images need alt text', + helpUrl: 'https://example.test/image-alt', + wcagTags: [], + nodes: [ + { + selector: '#bad-image', + html: '', + failureSummary: 'Missing alt', + }, + ], + meetsThreshold: true, + timestamp: 1, + }, + { + id: 'issue-minor', + ruleId: 'image-redundant-alt', + impact: 'minor', + message: 'Avoid redundant alt text', + help: 'Images need useful alt text', + helpUrl: 'https://example.test/image-redundant-alt', + wcagTags: [], + nodes: [ + { + selector: '#bad-image', + html: '', + failureSummary: 'Redundant alt', + }, + ], + meetsThreshold: true, + timestamp: 1, + }, + ], + theme, + ) + expect(document.querySelector('[data-a11y-overlay]')).toHaveTextContent( + '2 issues: Critical: image-alt | Minor: image-redundant-alt', + ) + }) +}) + +it('rejects every unapproved raw color and gradient in accessibility-owned source', () => { + const violations = files(coreRoot).flatMap((path) => { + const file = relative(packageRoot, path) + return readFileSync(path, 'utf8') + .split(/\r?\n/) + .flatMap((line, index) => { + if (!rawColor.test(line) && !gradient.test(line)) return [] + const exempt = [...namedExemptions].some( + ([marker, allowedPath]) => + line.includes(marker) && allowedPath.test(file), + ) + return exempt ? [] : [`${file}:${index + 1}: ${line.trim()}`] + }) + }) + expect(violations, violations.join('\n')).toEqual([]) +}) diff --git a/packages/devtools-a11y/vite.config.ts b/packages/devtools-a11y/vite.config.ts index 2e55f268..29e7a0a5 100644 --- a/packages/devtools-a11y/vite.config.ts +++ b/packages/devtools-a11y/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig, mergeConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' import { tanstackViteConfig } from '@tanstack/vite-config' import solid from 'vite-plugin-solid' import packageJson from './package.json' @@ -7,7 +8,7 @@ import type { Plugin } from 'vite' const config = defineConfig({ plugins: [ solid({ - ssr: true, + ssr: process.env.VITEST !== 'true', }) as any satisfies Plugin, ], test: { @@ -17,6 +18,14 @@ const config = defineConfig({ environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], globals: true, + alias: { + '@tanstack/devtools-utils/react': fileURLToPath( + new URL('../devtools-utils/src/react/index.ts', import.meta.url), + ), + '@tanstack/devtools-utils/solid': fileURLToPath( + new URL('../devtools-utils/src/solid/index.ts', import.meta.url), + ), + }, }, }) diff --git a/packages/devtools-ui/package.json b/packages/devtools-ui/package.json index 1db8a071..35b5513f 100644 --- a/packages/devtools-ui/package.json +++ b/packages/devtools-ui/package.json @@ -36,6 +36,12 @@ "default": "./dist/esm/icons.js" } }, + "./internal": { + "import": { + "types": "./dist/esm/internal.d.ts", + "default": "./dist/esm/internal.js" + } + }, "./package.json": "./package.json" }, "sideEffects": false, @@ -44,7 +50,9 @@ }, "files": [ "dist/", - "src" + "src", + "!src/assets/fonts/*.ttf", + "!src/assets/fonts/*.woff2" ], "scripts": { "clean": "premove ./build ./dist", @@ -53,7 +61,8 @@ "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", - "test:build": "publint --strict", + "test:assets": "pnpm build && node ./scripts/check-font-assets.mjs", + "test:build": "pnpm test:assets && publint --strict", "build": "vite build" }, "dependencies": { diff --git a/packages/devtools-ui/scripts/check-font-assets.mjs b/packages/devtools-ui/scripts/check-font-assets.mjs new file mode 100644 index 00000000..09542f69 --- /dev/null +++ b/packages/devtools-ui/scripts/check-font-assets.mjs @@ -0,0 +1,317 @@ +import { createHash } from 'node:crypto' +import { + mkdtempSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { basename, dirname, join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import packageJson from '../package.json' with { type: 'json' } + +const MAX_BYTES = 163840 +const expectedFonts = new Map([ + [ + 'BricolageGrotesque-Bold.ttf', + '913ee3631949ee1b4fb2601269412fca5775eb994d4f87be2c366c94ac123dc5', + ], + [ + 'Inter-latin.woff2', + '2c295d99e26dcf357d4d01bcf270fd6924b600c9a13dd8c363ef114f4c6976fa', + ], +]) +const packageRoot = join(import.meta.dirname, '..') + +function fail(message) { + throw new Error(`Font asset check failed: ${message}`) +} + +function validateTemporaryDirectory(directory, prefix) { + const resolvedDirectory = resolve(directory) + if ( + dirname(resolvedDirectory) !== resolve(tmpdir()) || + !basename(resolvedDirectory).startsWith(prefix) + ) { + fail(`invalid temporary directory: ${directory}`) + } + return resolvedDirectory +} + +function inspectFonts(directory, expectedNames) { + const entries = readdirSync(directory, { withFileTypes: true }).filter( + (entry) => entry.isFile() && /\.(?:ttf|woff2)$/.test(entry.name), + ) + const names = entries.map((entry) => entry.name).sort() + if ( + expectedNames && + names.join('\n') !== [...expectedNames].sort().join('\n') + ) { + fail(`unexpected source binaries: ${names.join(', ')}`) + } + if (entries.length !== 2) fail(`expected two binaries in ${directory}`) + + let total = 0 + const hashes = [] + for (const entry of entries) { + const path = join(directory, entry.name) + const contents = readFileSync(path) + total += statSync(path).size + const expectedHash = expectedFonts.get(entry.name) + const actualHash = createHash('sha256').update(contents).digest('hex') + hashes.push(actualHash) + if (expectedHash && actualHash !== expectedHash) + fail(`hash mismatch: ${entry.name}`) + if (!expectedHash && ![...expectedFonts.values()].includes(actualHash)) { + fail(`unapproved emitted binary: ${entry.name}`) + } + } + if (total > MAX_BYTES) fail(`binary total ${total} exceeds ${MAX_BYTES}`) + const expectedHashes = [...expectedFonts.values()].sort() + if (hashes.sort().join('\n') !== expectedHashes.join('\n')) { + fail(`font hash multiset mismatch in ${directory}`) + } +} + +function inspectGeneratedFontUrls(directory) { + let generatedText = '' + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + generatedText += inspectGeneratedFontUrls(path) + } else if (/\.(?:js|css)$/.test(entry.name)) { + const contents = readFileSync(path, 'utf8') + if ( + /(?:^|["'(\s:=])\/assets\/[^"'()\s]+\.(?:ttf|woff2)/im.test(contents) + ) { + fail(`root-relative font URL in ${path}`) + } + generatedText += contents + } + } + return generatedText +} + +const requiredExclusions = [ + '!src/assets/fonts/*.ttf', + '!src/assets/fonts/*.woff2', +] +for (const exclusion of requiredExclusions) { + if (!packageJson.files.includes(exclusion)) + fail(`missing files exclusion ${exclusion}`) +} + +inspectFonts(join(packageRoot, 'src', 'assets', 'fonts'), expectedFonts.keys()) +inspectFonts(join(packageRoot, 'dist', 'assets')) +inspectGeneratedFontUrls(join(packageRoot, 'dist', 'esm')) + +const packPrefix = join(tmpdir(), 'tanstack-devtools-ui-pack-') +const windowsPackDestinationVariable = 'TANSTACK_DEVTOOLS_UI_PACK_DESTINATION' +const previousWindowsPackDestination = + process.env[windowsPackDestinationVariable] +const packDirectory = mkdtempSync(packPrefix) + +try { + const resolvedPackDirectory = validateTemporaryDirectory( + packDirectory, + 'tanstack-devtools-ui-pack-', + ) + if (/[\r\n"%]/.test(resolvedPackDirectory)) { + fail('temporary directory cannot be quoted safely for cmd.exe') + } + + const isWindows = process.platform === 'win32' + if (isWindows) { + // Node otherwise escapes quote characters in a /c argument. Expand a + // pre-quoted environment value so native cmd.exe owns parsing while the + // spawn options remain portable and explicit. + process.env[windowsPackDestinationVariable] = `"${resolvedPackDirectory}"` + } + const result = isWindows + ? spawnSync( + process.env.ComSpec ?? 'cmd.exe', + [ + '/d', + '/s', + '/c', + `pnpm.cmd pack --pack-destination %${windowsPackDestinationVariable}%`, + ], + { + cwd: packageRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + windowsHide: true, + }, + ) + : spawnSync('pnpm', ['pack', '--pack-destination', packDirectory], { + cwd: packageRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }) + if (result.error) fail(`pack spawn error: ${result.error.message}`) + if (result.status === null) fail('pack did not report an exit status') + if (result.status !== 0) + fail(`pack exited ${result.status}: ${result.stderr}`) + if (typeof result.stdout !== 'string' || !result.stdout.trim()) { + fail('pack produced empty output') + } + + const archives = readdirSync(packDirectory).filter((name) => + name.endsWith('.tgz'), + ) + if (archives.length !== 1) + fail(`expected one tarball, found ${archives.length}`) + const tar = spawnSync('tar', ['-tf', join(packDirectory, archives[0])], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }) + if (tar.error) fail(`tar spawn error: ${tar.error.message}`) + if (tar.status === null) fail('tar did not report an exit status') + if (tar.status !== 0) fail(`tar listing failed: ${tar.stderr}`) + if (typeof tar.stdout !== 'string' || !tar.stdout.trim()) { + fail('tar produced an empty listing') + } + + const files = tar.stdout + .split(/\r?\n/) + .filter(Boolean) + .map((name) => name.replaceAll('\\', '/').replace(/^\.\//, '')) + const packedFonts = files.filter((name) => /\.(?:ttf|woff2)$/i.test(name)) + if (packedFonts.length !== 2) { + fail(`expected two packed font binaries, found ${packedFonts.length}`) + } + if ( + packedFonts.some( + (name) => !/^package\/dist\/assets\/[^/]+\.(?:ttf|woff2)$/i.test(name), + ) + ) { + fail(`font binary outside package/dist/assets: ${packedFonts.join(', ')}`) + } + if ( + files.some((name) => + /^package\/src\/assets\/fonts\/.*\.(?:ttf|woff2)$/i.test(name), + ) + ) { + fail('source font binaries leaked into tarball') + } + const requiredLicenses = [ + 'package/src/assets/fonts/OFL-Bricolage-Grotesque.txt', + 'package/src/assets/fonts/OFL-Inter.txt', + ] + for (const license of requiredLicenses) { + const count = files.filter((name) => name === license).length + if (count !== 1) fail(`expected ${license} once, found ${count}`) + } + + const consumerDirectory = mkdtempSync( + join(tmpdir(), 'tanstack-devtools-ui-consumer-'), + ) + try { + const resolvedConsumerDirectory = validateTemporaryDirectory( + consumerDirectory, + 'tanstack-devtools-ui-consumer-', + ) + const installedPackageDirectory = join( + resolvedConsumerDirectory, + 'node_modules', + '@tanstack', + 'devtools-ui', + ) + mkdirSync(installedPackageDirectory, { recursive: true }) + + const unpack = spawnSync( + 'tar', + [ + '-xf', + join(resolvedPackDirectory, archives[0]), + '-C', + installedPackageDirectory, + '--strip-components', + '1', + ], + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + windowsHide: true, + }, + ) + if (unpack.error) fail(`consumer unpack error: ${unpack.error.message}`) + if (unpack.status === null) { + fail('consumer unpack did not report an exit status') + } + if (unpack.status !== 0) fail(`consumer unpack exited ${unpack.status}`) + + writeFileSync( + join(resolvedConsumerDirectory, 'index.html'), + '
\n', + ) + writeFileSync( + join(resolvedConsumerDirectory, 'main.js'), + [ + "import { devtoolsFontCss } from '@tanstack/devtools-ui/internal'", + "document.querySelector('#app').textContent = devtoolsFontCss", + '', + ].join('\n'), + ) + + const viteCli = join( + packageRoot, + '..', + '..', + 'node_modules', + 'vite', + 'bin', + 'vite.js', + ) + if (!statSync(viteCli).isFile()) fail(`missing Vite CLI: ${viteCli}`) + const consumerBuild = spawnSync( + process.execPath, + [viteCli, 'build', '--base', './'], + { + cwd: resolvedConsumerDirectory, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + windowsHide: true, + }, + ) + if (consumerBuild.error) { + fail(`consumer build error: ${consumerBuild.error.message}`) + } + if (consumerBuild.status === null) { + fail('consumer build did not report an exit status') + } + if (consumerBuild.status !== 0) { + fail(`consumer build exited ${consumerBuild.status}`) + } + if ( + typeof consumerBuild.stdout !== 'string' || + !consumerBuild.stdout.trim() + ) { + fail('consumer build produced empty output') + } + + const consumerDist = join(resolvedConsumerDirectory, 'dist') + inspectFonts(join(consumerDist, 'assets')) + const consumerOutput = inspectGeneratedFontUrls(consumerDist) + for (const marker of ['Bricolage Grotesque', 'Inter']) { + if (!consumerOutput.includes(marker)) { + fail(`consumer output is missing font marker: ${marker}`) + } + } + } finally { + rmSync(consumerDirectory, { recursive: true, force: true }) + } +} finally { + if (previousWindowsPackDestination === undefined) { + delete process.env[windowsPackDestinationVariable] + } else { + process.env[windowsPackDestinationVariable] = previousWindowsPackDestination + } + rmSync(packDirectory, { recursive: true, force: true }) +} + +console.log('Font assets and package contents verified.') diff --git a/packages/devtools-ui/src/assets/fonts/BricolageGrotesque-Bold.ttf b/packages/devtools-ui/src/assets/fonts/BricolageGrotesque-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ea2465b48adfcdd6ae84276f10f7938a6c8e6b8b GIT binary patch literal 82204 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%0Ds8~X5xc?yvLf^(V3~X?aJOPQ9 zFOQOg9sBtuIWXNw_pr2-Y-BZgJuO+`)r@3|R!Wj(z0N8_5g+5dPSzv5F~)jZ%MX$n z8_0f$xI90!FVU|?^&m@zBlMh6cIo*HyoBt50GM5>;$OK*+c6KF!sjSl*234B{8r$R z@-rL#x(rtdwiAcB<^Dw6DcJEPpT$Q@D{Z>@rUXNrnQ*(HRwlga>U7|k`Sh^j*q?y7 zHM8ns+NvvH)qfl5^hPGma@(B+`6b20VOQ44+Li^0iuOvAR&XcV-HADkC92dqI=x#{ zT;p^lOPzIIi^o!^*s1o`O$yg8Zq;hF;l>Vw&gZh{2!3-@TeZ6!`dFE4aTTZ(xdxri zUZfA${fQp0G1y)i?g;A3rmXJ~CyT8OQv-p?HMQg28k3{2#9JRUx_hbv7$H{Xn@fuh z_zZ?zZHe89@Pa_vFn-Myp2SQ)EYINFYIk8FBJ$Y!5og;nr0Lp7*jytT*yJM zdqry{=7lBNlG^e>#3b%gIvZwb0^Q5&n^sONFP{((mR6cf!A4*81b=C?*yitul|YN6 z`clw}`{mK_MVti*E^0vsUt3M@PQma!1|?GDgML3f@9}B*cM~O+@M$=w&B$btq0NoA zZrGeqqWPa0xDIawQPAvz>>5V5VkQ(KM-34+lpT*F!iAK!Ew7F&?uvI+=9|>Hs{LE_ zxuJ2?8M13~_4a_-6SI|6Y0zdp6rNV*Q z!wV=g|x5%4?KM)6KN~X0gLy-cp40vd4#R%>k-&`Yyl*FV-O+(GgJKV8_4S{LR zxh93JYUZXXrnzUX=&H~;J63ktf{ubBXUOJ>TdaxopM}2AI+_+Xj9*h<+STZoIJqmS znVuIPH*4|INXv?GzD+t;tW;&L^cww5Zcl@UK5ji;lpxoP#Ja{uJ@J&Gw} zAkpCnOsbwWRcp3utuPmC-B7BT^^Qm6-tl~zf0KFZ$Zo#cDT z8{JPJA2C~|68^&=P17#ECPOi*4&n<~@LHecqpBCPnvpWh_xL@$R$i9t1jG0m=MQTl z$HQUT(>PA?GbY)d$nS3kVG4qBCoZ~F@+5eRh+iGWl^yR4zMkRvH%C3Skrm{P^iw_p zver}yMawgC5|}lS+E@omD8V5iZhg{lCLzf(x`h;EgWX!kBN-M_Ilh)(&(WR$S!u8FYnsEp)>7a++zgH#6MW zRZ%!>Msp6e z)K)L=%(uIB2CXIk5`9ZcMeE*KjU6S0R%_0#a<8qnEE0)^Lsh1VUs$XBp*oe`RAQ^h z*DJbfV^hKdCR28(tgOi`C^Uv_b&f(=T$`wy=-HTGV$>V6MTOUFG-lKBVPBvs95Ot<2NZTp4%!=E8H;1vBH5|DBmsv$+d2{nmA1{!)FQpElz?1rCPp z^N*~rlu5h_yPp1$p~5FHD%gC+&8!W>1Zq;CMvmdL_)`1%8(CxxbP2P#Ex-;4@}!}|@j`3~k|!S~e#X)-?^M#v z%)t7WeA%{9rwO%IgqvK-0&RR;FxXmQ++XCgJ0muFuHIMTiWE~i_19O|P4JaZsIH&r zakclir9L@a-Q%g89Q*9QH9fw{sZn&hbdR<&8_Osd>CJ_5FwfAond_eu1l5Jo;=5twgOy-zv;m`>0$t1P!m~}`V(=jzA zm~S*jb)q)2CdFE~k&oL_tuAKYGSsk_L!fKC0p zWAjh1De@WeYC{)z$EDUEEsML9%B@AE0h6_9itny5DCJ}Er@Tfj-~Tmg2EghZnPT#h zgGQ!o?daS#Y09?Faof8632RAB&|7Wm&@}H@xNv)8(~gCUb~K#m>8VOiDs%TFX3Yhs z%rj`@mWu71WIo@^8PA}R`-uz=UvDex-8?t<6B!hK;#SE|{405#KlPxkm~Y9RGC%QI z$xlQsMY=3j1ngKN=l;1oIvz7&=G=^I!}=Ds)?zO>cMHFbJ^I$s-e4p521nCw$7-!a zJLDiTZ*cTe8yU=agN@u9WKZD@u9c(#e!2EAD6_+iUp7nPGUc4QGCl$J(;Xcc;C!+e z(zmr-F7tZ7mAx-BzFi}?=Moim%y8LGOGd?1eD5s>tCD5ttfTi@eW&2*JAJgGOf9a; zDU7^6wnovn@*JfJ?6PvFbl*lE>qim|=+zV}AV+AIhtZaSOOlgxWGY55iDryc$4@mE zR)|hH%vm3$I4rbXTO}4F1ED(Pr2d&~ujU20EqLMSo6AxoQ^nBg@cETVG-Ervj;PVWxp!(1)c-em8r ziFEoZrwz7?Q-p@<;H0Xlv$Uqt!V*_~(BkfjRdfYb7x|hhLTzO_t*1F0X%7_fabtKc zqI6a%N@s#cXfln2<|M)~LK@&}vsbh*GJCJs0HQ&eou$}uWYF+BdWb$c5-^-Sq>oyV4ABdO9I`o(6}uc~4azS@RoMoOO6$@1 zYuu_7)!x-UGeE`5)hbVW~xsrJh zM^`0k(=o>y>~RO(a*goc5Fe(b6n2+FvN6_^GCQuAk{z+(ito4LQ%`xjl^$>4`BVc6 zPh&6?vsL@NlUmEF`np?}HRoF_8PG5D0(_Lft$F2FodHVA9MU4~Ix7wQH zO_MxT%QS^f^Gr=)c8=bzclmXyg+*l!w^vhC;&y9N3+W}5(^|vD<PvD)h)qYBKl zpbwa7Nn5VCRmXN&gPX0mf4*?h{!rLfZr9TNL$pX+*{RvNo7z*KPnuwhb@<;;J^rVI zO;fz6LBhqf%PDIYW_}s?OBr}E_D(R0cz)(3^8D~DcVwW7*yQ(EE=rh3H5Mmwq)eiYjrS5ab{3bHslEwc&P~G|^fwu9Zi2@m!Lcui z!FeA`OTmM^E9r70&m~SoE)7=REcwLW!V7bQM(w&fmMv=F8?OXmAg3srI{0l&(vObb zcg6U|GvwIW3PzIpl3U_uYBzV8P(!KN0d$E9e!6KSwU? ze-fuc$H&+Qb-|uX#_Q(ZzChP~(W#G4FPtpor~2uYEFJ!gUY1$~4y4jdtFd#IwI^M~ zDdljky%O9EKZZ{a$e~)+^_NR`=o}hd-2axugN}DT_ zY)3pF@7es7ug$W}^7!Zl%?`}yDK2v?4JXEVMU^q9vNK#e*}uO$X!2RL`;Ec2IX~By zns+#z!FlZq+GhIehGxC;%SF>IkNG}s1_!>s`b`<6D&q!Em?In?SIit72WR!@I^WBkn9FBgoC-cS zkT|sCTZ0)6xz#=J!aL)Y)`Dw+d9s! z78K%2K}EF%J)+L%$QFtUgCU=}l;#$Po#hd8j;P4e=I843wKko-w6a9wtE)HEn-kSm zZQN8*GozryP^ZYrRpsc53}(<;gzu+bEL?|J3>;d}D*LTt#6^rR%vuwjQ#- z`PehLlD?BN^K{v!O`wOpQL{?89&ZOCf+wx@NGqe66;U|?$EQdWnpckxjNdbT*2$e^ z6IV4BN3nF=RPJmEn*4E1XwJ#gHe4}z>g5A7PMK4=!JL@YJYjV**f*zfPSOfBVrv<+ z5c~YHpi7AI!C8nG?(PjYX78jA?{Y@@K2W@cAcTa2Okd&&Zxi2*CYGceLn1GPbjemabyd-v$eOyqu3)DJ+TsBy|ZESyw_3>(&wLD zvNqhXT~i$Et|%+TQo1qZuBb6&O+PPN+;qmiY-MClyk#+_Pn3Bq!aUT5_DLpKtQR}w zVaU|jVbB_f+jH zxUtC=au($j78lW7Tkpqrk@`Dlo`^{R(~OUWWoQXMe~%`usfbUHY>zO-ed+0`ynH@d zS(YBSvitOfk?GClS!XDHQH!s}Q>awtParO8LUFH0?vMFu;p#q!Vpg@g67oi^o8K0T4(#Y=bl!Zt=PP^e9O64Pe)8I49=E;WGJ0R^-@sQ`839J!@B&zq4}p?#fPI=dPK?aKPee z^7YhqnXhCaWN}F%`HG}If zn@9&s;%-k_5o@{9hy|3g_h;ITZT_BRHKw4)fa#>6 z!fP^kDkXz+`IHfZGrI`}$DEkk+AyaE7Dr&-1HMAb_EIzNgWDWtbl48#FZWfqxm3GS z*9taRAC1Rxc+J+WOs8N6@wM7xEs*IL3*@Fn#X+YIvtCcUREy6mH=i_b^5%ADZKwNZ zboWI!7dX7-7H)!)juOcPMJBbxbjz3tLQQNYI|14&OX6E>kUYVQv@dV-C{Gn&Y3+eV zchmL-Z{uCQ*Zw?yc|&p2&TlnB3Tst|PqspcFyKqPV_G47q^H`iffX{>w|eL;+zKtI zl?1aG+m>^(}}Uy|G-JK743Xc$dS8fraL)gGyB&MPi2(K~F0oC}NTld0Ra zx!S6BU;Dhuj%}KXsddL#Ay=hQHbYEbKE`SmvqDK;uQXmBmla~8Jsr1if9KBG6_XnL zS*I&pm3n8)p%oQ5)P->(KD)}CTs+?7w`nXDB^6Ef(6J_{I5Cz9YOaD(qXn6Mb&X+v zZgO^>(33hrC`m5r@V1|~?@0R-2T%3?_x2|_c~h&axqRF9@ma+)`+BXpQAITx|;^ERAcrx=*aHZu&p5KM8Qu{_XY$o2#T4-H3J^V}E>+X^npM zX)2@5QRuIB6~OPxb|#zbk=Y4z&HVPor?hqMo^K16*s;gY%Nne}RMAoCF7_I7V1B~b zb1|m*sd9}nKd=by;b-vm@+}xK5OKh~HSK`$Z(qqPQ|VU<->8`tiO;EF$GP!*hsj@B z;LsZ#(a55%@rx@%i^laV2`ws&MeAD(Rh8icJTOWe!!O}G^n=XDF(qUlXMUaCr5Fr+ zae~R2T8%v3;g{%XbO)y(G4c+90R~1g&luFzhNK~IXTC{evlLrYo9Oas(TNp0M}=kT z6M>D()I}zZ-efGv(~g^5QjZ@A_S*ByvJ5L21qN`qhpuD(CHR9;fz9X9V&>PVWxoa^ z*5mVLWUZyTQbVc3*%sUUYg)Hw>ZAqH=2}CcDW}+Av}=?#O=?B&2~D|KwRH{S7;XzF zKLp$b+>a3?KZZr2oCZrp#GLiQX1qaVrzIr=wNskysa14V#aw^R0+bAasfV3kr+X8w+XGb6GjLNw2@!8k`cf#oa|ZeTk*W zs>#p)pm&bG*iq+?w>fwVKaH=}?|@Z>8^awW|H#%$ZBJBohHyOOx`H)M?stxXXcY&|7PG{AY+Dsm8acpqKge4)Zt;}lD>P%6; z#+c>yDPiKi{A+qT#d#Oq-am`4WCZU>)Bf0Ai$8fR{4K)m8O8Z7^Xt3nW&r z469UqiwzyK7Kh-YxQ3s`H~4qJV|0O!4q0k!bIvFiCA|v3SLBTBs|4xwa>ff>6sF$1 z&v6gsb!c*;Z1IwkGE;a~jmDak;0sve`FjskbaO3wy(rQ9t~Wc#7~l{Qd@~9_J0xP7b;hzth7wb__p*Zwii;qgN>8 zIlRNqD4K+4P?qz{aL~)y{GImV_sS$3FNtT4iDMc4JI`Sleo0X*JdCnjLLA3#j$@fH zB;$Bmym(9;EAXo-tQN!Y(+dBXI93Sf@*E-LDCao1jCn1m@>*8l7l{}S)9};cZ#fPw zVV(o=YVfiGZ^a6Hfe4l$wB3|AzyER#$HTOiK};w$gO_45We^wgT*31&txcD@l0%r*R>SUTh7Zsvv{s2o z2i|a~24))e0mP`|Iwl<IR%Y5(ja%?^k&_h0>7YR2amV!u0($}Pb$z=YD z+RyAfn=5D(rcpGkQtCZHza{K5#bf3yYh8n_d}6K37L1fzgg7TH{9>pfP-64q8@PE! z?T!L{F6MQi@|u9d6>?=r7D&PHiS%5&8^~*>MjkkWXHkuJV_?bo>l4^Y&ss-fgVrl_ z-tvGY>M_Tvj9JBX^(EyK>zuY=ILN9Fg&-DMTiM9!w))+92JQ9&Lyi&e&Y*6mE96Ao zjPL2J6{rBO>POIHv;ux@ip~5lq*3%W46w-&iK}iL*VQ!+|1NB3ZfR&}X>LGv{2o>_ ze!cuj7R$zq;C_G7giipgP1xO!7oPp@%THJ>oa@`Aw4Gs7?Fj6nt4?UlqRkVRWEJl2 zQSRHP?AcwIwPfOMW;uF>ms0j6WFLKzYr%`;?>I|e%w59mv;4hDC*E>q-)K?d%)ixY z{?X2xQ@e!f!+*Q^q?>o*hf`p@Z(h9`$nXiTYge)^GyL!2Tj)mNF=pk}D3|l)-Y@5f zrQf{Co_&gEgVHmlTf`8s_shL_2%1z>K^~&FaGm{#7GjaHo|=chq`&46FZDnIcKohW z8=b%~v(L%csps)Mdt~@mqv5Y);G%LAT==I1C&Md*^5J(F9s}?Q|oOYy6h9MgmloAGA zT$=K#-ON)62{~7Iu=XDGT;T{6=UFg3be?l|dplC?p7SR}`cGKP${UAIrQ?KOvB)|i zY>57{&rp~QKJ*NYh*eV2WEa@lB-2!Ujt)UylR}Bl=y>woI*Z>#smT{InL>DTlo|sT zohQ$o@S2ngqvwOAiqght+w7QLA+8nj!f}67g;ilvS}Pg@@klORBP#T<*=-?wCJd`L zprS>h!V4#!zcl7g7$F7({SqM&HLDXWPzQGu|7i=pJEBmUya{)nM_24OQfl^wOh)up z5ahRD;nb7x8kGu@$M}KLUKf(cSVMCo@jzpRRcTUKE1LZAaGtPMA|u*ZszB@X44+La zg0Twsb4WCW^Gb+EEZQkS`A&UR! zw9}AZE+EVqfX2zT7E$pVFgmgOd-1DQKS!^)0r4j_jt}9C--5<3GMy|u1)GeXhkd#R zH&X~We$oX!nc3$;i*_bI7i)y4D!HLC@zX zj-Tk#Nbf%emyCnl3ml9#-~d(wezEE<`UGfxg!kk}*bYL_Yz6$6fal|ic-^2Ei}7(d z(@>-%?&zL<8m~_0)$GeDDJUp;d!NRm*L&o7JhM>gwvM>gt}Jo(Ua0PN=Fnv$joGIF*vEueAj9cM03JH`D@~5{3aN zJ}Y8Qba7Hrar8+bl*8NPI|-Bdtwm?W#bve4M?F(%gVEHygzqg%zqNvP5@^I*4-=0h zvj#1+EdG`Rv$~~4c2sq4wlCvJjqr}_3xq@&QZQ+qTKwPa7HuNaZcCeyc50gc6BFI_ zoW4bQ{Tj|Xt?T9EAAO`p_hBQhzi{}V-hHmMe>-PRvvcR2x7fEOm-ntI=~5UP8#1a- z=V2w6m6X@ioK{(0ULu;cqchw03_EzSTAX#s*evdxVQXg>7{6&JD2k`;>zVX}I>Pw% zXF*9RNy*-u`ex_Ur)GBTI`gTi7e0M?w{Dj|ec{D-4;pmO)Mv10R!7oJnp(8d3oj-zTSdJV|*6%}UJ=4DhT#V5yBb&KhE%0-t=tLiYW zwsJsaez*3__;$(3=0%$1tl_7Q>sOw_JFEo}mt2^glsKp>Yf>T$`*&#Hr!>izl-4ab zr#dY$zBH#?zT%wcoMT@`PujZod2oE|)RF2W-guGi&uf#-IjQH|(!Xrrxt$mEd28T| zmR#6UsU8evSA3 zr#s7^qnk#=u6<@*NzLdUg&E!II(JK{n=-J^ zq+XF%1n1S}SGOx@EN(x%OHOjv#%_&Cou~H7u1HBQE{`kgkXlswx1^HJ1=VR~X?eM6 zc`0Gx$%S1i^BYRSBO@#7Q#ipE73Jbiih z?w3Dx;l=k19C**g)8-BwICs{db31lCrynmhgk9#V%8tuPjZUk|$}UQ+Q)v>MkZ#>?{|W6bU)1Z!KD<_O8%!}W+pZ%A;Rg0f6Wdevv6ojUnCvM`mObj+ zIgvth%9Oc7hRmIE-aSKy-gDlNb82eN8N%mu%ysR07MGW1g~gAK3`xw$$&;y&#IVZy zd);u>SvT}@&y!0>_v$se^t6c+PpdI$xO!qVhfm;PXeRxUX8)W@ctclsnV$PDr?-ZXjgCNt>O zSN)4uty;NKdZBc_^Ei?x+HvyMQQ(O-^^*!pyXJzK)WoG5OdjHSX-_=sJ5*sjrQ*P;E{(Stn_7s0M z7jdW5tYJqB);Qox4s2dVv^w`7cEsS>Px$JZd8LFs{95mXGU^hs)cdn%KVka%-?9@- zfuH3>Jf=0p=4D@sJf%wvt@n)YVxOmM|bQc|&V+?Q-Wi>2N{ z>Um;bIyE0a>dnt}|~$23HOoTUI;1S7GqDhc3F{;qjv%n;9IvFuJ@fvu90*Zkc9sX!z4~L1|g# zWsxt1BY8X#4eAE>`Dn2`c?Z<+fH}DBp1oc_@@pLFOE;1JgDvgb?BZ+x11`<1^0>4( zBR3B&EiePe9BjLx@4x2SP&jQ4Hc5<1mh9)xS7T*wK(SnEai*3Q!2MVD$zP+UNBuVH zl4$?ur$kNPcuMpo{x$#3PMLK-N6Qm)=FGY7-fzOm;Sa3oqvEWW%gO)t; zAYUv2WGBXVJnfG3%hFCy@BAM(Ua$w;d1uSCJMX-P2eaLD1BPK8tMaN-QYyQbb{bNa zryDTTqP}Fu)5oDqXlv_r66!Rt6>Dl}EzU>S_aCX`Oe{IiKe6h}!ogLi<&2nEa(?80 zCfCTMrty zY*R6~rn*~RY5$52XAQ0He)*WuSM|1UOf0I)FKx)K8d6np>d~ zl~RcBeEJW>@9L$~KAxDdFMi75y8!jF+bcCC>G2_FdpfseC7< zZGRcqRhC`|drxfSllF&@7f??zoBSXC7n?7X$Sw`P9Wda^|8fHcyQ&KFBNFFB#S>;gbuTV<9V^yf{vb()OL%vSAwTh~dCdTFK9AKMCK(!J= z&4p**&?_(5bc$){*`+wIXLUuNs<4|vs|TO!ztz6EBVWu#VILO1*Mg}Dw&*UY`n3tF_`%Ll$64x#FDJy z4g*U`onME!qcfTIYAl7o9GI^@v6elD17CWuQ`f9o%KMWEE%Wc$XWwiJ$C+&ZJ7NBp zbPlX=+&HOyJ37OA(G{{ga*57yGHnugN_0|-@CHBo#Kkwnr{WHj4{XS(8h71@G1m{a zuMer{Q{7=qhx{&sOM}=8bd>+0`cCy{RYaC|8(Z7$qS5U-p3}Fms%uX9sr@>i*{d+^ zEWQIW%-o&ezbwMcOYl#`_Th_Td+nDQGc>Y$^8|Jdnq)E0-TE{!g~>yGnurM|tU+%K zlJ3M#sLM8V*^C`xZ0XCHa?D{q$~8qxQ>B6UPKON4aH;IxSc-dd^l61%iz^xuVohdw zVn%*$Ze~$JT5(ZYw~P8Vp4OOU-)533ipnbzv$GPD+l@A{^~2kRN2Nq&kr5)oUN`m8JB1$`@hR4PP`(g`X3kNn9nc~`Zr`5JQy?sdi z8SR7H(@L0%&Y?fQ5Jm{oeg_EXv2&g%8oCnK|S)8qZW zzw53W%^Us!;ts_%p4uhr5<4ohT}h((6Z4?2P98gH;G{8=v4=WS!c0og{h@2!ZDk>- z>@e#|CAMa5eCL#!zGpRdp3tu*?Y897hQgXY#gooAbsbLW+b%7&y3hDJ|3{`>Y1gvE z!U1*u&xFzFkMgWg-&yut)kR`WFw-NA5$rXH_-(Bn`|+jC?|%5C7Zx7=+CCvZ^kgbDuLzp}S>5LzSDe$cC*eq~9w z5U0%4e^FkcS>Qk8>Ad>}4(L7Lp~rhp8CcTqw#h%w=`?oi?c>Io8I64#yI+#scxu;S z7u3IM-_~*sqlLlFz95|mcXw_!Y4%?jE%>Z_=ZKw6y^Fn7&dhkaY#~#*J~pz@^>xgR z;-{ttbeK7{ASSnc#a%Zi)wat?$}1^~ey6Y^A-Q^RUH3s1@#X$X)9ep2&0%8;YqKs2 zPU})zRFfXbZXfMJimEdTyHsa}#Q0cscb2(r*sz7ur^BHq}Z3Y*D2vv#de*4>J4U^Y-Do5x*|AKl!$Zz;?+k6@iUO|8E4gZfsC5{}Nt!3o8qW zi~T3D9n?2|th7l2{7~lgz4E*Sxd{+{?LGeI?39-6b_yS`Z#iP_nKkRpQKN)@}3}VM!eYIGANLo-*8z`~@7(P5n z(th!Ffp7fM<|w4ip0Tv~lY9R>G4isPXGTo=!+q~v5;6Uk64$biEfvIFBR*J%urhxJ zPm0CLh-9W!pLU3}@#QqWvgEyXk=#kDyzGIu{R?+J^ZSrH{<8CqGtO9e28aKPmF6w~ za?``#ZxsTJ_P^4swG$Mc(oH(t)mQkBm`AQOkNPJ+$n?1(mEWx8d8YStrO`a%zwt)R zeKomLyxgViqG1}$oukdr(WCq?k21rq9%F`$VG-Z3vHn-2of@q=(hne{I!UCqw9T_E zUz;#xe3DyUk&|Cuo|otPl)1Dhzo4)vKd;z7t0=Fas5n2b$gRb_;J4kyBN3?5a|)JI z%p+#%O*efFwo-aWN*|`Fgi8t&YbBky73Xa}&p*e_(9zEL@x~h^i`Kk9c_u(@C|%4i z7`R3L@699q@4Y1Uk_TUS?H8$vNkqR8Y}QKQBrNLx!fP))NLr;=JT;l%_Zbs+jS{Iq!%gNy^ef^Ke{MhqSDASvl=8Guu~On;xDr@U-^bCUPPsL|kjVlTF@XKwSK_NEd8#9g#46_kgN zId(8>Wz3ZWt~{l>!|7M{>wm>5?b@GuRiCVGwfULtdln|QXCt=Ujy?Q8*l`!P>|sIt z>z?o zSju9yP7R4lKbb-e3H619+?Ldf8`LeL-)k}HQ)41h3+>?1 z!xECGMn|R;wHzNcT%eZ1&>PXy*udzkdYO^dEh)V@Xt5o1zRP>oK?j;#`hK5N$`flw zUNv^?)gx;X%TMX^zW*Q0Hs&{+(YfKwP6f4{lV>Hw^f|ksaLky#<>h_Hj4A4LPVbn6 zS+@jdbQsZj;5qGiU%d0k4(Y+Sh&1v1)?PyItxpYQuWs3YTZTOeO>3GS`Frf&%gmQz zM9G(aFTY$QX)teOxB0RgcGEw9*=?b(Tl0J3><@Y8#8_qI$+Q0`YqC@86Xo%EesZFZ zQ`zgUnysrrgZE!#7L42Es3$^CWu#BS0w8~Ct>9wW#^+icRDU|~*9GHCGA161ar+4Uo zYF*>Re#KqSX($=o7!&_heuvz+f=;=)T?(@sMszG1(5*VvzOMb~^Dpku>CC|$v+{eM z)-dSonv@Q#4hd#dyVkdu=g)EUydA9G?C$kzv&Zg?xQDFK(WN|3Yn@#_>AuL&*A1t3 zDCj((u;J8>jT5Sven=@w;+$G!SN^Lzypkwu)J)g%Q+hQFtx76CrT3syiY|^V%Hb`D zxT4(L(s+OSU*5~j<6dEIZdrV9HPvZrY7~5Kn;MOAM`FJ+Cw>yv|FKs8JGC~8{}nx& zhx~uhn-!}g2s_e*n!};t}l%SViF`{eSh}f>fYm5ocD$nM7=5|hHP6X&x=kX5*UKH3};9)0^akZi{uqfuo z!@1}=v7Kx3s&m8djUcOe-`_R6JS(T)MX4iWyN;;L@7tKHrNwpSAsK5G>`APzS%=jq z^lM{jp&%I9BOP>qraZ5-G%v5DbZ1^^Nq%lwsXZyHAU``hzc4eiP<+`7i**V18P>b3 z8pDc$Pg`?eTv1(JQCVGWpHYaFR#ccTD|3?UQ}`mJ?0zP5w5d1oBB%G>Y}-4`Zr^;# z(7Ud)b{z- zGrDdHumBw+z$Hm*js;VpbS69|2#3dvo#Kk8HmhtvT_6yh`7MS^07FbbA`*vl~ z@nOC`443kf+trjtC4~C=!~_-CBjb|evl9w?mBuB-p>2CJ4<{`%-Sn_O*OsA+N-`Dp z^z)ena@%YJZ=W;1NaO`Lo)W`n#VU!Bo7vIt8xi5>Uebr#vOyOYK4PC#JSw&$F8#ac{d9!j_& zDK_|)tmsWW#-!Dbr+bt%DJGQ^$a|`oG%~Gahw_xr`;<>fQc`S(js?NV9cvT4voclnFez)~YC7)#VYAnvK zNgGqrJ~cNd?1NwvU0Re~lEIq1`0BXm*dSw56Xh@2;fj@q7Gu(6p5ZTtd%Sq~jvD@w z^H|=O`b+NPLjXhCaG<-VzYB%^kUZ!)%sVZo)JV8`fbY@P_;!4uRpOIukrsAp=X;JW zHj}LN%F7y$lt^2O9JRA+Q$Zo|K!D%fZR>wN31vrpjm9NITE9?4Fm78FT4axgT#2 zF~jWdJRK-@rZ0^z`hQ z{9c|6oo6!jDU^<4l1!+5-n3~lmI~-wb|up_WC5MYu8L6Bm9byZi<;Bbm?7|j3(3`n zA&j)$YgeQ3j=l%!-)J!41vFTQ@PNX1^-_q2X6IcOj5Xj*w2--Q;O@)PfdjDc69nDG!z5{ zvGZEf3Z$+4jP)346RbJbt=3I^rQ!tzo{doG&nY9;H%)}e(b>CRd zi+%5KUhbR0`B`5b=jVJ+ael=otFR}c&5U)0{S)VB?5j90w=d^>xqT<+XL-W_2=*U1 zzh*aazRtds^SO*-jdiho8Rz%xSz;+9e~bG9w;IRFwdCcvCAe2{i*V24p29tbdk`n_ z0Vio)ht**x*ok&B-vdiy&!Y@>-pOKqGRMxfi|tZo#menUyQ-BkLf<-mSoToHM+?Nz ze;Vr^=T^?=_`l|Sp)*JNgrx1bjktBh`RshVfcBR*HIgGaCvlAxlr+JLG7}S*p%dOn zd^z!X&tK^I^V?iM!u>$H`@MMHXV83ic>XP(e?yz=w)tjx@iRStn&%7Lw(01^6z;?} z_XWQR-t{=oAAvsz*9X@P*9q4iSK;|Zo-gstcPAomdY|@7jKYO@zGYbn#|V3V!U3*# zC2URj$@9PW{I#C{X`Abnqzj~f&x>E``HMaOMbDRXZPP#J#V=@cU&?*LyM74&KHOZ~ z?YNsgU#@54uJW!g#h;3sf;$^`8g5L&Fsj3mFf3t!d!5kB^BX&|^89N(|B4gW@t4Hk+M4b{FMg8epXvH?|D<%| zPsF!{|F6=;kMiIR@%(2z_!93r544Bw(G|=d)LIr z9m4IwZO3i&{B@o$@n7S_UjwX5akP8fN4O7g%RFDM-@v^b_k7&KxcPC9c>evKe~;(i z;rWv87SaXM-_ROA%ggWjFUQUD(z(|+a4qm>dj2#o-}$Zi-SQ`T*E2nT0=SLCjld1U z_3`{}p5Mvy1NYl=U4bjY<>1mhA34_RD9@Ms$aHHu%Z-mc=H2(`6nlW{HuQ?!-nF0{>nDj?{PnnZmAc)*z=Kd&HtR|FKBb!Hs2Fo{6n69pXUqQw&~y( zg?D?K`-0!i-t}zHzX~5-h=mtKo?@rq&c>bQ`C~j^;)me|c-OsR8-;$czx3;ftBEa( zE%5v-&rk9ExHi`jqzj}Ea^qwCo`1yi_j&0hU7PeVP2T;@ZSGTU%my$1Tg!_10=F8s z0=L}rmv}yP)%ZpD&*GlKJ%)P_cQ0;^=ilo2H+lZGo*zgj{uQ`D`b)g~7ycsMB=7o6 z{PDO^o<9V?A5Q!pxXv*hW2*2=JU?(Pejf1|F-b8o_+j)pgYa$3if+ao^87unukqUn zZ}j5V;eU-=6a7*22hq!--|+mGJ^%SO*9*BHNH^b$f5h|e_k6kEHr+jK;@iUiSLyEX z(%<6wH~b?1Ebn?II8Vc!kDG{_fE$M!fusJ>eLTOL=SzGiTzgyvuE_IqJYV9|aN;N6 zqHrNNE9zL(fnWH$EGuej)K76aQQt?c_54pgf2HTY=lPN@gmi)QOIza?d-+{|Yt&*d zoqN5MYk~iw=RfD=ThN-{E&mDc`bE!2{-f^0&Bfi0yV>(+d;V3PAGm)h*Hdv*aA)IA z^ZYTMKg{#x{s3GcT`w=b(evx@Yj99}U5k3o4EFb2V$vkc+5rhapy3BJ{G{f3lG#FImTCMljUUFy)i6Sx!}(!N(-*chrZ00N z#>h@Z#+2KPcX-2AAdl5tH);4LFD#hcB$!y=^InOun%H~N7@q--wO>lKR85ZhC8hnMq_kg>yX<-(a2Rg^=PS9( zo9En>ybOqE>X|W-Nsw`nVN|IxQm;DRoHo2!$9RgZ^cgXkj*O3tX>af3++EXGITJNr z(idyqV!n1}%!`_Gk*0i0Q@*TsU*u_sF%=pv*Oav~I-?ZEYbH!f>?CKtG%NA-T6(QQ z>!{Fb?N5pCDKHs3fvxWEClG*ay{n_7&lG-wEi_!EC63UT5fa0fP8ms=BY zk`%)@#@iAjaDJ7P+h6Y87hBU6B>T8-r4pYcxDd!<#g`NyD{Tm-bq(U@y#Q z6dDRWJ7|qM_{I@#?_18fGb1||xyjhpw9}e)(>mtMh|a+6)N~x*3laKDx!ts^ZW`{V z@Ef#Vy|qTev_{AZDSJyA+m^HSy2f?LoeMM!|5$hGDEm$wanF(3@fBg}LOk5US@`*WrQw~L_fCOEyzt&Ig*;3k zn?V{Lq~S8XTPovpQ>HDR?M&8i40%N-akhn46#M*Qwn!b z3KuJdOO;j~l~(sT&7|k)C+AY7*bJeA>7zVTt}rWQB#w*=&(v!!o|>H!!Wt{7LtmOdm4vA67l`_AC?6J=?e2= zg*n|fp1ZRYhrc+72u~N7Nd0{h)^nWlUXCw~m>exJN8`I{{BUiH!Mq<~%s5{%=fRq0 zuzfG#v6`l@<{hb}^wqo*HO)i~AJ*_;4Np^??_i$On9G@AG4|W`0}|HrRIOc4t=&|c z-Am2ITCb^IjlLyjY7i1+rfN+u_eqT|)S6zY)ccJ{vHhx)MXA&jCph1xFt1i@f2Y{q zquBmV(wN^VZmjem>}dE@4gW#mtk-jkZpVkRQ_UG!0MF@Koi)v$XW7 z+SXI`?o_?Y^ei%673Xu5 z^UqPt&sEINQJg0#&gUr37b&)9DYoZmDd+f>fX}%>m7LGf8cp?mBVm_HT9-d-S?9QC zO0=0*HI_)o4C`I|_nALk&bps?YcmTK2CzTIBz}F&6n=fp1^oJ%Y5e+|OZW{im+>2D zt}<8i%GXW&hOkG|&3ya&cl?H#JNOMZbNG#5`N7?0r1=NGQRZ9olNoKcnH^?=*<<#x zn{O%@EX6Ixy@-1bw*dD9PB=j*J{Na8?q=LEjp5kFc|lxNv73F%iyg)<6X_L!RQiv~butj{gjm&K~Ee#5+H8 zs#2QpgxC4f;>dNl$&KBA#&iRg?-zs&M@hl4} zIxC$W&Ne8T$U7j_lpH24>U=?-WS8!;vK&c^rd8h*x;T$=9l&>mhF!c?Nx1cp7K$d_ zL3Utxi*TOip=Qn-@PK>Be)9}Sp!v(P@4WMa^P{tq^y`V)3N4DDZ!@^XIombe&(2S@ zU$*lZb)v6ues;D9{lUF~wasF6a5$fH%9JM-9A&st)&w;>A1jx=q_Og>+j4%8u*VNV zQ?FHslNJI;=w0coA)GEZ?RHQ*$+QV5vj5nMi_g@tCwl(aRJ_MxwolW6B1 zU?Z&-3C>?B7pVkkdYA6U!AkUIF!awL^?rrA4=A!G1k2S5Gj*ns-f=j6;0xw0vy^&; z5?9B&$&IFw@GwR%!Q3Bi1(9a7Ws~j&%SYO`$S+S-wdZJ*_sOju?Hj+gMtVKR7Awlx zgidVX`+aaMvcFHme-rvTt2ORB^w55(h4Z$v3cCI5Y$o*&u zalUc>;VdI0?=mHE_apcLE>X(7M+u+O9zNovL|0Ni2aj(^u>yUwL}Hb5jzEPFG}JrV z`kxS93lD}XWKJSe%IjW)w(y%kY<4zC6fNi0-g4eYG!{-r5|F8V_`A9A(N?RK z4-QegpMcSVyoh8RRH&|IKwm(a{pglBVnr)(0H@j6Byiv+_z;+)$@V&|X>q=A)@vH% z8Eq??lLJ49687T`C|wS~i*PaN_ke?FF41oBsxzeb{u+unxV`9T)l{UBr`=L}AGrJ^ zI)OU;3=bwjIpIm+-lN1Z2GTMQfu%?;bjM!U$GbU_{~PKjbeC_$i@uZ{Cq!y&@+Xl~ zJAiNx)u=F)QL4=wd`C>2Hh z4^g{~&St0}GTBTi-l6FXJR@&f1zME!?rrqVZs}=}UD33Ao$ryiFW^+kFD>XmU2Fxp z0=o7@dewSzzSj~xJ*K)>a_xdjvZtEhe$=xc(9oLpIJ|y@*4u)Hz-9saW0bfJ-Mdpu z+$c4W0?5hQ1!@q3=Cjc;yjw09aRQH=ujEhPOc*;dh}u+7>%-9Tbt!7HR_o=}X9q_2S`v zPbNnhIH{K%Q!O7W5m9DI=g+3 z)LPmoz+YRnW+MIcof50HJqSIai)2;KI0}s;=n>%nsUNX>pbpUCx}$1u!f9N!K-*Z_ zK>3{%`c+!FbK(&Afx`_e)Ie0LFVq7f8V~r$k{+t%pqkX6NhpiI7e9#dqoee z&oR!j3XC2RjUuhl1O)5>OPdww z5NRp;6}Upmwcy0v%BP~=<$AT2F8cjvu)$W;SgaH7xpZgN3%)rheL0u~YPSMB4~#k%@y+nqGa&xWq@l^*Y#o%bho!b@1(XRu=wF;YQ?SAHCT- z&U^HU>CWGY7wxwMz4fJn$U9Dvh432JQh@xKXtizKrM6 z;aTYoMKc{k0@W@jEb@B{9FSF&Q}~&fJwOmy6%_xEJaj zu_c60Tuq`Jqw(%38!r!j94YN%)#9Xfy+k$`bL;4wbL*iza15S2a(tfm#P5)u)Y{Ri8J@42Y*^q_5XrSdu8THhNEcK(Q%W6nFHnrf~Zv;Yutc zg_GhIL&#g99xFWwZs|TV3%QZ~?lzv4L|ve2c|^y(s;6 zmPRLW7M{=&+Cb8qI**bKt=mcctd6)eT%k&<^uX!Zoh}^F0FEm~t+b>S(b~tc-vXQ) zNz7rEFcUq2wTlEau^kbm!rw`nwR>W4qTgaC{YCkfAq=9WL?XU$LG8iEUZ4Pj~LvZ3K zWG}Kr2{4oOy|aO9n;EVjY3T+uL@Jnk&rRdJ&)L;UhP;d#XcuXb?Qn^-^iR^7k`jIj z19qbJ>WQ>UD1Bd~vyyu!EcbG;g7uchKTu)d)uJC__p9*kWLw(%*NzkX$z3U35AB;|%p{ruOgY?oDNpIOp1@yrs8m}_N&p}1 zNcX;S-!a-x<|e?#r5iPM`zHECm4*{=rEkJ2cH;whlg}nrMu^Nj=vjI~!nD7n6{}Fk zY}(p3WBBdFNLz^=oh-O$A1?ea^n)j`clOJkW?-@#+fd%j6`c~09LXW|GQt(`m3&W5 zI7()IWR66}LdT)C52=t=JVZDJx_?6|SLT>q)4Iv^cZ#c8uw2R98)0}apb;(RH)R~g zh#z128MqtxB5QKQPzSL~1aIln9c-*!vlC4SsSpQ^@{h2Xc2i%Aa7^r%qD3MGe03*6_!4a8ayp4pFXvAvdWLc>Hb=G*F03e5eqx@ZhJbd*6^z!r%72rJhL zKgc2T_tO3{f4`M$q1GOxQZdnM;R?C(xPz;mv^Z~Y5EExra@|bpOTQ`HC3BHBxQI<4 zv)^L3#Gz4-sl_S0+CrSpOslqQPtEs=MFtcD&BERKa=lU@0?*}M*ZQPx#0pHoDUI65 zxJ70zMOH;e?he10rZvUI7dk>cXoI*+D%&bP&*Mg z(rXBZRe;ext)b8l`_{!B`tT(K^i?o;$pPIpJWr$viA18{-y-3DtiP9>zrZWIREKZY zIE!bze{}ArjM-4JBQp+ffOWj{Cw?o1L%^VhyYdAc!2}KJTuCWcQ=0_onVGYbUe(cO zz+X#B^z(1P#xIsUzUY}v&h?shHrim8^QqLCdrdrPmD$#}owrm&y~&K&xAcMOVCWZV zWbRi+yZ>O8<0#JqpM>+|dCx*@#qR|Ic4La-Wt_k290yN7Nxc9KQ3B1QkmUEs;W+ap zF)-!4R5S#44)A1oq3kck{l)0lPq``c&|yezByV`iST%%HxnSUOkjyYjAJ4OC@~@&+ zJH+0EPdB>~OZY|Hk665$RrcRfE%F_h)3ZtpcYk)<7~cL~>^zNof%sLLMq!F^sdxGv0VU-1icpA97CCUEz?twPZL-d`+dXmem zEAjj4)h|!CiDo5o6!f&Lvt-w5>ogoZe$sJj>y?Dgz|GN^nVdz+uaRCw_UW_6diW4T zp9t(<98gSp6VdW=hz33;F2K3e{h)H2&en6azg1H92$kGJ`bcE%xVBx}QruBmfCho& zfn2UM>}kbOAP6ssedCQtxMqwGf6Ez8(yVdxaKcq@a`(8(6)gl_JduOPpCOI6mV}V2 z_3lT4pLC^M$73Nm}cwPg=iCTdry zO}1akAcgS1v_U!&FSxo?&d`2LdmXXS1d^QHz7${XAYqhsL`nh!i{3?AGDvd@=R|4h z5Jolf1XS=&=UybbAzi}EMMqJ0>BCro1fCh_5JvozB=3m|&4Y+JK%1;Vu6foWr3fBS zS!^EHp502RVsQ)4x%}roT`!ds9#3fNc{Ot(z@4-_+DQ3L`f<_V@`T;h2HtZUs1vH~ zq;?$lWTk@G#9I{?;c?M1K|)2aXhwSM(taH;kAK0mENt_VhPD1m@2lkwj9C* znp%8;Ejht)8(6CS3~!)yrPt!D67Shw$FK<)n;;k4z>ryC`kuX{&7!V;`g<9B#n5&< zc_(Wjz36eJDWeH^JeJ5uTQC z;7~q950WDr+}S34dL#6ttU_V7 z4(#%<$Xsjm3EJmlfTbhF?yRCn(r3ho9UzDFT-jI?N5EZlj*Gkd{9>sjP+Z-!rxRZ{SE9nME-{o^jJh&0qR z7Q00IXW~!vO|0$la*0%;-9!okX9;n*J+icn$NMfHlS(92ED^EBI5gz0I6LG@hQzoB zcf+(D0y)HfC)_$Blw_f3L(wUmRZEJ*|0LSNg#k>75sFCL9)ZU~=u=q#&6RwiCktO@ zrsxqQ2h zS*%FWo_YvBNV^}=`FFAR#U@ZIO4>~zK>2-~_aXU~P!K9ID!9m(oPFs%ko^ zxzyPQd}&o?F@S*d0^x|(VW(HqE!6r59Ez@^MS+a(!a0m4meJ}4a!UKMo&?MMfY&!L zj+eG0_kQTIAMLYAVH`vP#r~8di+;+L`}eSHw-Xj^wVz&2G)_3Ywt`gp%}L<9{BVT) z$Dp&&znPS18G5(_YAwmSTX_N+rt(DVY1^?L0w^K0hOGIJl6_#34CR|CPoT=&ggfSY zh59&*jEw4wBJRNviiHNfo{q5dIU|qn@IUj$eD7<&tEu$lj`==S>qj*qmU@f!0j@o? zg;XvtX%C>b%W|b`0Z@g)+4wIiKQi)g^((%Npgxy+Km)P4WUg4&GRpcpcXTB+jDSWi zU+B|#ku>rg7jET{uV4xPg)ln&MCbt3e^YAYF+uM#an`tQK62?!RPktu05WmFizb#97AOZ*|?quipQkiqz)_GS~4BItGwF|;kU@C{9l%b$Ny)f{d-7a0os~qgCM1qEKbs|on^!%d zf@_($)Qj+JF&Q~@V14{izH37N>~b1BD&=j^{5$lF&gN*)v(Q^*59~_BHdnfr3Z2pN zqB}*7WxkI?#u(jxb* ztsdstd7@!zn6o=BZy0djwd-8l&E=V0lHHwA3#|PR`$5NLJE^hsAy`N{_ayzdXSaZl zd+1vf=rdgx>&4;0EwWRs)KBK8V&IujvFW)ESAImcmH!0ia1UFVl@{yOJt7EuEAv|K zi@p|)b`RvH_3-eL-q)k5)B<^vL)wi)`Z1X&V=N?>|M!R2JAk1}X?X`m-gr4lA_1${ znNrU{O1CV@VO7Lwl9L7$RKbq%8STW8je53cVYYN6H9EUR`sbwbgaq z)KL2<TnhS&uH^MI>`TBXuW+U6`H7AWy?o2dVlpkgWd;RjEW zlq0}P+R!Jx0Y^4hvi@Ax^>Ur66r+C-%g^OkRyP(~dE65@{Q*A(Z6wxDF}t;8qbFkY z8AA+Krh~kTwv(0cFjDRcSW5qyj!_Cx4@3(kmd3AukuC(ZiM$+scmCj@-EZ#EI{PbZ^Rk&3~ zGfBwB4rUs5^83;4DM-E3(?n=txXkSXL|+EzAdp<$C;CooUJm!Jd*x~sEG&8J#?2+X zD9@7hfX78Xo8a`%DgB^Y<$Hwlz5DNRcRRK`&ru0|s$9Q~`4A3I(%gh6oz;f@%2{+$ znbm&L8|Df|Uzb&Gn zH2rt3tg5xPR!0U+|AfA37?0N4-deMV-G&WiKVMYtM!3x&mH$^zqdZ94Q?j@y17KsSR?w2 z=>PG{fKGzqI=jW0_n`F0YBO+fwS&hW+zse3T|K6KSU^KWc>M-A>N8sUuE2p^6s20< zuWQ<&gVc_bNG!URE8#VH`fx&;(U-*R)R`F3S;#c}3irqnkT$qZSkUyj$l;XayI_Kc7V-18>Fqa|{#yqZDCyC{d61;c5z*$y^9HIA8-KIA7 zPvmjyna2E#*sIuDI{(9}G6A477xHF&q`s%DQJj>=y$ctEjdB&xk=`9`LAAidhDg>F zp9Jo44Ia>NCuey>_XxZqJ-j>TB5#UEp!qiwr*9aOE{l{%AzD}aIzp~A%exLLgTSW7 zfhVwDNoxD*m_;L?mGBm&a`xI-Z&d5qm0VZXY4731x;&=2$S3`}jSXekFUsOe_1*D% z@$1H_{r+4HWYv9dYq&LzC(+~C1z`kxKFs2|^0n;8e5v)5wb5E@ZQ=KgwawbjPB={_ zi(Q^__(hvMK9>+<>P;6@V7jvJLn%8w3?Xg=yE)XEF=i~E0D7EX7c#fejcac4^w9bYa_a;1$U%54i`Wk9Fg12l&vd6Ze z<`Ff+_pu!Boms*z-z5{_6Mg*s{Hm-6*k3rt zdfb{1w>)V*gXBKTo)uBnOYB||V!h5U%6fxei1jAFaBGS67F@NIU!?W6wT$pP?6r|% zy~nRJJ8pbH%!lm170JHhYY2bNFVgygUxf7~zew6;E#LO}iZ)5LzT+3pektG6sz2~c zu-5SlqYc*+zky#r+VW@OH}dPREt)`!ZX;$pzX4X0wU?Cp_zhz3FL{1&%woR=tJ&CA zwDFk;-aw8ttY|V(CWdgVi6tCon8Psf>>&i){YR*Fe7$;70X6k<|MD&aKv zCedWG<5w8GlV@d_d{fK&%<_iwnZiR>lD_AB7M#?_ie+Ds0ahCPFxVO`TxDglf5=da zcc6I-dV;aNH=;$a~SJzF0rZf@thNWV9~w@#NGNvs~`PIIT#P4*=r&0Xd$YrL6j=31R(j}mLBdBQwl zjfIcrS)M)=!mr*CMVY8IJAR(p8; zC9AJ_*}P)q!11qHBg|s6*cxfxFmG5L%o4MNZ&JN&-nNR&GPBGoHt(2s2roCw3BPOJ zwerk+<~^&IdEdNG_yhBS)!Te%R#=5d%qpwetTCTk`AE(IE5jT#2d%;8kU2#7usLGo zB1=cDp2*WNYoJJ!Rbxl7Q%M0legzhmJmnCH>Yx%8k3B@byE&ROKG7I_;-%JJ5@NYNzg0<`#r z$XyVU=gQu6N)y=&QrXK;*-KE_%SZNZp~T-=cOYqZB8MR=hxICli7JPkR1PCl4l|L% z*O6Y4I-g2i7E-s2zU>`;$?RwYWs$jet)41%SxDWNq!hX8qjHsvRDF+Bh(txHM1`tE zMX4-BsVpU{EJdkyj6#ZbqEmKRyMe!lUold(m$1myV3n(I>zH9YW) zDM~;pM1De%pH#R+WF|#)E!>lb%(O>l%D7u@YOFZZ4*3aH$w^h2iBy@%RcWbI+32UT z(N|@ook~TCN=2n;W%xk2d6;r@59Q|W%E!HwgNLJ~pMgh(kGm-E4p81LRo)${yjx^m zH?Jdm!ofx8@Hdek;oxfJ;2P!NYUSV><=`^q;Bw{QGUecM<=`r`{z|J+`M5zjxIj60 zh;ncN9DEpF7Y^>P96U%lxHD~Q)2GRPb-Umf%i349y=p_%%Bp4j-q7E|s`>opRNY)P zyJ}KZ=c<~jw90QQzo=YYxuSAu<(ZXzDn6`VzD4kn+VQEFlBPC->s!BqNn~U!)y1nRzqAQBdE*e+3x^Pb6_=2W_ zbp>A+tSNY|V1B{0g7XW`EErQzRgh6&+b|O^fRj7ZkfDc6Mx!*t*z~m``KIuy5-P(Pu{Yh&nB*CMqt{ANgS9 z%*eWkHzIC|C<%Wqd~W!(@bkk5g?$?~Dy$~7Idom&PBc zSE0$SK_Aswzv0&fJ@g0q;`^;XqZ1$I*9#5wI8f*D>w_M8lGT9E@#}|8@FM;1%lrmd zukjm*c3OhAT#6Mi6utC5<*(p3hQ5C_Yx}-H6OBU;{lF^0_4L$dpktcQo4fd3h=$pV z<+9&8fDS!Mk3HRL<~M`y<6^s@U81bZ&@S=TO!P{Mbp;wF&6;I0Or~`WeQ~~ZohdfO z)=lV=cGho92h+j2nI5^``YnBNSL=6JYu&Be*vo6Ib-Ov+%(VVwX44x!i~g8zEv4^! z##(Nkr*~T+mYlUpEIIc2e2ZS`bFt*C_1Y`_$j)9^A4v8=Ya`O^SX;FB*@Ja3&q@VW zq*>@3GS4|H%wlJ+dE2?qEOY*3-f8v$# zoYm$o=NmKE`N7=neA^1`Nvw>g7*H_20rFvm{1W@7yo`GVC#5WLj!@EkN_v!%7EscY z^s@2vv9a{9@$|1r=0oQ%W(Bjox%3H^^%bM!?an?^2d?$bQPb5qXoff~W~j5%40AS{ z;m+q~gyWczlsJm`(avjTjI+&*<$9j;rkU@&Y!*1%!Ds^*9WpOagV%tw*!c=W2fFQ(MDxff>*WR!6~Fo%H8P-->IIR-9% z@Hhw_he*B6TtvB3ohCEGS!XT-_G{c*?EFNzKeLm}Xs})klwj*xX9qA^fZ-6komieY zQ}Z2CbBlQqYQ4|H!4K5^=3=1D0H4>K1Ek&uMC^9Pd+FrbORk;dItroT!TWR z!0iD>)NfLP+aiZ4;Q)}gfYXoYfVGT-z9PL_izDFE3_gDFIba6k4*~0;;5kfc=N!kf zw7_jY^g9ZaAHnaSw&R7Q7g}C|KLb1kzj>sa5B{$~oyD}n658`^+F==a-@#vw{~`BQ z;CLg`9H5W;&d436z_G z99#x=uR)Q;&KgQRMyW?A?I5MGLm2hlL|wN~*Im?gJ9TZMq|br$1ub{LOhXc;b6@Im zDQPaFtUxT!O7JS?J8ff1cp0j|{ zXGovWc{F9;h0H8wRxuO`&V_=HBM~2%mz*!Li&M?J$n*QGX9`npW`S*D;vxqM|A4I#eof6zUg4G;TX|yKD| z8)?(c^e9{DD_9$4SWRi-py)hjA(%g7p2mO1S&qi+Ni7zdmvOJ)J|OKXaNG~&S;Y!x zZ(}n4Bc7KTW{=a*K!Qm<`ll@>f`woprME401YGeKcvwn$9yylMUZ;Li%1W=KKq<`p z8J^El+A4HO8f9OE1k8Yzmmwjyas3w5`2btn&|d6(^Q&n@&q}(bU>VAck`1*E0qHQ1 zc1k|;7b}7?jxk=3U`EQV(GF^pVU0&iOhv!E%+ssSn05XRdVf!Uy`Iz?sNIkBz8j(c zRz^ud@I(|Y1FkKG&-+lq9pE|7c?jA*M2%mErq4ms=fLG5YP^*i|47Np==sx}=b+;* zO55d?Qa}mC&@Ko5`mhzsU#Rsn&KuC~IIXk@h%W%~1rOpbAnu^G-lwH@0(m8)5yLu{ zH0KGR@6ft{x8f+cJwq?>JpN1cHE%hOwZZ8%s9|_xDuU+}k+>LK8qXgy&}M>XJ-$$A z1n*x6&hNo_-@~0A&QL^3IY?UzzOO63ix`J4b-Gi^2|V}14+r1{!S|R_<834-4eKZ$ zi2c#}BPeMq^My0$1K95b-aU-f5X8=6q3lE!iyU=FjvfL6+(HY7lPiK;>#&X3B~)`o zlfx!QEIE?kDL-wNPw(Q&Pd2H)po|vAb`~|+?X^HXwJD>G>#0o_AXwC>i8lRNY4$pl zUTS4io4ru|C#Zf1&3}kmtTruZT8CNXAoB!TaUN|oAM9SIw+fXq6xQSLW?bZ^qq%4*VS&_bzDk2D<4WyxxV@I6@uPGn2cX)^O|6NH5X@ zoO@zL_M>(BL(2i!i6eNQXFO@9A_Fphy@nZ3si*7+=kjz1czPf8ZBKo>!qeq&UM2KS zk-9_kEqn!To8e7KMuH)R=b_3&U6J7agr|YoZTPp-+INudaojVE8=t4G7BL?o68;t= z{iTcO5NXq*O%i~ z@a|RENq1j{uD?vdo7-r@e0ZQ9o{-jV6e?h&&!8o~hYy%>^`Pvftv=OqUbkY9hD{!{ zMU=Nl%gcxRh4Xhq%iUNb?^@T>68G?=?+?uDKEU2M*lm`)YxX{FEp71?ZLuBuTC9wC z885>>b@Vy))XME!H`AByLDO%dA3ccW^%?z3G4n0O^bxD!?T@MPYB2cN>mycR@1?PV zD<92Nfa`$<8o`rh(Nga*+x$It|9Uui1OAU#{2P(U&7|4NSa=Awz-;Gfb3L}+4bI=p zjn1p^*%R>D^YGcz=C|mLThREwgT4zG#fxTG#3=kN=Ur&Al2-bjk%~bsQ*Z<6Z>~i? zABT!hLg6J)_ifx~P&9`=vW_{mq40{>gkrIb!lD*iZY*3r4<4A0+`ItaEJh>yXb+K5 z3$Bs3zi261VkO)w`(S2JpF&(cZanT<>U|IO{R92;13X1sf;N61x1Es|D?ce^CpukL zs%JnE`d(5Tpj{4;>L4i&m^4z&V|HT!T45o*$4h7=Y)t6x!*1nU2ZZ0jUylDS?mgn) z$Nvz{iGudu!tFP}?X$4>uYu!xVewyud|rjMKMQOBI@L(a(MrOnvGnGC<_4p&-W;^~ zaWLFZ>fMyP8|}Rt8c4a3W*OfpTaM$o2xZx31+|FKcDoJR;&$5T_qaRI)OVt>?;<>x z_`7j35=cPXwLnFm8Ayu`;_eXm?l$LHa=uQ^XUO>naxNg}3*=l#&S#)O2-xaqhQ2gG zF%IahW?Jbul-VgQB(S-999u)I{^e#SoIH21gokqoG=qR`wrS|9!D&jPnog?q7f}cR%8e%qttLi~G?E zkJI~{z*$lV&O0@~lpy&xwL*Ui`4AYwH3CQ0TYrHZ>?YL#QoT+oPg9E)YT=M_Eh!H( z7Zn87VM@O>C#8G=o0m5z6xOd%_dW6pb+@6%GO$GIX@&9hz1PySU(ZvXdswgh2N}oF z@4kc${TgE<_I}`;fz)CblVhFL1-~n95N-%=ByJRLJZYxmUy8fdc>svt0CBztu@HzG z6k-T>7jWk-?tF#rKzemlwNuO5;+3_9vVNefEtIu|vbIpx7RuT}SzElawouj)AJ z1F-~%`+>Lz?n|ey%%Cr<(>`-G5Z427JrLIeaXk>%193eNKL#RCnQ&K7musCnfVu*x z_W|{J_+=MRzX9qu*edDNB!j21jAg)c^S`P2W}pUY{zo8g0OB)1Txvbw{1)xB7wxp$ zBq2HT;cd}MjGmC|4BD=cTGlIWg3rU`{+8TNl6wicZzlI1`j@XvKDq1R%=ygNy6wjJ z3thI~c^ZA1fh^bK#w)G<43sTE`5RDH0O7E8fL=>_s~kp0F6|D2!(MW~iRL@Od0&Bp zaNb&SyPP)<-Eo5Zwvl5VGZkmk!%sv{oJ-F)$ysWq(8r#SG+Y4u+0G;MMbFX~JxO2m zBK&kKebGYtqQ~fqmeCi@r!RU6+-K4ra$b*46kB5^?*_>AH+ueB&p+syw-<=tuIG<* z?nY)Kn#|j1y=6L+@*F*>=*xv>jq{%QoW5;8a(w_gM=|@cUpe}1^ye~q{pHRpq?UOW zN&OY6zasTkNsaBY35`ApY~~>Y^J%MvSV_DIPFXTW{gT{k$-Ru+?~waAxsQ{JH}1T0 zm?JroCynqwC^Rs#DIV9R{No8)*C*n7z#ZM1_NyU4LwbL;~f z8Ed{mj>Y6yLXMZo@k*Pzd`6Bn#uo0wk^dz~>6 zGsqV0WnoQPSW#B1Mfrcqy8quQt1|%L@4clKN(CHWly3?$g3BB(QOrb#8_~co3@r)s zr~LtD3I0G#>}X+|n>xnCh2XMZq~*%YoVYR9)!GT@SR1yuI5*L;Dn%@J1%%pGXeoA| z=e)PoWNDth_rC9Y&UwC{=RD`jUGMY#pdM@h&7cL8#P4l*Vn$m;7k%m}l0T(|(*@@= z$#0EI>O7qK{Stx|8zfZ)FTVx@zi;s~>%skK-{AhZX5`!r`mYr0@L--f;Dt(zis}Z< z*Je02On-*x&wx4DYvHm;?;nxgu(A5EXfJZAA_3HQkk=vayaryUU4LdHU9NV6!}lzF zN8uVeVU>?s^GGYN@eMVOsL6gIPujKD5?&tuUASD}Tl>Ws9lTnM?}CFtS8y!26`7V; zPhR!8r^BM8c4NT&-H&53!C~zl0g<&n?tZrz>?DZ&p=W*n2-xFRn5Bj5fb}%S6K35> z^>KXIZdAAfSDTHeqL15)Yb6%7FPkftUxqIq;Pgot-j>f>8)F~JZWhmfSrj=cf*ch+ z-j36^8@&Wpv0^BwK1v?3zIRc=`xSH?@g$Ao*jTqW<7B`%TO{_t+h4AyzMRGq*DJl4 zq;6y7Mk#R3*i)G;on#3;wBiHSP-aJc@TR5SE#YSPEHdJ^M#SJFeAWQ@S<{5FYH!?t zza2bJ6`ilqKfYhsFVezuk_*nq9?Jvd(++Feguhp^gCMAJUUK5$Iu^LatVQG4)8zS- zo_2?tcIs)L(9`bH(>|l8-L9wIrS_0&c%?WQppP!b2!9VMo7qtdj+a88#l8_yn7hW| z*!z8i>_gAGL2$EJu+I!7_EEwz_J=nZA;X(jB;2W48DuC2X5-oE{uY z8aGL+Q?)uvi!;>|7Bj|w$4K)Sh}A?^@lDh>t}%<%9eyc%!G2nE)Kzu?R-sRaSVony zEDCn!ci{esxN{!4=Q)C?zeld7*R?DY72J$EBi?O>#qZs(!SA8;WEyJ@N{*79)sgHf z&LMf#JE1+1Vd;T$FN&PAtKF)g-TIZxT9wdjzsHBi&3M_u#!58$Mc1|%QKYwLo9~;E zEGV~@1?sy~UH5r9f81iM8@YycOyPsBVI4~+tf4JiO-6UIfF=CQ5}Lo9<}auDdP1#! z-lA*a{R-TAWq;yKvfynD!G8HWEkaj!y!Y=Eaij}AH)e-rX zps|fSI$2P(JEy|=pmwfWYEjxFHk~PNwG0Ne72i-xpXxkWuFP!M)WW7VVG=d&(Aqw_ z@tkWL3Om^JA>8Za1G?0+EL$Z9vX&fonrYolx=+i3zXGSXVR78Gn_Nzq2|ei=`)|*> z7qj&P@Nv?nk+k2#13~+OM91X9qR7#+%@wXI>G&FP`s3vFq!mFWJlMxKh8JpsO$S~d zB#T3A=CJE)uH)FvNt_N%3%fZbS0`qj;CNVvb#vxLtLbHMJuBP`B45#_)()*6q`}r^ zsrf23*Q$9gy5oe5F&EVELbVl!cRq}MR+}XnSZxglufX{zaZ~1?Erl#-~5z~go zHM_PXt(wCl%*L@Ij@ikCmNE{+`5p~oIDATcdyl5ClvxQ2jyzoG6--k5ky)EbcEQu| zh%v|fSuOXFrM1M`pC?Zi8S`FaxtPPKr{NL*hyevZa z1~yyp$8=}DyDN-OSMg$=$~SjszcGjMl39dSdGR-l9^Ujh&OYtsF-Fxe$tdY+{83Y3 zJ^tsj(~+@!j7I)Q&>F^L*|nk(r_~i_|oObeH12=uK~0Pmec+N1M3JA@9Xy;tq0D^#D#o|O?gfbv%PnRfkOr_XN~HNI*7@+}#c_i?jF zl+-I*Z&pr*{iLk=*$VYBvpT{a_oKc9gNraYE^-hyEqF?0$Zgv0be8x9K*53s8vikV#nOtv1 zaPCujvj;bx!MT`k_z&GZjeGCX(l~c;iG1-27;R1-=(J46kW5CmJjiL;ly|ac@HVn{ zKjxht15b!ke!|+etLdk{?*Km|uO~%!&3N2qWOfiHhe4;7y3|uN2UA3IoaB@1>WDmd z_~q(!x6H!V=`-s2u5EN=3j4iGQkimDK6~Zr#7I8(3Y{!O>@FJ=XGF)kicPe3w>~v8 z@W=$R*yanQP(l{;INnBR>6vfA%78b+X&PU#PEJZqZ zC9Gt)$oM&Q<^KJYzUo1(gbuGG-FhX%^K7B}jj{_(W6!h1c6{0;a{X;(zTzNH`fHKv z3klb;?0X5%SaEViZ=X$!A@bNUeorZv!ytRBfmt!}X$#G38>|B$9^u-h!g1I#QvCrSO8Xt3-9oKja6D} zUDt#AKt0&#*#qDqpEsl7VbGxOXrvEKU?(kYPUER+JT*<@bGp(0HKlS)mjMl#| zmfp&;#-dM8mUWm_y=J_LPL_4qSoOOsD}LptSA98;ljoG_QSxr9?N*CC zd*pE1Z|?TOfEI#G1aQNtLfNOo+8%GU&Z=rMU%IREM$zVGwrbAxl@LqE9uQD zC0AQz{4ngJ{i@KLG`0;b`Vvb#Bab|g-2j#VYm&0Bq4o~f@CtXjuAymbUCrZV*8j*< z56~Pw1w7>QW?J6>8u^Fad|V6OJ2}$IqhX`&^V#d*i0}V4F6-v+PJ%v~Y5$!O?Vu6u zppo}ERvR;9{WR?o4iDmRKP$bcpDw_D2?%+t0UN<)&;UX%Pl4Tl?WOpmVks$pjF=`O z&qnfU(n@Y=eJria()#f}n~1qy^tlVzt%b9>^e>TFH9eX}cczG9W|2xfc@S6fKIVFF zOu_#Q<`?w|p=J66-|JIf9(}@I?Y~g*CWl$@FSY-&_Dh8y$}~R8%f#$w-rt|&A99O{ zo!fWQiUQd=+b3~6a_RjQ-1M3-2{}BHVv+e;uB)^hHWN|AkQT0}d5o?{w7Alp@avI} zY2szJvc4ipK1D@Fmo|B)@=a~i)i&9?Q_*k%-v3NI+b^aW=g*LGvRB@yhqpI;vyx1m z8}&zPjJ(75%%tb@QFRMFzlom4nHihV;a4Jkj!%!cvr|M<;OSphxNhI$vilLT@1J@|M(@;1eFp5_+|eS0Th1> z_?1-zRRW2~S%5;==mSWQAfW@g1Of_BVZz~o7=n)(fZ0GuK;tpN2*AT!Aqv1?A+(tS z9Krt*NmIDLMRKnYW3h;&)6e>O6@ZxP}1HX6J4m|>%SwF6S ze}9vbCNQtqzP2zS3oAcW(J7+uO(2h%#A?+NBbFIrl%Zo+&qk0G8CM0Wr@CuAEVtnC z%$LdJt|oFG9j_@@ET2z$eid2Ka$1B2XMWkYff`jUbPFYT>RLG}C z1xl}(6srF`WsW-GS0*~4BvxNg zW9*~{=l|09sB_lm8x8q)L_r)B$yxbvV-x>;2w2gbjcpBGvN<9Ox-c&JzTGaa{u|k_ zaWUM@sw3(ryyGe)Avvajf~U}z90Dx#vPDL- zeL^4R&z<7_)F{RhVg77a<*JTC+pj){n5$#x8EWU(TKBAg&9ViObAVl3XwueK0p z+E8mWq|`U%3X^JzH2A<`&TsEV9IF9GLA!F~$;-@EaHHO^BP&r)wm*=B7!aNqk0{!# zAlOYSK4J-IQ^|+WIsFpT%DLJ3vJ3$axnDGJzbY3-hZR&hN6a2u>#WivgGyh z+ec*q-|k`h%lCvJXP#T`1l(OTgLU^ONrmb}RdwiT>zn@#+K2^*P3*AJC19d)WAiMD zw{K5mSZKAARgI=lCVFNnq=7O$M3Ep6PMBlmH;Hztk%lOWW+IUWW!Pc;K&g{ex69#e z69UX7=LMASn75G76=>a#mOcdWtN*w4$B$9>GI%IhGhE`@1cj6osiQy@Gpyr*>hMZz zVP9T%bl?WS7^Hy+V}#HUw$9JSSM)hN&S*0a@5L>{+nNceD9ns3F{;p@a^=K0 zu-D>aX-y)d*;ofPxY@n?J$H-nTq=#|?SgIHy!`$BLyypV2h9p*F^H#~3I4qLjmwUYPhxI1Aw zXwH)N;y(Mro1^^B%h0j}02k1*kn{@HQBzSVnDe>UniwaVXCvGNmX|>wdSU~ufGU-Z z8ps+GT(E}Lnn_%;{yFrlWe(kR`k6D3o$+6!hBeVzXC|1#Owz;&W|N=`zn-FpVwYs^ zCg>rUp8Hi|)$a9K*QVp-g4PN{jgrG~+(3j;Kzt7figVtTPH;@#*)gs3r;zvSU0rdB z6*YDvbgOXOEB@7{ZrTDiDi{<3E&%o9`=ajQm$VOyx32QE!W4D}Du1E$5Lhv$pAIcKBKltitVoEOFdnK%)jo zbWN^g(l0;?ZCBG(VPyL8Tk=iL#rmvEEaVYtoF%<8WBSPGrftg7mr~3>N6|t)zodtc z5PnRs9#Az#%za83gX|7<-`~Bk&n|&E4iIqCOqbZNFf#@<4O749&k~t*Gi*8i+^9KakxbO@Ha^pU_zuRw812Wy( z+_wvW*$5YJ`P2n$hP9Gzz*=Ib2ZeloWIB;`kl4ut8Nwmh{0nWXIyFawLBj|~5ni;H zkmm;TCfS>+4#m1f1_~+t1y6grzUW;r)hAQ7-nLwaSQ9Cv>b7A{#NuZ|sr=OuJ2u4! zkw_#e_CoLc?xMST^}Cvri3lZ-kg!bR2T}+~lj8v{^c!_8JtJvWdoLGw8`LJkkjTYI z|G`oy-t1o-(7WZV{9LLl?mb8eWdrCj2%gt>*~9mE=1cd#mqr+4x16dlUN)t031G=! ze_)~T@plA7sJ28`;p-{&>C!!x6sSbR7OQy~~BQ5>j<7u7;W6ecjKoB-n0auiN;WL8U*wo61< zOq9561Q#(ucpUH?$Ss%- z6bD*8FLVenCU#+K#SQISAFc+7Bk)2yM{`?!DpEsQ8GFU%od%E?WoG;O62=Qah9LE@ z-2xqdxG9{`3)3cwI_~&wqyAR4f`oZlS`bw?K?~Mi`SaMZbh4nIA6t$mr7IfrO0OsQ zP5hv)cFlh8{u_fY4=j|gsw&B#BdwPK>Kp3(YjOA8W}oFo{})Vd@u$WG^s?*X2yX7| z_l|UN_N`abbC+;ucMI@L-pex&cqi`_!hO|#9%}zw`{VRR(`&sAV9kR_q1NAoy8cO= z3u3;VqqLe}6IhB92!+dv577TS*S;H6yZbrUoxkKY-=pD*iUPPURCw`uGJC!b+DaUy({v_UuEc_~g z-=jI*?BqNpwgR=}I^xmuzgFx&P~q2)4JH+ST6Tiw^)Pk%{$0`Je9umlulDYGzG)1i4yb)2 z>v+8KAFb5>RN)<@JnT}-&qWp0E>)plRLtDrD`u0H+sIMK(f1T<#9Yx|&>sNOV5z-v z_fh{&P4T;Lc)`%eaLLnkd!LQ_e^F1gmF(SBWGBe)mF{A5pB(IB2P)LzwwImzIhwil z;&@%I6S{4dyT1mobT7iqc#4m}PdSowFcwudRCeIN#ewbXNu!h5@hx_9$&D=DbvK<9 zs@p@cADzRF^~i3v8H2u}1P$q1yW9CV?cUY$$KL#@^Ll0=EC0G?@hmVi!tU&G zP#eYRO-^n)$Mh_Syk)GXgTHhWL&{GF=-aa4aV(<>3jf#(V;=(Ry*Km26OV(bN$qR5Ciq0kC z(9fk)0n!{9V(q|dEc4o(%<;aJ`&X%`dX^u8L!tVu*K)`1%E0T>vbm`Bn`IqIR*pHU zWX6@0q$~{+m9$VoYCc?)CZPzU64OkPYo--8N-lg93%of?uERYDVfS>9Z-M8IgZ>%6 z5AShH>TPWH2^S)yK1+L#(7D4|BLOqmq}+IgHysigl^l>Axib6Rc5dXSE9#)^(F50mnFGsBE>o_Jp1d6A@QO9*24kjAPHX=C-{(+( zTdE%8Z=^cE(r+b*2nu!nLtK4G7|PMreT>ApTpg@pk63wd{OQG_|1PVu)&rwT{DNe* zaR`YOs`jsrmTpQRVmStfNC0EdQCiqJj$#f+0}9D(!L3Q9m>1E=S}<}L+&ma!n3h&c zd%KyTp~o)P*=g&v<1n^j8^X~s7S0wchdWWSG)WqkeQ~d$(rX$W?dIQuFGj|=s&2$c zr@20R6(fRe>d$0dgYZ3NrJp22e&ry=gP!R4%x+^9TomQhQr5Q~j4QrbKwe+L6x1R2 z)Mm>M;A;bL=lCX^pP#V7oQ=3d(t$g1mYa*|2*rXJZY5L+?9Tt!d8*zY3Mfdx?>>@i zkM|_-i`4YGb&pPr8N9fZo;5|6H}r3duFJZ~-mAXLV`TqV0ONy~T#MG&T`wS+Iud5* zt}AM2ZzbqW1Vy*n3l{72?~CpqYe~ddW>qI-MPR`p2oB*Pe7tjPU*)ls-2Q4f&@T=An@HatmnlL>}C{Vw}ilfpw%@T<4== z0R9(3%NzbS?*4&hI}cVJMxhW`B%QAUbpVVf?kM*?gPvetc@1~aRq#cH?YA)n$go^f z5I2NH>hlJIg6=&Ify?a#N~~pALrDVc0Pm-ye`k(l7_#6Q74uZN zFy}h4(i^VWSN|R6^dY3Ybs2dXAswq3Qu`_V5e@oX=l(-5?NAi z8)0X~?UomS$Es+O$n2Nbb++?5+c~^9{2;o4Ru^M^$mQIkhpi~k^Xqn^A072?pbp5y zCGAqlf|L5wl}`R(xLCoZHMi;va%5GR5_3!P+nq|E4NY^8~R{FyD;zeM-Jzz zsyU&={?(K8Bddu_(e*_uVzW!mX>lsg>Dw>_jYROfI7I*YM~OT4#w$(Z%}T9#B}P=T zNp$ME@N`c{$spG)W@an6Y|0+TsGbpCynx`(s7f5xMzJ`SXM6?Uk(BQC zEyG=SO(~5_Zvu5u5E+)!Ak~@1@SBfb`$E#r3TD;@4qF}bjioLMEoF%Yeand0xxI`2 z7nKrg`9VQO2MIWp;;d3y_hiVhiRXk?xYDWj+!IZ%Z#*?;rpe)jj4F*KG*g0HkAxd{ zlX9Y7WJJ9Eb`v-_6nwCFD54zfcteKdu!gz16KgT7g4R-`w@5*AQ!uTSCm2EZ!QBKS zXNQ}UV}9jlk{&6Gk1%PAhEUKlIXYZgWnI zSKQA|TAzyra@Lh}ZW{c8+05ru?^OSb*kMU0GVaOIxnX(lD<;y z-7|p5?N7dv2pKGc*5d)5?W;_Y=|*wC9yYvqWYYy=&Mtpr?MFs0RE^wC4#TWp4l=h$ zI=?V~glni7pB8c6GC>Kfkd~vnzf!+4c&|j{z`#Jh4YS*8nD8eV^!8+J-f#?#<_X;+ zZ0mv~3Ma0xMMs*_e&)5LDbg&3$r_uGTC5^;^_fJ1bpv&fBehStn;s5@`z0W{#NEcg z?K}}li)gC|&E1VM zG?u(W*v4p$(KP1DXCoWE;Y0|2sIgWYRcknz_4v=A$a+(v1-fs3T@Bzz{ zLsnljjg^7p0ydND z%iBbU6DL!N!!pRtg<(B@sTugKc_UCbg_sE+2+?ryMM>c0i)uW*i{$>4S<={gajLf0 z+>#lmdOL_41rv4heaUvTF5p8qarXR&FlK4!w*%Xq55*;2hiLiS!5b**`CtNLZPGNX-7Ln`k5!4iW+L z@qf@|$)KLFq^ahP^>l9@I;W9AvMrHlc2WymygGTov%HU4%DA)>q;`)wVjO>SGM;w~ z!B?22$|oJL; z56NOcP`7J^z;-K8f_7%dIk$lR+yD^Jt6B&&nkU>qAm_9BD<&x8cGeO0efSPAe2@6Q z)jeAQA2L1XK+6ra#y(*`n23#0Ln@ z+`e*ePyFLfhTi(Ys`d>hSliYd*T1^e;9k9JTU(WXU7HNqrY^p3qvcM%Jw0lL?-({j z9_=yC3|uOPb{1^vQTi0K1Ai5BdGOR`G27^JIj*KOu-oD*lZ$wzuoLP$*7mq_tXoga z3MG+yS_K_73g-;!(I&1s(Z3RFVnt#p5@e;l;RHJNsk2w_`8-=innI*gI5;TIrtwlI zBG*uQg@MVjCKof^lokii*Fz^3{OZWHVbmU8U@t@MDYIA@h+m$?`z`P0Uef4+nY7T) z69n%rbu#WZ7y~)QZwTY8`1Xuott!kvj(5bV!~P|kj-)2|ai%CPHM#e* z@{`|+U|!aQFkPD6#=tR7t*?!Rz?fm5xL4di;NVD^mu6y;1X?N0)t=#N z@vyUn_Yt5ZP9bJs5uPE#f?*DdnkHp!SQE&=V%}*43e9j~N+C6?)d^*ENYv!JN@mme zbY=!3{OF>I&|GNRCyxySoAQ~oO0ih6nqa+GGFwP%&pPu+VJ~U=g{^Z<;r;^c)z0@m z81icxc<^1^?`aNWJdt1Yo(m~2ol3xYrG0D04~^S*IUENUBpUwIOoPDWtAY>JJ~kYS zE$4c_!@q-lesZ^LdLMhLfvwlQ+&ZeLvZMkfW0owQ?crz)5F7PneAQXyp8=aJ+G@Yojc4^J9Fr7MSn)AiX8#qXcXb} zU81wS{Uc9l4kxrqO=Z-Rwpo$kgBT4qlp8&qvqV-F-o9m24)Cn@n%k%GJh!G8;YsOB z07#Ob5*w-u^$Gh6GPRoZG2}hqtE(?8);ZSNw?NaFy=k)|O~3F24z$6YI=O?Q*5+v60CeIK0ecwyDl2Kgho;C0Q)X6=2!EzZ+|{8x0x|iwxrm}h_0b;;_b!v6 z#wSE11UR>}y=_WG?qS_=KMx|U(_5sk25Y)7PYAIzun0jWc!>wBEyS9^o{Z#@ST`YceepH74T zz8^A~!80GKIyq0nq zc6dEN=ODxZ7L1AzDrg!g$!r1%XOd`U+XEchvcHgFlbsM@7+I_`27Mz*(#Z3i$+M1h zT7ve{Nl6_f`S0WdOl4?cVde7cM5e^25bfrdQBFv&8oN&q-=`Qra?fU2W6!E^-YxT^ z;Ev6{;I2eCb;HTCLxiO+fPMKX7cT|k!Ex8b!2|(m&ojSuz5~YDP0o(kf;3+U8pQ&0 zF%wrOE|wy9ggUuKGGKpp#~Zhzsh%CSCI7D_85yA9!Us2FOa&R(MA4rK`#u`@+8Eda zmp~e_ozyc>Sr4SCAmJMh2FCw96vd1{iNH`$1Vbt{hAe9^fPMp~-SQ3}AJzDe%EbeKuUE(i{7L={?}QCuEeou%3)%hBbS zD1kOPNCmS15YRwNI91URKK3B#`aoO&5u&Y5z^K5bSIsFuL0o{Iv?FcM7L}~=C8O#w zoI>AR_(Nqud}D)hW6NC98q(Sl{G_+ACp8)rhOL?Hkl!XeoVVyP}AyBW=Gv-!(nN8e^{6-b0|J6zT49<(JA^{txa zij@|7|wmy7KOo1os|CCSsZ6Uj>*s>}a7$qN~`@<5>*JZ`-leJ-X&06y9 z3qH}KD<;^oPnoy)Cizn$4O-e--LhGsgiXV^Ph>W1j&$!DheLqc$c-HN2iCSr4bG%7 zjaRE5fj&}CK4$9O4KbM@AP#;ZfH(HP3v#2TT>YXizD7N!t)u}{92=ZitFTxv)ye@I zoz8TQR;VUatcEWsbu6br^zW|DrI4kFL)Ks6%?7}anUuWiF?<>9m8RI|Z-onh-AJv? z_t(BnaLd8~C~JyQ8>6Q}q*i_1e0g?cb2Zl577o zBiq=uWL;t4B9TFC2@PqfcR1EWGO3sYE<{Nh&Di9fq2;Ytdu{e6G}pkNG;KscI&`6p z=EH&tpLsZp+SFG)ESJUF&ZJ4M@u8McwZo8?VsZ zYWbV*4?0bBKBTRiQ=gfd+?3(ikGcnT9HTY2VYQ<=(aD?lDOuyvCiT0w&!fk)+Aly4 znU6QE&vDhXr=Gg8pM}^u9i#Lf*IpMQ!*=aFR@!ATgYcWm(cfPQa17o|$hNQsMCpt2 zH~cG7<9dvqGn#8lgG!ih;>LM6?QrN3-&E;t(UBOCjEb$9ZT1|9`o0k5zwLb9`-#SqB9gG5Y3g4;{KO>2n2<*XO zm%W#oU5>e`+5fodDO0ozT&n{IU6vx}jF*nMIlVDnzn2lYv;$cmiUGVG_(wC58bU4I zSr1;lYVWBLK#mf+#v0RUjqyeVgQi@s6E9KVlYF$0Q*XY7h;^HD>(^m1KNCv1bj2Re zwL%tmzRM3-TP4(&2(fF@z?#msqGfRRY&A^(+Ts$m4<~!OeDz6w9l3A6-C19Y2C5YD zD9sa(lzos{VUbf&)$F&ZLd#p5PqceUfhkSzik*GR+;&>?Ep7Wqe76dF3qSuMaQAT< zU($c|ka3vfMoQn599JaQoi}f~@44z{A3g6P^u*yBdwO@?Rld6|ZBrNWbrxZQgrJ1S zW3@zU285Ubg^tt=!-IXfxT#^?%hK{O6cZcmh4lPxneng|?lPsb!~4`C++|da$yroZ ztI@pmM_r<;OK*txaQirk_^z#H1)|;OYdbK+N{g|4*1zY1VW%O86*Ca?gi{vh+pg;$ zbou*y#VZ@W$%73^8P5LW zv^M8MEp>3~)U%b9X5i*7|A>@+SFS_7ar!K6zKDTb!S|9hVU@6};PdvPS#Ik0lJ}&F z83`lJE{fFiT>E>nd+=Ja*6HH9#oE~{uUiRv%!r2$*RVY+)+KGA5^vuvgiBbhTrDPn z^Uu9MN1y0I8gIc08s3x>?&u(7IMJSs2dz=?g7AIFQ>462$X@);5R+V84xK_98i@TB zH0ss;f3Ej+dqTlV>tT+bi+KWmkkDhfzT!kXWIx`34pbbw66nQQ7Q@*~l`DgWxEiEO z{Zy~(t2YQj$sgW&lSSO$;&JbyDYGek))XK~jItxaHH3yw&&~&KIA= zf8Mo*a-_|6vfWB!bGAehz}`GqC!87%QwZvEL+4fa`!4Fcuf0yni3=B{?5Nee@-Ere z;zv&py^&^LF`7LWBpH|Mfc40k<@<*mIh51~%Ad@ik+JaX;uZk%VluS5lCX3eQ(qj1 z&w>OuXnJzE%?vLKNpqk$SEg%SxUBm?&%1?US>-ZJ94sy_2J>|Q6UFSxWh=+}&nfgR zSeDy`!-q5wzg#sVjV(14OEPNkJT|hp*kaykMRwy3R7W8VQ z-uh!k7Vo5kkB!3sU&1&_wwCi{8_@%_&tDanW+#eQMs$tZcyZA?Ka(V9kL$@AQr(tG zGhqcB%bhqpeVbvpJ9SfM_dHsxFB#8?)4+k!UvioYeg~VJcQ)(eCHRa#1ED9yUnrFG zn?rYRXiwrB4z(K7`Q+)W7BxX1#Cyc`-E)pXtvf4K0w_TSJpewPjSh0ZnX-C0hT5tr zb0GhRrZ}{H1!?O`?wYw#-q;bD9e7S$466^k3o_k`U>vi0r*R~`O`BhB=KlvD}Zxd#<67&@!M=@ zmnWBEke!h^2#I&^ckFtPs5rk3Rg+;&lo14vO{Sp8(wQwN6O_A79?Xcw%cSl)3Dk%T z{g9#-Bm?qVcCgtXFay-#C)fMq1Wo_EFB@%t$0Un{XG0GXwAr@PX$QG348?&L9MwHs zeJdWvg}ODDa3RGm1-=We?obps7H*Cjk5j90Qk!kkWS;1Vcjym#f8$zt^((`kzhBPt zi8Fsbs^fey|GMa{?W>;q31wjP5lSqI;BQ9yGArrCU-D}ef96P7WKK*6CYXF+&k|oC z_$-e>*}3elur`BXl6MUUu#|JL`gmtXDM{-20O^_vrOBWii|X`;T_-{%G$@NRx@@&h z=wJgT%mPL%NcE;OAxSO^*e_Y!X|YYBZJmjl6!H}7ofs+vC`HXPO40#9ibKj5-y@P) zS>b?_yZuupiWjD}9tSUlQQmlN$}Dht9P!lQpQ-`?bdSd$x>c;wX#uAII5eQT09oV* z0#BIP=s#YFZ6J%3e*gA(pCf8DBzY^4u}A%zVDpy0{jbB|wfy5aiSxGf@vP{%I1|FE zj<9?qOnE6V+y+EZ`n7p^aNF>7(yeo0Tb4Q0 zssfj-!!{(%+jO}MUYUenuSgu?Ra=K}Nw7IhxkrT$!SZOzo`rmu;~Y2s4$s{dZaX0? z+yYMu$3?iD`{%lczHCcptLs6%Zqjq)_!2*M^=)|GftJMm5H8k)wcPaz_jn@td}5|& zbcn|LyP+yEu>8ToM3165Ka2;x3#JH8h)@6vsHkunk5Gp1_%~swWE)176WY1f-!zzX zsJRe&*DZs`<8gmH(9~-GZE>dHd^f`=;e%{s0GpQ{X&z!OI+OteXKqCm^x%E|G5_C` z1IdTP@DdbhB3FS<_7e&X&wQ?!o18|hy1C_O1TxdnuP3<&W+lS<3ZP*l4V2CPK_Cz% zA+^7j^RCb=*6?7<#NAY+q018M>Gc&+!?w@nrs)UC#p_hFkdw2@Qx4hXQ>&C+RBLVO z3R~7KTH}9i6iUV${)U<(=xr{k9-;wbMX4m>Q41ro>=vjp;+$^L2mRpQ7A0nQ%HBf(=mN4y^NyzCIjJ#*nMe z9x$k<`mC!%7z+l1rvzbbsk71HFt^(GIXHkDBYp{VJb@oD8k43VAbG~ui$x#MXyP;^ z+AsH5&M8otf}3HNVr=-FRO=)w*UznGXkr>@%gH77onCvx^h391b#%>x!@fVcJa>o7ERP3x%ERdrhcHv*1r;|97(E7sQ z*P*4Sn)AapbzRTHtQY>`Sz#W1@8@Y3KD3hIf>44Hsgy$s<=dD(tH|(z!L(jUh|okf zgL(Z*vK?ImQzw;oIxQ)I4>2wzjSa32k2i=Akx`7rb&#iu9qTrERmM_v->GpOS_lK9`H<9D>gz#r5d?cu$=b%9(Bh*xVh zH-nz%{32}4A=hx?e=OK@?JS%>hWwTgIzqqV49IR5zWNGfN$cyJ?d=}!AAyhsidMr@ zzDDwM81>9T<%0763wv?A4GD#lQ~<{Ht*Tx`Z3;;(OtLaNn#I-qb>A`C?b2?O3pV?> z6K(m*MicHru#HJx-mcB7k9fUq6{l7&r&N=)s5U6a4o;6g#YJxouLG=1^xuOhS!#88 zrq;t)T#xFqTJ@}sx=Hz&Z6i^N)S@mFo>33c!Qk|t7(|;QK(knMU zhMs1>C#G|s>o#Ir8(OAa`cuUb?pB^-(@arUEK-@g;l?I6cW8&D4aiq`xJfN=QL;ab zV@JtFPT!MeW4^I4eovZe+pP-W=>cQbe$0`h4!L*P6g17ciuD`QBbOBesWOV|nrIN} zkTKHEDHLe5igew=Y~SsNU&*GLsivBD7{9;oO@Nitjv^+*k<1u|gMGw!g?Sp0M%0SM;;DE_=~mp11PtI4>Tt{12TIao?e3>2eZXmzeec%} z{#46|(_5TC$#_)rBbtFBwMsC`$ir5{ z)ZyI!9@__Wre^XNPDLi%5*^Ucn6l$ zkgbGh+o?jUedD(ZiE=0+TmGLdc8BA3J z2YAa#sSijCi-qsIgz#5td;S`39&PNf%|Yos1nA0u5%J)C)ZJ;+Un1S8@qi7zx*p=a z^Qnt&fG&7Lg&?3uddT>sBZxD;P8hL-)t6a&;0?_END}Uk;HUGY@k+t{Tjpl)R|Yp~ z6Bi(}zq+=oZa#7csM0AkQFD0qQl{AAPIxko{P`>1FKV$Xl1VN02br|9E*@c z2vv~LzZNy2+(AN;Ia0Erq@in7HZ4gO99l_DY9TgNG3BZqZbF=HW5tQM#+wETjWv;4 zHFK^D0v?za1Q{9~9tjFh5Vo_dL$9%iaAg$D{k;+jGNP5am`hJgPEZvwAuR(eC}^<5 zLT<*K#D=L#Y?V6CKY%jtgx625WC2SCJ!|OX0UQbBu4m0yStec4ds}U0%t*52U9(xn z1*;{gaTFx=;P?P1NQ`;K7=MGnCQY9AF8<||!zjmmrjaexLl&bGk8X{WdwT2@;FPWL3FUk_25)niq`-MJ4?j z^Gjqv?wMMaU2cmH9BZL9{IAgdtla2&2L9vW(n3Xb{x(Gzw~IC$>m%L*KR+^~zpHpv zFh$_Y;tR`)(34ib18mo=ST+6_FG981s%ZGjfA3MzNWCW{=`F8hgJ4#^)WSB>7($!C z;7p*zg)f~xWF}ABSb1iX=SsFS?(y>!<7w^jbKcyn_8FVNXuUcJb6%5YyO?A_vAaLK z;7p!%`G(`?wt)`xpcDIu+t~&C1;#3#R13-jLHp5Q$Z2_785MI#K{Vxl8evf0zzOSPYQmX z8NCD7=m^apeZt*W{@*syt@`4#0+q;K0-3AP&N$7)r9EgQhII+Z5~7E+@no0i+*LTZ zu+RdlqgLeuVJ~O2V8{l7Y~UlNBErb|?n@NG(d3w`!fmR9i2SEelD6zMsa-!evk$LY zai~4Q)NANLe|!D?(?vXs1VVqKJpBD^->~Pl?DY!7+B~}WTy0G0S6n6?KhuWjZ-P)* zR-(mp{M);17M%ZSC#s-jnmv}O73ltvU99l8T0dD(5JzNYn;P=Z86>)s6j@m7>FdON z0-|F&I~cU`hsjHN;nWc22vw9ti4>aN<${`ec&4cmQUo)U8b%3$AuFjbh&L=Gscv~H zmWOy|pHTe)}RX+tV48PW4N3<*WXY~^`=OdCzKLD zUNZ@GQs)<&8h5HZEQ_LaY_4oKDceEyD*y|p`V~~XSE=%*|DY%I((C-Z&y50^UsPZw zEm)c8d`TEb*q4pKE3~S_o2tW4=?d?V@>7kDxG=Ehq|Q<@(k0{4osg`&glSr{rnebn zK<~=z%##V?wSV`OviM%*?&Ob|qc+{gvbprEW z3W!fzEdT4Z?|tFZ=eh0G_D;J1GMwILw#=__%p(NDE7#oPlHBj)jQ{8X0cFTRCQjck zY;TnPH^%pP?e&A-+Hy_3QvVdjXLjyhG4FZGZ?^2-Qe@q0@6G+J&5N4dCuq&#eA}F7 zcze#O zwplDp?1pjFN@|zQUZ{sLO}^i=wf>voolWKsxk+0~UrmjrqB{3dfop3)MXGCWj#{Q& z@NLbVXZ6!&O-g3%z$JysEmLZtyK1G#$D~%Kl3L56X{rW?Z27JC+!oJlO|c`7SCRzH z4U}>sffJUbdS;@eW-4=zE7b&<7}DYp`<#vw>Upj5=WKGhNv{esv$CoxsU$KG5E2rl=~GCH(+ZXo6RB3H zX>LYFMP+3o2^5fjc(_QZl*?$chKZS3X=SBUA_))>5fO4m%4z~-!}#>HjH+r1i6jt^ zhzJSO$p43f`TuaXe>0Gyrl+S>R8=Jt!GHurL`I)bHnHdcdcK+^2@>Fd_}i3Pg-(p&lE45FWA>IFfSu`8*sWD zm{)Hh>#f;cnFnWPM$l@Du5LDCcEH%#G1S{**|u76x}far=xQ11LdKYKGgZu3mn=)u zIYcGc@3tMiy)S?9$}m7+xe=lKaglV=4dNAtg@eFFn|xcUSZAzt0GA3z95JSR9cQSS zq+}Q5V6iB?OdP1;2Lpls1=rrN?hGLN_?{~*@GS)%F_G6r+%PJta*Q7)X=EUyRC_#* zvvkxl%n}fg>|&_*6bb!dXmC=M#f(S;Py=>7S88LM$R!Rhk%(kn`!L@x*+h;LBw~)L zDJjIV-T#UMLX;!S`ETBQMEl>#R*FpcDZg``I4I@`)~oD!FLk=K7MlO85R$Y-D}|RO zB0o-0#Uxi!(JB}IBYDB=?0K72cGF!V>Yu2Vzr&~o&VXI%z^CnVlp%<w)sR~pFbrfuCBFHkt%~c6RSyGlY z8B}>vXHDY2N)Jtg-u*zhL~108`6~W3gE>q;7DnyExp{@oPVauz2saT~_^nidF4Ddy~2j)@y|qwMWH0C$lONChFU!I9w;Ar5*^jz!Sx@Qq zT#f$cGQ$1)As4s)M^>?n=nsmZz`BTZNFI|enZ=Zos{{mJ(KB$+AGxFf|L+t6VNOlS zpU~c#rxO>f-lW?{n7Ud?U7IU#o~D78N{x%()K+t-hrXf%{)4l?i<#!2b1Gw|49$hu&4 z$tFo)%W$?gUIY0(HeU@|&relHX#;H2R0Pkuw%1hbtC>NX$gLiJIp!quSzUvid3#1%wC^&=;&|Ri`p;b8z(||Y3kpi7q)lR(>bO-E z`Akf&9=;In^fh591(|UrtRdU`SkqumKGaVKd4csCkT~7Mpy&q;f%OE9mYmx8qgV+KY~fd@}=D++A4reEzWB0jSEo>Z93+zc@^vklur9&W{Iw_ojDzCZlri%lR22 z$yS|L&}MJN*(Bvy#(HXTOQzqh0eM84GJf_?XF#N-*mib0~$?V3m`l~5~Gt8{c&sm_>UV>F3QkQ8C?E)8r3xrO9hNt{Y^nnteRlq0N9y&GS(UdK z3mh6tS40!bILCEC0@M^k6f(+1Id7if!o!VDsM$lLP@HcoZFZWYjIm6PP|a>9lVo}R zu}ZWmyYUo|w{>grmXuZ7KC(^x7Z1I+iW8{!fh;Gr=;9;H1jZh7bve^_VQ5Ae$>f=E z=7*VLo*eWRWl`;8)P?heCV643O43&uVHQ~OZHyI-GK{CbO}PI~2JHK^f0xK}xP*QX zVBzwBr~@Z{$C3o}B(8)8ID-K);qTMQQ#NwPY=>Kp6Wa9Dmk|Ux>W-b>MR3Cu##{JC zM@_LHj!kc5G~+~B5VN{AWlX0)+1>@h!#c&m$W5uRAA$;tvtM(Zj)Mmg5)Co=@>7TV zr+`#}!bNKO`d|69PpjmQ#XnRM!>?Rn>H``=Ju`d4b?*I%+$%_MnG5j;8?;S8j~$w? zO{k86TG6NNv3E0$=DF2p-Hw#yX>3&JrDQ08Ev;EoWgnD3ClY#77!5v4`<35)E}8U4 zkxrqGI_|9`E?80)|5AC7;<|1G&>sp1`tKb_a9H{!cA}n3(Pyo<+k!ZMHn!sksc35M&yP@-Q24Wp%S`^3*2f>JC(ZNhL2y= z;0?~F#Xev*>juVJz_E%@8#;BzL-`(dyEn?0=?|qFTAl`&+nrT6uNl~7fVG=?bzGp^ z#O{td&mlFQbK*R9H(fKy|YvEt<+{AoyR@+yhXn%M+R`}8h%O0T7 zpQW!h#*03Gb{R9p$1nBMbU@F4%W*W;*ICEWX7{3+_k_*+8vCPq)G3{dk$L+b_$I1f z41nvS;AC6BQ`gB0_(7-v|F-(~F>(^RkdF`^y0IHmTea(RrmO31`=Pi-uf7xGxwiQI za?}&#$3N>nHu!kZ)`OjQvejc|XZ-t;Kw1aRI>`MO;jSkKAd2b~^f_f;O-B#6{RyD| zqhI%JU#1NNck2xI4TpD)lNwtG_gtf;i4sqBmq$;PmIpY=o$7l}M*V)Df?wM=Z{|;d z@QR!T82H1&2JX#oxBZ(UraKt?uLA(}=LQP+aXkjI@k?^dVzMr`K0+b&7Y8la>7?&PWte60MZm+{&k73kv-SpH7Wm4PfBBvuzOQro zVbK6soNxz~ouyvWx#9i3Zi&10D%=a#_B3XM&?fE`P=qo!EWBGlp*mH%M zwycobA9$V-S%bLYvLN-QNzy6j=m5#yvTTPlKu|>JwU?0 zf9o?5Prq~MNV@j;Z{KYHt5Fx8DAh65*FKM6nzK`R7cQR4S@O|sqjd+~I-NH)=YRk% ztw`2uG|2D&eFB1}43!ak>GznHzLt>D`RZ@UYB-&By`Qlwm!8Rg#_HcO>3G*=(aZ0| z)`}lZKimSn+N(=CSw7>q9dN!2u5O^w|LX?xhPDNdmVX_r5Pnz*OX!KPzpKU$EaJsj6rV;6HH3SE~pbp{8XJAy%5{ z%G6S8VinNuj;A^yz#1M85CEN@bndty%Z>|^bzGFZgK4J_xTl!xwa%IgGVu#XHWPD)F4TfF%|*O2!K4wdqs?`%7=puHU=<_T9~r$mN&o zAFB8kuO~S*3W}yF=tO$#9$H$+meV)R8HTHN#HLn=8>hzk!3|-KQ8Xo_9N!D5o>Z6W z4O1}<(>6V`Vs@HC;%`}M>)X~ETegk_jHF|*lQR#oV}apbfLbW-Zm*lNsXgwp*#Y`K z7MI)aX!Za6vc;O#Ur&FoWR$e>NdO;TtE^bQBI&Gm?t){}95g|roE-l(8+_eNbfoM>b-6`bAUJ0ufv+1@vrI>sibz! zzvIzrwbHA;f#rg4smHPClqKQ6=kKE*g=1kL8-pkf=E;ked+!StAySkuqbo05hD=$q zRrs#MVsZ-Qij}EQqfwJ)S~hkLU7opUM4Riby3+sdn%CZV>xC}=byu7_1K@ow$tuW; zaNjIRMhNJMAmoH3BR~Lu7~D>dY@=~Ni;xx>7Rn9^z2g+(Q|E=DBRb9MM149wtk1w- zqcg4Ds?Wj)(b+Z~%5$<(|3iYC;AH@ZmE8dV0Cxj0*J1n#9PWW!3z`4`KmhD^$pURT zrimzznWQUg06IzD#9|wAqek!cU7#Nt{(rwmS_?pdqU5G zijC-EMy4yqN&)@Dp6bkNRLP33ah8jtH(=M%tNOL5(hHxt!r@}bUwoxT|MUc!3+w9%J1j)p+zy{_=E)6Yg-xVr&gv;^+gm%ozD(tZUs}=cCpFzPrx)a-Ir))VTlfWfOPr$)6sUcr>j(F8E|Hwn7zB z7+v@Fl7EGvbVXu)+F;6~msH;@x}Vl&6iLd)1DnHd(bSNl`zRUd4^CYlAuK+(F)@{D9gkVNeX|l6Rcwtupp%MLR5~%K zwJfvkVzL)gpSaN*YTa}(0STx0X)KwW@W3^(Qo~Ii*c8!lq;2wox=-CNJCb{bwh)+$ z*zyxdPq6xqURgk~w01n^qYC8Fb;Mos)NKzb1M9{5vGi{HWD|y6M@e?!0X6*l3V#Lb zVztRJf3F}v9hFqBsF@O zUsXtTYQ3B<3~G<>TEf#pUmq4+U7-728pXI~29mNd6-ZP#7qL67(l+2fGebs`9O- zQi6P$-#@85ktT%W$&?(w@NsEUUCAiQ1`s`~-vNucm7ghRd8SG+%%1|cp%20l)lOEP zd}mEJPrVx%OGGL*E4a+btAhb8(Tlq{9PvViyIXYA2sM|gVFd$(>K3s|U-*S6=!z!@l`1kc{%h19A*LvJiSs zhO1p&)gw6oB998s=bKG0L?*h-sUgvfDG-74tp8iSkOlgUm_-ZW1Yu;ys4yq9B_q}5 zr!dToVyq6|p=(4!5@$^WoVw$XOP`I{1#O&Q^qL4S3#S}ir7L_QjdbO;J=&??H(2HV z0b3p9f&R+L#V<^N@fSftygU}i?TN19Nm9z4&vNEcuo;CSN^~6wjp$wN5S!XApj$(i z(yWA~-VAV4jY;zz@FyrY z>z@w}{no&12fcRKYv?cT5&yF4UZ*vzb7^lR?x8Y5<|k?)T(waZ&NyTPDW>rcV}d3w zIQ5&-#eTpia%oO^>9obfi9z&0W}%9-zviiTXwz?-NslaRUVvy!djL5n|_`T)r_JfZAAb&8PQfS=_m2kcc~9bav*i}XlMU71vf z&x8@`9BR*a7*+PAIn|2=xO`kEW$i-Jf=Uu*hmN3JD%_xHRkOMci`>gUmQbd5IB3_z&UyfbS`RrEL40IyZgRY#+k z)Li<~s!WXy4prr3s-ihPgCX=lZ1Q9r*Rvwv%ZPqoD;dhlBh|@%)V19a|KtVOe{LV% z|8A84YMHxt1hB5@y}LpH1)<9;qLm+UKtYt&@swBz6-wqAwD*S_Sm`a7C7MGMp~BE- zW3qQycuUqL)^&eQVkH8I#ckaqON8#?jGvJG?lEo}vGS0OEX3 zmppRyQ};;sm%Af1KrcGM?sc@hr+cy+%z~b7Yv<+ccTJn<&84ba1n?9SE?>P6z+d0w_NvYBl73#aAv|qBn^ZQ%y z9=+m;NEK$4Rh)YESH)=E!Lts{Z{h!I$!qC8xAuPjLZf|3HB&5c$7ybN-)Ps@zb4F+ zt&1!QR~#Q(u&g;2X{UGfPZ)21J}*#RK_o0Da(=4`+*RW&v&*|_a+5l^(~7FI$##~w z)tREV?_MiL8J2rcH+%HV=H*FFcoCuAhO%#ChJ1b_@9YL@Qf@0GI;|I$P(nnLV&(qCoim>8w}T6dO@hi(Z!_u|pJzy> z*Rkfexe4?fmw=ulm|2eM zSFQfJ?e5aW9LCTQW_kG}Ghry_;?mu=+Mm}~L5w?I?+j-zmh_+ZNgMAaV$$+ui`P1E z**r~m#^`LrcC(A7h&g8&&U$3~%!7n$urI*bv++%?IGbk=!+s|N ziPE9}sdHGG!V`v%*E>n*n@1!{w{FpOgi8bUU(Y{N0ofIxhP9jG_YKFqdhq)>$!HmA_2A%?lI!!>F42k81_e0O{r zFLJysHZ45IhJZs)I`5%Xh>oK<z+@t?Q`1`S7vI#I&^9WUTtT zCfZ}t!w~-4^kKI>)C%D-6mU#sv^TUHt}?zR#fj@-WGL0d&!g?u6t27)d)f6xh*S9mh^2ZCThnhi z=+XmyFzGX~q<*!joh4sKlNtw4JTyx*()}zs#_=jLnVCFz_O?lUbnpJ|n7Y!=%{jT6B^w+b2-ZUG5 z4T7V#p{Ul{lgLe0^?j{Xz|=4?7i#1Vl}K>C3TF2lW762vt8+;x&n22QXQHL$L0B9& z7%s^inoaWkyZb;3k>*$stVtwgo4dHA7MY}AO@aMd@Z4gSaqe@AO*-W2erA}% z^UBk(PM>S%Hd+U5=@7|I@PMZ&#$xh0=rm)jQ42+C%=;GArLM z*R(WK_t#FGLyz6CNtFGLNc$0ozMYu+*9idoE&)}#R9*EwZz`eQA)MhDj;!~g*4(>( zC|5pP{8Rtm5He0}4`tdz5o&Mp;NL)1>(XJJ7rO<8-Agp1@10HMtRutg%98!uvU+e% z&mx9V$oC<-BMgDyik+A3X67)e)^^8%&BepOK;FIr^wPIDz1dwt(7VhjJk2dBJoB)q zS2QF_R|lhtC=^tQ8k#NwLB*AER<3!mRwlNFEibA{f9-55`Q@^TN8|ZN(#p4*$4h^D za(y%s&Eo}qy?F!8MFszu z1un1LSu=puad|Gf4KAp+gTeQy%p$*9*gk3bK`X!AQa|0k#&R$xUgUv|5~YN&&6^Jg zATP=-7MHd*(PVs`B>%>>QcdBpc<#_v*2KQ;*)o0(vjXQYUPCe&T@&UNNAqRwr)}C( zGQ80>$|YpfP0o`k+%3CXoLWFw_u25eV~{#Xl2urPAEW_gc^KV&JT-`I2`xXZD9}5- zT%dniUJhefB>9UmomkWrG;3z(iO#4`pJgd`o}6J{!U1{e(EFJR+SlKBk?-dQ2S3bJ zME>?$dF1<Y&tOJegG9hA1c-EQ^+fM6mG~XkJMW87K3hrL9FDU&oP7-Aj?GE*9j> zWea3S*cMwDMv6>iGoxI({5l_$EbXvg-B|Pmf3Rrnmcn77YXU$oo&$P1K*dXMq*qL@ zXP?l6;WKyv*f=$n*vLqvYU`pHh#IGc1I zT%jF`tBs|$B*c@NYNF%d97r_XoePU0x75SE(v$KkSjWwwQnQINIC=G%P5m9yvc4JNU4PqI7BK{P>-fF zfVDL$5zncIrYsT5LXd1MHaxF9h)f|Sp1v-_*%cw+I9_-zg+H^6dums*O3E$lm&g1! zE94c#DBTI7+XBIpS{47*3q|YNcyRoPwkzKF`gx2;jqXxz-V(gnJDTpn4U3{QY3TGg zO$@b#9#3jgMFXwb1eQhq_FF~dJMsNoIW40MM%9GPix-m&XpOp-FT6o3!1MgJUX$;p zv$W*P%yMQb_f5KeV;rN&B}Xd&j0oXD{{wV#c#HKxYFAE^a~#9IG2>MUGp(w{vKAXM z{6K==ej%e+m@sv0<`}qkxy<1wKHR|npdf)G(&jX}B(~{qxQVIdHZT8erV<{R{eA}$ zKEfYXmNCl#^B?Z3jJ`esG&-Akp=ZD`9{@DI@pf+EiK1j;jk+ub_UKUmAI^9PiV;ut zi*#`%`S^LJMd$g3JHCY8hYS)E1+D1j*pyi9Kx96vFDkk&7VcRTlhp3;C_Ws+K|~<( z`N5Rfezdp}yIgv>DSR<1?@ADw@Bk04w1Knot8h~u{n3v=@J${+KRohHf|bBEZRXvG zeuU2VK1RUb5gt#!aRLE%bCq9HN)+54W*1u?jfAG=`N9Kp-BB5Ol%@52?vc}Uu4E}S zYpSp;YdkUF_w8uQjM&Z5*Vyku+p<~d*PIdwcVH2KfTrd9hKuNo^&*dZ^zP#2w5+4! zp>}wbWo8U=_!F95zftUt4#5mtIa_WfSp5!!O5@L?!xqBLl z>NiIVsQAt!HSY@t14}#fRCbzo3p^(P99+}(0_bml(FV#1KbmIa+y%9!@L<395svg! zJ&i8=@S!B?(d{zwyH9xDt?NFrpI&4gXwBJq2R^OL03!TKp`*dymtDDn6!8|ul`_*F-|AIdxdxn&rU&)^ zG^gcYukSS2)UfTgV7Yp9eAJ-jkKxf_VEFXkQjZrmhBQL}3O^A5QA`A)UFf!$`pC%o z$RFLn@%tGERsGoTHzAu1u{`}~9pT?eZJplEwYhrW)K=WB%Qoo{NDG2Zqaf*sE zo=I<7=o}jz(@}l#cwZkp&es=*^GON3&-Y&f&JXZdsozRDxaKo*bR=*szT0;vo9};m z2!;b|Dk#m0Fbt+Fn1sZCw+%tYMh~nhr#8!PyN#=W;=&lOYy+@lzyQ8efyNy>;FDm; zY{7mx34(G?BWLVOZ?km=0AzzziU}rE%DYqn1Db(3dzCdY;4b-e0TT4 zUHPZM53983>}pX-=44Ev)-ds?QG|?1Lzf^?B@t*87tPT$4#Nm@bW37R+fYnq$ttJP zZn&~%C->$}?#`Z`gauDC^%U*84_DlY@9&SlQ&FK)$24TYHu^NCOALgA-a~<6G$o@olYf33P2-Tw9ljYt=4Gn!TtV9#k(# zGN_(n&^)67@se*LG!GDZB~lqcVpEvuXgVe#Pan6B=wp0nXdo(AKOT4E1In}+{!i_ z7=N2v?=fu?>35kT$ZSo$(75LdJ~9S{qQ?W+A?Nh|2!zw(enh&-~`|RBqu< zf1WOdeG$pkI}>{yOye@M^Ap%4qOV5;%4_?c{_lxtkm8_VUsf6xvCcZoFPu%(Tp1YZ zpXzgL(b`)i#;2zzW<^GLdtlIB)_VqFwv2R7c0g!AemV-KXZy7qe<0eEkyLdUW2kx) zYYHcaIUGcsx_F@fH|oA%3e5fh^65Xo%xBX1aNEWQqSosbU_*+eg1Y7VlULb32_Xnn zOo-1=;W1^Mc2<^Yu_MeIn-)(FY|Y;=w;b&6FHP?8XJoEH9NnGe89_)6cJs&%rsFDb z_>>@bmqxppx6V zOLyh+q1#&Rp*xo@_ugz(;4rn7RhW95{B3T7Qi;)keJRe)eFK|TntBiMKz5L^Ejoba z>W1-h^ZZ`w$m`}6l{ZThY|KcpB~i>Qjsda~Xt-3VDgZ$>VLdPV$k7 z6u~BKxW6OSGmMa49T3NZV}c=mWO#5W3GN4p2u5T<-~pbVFh5^EsGp~2SOB1F=d+H+g>}keStWBV>K~%E+(xj+J_michK%D8b+)6Q|Y;;&VL>wp^DMs3hx%7w> z3Z0Us`>EM5yB7?lDVh#@ujtn+Zi=s5-p&HxUqA3TVP7+|17>E;`v(RkBrwE~84sZp zn1JitHjPe6q0l38mn$hoG3&T?h&Vn<4V4#dC7I#^&wvlSQYsbTl0id7lqnlu(RNdy zd57otkq6)`{UWwE4jd?&*48J8*A2xy1A``{m@$<3N`m5!9cpI!0Wza@jriA+z<{ZA zO{^63)yQP_=>D-yB`k{GB9cC5)PUi`Qv>TXE+8wcK?{k}Uht#m-?P+7Z97I1KCG@o zuEW4}Xv*E1xOz)}8XtB)>8uLGnUdsxl%h)j#nEKQ#Bq#L`p@}K@JFNGZU78>PQ&Fb zS&|HK%0FdB;D8(+%aBr)@1?iX1A*{J2+qN`hqzPB1IEQWy&|roo)K^_h!~z8Q|n+` z@!Bcj2ZQlH4K~fJoCG^e2GJ$qNH)?aD<{wqTmhEA<{EtOYZd}QqrH=)zx{~-CaJ$D zk16}ln5ej_5Q4eiM(ikP*Jp8kCv0rSeJKZCGu@+-*2Uq-G5O?D;P(2s#!+)ktJ-nt z^bW9U{a>7(Bb3MlnDpXK{UbUcvM0nxuI#v^+9Bd>eN-rQ(e35>10imS7MH^B($h#_ zBQh**Sj47bD~?3yCF|6W!q%br&Q(2ML1y?DCBASsynJ{v%t=F*u{byvI7A!204fW_ zvC!AI#IfMiOWCo31B|R#uq~2mv0$M_T&LCMU0Tu6uGh|I9&5+57NC@jTt#^QTQHwu z=E;!}@mVg*(|O$mlu;VPx}S3fEA4KOUD$rmHU*Xh!4I@ee2oE)& zHayW_O`tZxMSail6c7VOCJZN25MR2N3<9ly1Pz4=7%_8}i^iy2c$#yp#&mvePPJqh z$hE722OH5|Y6IR-b1F1dP~>lZg|vV_u7EL#i(^ z|HU;dBZ*Ojjwkb2Mo^I;5scTS1Rqt%t~V*oL6S4Uac@eCzpnNX0(@U~b#F5Onhp$2{K?G-TH~f8!GC{>$Naj`=S8w)%pxq0 zwPiG4za6rE(6=R*Jg@n0x%qM+QS#Zcr5)=?uX{PD|3D;NT8t5t`yyq2qw7 z@&8y@44Z67hxA4PJn%Z>_>AZg-nLp9-nLrf?_-gL`?C6j(g_|_iaWqs{`#U+N}{5` z$vKnvqj`i2`lwRu_ivbW;W`9QwjIq?lOqyIT`-<5s%0tlioN-8w{5*P60t z<7fZyPWbO9Hl9#f6t5=|cAr;r!3T+)Mi67;DXTA^aqzl*d%FGFmpV{>2O*}l)Ao$4 zL5rBi8R!!>=xDcRuE+^Sk-ca{fBD{t`lZA8nbBl2hOvlSRVs-l%!83^quK}y&@Myo5OUsdG zPkIN#BO}L$nz*VQX$f9vLN%O(h*+&U^%^v4(yT?RuZY^R*s^m-?=ox`!0G#qP5A%$ zoTta*=Z{=&mPha28GS#nZh!R8Soieak#PTC^UoI_wmH_tzaa<|_KzCc?DppxDgZ{r zFMDMEuL)YN&oCdDXsIVqG#ViuPp`ONx24$KmScsU#r z@=i8u5zZv5z%iA9iae+gYU;LG!U1=~+C^`s=vOsBe>x(b4N(r*q?^dO zR$}u-e+QP_2^_`Jy z85g))Q;6mnfS+tcBfO;3D|S4dO&tXR2*zN}f(Kz`7WvL>PGJ`E5HV7wS$9qK_*_F^ zeae@5K0-V%8Dzp%;t%;1Foi%E$fDE>^79}A3S9N=M4Xg?+)NDWf?p_?N?~1 zn*waf0#-lj8k)aG*3_EPBv1(PC6_G>PiMPhMeL8z$qnGuR51$;Fc&3$DxR%(itb5H z#+NUX6t01oxP3%1)e+(!Bu2d0S_7;B56f9SG=GI;DukM<)P}PWm?WJrE_OW1lS6YL zkfIIvD-_NN$6{rcj_s~bd3-6Cy^i>PR_np!8c76RLQVB~5GMCykvTAa9$mgt4{X1q zTNmV@{8h?MjXv#Hd0I&K8CBAa<}cVbf&>(;4!a%%Y#0d<20%op53Aq_+sPt-dwweB zub>)`zQ+=3szm)ofbW?lq{gt{L5ud?cjp>DYEeL%mSNi02o8XBnq~@ad4QlgL-+U} zcS5dQK~8Wt7e`rNO#5o(KkBcs_r(%eLU!9o4FRb-`4MYSvIN-GQh^&REd0AUOYlO8kTq)I>N;wEw2UI3AS9_zCYtUS=@l1ALHD9_`N* z8*H(!PtQ4z+JD$ZG)1H*7s(_7#_$3{paQg^E;+fJS2h zUYA%-LYJ~^G}C+@GOZ0{)EUVD+^v_XTz(=TeM`;}V!8CRI6vgYc0uszt-A?a8_1ZL zj{1Uas+)F@N}V5bG9-cT@{`=~QyB~XhMZR4s>ClifwQRnm;5I+#_^a$E`Gv$wU-$T z5#WOeY1zXm8f>xeOV1%#j+N(TcGy$(8a=O=-I3-WK`ElT3>}TIEPE|E6-p3gr6m~n zhNWdkrR_u~XWN-~+ioYNWjGT>iZG5(ve1s`U2&KoLBLV5fMVNR1AW1taFjoHspbm; zEubN)xSmyNC}5D6VYokfcVA??q{&}-kbO;+MY#DsTVHIYqdui+bkB%}iVUqTbFz4t zBh7(r1$RBt*R(%IXe)>boKORSyG<&=4uxPnxy$5HPA$iL<8)g)ZGt1dq!T89yxm%( z#3AUNv{(9QV@G}d|EtlPdZxL_xbX5*oWjM?M~Q5$B^W)OPO^-~v?94HaFAA5R|H}Cvqz@iHdE!$S{y*cZjH@RuoTR0q zbaCIO`~ENU%gie$Cr{yq&-8yg>&q=4Y&moK#A9JoQki{RP_Nte(YAAspL_zfc-rXY z4_%?GMSc6=_p`svzV_7lJA7ta%pA?oW*KI3vwjP)MUTaz<*?5%3ib?9^Sb)-3R92*>uJKlEun%U;$=7e|3ajJ5f za{8aMi!;`F*7=t6uen_=J}yx%MXtH7D%Vjrs9SVDP~Rlq-|B{bj($J;8~Qu>NBC#@ z%l-QU)&!Ubgald!h6N@EN&@?W$U&^&y}|y$F~Oz5+Tgk1Tftv@sUhr;+R%g0tuTC8 zaM*JA1^6)p2XPF^LrtLfMCfBeF*59D{C@lsf-?b4NF|5}orDvFTZCVTTZneV5F(Q( zAr251h(D1okQd3f$R8=|DEla`6c{Cz!lu+vCMlOGZ>Vdj2dM$nC~6^9Lmi`@qrRlA zp&g_J(qd>_S|e?Wc7yiu`oG>NM%1C}|xXqGwjtU*)=AcYnX5n8(;kGo{W_SYc5=t1=Ur zPHrw&D&Uo%@{^S_%KcSY)yGs3sz>VHn)DieO;gQy&6%3}HNR*$8as`*2CGTZlxk`< zhc#EqPhhP@ZD#FcokN|X?!S6z{ZfN#!)T*f<6;x8*{bpUVNi3ZbkQ0#PrJK!C+47s{5K<$3!Z@x=^mW?m!I+9Df|gGLpyB1Q+d#+_OU zp-@4xpTqCJpZRhRF`rl#3E{2Wu|GA-hPP;x!thfbkhfS!s8A)zJPK(;M$+O@Rc={O zcH}H+$Jk02X=#18zqV*UOfEdl$>P0(mDg_?jU?{5YBU;e4c zfTVCwHeqzvQ+onC5qA~-8S}+Z8R;nPPMLM9e2^XF!0EMI^QO~55|cx^5!1jQ$7DYp z5a++;O0p3UKtLUU2&f~CIv`AtJ0X#CC8c;20e4u+Tj`xV-|&+M)bdDMS@97}Kowu< zYh6bNApDO}4FKfjCrLs8q&Yt}4p4CoU!gdDjb{&-&rkPYK?MVfpdxil>HvaXbj<&i z!>v6^zc_bs{_>9}$Vz^NPmrC!*jDo1@L51X(zPxD;Q^b|s%w;de@Ate+z8^4NU}87iDHZX4>zAlpfZIAKVzt#_<6Aw(N5Kl**Ji*G;XS z^aqs-E|^Cy>UgDwj8qbk3dFRfv+1XEfyQ~_0Rn3?R!W{y(1?Q!9&6RAEE>48mdyM7 zaW!196@^_ded>rx0jpU{l&eT2Z7dUWu;@n_$5XbilBC2A$dqSR{4>ct@?eOwkju3k zKDOBx3jT@=UWGeZM&Je(^+3l+IDy9LD=9GOn(e_c@ZxHjon$o5?@@hBTAmQ0lau10 z$`D=Yw>d4OP^Ht1(;#`$Hz@ePPh@|4j^gkCxzsN8;fsMnd>JI5i82R?c)%);a}WI5 zqw;vLBXI;uf`(Tpj$j72OgLO{E(4oj}gte$x1(d|F6s2nkk&^Nf?VQu)EfL4kT$-lA zX5j$0p+L}!oEOGI=vSj%i4v?q6?^!WB>b`B!ZP~Mc~=E4xEH}L*8{2^!9x-hUNs-B zo=n~fUxi$EApJI9~?1z{7{E74lU|3Bpc;f$IbPR4@4 zooc@wupU~b)6?4%7gmk`mg(DYB?OPo|6&~03|Z1Vnr^7N3lnGAgpug2`gz#as;V(d z7olo~q^g>p@JZM7Jeme+Xg&ed7r^A}?hp8YD+Vy^mweJGKIUjHI5)=~>~p&%r-1?r zWS|(ej4LFNsqz}s=UlrD;&(0ywn!U9lHGGc3Z1+FV=-`)qPo2wX~JKy=PFR zHbau3=}0Y;5uhW6v77*a009U_9Kocm>vC;!_`vW(-88Y1Y_d9@I)CE){dej0-7jvV z{=yYtNWp2Lzs!h|{`d)*d^1z2S-i|sJpO{{QA8{gtZ4U*7kBC61LRos(G zb=Lx-qBE&yaXD4^?H%LVzApkDskV`kBZa%2a}UIMJIp%IOq2in&kcs(GsYn7(oH}5 z&_MWbBQ<~iyW2$O#!m-q@HnPB{hs11EF9A@%BkLGmR8N~n77CVAwrtR&KU6bwG7&A zr}@<%MnAc%T51cnJ?~y^uG=_vG5hP{ik15JBawjDsryggvJ_N3+hPViYGl8*)~FS{ zU{XyrgPv?Dol3+vV{WS>I9SCRpiBwFGpoU#NF-re4g>kBxVO2Yn+<g$IBAC%=C3)B0KH2i^zldzkX;AHJ8uAbuA<8*pDbx{@uq*|UK6z*A`6 z^z81Fx70W2thyG;BhCw%Ps|jV-l!g z?OD9yzL}@xQOt_m&gyGNvQy5H^9f}wb_Jba8?C(#3Vi7R0V((}1T-6JX<=YTts9CQ%MSXr-QxSWB%U&_8zh-AB(?!_hTd}7tClFLwk=8W zLyE(DtE~qmU#9FbsK?Hv{4=h81{5CI}Kjiej6cmuVCS zJ}JT}AiT{G5d*A@Bttk3*<(dgQe$8~ArnG0sLfZ;d;MbzbWYt-Wcb6=sl*1qDopKW zR?vdRC&l51Hn>mc7EW$rlLwA}-HU#X(xlfn9*IaH|MT~!D)yTrfVAe~N&CM^BqJq9a_tQ(MseTH-x z_6@RGEsB&yQ)$}sGM>l8WF1gFtgBxoSAD|ER?oLNv^t0!G~%4Ou@*EGA3Lw9p(2o3 zYTPKe_=U`X0`y?$$mD+-fbI}dA}#h}!RHWH94vVv_O~B1j|f&Dh|5OxfwS;96CJ-_ z>Awp~so*^PmA20x$Q(@y@JD1~mzdjwU*2eW{1$NG2Mvv$dtgifmKR|uu#v*lE3E?K z1?jU0JDmDc&WaD)0SNdmTu^+^9zowWX~Wh0I{5d1tMB#IAfOOo2E#~-!U?pDVii7- zD0&fDnR{zgjziH3h9Z(Aww~q&1{p!ZtMQl8=~%F^bYlW(qNg)eRs20klocYv-BD4k zj`9pqN3|g&mK&(5MKD6ylo6>qsTxsG@G;t|8In+jnms|^*H)#cXgfS1zt8P*zyiKh zOi>*1Lhq`*B%Z8RaKX*QVq2noX<5sfuUzG>ml;Z~b@5$bK%?V^)rnNOHXvBE0=H|oJ~+$uy?Rlw^47xflOMr8q zD#JPWYiIU3$M@%s_`+`bQP9D6!!HFodUgl`^a#r|UfkUrK`KIKG7P3DqQmnHiJwGZ zk>RCV@Nzpht;uMNCmXQ>pWtH`2jEiM>UsAc+b)+(@EbJ5wo$U5U&Cay;^YMHrqkT3_UXMcpd4c)4EG&aFjt zQ-7r>XolGcaTro6We%Y`Kw*Wan#=kV#;}7#}%;ENJ&dEhq<%AVJahf3TpGz)t_Wr?n!;teK z%ZZ-eU0;P^mJ3PiGv1A$p!7)tQBXDK zngbG?l7RqDHn}z3>dq&^4hj=XAk3^DSu}>^>^Y4KKh+G@J_c5x3BtmH9!Oe(2#5X} z;vK5}8pxPkcO2O`z({RMA^EdnAh=KVt6SO@MJ(|HnNj1(3Wx2tbV0`chR zX<`Sn0t9B)DJk>8!#zT?A1xmN?{q?id+xkO9~admx{}|G7uwBN%E@&!Sgv5O5sXl% zUuAO$GiBh^L_^C#UUyO&6TpiKk~#m(om7%Hwu+SYPBJwFc*#C3N%q< z;Osyk9ppKr6{G>WuK3QN3QUr=a>D515w{4>xtBqP8v?qq2)Iai2d=KlR6e?VoiMz7 zxBMNfeq?6v9oJkob%g8X?EdeQIMZQz8KrBkW=oW?uaVg%4_ITJzwa2_%amk3;JCoQ zG#Ij22`TYUzKOCS_gihPe_Z~db{1HZwR-Z+`d?z*`OED^y(d&Vti}wDV&9B%-xh$- ziQl?`hOo?z&PRj_JLb>Rk|WnoK7a;ZXA zcicao4jtzu*J1fd>yid-a6Rne^OAll{fO3Ye%l08ccKUjw zH?KtZCb1!7Yv_C7qDEu9LOHGCe>M-VNa?-O;7kn&p0Bf#v46{b?e%jdeUv^G;(xE3 z$aU$iv0R=R%RtJgospD2V`gN(|1)C}9WKg$>s2G2+7up4`S>5emF#YdGiutm0KZ*_ zMskESB5S4WT=@%MrF3gY>R{K8a|SHcf`wde`&{VJ0deh2g{G&Gi;C~JBR$>Srt3Lhk*yF+4>lrZhu*YShXg1c-h8Uu=|mFz}1Mi zlsl4s`zv*gKsSYMVdPU+DVa4JJ7rs1216$7MlNB8`CD!6vR9AuNAvgOhsN+0y;!7+ zP}e$;MVp_3^UZ)L%w5n=tr#{d%~TemX1S@k_S6*g`GY!4b;(55aMP6p)qJF!^*rEF z%LjFO!O8XzaP>&Fo|ALJr=iDOEx57Oere{1GJ8=V9U6MgTt)EO`HrM^fh?-X>xX6T&lY{(=FwC$FbC0O1W0vYgmM!qJ-2ugZ$g%&3-&q?a0f|RmI|D zmDya0d7j*0t+1!G?AUFUaLr}yKPUkcZH-3uY%Y`oyrB*2N53b19P)w&lR z_Q%UMU$16I9~X-7b3BeG7+~mluagz7oY^`Rb|fp*OZMP97YwNs%J2*W4Gl$$8I+&t z=G?~{s9Pq}0HeGLQLp<757WPj!6;ou2qUW<5QEB-cwC;ztFYcy2!!G}=T5Gv6l9eM#4jhlj~X zhbDMtU4zZXiCoivt74)*F|a826?6aia?^g3hMXfd?NKXvi7tsA)J2e;JxZ7smca?r z*~CG~tLiaf-Ezr=JFhNbiA}5CImSkAPG>HfnIe5g?#`xXWv0EaZ#y$gQ&kZF?S1{4 z=!IrPM>`MXawXA2&t$xoGgwO#F|%E0j;D19iIyl9SGoy4Gw zZqW$jv=*?Ap!9HqxVDtj1hGA72oTymadm=+ud7+7nr(1aa!|4gAPP_`a(P30XQf!~ z?Cow#boF(VP_9f$?fGzNmZ?1r_9T<)&Ns@+I#>GqtHlwD6RP8SCj{XjQ{w9EG$9Zh z@1|_0XC3+jyR~}&2OL<= zn&C?*BgyvpJ2tJXQaAx90NZ}wxc~H7{B`^jH2nVv)W)$d%OaGQ-rE2cf^WP)z-fR2 zDr3i)L(B8sTi^xfM4VH7V94xTdBv%tcjtOMX(+=G_^iAKz{RkDBs4d1c%ESpq|8VX zL0qSK0yJ8o2Z5QVdrH381TswG9LoC`=fD@t$L znJUTdRQFsyd9SDqEFEGZ8Tr_48a(5HbdD!_!EE)X2FU>lS^HW zTI8v}j_JUY`;c}6rrfh&Wh@0=Jr^$9?^QHLrgj4P>X# znBNT9IKF1;+Wjli&45N3S*!bEWUxR$CfBR`xu3_u{z^Q9rzuB^x@|E!%1Zw#;@E2!td`d#PbUGYS-{I z68?vB_v`17zw0`loJeyLN~S`2RY z`nv~{iOq0Iog-Ii$$5sOsnuZixVx30t{7Q}ozqn=UeqcVTrWzT=Tz@WFTE*Y6=P$; z8uA^9#{OBwzbr;Y4RthS>K4fyzn{$M=HWhPJi8ds+12-?Cs%3okquTh(gfrdd0>ga zgBBL&D@LZhTFgp1w%CZvDXiqHQcPuf=solLk$YFBt~uh^SUyRV zVs@(urCGx(sSxS8RW?Ryi7jImzsD)1OwE|M4{WsTm)$94R0)by8GfC|pu!D%FN1aQ z0$4BGf$g05x3sg#MI3Nn6&!dpQLsAz&zH0M&^f41A{gf5JpHvo zsO?)(6lI*r65m9uDa zWX7v!xBGm$2G#Wcxg_NBP$G}Mxzi*wY_$dJcQh0;XX#;)HaN&2eRR>5$%_Zp%FlXp zhEL4!tZZ21gDu`ju}-6sX0&{5wR5|o(C(`fdCUV`q}fclTkUP=>q-@T(f-zp9wT^OwN-8CU@ldkgaFs=rT&+4z#SEzo)k(%Hs-dOS+^)^= zI>;A8|L>j$G&C1xT{$d0A_;DT%Y8u+T;|3A6RSorcq%4}q*9h;ZrX?*j?fA3uE;8E z9*91y$|gI7%-Y@+2tQ=n>ec^TT@BtE_I)J{QoHHXt0DWoEr`YI@B}`kcDxj;MU?d9v`$m(T~iPq~WIoI3;h5|VJk zQ;wYSzAMx>6owtcxzA0cIxPn^ON4w`$FA&9@+mE+hXb<_+@Z(Hu`d^p zE01*=)S7L5^m82IkO(5+8lBNk@?HSWT@TtKQB)Mf%@KT*Ed8b4w@*MQx19BIKEPxQ>(e z#e=xdO-x2cyR_rO?ahDwc_L`K-xKEIKWf?zx^xqrlUFOCaT+K$x4yuJaxivPe@@l^uSu9t39s)=-F zTDF84%LvADlJBQ~=AIvbSJSkW%QUUHs`*+oZugmq7t&y>z-%T-(zsq(8j7x~iosS; zqLN*sNjFE`D3*6j{hgjZzaOL(0#&w3)%om3HgN|2(dBdkg~RLQo^BFFxqwWL}T*&u#>Ha4lsS zi&UbW*i@#LZD-u28;(G~T@~QmweGN5OnT}XM4#TOlBbw$R4ZrDW6mBo1< zCs9US_iS3GPG81MH8qq^T|XF#Iy*cE3hVz6UGyV+jk&%rP-q4F`i>qS&lZC4T-4a> zF;S&Rh%a3!b0^J1`3H`fj{GVfEL%_ZWMJx6v-82EDl+-teYrAL!IVZoOw z6C14%Tc&5!IeO^~x^`nnk6u>i%B)VM+?0kW?(Sq9dSw;XSB@>61XScU6AU6&zTqlJ z{k$I3#W(J|=bNO^wsr6X8GF)spUUlHVYE;N(r(jAs?)qn%~6(#NnaK?-8W{7c`sdX z%)UL21s0jj(=JIIOGNXW`kF3=xg#)y)?Eekl3U~^G~h#Nwb7DkYrs10rd%?c=3w%0 znYy%+L3JhPvL&q8W2WBVk_Av`p6$G2+ieHGcVW221!0p*gR0*1Qs`~OQ*@i(tjr2x zQ9NmI%}vLX{_o2z^~R^Gk*)A!jUk#z>AGt%-iY|fi4}GW(}>!b|2c^~T_>0LLzU)q z*uf<<0!vv+x(QF)t?u-Qo04C+CD}~}?dp7v34@DtZYz;45hMo9#}j$9m`P;QJBb{L zXD*vPeQ6%HB_+L(8al41rcAsXiX3NZwukLG5^AzHK;bv$NF*`u9utD4Xyon@7{anY z5DHUl&Al$+-o@|3FaPhIKt3{i&IGdBpk3-Sv#00SU7>E`8&adh+jfmawaW{WeTjBn zwZp~PUq~uBDF6BMYVTj2Y1(G9{y47zPVro+mhXlxkCD7TLL9j9!{g)blj7*Y^RJv> zhqfK?{DTF%C+`M;m23rqDmtYLB}}nr*;&P=J!7wD9{smXc?Pe?uTQw)y>7HJ|1TdN zzQ2X(24gNPQf8ys`~UMDyNSDI`q`z`w|?%D6)plel=aBXKrb(VR~R>gR*dl#f)YDw zLMz>g_JV7XZ)GlbODQ!;;G#RisU5nirY?F1@|j>!L-FX7rH|6briWBMX_zCHje{6v} zusz8MQ77c`(B7Yi3zPlcRjUlUP zMO!P0Wl4*oDzM)b?+%+XOpBM0eGozHKe$aoecC=a&-ms^pDGFy4&ry*JrhKYT9eP( zxtwJE#B+gZ_|DE&y){A2p~CxRNy1V*vE$h#2y#$5td z+$M0`jew0Y2iR7s5sw7nGqz3F5XSBBdE<7Ze4Br6q3=@Pn| zD4?ALN>=hVC=PV85WYk<^7-Nj+m2^34M0O3%@*3)r}Vk$7DKZ zy3K8wgDxv^1WDQh+7I(!I-I8lA)OZ3)4i7#yEEDoH^AZ51KJ1RlvlvPB?MY$KK|s^ zQfq2U(PyGN8&lKhp?V<;Q(Z4No)nN;)Kvzuc8NIMMnq}cNmlwAx-n#U;#`W;sG?}T zLuKT&!Pnh`aZ#jcS&nPV%V{I!87H;GB5*~;Vj^ysHgOBoVCCvwdr9Ybbs{uPFx)Tz z4|g+MEd@604&Mm^BPoWWTIc3ZoTdAm{~mHyWyinhDa{EVR_-`YQO5r`gTe4~z<&t% zaM_rP=B|N7o7E#VisH458(%GE&5U0tWw?ebm6E7jb}iFhwi2L@it!HpQq`xza50R- z2j(tL&sy?4E|X>E@e(DAl~g)cu2f1KPo_1`me68yE7n(Jc>=Ub=W)@PMT)4~Hf>dZ zx`^TvjL;OU9&J6jt6&g)dsAZ?1v3w&dimS;botGdWF%#+Rnf2aN-$en&bhz#Ez~La zUNOHB$GXNcwFS<1B`8H2YT>!?*p<4>cuyq0AJ}l2SxYmo4LjeGyl3wHXs4b359g4+ z;Jk`K8zJAW?b7yZn1AQ0@fWw-DFx^!G74eY_eDQY)5!S>rHUzX!`I;2{B~N+UBN(O zE`=U%Y}EY=0-PuFGX+*Z#c7;S-r;(Sn;UA4iyjMsvL9q<4zD^s^Bx0126P^yEcrUK zJMo48{mJ|ow3Wzm2kU9rFwsx?fNpQ2c5bg$i_w^4ntvw4AISqXDJb^>=ZoTSG! z&s8}Fbi!s)8K203ip4!qqnf4!$=dLeLi}+JX>a2Db%{CPMBOlGb#35gZoD9M%|QH; zqp)DsfjOi&f(4vixgo<7v4R60tSNjM=j8cP483d`Ueft^Je%dx7i_1CC{?IjKxLV% zN}@7RjQhjkAR8T=_6QqG%QtSQGR`KdM2{P_0A<{$o5H5xB8z7jKhkCyJatAQjA=_C zI7s9ob){;)a~SCY9ewcmkH`*?_^dj4+JD(8S)K8Zr9gz znA_U^eehproAuR=FRQ)Vek$isDp;%1xUO}CSnKq@$26h(Jda6vM9aInp zg&iJ9I!-K28r_@bIF*rU#w!uIG-*|+R6Am}xJ9l?hT(apDW$g=&PtL9l4UNWT3T4c z97Jj)fbg41J519fi55ZY9gO3!q)L2;Pu+2eng+`qpwgiFNRVVwEjhiZ5go`hrP}F*RkiMWaPx*f zO)g5yV{lOo`(z`KRQ&4+Yomtqy1N<+N&!exoiXXcf~p0|wL_iKX-C%+$wWLRqnnk< zx|ZW-3&}W%n8ip`=qIa}$E|gYRzsbj^&Px$s~v|trAxtIxeRQ$x4|ne0iiFs5)^Bm z?RYe;@wtnc3>%$Mg^W``!XYi0O*Fb}cO;6<#UwUs^O<_^m8v~fC3iH2=#nZS>5X~_ ze|HF#%*;3phz&i2s%`@cD(R&~KQuk99F{40%U4g4)@BxalQVV(6CmBn)Pa0H0j^z( zuOUw{kS{N~oxUES$?TD6)NkK4E2Sp-Yt}NQvZ_iJp(vPw9I<4fpq;l3?0WR~D!V$2 z18b2-`&)!O;)+uj1BfHd{AUS>5@YvRg@%S{P+E{(v7}LNPdNF`t86r1OMuR{4$FjP zK5=Oe#cohqmt-vKjTviAZqld?4VxKdCMqMNF2%^PSh}UEr)pssKU><*y4Tcuk+)c` zZFLAJ`_o<(OQ1n{y&>tZV0syMb79(W0&m9d@j&od_!8`g&%$Tu_Ujy4HpVE&Zm@Y3 zE`7m1Ri+@ppva|;VLFr90>wv>JVKyS4iP2NBnqI-H%PME^}$28>oB;_y?{TAEiIdl zfF0V$D19;qa^9C@%H!!QJ{3*Q$Yr@Px{Uh8KtGXdHD0`rYjH zRr8sq=Hi-~^U^H(J@Uq4B8;nr)f3BTqU7y!>b{wawl-jOi08JR8KZm2bNwzvOR zl&2=wcg`n?y{0{@;dWD7*@+5D(xoyA`hR*T`bv zs}DR^=do`bEbUs;Vq3qn-FpiSh^~}MKboA<eJ8OM3517=p*;zVq$%*Ef~!Yxt6xrK zfphs4_V3q`TPZr0RU^~==2$FInUj54@B7lyV}M^jW+c^S1WaI9)y*|tw`$$n;?|yL zZENAPbhh0`=%9)L#+c-Y5#^eQr*mg#)PO5k03K$kjZMHamlevOfw8Ck}=8}HO`T_Pqjtfwm*|HP6gxN zh({I`6&0OEm9d7Lfgfb-96ES>w0r24!EbT5h+b1x0#Fa^`@)g@-j6sE=oI%k*2kU6 zzZ}88R)qr}0yP6R#KC|h7=0O_YFQfd;H)WNi(fPln6Vq>Y}=1gtpn2L#8|JZ4< zj@BUBQ_n7dXQ8qEW7Ss)8TO-2IgE7)XS$irzc?DWNhm@tNB(iF0V)jo@eU9h=3C-P z*Y(um>NzQe$?|sT*vjFEn^07gNDuT=DvDa{Et0Yprer|c;Idd?@WrogV#O{GKe%<^ zS=R@Cdk89Q!)Y+NFDP?Ul{nkz8&slPEaOyl+Z=-yfK7ISAQA~hIfsJWIs83+HG7$m zh99WCktWV83n^Ryj9A;eBaN)m1X}gfxnro>_t|#(1wQV2vf5|+-3x~9>2;=wmC5-(Y;2> z2gZPlMOAl$@09uf5BTpW4LL(MxLTn0VJgZBrLUPOE;?cgH#j}mqp$B-?gA;%oPWZ90}|3#qfzEVe&f+9t_sXYhYnW1M}2g1J>e-Nk|e;%8~*@-KVw;=sJ~6kQPi6PwsMqkuzy9ayp+&f`cRcKbVb1M^7j` zjk63b3*%J<)6??`!;zHcDlCQNd!Zajj-P_K4bg;C5j#*o{dab!h)92uMU zqPV2m2?*)<8cOQs^Q@%CD!(XqqTG{h&wY3;jXK65%I&rteswbq z5ko+>NUAF)Ng&WVj4RE*-@G)P*Qw}$o~5IR2k8~dzMY1eo8i{Ct!$5>h~q{R({*hOmw3(dRl`&2QtP%`T)|61+B{fjT$DvB3Zc|W`*V@ZRlOO`cb9FQ z&f;kk9n@fP4`mrykwcx9l>l4M2IdqAG~V3o7IH)&7x|8KBTZqM`lgGTWoB+z+Id^S zO`-`*E`y}dpv)Tt6QUBB)!%^P{@2|;i6*%+H~Ni3xW~+?Itm`hGDc+B)CT6O1b-kg zANZ$EBb67mJ&^YvvzBJ8y4#;559Hquv}}V5Q(Ndue9CYzsx4&2`h{o>i4eQX6 z%hL)_4_yXx&8k$)g^9#a=p+o_fBrKqO*UOq!#e&6c=WhO!=2%ef5v-7UbdI|*KQF*x-l z*MOlq=Ug9V>X@=CY=aSo^v4LIe3Y4h7Y&spUJh_T_r=-Bo?6%3smAqbn?uQXAd0p>XID|06@>P}og@GKcBg3T8n* z)%gqX`g5V02C*^TdjTE=Lz8dt$wpdY7?9Ro*9=1L2QcF{0P^AW;Bp9;*x?JtUF^iu z_z^Npn(R@qBE(aP@a3uefdmGJGYgn7mWTRdr((oYWga8YU~`bxK|Dod1b&Q8I-O%O zEuP~!6pk<&%nQRpJa-6|d)antVoO2dsWPA0l`s*{0>N?_QB3G}175js+&^T?@-G7O znUsWyc&32~SA8hG%b@cL@EI=-L*6*ZI{XN{2|HmN_Vw>P3@__YK2IfnZll3h$}-Mn zvvCs~pm!tSBrmZ)Ir&A)By1hnGdMQmyL zcLr#I16plx?kjm&^YZ!RfRB14Jo3bIwt5EW6b78c2G3+g!h$c`Wf**Ni(xfl+;cWB zFPl!uMjLi_yVCZlRBMqMa!`N~$gs>HfDj=;6!TX)ZOwIcHBF7KaYYJNr6&(0ve{I^ zWPE$PP;9UQaxQ&e?QXEePwxI#dAWKp9@|TIn-WZ$a%TQPFDl?1lqhn1Eu)!lI4pjK z-dDB>H9LGq_I`G-cTg}TzW5x?sBX!#F!n9klQA6sJ=67DlzIA?h=Wq zk@vj%^YiA%aOId%A4jxNQJPsk>}6o@PlWR{p0hsQhhr7vL(DW4Cy=*XUkwxUq7NjSUxzb2? zje?N$KH+`(&}t>M$c@?mDMqqrD^Um~vs`I!J{Q@yj^@(^LN;4{yi_@s$u6Uc<5#pJm-hJ~DZ9KPvz*^YlD4MAqg4P&yNHWF?L zE9`DfaN)kYLn-;4jpNu4S_c~2+j|jec2K4Q?Q4ZU8&dj&>&jT3GxMB4 zr!sEH%^+S!zG zkFCcTg;cg3ndUXv^@kWG_%1^y4?js+rwx?yn=kygXKwcs|J$d?nkm6BRlUFJYZcW| zC9l;eg8(QIH^jdgAt8{7!r3V|Jlj*QlrbxkVR?aJM3JR#Qh<8WKt2E=Kp-qpMAQ*$ z1`zE4(wA)Ze#4SVX4)^*)(o*iHJtwYzWMauNWI%BS@CVyDWKrC-5%^xSaTe63$V5t z&eljLw%2iYe+Ch}{MdH(3n|Ra-Pc0y*$H6xuN@t0+hIf_C(N-m-FWD_R;e*{(Riaf zLJzGoddk*5(T}faY&qJoi$aNc-;<<83Ii$C#6c7upV#9S(S>2^{i~XU(~FyC;ZF;D zrE3!_`;>n(ObkRUp5iw_)>ihzbX=>rU{R;W+B4riSNm9&*gb3D5 z+se1nK~SSf#0}1&IlvC_3z}nDKy+h0h_sr9AUdFe%N-@wN?*h6XZ3XU@Ov62o2C>q z&I$q;J;;-OxLB7{8pmUR<<(EkmiAyH?$d|S0%t-~T&-I}2*j&5M}M(?|G&kAA2<^j zxmvFPktl&ttEi<{pE@NkYYAwKTEcL&{8%rQDwPbw116J17FT>K5wr(#18X_6q01-7=b*9d0@9SN8c_-av9$%)VT9YU6qB z#4N^ex#MkXBP5_w9e5=)CU32;;EFO0qu;$W){eI@d2)6l>o(fj9k?p1K`mR6)-ra; zlVBMF1J)+vh&#BcD`362JOQLPD=fDaV6qfQSwPt^+|k`;4z^WG7J02=Fp3#>1e%Z5 z@XJyt5E>2*hiV5fF}T9r8VFzdRah^V!tU6=R)LKab1eC*iWC4mrBy61CLWG0JvM4r%q#E{$3;;|58hlR=Jg1|>8b zkE4pU;wJoM;}3yK&crqSA_!Tv7*{(RW0yPWJ~s#hzZ$6?#dS?vuUO@|!dV`qwEfW$ zgzy0i6oN=(J8F!GI?{?jNu9zl4D9x|pHv+86%7B`2N74{cWwiide|-Sx5DzX_NIMx zn=lv4jNnOTddK=*&lh>x!jygZmA;^-GB}T z3-oCjO&N`E*?c9N*%eqC*|{@0qAevDNc1$D{eJDarQzG^IY~?Y$8#kVnpX~O+%z`<`k>gedXIrCEqZjzv-Sf{ETq3$|hUh&&`fafdqYLnWVgMWw>7)zmh z5URw(Brp>!IT$){^Si2@+#Bc|r*Ru+4k>NJMX(P(44Yx=my&ayDP#-DL@t|5r*Aqv z4!g%?b2xN1ySKY1)EkfYB~w{I+Ds~aAi?3a8fSs!27zk0=9n*&6<$Ok6Zd?5q-LJW z$JDJ`Mt0*&_MZLCZ$6yws3j*Ne9v$5SANMjJ#e!DclGbWO${aYbF~>9an{k1F!f5I zXV2O%k8~bGFE`DL%1=d7tg9v-iqWW6E#a5`9rSUKq;=h3W~AjzCOLTqCF|XsmQs~$ z^@U|5G+7i{Q0abt#IPPs6R1r1dl-_k{&F&#Eo@Quk?>l^mD{L0KVh8!ZY<2VnptRQ z@owGnr|QD?szWcU&D=>R2u)jrrjE&`A$PE}WI92VuizRC$CX2G5@Rl}DMBjrWj9gh zwRk3hly^9qR4Ws^XL*%+XYa^Nt-Z5;vDM-5ab~M+E#R>0wHi)Ml7y@pZLbJzZWN?D^-ZM91)0fO)m$@0UjQuc(|6EU@j;C8gbVOwXPe;>DF>a zpu~91ccJV8`X;=2zq6=qlhyM-P`kw6jX~#!eGBM;LD&M_(7&L5Uy?+eE|$wIYp?=t z{~x!=>WV_s1sVr#Dai;-BnnEGbLza1NZ8g2r+Au8CbM#@PgI1IuMq<0dz?1QZ8wxY z^!v_EOW_SOqW!-%nv^D~UBw?h`=6|O-cXO8zY<^U~-6r_;2co~f z>uwP2eB@@u4$>IgTkdxiyv;gTR}lEf9U8&yh}b#~7*N0&gl9xIRMlN@n~2g`c*r?q z)#=lPb32HVZkv$=m^HZ`QYPTPi0_&8-Wm5`FvC9%T*!V7eevCJW*8$L4>#yG(T2aH zvBPnV)Fw^W#KWCL{eLhXB)Y{vQ z{{s4SUgtF~O?^PV3OZ>JlDknu87fN4Gt_=sN+as}9LttTXlN8$rivc`cJKE5l8Hr` z?%mSD9+%#1mO*O4g&5wuu7mrTrc-XAlG6(-8sAFX@$}d546|*#{R@k>*#+J;*BMkE z_0z?v7DquGh376-xFFKQNzW)aRNFPZqVc`|=lOC2&oJ)PcojAjBD^&GBa$yIK+QE^ zCp&)=@ES4M$=5o}?$A}3gSM>tA_gM86xw*p87K#u$>v~PM_nJj?I`KuYIu$=D-BWE z7Q4-%y3D+GD8_IjrYw5Bo@{dj7~)`UlQnPB9QVOnu8HDz8)LE3QXoxZ&v@+ZN+prV zxl#5&JIpOQN$sc2dN1DIP%)Wc2nzq?zxP|)_^#V3m4ZjWkqSh=ckef^5z>C=>%4>r zpe@E*@bxvJO)a4DfRgeQU8_8yJ(PNTak~B#A1q}*ha+22HWTzVB_E0ubgjnogY*&p z>-G3nDBN|S!7?$qC=N@Po*_pV6ezu#fuU?td(Y|hE=mo!lCe7xbc{>8rC)Rhzu;X)==UaeLdHPUKjHJZZjtTo4imvw6m7zut;EqEx{)~2L zo4vn$)Fi$79C=kOV{CLyqow)L0KOS{`rLZGVn$0th3fA`V_r!#u;C@;ur0bSGz`mj zF=kuFjIIee(8U18kz+7XmIMku2$NOC43LB6i9!(sNmgV@U}>5a(oGQ@PyYkQuyF_p z0|W43_!=L84eWKq0R$dF6lYCar4VAXBwaadYN7$uWK}aQ-J4{dA7c?%%i8PW;tJF7H(c=NhNbgPu$0UO2Ns`T7C()W=Xti0o4 z@8wE4z{w6FwEMqIeK=0Ru2fdi*DOwFeUW-p-0`dX=G|J4$h&OM#V?;3y;rrB0bL)o zDJvUMNcPk}_dN|D);00n&q4b}MtSiesjt5JeHzAQtm!KW zDB(AA{G6x(pr%M7#|qwBF)UNpRY}ne$I3h{SVuqz;Mpvg8VvZ-(6=O(REPw7B^Gsn z))+Fffz!W1EkP1_o+Vqn5EGNwQ|Swz=jGzR2VP(=d^do37m&8l-+SKQWD~A*_}(>X zY4rJBJ-+AtO~(=Vj4t0Q+GtN}eZC>*)1THleeZ&Lk$<(M*Y|wmNQKGhzIQ=g#ctA! z@O!=&LQhzZjq+Qz_@#o$@HlLOP3NcaL|RpqX9;<}Ns*$EpPHt!sk^onXu83KQ3Oh) z^S6v3tJ}2Fq|!R5pdZZ+qJ6H&?ei+vr!pBu+UI(1pPvjYu3N)t33)z=ri$| zJ5XTJ+S~RVw@B6iyXU z(o1xX*C_vZ;C!XHs;>kUingiUi=(XI9ZweD-4X;frs@lL&7AD}5DA8oc6@ZYWfy~T zES~Pbc>Yad|Ej8v2a2q{3@yaV{a1gGMRP(6lIH7Fa3q*yvMOsEH*rj}oG52&iH zsmcs*&stUgy9VNN2(3|1;Y!=sZ}Lq9;Q)q(z$Ob8IZzg+HalKfDf=mJ#5d}xY;bbY{e5Cl_L-Q% zH+*3DqcEOFQ90IvyqW9%{9=*LjAS!HTMgW4aF+zH)Q3&f+ znRP>Qd>z%p^E~m+tL9SmbXMajjsSBHd{b7Bx_tl49^A70sk+lbxR1313hLoczDzx~ zwbOaelP4QA=u=mq2hM`CJB!MG_iia`;t>Nv9Ko!pC+hKnDz10gX5CAV?HP60nhXZ@ ze?*+F{j$x*4B;CrYcV-2*}eAqZEgpS1~Osm!SSacXT=dC7|`{rVs5|sODwzt=uk$y z>1Im!mKuP#cmEy{DX>X3s2RHRD_OB^XhWcsO!K~c&!e~37YqQXCldrV1Z1S#C5-@t z8ndo^F4j@KfcNfKu{6Wbslm~lz?Tu8#S_xqXj08|}fGQ$i_0%Nu zplF)e`B^Q}#~0i=D~cqT^o6QireWZeCQuElZqVa~fT8g3zbxm#t_2O+-8aD78%pG9 zY95{Otl-;c#KV^P)Y)S3|4<0Bpq}x@JUr!1zQ90t*xHG9jLCt>gO{Lzp}so%?BA0H zYsu-ie;GU9S#+@wV^^CGjiPYBinPjM?iV3X{?L5#-) z9B}=SGbs$?QFH}NGI&Y!2jh|(Y>ZrE?Su$_lD28@i5G|+#{DO z8jTDSyzW%12-~0j^CH_uPVA74P#7t=`y%o5FqNX}hy_fYV+n3N-J=f{Y8)otQ^%CT zsd4(Q_uT~!oIg$6Wic?!3@G;7H)%ao(tm;%4p{BlRgK6IP*y5x$juYan{hi+Y^qd> z^z+%Ncwvi;=Rwxgt9kz4JHt1Q_P1%&+7nf=M>dED>{q=KJ{5|jv3H42C-1laGhj;P zG-t8BZ-;BT6*ni!+di+1Mw)5fb_TW33VLZ;$%4$0WF|wZYA}Scp2a2q<4bgJU+du5?N-w>83vRcwHQ~l)uy+bBcp7`hk=5SrP(-V@1&74!D`} z<;%-3(SGM`^CEfuhU&aeY6$i zWi5UYn$UJ^H)R;8Y4>&I<&Dey+uO-rU89~uZYzq+!;Wm#woqs?hzW8hgooictfH<& zieb$|RkFJ!3>vJUSgMXnkt9X)Vh^a9wINzc=LFAb((OPR#cGX(X$3*t*Ad>_HX4VU z>mXCJMmrWEX=9#p+&bRXmBOoh%5UHlDgtGyBhG8Z+sMBqN*ww2F))wxiyYoE_ZybL6;nPKa8E%X@$i3vGzrV+0+YjxnEmV@yk^~fxyx-&^yda7cC~FS#MK}qE z4-YK3V|jim)K%rA9abP|0w4>W@h31o{5iPH;Qx(|D`*j6gFVF9>YWPWk*$&9#e2Db z`iS>LFh~AJuWys^Hgu{YRj+#(9VnQjD=VFjUaX`=xPFq}(}Lg`635}b^Gs);`v=00m-nV+uym?iNqraI(;#3J${< z3_(A4*$mT}hmG0R0R16?NgBqXrreQ*h<{W&Nw*y- zJoInkG%r9&UctU7{q; zh>8vV`O1uy<<)f8FUB%Q^j ztg6+$SmYI5`c>1@TgRJ-)~^K=)cF;AI8m~)uCb3m^@lqzfOlzBnq!c1{6B?B;;QCry z)*$D?SPyxP1S=TN@x(!%mVt$rV7WRN_f$%wOky^!wnw_OwY6$8IwkSU*6O0@QnmHf z)tg3}>byHJ8+&9fBV%JoJk8oy1#VR|+hjO1T^U|x?}dnlhWmkF_JJ$P8b`vAtcd;q zSbR+TNNd-zOjkGjdhU-ED!IUK)F*D`zKA{B+~pK%7@*tHH}=|1#&K&l8lr{nU>n#@@nsa*-uvWhA#G7uAj6w0Dx4o- zYU`AEHjdSh@$^fN%`r0{`?*H%97rAa0RrXojmi$XQN&tqk+KPWQ$%+wJ1*l4Cx`n{ z-vh<*venbY5h<7iIOLU1pu5at$V%yTq60g+BTj%;HX{eM~Rch+?-Xwhhr#5Rz% z71|RWCQ05#q)ddg4pLGRDVyT*X5ZAcXdOJ5LZUTL92z4DORbd&BTFqomNnUQV_HdK z21!^4fjk7RZ(|s_$M>OV9BYt9p_1gCaU7(Lqoywipa2VKLEA1;{~FJuXfBtQmiURs zRAD>=P9Lv&%s6P=8LoguDD0h9Ynz1L*u(!&GDrPEoPaaoJt&#UY1U%W{Kp-fnargB zp{_hgLd$EN!IK9a?1EsL$z&Rg!i{)dnF#ndxDI{|{|499`tFiR5~egcZRf<9%}RND zCB7Q$Lvj+Pe8E0C|jBx1K(T7q6?sGAS+%23dT|( zv|_Q7&vXTTs%H;_+Kj$l zdA!__hd#+|siVkv>lx_G|XdxOH z)hK0nt*jrM<@<{Au}S%s2_$^xdQf2pu0IOH^h8z9wdL8OIzz41Qgo{AooiT%t-@if z2vjC0YICxVU=+)c!hjn2qOKkLGTfypBJfv*wgAs%SYc?2AWLt1kab7kU-;-4i4ala z0>=>Q%@#Jmntkme0YsW8GN`;Z+!4Y=Qi2POtU*d2>LByNU@8Kt;-0W*5Emh|5(OKs zjp_-B_O;d02B>lYoAApc3V?$oA2tL+22T~FSP`!pNO(}tGT_^uJt754Ln;}3XdDBd z+k}jwKq|fI_Y!F(b;QVl^Un_dN8yu$T*oT}Q_LedMr0V(wiKB?lIp$cOczLrD0NHY zc|np5@eI$?#2n4YwHCKV>#E_z$P~t>;a06lqsa0GDNmBItWrq;$|!j`(j07a?UFPy z{GX^jlUhqDzv-sbZqCc;p%)UVTWT2vH}$?Bo1^VXj;M#hm`tVaSJu_)j^R7~Fa&-7 ze_p9LvX9x$TS*&*k0-%>;Lo4kk!@Ob2rkX#ZpqT#%@*^A<<8ifxiMnXz@xvhWd`Wz z8?I2Du1sc$N@Be`Hi5IuLwm+L4BDu6wy_(1jRo7Ozfm4v)?c#nW}o;*N>~Fd1;RB~ zR|n|?jS0V1J;KKkIHZM>sbZ5!t2MT0K{k-WKsj=gyK9 z-0B(xwP`p|a`UJ%KRR&R0oQ0{RVt{YMZ^Wy%}J9sjhi4osVTN?5KDbqi7Np`Y=KYR zjAqt8n~${3Q5dTsR3OVL2zz6#9|Gqe#A3s&VGIVXsS1^itf}hSCfW$#E#tnEMrfQ* z<}4V2FxU*MVMXBFiNw-)6M5p0D3HXnh{Q_H;j-I-i5TM`x{etl*emQaWPQTzmwFTx z__c9%9gXOFo9716G*vJ159;F{cnk+B)>+8ouIp@Ln!-MG%Shyy8a`>O{kU#u_0IRt ziRuoV_1f?;SD&zMeup?9BMu=5xfB8*!7>@zYqqDZgSS`744RnpHpp8{z%X|r^-4Ds z?k9H6K?=^gB_k%xIH{Hu0pf_pK_p8H1zt8;x*{V?%s|FjvEs(~S7ElUHUM04#(_A# zmVyiQNAFgUmVg9y-~c`7tJ2cm=TMKRDZ{6M#q6L+qcdSrZt&LCkYS=94RIk187x*~ zcAO4zhKoBw5tEV0_~LM=<9M|E#0ZJ|V9#1=72pHC0^SWU`i&Y;F4~H?Z09c(17Lsw z=3p4r#0gBmhP|p(#!QiE&+VW!T#te)0m))tNYXsfho)ah5yMnQmt3{URp$dXV8O6< z)S((6_KvPGc={Tl>$aoZt(_5n90R)-8hw}qV*yJxdfWZCb~$o%oA?$YVotG)9>$+7 z6zgOI%EM2f2NGyhO`@Oy6sC#*Tj3qJVO3CreQ*_A70q~}?^=*bBt5?$bp3Uvm%PFk z9CDu5IykERzoeciYgsCIJ6eV|P}*1bth9WH|5Y9rpuw798P zteN(y$0|lsY6X7Kcwd?JDwtZ41QF}W+Z}d$s(M{Vu`lv^2ToSM_|nn;$PeB^BE!o- zr&x}u%Ji)>IrfSShs|LaNHMOS7ccRIFheWYmP8b^q-HvAWu$h9GbfahL<%9ZkjOLB z0zqJ{kxC`aa#m6$n`(usosqQONG9VBQ=uHigZ-4ss$JE!COTj1pyQZVnT9IUxi7ke zBh!Tcge5K>qSAJyNRmc-&I&Tg^JMJcC#|`FwbI!)Mstqi#N3#^;XQc02MTb2DF!W^e0~r9)FTn(HH(j0gn_ zojbnli^Vqo{Eu{YvCYvrZiuyL|8K$QA7j% znlByqGPz+}>UEN7ni-<@-hkifsb`Injs^2fE|+QX^iti{wldacagfl*Y_}*H4#`HsyKtXOfD7?keuJc(}%CU1Mu3gT>7o zFiGNAd##~LHaV~=-!yDc?1>*FzFc}^@esZeW<-C3BzGa>>8g(8o6VdZfe{c0H0fJ3 z<-YfoecN-17=JTwSo@i$w6*h(ElQNH=9KpU z%H5xXtHEu=dkA+yp)toCO#DW0Ex3mfe?hDRx&IwNd5A}6i}%OAy?BNbd+41B7R%B8 z^xWT1tM5hyChBE41>jIBwSCn^LMN zdA?WaDmVs1CvK(k34Uw}E1ul0LciMjnhNFelN#pAPGs>>^;S<_)umekValcYzV|oD zYPn3H?)vzH1>XYd8}nV;U8nm4E)xw^Cuqmz9qmXAgNa^_nl_Ifx-XtmB4BUCEE27! z1uX9i`w!l(xX&cPVzade21*3Ck}-Ov(H)%#XKlmDNg3IF)ZMdoGPP-gMI#KTrH0~o zxlW+8Z~Bxfky0JzePKyNr!UB-E0;o-McSHTRMmNE)6?`5^6TS@BpDvsUAtPtnJ(fK zWj5(bu24WtF*SYfB$B=&JG9UTF+)!0g425B5R;SxG zmOI6uPm5M!Td6*|9Ch*SB=1winr8vw^sKCfqOVg^xlL_QKw%Fid2=tvWi}qmW>yI8m!Q_CUoOTdA$eYMe#W zNejo+rJg!aD7f-&Wu;-ar2s5}ZlfmR4R0TmFtu$)m=rgGv#r2diT-n>vu$SR# z*-$g0b4Q9M6PSD8q6`{Zb;jbw={W)kCH}i_KZu%2zjERuyJ{r1 z-qkyItror7B_B-k5AIcsQqf$hd^3~1B9YBKlHs@7YuXl~P=*zEXZYIv+fzEU(Pz(? z$L_AVa6&S+&EtCNT~{(F#s>aS=X>apukUFquURg94l_rx=n!rB$$NjT8aw<%dBBMHtTS4zTp>bXI0l(8=>t=t!Mrdn?ZI`Fdw3W142eO)Kqy^orH?hG z$O9G)83|HQXQptAg1KkRn!~%C>9s6yZ%$QLDE>j0X2(Jl%e~?I_JrwN0@H^33cG?Y zBA14VyFI*4=X68fo-LJ4R8?E43g|71Mpi-^7l*akmY2)F{nJP}bJtdEJi)UqSRCBg zGKWJUd+!L6zt?vEY`R#+Szx?eOvhM%R*YUdY4>5m)I1LgRc@;k5#06x2Fm=gs2tm~ObD_G z#Y;1G7`8K)iaU1asPbg+iuTZImkhZxii2%k(RsXdJ6sUo%3nPF;Np07Arb5@m-6O|f!JIW+SLS-WQr#uodh5mW`7c&oI#TdK|)ZG`@* zx`xn+i$*rWl+!snMO=q`kcRC)WEOT|v}905v^B%At>%EgnMlJPi_M}d7EPofW)kwk z2@?(Tv7{nICGlut7*iCRd$EYbJ%*c$k9FjQVPWvr52+KhLK<|?0(DSF2*vE_^*gP4 zlielu4zu3s>jlzQCS|)QDMd1+3q|kE7kZ4)2*I?wEac%UbDUwREZ`-}*Xtl%KQ*b* zTeHVNeY7pZoz_NzT57RBGMh~1pIp(PDD~6j?R@-SWxH?)3PX*|*oZX{RI*J}5P2KJ zNE)`qxXMe8HY;(FP=MDIX>M?_QlL_T%Oju8rDCC0&S+buUJ!K;j0MFdVl>oHSTvrW z$Jl}^v_oy7cHtJ97hJ-ba$$G-=CLvuPB}Buq@q-!rqfBw*%qB91<$4O|FSozw$bW= zW-0Ncwv?^8DAD@h*lrzjN-5gtL8!(o$5NZ@K>VS>a zgkiR_Bksu3n1VinRZA*fbl1jxsfm0PMPjrvLloVPCv|#o&+qu@}m8|5sO05*ugsORcF~ z0$vE2!|x)xoe}nqOvl(QlseiB9w?vy>wz8lgRPkj&-HG$5UO5zzF*i1oO{G>xf03o7HbuGFvlh)%ERb19|ESGoSk4%_`J>^EQlaLX)Z}Z%7Ytk5_!11Ea6iJ_ zEQi*fSlL6{p=3~F{_r84$qq=1Y>^w3a$WF&Y@_w-xaW$2`^=EW66&vQcYVnxi9vC1 zykOE3Rl@-#q3LQrXsGK;R#?Q(^HOAtTSfPhCjTZ+c_6?7edMTrxyKC+CGNFA^>|TVk|sOj!Beu+FI%^x)|- zHDy9HKLpmL-&RV%d4$3RIh)E;P6gfdN{UVm)x+j8Nmn%t?T2VZ;tiOWK@PDn+yxiG z1Bc;=dTL7_x3Pq(1Vfk0B_MQN$C-YuV|e&^vaeI&#kfIX&anp+00qdAr3X zAD$C@uRXGC{k387zBY;@(sot425XSgQgkqKd)_F z#o_QJeBNl*F7DQ|W#C~1V5rt@5nqGR;^2hEB4a#(hGEnI< zE$6rT6LAUa>U38jZHL7%S~SzlW5K~ZzQ&S0SqNvWv`#sChd2~-73N70x1j!BP zt`aY5)Agh|$A{K-C3CZe%B8^o&DAwiFKVQXl(itwtqnBCu%f60vI0FMHpn&Q< z99kogXaa?9EWQcJ9U`3ZBG^t ztjW&3ESO#534m{Xvz{uw%QyRp!h1?b=-=;t5#A+#g@q42E9`s#bkTQkbmz zyG03!ROrYR_9leTOxl3We1;D|h@N;zPt`5~u)9VHiedMo2oynZUe4|s+48+{!@1hs zZB2`N%4AXYcW;$+_k<(AvFZ5S+on4WWNdoFPyc+h5LX`z(c(2fXqH+%{18}2v2^L9 ztU>cLHSznyH;lix`igMVdgu;8(EYY(+x<{9fC6>kCGPI?wP2vY;u*?DunQ2j;`t@z`S8IwOht)i z?{@A`S3RUrUC9E|gb*62Nq~AYArRJMP8_VNodB)U+#HKI%9AXxH)$HqD(hfPHVsct zz~x~e=(kUorU5r4q*S!o>XpDRS7I?+f{vJcUY_8c+K^X{m_Zv#m=!g=ihn*^mEI1LepdcV1G z5H`X_nx%eDLS#>X=&zUK>!;y=(FWc$@=Dh%9UQO_q)wbxRcEZcb+=edvdJ<@6XcGZ?nLF@3+ z#SwLm2Z)d~AOIue<{$(nIdK-n02fCTegizVmq2mpe&|PHhv?t_LZsnsn+2rc>bJAw zuTmO08ltS&q?#mJsZ%t}Skg#t8NVIm&)S(*Jn*~@8^s`=IMK%_jz(wFX1_OlhumSm)ZmnTQmd2d$T@@lC0k+)b z6huYvn7LlmQD7%zIDkZQ()porDVYX_fIdJU+?zH3OE*kbB5?P$Fi?})Ze!5;Z8M3V zDO+2B^oF8oH0^gC69nKyPx+@1jzn}~9STH5FQ8)RPv`nWi;O2$ZU2V6eqnKh8r)wK z!B+uS!3ljf0Qkfy@gWkgjbI+IWG0*@ilIZLNEYG;$9-u-3t7WbR7$TEXElf7ADCc z5dEgJQZUm+96$=uTTRe_49H|N{!x-0wezG-_S;$G9Z!DS-U;^M4lptk8aoaJf>kA4 z*qe6{K~9wGRe2?c%iY zvmdOCA-_c$vKhc55XoMi@2BbZFVSdg{x_U!@!DluTds?9KuYt1z838gyBcy?2%>SR zg!`p&0&|gvD*7mf)$Lf|Rv-#`vXaCxiY#=EMe+DLalnoJhfvh*vt;}A=~y!3t5BQq z(uXs)y9fB4xgnVA0{7mK;HgRSY;7d*^aBp*0je>gQ#U#TcmWFSUWQ9pR7q4vWGei2 zWDXdBJm1oe&|hvx@Y;D1bt_IzT+R6|VuJ-DQh{qG!6-0#J%UB5U=(#ClUjr;Fkm%v zUj}ZXfO~akMfqbw5~)^`n^aLxU&UZ>j_#qm(jy8Tbie>9&;|ygrQrh$tIm@*K}lQO z7mvepjLa;lMCMVK&QfB($iie7B5ghICB9vf-kvcCT(cwbS`6O|iY*A62$VE*n+gyDtTxiP7&?HG7Wo8E zXYOh{kMHg@(Iw5SE7&n0QtcQI6O7->W@~KCRjs zr84a=OoF*y1tjW9J0f2q@OqYuT>X=qG@8)SXsX<8NCW0Q%4PfiXId%!H}A#-K4_Y@ z;jSDE!DCxo5gMx5)QJHJlHhJWxTZ=fn7y?88!XH5JSPkXk&0l;mC3^o-mG40a$lKcD~ zesFou*zfs8VQQR9Q+nE6RjWAJ$~fdG4hj?q%SK??Cyl{=OOsEAq7vyxVG>S*|2*@? zSFzW(F!aOkbfUP-zm;`cf&^(eUv0cO`_EV2{IGTNNC=v)33V3?!Dc~G6)E0?h87i( z<`v++5C+f$KMN}j7Tt1bDw6XEVqL5%+|y8kA}e0)0lzMUss#851k@NHiMN|9m3P*Q zKg<&k)K7l9;mT~YkHe+;p8Nk_GjZYIc5&Ph{Re5+T{!^I7semto(*`+H}35Hh)-z+ zJ;@1Y-_%S6`jfVKm-yws*PrlaXY!QjRN88^&-PgF*o?T~GNC6xIX1EDW@;;uNzVxa8 z-N$i&J^K4-@~gifMSpnV45*6Tza7*}+E}S+)VCmrz+DqLD7lYSc@XHsNxcZ4xQZET zG0GZS5ax^45h%&9H8n+zuTl_sZkNb!;9-TI_!994-qKKCEZGND1^SFA8+zZGt{G

pc1s;McND3OpYOB1w(~IhfaAR@krpZ&3xU z#CovHAo%-BcHz^Md<+6Gusl6GMo8?hR|&7RL0RCTDDliU3y~18VTRf^WSzclA6yja z;@L1<&)th?fV7zGIkla(*H<1`yCbq0ajJ2L?e(X%&3Nw(9?b9TfXsS+JYET~6)L&` zHT6)5*t~ZRJmH+HsN@ewGrT}^tN>h?Og2bC3c4WWrK>>zmqH1uu_B5w5TfU@AST6z zsey0oGR64_HYn{W5sz+U_P!5pM=BkLsB~ z%M1h*W#jN}A)<@apb-sUpC%AM!Pm*gBw2xCpWh!YqO77KEFwh1GZU?Gt?jm7Qq~tlNcY!n2XbC`4z~I zkEpUINga92dol!3Gs#IssEG@=g^olgT{CeL;kN4K6g0ixe?HG1cN*l*rgHPo58axlB&b@v3`Kcj9$)`;su-I>}0lg3a_GCi@Nd}f|}@vWV~>Hm|Q z>uEq9d%^_hKEL##P5dgESG3%f-PLciUtiWsCB1#u1Jd)(pXk__R*r4)E45SDRp)B$ z+yXo&g@UGQtblD3eO$G~j_TP7ja3ylAu%HuRL~5PVzw3OLv4KNMvy&$nnRax zu(LP61<6rov{84qBiioN8?(oxm|jv3)OnH*@AOpJrBs1#u8^;X$cJ6jk7)i)QQ$L0 zmc(yN%2LEs@y1$NW*AsfR;5`WJ{rr&l!7(NYQ0X%E4DojfVFSUBS9ebC2(S22N}8i zfdyy0Ly8jvvw$*$TaUFvb9;N#$uFg2HT&PwhcAPzy@xq_v1PBGMe7t0aM=^l!qN4ntQ`lXJm92hjY22m+0l$ zOsKReP@^It%pbX*`Ti6zHV9!w$w1F;0CosK;@j}TaWi)PwvAp9i|HB|5>J|L&_HQL z&Pc8IPPLDa<@Kv_vOd`;UW{3u;|1T;Z6?b_`*$$GJG-VkKnwzEG7 z?zk%!TMc1e_}vb2A=;qU_F5Kif<`*X4>Z}p10P!UR@8<=u&9~zfyk3mK}d_gA5#C zcR;JM1T=l?&CPk59`9MNE%{Fsw^0#o;~HQvGEle@P6MYy7Bd_)EM2|VHO(}&R7Mbr zzpR3x0}N*`*o&H4ka}5jj?-9H5NKL`^Z9{mPRmC_geTLoaPu8D*%{yHf>>@JhOi!% zNKk=4M+IJxHpd1R1VjSFSrUDKel5rdv$&k}ehx%$z1=rWaa~MA(Q5nW4bQKC<+X{u z{jCOiF~Opyov02Po+&?LPdPZb0R37B(-~k41(kjd_g2T_il^<2UJ4r-eVLz@tHb?N z@8xG-Rn1+n;kb!0PKj58CBC%70OU4-O$zD@ip%=!mT)hwbe#I1l2f> z8J6Q%mZnmDm%PBUiH}U?1o#-_!;|m?JehX9kGQr8brA+8Gziczn(Wr*sIgCO(oh9K zfwe_o$q2UHyxpV=5I5tvKZiU|I64uij;P5jk=9?$%8sM{enr#Q7P%tofX$JtG%Bys zNz#=Gr^JJ6?FGYRC4aFkjoi6QD8!>yZcsCHN_6LzGNvHL^yr(L?BdYJt7R$bqLgMO zL3RA&ZM#9nrFdkjlENJs1Id$<==jI};U}lM>uu#?652w3D1SYm^dM*wDM=s^HQ2M> z=-NwTh`CtUVqj4p8WSt{DdrGG#p+RQa<|l;+t8J?m;e`O{T@* z%GN#%i?x>!W4O3v2GwfV-A^}qck)=zoG?i1eor!BdJFnqJ#n<|O_`{w8jB-DZWf8i z)Kn|KOW75M$59R4fKWQ0wzLBWfPv^-4%OsjS zT(66&?Kc~`uw2LXe7|-#*OZko;x!-u2dsg>c}Et!Xr|0jXsHlNaTSz6zXMHb*(*$-}znJQqM5tg{1qIxN9}?oie_3K;gb@C0{}LI60kLMWfZlmhtGm z)@h)TLl%uTUL%QRZD`-=kjYVhcT;k4zdzf#tG8NMnik`x*E9h^+4wMB1mBzky1{k8 zWpc!!LLsTv5Gz3{bS}1{)Cg%Xwu#FvLyXd(WL6f9Br-%<;yENpvaIUsYBI=^lc|1;zbWgtJ& z?;VFI?mWiDKOxa;hSD`_hOxpD$glMQRR|1CbAs^H3Smz+6hP*A^SnW3@@7 zhig1PewtsBZRQ_oKyDTY0fYdE+Z?%$8%=b(oDP<2yYbBPH=qxADPV+=1B4|RtD)yA zjEWXy3~C>T%OuAVoQ@;=s3d~SF>#XXU zuG?j>{BwW7LAU{)g@f?z_t%#)nXKfXvIxDRB*O-SWK+~{-<#x3c+xh*K{%EBRzulR zhX>dfdc+482PB{g@k%yO{t^x%F-aR+>Cq2@A#xvcu=#TlIJdRDRv1VK`~!;KoTsrr zuuLNGV=^+YL6=)zsR? zD8xHA&mH#WBgfC|H+d&={<59NPwd7?=t8=mT)lD1ZoHZBmKGnIkqdiBUJe7>9b{11 zte@`u@XyvL#ML*~G74cob1XEmND`GUz{YuOF>CvaeIFI&SmDZozF#3P)NI_nC;GEf z8I}D6n1pB}OcW7gxz-~!6})HsRCMQ z0zNn!ssAAqLWABrZ2_4EL1#RWiV_5zOz&W~kr4JC#ID((!tF6%pa~YLCK8Xw z3D3sjD+MB$I_fxfEX$;-5f%%t zf1?)K9x+L!+FVrmU5EAUC!=ye@Lq~{OY!a3MtJLBle z4?IrB@8=kd!=|E2S10AZQYJSTGhyX`TG;Uo&%~RdXdA_k(&ws{MShl=4Y`UfdF6<) zMrS;!Am}iYgOBazKwS#5pd0oSx~Url(zIok&~?nmK|2BB?~pC5hUtLoO?vW#1W-Ca^I0SmO})C0D2EZL4~j=Z%`b>{iACNHfO; z!UpRy*p~vnObSiB;QQ5K#JGl!SXb;1=`FNSt@e9uYrJ8?#Q>R*CQV)tdbm?5)#htY zEbHKFu(V>_t0~2lgE&F*Sa$mcIs}p|#$usB7)1#5Xqgh$NG|Pwu1JE{6=Op1P!AX| zFGMh@D^8W#+;ohj(z5zhzNf9aF8sEvW`VEJAvs+q$dIoj1_iGpQC>%mmt%oxum#~F z?Z&lIUcYbGj)8H2dyBe|#S5Y!1Omh$K8k$oj(B_^Q05mYg8h~Mf%1b!<7+s4Y`>OR zttLs3QeBU%t+9riK!67R!?Oz=aYCbFgoVNrvnWt3$F%|A0O+I#R3b5jpkgd!LX3(T zxSz)+`j^Dgas1~ZcDAuAW{xu#sSyw|QwYVJCm}?)7yj`dC*7G0D!Ng+B;VqqZi+>( zZ;w!T?3&o|x-KGCz&5gUY0@zuILg7|875Vqqwa04_O!NOI1-TxX9c0Oog4wLiv(FF zA&kH`%UXfhxd2isE5NusmT)^M7Oo?^p6idfmh>$7a5B1A~ zVt-vH(Nm13`L25ecpjI$EVot$c6~0 zjYk^#YF1H+Gv)?J{Zxp8W^J7;ox6@`k`ojDy5Sy@$Hj6qu=xFWjH zCgnq+T%VEXwXF2LaR2TCqD(dX>Pb&3wBw#<7^b{o$%?H$*Ikg%HX|%wiFOoZPg)|9 z0g5;oA8$vQk|c&}Ap>^8ZrDY(yoa1>aX4A+2JyPrk&9l7-Wyj0XlX2?!C-(~S(aI+ zDZI!_EMDmww_ZPw7_pGYA|gj8_|9Dip;a}*VJ|t3 z@7sZCdaaP3qwT$7OjfC=TaikzrocCOo#ncEl#&9@hAtZ__UB!B~Uq(vmQfHTa1YN-CbpnRkr49WKE+!7Ur#xO%EgXZL$+&bS%r)!`(;b5{fy6qih?oWSe zZDpZEfB8$WE3SGR$3M38;bQSQAaW7M&+$UK_U5MW{p};_ZAqBdYLSgN_O#7-uJV(d z0vlUBF_A2Ua9&|h^U*TTOHzN=UjRgf%;Z^{Xaa@%c}6c?I9Kdx`pVGuMA#6}Bo^EzTSSa2yG-oo)WHat^iC~PF#jSR`1MtNcEd+Q?|#k3KH zI$qD@+m_8waRWATYmEfI7WFdr-sp`yRvsB8irZZN;2!n`&h7=Hsxg>>ij5WGy=xC-Xn%ht`Z0V0~ zY9g3Bqk1vmg;x>-fk-aubd zM5!P63q_c3Q_Z+>IK|U5>`?7XKR%4WcIYFi#@WjGu#>1STo($GpnIIL2Xksg=vhv6 z+8Hh)WN|-juB{r0NX&RyzYfBhIE_R|id|1-!hl&(FI%}VXRnua@YZgsgr%?5X*Qsn z&YZb^=)JO%*YL1R};;zQwskj=X zXj!K1DGYMPvQt<>VXb4Za1i0$d)@?tm7ri#dVL{#6RX~81lBqhXH|$Kn<&NdT!mmQ ziHWKeNfSMQ02`spOBo*(ez!BW|%-{wWiZA;BR@*AzCb& z=4oREY+w_arT>&|@*HNetR_+CTfqOcNPEv7r=$?h$s{iDK$D_x;??TxBR&AW2w#LR zz!%|*OC6tE!VoB1^=N^yB4-rc$!1sj_NbvH||N3NF{u2kumGT##-N4=6x)+UicQ#`J$L{#(c3cBd!iK4W@WY2xYT!EZZ?0(}K)6-ZSbW+OnLWX~Td2_q5U6 z6=HUT-x=67TCHOskb}`~>>abPu>$INMA$*0qdFa%khH6-b3i9$jvCw+y@k?dAytV= z^cD);{c@lk`i*lIm?Cj5G)ZP~Kw@jHQpq`mjZA>^SBG=>O=6jk&bMuJn{7<7TBY4s zOB*E{tts94LX(-Iwhnc}kZOMv(KrnOQV63(X}7exXra-{lL+)pTZ@IMA$YbKgTV;V zWEw#s**QYm5D}7Ftk5v=49598^{grd)gq$Z3N5_a7*^L8YLSX*qw0D=QPi~(t9msL z>dx=Rn1|uc-M%qIvnNQVK`FX3CNDUSy1OZ069YX`%I3{jse7%G**2n>q;cqQ` z>lT9fyka9|I_HWpOq3{f05l43eSy1k!*ITw8eO_&wKSE%IXUON^NG?pO6j}gHtfg{ z^LpiwG^$Q+HwV4qdSi-P=7n48-md&NOJv&WSQ|n~Iu!-OS8*A=T}$>ZOWs5j()X$4 zb&wOlDt(YHT{r)8puX0=AhMPuR|^Is$mIIz&h;2LbPNwhrjLQ6lZ#~RkUF^K1RtH3 zjEEa7D@6AvQiJVv$F28HwAD#g5^QL9tjXQ=Lv5fP;8MWi`f4(n9|N+P6g>sWk|(rH z70cC0!WLdVuj~pOJ%xc=ZN1z&B<~X5q^?5+9V@QNNaYhqcm2+iTt7~dMQlfno0d$e z8a{=~7QmuvD2=*oV0K=?>yxDrp%2}6lkv4xBQCA?s!rxjOJnnb5VJ?)pi?ifEH!#U zEl1TKO?wjN$6`phu_D!C*$AR)1xy0>r<@N_oKnZvdPkewrhcMB*%+5dzl`F>gkI_O z#|Z5v)OtqCN0!PP!Fv?}5bA6hY$txv* zCxOlI+YlNIl1Ua%k78F$W>5*lmB3L%wd==Xx(qYfsGO0-a>{@IhepeEwAMd_7`~}6 zio`XHW%Zc|WYrc;T4b(cI6)rM`%K~j|56cwCt^gEKt#Z#i5()q$f#J!^gLUVijotZ z1-6i!P(+AEyCjYnIYa-?=1t6k!+YuC5 z#b(O9*9D!Mz((96uR0hjc8Wd57M5UBJgeOua`<_76zPiD`LG+Vi;mlq5#v%OVU?-a zUe$QPl?)vq!N4l107QT!BSI!@Ck~0Ya>SECy0L&(UQEn2)~vaA^|y-|v0| zK=P_d0^+ujU}q*c+fiLmmixJp7)|OaVIc=7Py-YTqBbZ3R0u%SHtv8Rt;QUPF1b|* zR~48h0SqG`*UHC;N=KfMB;dJ1hh$1Z=s#}tyMSgNvJbalYCl8(EDILvn&)tV{q)zP zk>Z1onI7Lk*jALlxny(}eWpO9p5DGy7`ZTO@;?U=~sYn!q z&*M@Qbi?fHPUp|Ltbm56Jj4CG)DxU`k^!tg*qo>pLSCG_el*B0P8 z@EIqq7oUod(mcfhl=#R~6d#|MyVGKZ=>Z)0X_AYPtBfFT+9Md`VAZD33@AsI0$k4Z ztz<7i5m)dc4CD2ivWv5iza^YWf(pt*+8IQP2i6^FaBd&>B09KeUy;JcwJMQn3C=`- z-a(cNqXm#h`iXOy?&+v22jg}mEt0Y3#MtbiK#{vJ6#fCX!1eG`_;WVC!`<0t0T!3b z5lD1Da=L7NomX6*)*JBsND0f#Fl39u64?#9# zg%dWs0r`y6cROar%~uwNu6qr3#!*J?rt~Q^Tu3j^&6{SUM3|?kQIt7+$`*EIQREl5 zelr184Uh|W8x->HVy!o{o_XZ$`te^u@ju~zc_~&Bq>^#g7Hh6>7?OSeC=RKpf~F`| z&-6WLJ=|&Ed!!OjK{jutu(jYCc7ULXNuAwc!!5kdwW|Vc+t_3_P&LmTl!^*`&maYf zcHmhYeIz&oR?8Y#kMx0=U8zE<_UqNnITgJ|_x-q(}nm(F>2~a@lMyl}h`42bmw2E$}6C&keYl zfY$;y&XiAxFqUbIIi+rPRWuD*H)QMs|16=Zy{oO3OD2^tbjor6Rei?pwGGRDTz!9h z6r<-dnBud`C=`|fpc`N0DxS2)Rr;mU+~;G0q}Tl@cSF{T`Ywv%IBGF;yDj0!tcT=| zCLG6(+m7Yj6n-l_dGV{d!D`;rOjiyHK?W-3b|V*X(Fm-dy>O(f0UCv@Imz!`638jd z)tQj6Aa@IYC^$0@j8>Rzp~Em4w!$JG_`AaX4QOX7N;-{H%8sd#Q1v_^LG>-+q z1?Z2LH`HiAjTRdbdRYsk%{XUww*?l2j?bjq2L*lAegwz+knqMn7~A24zou*D&Dfd) zB6V~tIznq}%|HSsi`_$3sY>{msJ4ltHl|>r2n*e{TtTW|P>-~g>Wv8+>Sh|!QdM8! z@T0(_4YT%DA3WQW2!yElJ0M?HC&AR~9p0Dh$oZt%*&lgeolfVIzJ#?0M)~+vO$}W| zL>0VJ{srwQIF5ZTPS)hOgGRQ4pqmZKr2nh-#;9{`sAN6BFInDs{ zk1ugBQ%ZlFYm49Y-Q5p$-6XH{QknR%qK)jO28=H%>;wtb>$bJwLX01Qrt3rKyPF$f z0E41J{nqzJM~jL^)0zTILnaGsa3Kpe*c?TfmJF6)j0B}Fid+sL)Q6d+LlK;@RGNDwVbGn;Q(^LUU;WlIGQp;$)One0Xc zcVCN7>pF~|6|<-sfr|-lAC_3=EHEvgtti?eFaF~vGEg$W5l$nfqxuBT5L3oG22j$? z3%M!;>QO8>Jcl-vy-zI;#2hA6AM}tF6)3S83(OS0<%DQQQKHG9Y8V`(;7Qx%b ze83Xkh)NJ5^l%mUb01thb}WhXG?ej@uFuI~XV#txl0XInB#@#Q9vYyVAxAk-ZVnK8 zAAvPeCfRyG@yzf4-dp(K?iWjeI(U;qnN+;5(Eg3I(-yWdHrgnnd>X~L4Jlx%VPCpj zO2e2XaKcL}I{~j(j_lil2|+b&6$q>{01G1QJp|0L`2Yh4I$$&LHiFFdtk;mF=UY=LXrv}tZ+U296ko09JI^}Fl90F)yJf2 z;5?)jTyqHaQp4g|&b} zuhrpMl|}SCnr-?SF!h>@vhYW%skFRo=+NX9oF3^G3N&vZu_i0ubFa2KI+>k#xAWV2Yq4QkDnA5o-uPM^WLCQ|f-M>` ztc6nb;Pl&1b&j#H-U?ct|7;e%3TNPUs=>X|3%Rm3e<{1r0xc133DCza3z{vL1AMB= zGh0dnG-~4wRGv$$et{xPM*aABn#A`AeA%E@q$evOE~wN{sV>pFx!&QC?^x=rWyX|T zb|k&@=;O5ok&(*~K6NsFI#gaFBGT?hs1yW>U<3jN3nP`DjDKdgnBAQ=zd>!_Q`Pd@RuHX|Jr77lL?^?X zPCP6(>%Pw>s*>QO1w-fgI{x6u8d0ZkE`D4COp1}vX;l`{<_DrK>Rm#fm zF=lPM!_>-=h%(!}(4E6+G%KE>ENp+^<19)O-oci(*%+h&e0KkOAmnKzArvYCAy}`r zLauMm=m@chTaM$ks*Fx0dEfQ}O~|j`Ie=HE+G-wcMMiF?ia(Pc03v-c0-`KG^of?+ z_m76jaLdPV!Lq=pUMuAaTUKIQdJ&SN&EZFRN}V%r9*PkxdX2ANiCQWeYVtMyOEb-e z%^JRjjxO@@sh}Va`&1~`{aU`ysoDP3SaO0x4LMCkzf;)js%}>4&Nh3?|3{70lqz-T z=GA1D=x4y|XnM6atu0h65(tHs$%2&OLV~tLY21oGAA!!eAX=SR93#o@=7wl~)x?gh z-9dG2Jb}cO!U0Aig0%ZOzmQ86>j~6k(bRmTUQA_$nY|~_kp$#Gge6MqjYGNj!*c)o z=7XOc^xRcA|NhhP$!k@v!KZ`giZ1c+)4)qB@EzcLg@~!ZD)Es$5YwT6XnzviJ&SRs z2VN+c2sp714-JRE9$tJj1G#v>f{fb@s?$3R8I8j0qrXR>Ws=6Rnb$~yUiO(GA9&$P z&Ye#m;@t7n!EB>EIK3(b`_ltby@9eSzqRzfgXr+tCyhQc=lmL7I6-x0jgU9qaf?DE z=f{xHNJNKoIeT#xbgw#2jQYR37Zruw>|WgjWb%O?HWaK=9WVsx=b>pr7RM=Bioo%o zcN;eevk-`_-%73WPa?3Iea8V7KrkZnyQSp3O9{01!{N-uJ@3o}A;&>02xd435G=?; zs9~E)+I4cKIxWG`XnYLA-x14gN9D?``>Pgcyp~y*s_Hl~?y*=P5(#vb_^7-y|FSPH zvacM<4`P$bhTvT-eA~C;&kvxIdypgVb@#XtQ#&OeWgt==+=}^^P&AX#46@4eDg&G9 z*d+4NlmAra8_>$qXl*!KkpVJK+e?!d;@sS0NNiDDd}-bvt3bP-@ctPGl@`l;=Xchf zzVPXt`y=HA5{cfCM?CYa_4>;~13!%t>`x`;m+zbYrhWQN?-|bq86Xk(>HMqy$J1#F z#ne$3?^H?1DLqHFr8v*VWz2w!ApkQ8B1VDXOQmf?Mjfdpl|8*tc`7LqEeJd>2tchQ zs#hU@9*uEjM*V zBm?K^-Tg69XUwyAI#lrHV1xV9FOlF3)HRW%rbSz|-W_p<#grL)_(YN3{sMUP3tT`& z=!U4bGLn72yScg`q^-WWfo%L z@?}9>l8JNm?Zs!ugn&758TpB>jEzgjp(-^ARU5~J8o=PAN%r=n3+e%KB5Lc zDC|~lh_ZL*o$GWK)Kv$ADziNPp@lz_TcO z-d}>x;FaQ6{ZG@+A)S!)5$uBtZ@kJ2YJQ(c?Q$ry)PBjxsyTUj`eu;GZ5n(iXtj4* z94?!cw6N$ysy7r_nn3PliERBhIT!WCiT@rPD@==5o_*oox(zd&wtcRh`qoceY8Uz` zY5YfD2&8EGE$eUJcWEi98v-)yiG8r6R;_nbN^P~z9zp*6b5k=21cMM3rhP)`2f1S@5D}wNJ;;R1t81Em%j7)yk4Knx&+#k{QDlrF(@6%}MP~k$G#IZ`~Ok%#(Vk zT{eOond8aD#j#Aay7(nVH7{8~C=%yGN+m5`&hQ|6E`6xKg`7#|w<~2XWSr@7&x<{b zyoG=!>P$w{b;Hmt%S_m&l|__fgN)#5xC`opg(CX~!*67f67E15dw(DYT{Uds+6kUi zq~gOCrS&`>0?5Zq!B#PU`i*Dtzw56e__+A?cnb@=ozI`V)^`ePjET+9mQ!gvMXKCY zLrwjmC%+!`@=lj0?R04h6@G*Ng#}zwjaI05R9bYAZnIFOA>-H%l6EUZOpmT|0wVBsh#$*dx^Ss;Q++F;rwfW%$%PNGdOW%2$pbHM ze(Q_RR_M-0Zv+`Qqd=w(C4=@#&@SQ!Pt;-rUEz ztm%>fmGrUvsf%|fr!BcGq8-HCqMpon84ulx)!I7qPUf0!n%Y_G9-By5*>pN<8k!c1 zsk+Q@e9rLl5m+n3TDTFW^6}G`SOBy`$7hzuo;*W*^9B-(FIq3A(O`Xz3x(&azlJxm z(Et4!%JLcaoBo2D8|x*qS0E8PZ!mMxi_SZ)SxC#BH)Nv3c|+SYsUkmJ9$v2La*K{y z1c9Xe#h=-uXYC*=U+`%0PsUDt$XykYWjl}EEQw%Xo5HVj4YN;*hhCgPRu(5qj=Uq5 zV}u`&vX;9CmF?x;p;;Z}jvN1M7(MlIDA|F;k_Aagh(JNavNR{~F9amkPZ9_mkU#?m z^uNWRRIrSM5*ZS^c)1@O?tDjnx%5OF?Q$ zoZJ^{Iq?}&83fYB#QA5z!}9D0kZ4@ojzhB$$me$u`+D@UnAqP@o-Pmvi>HBh!AI2i zI?f|-i30QikrHpMfd2A{Rj1X)zL_8jz(BP1dALa-0cMvOzZ#B_(THhjW3MKLKoIU` z2D)lGLAwk%X30xlpci)qTvYG-Kw#>R8HWcB<^}qm`|cmBE-0MN5FcND%Y|H!unbeq zR7YL*N{W9D-wm$w|us`nB?>Y1wkl)cL#wM_wRtSI|0KCHh1_lmK(|3Pw z?ESe}VE_OC-d+B=N_WcE4gFdO1@%3o)%t(Z5P^*F|K@|yZ|{Tay}%j3<);uUuw-rZ z_kes9kXuLjnLFelW;+^SdVlfxQRRQokGte4DvucFl-rzL@e}mRm2CFmuJ$_UQy00P zxXtg9n$IxX;aBJ=-FbKg?#W7|TxjSVnXQ<`NE@8{sKXvkyBo<*@(E5^+c3*p+##3Z z|AZF;If4U~&$P!A*HIqfE-oRr;~tq#$piT^&y>jhH&by~A>9G-E-6)LyDPqe;b_)} z11PkT*f`A9BXno?r<$0gehL_gRgWWPAjr!EM*M}5{*znuc^$w(|9^t> zAs|@-Dw8LP-btp*n3ES#>ZOQWNu^&U+2vHu!+ANH3D02>>Z4*k%!U)N0nX?fp)AMR zI_l0}%HR~1^fx{D@P7oi0nz^S&&;jp$L^B`c0x;9=4=>GCCO&$1lcFx_! zcISAG0nQtcarYOo1Ae88qqB>QoA*-;MNF>|yZcYl7x)(C&Y zTe#e_9qybS4e^CuFBgOK-D2ljC|MmcU;3|bk^JTHiV9e52(ekM1n)~`vQO49& zIMw}g6qLPG_4}p1pH=l-oVFG{_BC$&8r3(1oW-K#@pqU^~D9Ld68orSIPwR#p_8LI6%3>rIfTo)CP0g_2TTXv)gM@GoS{MqCQM z=Hm_m4Ag-jNZg8doGZ~K*A$;b8us+%*XVm^bNrH8mZ5QKB%C~9m`Hkj#9eVRHd|8enySPN*&tN%vct! z?2cpJiCV#=-cp&@x*;x;pUV9lBJQun4e_x-*`T&g*uu9=dOdF%hAU* zA!yu`tIdt(QOxe+BDis9_|fJ;U&+&*j`g!!Zg-r~#~yjnHi$BB_8)D!Z%g^e;{o!^ z4(UyCOqFSO-Rn6vAa8c&!j%R+YVum1`59_${uC#4{e12t4?HwAPkta<4UfY?8U9g5 z3MsHr1^LsDg$W0TfNVsO(1n?1hWsL$9tO5T1$KsmFFZo;MJQqx8L=p{%v7u-5;6*E zV~mX^vT;R;ZoJ3Ji>Z`J8S8%IO~7piW)tPjVvC_0`j3jz_>}$Kv8v3m%RkNbLz%K< z%jvi;m0P??<;gFh0<4z$T5N?G&8rBfxvJGL4{1xJ(URzF$sZ|2vzyF8+P*`-+%10) zSfE~Uic-m0QLtvKWX86|!fR=$XsS*w*;NelwEP2|lV$OW2j{j2?W9Q~ZWp zDJ9(imJZv|PuaPwMO+uF91c`?AAEF9jUNb9>syLS5M^k4$&6yry-%k8CM(FbYD))i z31zpCsLpp6gT>(qL=qWfV)8$(DpW(&meQWD6xkpH;){KGu^k zy$+bBVVf7u5A9FKv;+w=+1I$}Mh~0iX%P_GB)~k{1yg)=YKusq=tUYT!l`Ozx40A6 z(~&K7kEqIP^qlH>({<)GAHu;|*YG}StnaqywvE|njX zJxN*bWHXgdA4m0EKeSr~wO=%3(7w0^q`S7w__g|_w!dtZbLA8mtDRFPa470sHm zJW91Qy_}A#b)!?f`jJ+3&|Ri0qVYg&^bvHG6igD1tCX1RDzFA0HOE6$wW;iMdv(`T zRKr(Qn+^(R#oQZl&Uo&4J-H?y1uyzx^G{NKX&y>^hYQ)s+emx3RG30$8mTE{rd*n0 zZYnBMRhx2YCLuk|CTbfB|C8jp-Ml@{w}fb9G7D?ci<{U;rEM@G019MHdXXcC#V%w4 z*l-LRZbcSAGm$6&=my}y;iYK@q&dG$-~ik^0MuGH4kH4fKo+$|4gdti3a|hGAO`?I zB>?asXxafK3YaLsA?~q3X{YJU7&-3GixjiepIBH*V#T|3{TN*cl*Pm|fz<0Z4fEFu zssI)iW#rWqNm(>8E18(K%C?hnt5{~2Oi zs<+t>6st^1N%k^js(9K>rl(;|$uoAr-I0+0TYc!pFW!P7mlD(c6XVqvcAO z@}$A>f`YSJEH#hWU}@ftUe7Hjd_!2Ywg`q#boe@}ro;(NZ_D=OD$QClB2JlZzzN3F TLuMqV=vyDN|CE#X0RR91K>c5) literal 0 HcmV?d00001 diff --git a/packages/devtools-ui/src/assets/fonts/OFL-Bricolage-Grotesque.txt b/packages/devtools-ui/src/assets/fonts/OFL-Bricolage-Grotesque.txt new file mode 100644 index 00000000..4dfd4fe0 --- /dev/null +++ b/packages/devtools-ui/src/assets/fonts/OFL-Bricolage-Grotesque.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/devtools-ui/src/assets/fonts/OFL-Inter.txt b/packages/devtools-ui/src/assets/fonts/OFL-Inter.txt new file mode 100644 index 00000000..9b2ca37b --- /dev/null +++ b/packages/devtools-ui/src/assets/fonts/OFL-Inter.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/devtools-ui/src/components/button.tsx b/packages/devtools-ui/src/components/button.tsx index f292cdc6..5568d71d 100644 --- a/packages/devtools-ui/src/components/button.tsx +++ b/packages/devtools-ui/src/components/button.tsx @@ -32,7 +32,7 @@ export function Button(props: ButtonProps) { }) return ( - ) diff --git a/packages/devtools-ui/src/components/checkbox.tsx b/packages/devtools-ui/src/components/checkbox.tsx index d87a4efc..011f62c6 100644 --- a/packages/devtools-ui/src/components/checkbox.tsx +++ b/packages/devtools-ui/src/components/checkbox.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { createSignal, createUniqueId } from 'solid-js' import { createStyles } from '../styles/use-styles' interface CheckboxProps { @@ -11,6 +11,7 @@ interface CheckboxProps { export function Checkbox(props: CheckboxProps) { const styles = createStyles() const [isChecked, setIsChecked] = createSignal(props.checked || false) + const descriptionId = `${createUniqueId()}-description` const handleChange = (e: Event) => { const checked = (e.target as HTMLInputElement).checked @@ -22,6 +23,9 @@ export function Checkbox(props: CheckboxProps) {