From 7d8e4bfc75cb510afa107d9c123bb7ce24d915d4 Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 10:59:03 +0700 Subject: [PATCH 1/3] fix(file-transfer): reconnect signaling so the peer isn't stuck 'waiting' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: on mobile, the sender opens the tool, gets the link, then switches apps to share it — backgrounding the browser closes the sender's signaling WebSocket. When the receiver later opens the link it's alone in the room → becomes host → 'Waiting for the other device to join…' forever, because there was no reconnect. Fix: - Reconnect the signaling socket on close/error and on tab foreground (visibilitychange), until the P2P data channel is established. Robust to either peer being first (WebRTC role already decoupled from send/receive). - Once the P2P channel is open, ignore signaling peer-left/close (transfer is independent of signaling by then); a real P2P drop after connect is terminal. - Keepalive: client pings every 25s; the Durable Object auto-responds 'pong' (setWebSocketAutoResponse) without waking or relaying it — prevents idle proxy timeouts. Manual (serverless) mode unaffected (no reconnect needed). DO relay + ping/pong auto-response re-verified via local wrangler dev. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/hooks/useFileTransfer.ts | 117 ++++++++++++++++++++++++------ src/tools/webrtc/signal-client.ts | 19 ++++- worker/signal-room.js | 7 ++ 3 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/hooks/useFileTransfer.ts b/src/hooks/useFileTransfer.ts index 554252b..c2b8ea9 100644 --- a/src/hooks/useFileTransfer.ts +++ b/src/hooks/useFileTransfer.ts @@ -24,6 +24,7 @@ export type TransferStatus = export type TransferMode = 'send' | 'receive'; const HIGH_WATER = 8 * 1024 * 1024; // pause sending above this bufferedAmount +const RECONNECT_MS = 1500; export function useFileTransfer() { const signalRef = useRef(null); @@ -38,13 +39,28 @@ export function useFileTransfer() { received: 0, }); + // Auto-mode reconnection bookkeeping. + const reconnectRef = useRef<{ mode: TransferMode; roomId: string } | null>(null); + const reconnectTimerRef = useRef | null>(null); + const stoppedRef = useRef(false); // set once the room is full or on reset/unmount + const connectedRef = useRef(false); // true once the P2P data channel is open + const openSignalingRef = useRef<() => void>(() => {}); + const [status, setStatus] = useState('idle'); const [error, setError] = useState(''); const [progress, setProgress] = useState(0); const [incoming, setIncoming] = useState<{ name: string; size: number } | null>(null); const [receivedBlob, setReceivedBlob] = useState(null); + const clearReconnect = () => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + }; + const cleanup = useCallback(() => { + clearReconnect(); channelRef.current?.close(); peerRef.current?.close(); manualRef.current?.close(); @@ -57,9 +73,13 @@ export function useFileTransfer() { }, []); const handleState = useCallback((state: RTCPeerConnectionState) => { + if (state === 'connected') connectedRef.current = true; if (state === 'failed' || state === 'disconnected') { - setError('Could not connect — the network may be too restrictive (no relay server).'); - setStatus('error'); + // A P2P drop after we were connected is terminal; before, let signaling retry. + if (connectedRef.current) { + setError('The peer-to-peer connection dropped.'); + setStatus('error'); + } } }, []); @@ -93,10 +113,14 @@ export function useFileTransfer() { if (modeRef.current === 'receive') { channel.onmessage = e => handleRecv(e.data as string | ArrayBuffer); } - channel.onopen = () => setStatus('connected'); + const markOpen = () => { + connectedRef.current = true; + clearReconnect(); + setStatus('connected'); + }; + channel.onopen = markOpen; channel.onclose = () => { /* transfer completion is driven by byte count */ }; - // A channel arriving via ondatachannel can already be open, so onopen won't fire. - if (channel.readyState === 'open') setStatus('connected'); + if (channel.readyState === 'open') markOpen(); }, [handleRecv]); const setupPeer = useCallback((initiator: boolean) => { @@ -111,18 +135,27 @@ export function useFileTransfer() { }); }, [wireChannel, handleState]); - // --- Automatic signaling (via our server) --- - const connect = useCallback((mode: TransferMode, roomId: string, iceServers?: RTCIceServer[]) => { - cleanup(); - modeRef.current = mode; - iceRef.current = iceServers; - setError(''); - setProgress(0); - setReceivedBlob(null); - setIncoming(null); - setStatus('connecting'); + const scheduleReconnect = useCallback(() => { + if (stoppedRef.current || connectedRef.current || !reconnectRef.current) return; + clearReconnect(); + if (typeof document !== 'undefined' && document.hidden) return; // wait for foreground + reconnectTimerRef.current = setTimeout(() => { + if (stoppedRef.current || connectedRef.current) return; + openSignalingRef.current(); + }, RECONNECT_MS); + }, []); + + // (Re)open the signaling socket for the current auto-mode room. + const openSignaling = useCallback(() => { + const info = reconnectRef.current; + if (!info || stoppedRef.current || connectedRef.current) return; + // Close only the stale socket/peer; keep transfer state. + peerRef.current?.close(); + peerRef.current = null; + signalRef.current?.close(); + signalRef.current = null; - signalRef.current = connectSignal(roomId, { + signalRef.current = connectSignal(info.roomId, { onMessage: msg => { switch (msg.type) { case 'welcome': @@ -133,11 +166,12 @@ export function useFileTransfer() { setupPeer(true); break; case 'peer-left': - setError('The other device disconnected.'); - setStatus('error'); + // Only meaningful before the P2P channel is up; after, ignore. + if (!connectedRef.current) { peerRef.current?.close(); peerRef.current = null; setStatus('waiting'); } break; case 'full': - setError('This transfer room is already full.'); + stoppedRef.current = true; + setError('This transfer room is already full (two devices are connected).'); setStatus('error'); break; case 'offer': @@ -147,13 +181,46 @@ export function useFileTransfer() { break; } }, - onError: () => { setError('Signaling connection failed.'); setStatus('error'); }, + onClose: () => { if (!connectedRef.current && !stoppedRef.current) scheduleReconnect(); }, + onError: () => { if (!connectedRef.current && !stoppedRef.current) scheduleReconnect(); }, }); - }, [cleanup, setupPeer]); + }, [setupPeer, scheduleReconnect]); + + useEffect(() => { openSignalingRef.current = openSignaling; }, [openSignaling]); + + const connect = useCallback((mode: TransferMode, roomId: string, iceServers?: RTCIceServer[]) => { + cleanup(); + modeRef.current = mode; + iceRef.current = iceServers; + reconnectRef.current = { mode, roomId }; + stoppedRef.current = false; + connectedRef.current = false; + setError(''); + setProgress(0); + setReceivedBlob(null); + setIncoming(null); + setStatus('connecting'); + openSignaling(); + }, [cleanup, openSignaling]); + + // Reconnect signaling as soon as the tab returns to the foreground (e.g. after + // switching apps to share the link) — until the P2P channel is established. + useEffect(() => { + const onVisible = () => { + if (document.visibilityState === 'visible' && reconnectRef.current && !connectedRef.current && !stoppedRef.current) { + scheduleReconnect(); + } + }; + document.addEventListener('visibilitychange', onVisible); + return () => document.removeEventListener('visibilitychange', onVisible); + }, [scheduleReconnect]); // --- Manual signaling (serverless copy-paste) --- const manualCreateOffer = useCallback(async (iceServers?: RTCIceServer[]): Promise => { cleanup(); + stoppedRef.current = true; // no auto-reconnect in manual mode + reconnectRef.current = null; + connectedRef.current = false; modeRef.current = 'send'; setError(''); setProgress(0); @@ -173,6 +240,9 @@ export function useFileTransfer() { const manualAcceptOffer = useCallback(async (offerCode: string, iceServers?: RTCIceServer[]): Promise => { cleanup(); + stoppedRef.current = true; + reconnectRef.current = null; + connectedRef.current = false; modeRef.current = 'receive'; setError(''); setProgress(0); @@ -212,6 +282,9 @@ export function useFileTransfer() { }, []); const reset = useCallback(() => { + stoppedRef.current = true; + reconnectRef.current = null; + connectedRef.current = false; cleanup(); setStatus('idle'); setError(''); @@ -220,7 +293,7 @@ export function useFileTransfer() { setReceivedBlob(null); }, [cleanup]); - useEffect(() => () => cleanup(), [cleanup]); + useEffect(() => () => { stoppedRef.current = true; cleanup(); }, [cleanup]); return { status, diff --git a/src/tools/webrtc/signal-client.ts b/src/tools/webrtc/signal-client.ts index 7a3e77a..95d6d17 100644 --- a/src/tools/webrtc/signal-client.ts +++ b/src/tools/webrtc/signal-client.ts @@ -12,18 +12,30 @@ export interface SignalHandlers { onError?: () => void; } +const KEEPALIVE_MS = 25000; + /** Open a WebSocket to the signaling room and relay parsed messages. */ export function connectSignal(roomId: string, handlers: SignalHandlers): SignalClient { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${proto}://${location.host}/api/signal/${roomId}`); - ws.onopen = () => handlers.onOpen?.(); + // Keepalive: the Durable Object auto-responds 'pong' (not relayed to the peer), + // keeping the connection alive through idle-timeout proxies. + let keepalive: ReturnType | null = null; + const stopKeepalive = () => { if (keepalive) { clearInterval(keepalive); keepalive = null; } }; + + ws.onopen = () => { + keepalive = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) ws.send('ping'); + }, KEEPALIVE_MS); + handlers.onOpen?.(); + }; ws.onmessage = e => { - if (typeof e.data !== 'string') return; + if (typeof e.data !== 'string' || e.data === 'pong') return; const msg = parseSignal(e.data); if (msg) handlers.onMessage(msg); }; - ws.onclose = () => handlers.onClose?.(); + ws.onclose = () => { stopKeepalive(); handlers.onClose?.(); }; ws.onerror = () => handlers.onError?.(); return { @@ -31,6 +43,7 @@ export function connectSignal(roomId: string, handlers: SignalHandlers): SignalC if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); }, close() { + stopKeepalive(); try { ws.close(); } catch { /* ignore */ } }, }; diff --git a/worker/signal-room.js b/worker/signal-room.js index e055d61..0139328 100644 --- a/worker/signal-room.js +++ b/worker/signal-room.js @@ -10,6 +10,13 @@ import { DurableObject } from 'cloudflare:workers'; * Uses the WebSocket Hibernation API so the object costs nothing while idle. */ export class SignalRoom extends DurableObject { + constructor(ctx, env) { + super(ctx, env); + // Auto-answer keepalive pings without waking the object, and without + // relaying them to the other peer. + this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong')); + } + async fetch(request) { if (request.headers.get('Upgrade') !== 'websocket') { return new Response('Expected WebSocket', { status: 426 }); From 50cdc03532014d6bc972b245ce0e021a8b6fdc8d Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 11:04:21 +0700 Subject: [PATCH 2/3] fix(seo): real 1200x630 Open Graph image (was a black square) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Share previews (WhatsApp/Twitter/etc.) showed a blank/black box because the default og:image was /icon-512.png — a 512x512 square that is essentially solid black — while the card type is summary_large_image (expects ~1200x630). Add a proper branded 1200x630 OG image (public/og.png, generated by scripts/make-og-image.mjs via sharp — brutalist cream + violet GWT badge + wordmark + tagline), set it as the default in Base.astro, and add og:image:width/height/alt. Fixes previews site-wide, not just file transfer. Co-Authored-By: Claude Opus 4.8 (1M context) --- public/og.png | Bin 0 -> 43074 bytes scripts/make-og-image.mjs | 18 ++++++++++++++++++ src/layouts/Base.astro | 5 ++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 public/og.png create mode 100644 scripts/make-og-image.mjs diff --git a/public/og.png b/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..62e1c6e5dd2cd04fc148808ab6f7420410c0776a GIT binary patch literal 43074 zcmeFZXH=72w=NuPpn|A?fV4+^1OWjN0i~*dsPx_gh=2+qB0WIDW1$xtAYD30mEJ;7 zn$%DN2!TX;2_dv3KnR>W`0jn)J;wR|?eUFod}se)+>8~n?p5ZR^P1N+7vTnaT5P8- zo&teDY!4r37=b{?%0QqaJ}f7I7X3Q`Ho)WLiwBmTAkb+J#t#!HDTN0F`U~_>+u_k0yxN+zTQ=Mm?kwIKKL}4biu(&=3=fb1%M2 z%@;vOt}WmEpRfPV(*6J0Y)k~~A;>k^vAeGN-Fn!mx|I)EX=r!lg|-zEH|o=*e{au9 ziK{bZf0NAx_1PAN*&JJHP)-mi$8EV`+j4ITRvJV*sEz_hW>NlLupH5c=OXLTmlZ@rE$6QyHxM-m)(*9^C_Nv8A|{AZC$Ko!fy2|pBE~) zowtOnqRIYF&hd0|5qAaBJT0DoZ_i<2;bkebhAWqX+$+8S7x6V1O{G}9RLZ1UrJ)l8 zlt*dQb5E+s75(>4vrYbQ$6#y>0H7lC&vA2Q64U5qXZusj*36Og`B{L zHLgq?t*?9aMQxR0Q!=Od)656*bxaL~pQ@W6+N4T66izTB zu16QtB_#NU;hj2^({ln#R3s6XsHe|#*2K`whzOr{!!X`NL~=60RCV9LJ*o=lJg zOrI<}^ZVmL;1w+0b!XW)S|sE+F7WO?4$RH}p_F@sgF45z@7J8P^W%$B=~6|*uWRZ; zo2!vZB}sk)#zH|=pVl5R0BG} zy8rHXbJ%xn%Zi=UZvjo0;Ie7FN*F%rYmp8gu-B}>@3-@>9HPnKRX(7vzndd?_tJVT zpQwc3iR9wxso^lXS#o?~us;rLtaWh9&S4XkN*#RD9UTD82>PFOQ7nX{g<8sX;{uvuUX~hOO#;mP2@ol1$ zt}?K={V%U$79a5%5WLSfv9TU*k=1XU=@ryI^9M*fnK$dRk}f)nVQiv;AGrG_Jg33B z>)}o@H7V<_kngWU8ZUwkCEPM0(FQLv^;u0h8F2ZIuL%G)t{Y-w>6T46{nDl+r-p0h zU}B*-v1)KzopxW zp;oZmjxL6x9m5~+d7>yi)IFeX-TtePzRGnEbS}?{TO)YV#<1ATKJh)D3YO-ZN4f!r zmg4tbtbiT;pT}x?(3LBsPcf1d9y0-^ky!;pEa|7E^z!&nJE<5WKhPkqe@E7gy&xQx zORVfc?XSKg=?PKssSO9eS`JDS{ih*4##-8kwsoDrQYL~59~Z1xpW8`k^4zIlHxncE z|ICu#H`o6HIWjdeuqxfH+fIe+1h2z%iO1(~ zB;W@8R<43m=`0~QPN31f>ButV-1osAZc!;c!~ir+h)jYapR62N1>(xC!E!jQL$GAK zr=>KeDM(u<@daM%di7Ab<7of+!!-xI0ZY3p6$luKbM%$!>DjBlRST6$6K|z9(8d$9 z_KBagz`X*n$*nx@j=h>vWW19SVlMg;<{V$N+SYYd_I+ehz2o7MpLobv^61gko%v+x zZW=FC9=V?KjpjMF!|bc=LBTnWe!QsL!+C39;Vo~eleOgDE(+_hB!>L`tVo|#KZT4> zuNvJZ+@8;a^5^NAyPcVnooC;jHC;atBG*dteGJNA3;0)Sl>3?tC?B@V5XH%>ZY+j_OF=PB3qtAZh@fV#&Et;TFHm!t11?;F zvoYT|ig@)=s#}Wo(a(aQ9}jcf`mFQzY{Bc}^c^%pwVO8PJkZH&FK`g~E%aziV6G}< zH+ZRlxjoHvt?{T`)4#t;(uo6*(+xrPn8|l zfqlHxJPr>R8|#*iR^P{63{4#;*hebovC7gAiFZWl;mVlyH9bgq_u@perp0t7<-ml=tZ4p?ebcims*K z4u~AoSb=Mr-^mGSLIHL-kGUQk3hPj?9^luuQ0|Do<(lzt%N0Jp-`qQ+ldjM$>O8M~ zQackj1lXCUzrV8_(+Iq9?rIYJJ1QdA4$=Orh4y$6??>?e9si9wwOu&MZeN{} z+qew`vqRhUlM@0K?sGnIG(~a!TF`nOJ!09p?Q}qp>+&1yyNmX3uVPej3*of=;fCcY zM3lfiO;XYNhzD70hSY7Fkgn(eE#poPki=(9a%+86xRn`J9OJ;z5lrF^ryc7$0$ldqkQ_34!==ck{DF}o8wW6oNTvOJ|3tP4uY{Z7W?Lf_SEt-x0;ORg6 zR;6~N=+eouAH%3_p+=3nR1Snn){A#9$0=y1(z)*5M<>h9SjgfH+6Gi*dSuXwdgFo9 zR@O6I{@jPouSOd9_Ng)-OZVrL#|nrf&hJB9qK)q$6;BVC9$^ORXMZkq2$Y*^26KJy z-4flkc@|H07Ma-9-WmC}ppXY(Wt<||5As8J{ctdgP5%JN@hn@}k8}zF5jKhG0Qc(Q zcj|1I^pA;l9PEv)WMDUj8^x8HPa#w`+nPpc&5|74FVcrSjd$5QGn)ehrsDq7cG%2= zxsDH$P%@G>Fb1UU{B(d;FjCR(iSl;t!Anf9*)+%3Izb6LaaMP0Z`kCpF*= zqa0wH4|4a<7XeUB+9tx`13fTTdE+_c-)G_y%ykCr3xitN+onI$Z%iWA4PsV$RMHET zo9oy5_f)cle@14+(XSyAO7)fsi875we{AD#U~|rt{Qxp90@t(BUD=r>^8=MPt6|Z( z=7RPDm3@^HSRazzP(&eBmUXiR}8!>$C3L~(`zre#xBVyLQKdX=d@XZ5QD0wDd zcZ2Jt8mYPDaQVTyq5#F>bi$$nZNk&mPipdUmbSP^;vj!nt$pe2GXkD;tKk|ta!Dlg zzvP#v%AC2?nW%OUD=6j1Vi4zn!l?>Ur4Evu`gG*qAT-GPxW(C!9h;*d&=)KV-~-Rv z8|M9cN5?EM&pxB&0PCrb2J$2j(*^H~^PX&ghjYJt%;M;sV9nJ3`TGAX-T$}DW@Ghe zzWUGtVAP=3c|*;z1)#zV>2u5dTRT8Y^Y5jbYdYf1&lk1dDBcWG1)8q=DY7YlJXLma z_k6|~-E?-Ki8W2{C_4K{QAm@?<)}gn$lMDo7;B}x%%l~&94)G%B zX;sXjlL?jwaumieo>u%{-w-LT{Ng}h?D#@@oPf_F(^#oI_NKLK?{Ec|xrLOq^U{@y zw->C|@bR|J1uL!+Qt6Pg{ra(LtlI#MnqUiqQ+kZq6>tn4?hE$Rk!z3Ef2vcre&kh+ zT3x0sYN>so1@P)$b4jfl{TZV~3Bzq9yI%(WmO-pXK9S8;OBC?jnnHq2ps=CjM<u(8ppWo{<_i`;4D9 zl%Q5400twNPJM*SMI-hrqJ^|{xA&<{$zCfsUo<2d8Xzd`m0qw#Ezy;#UrTAHDv-BJ z4|YqNVq*{XNw+WJ-9aJRq6<)yL;|P1?#pw35vvwT#scuPwEOP)3!i6F$IuG&hm>EJ zsd7!yU&Kdkyq^v3tg9Vtu6rcVFdXa`PNi_$y!dAjtLV7P@m76JoqY`Tf#gtSYW@(H zD=B~AG}B?o(X?O1^?@>K>pL;HUA=Ir`SDr6CO@+(-bQ3~+2ny-KfspW7L4^NK@xgp zI{2gUCZWG#kkoO>&szx84BqFPMSU(FZ!0saG!A?P**j4$L?~{Ezx0;%jrh@`1iPd(-?r&P-otUSi;WU0y{^Cjs`Ew`nCt)4>+nnV{mnky zr+HocE$;O=rv$Cc1Jm#E9WBxlYN|1SG^n4!m45OOg#Y>{ zCw0@am)to>IoMf9O>7iM-e2j67E;p%GVU`>BWq{vnXdDCz1+VK2)9gT+P$CmHJ(*H z9e-S7>+2SXV)R(nd?a08wPpnD8f`RQH^q)JU%q_+n0S1-E!GX`zagLEyUP3j*yCwU z{hT-y(#T2SyT`}P9BIi<#nsE=NahhC$kyu>X1tx6HCai*<+Nm_J*C8#?t6t0w3&=g zv4xMljV9SWbquZ=jz>LGtx?@ykBuAblZLM&v{e?e7EE}tRXu~+BE@QgM;c_)p6_ix z`v7u%0q~BYpwq#fUp{UseC_c*Rc7_HL!erW-mU=iU8y0Zjp3n9ssTi`f0PG7pg*+$ zaSQFXh`oaY4?^@1q> z>j)&Ipw+=n`hjJaJOnvh%7;hySA!&8CHa9Ks+8*8-l_dDnpQ4UYrZ=_dYVK-?ay`1 zl1VktI@wEl_g@RzYpz{X3GFt^-ha}=YSkv+_V;OB5_I{$p70Cd%)f-aUpSL9*n_Ss z(Tea1^+5vxg;AI8dpKUuEKJ7X`*T4Ot=Dmw&1Dz0$!r6@*xS2nMkOj0NuGV|TsSK8 z3d($A7m6b0w#TOCvcZB)<1WYE`g^CAwrn2%ERdEs@m~(nuyDZYH*GQYXqlDLqUp|x zfb|`*G2c<^hBbM2O1CZ_9uG{Bkbjw7S2gbf;34lp_Bvw`CS7C~A9j@&<{3n*pehok zoWvXll86RHiu@OkSa>B@b@M4@)fZK`)-s*sS~PMx@$1A${(Doe>T=(v&D61|C4KoO ze1Pc4r5`J5%mbEjpl*c(v7201*c}R%v6iZTdAHNeEcRyGYIAH(g z1%}USI{(GDUL4mnz!zfCL20_NIfm1Py(g;d*IwqslNietxazqnnr_T(x0RO-Cgq}@IjWcryu^SW&RaH(~Ql=B+s3# zsz^huE?L#k-iNMs;>aZjQYQ9I1ws33uYFxmYQ$EzJb{|trkd}tu^Zjj;)145r+3e% z1(bj%)2K~U736_35cZx#O6}&j0Rg5HPA^P%2^I3O_h})UlLfbvRo!>V+gVhq;Ii+e zQ|L;l+ONN8UFF&?2Q-x3V%6qlp`90$G-_jmg7$3VCR~v=m~f%X%yP#XnUNOC-)ILF z4!#;c;l;NrGqKfwbKQJjHpuho&;ZYv1M(57TR7%@w09 zSEKOn0tC*1gAM}l#d8k(tsF5A$Ju43S}TUq@`;K(6)p7nwb$j4YMe3+*tS*jegu%E z`FBgocT>g!s9^od{7zG-%%#-2n@aQz23< zf1R-K&};p6vBQy-H^%XDL`w&>xkGLL8xDHMreYe^aj+juG(?xOmbxT3;uYUR{GMuI`tFMcKkS{gD~5 z9$DZgh)@>(84*drmuddXQ|+-C-`ua4)=y94)V(HBxFMbs2GYo_4`X+kSBFlwB)Q{P zz(M}^eZKFwIH}>DWPw={@^qmH8n%MrgQaILLKh*aOlJIf-l0DR_8SQnE=0NVqwK$w zsHoVC0gr3BZsy7la#uoc=@lyWP2ak`8D$($1u}O28c6`Z>F^*%h0BPz26NrrpGWO$ zO8583db;aeJFh;04eaG_1xef+BGzEPQeTOL@a9#EwcI3eIadlM;8h+Ch^3kW!LNxrPT%OHFB&4X?!HT*Xl~)eHsN()g>Lpl4Ta<4WQlLb(IYLm; zvqwhj<>%BvrL_dqw&kj~2KtSfozGp#mI0$s*KfM{FlI6Bl)stc>pKzV^3{wKN}ouJpWCxLGyxT?P#IG$mkZ8i)u9B zC@%7?pu9KzV9%xEqn>j02KX1`RGDDyL5$nSXoVGelgT&z8LInh@+gr!?Y4XC$QNuv z0g_{%C49&E=R{A{Y|syU6W zH=8}7Dp;SHr)~>5(=APnYhRvg#r}?kZ(Yu*7KQOl1P*yeAGeso;&+`lD0#}qwl@2@ zkrSEBx@uTX_CUJ(P^DNb8K#!K@)|Yi2YJ@nN9+{ByL!hs zJ*h!K!{^=6B9>vtZM)&oCd;<7ebtSu$G#|KG>%gL%y#)~;yIMcettXltel)t4Fc&s zDjpP&S#7xWJb%;Khm!m=zrtkU5SjjDWF+n5{0i6sS9&~YPMU%meoHin^m-GmBwIi?Y?1Q*IlYqU z`|w(6pQjkCcfxZNZw*WnA?3)kVIk}lOKRwIo&R#&;@tK9-!uXhoQ?5sj0q#RzbjKD zJkf;CEUF}3^h&q{JFiNgRX@sYuSxou&oR=f8ZC%Jjk^s!H23AO=kb0C#&!c(ztBN* zS*ES5R(`J;)gLsEKVDAr?&|DFsFz$wp@+^+nL9;Ft$PQm$zHyqiO%>+u-w~h z!d9RIbAOd#8`+<92n1+cW->eWp#UpDezIcI7o%`DR#L1^3CMC87A z^sc<*!$7iKSblFFM;6B+BHOuD+Skj8JfPI_sfs5GWD*yathSZ z8#u{i_JD0E)5S!M5T2M}B-m4fTOO@Dkcyb2TFZ*`rGBEMzufS68{pl#;;NgjbuLX{ zJ^3{sTiLxg@!r(|)JdPOE3n&Pz+yxR?fquHs0Mc7LS883KXKvX2xqash2|pv$6Or> zNFw5^mMuD$x=hofe&3xgx~{B_^*TA1PMN&F(B_RT7; zgwkw1OC3vmE?@5$J|$jYqK%JQH^2MjGP}q*&Jh87vav;wkD-?W3HDf|uCY5B{O~ej zl%$%7EcxX(9T@JALa$ajvLZb73={b5<<-@d=_L8V(`Bn5!Y-lYrz(BP|KiEj@#{C? zH;!4zU@9R~!$m5sSuPf15ctY_+Ql7#pEPR^f+QlflF6(wKm6o{BwTET^-{7Ov`vEv zRMp5pz85Q(ea)my)@q6rr9V#2CD``YgCA7Ta3=_HLy3*usd5%!mt^T~Lxx&{Ndd{i$`9G}jH$l-g_mXS7S%^SMvK4NxpiWBUougL$&9r);@-5bQ2Iv4YjHvfi=8uRCS zN?n&luvZHwJ=mVrtN3e3_hm{$m(P@$*%6hZNj2-Oq)#vl5b+~b@sE9k3r!2F&4&uW z?#w`*zJ{NAs2~Z`(Im!XLiPcs0qbYIyY$clfM8s!4x=VAM3JLK@2n!lU}B$aucyA2>TQF4815tCV%L!~agy7G3j z;?_Gl-+}wCN6$bgxBDhZU+LSr&z58Cq;J|yCNrI5#%2v!3VQ*#&DFGW>XLiI>aSm9 zQ1EL02Ds(W$y`dXGg}Fn8ZVS~ZDiBcF4n$jIIy}nz;*mFZOQh-=u11>YJ+6yhONCv zAy+k#AMz!P+IrMaOV!>fts&SkIp6t+NAP}rai zj(`XN<`tvYo+2R*Xt0VUO@X}f1ObQptcKcf^Zw)Mx3!|FyFFQ@*6znv2ekM1Hv4+2CmvV)Q0JUcy+Q^b33k8opHnx~&i%>0 zv!ZaS#kqY;RGU|kN4+vr%edzzwbk(;P@O&0LZ4H965db@W6yD#_;r6R-irBo)6Tr) z98&gJ z#c?;S)jjIV$=4ofh3Z%zExOegxjJ;~%#70`R#!$Q0z*kwu-nD_z7B(F+1A}?>L8;U z^j;<-C_}oB#zJaw9aPw;fC4H*;n~XTTAW}I#-UU8OP^F9vYOfAo3v4h_JE@tERihR z;5K2s(m3Sv7S_<-jTb5T<1Tk?58dUMcY?qNMDxt0j3pw}YT_6dvMvgD`JC09t;@u3 zLBD3)+W@gPK0Mrg_x>J$`yUl(ZTwav^&tEFu)LRy2BtvkJ}5jhiDe{{UI?nqA_PZLWwm}auZ?oCRuDqf=jaSTGG>GJs>No8vYy^ ztQgZP{LfQXNS9Kjbj){nh;59Bb1CFTvsK`_X29_sA^6&=;V5^8Ur!3pT@PMDWa#9L z5HS0_V6K*Lk*jEy>|KG8pmj}z_=L91y$_#wcZKaEamkt^^f7ebD4)nE8kVJH$csc| zjd~>m$R)#QoC{f!^jWUH)1mFq`;r?Dy>Iz?Ec|nzI%kEt9FndZkzq*ZOcN|-Hrfh% zV2kK!x}eS)If0?j$^aeo+1n9b^{xnvZe+hJgMxFtORYff24#@n&Lz;gKC_Q!g-@Nq zTh3wMj#@hh$BoneLaTeJsW_K3?qwRf949PHy{rV=IN9*cOx?=)xx+*wndU2C)ZCHZ zNZ$8J@r%(P>T$7%J;$KqznzX-7(UqVk<}v>7O#`k*8ZV#gMCwuYNE65vXo^mJbh2E z?QG|xz_pD=1f>6p7)j9)Tg0KS&kYpSwLh;5l|oCwp0&vp{@hKlbS^Mswiw_j#gjD_ z)dJY7;dH-`(H|%-J@;*>Re;y|ya7i2@j9CB8YqBFi-saTQm_h;kn*!odf)a8=@hg@ zjnshbf04*@>KA%fPkHL|J%xb*$IPe6mnXz-Kmst=mlmd;s{W2{45_5X=NY7FDZ~}#aDTa4W3pJ8kSIU*4d-^hfDk^VttsT^$O3*(swo1B z|8D0f_q^ddfRR6UN7q9&Pu0e5f2WS3ktu$lSdveXlO_c9DdURaU$uDpMQ>bE3Y)|! zAW{E-OuxW1Zw-eri~?yr zm14Xg5u3iZtQMSnIp$hSjTJSy9|X$Jy3gAd-#~92%31m58+vv5x1c zsxc|#Y~x@+at32s0VGE7u2Yqq^HcFb zjv`=#tIUis2wvAj{Qii7r?jZ^FFx1X1Kow`ka5pg+jbkXt-c5Tw{(_l&sUiwEeFX7 zhy2La<6q13)GJkPjsVs4xN5!su}VMFAear3I5LibSON+EBfNr4k<@dKTzBKfu%xE~ z1KlFtJBFhL5sr`BxZ<^OxfOlN#okP26DUqzGv)E3LJ(^LK)y#G0{RE<9bTN9W(7!q zijz4-mz@VBth_2CtaSa|XA^+zE89tH?FK*-i9hq)`dP4XU-eZw24`-VUer@fk<}>q z3X;gJ)R(-eI&ve6#}79brQmAW)5rV$3C^i_H#M%q#*9;WwI;f-Xs@`5>cZ-%lX_eN zrV;NSn4@>`o$pn%_aFi~!Pelp^~{9+Wc(s|ed{B8T)oQ2E6w5?(;_*(j|FAK9xTgi zk~+lgCbo<(mk1%Y=?^=xoO9>LFW76AmSHGwKEL+|z#+aNR?NzkHMp|?V2ZO2J@yAH z<=PcRy^mte5wA+TCzUCeop0()me~5>;6DT}7N`Up39f8L6nrmCGQ74-tX%31K&eCl zW$Zsh)po+OAv}`(k+1gr4^)2?uoSdotAUjN7Ij1T79WInAW}S}os4a|4EKBiQL)O5 zHyGXs1>ymplYggPB;I-T1e(!5OJ4ipe9H z`L92I``H)WXimEN)IF2x90)aaGiC%yT_c3ugfHCOtfh8T~cJN3-n zznWSo9mj zZ7YM_Y-9vud9ItaY+CstuU+#w752K zkb1j=-*anuDX!qBw?YWkjLUv9$YniIS<4g1FLm9a{57WFp>ZC2O_sQN0jC~L^Wn7} ze7eu{=WgWU4^Jzlp30wVJMbfG^}xhv^*5ITKM_DK>ZGFJo-K)ynHy zrzPZVvGKA{YD=hFf3jM#=2sEm0Y4oLOW-%P&DyYfnsGc}VIpeD5!HY6OV9>*;SaL$ zxcqTf{a{HSE&GXSL{xQM?O}Ipk_6<8XL}(!&Pqbk`F>G-FLw&U>bP$}M?rgj)$Fpql+$ll{3 z`oRf6g3d%0FBK4Ta0>hVt&8~FF*weOCPncWrsY5QEDI)e3i$pKd6syt(ahFWEMACE zP_Jrk%VajNn`ye$6LE=_HT>`xWAOMtgCB@fCNrzLyM6Vm7^i>;NK%mg;D#d)co@Hs z=#r8&u|IV1VgtYx7uCeNZGgBT0o{}{w@sRu&nL=_yj(~ke0JkG0twg||BsI7V(4|d~@Z|`BHTMSXA3sm?ZwL|=M z(PEIraMTBom-c&V{hx&Vp}&1${T*Mx!{rk`mG3dQ=nYcE1KX(eQ^a;1Rk7|UMI+Oi z+j2UI`Tg$CNl{-TsrSFr0W@(|ZZd)srMmqk$n(Hf`N8~Ti7|qTZK@V?PUhdi-qIqZ zyqEH3vBu7dRi>S{HtRHE))^6p_S*>U3%>UZ7+LxhS zM1Wiwk&WtetzzrL!9GoB-~ROkf?G3b(|r`_i%$w<)zB@1V?XU0aL7_~1ZyB4UE=FL zfeO1|FE7QX58@V6E$4iy0L~ArB!)HcX0fbv@p^N`QL-Z46;m{yT9YOSD^SV0Tnljx zmNQ~^O-QXl<2K@JFfZxLdA2gAp|%UEpvj31aQD9sN7h-jt^&11HQJI$bd7@#YTwkV z2BA{6)hs>}pgQ}TE)IdR+C5hfCU*?!F9j$~U-FzKWx3qPp&AxSP*hXC`5;mNbkOaq z%>={9}NlNu4c(wQM(0#>)(5Z|ZV3z`l2R zSwi|@3Cc2#Ww^bN`xJ1C^FOI3r+7>8osPy5g=o5k*MRhuDowU)a%IkO7gu6N3kz8p zgdu|ny}NU;hdx*duom=ROal`e8`upoCAlwBU5Kx4J?j2G=<|4T z1%y$}^5{s>R~H#uw{Mys#%XQR`_LS$+yl9?<$P|l>=R~N<>Cz|MjCKrd=)?6Ym1|< zJ|4taT9S0&lxLh>$3_HQ6<5>?@^Vd}Ui@Rx%QcJZE01{|4?%d&T{7uX_r8AGVW;9O z9B`n?l@(`mk7+s(is81;0`QdTnkwPuM{KC9c@s6{yiv$|NJH<1x&$HFOZR!W)zx<) z+L*IN4Syl{daY7WJ)pv6?!PzD2Z`AfGnLA3(M)E~q1lVWx7;eu-iIhvoHg4l7Y`}n z=9M#u_cOF2`?B6qsA(J}9YVl~L^=Yp>`eHx#IPo&!Cc;xyOZ%q%0!id zjT{MucP8{oq^AfwYfJ#qing0v(?l+2sJa)%5%e>b{3j=b-I`{nJ*_XpN=?t%_bSd- zo+{vwD>|6;7N0DUKVc!mVcjL)dOC+k3X5}?4T!0mU~x81pv}&(n~cv}z?Z8b-*%)$ zor2{um1YW7*imW)@4i}V{YNc;IcbZbhHk*GK5qmQfSrh=j{jjdA|C&cF@3Qd>&Lkb z&!Fkd5Lsj@vz9@wfb#diYQIC3GSG!hSL|`jq#OherT;CtNrWN-pk^83*c8Q{%M(D& zQ4YIwghGXB6`4W5nFf1r>W-3;k_`M~hg5aket^P~7h9;5DqUuQTK&=*do@JG$_;HU z9D+f>0ihbw+Nt0r=B78V$rMx5Xnx9QwCJ{FV0Mo8-9FEMq-Y zRe!9W68VixLcr8<7g1@2SCiscenxDAFW;<|X-+cTdKH1sVCY~9UVEt|9aI9*oYl3c zFcsw0@t?DnP=u@lbi%M7pOMfijje2#SRhl#vurS@y1locmdt-Ire*>tZBnc2t-<>k z=RjgYD?bq<_%_#JC$cX{#yB|0+`h*JQCaCfY6YOX7IhS~d4?x?dcu^Mhwe5?mA5D* zCCbnIQ&5mN`BgPBP1;&9O5VbWSQhOYyzLS6aTNk)l$H8uU4KT3gW3MUn4lCw(8O{=CJ zKhWkPMrUXP`rJK?55afQ7DsZ+wD(q)Km_DWU@=fXQ?t?!nEHD%N1vGDU9d-rK3&T8 zUui%Kb}mruN|1rT-YldC)GHjd?()%K$H_OfhQVGLg5Zt8`zj=V-@)>%JfAp7%t<4o z*hu*5u!wcA+_(py1K9`bp2$!YJ3C$&SDZQ8-+P7ReMl`?U5~ncFJD+~)=;#dfT*?> zj697G1I}Qk)`MlmuSKyYPk1!9#FO*qHI}3CH7Ig#*EoMwuo1QqV0O&ppExthR*=Oq zIttz+1&r+DmGRa@;qwq$YA^ga_FBk1jY#=%vwc^8v6Uy^OHF&!zLRH;d{T{OT^rpmpQ4cMo3y= z8d+hKZIEhIR?V|o?=IKL2`|5pn-l@n*Og)q>Hi~|^;2l8FLkTLnCTg(p7}V1=2M$} zw54os4UnNJvJAuA`l2meI_1TVwp5)4uwos$rpQfs3&iBCyw+wMhyb7i;=+Grd zG*Ndl4VakUUsq|xlrWwXAL?y2mq#&jSx>iSA6g#VJ1uE3MzxF+9?dHcP;M(rzZ#;( zO9@IoQuO(ow6}F8w+v@A!1CDp_|v%l^cI!rkZ6eVYwsggs*Pbge(NmZKATwqz z%Vl3R+OET=aTbhDtUI#_RDxWAN&{fT)#DrHo!IlE`wnbn5nF!6k001>E$`wF==XA@ z-Hk-P@-WKIN@hPlAacbowCMt{CP-E6hkzf}m zV#C#&DdhrukZrmVss+yvH=txi!~1I-V^kYAfP$!>j+}h87NxIBu!-mG+jNOQiXGQj zVh5pY4pQ0};V*xZu0uus6h19l>cUW@i+dIJ$;;q(@9HHDrLUo3nKd>;D&Etz*DkgK z_vYKEhVyki(|c0&wgaAdD@1YG&#W|3f_$>Y)E5lZPMMQYXmN+3NP|?ZQaH@~N_Ep55QfY6iEbJvz@oWc~;npiHysn1#boI=$AwI_tjUdopypg=7j7)CFoR zfQ(M8lD&KJHb=+GMt9H^f4=F!NWnJ$QED^YmlNi2esnEUWpIWRd152MPJaDeC!kIW zk1qOo7>#%cvMi7b@X+5c>8)=&9qrn$NS_UbhBr_FCunDy^33 z+7Y5MYcxl=JKVNjEY%dRkHE>pO~zf{Y`ufIni_E9Icc#@0KtDz!O7jY7GR_w;e91a z%4`6l6;k=kSWp}6+Y@Cq`zz*MRSC%T@7704;an}f)4Rq`CePtaf&{BnnooV35T?n5 zAo7|p5%{I6DYj4OVsC3kEdzRg{1dnb&&7SzBozn~VsS%agc3#IB6p5vQ%AY;Ro$RV zyj6DzO(LNFpx2H6GWtZG%mgdU%gl?cg~j!cNV&?m;HIq~7{gH^l_!t=bdy({Z z$Jn2^%`{(bD=21qxduJ}L7+0$UPJj&hFD0?DZIR}q_(!y`#!ojlYNe^+!)7YAI+&3^P}5;gl}|& z1StbgdADmEH%(V(aDbP2zyP8rPrjZX)b1k)5Om-4ubq$W?&0(7+LV7_>!|0apvunVUU9Ch|8Uu22|g zo)irqu+QxShN617-ew}d+>aSs4C1LeaarTD3V%$3oG;#AyOc@>qQ6xal}EbY2y0WR zuv&x^iB(-LTfsFzgVdovZuw^2ytAj;aV^BDC;K8nn^<7t9c+4^G|3fz@SH@w>FwN$ z5vJ>6T91BF`r0_Udgyiw@^=i|*|1OEowt8mJ8&()dOBC;N8{pK4u#iF(rPdc&Lx-- zdlV2NwzeZfmwALOD)WY)KCW{lZzul*DqCzGPFB?erMYhxO%2;AeQF0TkFdFHBX%{F zbU)xifJrrJnyDx%bojj>0{E{Eg0+r6!tD0;@{rd>!RJ45-MAn76Fv@HYcx&xmEfJC z#^8L%A)3yf{JQ+f4Dto(>W%TaMhM(SZ9a}!nPMjCxo;ZlEB`tz*)@^-)RDD2R=(pv za=hEsN9MM=8o4Qnr7VK=a>_76i3OK#``zXI?(&9Qfm>)r#E{gS-^cuJc#5u0r+a;D zFF6hJvJ)j${lRkLI4Mc$u#+}2RmJI-)9GaortbBSE;k8Q!sPxwId7WK>s3!oQVVf(HebpRj=X zcUxa92pR?|T~;cntI-HGdRPWk6T63;pOI2rZ{!Y7nVN1E`GOv^F-H{ z8?@M@ha()SH&qs_bC%Bwq!r+oNA&g6^5fP4Lay{m^C=mxoS&a?-pe&FXcn0-ym-}5 z68fQ2lmQL;*TwIHUe-p-Z!rMkzdm|noQFg8Q&%)-v$*>V5{#Wo?V<*z($xuf*6P3O&~3Q!0+HO zM7vfh1T8nr8a)oGFa1dUBS1@X%Vu&VK1WJ<9Q7J65ZzOHvA7VD{~(wvlcIx#Hb1_& zdLuTHg}Dn}VOi}L)~#f0mF6Hzf>RqVx@u!I!WDHOY-Nwt5)3({UX_(sifb%rhC!UJ~A&@YtB5lS8EMneneBH?4o z3oB}W*N9zdj!3#A!v}e9*%fgNWN6V?LSh)rR38>9b)~lC9%ir};9(MQ^Z7ZLk>65B zvdcq|K!q<*BDN?ty0y=z-&iKGdHghJ(r6YLlK@;F82S60?HZ%Rc<^-e1ug~4yo0G( z1vhBVEfW`jF!IbRy4UFxj`MV?g5Jza3A7PI+H`TMf6H8S=OWTuNcvyNN-;rvZ(STG zA?#(D{r=dH|3%)LheQ3w|G&0SLb8u7sU%yn??p%vktO?5Ax)Eg8H|)A%ShR>W>5Ao z_8B`V#+Gd`S;s!cU@(mH_W7RQxvum7`JMB}xh{Xq<$8P1+x@=p`*q*X=i~Ld*T6ps z5#b22KpIYg=#dIWanx=>I7l;Iiw@zEy+_vC)Mu`ohb&Cl;bPt$ATc|iRCsAh-vY=@ z?Hit8e$6wqG^zHmT=k8cZ4{^ahu;@5>lZ7vri(gDKN{kDs_e=$2t8Q&5JReOWcq-L zrT>yZe%Sz|Mp=VLjAH4yEfLplJN)~if3AheAG)z!4yZB5l)m#4$9)mjsJIy?Tay;2I{yA4cY|P z%wPDucj0?{ZQK)9as1D|tsHNFl1@I`WX2&ssvmvtc%^NWH|G5U=iO zF9rrsjlVMNuKaau?Q(~X6$|_=e%MNATHwf0$;t#hDQauoYwDkcUPPPl!-)A+JDN}mx;}~nbGv5qJb;o zvvOJ6m^{%8vzu>JcBHDK`DW|S#|VcHJt{2^X2)s=3B6GFFJuuZsgAT$lPEjhOacWQ zWN7RWW;?VeSzn*+GwtpwkdFXYiPN;M2+J1sISrKnm=%_*+;=Q~C@)H?LIg1^jPzyB_^zxxP1nLA2C@_-_zV)sqLTe;UH} z7wia_A8&3hf5n+jR`nd;@KaCwD*gDkZF+H^A_A>R^7id*BkzUP{n$5D`*wQ9pdup+ z@y8H}UCLgH&I_(W)^v|=x4^sjjMAV)+YD+*iugnFo7cJ}q^mk-z z%*u+TVF84EaP&gmsTWbD&lbk*rb)k+e3H4T@*OEw=wZnxQPYs{<> z__aG-!wKC*4}J+Csi*pc1aR=++~8b47moeYE;MYaNw+ZLk%v^basx2Y*IlLCviNiR zsL2$LHyk4n;$adN>3M?(R_^6l0??73VpM>V@Etw5Zps`5=LavO7a-LZ1q4$(?b7qH8~x0L<26XWL)*y?6F?U_cP;e zzlC{h{RH!!d>Mw>-M2WQ@?E`-+Y59#c~xy#{nvv07%^>;vbmXkmigG?`20!A^_NSB zfK|4^Sr3=mWV4+4F9i~|X8irPbkh~p<4Rg+%guSmJ5&4}npa23yG6LwTIDxm3;q+^ zk=Wn97@wYv#4qvq{-e>|eUeBaB%XKFi7YZY<#6Hy?ND8S#-=Y#FtBME1TPPD*CqYh z{M940!pRH&bOl~3Z*E`fR~F|b-U*AJg<{@R>r@O_X1oWpw3v1VaI~B{X?FhkJ_HpD zOze8w3oQ1Pphv$VL0DbN-e+;U#rv!?JdM1L_xbaNN^V97bO+x%rtgH=hcr~|M!5ET zkT_$WA*psVvSG<{Nng?XTJuVRdCAttT=y$EN$NvmE13eaINf^(6Wjz<8PD?WYZrVB zI`2HIfC5Ui<9>Pv>)K|;N;u`rSHeWHJ?ugHP905agg50VSO@?F|6Qm*oM=XG`9B}ie)ds4 z`k#-c=DZI7b0@dH|3`0HRwP9E*L`F?ZX*M1746KhJ01AZNS0|N7_%Fbed4-+4<;45 zTOi}Tr-wRF6{y`V^e9AqL2|FeC_v|A%G9X%bi2v^&GGRv9$QIhIawSNDh-0l3n819 zp^G;5d;5R9GD4PyrQaxduK0;#Z@4`{9sULL;g*=OlPHhk0iP{9pq8_;E;uti&Ux?CiV71STDDgby!Q_-vg`#sI$4$WBKBGsAVkm&_M zQvmZ5Dm^{!Ie9cwSeF?v)r~!@gF!p{ND1)2Smf5)kp`)Ol)UiIMe}g3+nutG%KAyb z)7g~FzU+_0mmdmdlcrrNf}qRi|INz;V&YM#9q?zaJtPSUT}VY@PqvzNHr+bZg|lFq zlp#M0q#>YXKweKt2_}{Wrq{FxZikW~L0BL$t|18$e{FTbgx8_0IDocvMJ7IA)P(On zz2(SGil$J8PwGSL$s49hjg#W|HlDjCAG7k7T$0QpGIX4uQo!nB1i165&~J zaoU~uQKzpOvu>(Aq4g(5~-4pfl^)ssTxy zrr`A#>am(2z$nA)+G-L$##dmWv&wsVnx6N9-=0GJ0Ov`whg;*cxS$z_qO8AHm?buo zGFo=f^%k@eg*4rGd`XIZ^Ez#5`kVTfBM8b4uC6o)@Z69G;YJV@dI|}m7gHhIY86X8 zb|lmKIlrY0cq2wKtw%T*(gnYMimT9j$vjHRKFvA;QtNsO>F31lC_f8ckC<85nWKmT zkj%q>;kb$*p|U2Ukok^uBu+JiTs^<8vG*(fbr57U!*b6zg~wq}wslIG-Vy|Ikcn$* zK<-dE{>8Ka9)tbq&RE<@Ho-}Ne+~j@M9r4Xuy6pa#pjm^WKJEOYk=SjsSrH+Z2Kr zC5)=P;fSJi|FzFF4A098TG+$d`1etZj7gFEQeKKHnv{~(N~9`CClm|u(^6-e(nz(% zHq4G9

+OhMztto2(j?kB+~zFa@!F20NTb|Fu7wcX^2&O0~LS%2a7NMP9dWdhV-s z`yA2T{$!^;kB4=G-2YlLgyNN*A>hvM<-9*MK@~$hNEQkyLLUE&c}Y%0Z_lKcXL8zR zTZQTfgDb@_OsCgUm><$T?k6122AZJ zax#p;hPDBL;9RsL^(e`u zGyJ%))sy8t>RU2=r;lm304xsOjr~VPqsS-tkfxA<>TKooLaV9xAQ$eSe=XIz#*`Vw zX3A(BiVqfvnmi^S;#K`dDFNsP{M5k=?6?Ou?Qg`T!s8hKewKn5vF7%x$INV*qmCC-r1<-ct>aJh{Xy7Md&*J> zANj~xsHIY|HMb)fOS>V>b>&JB&rkIX@+j4F7$O)JAGFxv(gv{>jlidD!g1!+Ji5fM zuD$4L)I8y_UJ?v*8fiZ+9Wuo9ay)%m?TwvLk+7XbBppi-WqGd|vMSFCozPaa>j(~m zEx;PMuzTfxlI1UrgW_Yc=rJVH5id`s-OyJMyJdU7Mvnuk4e~?!6?G#guM&Nk3jeJa_i^gl`RY zO6qyjd|I9DW3ukDsIhyDQ!kyfYm2geRt0Bh24^03Gt9zf z3s1Bvt9sR$3mDD??Jq+H#UNo3iY)}6Jpd)DkR*Q~CP90!6i@#x=P&EO@&xyK<4~%F z$v1oM5usqLxG>eYM!9~0J7Cil@{$S;zXC+t;JXav12&Z@ZsW@yd$xdrOV(6|dBL9&gkeX&%q9 zrmGLWYBK8C7wGY@;Cly-#y+zLo0MDhyEn|%%n%>pw1B~Lq;Vr^fzlBYyYyf_3dZH* zba6%Of3W~-(Ct>dKpr)q1IE6Q zoMATpZDfTLit21gcj5RT+bX$K?v68V+pIz@Kulbr_M<>4(u^@ciyPH+14yL(N-JTm z8Y}#STjg!T`@ME}i%$z9)=MaL9+#sOi zETh0-c2(7NZ5j#qiPBCfljHe!d(-3Ji_~jLkj?~%a%(QFVNU~IU8(yBbRdU#By7p2 zQg8U_(hN?6v~@CrV0#%1^xPAba@Ys`Lsd6Bw9j=)Y-K$jGWgNW?lk-K@WZ_OynNP) zZc!;mtVE!cZ;n@wPfb}hkH@LF&lHF*hp^YINEXKazC%$E4jy-HSCBd8{dvLB#xi_e za-6`=l^n)G!xM%^qKA(+%Zd5ot$lg@2Y6wub2EO#4O?!;c)Fk|SV!;@RAXCP46Ryx zh7Ri1%F~%wf44sPX=pY;Ge!4QP{Y4RB9SMsCGcb&z!nSVH8|{5uf1N*h{svj^Cc!c zP~#AJI?ywnOU;Gh!-IpzzaboMO1;ufkHlxbRYU2qRQjXpvIxodaF$-SlIIj_BInl| zbvcIwqbrs)Cibng<_@gNCkeO=X>EM)wEYpyjH>5H6hJo!`rM{(mzgV9B$7emGOf969DG5s`*SS;KYLx3LC*s^ z`Kb*z3=Ce-abfQ3iN2x5O^e$Teoo?qkC*hH3tr#(AbQ-hao!n&*uJlT%wiE2xEf|P zn{twro%$24w1qog=_=73JaA}GliPdE8g%`Q@TaIyasMa;yjtq*!= z5IP8Ol?xaUQ)iC!C#r`~48zq55H_nv<8Ro83f^AD6oVixEWrsf(a__J*1XQMlC zeYo4F0(vC_vNZ0mL%a5B<$ee=SlXo6?Y83}${Gg}8=AWJh^;qZdSLnRMjDBMoO(#T z(0*|Ip@t&3?XLhVH-@;?#pO zLk8;3s&2;B(kli;AZ3g_5|+Ii%N}E@==OSKJC2+VdR73odBI%DkNg?`Fgp@XlLLH1 zkL{|u+xMjbH?h?D;y^*kc1nh5Yi`@raR!U0^}l-#oZ0zxhR~@kkR@{%(gydz;gjy> zC-_YPoPU|K%F9(Tbv!R^R(Zk=%oAF1z_)uUmUoZWtR1^(GyRK79I4>tEkX*{1)@V%S;{&zq0POHeJQ8FXR7X%h(oOA#iedYkYoojAEt%T`1rxjQ;ZPPF z>~+i{qK_woR_36vWssy=1-{Eygm0sNXR!K?kNm~m3tQF5PHpb>$8+;v^E{>dP1Ux!p`We^)dkh59mlYWVajO`Sr23JS?%F2v*=`{Xcm;%R_MPMBsL|D4 z;9>Ldw=$+V9p+Zu(whp|h8;h~y&L}0Zz=Sl*wHpSlip#2BN-HEfWFEYBjVbKknxRW z<8${eTtI(c8s68K?sVqqj)U^{^!fs6%f_!u-hWn&wapf&dZ}w|n`L&+k{?g_c*Ri* zoA_mdx-cO_)n=5OOD%v(FHf-QHHvv8VtYg*p%s;bV{&|Ze6i{Y>I=2GMP;wD0N)IM zPr*3b!JZGuDe~r6i@Pd!ooef=edzepvdjyW^>1X$8h5P*!u@SLZ0++?l`}j1B^QI% zFAy7yx`tQ_-qvwAuu0Tq_NZ+~<&|XZB#LgjhMuE@SPc1=i+RwA_4tg36fFp;9u~L_ z2UBxp{yHEc|IW<^bbgNyi2oQsBcWUBZCE!T{G^y&<7tqRF8SRmOxrzPp>}I(V3q&D z^BSZ1sJHOEAp6;mR7}&NxJ->1ZigY=l^@h!wX;oBuQDwB6Mlf%3lHYQ#MY!^%HPNF zuiz4P2^v{?kCJ9Z?3f;rXa)}J<2-KM4PiW9R%}6fVGg&_#ywekM;zT~I}>tj+?%F) z=t^VxHv$!zYX1gU&E~ERp^qmR(CtCP;T!AgQY>pq*!djbFOl90~#@19r`d??Uf)h(0DncjQmP2x5?O znK3RdhgmUeRZfSYhBYf{F+cC43#9J(AK=<_AU&YVIgDlD)72v7p#C1Kt%H5expU08 ziC>cPgJhago-~=b>UBKZPeM)oq1}9RC~y;*lVWL!*b&|-6*`Z!^QIvY2Enlc89;Z4 z_1+9Tc}!<%10|b7|J)0)U_i3=ydY}ONoku~#jr9>>^HY4x6apZm9+5`-9`vlV0ayT zX879Va0gl1T%wL29$e(x!S9~4q+^tsa6WRvV4Z3#gQRsYZ9NbDTX$p|Vq|WV4kMQf z2En3xXf_f2mpk2+Hq(W>c8Jl#9#;E6youe;^b_(uXKC|3Vw5X}wY}Z~w{FZ*$w&cM zzJ1f6lTXESYEjXf!`y184e23|zj0z^Q<(47)-}E=^R-0D zpZP%<;3pB;3w-C?_)`9?o7UARD2ko1~?C%J)XAxFbAgj zn5~~x5%}JD{|!DJHAtv@4dn^{y{8v4JVXDMnk(nug;|g#e+xfvHX=IQM7(xQrWDZsIUU$2v0`wIMPle?jA^LGZW~dL!EzMw zu_A|Jyb9^MO4imI1ghj}Qv}Bss=CIT-u~%IxLuH7+F<(y^E6=8$~toZ8Ob+2?yJ%Y zG8u)FND%Ra&I)@%P_o*y?r9bqKyH3Azavg4mgIo^jVzX^vF>iLM zTzN8^+#Dz}F0=-RXXf@W*4IOdMHF}anhF!gppPsVPx5pztA^CFO z=S@}$;QARF4`_2ZZgkS$O&|Uqwt1m4f=b6`HojV zPd^V`P7ropLfbT!SI@fde$CRIDwfLAw=%hF{_Z^NCS;*c;+$*2 zFBTA9pAt;8E0jGhXjU@=3*J{>C5cL(z#hC!Pz><@=c_C+!)%Zr%_xLA{0Ej^<&r6&C~msOmq-kE9j z#630^>y2Fo8%P0fHew~UD7isai!eKEdVoX=R9&#K=&6)$eO1WTwc2n!_HN)jyO3rH zxTbeBTZgu^dCdwVqX4v4>Wf?M{O|58s#N`;a__zQIe@0rKThznLv7iGMgeMbyRTk$ z0icI0l@X&}fY=KRhPgbU1_v>+Sj8umYppb-#q!m6Rq)9>=A#094tsy$7&w(UM7umV zkvmf?$tY_4&OjUT2=>ITUs13^mCa#~lhOWK>nl~3M@|?SfIh8c^f{VJvhAAav0+WW zax3B?cvFmv<<(YO|4pQl;q9@uXx53wLM2Z-MLZ?~)#F}|OIhn*Uj0h!d%@!wnnX&< zZz(y`eVgQ4`#Lt=274l`_;t~SweB^n;^m!5Nq6>{<}PA%L~fQRCjtL;bbsy$*MdSf zso;yCUR>9|vNE<7YueI6fKz`0o>Q6nU~hnvl-#%^u6BC(lAlxPJ=OlR@mf`B-PQ+7GB8y^=bH5SDkWePalf<#LdN^_92BWnPoudu zPVq*o)49QVbayPxZ;`r3F*Tjv%AWOq!54o;4$$VCY&Kdlh>yYeJ+s)_f%Q=nju?!C zdLzgBV>rPJWRQdY`v0_~a%*4Lb7#x@bIXWy+xwy)be)J+piz#heCk><0*{liR)?m7KgOJYzR6+NC}kM`ShzL%;4!22fYM2S_4Q(mR;h{J+Y2I< z-5)ptULXo(zvwwIKb!n6`Mh7?4-kL1dWZfkpVtsLRa%nwEsThbNOKC+d7G+dD>IxR zmPHp<@V9Iq*u(97u~(U-ia~K=BezlbMvKqyms_|TmR5fm;Imycm)x|qX(fzZkpVNm zq@nujEt$oK>z5s>ZdsRRkJGU$zy9_Q>gH{ROlHosV{IoblUyO4+5PMeU006*Cc3+( zYyM$VL)OYw;3*B>_m|vGc}3HW@RVhm1C(-U_|?V6%Ot((A4FNBz$2T@sO2o9lSf-V z1(QsrpY)@~k6HF^E}#SI718pd6*jJ+*UKu-q=(IADke3@Tk;`-lU_VGMCI;k9n0|CR%fS&&hoIOTjHPAZ4UfML1awT^K*IK2Z~xH zh*k-y)^?vrvJJ8FBh{;fu1EgTTZd}5;n~WwWX4~S(;Yp4oj7K37MOeiDu=Jtd)v*p zn}wJ8sQgG(X16z7IFjSw4>cEasM67gKxOS&V!lhpM3bi@OX&B=`|mX$!=~YUq#raA zDkN{+HqWJC%r8pR2-BmI)zYi2PSK)9SDWlcB4~=i;&NS}x7sO;=*&bDTqUqTT6H02 zUgYI$JLKW3vF$-80(Du;7I=+dQ{Og&|2PZQiqn)*{Oy}qk#OWR6r@YWyhE9rIMu@yJoYSF#)DFSG(bAG#K$KFW{j# zeTpuwJ8Z_bez}WyR8!YnC3}tM=WXrgHvkIUXKEV>59(VI1&&m5$e*47x1I^T5Y^Om zec3yy?t+{7FC(aMW^c&dNz4tR9)gOe{lR&=iU-I24I0m1SOVNTIJWt!tnBrOEmmC> zpt@SDpu4I9b7lVCle108jG%+e(S2Tzsjvz%>nGb zd(3l1Y(no-5TfaF7oEPf?RV#`-bf_3F)asF6x~RY&id2H+X-@T73DucXue>g}E3ap*GRjvPJj*f;k^ zY(6W7nxxK&Hg!7dgh%=xE^CKGhF9D6V`!{H!vOCdr(mL~e#m@_yJiI*SubpN9FW=YJi8S>gaY3+sYUnOf`+EbQY z6yz<^OA*|T1qih7qWf*1&KagA^O59BkcgIKwm^@>XI@xGv+44EC7(*C;y47AYRGK) zRXmvKerKR{gRNo>bDM9EyBEv%i$F61#eDv2#yK77AWC6TgvaxTSl2Mi)96(z+lm*Q z`3Fx+ftg&s_-+wuJ(#!PZ*#IP(<5otVzIVYfRh7gb6Wu6NiU|Y+p>Mqsp44W`@q?Z z+FVLP{rQp3>(t?|ju-UCpB4*?G|e~Y;BrSl0;`OhT70T0+U8HMZSxY<9|O@qAg5Wy zR##8@@wx!JL)_P`>Qbth$F|cE}bU;;X}vP$AG+5(k-dl z+J2^ij_iHIQ~n^j6lhH+z5XdE08jgSf-+J3^7&3KuH((c${pGZ4{1B$n&N#vCvR;2 z6$Vs}la!s5BM?VcRp2hU`5fE`2XNX2=|TGhPol9gY=}x6?nwi#M`AyYP$^4ebTRLU z%>iHu4Xi>};-y<5=-|yN|9B_t#eiO|e+Z9^d{E7yo$B*~Eq|%o?pLwdJ@l4zod)>V zvCoQ4hpY5u0J|ItM25UiPFK2sN-6nFM#X94vVFN_+5^2w2l@5la($Ug& zH&*rx&;1^uoyQ4O_YJ&Ztla5~v90~%Il~clR~NA~{x;SxJf0X8zKD7CiS2S{w`K>W zX&}7*vYB;9y0faUivLa^P+7DVDXJM0M;!JW<#zCRjVmJC`pB*<_5=^OmIjbyVXE*~ zuhwH<1JzB@!2>vi({3zdT#l#xr$jw(sk|G#6I4td0p7YRN#z!F-wWLTanMSr$gu|f zqg9A|3?>gKl{@uLaZz4dgeEcZ)t%G8usO&Ki@>F>#X_1Ger^(`27R01%$2G1LUpRM zBN2~bm>BA@GrezGG$XqJYF*C6OA@=sV!DCRpk>PKL=Pdb&i#W3w-5ZHiT8TPKZ?;= zPM=NLnYz4ki3U8{wSDL9qD+3JoT~m+y&Z?TFUTED~L(C{pJ7OJUoLunROl z5uc)T{p?O6MX@P%JK@i$s6f9*{bJ&5w7xe7P7Ge%@(8@^1t0z{dgU8EgXys4PWlO` zE{+P)yZce9>is=ExK|WP+XbRZTWZ4{j@DsFLI3hFyxS4tzLu~(qp>7rG*yFob|NDk zE|{VQDc1~w1r^o@j`%WI2I#2lOZHqGNlv}xz2~MZ<*pzv5#9<;6}(&&Y$)GKlQTEy{?Q%hL%_nR^X%{n{ zKzMQOw= z65N0fG)>sO4eJ4qoPTC0`uN0MRcfNphV+B^9Y;^lIMg$U*s?3&9t_CJES13?uO9Ki zV-Tz2&Q+Q_jJ1Gl%fr^@9f6?w40BmD@UmeySk#4ywL0~g3!bYPq2}^;Lin>jY?pNy|ZRT=L|IJGKI5<2k#wvj%Hb@0J#YqAf|88p9*xqS?R}z zoy{VbNG73482Q*RKI@9eq2k~+W>y|0O4C**LDowGajruxcHtgdMa;QV3jqfY{$by> zMx&qEJYOgGvViSWe?uE18cCKLp^uF1mi}AV*?`w5A?{UWfksa1>-J8V%j0S)=Kq8# z(A~FX{nfp}s=Mm}!>jzaBm!c&+su=Uo%h^Xn$Y$Kv|;oTrWYU0T~Z9nx*|iD?Pebw zF8M4ewp)^!2}CVN#VgP%S08|oAA7n|l3r^unDZiyCeyPJJ|y;nM`MI*S@ zlfB)MSvgfWrtqRv7-_MESF-KFRU|toKS=Mjo?XuCXDdMpSfRF_Pw?@dZ>3DHWek4C z#~R0YHF+Ns)Md5c>vTP?)_GY2f&vntS$<$CX%_xMd0&Zs^@At#V%h^7?189DU+PwS z6h}EemSt}GAJOBYVNR4ZIi)pMnLhPK%Lt~G(0AB*@{xQ2zzDu3#Cvbld1-bfsDpD> zRHB#IA`*36RT02h9c$0t1x+bT^VQ>xB{CMky=y@Qk@pNVm#iMNZ8K?X&ra^%rv(2c zm$LRKVftGVhR7Qhf|bnA1pImHkttgvqZ}AGdH2o}7~nr_mJh4LRt&<1DZS$_r%ako zkRq!S2w)4~dmePjBk}(DZLt35!dM51v75Iz8SM2u=-Bhh10MWb1Ao3&oW)2h;g;g? z{q&Pq@aI#T9E1PG0$ei`$V*swS{dBU@UC%sv6#dkxfsiT6h~I5tILoh z@H8LDmZhu@PU@GlbRD;4n9??`$Evu$ihVK?Z*{jnM0u50v^_FdgnkJ7%K0cm63 zVH?gu>ed0b$}_Zt`^_NP*YvdoRNj(Hv)IKUKYutj7gb$p|4|1q`%doK)!*7Hy{PEB z_fr4!m>~dP8p^ibl9c>&%h1$P`O)WmU}v_P)jfI;V6ikKPHpans=SK%QD^u$UksRZ z^H&|i+t?o~Ip)dy{yZf?{$oNanN`sL-2HFX2d9b+7^%6Zm4<;4N0K0zTfqRX1-?)& zc+ko)ibIl1C%xOvAnDiiO#x;?f8>74r!#Er8TgW(t^Q|US*@?w<2A$)$?v#b1uizd zU}r>4GK0hbms|XAB0T@MV59&4ylFm)ycrK4Ls8lim>FPR-b6aTi89YC`d=TEPwq7!-pxkLSieXbP&C!^qW<>+#6> zyTMx@8P=+^y}u#<9Wtyxn)Pwzl3)oI($Xu}5eg)<9nAp8^>y0KhkcV zfcjq140R`eTzEc9YT6D@$IC;Yu_TcGa3HYt8#5AXZI4F{%?v8Ef_kXjA?GZmH686C z41qtW#|m78($2k&!w0`0!St?s&(Bj(D`28eGjk=E*YCf23Y(f*UxZPlnMbVSMYGWL z7O|s6ca|}j#xOFA~IL$rpm(1|w5=iii( zP*zLwg6k_%ZwtS}$sW2#!4|ju8~=DA%N^ir0nGSztCcoTG3#sm6n664o&WWWO88X< z-xr}iQyr`l*C)QJ(3bvP5Nv-?UqwCEZ?9>S9aOZneG6%CeXyJ4g_tdx+MYTle{hDt zrk59PsxGiGtZ53~JUuzp2&@SKocU9VnNS*p?w{|}Z)z^G2FyY+#0^BUIpf9QlHt^2 zpjS+lcOU%ShtgXv@HlL?P%&v9Sdno68OsM;FaB8Pc-Xp#`@X?Y1=8=if>{NJlFx}? z&J(vqPMi@t{fqJ7m+Jw&9H=iw6Gv7wzxW@9jhn>WME2mYKMyrXKbp2G&_Mv^nAilK zg&Q?D2EDconI&LhzqR+#x^!hns$WH0fr5K)G$@)LeOvW*2Fk5RM`TvWRMBk!Q(v8M z1>o85;;p#}=-rRQcwUD%;K&+9T8Z{JRIRrGbLGBnXn`Zk@!HDkhcg(#CRE5waFd>a zPYbI^(hff8gf>!9<$%r<67+8wpZr9tfl*1k2#lI%R zjPVDfC;XUl;C)Zmrfa?ZH*jQ4%je?o3M7t@jrAUqtJu?~DV=%MM?LoQYyurK14OS7 zzyk;(#A-icsytB6jHt+jxhh1@eXeHdR}t4d*-r@1(kO4w!Nd=&eF!E4sGpFXN50R6 zUstKQMx4G+-kWn+b{Da^VTs#Us?xkir3f-vs zji2!!N|+=YkCGAY@w;}A!pjabl!d%Kl9Vcv_2A9vcC`nNU#B}Ai)$v&7!`WG*|l9* zK3_QyIKR9=Y!L+qtCM9=5PtQ&aZQ2DbLIfl6@M74)T0n?2C+lF+@aKn<xK;}2RZfQIKx);I--<%K1j zqF`|ad1G$U^|tP9PifJbnSq6;pYsEM0TTsaOuO#(Oo?#R7vvxxgm!%%;=nA4`HZ_t zx5uDPNS^BtYrv+IX-G)#7`@Ot?Vx^EWS~FSZR0`3ahJL3;0;cOmu+JJ`4;D+awQ<~ zUe;E-?*_SRXm(YpbYd?J_6AEbfaI`Yyr-ivyLHah5rJ+OSg`_{eC!TY(!~NJ2*cP| zhaV5^MPG0NnmJE3N368#hOf!iss+Q$+gG==)q5S2aouF$h|_?47NcMJia?j=x{&JLR1Ka zZZl*-zs;Tk?5tl0H!HgQ#owkq9ye%>$biHQs_JD&8jyN>rZufFk=9ZfW1yNDy|$u1NC z?Y<*yen-JYMQ+Zo1H+2v`r#356=}X*=ha`(y;;Yf8SZ9_LQ@)cGgj#=gIvZ#*0#AC zssXr2=BE_waajJa`+4t$^?HcBrgNX;F>mJSFyl87lt!t*td46xTNpGW_mYQ?YdG{DF(y!l%ub*@$q+=WBo2 zokD9P0QpG!FYzXO*3)?A$^-oLFP<PIr_pn(Fe%AFGxK}2Xu3Q)T_sCnV$SP_sto8Uxy+XmN zF-E{aW-4b_)vzZ9ty$w-M`*!%VUq_F0mgPj;Zcu~;FBQ$z6mJ!f>=Fh86hNm2*3hX z5p#=qA{?yyH&Hh}aq*KoVCN2smDo~qy|bXielRE)zkCfxg*==rvadUwUeFlbS0o;C zqpnK#>PJHywr1fJaV`r_QjRf5E3j-O>e6Kq5uk1rIFa5 zOa;$n^yNi9BK79ro|M`mG6BeOoku?z8}q4nQ-xB<0G9lxd9Br{6FUgiMv|7PVj_*X z^YM+WBGo;~w?17fo^?D6h#U?C{OIg2=G=)^&}E0_&~_f!~VSDq$= zK4>-`^)Aws#s%+wKdGK_7~Oug?ur`{1R%#kNwyE2N2SI&2m|r$%V=TzrwC$O0Yqw*-_~Rdl&~266UNDd06j% z*gGIY4s&J<0Nu>fNz3kH`8{~4ph0T!u|(#r0rqy;tYxpq*`!_C7IEcdt&1vHeux39 zQ@&7-Csui!3z#t<;*2rgFqX9&v&%4PsAEiSeR7qobj8EVf5pIWud7}vqC?w zgGY!=_@26`VCv?n&86TXno_mgi{QQOofrVQQKV7$b1ZfELGM>2s zzoteyt>7kVMDfn2#Y5Ef7**?+<#3GD_wBb^VyR-DXdU8>JF^vNa2h`8J3d1E5beTm zce+wqKmGeO3(Co=V|> zn6#cJjXlyBb#?A~^y=E3zZGS1pJ@REpk7ha|Fo2zOC;2dJ5jgU^K`o0*%F9T=g;Dl zb*N5}@VGCzn=CSXVK*zHWfSwCVI_^M?RvPZm{FRe|djhW#bG5oHmIXGy4~u zMHPdd7&VDrIrg(?#9OpAwew{5*O4lVg^%doN+kM{qfJ&Bk=9 zy(WlRw6j;(#0}4Vbl{<6o zIm>TV+%Vm|((@kxGHe)4d=P_#3mBOc0jT6kEvJgK{P(Xf6@b?lOngu?yk;F|SZ z{1FzP+s%xOpAWhD;Uz$X^!5t$A=(LQY+C@cas{^?mT*n&q(4t0b@m=}TK9ymxAphC zZ5ZIdfr3E04>kP{LXm)lDGqVAHoRuMe$7v+@d6&A7iyN~#N9FA|7S@o=^u6n+I{H@ z970hZX`>Gp@S6%k;;3T&#TiSYG(|v-y8OUASew}tJGu&L>9~-6h|3OkJ|vC=^q)Hf z#L*p}kY^=Bz$Hshf@BicppsY^QGJ;O%2JTa-<;eoMPps)AU{IiU~Y=NHKsU!H1?rF zxsqyJ2@VOv>gGA=Y( zMHM4vRcKV)l5!Fv@tzyy78 zar4UMe+rto478@pX)V}V&`r)L)C^R1n2-`zQiT9Y*BcG>iD>27;G&uJy8D#s{F@?XqHaz&*c<#LT zxyi!|a!BGbTn+q|VzCPOcl$DWkTRwis@v!)aXO!fsAuD3$^sT-WQ2QkaIPYQqt5|M_ zC9%ffuC+%#2FtupZbL$KTFQ~6LU!F2Zk2B312=Xwu{m(Mc@GUF6P?ejohH}@^sDKnmHD#vPHdfLOlQk|s9G!o}`6>#~}hPYI4pnG(qV4TEU`{00P z-s7GS-9m4L+9jS~9`vLdr-T!SMHYMpSAxd`W;Zzx;N5{N&PHGD0fGWI@*6Pk#PrB1 zxt!r|?!kPzE4JIm@2hthiFB5u417PsfIR3wJPt{M<#9tk=#*>4b~1p02bi1hit-8a za({rHrK`}How?V!^_aT$HkYj%xCt=6qU3XD^(h!{xd?j8f)n1Dj^xFnwhtNvoLbAk zi0V(1-b)=7IUk1oNuz`G+Eo@+{K9%RUyYHKwKq;7ALKlMPl^nJPU` zN^M4!@>on}HNmFZbpX|rcl}}NjS+<((t?lxklX*!-gQMa*{0i26ai^c4G1DCAVt9- z5V{Z$FrpBOGy^D7j8Z~Zn)J|zCKwAy=pp>lq=P_^js$5UzYw}ol~4rE%YVkXJ#*HY zS#vYF%34XjJYRXAy7zvMlYGY-i)WSr>glcPAU8WUUOG#>$v*78Dp+=4FEgV3pdWPc zOcAI2PH2DKqaPjKM=f|n`d&6-){ZP|uX!=H2x=HM9~U(ppX|m#TW{WzAmVyb=4W@O zGADU$*-nnCz^GkPZp)}(L|nV*oJE$ejLd}CvkMxz5LU`Jhn>y-zO4@?91of8cIk_) zC*wWymjb%Q_#FUNsvOZE#wT@q_NOKYmeC+s_9sGc{_K8mGuh8$iu5>1ZWCxoYMv51 zGs)n{P^~Vy&F%HYCi|KEU!D>B3D6K;xZ49G=T=^^uM+z`FvZFO*qK5s9)ekg0i>>rWTj<#+o$(kW8u=EkI*maw4}4Wgx5Z$eN2*2hks2 zQ@P9dN#W&4P3f_Yh(Nt393`L&Zr%Z67#lN5lcBgYiSKwnx^*55=N4~CD%?Qlw zOA<(t1}<$;cr#Au;oBGp&afIdKa zv+Awh^hh0MJPbVm0&$2^(;dDP;{u%7V$>eVL5B7svr+$1}L`5m7EY90Hxjh7Sqw0RPh=8#%3J4Y$|FenfaA-gu$Q)s!S?>3%t-lY+H z#3D?_Jrz#htm=h>gt7<7RwE&bKOs0$Ijkm61`wG$OTQ{PkH@P{g-wS~JF3BgQhG$MpwkeJF$`s@DI#VcdcIM zdpdhH5C_~G=)+avcR{L}DB6&gy`9VZ$snf#C?m){>r-hN`FxA&rTixMwD)5qJIVeg zxxDU|&wNxmeJdUyVBHMo&hAPa<%qSEMmA1-2e$3-yu6(7MJDc8yx6@F^X&kKG2eYF zN^MN3-^5;fN%`n?L|-!ppinZa_J1HhQr%zgtG#qWd>DF{YSd75LbiUnA&b_`^rF^J zg`bVoR1d#+-Wb|rQKRt|z5r+!n7J9w&$!y&iF_D+Vz?TJ3SsxQ=bkgY6BZ)76ijde ziVME=rAgqcAEyhk2QkgXpefRN*c6ci)$Z|}L=T3%B*A_WjeD-|V`y`d^PtCqM=G4Y zet{Qy5h=K!?Ubf>mNT2H<=3CCk@vrv*iPJk5R4E{??X5|=emcDF+5`9xknVG`}ClV z%oE0?^H;i504;{KnT+`S+Wpk%^X2jtAJui#QFu}rT~ENw)iOc0g+@YwYN+K_+jVI% zirv(?aUxmw9avgu_uyZ`lv^>)8vrSe8T8NE@#2x4gMO~eo(G@^11%;6{sqeAn3>E3 zkuM%XGfQ`@$~^buZkbSGvtQiVS5^S-ljs<3o6_4R@Qt%+())W--1NE0$aZ37XVh5T z_plSzijH#~yn2<^RR*-@hv9Z%BUW}gfVw+X)ADYT!i*c%CeW?i4MvQ-Kc-Sy@Z;4h zRxyK(`b5uUOe~!gnIHG5b=X}4w=%?!eogsgf0w^#$z$@KNJ_)>tpCZ$B2)+7d0R+4 zY@70iY9~@8-agDpNU%9)0sPI&8hu1ln zNfE^7dYXxO-oWb{v_(d~Y6_H{$}*2<92|FAm6suXSJ62xmuNods+|WnI{2VKl9;#I zvC;iT3N;_+sUyNQO;|{pd1$BLHd-40HGxntis>8Vy-MZ^0vrMQ1tDQYx^pHX3r(QV zRz<|Jn+~J8lRsj|LA~M$OSmq9ybNL>uGeiDQuUglewbdm<$>-J``)thGPv$0MeAM- ztfje1wEn&!>n;PaTFs1?v(Ry(jMPh^$MW zK#QicSYW*dKT@Vg<$PP#pb>tpR^!KNLC6llu%}oiOax#7m31kC4-lA5ofnJ1ItV*l z2W7xNixFXJiMq~KImEE3MP<#CPsK-{7^ZJfuo2PCBDmLgxZBa!V(O;iafN?FHq@b_-^0MEbRm#^uStY>1Z#>k(Z9o;8cxq@X&Rw zQ{49MkFhdQ{dussM*|SnmlCnn-2%$Vc|tSJNHlDyKX>tDl10rXU}04^kQPtnuuq}z zcy>|~Px0HAk6-!t?Gl2wY*|Q|lF8cd^N2gQVyUOr{|p&UA-K}QDU2&NQjl3VCf5Pe z%@USX`L$E*DFb^8PId3WZ92Z{`*Q^^ZDiE~mV7D!5uo78%;88vF*2fyO&2-q=5j4K zJWE#nGki?G^8UIuK?of(@K77<${3OqU)11zEg3~S99@CT6&7ZI#~F+N@obD%W-c`U z?b<5rSxau00I?Sv{}HAVD$~O0qCxe)4%$wEk#371r@!;q@wsRRH(-G%9_rekB0KtI}wAy(X9!Z##~eJv`${cjS<3=wLeRCLz}{M zeI!w67N@r!=755V?Pb=4YhyGZ+Sl!i@cN6=4h=-HOS-H&87vHwcXmw(VL0~wH@>1qoT6StYpFQUZ1*-D7ng{0Z4k?(w4g2q}z?E zBeMaf3#wvK@xv1Iji!(0{0cEWLPENHprJzt^t@FDLI)=x{Wl zwPaDip5;_UbEfMlyW3W1*p}LXFP2Z%U3FdgU6A|Zbv4dRS&ts639_?znNj2V0j&cO zFDlHl9@tZ#o@%R17I;@*xN;3RSatwB2?pv}l?+4NzOU1~j#P zvV%z`Js7vDL@!kM8E}Np8j4K|L?_*Goqz&xLkSMtw;h$k&*Mx54U5=pL4)DvkchIn zTJaAk(Oi#K61WQly?`edK>QcC&Zg=O&G~!cf(pmkFJBLW;#>-8~ z5iF9c^)~j_4KLd<*R8*pqy;+Jf+&6)uzwc%@edWV@_)aI**|O3e_n6x zzm|e4@c+`VvrPXjf)`+g`7NU7((+qG4`uZ1w}{@NBv@&G>FF7(0Z!=uzAen_{{8!( zDZe3B1ux`(EAtJRIQ~AUf9H9@zpL!&|4jbBzWZ-6;9qyp&7ez-iDujxwI~R9=_79F J5U-*`{|83_VNU=6 literal 0 HcmV?d00001 diff --git a/scripts/make-og-image.mjs b/scripts/make-og-image.mjs new file mode 100644 index 0000000..9ecedf3 --- /dev/null +++ b/scripts/make-og-image.mjs @@ -0,0 +1,18 @@ +// Generate the default social-share (Open Graph) image at public/og.png. +// 1200x630, opaque, brand-matched (brutalist cream + violet). Run: node scripts/make-og-image.mjs +import sharp from 'sharp'; +import { writeFileSync } from 'node:fs'; + +const svg = ` + + + + GWT + GoodWebTools + Privacy-first tools that run entirely in your browser. + No uploads · No tracking · 100% client-side +`; + +const png = await sharp(Buffer.from(svg)).png().toBuffer(); +writeFileSync(new URL('../public/og.png', import.meta.url), png); +console.log('Wrote public/og.png', png.length, 'bytes'); diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 60b241f..ac99e88 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -26,7 +26,7 @@ const { title, description = SITE_TAGLINE, keywords, - image = '/icon-512.png', + image = '/og.png', canonical, noindex = false, fullTitle, @@ -67,6 +67,9 @@ const jsonLdBlocks = [siteJsonLd, ...(Array.isArray(jsonLd) ? jsonLd : jsonLd ? + + + From b9af331e6fd3ebf7d821f83727dda2d2a795d66c Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 11:10:21 +0700 Subject: [PATCH 3/3] fix(pwa): brand the app icons (were solid black squares) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit icon-192.png / icon-512.png were near-black squares — so the installed PWA icon and any og:image fallback were blank. Regenerate them with the GWT brand (full-bleed violet, white 'GWT' kept inside the maskable safe zone since the manifest marks them purpose 'any maskable'), add a dedicated 180x180 apple-touch-icon.png, and point at it. Generated by scripts/make-icons.mjs via sharp. Co-Authored-By: Claude Opus 4.8 (1M context) --- public/apple-touch-icon.png | Bin 0 -> 3044 bytes public/icon-192.png | Bin 408 -> 2875 bytes public/icon-512.png | Bin 1488 -> 9052 bytes scripts/make-icons.mjs | 30 ++++++++++++++++++++++++++++++ src/layouts/Base.astro | 2 +- 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 public/apple-touch-icon.png create mode 100644 scripts/make-icons.mjs diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f4b1a25fb287a2e9e62c14ba47452d3e350aedc9 GIT binary patch literal 3044 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 literal 0 HcmV?d00001 diff --git a/public/icon-192.png b/public/icon-192.png index 965f567285a10dafbf0414a32f0321731f12818f..5989d6e0916cc3743284b743c0f47321429feafb 100644 GIT binary patch literal 2875 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 Lu6{1-oD!M + + GWT +`; +} + +const targets = [ + { file: 'icon-512.png', size: 512, font: 0.28 }, // maskable safe zone + { file: 'icon-192.png', size: 192, font: 0.28 }, // maskable safe zone + { file: 'apple-touch-icon.png', size: 180, font: 0.34 }, // iOS just rounds corners +]; + +for (const t of targets) { + const png = await sharp(Buffer.from(iconSvg(t.size, t.font))).png().toBuffer(); + writeFileSync(new URL(`../public/${t.file}`, import.meta.url), png); + console.log(`Wrote public/${t.file} (${t.size}x${t.size}, ${png.length} bytes)`); +} diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index ac99e88..2515307 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -93,7 +93,7 @@ const jsonLdBlocks = [siteJsonLd, ...(Array.isArray(jsonLd) ? jsonLd : jsonLd ? - +