From b5c4c6cc2a1f3057505532679f373bf0b6a0bdf7 Mon Sep 17 00:00:00 2001 From: Jona Date: Sat, 17 Oct 2026 01:41:17 -0400 Subject: [PATCH 1/5] Extend selections with Cmd/Ctrl+Shift+Arrow --- README.md | 1 + images/range-selection.jpg | Bin 0 -> 6426 bytes media/main.js | 111 ++++------- src/test/modified-arrow-selection.test.ts | 228 ++++++++++++++++++++++ 4 files changed, 269 insertions(+), 71 deletions(-) create mode 100644 images/range-selection.jpg create mode 100644 src/test/modified-arrow-selection.test.ts diff --git a/README.md b/README.md index 30c0813..7ee3c08 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Per-file (stored by the extension; set via commands): - Replace: `Ctrl/Cmd + H` - Next/Previous match: `F3` / `Shift + F3` (also `Enter` / `Shift + Enter` in the Find box) - Select all: `Ctrl/Cmd + A` + - Jump to an edge: `Ctrl/Cmd + Arrow`. Add `Shift` to extend the current selection from its fixed starting cell; whole-row and whole-column selections retain their shape. --- diff --git a/images/range-selection.jpg b/images/range-selection.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a89e9e62f164775f0f20057cf3741152c8a84dbd GIT binary patch literal 6426 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 S_RlK(*KXPW@0G&Y)PDiq{(>9; literal 0 HcmV?d00001 diff --git a/media/main.js b/media/main.js index 58eaa5a..b6251f8 100644 --- a/media/main.js +++ b/media/main.js @@ -985,6 +985,19 @@ const selectFullRowRange = (row1, row2) => { }); }; +const extendSelectionTo = target => { + rangeEndCell = target; + const start = getCellCoords(anchorCell); + const end = getCellCoords(target); + if (isColumnHeaderCell(anchorCell)) { + selectFullColumnRange(start.col, end.col); + } else if (isRowIndexCell(anchorCell)) { + selectFullRowRange(start.row, end.row); + } else { + selectRange(start, end); + } +}; + const getDataCellCoords = cell => { if (!cell || typeof cell.getAttribute !== 'function') return null; const coords = getCellCoords(cell); @@ -1430,9 +1443,9 @@ window.addEventListener('csvChunkLoaded', () => { } }); -// Capture-phase handler to intercept Cmd/Ctrl + Arrow and move to extremes +// Handle modified arrow jumps once; Shift extends the existing selection. document.addEventListener('keydown', e => { - if (isFindWidgetTarget(e.target)) { + if (isFindWidgetTarget(e.target) || isTextInputTarget(e.target)) { return; } const isArrowKey = (k) => ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Up','Down','Left','Right','Home','End'].includes(k); @@ -1441,6 +1454,11 @@ document.addEventListener('keydown', e => { e.stopImmediatePropagation(); e.stopPropagation(); + // Select All has no fixed cell anchor; preserve the complete selection. + if (e.shiftKey && !anchorCell && currentSelection.length > 1) { + return; + } + const sc = document.querySelector('.table-container'); if (sc) { if (['ArrowLeft','Left','Home'].includes(e.key)) sc.scrollTo({ left: 0, behavior: 'smooth' }); @@ -1460,19 +1478,21 @@ document.addEventListener('keydown', e => { } } - const ref = anchorCell || currentSelection[0] || document.querySelector('td.selected, th.selected') || document.querySelector('td, th'); + const fixedAnchor = anchorCell || currentSelection.find(cell => { + const { row, col } = getCellCoords(cell); + return Number.isInteger(row) && Number.isInteger(col); + }) || table.querySelector('td[data-row][data-col], th[data-row][data-col]'); + const ref = e.shiftKey && anchorCell ? (rangeEndCell || fixedAnchor) : fixedAnchor; let target = null; if (ref) { const { row, col } = getCellCoords(ref); if (['ArrowLeft','Left','Home'].includes(e.key)) { - const tag = (hasHeader && row === 0 ? 'th' : 'td'); - const rowCells = Array.from(table.querySelectorAll(tag + '[data-row="'+row+'"]')) + const rowCells = Array.from(table.querySelectorAll('td[data-row="'+row+'"], th[data-row="'+row+'"]')) .filter(el => el.getAttribute('data-col') !== null && el.getAttribute('data-col') !== '-1'); const min = rowCells.reduce((acc, el) => Math.min(acc, parseInt(el.getAttribute('data-col'))), Infinity); target = rowCells.find(el => parseInt(el.getAttribute('data-col')) === min) || ref; } else if (['ArrowRight','Right','End'].includes(e.key)) { - const tag = (hasHeader && row === 0 ? 'th' : 'td'); - const rowCells = Array.from(table.querySelectorAll(tag + '[data-row="'+row+'"]')) + const rowCells = Array.from(table.querySelectorAll('td[data-row="'+row+'"], th[data-row="'+row+'"]')) .filter(el => el.getAttribute('data-col') !== null && el.getAttribute('data-col') !== '-1'); const max = rowCells.reduce((acc, el) => Math.max(acc, parseInt(el.getAttribute('data-col'))), -1); target = rowCells.find(el => parseInt(el.getAttribute('data-col')) === max) || ref; @@ -1486,13 +1506,18 @@ document.addEventListener('keydown', e => { } if (target) { - clearSelection(); - target.classList.add('selected'); - currentSelection.push(target); - anchorCell = target; - rangeEndCell = target; + if (e.shiftKey && fixedAnchor) { + anchorCell = fixedAnchor; + extendSelectionTo(target); + } else { + clearSelection(); + target.classList.add('selected'); + currentSelection.push(target); + anchorCell = target; + rangeEndCell = target; + } persistState(); - target.focus({preventScroll:true}); + (e.shiftKey ? anchorCell : target).focus({preventScroll:true}); if (['ArrowUp','Up'].includes(e.key)) { const topRow = getFirstDataRow(); const below = table.querySelector('td[data-row="'+topRow+'"][data-col="'+getCellCoords(target).col+'"]'); @@ -1565,61 +1590,6 @@ document.addEventListener('keydown', e => { return; } - const isArrowKey = (k) => ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Up','Down','Left','Right','Home','End'].includes(k); - if (!editingCell && (e.ctrlKey || e.metaKey) && isArrowKey(e.key)) { - e.preventDefault(); - const sc = document.querySelector('.table-container'); - if (sc) { - if (['ArrowLeft','Left','Home'].includes(e.key)) sc.scrollTo({ left: 0, behavior: 'smooth' }); - if (['ArrowRight','Right','End'].includes(e.key)) sc.scrollTo({ left: sc.scrollWidth, behavior: 'smooth' }); - if (['ArrowUp','Up'].includes(e.key)) sc.scrollTo({ top: 0, behavior: 'smooth' }); - if (['ArrowDown','Down'].includes(e.key)) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' }); - } - - let refCell = anchorCell; - if (!refCell) { - refCell = currentSelection[0] || document.querySelector('td.selected, th.selected'); - } - let target = null; - if (refCell) { - const { row, col } = getCellCoords(refCell); - if (['ArrowLeft','Left','Home'].includes(e.key)) { - const tag = (hasHeader && row === 0 ? 'th' : 'td'); - const rowCells = Array.from(table.querySelectorAll(tag + '[data-row="'+row+'"]')) - .filter(el => el.getAttribute('data-col') !== null && el.getAttribute('data-col') !== '-1'); - const min = rowCells.reduce((acc, el) => Math.min(acc, parseInt(el.getAttribute('data-col'))), Infinity); - target = rowCells.find(el => parseInt(el.getAttribute('data-col')) === min) || refCell; - } else if (['ArrowRight','Right','End'].includes(e.key)) { - const tag = (hasHeader && row === 0 ? 'th' : 'td'); - const rowCells = Array.from(table.querySelectorAll(tag + '[data-row="'+row+'"]')) - .filter(el => el.getAttribute('data-col') !== null && el.getAttribute('data-col') !== '-1'); - const max = rowCells.reduce((acc, el) => Math.max(acc, parseInt(el.getAttribute('data-col'))), -1); - target = rowCells.find(el => parseInt(el.getAttribute('data-col')) === max) || refCell; - } else if (['ArrowUp','Up'].includes(e.key)) { - if (hasHeader) { - target = table.querySelector('th[data-row="0"][data-col="'+col+'"]') || refCell; - } else { - target = table.querySelector('td[data-row="0"][data-col="'+col+'"]') || refCell; - } - } else if (['ArrowDown','Down'].includes(e.key)) { - const colCells = Array.from(table.querySelectorAll('td[data-col="'+col+'"]')); - target = (colCells.length ? colCells[colCells.length - 1] : null) || refCell; - } - } - - if (target) { - clearSelection(); - target.classList.add('selected'); - currentSelection.push(target); - anchorCell = target; - rangeEndCell = target; - persistState(); - target.focus({preventScroll:true}); - target.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); - } - return; - } - if (!editingCell && e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); const refCell = anchorCell || getCellTarget(document.activeElement) || currentSelection[0] || document.querySelector('td.selected, th.selected'); @@ -1688,13 +1658,12 @@ document.addEventListener('keydown', e => { case 'ArrowLeft': targetCol = col - 1; break; case 'ArrowRight':targetCol = col + 1; break; } - if(targetRow < 0 || targetCol < 0) return; + if(targetRow < 0 || targetCol < (isRowIndexCell(anchorCell) ? -1 : 0)) return; const tag = (hasHeader && targetRow === 0 ? 'th' : 'td'); const nextCell = table.querySelector(`${tag}[data-row="${targetRow}"][data-col="${targetCol}"]`); if(nextCell){ e.preventDefault(); - rangeEndCell = nextCell; - selectRange(getCellCoords(anchorCell), getCellCoords(rangeEndCell)); + extendSelectionTo(nextCell); persistState(); anchorCell.focus({preventScroll:true}); rangeEndCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); diff --git a/src/test/modified-arrow-selection.test.ts b/src/test/modified-arrow-selection.test.ts new file mode 100644 index 0000000..7ad5556 --- /dev/null +++ b/src/test/modified-arrow-selection.test.ts @@ -0,0 +1,228 @@ +import assert from 'assert'; +import { describe, it } from 'node:test'; +import { createEditorHarness, createWebviewDom } from './edit-integrity-harness'; + +type View = ReturnType; +const rows = [['A', 'B', 'C', 'D'], ['a1', 'a2', 'a3', 'a4'], ['b1', 'b2', 'b3', 'b4'], ['c1', 'c2', 'c3', 'c4'], ['d1', 'd2', 'd3', 'd4']]; + +async function fixture(options: Parameters[0] = {}) { + const host = await createEditorHarness({ rows, ...options }); + const view = createWebviewDom(host.panel.webview.html); + const container = view.window.document.querySelector('.table-container'); + container.scrollTo = (position: { left?: number; top?: number }) => { + if (position.left !== undefined) { container.scrollLeft = position.left; } + if (position.top !== undefined) { container.scrollTop = position.top; } + }; + view.window.scrollTo = () => {}; + return { host, view }; +} + +function select(view: View, row = 2, column = 1) { + view.window.editingTestApi.setSingleSelection(view.cell(row, column)); +} +function selected(view: View, includeIndex = false): number[][] { + return Array.from(view.window.document.querySelectorAll('td.selected, th.selected')) + .filter((cell: any) => cell.hasAttribute('data-row') && Number(cell.getAttribute('data-col')) >= (includeIndex ? -1 : 0)) + .map((cell: any) => [Number(cell.getAttribute('data-row')), Number(cell.getAttribute('data-col'))]); +} +function rectangle(minRow: number, maxRow: number, minColumn: number, maxColumn: number) { + return Array.from({ length: maxRow - minRow + 1 }, (_, row) => + Array.from({ length: maxColumn - minColumn + 1 }, (_, column) => [minRow + row, minColumn + column]) + ).flat(); +} +function mouseSelect(view: View, row: number, column: number) { + const cell = view.cell(row, column); + const left = (column + 1) * 100; + cell.getBoundingClientRect = () => ({ left, right: left + 100, top: row * 30, bottom: row * 30 + 30, width: 100, height: 30 }); + cell.dispatchEvent(new view.window.MouseEvent('mousedown', { button: 0, clientX: left + 50, clientY: row * 30 + 15, bubbles: true, cancelable: true })); + cell.dispatchEvent(new view.window.MouseEvent('mouseup', { button: 0, bubbles: true })); +} +function dispatch(view: View, key: string, options: Record) { + const event = new view.window.KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...options }); + view.window.document.activeElement.dispatchEvent(event); + return event; +} + +describe('Modified arrow range selection', () => { + for (const modifier of ['metaKey', 'ctrlKey']) { + for (const [key, bounds] of [ + ['ArrowLeft', [2, 2, 0, 1]], ['ArrowRight', [2, 2, 1, 3]], + ['ArrowUp', [1, 2, 1, 1]], ['ArrowDown', [2, 5, 1, 1]] + ] as const) { + it(`extends from a fixed anchor with ${modifier} + Shift + ${key}`, async () => { + const { host, view } = await fixture(); + const source = host.document.getText(); + try { + select(view); + const anchor = view.cell(2, 1); + const event = dispatch(view, key, { [modifier]: true, shiftKey: true }); + assert.strictEqual(event.defaultPrevented, true); + assert.deepStrictEqual(selected(view), rectangle(bounds[0], bounds[1], bounds[2], bounds[3])); + assert.strictEqual(view.window.document.activeElement, anchor); + assert.strictEqual(view.window.acquireVsCodeApi().getState().anchorRow, 2); + assert.strictEqual(view.window.acquireVsCodeApi().getState().anchorCol, 1); + assert.strictEqual(host.document.getText(), source); + assert.ok(!view.messages.some((message: any) => message.type === 'editCell')); + } finally { view.close(); } + }); + } + + it(`continues and shrinks a rectangle from its endpoint with ${modifier}`, async () => { + const { view } = await fixture(); + try { + select(view); + view.key('ArrowDown', { shiftKey: true }); + view.key('ArrowRight', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(2, 3, 1, 3)); + view.key('ArrowLeft', { shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(2, 3, 1, 2)); + view.key('ArrowLeft', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(2, 3, 0, 1)); + view.key('ArrowUp', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(1, 2, 0, 1)); + assert.strictEqual(view.window.document.activeElement, view.cell(2, 1)); + } finally { view.close(); } + }); + + it(`retains whole-column groups during horizontal and vertical extension with ${modifier}`, async () => { + const { view } = await fixture(); + try { + mouseSelect(view, 0, 1); + view.key('ArrowRight', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(0, 5, 1, 3)); + view.key('ArrowDown', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(0, 5, 1, 3)); + view.key('ArrowLeft', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(0, 5, 0, 1)); + assert.strictEqual(view.window.document.activeElement, view.cell(0, 1)); + } finally { view.close(); } + }); + + it(`retains whole-row groups during vertical and horizontal extension with ${modifier}`, async () => { + const { view } = await fixture(); + try { + mouseSelect(view, 2, -1); + view.key('ArrowDown', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view, true), rectangle(2, 5, -1, 3)); + view.key('ArrowRight', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view, true), rectangle(2, 5, -1, 3)); + view.key('ArrowUp', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view, true), rectangle(1, 2, -1, 3)); + assert.strictEqual(view.window.document.activeElement, view.cell(2, -1)); + } finally { view.close(); } + }); + + it(`keeps a backwards drag anchor through repeated and mixed jumps with ${modifier}`, async () => { + const { view } = await fixture(); + try { + mouseSelect(view, 3, 2); + view.cell(3, 2).dispatchEvent(new view.window.MouseEvent('mousedown', { button: 0, clientX: 350, clientY: 105, bubbles: true })); + view.cell(1, 0).dispatchEvent(new view.window.MouseEvent('mousemove', { buttons: 1, bubbles: true })); + view.cell(1, 0).dispatchEvent(new view.window.MouseEvent('mouseup', { button: 0, bubbles: true })); + assert.deepStrictEqual(selected(view), rectangle(1, 3, 0, 2)); + for (let repeat = 0; repeat < 2; repeat++) { + view.key('ArrowRight', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(1, 3, 2, 3)); + } + view.key('ArrowDown', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(3, 5, 2, 3)); + view.key('ArrowLeft', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(3, 5, 0, 2)); + view.key('ArrowUp', { shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(3, 4, 0, 2)); + view.key('ArrowUp', { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(1, 3, 0, 2)); + assert.strictEqual(view.window.document.activeElement, view.cell(3, 2)); + } finally { view.close(); } + }); + + it(`preserves Select All after a stale range endpoint with ${modifier}`, async () => { + const { host, view } = await fixture(); + try { + select(view); + view.key('ArrowDown', { shiftKey: true }); + const corner = view.window.document.querySelector('thead th:not([data-col])'); + corner.dispatchEvent(new view.window.MouseEvent('mousedown', { button: 0, bubbles: true })); + corner.dispatchEvent(new view.window.MouseEvent('mouseup', { button: 0, bubbles: true })); + const all = selected(view, true); + assert.ok(all.length > 1); + for (const key of ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']) { + view.key(key, { [modifier]: true, shiftKey: true }); + assert.deepStrictEqual(selected(view, true), all); + } + assert.deepStrictEqual(host.rows(), rows); + } finally { view.close(); } + }); + + it(`preserves plain ${modifier} jump behavior without Shift`, async () => { + const { view } = await fixture(); + try { + select(view); + view.key('ArrowRight', { [modifier]: true }); + assert.deepStrictEqual(selected(view), [[2, 3]]); + assert.strictEqual(view.window.document.activeElement, view.cell(2, 3)); + view.key('ArrowDown', { [modifier]: true }); + assert.deepStrictEqual(selected(view), [[5, 3]]); + } finally { view.close(); } + }); + } + + it('extends to the existing loaded-row boundary in a chunked table', async () => { + const source = [['A', 'B'], ...Array.from({ length: 1100 }, (_, row) => [`row${row}`, String(row)])]; + const { host, view } = await fixture({ rows: source }); + try { + const rendered = Array.from(view.window.document.querySelectorAll('tbody td[data-col="1"]')) as any[]; + const lastRow = Number(rendered[rendered.length - 1].getAttribute('data-row')); + assert.ok(lastRow < source.length - 1, 'fixture must retain unloaded source rows'); + select(view, lastRow - 2, 1); + view.key('ArrowDown', { metaKey: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(lastRow - 2, lastRow, 1, 1)); + assert.strictEqual(view.window.document.activeElement, view.cell(lastRow - 2, 1)); + assert.deepStrictEqual(host.rows(), source); + } finally { view.close(); } + }); + + it('copies the entire extended rectangle rather than only its edge cell', async () => { + const { view } = await fixture(); + try { + select(view); + view.key('ArrowDown', { shiftKey: true }); + view.key('ArrowRight', { metaKey: true, shiftKey: true }); + view.key('c', { metaKey: true }); + const copy = view.messages.slice().reverse().find((message: any) => message.type === 'copyCells'); + assert.deepStrictEqual(JSON.parse(JSON.stringify(copy.matrix)), [['b2', 'b3', 'b4'], ['c2', 'c3', 'c4']]); + } finally { view.close(); } + }); + + it('supports headers off and Home/End aliases without losing the anchor', async () => { + const { view } = await fixture({ header: false, separator: '\t' }); + try { + select(view); + view.key('End', { metaKey: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(2, 2, 1, 3)); + view.key('Home', { metaKey: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(2, 2, 0, 1)); + view.key('ArrowUp', { metaKey: true, shiftKey: true }); + assert.deepStrictEqual(selected(view), rectangle(0, 2, 0, 1)); + } finally { view.close(); } + }); + + it('leaves native selection in Find, Replace and cell text edits outside the grid handler', async () => { + const { view } = await fixture(); + try { + select(view); + const before = selected(view); + for (const id of ['findInput', 'replaceInput']) { + view.window.document.getElementById(id).focus(); + for (const key of ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']) { + assert.strictEqual(dispatch(view, key, { metaKey: true, shiftKey: true }).defaultPrevented, false); + } + assert.deepStrictEqual(selected(view), before); + } + view.start(view.cell(2, 1)); + for (const key of ['ArrowLeft', 'ArrowRight']) { + assert.strictEqual(dispatch(view, key, { metaKey: true, shiftKey: true }).defaultPrevented, false); + } + } finally { view.close(); } + }); +}); From a99936a38da6bf297764763429eb76f625f5a493 Mon Sep 17 00:00:00 2001 From: Jona Date: Sat, 17 Oct 2026 03:48:16 -0400 Subject: [PATCH 2/5] Keep held-arrow selections in the viewport --- README.md | 2 +- media/main.js | 109 ++++--- src/test/webview-navigation-scroll.test.ts | 327 +++++++++++++++++++++ 3 files changed, 401 insertions(+), 37 deletions(-) create mode 100644 src/test/webview-navigation-scroll.test.ts diff --git a/README.md b/README.md index f3fbce9..084d66b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Cursor (built on VS Code 1.99) and the latest VS Code releases (1.102). - **Edit Modes:** - Quick edit: start typing any character to edit the selected cell immediately. Press any Arrow key to save and move the selection to the next cell in that direction. - Detail edit: press `Enter` on a selected cell or double‑click to enter a focused edit. Arrow Left/Right move the text caret; Arrow Up goes to start; Arrow Down goes to end. Click outside the cell (or blur) to save. -- **Keyboard Navigation:** Use Arrow keys to move between cells when not editing. Use `Tab`/`Shift+Tab` to move horizontally (wrapping across rows as needed). +- **Keyboard Navigation:** Use Arrow keys to move between cells when not editing. Use `Tab`/`Shift+Tab` to move horizontally (wrapping across rows as needed). Holding an arrow keeps the active cell in view; with `Shift`, the viewport follows the moving end of the selection. - **Multi-Cell Selection:** Click and drag or use `Shift + Click` to select multiple cells, then copy them as CSV using `Ctrl/Cmd + C`. - **Find & Replace:** Press `Ctrl/Cmd + F` to open Find, or `Ctrl/Cmd + H` to open Find + Replace. diff --git a/media/main.js b/media/main.js index 75f0ba9..8e4aca7 100644 --- a/media/main.js +++ b/media/main.js @@ -150,6 +150,72 @@ const persistState = () => { } catch {} }; +// Coalesce repeated navigation to the latest cell before paint. Native smooth +// scrolling restarts on every key repeat and can leave selection offscreen. +let pendingCellReveal = null; +let cellRevealScheduled = false; +const scheduleCellReveal = (cell, center = false) => { + pendingCellReveal = { cell, center }; + if (cellRevealScheduled) return; + cellRevealScheduled = true; + requestAnimationFrame(() => { + cellRevealScheduled = false; + const request = pendingCellReveal; + pendingCellReveal = null; + if (!request?.cell?.isConnected || !scrollContainer) return; + const width = scrollContainer.clientWidth; + const height = scrollContainer.clientHeight; + if (width <= 0 || height <= 0) return; + const viewport = scrollContainer.getBoundingClientRect(); + const viewportLeft = viewport.left + scrollContainer.clientLeft; + const viewportTop = viewport.top + scrollContainer.clientTop; + let left = Math.max(0, viewportLeft); + let top = Math.max(0, viewportTop); + const right = Math.min(window.innerWidth, viewportLeft + width); + const bottom = Math.min(window.innerHeight, viewportTop + height); + const target = request.cell; + let fixedTop = target.classList.contains('csv-frozen-row'); + const fixedLeft = target.classList.contains('csv-frozen-column'); + + const filterBar = document.getElementById('filterStatus'); + if (filterBar) top = Math.max(top, filterBar.getBoundingClientRect().bottom); + const headerCell = table.tHead?.rows[0]?.cells[0]; + if (headerCell) { + const style = getComputedStyle(headerCell); + if (style.position === 'sticky' && style.top && style.top !== 'auto') { + top = Math.max(top, headerCell.getBoundingClientRect().bottom); + fixedTop = fixedTop || table.tHead.contains(target); + } + } + // Frozen panes are leading rows/columns. Read one representative per + // boundary, rather than scanning every rendered cell on each repeat. + for (const row of table.rows) { + const first = row.cells[0]; + if (!first?.classList.contains('csv-frozen-row')) break; + top = Math.max(top, first.getBoundingClientRect().bottom); + } + for (const cell of table.rows[0]?.cells || []) { + if (!cell.classList.contains('csv-frozen-column')) break; + left = Math.max(left, cell.getBoundingClientRect().right); + } + + const rect = target.getBoundingClientRect(); + const revealDelta = (start, end, min, max) => { + if (max <= min) return 0; + // A cell larger than the available pane cannot fit; keep its leading + // edge usable instead of alternating between opposite edges. + if (end - start > max - min) return start - min; + if (request.center) return (start + end - min - max) / 2; + return start < min ? start - min : end > max ? end - max : 0; + }; + const x = fixedLeft ? 0 : revealDelta(rect.left, rect.right, left, right); + const y = fixedTop ? 0 : revealDelta(rect.top, rect.bottom, top, bottom); + // Even zero delta cancels any native animation already in flight. + scrollContainer.scrollTo({ left: scrollContainer.scrollLeft + x, top: scrollContainer.scrollTop + y, behavior: 'instant' }); + persistState(); + }); +}; + const restoreState = () => { try { const st = vscode.getState() || {}; @@ -1226,7 +1292,7 @@ const setActiveFindMatch = (index, shouldScroll = true) => { cell.classList.add('highlight'); cell.classList.add('active-match'); if (shouldScroll) { - cell.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' }); + scheduleCellReveal(cell, true); } } updateFindStatus(); @@ -1496,25 +1562,6 @@ document.addEventListener('keydown', e => { return; } - const sc = document.querySelector('.table-container'); - if (sc) { - if (['ArrowLeft','Left','Home'].includes(e.key)) sc.scrollTo({ left: 0, behavior: 'smooth' }); - if (['ArrowRight','Right','End'].includes(e.key)) sc.scrollTo({ left: sc.scrollWidth, behavior: 'smooth' }); - if (['ArrowUp','Up'].includes(e.key)) sc.scrollTo({ top: 0, behavior: 'smooth' }); - if (['ArrowDown','Down'].includes(e.key)) sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' }); - } else { - if (['ArrowUp','Up'].includes(e.key)) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - if (['ArrowDown','Down'].includes(e.key)) { - const h = Math.max( - document.body.scrollHeight, - document.documentElement.scrollHeight - ); - window.scrollTo({ top: h, behavior: 'smooth' }); - } - } - const fixedAnchor = anchorCell || currentSelection.find(cell => { const { row, col } = getCellCoords(cell); return Number.isInteger(row) && Number.isInteger(col); @@ -1555,17 +1602,7 @@ document.addEventListener('keydown', e => { } persistState(); (e.shiftKey ? anchorCell : target).focus({preventScroll:true}); - if (['ArrowUp','Up'].includes(e.key)) { - const topRow = getFirstDataRow(); - const below = table.querySelector('td[data-row="'+topRow+'"][data-col="'+getCellCoords(target).col+'"]'); - if (below) { - below.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); - } else { - target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); - } - } else { - target.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); - } + scheduleCellReveal(target); } } }, true); @@ -1703,7 +1740,7 @@ document.addEventListener('keydown', e => { extendSelectionTo(nextCell); persistState(); anchorCell.focus({preventScroll:true}); - rangeEndCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); + scheduleCellReveal(rangeEndCell); } return; } @@ -1729,7 +1766,7 @@ document.addEventListener('keydown', e => { rangeEndCell = nextCell; persistState(); nextCell.focus({preventScroll:true}); - nextCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); + scheduleCellReveal(nextCell); } return; } @@ -1757,7 +1794,7 @@ document.addEventListener('keydown', e => { rangeEndCell = nextCell; persistState(); nextCell.focus({preventScroll:true}); - nextCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); + scheduleCellReveal(nextCell); }; const cellRef = editingCell; cellRef && cellRef.blur(); @@ -1938,7 +1975,7 @@ const setSingleSelection = cell => { rangeEndCell = cell; persistState(); try { cell.focus({ preventScroll: true }); } catch { try { cell.focus(); } catch {} } - cell.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }); + scheduleCellReveal(cell); }; const NEWLINE_SENTINEL_ATTR = 'data-csv-newline-sentinel'; @@ -2150,7 +2187,7 @@ window.addEventListener('message', event => { selectRange({ row: startRow, col: startCol }, { row: endRow, col: endCol }); persistState(); try { startCell.focus({ preventScroll: true }); } catch { try { startCell.focus(); } catch {} } - endCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' }); + scheduleCellReveal(endCell); } else if (message.type === 'findMatchesResult') { if (!findReplaceState.open) { return; diff --git a/src/test/webview-navigation-scroll.test.ts b/src/test/webview-navigation-scroll.test.ts new file mode 100644 index 0000000..a2d844d --- /dev/null +++ b/src/test/webview-navigation-scroll.test.ts @@ -0,0 +1,327 @@ +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; +import { describe, it } from 'node:test'; +import { createEditorHarness } from './edit-integrity-harness'; +const { JSDOM } = require('jsdom'); + +type Position = { left?: number; top?: number; behavior?: string }; + +// This geometry model runs the shipped script. Smooth native scrolls remain +// pending until explicitly completed; immediate scrolls cancel them, even at +// zero delta. Keyboard animation frames are controlled independently. +async function fixture(options: { separator?: string; rowCount?: number; columnCount?: number } = {}) { + const rowCount = options.rowCount ?? 40, columnCount = options.columnCount ?? 12; + const rows = Array.from({ length: rowCount }, (_, row) => Array.from({ length: columnCount }, (_, column) => `r${row}c${column}`)); + const host = await createEditorHarness({ rows, separator: options.separator, header: false, serialIndex: false }); + const dom = new JSDOM(host.panel.webview.html, { runScripts: 'outside-only', pretendToBeVisual: true }); + const { window } = dom; + const container = window.document.querySelector('.table-container'); + const table = container.querySelector('table'); + const width = 240, height = 120, cellWidth = 80, cellHeight = 24; + const viewport = { left: 37.5, top: 29.25, right: 277.5, bottom: 149.25, width, height }; + let scrollLeft = 0, scrollTop = 0, state: any = {}; + let pendingSmooth: { left: number; top: number } | undefined; + let clock = 0, nextFrame = 1, nextTimer = 1; + const frames = new Map void>(); + const timers = new Map void>(); + const messages: any[] = []; + const scrollCalls: Array = []; + const focusCalls: Array<{ cell: any; options: any }> = []; + const renderedHeight = () => (Number(table.querySelector('tr:last-child [data-row]')?.getAttribute('data-row') ?? 0) + 1) * cellHeight; + const clamp = (value: number, maximum: number) => Math.max(0, Math.min(value, Math.max(0, maximum))); + const notifyScroll = () => container.dispatchEvent(new window.Event('scroll')); + const instant = (position: Position) => { + pendingSmooth = undefined; + if (position.left !== undefined) { scrollLeft = clamp(position.left, columnCount * cellWidth - width); } + if (position.top !== undefined) { scrollTop = clamp(position.top, renderedHeight() - height); } + notifyScroll(); + }; + const scroll = (position: Position) => { + scrollCalls.push({ ...position }); + const left = clamp(position.left ?? scrollLeft, columnCount * cellWidth - width); + const top = clamp(position.top ?? scrollTop, renderedHeight() - height); + if (position.behavior === 'smooth') { + // A no-op smooth reveal does not cancel an already running native scroll. + if (left !== scrollLeft || top !== scrollTop) { pendingSmooth = { left, top }; } + } else { instant({ left, top }); } + }; + Object.defineProperties(container, { + clientWidth: { get: () => width }, clientHeight: { get: () => height }, + clientLeft: { get: () => 0 }, clientTop: { get: () => 0 }, + scrollWidth: { get: () => columnCount * cellWidth }, scrollHeight: { get: renderedHeight }, + scrollLeft: { get: () => scrollLeft, set: (value: number) => instant({ left: value }) }, + scrollTop: { get: () => scrollTop, set: (value: number) => instant({ top: value }) } + }); + container.getBoundingClientRect = () => ({ ...viewport }); + container.scrollTo = scroll; + window.scrollTo = scroll; + window.requestAnimationFrame = (callback: (time: number) => void) => { const id = nextFrame++; frames.set(id, callback); return id; }; + window.cancelAnimationFrame = (id: number) => { frames.delete(id); }; + window.setTimeout = (callback: () => void) => { const id = nextTimer++; timers.set(id, callback); return id; }; + window.clearTimeout = (id: number) => { timers.delete(id); }; + window.IntersectionObserver = class { observe() {} disconnect() {} }; + window.acquireVsCodeApi = () => ({ postMessage: (message: any) => messages.push(message), getState: () => state, setState: (value: any) => { state = value; } }); + const nativeRect = window.HTMLElement.prototype.getBoundingClientRect; + window.HTMLElement.prototype.getBoundingClientRect = function (this: any) { + if (!this.hasAttribute('data-row') || !this.hasAttribute('data-col')) { return nativeRect.call(this); } + const row = Number(this.getAttribute('data-row')), column = Number(this.getAttribute('data-col')); + let left = column * cellWidth - scrollLeft, top = row * cellHeight - scrollTop; + if (this.style.position === 'sticky') { + if (this.style.left !== '') { left = Math.max(left, Number.parseFloat(this.style.left)); } + if (this.style.top !== '') { top = Math.max(top, Number.parseFloat(this.style.top)); } + } + left += viewport.left; + top += viewport.top; + return { left, top, right: left + cellWidth, bottom: top + cellHeight, width: cellWidth, height: cellHeight }; + }; + window.HTMLElement.prototype.scrollIntoView = function (this: any, settings: any = {}) { + const rect = this.getBoundingClientRect(); + const nearest = (start: number, end: number, low: number, high: number) => start < low ? start - low : end > high ? end - high : 0; + scroll({ + left: scrollLeft + (settings.inline === 'center' ? (rect.left + rect.right - viewport.left - viewport.right) / 2 : nearest(rect.left, rect.right, viewport.left, viewport.right)), + top: scrollTop + (settings.block === 'center' ? (rect.top + rect.bottom - viewport.top - viewport.bottom) / 2 : nearest(rect.top, rect.bottom, viewport.top, viewport.bottom)), + behavior: settings.behavior + }); + }; + const nativeFocus = window.HTMLElement.prototype.focus; + window.HTMLElement.prototype.focus = function (this: any, settings: any) { + focusCalls.push({ cell: this, options: settings }); + nativeFocus.call(this, settings); + }; + const flush = () => { + for (let guard = 0; (frames.size || timers.size) && guard < 20; guard++) { + const pendingTimers = [...timers.values()]; + timers.clear(); + pendingTimers.forEach(callback => callback()); + const callbacks = [...frames.values()]; + frames.clear(); + clock += 16; + callbacks.forEach(callback => callback(clock)); + } + assert.ok(!frames.size && !timers.size, 'scheduled work settles without another key'); + }; + const finishSmooth = () => { + if (!pendingSmooth) { return; } + const position = pendingSmooth; + instant(position); + }; + const scriptPath = process.env.CSV_NAVIGATION_TEST_SCRIPT || path.join(process.cwd(), 'media/main.js'); + window.eval(`${fs.readFileSync(scriptPath, 'utf8')}\nwindow.navigationTestApi = { select: setSingleSelection, anchor: () => anchorCell, endpoint: () => rangeEndCell };`); + flush(); + const cell = (row: number, column: number) => table.querySelector(`[data-row="${row}"][data-col="${column}"]`); + const select = (row: number, column: number) => { + window.navigationTestApi.select(cell(row, column)); + flush(); + finishSmooth(); // settle only the explicit starting selection, before held keys + scrollCalls.length = 0; + focusCalls.length = 0; + }; + const key = (name: string, settings: Record = {}) => { + const event = new window.KeyboardEvent('keydown', { key: name, bubbles: true, cancelable: true, ...settings }); + window.document.activeElement.dispatchEvent(event); + return event; + }; + const visible = (target: any, topInset = 0, leftInset = 0) => { + const rect = target.getBoundingClientRect(); + assert.ok(rect.top >= viewport.top + topInset && rect.bottom <= viewport.bottom, `row ${target.getAttribute('data-row')} must remain in the usable viewport`); + assert.ok(rect.left >= viewport.left + leftInset && rect.right <= viewport.right, `column ${target.getAttribute('data-col')} must remain in the usable viewport`); + }; + return { host, window, container, table, messages, scrollCalls, focusCalls, cell, select, key, flush, finishSmooth, visible, + get state() { return state; }, get pendingSmooth() { return pendingSmooth; }, get frameCount() { return frames.size; }, + anchor: () => window.navigationTestApi.anchor(), endpoint: () => window.navigationTestApi.endpoint(), + close: () => window.close() }; +} + +describe('Keyboard navigation viewport reveal', () => { + for (const separator of [',', '\t']) { + it(`keeps held vertical arrows revealed at every paint without a fresh key (${JSON.stringify(separator)})`, async () => { + const view = await fixture({ separator }); + try { + view.select(0, 0); + for (let row = 1; row <= 16; row++) { + assert.strictEqual(view.key('ArrowDown', { repeat: row > 1 }).defaultPrevented, true); + view.flush(); + assert.strictEqual(view.window.document.activeElement, view.cell(row, 0)); + view.visible(view.cell(row, 0)); + } + assert.strictEqual(view.pendingSmooth, undefined); + assert.ok(view.scrollCalls.every(call => call.behavior !== 'smooth')); + assert.deepStrictEqual(view.host.rows()[0][0], 'r0c0'); + assert.strictEqual(view.host.document.version, 1); + } finally { view.close(); } + }); + } + + it('coalesces a burst to its newest horizontal target and reveals reversals', async () => { + const view = await fixture(); + try { + view.select(1, 0); + for (let column = 1; column <= 10; column++) { view.key('ArrowRight', { repeat: true }); } + assert.strictEqual(view.window.document.activeElement, view.cell(1, 10)); + assert.strictEqual(view.frameCount, 1, 'one pending reveal for the newest target'); + view.flush(); + assert.strictEqual(view.scrollCalls.length, 1); + view.visible(view.cell(1, 10)); + for (let column = 9; column >= 1; column--) { + view.key('ArrowLeft', { repeat: true }); + view.flush(); + view.visible(view.cell(1, column)); + } + assert.strictEqual(view.window.document.activeElement, view.cell(1, 1)); + assert.strictEqual(view.pendingSmooth, undefined); + } finally { view.close(); } + }); + + it('cancels an older native smooth scroll even when the new target is already visible', async () => { + const view = await fixture(); + try { + view.select(0, 0); + view.container.scrollTo({ top: 700, left: 700, behavior: 'smooth' }); + assert.ok(view.pendingSmooth); + view.key('ArrowRight'); + view.flush(); + assert.strictEqual(view.pendingSmooth, undefined); + view.finishSmooth(); + assert.strictEqual(view.container.scrollTop, 0); + assert.strictEqual(view.container.scrollLeft, 0); + view.visible(view.cell(0, 1)); + } finally { view.close(); } + }); + + it('keeps the Shift endpoint revealed while focus and persisted anchor remain fixed', async () => { + const view = await fixture(); + try { + view.select(1, 1); + for (let row = 2; row <= 15; row++) { + view.key('ArrowDown', { shiftKey: true, repeat: true }); + view.flush(); + view.visible(view.cell(row, 1)); + assert.strictEqual(view.window.document.activeElement, view.cell(1, 1)); + } + for (let column = 2; column <= 8; column++) { + view.key('ArrowRight', { shiftKey: true, repeat: true }); + view.flush(); + view.visible(view.cell(15, column)); + } + assert.strictEqual(view.endpoint(), view.cell(15, 8)); + assert.strictEqual(view.anchor(), view.cell(1, 1)); + assert.strictEqual(view.state.anchorRow, 1); + assert.strictEqual(view.state.anchorCol, 1); + assert.strictEqual(view.window.document.querySelectorAll('td.selected').length, 15 * 8); + assert.ok(view.focusCalls.every(call => call.options?.preventScroll === true)); + } finally { view.close(); } + }); + + it('reveals a quick-edit commit and subsequent repeat navigation without saving or re-entering edit', async () => { + const view = await fixture(); + try { + view.select(3, 1); + view.key('x'); + assert.strictEqual(view.cell(3, 1).getAttribute('contenteditable'), 'true'); + for (let row = 4; row <= 14; row++) { + view.key('ArrowDown', { repeat: row > 4 }); + view.flush(); + view.visible(view.cell(row, 1)); + } + const edits = view.messages.filter(message => message.type === 'editCell'); + assert.strictEqual(edits.length, 1); + assert.strictEqual(edits[0].value, 'x'); + assert.strictEqual(view.window.document.activeElement, view.cell(14, 1)); + assert.strictEqual(view.window.document.querySelector('[contenteditable="true"]'), null); + assert.ok(!view.messages.some(message => message.type === 'save')); + } finally { view.close(); } + }); + + it('persists final scroll/focus after repetition and restores without viewport drift', async () => { + const view = await fixture(); + try { + view.select(0, 0); + for (let index = 0; index < 12; index++) { view.key('ArrowDown', { repeat: true }); } + for (let index = 0; index < 7; index++) { view.key('ArrowRight', { repeat: true }); } + view.flush(); + const top = view.container.scrollTop, left = view.container.scrollLeft; + assert.ok(top > 0 && left > 0); + view.window.dispatchEvent(new view.window.Event('blur')); + assert.strictEqual(view.state.scrollY, top); + assert.strictEqual(view.state.scrollX, left); + assert.strictEqual(view.state.anchorRow, 12); + assert.strictEqual(view.state.anchorCol, 7); + view.window.dispatchEvent(new view.window.Event('focus')); + view.flush(); + assert.strictEqual(view.container.scrollTop, top); + assert.strictEqual(view.container.scrollLeft, left); + assert.strictEqual(view.window.document.activeElement, view.cell(12, 7)); + view.visible(view.cell(12, 7)); + assert.ok(view.focusCalls.every(call => call.options?.preventScroll === true)); + } finally { view.close(); } + }); + + it('reveals a modified jump and a following reversal from the latest target', async () => { + const view = await fixture(); + try { + view.select(3, 1); + view.key('ArrowRight', { metaKey: true }); + view.flush(); + view.visible(view.cell(3, 11)); + assert.strictEqual(view.window.document.activeElement, view.cell(3, 11)); + view.key('ArrowLeft', { repeat: true }); + view.key('ArrowUp', { repeat: true }); + view.flush(); + view.visible(view.cell(2, 10)); + assert.strictEqual(view.pendingSmooth, undefined); + } finally { view.close(); } + }); + + it('keeps navigation visible at the existing loaded-chunk boundary without mutating rows', async () => { + const view = await fixture({ rowCount: 1100, columnCount: 2 }); + try { + const last = Number(view.table.querySelector('tr:last-child [data-row]').getAttribute('data-row')); + assert.ok(last < 1099); + view.select(last - 2, 1); + for (let index = 0; index < 6; index++) { view.key('ArrowDown', { repeat: true }); view.flush(); } + assert.strictEqual(view.window.document.activeElement, view.cell(last, 1)); + view.visible(view.cell(last, 1)); + assert.strictEqual(view.host.rows().length, 1100); + assert.strictEqual(view.host.document.version, 1); + assert.ok(!view.messages.some(message => message.type === 'editCell')); + } finally { view.close(); } + }); + + it('reveals ordinary endpoints beyond sticky rows and columns without scrolling a frozen axis', async () => { + const view = await fixture(); + try { + for (const row of Array.from(view.table.rows) as any[]) { + for (const cell of Array.from(row.cells) as any[]) { + const r = Number(cell.getAttribute('data-row')), column = Number(cell.getAttribute('data-col')); + if (r < 2) { cell.classList.add('csv-frozen-row'); cell.style.position = 'sticky'; cell.style.top = `${r * 24}px`; } + if (column === 0) { cell.classList.add('csv-frozen-column'); cell.style.position = 'sticky'; cell.style.left = '0px'; } + } + } + view.select(2, 1); + for (let index = 0; index < 12; index++) { view.key('ArrowDown', { repeat: true }); } + for (let index = 0; index < 7; index++) { view.key('ArrowRight', { repeat: true }); } + view.flush(); + view.visible(view.cell(14, 8), 48, 80); + const frozenTop = view.container.scrollTop, frozenLeft = view.container.scrollLeft; + assert.ok(frozenTop > 0 && frozenLeft > 0); + view.window.navigationTestApi.select(view.cell(1, 8)); + view.flush(); + assert.strictEqual(view.container.scrollTop, frozenTop, 'frozen row selection keeps the scrolled vertical axis'); + view.window.navigationTestApi.select(view.cell(14, 0)); + view.flush(); + assert.strictEqual(view.container.scrollLeft, frozenLeft, 'frozen column selection keeps the scrolled horizontal axis'); + view.window.navigationTestApi.select(view.cell(14, 8)); + view.flush(); + for (let index = 0; index < 12; index++) { view.key('ArrowUp', { repeat: true }); } + for (let index = 0; index < 7; index++) { view.key('ArrowLeft', { repeat: true }); } + view.flush(); + view.visible(view.cell(2, 1), 48, 80); + const top = view.container.scrollTop; + view.key('ArrowUp'); + view.flush(); + assert.strictEqual(view.container.scrollTop, top, 'a frozen target retains its own vertical axis'); + view.visible(view.cell(1, 1)); + } finally { view.close(); } + }); +}); From 82b12ea681114a08a1e92687a1016545753974c8 Mon Sep 17 00:00:00 2001 From: Jona Date: Sat, 17 Oct 2026 03:54:16 -0400 Subject: [PATCH 3/5] Include held-arrow scrolling browser verification --- images/held-arrow-scroll.jpg | Bin 0 -> 239848 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/held-arrow-scroll.jpg diff --git a/images/held-arrow-scroll.jpg b/images/held-arrow-scroll.jpg new file mode 100644 index 0000000000000000000000000000000000000000..053e266986b124214011712c0e03946432db0fc0 GIT binary patch literal 239848 zcmcG$cT|(j_b-~zi}WTWaJd&WE2$S6qJ+{R5Xk_57&%z4UgcnAXXoMt^Ko$t z^02e>i}MQ#K}1AFIQgJAp~5%8!Xm=|5CKwBQqoY-u+Y%32y?M>3I88I7assjR3szh z08-#}00|S2lnHq85x@%okdOlZc>sSOa#Au1N)jLy^<}jUBLGNBNW1CTQ@3sA5~vZ|Y0H#M`zP_juO9ns-wMWbVaAX%T(^w;m$rDZgKtci}B_SuF1pbd6 z{!_)oOe!GBqE2RFKe|`Az`mTLH+!P}yC9Q$HmsVXQ z9XS1uY&l$AAvqkx&rUVjyaf5YVD{K&W{mhzd{3o3c$LbprO6s&b5h2%qRjM285@>(Fi{Iot}^^Ab*oQe*Y^I$1g|>1 zsOuz^8uJ4QrXo%d8Z}B2MScLq&!2mg3Pp`?eh*r>R_#A+ExY|PQI21)u7$UQ+mE5y zq@0YjRWgEtN>+T=d-8h-Ti2MU>Ag=a#6%gHLCT3*9dwU|Sqw`!23eTg^lSm`6~eg^LJk9=R&W! z)tfRz^bbsz7hW%404)7VJYOEaen#+#AH`T$S3f3Pdsr^P94Wa!C>M*ZFiqmM_< zH{j|kjU3-Za%Vtdl(28_Zh_>Hh0P*8MNMDgX?5`wz0zr%T(Kv;QQ*hdhxaN`7;93l zUvEoIdfK_lyiR!9u5S)C+LVU`I>q36H8N|j4$8IgblF1$9?nNDzcN3flqqVI&R?#N zmat%dXahB#*NC6!nsHRZRuz8($?2w*i;5Vxlls0gcMUGxOOv}Mo#6ED=GK-!$S-;t zytwqn&@jE_D>`Q0E# zO~h~-4rLbRdpVz8X7b$;^ggxl9^2(Hfx9c^(Kz*NmN^;gRHpj=e0$Fjzyd$r*$MH- zJiUpwU+JGEX~BKAeCXsh6|wlX8Hb~e*uWzY8~X<&1st%U(B9*xE!)RA5Hv zsa{uELWGk7HF=&Bd38!I!>*WOzaAiIo!&~@SMq~7@y^a{!Uce-J=Ha^t9mIjBP)-- zAPhBDAcHaN=TCZ%>Q!Bt1fwl@G|}?ZOyX|wvFo?HUm$jN=?m4E8qg|2w}-@k%pn;#q{qj zh^T0Hefql1e6`cnHya6esi~RuUaafA$}5?-*Rp@!>0*wSt^ z6t5)Xk-K3o6OQq@M2L8?is{f^N&go0*4YIeCimGh?Z8?w{ZdfaVz`b53pVPCE9ccA zf%@Q_PYqr*=P7r)Ion7T(u&obMsR@0bg~YGL}F4~Ka^rw#=_c-A_ka{XAFdS zu@Q}Fs_v;BlecL6cL{EzHt+-oO&Efcrkf$oxXhEUi0Rh_-~wP?Y4t)2ixMR0X}-;z zF#SS)kH?rbbT?`P^VHvH>GkZXZ8qe@@sHAGRx5P^Wn4~OeW!)_RjGWZ25C=#ESO58 zS5RLOuB6fQ%b`tBBAKF5(KAjl#=eQ`A(+W5hGC6Cp(v%_Tde|4GeHmN_966gs{I}M z*0_JKr$Rb4Myd!fc*$bA|3sllQjVLhKkUq#*Dpg)V>0ursq^G&bq#19#O6psMafx8 zh>5vB<)I=kD${#tle^S~6ohKm;%H%pBp3`;9~q-Cz3*8_$;>0gk)qtw`7iPL0Y9er zDl865g-POt3uuIk!i0lH`bm2F+_peYYJ#hkcaWP8`U%^~JoHCKrXty9zh?93Ebx%(KLHr_E<-VTvl- zr=~+6iHgm-=I3++Ize7%Y#ikz3a5lz7ex|e@*vV z!Vw6a`S8*b&^>eY7rUxiW}OlWnW=+bg`h|XzpKK!6zo%FSbY4r7lfgwWhQfF^vl)v zU1N@ov$7bRi>CANbUHpJjONzF$}r!;nba=wt-%ba1aGi3f^2NY>Fr>{!o}R5kf7D45 z+*gf6Y9xk+bWDC@fU1Mm2qQdW@#Fgjbzfh~79Bde-gX{OKNCwlD;yh7VqK%9HdIH9 z1-bj&-p!>sxLTuEPbM>Sn$k%X+F5CnWsJefNVd|1gXXV<=cWi>fwI-T+(G*#ZZggg zm`Q$a(6|@^?mCgXI=jNa+WZ@WeF|H5wr424ve~3;vMI!PQyUVjab4aiixbhuWCB_& zB^DYadZ5G+PA<{uc~wut_T*IP5mb*i>!2{m2ZPCf4aiO2-iV=_?ixzyTQ2OF|AOl&dhbGUQDSd;7P~Mhap3puY|HyFL=@zOx{fM zYUqyQ-3lp7OsmL!pW;VWFz@KN1mTtPFKzkso#}1b&USB{+|t*134N3K8=B9>6{cZi zg}PU*)F@>r5b^8M3Zh&Jtpk)S1s^k5>J6WZz@_->@|->tF;n;o8uzOoMC&F_wo*r5 z066Rlip0-y^?qCcLOiy0{Gbg2wfIWA44me?ax0|?%qLzSlSnk7v z#S6gA8r%}O8-k*T|C~3xJTsp$gi=cd*IpB2@|&wq87eB_s%xcRVgG^LWsu_*E)7y>M%%Cu<~L(=a#Cxfc#{Zz`T5nNm}mv&rIV@s;A9@^bw)%4HMuBa zECaMXOQB9?OygV`<-g!2Eh+KiYlL&lG{3Ck2CW}B>;1qgzo4OMNzP4gAM9FV#WaCy z`U1e*k$^^@{#bSB^z?}}o?LDKu+}6lKOErP3X9?*%`veaaEj<;78~1*%|Bzf`^C*( za#r67izTUtizvm?>abKP?^-3aWNKXlzj|O#k4RIVILvV!|KdcUB}$IROsAuy<&h}zbEF8i41ub86&`tm@@qC zv9E%Cjhh$-_tQE&5=V_p57^-bz`Fp&@iUS;9zHf%4!s0aQET?<*Xe71fq}n8kQy=t ziNeD7a)b73QPpR!a6Sm38IJ?OTlC&JDV+m!HD^?0F*F!@W;2W)8PiA(Y5u4+-+J)7 z*M^TP!NW@UVs|T(S14mxC>=9U2Pe$Nnl22f^r;%kzP2h~?AY~unjgC6R7<{$b330} zlg5gt-%E-Gb49DOQn1I*PPKeW6Wt&$lW+2#@81^QW-?psxa-t{a9?R{!meEv8Kzbs zhtOEr&paB-&J!@SF1&eOlADlXs|gqFkOd}L0@&!@6Lt!uYb?~Ccgh9U>w`{*`dxdZ zc(SVn8`qIjOvhV>RjDby5K7L2H%VikJ2Qz8hP#|6-HU@+j_;u=AA2T?M@D(x<4`b( zfCxk7_f=Q>#R7xtyztdd74Rg#oKv`3t@bj^g8-m{kQwvQQuCimX!zfbP^xsT_q~-&PwL( z>;4h|_sMlH%s>x_1e;si_CL~BBX+nzd3uz364-~lJ?{5XE}f5VwTA6)XUO8hui;J% zN)z`jh63y2BPL!!^iD-TAj6dm$sI~b8qMtdR5a=u&yMKBlO=9A;t3(jrYqrRuu&3=Ih!a|y<=fAB>nDzM@3U5<0MH5$ z&wW%dMg>91=tBmm6O~JsxR@vi<9Ba4KhN!@3CvxsB#UBpLB6PJ;Ktro&{dQEXRWqxg+mpl z^LkG#QYHpWGxf!l%`5}h?%quvPq0k+6`jE*qRY>k0U<3=lXbi=EbW$vea}6)cIt#6$9=^m%C@p%_rX&KxZK#{>RE2SE4MK8T%b&h(eTrUHe6AK1M86ciZ z?w2DG%~X9GOYpaaw{RI?y4R*6CR8xJPlIx}35tYYK3%VV7M)!H4k_E%9=}81K6`Xk zV^Psi5&epHqVbN%XQ&}-A^^ve|AJ4OzbK3dTN)dB$Ntm*#n@uPLaPY9cvx-b5yhvU zL40}>~_z57q^3pRvfrz<&BF-Lq{!|HCDX!~K zE8spi{*floRlrb*83;e-d3ei>QyVzKbAkvwwa^>eX1aE}X|z?QXmPA`7bBg>Cw$N0 zDRYSA#wS0q7ioTJ>>z)TVWYGk?S!6(M_yX;QU0&BTA7<{(8y8}+BmXnzt`y#f={fn zRkz&o*uPFVb>>QlB37Dz-SrIs)G|iygrtWjZkzD=DWWIuxYTMry8t95%H^^Q3Hs6W*bFW3~?cuXdIbs@z_jGRZVtQ0H769}ZU4baf z(AUKLRcli%UzJmj?(3|jJd4r}gptl!-{pZea_}(2G z4{EXGr6jfJ$r9RDV>l3H_O^tzy;Zi3mSlVQTS{#u%Sb|a9JNjsO`M&um`-!)PsxGGX_ElL_2 zEG9_rh^6@pdzgT_L;{)ZELKLbrZ zeyWr-gFL4@ej?%-Z?h*%?Hg8d2kB6iX$@r`g9F_QAAhQN2J`Kfkn<-D>oU;>TQTM(oF2Sdql zo0oHOPlbMee%q^tVPW9+zJrg$fguF$jW2cK36=HRZn#^}n8KEn** zE1uF5laCU1>f)-tdsW0cG0iJgMW+zMEzz*c!(Qdw=O@P&YA1_!eQkbjdtXKaUtp*t z+qokIWlx=H-nZ5D{=VOqr*4~Bt56GlC~DHD+IR?KHMJH>e<#^N)JJMa4tj`n8--Kg z{r_$wWUQut{>25}zuE;PXT1RYUEj71;p<~#bq?hv{mooR9zEr|+Hqj=~ zs7Mvkkrs+{wGf$U!vjL*ZO?Ze#CWf8NIHzw7~+jP*i=R0L0Xa{$?2@ zOkqJg(Gc)6%m2$1xG%6LE>Yi}+;JJg9Uh@OWcyt{OMJZnT%59YX_9A@reb}#eFQh$ z8HzJ5=YimpjQqsWraIrxm7scjZ9_n>Kf$J!P{gdWEdly2o{nizik`GSF2*?vLL}s9&HiTF*VJ zUVL#=P)1EHwls2+zYbG#ga7m=AXKI(p43s{SFoGE&4aXS2j3~?B^Jly>C+2gTNi*_ zoQS$A>BkB0)6I}|!=m}G8InYAeC42DM+uL6YZmg(BGR;ERrO8pC@d6_9!1WdUdj_e zPbFL3xPMN}`1W&GD>-7jVV%7AAU)aH1pVn}d?t?1s=|qtqTh}O{{w_)Ck)xF$_+>h z;oL+6>C$<~(jqh1M@y3lJAK6?p`XEBekY;Z9{S$y*<}bvLZs7S26-WbR`3jaRQX67 zSniN$K0)RFGFUW~>)Z87zR3c!fx_Xkjr1&5pSd}6-lcl>@!_H`oK4tFt0YlZ^X5jP z6B!-n7^V^>rE3%5I4KoRYE~(U?hBC5ukZ0=*(qxzj7=1zU=KsW)G7Hgl-HDL-gc`P zpDI@g!A0kRgB2>Cw(*tQeb2%S9)eO z!Z9w|eF1=1$g)x#*flzhnff3J`yVnqZ<8ke6g@arQX8&xYq*^Mu_yG4SqdKV6r)$0 zXC}v@(pBqt3^QD>qNw<+ zlUtrg+0!L6%k#NNy8lS=3@QvQg^a{QxaY|N9#5*qliFSYxD^B|p^dYH!WzJc0XP}~ z-3(^W5bmYJ7sK*v`PJ*(Dm&-dzS zbY)EVvAlF+9MkZs_d(QShQ8!{A&d#kWjh45{6z9;?2d6ZVG!-+C+w#!;?d!6d;!QJ z_;Np{x$}e|=}mZJV;LW7v7$h`^%(DnF*m~>`23luiw9=Z?F&JeG@F}o0~y!!z2gF% zw7bCSw#dfa19DHS2IX+>e5 ztsb}g_nP|$lS;I>Vz$1km>1}I0kFcp%3(g6Z*9c7sos5z989h+nl`g>!MialTC-p(wmt>CY2dX^^UfC4Wav$)zh+-I z?eg#q<5y}J-%UU$E2Vm~*C%SC6I!gRX{$;+&BVjh1vBEeW>&8&ZE=GNM#q%MgOTSI z+gPCIt({Q_FJB7qv)*=OQY`bRtssOITduHYWEmQU6aZ+H9YP%GMOuhErk;#DMzs>I z@yivAfp0`75aD0*tdG7e-Dk}dPXnd1{d*WvPlEXUC38JiMo6rX-J zB{hN>sowolan9aKpiBOp<4LZ~a)M{F9mITE)^#+$(Wf-VXeQ}okJnj%eA`VUt z>Bb~qZzj8$K7#{%#2L8J@)sT^5*WPe*j9f&X|&688K~(2Wrpk9TT;Uy5Wu}Fd40PD z;4L;6T#=X(|$gtfK4DNg68iz$R@fSnW^PH z9e(kZ5V2v8{yZ(Dc(oDW1z=@+mZHa>_-y2ucY^!^;Hx0%k?Ru}M_n-S09y*i=Dno! zPCaydUW8*0R(%y39C(uT_Q5Ko_E2S5^8B{Tc<6+sl;0YCiN&dgzy26V&AU3=yqm zOmLqpFOcr!Oe-WX^66~mxY+LT{sqAJ`PtYsY1RI?*I;Sp1J6u<&d~B84vz<8k#NJh zY-BpfI|zel<L+FlL;B!rbYlpHW$oqac>YF#vz$sr;Y)+^X`7!D{ z!xI$Ux>R1j^ZtNcPZQp}p1x}>oI6oR)ZaC_L7n%u{{6_UTL-b~W%=x2^N!k#@>vCY5&L7G}{3;KWrpFE} zhJbKt4TK()sX>ua+4oJVJG=0?taGX_M@%J$zoSY=p|kKkxmp1MJwdVl6nIBkm@aA% z(LCcs8s50Glhk+J z(sMA^;=R!9Qt3jOm7cj$x#-GaICA7Zfjx858sWPM0DmAk<|*Zk)>c{aWUL=vauoN; zVL8n+FX4WOKt|V8d8d{*J-K5mXkU+7V?b+fZ(VoanOUx!PR9Ww@HN;K5lESya`?y{ zLJ0a})iV5v?5gP_xUcGFI9b^T$+yM*-BVYIuB1-15sz&ofn~$Cx64c_nFkV^M>Lq& zdUW!@oM1iY#N~Ut1O>L)g=?J8J<|exZP`58UwjcND%P|gsRywmSl4clIVtw)eLbp> zT;zUdirD_-RGjZbsYXt}xe8e|uYI^CyD+dl)GNfb+E|K<)Pbhei-<{p)oxlr)(Vtl zY=H}(?L2R?iTdmuUjHH7QubmPU!SetB=T_`{lPqaot$$k)uyjFfx}Fu09#hyp=a4n zCdQq-WKg(Y!qkag6TG`3S05M@D_HJKrOxfH0RtrR&=@cwY<`$kn-y)sLCa}5z>2nQ zqo>1f*X|l6a3&WkbPw}uy3RQ!arQFQa|)waZCp6TNRbV3sB}M;Rt7)szNgdq7l1sw z;p|U~Zi8p~=6zX|c@dK-ndv$Ug4ATk#(-@M`Wwvry)21VhNMqm?>bt^Iwfyx?BJ$( zu44&d37xx}$nNct>OtcwJ;rqXJC03U*H6qT-9-Uz+>1rTq47$;o+Z(3QzM8sbZSy0&v+-$b zl!J)s>u{J~KD$Q~ZmLhLL7hOmdI2mop%`iJbuBN?PrKI zIbl~lLumoc430^SlIxo&EP>6DQulQP3|DLqcw{9+;b{z7trNjUviy8>x&CSeYXpFheEFJGWULsMDnzje4 z=<1B$t*7L%8A#jV5PTyV4Korh6~+Q?0*%R*%HFDtCCoMF9|f-n&TjLx8sckQKfk`^ zyB4b09b8H!Zuh14x5fov8`b8M;A{Ps8@2GsS&i2XnE+bKK}KCBTmnphY6edKwllX3 z?A@(9q=%NgG)A#Mivq)z8Fe*bzCseDaSGP4j{D@**G=5a0xWHWhdx>dU>-Zq`n;Wqm7gWNe1QEQ^cUgWO3$BhVJ%O`+qhGaZ9km2VYonG+Pc zHMY3*9UL-jOQMlAjtnD3wmcMia|sty4%(7y6xD{%{Ozsw^U@uld(>c~_nt5sdpI_lSek|yz#lT_YWqgH?6w1v z&y&inJI}cvH0=%Qhqmrsb4XK&kntPT(w3_;wtSqPbdM$XA+Hp<_g2V;+{Se+j?94t z+DE9M4HSY^HNEtCF)q&k6}^JrHP3V!u!1avnOP8jy+M}2A>0bxN^rqtPIQ=B z*zWBXenm7s^@e=AY~~^ELbC)tORf+0%+^T zbXz>&Wm3FLmP_WcML(@8QZfp3j0m5Coib_zs~^Zb0X@(@iS>}^cuql_<~?b^cFTi( z#zs&SN!0DO>{#q`{_QlBm=WdlXJ&&IoSxiSO%oT5-lGLmUQ6$4F#t3SHk5E-=5RgN}U=-txOaBXK&o@&v=j; z;P6TSS6B(RgqG7qv}ogYWR?USTAYOT+n5c|xW1-dRE`9zFPDC-uoZy*4V?dUd2I4+ zmQczV_lr&BgoOY<>7JF92};!gZ&x$IOuY!NkeNMq?g$Vi7jCbqlH@W*Y@QH%3O*g70k#0o$z#+4ENS(g-2 z)&=}A_eYXvDiY+{1HalR6o3CPV(<$YSL0NbNat6T|1>g z(#%@sc}R!e@gdlH3q<78paF2A#7mQD>YZkjPRD&Bwhesx={+xf@&vZ7R;5ZO3Jh=W zh$H7`ps63`erH*cd%x#l8g#u9W@ZIGa-t5!iPNRF()%$b!601dR#6C>-E~pL=RPU< z!|m~3A;$VzYdZ8@itX)t*VtD6Kw9+t=a)`I=qu-^QR(J*n`NxMfYny`$Z>(3ffrkC zh*3~I$7<}}+ne~Ia-<*oEE;YCF_g6zhx)-}C4;)V9xPu!kg`5@Shb=Ev9%q+GT>v!NE&v~8F97{3LIs>ts&t)~C((SzOsHXK z@_?Kzvo%z1dbY02Q$z%Szl&SQxONyuVdTY@x$t9hBR4t)SJ5*i!U;Ku-N7q!R0`jp zc?+I6vA{RWQv1Y3Lc>U>mhyC7*>TTjSw0TkUS zH{n%J-QxUQAa=JNjB-pb8;n6m)Zhd(`RC_NqX{(QXxEW%MP^x-IfLdPeJL&^%Jg+V ztphc>nqI@(j4k=~^>-%?&6**mqD}r&Dk;bjYh5bJQot2pJtF_kQan!UzVv|v!YN8X zkIRfo_^LYS3MT)1UQ;Pug(ufL>z$=x&#j(b_$ij+bz(ugk-rGZaDNZ z!2vAcP9as`Wn^m7=C>H|eWO-oyUU9?y+F#i!Uf)sG?YTp&+|qB%bqzbL@OK+-L>=w ze&-*3Yt_RP|GW`VlxIbCpi;O;kB5Isr$4^`x!p$D>)uc{bp47!{*hjv%_PPl(uG`$ zm1JY0aJj^gG7w~r2t7=I{4o3?w7L+#XzQ3>%1X$!4ZIJkouulUmj(%?*Y8ZCPpzy73;6B>)XI>4oUkNP(Fg>>mOva?%8lDU zblpEiefCUqZ@&A87vGzCRHMkmp02Q%Yo3Bg&mhCzTXSe2ZN-L%EDnU$z+$d1PQchx zrj5Or-$tg^dHLk}4j&^1uK#vSso&Ocf5;v&-H!r6Z+WR-73BBZE08-E`;cipQ5{1! zsS@aYCIfM7{^%E8DEg-&qQs1*Tf;Z;-Wmj`86xKT^qF#qyklQOk=m`jI4J05U`|Le zE6G5RS74k8=Pl7hRq&Fqr^<3*y_UjZx5gKoRu&bY9OSMC;PE5n6)kK;OTIswyi4$> z<7qX2mc&)-GAv!9yrfVdlN;RbZ_8J2z*q?$NTeL;#8>Kj`czXPBbb41G`y^LQi8T^ z`pGb5+#x3cBEg=|3qP4A!C#up)~-aw$+0D4ES`pwMjyfdTj8zk(M%|b-9-6L z@r|b@49C;H8ne1(3Go11E&rcyMQYe6MpIZi!Jjpe<`M^2G& zr*3l{4I4PTC;4c&sO6md>1BlQ>83=Ol{T9S0b^fQGhjbyw&W5B{FuI}5sfU&FGy-H z;4i{1H}&2)_|iu+Vl(Eu*^SQ6?(x&TjOp3F)W}5(!$eWB0Qv)G2H9f}P8|+n^rqS= zE75vFHpL_)Q|#D-Hc*;wEOb%e|J6=WP#|%(jXwJM)aGsL^UzAwS2wmYPCkpG?ftqP zhOH%_wF{(3YDzrNbk6O0$-u9b(Bpjz>j;cS$pHU%^+4;(d2kJ8U_Kk#ZM(j;Ym1() zYPGWok$5pYwLgtZb6qU0OZ81--uqlrC_$~SA6}i|0RKErDafZ#aI?ldVUDvk8?trh zWOO%i?snSoo3r&R+iXQA-H(dlzdnZ^1uw~ugMPcV{vuV@c5c~!xq*37YZt2NAG)>; z&H9G!;QJtEqHqxEOXAPxubkVvLm1mn(ldeoE~@x_V@d*@UXveG%JzV`j_|`h)N*q^;6oPPg?7pHbFv|JmmI zD@_8!)^zyK^W_L;GcO=${xQI4)MVs;)%FP=>Sg*bA~L0_qZesNwP6VDKK2g1P5i8C zTBeTHbHqD>{7gs*k4LC({WnHiCXb$d%;FBo-s5c4i{VyT%bCOql0ra_={|+cX8tP; zpvAvE%lMySxpq2q)Tf4e+Zr}JinYIp8#sR{vIH>Bugv>`{h`dJH^omxa!&f?Jjc+E&$o5c7FuDeiwzEKj$)w zeIq2jQ+?+hw4y|H} zk}$7nk)gLyyIL)q%7l3`b%QQNgv8mVl#q---q3cR#lA-iHTx*`{NL>}#+)cI?SDu7FZfXS$B+KI*P_9UKE>_4E%(te;Ow%W1!sP^4eu#vbz1;6kH=UnsU%?bSf z7S=+tMNYPvJ9iuB$;rqoavyCKNKcp!=CjVv^A?5S#AUYi|LpCpzfcLxE2^>mE6?YDF#-0_8PD+-7heCx;1SMgu{#0- zHIGLfRgY?*WSqoLsHE<|&b8Q(M31srh7q ziT}Z+;xNB~P;{nL; z3gHrVKbFVe)Ae^qW33Jw@!AyS)wMGzkTNqJc)zRW;@-NI%=ib0it9<+D%;*gXjikC z@CJ%rrX$joM!YU3wVxQ_>=!c-x-%>uz+6m014KY8IC; zf~j>uC8sB2PiaV{|6|hhJ~pMtq>M8QDQym?7t$irU(W-!U=N(}w>c``uSzqsjPqd* zI=DS(O_1q90BJiRy@t;8?=Dh~g_q&+n!~tuUY+i+fl+IS;M~jj1~F-52d7q)IKhh> zRfD@5#8PYITDxyBdwm5}WGmF85zo5ptFWjMMwXa-&WPSBxH(~7Df(7wFr3r$o8(K= zPh=twGb7KzVjr#~rrn0JCyOhHk7Z95CEQMBXI1Vza0s*Yp!O_WW)y_sG>>VkqJlokYkh|NE9H z6*aYG>Jk7vgtCo{$trt}==kKF-EdgM|M7LN9l2tV#^UU?Mfp|j=bedG0zy6s>0!JY z_rYQhv&xYZmsdD=4ZJpm>8tr_rym;|#Ze4yx3de!8>vbg*{|rM#Y_<4mpq({<(=3VKMQfdLtrYi|&`J8njkcDXR^fx^@#k3L0v#cBfvH`z}E^^6jxV5ZIX%x?j|U#kO=$ksc)EL6d1fax zE9SAc(p%{~CyNF#&F$*`yPugvOtopjE+aI#R>N$+Efp$V1{ic=vA4J%ATewL)cofk zSDSx01+Kqpkv}b^$BZ+6pBsHCIJ(V{bwfg03GvmVi$L_|9$!h(Q|H{B;qfhJyv8Bz z7xc+(p~o}H2KgcR=mF!jK+2v=>a50hNn2U|f9ZYiKO9vHdbDrM8_nZMi^4T$ z&R;bcPJ7@eA_6L0Lt3ZizZr?xqft&0e#61-oafX!Met*y9eHRy#ZzzXmQ(z~k6yVn zh293Vc^tpel5HCV*){khsw~xfWHkHo+>FvDr_QNAWyT4$GjoS#oLSKfGrmojM>XKU zPc#z{v@XNH*^)D^Q_^S??mmFWxT57OUShsK7|Xb>k)NEdFs3TSc-qTLVJKIrHkGkf z^yFx6@%4MLPg=h>+PYwVZg0eAepWU?Oe`m;Oz7|;yWKgR z!A0^+Jk}s$PVz(mXYz@J0{~FUd45JNzX7n;V(Je2NwBpXX8)f!=8K|>#a-m z$|9A2fMmR0SoNZs?e`aR3Yn#ANI*xjQlLilIczb{2Evh~?^rqykh<$ba2Ngb_w%wj z-)D5d?n$gb*b*29bZ^YhgNY~yOF%A3S_(Y{@cIjz*ItJq za5pIr_8Kzy3)JLuEi7Ug+;a%o#T{JKh}VAZIE$ZA85IlC zXHF#b9Il#igj*YDyWH2*p6`R}aWO29R-?<6pXDsG8uQwHBVlD&7Y~#yB0@JkOfW=Y zmni5=jh{%#w0S+VUjMT=1<<|3`wiQ#&Op1_e8U;0%E}c2*#@n@I3>t7#Ax{Ee}xw) zC0bhO@m%@+`E%Sj#5z%DbegG%M<4z3GXD>6;HU6s)B=Sw4Pi;1tLMXxpX*VtUBL&BP4ArAwGRJ05@t5va!Z6I!KLwk`$xY2Y6H$i*NC#x&3c{ah;o=;e+kuxFAQ$&OOR!~ zi3;#0!hGi8t^3XC^;U~SyyXME_hju*Ct%DAz))1~4bR07ZiLoT_wNy_o$Zy*RjEPm zNh+oiXWMJ`zB`r z{-uv5gL5$5WXzrSSg#nr=>tHb58l1OB;2y&=F}yjnkExjroPZ)amj7S4&IezdxKo@ z)+_(N<;MSpZ`kV}v-A9RZr$N&U{3+waXKdh4Y5^qvfvVTnVLw|r_QY%M=pnqyVE|& zK!R9wpT9cK%H7DHAJ8StGU!I!A(3AgrRCcw3Vfq=gEhb}jO$My^6#nF7es(~J_0TI z2Lg9cB!OQ?W=FNA)8=L)ESq2CN(>wvxmlNb+CHEdcp204agd{xS(%_CV(Lwh{oBt^ zJ*?8}40#Be`opZ9Hj3$MfxL>gHEvo!%v#u|@dugl75$7tN8A3s9=|YR%oQ)QoSMy} z`|#LIhkyrrnmG9bOq59H6D1ie*Uo}W5hR^}bn{#9Y}*LOco+G2QtF|Yh0TiWt*YP> zQY7Zm>RwsYMN;1kiJU@SDan~h9WZ%5njyC-kr#83lUH2=$r+L?(l<+z*lv?4DfQs{ z>qA$^!J0etR@o-$QJ?H2XdG(vUFwqEF-t4f=mrwgBCA|%5%wOH%E4dOR176Pyv^RN z!BN_1IlDgt;2x6)vr4k1i8C?umpyb!io?E!|QdenaK zkAJV(@EW#UZv+^OJ5ZX;&v5LW{>;V1c{fXp&Vrsa3bUsmQ<9swBXz(UTKbK{8?G1; zwh+^B0DvFcKp7xVUAisU_}eIVwl}F7y@XbJP2C~MbU}54x;;i#{HjsdgAPE#9$A$| zzp0_3rJ{AN30E_Z*0rHJ%EEdKoZNL9ngtf!Tx8&T!3_%S<4 zb5_K4rm&NB^dsUqADPQMxwotbsTrIsd1}C}NM|5Rhr!VlO_&*Re`6@u@kR zOx7OmO&&PYM%*x*2C18XYF)<-HCyzbmj_(KSQ=RbRNqPUeZpHf2h=b6{`WMAJ_>DPkdomOp&m;WjR0;|&65{#yLOCa;t8Z#)a9%j*8LGn zcSAQ$L^+!-x9dFOtNVJEM^C~eGB1qr6u_w>{Zv{uQ$m!e8S~?Ahf|TelHoG|Pa$(R zCB0i2r_^6MKL(Wyr@qe?OY}7T)Cn@p)ZC7HgJknztiHA( z$+7huI6UiiiYFSUR*Y{X-i;c`-=gv)0lpv-=jhiO*TGY=vxhrGmmLir)MJKaFLZ5< zL1j5=iR}n%5LwX2U?u(;Y~kMjJujDD)y-KLAM&s>l(Dp)y2YK%MbUT!&LhaP>hXmr zac~MPEQ)cC6nzr0CYQc_+2eItO1dZ*Av@Ms^^GSv8&Cg>KK z-**Z;{ATYF65-{ZCtS1%6xDoBXd1p+Ntcum$Y4y1ZYO}msT29G5UpLl9Dp_x1ctA! z(Bf?OPrH76J%#`88Z86=)9PanfBcFrLNfVrTW+ zj_Iy*64F4xT9a_B2!@NU(z4#mR){_?Lc&--kydC+eIB$^?6;pW6KkZ&GrwjbqlbCe%s_%YKcu0 zRpPaNUphJV zbLxH2qNI=0moCuF#s16j$8I)!vAUD2DsgzSR!`bogNaG!qSzjDxW%9xb9X1>XV13-2w90)VC*;R#UhN%(>eVy zwVd{HiEoi_t5h;2)Y|CcK6==aZHIH#4K}G5sP}0*3vH>sd&)U2SWJx(C-77_&#St< z5hYT`8*OMkYk_izhd?YD7V6iK%y>IV5}2KVjK-TeFo;_t-l)v*-;SumEVmbr_U8 z)e*ev^xOk;Ei2>p9Xxd;=XdjNXQ|)NL|B2q%wgG{gUxt0FT@HtqEGu6bhF!r)M-ZQF3FXe?=*KcER8B|b z)iT!D53={xzPxk&K=m1mp`FV`ECRf>I(^|>OkBWJVC}x$)HwQoB)rU}tlb zT#)u&Uf6_@zSGD%^cFgrI0Ct0tpvmHH>>jx)}%Yh!KGAcYS z-6uZ25L>+0(0hsm+QR+3eU-3P!LR_92K}PJ?6?6S>hEWz3v@-2j*Y9PQY`gA=pni_ zO%*7PEOE)$=NKQ8%8A77P6~H2S+z+31o|5Al9dhT$W-Dwi;Keh>!q7)7QxPMbRXV! zKlm9ilIpU|QDH2pCz#NuUS=%pC-ilgW@ z*4Esp(62|zvBUeN)x6oy+R+_VDaEx^>n-I#OQvZ3-pjzr$3XwXdOnt+Au5WA5rLYl zwqW6s;Jqi0*)q~mX`8F#&(6<}tdP}yB^~mI`j1HxZRwa3IWNtQ^Xgt;T4C&dr+2rF7F_$=@JW$n;h)z zeR*WfTEc4+?j!3fyOZLo(gy^U<2NZWHRIu!VCwzxk{9pHE-*gEX!_GuPtb;$JU23M z&WVW32Yhz=x?mjVTa5r8_KTDeEJ z>P!QENR;B@wzOpUQ4z74i%Z;RMuD3CO=LwvT}-}HTa$iUh%>vTkKIWQT1*yB4%l^< z8Y3L5)-3m!f#Foy$T*N2Q+~b|xUV6N56IdOugWjYK~N~O!EM^}bzfbWSl)HK1va&+ zW*rdr6KpSUCTH%_pVHkVwd*uWnv|TnWvdkU6uY4SE;wT+s>Dp6Mx#H*_9^8+-;rwB z7OER0+x~;=Bl;TSmU&|9uX%iRbS()^(>#cqkohklj{}yk#s>{^2kXB$<;^)8_7zwX z*QE4s1{Z=StCr(x}O z#nlF10ejSy<_ftc9?qfya;~Oh0QUZEMm^?QOcnJTO)jCI8?`Q;H-98qIv5dJ8ed}P zO1mvj3BtNHUxq$}p$)x?r^d{~+vmDXUCpMoCsmGk@^wk&eTJxKz%7AY3?@@ayuSX? z@B)u0ibUkdW~XSgSq{5@nE?^py^3mXVCq!4+_fHh?X~)9^L<^iTOBAW?0FUu)RXJv z>gV%`hIXkK^5&k$51b zf=~g5IX0^&*Ls#ME$^#unV0z9zMg2=;Z%!#+dh#dyC6*fzX7QsM=WR0B;}wCm9o* zx#t%$GI_-tlXfO2&P{nJ)`9AYhXP|cXxhb3Mgzc8*V&c43#i7~XKl>^tq#-!wrsgV zv}ZrO8T5Z)_%1&^s7$c);M}f%6jmdVozdSD8NSE0oQIN(`fff08F}|ip+?p@ zNRMQK${?AhSf`LgfZo88BoOQQ6fFen+y1%U7sj9oC*G@)PLIM%>le0o)9ezfL^L2R zP2zn2%C?P5ooaja&jT;Q*yW6P9S~ zW7uTwiJwqhzrB)bp0V44y&|eTJ1wUDJRzwIrzK_IcT!L$GHbN+NP*EW`Bs!dD}Qcm z97q-;YUvH6p~j4YkzUQ0IatkrVe-#}ni*u%0!cMTS!>b;3_o?Am^W7P7sRyt3mDy& zwu*CKwb<*wcJw09C75p5ckF&JZ`$^b*OCcnB156!xm92^b{1VV=t$C5&~2kyh~s9c zPh0Z~17ZHK=JT^A^06rA^yP2+&KZ>%4fECuy1^rn>$v8?X%W34y9_kY~U; zCRd_r#r9_~z%ari zG>|~Jl~YU&O?dNBW5L*R?uehIo?S;Y)pW=fIuYpBkpCbrHz$(4%$H&FTS*fNJ72xE zdas8cS_UAndPrWOX$YAP(>=xaC~O~;^9L!P3x^d<`KHZ;MIqAn+HQOB97A{U#9kXd zn74=%E|k3cbu?^3wbAaRG_95JHp)0v!IpFrP{So(4u@1wNyR-DDi+&M53TYj=x?qL zEupY2+qt5mh){)^*Xiamt8-f11+cQ+4ut)s6{q)8ieIzY@OZjf8+52R4{B-ab8(-r z#m6Kvq%L->cZcxjQRS}|DbWQJ5&{8SkF1%t888{9(hHZom3BEA14%P@9=uiE2*IIT z?C1H_gxM z@PcSg@N&~`A6@{z=-b&`IbT(6pbF-!IVvRWF86;e-L|)6He6 zA^bC%M?dV1I$xtlM9NyeI%^Dncbp@m^EB<{*-YEd*&rV>G@g&{YRrk)z*0uq;U0H=L1-SpSbsX>F-*l<9fi@eS^bZt~huI~{P5#qY#Dx0MD?+;ziijQpx9gi)(+85SY~neWEUa&cjII*_#D@-_+! zMtY~+Ka7^?vNC58GmOd&rK8@GycWF1hw3NiCM}$s6vj0yurF8^34I??_LoG6J$79` z*SXv4bE!GOTiB~B2MLZ!Bp<_s8VjdJ=n<4WUg%(>?hEhu0s=3AL1%r^0-#Sv)WtGe z!2zl?giIo$bX}((?$E(F5m(8)pxP>s4&~iN?plFx}ER>f=R6 z&83oLZutVIZNiY{6lTVC?eOr7~@anN2-) z{gwK>uIIY%N5(`?gQ{8ryCmXZWF=O{g^1Y}5llefNll1n%=h8%VY^DDe{8*QPfF{=>BavWK(gJ3RZM#jtwZUbJG#x5(l z8iDmbxvBIBE{u%1=aT$|`N5Tnp1k83VeHz^D3zo0Q(Iy=v^oN8ix$y_Tek(bsJrL! zhg!v;AzKG!(`SN%TpB`w6y?YC7_ir(Wo^rjTGDH4c#LMi_YU&bhW0KoNi!u_*r`=3 zOc}w_9y~iYPZF*^Rt_3QFy}!%dJN??uneFHffXbq_g<4sJwPMKn0M;V^$m@w<{KQ5 zYYmjW6AM01SWG{Yk96014^p{CQKZmwcSyl7$&o1W>W+&BPv`}+l2SSW0p5Iro0xd>=-W!(SZOXbTK04P1BM8+Q0OOJsgjrQ)S3%_)sE#*vGzp)oq8qd+ zG7!brnj24u@r{%NU9j0bscL9dD*2||***OWL*$bGu|qQOu(Z!5-G@LzCpX{WyWefm z72Y5WX-LZ4a?G=9jNdMUKeJZ~3>jIYi`oQsEwHFT&%SKhPsJhbk-z!q@}*EQ>gV3J zh(no$x2^j}rp$+I2`3QAN#A=5&|%~cRm<}W!I+ADbzr>#gdQ}WTk!JjisGvXBDLP} zAzj_`qMhKosiyuZ%^_cO^67YiQ+s2+xn2Qvz%Y^!z6pN?7qw~zTDjMxN|oggm7x=k z+~Pfiql^oY4=fv5V5UWMONH_fgB}p$6Es`dhh23x<?%?_=zkVw0dF;9|5cIu1WWO$HhMr7F2*t_4=6r%K=&aZ}il4LT?&YX_ zm*$j=xiU0OHJEER-WnriSC8g;<{GEgI>EgjOV{8$pU9Y6N~)1;5e`3k3#*os5>J%z zoa497LPBV#a`q&GjQndHx6f+>vDPP}LF7gB!QD>a56oc{nu!)pN&-(7b);&&br0$; zMwVYHTSj)q40`o%+B&|NXjHkXd^1Byyh|Dam+lUQYL^af;!=Z5xoO>k0#!8o_WV5( z3ukw}iqw4LFIVzO-0?xxmEfAnx!4axm3bo@ z>apaqIM`aqH*JZW-YC$C>z2)5w+dib8;Zb{%^I4_4T(n*ILj=5l0>%c!tUySQ%4h<~I7~y3$O7qIv=e|1; zonG&g&Lw!8R4X5A@N@PtdGyAJ?m}fAm_5rD@D2r==!2<-s4%$r3WDGoZ*BXpMy9rd zS^MfmW=KJ*dsh4q)2cX-|L`|P0vveGSC3+A;k-1HQEhUx(`#I-@1$o<_0C%ru&!IMwo@0snC~&4 z`6;WxhB%ANNG6B7-z_Bt9q)kq42weWb(O(LU5A&Fi1^75dykGCRX>3AoMX#I3VNO; zOeE3X2TEW|#Nb-8vE4xH=;zBFFuwiZUY!_Q_r0q50nD59ACX=?twxfk`h7<=ENiF$ z(Qo1kvjV1Im{6V0#37QGX!flp`a2BD$6sIR(ALnUStDfNI5rVEnODoiEh|$pl}=_` zNpvAvRn4cPvTMN85@Y5Cg`g0ZE95hFQ-{{{Pb4Fd$XrwOBOnYDpddg)i)BxFpZQ}86ZyIu1I<#7)TEs`#kDd&f#xLZm6nfxOP0TiWVmr z7rLZ`Mc7{K3L40|h?DZ>-fGi!`Y_{D@A&&)2uMg1efcInSh-|7Rkv>^9}eIGhZ3pm z5gZAx|K+NXX|e1c1zq2Hha1;9PPy4dtBcqJfK}RERxG?z-$t8tK9BtD8ab}Hek3uU zloG`03Y>VGNhZ?2+?Hgy>o9*{rr8-|H-_A$jaf(eMw4Daps@_KV&}yKa^zEB4vc$w zWwPib<&VVYw|CJI*${MZL#+92w!Jc2X`JzQ$WJp{lBGjPwoo;SPb`Wp1MP$rwHS(- zT{_iOx}@1BGLp}3wyZr)h(}MMa36*4OFb>tuXlZ=`xbb7<49ukAxS$lj!`dZTzd+j zjFWa673tXPpU(R+Hix(BSezL28anP`s2y=|$dnci#`8SZI-(pE@z^aL;q_R#Wv*H1 z+E2{3unEIXS$j;L{{G~L&HMHnd6IEHSSdMeAcS%RB7*UiqXm1--h@Tyh-zgY?!*Z{ z{PJU7ytI%@GwPFWsro~)oG}kQt4Dq6 zWHPB!)nYlRXo*oTSp)>MDjI%xUZhj7BcU3d!-^g#^&^C*^pZ+BjU&vPSvNFS@~#}v zK^Y{LFsvuIf_@ob<;Px~71vNmbLb;4ZnyIl=%z~Oh)=9YRPIZQo9_&+x}{R&UCG|O z!*%+bpvYBbzPdQs_V!;GF7{O}9L(=bFw~;vbKA-sbW;fdcggmm!VtdLIfOLd=Z?x= zwG;P7__w(-fz{}^=>6Zs0yKb%JRIMUzcBKu6BfS^Y6T^dey=i)Wv*H{A+3*GKc!m>i*+ZM|GSlLT0uN5AohJVIFF-{C+iW2i|Q2A%W7yx|=TDPugD?`yXyq z^Xc#8EKdwgrx|Ex88#UH1sTgcfW}OL;J)ON5q$Dw=x<;_tcd{n3 z>QfvGNPa9ITMpXd@jA-JxC${Bb@$B(iF}uMgKn5;Q}f$r7{QuXXn0hUub$Md(IJC9 zvsa!>$1pNK|3HF#h}jxbmCm|z`$Bua(`kX%wWJiEK90>Z45=cf+AvNhd(7Gr^9v*O zUA4M?3Anb2s!^jUc>_rnT0w%q{!)6T@%cIEz%40C*YNP>%J)4Vv;`Q};`QX!wQJD# zh-ky{w955!-@S$&uU)9Rn4Me$+#ouulRAsbRs~;!L#5}*U}WjBGN-Iqqs(%EDbc|U zg+bS5r?-MM1Muv1z=s**keIEPo}Y1V2lMp=)#v?QI~%$Cbcgz%j-{K!cqOiaeRze? zV-X0C`i(UXE~a$Hqtz-5yq3u=$KRV=2QKpW6`$rnZs>WouH zRHg8#5o)T+`*!_#80}^jlG0q%t%uv%IaUx&;>p2W1YXsb&Jf92S3@WM`aF9z0M=y2 zPs$Ck)y*fQmHgqpuQ_kQ1*nc-RgR|l{7^lvj;tzc1bd)w5p#V!@3qoQ=$yEW^Y`j| zXmd%A_G4zkXf(_OHfpJ}6{-d_nF#N2eF89%Vd4E;7*}VHoaAF_P>M<8sXK!QZ;R2) zK8MsRi`z7`t7+!ZgYQ>m0QB4YmMLEkc3#@A$7FxT-LkrY!Pts(fMSkvQtX(xP*4>QW*~>>9s>-VPPZaM1TFt1eGw_)2hF&r7Tb*dz*Ol9yzLjr7!JkN+;uj+N_kaJx`yA*wrbN(iGQ|81o!H-W*?G z=VH7qRnNgxMk|^}xt=jiE0m>{efzI%2nY?rKlP)(xy6pqaj{f+&kw|#_0*3%C5OBQ zk8;?8XQ=AVJ_C?$|T> z5UWMfJ|A>gUegi2%?=9hcTuwF^CH3qV`8L@Mo_Lb)8~B%Y~!t!t5vjGA{)3Pc)y3< zP19jhJ>ZqDj@S2Qcj3I9E^}+jzar<4H^Q?)rXxeT1|+)@M4xc-r3HgXq&;TV=2+;2 z1}!_JL;?JkI-|pfru4OVL1Wb%x+re3zIG5>BzWnI^6kLvra|cuj^P6ag$}t2ae|JK z<-5n59%6;@E@kTWzr|e7Vg>y|*!zkFzRK0s;{+C$}qnoKTFQ%8@v>tqVCn>iVm$UJ}Fn;gQ9I( z>bSld#mUffLnM5`Y%80pM}Q>Q$!+OeCjnL2N)2qXf->a!%^Uu6uU)M8{`mr(tUQ{b9zvze5IWRk z1ciQqKqS9mOeWJ*k-NYVje!iBboa=gU%O|Be^YdqvWPM|;(46+A_G{L2-i&3!Tb5@ zdM_G!IPSMSN8!0E<8}~UH@kX%+*F1-6m|=Ln>RH-6Rl+HYji;#>W-EcI3x zNr~T2n?HAZz_I0PG~&DDI5mREHy8;?)&aZC;7eMyfYbP1s3Zd~JG@jpI!gTJ%3GT> zkk>3ROB^Hq@p6s*X9KmCSLbYxlO7_jqw-g>oXGG$o4oq_nhbycXYjMKdjjkO@CBQA z=55U{ec)*w@^h=OyaAu!$6v#cmLG#cco6wpl>2{PR^YWIw1$sd!IWM#jv6CBiC)6R zH_H1rCAfD!aBkn>z~&%)=z-C>IX%61M*H7)lEp9Dn6#Y~H$l zz-#4QH0%3)X9ALOy&g2iiu0Kp zwtXIEwvm9B>hEbJ#O5UpCbBq$JmKAb=wqb27KwPB0QoH^l7G`0tO#?|=9TFID!0K+{<2PwPvz%KZWI6*w= zHr?JWUKCJ<%=zU>}kn6g*~g(-B<*?^Nmq^ zdDdv{cSDb!K=HsSVOyr}y5KiSsRrX4lCG%)SbJIx=VIQVctwF-DArFHF1WyAqrXIf z_2@T^G)3m5n=U=-D(W0(fR!}RpvlmO5KPVEPtu;0-zdKw;^xnXxP4%#pDLWJ`2nZO znM5*-mSrmRt7?L&{^};33p2ni?-818QoLHl%`UN*#ks0zS|@OwbEipJPUrq*KfT>v1~sQJQzgm7J#jW5}1MFm@_&R_lntz zo|pZN4a$G>qbSiuX<;eDxKaHzm-!O``S^)wUfvD}|AgK5pB)yVw5C_iO@k(wcU;gA z!0-?I|CQ(wjm9IR=~e!?p~i6&8>jp0MVdc@r?_f=_`X74u{az*4 zaC6HiOhg9&)ZF+;Utp@3Pb47iUC3(0(QTeU>yV^BRV?pR$SNa-*YfReaQ=NnAg(wc zjXs!L3laf|=tqx=)BQW6`x{s(%`AE+`a08~AW5P;u$+8{Ku#Evbh2XA{;%RjtjY;7 zjsPQQtaqc5e}2v)pzi!xr~%Yilhb((J1t>vSL;uqAo^fP*3)9R*6*ueAJtHPW>el`HZVx^FW*6&G7A!f%kdNGn z4>cIvPli+(Pn6k1GI?WZfXhM(Yf&~@;BgoQ34x5T`JZ~Pw6K`!v9&GjQ0CEL7*YWn zPxAqD%CMppe|bvF&`I0L3AGUQdpW69ee6$GZGWFTw&}Nlc)GF769Hp1^qe&q0fzQN z=(qsrse;&~}P;ag#>z2xb0$|4dsKJR3;zyd(P#69D(=J%dz)JNSZ{f*5Hh-a8FJJu) ztuGpe<30oes2Ke~Ngn;YM>@pa0w=;2cTm%=43^T1w8Fjn531=2FS#O?7D+idl+m{w#UFMftbb zsyjhZq5^UjC8p|OB&d5>uYTN{GyZ+ef3XJo&YMrRt_q<_KooM|JiKrB`cD;C^|GFO034^6j~#-q zE$HR~oeB^cq;0*X*W4S;QFCv}Hv9a=N9bJ&ta}5}+rG^~|ot%U_xowIVB#5Z?F<~X5vx6}o$p7QGh)|88D~F!| z@{7J(vtn&-_c@{lB`~s&FP9-K{2$&H#yubmf&vI(IJp>Pm;BIij87fa@pV(XqM+TN z0?-FGd98sIl49B8@8aD)e-iNl`-|AuElQ?$dzwZq5qAM~8>K*%3k=Wlc;{pr*Rd|u z+%0M)_qxdt6tKVl6F!3-$=_7D{OzX5>}k0rzyA+(D7~bqo6*^9GdNAk3IQe)ff(@_ z?6?2af{5BCwrJ0W9>hV)`KB=Q;yHtS5s|TjXP?H{!9y8Oaht z*&>uCf2);d*yN%$^PGCN1)`akOD!Z!=T5sa*B$Y*HO2RG2@baS4yaX+DBzt+FNo``VSDh)O0b3=+Y~IS@ga0N*{_a znQ4l+L*zA`gG|^L*4Go?Yhd@y8xTwgVoQELL`}`8>sEo`*AWI7w6&9uL~D3ZA`{k= z`G4%3mSfV~8ux@KV9}siL+z403nnRd6{+z?7pBztji}J2;aQ_pa+v^n5+!hq>bl5P zR-A)|BMoi1nqR*ku4Eb#I2id zq9k>Ao$Uc0dcRc=@R^KFCRCNvr>jPXN|Qnk?UXXgG^JhVUXjuIUsW0Um+viV7jY>M zxz+xM(jx5H>#9HxULoS%u&ncsg@k1CUT}lMUZ2jViD&;~xYV7}f3wr}dlbkon*5_;)>f}N#E1Fj)E*Zt5!=adNXyU8SPzPgvo z?MlfVm^q~PN5OpZm8AHQ|M8nbmBzuLVm-4ZCsOK-t~6k1t=-r{k;-TcX8CW>ZOd~& zaRRsj%9-d-8VLKu>^0S{3@dI*wMA9GZg%JrM9I&ESeenGV4UK{=TXg%>SQ_>l(MuJ z-OQh^7J3_1$M4eo?iut_)E@v9SA$Iqn$u`=GUb)#da1jA#M`S2L#HGXACf(L_6KfI z+Qd97a`>l|F~?5zpyHfZW{<`XV;}uF;DQ}r;I(qxYUpde#%4ypxHr<-Q~aA|_b(X_ zU8Bs#5e)*iG6#FMn?-Do&A zz^6wD8|&+|_21-S-RhrjTYBWBT7_=UTNf}h_>OsrsL1%hp7n<%h;2ec1=One=c?}4 zc2WJF`^r50^AB<~x+jq4Q_q|K-osy^(S9lmtQ%jjim~GTHS5lkak^1c1!m85<}BQr zDmES_wkV2qu^vL|6JZ=WRL7Tv;ZJFN+|GB!l8hl(y+MJJKZV!-@ECxKbPw-kN*e_sLB}rS7hWJa+^tT$b^mig|pT~G4+U)>#|<{ zv$*7OvMqdI^_W+d@IL_hP`^>#^2lDBEo%w0)z;(bE(4z1#1z`U+$nO^$^%)d)0fok zB@}!|GW?hs`9lK=RiDkm^qudT3AS1Hwgiq3Oa$JC$8T+sq(Z!>NQE>N5oB z2mY}BLdGsIpv>f4_%-jXD%CerPj8LKpSe_gg3DoEl9y8gwhegm`+SNG>|Dr0dnj2Z z5|#8xob5YYC{|NOIK)4hwWTgY?t=$D7@ie$FGRz+;R)6&*g6Gjk_^JX5fvoiFRUAQ zg&w#=mGQe07wM9k6v|iZBxla^7%5e<%J||0XWZ)-Mx`RUvF5y-f3uk(f>=Z;Z2W#p z%!=%-S=25{3J_nV20Q$kByXRxRKt637n4Lxh5qwWAmz-Ij`O%q@}ZmKJ19zGetv5g z*9lRqqRT&d`vn1)UNgXYEElKXUPw3@2Mh54oiY~1Kj^n<5az{1lpXH@k(pc_J$mcj67WAFG4< z?RD=UH*tT9oayo2q7R|wDNb}@!uiJHBhI(LA$fuE7d{FdlF<}@9KZA@S$;?2G$TZS$-(4}Pn*>8IUkwZ|eI#$5f0zYxFmimA2 zO%@P%?_PWLOlk^+!Mh|a%y^@d^Z5}u{_Eo9<*kl^8Krk5L`S4_j_bf?SWatQ0%o?ZnjWg?{r*&pa5C@}tu*#@5E3 zv*)qK@J&&!!Q0PkjnsgAYG|#kE9MJb{`eJF`JBlKlb?Pqaa+AM(fla|h|i8N5P^F? zSjUSM=u8)zQEE+<#tqSM?G{2yD{;fFEe}x5LTGKC2#P7r^pJS;5CsN+xuikEB);&s zky;zok~dHGw4q<_)Uuys#M*mD~FzLLs+go+q#I{G)v6~}1 z{7A3Fys%(yjZkd>zDE;E8;8i6T?{_cA)n~KFc;s~FJ#d9I^}roy2ewdFw!VNZ&={E z5@G8Y)($kzykq&&Nx*kKiy}GVrBdWeP+&4$w1}9W&{{3J;sY-DCO*EhxGsiG&6=;g zsmp5dH3xH^!An?d5?of}4aPC3Y)1S6h`>{Wm$?S-gJVr_Rt-r6cBk95UGBlqCO$)? zux;!CbkT_}~upFD|J8ENUKhvJjYHl=}}md;c@{J8ok+Vq}A{qHJUW zc}ucqx^EX>wb_Necm`10ZGoL7c9JcotfBLfWxNvfK#&4#E1Nan0sR<`EURlm%--m! z@c2p_p=BFKoW&BVPa(y|mGwPCAOV*cql!K&;R#Kyo-u!;;UN9DRc7LTa;nfA6ruld z%*yTQ{`Ip5Bac4#LXjUx=H@AC8VU)*1J}ZL6Fpd+5IN|nG&_@^##X;e@fLBugW|5~ zK@wn?{$>iRF;QGVD>66^`I3G{u#|G}oz-yNN8EjgN-dY`33Iv^UbTq20HTFz~3sApd6KRi{IPL*|v8^H)PrO**U}Ts&YurdAwhoH{@ah9ev^4sU`EYY2dqn*$o4(yW+f|Y#zP*y1~I9SC9mq zO3z+D5!W8Z;*3P8Aibq{vzn?~Tq%TA5v0oe-Nq$fG9ls1t6?iU{O!DTe1GM{O#I-;HYPM;(mfFYuYGyyINhTsT6?M6m_-|xTd%5iFxL&VJlo9|-C%glJkdYvQqr(b_-i&@O zdEGd=5t%Rkyy>Bf!1VmlzLT6;>0<=stU8#%X8EPYwe~9g$1YbQvdQia9T35051+vp7dm=4N=6V^zs zeTQ9>&`Rw4wMD_4lE(8=$WRsdEN@U|@hm+fIp@8E9O6nnzTmER;Zv`#MSjXI@eH0d ztxS9u6g+L;F%XobD4a_~)XiM42cHJ%Gevag+5`hpB)Zd1^{GhIwwOlD##WB+*Zr#Z zeqHq_$3%K|0Xe<+w*_9GWs7F3XQIkJeEC@V3&SY%2C|d*vc8XOxGM}Pw^>L_kq{_v z!O(X+T2}qdlP=NgEyM9%nm>WbkPc>{lb#RxtZj%$Cuu4GWE(8$0MIv} zIwTT@WiC2IAg3fb2?t$}zGx0PQuwCxo>+0KB&dG8PxW1IWXP+GF7kXO?ME=>$0+qu z;^5M?^bqThlrbL`#*SJPAc=LyN0MqYe1<^SA^DVy-2ISm9`*Uvft885mzQ_%dXzYd zCI-N7lykDeUE^-W^CU5GKaseo9F2x>mUakR6FVwOOxAicNS66?DXKD_HEi~+su z;Y)hsQlliack&P&5f>#JiJ1V&u|A-+k4r}M@xe}`#ZOAi1)HWm&k5J>o?r6G!xq!; zc{qPEh*YZ^L*ai-Bnv^l&0TREZ#~&eWt%<4*&>oO2&s`Ow3U@y=XAy?#_xd}9L08+ zZru-MFde7J`{KYW5$dAk%PqvV*Yq!pE&_rJ*c>aeKS=WP^FK&2!kB^F`9 z1?leG1y)!(70F$4=~P6zSxR8(?p_*|ZgA;tkZue0y!_7jdh|X1#|2!^XJ($6d!Cr* z9%E!yzHmJ1+A+H{rZlo4mvnNxvFW3!>gbFPTZLMPA-fobRwg~Y_W0i^6n6wTt-JPv zd_o(!hVWXx>Ng5Y-Sv7RPM2}R?PH^uS%mvD>{NSz?X?o_sch|;wj>v~A=7C(W@Dw>r_)AzpInutL zgqof#6SAJeRo>cX$S5a8sDGp}|GY)lP*P($94xi$0`4MrAXwL(>Cu>hAfPa4fGX9K$fo7f=`vlT!~MHvX$~(Aq#+DnDPB zR^)11Vl%Z6+HZ%!#?1;+Ew2&S`TS9IP|(`~~Hm<)9T|^QOjy z=e<%(bM#t%ZRAwWStxo4Od$QN`T`~%L%mQaAJ5RGV0ZPT)UQXMz^D}52MawK{k}Kx zgSSaNLNJ6jaM@mk+!!GSYY1fu;)wnh^GOhxZd#D`z4gtLs@^cux)%!jkQjtpA*1CQ z_7bY<=_Rv!D)HuHFQoo{(2As$8&5AU_nrAkUN$Xk0|hL~r{?ZPUUb^VZCwr1jWj+` zbOX(YgWXMFkF1elG&IqTMtq#CM~|1rK6v%aIP#e&Yi(ibx}ob1tliFWm!d$i_{O2B zYISO3Ir5)z7z zx}Mj|8|F>G5$79%V$QMi;-4JA7r6TC$oRp4z%uq~as#HPd5MT0Zf?;mse>^fV04A~ zViub{{*8st{R};glEO^~F`?n52m5Wo#|vpBp@p3dDrEN6o1oswm-z%l?X>*HWJZJV#JF}NaW!lf_GnyF*279T-MwMVp)mltMa{l>u$sti2EYSe+4!G@1D zWOuprTWz)qnxjn`1biQz50*8Zru%Zc@X8r_Ej(AF^w)h{3^z{djsm#oXzlP?E}my6*^Eg5F~+ zfM5$Y2Oi}PAOsT1SR1LWCm!*MTBG5d4V1-NSWEQQSB5i272!2TuU($L2=TD)l0 zvU(0N3W06Qm(??$C#gjO;KqsqN%zR8jNu>w@`U)vgJra~))C(Wc`}Lxck`Q~*%(8D zYx=CxM+Ta4Q--Pb($Zt*-EyqS5AKB#c1ab?EOJ|GfC~EwdrhvoJ^Fz?Y89?twS6?Y zElp+vnI}hm5eviP!htizY(wt1^!t$obV|&8b(NsIHUuzZ%J^n~iLWm7ozvKWY+}J` zJ~petOC2wFCA>cXUlkHWXgGQ6W|FJvx>oY(C$` z;)LRSiOiYEu`WAngh(QhiB*Z@AEXXZJ(5>$zQyoGsn4l*~LQ?;TtY zIov)R8$!N-DrJ#ZCocH@#th;v&W*5J1QzTw{dVj;NWOue99uUDoRA) zJxj9oQyDUb^iY=oDW^NO)twyMeo;|Vct$}}qXVIPRYtW!A4357v2$*vZjDNh_@ja^ zb@UG02cMAXr>ovjFnD+C&I1)gjdm+^(9UUmgBmR8#IoC^E18VUyq3Tc)wc=EawgZ> zl^!TD;TgXwX+^G$Td}xS(bzFDSu^2CZ~{hZCPKL4hC3c|CDXkjXQ&=aPca z4Cjn!_(WonzmnF5KObL4y+@4x(;1PPaZwem*s(B;+-x9$|A4WoeZB}C@M%%pigFaP z?gaE!+{Zu*LLFsNPNW3O(W}&8=;CtnDLL4Ss`wOPTebPRGjs5O2h*XkUyj?i#o6y9Yn-~=& z0r-HpxcfSF+$LV&%U4$~T)4nC>vTcQb3>HM4eBur5MwhTPE+22XlQ`9#Zgp`t-3*I zv)CvO-*8JvBbd9G@uP9atT!hNWi7QAE;}99OeZtC&u|g{Nvw4d)X-nXvd&B|ZlQX* z_>=>$9LNsgk1jD#`FhpOtkvG5cAhqfq8nzOOi8(|4_OS?m8hAEc{j`%=9x@2DmC*? z&K#(edFL_*6x7b~-WAGY;G%vzZT&$gPb z4<;20Gj@#INziTL<#TF)$M%iUVx1AbcL;bRf&g%4WumvbGd)QCbL8-NopCy9xc!-RJD_>ubw85O_4;)z ziF|;fjz>hr$ce!kTl4#-BD`|Nyo86&Uo0rjnF`I%rjWjL0}_#qh()_LlS6X4+Qu|v z_v?d>#@GQGK;Wh$kZxle=k`W;4?csCd<~8z4`R926W!Oo-OlvoajdD zxefEm2hYaWJ4A*dJ(BBPv}aOZSd0Y~Re8;oVF6R2L%|nuK*9=rCb{=^#YJKPmZv7Kb3+BeRk|76BA0@{ z$eJ~IW}i`CP>y!p%B7|&_;E>BJR~p#oSF{Tp`UN6)>pY_$DEb3^A`?Za@;v>#oU|Y ztyLE+y%5T8@BK!xA^*_ixUR$-#{DVLC1&*In;MSVfHAYjH+p&1qeD6OqJn%LlS9c~ z$tZ6H`-sF1(2p(g7U~USyr-qoBxAaF8X86)r+E|Fx^y_L$hJ6r_9|RR14(9yCF7|2 z6tRgR-@FrDZm*g+ob9ST7*>`CM&~P!vh~su89lwg`AnZVZ}K2bJBBBCA{u>~JUsYK z*eZ-q0=n={8R3x>k_ZCDH!{jSf>b^JUOHJ!%4*1S010lo6_d@W?*Yvo#xd^~d$BA~ zv)IMY#LG|+M`Ohoo?;Xk1Z+je5?)MtCVTN(K-`!M@ybHd+^X_;{7gtHy|2z0rz+5} z3XlDkN@RNYBC(o>#Xwl-?u(?LCP5J1ryUwFr^d~#eBnMYX6b>v+yJ(-L{8f~mUy<5 z^az{7he<!Ey12t~;b(gub|>cyjR&_EDVd zQn?s7HJ$N&hl|cnrc_!>pGSlBNu05!v9MJ;(MY>|e`4<`DQfD)XYLF{md^klEI8}3 zvOQGJMyAinJwzBLx#(@(o_$-KttOQkWJpQ+f}1q>f|BrX_XYfJ+Ljho}u7n2q3*7@e^%D$>K8wo}xV&{m;gwYfV#nva$rMwNVbWM(Kg0}G_V0>4 zwcu2<>9?Pzr|C$@K8wc)FGV>h>@cuLOf&4!cRdM>e=zG3eU=In=(V5ZaJP8sdM6(c zb9c9-RbS6}HJUq)bH}k!-fYXXK1V#vAad3N`bY|AEK4Bc*q2|Qm}zY=>XgSi;dQxC zVT*i@IH4o@khk1(*Xi+r;MP(1U2feoJmr@}EJ+N=MomB zdL+Z8+#iSNiI)G3ALVC;@LFEDvkj|LRMX|%zBr1GCSiN+jbc3t&}2gHlZ3aAQHjK9 z8`t@Da6c+szV1N|ikEscH1_K!2N>VZO59d{x4h!viGA0rXGYoc%E_yu`%!xQZ2Sna zT+FrD&;8!=1b?c5!<)fZi05;35iE42Tc;a*$pSpkbTGRU@>_`(w`z6u$`7&z6Eh>$ z1@n8@3PJ;fTm^?}_@be9u)s#4or97S(s-#}H$$|=a4vwg0snD_tbG`F4cFJl!cc0yVp_<>hwrLS&c_XxsI`h$d&s-ry=L>E1SBiT^#poJ5`1r@L^ZxJ zjhU$b3rAgmCWg-0%)*&`$Md#hk-yltw=m6q&2eR|f_b4aZ0OdR7@r!Run60_Kh?Yp zM?&G-V*PuJtj$Fu+FPDA7PjOqOmTx=!y?zJG$_EjLMjt3;P@_nucO9YeV>ekn;Ltv zV1vi+G_T4ySWob#Oj6fgk(8L>AG*jIRxh4;x5-syjml5I0&^K%{)Gb&rnytF3b+@^ z5u(ipIrWgg{qEO$PI>d@oO~Mf{RuhcHqh;>cctrQ<1Y%D zGsy14ZO3viuPRT)RZWBL&$*oFCUrA%eJr30pUl4uSkm7dh6PLrVebaqfPS}qutqT- zE{BEHp$H^<{4?w7KWKh0l%{0(-&afODR9|0gjkd$;{u4wYuc`Qprn?%C$$kt*KMy= zR{jsM)ILAsv|w4uCM$j{G^SOV@T+J(Z>q8L{85X1sRO|lc|fT**`ua_kTlb^j5 zg_f^NVW+c|Kcc^B{}iL(69*ycpc=#4K_k5Q?07NR`Bdz7Cu)vQ0XN zUp1>B8r2ghE7bgQl39>sWe9rkt^LKbk^hcXiD~SxA9(o8H6$HE$nZARoEceQtmzPg zA9|2N2q!MoZAJ8KplJ(OZ|Q>m&}@9MTvN{>wjj5jX6uQHJ#ez9PA7|U;{ssSW^%ww z=Fpf=HgDEf_>dIZK$?hGtlHmm)z?#uHp%@t739PREo77X!9 z=Wht+P@qGuZVVWQkbm)!)v!rFw^r`xxuP*>qd9%}#~7B@)jnGtx1j9|20$DoSQ_|n zqWx$F4#Wq%ny}X%%I3&hzHQDYZyxtLlD;m!$#fa<%PVUCK8Ma;EoG^c5Oq?pGUU?& zW)40yPgqJ!B=-sKlvY&QbCx4^h)e(DvtBld|O0`d9SLLy$u?7nz)pO|1ivl zZ$_5&^_@yvX%_mZ>|whSR!;qCGE3R2)hlw9y1Q|&g6=d>WE_U-F%nX#tb9KEZI%6)Nx_z4xC$c;D>ez*+6usMbdODcq; zUusv4e-x)EGpb7dgrT3J86G~+X$c{nOe~cU<>9<*``rBNVCyv9WCCdx`Q4(YPq*o& z3Klzb8$ckLcp`Dj%V(wtM0muAnwH`|yPp{O%f?>MlVYo+c!#)wpE&ie(_@V9mAXxd zA8*gB3fo7J^|CFF5*km#*Pw3l}ZdimRs2fuuM~dFhF9zU6AD%Tp z47}}Ap#O!0@(>cI;Ub=}oMp@MyY^+x0+@lMqyc(4udJXuMks-}FRs|_=PCEX z_D7w*f7Gdw>x_P+wdt=_1Y`z_h`$ODIxLBP9%X(p@&ceSsJ#>NUzN3@?}Uaq8oQ7c zJJeG&jzhP>x^>ZO;$cSx6|>n9>p*8(jXe!LPPGS95U()2z*i`bQz*ajQVulw`1lm9 zo$SK)6KSIVr?yW1nn=|_I7Xdxt#Pe0sxK0iHRi_TeTK;*{yczan_Ef=7z!Pmi*SBT zVQf|w7--#RfR`>xBmHQYD#x{}RU&7uZPL$nLGi!iYVU=!%+Fxvj4`==`ffz77p~c| zV(saDj5kp8=I-;6`ptD`i}D7?6Eaur?%e;& z?+Yb=0lnFzcGu<0ff7GQcs;L`Rm|BLjGVB}t~~Q&Gw!8>bRuA<$mZIp5P1XdFr80g zJj9zxJg?~fE1gcRw>bG{Q;X@RvN412`?M7)ACi|bKAmvlO&<~Nw%F55QkGN~Cc1d| z@qfuL_h%r%LGB`PU)GEhMERGt0q)PH`0j#19~>A&8QK@;qKcS!JwS-xb@e#SSq68V z9yZiQRsJL!^@q4Vhp%WUjMk+-_NeKPbXl%o!D5hD60NgI2O^Ae^OH1$%@sq#Jc>aM zN`wtegx{R@2l^N!7o5G-`@;hYt1E8aMY1)sek_X3&1zf^cu<#2nn1ZDs=3_XqeOh( z8}5lDx7Q6>k$omOyCaEGP?@XCN2nN*yWQQyi!1v2{ffd(v9U>UHslW}Cr%QZr*DPW z?bb7M>3e--Od$Cf#Hyq%J%}pvt~THp5`Cbm#wRGY+d(b?7NLL+L5cag;{PzAIrlqz z%<(k&je-U#SK)h>9p(wndPB^H6yQD)R&m!I?L&HcgQ~u!u6WSYdj72^QrBnXusJu3 z45V~7Jnj@*WkklM{vq1%E@Y5q{HW$1m)v zMWp~K2qUCgBK})L8fI2b6<0Lnoy#hfJyix86IS7$rYu{q5K0S-efB7aL8G6m|B8b> zHEA;KXE7vH_UHfo3pG_WhADA~I*@zck+K(eiE-k?eEU3*yoOkaj06cJ;GJCnbJgYh zIlqu)Cy}$cs(B9VjaY)y1Z|uOKW%U7;P~<{mp&;D|HbRVs@W^1y*+vNnKPZagjwW) z;j5?XjsB0WiC&g=H!tFQ6!VmF8|udm+THz*_TM}QTQ+o% zJX{$B62O~?nXgua9R8e(!Qu=_>rz@kzen?6RS1CXicmrc*?wc2(s^!(L-?WM_y3Oa z_+dG}TOy}Q!z0~JTm`EOJdxcba+H!w)F+z50zUhXMGXbUk}W{wIl_i{^jfODx?nAWgEYJC!(Lt{Y<;a_>T~G@0ZcruRY3eo);UigGl(z?pI^59LNOtD|ne%NQAN+n4VUJ6A#G zAKC$SV`ZvjMgBmP|8$v%e=nr!od3jNSKGE=R;LHWmkRn*TlRfk#eD|E)C1&lSDUd! za^9GNcEd)*(nYg`a!yUg_%-eV!+IAzHXCldLSHyU>C)44%2_%07~3p5{S!O?T~TBP zCf-;{luxq>eSKTm2vun5JMomBgG=|?NbKFHT_|C#9P-*_E%T(B{YIA!+Aeq^U(8R& zKW>k`qx^Mre4^IIz#k^kEtjls#Eei;Td(p8X@@C^tHhz^$P)sf*JuE-QsF|+c>Cmk zs{L=I)U5jWipJ(G>N$kwH9g|f*fFr;V#asiG0*V77O(dr*ZtCW?^^;3?M&nzM^cr(>qOsf9RO>E`(HpQnI>2CX4sYIW36WJA|i+eJK|j@B4mF?{5r6dAev zU+Zg?))&lWa2ONtGj1d=*vlxvBJ|k_RfyR9c+*RJ?>epRSFfoMOJM+sd4yz%3|++{ zBO2g&8NSvwuwNv^Nm;v>1LI~x%3ovblsQjOM0n~ zqw+K8*6{eR3(!z?P~(HSc-qHp?=-IGp-J=a7UDR>N0rj#7OJB|{;;02j%}-x`k`+G z-YqR&Rr|Mb^iAcx;SF|)9DAqE(0RAwoa4>rn@!*M7g$4h)>!#vHGlNYEkZQVzLtPQ z`g43i=La|boZos!OYYeYj5v{=#Vglj=ZJZW79N8Rih4E*sDDerK&n2I(SUXDUWok%Q9&SxuxR3%<( zc#W!|&!E)uppBx9hr}y47T3$wqRy9=3P}FYc4#{-PDp;JCW#i=4Z6dg`xG4*qj5#c zMYsAoz8u_aQc>K{ZCrilWgmAk{aeiu0&Faiv&SB`^on_BW%Q4Da;%g95^eV`sFJ_C25ow`u6?wY*Y0i$k5LHn*F!PV);q zZV1*2;Dkg6pf118&@{-}lIbwWsuhgWIB?FJW1U|w18&WlMQa!qv4a2E^Q7IRsS5?L0E&XFW}H!N05^T(Km_1}QcLQA94J!n8@ zY^Xh?_EcUuPTVG!xb;okoB0mVM6^I%e(6Kals`1F@BAjkbB8_YJfSuJ!!JKb*KZI5J#$CegN9d9+14A7tlJ7ZTaxy}07L~%AADwUH1vj}oJszG#P z>(0qM=N%;)07wR<^@X=6^5A^(v8%rO>){!bYUPLBaRjBXn1n-mnB3$ZGt>IC9*ao5 z)xT=~9JS;V!CairnyqF1WP&i;c{YMU&ZS+_b68^As4*loYnH_z+h|?X$v#19q}_v~ z6imdaNhHFwjf+(3h)Oa21N1Vxd_}FF_s>z13l@Jkn*+k9h!F8O>kVy3TTnfv| zQ09NbT(>f)k^Y3TDVYV5%GOeb7vT~f9dvt@%a{K4iU%nlzkahxTtNd3GMG_>Ol zV78y@bJ$-vg1SEiRP6kE{ye|(E4-ZNj!}zxSc5@*FZy(=IR8u=23rbD57p?^T8F{9 z4cb7$MLRSV|Fw6yHk~#_o0B)cI@pX~TpZzxnpgMF2U+7;<2==&IpDQhqtRHIPf2Ca zvJ7mcC}#!}VR2YraNN!$3Wx8zK4<6#3bFosDz0v3`n1-P>Qi||8PIzP*(6c3GctP$ z?d+6lE=2BUdU1jq{^9T6`Ty?)VOvMq>@#)q zlwph9m&OaE% zTsS9!tjEv$OzI2_9E{XsJm59sl;~J-txPM%*=C0zF^|h8AOBJ_SbI)8*u&^Jb^hDy zUCSLV`IxenN;b*sNx5~nI82iY1lHBd8|JYH zTa8K+;6l-X>*nX5v7c*a73=$8=EW1#qD3(JF<6=T+YRmeDc^GU-+##*kQ#;vToinv zov%DlbhzeDyXej+@9A@uPtgf0M{f)j>TL}ZBQ3Vdelx4uAPfeUdiVD`CAEj&WFNeD z@RpEN@p7i~m7tEa8AT}x-;_Sk*>Zo}hq&r>qm4Fa;C%DrN>6n)WN^ z>WsEPJX8j8`RXFq24c%4i65h6i`JH(RWdXxH%p6AE)}; zP6Zs(x=KmCF7?@Z^%Y`qlulk?vsnXVXSrxc_4FGY57)fpy&P_+qJsNv&uW9)e6yyt z{$K$5HVS-L^5&QB6fk$jTwC&gUkyp~e^H(-z5Nl|&rbhc3hM1S6>ZVV?)f9;go3V+ zwa(o#@wEQKWHo~ma=N3!pfP{XGP}{&f3MoALF{g~=LSfcz?>1LjWf@ROy3dyKa~ak zwwpXe-3iec61lc0AL=l%Za0{kQrn3dnE0ad)}9k9z;qos5g9`i5$;)mzqysyS+DX^ zNGh;LBb92NKv^NXT_49!(qpOi$uGG%Dq(8a{`I}C5xR5)`gDU^tJuI#ktqH?yj%*@AC~1B!cp$z(jz4NAK;-;# z{7%DDpofZvo?{f?%@?704Xn;!Z|6A1 zXT8+w1y{y)uf`^N#qNd^7lZDb|1)OpnRJ3*Dvh&a{Nv7VCdA9MBWY3jT2JaE3ZH?< zz2u2>+qh_fk1opag=ja6^hyrC<3jxjlHhMuTpqD3U8%|4;UH;f>5jy7!& z$;RZLSjIX4CGkHgyK0m4?+nRV4KA2?)5~8~;6Hm5&Zr9C$oCF#6-uyklHx|Z%FNaO z?vmM0`RQ+Es@nUHd_=c*wwj_-My`6T<`XQoSVvfPR@F+j>$XqQSEuWbCjzz$zKdyz zd)#fhGjF??X>>e>q1fifpNdg9pDAR&xiuI6TLdTLeGz7WcT~R+rSrMGi!rLT=ava7 z2S=~Zav4TL63WGoqN*1)AGZZ9D0VC?ysian8dazeCt8?00?h5gO@7VGIG$dmP)QH< zbLK?X`pytQd>R+~zP)KL-IX=9>Yt0O4rAcM8c}vO#B~78(!WjEASV}yxz;qyQu>=e!xg!2 zk2v^7&##HB);Djqsb(}<(PogCiMhgw#tT=?89vXob*;JIBcpf5jRr^2_ka&*jmGl| zb)h7HEAtrot~RO>N^(5vRkx7M?P@!a0^bwtmUeym`5 z2Czlk4_qISaVpPAK4&)2tptjQGM!5c2R--|IfzE~{?}IxS)NSqDzYv4CY$tRJR7J+ zud@q2{O4Hs?`W6|OOHRLy=uEP$$qaF^1i=CvepCSw2rB(lS>AXzk25h#3o+-0&PA;nc zjb{G@poVv?UyrXr7s@kQ&DsKx^=QD5kdvq%2cIizR|&bO{rUOsLDGKL7HP;WFCzOF z_;3wou(Zg7tlv!WK3P=MVrjjvRrdO%SHpsxQ17!AhHjAAj>K4%k^l+d>C?FMkqfrb z2sA9$r*ClTc<@k_8+7Or;q@+vqv+*g)m-h>WAI;TM{qI`#$ejmyy{V*g~Fq{-q?_He~NMiAT6}K|AVrjyXO=yyqD6> zO)K61DbTO%G&aLDO2rY{o85z)?iGyxWiPl&gsy79?_@zQ{!T8`jQvM}>Av5MC#~=G zI3v*ghHcFQAo@TX=jBqO$#m}|ok)*6Cwk^cx;r%+9!x+@yo!lI;0qPGcHpUR-j4y* zU(r5gd{?X-UU6@Z;Y%RDglb@eg}PJubn1&F2z2v4-io#8Nl8^D%KnJV(I~QW>&y#Z z)|Aeg5tpW0>o+k2*mZpz-yNPv4(5+2^eWvd{Zw9@br{~g)XZRsTJe!D{m+$mv&JPcZHcXfY z6Q{%P2vw6c11ifi=do}8$H19eX<2q2Hz7Wp?&hppcC)tyvGp{HHQZ(?(}h_1-A56 z`N@1ifwxIm>0B8u5SuTase$*a?@Vwbu0E zF=}CNC#!S0aM6lfr|B#CCkCdX&6jsHbxvILNVKh=J)k>@Xc6)E&kTx+mSMH8)}>nq z34O3J7UcHj7!0F1{S-S#A+Y;CYT_=&tfWLt}h!%aU#L4O!CE+&uu`cHYZ&!Wr~;iuhml%V(ZUg%`fN zby)ss!D|wM?4~R>1)$@hAmPxVIW)$EnbVl%lO%nEm#w|$JEoNlvd93S<1ftDjVu;8 zAOWGlQ~;yIwn3lLx*;TY=m!)mk0Sl0DY5L^k;Ta!V^~TJcl)J0Pj{n8$Su2;K`n?< zKi26*3uf(i2nW*c4hHm+F5X$&DmISoG$XLb1k~He!N_m;d}KhV-kB`*e$bmj12a>z z6WW8j$OThSB(mv?2S{*c{t^hnpVr!v`eeuT`2s~6VlL#!sevjjKIHoKdi<+i8RsMO zN7~%qaprb`)oERBk41>u5|cO6VTgntwPHTa0fr zwnUut{$&>VHSvtUaA>=KOOU)uwZ60?>}&I&-DyTT32Bylp`)BEn#hT9$xTC1b4|$& zbSCWu^&;PoG#xB`KXkyj2R80dlt<;11@6h@Vj^3ds*(N$$UFJHm{FS z&k@l<64BITuPVWSs9s~;=kr~hudLd-RPkN7vk8oAwutD6Ch`K;FMDZ34|je~ZqFj( zIj0Ik8j~ovI54Il+eT%Ce)@fdXie2rIf%{%IxjE^Bkr~o`wMOa5Pj)R_VnvBwWjS6 zi6ydEH}omljpM4Sawvka{2eir>@lOD<`3bmle1P8P?0LMzR&r-$VzT34HFLy-R&sM z#gx&nnJwUGss&rD`Pdilb>5!aESN@G-T~IBcOE3fmW8;w0_dY8NlJ=OD#?VqT$seV z7(0eUY=VOayRoo<970W;?SZRP&QI7Ezqwo!jacsbi`*yQU0(FNNut(|EGAVpU7W|G z;FE=`fI>+Uwdp+f$39EF(vJmO&qtizd`JAV!!oQU)C)fvRX?u z>8)wZ>kD>M)N|w&C-ZGd2)^u764Io*oBBC}&B7*rhVm$t<)g-t_^Ic3u5P4TZzV;{ zuL&s*5PQxJJN)7EHPKcbzNT_K+_a;+2~NJohC!2j0LudWv-1uG>FKf3zieI#K6M$N zNJ?vrE;m-_62WY3$RR&5i5c6fMG@%glny|NRV2S0mpC0BU=D`B;igg2tC(b0pp+D9 zP9E59)XUW^7d;%At^9jA`Omgohaw$iSd)PC#r+FsCg(4l$}f}iCfijrN4kf~{L27= zXscG|a}nxsy6t^0FGx6}vAe^0}d69yB4H?M(Y) z?dAAJIV-O2mtHr8-g_Okp`co73*z~MxG$tvt*Sogg13N*0H6Wj8@=msS>-Q4)uON! zl1T5yZ)s|pp-XW{{h%MahwTayFADJBxOdfKPNXto8_Cu_O=VR%G!HZkf4Nl&*n5jh zM{oW-;kT*DXO5Seb0C?kKw5WcsD+NOO_1~P5qn%{&4-X0Ub;|o38wc^E2yf~tmRW1 zfx+w$&Ag|SPYpz(^T&9Y*|v9k-))`Ip=AD)!fuJ6IxHMempZ zI7i<29XbVylKbezRjEuRz-Q8NStXYxEq=@Mf)?r(cXAp(a;CaX8k*|7n};_5t;{yE zCE93Xm(pY8l&;o;0s)J@q04t#tQ+SSP}4SiDac~_e4$kt@F*%PyK3Gc9j^Dwz-pS# zc60iTr=TVwVxb$KA4zb8Yh#?_dWtTV$ z`hDIiR}5~RytRU9p?4jDn)u4^`_;?j-MsD%KOVX2bJS{LuL(lsRLI89-nTXNhZ)O- zXvdFNK9>q_BKl6lpOVHZnf+li43MiaL4e&7|9U${N6*``S*`E3s@Jwz4`eX=93M&L3^k%USIdZiBD8+)v6U18-k#^3Ax}+F*ekH(>cLub6v@TzP!SacJni z4qvfPYXjeJO!Vc5Iz`0}<PXsSWNp=rU(7` z8;_1)h(|-OSsMwk&K-u$Ef5)flogQEo_F`#j)dM?lch6#i0)vR4>|}BL3E0YZqafn zptYTLhbSsLq^>6>l`PJPXY*gVobmOy*!9U~` zNO+2aPslK4llen_lV35HeVg|?6-fNV)BQ)@n#<;-;I>f@Mq0NeVADK^U?Mc-$tFE6 z!9BdI&MUhmA`a%Lba+EH2$M$K#LTB*AP0Usa2C03=T-nas#DgcbNX7-TI?(& zQn($@&^~#Zaz|(|AG310o?{KP@#B~x&m(t|S;ph|3#Z;vRCBUim}hz4%Ql!BYRo1- zhGmEx^Xdjo1l|~ts482DBz4htz|2xL0j)HYbp6+;<~qjeuF4AN$c#kzvy6>fv$r-`UD1c-cAoizseyjq8sMU zHWHmOea!*%`08AQiSZdkW(As0_cABRyASM@YVo=_7QuFe>>%DAt*zA8Siym=0E5Jrji z;oif`m%&|~N?O?e2&M!2_Sh6zG^Hm0YM4#G1Rg%So`7@t5d-e<#p zQx*IM$ZmHB{ScZeIU|V6CRtHBM&x3WkvGjOg&1Hf`2O5zjWo&3oiC$tYAv~Rz+i98 z<61Z^D*9aYFsQ?+5zd_hYX2OYEJwx%iPd-VSck}t27jLnC@+f$%9!jFE~2`uD=q!R zH#Hb-Aa6)rRBph|DblmV?%l1UED33ga$6dGR;_>l4STRVa(Af(=)-Uy7Q-h3ImoU^ zBAB9EtkCnHZ_p1y(n!>(&Fs}XQxXVPirH+e@erIGV9}d)448bqS&S63S!qJ?Df7a7 zaFTI2tF7utHZ3f#QUqsJbX5K8_&Kt>ZVu#3b=jBh=-=2Mm?Y$O`reWUICOZOM9so6 zx}G>p>C?*Sad&3OQ8v{%_wM9-%>_3Yh0*>&F(K!o$YdcdP2a+KnVUm-yok3n!AC0Y zQ>M|^($cPS+(7stoO41Tx3}vnrVONoE{Q8!IV4W>Nwo9&q3ZsH4>dx~3~D@;3*k>9 z9H=63(~asla}2Tlr}T6;MI;MlmJJMS+<3;I-lbf7kwGJah-lLzv} z%xvAn8gE@ERzeZWj(R{c-gPC}3<3ShH2}9>p`O5==6_=2QvpJ>D_>q6MmVMhPnfBw zcDB+Vyf5uVBva9i(U}@}oim$t6%&fR6K5Y_)l-Xv^+`y5>*t$Om^TMUW(A3z_l(Vj zjR=46C4M<`Y9-x0$3#S<^bH@y;)kV>@mQZ40@h~)Oxx3@KU>%q zN`0WVX=s0#;?$6Q+Zr||ixkt@s0&2D+ym#0{IAklN3r}%^6B^5lQY2k;q&E~;F zQi>dkxTtq01w^YeJ*(8JSTg*X@YJd`Wx+ZCeWWX3-F&ZvUeuMU8fj>PEL;pXw6xi! zEM~8Ce>?6e0lvF@PnhtXDyl$_%0V->Zk^CneB=?qg&vKiLyRd3i-Z_|A4raPNN1ij zOOg`y3RDkNx5VF?z!3xQwC81{S?8+^hvPLR@zXe`MIa5w^2A`qO7Q`*xOnuRJ!mia zR_#(a=V|SVNjfbZ+8)R_LedRKRB*-b_5_F8+bXYIYuTF=@G!*a_I?L$zTGp{$FSM1npYTqByhR^yuJ>OnAW2}vs z8-3rdJ0lxpC_t}Lkx-l`F@Noeog-)qEu6e%lE$lPB~7Eb@&J$8 zAI#$Cqy~_zvD&U-@9`F~t?#E1;gaYV6?Zw;evQ`7@n`C(KhzFMGWM*w}xQ zQdbP1#+iI-M#PB^yJOM$q@s$bOt@EaXS5olLSByB7Cw1M!J7!3w=e=aW-1ma4t*#({fmfX(2-0v>@)phw(m$5pK~^W z51f796FMu1YwEfEQp3!Pd&>j>$2OsAE9dET>>V7Fod{Vys}Ghv0EhCS^mAn{Xx>0E z(Y?YCG;_iO&!)j&3~X<@7lG-Rh4g?OySNhhmq((sT-7hIby6Km^UlD z#Xw!@@A5^~KdB>`4!obiDwf*=m#C^{%u)3RvA!fbS4gPVRi z5i2H!1Hw&6Y`gdNN~s@zfA#eGabdT+;v2?-J;;(w^T?EvQaVUZjAA(B__Lp2qA>pI zOuh|*3LQF#gP^HSHTyRJlpFr{UPbA!xOCuHPm&Qc|lyMn*i$c!8Dw+Z4{ z%Ci=x%vZa2TJ#$e!}MbsqSlol2Dvc@0Y<>~4W`ATx@3_DXZN1nmU`|jW3WL&`0;Yt zmw2=2oX^v#@1?y~;@Gkwmt$j)FLy8ovot22nNw#Gy6#2a6Ul~{-SG0@@AcROF5a9dyJD5qO@sHqoul;gTXBh3{!ZJ|nhMM*z> z0l*oGo_%c;-XsTv^7!fGmkL~YL&lfTLuv!H;Cp|}X$ z^ga?j``Yl`KYJ(LLu0{GwS&a6q0QH%*JYsgv%Jl9&}e-j1iYLt}WCAU7{ zYMPeu?uzB3Da=s6OvwH8Yw~&R(J&GGAxujeV)fn=IpW_vIlxNXT!Ab}&lK2sAh^92 zUb0m+x73n|l@!Hr-%(N21jJk#axYXaoG#UV?;)IFCUJh|{p*7T=~^&#eFx=>T1CX8 z`_(uKVmZALJ3W&f_|L7FX76s^j3Y85CedDr!@JoQG)nB;u`EkNXZ| zjk;NqNrtm#0X35c-#|X)C(8a=&`Lc*` zw{nlLP(m+NHVi4n0sa2j<4#yxolZkvePgF`iqabrbZm6_#9u^bHq~`OS;bbTJHh({ zi7PT816$9*VH92q`L9C9jU3CEQQ@vOxK@?030QiWqyUm074=o@k?BFJc)V+4cj6F3 z$c*j~<2Vs^U23$&+0F3_6s$eQpIvVbaLTx? zTyos17}X)!T7cC7m<-V)$)tFlV_B@;F@NgEe#|K=|vQv_XvH-H|*P}Y5~+5CtD^5 zIuyPAY0@IPs^qaxM3|eVa7WTD3TBlOhtDw|;fC|AKJGP3YHIj&AgCV@ORF8rN6XWO z=!mC#Yb4#eAtV#NF+=7e$Z;!mcl5{{V|9SY1H&0QF$rQ|{VH0llv5zz{K{6q zd!{+6Xt6m~n(mV>U6uJV2(wNWn3acF5J0lb-gv(h^1Uv+GCf%MWZdEv%S{NSs|iBj zrm1?E@ekEUw<68+rs3X?WWqha7g>wy_J5Y`5zcOIJfx5ljsFmB={8lb9S(FIw{9#f zAU0y!5s|1)>Qd*c=Hr9qGY6BSk{+i8>{R3#-0IKScY80C^h~f`oVJG zm|$fY(hJ>jZpjd!6CI?j1b7Y@m;`9vVVp}&!C zzMYvq>Pbk@>lz;(=pG9kcf{wkH&m}cP~Dkddn<2Z1tEbR097yv5i$F80_g2i$u%|# z3Q9d_{D5tJ6;oHusjm$Dh6as_u!fk4pYRQ`4GPE=5{`Mr)6L4nj3+A%*RD-FaczK4 zKDo>!CL>H8vqjLf2H-9dB2-l7nPI^>L*kB?XXf?Wq7<$7r?@a~96~IF(k%K*Ui{;F z6Q%dp6Vq@h`cIz8_6L`XS8hU)5+!z@kp>w}&r~&p!Z~Nh{UZ6+vRYHOMDEQyKe=oP z0uNn#L9{&((ZA+i_08z>*|C_HCmquYS7HjXnYM&%IiUAZq4DL{Iq#>0Ax@>hu*NDZ z<_0@rG>EI}uuE^kNfoxZm2d+#wXC|z1eu2OIZ*-nu278l*)r~ithU(m4SjL(a1@tD zWyuFQtP~1SGP8q#?W4H$1-0!XP@!P)?=)Q2>|jS7Np=vkG-T+8O26rDQ92I zCdvP$AddGvBL+la%CUyYZe_F-fVw<-T)+M3{aHmM27c{MO+BsagaW~-%Nxk+%Yx4>zo4J)*L zrCCYiyDd=T<2s?v<@4>a5H@cunU>R)#+E_^B~L-;E;Fmo#v;E7%Mzm*YJa&k4xho* z`Ej4Aaf6RHu95!Q;8S4D0h3?iEOkm`3b!nwMnTJ^8>Fivf$@TC+L&=^r*uACdw5<{-k=U!G5rZlF?2}{lA9ioF!T;_mt3LBrAqS+bnC3=)pdGVbC``F(;_72%9Rl! zes*7}(g)g7A?W6>Q(teS2)CQN8M5nw4Dde~YgchK6IABcSiU*bPP}+E>g6fCY29tu zK2xj290Aj`HmBCq(<33!t-V8g-EJqVd0w%1l+$CM&>HiYJ;}|nQ`yB}N}1Xo07e(E ztIVfU`5r~#n)3)Xmb8n-ME*cgd$H1=WxY5(V}=ehohj^PcfMp+1oh z@D6ZL8DHb}>m$$`wMSeQjYB_IbfH5t-X$VZ*8|rIcC?(lEOC(ROKWygZNm~;*`O1De313GA2 zaLs4z5KB*+9PDy=Z`==N8lV?-RzWnl!-Er-Ph&=?TW)}+jdal((l1R9ENQuU7Ycw4 zqQjaS;}m6`O>KAxRV_^}KI%+g;K!4?IW7L*?F2K0scoe{lB(Fta-g?DzS9`Cgz=lc zL-Krl#p*yvEzPK?)7HAbA#Q^`zWr`4MNl6olSX0m zk#~OW!_6W{ZT-k@J*IfdkbkaF^i1^W9XEQ$n-YfkoNPi3Em)h$`qv21LC~B|47!AJXjj4Rt!EpSsXdW|pHk|UJwCnWvRrviG{U4}3S)Co*2*_K zKOMQ3Cc)UQjBWY#VYO7qXIE-NC6bi9xRh_Q`ny~^8z~2(tEC+>5F&Djpl?c}QAx?V zyAN+#Ev=Q}BXRYntl?8{J!OYwHFvNXY*i#1VEXhE{)gLyh8L1=YpTp(hOgI7JPqO< z@DH6LW#+7_hLNp?crFb+Y330m(GNA*HlM|5Atnz%%`>yK7A%F2q6WM*YA2o7$@8J4 z-y+5RE~hrywc6yUg8MV>?EotLTnXA6yeoe@7cD{}OOoa*9n)`?6t@6L6~<F-D4Bfk!utEVE%9vgIsXmdIs__(qvhym?l3jKr=Qe1S5>hrrp z!bwI?>%w)aMVEsG@AC7c_|q_^+wFeC$?LFqL@xgH#eZ0DTBGb>+NfuXpU!UA+$mJ$ z!V2V?z;z+$b*qb&;lhV}4)Vmjt2oo|e)_md%sVYl zvzY5az)k%(CA^zm`+iSur|iux&XV!f;*Qi1p%KeiI!Lv=q&3iFooJe&p6$ElyGQhz_I^Ly zYnKzBxZ|X60wpoC>1$VnC5C6-5qeq(eAxQseiGz(JbBzOb5@pKgWImc;HfphiO_lV z6Qqx=1flHU?=G+#ClmU;(E$5G=rjr`SP2WY-n&=`VOt_oJ-4}*~Gdh2%kw- zh#~Qj>Sp<uhyKmovPzWnjYJ4c zFoPpvV9YBDu4T$!&7S5rMj0UqxIvG^I+tsJ6*{)_q{g?6UU6%sJWx5c8yi8(M0&$T zKi*WfQ+WEZecJtqP=w-+Z?LW~*+yojM|w$vQCq%cISb@qdJQg~N3Xqiib`S%!yTDA zoQ(>g6PpyC`i0L04s~>v&u8tH8y*M3$0Tz@$KAx!lf$x4`AGBWH8hOD3V<&pR2h{T zwf39E{Isy0=j!*`yhRvnS^Q;VF*a{yx!az)vzQ^y;U~u&+&I9BDp>%yZPgVL}?In<5hGm zaH;!|Fl3sD_dc3<{72u(BfV-n%Wsb(B+C!g5^Ib-hib#{cw)f7e(I0`-lu@kYONIg z0o*mc2IJj(wtXEdN3SVvD2nBK`^=0I4j6J#BnPnm*n|nIGH&|{xzsB;sqmZQIv`l7 z8`Yfh^Ez%J2bnt=_33bsP~o04;{=Y{IK!a2RYiqPgh!XYx@pJoRB7YOO~7=)aKKod zwZUWxFEXd^cfC&DSeg|%oVq*tc43Ega+j8$tT#aV6YJ48tkkuw!5@hqHuBP7&bzQX za8c3|7F0@vw}LMtkCFm;hl?lXU5(81+Ps9x3$mh0Y-(Z#7~d85%s&yl-aI!8U!XQ>^u>*5llw!W!OqY11B+C}5QB z$GB_3Dt$v^KlkD7N@Lwaw8<9QZkOCKyu!+|aYVq41Y@1YpOs_Kd@KzoXTIf0FIuFqv z54eBzj^}k8o1IH|+PuI2uwrc??|!8C4aSO&CT(%iwsk5%5(T+Dr{&KLt&kQ7J6uRJ zTq#0nc+gl*mH6)O56Ox@NxL=awl``oK=!D^u{BB-Mnnx;c)9x*kudaYpO52f>(4Du zpZ?RJ30pngy4(dr(y`Q)fz+g_IaFczu_A=r+0qC)a)tV^1m7j{REq z-JZ!5_L(aWX|Om#kBZs%p|rd|7Ph&JmrPm-4jJjOgDTZKf`?AqxufG&W|Fj0l5s+=-8yY`oD-ZoTtY;sAPUl$K&MM#%%$5VdX zooAmltSKld-gZ*n)Fu8DKku7*@LRv{rJPcilX&nn;AN@3DNV4T&i5Hh9@mF(<=e7@ zPxT3jI7tb6af@IBd6(w^eun-PyQXyOw+#>e z{shjYFT3aHm>rj-JB&2S5;l*!&+qFyGGt33J2~G~`8nDnQcO1}lo>n#`Sytd0wrc9 z@RnnlMzgQ|UbNq@9E&;nude7L@TnQ+)p0E_9sFfqn78c)?vpFKE?)c}*~y zM{epHCO)IxK~_m(the&F6u%8o;@htq_KfBE@4Q~btvzI|7N>Y_nuwtVyJksXidiqc zBs36U4X^=K9fz!x*?RIIXqBKwlLvd%mLUED9aq(>K~bOoE7tJYL&{38m?^F&*&>@u zU~gIp>4KoZoD3iLVDsxyV^4zYa{d@I`ipJhbbRt#N;HQ?)N;s#E0`HF(2pfX)74D& zFB=6-h&X=`rsdR1ov~$@nzaZP=6v(TAE6kzod1VF-EA#5V9R!;(~B}odN2`yVs%^v zrCQKDTWL2r2hk{~Mzf^uZbzhJ`e6tSyIt)Tgg`EpOdNkW@T~Lq!i=Ijoac_!9W05* zX*YDZhK?(Ec9zua%Tc!ULDH+i50B>iEhB?R3CHOuw!2U!W38 zviMrTh$Uf*{M%X_|DzGoezQrLit&*exG(kiDfo3M00t_y~o(6d1sKv;UR7 zP2^x^T*H4Mz}^7^9Hb<1cq2~1dp9wdUxTB9OugiV;r%rIcU(0?2Gs=8br0S{M*a7_ zz^aXeKj!kJZgSe6X|Pt^$IbqVtGFFhaa{p=4`r6^fAorqd%%92Wxe`0ABCe?)R*y+&TH&L%vmmrYtsSkrjfJcO?TL zujy!(ANq-Ls(m$m@`sDq8NLJ%;S%3TP4ByPs$C&Ajn5#G( z!K;w+SAY}jo6RrOG>ur(Uq%&tW5$!XEwgKCj7PI5sp*s~!-ueGrml7}bbshwCaHzv zcrFjNYa}or3k1K3P#W)}oUsEj%W|AQdoL7($=kO`K{VWp;AgHgx?33^*Jvp>Z^_(u z=RLst%Gv+o@W0lHsc(y>2dM!1`mHcRgq(yZ_G2yuw()7<3f0Bc)|(a&)-;F;763$6 z$d~>^nK^|~j|Xr3SC_*9EjVzU(i@xlwpuy9=E;HJ8rF|pdLv}|QR%Ul^}c-f_AI!! zCJHF5IkmQf9}i%nHGn*nMCTnJd=ee<1s#q4@Z6!(7S|HOwrkQ!s_3~MRcz4MB@pFN zToz&rW2n_FB9dbJdnJCyU^^tx)!xzV9c1#AAWsNt>an%P8fh`J25`bp&OWNau$|5Q zp$JLM&8MFbV+N?=RbKO2Lo8vv>e2)%NCV1!7nHx4PV4zd7fpJK`hOK_FD;y%KNgaq*{RWN2I3TWF~19i zLsw?ly!OMaL-cw**2J+C8?$xNvc%90lVGMmfF|1F|4!$5de5B)bSw}) z9_Fzi0FV4lU9v1ChQ*;E-a>mI#bw^q(PaCK>&O4wq5cb)rfAy)CI;KL@R0`xMbGc$ zb?S2R9v*G(6@7-SmL0(5=#`!7_GoKR45i7IJ`oyd!b9v7Kkl!38EQ~=|2RTIbv?5Noe0&;Hy+kbN3u}iBhRyP%Z=#d&|b| z|GOQMfIZYu`RV#FxoA`?@#TM=v5Nk?qGQ#*Nu8>XgJ=+*d&AU#b%+7PYsY&z-KMc^ zfdT^mw5(xUBM3+h^D%&$kT2NYBsrnlg$xd3Ws7M4kRDjIw`8Q2>LfhIRVNcHT-(+1FyX+@TUJr0d=0 z%YfyyZIHX4&32co5P$m}#3Zooz2dc|GAvc`e2~9b$?a9-pVo*hYjxj*MjVVJ3{kR63E20A95Qc zcg2|~3ywt*y0>NYPuVxZB%4(SnGRdx!tFVk#)bkEc)h_lJ6Vs~G=+i^iiG~qtVyl$ z&OMRaSNFcGp$n;ls!F@(mjpVygALOt&B&GYb00D{i_soflXCziL5t%8)ikn}nc0EwEu2p|Ml*?4}0M4fweCD zEVtRPGm^U(jsgZ(Ea-tIHJC{iq5{1NozscMMr|`h#Vx^}Q+b0fF+)bl-A@$@q6OO( zWC)DG1md3F{QoY}81gB3<*v$UV%pcMX^Dq?U0P+K*Ry$-AO;x!GK!r7CpMA3ilONf zp;pidzh$1L=Py`G;0p;POhNqIjaX7ELRnmXgPiAX9;mib`VUZCb)$&BuT0$-Ixrle^jQ6q zF*!?U!BOG1XUd7{9oPaG#rnymtUI0h&0Gm}=x2kzKVzI%Wp?25489c{PL(5jWX9JVN~9*9ncCF9aKK!PWX)iofUw^= zP6r4mg_%Hd7X^(?{{`btIJ+lx_fp7fohXq*&b@|+*9GSMCp+`h5`Pu?Y!xyTP zyX4AaCJNq{sWn%?+Dn6@Y*`ocKpO)i`=YfNu1T^S5f?^h_kf5vvYXQdg*-d5E~ zZY6+TQLq5A#;0c*3UlM6HG>TQaDpR_X&DU{^Hhsw8+ms#+#gni?S+P$RrNnzidq(J4_ps3s8lF!{Vqu(a0Wte{GiAao$ zr2P$N|3=;CkLSCD>P?qmd8#Q)%TsTk8IJ^!@W^yGhv?^P^pE76UShbxqTTehtxJ#` zl4=KE=V`cl`5_P+-C1nX;z&?fJ`elYUr+=Jix-Aq@*KFZBqK?g*u;?8fuj%7?$5?O!-H&76Ai#COYg z^0PSi9W}0ZFlS(Ml^h0R2d=p_xAg&xR{?)#_#<>^t)#Bbz89atYpXC4sRZB4nk3Ih zN9mL7MQ=mNcGd&<5YPn$u!+f&IRAGx5=)BY*0!}HRe*>;b_;nnpWS@s3VWyX>PR`! zAZ1G7D39*)h-`y5fWhgz`C=1~meX!>y{3D(QZ)P9Q2(Xzm-LynQE70selHhR09%#0 z9Ii)5dDf*N`+3BgtM?`iqxXkZT6J8Lx6*X7>{HdURi-ozL>D%Df)$wM#19PYD{o6| zAe`KiLuxm#>?5&T{qz3hTFwcJf>zP)wmqt>ze!EA7uIoYHD?PPR@Z!IMnWKmD4xpb zPbMU5dOLet8awwef(v{a&Ybl*QXIF7kD2A2wQ|EnCdIc|}#`fDJ!tn1+Qb6v*g#g9WCrJX6owD9-+%vQMen{fZdD;R=n)?nTO ztPi*X-)`>ya+FRLnCp*bxloa8a&Q5KR*x+795~K#N87es{DE3+mM*u*87FIpb?Dn z;uT6nXIS7AQ;z_Axt^7j*ZRdSp9%lkUXj#^L4%cnwqN)SqRsxiMD#)l)g^trS=K$CRg-hP9nK$7Sz&Su==smxz?vxsBd~4N2<~Dtb z=Ho$5u~C|iRg(&X+0}^Je_g7$s8UGG=QfSGnV&zqtELy;pRm>$Jt5umlDY z4dzOd(7twlSMmOWCe&g~eQHgL@q#>b1ImMug!3uk7MpcR^&!O>!&iSBkQz=eL%Q}se04RjSIyal zPLS%bAjiJG&E@vGHi33cv2eJgvCqV%`E{p>X*!x3X`(I?{umbLmxDz|jBvMPOK5 z&M{Zj^nWEV?w)^pQCY84s|F{&ZaBIl+Hq0xL>$>FFI(A}c&2r48iVVA(xamJ3uP&X zr+{x6()>~#a;U2&ccL551-huSkH-W0!5ZC(>9mN9@}asf7i>#+#)Hj}{bBTNHN!;h z9J%J%&J^jy`I-?-ZAglSp8k%2BNBU;U-oE+p)`Ebrp-Q-^0WN7;3V_$G6*Jair9$t zg;VebQGaBg)mbgexL`GqXgSTV9`X_mlJ~!~miE@cuv04`NmJJ;)Xjd>L{lL@lMGdQ&YFCp&?KHB}f5JNngT7eD z%ThJ%q_?VM9fI?%>q|=b?0tAI4`wB`R~493awy%kE)@5V=47cdrH+Ha64iFItZ&(B zs^JT7WM^)S0*x`m^2;tZ?G0^jC}FEbuE$=jjCnZq&ORn=dqu`fiN0!! z-|u9dOepmqB>dk@?pMoEFKfOLTKsiCmScrI#FiWK3|L4^RcO6jK-;*%4A!Bs|3IN; zU0~#Yo(ypcM^0&qiC{w*{27xxvh7xBJttw8w4a^*Ef)LVDo5$$yD9Eu-^#*dhftuG9h zg(tm$Sn9@m!5RY%sj$@V_spPG-oV}6>Cs2oW?A6ck7&MC=4e6IxQrNd(3Q2D6R$l4 zKR^DFp4xs<&u)pSPW?Y#aed-5dzo7NNpK^{OzX1=GNTj z>s2Wg(mr76h~fM^@fx9-f38XYvAPnikRNq7opUwF5szu8r^^Wl{ls#r_P&v5*mVvm zCiJ8=wKoWEqO0RJFTRD-f~7Ogl=c(y*bBuII2MSy{3yU?AYJNOA5zW&3x4Jyp``!g zC9>*|+xS*aPNejCreC~?^w02!sIB>~Rkc-&Ogys;Qy}58=X5GrdBXbLpk}KoHEQ~rSXBY#Il2L%d>hnw8oPm z2I#wbLzD>>sO$cqq8SQ#2^ucAz8O`AcfMueka*8nU|eZd{2}C@jfwi-2#STPapZm$ zH*C?JT6iZ|1FG-$uYFfIW>8Q8*-$c=RIAJfau~f1n96wSe2=TzA=Zs9@7~Cfh8ED7 zZ$zw_^@<%!$aH)`%+G_d{{zX8Xq=;ElwRr5)r_L0=ES3pSFlP$5oPRMU|e2F|z;}HiMCK ziXJLX5480IvTB}KD!yr1M~J2mv1`Vd113%p#=3hz=^4W`%id~dI#?R5RwSTRQ0WVZ z4IO8&cz;cv?EvrXM* zVpFMn^7@t(kg8}EpNL9*m$PVT$_;0a)d{nj_#Rk3q4%DJsm4zKPdl#*YJiqcMR`1w zP0Boe{sKV-v*`_Wapeh7anZfJA&kZ4-p#;yP$6^Po2IMD=Mm)vc& zCb6Y{zAQ);wvCNMAS)YQP3?u$I zLptf!3hB;YW-|@dUwc=%Y{5xYoWM2?)as}w0G&50^aeiZ?nhAP3;ri0_fX&Y$*_S; z6G}6M43bwd4*UdSNi6sFSq(ro}@?3#(eE`y$#*6x8k<&>rl|!?>A5Y z%6;UhPPho_79CkzZg~ESTus>^?Pf959FYg_&I47WzaM5Cn0J>>s)g)8aL-*YUoxzw z%2^DI%PO1FP#RCS0|Mj(N3M?C7ar^C?T(n%Qe5c;zhzrvQw9@Qi)1#s1oz}#7N=bF zE!*oujau*4$M(+2<|i5)g@Q}Jc-!gQt!cq4=_<>C{4$-LN0hcLMusATEq#^pBroEb z7DcoPy)&SbZbqt3Sr8DBSL)iuh0=2D^(Ze=ak~mmJdL?xj#DjrnUL{esw#0r%4>mS znk9iTd@v8N7fyrC+GOy*52@+o$q%-@Xn;7jTtS;J!I}2L$Q4H?17eR@0XgP1UOr)~ zT%|-bI|?GE{y9F~aLu_Zg+DB%;&V)Ibz^gG)BaG*Gu;@|2m@Tew0xPi+q(= zP=$OdZ#*157x5PnoqVl>^+(+ak9I`1(>lvYIy|ssH0GLzT+9(&p|ENOp+8$o43L$a zhPRLAsMZns8H4)=n#?hqIoQc*uQ|Hz8p0QL!Ub>VhQ+K7(&f&5r{TjXLaoJo