From 8e613d10b9805b41f832706cd693fefd68210112 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:54:32 -0500 Subject: [PATCH 01/24] Redesign UI layout and dialogs --- app/globals.css | 12 +- components/ProBillSplitter.tsx | 1445 ++++++++++++++++---------------- components/ui/alert-dialog.tsx | 157 ++++ 3 files changed, 895 insertions(+), 719 deletions(-) create mode 100644 components/ui/alert-dialog.tsx diff --git a/app/globals.css b/app/globals.css index 1c62062..a686a76 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1031,14 +1031,15 @@ /* ===== Pro SaaS Design System ===== */ .pro-app-shell { - @apply h-screen w-full overflow-hidden relative; + @apply h-dvh w-full overflow-hidden relative; background: #F8FAFC; /* slate-50 */ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } .pro-header { @apply fixed top-0 left-0 right-0 flex items-center justify-between px-6; - height: 64px; + height: calc(64px + env(safe-area-inset-top)); + padding-top: env(safe-area-inset-top); z-index: 20; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(8px); @@ -1048,7 +1049,8 @@ .pro-footer { @apply fixed bottom-0 left-0 right-0 flex items-center justify-between px-6; - height: 56px; + height: calc(56px + env(safe-area-inset-bottom)); + padding-bottom: env(safe-area-inset-bottom); z-index: 40; background: #FFFFFF; border-top: 1px solid #E2E8F0; @@ -1057,8 +1059,8 @@ .pro-main { @apply absolute inset-0 overflow-hidden; - padding-top: 64px; - padding-bottom: 56px; + padding-top: calc(64px + env(safe-area-inset-top)); + padding-bottom: calc(56px + env(safe-area-inset-bottom)); z-index: 10; } diff --git a/components/ProBillSplitter.tsx b/components/ProBillSplitter.tsx index 98224ff..4f28aaf 100644 --- a/components/ProBillSplitter.tsx +++ b/components/ProBillSplitter.tsx @@ -19,13 +19,11 @@ import { Percent, Calculator, ChevronDown, - Code, Camera } from 'lucide-react' import { useBill } from '@/contexts/BillContext' import type { Item, Person } from '@/contexts/BillContext' -import { formatCurrency } from '@/lib/utils' -import { getBillSummary, calculateItemSplits } from '@/lib/calculations' +import { cn } from '@/lib/utils' import { generateSummaryText, copyToClipboard } from '@/lib/export' import { useToast } from '@/hooks/use-toast' import { ShareBill } from '@/components/ShareBill' @@ -42,9 +40,25 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@/components/ui/context-menu' export type SplitMethod = "even" | "shares" | "percent" | "exact" @@ -127,7 +141,10 @@ const GridCell = React.memo(({ value={value} onChange={e => onCellEdit(itemId, field, e.target.value)} onClick={(e) => e.stopPropagation()} - className={`w-full h-full px-4 py-3 text-sm border-2 border-indigo-500 focus:outline-none ${className}`} + className={cn( + "w-full h-full px-4 py-3 text-sm border-2 border-indigo-500 focus:outline-none", + className + )} /> ) @@ -136,13 +153,13 @@ const GridCell = React.memo(({ return (
onCellClick(row, col)} - className={` - w-full h-full px-4 py-3 flex items-center cursor-text relative - ${isSelected ? 'ring-inset ring-2 ring-indigo-500 z-10' : ''} - ${className} - `} + className={cn( + "w-full h-full px-4 py-3 flex items-center cursor-text relative", + isSelected && "ring-inset ring-2 ring-indigo-500 z-10", + className + )} > - + {value ? (field === 'price' ? `$${value}` : value) : placeholder}
@@ -165,18 +182,25 @@ function DesktopBillSplitter() { const analytics = useBillAnalytics() const [activeView, setActiveView] = useState<'ledger' | 'breakdown'>('ledger') const [billId, setBillId] = useState('') + const [loadBillError, setLoadBillError] = useState(null) + const [copyError, setCopyError] = useState(null) const [selectedCell, setSelectedCell] = useState<{ row: number; col: string }>({ row: 0, col: 'name' }) const [editing, setEditing] = useState(false) const [editingPerson, setEditingPerson] = useState(null) const [hoveredColumn, setHoveredColumn] = useState(null) - const [contextMenu, setContextMenu] = useState<{ x: number; y: number; itemId: string; personId?: string } | null>(null) const [isLoadingBill, setIsLoadingBill] = useState(false) const [newLoadDropdownOpen, setNewLoadDropdownOpen] = useState(false) const [hideStarter, setHideStarter] = useState(false) + const [isNewBillDialogOpen, setIsNewBillDialogOpen] = useState(false) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [pendingDeleteItem, setPendingDeleteItem] = useState(null) + const [isRemovePersonDialogOpen, setIsRemovePersonDialogOpen] = useState(false) + const [pendingRemovePerson, setPendingRemovePerson] = useState(null) const editInputRef = useRef(null) const loadBillRequestRef = useRef(null) // Track current load request to prevent race conditions const previousItemsLengthRef = useRef(0) + const newBillSourceRef = useRef<'button' | 'shortcut'>('button') const people = state.currentBill.people const items = state.currentBill.items @@ -188,7 +212,6 @@ function DesktopBillSplitter() { } // --- Derived Data --- - const summary = getBillSummary(state.currentBill) const hasMeaningfulItems = useMemo(() => { return items.some(i => (i.name || '').trim() !== '' || (i.price || '').trim() !== '' || (i.quantity || 1) !== 1) }, [items]) @@ -324,6 +347,29 @@ function DesktopBillSplitter() { } }, [items, dispatch, analytics, toast]) + const confirmNewBill = useCallback(() => { + dispatch({ type: 'NEW_BILL' }) + toast({ title: "New bill created" }) + analytics.trackBillCreated() + analytics.trackFeatureUsed( + newBillSourceRef.current === "shortcut" ? "keyboard_shortcut_new_bill" : "new_bill" + ) + newBillSourceRef.current = "button" + setIsNewBillDialogOpen(false) + }, [dispatch, toast, analytics]) + + const openDeleteDialog = useCallback((item: Item) => { + setPendingDeleteItem(item) + setIsDeleteDialogOpen(true) + }, []) + + const confirmDeleteItem = useCallback(() => { + if (!pendingDeleteItem) return + deleteItem(pendingDeleteItem.id) + setPendingDeleteItem(null) + setIsDeleteDialogOpen(false) + }, [pendingDeleteItem, deleteItem]) + const duplicateItem = useCallback((item: Item) => { const duplicated: Omit = { name: `${item.name} (copy)`, @@ -387,6 +433,18 @@ function DesktopBillSplitter() { setEditingPerson(null) }, [people, items, dispatch, analytics, toast]) + const openRemovePersonDialog = useCallback((person: Person) => { + setPendingRemovePerson(person) + setIsRemovePersonDialogOpen(true) + }, []) + + const confirmRemovePerson = useCallback(() => { + if (!pendingRemovePerson) return + removePerson(pendingRemovePerson.id) + setPendingRemovePerson(null) + setIsRemovePersonDialogOpen(false) + }, [pendingRemovePerson, removePerson]) + // --- Split Method Management --- const getSplitMethodIcon = (method: SplitMethod) => { const option = splitMethodOptions.find(o => o.value === method) @@ -411,11 +469,7 @@ function DesktopBillSplitter() { const handleLoadBill = useCallback(async () => { const trimmedId = billId.trim() if (!trimmedId) { - toast({ - title: "Enter Bill ID", - description: "Please enter a bill ID to load", - variant: "destructive" - }) + setLoadBillError("Enter a bill ID to load.") return } @@ -424,6 +478,7 @@ function DesktopBillSplitter() { loadBillRequestRef.current = requestId setIsLoadingBill(true) + setLoadBillError(null) analytics.trackFeatureUsed("load_bill_by_id", { bill_id: trimmedId }) try { @@ -435,11 +490,7 @@ function DesktopBillSplitter() { } if (result.error || !result.bill) { - toast({ - title: "Bill not found", - description: result.error || "Could not find bill with that ID", - variant: "destructive" - }) + setLoadBillError(result.error || "Could not find bill with that ID.") analytics.trackError("load_bill_failed", result.error || "Bill not found") return } @@ -452,14 +503,11 @@ function DesktopBillSplitter() { }) analytics.trackSharedBillLoaded("cloud") setBillId('') // Clear input after successful load + setLoadBillError(null) } catch (error) { // Only show error if this request is still current if (loadBillRequestRef.current === requestId) { - toast({ - title: "Load failed", - description: error instanceof Error ? error.message : "Unknown error", - variant: "destructive" - }) + setLoadBillError(error instanceof Error ? error.message : "Load failed. Try again.") analytics.trackError("load_bill_failed", error instanceof Error ? error.message : "Unknown error") } } finally { @@ -473,11 +521,7 @@ function DesktopBillSplitter() { // --- Copy Breakdown --- const copyBreakdown = useCallback(async () => { if (people.length === 0) { - toast({ - title: "No data to copy", - description: "Add people and items to generate a summary", - variant: "destructive" - }) + setCopyError("Add people and items to copy a summary.") analytics.trackError("copy_summary_failed", "No data to copy") return } @@ -485,6 +529,7 @@ function DesktopBillSplitter() { const text = generateSummaryText(state.currentBill) const success = await copyToClipboard(text) if (success) { + setCopyError(null) toast({ title: "Copied!", description: "Bill summary copied to clipboard" @@ -492,11 +537,7 @@ function DesktopBillSplitter() { analytics.trackBillSummaryCopied() analytics.trackFeatureUsed("copy_summary") } else { - toast({ - title: "Copy failed", - description: "Unable to copy to clipboard. Please try again.", - variant: "destructive" - }) + setCopyError("Unable to copy. Please try again.") analytics.trackError("copy_summary_failed", "Clipboard API failed") } }, [people, state.currentBill, toast, analytics]) @@ -525,42 +566,6 @@ function DesktopBillSplitter() { setEditing(true) }, [people, items, toggleAssignment]) - // --- Context Menu --- - const handleContextMenu = useCallback((e: React.MouseEvent, itemId: string, personId?: string) => { - e.preventDefault() - - // Context menu dimensions - const menuWidth = 192 // 48 * 4 = w-48 - const menuHeight = 200 // approximate height - - // Calculate position with boundary detection - let x = e.clientX - let y = e.clientY - - // Keep menu within viewport bounds - if (x + menuWidth > window.innerWidth) { - x = window.innerWidth - menuWidth - 10 - } - if (y + menuHeight > window.innerHeight) { - y = window.innerHeight - menuHeight - 10 - } - - setContextMenu({ - x, - y, - itemId, - personId - }) - }, []) - - useEffect(() => { - const handleClick = () => { - setContextMenu(null) - } - window.addEventListener('click', handleClick) - return () => window.removeEventListener('click', handleClick) - }, []) - // --- Global Keyboard Shortcuts & Grid Navigation --- const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => { // Check if we're in an input field - comprehensive check @@ -637,11 +642,6 @@ function DesktopBillSplitter() { e.preventDefault() return } - if (contextMenu) { - setContextMenu(null) - e.preventDefault() - return - } if (editing) { setEditing(false) e.preventDefault() @@ -700,12 +700,8 @@ function DesktopBillSplitter() { // Cmd+N: New bill if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault() - if (confirm('Start a new bill? Current bill will be lost if not shared.')) { - dispatch({ type: 'NEW_BILL' }) - toast({ title: "New bill created" }) - analytics.trackBillCreated() - analytics.trackFeatureUsed("keyboard_shortcut_new_bill") - } + newBillSourceRef.current = "shortcut" + setIsNewBillDialogOpen(true) return } @@ -801,7 +797,7 @@ function DesktopBillSplitter() { if (item) toggleAssignment(item.id, selectedCell.col) return } - }, [activeView, editing, selectedCell, items, people, addItem, toggleAssignment, addPerson, copyBreakdown, dispatch, toast, analytics, state.historyIndex, editingPerson, contextMenu, updateItem]) + }, [activeView, editing, selectedCell, items, people, addItem, toggleAssignment, addPerson, copyBreakdown, dispatch, toast, analytics, state.historyIndex, editingPerson, updateItem]) useEffect(() => { window.addEventListener('keydown', handleGlobalKeyDown) @@ -849,7 +845,7 @@ function DesktopBillSplitter() {
{/* Left cluster: Brand + Title + Undo/Redo */} -
+
@@ -865,7 +861,7 @@ function DesktopBillSplitter() { className="block text-sm font-bold bg-transparent border-none p-0 focus:ring-0 text-slate-900 w-auto min-w-[7ch] max-w-[26ch] hover:text-indigo-600 transition-colors font-inter" placeholder="Project Name" /> -
SPLIT SIMPLE
+
SPLIT SIMPLE
@@ -877,6 +873,7 @@ function DesktopBillSplitter() { analytics.trackUndoRedoUsed("undo", state.historyIndex) }} disabled={!canUndo} + aria-label="Undo" className="p-1.5 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed" title="Undo (Cmd+Z)" > @@ -889,6 +886,7 @@ function DesktopBillSplitter() { analytics.trackUndoRedoUsed("redo", state.historyIndex) }} disabled={!canRedo} + aria-label="Redo" className="p-1.5 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed" title="Redo (Cmd+Shift+Z)" > @@ -897,17 +895,35 @@ function DesktopBillSplitter() {
+ {/* Center: View switcher */} +
+ + +
+ {/* Right cluster: New/Load + Sync/Scan/Share */}
- + { + setNewLoadDropdownOpen(open) + if (!open) setLoadBillError(null) + }} + >
+ {loadBillError && ( +

+ {loadBillError} +

+ )}
@@ -997,6 +1024,21 @@ function DesktopBillSplitter() { )} />
+
+ + {copyError && ( + + {copyError} + + )} +
+
@@ -1009,422 +1051,452 @@ function DesktopBillSplitter() {
{/* LEDGER VIEW */} {activeView === 'ledger' && ( -
+
-
- {/* Sticky toolbar */} -
-
- -
- - Split - - - People - -
-
-
- Tab/Enter to commit - Esc to exit -
-
- - {/* Starter banner (shown until the bill has meaningful items) */} - {!hideStarter && !hasMeaningfulItems && ( -
-
-
-
Start splitting in 3 quick steps
-
- Click a cell and type, then press Tab/Enter to move like Sheets. -
- -
-
-
-
1
-
-
Add people
-
⌘⇧P
-
-
- -
- -
-
-
2
-
-
Add items
-
⌘⇧N
-
-
- -
- -
-
-
3
-
-
Scan receipt
-
Optional
-
-
- - - Scan - - )} - /> -
-
-
- -
-
- )} - {/* Live Roster */} -
-
- Live Breakdown -
-
- {people.length === 0 ? ( -
- - Click + above to add people or press ⌘⇧P -
- ) : ( - people.map(p => { - const stats = personFinalShares[p.id] - const colorObj = COLORS[p.colorIdx || 0] - const percent = stats ? (stats.total / (grandTotal || 1)) * 100 : 0 - return ( -
-
- {p.name.split(' ')[0]} - - {formatCurrencySimple(stats?.total || 0)} - - - ({percent.toFixed(0)}%) - -
- ) - }) - )} -
- - {/* Sticky Header */} -
-
#
-
Item Description
-
Price
-
Qty
+
+
+
+
+
+

Items & split

+

+ Add items, set prices, and assign people to split each line. +

+
- {people.map(p => { - const colorObj = COLORS[p.colorIdx || 0] - const initials = p.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) - return ( -
setHoveredColumn(p.id)} - onMouseLeave={() => setHoveredColumn(null)} - onClick={() => setEditingPerson(p)} - > -
- {initials} + {/* Sticky toolbar */} +
+
+ +
+ + Split + + + People + +
+
+
+ Tab/Enter to commit + Esc to exit
- - {p.name.split(' ')[0]} -
- ) - })} -
- -
-
- Total -
-
+
+
+ {/* Sticky Header */} +
+
#
+
Item Description
+
Price
+
Qty
+ + {people.map(p => { + const colorObj = COLORS[p.colorIdx || 0] + const initials = p.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) + return ( + + ) + })} + + +
+ Total +
+
- {/* Body */} -
- {calculatedItems.length === 0 && ( -
-
- -
-

No items yet

-

- Add your first item to start splitting the bill. Press ⌘⇧N or click the button below. -

- -
- )} - - {calculatedItems.map((item, rIdx) => ( -
handleContextMenu(e, item.id)} - > - {/* Index / "Equal" Button */} -
- {String(rIdx + 1).padStart(2, '0')} - -
+ {/* Body */} +
+ {calculatedItems.length === 0 && ( +
+
+ +
+

No items yet

+

+ Add your first item to start splitting the bill. Press ⌘⇧N or click the button below. +

+ +
+ )} - {/* Name + Split Method Selector */} -
-
- -
-
- - + {calculatedItems.map((item, rIdx) => ( + + +
+ {/* Index / "Equal" Button */} +
+ {String(rIdx + 1).padStart(2, '0')} + +
+ + {/* Name + Split Method Selector */} +
+
+ +
+
+ + + + + + {splitMethodOptions.map(option => ( + { + e.preventDefault() + changeSplitMethod(item.id, option.value) + }} + className={cn( + "text-xs flex items-center gap-2 font-inter", + item.method === option.value ? "bg-indigo-50 text-indigo-700 font-bold" : "text-slate-600" + )} + > + {React.createElement(option.icon, { size: 12 })} + {option.label} + + ))} + + +
+
+ + {/* Price */} +
+ +
+ + {/* Qty */} +
+ +
+ + {/* Person Cells (The "Cards") */} + {people.map(p => { + const isAssigned = item.splitWith.includes(p.id) + const isSelected = selectedCell.row === rIdx && selectedCell.col === p.id + const color = COLORS[p.colorIdx || 0] + + return ( + + ) + })} + + {/* Inline actions */} +
+ + +
+ + {/* Row Total */} +
+ ${(item.totalItemPrice || 0).toFixed(2)} +
+
+
+ + duplicateItem(item)} + > + Duplicate item + + toggleAllAssignments(item.id)} + > + Split with everyone + + clearRowAssignments(item.id)} + > + Clear row + + + openDeleteDialog(item)} + > + Delete item + + +
+ ))} + + {/* Add Row Button */} + {calculatedItems.length > 0 && ( -
- - {splitMethodOptions.map(option => ( - { - e.preventDefault() - changeSplitMethod(item.id, option.value) - }} - className={`text-xs flex items-center gap-2 font-inter ${item.method === option.value ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-slate-600'}`} - > - {React.createElement(option.icon, { size: 12 })} - {option.label} - - ))} - -
+ )} +
- - {/* Price */} -
- -
- - {/* Qty */} -
- -
- - {/* Person Cells (The "Cards") */} - {people.map(p => { - const isAssigned = item.splitWith.includes(p.id) - const isSelected = selectedCell.row === rIdx && selectedCell.col === p.id - const color = COLORS[p.colorIdx || 0] - - return ( -
{ - e.stopPropagation() - handleContextMenu(e, item.id, p.id) +
+
+ +
+
+ )} - {/* Inline actions */} -
- +
+
+
+

People

+

Track who is splitting the bill.

+
- {/* Row Total */} -
- ${(item.totalItemPrice || 0).toFixed(2)} -
-
- ))} - - {/* Add Row Button */} - {calculatedItems.length > 0 && ( - - )} - - {/* Summary Rows Section */} -
- {/* Subtotal Row */} -
-
-
- Subtotal -
-
- ${subtotal.toFixed(2)} -
-
- - {people.map(p => { - const stats = personFinalShares[p.id] - return ( -
- ${(stats?.subtotal || 0).toFixed(2)} +
+ {people.length === 0 ? ( +
+ No people yet. Add someone to start splitting.
- ) - })} - -
-
- ${subtotal.toFixed(2)} + ) : ( + people.map(p => { + const stats = personFinalShares[p.id] + const colorObj = COLORS[p.colorIdx || 0] + const percent = stats ? (stats.total / (grandTotal || 1)) * 100 : 0 + return ( + + ) + }) + )}
- {/* Tax Row */} -
-
-
- Tax +
+
+
+

Bill totals

+

Adjust tax, tip, and discounts.

+
-
- { - dispatch({ type: 'SET_TAX', payload: e.target.value }) - analytics.trackTaxTipDiscountUsed("tax", e.target.value, state.currentBill.taxTipAllocation) - }} - className="w-full bg-white rounded px-2 py-1.5 border border-slate-200 shadow-inner focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:bg-indigo-50/30 transition-all text-xs font-space-mono text-slate-700 text-right" - placeholder="0.00" - /> -
-
- - {people.map(p => { - const stats = personFinalShares[p.id] - return ( -
- ${(stats?.tax || 0).toFixed(2)} -
- ) - })} - -
-
- ${taxAmount.toFixed(2)} -
-
- - {/* Tip Row */} -
-
-
- Tip -
-
- { - dispatch({ type: 'SET_TIP', payload: e.target.value }) - analytics.trackTaxTipDiscountUsed("tip", e.target.value, state.currentBill.taxTipAllocation) - }} - className="w-full bg-white rounded px-2 py-1.5 border border-slate-200 shadow-inner focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:bg-indigo-50/30 transition-all text-xs font-space-mono text-slate-700 text-right" - placeholder="0.00" - /> -
-
- {people.map(p => { - const stats = personFinalShares[p.id] - return ( -
- ${(stats?.tip || 0).toFixed(2)} -
- ) - })} - -
-
- ${tipAmount.toFixed(2)} -
-
- - {/* Discount Row */} -
-
-
- Discount -
-
- { - dispatch({ type: 'SET_DISCOUNT', payload: e.target.value }) - analytics.trackTaxTipDiscountUsed("discount", e.target.value, state.currentBill.taxTipAllocation) - }} - className="w-full bg-white rounded px-2 py-1.5 border border-slate-200 shadow-inner focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:bg-indigo-50/30 transition-all text-xs font-space-mono text-slate-700 text-right" - placeholder="0.00" - /> -
-
- - {people.map(p => { - const stats = personFinalShares[p.id] - return ( -
- ${(stats?.discount || 0).toFixed(2)} +
+
+ + { + dispatch({ type: 'SET_TAX', payload: e.target.value }) + analytics.trackTaxTipDiscountUsed("tax", e.target.value, state.currentBill.taxTipAllocation) + }} + className="w-full h-9 rounded-md border border-slate-200 bg-white px-2 text-right font-space-mono text-slate-700 tabular-nums focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200" + placeholder="0.00" + /> +
+
+ + { + dispatch({ type: 'SET_TIP', payload: e.target.value }) + analytics.trackTaxTipDiscountUsed("tip", e.target.value, state.currentBill.taxTipAllocation) + }} + className="w-full h-9 rounded-md border border-slate-200 bg-white px-2 text-right font-space-mono text-slate-700 tabular-nums focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200" + placeholder="0.00" + /> +
+
+ + { + dispatch({ type: 'SET_DISCOUNT', payload: e.target.value }) + analytics.trackTaxTipDiscountUsed("discount", e.target.value, state.currentBill.taxTipAllocation) + }} + className="w-full h-9 rounded-md border border-slate-200 bg-white px-2 text-right font-space-mono text-slate-700 tabular-nums focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200" + placeholder="0.00" + /> +
+
+ +
+ {formatCurrencySimple(subtotal)}
- ) - })} - -
-
- -${discountAmount.toFixed(2)} -
-
- - {/* Grand Total Row */} -
-
-
- Grand Total +
-
-
- - {people.map(p => { - const stats = personFinalShares[p.id] - const colorObj = COLORS[p.colorIdx || 0] - return ( -
-
- ${(stats?.total || 0).toFixed(2)} -
-
- ) - })} -
-
- ${grandTotal.toFixed(2)} +
+
+ Tax + {formatCurrencySimple(taxAmount)} +
+
+ Tip + {formatCurrencySimple(tipAmount)} +
+
+ Discount + -{formatCurrencySimple(discountAmount)} +
+
+ Grand total + {formatCurrencySimple(grandTotal)} +
- -
+
@@ -1587,7 +1599,7 @@ function DesktopBillSplitter() { {/* BREAKDOWN VIEW */} {activeView === 'breakdown' && ( -
+
{/* LEFT: Bill Summary Receipt */} @@ -1596,11 +1608,11 @@ function DesktopBillSplitter() {
-

{title}

-

Bill Summary

+

{title}

+

Bill Summary

-
+
{calculatedItems.map(item => (
{item.name} @@ -1609,7 +1621,7 @@ function DesktopBillSplitter() { ))}
-
+
Subtotal {formatCurrencySimple(subtotal)} @@ -1643,12 +1655,15 @@ function DesktopBillSplitter() {
-
+
{initials}
-
{p.name}
+
{p.name}
-
+
{formatCurrencySimple(stats?.total || 0)}
@@ -1657,7 +1672,7 @@ function DesktopBillSplitter() {
Share - {stats?.ratio.toFixed(1) || 0}% + {stats?.ratio.toFixed(1) || 0}%
{[...Array(10)].map((_, i) => { @@ -1665,7 +1680,10 @@ function DesktopBillSplitter() { return (
) })} @@ -1678,14 +1696,14 @@ function DesktopBillSplitter() {
{item.name}
- + {formatCurrencySimple(item.pricePerPerson)}
))}
-
+
Sub: {formatCurrencySimple(stats?.subtotal || 0)} @@ -1709,42 +1727,101 @@ function DesktopBillSplitter() { )}
+ + + + Start a new bill? + + Your current bill will be cleared unless you share it first. + + + + Cancel + Start new bill + + + + + { + setIsDeleteDialogOpen(open) + if (!open) setPendingDeleteItem(null) + }} + > + + + Delete this item? + + This action can’t be undone. + + + + setPendingDeleteItem(null)}> + Cancel + + Delete item + + + + + { + setIsRemovePersonDialogOpen(open) + if (!open) setPendingRemovePerson(null) + }} + > + + + Remove this person? + + {pendingRemovePerson?.name || "This person"} will be removed from the bill and their splits cleared. + + + + setPendingRemovePerson(null)}> + Cancel + + Remove person + + + + {/* --- Footer --- */}
- {items.length} + {items.length} items - {people.length} + {people.length} people
-
+
- -
@@ -1768,71 +1845,6 @@ function DesktopBillSplitter() {
- {/* --- Context Menu --- */} - {contextMenu && ( -
e.stopPropagation()} - > -
- Actions -
- {contextMenu.personId ? ( - - ) : ( - <> - - - - - )} -
- -
- )} - {/* --- Person Editor Modal --- */} {editingPerson && (
e.stopPropagation()} >
-

Edit Member

+

Edit Member

@@ -1883,14 +1900,14 @@ function DesktopBillSplitter() {
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..ddf2ebc --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} From 93ddc342f6fd157d3a6e0f2a7af6fc634a299786 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:12:27 -0500 Subject: [PATCH 02/24] Improve UI accessibility and motion --- app/admin/page.tsx | 9 ++- app/globals.css | 14 +++- components/BillLookup.tsx | 6 ++ components/KeyboardShortcutsHelp.tsx | 1 + components/LedgerItemsTable.tsx | 3 +- components/MobileLedgerView.tsx | 1 + components/MobileSpreadsheetView.tsx | 1 + components/PeopleBreakdownTable.tsx | 3 +- components/ProBillSplitter.tsx | 108 +++++++++++++++------------ components/ReceiptScanner.tsx | 33 ++++++-- components/ShareBill.tsx | 2 + components/TotalsPanel.tsx | 13 +++- components/mobile/MobileGridView.tsx | 2 + 13 files changed, 139 insertions(+), 57 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index cd3c4d2..f58ea65 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -772,6 +772,7 @@ export default function AdminPage() { placeholder="Search bills by title, ID, or amount..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + aria-label="Search bills" className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" />
@@ -872,6 +873,7 @@ export default function AdminPage() { setShowBillDialog(true) }} title="View details" + aria-label="View bill details" > @@ -880,6 +882,7 @@ export default function AdminPage() { size="icon" onClick={() => window.open(bill.shareUrl, '_blank')} title="Open in new tab" + aria-label="Open share link in new tab" > @@ -888,6 +891,7 @@ export default function AdminPage() { size="icon" onClick={() => copyToClipboard(bill.shareUrl)} title="Copy share link" + aria-label="Copy share link" > @@ -896,6 +900,7 @@ export default function AdminPage() { size="icon" onClick={() => handleExtendBill(bill.id)} title="Extend expiration" + aria-label="Extend bill expiration" > @@ -907,6 +912,7 @@ export default function AdminPage() { setShowDeleteDialog(true) }} title="Delete bill" + aria-label="Delete bill" > @@ -996,6 +1002,7 @@ export default function AdminPage() { variant="outline" size="icon" onClick={() => copyToClipboard(selectedBill.shareUrl)} + aria-label="Copy share URL" > @@ -1119,4 +1126,4 @@ export default function AdminPage() {
) -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index a686a76..9d8c2cd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -520,7 +520,10 @@ select:focus { @apply outline-none ring-2 ring-primary ring-offset-2; background-color: rgba(var(--primary), 0.02); - transition: all 0.2s var(--ease-out-cubic); + transition: + box-shadow 0.2s var(--ease-out-cubic), + border-color 0.2s var(--ease-out-cubic), + background-color 0.2s var(--ease-out-cubic); } input::placeholder { @@ -1142,3 +1145,12 @@ scrollbar-color: #CBD5E1 transparent; } } + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/components/BillLookup.tsx b/components/BillLookup.tsx index ccfbd59..2b5c1da 100644 --- a/components/BillLookup.tsx +++ b/components/BillLookup.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet" +import { Label } from "@/components/ui/label" import { Search, Loader2 } from "lucide-react" import { getBillFromCloud } from "@/lib/sharing" import { useBill } from "@/contexts/BillContext" @@ -150,7 +151,11 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
+ diff --git a/components/LedgerItemsTable.tsx b/components/LedgerItemsTable.tsx index 3fc7123..e370aec 100644 --- a/components/LedgerItemsTable.tsx +++ b/components/LedgerItemsTable.tsx @@ -576,8 +576,9 @@ export function LedgerItemsTable() { variant="ghost" size="icon" onClick={() => handleDeleteItem(item.id)} - className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all" + className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all" title="Delete item" + aria-label="Delete item" > diff --git a/components/MobileLedgerView.tsx b/components/MobileLedgerView.tsx index 8ebdace..7ea2d94 100644 --- a/components/MobileLedgerView.tsx +++ b/components/MobileLedgerView.tsx @@ -254,6 +254,7 @@ export function MobileLedgerView() { size="icon" onClick={() => handleDeleteItem(item.id)} className="h-8 w-8 text-muted-foreground hover:text-destructive" + aria-label="Delete item" > diff --git a/components/MobileSpreadsheetView.tsx b/components/MobileSpreadsheetView.tsx index fcbf6a0..b13c4ec 100644 --- a/components/MobileSpreadsheetView.tsx +++ b/components/MobileSpreadsheetView.tsx @@ -67,6 +67,7 @@ export function MobileSpreadsheetView() { value={state.currentBill.title} onChange={(e) => dispatch({ type: "SET_BILL_TITLE", payload: e.target.value })} className="h-9 text-base font-semibold border-none px-0 focus-visible:ring-0 bg-transparent" + aria-label="Bill title" />

SplitSimple

diff --git a/components/PeopleBreakdownTable.tsx b/components/PeopleBreakdownTable.tsx index 4896954..0dc0ffd 100644 --- a/components/PeopleBreakdownTable.tsx +++ b/components/PeopleBreakdownTable.tsx @@ -181,8 +181,9 @@ export function PeopleBreakdownTable({ variant="ghost" size="icon" onClick={() => handleRemovePerson(person.id)} - className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all" + className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all" title={`Remove ${person.name}`} + aria-label={`Remove ${person.name}`} > diff --git a/components/ProBillSplitter.tsx b/components/ProBillSplitter.tsx index 4f28aaf..2431b09 100644 --- a/components/ProBillSplitter.tsx +++ b/components/ProBillSplitter.tsx @@ -59,6 +59,12 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' export type SplitMethod = "even" | "shares" | "percent" | "exact" @@ -151,10 +157,21 @@ const GridCell = React.memo(({ } return ( -
onCellClick(row, col)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onCellClick(row, col) + } + }} className={cn( - "w-full h-full px-4 py-3 flex items-center cursor-text relative", + "w-full h-full px-4 py-3 flex items-center cursor-text relative text-left", isSelected && "ring-inset ring-2 ring-indigo-500 z-10", className )} @@ -162,7 +179,7 @@ const GridCell = React.memo(({ {value ? (field === 'price' ? `$${value}` : value) : placeholder} -
+ ) }) @@ -860,6 +877,7 @@ function DesktopBillSplitter() { }} className="block text-sm font-bold bg-transparent border-none p-0 focus:ring-0 text-slate-900 w-auto min-w-[7ch] max-w-[26ch] hover:text-indigo-600 transition-colors font-inter" placeholder="Project Name" + aria-label="Bill title" />
SPLIT SIMPLE
@@ -948,10 +966,13 @@ function DesktopBillSplitter() {
e.stopPropagation()}> - +
{ @@ -1090,7 +1111,12 @@ function DesktopBillSplitter() {
-
+
{/* Sticky Header */}
#
@@ -1102,8 +1128,8 @@ function DesktopBillSplitter() { const colorObj = COLORS[p.colorIdx || 0] const initials = p.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) return ( - +
Total
@@ -1315,8 +1341,8 @@ function DesktopBillSplitter() { -
- + !open && setEditingPerson(null)}> + + + Edit Member + + {editingPerson && (
-
-
- )} + )} + +
) } diff --git a/components/ReceiptScanner.tsx b/components/ReceiptScanner.tsx index 51ebfbc..5567bd3 100644 --- a/components/ReceiptScanner.tsx +++ b/components/ReceiptScanner.tsx @@ -218,9 +218,10 @@ function UploadView({ onUpload, onPaste }: { onUpload: (file: File) => void, onP
-
void, onP onDragOver={handleDrag} onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} + aria-label="Upload receipt image" > void, onP

Click to upload or drag & drop

Supports JPG, PNG, HEIC (Max 5MB) • Preview unavailable for HEIC

-
+ @@ -341,15 +343,33 @@ function ReviewView({ {image && (
- {Math.round(zoom * 100)}% -
-
@@ -424,6 +444,7 @@ function ReviewView({ size="icon" className="h-8 w-8 text-slate-300 hover:text-red-500 hover:bg-red-50" onClick={() => handleDelete(idx)} + aria-label="Delete item" > diff --git a/components/ShareBill.tsx b/components/ShareBill.tsx index d71c5f3..14e1c3e 100644 --- a/components/ShareBill.tsx +++ b/components/ShareBill.tsx @@ -218,6 +218,7 @@ export function ShareBill({ variant = "outline", size = "sm", showText = true, i disabled={isStoring || !shareUrl} className="flex-shrink-0" title="Copy link" + aria-label="Copy share link" > {copied ? ( @@ -232,6 +233,7 @@ export function ShareBill({ variant = "outline", size = "sm", showText = true, i disabled={isStoring || !shareUrl} className="flex-shrink-0" title="Open in new tab" + aria-label="Open share link in new tab" > diff --git a/components/TotalsPanel.tsx b/components/TotalsPanel.tsx index 580cdc4..c383763 100644 --- a/components/TotalsPanel.tsx +++ b/components/TotalsPanel.tsx @@ -112,6 +112,16 @@ export function TotalsPanel({
togglePersonExpansion(person.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + togglePersonExpansion(person.id) + } + }} + role="button" + tabIndex={0} + aria-expanded={isExpanded} + aria-label={`Toggle ${person.name} details`} > {/* Collapsed Row */}
@@ -135,7 +145,8 @@ export function TotalsPanel({ e.stopPropagation() handleRemovePerson(person.id) }} - className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all" + className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all" + aria-label={`Remove ${person.name}`} > diff --git a/components/mobile/MobileGridView.tsx b/components/mobile/MobileGridView.tsx index 81160d9..0636517 100644 --- a/components/mobile/MobileGridView.tsx +++ b/components/mobile/MobileGridView.tsx @@ -440,6 +440,7 @@ export function MobileGridView() { variant="outline" onClick={() => handleUpdateItem({ quantity: Math.max(1, editingItem.quantity - 1) })} className="h-11 w-11" + aria-label="Decrease quantity" > @@ -455,6 +456,7 @@ export function MobileGridView() { variant="outline" onClick={() => handleUpdateItem({ quantity: editingItem.quantity + 1 })} className="h-11 w-11" + aria-label="Increase quantity" > From 1ba36ee67bc82e0c2008102e60e65a794b7ae828 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:18:27 -0500 Subject: [PATCH 03/24] Add OG image and metadata --- app/layout.tsx | 42 +++++++++++++++++++++- app/og-image/page.tsx | 80 ++++++++++++++++++++++++++++++++++++++++++ public/og-image.png | Bin 0 -> 78692 bytes public/og-image.svg | 61 ++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 app/og-image/page.tsx create mode 100644 public/og-image.png create mode 100644 public/og-image.svg diff --git a/app/layout.tsx b/app/layout.tsx index ba3dd07..4991a26 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,9 +8,49 @@ import { Analytics } from "@vercel/analytics/react" import { PostHogProvider } from "@/components/PostHogProvider" export const metadata: Metadata = { + metadataBase: new URL("https://splitsimple.anuragd.me"), title: "SplitSimple - Easy Expense Splitting", description: "Split expenses with friends and colleagues effortlessly", generator: "v0.app", + openGraph: { + title: "SplitSimple - Easy Expense Splitting", + description: "Split expenses with friends and colleagues effortlessly", + url: "https://splitsimple.anuragd.me", + siteName: "SplitSimple", + locale: "en_US", + type: "website", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: "SplitSimple bill splitting summary preview", + type: "image/png", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "SplitSimple - Easy Expense Splitting", + description: "Split expenses with friends and colleagues effortlessly", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: "SplitSimple bill splitting summary preview", + }, + ], + }, + other: { + "theme-color": "#1E40AF", + "msapplication-TileColor": "#1E40AF", + }, + appleWebApp: { + title: "SplitSimple", + statusBarStyle: "black-translucent", + capable: true, + }, } export default function RootLayout({ @@ -33,4 +73,4 @@ export default function RootLayout({ ) -} \ No newline at end of file +} diff --git a/app/og-image/page.tsx b/app/og-image/page.tsx new file mode 100644 index 0000000..64757dc --- /dev/null +++ b/app/og-image/page.tsx @@ -0,0 +1,80 @@ +export default function OgImagePage() { + return ( +
+ + +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ SplitSimple +
+
+ +
+

+ Split bills in seconds. +

+

+ Split expenses with friends and colleagues effortlessly. Assign items, tips, and taxes with a clean + spreadsheet workflow. +

+
+ +
+ + Real-time totals + + + Receipt scan + + + Shareable links + +
+ +
+ splitsimple.anuragd.me +
+
+
+
+
+ ) +} diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..08636e47c187827fa9b5e733a608675e2e012c76 GIT binary patch literal 78692 zcmZ6z3p~^P`#=8PW*c)Fn&db|Wm_eoa-3rtO0s#AGz&2lLP~1GkW$h;l}?DT19O;E zsyP%n6f%W$>p&$+D(<+e|9h(Y^ZorFd&t8ckKTK|uGe)vujlo=uGfxvZP}!Z(Z&D( zpuE}5*&6`lL;wI!kVir9Y$YcJK))2C-Td|gfTD)%2L|L{)B*rJu-VzcC-LIM?TlMr zY4IOAjz)jHF?-o5HuuaSm2x>#Be~wUY1-Z^4axE_b;?SGryJJZUcCK86z|>ba}1A< z_fa}JCmLBD6%l8p{92{yfUwH{X zJkt56Q|H7JT(#amwXl=o18(23mx6(p@8ajKCtj#{QyE6`Y5nSJ9;&xh zQ1@Q;SI4KbMm59ZmZjSJd}`iYY4__(ysB+$@BAv8ci{W6Lk{YZ8f7sBOueH;AC_l7 z@tOQbU3(3Ga_gd?3xh8c3bogi_fGqjth)LsU^H({`KN$Z&ntS@gDu9am~-w{npuH+ zl5a)#JUTTH^!QZSTI;a6(^O{Kdpz0LW%9`~-iw;sbD5LU?{0&_>}%8ayaz&gm&U^! zj?^Pqj_&c)U+CSivzyE$YuX;lDNvv+j%SVqIn-1RMJ{+33n7;S=Th)WRRvn7@ z(8D{i{LZP^+np6vkKg-jx$bwE6^C9n_GQz3gJ8oJKp&h1c3z0fUTRryHv&t<@6!h) z@c-vUSwt5$5&^*^2|+rP7?;RF^C~^;+nh2Rm#cnxW>{&(+BBk;2`0H0wXD!F=B2v`;JvKL`|{kx!AQ$ z@o-fM;@~s@X#V%g!{I=SubNFEncP#FO(T{~%Bs;_EGBG*zf*lh={+8`;a zE7tc%XWL7hGTA54-4yvNBY`m0*Iftz^pgXR+zF+x>S}CKB6Uit!%=zrl973x3FptV zxtbE*xEvUZjfl$24FQO{j3~hG0=@s*6;JKJC^>N1ZXjZZc3?SNuI0*1L#zJuYt^*-1W_nrV4Z!-@F|OyWE=xVA4Sve8NA+WAffEf zNs7f1<$@I~dQ~cO_ZoOCrf2uR;AR8IX>E-jdY@H4Vj0+bh`6p^vEtv7^|BB^s{(W( zob>lij6gyVz-e?&rSr-vOwe^VAmX4kAc*v#>zTKj(CFFTFtF6L{d%miBaX%8ST=3I zx~l{qW92=Ysct=UZ=5f*fs;BPul+fqVu&K>k8sZ*$fN-K`XeLki6s0^mW%cpcagz? zk#z@5Pa4hT1QmTMN4=0aLoOOTvOzK^Uj>LPsq{|-K>(Hh?HC})#Q_n25m{V5fz*X1 zYPV%w+b}}W4?HitGgzJQLyD1PiIn#lntva`$j;Zl>B;BxOuY&NnX@!3ILic2GOdUd zz2=E5edyxj7}38TfS|d5!vTQEwQSM?h+USBpQ&JsAZ=#S!gsuotF9@sd%$1 zh7~ebh;vjLfyJxR3#=;)0_>;A`ZfTIyISJEZ<$98r~5jJE_5IPBzGY1u8PRN2cVf} zT5O5QnO{V=C_<2i&^P#i;NM4$lv~*%Ei6$lTd9dZLESY8(9a(|myS&(CDN{PJ~Ks6Xazf`9-CgwzAAWL8rMY$JJaR&5NQQJY*=o?M?z#a8Maa5Y7PtX9Y%5@{$ z*Duxgi0dGF8=I4c*u3W?>gK5v251?|O|cSK7PSg=msAh{-_ht%6-^Wk`nPs~C=Yo{ ziPZI+ouoQBC+Q|YJi1@hS*LcN4IVMW63yDQ2b3!WX})&?6GOu1@&59sBk?afkD+z< zwVW|~+zNy240+6EmPOlYNzr%BtMHCBx{~T6#*Q2e$@&fb9s;a;)Yv|ZpItO0U$cV~ zV$$Lb_uC9x^(kT;@QoRv8uBW~d+M%6nIaS)pwu7gCbDSDTC$499wB7Q&YBe!Rz7t- zx0WTnj-!90!2}jsTm(xjclFTCL3x;{Qzh2mO=3=x-5YvHA%@7MHMJPh;&O1xp~rx; zb4TEXTx`HS;m!8d4THzFCFA11Ishaif4AD$F&G?_tM>S9902HFD)AOJX|J$q+o>pt z-3|xCh0gM<5=%7EaWwA|4%B*m`JSbtPtZL%2vQjRgp&#EA=^T%ibcXhN_r7}-KrA#rcous~DhR#JCXW|}xGA{4i zen4h*An)=M(Fo-2XZGw!-R{lvd#bZe@8y-Fn@$_X> z{3IMQg6VERaleR!2H(lz2Geo!AgB@MV48X){pW?$6NZ0(ObDsJCUoJhD2iJC(++7U zw=Y+4KMPgU%krOYReSRt#pb`I`X)&Ddd*T_0CW@O&_{1zA~JCbo>$Pa{g?xSAPPY2 zarm%1JhMcZ?q}X+yPc@U$Vp)Wft?#Cmz9uR&Nzgt=l0r9O7+ZJrHsW>#|7k z1>BP_@01!fN@?sDI6yO(Xym)`GKYgx==!ywB7n533PV%`i1w+^j*Ha|$LKC8+YVkx zEdi>$I9w?~utAunjqhJXo20o$9zb3D=gRgjJT1yQyrg%F)-!J{fO!q)7x0)OT+*G- zGKcR#1m3>KrX7Tf<5;BaUhZ#V+*64Ns_)Qww(ps!^*d`IS4`o^5li)>8`QS;{RttK zO-rw;$R-s~B&vlX=s_y9`(t!*5lj$Q6XP+(GlEbf2*OY7o^}cNc!z*0R zRa!PBQo&8xWdQN-8d2|FV~<#UO<1b!+|d+3j_)RZQQOO`nq2dcen0~s5#p*@*^=Ki zFkU01996kq=Z)NaCnl^UFsei`;2TXs92TZRbi$3ZgKP<61|~SH z9J_0;VGDj11o&swCFA38@Xk`nbT*t+tER0zcKHylf29T$JY?9PIZ4fkJeA6W{X9dc zNSQG>D7r|9t?`OG?BWQ)(kaHFnkk@VvQdZPY@2*oCRIcoH%uIuC z4tZ!hGP9{cz2nPo3%uTA>}YQdd8CTw7=5%TyL*kRjv-HU!iiLpC;3(7f<}krUv07{<*IOs(GGF8!W$b}tc%F%e`)+4hZ0l>;sm2Je zxId^={Y4^pJTBPG>6Z)cLN!%ZM3x;jZIH)v*gP*fZi)Y>a=^E8i1?9=7P%1=M_`nT z-L7H3u+*y)!nQVBCAv$-wi_2xA*Rs>X!1l~I-A-doXf>Vm_25iwKCx%*H5ThJ80P@ zVggYvG^%UY9(awL`KI@I@7mKH&l#DVPj*S6QY6fEx+nIZtW!kE5X;N3kibIEA?D-^ zqSCJY$~(EUB&FC^U4f}qiTs^ld|tb*AUZQdpFS$i#!|uMzDDM2yNBVK)RG&tGfe&i z@(e5ZXw=Z@(@a4enbtX8hCfHW`s!k*;srr)naY(~GViIRJt2BDBGtF_iD&f`22fPM zsq<12f#zpjR}eB>iIfAsX}{LrfaYJJ=@iy;uu*B#0P(a`eia zo0CSSBl$ULHfotA&_;EFjZroI63C=6+u~^!@USdmSJbgXgeY1p^J_gVDuz>5bD9*XLd)~Vi7X|{l9fEg#4w^AR^a+x%{b_yY5i-}~V zDjy)vU}z~~C~K4mCs8wv0r1l6A$cm$ z-~WQ98jXmOcbeMz+j14-MyxvA3Ju>Tz;<-(Z=TdcdDehaTiJ%w~T>d^H5vOWh* z<-_Zh`Cs6~3ys{ns4Fk`R>CXaTDc-M)fJ4+AV0CJwbt&D1@jx2Y-vLP4QNrHXboja z-Jv8BO?5ENTJCsM|TD192f;>Wt_8tu5Qi@=+_ibymuTv0* zc9F^3sl%&+(`mhUcV*@S*lJrmv9a-^RUy?QS{zk7PBCb3R5VmKG4e)E1ICGA%gm4J z~S)lUnu_=U0F zdWvLYOm%LWpGgHu3m3=Dh~(Rh6y3dm+`hJu>oUS@4U`{@vq?eI^txS~kSJo%Uc-L5fJu#pA{^@M7^8f}(-?kLoEXPM_$^`k zU1(|aPeQ1a`CslzJ1AKYTT*fG>u`s@EZXdwndGsV z*7ul!jK{O3+MB!|eJ<6$!ZWCloTtHij@+tPP+xAHT+myFK0R^ydvTcyU!fvgmlyhI zS8k`J!;q)B3iayyH4%ZQEmWv|b#^f!eZ?3;^w7yQ-HD#!x}&^@GVvv9UR}Eq5?oef z0Yzg&@)m*KH{NaS%k4?5__t1diy^JqSA)ukO7VnGKDkFgZjK%D9K1^4=Rca^Jx*vO z2+iIex9~jq&FjD&tHS(*L`9tkzK;iwFeV}#hWfm(FSow~$193IK6tQo)93>x=gZ}N ztN0U6{{~vZSLm$`+_=wL`(M+ZmRFiPK{)j;06 z&$6>;Jw-5q4qrJChK(pNU?AZFH*5r{@-KjfZ=uN3%N+%D7EX<^N{r(Hf<&zQVu^nW zs0QTews<4K4lG}LF~da;tiscX*;EBsRWVGH8tsEVQ|GWG8V6K|K8VwIpcR=$AiA?u z>T-ddU-UB~FQnhz1Z;oT%!@0Kp0pTY#@@a}^MKP09mu)zs&a`eK$E&&Vlai4v9Aes zEkFz8i}GOoix`_DOOf=AyIo|6;)tWo@I>-fu%{W0N@lFoz7FWdGl!@D?PaDH){=9p-uTIxL zCfWkC*TfvmF3|`!R;ckt`m4RjqxFz@?ZKA)TX-yjF8D5kcBf>7a7c;+@{p>dSO(q7 zq#TJVuD5|%*UCx3Exu~??we>(?_Eel^Udevw zsjgu!OlO06q6w|Hmq`*ch}=XHgX-|K2C0I3c9njnxG=vQGaVUmuMX<<~Fwy6t> zIiPqwA#@cdqIJa)`|x`SE0AF!I`u^n@t+2f5=S^kj#r>B(Ix!?de|+<2Z!Vp*LCVq z!E##Ay>w6weG|bLRc4sfYIrZ*q32}`ee*-kiuC_OA-p&xP`sEyq0}!cDb8_>6{>on6!x&0 zd|*4|a)Vp6G#CmyFec}X6J5Bzr+rO2E!!w^p`Psiz3Hqxh0NL=OR9%`7={2%fy}>D zETKA5Wo6W&98weF83L+dp22vc5%e>S5w;RSaIX2;@m`2`l6->vhA8K(Gc8V5nFXCT z>|dVfGLnU=Qw-f5o&sv*;I=Dtwo!`jnW}da^!xIC7@l~|E2p_|QSYvh(G_ySUOr^J z>JdX-N2SP5fcQzb>486$1bvrI(DRHNS24u>RFI?hR==@9C{{$V7ENoU(eB)5^20_jtJ2&wb4;5MHV*r3 z08%1s7Lkb8(I@K^o+pr^qS8zrHZlD-C_-9mIh>>+O!G?xUny01oD0b;;g>{XM9wu; z)j&1#L5iRysDDwI&{62f7Pd$83TEp}P~nXPdt_LNPC)stGa#os1syBD*7O~_VqF^g zg@a66OhZrG%bYE|(ye|819G{LqXkJp{t&&mQ{)XK&29>4fd?!Yr=bFawP z_QIg2=d8s0rJ?K5Z#9lqp5n^CSU7Bk>i+Acn1~=1&CsAFkS>T~7$ZbkLzGMkX~-}y z*rIj0g&M-nT&es_-b;jqCBETaI)2;U2-duc@!(mYR;W!HUMOPH1*@CfVh5ZH9y zD>~>~azBTw-8e--26G{uP~+U_9`#T^bgh|jmeh0`o%`j+-=3mDfa*-$ddM_F)JcWVSdHQ0SgRgtz$mFPE7ZewypEnVB5PhbXbh{2yN(-2 z`_1vId(-?_{yp@^dyOBCr-l3aelsESgBxWki?NaWl*C4@cq5pJu27JjaTdDQVC(}# z&^@ulvsCa-JQe(=^>*)8q9Hy{ixHDUrDs~eJg#O=QI75?l;S%~I|r4-Uc!5Znvj~Z z;sn6!&;r9vPi6BiVgZ<%r8h9x#{bq-gV8dsTha>54(-yOU zY^p&OB0q^bdsXOj&b*DcM?Tok`w~vbBp$~FynHd0BsR2f4c%==5w#OGh8ZhXg#L+a z!GZw!&w?n2C$vD>{u!<5GJ{xHM<6MpZ@#(1HFa_thE-LYKHFKMeVnFqQLsTPyu`8V zbCm~6;2e}UN_N+aE2%8|$Y$K#p(h$1?A8_WRv?8FfDEGk%30ZnC{uiffuH zo0ed~NRkFQuGKG568sxnNR_Wwk2-}RDe6lMTO_sr4;c+h1#kGB?p{O2(eh9?_XxS7 zr?M^d?ln=Wyk8VF`C@8(5#t?`*r!}=rd(qQUqxOORZvcOOXY+uQW{}|nIME@;vjObyy{efwh#SA9WUt!xZF3>W)+mWO*CZj$1WsAzE+qdKxBWlWC8XYht%0>pg ze+PuCy~392p!!=UshC%^-Puorc5mR&xA_u1(81yR)_4@&>((wPk*lxvd4N0o%;DpH zocE{w+x0}>`yM@NBZNT`9<;4(jzw~+q=NdZ{->Yx=xKO#^0aVQvcAkruJ3Q6}z3Y%s5~=;8 z*orX^3AkIez{%3LmBj_p-aAfdG6%Z}IT#9Rtev<2RZ?-l2LS0wG%k-xEWAt*6xFH^ zu2)sWy02)NUZ+x(8)f1l5ay)Ut!X6MFT)dEWA`oT^iTKKl<*z1=mTCucV@g%v zOusQn+E49HR51$m@Wd*fU=dn^V4g>Tc0+G~h;s^{_zJw%fAFZYjFn214N;bfl25KS z$JQnP&Pt!~v^bUp8{|PyD5Zi2_ZcgtgRf3F2x9iqmYXB1vaA+&FXp!Wwq+ivWIZWV zGs=yN`mdC&?xk;bAEJbdic^_Ki}D31cY<^dqunXqHCgjLKMS>gk~$wTcve%dnKenB zi#+R-Fc2tJepU*`ky0NVsSSg=0m+kttN9U|T(tu?U9UTNyL|3^U&6rx!KHSu&9|gI z<%GxYBUKdcRhP8$g0=i%bcG?+UWD(#RO*=)SUW>0U+-+ZJi{;gw*(zyW6GxJypHUb zBXEdeS0*c32Zw*$bewrSerWsGW#t2(uKBawhodd5o~O)BGm{jHj&?-(?n@$$yO;M* z(-z5<_EVz{!p0S;;fLIFhZEv&ZU4$4tZ&$Eu^5Lcm;Wcxa8N1hBoVA0$6PEuMTttEUcR6#pWs+W?GB=&Za(8>(>o&Gy=UH(Q00JxO zpO3H)Cz6%v2g_lkNi1In$#{SeT(iuS`euMp8d&}ntrmYc$O35&f>bCGK3bYNT7A2^ zW2SV<#IA*c3f>lymu*TUboYKbG#2CwOnGlxN{%Rr-HR#H#L4lSTHes1f;t9IGl7e$ zasJ%UsK&>dL4aV<3u>>VJ+2xEu|{>BY_g{OaZ7rHC3cm0Iu9%v*ba+AP&- zdAhRLN#N}a;ueF(D#Cv}8ou601i-8*%KCaU7+;=7&TQ;Nbk#NUMI*luZu%u#!3X2D zCZLqN?SlB`BU;z$&l48Ee!w4Ho%v(c^_{}eincS} z;&&B9Q4`aTdrb$wl`@)s%V;3V_qsKAG4IvQTY~#VO?Vv4V7>DNhn}D=TdJ*_zVFD4 zg)e^JRTch+a^{6aN~J2iiek9L``)FMA7;F#`+W$v1U|=>?%O<%^L2QhyPUk)rF1GY z)L+QlM{TvOOX}P4^T@yjVwio81$(P?Yei_6|Cd~EuhDIBQJZ?ch~pb7qRe38xD>&+ zhV>zt7G`h(bohKJ&4FMep{NJGOQEs0kDV!4nn@7(KPHeCT9Is^EzBi8om;8z(89Y7e@# zX&6Z4dgjDi?IUaqM{LaUB39Cgny-81YoPME?QU#0BwZ&({bfxzMhF2BqgMGEvcki6 zo2UL<;nI5rJs)B+L*_Qfq%Rga?$AiB8G$_9RW$#?1jNv~?aK4T0|`%zpZZ>%BmpKH zr8^#`XpVSW2Z-}(@VHNds#Q?$x%Py@3PUy3L#ijk|qlJti9lHzxGQ_k~z*y8Yk>C-x2I+1cFX zQ{nS%J%S}~W7P9H_S(kQntb^5)+R6UW3%Z~if(YvPt|@0DO@mE?CmIG3iZ~;gc++= zV0N)XipDAE+I-mZ)=R5OZRh3s&m8vRU{mR;KKZt^s2Qw5z%m)ITdR_q&mhH)eIC9z zst|s8<68w^(~+vJY;u`l`OW-1SngcH=|z~TCY(|MDGz&@^S&T|XAGHUQTaWqd4|8% zWxlMtlgQMWc=$6abtV>X(OvJC6!X_eO!(0&Iy{BhQtr_&vwHa@w0D~j{%gtoa})dl z)7clnbvpY@bTNg}qM6#tE$0q%sZRr-b`-m};${}2VuN5h{V+E2wx{a;oe5s=tG!E_ zPrjGa380nI;LlYIuggJ{9vZX&ARP?)MW)$wr%W$Sy~j~_xmR^zN&-IxjCP1;iY>m2 z`ghNBRfyDE69t1Ix8&Z)M^n*^0v*VoY;e>F&3rX1QrYe%-n__qp!ng-yh-|6vW}ltMc*3`P!j9Z?xZTm7t^fX}lcm+h-9H))+K(y$ zqRwC4tlYb;E*y}xNHzu`L<6QCts_bg*`M|U@3I=B52wdpgsssvk=~4~q^vrYY^NBd zQjuZ=t8(14`_q+oYbw3VmVb=Jg>dQD^oHV31%oe) zPdXhv;jD5@7tpj}&C9F~FqUMi%Q7Gw2PS4GB7!j1Yi+T-zlIX)o4(uygu#v;_3dT2Nwm|HPs9U!GlLaN5PUZ zC;(3K1eOkv@w&Lagpn-g)PYmXop-dm!%p0wxjj`{FF97`yRp8n?8j{6I%3QEMFV`^@CO^O`N;Vj`No62({W5{e@DfDApqc`ED2 z-JWZssn&<@-ZvP7#>D`rs!4`4_FObj1!MtPvF~AzkL_KXHsCw<2o}5)WxKD%P?NfQ zTgMerixrYaB;vMMu&JqJmT?MLq6Fucbz=Z`82v#LahnwP-Q%EDf!<__rqSB&Lyj5e z7D%R-oXqHGWoW@UvIQelgR6u&s)s7O3uYS^fj1iYnz@M)Bh{9Uq#tkaF|2sg4C-{1p@y6e9R*6~Y;}wMI11)f8xHBOQol~&jVmW;QB>$2enpz*cif{qe{~X>z zkS4FzC?G_I#Xp@I*tWkjUe@f$GV)`UCwv(nlb5PduNRbpSe!?21PgU65*_biS+k${yB48Sybe+>)43& zkW~7vlUS>~G^o?LDHHQYQrJIhkIM{kaD*h~DS)h5pj4n_Lw}Bl+9TNmnXP|F;R(5) z(7^M0KN6Q|odG7>?TA1LX;g{=eN{6rjZNK;11~x9U2vLoV};7WooA2!tY@<)-R_&^ z=5)|>exb>0DS`z8hO8jYH0xdPVp*3Bn#@7ndOV(Hy*=|@kn~e|LWkngL~+p)$xB{h z0+kPsVaqUHr&!XgvJ~bvy%d@vQ=>MYCyl?sT$RDq zu}xsiu^}-D5RJf|LFP&XupMwZ)D2;IjcG*6f-(22B<6PO3{PdqD&MV6JwnLM`MHCk zNpotz1dX((nNKBeK4O)n(2h|MSvKOeX=oN!cl_a-AnH6D z>u&xu`39!r3<^X?6c9o{3+{&jzvJ_(btS!0E3JToqt*UHLELMpX$lJ4>ugwmPMJO7 zL{iL6lwZ5@CK^y%c&4ZkF`C{yL;Rpi=Qs+mESH~m{W)U$C0J`tRJ#UBbTgJdLC5g@y5OgcV5*5o zyTT-%&g~vgD`W{QE-M1=+u!4l5i0ZbR22}p_a_dHoIw8RR9-+gjiinz$hwFH+KOZ( z8L7WA4Lu%pYSK4}O$DtTh-%aA{0CDr4}b&y*;8LW&ZB^SD*;6x6gu*cy-g~UWKomF z@|ACYdL@TARR3?Wc;!D)17va)f)Z~*+DAxo!Mp$y0k4p!Z;ULRcz6#jepCxohITWr z_wPwMtU|*;J!M@{mUoC4Bh;k+gCXfn@djc*;b?4AR*S;1?PE6h3s4r7O%g5YK|r-F z3uO7Qd}s$HaZ6<*!{oKLUJ(Z)E5}8`SI>3nP>zG9Bmy`gL_yZ}5ZrP=a1J;=A{ERr z-_DO2)N1s;*3epceeH&-5v^a8Z-hNMADa{W;f>55M&s|je82dnT$N4#1Xfb? zViJ+y^-k3D&xsf>PJ+D=82}-4l7|fbx*Ji}0uO)(R6j>t(>NfD>j|X4(@!OXu`Q;| z_vglfR#m!gc)a(N2QO80|0^f&wpP z{n!`a!7?7QTgkh_tkp{h__(&x-titTw{l|nE&znG_5xF2O?=nJDvIPqE4S+dJ`NDo zv$oB#l*HavgBWGx`?BjOk)@*mV2Q88t97*O1+sf7~IkSttPa&*;P7SQh?Zg%~l zB}{R_fN6i3@-aqGJes(#rGhgbv)sXAh}z^4Ix z-obw$%m@6&f@DaH3;yKrL27sFYuWl;+0n1E($=xWK-G+V0M+)nRlPko#>-f$QikFK zqVyG~Vg1*jle+z?irIl|aP&Ss+@g;A&z$6oNgD`N02knzO~NEvvdNSV#AdjF@;Kxc zE(0NO$%Nnsf{^m~X#W^->z-ZPkhCk4Ps-sDVM??Tq`-?^^3<;0KlvVv))DfxE~8!M z3m$iX%HLdg1g19|OD4U-LQViwccwyPq#`|rG+!SI&e9Ry#KU-uXiL1+XQrs&$W5RF zFYxb+d7s9Qc6#=zPW5sVU!N}y=znX35asmY%QqwGFvbi2W-$sXG6rS=`~pj=umP~C z5R#19=EzBkUY@lLWJ^PKwED26yN5#HekwyRupP=MP!VFhBUE+39ph7i;A^s_JV4|# zgx+EM+|KPMIuX*5t&8)f@Ou~VPlEV11sOf&?JuJjLPme8RF$un;{5`MP-7rr0$XqC zb~NZYPp4{$^22O=gJ86aGY%=f#gyh3_K+DA^`99+tvKMx0-O#*Oo8Rba=Y;bC{Thx z>h)E1zI3wYcC4vHoffnT5Da5r0*4Kf+;pFJDTK8Ge-w=m=>?Gn(5%6dqf?wL4TR6G z{~UGm@ZalA9@fcV6cWA{P%l`Au?jBO5S5l>vCj<6w>$xq_oAs*EUA@5`=V%R`J0pV zHy=ht;29Z^Xey>Lg&Q3-9v|s9Wy4onQ7Z6^^*dUM(u|9O7%1R=3|89ItZ4$qgqbp zuYhUmoYA7WAZP$+2t+`v1JZWOV7d~2AmZZXf~klHe;uhi5vmm%Bcny5ENzaCSxPuqCH0iG_xS7|0|*iA+kwU#+Rew-mh9 zchjRII~xaI0sMUi#l7-vssA==XiMd(*W%@6Hf__#;7)*hx5m+-7ENh2LU@h(jbf{ugkQCYL-D$^% z-*v=yyb>{w@-|9%iJB>dWYIz`7?po?p?hgBmk1%V6%-ll28fO;0tl?4zDSM^3zfZ$ zl|_S*?{o09j3@)OmhtLKyHmj1@<4=ShFB5g#U%-rQU_NAjFw|;5yXeU>;@L`mm0=95L)Jg;^I!GW?*!7xrfO+=PxZkB*}Kg@dlVrr?#G zN+$9tFe0Z42C$LmLiCHt129moLh}WVP3-;fDSVWQZ{a-z`khO7t?jGquR})}0hRRv zvOJ4ZyzKL_75^t?zYl~S#)4%cRfCo2g;t7mlaF-IHIgC>X-@^@Tmdu)C#Jz;W4k;p z5MZn4-`-w01KO(aL%N(O!@po)Arad@2wf2H4n5$FO8hSn-`*)jK-Y`_1{8}ZkY5Q= z>3flekyXM}%b{yazzn@Qr*)!}5LI2}NLnk271QWlcGSAFp$1e|L8EYs4|EznJY{k50wb+N~dSWeh>1l_h z2$`q<;vnRgf6lPLR9|b)1PHbCTvf~9f+a0kaFNP2#52|RPBh-v(&)COZY63oPxCY6Tw8@qjabnqOJG3Q`ScZDGq8R_eSW1Q_^Do#SV{3Y=z@(v ziOM+Las|CAy#AnH+v&7tIk9A_ldO7Vp?fpHqDk3Spzh^s*kde$uvcj*Oh7p`Oqm)N zHH$r^zOA+u!`02kC@?v}tJL0>wm16(n^!FJs8AMlZ(I*_i=g>Vv4&J83jLy&$L8-4 z+N$xNWSA+@{qkV{glDR9nbz7&xM+qjjAdE$G;l2n7l-1=0!M7Div=)mI0-%dEhv80S08FE#Fk@J}lSWN0_ zw2ix>L188i6c&@YM?i5Ib3RRRRT?w`U_SjYeFRD!VlS5~52UMZ7ta+Foir5kw7 z?4~QxskR9XAX`e&_A*v1PgzJcvP(&?jR)i!{_}yzDb~uM;7;{=z#|@2!foRmbaE32CQM z^}A7;>Lw8yqrMdz9XCXUEEQjpg zMo;P{+(*0#3V{Q-Cw|ZcAFL>B5g&?sP zx?Ab@86b#&>%gsEHVDzdB3q`R&4;Sw=XP&J>KWi_>oN!>HZ}cn+ADQu&U=g!(<(YvU?@#>`@y71_j(<53bKvhX88#1zp=I}m^&x*@ zdmI9Qjys=Wqk44lQ_HTqR$(=I`A6fQZi9m*SrKXly>dKFx|GKRIaq*rEaO#USZDsQ zoi#6hAV}QITa~d(5Dp_95;7mZsPt^@p7{E0?WmR7+CPo#DRP1ho>K5jq$gj4u5&`L z1w@}mdmTi9R3VG9tV~_P!w=v8biYAdvC==%z*({IIhv8;EY0g^RAU?QSZ3c{TFI?u zu<+G7V+z48n=SwBLZ(zDzEoIH|ADp;ealf8UC(D;7itcbnmh5ZUN{sV4x>HNTarNm zz=5M>%iZFAc7tQKU;`;Xz&bb3jEML zXZJH_C*baLFn$A0=oqG4(5u8tu?W5X&T83M;++^#8b%PwJnZNmXC>&3`{(bdpW3Ik zf;W10f335=-4pD?`Y-m$fsiet|1>NhlKv3E3d{odz1hQ7iEdLo*Q2w&Xdk#+3EIaK z0@%H;RJMSvdTX!mIze;PTa{J|H>4R}Y~s6~var(c=zVg2-JT7SeDd`ZE3~hjLjHli zQ|OmuQqPPG`oQ-vA6Za)%@uegbuNPo+HSUL|Ct3?(&CMfq1NYXK!;LsFPdlB%p@g*I&2r=Jx_1lt;{qiZr5cR zL~azgPNsJ0^PzXIb`T&4Y|UOB@zur-A5h#Nv~+9kt}*>g30y#s)ayR&XnQ1N7>M}s}DZJCo!-{dX!#meVyu1!vm@o}@7D_TI z*Mw>FeSzTn8cIaNw)1|4reyKM%>`q?@xHHz`{R>e_p0eSBM>y+hKVw}PV-{*MuMRv-)Chu zxTY22osz6w$?@RiG_ShhaL>GzAmM3ra14&t7yN7VS%!m=a6e6>nBSIW>2{{6%Ymb- zhX~?&O{nj#{W5U!b6wLv08x)z0_V}Fmzc`*jo;4%TvvzF?h%Nv=jVV8)_?&EHav(Tvmp-am{<@7kiJso`Dd1Z4Cp?0L9YUDi$!3swq)s7n=A3p z)H3|8u{D6aotvAt2|M1nj7}bZu5o+)<4?-TPuKs4B!CSb0oneW2hLugz(^5*NrsG%VmdR6~cvRGIgnB{c zGiq6qx`l-JdoUWxEt%*-NQ78H{bSP*;P}`s@zaMvJJjemCPbR5^mlL{e+MZ>fa!H* z-80Nv9~!kMOxFrVA2>~py9B%YHtw+PM8_Kuv15I7&)+UTx4b(3T<7+t_v-M+t?*w+ z2ZcopKCknmnUU{rDPaogZsiapv5D3%ma|H9wjdEatsDy6XLF@vJ%PEbIKlDphyov+`AB=As?HWQ~1q4iW76D$?iyYXyO!bJX;K?I9sIaRxpbyVZ z?}^5#(_Ow7YyXBf$Z5YoT<2Rb=Jy+JU5GsoO$+mFPjPDj!4F+J^-TJ9H9~nVbC)3g z{{GDfQt8pAjGMQ3scXm6uh}~`)N5ba_#&T8^8NtppBT$%QCN^VASY?NfDYFG5kHBRZ9!Ip&cd7k z_ot3eFFcf3Mb=C)(pQ=c@lV|!=kc^Zi}gN6GFlO=bknFIND^;@XUHsS(`3s?d*l}-aR zcV#;Dl7b|^PGhe>r`zUY7P`C^PEuEnTaoD!#ks~_L=aW0ijMTvbCp|{C6v1VJpFV2 zX+$*c8V5Voe%KrN+Z7=lbIXz6`I!9@MtVS%Kd2ML8R0IL7xf~yKX4>?-%e6rX)JSf z>z90ue3UnoiP2!6l^gJ0;BK(XhJ{Sz6ffDl4styS!sQFg$E+aS{?EDC(0d4JL+Di% z1G)mJuVmixz2IeZn8o(LsPa(=pQT1O1;XZq%P!mA1{G_ZMl0 z_78mlN;eF0=bUr!Ex*#qAOeylW(jF$XpSpjEH0_Q8%aF|kTO_a_aU89G@<+Q$V2Jk zDue!gt5qcM2D2;Xzd9BY#7rU}1>NjCS6unEK23pRy&B!~O_1JZgdk4hqn^X8S+s|~ z%0ckX>wxx$z1mw~^bifYkTAO{4T2sGV;SFs)&xo5K(HZO&UxW4e5$@1g09yuPyhPH zYOhi(s%(pcboo6Lc$^E$)p#HXK6GeGP&H~k68Vf`GYw7b^e_Y)yaM~~j~FJE)W zmRV!#&z`FP|44i9c&fwqfBYOf3MCXFm64GODLWyf5*a5MM-o`YNQHbm<^Ef#6 zc8${`jQAhPF=-%eB|)(#!W}P=LeuCnesF3<6@dk4^Q-cA!vXIT1X1KeKXrX2efHg- z{}(KR+4>*_vZwzcivpyO9UF5FNj)%RcJBM-r~s<0EANnx^U@fhZ+BPyfHC$+QG*kE zBOO>>*A}NL>B^OMoqREUyX%z;)rqeStEY7DTsla`Z%{ng8&mUmlS>CXv{>0T#It9M zN+b{O>ID<4WY|?XY>K;Ia}YWP9>C|ab@WOdBm-zB z@%#FH)w=3UBn#eFaE#>-Dm&eKk0 zWoDJh9JDP=#l`T3gv}!wIryAkqSfZ~WvOLyX1fVdyjX3?zkoPdQSD#YQk@P22g)M zWoFyjFW~W)f^r~1nMZnOE4VPyq9goEIV>n4(3uGDCVRF@bnz(6P< zN5A``j9}cjIl8iQ_tA4Ql}@MN`0cIILV_(X8AMaPc>h?Z=D_=sN9q|glR~rS^kWGP z8GKBjOkHk;Q+a3r@;lYcA_$TPYkxQ>Y`8I(hRo`M&GPLn1xgE$qYd!=!8c&R6BA(x zc-{WR@*!}4WR2@5>AXKlOQ84Ab0uCd<1fX^3&Qe+SFR|Xf_~~tCCSu?%MZVR)dl$F zr(Obh=dsaQkm_Wu+bsS^oAbB!@uuty>rn}l=`e<@V*pCrY`wTww>{pOTXyKz0YU|P z?6Iyi=E3PI*Zg9mhdGomegfDOWV%?@G(**_?cOmnhEQEKt4*&9vebWd!R1PJB@*an?JA=`+vcDS1Dn{PeR!<*Os!@cLWOJs&;XZSo)sxL zX-qQ+MzinD+W!onWi4A+Pl1~vNmMt!>*b0LO2~NJb8p%n??~9KqZyQtcDOezfl>ma z=wNdeE7OtiK-fxwO~HWPkpU0I2&{?wIDJ^X|bDm6PIW=kiaGV`lk|3q<9 zAOa300A9kcy+%-;14BfJ$i}^(VN1*-`s{jB_^CeDBX!x2SM(50y_09$CrnMsNn8BxAoB8SweJ|MFAQY_L zj#sW_cwF}QY4MW+&E%;01})?{%|dHk^R|z!7%YH{NGaF*p}3zVOf$3`;#Q!3P{IYDyVAM3N4dXSo@6fa&dIL_Oq%Dslr$1=fh6Xw{N?OMr1Kkt9zvpLTC`hoOjMj_oXpzCb+G!y6*ls%{N-4^-_(PC$<@o zDoHAE?tZGO^p8jk}>HzL!4ZaJUZ&DIF zs6~#1<~{f%pKt-JX%xcuC<_?2v)=p}mo^88UzG%VPV$0k z)8-+ojnRC~)~W}^`Qol)e4xq*|z@ck~#Gv3pp2TDEy%lDI z2Ewju-aJ%=>}LvzKn#tdaQf@Fk6EBOO_ADi=Zonn;+a`wZKj6)AUM@0munFA*98EG zINFHDY%~?L{5lBrHbuirv;V{b9uiE-++{|~fv`&hzE6s!b_HrwOq(ZVGv3<=XU2yh zyUm`2FB<=~tJE{VNlYvM>W~LCxspm^fQGllAE@}4gDm1=97y>r!2MgcxScuRl4i;5 zfT30{pgYGm|3{pFq<#eXsXx>BYsamEq2&KSo!DDSC{4~W#N+>(ZM#J;NH%?hgh!dg2O>_NCWX-N!!S?o<1v@P2tsfEOJ>`Si1|2_yETxMTwSfDz)}EJ$!Bfy839 z%I`&G_pr>-YJ8B5(*jwf=Sp1MSlijvF+Neu$<{MKTfgG^twYDBs~fno&HMd34@Ccj z_a|N`pyP8t>-`E^LY^ZWpK?9zOQ_F&zB~c8;jpCP&udcz^+WiR0-g!J=E7d-Uh=VW zZgGpr2Gq=y)IM8Z#igy8-vKO^|C+xN?c{GSe8NkfufPO%!WTWEQ=X@Vb`lb#u`Cw; z&y-7qb!pH_B0uzTe7+#aCc8@Ps0XcG2JT198QX{LmqW#_+5Gy`N!p9r1%N{mApiNY`;maY;IC1XD8DMRa_S>o55(=~PQ%r9#z1)?=*w?> zPn^(`z~H6MQ?Lp63h2totxJ}qr-XK|WcN7i0Xf~6g|hqf?Z&gOiJF2ySNA=z_#&3}5BZ4z7jS5_RxjsCV)o!(8H-?K_LxJN`6qUr;;~ue4{?-+l|9=) zC8}>bwZr`IT~C&I?<;+0?ADe7KV83W?q=EVCH(Tr|6n~QANMFBBG}{1#=VTo7W0{{ zDzi5R?w2<^Lf)1IA=l(ZgO|(360=7xpxsObaujh58lTd_FCs*rb?rW{6r1rL1F{%; zarKi?bdnqZ3fGTWkoxX^kwVMxukiid$3H}31UEu~QK!;hFt_^a9f0aU%*+8Yn4QVw z8_F`NL#f<0oJAm&EKOs___KK}cGb7|(5FY9O3$Pp40prx19 z=8ZoL16OEgG^%-87}aBYm#R&Ji;0b&%|2~$1BYN0{R^%A)2;)v4`IR%-Ai4{_8rs# z1pMyVMzCf%B}Daddo)7ePy(baF2+G3yBpE`Vzb~TLtxP7pm4rCP~HX&HQn|9vi4M3 zG%d@K+A*NeGDCfy`I0~aF9%-k=MM3uMwwVY5qTKkmt5)&t8pm$q@`0dbeZkyYlBJV z^XwXbWb>atfYcO~0TA9CPd}l;#vm2x+g~+1zJqAq?Mck;ximxlgdC~L(iJX&0Lt%< zp&>vjapbYN2+MX`dj0hlmILG7kpK=-HX62PIkPW|?;nNMc~k!~%KtiTKff~P^gbQI z68IxV2wA@PN|GFXc0298hNU;8kjK3s6|jcy^-_>U)|GZCa-b(%>p05)!EK$ylN)~t zK!Yrtq-aCMzg<@Ewh)nEhd*|+68Fq+*?ndZVGvtntqN3f#Or*8HuI6iJbPbJynUCXtSi0+difGre5!WkL49?bF zx6?9b)6Oa>4_emmUvkxWEx7df9f0`}-j}YQ)w>e)C3Qh`p}4B{J*H$4_m>CpuXk={ zm{gq)O{Ld4(Z_A;_~<)d(iZaNRcyf1`&^`1b($9tq73)>Ni>Ti>W!FAxYpaulK-SF z|9KNcC2shXP#ZbUoZryJRihbg)NXNKaWQO7^sgnz8%Ie?BL`ugZnd;Fe( zAYefsqX#3bR*_xhe7&e_x5z%0D6dzCivQcc0gd^LjiEso1hhwkI0kD`y?m@?MQcZz z-jHG}h~@K92lbyGcXuc_!HVPMIceQdvJJ^z_HHPm+1L!#j4Jh4#(s7qj`o#Zg-M=RNpRdy| z1Bw;EUa=MR)`Er?qcDQE)LLR7x%CM*5_B6udG z&`-tuk8c7bJfK4ckkAv$vK}y6$4~J5^d`5)6M1aTBxFvWA;@SV(ij zjX7I$x%@D#o8$Cg-q>330|j~^OO8m-Ihy3*@#6%#Kv-Oj%oEWY9E6{ya^t;H>%{jz zy80eU69&H@NNGhL_ynNpx+t*0!*^-)H>u44PX!_gq5?=Nei5CGA{>2D|5JC#JXWH9S|> zB7}ywIMpBuP0b04n4@vNyEDitNZ*RC^-f!+`M~aqTKZvn_ zUnAo;8YU9r*BO>CU>qB9KVj_qxU8nxYL0!E1^0^fQI0Q%4U{C|E9^(HCwvrW-8#;AuiHM9xTrLjd5gdvCdNr z@chr^ql(&Lqa2NGZ5oBuQ^SEV7$Z(LiIv`N?r^cD7eW1`WOErvmCGn zW<;csh12`bVitK#fKk{H!UUt1hi4s0;XcxCw-}VwNDa7Ghyh}eDNIFpLWkb z0doT=&;0av+)#4-^iOm+1$y~>`tC^NIzP{zSB~!6ArCL|bn0couS$#wpLG%K6*`{d zD9FaZt2c2mVu;zh?CyivHzxY53t=EApug^be`m^}*}oHCzr0+p6n9w`XkP_N)7qX* z6F}3lrE;fZ>VaTt)~{LX&0aYYm~`=8NO4@4!mfX5j8P;g~!Nn;pHj8yWlv*5Wb->K`L;;@kbDou>cqv4DO9)ecFT z8nyt{K+xxdVWv#^CifoksB2g??}p1brBsa6ZuERUbJdK;-c%&!Go4vRxoRn<9d`A; zKuomBA$RyIgyR8o4O0c(3A<#N>^EubKMqq6lr3kEzLaa%8K1fKR*MCeYi@s2Cr9)L z#aC^e>yKC;fuTke+I-AQ@VIRa&X9x7@#ANEC92XbF`XOXWEkg8x*e~iXp0QtHO z1%ZR@;Zb%O2npLj19nT)0kDw}lT4?tLVf0N>{xRRf7w`?{nJEh!(2U-H^xZ&mUsxwcD)(5Hxf=xD>>VQtW#B6@5 z)3}lCFO^7^AoVMe{<}=%{3xk*2iC6OOon!!lUhF+z@13LFYXp+tW zeHB2e2UpUT_|)yHs|)jr*6ZsFY{x}}RMS-~E!<_E?y%e+URjB_EBjSjQ8#Oy9p`=* zUx^{=NP7TUh;UQl#=u7sfp;viV+cNS?OYL_bd2~yr&Px%sR`?Ao-!;G3jS`PHFpI;fK8()6ru@{$)pOW$ zNa9x_(GO(^7gv(uxUP|9_}g*+LP}z%%-gPB>aR8x8|U^K^E|@t1x2=$M>5uKpttW2 zRG4u*Z0>6r4)d=$++E-B=7#)2o+~H$ECaTbzc8k*8P$l z2VXEQTW^8S|SKA3Xe z#i<~uD4HF;XjKR ziaKoiV{M8EP?zZ|F)~&4!89WTx z{^c(hfNRd}SQEs=)*fuVf6<-jFWqX&Ez`O{!#xo8&S>PB7|khpvVBB(bPsM%!_Jp&+MD!5f^!Dl-1n(w+E*acQ}q}x8t@A z8V&Y)2Dbeh0>=r`d$~BzFQnAn(uA&c;$V~U*VI>z3JPvY1PpumE4D*R%kD zyIjPJ+G7lEw^iD+RW7gP~ryr}m+=#wY&IiIS7i1?rRoKU9O8iJva+p30d=@UesS;@is zcM`{&hKe;o1ia9=*KuEcVoP#qtNU+W84$nKPy+CHQ@_`BkN`?9YF|M`*^g>3B*A^H zo#EI)252De?|fJk%P#pTDRqIuO*_z;=O zTbdOyG(w)k?%~r9w4T6^nBj~Ej*Im%HlFxOU7&y7)c~f9&wcgmoAR{bk4NbK<{DE# zsQUsJUeSCR;x*L=D3LTqD4K8;{41Hxbf!2~xLctXD3E<5>qzF?1HNLGwh~tm6aWy- zA5cpq-tHs$L82>`~hIQ9Y#Dsa9b<;1UONA8$yU&uz<7-dGWiggvLhrbL)W)6J&5M{>Rziv5$U^aAM1 zOcI&E*+ID8_q}QseFu^H4y_V&$uj?c5t09 zF{bAu9rC?$#cfqGbf%zs^8>z4s?P^L>>wb~d%Uzth$5w6n_tRVKax!bG1Z=rKQ)+Q zHV28jXIuhC3Bh-VtJrQ@>v?VttfPu|A89_%kXTIkfQ)=McgE^mvE#CjDAKC^#0kEO z?q{PTxvw7Nt@||Wu9$<&(ORF(cL4jp&%}lf&W-1_T?PF559EQ&*}d{sg;x-}2ST1w z8i8-lt82w(@d0(2zk?5LMBfkV zih3;hS;SF?Zz_RTOI(wkCtc<@bpO2J0z&@)MLbb2xEgi@P`XRV@v*tWKyqw7N@Qal9ll zBiXvW#a~*mym zrqgcUogy|Z$JQdF?%gd)xQompUNnbWxv!88zVj?K$Xi}E3TyQ9 z?bAwo&G(7}R~p6J?ueM^z2=Ufg60IQeHGbBSUcm8kZ@Equk5YiHlDH{LIC_yVU5b$ zf>aL%E^EywJij1I0j)mP=QwXbx^pOE8kEbXi}jnk)g!8q6sleL<@q%)?b=6&(tl`v zK=qw0u+w3*QK4YXD_|fs-dF?4a$Gw_+=^?V`*KBHm9&#B*}p*q)X#Ce=zY*Lmkphc zc;S4|rhXfrbnns~5`pGTm+;|7MS4}dr=A}jMAz(nTE9REA-f>6nbl63y-QWoBFo&< zEPYTuLMotgbjJ!e>|B+hDd*VzV*IKkMGRY*kI*9$fr>frsRw~N@8?!K&d-l=eZ`cm zzc!P{)x?j6+Hz=)UNg6MZnTG++KCha~o~c!02Qm1*j^{ zm7S@4ZGwEWP$R#(c$-j0e85pZIW=YOCZZFxCS^Lh3f91%gpDXhXt&Cj`~2)Z>-2|7 zvadNJ^$FqMr#bhp_D;F_%T#dSn@V%3M=14Q!1sCRdPvZ9vN!HSqZ$1dABc5R@)q=c z{9~(S??^$$i(~b7SoLJ*ddT6I z-7>vGTHr7#v;tr6r!H~;xJLGav#Hd>%#HhJgI$qdGPX{mGx@j$ls)zag+5Em5O-$g z$+UUYxmdNh)X8-cfCi@VKCAvofgLq150o3dWlxRT+^ODsmau8qgQrINC2y-TX6{Uq zHA)LGr7*HC>YmMMQp|4cw(#rJp(f3BATkxV_k`G;)lBVUUabF!K7KlUls;(VAo$+n zev-HLDvd>S#*$PF~$wTwnQ+NEMScjHv|@!d8szK4>-Roax_KjCZGS4Z)~B*BKIKC+L!ep02vzy9VNBX z7^z}m+F$!9GmxeJA4sx9YR>~j^x8{e{BB7y>k4_QmPt3IsCweEcJKulbQfr$wJ z52141giMX=0*WcCV8HE`t4ZPPzCHgT^!qP=11}*d0K90V+nr`ONSi)fUn*>_S8BtE z*uo$Z(C#R6^mc0XZ90gl_;lN)u40+}d|SgGTVFIKd0pw;tyETCQLa2&7B2oMg&T?7 z?w513DX1SliT8a5q8AeG^adL2`F)3V`%@X!uQ+z*ovY?KCn$HWNU)eIpaj-jDSZu= zaH^^y=O`Qu(d^_g&*rvTUun*zW+-+&7Zo1AuF$7$bXkWCy}*89uR{*!ij)q!$7F7w zMyE(cra^P(z`*InA4Ql11&GsgQvi>3_KFK5z{q=EE!83!0;K3NfuLO5`6jb2)vjSx zQOf5IOGYhqd~8Pmya2SkZFs1dn)(?TR=4-?`^(-v5M+rF0^i->QQYFxy8C`lc~!!d z93sN{5Sa1{jbW5P%k8WpzNh$m3f#gSJzT|*E>qUyj#Jb)2` zh`H|zuJHVUq@GIB$&*3^bL&-N+1|eIGiuiKRmi5-V4alXH}V~#V!e?KH1O}8o-j{W z1Q>t3U?`UW2(@$LC5w{^-H|$FZ`=D&ZZ^;|?puX(YO0?+sa5@+vXwwAvH`GmfFA^r zzo+)FS!*(m(fm~_Kah4pI1Yk)g7NdF+impGu0p_8{itI~EBKhC_>;h!|Kv7zfnbk= znp+1$LhG1QE2S^ZWBG8iF$QoVFTN5-%3l1|kgW8MR?+**bZ4#w+g&1aSV(vfE?a=m zc}>BrCB&WoROJF96bwmms#fUsKV^i!l4ms(F=zZCXcmBP=&wtQR0_~yL*vC1vn_kR z6Hokk=}bc$vh}Z`c?)7XAbFQDq>8hE#H?3lN$q;F@=)FCJ`W~0f`Y&`=ByYLG!MWOKLO`tU6;vzR~E#J@E!V94T4?!`0XbSysw`H@Ozi)~;B+2cV)Y5+;k z5?-YHnjXbeZCYW&v)b){w8|hCJZX({iR$rOl&05?R!rG5&_XB(Cpmb&f8aeKW`zg9 zjav>c4zc`8{wib_6tboLz^Hl2^;aqbXLCC4k`hw}|Djh2QkO~5x*&+a=_iiW49O>0 zjYc(kvG$9!SNiODR|w}Y0h7Gi)9@stwYz)aQ@z>587F=M6?mhL_2^apzc8CxFWPCEN2OU&-ox+ zhbT7G%SGK0UpW%l#T=%;e_0j8Z{GODOrZqmlKtT|$&w^Sgn#K>;25L$vo~+YlD`c^ zbhjHG&lOMa_vm1_Ms8S=KVS=uzV}ct4vDAJy>6%Oy#Ez{ofL%*xIWmoX98x*Sz<0( zy~=?XN!yn^>Q+;yfLznX%4Vo#1?)Q7c(58iy|(BM`m-RYKjqdbbdyF8IS{h~Op6Qv zh~)4PkPr(*%1wa;4pTK}?<%ZPlbO6L`MneirCG^tWVjJN4QRmuWGF3Tv@iXLj{BU+ zw*M^g_+>6g`v>(!^ZR$spsC{C&ppjr7=Yu~R*Vx*+f?G~A~DIwjtbNOc`gR(`}lV1BLAbd+CWdRu7q%C*-dO1U~FTJ3T_ELIm>7f1^;qqktUrANOkLhZa>fD>IwhB7A2na|6DTU^ zOaYq6vROUN7RO@@$~PTfVLw}|cjyF9hvGcs%h+-*$IjeKx(G`#-XEFAAghRxHCrUF zE(no8)L|+(LEsXt1{e)XPZ}nCJN zFw?r(=kIh8QJmE$^k|2kPj_^!o&*1Aq67pn2Q3FVSHrI`LPR<$fYQkHb%SHlG4rci zHk#dhZ(KnTA-6$R05N@?(l@+7;IFVzWX!2x+HnJJLu64+&_^@G{?gm0X;EgC$35yP z`8=}R;(m`Y%nn7X-Lh|i)!D~7)D_)CaGCE0iu*s&A@5J|bg1ug$^M$zBaSrKJ}gaj zllhwuv7`Wu!dJj3=$v}#2q{+ta*Az0@={9s4Sj;#~>(FRiheK9xcAn6&t!6W@Ln ztE*SMT31@-Ke}EU+)q+y##pGG9p;MIEzr>qc>^TU$~ETg+cJj~C_4_rheyNvmO6_E z4i3fg&RgSoaEBIq%s5!t9d-QB(%bKs6uRFY%{0ZH`*crh{XL)aLR{urqV96t9&A9y z{rzoh3S9!AdsA?mk#sPWhKD?Eh!#MwUl4Crrr$wckg8*zp!8vHTvUyc{f^!}4J~K@y*X>|$ht6V|0N7Rf93=&66_g1$ z!pyHfUCmUXS-$?0_wR+L)_+x*vFFm|x?$OvElG;%Yx}I`v1BKGEry~fo7?p59KMAL zq$+4Ff?qs=DN1j4C)xCEvx+qPYo9r9Ra)sBk}0ILQ>o~1iLb9jT_M}ikB#(L`q?N> zpLmp`%vMQzFg zuMddzyHkJt;z>AiT|bHAa3RB&&b&y^X@u2_3iQ2$`Ece1BXkrG4i4~m^n>Dxc)EGc zXMlE=!%zYiFxLsFFx0JnDw-@njiqK$d@T4}gvO8WhY5o)$u(s+GLWj;B|5tHOOSI) zNl3OqU>$m~N7s=P!(`iz0n!chox~I-94|hV(fZ9MmD~6{W^uJj%hTOdrs&)k?QPYU zqFRTrE66psMvHzO(;^^3mOw7x=HHexExKeLK64;U15ByzTRrR7V$MF)@+%nVzRJG+ zqap9t>$m_-gZ)T}B7x+d`m6o9oxH|8+JiRkf4bZPzi zxRsXRuChg%U2uYgzNEn(9z`}}`+rib(E{g>o&O1)?T<{YVPhs%QyLz$X<-eWG463@#0-v@aQ(K*Vp=yFCf(Vfz* zLcG58AverM|84PJBd-$K2%y5up2V&_H{#geNUMV(K?1sy&-QSaJ0V~9w9kIm(pNue zp>(1=d4X0b;5A2otv3kw`d6YX6JtA&c#5*pL)86jx+i@rr$5PNUBmu(l)`sQ6Ue3j z*mw0ww$H;Xm6}Hv{P-17&GWE4U#9as7?o+|v!g6?CP*jV-56MG+dwWoJ7p~cz|I4! ztCYOLDbt*Kzjsi-Y||l#P4Q)-5@A54l0{L7GWaANIPeL3vG0~;__H^kms{S?T)j$$TUechdHK8cX>P>nA{J@;uG}e$awlP1r-tTaZ57F(S#i ztJ1u+v@JBpc%$fwVD&)Rb_><_);fcC@RX&_q<{w%iSKiDVm4>+(@GpO-#tpvu%7ku zo8MnDU{eAXhY3s1fpq7*7hDJ?a9))p00|15+9MX?#?-9!UL=?j8S%Z}4o3&0KaIvm zM=eAr#19VU=T#cY^d840?4>uV3G0C!Csi9=IaR)CKJFaV1 z2NAq>x=@TH5swLcz8_;?yPb4vn1hEz)KY3*PeWN4xj+>dnUmzW)fQ;srpT;kr{B1} z3I|?{dHV|3!uk341nuo7udW!V9vFD;b~1LA&euHT@f3P|1y|fv^<{}=1FC!|?HGMi z@B)A2_Qs;AVXhmSfNn_oaT(F8IXfZHLrJ-Az>qME$v)I4OA?vAk8ePF^nKH@+VpZd zhwc$b4@tKXt6VH`&rZmWL-_hLvO9*?_%}NWzOvt5#c8q8X&n+`Z7}Q0n)k9_32;5T z_#O=GSwt*CtH`L1%6YJ5{}H#Grm)ZTQ;=7d0y4{r_QG6Y2rW3*t1IeO8>W5(l*!k^ z&*iE;?%G%8yRr91XD5q^I$RN3pjCpLHLtyzG~@~q%nO~@sHB2WIpuuI`9TGyF+L}8 z@A+6ZoluT>>z7;dJt_k~T1m87=#=HXg2g-%mJV*-BF@^-Wx7Z&Jrut98BvUugczZF zwxNAR&f~;ujt8M+RqWcEhyxiyBOJLuO5B7K#kA^Mvr35z@M9fEe?!ha&27!woVgmT zkMa0%f)M=q=A|9}*`cJ$HCJ9O2|tA18*=@_w()hXgJ|fN=f#m{R=J^4M?ZVD)&uuG z=zZ!N@+r!;jM&B^BT7Fq>s4(izqqrb7XHn7DvMe3h(J723uG@SrpmY-?sb8l^kotQ zIZEB8`q(%(_-38grUum{=pfIJXXNrho+3B%DV?h`HAAnymJbFor(T1GpUu4n70pNq zy&%>(aqH4)LLU)lQQ8G=E;rVWTeZS?>+C~NaNxLLI`3|+BBoVj}Id@5Hq^iPv^oqpKb=T-&5*{-nnk6xWzW{Fv> zT-ohSWa5#|eOipFFl`+tukYHt@u05q5CFZR}zm3i6MMv9R*3W z%^tD0c!l=YcT{ymAspTzL?v%)>y&SlRIrf=2y@3iA|W;!9zRBY$QaBrHPG0N5!c7o z3D1oaGT=&D>@M5r9+qSSpRCJnwnP^i$c-oghBD)mU89tvuY#H%m;pNV&l)6eue}n#!lFJjRqrP=oRLuU+TC(mMZ$nv8$AE>4tJbo+ z%i`y`ga~sC;n*u{>rHVi!uRtyD#Bd>#HEgzb;<7I<9f@366(CyGVioG+c;wR#pYe2-iy&3t`=m8dzHJCO0FV z40Sa)yGmChbn{~&JzDAeN0*Wx^JH{=cAEk`V4ANh^srdb>lRv1m`*`jXSeFwmytIh zPxB@fPno$>QnoReqRF zF0du?In(93$7eI~4x4^K5~h;1PUmOyKa?#V?a_zp_SGn1`3&#aV78O#jI3w26+pV% z-@`;@c((NVh7+3naL^&f)6Bv|1Fc(05l#t5io1H0BsNzK*H-eoK4;ax#j^k!CCFjk^SdZr&Y1$`iWP5nZob)NVL2Cn&|V>M>g(W=`Uh!?JVm) zvfO+3%-bich#Ra*qWz4;b__Xno4Ab`FU@^QE9tHDw1fJwR5hui;X_@()+yVQ`I;y2 zxrpEV$UZQeX+pm!zC>EQQEnBJzT59C*8jO)8BI<6TWR~n{z-nd4~ zlgKm$tHunkk!ZK9aqLN_@Cmt0ZCT1GvT#A8^N~x@^9GZIF8G0=Ir5<%-T=?GS`X>H z_A~e~iMH`l%x@tX4k*7c_LmF5^Oi%lip3u0J)H}u+T@!*^go-oWep|UG*3yf6)k8V zlEa#Z+~fkQ-a`+y@h^v+F)SNicf-=i%orT@5}L~l340(L%=q$6nQ~}Pqut7TMgfbl z_v5(=Sn9mU56lc_B9_M7iN>RwJn{3}iMCb(k@1evcj~1__roifDqb9$_{|pr=JWuM znF(M$@;N8;4wZMSb(w54uy4!d3FRfX6p2-o>geOr(d9eGWnA0K7N*8_zL96@=uy*- z2-(KM4C$#`cKxn8Zk#OLfo`97Mcd8YYaZ2cxNqHao=h9SyL}2dD{aWV;5>F5LRV`( zw>|4ikzKMW_pYLCeb^Dp#(yoeJ$umV^cDKaBE9B|^Lmp~{bpjkk&GE}>%%VEW|uCp zyb-s!MhlbuDGL+ zZC%mMvuHD0d^vQ|stq!k(yo1wkF9+eHe(K`;0qHauTTgB0DRn#DR z=d=JT*aFFBnZchi-$JU&4r#CWoc$0RnsvGXapJtlp8n%+KKfH54|%wNmP|kFzmNg~9#O1}Z6jR#ZQK57pJh)Am{MGWcJli?Wrqyv zG&~VI1sRkfjc)sw(*VAB&2}KjP_CyS=g-jixAUKW+a3)*1=&7_1}1EH zR$}!3nD;sK92XRrRm~NrisNPs!5fbuZeb#dnGHR!Zd-i$&z+?wjc@;_IsBK0|B|6o zkms@5oIm({%-{dG_~9gs_;>jVp^RwPO$|jb{yx#U*D8RBeflEK_g{WXPYTRam^tej zFjWm>R1@T%rSu;Qw}X~{V??LV>5C0QtouI&b$ziku^bxSSol(!{8`z?`^1y*+ZsEh zv{zug;0!x99&_`zvlfvF=QE0BQ4#&}ufqRl*b2!H_#0ASt6B$&UbaX6H}&frsCxw! zNd-x*u3Es-`hKbXxp4ho-qt^+^5-gfJ$WTd(qBa;;CgZpe?riI5%-fX=g{0UUTO4l zqGU_|?G^%V5I5--HYSoXLh|I;|Mx9t*usWVMzD}Lp7>7xB45A0h=+k)p`Gm~w|-DZ z{y(Mb_bHK~KA@z~ThF$0V=YC*mKB>!RqjaMS+z2AXd9krNiFr7JY{qI;2>pHcdX## zg6;*oE?;arNW0manS)$0-)Q`g^#PM{$fIOpdAE4Vj3!+;Y7Hvxvv@B3S(Npyb7mLK zSf4NR=iE7w>DzfL|9x=|d?y0H1#Kne#|QBKiA(=Opb+7}7Rgcd(Ps+(yC(a!9p9r* zw~$&GutT;^%~PU@c;WXTwQ_rs-E!@{{=k!$F9V;A|%msCyzp{ z@TdHzBK-Xa6765})p(Hf?Y|vqgRJT8%XFw;kNW@I8cDQxG3SK~ERLlp&mU|KBjsz6 zaPg*NkKn7L3I1_Q-!M~maD?VWu^F*7Pum=cgE)&3=h%`jdz-NRt41?(cq9si)Kf#0U8bfIvy+M9;pxtc?Fl%H2~KnR69&wq0*oy(seG4M}VA{^rU zE2U_~2VuLb!HgLD^?Eh@IB^c2IP@-jaq&4^irLWKbZ($WX}HL>5|h_Qhw41i)ix+= zi}$SEO~Y(6JR=OukY=h5&Y@g^`OyncbLN|nDJF*`$T?*;pXi2;{15} zjY#X4NCS!af>>C2ok>}uw5Njr20A_%IWqI)9&V^3ubkajuN{`_x>Mefmp1yC9kP9L zJGJpcO+pxZ%-H;0T;Vv|9Oa=qyGOwAdZzHG(4OSbdVOxy=9t1Kw;ZRvG%>tAtYCAw zrzY%M&Cojj4n3%bU|lFNv%fv^tP7LiI@qFXNI@rckYYMj^Dsehm9^_}+4c=gW4vqk z8JZy2#OC~QK9!p4@u=gv$4r;6i{rjBbHxLlxOGKEd*B$x7)uY=J$4o&t!mpEW{NNg zFj~XtgJO}2Pg6DDDrTBG^2oi2>wA*O9Wl}uxu5O`FIy~YOYHC49}Dl-d-0TiRjY2m z2n_ZMPZ&1*EWEe$B`{V)E&36`ZQU3)S_qS|UfjgEkF>Sl7K;6up8oo%c=H`H921zS zG~8xmXuo|dh*@vd$92*Kpc26)OM8^ZyH!j&!0Lx2i8l*qAQsFn7 z%;(lsu}A~Os*h98CHMDf(wNEJ?iZKE0sh2zg2Ow&HPU~sie0h}Lx>8UL_h9BGr=s^J_l3cdM=^RMLF^w9sW5 z@_dWSY8T2c+BVm??tWz)9lp-fHsdG3zJ;sBP+e6viW(6v*WD*J3Dww(jqbi@er8=g zpI=ZOPW9sDJEv7a3y-yUi2=#kn*9owPGvA6R+-Q(DBam zTbDK4%ZQcsVie}Lql6`dsx~}wfx@vw^xIF$Q+oNdGg65o4C9mRE6$-8TMleh6XqRD zr6=;zLJSrJ3YHzK8#c|K&UmtVUGA_jr67(l6UVF4nj_i9YkRl7 z)!G7md_3Pi$qj40F`vuh6JpxD{xp%dOJdEv$BH*hn2ciF4QC9uVm5HB%*!i!tlTkh z1-c3Q>2-Yw{7tR=OSWpiwe9$bjKDMNaJ&54#Z{ctWN=33zR~ubE^~<5%_RnWrs3v2 zGn#{sZV3*Livsl|aM0^)I!2#LUKP7T^Bpa4W8VY z%m{jZ@M1XVGp0Lgavc)IO~vf-xs~6OC^2aQ)aNOSol_cpY_XuecpNdpWV5W zX>)*unQt?oG5RX0I&>qUThLji|NG6CO>sr6wh!FvG4gy3#;UpAcrP9cwZ+16?lQs1 zM7|XnC}5?Xcykcs#&L5E_0`+v|K@~sJ5 zQE{r@EBbu1X?{d4ZIjZ{UUK*Ibh+o8!N=(-eqX34cgpJqUowNiKJb`QN}@7=x<$^givHTSzW`GSQvk=lNDoP zdUAI=py!s+ja#A8e%?kOXXF}c7kMU(uad$p z&S>xB!a|+#Dc@mSi?9mDs;gJ>64Dp;{q0);hAvx~uv6{ub826r$k5}V(B^yS4q-kK z)gDU{Cel19+L2?uj;L#mvur@!Q=Z|D)JL#HR|GN9ZOqYA!zl5V3+Hv)@=K6M0;o9n z|D*0b!P*c8EMj6KnckG8xV>RKtQB}V301MHzibQB4DG1-dkwWL6P1= zQHqq%>s@%xKHvV%-sd~_*L|M*2l%l_)|~5|bH49*#~ACZBU^Ky+Jr^n*rvG{-DXRw ziKaCtLBah5PpHHFg~}MG*H55$5kwGTdQXKFvYBg!dfm)Ha>UJ^>?;OeuJv=vo#nVk z?5+hH&TLuJ&M*cI9Y|&PSxo(Mo6rwEWD(6_z>5rT+Y%I>c|;O)6JK`j(i^hbIerF` zPkVKkl30F>9T5a9Iy~IEehJcsVg;i%-#lz!;?_sV?Zekv7u!^X^7)x|!+nQ}+$_NH zMyAf6_BHmKB12$zElVCl?k{=j#M`~`octB(U5>juyyuUZYH2Pj*SCvWEysz=pxs0I zQ;!(+o-rlkm_gRZ)SgE~d6y?vHSrlvBf@1|y~M(8#c0_%5((sBQ?uU4JHze{5coZI zgCZ38Iw@ybY>E009UP_AR**XfVofI3mxR^5Vy2^2_@}J)FVwi;S^q*_hE?}s(@WoW zQCInQI@0SWt0(74@BC|&!bFMLhA*>d0okoewk)XzUXXB{~c@59|lo>y|y9SK`q3l$d4x zRcUDlbH*JuZ&strH@cbEFI)E&Z!ytt^|!AVyVwYKdo?pG1IFOGE9z_2Zvs@!7F-GOo`e7$Yi)H};4%sRb2 znks&i4mX+Q!wee`4QA)omK)t5@ooOB78YkJ%O=jQODUJ!B=ThGaKI=;@br~Hy1Pg+ z`ZhUR_0)rM7uM+mH`eg@meZ3AQHG*d9W3m7I2&n=rgu2CGAj-cc`}-MiV)IMWV~~# zcXo*V{uovAbcE78?Hh5gDT#l}D>9fIL09CA)od4wqq!Lweq!+Cwp=nLw!D>>HZWp4 zQ~w8|_Di-)!>bVj6D}=n-r_w@C2D>RdnJddt~0F0RW1 zcNGRh#qi*FJN&>|h&ba3jDlZ~{>s`U!g$-4xNNia%u2WRadVSUbU>R7Q_h5BGUK)N zP*>vaIS&&r*T$vSnqH!xvQ0(2{g_zYkz zjpqm5Qsk=Lt{`%LRjtM`)MT(kzu)eMQIS9Oag86GHF3 zKCSB%Quv_dTQ8}sQn8oPsN;Z*F4H0Ovb+5#z=XIzjRUG2s|1XYhG&w-Bfx&PE?zP7 zTT!w=3NL(+T7oVi(>lyNTxQ4fDn}v&JN?ZkrRwzQqI0AIP>Uld}85t?T zTsIQdq&Z;@WH$GtHr*^ZK*lp9>o05&cL$aR_68VTgk8Fs0?2sxuaa|u;mx=$Thm|1 zhy#Gjrmombn5J=UQB7Xy(xU86swX?nCwHpMGydqy!lc>WcR|9f)L}3eP>MpA1o+`b zvR}ISVsg_L%}%?<{N0f1^OJ$ z#^&ng%`4PON zt*Odlbh)GkK7(W8DZ3@36>V3*2G3}zqAAfEC~2aXQ_p#R0e4BuXl^W7N8(PxS?+O$ z`te!Xi{_1Of8nkT*M*ldRsnuSUo!)NCEEZ

0#@Mh2rCnx~Erc~kk~865mk2rKyfw|1D25-wUCJG^~zV<{^E3Rw zdOA(q1$|OZ#uKz&wRTuYHHnc5wqA*=@LDFc?&~QA|;6M=9QtD0k zF)hn!d1mo|cT`1JB@d0_gK>2hC)8BVz%zTjSs@1W>@|%aI;jNRDn5nc3N-7)O5_d~ zV)kfqeyS{i)>28j{NKwAEQ@Y^3@sFg-VT2K)I%6Lt|!MM{hZ0B_q-R&qbmiUHyDK` zbFI;8TF-o19OcF9R)G_?9UL2kq6qWLPCW_$pQ!FYYe%d5EV?WhTT;0#FWTr9KPK^_ zl4RvGy{UlP_NDpMd|qm%-m6#pzuyO^Eyh(Pfza~}-=fH#O^)2{@G@|Hl(|exs2^ZAbeOc~6XeDVGV0tAaTnuFF9`Xk6Xcj+<$hStNw8aPS7xbN9J6meD|I z=5xi?rQBl<5bYGhx*-{!dnNqWYzv|pQA^j2fVSR0PTH9#Pnfhte^9h6h!Bs|rD$A6 z$GM5XK6rg0Tb9&sde`zH=-mUI2>K5^nBKJJTPJ%Q_nKNcLA3Lg;b6{qlf6_j$;@xe z+fs*8Cjm8416Iy8E#V=OJKBXo!H8dEo0m9SiLSp4(* zXGyRo%U2+Gj;lew!c}aZN8EUV8n47e!B?0aHD98hn;6FTNl)q6rXvcK=d$r z|6b9WqkVFB2Qjb4%ZF)jmO?KP#umXF1p(G4d^$HoM#YQ2jpw+{IYI++bn=0= zUrS$HUnB|atGM2EbMQ2Sr=?lMlIsJfEh?oOiPzz-{R!h1_8+#MksGOoVGqHJ!Fe`e z8%sjTJJn>$7H2uegpkyQ)@f%8jwf@spy8Fa%%Q3y*WpIGg>G{qr2bhiUif=N%E$(P0j5OHYCgcCtc8-Zz~al7Ara87!$#-rn!Ss!8eNOR18-U8UsIp106|FYC<8m}EPseTl$e}|+D51TxgFxGoOr<&#Hmbn`PwBW4^plqu@*FgB z^^AR85OobyH(qxlcX5pY=DDzfXYn1~FD;PI6>5wu({7h%U!UILbIi z=Q0|?nwR!GG+kEXA{!FcZz?%MdA>Exv^-4DFDS?HdNmsNv76bnW*XhNl^QReA(A!N zl@xvh<8xnZ!vJ{3n38--ho~;CHjxh?ZvMqUfg~H(Bnh<%Gj@6)nWiU}wD(eza_R!I@_)==4mAXvj5(5a-Zh)aC9c|* ziv6=&TK-`!#WOb@3grB&`uR-n|n}XH=NGx}Z@w(%%iK(2)!PC_hmR$X!X0v!j zJuciCdl%8FBsGH#4xOSn!Mu>qqcMhABG?uog?VM_J6;R0NKmx9f7Y_r_(cYsp`zN-3h&t7W{k-Lv9j3CT@0+r!<>q%r33Z3 zq_C)JZFqYXV>?Rj0q)iH=k-hV$Tp4hRW_1|h~Ox7j2|tJo$njQk741g{nAl5I^SC} z%x2>3K4F$8nZ9%T_Vj3n!5H9UcFdtP%?PeLGg=%^JtToA;2{+~o5Twri@alYQ~!lc zV(fPY6EYM|d~acq&1`JA0!#+e=)aFY9K|wrK@&|3Q(J%R|9Y3GY#i}ruc>&h*v?rw z`@uA^I=G&SQV+4p(Z%Q}vRv>#z1Zr(lZV&K%cpgV*m|$7J-tzjtZ7Bz5kiPRYB&v% zS`}+OzX;@y!QtxP-Yj0WeA(V8(YIBf96%p&Ez9UmTJ`#P&Xkhx*U|eVtre_w_1{av z)A*_iq)u8CJDTY-rJ4vM%EFU{;xTTvqMPMX`f`F*Sh;6@TTPz9KzU-ZfU_l_zhlXG}G_>xDhXiBE((IuXy{CtZ7lnHZC84y^se9NX4#6 zIZqw9Q+$BFmRUS+wiD_(a+9??5L9%MvUCxZA+r{!``{NEW-sk}Tkl}fr48eZMzxr@ z*;qX2r@d^TM&yj;cf1_{_QLYJ$lki8k}GJ)m4?Y~46l7oKzTskm`o;wT@T!y@&+ty z!y}K5tCK?fst~i135-m7MyiP1XCc|m|I8f0MM|}koApHP{yC!OZeD;j*V0^VNxIW z$W}pB_!;I&0W&blQQgab-jdxulhIfSsEWtIDW^wbAj;~a_Hl>_FTOZ{SVE0DSxRG7V|y$Vf!; zey#ytWT#oRoZt|T0tFGIuiIVs@+UfR>5Rn+!G!4GE-;^wHLVQ+YCsuy!6QW}x`x-8 zlm(ItoaDvs)i=Mi28UC7V8CmOZu&+a)nNQzNnsUVA62{Y)x8eSDYo&zyDj$d*r_?m)(sDFsgePuifuA6pQ0lvq3#rog6;lT}SsXR>#5XsLg!8$ML^J3$RLqWBwU)Us z#-O`c13$5TSMSB`HUvg#&4dp=hLm)WiXeL(i8{!6<*dMe5((`(I#eTrrG-8|(3t)? zoRQPn|3K(*6$74NIKLL3#WYm={g$cDHe5_kIi`qTkHcm3TyX&-5E{u`c zrMpT(qp74Ce!n*0UPe?Xgm=p9D_?LDXvzK)4W>=;f)~{Fo7fU9M$w$jWkbtYw9q~q z7D%3n13zw#xLivJxmtqefq%c^9!^El2(Yy@Gw>7Yd}&kWsXM_x=57UeqwOonq%inw zm)YD8pu@oR*eZh3@Y@Kds{m>zQULp5are$L$1>4p>st z{3gDrB=%o$7S44@B33KR@gQABEelan67hO(R0_2jX!~5UW0UYI2Gnqrq%#BYAau{7 ztgi8t6F!3)H3P_YUOxlUuUsYP0ZeUp3xWihC2uO&FUF2|a}XcTJAL`6Jv`p3ly4XX zR;j>$xaRs0*epEg18-b8uTt0glB(8Zd8=8VKF_&&-J?Yy2wrluiSlkyR!Baj<2suftJ`^|4GGUP52*z4y{+V9uBxZ;>7 z%0iy_9n7;wEbq_(EYG~|vi+?HzfC2X2h+oQ#wmPWkz=jxU0I1%f@CnJbw1Fo3<6;U zCY*joa^sVxMzP~%n&t0xrrHvs8*;cP-OJsMaW+;#zcW=@52lIXflsJyEBbJs|tlD1y2^Pa#t!rk)oaE!I zNC0dZ&mB#Tz)c(o{^GHiQM%DW6mSp|JY;O5Gyyk@%5u36zy#7wCZ%a#!e6}!xrQrz zw@xO1E?StNkYca3@&T9#oS)6)_a}JWb%J&e=Ib2fq-rxHfIs_8X&l-y`Q>y__B)r+ z4!0?n7>2N$$6*0cpP0U(-pc1^yDDmq4fdN%Fe~_95Tv#*D1DM&9p=MCUs;v(ERb%s zfQ=>0wzl7c@wb@S2$UE>qPU^(nVwvF^qQl5&=YW}YX=<*)#pbP1R`K;7~ZqH5?L#j(2H2-yo)7Uzq3aSFNEGJ2Hsa9w$d7bY`sE!n>*usR@eQ2*%uOEXf(j zFOgnOWZ|<*Nae+gv~>uA-gHl01GxW0G)sHbGhxpcY|yRK&*tYYWgFlLvOW|YeM|Y# zdjVqAMEW5sppCOLQGMAvOkL!b{3bz0klv>nF|Ltj@oaVxJC)=Rabi&hUE0vSq3du_ z(NeN2`EGi0hDugK{AyS61k(m^tOrzbNgpFCR&1m{ig6|Fg-Auok*FwA@RjQCJ*7h{ zK~uhnyk4ja!zHqCQ=+o{Ty(qQX29A(;gRw@f-|q&{YJntB5kFrB6A29fsn@AsWyvY zwA#t!9%&V*o0yG`?)MYC^zUiO1k}f+>>nuICJ%woP;uqfrKtL3Q)N%uh2{wbJLMZ8 zE>D6Cz7TeRy(Zni*aMj{Rr~tbSL*vxUhQBN(RuXYPBrEGi6}gXH&ODcFc46<9>_lVv>hp)o6PrY1_UA%`(k zbe111S9RCoy&hY+nf8@>(}%2i$86Y}ZJklit}LLfap^RekreR%-}%bXWcp*wUgmbo z_v!1*R+=*nYaV5ajp33BgpjU~J#q^k)>KaE5KNHQ2!`Iuv|X&Mb2u?%t|z|8HT`{b ziCB*FJ7HzoxAnJKKWOLo^vG5i;87^wE&qYv9Ei^*!6^Drr5%&H_rfkaonK`c^LLcJzI%X2a>biJP~#Cwmin93#OAw+-hdgozrTa9L|^*SsrpsgHMU%I7uDrNRL5kEjQg-tKHx|jKrVH!xSnxg6wY%sXf-)MqsqQ zVd$bYz+GjTlZFuZSSh*T2nHTC6TT`rnJT67gjB{(DYo|q%@gyMBZ4AQo~?;R;5GzF zy0@&`Z>5z)DU0RlsL@g@dCu0S1z6{|nSN~$;fn1>b;5kovl*il0a-_g< z7N1nl+xCOtr?S0ln^Q<7Ds z-z}Z&W~)G(_^H0fJ zH9*rB{7vgBjZ#Ywp?)2H+xh;l99Yx5>@zjWXJ|E{dp#zeH_wbOJ97ULd7F!;bNS;_ zNdVMSXDTdzHoF*?)~@!UXEsiXqdZK}jxv^>m?2HE_idytfQTy8ZGC2Vk3`;N z6|}H;awWhn8jsBq-?=m|xO}v@?daxRQI*dIrD9fS<4-rXm^(%#AJ$I0Wu_n=I5B(H zW=KDL`_lWv~KG1seU$Vxkh)?CK0oSt%36in0*JyGYVamujR+*)p2 zdo_`Np1iS_@Y4$~-GiIDMh_|G3KvJ+90@^VE{na83BC3!WklcWRDhPG4|RfqdcNg- zhDe@X=+RR%IlglGp&(0^2D?D?utT=adk(<0zcI4vem6Izr&IgGgq1*@?E*&|Qw$7j zD#z7{@bn3-3Ay#KFtbc$v%Ji(%zdXZD(~|n_4@Yuug?b)Z!CI!YZwF=8LYeQ?ZFy{ zS6D2{qk=3lZM#q3uV)%tm}<$d+ZFiI`93N?d;6wY+V?6pLmCN<-a7WlT+K4i8`8yq zI*u9|!WU5qxkjVWM6)o`K89ecKxq?5A46E*gS~<1Yaa$;jOH0~5$x zX{<$+;i`33!1pNo4*O_PM;>$23+2PxQs!3yk~48_hiBZY`grB!7Jrgv)S&RXKdIdb z_d?DWKNaM^Z1XK&YNcdRpBQoH)kutpno=;Soi?hc=3B3UH&1?q$P+nlQKb=xb~NpH zeo^UzsI*6rm2Z@u0O&lYT5>*UE2K%p3DCs@f7$%z;pf|gnPgWRNr@>uDER|N>Cx{u z-S0{0H$8U2i6<#ep1Tle6W9;ae|`iA&i49mT@d@HIxl5~RJ=2DSlEcbeF8?Y$kg;e zxTh4ibQD_~!syn}S}iV=N#XTm;WerOVz043;KUBoqMEpn)iiBe{X+vl2J7yJ1;4CB z74*>zYVzB$%N^fOz_#J=Zkj*m|7t_QLPHP zq|;Pt8BB>m{LM0*-;JjF3>@Rj)tNazi!qq=T@$=eq!sh1gQM*mDYb(3;b3Hd?CntRV@IJ{mEcYqrNjm{zsh(A}-)U5}>jY z^|zn%xaU^Z?C0?(eqF#EK5{;BcADtd(+K5MOo~% zv!5q?Q*`@#-rqdy-Q}naijnU)U1wLXnpyLH>HTZ5a*|$Z65B5d&FDYz`->)Qz>87< ziRQc@2Jg||tEi=f`s_s?k8M!9FPnL4JzCtJR?j@#FbC+^jt>g4)nw177b~m8g=@HQ z_MbZTZtgrZe~)?~t{*!EFwFfLI$ob0oW|Vz#^%x0_A6Vj@fiu*^THV^X^A(7r+*Pt zy~Q!T3ebObae@JQ5tC{B;bn8*jiOI1g!vxM{y$>nSL0Y1*OCD7nESs=oNiu7r-=Tk zuqv6d=ETb@CCcx}^lIbq-Vd_>um3eZ0&v|Y-HYlL4W`as)gHW&of699@ZfMdz^qTo zG|pB5gwSUB(|Hyr#N)}+vhtBBzYWl@IQxz5bHC$Ly>JUN)^ltk`+iI8#H7B|^r}-8 zmMc^7y23`mzTA@k@wmxwmEnxfOmE-1fOmbS-&sMed+&o^C98&pb!Mdn6US34B*RDc zi?D6;1Q1%fqn5*IXTP<&sCE}`f4k=S)Xt8B5l#Nsn9M#VZ{y%Z%k-O-UgNx4hnsbg zf6MYrO{$^dN@MroNImZLM6s3a81|Yy@@iq`4D1+lGbuCK*rkU*(|6secG=ZmC$vy{ zCh53?rSZDAZdasL+_trAqYqT9W|O^2ZLe`}n1o^v<-7vOD%=h8YdXw%tWR5(93%T= zMI=@l&wKY3s~zP}e~p`r92}oN#*WYvW*Gxkg+U%Lbpz^add6NYzOhvHpv&2@?IsBONd0 zQhhRteTzEHIWO|}uR7kH?l?NP`jGKF9gv3zvPJ&D__b!6uX&Xnrz*0}ow>|vCW#EEmlV|xs&{H?HIHRJoN{pC<3Z{SR8j#ppXdG- zP(l@rohbnN5JoZ@QbAMxOp`v|I6ocFHuWeRuBFDR}Q^u+-lU&jn< zm35_yo~1EcM{Hi92tG0UddlwlHP{L6@^}zey+8NiboXfW9D6VKICqlHb4|Ln;^6A2 z;jd~%fSiApTssWefnUAKEO^?EE2^uX759rhl@p)X?>lChR6Oz8FI~JE&4(b%BvGQz zy@mFkP^sR{?OyJu-KGCBYB$@LK z2QMl&pRN5&Iy?3Gmz1hEX-&s^4h#5Lromtk(!nSY2ni z9u9KOKmS6(>Z8W(ESoViXG{m%bW;*xxyp_+(+0yY8ohMKH_SaM0m;mf?}@!7Bfb#? zo-!Ija%H5(ODopIbvPk>`VoS^nBz5Nti&(Iti_eoUxEoi@&v+f>JCC?&Oa#Y;n>v> zxm@#f3ZbMy0;5tQrk7_F4i+fm*o?#2hVdAx~>g=Fe`y8n&~bQiqs|5-NpUt zICGZ!dFIrgUDveNgzfic7qIHG=~s@h9P*8otG(0oG8FjzZDyuLui=O4XI{M3`dH`@ z&OH;Mm%_-j-@X!Srd@xoalYTW@XQF0qF)62vsAco-9Md;OiJrJq`Q$zi`+kqt|vNyQ)mzw*D2%=BJBiAH{LG=I+sNe*ZNdEc0GbZ=Lc zf^1?$@hC*EQ*Rybg$kKb-bw~wDq2bm*87OK-ganX&wQ@lZ+=xQmK#&8U3H6Bc_2(4 zt|Yn^Zfh>IF1tASu9AVQ5sRg3wtO9aY@Hd-XG01?WOL;MOJKxmJ~YLPY~6LXGuM~Y zAf5&j6K<6?=KR10+%spb%G}14#PpJ`qp0Szxdv6mQ|EGqmfI?v&)8E~d$y5&h28(l z*0NWT6exM^4TslCuIjSdmlo{0A)sfY(9Q}_m1&)}D-PIg=FjwYnk8@*SJ)dY7iJ_X z3C%P@D+1LW1NeR$*J~~*r1TX{(O zc|pWmD%Z(%Y4|c?Cp9e)8H+!-%S3)20J64oF9D|E>u|d5&aK365%#KO+&Qt!0AcFh z+5S;P_iCHMsp-VI+Ym!y=ESmvgo%XII%+eGdTZp#oR1ta^?C)dXQ-~zPSC=r?|&h67? zODihTn;EO8yA?A=t{v$NhzJX`hUM7vt%)+Ak4V(&mhUBJM|g7=OJzlC-~&SZ$u|f; zxzK}2fCa&!BA`)YUY7-_6QSg2#qs|{^wgzjKg$Q03~2_)Q6mKjisOqsC4wubzuv5i zk720|6BN_fX|4b3gkk$|03-~dMU%Sb^TOxw+8Ys}u6kN@=r5IRGYco0H))j87EhE& ztvRxSPaLzN8H{V}aC?`ro@c23YMUfRlf#_Tzo=DzumU=6`ipkNQ*$252>oBwid!#< z!i&iQc`6@3;K7|O%B2Z&J=k_ah=EUJY^V%|IkUy^25qu5aaL>G zv(&fEpTiR5l1Y!Xx*jBS>%ABx-rxd=*mJLOcW1pA3YC%pV^M*F~6L zDm<001p#1#A>K;*bM?_k0N4g@u3^DB4xLo13%a%byyuwebwZ<_G>%+&XbZCbypvu{ zL9FV|w#dKdPQTTtczM55KHJf~>DwEk?mr>`)3}$xvjEEM^~O@x$=a2uGTPkFP`^n` zm=RejF{1j?#ia99o!&w)xiW#FB6`^0vtL>7xY1}(xKEjWJZZ*3^9ovt$MAb(QGu}6 z6rABJKu)#}$8s1QyL~?9;}z0nHSYy1M?|#l#y+Rxrnm4mC|vNFS0JTRf$+{X`qBi3 zZ4^JXu68$J7q6=ZAS}Phuh{NhC0dJIGrZLbZ+78=w3MAxQa9iY5;BElfCX+twLgZG zPE07P-s=-rQwieI`LeOGk@{cc+XHg{df1I>48l1k8(t96x`=`6Ad4@RSIA+Aj@Axr z{mTQo!gd3VrWd=t`=_@2>Fyr$QLR!YcC^jc6omI3HTs3fd;va&To>&F4g7v_ ziF;)Rc-?O4uaS-S3(KXGQi6TP-Ah>wCr8*x(%0nig!Gu^;*I_$rIHBwS4vfMyLV$d z|M1v12FGPJXPZxXhue|x)6bMKB7BDVo{EgXR^;qLe~B-E6RVK-1xZ_IwD!_AK{`;2 zaBhA^|58P}HWi9}3RF(2A%lg>+~@H88rylzAAzbLVP8K7pG=E>c(kI(6IINP7dfL6 z#YNfgh77idM9v#Md9%*n$awtAxYV;*9(PVr2nn(077G@<&7j(#^+3GIJpd0W)}Zy( z{kTUis>ydW>zJb8Qtvbxi9YOdO+jdR!Sj}?< zV;4w25_t-K7@X;vPZoB<67hD)NUfUIlVya(K!=yaWO&!)cj5kLRd%^JP%Zq9es5?7 z5-ZBg-$66?T)4hr+WT>FO?o)LXFeG$zi@xz)Bq6}HmUT%9Q`@|t(>-}nMM<46RI?D z+q7!)^Td=mklqH9U9QB*6Y_!jNx67At{lrS^m8@nWpKy&p-irueWxu#E9(kmtd4#Y z#tCx7out>9gk3ET2)@%^&iUhl9J`OP-$39+>mSx7^>uY?S!x3?Wc#7CqS_DkERTin zVxrLtOIZVZQ}2bQ1ta?JFq|ET7aeU^tRCX%1htf?pki+X`FNKP%H~om!g#Zg6`5ko zhiHy0{xfzZ6xl6O^@W=GqaG8gz>d126?*iSZ`Si%f;zy7WZ_)DA8wmiVsRT!I>KDHn~F1!?xsB1i=( z`1io@sq-JD3OoHVs!X@am`GKE0K{|;kZLEW(25iCA|>TemU@CJshcpl$F%RiSR+T3 z5J?4Hq0!6Wx>;)_N>4!fd^Dre1q794)#a>5G+DuE-7$dg2?jRSS69$O`(hJL5zHd) zcH`bKSlh@88<=pVMy-(la-<3Vjs)dsL}=nixs#r{rmk>{1+v=hc5lY~$DO-#399e* zH%DI(LMZP=dv9kUYZ+)isy1EkV2e!srXil`6HyfE6(`S(gc z5d-*Xyu9cstWSmY^z|L0Z@E!%F(J9{Lg4cInxfaFxU4gM59{|&j8-;4@7kPEdYyHJ zag>399rkN~ z_i>!WH!teC;9*i&^{1DsV8D&3dTwhhjw-qr!NQup2Ne-pEbvE^JW&Qs9}(lGUy!&J ze(*4?HknvIuC~q#}?r*>877&?TfLRAHiKIjSaJ~F@obQJ=@j{?JU|REEJXmD@U$}k% z&jN6Q06?+-5P+O`g8VdqXUr$AyS9Hd&H=*oq6i@|xc%RqOcwsTlgahpHclXoU7d;GRw&QqwX#s>7M35;GHpTEu3@wxhg;jc{Ov(&gK7pX$5c zaBZddPF(-PO6Qi8C}1P=d%WMPp_1~;A?@!c_X1r<#FO2FvO!J~QVEBx%EM`5Or1K1 zwmqfO3a@be?8GHYcYQp;xcO_xwwxvkK7ZQJ-}L|M;20Uz)$Oltn5$c5>#Ckyh?*#N zpSD{qjJEA74aI{!`4FCduvVudXJ@}$kbd~5^;s74$EkAj!(jS^=mxPG_>|M{sm+a5 zncxcO7(B~XV0mn`*vic3DM)WTaees4diwtyP;YntIBvc(M{n%(P*?hjEGr9stQs#1 zX3=gUj%tcH7^;Z>{l!A|3V+J($3*@}RhLxBELvh#>@1_-`jX14B%=$Pfb?9 z8D-_TM54_9sQ?4l{=l5`>RIM~J|fsjsZKZQr!VvP0u?uW{GL98Y~KCQd0xXr*12He z!T&7%S3(!t^98e~{CS@s#p{9h1qS_uqjokiOj~2}sfe$`>wliv65D@O-jZ=cam&i7 z6)&Kpfm~ak3g|8_?)?uJmv#P>X&irhxxzm0c~E*r^L#|Y{DX=6Qd>C%CjV>NN&a;^ zA9kshPj8u~R^&Q}5NgdoFyVm$SF`+wt21@hw;R5`FQ8+ANPZQ(?c=!*Op*ua&r`H# z@vnO?%__c@l$zH0g8i*yio->u%0c-Q%01Zhc(SohxJ34(Zas;`lkr5w|MUUt1X!nz zzQYjel?%E2WWV3@QT*e@%otbx4Bv!f3*2zXN&Ccx^X78>%6so__6Gu2CUHd#>kTty zZRhfGVoQzgfR3u#!0|9#JUj7Ys7+i0k5?via;$TqI-t3DAO&>jcK)SDJO%WO^;cM!Gu>MNIv~r{^ew2g*t#Pe(PeTz@OY4z zziyXmp?-I7*;MZwF-VNNus0(g&KG>h;{a?5dMXnTi`lepC(mVbuRPiP?bInh_4J?n z>%OBu&EDDt2YB=f?H^ky07fR~QWsP!G5a?^mFEGQeL10sI^ z{(0yMkRewC{il<6m4^uCdZ5ejD}7(lBio9CKS|qIVeFthbD{BI{6(dQxA{{%imhcN z06BNaoeXK6)w;XOPF}Ptywqs7TV{fkvz@#*0?yN86;8kULnS5G{e$dN)83f5{3_n6 z^TtiZPO)tGJRH_lo%z+(Q)d^pihEEf*HXqYNyw>R zYdkRgN$-_9pg_nyT_nuUXu$KIq$p~@LjATJwfF-FvO#{<-JXl3gJ~tZdoZhjS#i;jx+HrSK#v9{x|)|kL8^BC0h zH=H=V29&CPKX;!`?JGJ7%}m!f_yq<~ab%!gi~7d^{f&X%o}Q!rCyfUam3&s%$o|8z zU(opF#DzejMl^*x7lj5p%rx@6eqX_8$2&m|-R|z?PgCaWIdu|k>!3IDwvjzKEi+SC z5Pr}9fV!g;@W*MXjO?#JNi6a&>n)hR+Vgp$Fpl|OBe92o|-n=2!Q7)Fm4AoK(!M1!@YSN zMw?SCzYtIEk#eD&&lCWdi@~&GG0&j(G*%&RXFsqb18cJ!ktZS6d;%CmX=Ywno}Dg; z*i9U7eq%HZA_H1EF8wc0{VSL6nL$YTYitmLvu(|ip~u&>z}^7nC@$PEceX^M9^Vz^ zuP47To)``jA-afr9vN$%$!}c~U1_$KFO$6%lG!S^OTId{b0KZbUu2(qF9z2(w(N2u z;;pVvf0jSjNv*5~e9*j_y>u0pS1x0d%$|?D8_=|(7z}Qtd!lXHL~H+s&$3)xFaA>k zffAMD?YJhp-8akdex(11LZj1j?@BEf8X2XBCAM7(p!kCxq#%M)89sCBl6~ISmySED zZeK2|XeZ2X(W`4Zwd|kr$@JKw;xn+sqZm{;kzZ*$qERRiM85J~-T+2qaa&s0pPvk- zCx2dZe%{=Xx!Udyng%)rYeZ@8=Pb{|LkhMIE;^G{ac@WP%pHjagqxy)GdYvA^o3P( zPR|FwSAOXGR6f*O_0&*-rSlrQkQ`PKa9;Dz0Gf(pdNnBvxVjDRg_N(QFB<^^x<{M& zU1Ts*5dI|}mvsNee%ER2wP+H9^5y>eZ59{gGF6syI}5ucu?(95j8k`o$UV*aVu#Q0lGYJ4?E{V}ZDv>p46bGt zWwIcLCuO)IT&78FAUx((>0Yo$LvNPQ%gPO?9L1eE6C!h1VaQ-nSIG@lHz+`$08eh!GMf3;?sIbQ zFXo(j?*0mX~ym|Dz@y_AkeJrC>j zUz~jtFFiZ(MElCpeKW#nz!fnrs-Vt?VA_q7;I%H_tK3dq1LM)}Qx!%%4Wx}gA^ z%VoUX)vT$0@k|wem?YEZ-ivT}W92qxRSVz09)CDNiXj*Q)e3Q@a83>gcClG^5nz3@ zPk)L5`oj+lOAd1{++I1l|HX9}p!^5=EUA1cPXHdRU41sE;q0pnQf2A!US<2@wQ7Tx zN6&>sHnRCXb-jYw1JU1^6+9dp^;Q+Poos7S(+!>VI>mOchiEYZBw?r0joH zhCd7#CPE&`qVx){=@Q27nl%FJ1V$YJ27VlGzn2GCaY zm}&+!Gf^VAnPZ0QJmYX9vCu}l291sTVjoaH_@$th}PR;>j)PxfrOmVkK zTLwy(O3US?yQ#!-wUJN+FVbZdc17%-;uZATLC%bF+GoS28f*i<=8PyFHBHgoCb7Vj z%?Xh!M_aaLyZbh2?nMa(`VQ^yX)$XZV*g6`C4&A@p7d_6TxHL6S4PtPD!GjzRkxkI zWPt>jzh}koJW>MmwV%oj?o2B2PL>bQc2gmfDExWk#zIB{f@5GG=}}f-NSKT&-s)Es zyh!>&QgI2Z%XTO=tsXPorkbt3P+&7b`U6p}J%6BxdE>Z-J?mObQX)lbTxU77mL{1N z%AKh?RA}tqo)gi)Ze)6r=`JkHJA*oD7cd$&0`S-SA>lmuq}EL;!*u~Lt^#@!E0mK| zM!%X{#K+cUMCGU_z-T!YZv#=pvMPk?LFJB<(+s-1>-pWA#w|f z_FloroY^7zx|T1Z%c>#zdG+et9X#Zmyg)DA>`4It+NSV>%IzMIUMpWOl+-&q4U1hg z;G!gg$W9~gV#0OMWJLLQ(~4)P$9O1dekt|qB656RZU7uqCGs(Lpn*%o9-I~M|Iqa% z;83^S|0QIrEDa*b*lK1FLWoMpUY1$xV=rR~sSq`V8dA0yJ0-#_jD6SG_f(p(MJY>0 z5~3pfzn=Gf|IfqI@48&2uK9lFzR!Km=bZC7=f3ZVz39QOiD3$;OW6S1KU*4eD;zj! zB-3l!KCGKXBI%n0>$#RQ!$$tEx0;ZCrr*t|y8Mq`wI}}xDPr!Zs4C9ec+Wx6rnO;B z9QX6=#GS&=9dlA}3f~hn(4RGHvzv6NyDQ1~k@r&shptJ^x9@{XA1r?tPvnDtKj_?> zd1PYF$->1!f8s*1?x55+(}X?Iz45uqjUg`U?Z$%C-ye=X;dI2tWW{-Rbk2A> zcO1*SM2kFBvm4WYyt_*+dTUjZxJiPGJiaxvPJL`yOFTad71`A|NK;xK4hjHZhR5v9 zy4g5-wf)|P?_z+2nY1EsNmJ(SI`kI~#8MY+Fhrw3X6yBWYb0JeY@fB7_)KBihOH zkloxP+OYwe*G(U$9p^c*ySNi4Ez<^Vjn8ob%AC(HK7ZL}8_1pJVsZE8`Jab<2U603 zZF}19zUf^Flxy+;4u2>f=ZxHY&tAE+wz&?r;LgGtd8aYb;Ou^I+Ubw4BibXpZ~2fq zM*V+0+fX|$y7AmArA6tAZa&ReX1|2%qKDgkbN8E8WlhlCgydJ{%2|LXiTQm-#u*is zU(_&ar6Jzey$D{YTbRI__Q9dk&&s&OKSH8+qeP8t+1r21-%a2|aTHzOKiqJFGq{dRV(h-etQ7pf(l6k) z0InsRDh6u#^!3b+hG**gn4IF2R4z(35_LX2F27J8M?IvFnG>@f&SIT%b^W)4 z@N+9~7nW{yjy)Ku8U!82R9 zV)i%R@=AM)7%Br=Wz%n@*otzaoIXE^IN&oYx}<-MW{?!Y>m1q~q^4rYa|IwX#$t5a zs8ZUq?>S6japYs^+7@^6N2VoJ!hc^(Url3iN;Z%MedW?Pp)}LBh+kT|+>h|Nm(lpT^LgOW zkEOL+FG%|*`Qo}ovgu%0$**x4h4$o07RKO1QQ?dg#u=dW;rpM`NAO;dPO{ax8()qU zKG)Sg;63a)^iAiEn6M{eFIctiJ!{t=AEiUH@HL(24NA}Pr? zhK64fhG~NjXD?8P-9m5L1Ch?ycxU!_pvuYd;BPe|J+*T;?b#|Qx8Hfsi7!=9y0xf1 zH)|iNi3DjU4%*w+HL`lq3f5yGCdoWonBz4m^7KQ*WjxvVHp9PuHg;@;+8FgUboHH-`f$*gcdoSe@F04(i~%~`ZNKnB_35o;%c#6Q{*MVB-HoZ+*8*(x-^W|S-sWz< z`PqjLXp?n;s_pstKcci{izwCJqB;eAPcJRt4)yJVm05Dc{GViL5E^77iQaed%eC^s zj)_~LjO)O4C9C?tbtU%mCmc|gg*P}9V5f24no?u;GWf&qjw{+p1&p!l%f2)4>AH_N zF_?n zi$h7ZWX*p%hLiV!CUBsCM#b(Ya9a+$aR%W}btNG1lE*wIx=my0muJP({6e>G)T)=d zdZ1U;XPf{~_2C|K>Kf7f1k6iQcpXic2g6 z5y?P<|6^d2gp4~XiSNf#uUJplZ5`q-AL#JMj~>)Pb=ZY_Vs?{8Srvh?zP4zW{=-U#=Qg?B}nSLKnnYT=?@RSrAWwTcR=ws~HX~*2z^`$AfC)t4Ms77ycv! z=d&bU!5ULq*Ck%kc^a{76|B2cEC^S*kmZRFLUS0`X#N_pril$w!e`YB?d#J#kHvCTWqxUSW zO0@H0?p=6Hs)))TFNx@WycLoMp;gZJ0FV3C-*lQQIZ{V8%0vnHtX&!#sc1}B(}iqp z>xjAg=jEDf`!Aw4{@$MyQJ!~4Wv1d&1b{RZ;}I&!H*6Q0L^`VF#(>Qd>u=96Re!|X z9ML#-fJ_$U`Wr(3dD1`745oBo!M1`0fdCJ_VIwP6rkrSMFs}?ekq6xOkjRAnVXwrK z=)a14EC^**1>hbKq|cvUP2dJ%C_q%L7z}oR-jO)+XaGJDv{m*J-Sy`uG_d~>kDD|a z&n;H=tx?=M0=U8{MUDUeCpFuyLvZcg5=Jy|A%s{Vg?yeB_K?nbVbQVx-CLvchgN~V zo|Qt!RG!mWDK!4rK=Yp=-*6pX=DCRXTI2ml&l%A^HSRrfc5H;PkzNTLDWNvy9WbB2 zR_xJ|25cPJ@cjKJVbmVV2b`ksJK*TnmjJC@zr4@np!d}`0Nb4iTt#uk_nxHkSfsv( zF>pu4bek-fz1wZY*tCE-SGlFXO3Tv!M`^i#tF-*tY&U7B327=34%`S)C-H}SWVULp z8CxH{44iY&O!o*7yYloVaEsRv^4k9)4tt6Gm-c1R-;@A3JbqSWQYIBDz`Qz0b4?V-ih^30fl^w6c$F} zh-&x)K;8F&olUDKdmf;IDqCf*_R;0gawV$yR)1EJ2f%DUi5*8zPT9#2;!YrH{}ao< zyZ=CCD(s6`U|KM5u|9x3&#G-z z775T9)Jg>zI17$ZdT=+%_pLWm%0x4AB%Fc=d7YRMAWbb=HzP5Y)$AGi@=h77tVY6NTyPI=omjbY}Zj3z>?k$Th zkLDmCXv1&Le4Wb!Vz;z4{rdGm&5x>b<}`?h=jnfU`ut_S8qxptzvsVsCIVMsbFv`R za5nS4Kf6Y&R*(BW*81Vz>N0TqXGF+)#y->f%wF5I(e=kK;(yh52_`jqIQsfyGH=4& zB(--A?6UDz5@G}3RRI0PLK#Ctp^z;be%VxzdZ)&L52GGsV;jO=M-AxL!i?$-yhkn! z)`|zc9=-X)5|XGXt(+MJ26n-4mgZT*%hlztV!n|-f|<}3*HS$A{HT=YE5@NLkKX9}YW(`05;$QXMTU1ru)8_?t9yQ=11WA(A0|;!j7}cDYqkUG|ZPW zqaD`^YwGJAPfyuyE=n#JZ7#SjOm~w+Lwjl0_Vo3CO<^wGUK3lconH%B-nba12i#s6 zeEUm)V}IXf$oyvd>f*o#JVwEn<+kkndW+-Ci`x0k&nc@Tw|_PaYo7j5A89pOzqFnO z&-(pa?R$+|0c~~a#_jx#4b9D`W6N<01%2zwX9{E$9={b2`@FPUbz*wadLnS+JNNrb zk;^~TIWfETocQNYEMYe*gB1(cRY8t4SjfepY+UV$1POi&IrUz&4JSGO?Kw}}ou7`r zAz7mN@qZpN&NzgYBHKQys^Yi7P7ZNvj~Xl2QFGnR1d4gasX`E}HKPtl9MAJ*}U z&(warb-Xp1^?g?R{TX=H#+x&rr_;M7N9tc?O;(PrA6mY*d8@GXb>BwY+A^j$>%8kr zk4@%C^qB(}l2%22g(Po!ZJY+@>ne2)KM;-|NPNFJIk2G@x<0a5nAva|;j69r*54rf z-S5?YePq!mWW$25%~71yK|)TK=Y}vJw#3Z0{*=XSU^~StOq?D%^r!M|k|Q$HG1E3D zyDkY3(iw*b?cy5~xvMjk^~lgqnwu9l#kxFxoq4gur_xa8{;Wx#jUBz~A3sM0_Hd9p z_w)D_*`R}n#iH6O!J;>podaV6B!h{9d%R{abY}&kqaWpPiTN{A(()h98177^vy}tfp{ae8*{rzUu zhC@!5UDw2d_2hzu#P@ICek${wp6ywQ?}KGcQ-tnoFMJ2YS8=n)V`I^l*_`!@X~Y7Z zyR=;294UC~#*G83eHP}+nU6z*7DD=ucov@&LbXqet^c+dSGnD@_sp_d3LE47t);cq zkb=5|E~Q@^LSqZ#5tlnJYZJ7F7Q;zn|T zi@YfCg&d7pvH6WMA4|+2o|t<8+QUp~J4 zNp|n*ywO;H(fYV-C`SIa2Y7Jtw{IMkeVdI^*LjXoWjM%SS7adzw3+t@aT+zbzN+MC zNVE%^uD@OAcBcoIy6Gby=rjK{G~3XoqU#`vzgdJCWB~Ted2?Y(B?y`k4@~&Q(z9q! z`719H=H1K^%c>`rttW#fl~>pwDJ)&DJQ)X(k2~$Fea338u%Jk2x6E9I!@}Dy!p1DK z0BdWUW0^>c|Ap?JKSv~oeR{lM#Vr*Vp_F6{p-n7Q1Uwa&R9YatU&YSXIEGEP3$hqb z-It5jEQsoYQ8$7h)QR~}?F17dd#?RkfBjW0L(%cY)Y|5P%aY{XJhWZ^_&ICC&WjRY zMeyNmGPl57d>3Js#;=@tYSk_T}}<@xQfP z?36PDc1#0JLXUGiaJTk)qpV;6t}d>BS{L0ByBxiMuc@x@mZIi_n(Ov@CD0guGnJ;_^bk^7oKMhs`|4pU=bO)C;cus&-@aXWjlK zzAR#(l3XXwcg~uX(4(5r2c_JzFkN1F@iSNkP(;(k)FpxAav?b*^7SmnkQUdDdLERJ zp|sLh;4Z{kAr@nXt__zP`=Ff_?unfsYVn7BA5>ifeKoqiq1!*4$c$mT1$Ak z0Ddz;`VFH1ASioVi2qmJ?O!)Gv%@BTXcG+4xLXNbiobZ2G)>$uy78d`IAsPlKhFQg zZJd77FatvIr(Be_FghZ;R5GD59e)M#egE=w(ySXL#~JF24Gaj#(_U`%W&Sw&r30Cw zDRX{eh)lJ{?iRlDkDs$>Q>H96xLFi0gTWPw;EL_}P(j9&$?2NvS|la2pk~j7(J`%` zVLz_P1Wsr)`-d)Oe=%^2=~0bWRX%hr^T*Pe?uAk#m9#RouC&wl2SPJ;CZB4wk@Zq^*5UWimBTa!DRJHm2d1@elbK|MWDw*j3YZ z+i=J(^P}8yc*e`SSx5#?K{C`uG6xA$#{yKorpC*CgcAF?a`BeLx*QwQWph2$o^8{* zgUuEOrgr@z8YExOi^}DYONo<~bJnXk#gKmEo_zhm>9{W902A;?Y@;&2lwYi9k2W1! zYCwtp{HJh$-v_M#!%rw-Wl$ip%e?m9YZm3?1(zP~jOU7PhH|;c*^sg>GV8zaVhcN1 zFc!Za66N?&%e-hXB^6AbN{J>lUR{9ukKG7-x$4jLH2rK<2n%;gtLdGXe}c__e+Qw? zvm$<#K+Vvc|0Cy9V}V$B$#)0>t(UuMly4tz{kysA-yhr>T!Ed21eL*SZ;By{ zOtreW-lA;pP%n6_ZV;!qG z$&a4CWH)4)RfI=_O2ohdNzfBqjOt}cW7}b_!HQ?u z7_I_rh;_Z>qSYua7^GBH*VQ}-zHW;9m!<#^9-&OtS0k3aJ$pSU7mk`eDi>}*slmY} zKx&4nMmtt-+3gM&xl@pf+|_~%#+E0Cs+BQEdVp^f)uG5 zqbHBmzN8dus3{1lE9)1qF>c*iI~VdKfN?|k7DaY2IiA<7)uv% zAQ3GhK5z-4Bm*^gfk;_4G!9j$@fFwN&x_(9cCj~DhQill9{g#NlXN7Wn+(YrsMcE9 z=XWMSRVN_FEan(i8jh;a_+kVi+Z&jscQ?*vmI63D0dV~*xj zIG`!e8fP|wqLu6${Zm)z#btaeVu5cf1MjkQU-AgO!Ga*ONQ$ou|I&$ zXv;%SBCg&A0tc^*-W)g(G6qnC3Bhy~I<}{gDa`bWy-(Bel%VF*h^vw3ccL+n%;hXX zNEu|{jBu3SN22gw-}EsGp`*&_#lIs~QO(#5*@`QPtOP;i(h%~9AHL|XcQ2kPkNuaJ zPTZmSR~EC7)axur&sA#nyGOGz>>TDQ_iE&&L{=5?6U$=l9>9DWyl6YVhS{?Vgi2WD zCf8X;vW~#8<-}LcCw>=_&(Sw83G8r4X!O7#z@2pQqXr(R;YA`mTGmJ+GWxhPeQPeK6@SyN6^8XIQV++y?;#uQ0Yl!QpF`S9$IHc ziPKO4dTT{ytQ4q3K^uCskdBI=D37Y}4#iIE0;aGPmBwS_sZ;8Tb{U_r&jQ|7<~c$I zPJC4zK0Ab%P2xEcHC&H?)115evJ=XeHN6G4gGd_zuWwndvM9U}zn?B3~_?T}R z3kIZ70aOQ>m}@T9yf6CS6El;zhB;bLVT`66uc~8#8rjW7(sbPC{Fg!e(fhbW5AFbp zTf)gER?aPAWii}z=Dxt;^*H8tg=;nUip~0}(%b6Z%-9ZS>{-)qzv?5TI9l5k!OucM zH{$itdN|Urf$EmwBUpH357FTbudbllWz{P?Gz88Ki$emswZcUX_+f#^+i<`zIc7cM z;PyyTf<8KkROC>x%5;UY0nj|4C!}_bv8#E#i2Z-P0NXKO7XbfX<+;fk97P6Lm8;kG zuT_|NVpWFSfsmpn8$$Awi0E)$YMk1${jn#f002r&l%^J1sTbV;{D!4lvY4L@(f4*} zUPL1#!ydFH%?N(}Xw$uE`*%oZRq;{ByZn<#1PO?3z zYxs8`e0d-CUj`NUpNt12QT<5Irnd#eLh_6&=T^elYF}7ZWBZq20MUl@N|$K_qdceHgL*;Sd}PW#U<#_K6N?u zA>|$iOcz1_m~U3FDsyhfQ+Kc$Tjuxn0vd!TN)0@t#Y4zXHAEh?_|u&nr3j_Gr~n*D zrtsR-`F900;u8od7@M%Mj$A(yi-Ny@wKK37=|20F1*1yKkocOg%0YhPiZsGbCZQ~F zZlrsexhu8c;vNpHTb_2r)k+%!jpz5(5YHEcxRkcH_CwV6MOTm$)U8IvBl#O$sNk)AI z@a?(|``&iDJ7#?6MbRsS7;snmxYp5uz{ryoZd5&UuHZCQ5-7xQbt=ef)-VAalkHu7}+eOA!MFb5-`x7PGVZe zus2rP=^1Tj4Ecp+k2Nd8+&gce1& z40{Jfv=BFBjCL%3ehN**q2Ljg=0CuXdWixNM7oOHu%HnQVWmx`9;rx@W!CG2)$@?f zwm0W({>oxB2f`EcWQ@=N){VW)X zVpn#=d-KQyAPnz{(;7)P<+l#^Br|>UcYr$9a}wWDR%A7B>mUkT1VlYVHU#HK%)V-T z9!=v&718%GAL;>&SakWgt0k?CcbjQne~5*#V9<`lLO&d>CLhj*XpM)S^O-zOok*=v z)J2=eMbMn(5tC{cci7w6P&P!na6x8J1g&Q95VrjJhmv;fD|jjD6hG8zOO#X(lD;?= z9%}29VZ(jJ+_g$mR94k&rwCm6P`w+MmV2S^f0KZm$7=(mOqBr?Jie6?H!kmg3dN7K z2Y5QbyxiZzD{uVhp$&_~wZ0>kFrF~e_Wdj5>*7TL+#yj#n4P_<2u$r6+Vvyj@W(Lc z$8f#^2b&0w*E*MZ@>nn*@{yq-d~$^~jVq1R1P(p43VWf5<0zO45kdy$x9QxKC?4EJ z}Nfi)}PTr795usOQ1Q=kAG}uaAYm_)rw!C0}Md<+S$BZm4*N+e(%o>a|HQ$(|Nx zffgB#ZAwxRyg^^TcjUvEvOFJ3X!a$|Uml)Y9NkwCP~H)zXftv3gxv((QI|m49}F%& zTwWwipsdt@i($(plKkCP3d7)Hp5<)mx$r&9bwkMMJ;>@$m|>0p(%r5QRnSnPO8%;h z?hX^XgFQX6_x>jgoiTUsjmFKV{WFgPYu1f3#y)t8e6q>&9sA5fwiRn`ux+{aRme}_ ztyyj`;Myk^jYub1F!IG8SsCW<;bH*~XkGOg2jbx6mvo%{mfZL3-41wE_q8W}co@}v zOXdK2O&+Uk%E2S;atoUlImqjUl+-=Q(RZV-^-%z5I#lpNimOsepWfT16{F5aDRnjV zwaB@1RWu{pP(3z6@Ih4j3JM=r(?QAUf&yAr@yLJ#_@yM2>>lKymq6&R2`ynRYFEg@ z-zV<$1t+LxYfol6X6_|v2bDjfDQV5!(;U*d>E%fLGGsRtn0#W*(;6)oP0Ckox`!q0 zEyX#7YNg~DEuZh5O*9p08E#Sinn&iJH;=3_dTo}9vRMg}+E((3s=8=O z8k2S4sk+5|f0l7r(f8xw#!E%ctb~bb15!#=DmSe2%6s^8V-O+;3{^N3Nh8`=NE8!a z@a>GQ7vqrp-PJZcz6#EFY9nYw9rsDu8iXL=Jid#GX{~ihQo+&;6FV)8Nx;!k>gxHx z^B_bg$T~0yB}Tu0xf9phrh4e4=~|d3&aHK?}hKWAXGtq<0;2w zSVk2DR@9>LwT6hXKf#Te))i!lHu^u{F~>JXsI+L&Q~YE;&BFJvW)eivR;t1QNQ07F zqChBn%W1v#S2%uDh?uboFgm}WZWwvkfUGnD#A-0>j(sURgT#EyGX3YGS>S2e1A>a1 zUGM0EO_Cwx5*hq=Vg}C=2RXTmbZl$Ixv}&kCjHEd$_c28eIZQv0G!g$efGyd?HIx! z7UcySv}^h&Z$y*d=i|H2POd05!|C(Pz1>()zqYU%ZBz&CmSi2#QN*IhfV!OQJ^FN! zh>=D*$r+M8z{RPSxWIQe;O&Rap!%oqym1jd+59ASHHV#ut+>cs)-5Phw-9rwi$#*G z9CO7!_{?R~Gypo}k-ES9Gnb;wJ;2-1RUKjPGg{3quD3dnU|2KZP+ZpgRD}DuJ~ue+fa;TNcz@|J z6WU#GL1~Qe{LxN)eTe9OBd4Ef|GA@2I%6Z(?8epH(tMhd@3mFN9C*)~4_|Y&W6L%1 zYw6tOmIV}=jiC?a7`2WcQ#1`*EYZZHx)(j=3*)5-R=z5$h5l_w1HjET73jk+znj=e zs$uIU@R|(>xs(t$ha#CGepUj~;SdLznQP_b3^hXQA)Km>Sum+WHm=rjbfgoDu`#(} z%lc#FePdDO`5n%x2>N2ayi@ZIneO75P%Nug5hiY!I8ZD@CqXDfI#LE8e2nw6a~aqa z2Xv4$V=Fp@S!!Y0k&eHl5Ci7D4bLvhRMceaDX9fwrf95zt+ zOKtg3BUzy?r|>`(b)UGvi2l!G)VuuMm-=kL#q3xnw9|HaG@`|mC$g*hNyb%8QZb{N zaI|>v6g6nO59`H<9%c&cO`3!sn!7h8&qYQuBDy5gq^P$JRcY+aoR?nk4mW0j>O13T zsU9ydGbt#g!S&3>uPSjUCol#CIQwBMKIHPg_#FcVcqNMr)t|xI)X<3p^JS%bUi7&bQ-pROYupH%+^jTCfW8PwY)```O}{rtf!p4@3jEIqUY{Eevvpses5Xe!(z{6 z`SX+TBXs1Ik-e1hSurmOm<7;DS>q2vDZ)|R{KnCwLUlZWp5Bfx7_F6VXO~3o?thLneAe*g&$-Xa0y)p9pCMov*Q{phjeE0&X{p z1{4sG1G^H`>guFyMLa9Q4n#mYU-6?W-o<%K^B9q*rDl(wJ=j+$x$K{Ir+*o+|O&w?W3(lk40vOtL zE4xvsWec4K_jV!Iw3TaM>?fc_N(je@@&h(jX!=QB(XkWCG1cf69Z=ST^$LkA6)ONsr%%#QO&A#6v_ z&>+m5##Dr7Zv;K_0nvI9@ask}09+qC z27;vsNLMXPO0Yg-h?YTsiJW( z5uEP0#na zLS=iCbsg0n8V+K3FlmU}2jiS>vOhzwE~F52WOdIl=#G|JncHzA(2Z7giIVcf^FUio z4bjD<_05%~rlhMulh5Okh5SpoW|NAdVo~vGeYFx`Xq^c3 zSm`RWy>u3?n!Ond zUO$_8c=@g6ui(=iiFvXT>SixX6B5W%68)8BexaK9Zgn-P24 zpKBzxXK64$_iT^#&mQMFoONKJ%h2Uun$#5#G7zLftv3qV($$j5oQ8CZ+=(trbkJhS zba=%9nR5Q;aVS5qaYG-Zgy4oa>l%Thz`#)W?|9hJY^AB*1nil1b51T|r5SrCC!f_t zvxkX?VBkfg?py)MfXod22`TNqiEQ|I_;9x} zEj1+?XIJ{XZkr`MW3zMxhg-GPY22WbO7mIW=;PI*nZo9Q*R&r#`27-oAKYd(_fC~> zd;ZyxhLti0$x_}tAd%rn>q!8U>3|I!tz9J8bhAM5E%8QV>sud*E=*TE;2u#y4`4;F zj1F4cxVKKr%I?YN0b^vEy5dmxm{Nqo1|}&lZ-`BhpJadoh1q{)fhyqe=g@jaWIQXw z%Gr>OAcz8&0g+SH(ADPuA32?kaeYf{@`U@rZvdinW@A7xIZYNRgGR?I5g?mcE|?qu z%@{-+fIIwrh#&)4QAcVw`6Reoq3muvbK%EqH2K=h85t8@w;tQP$TPC$3*1E6Jbizi zi9~|=m1lZyt`JPmn!Wu}y@bROM z?q+*RgzNDCI_+U*a(Ccxy5>d>pUJe3NmJGZB;2jT$t8b(-!py(zreS`>;jU+N21Rj z@X*IoD33%6HyZLy*|sOOQ3SYolPAaz9$BY@E?8zoI2^b1%8f($uYLfN5Kg^p3|OBQ zxEoqnVnojOB%g}IOO=NCi^!FLOQo_I38*7+AhNJl$b?+n^1Ar) zG!QCiKdR`gzuQ3@ZR{jY5Do=^mFgZOXdViZEiSQg`K61HNueJwRE;Z7SGyR)bLjMz zcsml5`?LDPI1M-{T?3(eI!*qQ4qiQe0(+G+ua3F&9(Vek-_Dp}PYWCH!wug+VydD@HAZqp{+?pG=z?rk zL_;>zw?Mim^e71F$B)T`QNoS+G1jg?4cFfS;i6YAxJoK{!y)&gC3wjzHod<}n1mxI zr0X%R1jhmSMEtoKi=guFOB%*xSFGCwZZZ!))La`6E(RBNAEh!O#X4*VeVp{;q$v3; z+9DwDbgxr?%X=4lSK>vIlo2WykFX{dzCk#EUTa+5=LiisSx^V|Y=%<1;%sL*D99-T zG2!zWx;W_>%^@4kdX{d1qOxFciBS$b%99KO&r+7I{8j=K{K%&y>sdoGHh8wDBev1S zY%jAjrPx0+Gi^iH=y-FI$Fw`p?1C>OMs?Ypjrjewl`Ck7dY7}~SbtJ|Bwjl}f=(51 zX7hhV%~S7-jT8`*OSn5w=Xoj)K#oZw#Bu8%H5(`60|pPb>4xLaTfX8cRvv-XqXmfs_HR8EGZ;HpmL#Huc-BBRSQo&C6lb(*T1nEo$@+jvD(Ll7qlGTI9yTt#jZA zKe+28++rDT%7&oFL(UX{pj2|4#)jTe+kP0vB zUd*yBmb~suub(=a9I3W`b|vlJwJSH@oaE=P+jw%tq~9P5X{xNTxR!fZwYMYu%>7&C z&irAcPTj#K%IX*7G-U`Ho~r@xN|dK_=JFE|s4uV5D>A;7Wd|^>2R=$8XkbiVWVHpE z%qp6vv2TxH4tAauF~m!e8GJ}U!%7ei6pK)|c_0tuO)1q;HT9I^hGn%Zwn-LG;qa(Z zvQ+UDr+~FFKh(&zwUgZHBC?(c=oK-uBcMfE-$cusoP6RCUa2oF^tK3qn-ICocz!V1 zL3M9q`j0Yn@Net)N%e>a4~-HCd?x*AA5_-lB<)YfGoQH0v~WzoyVC*F*285_URFPN z5N^YvC$$)4bA1ap%e`;gbgKJkD&lx84*3uJ(XVJi#!HywPwf4VUIuyO2Mm`8y}$Tr zwe(}#j?#giGOsM@Baqrxb=d{NC8<#f$mKZOXP3-D(Bi2D z1M<2Qj$Z!U&^iWh<4QSNI%R#jB2G2TeAy~fQa*U*WBPK~HPlD+c{LbwDgB8zl0+xm) z9kT!PMjMA0={Uo0sh=Kwx~QoxE2bvHrvq_$snPrZK+C1CCz8#s>7ots@MoGfU9Yl0 zY6rIa_+xjmrwq%9Wj1JTF=nyT}GOcw$FXthJ#9 zW+BY#O_5h+lLAZQu`BpZ)gxIidQekj0i0_m!Ebc?@#9Li(VNd^akP@Sb1 zhW%V|Kpw9;>{a$RBt8iPd%~^pQj|r0q+2I6O|XfX3x93Vtw22@zzaW;0$QnE$2vOC z$r_SVI&282D|(2}1rdkB5w592OJsbk@bTq36@x=TUJoA{rB#eygu)tU9V%jehI0_> zE7W5Qs1?(F4Z;n&Tm{L8uaVKi-3#3XNHOS^mQq~+2Q-oIa!~+j50Z^$`FQLYQPAS@ z@wmR%eggb;Uvp-)pC_dEe?9RZan?TD5~sfJL6Zkh;@geb5T!Tx;^NM*c`I$F`C^lD zjG*ME9Jq}iynIVP>DjZ-@-aV9l5o%#_3|7q zV;t`p=56JiIU6)$kkaVVT~ITS6Rq!O<80B$Zp?R|8%A!bDTn!=1pA*myRc9L0}_d*iv!4_@}y z#JcmdBM=(c$=+wwVF7E9bTPpd!3PFHd4d$Z^wHDO_z$U$j^Q1{S8@1bTO=Vl#+O?P zfxzNxTe$d8fp`>-1$yAWA)p(U?&m>=bHG#?UgW>^10B$6g>V8vDO|irQU{ckVDU)S z_lt4#yVN166p$=6Tbl7o767wE)SeeD!_f&SMKBcM9@ujU2O_)$5_$5;g=pfmn52fH zKU1&Wa-nZ;EWe`W7>Ods!Ysn@Ts)Z>Az72TO z3LI?~;-iy()-3qzNk2N=ujJT&Lcj$IcAQ>8>M;CMBFLD(`gBLZrM4T0Z+EfA!*$(H>S zE2GYW8{66zw;A+zYXH_%$TGzZezy!_VK*sJ^d;jUieaGV?pBRAy-v^7fQINd93B>W zUHlst>JXByW!&d?`7!ckT!4B^hK!5jqm1s)gRj;73N7@HgcO*4F%D$fQ4*DZ42#Rc zkPYdz1D$?H`eO%?#PKnyiQ%NFZ~7{uURU<{-t()k=FHKmGZp%wm@#f|;YHxM9Iv9Gd2V&tm_oT%J?uwTv zen|mDs_R^>5|1ii+roGYH05Yd@dY*D&jG$4RFf@|$f+jsaM zt;Ikda;LPI1tV7t&v>QdN`aK9+BvHOkNbiMp}8+9-NQ!Y-z4hSiYHvNP98++!|{5h z`v;z~L_=Vz;$CO})?^^+rWFi{SV>Y5{I-+a6kvF6g>528hnVsu{%!(<++ENEIp-|R zOZNoqsqjSF|nihY(31Z6`KmexZ?E@m*qBoCd#aBVuN(<9p<^ziq3hn zAqsUc_jD0K{Lm~;RImk+8(m7ltU`eGb_>7kE2IM<5=j4z{wVln9cZ_&Z zj7yX8rhGQion#$iaa3{)Jgx--M1JOIymYZeZ!=K2s;S_&h=8x)!Vmm1ARlDujveru zXORW|uT>ux!@o^&Pl;05ed|uz;}dJR3van1Ho%wwVsR|Wt+!dDQryFd^418|Y{{7t zQGN)hKo;eM@?wEjx1{Y;JMjd;hdK|+aIG#Ud?QBpL=|)m1C?s~1XSATxAjj+ROYBr zRRA0XE#vdC0THYmmq%^gc-Ce4ryL?$Pux%id+Bepfe+dI*m5JrL0fniStOc_CjkS> zec5bO*i@5hNS>vLd^Q6iGDuX{o3&PsT0d>Lu1LDNG*0nvD_W+H3ZjN45QlU~W?<5a zT!bwW2XDLd14N`U@F-EWSM{GEeO6s<0Ob!x8=Oiz(soQ9ln&XH;GNqJDmRh&UPkt z*B;LVRz%$^h>`oG;ukR-{(>~)l)W_Kk*omYDV{3v_$e2u1VSErRK^!oB~o<&E5Hd% z>sgCF=eB97#$ZLb$e?hB8IHV<+)@&aN8xz_`#xFe%=*u=*9h51-~*G_3gJ#cY=qxI zHlfQJX|(JZ>Y}LfPRnCsV{kXatxIE+w)$!){3OB>mHEc#*c;*m_N%QHH|7%?!VHeW zG8)^RzLxQo@HtCv?Np?emsX^?2;VtPEV&bF;1&7 zf#baMO=Y_Y+F?pMa!N0<&a+!(}`tZ4Neanw)xa!Z2*DFyEuaHSK5xN7Z+K$ zGW3rqqUdG(d|+RgNLic$u>sC9-r=fHsv`F!ie?H+h4=2}=pdB^Bii6J=Pq zajot#y&^l%K!NHEKyef~omA{kYEcRxwkQpdT8xkpfz^Nk$5;bMJQ@Qbk9?FOSl)xc zZiwNQsqroG?H$vH4VK|KKnIa5HNVOES)2a8ZECAO0-m9ME+l+2nq3+dIlN(E>f7h~UVpMr89jX@r<9C&oMiZ|zK{a*_5ozpJFioCy0$5B)G*#)r6lZ#$d2 zizmN^gZ$l@^b*XCgHH#cmXCaPFaQx=MzKUYeO%JZck>8Tq*F5|c0w_7sTQdECH_*Nx!~vT^!*8G8 z*{hm=8N1wb4KA-((q=7Hu4^nY=pft-Qcgfmy~^TI^6ER>+Wt-E)1{xehm(blf8ujw zM4UPD>)`b{2qnCrC}r|hLwZ}3vF;?pAFMlA9Et}&#@o#hD~<#(?as9AH#VUoP<43Q zsYw&B(H7Sk-Da!O(j)!XfcUM>P@>M66*W?C{J=3RF!Dx!)y$i~`Q}dsUs-UTnh$U7 z{~;l>(f%Xyz(xz=s$ni;Hn|1vg{!MEP0w|EDAOezDXnv}b0(r2mxxySY_BJEibFg2 zo8}Ir+Qn}5QDj1v%80j7?;@A$67o_p+|r1%=t=3~I!gE zl89(BE!v8>rt$<{HazD;eHG*&YXC|Ip%R8U-16(>79rJ(U9!bW9Aq+3ifh#m?>)B7 zdI!_MRR1`ea**}N@#6d@pnV9Bbc#rK&nHoHGEPuhh&E{Y)0nHaQ)syfy1h(L>tUFx zfY-5Yu9r3zuW}4%3xSmZnR|H>s+nI3h)UAbdqNQr((#`1B|3X+eJ_ei1W@Z(X3ZVk zC=J9kn)80dvRdf&^kq=AhCP>qe9H|1qNA3RQ03})>VEI=6+#7Huq(W(Wq4Gz$)>N- z1`}Ibw?PT(8_(nO={O8y1zOEQcD|{BJ(rto^MJfB7NrS95^}Qqg0J+ecu~qYx+@k42ZW4g(xQb0be{V@3(gzUe!+QVD&JKUkOd~u_!E;!>c4RUqA zAPD7EIB3`S|H~L#d4#<_8$+Pkd?sGU3l1r%uylqiy$ZE;ck%{0X0mCihNRY+aNARR zyX9`|M}W3^ot_t+(YkWtD!&uJSTfX6UOt2GcI>YiY~>=m7UT1Jh7mY;JEExyLZ-9* zzcQ};tBEU&wm>ih0a;>IdaMK@mL&8T3P_YzQx-!Mt&m~D<^Vl#NI=jSTf{98JcbsI zhzS%~>VlBWBt%G1HX9=JAO;0%fPzS=#i&urVQbZ@ecB(|;QR$|-rVnd_rCAmk%cFF zs0;a?b6U4V`EiZgV5TF)%D>xmvr}PQ^L2Rj{m7U^oGA%6il23bm+niO?RsEIMDD2Y zSoT?InITqNo4wI%us9}(!!UI85U}=9eA$C!ZrN2HXjaBUC))%Kj0NS9vFTF$P)$CV zh`&l@TXO9cJB6g*i}D@VagQxhcQTgn|ihNhzFR3+dP z!IFMj7;-)L;qXl2x!*a(nPN^1G7#2tykI-Uq9(rMQGoxRi?XN+HKATVvO%Ox7pP^n zZEV1@sb=1J6OuX05+~49Ue4Y?UR-Hu>a;L;0o@4X*54sefJCd`h7kc_%py~j1b*Ns zK9GylO5va5c^R_Am2*Zg@**9w`Y=1%J`F3lMS_hy`A;|nkSi%_F+PPn?42;-_l zw%RAn2MR1PIUIM@7AV+POnwIsqkBPawghSuR-!%>}&k4~4EU2vc;Bp&-y+ zz{VX}HQ@+?$hIxvU|Lsw(VN(Nh+49%&dIsZsf{hg6uJj z0}l}42@dd~U6Lfb;{LnG1wDHKvcuv-p&%C&l*}04bDOuiImHpGvVj`ABD~dJCO=6n zZZM-ix5~CAOf*@suX|UM)o>~syDSfZ7fziAb9sMf`4;;r0mlamPiSe?p z#*!N@9Df5V#|+|Xe#t(v%(P$6Fz6K=_lc>#&XEdGdrF-T;t0iGh~OPbKC5bEA~eMn zjrx=ir38wNA0t;Y;U%J4GFZmU8Q(=m@a9RHI**BNlSsAFY!uY@zZ^{=(kVt+V!Iry zy+UhuGm@6^NY?3)>9K!^@8R~P@lsaZz>x1{H{$>Hr7{ZgvWI;%?Na>ISUG8`#b;pjW`3JS#yTUC-Nl4Vdbg$R&@Y zD1ycJI?zHBKVxI((vpnwqU_QoEm`+i;!pxM13nM<!RARUEim>rC%CY}^7uf;O@r3$kO2jHkF@ zioM;G#zH54!c$+)Lr0$u2P#mCl2q+|UPmx=j?AxwHOm@HQamm3f4&_D`IJZ#l8L{B z!NVV7F19UMGc0j5lDQiPMY~U=p2J>t1lM%@g3a0 zCk9*FivUY*>Q1${V9C*1E3>yX+qMn_0x%yszlgDM|H zyykjA;oo98Ga!L2T&qY;=W~V1RzbS|nRMxa6B_}E}rEW2s_V;hjn%87CkN$-QR zdifh1euIM^uY?zhRCg-EN1SSTY@4Xu!Qyqlp-+qI^k{JD?%9VmeJUf7!)O{GB{l3h r%;X!RKqZ4)TuKIPK|VHfnr&Zybd0glKari>{T_H&p^+gfCX)9LDPLv* literal 0 HcmV?d00001 diff --git a/public/og-image.svg b/public/og-image.svg new file mode 100644 index 0000000..4d995cf --- /dev/null +++ b/public/og-image.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SPLITSIMPLE + + Split bills in seconds. + + Split expenses with friends and colleagues effortlessly. Assign items, tips, and taxes with a clean + + + spreadsheet workflow. + + + + + Real-time totals + + + Receipt scan + + + Shareable links + + + splitsimple.anuragd.me + From ea53255e81a6fa8daada90bf759f13f45d03b594 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:33:38 -0500 Subject: [PATCH 04/24] Defer analytics init in production --- components/PostHogProvider.tsx | 120 ++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/components/PostHogProvider.tsx b/components/PostHogProvider.tsx index 619c3d6..94ccb20 100644 --- a/components/PostHogProvider.tsx +++ b/components/PostHogProvider.tsx @@ -1,57 +1,95 @@ "use client" -import posthog from "posthog-js" import { PostHogProvider as PHProvider } from "posthog-js/react" -import { useEffect } from "react" +import { useEffect, useState } from "react" export function PostHogProvider({ children }: { children: React.ReactNode }) { + const [client, setClient] = useState(null) + useEffect(() => { - posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", - ui_host: "https://us.posthog.com", - defaults: '2025-05-24', - capture_exceptions: true, // This enables capturing exceptions using Error Tracking - debug: process.env.NODE_ENV === "development", // Debug only in development - disable_session_recording: false, // Ensure session recording works - capture_pageview: true, - capture_pageleave: true, - session_recording: { - maskAllInputs: true, // Mask sensitive input data - maskTextSelector: '.receipt-title', // Mask bill titles for privacy - }, - loaded: (posthog) => { - // PostHog loaded successfully + let cancelled = false + let idleId: number | null = null + + const init = async () => { + const { default: posthog } = await import("posthog-js") + if (cancelled) return + + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { + api_host: "/ingest", + ui_host: "https://us.posthog.com", + defaults: "2025-05-24", + capture_exceptions: true, // This enables capturing exceptions using Error Tracking + debug: process.env.NODE_ENV === "development", // Debug only in development + disable_session_recording: false, // Ensure session recording works + capture_pageview: true, + capture_pageleave: true, + session_recording: { + maskAllInputs: true, // Mask sensitive input data + maskTextSelector: ".receipt-title", // Mask bill titles for privacy + }, + loaded: () => { + // PostHog loaded successfully + }, + }) + + // Generate a unique user ID for analytics (privacy-friendly) + const getOrCreateUserId = () => { + let userId = localStorage.getItem("splitsimple_user_id") + if (!userId) { + userId = `user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + localStorage.setItem("splitsimple_user_id", userId) + } + return userId } - }) - - // Generate a unique user ID for analytics (privacy-friendly) - const getOrCreateUserId = () => { - let userId = localStorage.getItem('splitsimple_user_id') - if (!userId) { - userId = `user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` - localStorage.setItem('splitsimple_user_id', userId) + + // Identify the user with PostHog + const userId = getOrCreateUserId() + posthog.identify(userId, { + app_version: "1.0.0", + platform: "web", + user_type: "anonymous", + }) + + // Set user properties for better analytics + posthog.people.set({ + first_seen: new Date().toISOString(), + browser: navigator.userAgent, + screen_resolution: `${window.screen.width}x${window.screen.height}`, + }) + + setClient(posthog) + } + + if (process.env.NODE_ENV === "production") { + if (typeof window !== "undefined" && "requestIdleCallback" in window) { + idleId = window.requestIdleCallback(() => { + init() + }) + } else { + idleId = window.setTimeout(() => { + init() + }, 0) } - return userId + } else { + init() } - // Identify the user with PostHog - const userId = getOrCreateUserId() - posthog.identify(userId, { - app_version: '1.0.0', - platform: 'web', - user_type: 'anonymous', - }) - - // Set user properties for better analytics - posthog.people.set({ - first_seen: new Date().toISOString(), - browser: navigator.userAgent, - screen_resolution: `${window.screen.width}x${window.screen.height}`, - }) + return () => { + cancelled = true + if (idleId !== null) { + if (typeof window !== "undefined" && "cancelIdleCallback" in window) { + window.cancelIdleCallback(idleId) + } else { + window.clearTimeout(idleId) + } + } + } }, []) + if (!client) return <>{children} + return ( - + {children} ) From 49c6f7f6f98158dcbc07a7486bea53ef782596c2 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:37:52 -0500 Subject: [PATCH 05/24] Fix metadata defaults and robots --- app/admin/layout.tsx | 16 ++++++++++++++++ app/layout.tsx | 7 +++++++ app/og-image/page.tsx | 13 +++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 app/admin/layout.tsx diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..e69f834 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Admin - SplitSimple", + alternates: { + canonical: "/admin", + }, + robots: { + index: false, + follow: false, + }, +} + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return children +} diff --git a/app/layout.tsx b/app/layout.tsx index 4991a26..0e046bd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,6 +12,9 @@ export const metadata: Metadata = { title: "SplitSimple - Easy Expense Splitting", description: "Split expenses with friends and colleagues effortlessly", generator: "v0.app", + alternates: { + canonical: "/", + }, openGraph: { title: "SplitSimple - Easy Expense Splitting", description: "Split expenses with friends and colleagues effortlessly", @@ -42,6 +45,10 @@ export const metadata: Metadata = { }, ], }, + icons: { + icon: "/icon.svg", + apple: "/icon.svg", + }, other: { "theme-color": "#1E40AF", "msapplication-TileColor": "#1E40AF", diff --git a/app/og-image/page.tsx b/app/og-image/page.tsx index 64757dc..09c278b 100644 --- a/app/og-image/page.tsx +++ b/app/og-image/page.tsx @@ -1,3 +1,16 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "OG Image - SplitSimple", + alternates: { + canonical: "/og-image", + }, + robots: { + index: false, + follow: false, + }, +} + export default function OgImagePage() { return (

From 2dba37652cd4d1b965e2d25edbbe4f58f1bf8b79 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:41:07 -0500 Subject: [PATCH 06/24] Fix idle init typing for PostHog --- components/PostHogProvider.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/components/PostHogProvider.tsx b/components/PostHogProvider.tsx index 94ccb20..8bab7c6 100644 --- a/components/PostHogProvider.tsx +++ b/components/PostHogProvider.tsx @@ -8,7 +8,8 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { let cancelled = false - let idleId: number | null = null + let idleCallbackId: number | null = null + let idleTimeoutId: ReturnType | null = null const init = async () => { const { default: posthog } = await import("posthog-js") @@ -62,11 +63,13 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { if (process.env.NODE_ENV === "production") { if (typeof window !== "undefined" && "requestIdleCallback" in window) { - idleId = window.requestIdleCallback(() => { + idleCallbackId = (window as Window & { + requestIdleCallback?: (cb: () => void) => number + }).requestIdleCallback?.(() => { init() - }) + }) ?? null } else { - idleId = window.setTimeout(() => { + idleTimeoutId = setTimeout(() => { init() }, 0) } @@ -76,12 +79,11 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { return () => { cancelled = true - if (idleId !== null) { - if (typeof window !== "undefined" && "cancelIdleCallback" in window) { - window.cancelIdleCallback(idleId) - } else { - window.clearTimeout(idleId) - } + if (idleCallbackId !== null && typeof window !== "undefined" && "cancelIdleCallback" in window) { + ;(window as Window & { cancelIdleCallback?: (id: number) => void }).cancelIdleCallback?.(idleCallbackId) + } + if (idleTimeoutId !== null) { + clearTimeout(idleTimeoutId) } } }, []) From 4f236b49f94693beb82e039ed6437a41b11d38c1 Mon Sep 17 00:00:00 2001 From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:05:39 -0500 Subject: [PATCH 07/24] Narrow sidebar cards --- components/ProBillSplitter.tsx | 143 ++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 38 deletions(-) diff --git a/components/ProBillSplitter.tsx b/components/ProBillSplitter.tsx index 2431b09..ac5e08c 100644 --- a/components/ProBillSplitter.tsx +++ b/components/ProBillSplitter.tsx @@ -218,6 +218,28 @@ function DesktopBillSplitter() { const loadBillRequestRef = useRef(null) // Track current load request to prevent race conditions const previousItemsLengthRef = useRef(0) const newBillSourceRef = useRef<'button' | 'shortcut'>('button') + const hotkeyStateRef = useRef({ + activeView: 'ledger' as 'ledger' | 'breakdown', + editing: false, + selectedCell: { row: 0, col: 'name' }, + items: [] as Item[], + people: [] as Person[], + editingPerson: null as Person | null, + historyIndex: 0, + }) + const hotkeyActionsRef = useRef({ + addItem: () => {}, + addPerson: () => {}, + copyBreakdown: () => {}, + toggleAssignment: (_itemId: string, _personId: string) => {}, + updateItem: (_id: string, _updates: Partial) => {}, + dispatchUndo: () => {}, + dispatchRedo: () => {}, + toastUndo: () => {}, + toastRedo: () => {}, + closeEditingPerson: () => {}, + stopEditing: () => {}, + }) const people = state.currentBill.people const items = state.currentBill.items @@ -589,6 +611,9 @@ function DesktopBillSplitter() { const target = e.target as HTMLElement const activeElement = document.activeElement as HTMLElement + const hotkeyState = hotkeyStateRef.current + const hotkeyActions = hotkeyActionsRef.current + const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || @@ -603,11 +628,11 @@ function DesktopBillSplitter() { (target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]') !== null) const editableCols = ['name', 'price', 'qty'] - const isEditableCell = editableCols.includes(selectedCell.col) + const isEditableCell = editableCols.includes(hotkeyState.selectedCell.col) - const colOrder = ['name', 'price', 'qty', ...people.map(p => p.id)] - const currentColIdx = colOrder.indexOf(selectedCell.col) - const currentRowIdx = selectedCell.row + const colOrder = ['name', 'price', 'qty', ...hotkeyState.people.map(p => p.id)] + const currentColIdx = colOrder.indexOf(hotkeyState.selectedCell.col) + const currentRowIdx = hotkeyState.selectedCell.row const commitAndMove = (direction: 'down' | 'up' | 'right' | 'left') => { let nextRow = currentRowIdx @@ -634,18 +659,18 @@ function DesktopBillSplitter() { } setSelectedCell({ row: nextRow, col: colOrder[nextColIdx] }) - setEditing(false) + hotkeyActions.stopEditing() } const startTypingEdit = (initialValue: string) => { if (!isEditableCell) return - const item = items[currentRowIdx] + const item = hotkeyState.items[currentRowIdx] if (!item) return - if (selectedCell.col === 'name') updateItem(item.id, { name: initialValue }) - if (selectedCell.col === 'price') updateItem(item.id, { price: initialValue }) - if (selectedCell.col === 'qty') { + if (hotkeyState.selectedCell.col === 'name') hotkeyActions.updateItem(item.id, { name: initialValue }) + if (hotkeyState.selectedCell.col === 'price') hotkeyActions.updateItem(item.id, { price: initialValue }) + if (hotkeyState.selectedCell.col === 'qty') { const parsed = parseInt(initialValue, 10) - updateItem(item.id, { quantity: initialValue === '' ? 0 : (isNaN(parsed) ? 0 : parsed) }) + hotkeyActions.updateItem(item.id, { quantity: initialValue === '' ? 0 : (isNaN(parsed) ? 0 : parsed) }) } setEditing(true) } @@ -654,29 +679,29 @@ function DesktopBillSplitter() { // Escape key - close modals, menus, and exit edit mode if (e.key === 'Escape') { - if (editingPerson) { - setEditingPerson(null) + if (hotkeyState.editingPerson) { + hotkeyActions.closeEditingPerson() e.preventDefault() return } - if (editing) { - setEditing(false) + if (hotkeyState.editing) { + hotkeyActions.stopEditing() e.preventDefault() return } } // If currently editing a cell input, let typing happen but keep spreadsheet commits - if (editing && isInInput) { + if (hotkeyState.editing && isInInput) { if (e.key === 'Enter') { e.preventDefault() if (e.shiftKey) { commitAndMove('up') } else { // At last row, add a new one and move - if (currentRowIdx >= items.length - 1) { - addItem() - setSelectedCell({ row: items.length, col: selectedCell.col }) + if (currentRowIdx >= hotkeyState.items.length - 1) { + hotkeyActions.addItem() + setSelectedCell({ row: hotkeyState.items.length, col: hotkeyState.selectedCell.col }) } else { commitAndMove('down') } @@ -700,17 +725,15 @@ function DesktopBillSplitter() { if (!isInInput) { if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault() - dispatch({ type: 'UNDO' }) - toast({ title: "Undo", duration: TIMING.TOAST_SHORT }) - analytics.trackUndoRedoUsed("undo", state.historyIndex) + hotkeyActions.dispatchUndo() + hotkeyActions.toastUndo() return } if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'z') { e.preventDefault() - dispatch({ type: 'REDO' }) - toast({ title: "Redo", duration: TIMING.TOAST_SHORT }) - analytics.trackUndoRedoUsed("redo", state.historyIndex) + hotkeyActions.dispatchRedo() + hotkeyActions.toastRedo() return } @@ -725,7 +748,7 @@ function DesktopBillSplitter() { // Cmd+Shift+N: Add new item if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'N') { e.preventDefault() - addItem() + hotkeyActions.addItem() analytics.trackFeatureUsed("keyboard_shortcut_add_item") return } @@ -733,7 +756,7 @@ function DesktopBillSplitter() { // Cmd+Shift+P: Add person if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'P') { e.preventDefault() - addPerson() + hotkeyActions.addPerson() analytics.trackFeatureUsed("keyboard_shortcut_add_person") return } @@ -741,7 +764,7 @@ function DesktopBillSplitter() { // Cmd+Shift+C: Copy summary if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'C') { e.preventDefault() - copyBreakdown() + hotkeyActions.copyBreakdown() analytics.trackFeatureUsed("keyboard_shortcut_copy") return } @@ -757,7 +780,7 @@ function DesktopBillSplitter() { } // Grid navigation - Excel-like behavior - if (activeView !== 'ledger') return + if (hotkeyState.activeView !== 'ledger') return // Type-to-edit from selection if (!editing && isEditableCell && (isPrintableKey || e.key === 'Backspace' || e.key === 'Delete')) { @@ -782,9 +805,9 @@ function DesktopBillSplitter() { if (e.shiftKey) { commitAndMove('up') } else { - if (currentRowIdx >= items.length - 1) { - addItem() - setSelectedCell({ row: items.length, col: selectedCell.col }) + if (currentRowIdx >= hotkeyState.items.length - 1) { + hotkeyActions.addItem() + setSelectedCell({ row: hotkeyState.items.length, col: hotkeyState.selectedCell.col }) } else { commitAndMove('down') } @@ -799,28 +822,72 @@ function DesktopBillSplitter() { if (e.key === 'ArrowRight' && currentColIdx < colOrder.length - 1) newColIdx++ if (e.key === 'ArrowLeft' && currentColIdx > 0) newColIdx-- - if (e.key === 'ArrowDown' && currentRowIdx < items.length - 1) newRowIdx++ + if (e.key === 'ArrowDown' && currentRowIdx < hotkeyState.items.length - 1) newRowIdx++ if (e.key === 'ArrowUp' && currentRowIdx > 0) newRowIdx-- setSelectedCell({ row: newRowIdx, col: colOrder[newColIdx] }) - setEditing(false) + hotkeyActions.stopEditing() return } // Toggle assignment with space when on person cells - if (e.key === ' ' && people.some(p => p.id === selectedCell.col)) { + if (e.key === ' ' && hotkeyState.people.some(p => p.id === hotkeyState.selectedCell.col)) { e.preventDefault() - const item = items[selectedCell.row] - if (item) toggleAssignment(item.id, selectedCell.col) + const item = hotkeyState.items[hotkeyState.selectedCell.row] + if (item) hotkeyActions.toggleAssignment(item.id, hotkeyState.selectedCell.col) return } - }, [activeView, editing, selectedCell, items, people, addItem, toggleAssignment, addPerson, copyBreakdown, dispatch, toast, analytics, state.historyIndex, editingPerson, updateItem]) + }, []) useEffect(() => { window.addEventListener('keydown', handleGlobalKeyDown) return () => window.removeEventListener('keydown', handleGlobalKeyDown) }, [handleGlobalKeyDown]) + useEffect(() => { + hotkeyStateRef.current = { + activeView, + editing, + selectedCell, + items, + people, + editingPerson, + historyIndex: state.historyIndex, + } + }, [activeView, editing, selectedCell, items, people, editingPerson, state.historyIndex]) + + useEffect(() => { + hotkeyActionsRef.current = { + addItem, + addPerson, + copyBreakdown, + toggleAssignment, + updateItem, + dispatchUndo: () => { + dispatch({ type: 'UNDO' }) + analytics.trackUndoRedoUsed("undo", state.historyIndex) + }, + dispatchRedo: () => { + dispatch({ type: 'REDO' }) + analytics.trackUndoRedoUsed("redo", state.historyIndex) + }, + toastUndo: () => toast({ title: "Undo", duration: TIMING.TOAST_SHORT }), + toastRedo: () => toast({ title: "Redo", duration: TIMING.TOAST_SHORT }), + closeEditingPerson: () => setEditingPerson(null), + stopEditing: () => setEditing(false), + } + }, [ + addItem, + addPerson, + copyBreakdown, + toggleAssignment, + updateItem, + dispatch, + toast, + analytics, + state.historyIndex, + ]) + useEffect(() => { const saved = window.localStorage.getItem('splitsimple_hide_starter') if (saved === '1') setHideStarter(true) @@ -1407,7 +1474,7 @@ function DesktopBillSplitter() {
-