From 4485825dfec76566a2ce7ca4daef81a05b29edae Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Mon, 28 Sep 2026 16:29:17 +0200 Subject: [PATCH] feat(ui): add a self-hosted Material Symbols icon subset Icon renders Material Symbols Rounded at weight 300 from a subset font that holds only the glyphs listed in packages/ui/src/icons/names.ts. pnpm icons:build checks every name against Google's codepoints and regenerates the font; a test keeps the manifest and the list in sync. Icons are aria-hidden and untranslated, and directional ones mirror in right-to-left layouts. --- .../src/features/applications/tile-color.ts | 19 --- package.json | 3 +- packages/ui/src/components/icon.test.tsx | 38 ++++++ packages/ui/src/components/icon.tsx | 47 ++++++++ .../src/icons/material-symbols-rounded.json | 105 +++++++++++++++++ .../src/icons/material-symbols-rounded.woff2 | Bin 0 -> 37228 bytes packages/ui/src/icons/names.ts | 111 ++++++++++++++++++ tooling/icons/material-symbols.ts | 51 ++++++++ tooling/package.json | 3 +- 9 files changed, 356 insertions(+), 21 deletions(-) delete mode 100644 apps/web/src/features/applications/tile-color.ts create mode 100644 packages/ui/src/components/icon.test.tsx create mode 100644 packages/ui/src/components/icon.tsx create mode 100644 packages/ui/src/icons/material-symbols-rounded.json create mode 100644 packages/ui/src/icons/material-symbols-rounded.woff2 create mode 100644 packages/ui/src/icons/names.ts create mode 100644 tooling/icons/material-symbols.ts diff --git a/apps/web/src/features/applications/tile-color.ts b/apps/web/src/features/applications/tile-color.ts deleted file mode 100644 index a0a117fcc..000000000 --- a/apps/web/src/features/applications/tile-color.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Deterministic accent color for a company's initials tile, shared by the board card and the -// table row so the same company reads the same everywhere. -const TILE_COLORS = [ - "bg-rose-500", - "bg-orange-500", - "bg-amber-500", - "bg-emerald-500", - "bg-teal-500", - "bg-sky-500", - "bg-indigo-500", - "bg-violet-500", - "bg-fuchsia-500", -]; - -export function tileColor(seed: string) { - let hash = 0; - for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) | 0; - return TILE_COLORS[Math.abs(hash) % TILE_COLORS.length]; -} diff --git a/package.json b/package.json index 0f807132b..6df3340b4 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:agent": "turbo run test:agent --concurrency=4", - "pdf:translations": "pnpm --filter @reactive-resume/tooling pdf:translations" + "pdf:translations": "pnpm --filter @reactive-resume/tooling pdf:translations", + "icons:build": "pnpm --filter @reactive-resume/tooling icons:build" }, "devDependencies": { "@biomejs/biome": "^2.5.14", diff --git a/packages/ui/src/components/icon.test.tsx b/packages/ui/src/components/icon.test.tsx new file mode 100644 index 000000000..84e092a63 --- /dev/null +++ b/packages/ui/src/components/icon.test.tsx @@ -0,0 +1,38 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import manifest from "../icons/material-symbols-rounded.json"; +import { iconNames } from "../icons/names"; +import { Icon } from "./icon"; + +describe("Icon", () => { + it("is hidden from assistive tech and translation, so ligature text never becomes a name", () => { + const { container } = render(); + const icon = container.querySelector('[data-slot="icon"]'); + expect(icon).toHaveAttribute("aria-hidden", "true"); + expect(icon).toHaveAttribute("translate", "no"); + expect(icon).toHaveTextContent("search"); + }); + + it("uses the filled glyph only when asked", () => { + const { container } = render(); + expect(container.querySelector('[data-slot="icon"]')?.getAttribute("style")).toContain('"FILL" 1'); + }); + + it("mirrors directional icons in right-to-left layouts", () => { + const { container } = render( + <> + + + , + ); + const [back, search] = container.querySelectorAll('[data-slot="icon"]'); + expect(back).toHaveClass("rtl:-scale-x-100"); + expect(search).not.toHaveClass("rtl:-scale-x-100"); + }); +}); + +describe("Material Symbols subset", () => { + it("was built from the current icon list (run `pnpm icons:build` after editing it)", () => { + expect(manifest.names).toEqual([...iconNames].sort()); + }); +}); diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx new file mode 100644 index 000000000..3f84857a7 --- /dev/null +++ b/packages/ui/src/components/icon.tsx @@ -0,0 +1,47 @@ +import type * as React from "react"; +import type { IconName } from "../icons/names"; +import { cn } from "@reactive-resume/utils/style"; + +// Icons that point somewhere are mirrored in right-to-left layouts. +const DIRECTIONAL_ICONS = new Set([ + "arrow_back", + "arrow_forward", + "chevron_left", + "chevron_right", + "redo", + "undo", +]); + +type IconProps = Omit, "children"> & { + name: IconName; + /** The filled glyph. The design system reserves it for the selected navigation item. */ + filled?: boolean | undefined; + /** Glyph size in CSS pixels: 20 on desktop, 24 on touch. */ + size?: number | undefined; +}; + +/** + * A Material Symbols Rounded glyph (weight 300). Icons are decorative: the control they sit in + * carries the accessible name, so the ligature text is hidden from assistive tech and translation. + */ +function Icon({ name, filled = false, size = 20, className, style, ...props }: IconProps) { + return ( + + ); +} + +export { Icon, type IconName, type IconProps }; diff --git a/packages/ui/src/icons/material-symbols-rounded.json b/packages/ui/src/icons/material-symbols-rounded.json new file mode 100644 index 000000000..75ddabf03 --- /dev/null +++ b/packages/ui/src/icons/material-symbols-rounded.json @@ -0,0 +1,105 @@ +{ + "names": [ + "account_circle", + "add", + "add_a_photo", + "arrow_back", + "arrow_downward", + "arrow_forward", + "arrow_upward", + "auto_awesome", + "bolt", + "bookmark", + "calendar_month", + "call", + "check", + "check_circle", + "chevron_left", + "chevron_right", + "circle", + "close", + "cloud_done", + "cloud_off", + "content_copy", + "content_cut", + "contrast", + "dark_mode", + "delete", + "delete_forever", + "description", + "desktop_windows", + "download", + "drag_indicator", + "edit", + "edit_square", + "error", + "event", + "expand_less", + "expand_more", + "fact_check", + "format_bold", + "format_clear", + "format_italic", + "format_list_bulleted", + "format_list_numbered", + "format_size", + "grid_view", + "history", + "hub", + "import_export", + "info", + "insights", + "ios_share", + "key", + "language", + "left_panel_close", + "light_mode", + "link", + "link_off", + "location_on", + "lock", + "lock_open", + "logout", + "mail", + "menu", + "more_horiz", + "more_vert", + "motion_mode", + "note_add", + "open_in_new", + "palette", + "passkey", + "picture_as_pdf", + "print", + "qr_code_2", + "redo", + "remove", + "restart_alt", + "restore_from_trash", + "scan_delete", + "schedule", + "school", + "search", + "search_off", + "settings", + "short_text", + "sticky_note_2", + "stop_circle", + "sync", + "sync_problem", + "text_fields", + "title", + "translate", + "tune", + "undo", + "upload_file", + "vertical_align_center", + "vertical_split", + "view_list", + "visibility", + "visibility_off", + "warning", + "wifi_off", + "work" + ] +} diff --git a/packages/ui/src/icons/material-symbols-rounded.woff2 b/packages/ui/src/icons/material-symbols-rounded.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..214a6694517e00617cb27dd9836cdf2c9a409b49 GIT binary patch literal 37228 zcmV)OK(@bkPew8T0RR910Fi6}5C8xG0ZP~a0Fes-0RR9100000000000000000000 z0000SphN~>KT}jeRBR_C0ElWYL=gxIf#ghqwh{}1Y5*{T_Y46x0we>1Y6Kt!g%k%M zTOTozn*r3EwDvHfs@B6ZlCxS5nX0O)s;a7LPYH^F+*Q40S(as4mStI%Wm%SGS(as4 z77;PPz|bCIVrq}Du(Ze6*xC~u9PKGCuJ#NMPkWA!ue~53&|VS}YOjchwAaMM+8YuQ z?JX&(_Ku89drwZTeW0MwK2lO@pQxy`&(ze~7aAJvD=n?|jgC(HPEW7>;K@__#hbVG zhc934AAkPZXW+OV>%4iuUH|&2M^P9iEx6s*jsl>AhyS_P8!XWPnF6oBAt+_S?99l? z2{~albj{0;qNhBqynjb2EgL8i!{*@K~` zOSkqQkhZua@DAtR?`@nb2s!<#271qL>OqoWl~^`Vo;I7se=jViVi) z#)Sbt$e(?O0sVab<^S(~uOBe?Q4t-pCQMQWPpF8J*RlfOZH`fQlil(AN$=~;hbCBM zX6d36J1AHPCnP6Tq=G|5l^ojNd+&2#5|FlLGfF&644w1I{ccOf{m3sk;u>MD?eNxX z;@g`W7^fq7xj8{j5E7*JC+C0Z?N85}Sm!q|V@^lluH$)PT)xaCM`31j zha(sjcO6n>lG^_AFW;E~yg&9{AXljlMHR`V)yrL~O#W|N{S{`G=)yS*N^m5TjCl9D z5E?Yk{_^AoC=*fAwsuQ!+FPR%nE#bNMfUx$zkCPa{rm9}mEgf_cW#41nyVC&+`v%x zL)!eWHT^+yw}Ar7vO{tJq~eP70^={TfU+x8K{2kY1Y4edod0eSVuIst$-cf$MWwaX zIkQRMq5dS)@2j!)2JEA%dqcX%nuZ*fkb4~lXq7vsaw8%<&;IfxG^L-;&5oC>fzfVDOSiOlB@LlTlX8|+dI_8W4l8(jHL*W` zkQ15KAZ4c|IKH^;TV2kK(sf?{1MBiWNvNO52@z^r5*<|~hvb2E`LGA!ZnV>!7^)Dr zBsgkffA6Q7n4k)(%gHWb(Dg07``1V0h32M#Yx!M;(f0J1*sG#V%PMH?+k+f!90juZ z#=SSvAJ8NvNH<2X8LuuD#G;W6OR`AWNY*6sU1xT)xE!&uBtj zQ->VVuDdO>ZJzz*5y?(~lexu(a&1sD;vDzx^+9e+s`t0wewy58Dxk3C5g0jR==lPw zk!Cq#u1=0QKkYZ~J}R3h`jVNog4e}Hshfkp?>@M#@ZsB;11Ii{*!Q7*uAfD9f9m`a zJV^#wlFdmLkh6OT?Ojt7B|4-Eu_D0gW-EdZ9LxW!ngS%mBKc0b>Qd_HO= zZ%=)DCQnIs>v5OvPS<%NB@%4FCkl_YiCPu01Q~MjL}-a-+x?_26MW7`piBl%}Q9^;ea_HY;KzP#~DDsCDgByfiC*ro!WSCNI^_S`9i( z=YNn8`=&DW593W{+ud$TITIbme;H4#|4eJIO{$QRKoSD;?axn)M2zlXrR;Su&3FH+ zvl4uPZCNq|C(!~@_7KZ`{Sj9@jt&4SKrFf|0dN3K_v+-8WGBuipsC1Px1n#3a|Ikb zOuT(!2$5x!r91if{dw=x0~Lx=TIsw{`Xb<=x%7a-G-ie5fHaWsoLrD@a^~OAYYBy+xR0A28l))tx9P`FRfYRFJ zMsh>Qo%@txQ^=)DcP`shw4MLnRI9WfM9QqCcK56w=PD;D>TI?)I^%-mBH;bs4dHh; zsQECIK0s>pK~vkHG&>DYDmAH@?gk)&AgK{4j*&*=G|sQ{YEWh%b2?C(-Q#E-b;d=e z@-;p>Z=z%_oFKa_wvi3u=<3XgR_o+S>A_@;F-C|%Kj=m9m0_%z7*_jhuB zOH&j?Yv0`sVpBuLCP~|E;?|UAE)5X){mGL2;7_vk+F?7Wz|Nm%g`}b`Vrgv`(ow^C zb~ysnB5{C6eb5orzp^D&C)~zBpB6NKR>(_QACxoeeiYQb694}IonEngX`f5Ibl!*3 zA_X#csxy!%M|$s(m~`~-nu11!Dr(fxKoec*PEYzU#4sa_GR8O)_ymMR#LTh4A_XNC zH4QsQ7dH--tdkOeC8`ARxuVAVaVUFYnu) zq)pNNo+;yunTimnUewbcl@0AtXFbmZ2j=W4ZAz90sjNdXWY^GWsNB(UkJ; z`ny>dv&dA=l(H4C4TI<7$PtlIhUuN6}FFZ+7@agkFN{V|l+`$@rJy!)QlGAX6#mGn- z8`*1)IY%X4a?L~@o%+@him2sv z@eZAobIaC1PJmq>8ktAcC|#Cy)vYPHkB-p@#%z3mPDm*wb20`Q2wU_#EiWOM&#Fa1 zBCQZ8tOZ4?^vBa^hY~Gh6vkizBKZadp;Q>cywY`aKrmggEsAw9 zKc^4%&7Px>W3mi2Nk*i`FLOqBAkz(H$Z<5=z}1k#2;Blo2);%lf(C7WklGPg-Vq`U0K8lDu7j&RMS(TT)-$ld|))ddacIApE3*D>rsKud23L>vI0 zes@L{p$Wus+k(K!x##oa~# zhBzY=Io3&`Phi_SB(xE;_x&3h)L*1g`Ge0y9r^OK@Pj)14c|x?e1XOf?ioR0mI2D? zLzPz8Xcd8%Ojtq^&iDoOh*Rj=dx)AK20_xttbhReOgV@p({^w`9rN%EB9Wa`gg3A> zm+6PjIU<5~s*nW>XWk}d6*+umK@clns>TT&5oXntQyH|}8s#YSK3a;bd2$jF3n1+B zP$zp*7(G}3X3F$J=Zdq?iU*neNgq9cMIEM6 z4#BeV(LP8dq;aiqB=WQgJv=H(MzIxWVi1+V07DLm9=Eu2RC}?Yoy}CXh#92PnmS-X zfj`<9c+S%jk5tDiQH>yiEDdM`tn+8+eBg?i zjA3fI2nK=1!WRh8p^t|wBnM>y?!{b|f|7(&LDB?2+F=%G1K&`D&jRI^cLQ^qrH&HN z4Ma>ks)sFzLrAKummW=|*8^k})0%X^h+f#hW@#CPYp1V}#H9?tJve20VZalKN^+G` zs2RE1SZlyBlL4^{Grnyr21PXiNl0~xWpyU`f)vEwBNu7Sa@{b@`<^2YEk(y*vq5OzFO&mc z5f>=6d;cIUr1+>xJ`TuD4r5frTt>zsOTY1>m(rRSKsXXXhjU*w<8UaPvqU@qzEM~) z(QFK@Mpg<4rddufF0u~7U?LmKWIs8LmTl`1p#tzkKAAMGHg8m5f#>&b8W?AbdWCPa zIF<&G4WD%VlXf`ZL^sWcAdT~qPXd|r&p2gV3Z}~LdrJ<1EC|W$aG?(YE<|B@KRh4+ z%#cGA8>v2B3n<$0utyWJ{O23n8ZA5012bU1NOAHpglT+#4tdCq(>&)=ECWxIq446N zXeA&ESs_>e-+^TzyQF~W5OAOp`+5l`$j$5h z4HoFjTZ-7o;7Fary)46CpKJPg1gxCPJQ4Sa;J@Dq~`nUD|7FbT7; ziY{yfd%=EiVO$)Ke>9%ON9!3ilV{iC4k946*X7MYm8cpupb0dEHU$TQpC|Em#=FC3 z{AGNUeGL5n0|1&|f3A=pP;8fZD4L2dxaY~Ou}vLKBn4M1~`R&~{AS4WKub^6c&wHc`1_^bRUesQ{)Dtp&-3sC7h0YF7o zwklV7i^^`LUz_Ak>=vltDk@q|Bg@NAzxvHltX;BpS=+zcwUkxMDvuA=JKLnvr9Z4T zY4Yx~>+NO{j{%6M#V3p|KPANd;??KnCyW~LlrEdw;>rho|6ULsFfcJ)v`)_~qoNLx zf02udw`##H)pOY)j&qtgm%sQuB%IK^Oc-GA6Fvy11h?#$6O(GvPmR=-awB{uLLt8j zd;O7zo9eHgS1zsVPyCh6Q}xrrT8{r~9LOSUXT{__5(WkK?oPt=Qq11_p`_$=nsma6 zE&L8MZxpTfn2%pVb6henn?(Jn4^JCR!Ukjm-p9Z zj|$j{$m{rvGLsJ&D4-Rv!3nH15C8^@zza<#_JGjqLY))|c`RKVBlS?YCgk4k{~r*F zp%p7>#q(Ot|DD}etadP(2t+_Y0NjQakMY4wG7y0Q1V15@@HjjkpD{h`LTP{U}VJa2Cu6_F*6(EJ)l5BIgKM zP6sNI&YTDmhs-3h2WSX0Q74hliN|F{HxUZ_v0(&+cu3O#i7=HY0DkZT5QWHL2>ZNH z$g^$-K?V0ZWHti}LL{{i)k2vuK$!qNyaY&iISZTW{;cFu*NSMfa1YoRljpfH20-U^ z3Y6ABwPMvVJAQ5Kh!Kz&nW;({uekvL2qoOAQv>#|I|VRXW;-4wdM!vKRZAq=gN~^_ zTCq_y3wEyg`P1dJXWLS{Q0&6%PD3J3w2?9gpOJfX8Hrn@ZARXPR+ixHv{^3hXrnhYM z^B(^{r1RI`k0_Zwm+l+?EWtT6#qm7HWNP^%mx;cIj`O-e|9%u-&)K}s?%^A9HiO0h{IQmb&{(AU)J%0CN@9GWC4uLvp8$-j5`%c)na*`K zl1V&MC`QS4(NNg7SaxpI_H@aTpeR;b5t9-5$dUI#oHnt#q=f=?0PvIzF@XcG`! z;v2OLAuG)mv<9Me5wZ5Zgi=735f(Py2Cu+S1sbXe;29A$@`aGD53sfVK9cEpN;gm# zilLLaSnvkf+VE-;wVDEEcaE$1?C0)^T`LOF$y<=+3ZyMa>xgFkxD9%*E;J*L z=kb*z>9hadLNrM2;LQgA-hv*2k5-@Hpg^Tu$?dajacW{F(nY`&&6UWxMuA;-=_4XnJyM#o1bN?`(hyWurGoT&Cb2S*htff8Euvq z`rE1SliX1c2@@1F$9r2>^f`HCMdq1l2{uIE76aAX|Uap`@V1(V+I{`-Q zfG&35G_H`gnsfn9D6%LE=b#Z$f$(4IlNDxs+4?XAz!+T04HJNor~&_SX|1`Kc#3LZ zQFgf{&Wtp+$abts##YsY0vTe8ijmF0=lC;>JOJ%8h7^ihO$9zuoyx1 zBLp~bNToia#tEaoPL-E1uFZ8m(lr%mlWBTPF%Y zQ`Ce`s2K$@OgNB$Hl;=rR-LI9L6FKKp5-=fWE~;)5F5hMph`>ULRn`-;Ex7zQh@4L$g>7!>h;}h)-BG@~#OSU=H4bF#*QFdZVHq{= zvb6-{yM^?gobCY~c#&Bx_qfJ}nrT?%RrB)!qLCmH-Lg=^hX35mnsd#l%gd^1OogIi z?0zDbdG;5hMEl?x3i^)xBr_5eD~qKorYGP9*z`Rc$d)hOHU4WPpX?4NNvXMb0l7gN z%HEHFClg@Yr4v`kcs@9cLcRCXyW~?WO%1Gi#F6(_#&yzb+#- zH|Z9Q_vrB_t_nUUh{fgOnEaL*s%{;gXmCos6VP$8^|cm8u==E~L%gm{9rI zwOHSa>xb25h`J>N6`|TQ(!@=9T{ITe8J}~YOiJ=meC3WaR8<`KFJsLf} zNi#$4R%_5Agl#---QoRjl5XizVH=5l}i!+v6HL zlh&Fv^z;tc0S^TCGIzz=nmD!8(6(naDCs7~?`s$;oGTq5e_Pn#OJCT=#-=zs8ftmf z$v7||9UY+6(0Z9`GSpHz*VpKPq5tX)Oq=L}`4f5lmv{y0|BY;iG8Z&8LDOcAx3uNP z;S8ujYE$jdNsY-%gJV0v8-hzD7?Z~%1fFiPXuJdRX$eXFIh&H!?ioseQ{4> zFJ1MUPR11ZP4qiAgvF$pT1bXqCSNY+cZ2A_zFLcn(Z1qtm8FWW!Zh4F`=AdvwxWLV888NvBB1rnwMVCm#svx=gU}ub zF`z#I4^+a*1_h@4^4!!i<2s|G^@Qq`8VYIk5+)WYFR0ehZc?~aOQ_4Fdq3#9D(|A|DOY zHyhQ6dhxD!;JuILZ>nJ2AH8@pi0+AIePMb)%DwskaMe(4z`$SWCp5G?^wrH=M_$(~ z4fSUzOL%!$vy?6XZ?UfF3^!RyeqsF)b>UHJwB57vF_bTRw<=0;Z&(&g4BJ=eZ8F8U z%gu4YvN8RPpM?_o?GB3l)@|X98&8xXvnZr+|E z`r_73>&R+jX^kk#`Ev6+(Hbng8pPT7io+Capc%(Jjp2?QxGZQ;8C|wyhs*tyT#pdk z$C%%1J)I_&!a_%*@31tZ!c*u5%O4pj2)ThNn!YW7T_qh)->IH!~j_HK6cE?K#cQd#L@@GWmAdjpQzkbhw*Lm za-)Jl+^X?leu2!DfNcMhio1)Fr#p!LwFcH7x&D7c-2VSS__>mtL^&14Zj^Em)ey>B zs%S&GBzeW3)}%i60_nzRd%{=o;veZMO|*b)3@JlT)wTXu`7zi>LLuDT6EN0~(5l0? zW_U*;;(`w05(>J4I>O6u_E=UJQ;kGZ%LuORXW)6}0eTcDNBe!e{sJ`X%4LYKJ^$L1 z{S-Pv-4un+!AC{2ZGgnS^{EryABF>dIRFo~dGF3u-^NsrHbNtXPUl#stSS){u`wDY z%h)cR=j6e5${FdQ*8m0}ADb6nomkE2OEU_;)<@;g6mZ;-@m8ej--Zkzn|kC7&y5_9QdK~uW71}9g`nJXVREMvEOjGZpK-or zmm+W78W-=)yh&y)z)ak~%XPu=2_Zpou{DHHig7_#{TWnd$H~e~bf0BJ*4`qIl$1DQ zd{k@|C1CqsyD66dmbKqeKo$&YP`Z2l}^By_=Oipdx0g7#9x%%FSIyi5^(+ z6^mEVvdrPRD^%v%wUs1WUFj2!Hu`f7hsx?Q5!dEJ<;8wl_CBiUSKDw~#i#6-^d)Y{#tfnFDSkh}!*EUXytU8cxpU9E z*|eHum7tSBfDJC|`*AfWnW=)aK9enKC&bM8&wcT0ZhQCZ%Kq$q*Q8AB zF+#{aWSME~Ow$-m*xTLD^}F- z<~6ndvNAi_zovEVys=$>Us(3erlieN67LOJv7E?iJKk|(sG!a#_R_k!zGGad;e*XW zRIcu)eKwz9T^KV|o?3E3uWpfFpbZYi;HHg+NYva&sq6|&_KbDwYvc`8$%+bSQ>CR6 z#9h&<*)Wi<8qCJM*djy<>kvXBA#~Ns#7=+UfVV^LZfpa@j*wx~qgZQs5UX1tw()lC z@aZ`lMy!!R{=hg`P0*>vAVI_5N$VztgJ>FV2m`%}cy}lcg%+aXaO9)IPCOoDjo9I9 zMLxQg5lmIi4okQ(7aP$*icu$jTl+XJ!Db&uF$Gs;r6EXvzP4Sw&T#rqvNTz=Z)>h!NrGxm72KG$TgAwxkODUuqyBc(lA#b zgO%RJq5qPdk+@*$pBZM%Foqc!1sdMKz_$++H3!|#N5?7n&twCD+lj57-_-=l$47Vl zzx3pu)Y;?TA7=~OTq+g>FKDaT5#QTZ(`Kny5Ii5_)V)YbS^__5x6YjM+WK*2SLWsA zwOIr=JNFCv#)x^+=L8)0)wHaJ@w)r9%ev~v>pBD6O1dAu(p@r!L?-?Dy0vwyE@^IA zjB3fuT7P_vkZVnmG|Y>`orU5JTd-g}M_T3%Q6%25SP5usRNv?PEL!+CsMDe2h7pd* zY-!1~8e4swAAj$OrAc8Eb)7~=fO~Pz!&gqtyZ{;==NS_Xks1J3L129Eh2FtU7$zJv z&UuxYn&aX_6OLk%)niCMpSb#@O~U7{nqk9yq=OvU8M!9%w&a?~Zu16#c8o8SJrqlM zo-a^5WGi{%lqJ$8U8%o;Q&&#RJF`1)aX_?xbyRQcs=GL+P(i zPfy#5X?|Fd2BRPGKyXkRbyxCyC|_`(lW)OXy})~YjZwyZ{$AjurtZ@0CB zPi@eUAgPwIW%%iu%OSr$r@3)+wpq`tXAbb$iXZiA#Q6>i6|u-F7DJSZg{VM`tXS{q zw!)#Ss~&roZvXii8)^!y_vf&#*^w3MJ?Zw>t8Pr;)PJfK!I=Sml>oa}O0YV$(i?~M z#^FfV`40T$rb1Zf1Ai)SHjzVQlUU;b>;-z@VhS;7%v6Hl>4S=fFVg2>PhroSi85pB z1!H-8<_a9YG@Z}Q=Dc&#)wOZl;sS&}lT3}+h9{9n*vc@eRVc6icFx{EFnE34ZC3!_ zLczDteBEc(!q59w1O0QR8u{k6MFU7y7>fELkI7Mdq$jt13@V(4eNA=kc}!+*TU}jl z8m*mIs9Xa}ZFQa9dNlGRP zyJNonN5msA4El6?Admf|fMORxG$!P`1I>Y5^}UJ3k}wW%4+)F41+mS6O+a)W?sdvK zmjUB>$SRRQwCo-&lpyP3zb7ynK#$v~wH5QLCOj?Q)r9w4EODL<*GV6Em7ARO&mP63 zsnfKz*q`Rgo6Pc%D3hihl3%UvGJ1gXI!vV=G9x4We-Pc>|J4y*SZa#6P*1v4QS0lf zs9j^rf-xf58>=E<|6Ap#-D%$8oz&co{1&!lyfLnBo6Mb0{{w4(UOYVkxlZxISksR< z61qp3pCMmSI40DOL83>*g!++&zEBN8N!E0HL{#xrd=+0&NMOb=C=l*o6YNp|prKKO zJ^JNDz9pzvZ9#9;pf@_{`>+N3oE6BivbO50ohY449ajdzHp_}UOD*hk_CaHI4r-^5 z!4|A=SD@`olHm3me6wvs8Ywe3Z}FBj9t;q33ASgF`-CB2E}xx0qUectg%8eQ$<3Z) z+f)A*qwdyiY+Oq^wvV%t7~7JWy5o&%WcJYxg6wiBEfhv%wrZ2s>}i`zLHsa&a+I>f z-Ibn6Tv!-AAc`Ef&@=Oh2ArkrWucPiZ*lafxuvA5i+LW6i#bu4(18F;4Z^()&4GALP=m3TUQQIPO$ylf^2C}7B+9SE1|T|m+Yxo zij=1^lanVq#d~Wk_@OUCMQhpn(xxZ4-aNb;N1XQ}G_=rfA5cE(Q;E+hB_yxO;N?^f z_GP&OcwDF*3qYs5r>r`KiR|&CFVV4xOV<5ll`dD} zzNNE4NF-1cVO^D@S#-FvytA{s#~rFm*+Y#=k+M-=ffxUQ18 zb~EEmn7N+XDjrjHf9KP#H);{Chr>;sJxOfr4BO4V*@hkxPV7ZRHI$Cp#NM_~-YP?0 zE4tZlfW!pHRd5hXOQ%t6KW$CcRog6UCXVyTAcIB4G>qwD5YsVY zVltzTb`kVHVWVEV)$leXhmIQ0iHoAe^Hnx2F3IrL+6Izo6cv?S)k=kmN0I?jqwt90 z@Cc;lfsBA`x_hpBvgr;LD#Ysnm6M=34-hjZ>8?>jE!n*3@xYl+dgQ36#33m~fozu2 zlt}6$u;U7z&uN}fo#8oo{mez-5l9pKwW7flmDtH4zI?I|ck8f-*zB@5sw}einKOjo zX|rjsxK4OFZJJv`dB|`tN;9W#J|SA4ZorybUS8OIL#U7ar2@V?AamPEB24Pe?nt;x zdY?(VasKl8UXql}c`pXD`V; zv5naMZzRb6Z$WCnMxdBaNe#jL)OS5$g5gZF3v{oM>02L8b81>B=IJ1C3-5FF545%X z32yu@3bgefs$TO?m_ojG%1xL>CjXzTwD!OHC=B?ka3YO+suFpsR{zE^Vd^76{x zy5g~To}1zzFElDEvop#)BLU>)F&)k5-MVkdUq$?KezEnodx!`|x(g32_ z0_O!6KrogT2NBJ{w+k+j`?}NdAQvrx+OB|w)!3HBDj59!j%m=l)PSymY}C-kvPe(e zm%gR?UaNP(W}AD-lGWHg&^?RK?<=?jac4I-k)7W^Mq2;=fXh9;K6SH0t&^LoHxyl8 z2UrEy6mDv{i8PJmouftRTov5~E_B7hyl!xrjPi29H%EE-JUKMTsg4$67tdV@QVkMS z9k=G`Fq={+9kngBxt7`hZQhJlTTJa1Qq34{83?~V01C<^me_oCVG@Iw2F+I-4WS(@ zX?<2@;i+J5YYP%dz=oC2=Vl8XeX$GVp6&*LME-{9P}((-Fl_ueP{T&_C|%0YBt{T~ zP`F{!tyGJyeKu20M@N{dG|$ta+3>oi-gk22 zOnQ%w)_C?MQTCk(Ysh3lbwy5CEN*um8(Ria#{?U337b0%NkHOvJROb#@wx1_YMnzGJ1Nx z87AdL`=4~bbX&JT`s`6`whUT|5HW?`-rxgIq~9cj^(|}Hcmmh%t?87 z>Bn?H87m#(UR`EZ_fvG3qmQFkpJ)xkeZtM!J_Ux&_$HGN~PNGPkjX%Sbl1F zX=D>Zw{wD^M#yD^N>e%?CcNAGj~=*8RoU_E#l)UVBbyeQ&V{w6*{Q3oB=lDufgb`82f%&L{`+_ZXxbS72uguSa1ca9zyOGu ziFFITrp{HmSHezNOb_%91O!h4Cwu&Ri?>n8;{?DVa|TlYdOfCy_-Z^@2D^&@K@8$K zI5^mzSR5UjpUn;qjPuKXt%H!+>i=FI3kjVA&s|o;KwS6kd&QL*0ffUmDK!^SCO3P| zd6{UwuKtWRr&oRTw&&5fmqFW8-Rg#FrHP-0x8iO1tHz)IxiAJ7HUa|j(YU;k|G$MR zI?5G5MtBa4{xgMoC*Vl$w*zWqg3a~$vgp@X>@|_ndF0UOHZ<9W=UBA`mg~P&Vw^$z zRbx=bN*IF;8zBSr{JfE1#^^QO18=?SBiM+DN!|DF{O0Em`C${$1RxL{C3tsyf^2Pxm#Uzj% z9n@9iTKmG)z3n9-l950;;~37=eM~2|_y?M0$fS+;f7$!a?YnptiR3XmO(-$!<*@Uo2$ShuH<&rw!q^DRU&8eGGJF zWEK<=Ic_N_7E4MBzm|*501R=A|9VeH3NG(pnk?_vXuU3de;H zCqAb1-szeafFSUzfoLuHoM)*RjeaQyyy4znfS_Ummb|p~*0e}LO9)8{pDg4@K7=}FPkP6QVD#RXgp6%V$X4Z!~^>8P4o`Cta!R1Uc zi_H9-Mj(6uBo~qj$sve=3^`r9kf*Qr&)H#>`Nl)L!! z-)<3K)_(qt=CZUs8tI3Rbw42U#})rG|MA5B9)HGJCx$+=sppBpsD2n^rEeY- zWNsc51b(w?Lz8<2x6c&&kt?WSe@XE^Y z*?kT z3!XC~_I&n?O=<QK8W3#nqeCW%y@MT8K%$0m z?j$bnLrmZG7ssosfFL_>8!AcT9b~TpjG<(kJ6q(~8C$<~3`!u2!!;|rir^gGImi!I zUZT}n*HXG_W!Na;SB*PQOz?~^f&`hvtFI+C*Ntaxc|yd*n2Q5FzYV*MBu z#$_-h54ixn5HH}F>-9KK+1zHPvRsH-#0qC70Tq-D*Un!kM0&USuPH= zu!~LB#=?)MyURYcLsgINY6gi0iSzx-O}Vi>+Mr-^2`?@q5Xt{I_a6xKesKPV_}`T} zb1UA=sd+SKWwm%Qg;9qu^NKt(U7FO4X>)v9Y?qg*wH}@QQv7h4`!(2n2{fRYTtm#< zJj23UQ3_Nu{X2E^@|-$fuQhIX)|Su=P>=fa=kXXd`t=wOMn{80bB|q0W_rekKRJ+` z(n9$?QC4rvYqDGoXkpi66L+hy@K)B6tW)rcuiw9T&ZrYw1TZ_iUr=W|JJ7GPxDyf;;n7*APav&L)H|p0K^Hf14Rx= z8S;lg>zsog;^{>Xpw8I_93*cScakf00Pu%%4|0k)*xDc;Thn3>au`09_!?&hTMofH zG<)%lBV@UOB_>fw-*`mR{dgxMPBvkP_3`708JOGx=Xx?w4o7C*H#ZPl?;9&Tj8*A! zdjq29q_xxvltYSR=Ou$ndB3xQEEM1`zuMShdjO)sBg|4JzAnZ^)>Js%T)JB&vEG48 z+ltLtT?Kj>Y2)hqncnHwZezTjFt^0V)uLU_$8=B*gXWIr0+{r1^Z`a(fkp#|!xd;W za3BkVBi7-Whwt<3S{iNpzYM5ciKp$_K?+u12|-cYE1?Q~e`?Xnhkxhee@s;R2LS)y zIO-H6suBsqn7zL!^&jww+x%KR$6kJ|Zfp}UTn&fE58?C@P1 zD2Gr*tg6Gom7M27ra8o_N+3D}4Hc5fVRCZfux z#6!`ib0~IN6&~8QH`^`fKALN3Kbq+k^d3!r^~S%l3on2^6|rEVB)@31F=JC!yqOT= z-VVV?Do%}LGOI}O#9 zYXhBQ3C)on^~GTWGT0&SY*sp}y(MUWiIci%;|NcXv!jAh;uv{sgjlu!nk0925%Z6Z z^5+@5!F*U0YuANoHV6*! za81t)HQark4*~!H0001BlfU_o|Fa-PX!)zyv0?Ck^bNo_uoJ03&Xej;Ul1qy1NH}h zq#q&!h;GDvbcD3P=CNbQ&sZJG6;4k*$4huSyl;YtenpfER)v|YF4jNgiK>!JNIvLl zK1n_ye_`G#waS0?vN}V2rYV$c=(xHmeZT%s_#ukd4%0rCbelh=n^v5)R-b8Kb_5%7 zoXN&ySG)^2KoqD4OTnd+wbP-q&-2z$EsVRmx$eF3#{Tb;N$SJp-7|d zMEDrvsAnJ`W8{M%#Q!p)Vv-Q?-yJyO+veV(L6*Zum@p#$hhBAh(3T(t0SN!?1^?@f zZv2h!LI%9M4t`%Q=>+>|Dg(Z|#1%-TQf8sVW#>|#~qdq0wlh*r~uGO9hNb{b(^TrK*VDtn=g))f?Qs;valzHzRN7pnsu|Z6v<85-NRTr~YP(fG?}US@ zIIFIfCfByn84&0y0E=K8>qvm#NgvB`sZC<+tJQ7rr%xYz6=r_;>p=)t!xd|_-BXZ$ zaP79cb8VNv)_LPnJX1Yb6wyitFL&{|H~zDqJ|d~3(as_!A9m>NL=jrbJK$Y`wcDLe zQIx#va8Z=t3Q029-QDPI#Q4JvH+FZ`+*}FDEW)X7t$Kg0psore(euB3(F=5qhI0-e zTmisyfj8-mw~ue1)>wf+8kyrox*CTKvT(?%cJcU{U zuGKD24(=-0y0D_63Qcs3mr22cBgN7+O$q?cfHNA*g~(q;+)SFLc^NHEGfnFnkw%Dy z_{jKpt}$#RG_H~>%VijoOLSHpT)pac@Vv}5|hj;%-rKRAa z#@JdG3aFKmh|DjeLalll?nO~p+OF1G(3x;^b9*;Zw$%?YrOxCfD6N%B1;4`q_#eK7 zZ$Gvmkth!agF)KA$YW`KXc*DX(c$p*y?giWee%3C`~<`03q!BZ_?M!j!W^tBCw4~p z+01X`7i)64?gen-b8;B3*x)|WsfSMoNs=Ukr{Sl2b8)=5ISXhe9jDT?wRlYZJCXb# zUTmG>K#P0O;p=x8_>Y7bLvSKXwt`WQo8_AxH!*Kdah4iDh&S9j4e^>XI64pri< zd2-?lzKTpxdy;iMi#=_-FC}BgAq)}y;?_KV)Hksaq@+;$kod0stLoftP%CAq?Fb<@ zBp@r|%-^#j^`Ok?xE}`6?SIsb5|ZK>LHj3i!N&PH0u#W!T5Aah3y^v$JmGLtU_NSt z4YCfcId`Y;(V08#pMvkWwQi)Qc8y!qhLo|CHtE_+F$K0f9pW>MJ|)$u!Kzwj+JDQF z=7_i$kAhO+p@IP^@+^yB%3E4-b+A(z+%%1ot!b?F%^VSGz{mwP0B#EFaTpT>0V?kEfY5 z%>l)j>b{58jP3M2T2lY1puOW2%q5nBm{PPefbKnWcB$9eEx1onLN`#b-S)x(%*}R2 zYul8LN2j62zwjhG0BaIBQ#fVIi zB4LZvnQ=W=34tw4X-P+-2-{rgGzQVurcjwmG5aVgE3kMP9~NiD*|zJ9P3I1)UNpoU zjN}fB6Z78l9hujp6rF4^xgM=F4I%KutMSF%IOFH$-O^*+kZn9^ z(3q87cUXmVNO!IU+pynY(du};NN5y|E|)V$9@v)lwZ2=wTmt(12lpq}&9Uyzfs3Wt z46KXvsDP$!0d=*&`2(1w*aZJ+>iwsXSipSOa-i>pEBYvD!E-5z1DfUsxL%XSJxiXS z#*ALC$>19QNz0*kOLyYVO_f!kq+#nw?!@uWzlpE@**cQqJrG%>xwG|~@JM}MA| zcEM(hjC?_VUy=vokhRwW$rEvCa=pdlJP70BfFmyM+h*!~+x##LmVq!>{|!KupYGvh zWq4YfBR4O>%gw+sp}=uNv`X(#4A-Cw;vm5`3I^Iy-|}M4m-`b|9P9e|t__~rL-+gM z*oc+0Pog_^f4>BWuO0z6bC8z}VwRj&C3YW69m`-aZXu`W?7^#edvlve8TBi4EQEfo(KV~rarMk4Da>uVbV?aRnT zG+rcO9`?I55HThJw^Uef&aLpL;nwopW{v74zvZ{gco823_%-z047jh}2|7mzv@j%u z4vfk#=UfFnnnnjkluKF&HBAJbd_^hH1m%+_MlZUUJRwF}G|V9EEqZhj7lYf{7hOb4 zBP4J7yxG@H;sPqpx>rc^Q&alsg`{Dy3OT5`)Fcko<=;%BzGnZEHN|5M2i%OcgBQ|413w zP>79oJl9$i1|e&-O^uGfI{Ddgp_6%<1KIbo6?8|1ajJm(?XN4-t#{#W>y!JiovC&w0+hQw;~J$u zLcoOB&5dQXtj+7L8oObL;@NnXQdneJQ7MKQ#@J#uRXvm>ls=GJ+EG0`lK3SjF{69V zq$*AkP5?sfV<*(U?xGoEgg^x8cX>J|QNluwPxhMz>w;#7DSi$dI8@|o`=`iPKyEd2 z{L5`q&a?ymkl;+Iegvq)^=8+AO{Iv~C6TW%x)sLIutNp`=sibY%iG5N;ijRm2MCf< zDj|waNQ{q-jZcgeB0vUSBNc+fVsC3}Yp+ncD27Lp!ix|VZOCcgv3TgarmC74==40M z;b34UKsF!`k31PYCj>@htPvZV3=6_c!#MJbyhh|A6nm&plzTh<(E8M*L|t%Aqp<-n z*q_LKqvT}1vN8GF3(i0fRxW~E@KAoS4uA%W!Yi{0%QP%B#F!>N)Y@cOR?S)91VuZE z4nv&~%Lcg4lHeY=veG7K|2r*$13-uf7Vs!MuwB<*&2Af}%pufGtxl)aaG%LPoy4z| zFG-T@s1AjjJG>d}(>}*%PaZvb@+=;H9F0c)CCfPHWibh9pJ^=LC0v0_(Zv`>v|UwI zz||J)ZEkFC@9d4I%3#AMB}Pg~1}So5o2(k!xV~@poY|drksE5uAtp^Xt2yF~F;>gr z7Q=3FQTFFEfD4X5#aXw@V7#hogsWgw6RXx1IWZ}3yS(#2!2)Pylf}~=)KbbLQ1k@5 zr@6E!n+79?iHLLwm`1RTx>yzxU{kJ-&^iz$x)CN$LXqXeS#SZoZNAM7g$wTFI|~T> zQPR5$9#iyih3aldtyc!%81ccJ*f|7Nl`54|LE}AeB{=VP3O+iO7UiO1oKC%c^Xk=` zw>cYOo($@yX|&>L72H|(4v?cUWn}_Qgzk}c$I-M#HJfy1TyqheJO<65Jo>Egxb{CkclL==J|bsN+&d{Im%X#_%A%T zda;+dC(L*}ZW^G7Y$#d>ceb{+c6JXQnl)=yd;gIApO*j_t*xxAtnCK9i9Ffsr8!5w zMzzW~l6JahZ{GGbAHd%sv9D1fbP)|Fs9Hxa?1yq@(LbSQz`EF8d2_FxH3QqCKed=;rO<&44Rg(Ua{>)`h%FJ32@OEZQIH<=6gJTW{fBAo zd4nWQl;=$qah26zkOCOX0uTZiVJ=}2CmYvoRy$6CHdYISaF31g)B-q*SGn!J zdl>0vsc0^S&itf69WnqGR#h4b5vKDD?J=F9KpGXOjlI1y1RK45y9uy~#CUUaED-$osDYk-Z!1aQZnUaF zO0BhxIyMWwZI!<&i#$uGL{$k`Sj@|+G6rf8D3h2hvRXhuR#bs3<7~8)9I?Q4hE|t&0^cTy~gwuZrUBU~_($!P~B9 zQ&0cZowfW`%mh6?fGT@#IBQ10Ynz|kYopye8AmL=l1!8FUNA(&Sa8g63rmA1L&n1W zWE@3NeJr{+^F1{HkY@&)o9A~2q%VjkZ%x6yOHe^D@xTSzPv8HC)t{5~N~Kb%H-0n$ z6$y*2c?3Rz&&HjC+N~==F$#F$QHbeKL`UrfMggSXb5^n#<1Zi#w~&$;Rrxw#!PRgn zC{PRh@HN1+)!2avn6YfgXJ0rMax@-~q~Pj~^ zvAX#^t0^i8VWw!b8Ud@S-|I!AvFD84<=L+mgH%vC+QU9!c@oex7nBkar(gpeN_QrV zC9_QQIlQ}muhVL^5mGAGY)&hgp#5sR_#9FU|2ye6q?3_kLhA1ou;uTc+p{)7W%>Hz zzo$clK!n|^j`7_#l7x3k|3B#7yb%eXE$)Qlel+nbwJX6q$R)UVmV)4BnCy@4iqPxp z8U#>bc@H`(LSIRD*83MH7EPQ9ZzqNRe8j8X*6M+7`x|P!RbXsA;-aYc<)3m!xm?Vq z)9KS6fBg1SqtTeTbGu)@M<^tyvA(fLN9~ePFo&?O@#v*-uGsNe??FVuW#-Q}{7 zh$+_C@7-pz*$#KD3Y*=?UtpS~x$667?>B!$(Dic*rE4bxRTu>5#_78r?DwuDu@{!x zDP7;-xDU1@S>9%h6QeII$*^!CExFt!j$he>&=KOCwb{m1?B_hn36fVKmf?O@e&Jjd z-844>*UxN--zmQk`SG{@z+Y$w89s$0cGH5CK{UVMao|c^PjcV9j@O}98EV@Fn?fvg zc;Gg33#}ul@e#0dxKXNbEkE5{1ImaJA*0>#AW0_gV6ZgCU}ot4eD* zaBbJe?I-NgTdHsBZg*8OVx%v0EW`Lwni`qUM6Q$^1rriw$@p0SSfE+9ZQI1MY+E-- zJ9tZ@ZZVxsrc=&I?L+QQUrh$%vaqbHiBmsGHfENBM{L*~4}rv(l;)pBDN$waIxj(8N9K8% zeRov0BQnk-NWW@F#(4p$){1^FheNCxj`u@1j^nxZ+O?6XmYzu5Y}46I;<x^i4 z?O~(wdK!(!g$sYhaUAttvJi$h%V37=M)6nE| zW(hns%(rZv{*bjQNc|FoWb4jFvs`H(Saz8gWxfA_w3jRoQ`So)%I07ojC!dM%0R?= zN!qfMv`6xX}es3?|$6{d8nFp5if{h^yuZCu-`3oRiw#PrWFLr);Kx z%Ce%<3{&A1c*gYPHU~bGw1YDBrWJHi2B*Fs!!3JnU4M6?{dMkn2~?)$q8u4<$sPXf zfLs=aUheJ_&3dCg4w|dz`Q}0FeoSy%d3b)O-V`vL*3eOT7dsTyk!Cxs$xQuO`Gt zJjTgU!$D|0Qr!EYojLk^H*!)*%Cyq&b>U1nd73j*q&?C zmQiqUoAV%v)}V%L!%2=RAz2LX!E4|{dTSLqd8Qw%50lbF&{*3vVJark16nJydyo`x zhpH{3NCZpxYE&^-<<0FHLZ!&`2k`(|MIOp)m?G^%iqek4c0VW9hU?)Dp_Jg31uMxp zm#F&CGZyDuYRe+oy%kOGz#c4e&v>vI1Q)|OQ=ChkWn*xTBBfDGovy5!Ngx74*8Ouh z4^Xba+9Yv|tqD17ihYL~lu;miCur$8bw9UUf z-d*$4!lzu0Zx*vCAIZL@bGZoY%q>+)Lj?DkxA7JXs~M5qgRcLx0rp}HX;es56gN+6 zFwe|5*FbG6)3$NJV9@KuLp@s5sqN2W*!Ky;Wa1K-6O+v5fQp-W>ft9AtXv((zE6iz zh9VVmach*h`TY@>3n#^Asw&#h*x8QBz@@fbsXm@iRq`c9?&IJ`t&AYW(iYp~;jn7S zCmHWnmq}vOQ~*lf!1OuXB43c-o6veBy!k^$a4s_IzPZYW6GhslW<|LvAuCG zycDMmM|Bnhhhu=M22Ov*x*#QA8P?PKJYF6b%QX$&@m>gWOYSR295EFDtxT z;6m(xByFb~dGd1ZlLsOc5FfegBeE9rk+Enn4IGbi&k3{Pqhbpic}u5?4y)CtlMw|j zX#xn>AYT5`yhC>+vs9F(!d_n&B2C!r(YI_^u3aBTQJf7=s=TyRk=1pFr^UsSrZCuA zJ?wo4<}q$X&Cy){K%lh{Qp%Krg_3A*rK%Nh%@`vAh`Qj;T(2KRRlgtgdffzdqe<}; zi7hXdzavPbUk8t*V`Q&Mj@SxUjlBt7ETBo);CwhWv^ZH~Bu?>5eNSzeW&^zq)j3P! zv7Q7ViOPB~e|UMHucx;GvqN&DQcp&0Eg{xUSH&KqajDB#0sKS>WDOO-0aD54oT*eK zWoe#$_(7fA8C6F^-O}p$wc;$zb!JU6ce8}5I|g^cJ=yw`?006&+6&tZ+v50Fk{*x7 zG>s{7jd&Ivq*+!fU^KfIdJa0BGEO$3Xh{GOs)AHwGGg7j5$)v>y_qo&EsZ~uJdTj~ zY3hLme{ukI_NDOuV6u6IRmK)9w0p*gK2w=3n5P|15VG7!7RA)TgK7~NN@WJ6)A9G9 zL3N)!6b+|jRi%0>wJNopkW(&M%qStWH7m#>h#}PBvsk9^R@psLL$mW_7{#d;LRN)A zi?tS%&C&)JtbsOs2gku)nWLnamf!h~^fCCoANpXpEcX~%NGJZj(_e~yN1Qre>hH{T z(x+SK1os4;{s{ip4lkv1G#s!K`@xUkor{eaY%r_MlKE9vn+@P+5Cb|7JU0eEPM`Gl z>|El2w$nepXv(4Mx`5}ocr-rse-;ncDyZaSWXLBmrbRueK&vKJfz+OdQ|L2e7T;Ea<3NtYJ9B)>&7!=m??%V8!;Z@ZDHAbkE z$!Ii6WQCL}a?#^#<%J?I<#uJ89U@qkTLM+tL9!9wWiyY}>;+l6igP4B#L&AS+$5SJ z6Gh)l@(z;}!3s67D5a3;&6GIjTvPFg|Q*E6J(i$z$LXvuv6;t$j zp;2l;EJCD83EbHn#1^<&sHO+7?w#CZs|&~M=A-+p-#KwN~o%UK4) zY6ec|x3S_Wh~drji{JKTpA#~xe?RJdYl0wf%p^$ySN|SGKbleM8CX?MLWqjc@WL$1 z?B*CB;uV$T$)W}392~a*1aAhV9t6U21YQa~;lfl2ES97lbUlP=w{m0&ql{`gO$c&M zq_)7@_>jbEDbjnTLMpP;=X)%Xxv~`%aUiC%J%oXLAH2*d#&mG+mh{6ghsf-yw$`>2 z`xplo#)6Zq%cOT8hucjt_BnNb6yAlSX-TqSkH6Q3{^Q zP(WpdKks55e-AiHDf>Tp!|RxCxdD!!FleNfg|u=KrKQx9iDAVOk@gu_Eg7jac){*= zIEUNeo>r~As|ZC5q4)R~GOfzvFi79FnJ-|<`e?KvZFZp65m6hW2bc5)!=CKk8f{eK z>LDhF=%N+%#h^RiN=N=5{4i|?iM=vOZcc;|U}KgvqUvuFu|w&%bA*JGnfMGI96rA7T}o(o8=O9+#i(?lZf>_M zMU)#cll+1PB`p!}a^g9AZF;oSQUR8jk60IsMF5(Q?><}=|5C`-`%rzf#1}%DLyC2*3RF!`}T-|9z-B`eYu`6mTJV`Jzr(7gGHbaSaQjiB?V` zEyoiA(kdqr+T(Q1i*wcY@|186oMD=Br_o!F9tpk$oC;gEQ_#u6tx^0Lm;%aFSPr9` z&CA;znu(=-UNxDOD1>q3r?zohsBV^LYOmV7!2~HW(-Y>ccaI+hTcC>nj_#!gT`Fo& zMN)&c3$P@$Z+_~qH*=DPf;~w740SfH^_VlE$BzQbz4a*YROL6&*RyeNJ<5N4`s~Rg zhd4&)=TDwJZ6v@7STgIViTi$`YGURIw}SWUMop;HTn!h(bLPh#P&lMNZscwNJAvWZ z-kRcnA3u2T_}Pw3p)eYc2IyDA>Ae3hFQH$9pF&UIs{@Ez^buY-Owo{WoyY-<1X(W570_-OwcCmNO*TD=7ze8J~1;iMP&!-Jj9 ztEs5cW7vm^IE~&4$B8J_AmHwSafwM)Z(h7UGS{k?D%Ssw$GG%`z+CdaCJZccYRte+ijM+(MnFJSz(>$lCbBF|3X_b7ft+1A41=-YXvWblCN)fk3^f+q z3XgW3S)cV%BZe9H3vQJL?9K4=x0{@OKUbcjpK%pBTl&R zSXe1ABo{I_w_D5VY!Nb9i^X9;QeMa18O05z%#H5bsH+peB zv9V&XqJ%T1)*G2~DKE*&nuwq!8Dw(8%oG@>r70c$Vgj2-r_8}6I5HA|7gUH6&xv## zUOakyH@l!6W{!t4%dD}pp1FlXq^S%;n2CYS&mfN^n-)@(LXHK@EYZk4!GO_*T6FO; z5(7m6WIP@xKVpd&kR%CI%wsvgB#e5Hb{)~)Fl^`6^Ln!27o2!CEC(1v<1BNN@HDknI~}+*E5ZhR2ze)h*LFn zYRLOE=Zs@>NIJH}X%AE=X}7aV2>f3vO{7_Yc_#OTeY8k!+?ya2HVCBD%JhwO6vo8@1~x{ajGmwskzU>kLbl^_=Rw+ z9gDG+(%KT)8N9xEU=eh>eQOuk#6WS>-bSZ8e3EzVpR|%vqr|96y~(=gW>#yr>xr=_ zH>p6>L{d5$81W53`L__g{~T>XreDciv@KT{(#TmAr!%emhi?c} zycT!bje7k}JzMN-?Scii+i|@FDVdxz}m~Xvc|Ik?U`VT& zA+_<1+~XX)mmqVww>Ov%)ipFe-e)mPfZ?4QBTt1vtDHD79ahl6hB#LQ1MaJkx_o z*^ngD7F~|SaVnL+dSxcV%3lBVtvf(ox^?S5yy(RX=g*!wy{GM+cRmTc(NhXm9R^Yk zx)x{V@ra-0cr3%*kg70wxW0445g8`bWh~(>rZOXrBT;0u@+g-5iVUTc5L-{??Yb(~ zfRHkTG>@|gkRAziD9O_d=9-eFB|-|9p(xQvB{qY)be5D<(&|(dBx%L;^%xm81UH!W zBqkl_{wi@=IqNo9A|?_Yt>}FM$4F314Jw%@JU1I$QYleP;rSjk&)`w>>m-7CUEmyO zh3P2@>#RRhcMkpOMLrm8{FeW(@f&K5BwXQ&(ounVrKV)ullyo^AlXI?^ifrT&NmN( z01+g~RFu2_9xZ84SR^qra}>zs*nnh~z=heA<)EIDE|u&JEmVf~chi@)*d~ zoy<(DLI~Xb>cyKkFP?f9luhTc_Ag$68+w*`ls~D12&q*%%L))j(}ob-4|l`|Tp4P= z$t&*`c|oOsbgRLj!OHq+q$h-;L`;?!#VYqu(JLxcHCf78S7}65=B}fa2vp#+6thu3 zuPK#$I#4PQO0&w9WpCw@h0W%OWnu6io6`0uJ;SBxx2pets5Fvx9b6fm4_Ii&+=1Y) ze96^TL)Dq?!evl94_)I9)Ak^{?4kTc4WoL%2uhc_CdPy%kL!ENG@P7et#j$5T~G`p z>k*hS)XirmLQaD2_WR2%`F!AWo9z^WgCknryI{B5YBuVPn+m6IZNJxUbGN989u?BH zM*TVTXBYXxxt&hCsa3qU4Hd1N6wFU5{fbT9WBz(CUt}wfW@kzH>tz5`N5v6<#s>gJ_helkWTXH&K zfy3tS7icn>HnqlPvYAvYingV-jbj!yYKkKtRLE=xONL9jifmZe@%YBibq zY9Trauo~7GQPoWAbDnCt8Y;i*nw%kET|`c)JY$(LmX#5TM^kU%lv{F|C&Zk4KaM$% z2jq}X)NU}LhD0~w@N8bGEB^*PmAuu8vu$S6@pwAB#N^o~b$^a1C2n79aUt>*qFkAN zSGNLo!Vc@oj9Jk%{CakNDt(by3_f^Bm#_EB^Wc-MCOFB4S7uDmqIS&G?>Fes6ja3* zQGgUYE>)3m##RGPa`E2afQUyDdQT+b_%MQV&PHT-!cC)}n=QMI2eagZPr%x|9}pj3|bXbzG{u4 zwhZ&e*!i~G?M1`vvoIFNxz)_FOyWRNThGdDO7f*W?bGe86R&4!!BY&05;76(GelLs^BW-^&z zD5fH`w%hH!dk3GJ)Q0osdWyDDnp%395Z-AODV4cm5T)a11oid0k`JxxbFMe18+sQ$1-F63C;Nt7 zf}Rp9#>svgAh->B@z7_V_Q)->Wl9vKV)CD_?-vj1V)$C(VcW2)7NlzkVbUp8y;*`f zI?^)Qils78H|9B+lejSOhRqOO{g{H3FTYuBt((BE8gGc)lA2EPqt+r%X4)p1STeb{ zqf~0ON~Lo7gB`mnPy0)AIOUEC{5r&cTKKODyxn+EApKT>qNy6(E2;^VIYyaQkl zuUiknF|69D{UaC)(@i zZd&}!G29j{M>&=MGKz}3W+Ep{|2|#_2}7xbAt9IJxoHYGu9~6EHrw>KG;TSGdKt!Qg-#P4&!D3OHnDc|nv5r~2YTklZr&Xg|bU0Pg7z!28!X{}0K7&KtNT2e$sZ%BR&E_^W_( z#M%0!TGqO8nB+-Y@+3{!`LH`yWz%*&s{U{iI3=nWbMIDw1?EugjIjdT@XadH%z11% zd;O#nejy;FlHa;%ve{T*Xo7 zhGsdZw}5dtC!!&LL`%#obcLctD2}+k)B#HKBf)$IK0(q##1G?l%LMk{B;96r5})i@ z0T^?vb8+8)3j_QXVB4kfVBIcbe+N|pV`#^=9os^UK_#`p6a#h{2?vkEbGn}ev^v+t zYc7j00KOlyN^=!+e_$+1C`1XTnA4q<&dwMR7>g_8Y83nxPg~ZsTfMK6V$K=(ErjLo zzq08U?aLbgtyTuKexq54;2zwAC=9~j1dT!V!hHc24YJr6;BPi~0k@nE_ac4>8^oC? zO#3H9nTB~+PdLj0Bcuz21_5yr1~dzZCToLgwT9{}%3kcYHB7$6<^;0@hwLyBVq#iQ ztK6E0ug__cBymJRSrjFpQJf@68XY@w@~CV0;&?B^q6#mZz}W0;e}k^M3=7_8PA`>o~YB zjeG~J`txh$ALn>}l(LXX7CPUYIp4Og!(34YX(Ja5TrRleM6a+HA+id|#hKdy5>go# z7M|7(2@yqVEX+Wy1>}t7TxdPYB~x7a2ClthFfnzK&OJDYIDBMD9ED*NCrO%RzzZRi z5+Wu6-(KSpXhdw5Ui6K_(I)+Hyapx!+np9PMM07SreJD((p1CbqAr(y!`U_-@1ZQ*?=rHV1Ts>m%ku<_C2bBZo-# zMFCkH``%NNp5M@*%u~v_w(-8C3n2~rttDR0bcDEfI{hF`fmfwc(rkWU0mX5*SW z*E@@nwvwJLmX}7ly^Alng#Z8f*XismD{Jlbs#WA`Y`t={2M&ev_vd%Z%w90IoFFc( zsHy)OyLl|&;AW-L>)~4vc`Cxu1O0r(YP0@+f!|=h@k3QXFH5X#M%bbv1N3_ zi*8;fFsX5uhf58hzU+{%@Yz|NW8-1lc)p&h!?Rx*P3-6J%Z8sfPf=WNJ~_!vbd)W5 zdF25*%4X4ErS#%?`f)qar`(j^UQK$tD|~#(tylO^8Mv+g`QlAHdNS4TY^Zv4l40N_ zo-HS^!!j+wAd}X{nfx+3mXv07%hJ|lDfn_bz$W(n%pmlfTCYSw5Jbrp6EDhf!uH30 zuOlSF91V!aDH$L{-qPB00REQe>`4?{yRDHc?r7(dr;d zQ)T$M?3JiL%Id6X&?0MXm{>`|@87pfNUtGd%k^@(TyLFgFZ%Wx$%UG-5S955hBTW$ zL%R<#j$`Ze&BW4*V~ddDFe)hxu*8JGT^}7Ri(}R)RQDF;q@XnRpZHURdB8znMIgmN zAO%Jl%N{gVRo9gY)iuR3gyY1->M*umtz$gmIfy)n;sJ|saQ3~(#vF@qMZ{$ zf|T;+$io7}PvkRy5*RcytCfg|$+!tJ?x~eXb1Q~CEid}xyx%lIPz`3^d--9MBm%RF zSmEkWejM}bWW$*5x1+4+BP?91l?&B%!e)IPgtV;k_Ewwr%40uJDa>xQ$u=G^8ykdF znsQrcyAxfTBkD`Vda9sy%gn2iw(t^q;Pl`t$j(gYGlX--Y+R==X9m4h*G7+`=wlyY+WW@?S;ea|>(l7&s`tZIjt8^>$la@=}Xp`pv zIfs?DEl?@X)s&{RC4h6{IJ=@a#3sODgH>q))~B!uQjcaXNs=jJlzb0z?j<$`O9chG z78>Cw@_`ieG%>nRVd0#paR^Cos)XALE@dbc3xnX#{UlS5x4>=-%BI$^CuW2_`gEmj zBb_$k`ccB@*2U}+_8EQ2$(qrc`EX52RZk8{r(c;;f^gWhL|sKpD;4c}YWihxh!g-w zL^!qy;1mk-;V8$FmxCI8Q`(lGS34R@LVyh3q3rQcIoz3Rt`~b1E#^$O zu~gAQ!$-UJX%!1K;oJM6UQZ~-%qLfEpuj>Xfj-z>kQP|L98dGE%7 zwT*QB#Ay0>bu8v!Gfs*xl^z-lI`kTN0shS{@Em?V2KyH$e_1^G#B{pcpbx;c#Axil z+=w}AkDdX@sT*ihC#;K9o zGZWl-QJ7&SWmXM?oGMeC@83#5I_7=RO8y5hukYqG1bf;@ zEtt49)?JNYXSYSmHibgocj9JR2nfB~dg8j& zb*#k5Dzj$>lw=Pvq@9<6Eb#lCHBrsy_r$VWwa#X2E2dICoAn6}`!CSb*Q9mU(kP*P zt2y;G4s%EIM-t9Agv*kd`hVtHn7Ljc0$;@T;3>+i5h@K_-8Gc@=Zc&*ozT4vztgOu zc>#_3N6g~_juzLNQV5na3yoy3N0irw9me>OANlX45`B&)8Ak8?4(qd^+W6EM)HVP< zqS6!_I~ydPd7K+44C8dFTit3=ZJ4~CxCj-PgizV(NnA6`bmT%QZ0ma2>D+vhn*muy zc^u{UQ=UcDPSp!!yT9g=mLn+d3B(-%plYL>%zSOy80zO1ViI~vE5 zW%FvE^vUsq2M>;G(_i2n2(>zB1jjdj8nfe2`kS%YHubeS2;C3&5|2B2^WgC_W3Uf6 z;nh(F=iC5e<6_OkrYJ!cw@ebS=Vl4G;^J^dYD?v%FQfGPT;)z5OA=f(B;e<-F>DZye18m)4($au8e z;U4!b;J28?$V2%NYP%L~J&h zu4&wVy=vv^o41^A5ft;~bbFlg?H=19lRaoGu4OC(==-#=+qxcc>u z`e+VRPz^Lt!?g&56XiW3uWoOCwRmo>zWMhf@+Plui%mKrF?C^-f}`rFhGeu8D);Ic zkq=pZR6n+$j)a^V)yVYt#Qr zPaINm!~2Ig1zQ-jxpw;HcH>x@W>GO%ZD^c(PF$Z|%JMw^!)tl&+E6a8-Vqb<>FbPr z_e|w|=O#dp;UWbJ3Y85U3*761u;RbAiPjedXexTo`WA&-eO9>l9NA zWeq7syS>Oq=*B=CcFR}7ZmZw|SO^P`n3uP^-|fIaci7^3wEC=qub-bk{}ptanVG3< z*8qb@`EHK>c>whFbI;ZtfEy@MM_4Sd2wbmZfVLh-g>Gx=CM-EumbHy$cmdQ7P9g3% z3er%z5c2v`!Tf`Euh3Q=Wl!Jj>7OTOae)w?E)$_%z8tdPOq#yV3VCCLcN5>hlJP?P zX(`jis*wgw{QAN+}~VG3|h5IU=WkAxym0_z&`n+}T9Cr=_|bM$(YnZ01a~F^Z20Cj zeQS5*Eba2oe;PKn?twU57Y$Y-Urqw41v;rg06iME+V&i`xNT1@T>HC&DI^r^OHdvi z;*!LMD{_?oW)C5jJ|>aLC;ro-Jt)6JSL(Dth{c*=yEZ3B`EHK>c>n-0Z&Rkz$+Y0@ zuFrYX|CoiopVtU=xBsoQZmoU_{h>JPT$ZB2I7ucKP-q9d3q>dGR7x2>!B?Ya40f!x zsC<1GkioF5j+KrO#+$BFOSj1`%oZL!aK;_^>|}I79jcDobENAmQYjUWDZG=4byZbG ztk(M=w>8)Igwdfs45uE{WU~`@H|NDsiCNJ$xpha%<>C~@WL>1KOL~N4`uTzcAL5Kp zu1!9X8MRl}2$cca5GC(Yn>OE@s=huYo$jn*KUSVS!H0Z;uW>7f1ud7$r+&jsxm-7r zPkR(7g+~q!ew9;6aucfkRMAv&oIn32sH@lQGPtnN5snaZq zBEel>WEJIbol-cwf!E1{G7kh2C>4L3+Mkwo3sbr|NuC5E~nW>kVsD1r?4|RedZ(Y%4L| zitHrOm35b^Bb;SM?#VsLOc3yiblPSVgHwwzhx7mz0O5&fBY-P5wjfR-t(MDNh_RLR z0kb4cQx34pAY%^1p$XeHldirLqgo63EaOUBhfZ+$<7sJ~dH_rK(L!u1DQzKl`{xef z;h3z@I%R6G(-TB^SgX+fH*0mldwBOT4eklaNSR4%gw!_62q`gXtyIP^RkxkEzq=2+HuR5MwV2WF0a(8X)t7&l8RNRqok%GooFl;i`isC2Nu5@ zH;ukpab!@*dFj5)rO{l+U1Y05e#u+mT08Y5d1i(*xCpc=b!Owy9w!dJ{`;p@|K9)> zazm5~w-g5!tel@--QOJToj(F(*@SsykSvRY!HmK-r~o*U7NO!w15`k)(rj$0Iy*?N zI9DDjY5tPvmPY$;QAE?nZl(jmhGWHX82I=W=e7{7)gNrrP5G6Ahc-21&jCFB&xn1# zxM*}tB_-I=M^32;Oar2Sf= zK5o-hmG!w?F6r0#KGk1QVdDS)|0GTRH#(-0($d9CME|K8Qr`SJe#{_(}$@&5MC z&e6qvMZqRj9_gBznzlHO9We1f9Yi-I?9>{o2w`zCemM3-jRlgq03()4_UydbHx>x2 zh&Jx&$;}ArI97QW;QL%))51DOBfg-LF3C6R9{@ShgbE08MD?iz%Xm#|6!Vh~vLUBi z=cZMvrRZ^~Nk(!tDJmAqAeCvtY?ercuRcu<1&O%33Cft*V0zl*=ttl*0?3;&PHuUWplHg?XwMZc1(&eJ!(5vt>m#3xs*0w zY~w_a5PsM)sRop0MSlX88V zwu!B{O9pIG_~s*M-O5@BW3jB8TE+-W3o=;Bm{JrNPy%`Lr;^vb!mJE-YxJXf{>ZO7(~S#TsR+N z@T4-N!QGvnVr!{A$X+%^35tvpIAvfE;ax)>sYn9Bu~u&Y0X<4k+*!-}m4srVsFdy9 zE%veZ*mOU_7_i6XMo@R_}(Jif~zHj z-{T258X5WdAB_{__YmUMxK;Q5!#|HMEv9rgoJAN7rx)-RJ}eu(g%@NvBRB`!@QPBQ z<9#0H$!GC6oQBJA6V5SqKKO~lE@P{!5q$wC;U+wR*8&Z{A+O;HT!RyEf4sR|bVA|h zFPG-w9NdJ;LD>?pbl&ACkWs){U*9y&#p|!#)`sH`e|u8N$#@4~UAmp05-I$1wfmXG;Q8 zL0%Lq^E!<7AK>>mG+FuI+PUA!j!d60bUChqU~v-&gq~nq9dWXZ1-yUmC)w^$`np`Lx~QEIde!HFvVw6scGaphgQ>&f+60YI zII;FlQIOh%V^^y==XRRZbV~6|Uo+9IL=z|feNF1D>lu&A(7-oF0oAfN)htn`+|FO5D(aHy(ek2Ds8VU08(`=fiyIkeXYZ|&UI4ODv z9eX5wh?BVU#HLBRrF4%IX^dtVrAcRT^K(3qr`7eMSYOQ_#)l~tmine;HonUX&Wrqy zHTZ3_wYBYzeYOT6A*vg|q<#q#RSFS>rjH%by%#0jk8@mL)!s#OQZ0Qz1U$a=SSMbBtReO z>|^iuCc0(kA$DB=-(@M%zIWDbt+NoZ0F4c%J#OhyK1X82@bn&H zn?SbShY`pQW3R(@Tg2$oUQa}NVGRo7*sAN zN!3UB!jQsw*lkP_h^%i5Xc<48Nu#uYLNM|GVgZbaODoc*0*0$r1mTiRzwhBPSSTA%b83DK@eoJmhOqwd;6UU*5SS#jhX&9 z#;^(?S(L7eM#)C5%*GiNQV6aKY2CyImNKx;5z@8hZ}<~SN~^7Vloe$!W|fc4Q4dQ6 z8z2kWbG;1vXUk9FrSKEk-_hg~c`c8jj>D@nakx4So4P$W+CBW-Bu?w+7ak1rXLpQ=QQ3vKm#(J;EG%K3Vpug}YBL0Myh%=tnZdS$hh&JL@^ zu~?WNr)p(U6H*U)4Mg|+)C8fOQt$dMZ}J7lZ0}FaMh~@O4?~`tY>4Toi6mUa`7Zg4 zQ@P}uB70Q^kOVb*|ErRR)ocVp_gUd2 z{<|QqmB)|j5i4Ev{~tsIz)sllFI!GXU75-_h_~;`O!V#7|1_9#l|oXJ&d0l^%mr;} zWGf5zHt0Kuju+z!dLLdA2Mwe`a{BR&nYfuRU3TdaxT3GZse)lM?gMYK9IyTJ^Lm^O zw+cOkiXJ9KrK>Z4d71m6GUiSA7Q!rFmHRl zd;cSN3#({K(+~aI)UqQ>F(w3KgoWuJb9rP*LYwVFYCDdTDL)Mi7t&s%!)Rb3&F)z; zLtH$}#kF23kk&*P_VR<-h*u zK-j~;Z+df8EphJq17pPyV^W}ul4XlA)N}UqYeqlGaU|y!P?n!$oT4ZygmgUJ$aXMF zsVL!!7I)V%08g7rZCO&_|00D&0aGM9wEy3009pOh;1Vqi|Hv^(p;0_rWvEaL&|YiP zdW{4f;jhO3!gJW$Z8}-0l1~NxOyaa`?@PMgGJch5KaUK;6@yL8?cFkm2x9~ZaI{3^ z{^q3|7zmYvol-S3`bRW#mPRp#1%`3mYZ1^MXAerPkR>;rz0GC3jQ=;-cJtt(33;W`rdNB*Z1=zcIj0grXgQ3(AP~ ze9W?#xdrFwvn}3ljJaW5P4IN*@u)au(PH;&)-@Am@0{BaaX|^+ zpF zdAVXKWT|cWZ;DV};L)*C(X+d*{F(v=DzKg=MmUO^RQzciOqG(R5o8%4C@%=kb*Th_ zwk#Gk`M>JbYF_BqJ@3Dt2w)*Ru2%p3^W1ebs8T2jnfiROfUM(tZc*Zz==cUCOftk3 zDzM7_Qt=W@{->DA(vSUmRO+@4f<(LuTa8e-Hc6V(lUfqrD&5hpnAh6cf#R*%V{a}r z`Rn30SQJPJimB$|{wh$6bJTAC<{GrY$5s#q6$eRMb#~*~b;Q8TS}+T(VA2|M)&#GM zSD-b)gnIaFAl}~oSdzmADHv$Qw4?NM5w#NuT_(Oqoz_|85rDvQ!^l38;kM%b<^HzogLCzhtUrTg3+ewr`DRqg7r`MJpZxM|s-(;m%>s;!2G~j#1wDv zM6wdL$ejq)s!cbgJyG%&I;iK$&vveAu7wN|y`Ms(QrmegGL5X^YT)v0uYzWk$Q4w= zZ0gya#~i|hfmMqa_86oly$x_SIgHlYd8a<1Pgo5PL;rRj+oH^56x5SPRM_=GD_mJL z^LVsz83u%5-gytb#5!fwpf7KhOlG>*8XV(=Uxr=?(EOZ!| Q!ZsHA8RLtaJsJP?6*WE$lK=n! literal 0 HcmV?d00001 diff --git a/packages/ui/src/icons/names.ts b/packages/ui/src/icons/names.ts new file mode 100644 index 000000000..f83b5ff46 --- /dev/null +++ b/packages/ui/src/icons/names.ts @@ -0,0 +1,111 @@ +/** + * Every Material Symbols Rounded icon the app uses, sorted. The font in `material-symbols-rounded.woff2` + * is a subset that contains only these glyphs, so an icon missing from this list renders as its name. + * + * After changing the list, run `pnpm icons:build` to regenerate the font and its manifest. + */ +export const iconNames = [ + "account_circle", + "add", + "add_a_photo", + "arrow_back", + "arrow_downward", + "arrow_forward", + "arrow_upward", + "auto_awesome", + "bolt", + "bookmark", + "calendar_month", + "call", + "check", + "check_circle", + "chevron_left", + "chevron_right", + "circle", + "close", + "cloud_done", + "cloud_off", + "content_copy", + "content_cut", + "contrast", + "dark_mode", + "delete", + "delete_forever", + "description", + "desktop_windows", + "download", + "drag_indicator", + "edit", + "edit_square", + "error", + "event", + "expand_less", + "expand_more", + "fact_check", + "format_bold", + "format_clear", + "format_italic", + "format_list_bulleted", + "format_list_numbered", + "format_size", + "grid_view", + "history", + "hub", + "import_export", + "info", + "insights", + "ios_share", + "key", + "language", + "left_panel_close", + "light_mode", + "link", + "link_off", + "location_on", + "lock", + "lock_open", + "logout", + "mail", + "menu", + "more_horiz", + "more_vert", + "motion_mode", + "note_add", + "open_in_new", + "palette", + "passkey", + "picture_as_pdf", + "print", + "qr_code_2", + "redo", + "remove", + "restart_alt", + "restore_from_trash", + "scan_delete", + "schedule", + "school", + "search", + "search_off", + "settings", + "short_text", + "sticky_note_2", + "stop_circle", + "sync", + "sync_problem", + "text_fields", + "title", + "translate", + "tune", + "undo", + "upload_file", + "vertical_align_center", + "vertical_split", + "view_list", + "visibility", + "visibility_off", + "warning", + "wifi_off", + "work", +] as const; + +export type IconName = (typeof iconNames)[number]; diff --git a/tooling/icons/material-symbols.ts b/tooling/icons/material-symbols.ts new file mode 100644 index 000000000..da77ebac8 --- /dev/null +++ b/tooling/icons/material-symbols.ts @@ -0,0 +1,51 @@ +/** + * Builds the self-hosted Material Symbols Rounded subset used by `@reactive-resume/ui`'s `Icon`. + * + * Google Fonts silently ignores unknown names in `icon_names`, so names are checked against the + * font's published codepoints first. Axes match the design system: weight 300, grade 0, optical + * size 20–24 and FILL 0–1 (filled icons mark the selected navigation item). + */ +import { readFile, writeFile } from "node:fs/promises"; + +const CODEPOINTS_URL = + "https://raw.githubusercontent.com/google/material-design-icons/master/variablefont/MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.codepoints"; +const FAMILY = "Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..24,300,0..1,0"; +// A current Chrome user agent makes Google Fonts answer with WOFF2. +const USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"; + +const namesFile = new URL("../../packages/ui/src/icons/names.ts", import.meta.url); +const fontFile = new URL("../../packages/ui/src/icons/material-symbols-rounded.woff2", import.meta.url); +const manifestFile = new URL("../../packages/ui/src/icons/material-symbols-rounded.json", import.meta.url); + +const fetchOk = async (url: string, init?: RequestInit) => { + const response = await fetch(url, init); + if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${url}`); + return response; +}; + +// Tooling can't depend on the browser-only UI package, so the list is read from its source: one +// quoted name per line inside the `iconNames` array. +const names = [...(await readFile(namesFile, "utf8")).matchAll(/^\t"([a-z0-9_]+)",$/gm)].map( + (match) => match[1] as string, +); +if (names.length === 0) throw new Error("No icon names found in packages/ui/src/icons/names.ts"); +names.sort(); +const known = new Set( + (await (await fetchOk(CODEPOINTS_URL)).text()) + .split("\n") + .map((line) => line.split(" ")[0]) + .filter(Boolean), +); +const unknown = names.filter((name) => !known.has(name)); +if (unknown.length > 0) throw new Error(`Not Material Symbols icons: ${unknown.join(", ")}`); + +const cssUrl = `https://fonts.googleapis.com/css2?family=${FAMILY}&icon_names=${names.join(",")}&display=block`; +const css = await (await fetchOk(cssUrl, { headers: { "User-Agent": USER_AGENT } })).text(); +const woff2Url = css.match(/src:\s*url\(([^)]+)\)\s*format\('woff2'\)/)?.[1]; +if (!woff2Url) throw new Error(`No WOFF2 source in the Google Fonts response:\n${css}`); + +const font = new Uint8Array(await (await fetchOk(woff2Url)).arrayBuffer()); +await writeFile(fontFile, font); +await writeFile(manifestFile, `${JSON.stringify({ names }, null, "\t")}\n`); +console.log(`Wrote ${names.length} icons (${(font.byteLength / 1024).toFixed(1)} KB).`); diff --git a/tooling/package.json b/tooling/package.json index 428d76104..e4701127f 100644 --- a/tooling/package.json +++ b/tooling/package.json @@ -9,7 +9,8 @@ "test": "vitest run --passWithNoTests", "test:ci": "vitest run --coverage --reporter=default --reporter=github-actions --reporter=json --reporter=junit --outputFile.json=reports/vitest-results.json --outputFile.junit=reports/vitest-junit.xml --passWithNoTests", "typecheck": "tsgo --noEmit", - "pdf:translations": "tsx locales/generate.ts" + "pdf:translations": "tsx locales/generate.ts", + "icons:build": "tsx icons/material-symbols.ts" }, "dependencies": { "@reactive-resume/schema": "workspace:*"