From 19aeb40863e9694d04c0964d5831e399732f82ea Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Sun, 11 Oct 2026 13:35:44 -0400 Subject: [PATCH] Changes for 0.3.0, adds a welcome dashboard, configurable weather, as well as color scheme and UI/UX improvements, such as returning to the previously opened note when changing theme --- AGENTS.md | 25 +- Cargo.lock | 424 +++++++++++++++++++++++++++++++- Cargo.toml | 3 +- README.md | 31 ++- screenshot.png | Bin 0 -> 173527 bytes src/lib.rs | 2 + src/main.rs | 7 +- src/pages.rs | 451 +++++++++++++++++++++++++++++++--- src/quotes.rs | 158 ++++++++++++ src/routes.rs | 203 +++++++++++++-- src/weather.rs | 571 +++++++++++++++++++++++++++++++++++++++++++ static/app.js | 102 ++++++++ static/style.css | 556 ++++++++++++++++++++++++++++++++++++++++- tests/auth_gating.rs | 385 ++++++++++++++++++++++++++++- 14 files changed, 2825 insertions(+), 93 deletions(-) create mode 100644 screenshot.png create mode 100644 src/quotes.rs create mode 100644 src/weather.rs diff --git a/AGENTS.md b/AGENTS.md index 29a304b..7fd664e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ The name splits into its two halves: **Eterna** for permanence and **Libre** for ## 1. Project Philosophy & Identity * **Name**: EternaLibre Notes — "Free notes, forever." -* **Mission**: Reclaiming high-performance Rust web development under copyleft terms (GPLv3+). Rejecting heavy client-side JavaScript frameworks and proprietary note silos in favor of software freedom, privacy, and near-zero memory footprint. +* **Mission**: Reclaiming high-performance Rust web development under copyleft terms (GPLv3+). Rejecting heavy client-side JavaScript frameworks and proprietary note silos in favor of software freedom, privacy, and near-zero memory footprint. Optional weather lookup is opt-in and sends the configured location to wttr.in. * **UI Paradigm**: **"Apple Elegance meets Classic Desktop GUI"** * **Apple Notes Polish**: Fluid sidebars, smooth note item previews, refined typography, generous padding, subtle contrast, and graceful theme transitions. * **Traditional Desktop Utility**: Top menu bar (`File`, `Edit`, `View`, `Theme`, `Help`), quick action toolbars, collapsible editor/preview panes, keyboard shortcuts, and a status bar. Panes are not draggable/resizable. @@ -46,7 +46,15 @@ The application layout follows a classic 3-column / top-menu layout rendered cle 1. **Fluid Typography**: Inter/SF Pro or high-quality system sans-serif for the interface; Monospace for Markdown source; clean sans for HTML preview. 2. **Subtle Elevation & Borders**: Soft 1px borders using `--ui-border` / `--app-bg` contrast shades rather than heavy drop shadows. 3. **Apple-style Note List**: Sidebar lists show note title, relative timestamp, and a single line preview snippet in muted text. -4. **Progressive Enhancement**: Note editing, server-side search, and the login form work with JavaScript disabled via HTML forms and page reloads. The visible theme selector currently submits through Alpine's change handler, so theme changes from the UI require JavaScript. Menu dropdowns, mobile tab switching, live preview, drag-and-drop re-filing, and automatic opening of the PDF print dialog are also JavaScript-dependent. +4. **Progressive Enhancement**: Note editing, server-side search, weather-location settings, and the login form work with JavaScript disabled via HTML forms and page reloads. The visible theme selector currently submits through Alpine's change handler, so theme changes from the toolbar require JavaScript. Menu dropdowns, mobile tab switching, live preview, drag-and-drop re-filing, and automatic opening of the PDF print dialog are also JavaScript-dependent. + +When no note is open, the editor pane renders a welcome dashboard with up to five recently updated notes from the current sidebar result set. Its optional weather card submits the location and a Celsius/Fahrenheit radio as a plain form; the server stores the location in `.weather_location` and the unit in `.weather_units` under the notes directory and sends the location to wttr.in only when the dashboard or the dedicated `/weather` page is requested. The `format=j1` response keeps both scales so switching units reuses the cache without refetching. Results and failed checks are cached for 15 minutes, and requests time out after four seconds. Without a saved location, no weather request is made. The table shows current conditions as its top row followed by each forecast date wttr.in returns (currently three dates including today); render what is present rather than assuming a five-day horizon. + +A quotes manager under the weather card accepts one quote/author pair per plain-form submission into `.quotes.json` alongside the notes. The first saved quote is server-rendered beneath the welcome tagline; `app.js` rotates the rest on an 8s timer with Prev/Next and a Pause control, pausing on hover/focus and honouring `prefers-reduced-motion`. Without JavaScript the first quote simply stays put. + +`View → Weather` is a `.mobile-only` menu entry (hidden on desktop via a `min-width: 601px` rule, the inverse of `.desktop-only`) linking to the dedicated mobile-first `/weather` page with a `return_to` back to `/` or the open note. The page reuses the cached weather table and settings form with its own element ids. `View → Welcome` follows the same pattern for `/welcome`: header, quote rotator, and recent notes only. + +With no note open the preview pane is not rendered at all — there is nothing to preview — and the layout carries `app-body no-preview` to reclaim the third column. The Preview tab and the View preview toggle are omitted in the same state; opening a note restores all three. Re-filing a note has **three entry points, and they are not equivalent**. `File > Move to Folder…` is a submenu whose entries are plain forms posting to `POST /notes/:id/move`; the *move* is server-rendered and JS-free, but the menu will not open without JS, so it needs JS on every device. `.move-folder-bar` in the editor pane is also a plain form and works with JS off, but it is `display: none` above 600px, so it is mobile-only. Both are present on a phone. @@ -65,6 +73,8 @@ The status bar currently displays `Saved` as a static label; it does not track u * **Language**: Rust (`edition = "2021"`) * **Server Framework**: **Axum 0.7** (async, Tower ecosystem) * **Auth**: `totp-rs` 6 (TOTP), `qrcode` 0.14 (inline SVG QR), `rand` 0.10 (session tokens) +* **Weather**: `reqwest` with rustls for the optional server-side wttr.in `format=j1` request; location in `notes/.weather_location`, units in `notes/.weather_units` +* **Quotes**: local `.quotes.json` in the notes directory; no network involved * **SSR Templating**: **`maud` 0.26** (compiled, type-safe HTML macros) * **Reactivity / Live Swap**: **Alpine.js 3.14** (vendored locally in `static/`, ~15 KB) * **Markdown Engine**: `pulldown-cmark` (with table, tasklist, strikethrough, and footnote extensions) @@ -98,6 +108,8 @@ eternalibre-notes/ │ ├── markdown.rs # Markdown → HTML with code block interception │ ├── syntax.rs # syntect engine + per-theme tmTheme generation │ ├── themes.rs # 40 predefined themes + CSS variable engine +│ ├── weather.rs # Opt-in wttr.in location/units, cached j1 lookup, forecast table data +│ ├── quotes.rs # Local quote/author list stored as `.quotes.json` │ └── storage.rs # Storage re-export module ├── static/ │ ├── alpine.min.js # Vendored Alpine.js @@ -289,7 +301,7 @@ The trap is that the CSS is *correct and present*. You can add a complete mobile `Theme::is_dark` classifies on the local gamma-encoded `relative_luminance` rather than the linearised one in `syntax.rs`. That is safe *only* because the shipped palettes are strongly bimodal: the lightest dark background scores 0.095 and the darkest light one 0.63, so any threshold in that gap agrees. If a future palette lands near 0.5, switch this to the WCAG function rather than nudging the threshold — the two agree on all 40 themes today, so the current answer is not a close call. -**Every outbound link carries `rel="noopener noreferrer"`.** These are the only links that leave the machine, and the app advertises that it never talks to a third party. `about_page_external_links_are_hardened` walks every `$0{!;FJ;Atp3Y0?>&7z{?hJq-6cY*RK zfx7Pen+F1~3%mmb4XYk|@AB^+2%@BJ>iX^U{+fJ~Dm5K6$clUU*l(wIGUjZ2conrI$)`{`NW zF(V85m}u;pvonr5|8bmbK+Pmw&=6U7f%Q^o&u~V8;d%J!PEg}llV}w$Ld#sFcv9+4oHjlPe_T^n~@0}{QDaXw%a{Py!0n{C`xO3+(sk*<%v-0r>yl@ z)UC0Fw8^;RT9HYT?y&)KRfbZoM4HpJpR+w-IVNs z9PQ;$Ep119ltqH9uV#aLRuz&{-8 zG;%-jhONP!J5OV*i;T`oaXGsn@{EWo?LFGlj~*o{+P)tu+!xg1dJlnT$)>MRY`n4# zkxg~uSnNqh#%nr$Ivg{(C1|*N7S=F5{b=77(|_(wAJvq*VhG19#Qr>J(hHXwfSqTS zu(A{(NrWT~yy;3LNm$KY#Eikr*5MPpd7Fg%^@IW)&03Fn&(Xp8W%C8DNXys6Wx++b zcAE4jr;%_vC_S{n0nvZ%U)!7q#!aljE@Smto$+07>PRh&i<&g{oD;qwY^m~|#vxtj z*!Ag?g}HV7w-%&WzqFmX5O5z4Pkm*o>wMu%>}|=r-Oi062TPZ%rTini>)voFp2=L6 z@tE_#as_UExMo&26(~{$l2(r|6nIb=tCcQmpJvue4_!P^(4xg-#8DP{-5W{ph1lTQ&-j)uY>SjXsH$|=yI0?iqxTkeK5E}QS?-_Oql*kH%^SWlkI zWobJhD75BrJmFJI#|QG8!HjT7N(tMQhk7Ga;a+PS6eFLynj;AIhvRB>^VKl@d<|tL z@`_mnrV~L=JzB5?vvvinh@YDq@KB zo@zy_u0xIujUIdnOvN!z#&Gk|dfyJX>-Pg8i=5%AT|*YQZ2PXIPyyZQ_jwMSjsi*P zaX~PiqBj)bIZMN3;VY9c$RLlIx>8TlW~9RhDS9hr-FL37A;q+2CpBK!cutQJ$y1Lu*Gdy7 zMAO?O59dfc*@_;uGmyJxm{Es?d@RV;IG48+R5tCE@r~E2#<42YmBgOBQf=eBJ^P}^ zYquAjyBI5(XzGDYNHJQ~r`V`h3?9g_&=hxNNa_6|8jk!p%~7{E6d$mhC@C8(v|vKl zzMKMHynGh4D9*b^6`?6cRWXscQMD@h;ECAvGO8)0&8>#5A#NO%@4ef{8Zw`#GJhGM zjz(Y{1gu>fi{b+0h6WSGC(?opnOtiJgcyThIDDueNUN(lporai9XH6Msh4|~K4_yo z8ZmahWZNlcL0C3-U{)m0WO3{h8o!a7;bI>95L6FAoz;|73`9B%qAF4n+!m@7?06z59Idf7YBOdn8%*%Zw;VQ6PW6qPpDW> z26U>h;Q2Si9+S)5U)KnN&}Giy*_szV=S!?CzkzY~>WGQ3CCyjL{9eIKrx)K8tyXQV z``>>p_a&s}fK;*6&gq(nx$7J+d52=8BAJZ+e4ExsrjvOxc#+iMNQpkL8zQNnW0orK zZWdPr70hx6rYVxlVlA%|L?UEK-sjiG8}IX~ zC!pMQ#T~>_msp!5$^J8WH5%#F4jD#Q3E9cX$tSCyt3>Y(wqJ;u74M!?!Emh=!xOrY z``(?~Q<>zPn6_wX<@d$S3-={2SAVT4`iR|M0~@=%QpT)Q;a6wdjw7+D$tgj}Tmfq| zBN~Xams%AV&I&A?;YB;#$agZT05|p(yf*7m!;(%XHyjzWU_V9OKL1{gTZTHU4V1$t z4sa!bg$k|;(|T2-ns4Al|95ITiZK(8QWDWm@u0)43RJ2TR~;cx+qGd_4d&zI)iD#2 zQhGxRbU++T@AqQ1T%FE;RfR0*JlpLNA5tAxXA!b#5Or_$PbW-qZL%qncihJA&*_oblT0!N3fwp!oE6dO=xi@}b0T&!Zqqo6&^`u;RbJ7u2k zbP^a`UF1tSC8FTpascXiH^Cj)G}XSql}fR|)zpVLO~unDH1xFWcB+&fpj35kYn)AZ zuIZWbn) z#ZO+HO`bra+h5?<&?|ZwLyo<7JVUtF1A`DxxB1m{a)knu7Z= zLl>NHoHa3WWQOIGFkNAv3{GcYyP8MA)i~t?S{tPrzLq?`yvZgWg1^imAF$D})l078 z&5#_E+bE^aNUc7oVJ`2kzjZ4Nnd>gtEiRM76mJFrp&ByElE&TBjY!+-IWGqE-_`p~ zYv^tsK+02W5>2s(z<;>_j_MSwu&C!CIjIOZ&;w1BfM6riu$?V1l}n%I5%3 z{W!G?Zb2i;p}E+ZJ&xC8$d77Dqa!OK-A_~t;!HE!li7IpQ2RNpZ5`#5btJhroiEXs z1FuP!{HLNF+GlB~whWObkI|{5d>7F94k6MdTPGvbM5(C~F z2&D>|Fx59{XpAi?GiDc9uWBK7M?46Xa;F)gimT*LS0~eR#{IXzp#<$o=C^xB8gAGF z=Y3Xr&^1eR={a$yx<>NC?FxlM5yS)f%wQud_5LE_sVPq89MLxWL7Xf?c1IF)?fxgz zFbpwhwCClqrUv4h>+0NAec%#rt(B;wO?w{Qw zq(VKqn5nwU5x$8I=(FzFM!&BbfF>~3QjQE$Y%obpxRi6VfRY`IVC&Awc(YI6)F5K8 z)H#r@PW-UP;HEGICLpZ{fq86@ZdSd-jFFfwWP+Mas^@lBe;YcxGs3g)NfuWZRvg-4 z((Prl6tHQYTTEy-+OK<*dDg*i)$D>yAzbUCV&TW8l$FhIv;z(r1+7Qnq&e!nR6InGw;6h1k;@S0xLRB6 zn-l{2W3~vS^?fM5%TS8AGFrnD;Nv`9zw>DFur#ho^7^<-{Q0|`%@d&AFMDn62Cc0q zMHq#@2h~)AO(08MPgPQQUjo!eAJm5GDI6*1HKM3@Y~#bVYKDr01Qd>h$<)@RQW zy!tQePT3L!ubG#p&|5+rrqgw;BB^3#tvnO{Xt@U_A!16PYr*i0glqOdOd@*hyMC2Z zLN2=0rB*j~Ie)jxMKZI%xS`Rw+JBh(TeAjtz355+JKmt!r@ib18btzcwbOKE1W+}RQ~SK)w-J$%!&!MC=c&LS z#?^ic{g=reh!V{|#h}Q%Hu)I{KU5@o4M?L5Mka%7XJQBRXvrB&X{JtK4O>%!bvb|RhCV3FY0k408nb~j=ztfdv zZ`sjl;mJ4txZ)b2J3A}5+TZaJw$S~4MzV9r#D0y8B3(5#DUm-JHgQ5*q_mT0kZ-J7 zf_HKa{_Z1P^O6(9Z_R%0!|i63?t!^Ul@C=N5z?p-QLCgpKlG;_#ent!8~?ym7{!KH zg_`dz&m6Cjj`kDKHqyb4M0)s3nkI6g(R)Ba6u$xS?dM3_yjv|vHu49c?sTz{JtL0> z`Pt1Ww@be{=UGWgw;-VP@#>p!lB}tI0n!sskcTW(>gE~uIt7iaA$+ox%3=|zjP7{2 z{FCbXsKzx9z=Cl}KtPjypTY%_g|9k;92q4r9<7s1hf33@rnRuF#8PCT&&cdBkUqSu zwCj7oAQ4wyeHwM_)e_GXT)A4n_t4x0PfX2A;@ht3)EML5n|sM) zEbW?aWE0?-f-6)HBzXFke>MiKx!jiKqM&}D)pU*jWW$QgJhm(KH{msa_0<~z$YHW- zMD?Yq3=lufPS><;y#=y+jQ2ly(`4^q{d#Pk) zmF2O}kuzS46Hp&=GVd2idiRdzT7-H3@&LaV-(2j$!NH9iG(oCAs{&1n)Vs%5JZ~pq zWn)jL%g8f8aS6HE@!GE+*pP)@i8}?3Yu462k#81>aA@sS_1c=sj)jT(-un3fZVm>k zjua!n+88_>=6#lOV(4?^;=m&jyYdoxYHI4MhFH0d6q(2qTq6bFH|Q)@{~+D`<)3de zY@8RreFBE^mgQCO3^)T)9Jd9a&UP2H7haYn-M$wv#S+;)-3kL-vSB~B0?JRR{3@<* zui_E|z07j^ylJ>nKEqtQiP8uqrT1JfS}*8?)Gvz!g;t)L60;kX#>LLjn|9l30jaeK z=TeDBmC5NtK3AvWmBMcIkiPP(q~aA$)2N^CQ3_zC>v*Rc8hTUKXIm)%Ly19vD1oJV z#fK?`9BLd}5;nW@b2UxAG=MB&Pm$KwI3djF$W;=R!dNTihHYspI| zd8ZZ=)Ohd34K&J+>HM-6pinQNCfi;wIX{JQFKtE7IxY}_ECRwm(r{fFCAbc5WiRxe z{pGp+8Xvp7^(!HZ+2RmR=37m=6btz|9;Uxw=1>CN2Bd)yMI-$3>QwvqWe|SL=niOY zp6U+n=h1th*%ZSpoi}vOB}(ZJ$}atE+FvZfvITT*FSAOp zfJg_=#xZF}POdh5%>XjL(Z(H%XAu#+(kIZpS|ur$QMvcix-@|}StOOY?*oDTs_)lw z2hw=SR?mB6#`sPTm+|!lR*8_uA71Hy5molrok@(LmH)UVvz3fzQtPW8Ne0Su(pRN^ zYS~?WN^Edbdn!NN0p*-UNfRk6FXv@hV^Ct68_9;pK$iIWVkT>NzMj&~Ppu{cncN_NSemyKPWR6-_-!SMX;Mu^ zE{0Y4Aavqjja3Zsg`7DS#n*>|GYw)~@&^viFQ!z|K}!}AsevVYO8G@u06b)T4>}9z zCZQVgz@(;d0H|EV%2dEtK3td9ZVXG3+2P;Y<0aRorKy3|@=W*Ph}7DoLx?)_PhC2B z*L14N+yb25`6(h?jph~Fa%FjaXLCm?<1ni_oC)f=&{d#KR9LS+=^VX8v2oZi7NeZ1 z2d0O{w0wBX?|(yTKpdnH6j;I+JY@VDyY6^v%D`O~%K#-+921Ze(kn{m$P82#Y^8^4 zb4=uYjv=n>qIWvpm3&x1@6_MZw=eWpgcs_n`YbiF@d@0x@c_QnQ%!eISJio5YnB# zAS4>kqw0c$FZ<|TGQ9)f;r8=e{<6VqA3a^t>S+c6$hRXV2Z~6O`HU1iyV9E>@A0fI zd8kBhk3m9wF?tiRx5F;3Z_aWp9nAizs(@{|=`i*FVzDS!xu8LDk&ult4^cIGhb%;R zHvN(LCEXV>!_c2Ps?>x6z<8*$gd6TG$C;DovJ|tUkxwn2%UlF(XD^F9+Mwm0g!j^v zLJ+w-V$?xFoArvGM)-40)G!x(ki$0-`LKdEF(EKyP=UqDCwRu1hGyPN0nWrZtbXbg zP%LOOCybzCw)-GGc0Jgl%@d(Ha8w&=@{_Xq>L%7TH*C4*yOz1gdxL42WweF3;O_UJtZ+ z%?aWfhpQ`UJzUnK8oT}_uw^CS6LCyHFZau#S&c@m=iPIL3MKa4*tuVElfMfQb$#|L}?L@1(xs=f9hDVbpGc)+Eof{fwk`UqE48 zZfg-wAWjeAw<4IauglWk6`gE^Y1(2&s`Qg33-y3{nO@DIbJErrou_ivXyy7!VADi} zW8@yl^fuB_qW5W8vqmoa+}bLvQ^>i>8h0TT>}w!wqF7k85aK#ful0-}L8sZo#BL$B z@6~mYj8RXF`8~WjK0K&m0VD1`b0t+CGODWb%;iu1DXgjTsUJZlh{EFRT_I2!*fA{l z!cHxHUH(#$Li6^_0O134`-K9hINFFWU^I(MkMO)TVD4qYb|7J>zy5(t?)_FZo*GErWSlSP$kri2b{@nak!sNi@>JtL$ zSaJG^ea%h{2%q+@*5^)xe#VmE{F~_weC$ni_*;!d_UsZaKGA3hERtmqJzQGLDVc$< zFiQesDoEqi^IV##+d*Lj6Zo|S#x_Y1r{t2^#K``oqr&!Ai~3YcP~i54LXfCe(gNbb zdSQzGXJa#Y;)4*9w1*Oaq)z&A$RFK64q3*5qFq-cMb-9#I&)IOKzJ(qd{>+yNrC-I zDG6XPbmqF$VJ5QXAw{%mEu?gs#2t%S??biuL75S*!#ubk0iA5+DvtxtR zLTg{$p=nCEmNpcYZh|?kDP6>iCkj>eP)Q>*9Q_Qk^D?f>uW^FVodXy zAo?XUGU8rokiIK+4j;u0@=1o=DpErH)ciyuwY)pE?A^)+owDjEKsO`9I*u1s+;dR&2_04zpGuSuj zzkm(z|7y$Be6TSH zoC^BV*<2$l2(B$Ax_qZKu6i7bo9uxfl#TTG=T(6_fq;0xHRslTrbw$yTpzj6>|zzI+cv|LoIh^0 zHbFrzx-BWuM@5>jJmLEEtni0Zte`e2a{&Ih1xlH7y0=SBB)lRhau(qLWN&4;jqJ%~ zZ*D{Ppu`Xi4rzW7CAKq8@y}ey zD^cetHum8v{G$rKi_{dQu*WIUKOLnYWm;g9`_6>Hp0Z{jwuQzi^@Ou+f z_)rp+H!N&cU7l}HVvE1mk=b>4h_J7g9S)|err6jm3-^{0*P9p6Gn8T3Jrub9Z|R&Ec889}?B zYZRL_sj;z70S%iIx~CM%Z)VA-NnBcZcbn+drI*d;#bwf-G&l{!EM>&8Kb@2ZyACF6 zEP|w0-qlFj7cB^b9hxFUq$xIxS?(rW(brn9$G^}@yk~~$Yo>0?(bQjxf9>c^4xiZB zXXJ<-CZU6QZKg8>LFN@JA`;GM1B~YP1H2$}qV3_d5b>fss5-8_Xg%mp|L3bOI6$h* zXtP=Cbxj(#2NhR0*B3=}{-h5z6e8;$E?DD7z1b5t2#-?5)uC;^|4;xQ3CUUrXpF&< z9TZ_Mm!oDeUZ4}>6Y(s~lhV?V#%%y$amDrBtNSO6D8ssvHvKj1#+|eq;d961p!qiX0%xK}0OJFxnohi-nDZ*O0=s9iJPe$y|q= zs35ymkmCd2T#fA%s`4@{&;szPS9x>txf+RLiOlWVZR{X6K?9W?cFBRc%}M2nETylH z5vO~Cvje3A9y)1rgYa=9AN&-@6ARAYero>5kD{KH6@@R=)W*hj`4N(E#hhn>v{SJ> z^a<^|O`0YX$zrVyh4%Bz+94&;6O$LJT=k;U){{S{tViV+HUI>$uE;i4&8Z0#J=7VA z5q`aH&<#*BB5cSOhV!1?vP?UEHByzEldOov0kv7U{$m2MavdjHcNW;UNk3BRSB3_! z`#Ua9r}+Umn}iSCyX`II>@J@Ul9mzUwMJYustel>+->;d8;=I>F3YHGej|%4kV2Vr zfX3cnCn}Xf4tocdE0~~+v4zu5zfpwxYg)Vc@?SP5qpnCekQ+y$YhK}6E+ zB}V!C^rO~H7bL!T8xyIzAZ4R z2yd!mBJ;Fv2z#Deg{k4T4(giNL`VIlhL$xlY+|iD2Cj4;m(dV+_(4+T(J8}tpqRrr zW~;&sjXWz{^rWp=h&8pBjE^hpv?ICM7Tbuk???VP&2KlU$C;o~2gDkM^Wrx{gg+Dm ztOG$HC9wggUPG9FJzQu(534Kj>ru#!JpD{R(Pl6*`Z*DmY&(KXodL^{8Wem&NB~n{ zHgvYiY12tFRr&#Mr{18t>+#-=60}&ln$#T(Md0*-QNtJ+Qe9T~^gSFKpeiz$j~`RqDW`ZA(CN&HE((m%1M3692FMFL zJL8dD4%^?gGvlFJxs=1#MuKG4%2iIRCw#PRP2R&2hFuI-Z9-+Vx6`^T&L`Nw%y7{K z*?{w3Y*TEb$X`CQ&`DtqYTJ5_E7Y5H<^*NU5{Cqn>0=7U zw@UMqt#dGCzO!<|X}xWgEfyT;lo&hWI7jgRq8=pZlZ2pRqeUB5p@({>_&_HV-2W6R zsCvk+&FOyEBkA5`os_ufq{yev1QEw%dN&N$g|$Zsk;QhDPhg4z2Dlvu376gjMgC=t z?|IT0#b#-RX(P1K>3iHkU)OH#)EJd7FHMu~IngiOru8k*gz;-W+tT1r^tp~EI}=VV z+)by%B9Pl7>c+2sr~_1K>qs9k@4qQ`AAX7U_HU{GZZfbI8em@0T@8-u>QKk(fuf0f ztJLdHsE`!l{9xiLcBDZ9{l;$}5)?=hHqCmPHy5PKDv0t@Fw{!iZJ;-9OYV91>=UIW zEXTdeC&ONO_b&hP_cPtXd0vzj>n?28ynK^q5qr`ilzaqfc*s! zWW3^d;3W#TW1N9?Z~nP9D>n*lg()ky8+9SCsXr8;=+fGL8awW{VU*}C2x(TkA%mqY z+OFfNp#Il9f2;QQE&ex0y^o)hQwE4YgOlou*$YofOM;7v)st*+u`K zI2QVksdn0nuJ&Ne_lJ~S$S4W)_Sv5{L5ik$XMM+~Gvk4^z|synd?(xJaPO%RQlKiD zS>f>hY{+>B{6+ITljKT+HiZ06znHVv{&xFdusFf@c2i6(fDRnnsFIEY?HcFee&TEt zt|tMc=Z#clvZR$9#ezdCyJx^pq;CpBiP|UZ!;F0&z)on}cQIQm^(Y3bvsl7N_jB6J zbwn$u2{$Xe`?Psz?>NWB@-aB`5v8@ltVdQhbdTL|=`(J>wnsB>?+o@A#fG97&IlW;@N033!r3e~MkV{NQ_4zhlTmnyhxd90gLE8!jIhV>7Qs(Ac{^A@ z@#PrL50A+`l}ZVaHVrIwP5^mK6PW#O!O80Yq8dM#_*Pdo1bdwGw=c1D9A2KV-s*`@ zJt-Z>7i%_!-D>g7-miHiARjvRtEwtczd9rMB&_NrP_|A~xS$~@77Hyp=yp(G=aQ+E zy1kQ(_+|7|!4Xb~e5aXCs!Qrg;iPGzxo3bfn`lgchnyWLeUJR8>o#+)w{$~ClWVIq zjM+u)E4!!QKYbX0|MsUV@r8u2UVZQYpkfKk4XT+vmRZ`CdZ92pvlc27D`;qL-7!}J z5Ja!s_~RQbL@nWGzH{=sAMo;SzbDw&@>(zddihx%btSEd@#>nRj7uYt)X>L2QFT>L zNCPs|Gc-fLenn15e4;A-*e_Cjdx7$uO-QV1?bcPpw7`z{yp9fXVURzj;+5={lySFi zl_FB;E8>D5} zto-EP71KbWPl0M$BFDddBs7}tB#8K3sh{Z=x&9)iUy626&Dhj}?ojd5 zrF+LI+%7!Aouj^y1fp3Yvay%NKswr+*s!pPTCU z84#D@3C7pAnxu~UEuLIlruohAEH0q*xh-Z;c_!40UkQ|geIE6zKK{??D9N$`3bROc z7k6sI2B@mJa&nCRuD08F1t`%)X5LFbxp5j8E^?kO>38&u^D0n~bn|fu4rOi(u+{-| z3jZ(1`)`wJJqwhAhiA3dPu)f9ZW5A28OTa{S!y#=bBFp8OJ!*?%

Qg`(aJ}0#+BHt$Gjb_;FA^EuSFkix-ZIEQ*`e27=0B{Bcd4(f zEw}1kNbwkb`pwhkbu>eS*c{z}{eL_;fc+gN`Q5DCEj#?Y1<2_(8t4SD+j&JJ>p%0t zVaPuBo3gsY!eF`=w#%Nso{8>zG1x>tD+p4Zh&wAwkPTYN0z4^@x&s8-Dwmy^T#AZN zPoSSe)+<1Z{|#6F7O&@-bFsRwlPdf3V%o(0zr8pv^Tq2zsH3nFD6mKsy7-H+|C>Im zu-x4QNdu8XOk0R;*l@Eu5J-x95@{iJWAP!C8$DOwc)z800LzW*LVR5%kae>uCDu~< z-cfzQNU5qCy> z+_|AjqYNiOIYm<1xBnLv{b6e#h=b5Iwy=sy&yCH@q#N#50G3t9fE=8|6D#cw#hphg zCGSld?5L=yETPgHeH!ESKu*mwJFD@kX0C*DcwA=|fZ`=9$t3tV5e-c=zXvWgxu^gd z|DUQ7uv>ApwmoSE4aYQ77>=^R89lC;s=!298l9&Sq^M;ANqaJO#}mVH$z+u~spHCG1Sd%m zpl#h}X+7b6B#e4TqDqlb)s>Zf-LGxkv|;$adB zQf^`g40gk#Gj>$JG1yj@L(ecnG3ffE%5SQ#;*xY47{Vt2s{XomJNh8~1jl$$qHc&s zqXls6tL({HlCI*!{cB&!OI#mNxCKH$78dIU$h?q(U@3xpX-I-+I?Px;YlgRY;QKU* z_0*}fcdc=ae@w9gR1I^{Qd|=;o5uH z0QTDY^OSz>LtUeC>w!NlB9a{@EF%>1K)XPiHd^~cckp^DJ-v@W&=Kbikq(fl*s58- zBOND>I&>U6TzPKvzU|kiu2%&%2+|rB|F>=VVQ>IO(8;@1AZc5KoCB&1cFny0q&|g{ z8tLFbZ*||qia>ROj&86lleQ|b-3dqtuiTT%14-k1*TyQ$fL5h~Z_4-bUBX)h9_D`d z)_iTXH&hjk4w2jKA6QdVOHH ziRyEoOH))wY@X{cynZ*E{y4zV)?K)EAyR4oDAKf;Rd)PMq{&TU1WXcHG>ewbuLuZZ zP$!s}erKJySOG!yg?=`n^9u)tOGQyS|1eyC4>?1lE&~qtol0`!A8%WM$=e@C%>I4U zkHvF01Cz1cy$S++?@?qFP(6PB#~=SL$dfO$0Z;A!pL**5Z+`tBe2D+dV(0$FodVbt zfG95rOR4-XQ~e>N8CEySsS8yuk$=(xOS@|y3WNuC1cc_*AQS8_CH)_&_*41cLP4xe z8bFMHSAs$Ss{1;g{Wp#cM7at08Zl8ofsufT`?sV2(N6qRlYevgfms~29s{!g;=j!K z*55=XP^Efc*ENp+hoga$<_XBI-v9_nog4gr7jttf(fl>`fhI5(X;&-y8~e?DvIKZC z3&4Tow~d|Dy)| zzig%NYFJEZiP@ef5s~&N*~``98JCgv`$KRk34O(MWj&P zh@K z3t8y@$pyF%GOhGGpL!h<{yd3fEOYX%Fa_+Q7q)&+gLR|PkL!NpW8sQ}U3as_i%G_s zQ;lT$;rWk1OSJ<)qIO9H*3{DSYl{Cjxi#rC0&7aRrmF_=ajjj6@T8(X&9Wf|>4^tQ zN<_RM)3n5O*1%zmEgA9tozP*sZS83=ut zXSfx^Wl0ne8Go>pPU#x+Ok$_xRX6Nabd3Mv{_IaUtBbq)UwE04?CC4|+4Z+X%=)M8 z1mn&~70g}H<>MG8z}W`Z)b(f>*>wGett*^c-kp;_e!L?i?Aa?B!WXa2FkAx=?+bLL zK(;r6t=lhreB7cgV-fx0#o+SPECW5gmR`Ge_sb`$UidZp#c`l%ML_3E?g@@zM}v*p zbKXIZodp3Lma?3-)qiNu)3!_Udc@;O;e9nC9;$F1y2cJ%KG~Gr~zU_-SS$Zu7DNg z016V6r_5-xR0A}bUU>LP&tHCMo^@VCXLxTt6{w92)}()!TEDks*je9&e~2>NK`y#F zwp9y`3Dx%z1!-v0dwXv-q^?>7kOt1BnzCsahbJirmFZ zcBG6Pt zr*)@(ACGo@AyAbDTTC{17O0$ndpHLY-Z-aSzQ+GItyOw{5=h!lHm3xG0f1NjiR#hi zrutuwxVQnz6lei){J7Aam}MWMy82Z$-H?N2sgaD()dH=g^G2creP8H~5IdtSIt_>l z+jiOGaf1^7aGVCa9(JvLY9Eu%T)vnoYV*}06IwcWT9#!qmeNK5rZ3mkgkO!s89dFi~LMv-}jxE z+_xWO(}J%%6P~&_`b6i&o-jT2se#Q1AE{k=Hsc_Uh!Xq~O2&T4)r{R0TyTX|_YNX51dHDVPb6+I-_5sxNKe zaJp@X9>><$J{zuL7I!U2^rvv6a@WjiSCZ6h*Kv{Ms}!XP$Di6C`{nd*oCvMoV_5D+ zTdW>Ds{nx5Rj{t`z*aW~M8 z;{3hNp5&nW+Qgz8>$IpBUi{L)o87FL_4Mdel|2qpwX*(fw0b`M9K~N^wFOeMZCkQ+ z0{qbW3yv2iwGJGpEFfVYx?0hLNs42X2tmwUCS3QK5D^`7Tu#=5KaQxRVK}Q#t9!Fy zGzvA`XQ9$??)uVzRM+mfZ9J{Wr)b3`e`|!e{pV_eZy)WtmlRQ%>y23ZtO=!TU9Ind zUN6Jnps$T%q2lz=G5KCITKsx{Id8?MeJ85X6U>SUmP-Lu(x|u+o5Q9D+IVIgpzkbc zOqtJ5opP!GsnxJLAPcI-ZFRcZIE?p%YkGIbIUI+`ZS;!OT|Arrl8jkSI4$oZFNkkw z+&|F7siFnr#5Xx^%6O~-b=2*ynC{uYF@qP|fy=iWHXpae+C7ef0FmoIsj?dsq17C9 zd40~pC3WcDf@B5itq#JYR|4_Uu{$hG%RAu3Y+vB2$Dz&BRn0s4V#%)}GF zZy}c)0|Wa6bv);~G7Shml8|*R-*3t&&Zc(z-?tc1q|}@ID6nZuCxT?^Buty zPc%mA7R=XWh%gy^@kx&%%oVF%5|Uk2knrKUyJZ*&=!h&aEz=Q!iz+>1WHZd}O3b93 znyc*IfdWX(5-Ad$%kJWoFMwQe*e0YWH@GzG;p+re_B=vfvy0tPKir>G&v?nCbE_KF zFO?UpJW1vEn!dFsU2%1Ll5?bjc6s8ECr-j~=-kAiBU*e!K3vcsuZXj!p)9yA`0_-ObzO>b2b;w!&|9kCA~6v5VRH zWrvzI1xjE4I0~i(6M6>RD@@IUaBZ0zrEa=LrA;>*NC=|$ZZBs=A#M^8QsXpXL@qSR zCqZgdGbRC2U`Kq(g`LTm){p|mg=mp#hHr;`d~l;zm~XXWqZ{iCn9eR#c2j8hU*^K2b%{(dJb)P&3*sDMMPr&Aavkk z4d5Px%>q)*QEc7=idMK$4Pl{7XnLMS*y*mWC?X|UgANWL& z8gHk8PK=Fn%IB~IkyLj64H&Gud4Urn-lW%uC9i+(7#=Spe5{Jg?`atB*$YAe(9t1J zdvz@~82~Z~@@R+n4?Y||we}|TYok3w_-5=W)6XkAb$w4K!)Y65h18rw7E5$%-Yt-2 zB(#vFtL8uhV*B!Khh08o$u1t`v$L3Ok^~OLqAa#==uZ<|&N*)mblE9x!Nngh^jeqL7<9BP}4*Ou*U?M_l? z-CHMO*YhWXIUEjqy<}9R9L+)^!@9Ue*DBT?rK48Rs#PeVnwsg!`Htk8k1o!E(PxEI zum=>aRkWo{>CQ`aL92>dcTIBL=qCal)%l^ompE>RVY(iFlVS(0k2dx_ZE@6K4`;;& zjFI7>;bY%@$Q6Nmj4~nh$+GUT*UaT?_Xm-C`)Ot-dX0~n^h=OR0Awn5(?Tmpm|SdT zGg>}+kWrX>K?-IVabiuBv_C=r-c{=Brv{((Gz@*Uu#ix;X2AZ-F9sAFfFmdZ7@X-U zL4-1v6;)l4IuqRg19I z6Z)9<_lOjub$eVBLF+tP>`tH*9k5~1!;dwS>@N8cjn_RFMN*|iZeLymK(9Ba7ymLx zR^p+5-nvKJdIZtkG0Ah_z}=d)UgU*eWq}($9FBXX1BtzAVnnEfXtb!uii%J^mxjX- zsovI~-w!`}tb|v>kt3KTL=>=ty&{vZ6J#4fEZ< zeVxQZV!^4FVCSk5CMbUt$Bdq|s-h`tQy<(nojxc*c&}McY+Wn_+KHiV%|6#mUp3~v zR@~0g=qVrp5OIUoEOpPHS{dJJB*K>0KYMnib%0aJjeFHFZ`s|ay27b%r!%Mw<0rJD zRqxR>5#)Q3d7RNf$HhYANVb?bUi{j0eg|g9*jE2~+_-R8Bt6&mN`Cc%G8GVwRIN(% zW4gyy2ZDRRC{x4YeBDsu4x8&Tz2!PmSDcLoJ=&Ny1qvt)xn);fJQB0;lu)IFuD3C}t-_I#jv)A2wQ~>r&5_oq+`L(XzAK@&c zvV=?kl{l~OdMW>hvA2$@a_!cK7cJf0paN3T9SVpN(%lU%8tD${Qee?7-Q6MGsMMkx zq`T{TaPRkg?sLxAzw?I<#vsKUZQ=e5`yv zT(nlXFP(X;8!0)?_BXVZ^w_mv=|2d#fjo{cv4tLvdrhy_BTzUe)=EM3{d*S$cm6_# z(KCe~wSejWf?{&E>hv`Bn>tVErbWxuzP#tzRkB;t z?6i|N2boEn%UXN$EzB=K*5mbY96>tLDnO*d{jixas!IoYUnYW8{kN`&61!Yl=YYHD z0lKIDzwXIWRHYh*)1STVc`l0T3}Iv3gm3qqc1XDMPL}wTR?%b*H!bLjQfOYGm}Keq zpU}q2PW&qX}foeC~uFvB)Fk_QzycaQKZ_h||^bWY^$A7VS-d&N} zQCYrBVb*dolLK}pG}}Nv?-$WZo(l~6&VsWkfkayCHWkEv>~bv_#C#_-wWDjDK9-(|Z&E7LP53_{(m&Amu4Nif^VXs|gI^3kgEl$ou&1)nPe zy8#FpaR8#=!CtO(qndh8IgRMr0NHV8Kuzg!o7?!}o1_GO_G$cf z()&9Bh0e!uC};)T$h`sb-2s>b0oVJ36jHmZ%+g|Qr&6v|olP@lqz;UwT&25Yo%#Gb zfD?CkJ=ZSUBy!+P!ebh|`(+rwpty2#I32d-3Ke$VIC|#R^+ZzSKl8pT)sl4#(-C%q zsvYzcD9bT{oc0!PW?4IjDZT^a0Oy6k`is4r_jL*GpyYJ?CZt0kWhM$0P9+P$Q}VEK z&#fub(v6P*LuE=ZyAd&|+<%JK{s(8kK=rsZbJm*l2zQ$^he_DRb5tL)&nAk)&WEEzPbF)ehaQG zRZ5B0@jM5>Xojw9X$i+>)}u$CH!J2aCb>>4Nyx0<=$@=}MA0JssxQF7WIwt5yBkiO zRVh)>Bd2gv)u=KV3tjgvY@HT?BDRl=Y6$LCOD9$#*(!4Q9{ei~y`@A2xcTWjv7(k& zR3&iwHovOsYJHz&v^W}@6aE`FaVP@tou>Y30Q2QOdieOC`gffFffW1~eFX41)W9!n z1peQD;ZY|O%>rn9TFjt-09gOSTmJ5i9~Z&CjsJ~zSw4bZ1l92WqagkxQN8^Est2g( znI^MjJmL^0Ks2+oxk*RLzdsMicFmN_rbUr3b`2?VXP(?1^f)nv5-?_?Kiuz4j^Usl z&NpKJJG%emsdRg`(4){i8<#GM{`BW3W1)Z;tV3}sfTK9xV9f7Cy6AFu1?DatH!RB& zg9wEhgr@4Lqq(%4*84td3Dn;0^r+zcAF=mxbU#>s`(M94!>P@KtbIgZ|DmmvY0g_? zlz)45fzATHvtffroxG+IE;%Vc1;EOmCW=?)%NzplUj-JA zcssHu+9f{dwqoxQl7WfhD_{uu#t8uhPwD=(1b|nHR+cSybs@0m2$liaZD$R6G#>gK zWD5D$pk~2ns{&&?!yT7L?IY@j3sn0yv~S%1B?nQpJW|{j*kA9SxKse#0U;)a?7vam z>A<#=^L~RIr6(>yz>hnJ!b+Z7u&Z80Ong!1X(0NzFm)6 zPwL-yu=NE%gef>4T& zi-+kZ)I<8x7W5^Xa4hXmF*|x)lfF<}Ph&ebuz^8odbHREgla&nS~ z$>u&Im((bq>meIka2WAn(VOM=6FPXqfA1SMme$N)Ro^Q6SKw#%g~&Ct7` z?65kDD!IwrR@*+f*i|0{@sji;#DAB|(9W4LA5+k($6ru^gN6OR87$gTh95O zr}WPMRSW5K@kH*Jd~DHW$#%Vvz+_0ePPTG-gCU4);pC?ANS~3sGCq7rZ5)UW+jEa9ebGs@GVtZ|)FJpE=^*xC7_Ju6NUt5=|OyUBX*!&3k1 zO=BnWW zbl8A5i!?hx>OwDrE^I0^QD(4V^@ZI@DCqZJ|8|YPZ0t)!pbsMJglivEqT+tE?{M~D z>u({BJJby?3^V63AA^1Jpa=%+L7U7 zLG|KWHr;yt^^ec{U2kbJLi##A*6izF-p>n2D=v}Qyr*B)H#87y;o{*1L(rv@K9NBt zAGpY#PN6}-NXgdrcQOr;rqI1I+?$uK+H&*mqabzXpXLIY903__h+zf>shI~qoM?a9 zQoBbeiOU*+EK5(8?qyGR&f(U)<(RC{d>reiT~QwI+JBt3-Unm`YHFvd3d0L#fB~G2 z!Bsc@FPD|pza4jc`(U9uv5t6V9ZouRuj*-Pai`abK}}6kNMrSaaYtrZ(1yjEN^@r1IvnQ|i@R|6<7PI%>d|!TjPLda#)B}Q6M&{U02MQ5` zc5JIo@c#24kI!Ta12`kg%f4M21Vbqw7+aa8ugNz5(E{|JH2(fsEFIVq0kk()1B(SL zb>_gd)pUdEnTE^G1Bp}8%R4IPRO`*`7zBG>X?W)YXq~(Ei2ZAO#Ga7G59u|s+ zE}6xcNucvwz01u$dYX`{52~5m($7UY>P}N>En~Q)df}tbWugjeI8(AoOwU%j!!Y%* zjYZsS)ff!ijyfolf;xa$)NtYzB6t-_CEFPOATlM;>~SPseTO#^ChT$OZFA;PB5)g^ zY{5=+fr@tNV@JzRZIJa9SZ#w-X*}vL9uo6SOj`~@-RgJ}N-xojG@35NPC+|3HGs3& zAffdo%&XrqVX?*YQ*u^i2$Rs>$vFwUvN7UDl{^3OMmCDkoD2#-tU?GuffW<_wPs)u6Xg( z?uEjJcL*88Q{5sZzC7)Z&~9D1isiUocToOn?z+~eg7xl;C#)PMtDTK-FVLLW?H;n& z9kf)xDfVDxMDM%duA)DQqqkG+3c)38Jy=}&HlJMYvX7}vU=?CM=8*D2`3mEil$x5( ze)~cMuf2ZqcGV&55+>zs>?W6IPw`tU5$xN$yTjcru@oUsc<=NFj=6M`&&2WN=%L%5 znI;+4#UbQ}%W&6DWkJ1|2T7^yl`yu->CTd_=Q6L3H*g=&2h@lx#zG&gy8yx15y!*C z;AXwu9c9Y>2}n0A{bcU%bn@F_$g2&KUti#2!Th&4RRm8akg2CzcQlL z9#y2!{^}wzZu1krpcB3>m(G*%bvZLBfzi#z`f{MiG0~VP11G2QmUCyWP8`+L&*xTZ zq0vi4H9H%rhS^UqjQ{8_S4`XY75;})|PEBB|NT1$1 z<*YjgZ4)zX|8N%S=~1+Gy*+FYVSy1O=ZV z??oA9ZOd^^geym&=y~tQu%UXQk<4CCgFTJtMKH3@y4U6?Sub>%eCR!KB1k=Ib6z1$ z2rk%TdRek!=S`5t*woMKHv9QSnM4PYs3yBJ8?@~)=;eXE>SjWJ zk;-OV)kWZ$jrC-&l*&$~MbX0H>1pN6!%_y-vlL-VoaYwk;5CG`;*OD)9!>OQw204D zk=}_sBtXimgSMKMTxShTZGfqQrV<_Uq71eJgrmPDQu!Ma?|Pld8p%R- zyi>!@iZ;~+YbcCx6q2(^x_((%537ypvlV(yfi0^S%*u3y#exXp@M)x3{rWb=TICI_ zZd{GcQurLZEbII}No9i%j+R^J^xZ`D^{JjmZawelHZtA(>5x)=FY{yDd`7+PqLoOX z7_L;8uk2cC=c*$zOwd>N;!}-X?4p*v7vl@GE<%n}*@I78E9()fE6*a!lS^RAbw``B z1>JwH?ARn{C{$5s*2j5L2-lYkefK`P4cRcNr~T9DDLR1BtD>l8#u)dC>)i7+>wwsMN}8C+P@i>^Gq%@9euR>C;s4_IGm{(lNjo$d}=GZ zt(A8Z28%qq0h(u;Gxje`7L~loyS}o`$9g}+VL6+ zw=UC1v7&9Wm@h58!}ng(JJ;N?+W6#l+URs&QSyvEc=F^EIib)2DtOfIR#bTYMh;C^n?meTLaP}K|!zWF1?%2TO6 zReedPl*${yGV57cV<#%+-C=(rpQ5rJs#MrvYwlhuIx^6p;|Gp~uvFCOjN#)zH z3tQ*G?t|H7zx_yDk!RV5UtcSPsHC$=YS;Re40dC=On2G_vU_p}OSNUcT_!fr z*p?oAg3qF;J7gM(yFYAu^^@2HUA2LtoCzIV(0pDtm`6J097p%lL58MLS}E@eFHM(n zmSrk)B#oo(!-d{P z#?ZY~^&8jCT9tacJxUiBHyZcI+x!=%&}4{+-op+Vo4;r2wz?#W4}OeN(9z`&oeByU zSUe`}H^oIM14COVp`0zxa zrVje5D7;{5px|MHA07N%l8aWOjNg{>k#KHhHEyh2t(Jpt$_C7GtM1okD%wg?qo}h9 zsm)6LdREu2Ywc5CH=tA2p-tPbsiao5s7s#KJYhxGN{$rkl3_m!7v+*iW12|h9yUkP4J|Kk-{nePteM-A-DhvyD14grWL;Qi*2|>!d1Vgh z*J7KTQOB>+LXD4G9LC*!>G4`jsZD=XXp>Mx4M>evI76PfCTt~&{$ zFQZShdJgj^!=%=tjKXzmW{9}EmZctZffRwPqQcbACaP+ z7;p`NPWp-nJX=M;HQw{vT5|sm(a@(ygFSUD8-2z`wnydZqtr< zJBziQoiUj-U6s#J_j?Uhl`hV?B5nrcKQ8=)GhJRk^kqIecrfz{v;rWcxDcBFBF6KgoY4`ryRymxyug_DOkF z(isJA2~XxERZrT+&SDaudYEdhAr& z{Zj>w7QwYT?>!^*0M$stmXW!k>2> zm2TiSO;~+*kpoTd{a;p0dOaGI*O(?wO$ISl-2x%;poJEdcurQ8eeif;k*?uVl##m% zM^8;S;YH?uK^S-$K2<1CGSDE8&Fb}Kd*^DKh^+v4bJ`kcubX^QCRPtN=_WZ&r#!I! zY_-)z@BQ4rg?1L&k6auq$WP{S>O81HTC67Oi`li92)^s&? zi79W2*nK3-mHh=iN^(8Hmk8S5J_~UU{#gyQ%!&hw&IDC%o!0RWLSY7X$wBUPoaQs= zT;PZN>-&r0!HV7Zhe<_{YcQ6PoCDY@VhaSNFftnSG(D`q16}N%l2a_SS-eqSFS<6L)V#CY zTWo;=3e6|+TBXRh722+_0~uQ&`+PiDBt`t1UngfE$=nanI|ER{)yXHLa5DhM(S z1U!$+>4|~2RUNPH?Q~z+S?Flq8(Nvy-!;>k`>JTmtq$1a@_LM*McLLqLW|Fxo0C%< zekk+d&O4xeewaL+P%&PMJ7E0ysb&X>G>04dda)JxE9p)4)OmBak=GIfnPv|UrXm9e zveGXHM=g;7)u`2{15!`4Z|GVsFy%}+Lavjh6RmRTf>bd;B*1hX^tfmNWN7=Pd1Wgj>&}r|*H-j)ZA#K}@hO z_9ynEPM5(4f-INC$Ey{?=?4L)FpJ>V#V>8M*jnjE1*cymKIi)zxG)S6joA=VxJ&LS zT}f$h zxz}ssY9Q_z)M;fgZTqC5E2mH63~8WfaF^-?b1|AF)O2bB^+JNVUF?nd-?rC;Wd(1R zpSDc4&Kg--HjJWKO$mS?tQI^r9H6+uiJHVw4Y;MprbVI&rtp4$p#rgO=uTas{VxwU zp{f?dOiR-xVtn!Ht85v@*@IxKTH^8tW>D?L2}R6k8mIMSA#0(BjvguxOWFgRG~sQy zyK@K5!~~PS(v{ti)eDl-My&fgl_;Xvs|eyIaejilF8HERcP)s^eZM2~C(7|Dx$6)T-+X=qG?%N>+O*T1-mjRP_w> zlLIvQr=$L_#=5^wer`Ul-BMfE3@L`vigiqtr7=D*b&8LnfJR6_YWc_Hnl%q)WUae4 z(4WxS@Gzl$ROX9{$xUjtg$^P?!`5krGM$DeS0^ubP5I%V3#0Bb!tQ}ygNX;|$sffA z#qPzGD)Y~L@ zo5X}*$}*R;9nkjR-W5SQdYOo3G{aN&b%su|)Q^bY?eh@_@nqleu${Q8d8gB>Vhi3k zv4I#(o=SNL+dTVZ&#KE|q9LHmpf{;UR`Udf;8IO?1TAisiUxAXKuoCy8!qV-^OFm0 z1PSDw1%F|PE0<;bz3?Y&|8b5?D`X!ggm!y^^KJa?oxUB7WSB?ylb9gsf{x8;YSkd> zPL3E||NqXnH^~8lWbW^FyJL%Zy~Yn?!fpAmrQGa(F>~n(TS0NyoO7dm!v_XwXR9i4 z-c~dfvKkEb&a$(d*Rxj7b=C@E?lqxFZoM3Q65#B`{4C?_nwW7GD`r$z9gYuGSkKq+ zvQk|_K{os4&#%Ko`&hsE~9@b6~mFA@OH)(u7$#&+dt|MLF+{%NWS z5^&S+;MZmXj|r!-0SNQ#WFzr6g42G40Pr`Fa*AJ(>oNhLsQvWaoywxSi;Lm?v43F( z^c4Q@t~_6uMQfR~dQVX1Y+8@*++dm&mrLwZ=mSK=`~2;+o-~MQP-IZU#GtN4V9IDY z!A5HlFAcPG!;)%X^^p% z(2sJuS_06H&~r^B#lLf2&9lQwr_%C8@P7qUQz5ve=BrS{cH6VjProS(=d*AZ*F7j6 zsBZry(K@4dUZbokOJX?CH@`@q{@b=~fNha+pLGWilSa!k_2d6AC3#F_QT=ADG(lY< zG1O{l9rOx~Bq;dINW@q{^R-sGf%t7QKiOsGg8mEU1=`W~tQicz1ts|nK)=L~1|z+x zW&>>q{S{$wX52!! z`>RH>a(avB4bJ?mr~k#l;wSe~&Mo%N#RGDg2h!4Kppn>hj_|a4DaZXhIf@LeCX&MA z@j&vgpE!}5O7xgJcJ(%?WRt=Zc){7*0ss{C@q-Tef#;&8Wm}HEWMCURJVw(r4rmJi zhyi=1lb5^-7aA_9nSitp&NOs*mDL=a6On4|bBHCMq4_X>ikHN&?kr~TA9{itB5xU) zz)a@kutwb8dCe2V6lBxbAOACNWIl2&{i~wLiAYUnH>x-NAGQ_7WIu#_a~4mw09gV_ zDK|~1q4tRI(_jJph74(V8|$q_k5biSl=-0%&%`F=#Rqd@j(?;j7&ANoDGAT+;Z|yY z^;o?3iIx-pg*;-xsA@(?CfOQmzxxlB2I76+t(25fcn(~Fno0Q=Vf{S-*3SA0iy-uX}l?=mak z!^IZv_44J9RMWij_=cL=84?!_f%Ii}t?SYYDAYckc&5kAX;7VqT(&CEO_)&%UuNo5NnKJ& z?GFx5+Woj`{ceBoX2X?;eS>iAcK5_q&Ia)L6SHDLXBi=PbcqZtG598fI)JD%{4w{# zl$);`2UVIydS0ZXjP|hhf`CYGJLmz0Wn4Ltx=Ym9R zUJcy`){D*I_d?rRwH}Bk?JZOW?yJG*<(|Gd-XPvUnn#-KiqJ|JJ3W&x5Xo{ zqEq0pcN%D@fp52OD{Hgzdo%2oKZ8p`g3_@6>xfq1Xn&9KD*^bWYSHv}rs8N{r96lV zH5_R}9K8;Cs$0UtPu+Vzug!D4qqQTUr&0-M&jvj|u^Xbxl;j8kA^#M^=Zp-uWHdA< zanpXc&RUykXs$aQv&9xD?V6Ibz(qOjI{*;$NQc#ksVwRVdc*qq`u2=kZZ$3ZuFuWO z(_m~}L~PcishYY@4t8!3iG_y)2eLl8ZxlBSrL2>zGn2$BdzSX{&bPRekBXaseuKgT zZ{}M(@hiJ-{cq~9rs&d+Pm077w@vR}GQ-*I)c^9-GjGVG#RXPIB#>v*6)WGDnpsrg zfo6BiikrQFBXvCq`?=XUo-=CVzBaSx?p&e-;fj`KH11C#9qzJRmU+%fyf@c~B25rE z^*_TJK)G%OMU&e)!VsVKM3Bl|g;^WdE@g;bhUs+;=tCzXV9HAj=hFYG4Xb?6)Ch}t zE$xIBJ)5}ozgeDH3i*E$v|jk>A519XODyNYk&GsA{)-10*u)iuw-eeOQ*JfZo7kk; zNzzbyP+lQD>{U>j0~59x)yaar8#wQFo*oVq;TC+_HKTV;JTEiU;HrrI?CG>%sia{J zA?RThAEuS7xGfT4m@t2WWva8r!}Spx1bamYjsbR(q~9T2Zks%)zM+kbG%lKf|rHw7qXL^S6nA8l@17QZlBUOeA^d1`vY4TR|R) z;v+5?S{l^st67_#nK|>Hfhgfh%DESqR5MYfhdu=ud+=cQthslsipO}IW^G94I}NbT zfv-7Qke8Rp-;JxwszQ~U=O4_VA3z7I5t*E`O``N^kRPq|s+l^Ao1mhjWAE%;g`VBK zw&X{L(@(Ci=lI<+)%&!!8#sKX^Tw`93sn0uRc=(~JJ!`0m@^!i%;ks`Nd_e-O{sH1 z2H>QEzSO#M*~Z+ejB3R8W151l@D-S>bOy!T36J4JQ=RUxbop4ERNq~`6R_~qSnma! zmoed7n{($1TP?gOP$w=z^e28Ym~KvzfOfc^YrYc3UVZ7hk!vf4IR^BZ0JKmjSx#SO z0K|+LXQAH5MrJA78I-~-sjKk8Q+UOtw}u;@pzUB`nN)?rMW}~?A8rX?F+#Z7C%_?+ zrrM{?tkr1Z#(?U}hVoX>Q%LyT=bl~mWk7u9wqSHTEuBEmfEbk!b((HE(P1@bw*I9* zi@ifJIdXaAA9akWuQ{fV33BC77TIW%a~ZX_tI_3VP*ycAU}gXeVDkTk%j#PZn|)5p ze84b8bn5=_KA?TZ43+~FS;!xi_gHQELZ8c*8Ay(oPDxELUoB2L4Zm2~OO9W5x+qGL z{a*j6vAS|j0}=sQ&!(MOW5+rp`6%kB;54m?b*dU`u3KYVs9|mhlsB_qc#R4i&deFT{auIiFt;&nkKac%px^o{W)CU3eQNWa2 zhEW(94pu3g?I2!Os8EIlB_Ouj)3pF8StUweLAR3=1WBKsac=qDF+LiaX*5}2G5*)M zC+C-^cTeXGqQP*3&kPmw?v!hysEfHg@_TtDxIhpQx`V|Qi&`%iXy9OCn>68jtej1r zgfl+&Rh;=zOThfoBLKYniuLGc)E0p*AYS~Po~QEykiTSjjj_=mtP8*(796ubzTvI`@h%t0cPewAN20W6=7s$nE zO7!5m5T4oNxu!QzWjY#$dGA&4aMN}yh>Y_b0M&D$z)g><(V>|U9!?Oug&oo=_H?4I z4adKE1rsN#oqjox)CQ>ySkX8-4$Y43L&ZQ=-J|hl+hs9D8+2@w;c0kY-j-fwYJu3E zRPZ<3QXl-NR(Qdo*=dbkvI>UEl`8SNeQ6Y|)Xu9D3mZsUH>PKt8W`5fp#PaW3VXHb zt6zb5I2bP%E4n_q>Tb7P=1=gTWdjCTnkz@9Ajmf>tE>%ZJ$V@vsIzqFe^8z(9rHZ9a>1q*wmWvZyYkN0mr~qy3aQ>>E9H3bW-xw4{jB^M zs1grkGhYwS7%%RHXbVMrVf^AYKzNV z_*;hl;|Gc$CXpza#cY1=+Bq{sN_Y77<2ieXnZyxnH=)EJwNB&EA-1<%^s4v42>KD( zey^8;-+eOwaQ7g?LmX^;FS$@XIY%BECq_EOD&7R>5~6To%EMPs9MMV;*{ZPc9W;y$TDhlPAuV2@}&fM&4w{rqke=qPW>s*d8b&eY3k!Lgir0B&|U zSN|MS6_i~4>yxn|LC0yxo!!kOK725{LZIhjvnvc=Hq9>N?a<4&*Y935<88XozUHvT zvo(g{3*&iDW>fWiV?;?kR<1Z=v91(rv=evyDwEkdOyjf{<1P|WwrM$pp8ConcNbdY z=~xe_vpjss< z7%tqZFDAqm4XM~WL3ixV)i_Mv7hxY@wwLq0Pob~-X zRz;x3a}$Dcv|6-h8Bt_&e$?3nj60}hT3np?S#LCRLo z1$%pUG?xGA$&9K9_NP-)HNt-clA-G~fVi*RCaxw37(Ka0OQW778xH*DNy2Cn4-~H^TN`oG z&$CC{iR)5mFwC=<%99|kt7Bx6&@Z)*LNVVKQv5Pu|6JM@^~Gw*Ae$fVOCMKB z+JQ^i6;*P*@+*`4a;HnT?@USHjL1K1^L_AH2%*9EU{{y(H5iP&MVe9ve4EAQjv}?u zqi+wMA0yz(#9)%)ixJu_ewvEQ9%1Y6<_At(loi;F)adwWPChk1!6587VK|(D9Lb3N3{IQv;55R0N!2ibmhnHu7=nS zL%Faz+gPH!o-_NABor1wT3`O4$9&4Qo()tg-+-|_ZUg4#b+*>&uE!=nl31?HzjlPf zrZ7`DzuH`37{Xn4)YtbJ13u|^mjre>&Ii&W2NA^oFa?f~#5|y1A6sW~>&$%yGDS9@ zstE_f`AlV0!ZTtdrQVAkcW*s5j!Ndr%|1*XXJ?a$74$6T z)>a$&N*PDx((@J^tTvZ(B6snt55!~+**>(WU1uSfa&mGy$wYW~@BrR3ylD(7P7D?J zQ-}J`p!hrJ`qEKygG*;hqn|)yOV{@m7uoDfd5-9kX5aIthD|=;#m7V zASc_i8Df-gFNI~ljUeS=5fp5paZJ@$V4r{Rr+h6|J$lQf9Smdpwa*pnPb%q;r}@v9 z<;g&Y)%4@I3olp~24HSYWc4wRM4WjrD<*4_yAk^`c%E_+eKTwUPzRg0Z*9Ml4uE)n zeiZymQT(4j@^^mxBiKSE02z;tx~n2eeIW1xQ$%LxU;aJ2Zejx%mY`8LM6rGO5yP5` zE`Rm!hN!Ri$p!Jg`zg1yVcL*V9ClN2gJ-MZ3!F={*O_3){UGH_0DQ35pmj?5_m@Ty zuY3GUuR5L{9v_G9*B-e6XAOr6kBGQ3KdXl3L+%m|<^1>Pr7b6_1S0F5B|TcDvlk*i zPk_aHqSk&fPYMt3p${5z5BPsP0dF+$1nRZ%RX*hA<4`=5u5us9bQ_?(gdy*Q>mK~~ z46tMmn*^udfN1L%5VE}0ulSwCIljCT?+Gmy9^ThjFJS#%&2|R}`U2bee;(8yX9j%P zh+>Z6gRT~VOUJVENF8@7O$8nr1J*D?g-1gAwm2#m>v=vkC;vw$@y|c-=ZSrnXa|VP z_@d*dz#otXwnl$+N&f-fsQ|?R1}Bjv3W6cvnM+XatlfXGq5sq6B3S_N z<%eYOUbYV|pvQJv`DQfs|9BCpM_B`(r6ngqiWuOh4&k_3@Ba1nitYm5N#@@3MJUs! z6B&4sdTImzzaQl@f%&Wj{e9~pVIUNEC^d!6;SiTuktKZ>U%6#uNTY>pZrM$-oWh&HZ7lN57;GTy}CxQ7GAO z2mBp6NN1eMLwck34e!bvcJ0x}VmA8K!~@bX57&-b%p200-^w6`*{yU*1Xl3Op5$|vSj?%d<>mJj1U64RQr-S&^FNSxJSAE;h@@B-U$aZ-SIZSBWo^ob(iej^F z(q;_Yb4t`ZX;8i24WK7ay8{4YQ`*obeSmIP%4}9*geE4bW5M@E^qpKzJ~Kq zk{}4+QX@Z#Fp=kXbL5IKl#oYm+5Mau>D%9ATGii5tRe{b(11z1L!i?uk-L~{6}H6| ziN1=ZvpT%dIal=+SMdr|6v#q!YSY<#Zk1Qz{G$$7I5)r{eEA3LjY=#o@>}tx72O|S#TJ?soixw8f<36 zcAD*_6E2)I_hFhoZa=saNWtCFCb+I-0<`S7hyzL6#2MCAE zj}1vZj_*op=96TmDRL z>DFy_3ExqB;NEGnSZuv2gE4@;Ybe_cG#Ql*Th4k zdkZfmvcK3CQqsm;@weCfbdM*>%afzbdRq}1T~%TxL*nBD@)pl2nItRLnVsQHFqGhA z0R}U~vp0m;+_$Nku`f#IW}Jy5V+@71+TQQh@aN`Ue0KQ+<49WF+f3XU8&A^QypkbZ zb_8WkOP3~;)AZOVKATbmk^O*rXyqXoTYT*Fyf&#o>5Ho?KN3dYiD6TEfBU^2H}`%{ z()EX^Frx~Xv+Gk;Z-wWHy@?v%B(U422Owtvmzi&HxD2RYTZ~;bfOKt-4UY}yfi_sQ9?HlI4S zKdlPXdee#yNY4X-=07Wy*J(I3vEKELa^1uJxn3CeU4_XiPP5z5_=jX!n7EUN?>*|U znj`wVP*2WU%V>@qIbvsox(`9#ts)q$4m;vsy0q6_Hvp`24CcD#3 zba!GGz82n*2A4}OJk@3S4!nRp4Ii@9I3>z9>-M0Uf>rfu9U?47;PHi>d?bzE@@#U_ zAtkoA~n0(4W>xM~pwwzgL5N30*beN@7`x$Au9v#vz% z!`S!O596vndRtBqR^_58-)mc1$;cki@*Hh$j|ja9`*MI}yW59x6C)zr`&`(*)?;d6wE`H!sCQcTdr~G4 z?9?i0%68&@{q-IY&b}f%y8D8#8oS9~IjZXNEQ7YOSH{Lo48)`t+ka@G%A_Z#Q2?Rh z0k7Wf_KV3HSqzRg2KNA@=f4IZpKRnlJ>{OG$fBX^5OY)v6jf8_fWW}*1q8(Q#+c)P z-0LiWQ}#zVgQCr~VR2`WFj~5G=h8!%mdYbIYD#I>? zCpu(##jp^V)Y=eKg?2OX11K6=B|3GrpaCH5i=D0lgn{g+KDc!6BH7o$p^3eGvG$e6 zqZIbZ?5%`9^F~7((Pxd460GQ@>yoTBXDr>qEkG%JU~7-X{Op8+89fFyE9ijgEkPo? zcP_k!eav!%MVrhhIkyX{O*mR+EZXxDS4m^q&To&RBFihALqO7v9j&M6w?Vg(USkQl z#+|nNV#Owue*ES2D4SG>mBt5w7gjYJSU+mk!$Kixapmj2oo*Hlc>wg1IY(OAWoKb` zkc*p>U?NgO2JUZBtZ~Z<_~>L}>KUOa+YRUy$_>u6b2Kz3JGg2d54gp-bKmALk;Cz& zZkw0E*C(Tr`&4@0T~0O-F!pEt08ydA!jJ*b_hNFFYJqY%TjBi2=x$sVbZ{n~SnnQJtQ201HYB<4}Re<(vAi%S)t`c=SRJHoRS zg}FNS)~2LfWi>3}QV49j_wpj}R?Sc9DN!<7#0rId$3M!aapc2 zP7r{zHdF`n28Yi`FD~(!OM63hx=rb`=5hmY%Z`3mUfhL+93(&yv ze(||okEpv`k7&B-)!@~vi;kAhK}$*F@FfO;CpNwQx`pAe_U8u?;D@DgJtXDf_??F9 zR@aD#tbp<@Q+n3h*_o?)p!{2%%7w9w@`@2B-a6xTzs|3et6VD(w>`sEWlDQGAgHG{ z+!I%2Bj89$>zDj6i3REH`%)HI^HDmsFc5%mI_HSRI&<#TH80f^V6W>dqXb*U>ON<| zpDHN8x)p6Fy_1NZ%pJpG(s@aNNjax_J{p%7tq2QXj&@yc`Q(ieJxOsKvBaf;C+|o# zywn2uCTA>XAUSECv@0OBU&R&OTb9`^4c?2}vNEvoRhY z=R_Id)mHDH6%o=}HgP4p9N~m=eHug_vpT$Z;1$p;{lVm4_0Gs$-4hQO8Us`BlCo8C zRsuIye{V*vJN#F0&o@rcZH;pcNG*$ed3~i*oi?W0V8!yhBrx3BBVZ?z_dfNN zEAgvV5EYN~XiClWBmjlKp*(Wvv3(f4sRlmtlVxh2`|Puo6foG}!Xg>Meikp@?xv$b zC;%XO`81PQ4^*%9uW4fyWnkU?Y<~cUNZM_&e{Hhr1$& zc-1}o8sPpLXhROK%?}z$EXt_7R{%t5FO9yvul}I@hRaS(} zOT?G;jCIy;r!z#LooD?gzk4DeavNjiq>zRxj@X;d#PnXM3^P)hPOAVo8T+8xNITbj zD?YUaf5k?T_K6zA`mWL(;jyQvwYg?(kK=%Pc;L|IgbB3>cFU}m#VJ++0(sZ_N7&M= zbhP5;qQ>QL2~ugZWzQ9R-a-gA8%Xqyljy7Zu!Se+ecx4|W(uwCmlv-!+pLXSA=1NR zR>f{O#_zcW%BRuJKqXz8r8scFLgNwZJ=PBNxpW%Nde@K^U(v)%S~WyPX(dL-sv@Jv8!PaCTE`E;mEo9rwx zHwaQ?rR78v3>+dOm>GQKnrFgPb6;F_%F$K*v9880`a#y^?6^TuG4&s#i~x)O3kM(x z(waV|yHxQO6y5<+Y|*J&J+Tqv=&Gk3_Y$)UW zfG8>5ARvr%2}6egNUMZ2f`qgTT?3NR-6ge+iBlxJCzv*MvCk@u`vjpNmr7V`4=$Q-)WlI8d|5`JbzS4f@KAfrTR(7I$ zNPSY`iv7@hcZwq@nw<-VeDY&aOOWNLpULU?$*Hl(&Zy6qi-=!+s~!P2Q+uWJ5=2WH zW8Vg-e6pOh$9P0ULKRK25|46v9O*F7B}sVQXv4K@0YQ_#pedS&TF-CZfL zcWp1hsS7BH!W96@%F1DDW&s6XZPp6__yo-qq7%}Opl(?iv+ZOu%YGUZ%Av)MF)LD5 z@Tj2NX#OBw+~h*h#d(lQ7kId9JK~j)`|^#7MGil<)o`f*VTRC9vkbDJ!}c<~AC#ss zn9urniWg6W13lHIJ9Dka=(>3pm3dXbZimiF0f0|smq0`rt8$lcj${s6Ijm!MQan^Vj6-kUn*Roi; zk7hseDUz352t$Z`YLaV&u|R0*>zWrgJSmeDZuis65ItbWjy%1e!Q1bMw(H0l2PARw zjSKSdJdBM>YiWjP6Uih!Kl7gG->ECce5$aeBmaCg7?6DcT2-c`xc$)WTFxk^)(h6wk3$J&t8RgIM}sR+Z! zuWzk3IQ01H_nMttK{$uHvwb*OIyrw^dUQ!p4gyYQEEc_4y-&>)g;E`LBSj9cboA~W z58Pv2fPS1Llo+#qluHp|H&NwL9>2|N(MR^xiAWUW5AWxLM7MI-Vdbsy#;5qWgrBtY zZ{2de=zBe_+4RgLth)?+2UTCQ^pH*#H`7LyBiF?DTfBoszR?=W$V}<#r8(NlN@#Ws zMEfM?>F3dL9ee3^luwhdDMl~MA0JQ64SbQ>*xHb;2%5g8^7XMciyQ*`)Q9uy6T3)K z{fFz&z_}UB=Uq&HkEPUMp{N;MY7B*V1R_$xl1m9&nku#QWGE|9NJ$ok#J09sm#!HN2@@;+gCStWSGYoJs;K-QG?62v72r;c+FpNJFyaZGQ zIB%$CN2$j82AjFZT(wsB+jV)Bwwl*0*S*L=Pm{8}B}!L~R#VyyQ;VtaDZhOdzV5I% zZ#)=R9X<3dQQU`_{EL@Np?=Zoa4O$ZwGT>fNgS$cqMTeT4*5Z^3*u!+c^8q*>#L?N z90o#$gVBLwoKP00G)V=#7h34LrC-|HsV}qn1UNy%XF*hRR^TmTim4-}o6WyGWB>BJ zvJmt8UBUO=^f9F2948=SOtyNrUB|Z($^!A`7&@XAGrr6evdlI_B#y6KAf(MOL>*=c znYfqBdUSQ>xx2k28fBhqmCNw~#@jm7QY z4WJ-eL1McZ!3?fxfbqXuqFb&%YcOyEElH|5Iuu z(G<}uvFqaU+Y$lSs&LC+30|q5YbNDgZi>GsV14!K`@NY{y7%wj3(#Etkq=yFY4flC zFZMD4J38)NIVo0xd|hX!0V@_~!z0f4i(Y2SOCVW>v&w2J0IkrR4SD|`oBy9t3X~8( zMa2y8uPfh0YaxxeTBr{n(-nTAvm?wx(F%;sG1?B%;%)yVGIb+&-tYxmko&+wGwMSMPLC#At> z8dAZ{_Q9@hzo(^dz`(~x_WtTs9FkLfJY4_yoacq7e+H;~iNYuwGw>C%!ZI)s5B$xe zvoDF&P|_rqH2hosBy%L~AM!kVhS7)&c~%nVAn|(uRIV)uoJPNXEWtA43w}jw|8V5S z*|$BYgaob-QQY4ob0Lp_`(a?1;dF%l6`QY%0>7{7djc;Y3p58L>%G|3ex_I?i$NSF zOu*7G@YlnBEO?k$*jI&ESc0Ilvws%hi{5nu@Fw#jOzbvZ3Zl| zE94+485`3Za@Vx{YW2Cjbfz8tHdsD0c-wY!f*49RX4t@z49q@^DxWV{a+Med4^P_O z-hTDlMUOjwshLL_V6ht4kU*}bHNn1myx@A?xkjiG5LTA{YH9fIWfY=cy;^U=IU||+ zH^o#Q2p*%LeB#pYj5RSiIXgRx3nL;buc}}tzWN?;&+gX@tBdfdeBlY^8G!lKL!LwS1&bmC{?C{JQ!+_3V(UFK=#EX5g{JA5AIgL zQG3c1HO8YF88`8>b=s2wPK4dMWR3lsce+?07C2nVzWcX*Z0`u3QWE#I7RTZM>OHpB z!ON3D3Z;;Jj*pL|adOwKAAw}Zt%~z>0--dpL?{h^Y3%u2XU0&p^*9>!_$^HO@_>j| zp@JSO=;*h$w!^52kjSymMi>w7-)9luLkv4J@c<%~&yL8Te!;~C7$t+9H2G^23w;9b zXQmSsNc>F;yb+ceN3_DKD(vV2iC5wVE3;FHRsxO|(_Wxam{73~?+#KwAfW>9>sSAt+lY@BP^Ep%%z zp%E+ZT+EruJwDPfY zPey4#BARu--$X3B3h_qJ904~scQBO2Fq;ik9)-PYC*ukNk&>vm{$2JHk;uQP1ECba zENmJwf!`UxemJOD>xIAYQU3K5E66Gm8RjNWWHJ=#C*<`rLgka^!g>UIH^tY@J8jU9 zs+qqnMNvoc0i|$-CDW9u8WSG=cCsKW&*z#}KDcg#gOF z8$~#`Tm^0J1Vk_My2WtDF_??cQLLoua-q@6VnUswR~xo)kL?!+j7`T8b|bj4aBqK9 zM>kNw_+%~b+fjoAkPr958C>Sc3ji=6geHj zm^pn|`>N1qRC;D2b&_FD_!6^{e_DVK?A>U8{-(B&o$_2;iI3O>r;DU`092W-9_0LA zTYYrj9_Oe@x6U0TsQm%HbtHgBB44u#6@P0#X`uSv?f%)NWcIXZ1{gDB*Nzfy!`R38 zSOY-&5k2*-jg7`VixvB$Rwe7naijBb(M!5HPk@_h6FL;KZ;sRlO zFdc0ziQkH8PRkd{W)YchYz?Ci-~y&i+@zo(oRWIBE>Q`>?q#V&G=+^Ly!?Va$OW06^d)7iwSM3GZv(WUyE=GGCQV% zysCzR9f#SaM+|ThJ;Iu9Id}mtPx?uX`~F3>MQe@Eh7H2Ew`ra)AQVg>tGByNBR#&) zkDP_t*a)}#GNf}?(SD*vbS7a zPct0DK;_CKWYO{0gTWs^o76xC=S3Urz`}|Llb^|D5s%)hJ{dMEe*!a`JILCma*-T+ z8XM(X3dgZBO9tvba*lv8g%hhT`ha;@KkB{1q50*|r7O4GNkHaP7ic<~)WWjMmkJRw z`+jl5>rn1REgFkz^|h%QuTUpwSe(IIH2cT^KW3ClkVK%2&YA#*Sv6 z(`>dewVPMey|pj$>Aq)-&;!T#Fm|0CE0uedtg699ooAM)Gq6Mr17jP1IbZD8yZB8- z`qI0fC1)1M;@`v=k?6SiNZ3>-KZk8tIgdr|8zJcC=BfY`j|#}1P94~k^VERfys%~c z=!h#7GNO9=mGSo}gSo;#XM4Oiohx+E z$26Uh%4dglKEP)qmt_;Zejger+_OX{QTpcq#T&T>6c0y3pAKU6^G}(#_IsyRb8XEp z2>xN{mh=~1C`|X|6B4?==>TF%&EIfyc#IXaK9z3hxVkshKU)a6cG0({x|#>JjHVFZ zLjhH$y;+)P*bvrwO-{vbx`<}2Dnv3m+Bcd2eYUMt0PSLNGCQILZsJnyVh z;^{KKCT=$YZyF_J+>zbMh?T)eGh(-BnJKx`uGT8-xrg9fXM7BcYA(8GlRSB}IRyK{ zg=-Hd!riSo(DDMVb)N&2FqTK%kn%^feI{T0U(CAb)#f@MwGt|F^e@^_aC_R+S8Bj# zX@{|SDFP*OxRU}gJOnIn*DAcUv!5HA{@V0qC2pt8jCtR2tax9f@eMO6Us+4^rtEO- zr*wLiAC3ag^G#Uk_o0iRxzoM$@w08`+mC`_l~+xl{{g>}AF!SR%ba~aWCR>2^&pkQ z{>Nvd`wZr#0(+n0S)vkfG291aX-FO(i`s}0(oguW#YbO#P(+qxP!?6RSWpH0Z@j*z zwHuj>bB|dqb-LWPB1&Bk=C7gx&D+$ld!Rfw%BPGEL%Xyl`boeM<8Ur3hW*lzhME5d z{UumF%N;Ld$IX!BNzE|gO`*a{am&AN@MlObVbe~jJvlYKws7sU0zc5V8^8f^pu+}7Z)cN;esBM z9~E&tj{OD_@dG{FD9|xFpBwr}NSMZMX5rlewS5fdwS8!^mkYHZ9H*ijMmHk4oxgB` zw3++wcyN#qndab?<8~FD2+N zYux-f5p=~$-}9I>(W~tALh(kZ28e$q{au&8jKwG9DD~4^1&s>fw;)GO_~zp+lMuQwLakG=%#!g56!4eK1-h4p-T!ss zxhh8C*SIsUUga&=Ez$_H*2nt>4FVQJ=0}y*Y$u|%F}h?u91KQ}H^QiuJF;N?o*@%A zZ`@Ena@?zNaCxnW9RTOWcqB&VMtF|brTxC)`L-w+hK>uKG|{NMZhk7yYN*7aUo85D zN&eN6GX2+{+Rp64QmHbPuUPHsSA@%>9KWT^ZvHshzuu8_TYG;O^rw9<{n4`AZPQ5q zWT|*`f1zT*l7tz91`!=6Wx^ zncb5-$J*q3oS)&$M1`PtS8q6;$tbyUf`}U?=q{CG8GP}5a8z!pI+^>ApR9z)(Hqf4 zuBH<$aZ<64{0{3=1dTt!g3(QcsRVDfA9R~~*v}nE#M$Kq9^_Tfr{=>{s8p^<4L!4j&U9CZ_L`1 z*VAIvvap)J_Lv<=#zrORwvNuo1Z%0c`JNtjQ{GHFj8JU$Pc$p}aPg~tRJKnzsOXtn z-}GTy>a%`i#-S*3x%c?`Zd7_-122Dqe66}3D86i@I5Z?%i#jztJRCCFPig~Zp{fhq zCETJ`<-Fh%n*yA+GZ>AFf4RwM0eo8tq#RTWr9sfFXm;02P$;p%vsMI`pC!>EnpTFn zQ{64;)=kiDZIN=UPbe=~jm}s07i~F#rrFVx{Ob)6ZQjeU^ENZgq=8`&A%{2f?`}ca zqdBfXOYb3vrv+p>*W+dSBZGC(kBc5=S4E@?2FykNb#>w%s9`uLq3{A^Rc#Bawn? zg1ZU3pNaGiemq0?^d2N#ijS3XJ22*jBicUDy^rMI^-vn9}tyX zbI*%Pe{zwxhBVMp8npT%nLRZNp&!**&bjCy8)D=S3SrpZ+m3YhC@=5a-P%syJ9Wb& zQ+eiIQQbjdVz8_S+NG;ScSqdA>;XB`YT4u#HwE-%x%jW;TsG@W0#(OPPgZX_aMO^Q zr4U)&Diny$5LA)g4cBjOjQc2>oM1eax(1o3^}wPJ2-|8rn%^*Aahl3~(^q3u`&^ANca(NXtU>)jQm6hTX0lz)sP^r6HCh^F>!d z>(%CJN~sXInbp7niSzd|sg>sRD4o#dK4^U_E<2BfHOx$yoHBZwh6f8A`eg0Jp4l7* zMS2|kMaG??!4npZ*5kdTTE=bVNkXv2cQ_pQX}c-QS8f)jJ1kn~oPL7Q_o!%CQMV*- zy#|A^-$zwxtPj~)BZUkuBqZq%3i{N( z%#R<=G)4%#<$2qoa02BVt{E1F#!QQ=*d~px+xB;_^3~j>)tyX8C!oHxvnp}V?%js_ zC^R^s+XgaHvwpv3&l-CZ8u`TOWW8OtXirh?hxv=mio*~Sn8lP@c5;4ZXZ}K&BINiZ zZr++1tZwYn?(z`0)8-Q$ZI{^TEobC@P)4=5bTW71da=q<$mXC`-IY#nrx5b^$(9O( zq(!eOEG^dr*=@Czcay_&;1bynTPJNAS*a9;UM;v#C_-4bEN;$;YRcWnU3k@;v+|o9 zso|LKQLJIOkW{fq<=SY^XyWN=($b5Y-M2-8*UT^mN|nVj={vb*h7G;OMnBqM?946( zqxCZ*R%v+WzPPR|I_tQuD*g02gsqeqw>ewby&HX1iBO#s@ zvhMT_=7Qh(hxz-;jC?*&+u3Hb=L?@4P0M$1EegeG{2&KR5LOIn8C zRwG|`1%L9ILf)V^b9pn~BYu2j#C^`prB{R)VsS3~My@>E%MM5+#MS;(;J~S~+bs#f zeKLB78eAJ9X$*Kqks;-BP8Oowp~2xr<=AI8x$0D zi!tz2K*VpoyW~JNUa33#Zqr-E2g|aFOY$*nX{Cwa>ToB`HNRtz&Wd6qA++?vi{l}| z>x7iD7uQ}dmyj#a`X z0nU7_SC`U7Z*S4^)hC2k&y{<(#T4Ym+^fmyE!c7BaI6yT9PbxpQ_sz&knQ+VS}@Gy zl|?k|A-@S#jubPsB(@zGTVn30yjp+t#mb=kg2U1x^q)0^SVvTAR_ zSGsE6ikd>!ztqK-bc~?38?~AT-A<3nCf`X3+QCi^QU6lUM)cvp{K-()KEg$?I1|RP zc@#V#G5?MO9p}Xd2%7zUF7~%~f8Xa*beRFN*aWs80vGJO&}e_aDKtzxox15WR*|zC z_OTit9=5IX!G^SHh02w?_6tbGpSB%%@BF9=3lT%?u5gR@jPuIII<|jux9O7|D>bW$ zj_2t>&eMt%ojTNOn|F0AomNT|=vP#Jk1c8`o0w8CHiwxB!mIcg85hc1CPkDdMcjDH z*67wdSQ`>H11E=_q@b3QV;XUMJHiI$ZAKEtZAOU4T?nzqT?AdO>2VO((-Sc|S`(Ab zik9YuPC>)LKBA=$f)2-uiXCpvYYCe!b8|!<+e~JH>w&E-EtBzXPpy1nc2-AUPsMmJ zZ`MtPTQ(dF7A@4$TZ}f0-aLNCnm4)f(L;S7ofGpi9_T+m2ss0U zaHh%l$*=3Me0{^g%{*Qg<(zOBHj*J|Ja!nU>%1hh@b*L?UCT#CdfzL04LCdUd*Tz| z-fIiuDpIMEr{I#`l0pX=|?X=wx)SYmtD=7rN(vG}QHd}RgWCWXLE{cvl{IPYSo~_@R zW^sI>ET@UIW}rmM6prCp)&KJNT4Q1FW~W=7op3s0llS!D@e<|cU5H=mb)eIp4#ma( zx$qRTt{MY}VMY*KiiYUz<2mEaReZpk(xp>F``*E2=s{&BxHnGu`++~qYM{_??3349 zQkp4;T~o*O^xF&1*P`~1rkWM?M4;;~2fH--uyWDn$Y4X`uDMQ6Gw<5b2Ib{AY9!y% zsmtI!xZWU=p(Op)srK<(VR={k=q0N`;bB%`o97+7OUi1?MVFtRKBraOpw6uCF77o@ z)LmwC;wtbEEk%O`t`U0X3})r|aaSA5@%f~dWrp)D)zA&{4^O{uFR0Yk+7hs?2ks_*v|84YL~NR|8v!eOHuf0!}<)K_V3Ff~6hc{*Y+vVkwZU+}dpZ!r2PES~osayEyw!u=z zCf{R8y}X<2`I!zH`KnS{g5`?q9W-;E%f9#kG~i&Nq0cs< zf-iAQ9N$g9=Idu)+_^fl4%yo`h8J5jP-Ut+HRm`EQho1RxlFpKp<-#7T`LG3iDuRM zf~jxT*X?dI>5zgeto%R%Q8vt*p*#gE!PZiCtaV` zHdis#+FYRruGScxx=(xeGeA~n7%a04ws}g|W3XGQ`cxrzxg~utr*=4AEdaoc8i&HA zuS_>+Zkh(Vba9mqC;J?IjeIv%VG5t)c{jd>Zkb>=DAPuDIeE*m@AHjg@w489m}R@AOBo}^zqww!%i(vhD$F-7lz5X{(- zCmW9A<{D}D)-hR2MlAQS+wvGimS3&0cC+2I8>~fG4vG(tepG@a%&oLw<1i3E2G<1M zDSdSD&z0FiXn>cbb#p41#H|M4;z7Z)y8{{2v5sgV9**pK`Kgz_h6|xV?--OSI6Z#+ z0iMR^^dw>OigktAVTa#w6+D4-b?x1CMOVG%NM3R4F0?M&d|~0o;-_WOulj;`(qq*Ne)e{Rs)km&(XF?i+CH6YRynp85sPkeM$@E(Skmv^obQvoWz{}k4NjzZ83IRG0x zLR=nARE&3Pbb+qFzbNty!{c5EA?jO*A1MU`MBG72_cv6>e1@8=wq}sSHR2a2c9cK4 zNO=au*?~$dc!#%^@ir$h-(Kyf+vc>-<*_T@uAIL}k`x?t#g~mm{~UCuV6PoR<=b({ z+yED^o#H`I%YTszCe+HsNqT-~9>Zfm3Mm(h*eky_D?^54~2umz$m>sR^~yH zeqa$4Lc+9X#}655bhJDD1ivV;r{D`2fkyJZK)n=2I?QVDKpi&;hBSjKpxSfT+7)5c zSCU9DVDtqAbDlQmZ`A5k1)$Y#*l+g@aYPqXqlz&VJTC&N<| zBVabMlz$kZ0~i7O|HBAZW@0+J{J_BbFJ+}!L^&qhTB8==_fE-33HsiGFqG*I_Jq0Z5!7_PM|lOQw7SbEilwG0%`uVBp|T=)L)g=_s_-dGVvH zT2w+pR&g<3GCm39Fd_278QMjc2CtJOe`b8PS_WU=VHCU69IpRfAMqCW;11%20>m#D z-Pk<#Pc(*hM?CIL$t)wGhD0>#|s9<@jTX6 zlihB+c--k{uF0+OaCuCr)W1l(1yFU#&lo#AkF2fmGtNoAM{)V!5)b6hQtH6gzz>NF zBWAgRMt#uSNUr8u4x26TQ0LN$dvj^dtCI8`h?5K@7@iRZ&ye9E`V8~!i#-ABoqhh! zIpI=)fguBF%3DJdI&&p3yP-z|F(ze`+kr|227=>EFDeSeA}aQvQ$W;v&=RRf+QRRo1`Ir`j{prU&%;fJw}%j=*kvS-oCly zxd@uS@XAr0Ned2-pCpkle(*{cQ(m#n%7whW#W{3JbToTEo_^djkqiXP3TVB1i{STI zb@*t>*mZMLd(r`ft6IZ^p`b8w9b?UTNi2MM|0HjFykL6qc6FCnfW`6HG(Rwzf!+V< z&BQZb5*(s%dpQXxEQ(X_0l$)nR1~{b|6#EC{5LcCJ5~B0#edv@?BHGxJ>McJk}24A zl+SxVG-8Skm4=J!+|r^jaL-FMHQ8*U4fM8`1Dr}cc6VsBZd5_a6=Lx$dkQ~gtLMF!3fNhx zvWvFd&ZbvSm9~KDOL!i}c723>EVUg?d5(SByClCPvNkMwY|UT(gG-U>5L`-@l}c?} zp|#MaHRN}E`Yl*_ab-Q!CvJ`_j2q41%aQQXL3~`+)&sK=i#emkQ(5&sfRL6WeY;ll z(Kkv=@1LuH1fU}}aDV(b1EyzUsgCZA>l=KTJ^d=inB`#G<>f0RB(JIV4-VOMvO^57 zi~!$kmLcf{Y7TUj-kZSWZo4(1t#+x>`}uf@S-)oj6Eaod1-E{+yzj(h8IKF) zuK9T{(Z$_BF@8%mjQx!;q%=K?-o3c$M0Yc%R%5L*_BH;tVytk2ZU);rEHM)wfu>@B`?e$f=`&V-o?DU=gB%o#Xi8 z%<g zIUF`L1ZJI2-hL-;gg;_{lXmVWyItSpuGsAA!vsF08#5%|!IBo zpvpG#Ik{J$e0sO3liPRI%twd!=R%Sa?l(#`|BthrikE?7N?c!5-pLX7(=*Gl;_cuz(r>voE&JS9BWIoGBUf-Ml&!=X^6!*;|$&*&$s zWKNYL;8Cd(Y;iCr^tDQIiKdzwQ=bDCTrpqF=EQ>5pn*83kw= z1$;|W8J!|RXq(?PrkC8f{Pw19GW{Ym(6xKgTdw54PCQ~9E)<#Hwh*eUgkfx}kKbFQ z;IE>6E#6&=Ou!oYmQUVQwtafwl#c5x(G&88du7E;9$fvs`z_x?@B6C&>UTfkT%c2d z`|=y%xSQLT)d-OsVq%XHZkN><0DF>v%i&(IFFn@R4({-V&kc^N_3;p)&ZvwRFRqJI zGNKhKU$HpHi4YWZmta9|c)kJPk!5Yjk#>CT!F4P|i+3d6oSPvcF# zwdt~!RR+UVb+RwZZ8VVazOJPtSnO1SfavjScvr>!rPJvsT_IT8Cx}o(dA_ZEyn=hq z_R7h_=hVo7r<clVcv0e=D+}) zTS4tGT5x&L0iWOT2{%CU;n|Pcx^1Q_>ti*(Tw=DEC{`YPKgbDg(3UL)NfFouy{{Xv z6AxvT5AK^PMjWju8yy97tvH+VarJS0oALzIeNC)rUJLxEyii}U4{aKjEVr4ORo3?!Pm5cQ z-mq5D)^WxU!@Nlz7rZXuzG*gnVL6jN`f}o+Nd}H9aSA410nRy8z`9P%hPDBW-Hzl@ z>~FkkVQ$Aph~*GC7hpZL6>@woDj_n9X}7GmYQ5Sv%xW})w@cheYY4GD4Tr&DU5*zD zg-;ghTPnYIOL!0NPJ3H+LGn%5(8W^2r+j3q6k?Y;&t(C5AAh`MzPtXAR*9MM{ar{c6)P3O=vC_AbH&S>5e{Hlyg3CHmT@ zVX+n4JLVY!Tj*;1Pvn!9RC(+!)ttkhhxUMgSWrdfKO{89-(!T(iBuy0y&)EziT;Nbm6Mk=~mMV9t$RsG~4)NpzZTALlicPaXz}?}NKJ zc2Rp&wvqRdZj8Qn4X}o@^5TaxyZbdsvbFagI~8}--2Spv@g-y$`KS~M89(`XJ4VuT z{R-(hHNImWVmtZ6NXT^A=f><4fI*_ z7)5J+{V-Y&Qt*}=LJ0lsCK<>ozRcK}!2lvAlDNJYz5X|^o*R#nj{u6pUaaI{`-80L z(Y*`b8_@Ux}j(O^Zn=( z*zKhW`N8)qk8H;FVuYc&+-QXon#lrEstim_N+cQc?%rI7@OP1s5y`S}FaN*n5aLHb zj?hXOE@J;u^`c-p9yh*T_&pnj$C#dUkbsbjq<0wZV}1VonT%9VVRqKoEzYk!D&s?= zYkGS6!g9rgEfZvil9AwSRhaL9DdNkC({Sh9xI+sEgV4PyNBeV}Py%HYp(Dritj-7B z*mzcn^$GO_52w+d;b?Cqc_%lgs^MKQ4aKI=Gx5H|1R_w6@tf_>3;_^EQa_TD{_`@) z6cjk6c>U5mPDZ)Tzpl6UCSCiT@uKc8UtWn%?yn3nLAc!4X?an0XHXEBnTn3Z;;STsIi1RZ3;fNs@yni?WOx7z=JPXO_TS z4e+VDRFc4P)I=!zd?YQ`xP^}!2kPZ>B^}K=2xkJrKa2CEHD*~&y$_<(P70i)D z`5!hFnt`YDmr(VfglettradM)YT%9GK$0^nY#tcPLO!;lFMsWTm)9erqci5`KYU(J zF#JVbKqfqWU{lw9StkCcM^#1w42z-&ta`7+ToC~0_I&SnNUFeo<$XWEMr*_3*s`UvG1_Y$T_nL zGd?gjHohZ*ec`VcM8Lq$s^foPqT{%-0#64cS=sq!NCfNX;iSYk^Lt39wBn8!AXr&h zUm#>`7h?Yae+ClBCOIJ9G|J0zPrl)7w3mSW`mOkqyJ!F50AEU7(sul!BkSNkyTa&PE5ZK_p zfR3{V1YpFjm@njidJ;7R@JJ8>j@1JZq{tPl`K9On`!7}@i?o1y{|IZH?Tr8Y<7W-l z<#rS7emgLC@3eH|-+T5~+*;b{VieN?kd@Owj@DdB(pd4$cg4yO12X>pH1 z)U+emPcjhyML+=iufR7EKFoYY&gsirlr5B@Vr9K08?iM5{`9Ez?M3dRtC2>K-j zoYnEkI?TGMZ%#yKd#DgUlHEw=GMl>QQ)~SmB}U=c-Ki9q&N>08Ljh3qaXJwl(Ua~HgF z=I`>g!@71oO&zXju!OH3K#0_GRDEVA!csS6ViNT6@fX!M`C$ksC^p>-Q=U`u#9 z$0O6D7?{8aMe!TK>z3-uiSEL(=F;XvzH`FN57y_z1fYU||CA*lj^aeV zEj2%0jfy>NrYxKkE^LW>edxLD*%>c(r{ZOfN^dWfxXY)c^=aMlQd{8M7=P1m&jRE} zRwzB$sfw#St2R5yGn>5I=lb}#HS3cY_CaO^%sO%74@op&_;YrK1u&}YQwJ^2E^Wqg``2?$~g)d%# zWPbR-!YKTOx#8MB+%tK%mzHPz#N-}4Agt?69Eq?VodzxdOW^czv|))PxTQoBZb~(E zJ7HwJrT^bEW0=85LtLmsq6t{_j;=;zeOP{L&~3i|o$>D_4?JpmdDh}K6{ zlu@I)#`|5kii5l(=>xa8-aMaAZ#u!#X(`&o9YI^|G}gHbC~wT|eOqx1M`c%FyzRG_tzIK_lN1N zP7+>at888rGaIA+;YgHsxG5OwF{f6ur7xSef6UBpd+-8e-B3BNxRzNzSWZZSuZ=r& z#PVy>b9k#|4rk|di5d0cKOeJN+EEk^`%&D#lD(g1Fj#9QgnFQF>g2Da?dn0Xc;k0^>f4jaqC_f7vpXd<{#^1KX%WM+I% zt`XVpEF#(han0Kw6@;1fZMJ9lZ#(X#@XqwU74I8)eY`!+J#9`6?(Tc21Fol6k0%$* zNS1f+#<1z6%;4uYcco{Bl-@xWb(6E7wBCU~FMH#H97)CRAZ$rUiWgW;=Sa`egLw|Q zdG}U-1f&Spka*HDpH6ivB~KvMXfHJW;G?^9h0%DKZ7YR1(bX9%zXbFd#|5^WDXf^4 z{_tsykE&+4zFq{%9%svtQDPWqP_}zk`%tiTPZemq2I_!oyF?gF|2|M9A$Y)=b4f-6 z)>P?M5rMCsK!M%oA*rsG>qbVY4@|L_Dc}utUP!xU+Pc!!&22#e&17U}e|*k#%4?84 zmVEHk3Rtb8M!<<&*p=<^+U4D374Nj`Agk7Jo#@+srv`t~C^H%X_0n{txdL7E+?BP8 zW*dboUNbY|@VPzcq?3=;wr92lgo|B@EgM}p$~S*{#wAgIt$0p*RTu$>hJLv#gZnM@ zV28)V72x6;CUM8}1!s9*dtUN4bzEO%^@yeOXo$w&BndftCP|XVv1Wh#O}{|{8Ka*s zk#Cu`ZjM}(^35t+kDRX9?Wf;nyg6NALD5{XYq|Z-1k@{G&EeTkWAVmHozqxnkBT(& z^k+Sz`<;-g1N0nmAKUY-QSlhgBtgY>&II!=;;ssXl1Vh{0$JUihlz0RhwELtm(yV{ zD=xf}R8UB`cJ2C4{;`3yk8*Nyt;6t9G3kGV8Xgi!)ZiZ61tn|j0jOh(27OgFYWB}z z|7*iPynzJ_PweL=E?4agn@C zN=s)Sd<3^#oj%zXSV-PQPA+f5Cjuq!;JtYHQY1aa<_d@?Ng3%K2y?ED2#?oRg6tp0 z2O{S}#q>9!S~?f1l;hl6bpJ-GL8$<()1lAAQ}g#vDkk^D+f`>Pizn!jO;aq=NTUm2 zhJ}Uw&?X#<_EJ((+FI^15+p^lC$8bSco~H~7?ja#2_@R23qC-N-bIW1zu3Ee;4w$l z>E%&fYHOIB`Dy>FInXgjS1`v!Yf65WwFKFT4-MdQXAmtd`^}Y@ZQfS&I>xdKaGmga z9?;pRW#pM6vzal!vY$Zrv$;N#w(n2u_K(FdFQ0P%Lf!tGuU9djsWXU*>bvZ^yGpT{%17P@90Hx1;Hq7h!WLqwYT<@?Z7vOvjcD0gSIVYY9eLY@GSw`Z zvri_5ucv=9Zh77^^P>gaCHvv~pc8$#Dlao47n^=1zqR!~0sYvd#oRL5T=DWcg3qR} zNA%Uc$VP`nlAfSlPhOeeFmPvOzDbu`0gk|4YSI8U@so#@veR@#-;>2eoaZ_{iM5w>+F2V>MJft7Px3yXI6RL@Qb>ks|u$&81?GVSyT+!oJBt}7pL;o@A0%bPcu*k2NTqPe z>+JJ5F{=_C^qLeu}(obnpXjOVt`4rboL zng=5@)3;NQZO{Ee+D$A&qo@duH^A=Xc)g z`u;hW!?&kuA$2iIj7q&mmoEJel=TkWo{G=j}bw!TThqamwXWYhU$ zu64XoM>=z@@>ov15Zc_u&clK=yWvBnpD3@pD`XZ$rWCoB9kc1-pu3cwwI2tFuD8C- zoioy1-Tx3gC(_-_1*x19BZGjn*F`gD^|C6XiOlE1L}P{ZUcXH4KkA-wLK?uCr-R@5 z&~F+51m#;5Is{>#FL3}-=r*C{KpX82fvtW7h;$j*mOn+DnSBc_BwTQh=ygbi*I4oP`CdLaz!B0kM?#`9{?4&XMEm842(%7-+6A0I0j;r+XkyVGt zr{v=`p*lLLuO0{G%WfqIB7H^MaJt^U!s5sd5t-}wXOORaj38-N*#-|qJsIqqtKEGx zyAVS4;px_bQG-ptL;cSrz&`^hzHiLxrIL~R1TlmEhyOh{#=D@-~Q9*FbN5zXQ zcA^ps8lTOsrZ+dGEPj$NyN@o$^SYzr-%Ek{37BdU?u$2WF=_#5&d)6A0t%5dKx)WbWXQ(_J!Gk&ISe5CPo)iT3bPUi+ zeoR2~OIeWLe8c4hb-@;{EDEhO+<5-z9^KrEfaKVGpkL#yKbZVKxW*Bv_gHo@5d#bz z1cWzQU_J*HMJAPCPYgmNz=MijidkG~yVn~ob&i=eAYH9LvICR3PYwzS0@B-~N!b_y zD2o0xnFe8>e9@_Jx7WMD&i4@F!tzhF+%^r!_lTKLbg8rcpA01eO~=P1S&sr5Y5cJT zGHV$B7E4z0Xp3cmzzG8#fK9R|jpu$t;J@8n#<#ZMjh@{bi~xo`4>0p9b@RwAt-cC; zGgh?q4Ow`g%jPxwPpYD%U@Q-v=mJyD%N`Wf+VV%t2Y!6NwO$WB^BW#|`0#f_j*~xO=u^ zuZ@l)AC3H5+@8G$;>MQA;&7c2K`O$qzJ7rSVYy))m}lp16nBi__u@~Kg8CPm5n{Em zQ^YX?5RbX91D!)M-?)4o7WLCIb%k$RSX>g`$jRYV|KgqoeEfGyBeH{x({&pl{>i-Y z?O2BQ(!U*vrz&6_aH#?>G5w9-GiIX~SoU{+syz&e4vFZO$2d7zh!6>7yRQR)v&bc_ z8-Jb)ubMaqOv+!Q^}WmpplwXbNcPJrT0qM_XI8F4uEhx@n{H)7b7Fx( zMhGUzusZ2v=C@Ye?RH#%vE#(hme0mNoDL8-i)*`+ty8rlJl}ykruWJ8t$ih!*0*dw z;WeV&f8^=6e8B#&FVkcI>q9i1tM6GAX-N>%_T#iof+Yl7E;#%TUQ7Z*pKZ}pF9`^x zcz9AllcD**q}+LJ=#$URT=r)twhN|u;wZ2;=R|_n=I^}$$rJ?taX|C!2GiR>nTWw3 zG&EGKH0U5fw=R6`QuQ-<G0a?-NJ!9ywgQ3Ixo)>s zW4Id+-B&1UsSOwR1)0#PP$Q6rICBQiJ^`dawv4&oK@HEA_D=et`T{|pQSSs7;5vSN5v-pve^}H9-d~PB$%6S*8^}Nn z`*HNBlTAg7+F|QvbY3S-Uv^*cxL53oS0VY=me55qY;>bI-tWKguK%9nL;*}0V*=#L z123G$N&!NUSI(Z&P5H)!6MwNUx7zr7$~L{Q=pi1N@Ic&WFJ(gZBVQvZUr`F`X-iK} z%XH#=_&V4(JoRhKwvUVAEc@HJv{#kx+_|stdj;Oa5 z0}K0>hYNuS!^;2iJhMW&`DvH?>~n4w*C(RP@#Yb9)r7VdX#*zp+mln;A2<6%=Ett8 zTc+MzGDw3u6R(Zt-ynFT{GBR}>&eq;(&_PjJ%{maVZm!Fx7T>0lj}EAW)C7q2}Qly zcR@85z3P*L`0m}h+>S3@F~MwEdmCQ;X%0d3&{25`j@XRQDo)KG-_5pr^L7gyo2;|1 zm5}<@P`)fMKfU$LocflU+MCOlFNZvC-{aEA2+69LB=Ule+3t2&G~noc;hvW=)Wd{V zs(k*3sCd@1`z_~i541RJc;VM&e!mp43Z4k@I~;%!)+^U zH5I_YDZ$zJZe;Lt=u|5ekH&GSY}Lt;Q{iqB3PNSgzw`%p|6k|DfM8DmQdn_IzH!mV zMItqwM&LnY-t6|M@LfTx1Ki{M-70>VuB2;V5Ute0xhGqng9V&TeQrMUPX~dBW%pFW z)~5WLTYP3(te$E&OU2ByazLmN7Iz~S?I>8?%g9f6L7<@Ck1$4B#bNnN!?7+ypIS%? z+-BUni&`wIPB%Yn&Cag@qUZ5d`-hCUuK2K(3wOgL?rdk5>5qTp(On*_;A+VMhu)Z( z8d)aw_gVj=1l~#`$feZqfUWPO=o(f7%OPyLt3gJ!X`+_{ZUa-GX%FZ?w>D@`aVf8C&4gJYGOv^P%e<;m z_aR6GjT6+XKIeF=inkCEVZukV)qPjo&~>8#;jr}`ZK#H!siK$K^}ikalN zFB$2&1;le-UHHdTOIG*GPmZgGId(avCM4X!|(eZoeGCYI5SbdyUT_FuAuIzv(hF5C~_BuD=wEE=(L?w7KDIvS;(VETm?V*;Gb z{@1nUR)BYFFo7Vf-j~LE@_B%AZnc*u-7dL+BNzOvH{}aJXGLtHz@9S zzmdma1#+^|KkIJRYNEQjPv@2MIJsSF*TiW-Lav*II0@5nY@9uoO%9D_wV|QjjQV;+ zjbnBFw)*B*{i&I?jJbx|85FuXUg#q2SOi^L6oAL?3`(m zL0MvL>A0ZP(CtjaG6l01_10^UlOO(OX=?lAh(gZIPN6npKlWKHy>ONZGqUaO>*o z*e~;4He|X361ty1Kl7A~8odFsdWoO^kBfD7@Nf=<dhy2pUzI+0iyA%{6m? z{?OeOwBAA|(C!v=y~DzS+tz0HfF9J4d?_YHF82TU&-yDsajtsf)KMYVM(?JeuAKD+IxJ+_Fk>tl z`)-lU?c#NK$A2K6yFGx#Q2tE-#kv0GcWwh&;0KP;{sn7XiNzDKQ-6?>w5?ln1g|& zF8oz4^OwtV<}m+vVer#?jIB$g{%$_${m>`>)JK44S91fpPdN3=kGXPz)?cA6=5r7# zsBn_`$uWumhJ~NQ=mkM!#KD7ka@QWu(QEP!DrJINh#94cJ$QtSh^_kwifOtG)}gxc z=^KWB)y8_0h)^D!-#hRPV>zQdMXvjvq8rytkbR!9N6f>QkpvKBpWZZiiHnISZOX}_ zokkL6)(s})Tn-+r4SHCuo^kRS^JT_r^_;OcDUrMqazt@pLQZolsjn=E_PDROmLgM5 zY!Kh_<(+Nr8MApiG)@}*KL!)C0=8?8=0W5?NI|=mjX{Q7*>BeYn5+qCNPjIln;M$( zq-`HO+^$H4>P`EfSp-DL>|CC1k+2=9 z*SiQIFffv*V1F(Zrz+z4%8-Jk4hc1)MeZak%Y^8W6HZBeK==c2w;4GdUCOsqwwrQt zv0D8Se+9<-l(&^6(-P%XK^_gkyl6A@k1l*d;Ew0J+1BQVd>Gmlz|fO-v_g!w5Vqm~;Y@k$q9>Cf%tDx?AWX#M`$HB}zcx2L~uGSSHjF7fp2jV$5 z2_x_H^ zW)*CHefKlptU4&-SQ*S$Wp8>_Ep7Jb_v67&WnSXT$v39yJ-;gEYLvad#2`CCqHmsz zB*gtE2Mg=+1a!~iy>o6)Q0#9mfc1Et%{aO?nb?^;(nK9*O)ZmJiGmX#^~(dw3lpZ) zwR^EhVa8PfivrQV@Zz})8-j6IbCnNK2Nj=+Z|bsYo1E8CACT_3e|0D~aJ?wClDU=U zA6*L0B=C0CODqZJUq?6H!4rJ}kH(>mUTR52isJwpQm=Rez-5PtTM5Yr+|y<}9IY~L zKG{0lv$rsPxJ}+z!7%G|sY> z+Yk~MWy3{tMiK8+UZR7`WAnx>&jr8a^@$*Kxi$57+)MW-?uBQgm-tr|E=@7Fq{?l) zAIkTLGL)B{{kK4HBEIu4~WXpB`FAruH#h%Pe*jKS#-LwCGR{!Bf zzm#cuU`9?&ZB{u@GAiJ(?DB9h)QbfoMQRp{QXJ>NPSvQSC}SRRl00%g0Io>W3;39x z(2Wx36d|%xQ>~J4L?2R*dD0T1?Ahb(Z@?qlCa0tsY%ZWHLfc& z2?a&s6N;-AOLK&$V{z9oPi*rsK?BKX6xWGF2Aol}uNTf?p&8;1bPBT+M%0Y@hr14@ zqA0d;tC)aYdp>R$=_#P!hHtJK(qQ+ex+M#p9&17Zqdxs}fDjUH04|jd09BpIN;LgP zWt-vpa5upjZ=|+`ljAo3i%4}E(_!?gww6iYS8xU${3>L)SW*y|jqTvN@S9>apY!C> z1cz@#gvd>~IfEV1luB#S1;G3?B)s{@sqsZ8TQ5HZn5Cd{c6ycY(S4)D1g10Aiex<) zcKl=>RJ`Sdc(<1?fsvhTq^Kjmaf%B(wbc=a2=3Ca*9)0qu9-zpcErtn(8jcaSbOzl zAM0;EQdapp7WbTzWx^fcks0{jF{p3WqVXWGyIp0rxp(U%fm+mL@IhzgRr5Q*@*>H; zppLSUA@1$=^7zA~_*zAXrx36#+0YI6Z@v@(Xh49cUsb)*p0&r{QKABJUg1ny3JL)a z-hc2!SAZ;&-MWZW7i_#}YtE5EPX8?9cc zDN*g2Xeis#t7-(b37;u5J-+NvZ+~^^AmWOUykqEDcsn#rt(zl}YGE=t{3Gwx!Rr?! ze=r4l7X%kVM<+sH-n%h)rE{g?rY91vBG-Kb@a_a+@YU}d_=EplT6hbc! z1`QTQcAF!ssa}UW9u!pX<$LKQg{K8cKk1C@I1}U__x|r7zp9c}fZh{bXn`;ox5Iwo z+O_(vDshHZL5OowDG2q^cg}L?*+hItFpPFr4aMn!ZW>AJ{6+6as+SY;JsuDOn-&r5 zRq0oS`0mUrbRz5^490_n&kQ*^0##_>UZ<6@67`O%qvT%^ed_;nH}QzH8{| z**|~Iz@ZsAI;#YUo>Nejts#wj39?tu-rYj#3VeWD7sFG3oJoWFLJ)4i+vi5WcLh== zMZpHHU+wklUjQl2(utb%p3-TV0t@?x2a$=p5}g_de_gv<9(14q6eNW8r+pg+J-9mi zI3QWclU)+Rty)+>@F$jD`&x^x@GM%fl4$g2?mjgG&yMCy@=!$beRVRMml_ZLCz$0pZ0j zlbO2~GdZ!K+-yxq+!b*>2k8wDQ2kY48JE~NyxTD)1gad#V%xi@&118NSKSAe(<*L@ z7utSr1-IR-v0P|heKQG4k-DyIj@$t^l#0wHF|;>rWdzS|=;WLQ6&O|R048GGCM#DH}@yAR`>BybLVpmB; zTQ8Y6OctVxQwKF=NozJ0^S;i>~k1uU-YBQv+4hbOa*JPt?8ARj6=n+lCwY?_YK zTe{M2^Wp{>Q(@~fbUGsLlEa0kk@eKQB4^#H(Ri1XG8TZ(Xq>JXhb zNF&n6vFdK95^$B#%|jb=dr7CAC zS2<@|74a5SD`lJKd%XNptu*0+;8$kMpDHXvb`GC(B1tX3LIpOMsMSlmDwjrqO*OYNevZV+(m~#`TS-MVu1yZrkY=0x9iPxc@^|gIx)*&`-TDi z_I^dLWe=Zxafq<_1ux2Pb$`T~4H1vswCjpQ2(t<%BBgBf4yQsy zKY#pUI-s^>1I6>B5=T7kZkf2_1YG($ulozF3;!QQQqQ?3*dbEhtU1{o5UKg{yRB74 zwRBkFElFM;E*A4aTnb8(ItzTx5a&wfyPQ>3am5c^Sa+*W(XXNPOSZ&W`Y|wYYzx}&Ti=GP5 zl-Q6*d>z*mSOT6y>-njZC!yIG6L)OAGhe=(4z?S}_wY}C<72?#vtQUrUA9ZderW#P zS6Mn4ki{r_(Cp?)W^lqGBGQ%;|74{xli@ySc01L!4Np3qu_G}i;n5U{$odAhMZ{hl z6B?ad;xW1_a7#|^>d4Up(UFS$Z_8UwNy%bm0n6M>KKhvOllMvb$hnUb%rHal@r#>u z^o`S;tPk>zpsmkr_{H#;^$Lgs3*s6cRLAfjIB55CxKJUQW8g<~kz_o zdRxRnfcO3+NB*WuhrKwgBg^C6<%G-I^NWTdAjx$!IKAw^^Tb;`>>4~MkRy4>yr&y5 zuL`_Z*6tmq#5yMr>U+M=FB9oK+W*|HJ#TSZDJfQb%AlU5bHiiTI-IbU-+);m_OohQ zOtR!$KDZCyof(cDpqQogXC%&imV@TsJEc!~KdJ|4(aQoWO?|3yQXbT2XjSntSV36u zosMMV!PO&X2zTdfJU?=!7!WKP-1meu>t?& z(#;=9f$0&vUqKbDl6r({Ob2v-_FC=XVz5ugM5g+op3{O@ohWg?!Rhkgm;2dU&d$qo zJIhVI_xd+^I(k<1htv@uwoq{F=)TNxkj% zs@6e$>z2F3ZnIph5N?fxkc7m97{i)2z?LT%kC__r>0<6*1Q#%p(~&KCI`6Hcg$nrQ zlwp3AgSraLP(TtDlkS0I5eW)QUKDpq>RhsrFGvCyOE&FJn zCIvWLkm1hlTp_P5z`aMLkVd;RVrSEPEqCA%UWRM;%~J?9U3>`WtEn*Myb-JAwut5>9x{YLw-^QA_sxt__Zx|Q!Q zjvu~PnEk_iPR6d z{Tw`nE=|59gJd6Cg$_PHOx?^Ft*~<0*jd}pVfWwlmjG-oKm?=*S9#Z%-e}ia^~mu9S77Mnk6ri#)fN7>Ds(a!^2vMld3*aj zv{izqZ0Z&(SS63E@6*(c)x?ACg1A$=YhJ8R0R)9B*5??VW$PZ$G#R zE^2+Ekf$S9HnblXIX~6Z7*@5@V@|q9Y8>$Ee8FVC;m-&{3Q2a@&U2##2OS(ak++)u zxtB<0z#?3)gdr<=*<{gu&qIdKx8&N_I~i^giP=psq2w`Wo@=J54}0qf*rts|iXIeqjG~#}>j;zj60M9ranL`Sg<$Ies#%(B(ucNtvl}0! zJ*Mm*elbW4ixo`Dkc>f#z6Kb12jW``WEIaPlE*k zY5uvY>kokhAq15axFL`MFKj!&i(bs*ph;eHl&o;p-}bP1zwA~}5o@^^q5~qkgm#G~ zbg#|c^MgL~L>Kt#*?o`nPFZPbbsP4##O5sl;4zvzd{t>};%ePDH} zW1(=bkXC)g{@?X4mB6%}n({-h{Z1b2UN8e{eHk9A|ClPSKbcM4u*SL3EN{KJ< zsS8@h8aD$|zPDF<(WIRZaUG4jlvB3o@IaEYvQ*05tm5k*8qR&6Yg_6$-ESJoaW24? z7_Auu4VK5P#15lhN7KP`bL*qO_Zxbas*trEvxc%~8)vs^T7Kk^gtSXgDHogeDi5%&twu#=VMk7)Pst5?NzVur`Q)F$gW?n*JK^&d&npr_C(Ik)2& zY#u7RpG5YRNG-Jy8R>cttzIcK-vpPlN_Z74A9JGR=sWLTD1;8D8Ym03{bbRa55LuF z?~I$RQzDzB5PBcfM#UWRVZ(QPhV|j#8W+cA1qPs3V}?gRfqRiyaf?1>9!vCwrs;s| z6=RFg)`Or?04TggVRB-87g{-gb^$QS{}(cWR*5NQ6?S>?hG$Hx5Whr@coA6Dd36% zkusY#XrDp2o^#BUV&p=Va8{RYw;{jz?#Xg%wp`PPp7NXKF)r1i?WZUE{yd<9&5o6} zeMQ^W8KRk{r?etQtTB5v<#E>tyUx!OU2yS(@Q7zNz3120?5dJC9Ujq%Ckk(>r|*=w z&uq|xdmhN4!{uH3#lRv35x`Cd%kCcTu5_^{n{L^tPPsQP@qF&cmwle*o}q6@h8{QV zS=c|KyLQy_y#rD?^OL4$rm`=udS_8xI#r?fVwzq-Y3%BdqOMS;`dkSo?>wGI-gZw) zBh?!k#%9)>5k~@&<2oOKDKv3zvOF#7q;GU?sRqZN|||n zla0pDn8T6zuu+FwW0BS+`_7M|>znk%%U*7}wP<7n-uD|$>#Yeh08L@;>&no?qFGx| zm&YNS5wU>_-@%>jVYr}UnB41KWE&n>KKSGu_Rd6{ec~Bth!$hlIbe$TR<0Th&29_Z zAZroYQ_lZ00Gcwp(=%P?R(ZeV0!031UcMkag% z?m!JQ5g!b*=bdpG+XOnIZ<`UxAQp0%a^TgRpmE)Yvs0V|zjaVRJ_p8u_+|ZlvpBX1 zM(|5IQ^tXFh+mTPB6>q|lG-B1hFTf&c-URUx0{U3QV@Ttb4LR$HofslVCaQpMi6Up z-7rPu682GmPx{JXzq>jipI}aE1X<=}s&__>m>Il`t81zPJoF~wpHow9%`~`)+i`uy4?WL z{L#({OeagKX4x=<%Fe;T4|Ka585y0VY*cc4=6&wwTFyxy(FiQWBCxiOIJ1|R{>O3vvr(fGDQVjlx!@wwnYJiw<1r8BWu=ae3 z9>hf=pRu*QvvGaE@KKaa{yCpx| zy?HibF2#rri<%6^J3M`O-wcf!w}Wtmi$o~5B?Y#a#wzjh78Q)J;gp1Ewy2g*;$ZO# znWWNxHGo3ND55jRFln4RvYRHX4Kw2+g!OJLe!kC`-#zG6#)a*c+rmR_ZFC&MG4S{? zCHvD=rw<*G9MUeamqGK4XtDkfDlC(9nUP{CyWP&Qoxza65u$^YVWae}YmmZ=%mfL@ zt)n3TufE3nI_1|!BAV{N{15`)>1&*ioFStQIyV`_!`;oe;b?rlcEQ%}bS&`kr*&g| zk#yJ*-}8}!!2$JnBMZ|GbqM`s+DfNszI!Jg-xj={a4dipxalO|4fsq1$ToVu7f6gC zv>4wqo}LvtA~qnRXgJT6kYnIE(gAFJ}Pq9DYEYZ#keLP{?Q2H2&CkVy5jf(l0i%PKw* zpK}NG9Vl89HXLy3uFr9nfM!|3*lL6k(@)+M}9ZU9rANZ7)6$A6n-GYK3Gy z%>1;p>?WI-#rKdYCl|~;=?80}UsMQo*GAx@;+jk&G#AeY2v2buS?Pfj^XVHOYM)qu zhHHGrxAd3tn#KcrS$@s%5FG-?7?G| zv+>6layEoeWSxL?Ud~=aJu$(}lC3tw;Z0 z>il|B_zm3{8tB$@KM*t_lxyyxo;WKOz?lzj;eD>9}2@L}Ws$AQ>UWoMB7)n+xzo+Qy&?=5}{y`mjsies`zfacw>X ze?S(l2U(t`V+c(bt}|@howR^IOjH84<@Gz`NaPeJ7Jy*Wc|99Ibw>TQ-2=CPYJ}=( zUqw!MKoc?H$PYd*p|SZ7dnO8TJ?FC5j0XdpREn6da6T>Gr3 zM4ICb4^82XSkL;6Z@b(YJaIv<=TCZZ;4wpLB-QJ8x698xrs`k_&QA$mYT47V-i{Vm zCfz>SYQlKOc>S_k#&-zl4=R`jO_Op1gnO?`#7dG}2R)VDjJcM@kzVp|3RGoozJx$~Xj|`s-Sg1MNWbrxhOROgJ|-^Rae+a zGfdigBh=Ar{<2JadxFPSYl`tjHZ?zK} zr=lhT@2UgxV`;^gn_3eH=|+~hBLDc@@)tn^N<>W?oGI4 zjXUraT}_ojStSA6O|H_y%=14e;FB0e+H*aky1DYPXyqPEPv$sbV}7g;>)?Ga8KmDe z9XOVMcw{XX5Msf5JL6<~*W-ehQjJlFR50}h9 zyS1nZqi;OYTGJ*FYr>mhsE&TufSV!05~BU5@h6Z=T4f-U(}8%pa!41ciz-JO!ddh< zEZw_NS?5#DNV;Zw!krcylbg1x&pdIf$6}T` zg#zfON|PR@SpVGNR8ABUrbx{DRygvl7>19YX z5Ur4S*&`-HP#3s_;X=I2)_d7UXuOgz=zD$xJs7;80wro-LX_ZqJv5?3HDvsyM{s~c zw;YdA`{-BiXM_coPiOXwKqt=$fE{5u>#ez(5NwN3hEiQTAhjLG{K7Sy=>&{x*p(Jk z&sbfAaV_9&`OCOIL>Sjrd*gJJLUsrHpE%!`!ang9-F~Y0OJ;oS3E!}zs@VcZk@v~o zwVt$NV~Dl-JxY{cDFjZ4^$k5rCFFws9w`|9GTg5=88y-nVB+K>gNPr%bFoZuIXkL^ zis-(M?Cg)032W%@r++YpEDtz5%S5(K_B{pmVb1!j$>_oI>%&mvmDjA~nK}Pp9U#k=va|yopc6ng#i7co)V*@tD-NZS6~#72dCC`nk2d>i8bD z`(C-DI76KTClSA1{+)vm9<^o0s6y+6%T zj$pfHqbC_0AFoMSz~S@h{pH8bwI3RZfm*hCW==qU70VujUb>W6dc)8moEds2?pm3W zWLrV+$l;4=`<s)HZEraUf-{rr}mbqg;-fv>|)9v*w=*GusI~SM6?euwG z8@UwTj=45NbU*nvjcn-PI)9;!mT95&-Ju!~cBQD>*cCtq-itmWghym`T!cQN{0{OxBs6Ic%dwqA4kcs*5ov4+~_K@G9@W#og_=op+ z=UJ}5cr!S$w)VQAt!z;f5+}ZC9>L`}-9Ry{JkNeb0~2Sew?*d4o%iSavt#_uS?GRU z!76#`)|(e+w=;7M;UBKX(igk`;2f(Yq(sZc`ANw}lp80|9&;?NsOyN9<7t08?$NrJ ztNf{VzpZ?t;&bJ6VL}X}!7xQ_hsn`lVOO?8*24YJ@Z3TPZ%5^d`N_#kic(}R7`9km zpyh<6d^vdY>0_y8(dd>#?q&5L&C+v&>r-x!BYJ6E)i+-@cni=6)VxINH73VQC^Br$ znKD}z6zm1toSPA9imWSc;saEaH$}EjYrobIe?P0kqlTem^|&Sm%gW!a=4C<E6=J$wvQ--P6(o0#OcPpbq;Ru`B=W}x% z7oJN8uUrS74zKB!-blGO=|g`fT>dYnkfW)6)hkM2=c{b=Ul69xr|p^DOFWw@w#2Abn!|;|{{7S7pT75k`*K`u|$jdYCAfKJTq-Hq!Rf%^* z`fz7K!i6;r>}jw0J_-nt-MbdcUJUpPGOCYzb8ap z43CYmOzkb4X3dtpNV)?Z-OI-n@#|D8KV9r4IJej`dJ$sg1m~IWy%-hO zm<)6+w5cVsOv=!Ja1y+|zMAM?lxwu3W!e$RVCZxK2D&4sLhfhMQl>{%tqP(0(2oZ0 z=if@SSfby{^VZEU6r`zw>VjlY6J&37CEbo}Sf*vY#BkUB>YFg%NY3eXU+Cp?+1cn5 zG!NzG;G0tTCz_E76nBq8LKXKX;l|S(oEnuQ)@QK9$~U zoi^9|ah6|T7`!m(p7x7rYwOB%(d5bf)lr(ZLdybZTm;9YPbGRC)dg4N5d`+i$P4Jj zK4G(0NWEy}13v9AU=+g8xe)-DnSU4^!un4TJo2)A(P@-fb76<(fUNx`SCr2|Zk&6GZrlc& zesbv}$kD(q{fjN=i_T6JFYU<=wm=?@qUPSR>gwz+)n`!;S-7cm<>u8ceYLs!Xtb6O z;=Oo^i?d_>K=+RFer5Pjg4Pa`>KyImg{Uu2ji@1O`%VS8A2(l-?E2vsk+CQ|t@KL) z4pNl`xQmdwl4~D7tS+83f4yVlID+4+xHP3~!Q94J$Y+l29JQdlwHb7zd#%eWfBVP5 zrImuAWCED2URk`fs&-aDSbCy7u>}tGf@Prm9%Mkz%fMhP;<&}8+t-r<3_cad zYvW_xR}{TX*X;H_4o3wiwi(|>3(MXi0bEw5cqK2<3VnH(U)ty!r8L2 zV^)0?4>!*rmp8LD7OjTa7y5A>hGwN)2QMM*F#uu^$MCJM5kJybgr@<0n%Ue`13ujVI~q-l z+}#OI`N#eB{Qt=$##UY*2Z@itm zZMSz98*W<{F!ssoW4ikL(-S9|ujW!L9YfD<92QNTR7CBiJp@#WPC?5ewRH6guc;re z-FHvu#EQ7HPiQN-)T>|V=TuoTaBwms*X6SU z>X0kGtw1RY62{>xPk4Y|TdDhU-Xv)aU|cHd841u~dzpybeyrIi1PdHg%25Iz06VmQ zq+H?M)tE=44k^L7{I^Ocbh7ZPyf*W+j>~Wkcdb>-MXd@@8j<5DI*_-q)wNkFl7=I* zkos`{Nmz&4Z~SyZ5n-_`z7A+0H{fLkKo-6!Y=Af$ZUPpZ^1}wo?auiCOUDG=rB8fU zusPV7=O5;N+y?GI1-NIjpsS?u@ll}@R$q2(ra{poh~6g!NzdapI|;WV2)k;ErY^DV z@ZQ~L??ORv>Rx=~bw`)a!a2{dB3^H*>NTo@sB{(GthZ3kLhCRN*y-^SV`W?(er2T# zug4-U=H8)6Ux$ZUWHPuaE_5;8VfVgqJ5`@xjUF3E2Ln(>#@af&AJaJsc)wo^UPsH} z?9dyQ;xQx`-=byGa67%EaT?Yg>=cP#(78Q)ZJ-3o+?V+=sZ`~&tJ@Z}z)fhj=gEll z$45HbAEPuk$gX_P(6ynaKe@_SIiZr^C`eAq5iuJ!TeUWfULu&MA^rvno;Q*jLQyF> ziJ|QfD_+PgEUu9}ROr|xmjx;8t|HdrLYkR6DX?<6wylE?kzT-cfX!Q9Fxz>D?tC7E zGOVr|cadvKNg(0@V#8Uw?PC)Oj|0pbheDdVysS!L_;IuBPo;ys@D-di3u)c19r!aB zI~|_~WQ_RsQ991T8|_h5#dDOM4H}Rojl&?Cu{+i&p7+!NDq+&9S4_4#rD*KdlyAl` zQbld7XbM^8Za-b=OgN_ydMt|-mdADCO?)lhoJrvE5H(?x^8>V;HNRoY!#Sp^H8{^^ z1Jj-Fo1*stZp03%V^phAy4%Naux5VhnY(!jmov>!_LOq@uZ>kE z7se405?%+8h-Xy6NaG z-K;^EHDhb8VusC=x>#%9!fI?7O0e=j=R5A>n$j*8HcZQNXvXIqQ95J_;q3S*s2P`~ z{H5Epr(9V7h4n|Lrrd7i0Vp~6{Jh*z_n9=|`i*fLr|lb^RufmqIqu&N9Ne5&t{0k$ zV^Xmul~lvrzMgHk(8ygr$g^1`Y-N(gFt^wz$fy4C+@5i>Wx3BOedFmsU8DDNDa$EGtbD=QwF!`~bg9LJ>(*0O zGD~N_VBxB~K6RzZ{zuK7&r0Fc7T?Wk*3mK%8HNj1cb`7H)0uojn*8bm4RxQn8PW_L zp&Ox}C?G8=eR^Imj=M@VU%Z@K{C<)6p*ci88A_4fCl6fyx~oHkTDh(Pl_gJXzFH6C z)ZbM<&ygc(H~ZZpLr;v*+l2mGOGxC%tr5KnWGr%{d&FehJ-=`Co#mnDD&T)G+4x&`C4vpva{UqWqbKkq7%vN{Ee|U!s=U$Q&%FGd)BD z1?vcbT%0wT2BiWT{%!C-qH%s(&!F=V@z>!c&Wkl8h)ii{ayio6q98}MiBw<{=$uRk zxKTC%bjO}pSlH0USCLT#nK)Jy2#w5-Exl0-ipJ_pbu*u#(B*1@rZy6u4vAsqlcUTz z3P%_~q_6^wv{8xC0d??6e_vY^OHUX4%PN@1LmucucqoXebj<~3-SUuWQPbaPQTf)_ zzvT$=&>#?}54@B2hoZooRQy!XoM``mAyEeshxk)Gv)$nwGx{&Lkk$aaqL!GH<~GWV zp^%LzMD7=lgCGexK5+s&tehAeSg$HP6ss70AXGlw3+XHBfMw9Pk{F~pI~h3*pw3N` zTp3UbiPs3Tmkl=n@#YNecM=favP7AmaC$RB!hc zqaA-ZY@7p$H;bKMbZ3$Yd?WGrdwxTS|eTA>>aa5HUFFt3n}XH(UdeW&Q^ZBtpFF zI`-4H_%kVOM8uX1e%nv0x${?UcMix@6xq}vavo=}ACXt!+pN@VLZ~dV(HcZ#-u$^r zf~i^E-M1v(!K>4qCz-ArQ`DdrR$wXNu<>BW2N8v{s}T<)1kWCZgx*Buxktf%4}0Mb zK=6yR5F??}-9aFJuyniue#pJp%Y=(UmcjbNno|}qhGsxeaNQs?nJ=UO0tQXUl|J)i zB33ZL$rsNEQQ1lkgs7rlehs$&4bYo&ttbgLC(SX3M00ylUb*}3;(W(RX7TsrmN(74 zG$z}5KdwTuSZgL*sFazMD@|~TyaT<;`Yypw?Ji7G&aFX2%|8oIcG`T{2+t2c zv$nZF8`x#Up<^OOH1QsByq{`|o#H(=>H1d|AzBZp ztq;VEQ7Aor3ka8k3D5#U2>u1X4ke0zp+RZ)3?g#dy)v=nq|NJHz|Gnh_s4c@nRVme`n?Q6g0IFU=%_)(D2N?E0{V5~*cnw!rY@ z$KNu8FCY@K4wlGo0#40HZccK`?Xc4Xi~JC zNs2*;$iw92RFx#DLR*6j*^#TgG?&;Gn0Hq7aCowdA9vfsw6x1_W^h(Me_)ozq}yI@ zN$^;C!yw(+%kF5wW`xunLUmO~14>1syK&6Qe%gQZ`dhT1@DcrJqyJlC zwrw}Ty6v56UrCxasm8x(Apc}bq!9v=s|wPUIU|p_uh(30-8iC_@^noxC1br41kjP( zfJtfz35mCC_D{;ax6y6KvD0^FOUwcA+yGotQXGcfL*wjV*NYQ4!on_B?tDZC9%@=% zZFr)`Elv472#+po936GliA(1v8f}U z(}}rlf@pSNFhap$(xncmM?uSNn0la@AoWFpzlbsf2$010x+xXXYh!#0(+4>EMSWK* zPKcDp;sro0J^9kb@JWL}_aelf{RTN1j{&;ce!{hxrC`ss%jT=H-@=%({~vd69Ts)h zb`1|8f;38qq#z*Oor(fVOLs{~BhqQmDM*)yfOHStB@M#Rjes(AH{Tx6`@XK{Ilklj zj`z>k|3GHuH#^R~&ULPJZj8)xirEzcP`ArNh*5&#n6HmcD@4I6vz9sOpNB9GfeEB?IUt*g1QBLWblkMo5U z{eSlqcv8e;RHrb9UOgL`L}LNcR@?3+^`Y5QyUjixVXrfB2h3mH#PajaRo=`vKG^ZL zIzr(>e1%WOc&lH)PPVr8nlGkF$&6LTFfb|uz{e4jNIM~FL*mQ`nGWX7c-`wH<KC^=!q;wV^Uo>~j%3dgAzg%TjTYBQCvazoeyDRly_W;C`toj^-^L!~ z3e~%Bv9+yULCET{TB1+khX111$4iai>N-Rb*f4An7=Q|JiFYyj*YndKV(>=b|8G`2etCkHB@!<$q-7e`d!K zu~$Z;GOqWEF=C)%bY)Vgc`yhr$eu-hZJexVs&qZ4F3=IocO&;AEbz#8$?gJi&H0lW zg!Dzd`{_*~C^KvnKSi_PIjeG7jGLwn(>eDX2Pj%;e`xXwjSTkdhQ{TT@=xDkx2{n zPc{vV&6&C;0N6rr^P2y-1=fV!-aw%fy?l%?&nmB#zloSa$v2i)Lnfg7%0rcki{TZz zYj)8*KGeb8y>$@gkE82nG}H5u2xhXjEVw^Ot-PFsKjXH)iV9?4Wa~TFnC8sw`>nHR z_rn_k*Ounz89XeqX)FPR*!q4*5k@{9*l2c<@bpPW!FOoq_(t&RREU)r3&ZTl;_c`i z<5RNTgTV0Re0PJT$n34X5X+s?wn+Q1wJH17)4bzjiyUtbC4ZWU5;-tbi-`03jKc`? zdS|=_V-=IQ4Wa=R0;tj4YMZ=@c8wdzVR(>^Q22Y$FV`6Em#UO5FLSoL;$<(}8HLV! z>ki$cMIzm8rP|Vk*iLk8?DAZFm1g|$QBb6q-)dT~1X{hw2XcN1`cO5`dQc;cIxkjr z82}7so;D9A8*=JID<|W;j&Ij~!~;zXGQlE3P=ivECM%3$vc=ESN5s(O%8A1|5iHoNO~7PtrgjWn|_Vu;{^Raw05phD_z?Qhv{uZqe7~y$AuKp%1QA z=7B-d5mX=1(Ucb&hB^>Xci*c#L2mx=7t)20gU09Sx&OHq)wf zrdni|;x?>%Wwky8fPbqtLFou~4ge=YXAiCM-!3w}#4A+)rbE2FK;H_AKK6E-lEsY2 z`EHU0Ni6EeGA==?ITcUNHA_7+^^x*aV+&}qUu1Q}Vow-uzoXpqjPkbK+S>!jExWqK zAI)`_SPIAm+Z^;NT^DcTP>#_dZ{zhsNfXWIQ4I1$&8zNODs$paslEzCX|Zy{PI7`A$dM^;G}PS{In~_Iu%jdd7R%1C%$Lx99-x& zAKub%AIK-A6Gem3=SFf)OP0RpLc?b2U2iS4+TWRs$HC9lGFxmpJv1LL;0vQ1&u?C6 zZtDsh;6vlhPsBaOyi{91%~9NJsv~`iO zT49=#P{6V;31e7K2F;^H>J}b*y_y@FPo_?)^8?B5H`v1EnQP-q-OLH+#R=CaW4iYl znfcYmmnoqZr>iA|7WYTZe2XkkpYK0?2Xs}<-NbK!+>@_t;w$`#%7g(c#AfD1yExJ# z@uceb;l-Wks%+JcEZC}1eK4DxaHaDdTG7HB=+X$F{V&q}z1x;Bv$kBT?nOAf1W78O z6dioPj!%6wl42b5$&3*)auUKtrk-Dw#*@|K*{*pKZ}lv-d^sge>Wx=Qx#)ZAeG;1k z{hivk2wH#ygn}gMt5abv2!uCXdzEo|4E}!~06iG|k0CnwlFCT}`~Db8CZ8?VJoHHz zhpopbWLj5S0EO@kPw{3Upwr)CsZKgle25OtqJElFhNw$od~{f54M6sN4o5K|!RbDQ z+UkV-WEjeUg>Z7m#GYj|L^G}4ci~xL~48Lk|a~c#?B6*e_F{bAqgZ=QI4d3XpB$rC#5MT8VH@A6y{7DbHOrdMkFckDMu5Y5t4JzHtJbHC_2* z$HjyyIyfgxH24taL%X^3@2j;HN>+>iXORy@zh0LcdVvs*O;$N-l9%mn;SrGIDcZ zo%3uh0*wL(4gFM>{Tw8Il1_aI7(V68ALlDAda?5%8gMQ~0FH^g>9l{C9&ej9g6tT36v#0CJ0Hu7vuzauAfH;^H|B;7}4uU+q zn0%X}JcvJIOz_Z0W1i%K@nK(5uYM)>Z8~XWKKvVcHBLCE&iPXH$h03tCDhItrCNZ# zqQKp`fRr4~+1dFe;k=3e5uSuWzl>{x5v&3f3rS2@n3kh4D8qT4yy|^85KC?S{4~AX7UTO%LDoPfmiam^GRYoBl@xb_w&DLT+ zPlod5YOl?=DCP@Tg6)SgU7BTCsX8_MtNQXD^@UbZ#9^KnoXmm`y?@t z<>P365=-A|zCV0{Ovja?aGq9u1G>(Qx9xHJgX^?<(r%4v5+O62t9&ljC*^?XI-!a} z#mU{SXq{{3B2{f|RSj}k?MMQ;1k*u7S+n(#m}dbbqSsS+ch6BPkmYl1aK7((gn|`I zvY@pjA;hdVMao(|KX-^7GLq9h+pj*mA#0TJ(!(CQMT@ToFL+_p_WJB7&~AQemJZH~ zg$r<)D*O4PBn~EZx$dn~klQq`$8@g9$=M^80Pe;Yd1Wx*4ktgAg4H42T%k)OjftZy z1S_4QKlrqXA?^XCu}kr#ud!P6Gm#CH*Q0V1DTYn=Y?}9El50sQLEJ32bBp-AAB9G#?VPJ38Ue6UjcT0?FeE$MT*j zTlTZ^&laHVAK}>-pkDFINbQW-)3n^EbKBOTgh;KdXu`S1)Kce*j|$n=1OW?83noJC z-Aj(^z^Dl3i`;^L>@G%S4A^I*_Y@BN5W$lP6A(-CN&Es`DtQ@dFGg&4cn&V0L_l%T z645hwhMQb2K?X&R9Ty9!sc`jIt9`FOt(0}; zM`8x%zh4^3;IxV{(}miM5;p|zdQb*zl}QH%8EOCG=J#G!z<)AI+_Ysxcvn4p2K}|~ zZy?*9TcYiUY|yLSw{4=q7wvX|}1u=jEZ$>}uI^1?V#yS{o}bjoZw>`-pf(=ydhdf7}$1nsmJPpx$mPSOa$P!t)Vz z3V)WCO{vw<5{jmj%1Wjhxt@{s9xplCH?Bf}tWb}i`dSI>Uf5#(k}iONy9IF-6rz@s zj5F2arA)&#InCr!kn*-k%#Iv#dHBkfKaJd#TH+dy6t(oMCvkhyF36bL1U3 zQnSV(h)nr_&1-cd7lMXqS29jy_#fscMJBsT=Qe!rk@IJV{i%9KZ)T-lcE+gRu#L^9 zqgUt2yn{5G>b>>|pr7CUUOai?DfnkQCR7)q^=z_rJPlU9^wfKw=2L4e0;ph{+bCc> zpLnNEg_S*Ca+6LF&Zt(uSq~6D(7e@|0O4dLsQ+DF$eqX@k4s+c^$pH%E-aVgXl$lXi? zfY_E&tLzDCxf@I(DbO8wX2@=*?Ex+=!J3cP9F!wHW>qME9$5lV94?%ARen%%NyamN zv;33tV*+1Efsv6rT=+PsgpXet4my~u8tdpcliV|70%D?el@CWuwey6uEQ%KB2QlUR-!;)hX-~w4kw<~&8xg84w2qi zLEj>PP*I}K**m}4^wZUD)C|!=KcHfk?C*tYqi7aS-{9w4`@@rHAWDX+0-9H4;(wx~ z{~~1s$(n!wQVu%fOc+np#l3hx5lo7LAciYCv%M{z7b)gpp#6bmB3&Cd)map&4||1_ z3krnopM4k*Sd8@l6N>>UwoL24sn7pb5WtuJZ*jqA9Z?$Ui=n+<-Tz`KuD}7H5i}8} zPq%b&@-;Jns7J|^xd0x(03o^#u5`IpERHFGZ%)3gc~$YdW<4ONYl!?H>?rTLzHUVa zeljg%2Hyq2m;5COz%T!+X1IQ(&%d6;#~8sPhS(c0{j(GTz6dJ=crC4ebnd$Nc zf9fW{P@-IYrR(P)2uKE#V!TpWI*RzMjxP8*x#8@mQb_-H00x8M&~9x@yvqIm)EVYD z5%X#Ka2)^Y5!eAi*Yg{^?U*chlU{Sp>w=SpCip4(ktW&I;sTG4_?#q!%svW>Ao1B% zLPdnAHwFarf%*8i?NZk`2}HL*O31Vk}CKU?dT}qYeMQh`WOo&3{H*<;E&`0oEVrCYhePMCY^ym zn7=R*Dg%W1BtO~qMZM0<_ua^{#kCV9D+=d39#k)lFS@$#HTCvlA7e(h`{QofZ%;KG z{BrsPfBBDM2M0p3?$@MA{?D)e4~GHbq0|VAoSik>DlE3N0Z(Z;#+rUoA#M5L1`qq zZJklEvJ5?Odh6#P%F)S8;Fbp{EJ;lHbcAor7jKG`E@}Tr!HR!9Hau))+&g8!PnV@Y z5o%-awE!N`)1RwCqDy4aDnj_(}VpWLj)L)!Y*r2h&7 zhg^sf6(i>MVL2gFRKI3kozdd0%9@(R1oQ}?gd|=*K0|$OIOe* zR}n0q-dB>Bo`8HJ;eRV=!Hug-{D(h}KMwJj zXVl_yK%%#5(FRTuAzOSvCSe1|lxpF0wHMO;w6qh5%POwtM^{^qqX10hR}gfCN;`#-cG_MHvJpa z*<*|+6R=AvlnFKRlg9-Hdca}Pm&70Rli^3>bPI|)Yf3v{LaC?r#evh~{y|i`x7kI= z0Ff?)dG_u=`h5z~+Wdwr_f7kU>c3_7%>Z(Q(wF{P6jHiC8SgmUgo(6I?0!EiaO-br zQLg&%!DFBXWC>cCnX6nkEm!rXore0`a)#-RdN^P9gst?Ww013@8U}Ij&;fl~Vh)pf z3iZ~(!C?5?Jn{T8qRYY1w^MWirFZ!>+w&7+w-4kL_vcaUhd#b5^y zi)G2gqMrN7O8|o%EOm*f67)T5qb>fzcc%K~4dLFe-MTTY32k4CLOoRKH1rRSpu50L zbI@j>ZFoFU6+#At_=He;~7dph55k8aml=*h;Hq5_|9 zTaj1q{mCD;TPk0l#Xf_PlI`6S>nHaICITgI-X&Ufm&9H6Q8Wif(gd}f3GtA9@pFw? zqDT8bH{IvQhv*a5*`cUqYMS_T8RYr^=t+5~U8_O<5s3eDNHm&&*k9A5SVD94D+4Nn zL$Cl%`U@@RHrdqHp&Qu=2XZxC7Uk=$sUtMjC(L8DHc0QK*#;V6lg(I$bj_<+qBrG7CXI1mQneG);2xHU)sXY7UP5L*Q1rL%XP;kn+XC-e*xR~md~we0~c)#WGVHhP_~4lPHqZTHXga&ZqCmM=<(fml+b z{3rd-K6L^f3qJMT=V5hfLd|H6S|*(WtJ1_M22%I1p}tqgY8jX*2d_V8?A7M_UtJJt z&>BYzlJ1x-K~?)`+F2gYoV9V}dCMFfwRU{p7F%46E@k|6diMl>KjDqa%5E6R552mA znx7cck7!KiKaG{R25BfU6xyCDJc)nJ!;LZrm~SKTem``=NhNr}me~fKbj01=m^CTI zEv7e?`h<&FdH6tG{}_0%(0-?tNz#q-G?@-3GRETk%Frzxa--9R*aajt+hjzuhb1*xd$YwKxHjqUx*_ zpTO@?6>;d_{0)!Bd>76P7c;{>H{axGiy>n4Ri2Fi zB5SwDDLkI%69Q3si&9RvbBl-K_W1Y;ML*Pz=DEY$Q}>l=lw*=sF|h&X-4?!B|J2S7 z57;mKh~FX|fv}q}G7tFE5**5@4aHQ=znL>)vIee@(_ixvB1pa+0~n9rDNG2|&jA>P zp{`Lt^sC<71Fj&IzV}jnkJ7%FuuGn4rYxQA^U;Ky^d|`3@V?l7<*pBum4gK24J3Zn zq{PP4ISSgWa47bO7Lv#A$1aX*yx~YzebYSIct?Zd7%yyR$-8 zVXOE2oWsLCXVl6MN7vsdN`tWO)%qE`HM=m%Sln+_L+b+37G$72R#&GhW+@$s`!I_< zp##>GLDg-SI6KPy7NZ-Y`%(=Q``f-6PbcelBiM}#F*}!t24eeDIe*ih=BO4{xkht3 z3iei;$&uVP^?1`%u7mFdZ>J>_D!~LSnkrHm5Pw@E<*UZfc$#xSoW2ekXkRUVaiD!- zU-_4G=YNXy?2aHu!+|tojC~6{wG_uQ#JP`Bf9vNc{+96=awl`L(hK5YGW|B1PXgNe zjNs|Ig}j?hN0W}GtJC^e>w5k;-9h4Jqm2>iPu|erZ1L05+Nbt*vvk8}CD9nr-9W-V zIc-OEq^()+jgIi4(U!w`YhcnszfkaK6KFzHtpcifjA-~mqTZ~pfWF2kolVBqf3qo~XVpAOu0Cu z5dWZc$A(jTU;dW;Kn=M`MXUHzEvwPQ=QncNfFh@WeKAruXPs$$59Lfl2GmycjR7f99|aAROFl3j(<#WoQv$R} zI6450A&6%cG<}(f+q!e;V$-Ki))(_cYdrboz4u%%0jyo^2dq%S0!z(71<};S8 z5S-&J`9DFUlNU(lA-l2UDl3BBmY?pLRh^gF2)%UO*a3p%Hm8~UzlNawWvulD39h1JIp^^2rb>_I>Q zK760a%u>rA7eK1j$$VB}5fOa)utY(6?G)cN!ckJ&*8cD_qj%~$l>7Z0z}y|i@M=Wj zDvcThN=!1{N234ojIV&Kb1iAHUF{ktLw#C+&Z@>p_lQA#W(Lo!YMD*vo}Qk4n?R9c za~>$qe=@WW(>cB`F&z(7~__7aD1nuvFEKBKrk#N*eE?t|d zomHMlHS{~)Q8K#LQq}iZwxs3--#hHD+HXc76$#q{;dXUc#u)GJ?$+TQhtGaqSSQ%=ehV3lxg74SCmP?(HGxs%NfQ3ljY9ppfpA@yLSKjW zg#9Zh1iu##DBQE7p_?8@TNK`8$Jh+YnNlAYmVk1Ie2iMH(lRs{%Q`kszz37@m9{vvj3A{hPHGgVa;#;Hb#)3Ps`ZAo=6?EOm9nSi5JlIMmfGE*`9RYm{2~ zVeq%;Gbm?BjiwksLDx~Vf_dNq%>c}`8BwPRS>v(e1WM9}ebMKr+HKxT2=&wI+DtyF zSJMfsRL+7FOfEnP#kvlz#nuj&8`yJjcY0Y!?W<{z!3ws4JV#t0+#)0HOBJu)(|j@= z&rzwsHC5w)-98aTW%#0OlN--BI9h$3e}D5nv%aw0e^vD4LqJ7ef>6;%Z6Q?jx}X=x z6<4%P3)9i+Ofa*jnYZ4Jf)YjNxN_EGl*gq*kVsg;RdG;4S8uf8@H|G*I8$hOxK# zkxAix2-z@E{=|Xg400QXg|twe+^{Gw^(6r?L}_ndOJ0i#0XcJeQu;5w&KNiZit2v; z5pj&YXk11rVj-oBGJ(IF0*<;r>iS)flk}2q<6LDbc32?S4dcK8>XQG&JD~5@0`gSZ z;8I||0s-)5ajQy+=_i+))z+cgO{$|*HN*N7J-^u*-P#q=G({@p)BX< zS$pu$_oGI{)UV*b!Dk@44Op3~0ChcR?LjojG257w>bJ0+epd=Cg}Xc3g10#6^79Rw5*Kv_`PCpCs|yfObRo!I>D^sP|csA%Ju^7XO_3*tZo3 zM5$lxsJLA8P}bvaK6yCQK`;8l?7LsY0B$7^F3&W0lP*)}CiW#Ccdv0rT=re{4+tQ4 zc=t{(zHW9H8){XGbxGGd77)vpV%XZqn{#W>x}r}yhVb6KATuN{ixAL^>&bZI4>apn zra3I&LLYHK4(FLVOuxRFqe!QPcJXU1N3Q~`!mmvjm4lliy4BJq889?dh$;wyyqNA_c!V^U^i= zp|aY1Td??GUsz5kVi3L;wOjys#P4QGyN$J3A|Z_(aoIQ$ONExqWThHCQkS;<3vTCk zdPXfd0u~#sPZ}+3ddhT!h3l*+6ca5|X^nf_|59`Th#EG>Vvp9<3Z_D|QR~R#vrHGy zyNyu3`XweKsIj?WWMDRtX}#~6;Hz0Yxj#AhP&o~{6OFV53(lI~`299Fwr>IS=)xy+ zr?P#!fagxS_`^3Uj@Mb}Hc*YWph4$Hdb^L9ZSg;`o}yAr)dYea^+m02#C$N5Sg#(d zb^iV2Jj3H+(0eGC-Q}Rn@aV7*C8yv!qULNf2#rNgaxnbUp}=!sZF$*GcEM5eU|kzQ zJlK73U6`N8r?HN)?o=;&AX@8Pm{c~wvLdmZSEZ^=IYpeCK3m5n*x)EBLnN=LEn`MF z@4mE(t@==~QeC-xLPM|QGQMQFxu(K)OEHFif~t^A0JD+a%LDYccnl;{+g)CfZI!$?07v9%mFwk88b6SZyAjo}>Dy?A zbmHT95{?qVYOE&gzKZ58Hp~7rewSw{gU7T4*@4v|GrZ{esciYI&S}z#L}cinb}N-d zAUqLM=r|$tk>bn<1#&Q2s&1A~KA4j2D^kT#Ht&u3p6@2%@8^m7@W2!6#gT!?F0XE&Nc zK5CT-7<@mu(6^qun(~E~@$+G0=ShS{J<&MCW#h+r09)6P?j#>e6VT_wEC&8q{bAZb zW_ADlkB0C~OfD-xrv!fFmFf2F#=Q?T2X!^We%Vgl5a?**Gimw>S~k}P9T64@a0v!S zdQPL#IK&&G$0nGx(U)4;NLMO&VSRQqT~XpwGglY@BJCB@jEV?Ehsp zB74vxRqeF5oUQrB0_N4ZjEDyxz$E$Ky~%rRHpoLL*1JA4hanRjrGo$IyLo*=5FtsS z`)mfWkH}e}BrE1+uF*GZSA9|OGt_q`O;g+LqEftRrEpG}J$fNrO}c9qDmgSFvSo4( z)o8HbJy@UcA;EN_(mQ*?U+K&R^z5XjJOFl~a=bpHo~m+0O@9Kha_QABQ4kr;bzl)D zHhcqQ`D+5Zi@7)`5j;{9gj*>>yIESd1Pv#&fFl5@23mOA`#>svS(i$E9tS>p8)4m7 zUtd}xr?JMNgpgVumS!oid+jiO#lOEb5-PUXn@GiNJ&0Y{N;Ll7jVI7fzJwzkLE8c| zVH}SmdO={Rop(ZoMBXq?^Qs9~1oKl1Zi^Ar*w&y~(6AtIt0D~jt(4*7#PL|M>#fZG zOI6f!p#2C1e9SkN+@Y`C?`+m{gLa)}TPx$|&%Q^lbA=>V%A$_8pp}g@(DtKT)IMgp zQRfy|<6Mvp(a<|}DtPbRXv~f8wX-^&?Wa{-utchGqg_j{y`5RyWBieGe4

OHTSk zeMH1z!g~J$s4nhhslk}2*|G3Tp+E7iSPlexD^mM|*sI(Gan=d7$Jc%J(M6y!-{j~k zG$u}f*jq788*fDthn>2r2|r78^hjj(2t2vVuk2e7^53Qeoj?+@vwI>!qpfj)-7wb% z&d;CB&bq9_Y7Qybpi6Rj4>o4%{lI_Eg-bLbcYmV`_za6o6w^$BvZL&lr>clPuem>vLcv?)Te^(D^v3?Wzn8%;xa`EGR zrPq>}hgh}c2Tk%jrhd^x%x3~8xQSuAMkXaw8{=acm>Pw_4xd2>hR;hpPh6Jq3wAKk zfZLPD6BL@$ew$eJG*?J;(^juXY0D-iQVOH+V^B7Fe7K^Z9{w+jBWSRt^VJ2lm|R!M zk!~FH+}T>8bRTxof7*KRTBstw=$V-YfGBesh(dy;@Yubp?ZwVAhmN#GTKxQtdFrwS z>Q`ZZ=f#BMIAKfRqnQmlT{xR56g&WlHE^G>8;r>kJk-HJ@W2(G-i;#oV_{^&lCzIU z%A&J1NHe-sr23{%+cfj-RRqBBD{G5f(yRTJKTlJ*8}W>bH)lP;|G@gKKd#MFCP;x+ zDZ_*lnGY!a2YK}%W;BTMGTs%&Ip z)tDw-qs`6Z#sf>h`VQSD#+PK{o}klCBvI1>XbbDEsIrx+=VL3)=@xVMH%M^R;+^AD zCEa0>bN^o87RunzI}><+^zN;IzJuP!TN-jzYt!X=-Z$-3Cg|0L5-q)7PIUAeT@5Bc zm}CeHY#fTFeVr+e;3KL|9LhN5)drgdj(GZpQu%Jyt!!=wi|>z~qM;bTvB+f7e7F!5 zNIxYb(yKitj*z=CS4eFPJkBd26GTZ}VhNdjYH$kVQH=aiD`&hmGpdvj^=~h5^}V^s z01GbOHT+vu{NelNJ(809%0SPF`dhBNx3m`FXEQV={swuyQ<-v?|8t8 z2xb*(S>n--X?Pe6wz}}$28jjp7NuJ>zBaNm;rvpcHB+$-j9FCyWNm|H0|e(Iu<9=3 zoZgr}oezv;Ok3apGXq9Xw?)S6K}G!mQJQ-&uL$g592nA?66dHKaY}SxaJV~KX%>!z zPXn9jd94&-8n{uxiJTRBSU|64J1b>wj!x8jjFVn}`P6!vr9u33ag3tUW&TN=n{ii9 z(T_L3OO2WpENOa@zjnSz(}ZsTOALK8T;wB$hTJioeB@MKtXiW*pUD?~OV{t;#@UG# z6D{-ue?+SS)s5I)$nGq#kz8T^6@*R8!x&sAiQrRwT#*uQG|^guxdsL3KUDSc_pceFUMIV_*!6kHHSDqJ3g|Hd zWq=iVmrUlbp3LAW;%r-*O%*D8`2NMhW;f{8kbV)i#&#jf@_yo`oKU*qko?J&vn{U5 zj((~wrfes2WmOa}t&r6PmdwF48|9sq!tFrwBJ;Wb+q!$fuokWet)$-5-6?y1w_ob} z9w;jpuMl!hMCa8TSeNEq7@cyi`Vs0%rBtx7n`9Mw==(<`n!Ts0BP zW~m2_T`;O%RJxy9;P+940W^4?%;_GSAjL(8+nVhi{;k4M+nrLh5(PMG(m~ZRzo^`U zW|7dV2W!&2&m?Fp%%gE2RTc>KCWzyY(_o@ZUN4Iu{@scH_H>X*A}mdYo44uZ{c?B` z3??(%q-oXYRSIEPHBNc$nA^QaZlQv6-_yul*4z~?rO%FT!N!d_*<%_25Dl4NgjNUB z>w)Y8)yYPm!3${zM}OENj44(=*P#zlewc13khB}HM0krp#`5i*p&nRgo(Qlxnp%Ff z>coGdk}*II_V$`(@gZ*Rr@tyLu_+6#!9P-CZyi6d_U8s$ zY8VORnJzRhjz{)o?sDbTojf*~Z4%3F09VVqYI+3zkhlRA*gmi>8iQ8kdMy>PPJPnm zz1)N+G{)cY4p`K6@^6aHrCy2^UCJ@8g6%bVCJz2-f>1}N-qywS67-8Q{)V0l9U%k+wnouwZmsz9 z=MnY2ABySeK>vmrA8OOP!RiT@%jaV|hxBd3|aXE*{cQ=so-2tB_z;l)R0Wk}h;}}E3 zSc-HdwWN8WN@r!^ojOuO)S@@}YNhdl#4e}H8DnJ!LjpBBk-6{TJR3e=XqoOmizHas z>_J*x(?8C5eoQOtx1aVY$-Q5EkJfggkQ#yxtccAnyBsvMq3bkZe%TM+DdO%g^+wBNHEmvw)nKw` z6_i^yD#X8SnnwI150VI0&fCZso-EALW0cm`%BvJ=3#xb;TvJoia#$Z6l_V?8*(uBU z(e=PP8P3$%H)A<%NgpNF?=>2k9kyOEW?ov5YDEbVvZzqQsDKOOfxdgZp8Ou>d~{*w zj~@5-hL=w&MVHB=#DUGQ3knn~^FuG!^Tc2m?a>bk)QZ)0}PX3LAsV8SB^Q z*srgJE2|OgK^_0m9GbLR^y;-IR{cOQ7Pljj;r|r(SHj;TwR{*u3ZJ- zIZ~ul)JC^M$8^)=;)_ia(;Tzuy$+m79v?XfGpySGKGd^s)?(gBM!Nl@0LThY^ z*}r1g;u#8i!w49DXk_Vu_Gp3`x{+Tyd+q@=`Jzs-%*FNC&?3pU)kOtVl&nF#w^6p2 zx{69$r9KUygT$NJ+qWBbPliTvrhk!1=?~Sa@|8vM_?0(rJ7&{C^9MpVI6Tj*4XjEF z<-Q1t_I|m&u=_V>YJCdc&)$A`+zj!CX>4;*5RJ_(+kr9~C#!S%7mkHONOl{wzDmG= zUCL`WQAX0UH>d^MYP}=gLYK_$JaTuc#(7i_b?;gHL_u1T%xL0|?2b1W=DMUV@EFH5 zF}Jn*sK~%_ZRdlFmv}8LPlgS#G7}kw_h9q#(~;bT@HtbzM^l zs&V4EwG5YcIHdnCBQE|KD2;H+@D1IsHwGy(xHp2QHD9&R@$13wds>%y+V@OQi`4c; zVn$6nV`N4SMd&Oq>3CpuTA;h%Zv)tT=kWg0>x2CixrqP*Kb(Kdc9|+m=>ci+!)hlvPU*dNN&Z~|0V#kZ z{8hEBdNrtcV~9udgqbAI9DhIB-a{71t0BpfiM-WM{k7f-BQ1}5VK5(NkO$2L=9Q841 zK4NDc#^nAK@md1#XZ~0E!fUW|{fr0(by#xh+CcYC0r6p>B-*lXKLdLW8Mj26A0k=? zAh_7YGumq<*Yz5E%g+cZqG33~HHeBRRV(7%uUvjxpy`Hz_v-G{_0B;E^%Tk~uMK+$ znihQeW5k#G6+-KNZQr>Bu&XB*s2jHs7Z{`t?lfM%6wy>e@8;6{-w8oHHy)zd|NH+m z``Vy7|8fz}wR zt8czXXR*KTz8uNd`(P|}sGY`T)){EQlSr)@q{8pGfF8q=5(XbFpmI#WG9Srdk~}}t zG{?cXnyBj`Lg?xd3kCDL@HOS#3hoP-)1Y6u#enCGU`vyWO%)Imexn8vD-LvT8qKCy zKUAzFxX|Im?6@R_Sk|O$?B5&7)9{f%bNXTtN*9nyg+$bw2rB+#Y=Y2#-0eX{1>O*? zmAwf6@3^O;H7e}85H^Hg+{@@v=}4aB+umF*$Eg|E&R(%U=&@(k<8SNM-8-`bHp$16 z^5-0TlT8$L{FYGRZ<-afd;5F^`fcsrY6j=C-8vLBN?#t_-TFa)w6M4$A{d*VoVqrk zz$k?wdN2%Qisd+~0rv)bOrpH`y&ZIhCqRjIVC{TbIX}Oxl&i-?@KEFS_JZ&H(YAyJ z)SQu3hktSB?w~;IWGF@ndAhsxNp2$8*tGq-SBJV!7V-s-56e1e00$Y#(=x#J0Zw{9 z)@l9bfX!LH8gm}IP(c=Uue}O7U}UN$by^?$0GwfS;Le{T1BZOjejNy}8#AW9?=L zOLZuAIK7z_x?>8la*rg|zr5WLb|j`#?ZUcDg%gICtO(UhcUt!E@^$D)^r@Bg)q40M zkIakY1Q;4U=E{=6NGb@DL(oF|%76EpazPHVMEkFWM=9txEFgL7t6Rek8keK5m63F^ zwIt5FLI}G}<2O~;g>YND#8wPO4?@4Glq2ocWOz}QPwy4?P3yHz#T(mQoW0_<`T?q> z*L>nz#YhIL@#Go7j=Psjw6V0)psxs35&$E!@Ae&jj5^P4m#t7YLlGA#z@_MJ|-8X2OIv@(Iiuec1} zpW$U7<=Dt+JGFJ*@$WUR=COpyjYtUj$ida)R{g87C-1A~l<~8Q8L%YqA*mPWaGx&q z=t9cjB7rYEm7n>xcrnLu;f&JS^?b~Qtae%g?`v3#|A9fu8M@O-AIolnjw?o*{Ci%| z8vSj&7n|+H7htJUUn$w6F_r@!QVd#psn)U?AIaws|BzK2SO=>%X({5i(2b?M40Adr->mu5_#aX$XRaNYjCF9-^QM!LQEq^E>R&0$ zeH9~JRH+nE4Ax%~#hXZuo_IdKm<=UW=dfQptaOC8r!#!cKz12cxa1(x>ePlBk;^8t zb4SW)XjC3wxG(LX?z#lNP>o>qG>vyPEAhZ(VCLFaiBfc~bnR0i6dRwu(RhqOAXR*u zki+^1`tH;ng;t5tnL@*nPal=~<`!U0^G*LeB<2vUFGX9I(ymrgqb;&1rcb#PFkbNv ziEhSZ|D1U9E7(50-f3)*qQBOcmXpvfp^PML=%FPfOZ(y-?bjcl@6m;x&YaD0D`q~w zQ1uG=0Vz;#s3`W>esgCeCRc;B>NRm|oPGnhDsJ5gE^k_jUoFgn7vYB}elxGKdqi+- z4^R5w!7_R9DZ-j^q(H|Yhx7!?V^k%RBDy0-`fjX3U%s-ql(b1g(XIjv8UvlkH;F9r9uAdo?HYSDC ztiI2M3Rw2X$mPzzQZ4XHQGjj+Q#O$NEb6><`$?QAt1dS-+TrGtmP~S!lV;@?k3{-^ zS0$C(j`qpPcgUj56^Q7!k=?q)U7My;eo-N5nEg0K^i}1Qv>keAq9dj_u@b02o5p}JW}ba^76 zvqn&CfsxIJB$4xqjuA*-3=JEjnvGBAqPI&PyiG>!@n9t3N9I@0HG^m<4mPHP+IH#F z>;_*CjQ&0}n6?7@hL(xOvz=L^)}UnHMeylZ!%!3^ zWfQCT+nO!ELr6-WIv1Su;<& z4*it&C05k_a8mLlFND`{t@U zZRWu$+@c!4rHXN_z!+nNUsQ=0?*=p_yvl^ZmgN$e8|) z@HIknVZ;M>JogR1$kf5MBrND-P3Kw$~&JO*z3%f`RlU zp;ZzB2E9l1lxAE=MHl(3y3mCEBPddlm%@*D`91fKHZd4;0$h@v*wG&w(A-yh&;J$N zA0mRg$%C=$BL7yVj{>s6dGR;LQ_=A9U2ve9CTLH}Ni+onw7w`78WEnVnv@xGl$k*q1sz# zvz|*Z3OF*FnXLf4q?J`T!cfcwG~0DG6>&kqLe4fqo^_?cO~93zgWETe+2J%oVTM!` zrk}|yAY1Bnw92B~nWqhE85zixxb>GO`OzCLd+AQC5(}-*B7~n>lvxeQWcpC}?V2g1eNquLSIkDT zt3PWqsb19@ahL$NL-aXnWzLscofvCGeq|_f+|_~`gD~wYk6-KB^86~iwlp`(x^GU` zSZ1c!uv%7=i&jp`7O&*3_p#zlLdH}n7uql#+^o#jkKNaAhco%?qqV**N!irMo)dLH zsVpCl0hUqCvJ*5>^ip6TI%U-r-rPHJp;Bg?RD;xD_<*|@mdfN}aGvC1<5GNf ztTAIhJ9X|gTpNXisUWTSS`Lj;QWu2v|07p%p+@Y$gQgyI>RDAsC|UAbN!yg649+QR zpK@E61rOJ(YoTu41Fl6fPD_aP$U!fwB^vy2Wb0_#{^uNprUQS4X5sWuS^A z@9W}Z?0)dD{`K!ct>&qJo|1SNh|I;1U>xmg{S*;zX+n}1RRuNm%cGACvs;w=jShxq z?Q|Kjt@yCzNV$I_AZc`lJY`%KH~Tu*obDOt9*s`D2gNd> zu6vEzN@|(5Bm3`otVZ@d+~M~c9lSd5HBmHj@4nIc6$-z60`6>KH(YZzoi3G>6Ozb% zWF~hCtG-!OK(gUiHThNg>WjTI;KGQms(X!G0vAVU(5E zxY}&AVXVw6!*T74n>*~=5VC;Dy%yI6k7t?R`xs#ld$pXn(l&i}%?3(msWdU&5;POl z*?PDEx?IokmG585^SoCHcWVwSz|}<=n}%p)u{w{?$>^sK-K>*5?H*oU?7(U&w~TFT z5V_rrTdn15<-YqirKLpF@lAnT^2BzT$~2rO6RCDa*wB?Zr+8;%7o?W7or_o5u}>i? zGA-o5QH-2iKPz68hL3lq#v7NA5f)8~f5))1nc>15p2DCC)sjUwYnWpD1BX@fokToWzumfgV>y^fFQ3dG(lEjd z-N<^RcJ8+xA*d^Pt18UkZC;|K<}4p5WSG{Ct7H!);%T&$0xs=y!J7Uz=~uU!Sr_6| zXw1IH^;U=3fh#m#_Krg{cMP*snBamXF^)#M$=%R-EmhF6NaBvpOr`(@gAFg5;e_u1pL!Rybce z_i*gW&^lD_#TThG@28ZsAN)$aq-okEboOIzKLz9q$9>#ul^yZSQqhS&V;eAiG%Ar=8O&?W!pVrj;miDDFVSt5$*8uls0~dURew>3`0~cxzsq4x)q~xIZk7CWy%pbW#F}hre_L0{I{s{yY-(u>yCT zfJfp7dbJ)*z~lViK%#&V;qHj8`xkGf0-{y=Uy2hCaV^dJt7~a)0ETIf>OZYXL0>6i zq<;?m0%&wPSNW$2>{~^|W?7}_xln2h2XAp6(64;W2}*&VL>3nM{x!r1#nyeUPiyE` zZ#agK2QGoJK0+8O`^{Yqt`FW4z*@1r-?97GOK&4)=RY?NxgeUq(y(rTK9nwqhOBp3 zHm;k$UVv#^Q}FzM7<=omsM_`mbO-~K5CoJq0BMnK1O=6n?h?tNyF&p%N>sX(lUQYppve{_l5t0@Wc!-ID;S1zxxZ z1Nee-xzK|PGr;d{iq9K|5SmnCKOqb%?z3IZXGX=ernwH(_(ZaxBJEEg8trd#s&lWY zn-EEA$oIed^h2sl6}ftmg;N^6-?P)BIr7BqrM*`m-JNcqyVC3Gnp>aoBJyy!&Yl>E z87Xp5b3g78J_F++Va7E=c78ZOk6!lw@#tcoup0r%v>Dh{em332t+YpxI$2HXuP#C< z+N8;38{0vI^9W7;CQ#J5Z#1x{R_(d$T51+RifJ2e&PWqNdlz1*vyfvlHvaozdHi>Z-Yl1fVs9 zSj^F0kl~xcrJkxvr`6&r{J65Ew+f=(q$-@_UK5dJCG~DKRNbRc*8J$v-O>Q~%?HMh zw;9}D{3;Be6R0vd9G=cXy-YyO9PYq%D{Luxo5J$*&CsY9cGP-xJO*FCEli+fuTnj@ zF=1M;dc%FzWn#_QA>Zs+aG=J%oZ9{#!$F)%R?j1rEI8Wr>vQDl})z8#sw^1v+af*tCSDUE~I}1V_Bm6=_ z^+%yren12NeRA4`&vEOXYC+5!X|kRwA&X~KY)iam;aS9zHz+^?<%5EzJMS#>IU=D?^y3J)iag&q&Gz5hGGuAlvmN~90q5<&x zLWj{kcP>Ok=5RkMdLOybQ=Kk{r7!Fiy~aM>lU`zDbIJznUCDuIJ`ZZ72CX-cJw*QW z{oM~JlbMM+_b<=0@D{BW8KFfaF!x!3o!>VAhA7oOU{>VuJg|GVFTuECdj{2jPWhy@x@6Y>*-S(|b5jv2hZ^CNrO1 zEGKlOVo}Mf6+mWP%~3S#;nX8&pu$2CZOLmpOx?-C{={trw1A{rp~Eb-Q{=x*KU1oF z%WS782Nxi2mjEb)X}{tKC;f4tc3Yy=;)`xg>~Gexte1T>WgsA>%!&tXp*G!WESibi zR@>h$m1)wtzWvB|cd(r~s`J~p`)N*GF~PIarVF@{;!U^HIqcinzsCuO*Vf-8lGgWH z`HsP&!}XY{+*&$t0+*~ikwaxLC=K^uF{xm>B3Yzm70E06^Sw5u>Jymth)1upmNuSF z&92wDa-mM0U9VeR>GA;0wsH7}B6*+ryc&a{Fjo~QlGp`SZ3gP|JttgN1WHRE3RmxVNicbdKA0Up`aul@%A{<%LctA)BVUUL`6M}9 z-+pb1#@LMY^U!_Rv>eVQ#$Oq?%8%6IwSKf!TH2hr{4&1QB);5(ye*u{PnW^%PC)wY zfeBZD-yl$>ER>$8I`e*;&Tg9nPD4JwXL8ljY3ed!6D6n2Q+~O7 z^w$!nY%?;uEUtZrjOG%-ig^Cmg}lL??g{B~l*LF!bd@ja`*d9droNUS5~f~&Vh|L1 z?R}ydP*+pV=h>jyJ-uKrD~jc|L&3r;d_-eDXuY~&Efp%zb*C;9DC3A*-0#ev?0Rpn zjn(@<-Wo}Z_k^wFa6C7arOma2(OR#R9a-vwR&Ig96xj#O}iFR zZE~>bwMe6+@Wd++@yuY{?%DU})fSEk!uH}Bd3EMqd~6!k*;yMhANd#soZYXbdL4!z zJwN^_6YQal86`{fK>NU&_DkE(=U?6{Ut1>VDc1rNPMf3eUc|IgB(UXWi=1y<2@AtJ zuDKdTus3(F_QJS8gd8JatDa`>=ijpDRf|cDy%A zhj1FXMECSVhL|OeRCl7V*(*!#L_zzj-N~Y%0g8E^;^Ngi-t5uOs)@ee`3(AZHb`^p z;S7A~?~@s-lgC5{$O^_jTRwKL(CC>Bu1C%ocfTigq$uYmnGNIV7?@^0O?VAx!YS`% zF!GzqF0ytfl(&~ocOP1N;|(YEtAOMrOqM8+k3vdq@UuWmNx%l6-btBMH9c%oz=|>> zev=BY6Z!!?#^nOZOj-1<1*cMx;8tmw4N9OFt{9$9a&Q>?DOn*3HasyRLhFDCkWv|D zkEh@W>yV|HOC#6Jj4LDX*&(;#XfXQllSVa`UjL11)NC3n!_<$?u_!;*<4ux_g5u7TzS;I<%(tn{kd=> zS5T>zg{^d($f>LIx$U}BG&~#r!pp$7U5wp$n1tv-*>6=9B?Ax!6(*8KV|5cm(t6~XayQ6#10Xu%tvdDFo zNh6gj0tcT9Rx}O{*YLwRQKH7Hjh#OlVzLGCNFBR$@{a244sMrQXx~u@D=>I3W1pjR zY}^q+7M%80jTeuk{n`Bpy^Nz;heaWtC#G2igJRhL`awJ&v6~p(N&0Bm?sb~G!3RT_ zJr0iiVv1K>cC3@GQ<4$#?Ly5Cecd>l8b+ZiYZQx4%V<6@P>2^ZXy2FW$6PjUP|Xl2 zcORn9+ILf~%Q5gt7ngqRemt$4X*t-aBjI$6q1UD~G$PAl5EZbi<6ajKWC;4BFd?lb z)@oLZxQ7eW%j)?HQoH<~Dq`gFUS7yRN9UIl!Wt z>&lderK_dG`T2`&Opg-FYwkab%rX{d8JCIN{xD8+#fLk@fv0s>i4eXqJRAFPU&Wa8 zC$27%2Pu-d9IH80T9ySv^ibykHSwxiKfPyYeX;`r!x>D=6EH0!c+0iUrsd=MlAh9! z54zlf9;nEqm$x8ZOm~}v)z_;aCo+iCD!yP&Q6Uj`K0Tmd0zwhemD=U;inW&tbij2loZSe&NRuXZ8BhlBr<;t1ph)i2t7%o099Tl zNh84hB@aV13SF9BE>|I)C~5m8*tt}+!+5OAI#kqEwGqm>igmVW{gaipg90i!A_jVl znFjfC4O#uz%NcDy+pAtaRT6)lSLa^wV)N+fd=)5t&xE^vkntb>xG5p4UI6bzz{k3) zcXQd%R_+#wyCq(;_=@AGTFhad!v6RRpa7wtA?7sqEBkaUx653#JDOc5@H%C6KYrdf zC!VnUQD7!$_N!CnInxw-3@f$@=kU6oNLX(03JBVy=ICx3W`j0R?XMw`=22T<}6(0n5n1$ znTR$017m(POTyML^6U6=C!%!_)x85(8z-kRv9xns`SnBTjx<6WVfNNCaqOV8_ovom z&a4-@ov#wt2NdJr*&*v4zng)Mq0Eck^{{Gw!T$TmBtA%|q`a*8PRE>wCQq!Bj23-C zG!&nXU0OUu1Vh`&tYrEi%$lS-STF=sFU?sm;Z{9e&X+m1Zg9238}-)Zjmoy_kcCB| zewVx$UlQ$na82P@)^aevA~s#zWE{u}EyJ5d`#{UrTac5E-=%1sGjIGi;An1}hI+S5 z!NCNpC!OCsue@nto?P`QBXIM|8|!`6wNw^wn5=wjNi(KA-tt}+{DS$T+O3wxw=I+`jZX z9Sp~)fMc2w-qd3v<0JvsZ?iCU=DxSm&q28(^(~+Dg^h%psV_(SJ|KAZQ*`pb7K6+v zZpW&Os+$K5D*ClGwrR# zd616k&rsAFdaGSyXOO0Jn6h5Z@Y8%NGkweDRXzp_M!6BCgU$lT44(+zNx1l;JHdff zN$uE~w7z;NN3~w?N=jl`^B`X}u+C&E49fM-gP*b`i0Wqhvd8Qs0@qR6Ww>$lVzDTv z-0s6n%kOh5WBIy<-g%E56Zdb!5bN{l9jYa&4T4vEbV7?fYf71lq$VF0%Pq~4G?w@9 z=YXv%D5a%C3&7KL(f1WzfX1?YtIM6c$7@UWsHWpfRu>w(M?XGAs5|b)MzLr-;|x8$ zs!=h1a%hX4$7mJix9^#B!@+9N9EE$ATX zjHM5Dtx+Q_u!|e1hjb^11lXhG;(7o=8@w1Ze6jIx?|H$r`vc($uY82{m@dM5GF+BI zRit$x$agh(XFVVvLkQHbG~OJ4++mc4h zQ^4UV;h;al3hhm{5YLRV0XFQ3yRQiLEOtj%i}O7z12A9o^H#F7QzNqG`##zBj^R|g zZo8$ijlOwLK9epNk71v-^c+hQ#J%Wk<#4r5f#hnd6swzbwWedaoG0q{e<+8W-O$!U z6o3lA6$pJmR1UU9b#S%qz3h_Gq2bc0mNBkYN7KwCj=+%wPw3{#y-y`wj`od15g;#( zVdRwlUfLm0R(9<0t==@+6J~*A!%KGveRdq7o!Nje<8@?GNyJAMk7zB=%F=NSJU_|x zBN&8NylXU1v&~{1L})&=h&9$?u6ES4(lJqO-UZ^XCytI1oP&j)b0zGDR-!y0i8+A_a^A}%JVET$LcUwNw)@_kSR4|d>allBoK9}=9T!2%{JQ@)Xeao<%dTQa~ z9<%$XTq@1TH4DnbvZlV6{5n0dV67YMO_f#xgN6C7IV$#1wjp6%O~-=cxHonZ6m%FA zE;N>VhP-?GMA|Vg$7#82^05e+L*nSH6oI%k>_=k3hM|%eJUz7pK33gbpuDm~J~}sN zf7|j zYQkx=8=JxF;Sj<2D*A+jDpQV<2YwUverLoHPv2yyaMdJCwPkOGcUUGp556YJeW8?E z?G*F{8@i&@M!;DR)QyTeM){{UP*sQLRe0$N)EL~UJ~2;g8*DJzO>U0D)%VmMQf1Ss zC*`r$G{OW7jo`HyQ~v_FAW#iJeJsjlQIhXTI*C8>tPPIa(Tug7DB9^&9xGqn{nj|q zpjdXX*g-;9gV;SgHe1(Z^UKhe#IZuIQIN(8{X)ojWBoNY8}4Cd^f_1FxfMfAIsjnBde13)MvY*+n47pc5xvpkPig2t zC4e{>d5HlNOn!j9n0P%OdMCxqw#sW=0k2k`E;H}HBJt)bPiC=ow8mbAByoTK6;$_= z1>rl2Es?1(z8qzhAv+=sF3-nQBwvNiGi%aZ$FT56ATy7;g<{^RAsaH@QY#npOb_)`}bkCOZ})ywT-9i#6q6}hztnW2rML8+@H;zO7-_f12S zbij99?8OYN-M%JCFc8d`-&Rn!+2 z#tud7NJB>Sw*NDG$I3iYwJ-|HA2Ju^{BYQGd)*rG+Wlp@&sat=WW6O}Vwk&X&dBSw z&_zuCR_FcRHj&2Z;d>d;^(B52-;5+RQFQiqG>1_;7au)5noWxf4^%#DhGH#GcU-zf zoKwh^w6LTs`*vz{JTr`XB6>>swKKjV%F-NHRq)Ic>!K}kgdAz`G~Ofb;E_L~7ky|Ac2kQ2Uc3_>9aQgT4@|PKe!>I#Ey&6g)9Bk5;Yt4?CRz}K3R+h301eCxbN40_eVogn<>fHKm?90Yv{ch(Dnk;#MH*ylKsph-q|Xz^NrbUVF{mV!zejexZxrM2@OiIF=O32?L+R ze4jq7>1LtK!>||`8nm}<;>qwFc&8%qQfm2D+wbAE`Uwhe!7<_JRWCOQCDJUR3Ym18 zL_?Qt?!!ddoH0s;5-WHn{LAtIX4beHR`kVEH)0Hk=4%h4kEHlbk~1Wpa38|XyhDb? zak29NsPKtOfCuW)B9=gWIUP2v%ov&bGUI6QS#gYo0C&D>(Zx~HFRO3yGAqaShBxr$ z>=#hluCcrP^7Xf$e7ql|{F$38WfKh$-^=p^4fS1$VBg-If1RJ;XT|Ojc`nUj2HLWMUpCI#)Qi-+zs6G*BkNNpJtgWia@C_7M`33q+Oep|d z1MrU-9=Uz9`W7J8!n9KaZjs`Z71gSCPFK~3l7N*>gXMdK15je>wm&ZO_eeWlmi&D|Hd%(Qjt=47p9RTJ* z5#?(WR*elBfw!G-QX@942wFo>LNI^lh}|r}&-t8QLYxZ;0QhumhEa~fPYN>8TI5r| zk8p@n#Xi5EKQie_5JOnF+8k`TCWtt)Xs(td$RlGVSO7j5G;40VXO%X)c)d%lKwF$C zw)miZFgRQVM#id(vHA6DNHeJDC+OBb{g5tRzEWkoyYdvE#-{)?I<{46d&G<6*et&@ zsdN1~csI59=Wj+GVn+YtI{Rm?|9k!t-hk+Z+jHz;WV)xApxXzh@ZkMDWRG4;y|7jU zx4Ulb@5AWtG5j^IYxAMY`v+|VMNT{7FWcW=(eo0O-4%U(_vw3DgOt0*$)eF+k`uL$ z-#x}O7{{j?3*DPs2?LZ?x6)Hf9DSWcy=3$X@&}*$As=vh2mn>R_-)zbBVzi~lhn4= z{_VlseoXm2H#{6VZG>$2am#64VCtJa&G1YNo0)Gv2uZ2Kz&_ciCfZplhcOP=6s3#9 z^5W~lE^3DsI326(pY2HChK*Nf?mT=X4k{FcL-{)54{-cgmBdWC6NuUC4oDuDbQ$To zJr!SDuXiH`)%m6DkgqmP7N&M3r z)a5Qvr0}t@P$qlx;w|m@SGQ`T`k zr>E?p%MmvH&q0_y;B8gfZZiQMXZZarFX2jW22%EfR{}elcbw~Q-{4;%wus*7`*0l1 znzXicbPb0Xh=ePkdPag@nWZ4XqZnlt(f&LhPbg>(cRLgt zK|a6s&dHmrvVz-XaC>#Kh6)&3eH(asCzqs=GxOvXnJyZ0C|~#MC$*ZXgc|~*M0W!l zE1FTaUEdDG2-=tWedFy-k(Dir8JT|Krw@L~;-irHo=&xAo=}!eGzt|2?*9X%i zUh&1(Noq;66`d+5la?XOGFbnlCJ>97JaypOBDoi^H@W0HF39`&*-qA z?&M#+ClrX&Al^x;rQb)(r@B^{Q=abS(}_dL4^ZCd*Vpc8=O&o-XcBZVNp}BR3d9$UhC-(Sa{ShORnx#IbzVO~uxL0e^xYm86qvt{%8-!iR z-(noGnum3~)zeF5%+x!~Ol>!nyzmR96T3Fwz?glGKX==oyA3#CgL<7*W}U~s*ctbx zsKA<^u>GZQzUm9HJ&0gK@L~M%WLILP^gf3rKZ6K2?FL(Shvnmo#&S1vPtG4PH583pp*mPZX>{-x;0`r}ev%R3V z^{SKW2`GB(*5fk#sEWc>we%0=Gj>% zbT+%(@{AYIW0!yv#a;Rk&+A*7<=NcvpH8=F49aEJg}i?A&QCv>GoPBPsBZ$*c7xGh z$%z*utm_7va*vm-U@NAliGn7VaOw2-9BevUCD zyy3>1aly2=PCrwbG!rHHt$m61m-@*zS_MUC3bCtR>n^e=QBO)0c)eDr@q&lkJfP#T z)?QU0T+GHCR-mo1nQ$o~%`e%u9xv|B!PqEg^^RREh-Qxsx;EMH25_egzHhh|vu(Y; zhc#s=wL7~Vo(!Ho@h+UUt?g5^;LLFEF+O{uP%-`1o8-_I3=JulLK&Sr_&owQZ@#zJ z*{w5t>fLs3i|qH@(B|_dEofKn=e5?ATH;z%c%Ct*TVhw={OqOloTCFJs)w3_HkvPf zS{_3chE_^OoyY6)P!^if@^4**0JI;Qu#6C&O@;krhM)PKME@NjXcq&)Cvt3iH5Q=W1B3}H*kmT&0PeGB_#LlnY^I38q z+k+IwARC1{?pF5}>^ZSXR!1iMlHo_VLuo|ZK)AHfcWm@AN1bANdV09Z^MladO}S*D zrD7t-pUjC+NLK;uUeR#a%F>ZW2`bbHwzA?u!aO8Z(~b3#3B@&BZz&AS6@p}9?WcCg zMyCPmLE&QK=1fa7Iyo#r^?>czkm*DhZHjA#l+BE)XI{n?-Cl7g!X%4+w|5W3MV1J* za_mNMfThm%_k4Z_1{db=U%2VX@B2gwn0tR)>v6Pg6j=}{N26F8%O64 z3I*BD>6PbgxBr|Y;vj~zF%4yKKX;u7hn}8zm>1J9BR6{SQ!};T{#RFGj-^a_PkTk` zNX%X+rj_NZhZ-{j&otmgZ#fz0KjA9{LLg8cv{ z|IK}c%cO_0IYPqS)6D}KX!8W0Sr8IM z{^xY{y$3!b_4FIn`K-7M{mQ_+v>m;arW{Pz~l$H2D|%7U!^*uv5dKo>f^T;hFw zBNdFk=*;%O+2^Jr#6DeW9IbytKockO_|aYQ6LC75?L)^EWX&L`K)ai)N+DKU(+GlW z_ouAJ>fDiLA6``@pE%7Q{Kb|Z1I)FRdXl}fXZD?f^qE42Em4ZJ3(!tC{;sGAy4FlC zBnM2PYo85bm@!zMcMK1G_~`4S{S_o5U{2izXVewQPhY+vDeyJgd8UrH#?`Wuvh0@Z z=vC0>G1{+(pwMvHO~_?~b`&KYbB|-f1ts;XUFkdJY-&?ok@H@ITsSJnOR@&b5{DSa5%}Xf>FaLt^>DId3Q>+yN?O%g| z#PJcXPs7P?y6ts)6Z!9_ao0#M`1vs28BO}T>*h4%L|36UYg{;vgK{3`i#i_ zng}mIR?8RQoMO>*OBM{Xoyo{nr%aA|$VSr**vApMeoZK8{xr{~hCKRM+ zFX+M7jJ-V0J7auH5~ID;2o&Cdy&blTeXc^2nMK^NdaqfgnKvy`uLAvyJ*NP-D4t12 z+9q2`l0h!vs=@U10;2x*1LO8El4%2-uwkpT43HN&aJWrAsC#+(+ ze-EwkdSD5rhX) zl*1KRWb8?gfsb`AmK#XUY9D-})28a_BQQB0cx}|4&}@|aVjXuT_H2!wuiRIl&xuW8 z7$x#tw3rXYZ4A#oV(*ZbHGh6xqfqGXq;Di-k{8239>@)>_N~NGEEa4A!L9G~3Dex09-t?hq>?hI^IQV&9z~v^rbe zash-34bh&b`;;ol;)~=)dVf8<2h>r6H(!Y9Y#!{ZH8Nlnrb|PIg=JfP1m@I?BFXdG zmvq~y{i|BMMS9t|7R)}5Zv~GH?l~uY4J2c~dTOQl+MBn4`wfdmHPhRud+Y^uIEj60 z3)6M;We`g%t9bAlHFbbzO~njI6w+{Ke7%iafs-c;oUzybbH-vD!g445C%wvoZ+R6T zbDEyKB#@y*Qvn>p2y?2RA$%s34EZDOD*;qitv#4{zJlCl&yYRDAy^j`>qqE7J>Na{ zovgPj(=pZehSqsOM0i_AL}Tlt%jQl&r}9tFnn6jRv)de( z-?rVJcN9X_+?qf;aM}IpG{Kzj&kRdZsvztAWP1rGRkSgC&i>lqCJRPkiI;gLl}hrf zxkJOhgMlLkFo1u_!D&^ornnU8p6hx2jumxJP`)|#ur6S=&}3--35E^0=sP+sE6|fd zE1T@=Pe9=8j5*>Y>X^lgb7-v9FzkXa^O}c)yeXFE13A~CDT1iRbymQ9$eRf0@1{!o z>T8T$UgB1Q88~dow07Qn-HY^cD{O!2B++HP%StjIlr|Smze)kQ+XIl!zQEK+qTqvY zf2+Dln=ePR71px_gWl?(O5e|$luL3XmJxX(#^e)DIbq_d^SYI)x7w5DuFS{b!(ple ztBD$W?(KE83G!Dj=$NN!3MRfal8Hu8@a!3Ez{ADO(SVC%4+f9hhiYdVgr5q#KYF4` z?#YyEe_kIqiX4RvU60N3l5YrC>~g?WV8mlDQL9jaga0ZEcpVJ6ME!Hifc zW~UA~47~=mu}5x}AXhIg*HFyWI$nqYvZKeqA-%|S0y~^zAM+oq=|}1rKIyZL{Yn^? zuU&U72?$B580`+;$I4+%btzH_Nh5H|#~pAN!6vMMHML&v(LDApt6vcQJ$rh2T+sPf zkDq0F*a%LXA*exRP zNfk&+g$cpN+$|98fjaE@V6I((77+Y*{fUQGiI;W3$!mo?W2K|r{>uSU00{h}6pK6>%4IOd>m-b@_IBThKGdr)op=-il!5Tb z&9_D+n;k#Hf6aH6?k~NGtz?%jP{*7~d#a@WJ<#>?6$0Kim*w;@KCcy@18X#FSg~wn z<$R|Bd^KXDu(i)lkpw3<}XF?7%pj$(o;eWqNhl2`@r^h%w(b6!6mG9M#B z(AwZSlOm}!sqACT*ay`>BW--l5$lhS7w_cSyUb!YPIdVi`X< zCW0=Q2>;#hMb6hRV}eNX;r%BTx6k&o8@^(odLsGb4Ub{5>oJps_9=s8h;Ep4bocCm zNT>mhq+m#iaj--JS8#k1%f-@rh>;R@$?8IHQ2M}#c^t^OZ>ntF(bzss5=>dper{}BVQ zC%fMotl+mJZ3HLp{iW!hA~^aYT+E{8Vj(r}ZG+pT=O#RoDUY7o?Eka^TlQ;@%n1uQ zI4JMmDzI`SFK_7=)@@1;M%`bSlLO!mB_x`@4us~JNz&q@ArF*#0BAHj93ch2Fl0H5 z59}gF#=h}8etiy)22~yHdPSYgwi@r#Bpjc_o&H3TD(?$6@_z-~m5_T#ewYyY%sr&^ zf(JRwJzg%;S%i+H;o7xpiaSUgpjC=|xBsO=zIhhx61remSLmKlLk$gqCV~z4Yz(8Q zLRyG_-`rpWl&BDAU2pO?1Z!a}oZHH0!QY<@WdraB3Q@)$=QbMH^^iY`fq}sn<-A+P z(wB2Ze_Bog4Y##A$^KW1TNh>^`YE5#KGa=N%$F7vZ7H`gI^qj%XjZJVs(hX6y53KN zzjSl^_15KoU8D%a`!rbF8)rY9BJ=%(<8z$S`~jd&97N!6+!3jVTBm_14D=_n=`3=L zjXpDggR6no zdz+)ib|K(he4fipkc-PfUP00EWCMryZsn7LXYp*r)jO_OIZ-_=)q}xfLbpE^z>!&^ z0bYW`i*OdyAQfo%6uym;1LsmORQ#*xna!iZ%3_^ZQi`T7(C>PCz*lJPo+hCh4d=Oi z`x-=f5lUjT$9Hg9;=g*Zl3_HO@AB-Ij1PpxXuY&6&s0_5xSY!dvSzK>7SV zN|*q|Rv{$PCNEICctz%>9GCsdU2se(6ROdy+4+ z8e3dg`m;p*s;%piRtL#m(LzoR4X#4}_3T+dU;ZD6{(lS5pfcpXLlN>Zt6zlvlflMj z>ZQ(Ea>6}Oumj>*Lm-}=5X!jqVo0~Z;4m$@(=fK=*Q0Tu!DLd=1Y$zMU?5=>4M^{J zcLBKUw>EhSJ}(5y%kYA@b|mOha?2zilh1x0+gO&uj44QXL=8E$lAsxZ1fl85NYw?P zkNnm}F?8Sk(3+1KRqU7ihG6CW5>gfj=o8CCkCRTtyuJ%sdFA%gtlB+6FFj2LPA67h zzq)d^#k_kSKAB-CN*TelAQM|`I6n}9z@55W042I@$(*8Ex90GRxq#lu z43L8Gd)?fPIH*v~M8F5Afqi$=i6`=#O-hKYK*h0k>@a?zprx&GPTP;z8kqTzI}vcz z{0e;B<}!S2L1R%m&C)gBYofK*a_LPu7xRGQ$n<8VfZdZ@^xdt$57b1Zj<9h$UDa3; zLxFm?+-a}349!vh;q7FN^ZF;xXcJpdPt0l*v9xmdSr3dFzmgLQ3OKvC96cdv3_JAJ z$%k9lUOL)Q_=cxTB0q7k`q? zN_tZLxDWIJrhQvxS1Go_B((5^-FMuzm}oQNY3kZlaznMNuDs|h(2LtBfcL0zNO|25 zEOe$YAB2Yj|GK9(LfWrpBp|L@zIMHjk)rVQkNW&>v)Fc5AOjw*cBL{$nE8UjNgUxY zkym#zH$Ff|AE@+H&voft1%lz0MI|ZQ)lm|Q@lsM?Q_}ho6rki@&4LCO&!%b!;B>=f zlzLd2+|p4j{y08^nLwNV>xLB2!nsD|Qu^m_&N4J^7kGxz)KlN>zc$H@(yBHk$6uQa z0y=~^vlQp`(V6mOW@2RPqrs?41k`*~@_7M+PUe9HpIyr0WZ&I=YIjs;71RM69*5ID ze0BK^J~?9f_gGk~GI~B2Q$2Zkvr`M0F5OKKaK-k#SpO!AH~@0hxv_^-%XqJYb<%Q$ zzImjI_vkp28|H*KeQf7(_7bOC{^eCM1sYFV?>1vlfpiF)!RMx}qC1y<)l6Tn!r1H$ z;pTt2n+R$fw}pf#Ykmou-m0qF0&1N&6lm2)&r8Z9@%DPwoM4Xvi{1k)Py*cj7>z!I zGXI=)VJ8Hf6q8XwaxJOK@|Q%J{0~K6?BsV#5)zMr2?sYhVrp zF#y2f(o~!E1dr_Cu3G-YuyU&AS9!~D)%)Qz!q{Z&2@gAX>e7~+YO-c(Jr;#d4&YhX zHH@~4GyL{f@qTa`cQQTPU1&obmE~g88ugQ>MYg}w==bup_(J8MU)_JDz|(-GFTKa& zl|3GvV@Cov+0wHo{NCN`x*Yt@YiVV&3T1SL85VMFoOK}zMT#Vl$^UUC>2nNftYTe=@Q(^6j^gLcm`JNbb(9tRi~KP@*Z-JOy);e)Ivd^& zRfh--W3J&r4Yzic?AL%B9J7u()Nae~>3BeY#pa1|6{AK*HVf~2G9?E5;FgH#tiQ0A zdsA8<1ghVe>pi=nM$t3h!Vt^x`W{2!zHAIUX;#SiqZi1@Zm_}U1xDLH7ML;7&UfB0 z@;n5ZHiMSpkxH-QXK64cK}8iz{f^Dx>+Io}M76T^xr7anY->SoyuMhLl@fBQh|4q(I> zPS20cuzkiXI}v&%@yX~}GrbjFsn0n9nLTxfu~{%SmRoLlhk1AER;rb6%SV{2AUC+G z>EGb5lu`?}mtSXHb`a`^+*4vpv@!)sDY;(zwXR)kWJA-=hf#9*IfB=E5xiF5$@J?| zUkd$jgTtl}Kz)MfS(6?NA^|G$&M_A_BrOO`gmveM=Hk075_@cJZX44`9q%FB8%1f@ z1@fG49W?EG4+S=iHk~`T!7ii_UH%9bHOyilH`rsH0{^(%xP~9s$8l*vLG-w7B_pyH z2qq#lo(4#|=>Yer*w{x6(F>~LR@(jX(7d;>GG$T8i1Vv%?y|4Wx7lW>iHGs&`Of@| zXJVav*rmu?rw1shcWfV8Is7`0*Up1D-{%nB1~>J)$XNvLQ+HMm3pxozlpOje|;#_3^?%)A3 z7#@`-r8K*lqYI>tmQp4!oM*Dyu|JnLPi5R~_MtA0KkzN||15Dao<~ryLUmRh zQ@_l1;FM)#%Vkb@svunCV&lnslV;Q4iT=02|BB zalDvd!M~OAV(wy$(8-%NT`d4iLIu1Z?=v0&a!{f3x++UlIz28K>m|?euT`^M@c-IB z89%+!kzuJ`VSFpBorwPbWMNX`6svj_1i2cWNrx2qla+_Vmj^&OpHV5Zd*61=jP0^>yV%DZ6du=I?NiEghKuL(7zhS@#J+toMftOY3oRt_@b}^ z2n!4jfMZG}fCYb=eRjx6Hrnc%xSpDq_;suzq4 z*POIu0LH||^y{{Ky}Db|yLnpk^cTn9w^w)7U8)T}wyV?3SaEV%>!MKAz|uE49$IqN zq2SM0$FPi7RZ5Ox^1w@)m5!`b#n7PM9+bL+rLXs6tMWWzvc&Md)QCfL_fbc=u#seh z%5V)lxOwd{FRYukBb3Xu?`geSD3A0<;0D#?ap@;a{ZutNih_t$r5^BFCtRR@?r+$I5U{5W|>asseB*iRn_E}&EnvmkT?}o3w1D3;A zV1>oEq6B!v1NoITQ9iU>zP&X^r@vd(z@Bs*T1lhF&oFKeYO zBQ8s}4zg9F4|E)vvT_FrT~1bPfgC}=f&8UT{~FNt8y{Ew-nx^KiBU*B8PVK)i9j1P z1tw9F&sf(jF~>NG*vVb30KwVp;b6zUVbk4ZrEzp^5WweKF2-Vx4c^fHKs%|3v66i{ z;X+_Kg{(OhY-Mdw_Ro|~#*v=$M3hV-ZyaxX-_;HmYDudQ@%e{%4o)q-EBl-!mWSn48kS(VDAndA1T(o!VU1DOJ;oCPEtZF4f9ZG+eyE z%g%qDA+M!?Y%1Fp)}XmEh93Of688vXuO6mD!W{r|s>0y7##C`5u{<^Lqnjmbc2 zIn|eTpcJmq6)%inp?NWq0vt;xvj@!sJPv|qM+c?(Y>(b)U=1sR{uBkOC5&HZ+u8t) z8-n0LMMv6R^S=g1N&LP>MA!AIew(ry{QrByLFHSJy#B9!2PA!CcW#+&o&$|;A)0a> zdhhvXMV&`?W&G%_WV(1z)}P<`p8%i43bi}~6MVmb`l}S(Hx#3ouPNlu%R{Ai z4(}JS{IQ7nheeDNMk7coUD?UiVPo?zCZ;xY`jAP+dP3D*!aeX8+v+$>!WjwS0ERaV zjXSpx_Pv4oZ4lJ{EdJE~{(wcVbO9>yli-%%IS7OgzP()xfn=cK_lE}yRbS6b^RDMc zn&;pU?*Evl{DqSIg`I%M_J$yif6PJuEZS43HlHlq18Vc&PU<1RL{N)57Sk5GpM(bN z%h3JM{@;i=piFz_a^RN&0HWuVD!UNPRLm4y7Qf5ZsNm1YpIw*`(9);A`0Wdw@%5#(8KSL}o2_EVDRXV|Q&K(a#GvNon z`wJxk*QkYB6LowNqC7_y{!I5r;H&ShUXMS=jQ(OkBtg*JqhR#8cdaN$9sbMNIZrfw zPQklG_avE|!&Z~ffG4ts%lPuKgDKdphlve686g*D6E!UJNyNoy_GAKbYn}mKOxsO? zz+lhau0dk7RA^pNaNhQ}dVSO6-hSI{hg1E&D|7W1`uPg;{#_CMuah+46=>dJ#Acq) zlY-0G&tPL25~4DIaQc?Czz2BW82|(ZaU#6=Na(lcu4l;csx+WC|2TAk+TPS}!=P0P zOan=~G#y)j6Jc<9@SISplo#ZB95eed@A(yedy;Pbv3;LPM~@C^Gsbhkd5FH zd!JIPvFX^jB!ISd-zo_dxC@Qqb@{y4uB);o6A$Kdc%3}G*}vn>+A{sRCXu>hT%=~S zXIG%o`?S8_zN$azhOJ+qYC%;w_zklP`0e=m_zhH=ZHrz4h2O0ihjA)l+^lsqhu&3NtGmYaGkgZ;)>n zo{b~5b+|k@g_QFVmtD6=9)bYIX6ctgt}X{bG+R=z8E@^@A&7z{cPS631rgi_3CV)? z4k04Wb(V{ZJJ;f8K~D1-|J5oglve~A>&@}N842Oz$CBNat?2I#=e=Nc^3s`dxP}TC_I@z?%>H6Ag^~UV0u^^|=kdrbtCdRhP)q4ptxomI zUHJ&2H)-OIE$?G183t1|%>W@7iioxm<7omGS0PUaOI^U^`Q_O4j_>IMN=<*0^t!X_fW*q?tDa+>eN0am!zyThil2@c()ES*U1A>+-b zBDu`$aLoIrCuQM)beG>Y)}PiDDA^@2`~9n3ol?rSq6ic;ZS74hD^4qUwh;X`ck zLjudvpn`Z!RLFP4WIXQiOfk{OuN@V0u7`au8Et0vZ$T_nqNgMk0_&qiIDmJ-fkFf0 z>nuePsw~UXo(Y{sDaTUJ*@kCVoxwl$=VNc(i!d!eudz9gFZ3(orn9zyUJ$~3*hH9q zVjENMH*|+K)^=Cc_?dsOF&`wJqK-w4>;ZgcFK4&D-WgKNF)S}wt%%R0RbNeBMMXsP zEP^5YC-Cd@W%n{wN&?W=^JeUTU3_Ygm0DYahe>4?riodv{|l^ zrB?qq2pt{dx~-?X@s5lD^kR{|Ce`eSw!LVDwd_9Nhn5Luw}^6UjjScm<54<}-!ZMf=PG|V=dmnOcl3nmcvN@to^oZH z8x9>GUrZSFNZZ~M6>{Er!C zYf52fD&dONaY45^d_24oQ{FoCa`$TdJbj_p30->PYGm*D-sg$!))AAME_N0V7{=ww z$1ot)xM53lSqz3rxWR#e-6(>fy@!vD`4om>Lt93_go$xkjJEJXEfp--DxGO(ptOg9 zpY{WxX?F+*=|cC##&P!s8BljnZp!dWJlG;rK)q*Qm@`=(>Qozg^6Q!zk&v}+gWv1! z%jRRbuW{dk*OP-ewfMms74cozbU9;=^j4N%WF5NUv-osJH2ZUD>O7Ayi8&oXL{?#` z-QXctFxCW@S%1!P>*muQZgCWTPq152i5F0{-#&}Ven6@46PZ4eYLlm*;9!Fs4h#KOA|Aw9sw{45J>E0O9;|lupyFLMP6Vh1Eu6QX+>7Pu~qnmf89rmZ>g14UB7%rKFml`!c7;KHq8AQPf`Dl1YGa3M; zb*#`**2p14XQ!PlRStdCB^Z$x*WtPNkvu}P)J0=Ft8+E<9`|8v$#JXOqoiH02u9D) z!`xwtvK;J2Vwbe4T1wj>J+F85=E_OHut{?)b#HCF$kcmP_}4R}MzYkJFCLuq=i_=B zcLERT@eU&8_Is!1+!R@4f;?N-d%HzA@u`LvmAy-1Fh`eKNK#MOca%L}>&XQ26F=p4 z`CTkiWI+X!_KwKfChI&i#V3MhBoEGHNy`RxFOOVUf@@Y+Nl!1I z*Tkab?i!w260hW3>Ci>Dx!x;2p`(ql=(Y)ar0#fVl1c;vek`(U;iON5rp%L>an(bA zlBXGs9Q}W&d&_{R+AeH#KokK%L_!(`q(izod*AoES6u5_Yr*v|<*>JGe96#x$tT<&UPsQ#Xmjfw8 z@cO@b{nWfij@s$tegn2kxj2hXJq4!cUgs01b&>0XjCG^^(0qsh-#*Xp*c}3l-_lzB z#Ds(eV*Fu_k&`;#Z(~`{d7fc5$#J`Tj-Fos@cC^Jmx>18TI~FO_X|<|>`FWj!OkI; zor2tm>@t^f8No4^Re1lHZyVK;`WIH%^9^L3l|v2&7s2-gXsTE;Ulo&Lg0RY(W|p-& zvs(ErHeH^$73E=79{stqp zWL_52G5uh5uiMd#bKYaxND{6<8w8bdDh*nH`2!z?#wrutQhNe;lzrc3%~Jc$LnfQl1HFMH zbj0N7bm!TtjT$x%2BMmN|Zj6L_F7Z9P?m5AcAq}oYbs{m3vGGY+1YQ3gd|f zLohzG`$GhlTm=X&CCOV8NYX|nuIvr8CK4b?2lDT*QJPNr5&Fa}e|kOZ@uG;Z3X44B z{@b4kARS+U2uSUiMX1B3%hBv54N^)CT84;+8+=Y~L};AKTyM@746W_H|EWMqB=VZZ znsb7q^-+5Tlf-c+9W5>G1H7KROzwdiGX9kxwC`sI77?$450qx#w=6pWLE5&1IV}eI zWeMecl+5l##cqw8l{uQB)JoU{i?JM@re>vTX^RgLP2V{j^)la_ROO$jn%*!U*wx-6co%A4WsB(* z-o49ThsKFHm$l!skBgNvhwLCxR9EMe=LL$W10G+sUX;w=w_t~?m6`J5*9Ekd<2dE< z{dI(~dqm=Uu@|P%W(rnRb8K*mwV?)hN4d?8EN`U0@`b~kM`RRV31iR2MKFxlzrtqn zN{YDUMoyB_hTUGPT#u82kdGT8JS|Q%R1n(7$qihV#hGXNF^MNV6FblqyEE%yD@E@c zXgBE$ffd}wb+~b59z}t9Y#e^bcn1R=3jWZ{L(hC^9w)EYlR|Kc^qO=xUhw$zZPZg) zc~#hE7TnGs2U3N(Y<>Z$H9~Rd zwKbT%tVk~W|tLE+YO4LC$B5< zj@;G08pI_nrA`mK^$17t$$N&3zw$`x@u(jvb#cnAQ8uhv=if&s^>Qg2F5=QrUvJ`r?`OW7IXTLF z*B}!3^}NBXcw*?_W4=vxq18O=Xs&#)ujLwYPA+@fe3FHZgBNfdJ{Tr5{^AHYO?9ne zk5H~jTCpVaIen4~N-N%sA*#ZapO9^my zM8D~ulE_$sIXPjC(q&UY~@Tp6q(k9;w5BN5p^uwXe=CBiBkTNC4Z7x)8KSk6_a5@ z8QNZ-Fx=x(PBd!~P0qiT3KbBa-it7aWD-Bw^$W>*ekhI?QWUQ^B}+5&O9H|DSxN&C zL*8V2<7G~qO{7-T582$pELC?8o3|Ep?02QQzt|0_w(spzt9l}*Bj83M>b>bt#=9cD zAhHu(n%eBRk?@;RyBhExhp?y{oyf z!T6EV{nEV#5|{0F{lH$VlnRZx4jo8|CzC$T5=#-9(%fmFv7+eE>B;gM9W@KWNCry9 z#>?W3wioTBgh%|Y*r~{;y|h=|&y?Ac5jE!TOZikE%h5kQ3?$L+KZKsT?3nw6RJ>T* z<%WrsrVO+4*Gd)0uke_EQ$><2@HU#|GgX=pzcnqa-Rj`J%NVucb17EjFh_*dwFWa7 z3t9>HK}5qsWy8Yj-m>Li-!9ONU?&|s=?2TOe_X?b{eG@*@D`@+yJhZxYTz60KQ8vr^Iz9xZiLy)nk7=-JAb3ACB$H?+X@Ri$WZYM}bt!xH zjFW2+EokA~T!|5IoCjuVG`m)$EpcqI{R>&RW2K5+SGlCaqCRs>=~k?C!Gd8qH;eO` zX-|x8CIBgR>4vA}I5K-oDaKlGu{~2jr^=H`zlOr>4y0objMR~&ti3g#n{lBG8zbYf zm7O_A`bz z#?IcO%1+zjt6WEA!$9m30)I66@lON&*{Affn(5i2RwTg#Aa+q%(~IchHtU9(!J&N! zA}4F88G{=}iC&duSdgW&9cQsJ>sm>8i@G+>NUcbpD=vAJrq5#vy2KdYS3G3u#WwP( z;fO(lU-~6VSc43&-@F+lD=+<7XaFf-yXckhVnsLd5_if%nn#HHFn@m#>N3#XU2YYI zkgvWVo;32Uw-xu@UHsIKR`R6|;fe@HmfF}QKNL*n+ayC~6-uROx(n@&m-``!_dXd= zb2>U_HCc>xi~@alpTmx4a_{$CU|_#;n~W*?m)#9`ZB^}aZECmsL58j)slK+9$)hV~ zP#12;N_PLi0d&^g@rC(X^zBKttmR@iWAuY~f6=kSngu=Rn5a(*tQAAgc zlp@NJs=$)Pxct4}z?n+)mPkZH$LGX<3<)i|y3P3mf5?a^TPD;|y{H8yy^w2>`Sp4@ z1OY||_N7=pfX~N^tT}xpm}8V*b|!3r%?}qR+i^=aiVtF$1VS)qAZSRSZdbi*UK2dI zJPh05U@#!@%|J{5PRQ-c6XKtOLj&30x$$i4J_ukhKLVG2M7_Gq+m;uo_ncY)@)9LcMt4e`c|vy)04J!J2Zp2KH+$bBZm$~t=;Vv z^GBZUi&lmzXHXk;kAiDPs&UT)8+`X3tc^GPP8ThoTR`hni)vXm({auMvmSa9BvR&- z;hJ5;Zq8z6NuV50x?-6R2D;7lq2a|foxRLqj^qn0_!FQGSZRc9VmryQ283T``?3&(rz`$P{Fe32k1 zY2RNSL#nQMb}iLi;KD$^+#I9b;|i82UR^vLPA^tKuz9*h5+lSGMpgAe>LF51ybRC3 z!i3Axv0>Y9w>3qr=%xJeR=%bn=Y%<)=~@z8Jv%kk=yks@U!A-M+JclM8bk!{VnQ~P2j_`sx>9YONGHnylDyf^L52&89-NIM`uo! zh;$Yrc8wqd%088_&S-g#$A!ZsytrO7R=meS&+cx;qG+^IL%3QOor7Ic}n!$fDHMOjt4B@Ro7-{Hf0e zv$0U`=lM~X)XIDETj)^m9=-gRFWPJf4)uwgTw3DaYVY5=aFQI|0M|eG;;VnDakNW7 zf^y4Go7{`ZgTb46r0?@_mE@<)vz}|`sveLqYObI1yjWGVq@3K2w(MoG^J$y1F?9)j zTO)Vm)KAu=Ge5MJ3=_MC%+Mbj>f78v@)*u+w}LKUOU}!plk&&m+0SG4Jj9Qd?U=Q4 zFyyKD^{#&lAA$>| zhrP=mwtQ~aD4yuLp{FSBw^Lgip1r(8u1}h|DT#^#UCZ^G#2sOgVnVG=qWOB7C6=S7ZDTj>a7D9o zxZ3N0uVF`)foyA-3-Q&29`t_^KI5`X6Kad=XAt<}3x5cSQSLz8X&&P%&H!mFohbFX z^`a$Aes{X(eK)x1Hms zshrh0y4ZRS&)M7y)ND-6PADHP>{!@1$ewBY-Zhm|fK2%EQ5v){Sa5>o@V+tWFigEi z#H(>d$}8s4cUF>hCk}@1G4;%1f4b;mXUiL%ltF z3(QpfXq&@eWuJ;tq$CNOh+PKM|7Fd|W%VC6HO}n$j`fBVpGWX*sdf9E9>1?S2Sa32 z#oG$gybjj;dIIE^QkW8Cviyg_tp{Jqd_3)ft1{Z1KOppCkAJBkwe3fuaS4J~7hb5@FRTjO7=4QL6YMm_SeNte zxKQx%36@hCCvXG7!lQ%`LOQ4 zg^1=UB-G&l&ptZvcVCqYJi?#>Uab#G3xGriD*e_}PS}q1-&h1J#sP4UT#FgH0RRd7 z#75u+kMJYVGW{PvMtM%ZOeHf>XAL6*cuxM-8fw^o>l-|vn1$)S<6W-YWH8FBDCt!) zzIhk|1k%rfDeCU!xBnYW0c(U>QyLTS(+Y8G;X0@dxakbcS-`~i9BPbu5Dtxm99HvO ze(&Fr$v=|pf9ElTf>ZwehbyNRAfueM*FX%0N+wdlTHjy#In7<@=L8TrL26OY*?EL` zT)#y}GP61}v*y=_C{zjba62=`PIZb#5B#7CaKa_E*+;c^{06P&ngjpxlj;HJHsZBk z&2{-S?h4JI@HKiFH&-ukna*(c8HMm;V9-i(C6kWXmX0grKt_%oK8SAyY;OHSaHZ95 zHRsiPps|>7@LB@ zG{z(8)j-%3I!UqVEAq7V4o z($Y66MayHUM5@hHTS*L#5!})qIW;5J5^V^_ykP+j#cXsyi*U&k5vd zWwNUm=moD&G{_&UPbeEgEu1s zVo_~Cd5^!wdVAxu33F~!ncYM+G>&^AwWo&@8yhQYM(X?0M1$}tzb|L>oVM@b17{lV zy1-&4$hli*6~)VogR1I!4A1RDw2)F=%;x=e78}(ZX(pG#FJ+&0C(nIawv*op7%4w& zfC%^mbw)KkR_0}{aXKKehC;~o;m$7|n6)zK#+%$?yJbAw$*lgy4$fJCF#C<+(O(JB zE3^oV=B$prJgZ|-as}-;gB_I@D>rq|hzZLmyr5bM3OXhmWnvABy!gH5G zn~qG#KtBDO-of>mumdmJL!IIVr!9aDowAWa(#+vVV>|1LgC(*@xw>YH_1iiVp6mw6{&HPX)wcXa>Ypm8`&_^8#LjlEmO&9m7WUJ~u zwu#diWljw>1pY{Q-TXUZuEtkLiT|T=qc?+PxrNCce2)T`U=`A+jCESdLxF1&8 zw>IoW@62@Xfis28PeVCXLcdv`3YMxTmCf64Ezq5Xs#|-T%gAnnM}UPN5o_R zyO~NB7=6ud+_$dbzdBa>Zmtdr?l+>Rbpj>5Ejc zV&rEqJk0o!jwbpa%Tv?yyg6=<<(icN5N`umTnw}MIw(xEwww*v*NS^6qz@)?cdIY& zpa;q-9ev;(WPAmVB51P;9NJ-O&8ri*aNV08!pHp80HT+wZzXh_ir^^K>2yY zoNAp(Eq)lv4fe4Y-?Ehf-7lXM%CMppSjbwa#(``V)j)QD4x$z0#uiH0?UqW_m)^jo zdf(5S06(^AQTgeEinRg7LaWocwl_;dh3*O4I)G6y>3#H}CxQC(d)Zx^EW<`W)wSck z6+`8<+AcMaV|y`HXV4iJNs$u^`|&ooT}a|*=k8ChmO-^1j(xjnZ?l%9?Z001eH1jc`jYK1RD`An z$S%n`MM8xJj}l6EboaLDhh7WL&?#oc*m(9%U%$&19wzD*4~Ce_pc29>$zXo;HvLGz zrj6}`?-tb<$w5L~E%+GM7oL93QNLZ|fje0+>WFBV*zaP=m0U@-9B^Ld zIsLwJfn49&=6U1A@7||RJbd|HS60*}seISxI$y$7tU=X$*CV1Dv zp`hNv@6=p?a*k;T1zMQ9A$D}?*qm{*PQ0K@VcCsRz z0I9ZZ(b)aok<$P9DAN3R_RcbydLDo3F!D@BeoMr!yQwz75)k~Bj!4AQLr7jSVk%0B zn*F>NFTO%e{s$GEGnC;K$fK2sy} z4NpTFhK)iso8ez*KV z(ok|u_#j)o!?f2j*@NM3a8uKkbC(0fKqJ6mxWfLNZNr|Nex5}$V4mBG>g96WFFQEN zP4#I0csobAACJf6&qeR~W+7OL<%e*>1<5V?m44&;7k21|nh_oE zu6(7lLi5{%y7A(=#1sh1yw=kueyG^^o&9<vK_mDm~FGzE)#ue>n3#uUZsV11+%B&xPBh+wxvc2uLF#Nj|y(R*Xsf3I<+tNGxOV@&h#c!{n~{9=#1u&_OuWE?80Z+tGwR zd9HC>J2OU1T=0$kf_!w7e>qqvLw|v|MUO)3AZTsqzIP$QMcDgbF}&r0>r-5Dv;ErT z5rv|k--F2M%Wj#E*fs=j^Ymse4y`4WyDi*^7qfUv#+Jb1hDNqOUlk>+=&QqHM!_~& z;RmWEr9ksl1u(QD9bm>nuErelWc-QK?y@Fcw3Z2#t57(ucS!b2Dygz;Q3LT2PdU^J zPm~N;RQqzMb|K`|Mq!GSGsj`GnMV6HfYpIMNg=r1x6>jvgF6%YVqd`Qirg%t^KIC|IBP`zWdh)G4*xt%mAdMg$g;*F7JF9=bpuU=6!AGyP}&sA@CV`v5(cdrBgrh z*MrgEOM~CKmJ7xZTtb$dSOBm!1Wzo23KOrQbnqJ0C{|*^69=KoFI}C4n}_d#P8PRd zB)p2G^$5Tk5sL!w+GXZ47{=)`31a*^1epJ|=m8+!m*OfP`)xpE)3OaCIezP2^Uf82Dil6X(@h+d`oN;msa#$0v(G!K$q!ew5yL8?l z4D#L@64`(-s+{(n9)*SxpX_?C^VZ%YkpT2wF2C~u)!fCx+kTHOPR<+ZP$(NJjS|a| zq?xk7p%hBSyY?8x&;ezPJ;_Pr=ELC?%Vs7W!>P5nI!a(n_J9L0=BS)q{`B?T>$Vmnb zkUOTFugdCw%ceOvi!hX$xFoQ+vqGqL`xv4Gtz5~uvEBgo&9csNU@}3f>93D*rKWI6 z5S^;ND6$^0{bpKk2wqV;H-rK{_(n&!|xY_&1DnbWzu4R-#1Bs1Sf| zbgV5##f0)Ji6|1f$tgXe!l@?YFuzyD6(96jUBIe$9y#xK%4A=g3?(g73T$`D!HeJ% z18p+a{(KFhnrO1yiF4sBssQtGAfjUyG$=#FEY~|dmjk>L9*b4QWhCeZGuPY6#>7Jx z`XGEGn@BFJx5ExleAAW}$cI=nK-z2c3;C#}+iq0Z1TRIL93+v!?!M(vp0G)GVwgg* zz%Q4i?lV?QmsO!JzyfCh1Sqd}g3Gf`W874&TAJxYh_DpSd?;_kudak*PF|bQuu|jS z3XjZSQJUVT>O?WFOGgAr^LIcHOPB$A{cOW&)pa|D>Mjz>W2HV(i7 zVq^#~w-UWd&%%e6hy~E4+Tq^G19_?oWK@@DYafn3X5(gByaK=O{vS(OGpudnGSayA z0Bs^F6J0hN*0 zfoETncz=>`RgT2)6X(Nbc5Gx_9ECiZjd4C(aCYKe(bO+*c=sNU`67d5{6%bG-z?(kr!4p%VMN?8G<6d7+99njV2x++Z21Y=1C?`DN=x89LQ{PKMsk3VorUQT zUyM&dp4#X0a9?yUjNsu)AsQ>}4a4cqo7N-xckC+Vp=5C^_e(>4eK#i?yUYkCEcwl@ z84+=UX8mjnOTn48j<)@#9FR)0|L)T%011gk6hNMqG8y2YmhCW;SoZjI15VDuXcpQG zavn>GnG4@P#Grvq)=O(~yLrPr&==uPR_vV&UkSsD&smD6Nl0dz5MH;;wV=lRXd%V4 z2lTm|nBPGIU8`ON&}T6-NFLE&{Rp`~KbwAB$n^#B;rT|8-X5!m42Mz?(b3`T6|Q<!A8#E-l*OktnctSa>eie8EDAVq%jHven?r2m!lKsN*^MhS*} zQtj=py~6Ns1r~^xqB<*(YMC7cLgJX1?46_x*H|WZ6WMvdTv${mF{FoVlK~p_L1*~6$J~dp1;S!Di4v`5WAR`+Ff`DcVC_? zqI;52W(8rWz!4;ebR2ny4@53XDVPgE9cJTnUZ$Z3QBHID4wNtd-W^U{LCHOl$4aos**~yJOPY3eBlrW zps*d!LVl+GKTLZbU?u^hRChfyMdYON(4T0IrgGiRCFyvfEzMxGlF-iQv0`^AN2&(B z-QPfMWU`*eJl4)?UmHD8Hz0Szr4*~~uaDr{yoDoIi2hAfcqpIuUF?>0{}5bEs??yk z?wXMo@yov4F&s-vtI={RNC#oR$~-_=a+bozc4C7;bz8MTJ}&4vU#Vj!g=Vjk6jt;C zt_OAKFrNTullJa=ikfXfN=WQ1O|?!TlxOcEqSQwO{g5S|ckj0BWC>CT!md?K*d)*s zaPk5=m$JE;)>>iWd?HuuBXbdY{vkWgS438-do=rkoF_#Jr(>X(%hP*kx7Yb-+9Nru z9&PS-8(O&nL@(V~a<4C7*KQQh;Dx>L+>jg41qsM$Mur3Bf&_h-I zbW`Sv9ql*oe{C>R!Hi zs(tbCdGuJ#Nl3Zf!9zry7j1B&NiJw&S6{hoR?lOZE~M~%?0$6xXVQ-3K zd4bEZ-?KwAl`%L=!kuT%9WR-dQ8f4)d|O%`FiGJ>xXhd)L>^JuIR-WkIWCh-uyFc! zk^Y?X)={8YsTVPU#_hR}4$D7~?q^dg1Buu6%0=iaL+zs{s~!5o0r(*;>%Qr)vsqhh z<`KUHoE4^n8L5MQ5a@OVsc1`84i{fk8I*$Ie7W5^&SqVib|M#jQuPmaDJq7FohPhM zO_s3E?bZl;_MwpAprBk{kKw|2IIRVql1s8<4P69YED6yry5e6D-kEL#4d&W= z$Koqip>buVn*H{m%UZ1EkWJpu?c_~(gr&3Vj)tf?7_}Y+(tMVKmHgwiM8#R3D;tw7 z&L)8OpP&1EE@WhyzJtL6*9hkHYXHe5S^GSK>N{~MU+EIJq6is)B%Ts2yuI{86mNo) zTv?>3o-9fy@sm{q;g;MQv5P*r-e8y&wnDPi5#yDka7MiBc8ZM`P?6w0lwhc!+e!O@ z!?5u&+mYR>)2Ulwrz5SOALs%agY7!E5kcmXJk`X}k=qnDfD?IU0AnMM$!$tt(VQ8T zb=Qa*J$*^e_qT_O0oW{)k1b>duGlQ)=u|lVCQ+6FPg@6gT&@|Oe*77J{u632q-8Ud zmw^38OeNQvtPiQcEs?fVC+YU`!^3NSg#r1g`%a4Jod6_7gcGf{8|- zkZ_7WtqZ!pta75KnDPuunBa*^uNaqS?}Y4V6zE9>zRlX!jet~&{cfk{X7t`LjrEEX z#Sx5quFq7F^6AcGD*gu%53Mk@m- zFZpc3t*dNxY$t$opQyGVZJ;<>u5H-zOK+$?(#0>uVj{5ADWQF!vm@rYYPJI(B*uq+ z)4Ds&W{9Fj@Rn$of$AVx1C-WwuJvt09X~1&Q@h4&Go0aJ#;l%Ix?IQA=ff5}u>JY9 zcif+-VtQH&p)J5l%(%Gx{&4Yte$0zk2XG6VcAmfQO1sKn$2$==F5tM5oF+uyt zsaVDfI_>4BbxYTqxm~O{-H4a z10ehc5q~197{yf#FUC9xXbe&-Aupda(+2=xCGXRWU)U)iX5Q5kA{G`;es!)kdq?RX z2*BH*nFQU4_m%wclhdaep+9}as7q&VOxF|sje(aIRb-4W3NpWC@q`{>WwrT z9c|h_ij~Dj(><7m6zT{`G;lJuu^Eb}Q^Iil0TH)@gK{1QK*xAEgULvxiD1(EN-Qpr7D>upxHFWee|)$;K!({swUJs;@%nWAFfEpl(#RNRv5jAP7G zuOcO4Q`V42W9yLG1?f|p(dy8253J9!>MOZSux!O+QOR8_wX0IXD`(FH(47=W*Ftqr z%r|KZ0Mw}Jguoi{-Cu)$rqJ-QU%!04-vpbc5WSDX>o)!8f(hFk+5+!EE;LSD(GtSy zV%1Yk8O^Bu$rj|37hWm=Dwe#}N&6mfY307%zwa!xEHKBS6!q}zq^!1Rb-2XAtNk+@ zrhcffl$9W3c+@)cCE<&uIlI8x@eJ?#I9`A~5@Ag2IasYJYzto=&R!@;@>6MTo?hXl}p zTPSc*;$4bZ00Zksm1{=1Zt}_dHCpc-qaL~leGnvh5XBZ>Oe-mZvt6u!5~5)efOIZ>xgEBQwRE`@i~9|-d8)aj_J zpK<{|WnhO+2tf}3DRBY&F=Ad6nu%`?e)L+Qq%!s8MJSB{7DfOC>IyQfUhU91DudO8 zz{w2sH$;JQ`SVX-pwRCObMTu?wMWVx|GPGT5J21l*ZIZrPyiZ@KMG>ApGmjG-$c>8 z?5ZF|S1gEb6mdrb`{ZA)#lJ{`G{CVyP1V98HpB+|ZIy)CF+ zFp8}%;RY9@xRsjaB^;M8=B^4hihs|+=6_F&cGDeIuyL^JU~;u?iQob_D5%Ak-a^T& zHwol&QJPSYCBJ;&>q{c%Kil722);7j{yy^ZJ^m_#0Ao_Xx_(txW2!5lDe%AZ5lcpHj8jxWN(v9*hbyWRAsCgA^?Y>kc(21;ome_g zQYAtTYq3NIt1wjPxD>Mf#L$)!$xeP8ZdY|%cH!R%C~oi+qB9@Lz%zq=xi|igxJou_ zqEDg#q#Evv^lc)m-m??8rC({|rr)!ZWhFTpx3A0JJD~-Y0(R&1zOc%ukDLCLwt5P= z{TZw~ZEdymr?`-z|z(apF5DlEVQ68Itivrx3jLF(@cn6}EYSo636&3#*LLPUY)o z6~$b|y6`jeQ=~ZEUa10gHd{)=iqKe7Vk4((Ig8N%vCI^#sDah15SD` zcbAZIP^h-~aTM|>&WT=TuuM!*ON*>3kzXe8k-#s?10$7Wsr7<4+3@H;=d(H9GHW}@ z?6ELp?)su|xHZ^^!!oe-ezu}fl$WPE6v!0J4s0J43e$X~lX!(`{$ZWOK`VKSTZlNw z0(_7xVnEdiFU(&AY5njXc<53l7E#xoFtF{uT%92jA)-#Km^j=I@6I?k*mSP>!PPT1bocD~ z!drjPn}YKgoFQ;rG14KxFSAx3z(N;8RnSP0p|@V0_h3|54!Y>-%WGOU{Qfn%vyeX6 z){W6TWcWOm%`hnsaJlc_Pb2i$zFBA}nsTG@@O~ghaGsOxSm|p}WGr3dNRkQw0teF4 zzM~QDa@#!-ROb;g^|49`L!jOy1wEq1d8xX*&i`15nZ6NfRIR!X_&>tIg;VqCZfk;} zr3=3b&l^B7S*@dBI2hw20Rqs{duSp6dJ>xZrHD%|j-o$gsfq}BT#Mzu7AvD0lzZ2g z*HEqT_C&u&C~{D=?8(%(fqQIOg|p1z%s&$0j!hjzgFrt}mq%e>Mc87`CG zx0k5MOXn--N?7kPo*p`sTx!?ybB)oa_LlR$GhNCn;)sDpvzZJwn6KzwL&X}${+jS! zHVCRzK$V(WoF;pcLM(^AQXOf9_KYdz5~vU4#TLSixVdq1TF0|qys-#)PHHteK9yav z-92Os~`6&3Di$$9EuyW}D(^IqUJT;sO!vv;^9e1ZfoQ;%M$izaCv-VH?{^ z|MgBnEuL+CqQ<7T=sHex>T%fEcEg>g%AcfirF;AMV9n~tMUlrz6307xE1K$tJPn-b z3-^93^oKXPXj{l@-L{(lYbN}1;x?YD~FhV->X9Xl6FVV~cl3w|mm zd0S_uUet1D6cw4-8wqVbJ(bp=r*Z~%v{ip6lb;eGuMI@{QQK)Zz4bGrde|T&Ci|QR zDJSSSpE-Z}eaJZ3MWXVlr0mwlpNDzW?WP;Pe9M^K0)qXO+pNRq>xsfM995m>FLowd zG4a7PWsGLDzK=OKu8ccyKcK-t!2b{9K89AJmaP))2~gQcisfIim0%|7*PP6JG<#4M zk_(C(&G&c3)ql}F`SzbyCCX0;s&`ip$ZSSSLKPe)>F&P(Jfx%QUgnIIi4zm57j%IK zr;vZR0K$gRa#^lLdKF?_(;QX(8S;vtXUmhBi{oTKZnRU@zzV3c(Jp$bg~hRlZY20v zSci#@CzS3cveU3WMU3c4Sb~dEz|i{{OtyBKCs`Ia*x1Ud25t6WRKP}8b45#{Xo%{K-TJIzDidl{` zxC3XSRhtkwgAHlniUnLZ#Jm()?Ry8%E`yf`?9$N3i73#=gr%_XT0S@^)mm>6s!Nf! zR=dto@II&mbpV#eyT9MPAzm{PMb|>;ZPeRH9IxXd;vJkMBZdXLpxWI5LHN06@hW`$V@?{t5%*#A92glxmx+c zHPf)Vw3ypR63)-Vy7!6$j~t#PD#>ffKh;xLU<+Y&MRm?OupT5+RBb$!9N=zQ6hkz* z?VF%>IOI}E&O#)FuTLYx>qZfj#&{h)E_tb)}P)&0Z8i&FU~~D z8Wk%RzKRvItZ@uKS}udG-EA}rnQC}B4wrw@*kJay41STZuMRu0VMDHX!nY(aUCGYH zxCS|LO&s|r|fKBcr}73 zbqgd|0<8SbM{iLvMW-~;#Xi=8cI%+J2ADXT0j_B0+^?kUR5HQZpf}=PR#UP``A$k2 zpI7eFM!7lBu-q>O5O`Xxg(#@oQICA{BTf~XmmbTc8;!m7Ysw_#Avu^X^W+=zN6>P~ z-A5T~)nqsJt$ldbzVpxthz7Ycg%~0rG5-2|q?DKaT8!fw`0AVvDzRJJwjBDhjn&HD zl38hVXZgOF$OOY%v+L?3T+8A>Hm zG;ZEG>wq=R1!DC-{8kmpZos)d?g+o`MpMwtK>}nY7)7o5R z{i_Gx$+|%t0$m`vpRXzKxLu|G|4m0a4$o2bJaPcSIJ{=laW@9yeJ5tD{BH`lC_a&N1lqB(8* zT5_ot`5~Yb|F^?Q>^H*6iCT}Ahu$`S3hqX9hQUORu=~HZcrkv4TQ$&w$xm7}coCg8 zHV%1!*4+cJNCvA zd1HsGP>)L@>^Pa%g)jn20S#G@Pc<#9%e7D_|k7p%S_K{Ov^+jZX7?jHxbK&bcs-X6K$mSWot;PpP_6L^Z-DhU+5e4*3H$)>p+5VlOu8wM%EqPZF2 z%yw?LVl?wLEpMV>tb(Wsig`&S40sIUe2RC4AB3JXOQC`vX^GT(SHX`GDtskWC_;uw z*~m)wm&kYtee2(CC(A-~fx$^bTu~pl?-w^P!agKat#_{sC1(J9N(ZBviy-%9bgf9{ z`iydL)}3aq>!vTCrpq737ek865GC{x%<_sL+5H@_3~an32_Pga$_DZzY{P@L`_N(H zS>>->-*-R=F)v{`X!A40WOgYrww4zXvqz>|zSOGvSWyYhz2-v&qh>NXJD_;ibF{X` zue{&@ql?2)9gbGSf`hW=d!zbuy2^Y8-|=Y+t3yBNq7>EI9!LMtoZYv)LGmwBE@L?U zNj4Wsq-dwpQarBZb2p71F&Tc%^1vwBof)v`Q%SB5ymfK_`ymDBfBH)-2%7wx#nEiE z9Sd6sWGU&M&kFpFhYxbW1cNi;IA+OeZ=Kf0e6IU^X4M09@V7h=q_rZGJIXAEQXt_R zv8(DUU&%yU%6+uR|I{9fa0^9v7l{_LaDdV05tVRRua%031}{lnD9F#5rCi9YV%uFkVGa`zPuA> z7T)c(QFeI$ht~eZ?j&8}x&Mc>w}6VW`}&3lL?u)jl~%e%x=~8HI|b>K5NQm$q#LA$ z?rsnPrF-a4GJzUV^1@XYo9joYk5J^z**nfEUn<*EBzdbtw<2F?FnMC--e9NL zM>?nmVmTPeA;y1nYDh+%Wv8o5kK$!^<65?CxM>d@YI}BNdR$ifeiQgZu z1L%P8(s-+fo`ye_TPS;Nn>bax{z!i$FAJ25qA4PhM%c`&92o%MSQ-Kfh22umzUCd* zeiC~v$*I$zz@Q^kl@T|u!P7Bn&u4l1Q;TP9$ONgV{>qTMu*{TXi>?G}38GSAHVpu% zWC=hQ!I<+gb75Cze9FXQbml^^KMr3@m%9u~?-k`$j@8Z(l&KW7GI1--IlMOCCZB*$7EH*yrgmM@#gE2V7C2CX|q z&CX5>B1GS{YnHlJY}sicDvJRm)AP1p?%M?q z&7!_)Prww^)>!L4I2#_mj_7AGWGG~@s)>F_606}co#(M~luxjBmoe5>7rIvIC_oU| z`jpCeV&qKSito#pK%ItP>z18H2Q!bZKZ5oqWOyA4!ca^W>Wy6-0~jfd<^Rv@fMcdS z{QWCCKR!w^WLYP}6x*XTJ0%yPC;^NC@-`dNeo?4P0DP+a1wHW*L`L}A0@Xxw!76_bMrFFbtR3OaP z8K8*uz^h&-{&z3=N1_*hgyLAYjB6ZNlE8G4*PwDx^g8%z zT%W;Y@UUz#{M&FWWK`#RU_?mNlw-C$VNv7@Kq(nU9u~j8$}=|_x*|I9x(lH9KZlr4 zGO6y@bb@&#C9BT1=)((F;aqm?+%D{4!q8=K`J;;hQWd`oo#Nr z^&h^xtnFu!nm8G0&M$=(ZwYQa05VU#{wQ>|cLgYr!7<1v@T5Rpri=hA?>wjf(nv|* zhFe6X4mv0g!w#Jn)ve-!$Xt8zxH}{XSBB+#((0pWcMkU1P8|E(badvrRb;Vh453)g!1@lC=S0~uWQP71F4{hQ?#;Q>Gseo%G!Q3^!e+D+c; z->w=S3gY-OZ+~-h5Iq4024e>soFw-FIcmzAlT<|x`n1(X=sIF(;-y~|=v2R1WqFtS z?VDVDGQ1fHW?WVC(mMMv;T?;%>BPOSlspQ@JBJ6#Kme*oBk zLjNYXF%Yxqukp`#YEvj!gV^g6uA8SlN%Is#oTXQ{l?%s9wB*I>CXfZ~9^V0c)x-t; zk`y@+PaSNhs?2l%ygbrkwVWvX{L-)^$Nj^pK7IZIP3yZ(-6GE6oQa5$f&@ubM%@>> zOo~euLjat$2E)Z040{9!k&%wDqjGk|u?*?xPa5Ux(g3nGL-KLYSj4lfAL`>JZ=X`^ z#HlzRRxqYHw6^}4o@!5FWB)gGAV}=Q}Cz4LDS@a7Tjyfc>vr~;AdvrmY)H*Jk&7AMC{A8 zVFnByJWx9dBEf;~w>)tJbs+j`*{3{YU?deCR(&*!Kytn^tW zR&=av^==syqP)$y{*4{i=NB;lQb5kgp7QJ~swc^wQm-E`mgDKXX#|TSLOf>EUMpz* z?hOqb@767PLNwm^C3t@qJf#9?tIh--OcMo{luoT#629Sv)YHw_6VYhUV?N6dn02f4 z=PfwOeZb1S6K!+ zGFY?#63Ak}GMl$5Q?fKbyjj|hLDa8*InE+3BQ&pud;(&Udb$?45hynseE@ffb^o*L z@JIY#-L~Kfu}8II=p_91wD2dicGFhO$|G`$=XSuS)h)*R-RmEjr&oN>CcmZ| z4u(;8w2jpfUWpOOs0v3k5jj|)!6oGZb3081xKsf;0N3dBFBnfc_=f`^ACj}N(4|5W zaIqp_OIe@^8OSJjrR#s7Rc~Xa;L)6bAPHagNxSX!;JC|+4_UvPLwV1I%=0jKu%OUo z?n2!e>SSELwbip}PA|HX1mZiO=Pc#H(~=#H7TMT4vp`swO{9=yprV$T_=UHl%h?a@ zlHYBDnr0c|uigRpIC%&bg@GWz?pn(6T?xbHUsCDcZNq=}^&fana5}!ZIW5=#t2{eK zdO}3Uh2zD_|1_ubC!o#lPy$IVG zXv;$m?;1G^R10|TW&t3_GPivRZkMI>ZlwWd!!qNrpX@*1V(h2`7UiE{rG9dAveiIx zNi_ODY-O6w^(68IKw(6xJcK`W4*427*hG<{`!IKcjet4}vF zNNeLOf_Wre2sUX@B5F{BTxQXaV^_Sl$Rts|&XEaNX0C zHiS2?{xbcACeT72l_V0kX|glGV*M*@(!}PMfg24bH~^l2s{5fBcqIb@xgQWwYHujyDGOd5`56h`xhPNRlTwyi}fnl7gfe@D8|ah7jkQOsN3iw=0+t_06HaQR#r&CZ7Si5&3i8 zs+SXhvZSYLjtSaz5I`#crzbwPGTh& z`Nr)w-LChOCtPEH>-Lv`ulFCBMqYU8)vkgQq%OhdtKgu#E`0C*RbcZ!01E!mOYx5m z3-Ik5c&z9AjQ?t@`1e2mk=cTqLAOu%f2Y&Hb`flPDhQI20@~kySDJd$Xo?5`oNNTy z>1c0L$A32$4z56i_4d}{`bX!?{{VFWynY|P>c3}!^xI2t>CCPVV)<~+MXCY2Dro(7 z2>pKmz5RRDx8D|Q0z3041}ZFab3s8ec=i)CUhTwy?G5Rl|9SIdvqpd+tnMAouckBX zgDN@D$xg$)_5`4(_ZG2cD`ceF;+hYDu|g{#u%Sa=zO7Xwm@*&1CHa9H*C`Ny#T_Po zP9guhMxaiGz%u!e0tRm_S-5g_i6nXKBdCW4o@L`mWE1`@NMW9pci_NG1B$X4CC}<@ zX@d28M8ir28b8hvCWi1?+9jbznqlgCg87+}?=Pnpy753E!5cAAyuU%jDF1w*>Q`dY`;lYX})H;`Zhy-c{! z&d0rj)iH{{*Hl0zH{)2~@{2zXU0TonFx3~D`BC>4JMVv^GZ1V=vs*fM#5YpIb|1C2 zB~%d5QspA@+w7nfy2>~Zl%>wi)C0s}_H#i$Ii&yEwOP1SPdaQJyA?M)fGG6~!MU$V zMZlKGV0H&e!0CgG{15yEg3iO-$90xRI8kZR)5Q!&j4dzhXA2>m7MgLp!dm_Rlnthu;1Ck#1&2bM<|cQ`YO1`Py6@>V}8IUoih+Jb|74C;<*@=DYVg zH>B8s0#37DTNuYcwgCh|m~P5~%l%IfEYP8TGQ&Khps$~*&JIj%5yFQYKE=WAjQ1O^ zojJl5q$bdqJVYG`_=%^?h#>Jj(QKMBVWjLJQ*v;=gKoN9G3*Vib3c6PwLK-Z$^Gy< zNaQP}83O}LM9an*s?5=mz(o*5yvt=dRW+3AQw6HOvgW}Es6OCKI4RlS@XgD2K&8iX z*_>X+g(qxB@{=-wJ8jt?J7D}vO2GY+78s~P+M3WX#$PaWG|99Ar|qqU z(4iY>i1)~Dv0|LM-|OxY!)TVe#Q7=T1S!e9`S`{-RW|E-eeH@8o2x)CGz?xjz)eNT zn^9iFA7aC(eztoE0){#AUAr(Q#YwLx0TrQVVAw|lqqcuW&y;S_Q(z|XA+9ic=KPoJ zcny~wecoc_H?pG&$PS3Lo)S9ZG!VQS(r#J=H>yGl6OI=k7;@b{tqw+Yuvxm2k_(bn z-E(Bv+In=?U+6tuLF>f8Yt!QSM_^J%fFku4#w5i@Pts40OFPqsrDIbY-cxyhxV8&t z?$<6Su`CN1Gu0k*u_%6Z82XL@HSe!ln)cYeKxqw%{x(vWB-Du~Gl1f`kZG-unpS5_ z4OtISEzl=AYf!cZ-N^*f_$+c%5Q2Ec=kD#@qj^XRuj&Z~rSV>5xP|9p9IgpO^(1n= z_P82n9_6OT^^}Y%kO|T_NH?L^0lsfTSn$c1qKCv;z0UA*pE+uYUqdmI!%Tg!kf5u55Y|deNebu&?xt?q^ z7LvJi)eh3mGP!$}vTjWuw*9WSj)Y?20H*D7Dee~zmD`piFGpVgA+Ml$y$i#lAeF>7 z6_;nQ+}?T!wnYW+-Jq+^!l5Lqy_}|hG-l(NHl9J71 zc{FjI08BDUr*_|rCCyrWynMBn%9rX$`(PYvF0wJ^_#MQgaCEdE8mK0ASIA3x7k7~a zA%B0gA|Ob5j~xiR?jc}65HMayLJ(iDBO`lyPJts>L7?&A0U{{`kyOtBvFAA?gm@&8 z-~9{=!FB1+Gl~vV#G>kB>k{85v4d{{KEl$39y%oYe#v)#@&@!=rGyzaveAK=sVaWs zj!(Nx!;kya@MNUBY4mfgOIQ!qcbuLLa7mI|>_x0nuLynf?-{m^LT<{9?oBUN9HD#b zxLfx=f<}^&RyCVZ4BHxNihY0Z8zvexN&x&b-R!o&$y^rw&6h5$>C`$p5`DP37FsOH({(7{H4evf>}OzwI^Po~R1{WA%BiJ6(82gD zIoZpM?yM94I<78Q(Yg`pW!FZFP|Y4vh%y}qp>LD)xAa3Sm@49PvLUz2WRFxjILPwL zrB>PaSmrc^i2CkTwR1imZAK+>lfwE~ga9<~daNz^^@IC2qz2T6AO?dYH_AN8Oo!|V z0h{550ycw5H}DZ6W*h`*7J1X1o*2G?TK=A;$W7jT8XPnQ!rY379}j0d<~@=XnG3() zldkQ1BO~h?*Sg@|Cs8FBPUloAPncwMKG!wBqb#+66I zT()d%mz*}d+Psbl?$;T7PR^nkU1PTEUFgo&pTHy`EDJ8MNnu9n!qb`@7bbtUry_o7 zMv;Z<+7Rjb=|>)FHQ4*BfclQ)&-@DI-V+=;r!7r^)W|)6S6^6W4ZA1P znEjHHq%b`pf%OPCTQ(aE)RND(+p*9 zoiAzSq1W;IP0>}s2Yt!oIj7|~qe3U!G@K&#rR;8>$7Txl znVQWY@q0cSn5=MFbo*li$7-Ew4p6&udla4_)S@&gT+?A?;;~p1RzV|psU=$23jcgP zTMWotIb4VMIEj|-<0OxBSjeY24X*g*u7Wp)DqY+P<|4gGEb^7I`|jE1m+qLH5oQo4 z457Q&Y)3g$XO{P=nP2j9-PMk=IQ_`id-80kT5oAR1oM|e=ay{{OZIifOc{U3)(ijy zwEC*nAS_-q$b~!0V3?Ni!Rx+Q*BrdEji`mF^fNfG5M(vkCUvCQ!fXV3Qy)#%*{orA z0~)&#tpjwlM%@x z2G#KiFGs^mO7&=;zpIsL{Kkx+R@-*n@3uC!t2xCBhG?anNrN9^Xrk z6x)b+FFkSDi{1)lEvn;GEKga&sdk#A>qkp}?jKt#LIQf8J@@1~JLL$bekA!&Puz}g zqp*v${M#2 z!f1qmk?eKL89IK!e{C=Xfr=amJCsDk@LR3a5a&6CH0#tEk~Oa4y*U|6#`;cE%!+O{O`|o+jfR8k*iRQzWmKFDk=5$_AK_qpv5fW$}L^XIl5^_A~q!my@ z;v0TvK{i%b#lwv(snGEP&W}w5kGH3kU~f?86RHFH>eSO#hI6Bp7HXHY3O{gd&c>CG ze|#<_`;^1i9iKZOiQh^1^2BB-o}=*1&ry*78En2x7LgS~;N*w>B;qq=n3_Xgx#Ao?s@$l|eZK*eR2Qgs}?QAmm+75ca`gx}@tg~ROjvCD}|t*}qKU%V%JQ9%l6 z0hy&Agz)FgsmIl&bMK4Z-2j z9&;%ox|NYQ2(@TUER@%6=&$)#mprZxFk#FtvOya5nL`(|hK!j`Jn!EXZXtZ*jSx>{ zFI{6of0w=#%gS(BBD^4hWIkDivbxmW3AY}cuZDDoktBO)x{?Gux-hdUzvQbHNVgg^ z7(Ck#4m1@$VWosf;)on2c)j`AXP0@6MhOXdG7xK?oFTvOPio#F%Aj19?eNxQ&YF?auZmDk4S1f|c$e&ajX2`>%wfB4 zGrBXX?NnjeYUzL}UaZf|(xUUk8MD~~h z90oVHfJmS?JUweMZKx1N2L~nniuzMUGTAzfZR;mZbtxc{v@m0jRElST zTv^+)*eA6XN))90E=z@9hd=IOXa7?1Q)9C@Byfb%&akJb7V3u0oQ9JlTKX$JUGclV zgWvS@gZ|l1=!_Vn(hLlJV?qiAn=&K8$$#K#iokUIXY)|*HD~Z6#;jD^LM?vDu~Wfp z*XXVO6lg!P-tM~Zdphc8Vm1D1wB^Bu{HDM#!0MjJfZ0TPPi)VN8bn%)8CnkKz?x)HnTOD$~cFcB${OMk3^?fR-U1>IE zPi!4_W|v(uqcAukuy^N$^`YY`kFAm`Bj#YDAtRdXATvSe`TPZG*Efm3}31#lZ4u zDb)3bbS=72Ol{>*Zhbki(=Dh=m1u)%kt2^>Zm;?SBB#1vsnaVgOcC z``J{HFj8sqn|3JET-%{<&-}UPL$@s5UX|ybh8A2AtG{Q0l5L!q7ut^o&LZtzM_`A( z|5_^C9z}CaBrdJ@TZ-(*hJUN#kfW7pEN7~!0)3`LqMz41gu5rSQu4$lQ}{yE5KJ#z zkWunnUlI)=7RvC_dFk;SErDvN+#6(RG|nE($n96S4Y@j6YpY%kr1$EFLg?K}-4#8} zhD}9iPF>#X&6+bB+lji(M1IK__!;aiT`S-4IfL_#0#41xD^5^+q~X^h9-hHY1Cjh$ zG{jk<1YsZ1HG@2+)(T!@HR;v2bLI9pqNX^M77=$<9Prc$8nzDXLz?}H;+C!4{)!&a zI8)4G-$2GP@r$BO0K|1~)V=h|CbE}f1L;2|2?W6o*n0*ZaI!&=tH|KeszseQ0((V+ z$74wV50^!8f5eTNA(=ILMGliJncwLbx>!_>4>?`5R0hX|O$LI%q|@o%T^MritKHM) zuR}ADlx$zOr>aHGRl8H>45G7P6&6~rujsc%7R`A%Z7a3+UrMAWHnA1`dDVT;JSH4)s} zcxwX{STIJHbSMkEY{ptpwrQtM-H0K;F5b=w5vCDdI{3`b|9<#TztVTZvifLD0PZF2 zl?x#^Ep6DgqvMVofCgv1jeEDEUiI0m3Bsr{XS2+Ii+*4R*l@rtyzaEWht?CLs1{=3 zTdlb6PtVQ&K>iLVFwt2}em$u9nLQlE6epF;X=xwC&7hn(t9!)Y)qy^c{b;ahZhd`$pgK;RMQ064Ypx?)GJ@GKDlu&Y63Td6W5-!+)PXA z5FKoCxZ?QXVvWIouoCWjhyDa+`>$!(4=f*8YM1EPNtenBE19ki7^mHMgCp7#Dh7v^n3fWM>3G+K% z4edYD?1tggWPtUtnAeAWt1KOmN3|hQzKWN~vmVFR3#{#*fU}ToK9O@NnD(bFWK9xQ zsI4%+Hr%;vLJGSd4n+a}o2AWl)LJ?J7fRd2qUQ%_*Oyq0I@QkDITJ;EA`3$n8ZPY) zUhy!dtSfvx1hpCOA0l)L%Q_xe$ba%Ll2d|G^4Dh@7>pbP+^4kj7doS5P3Kn-BwUAe zztD`tmM+bW>#$T?jnQZ{_YVW^$TB!iv5!v6a&gG93=^8uud7J%=u5dXVY=Zv?H`G} zTF(Q6?hbMczAd<4`K}r1%zTe~yxM_cYCf*3d*T4)86>`>(}AC|h&2!HWJMAd+_^c2 z$ku>3E%BU7w=e%=PA-E0@hH~?gznasw2*AtLM+NYE4;iXE8kx)aA?|rS(hn}EfGRA zCX`zZW|h`@Y_p4Pw-swZ+`G%2Gem(BfpKZ?h5Wf*A%^8lWqVgdLbURfl-K$kkS(=% zLI5DPp#p@xya0k{*UcskiBcL<(9M%C-O}`GqTbRUMg zZ5*s8?<+}%zy{As-#-roFLD1TD=tp&3A7qZ^@g2vvm}#L`^SQyk6Cvq2)+v5{FAX*w`B607 zID#O5|*wSIDDJXd8QYB+CvCR$k^(<)h9{-xtJ^-5jU?W?=!9-n$>UF2tmaWpR!S#Jw z|5IET)z$#0*nZnQ_Tp~Y0b6B(qqXkfXf{Cv!_>H{Vrq#y3yu*s`Cdlof@-K>q{c6- zVGFZ|KhCb|35FUv;o%Zz;HQ7HGTDB@&l;JQGGoj42UrySU0&zETUx$+{gUq66GKDg zBobt#0NO4NQh%3LDwdf{gJBe`%9Udg5DPQ((T_|9od3hk3d8~djG)^9#toeSsW8Jq zbjf$sF%KUuTP71dr|#+rd1_ovl8Y-UL{F1ZXIrD=V4(|y+5Ke|AMk7p>|g`PDY%Tsz2!SUMWt(!20XEEBX*wBKj zE!AC|f9iZ7ktYDjEHsmzJiL{>T*#x+kPpF&&8Xl#G4S5To3j{#+xOrDl=4%&8g5ze z@9$ZDxOfj?D#p4|fS=$Ul?EJuZAXK^{c2NMn{GeIP6>oqn;&FN4WP+ zRPWDr3*~jy(vQhww5R4k|uFX^| zqW#xRkm0eO7KE43TWOB$2!Bb$MTzvu7bKr;e(tiVpu*Q)Ak!3zXmf!ip5?r`RA##A z3V^Sl;WDUu)ft@hwXyC0nsfnT(+$?_{?E%B!&Sr6Itan-5-g;F8qN39=LMf30*f1c za7(U&*Wlts)+IwYOt190L7?tOBsg4fa7g4 z0i`$?y?%wf@mmp^hLw>^_xALtJGabk-PZYdD}OB-c>J^~Irt)eSY99_*=(?wGB$W6 zw@o1v_E{n#Tc+5cJ?;G1s=t6Jmu7a+PW3(AzuE4S9N3>nxR~63MLa2-4OUoV9v~Kg zG+=nti%W?Ouip6eW2!Y+=5nP|o#@^kvQG|mT4$|6Pd zE-4Gytfmv$ZG?9h+R?KVhP~~NkJtd}@nex7{KUAjmF%xmJe36sS3eACw7o4PAW_Ho z?CTOBVhIIOQ?8({m0G0Z0Dws2SUmYBkuhxGS)r-6NHvf|0F35I3@7a)K-v#vfc#b_ z%r9>LAA|zppb4aJ6>_xdQS!C8BX#c9Ay-JW94n!k1VfToE@g*hpMz>MT1h%Q*+(A| zeZ+A5F19W>6EbP6cV;!&>8O!aRxPeGb}5Nq290-D7P zgF`N%&qbg{mhS20oyPs)ROpr)2>*<;gXj8Rp>}Yn8Re=7LB{A?&l$GZald@<%RXzb z0Oi!>-iuWbBs$Wj5WNq~Gx&LY)Eq4S>L!`;L$K*7(rgT~!C<9>CSZB2AQy^J+Q52a zwL6?5IR{EvZ$vaIO!ScvxomNv8y8cPR7QmUgj_FAPZOb=@lgsR8>iw06D4m$YeaP+ zJ)ybC6cS;9pj#D6P-xW>e6anA!G67M@r7I)#Vw!y{XOdnga}WvtRChKU7KLoMez`= zl8s9H;cfb<8q>2m3=oKPBn6WajNxJ>w*JnS=B z={0ccoC!|63r-SI@DS)RQSSOjq?N6f*xG--pUCa*DU-;V)dt}7o*4G3dltG0G3oLy z{JJCHU%~kDQDjxs5M-x zTvlXrbJD2W9mH23gDjw8x|A42hpQw=9VtAwirp(;JzS3lV1`(Gd;SvA8b|Y43(ABJ zu0g1HG_s~F@04l2^PSd|Io?r zKQ6YUCC>Rqtj29N4*4KlS5h&bk*nw($VEN7$Wqfe)17gzNM%BGiAe{Dv)zOAco<{$ z_Wb@eaYkE&JB0-7d(|%X07$>BlHmF4@}6r+{#BJX2L#-D64U|dIqp0kxivWS-ZWVu z+6=tQ=8`z3sdqV#0!4?Z&p+taJBt_3|IF|>wp9KQBY}`usAt*{!$l=>9Graynwo;b z&Y-EB?-z2BkuJ^CK01eT9G^R0FEfLUlvOWTUhws9-C+l!=L?XVAByh0;JcMLtw@A4 z5KW8pL~f`=xE}y%lqUJiFW3BIk=m?mrOcqymZ#hM?JcYPhj**tyvyj(gnujICNPQu zAoSVs20stvQwv!!4OxskTTB-V!esNR>{puD4{Jbt{%nas&L2poHcZssY?Uc?T4&B| zYYBfM9)6<9MvBq$aV?)m7T3J0IS)WZIRdU>;5|grSv9zB?j$L_)y--$q^YP)C0mFI zM_fSo;_zuasIfbz-CWw9ww|slAtZMGx50~s4Q!7eJzrLDWdbG2s0id}$M^MNyuE@$ zYm$JAl!&BKiYfxrO2=Yp0%A=Ama%~A$3n>kdI#5XN{7~xcb#H96$RgsP_Puo{2i)v z#w-d{MFEO3*?7?hog(w?se19q9JX`@0=)ipRRr3*WkdJ)-S%{=K)kl887_ZD5n^r< zl7db8h?vTdiYFM{|9(VZXF)cg!L225(vp70h7(eX0;^K4A2lIK{W(fOe^TQ5%2cgj zM`$hb;xFYG#5=mV$TPqRy_*Z8!>l*#fGv$1m{v7xTNYdrgp+w!=B~WGG@q$|_~)#d zYV}98K?Ddlc7!*Q1N{}mc%C?iGpM<;&>n(0S+2&9%$Wojul*c?cdJ9yM1f>5Oz1LIn0K#2oEQGLAQR>^IZlPDKBQgDi(s^ zIuA+pk#G3plMTq)a%#OWxHa$oV>;PzgUwCsp?HCOYZ~4|(h>#Rv-AB&E1kRq-ls)` zsjzaVBFs$uFVRv32KF=+%u%WRwFZe0qA@-5 zHSwGQ3vmLt=%$|8yQ6=P89>2kijsRx@^3{g09Vw#nQFI+!!sbm)4K)iju!mxu)W85 zcfkTa`2rbgBYea^^5}*pw->|+g)g%t5NWbN4kKXO%QdN(Gfr>7U- z4NYX!slVsxme5Vz@KeR&qtA7_K7VjO3s9_xPzzG3793?yp4`%@4Sadx3AOjX9sI}e z%(w{QU6$LEn)e*Mq}5shWeR$rO0Q|8@3rpjGXL8k{~=2HVdLcB;&h=%dxKK-(4SscLND2_kMcWzZ~qYY$7&7t$0?Lr zefci!S18#asIoP=P6`6frH3z<`G}=-DfGc`Ju#gOui0c7511`TEyct5cK)4Gg{vjp z@Qb2P5M)Z?W9F6tPT)|gr9j%gaPY<}FaM~_2eEcx_peX4HrMNp9^zo^@Jg|Bb&AX{ ztd+<0Cp>M*Xa7Cv&U&15CCs*725rAOHr*~bROz+uIWp-t*qPpZ0|Y&M)~QNJzQ?C( zC;BN(T|>|2)|p+m*}ZU$#;y$4M#WY*d1Z;djoLYZ=miic9W(Yg80<>-J*T>f#2(FB{G z^xl7J0gA6b+}8}tWv*x=SwTZ=16U8x&`R3Y{i=7<}U>SQa*O#yo5C4cxZBbIvzUjYR?sI8g) zk>ZR}_3hdAOd>ae8o|uqV1Vr+gXq(jiy)HGwt}Q*u;1wk*La+}Sbe~9U+rF3jRS^dc2n$ymw2(@YH$iT!!d0}n=+o!?2FPMK9ZU5u8+r^0?iP{hPyb8^i z749T<`)W0nd5MEs9+G$UG<#Na3}W}So1ZVPZccF6&#&;8A{J$sSC;P>aTc;5TX~;P zVV=)md2rsLliV0O^ZkYIbuC?Qw{8}L{>Q{%%$dZjB9Z_7i_tKxpklTwL0ya&Kt#Sc zx_hfSdEk!kC)I^E={K^;?oKC0%@iw$h@?&6lo$})iqtAp1TnOxpg0FI@k^6YfP41p zczaroN%Q$$9JY`3RGk#S{ebQ-2d}o7kJoVUx!WfLunpqZkSLkwh?Q4wF)sNmpPW-nw#J`?vsVy^J#?NU&ticnf3iSkEO~IxA z6Nlv~(q}OOMQ4cPiPCBKn6sc*D1U#v%IcFd!}~BCrBo@0BSl`^Df-urO`Xd)0wCtD zRnI(C=k7OPeBnJ^=OhCBqjw0^NEEWMGP_eX@BRGAE1-o>p24E2wG)9Snnk^xNNC9r zI`*ST;ZPKAjvMww`5?~JK|vhYCVlgcp-YF-(=4@Bvp>J($6%cE^wQ&9HOP937Kg`4 z)t72e*L=C-9CnqY3knb+Q*(Vm`V3gYf+CIL*yp*!77~tDx@p3M^$e4WI~oB|c@oTS z`ni`UAnT8lnC5CeWF-T9R4PoGulW@+Zn{yqO3}dN5dz6@xdqiMt>mq}<)yCQ9zk1A zxybc`LybIu;{UwK;P;)QYy$86b+X&~qc5Y@I%^po=oX9Avyyns->#xgMxHEl6yBLM z@*tQNDsSveDt*TkJ3m_DcCF_g8i)dQwXSMR=5y&f-i>z+nM&)*)-Gg7o zR6x_rpu85S6u6xspDLV!wk*42{0zN2>By(QH?-I#9et*hR@pp@Ulq7?Y0C+bZ`-4A z4>r_yHXeANUp^JM)8AORHQ&M*U^>Gwh_^rkzI~|Hr$_gu*vLr-GBD`a(ysU zHbHqIcyLo_OjUov>)L~(&%zsGv1audHWdjJjpHHx$X&RM$BYNTt=VnOBT&3`4Q3Qb z_VMt$xkzj)rSSe-8@n#VARetU;!ot+Ct))k3wX3KCJf3$%WI=qHKEc^2sct9(^pM< z(7*gSth$&@WfVY)<7KzVUVZR5rJDxb_h4}I7kVN3#ex{Iz1N6E_Zx+I@po_J`tDB+ zofXFJjqQmjo_y*#?dy6NeQen+94HgUnsHW4z$4=t$9EZmk^P{>@U0OgO1E`z2|8j? z+Ajm0Tb*VOciHeRv%{CZn6HjIScY69fiLb?FL-eRizg0gI z7n_{+o)gpAo-SaA*cggA^mO=o&GBEnJ{*gvTHs`_dMFRoZ-+E~QG! zYn+}`AnlF|hvu;A&)&A~+Cgu&{gvH?`$~Qyo3->Y#9VcfC<0+MF56p&_H!G|v|4$o zSdx_`$Q!t9_-u1ZCC^9R;IChJbrh|HE2A_)M?0*UT8yj&yem}8i}77&L^ArL?v#ti z?v4Zn3~R<+)~e@4Uos{)iq@%|YTGZ5;L^&a6SnRg$(ys4%eWz^jZmH2Z+0{e^IJCi zU`El$eE(@{;Iag8r6`|1we6eH^!^~JtGR*J!u62Mli{tNpS4#ka}Ow#DF4l!cS=8M z;1OSY8gt&YvQ1$}czCJ5m84fCtB@`sTfK*)IEM7+^s{Th@=m>i0Ku10D=C|Br=jIo z^n^DDCLq3|9Onko2=ys$!Th$+y9Vc%dl@Wi#N`I-s19-&dULio0&lUV8t9vs)! z6Rw&N9nMOm3L$z?ts*qTeTS}_Qt_CiMT+R`nBU;o%6QCYyawb;&K&L$ zO$GwS=dh!Wy0vS+BabRWqL_-x*UIIoY8vwgC5GLc_pTLJUsh96$~--0Nlv=P!u@{Q zM*KR7rV$wKhv!spVunENS8bDE9Ge5R*j-nd#bsoKDqy_R@SXpwFKGYm+(lE+`t)Oh`JV-De41>baL|j^xPf=aTdiLjU@Heb& z@dmmX=;O*yLq`~sMMA5li;<5sg0%fO8JiG0OT4g$)znLtQj|6nue-E1{h2 zv&eRiqcclemrMLCQMN5XV#p3zB z&fm(WOGvW?^4kvj+USBK zK35r?A4IEBoQka?UBjg#JDj~BF;m#mNp>bUR?!>3T=hDlkUf)k6!n2?+D3#CHN+$M zO;$VAGIkcLsjcnR-OQBIh|sQBGDplXd~2N~dF6wzftWZr>2|VtYVVaVD}kFgu)1(P z;Njyq;N!Ucy6)&;mnaK%8&9hFUY;ai@Us#89b7f6hh8=DUw$?F~bsO(X%e5H<^P@1D;4^pT_6_BxP7DVf$>_rFr)b1EZ%zUd%v6tZ-m*|u z8FnY>qO^h8$pdal(m)Lzor961uSW+;3JGqUUsTk8d>&$58q*mJ&5-)iGfQ$?oo;n2a=wmBX-?Z0Why?bB&7`;mY54 z6dsO1Q^_u;FimE%O^oAyI6nB9t9g)56B}WpHB%{rI86AZ&H}fdg6vZ9wVN`!#9ENw z6Z-uVn{MjUT3%jGw|QOKR<>sH?l9lnFj1*CwVq7l%hOL9-8K1YoDE%=h4Bn{o`xF_ zbC(AU8(EZM(u@>s-16_1iqU}@fM8jzw62)?b56s=k+Q?y1tq&Z16wiyoG{joGejGu z;8d&27Qx6L(h3~ns2tgI6zJ>et75fr`u3j{Y0Cc$_3WSGXU9|gSgRfs8xq3Icl!8I z?#p&9?JEU^UknK>-h*TDD?hnB-e0h`nO=hF(JQ11uT)&6p_(dw|#$Z2#o^{ ztS|}tN2|A~gh*gi+3)~;KEh<#VMv*p8RS}kC3Gjs=R%W%!APDtmRqjcd2BgLg8@;^ z;`CrQTpJW^gsK;tk;x$NsiKIDakdrh-R$~Lb2L&p_Ofzui3bh6IJX7oR{+{o6Wt+ z?}q0HtmYjs5r`%kWWARL_Ov!SUNbkvI`LCvgLK<}^nl$tGyP1V%|m41J%VV~Jk99e zXx2l7TGXcD21^N_>nU;Dms|!*ggJ~+?kp;@M{}1vS}x{QYfvUDqpk!=zvSl?68npa zn~NuJBYmM~4o-{r8+m!-S&hE0QPG6^+oXF6TRIE-{Xl$AsC}t|nH9eCCGzk>u5|!e z@3K18M!Sr$-rG)k_7iQY+BKiz(HabQFj-ON>Ek72Z7x2I91CgfhkB=P$xUO~cC`vt z`!+c=URoobp3ju}_M;Z`PSgy_mKk0?FDQ0sGeI>_m&Ct%Pt)JLw;+;QM)4fB!{(>N zo;Tqdx&A@ipREOF4Ep2^jxV$BS}F5#`;LBUqw3*ZJ-dBH@)8pg-=-@e*o=~;sUIbc zd``_?amKk;X}3b7-_Ex0J%mxrA!e!@Z(L9f8F&G~AfSv(qpmLEC$SolPk@*E#l;)+ z_Bj$uX#`CdHBhA*)gk$r@`-r?AdNvvu-DIH-+?eg?|pXFz5wG8<#f%{jzhJgL`g6Y z1GNGVQ(`h+1)}X!N{VaY71G=4^Bs!G-U(fB(+TT6FYAJ{s}W899iYxeqciovGBBu}d=$L(b1~ z&yyBsbGa~I313krymncxQ1*`>8jQS?jzweXpYMBp26RH%yF%!nh@p(m;XgADWUJUP zib%@F=s4DGakS-Nd4AaA53R-k6H@g))>hGIGLMJ2a@qmLwN;T4OmpW669&vVC+xIbmzeqbcu!`O^jJ zmjV&}$*_oe&;vJ+-)z(49K_SFEwf9=reU85Nr+aQ`R!+M`Njtd+(Yzsat$?Iat;pT zwX)bZYps+rVPU}QS6W>skAsaH^%#TO(zrY#8MPXUKC^hxK5(2y7PErZw6qkhF%q@) z1;1Rg52U=f!pOMlQISc-vyE5zD@Uw@*U%A0<^M1<-NvEHkq8+el7CVoVmPa;(Tw$0 zQ*$$N8y{&3LpqO*xw^KbRW#U(#Glq|+G&z{|Uy01f-PPk#qcRPk@DEm&RCYE5@midn zz3LKOVtWt_66$6^ZxqRM-QDn;LeiU4Pw;4e_bcX$fs6=}tq9&#W&sHl7yR0P;sjAmqcY8^i4fK1AR*xClQO*!hu zB|#{7U8d|Qc1u?3m*OVAG-Dx7B*K-JEq<8GpX-|{8A&shJDDucYQ;mU{fPxqh%q}& ziS(_n9bk-e*7&QvLy%*GgPv^@mcN})9}0{`Cj*Q{Rf2RkthfwmN^aPjLMaHIY7_=o z8*!N+Nr~$HC|{k~!l`9UNAjlhPY0ravqJc{I=l`Nuho* zcZ2($!8OJB}kr$zew1+GVGNYixevSqk_M@)_Su1F@(6)YM1Tl>CcH|_Z2|S)Y@c* z_bzs8T6#G*Gh`zAOFoDU$xDT_4nKi`{lii582W2iiEXaYk}y`&>kR!wR`bAP{goy; z#YfOYm+(QgXeVcSp&&C#=z>4Q28Ux8p3DM}jT^wth{%fkaGAHt3x3osPrE!nPg|XD z4#F}%NmuR17o9xqh1@I2li$o1xGNbe#+d1}jh1f9>MT^67jLw#K!x-ii?feat`mVh zoKxQj{B$7~bgoA@h=C1+`{@c4OT_v8+mX>#gYN(ulYjmIe>Z#x1iL2$IJ2WW4%7kK zBi}Q-*2#FroH1B_YUUluiV8C6(h1E}X4iGH0cJ5#Xg-8poTXE<0Biw&l^yQI*f~l) z{eFX0V_j3dnmzHhQg+of;mSfnw`}wA;_KcqsRMbU9>o1Xbf@MZ^r6 z-)^-kuOb~6Ba3)ZKSwxV}6bHlBy~&`1aWPknMd2L4;iTs`C7QQU zem-{TGaM@T@1MVcm@)9+_rKs-fcRm^gWFM@@5JH2o6IQ-CN-7_ z4if?&fSB68zVI_vs*$9yFIlKgpeeUg6pZmP-UIEM=8Yc;MHi&g$rO0t`{35_UkJva z&~^}eY41i<=f5VS=tsRhpglW;mA=;-!vtQ)R>WIm#_%QSMc zAH1RXVmXK}m0bs4a=91g2fhSe9e9_ojz4!4h{QDnxf)Kds=mWbabaM-j&P_jrME8$ z?Q9&lxh`9;oCV&{KihZN-vDq7AC(bw~8k}-dPkB$7)MsaUL%Msa4dQ zixRGX3<1D%F&Gy^PL`vKBu;65cd)%KC^==A{}wiiei|rwN-M#wOY=oWZb3EvFzYB)wl6~O%i$Z$ ziKy=7b@nL=c$tXHoX8)api4pOlEX|cV181v@K$F{lw0Cs(14Ghf_Tl5F6_SysM|DG zzYXU86B;lCpCk*0u^icQ7>4o0uXanst#fcJ>7ji)J1ssP7{G{Eb{!cP?*nN} z;E=vUG0jhF$_faCtYz2*j$Jko(Lu%%vtf{;kjC}^<8_`a-*!?&+|3>pG|iwzp^O0K z4><)t3^`gpB%${#*hoT>c7*H~pz7Z?jDzVAQHylo?^B8}>;n7z%g+HES9$6gN`k<|dvY3)ei3}wuIziUD*Oo^ z@jb;zAoMkBf3^IU0LeyN+4%8aOTx!cMh4N_qO;ObKaAO}U9`)%Vi_E1TumS10O~FY zw-?3B1xEy>h{#txr(2@N&af{P?L#+@3F{vVx36!6M3!t6i(9CZ(@r1_R)g+KjjhTa z0JJti^n-pBuu<(@3L7+}zX3ZEeEE@YMZu*amii1}TW6w6egQrrXTPxkf6lJ=EJ(`A zT=K7Ou>$>`_4BC4clZD!yP}kEv3krm78W70;u4;jf{1kSsaD1}Z!-aZYQBNwK z&eee9d9M^Xb)tn38xe~@3_ZBwQ=8+;_EP@gfSYiv3Q3Nb(^fk;`3$zUM}POGePc^vfO44je~S-{@yXJNpa1VAW%lL*Qc=Y>dKA=F8g8 zTtX7(2lpaepUgAbFxK06hcTy1q}xHOs{t z61hxgot|mxVm?af#c5~4hcmWMuHFAlrkj9w&-Dm$AW6~;$J36IBKo4_ezOym@xu@% ze4l*GHKN&L(iGDY5-873JGwB>dJ+oiY4^ z4Q?b7-tkHS1j3vZ=zcNzyei3no;)7AvkSy5JRbOQ1IQJ!J^9~5O6 zc%e9|!j;pb2H6`kL_{}sv)@K;F7x|ndjgQ`bdyg|o-w)y(ZFB*0Vx~gD0&&D9u`C5 zsfnce2Ge|^OZtILrRYQ|Uq(6r={=n48lL*&g~~oLgOm#;1}yA?|7Ct_y>7C8h_A z0VeGagjo|^v1^^?ZZOKdPJYxD^5p5d+LcWpj9GgSeM3>s;RkcZqVmb|#?-2d>2Uf4 zKAYA%ws7+PfC!o?XpP&gGG{Oa^g6H_5JQ+8;9I5I{OG_UK&Z9`Ph2PTF31r?1?4i! z*>eAWKmJ}gJMb~C!rtPVht3`+rp2PIq~1f1JUrE3)uymM7im{dAi3zK=3%%q-=pX4 zPxH*pzwF>2#0U%;oyLb(-#xq2)y*_BZ`#MpGU`nDWqUEfZ1*|pY#PjSQw8ZgOIQ-s z!=IicThD>+K!=h3^r}s4UNn$yP>-chWRWLU-h+Wt0pQH3O7|mdgQi~3j0SPnVcGF| zJHL(E85yqhK`S0LanhD1Xm2W@7TUfBM^_UKCDU)!EFV_XDnaR!mC2_fp+Bc8N5$h@ zz?O$Ke`_5bxW8_u0*EDEj{rmTs*X8~lH??tx@_NKJzaIn{bQnSpy@=X`AbA*`1dMA zkko8JAo$pSBfhplSC>Yib!OJ|4aMz8L>()aJ^cm_p12n6V`{SZRlL3@P3-%)L!E_}g7vumxC)3~X=$71_`{W_w3TrY(-U_Bm`6B2> z!C!iw>A_?>s3x7H8?VpO*U(>CMKN(J(U@~HoMtQ@cVi}FRjWb?3a288n&M6faA@YE z-PEvZW{o21ZRNgV1(vab(rPVJF=03$$t#3%W9Wl`sN0E(K`s~MxHVJ(lkliu1n5MS zs6E;oFTXF1u2AQ4#6I=GE4*ECB5&c`;=0TON8N>@v0rL{`?j>-b#vErM1<~y;YTnH zjpO>A8DcA8=kE#|9&hMn_TK^_fY-^xb#7D2r#{7PyCGi*daRZSc1lO`uF(62+ft%8 zfk1ByZu9Q%1;zM57LF*J4%0C`%=_pJs*f@JU|>FOcnrokVo%Kn~-9Q~ox_VVMXqOat z^+@l`*fKUnl&)<`Zuiv8z^dJ^^bik6Ytcv+RXBNzGQBQ_+M&}#&WDo0oZWe}?kpS69@VwM?q2PF7LzR&uEd)D9D+pBg|`m^#Mlzjbq zlPx@@_HOti3C7tAK+C8~)PpwMQuoa*K+Eda!c zz0|Hs3LY|YY824al0~9=bS$)s3brY2kzWwAqcK#%lD^FQXX%4o>CZ!v*u#+$x42Mz zV$0tnkgdOcZ;9D+l$V%IM%E|R3A=4ZZe8!mi>A2=9%9e_9?);_!NOaVeR%5r4vEEFHBjAM_bTXcPYBNO#~}| z)%9luZUK_5<$8fIpXVCS-gsQN!soQMsanr)beWB6sNwyPN+NTcbO~zc*-WOa|DXtt z4=o2yl|C6D|CUTNx(Q>w6I=#rH9Gz;0c~#Rr{xd}olP>&nMK9wYs)E<~d#&qJ-0n%Q+DCy!(4n5yDUlht_ck-=9MDqtzbCp9S=gyQKiTj4DwMJ-S+VEC z-=afaWRceBz4Lm7Z|SXCu+fjDxplob^|4=7L*8%r=UqvYM%eo9z}2=)y7b|sl16}W z37G>MnpKx8NTVN4ke~>6r-;s&WvjC)F0?Ui5t9?;>&^B|B3f*{A7q!|qRW?sUk}Q2 ztOl6IZpGSCuE#KpywO}A`|M6fJhT}hSOP%PAs19h@)TWdjr;%{D8yopxaCFQx3>yp z%Kg8jqnC+t!%I|ivkQn$6lNB7Uqfd?A)e2r4mT|T;QDb zpG~54HgL}Rbnp0*9F+A>VmcQ`s(9*fNkSWI0hZdiQBH3_MaR;JU$HEGC@-%J zz#c;Y|7|{?8)3MeRN3P5EgSnyV|vS}UVqz8N4{kU+h94c)>;$p6rsj6Z%U!m2#1xe zsC#vJXDQE0=-b#NA-J}@0XxTFYj{m42Z8hkT$LZH%{+2jd|f#KbF*Ieay-s#%9bmg z1q|{31_l?TH$FRd{v6XcjMWf)=A60yJR82b&wo?*_nvDq`aW_~`-!CU z_3Dvqk~p!=_a*i_FjUR+GK^-J05PX-l*j?>o;2-~`SS~<0Q3W|{CTL&HXy*Kr|G=h zt9urzW&gMmqk!IjdLCoRFva?2xt`P$MU;40{SP4#Ehv|fShkDeR}{TM*0iMxO8|=1 z%+ogtsRj&|8eK>nW~L+C;&*l(GIJ9F(*^wkrs?kDo0@|)-UC1itmKMMlG4M9#ewW9 zPJm-`UKM=~y;`cK+QKn$02hhr1DZ`kD&RjCA&62cJyo`R(whzY zm1Q`;JDO3m3S&%dMmr37WcZO1WTVHbJbS;i5#qT$$!$HeC#YNVRDdH9nP3347nT?OTj%nsUU6+ZodlFK-PaRVWpc6^zcbZK4xD8LFVuYcFB4w^bKoifj z3PLGvNKcYd2N+?R;(~Vd{_JsfS+q(tr~JrUrsv1{@$Z>r2%8uc-~LI%Z-dkD-vAnZ z(M-lu+qo}3Pkz)k(^$?;_DJSdYUrocJgN$;wbTV-WXP)@g?;*9Lynqhqxv+izt*Ts zNPWeoq7rfbmeRO#^0ulkj1@G4D9c_5z_xJBtf{{n1P|1)$b)V)QectZ-gK5AoFC}6#<98JO@=>WHiv>hlT%0L6+sK=E^xTJ9d_ z1^i69gCk=!^Y*r2>8~{P!lp1U@dK!mX^xHYjJw*%{?HgR-3Keyx5o!IdHccMC|kR7 zO$DEj=}Q#GEw@VtvEl?C$&!h9JC)w{6qtI11DwB);W{W6aR6U>{9246_iH%2v1A-_ z=%?4Jwz+37)madh3&$C~%zh@2wlBMn4R$Pr-?@(-FXmlEy#M< zRvKs-n(SjP3y1a7f4e#WQPLKDnHdL&hT91^EMh$-EXhzPO>bdT!4Z@N!u%lSWoD_5 z34sv0XuXd@$hO+?HRLkKsplX>9WQQ-gXnh-3~~kZ;_$+>voQ@e%jGbq-s&xs_TQUP z#9Wz0d64b8Kt5+IU+&;Q#_v;}hVW7*fo}og_s{*S;9=0egqFZk)UDL)Ild@+U-B8j z(CV64fY)D_rwys*XO(44C%RhbqbbjfIS_v%i1a1&-675MO(rGiInd$8U_nk3Vsy{? z9Ca52dufpgn-|5AMW__e{LYD*7ishmv2Mg= z_VCl{r&^$X9Ir&a^aO&9%@K~wV6Ge?i(;(j!S#V(^z`MA@#M{S`Bt{aA# zAcx!sh-o&ro_?kS>a0pl=r_w~rBB6MMYNxy{YmN_2Kf726-j80fUD^Of19vL<+QHf~}P z-bfb-7wM02c<}(&3Yx@TMRoeWjSNJ3ib#EV&AO%M*+z1YD(?3(pBa7KzhXI%c;vLW zuW3D5&kQGnkZ>7v<-ILzZAfG82l_62-Wo=*t+|FaWcy7&BFkS1NPt#1IgBky#ijcC z=OBQA!aP=PRiBE%DXpC`rHTwe&RBcnWlm+W5aLedPg( zxbrIS>p*O4)BY172O_nglBT{GOmX$uq>;gTMVj$)H(`T%dwO;STmjdfGoL;nR`-I6 zUN%;Qf&Ygvh~R~&AeW)FC!{4SR`Qu=mUkIL>zWoCGsY~snPjFt8dpB~haq;U|D{jp zZy*(-&cZcc0I~^)=7u6`mC7-TxUOKN2=A4`Je;1;tV~<@%OUCbw*g&={xGs0%hmQa zI+R&YR0Vabh+F%+FD02H)-n}W$@<^Y4Scz2&BuS{uX-x&f%yp{$P)l22ae5c+^rL@C% zz^#*tEfmZEp!^IF^{qTe+pBaPac`SHdBaek3`mdiDpY5B7M*W$gH`LofYeKyaK`># zVf-#^@1}^eXOjVgZ<7{;v~L}V{93kW87;3T2@b+RnpRVuwGOBK+S_ps6V4P5xUQu3 zweveJ?-v(Z*N^W_sQv_*3_ul^hXCm3Cl*(NUj@UKo!*#gstaV@X}!{4LVK=~70dGG zU~4Y>Dsl@}qXQ(CLcGo-sShbo!4BBjtppkY(@hR}wn0XgNB#L4QF!-J&>EQt3b`s& z8tl6hN*|^Bl(Qne*A3r1s@RQD%LVEhN~UReChFPlrta6tWr41pRuKON4}c4-=6N-^ zA3dnG-$+Ot9^JzvV$1~atbQqmzSF1S+}Fd)AH)hotELSumiMHJNOc_XCefyIRn6Wb zV+eGjH#PNy?WzXJ+%7X-X{0yhw;2v-k6bG(VQq^#k(SrcN~_9MrtSk|L8y_YeKq9Sa!GH1oA7vWgVy}O>+3WhbbNC+HDHyA z!@a-n+(ozw!VcxquYXNAnSJVN&s_YFdk4y0=+yYp{&*pqoyGjI>Qgjnv+X8@O#&c6 zgKYj#2jIWoGs`Bwq}WbPZlf>FS{kOmeNfI~^ro$6Ioc-^@b6{!6S)@zOAL5w(Ty6e zf^guw0)Bxf1M%P66=Y3iPGsSPVhtH03~LLfc?bk_He`q)Ul;S5PY`TyQ<`_%)lM!T8% zq7zfLt24d#)zd5p^s+fC(v{!Mvq-q@#Akirl~D=d;b#H5FrN6|eZP1-N7EwmRh-h^ zMKva9OsZPxTK?vf3$jTn(Qyq+OLIPe-O}k345|J?+D`jdZ{r_7&q>h&oo^u72Z+c< zq#ttfcYAazq#$vFAL&C$O^_T2WaM0y#H6IL$+$GbJs-8DhqEupfBn3i1GL;bmUCp# zht(66Y&~M0Va1R!!G?7iaL}IVPGC9NP>L-uR`NVVCO37$Z>!R2=v# zrRWzqRGvS2x8C0#zIN{nZ!;>3=WI1HRU_8~UBt=$`Vo_&?VufA1Z6}TEw`->ud=}= zHtSmuv{44+C(Y8Kk?8!Mcv5~l zYtcY~MA)6piH4RuhB2h=SkNO_Vs+D}9eqZICFR1UINqC;RFN1+3WcOT3gg}NS4#@k+M{`yXM)F#8 zDP*26oei;jMd#*3-lfr8v9jvR?8pLYg}r4h>#QWXL7dw8xE#r%fvKW9xe-%Aeea}+ zH2uq@aKdPH<7Q}fEVvkpu2s~j^!=h#O$=6N@iD9^Mw$J|I?z`$Q*|}&UCrHS&g5VF z3Gx*;Ig0pw9e=WYeCgN=@^OPg^!sie9a$Xg$Uce#WIF{Tc1l33E%&_2^_*LC$b_MfJS95E7D!eDJm^r<$}aghHd*a`o{< z8f$*^d`cBpCkc@{46RvwXyte{@Dst=n(r0khK2W7?Z+FuSf3cK%pT#17mGh0&Ey!n zhh(1*{Xki%oO#MH?STyw^D)?agO%0*eY99}3X87}O5@pM5@fD#vXETzuIDXm=!rE% z+WKZ=%jNaPW@!I=)C~U&gi}o4aOupU8y;Qpg0zt`upAj&YOVUKzZjf7SbMt1W^Tq~wF`6M`X)LG1j&G1- zmU2pZ%UHuNha5czU$Yvi6$xKe^#? z#-J4=R~49hOl>yy3|Ozw+bYx#47N#+eXm0s-^LT&b`q}iUbh`01u^FwLAdpuaM9NnHuqocVwrEqs< z@dbzC9Dk^Obg#GP<2m{K@kVKv&6D~W3rWjJw=2e*j*L8zYUdh(onhqh1qmQHaEjU- z)sw1I8BMd|`l5%<7lMpI`yVb>2*{lvHZ6MIebEiwrye`rI7{-AGiXGRiR0pN`~74$ zPDr)sc=(&AGD@jQ=O1bs?>7$Ks>#jOd-@cs()LL;)(mb9AN1?|#EqNBVsib%jlT`?oP}8Kl zcUU>5y40&khmE|-x%IEva@Y#m>M#h158*Z)S08RA8jbe0^o@ReK;!AQfa{ zk3HdAN}UAz$?BS=V@qO}CN9-I9N&A;_&2eu<}{nd=UUZTp5#N^nf?-YWfv5 zbptmo)MhQ`wVEC#{aBH=5FyJnLnnuKXIpa)#Vkg1buYa=zVtl(K4dk=|EnY<<=N5B zs#D3ckixvp%{Tb9n478FV%qdqj6FE&H$-k8ZT{%UHk>fl^H>w(s8>cmoVHM|6#cgP zAIAs$LE;W78Wp5t$NYl;GRBDyDj^}^jrGJ;v*9%leX}E3s^QwY5BaM1S0f*U(im{2 ztrX2&>1uj8I4{KYpNAcT*8V<_hUj4tB)bRkqxD;>`{hi1$UgS|Oi_DYoUnr>WyXfL zx-{jEA|WABwo8wIEv6wiVf^>x|5{Of5TyF?v0IwppBIsTuP%P0Son~R=@?%6|6rE? zevAW<9gD!aO(y-1>(alAL_&GQ1&)*RQ11@@FXoOYw^bUPYR9>zPyE;0|8eCDZfI@_ XnE3Fpk$QUw0{+QKD@m0|8bAL(gTwUE literal 0 HcmV?d00001 diff --git a/src/lib.rs b/src/lib.rs index 691450f..e5e8c79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,9 @@ pub mod auth; pub mod markdown; pub mod notes; pub mod pages; +pub mod quotes; pub mod routes; pub mod setup; pub mod syntax; pub mod themes; +pub mod weather; diff --git a/src/main.rs b/src/main.rs index eacfa36..7963bac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use clap::Parser; use eternalibre_notes::routes::{self, AppState}; -use eternalibre_notes::{auth, notes, setup, syntax}; +use eternalibre_notes::{auth, notes, quotes::QuoteStore, setup, syntax, weather::WeatherService}; use std::net::SocketAddr; #[derive(Parser, Debug)] @@ -61,6 +61,11 @@ async fn main() { let state = AppState { auth, base_path: base_path.into(), + weather: std::sync::Arc::new(WeatherService::new( + notes::notes_dir().join(".weather_location"), + notes::notes_dir().join(".weather_units"), + )), + quotes: std::sync::Arc::new(QuoteStore::new(notes::notes_dir().join(".quotes.json"))), }; let base = state.base_path.to_string(); diff --git a/src/pages.rs b/src/pages.rs index 90095d9..aaaefbf 100644 --- a/src/pages.rs +++ b/src/pages.rs @@ -1,6 +1,8 @@ use crate::markdown::render_markdown; use crate::notes::Note; +use crate::quotes::Quote; use crate::themes::{get_theme, PREDEFINED_THEMES}; +use crate::weather::{TempUnit, WeatherDisplay}; use chrono::{DateTime, Utc}; use maud::{html, Markup, PreEscaped, DOCTYPE}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; @@ -43,17 +45,26 @@ pub struct FolderContext<'a> { pub active_folder: Option<&'a str>, } +/// Per-request data that varies with the current app page. +pub struct AppPageContext<'a> { + pub current_note: Option<&'a Note>, + pub recent_notes: &'a [Note], + pub weather: &'a WeatherDisplay, + pub quotes: &'a [Quote], + pub query: Option<&'a str>, + pub message: Option<&'a str>, +} + pub fn render_app( theme_name: &str, notes: &[Note], folder_ctx: &FolderContext<'_>, - current_note: Option<&Note>, - query: Option<&str>, - message: Option<&str>, + page: &AppPageContext<'_>, base: &Base<'_>, ) -> Markup { let theme = get_theme(theme_name); let css = theme.to_css_block(); + let current_note = page.current_note; let rendered_preview = current_note .map(|n| render_markdown(&n.content, theme)) .unwrap_or_default(); @@ -65,6 +76,10 @@ pub fn render_app( } else { "notes" }; + // The welcome state has nothing to preview, so the preview pane is not + // rendered at all and the layout drops to two columns. + let has_note = current_note.is_some(); + let body_class = if has_note { "" } else { " no-preview" }; html! { (DOCTYPE) @@ -81,16 +96,16 @@ pub fn render_app( (render_menu_bar(base, current_note, folder_ctx.folders)) (render_toolbar( current_note, - query, + page.query, theme_name, folder_ctx.active_folder, base, )) - @if let Some(msg) = message { + @if let Some(msg) = page.message { div class="flash-message" { "Error: " (msg) } } - (render_tab_bar(default_tab)) - main class="app-body" + (render_tab_bar(default_tab, has_note)) + main class={"app-body" (body_class)} x-bind:class="{ 'hide-editor': !editorVisible, 'hide-preview': !previewVisible }" { (render_sidebar( notes, @@ -100,8 +115,16 @@ pub fn render_app( default_tab, base, )) - (render_editor(current_note, folder_ctx.folders, default_tab, base)) - (render_preview(&rendered_preview, default_tab)) + (render_editor( + current_note, + folder_ctx.folders, + page.recent_notes, + page.weather, + page.quotes, + default_tab, + base, + )) + (render_preview(&rendered_preview, default_tab, has_note)) } (render_status_bar(notes.len(), theme_name)) script { @@ -122,7 +145,7 @@ fn escape_js(s: &str) -> String { .replace('\r', "\\r") } -fn render_tab_bar(default_tab: &str) -> Markup { +fn render_tab_bar(default_tab: &str, has_note: bool) -> Markup { html! { nav class="tab-bar" { button type="button" class="tab-button" data-tab-target="notes" @@ -137,11 +160,13 @@ fn render_tab_bar(default_tab: &str) -> Markup { span class="tab-icon" { "\u{270E}" } span { "Write" } } - button type="button" class="tab-button" data-tab-target="preview" - x-on:click="setTab('preview')" - x-bind:class="{ 'active': tab === 'preview' }" { - span class="tab-icon" { "\u{1F4C4}" } - span { "Preview" } + @if has_note { + button type="button" class="tab-button" data-tab-target="preview" + x-on:click="setTab('preview')" + x-bind:class="{ 'active': tab === 'preview' }" { + span class="tab-icon" { "\u{1F4C4}" } + span { "Preview" } + } } script { (PreEscaped(format!( @@ -154,6 +179,9 @@ fn render_tab_bar(default_tab: &str) -> Markup { } fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[String]) -> Markup { + let theme_return = theme_return_path(current_note); + let weather_href = dedicated_page_href(base, "/weather", current_note); + let welcome_href = dedicated_page_href(base, "/welcome", current_note); html! { nav class="menu-bar" { ul class="menu-list" { @@ -280,11 +308,15 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri "Toggle Editor" } } - li class="desktop-only" { - button x-on:click="togglePreview()" - x-bind:class="{ 'active-view': !previewVisible }" - x-text="previewVisible ? 'Hide Preview' : 'Show Preview'" { - "Toggle Preview" + // No preview pane exists without an open note, so the + // toggle would only offer an empty view. + @if current_note.is_some() { + li class="desktop-only" { + button x-on:click="togglePreview()" + x-bind:class="{ 'active-view': !previewVisible }" + x-text="previewVisible ? 'Hide Preview' : 'Show Preview'" { + "Toggle Preview" + } } } // Available at every width. The folder nav is a sidebar @@ -298,6 +330,15 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri "Toggle Folder Nav" } } + // Mobile-only shortcuts to the dedicated pages. Desktop + // already shows the full dashboard, so these entries + // stay out of the way there; see `.mobile-only`. + li class="mobile-only" { + a href=(welcome_href) { "Welcome" } + } + li class="mobile-only" { + a href=(weather_href) { "Weather" } + } } } li class="menu-item" tabindex="0" @@ -309,6 +350,7 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri li { form action=(base.url("/theme")) method="post" { input type="hidden" name="theme" value=(theme.name); + input type="hidden" name="return_to" value=(theme_return); button type="submit" { (theme.name) } } } @@ -347,6 +389,7 @@ fn render_toolbar( active_folder: Option<&str>, base: &Base<'_>, ) -> Markup { + let theme_return = theme_return_path(current_note); html! { div class="toolbar" { div class="toolbar-group" { @@ -381,6 +424,7 @@ fn render_toolbar( } div class="toolbar-group" { form action=(base.url("/theme")) method="post" class="theme-form" x-on:change="$event.target.form.submit()" { + input type="hidden" name="return_to" value=(theme_return); label { "Theme:" } select name="theme" { @for theme in PREDEFINED_THEMES { @@ -544,6 +588,9 @@ fn move_select_id(note: &Note) -> String { fn render_editor( current_note: Option<&Note>, folders: &[String], + recent_notes: &[Note], + weather: &WeatherDisplay, + quotes: &[Quote], default_tab: &str, base: &Base<'_>, ) -> Markup { @@ -629,25 +676,355 @@ fn render_editor( } } } @else { - div class="empty-state" { - p { "Select a note or create a new one." } + (render_welcome(recent_notes, weather, quotes, base)) + } + } + } +} + +fn theme_return_path(current_note: Option<&Note>) -> String { + current_note + .map(|note| format!("/notes/{}", note.id)) + .unwrap_or_else(|| "/".to_string()) +} + +fn dedicated_page_href(base: &Base<'_>, path: &str, current_note: Option<&Note>) -> String { + let target = theme_return_path(current_note); + let encoded = utf8_percent_encode(&target, NON_ALPHANUMERIC).to_string(); + base.url(&format!("{path}?return_to={encoded}")) +} + +fn render_welcome_heading(quotes: &[Quote]) -> Markup { + html! { + header class="welcome-heading" { + p class="welcome-eyebrow" { "YOUR NOTES, YOUR SPACE" } + h1 id="welcome-title" { "Welcome to EternaLibre Notes" } + p class="welcome-tagline" { "A quiet place for ideas, plans, and everything worth keeping." } + (render_quote_rotator(quotes)) + } + } +} + +fn render_recent_card(recent_notes: &[Note], base: &Base<'_>) -> Markup { + html! { + section class="welcome-card recent-card" aria-labelledby="recent-notes-title" { + h2 id="recent-notes-title" { "Recently updated" } + @if recent_notes.is_empty() { + p class="welcome-muted" { "Your notes will show up here. Create one whenever you’re ready." } + } @else { + ul class="welcome-recent-list" { + @for note in recent_notes.iter().take(5) { + li { + a href=(base.url(&format!("/notes/{}", note.id))) { + span class="welcome-note-title" { (note.title) } + span class="welcome-note-meta" { (relative_time(note.updated_at)) } + @let preview = note.preview(100); + @if !preview.is_empty() { + span class="welcome-note-preview" { (preview) } + } + } + } + } } } } } } -fn render_preview(html_content: &str, default_tab: &str) -> Markup { - let active = if default_tab == "preview" { - " mobile-active" - } else { - "" - }; +fn render_welcome( + recent_notes: &[Note], + weather: &WeatherDisplay, + quotes: &[Quote], + base: &Base<'_>, +) -> Markup { html! { - section class={"preview-pane" (active)} data-tab-pane="preview" - x-bind:class="{ 'mobile-active': tab === 'preview' }" { - div id="preview" class="markdown-body" x-html="previewHtml" { - (PreEscaped(html_content)) + section class="welcome-state" aria-labelledby="welcome-title" { + (render_welcome_heading(quotes)) + div class="welcome-columns" { + (render_recent_card(recent_notes, base)) + section class="welcome-card weather-card" aria-labelledby="weather-title" { + h2 id="weather-title" { "Local weather" } + (render_weather_table(weather)) + (render_weather_form(weather, base, "weather-location")) + } + } + (render_quotes_manager(quotes, base)) + } + } +} + +/// Rotating quote under the welcome tagline. The first quote is server-rendered +/// so the block reads sensibly with JavaScript disabled; `app.js` swaps in the +/// rest on a timer when more than one quote exists. +fn render_quote_rotator(quotes: &[Quote]) -> Markup { + html! { + @if quotes.is_empty() { + } @else { + div class="welcome-quotes" { + blockquote id="welcome-quote" class="welcome-quote" aria-live="polite" { + p id="welcome-quote-text" class="welcome-quote-text" { (quotes[0].text) } + @if !quotes[0].author.trim().is_empty() { + footer { + "— " span id="welcome-quote-author" { (quotes[0].author) } + } + } @else { + footer { + "— " span id="welcome-quote-author" { "Anonymous" } + } + } + } + @if quotes.len() > 1 { + div class="welcome-quote-controls" { + button type="button" id="quote-prev" class="btn btn-small" { "‹ Prev" } + button type="button" id="quote-pause" class="btn btn-small" aria-pressed="false" { "Pause" } + button type="button" id="quote-next" class="btn btn-small" { "Next ›" } + } + } + script { (PreEscaped(welcome_quotes_js(quotes))) } + } + } + } +} + +fn welcome_quotes_js(quotes: &[Quote]) -> String { + // JSON keeps user-supplied quotes intact; escaping ` Markup { + html! { + @if let Some(location) = weather.location.as_deref() { + @if weather.current.is_none() && weather.days.is_empty() { + p class="welcome-muted" { "Weather is temporarily unavailable." } + } @else { + div class="weather-table-wrapper" { + table class="weather-table" { + caption class="weather-caption" { + "Weather for " (location) " (" (weather.units.label()) ")" + } + thead { + tr { + th scope="col" { "Day" } + th scope="col" { "Conditions" } + th scope="col" { "Temp" } + th scope="col" { "High" } + th scope="col" { "Low" } + } + } + tbody { + @if let Some(now) = weather.current.as_ref() { + tr class="weather-now-row" { + th scope="row" { "Now" } + td { (now.desc) } + td { (now.temp(weather.units)) } + td class="weather-na" { "—" } + td class="weather-na" { "—" } + } + } + @for day in &weather.days { + tr { + th scope="row" { (day.short_date()) } + td { (day.desc) } + td { (day.avg(weather.units)) } + td { (day.high(weather.units)) } + td { (day.low(weather.units)) } + } + } + } + } + } + @if let Some(now) = weather.current.as_ref() { + p class="weather-details" { + "Feels like " (now.feels(weather.units)) " · Humidity " (now.humidity) "%" + } + } + } + p class="weather-location" { "Location: " (location) } + } @else { + p class="welcome-muted" { "Set a location to see current weather from wttr.in." } + } + } +} + +fn render_weather_form(weather: &WeatherDisplay, base: &Base<'_>, id: &str) -> Markup { + let units = weather.units; + html! { + form action=(base.url("/settings/weather")) method="post" class="weather-location-form" { + label for=(id) { "Location" } + input + id=(id) + type="text" + name="location" + maxlength="100" + placeholder="City or place name" + value=[weather.location.as_deref()]; + fieldset class="weather-units" { + legend { "Units" } + label { + input + type="radio" + name="units" + value="c" + checked[units == TempUnit::Celsius]; + " Celsius" + } + label { + input + type="radio" + name="units" + value="f" + checked[units == TempUnit::Fahrenheit]; + " Fahrenheit" + } + } + button type="submit" class="btn" { "Save location" } + p class="weather-privacy" { + "The server sends this location to wttr.in to retrieve weather. Leave it blank to clear it." + } + } + } +} + +fn render_quotes_manager(quotes: &[Quote], base: &Base<'_>) -> Markup { + html! { + section class="welcome-card quotes-card" aria-labelledby="quotes-title" { + h2 id="quotes-title" { "Quotes" } + p class="welcome-muted" { + "Add quotes to rotate beneath the welcome message. One submission adds one quote." + } + form action=(base.url("/settings/quotes")) method="post" class="quote-form" { + label for="quote-text" { "Quote" } + textarea + id="quote-text" + name="quote" + maxlength="500" + rows="3" + placeholder="Write something worth re-reading…" + required="required" {} + label for="quote-author" { "Author" } + input + id="quote-author" + type="text" + name="author" + maxlength="100" + placeholder="Who said it?"; + button type="submit" class="btn" { "Add quote" } + } + @if quotes.is_empty() { + p class="welcome-muted" { "No quotes yet. The rotator appears once you add one." } + } @else { + ul class="quote-list" { + @for (index, quote) in quotes.iter().enumerate() { + li class="quote-item" { + blockquote class="quote-item-body" { + p { (quote.text) } + @if !quote.author.trim().is_empty() { + footer { "— " (quote.author) } + } + } + form action=(base.url("/settings/quotes/delete")) method="post" class="quote-delete-form" { + input type="hidden" name="index" value=(index); + button type="submit" class="btn btn-small" { "Remove" } + } + } + } + } + } + } + } +} + +pub fn render_weather_page( + theme_name: &str, + weather: &WeatherDisplay, + return_to: &str, + base: &Base<'_>, +) -> Markup { + let theme = get_theme(theme_name); + html! { + (DOCTYPE) + html lang="en" data-theme=(theme_name) { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"; + title { "Weather - EternaLibre Notes" } + style { (PreEscaped(theme.to_css_block())) } + style { (PreEscaped(include_str!("../static/style.css"))) } + script defer src=(base.url("/static/alpine.min.js")) {} + } + body class="weather-page" x-data="app()" x-on:keydown="handleKeydown($event)" { + (render_menu_bar(base, None, &[])) + main class="weather-page-main" { + p class="welcome-eyebrow" { "MOBILE WEATHER" } + h1 { "Weather" } + section class="welcome-card weather-card" aria-labelledby="weather-page-title" { + h2 id="weather-page-title" { "Local weather" } + (render_weather_table(weather)) + (render_weather_form(weather, base, "weather-page-location")) + } + a class="back-link" href=(base.url(return_to)) { "Back" } + } + (render_status_bar(0, theme_name)) + script { + "window.__INITIAL_PREVIEW__ = ``;" + "window.__BASE_PATH__ = `" (PreEscaped(escape_js(base.prefix()))) "`;" + (PreEscaped(include_str!("../static/app.js"))) + } + } + } + } +} + +pub fn render_welcome_page( + theme_name: &str, + recent_notes: &[Note], + quotes: &[Quote], + return_to: &str, + base: &Base<'_>, +) -> Markup { + let theme = get_theme(theme_name); + html! { + (DOCTYPE) + html lang="en" data-theme=(theme_name) { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"; + title { "Welcome - EternaLibre Notes" } + style { (PreEscaped(theme.to_css_block())) } + style { (PreEscaped(include_str!("../static/style.css"))) } + script defer src=(base.url("/static/alpine.min.js")) {} + } + body class="welcome-page" x-data="app()" x-on:keydown="handleKeydown($event)" { + (render_menu_bar(base, None, &[])) + main class="welcome-page-main welcome-state" { + (render_welcome_heading(quotes)) + (render_recent_card(recent_notes, base)) + a class="back-link" href=(base.url(return_to)) { "Back" } + } + (render_status_bar(recent_notes.len(), theme_name)) + script { + "window.__INITIAL_PREVIEW__ = ``;" + "window.__BASE_PATH__ = `" (PreEscaped(escape_js(base.prefix()))) "`;" + (PreEscaped(include_str!("../static/app.js"))) + } + } + } + } +} + +fn render_preview(html_content: &str, default_tab: &str, has_note: bool) -> Markup { + html! { + @if has_note { + section class={"preview-pane" (if default_tab == "preview" { " mobile-active" } else { "" })} data-tab-pane="preview" + x-bind:class="{ 'mobile-active': tab === 'preview' }" { + div id="preview" class="markdown-body" x-html="previewHtml" { + (PreEscaped(html_content)) + } } } } @@ -817,12 +1194,12 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup { p { "A server-side rendered, copyleft Markdown notes application written in Rust. \ A toolkit for making a private notes cloud: run the server yourself, \ - on your own hardware, and the notes go nowhere else." + on your own hardware, with notes kept in local Markdown files." } p { "The server binds to all interfaces by default, so you can reach your notes \ from a phone or another machine on your network. It is protected by a TOTP \ - code from your authenticator app, and it never talks to a third party." + code from your authenticator app. Optional weather sends only the location you configure to wttr.in." } ul class="about-features" { // Counted from the palette itself, and split by @@ -840,7 +1217,7 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup { } li { "Syntax highlighting for fenced code blocks" } li { "Notes stored as plain Markdown files on disk" } - li { "TOTP-locked, with no accounts or third-party services" } + li { "TOTP-locked, with no accounts or telemetry; weather lookup is optional" } li { "Note editing, search, and theming work without JavaScript" } li { "Export saved notes as Markdown, themed HTML, or PDF via the browser print dialog" } } @@ -875,8 +1252,8 @@ pub fn render_about(theme_name: &str, base: &Base<'_>) -> Markup { } p class="about-colophon" { (format!( - "Version {}. Licensed under the GNU General Public \ - License v3.0 or later. No telemetry, no accounts, no vendor.", + "Version {}. Licensed under the GNU General Public \ + License v3.0 or later. No telemetry or accounts; weather is opt-in.", env!("CARGO_PKG_VERSION"), )) } diff --git a/src/quotes.rs b/src/quotes.rs new file mode 100644 index 0000000..f05d83f --- /dev/null +++ b/src/quotes.rs @@ -0,0 +1,158 @@ +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +const MAX_QUOTE_LENGTH: usize = 500; +const MAX_AUTHOR_LENGTH: usize = 100; +const MAX_QUOTES: usize = 50; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Quote { + pub text: String, + pub author: String, +} + +pub struct QuoteStore { + path: PathBuf, +} + +impl QuoteStore { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + /// All saved quotes in insertion order. Missing or unreadable storage + /// reads as empty so the welcome page still renders; the parse error is + /// not hidden from writers, only from readers. + pub fn list(&self) -> Vec { + match std::fs::read_to_string(&self.path) { + Ok(body) => { + if body.trim().is_empty() { + Vec::new() + } else { + serde_json::from_str(&body).unwrap_or_default() + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Vec::new(), + Err(_) => Vec::new(), + } + } + + pub fn add(&self, text: &str, author: &str) -> Result { + let text = text.trim(); + let author = author.trim(); + if text.is_empty() { + return Err(anyhow!("Quote cannot be blank.")); + } + if text.chars().count() > MAX_QUOTE_LENGTH { + return Err(anyhow!( + "Quote must be at most {MAX_QUOTE_LENGTH} characters." + )); + } + if author.chars().count() > MAX_AUTHOR_LENGTH { + return Err(anyhow!( + "Author must be at most {MAX_AUTHOR_LENGTH} characters." + )); + } + if text.chars().any(char::is_control) || author.chars().any(char::is_control) { + return Err(anyhow!("Quotes cannot contain control characters.")); + } + + let mut quotes = self.list(); + if quotes.len() >= MAX_QUOTES { + return Err(anyhow!("Quote list is full ({MAX_QUOTES} quotes).")); + } + let quote = Quote { + text: text.to_string(), + author: author.to_string(), + }; + quotes.push(quote.clone()); + self.write("es)?; + Ok(quote) + } + + pub fn remove(&self, index: usize) -> Result<()> { + let mut quotes = self.list(); + if index >= quotes.len() { + return Err(anyhow!("Quote not found.")); + } + quotes.remove(index); + if quotes.is_empty() { + match std::fs::remove_file(&self.path) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err).context("removing saved quotes"), + } + return Ok(()); + } + self.write("es) + } + + fn write(&self, quotes: &[Quote]) -> Result<()> { + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent).context("creating notes directory for quotes")?; + } + let body = serde_json::to_string_pretty(quotes).context("encoding quotes")?; + std::fs::write(&self.path, body).context("saving quotes")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_path(tag: &str) -> PathBuf { + let mut p = std::env::temp_dir(); + p.push(format!( + "eternalibre-quotes-{tag}-{}-{}", + std::process::id(), + uuid::Uuid::new_v4() + )); + p.set_extension("json"); + let _ = std::fs::remove_file(&p); + p + } + + #[test] + fn missing_storage_lists_no_quotes() { + let store = QuoteStore::new(temp_path("missing")); + assert!(store.list().is_empty()); + } + + #[test] + fn add_persists_quote_and_author() { + let path = temp_path("add"); + let store = QuoteStore::new(path.clone()); + store.add("Stay curious.", "Ada").unwrap(); + let quotes = store.list(); + assert_eq!(quotes.len(), 1); + assert_eq!(quotes[0].text, "Stay curious."); + assert_eq!(quotes[0].author, "Ada"); + let _ = std::fs::remove_file(path); + } + + #[test] + fn add_rejects_blank_and_oversized_quotes() { + let store = QuoteStore::new(temp_path("invalid")); + assert!(store.add(" ", "Someone").is_err()); + assert!(store.add(&"x".repeat(MAX_QUOTE_LENGTH + 1), "").is_err()); + assert!(store + .add("Fine", &"y".repeat(MAX_AUTHOR_LENGTH + 1)) + .is_err()); + } + + #[test] + fn remove_deletes_by_index_and_clears_the_file() { + let path = temp_path("remove"); + let store = QuoteStore::new(path.clone()); + store.add("First", "A").unwrap(); + store.add("Second", "B").unwrap(); + store.remove(0).unwrap(); + assert_eq!(store.list().len(), 1); + store.remove(0).unwrap(); + assert!(store.list().is_empty()); + assert!(!path.exists()); + assert!(store.remove(0).is_err()); + } +} diff --git a/src/routes.rs b/src/routes.rs index b084cc6..8cd75c4 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -12,8 +12,10 @@ use std::sync::Arc; use crate::auth::{AuthError, AuthState, SESSION_COOKIE}; use crate::markdown::render_markdown; use crate::notes::{self, Note}; -use crate::pages::{self, Base, FolderContext}; +use crate::pages::{self, AppPageContext, Base, FolderContext}; +use crate::quotes::{Quote, QuoteStore}; use crate::themes::get_theme; +use crate::weather::{TempUnit, WeatherDisplay, WeatherService}; #[derive(Clone)] pub struct AppState { @@ -22,6 +24,8 @@ pub struct AppState { /// Empty means mounted at the root, which is the default and the only /// configuration most installs need. pub base_path: Arc, + pub weather: Arc, + pub quotes: Arc, } impl AppState { @@ -82,6 +86,11 @@ mod tests { let root = AppState { auth: Arc::new(AuthState::with_secret_path("/nonexistent-test-secret")), base_path: "".into(), + weather: Arc::new(WeatherService::new( + "/nonexistent-weather-location".into(), + "/nonexistent-weather-units".into(), + )), + quotes: Arc::new(QuoteStore::new("/nonexistent-quotes".into())), }; assert_eq!(root.url("/"), "/"); assert_eq!(root.url("/about"), "/about"); @@ -89,6 +98,8 @@ mod tests { let mounted = AppState { auth: root.auth.clone(), base_path: "/forgejo".into(), + weather: root.weather.clone(), + quotes: root.quotes.clone(), }; assert_eq!(mounted.url("/"), "/forgejo/"); assert_eq!(mounted.url("/about"), "/forgejo/about"); @@ -122,6 +133,11 @@ pub fn router(state: AppState) -> Router { .route("/notes/:id/move", post(move_note_route)) .route("/preview", post(preview_route)) .route("/theme", post(theme_route)) + .route("/settings/weather", post(weather_settings_route)) + .route("/settings/quotes", post(quotes_add_route)) + .route("/settings/quotes/delete", post(quotes_delete_route)) + .route("/weather", get(weather_page_route)) + .route("/welcome", get(welcome_page_route)) .route("/about", get(about_route)) .route("/export", get(export_route)) .route( @@ -338,6 +354,8 @@ async fn index( headers: HeaderMap, ) -> Result { let theme = get_theme_from_cookie(&headers); + let weather = state.weather.display().await; + let quotes = state.quotes.list(); let all_notes = notes::list_notes()?; let folders = notes::list_folders()?; @@ -359,21 +377,22 @@ async fn index( } else { all_notes }; + let recent_notes: Vec = notes.iter().take(5).cloned().collect(); let base = Base::new(&state.base_path); let folder_ctx = FolderContext { folders: &folders, active_folder: active_folder.as_deref(), }; - let html = pages::render_app( - &theme, - ¬es, - &folder_ctx, - None, - query.q.as_deref(), - query.error.as_deref(), - &base, - ); + let page = AppPageContext { + current_note: None, + recent_notes: &recent_notes, + weather: &weather, + quotes: "es, + query: query.q.as_deref(), + message: query.error.as_deref(), + }; + let html = pages::render_app(&theme, ¬es, &folder_ctx, &page, &base); Ok(Html(html.into_string())) } @@ -408,21 +427,24 @@ async fn show_note_route( } else { all_notes }; + let recent_notes: Vec = notes.iter().take(5).cloned().collect(); let base = Base::new(&state.base_path); let folder_ctx = FolderContext { folders: &folders, active_folder: active_folder.as_deref(), }; - let html = pages::render_app( - &theme, - ¬es, - &folder_ctx, - Some(¬e), - query.q.as_deref(), - None, - &base, - ); + let weather = WeatherDisplay::empty(); + let no_quotes: Vec = Vec::new(); + let page = AppPageContext { + current_note: Some(¬e), + recent_notes: &recent_notes, + weather: &weather, + quotes: &no_quotes, + query: query.q.as_deref(), + message: None, + }; + let html = pages::render_app(&theme, ¬es, &folder_ctx, &page, &base); Ok(Html(html.into_string())) } @@ -561,6 +583,8 @@ async fn preview_route( #[derive(Deserialize)] struct ThemeForm { theme: String, + #[serde(default)] + return_to: String, } async fn theme_route( @@ -570,7 +594,8 @@ async fn theme_route( // Validate theme name let _ = get_theme(&form.theme); - let mut response = Redirect::to(&state.url("/")).into_response(); + let return_path = sanitize_return_path(&form.return_to); + let mut response = Redirect::to(&state.url(&return_path)).into_response(); let cookie = format!( "theme={}; Path={}; Max-Age=31536000; SameSite=Lax", form.theme, @@ -582,6 +607,144 @@ async fn theme_route( Ok(response) } +#[derive(Deserialize)] +struct WeatherLocationForm { + location: String, + #[serde(default)] + units: Option, +} + +async fn weather_settings_route( + State(state): State, + Form(form): Form, +) -> Result { + if let Some(units) = form.units { + if TempUnit::parse(&units).is_none() { + return Ok( + error_redirect(&state, "Units must be Celsius or Fahrenheit.").into_response(), + ); + } + if let Err(err) = state.weather.set_units(&units).await { + return Ok(error_redirect(&state, &err.to_string()).into_response()); + } + } + if let Err(err) = state.weather.set_location(&form.location).await { + return Ok(error_redirect(&state, &err.to_string()).into_response()); + } + Ok(Redirect::to(&state.url("/")).into_response()) +} + +#[derive(Deserialize)] +struct QuoteForm { + quote: String, + #[serde(default)] + author: String, +} + +async fn quotes_add_route( + State(state): State, + Form(form): Form, +) -> Result { + if let Err(err) = state.quotes.add(&form.quote, &form.author) { + return Ok(error_redirect(&state, &err.to_string()).into_response()); + } + Ok(Redirect::to(&state.url("/")).into_response()) +} + +#[derive(Deserialize)] +struct QuoteDeleteForm { + index: usize, +} + +async fn quotes_delete_route( + State(state): State, + Form(form): Form, +) -> Result { + if let Err(err) = state.quotes.remove(form.index) { + return Ok(error_redirect(&state, &err.to_string()).into_response()); + } + Ok(Redirect::to(&state.url("/")).into_response()) +} + +#[derive(Deserialize)] +struct WeatherPageQuery { + #[serde(default)] + return_to: Option, +} + +async fn weather_page_route( + State(state): State, + Query(query): Query, + headers: HeaderMap, +) -> Result { + let theme = get_theme_from_cookie(&headers); + let weather = state.weather.display().await; + let return_to = sanitize_return_path(query.return_to.as_deref().unwrap_or("/")); + let base = Base::new(&state.base_path); + let html = pages::render_weather_page(&theme, &weather, &return_to, &base); + Ok(Html(html.into_string())) +} + +#[derive(Deserialize)] +struct WelcomePageQuery { + #[serde(default)] + q: Option, + #[serde(default)] + folder: Option, + #[serde(default)] + return_to: Option, +} + +async fn welcome_page_route( + State(state): State, + Query(query): Query, + headers: HeaderMap, +) -> Result { + let theme = get_theme_from_cookie(&headers); + let all_notes = notes::list_notes()?; + let active_folder = query.folder.clone().filter(|f| !f.is_empty()); + let notes: Vec = if let Some(ref q) = query.q { + let q = q.to_lowercase(); + all_notes + .into_iter() + .filter(|n| { + n.title.to_lowercase().contains(&q) || n.content.to_lowercase().contains(&q) + }) + .collect() + } else if let Some(ref f) = active_folder { + all_notes + .into_iter() + .filter(|n| n.folder.as_deref() == Some(f.as_str())) + .collect() + } else { + all_notes + }; + let recent_notes: Vec = notes.iter().take(5).cloned().collect(); + let quotes = state.quotes.list(); + let return_to = sanitize_return_path(query.return_to.as_deref().unwrap_or("/")); + let base = Base::new(&state.base_path); + let html = pages::render_welcome_page(&theme, &recent_notes, "es, &return_to, &base); + Ok(Html(html.into_string())) +} + +/// Shared guard for client-controlled return targets. Only the app root and +/// canonical `/notes/` paths survive; everything else falls back to `/` +/// so a crafted form or query string cannot turn a redirect or link into an +/// open redirect. +fn sanitize_return_path(raw: &str) -> String { + if raw == "/" { + "/".to_string() + } else if let Some(id) = raw.strip_prefix("/notes/") { + if uuid::Uuid::parse_str(id).is_ok_and(|uuid| uuid.hyphenated().to_string() == id) { + raw.to_string() + } else { + "/".to_string() + } + } else { + "/".to_string() + } +} + async fn about_route( State(state): State, headers: HeaderMap, diff --git a/src/weather.rs b/src/weather.rs new file mode 100644 index 0000000..19553e9 --- /dev/null +++ b/src/weather.rs @@ -0,0 +1,571 @@ +use anyhow::{anyhow, Context, Result}; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time::{Duration, Instant}; +use tokio::sync::Mutex; + +const CACHE_TTL: Duration = Duration::from_secs(15 * 60); +const REQUEST_TIMEOUT: Duration = Duration::from_secs(4); +const MAX_LOCATION_LENGTH: usize = 100; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum TempUnit { + #[default] + Celsius, + Fahrenheit, +} + +impl TempUnit { + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "c" | "celsius" => Some(TempUnit::Celsius), + "f" | "fahrenheit" => Some(TempUnit::Fahrenheit), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + TempUnit::Celsius => "c", + TempUnit::Fahrenheit => "f", + } + } + + pub fn label(&self) -> &'static str { + match self { + TempUnit::Celsius => "°C", + TempUnit::Fahrenheit => "°F", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeatherCurrent { + pub desc: String, + pub temp_c: String, + pub temp_f: String, + pub feels_c: String, + pub feels_f: String, + pub humidity: String, +} + +impl WeatherCurrent { + pub fn temp(&self, units: TempUnit) -> String { + match units { + TempUnit::Celsius => format!("{}°C", self.temp_c), + TempUnit::Fahrenheit => format!("{}°F", self.temp_f), + } + } + + pub fn feels(&self, units: TempUnit) -> String { + match units { + TempUnit::Celsius => format!("{}°C", self.feels_c), + TempUnit::Fahrenheit => format!("{}°F", self.feels_f), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeatherDay { + pub date: String, + pub desc: String, + pub avg_c: String, + pub avg_f: String, + pub max_c: String, + pub max_f: String, + pub min_c: String, + pub min_f: String, +} + +impl WeatherDay { + pub fn avg(&self, units: TempUnit) -> String { + match units { + TempUnit::Celsius => format!("{}°C", self.avg_c), + TempUnit::Fahrenheit => format!("{}°F", self.avg_f), + } + } + + pub fn high(&self, units: TempUnit) -> String { + match units { + TempUnit::Celsius => format!("{}°C", self.max_c), + TempUnit::Fahrenheit => format!("{}°F", self.max_f), + } + } + + pub fn low(&self, units: TempUnit) -> String { + match units { + TempUnit::Celsius => format!("{}°C", self.min_c), + TempUnit::Fahrenheit => format!("{}°F", self.min_f), + } + } + + /// Human-friendly day label: `2026-10-12` becomes `Oct 12`. + /// Falls back to the raw value when it is not a calendar date. + pub fn short_date(&self) -> String { + if let Ok(date) = chrono::NaiveDate::parse_from_str(&self.date, "%Y-%m-%d") { + date.format("%b %d").to_string() + } else { + self.date.clone() + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeatherDisplay { + pub location: Option, + pub units: TempUnit, + pub current: Option, + pub days: Vec, +} + +impl WeatherDisplay { + pub fn empty() -> Self { + Self { + location: None, + units: TempUnit::Celsius, + current: None, + days: Vec::new(), + } + } +} + +struct CachedWeather { + location: String, + current: Option, + days: Vec, + checked_at: Instant, +} + +pub struct WeatherService { + location_path: PathBuf, + units_path: PathBuf, + endpoint: String, + client: reqwest::Client, + cache: Mutex>, +} + +impl WeatherService { + pub fn new(location_path: PathBuf, units_path: PathBuf) -> Self { + Self::with_endpoint(location_path, units_path, "https://wttr.in") + } + + fn with_endpoint( + location_path: PathBuf, + units_path: PathBuf, + endpoint: impl Into, + ) -> Self { + Self { + location_path, + units_path, + endpoint: endpoint.into(), + client: reqwest::Client::builder() + .timeout(REQUEST_TIMEOUT) + .user_agent(concat!("EternaLibre-Notes/", env!("CARGO_PKG_VERSION"))) + .build() + .expect("reqwest client configuration is valid"), + cache: Mutex::new(None), + } + } + + pub async fn display(&self) -> WeatherDisplay { + let units = self.read_units(); + let location = match self.read_location() { + Ok(Some(location)) => location, + _ => { + return WeatherDisplay { + location: None, + units, + current: None, + days: Vec::new(), + } + } + }; + + let mut cache = self.cache.lock().await; + if let Some(cached) = cache.as_ref() { + if cached.location == location && cached.checked_at.elapsed() < CACHE_TTL { + return WeatherDisplay { + location: Some(location), + units, + current: cached.current.clone(), + days: cached.days.clone(), + }; + } + } + + match self.fetch(&location).await { + Ok((current, days)) => { + *cache = Some(CachedWeather { + location: location.clone(), + current: current.clone(), + days: days.clone(), + checked_at: Instant::now(), + }); + WeatherDisplay { + location: Some(location), + units, + current, + days, + } + } + Err(_) => { + // Retain a same-location report through an outage and cache + // the failed check too, so every page load does not wait for + // another timeout while wttr.in is unavailable. + let (current, days) = cache + .as_ref() + .filter(|cached| cached.location == location) + .map(|cached| (cached.current.clone(), cached.days.clone())) + .unwrap_or((None, Vec::new())); + *cache = Some(CachedWeather { + location: location.clone(), + current: current.clone(), + days: days.clone(), + checked_at: Instant::now(), + }); + WeatherDisplay { + location: Some(location), + units, + current, + days, + } + } + } + } + + pub async fn set_location(&self, value: &str) -> Result<()> { + let location = value.trim(); + if location.chars().count() > MAX_LOCATION_LENGTH { + return Err(anyhow!( + "Location must be at most {MAX_LOCATION_LENGTH} characters." + )); + } + if location.chars().any(char::is_control) { + return Err(anyhow!("Location cannot contain control characters.")); + } + + if location.is_empty() { + match tokio::fs::remove_file(&self.location_path).await { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err).context("removing saved weather location"), + } + } else { + if let Some(parent) = self.location_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .context("creating notes directory for weather setting")?; + } + tokio::fs::write(&self.location_path, location) + .await + .context("saving weather location")?; + } + + *self.cache.lock().await = None; + Ok(()) + } + + pub async fn set_units(&self, value: &str) -> Result<()> { + let units = TempUnit::parse(value) + .ok_or_else(|| anyhow!("Units must be Celsius or Fahrenheit."))?; + if let Some(parent) = self.units_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .context("creating notes directory for weather setting")?; + } + tokio::fs::write(&self.units_path, units.as_str()) + .await + .context("saving weather units")?; + // No refetch needed: the cached report keeps both scales and the + // display layer picks which one to render. + Ok(()) + } + + pub fn read_units(&self) -> TempUnit { + match std::fs::read_to_string(&self.units_path) { + Ok(value) => TempUnit::parse(&value).unwrap_or_default(), + Err(_) => TempUnit::default(), + } + } + + fn read_location(&self) -> Result> { + let value = match std::fs::read_to_string(&self.location_path) { + Ok(value) => value, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err).context("reading saved weather location"), + }; + let location = value.trim(); + if location.is_empty() { + Ok(None) + } else if location.chars().count() > MAX_LOCATION_LENGTH + || location.chars().any(char::is_control) + { + Err(anyhow!("saved weather location is invalid")) + } else { + Ok(Some(location.to_string())) + } + } + + async fn fetch(&self, location: &str) -> Result<(Option, Vec)> { + let encoded = utf8_percent_encode(location, NON_ALPHANUMERIC); + let url = format!( + "{}/{encoded}?format=j1", + self.endpoint.trim_end_matches('/') + ); + let response = self.client.get(url).send().await?.error_for_status()?; + let body = response.text().await?; + let parsed: WttrResponse = + serde_json::from_str(&body).context("parsing wttr.in response")?; + Ok((parsed.current(), parsed.days())) + } +} + +#[derive(Debug, Deserialize)] +struct WttrResponse { + #[serde(default)] + current_condition: Vec, + #[serde(default)] + weather: Vec, +} + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +struct WttrCurrent { + #[serde(default)] + temp_C: String, + #[serde(default)] + temp_F: String, + #[serde(default)] + FeelsLikeC: String, + #[serde(default)] + FeelsLikeF: String, + #[serde(default)] + humidity: String, + #[serde(default)] + weatherDesc: Vec, +} + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +struct WttrDay { + #[serde(default)] + date: String, + #[serde(default)] + avgtempC: String, + #[serde(default)] + avgtempF: String, + #[serde(default)] + maxtempC: String, + #[serde(default)] + maxtempF: String, + #[serde(default)] + mintempC: String, + #[serde(default)] + mintempF: String, + #[serde(default)] + hourly: Vec, +} + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +struct WttrHourly { + #[serde(default)] + weatherDesc: Vec, +} + +#[derive(Debug, Deserialize)] +struct WttrValue { + #[serde(default)] + value: String, +} + +fn clean(value: &str, max: usize) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + "—".to_string() + } else if trimmed.chars().count() > max { + trimmed.chars().take(max).collect() + } else { + trimmed.to_string() + } +} + +impl WttrResponse { + fn current(&self) -> Option { + let now = self.current_condition.first()?; + Some(WeatherCurrent { + desc: clean( + now.weatherDesc + .first() + .map(|d| d.value.as_str()) + .unwrap_or(""), + 80, + ), + temp_c: clean(&now.temp_C, 12), + temp_f: clean(&now.temp_F, 12), + feels_c: clean(&now.FeelsLikeC, 12), + feels_f: clean(&now.FeelsLikeF, 12), + humidity: clean(&now.humidity, 12), + }) + } + + /// Every forecast date wttr.in returns, in order. The service currently + /// sends three dates including today, so callers must render what is + /// present rather than assuming a five-day horizon. + fn days(&self) -> Vec { + self.weather + .iter() + .map(|day| { + let desc = day + .hourly + .iter() + .filter_map(|h| h.weatherDesc.first()) + .map(|d| d.value.trim()) + .find(|d| !d.is_empty()) + .unwrap_or(""); + WeatherDay { + date: clean(&day.date, 10), + desc: clean(desc, 80), + avg_c: clean(&day.avgtempC, 12), + avg_f: clean(&day.avgtempF, 12), + max_c: clean(&day.maxtempC, 12), + max_f: clean(&day.maxtempF, 12), + min_c: clean(&day.mintempC, 12), + min_f: clean(&day.mintempF, 12), + } + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + fn temp_path(tag: &str, ext: &str) -> PathBuf { + let mut p = std::env::temp_dir(); + p.push(format!( + "eternalibre-weather-{tag}-{}-{}", + std::process::id(), + uuid::Uuid::new_v4() + )); + p.set_extension(ext); + p + } + + fn service(tag: &str, endpoint: &str) -> (WeatherService, PathBuf, PathBuf) { + let location = temp_path(tag, "location"); + let units = temp_path(tag, "units"); + let _ = std::fs::remove_file(&location); + let _ = std::fs::remove_file(&units); + ( + WeatherService::with_endpoint(location.clone(), units.clone(), endpoint), + location, + units, + ) + } + + async fn mock_server(response: String) -> (String, tokio::task::JoinHandle) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let task = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut request = [0; 4096]; + let length = stream.read(&mut request).await.unwrap(); + let request = String::from_utf8_lossy(&request[..length]).to_string(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + response.len(), + response + ); + stream.write_all(response.as_bytes()).await.unwrap(); + request + }); + (format!("http://{addr}"), task) + } + + const J1: &str = r#"{ + "current_condition": [{ + "temp_C": "15", "temp_F": "59", + "FeelsLikeC": "13", "FeelsLikeF": "55", + "humidity": "51", + "weatherDesc": [{"value": "Overcast"}] + }], + "weather": [ + {"date": "2026-10-11", "avgtempC": "12", "avgtempF": "54", + "maxtempC": "15", "maxtempF": "59", "mintempC": "9", "mintempF": "48", + "hourly": [{"weatherDesc": [{"value": "Overcast"}]}]}, + {"date": "2026-10-12", "avgtempC": "14", "avgtempF": "57", + "maxtempC": "17", "maxtempF": "62", "mintempC": "13", "mintempF": "55", + "hourly": [{"weatherDesc": [{"value": "Sunny"}]}]} + ] + }"#; + + #[tokio::test] + async fn location_is_stored_and_blank_clears_it() { + let (svc, location, _units) = service("location", "http://127.0.0.1:1"); + svc.set_location(" Reykjavík ").await.unwrap(); + assert_eq!(std::fs::read_to_string(&location).unwrap(), "Reykjavík"); + assert_eq!(svc.display().await.location.as_deref(), Some("Reykjavík")); + svc.set_location(" ").await.unwrap(); + assert!(!location.exists()); + } + + #[tokio::test] + async fn location_rejects_control_characters_and_long_values() { + let (svc, _, _) = service("invalid", "http://127.0.0.1:1"); + assert!(svc.set_location("London\nInjected").await.is_err()); + assert!(svc + .set_location(&"x".repeat(MAX_LOCATION_LENGTH + 1)) + .await + .is_err()); + } + + #[tokio::test] + async fn units_default_to_celsius_and_persist() { + let (svc, _, units) = service("units", "http://127.0.0.1:1"); + assert_eq!(svc.read_units(), TempUnit::Celsius); + svc.set_units("f").await.unwrap(); + assert_eq!(std::fs::read_to_string(&units).unwrap(), "f"); + assert_eq!(svc.read_units(), TempUnit::Fahrenheit); + assert!(svc.set_units("kelvin").await.is_err()); + } + + #[tokio::test] + async fn weather_fetch_is_cached_and_location_is_encoded() { + let (endpoint, server) = mock_server(J1.to_string()).await; + let (svc, location, _) = service("cache", &endpoint); + svc.set_location("London, UK").await.unwrap(); + + let first = svc.display().await; + assert_eq!(first.location.as_deref(), Some("London, UK")); + assert_eq!(first.units, TempUnit::Celsius); + let current = first.current.as_ref().expect("current conditions"); + assert_eq!(current.temp(TempUnit::Celsius), "15°C"); + assert_eq!(current.temp(TempUnit::Fahrenheit), "59°F"); + assert_eq!(first.days.len(), 2); + assert_eq!(first.days[0].short_date(), "Oct 11"); + assert_eq!(first.days[1].high(TempUnit::Celsius), "17°C"); + let request = server.await.unwrap(); + assert!(request.starts_with("GET /London%2C%20UK?format=j1 ")); + let second = svc.display().await; + assert_eq!(second, first); + let _ = std::fs::remove_file(location); + } + + #[tokio::test] + async fn switching_units_reuses_the_cached_report() { + let (endpoint, server) = mock_server(J1.to_string()).await; + let (svc, _, _) = service("units-cache", &endpoint); + svc.set_location("London").await.unwrap(); + let celsius = svc.display().await; + server.await.unwrap(); + svc.set_units("f").await.unwrap(); + let fahrenheit = svc.display().await; + assert_eq!(fahrenheit.units, TempUnit::Fahrenheit); + assert_eq!(fahrenheit.current, celsius.current); + assert_eq!(fahrenheit.days, celsius.days); + } +} diff --git a/static/app.js b/static/app.js index aebd8bf..2339f06 100644 --- a/static/app.js +++ b/static/app.js @@ -42,6 +42,7 @@ function app() { } this.initNoteDragging(); + this.initQuoteRotation(); }, /// Wires up dragging a sidebar note onto a folder nav item to re-file it. @@ -146,6 +147,10 @@ function app() { if (this._onDocClick) { document.removeEventListener('click', this._onDocClick); } + if (this._quoteTimer) { + clearInterval(this._quoteTimer); + this._quoteTimer = null; + } if (this._onDragStart) { document.removeEventListener('dragstart', this._onDragStart); document.removeEventListener('dragover', this._onDragOver); @@ -353,6 +358,103 @@ function app() { if (editor) editor.focus(); }, + /// Rotates the welcome quote beneath the tagline. + /// + /// The first quote is server-rendered, so without JS the block simply + /// stays put. Rotation is a progressive enhancement for sighted mouse + /// and touch users: it pauses on hover/focus, honours + /// `prefers-reduced-motion`, and exposes an explicit Pause control. + initQuoteRotation() { + const quotes = window.__WELCOME_QUOTES__ || []; + const textEl = document.getElementById('welcome-quote-text'); + const authorEl = document.getElementById('welcome-quote-author'); + if (!quotes.length || quotes.length < 2 || !textEl || !authorEl) return; + + this._quotes = quotes; + this._quoteIndex = 0; + this._quotesPaused = false; + + const show = (index) => { + const quote = this._quotes[index]; + if (!quote) return; + textEl.textContent = quote.text || ''; + authorEl.textContent = + quote.author && quote.author.trim() ? quote.author : 'Anonymous'; + }; + + const next = () => { + this._quoteIndex = (this._quoteIndex + 1) % this._quotes.length; + show(this._quoteIndex); + }; + + const prev = () => { + this._quoteIndex = + (this._quoteIndex - 1 + this._quotes.length) % this._quotes.length; + show(this._quoteIndex); + }; + + const pauseBtn = document.getElementById('quote-pause'); + const nextBtn = document.getElementById('quote-next'); + const prevBtn = document.getElementById('quote-prev'); + const quoteEl = document.getElementById('welcome-quote'); + + const stop = () => { + if (this._quoteTimer) { + clearInterval(this._quoteTimer); + this._quoteTimer = null; + } + }; + + const start = () => { + stop(); + // Auto-rotation is a motion affordance: when the OS asks for reduced + // motion, leave the first server-rendered quote in place instead. + const reduced = + window.matchMedia && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (!reduced && !this._quotesPaused) { + this._quoteTimer = setInterval(next, 8000); + } + }; + + if (nextBtn) { + nextBtn.addEventListener('click', () => { + next(); + start(); + }); + } + if (prevBtn) { + prevBtn.addEventListener('click', () => { + prev(); + start(); + }); + } + if (pauseBtn) { + pauseBtn.addEventListener('click', () => { + this._quotesPaused = !this._quotesPaused; + pauseBtn.textContent = this._quotesPaused ? 'Play' : 'Pause'; + pauseBtn.setAttribute('aria-pressed', String(this._quotesPaused)); + if (this._quotesPaused) { + stop(); + } else { + start(); + } + }); + } + if (quoteEl) { + quoteEl.addEventListener('mouseenter', stop); + quoteEl.addEventListener('mouseleave', () => { + if (!this._quotesPaused) start(); + }); + quoteEl.addEventListener('focusin', stop); + quoteEl.addEventListener('focusout', () => { + if (!this._quotesPaused) start(); + }); + } + + start(); + }, + saveCurrentNote() { const form = document.getElementById('note-form'); if (form) form.requestSubmit(); diff --git a/static/style.css b/static/style.css index 9d1b447..1273004 100644 --- a/static/style.css +++ b/static/style.css @@ -248,6 +248,7 @@ button, input, select, textarea { .btn:hover { background-color: var(--ansi-12); border-color: var(--app-fg); + color: var(--ansi-0); } .btn-primary { @@ -314,6 +315,12 @@ button, input, select, textarea { overflow: hidden; } +/* The welcome state renders no preview pane, so the editor takes the full + remaining width instead of sitting in a half-width track. */ +.app-body.no-preview { + grid-template-columns: 280px 1fr; +} + /* When a pane is collapsed via the View menu, the remaining pane reclaims the space. Each state sets its own column count so a lone pane always spans the full remaining width instead of sitting in a half-width track. @@ -323,22 +330,26 @@ button, input, select, textarea { on specificity, leaving the Preview tab permanently empty. */ @media (min-width: 901px) { .app-body.hide-preview, - .app-body.hide-editor { + .app-body.hide-editor, + .app-body.no-preview { grid-template-columns: 280px 1fr; } - .app-body.hide-editor.hide-preview { + .app-body.hide-editor.hide-preview, + .app-body.no-preview.hide-editor { grid-template-columns: 280px; } } @media (min-width: 601px) and (max-width: 900px) { .app-body.hide-preview, - .app-body.hide-editor { + .app-body.hide-editor, + .app-body.no-preview { grid-template-columns: 240px 1fr; } - .app-body.hide-editor.hide-preview { + .app-body.hide-editor.hide-preview, + .app-body.no-preview.hide-editor { grid-template-columns: 240px; } } @@ -757,15 +768,462 @@ button, input, select, textarea { justify-content: flex-end; } -.empty-state { +.welcome-state { flex: 1; + min-height: 0; + overflow-y: auto; + padding: clamp(24px, 5vw, 52px); + scrollbar-gutter: stable; +} + +.welcome-heading { + max-width: 760px; + margin: 0 auto 28px; +} + +.welcome-eyebrow { + margin: 0 0 10px; + color: var(--ansi-7); + font-size: 11px; + font-weight: 700; + letter-spacing: 1.2px; +} + +.welcome-heading h1 { + margin: 0; + font-size: clamp(24px, 3vw, 36px); + line-height: 1.15; + letter-spacing: -0.6px; + text-wrap: balance; +} + +.welcome-tagline { + margin: 12px 0 0; + max-width: 560px; + font-size: 15px; + line-height: 1.6; + opacity: 0.72; +} + +.welcome-columns { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 16px; + max-width: 760px; + margin: 0 auto; +} + +.welcome-card { + min-width: 0; + padding: 18px; + border: 1px solid var(--ui-border); + border-radius: 10px; + background: color-mix(in srgb, var(--app-fg) 3%, var(--app-bg)); + box-shadow: 0 4px 18px color-mix(in srgb, var(--app-fg) 5%, transparent); +} + +.welcome-card h2 { + margin: 0 0 14px; + font-size: 14px; + font-weight: 650; +} + +.welcome-muted { + margin: 0; + font-size: 13px; + line-height: 1.55; + opacity: 0.68; +} + +.welcome-recent-list { + list-style: none; + margin: 0; + padding: 0; +} + +.welcome-recent-list li + li { + border-top: 1px solid var(--ui-border); +} + +.welcome-recent-list a { + display: flex; + flex-direction: column; + gap: 3px; + padding: 10px 8px; + margin: 0 -8px; + border-radius: 6px; + color: var(--app-fg); + transition: background-color 0.15s ease, color 0.15s ease; +} + +.welcome-recent-list a:hover { + background-color: var(--ui-hover); + text-decoration: none; +} + +.welcome-note-title { + overflow-wrap: anywhere; + font-size: 14px; + font-weight: 600; +} + +.welcome-note-meta, +.welcome-note-preview, +.weather-location, +.weather-privacy { + font-size: 12px; + line-height: 1.45; + opacity: 0.65; +} + +.welcome-note-preview { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.weather-summary { + margin: 0 0 6px; + font-size: 20px; + font-weight: 600; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.weather-location { + margin: 0 0 18px; +} + +.weather-location-form { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + margin-top: 14px; +} + +.weather-location-form label { + font-size: 12px; + font-weight: 600; +} + +.weather-location-form input { + width: 100%; + min-width: 0; + min-height: 40px; + padding: 8px 10px; + border: 1px solid var(--ui-border); + border-radius: 6px; + background: var(--app-bg); + outline: none; +} + +.weather-location-form input:focus { + border-color: var(--ansi-4); +} + +.weather-location-form .btn { + min-height: 40px; +} + +.weather-privacy { + margin: 2px 0 0; +} + +.weather-table-wrapper { + margin: 0 0 8px; + overflow-x: auto; + border: 1px solid var(--ui-border); + border-radius: 8px; +} + +.weather-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + line-height: 1.45; +} + +.weather-caption { + padding: 8px 10px; + font-weight: 600; + text-align: left; +} + +.weather-table th, +.weather-table td { + padding: 8px 10px; + border-top: 1px solid var(--ui-border); + text-align: left; + white-space: nowrap; +} + +.weather-table thead th { + border-top: none; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + opacity: 0.65; +} + +.weather-now-row th, +.weather-now-row td { + background: color-mix(in srgb, var(--ansi-4) 10%, transparent); +} + +.weather-na { + opacity: 0.45; +} + +.weather-details { + margin: 0 0 12px; + font-size: 12px; + opacity: 0.7; +} + +.weather-units { display: flex; align-items: center; - justify-content: center; - opacity: 0.5; + gap: 14px; + margin: 0; + padding: 0; + border: none; +} + +.weather-units legend { + padding: 0; + font-size: 12px; + font-weight: 600; +} + +.weather-units label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 400; + min-height: 40px; +} + +.weather-units input { + width: auto; + min-height: auto; + accent-color: var(--ansi-4); +} + +.welcome-quotes { + margin-top: 16px; + max-width: 560px; +} + +.welcome-quote { + margin: 0; + padding: 12px 16px; + border-left: 4px solid var(--ansi-4); + background: color-mix(in srgb, var(--app-fg) 4%, var(--app-bg)); + border-radius: 0 8px 8px 0; +} + +.welcome-quote-text { + margin: 0 0 6px; + font-size: 16px; + font-style: italic; + line-height: 1.5; + overflow-wrap: anywhere; +} + +.welcome-quote footer { + font-size: 13px; + opacity: 0.7; +} + +.welcome-quote-controls { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.btn-small { + min-height: 36px; + padding: 6px 12px; + font-size: 12px; +} + +.quotes-card { + max-width: 760px; + margin: 16px auto 0; +} + +.quote-form { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + margin-top: 12px; +} + +.quote-form label { + font-size: 12px; + font-weight: 600; +} + +.quote-form textarea, +.quote-form input { + width: 100%; + min-width: 0; + min-height: 40px; + padding: 8px 10px; + border: 1px solid var(--ui-border); + border-radius: 6px; + background: var(--app-bg); + outline: none; font-size: 14px; } +.quote-form textarea { + min-height: 76px; + resize: vertical; + line-height: 1.5; +} + +.quote-form textarea:focus, +.quote-form input:focus { + border-color: var(--ansi-4); +} + +.quote-list { + list-style: none; + margin: 14px 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.quote-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--ui-border); + border-radius: 8px; +} + +.quote-item-body { + margin: 0; + flex: 1; + min-width: 0; +} + +.quote-item-body p { + margin: 0 0 4px; + font-size: 14px; + font-style: italic; + line-height: 1.5; + overflow-wrap: anywhere; +} + +.quote-item-body footer { + font-size: 12px; + opacity: 0.65; +} + +.quote-delete-form { + flex-shrink: 0; +} + +/* Mobile-only menu entries, hidden on desktop. Inverse of `.desktop-only`, + which hides on mobile. Keep the two breakpoints in sync. */ +.mobile-only { + display: none; +} + +/* Dedicated weather page. Separate document like `/about`: same menu bar and + status bar, own main column, own back link. */ +.weather-page { + display: flex; + flex-direction: column; + height: 100dvh; +} + +.weather-page-main { + flex: 1; + min-height: 0; + overflow-y: auto; + max-width: 720px; + width: 100%; + margin: 0 auto; + padding: 32px 24px; +} + +.weather-page-main h1 { + margin: 0 0 16px; + font-size: 26px; + line-height: 1.2; + text-wrap: balance; +} + +.weather-page-main .weather-card { + margin-bottom: 20px; +} + +.weather-page .back-link { + display: inline-block; + padding: 8px 14px; + border: 1px solid var(--ui-border); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + transition: background-color 0.15s ease, border-color 0.15s ease; +} + +.weather-page .back-link:hover { + background-color: var(--ui-hover); + text-decoration: none; +} + +/* Simplified welcome page backing the mobile-only `View → Welcome` entry. + Same separate-document pattern as `/weather`: header, quote rotator, and + recent notes only — no weather card, no quotes manager. */ +.welcome-page { + display: flex; + flex-direction: column; + height: 100dvh; +} + +.welcome-page-main { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +.welcome-page-main .welcome-heading { + max-width: 760px; +} + +.welcome-page-main .recent-card { + max-width: 760px; + margin: 0 auto 20px; +} + +.welcome-page-main .back-link { + display: block; + max-width: 760px; + margin: 0 auto; + padding: 8px 14px; + border: 1px solid var(--ui-border); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + transition: background-color 0.15s ease, border-color 0.15s ease; +} + +.welcome-page-main .back-link:hover { + background-color: var(--ui-hover); + text-decoration: none; +} + /* Preview Pane */ .preview-pane { display: flex; @@ -1503,4 +1961,88 @@ button, input, select, textarea { text-align: center; font-size: 16px; } + + .welcome-state { + padding: 24px 18px; + } + + .welcome-heading { + margin-bottom: 20px; + } + + .welcome-heading h1 { + font-size: 27px; + } + + .welcome-columns { + grid-template-columns: minmax(0, 1fr); + gap: 12px; + } + + .welcome-card { + padding: 16px; + } + + .weather-location-form .btn { + width: 100%; + min-height: 44px; + } + + /* Mobile-only View entries. Shown here; `.mobile-only` is `display: none` + at wider widths so desktop keeps the full dashboard instead. */ + .mobile-only { + display: list-item; + } + + .weather-table { + font-size: 12px; + } + + .weather-table th, + .weather-table td { + padding: 10px 8px; + } + + .welcome-quote-controls .btn-small { + min-height: 44px; + flex: 1; + } + + .quote-item { + flex-direction: column; + } + + .quote-delete-form .btn { + width: 100%; + min-height: 44px; + } + + .weather-page-main { + padding: 20px 16px; + } + + .weather-page-main h1 { + font-size: 22px; + } + + .weather-page .back-link { + display: block; + min-height: 44px; + padding: 12px 16px; + text-align: center; + font-size: 16px; + } + + .welcome-page-main .back-link { + min-height: 44px; + padding: 12px 16px; + text-align: center; + font-size: 16px; + } +} + +@media (min-width: 901px) { + .app-body.hide-preview .welcome-columns { + grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); + } } diff --git a/tests/auth_gating.rs b/tests/auth_gating.rs index 3a70f1f..9098294 100644 --- a/tests/auth_gating.rs +++ b/tests/auth_gating.rs @@ -7,7 +7,9 @@ use axum::body::Body; use axum::http::{header, Request, StatusCode}; use eternalibre_notes::auth::{AuthError, AuthState}; +use eternalibre_notes::quotes::QuoteStore; use eternalibre_notes::routes::{self, AppState}; +use eternalibre_notes::weather::WeatherService; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, OnceLock}; @@ -78,6 +80,11 @@ impl Fixture { app: routes::app(AppState { auth: auth.clone(), base_path: base_path.into(), + weather: Arc::new(WeatherService::new( + secret_file.with_extension("weather-location"), + secret_file.with_extension("weather-units"), + )), + quotes: Arc::new(QuoteStore::new(secret_file.with_extension("quotes"))), }), auth, secret_file, @@ -200,6 +207,10 @@ impl Fixture { /// POST carrying the session cookie. async fn post_authed(&self, uri: &str, body: &str) -> (StatusCode, String, Option) { + respond(self.post_authed_response(uri, body).await).await + } + + async fn post_authed_response(&self, uri: &str, body: &str) -> axum::response::Response { let res = self .app .clone() @@ -214,7 +225,7 @@ impl Fixture { ) .await .unwrap(); - respond(res).await + res } /// Creates a note and returns its id. @@ -628,6 +639,30 @@ async fn write_routes_require_authentication() { // Theme changes are gated too. let (status, _, _) = f.post_form("/theme", "theme=Dracula").await; assert_eq!(status, StatusCode::SEE_OTHER); + + // The optional third-party weather location is gated as well. + let (status, _, _) = f.post_form("/settings/weather", "location=Somewhere").await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert!( + !f.secret_file.with_extension("weather-location").exists(), + "anonymous weather settings request must not persist a location" + ); + + // Quote settings and the mobile weather page sit behind the same gate. + let (status, _, _) = f + .post_form("/settings/quotes", "quote=Hello&author=Someone") + .await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert!( + !f.secret_file.with_extension("quotes").exists(), + "anonymous quote request must not persist quotes" + ); + let (status, _, _) = f.post_form("/settings/quotes/delete", "index=0").await; + assert_eq!(status, StatusCode::SEE_OTHER); + let (status, _) = f.get("/weather").await; + assert_eq!(status, StatusCode::SEE_OTHER); + let (status, _) = f.get("/welcome").await; + assert_eq!(status, StatusCode::SEE_OTHER); } #[tokio::test] @@ -657,7 +692,7 @@ async fn delete_form_has_confirmation_prompt() { // Find the delete form by its note-specific action let form_start = body - .find(&format!("/notes/{id}\"")) + .find(&format!("action=\"/notes/{id}\"")) .expect("delete form should exist"); let form_end = body[form_start..] .find("") @@ -1753,6 +1788,334 @@ async fn theme_cookie_alongside_session_still_works() { assert!(String::from_utf8_lossy(&bytes).contains("data-theme=\"Dracula\"")); } +#[tokio::test] +async fn theme_change_preserves_the_open_note_and_rejects_external_return_targets() { + let f = Fixture::with_base_path("theme-return", "/forgejo") + .login_over_http() + .await; + let id = f.create_note().await; + let body = format!("theme=Dracula&return_to=%2Fnotes%2F{id}"); + let response = f.post_authed_response("/theme", &body).await; + assert_eq!(response.status(), StatusCode::SEE_OTHER); + let location = response + .headers() + .get(header::LOCATION) + .and_then(|value| value.to_str().ok()); + assert_eq!(location, Some(f.expect(&format!("/notes/{id}")).as_str())); + + let response = f + .post_authed_response("/theme", "theme=Light&return_to=%2F") + .await; + assert_eq!(response.status(), StatusCode::SEE_OTHER); + assert_eq!( + response + .headers() + .get(header::LOCATION) + .and_then(|value| value.to_str().ok()), + Some(f.expect("/").as_str()) + ); + + let response = f + .post_authed_response("/theme", "theme=Light&return_to=https%3A%2F%2Fevil.example") + .await; + assert_eq!(response.status(), StatusCode::SEE_OTHER); + let location = response + .headers() + .get(header::LOCATION) + .and_then(|value| value.to_str().ok()); + assert_eq!(location, Some(f.expect("/").as_str())); +} + +#[tokio::test] +async fn welcome_dashboard_and_weather_location_are_server_rendered() { + let f = Fixture::provisioned("welcome-dashboard"); + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains(r#"class="welcome-state""#)); + assert!(body.contains("Welcome to EternaLibre Notes")); + assert!(body.contains(r#"action="/settings/weather" method="post""#)); + assert!(body.contains("wttr.in")); + assert!(body.contains(r#"class="welcome-recent-list""#)); +} + +#[tokio::test] +async fn weather_location_setting_is_persisted_and_can_be_cleared() { + let f = Fixture::provisioned("weather-setting"); + let setting_path = f.secret_file.with_extension("weather-location"); + let (status, _, location) = f + .post_authed("/settings/weather", "location=San+Francisco") + .await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert_eq!(location.as_deref(), Some("/")); + assert_eq!( + std::fs::read_to_string(&setting_path).unwrap(), + "San Francisco" + ); + + let (status, _, _) = f.post_authed("/settings/weather", "location=").await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert!( + !setting_path.exists(), + "blank location should clear the setting" + ); +} + +#[tokio::test] +async fn weather_units_are_saved_and_offered_as_radios() { + let f = Fixture::provisioned("weather-units"); + let units_path = f.secret_file.with_extension("weather-units"); + let (status, _, _) = f + .post_authed("/settings/weather", "location=Oslo&units=f") + .await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert_eq!(std::fs::read_to_string(&units_path).unwrap(), "f"); + + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!( + body.contains(r#"name="units" value="f""#), + "weather form should offer a Fahrenheit radio" + ); + assert!( + body.contains(r#"name="units" value="c""#), + "weather form should offer a Celsius radio" + ); + + let (status, _, _) = f + .post_authed("/settings/weather", "location=Oslo&units=kelvin") + .await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert_eq!( + std::fs::read_to_string(&units_path).unwrap(), + "f", + "an invalid unit must not overwrite the saved preference" + ); +} + +#[tokio::test] +async fn weather_table_renders_current_first_then_forecast_days() { + use eternalibre_notes::pages::{render_app, AppPageContext, Base, FolderContext}; + use eternalibre_notes::weather::{WeatherCurrent, WeatherDay, WeatherDisplay}; + + let weather = WeatherDisplay { + location: Some("London".to_string()), + units: eternalibre_notes::weather::TempUnit::Fahrenheit, + current: Some(WeatherCurrent { + desc: "Overcast".to_string(), + temp_c: "15".to_string(), + temp_f: "59".to_string(), + feels_c: "13".to_string(), + feels_f: "55".to_string(), + humidity: "51".to_string(), + }), + days: vec![WeatherDay { + date: "2026-10-12".to_string(), + desc: "Sunny".to_string(), + avg_c: "14".to_string(), + avg_f: "57".to_string(), + max_c: "17".to_string(), + max_f: "62".to_string(), + min_c: "13".to_string(), + min_f: "55".to_string(), + }], + }; + let folders = Vec::new(); + let folder_ctx = FolderContext { + folders: &folders, + active_folder: None, + }; + let no_quotes: Vec = Vec::new(); + let base = Base::new(""); + let page = AppPageContext { + current_note: None, + recent_notes: &[], + weather: &weather, + quotes: &no_quotes, + query: None, + message: None, + }; + let html = render_app("Dark", &[], &folder_ctx, &page, &base).into_string(); + assert!( + html.contains(r#"class="weather-table""#), + "forecast should render as a table" + ); + let now = html.find("Now").expect("current row should lead the table"); + let forecast = html.find("Oct 12").expect("forecast day should follow"); + assert!(now < forecast, "current weather must be the top table row"); + assert!( + html.contains("59°F"), + "active Fahrenheit unit should render" + ); + assert!( + html.contains("62°F"), + "forecast high should follow the unit" + ); +} + +#[tokio::test] +async fn quotes_can_be_added_listed_and_removed() { + let f = Fixture::provisioned("quotes-crud"); + let quotes_path = f.secret_file.with_extension("quotes"); + let (status, _, _) = f + .post_authed("/settings/quotes", "quote=Stay+curious.&author=Ada") + .await; + assert_eq!(status, StatusCode::SEE_OTHER); + let saved = std::fs::read_to_string("es_path).unwrap(); + assert!(saved.contains("Stay curious.")); + assert!(saved.contains("Ada")); + + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains(r#"id="welcome-quote""#)); + assert!(body.contains("Stay curious.")); + assert!(body.contains("window.__WELCOME_QUOTES__")); + + let (status, _, _) = f.post_authed("/settings/quotes/delete", "index=0").await; + assert_eq!(status, StatusCode::SEE_OTHER); + assert!( + !quotes_path.exists(), + "removing the last quote should clear its file" + ); +} + +#[tokio::test] +async fn mobile_weather_entry_links_to_a_dedicated_page() { + let f = Fixture::provisioned("mobile-weather"); + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!( + body.contains(r#"class="mobile-only""#), + "View menu should carry a mobile-only weather entry" + ); + assert!( + body.contains("/weather?return_to=%2F"), + "weather entry should link back to the dashboard" + ); + + let (status, body) = f.get_authed("/weather?return_to=%2F").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains(r#"class="weather-page-main""#)); + assert!(body.contains(r#"class="weather-table-wrapper""#) || body.contains("wttr.in")); + assert!(body.contains(r#"class="back-link""#)); +} + +/// The welcome state has nothing to preview, so the preview pane, its mobile +/// tab, and its View toggle must not render. Opening a note restores all +/// three. Anchored on template-only attributes (`class="preview-pane"`, +/// `data-tab-target="preview"`) rather than bare words that also match the +/// inlined stylesheet. +#[tokio::test] +async fn welcome_state_omits_the_preview_pane() { + let f = Fixture::provisioned("welcome-no-preview"); + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!( + body.contains(r#"class="app-body no-preview""#), + "welcome layout should drop to two columns" + ); + assert!( + !body.contains(r#"class="preview-pane""#), + "no note means nothing to preview" + ); + assert!( + !body.contains(r#"data-tab-target="preview""#), + "the Preview tab would only offer an empty view" + ); + assert!( + !body.contains(r#"x-on:click="togglePreview()""#), + "the View menu should not offer a preview toggle with no pane" + ); + + let id = f.create_note().await; + let (status, body) = f.get_authed(&format!("/notes/{id}")).await; + assert_eq!(status, StatusCode::OK); + assert!( + !body.contains(r#"class="app-body no-preview""#), + "an open note restores the three-column layout" + ); + assert!(body.contains(r#"class="preview-pane"#)); + assert!(body.contains(r#"data-tab-target="preview""#)); + assert!(body.contains(r#"x-on:click="togglePreview()""#)); +} + +#[tokio::test] +async fn mobile_welcome_entry_links_to_a_simplified_page() { + let f = Fixture::provisioned("mobile-welcome"); + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + assert!( + body.contains("/welcome?return_to=%2F"), + "View menu should link to the simplified welcome page" + ); + + let (status, body) = f.get_authed("/welcome?return_to=%2F").await; + assert_eq!(status, StatusCode::OK); + assert!(body.contains(r#"class="welcome-page-main"#)); + assert!(body.contains("Welcome to EternaLibre Notes")); + assert!(body.contains(r#"class="welcome-recent-list""#)); + assert!( + !body.contains(r#"class="weather-table""#), + "the simplified page carries no weather table" + ); + assert!( + !body.contains(r#"name="units""#), + "units belong to the full weather settings, not this page" + ); + assert!( + !body.contains(r#"action="/settings/quotes""#), + "quote management stays on the dashboard" + ); + assert!(body.contains(r#"class="back-link""#)); +} + +#[tokio::test] +async fn welcome_lists_recent_notes_and_theme_forms_carry_a_safe_return_target() { + use chrono::Utc; + use eternalibre_notes::notes::Note; + use eternalibre_notes::pages::{render_app, AppPageContext, Base, FolderContext}; + use eternalibre_notes::weather::WeatherDisplay; + + let mut note = Note::new("A recent thought", "A little preview."); + note.id = "550e8400-e29b-41d4-a716-446655440000".to_string(); + note.updated_at = Utc::now(); + let folders = Vec::new(); + let folder_ctx = FolderContext { + folders: &folders, + active_folder: None, + }; + let weather = WeatherDisplay::empty(); + let no_quotes: Vec = Vec::new(); + let base = Base::new(""); + let welcome_page = AppPageContext { + current_note: None, + recent_notes: &[note.clone()], + weather: &weather, + quotes: &no_quotes, + query: None, + message: None, + }; + let html = render_app("Dark", &[], &folder_ctx, &welcome_page, &base).into_string(); + let note_url = format!("href=\"/notes/{}\"", note.id); + assert!(html.contains(¬e_url)); + assert!(html.contains("A recent thought")); + + let note_context = AppPageContext { + current_note: Some(¬e), + recent_notes: &[], + weather: &weather, + quotes: &no_quotes, + query: None, + message: None, + }; + let note_page = render_app("Dark", &[], &folder_ctx, ¬e_context, &base).into_string(); + assert_eq!( + note_page + .matches(r#"name="return_to" value="/notes/550e8400-e29b-41d4-a716-446655440000""#) + .count(), + eternalibre_notes::themes::PREDEFINED_THEMES.len() + 1 + ); +} + // ----------------------------------------------------------- about page ---- /// The about page's mobile styling hangs entirely off this one class. @@ -1811,7 +2174,7 @@ async fn about_page_inlines_the_stylesheet() { #[tokio::test] async fn every_screen_declares_a_viewport() { let f = Fixture::fresh("viewport").login_over_http().await; - for path in ["/", "/about"] { + for path in ["/", "/about", "/weather", "/welcome"] { let (status, body) = f.get_authed(path).await; assert_eq!(status, StatusCode::OK, "{path}"); assert!( @@ -1924,9 +2287,9 @@ async fn about_page_theme_counts_come_from_the_palette() { ); } -/// Outbound links must carry `rel="noopener noreferrer"`. The app advertises -/// that it never talks to a third party, and these are the only links that -/// leave the machine, so they should not leak a referrer when followed. +/// Outbound links must carry `rel="noopener noreferrer"` so they do not leak +/// a referrer when followed. The optional weather request is server-side and +/// is covered separately from these browser navigation links. #[tokio::test] async fn about_page_external_links_are_hardened() { let f = Fixture::fresh("about-links").login_over_http().await; @@ -2085,6 +2448,11 @@ async fn alpine_is_served_app_absolute_even_with_a_base_path() { let app = routes::app(AppState { auth, base_path: "/forgejo".into(), + weather: Arc::new(WeatherService::new( + secret_file.with_extension("weather-location"), + secret_file.with_extension("weather-units"), + )), + quotes: Arc::new(QuoteStore::new(secret_file.with_extension("quotes"))), }); let (status, body) = fetch(&app, "/static/alpine.min.js").await; @@ -2171,6 +2539,11 @@ async fn alpine_is_available_without_a_static_directory() { let app = routes::app(AppState { auth, base_path: "".into(), + weather: Arc::new(WeatherService::new( + secret_file.with_extension("weather-location"), + secret_file.with_extension("weather-units"), + )), + quotes: Arc::new(QuoteStore::new(secret_file.with_extension("quotes"))), }); // Nothing is read from disk at request time; the bundle is linked in.