From 7cdaaa2878471c1f3e8343355cb1e13d6e9a6416 Mon Sep 17 00:00:00 2001 From: Yurii Hulyk Date: Mon, 26 Oct 2026 14:53:48 +0000 Subject: [PATCH] feat(gpuix): add tree file navigation --- .changeset/fine-aliens-peel.md | 5 + .github/assets/tree-view.png | Bin 0 -> 228612 bytes apps/gpuix/src/app.tsx | 26 +- .../repository-comparison.tsx | 34 +- .../repository-graph/repository-graph.tsx | 22 +- .../repository-pull-request-view.tsx | 1 + .../components/repository-change-details.tsx | 30 +- .../components/repository-file-view.tsx | 184 +++++ .../components/repository-list.tsx | 650 ++++++++++++++---- .../components/repository-navigation.tsx | 28 +- .../components/repository-revisions.tsx | 23 +- .../lib/repository-file-tree.ts | 187 +++++ .../lib/repository-workspace-model.ts | 14 +- .../components/workspace-tab-icon.tsx | 5 + .../components/workspace-tab.tsx | 118 +--- .../hooks/use-workspace-session.ts | 7 +- .../workspace-tabs/lib/workspace-session.ts | 9 +- apps/gpuix/src/fixture/overflowing-label.tsx | 104 +++ .../repository-comparison.component.test.tsx | 58 +- .../repository-graph/view.component.test.tsx | 18 + .../file-label.component.test.tsx | 182 +++++ .../file-tree.component.test.tsx | 369 ++++++++++ .../file-tree.unit.test.ts | 154 +++++ .../repository-workspace.component.test.tsx | 12 + .../restore.component.test.tsx | 43 ++ .../workspace-session/session.unit.test.ts | 16 + 26 files changed, 2033 insertions(+), 266 deletions(-) create mode 100644 .changeset/fine-aliens-peel.md create mode 100644 .github/assets/tree-view.png create mode 100644 apps/gpuix/src/feature/repository-workspace/components/repository-file-view.tsx create mode 100644 apps/gpuix/src/feature/repository-workspace/lib/repository-file-tree.ts create mode 100644 apps/gpuix/src/fixture/overflowing-label.tsx create mode 100644 apps/gpuix/tests/repository-workspace/file-label.component.test.tsx create mode 100644 apps/gpuix/tests/repository-workspace/file-tree.component.test.tsx create mode 100644 apps/gpuix/tests/repository-workspace/file-tree.unit.test.ts diff --git a/.changeset/fine-aliens-peel.md b/.changeset/fine-aliens-peel.md new file mode 100644 index 00000000..824880e9 --- /dev/null +++ b/.changeset/fine-aliens-peel.md @@ -0,0 +1,5 @@ +--- +"@1git/gpuix": minor +--- + +Add consistent List and Tree file navigation across Changes, History, Stashes, Graph, and comparisons, with folder inclusion and a remembered layout preference. Use a compact icon toggle beside Search and Refresh in Changes and in each saved-file pane header. Reveal truncated file and folder labels on hover or keyboard focus with the same sliding text as workspace tabs, without text tooltips. diff --git a/.github/assets/tree-view.png b/.github/assets/tree-view.png new file mode 100644 index 0000000000000000000000000000000000000000..903d686456a8768fa1e3b6cf74465f8127614df2 GIT binary patch literal 228612 zcmeEv2~?DI`}QCzr75{2<^ttLWjW=B$};+@;gXS=Q7(*TNrs9@#y*3Znz>M-k)ljx zrsa#H86q+;ARsE@Sfr>R!#*m)%rL_2^L+R9V-btc{^xw>{Lgp3b6$Iu<(cRCE%)!f zuj{(6XX{&UdiUrypc{|J>oIf2w6}S@C%)zJx}1NaD}BY1Ts@X&ADK6E+LU)f?P@dZ zzZ>@cH|MlmNZpR}mkzI;z3kYkqEpTPTd_Uin_1F?(PM_x89z(@B>bl>?g3Y&ovMF1 zdhE>I$NEo#;3eP1>#oj!tp7A@n49u=m&M0BZNK<JsiivGmk8K+~HVvwfpN;k2T8d(fnThUp3TR@~Km&LK}R=>->y+gnQ#6QsR~c z1Z=B0mb%jzd3lc8t!atP<7Nvy-EeK;-k-L**PY&Y^^+Wl<$^?GmUCu{)`fe`!NH;9 z5legeiC3|_xPdz3e!ZB}tI|qp(n5`K;;^PEH*u@>jj)-d+&;Bor@Ay%Zccqa@!gIu z>gm+^Tg)maD>&?N;LC4R18x z<_bPn&sW4)ug7Rh9R%!CXp&8F#o;xHviu@h53#jSoZsQnerEC-TAx-)qfO1(EG25I zv!h6}v1RW#R-tMcU-{KflFH}%Rn0fl$ZY}b-$e7K=aQdZ zf*;`Ky(#g^msoE|9vUJ28CoV`d*i~M{VXJ;CrmcpeFH_ zQp~NO%Btk76QfLHo8w@E%bhbd$12j4=CsFp5T7uqKAhh&c!Df5{bo={A<8QMoQG$& zN{%Nb<=AXqEgvjdx^Kjr`zK^(X4-Walev+d*`|6{YP&1F-16l9KPS~+lv*x!Ja+20 z*pgC&R{4rPYPxBYz#(?X|A`iEn z0#VZsa$|aUJFO_l;QAIE&%SZn?M;pJ1dZDxu9As+8Zt4s`YHd*`^&_?#;-oJwP`)t zRL}ZG{I;T0yl;Gu?MFRrI?M~=i zTD6b(xTdY9n%!I4_=)t~E6K%Y2Zt2fWl&(wUX^_7oQoyXWl20YbEQtWbddMEN1x%I zt=3IkT*wB$L7a2?=CT3bSbW|!cexe0#VKd(oKF`&V$i#>K{xKoKALv(Xi(lO$!pBD z$vVV&p3HPqJ}J3)ZFinn;~<4xkxFKWcOtAPb zOnmpl5p11sP7?-l1rrB$xJ%~qJF`VFXqfgB8b@3Z7q(Db_Bxd!StX*NDk%z-A6U3L zWs`42lJC^soXhr#^Wr^0gF4*!Et4ni%x3ug_O2hc`jnC4N)*D=>XfQ}ezjNqdd##4 z-RAV+=Kr*!T)a_Wj1`nsc-2)H^5mA;wu4F6410veXGF9wH(ja}|DsbVbcjI~iif>A zeo%auo5^CrN4r9kd?Uu;Bk|FAMwcSlbM4>EH+wC<7v8WP^lbRqr)sgXVR4wwO&pwF zwJ?1X_XHiQE|){13PZH7x#(WAct@NYlaPt?qyD78d~#Z6Zl3i|A8TKE{Yd#)x2OWQ zpfzhUI@*&gvuY6c#GGszhVj^efyV?;{8i?T=Xw{hxxs3{s=(UcJnYz+as_8m7_Y}z z-}5qT6ntB>I&}y4CVoW`kajIVOQV-(p(NmWpRkMb3^AI~eBEfC?EY-|SjSJXh+mS{ zzr+?&#f17mtsz(79PKSI{ciDT{A!Cw=b4Lw^T(}RxssjP=HFISmbfRJONm(RdQ!`u zcs=#}m;_6RN5uuPqRckDW!~G35l3x(5Lr1@yQ^XDjcQ~+VMw_!H#}RZqm5T&5hpE3 zG<@%3dfry)WBc5EqgtL-S2v2r+a*h3xuoD7Pf58@R`xtMH@#t@e2>C*Pf@*j;Of%# zwC8;y%lp`!f5Y}lg64Bqp61(JkImc#KXrp&3`^%CBI3Vb=Uei`VtF>-l2N#}XdbtI zWsm3u?#{MyX~sZ#g+qbD$>Xi~*F{yi>3hTmoicMguh*uy$U}uy4a!FA7jNUB{^zGQ zIsS)+jfMqHE41VYFR?7L zo^mZ&`N;Gk+yO63mSDfcxL2~2vd}=|H-TlVl#Q#x=FTa|-d2tW7VKM_i{a!e7Vz(0ABtbB6jD zY6O*A(zPeUR?9UTg$gBiBXC)~`J?eRQ;cYb(0C%StfS{Vb64;*#YdBP)`NIlo23VoyxJ2YHO-@BB{3n(f1NZ9z1u`!VQjAc8B zv9ecLT8Yy{H&Ct}=wZF%qsnv?y{a~+tMlB`3f#5Tfwt-y<;kL&tEFhZM2pU9I{2c(ZMo6W`Qaefu!UO1$=thQW^{L?$CFaJwyvaGDBr;?LXwC4sI z<05}|it6RW_p6*LxIfjX6L7({`##+6)Ved<0*<9@#&)i1pp$>1*ybUixw)jY-ckmI zhMv!NQ(hWLTVD4K&5}oQTAHWmzPEU{tLL4W#lZ^Cm-}^hzW<$rmhHLAHL{+L_mjou zh#lv_opwCg?{a5z2aCE$TbEUDWr@^&~EoX{@%->*+ZQ(Q`3r4 zgRBjONRu*SZFN4^m7b0nHEH=~&1k9V^*0L6^KDlObwA3jed^LGRxFwj;pVDXMrDygi=Or%WwKv!fnK!*UOSfM?$io0y(6=|H8^$tTwsjzs0)gzctu(N%ESp1 zcuU5|YkxcJxxE^l$ks|cmnD-LjEmqS8 zawt8!sk`imGiS)P*BtTo>^9RfyY5WQs8NefXJ8Dqh%KKkcF0K6m*gR2BNo$w30!Nl zgq=B-H69nE|0PTb*pVC-pIj!=mqhT9!{?WjIP)lRpr6M)({pGX_j^Ws6B3ylQYqT9 zWlM3G>Huc-HYG{rbQhNMF_tH2O2ylT4a0BI7znl5u3lfGa8g}Av5e9IE<}Gz`{}6| zN%{SCZo0Kn?N7Y>hcw-!4b>iZ0=dr7I(_lw3u6lHHM_-z?sfX`K&#h&k6=%pRQHWK zCnmB$6`eoNMIW~)IIFd6YCj5@94Au^ZgaU=Qc{iW5~7T9Ysw>R2d_5;+}c9XDz?u; z#QKnXYPF8LaAH}mK~kRNSCbSv_g-zS7W-t-pg}CONKL;;%d7nAs!r=JyBmKBG>u?^ z*Xb&)&ZiPAm9Dy3()+V`CBk6kW9XKG^2ZfwaOxIE z>hkxyhwK!Z4hhSBDK#({C0tmEDN(sET^%;pQmeGRqSo|OXAG2O4*Y5_OBDq5Lj`Al z|Gk&ouuXnGRhE~E7_CgpEE4kqGaNL<38vHX($QOM0>dWg-Nov`y#2E3uB$^eLsOI+ zowemFT?9F{y0FX2VE2xb4sHsPVs7fdfdhGTk3EYl-Y4@@f>oz&rkWequP+gcB}Z-k z=2(HTPu=N?o)mkG6sM+OGVC?qIe31-H~qjbuS-=|t3-7wgLFx}t4GkCO9lnWj~uxbRxF!xI7{uSYLwd zrUMK~{B$!8q(wQ-lifX<{t^OX*mE;ws)%$~fj(G}E8c`aOtE$R`0=4vHacg^&uydG z)J=AMbN>=A$;kzYp?@B9&aqz{a%&uWeG zdxx*>&Cd*upsRGw$l2zEyf&dgU@@xK91r>0oR&Q@y=P41R^Q6?qYlU&qw{MkJd)`@ zyn6}qxc2v43r4{`*9_W~BT?jq-aqJ)uhZckEWTl9`y}u(4mvlyFia~wk(QR<{SUpT zW8YqMvx-{2t(HvB&t&$#STl%*I)eSR0Vf!+sI1a z1pZ8q@?J5B!F+V3vD{!0l|Eky z4V#3#$-Ui5LWdR=6|G#ox?Ux=F)W2kan|;hK9Mn7_D9WH{VJa_bGm8X`IR)xwoYz> zdnb7N&onN_-Iml%OeiYJImz3fUbp=5ZdW?*@S9qeE4}J8YEfx%O7NlvxpKDr{%m{A zVX^7(8wCW`Ihasv&^_CfmzmLa^uoV)dT;1B0<7wrcn$_hiVz}9e2&%f2 z0|kY*p33kH@z`;zyWVb%=}i3NB#FMB!b5#|35~xU)*`{NoC~ycrT>>_2GUYWQ+mXP>CM^Nhdwn8!NbM$T(E%eVZ;mN?2!c7EI4k_dHGdCoQpzs~hv zZP5s8FZhT?@0~eKb1l$%jW$BC{MN0-bu!(X7+x-PTmIm>l!_G=t(5EQV@nm}?A@NV zPid}lS(-MfQ)y;Q_=|P=^Kp^G`>tOlsyHl<&$7-QbZ>E`o7j|^Ibv2>FTU-|4gnQk ztHbxGchla&X&IqT^SCmua8cHL{(zah8Rg<%w+Jj{k&XW1@knnkBu5n`YmZXcXNfPO z46FX$Oz^lsr+@CYyV+a&6suju^{&RR9F0#8)AF14(oh%Uj9D*Qd{giI6wjlC1NdQK zcw%9vC4qr4g!xFb$Fu2_#fcN$D(?6gMs9A{B{3c2AKX1_QqTxqd{J)Ly}Y!p`qZ*1 zF_l+KG=mLRp{-Yf_D6ZuK(89>luiSq3&T%_RO}n*a_LbatQpssM(3vblABoP7EFMF zMsRBjoq)QzE)8=B2}Jw-2_Ugh(bk`qsQBVR7wgBOQU_7Acn2_BR9?|61H$$~4v{af z)W(QCcrl{;2Prt+{qsd<<9Y8+nna~3F_Kcuv699Wl5_ZkVAX+;wfC;S)-=6to|V7L za=oLTFI3jA8PY9r&V<;T4IY)h^YbI3isRN={!H??a#WUCMBDTJnHimU@n4+HlieJs zMKESk$H?6bY{G*R) z7|S(SPH^wWNu*%_t@IIceT4&IFDaki>AmZ;WEgMA28UgpqlYEL(?uBM6J#l8QbfrKhsdx ziuVBG0OHS+Q7CwATh0at6CjY1s>LOnws_tD-a^^VL~g**t6whdf4#W)$K7YUBct`A z3}@G|8pEkT^LWY*OL56Ow`X)kOFVFlywZQM4T^+_Jn~?2Zf(n00 z8vT?#_sl1W7T*Og_lrF18rcc)HeYGUH*Vk?I;~DOF29`Mna3hl7onyLPgYlxK8jmF z%m!hV&Fj}uY{em}0L?agw7U+Oc%FU-@vuAAIZEC_;kOY5HGJ zoXFgkc72-`kaJ|ct9UfcANO+HsKw>`0*;mC_}$CFz}wVs_1(XNyU=&jKd(64x6=j! zk*$K?{G@iLtM>)qPv}z0)fB7J{OmPXws`FpUkFdD=4jq|R2EtMiN(N3#UU%MM!>Y2 zFlo{UUXnh&EWU=*G3E(ua3f~b?%h0`i`{dxg=Ao?v5d<6jv6 ztCuW;`|xCbERf!rk6<9W*r5zH2{KvqrU6ilIo3#kIoIc^Xo{P%OgOc7`}f2 z%>mnb_-?iP?wdNnoS!`j`0aSW#de(#=o`cvRyC~~lO$kK?Q1<|+gy)WH>^(2l5ZmH zjb`wl{=@k;6G9E2B+_s+-2->>qk$_bc8%{r`0Sp(o8FF?mHgbST=||yU5f1i&b9c{ zf;ml5xj`V@z4L{^gSiM#L#AW(wS|ElvaKfZ#wHTtY@}BUTVWgd=YkG#T$}KDwnTkS zQ|i|eCAe;Gw?6Ijh*4*$FV_dPOv}3Bo@x89c7NY0{|_F*Bxv0o7+bCz<;?tZ9UZz2 zToXReeeNXIIz4zkRxADU3<0ugUSYz-YZq^v^q(lQOlZEf2FWT~&V5|p1oHWhC5u%) zg(0I-Ju*<7>&@%IBwb@M1TpsbA6E#z!S!I-f;i1B;-p(e^S#j2!5p_{a%ny{9~C76@;@z3c2ArW+m_r_KjN??4+x-RCMm5mH!UTh z=Q_**I6jLC^SJj)+yVrHL+-2Al4$pla9lW!q;_ugx7C!p`4E(cD(a^rp$27f_X&UXyy7 zlF8O(|0b5bQXi%4m?;tu=wevOmhJp>!frR{BNh6}mT7ejtsLqCK$b)W#%DU7o7fjJ z{PEaU>01F+Bk5WO%4t7t`5o~t7S|2h3K}9yw|2*+WzljIn^#IG6aaXzP|z5ym-(ooXg(EKLF$Z}6jJ9H_#1HRFHy<~dZ?1Xwb-w-+l&OK9kuvO=L~{4TQ+u^ zk8Pa>ZpBJn4&8QSok}!{vTN?c&a@9{IO<1v+?nZ9KF_Oa-dAle+urRpf31Ow8Ad+-FL+y>S=D~ z6UR(0>ZOhm{Qns25D3pioG=Ug0FyVSK$xQ~XfX}lq?&G)a=vYdn=*1t2jlb*D9G)K4@}?777n`^$7yS> zax)wwu4fsxN(@i7(X+I9nuo5?`bpdF-oH~|$@Z~yX20&p7RUrPR7YhOkG7ufR>kiN zmJRY=5I|XLpXr?s^*wX!;O?{Yxz8SFOL4D@sRqwJntu0ay-RfIFI*5KiG9QN__i`< zCdsm-1wo&Fnmu7BeZ;(vNDi@<{`Dvo4p+m5mH4E7Hv6X+H3%*Ov(~J4F|2Pro4H`x z(C30G#qmLd-X9g$`t{a9XuAW%QG_!*$2PaT59PSrP^xDV?*Hf-q|0|}J!q|p;|mfA zDdqC1?DCQXUzMY>dgO+e-vIFkGzJyY>sSHWy@c!lsBgV3Zp?4jaOv7}K|dySw9Xed za0Ou0{6*FN_3CKa+TzrKt52_|5NbcO&E<$Lpr_|H9IJtLtKo-KLIQZXI|sMqQ*Aa_ zYsPL%KA8`_2!X^oa=T0#hDh2z6CPBRm+Nf!h~99<`FBtK~hBvzA4aj zrSVUWC`yi6_|QGK*|dL+ezucoq@sI;>&l*Hl848K%a(0y{UPx;gqhp;H;7L9#p%}L ztOs!pF)b*gNA_COpu9Uys2fMz2a<^Phx0Gr5EPv|*HvoVC%qu=O;eP}PQl0g-Cwwu z2rHGIm-pUt)V46Ka(4Q#_Vz=2U*qQc)ejf$5)Xfg#xLBE<QN16vj=aW}m?tin;L!dP0ZSl}HZWSkn@k?rUxEUpNe;k@^ zp_KO}1|woITM6=+8x!7_4I8-Vd9zw$P-JG^wNzH#1iw?77gu#(Ez}QDb-`IFqV7(K0%|EK2mShY%+0}(<&p=GkBO|HMFRw;+xrT^mO*c9Mc6q2kmZj$F`P7=lg@A|H0lHXUTvRIl$2`AN7^&16-jU7+-vR)R)f^~x_Al#2q z$O{RJpkF1T=vRU}tE)#)$fmPK$K66=3P|^)pLFXJtd1fSR9@jKDtBcfu+^!x8Aj+`L$_Pli zzdbJW<6NcZs>%>s_?+2(AL^V-s>k(1loI4iYbSj}y7!le)CR3pL6H+jQ})gjqH?B4rPK85Qn;@^m{1P)-YSs-m# zzy^kYo+)&$=X&*vhL}aw#R%H11egJYO1I~@!&fH#V5+$Y#-zVT5`Tr8n!(n*E)@S=vVOrI1>2Q3tw_8DD5HP zNvBd&E5^cbt&=#22VLWXXw%N$xUY{`T*k)d^Js(okaW&I>Dqgv;%FC+j?YJ07APWk zAa-eg9usE`+2Ay-H|pcd)G{zNagR#nNZLFDjnN{4pY^FEYfr%+=s_qcwKJuwL;`Ie zLFN|G?Jc8s4%O$qkx;+;ux9sRk4=Q+$g3dom`Dp+pBkzif>qDtizG666a1x>mK0I9 z@@ypk8h>%4KT(#t$r8%@&!afiO;%k#P|;nk=?+{!18oeHVbCV@NRBZbi7EFMReGP6 ztg?}aH37A^iWQ!umRJWBv-53xfyUlQtr)AN4jL;gIOYG7b}kvMHcf$T_iT4=2AKAj++ihR90JrB3Q)hD4d zlt>-Nz@c-E!Xwu%Qn<^j+O@QQC@~>C61*Cp&Fw`TFX!S}^`cLiC?@LOGGp8#QZV3; z>^Y`ZVSK_SqSZ?Zv%5U9Lm&T>CdwJR^c|UZooDd{f)ovDP4`U~S#JbP1)g;vEoVzt}MxDIWF` zH+y<#3e3ExvNGoL&rksGbcNIzs6F+QNG?y$B)C9X1rM?}1EngS;Aw1cRa(dNW*DKio)D*d@XA=iz#saWc{{yC@QCCecHRzV!C0|C)y- zj~&f=h#)4*W4mE8X+D)*o474cr4DLBo}X@m z+C4qmoL1qUKD1Tn6E8Oo6zN06aS-)%F2!NVqtYp|+Fvw+mLtQo)TU$Va;Ps?B%Vl4 z&b<)p+6v5er=)gf;W|ojZ-prHfoB$;CiI1Oy76W}%MU#`jNAOp<~_t9{CMApo{o-o zAS~4p&PS5EPNt}GxMmV>K>#~H9FdyLPTV7wLUdk1C%DHI$`k+u@CzGJNz#u}jgS0QxitvK(^GH+fwUgLT`-UjR;i#L3 zwUomvt%DQC@x03Vc&%48PLk>-dEA{F)2UR-nWQO7!ulLCs89zKP3MT2Q7#3P`WgSL zFumL+!?z0SAwP^}S)q{XeJvI_&!hh9m=+MYop?;JkIUAJ+}l~NGCeaz1|I(ECnchk zh|Zopdw!f4B!9cxwwzAU4dMBmH<54-r4-LTNb?&LpKs8IZjB3lGC?xN|1y3nY;{CL z1XFwy86wU!>Qpz1iYibiKvlxO#I#Zt=(>4t6l7aja~Ua}X3IJgFHO8@?_M8{aJ&x= zzgUg<5+-UARu)p;Sgc+r)UQjyJR^zT?PKiA(leGeBFd18Cae#t*XEkXD!e~HsvW?J z114yxp$4}g6dXSo?nrDa8ufYZUsjKXzvUUho) zDSPTEdCERYd6Gm_z*TxpDO@+>gZ^Cs2OK1+ZrA8*10X91{W&iDMVluvwhEtVRI>pO zB6ipX>gZ`n7mdF{VxN|0Rl3V z?D7Hk+!#`v)E}gui?rm5J+NvN*pzE;tt)JSFu6DuQAv$2zl3F&PI~0cKIY$iDhS0V zg)?Lz3Tv^#j`WI}o{#K-p)=g^>bk*;3rRut-plQTmOVM3aO#l|#ZYb6GP@k{$B=(t^pCpcA5^$FMEk5n`)p)}LnM#1p!^HL)YxJn z{qR9`vM z`N;0G;@yBN0n!k3lTM=am_pK>SEP{qGRlcI6(uymau)unh_wVjmnfrp#vOImC^JBNTo8Y(*WY4xm4`7mYCuQX_* z;(oh!B&pLHO1ocu%EPVbje^dTmwyn?sVUi8OeZ%llr-8bGuLgfpy>th>Jv17q^DIk zF!dS+h^p`U$*HNS7E+@1Z3pg?N*P4!9GE626;o#D*bbIh4oaX8IQ!6UZ@Kp@kKKL&PC;oD-L-rMg75AWlp8# zQ04l|!dZ2x-#++S`nC&EPO;`uBlL^kX#K{oA-gR;dOEZBBQ;fl*J(OJ_?EMLO%#!U zO~NhGJZ4aLBGv6Onr|nZ(U*u>0E^b0PF);{531;P3U*uW_x193lzN1+rm-dLk^+Kx ztcV10-$c}-5mm)DFj6l-5!?)RpA{}#C@cFQ53a_-XKW|2QiOV~hY_#1$l*bnE0j2Q(FOpIfcRgik&bc`E`t*uKj);DhnMysa zEH6$caG@kO(6BkMMG&F4mMNviEV)oOS!(@bj924=-NuvIX_&{y#D%Qh6v)MBmJGL6y9i}ke=zRY<3Xy(aqL<*@_*Q;` z;tS_T1w~*c1ywr(00HO5<6BrmFpCU7`$WBjb)F~wk!0`aPGAiqgWNycer*6$k2DF0 zxE892U!e8nAz&In^mEy15Gz>!78n(#2SoAIB>hGMOtpwm4wr07*e<-m_#G74GHOE` z?HF}?s*&Q87tibVMz1d(pvv6hn6S5U=dY?>KP*OMFt8%K>=QvX6f;3UjJBB^Mq8-jJ z9hT{|&W^*LB)Oq+>l361RDDL;0M-tEZ2)~!e1XK?VPN9{7x1W(rH>$N!$9o`ek`M* ztXx?ExMHG*N9WS&_`vF+)H%Swx*;3Pp7)kIR{ zD5945=BUf9!ZP+Vp71=H>G9U*6QW+`8Lo%itoBrf#=fEZVW0j3|1k#nZS-=n#F9kz zk$~NxtYdvM;62*Sj9Lyr`QT2MxaTMpA}sBbqz_US)w_JJj-arJGcS&0OVo(l8S=9y zeL_3Y3FH)|)IpVqzE6w)0%@T~?FyEQAT6-?8z`F@N)qV_RPr>P!V!POCS(Ipwr2(h z9*rkrs=7dQ8B!AFS|}8IlKj4OdJw5C5X>;Hv=5r_G^?79Bn3B|WZ@Q{5S;KN*E15y zyA@{wupi2IZ0?W6`WDV87p^BUlN|`2J$`rg7^H+{Fxja1C$yGTf&#sLW~Yn!gi8e& z2Xp~aVBCdnqOORdsuM9$FY^(rL11$O9#S-It81NvANZyr=Yocf>A0pTdOpMA`21GN z3096Lg(8jF@qD>23+n_n6SQLS`G5rUIz}C%_pCov`ZZE{0%|>$7^@T#2dYzjBXfK; z32I|P3xHr>OAR^V3yqEAxrM}WjgF;iz?S;hqhx1SEsm!6AL-@YTYoa276la~>fiOA zRNd0$=$z+buUG%VEuy9-E@Mm7^)0Mi@kmCFi;z|ge7fb~Ri|2}@NpS7>DSrvPMt1^ zPg1UDO3xi%F=5x(QqzZt>J_IB4+T-xpM@Ul2eWjn}&BAN=4y{>>V9uwMD!-JLSV&hZdbOz8Ah z$6U=rp_NbDbMI9aByu@)WapR%H`JoSJ4mT#gw??^=WTwYYoS5?B zCWn^}Q5PcbJ!xUfG9{6cFCpd9W|ikZUf8zdh{bJS?$=BE6NF61Dz?_vC(aP&o%QOg zJBPMH@AJ7mvtA^Q+-dit8f@LDD9j)%e(!~q<7sN^{NpJLCiFw_pXCoU5Dh=vJ+m`t zkWo&g#XsH~`vz{%@dT?92qd?j3(6kP`O)H_a5iD*gQrb9GV4Y9K2ctQ*VYXtA#;19 z*7`Me(GTL6_BX^Vr~f|42u+3i6sqgX$==0{wAB(t%SLNI%9}!-d+@7g?rtItHr)d^ zesLj{e{H|>_xUu1G%59)1rqX)y(F{*Dkl7~VCWd5xpQQR|w)DF1(psE3B*=>IZ{sP=d1!?#S+`2T_%SA$xk6CYQ^ zaMy;{dAPqePWQu+>BXTx)i+9mx4Vgxn)UI2d-SG2#QhGn8`%jw7%(oe#H>@7KC9nu ziSVFAO>Mg$Z;(_ILdin~^xy~o_BWIK_1;`JX5`TldKv;X*Cd|~RGjv&RMyZXWeu*k zI@S~k`aRTyY8{!TS1B_OWt|i6p1oy<-ez&{Fq{&GP2cCHkB+_6is_ovI*ibmx@V0s zjoTw$7ug&eI}GPk`meppE!=y7^-WXTUl1Wj5Ntb}u51BnsNF!=EW?-xb=GdI#D* zDSB`uwv*s+2kF4z4pAN5#5IA8OGzW|l;LnP`(l{$$CQmSZ5XxCdt3qZ$*O{T_wH#V zOPkqOLZKkwpyt^*O1)(^s7+R#fmWYPW5Ea|cw{JQpH+l*g{mTtfO-+b8fT~dIa~Xr zMDyf~CSwWGqFx)o&1VX6WSNXbL+;PLPlUvYW{WINcPs3(a%Ig%W%V$r;oB}Q#`z(^ z$KR+6QQc^(wQV^F9~`&hK_nNF-zRSSkOw%tDURV!GiCGEKCUo{W4s$fb`BgETv9ow z5H3c%`HAnbW#IA@o}&kl#03vZO2^{Ysk^FY+s&iOx|tH2UC z0LAlR8WQwRMP61#_FPe?D0L{9gE~DK&?G1gJCxt4BbdU2Ixu*ewd9Kx zc_}yYDdL00=NMs&wknHvDk&mZPh{9!l80M+5$w%KBlvctDD#N(q{LEA1DQo6zmT!> zX6q$|<*DRbpABS=8z3Nu*=f#ud+h>aT8330M)o1^w;?eWHw5aI28Mi@EIu%J)V*@J1wD&gjGk*2zDCGBhxMS%4EHm zXGsSSg+fVbkCJHBXVi(xTSGm9&bNVW+J(|A`wt6}qZDP^fiN?16JW^jr`ZIp;4+MC z2iZq}Ru~LOK!u&87Pnw#V8E6gK;KfRv_K^>%zl-Y^Pn~x{E=M>2=_jGC;;G!EB z5?qwlrYdP?g>&6VwI^1gNf$s|m1BGFO6BY*S zK!oeeI@zhTr;nO5O+yNsZ&Z!&&tQ$AaQom$VPLqYu&rd~G2{w|wgm(ej3`#%er&zv z#Ev&@3ou>XG6-;hP=KZ);VdrH7Dwl-4?vy0(OVmlWR8M+?-LEdOW_#~+lyf9YmyG{p`i0FaH5>=ElJ^kU` zXNd~MsyYt~g|&cg zJ(;yB?ObkT{;lHA)XkW4I}=ISM1#C52}OmeMC20{Q3bw~x>oEb*rn_|fLO?I+|m3hu}g-1 zQsaOo{GHUTWnihppuw;uK^H)jroK4Xk;DPw3;^|XO8c#6aZN;_V@dz6B!Hju;wFY^ z@|YRJaHbMKGU05~Y|*_aseh3WHIkKp& zG5|L-&@<0Tr?s`8=_@uFVZO%vSqR^9B|R7Xb)Zmlv}8DHNJkJKNoeXs*3E5A#};L$ z2@Wdi|EfqS+dS|zL4=G!#n#k8fWv6}nD9RjvSDCd-9#TK%g}?w%wedIhjzD=>_(Vv z6JcWN3v{=@%?S+>o@L7rU9Z?Tj0-Qv@bW^5ar0%NZox=4BnXEE3x+aFO**p0lEmqn zB$r@MvB@x8X_@B~y1MOwwk*AR_3G4+*1WF0oYo{HKSxe#rCs$hTO3v4_plL7Kc>7|^r(-G* zmoL0AWCQmEG3~-!>Pk#eE|ldg)L`vXCwwaTck(8zyW3Zx0K0IS)~hkDm{uqcN+Ae2 zKS_)%cNGmG^I76L>+}HEuUCV%3L{&TE*Yeyh@ve zUj@k>kI;U7m_$!DAEBLaUP5_A+H#(Si2E0L%!G})czEch35CTc$!wBXhXaYW`IjKg zLX=r8)4pCpdOpe9-FRR~*pH@DL;KgzG%@Di&VZINI%+_*g`ISZ7*nUGUg|$&ba0Pp z({sl~3@&;}R!}qe;OBeFRiVGspog&9P#CT*x*E~1a4 z)6>2FTtK4Iph4uGR=jyBGp0HVjNHBy06!(4lcAPAH!!dg-M2t1(P1Vyi@qK}Mi^U0 zy@B0i3KQzta5)ds2pA!0JT#W4vUr#eVLjB5Gc7i}MQ5)!z!S*C6~7i9@6-wAVcRCUepIjvbQ_HlwkMTm(Ra(PBw4R>y~ZM-mD| zA-{JJmhG@uS@ViL9ijcAT?NJq#cFz>E~Q8*!d8=8AU_LhqB#lV-gL;T#6C&Cd+?a5 zR_N~#9cb99i@aWRpq&1-rF+DV7~{Uwl-iQ7|5m=W-A+tGEpL3A-|)8V&)I2xdF0He z&S8`Bj8OYbBy}wega{5Db-P#ta_y-i+$<7y$4|*xiy{`(!=#@*3l}c7{K}i& zTVR4TUU6u}4jCGAN!cpxk}+W?OSqtVmOTeTg- z;EoH-LSTN5%{MeUX9O?ol0Wi4>gmBuBD_MJ`PLCS70(3)gT=DeZ_itsJdajb3~Hb ze>9>Oqf=phz{5h|{FvYp$rp243C??C){DjJKYyObj$7I*9?7%gM*ZF<`b%lpj2J$t z=}1kOVUFixCwiZ-M31%X;W3!grf>Km-}02i!T@Y%a*o+NkjIWszv_?tHanPblCpue z4TsehFZAv_$;IG-o7cl_@a=Al$QTk)uV8^j@`Z(UF)3z_s zsgOZ)yI}Fl(pBaamg5^a-T5|9iKf)bs$4^vGcsu1PHD(?7weAMkIufFsli9h1f&!g zp&Z>W5GOd8n4+=S`4ApjR<@l%gn`myHHQd?6NA?i5z)>^3B>y<8~3qN2cNeON9b9%4YS09$rh0~R#Un| zDMsA!Q!43*naQkh)=wZEr)WpBNesp8I6yio5lRy~);Gc0{N%JLw4`Rb)l|SX$h}`F z--LM0=Q4vaPx5zPc?6limtp5lr+x{X+az9Z zvg3u0btzO)+u`(Qp{XCok}T|SY7l~*Dh+QrJD1UvX7wty_lkB`%VOC!$nv_E#j0L> zJ|!QUhn0|CxZliTHQ6xPl|qpd1|cl#QbkKQ@khCo+Sfzg7D;MgH{9N35cg1IWAFUF zZZMOvSXrON^1#OBq=2$?!bmF6DMi5>>dNakZw_hAu(u3Uyps^5I4|BzfB8E~_5d%V zE?M$8&?)bsM+Y)Qz#@dvIou@C2Ybk?zEShhS%C#cZ8z$Ob3q@Tcn{u4P$MImfmu|O zbidb0H`w5ehu690)1F-jy&H9(XU7O^5$Fa|(wRjZ!ro zGBrSsB$LQC;yMXM0?sMLy(je%PYI zL4di*(hkGZM8f+E+NTuBFxJ6Bcq{2fSm{nTHZyS#jP1&h{l9opAkf`DuAxWCcS*&j7+t5@|)510<{@S8d$w4%3^(gPqHc%Nt zSR{|>GMko@Ns`!4>n#>%i#O}V+hRTHzj>DHLs~T+Eywn2d&Yb=i1SqhKWe1WJX&<$ zJ4U_0{6(Eg98cA%HWC}Zfnc4pR^@XNlu9grR4)=TKtdaQ*}pz za*X5`rNXsXEhL?NmAGK)Eg+EI0@(*m=R_*^5OHXZ4kewV#ZStL9BMpC97p2wt;78K z7r_dFD0LbE{u=>17{mT{HwC>r1nOj}AjlQj!aN@+P*auKjCkq8)C&1V|Kc(AE_X;d z9jQO8%;x$&R>%9eT?XID(Z;|=OcQHBK>TBc+$V_)+=CBf;y7ph>0D-jOF&fI?zdUW zf}@Sr6nuP9ufV+C=fk!H(T);R?COd?mR1PPp2$DzBfjv8#b@o~7$XRw^w_{HrCUHm z!7V@x5B~w0VR7!OPsv_b5WozFAzNe(b&G;~K(Otj)VD!&8(dU}<(2)MDMrc*Hy z4nO4)og*)I{+3(La7&bztOE)7Ib52_;*ND!(xRHe08N_rFwv|Ro5qO>8uaqODR4!i zRWZ0?^!R(s3~GZ#)g7caj#p6j#tahx)snIbii{06cg zhAUHcw{kpd3Lqf8TftcES=Jg#iwv-h?75_O32gM+LM;UhcV-8dxOZE|r$O*o79ips z!)(tyVRP7*;%?hp`o=;|~T+*T?<>0CaMkNl;*p=kGO{NA)>K#ie&T)Y`j31RS9>BK!h zdU%_!nKpwWLKxCJ)=>^*)MW)Naw;(>wcX8}0OR;*WJ3?iq~^7%*U-pklBMiDAP#Mc_(9 zaotGNSX;jh6Q@E0GV&g>;QnyL-V4+-0O$=ZvmH*E-wSk-;YAqHM0c;PzVs~fX(PbJ z_6qh;OW!fj19}mP=gNnkl8>i^7jTyp>eNc1J<;CawSKMp=RBR@jl?}@1tfsw*!M~2 zP*9@cp{mXpN0W?%pF!OVM&jBa;NUd*(8&#%D^*v^iO_LcP6UWIVNV`c9?K~N=fu-X zY!17!ks&h!j<>rdjstPEhLR$Xmeeekq9kw`eT;Yo6{)g`^IoY#62VHTQ%1?fW~)f1 zNU*AyEyL(orx8O!QP?@(er%Nqwg3Oh@;l&U&eq%BaOM4+k+1i^cdQZ>j3uczNj{lY zb*oS05Srb#2?V!rc|HHa8xQTfzr}x_y;OBXu+#aX}t@rNP~0c=$06TL;J z8!VgC^nL(o=!p93W!eUZwixR#+|%#a;>czU3_Sk);}8Dz8@9ZM)P9fn!2hLEHB*S8 z!{n)T@wXRixQ`z$UX{1g1YA!Pw(@I#UV_r+OP5Kx&_t!13j!a3mu!1z%4|JYddu`r zo)rmSC)<83>R57=3ow`4$liW|J#9n>CfJ-Dt}m6BE<|#PbEprOArdVsj*DelrcbXM zV%FX{t}T&>9hBBPZXKS5)CPt(MmQMJ=Z(@qNA5A`1`h+qXKT{2php5arFy*Up=HT) zP6E!s>_LyieGD;{id!Oiv^Z=odq=`#X50YelX7y=R%GL<)(>rcoRbux>k+AepAxV{ zl5pmy$WEeDE?nYli@#B;Jd=&YxZ}Bf0dp(bFV5P!Zx4AmmGuqU<9vGMa}BQK8WIE$)N26zydrz_wxTp%JWhw=+-d2 zGlX(AW;_jot9l0sc8pTyP0u=mmRtR^7z_!iIY>;a&61J!Oc0?=l%EdX8U`9eD(&K72?LR}>u$xQSFwG^aQ^s*!5 z+`m$fm5j)SaV;8clUxOegUTJm@{rIiA(MOam<$NrbPVDnuL`)U+gwe56@wh_IF@r- zn!{%_&>}E`lv!2~w>SL+y}}C4&_gz$g|S+Gks%&1E;(zVCdN^O|5+le;8fw?ux%Cb zw+xYj=_g}r{MefnN}0#GFmVsc9=v;@6vv(sh!^Yv?a6e>+MB)h%St#Zy?_OO7>G!E z3rPl;L@RTpL(bg=LLE{VCMK6$#2*HNprd|9b0p&y0>rs|xhE>B&BN+^XO*D`3MD)K zh<|X5NS!CM*4$gORH_A0MTAwy>5~-iqKSqriV6T48xk*Cm0webN_To^2m8WG9eHzT zEy7R9#J(P8qSuK_ss4ze!_devKp$Wh6EP?h40nUri^t}f*|8%vL#q-jC>W50{v!5@ z0qO&$wZJd{ePEVm6~1U;62?s(=y zL2GL?d*Kb*;+eNbQ^+CLt`ULtBd8jbJ*iMo^SvFodamH#6Qpnk?nL_xzEq#DN+(m4 z-L!MMQ%_OzP|c+8pl^zA0wqQHH|R_DS~@b#+dho79O7l65Y?(wz<*EV1C`XG!5l5a zZ+D9!1eH-x1jisl1#_x5gSlF%!>cA_MX6~6iRLDvjy2<=<&9D%IUSR{ z&Y3q;c70n~fA%H^=F`*shAPU%I2TZWaEhV)9OXVYYvHz1cR|DJUWQd~)?5Dl@H|7q z6aEt|9#?u}$I^I`3-CW8C0Q{U2oY8({BjkuDWUIk^YK(X*5pq6aXmCwBV}htYAj7` zK0ANy07^LM1B}nw@4dsb4*k5ojXO$_!`E9fzcxwg~XaQJoctcT`+yazUk#6a4(?}lm`j2Utf#`4q-?3sx}RUraO%EATmRk z1@OOMLF)syc!SaM16|K5d)v{$FUK+!!0|$+6~#01{=Lv^ zkq^(_U)HMw%&wSnK%sz$Bp9#WA$t}cghL(z0Q5Y@C_@%ORRWKhpb0C`)d;2NCAhk1 zfY{4^ADGsQ{J~+s1Q&v)WUWD5NL2-_UGV-oNFbnknU%PQd$Y~sIm&-lk;eD&BvCtP zru@)r$Ts7^2TPFO^gb(`ioab8{f=NqR&W;&!wK9un%N2@E zd$?vV8&uSIy$yQDVL8UbxddGibBki)B2vP~!fMs7^nndN7f&!FEU2K5N}+KIjnRiX z60ghk0mTVHpyjMfo$m3DE^pFh@Uv+lp7g3YX@KbqmYsyL)xDU7GLybFg!jlGLlIJ! zD9?g~)9K)?$77rs1Ux>=JFTIp$(%okDgmtpK(~u`hD0yFc`&KKxBGk8M?=5cGPyp>>zR!<9skhMqoI^^V78H0 zhxmO9{X@NrM=k{!OTg73kCT*6W+FFvE-e{l4OmlV9n0i@M9Cgy($7M|(4kl)xF3D) zz&cfp5F3IpXU|1vtuh;C=xgZJGzf|-(Wi{nLOjZ8CDQM;IP;M+Mv^Gm6|$R4R0YFD z;W)7B!AkAm6eY8og0>xogLuJ*2X$gEJMo?dpGi9!!5su=b1uSpvb#sB&I3+D0ljNN z5fLa&YRi*oW1)Tx+Zg&w$LULRVPs-PylIed>ch#HV&3RjXdBDXeSnP#GYDjyfG8lJ z_$oU%0tSyr(XjHFi44gQD6?YW5~6o3;)EhtUcknNNuWUWf!IA(!x&{Vr6y}(&nt)A zi}AG>Bj#*FsZQq%*rbw?V+MZk*OMok-FG-W((lOxFnA;rrzrl31nkQqa~uy;2(l<> zb^M|mE~uq(C!rSb221M2pEDO3=2$wH29#p){)xd|XUdMCReZvPQsQ!Fluw7vF*&}_ zAW)NF_hrz*KEJgM%*;L0+5zMper4TY)TLUNT9gT4bJ?zP{PeNu9+;#TQ6Fq+gFMHvWUTz6LQ!YKIB44dt8I(cAsn|5AACA*Anb zYb8AM;icE|=61VYswV)_Gtt+Wh9^CCC{$_Iem&=cPZkItZo*X;-w{^LF#C@_M1H*la zxvc=PBZp7w+owC;FpN?m(xMW!v=VQtdi)`j6J*V!ac9ecN`t`l$VN|F>_X zE&0D0DY@aG&X@q}2Ls;miDdR~_wY~m%B_CK=0rP)UTuq4e?9gyS+|hSR5lW67@t03 zZ9bS76My>u#h~y|{1tDJa2uo0T^d9H>d$+froU2tZpCEJRc(o_zm1kP=Hb|7 z+tHd^1kA|?AT2d~A8auME-p_MSVwgD_#8X0#V3`IHQNir z_qJNr{lZNOw9ZV%_tg!QKr%r8(dod~h{@uExV)Fyy{U3hNE=4S zTZAkPhGrEA4Is)UPaO^Vod~s%bz)%od@jODZq z4Ia-%iQd^`RTFvy6jrs%6jnyB-2cl4f0o( zkuKc3s8TE6EV~opxTp&tpdG_lzblH25OZy^j=gF9 z=FzkPbm%CnApEC7G3*&u_%~hM7GeTvApQmn!b92GUzZwdXZ?k?az5%xAwz0I(o`hQLhSR4r~_WY8)@zI;P-Ew}XPEf<2 z^fh>2>MS3Q*hR#^xv_JIiJo#SrFwuZZ9%~9w+)3v1uPiJ-C7X9BjuIorxib3yC~kh zV&9RZdJB6ku2T+vhYh(&vig&1Wknk_$|L#by?>p6hjx#65b@K*$x@e0y83}KHo9Db zyp5T^11eK>Vb<6n0GOd9q%-goZ}QZ`=CF@~hLc^8l!iP5Tnt;g2+Kt{?DFxd8uD_~ zbny@bHoO~{!~TsV%4~%om;JJL)Sup3=$mC^v+arYf>3no+Ox)2_Ig;fjzfybnh6+& zhN7Jt5U3j;n8hxMjT-s8Q&cZ@MKEi5X~lV3=I#gdiM6c7&~@<^^RUFi@K@65747>} z{`*Fx)2AL_@@%~I6@F~2mkJ6=wrU8b{`ZWE#gS>}$Pg1R86zIeo44AdKr39aZ$t?} zuixTju${=)XLKz3VJjKW1fe*&fQvrAv9c?ggiyTl*lq<|utreC`)~w_cU>~jX=ne| ztjzJO(USF80~CuSdckGk!i64JX!o+W$3qE0ufZAQ&5B6&Nz|DbNgj5u>A||;6NrQ= zRs#h96njf#uH`K{y=J}nM)gMW+GG6z#Opw<24a&jd#t^W*+Vl=^F(Sz2KL-_G8wp1 zHx7HT8#4o7h`>Y%`5Hp-1zP8N*HW-BbQ3FAtqPI@y><1{Ze=utLJ^yAe;CvPgkh#A z;d2Mk3W@EYf@D@>HIwS}=t!4QAIQX-h5^x}Zv=(FjI4p)mx1WOYhI=|3FJx1uQ2^r zQ4S?8DM@)uQ%%270bL1mqJY+F<#_TFLMI;yL2YncLT|nEZRTf3mY?s|6)vd2g^i?o zk77+SEWB{D|9u-@C$jLcIyP1bLU)F6``ttJij<9LU$FR^@Pa7D6;fCMT>(8YdkVZe z#J-fh;S3L(AYr0L00IX1VMC}3C$hYc@r=|wi4tdKC=f#K zuG(UZGXg!@s9A?HT4LFu$fGwiD=*TH4qXZuUW28@I_sF!JlIPWd|HV{Of8^ibg_|! zCk+a4#_}KZx_giu0%NT2QR9g1b90ZZ4Ay`QU<-bV5(1qxFho5RPAJ*MP#RQ& z__brK6aCA!9&dU<0|olOkU}&iF+%NM9T!eTct3Rf{Iyo+#^H6A;v>K~HwlVWBDrRZ zxj5{zO8G@*dF@FH)dG@~nQ{=KNc|y<-$JbGl!03#9tz#e>?!f$DS};hkrgLqhDR=< zk#;Pk5q5{p&s!z6w+!TV`wp!L_QnI94snu|DTn$px{R$JQd8M6BXOht{2O5MdqlIw zA<&z96)M8dHnYuz06-FW^vJ736o<+6ora!DHUk6~=;zu-aNzB!Jb`1$hj5x3A^ynjR<`4j^V!7I_p?M7^F;<-(t=v4uz>_PlBol5AG^#zQ{hHa& z4q@+i{Dtdrs$XV^=LSq|!$nJKB2sRd3= zIg_uE<0w+V!4=KrRHkU42SlYb*^bCLa5&Fe}-Tr?+pX@^zhjX6ie(wAG z{a)KYVM9bo5W=@HC1puhS-`WpOOkeoL~L1>Ryiz69JnmshotM#VHLt03J@PSK$wA$ zkdSom<9GZ11Gi(F4EdIo0Hqs7ImzAbQJJJP1il7X6w~ikIXZF&kunaCQcP2gFSqlw z_bU$tS{0H`h2#N_TD=AH(_OscXVqwd$1nKtn8XU-{A2nv`4yj~eYHK{ewBWO)ZUS3QYVOo{!Mi?wWVd7?4euoLw0z!sT zh-8(_f~{FyY{w=f$93_p%KQ45#9Z2WaeAekDE)tH;vnyC8cvx#cuYrF2ZnILj<<=AR)Fg#m3m9N}E+_v~U8@rPg&P!+=pg(zaF z%!GLqW&$m}f%}EiR!O0vAy+hFa}NVfWsOlClU)657jF&Wx#jZ*GJ6s2W(moPA@B1C z&e8mnc47Lhf3fB_p6*#}u?(DlVL4P$@BeA`{x^97sCVEK=1O<6Sd?=MU`aJA3y{-M zNH^P9&^0F!zoynw7Tx9*-22!x8M56b1zMezH)cm71R6nUpP}QwltXaE3eKhd{REK1 zflPN zR*~;GX*&5{BnDCpA8O8#fLqh5NUnv{Z?!U3@sj7_Y5UDgbye z->@fkkc$YiCOcgU4KK?Vy+ju784Kd>R_(-FrYoj8&+;+Uq#ok6XB}08zY3_{oIPjE z)rRN5%tt)SQzqu-zyavKu|K(zkJANS>=J+2?OakKdYJ4igg(rKw~n z(1XPLi$P3{;p7co?A~*3$bQeHIWQAE7Flw(3~4-b{S`(E`|^=i$FLdkC!z}ia<+W* zd;3hw?B{+5B94(-mzY;RJb2|6%Nyhd@9snbP207WcPT^10gGbAnf7nw*}gFa*XiM& zAz_?*fOpBa32FI)J%_;@d1>TG8s1d|Uzf|kMx0+blS}$O)}Hh+S}5W^xCdj{Mc|5H zHs*d2?@Z|82MbH{H}oKYB;FVJg6bsB;^`_r^h3moIf3wdf`B{46j*Fwwe?6+nVeCs&NbULVL&&x#|OnZ8ZGI_DT z`{dqOH?E4~Wf^W3`$VIb_caxHpn|)?1qeQxVw9KQ@ut_~)B&`2lg zZNo3Y<0mmQes!Vn_@ztT7@NJcs(sJ5_vsJ5_WVf_UtBQh!?FYiB0L0I(eO!`49`P` z3FxjFy$m@(K5v=~Z1K!q9E0w71ajQ#TN(KhAISMBFNS$9a={#8>^aN=B9@Kv**CaG ze=Cuvns5el@9xA`0N;tx{OCsa@hTc-wN^ePb#V8JxKc@7kRSu1(-dSjn=9r1`>m6i}XTYeev79Sx zlg(Tk8s>R=!5YUe8-Ca}>*H%C6fgXS1ibSAJi9xCp!jZ|#8O6fS}fex&;xJ}=ku#L zM`B2{NCnK?Er_xWq&a6}(A@GirC?4f)}UNl2pfTu$ap5*ug`KdMpT7$de6KUYXK17 zK~Q9xlj?#nRRhpp?BiX#bz^!nprY0QE(pLG65N`^a(D?k#igxEo?Ex3eV*h_Co4 ziI*bZ5BLh+payH}Q-}EtA0Cq|<;bixduc0yWB7HDco+z#3|r49+_Q>m6#SQ?H#`Kq zhFt{1o~a7k`suV5IpQ(w!jS3ij3;Ai_W3ugDNA-E&EVM|fGgLUgU~Mgdp9GK57>`j zz>FCSzVl3#Wmx3KUW*#uZWy(Go-B!4K1!Y39U_-0FwH-g=#svFbxQ#?L_UW+kX7KW zN4|hBZim>nIi_;vUoc^!BmAsy`Yx;&>)Y%krE)XgiOMP$2R7Y)(%6y>oJIlQ@iUcC=B$E>g;5r8 zroq5v2j@N93%o)0bZlh{+`96~u&O8H_smGR7Q1xt7D6oSq8%KH&;%(Yj9!fL+~TjI zOQ8tmKo56ov|>AUKHxtDTeSm1tDX#duR)JPr+c-zpch0Q9kc)N7gaYQ* z#&Q5b>-F|Y+%3p+msb(BRrGz_NJm*XZo-@CE{?XgF-M#XZgCU@mU-8*+5gl@^p)?Q z)n}n9tlN9$dc;zX9ZUOAFnnM{e_7$C-d)Z1l2uA<1aD?}_j1Ec8lxOH4mug^gCHES z=BZG1P}L-K%RomKyp`}C{vODibt7RS#~^U%t(!QQR38fk`iB9-?^qz4M~*Tdx&NxR z>KL_M(J85%LsPjtl7#Q#=%F;PdLR`FGH9-3K$=i%lHU_$HmNk&l!2{t01VN*lzK-o z@@XO&3iiWClr3==YgW!JM4p1xE8EENTi)DIz8esy5vbH;OfX$q6?MQwCodkBE=epc ziNSPd+-QH~JL^*t|CD;qdjM6HRbMd30;`+A0UWSsbFl7QU>4rJzb>k@a9>!-mD%n9 zTZR3YL?jeg)9;bfSV`>)s8Dib37Ij`8c#n@)R?p}c=T0JuP875z~kIA<;83SRZj@Y)nXuOOGH{z z8|rB|Aj06J7FM1KSJT>tTwcJm)?6NqQuEx4z(FG(eiU619O{L5T+~NO?aPTw$G8<8 znT9+T>FN}26b6KSpx90yf*otCjt@^nkvP|~jA!d1mNT4Y#nLpB{&{|)zmQYGs8~VY zToa6^Xqm&!$uUR}{X2oKt42F0+>=u-fh?kUUd3M{4OH1_j{fI?FM9&#p4U6oMsyyM+tiIu zXYG3W&NEhaV-g~xt`)7@f=Ar_Wl!%o@lX8j#!RwmqbyNiBZ7x7#5Cf_!S&+doi+n4^bV&1PKf*U8 zZ~(vzT_1THGVF?$q)6z8;T%b(!fQGphlwT-&1Ts7Ttq=e!cnNGMVu?&f0;Hj{x15V zcu%{@YkxJsYz`jCKn0jbz6hx7-K8P0`l_jRGCo6MEsh0A$&|b`@HjUgNYSpD)to^7 zH&JmIutpg|dc5o~8VAL}cMNKGcz43caT4JXPS}OUyS!o zX#PnDm;VT;|Cu#gPFh>Yx4cW3b<5n8ye~tv6%n6E{36;_Z%5oZ8r8)g;N(itcgVM@ zIXshIUvu7G#5Xv@{=Sg!<=~e2B*>CwiptyzYyjc6e!8kfQV;JlBqmcL3q^k~dB)A3pxIu@ zpB^rf#krUJLXjoJ2Q`V?&7a%Vjo-*Ja3A#2qsot z>=N2GR`b~nNe5P?a+>1r7jOQwgLmJdFGV364CIXGtMqBRRyn+GJngMv5e#4JPD$|O z5R{F07sSwK*y%-FNdsE|BfN`rG34XlF|nwlGp`dO%fVz%#Nlq;2`e#hSyEE5k$5Zd z-~vb#rH3At@Iw`i2Fkxr{=iYbc(N~m<(%8~DqNFvw5GF;fY`y0h)tVHSfs`et1F8x zUO2}bE2}BdI0-_=|F|@v6Drr>O=M<_w^HlYtPm^B4cCC*27gX)CKEiv5OBjRI|x9d zwFFHIUtG*S#E?0eK~Qk-k3SxO-ESjIocOOn47E)TOp=^% zWxd%D1RCgiC$I=s)&*sxSt)+Sx=A!9?#ptlbHy^(!Ln4(y5dkc^ym!PXk`Ii7wdZt z>n}b~d0FU@$=wp4w4HY#2KxS@M7Z6)gWa&~I0La6TZ%2kCNo+W6%6}pv+RpWbvuCA zRHKmEM9}71cf3PR7`j@4uVI$NceDa`LmS{AX-EYHvIap9pEDq9=KvUD5GRBQa@kh@ zI&Pk@Pk!hCU(@f+mP%R{TCHQ1b-I@e%=X+*_D+T(O~^b6EiDe612#_`yBlwpEUJRx zMrzV^Oz~hrA9$SPM0fP$Eeqm@$f1!ehC({&f@f{TqDcf)TR(t{_?=H3UFMve>-^5X zX2ZF)`fXWC2G2wD|3z8N9XyJO2h!aYkBA>OfB94zqABYTh^7km4qTs1MrGmNM{bOY zTbv%-nJhDu*z3Xpt<&$BUgVYQv}8$rm^Q``n~slt3;E!2wIyvCc4$Rn)1%}1jdr9}(S(+16>zq%0HPRykmhUfsLL!T zk;L4R+`hCXwuvcBX|8*uY&9P4DFTI<$52HZlm^M8m?fR}DYyM;R?KtcyKJ?Tcj47d z8#=4;=`!HBB!XgR-^O|5 zczZ&11-CdaAb#T2v`u=yTuKcLyo|1}1rR!(D8>lN?BLDFAEV`b4DW$VtjSx*BnO-x z(f2_g1Ct z#4eg ztadE3+vLy-MaDXEYNhuskHz&Qzt-dFEB8SPoq7nWn}%7&X;&ZhmN@^y^ojMp7@F!| z4n_3)I@b`ob7tJV1L5Jo<6;buUup0u-ptn8_!3UZbnU66OkF((-HAyxUARRkJ8MNU z7mHo7@KYaLT8*&t9U)1YSc^}F+Bo4ldZ=vR)-xW# z5l^Pp{1?k-y*>+SxjaqLVosmDES>hBobmF+#;7Z~kS^I|_FpmR{_)l~jq7iWxmQ&d z*`RV##*W8=1@k9?@$z*~oVUh4D{IfJ3eq)TpQqn8sIA|i`^yVNF<`EF+qM1)9EYH3 zNL|mgDkwU_Cx1VmdOCJtqDUA$yj%IzI0m~EhiyGC>9%La8h8lDkZJPSE6p$+pwdIk zYj%~MwDW<}VJ_AfCy#v&Abw&JlWq#>v(CK*+j0j~x+JFa?jcoR7X9D3|1Sv`*m6i_ zFSu_a!jPi$K6%fqx4g@f7#txs5!lkqdGNG>v4Zs*>^X>9KZw@j2A`LBtdbd(s(2hE ze;KWX{m2xKHfrgZ%@zI2UAEOdvov%1KSIKTdB58i@6MV-GOfZhPZ$DO0Hw%yE`y4&JlBSzx&A@0WD zFpwtBy~C%B_oU0jm`Fg(rh(nfZa!MV*@uQJP)2bc1uk@6MBMEh_q;jAH~$AbWIh5O z!v@X)cris#V$Fwr*!Q}Y`AYb7oz2L&VJds3L9F=~-aVPOL+^19W})DfU!BA~5NjGy z1|vw!;hG+5S};#ShuiRamXwQQCcx~IK#r3=2N)wv%Vfs@PAi`0sa`Y18tcpF>lAFR zf7}&xnEM(zYV?&?>%1{|75r-zHsUFQZj0 zq~D%Tp1EhV@O|3OoE=qXRPX2Jy$a@IY^59eVZ@$|SE1W*U#vEA8OZ$$`;m1S&-8Pf z)^vbrO;y;p?2{@7!F7Ve?E?alPW=rv)+6+{rH&E;9kTF4`sEW{*B|g@0XdKNS$pE_ zx1FGmjtM*arx(OHhUNQeoilAPU(^I4xe-c_7Lt+udbeXj5qRatL1ficvFpmYQVAMXX!%nfq2 zRl!9;z=Q2L;N4F1H?aqjT^}SDB`VTa@H25wYQpGuNTCt|u@_)M?p?heOAUI=ZXCZr zz^940Bt8W|gn508m}TjcZ{v{Z@+675*!fX3#$-UjgHnKneRt~kd)iCqprj9`@hQ-Q zuo8kAidaX58q5?`_WkYJdPB>GDrAXD|Bi<#x}Y7Qy%_bL25!jCxvw327kaN#a7?s| z6{;efOQSJj=K8~aiP zX)_;yNZGQM<9Rmw?}ocN5NQWHLNa28rzhfVEbJGzvLA1Bz+{H2Q2`{T4g;17%1CyA z->cKl*bfi7ce#*W2Gg5;Od$r1wd7JckwbDX{9Z$+)UOp&jC=}E4ABs$94ofV4lmW_vi`&yY|xyQz65?y z+F=-aSgEc-C!Py>5F>;yvQ7+_eJ-v4&}^ZXHn<7R9HSjOIxaoB81rMy=lR+We89CB zLQsv*cFlL+X}EYOaw6i4{5T<_Z(G#Y&~zIZydta0@*d)HNn;6e_q`=H@xpc5mxEb0 zaObHGo-tz^?z09n_5c?Na#2Mt>2zacH`{#nmJtd(5OED#%^ARMH*^oaK z*U2FAyE!V3pqixElw8p1w*IV|oX9)|Jn0L!a3uP#XYTO(ayvU?I`B1nb}`*QZ>x`! zy=X-x`80lbmYZ7i)}3xa`%)TWp0`kfQ!kAIpdYl69=XyCOMcq??$>|0)^?10a$%v7 zM2qYC&r*{9(o?RPUdgw(^-L%Kwt5?6KdP$Ij8Pe z&^w)#*XbF?2~5ddScaEr+Z6e0vT* z@ZIp@M4t`!S4Zqe2r*MZ9kXYBw+DMp_bN4Fnedno4vIlfVSv`w?|m>3Knu*huC$-n z(y#J@QoXOiKyxxdZ!I;(bjunQG*r6E;&F6au&AhJQ||A)h0@v(R1w2Y5;VGt7BL*m z6nqHREUG@a`O<^GzVNDh8v9M)0ft!rW%&fy7h-sePsX%1h4h>>k5#Dag+N)mxX_w6 zY#k?e3iALn)%ZLt+jTj#rhJb-p)ki%Eg?D@5+IJDMSpR3%DYe}2ZW$*8!pt^x`wqK^5AMx*p#7tF>D@9~HI z3P}TDFh@w`+~k7~RLT8|HZEF43`~}z&=Z$q(gmi5`@K2s(#W_<;I8%=w8Tl zGKQqLdlLr0f;ur6Za+k}lIFr!+dkPlcr#a(D=?+l4leH5jKrWPqn0+HhC?RLKrlbe z2#5>EZDLMJB?}JO!_W~0(tUc^0?a2POgzeuCo`i($1;y8l7$cQ;2}ClV#;cSVr3Q& z$nNg@3{&sFj3y1+MG7JdlMdC)+(vYu?b(cHnu+pCyuy4Go{>@)U4burrJt__XiTUd zT5nr4Mdt68G%Q}vK%EicP~{!c5m-EDfRnK`1{q1R2#1Z+oRD{V7(FVcW`b8R0OLkC zQtGcJtBYAfn0T{DQeY{iV)dWkYEJrX$GBfmKu6e=j6O(TiSI^`k}FO~SU!i?*#~Cq zs7Nh3e+Af3+X+rNVI^TL30ueWpn@=q*|NC*M3?|TVaG!{Y#rbxEEQg-C+Z_kTCFyXqhoAe&eu% z^|uWQ8xBDk<~2ADuqrh#=ROFNe@|=G$=%Kxd{wgmXl~tA3iqNxekFY_yro`M(8z0B zFr#Dw6i4heIB}F*!d)29eWv*+2cmGo;B@pnQ@DoOijVCtJr*;T8a6qdWrG5lB7X3m z6)$A=BKn&=`G-%O<}bJyl)RnvUbcKl#Gi5z(1YGD0fi1;P4vM>jgoq3$IA9*ghmJ( zbARjHoc7gX%*?ECKR=JU6_KT6A8tN-D%PxZeUW7S_`i4G`f2&cuP%T5;Ue#V)Vs#A z#;Az%XhA&5w0!QWYTc#7i@;*|V zP(DS{-)HZx^QR#Xa(;r8-_1NYvMfo{=PTG~GLn)HQniqBDPA}rWENPa+(E#X+Clq{ z(rR*4WRXUjz(Tr4s0TJ=)#Qr5GN;!SZcOg_i#AEBHe?-qw*>qZ=jp9MCr9v&!Xz3p zLjzRe?hCi^GG%#HN(>7__WrgQA{;j|4e^@Q`60J@VupIbW&GAuD3OeEa^AuurOsRsDdgJ5FKeoNU85skOV=R zkIKeA@Seu4oNDrlU715OYPQrpD)^&fiLMr%l~M+|HHv?I(n*QdT4YVP$f2ta9Zfj8-pWf}c?cAu_@a)9H-PLe z4@^>kcH|}O%k%oDVvKq>oAqt}b(n@Ur?m&eg`b4$G3T)m!tm_Y~TMj zCgkpfri{6Jbej7AEpR zClc}0Z?B18cT0RTG}eBFHAmA*Xke*{JyZ3>OdU4)(_sf5@OmVZDo-yP1W3nHJV7?t z8_!+uBYrji8WRcTYavrrvr%S~Q6@SYPD`+IS=BQ8#0y`Pga=sq|)T8?5S#(^IsTNljdIb;J~z_5}APO%2|W1{kputQgZ?m&9aB z8H3gR9G=@(T<_TF!(y6~CRG}(m@V=Qv83ErLKtR1MHc}UTMAeEAt~hydG%sy7qbKw zg(Q0WYlG>y>#=_OF3VBQ9hdos*y?LB_sHs?yqAXgSJtz9!Zwo}Tuu&_99n5`3GqTd zUODTJE0(VtkhT9#ZMcIjd@0V*QuNJ)Pg{xx=EJANj~yk`P^c*x?^F&~O`bg_C6$wH zGLlR9FW0m%r#}Yto}}f=Z|{ak?8;ZuPP{59@uwY1t0Am7p@dzG2W9B2r=IbVqVF=x zi{#{jGOiFq9&EDIol3ws^B;1gQa}%9j`FbMB^nQMt>7#TqezGK6$-&sPXupl)31PZ ze5D9N<{{lB!+a|F>)gZc{`1X2jy}$QiK%zvgwttQ#SvwI<2{}dCj|gF;c_)<) zUs|m;db(lH%D@RD*8mxak_l^EKT|k1N5%%GWn9hVo^wZ6^a6kX{dtoNf+|uU0cF4$ z%4(B3UqVT!brNUO*AaV8^CUsas$lq<6zRY*i9gmap zbsf|6hv@bXY*ZW(a_gSZW$(UyFM&!*3n@32L0|I6Iar)qk6zjA5ESuqNsc4M207D^ z6{YZJq}2jP+&$8n5?OF4W5hj=dYom+G0PY1J$R^FR^hJ5iSvh9{@JFHo+RSG%IZG5 z#hLsX{X##M#WfPk+T;MAYO*gk?PEP@m~Okec&?Ve=Aim5Hor7_y!OKJ>rLVfTQ z0B3#Tkc?R3!{xziU24D)NRF$jxTQ zD-fe1&ik=PcFvutMiR7?AD*WVqNb8t|$%Vb`f<>qua?!CH@kZVr0VZkrn1+_UaEq6BZ$} zv64uk7IfQtN6i-PoHY52IxY=a|I)LUazzaA{c=!ll4WY}!DvMbbSjIX;iU@T5SzR; zcKOV*kU}nO%N&X<;R3S-jAkdmmxpn7hrWe^#!$j3^r!Dp{Q>$!3Pt?|ueEBNPWPd^qSe zw#pj-lltj3clL2irBNs))Sla4ZQC#}IMXb^?62IGk_s+OcO3W(o-EZUED#UVw_LR- zTV$R}0CL#QvMdZdj@aiKJ$>j{^n?V)?O6}!JoN@B?&Xi=;x7YhdkNcT-XWF(wa*pwC&b{v?GK@K5D@Q*me@nKgeTqwDl~j*=8?)&KN$E> z8Wke1^6Q6YB7mP1l`c)?c{_$=?l_uxC2}LA_83{vyE3)t+_Dc}%9?q%76&O}LC^Wv zNOtLlTofv!P#05zaC`=?uZue#okWwwMNg!HoKEJFv_bYq4-riZp!y0PFknD|t*@>H zxj7R+YJvCxo_f-BWRm=CJI1G0-81!4@SyPnE-Gd}Z^riLR}@Ye?Vw0b$%OEAH453* z!o23PLfV)@i)_z@9m2f`V`R@yQdD$&ZuH;ZUFTg`wo!|2*1@Jg>asHbtJ-pplN&SJ za&uv#C-8*g-z{$>$1T{&agQVD=U0%!ThIZK3WOq`lBb(iXQ26n6Jh&X{s3FYO?`m}GaNW2zyBQ8j3}<))m)GI zKM)NS4BQ~l)WFNfvrH7h|1O!uusXyHpL|5k2Yo>~zuJm$P`w=4`{HH`X)4g%c(S^I z=5lW1pck4l;Vqy1YFR?ltSEaCsgUO$x2?S7^5^Z8>+7gX;6jtjaS5GldO0o`DmLvm zF6mvG3MIgdYRX19YoR#w6hp=TiO{F1+a`_60IP6f@4>(A6WOC{kk z#=AujBU>K!bKmn|fVA@CtO4yt8@F$hb0_bsB5Opr0E& zfd74QN;|d9FN7T*wZL#zCa;;ZAnu2$mRU;+?kx9I>npyo-`5fN4`>`Q`h2{R( zI3u@he&+S4{L}h8*52&25H5dx?wY^tVAwijJdMO~S}$dWfA+FxpoV>PZh7J9=_P&p zH1>o%S)%zCmb}x4)g?!RBNdA7W+82xioZJgMKSRmfB>#`9OkCvE4E14Xs~SxR6*St zJ_h60x9L|-i-3@CcETaO#ltJPhD?@1}p{_@!me?2-3)27rqX ztGfMfQ221ujc>L*89+uMzmr@|u-UxH3{3e_EIYZYds^i5t*)j06XI;LTNy(@{^s+e zCPO1ea`VUxU1Oi0>VA=uY@vK~Oz3grI~zC(0O4<_GQsGRPy6$N;NGsJK&O!iFM@7K zPh7tGKPR5}Qr9|GMVjA@a>a}U`Q1@VsUa?ep5pUk_g+(cUxay!7OP}>zzD`XNFik{@dA}1#U zxw7^g{=k>e?*lG~p%-5$$4}maWZbW|S7SkD(rd9SH!6++G7zia{NnvCIWZa05}075 zoDHt4Il$3A;7CV+fD1JEus|(iGiR|;*Yd|RK9n-qQNo5UuKwM;9 zq~sIh_!6!Y{sU{iOGcK=UGw?)Ted(%n2TwtpI}U(Q_o#%$SY%3dQ58(qieG`X`+dn zVu7$}t+CSq6P7)(GZ)0P^2P*GZF56!y6n)FEoLma0e|QT=Qxz><~TT^(3e#yb^)g) zU!HRoe2+KuY|_?MUB@w_tL(AJNsBw%UULr{fCLWi2e#$!uKT20)vX48t!GZj>x8u4 zag^8`8Ib(RRfv{TlGA{<9T}8yrj8jj_b0gz33xQ2co)TI#BHU4zI;p+g ze5zjsBfQKj!8w}B5iAOa2V@kgXO&y73xoRBH%=AqRfS4r&)&U7)8}rO_57lnkUMVdE0*)cTi(Zd`txgVB6EYk13E3@l?7!e zcmfJ3WQ87p22?t6JXW86BBTDJ=ZCr;q)O-fjr0VNNBjc|K_O*Z>+Qy3$FAny-ZF<) zN97;aaG#;Lup;eNdr<%V)0x@MRF97|)^9ICL-b)bnRlS6Sm%#bhK%}>v zA2#x;o(D4qbKS6wQL?7Sr1_lqSf>ak^TZLb-SL_MB zW#UW~kN1Thyw7Q)&At4h=-*RNh8iax(Xl;f%h^1KwNQBhoj0%Zo@aElGh(=db^D0| zCf#S0tywrf3d&%`m(U0RUr4TI zPLaq0tNnoLI|D-Dmvn6sLD&}|^6I#YHc;NhM>icAe#V>CmN2s|pgrbF`YAg(zwnT}b+T zb@91nWh4-0z-4Cq;Q^LnWfk4Z)+(@+iHQt4`sJ5jG`_8}Jazkl>3kA`k=IIm$NHz+ z51a*Fg>0MDgAr9_eINZS4Jl<aVjV%8<(V#LPLskf2vIhbS4EBdY#3fC4*&`ZwcWLv>n6a^PcRw6yHVF zEgk5LLM}ua?!t94avi}s$&{)ryJnC4jpX<1@x`eNs7Z2!*&mNgmAPld@@6@aArrP( zcOlCiJ59{yHUrJMaG(NGP%h>aLn+A`WQ9qp2(1uZNM?PG3QBLzlfyxSBw29tG{L5* z*#Joyn`gwgBpELd##W8(t)`$cQN0S$o;awnXbF@62K+3UYyrXtZ!TlubgR3@tI;-{Ae{ z9{Cmivfir6{0q2T0Q_1{v<<8t55pzu-f$?zsfJ#-g?tyK9t;9ZNk5|J;mk|2L+7z; zyt4^&*8W;YS)r)NlfRwR)n4~=zlxw^zY>>N9?B z21#vAmQ3N_0uyfKYTZr|HjV}L-Xhk~6E6gn*kQI78iv$Zf8p$kbFS`P@_*|-zMx>8 z<&BG*Azk(C7d7Kvkka3>KueP__a3`F;8|dlyF1aI6jrg7%m7w&o_CMVYcLqAj`3Zv z)I-1Sf?8?b!A6YO~kbcc>;4sH&+=(K(gT|M6d!1YAuq zflKB|O)-H>s-jb#&tC(PG+k=m>DkHeUAol&$1e5%JpwKgAo}G0ByT6YiwE1;#q?sIDa*F`i9m8;rOz*|kb;RVR~Z zF(z5PD~sE=sKeI(_oa3YqH{n{ZH?WGFcO^ut#k5Xe!8uETt#4Y$jpB3!_^0mTFkiE zmATn>m~-K*p_-K-#(1l!MzGT^}Umf*eXGgB~!O8S?2h)~(tf47&o*YQaBxZ2JLFL-~! zf#wT$U3c{7W)R||L1>XAj&^HIO=FgbLiQLkG#EpB(oMwr&B@TEx00VKpBp);AAC>} zqf69r*7Z>YHF^fGR+Zr42`^z$y9Wkjh?mEUGmON?!_E2Pj8CM z>X}GpmnVCh-5L+CG9KH04pAG02Sd$|vv&EJ?-84sDRU7HUi_TU2mApzS;TaG4;0U4 z5@Er8B=%}W{dC;LARmcjNdM)0G?V03nQRMWNSg#FEe9vvQq_?AC#wxL<5YANl%0%n z0#=v&%A%OD1WwSp-y>z5x=rhZE(n-$U9-QUt9>pNCCr4%1enrWDJvw2>WxYpSzlHr zT$GlJ0l(;Ih;>}C-XQND8C%T|J7PetC&U7gN5OAVU&wxr%wB&CEK|z=dJFyunL@Yt zVs(JD&W`8+O@U=QeY>JC+Ch&kjElL*91Wwjy*(Q!&EkHs^ZOM^oCT!`=XXT;yb*st zv8R+6i8r5(OldWt8^u9ak6K>ANuuO<^4=z@MoiPYh1A#&UAPdyyCjO^zgZf8Ij!kQ z@Vz`IH=!VR0{ZeOve7wvwJgWIO0JFxr0`R`Pu}qoGJ#3^*Sn`qilfOpo>`MK>i{o9 z#`vOUkS(OGW#6o=kI)jv{h7@X63^WZXNVaR~9pwe5J`bFBHEDWIQ8QOr>6bO4s>>bAPh=!ij~#2zTAUwU z_VPC^a*ll+9aUSpuOK_F-;DKb&BZcfF{AW%q@$@TRHugeLdaGFfi^`U0XlPCCIXtJbyo!O;hS=h-;h@(*%D-Y6*%f1M~n{o|Zk{OrOeF(gKLo3IHf7Y-fYuhA}0!kEo<61nL517Y74 zOAm2<3sics3P^zHxRE=Lu!vyi)ZjvD7H75;)+0Tp0__+l-|Z(-t^F`HbT)@l627;Rw~+WiCT|+T@S7F^N{GyzT3uJlZ_E6?4 za)U<3^B`)){(l?8o;u}g=OpBVCXXqO1!Y)2v{Ni*exN8n-F{*Yf*+wWCRqd?C$$EEtwifwYQ>Zp ze%Cpxt?ihqv~Q<%+W^&^2sEPkfE1D~@rtzH7~j_yW!7OAjsytx`jx+NFxao0un9hVx1ac-;c<5-IQTCj*{=Sgq~vqub9OFy zAr=d17Ks=?FM=%@^dO-#c{S_hWM%}syWAs@cttmelP&crUjYEY$Mysb$e3_Nl0ELn z)QWLcXd)J_3@C~Hp-g73oo?iOFt`y7cs^5*buZ=abtGZgTaDO>kX`M1A#dCZqdVw<6)qeF1QYcCNh~H5X ze09#5$8X@_h$JG>*btNxNDHe9lS6llo2 z&i*5)4r|mlo~ctb^By`7>E~mwe!eVaCj@O0+ySp^Bbh5`8QgRPeuJ!$s5~y;Nms!Z zIN98N6L^=9lzse<2LPSJLNe-YDm6U4>u|E(6#SN?*i%#|t#~2#Y;B`Csz*gyk^%gu zb3RvY@Sk7)S40{F@V;ei-AGaHg-4Wl)g+#PSuNp{ai&Xg>i6o>)xS1@{r+=(c?$9Q zluhQ8|GYHSJ7q_{G(c^7KwUy$nm#fAk0LPLo9uMv`WvTOhNxBfkDE%?AW?CA`%IN( z4@@Y`d2ncMxh(yif|Atf0ZIOe>Q$Cd8|oA$(~|27&LFsJ)os$}U`dgdSW zzW=IbO;JU&nC3|^G3gI~X(Trq4BLiH&N*M__czoRAt$d(oh=Gh2nHz$XL$>Ck=R}q{HZsI+xb%%)w8r{iJM;3E zy}X)DGm9aO6)cPBl{9Vz+SiI)Q1JNFLtcZ4l!Bs>qXXfSgM|RP><+tNtwv#P!^+G%xUd7OLr!;P-KI>9XOZvVi%Q!{SyAEB?IW zKHBG2$T5r4A`MM2Ve!2;*G0L)VMt&mckSBMul(B?q3>FjQLO;BGG!NE)T9>C%4i;) zrkD>`fAjy`$m*@p1J>v+*WY?CsX*d8;Ui-_MF?D;Y~7aPB?nkMA@VB#sV=OXp3}HT;?F8!5)M>% zRoA*w&Dca}Ru(9#@!Noet8B+OYodV-Y{9w6m%h7h))5jFU08&P(ybuV;BO({_MhX; zp~LJghdwKJ&6n9S-P)M@J_P>%a%P}cizE>Ag=Z8cmn)gd+vpL{?WY49XT^WlF`+Gd z3y-c0ouzJ!{Cg(I-4dR1kp5aROQR>>f*mpD@pIeA8FDfhVXqp29yk=Dki}129{6t< z-9bz;`mXtbwO6I$&h~nN~sF zV4Br$q{+LF75e3)r?28hhlp9B*|B+4ehg&kwodAC<5V$-#NnygnYu$+oP%hSMHe#| zjR!1wcOA2KFN$k8fC;l^CJ?>_P6cA5a{bAypo{uD^AXss&G+s7{Ur~87w!Am@~{7*8Y z;1%8Cul_0Bo;ZM}gERw74cfwX5(9AQb;KPcb4}p@gpK_^_-rN@SqW+02-@)az=QD4Ri<-2KxWC*0Oyvu={j=-LuYT6_i4?8AC zSM${T2SHq=)w>_`-t0zR$aN~4h#QCI#Xl$Qf zf}{H7fFy(_V18;@N5Nd>Ab!~_XIb`U)cx*yvusM;CR%~$zJ-Wmf3x&3eQejZL#=mUH(gdcIiDrSXF zX1v<%l4KvqA)*)b!D5hyVAR(zopM4b^cNCj>1btuTek`xdz$u1V#kj|5^Df0j1F;h z3{`>zV07dwMl|@Oq1;NhOd(qFyj~doN3(juYx6NN!k69BdxzTpzRpeEHng6G3aoUMVM}~ES9|;51^D(fpi(lBjH`6QOpAqP{u^j9huKPf+or@ zPs_n&z$=JEJ%!NI_#;2`+>sh)qqPn_Jewu)G<~tlVDVMA-kxrN1i>|6C_pPJZu5QJ z*2!R&Cn>Bsa+eqhC)Sh z0hl0vU=K`UgaF=aS$P()ANk=*a9%o@|bG~ge z3iBv03t2RjpN4eq~t>TOY@`3Klf+RG7*)ns!&l*sQ8Qg&x}(N;g;piCl>! z_(za5p8Yl;C+lItm9f;?rQ}OWLTQRNHR^=}>5;pY$5y+xk{j5L37}^+ebPlLa#U7? zgWIdSz*dLg_}4pQccu6PW9B;5rHo>U$GLMCs4aSTh!PB5&wAN0NZCsnQ6)~ePCko$ zC}LVbr#Ge~*Zd=y(bYn~SmJuClm!IsDdUOXHF$(={kS7gE;&C)%UFEw5GBo4JbCYI z-~(dl055reVTh+9xr8=LN=(9lK9+1*K6Uhe%j&J@%f_WkyL_DAz3A=Zme zQ$>N^kbZPAct~sPY<~@vP}>epK};2@oaP3%F{cd=J^hgxC^Tn`0aA^&eG*jsdd=xh z-ALeZdSe>yEvfES-t%6DnFTES{mK)t#Du-r`bupIRfPIvbGXkp&{{sD_WYb%kG5sI zU@pr#GG#FonfbKIErMsJENN#k0K=dE!Ax(_dX%e zjDsuWk8;}UltjMBjlKQQv_b%NjIt%Dg5jY{J9+w>qJUTgB9igWLc!45a9O0&AvPs{ zROY5pMmc|ymH>mpYQCdWm-*1A9SCpxC6RqtO!u#kSEjT+%$^%kf|;-Lf=1 z^)PL(GoC>ZBAWJ&Y_TY$ibs#wOENA#t9&oU>#x5a1D%^i*d<9wA>S&kNJu+*NGjG@ z=rA)7dnW5F7U)xuoyL$s7aLU?075GqI+d3P*1Z#7d+o9u%7x3(KO6{2XLmq@U)b9> zE0^W#NNBj62BRbCl@N+9V#bq8|KK+slPQDF`};vLeW!LH!}O-1!EnNs@j;yHiPI&D zp@RnOBc5AybB_uV^JvOkWG`l6NS|iI2?XjTa%(!VE{G_Z6fpy;ut*8c#8{KW9&9%6 zjzBLx7<(?k#8m}9D>rXJh1nm)z3Xp5t6I*UN0K*|EPnVaImD740&vQklbW#)NEjm< zdIJ*kF?3G_nKbaM&%p|Ul=s=_=ml(trE{|%pf3gr8Un@UL-UL`Tc+fLkXJhq)_NlA z@c7}|FjN(hd9_&|2gh}Q>vMcrgeQgyU)68p=IT0y&Jj^>ALo`Df`0d5T zYMBX|{BldA$>rmt4n@KTg;tI+P<9TMXJ_OwxC}qC;Eo9>5%Dy+u!%7TKEQDTpX`Kk#kr@T(gN&e`$ z4G-M+!w*Hwew3#uU4g+7lBhEyVbjR? z7TW9if+r1&0w$ksS9HOX;(?&l9iq{x3*zQkN6#UKb4K6AtY_=?)3wHW>LxJMnejVrHOBZRW*rfZ=Lz+{|Yizh8ImR8F6nugaJQqqT{2Pl0N?YwIwo!o_( z#?fRH3i_Da|KaifYXjbGSxu>QJbufaTMxBM+B^Eem+zX>-GISpJce7pV`!-}?KnXM z6=!sKHnj8yCmtL;ahGK_=wRwPF}MhThfIQfC9*l+Y>7U0|FnMKXL>F1o5%WYSy=_> zzS*@q#~rXN!us=LO7qDQ{PeHPPD9+c`8XpZsv?U(=XT?g_o4*~T{7Qn=}Ss+FbP@) z#;=AsdWWs&jBN!rQ1b2_-5_E{Jvr2z1S4Xagg9B9muAR+A4>?~_<0 zpPSwAU~w!fQ;^lrqGp$Q3*S7w<8y|4o4@sSYbVctGxj3;snc&}<8JH!QHb?dnA4pJ zvpe~{OB{{Hs{?E2|8pD-GhiSRB}&+@#C|_NWdnO-kgYI9Tb6Q+M+aRw=r?rAt{r@t zk79Nbi8Xmto{G{|S+~l^|Cijvw}~q}Qx*Jm(HoteG!B~+6t?5!k8`wR&gkK}`ZV}? znWrSzVvWPE1 z#uS9Z$tO$Ka9E|0$Ep*u0pd5Sv5_{Sp82*XOUfCx@tV}-t2T8^_^xAxGdC=S$a^ zf5AQ)!1J#0Xrq~%J{~@1(_gTxTT&AmMw7p_{F5H|=+fLA3MQM*c<^||@7D8u&qkB<)06ujzA~pv3q#@~ zXoM9nL*D?xsK(|pDl-$9F|_wcVj-%gu&DB_PFlN*5YL1v*IJqInR$=)=+KvUdvou=MMVC z!T34Eygj--^VV0}g6j;0L6>5d6z0q`RS1^P7+ddO$WiE;BVl>x`OsvY_RyH#G{URH zUYi3EC4*i zm)bhb(dT)OBO918sl|+FqG^cD#>RmjU!G;Ni#HP=V8#HdZMiLh)SYp}RZHt5T+9uu zS)=p=aO*mM*GIOn|#&G->mOdP9cln2=BGK#M}%RYof9Z|6d+R?q|p`O#tEu7VOk)aaWpK3=m z{{`xRI0bKy44H;Lh_fTzpyJv6DpQZ0^p&kRoVL1=i9QeCuzcSZ-*9qC7{m?a^b)`v zA~tbJ1v?xJF%b|Ek1+DCa)%^l&MqqA15TDXYjejjFe*T^{}>vrdY07_2nN0U#KT3F ze^J+B$iJojlhDYAlckZXhEK4nUqugkgD`A@63C$ZE#$Y(>YBpzySLmH`tsVriM#f} z`mJ_f?CVYKo@(qxzJ06Vh-5)f{vQefFO6xI)E%xjlbtO!Zy+juIPzbhtTE-JqHKlL z8kj?D)^pQB@MXYA`sCepmh|17+D_?_vUtqs_HEsdaQfHUvJX_AqjtO94JOwmn7x956f0*J}mgy@3ikyHOgk^RR6!0s@{C>-aA+hZ^Oy?)i43A~OZ?9=BT9vRu zKEttz!lwlzk$!~9B0(z=m8cVDVqRiuWzW=g6EhC#i{Ia`X{w~B2GMKmQWZwa?IYtijVvDo z2RrKqMZ3HOW`%}_)p4B66KD`|k*l9YtbP~yUdhOBi&?@PxCbJSKY&e-893QMdsCSr zLM2%+R5b@^8rK~B8G$_X-0HVKrn~Sc=r6&&Yz>JNpG3OVlKUshssTmV!49W2ayT`)c?v;q@Ns$YH1f}HnPN}$p0@e>{at3qAsLF#w z#4}^1L=&k4E*Ci*vVUW`6017qv*{P_%g^eHXO1qhB+ zVHX(%g&E$>cF6Mg z0=~inNkC1bJL9Pl-2DoYd)x5+qHalwcwTUMgQPE03_#QElkdwAu5{7rITld7MxuT2v!!?=wQiPHOu`z1~bcfA{4L~U1f z=QpP4Mbcc0(u#;=1-}JnMTzBDg7b-xMMI)gU`*9{XCY>xEH{%~|D8zjaLDXq< z>C%NcXWviiidH5y7ya!c_dmOs7fvuKxr(Xftx}VElvXS2l{hnSQ%tTf)T64Azvz(0 zo_iN0Jk{+dz85Fil%>NSE+WFhKN6rZ+m+<+Qik8C;RF|=*&iP+;^xIaB+0q{hppzf zESuOEb^Jx4f^b0&&c(cWiVzFBU_6J1gjRiA_wMj>vy{xv?4`MpS2V}Q*3dlshdL@; z%w+#~Jhvh8|I-DY32R{HMt%U2E5GslVs&WdA_)qKI=bMG@F!UQCe9GOiQkG{B8oK+ zq=nmG7M8C93tO_T6ue^ZmbVDE?!lpza}9C}$hv4=*cG;Ja@E7tCgh+{TwjbZE#Lf- zxF5@ZpnYgjd&5chYW&Y(+SdgdTnVOtI@CnjGSI2g58Q$BE9K!$$4G3{h18?!xuQ8G zS#2K%Oopqqkwc$LB<((dRK@~4npzXyL|uF~`$Q{&&SK_=f&9is(61aO_45ou&4}oZ zr@;YHQ@_Fl7w!2K5fT%v-yy1mvb=8v~kZ}*S#@?QZ?1& zfr;hQ@F=)CtX{o(jh)wY{=!T+c=RMK@2wtW(RBn-UBEn}95bmM4SWqH`xB!M={e0< z5O+6-J_yx4%c*J(IL&BcP2n|yo32PoFC&hHXe znC1b@w0$1vQ0)j15s)9)OrF{gH7mrPE0+<*_Hth5ilv{3I54$Yr~*Ud;^JiN0d9|V zBl`q!9lX#$EHlY!vIAu0tf)1T#Q4yO6C@HaRgiZl`XozX}I4U8%Jyu3S$>=;wWNM&=ZrtcYg=kBM{u&O_ zQbtvHmW{rBYFga~M_oH}gI$MHzsiZKJ@?vt?6kER`pf#zx0iXUL8pVoNYt@ad6o2p zSTOmY3l30t(WtYpvr;2v-bX$$G~KQ871D~@vt%_Y1jes6%KQX2{WS`61Tc_OSy4dk zLANC0;sqSo$e7BzB1f+rzMPM|0UD5dm@YWB(+tjl0}7ScIhL$FBZpH&up6e3YT8WB z6}1D=BLD5^J@lIV5_p=hs*~&?o~UmZx0IN`fCpv7TebDC3Kt(dNg>}NuaM5a201=E z91d-pz3SU_Neoy3sGJ9c8~7R~f}oN3iRqvt{gp*Ve^^C}RoFeq^-v0ACV2=rEXvTj zGBA98K03N106$K%ZKPCwTRo5!1#dqHM&Ozil)Vxj*U1>?d}G|^s5s;tuBAH?Q!*%#l+TIW{W&^pvbl1XiW5=m8N61%+h{>wXQC~b6eN^-(>DQ+Esm6 z;PG>ki_LuBu{{Ci!V`9xV{wO>#z}&X8w{Ll*?a6@>}n-PvW@yVkDba~b6WiZnaM-9 zNrXC?u$&ld?5oWVSKnNwk9WoToVd)~xbms2?r!Nlv!#KELl>=%<(esi2GxZ`-v|aXjSsDz7dt zC}T}$Fnsw<5NPD$f@5uf&Jc>7xI?RS;~vGpaa1(cb+Sa)UxQ#tBMd2~@bJUk*!{zB zsv<0DYKO?=@sglL79@D=V8zKx!*-VgMNH>usZurdfy>Y&k7M40oWxMQ;`n}X zxu6>4bIBYY`oEDDyKwIxC3^ zDVwN|)fzzq(8l}|PL7atJT9J91cK0hW17jT`w-E>2MA4dOCmGT%W}J{kc)sf0SP^P zP+JsIK?X_y?a(yS!V%!5CT7_AZp1m{^g|d%#y-UI7+yZx5&8|e)&;wNFGz;RRlKLf z5{k)1NIHQdS#wMDfRQr_PHQfs#U?^SD`}I`^K%+!*|kb*794we^fhmQbwtnUqM4AovDa?Dgm}I-BLQ+J5YGH}SOmeSIK^_iXTl zlM`(It-i+n>zIZz;ftwsA$j+S{+I@kxnny9Ql0u>Y`w+Oe;8hR*@-V_+%VG{uwl+a zutG{cfB?T3?sQRc1c|Nh_uz=fznnv{ko0W_xudfhePuZ&wVLB{oDT9|@oc8yuoCeq zkXVH@AA~@aJ+rc|)er)ADIiDZ!ypQn{LrmATi-Q5;ML`r-B)z1zx4;W#UU_R1R&gR zHs&;Y`gGN%IvUdPv|wIM9m%9D6CJePGBm&7=tGZl0n-XPFhB^bX#X7A>KTf_rKAYY ze4S{0I9}Lw8)8S9$hQ2U1E`SQ(QE2cDqPdU3Q9=tKT{rw`Lxo=Sl#k0)@^5~QPZcK zb>g(`s1qr`wAK6%+w(*duLfV8<*^}?f3~H_{quydE!|N95-Z3^4)|WNwJqdR!YOkZ zyLuV=;6*9j_V}5k`dz#YPN$69k#-|;0xcG;a`xlf^mXca=UFupMca_Rm zOIx*xErp@S*BuY$Tkmj7&CU$F&rT>HNu6;bho%N;eiZIz*f}NB7E)3|$lnj`m9q`> z3W+d&H&9l9Qx-2DyFsI7qwp}82r&U8DIl#b8d;*i%CL*rOGT^BY!*f&H%IguBE2fD z$+mJhSjPsK6|BF=!W12@{Ncuk4;VScATXr9yGQN1u?en@nPyF@uvt!YP$P*I%;8*< zrSc%9C6l}HCT_Ln;!1|)Q~FTJJRem?6tvO!6lWkO9FOkYfC`3=vD@mrqbbyOhv+;{ z4sr|~arlnGwI|(_Pk?O1OIgUi2(u%IhWvsQxu(|zE808cQ?$GDvu4B89}mP44X~e zewDBM3!zY~%_g$+r52DYwgCGbiTnkx=a1o(Yxr%5Y9E&q^)>YL+;$0++zkh?)qD5t zYhK7nJv;0*c5Z(wFb;wSlC#L1+tUlyp$6mD0%VVq`jQh=Xi?Rx&R>^gt-XS*o2wWPD{w z!#3>6y0MutBqSXrj-FBKiqr}6ljCfLq@VS?9AdesYO@YLlHEhK4u`-@-=n@7lYWk- zgck68?1Gn6x)Q}Wnt&0#h9Y~79nx5ZBLIfYp3MO(wH7Krgc;#ENS)QA+e7V)c`^4& zQOF1r!U{S`uCv9oFepNbC1+VNBDDiS{|PAMydYS#^T!|l+p0};_;U>6*qa{9v`^KB zmU#Z`?$$RVQ8DCOO@;_ZiO5VjYYZjQv-|DeEQK0peiCA`GvFWW`W(*ym#RL%m2NwR z$`J~c_{*R-vTr0)R~iT2cGML6=OAvU4x$I?>xHSa8=~;R@Yb7**`W;Qxz2byoJy?( zh^6C{Z;P{jCn(Rx8eE=t1CuUJGyYOT_OO+>0#BiB@ukT4Ul2uL9aSPGN` zwTdkVBSi?&gb5cxZiW`QnZSf3FesAS5GIq%%sy+aJqZaQ+H=02b3VWG>mRm|gvrd_ z?|$FsS@*~-mrK5(;HiOS`$MegMTcn>kt(%k+zu^i(Pq}M%|Fqf`pWNWO-^SWJ|V1@3>IMc*) ze&eG2vahLkH00;X7c&QHqv5RAg46-o^=W>hRNmO=JhN7#J~n}7*X8{uP4B!PCkF~O zf5Mrm!(HCI8*wOcC}h5fPeJm!{~6Q+I9xNZeCYV+m=WThFQ-$E^8gx647>AgMIp<> z*c=)9cke9NO3n&b&xC&1^mqt4X8th3xj9g&Ja?R2aL7hA&tD&YW+ zSwCTpv;}U?slpWu=99HO+_B@%W-BbT5$+>h!#L;GEbdBZ>%%nt7X%oXDJ8MsQ-$ln zCZGqjm^M7c3(p?OvR`#+j(4+ntfJq{2TBsg1E$$`%(W7#j|lXtrKxqEZe8q34eTX2 zmjFJyhBT0i*|!JG`=H_ms@Uu^);g{g`KH;gvAPajwe$byKjh)_uG9Y)$it2`B*-;q z*;-5NmzH(l`S)1M`-1-}n3}|Jnbh_jKX! z`;U=f%WLxg;H5T4h>?Pd+;*IeQX_R;=a@!Jgj<3(pS+j>94r!hJ!B_ZO$Qw9r8GEd zAyPHn*2a6$Wn61+=42%&ZC8$5ojA?efU;c4BI-(M|62^Ymp}ehPU9%0!5DA&v0SKd z8T2W`Clcxu1WB%B^T(vty!sM5bg(d$`ONIW65&Zi!u-O1DOZUw&-(g67xx#-%ftbK zMaWYsrs&}#o&a&&I_F7VD}e|=KRDkTcC>t$kSTUoBNNC|R5}_bn?gh-CK6GdWT56- zVlcWfbmiO*QlI6%h^t(1thNfna$$(uz;2O9M=AOg2|SsE%psEvLl1ep5bJ}0tt1hB z5XsMV&T^LFtx{WfXj@xW<2^My z!f8%X(9L$Qa~}d)f{*ns8s8p+gXIuR);nAPO1Z%9r^O8sP3H;z_NKQ*!Xe&Pug`(| z$#fn(gNIP|)ses2;`f<1{cpJbnK7CqkRV6UEF!HS0=cd$BI|F|edln2b~dG4;C0Rt zxh#Qpu^FpITIIN3s*7%leCG{U%g}N?wrk;@80e}zyb=Dr_cKA}%iVKcdg%n>1XbSW z+suwFKHuN*9${>Kc>(hT?#?Pqc>*vOmC!#**K=#W!T)Ph!Zpat`vEQ+M|R#HX>1NZTEB37*}q0Z{fRfG zETIcaJ+s;xj1su^C*-5)j1lXDS0k9s#M^e3-aES%THqRu@}%cw)z!FA%&}rvj&UOX zFzswo@pk9-Sa(mRJ(jI?Rd(UK@?|S+QL%gpu&G6zB=ksJ`UH2MuRoa=QBTf@fhIx}d2~GSa5UE< z9`r&;v~NcKP_A_b=5?`e5dYKm!KqNZ&Xlm^FW-&&%)EvNiQYUhHHNs&cE=D4ruP?O1|1MFu zT@(MNdAt6Pv+m=ZY9+tcFFBl>_8Wi)xK*~QBC!`rArB8wo#4|nYq&qlbAODD>Cc~W z>y0|eco}>xYfG~`Z{Dm%6tP$kv@MB*b=L_(oNR>2G%OUbGCd81#2+hAi3nA=5+!8Q z#8+=qps=Q&;QN+!6DmVOVa%sV$7|=xAiqYXBNFam#Ydqb>;=e+&Q0FRO0BouJ-NYX>-tj@^f9ncziOMuie*_2*6k zJXK}~6-hHb@qF8^VD8ulf~&w54O~xZ-rRBIck%kBV2CjS2g}hvxZRCZS9b5@RS+Y? zM4B@bgi0B(L?bFjb^xfo|KnhSY%~D+r~FiG8Ps?#*phTCpc#V;E;}PJ8>8Y%g}(C95&oDnWmQh7s50}vCk@EVnM!^4 zh(KW#t4yr{)s>pKR%Yd-jHh@ z?+Y=-FfF@A0)aliiC z`0(7W*dySea>(GB0&C?~B`iynEGT~wlA3J$d4-ILB$=U_$V3?PGG9Z~FJdC-%d; z4jp2l9X4<+wZ$VdWOIkM zIFU=3cjV*k(IGk5zFih9@7~mv^e`_2DJkdf#Bag|wPyv7z`mB?`=&DIR~*=eOC0WY^WT_VVMvzOR*q{5|AS@eEKX0sow2NPu|1y1 zow~VaS}Q;cW5DvJHw$R69TaSWv)JasKwIRO8Ts2TNrtL-m2i6sOQxZ0vr-Q2lv|4S zK*kx7^TCetx7v4K^&qm-pt1gmxsY~g5mUDH&fLURJE<_%JJE6gqjxn4ToKkOJjjL# z%x9!`(zD+Vg1|yLJkjDAkWE^9>-q#LCx*h!Jlqb+?z>3vp4k?}LB}pm?)JTMgC2=y zB_2Nd^;VJhrm~}cak-quq{X9K7!9OJ(w3Mr{&?4o$GRrdyHpv15f5M-nZ3WCY7ks? zx}u_nz?7k(DQSw$kBbkdbd>trS#A@2kwdXTr zon4NJ@6Q#qW&N+MQ`jK*uyg#~X=om$a^Sa|SD9V5ym;5QeT;z( zfe(+Zi+f?Ac?UaZs61(VJFCR`)#TjYuK88fboe5^KAKi(u!3K-gf82ITc6vk0+n9E z7KIzemfQaI8vH;jhC64Qx1mXBO-qpGkVH%MzpLWhv@>Xua&CDy)xExd=+ zW3qbpJNJB=U-W5M6}d&tn$%k7Yj7;FaGdPgdzYo1ZTQWW$iP=OXW9=I+`YsxcjFsF zj@(=Jo1-Py@!S5f4}ii>Vt{kkb8AXwEGcxW=AKnTQ~S{F;;QBha6({c&Lm(m(Z>$G zaO_iCz;Y@+x9`DH57bE2E<8BJbAYr?#?O4tQW*Mp?#KD3&a6lRHt)z@Yj-5%@4lsJ zw|9O)h^uK%>`ew2_98e?;j2K6K|}6**E7bm4hK7hIkHCEivV3fn+*@OrsseBDR4J_ zdkgi5$AN%?E6Vvdk1`CVx3HL}6aJibR)|9K+=C_51vH&ujaAy5mN>fepGl1Ox}@nt z^ZM(PZybNZkh_}B_M$n0?M@}xbI|xW_k&BM{fEVuPH>bIEg$By#!{{Nn@3D?oL?(v#2vyW`4J55!%~i!lu?A*8v8Wuz$D9Epq7qnktx zxOi}@A+eZ~VBpoX>%0E~+Z|{7OQHL&1|q~@KiS*E8Boa!^gmEp4d~<}6bmg6AoEh= z+@Pz0S;mA^(RJ_S0R(e_mX-mp?C;$&{ifv&F!Mq3kwq}6r_G)kf^M;$TY%Rsakzlf zOLH{KdZy)z`lwxOTDj-_$xLPIPHsu_*K6O>Q@QDu>V-`&_Tc8b1opZqVJ+9*d9Qcg zU)zgU*NE!2e7EOqoaFmm;$ydX*Z3{9(=T;-&wuUz1ZK6o2miU$&8H1m)tfG{s%y=i z=Nh`|*~822;@JsjBjU;K=h%5G?ov<51(VKKsSBk!Jh4?xSy872wAZzKo2@SOE*(b+ z5QznkPC{&N%!b6^BlGg(1B~5LB~27oova3hvMguU-{RS=HKST?iOqM7r_7Z{K-f+L z1baU^i9DJxsAL+2vUei%T8Qz$zD-HGh(xRxQW|G~1-CTRY zAkDa7P_`-Lb^kZ>(OO+YR@RNI}f5pFAS%~~L42VEZ4xW_SX)m3bO32V#clTph_ ziXYAFU2;w7nzyN|X2EC+2)kaBE)=ZgKXQ;r6tJCr(D~|R>AlQZr;>Ar+*y~Cyz_?e z=&H|>YVh^h2d69<7VHjMh45lB9go?SIl)gNECxm#`IzXXMNn$8heNkI$|Pmd?nP&C zqQ$3q(FSK$aim}5?dR-~72|p9y?@hGLr5aq=(I0~Ouf3-8?phxWL10P1+)@815);E zzw3tkjJS#?+6Pxh*`F-jv3&Es%BZ@Gvz$rF4;a#_o1$xO@Ix+=m+dQ0qHEUTWqY40}6oO_b#Wg|~r#DG`qho+;S?J7q4XUJAyeFajOQcA1pR&E8%uN_hU72O zzHDJ$>6mNN%10d8v+3Sk7gFTTySB;1CJA#C>h@8+B5YD#TC|4LoVwBL-43*5F8mAG zf;VF4=e?j;N0B*3V@By&eYxQB*X1mIfA+}R&0oJhdPQYw-2C4yg*MT4_`YFZjtun% z_Yd3SPfcrRY} zBSRA<{6O~Rh^c!cs^9s9s6U1L8f+1=9x-+Qh~sqls>2{9AsBn~#JHockHD1r7!M6g z7Rd9TUO^hJO;gN_g4jrEr&>3K7Sak8A*DWIiWL9HpNgA73#QU(?EaeGmUJf5g1F#+ z)^Cvqh+WDn{hbhFSe@LY_zL?{o;(S3mZ4Xn=*_y2u|?=JhcS?Uh@6U} zxM)kI)W%Sos4CT|3b@cj8ncgHpsr9&!~#PD60u?I>vLV29jT)**RAu`aS#U{+Q4*) zHApw3yvL}cP>$lf_URc78#2ncO_B-Xqcx6#7jRW;9Nhs&oizRO5woh%gCD!mSl~eg#XP2_s7uf)icG%R!e{yzYVnqM){PV zvk~`u2|TyLtIk2ebCAu!p9rg4n(gbNMK^zD%pasyvrgCBzh}UF-Zq6j%Gp+1RoW3m z-1PDv8|ZDI-tmjI-Oa86jNb0lDgk&zTbj+g@Nr-TNJ512(x?U|9!v}_!;z6RWo1$+ zHCM7lOdB}1?=ma*<jO6gRwBv)XCQ=2*pvOWP76&ne~Iqc0oe@=8y`;p|Qp{&>rLhn3q;!6xt@ z&)nr>a*AMkvit_O1{J}zwNaaGM#D;^(*I>wA6*MQ-#>h6S;zj}e9AhK+(2Ig*BCOx zpyVLxMcCl%I!uFkkFq5f7zh2<6OTyH=m9|g@ruF!>cIoipi17+~(W! zT1hpnMv%NU&MO6}(|HkMD#vcZKsDqlkm$()=KBYB(yYQm$T{z;Rry`ytBGsK3Z_4T zqs*tNp|hR>>LRO=pz~pInrDxZM?09pLhV_c=2^yqoQ&e1bKI+LO8Y?2cK^SZ`Bk3* zM~Jfia56>M3p$&C7XYsTthLz*->67SJ2H9P_GKHt>FcbU*-y=r`7*^+tYTq@DK zr0ekm3oGwNd}_y*fCiA9IhOb+0^*stBXIyw-9otUj4zX2!}FLw^*XN5K9BeAn!@Dy)lrb zM7Ak!vnp^Nk@%g`P)*ZZo>QE}mlezh47FpgYaPwH& zkiYfOyw;p4a1M}*fsl{JT?!z^j5yqiq`~Ptkb-+Q?mOA{vmOrSgGQ+_SyBFxB19^S zyh4r@IBoXXTz948P!T3`mQaxd^q(BBRv!Te<{9VV2B!=(eLB8`H3iycc9IjF{qX!1^wlE+&pNs-BE#Yx64!DAcv{0ni=gly zAzRpVm-F~`L%SPR!pohqf{}P7baqqk$h|PUisMZs_FC5^n#Uif#Qx3E6`qT6Qp{ho zhH0Bl>OB2!`E(tGuZq>Ct7U8kT$y97gfchWp7O6}(?en!$G~4eDwyk1`r6MXocWo2 zyim0`zi_lc<1P-y+Y@4%tY4Q(yEFv#^QWV8;G-UscIFD*x=aG0E^O$GH4aUOWun6`mGHv zmNqr4yuf`>a_m9Qf%t_b@6V>8((|Y`iAdEtp18AgMlS3U$QI)Qb3T1%3y2`@mrA}R zxW}P6D_d^z<`g)cIypyIqs6_l<=#(Jsy?afE&E1P+xA0lC;~TBi`aM_1vHov#Z#%B zlovB~SHlIAV5!wv$0UKrRhsa!XA|gRG31bH3qJF1^d8;;n(yndzz`(rBxbJbX1BX{ zAt^ONLhKoc@@4n5<1s^9Ttfgm+ZD{(+QEoT} z*E>&Mbo%YwCYv@n&ZvFkh0FHv+2hmOx@0*VcAG;6=6#o%gIiPWZmlq*nr8hk?OLmN z;EUhR5h8mv+AA>=i{6_)GeD7nj# zk7w-(2z0J}b8EQe6{c&?F`G+KVN7L%Bw7D<5Y@+Zf12RC)-t3Z9T47L2K@^bh(pKt zN~$&ZMEOAE4CmaAlJ^>NGr`bAv9G{)t^{QgIZ0SbfdQ$JhU2Qc3JE>WW*;W^)D_m8 z%}xwaW35G6e^5jyb^~M{6HX4<_6I$rU(g>J0GJkzxUc7s>sm*bMy<0W%jH}3AQc7S zCFPI5UAfh{?e3-RQ?E#G!txy9MU=KPfB;Fn@YH?;b{3T8a$@&zscqLw>!5q~Ob5la z`)s%YWEP<|WA5|T*;mop#VSuT_x)hw_ru8SAnAs7F|407WuV`qp4sb%!;Zrj`m`#b zNce+=YN)!<5B&yi{OO^zs9M*lq~JI6qwCi_8WZ?%+UR@DE>o|G%zfM4IB{=0M$Dcq z2I?!F<<|y#UNlw2N39JBUi1<{w=aAO0`sGP#8{U*QuG*%km;lB$GYhTJ|>T=G?1I` z07TaK`}l*-jEkk2n5&OAcq&V%NJx!AY96)Aby+7aCVNUkPGl@90?^w0Z4DA_jw5?h za;xzheJ{W|1+o|Z#$HP?i$^yqD^Ifdg}4ITvbGl50lc$lvzDCoKjwgo1&`Fbz9no) zIYM#DtIxf3;-M$zY^Z+h`004}brXUw75Rt%9%f2LQ(|N2wLTTfMh2y(3H<2Mr>F%- zPg^OfcJrOrHS9|B@^45;Np$h|!yE3azUR)Ii~Pxa@q(kte42C0D#qBx()mL?wBd;t zISJ2yy+C|-7^F1D7n*TYdiwWf|E84k(o*pH{+Fwi^Y@@&HluMGPq}k-Uzu(5Sf{H` z-ho{&*F?*@i+~6VFD0zckf%toa;}am##0Y`JueZX2qHQv6;}KLUX_7 zJaboOa?#R@3}QbR^+KAb=g|VkBH?V5V~AWU9l|?x;FG^7=dF>q6Z;^eE_97+qffI` z$*y}l(Lc6sL(=_nF)^>YSP^mlqiG&|81a?p|ANt<{(i4v&4#Dc4U6rWbop>+YKgOm zIK#P-sbdxUx#MIh)=?lG?Dri?PTTej^T5r)waVu_|Ee^)^>F)eyJZxqI6)^*v7fbE+-u=JK-_hI|CIzPL+gFgwAP3w&YYu|q%7%N=x@$95O zLRfOa4D-APzVX#Fk^o0|I9kplGS#G7q*>qSWfAoR@HCc_&sy#g5PH-0Moj&Yx@(&I zs3)U=BPJjIE4&R&(?H_#ZA*y2STb7Nk{RMQPaE$Hys8N#n=&_jggKn_F{D@XO82XL zPf|ltu|WET#?|CEVN&w|>_3Ckge37UCoeyka>IdBeG12+(#JGpspg78f;=Amox&4u z>fD3LSLl-{u40-76&h2p_$BHme4fLcM5wYWyc~OJqN$}sEks%d)G2eaycPMp<@It4gKBA`nO^f z3ui{`hb4Soee|2vnj#^#nmRS5Ch-a|iNwdStBRbExVS*8o__Pig?*WUu#5Z=Ky4a4 z(ge^m2)RSfZM-kcl@Vds?Ixy*uB(9y$B>QGRAT4Ro_nH0wpSDi6OEF5{HyFh7hzyD z!f)cUK|@ekCRP}+O*o(#b#w_DK1* z;go3`{Sk%L62#!KsB7Erq)T=U)ge=yq8;w~JOK6(CS4O9*EgK#VyYBgLZS?u+S&DMKdba*Rvx<(;*Jfj6DMxrh!L7$K?M{s@ILK8 z`cDd=%2>>xkIoD&&qE_yyL^A^SF>qC;sUq(dq?Z%)idTQr4yR(!iEDVZrtpMZ$6nv zuV+K|H@*g=rfbuc8K;0n2LI8-jj5;;YHpdZc@ETA-((xftTmM0Qj7?aqwUj4Kfu6@ zHldkWLV_Vyc@DmpDhlj+dKo{(2rSw1tmd8#cfgJX;h%pa=nXNy^*^zW6^o?iuIhe#J30Q!Q3pUkc_|HrbrKP1KO z!6Ts+K&@~X&&Sx&=iPZ-aD8%7tTARuUb$nYxGWl&B5 zgXF)R-A>k^j%)=Kk!RvVTG_)CJgkk&_Tia{mAY9PI8_bfr@EM6SurAkR=DUj-LHg{ ze2Js8t0|YvnOZZKAATbKez8f-d5^@1K=wl{sq%l0Ll;olcJo!*YDR$YSf_ioh6=T*`ka_Tj>MmQ7 z>VqRBHVzI)u7-v^6z0SA83YH;!wO07F;Eqxz)Za$v4~?dg(Vnb2~q%**ucZa$_Rn0 z$0?3}EqZ_*GUT?}5liDw!8Si;FNrnuIv`#!(TNFb)tj)T54l%x93QUuYEDnz+{*EA zO&3`6(j8oC+|WI)T%S5*;E9Ba6P<$-vsg`WoYeflZut?hE*w^HulX^=OM-qiRTlYV z1-T?`c@}WzE3TwLi?%Jexyp)T@KI0_V7#>G02 zaK>f-7A2ifYbN?>8JqF@zu#E8#kDafiTmux8GY~6Ys{a_hn0VI{wtP@X}b4b?wYGV@9<} zcP5uEaIAt861SQ8U8e-QKh)ajw8?W-Cu4AHNaKud(<6}PSl>cK5hH@4%p~EMHO*U%4=hY zIY(jqYySfln-D7|1l)P)@KO1r!erEK3kR5O$NhDFJds)AP(T?AOl+@pkuHni$rij~ z^Fiywo>wwi|F%7tv~}#GWWy~h3$jTNAmX;|Q&!#QLk2Ej&>V%o&(y}3ad*_Fs{uO6 z(fDd5jTuA|)-;S8M2kyf8sEK5#if&eaEv>5G6ELOs2zjbq z{4GDDBTOZSB_W|!yqei=yUTeyT#)c@A{`C!Yjwn=RN{$Q>6nOynPCUv&n`0`1rL89 zK&80{S1j6$x@VQAm^5ED;>OD!$3GcyLL;g4ad`Xm-Vyu;`FZggUbTpLos1p6}=8+!NB-3J^lc8F94GWp9|qew4*191Y3@nx+IrR#~X$S z-oV2&xV14H4jhWGAv(ou*dEn`2mmuW33N5uW1xcfWUssEuRsJTsW<@J!i#9i7IWt9 z$oIhhUo8uS=hi@?LdXqfWrJJe_Z3$*l4}%S)Og7h&qfP=`b@G2qt%uF&X=A|iIy%-C;712}mCC#h%A%9t56B%)agVMA8x?>vxt_6`D1{3y&4 zBTehgt%;+u4}!UZ&2EDWmw>*z02}Xts-l?;bcRTz7k}7jz_z<}2|~jd{Wu*rv5NH> zEna5fWXLQ62TpsAEHcR;?NLoKoPt={bl2qw2Gg0T(1E=m^((ovipr_H8`t~(o?ZVF zWkk@}r(tL*IqBlpG2e5dlWtaYIdq?$H6&(wLx6@eQrXP;hHa*gVgdji!8d-lbb||> zkzio%p@|GeEf%N-PLWD;{sTAd;MA|8YTw|Z0FAb`HLnA1f>sZx>D^4@x=cp2Rkr|3 zVIxQ0PfQL=sNzb-!dfP1iAq^Jm z#$v#cF?;{^ zJZL-fAY4w0|Fr97I*RQkHkyd|28Ok9!e)quMe+c2A*>K6bdBc8H?*B zn2!BG=W=8|r&YM^cZu8&l)=6r`Q3g~x(|fHF@x(p??&12DUgw%)1eb~gdF72|CNC3 zHWHaCyu7$+NJ;J)30&^XFu70}91-lGEpc-lSbKp7-?7e_YgXDR!zjs2tvJT3nKD{o ziK6+-n{hmZIBfFWJl*hs$IKt6nmi}<)0{HkO#TF1$-tG;XG%$=qE$Eam3Z$fc{l1( za3YvUdFY_^j8)n#$7a0X#yb{28u!cfDNhJq2P-stJLh2c47)1w_n-n(`BKFds>qhy z!);+K2fa*n6nahn72^McoU6;jnVp7K6<$3*Z6=W zI=QJFqicZCAC6#_z7FK+YLaQw82lu=W`gl`L=%F&(=hGoqeF0duew%}z;e`nf&qgY zT=XxNHZP){N$<>q4XcvCl#etCxjtfdWPe9fo57{@>@v5T&$ve7M1W-Y!tr7`t#ma? zHSS$VTu`*$$lw0E3vvacsTk>wNA+4u?M=?IwAce15U56cx7D){rpOPQL`vlZ>N;Wg z;hyAV`gsYrP#=i1&&xidSpyz?=mO5iQx~Hn)|&r>csQBDDv=SPD1ZR(4xRk64JP@)<`r{IZ-w?e~H7@aXPfn_1<%U@h>s6S80KaemQUdaR%b=*gtWdD}T$qLN*0dlZo z7}IyzBxgTNa*t_O*nqqu2$-|0Nt*_~mDa(a?fo|OoP$taVuJb=DV}2?^U#sM zWFUr$n|6HiWd+<-JOqeo}?6bet1e1fG* z>k_SGdeBvPW%UWg(6g{fWLzY$4@{)smM8_Op&g2s1IiQhB6_eYyevR*8C;ZMMNqoY zi$4e7KOmmyoX(ET<;C|_Gx^+*NP*v>wG{7G&eWou<2)b26nX$B(Sg$&P$uSg#(`~% zC|I@en^iTucwdf8S)rLE|FwnUscLHofTz)sSax^;W5TLg`<%lY0*+fifNIhEw*Z0M zvRP#Mm)4Day>@ZUVTmi3GgnQ$J{=fFFLR&e##USjLP!HYLgbfT&OBWY4mKo>%exgQ zdvF$8p>%n&;FC{H8PB2Z=lnO}Bt@v#^Rll!)V73sdZ;c|b~?8AzbP+W!NN@Wbo^DD z2LqA$pJo8#5wj+l40|CaNp&vg)Es2)h;JG)_0H`)-F?|LPh#;rwy$*o_n~Ub%vFMM zw#{L6STT>r&DVvcsvvoVB>6a)VZT#zQ}kx^4v}XjL&t$fw@W%cfzxgKigDAUyQVY5 zx0FiPsFfTErd9N@U_5-Xz&D84F5Nm8jhn~cn~s26eKazTpLz`gUQFHCm+ zZfPAWM}udtqT#bxV@T?v(egvpy!#1hAX7 zPSkEr&OqbK*j8txm3L3EGe(tkB!hDw@I=}c+&4_~3xfj;y2o3efCr1bPO%pv-s8Fa zxNwb)2&lN-Fu5#~Vv=S7nmsf>CbzV7#B27g52xIurdDEcxWRam8O+YgLaKRrQ8HIu zt|Uu#s92l`21rPSCXeA8IVme4xd#z><C-BU4knfVhITi#T* z65XbUUYMiI+^^qXmOP%Q4*^~7W_KXC>$|Pura;!pS5|Z-&mz5zci+Ym<g$!Z56qEw6-GB!6UP(IvF=2~z#+M45~W-#O;zlbrPD`hnM}zF&WcoLQ3> z()59G(TjGMvu78PhSxrvCrWg$&!^-A9}BDEPT<&q@b_F71lfy9{2sOMO{s~NY1s$qg z0Z$c-qHL@C^yt(nk3p#BGO|T^v9V1+B?dT^-{yv{P1wAed)aQe8DkOw|1xCV{xdqz z3DvrD3o2QojuZ*ax1mmqFT*H1CWi%H-rc;SUSMTWZ3|{!)iZeo58<*lzs^30qQoG7 z%a{m4UWJz|yB~OUr=-)M#wt0J`S=p)jGI5JME4bY`jcAa#zZ(TcbmSuoMz$*_RN?J z$N0&*k^NHF_A84oLn^MDir@7q3?*0T9ei1=RL2)T& zz#ClbJ109u&}<{bQ%2M>CfG6Des8bOv2c9{5x1;;Ou^?V%sI)-V!>JB_=Lzvu|Uj* z0NsXreNHbM!g~BYIDdw}f-Nhvh}}=3Q3B5LBI%FOfwdL6n7J-;Zznog2p#7=$yL1}CASJqg>v4iiw6>D$8==8$Cs#K z9=~LVQn^_Mh|$TO<@12J`5P<4>VP+l*SyJnmV{ij8dCKyPdd7To+(i;!YjEUfS;l&hF&Z)drc=*&?enAo1bco3ajjhwq{LLhP>=xcq6)EyaT(QiN{69Im6 zXy1PQa$Us2Yl`)j(0XF>7QcmkTTQXz>6o$_Ht0(6fgCyF@UZ|Z-YxfCdtlJWEN7ivfxXX23|SardMA95v&CtzB^OKM;sQ$tlOMhsE6_V&Rw z=UJT~^gJu$(H;45ZtjkN-e>cT%koueDovX`p(A2RZjxMrSnJwn?o#tD2Q6aynr)yV zAkUiqkjK~}<+^AtF%cDOg_r0t=sEr{^^vR$73-{;la49C7TL9M?f>?=wuTJLJ3qAg zE#;?ksghi+J`oSn?TXXT6d1Gxq0HP;A`8fFrhqxo$!9~U=_CdHahzUHy(c*>e_K~W zv_>BJOD+?A5>w_>Yv|`iXXEU^eNs~;urHJ%UOr+SeAF0u8DSE zK3e(}1lsDdzG3tL1U8eDuPL0+0vwl;_L3~(xeQ6ssE4_dm3@$hqV$24k1sFP zAVq*$W^DU{qP#st^xUkr?cl_U&iq%U4{!K*c$um}##u`X&DMmI8PeR{Z0hW4ADz6y3pVpHVkJTmc}^>+O_BvRau`&T z{ivI|uy## zx^f&r3*VH-SMF3Ik@Ua$Y|b~D&nQTWM6m;1w7K!btSM&q#ujLwH#YJN@jh0Y8poiZ z5=zO>2Wk>*Np=coFFuM;^SAi-ch+rVtHc`-UIoiZPu@4HHvYt1AfGxy0m5BrIHK9j zAg-`sc!cZVi((JLs0LNBBHWNW05FO*U}OQycn?o1BMVSKJjApZ@am8Xn zbN4&pSgPy(DPHKs)suKAiSXwKu^`66+36i2zCJGDd)%ssXQ=)zJWb3T;V-@)1$zQW zP{qsg{C*hca7Xd6xy~%YR;2n7rFvnZLL0dlgg+DVP9DGR>5VAw7E-?-GrxzpB#hbS zX;nhu{xaN{IzCsKMKb{rTLw^jaiGbAi`2u(MdNa+nRuj!D0ewJLtf(N;Ki@&rlO2C z8-@Zhw7;u{`T-0jNZ8IFJHNX&+Z*9NS-Y>1j=ng>7Zb7uPaC4vK zNM6+`+wZ!AKY4GW69yYN(-~(EpN)~XN1jo*4C?}=$k7Fe&Mh^l-Rfn&r47CPWV9Sq zAYMASj3;fx0n>d)Ie@G`vOF*G{JYnvsY-c9z@jAaXj+91LPbl7a zLji5QgNfkvH{9^VBggRh{0Zw0kPo%{?hfRYjs`w!-U;7Z%s3bQf-PfV#yCO|x!78^1W`?rrs4l_B927pbdtB zlEF28!6!ZUqP32>&5TZhsWOkI#qy&w=Q`$$SvKPwZ8h|(vXrZf3|_|{@a?SNf>Cu@ zAC{w8483~) zCwrSw(qgVWIAGhF)teKSEp1L3YYv_0%{RW~A@!_QB-G7d`ZiC$l*LIYomn7@%ciR30WKz(dLVF+=yC=+cJog|U0$Uj!^GIe8 zxTMC3TrO%dt;V+RlX z4FBgDAEDSI%DfRnBi)ldhOQ1u5yFg|lx%SQJ1X9&lTMmxkqq2@=kra@pfE)?F}T-FwLuuBnj@DWH~4T%RdUNlE9BRkHKgptb|Vq zJ1QTe#45a;ZGeQ`lvhW(mbz6AvHD{bh>aN9``3M+*Jv}i&DtWq+eLVPx9QRKZ%3CB z*jXUT78NTU)356JcokO=ao)kLHUH&<)~bpPA|^--%)&iSKuIWW0*Cv6L6mKYq16`z z@19*o%fsV#Pd{bM*rtC9g{W@+k5IvBHtbpn|3#n4Ukrc%q-e;yq2 z1k)6Q>wh%E?bhR}@N+D*$TRerWff}uBlE{UK%yIZ+@en%divP%xXSCaxv%*56{m8w zKtAm=Mb+ zqJZ$oORHmYXyyv=F0+V8KzJ2o^SQ)T&@D~I+#?j!#r?w?rdDPB^h&mIkK8tctfTT9 z!1zyrENkni{QaY0jyMMY@z?f4%d2j0$SyTJW#0IeQcmHhh~jmnk}`rEu1O z0f2#aEpGnC%9QbRoJTjvEbh!A;%gq-E_3p0-XXA7a}WycM5bZKVWDNHPBMtmoFJ_r znW18#qb4JT9Bh2rs@nwOkts1 zZaLc4*qTUGILz>4rtR+0EqMhcU9bnwEK(Ow04j}E0%R^0I2LoE3(h9?zlYPyx1;NC z0=%(t8c*rlvTAPgGxa-EJ8l02noz!DrNhp2eMv#^4-)_@DJPI6{*EEck5h=k(`4U- zB@>!N``tNpP3P|2I831*Md9g{=#G=^Bwn;lr{DHxeG5;sF7Z(T#Pihg2N6#pqbcEo zrlk4+Au>5lGF9Gs&_eIuy`k~_)8ycXxm0Ax0@YSYOl||_-jp{8ro+_=0~pC|Pr!(w zJt;tiD$^H?foC4HE#knsO@TEnV3X?c=T&!g-h*vLx8lIqsn@RBf4O0K9|-tWVkRxA zdja(E-JqcY3mbrU;mc!Eu1qH!OWl+;{2?IwT`Jem&M{%coaE8tKTRs%<*2%z(YM)8 z&1vpLdXbV)GM>PvS1kN`RbAFgn3T|gvP@VYZIcxE^h=4dQvCGZ8)&T?6C&;}YKkVt!4+nc&TY&~|eu zBj}ibuMQ4xDvc6!a?@GV3iDtT4F$0^O=?!+|AjPgt|szL^xuy~6w6%=NJbc*hsLXw z1feSXyTXb1!+B>#i@#b`dBFDS%d5bw8vabpgxW;|+9ItpYVw1Hjvhl1-JLtM)u1a2 z@9bvGY8Q2qemCm6G=abXmmg#@j2e|;P3C>t;l)#bTzHziR>~Qqe&*a9FCMA?-XjBM^1a=74y=VmAm5uEs{hpvaT8ttNKdM0LLqz zk9BOHYW$`6wlmA?4qo>&Y$jn zFz>_wnLb_SNR($AE8pDe={>JIc5sy2IBpA1bEpW0G;!Xh*cscl|5NhQP<`^>)5kYK z3;$hCYfZBa$J#9OwW)T0&RvrbBJ{TQ?xR`}cId;e?Q+TscjB&@nPn3ShK|7rp{d{8VN zB95;6!KG&<4-0jjGy2rL8oF5^xd6SG02-kcu|Nhl=vt~;f~2W(Ac+#n?uqH$LwFr`}ND;T0Vc2ZwF5x2XFk}LlNFJ}vuwp~f(!}=W= z?DG2KWn~ZYi$?vV>G_8zp#Vln)*o{zlCNB4@> z-#A?&Ei(xo9CDBY_aa96`XPXk%-1GekM{{?K(H5^*RW)qNxz2Sf7~4NQE15!7XrYk zE7}}RVSzNzecUxB_Zg&!?7OeYSTC?xj#B5rWBjY*Jr)0Gd|H*r{umw#pVQ3lo@IN8 zJAly9YdgglU>2^~BX(r3!$n17t{)Qj%U#D8z+}L=?Y?1jeR*f+V;odq&-egm7o4FS z*$29S;P%&b+nkHX!OCaHW5GV)bHLTD^Fs-9Q7QgwBzRFc55RSd`z1Coe#&*=`3-)d zMzahMl`Fv!GiXeOppEtG!DpPn0_Wa^=40y*A2|Xl)Dw3u=5;W;EZG4f&ypR9;OB5b zX7lH^O)Ae_cVGhs39I|W&4(2@n2{rvy+0dOcyJxw5$BZg-FtJ=0PHS$^mT3V#Wj28 zK)z%BVP{|P2!DHv$*M4&PkiUk@LOPO6Jz7=|E#as1%JT?QeFHqp?R3hnb+X$;LOYW z0Dr+Z{X_n~?jOKS+H*U$JfJOccJz$zC}#qO?JU-q{3aUr|@a)_wI2}8oq8xZcCHyjt)KQQ8q3f54R~HSm5H{*qaY1#^U>z?0F&Y1BOAq#Zv{MtneyQ1^lFV8`$3} zx5Mjl3yphfD10L-q=|m7H$JWSWR9poq!Xs5rlPuv@|Q$-I0Y81*tV1KO%;~}eL^6K zgut9kJ~8j<@SivUQ#pR0Mu@p6CBuZYeEwjRB>ZbJiV7DG9*?-Tbvd{YL!FY3^dvzp zoWKeIrd*RTE$gwH>GzXWavNs3YKj?<#x?L?gR6MhVvXK3d#25QOgSG0wqfVyvS>4T zWCAKe=JJu%g-o4X*M%S55bnjL*nge@_Z5F@UWQ$e>iJ}G8hh_TQ3iNj2uflD1UXfJ zIeiNxG~Y{VsWNt{`5k@z5Xe%3ufO(MxPFWPlK)Y%Tr^B3hCJ!|+3jdUqm?GJII>OE z5Wq{4gm8+o+ocPQq#(lh{4ue&xUZ5$Tm&qGxRRKPJQ3Vy38{EGzOnEW`dpOuh&Ypgw6ss{KyF{Zb3j z(@P9DEkyYC+{~v_v^(BrxM#h&l?!$mqm4@cR>a?DUkd?s{K4-FLonWJ8ikzvf&LxSd6kZjSz^P<>o_{WP79=6&B){lt&XeTEWMqYC8KQqHdDi>^uv zh_UWzAXFe3{)0JfJ?>q|=aj@($%JKG5WtZj77m0MSETol7J&UBnetUcD6v$cPD;!` zl?}3;$TtTD>pa1diIv_iUgSXLnZNd|X2^&(I)t{69nXrGbdee`W!|@%@vht76xIw* zZtFo;uI{5>#9|sLh>Xa|>TH$4oacf4@m~pZx#a0&0g;904U|=oCxLu1kN0?MUt<9d zZ`Pbt_8%irD1~FE@BP@yu_9u0-2UFxaJ%Bz2o#E|(IqMf*N#6xnn@|Eb@5(KOiuos zYY}2z#z$<3PnfIUNx`(e({}m-mehu2a0U zRCLDPQFkpSTbp`bcq{;VL(2pttNU56SMql3y1Y3!nRpe$sz{0{Mn6EyKrHz7__;R+ z51zjy#9y$it|TRSWPq&72- z2Vx=b!DO&-Q040=;<25K>l)7{rXnXU8`m3a!G9S}2AE95*YFba*1Wy$XSdA&m62r- zx!sap)9>pzxlL>EJ0Qqf-N$%2(V}m2zy#MYJP|TurQM^T4^EjI0t^qKh>F6mcPmrz z`FK+K)=y~YE2A9NVwSRw2g+sXqpzcYXq!MLlsiGD0B-*6El?ILUh?sQ&mX(+o!cds z4C&Hlk(0v$f=B#sZy_0YN8t6*IF;##RC5)!uMS%V;R3T^mED%=BQW_RKy|`3#0?d< zn(Q_e9)I4Ax|d7p-F4d%Q^_sonLK~V!>vlNQWcJ}Kvq~$C#CV_c)Ow}Z`xx1$GwB< ziN7w|?A(t$oBTK+Dc(;3cDmqqz71@?1+;USK9AG(M6ul=P0n`ZcI7p-dAqWaY5GVWSA(Z zi{uvHAk!qa{GB$*!t~@2TbFbpR79!y4ek3R7fyESXeMG6mJFiS^KqaZFF{E@{?pTa zC22)bZZWhlwnJuZconhej}H*!O(xY5NG_ph9{WQ1Nq1FX+$^Jnl@raH!4&4B!5k%S zV5wL_GMi*wDXNlex;LX~gj9OecVsJ>;wP zoAPeIQWjHGR+6Qe%$v(=Q^Nv`#pECqWtPesb~qvEYv>w)g5|^^6|jKsW>Edbhw&-ff-^>%Ca0)>!_PWvDWhl=goUWV8mF`sNxPn z0fkX=Q)Egz%_eysa~$3a|At*b)jPN_Q@X2A)~QDmDfKLb);$x}VXK-JT^ELeqp`8! zt?iG9GHC-@j7TCNh8{Fr{!twE`XOk#Z@uPHWS~!?;)os*1Z~A2WfhS^>jRMd!|LdV zZBg1Zo~9PpQ0GAtX?_F4Ap3MtCvsUWC~*#jS_~xPe9A?d;M%}SBAu7@>Y)p-H>5wf<~fD^;xTXd7e0k0^c1?6nFD19Isd{(>Q&0J7hVQ8XxIll)R6NkB^U_B@KGmm{G!=yn<_xs%}FJchZ3$ad~ys zIXq#RicaPlYDEnQ0ubnpU|*EBZlWBs+mSxPJ&ZLPS48!z@ZXNf!2u`vmUt*9PtIbU zSxO8N4fS!3(BwSS-h;(5&5*c`-83D+nShpJ#{%MSRL&n-z#*7L!-@=cCZ67hPLRZ} z?{Ili>nN%Oss57kU|Eqe5wjQQ63Lz1FA2TLP>7{7UR-u9B-`?lWQZl(6LpU`WXPyo z(tB+3=< zbLH*tHDNz@1T_AsHhUtN1(XAg8i<;OPsFdD zbSnHuulW^s8oG4yYeqWpT@*DHUVbJ+(RK`a=CK9e1O$Z2@OjZMILha2%C5(TsKwz! zhm2>z+~#`8^9*qICMExo!3G$myg7!lyW>i4j+?%*@-QOEIS5O}9qU*?&@p)Pdspj? z)9s{GcNo8N!G`K>T~)10>x}BpT(BJpj~^d6boj8=0Uyd)?a1mSA7dUqc8_Wzut1BtkEmG(2V8FZ$Q^h4H6T{{?H>64EH13u-n7-g7mIp zpr(Uc6U~tT4X8gzAAGbMVqtr+y)VVR%$VK~tUooo9eK4!fh;dU=j2$-odiz(KTkN$ zF@NsyKaHBzj(1%@fe((iLk!uB;^lsEHKz&#HkJ*bIR8hT_>1K)54QTlrEKq+txmH$ zLliRU#hzQk8z^ZhX=Mf<15}PNIeO{|`+yNekA55~e{R4oncsyz7$_n6MXevKFxrKV z=V`%cSpX-0J@tUD4KAgjz;n(M(KF#fu^IWgAxh>tW=ASci~>&9bRMBBeKO%V_Xb+T zK788@)EcZiGK+B8*XNSE8Y%1f1Ng|6hZ-XwRO!NUiY-bLtv30yO0MbpSsHn@T5SJ zUTVjubX=iSS`TL=D6oMLTWB;yR-M!NF!_gk%8MsO63!t5OO-w>axmkS{~VE}O(m$~ z_V1u5*JeMv3{O>^HyL=;^d3RJ0X`=&X1^k)178yJz z2x|`#^A5BUamN;8TE&d)!27>@Zrw;&Pt9&eqryV~^=4QLhcAaxBxMB_shr$9gMOdV z4!9!TLC19D8`AQPfPoiCG@7s}`L}f2;6ja$xqieafWUwUMSOzGx50&L5A%1F+@Lmw zDefV&h%1$N6k6bv71)5Ul#&7 zNU63fFeFZNQtBS@0M1r=wpdaRm@@w!4*@6J*m9C@9Lhl(0f*7o)ny+TStg1B+x_`l z!;iqT_nFkw>-Ze-znGdgGs>s&*!Y(>*6hMAg_>C(OJB@M2)q%({8b2oj2{ER11VF* z6RvBEhY&jygDNVL@cUm{jUZ)JLX39M3#0sJDNG99bb3)@umxaO;%clr^I?82*!sI) z#n~4ovKjW#@H?+T9Dik&t{lLn ztj2VKL;V9?2qPP`W#(x(jc2ccK`aL?n30*c^5d#5VUsHalLnhyqF9exyGN0Z9HJ(- zoPM5juBjivMQW!~j5UJm8!6|(KS=zPYk`@OO zB^i{N1lJMjOkB3-g%}%O9L|uXgmnR!mOb44xIq?AgqKXpD!27q^@!@?<9}jjXQK&c5tf4Yk08+1oSPliS>Q zTpAq6d{uH`n-t3t3$_Mu_&~ChclWYo%VPQsrNBE8;wl#jGZ(#VnMK$I&5-S za4lvQftG9Ag5O!VC-m`fr3wpC#Twkw*39iuk%ReE;=<_WCj;nRBkQDof&%2vt;eQ6 z1+Nj@PxO+*^5ELTfXRBz>S4SLdtpFKzBgN^kE=+c3n^oHG=Pz!l%scaK5V^}{UcT$@mO z;P|3_56%S)5~wen4hGR-_(6v8Vez8;8H29O{VIFvQ3-P!V)?M-9)SQ9Q*0VCOtKY$ zbmV(;t79=BqEs63B=Uefj0cR5zw5y(4VX(FYW%#3`>$D_o7Dp{Rlb$Y#NKC4Jv6gg zTQ+o+;oQ=JyAet_CJizDz*IAI{!IOINrW9xn1Nl8TPqe5YxK-4rJl1xHUR)mC^jBE+rKC;=a5PjH*Y;_%1PJb zoXp(JUn*C0GUVTHCf_pqemAg=ONo7G9#`0QTd-OkClK{4vX0Pv+gg<{7Us~QLnW?K zcxgniAQ*ie61yV0XyO|-5!Id2;HnW+JgW(CzH`#Mc( z$gO1at|knRF*k0KPBXm8k(d~dLI*s&6n+baMHfI9%1V(wSMD7FXah+3~+ew?EnWD&!8=L%QT*6%ZHzo zV$k#+A!Rags8KD3p0IrZU3?vj&;jTg5x<7Hh=*=r0t`TX@k@$%=8^2Jkl&gMG~~Lj zEejsmqhMGJ^jf1;jxkc~+{{&j6E}MfuGnh#DTBBl4gqxWR;g{?w?I|n>GegT*4g!T za4`@^-S1P>aP%b+^hVme`4%=^I-YBG473x-jD#0*rKbG`UD#XN4LPr9IwW;cHvLcF z_Iysh1kCf}?Di~NA|K0;P>8j?V-*1BFl>i<>c#_da=bQ9w>=z96pod!5RN}QGsQkyWcJ$fMp~w+u1%A?Nt=YXJ%fuW$jU}etO@P&6Do-SgXUhaRYK6DF zr)`O2F5ii&ZPE?~VLPC^$8l1!lBpGU?#F;MtN=sN6t*G=B(e*G$UxhcV^nc=$hxM? zj$@Uxq34!DRgL>@6!2uDhGsnXL!sjwwFW+f^3$MK45%0)FlYM9dwN{QfpQon7;MS| zBZrSa`C-Qii@M76O(KB6ZiG(}uk+Eenf*&RpVb>LD-u<|@H8*#M{|4d7X^D!FX~!# zU?<9r@B%W~EkAia9R}BKFdv`>uNQC6kGFS9$KE721k4837lW2rQI>l{RMd0JB0d2` zYb29D1fPk=22XUd2#d)<18N^{nneg3M~QqR3k!kdK!E#l zlQu3eQ%bcajHZdI27(Ujm=lcb5l>q~E8MCX_}66b@jz3JW4Q-JffHgW8fd$!bF%+D zyeTmP=q+PN4m>a`(aG}Iz)->;-iS@F7uF5Cr6*w1Id0q}XgtC{dgd-4!C1zeR?H~3{dt3bH!lwFWx z5hd%a{10@I^qbpD4cQ1{^0LP6v?(!_8V77%fJ$%Zn*Y$L}o zDKuITr>pDwKcBD9 zmD9}J-~avp{qM*3=kd7h?r?P-KA-pd^?n_mhitw>gx{kMLfyjQ`tJ1A9L1nXq(6jn z37V?TCoD0CDJF&+p{xHC0T?O3XrN0LlQnU_lFimFZeP3Zu~~9kc{|YZwFG@bfI8ft z{Rfm21nk@#OXjf|m4)Xw34(dleCa#csl!3+(ZV`f{Zo_@@8BBBI0%&#gRe zs(t`YCj%x0Pxm2!E(s>IN)%800T^*>-=BVf3Rn? z!e*A^8GamsIK?aBQw}pmL9jwJ>DiRL-?I2KhlsMszfZr;omfe24u%L$Z>8OfVh_Cn z;xj>}Jt=hmteb4iRZvlcEPgJ4SL!Pn3Rx{8+_^}QvP?@@P|1}-I@l06l0u* zDJ;~)GM2P38Pl@~LzO$=Sx`QzU_s881Co3f3`Pg<@DgS%1P%y6 zLxnfWQ4I&t-0R35dkjb9Kr3%cd$+$+^+6~VT%x5M2=T=8ME?MGs#ydO5E!mK&+-Ku zI&cZ1BA3wq58eRV04T;cnxX90e1`G?=4n@!NO<_ni5dt235=35>x8H4ntavSWEgt@O|~;$u1c-AC}8=mNh-9LqI7N zH>mo5iBqwL@zTyCjBt(|4paT(q67L$$Aah;=@J5ci56-k3mHH{x~&H~8yLk!BJYUe zkzrQ&1%&^wLxQd`y6{bo2qwt+ydjS4wv zm|2Jv4DTVa%%xssJbopom6NFKBkgg595IQ8oKWjv9uBniu>Fl&Qk2-(E8v*P2b>Lz zxa0kr1hI19spoixl2Qzlvstk5Pl$(HP-LO$qR3&0!L}l^6joRFx+aD17`+wmpS$W; z`o;p?LUfPd{4<-EdQVS=nze~cvCtGtrMC z7ib_!IBb%vwG#>m<)3Q3NKz(axe6EyU5Rc?I8*@?Mh1b~BY_rN2g^G_5z7p3JNBy3 z94V8<`JAT|M_{>B{xrBmAnkYp5q%UrZXvA@W z4Iqxfxz7k7<9iOYab{e28FdUBUqY`z zOU7b8A*gK@GwczZHn}|>Kz<;bYTUZ*+xU%?TjHbo)5-g`_>rR^22oMVWPy2oj?47b zh^+U()!aTj%!2G>Qnh0~C(L%mJUBC9;fxq*=Z*a#aawe479qDI7zjcAbfL`40dO{* zjZT$Ra%_X6=CgT~+@tW1fFv1MCvCXZL9~ypc@3$gEaI#d68Ib<3qi)%p{0C4e`YwF3f(#az52| z{x>FzctJ|B@WakQT83z2k$~=*bkbX3!bPIjA8$=LurFI!_a-?Aarqvf;O(c9aVfL! zi*gtU3>0-;;VXnf9spsT=dW2=7SvX^J`YB~fwA7`;@cJ5WAcv2QS=%m<)Hl#2=Bdy zOCG(uX6SUInSg>EAx0Mfn@zxbE?&9vHQbeUf@ljQ6$WXOF`TpgjT@fxYRr-(FIj{} zIvL*_#1m&CLY=@}qlaMCL^YmD#=Xz&O+n%|iw!O_qT=F)4%`DvMF>H@NZPexXjXUoBS;vd&oA4gE%(-Y?+_yO9~9>ducMNb3E zN3-I>%UDP6{^P<)%@;V}klUhdhbPc1^=`@TI?@+hMJ=#eJ&=i_;irYd-d8W&tHbhL ztqcwRtZ9`s715e=4#T^6R^4=jeMABK?+}eq5y=Qb6NyN|2JE|gHc;O?;$!$mK_4uQ zz+uu@EM3H+lBEWr~E5s_$q}Pqea$nXF9z6{({pYV7OO1kh}GufbO@=yxLN zBIjK5)!v)5_OSnsKMwz>LglZOieyE4SMsc#=$*Y9ADPpQ7g`K|4m41y*}Ij_c%0s zWXsJsC*YJ`ySr=GN!ExbEE5ROY+MelxO}GzJKEi4V?z4Srg`N48G1rQt-nzK4|L*&A=Sg3iUvDxFx1PcTI2px zSiie-E3?{kuf(We-i%Ts)?MGB60-LmQo7QmDVtLf^(GrWyi#3Sm@R5j^Uus}l~wZ_ zgjA3Jf%^K$`@pxVvKQB#VUoL{I;&xVr8Ymk_+`t5=K4D{st?WxHMAX`_KF$pqs1;3DjHO z^(VXZ!G-zgVy&`W3e5t&ZQSfn>j$O#-FUsot88L=ztHXjbwmsXw4 z(ANzb6My|HzX4O`o|yk*!YKP632jI322DLWfxxH2`8xV5BC+9u8Li-wei|kpOYcl= zEN!}lHr2(gxNAeFlHCP)6EIy4>-~Z)Iz|XY4M@{Cdix zm-@k=_Fb*-_s>^;4>CN5Znt=thynuhe@>=pic?JKq4?JHj!B|KPzfziSxbt{5W58@ z(a@T%L!s49tkxv8W_d!Ih2Rs}@VEbo?3NNN=c{7E!4|S;Z+3v>>}=%iHaF<@>d!`u z>b)UVzi6}8NO3P7gnSaeSpp8E+C~Ynft=&OA~wD*3;C0RzRo1%Ldb2>G(Z(EMy~WA zC*Z|Dc%xr^C(yma(KU5@0R?tsvH-ZW@^CIB)o<_y3r2r3iwLLk=BIiS?^;w&k>=6& zq|9(xuDX8iyqJCI6`LYs!z%9qZ7rn+s+Q6aL0Gd# z?Srv&G4U$!I@6i6u8t_}6JeIX%j|vf3FLs7`hmjhPLH3spfHw6VX` zBy&1MXg@{zPem^nN&xUGw(9wD^Ehti*hHqpW;G-Pup)mh;oeA_Ju!%i>FP;7h{<0C z^)Cv_$xq^Y%@>ahPSp4wk2NeMAydH2m`Q>lazX`LqpQ#6XcBvuXjMpIAAa|TUBxM&Eu3Yf)$mhtThX`6B8c-W_UQ&%3298mC#1UYGq4}U zewv`ACD<8k7eWy!=eE1&=T9vn)L8PDDBW)}HOB_)uX&dCFTVw&qt`WM8KA&dAB;$S z{YuGui0)z&>)3En=KP-=gEvY8YZIY_U0+nEn!P+j8&BLN;Au(F=!A zc|?LV@k7vDuD5=4#>A|lc-muA#W5S|0NZ^zVLDlZIx>b)0M^-yS-v}EQLD6)l$ zP;a1stI3%{@;$wFdQO;xeviCCJ zMK*mZh-mM^R=0Tqi8EbLmu*jSy1SpVIn^pH#`u>cGdV_!(-@64I9&zYNf)5{TS!|5 zvn(+PAP>@{h|Ot7XTsi7eFAt6D1tmc#4e>GL=Bo12)FB?i3-fMvQ&GQ+RG=;hzh#*nN%y)B|9DYNpDCPlvZR5xEK(A{#* zJDsMVBwrLd6_4>d{*l`X!!R`b3Qy8&0&dvi`f$%LeL-f@!j^I_nvoU)G{tj4V*7!? zD4U^1l{5L2Yd7o2akWoTFaBQ%p(*0=g;}GDc{B`Dp{|o`N}GpAs6$fVJbI!<7ej*b zEQ}U_&w-0L00u&hQ)FU!ub(6b0ckN9)v3|Jy44%&L!i?YyD#03E0Mx!%D=xZgINKV zIsby1R=9ei^-gArm`B~C(a8ZNy8Z0YtxH7vq&$EEY7mx)mcUB>IU6T=H*VC!d|I?S zeX4QIRC{NRw%NfyFk0W8+H9C0rMUvaf)-?Wn4Lb#C5KP!PftnFG?&ge2k+FW7oTb& zBk_FBudN$u-2YRuYlaWd`4vny4M1!C2OuHSRJ;v&KomPbt4T)sw2uOvfWu0LlzifD z4WotMM>UZmh^#JEdsU%?U8Bjxk*q{}j_Y)uf@Q-9HE^|{k|4$QO4@CbuN2NjQZ>6I zqoS0&!lCw09N3i-RWAoD&+wEE3xDY#U4$~t&oj%{EJ#VuOdYfET-m=RIoDI#2kt*~ z&&tvMD({aQ9f&qlptX5FW%9@tt3o7RGYlhGI+#?@6^-yTx$)$iy#)OYNVw&FtW@Cd zM$IS{fx#{Edo&<{SY!L&q=Mrtm2|-2TPX<;Xatt%Xj*Q4oZf+^MGU|{T=3ftx+?OC zK?=3umKhp|t(9<%`6k&0&Mc%Bg!`RA?ulxcVk|Vqn|T?B+S-=(^et_uiAhUZka{_D zUg5pb%wwy0tpHxj-dcD$BhbOQsK7l{b3hIhkzb?DC~Av90%i#&QvALeGrwIyIX{6~ z&9TNAKE;&KhKdk_pB!(zG#$G8dk7<8?@bJQDjGY{^ZEQV^PriBkzs5U88wo>2zrdk zqQ8ey#CyE}!vyQIJxpe!Uo75*1jBy3r9FMIYS_N3A%Dm!LpM}Z_>o3ouHN10_F1RZ zXiC!luHpyR>K>9nVG2PWo&J!>{%!ON-k8mb!+$%#$*QD6qCMyp5VQy zy2lBG`f=5iF?#nP_+mk0?z*$z8bt2O^zQ~esf-_G$bFu_w?D_+BmUNK`_m1h!s|{s z0K^}Yj@H2f^YyR{XwK$wny@}OUjpG;+JAyHRDB>hkOENwA^;2hNv9ISGd=`~&}5JLJEB}A1S6JD zgb0F7d)A*gbow6F3F62)pOgl&r&Al2l9wVL$)OeE8B8(~Ju?LQO|RRvi9B11Na)lB zETrcHaR9VS2dA!@)wy777SPC*q=8Bx$)_p8Oa_jE^k zNV=klYmxSMeH8SI|DvLzTBsppTW0UN35ES`r&UB(eI5{=a%oAa zlqH7BYX_h@v^J4pg2wA}IN2sORoWwIw~0os>5pdAP|BUI4_kt0kgbAy4M2KqTN|H0}4bUF;JKs@f`-@b+g2LRXndUm;*?Q`K+ex zgja|MZhy>kE1cxB&mO|#`KvFEGJh}OGDFx}8SU3les|Ar&v5&Se(sv$i0ilA^Pi+1 zkKR)6+NLoS-LHCUlP&Z&cA3sjz0eVx#eGu0#+kmeZGyZOCfQ$0wWhFYA zMC72;qi}Teht}==Udkf^_J`!-rW^F-C5hGHC+5Fgk-q7C_~7xSe{<5E&syIr;MUFB z#ccRrsI?~-^P&GdY$b5NC#BD438s*UC!788|IuT({)lAV{Y(G2ul_IWsXzY8)n4gm zngm)=U}Yyes}v7V-r_DEaAL*);%=GDz_` zuW6Ant}N@Oum1P7wp9m)j(c;4?jsE7;!m|`nR~!(%KHEK{H|6GSU0%hR(agA^!b~I z>*GZCN8q(_hce=d1&Q@41bkQ%thVD!F9Zmb(UNF_nU_kmj7Y4gZ%^+PP?)A&;+feU z3G0CLxeLm7WhEM3cyRz?u|q)8XeiycUMe zY}KD8?phpl1cW!p8yaXbUMNE8R4@bhQm-WOh>@ltQfxQB&_aiwJCCuwe489o%@-vwVLxb66 z|9M7}n#ZSpRkojt)VjOJmiGE~!hHxunVEEFV z@Ba_|ZjxYLwjj?lu)TCH3CW6xEZQ$r89=F1*`lD=@TRP~Qe-S_?p|c>7|lL;jtqgRms~6ER2KiJ zH%ZGkYU9Df=U{D?pm8+X7vx`R64WB?Q{9+f!4KeIO*-zVt3YcYrC#PInAK?#rQt;V z{>(!58o0uQR(XfYD$(DMU=w)8XuV%0$Ce+ED98%-SfDI&x6$O1A77m6Hu4n8vJV`mSr2|~ZTwZfnvSM;Z0*mF)Ryjz-7TnwrC00)&Q z__M!bEKq&^{fVk7jGce;V8GzOtCr=lS@ycCPK70PlVa%IvXmh0y4mc<`crHI!rJJX zEvUmm5#+Yer0|e4iHtzt8KtX8g>Q>&9zW=&xE~Q|3N|eAA@p{4=H&Ji5Bw=Cs3-!`}+t4xk-#5(>lk=L8JD7de4H5(>OHnEUp0+qs zb~{0Y7W5fsL7e>F67+UjeF}(&7+C&mO zpf-gEA&mY0#6a?GnpA+gHOHy!edia0I2O$XH9&2KYyLCCx5$t7-u62?=5k)3SJihJ zVvlx`qxupWv~#Bb;ZgLSec5|r{)CaSZIX<~cgV+8gwdcAOt3g^J$PsKm`?=m;t;;J6H| zI&3k4+%O(1jGfo1)SAr(AYHkVLTvM;rH%7hdsNgBfEQUS?k|J~RbVlx#4q6iaVl2{ zCy=^VpkM&qDzehGV_#P~i_p#J|oBz~v@s%=veCx8` z*SXqLfW&bA=0{G^*y$?N0w+SE(1kb@!E^*E5oaVq)-&44`I+BwtvfBaqACo1K4Kke4a+YU?{alYGCJA!#v+U4~=>xEWZOJRP zN9hke8{|yM;|TVW)Y!Icr0sF~S^;|SZ~^oqzf35#5*8dFq6YvmgRb!ZUwu2wFseA*FgEOt0MUfM{`z9G7Gr}VVhH&GsxzX z=5=u+^eYg1v?m-CipYVw*^gSpN&+|&9B5LfhY2oJ@SVW_tew0RH?che;Qk|vNO#n> zp!DgirUuiU12J3jVIsi`@6IX&+&;6AWGbeSiVO)FmLe#YP+pqEJ#KJFpEQGHfu&6O2rLo`^73rj z^TcLRQC^X(3bT>|%CIb4KvN6DSUI{x;GA!P!%aX7OpCbpg@S`iFR(#Mj|ooB@(#}u zh0Yb6ZdJ>;NGN8cLP`oEvq_@)$%YjapW2e2dtkYB9+nzC7rikBHHp3?G>{iDwiA zo*i#_SmshZQOHl>5mMuTfp~f9o?@B~3ATn?!w*6hsnl~sBqQM-{1nK!(KU(*u&5Am zP+3Y%RhBBI+1;ITyZfCVvv5>FIS29s>yRAick!6b3DM3cr&qj9X%B4}E3Ly-AdZI zhYI#pYS8V1Q>n(Sr3e?MQqlpT#2eM2D|Bgw1stuZ48OY=k6u8Ov?=>$a53`*$1|RR zd7dTVxD!Bk(JhPTa}6c%mlIV1uGg$=R#2SoR$v6&*XHiN&x2PhiUf0(T*2so+Ho&j?YO03oT*a_$O{=~q{?O>T&%3B`ZyT&Jud`{lKI0;~i*DM| zF`rAn>o?!Vjn-GD-`MTH6bp7+M?ZB{?*7u4xxtRzI?5*;ebl)`TJHwuPko*A0dlSUGe|v(ccG*a$neyaDt7ZqBTuf#oa@Dpj9_WqU zi<5?zU39NM70K=~SQ7Qh{&zA-9`|AI|Lob+>)}Q5Z0hxJx9{21>tT}Kv$6U8*dbs2 zmj)1hE3&GD(zMO9!`D;XUO2EEASuD+3%7p|MDd@x12z%e@(6$ETh#_D9yM zx2>+qGPo&$75%hpRGwO~Ww~Q;FI`^wwVN74fPYld;m!v4h3KF4lWsDoTH;-xelCM= zjn4K~DFH@1`{8m&z{T>4?)(Fj(1!z*gm3*|qQwnKom*n4*VCq|f$pP27w3jtOS#|@ z?w578qVQ8{yNfd=HKNH$YFJKDBAKkn#K!pQZ#wHg+E|fRlB=B?d@nb6WXa37nwIML zqik1nO=Bg}@6^!Z#VNn9D*JSiVFa4zY|Sc@+mAUko4PZcGJtz-Z0bk)z>!lEX&D%{ zK0aqsvjx}2TuwdGEj9}a_2I+lkvCwZqFMjmHn1ehKhjp$TOS8zzUH4CsL2$KU+lwV z&3eO3nA^>`XWbVyJXIG!x9uEyjVB#!kT!GLU$70;v7PnpJg;ja&5r_b{4F z39;=`*rAu$s6K6Wh~hb-%v>W<7BZA1_R?=Vg@W#lutbT3DrRJTuqq76_dGDRcr#kC z;x$LMn~d0^dIfZsQr$X_vj%bYqzsL86j@M#8BxX!gJ^pS6m5Yo7Q7ji&kD*pToYF?hyu|Oy)*0;S^J4t zB?)YTbB)b{`~l}br+F2cHjyjF5=>q|bf~~eT$EPzue6-lngmOof(eh-9AdAOOc6hE znej=-tmt#+trk^ZdQ~=KTh?50fjk$=%#Q;hdcRq?bkhCb&Jk2T1$<1XIr-AEMfZ7j z7$goC2b1O5=3wekn~|zb=rUm%WDX>Ly&$j>*?Hf}iB^X;B3QyXk0?k8IgnC7m^Mb3 zC&TIfMWvvO-%Wy^wcY7SAeSB7tBPKp}{XAY_S zC?-W5%D|DYpq#FL(wyYXSUSoug>@$vrKArCZ;C)+8G~Y>?^x4J#z17_iQhJ#g{?BG zips2Kk<7C>CpF7H3uL)m7`|_ADd2ErTEsTP9PLw6+w4t=dJ%1x7?u`bKxu3 z(U--u&rIDjg$fnYw-Zl0Gz?@~Lggw&eEbyYbF<&4Kyb4y#u%a!KPMJYT%Q2yCLM;x zD2xs=;tsWd8^i+80lJa@1)IgEB=0GLz7cI|PL%2f*@@mU*Nm{j*j_ZaA+l*2B~Xp6 z6pnaS7|B;I52>if@C&vBO&LLCD9@Gd3^%HDdZDOfpGy+p@_N``hl3$ArXJ4r8Ds0X zS9eV>-4)_qlIlDcI_@4`zAK_Nkix#zQyS#xUOxHg8zl-5X;*)l7IEmjN!o34=(3Vm zPtth?K=IKmft{(MeV_MRB`R&u`>C5vTux>Be2N=~XLMS! zXmr^xwG%Q6QSw)vk8(Dlpz{^DjO$pa;j%FJQC58aGA{JOM)d#{nUH9zBVF-Ih2R4B2AX6(BDRDCF` zx){^nP-@{Z5K<S;J9#ohe?^ z_A2K;{;?Ya`rrWI>{k?kp)w+$umYfo{LH9@Oelm9(daKF)J5ogFC@H)=l5w*N<-K$ri+@m1d`S27YGTX zipmIsKc4FFYNq%T;0eLRKnO;RxQV>Z9$}i@GPaOh1wHIHCH%;)N1OY1Rm_$@!<-Q$ z4w(xgS9?i>7s)^Y-@V8Ok_Xa-WwZVy+JY73R0R_EN%7gEvkwd)gvTXgH%e`BR{Z%) zRj0b9jj4-qY|ZHfIb1#-k;Qi-qmD%{ELk02bMw}0eE)P~qCx7Ih!RB{DGF1c(rnWF zr^Am@fz{VpD96^+0jYrgLVHC57;|R2Rn0$8o$tn?+IoTBgP2?RT75$AGAwFu`zpkf|X zfz)-1zAbV#@e;acT`4Mz>wEb(Riy1N{G3Ikf1RkKx6EO<&jqau`WXq1VLJ!K<>Q7X=0M5l1wvB@9znq>=}l}|x$5bL0)9`{-#z@;9a-PR<5wJt}pF9fmVzJBi8_oa{`N=d~! zXs?~aRGiMl4EF5#Dbz5lSM~>Qi@c(zaPzl>AO;D9%eR|HL$n~MU7+ z9TV}M2{U67Dkl%qAZjto`BG3W_*yzy!E-W?KS!R7sWD(rj*7sVB|u6K%fK8QDYSoe zdY>l^4$%>PuC^npiE#JJ^eS28T=lIoE^6+I4{ukdf8c6X-fF8w_D5O84b}GEOW4(f zixR4a#iAREJ$J0jzSvj@jaY2?I+#Z?B%HZ`-RR#;%?mP<;j5&NqVOVAtLLEG@;$r& zJ-V&uua{?2#d9ebjAED#bxJ#mF8X}*5F^s4v-jdNmSXLk_g)iG6UAPt5ChPE{C2bT zpiVj1v|Ix5mAhw_I{*AXAL{OY#!`HsN&IYx_;8Q@*%0yB5b<6ZdSIX2``Lf#5b=rG z=kEU%tNOo}rD$za9=70Ep~lvF&AiI!-m%_WEd$ERvnhj6n_?O|c-6Q>d;hkt*!Jqh zDOY!CuQk(mczU+{V^SIr6)*Ve-nw#Yf8;5{2Kq)5a}_qg5qaqyfeH|<>G3#aM^SA*#@LuhJ31~M#W zww)(hkL*UMTcRrK=2O!Qml%ns_bY-j=d`EJH2$v>{(W3{y#KTRO9G|#JQm24D9!gy z4qTcl+kaQny2@+r!)U;&2=bwxxN$cAce2bM0Ds&Edfxl$nacM4sKc+G37V5ccmJCF z9~3mdAHyE^)qhi5GpvWx7O>eDUWTnfn>H1O9JY~Z`UGvS$dli`zw0R@lYaa+=Z;URMgs~; zmavkr7#o=m5vneYo)~ZMUj_Sai+AhOPYnYCA+R9?38jxq&co@gecs-)fu>F5j_1(H zQ}Lsuwbmc5h^6|DAVBdX-eF-Y3P2&yA(QiRb=Q{0(~R zxxK&FYSUIuYhD)~ANJo(-2$S-F$en0wk&GzRnlI# z?rUGv6CbF%MV_Dy8nlOJ zid$!1X&>pVvvUtd$@i(mDu^`_F;FSj;E_)IJ<&}7Z^AhAq0Ty_3=$uyhG4sC~^Olw69jZeMunHXU$?cmQ;q~H173f(e+L+*> zYmSn&DgJ_V@7j|-zmpKi>C!4vixl$o+m7KXhIsnLY<2>4(6ou{KW6Y`{XbO@(Y*26 zn3~9P?ABH=c*Aw=ZT}MnDI_X2U$zL;&+T$^KzZv{zMi&4+?<@x>`x^(t)SPMworMa z6(PA+t8$0HGKxMYj7NFwr*?GpZvdy7vC|0w5-Rj~jCa{;SI+k6+Q>|i8VH*ML`k@0 z6Z%8DZ*|!IZpR-kkV!n%Uiu3Ff}MZyp~;1yC{bo&ocz@b1?3_=4z-Mq z%EByIyy2cwzgdTI((?jC6emfv%}Jk3ffqM@{P5!3+mr$EfxxC9$Es_nU(R5bRMTd0 z$~D^Nfr{P$rk6u=RGCY6; zptxgF6%IDm`HPl$u+J_iLDGFCM(f~0j3vQaEPm=hkI~Ud{GNFTKpojtQ+1Y5inN&+ zRf+X2JoaRPfGkIX60voj04E5wmB#nD6+*$qH5c>?ZN2P&63QN~+)0_La7HfTxHk_M z+$@Qli#sh!oJ4~lT8bh+XSf`=V=7CuC;45lfUG1Jm6u!Gfx+0U8e%F@1oN5777B@c zix}z1a-)ARS5+pg(jf<_dyX9VhW)3n=77UQSieH%YqJdfJ9gYE2O&l@hW1(P7g3K3?=7QqU*-_-)_ta zl*J_8$)bnOkIY$=wq~CAhhl6I9zn5}>(CQ{kfcx;$#n!ll~N-BVCt7~&*ie(2V$m{ zhV{f=6p_JI?u7(JQWesxm~URZj0CBQ`!>LeX@`giK=*lbvg+tA<6x);49Owx%W~n3 zw@siDiMNmk-`-b(IXGi%RcNdTHSn^rk;gHlUu-ub!YP#~2Oq5<$npFp*@(k(CWt(W z;YDGFQrk&RSedPt2LS%*PhI!to5016TJ)_z%)JPout0V5lbbF)J^)5A5tHc{nV$+N zjkC45k2rJ{%3v(7r!cP(kR9Kz%xBBvps1_#h8_T&^ zCCC__r}5sKP|UnWbg^I264-L``abcN8FQ^F+u&^M{~2bW!)E1RO_*nsqKbpeVhfDC za+-uF*;Jm1y?yHzDQq@3aJ$6fb?obwpJHti!de+iMR7{|!uA5j-is{W^vY@?hj>$E%;HoZvp6Mmt%LwC^K}2)Y=JmPfXlM)TPlyOL3&P2TpBN*OA~0NT zDp*?B(#)!uC|L?zRB!9!Q#XjyIY#t6$jWa&P&a`Wimd$ZVX1H+6lq=l!KE2cy~Q&F ze>?;>+BC#KZsz4>86=wZ(R?fT2t}EM56m!|P2B-7L!psjVPSzBV|HrKbY(V0V;?SJ z9-nU+h9FsW_nI`xEQlC%FTqD}z8D@sCiZN=C5kiQvj+P_1$++B<*ldCH8ZqMkR24x z|2DA5eJTW1ks$hSf3HE>ZN$Bcb+dW?`FW7n2}urUsNOu_wvxK#6QF!clb0OgWND}? zj(|A*PlIM0lSPObRJ~pD*_bnlWCN$~=f=W0I)}P5RIo(`LjSoL z4)9GUiPj8;>ONYmOCDFVkajvor(6r9Bn%HK+FTQ?`IP(XNeVL#itNY0szijJc=K5?`Hr@62$CfEOM3HcM+HI)Tc0?*Lj`Coa^qo>`S7IaF;yI{n&w4T zRN`Ph!@(?@LR1XGm690<0E?UfCY%Jt&lHEN-|fGfyBtqPL@C9hheFbfp^r zi~mJ&M=h(p9LY#Q4FTzi70 ztF5jVIC&zv5I>Snn;F;^@KqG*;pM2m}8f?-uCka&nD4IZcz5o|OrX7(aP zyb>Z9k-3^m>BQg*W_rB@EO@ay=eG8PSl;59ax2Nnz=4m1>eQj~E2oe`I~O-~K7tPd z!w1wd>tQ^*IoT`~H7PctWH%@mH!ROSVj0-wfEeJ&sevWH2Qa%cL3SmokHDDuv1Fp+ zh-nZ{S{)L-H}Scsxdp6h(;(O>xM$PxSh7gjVNVnDHLs#tiYTH?j95XVhU~{dS4eWN%}@a32e2iNPizWNZ6JB&Amu<>k~T&?dq0l}Gq z@u1u@1&=~8tJ%P+F$aQaj=#CM=G6`-l4t|5++ z-ly|?eP2py;v!j7c>vMK0P(!m;Hblsnz!s#C6znJ%E3 z))_e86&ySQ`X*+ZW+AsJ+ye_^a64v$J<@%6Qxx+MQ4|>o-ejE8_dR@P(Xe;6b~${X zAGAT{V_FAW%;)DoPUDxEYm)@PS8ngn&AOqrZrht(1{P()<9vDcB6Q8JEN2N;yvmc( z^0Y2y-6Iq)y1k)L^!4B9dzBU)5-<^eP&XpEq~l4I%Lzt8}-3PM{M?fJsYR z;FM2uV(GkbC!&^Yj%6jI!t=(nY#-{ zfzu951SB0Iwi|KZ&y1Y}YvSeEOE-TVj3XcFD#*jEr!=n%m@!OOxG)!U17^dYt~U9! zn6{^wDG)V&D4`?0Bm18ar_jMWbZB_TrPh9pKqII=C{(P((}-_eRS z^a&+^nGLn99bX(p1gzA)YlV!_`3M5DQ$*>GU$N~5jhcQP{>SmJRvq32i)6N*vWeC+ z@#D^5_=9AWWI;?5U=CxEu*Bw0Yqw227GF3p8kXHfy8CbOiH%ri^V-4nD8olY0@8Df z7hI*M}g+ZE_G@!UeYqJbP!>8s3 z*Xj>r_&oX`u;J71jj)Hn@JctMP1b{c!&OY;gU^$nk%L>G>nU`H^p*#p;JM@n1Q1`z#5V1rI|iZ0qg zq!Z9N4{q#--}S@ybhrNUH#&Qf-V%n&p#7MdQ@s{TWm12BUC`4 z4-kL@X|8)VBmvJ!1iq+cK6(7`z;A2fMpn9G2JKySXDTJOsC(qLi7G8yofd96T!>le z(|@q>_4F38ggL?0Eg5)}^02i0_{5>{bwUMHzmax{LvkpIw>d38(+e~5!A>RX2*G_y zuFRP6Wo>qrI41(*OD4rKJ28d?^x*sG=p-l_4eV&1A(ndXbEptX;4xo8+*<(oBOrk& z(g|UJ+5$&VJLmoDPw4)cQtF&=Gye2V!EM5431j$FybT3V6~eZXob|;VR_`EAmIwsS zk#AJn5G4kf0HKf~e+m_xPWle}9u(*{ls>i2_M(#pCEZ=qkN*H06kVo+fFR@nHA}!0 zkX+gmKnaK*D-CD80dVt%W*OeXxcg6@?Kk5(tHz*dho> zZz>Q06PE=#5x>N1nCwgCuT6{Spf==*&Z!S=5$hG%+DilqLfbC}AZNND`j6a(6-X?Y zKs|`R{-z+lbOc#5c$iq}JTfKt^Kb26*kG6j8toWqAW64VdhtB{Dly+tl68x`CXtXNf*gA@`trOM_CL{%hq{N;_7JcJ zLWMP<$X^n~3n3r@ge(rtDQvbKIWGkd@BuLEgan1l~M%Nes-)p#Y3{m;{ z;Q72`CBy~_|JDXuO;}dV&9}qn=MyYq?s~{4+t*OF_K7>)xz!QF^8lp9cLAS9A?xL0 zUhJhjsPLIEa#&9RVPv;kZz>QN<{qrnCO8$(16pf3}R{X(kg@b{gYqm zMHryRng*Q1-E3 zB)`zVm-|T4^w76eu5TAPC&Gl%&x53!7+`7FCHkyS1Y*)6fyBWS1g$as!e+YtJ=AYj zXhq1xFpXtKeCtqn7N=^OW$3&*1RI8gRz!na_~lG}_ltSX>u2htqf#4A46{Dr5L%IS z(!ucZ!Pu-lzLxyF_7{y`JaDEwbXJWrOvS0-M>rNY#H297>GSh}Hz#V0>z`DlAvuAe zB2voRjqTV+5XzvIu9+#Mn8E#iAE|TwxJ5>MmxGldZGsIQ?TQOu;X76K@vUhZy6C0@ z|C-+}SkuWxxtKS|Bg}fKoxZzEj;5~*KAxY4U9onvoid|TcQJ2~owpVnNqzqdt^Y!n zzQ^D7VR-qF9Malw&{GsX4-SOa%qyuIegy)TYYFk^;maVbkZ(bBBNPDn0JpV7iHnNm z2KUMhA~&eW_HkZqr*9X!5C65_zuL~`CtTi;2`yYMzGG$on!M@O8AEcMm)Pl}`Iuen z6IZ@vWf*&KU1H4)SRFOQFSCw1gvLb7v@7n9pJGw2OVspoadZy#8}QudWGwu$#jXDJ z7fZ465ls{iNks=PSRaX2DTH#;^^11`s5;{1M5>u+f4|~3@;f}nK85;iL|K{@K}b2& zFzaAtbZDa4r991R8S$^$6=&KN+YYi)WO3oAmFp6yI_k@E?G_c3|+vbysV*ChWGIo$K><8)w>qw_Sg-kFa2Ak*WJVp zPU{GY%_m%!(BF!U@VF}>N01={@J9ukgNQ`?@2Ehoh}xev%+BovT)~DDTe#?k;tCrY z9bESruPxs{Za@@njKL~n$Xiwleu1^rHrN6`4-cDZhov|Tf2zfj>x7Su-{dFpZnoiP zF>}dr$C4ewyYm8W+Mti|0%O)EYB$=sJhVgbL6Bv;E5u$WMjyj};@Xg8%Iptum!Dqumr{D&z zo~iJm@MI|P3^N47lGzzSfz+Wt0ATnfTQH?f(5pA6s$ulj#y3!2xtN3=&e(S-%yzJEJCc}_Amn`R*h%A z+&XPta?JOqmQD&YJjd_>@+LoZY1?4yfmny>Yk4aaIbEj1&Ey#0zMDgtYV3HZS2tMOZ`UN9V2&!lNHLJrWQ z43C;O)U7F3;o%_pK=kZq@tVPmnKJ(|rpTcSh$6fbl`f6U2;vqxHS#?B6#!TY6^*tH zhYL+WSDgejJy^fw?Fn~p7eWieMX(*-t!^+DEWVRa9@0J-3>n;8tZ(c%EDC*J`3}n# zw^Lom)`<={rtFH{lkxwv5J&l3uQNtrTSw#B^at>h>Pzon?O-)I_Cg>=G&inwP>${R z(l$4UKj*PcOskEM#zrEO3=3@(TE#qkzMgoJUVoy z+w(4&u8ic^4=_X4F0-3ax)>+ZE5R@3KTB*)pMDdFHViY{E>O;R+}YtaSkK#+$>L)*r0?gGjr5qF7Rt?ZE3SwX5Ai4`l|m z3Ago%lal2K&>loQ9!``h1`(a2%%=(9wy8Sv`5d6bQ3+zd4as3nD`uV34wzDHn_M>F zoRLo$KEg^sY|yvm{|~paZ@{ZH#1tuH16f-9fX*uzArT2nC$&&l2p(L{Ak-Y;4|@vN z%-zMx@N#HHIaZW{L%^ejdH}lMz-JIPX^l-ggb&m)KScnR!|>-BZRO|gZ(A83#`wk6 z?blk?=s}Rbu==p-Ug;iO56rfq(^tROAT%bcOCzB*V~>#!466C9Nw%lNuz>7qt`_dS zt#8h4fHxw~5+25PmMOdU&d`2Cy;;&zN=~N2CuD+u9r{OLtGx zUIXDQ$w7!PcqfIxe$$9j=Y`GHiAO{`*-~k#4v5!{gN$bkPaC)9vIcQRVbyVqBGg%9 zGvb1n$tILPR1TV~l^udRn6F1@;J8x%R1wQ9`Tu*%`Tz1AXHTi;kyoC$7oI;N(&-AgR(@l8*xd?>d<7JuW zQK^{pUGhn(ZNNm3|8VW8-b>OgBC_GPcM}#g`*c*j4??CK81aiPVvOmm!v}Zmo}G34 z#8*pBXzc1wIdo%p^Y&3Tt5UV!6eh-7A6V|)ZTFE?>!G5yfBeO9br!x@w;;xA^465H zU9V-dSPEyy0Q+H?g(teV`YNFya8hBHN-feT&%KM4jWbam zuNW^j78P?0>iC6?}xwA1kaS`!Gp0on4cjWf!o1cl{=bzb$&8@-GqA=p<9-Vc4-~YHDlJy zG=HVIf88&4Un?kW`hIuAn3*X{KJ1{Z@iO1|+{@*=2YnIL@gF@)Bb%6{NBoYvkaMX~ z%g*cL(n2PeINHS&{QYCkokjiYb}s07x?qHE`qno`_g?s~lK!u}I_TDnW_Xep@#D#J zE}anizI(Nhgd>VW{tcWLO2_K@DM(-6L+$1qL&?Hk^z%Y+z8kQE9 zO^+fQh>1Y1r0!4LW8Q>7-js&;R9G46U*i@s0%Cs}Z15QEK)Ia=-m>FbtAF-DSTi(& zr)sF`kcG2An=``Tomn5&;AU1;Ms%XCD+3kvRJ{5J=C zHBhET#YME-8(Q{>a$)ChefPf{I!y1rR$}>+@9Chou4{rxZG@mlR(X6EwAe_|G%)Jm zUNN_jU4g_yG7kLAT>X*c?<-F_AZf#{V3mZ;#yx=ruWksRDC`FAg~rBPA=^;y>tY^M zazm^bVzb^>?>I}eijB;R{06Amfs_gS6RZql5LPoFywU*X3z^JYB4*+bxt#bDgiyxZ z0G#SlubGhXB^+?G*~S(fS=VHq)`0I!=@R}+b`9+28d$R4AEDGDVfW$RcSE^mZ6Y!U zhL&mW4;+^LoD!FPC-~?N%V|w|ZQbj5`?N`^Caw2o zG!T@v$(rSp!UnjTCtQ6kwlF8L_vXVZZjKn0xA4rE{RisZeQtgA%+8DV3{tu{_}tA& zy!zUuE3c08+3~zphj|UK(T`2mx3jvlP3hhHUEF8KGpPQ*Xfw9E!4|7YKA8ln$0i=e zeLUah@A{1>qs5NGUGCsoS@hUs3Ao538TjOuG14)y#Qk3RuKf2cEBjT5nDXgS=W~-{ z4`B~7(OSj5$+g{v@ew&Wu03iq7ajlY#r8d;-vP<=p`?8@X`9)SB;o_ zbk5ANTP&>3_?k}Wx8?aa@&?q$`Py$&b=kH}b4@4o+~nOYZO^i`6Mk@cyZ*?AUu1tj zcEW}FL1mxl+w~1I5Ax{do7roQw(OGv$G*k;S_CK#c}K2z%+cn4w6uELq2Bv7Unt|= z{N$YJu8>i6_+EXzbC&-0-K*w_FLv^{P~Ys>C(5)Y?UaGj^V}zOZTMq@-A5Yj^!IqH z%grT+SM1+h_6e5HOSskfQy8b&26$rrn!gf^_eoe5NEUI2aYLDi;j9vE0~X`hX;=^H zjgYfW4G;}^KJ)uo3?RI5yrsr)%yq`w%dLgh5}rZ=4Nef0`SlUkvGkU!3xVmZf_#=G z64wZCl?P4+;>tcL&JTv@8s`uNjn7trb zruePI4@5EQa?z3O!pkH_;w8X}1an+cScv72y#}(Z%_sXm*oT4!c{z@OR(vN&5BXZ^ zmXf!|x2*I0^-8_O7p?EMz>BOc#hYc=-HuR-xf=&Q>(L& zq?apnpqsNTzDvO8tH&n({F?u)silik-Za_wa%j!YYm1ONAWfLoWMKz=JG8vp+7=lpPD=V9B)_sn8gw+Gj>`*4z*u788=cA}_W``SxEDi?`@4H-LeuJ;Ee0V6JfMh_C zl(3SeOYu==56IWlY##Z=^9VJ@JV91h?K5K4qO#=5L-vB_V2260y_)LP7C|_gaN2Qk z)mB)@@q}dL#)ZS)MbTy$(xNmwFs^#FHB$FIzKw^h&FeEx^WCI?wtlmkO!VB@ zI&pmdI|Uu~elw_L#%qpigU%+swrgJh9RIBsJO69h~_!DW#trIrE`TM&z z+}r0vEZ#anev0>p?f>~oV$?rl-@u`4efMSipT3>6p}N(clp4ElMky`#ruYm!HfQF+ zUIVj&H7-N{W>P$3>b}N#-HIc9E`NLS^gCB$=MKUd_{%T1TD33l>$79doH%8|_OHHu zMVaT&uu0D8C`c3=0~lJ*=xhZ3MT>{phJ$fijQ0S`i5bTtHPyswT#FkdY3o|aRpg|z zf6ch^)ZNUkubfaQ*Sg4Y>A^q8<>azpQH@2!k^sdy?lsC*EC4(;?k1Bc99hVKWqo47 z;>%by03uMK!sIie=fee}CaqjDjDE%jmIvA&h|{u=@wV|k@Vzr0Ez1&bR3h4}gKXm? zZM}~2+vC7klP(cy{*Vt!VFk>ASnCYhgm9&>Z%$1E{^MLAsb7J2OXt9?)QzCx599%Y zFU>fC!5(g|Egbqkz&&Dl3B zZ{h6R&F|KKWx!CM^w{8?qrTr{-fP>p1DCJ zdj|b&_cw!Pq#WO2sXR9(SwAv2aN&zNE2ef!OS{;p|CoXGeu?kz8t2o0@!%_tX4wds z*5xe>qm!Z$1I>fZug&!FnAkG)gky~U({^@y`(ET_W=kw#xnoOC*}Z%5_DzE!nF&O&CR8P)KwM+3mQbf`xy{29?l}v z!B!=f$@CAln>zz;xe;g#WDx~LN~MgWp9GD6=EI5LcDbq-eMQ^UIU!c z(nC=Tc(t{XsQJNaoWrWTtcjCslplKdZVPMP!EE2S4!+^7{Ljp7)h-b?bYHsPEZ>NT z4;t545J+K zO%l$fwDRmQw!4DEJ-oDOudM}ZCvp zDd->m`M_a8lW}Fv%j85tCXJK8A`}x4=3dfklZb;$uLhg`2P-!+<-ppgzI4KYJwaJ( z7`>UZ8-bQM`j)MkrJ-r;f{@8h2+Aeq+LPE`$C8MYC8{1_i||vtEU!NsiQluCibWMx zjSR}rjGF)r(*C?V@;JVHj`J3oaeCkn^4lYDt>tOMbC>Uty?(9_U0x2_zuCK+Wsg;POZ$x5Z1M6t zKHBSV@?mLh^h0qN_1p4g9=1Np=}8xYJ-gzkALnT=Ol+^b+oVPJ{fC;&@T!y1V!)70 zuL~B+<}SnPPVlioExo2`#!`R#v0D&MVutCXFl>^SME`<43naiY zs>QWZ12g-vEU8*^OQQ5)w+OvHq`<~D01sf4N4$zi7c-EAb7xB-gFnPoms@3A{g{HV zcQ0u)a}sQATwA6p=<};V2GJk;v_^i%bV6E~3Sy3WN~oC%QVj_SEaio5b$9r~yJ33+GQP zhh_Lj)sQsT>8ojdNr58%yYR9?!Aa_opeUORZ8dc;Zk&xo{qWh??U6a|yPJ)COKCKz zMAvAN>45qpS8m!8>;CnA|HCuFKksTkY-s7$l$sggOHwtlacRRdy}CA>=dc8Y(%$7L zvf*P;FQLok>hrIKN9=8&nB`8^?>^V&-Fk`%HmTo$DcfSQFpi$(c=?qh@s@cDl`cJ_ zd^c^)3j67rmKWnMJIWlA#LT70H`H>NPlJog!3|OY=6+Iqu#{$0 z$9$X@l%0)N#f6{Q9o{z3JG(LpYlzMoxj z#MG@R|7wb+NAq_|cYN>f5S%|WC(vO`zq50;y^-$HY2x%IDb?FQ`()~gUYSd=rBmjaiXSM`arFLbC!8E#`2VA{dMvbMFvU50yuiX%iltJUqMb~&WqkLTyl z)btK_hRsog1mkhcilx8I`RX%3qZuvFX1|KP)O!m+VlJmLYD$5CjWDaWG^M&V9{#N9 z837PM?j8FG@{AT(8mvq>FJH`;$4cUs#vd5pJ!eJ?@S*4#a!|nqsb;v+^f5Me)gcJi zmt-4^V~lNFTEvnt&E#YUB{885{Ko+W(Rp|nrxSK?K`yqU{A}fd(;NvWG6=CpAk!-K zMRkMwj&1kRN+tW+h^shK(_HHpZ@=01t=Tc}B3JzRmsP2s4&o7xYPHLVufM7^{IeO# z-UbPkL!0hC;u!7GaYa?MC+I>FfVoI~>W%&7XB+rcFID<;OO6Lqg+D zua5ubMDo_SbG?1P9chZv0xh@kO-h|UNLjpdR0)6-%^Wwn7#FBb&wAn+j86KfKj=1eb8i!FkbEhl?J zJ1&(el>+rbqWsusCBzJE6vV}uSlMb*5z7bLM&JFmZ99jJtuo$Vsp;%F;)NgEyu1AR zubEy49F(w?6S_9E`J`(@2WyjrXtNAy@u|>}Y9wNW;}jroC!5!IFb*EZi6( zuMk`K9pgOy6c-zrI=;Zs0B8_yJXZ|fyT$j&6Xo6Gcihazb3;9b7%1g@V)?KVgPjm5 zmvKa>XNojC@KE;nvVWZ(c^w#-i3i4Z#rcneet#+pP<|*iF;Wj0jPqHB^OzZKWq9f9 zDV=W(N7u_F=I+3cyQ1|vx%qJSHlsCH@{?PgyfMInVLprB@SQ^7htt9r1N!4iKC1EE9uM zqsC==AtO{g*I3*29@{RX#gX9Kra_I-2{BBo9=v+bpy#c6jX&#M@ZO~6!|V567~kz? zorF2t_AT3+kbK)T;qxjckmv zOs!Nk{N9jH?j<(RBN6=((q_IaNmQg4MO{`8aoy0HIBS)46zLkvb zsN62xX7){S+SRTztOnGzSR?W|n1vwaKyC{p1&`y(4|M^UxibDdmTEOQnS;M#f`t7+k zd;5LpKP$hyJ*F*&)X~$Uy5@e*c2>v{Bm=1b8v4xgojC97oy{bp4({5WfZK;er=({Q9#}moh-s+U|_ zKw_qr*@Sb+UE1lM397b}BQuOuvLOyXh{|w0(=&bC+XFdb{eJ8of*{$5XKKO&k_9Z|7E4kA ziK!J*YkXGcEsfC%=o8nsYtPcmUl0&x9SXQMdxK-NPu%LLILulnCIl_I{w5yaJuAAn z|K_?0dH$2)##$5MHv74`41-hOnf#k`T>HhEl(E?N7(qG$JEjt zV;kpP2zUnx#Fu%C5Vcx4zzu!E))b#O#D%4YmZkc9GPNMCx3_m>apbzZ?|Y=RP5pRk zY5&dRzD)M%jaQxLJ27@dqfM`m-Qu&cJqA1z!*8#NYKtamr^Z3C6KCXQeihs|ZH+7* zp5&N?2!Pi4$wq8x)<)&T9Sr1BB6wgYvUJ13A?}AMpX@~JZX$A-a&ru?c2H$TgSgL# z0-!4%eDBoYaVArIbhnLZCNVR32&S)ii490#F&^d62Ax7ag=7p^b;v`EfHyvwys6fB zRi>fRP2{6xgp9L6KtxIcV zGD!>;p#nsCG_y1_O*1r0 zY?`w2gXWYW3F0A7nc7S#7A7bPrYR>CkO!XM_geeL?0qL}{`w{Qu$ry5rWE*2BU+&#DR>O706bBYHS-GHX(?EZ=)@tr^dwxQ~>>pANu9 zyKTCqGl*dl^VL;hkpXVy&{k?ObAWK;ILptefmJFl-G4LXP=}hSswVrM3cgYpLY~1nq*~ujfc= zwe(wIPU2$94XEJ>2vrSs>F`6t{}MWMdUeL#cXg@o&uMf;r(k<>`{jc=_ud*9Fz3bL zMZIqZN>)5PO1U z%U8iIrosufZm!7sG$QVhi*?aCqu4kA^2PaV^&o{>!@B+~j+Al(4$6HO52B*a>$txEGI5&Xe; zh9bD8THjV9^EI!{Q?c{)`)Q{Mcgc)V1wE6CMAJvG_Uc8|Ln5A?OjRj_QR5s4f&6qs z!)3mTD_0!yU*w{T;2m|;Y{kdKKx{1TXt2^CNqQ||wL7TeQ^7;LiLMM$QePj#B~qJ@ z)!b&qIqcS|)S6r}C?zqT0Vt4#YXn;%B}O2_6FFvf(L0T}8V0(!+EhWP zUT3r4au4BwsrI6648xVZt*=`k#v+SrYtqn!bPpP(O_sM1?w)`R>>)oPtLZ>}iE$9{ z%)JFSL|<$UKsWt?mb#c*SuEKOqlElP1#Gy{`>!1C5w_kT%?O-dD!<1CG?6^2u-rgX zk7qGR$s@y+X_E$;ePR6OmQKu86F+`IIxs?tvP!+?sg8(BPQVN_G1gDXqN%z+gh21| zQs#;5Eyktk zEV3cvW;}t~ejk(zSEoj4JAd}uRsB>!OcE!I3BQpWIyIZ6+ASs{As~1scL$`Ae)4#8 z0l#Zl9{2+);d65Fda$M^nF_2c^3q5_>DfR-2>iDxX{4b5?=0j_;@Tbl!BfciMt%HiZuZC-I{S|!C;t>n@=hpOc?bN z0R+a*y`-LMNYD?}NHA=*lY_XX^PB!B8zib3#H!lCL}69{XoU1d#;3Vr{0fJLRCJCj zOE;U+)O?KRdI^rE$ym`uexewol>r`9w}CnvO}F_{5faDbv*x6FYr>N)oHDf50iqyl zH)hNlNOIPVB^bGN8e<|4NZ}-Xrx7^_c35h*t#4XnlBXK8U|L`(xQAUD^F(dV#F+?9 zsHgr8T8h4`4uf_p&t%lUQ~4i7mFyl(2G(X!+j4yz0Hcgr36lbqCfCIoEjX5JTrTN8 zsd5yutc+%W=M2`~eyAan@zB=uGHKmRw4z=o%{c4gWiWzG5Y;6JY7^eXK z{^Nw#*Br+v>fY*%!`!u=I0#sI0{-8l4;VF|k2ouKqse2s426}Ks5T%Y(!9$3?6p7% zpX&Hzo@^pmD2rdfl9|2+s>!T%QOk6zdhB*H0E)P9Auf{iG-pF#WQ>CF1n1Qt@Ns5^ zx2kX(O^KOIBgJjG4+CYn6*HXK%l|M`2O>6jvg zt5<9(7@Y(;&Z5)!@uKHjPrasYWuJW?Ap@29-+q3W{ZwO+&o>+vTCaI1n2rsoprCho z084?lOwSTqOIxG>c-o=GK_h>rz{;j{H?TnCgb+vz)>rjzn03FgQVRwTEoTZJLJ~hAxbv6VN^mL)m{nEu|OzC{TwwxT4^CZKhldn}w2G$lnaB*`2i#^3>FAmvKl zA^4WL2~2UM%n@5BEv~Cvwv`NJU~SEaZIe6>r>~s}lK$jDlC=1G@nl06N^p`%M6H!- z>5fKuGd;$qi4=mGVVgzj?}`I5C=d(m)H2J@z3;e)0^Uz8zN96i7flkjgsJUEkA?S3 zuhAU{knmC_N1@cPLKvpW5>?VoyfU@OIH70yQy8aSNwl@$UQV=Ejlemr8lL3X>L*oYddL+M;_iKr#j#fpc@I&Y z28Z%XzHfE0Q=DijpbOzl2-TwJ$Q{8;4OkQgiMrf_IFCgdDqQKJsuXlb>UAOyLXhK& zE=N*&%=}rD2c;mL4v-X4i7yUalu9Y@0xDp>z<~ju{$gedLJNuOpV9I^7lR$o!rtg+ zP}sL(F7v8(3cYzIqN)6Zs9({lC7%+MO=X7UP`i|jI8mI`%NEvnsrk0>NNG}v& zJi%aS!->Nk7toXPvMM)jG)m+@?mv{ek*wBXvV~B~0-W!-{t&TBP|b4?*|I0+s6j=Q zEr7t0K)C>bdu3;3nKE^IH04=Z8)voVP~6d+ooO;{)w2m1*s02jGAl7c`D|vaa1ige zD5b0qAo*&)v}=Y>2ZrK2O zKoYsLz6@ysdT0yxJe%;6r;x>2+Qf@w#%;vMlI~NsGAnW6HydvVUAe57;Li7m?_|^Y zN8gEj?yc@I`V6K74QzOykFn?bU)E9uljhhAuc$eMgaAkw_&aexZZlgZyc zfOB-fYFitzGpP=!l3)P!a43BZA{_ayoY-bfh@;7i=)2Mv>HMv!sjis zO^v}?ATF$--H`aCh1CVIcl%Sg>F`#(ULNc+5Ej+mL_+y6g@ZXWxO5~*Y(Fn+_q(kh zPWWiF-c%KRPzUFA+L@vo`#JC`^YyuJf0Pw?H_{^0Ev>**9QMj^5|kkgd)Qwucy!6h zc*2}?wfjha2qxl#SfL6`=wX5YH6K`XWyc7b-OW)vB8ecSWxrc#h)fN_skr*onDzQ(9%f7uJyYJ-s= zjj#LsH|T%D`Jdp+^}C=vualOg^YHvF>>RvT+jia1Kr&bx%k`T!nY*}-wGszwcO7r9 za#M?G)~-=+d_2CWgi<`@(FbjP#Grtzsm-0v4lrxN!y8*#EW3lIZRr=@m_eer+ndaa zN*lVkQHn8mqpfd&(29#oIAAt`fO*z@A5CSNEZTUVQA!MW*{Gcet2MT_b^#}ac2r!g zv;4}Uq0NidMrLN~Y}o9UTAz1N?pDIo2`Bi<3c3$RjA_bQ6hZX@uipNWDR{=JK0dFT zuGX&56kAZ?Q;!Y*a2V!70pC_noo`;Bp1zqZoo>S%=>|`t)X%5$&TVx+!&J5PhP|Cf zL#Be3E9rR4*P*jQwnbbSme${vV-v`sYq)|iuB+gu`EhB0o78$FkhvIiq5F7|B&!qi zFmoQLkB3}CZo&cH3}2zPH2z5@6(DZ8h8`6>U(NJF5$75X2AvqJeGqW3Jlo8GlkXZT zs|GLc6SzUd<#{h_Bv_O5ne01nvuPqyeN2EbeS6;7YKX9O49o_^2q+S4eb=;99y5>j zYU^2*#^KRj3uCy15){@#byz_+1XCS2D#$T`1VUKjl;(ov>QoB}rhfLZ`N-b<^VeAzsLKiNpFWZ7DejCwd00^e9&BOVPq$qqtLPf0{ zB-nry1sR+%FzX+vwa}-v?BA2q&LS~XSOiY)h9{xSu4qAftui$wZ7v~F`oITd@h+3c zdUv(&%yY|^=4hNMO37X(Oev6h^kjP_flk=xvfRT)=Oe{7@KU@s_fiepEQ0I{6YJST zF+}WDlZG`|W{mOZVQszOC{qf=i&ZFKm@J)Kll)TvCsHkf?pT8c?J7Fl3*i!2( zlk42DJcbF^nJ`6FZTw72s;j~KWxOpL!CG!25i#<@06Gb6aH*-Kg)xdIpq_2?Ug0;dJf=%9-V6}Rf>lIT&&vTZ~}V( z80A<>m1XiOa0zFG9)dLE+uX>gf6IxIGgV|TD&Ud;5+5HaZ0bDMxu&eo9u35nM9LpZ zqveJW9O;?z8l`$tIy5BRPEj`E?x6_ZdUoUJl8emXtpWDb!eqww9H}@?OWu}#*rUD4 zOk12)7H|RCk%7Gmc|CfTl9DpB*4JQCk%7Lmw29%38J(SjEQ3=&1>}|qkY#vGju~!H zpHueBR$45Q>8%Plng_fTv$Dj5Ct|ST_y0VPqbl(ZsIXUA#r~dX&wBs>mV;2#0YrRR z+wj@*G;{pRd=fI#V%9ug&nDuzN_Di*no2E{xw2LT6IT43FvUlRXK}THIHU9oo|L4U zW&_^7ZQ^yr$IR8anf2TVfkCjRR7fr%*4`&P91xPRV(3Ixi8fv|xn;J~q%jPokxt9~>}ds{3O`&{R8M6BuSNRq@NkaU)RIM;;E^rk zlvT~!JZ2gn0s{Mha`JIGFtNl~2e82<;8`}ouPat6_Irv_eQK>tl{7T%D>hUTo?D<$ z>S9XmnR-PK9<_fAYu%%#>dclNE&OV~RlX#NB0sS_q%PtpqB{qPRn>r zV8L~rYAdbETQJKtbh+@(oBM(3dV@hXX9<;h$}AEWGD8GXLn8$$d4TOnXY-H2^GS&@ z0n^y3Ky1YnjHxEEzpb61MjNoxNU3<|06o{ zU}i_E!+8%RkKk(>gJl&`-wkOckHYO4qpl;PG9cF|7?a|w9tJS8Y}la-4{gy8TlX{} z=B4FCO|_-n^r$NW<2^Dz18c~jX?9E$tEM5FmxB5SzH z)ImJ#fQZvMAMsy~^0Iisq z6GphUTvREv1Xoj#6qYr45>RbvfM_Cw7$7lI4H24I#rqfZuq+s18}6ceG7A`kq60?i zXPhg0*wYpUy;pNpEhsuDEdS@YB8IM^*E2zEbGVS*>0IF3IT)WD-+nc1lQ$t zKidvVB#oKSVw%S)E<2iP25t&BW@wDVE@y^_fwq)S^A(0F=f(h~EBK&z%s&&Q4=WF; z??5AYg$9G|r$!klz~P)PSfssXU>)MJOd?)s-z!cs~_xQYgNu zv$x6;Pb@T;HA^nnHesx!7_7uuyP4yg${{oyxU0e~|8iLs5t7_M5f?Tl3*(x?M=Mkr zFBHe&_q-+o!E{EXl#<Vb*hVGFgijCcg4s0g(&l zYH{C#C>A3;+@hHjO&;B(oJZ%=(q}wU5m~W8^(%Z>{(=>$P>6`)dV9eT%XkfUEuWM( zn(11!^FiI51|s&M5W%ZdBIV|yEUhM<#7axxvIyZ0zh~lEJwlMaBODb*HoU)-HP`A9 zRdls%i;51$qr91U1Vw;O8b`>x1dqU~xq|XkiwKIt&>TgHP5l%Kq(C#)Cy6qS;V}ss zsCGF~pOMsLWteJcN^wuggjrf`imF1n3HHWMjMT`TPE}MDISEBBtFWl2tbsWZL&3#Z zIkk5Xp)?Z94Va8mio9WiJ%$39G+Pq@u>_3F;Tv&Iwx+q~5?-POKO~sTG;wDh+IXS5 z((3(6kr)n#c%)oSDU~eU+yxFTRTn2WmgSz1xTO>VM-ZYaDz{u?ngPs@%@dZvLFhxU z7eQR+WQ9zl0(x0l+(>h8;n->{7Adcq`c)I7!(teoTw{-hSrWzN31(}?+HG@GiM*nb zGhy#+NiGg5UL}$!i-WHB7}{6?!VO=X9N6Gc_Yan6U~yCSo_A3+!@ zN^wSkW_+c>rd8_fC2o>mjF!nY^1&i%wEUAVwGT)8aJ6XG#A**t4^MiUNG^V&MgV0@ zAi`t;h==CdPs?TlG})?lJ;yHf<(Y=SW>7AXkX_^LY}$*>YTSZ5xinN=wq!XdF~Xsu z0<$%OJO6vKJ>*k(tpm~7509oalK==0W^9hTYZU4vui-)MDWD$m6(IzE>)cL>9BL5Y zsx%SFVJMqnQsq^pRI@26C*%<$*2sBAaS^L{N}9T+(ad>#juBy%q6eNP;3-auhQd&m z!6-5VF1PYJ)eDwhDWw!mo?Y`Oe^Znd^D~mcn!trv9eIS4S?o!T9l0_th{aPYR(z53 zM+JS4wlvuG(oMYRh@y^@zg)VqmYL9Iba;>RKFEco%3vmTj4VUYBP=T|qm>29Ak4J@ z=W>5%8KmA!oir`{NxCI7DGsYrh~5Dx_pPc0`wt^DqRrUQL7q*cEmX z@+7=f5R)U$0v>CH4dz`#q6KU$LCPATkt=JZWxC9n?j2%*jku5@zb-c>$B2aSO_$LdAREwFvS2L? zdR@|VK*E~dfwi=vjhM5Ey!rsdREuGBh{cp+fx->8kp#nOWb5so({gsp z6Qnp2+yDp+)8;^ZI!Z(3Y^GlbE9?PXKrx1gneZH^5)jOfhcztM99z7~Ay04-&vIqd zPEb1JyY?u?E_VXSwCA0e)3EuH1W3#3IbO^~Ot9t0J)6`U7M|s5P8LUu8Hz>1YPhG! zt3zk&dACvModKho;_OI=)-9*c+5nVEqr@7$MZc9N(||%1@+RP35u4%ajeLU(BrC;N z*dv`qMui;L!efM4mb(g{7z}zCXe@>Zs0oV%*j&_7M$<9GuZ$`%>$YWzQZ{``l?byl z*_Dw6(M}*sP9kv(yERzX>Np@0wnj_PMJaH+QItnwyU}gD$#FkL3`0dlc%?ha|D?|o zl?54qo2J84KCHJ*q{T|UR|{-`gdPJ%*eP|4sg+?*xO120D5k$R=sR%6@R=pRK0#6!T?|7-aOHQGzs)B%;7Mu71HXtTlGb} zgu4I+N=gt*l>s|Haj7f$5n)0^6!?R?nz>k2uU4Hn?$U z(?3keJd{K7rTV4T14wnj%}SRsGqtrQG}7%}APPxt3g5P*LR3ZFA85NUNSC&{A{3~m zY=MEf1WHOuJt^X|UfOF;Xb}l}P^1zlI|&o@17i>fd%j|iWt_n<%uxcR+dQikJ|$Wz z>}qnox*Lx}Z@IaF#wW~mnK6I2CTeXve=3{QpuGEGIc|RtfS}$Au^4Y+j5K>?WTht9 zsB%3?a5fcIlQc|GjhU}uboKvaNR|!CtX&OF+e&Cst%|JrrrU;qYz9OyVYmK$F1NIS z!91_Y!7+0gvEsSYnU1z=y0e@n%uHrn1$-<6k` z8@a4Hbv-?Y34;j8>?Eja^srDh8Ox^iK~p^8@r0T2;j0CWy@75mRQt1P>Mt z!IiNgeb2ZlpE1fTfLjp@4HbnT{)&S0Axj8MWfZxTtvJ@-^tGNnxwB}9DeBfX;F1!s zS(b)>OS_W!x`dyE)_uwvupxJ@QsC|}W;Q@CN-9FBdnhB|3G{_s=NX=2m>ss)}Qr6(-aMH2uWC~TbWnej01~sFqJz54h-0HA)&X#iI!`; zx8KawVLifrySCY#TwB)-7Xq4}pMn%*@ZX0wHahPFamAE+&Z&Gt@y}(h0!EP=T*YPF zz_djukvdcd!5X(lg%A$NK(HN^A{c|TeCOltbyEYSdKzpAAkQKD6OVtgJ-MtogA2(* z_@A+b=`(t2)Cbt(2}n(yd-hPklV+fpt(jv4ViB?m1}TKZtd4ux z`;ankDFLMsg6&pb`z?ma>S4WZQ$KECQ)Oy`RzQV3TNfE7#?7tyQ^~zhRSJqJ0Sa2A zC&W~J!g@c53kEAm3x%cLhh0#$Pcw)!Y`r}>yejOc*Jg~n#^-0lrLEsAIB{#2$jGDv z15=)iDSkHQCAL!mXmsdL#aOIXFEeR7p1_XS+KV~)A*%ef8T)CJ;sIT1C>kg!n}i&C zD3ihK6n}*+K0pi*k)-`6(m(5fP%_E;S=J!IGzi!@r%2_*l$O6ATFxO7YG1;$?L~7; zK}m^-IHX9fs=taa$iBT$!avX(6p58y3UF;8E@ZWbfZ;e)!bS8DsVp&;8F=}>(E&46 z3DKqvX}aY`PQEbMVw~3UVcrIMv4GnA%<#aRzzdm%c9@0x1^KDP1Z)luvHZoDKXYIQ zwirWujoLJh4scyHb&86wDh0Z9+l@lTPCLkx?NJfvc9Fc4N3B)!>R`)z-N5!YaJC~R zgT7AFi8QtPPG4;YG#HzgBBu5XV1hCto6*xFB39{QgO5-~9&An_0sQy!Y^|u3v)#;5 zm6Kd(qMA>&AATvkCT@aLxVtYLF-pk$I#}7T8V^RUtUGyF+fL5l_yJ!P*4H=Y#FMda z=p4DMJ%_W~<&@u$v-Z~hyG!P0*<8Z+%&H}P7 zSn5?jKvmS!5=FrUQ{iGKH52y&glXS)M3U9i7%y=ia{)yqJa{YPkx91zVhSX{Mi6YZd@cy`cM!w+xA@Z-mu`X;X`zU;QW(g# z+Y=BoC+QwgN-XCB+}op|s5chiF%*{{faEGW;Fq|gUDdn}^QD|iR;8{;?u2{- z>92r_D)yO9w;$ll59tEtp|Rn>zy| z5U2FQ0+9`dn#a7efqqMB6>7D#9Bwc_XU$5)nJkyCaB9EKxyniSMquS@0? zlIvF&VkoKBqJ}t^wRMqxSyrK0afI2;J%L3p^t)(ob|Qy#t#|f88I2IwvrlFXiNI1X zFv;>C%?TD^6$}{|pe_cgsBOK2a0$I#qBrv~pM`4Nl;N8-D$L3>ZGjc5qy;4mF3?GznG24f(_FN zvVv8n19Su0L`D>B3=~`s3fAP3bq&-UN_^R9t>_QmrWR4EH+E$J$na}yYQ@gH>Isj_ z_e5^$^D&%3cT!snPhm+Vvn}?mh94Q7XKfa%9fP5Hl6i*A&`=^_Md-n!kbP-}HCM`5 zjXaq@Dy+El$Ltn?NnpX`sTQtbvjAE|p`DRCH`n~erK}BWfwJD~pxGh_0FX|Z{GaTY ziea4A2m_UL~s|8%?D-HBqhQ#%c$k#9s-$Z zm=z#x6?Xt9JX7=S|u_aPWG||aaD$7<~jMCtpCmHth4bS#f9AxNVDIYPPc2=m?NUK@#xqZEq zLU17-AKoR)SHraR*`E@NL}--M)|)K(sRJ|PTgHVOXsdtZc$*Ibq0c_^45ze#se}tV z^{e%caow9e%CTv|59K`Agy=hqMX)q9Gu-k_L-WNbo$jFsr-#M_6kg=Xu)7dB9n(iy z&sdEm0~X3{gBvZiS(VpQ3zu8#;;`r?kLT9IdoY(doTl`GV&a)b`?Q^fvOA5~1^BhJ zD;v9%*XYJ3b_k1kUC8{w^jZu1^-2?;5fui7J#EL*GfO5MWmCediMEDgY9uB~Mqrd) zCZ^|}+8o7??pAauiVuH zUR}E210S+H0CJ{4=_zfpVgn7Cti5VwhAG(w1D0koyWzcE8sAqP_GK52o7L8n{-vA- ze8~h3q$`h}WBRiS0Zf288mM-i(zexD!R(wunZ(De&((4oY7cB)#`tY_R;l6Yn!U-IUq@iGqX(;MYpdPN+xcRP*GYYQ~??c$sKkjeIm*@^B7GBwfKgNB>cA8eq; zg+D2y zuSo^|4+=NV7(>{)`Qa6-b8<7s6#Ei6>!87fJ-uG>Q1JRD!MlFofODT6%N~pwFm7L~ zr_#%GGI@jY^yzs{-9ydmUb^$yLl=E>>nQdgD=)e2+z@Ru)nUMb{ilDfq1O&R@?ZRX z@E!8=zb?(?bi%cxGyn1WT6Vwu&EIc**__}z_jC*lW4rW*_OYmcpWk43+AB=qyYF7j z_j>>M`S}w#XRyIRo<1dGUuzi%UCvpDeO~x# zSD&3npOec0@n3A%a8|U~PW!XL)Qd$PPd2;#L*K-7;h3t& z=I3zscY#iW47TF5<)5cLnDk$~6IGEve8`YVkx8>7*K~=l=p21l-1KH|ZXKHSMc=F@ zddwed7Vwsn&U&vq5g&YmefaAZ{Zq$V*1J});^EJqIeWs8jjuM;)DX;NBg#gNiu@CF zI&;>szB#w%RCLW*_jbab%@q&avsWj|pNPxp^4>*k^VnVIG!BZBkgDBx{&1ncuGqgV zJ!8+8oz4vm7|%gY19cYuiEGxT_=9oUw%72`8!@8To)({3;xcw z?c-$F6$joJyy8&s{b5msmjyPb1nw zwigVj+dm+2&cLLLpI%ixbjj!E6jY{^RC2^wZqnPidmFG3!@|0-cgv%{mU_D2#HWj{ znhG&Jn!v&LF-La9OlLPO^7@=LCAE7>%6hza(bFsTo~5JyN)IiqJJhpDuq}74dqJnA zJoCs#H_8EH|2?}(6@ZvS@5Q`~T`W17cEm-pVo#O+9zQ;P~u0I&*#BhXcl6-*U~mb|*T; z?7cWDO?7R4aQuS0V~fUU<+t9JTUyX8dhWpSI|e=( z7dNe@R?0tz>-skx&f0dk^dB9%P1R}T7@BtW+f#7fo(c2gSIuj^AKPA9wJthvS4`c3 z*aa38C#D=NPI*;Fpk<8D%Q&E|`tuL(;cUH)%vTa8e~-GRtP%%(c*V$xtvcLW`q9#p zA7y>DCF|VnM|7%j;=WdCn{-$`aZ@2CsXEo4BxkC-mS~&5_QZ)UGsgckq`EM=Ziu46 zo+Cf(nf_9@pC0pRk*s7fVEmy6$e3UNW#=p{JxAwW?JoIwq?OOcrs_1?oM27Pu;qh{ z;iabhm|FC&87a4JKceHWS#tk+g_K+S>+JHP^)WxKcZYZW568R3GCc=QaIEK67W*E@eh;a&~T^u4rD4C){RnH*(y%$SbE-Cg)TR0aHirUE9=D zLfH)|#psHykriT`T8V4JSLA1m+mi8dcJ|gGVKslT59;)bgyf8bA&etBur7N4f!MkO zN$(9Ddu_Xe#mxMQBcJ6Z=4Ff@tRoC#<}Qk^{V{MnHh9|r&LY2T<7vJZz956DaFmmXyVK3E;-;yz|1!TnzcQ>nfB;kkk z1X3+j(hs@gANgB4=T9=7nz=Ha5IUvcvC!m8S)8S-&x_o>I%Im`rZA4VF8y>-x9Ka7 zem)Ju7`f%94ER&&Svrt$6>F8Iec>mVafj9I4YNa2W4Pek&Rl2s1BIE#cMiT|)V?O6 z;}71Iw0vOlZJ&MiS>nP;lS1kWLhB}Uh_2oY9hs_Qs%&}A1o%rn2Qb4wOFHyG+Lv^z zivk4fMCO5A38}s<2WHi9gzb2p?H6^RfZu&GOvjOK+_LJY zEsaNV)a=tawa>My-qgFCnsD@qA<;%*{+6zKpB<;f#LHl^w%25Zj?WJnIdbG`u2DTN zt@<)YxZ1RBJ1TEPM%j39WZW0M$2DQqqoReu8Y!eY!c6gKM$FRc`8m~BpoQyg6# z@=pNW8&msoigo{vll6(HANvg0_X9L9Nkyhw9ZFvs5?vb7@0%|+&+fn(zBYog8UCpB z+hWSEjlFOJqP{$HWMbkbr7Ak3K#&CvL@XPWo|v9H_9h>v?%kez;zsJHzHbM8_x<<# z)=qBm>CRo>f3L*1S+i#2n8PrN0GwVkNr$J2$3z`@YCs{`O4jT{Orr*b-vYW)-+-O`5V_yYyBN3ZE2Y<+n#vVfaLGL5iN}`yCi;1?aeV+ zcCU$HRgLcHXh#`y3Xe#P{785}O+kcA|LLm=jhuF=gW9#&O$yG-FFh|aziqd+VL$8w?0u=nyqgKP~?D!w1kSqsv-FuW7ZQv=B(1?^{t_G@qLM z{)=PFE>0?pOc^3gF0g4vpn3Xjd-k^7GqH8@veqF0PeEvv@IU3mF#j8sNokc=g(pr; zO+35q;EcK{W}%9HK1^XhPhdORD^YD)%_h~oBxl^PYTpgz@5i5f--n58FQ{SURX?WH zJtbHE_p@z7{Z;D(%ZVF0FIY2;yzdjXJ%`jZvLD>q|Jy-H{HD?BbzyZQW9Ay;5G7X^ z6oIN_V*Ah~S6N>JeG4`T)o0kT@Txv|lf|7RM zZnUC!K{EHOQsGQqzJdj6RP}a#b=#D~87b3@Fx}OC|6Nt7PqbfJ_43k9s-LMVwWxQ> z{@$$Z`6Z?2XQ`#e=PzyMU%j;U`=w>_af7kYPw~g%gp^?-QX!?PX9U(p1R9&MiIJdL z+M0cHYuTUUkN?@2h)>b7S_T3(Sf~IN}p)tuCUvlNrP^XHENn@ z9J%6_h>BZG2ry`@niE@nF=5k;_{VP+ZSIFXq`Y9KRddnqODpbN8nRmUGlrODBv;Ig zgQd9F%2kI~t^fuaeDzD|Nign>E!PI`(Mvmb`s<>C-zm&GKouR(Jj-P^p*Vu=`*a3H z=!0uqMQ8qCuXJGR3frXTPce6t?T~86m?~iD@y0Bg4?MEv~ zy3e1GI`+fVeUn9G<{`9@QU0znSwf~9yYs78>wF8Jv;*G?KGI0j^~a&*`(Vzm6mHTX zNj`-yUn$whE3J!Ai$V%y9$d@!`u&MGRGd#NU_qX3cwfH1j}rPBNnZ`|NlBWVq?71< z?HJ0a9KpOVYk%KW*92#Y)fLE(FMV-h>-C~+@3uRB9c=}v<2I)@c{Cv*wdM!Xv|U9q zeS_G12~?xWjz!XU9=ir=obf4w=2WU0HL8b<8U@Wa-(L>tHV|FuZfp zLLsM-=gQ>C0~4DnSwBBCG;`$M#=hchZx=#|tV=`&U75V;u6Ap;VBD}QzOG`LK0~zi zz14mEo1wK|haTv*r?6X9#^-?K!I}k;$)7~tb)ip$bwK%LD~i9% z&o6E{Be98mXhW?D4AQqr*683-AoFEGu2PwQ{A2te-?Ccjk6Jn86zWLWA16lIUPBCCjl>ImRqqL`&;Y*BR2w9+jaKMLDVtJ=EQ~R-=(n3ID2)DJddH?Y}B? z@3pbz*JhS&xS~2^1rjJH*6-%JOx8k7q}>FiSk3Ny?x#z!fZZ0)M92lpz zE9f{N=^R+|zW3)p4IF92E4WC}qbk&zHG{;~7R5Jf7V04*wm!IbvMjrDgM zkWxOI@`+hPNir=!#_6!!Pj~tRyuUU946Q{UEpyb@+qPK<+cIsRnUaK;m6&ilVF+#Fkn8Wolx`*`Q|wIOr({d*Vj`^F0q-|UV}AAPgE0)mCq{JJCgCIoja$(k+Q!BTHo zS<;c*6i%A>#XWj0gPe=X*h##EJ)J&IpcR@{@l0x{o6EVK?_IQDaQelhCY%+%hIhxS zQVCqO0)=)5fme<0_4}dhwA2I!PcLk~nsip=BlH=PEnWX;WdByPkRW=r{C&sCXOuj1 ziZtQDdmdzW#N!$MnvB4(WRu!N;{1!v8(KrdijVNP)u|Tla_SD}Y*R=k@>_sj-(n1r z62!z^?kzcXZ=kP)c9M+N?xEP@4>d|uNGO36l zME~oPbheF;CLQpTs1k@*Bc_9;u!wjV);Yg)iD-32MslLn4kKhOv#x{zs>62YpJ zgPW={=mvYCw1AQX^bA#E8s+~Sq>k1Tgift@^v(XZZ<(Y)5d{JC-TQ<()v5AcpUw%7 z%Ag{I4SY4PvvNlY3M8&-J4>KA+L%v8mZtqE=qw=BS)Wvs;ZKg4tBWfulBy!m+8zI9 zdu2iBN$AeBJ{Z~-%bzUKHs!H(kq!deJRs#3sS5hL`s<#^j_Evq>zOHDU#njEuhqW( zwPaQugWu?#5#Ul{81{4puf@%pIXTiR!<~+rOK?wJJ4>-s@YWsG9aErFS&mI+D_m zs0XE|M9I2!SML~XQ zeE&JJxowu%Nl%PA_D-mZ4;{Mw*oW;I4FW}2c3v)P-bw(zV0Z;W)_HlxXNg?*bTpkT z`uGFU>q7&pLrbNcPbt_rIQ~gm7r#dcXcL{%W&*^yLdu3p5|&tyeVOD(xar-!{53o( zW^V7OeJ#bo!m1{QnKq5w5`Rzbvvx1@UMaP@|8y0zNhp`CT)D;SJBlQ&A4;EN*A@=@ z5;v!g>%$AnYX)cU7+glBqR=D;snS%;MO%q^Dy`9>;A7q5*DAFt7krxnh=L}LF9|_; zTq<%=LG%M1CrA1#B8wOKPcAZTLV@6))HYRf(_2Qm>EF_Qwsi7Fl>!`vW>wXWGMd^J zEd$mcakNrbzW*F`xm1%arJtTzj2~bp7*>_1Ds@?;>*`R9LAb#fq)!NU$l1 z4{V6vueh=x`RLxCzWe0N8O3enI)S4lbvL`vT&eUn3#abJDfikK;5zz4qoO6KZUroK zT$@I^tbcxG)}h6#uD5rq#v~tmH*D+hE<;#tF(JKL#H$)B!Mdc>grwHYY0682J0!%5 z`y~+ZW$~&*i_7(5PE>|&Uhvd_s z%p6(d&^EnI%#!-E>;01CnBxz{&aom-3LI+*aV`Kxg`2P^v$m`(RbATTSV_YZ>EGqYXFCvKlWyOL=bfEzrViL ze{tSVB_*FMU*4-{Y1#n+Y1M~mCp)S-G0V!X{#oQKpD%g0B)RV0n^OC}{}hkeqEFP< z)}A0?FYAZA?6$T{e_Q@|cFY(y!>zk^@s4xyZe%Lv5(@^!p?O|4)jxeyn?}U-;x7GL zQSf55>(aSh-HT}_KTI>im!xuI-A#X9d~y11dadj+ZkGA*KdD;!QxA;Zd~1KZ4sY=$ zCnxg?F1%tgFmnGnG}d>${q`Og&YwR&tJAtp9}h~Wdf-k_4#q>N8yA3s4IxSJV$qi` zrSOz3?WvQpqL1Ab{e@Z&Bs|qc{^iC?0>c(BAFXFw+~;weHh5#}n;%w$IFNz5Jo5{I za9Q-$qN4V}zak|@d;~|O*RB`m4=3HHWsKkPY}<&t)92{9ZxRg^31VqEH`$atYbX2r z2g~ho7hw!ELLz_^_WBp!+oWcqmsgbl`e!zQgA$X!4z0Z)cot&gjM5SLCq_Jc+@G(c z+-g>B+O#RX6H!x%^ux!4dH?wr^_^G0+WauEc372Zzte5Y_yooEjcpUIoVvKKB`L{e0+-XCK+fqP%xeU9?JPE(dW9 zi6_&D?jiQ6U$5T=kF28qZLV>t{N{~{J_@#kpbHdLwK=^fB`5c{oWPm$ow9(d!>aGb z%!7Z_0#WFz`p1Bizqtjh?%i?!?lAts#XDa1>j1Id)$fFzyviCk&SV?)y*&PZ$}PXDM(oKb0N<*(1{FNYd?Sj5`I z)lltge7CZpB(?4+OE^Vra{L2zR2yCi>WHuzl-*7Yqm3o&d~y~4Z|(?gQLfy^eUD3Q@ONU>HYTeVzyh)w zcp5^d(;vTEhOL}50wdJ^##}4cs%v|WKaxL3B;}V=`|C@f&ngQ&Fsb{&NmT=M)XnvV zd$P_|2dAxS-tc>BKLbNBxT|Oy->{}zL(#NzxCHIOw zDxOP{+H6D5_i5-4#8liCJKK~ErAtZDV^pF4@PS1HgI~P3@Bi+%cuCsUE<`;F7GuCj z6N8N=Dr>2xW#Eyz`B$wzZFbItz7@R>{QAnCHV+1MF`cged9lv)x8u*FYX0VHw|?cz z)9tg~760?kvLAiWL2o?}{qKSP;)xJA{_yDY{|{OnHv#|v literal 0 HcmV?d00001 diff --git a/apps/gpuix/src/app.tsx b/apps/gpuix/src/app.tsx index 213a446b..8f00251e 100644 --- a/apps/gpuix/src/app.tsx +++ b/apps/gpuix/src/app.tsx @@ -88,6 +88,10 @@ import { import { useWorkspaceSession } from "./feature/workspace-tabs/hooks/use-workspace-session"; import { RepositoryNavigation } from "./feature/repository-workspace/components/repository-navigation"; +import { + RepositoryFileViewProvider, + useRepositoryFileLayout, +} from "./feature/repository-workspace/components/repository-file-view"; import { RepositoryFileActions } from "./feature/repository-workspace/lib/repository-file-actions"; import { RepositoryDiffEmpty } from "./feature/repository-workspace/components/repository-diff-empty"; import { RepositoryDiff } from "./feature/repository-workspace/components/repository-diff"; @@ -213,11 +217,15 @@ export function App({ return ( - - - - - + + + + + + + ); } @@ -663,6 +671,8 @@ function AppContent({ (tab) => tab.id === onboardingId )?.workspace; + const { layout: fileListLayout } = useRepositoryFileLayout(); + const session = useWorkspaceSession( state, saveSession, @@ -678,7 +688,8 @@ function AppContent({ theme.smoothTransitions, theme.showScrollbars, theme.typography, - tips + tips, + fileListLayout ); useEffect(() => { @@ -2188,6 +2199,7 @@ function AppContent({ /> ) : activeTab?.comparison?.intent === "pull-request" && workspace ? ( ) : activeTab?.comparison ? ( { }); type Readers = { + fileTreeScope?: string; readComparison?: typeof Reader.read; readHistory?: typeof RepositoryHistory.read; readCommitFiles?: typeof RepositoryHistory.readFiles; @@ -386,6 +388,7 @@ function ComparisonResults({ previewCache, diffSettings, onDiffSettingsChange, + fileTreeScope, }: { request: ComparisonRequest; base: string; head: string } & Readers) { const theme = useAppTheme(); const { tokens: appTokens } = theme; @@ -589,12 +592,37 @@ function ComparisonResults({ }} /> - - {commit ? "Commit files" : "Changed files"} · {files.length} - +
+ + {commit ? "Commit files" : "Changed files"} · {files.length} + + +
- {`Changed files · ${files.length}`} + > + {`Changed files · ${files.length}`} + + [0], | "readComparison" + | "fileTreeScope" | "readHistory" | "readCommitFiles" | "readDiff" diff --git a/apps/gpuix/src/feature/repository-workspace/components/repository-change-details.tsx b/apps/gpuix/src/feature/repository-workspace/components/repository-change-details.tsx index 7f1d1783..236115e4 100644 --- a/apps/gpuix/src/feature/repository-workspace/components/repository-change-details.tsx +++ b/apps/gpuix/src/feature/repository-workspace/components/repository-change-details.tsx @@ -1,6 +1,7 @@ import { createThemedStyles, useAppTheme } from "../../../styles/theme"; import { spacing, typography, fontWeight, iconSize } from "@1git/design-tokens"; import { StyleSheet } from "@1git/gpuix-stylesheet"; +import { OverflowingLabel } from "../../../fixture/overflowing-label"; import type { WorkspaceEntry } from "../types"; @@ -39,18 +40,13 @@ const themedStyles = createThemedStyles(({ colors: appColors }) => { fontWeight: fontWeight.regular, color: appColors.editorInk, flexShrink: 0, - maxWidth: "100%", whiteSpace: "nowrap", - textOverflow: "ellipsis", - overflowX: "hidden", }, path: { ...typography.xs, color: appColors.editorFaint, - minWidth: 0, + flexShrink: 0, whiteSpace: "nowrap", - textOverflow: "ellipsis", - overflowX: "hidden", }, }); @@ -58,7 +54,15 @@ const themedStyles = createThemedStyles(({ colors: appColors }) => { }); /** Filename-first identity for changed files; the full path remains in the row's accessible label. */ -export function RepositoryChangeLabel({ entry }: { entry: WorkspaceEntry }) { +export function RepositoryChangeLabel({ + entry, + showDirectory = true, + reveal = false, +}: { + entry: WorkspaceEntry; + showDirectory?: boolean; + reveal?: boolean; +}) { const theme = useAppTheme(); const { tokens: appTokens, colors: appColors } = theme; const { styles } = themedStyles.get(theme); @@ -87,7 +91,13 @@ export function RepositoryChangeLabel({ entry }: { entry: WorkspaceEntry }) { : appColors.brand, }} /> -
+ ) : null} - {directory ? ( + {showDirectory && directory ? ( {directory} ) : null} -
+ {status === "U" ? ( Conflict diff --git a/apps/gpuix/src/feature/repository-workspace/components/repository-file-view.tsx b/apps/gpuix/src/feature/repository-workspace/components/repository-file-view.tsx new file mode 100644 index 00000000..c656f9c9 --- /dev/null +++ b/apps/gpuix/src/feature/repository-workspace/components/repository-file-view.tsx @@ -0,0 +1,184 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { Button } from "@1git/gpuix-ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@1git/gpuix-ui/tooltip"; +import { useAppTheme } from "../../../styles/theme"; +import type { RepositoryFileLayout } from "../lib/repository-file-tree"; +import { WorkspaceTabIcon } from "../../workspace-tabs/components/workspace-tab-icon"; + +const emptyFolders: ReadonlySet = new Set(); + +const FileLayoutContext = createContext<{ + layout: RepositoryFileLayout; + setLayout?: (layout: RepositoryFileLayout) => void; +}>({ layout: "list" }); + +const FileViewContext = createContext<{ + folders: ReadonlyMap>; + revealed: Map; + setFolders?: ( + scope: string, + update: (paths: ReadonlySet) => ReadonlySet + ) => void; +}>({ folders: new Map(), revealed: new Map() }); + +/** Window-owned layout and per-workspace/pane expansion survive file-list remounts. */ +export function RepositoryFileViewProvider({ + initialLayout = "list", + children, +}: { + initialLayout?: RepositoryFileLayout; + children: ReactNode; +}) { + const [layout, setLayout] = useState(initialLayout); + const revealed = useRef(new Map()); + + const [folders, setFolders] = useState< + ReadonlyMap> + >(new Map()); + + const updateFolders = useCallback( + ( + scope: string, + update: (paths: ReadonlySet) => ReadonlySet + ) => { + setFolders((previous) => { + const current = previous.get(scope) ?? emptyFolders; + const next = update(current); + + return current === next ? previous : new Map(previous).set(scope, next); + }); + }, + [] + ); + + const value = useMemo( + () => ({ + folders, + setFolders: updateFolders, + revealed: revealed.current, + }), + [folders, updateFolders] + ); + + const layoutValue = useMemo(() => ({ layout, setLayout }), [layout]); + + return ( + + {children} + + ); +} + +/** Subscribes to the saved layout without rerendering the app for folder disclosure. */ +export function useRepositoryFileLayout() { + return useContext(FileLayoutContext); +} + +/** Shared layout preference and retained folder state for a file-pane owner. */ +export function useRepositoryFileView(scope = "") { + const context = useContext(FileViewContext); + const layout = useRepositoryFileLayout(); + const setFolders = context.setFolders; + + const toggle = (paths: readonly string[]) => + setFolders?.(scope, (current) => { + const next = new Set(current); + const expanding = paths.some((path) => current.has(path)); + + for (const path of paths) { + if (expanding) next.delete(path); + } + + const last = paths.at(-1); + + if (!expanding && last !== undefined) next.add(last); + + return next; + }); + + const reveal = useCallback( + (path: string, owner: string, force: boolean) => { + const selection = JSON.stringify([owner, path]); + + if (!force && context.revealed.get(scope) === selection) return; + + context.revealed.set(scope, selection); + setFolders?.(scope, (current) => { + const next = new Set( + [...current].filter((folder) => !path.startsWith(`${folder}/`)) + ); + + return next.size === current.size ? current : next; + }); + }, + [setFolders, scope, context.revealed] + ); + + return { + ...context, + ...layout, + collapsed: context.folders.get(scope) ?? emptyFolders, + toggle, + reveal, + }; +} + +/** One icon button toggles the shared file layout without taking extra toolbar space. */ +export function RepositoryFileLayoutToggle({ + testId = "file-layout", +}: { + testId?: string; +}) { + const { layout, setLayout } = useRepositoryFileLayout(); + const { colors, tokens } = useAppTheme(); + const tree = layout === "tree"; + const action = tree ? "Switch to list view" : "Switch to tree view"; + + return ( + + + + + + {action} + + + ); +} diff --git a/apps/gpuix/src/feature/repository-workspace/components/repository-list.tsx b/apps/gpuix/src/feature/repository-workspace/components/repository-list.tsx index e786c8ec..6c7655e2 100644 --- a/apps/gpuix/src/feature/repository-workspace/components/repository-list.tsx +++ b/apps/gpuix/src/feature/repository-workspace/components/repository-list.tsx @@ -3,6 +3,7 @@ import { spacing, radius, typography, fontWeight } from "@1git/design-tokens"; import { StyleSheet } from "@1git/gpuix-stylesheet"; import { Scrollbars } from "@1git/gpuix-ui/scrollbar"; import { Checkbox } from "@1git/gpuix-ui/checkbox"; +import { OverflowingLabel } from "../../../fixture/overflowing-label"; import { useGpuixRequired, useWindowSize, @@ -10,7 +11,7 @@ import { type PublicInstance, } from "@gpuix/react"; import { - useEffect, + useCallback, useLayoutEffect, useMemo, useRef, @@ -23,6 +24,11 @@ import { type RepositoryListState, } from "../lib/repository-workspace-model"; import type { WorkspaceEntry } from "../types"; +import { + RepositoryFileTree, + type RepositoryFileTreeRow, +} from "../lib/repository-file-tree"; +import { useRepositoryFileView } from "./repository-file-view"; import { RepositoryChangeLabel } from "./repository-change-details"; import { RepositoryCommitSubject } from "./repository-commit-subject"; import { WorkspaceTabIcon } from "../../workspace-tabs/components/workspace-tab-icon"; @@ -50,6 +56,7 @@ const themedStyles = createThemedStyles(({ colors: appColors }) => { }, row: { display: "flex", + width: "100%", flexDirection: "row", alignItems: "center", gap: spacing[2], @@ -106,6 +113,7 @@ function RepositoryRow({ onPreviewIntent, children, itemHeight, + treeDepth, }: { entry: WorkspaceEntry; selected: boolean; @@ -122,10 +130,13 @@ function RepositoryRow({ onPreviewIntent?: (entry: WorkspaceEntry | null) => void; children?: ReactNode; itemHeight?: number; + treeDepth?: number; }) { const theme = useAppTheme(); const { tokens: appTokens, colors: appColors } = theme; const { styles } = themedStyles.get(theme); + const [hovered, setHovered] = useState(false); + const [focused, setFocused] = useState(false); const description = [ entry.detail, @@ -140,9 +151,23 @@ function RepositoryRow({ const row = (onMenu?: (event: EventPayload) => void) => (
onPreviewIntent(entry) : undefined} - onMouseLeave={onPreviewIntent ? () => onPreviewIntent(null) : undefined} + onMouseEnter={() => { + if (entry.status) setHovered(true); + + onPreviewIntent?.(entry); + }} + onMouseLeave={() => { + if (entry.status) setHovered(false); + + onPreviewIntent?.(null); + }} style={StyleSheet.compose( styles.row, !!entry.status && { @@ -152,13 +177,25 @@ function RepositoryRow({ }, !!entry.detail && { height: revisionRowHeight }, itemHeight !== undefined && { height: itemHeight }, + treeDepth !== undefined && { + paddingLeft: spacing[2] + treeDepth * spacing[4], + }, selected && styles.selected )} >
{ + if (entry.status) setFocused(true); + onPreviewIntent?.(entry); onReveal(); }} onBlur={() => { + if (entry.status) setFocused(false); + onPreviewIntent?.(null); }} onKeyDown={(event) => { @@ -201,7 +242,11 @@ function RepositoryRow({ > {children ?? (entry.status ? ( - + ) : (
{ + setFocused(true); + onReveal(); + }} + onBlur={() => setFocused(false)} onKeyDown={(event) => { if (event.key !== "space" && event.key !== "enter") onKeyDown(event); @@ -297,6 +346,138 @@ function RepositoryRow({ ); } +function RepositoryFolderRow({ + node, + expanded, + tabbable, + rowRef, + onToggle, + onReveal, + onKeyDown, + included, + onInclude, + disabled, +}: { + node: Extract; + expanded: boolean; + tabbable: boolean; + rowRef: (node: PublicInstance | null) => void; + onToggle: () => void; + onReveal: () => void; + onKeyDown: (event: EventPayload) => void; + included: boolean | "indeterminate"; + onInclude?: (included: boolean) => void; + disabled?: boolean; +}) { + const theme = useAppTheme(); + const { styles } = themedStyles.get(theme); + const [focused, setFocused] = useState(false); + const [hovered, setHovered] = useState(false); + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + aria-level={onInclude ? node.depth + 1 : undefined} + aria-expanded={onInclude ? expanded : undefined} + style={{ + ...styles.row, + borderColor: focused ? theme.colors.brand : "transparent", + borderRadius: radius.sm, + paddingLeft: spacing[2] + node.depth * spacing[4], + paddingRight: spacing[1], + gap: spacing[1], + }} + > +
{ + setFocused(true); + onReveal(); + }} + onBlur={() => setFocused(false)} + onKeyDown={onKeyDown} + style={{ + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: spacing[1.5], + flexGrow: 1, + minWidth: 0, + height: "100%", + }} + > + + + + + {node.name} + + + + {node.fileIds.length} + +
+ {onInclude ? ( +
+ { + setFocused(true); + onReveal(); + }} + onBlur={() => setFocused(false)} + onKeyDown={(event) => { + if (event.key !== "space" && event.key !== "enter") + onKeyDown(event); + }} + /> +
+ ) : null} +
+ ); +} + /** Shared file/revision list with native scrolling and roving focus; remount when its data owner changes. */ export function RepositoryList({ entries, @@ -315,6 +496,7 @@ export function RepositoryList({ onPreviewIntent, renderEntry, itemHeight, + fileTree, }: { entries: readonly WorkspaceEntry[]; label: string; @@ -329,7 +511,11 @@ export function RepositoryList({ onChange: (id: string, included: boolean) => void; }; multiselect?: boolean; - onSelect: (id: string, mode?: "toggle" | "range" | "context") => void; + onSelect: ( + id: string, + mode?: "toggle" | "range" | "context", + visibleIds?: readonly string[] + ) => void; onScroll: (y: number) => void; onEndReached?: () => void; fileActions?: FileMenuActions; @@ -338,10 +524,58 @@ export function RepositoryList({ /** Custom native row contents retain this list's selection, focus and virtualization. */ renderEntry?: (entry: WorkspaceEntry) => ReactNode; itemHeight?: number; + /** Only file panes opt in; revision lists retain their chronological ordering. */ + fileTree?: { + scope: string; + owner?: string; + allEntries?: readonly WorkspaceEntry[]; + filtering?: boolean; + onInclude?: (ids: readonly string[], included: boolean) => void; + }; }) { const theme = useAppTheme(); const { styles } = themedStyles.get(theme); + const { layout, collapsed, toggle, reveal } = useRepositoryFileView( + fileTree?.scope + ); + + const treeEnabled = !!fileTree && layout === "tree"; + const treeAvailable = !!fileTree; + const sourceEntries = fileTree?.allEntries ?? entries; + + const treeNodes = useMemo( + () => (treeEnabled ? RepositoryFileTree.build(sourceEntries) : []), + [sourceEntries, treeEnabled] + ); + + const filtering = !!fileTree?.filtering; + + const matches = useMemo( + () => (filtering ? new Set(entries.map((entry) => entry.id)) : undefined), + [entries, filtering] + ); + + const displayRows = useMemo( + () => + treeEnabled + ? RepositoryFileTree.rows(treeNodes, collapsed, matches) + : entries.map((entry) => ({ + kind: "file", + id: entry.id, + entry, + depth: 0, + parentId: null, + })), + [treeEnabled, treeNodes, collapsed, matches, entries] + ); + + const visibleFileIds = useMemo( + () => + displayRows.filter((node) => node.kind === "file").map((node) => node.id), + [displayRows] + ); + const renderer = useGpuixRequired(); const viewport = useRef(null); const container = useRef(null); @@ -351,7 +585,7 @@ export function RepositoryList({ const rowHeight = itemHeight ?? (entries[0]?.detail ? revisionRowHeight : fileRowHeight); - const windowed = entries.length > windowThreshold; + const windowed = displayRows.length > windowThreshold; const window = useWindowSize(); const windowSize = Math.max( @@ -369,12 +603,12 @@ export function RepositoryList({ ); const start = windowed - ? Math.min(windowStart, Math.max(0, entries.length - windowSize)) + ? Math.min(windowStart, Math.max(0, displayRows.length - windowSize)) : 0; const end = windowed - ? Math.min(entries.length, start + windowSize) - : entries.length; + ? Math.min(displayRows.length, start + windowSize) + : displayRows.length; const pendingFocus = useRef<{ id: string; y: number } | null>(null); const backwardTab = useRef(false); @@ -415,19 +649,52 @@ export function RepositoryList({ }, [renderer]); const includedIds = useMemo(() => new Set(inclusion?.ids), [inclusion?.ids]); + const partialIds = useMemo( + () => new Set(inclusion?.partialIds), + [inclusion?.partialIds] + ); + const selectedIds = useMemo( () => new Set(state.selectedIds), [state.selectedIds] ); - const [focusId, setFocusId] = useState(state.selectedId ?? entries[0]?.id); + const [focusId, setFocusId] = useState( + state.selectedId ?? displayRows[0]?.id + ); - const tabbableId = entries.some((entry) => entry.id === focusId) + const tabbableId = displayRows.some((entry) => entry.id === focusId) ? focusId - : (entries.find((entry) => entry.id === state.selectedId)?.id ?? - entries[0]?.id); + : (displayRows.find((entry) => entry.id === state.selectedId)?.id ?? + displayRows[0]?.id); + + const selectedPath = sourceEntries.find( + (entry) => entry.id === state.selectedId + ); + + const pathToReveal = selectedPath + ? RepositoryFileTree.path(selectedPath) + : null; - useEffect(() => { + const pendingReveal = useRef(null); + const previousLayout = useRef(treeEnabled); + const treeOwner = fileTree?.owner ?? ""; + + useLayoutEffect(() => { + if (treeEnabled && pathToReveal) + reveal(pathToReveal, treeOwner, previousLayout.current !== treeEnabled); + + if (treeAvailable) pendingReveal.current = state.selectedId; + }, [ + treeEnabled, + pathToReveal, + state.selectedId, + reveal, + treeAvailable, + treeOwner, + ]); + + useLayoutEffect(() => { if (viewport.current) renderer.scrollTo?.(viewport.current.id, 0, initialScroll.current); }, [renderer]); @@ -463,35 +730,57 @@ export function RepositoryList({ scrollCallback.current(renderer.getScrollOffset?.(node.id)?.[1] ?? 0); }, scrollSaveDelayMs); - if (anchor && last >= entries.length - 3) onEndReached?.(); + if (anchor && last >= displayRows.length - 3) onEndReached?.(); }; - const revealRow = (id: string, previousY?: number): void => { - const node = viewport.current; + const revealRow = useCallback( + (id: string, previousY?: number): void => { + const node = viewport.current; - const box = - container.current && renderer.getElementBounds?.(container.current.id); + const box = + container.current && renderer.getElementBounds?.(container.current.id); - const index = entries.findIndex((entry) => entry.id === id); + const index = displayRows.findIndex((entry) => entry.id === id); - if (!node || !box || index < 0) return; + if (!node || !box || index < 0) return; - const top = -(previousY ?? renderer.getScrollOffset?.(node.id)?.[1] ?? 0); - const height = renderer.getListScrollTop?.(node.id)?.[2] ?? box.height; - const targetTop = index * rowHeight; + const top = -(previousY ?? renderer.getScrollOffset?.(node.id)?.[1] ?? 0); + const height = renderer.getListScrollTop?.(node.id)?.[2] ?? box.height; + const targetTop = index * rowHeight; - const nextTop = - targetTop < top - ? targetTop - : Math.max(top, targetTop + rowHeight - height); + const nextTop = + targetTop < top + ? targetTop + : Math.max(top, targetTop + rowHeight - height); - const anchor = Math.floor(nextTop / rowHeight); + const anchor = Math.floor(nextTop / rowHeight); - // Override native focus's top anchor with the smallest scroll that reveals the row. - renderer.scrollToItem?.(node.id, anchor, nextTop - anchor * rowHeight); - }; + // Override native focus's top anchor with the smallest scroll that reveals the row. + renderer.scrollToItem?.(node.id, anchor, nextTop - anchor * rowHeight); + }, + [displayRows, renderer, rowHeight] + ); useLayoutEffect(() => { + if (pendingReveal.current) { + const index = displayRows.findIndex( + (node) => node.id === pendingReveal.current + ); + + if (index >= 0) { + revealRow(pendingReveal.current); + + if (windowed) setWindowStart(Math.max(0, index - windowChunkSize)); + + pendingReveal.current = null; + } + } + + if (previousLayout.current !== treeEnabled) { + previousLayout.current = treeEnabled; + setFocusId(state.selectedId ?? displayRows[0]?.id); + } + const pending = pendingFocus.current; const row = pending && rows.current.get(pending.id); @@ -500,7 +789,16 @@ export function RepositoryList({ renderer.focusElement?.(row.id); revealRow(pending.id, pending.y); } - }); + }, [ + displayRows, + end, + renderer, + revealRow, + start, + state.selectedId, + treeEnabled, + windowed, + ]); const focusRow = (id: string): void => { const row = rows.current.get(id); @@ -510,7 +808,7 @@ export function RepositoryList({ : 0; if (!row) { - const index = entries.findIndex((entry) => entry.id === id); + const index = displayRows.findIndex((entry) => entry.id === id); if (!windowed || index < 0) return; @@ -525,7 +823,9 @@ export function RepositoryList({ }; const select = (id: string, mode?: "toggle" | "range" | "context"): void => { - onSelect(id, mode); + if (displayRows.find((node) => node.id === id)?.kind === "file") + onSelect(id, mode, treeEnabled ? visibleFileIds : undefined); + setFocusId(id); focusRow(id); }; @@ -542,6 +842,54 @@ export function RepositoryList({ return; const key = event.key ?? ""; + const node = displayRows.find((row) => row.id === id); + + if (treeEnabled && node) { + const expanded = + node.kind === "folder" && + (filtering || !node.paths.some((path) => collapsed.has(path))); + + if (key === "left") { + if (node.kind === "folder" && expanded && !filtering) + toggle(node.paths); + else if (node.parentId) select(node.parentId); + + return; + } + + if (key === "right") { + if (node.kind === "folder") { + if (!expanded) toggle(node.paths); + else { + const child = displayRows[displayRows.indexOf(node) + 1]; + + if (child?.parentId === node.id) select(child.id); + } + } + + return; + } + + if (node.kind === "folder" && (key === "enter" || key === "space")) { + if ( + key === "space" && + inclusion && + fileTree?.onInclude && + !inclusion.disabled + ) + fileTree.onInclude( + node.fileIds, + RepositoryFileTree.inclusion( + node.fileIds, + includedIds, + partialIds + ) !== true + ); + else if (!filtering) toggle(node.paths); + + return; + } + } if (key === "enter" || key === "space") { if ( @@ -568,7 +916,7 @@ export function RepositoryList({ : 0; const next = RepositoryWorkspaceModel.destination( - entries, + displayRows, id, key, Math.max(1, Math.floor(height / rowHeight) - 1) @@ -582,7 +930,15 @@ export function RepositoryList({ ref={container} testId={`${testId}-viewport`} style={{ ...styles.viewport, paddingBottom: spacing[1.5] }} - role={inclusion ? "grid" : "listbox"} + role={ + treeEnabled + ? inclusion + ? "treegrid" + : "tree" + : inclusion + ? "grid" + : "listbox" + } aria-label={label} aria-multiselectable={!!inclusion || multiselect} aria-description={entries.length === 0 ? emptyMessage : undefined} @@ -600,11 +956,13 @@ export function RepositoryList({ role="presentation" {...(windowed ? { - itemCount: entries.length, + itemCount: displayRows.length, estimatedItemHeight: rowHeight, windowStart: start, } - : { estimatedItemHeight: entries.length ? rowHeight : undefined })} + : { + estimatedItemHeight: displayRows.length ? rowHeight : undefined, + })} overdraw={rowHeight * 2} onVisibleRange={saveScroll} style={styles.viewport} @@ -620,93 +978,133 @@ export function RepositoryList({ )}
) : null} - {entries.slice(start, end).map((entry) => ( -
- { - const ids = - state.selectedIds ?? - (state.selectedId ? [state.selectedId] : []); - - if (ids.includes(id)) { - const selection = new Set(ids); - - return entries.filter((item) => - selection.has(item.id) - ); - } - - select(id, "context"); - - return [entry]; - }, - } - : undefined - } - selected={ - (inclusion || multiselect) && state.selectedIds - ? selectedIds.has(entry.id) - : state.selectedId === entry.id - } - included={ - inclusion?.partialIds?.includes(entry.id) - ? "indeterminate" - : includedIds.has(entry.id) - } - inclusionDisabled={inclusion?.disabled} - onInclude={ - inclusion - ? (included) => inclusion.onChange(entry.id, included) - : undefined - } - tabbable={ - tabbableId === entry.id || - (entry.id === entries[start]?.id && - !entries - .slice(start, end) - .some((item) => item.id === tabbableId)) - } - rowRef={(node) => { - if (node) rows.current.set(entry.id, node); - else rows.current.delete(entry.id); - }} - onSelect={(event) => - select( - entry.id, - inclusion || multiselect - ? event.modifiers?.shift - ? "range" - : event.modifiers?.cmd - ? "toggle" - : undefined - : undefined - ) - } - onReveal={() => { - setFocusId(entry.id); - revealRow(entry.id); + {displayRows.slice(start, end).map((node) => { + const tabbable = + tabbableId === node.id || + (node.id === displayRows[start]?.id && + !displayRows + .slice(start, end) + .some((item) => item.id === tabbableId)); + + const rowRef = (element: PublicInstance | null): void => { + if (element) rows.current.set(node.id, element); + else rows.current.delete(node.id); + }; + + const onReveal = (): void => { + setFocusId(node.id); + revealRow(node.id); + }; + + const entry = node.kind === "file" ? node.entry : null; + + return ( +
navigate(entry.id, event)} > - {renderEntry?.(entry)} - -
- ))} + {node.kind === "folder" ? ( + collapsed.has(path)) + } + tabbable={tabbable} + rowRef={rowRef} + onReveal={onReveal} + onToggle={() => { + select(node.id); + + if (!filtering) toggle(node.paths); + }} + onKeyDown={(event) => navigate(node.id, event)} + included={RepositoryFileTree.inclusion( + node.fileIds, + includedIds, + partialIds + )} + disabled={inclusion?.disabled} + onInclude={ + inclusion && fileTree?.onInclude + ? (included) => + fileTree.onInclude?.(node.fileIds, included) + : undefined + } + /> + ) : entry ? ( + { + const ids = + state.selectedIds ?? + (state.selectedId ? [state.selectedId] : []); + + if (ids.includes(id)) { + const selection = new Set(ids); + + return entries.filter((item) => + selection.has(item.id) + ); + } + + select(id, "context"); + + return [entry]; + }, + } + : undefined + } + selected={ + (inclusion || multiselect) && state.selectedIds + ? selectedIds.has(entry.id) + : state.selectedId === entry.id + } + included={ + inclusion?.partialIds?.includes(entry.id) + ? "indeterminate" + : includedIds.has(entry.id) + } + inclusionDisabled={inclusion?.disabled} + onInclude={ + inclusion + ? (included) => inclusion.onChange(entry.id, included) + : undefined + } + tabbable={tabbable} + rowRef={rowRef} + onSelect={(event) => + select( + entry.id, + inclusion || multiselect + ? event.modifiers?.shift + ? "range" + : event.modifiers?.cmd + ? "toggle" + : undefined + : undefined + ) + } + onReveal={onReveal} + onKeyDown={(event) => navigate(entry.id, event)} + > + {renderEntry?.(entry)} + + ) : null} +
+ ); + })} + onAction({ type: "include-files", ids, included }), + } + : undefined + } onContextMenu={ view === "history" && !state.preview ? onMenu : undefined } @@ -542,15 +558,15 @@ function RepositoryNavigationContent({ } : undefined } - onSelect={(id, mode) => + onSelect={(id, mode, visibleIds) => onAction({ type: "select", view, id, mode, - visibleIds: filtering - ? entries.map((entry) => entry.id) - : undefined, + visibleIds: + visibleIds ?? + (filtering ? entries.map((entry) => entry.id) : undefined), }) } onScroll={(y) => onAction({ type: "scroll", view, y })} @@ -604,6 +620,9 @@ function RepositoryNavigationContent({ ) : null}
+ {view === "changes" ? ( + + ) : null} {state.repository || state.repositoryPath ? (
; + files: WorkspaceEntry[]; + fileIds: string[]; +} + +const compareNames = (left: string, right: string): number => + left.localeCompare(right, undefined, { numeric: true }) || + (left < right ? -1 : left > right ? 1 : 0); + +/** Pure Git-path grouping and visibility; never reads the local filesystem. */ +export class RepositoryFileTree { + static path(entry: WorkspaceEntry): string { + return entry.change?.path ?? entry.commitFile?.path ?? entry.title; + } + + static build( + entries: readonly WorkspaceEntry[] + ): readonly RepositoryFileNode[] { + const root: Directory = { + path: "", + name: "", + folders: new Map(), + files: [], + fileIds: [], + }; + + for (const entry of entries) { + const parts = RepositoryFileTree.path(entry).split("/"); + let directory = root; + + for (const part of parts.slice(0, -1)) { + let child = directory.folders.get(part); + + if (!child) { + child = { + path: directory.path ? `${directory.path}/${part}` : part, + name: part, + folders: new Map(), + files: [], + fileIds: [], + }; + directory.folders.set(part, child); + } + + child.fileIds.push(entry.id); + directory = child; + } + + directory.files.push(entry); + } + + return RepositoryFileTree.children(root); + } + + private static children(directory: Directory): readonly RepositoryFileNode[] { + const folders = [...directory.folders.values()] + .sort((left, right) => compareNames(left.name, right.name)) + .map((folder): RepositoryFileFolder => { + const names = [folder.name]; + const paths = [folder.path]; + + while (!folder.files.length && folder.folders.size === 1) { + const child = folder.folders.values().next().value; + + if (!child) break; + + folder = child; + names.push(folder.name); + paths.push(folder.path); + } + + return { + kind: "folder", + id: `folder:${folder.path}/`, + path: folder.path, + name: names.join("/"), + paths, + fileIds: folder.fileIds, + children: RepositoryFileTree.children(folder), + }; + }); + + const files = directory.files + .toSorted((left, right) => + compareNames( + RepositoryFileTree.path(left), + RepositoryFileTree.path(right) + ) + ) + .map((entry): RepositoryFileNode => ({ + kind: "file", + id: entry.id, + entry, + })); + + return [...folders, ...files]; + } + + static rows( + nodes: readonly RepositoryFileNode[], + collapsed: ReadonlySet, + matches?: ReadonlySet + ): readonly RepositoryFileTreeRow[] { + const rows: RepositoryFileTreeRow[] = []; + + const visit = ( + children: readonly RepositoryFileNode[], + depth: number, + parentId: string | null + ): void => { + for (const node of children) { + if ( + matches && + !(node.kind === "file" + ? matches.has(node.id) + : node.fileIds.some((id) => matches.has(id))) + ) + continue; + + rows.push({ ...node, depth, parentId }); + + if ( + node.kind === "folder" && + (matches || !node.paths.some((path) => collapsed.has(path))) + ) + visit(node.children, depth + 1, node.id); + } + }; + + visit(nodes, 0, null); + + return rows; + } + + static inclusion( + ids: readonly string[], + included: ReadonlySet, + partial: ReadonlySet + ): boolean | "indeterminate" { + let count = 0; + let hasPartial = false; + + for (const id of ids) { + if (included.has(id)) count++; + + if (partial.has(id)) hasPartial = true; + } + + if (!count) return false; + + return count === ids.length && !hasPartial ? true : "indeterminate"; + } +} diff --git a/apps/gpuix/src/feature/repository-workspace/lib/repository-workspace-model.ts b/apps/gpuix/src/feature/repository-workspace/lib/repository-workspace-model.ts index ffc17e88..d232aa47 100644 --- a/apps/gpuix/src/feature/repository-workspace/lib/repository-workspace-model.ts +++ b/apps/gpuix/src/feature/repository-workspace/lib/repository-workspace-model.ts @@ -1510,10 +1510,16 @@ export class RepositoryWorkspaceModel { if (action.type === "select") { if (action.view === "changes" || action.view === "history") { - const visible = action.visibleIds ? new Set(action.visibleIds) : null; + const byId = new Map( + state.entries[action.view].map((entry) => [entry.id, entry]) + ); + + const entries = action.visibleIds + ? [...new Set(action.visibleIds)].flatMap((id) => { + const entry = byId.get(id); - const entries = visible - ? state.entries[action.view].filter((entry) => visible.has(entry.id)) + return entry ? [entry] : []; + }) : state.entries[action.view]; if (!entries.some((entry) => entry.id === action.id)) return state; @@ -1632,7 +1638,7 @@ export class RepositoryWorkspaceModel { } static destination( - entries: readonly WorkspaceEntry[], + entries: readonly { readonly id: string }[], id: string, key: string, pageSize: number diff --git a/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab-icon.tsx b/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab-icon.tsx index eb5f7212..f284286c 100644 --- a/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab-icon.tsx +++ b/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab-icon.tsx @@ -4,6 +4,11 @@ import { StyleSheet } from "@1git/gpuix-stylesheet"; // Phosphor Icons 2.1.10, Light weight. See ../../../../LICENSES/Phosphor-Icons.txt. const sources = { + // App-drawn file-navigation glyphs share the existing light stroke. + list: '', + tree: '', + folder: + '', // App-drawn layers matches the Spaces icon picker. layers: '', diff --git a/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab.tsx b/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab.tsx index b4b80903..f187479f 100644 --- a/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab.tsx +++ b/apps/gpuix/src/feature/workspace-tabs/components/workspace-tab.tsx @@ -1,3 +1,4 @@ +import { OverflowingLabel } from "../../../fixture/overflowing-label"; import { MachineContext } from "../../machines/machine-context"; import { createThemedStyles, useAppTheme } from "../../../styles/theme"; import { @@ -9,20 +10,8 @@ import { import { StyleSheet } from "@1git/gpuix-stylesheet"; import { Button } from "@1git/gpuix-ui/button"; import { Text } from "@1git/gpuix-ui/text"; -import { - motion, - useGpuixRequired, - type EventPayload, - type PublicInstance, -} from "@gpuix/react"; -import { - useContext, - useLayoutEffect, - useRef, - useState, - type Ref, - type ReactNode, -} from "react"; +import { motion, type EventPayload, type PublicInstance } from "@gpuix/react"; +import { useContext, useRef, useState, type Ref, type ReactNode } from "react"; import { workspaceLayout as layout } from "../../../styles/workspace-layout"; import { MachineLocation } from "../../machines/machine-location"; @@ -81,16 +70,6 @@ export function WorkspaceTabSelection({ ); } -// Match Electron's OverflowingLabel: pause, reveal the suffix, and return on leave. -const marquee = { - hoverDelay: 0.32, - minDuration: 0.7, - maxDuration: 2.2, - pixelsPerSecond: 48, - returnDuration: 0.22, - overflowTolerance: 1, -} as const; - const themedStyles = createThemedStyles(({ colors: appColors, appearance }) => { const activeSurface = connectedTabSurface(width, appColors.editorSurface); @@ -149,22 +128,6 @@ const themedStyles = createThemedStyles(({ colors: appColors, appearance }) => { borderRadius: cornerRadius.md, cursor: "pointer", }, - label: { - display: "flex", - flexDirection: "row", - alignItems: "center", - flexShrink: 1, - minWidth: 0, - overflow: "hidden", - whiteSpace: "nowrap", - }, - labelTrack: { - position: "relative", - display: "flex", - flexDirection: "row", - alignItems: "center", - flexShrink: 0, - }, name: (active: boolean) => ({ flexShrink: 1, minWidth: 0, @@ -540,31 +503,6 @@ function WorkspaceTabLabel({ const { tokens: appTokens } = theme; const { styles } = themedStyles.get(theme); - const renderer = useGpuixRequired(); - const viewport = useRef(null); - const track = useRef(null); - const [overflow, setOverflow] = useState(0); - - useLayoutEffect(() => { - if (!reveal || !viewport.current || !track.current) return; - - const viewportBounds = renderer.getElementBounds?.(viewport.current.id); - const trackBounds = renderer.getElementBounds?.(track.current.id); - - if (!viewportBounds || !trackBounds) return; - - const difference = trackBounds.width - viewportBounds.width; - - setOverflow( - difference > marquee.overflowTolerance ? Math.ceil(difference) : 0 - ); - }, [renderer, reveal, tab.name, tab.branch]); - - const duration = Math.min( - marquee.maxDuration, - Math.max(marquee.minDuration, overflow / marquee.pixelsPerSecond) - ); - return ( <> -
- + + {tab.name} + {machineName ? ` · ${machineName}` : ""} + + {tab.branch ? ( - {tab.name} - {machineName ? ` · ${machineName}` : ""} - - {tab.branch ? ( - {` / ${tab.branch}`} - ) : null} - -
+ style={styles.branch} + >{` / ${tab.branch}`} + ) : null} + ); } diff --git a/apps/gpuix/src/feature/workspace-tabs/hooks/use-workspace-session.ts b/apps/gpuix/src/feature/workspace-tabs/hooks/use-workspace-session.ts index e1023bf5..17734dfb 100644 --- a/apps/gpuix/src/feature/workspace-tabs/hooks/use-workspace-session.ts +++ b/apps/gpuix/src/feature/workspace-tabs/hooks/use-workspace-session.ts @@ -1,4 +1,5 @@ import type { TourTips } from "../../guided-tour/guided-tour"; +import type { RepositoryFileLayout } from "../../repository-workspace/lib/repository-file-tree"; import type { AppTypography } from "../../appearance-settings/app-typography"; import { useCallback, useLayoutEffect, useRef, useState } from "react"; import { @@ -31,7 +32,8 @@ export function useWorkspaceSession( smoothTransitions?: boolean, showScrollbars?: boolean, typography?: AppTypography, - tips?: TourTips + tips?: TourTips, + fileListLayout?: RepositoryFileLayout ) { const content = state.preview || !save @@ -50,7 +52,8 @@ export function useWorkspaceSession( smoothTransitions, showScrollbars, typography, - tips + tips, + fileListLayout ) ); diff --git a/apps/gpuix/src/feature/workspace-tabs/lib/workspace-session.ts b/apps/gpuix/src/feature/workspace-tabs/lib/workspace-session.ts index af1c2a9e..9dc1138a 100644 --- a/apps/gpuix/src/feature/workspace-tabs/lib/workspace-session.ts +++ b/apps/gpuix/src/feature/workspace-tabs/lib/workspace-session.ts @@ -1,4 +1,5 @@ import { GuidedTour, type TourTips } from "../../guided-tour/guided-tour"; +import type { RepositoryFileLayout } from "../../repository-workspace/lib/repository-file-tree"; import { TypographyPreferences, type AppTypography, @@ -69,6 +70,7 @@ export interface WorkspaceSessionDocument { readonly repositoryDiscovery?: RepositoryDiscoveryPreferences; readonly repositoryLibrary?: RepositoryLibraryPreferences; readonly diffSettings?: DiffSettings; + readonly fileListLayout?: RepositoryFileLayout; readonly appearance?: AppAppearancePreference; readonly commitTypeBadges?: boolean; readonly smoothTransitions?: boolean; @@ -257,6 +259,9 @@ export class WorkspaceSession { ...(typeof value.showScrollbars === "boolean" ? { showScrollbars: value.showScrollbars } : {}), + ...(value.fileListLayout === "list" || value.fileListLayout === "tree" + ? { fileListLayout: value.fileListLayout } + : {}), ...(typeof value.smoothTransitions === "boolean" ? { smoothTransitions: value.smoothTransitions } : {}), @@ -317,10 +322,12 @@ export class WorkspaceSession { smoothTransitions?: boolean, showScrollbars?: boolean, typography?: AppTypography, - tips?: TourTips + tips?: TourTips, + fileListLayout?: RepositoryFileLayout ): WorkspaceSessionDocument { return { version: 1, + ...(fileListLayout ? { fileListLayout } : {}), ...(state.spaces ? { spaces: state.spaces.map((space) => diff --git a/apps/gpuix/src/fixture/overflowing-label.tsx b/apps/gpuix/src/fixture/overflowing-label.tsx new file mode 100644 index 00000000..79045289 --- /dev/null +++ b/apps/gpuix/src/fixture/overflowing-label.tsx @@ -0,0 +1,104 @@ +import { + motion, + useGpuixRequired, + useWindowSize, + type PublicInstance, + type StyleDesc, +} from "@gpuix/react"; +import { StyleSheet } from "@1git/gpuix-stylesheet"; +import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { useAppTheme } from "../styles/theme"; + +const marquee = { + hoverDelay: 0.32, + minDuration: 0.7, + maxDuration: 2.2, + pixelsPerSecond: 48, + returnDuration: 0.22, + overflowTolerance: 1, +} as const; + +/** Clips a label and reveals only its overflow on hover or keyboard focus, using native motion. */ +export function OverflowingLabel({ + children, + reveal, + contentKey, + testId, + style, + gap, +}: { + children: ReactNode; + reveal: boolean; + contentKey: string; + testId: string; + style?: StyleDesc; + gap?: number; +}) { + const renderer = useGpuixRequired(); + const window = useWindowSize(); + const { typography } = useAppTheme(); + const viewport = useRef(null); + const track = useRef(null); + const [overflow, setOverflow] = useState(0); + + useLayoutEffect(() => { + if (!reveal || !viewport.current || !track.current) return; + + const viewportBounds = renderer.getElementBounds?.(viewport.current.id); + const trackBounds = renderer.getElementBounds?.(track.current.id); + + if (!viewportBounds || !trackBounds) return; + + const difference = trackBounds.width - viewportBounds.width; + + setOverflow( + difference > marquee.overflowTolerance ? Math.ceil(difference) : 0 + ); + }, [renderer, reveal, contentKey, window.width, typography]); + + const duration = Math.min( + marquee.maxDuration, + Math.max(marquee.minDuration, overflow / marquee.pixelsPerSecond) + ); + + return ( +
+ + {children} + +
+ ); +} diff --git a/apps/gpuix/tests/repository-comparison/repository-comparison.component.test.tsx b/apps/gpuix/tests/repository-comparison/repository-comparison.component.test.tsx index c2c56e04..91ebcc65 100644 --- a/apps/gpuix/tests/repository-comparison/repository-comparison.component.test.tsx +++ b/apps/gpuix/tests/repository-comparison/repository-comparison.component.test.tsx @@ -7,6 +7,8 @@ import { afterEach, beforeEach, expect, it, mock } from "bun:test"; import { createTestRoot, type TestRoot } from "@gpuix/react/testing"; import type { CommitInfo } from "@1git/git"; import { RepositoryComparison } from "../../src/feature/repository-comparison/repository-comparison"; +import { RepositoryFileViewProvider } from "../../src/feature/repository-workspace/components/repository-file-view"; +import type { RepositoryFileLayout } from "../../src/feature/repository-workspace/lib/repository-file-tree"; import { RepositoryComparison as Reader, type ComparisonRequest, @@ -105,7 +107,10 @@ const click = (id: string): void => { root.renderer.flush(); }; -const show = (reader: typeof Reader.read = readComparison): void => { +const show = ( + reader: typeof Reader.read = readComparison, + layout: RepositoryFileLayout = "list" +): void => { root.render(
{ backgroundColor: "#ffffff", }} > - + + +
); }; @@ -135,6 +142,39 @@ const choose = async (branch: string): Promise => { await settle(); }; +it("uses read-only file trees for comparisons and keeps incoming commits flat", async () => { + show( + async (_root, base, head) => ({ + ok: true, + value: { + base, + head, + mergeBase: "ancestor", + files: [ + { ...file, path: "src/a.ts" }, + { ...file, path: "src/b.ts" }, + ], + }, + }), + "tree" + ); + await choose("feature"); + expect( + root.renderer.findByTestId("comparison-files-viewport")?.customProps?.role + ).toBe("tree"); + expect( + root.renderer.findByTestId("comparison-commits-viewport")?.customProps?.role + ).toBe("listbox"); + expect(root.renderer.findByTestId("include-folder:src/")).toBeUndefined(); + click("entry-folder:src/"); + expect(root.renderer.findByTestId("entry-src/a.ts")).toBeUndefined(); + click("comparison-file-layout"); + expect(root.renderer.findByTestId("entry-src/a.ts")).toBeDefined(); + expect( + root.renderer.findByTestId("comparison-files-viewport")?.customProps?.role + ).toBe("listbox"); +}); + it("retains the sidebar and shows vector artwork while waiting for a branch", async () => { show(); await settle(); diff --git a/apps/gpuix/tests/repository-graph/view.component.test.tsx b/apps/gpuix/tests/repository-graph/view.component.test.tsx index 969398d7..1f799f8d 100644 --- a/apps/gpuix/tests/repository-graph/view.component.test.tsx +++ b/apps/gpuix/tests/repository-graph/view.component.test.tsx @@ -189,6 +189,24 @@ async function open(): Promise { await settle(); } +it("switches Graph file navigation to a read-only tree without changing commit navigation", async () => { + show(); + await open(); + click(`entry-${commits[0]!.oid}`); + await settle(); + click("graph-file-layout"); + expect(element("graph-files-viewport").customProps?.role).toBe("tree"); + expect(element("graph-commits-viewport").customProps?.role).toBe("listbox"); + expect(root.renderer.findByTestId("include-folder:src/")).toBeUndefined(); + click("entry-folder:src/"); + expect(root.renderer.findByTestId("entry-src/file-0.ts")).toBeUndefined(); + expect(element("diff-header")).toBeDefined(); + click("view-changes"); + expect(element("changes-file-layout").customProps?.["aria-label"]).toBe( + "Switch to list view" + ); +}); + for (const appearance of ["light", "dark"] as const) { it(`opens a separate ${appearance} Graph rail tab and inspects commit files without replacing History`, async () => { show(appearance); diff --git a/apps/gpuix/tests/repository-workspace/file-label.component.test.tsx b/apps/gpuix/tests/repository-workspace/file-label.component.test.tsx new file mode 100644 index 00000000..9583dcbb --- /dev/null +++ b/apps/gpuix/tests/repository-workspace/file-label.component.test.tsx @@ -0,0 +1,182 @@ +/** + * Covers: delayed overflow-only file/folder reveal, reset, keyboard focus and stationary row controls. + * Real: file list, shared tab animation, native text measurement, clipping and motion. + * Doubles: Git records and offscreen-window focus notifications. + * Requires: patched GPUIX renderer and macOS graphics. + */ +import { afterEach, beforeEach, expect, it } from "bun:test"; +import { flushSync, handleGpuixEvent } from "@gpuix/react"; +import { createTestRoot, type TestRoot } from "@gpuix/react/testing"; +import { RepositoryList } from "../../src/feature/repository-workspace/components/repository-list"; +import { RepositoryFileViewProvider } from "../../src/feature/repository-workspace/components/repository-file-view"; +import type { RepositoryFileLayout } from "../../src/feature/repository-workspace/lib/repository-file-tree"; +import type { WorkspaceEntry } from "../../src/feature/repository-workspace/types"; + +const directory = "src/a-long-directory-name-with-an-important-folder-suffix"; +const path = `${directory}/pi-timeline.integration.with-an-important-filename-suffix.test.ts`; + +const entries: readonly WorkspaceEntry[] = [ + path, + `${directory}/a.ts`, + "short.ts", +].map((title) => ({ id: title, title, status: "M" })); + +const noop = (): void => {}; +let root: TestRoot; + +beforeEach(() => { + root = createTestRoot({ width: 420, height: 300 }); + root.renderer.clockPause(); +}); +afterEach(() => root.unmount()); + +function show(layout: RepositoryFileLayout = "tree"): void { + root.render( + +
+ entry.id), onChange: noop }} + onSelect={noop} + onScroll={noop} + /> +
+
+ ); +} + +function element(testId: string) { + const node = root.renderer.findByTestId(testId); + + if (!node) throw new Error(`Missing ${testId}`); + + return node; +} + +function bounds(testId: string) { + const box = root.renderer.getElementBounds(element(testId).id); + + if (!box) throw new Error(`No bounds for ${testId}`); + + return box; +} + +function advance(milliseconds: number): void { + root.renderer.flush(); + root.renderer.clockFastForward(milliseconds); + root.renderer.flush(); + root.renderer.dispatchNativeEvents(); + root.renderer.flush(); +} + +function hover(id: string): void { + const box = bounds(id); + + root.renderer.nativeSimulateMouseMove(box.x + 10, box.y + box.height / 2); + root.renderer.flush(); +} + +function leave(): void { + root.renderer.nativeSimulateMouseMove(400, 270); + advance(250); +} + +for (const layout of ["tree", "list"] as const) { + it(`reveals truncated ${layout} file labels after the tab delay without tooltips or moving row controls`, () => { + show(layout); + const viewport = bounds(`identity-${path}`); + const start = bounds(`filename-${path}`).x; + const icon = bounds(`status-${path}`); + const checkbox = bounds(`include-${path}`); + + expect(bounds(`filename-${path}`).width).toBeGreaterThan(viewport.width); + hover(`entry-${path}`); + advance(300); + expect(bounds(`filename-${path}`).x).toBe(start); + advance(400); + expect(bounds(`filename-${path}`).x).toBeLessThan(start); + advance(2300); + const suffix = bounds( + `${layout === "tree" ? "filename" : "directory"}-${path}` + ); + + expect( + Math.abs(suffix.x + suffix.width - viewport.x - viewport.width) + ).toBeLessThanOrEqual(1); + expect(bounds(`status-${path}`)).toEqual(icon); + expect(bounds(`include-${path}`)).toEqual(checkbox); + expect( + root.renderer + .findByType("div") + .some((node) => node.customProps?.role === "tooltip") + ).toBe(false); + leave(); + expect(bounds(`filename-${path}`).x).toBe(start); + }); +} + +it("slides truncated folder names while keeping counts and checkboxes fixed", () => { + show(); + const id = `folder:${directory}/`; + const viewport = bounds(`folder-label-${id}`); + const start = bounds(`folder-name-${id}`).x; + const count = bounds(`folder-count-${id}`); + const checkbox = bounds(`include-${id}`); + + hover(`entry-${id}`); + advance(3000); + const name = bounds(`folder-name-${id}`); + + expect(name.x).toBeLessThan(start); + expect( + Math.abs(name.x + name.width - viewport.x - viewport.width) + ).toBeLessThanOrEqual(1); + expect(bounds(`folder-count-${id}`)).toEqual(count); + expect(bounds(`include-${id}`)).toEqual(checkbox); + expect( + root.renderer + .findByType("div") + .some((node) => node.customProps?.role === "tooltip") + ).toBe(false); + leave(); + expect(bounds(`folder-name-${id}`).x).toBe(start); +}); + +it("leaves fitting and briefly hovered names still, and reveals overflow with keyboard focus", () => { + show(); + const shortStart = bounds("filename-short.ts").x; + const longStart = bounds(`filename-${path}`).x; + + hover("entry-short.ts"); + advance(3000); + expect(bounds("filename-short.ts").x).toBe(shortStart); + hover(`entry-${path}`); + advance(100); + leave(); + advance(3000); + expect(bounds(`filename-${path}`).x).toBe(longStart); + const row = element(`entry-${path}`); + + flushSync(() => + handleGpuixEvent({ elementId: row.id, eventType: "focus" }, root.renderer) + ); + advance(3000); + expect(bounds(`filename-${path}`).x).toBeLessThan(longStart); + expect(row.customProps?.["aria-label"]).toContain(path); + flushSync(() => + handleGpuixEvent({ elementId: row.id, eventType: "blur" }, root.renderer) + ); + advance(250); + expect(bounds(`filename-${path}`).x).toBe(longStart); +}); diff --git a/apps/gpuix/tests/repository-workspace/file-tree.component.test.tsx b/apps/gpuix/tests/repository-workspace/file-tree.component.test.tsx new file mode 100644 index 00000000..af7e5a93 --- /dev/null +++ b/apps/gpuix/tests/repository-workspace/file-tree.component.test.tsx @@ -0,0 +1,369 @@ +/** + * Covers: native folder disclosure, keyboard navigation, inclusion, filters, scope retention and virtualization. + * Real: workspace/tree models, file rows, native GPUIX layout, focus and accessibility props. + * Doubles: Git records; no filesystem mutations. + * Requires: patched GPUIX native renderer and macOS graphics. + */ +import { afterEach, beforeEach, expect, it } from "bun:test"; +import { useReducer } from "react"; +import { createTestRoot, type TestRoot } from "@gpuix/react/testing"; +import { App } from "../../src/app"; +import { RepositoryList } from "../../src/feature/repository-workspace/components/repository-list"; +import { + RepositoryFileLayoutToggle, + RepositoryFileViewProvider, +} from "../../src/feature/repository-workspace/components/repository-file-view"; +import { + RepositoryWorkspaceModel as Model, + type RepositoryWorkspaceState, +} from "../../src/feature/repository-workspace/lib/repository-workspace-model"; +import type { WorkspaceEntry } from "../../src/feature/repository-workspace/types"; + +const files: readonly WorkspaceEntry[] = [ + "src/a.ts", + "src/ui/b.ts", + "src/ui/c.ts", + "test/a.ts", + "README.md", +].map((path) => ({ id: path, title: path, status: "M" })); + +function Pane({ + scope = "first", + query = "", + entries = files, + disabled = false, + initialSelectedId, +}: { + scope?: string; + query?: string; + entries?: readonly WorkspaceEntry[]; + disabled?: boolean; + initialSelectedId?: string; +}) { + const [state, dispatch] = useReducer( + Model.reduce, + undefined, + (): RepositoryWorkspaceState => ({ + ...Model.create(), + entries: { changes: entries, history: [], stashes: [] }, + includedFileIds: entries.map((entry) => entry.id), + partialFiles: { + "src/ui/c.ts": { expectedPatch: "patch", includedLines: ["line"] }, + }, + views: { + ...Model.create().views, + changes: { + selectedId: initialSelectedId ?? entries[0]?.id ?? null, + scrollY: 0, + }, + }, + }) + ); + + const visible = query + ? entries.filter((entry) => entry.title.includes(query)) + : entries; + + return ( +
+ + + dispatch({ type: "include-files", ids, included }), + }} + inclusion={{ + ids: state.includedFileIds, + partialIds: Object.keys(state.partialFiles ?? {}), + disabled, + onChange: (id, included) => + dispatch({ type: "include-file", id, included }), + }} + onSelect={(id, mode, visibleIds) => + dispatch({ type: "select", view: "changes", id, mode, visibleIds }) + } + onScroll={() => {}} + /> + + {state.views.changes.selectedId} + +
+ ); +} + +let root: TestRoot; + +beforeEach(() => { + root = createTestRoot({ width: 1100, height: 700 }); +}); +afterEach(() => root.unmount()); + +const element = (testId: string) => { + const node = root.renderer.findByTestId(testId); + + if (!node) throw new Error(`Missing ${testId}`); + + return node; +}; + +function click(testId: string): void { + const bounds = root.renderer.getElementBounds(element(testId).id); + + if (!bounds) throw new Error(`Missing bounds for ${testId}`); + + root.renderer.nativeSimulateClick( + bounds.x + bounds.width / 2, + bounds.y + bounds.height / 2 + ); + root.renderer.flush(); +} + +function key(testId: string, value: string): void { + root.renderer.nativeSimulateKeyDown(element(testId).id, value); + root.renderer.flush(); +} + +const checked = (id: string) => element(id).customProps?.["aria-checked"]; +const selected = () => element("selected").customProps?.["aria-label"]; + +it("keeps viewed files independent of disclosure and includes collapsed descendants in one folder action", () => { + root.render( + + + + ); + + expect(element("repository-list-viewport").customProps?.role).toBe( + "treegrid" + ); + const accessibility = Object.values(root.renderer.getA11yTree().nodes ?? {}); + + expect( + accessibility.some( + (node) => + node.aria?.role === "TreeGrid" && node.aria.label === "Changed files" + ) + ).toBe(true); + expect( + accessibility.some( + (node) => + node.aria?.role === "Row" && + node.aria.level === 1 && + node.aria.expanded === true + ) + ).toBe(true); + expect(checked("include-folder:src/")).toBe("mixed"); + expect(root.renderer.findByTestId("directory-src/a.ts")).toBeUndefined(); + const checkbox = root.renderer.getElementBounds( + element("include-src/a.ts").id + ); + + const folderCheckbox = root.renderer.getElementBounds( + element("include-folder:src/").id + ); + + const viewport = root.renderer.getElementBounds( + element("repository-list-viewport").id + ); + + expect(checkbox?.x).toBe(folderCheckbox?.x); + expect((checkbox?.x ?? 0) + (checkbox?.width ?? 0)).toBeGreaterThan( + (viewport?.width ?? 0) - 20 + ); + click("entry-folder:src/"); + expect(root.renderer.findByTestId("entry-src/a.ts")).toBeUndefined(); + expect(selected()).toBe("src/a.ts"); + click("include-folder:src/"); + expect(checked("include-folder:src/")).toBe(true); + click("include-folder:src/"); + expect(checked("include-folder:src/")).toBe(false); + expect(checked("include-README.md")).toBe(true); + key("entry-folder:src/", "right"); + expect(checked("include-src/a.ts")).toBe(false); + expect(selected()).toBe("src/a.ts"); + key("file-layout", "enter"); + expect(element("repository-list-viewport").customProps?.role).toBe("grid"); + expect(element("directory-src/a.ts")).toBeDefined(); + expect(checked("include-src/ui/c.ts")).toBe(false); + click("file-layout"); + expect(checked("include-folder:src/ui/")).toBe(false); +}); + +it("navigates visible folders with arrows, toggles inclusion with Space and preserves the file preview", () => { + root.render( + + + + ); + + key("entry-folder:src/", "right"); + expect(root.renderer.getFocusedElementId()).toBe( + element("entry-folder:src/ui/").id + ); + expect(selected()).toBe("src/a.ts"); + key("entry-folder:src/ui/", "right"); + expect(selected()).toBe("src/ui/b.ts"); + key("entry-src/ui/b.ts", "left"); + expect(root.renderer.getFocusedElementId()).toBe( + element("entry-folder:src/ui/").id + ); + key("entry-folder:src/ui/", "left"); + expect(root.renderer.findByTestId("entry-src/ui/b.ts")).toBeUndefined(); + expect(selected()).toBe("src/ui/b.ts"); + key("entry-folder:src/ui/", "space"); + expect(checked("include-folder:src/ui/")).toBe(true); + key("entry-folder:src/ui/", "down"); + expect(selected()).toBe("src/a.ts"); + key("entry-src/a.ts", "end"); + expect(selected()).toBe("README.md"); +}); + +it("temporarily reveals matches, retains hidden descendant inclusion and restores independent pane expansion", () => { + const render = (scope: string, query = "") => + root.render( + + + + ); + + render("first"); + click("entry-folder:src/ui/"); + expect(root.renderer.findByTestId("entry-src/ui/b.ts")).toBeUndefined(); + render("first", "b.ts"); + expect(element("entry-src/ui/b.ts")).toBeDefined(); + expect(root.renderer.findByTestId("entry-src/ui/c.ts")).toBeUndefined(); + click("include-folder:src/ui/"); + expect(checked("include-folder:src/ui/")).toBe(true); + click("include-folder:src/ui/"); + expect(checked("include-folder:src/ui/")).toBe(false); + render("first"); + expect(root.renderer.findByTestId("entry-src/ui/b.ts")).toBeUndefined(); + render("second"); + expect(element("entry-src/ui/c.ts")).toBeDefined(); + expect(checked("include-src/ui/c.ts")).toBe(false); + render("first"); + expect(root.renderer.findByTestId("entry-src/ui/c.ts")).toBeUndefined(); + click("entry-folder:src/"); + render("second"); + expect(element("entry-src/a.ts")).toBeDefined(); + render("first"); + expect(root.renderer.findByTestId("entry-src/a.ts")).toBeUndefined(); + expect(selected()).toBe("src/a.ts"); +}); + +it("windows large trees and reveals the last file with End", () => { + const entries: WorkspaceEntry[] = Array.from( + { length: 3000 }, + (_, index) => ({ + id: `src/file-${index}.ts`, + title: `src/file-${index}.ts`, + status: "M", + }) + ); + + root.render( + + + + ); + + expect(root.renderer.findByTestId("entry-src/file-2999.ts")).toBeUndefined(); + key("entry-src/file-0.ts", "end"); + expect(element("entry-src/file-2999.ts")).toBeDefined(); + expect(root.renderer.getFocusedElementId()).toBe( + element("entry-src/file-2999.ts").id + ); + expect(selected()).toBe("src/file-2999.ts"); +}); + +it("leaves inclusion unchanged while Git writes disable the checkboxes", () => { + root.render( + + + + ); + click("include-folder:src/"); + expect(checked("include-folder:src/")).toBe("mixed"); +}); + +it("reveals a restored file beyond the initial virtual window", () => { + const entries: WorkspaceEntry[] = Array.from( + { length: 3000 }, + (_, index) => ({ + id: `src/file-${index}.ts`, + title: `src/file-${index}.ts`, + status: "M", + }) + ); + + root.render( + + + + ); + const row = root.renderer.getElementBounds( + element("entry-src/file-2999.ts").id + ); + + const viewport = root.renderer.getElementBounds( + element("repository-list-viewport").id + ); + + expect(row).not.toBeNull(); + expect(row?.y).toBeGreaterThanOrEqual(viewport?.y ?? 0); + expect((row?.y ?? 0) + (row?.height ?? 0)).toBeLessThanOrEqual( + (viewport?.y ?? 0) + (viewport?.height ?? 0) + 1 + ); +}); + +it("shares layout across Changes, History and Stashes while keeping revision lists flat and expansion scoped", () => { + root.render(); + click("changes-file-layout"); + expect(element("repository-list-viewport").customProps?.role).toBe( + "treegrid" + ); + click("view-history"); + click("entry-commit-1"); + expect(element("repository-list-viewport").customProps?.role).toBe("listbox"); + expect(element("revision-files-viewport").customProps?.role).toBe("tree"); + expect(element("revision-file-layout").customProps?.["aria-label"]).toBe( + "Switch to list view" + ); + key("entry-src/app.tsx", "end"); + click("entry-folder:src/styles/"); + expect( + root.renderer.findByTestId("entry-src/styles/tokens.ts") + ).toBeUndefined(); + click("entry-commit-2"); + click("entry-commit-1"); + expect( + root.renderer.findByTestId("entry-src/styles/tokens.ts") + ).toBeUndefined(); + click("view-stashes"); + click("entry-stash-1"); + expect(element("revision-files-viewport").customProps?.role).toBe("tree"); + expect(element("entry-src/styles/tokens.ts")).toBeDefined(); + click("revision-file-layout"); + click("view-changes"); + expect(element("repository-list-viewport").customProps?.role).toBe("grid"); + expect(element("changes-file-layout").customProps?.["aria-label"]).toBe( + "Switch to tree view" + ); +}); diff --git a/apps/gpuix/tests/repository-workspace/file-tree.unit.test.ts b/apps/gpuix/tests/repository-workspace/file-tree.unit.test.ts new file mode 100644 index 00000000..32957b75 --- /dev/null +++ b/apps/gpuix/tests/repository-workspace/file-tree.unit.test.ts @@ -0,0 +1,154 @@ +/** + * Covers: Git-path grouping, compact folders, visibility, commit inclusion and displayed-order ranges. + * Real: file-tree and workspace models. + * Doubles: Git presentation records; no filesystem reads. + * Requires: none. + */ +import { expect, it } from "bun:test"; +import { RepositoryFileTree as Tree } from "../../src/feature/repository-workspace/lib/repository-file-tree"; +import { + RepositoryWorkspaceModel as Workspace, + type RepositoryWorkspaceState, +} from "../../src/feature/repository-workspace/lib/repository-workspace-model"; +import type { WorkspaceEntry } from "../../src/feature/repository-workspace/types"; + +const entry = ( + path: string, + status: WorkspaceEntry["status"] = "M" +): WorkspaceEntry => ({ id: path, title: path, status }); + +it("compacts folder chains, sorts folders before files, and retains original records", () => { + const entries = [ + entry("README.md"), + entry("src/app.tsx"), + entry("src/ui/field.tsx"), + entry("packages/ui/src/new.tsx", "A"), + ]; + + const nodes = Tree.build(entries); + const rows = Tree.rows(nodes, new Set()); + + expect( + rows.map((row) => (row.kind === "folder" ? row.name : row.id)) + ).toEqual([ + "packages/ui/src", + "packages/ui/src/new.tsx", + "src", + "ui", + "src/ui/field.tsx", + "src/app.tsx", + "README.md", + ]); + expect(rows.find((row) => row.id === "src/ui/field.tsx")).toMatchObject({ + kind: "file", + entry: entries[2], + depth: 2, + parentId: "folder:src/ui/", + }); + expect(nodes[0]).toMatchObject({ + kind: "folder", + paths: ["packages", "packages/ui", "packages/ui/src"], + fileIds: ["packages/ui/src/new.tsx"], + }); + expect(entries.map((item) => item.id)).toEqual([ + "README.md", + "src/app.tsx", + "src/ui/field.tsx", + "packages/ui/src/new.tsx", + ]); +}); + +it("supports deleted paths, case distinctions, backslashes and file-to-folder changes", () => { + const entries = [ + entry("src", "D"), + entry("src/new.ts", "A"), + entry("Src/old.ts", "D"), + entry("literal\\name.ts"), + entry("src/file10.ts"), + entry("src/file2.ts"), + ]; + + const rows = Tree.rows(Tree.build(entries), new Set()); + const ids = rows.filter((row) => row.kind === "file").map((row) => row.id); + + expect(new Set(ids)).toEqual(new Set(entries.map((item) => item.id))); + expect(ids.indexOf("src/file2.ts")).toBeLessThan( + ids.indexOf("src/file10.ts") + ); + expect( + rows.filter((row) => row.kind === "folder").map((row) => row.path) + ).toEqual(expect.arrayContaining(["src", "Src"])); + expect(rows.find((row) => row.id === "literal\\name.ts")?.depth).toBe(0); +}); + +it("reveals filtered paths without changing saved expansion or folder inclusion scope", () => { + const entries = [ + entry("src/ui/a.ts"), + entry("src/ui/b.ts"), + entry("src/app.ts"), + ]; + + const nodes = Tree.build(entries); + const collapsed = new Set(["src"]); + + expect(Tree.rows(nodes, collapsed).map((row) => row.id)).toEqual([ + "folder:src/", + ]); + const filtered = Tree.rows(nodes, collapsed, new Set(["src/ui/b.ts"])); + + expect(filtered.map((row) => row.id)).toEqual([ + "folder:src/", + "folder:src/ui/", + "src/ui/b.ts", + ]); + expect(filtered[1]).toMatchObject({ + fileIds: ["src/ui/a.ts", "src/ui/b.ts"], + }); + expect([...collapsed]).toEqual(["src"]); + expect(Tree.rows(nodes, collapsed).length).toBe(1); +}); + +it("derives mixed folder inclusion from excluded files and partial hunks", () => { + const ids = ["a", "b"]; + + expect(Tree.inclusion(ids, new Set(), new Set())).toBe(false); + expect(Tree.inclusion(ids, new Set(ids), new Set())).toBe(true); + expect(Tree.inclusion(ids, new Set(["a"]), new Set())).toBe("indeterminate"); + expect(Tree.inclusion(ids, new Set(ids), new Set(["b"]))).toBe( + "indeterminate" + ); +}); + +it("selects ranges in displayed tree order and excludes hidden files", () => { + const entries = [ + entry("a.ts"), + entry("src/z.ts"), + entry("src/a.ts"), + entry("src/hidden.ts"), + ]; + + let state: RepositoryWorkspaceState = { + ...Workspace.create(), + entries: { changes: entries, history: [], stashes: [] }, + includedFileIds: entries.map((item) => item.id), + }; + + const visibleIds = ["src/a.ts", "src/z.ts", "a.ts"]; + + state = Workspace.reduce(state, { + type: "select", + view: "changes", + id: "src/a.ts", + visibleIds, + }); + state = Workspace.reduce(state, { + type: "select", + view: "changes", + id: "src/z.ts", + mode: "range", + visibleIds, + }); + + expect(state.views.changes.selectedIds).toEqual(["src/a.ts", "src/z.ts"]); + expect(state.includedFileIds).toEqual(entries.map((item) => item.id)); +}); diff --git a/apps/gpuix/tests/repository-workspace/repository-workspace.component.test.tsx b/apps/gpuix/tests/repository-workspace/repository-workspace.component.test.tsx index e0fdee82..4935cc04 100644 --- a/apps/gpuix/tests/repository-workspace/repository-workspace.component.test.tsx +++ b/apps/gpuix/tests/repository-workspace/repository-workspace.component.test.tsx @@ -820,6 +820,10 @@ it("supports arrow-key rail selection and entry/exit with Tab", () => { ); root.renderer.simulateKeystrokes("tab"); root.renderer.flush(); + expect(root.renderer.getFocusedElementId()).toBe( + element("revision-file-layout").id + ); + key("revision-file-layout", "tab"); notifyListFocus(); expect(root.renderer.getFocusedElementId()).toBe( element("repository-list-scrollbar-vertical").id @@ -885,6 +889,10 @@ it("navigates revision files by keyboard and scrolls each pane independently, pr element("revision-divider").id ); key("revision-divider", "tab"); + expect(root.renderer.getFocusedElementId()).toBe( + element("revision-file-layout").id + ); + key("revision-file-layout", "tab"); expect(root.renderer.getFocusedElementId()).toBe( element("entry-src/app.tsx").id ); @@ -1009,6 +1017,10 @@ it("reveals the remembered row when keyboard focus returns after scrolling it of key("rail-settings", "tab"); expect(root.renderer.getFocusedElementId()).toBe(element("rail-account").id); key("rail-account", "tab"); + expect(root.renderer.getFocusedElementId()).toBe( + element("changes-file-layout").id + ); + key("changes-file-layout", "tab"); expect(root.renderer.getFocusedElementId()).toBe( element("filter-changes").id ); diff --git a/apps/gpuix/tests/workspace-session/restore.component.test.tsx b/apps/gpuix/tests/workspace-session/restore.component.test.tsx index 84a150b1..7fd20842 100644 --- a/apps/gpuix/tests/workspace-session/restore.component.test.tsx +++ b/apps/gpuix/tests/workspace-session/restore.component.test.tsx @@ -83,6 +83,49 @@ const read: typeof RepositoryReader.read = async (path) => { }; }; +it("restores and saves the file layout across all file panes", async () => { + const save = mock<(document: WorkspaceSessionDocument) => void>(() => {}); + + root.render( + + ); + await settle(); + await settle(); + expect(node("changes-file-layout").customProps?.["aria-description"]).toBe( + "Current layout: Tree view" + ); + const toggle = root.renderer.getElementBounds(node("changes-file-layout").id); + const refresh = root.renderer.getElementBounds(node("refresh-repository").id); + const search = root.renderer.getElementBounds(node("filter-changes").id); + + expect(toggle?.width).toBe(26); + expect(toggle?.y).toBe(refresh?.y); + expect(toggle?.y).toBe(search?.y); + expect((toggle?.x ?? 0) + (toggle?.width ?? 0)).toBeLessThanOrEqual( + refresh?.x ?? 0 + ); + expect((refresh?.x ?? 0) + (refresh?.width ?? 0)).toBeLessThanOrEqual( + search?.x ?? 0 + ); + expect(node("repository-list-viewport").customProps?.role).toBe("treegrid"); + click("changes-file-layout"); + await Bun.sleep(250); + root.renderer.flush(); + expect(save.mock.calls.at(-1)?.[0].fileListLayout).toBe("list"); + click("changes-file-layout"); + await Bun.sleep(250); + root.renderer.flush(); + expect(save.mock.calls.at(-1)?.[0].fileListLayout).toBe("tree"); +}); + it("restores History, loads the saved older commit and its selected file before saving navigation", async () => { const commit = commitInfo(60).oid; const save = mock<(document: WorkspaceSessionDocument) => void>(() => {}); diff --git a/apps/gpuix/tests/workspace-session/session.unit.test.ts b/apps/gpuix/tests/workspace-session/session.unit.test.ts index 78629b07..2cb5ee79 100644 --- a/apps/gpuix/tests/workspace-session/session.unit.test.ts +++ b/apps/gpuix/tests/workspace-session/session.unit.test.ts @@ -10,6 +10,22 @@ import { WorkspaceTabsModel } from "../../src/feature/workspace-tabs/lib/workspa import { repositorySnapshot } from "../_shared/repository-snapshot"; import { commitInfo, historyPage } from "../_shared/history-page"; +it("validates the optional file layout without invalidating older sessions", () => { + const document = WorkspaceSession.capture(WorkspaceTabsModel.create()); + + for (const fileListLayout of ["list", "tree"] as const) + expect( + WorkspaceSession.parse(JSON.stringify({ ...document, fileListLayout })) + .fileListLayout + ).toBe(fileListLayout); + + for (const fileListLayout of [undefined, null, "unknown", true]) + expect( + WorkspaceSession.parse(JSON.stringify({ ...document, fileListLayout })) + .fileListLayout + ).toBeUndefined(); +}); + it("persists discovery scope and hidden suggestions without saving scan results or changing older sessions", () => { const state = WorkspaceTabsModel.create(); const preferences = { folders: ["/Volumes/Work"], hidden: ["/test/hidden"] };