pax_global_header00006660000000000000000000000064147770315060014524gustar00rootroot0000000000000052 comment=9c9b5cc308f27388e271d678148c631154fec4f9 slidge/000077500000000000000000000000001477703150600123375ustar00rootroot00000000000000slidge/.gitignore000066400000000000000000000015671477703150600143400ustar00rootroot00000000000000__pycache__ *.__pycache__ *.pyc dist/ .pytest_cache *.bak .vscode docs/build persistent/ .idea .mypy_cache .old requirements* slidge/plugins/whatsapp/generated dev/confs/slidge-tg.ini .ruff_cache /debian/.debhelper/generated /debian/.debhelper/slidge /debian/.debhelper/slidge/dbgsym-build-ids /debian/etc/discord.conf.example /debian/etc/facebook.conf.example /debian/etc/hackernews.conf.example /debian/etc/mattermost.conf.example /debian/etc/signal.conf.example /debian/etc/skype.conf.example /debian/etc/steam.conf.example /debian/etc/telegram.conf.example /debian/etc/whatsapp.conf.example /debian/slidge /debian/changelog /debian/debhelper-build-stamp /debian/files /debian/slidge.postinst.debhelper /debian/slidge.prerm.debhelper /debian/slidge.substvars /build/ /docs/source/dev/api/slidge/ *.egg-info .coverage htmlcov /dev/slidge.sqlite .python-version slidge/__version__.py slidge/.pre-commit-config.yaml000066400000000000000000000022771477703150600166300ustar00rootroot00000000000000default_stages: [pre-commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-merge-conflict args: [--assume-in-merge] - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.5.21 hooks: - id: uv-lock - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.9.2 hooks: - id: ruff args: [ --fix ] - id: ruff-format - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.20.0 hooks: - id: commitlint stages: [commit-msg] additional_dependencies: ['@commitlint/config-conventional'] - repo: local hooks: - id: mypy name: Static type check with mypy entry: .venv/bin/mypy language: system pass_filenames: false files: .*\.py - id: prettify-xml name: Prettify XML in test strings entry: .venv/bin/python dev/prettify_tests.py language: system pass_filenames: true always_run: true files: tests.*\.py slidge/.woodpecker/000077500000000000000000000000001477703150600145575ustar00rootroot00000000000000slidge/.woodpecker/container-ci.yaml000066400000000000000000000012411477703150600200140ustar00rootroot00000000000000# Build a container with a virtualenv that can be used for tests and to build docs. when: event: [ push ] branch: main path: - Dockerfile - pyproject.toml - uv.lock matrix: PYTHON_VERSION: - "3.11" - "3.12" - "3.13" labels: platform: linux/amd64 steps: build-and-push: image: woodpeckerci/plugin-docker-buildx settings: repo: codeberg.org/slidge/slidge registry: codeberg.org build_args: PYTHONVER: "${PYTHON_VERSION}" tag: ci-${PYTHON_VERSION} target: ci username: slidge password: from_secret: CODEBERG_TOKEN cache_from: codeberg.org/slidge/slidge:buildcache slidge/.woodpecker/docs.yaml000066400000000000000000000006701477703150600163760ustar00rootroot00000000000000when: event: [ push, tag ] path: [ "slidge/**/*.py", "superduper/**/*.py", "docs/**/*" ] labels: platform: linux/amd64 steps: build: image: codeberg.org/slidge/slidge:ci-3.13 pull: true commands: - uv sync --all-groups --all-extras - cd docs - make html publish: image: codeberg.org/slidge/woodpecker-publish-pages pull: true settings: token: from_secret: CODEBERG_TOKEN slidge/.woodpecker/package.yaml000066400000000000000000000020521477703150600170350ustar00rootroot00000000000000# Build a source dist and a wheel. when: event: [ push, tag ] path: [ "slidge/**/*", "pyproject.toml", "uv.lock", "README.md" ] branch: main labels: platform: linux/amd64 # We do not need to build several packages for several python versions # since slidge is pure python. variables: - &image codeberg.org/slidge/slidge:ci-3.13 steps: changelog: image: codeberg.org/slidge/woodpecker-generate-changelog pull: true build: image: *image commands: - uv build codeberg-pypi: image: *image environment: CODEBERG_TOKEN: from_secret: CODEBERG_TOKEN commands: - uv publish --index codeberg --token $CODEBERG_TOKEN pypi: when: event: tag image: *image environment: PYPI_TOKEN: from_secret: PYPI_TOKEN commands: - uv publish --token $PYPI_TOKEN codeberg-release: when: event: tag image: woodpeckerci/plugin-release settings: files: - dist/slidge* api_key: from_secret: CODEBERG_TOKEN note: CHANGELOG slidge/.woodpecker/test.yaml000066400000000000000000000024161477703150600164250ustar00rootroot00000000000000when: event: [ push, pull_request ] path: [ pyproject.toml, uv.lock, "slidge/**/*.py", "tests/**/*.py" ] matrix: PYTHON_VERSION: - "3.11" - "3.12" - "3.13" labels: platform: linux/amd64 variables: - &ci-image codeberg.org/slidge/slidge:ci-${PYTHON_VERSION} - &only-once when: matrix: PYTHON_VERSION: "3.11" event: push steps: update-venv: image: *ci-image pull: true commands: - uv sync --all-groups --all-extras ruff: image: *ci-image commands: - ruff check - ruff format --check mypy: image: *ci-image commands: - mypy test: image: *ci-image commands: - coverage run -m pytest tests - coverage report | tee coverage.txt - coverage html coverage-badge: image: *ci-image commands: - uv pip install pybadges - COVERAGE=$(tail -n1 coverage.txt | awk 'NF>1{print $NF}') - python -m pybadges --left-text=coverage --right-text=$COVERAGE --right-color=green > htmlcov/coverage.svg || true <<: *only-once coverage-publish: image: codeberg.org/slidge/woodpecker-publish-pages pull: true settings: token: from_secret: CODEBERG_TOKEN html-root: htmlcov prefix: coverage <<: *only-once slidge/Dockerfile000066400000000000000000000026251477703150600143360ustar00rootroot00000000000000ARG PYTHONVER=3.13 ARG DISTRO=bookworm-slim ## Install dependencies in a virtual env FROM ghcr.io/astral-sh/uv:python$PYTHONVER-$DISTRO AS builder ENV UV_PROJECT_ENVIRONMENT=/venv RUN uv venv $UV_PROJECT_ENVIRONMENT COPY uv.lock pyproject.toml . RUN uv sync --all-groups --all-extras --no-install-project ## CI environment for slidge, where we move /venv to .venv FROM ghcr.io/astral-sh/uv:python$PYTHONVER-$DISTRO AS ci ENV UV_LINK_MODE=copy ENV UV_PROJECT_ENVIRONMENT=/woodpecker/src/codeberg.org/slidge/slidge/.venv ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH" RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ git \ make \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /root/.cache /root/.cache ## Dev container FROM builder AS dev ENV PATH="/venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 # libmagic1: to guess mime type from files # media-types: to determine file name suffix based on file type RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ libmagic1 \ media-types \ shared-mime-info \ && rm -rf /var/lib/apt/lists/* # prosody certificate for localhost COPY --from=codeberg.org/slidge/prosody-slidge-dev:latest \ /etc/prosody/certs/localhost.crt \ /usr/local/share/ca-certificates/ RUN update-ca-certificates RUN pip install watchdog[watchmedo] WORKDIR /io COPY ./dev/hot-reload.sh . ENTRYPOINT ["./hot-reload.sh"] slidge/LICENSE000066400000000000000000001033331477703150600133470ustar00rootroot00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . slidge/README.md000066400000000000000000000074511477703150600136250ustar00rootroot00000000000000![Slidge logo](https://codeberg.org/slidge/slidge/raw/branch/main/dev/assets/slidge-color-small.png) [![woodpecker CI status](https://ci.codeberg.org/api/badges/14027/status.svg)](https://ci.codeberg.org/repos/14027) [![coverage](https://slidge.im/coverage/slidge/main/coverage.svg)](https://slidge.im/coverage/slidge/main) [![Chat](https://conference.nicoco.fr:5281/muc_badge/slidge@conference.nicoco.fr)](https://conference.nicoco.fr:5281/muc_log/slidge/) Slidge is an XMPP (puppeteer) gateway library in python. It makes [writing gateways to other chat networks](https://slidge.im/docs/slidge/main/dev/tutorial.html) (*legacy modules*) as frictionless as possible. It supports fancy IM features, such as [(emoji) reactions](https://xmpp.org/extensions/xep-0444.html), [replies](https://xmpp.org/extensions/xep-0461.html), and [retractions](https://xmpp.org/extensions/xep-0424.html). The full list of supported XEPs in on [xmpp.org](https://xmpp.org/software/slidge/). Status ------ Slidge is **beta**-grade software. It support groups and 1:1 chats. Try slidge and give us some feedback, through the [MUC](xmpp:slidge@conference.nicoco.fr?join) or the [issue tracker](https://codeberg.org/slidge/slidge/issues). Don't be shy! Usage ----- A minimal (and fictional!) slidge-powered "legacy module" looks like this: ```python from cool_chat_lib import CoolClient from slidge import BaseGateway, BaseSession from slidge.contact import LegacyContact from slidge.group import LegacyMUC from slidge.db import GatewayUser class Gateway(BaseGateway): # Various aspects of the gateway component are configured as class # attributes of the concrete Gateway class COMPONENT_NAME = "Gateway to the super duper chat network" class Session(BaseSession): def __init__(self, user: GatewayUser): super().__init__(user) self.legacy_client = CoolClient( login=user.legacy_module_data["username"], password=user.legacy_module_data["password"], ) async def on_text(self, chat: LegacyContact | LegacyMUC, text: str, **kwargs): """ Triggered when the slidge user sends an XMPP message through the gateway """ self.legacy_client.send_message(text=text, destination=chat.legacy_id) ``` There's more in [the tutorial](https://slidge.im/docs/slidge/main/dev/tutorial.html)! Installation ------------ ⚠️ Slidge is a lib for gateway developers, if you are an XMPP server admin and want to install gateways on your server, you are looking for a [slidge-based gateway](https://codeberg.org/explore/repos?q=slidge&topic=1). or the [slidge-debian](https://git.sr.ht/~nicoco/slidge-debian) bundle. [![pypi version](https://badge.fury.io/py/slidge.svg)](https://pypi.org/project/slidge/) [![Packaging status](https://repology.org/badge/vertical-allrepos/slidge.svg)](https://repology.org/project/slidge/versions) Slidge is available on [codeberg](https://codeberg.org/slidge/slidge/releases) and [pypi](https://pypi.org/project/slidge/). Refer to [the docs](https://slidge.im/docs/slidge/main/admin/install.html) for details. About privacy ------------- Slidge (and most if not all XMPP gateway that I know of) will break end-to-end encryption, or more precisely one of the 'ends' become the gateway itself. If privacy is a major concern for you, my advice would be to: - use XMPP + OMEMO - self-host your gateways - have your gateways hosted by someone you know AFK and trust Related projects ---------------- - [Spectrum](https://www.spectrum.im/) - [telegabber](https://dev.narayana.im/narayana/telegabber) - [biboumi](https://biboumi.louiz.org/) - [Bifröst](https://github.com/matrix-org/matrix-bifrost) - [Mautrix](https://github.com/mautrix) - [matterbridge](https://github.com/42wim/matterbridge) Thank you, [Trung](https://trung.fun/), for the slidge logo! slidge/commitlint.config.js000066400000000000000000000015331477703150600163220ustar00rootroot00000000000000// This files defines the allowed "headers" for the commit messages. // Following the rules makes the changelog generation easier. // They come from angular conventions, unless commented. const Configuration = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ 2, 'always', [ 'build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'compat', // workaround to play nice with non-compliant clients or servers; ideally reverted once fixed upstream 'cfix', // fixes an unreleased commit, should not appear in changelog 'imprv', // improvement of an existing feature ] ], }, } module.exports = Configuration slidge/dev/000077500000000000000000000000001477703150600131155ustar00rootroot00000000000000slidge/dev/assets/000077500000000000000000000000001477703150600144175ustar00rootroot00000000000000slidge/dev/assets/5x5.png000066400000000000000000000010431477703150600155440ustar00rootroot00000000000000PNG  IHDR iCCPICC profile(}=H@_SVP,q*BZu0 4$).kŪ "%/)=B4-hm&bQ1^;^х"YƜ$r|׻0j}ѣf,DYf6mp'tAG+qι,̠L\+MFzrp9r""I@A@pI"ȱ +zŠv|(0SC Tn}#̄c2WG}ΜaMz9SOU}>U_ciڕ0J h #m5hX)tZ56fa n<^lX @3v OᨑRf0n]$ZbvT`iďk0^yLld\k7C} 69Tġ'XSMoo )DhG Γg@,zO 1xw3D3&6xv_fVz~tĘ1c)N@&Qxj'͏67qSK߲OAΠKˌ :hJx3Ob\|F5{)jLY G'/X>:.[Bs.\ 㕞7V/"cL,Gx щV,cbۛץi?-i'T6!c Jtx8|HӁY+1t9acx_0n^OUsxi\08y&#m &+Gm5x,qI& &_Q-an8+|fP56'd/%UxP# 8n`1E;0ke}q`ܰG /3%kl|g/cLC8)m`WC&n.4Xax40>A-[hiK>͋@`'\A݄сc ƶIsBdVYopΎ_ǑWXoˊv4b, lY?1>1Af}I.{ R5:'F-vI8#.yx h`@TF#HjK0|8)\jKyfa|40|E擟1Q.s&nƫD'Gƛl Dyb']xp@p)BPm}.;#*kfXa#3q큐;w_=Ut}Yy:7^@ "? 1D;O|֔ pj- pT ' .'y #p- 0 )AOr2Q8 h 䃵{#끳׈j]>#b~ \MdV90US#à-=ڛ8NOv3Ա$|0.?ܵ2EjZ7Lүcq,yNH YMvݧw4+I'3f؟Oᡮ|+Xă@ `f r'U|mcl+`B!d 2C9B ]7!,l6{f p-p<E D=܈I`bs5Fʼn!.ljQ` ɽC{y]ˮv1މ?nBe)>q$0ばh/0FAۻ2_#ۢ>n! eyt;3&'a 2QCŧu,PC6ȷ|{}תwx&4b&-V ]RïC5"#|֫݀H-NĎ}{IZgBO HLdОY-I?1oQ81-Tu9$D=Tuuy7KRy ]Ą)Iq52QN~)C_ØF "p,bsjJ j]`LkVO sa:ۂ3L,A\mC]YWcp0h'" ԢGk-Zg#Cli\! %]usOs}S٭ 8˗qр =gijoHs u3$}eB/Go<ڱ_Z,?|X+R!lPrK*]SfVܷ J*u)O8Â,Ɉ ܈ђmѣ 3\1 ),mID8T mB|Ijv赦U\ ܀{\bfLJ^"ɳK5 -=WJUBcCr+2.&c;0!f !O,#eP|;1 r)OxӰmѶԶLp_|. _vS^)Y)pttȇik)k[`1_ǏINAc/bnڼۈ\-3Vj.he1Ic#ᬮJX`rX f]`Y jy ~&q-b u؛h3^G"4obHIL iAM&Xx1JQ-|{RA ;aDd0 Z1ms>1[|݆H^zMf=X*#S<<"e!⫝ߕi8U*_ޱi'|TGюXxq3Te-}P :i!M< y^X 삗3?WU='w#ƚWjl5X7XS[p2fau%j[`d dn@0 A$6kS4W^'O.xLrnMt/F $l bjϗD\*/˜=T8;/rfOc3ײތq!7I$;VbwH~5^vDt#P\IQ&FNWcxcV ua wf!x2t\6et}Np7Mxij#Wp!vMv7-*$_8q8ؿ3!`UiJ۬F(T_h MJ6 y*x_Dܚ'_~ۙ6_B<0M0O|Ey1NI {ß6l͉]EjF0P :Qch3E+ :RN:PMF]'&.LޚHd!3Sf8\oYC&ۢ:<9z+s 0_NxbQz. hmnd؉e3![y^N2Es& q;l=53BYwƕrnCj9Jy&3x?A3 )20Y>uၤ0a6i#=bG;LY+{3D~3 $ӢD lkXgz+шy k9r`~:4ӊѫͼeb@RQX&cqX$Y[ gL]x|}z An=A۸ց:zp; Bb2gYqNHv0ӈ+%0㧝ЀJy'Mĸ]U8YyD(",J⼡ϤEg,\-Va^G6ͤ--$lJ/ӁjV)m7S07C3-" 1W=Jj Wʸ8*=`i[`Tu*a7*F^wҤKYL$C\l b^mϧn=no0l}TiV3Z=iE #FM)4艿+ca0ūɀCj=F 3.^|7~N%$TJ@4\dy>cauqpaɼ $5*Yr8X !rv."| b+1vmc2-ZIV݈!NxU*QYѺoEɎh2/kMb0ac M)<2vl旧7Yp&Dl N#އ{Qg^Xc5 H'ۈVO~uOsg/ &ɷPo|L?E-wix%ىIxPSg |YTf')HġIuTQp1qJ,m+hj6p 4UT}>9?u338}2(to {y c&.gS6 glHA>|`i5o<8SV0)2*Vq *Qog9ËC)P6k L;+>5Y/^_;v4_ˋS@vQ9,mѧ-:YSUy:߰<ԬYMͪ6a&gFw|C›;/8 1VS~!fjf+PR'G\*π Ƨq nq!" Jb=]s۝֟ E)7¢?US|ay-{e3fdN߇1Gɦ 71ux4{xN?mCX`RSn{1sLN0ݾg,R&LFhg њ鞊ω*i@1sE`P^i]8=aϢk0og45 {-Ŀ1`76jFȃV'PO3nEFoj/FSs)8EoMfǃߪO J3D<50.t@g6T|ס? x$lZBxɝ|8pg4Wln<đF^UxH,g4pfw#Nܱk,BP ǖ=+ߟm d D]QqA˥( EMp%Ƨ$رh3;ߧ n0ѢQ B;3R` Fp1>'$+3YlB^VxgL8OOĖ)Y/XX>2wR mF'krH} Z wm mNDL,qw˷M=xBAayݓmC8c}hxavGo $𒉝Ϳx[&VUxD:$'bL+eeC l@cnwgGV"vt?XX҅WdfuaQڟو〭 VO9`w3S36BB#im&s[}&Nx>[k5=zX`F'/rNqR=U[4~Z}^An.}ۇͷϼo3)Lmu}7 XvM A45x%]Zں!a /JjrQ}͇}ܵd7C8BYtߒG}M;8 >Z cYj9o.č>GT ّh]pS!S~Y ̓7n8 Lnι<"49rEQG!zotߺxDPJ*i^u˰$a!uIF|3ðOxx .U(#fn\z xa#1mX?Ûˀ#(.7s3opQ~QOǷxu7"z-aw_W@q-AONM _U~߆\|ϤY?"ak>Q` 8 x۲ltP0GZH^ &#b{Lliؘ/NNpU( 4 &|h̿|G2Mxm) u8HN`P4_5GO)Sx\Dw⅑SD|;@/)!p5ڲ/&c଄;i5^;ycIIlB\K~B| ;x#Z |gKg`VgMϓ>k΁D|G=¢\iq`%۫pinP¯]WQr< E 4{Z0-we&93 M"~Ǩ> |ħ) sHq.5N7?NҫԸQٸIA7 iQpiۈ6ۂ)Vmi!LV ӟyDs^| Y,ѱ ¶lм@QJ'G 7; 8e=[Qt: hЙOso*p!pRa Πq.~#$hKrF)Q !.ȏ~P;1gBٱR1p) [JDCRLAc tr gx<>4b ^&g݁yU  6I|*}w$*ܽ}=x2*km VLHaH=x Єۭ;CaJFQo*9WRKƍå Ci_4ÈFċj 5>k(ixoxCk 3P1#n؄@tC\ŔBdB! ی1 e{x<T_˜&/m#J J%e^6,Vc,.xJ6aHV=67eICx-ܳn?򔁕|nԺ1Ě&-S){!nt倬 p6ѨbQj`loCs p20>ԶkķF&6d6!A3<9)JI%΋~Fr0)̱(;3UUqJ3\9/J|r!uj Iau[m@1a [%3B֗1X:Ӕ21Sl!D3pޔ]ܞ%4Q%7F.V'(k!/ѐ=.N',6v<@~q>OƤЦtEXtAuthorTrần H. TrungU.y%tEXtdate:create2023-02-11T06:35:40+00:003%tEXtdate:modify2023-02-11T06:35:40+00:00BSOtEXtSoftwarewww.inkscape.org< tEXtTitleSlidgeqIENDB`slidge/dev/assets/slidge-color.png000066400000000000000000001065331477703150600175200ustar00rootroot00000000000000PNG  IHDRtz pHYsatEXtSoftwarewww.inkscape.org< tEXtTitleSlidgeqtEXtAuthorTrần H. TrungU.y IDATxy\eϩ-BfG(N@E0@wGP}3QQeAeIw'`2HXtQDEeW$!Kw]G7B[U}^yk{{{q RV$oTm{4`x+ `U}Wr2S9.^ U?t׿0viKO\yWd~bEdST_j7s&eIz 1v: @ciO0ǁJl9Nl5OB41PD6=1y `ע$Qݛ9&̍J7rm 2rX3>QLkb+u@1>;rTq@ h95̍U(6vtZw<<<+0}}9cWv[mQd) cMH?(b)gМ^~{TE7SR!mRHb8cy7~gΟq!>#3mXP'M?sfq ]r'o9 x#πqD.2l-u8"Dk7b^3lj7>g-`a } 'Ś؟ Z6΅f7ΰ&%I*L\W0rΜsw;Mi$f<<ȟ/u<>7HR3ӧÚ);6(Q]QTh<Q4R|*;3ĕ$l@TU-98[;=;kX¹O`^_i-ZS0N-vLf׸{G0 +S9G4HqDy3c?r|uBnD1&I ~W]Z|03~ p3x 9YsV"eR (@c8#DʔF-%*\zcU~8ә"=q5pi9"=3;/_o4u10g]qsExHey"dB+g9$:6;7g[pe:uExHei Ԋq:DrtfIoo.[)Ÿ: v Nc2c8͹Ggl]VJVُװyc1?rh9y"luwDVq>HG4$ˁ/1n8^ ه\]+XŢ Th82Wb,N;줕37=]`l=o`d:1>zU3D*!ʫ(`Np>&oR?ߖ2Zaн7Vyb׼0kN w39f'hP&Zm8nsr !zx3}/ubk}-O)~G q @ႍ)B 0{'n^4W0mL;b={o/c$.cl3I,3#4HǓèf>&#qZm1ވ }hg=)t zdtsБЌl]˶G~狍^X4j0FQ@WDŽF#aة^N^gM U(6uW"7Nk3gٳ-ېm91kN/FQoby}gp$mq+`G0Z-^U9@m35*(6v\AtGpx6>1%+osE ߍxLbu!TQ)kNF:ݖ=X 7oaodq."Ûl)m[{7׫}>6e#8G[m[$[m]__xw#Sbؐ*8@G_ t}:p* Nf`FC^.泏M[iaysa^ak"S`vG jEo<"?V`3QδpI14y3_')l'Ǥזr-"x2މ~tk8D4Ɠd;9Cᜈ(U_Q\W!ԟǠ[8f.z-ay^q;qN7Vb ѬKtw>fߏ`,Y_>ـ):2l< a]Q@+q|_:N ʧp*;p9nݤ8z8zCo̧wb7Q:ÜG0;|~XR$#rc'ީ(R=R>{T}*?QfM.%-܎w2?<\8ƞA@Xة 9% `ӿE 섩괩Tv.l'z^.ވ~v.(]458q0okՖ,8PT*]ͼ8(s7[8A*g4\l̅ W| IN^.JNh{'nEYVa1㥙ELRZ}3>wT@5[!TSF1s<ݷ@1N3%$AOXg2;0a.^0̑4Nx Fw:(bdo{ScC?pD*B5zf0 x#oեٞz?FSAp. ncV[V{gk~`Y@gU6>Ng">IzscjU!򊰛Yʹ8Rй2U֍''of~3TPxprGTmF-ǯ37މ+DqU>h+tϬlХld8=Q$R֪H*Nͫ\n?~g2`^PٖvR,v`:AqR6b(:}/-N!w.=pDW ?ZoSW\1-ÍEXXiw( r$ֱޜ_E ?PDWT8Hla\*缐W4o$q!ywzOj^nC Qt8!r)H#]ǃNQH8eȔc(@-iZ`m+iDzЉ=/d>zF>kL&މL;C$o/p?,#eVOVTi/ŖϲE~YYaS)(f idPAǘyEE$;e,"e)U sCV$`l 6l|8g- +Η,8Җrjw2oO gz˃mHэ,{2ydAʜqoa$"+H/_p[We[8[X2z.B@J/@m+7P{$(_~#UȆCM( ( xkEmt#^\5UE?L 9?1/Q NvBQ%:QuDZi7vK4HQ=\*/gHEy`;O`Ȟ@-t0&&L~`ITč%:Q8gI0RG!O_OnHɸ)+aˠ@SPa wGle5ZX>sƧVZsw14g2G&Rz'n,щ"gc/aߕTd* ۺ\ ǽx=_D/syTOň 蝸y| cV9~"Kq~r8ZX/E_1y35XE인D'#S.K[1q1oz޶, BℜzNAE#8]?~`e4y e=!  ˃X|tVSo0f-@X-8*)o+lu:u)1GقPuW& xX1.NuΧڻC& uUzɖGER4R7.wSSC$Ibg:*N iOq^gl]cJ4b9UVS VryN,I5>L.`-q(cX^Q$xyG;q 㒀gN,"̮夹B%k1.UZ@-w5q%NODQD$qɭ(> 52zVOeObjx8!?eht' ^B恛/,冝d'DQD$\QL+:0@;_SoD,R>!Y3$މE>ܚD'f';DH5 G1EI t(KMj`6L+weuNkf6=v=8- Jͧwb}{'n,щ"GHg|Hb6ry()L t;q lxEX ȧrp;ƭ!!x8^^z.0jX`-*D'nɧxCT\؊bM2+xM,9U`koVy x q'pL!V\D[󊕐'Lgmåv' O90PDDND1Eo8#mEmoYsq9[xG!O[xAе}KJ])+TM,wƒ(RbE ""U'fdVOSY<WV)77s!5o0 [E GY|*8'X (щ{2nBDDB!EWgĭ0od3#>$7 AY0oZEdq!P? w8A$:Qi@<>aE<""U%l{U]wؘ{ĭ=;_+'d`>O,.2!pr-h#,c{'.%,5(M I0""դR(gtKyB`]+k1ϏuQppT!cLL%TG VRp"wUN2dF*W6YEyw]1Dz00 ;9nwl"|)q&Y 1X=n% FWq`iTuxB~MGˀ n9 ,w~8V'S9UӪ.#ӷA= Dzg^Eʛ9xg.J|gݪɼ yN>րi(Ah4#GDX{#G 7x;wEMay?TQ%GM{'n2H%44Hsl]`>SY1%\wd ռ9|z'ȾM%>Q|;Bq#"۰v &n,/V.E LvB*>tUb (OC&`p7Ǧ`{i~_*0gpv2!hD[\$_` |aNtv{8Ddiˡ1M5Ɨrb>!88 IDAT>Nv'g,LDSޑnnY'p2w܍q1O gzUM2( ;`3xxȉ 8{h% )L5QQ̣w*vXc yE 0hď>I|JV99ZD)Dqe=Qx?E#ID6cZ&cm tf"Lsەi,f+emx?hŹ oOFrwB-⨕EH~Hu#"1ljnKbp^J@Eq3%ty#[oq/m\\{'n<E+"Nt{е"Gy\ԡH,;b|+xމW. _~}Z/c|VZMORčMfG2"u鐿SLb"(WڊOa/@[z'ngZ&h?Ƙd=|6lny<{'MjlEE4ԾQ^/=>%(#EgDna )N`!q>jKy&1eNL3YW}^hXȩgR(&RDQt^މASqC80x-\B HXU8%"f|j<$QLpSnJ) b+jְӀ !Z`6üzhW8&>&FV⍦0(ukU͹~9ODT(YiH^b;~k,1{`=7+7s0;ߨcwh5ʧwb)D%^`kRC>cnqG"+J qɦ5;1gqM8L#;Iq`=i=|nl")qŸɖѐ{;Vm$pԢFb29_RVO2 5:?R"~,wbTUh\C/rt"%lEQ*w,X#UxJ;qc(+go#MЈ v*b3S㄀_U.a9X@&|"omx\)L索(:[N '{󀔱9 蝸No ڈӸsc/x\v=$A,e<k,ROtčunW"M n٨",EWE1Q,zEѧVnnHw;qce(W%!'̙QޝG,1omSpUHȅ=9D1ʠǴ,XcUa7VQ"@-1hqo*њmG2R ˮ#DމձN:jӁuEzd l?7vU"=W®Qi9,椢Us8kp5f%h ;qc(d;lA85)LR *\b"%t bcz'J1N xv;5TEL6"?:ܙen?ERY#`D ,FE`@zyNITqKٷI;P;0JdnjO;(x]XDJhE-OòXcHJ`WqJnu6`}N@y.M!STQL\+GbN(,ROwqgGŗe'CI[lihme'H2(jbR%8#`1"Uśm>mq~WLP"@_ݞvRQt02]~ySRv®QV'XQmCTs\aHi{ guqB<GM(xo6248ߍ3Ż|69g3%Kdr&N$$Q4Yaoa?4)>A}D]R ~1uV˲m6*_J ڃ8hMR|t֭^`kKfH 0D1T(ǵ|`%bdɍB:/ؒEUO=o\{.nvxl$mΉ8s֏eE˯i*o(u`"M='Rz$R%a]eK/_35g:}AS`ו:0ž\bByG[Q4"맆6Uה(nV[fCbp8ōyA7vM>M;Bݵ !qΕo?: 鶘'JHT(nCL/n xlxsYVf:}AcwǗ:0*Z%?ɫ(+>C$gHqR%%e;lAC=_bC8`vM L8sVoK D RX+0|H#a{JHԔ(V[mgowT3D3?2]~wc_:01`R3z깋VKnqgP8c.p.v?᫕3 ,d105o5LDǞnlX`_}}40HYz*ZR,yxxa2Zm+Q8ߌpuk̪ꇢ*-Q|)y+/ݥqé0TM]>jz鈔J8(TUCvIݎհG`cx5 rGl?T1QrJʳlMgg87~otG (R( Ϸڋ֙Sk6Ge@Ա1=z~z;}J2a㨢(RA(ʫe` #: 7*a " gWEQ(Qx=aۛFKv8?i+5-H%Q(eۭ7nyUb`pnf[v~HȻת*"El?y԰%Ew77"NE*DQ"=˞k6^Il4Lf!5(T%ϱ"j)^bac^T4WEQgM,{HS|Z3m3%Ry3ǐ8l7R-I(kDzv9:L uĔ(39aR o133M~rTY%M]48zӿbQ)<{e? 1 W*Ƿcԇ)1ӌR!NɥBn2JUNEqAgL1CYYީߑn*uizn񁉳ǭnaM.ԯ\Mԭ#a]eT -, lWxJ Ӡ[tf|VY^p 4}8M])Klŧ,v,RD6ݓ8?xT*ŏ&L@Ub3~n6RǒH.x3bdgluI3}X~|2ka2j)e e?nÌlR9/vw^ʺ}i< < 1fp;a1gY8߲^h̭7؁1>Mtjs5OsO78ܼV=Hq>vvLq8Ü0rbjܸf~CU̒3[>BȌ$P]+"iը UTEqBaQeЏ1:fWǝ$k `=H;s\lMxOD-9z8XI^6X/?™qR~o{mFݮ)$Iܒg/6M;kCKYl;@ۜPf 2(XLJi\(J2xCqDJUq@TEo@EBPTA@A9s49{8):eg}<H09z3bܷ){Rۮ+Aov:d}Ik͢iQ¾1ì崦|@fnq"4M39a-{!t̐S\lv1 _ IDATä;RNnU)(%kcQ.,5g=)̓%`8RuzϜ6BF&T{4lqBf1(f9YڊCrN*{rtÎN?[\!pEڮ_1Bs D2=#tYI*P?rHV+։x85% |R*2jf #aTE|/71vp@rN֌b6-pjC3r..w,@}]o "mGGo/̕Md/p6LOM)(j,v,<_xL)uTG _sa,)el|R[7W~3rkn|quyFrR-'. I,՘lz\[Ű ;q&OEKϕvˎsnsei]83'QH_![B)yzlА9 VNNzwQلJw)*~!>h9ŭkCŰfQ}qjTCTX\i|qqu!\񘎩b 2[PD:]z+.y&=X$7"d8a>"r0!#$K1[gܚq&nUEjyC޲ɤH{W->I. tzqO#L>T#jغPO:4,B+MDk. IiFQܖѐ;|RDiSSQ^v~GA\L6f8ҳp+/_+'W- `CDҔ}s,ǪV_5"oFc,lEeè5SsPPM.>&F1l*Y(23ʞMlN "3Hw=yGޑh?@"\myI` L441|I_kEgw,$OYڣgK+ɥ Qt48D#<8X$k=$|P3$Z-DX$|p<חgoo\S7ʛҖi`E=#^|qmBa8tglYڣPTnM(3ʱI30le [zNQxBK$S$-!+̕ygs ߡm?w< \\4:tƗ)bF\|vvIPq5W(fbFQKLU,|*VҳGE}}dqrN8<yh 3B|` ve6pe\2\2AJTjsK.nƌTZ"óISq3O=+GF"!%JTEzd~.;IH\p! .!ppC5q:(J&"xpړqؾ/Us"(rY96u}nt,pPaO/?SRm>a(%݈Iu8V^+*_/dihoA%O}qMxȄa3Gff^0qT8#_˘Vh= Őzǃ}nH"V;Hڥct|hN1=p^o]?,SR )v\ؼ>tit7UfÅ`aG9t?DAj!T"OәQ< R!rLn( C?Jotϑ܁ڿy{Oo\ޯܫoԦqq?7r/Uj7u8 U~V,ADzobBaezΔ2(,Г*LZO[!^$B} h~ W@x_L 9"n0B1"=;.,6eb9TD>MN=h#MnHjSGSp`@y(\^e^K ב}ۓDp wpx.+Ljr( vI.3Q]zi?nғr_jhM5~_ T?"GLD EB1I;uLz}2[jk!Lz ŠXbS忒qR<氅u:ضNRNxh:F g:RHb{X?jԥ 11#SZtY̑U">"D<Lnb"íq"]ڡwyL\'>If5q+8C$l+Bml-?ѱH͝!3ؑ&gp{Lj?jt/p=-Ui]$s\{9B|et%^tr!ļL^LOL{YEKČi*mL~3 eR\dh^wnYGL[^W'}~T͇ G;,54f#fz;gx cK'">fIE-268aL2bQ&3[%FQqQfɔCܞ,3S1D2,Nq[z|űPH6DoSіm$l[ Efw % yT9!'7$"x5}seIS/P.6CCōȥքP*eQoRvpH(0*\oM@銺<5. kf +|JfSG\S>0=F/_Z:+>Vhо3 =#R794@Hu>&Z7q@.edFQoy;8XYH_-2<*: ((Dj?$r*̕K-53u$R){cޗw[wZkFM\.̵&q9z.]3.BjP̌R!]tj c7yj04dϩT4q}A# eRQt2'6H#޾G Uz3@L E\fbpNS0_|Y"n3AgFy\fKgS\ :RK9 ڬPʤ4qܣoOЙS^Y萯>R-QB&zwV/[V,V t ?}:6q fgMxaCS~68LP,tȳD?}r/s-)A8w\|Mijg6== s=18(DpO\$Hmz~M]iOmPf8MpF@eI!zU/|=&<=-.u0ֺ~%r sa/L㌢T .=3w8vf5`sDĬd FĞhqxV*?n>D*Ph$brf&,ԤJ|xp“ yf\g<]  8)Q~pC91L*6\${9K~8\ѐ〾r-˜)9] !M81_M\%$"KsC(YwSe{SQN3HvӨ E;ӨScNxyRUy@ioٹ%iEp/|W>_ |#Zњqұ-E,al1ٌkEMaF@9s*1#u-=ga'%IJtg3@Y򜸽#zB EΑSG~ cF ߩ@(=>!I,մ ?R*c(9Js |jjY%@;{Vx/{85#ck]ƅp][>Y(Ȋ-zNzB cv@ܗR),Gsao[ZndqbIi@zn8(m-ISn:Q(,tO9 o8>;u[(,?S6p5|tQ;u>>ioBN}nWn^u^ET37<ݥgʗ⦖u|S,ː\ P)-U|A;GPŗ2?Zהy5S'/ )1[~QS]4'-MS&VLoF@#p{aGiIS.:Éqi eأI|_ahCN+I3b_(:Q1oҀ;ۻ_2=vti.@ߧQ+ 2yqMڣ0RecOA&{ !B/E#|YgɭG:fB[Wd*r(kBf (NGŤ,n޿[;KV0ک-TΖbm~H;?vqkBe x ,M?'O&pטe:̲Ip)tjf2S(BX xnwI+㙰PwZA!e}^+O'lOfBaн6WfEW +/0:5f2)ߩ#HnuEo/s{g)g|vmsJn="`)8!d&OۙD:ܯ[}lN]\ rpKzu(s"@ݲD51ŐzkBa4~ҩm,$Y`t平a/S 7AUQ"\6pˊ)D?7jMI勶 < 賂1nu!K&@VE\~4 \Էzlv R+ҧ4lUBhS}&Ȭ N6'<.e$ D{663$Jf29voO~z6ƫJW#?Ȳ@g#Fr H5t.b+EsI_C^& IDAT|TE#fvŢL'HvN0n%bߡ:~ղP_+HGq3Ӟk/}yORN%ޤpP=O;N:1\BxN:ilڃu'Ⴞ̶+,iz\:@: i z& ,7 ԴlP@9qbפin2ϕJ(= 51ZC:VB#(Vϖz;dU>.,lSh]_wj[VrNlYN]YUdko@ 80W.D$rD4XW!ŅfJ;6NEbO6."!ixTHr]rkKvId&j;!\FR{_.E(N 3J;=4{ k_ $&TõE[Ksنo$y}y$aqSՕoxg5Z/ԇP^qؿ C' 3c-3yտo_93Y+(r!Pgf{9"zw^@8[rϼvDr !W\#e)[_W/vR׍J^~Iw \X'ͧ* Ef1U OrŸEy6hVw^'j 5*P {qR3PUA8lG4O dVՀPnFݨ|ZfWn۳v #9"u&Чzz香ǔq(¯poB8_L=q6xV^#=}.I I%庸#]bh8?V7זC/BqoQ6 T(",'`Y۟cpU=&liAC #EF+{쉰$sKpUډ$ W5RrAC%/i7M'RZ=ZclDV.Ɖ?%޶W ,yO1ܒ$3)jC(Ȁ#dIe|۳K7 p=|!Ϝ.O1cjmjc(:XP$@߉5b~6e3$pic#+Y.ǽAM3xsJ"wqi}qm}7 p~?UO<@hY%R:|C-7DNۘD1('P*}}?C乖G+Iy%iKc=n˟+z;b-A29 z 9jدB`B)P>xΑ>$b7IM3ykB)~?XDS?9j|QiZq_K?oϋDƀw^j%ӿ?D hDxSom[Ez?,GX Bv'(mI <:FwJTL5q.=¢!ƕs⿨KDŽ, h:~L| c(χ<\JՐ:&(eWC^]ʐg;z;߁oĞ',/ AMiKl@v_~lM;4S1sѠ2{m4n}|(&6yFy3Y?w9S˭nSxZ2c;r,yI<ԶrO%t)-\*p.^2uӿ4TܴiD42@+ 93$,7OR'doJtd3[ؿ0O>VE"8Eb_5! +4*3sB"G4fKcx3HRn@y }K=uv6CmC!ގRW.!^TV(sgKoa\8n bϨb#{:y6*HbY$!gT&3Y@;m/fX}Y-HAg7~GnJ;9v}+du^(+ta=~v.)na=zϒ|[2;WU8JK*R)DS ٧o\bD=zt;I7'c Kf7i:SgnRUM'~}YJە,O޳Տ~4rX(Vf^xv1:ΑW:P܁۾yrna=')atS˄Tisr@ga=;5׳q k5C7~pׁ,x eFLt,QEoRd)cCK;mx,Aѯb}v EΑYCtJ |y͕m ő:G rs_2&Y Q c9p5TFF.7n?K0 pYxLfgoJ煦&erO6aM'ٲdsunމp4Jw [a-qD#BfC/t zwj{;'ϒ~(B9w\;WH^CފrTf;=\"pl] IDB+vk±(wGcCEA~?N@Ӊ*\P%mrJ~7A×v^#4HK:}Z5Z5lb!U͓k$n$~ĎB 0x*G^k# Xx<1 `J;JS9ws)a` !],~ mg:DzF-NT+qp6B1rp*!oBʃA{y{O~SF /~QΓ˼ҿm|srz+M]?oep! J{B /̕mG1$JOf#NwEdصN8U(P* 쬛1wKP{ kgc1j4; V 0WVS~;/u$Qve FiHs ZZVay˥c˟Y* Ec1:4ܶbYA@ͱ>cLuBf1SP4Yh1Tfޣhd1SFzSlFѼcL5:BѼcLurQgV(c1)ڌy)+1Ƙd{MlV(c1 EcLu^(ҳy+1Ƙry}y BcN.YP4/a1Se:\E2V(c1 `y)+1Ƙj:,;l<+1Ƙj8r.bѬ4I; vb 9Ғc%P1Rm_Ib+hL;'ʐ5iaJ@]ġ;ci&qQ"S؃O@:҈0j ul, h~a(%o co;mt"9f*6eZlQC/[3a$e5Bq.?VJJ(~ l@GYZ[ J&{;}t>01h/7b?6|DƤ32/^F5{[ݛח.7ji+Y_ٚ"{:\5SI>c:; lt '[r0fKަ磊GPKl1&@E+mdfK^yHk0>'o1f)|YN8FlMw=1FQ&pKJm﫧J7|TXnzߞFhrn20עM^zKͶm+%1?۽JLcO'Hv~ a͑1ٗx",^Bg!uGzJohVd# j~V;CIH'Lchd,CL KLktʹ5S@RO^!klvz6G[d=  Ry>\`'֐e[S,kH9A(QFM싲/p4hYbSǫ 9 8O@! !&ة= :(|VJA#`5*A-VkM6okɽl]g^߻ڮCx]Lz8V&PL3MBx 1hZ~7%c2GypK/Ѝr&6mg:p`>,=|oFؾ{t]F|34Xn^?c2Kq˶WT 1ll(fAov.(J]&1UF:!|Wzy(ds;p`򛑩V(Vm4㲡2lO7e M;cҠs\ wۃ2 B;`A;/}\cy{OoUYi'cLʶYqM%NE"_H4b,AiK V$c=&"fueKfP"#lVH7sNƘJoewJ}ERdAP+ͶYXetz2"%(n~v2TA=E*LeS<z*96f۬PF/I*r`q;lV3}mLt*bir?ޒ2Ys˭P4e [TH7)sɦgmjZv^odbu+gYh豼#mu1٣ob,:1ɖq)[e{1Y38^> dy ɘjebu/S|,2 T@-9w+O*fbuXg\.K2<ǘi_XD`Z!+SPn{$zp0h;Gr!{gPfo^V(VW%n8[@|1G ?Wf̘~# SPR`߄?vSt !qx?E>5!I: -~1 j%%r =L 1UK@C_W2+a91#MzxK.YX4&mY ̔Gn=tq:5I:f?٘Z` ֶ(s?zkL4#\@,y) FNJBь(V!?)V1 M|qjLdJiL`LMB9btsԄ~wYB|Lr 5rE? Z`b5m6w?7NSB.vr=HoLdBabv^ SgbO):Ác貓@Vā;e)O+M&QX~cb8mKdZgbP!ScP"z*9?Hz˘SEx<+][bYNv6YXM {h SHꇯs'\)a BzuFqLB|C^c⌉ag xe+M1i1ob3+:-n3N15I31t!2@B-CQƕB&^&KCcj&ČrTLr1aq'i'`cٗ%ޕ3#˘Zm{. ۠iM{]CYʻdej(VHT?H4&&2.rdL qqR15 Ōia#%!cjs20Cya2L0 ͣR25 Ŭ <#?'}cL-4_HxkNcgxl6gbn^%k$dL$~!ɲ"+v] ,\I7?1HS~q8&SZOScbVX{XVؒ16! {c2Ha7˒3(K,SP O@ekot,y_9SĻys=2S,!Fu\xwXMx/%o9z c,%Wz|Cc^dbT&/]oLL|[k/$ƸL@dEBnOgϐI3؉VrAs?xg*Nar܉ ng=2f V(fE?CO2&B0`+kN+<,&9V(fN7a}[qDz; ~IDATk"qMEL!B~>)1[ebipPWHCSsHɱlٰ`#^)ZKexlU] m6(oN!>r2&ob0ܼ!#VukKx9_d)?ϘBBi;1ho@l؞ 7)vF1}1#fKHx3 oC]n~}`2:F_/єFEn<,1O :Q&K{":QC?M{kv T؅!nZ*ƌ}fƣ ?g3f$g̏\͇o _n?%ߘB1e:g(Iӭ1UG'L$P*4PᄜZ&$3% cv TZ(rN[Ӛ`lc2O1094nHf;C 84B^]GE% 2fPLN@[~T23Tm7{oO56m,`26yD鱙DS$jц#]xq(;l& B;.a,iQHىoD8'"<̐8Q7KL//F `4TB& 2\igڳ{s8ts>ku7ףeϚW]s(kqח \iL~NeoPOm]]^{W.ߢn;Zw0p%|[}_n{udzVFC]LJ{rA2k^짪_vt-\n!QJ^F_o~Zv->P!QS[;]6;lu:|-V}E[]\zzkJX0jZ=V݊_}rO@a.;FI 6F?뎇.V'=RdEf6VEO*]mtauZ>3Vׯj+.5[\XyO=Cm4:}̎٨nntّUf7ѱfZ-we5Ɩզ#yfoٞoe^wN#6zuW5{]-D.oցP n aۓ⼧;[Jy˓^usVm6z/nY5st?]m%^VjuT޾`jS\5ZD՟џ:`*󍽡ه{#՟t4,{ݞ(6{ͺC`OZTVխҊ{+QLEc=npXw,Zic9K{*Qqr|}տ5zOO_غŞKݽEViݱp]TfkҺўLbh'H% k>#uN/7M]F'Ewmnj,5eRFF>5ֺh_%ߨYtSjvsmtKuxu|:ՉuߛW j;wW>ݢOSsB$vcGt;֡~#8hG6znFIFkǽjHsǽfG;5;hcG,ؿ<~\ufgk{;z|:f;ޞXc/I7w;ҪsvejbJ[v5wwy;f#$❿?_?ܯҡbp<^=ڿP]\?U=?SmL䞩W]hvFW-_;;fg[tEg`gL9DvyO7#lqJsTpYϙ>G0糱BJs-l]i]lvk7::mtfx3=JnZ`?5Ϣ=kIENDB`slidge/dev/assets/slidge-mono-black.png000066400000000000000000001165111477703150600204210ustar00rootroot00000000000000PNG  IHDRtz pHYsatEXtSoftwarewww.inkscape.org< tEXtTitleSlidgeqtEXtAuthorTrần H. TrungU.y IDATxy|L?d2dC"{d{kc%bi.hjhZCT"$v&Qb"HY$L&/T'rνsfy=ч}νyϹ "F' E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "hBBZPP(V(@+ E "he;ݝ<<<Ύ\]]ŅёT*ۓjz)Raa!=y򄈈ѣG߀S۶m֖N:E#YYYQ-aÆT\9JIIXrww듯/U\<==ˋ<<6l͞=jԨͥӼyHVs " QpN 4WP͍wjΝ3gPff&L;>}j֬߾}7oNM ,RիW_)88?$ C&j׮Fי)..fW^e4ikӦ {-=Oeł Xf |tEQΝ}D^^^S2":}4ٳKΝbi^zQtte{Mw9#FٳgIT Z1Fmڴ'Oʜ j՜QFl֬Y,!!A9󕖖"""YŊ/c#Gx",#~?F FP(yԯ_?߿?Ndh4ڹsETre{.YY 1Ft-ywEjذNܾ}?iS"UZ}]9rd)Jѣуh֭n::vMӭ[7E"(ݻw˗˘uEVVVy{{ ّ=Ӄѣz?dBQ/'L@ݻw|ŅFEF7nвehŊ;5hҤ4mTLMR鼎e~Q޽wޡ&MP5t 駟hTTT$z;ܯ{ڲQFD?~̙ݹrGLL=oGffFិcРA,--M06~꼆=M8nܸA+WzNɢ/_ONnݢŋ=O˗y *Ȑ h42db -_6oL*UVaaaԮ];QPBRч~H7nܠEQ*Uxdh„ t5ٙwJsttys0>b E}.qoF; 2uMz(_oSbb"-Y*W;(ARѧ~JW^ÇBdĴ>!`Dt]t?w7S:toN~~~Ӂkǩy0kQ֖~givqQYi%ӗ_~I}NG/٤h(33T3JJ'''&J%겧1hժÇ/ C1ݝݩrN*U"OOOQ988H2iXYYє)SC4dq Fښ&O; ~7у)Y(Z[[ӗ_~IӧO7!r\3h4,11Z90BoĄ#ԩ>}:5V^T*%y]y#; ?=w9ɓOOOm@Oxxx%KΫҜ={Vwcǎ<|[ߟr BRΝ; ׯskۻw/sttB"CTݼy{rEÆ EII~~~ہl֬Y,''Gsu&No;1vXVe=JNNf=k߾=S*G?]rŠ}cOf*UB"CR%''s]Xh>)Eہ*7 WMߜwT*… e?/䰕+Wh׮ gO>5HNNf^VPDc؈s#XFF>))44{[|K.,!!AsHm۶I7+T*ۼyAٳg٘1c3v{8;;qƱwիso"Ng.!!{rĐ!CDǫΝ˽-Fv>w>|/߷o_)߁r=e޽ukRȑ#ٵkd?NIII|\ZPDc888]p{rġCDǫvŽ-D@@۹sLvv69s&srr|4--M;wakk߯yX֦Mm5P*,44ݺuKc츷W[PDc8::_s:Yqqx!oElDAA[|'孭?H*eX[[;vu`^ѣ]vi̙Þ={&ۺu+W"YԩSs:Ν+/JSvmmBHʕcSNezEFF`hԨT߱RB`֭&333d;4a111G1c6(>cǎ㞻ammݻ'/J3bBH*?}nl߾թSG~;wN`⫯im6Ž}>˓xjֲeK+(*T㞻(U ( 6`,RұcX۶mKݗ;+,,- N,;Z4hdoso%GzdJ 99+W{^ E1סC.e}0Ǭ]v}6v!VZ8Q^BBBʼbG ^AAsww;[|nK.1mC<}ڵ_[|9cJ*lr]R +**_~) ?Z> {G:uXddFI>dSNe666{yAەhĒ G6nhTMgϖ83Xݹ"8SjϞ=s*tŋeۇR=S6o<]nРK4wbIJe:`%)Spo;v_B\x(F0枻aeen޼)III/СuϽ͙ٙ3G+lҥC8̝X2ѹsgK,..f&L&D"iƽM(UVڹs'ܥnݺ nsAR\Ш_`Rѣ%y'sll,_1c I4i׶PDcԨQCi˖-s7*V(xݻwg^z Z\F_%ZnE%]rEГB܉%5j׮-;{A&X}6S*ځBaQV-Q7r]wY֖eeen~~>srr^KdNOOg&Mb֒UbEVPP h2x?@d=<Ȯ\"ЫW/n@0ƨ]RDD$AmdZyfAVh" ^^^l=,u/tBN,.(,)))bŊۃ朐b% c ___QpZ֥KA چ9\7ptt̅lS<Ν}v9%vܩAe͚5t1e ZU(>K+W䞻>˴k{/s999J" 6aAONqq1dڸqc[r'4ׯ/hO?Ľ-iF'Ծ .PDcsvvvǂy2m6A vsBBBBصkD%8qkN9ϝ2Ȅ|ᇤP(ƽ{hƌeƢ>Sӻwo 0VV"$pK˗5k\fƍ5h Aˁ:t@NH˗/S߾}utQ 3/JEݸq#jY^ WWWI猛;:Vƈ"C׆h"ݻw jF܉Ύeff<.{9|}}%y955=Z'̝2LfDqT\9qI(#0F ,k}kkk (fi#ԭ[7A۷޽[rϞ=;v=_оʗ/Oͣ ON:b Çŋb2!$ٞ={իzmse`FL #,,-n:z9,,L6T*=^JSN%[[[QQմb iӦQNNę^ŊgϞ<ᲢFz?Ē9-$&N׹dqaѴiSQo{bŋKMM-sŬVZK.LJk[ΝVĈbpp,\dnlBŢKX҈bӦMaÆݼy3 vqq1m޼  2Dv(00OzÇe˖4p@~NeݻwSZZ٘ȥ=z~~~>EDDH 4:sJ%ծ][ŒL%B֮]}J)))wqq0d #+V7|Sв;wWvv6m۶ic.i̙׽Tn]3fAKe焄pF_(굾Qtu={&a&FCCCFвR<)رcޗ) +W_MvvvG-[>}p,ԫW/A }0J*F](Sjچ>gϊ^f`,aDQ(Nff&ܹS˗\qzzeիGH^mx'sPP!CHRZ=5jdr&O6&&tgT/-z-fYg1777@={|ά_y,cnݺ|^2بQ.$~mjݺduttdeٳgˋ{? 6=x@ԹcO>ecNNN#&4h ϨG===Z_PFFDـ)7nNRRRhĉ2f`:yDZ[Pɓӧw &H_cӤI:z([<<MmڴP`` 3cԅQC 9>&xtڵ&C3AıJJJ9s=p5mڔFMߗ!KѠAjܸe qYNNNe`ymDQPPh IDAThe]F'N9֭[Ge.Wre|[K.]I5ky3gPǎk׮]c;$.>}J*W,Q6F۷oOނcЂ -;m4ruu9#aZnMK 6y7nРAe˖ta4R*J 6p0ר E)שSGL,( }X_gҥUtwws Sdd$;vڵk?iӦQz(22^ѫW/rww,DP(H 1E;;;߿e8` i邖=z4mVݝ-ZDS4|]6͟?H/BbILL˜MٌPfֺuJ 1`lyYb,VVV믿 ZE 4uԗoTibںu+խ[MF2ej*VH={5kdF. Y}`^#aaaʒuaԩS-[~}OeƆ&NH)))4o>Qs}}}"6l(8/{/ښ={Vp7VNb bIIIb3c<6w\ʽό9T*KOOԧ/+=F5D%ۂ@V(lْedd6l־}{"JqƉ{[V-^PU^{%^z,//Opݻ]BΝ;'HVfMd ,o4i=W{eƭ[D/poQZSXJ#AmHOOgsFh?P1f4Rdsll,|8^UZ?R(l,!!Aa}i׮]^zƔbǎҥKs(3~gNj+..fۂ@h s*׭[S;-[=g8q%cyyyqϝX=j8p}Wlԩf7oy;%8qݝ ?{Z{eF׮]:_Xd \ EGGG.1>H1<<}>R 18dkYBCCIR\QQoHKSTR>Lݺut͋B׈bXXY[}=cջwo;$.hСarvvݻwӘ1c$.ågbJ#aaa;tȜ X*}ĒLP$"sJ]kkk_)22ʗ//.=SQlҤ 5lPв2RJf3wbI&_(͚5<(˶CBBٳԦMY`NpټʈQOҎ;d,.s'nذI, Ţ"8p ]~]תUh޼yNKKFU* XB]8,t;qƍ&1wbIfW(mٲƌ#_]v Eb#C ![[2܉ 's;$,VZEAF*ڵkGKM4} ͋( ͛7e,.s'_2g#=-֬YCpu҅Μ9C6l ҳy1źu ~Gq4t)2BׯdVVV4d|2Sݺu _cKE~ ͥ۷˜ X,N, E"(ڵ+eddl*FA sNL ͋(*J ֭[1w"Ȣ{ԧOA۷߿/sFBرcԬY3:AkeeEo;vwxJ.=cGcǎTJA˚>0n+VUV ^ޔ^*)nݺEm۶s)**?$.y͋(:Tr7oޤ8KceeEk֬U ZΝ;Ȝ|,P$"ϧaÆј1cduhɒ%@.yH ͋1(T* M#$eeeE+V} ^gɒ%\ށ.+_Xbu҅(,,Ν;GǏC  Xҳy1-Zs%YsJ7n{O:O>+Wʘ,P$"FѶmxB[ӃhԮ];&͋1(vE:t/4l0t 4HuW^M2ef](Ӏ(44ԠOEՕFM񔔔DSN|6ol^qDQ[uƎK/ j bccCUV͛{GtZnUVMmeddМ9sdpy'`,6nH˗th޼y7Ё(""vIyyyS ͋1(V\Y &L@&L1#>sҗŏ(z뭷hذa鼤T*{aJOOM6oMvvvS\z6/8sL'hռӐ E-֯_O>>>xb^r;C۷o .=cQe>222hСfw b)?~L&M͛<\Իwo4Zf 書y왨jęqDє󗝝Mz7oNE2(py }RJJ tJJ#Gz1EEEѰaՕwj`Ay1?.\ >HMM.]ЩSx")EEEQzhƌ;z1Ҹn:JKK{رcˋwj`>sw!}(׏d.oNM63gNEr(uOs̡Zj7|C999S* uޝ-[Fwܡ'OԩSϏwj`rrrhҥgѢEda#DD)))Ԯ];:y( c/GgmmBCC٩St@G]v>y{{s"777Oi;QnݺE;v;vǍiosHiT/7 *ƍ{ZF4{ly28p:w:=M2EJEoծ]ۛHTӓI1zQZZݻwRSS)55._LW\)W+Vd9KMMG/iiilٲe}LPpWKsQ,{f[la׮]c… dsC;w.= JBBB؉'t#klܹ-Z9ӎÇ|^Ι3{B,2PմuVjݺ5uЁVjU8q";wΝ;G18" E9rϏΝK睒h?۷)66 71B@nܸA_|UVvJ[n54rP*ԥKڼy3=x-ZDkHXAh`8@$ooo9s&ݾ}wZ/_&NHɴw^zښwZ ( EܹCf͢ZjQNhՔ;-Q{}vh֬YTR%i0(@qq14j(})77wjxyyь3Ν;AuE@hd (** F^^^4rHڷojީƆ())~wjѢ@QF,;;֮]K=zJ*Ѱahǎ;5XYYQԩStaի) i@|N1`^P(LZ~=׏^^6{(::N<0(MP~~ԭ[7Zh]~wjhт8@ԼysGP(B>|}}ߟ>c:x NL]tSNѶm( w:PFINN R.]͍Ok֬4ީJPP~(!!VZESˆ"P4k999}vzӓׯOӦMcǎcwzT*ޣd4i)J)X4( E DϧvQ5>*((ڿO?DG@X,( E u_W^AÇhUV_Ņw:#B(++"""O>TJ7nQqq1ʊFMԩS'X( EҲe(((WN}]zwZTjU瓍 t,F"*55,X@D={[>VVV4e:~8sR`DP(BcGaaaE&MDn4mڔΞ=K>,F"ɓ'xbjРuؑverъ+h͚5 ( EԻwojРsyȑ#iTbE) "yAzKJJw}jժEϧ,?((N֭K6m2C/*6oLo0wQ4Oڵ~Cqqq{n_cǎS#CDvإK!z{Fbbm{.:FhB/^ffee=W5ѣGI&4~x3g999ɶ/ڵklUZL-[$JOO#GЖ-[ )P(TZ5 *3988/_IVSVVѭ[t=YFpYzNNNTV-Y&Pr^˾rss)//ݻG<;wPNN$>|8M0ˌ9RSS髯d`b4YC Rll,T@իWEQpp)((^zeݏ^z:JUV)#J%͜9>SȠI&ц 8dge˖ԱcGjР988^QQݽ{.^H.\ٳgݻz噛sNkצ7o_sL-Z֭[SVYfTR%sm)!!ѣc#L~~>UV222tއ9ruu *B?'' (//s>)6VZrA,++K[xׅ\zV*lΝe^\\&M=_C7+%' 8 ;v:>Bpsscqqq|(oݺŜdC(OԨQ矢nYTTkڴ_w0_ IDATK̆p.6  anݍ:|2ٳ'#Fڹ5kBe6mb +P(L}РAsÇ}^>H M<=}T5~bm۶ӼO/6S֛Oϟ?=7|T*ن $ j4ֺuk{P(&%%: akk6n(uǘRhM||LAAAS1J1Kŗ_~I ///ީp% >Ξ=KM6坊,C'Our$ k}TJJ ]tCF`h/F$̙3^mZ*sQ]6:u5j;eJEJ%-]hR(4w\Zv-9::NGVkצǏS=2jժK<ˣlrpp gggrrr"ggg$___=/!L-o4`SPvvVe̟KRI˖-w*B4l0ڸqAmԅ-Z B򤧧Spp0=zT?1/TPP ,(:88PTTʲtXt%''SnnJ%ըQnݺԦMj۶-yxxȒsqQgggڹs'uQmgeeQll,?~]FߧSZZN}LUT!Viӆ:w,9Idr֭[74 (55(;;rrrΎʗ/OdooOJ֭[Gjn*^bر>f̓S"d!CH٨Qz;tPPK RQz `(SlԨQl۶m%331%ؑ#G$Ǐx5،3Xjjm?WޞKX\\9s&0`שΨR ԩ;v,[p!BjCϷkJ*Un+W#olٲEŋ E^l۶-KٳgjSi؈#AaoozVZŞ}XddWNZZ %KHX֬Y3m""֮];[dbbbE( zjsiiiwʕcFb$my&X!rokOlfkk=O cWs(sssE| gggvMQyҥK/zl޼yѣG޽{$$f͚%I{_z=ڢQFlƍLVj[TT6oViӦ1;;;nQ*lzgșIP2lllܼy3<cO< E4nܘ{E,6p@mlܸqΝ;zΝ;RV=VXXȾ[B &|||Dqǎs'"6p@ -_yxxpoӋpuudtȑrʿJ7|Sf 2{㉦M>}pi9b/6hЀ{%{QRrr2]ΎM8QRHIIކE=uQWjX[[jgdd$}||$yݻwYN cϞ=ݾ, `"ڴiS旍ZSiT79YKP(=T*YBB0˗YʕEg׹ׯ_{iaccN<ױݴiFsZl[ymkkΞ=bȨ+*WZ%g~;vZ=WE``$\^nꅢQ =zv0fcOtQ_r{ޥŏ?(3gr۠olFDDp[f͚eR7`yyyڪhXÆ ʍSL+8#&&FU2~J S/mmmE79::usXժUCs >11{ޥEgϞmb pn9wQEEEl^LsO d(GQHHeeL՚5k$NPP$۱$bBdO?A$ΆcXzuڳg^?1#07o{5k֤rI0?Pz4vXbIP(.X@Թ9ydc`rppehgϮ]… :Wn]]p)WN*U^1Fqqq2fӔ6 4sg#ݺu Zwt32~988tz˗/%KȐi1WOԺ.]O>DbٳE+U1O>>>:@=)+0EEEv|}}%rEw}ZpS. ͜9S.]D'O>!d3J$++G2d˗)//O5j$i-[hAGT.;PR+t:Jݻww}g_N/ekji^IGJ: ^P:Q"EWURKQP@ *T5RHH% {v~3g~ޯBsf<{̙&6է(#PY\ <.##CoԗZ EF#邈6WpQ^^nz%&ݻ2C̙3)>>^~eIDD...\^Onnnɓ%`+WnsͣSRG1/_.锐s>}ufmsqkY;W=WTTpĴP1bەӔ)SlB2VKG{:R֩⣏>JV=t-NYˣ,Y3}RGcd49gS?Էo_3 ~z2.jQ6mY^{*\kbKxb.ٓ.\H> 6y/ȩFi߾}\. r8ط8Y{xxp>HQs1cHVz::(rW^[f L {άϝ*z7H,1سgOIwYt)*iҥt9zǹ}QzIԳ 0ƎK<.ۖ'k{wwwN5(Jwqq1رClN=t:3fY6KRF hҥ d,7776mM>]ϗҐ!C{ے Ő֭уNKff),,i4(m jQ ƍݞ={L =zPxxYۨ Eggg2dۭYFU,t:7n͝;BBB_YYI.-X@/|wtJ5HҥůŇ5K*VKzlEm#ݻw=o^mz)~tqQ?(CիW+2G .|9x ꫊_aP6l}W ";wE)n_b/4hСfmn:e2zyfosaxբE Z`>GJJ W_YsܹsHNjǒ ΪFtb6 @6IM#ڵz~ii)mڴI/qJ 0m}4144ΝKƍ|@K,=Y`UU ~ѩiDёرc dc4h*7z[nmJںuBF3f̠K.s=ǽH4W_QTT͛7MfڵI[4!SE*#P xQS%泩iDBq۶m eb}J(J#|A+ht57o"8p:vHcǎ =ׯܹs)**~aZbuP(Y%''S&AM[l)i;{+2hk׮}ڵK5z1{YLݻ7:uV\iִJIIqQ=̙37GGO2e ӈ#h}Ccms>"lN5(hmcvw((iӆϟ7n(uS:t0{۷+:t@{ݻwS۶m_PP@3gΤHZ~U\Ti։rڸq# 8_Kj<?_.8ebl}D1--JRˈ#cǎQ=_=Yf4|Rˌ Zx1iӆ DoK'˅=8f͚[BFaoEiw=D^HNf͢84i/uٿ?uA<ĺȾ41F?3=C4a*((]5jۤje@5(J;=>2ieD1((Hv\322Ҭ[@)MѨرlԨYO7oT$j 6ӱc̾8}b])>c:w BթS'Y_pFRˈ" SÈbDDYoI)|3wDQi>>>4o(JdQqqX7ggg4Wb6k'8S%;wL 6P&MY۾}cǎ4i$Rxx9ڽ{7Oέ-=O=퓒n R(J)~***6ZE~Fwe6J'WWW),,?Пi=qU1bSb ,VnݨO>m)SR E{=FBQ缔BQ(/͛7.\@ÇWB"={6iӆ6nȽ}kaBqϞ=tEn͝;W6vXm2?jQ2i EN'i> s  &e^Gz=͘1hƌޏ`?6mJfͲ/ 2dŊܹ39[{~... Œڹs'-(J4[7H%U;)l(H iZzg4otRÇHoAfͲ)(,Z(s]\Һu^"lFw}Wv;|OHVZFKKKG2GBQ-N9R ; ]WWW1c]v^y;S=qرcosh"2kܸ1}Gu:t(SW損RˈBQ$k9.lobxxfeeH[<;,,Z-M0h޼yt?OP^ܹs<YPz[?" µM~NNNOv;ǛLjQrHȾPJm2G1!!W(ԢE I)U(}pLL 9s֬Y#4z}ѿojݺ5O< qo|_~I-[&X7xdh"7(JC@@LZF]f7kLLDd4iBm۶U$y͛7?.L!b||<^^^m6{CWM4zKv;/^"etIZF͚ؠAB1>>^ͱw^j߾=!փBߧ4m6nܘnjW)VKk֬!^/9s}|yS˩g96M-#1GS6m$mdo"ovT Ţ"9s&v~aZf ngx ޽vΝ;gbSR ŦMrĺeDϝ;wV()::ZҶJ(^|Y뒗G](B$ѣiɒ%(mP.]hΜ9\z뭷^o2)T 륖uQ~XLKLLdg~]!6k֌/^y-c4aEn+'|bц4hЀ~.ٳo!+ RϽ eddD-~K.v VPqN?9^ˬ"rNH/"}g &ȤiӦM&JzW8dr1 ѥKަEv>=jVaU֤UP$":xc"_Vηj*ںu"mO8*C IDATY#yspp 6Pnݸl2x"6(53jT100Pv" b:}Y 4HlՎ(? \_yyy4sLjӦ !rd"ƍ=~xںubr4 ^̥ׯٳGM#.\\A$TŸc eb,kpD޽{6QUUE+VH?]ߗY VS(иq`tjҤ"2>C7nF#=s]{E"*+i;Q,>|QLNNcǎqikϞ=ԡC2e pif5"ݻi銵ߪU+:~8U@Y{=W/_N5(^|Yҕ{V ӲeK\.;5 ;Viذ!GVJDD}!fU"UV)־o/ŕojHk֬rj.\PdNMM#F3"##)""B˴i$o+P,++3{ql'ʞo?Hz 6mj -*_%/ݻW?۷/sqkNڄiD?8LKӦMgSDP5jԈ P6b8;;ӄ dcŌ I:-#fbee% 6\ߟ.\@CUq~iԯ_?Nktޝp? ,޷Dll}KMMwu:uJygh4H[x'%;Gy+KOO} ]~]Ro}nG!<:#22ryύ7ؤIVo{ 6k,VUUxn۶t:}OP:ڷo/߶رcÓ'O2777}oP?c >\X#FsFF9pqqaG/jsuuޟ۷/,}ݜ8q"cXw%[PP`wŞ~ivV\\̪Xbb"OXƍx<67<<<$Cᡰ-Z5iD~PC9s氢"EիWYFWjPLNNt'4oF#y~eqq1{{sZJRRYY)owףG݇,ְaC' *++GѣjYtU V[tR6vXޏ+RBIZ mMDÆ ْ%K/cŋ,44Tx E{%JNcW~0 <xWVU__ghٲ%[n+rA+rCbff׽{w;::ZsƌPf.])**_J?lWvMڵɝK GGGaÆz_xî]&Ń``bꮺ^^^lرl݊Amƍ6L Ek@3M&)nrqh/JKK%>vK7X9ѯ_?V\\?,β/xLOOg:u~x۱cwe?'OfcǎetS6yU_PL.]Nt{I}`v##""޽[rqM5#Gܧ[ZڐD={ֹsg}m֬+,,Շb.TtI{ӢEx!~G^zFviujOi/٘1c_Ͳ˜/`P{(u5}ꩧ$ڭ[?lܹLV̑%Kב#GN+| ]*kx˹ N}*([b\tIUС;q~Ϟ=WNwȦOn<$&&;lĈ,::ڢ߂c=|MqFY͞=988^)j/k۶֭[3lGpp05kE#i~VݠF#[~= Q<6mڰ[ r-xݞuV.}2lڵ9~R# -_UUUq3)wѸqcI@``l/d ,`SNeGf=z`:t`I&ALJ9991OOO˚4iš5k:tvƏyj*cحx:RBQZ˖-^;\R5J\\\ن 2Zl)i=D~1p@Y˯0vٳ=wqqa#Gd;v쐝#c ~[ҶN׳ÇJJJ؂ l\<:x $KĨQ|˨dS)BQI>͚5h4\\dz_|έQFge}/xW\YXUW^~/2UVV֭['vc6mz< 4isrr}֭;Ϗ]xavj*֦Maj޼9裏d_S[YYb_b<==7M:Ep1zٳSXjժYۤQXXB՟K6""]9#ٳĭͪ*:x ޽Ν;GFFsqq!ooojԨEDDPӦMUVShh(|t:t(eee_E-[4+WP͕H~Ν˭|:p={\BYYY[nnnG!!!I͛7|PƎK6l ggg*++3VZŋ&GPPܹڵkǽ3g7|C6m$ݝ|I8q"uڕ{SO۹+ۂ#00-[LD_ZZ;vM-{SP󈢫cn'u"s XFFuSZv-svvR.믿3sw3+))...ڱƑ~k8p^ŋŋO<_M6e ۵k+sv5X[ƍi4zhjӱ[EEEd?>NG5(zxxPAAm)55sF 0~'rpp w13g͚5oLfwyEF{4uTIшNE|4h5?TRRbv[QQQ3=n\\\ᆪ'x"N.]xLʢ7oR^^ ___񡠠 jݺ5EGGSvAYVVFC;w*Ҿo E?m]II [h]^q\e#9^>—---lK}iJJ >\5cKjj+7iDq+t:[dͽ.JMMUD@@{wAfO… UQ$X*:d~<n&M*|I;v_[[nDž'ѱcGY\SR<%PNNNw죏>bqqqK.\Ⱥu!k sn7w\˖-3" }駒u[BB{饗d70yC۷1//tWLLǴI ^ϦL8ڳg֭)RDEEɓ'uֱ+W~q;{6qDָqcۖɩ^ϙ7n0!!!f݆*;;ϛWhтmܸQXxq6zh?>>>,++ޏgU#JELL ۼyEFx"1cy<Ƕ Z-4h;q#CYzb]!<6p@}Y$;UTTsα+Wdz͛]S,u^qZYYz)KKKv<ؑ#Gܹs_~rquuyFac~ծ^p-\S -ZPkl޼wB६(!!.]D.\ .ŋBHNGi&eee4vXڸqo߾~z 43о},e988P׮]cQ۶meO?oN2۰ahe_NFcǎ)5h4RnݨuPPPN#OOO""*--2*,,"JNN$JJJsӧ%3^ B[n5{;ggg~wss!CO?M#GGGa :p}yfIV E+)00BBB( BBB(00)88ݝՕŅz] *((377 )33ƍDiii$,,ON'?ޣ/Niĉ4d"VK׮]-[ʕ+6AAAԶm[jժhт"##ӓ˫yyyTZZJiiit JLLXx2 =$$LB 0~GZnU~m#G 6V___zGGGyڵkG:Nʢ]vю;~7o*xrPAƍu֙MUU6<==k׮ԱcG ???ISll,?.\@GSNh䜹rlNMQUU@&QPP@+wLJ"""AJ>>>JzfjFYYQVVeeeQ||<%&&(4" rqq1{J2Q\:q4ЊNCMG8ѩNXX8gk3ٳ)""g`c<==3gO>uՌhBBCC%m9\.fh4rJoP$7۷իW*K#` Bq4ydM̓>H NIAp){ͣ{ NEFFJ"¥PLJJ @NZ- F'.+VЍ7x4pO": H5-.fSnݢ={y4`˗DtVI&BŀmԦM裏"E̝;Wt VM6420D"6m5jԈON'O<)͚5;ѩXvI֭[Vu("{ZK.ԥKj޼9EFFRƍIǬeT;o=e4)??s7777rrr"NG5 mԯtm[JvjRΟ?OӧO]vNO4x`;}4uA@/IM~W3Ԕܚ[rMNK5HԴiSjڴmD999EٔCTXXXSUAmII }wI:1z7>_Vm_~ԇ 5nܘ7nL;wzWRQQAGW\3g/B'Nj$&&Je˖-OOTO0Wh˭[Da51MHHw]]]iӦM111Թsg2*d+ٳtY|2%&&RBBڳuޗ SҴiӬt~:]~͛TPP@TYY5w{5S_Ϊy|O{:iРAOSYtJSUUENӁѣ&?_%;;GǏcǎlIDATӧq!׏v)i^{.]9# gcEEEV~5yzz [u֭[{1 J~n<GXmO!1٦M}f4޽{٨Q@X*Xeeb 6m4.bǎ#[?jCx1rHVXXcŲJJJزeXTT}@-,Y¼lh4vMIϓ $ 8D6mXjj*e@/_`z/ee˖+GXo`mqݻ*$eѷ~K͛73fPff իWٳ[`z)yXAhC._L#F&._LݺuѣGSjjtBHHyxxpi+99zI׮] EP EsZh4`0ТE}taX=zpi'''G)))\utԽ{wۣP~7<==Yvv6N2effbQG|ײ_gSN|tQs'22RxQAj*iё#G}+p4p@L0S}mII )@P(ڨ׋NlBztѩXaÆ6.]J7n͠A$o{ 2[BF]|҄=g}FÇ}a󟲶?r c8 Evq!;k,zkkХK֭sssiȑTYY1+PZ(~D'IJJ1hʔ)rJ>.Z[:u*srN;F?9f#6,''Ǣ E"@=oߞ  y۷ӗ_~1#PY\xrss9f ?'X"F#i|z8gjO^C8f stt4o<<-hٲ% :ToN95JG :9"O>D"tRD r>ujR`0`@X`Dц*~K`kڵkGO?mcꫯb;#FQO8A7n*6UV}0a1{ [pB˙lذʀƏ/k~O"`k";|Z..\`@-+uWZZÅ!>bbbdnZx?(ڨG}t:v}pmh4ѣܹs\zΝ+y˗Sff&nj@ƌCnnn@~7BCCe_m@5H~3???}@NǮ^*<))t:}A'0hȵ,XM{H3gΔʕ+͛36l5mTVk׮%)#«U?e}ɓ'^'BK~UTTJgDM:uJ``<~ T@pf͚z#SZZVG dD۶me-S_ :O>w)N=V3 4zh Ç~(kŋs쭷ޒի9dHx:c jAz ߿_x=L$/UfcUqpttd/_:|駅!>4 ;} B!<طo7,"/^:EdFDlԨQqQBfO!:v(h4 e7oޔZ\l~ ć_ B!<ذa7ŋ e۵k'1c %Zh!/U2qƬR֛I||){1ƚ5k&/ѿ.ϥk &BxvZYo$IIICx?53~y}A ///$TRR!N=X-h̘1ژ4ir>͘1"""d?r? ng…!#+VbͲq[NxGdd$+--=c:t6lQJJ sss̈́"&&Fo$yyy,00Px?ݻ|߼yiZA`4rHATOafhZvYo$ӦMBr`g͛7 BL888pcرCx6@/7˗/a\>clʔ)˖-*..f7ͅfD`` u떬7zJx?ýZͅ a7nk&? ̈z#9y$h4@9+kpmx]atԉۅPvsTOQѣ e\>ܫ:tHxHnSXpp>!l3JxzzҚ5kHHnĉcY؟+VPhh(6Ϟ=˵=n{njРcӍ78dw(UbԸqcYm,ZS6i̘14bP~~~oc?ۭvDFFDn*_ K.{n}A = #Bر#y뭷 aW!<4i7ɓ' x衇XYYע)ׯ_O21an$Vo^"!<Dڵ++//)O"((z$'' +o8;;s;#tSN1WWWCWb+ӤIںu+999qi/--㹴`/\]]i֭K4oöI&wQLL vSSSiРATRRµ]:VAm6&g0Թsg< EH3f̠ .p/o߾ε];ݻwS-ʵ=[o~Y1ɉ***,V~f͚Eۿy&Ӈ.]ĽmݻwS۶mɉ{6lEё"##- +M4.\@_"Eb~~>=ctymF]vQiUvl^M6р<~˖-鯿`miܸqрɓ=@}PϏmF:uR1kя?H]tChƍֺuk8p 9"4h9rD^cDEE8'v}E 5Yrr|}pvvfc˗/zG`;v;Bxvݻwg7o޴țhd@XSh46eVRRba}W~pttd;wfSNe[laEEEBqqqiӦQ+'`W1~xiÇ#l۶m} lj'cqXcǎ0prrb/&e!|? ٙ͜9{| |?RhZz)6k,i&όFC}m۶1777 GO#""8qB{KJJb/"OK+2e KHHs2V+|)Y-ٴiتUرcDŽB/̙t:}@+4/VK/}.::D۶mÇٳgTtZh4ԩ 6&LnGp13gXn/HaaaNM6MRXXiZ8''}YڵkTBQ![+VP׮]Er_UUUK'OK.ѕ+W(..Jtzuh4HQQQM۷޽{SXXd+**}ў={诿+WPJJ贸qvv&@ ` 0 R``Mȑ#4j(:`P(rHs̡t:td~:PJJ %%%QJJ э7@t`\\\(,,aÆ5EDhh(Pxx8T-\BIMMTPP@%%%T\\LyyyANIQ󧏏O=<SZt)effN@6U(>tai q-YV\dbXѢSd4~իW/[+ݰBCSRR] rMztU  Tbb"m޼6oLǎ#lcl6U(ݞsN-b4̙3sNӢS6W(EEEL} Xk׮N;۷n޼):%dbvСCG-[R DVTTD'OGѣGرc!:-UB6777 @jРP@@܍ח|||jPQQA [.\k׮`*Uh.FCFMz\\\LJ\\\HדQ}'"rrr777rrr"""///jj˫kQjj*effRjj*SBB]z]F(8C^oggCJ5vww'GGǚ{zzNOkrX` (//eggNPR(%{%Fsjvߩ]y{{F#{̺~~vs^JJJnǸ`0P~~>UUUQaa!SIII6TUUuVVVUa@b* 5RkkW{/0@rgd)TVVgZmw~eQ^^^Ϳ,j.@m($ E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` E0 "BLB&PP(I($` En^`%,Q`3OGIENDB`slidge/dev/assets/slidge-mono-white.png000066400000000000000000001163631477703150600204720ustar00rootroot00000000000000PNG  IHDRtz pHYsatEXtSoftwarewww.inkscape.org< tEXtTitleSlidgeqtEXtAuthorTrần H. TrungU.y IDATxwX?PPPP+V0AŎ l5^07Do,7Q7,Q!zEcłFEH/Ufggv_Ó'xgX9G#B3ADDDDT z+ """"DbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ1Q$""""(VLH+&DDDDE""""Ҋ"iDbHDDDDZ٨Iddffٳg4ŋFVV222PH888`mm kkk899PT)D].::GEVV5kN:AѨY|:u xT8::)z.]xܿ<@RRq=<|يJBRPtiTRX"WWѢE9?<@߾}5k͛7CY&M۷_#BCC[[[#"%hAwD=ŋK.G;i4xxx86lM4aERO)))hڴ)nݺ+V60hyyy:t(V^v;vDdd$ )%&&"::Btt4nܸvHh4D&MдiS4i 4`Q;wƞ={٦cǎ!?j;x`XBለP(*8p{lmmѴiS 4svoctMT۷k׮ GD\p5B^^ǎC bccelٲ/^T;Ν;sر#\\\hi GD5 .Aaʕ EDDDQO ؼy3lقׯɰFǎ={ZsMG呟/FAbb"*Wl`d{=tLŊߘBD%{.V\+W͛jck.ڵ  ѲeK޻w$ʞ={0|p"k++^7##HMMųgϐL|RYYYL2{)E{bؽ{gvpXl-[jժaĈ6l/vhqY9s.''Gcrsseffb_qYܾ}[Dr+RƍӧFZgDŠ+P^=t;v`?*U¤IvHիWuR+++&nܸ*UBPP6oތ7o2I$eeea֬Y2d>,/YG?>WCҥKjdQ>}iӦr;v,OCRӧOu>ɓ' DB:)Ɏ'A0|p>T;"qI2Q"'' ,@j0fܽ{W,ZFF͛OOOL0iiij$t1ǟ)!.SK,Q; BݻWqL!22=z4 ZjaAd#Z8E߿3fP; "Qg0Q]: 00׮]S;zcРAALL5VѣH4IǙ_s!233waR;,Y`ooE ίy,\FFgC'N@1aL6 jDdvXQnѢE,*pwwc-:Q<}4BBBv(o5jpssC2ePti.]e˖EҥZ://)))HII$''֭[G||<^x!#??faTZU퐈 +oŜ9sHH._[%4muA5PF ԪU e˖U(S ʔ)ooﷶsIŋqq\t(L;u4h_~S;""""yD7(V7xGGG|)qFtUhQeggGGGϣh$/p}M\2GH>o߾8tj1XYYvhѢ|||ТE ԬYw#P*T:tرc8qJ|iii߿?< r(HO HheŹsu|\\ʔ)#S4d꒓1}tŒĆ ^z78x 'iK*}*U ]vEѡC1ŊCvЮ];--۷c۶mqcZl޽+V'2RgIsE166'Oԫ4&tŏ?(i4(.^G60i͚5ѣG-Z0+h߾=ڷoW^Edd$V^mНDvލcǎ(]KdN&\Q\|}<{LHT`ҥo~O? Ibb' &&W^ٳkI6jBXXA9A˖-qMH]\wXvHٲѰaC5J$uRJՇY'999߿?~'ըQ#,^IIIXd 7n9M/V\$,X+WVׯ_GVz?"-[Ȳu&s䄑#G">>+Vw$Ld-[&K?/_2~qqqիZjǏKxk'dvbnn.})˖-{EX[[r kIkxqa|G#H$VҥKe_HHH/2./^YPZ5̛7O+++!../{E?ۨډ"K/FB|Ljoggg,ZkiӦҥK˵0}tE&27(%77WhY#u`Z*s}شi=lР{=1dVw޸r nbիcΝXd dڴi8uV}vܿ_Tۺujw}B"#!~7ԭ[GÇ%#G`˖-U":]&lM6aҤIYdIlٲ7oFrd j!///Yŀrj"cŠ_Nb)Y$-Z$޽{ 'ЦMQ5i&9r-[|ku։v}?ExU :TG^zA``l}ԩSNa{u|嗲IdnXQkՄݻwj۷o_l+V,;woxW"88-ZRJa̙x" ;bڭ[7Y70D˗u}رcVl}tXb&O,kK,Ql9`EXbD8p 4 z%ëވ܏#BCCPzŋqyQ˱vbA&(~嗸x,}i4̚5 ׯ7!$ޔ)Sxb٪ `„ M^Qʕ+ES6m Gv%9624|7R^3mmm1b$$$`̙prr}+DkĂL:Q'yh4̛7׿d鏔1l0믲]pׯ/"sc`UV%KPF L>]pY,\:+z˲vbA&(?DFG!2R| ߤI-[D+˗/{{Qq999ؾ}H9۶mCݺu1|pdn޼9>}Iډr;&(~w?齋F… 1rH"C6lM&K_7o/"K_DDqs(>~X:uB_͛7)hժzL5kbӦM8vZjWLjXI&3g,}?~,}a}ײ?~`RXrEqV[O3h{kqfǫ̯2t駟p%Q3 cܹST[%&3fֺ0ܹ={g;;;;{o|H"&dffrR0|pYg2;VpQFھL.Q<,*U`Æ fג9::bÆ (Z}-]ṪVO8+Wjۯ_?k7?t̚5 j’%K0Aub̘1HII׫̛6mQN777QgRT?]MԩSe ?XQܺu RhύMNIwQQQh֬q ԩS[n+couDׯk'd2bjj*֮]w?s̑2=z@vԩSHKK)""fEU+++߿v]vs:7su]vOrxb\pA%%رCtS?d 6轟s-$SDdƏ񹹹'"gidݻWT[mk'jShQQZ^|I-2y)\X1ի6lA^; If󙿀ԬYS>8 S4D*g2y@sxx>MܫY&f͚%zRz&saaĂL"Q?Z<==EmDM`ԨQz~!2mVQ9;;~ҡC+Wve1K~4hÇcbddޓX XY咞>C^k׮I$JdN,xƊjۧO7tAd=$ΝCv˗/Km۶8y$6mڄի;w ɬIdNwx ;7774mTyyyHHH1""dI.k'J< f4g߿!Cqz=W۷hҤ|JډJObyŗ/_=D&k׮z/S$DKH =ggg_նߦnݺ>^v NҹKsVZXr3Xx1Ο?~`quuUtĂ>Qx%L5jfnT,agoooԯ__h'b|$U^=M"CuD-E@Ib-ZLYDDjժS"33SR[ɓ'm6g2&%%;vV(u?S>Iȴ5lP 41"d EU%JW^zu֨SNΟ?'O}>Sset_|!V0#''vpQ'ׯ_|z[hH]`ښ`ŋ/ٳ2Ҡ˦TULMM>ʖ-%KtOf}NK.ΨE9Vghq}Ejȴ{EQ3 䁐8;;nÆ HJJHY5kO?$z łk׮O>1*\xN,Ȩu \\\dL… uUJ̛7OL9WsssERF 4o\s+VLYYY?l56gϞ/dQuOf]ɐk'dԉ_/.y+2+Wƾ}DUիÇdɒsEqݢ >… n'O`رhڴ);&???;w/F2edprssEp7ډu#S'W^=;w.SZ5̞=111(_ 's(YFT;kkk|GSiiifwvv6~GTV C^^}ѣطoҰv!(6p@ΨES_qrr)2uŋW_}ܾ}GšCp ܸq&LP13׊ӧOuVQm;tȱcNJj'=c ̄nݺ/%S[F l޼GCsjquXH"!sRbEu֜BZQܸq#DUӥKQk޾}[ܹsh׮z7n|;,XK.w D]NTy(vԷIOO)""#㌽(vIF 44TTɓ'hHѤIDEE|BCC#GV Un:Qk'ؠo߾H;NTs"!"Fs.]dٺXn&(ڢO>zѣGi*8CdX\Qܼy3222D5a:v\ŊԩSqM \Sck'd" :T~ۇ۷Njb5׌)WŮXzuQ+3fG_hQ|HHHIPX1e)V]]]jĂLܸqcYqq[?"0Q4/ZQ}6>,1Xkkk_^Yvvv1bnܸnnnotY;_~F[5qd'!!Ç/"?R37jKcpQ[YY!$$;wDF µkװpBxxx!ډ: Xد_?~7l؀ Ebŵk׊jgk'CK.h4/rJUVjWn]Yl}8}lY:E&IJhT}=;&zbS&H̚5KEqlڴ 5kT8J˕;wj;x`яݝG![U1++ zNjCIŋ￯p4ʲſ/\z#GmѴiSwVZooo,8`D$StoVYCpa/_^>,͋ ΙشiAAAFv.<<<`̟?uQD ԪU ŋW;D#KΝp41ɻQ^=y&C$Dz6/VQijgD5ƭeeeZjsG&M$ܹsN4C;[[[cѢE\zxl}Y=S(];J*U8Tb...FvbA&(@-d_&667I^ǡgbJd۷OT )"''ׯն(RϤ3f@Jd޽{h۶-+kDCEJVE1<<h4F?yLbĂLpoLڵ+/^,kDCŔ*ڵoUTQ8Tb}ĂL:QVZ᫯\|ӧOeqټJEٳT8dz>|h6k'dwI&O#""ШQ#?~\ ͋TVq+@!Kډ7@D0 6n܈ի+͛7Ѻuk% Tz6/PQ D  ~` @2eF>f(M3߲e ?77fB˖-qEAd8l^LsN<@ܹspႨ{hVwgooolٲE111h֬>3*v"Sġgb EU*UUV GCN,:t5k}?Zj^/h^ѓ GHد_?X; ;cɒ%-)Vq4ܸqCLߝ-[ ǺuPvm 2qqq9/1гyR!6sUGGG[hR}fvbAf(@ݱo>*U`ʕ+@.MCX+yyyXnAAA\;gl۶MTN:lٲ G ;hذAϛHM6رc7S"cǡgb(XbOJy}wŅ  ;L2>|89vHD:гy1ƊER.YYYXf ի7tСCQD "3 N ֯_WWWgϰdj uٳvXDpټcEQM -Z1c 665$Jvv6޽,_F 0p@ܹsG\]]7(vO?v8z144_}={lMH"]qټcE c FD݌3/ޝ (S "##%K{nnnn۷/"##vhDгy1Ɗ"WhCU; Y0Qbqƌc x~WݻwGDDCf+kV"}bڵfs_3P ΝӧO:\ؾ};QL 2v8EJ:VHHXQ4999aǎZڡȆb!ׯCa֭RӧOrJt...޽;֬YgϞYJ*| eŞ={~H ߿͚5S;Y1Q{|2[899;48ܹ3-ZӤmo^hH_R%03[l!E޽q4iDPdDQopM|(^!*;;{#PB4ofB||ڡ*^8F)FرcaER 9͛+~.h4С8͛7&`ڴiy&+Hn'ODXXj֬ ///|78{ڡ:u*4h _|___#"WWh]M659^M"={6nܸF^~W;S#|X`Gʕ+#001 /_ћړ'OЯ_?ٳG[[[믿ƤIff9Át:f„ ={B]RR>$&&"11ϟ?G^^ %J@ѠhѢppp;ʕ+xxxN:U-DQF?ƒ%K?nnnݻ7___P%l߾8{,RSSQ\9o#FQ!ڷotLXXf̘PDDdhLH8q|ݻ7 $z8tg"mСC:_cڴi EDD1"((ǏGTTzikwżyаaC4jsi -"2]"if/&M6D||<&Ne˪dgϞ_| *SNشiw!2cLTV }ܹ}!(( H(!//{Ň~+",, jED23qp(5i&ܼySNHNNƬY鉀DFF"77WH(E/_&MBbb"|vXDVB۶mtRܿ[nG}GGGC޽{2e *V\zU퐈HV)Rw5k )) VBN`ggvh:Bxx8C""HDLݻw#99ݻ7M'yyy@ӦMѮ];ڵK퐈HAt>E"DD(Q ͛#lݺÆ CҥM'QQQҥ 5k`DdX(EdooݻcŸ{.ۇqƙԖhN:vӧOig̙xcΜ93ۇM>@\\Q(E3qa߾}x16oތ!C]Jl޼OvHDVY+Vz˗tfΜ-[BѨl2ԬYsE^^!Y4V[.BCCq_еkW)RD^S78Y,V*_<>Sl߾Xz5u[[[Cۉ'ШQ# >iiijCdqXQ$"&ggg`۶mHJJ/6mJ_|,Y^^^R;"Š" Q)U>SDEEΝ;QV-;wСCL8999jCdXQ$"&Vʕ×_~8:t @ѢEU'??3g΄URHDLI֭[#<<III;w.Thذ!.]Z DE"bH:)Y$ƌ/"** ]vUe/^`ذa:t( ~~"K"1Q$ڴi۷ҥK2d*X:uǏ ~n"s'ǰHd^(ԩ˗͛ C% z(4o׮]3y+DDdSlY̘1 } h09Q$"&$%KbƌqFiE>}lٲ #2w(ERLٲe`š>} 22Rs;Vӑ#G0n8uڵ+F! v d;#F 66Vs!""=zP\r˗u:wU("" ϟ?+Wv EDcǎfXlQEƁ`|||p,X|o@FPPI%ݻذaNOn 111”)Sƍ FS4hl20Dӧk/Ɖ'#|׊ j_V(&$$vvv:]Cǎ[>Ə/8emm-tE߄^}߿߀?iu߿_P,Yυ۷oN}}g*Dw1?ʖ-kJ͛'ܻwOh&(~gK(!Bɒ%C2eڗ9$SNtLNNV;t٤ 'O/z M Zҹ~򆓓#[Nhذ۾lmm]?v} 0@埴|233իW 7Vyeee%tM8p?B\x7N{V;,իWի+yA/ (vEko>C[^^0o#!66ϓ'OT|%%ԫWOJ%KJzk׮j.|o@W5kmˤQF!//sƳyY&N8֭[w~~>K%e6'aݾ}:t1cLZ{ݻwǙ3g$c n'%%!$$ڵ3:rz)rrrt>NILDDDDnݺ4iΟ?x{{cΝjJ׬YG.] )C~d IDAT֭[0aZ,IǙWFzv(۷oGӦM%oN( 9sF jd0j ѣGѬY3j8tݺuCbbdꫯ~ ,z)sfgg5k(޲epqdeEVVAmuZ.RQ|w//^P;L2[FLLeǎܹsÉ$gFRRի ѐX[[cӧcȑ&w34fR+X<{ :u† (MTT{=رCPTcJxhSNUdJ5ddd`ܸqٳ'=zj,&(޽{? 4p4dhXv-+kϟǪUdӒYBE1)) m۶šC(i4~Oka’իѨQ#m۶yrj1_tXXmV ooo""C{UYlӦN2/_OKeń4k .\P;eJI?aȑMfAĉ1h r`ݪЧN--"""ФIR{.-Z?\,9W=zlhPBTZK䄌 HKKÃpuf*I?eeea vNkkk899GYYY֗1]aĈXtڡLjj*u5k(2]:??cƌyÜL-OҥW?QFN,V322н{w\~]ԫW5jԀpm\v W\cpQU1mЫW/jIFSN@J(3EocgqΝ;vo0`,oH7nTRD,X /_^k._ڗR]j0qD?B %]c^ƑlܸQ0l0j_ߞ>}*KtŊitB(ׯW;L2"Ok۶m\9$Riccc5wSWRtKaܹBٲe%]wʕվw}J*s#:]~GZy}-[4~-꽗z^po^GaϞ=o)VZZ1D1==!!!EEbʔ)oٳ(6a75lRA˗/ǥK$元lٲ2FexܹSOaaa8yc5 &OK~3W̑f8{^}L4 [lLQ)M6ضm%rJ_&QE-ڵkWDC~ҥ$%%(X*}<3"=='O|;vwwwROvvkO{O?$"E`ƍ}5j~;x ,X xkkk,^SN5׬uXjc (ŹsbƍvMhT sI'NDsppΝ;ѽ{w2~j?$hda͜9իW\wQ5Qx"EߐZ$egg'f'N!bpJ\RR͛7fС&?Eh! E!SN ibٲeXt$_:t耽{ё{lAꄄ;w'Oݻ;xxxH'''Qsss3҆Xx@/OsϦE} ž}JZ(J٣(dp/..ի닐Iر#\M6zWFDԩy ſ/fQH%vgjժq<(sXXvV(*G1WU*z)aFo=_tI6wq0a]*RfMlܸNBf͸Gi?k.נ)));1 b_3nnn21JQ ׻J2ңxmhkܸ1<==%ȸWݦO>dWK.޽;nݺ=F7|=(@ #66˖-CFʭ̓'O$?1-ɢڻs_*q=x{{˾0zll,>nNt1gL)xBL>oƾ}ЦMy **J4raN=<(GzW7%( E=RӾzޓ=H k֬U:uꄫW:UDdRЯ_?bh4}R( 1ۛcرc6l3gJr}"""0{lŌC|I E7oݻCqELӑ#GDܹ3L̋RzTTppp jncS_یL&77W6wx":tÇÇ\bsθr ֬YcR_J$/4i=zp{ 1-wվvfXJ)?F)G1;;[7}effF)//O6NNNC7=,"Ν;qIŎC| Eٳ8~8xtܸqCB]v唍yQʭg9R‚zjc&P5/P a׮] [stt۷ocȐ! ]t'yh"͛7os(GQȠus,KJJ7i_C&d\{dԭ[K,u,,,#** ,0/ )`ĉb]p;v(VƍEŰG޽9ed^LGoW ]Ѕύ^R2aC/GBPVSHa˖-@HHҥ ]իW8W1X8j(};!iHKKW^甑yZ(*a9Z(wˠ;!{KHkeP[5jZO8ƍKrceBQpU #w}':)1[φ., % ]Sz zPtttԻͫ޿CE۶m*& Cs0XSNq9n2;v 7nJ*0`̏Rz%2^H(@)ӻ1-S"00v? R-===1fnt:ƌ t0ưpBqFiE;;;ۘcR([( Qw;( )/td?ϝ)i&xxxHr%1hfѣG>}:xDY.ƍ!ETR(111ޛVVVd#lXt:[ ?'ФIIΡD/0p@1W۷kLb tܙ>DRJG1F)c֭Hq NBQ—.]Bf0n8A+}ܹKr%3x/~ g̙1oJQCJJ LRzE煒[<=֭[R:޽{9sYZÌ3D~\=|_}8 6E9Pʂ^^^ڙRzlVX(I[]tib%R(ܹsQV-1>|w},g)NqqYGsf/oJ\F AmlRzU*޽tۂIYۣțٮ(l=_?~}Zt&MX)ֳB1**s&M)=/^(Q&%??_aR(HU\\\ߛzBV(ϖ$i׮[liӨX4A.\ꫯV0JQTDDDHR:%ĸ\tI(e(W)x}̘1! oIJuVe?ɓX4!2duv>}pȊիW׻݃$x)3o^ 5 b b) EOOO~~~,ٻS|rIbZ | t:$`Ȑ! ?#H)Lf &<<ܬ>G6iF&11QYÂJY(@$OP>H֮]ǛՇ)..ưapY.N*X SJ"AkffffUbRRvrh޼^m/Q6!55h{{+|;B IDAT n߾M92BxV ]u֯_V -?K,)G1((HP;s.G_x8p@Lþ}y_v҅j8q""""0gޗY FS(VV 6llroÇ'Ҙ5k6n%Z7J^{*;&\bǎK.]vj/ubZкuk.v튫WbժUVEFS(@ݱtR߽{ZⲴ c òe˸Ŝ2e޿PH(G1 @ǏK 3^[npvv3ưi& 3Ott4OQ1.`ĈڗC<~8CQ>#⧥GXrY|Sb?_~%7_-yzmllfDDD 22R/grzs߸qI]_v΀aǡC z[XX`ٲe{.C4+য়~B׮]%_TT)So߾fケA~n:n1k.qI^EY䜉q–-[3Ǐw^ViիK.z+))AZ #y( E+++ڵ ח<BƍgIC*'11;vđ#G]f $/2Bqݜ31.fb׮]޳o(yYjQc jL[QꊃJ6TJJ q!++Ks9sZµk׸ ù$[ЪU+h4_&K?CT sķ~+AFC3Bx͟E83*99/r_`5kԨ!wej֬)y;r䈬yt:A=foo\QQ?~<ܗVFӱ֭[} nnnɓ'r_)d|Y[[ر#KOOESz.;uܩB-57} ru {AKѣG ~B/ٳgFsܹs]ߴiD=z$[R),,dÆ Ex"[ojk׎X%''Lff&۸q#֭oB!C7RzXJBz/uhii)˭td5ܗ)))ļ{XKx#G䟭lXPP5k&*OOOv%ٮt֫W/MPdJamm6n~x+..ff#F`vvv}lj5[hn EgggA-w1򘏏פFa/^2!""u *U}ھ}k߿?͕cX`w.\ ޻w9::{{{}vYK. l9s&LP,O?IYCRf͚vi:l-]QjU'@HA酢Rݢ^Fs=YYYlܹ`M777/BF|]o&-,Ѕ:^~=kY^^ܗl2Qø>.\c7od76Kɉٓ͛7߿hoSgddǏ֬Y3V.hӦjSz( vܩ@D)fCղիW3?===e5>Zjl߾}ٳ^z.^jk-τ \O@@;}ܗSiW\a-Z}璏I=[bɒ%[.6l[h۹s'qA%&&Çŋ!C0___ ‚͟?04BoݼyS_pmѽp*M4eff,x石ի~UZUrG^!n߾͜D=F2 nb3_ces&JƎk+d$''I&1 .k>|z-˛MV޽;=z4 f˗/g[la'OdW\a,**tVXXȲYzz:{!{r 믿ؑ#G؆ … G}z5j$ :1GzdXm(J/B{ܩ-/??۶ml|w^6l0fee%gooov]AթSGi_=g۷ogz#0Zh.//mۖcgf)))r_Zduɣs R۷omVVV,$$DcKPrX\\,ywN4ð^zlժU,55Utn=b7ofÇ;V_6CȬS___g?#Ɔ39riZADDDuֱAq}>j5[z5j߾}.Njj*kР׻?~}:̙ӑ{h79tbSNRaX`?VZ+^ƍ%1sL0NE4ggg߿۷/|71b 4wwwTZ...Fzz:>}Dܹs7n͛7,yسgz?83ŗ>_4ޞ͚5h2@?ce.5 t/ //W-dyĀ8тhd7*_ҥKlʔ)Zj(xT*֭[7{nn]h .5ki30<\pA=O ЫH0vIК5k٫[[JO<?&&4?߲RCGxB ޽{٠A(U aT(rzj6fV~}_d7776|pvŽS V[/իWgYYYr8VKOO;mne+[lɶlŠ\Ozz:sss[*/^d4 4`|qgΜtnSpXRRZh!O}vtP(T~uI?XYY&M & 6{)tW8d'N;r¢R?St%ϟ<==%`vIe߾}z>ŋ%Xݿ3///nϧ {7ܹs?|Ar91tȑ#lĈFz+|G[Dtvv[oۣO> ޽ǏGfff^Nl߾[<cõkp9ܹsO>ɈGRR\䠨(((@aa!rss_cmm XZZBh4򂇇PzuԩS5jԐIEbcctR>|h߾=Ν ʝk`ڵػw/8s Ξ=g͛())ٳ'z=zjժO,* !qF;V6-++ ϟǕ+WDFF"22iii988 007FPPڴi7xjsұ;B!𗟟wKK. 4 ~m/ӧOdӧC~~~ [[[8::󃏏+B1Qz suuE˖-NC.s !R.* T(B!& EB jz11䟨P$BLPNNmG EB!egg݆ E2* !$Gn=QH! Q$|ΝCxx !+..!kX^o3j5\]]agg777x{{yh۶-NUqbccsww 1\ 3,!kf0i$;GZ\\vnnn3!`&L#GRH!ׯchժĄB$1o5oW^ #t鉁b,###EEEyK -k~GNN#)) IIIBTT3NNN]6UwwwZjprrSYWt?Z{{{ؔ}KWLyTKVec (((G#\x.\ٻ/3\\\P~}4k C˖-NM1nݺ%v&7aioK*UL 1[+6//QQQq.]/ƍ\zLEڵѴiS4mAݺui֞xU/})$,_˖-3Jԩ:u UVF3qyy/U^o{闳R!<}T5T(H~𑟟c۶m8|0Z)%x t ;vD6mDs3IDAT_֭|qssCΝѪU+n͛7 wQKP+V`ԩ3")C"55}V\icѻwo 8ov{Qv/ĬY$?FヒÇ[n4\pAp[Q$EKHH)Soɝ * :uTxS///&yxx 88qG 1T(;v`;;;?'O"5k"66kLZ)S`…/@Lc ժUCzzmkkk 2#JGLaаaCqqqrSi4if̘!DV n݊]rKSxx"H$6OPP?* ݻ%KPHxnիD3rIm3y*MH@@viKMܹsغu+Nl.jժ'OחK< T(Phb:vhٓYpukNt1*NZj8z(j֬%Q>}Zp{*ɫPh>3TVM4ʸѣoi&3!8|8t8dDf"5 15T( F>H4m۶i!ÁDYnmug;&= S D=z)wʼn'%w*ݻw#33STSbȐ!2"J~m[l JR1zuԨQC'صkj&5~Q۶moS6Diq%i8y*MXVd9 ?kBAhh(Ν;'+v+++Y%9x t:T(סMTTXj&L`T_}˗/e̜jo&l)Ef?#Tq!1cp̈(Mvv, 6+nj)Bфr_I&|(W_}Ƙ9gDG~~olBф<bsb °g[L}vQ۶m)bʨP4aIIIO?ŗ_~)y1%/<y7n猈$%%۫jt֍cFTQh${aŊSsMl۶MP[J~ֽ#عs'oٲ%<==9fDL}ژwJcǎXnT*d ͞=[poi\lذATI<cleQɁ JJJ Ĺs=6!رcٳxj֬9+4.]B֭EŸ}69eDL(3gHR$zyyT$'N'jĉH$իWj_n]*IQh rFCPV- 1u7oupp9s8gD(33;vw 1T(?kS6\Phb߿ .pΎKM"Qh"x&N6 { !?Gzzƍ Q˗#""BT ZM QTȝѣGGqq͛7aoo13B˝;wЬY3Qf͚^(_tt46l(zÇc۶m"zMeDbRT_H$DiӦz/9eClڴiD ,XOBlQpXv&L@ǎ9eDyq q !dƞ=o5 1gtY>QlתU w܁13Z'((nQL4nѢ!<ו+Wо}{nE"|xw#UP4Rk֬.%!… \~|֭[#))[#Ft}PhΟ?)SpG;>D <ZVt:Ij:u*ƍeR͛7/B%1FÇ0` īQ"\aK,Oqq C1.]=׸(diiibl觰ŋ r>*MCQQ,Y E;;F,GH{&Ry%%%9r$=jsfdd;'Ga۶mX`"##ǯZ*O4hЀ{lB*zӧOѽ{wܺu{lZ),,İað{n='///k֬APP}IDggg9r7ʢEeffgϞ~$$K)q!YFrn[naÆ ظq#%;#:-ZHvB* Eo߾|dHII,6! 11Dhhl9\zC ܹ`ǎqsttѶm[Ec;sW[NM6qyȚGq=Ys Vӧ~ ___Ph`6l'|mh۶-N> +++c>;hARKOOGhh(o?/_ CVpɚ!/B@ ˗r`ӦMprrMbXx1N\ŋ˝tx!n޼۷oΝ;y&;2};hobP4H1W\5Zj!$$CEժUeͅCÆ wѣGrZs… VejCDDD 22u޽+-R?rCHPNO?> 999rSovڡiӦ;-Baؽ{7֭[u#Chݺ5ϟnݺBxzRRbcc(DFF"22QQQUܞժUÖ-[гgOS!䕨Pȝ;w0qD?^T^hѢ4h>>>4^=ܸqׯ_+wj9::sڵ+5jf͚rōVEjj*RSS$$$ 66Gtt4Ljömb&\E"gIII?>~Wȝ(VVV[.ԩ5kf͚]6j֬5jF4 +((@ll,]VD!>>111HLLDQQܩׯoooxxxzpssܠhh`ooHGvv6"dee!++ e}iggg)))HNN6ڱRQ՘1c/^L bPIbb"VXUVՇ5Ujժnnnpss+;/=hBJJ Ґx(T-cc+++T GZ gg~677UPRRRV 4h+|MS!D/T(t]7nV;Eprr*;4 wqqy h4XYYQ1{iii)))HMMߛS 1oVVVƼy`cc#w: pm8p7oFxx(Nm(^lll`oo;;;prr%VB󅦵uֶlOi˽5 YYYet,ό1ddd //yyyBNN˞| ++ Eff&󑗗 ZfcҼys믴Q4/pƍ%􄗗T{.peHLL4@椲Z-Z->}*w*VQQb@x{{cѢE=z4 !gbII ֮]+Wݻ&d8;;1t#-- yyyJƘ [B999aΜ9>}z] B Ŵ4|DDD +B!ƍ.w:peR"c Æ TH!_:u*<<~H[dR n^z;B! AaРAhݺ5T*)"Qî]ЫW/Z[BHj4k zѼysS"hTb`ʔ)8vܩB1B֭uΝ;jժrDQ2B͛7g9saaaHNN;%B!-ZM6hӦ Znի˝!`҅rss$$''#>>)))HMM-ۍ%==O>-B!`mmu"00 /,,,NE2BQ_1ddd 77ZGAA>}#33}K@aaaٿ械 NN̲sgB1w兺u^zEZ $3›J+\]]NEEE)V}a꼼 Slidge Trần H. Trung All right reserved Slidge slidge/dev/confs/000077500000000000000000000000001477703150600142255ustar00rootroot00000000000000slidge/dev/confs/movim.env000066400000000000000000000004061477703150600160660ustar00rootroot00000000000000# Database configuration DB_DRIVER=pgsql DB_HOST=postgresql DB_PORT=5432 DB_DATABASE=movim DB_USERNAME=movim DB_PASSWORD=BAD # Daemon configuration DAEMON_URL=http://localhost:8888 DAEMON_PORT=8080 DAEMON_INTERFACE=0.0.0.0 DAEMON_DEBUG=true DAEMON_VERBOSE=true slidge/dev/confs/nginx.conf000066400000000000000000000031731477703150600162230ustar00rootroot00000000000000upstream movim-http { server localhost:9000; } upstream movim-ws { server localhost:8080; } fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m; fastcgi_cache_key "$scheme$request_method$host$request_uri"; server { listen 80; server_name localhost; index index.php index.html; root /var/www/html/public; location / { try_files $uri /index.php?$args; } location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass movim-http; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } location /ws/ { proxy_pass http://movim-ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; } location /picture { include fastcgi_params; add_header X-Cache $upstream_cache_status; fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie"; fastcgi_cache nginx_cache; fastcgi_cache_key $request_method$host$request_uri; fastcgi_cache_valid any 1h; } } server { listen 4444; server_name localhost; root /slidge-web; autoindex on; location / { try_files $uri =404; } } slidge/dev/confs/slidge-dev.ini000066400000000000000000000002341477703150600167500ustar00rootroot00000000000000legacy-module=superduper jid=slidge.localhost secret=secret server=localhost upload-service=upload.localhost admins=test@localhost debug=true dev-mode=true slidge/dev/confs/slidge-example.ini000066400000000000000000000033041477703150600176260ustar00rootroot00000000000000# More info at https://slidge.im/docs/slidge/main/admin/config # The slidge 'plugin' to use, ie, the name of the legacy service # legacy-module=slidge.plugins.discord # the JID of the gateway component, *NOT* your own JID # jid=discord.example.com # This is a regex to limit which JIDs can use the gateway # user-jid-validator=.*@example.com # the XMPP server hostname. Usually you connect via localhost, # (The jabber component protocol does not mention encryption) server=localhost # the secret passphrase used by slidge to connect to the XMPP server secret=secret # the slidge admins can list users, and remove them admins=test@localhost # turn on debug logs. they are real chatty. debug=true # For legacy plugins in which attachments are publicly downloadable URLs, let XMPP # clients directly download them from this URL. Note that this will probably leak # your client IP to the legacy network. use-attachment-original-urls=true # upload-service=upload.example.com # either use the no-upload-* options to serve files by HTTP from a static dir # (requires an HTTP server) # or set upload-service to use HTTP upload like XMPP clients do (this also requires # an HTTP server, but it's most likely already set up by your xmpp server). # no-upload-* is lighter on the resources but requires a little more energy setting up no-upload-path=/slidge-web no-upload-method=copy no-upload-url-prefix=http://localhost:4444 no-upload-file-read-others=true # this enables correction by retractions for networks that don't let you edit messages last-message-correction-retraction-workaround=true # this fixes the attachment file name suffixes using libmagic if available fix-filename-suffix-mime-type=true ;log-file=/tmp/slidge.log slidge/dev/hot-reload.sh000077500000000000000000000002611477703150600155110ustar00rootroot00000000000000#!/bin/sh watchmedo auto-restart \ --pattern *.py \ --directory /io/superduper \ --directory /io/slidge \ --recursive \ python -- -m slidge -c /etc/slidge/slidge.ini slidge/dev/prettify_tests.py000066400000000000000000000041611477703150600165610ustar00rootroot00000000000000""" Prettify XML in slixmpp tests, using utidylib """ import re from argparse import ArgumentParser from pathlib import Path from textwrap import indent import tidy def get_parser(): parser = ArgumentParser() parser.add_argument("INPUT", nargs="*", help="Python file or dir to process") return parser def main(): args = get_parser().parse_args() for path in args.INPUT: path = Path(path) if path.is_file(): process(path) else: for file in path.glob("**/*.py"): process(file) def process(path: Path): content = path.read_text() pretty = re.sub(PATTERN, sub, content) new = [] # remove useless language injection tags and place them at the right place for line in pretty.split("\n"): stripped = line.strip() if stripped in ("self.recv(", "self.send("): new.append(line.replace("# language=XML", "").rstrip() + " # language=XML") else: new.append(line) new = "\n".join(new) if new != content: print(f"Prettyfying {path}") path.write_text(new) def sub(match: re.Match): whole = match.group(0) lines = whole.lstrip(" \n").split("\n") first = lines[0][0] if first == "#" or first != '"': return whole line = lines[1 if len(lines) > 1 else 0].lstrip(" \n") if not line: return whole first_non_quote = line[0] if first_non_quote != "<": return whole return '"""\n' + indent(prettify(match.group(1)) + '"""', " " * 12) def prettify(xml: str): r = str( tidy.parseString( xml, input_xml=True, indent_attributes=True, indent="auto", indent_spaces=2, # we don't want to wrap anything because whitespace # in tag text is meaningful wrap=10_000, ) ) lines = r.split("\n") return "\n".join(line.rstrip() for line in lines) def strip_first_line(s: str): return "\n".join(s.split("\n")[1:]) PATTERN = re.compile(r'"""(.*?)"""', re.MULTILINE | re.DOTALL) if __name__ == "__main__": main() slidge/doap.xml000066400000000000000000000545021477703150600140120ustar00rootroot00000000000000 Slidge 2022-11-16 XMPP gateway framework in python en Python Linux Nicoco complete 2.11.0 0.1.0 complete 2.5rc3 0.1.0 partial 1.34.5 No creation, admin or invites (yet) complete 1.3.0 0.1.0 complete 1.19.0 0.1.0 Basic PEP support for contact "puppet" JIDs, to provide nicknames, avatars and vcards complete 1.5 0.1.0 To display http uploads inline partial For stickers from XMPP to the legacy network complete 2.4 0.1.0 To comply with XEP-0100, but it's not sufficient for most gateways (2FA for instance) complete 1.1 0.1.1 complete 1.1.4 0.1.0 When available, legacy contacts use XEP-0084 for their avatars. Slidge also supports synchronizing the slidge user's legacy avatar with their XMPP avatar. complete 2.1 0.1.0 planned partial 1.0 0.1.0 Friend request flow not yet implemented, and slidge always acts as a bouncer complete 1.1.1 0.1.0 To map legacy contacts to JID local parts when necessary planned Will implement if it makes sense for one slidge's plugins planned Will implement if it makes sense for one slidge's plugins complete 1.6.0 0.1.0 So that legacy contacts announce their XMPP caps equivalent when appropriate planned Will implement if it makes sense for one slidge's plugins planned Might be useful to improve the registration process planned For MUC#admin forms 1.0.1 0.1.0 planned User list and delete are available via adhoc commands, but will be improved to follow this XEP partial Only for room avatars partial 1.2.2 0.1.0 Basic support so that legacy contacts have avatars, vcards and nicknames complete 1.1 0.1.0 complete 1.4.0 0.1.0 For services that have no equivalent event, receipts are sent when the legacy service accepts the message. planned Will implement if useful for a plugin planned Will implement if useful for a plugin complete 2.0.1 0.1.0 planned Will implement if useful for a plugin complete 2.0 0.1.0 For group MAM partial 0.1.0 To embed QR codes in data forms during registration via adhoc command planned Will implement if useful for a plugin. MSN nudges (or wizzes) anyone? partial For stickers from XMPP to the legacy network partial 0.1.0 Slidge can send MUC invitations. In the future, it might support incoming-from-xmpp invitations. Compatible with carbons if the server supports it. Even for messages sent from official clients if the component is a privileged entity. complete complete 1.0 0.1.0 Required for XEP-0356. complete 1.2.1 0.1.0 complete 1.0.1 0.1.0 For MUCs partial 0.2.0 0.1.0 complete 1.0.2 0.1.0 complete 0.4 0.1.0 complete 0.3.0 0.1.0 complete 0.4 0.1.0 Used to sync legacy/XMPP rosters and to reflect messages sent from official legacy clients by users. complete 0.6.1 0.1.0 In MUCs. complete 1.1.0 0.1.0 Slidge recognize messages sent from XMPP with HTTP Upload and send them as attachments on the legacy network. For legacy to XMPP attachments, HTTP upload is used by default, but serving attachments from an arbitrary dir is also possible. planned Will implement if useful for a plugin. complete partial Legacy service to XMPP attachments have SIMS metadata when possible. Not supported from XMPP to legacy service partial 0.1.0 1.1.3 Plugins can automatically add MUC bookmarks for the user if the component has proper IQ privileges (XEP-0356) complete 0.1.0 1.1.0 complete complete 0.4.1 0.1.0 0.3.0 complete complete 0.1.1 1.6.0 For message replies. planned complete 0.2.0 0.1.0 partial Legacy service to XMPP attachments have SFS metadata when possible. Not supported from XMPP to legacy service complete 0.1.0 Relies on being a privileged entity (XEP-0356), to edit bookmark entries. complete 0.1.0 0.1.0 complete 0.1.0 0.1.0 Slidge supports listening to MDS events and triggering "legacy MDS" events, if added to the PEP MDS node, which is done automatically if XEP-0356 IQ privileges are set. Slidge also support updating MDS node in reaction to "legacy MDS" events. partial 0.1.0 Per client-type setting is not supported. slidge/docker-compose.yml000066400000000000000000000010351477703150600157730ustar00rootroot00000000000000services: superduper: build: context: . target: dev network_mode: service:prosody volumes: - ./slidge:/io/slidge - ./superduper:/io/superduper - ./dev/assets:/io/superduper/assets - ./persistent:/var/lib/slidge/slidge.localhost - ./dev/confs/slidge-dev.ini:/etc/slidge/slidge.ini depends_on: - prosody prosody: image: codeberg.org/slidge/prosody-slidge-dev:latest ports: - "127.0.0.1:5281:5281" # prosody's https://... - "127.0.0.1:5222:5222" # XMPP slidge/docs/000077500000000000000000000000001477703150600132675ustar00rootroot00000000000000slidge/docs/Makefile000066400000000000000000000012051477703150600147250ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= uv run sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) slidge/docs/source/000077500000000000000000000000001477703150600145675ustar00rootroot00000000000000slidge/docs/source/admin/000077500000000000000000000000001477703150600156575ustar00rootroot00000000000000slidge/docs/source/admin/attachments.rst000066400000000000000000000112061477703150600207240ustar00rootroot00000000000000=========== Attachments =========== In order to receive file attachments via slidge, you have two options: - **No upload**: serve static files from a folder via an HTTP server (eg nginx, prosody's `http_files `_, etc.) - **HTTP File Upload** (:xep:`0363`) No upload ========= At the minimum, you need to set up no-upload-path to a local directory, no-upload-url-prefix to an URL prefix pointing to files in that directory (see :ref:`Configuration` for details on how to set these options). Make sure that ``no-upload-path`` is writeable by slidge and readable by your HTTP server. You may use ``no-upload-file-read-others=true`` to do that easily, but you might want to restrict which users can read this directory. .. warning:: Slidge will not take care of removing old files, so you should set up a cronjob, a systemd timer, or something similar, to regularly delete files, eg. ``find . -mtime +7 -delete && find . -depth -type d -empty -delete`` to clean up files older than a week. For the following examples, in slidge's config, you would have ``no-upload-path=/var/lib/slidge/attachments``. Example 1: prosody's http_files ------------------------------- Here, ``no-upload-url-prefix`` would be ``https://example.org:5281/files/``, as per the `mod_http_files documentation `_. .. code-block:: lua modules_enabled = { -- Other modules "http_files"; } -- Must be the same value as slidge's no-upload-path http_files_dir = "/var/lib/slidge/attachments" Example 2: nginx ---------------- Here, ``no-upload-url-prefix`` would be ``http://example.org/slidge/``. .. code-block:: nginx server { listen 80; server_name example.org; root /var/www/html; # if you already have nginx serving files… # the section below is for slidge location /slidge { # Must be the same value as slidge's no-upload-path alias /var/lib/slidge/attachments/; } } See the `nginx docs `_ for more info. HTTP File Upload ================ This was slidge only option up to v0.1.0rc1, but is now *not* recommended. You need to manually set the JID of the upload service that slidge must use, eg ``upload-service=upload.example.org`` (see :ref:`Configuration`). Slidge will upload files to your upload component, just like you do from your normal account, whenever you share a file via :xep:`0363`. Pros: - does not require slidge to be on the same host as the XMPP server - might be easier to set up (works out-of-the-box-ish on prosody) - upload components generally handle quotas and cleaning of old files Cons: - more resource usage (using HTTP to copy or move files on a single host) Example 1: prosody's mod_http_file_share ---------------------------------------- In slidge's config: ``upload-service=upload.example.org`` .. code-block:: lua Component "upload.example.org" "http_file_share" -- max file size: 16 MiB http_file_share_size_limit = 16*1024*1024 -- max per day per slidge component: 100 MiB http_file_share_daily_quota = 100*1024*1024 -- 1 GiB total http_file_share_global_quota = 1024*1024*1024 -- starting from prosody > 0.12 you will need to add one of these two lines: -- server_user_role = "prosody:registered" -- http_file_share_access = { "superduper.example.org" } More info: `mod_http_file_share `_. Example 2: ejabberd mod_http_upload ----------------------------------- ejabberd's HTTP upload will not let the component directly request upload slots, so you need to use a pseudo user on the component domain, (eg, ``slidge@superduper.example.org``) with Slidge's ``upload-requester=slidge@superduper.example.org`` option. In slidge's config: ``upload-service=example.org`` The subdomain's FQDN (example.org) should be listed under the top level 'hosts'. .. code-block:: yaml hosts: - "example.org" acl: slidge_acl: server: - "superduper.example.org" listen: - port: 5443 module: ejabberd_http tls: true request_handlers: /upload: mod_http_upload modules: mod_http_upload: # Any path that ejabberd has read and write access to docroot: /ejabberd/upload put_url: "https://@HOST@:5443/upload" access: - allow: local - allow: slidge_acl To get more information about component configuration, see `ejabberd's docs `_. slidge/docs/source/admin/component.rst000066400000000000000000000031731477703150600204170ustar00rootroot00000000000000================== XMPP server config ================== You must choose a JID without a local part, (eg ``superduper.example.org``) for the gateway, and a "secret" (ie, a password) for slidge to authenticate to the XMPP server. Slidge usually connects to the XMPP server via ``localhost`` (see :ref:`Configuration`). Slidge uses different containers/processes for each gateway. Therefore administrators should setup these steps for each individual gateway. This is because each gateway makes use of an individual JID (such as ``telegram.example.org``, ``whatsapp.example.com``, etc). This documentation explains how to do that for `prosody `_ and `ejabberd `_. If you know how to set up slidge with other XMPP servers, please contribute to the docs. ;-) Prosody ------- Add a component block below the appropriate virtualhost in ``prosody.cfg.lua`` .. code-block:: lua Component "superduper.example.org" component_secret = "secret" -- replace this with a real secret! modules_enabled = {"privilege"} -- optional, additional config required to make it work For the last line, see :ref:`Privileges` for more info about how what it does and how to set it up entirely ejabberd -------- .. code-block:: yaml listen: - ip: 127.0.0.1 port: 5347 module: ejabberd_service hosts: superduper.example.org: password: secret The 'hosts' domain can be any given subdomain as long as the domain is pointing to the server's ip running ejabberd. Examples: telegram.example.org, whatsapp.example.com, etc. slidge/docs/source/admin/config/000077500000000000000000000000001477703150600171245ustar00rootroot00000000000000slidge/docs/source/admin/config/index.rst000066400000000000000000000020571477703150600207710ustar00rootroot00000000000000Configuration ============= .. include:: ../note.rst .. note:: For the debian unofficial package, just edit the ``/etc/slidge/conf.d/common.conf`` and ``/etc/slidge/*.conf`` files, and use :ref:`Debian packages (systemd)` to launch slidge. By default, slidge uses all config files found in ``/etc/slidge/conf.d/*``. You can change this using the ``SLIDGE_CONF_DIR`` env var, eg ``SLIDGE_CONF_DIR=/path/dir1:/path/dir2:/path/dir3``. It is recommended to use ``/etc/slidge/conf.d/`` to store configuration options common to all slidge components (eg, attachment handling, logging options, etc.), and to specify a plugin-specific file on startup, eg: .. code-block:: bash slidge -c /etc/slidge/superduper.conf .. warning:: Because of an ugly mess that will soon™ be fixed, it is impossible to use the config file to turn off boolean arguments that are true by default. As a workaround, use CLI args instead, e.g., ``--some-opt=false``. Common config ------------- .. argparse:: :module: slidge.main :func: get_parser :prog: slidge slidge/docs/source/admin/daemon.rst000066400000000000000000000117601477703150600176610ustar00rootroot00000000000000=================== Running as a daemon =================== While you can launch slidge interactively from the command-line, it is recommended to set up a way to launch slidge automatically on startup, i.e., as a *daemon*. This page describes how to achieve that with the :ref:`Debian packages (systemd)` or with :ref:`Containers` (using systemd and podman). Other options (SysV, docker, ...) are also possible but not documented here (as usual, contributions are welcome). .. note:: In this page we assume that you have fulfilled the basic :ref:`XMPP server config`. Debian packages (unofficial) ============================ These instructions are for the slidge bundle unofficial package hosted on `codeberg `_. They do not apply to the official debian slidge package, which is incompatible with this bundle. Edit and remove the ``.example`` extension for ``/etc/slidge/conf.d/common.conf`` and ``/etc/slidge/superduper.conf.example``. Enable and start the service with ``sudo systemctl enable --now slidge@superduper.service``. Containers ========== Container install ----------------- Make sure that podman is installed on your system, e.g. ``apt install podman`` (debian, ubuntu...). Container images are available on https://hub.docker.com/u/nicocool84 Let's launch the container: .. code-block:: bash podman run --network=host \ # so the xmpp server is available on localhost --name=slidge-superduper \ # human-friendly name for the container --detach \ # detach from tty docker.io/nicocool84/slidge-superduper:latest \ --secret=secret \ # secret used to connect, as per the XMPP server config --jid=telegram.example.org # JID of the gateway component, as per the XMPP server config Congrats, users of your XMPP server can now chat with their buddies on the "Super Duper Chat Network", yoohoo! Check the logs via ``podman logs slidge-superduper`` Data persistence ---------------- To keep data persistent between stop/starts (which will inevitably happen during updates), add volumes to your container. By default, all persistent data slidge needs is in ``/var/lib/slidge`` inside the container, so use ``--volume /where/you/want:/var/lib/slidge`` as a ``podman run`` argument. As a systemd unit ----------------- .. note:: The following instructions have been tested with debian bullseye. For other distros, they might need to be adapted. Create a system user named slidge (as root): .. code-block:: bash adduser --system slidge --home /var/lib/slidge Give permission for this user to use subuids and subgids (as root, required for podman): .. code-block:: bash usermod --add-subuids 200000-210000 --add-subgids 200000-210000 slidge .. warning:: Check that the 200000-210000 range does not overlap with any other user's range in ``/etc/subuid`` and ``/etc/subgid`` Enable lingering for this user so that its systemd user services start on startup (as root): .. code-block:: bash loginctl enable-linger $(id -u slidge) Create slidge conf files, to avoid passing everything as CLI arguments (as root): .. code-block:: bash mkdir -p /etc/slidge/conf.d/ echo "admins=admin@example.org" > /etc/slidge/conf.d/common.conf echo "jid=superduper.example.org" > /etc/slidge/conf.d/superduper.conf echo "secret=a_real_secret" >> /etc/slidge/conf.d/superduper.conf Temporarily login as the system user (as root): .. code-block:: bash su slidge --shell /bin/bash Enable the slidge user to create podman instances (as slidge user): .. code-block:: bash export XDG_RUNTIME_DIR=/run/user/$(id -u) Create the podman container (as the slidge user): .. code-block:: bash podman run --rm --detach \ --name superduper \ # friendly name of the container --volume /var/lib/slidge:/var/lib/slidge \ # Map directory for persistent data from host to container --volume /etc/slidge:/etc/slidge \ # Map config directory from host to container --log-driver journald \ # logs in journalctl --label "io.containers.autoupdate=image" \ # auto-update via podman dedicated mechanism --network=host \ # make localhost available docker.io/nicocool84/slidge-superduper:latest \ --config=/etc/slidge/superduper.conf # specific config file for this gateway. # Every gateway should have a separate config file located in this # directory and pointed to using podman. Create, launch and enable automatic launch of the container as a systemd service (as the slidge user): .. code-block:: bash mkdir -p ~/.config/systemd/user podman generate systemd --new --name superduper > $HOME/.config/systemd/user/superduper.service systemctl --user daemon-reload systemctl --user enable --now superduper Logs can be examined with ``journalctl CONTAINER_NAME=superduper`` slidge/docs/source/admin/examples/000077500000000000000000000000001477703150600174755ustar00rootroot00000000000000slidge/docs/source/admin/examples/ejabberd.yaml000066400000000000000000000023111477703150600221140ustar00rootroot00000000000000listen: - ip: 127.0.0.1 port: 5347 module: ejabberd_service # The next line is required if you're settings up multiple bridges global_routes: false hosts: - "superduper.example.org": password: secret - "other-walled-garden.example.org": password: some-other-secret # repeat for other slidge plugins… # HTTP upload service (XEP-0363) - port: 5443 module: ejabberd_http tls: true request_handlers: /upload: mod_http_upload acl: slidge_acl: server: - "superduper.example.org" - "other-walled-garden.example.org" # repeat for other slidge plugins you added in the listen section above access_rules: slidge_rule: - allow: slidge_acl modules: mod_http_upload: # A path that ejabberd has Read and Write access to docroot: /ejabberd/upload put_url: "https://@HOST@:5443/upload" access: - allow: local - allow: slidge_acl # for roster auto-fill and "carbons from legacy apps" # (broken in ejabberd when this was written, hopefully fixed since) mod_privilege: roster: both: slidge_rule message: outgoing: slidge_rule mod_roster: versioning: true slidge/docs/source/admin/examples/index.rst000066400000000000000000000015651477703150600213450ustar00rootroot00000000000000================================== Example XMPP server configurations ================================== .. note:: These examples are not meant to be complete, but rather show the relevant parts for slidge. Example 1: prosody ================== .. note:: Uncomment/comment the relevant lines if you'd rather use :ref:`HTTP File Upload` for :ref:`Attachments`. .. literalinclude:: prosody.cfg.lua :language: lua :linenos: Example 2: ejabberd/upload-service ================================== .. note:: See additional notes in ``Example 2: ejabberd mod_http_upload`` to get :ref:`Attachments` working. .. note:: This example does not cover the :ref:`No upload` option for attachments. For 'no upload' with ejabberd, you need an external HTTP server, eg :ref:`Example 2: nginx`. .. literalinclude:: ejabberd.yaml :language: yaml :linenos: slidge/docs/source/admin/examples/prosody.cfg.lua000066400000000000000000000023331477703150600224360ustar00rootroot00000000000000modules_enabled = { -- [...] -- "http_file_share"; -- for attachments with the "upload" option "http_files"; -- for attachments with the "no upload" option "privilege"; -- for roster sync and 'legacy carbons' } -- for attachments with the "no upload" option -- in slidge's config: no-upload-path=/var/lib/slidge/attachments http_files_dir = "/var/lib/slidge/attachments" local _privileges = { roster = "both"; message = "outgoing"; iq = { ["http://jabber.org/protocol/pubsub"] = "both"; ["http://jabber.org/protocol/pubsub#owner"] = "set"; }; } VirtualHost "example.org" -- for roster sync and 'legacy carbons' privileged_entities = { ["superduper.example.org"] =_privileges, ["other-walled-garden.example.org"] = _privileges, -- repeat for other slidge plugins… } Component "superduper.example.org" component_secret = "secret" modules_enabled = {"privilege"} Component "other-walled-garden.example.org" component_secret = "some-other-secret" modules_enabled = {"privilege"} -- repeat for other slidge plugins… -- -- for attachments with the "upload" option -- -- in slidge's config: upload-service=upload.example.org -- Component "upload.example.org" "http_file_share" slidge/docs/source/admin/index.rst000066400000000000000000000010051477703150600175140ustar00rootroot00000000000000========== For admins ========== .. include:: note.rst Slidge uses :xep:`0114` (Jabber Component Protocol) to communicate with an XMPP server. Every slidge plugin runs in an independent process and requires its own entries in the XMPP server config. To keep this guide generic, we'll talk about running the slidge plugin ``superduper`` that connects to the fictional legacy network "Super Duper Chat Network". .. toctree:: install config/index component attachments privilege daemon examples/index slidge/docs/source/admin/install.rst000066400000000000000000000024121477703150600200560ustar00rootroot00000000000000============ Installation ============ Dockerhub --------- Containers for arm64 and amd64 are available on `codeberg `_. The slidge-whatsapp arm64 container is kindly provided by `raver `_. See :ref:`Containers` for more details. debian ------ A debian package containing slidge and a bunch of legacy modules is available at ``_. See the README there for details and instructions. See :ref:`Debian packages` for information about how to launch slidge as a daemon via systemd. pipx ---- .. image:: https://badge.fury.io/py/slidge.svg :alt: PyPI package :target: https://pypi.org/project/slidge/ Tagged releases are uploaded to `pypi `_ and should be installable on any distro with `pipx`. Make sure that ``python3-gdbm`` is available on your system. You can check that this is the case by running ``python3 -c "import dbm.gnu"`` which will exit with return code 0 if it's available. .. code-block:: bash pipx install slidge slidge --legacy-module=your_importable_legacy_module If you're looking for the bleeding edge, download a package `here `_. slidge/docs/source/admin/note.rst000066400000000000000000000012561477703150600173620ustar00rootroot00000000000000.. note:: For legacy module-specific options, refer to their own docs: `matridge `_, `matteridge `_, `messlidger `_, `skidge `_, `sleamdge `_, `slidcord `_, `slidge-whatsapp `_, `slidgnal `_, `slidgram `_. slidge/docs/source/admin/privilege.rst000066400000000000000000000050351477703150600204020ustar00rootroot00000000000000========== Privileges ========== .. note:: Setting up slidge as a privileged entity (:xep:`0356`) is recommended for the user experience, but it can only work of the XMPP user account is on the same server as slidge. With privileges, slidge can: - automatically add/remove "puppet contacts" from the XMPP roster of slidge users - reflect on the XMPP side messages sent by users via a non-XMPP client, such official apps of the legacy service - synchronize other actions done via a non-XMPP client, such as read state, emoji reactions, retractions, etc. - automatically add XMPP bookmarks (:xep:`0402`) for MUCs (:xep:`0045`) Privileges with Prosody ----------------------- mod_privilege installation ~~~~~~~~~~~~~~~~~~~~~~~~~~ Starting with prosody 0.12, installing the `mod_privilege `_ community module is as easy as: .. code-block:: bash prosodyctl install --server=https://modules.prosody.im/rocks/ mod_privilege Privileges configuration ~~~~~~~~~~~~~~~~~~~~~~~~ In ``prosody.cfg.lua``, add ``mod_privilege`` to the ``modules_enabled`` list. Define the gateway component's privileges in the appropriate virtualhost block: .. code-block:: lua VirtualHost "example.org" privileged_entities = { ["superduper.example.org"] = { roster = "both"; message = "outgoing"; iq = { ["http://jabber.org/protocol/pubsub"] = "both"; ["http://jabber.org/protocol/pubsub#owner"] = "set"; }; } } Then either restart the prosody server, or reload config. You might need to use `mod_reload_component `_, and activate/deactivate hosts for all changes to be taken into account (restarting prosody is the easiest way to go). Privileges with ejabberd ------------------------ .. warning:: If you want to set up privileges, you need ejabberd with version 23.10 or newer because of these two issues: https://github.com/processone/ejabberd/issues/3990 and https://github.com/processone/ejabberd/issues/3942 .. code-block:: yaml acl: slidge_acl: server: # Make sure to include all of your slidge bridges that need privileges here: - "superduper.example.org" - "other-walled-garden.example.org" access_rules: slidge_rule: - allow: slidge_acl modules: mod_privilege: roster: both: slidge_rule message: outgoing: slidge_rule mod_roster: versioning: true slidge/docs/source/codeberg.svg000066400000000000000000000016261477703150600170670ustar00rootroot00000000000000 slidge/docs/source/conf.py000066400000000000000000000073261477703150600160760ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html from enum import Enum, IntEnum from pathlib import Path from slixmpp import ComponentXMPP # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('..')) # sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = "Slidge" copyright = "2025, the slidge contributors" author = "Nicolas Cedilnik" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autosectionlabel", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", # "sphinx.ext.viewcode", # crashes build unfortunately "sphinx.ext.autodoc.typehints", "sphinx.ext.autosectionlabel", "sphinxarg.ext", "autoapi.extension", ] autodoc_typehints = "description" # Incldude __init__ docstrings # autoclass_content = "class" # autoapi_python_class_content = "class" autoapi_type = "python" autoapi_dirs = ["../../slidge", "../../superduper"] autoapi_add_toctree_entry = False autoapi_keep_files = False autoapi_root = "dev/api" autoapi_ignore = ["*xep_*", "slidge/core/*"] autoapi_options = [ "members", "show-module-summary", "inherited-members", "imported-members", # these on-by-default parameters are disabled # "undoc-members", # "private-members", # "show-inheritance", # "special-members", ] def skip_stuff(app, what, name, obj, skip, options): # Hide some stuff from the docs if name.endswith(".Register"): skip = True elif name.endswith("user_store"): skip = True elif any(name.endswith("Gateway." + x) for x in dir(ComponentXMPP)): skip = True elif any( name.endswith("MucType." + x) or name.endswith("CommandAccess." + x) or name.endswith("RegistrationType." + x) for x in dir(int) + dir(Enum) + dir(IntEnum) + ["name", "value"] ): skip = True return skip def setup(sphinx): sphinx.connect("autoapi-skip-member", skip_stuff) # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "slixmpp": ("https://slixmpp.readthedocs.io/en/latest/", None), } extlinks = {"xep": ("https://xmpp.org/extensions/xep-%s.html", "XEP-%s")} html_theme = "furo" html_theme_options = { "source_edit_link": "https://codeberg.org/slidge/slidge/_edit/main/docs/source/{filename}", "source_view_link": "https://codeberg.org/slidge/slidge/src/branch/main/docs/source/{filename}", "footer_icons": [ { "name": "Codeberg", "url": "https://codeberg.org/slidge/slidge", "html": Path("codeberg.svg").read_text(), }, ], } slidge/docs/source/dev/000077500000000000000000000000001477703150600153455ustar00rootroot00000000000000slidge/docs/source/dev/contributing.rst000066400000000000000000000040401477703150600206040ustar00rootroot00000000000000Contributing ============ Development setup ----------------- With containers --------------- The easiest way to develop using slidge is by using docker-compose. Clone the repo, run `docker-compose up` and you should have: - an XMPP server (prosody) exposed on port 5222 with a registered user (password: password) - an XMPP component, the "super duper" gateway, a fake component that can be used to try stuff directly in an XMPP client, with code hot-reload. - the in-browser Movim client running on http://localhost:8888 NB: it's possible to select which containers you want to run, you don't have to launch everything. You can login with the JID ``test@localhost`` and ``password`` as the password. Without containers ------------------ To install outside of a container, use `poetry `_. If you don't like containers, set up a virtual environment with ``poetry install`` and refer to :ref:`XMPP server config`. Using another XMPP client ------------------------- `Gajim `_ is also a good choice to test stuff during development, since it implement a lot of XEPs, especially when it comes to components. You can launch it with ``-p slidge -c ~/.local/share/slidge-test -v`` to use a clean profile and not mess up your usual gajim config, db, cache, etc. Unlike gajim, some clients will not accept self signed certificates, a possible workaround using debian and docker is .. code-block:: docker cp slidge_prosody_1:/etc/prosody/certs/localhost.crt \ /tmp/localhost.crt sudo /tmp/localhost.crt /usr/local/share/ca-certificates sudo update-ca-certificates Guidelines ---------- Tests should be written with the `pytest `_ framework. For complex tests involving mocking data through the XMPP stream, the :class:`slidge.util.test.SlidgeTest` class can come in handy. The code should pass `black `_, `mypy `_ and `ruff `_ with the settings defined in ``pyproject.toml``. slidge/docs/source/dev/design.rst000066400000000000000000000043701477703150600173540ustar00rootroot00000000000000Slidge Design ============= The main slidge entrypoint will automatically detect which classes have been subclassed and use them automagically. Just subclass away, and launch your legacy module with ``slidge --legacy-module=your.importable.legacy_module``. At the very minimum, you will need to subclass :class:`~slidge.BaseGateway` and :class:`~slidge.BaseSession` for a legacy module to be functional. JID local parts to legacy IDs ----------------------------- You probably also want to subclass :class:`~slidge.contact.LegacyRoster` and :class:`~slidge.group.LegacyBookmarks` to define how :term:`JID local parts` map to legacy user or contact IDs. Defining which local parts map to proper valid user legacy IDs is crucial to discriminate between JIDs that map to a :class:`~slidge.contact.LegacyContact` and those that map to a :class:`~slidge.group.LegacyMUC`. You should override :meth:`~slidge.contact.LegacyRoster.jid_username_to_legacy_id`, :meth:`~slidge.contact.LegacyRoster.legacy_id_to_jid_username`, :meth:`~slidge.group.LegacyBookmarks.jid_username_to_legacy_id`, and :meth:`~slidge.group.LegacyBookmarks.legacy_id_to_jid_username` in your custom :class:`~slidge.contact.LegacyRoster` and :class:`~slidge.group.LegacyBookmarks` classes, and raise an appropriate :class:`~slixmpp.exceptions.XMPPError` when called with an invalid argument. Fetching info from the legacy service ------------------------------------- By subclassing :class:`~slidge.contact.LegacyContact` and :class:`~slidge.group.LegacyMUC`, you will be able to define how :term:`XMPP Entities` update information about themselves, such as their user-facing name and the :term:`Avatar` that represents them. This is done by overriding :meth:`slidge.contact.LegacyContact.update_info` and :meth:`slidge.group.LegacyMUC.update_info`, in which you should raise an :class:`~slixmpp.exceptions.XMPPError` in case their :attr:`~slidge.contact.LegacyContact.legacy_id` attribute is not valid. Pre-filling contacts and groups ------------------------------- The coroutines :meth:`slidge.contact.LegacyRoster.fill()` and :meth:`slidge.group.LegacyBookmarks.fill()` will be awaited just after :meth:`~slidge.BaseSession.login()` and should be used to pre-fill known "friends" and groups. slidge/docs/source/dev/howto.rst000066400000000000000000000000351477703150600172350ustar00rootroot00000000000000How to…? ======== Soon™ slidge/docs/source/dev/index.rst000066400000000000000000000002251477703150600172050ustar00rootroot00000000000000======== For devs ======== .. toctree:: :maxdepth: 2 contributing design tutorial howto api/slidge/index api/superduper/index slidge/docs/source/dev/tutorial.rst000066400000000000000000000114261477703150600177460ustar00rootroot00000000000000Tutorial: minimal legacy module from scratch ============================================ Wanna write a new "legacy chat network" slidge plugin? You've come to the right place. Minimal example --------------- Let's say we want to create a gateway to the famous *super duper chat network*. Put this in a file called ``superduper.py``: .. code-block:: python import super_duper.api # great python lib! from super_duper.client import SuperDuperClient from slidge import * class Gateway(BaseGateway): COMPONENT_NAME = "Gateway to the super duper chat network" class Session(BaseSession): def __init__(self, user: GatewayUser): super().__init__(user) self.legacy = SuperDuperClient( login=self.user.registration_form["username"], password=self.user.registration_form["password"], ) self.legacy.add_event_handler( callback=self.incoming_legacy_message, event=super_duper.api.IncomingMessageEvent ) async def login(): await self.legacy.login() async def incoming_legacy_message(self, msg: super_duper.api.Message): contact = await self.contacts.by_legacy_id(msg.sender) contact.send_text(msg.text) async def on_text(self, chat: Recipient, text: str, *kwargs): self.legacy.send_message(text=text, destination=chat.legacy_id) This can now be launched using ``slidge --legacy-network=superduper --server=...`` The gateway component ********************* Let's dissect this a bit: .. code-block:: python class Gateway(BaseGateway): COMPONENT_NAME = "Gateway to the super duper chat network" By subclassing :class:`slidge.BaseGateway` we can customize our gateway component in various ways. Here we just changed its name (something we **have** to do), but we could also change the registration form fields by overriding :py:attr:`slidge.BaseGateway.REGISTRATION_FIELDS`, among other things. The legacy session ****************** Setup ~~~~~ .. code-block:: python class Session(BaseSession): def __init__(self, user: GatewayUser): super().__init__(user) self.legacy = SuperDuperClient( login=self.user.registration_form["username"], password=self.user.registration_form["password"], ) self.legacy.add_event_handler( callback=self.incoming_legacy_message, event=super_duper.api.IncomingMessageEvent ) The session represents the gateway user's session on the legacy network. To add custom attributes to it, override the ``__init__`` without changing its signature and do not forget to call the base class ``__init__``. The :py:attr:`slidge.Session.user` attribute is a :class:`slidge.GatewayUser` instance and can be used to access the fields that the user filled when subscribing to the gateway, via :py:attr:`slidge.GatewayUser.registration_form` dict. Here, we added a ``legacy`` attribute to the session instance, because our fake superduper lib is coded this way. YMMV depending on the library you use. Good python libs provide an event handler mechanism similar to what you see here. Login ~~~~~ .. code-block:: python async def login(self): await self.legacy.login() When the gateway user is logged, this method is called on its :py:attr:`slidge.Session.user` instance. With the superduper library, starting to receive incoming messages is very convenient, as you can see. From legacy to XMPP ~~~~~~~~~~~~~~~~~~~ .. code-block:: python async def incoming_legacy_message(self, msg: super_duper.api.Message): contact = await self.contacts.by_legacy_id(msg.sender) contact.send_text(msg.body, legacy_msg_id=msg.id) We are really lucky, superduper user IDs can directly be mapped to the user part of a JID. We can just use our session's virtual legacy roster to retrieve a :class:`slidge.LegacyContact` instance. Just by calling :meth:`slidge.LegacyContact.send_text`, we effectively transported the message's text to the gateway user. Ain't that great? From XMPP to legacy ~~~~~~~~~~~~~~~~~~~ .. code-block:: python async def on_text(self, chat: Recipient, text: str, **kwargs): self.legacy.send_message(text=text, destination=chat.legacy_id) When our user sends a message to ``something@superduper.example.org``, this method is automagically called, allowing us to transmit the message to the legacy network. Going further ------------- Until we actually write this section, you can refer to :py:mod:`slidge` for the API reference, to :py:mod:`superduper` for a mock legacy module that serves as a minimal working example, or have a look at the existing `legacy modules `_ slidge/docs/source/glossary.rst000066400000000000000000000043461477703150600171730ustar00rootroot00000000000000Glossary ======== .. glossary:: :sorted: User Someone using slidge, ie, someone who has an XMPP account and registered to a slidge-based XMPP component. Legacy Network The messaging network slidge (and the :term:`User`) communicates with. Legacy Module An XMPP gateway based on slidge. Legacy Contact Someone using the legacy network to communicate with the :term:`User`. Official Client The reference client(s) for a legacy network. Examples: telegram-android and telegram-desktop for the telegram network. Carbons In the XMPP world, carbons (:xep:`0280`) are messages sent by the XMPP server to keep outgoing chat history in sync between different clients connected to the same XMPP account (eg, a desktop and mobile app). In slidge however, this refers to actions of the :term:`User` done from an :term:`Official client`. XMPP Entity Someone or something that has a JID, such as an XMPP user account (eg, ``someone@example.org``), an XMPP component (eg, ``slidge.example.org``), an XMPP Multi-User Chat (MUC in short, eg ``cool-group@groups.example.org``), an XMPP server (eg, ``example.org``), … basically anything that has a JID. JID Local Part The "username" part of a JID, eg ``username`` in ``username@example.org``. Avatar A picture representing an :term:`XMPP Entity`, such as a profile picture for a :class:`slidge.LegacyContact` Ad-hoc Command A way to interact with the gateway component (or any xmpp entities) via a series of forms, to trigger actions or request information. See :xep:`0050` for more details. Chatbot Command A way to interact with the gateway component via chat messages, a bit like a shell. Command Either an :term:`Ad-hoc Command` or a :term:`Chatbot Command`. Slidge provides the same commands via both interfaces, so they can be used on any client. Roster This is how the "contact list" is called in XMPP. "Legacy" may sound weird since XMPP is pretty old now, but slidge follows the convention of :xep:`0100`. slidge/docs/source/index.rst000066400000000000000000000015311477703150600164300ustar00rootroot00000000000000Slidge ====== Slidge is a general purpose XMPP gateway framework in python. Blog posts about slidge for a general overview of what slidge does: - `0.2.0beta0 `_ - `0.1.0 `_ - `0.1.0rc1 `_ - `0.1.0beta2 `_ - `0.1.0beta0 `_ Homepage: `codeberg `_ Chat room: `slidge@conference.nicoco.fr `_ Issue tracker: https://codeberg.org/slidge/slidge/issues .. toctree:: admin/index user/index dev/index glossary Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` slidge/docs/source/user/000077500000000000000000000000001477703150600155455ustar00rootroot00000000000000slidge/docs/source/user/commands.rst000066400000000000000000000015551477703150600201060ustar00rootroot00000000000000Commands ======== .. include:: note.rst Slidge legacy modules may provide additional :term:`commands `. You can discover them either by sending "help" to the slidge component's JID or via :term:`ad-hoc commands ` (:xep:`0050`), if your XMPP client supports them. These commands should be present: Contacts -------- List your :term:`legacy contacts `. Groups ------ List your legacy groups. Find ---- Search for contacts to chat with. Sync :term:`Roster` ------------------- Make sure the :term:`legacy contacts ` in your roster matches your contacts on the :term:`Legacy Network` side. This is usually not needed, but slidge is beta software and sometimes weird stuff happen. Unregister ---------- Unregister to the gateway, ie, stop using slidge, and remove your credentials from the host running slidge. slidge/docs/source/user/contacts.rst000066400000000000000000000014551477703150600201220ustar00rootroot00000000000000Finding legacy contacts ======================= After registration, slidge should add your :term:`legacy contacts ` to your :term:`Roster`. If you want to message someone that was not automagically added by slidge, you can sometimes guess their JIDs when the :term:`JID Local Part` is human-readable, such as a phone number or something like ``name.surname``. Often times though, the :term:`JID Local Part` of :term:`legacy contacts `, will be something like a number or a random word. Since this usually cannot be guessed, you have to use the "find" :term:`Command` (or Jabber Search (:xep:`0055`) if your client support it). If you try to subscribe to a :term:`Legacy Contact`'s presence, this will generally trigger a "friend/contact request" on the :term:`Legacy network`. slidge/docs/source/user/foxyproxy.png000066400000000000000000003476361477703150600203650ustar00rootroot00000000000000PNG  IHDRlRY pHYs+ IDATx^w|OfӦ{tB edȖ=DF"SPBUޣt7K:hzޯx].'qIQ B!RMB~B!BH("B!$B!Bj(B!BJ!B!("B!$B!Bj(B!BJ!B!("B!ikd#[62B!Bw1 w.FX}&~ xN_2t7@Z!B! Eb7]]8+}LժȡӗB!ڸ]vrlڣۖnj4x2?D!B!s$Q!B!B(=&$!B!es$QB!B,QE!B!5@I!B!%QB!RDB!BH PE!B!5Iv9yʔӧB!3;vعy9 BB!B"F[hdN{pIII;w ,qy?hdD"X,U@(BZUrtn#*GI}Yw _D!B!3Lج;̽]`gr bX,2(n2*Q*S?NY?B?BܮS-Z?t/g gY !B!YzfҌ~[09X)ܗ/+++;27:J Yo_oĂzaaja|r4HVp'u+,hE?O{_UIȋ<c~ j2_!BgD)hRK3('XsCܜY PbʞXze:cenQJXh>r4y W;aY>O_h3@r5h-#% ,>7MݩnA׶kqbB!B^_EA$ BP N=/{API`e*c̾7|O3srwoOGxAFKg*<-Ш*d?<æ:&㞍G(TΣ$ѿ$B?Zm׫ E]2E"H$z)هwo|KС)E3Lrf\9Q͚6mҬYNnѻjWP$OY.͎-I`.dǖ$\d㿆5[PZ+I,}d> ԸTM (Ϊ:Y !B̓$%%:|8G,7I._+Q iws/ۊY?7-Q5gj8RbOLF) mEF)cZfm(w8EkG/yH[s5X"m׶Kw:f媔뚬]`gwCۙW)~xrg+6nMdxsq陇R>:U B[(9>[ׅ[^;lQ\ٸ;Dt6]įWjJux*.7Qdl˳[";g^mIywUvQNzRj l]ӫYtGkW][RZb†shsea|~dYʓszM6?y7Ob|F 1|},:M?CGg'WJN]x)زe;ܳtז4'!B*Uצj|b[[[BqҰ ns/ۊH %(eUFѯ|\:]Vp6QD. -*>x(-,ծߤnŽBBU^ e&m\`C*l9Π/Lǂ:n/UxuTqgcyk+JTrگnmƘUvA--fl}-Ų><<(Ψ)* n=[иOƯb(]psK0s3FJ45o::NN Wu,M6ѩʸYG?oCsQyGS??իG$BT IT'Od~P(߿\.8ﮩ3iB'vkX4CAh/z7}wGkhV&PȚ/xu[P9^MǏ6u#?喋NHOc%w?E,chLݥܲfCxפ)I Зb3ldT55W 2(<^P`c2z}H;ei]~ M\Jȃ[V9g~),l4fSRnĭ\cAQcj8zJF̿tce2\eA1E6'kV {^ԊNjJ=n]PKV-f Z*?nEȅUt _9R;}QVR͓X9̫7R_^p,J~^12jΠ"Gɸg]$2 mBPm>ݖfePb{w,k_ e{{vf2F!bV ITDDBzxxxxxDEE "֠]ާ1L;[Sua#>$4.hswm4ܸY=:nPm[ϣyV$WTt fLuܲ[=`7 3-㖝<~HNhy5uIM\Z N ɻ1񷱼8r&u {̫:nښpSn8Wk2cl[O!V&ԱCQnA;0SJlfA*әX&/U0ܽRs粝Fg3(ap}#4عdp8z̫>sȄMO[&g{us*VYt9vݞ]ro?N$+;VUsͥ{?<tBSԛy̓N {+T;tRC<$1*@( Ap8ѥiyhYΙ3Y"Bq_7fs3.  KK;kğ^]֪.dHv,+s;h'By9g~:4PS$VH_"WDھʽ);XhB.mmD2E2G +n\em}:7?@({h 2!BZhᑔȟ 00͎@F՝@Q\b;nicfP3y1BPL MScI@g# Lsbs&4KdP9.vt V}2o,scUY:ھƋTGKVd̻1F>cx7ޖKiUqZ&QԚ.&y&87mf= 3m.#\[Jn1*7hb*LrSd#KF}ƾ<û:wsjԷ/.-_:e'BN}TIW Ғ7@U!Cx7Bm!΍ myOG"هbx5x}\ "[GsނFjWtj%IWYe&DnAR ;q ^+HdNnYӡͭWu»]g9u8;I'ޞM{= M=MC˹+ nn,'I̧@ o7/s;hKCۛ,c4:㖪.˚"ڸxc6Uؕ;$OUDA NN.%UcObyVinXd֩Ќ?|`Ye'Q6a1'B1TF[Y)}Ggu?Mz}$By>j;'s즫/7)pb 70r[@ *TH{-FC^&~ ,+@[k]P s׾8^~.?ժj5.2S-I3\!ڛ +f; ۣKl:~]>fW7STBbQ~17Jz~u۰aȰ, @"dْ,n>QYRayɊQG>?vUE2N6~Cĭ 1mngt0^a'&I\N] @} ypkX|` ¹yUwEvAn*W!v3hqJ0ί0][:d#kQg;.nΠ1[&nn 5 ̤}IP7rSzQwiSFMD:ykqَy֠f?x֭p+ߛˠ\[B!9͖(Hެ"Ƴqgwwi=tJ[g<l>QZDE>}zΝܪG3\c ƅ˵4 Ej-+K~ ))r{5Cd_1гHp}ũE$]J=f_^t(vE^Y!;lTH>}|[quM"Ngس;a7P \|z*UM}a:)8ױ4ٹ頊8UUɀ(]U\Nɼ/wF:-bz3O\ߑwUeXҵK᭽[S]\ۡJiPx}&w+*Ts]{g n=VOXkmF^g>oȗ^Q$.'֯c^]߾N3nW$'\{nmT}aF.u c6o޼pf1ʸ[R >/}r * us6cg1'4l}>H!&Un؆~x IDAT. ^wΣxaʙ:??}.o^SBvgEZQ7;d`FQ,_=Bh6nxod騝?%!t vh.τB!+[X"jK h<<&vumPRu#HDF  a1SnPl+UIUIգNϱ>\=M3ib ԐʹCmٺl?:=[$oK!Cg[<.<݈_SnB!6D4 鄻 33]cAX%HŌ=lEۊohgL@oB SDunesw=XFftD[kUki%jߕqR~|_ް^B!𥳐(YŃSN pkΛ7iĈ_QQf &*v͸ԵTZe&,n;+ FB!L=$ @7$&5ŋ#{nn_oZb&;zHINq7p>Ԟ-B!Byj>QVJu{L3y֝[nC Hsss"(8XTz{yhтn\J5,c42u{':0y°5 !B!,(-J}"vqiz${,ð,k+a!$$D:_Pj4[5 l$<9QxB!BHyI8Cmd?ȑ\eYʒ(J2t^(R_]Znn IE!ByxIԳe&pħ酞!0.93ޣFcΤ 4*K$H$Hmԭ0M"B!=$ dΒ_1\Z%?`ppQɉElEIeJ%a;;ydARcz& "[L#Y!B!{[ 7Ct5ux710LMdDj=3] /ͺd?# p,!B!<3{LTt{+pՃ2F-RiLa*tJ9""B!gyAxHLU&[$L(3!s8=E^.B׺-B!B_H&jkKQB!Bm.;96mcDu<"B!9c!B!gωDLB!9c(B!B!("B!$B!Bj(B!BJ!B!("B!$B!Bj(B!BJ!B!("B!!OQB!$B!VITjRlRn/B!/UXbHRJeFa$ VJeDI^!B\(/V*2BN:5f̘X,J===sNeU yRD)rr|mQB!(rrQBኋGѹs 6!C 2e݃~DjHS/>B!oI.~G 'm߾}LL o߾?ovƌN>ݴi興ɿc@NtUP-QBWI?5b*l1+ k' *s?Vtk)6gzedݺu2L&͝;2`HU{6ӟ/HFW~8:J*RkV'#E]pkKj7z~'WZG!FCuGu\\[ܚ #l.77"ZO/h@X-PEnM-rƀ=pkJ~6XaB[o5lؐ[޷o-KDž Z+ PrI@3C=M(,k{rB n ?OC-QBDhr6!Ðr.eRaWu®H?QV: ^_s(I*l /vEH;e8<ZcS.SYc y5866:w\U~s`DO;oX U:< ;Z9F"WS5cpz2N/XQ[VZWlk_eL=i+8; o+S^eZS㱡~v_p8ԧ!ο05: ȹqm!Ơf4~e<Cf4~mA=\WVu^gqg7xAKPM7mB1UUzxS`2 I`(!JG*DOplb~E9uEuP\apDMhn/%uyrA dsM^E.]ïdƎcYsѩGRITF%{PSg~LB#Sӳ7< nEN~:{?&hg.\/QOLmyd|b*/,cVvKV>`Yϰ֬`Шwۼ_&- ,FY 5Gx7Og9K[tyUly /[3uGk$9zF`4}-<|{JB(94<\g:O0dC;a;A`/3^Ô6>s=N^Tz*@ԃ ;T*T&zH؁1 sBp 8V"h}6_Bn.AS+m+]:O&Qle_~:W7g‘lRϾ>\жWU<׃Wٰe/~.g }1՛9':wha+{7$yNK6糯ּ6䋕:iҤsJK׈Qúax>]ί|=Xv+Cz "VQuNU;n>0åw?s%lT0&תX(.#l (,څ2N##4B vH 5`XdDÿi\xyJ8).u /!?Dh`(.Z mw\V>Ά2 ׆e_7eBU y"qk\^ڵ1ƍ/P. ~9~B" @i з4:B"&/Lq BJ'[ѫ>n,VǼիVH$V&zSy9TG[SzTlhj< !Lu\W,5O<>T*ک홑Ŗҧ{#sS(ZPkgukO*"8}ȈD5>/Oe?./<?_% SZJ%eu:ׇDFҪFi?+a@P١7z ?d<)P(RKnNw)ϻNw}5ʩ$>ZY VEKftT='a] jEN:Qp ) tDSd}r"vA%SZ fuG8m`-*nJamV/eTCc|[Kp}#b -T>Dv(;Teʄ:2`_Tq/3]SNݺu=O2M6hM=1bUv=<)(sBī0j/FiYjUZٛ?̊QgfqxA(B+FZ4˾?cL}ĝMfk|b;B|b7?l{?^n**rQrKW9koE5p<._zѡc=w_Nc xuDimA&uJJ2fuKVG\Bʎ _3 vӞGefu`ǶYg"p70 3z҇Aϟ6v~^~0{c~n!O奉ӧЧ3/۞y/vxeh\,YD|c]L >]Р~Ț'>^ ,_w}Цewpx-z`<5W]>=w_.(,f,q߭t^P(h$bִW~>Axo 2sA5$嶽2nD";b<=`;%$z{~Oޝ˞Tao=npwiתi0 mѝem۴l<1rh/=òb{Nd Ul=})' ^[F4nXJY3od)r=\yaiqWΰ^K4jpޛ'\ٴ@"/i PT\/]h|U[GI#'k7~^?Afc#9* ÂWn *?j*:u8r#5iĠQ|keu%իatO '@gӼ#˫$VSm޻k߉Mw7Eޱ;}ޔ-(㹓XX3?\n'+K:xEݸϤ &~s?7S]]zwo?n@ׇ?z Ým3FW? Z1&z"JN˭2FWvG)[vAiLn,q!NuBaoghrųq@n~bvP[wfVeWfδzʀ[ <`2PšBUroo!l=>_,dⷢH؉ |2P+(u 'uLѲgCTg0iҤ9s~><((hРAґ#G::Z$O)yTzxSPn6(`wӷDDO`&K +0xB#d849H=k@oJ`GFSʟ?!W x0YşH%yuGZ~^|7]IJl>=:,gޠRk,_+Bvjkrgr6@%xy,^1w8Ki>=:| />wM |8k;: 2rfM{@N-On09q7/0.JLN`z,YۣlE5gڷg:hENn*? ZnزCZhh_Qjz_51-PkJo J%wtws2nʯ?D%{mJyZYKJ;͉/߹/+ Xu d3?\{1#^Ѣ%?mvpĐ^sy]Ѿ\cOyGogeӵSv޹ɑ8pɏ4r6-}v[V6߂鯎wԥs[wr6-/xژ_:vտsE_ذm̘:ܥ[{r߅ekk3e˜Ds'W?tzHn_|vWQ)i;[3}ޣyƍx5n 0vc|0sQ IDAT|Iz̤+%x[w~hּYs-4}Ul^~_8;~M|1{/?nSGl\ٶe\s]3|^,Cfluԥ Yrj渷>}.[q7wWQSTۖX_X0nUc]Vvi \>7yisgYZOfyĪw J'_ޥaeUE^]Z=m۲ FG_pŃ.oX`{?^^L_윟Սe/]7>HB_0d:Il e3o-tΣҎXL&ߡ\ %pkb5~)yDT=̣ȹGhaU5"2_,{h)nYnM8ճ!~ RS?E(GʖCLaH? nȽB`""n \A$G+hge7ѫVoM֬Y3Ö-[֭k1׷s"E><敭'd/חỤ bLJ0|tS px0Qwb,][Ƚ달S*H=k-:L0|k7qolu_251uWgyoQV6~@ COe/k8*b0v w?[0׹3۾oyypO4/\N5ە \uuqЭse6]yUT$0GN\طyKM(gǦj4%i/=]ͫXN /]o`ݶõզVUb;UAs6=uⰋx.=ڷsn.Yv||aҙBDD}8z :wLHJ6nKߠE n#GƱ}vдh56"2^zNF~ m||ŽVO4R>?uh a[&p!p3 ;Jb[%<;tsvtaY剀$E=4sų(J#h;,ƐQ8[b\hF,Xw_&;i7GnQ^F7d~v!πJȫ#68U,N1\wBeU34 (A)b Qgwãh9X9Z=^m5+g^윑`Æ ܂QK.Ve/@3h5us|U z|&N%Ok p\EDp\yQ \esI2Q\3}y/WQsPw18,s{G Ɔzѝ;hj͛5n#Ю2;y7sHˀ~}&W6fMW.&ءM/PXdlY@ԃ@nml9NӽG&ZբmU 5x쭳c9}z|Qƍ,E^vIXvn-̍ PШ( iռ@ /dafj7"uX*ws~Zz ͮ|еxZ=1$BNU=]eedBa@PشØЬ ODb>%vh"ڪU׼gUصwhRC]chC )(--KN͈MxQ]]@Hx _ک.MOWK4'A@P:{=,nZWE\^bRZhDlRJzmJE{jfbhlܾ056`m47f"(fN-ilcDP,̍P> F泵) âgOY4`_ JDT߳Un`slSRRܷ}h+bqs߇hHK[>*)*F<$O}i\!''kb [CЇ.D]C7sH|RS6;wpNDuצ&+˘~2#3CsgX{RDD3fzF`(۳e"ZV/P@*AqWx ãr/eC yW|qWR9cXG x/+UކČ62uS`_vDȩm%[êsE=DĻPD&YanR=.DZD/YnFiq5ZTt?k ܂7Kdn'C@=/wQ0t@aI=\<+g2pk7md|&gV,v=w2 ~ⅷd6`ccsŖ-/8]*Ժ_YA(>Hfk/,߅xLB_oFME.yCm K&Jx&I;虜%22uj}im- a$;;t'>15<gʐ@MUju̬\6 Z_8%536.繟(i]_+X+iCD:8fg#q陚jC癣>xNƷKMS2LZ"z)j =#{Q_JL 5-SrEw)9=rbƽHy]Mu$A!1 ]Mw5tZˠ)SUQVQ|`&Y*OCz{yүw'ݔa3z%w Y:ڕbԦO}QW%~Jʨ*s.PIK?i8%MqUb;Ue%! '>]?kP/ ulfOc-3C拖*~sR]ȘO Y\5Qa>b0]QgL;J9nVY/)MnH9h:|Y< ݪ&ֈ5? T 2! ,Z.*q^\53*k;Z[OVUJ8\m /d+*sETPʯWjR.LMM}||Ξ={W^|l6Eƍ6mr]W:Q%Ecb? mTv;ng&WY,>b:95tᅤvvNxJvNcK{fM䴛ODʛ7y!8sk$zP)rrEYy,Y/:@K:>pȇOLSp5:ښ3ޖ ~ҸՎn%\hKIĐZK~]aj @Y /Pt +//]:C@P裪jks*Fn^hަKEG[C_/J W[%ֽS$b.Dt5d_Sek'/~?qƳO t.ܦD~ YY &L0$%%E |U7 t=Njb +EPZ0bO_jx䧄T;[+q )GO_[C+(ȷqjdޯ@4iC eٍY͟=@7!1},_ή_*:h%%6<{ ++Ө >~|u L垽/t(Vs?gok]R Ч q#+6b~켰XfS|r|b \ow*SZZ6vd?&*)᧤=bDfU}8Ϫ65kps#>ջ}YzW'ӠGv~r3ע;7:c<$,fuul_y_ ?{+ZhRwv/5ѤqE'实Wk+b#k5k۱LjZfVva$W1g ӗ??CD.kdtʶi`.//L7)`w:O!//oaaaeeOQjE%jA?x\ `gkյP(\Hk&;;wh]ή^<٪G+ӽF:C}\I ٣o p[߷ZOWkoqڥG{QK\oФƑ9ϖl~e2񉩡ᱶ6l6[|GiDZ̬6V~B, ܼ9꒑Mlڶnջz:Z 7n?fjl0-N55ԏy]KS)ΡI箟6i{|:q_475{a} 8pqA}W'ޘdی_G*++x-r66a،y/SnIi\pTY*+# (T+CKSUɐcߟS'32]6e̔7 {؃Yyy?{gKEgj ~l2~wB 5[BVFS}>Y[O~t~*w6%^u씍WiDZ{D$=}MCkѲ]=x\R9֢yQ۬zn^=cM|~,P/\,hk>^MUeӎc3318ky6~ ۏf\4hcS/̈́fnjukVN{>)#T%ޱHoˮKyZ~^fv1s{ z޳] eܨ+6wkZ6!ŅJY@?]%,Z},1ak7fz8Vkq\y)t=: ԮFzw03x _Gg#v ?˥G{s TU%ʒkUT]jkcyNb0hzukƱd*j,kѳ~Wc*B ' h8Lׄo5]T>Q*W~1P.ҷ%#î>e#U|?Bu_5K~4J~?l6;uGH!_%Xl8oƋߑhB IDATw}g./T%Ga<ѳ&ø;ʸ`u7iG8B!B/}`;ʈ{aVP|q Ni{ +x 8sx*⬆#J}1W` J)9x2,pZw!72+?qJ@l;o-@q Wp\"d _+~v繟{E !BHhdXȪ/ky{Z^St9e#Zlgl.BQ /r,g5 L+스pZN5'%@q6A~,Fs7ߎ[>' BN^ݑYr`.ôeQu̓'u y?:6Y=۩1ST-U+7,^G 6_gach fҳ7HXDTS1N]rӿXrjƓ}[;E!D*,|Bd~\|.oèy4s9!)¸b.fD^$BK~>^h4V>d>F)fl'- ˷JEU OY#ps( ` Vi N4U|4"R9umۏ%3/=w۶bMɌown BE{j(dbdjh &T؈ښHy >eo X GxW`*&owJY.P2(n)Oߡ8]W/ &=*QgpQPKyٱoqȣ_YV&8u7 )Fzmub\B_0tڤ!a1/u_cߟJ =ش]:8op9p쒯ܼ#ݾ:5)&;r'o2r45Z6}ƆUz䕈8VϞ:PwUB"t]ȹ`Uw'.Cs 1-EEIP)O)*_PK=úoyRԃqW\>?T@YUFae#P1'JVqVy1V)oo1bPJN27oIOWs7|O@!;oOqɅKߠ 3V$$rs>84oG/FDii%$=qJ>h؟x;/ ԣg6,+*@ϮmB[=p8\#.mUÌݏᕜѫyK3Y_>w߻{^Ms])Ѝ@ gzuk @fm@0kKS+ 7Wm:cjby+v0vwm/â/*-{2k3G/'/^w}чr~%jjʯ߾>w_i&FzᑟVm:p| j;Qvhԉ$71BII!77'/KH |^~%%|ԹS5 !Bj0.Άd>ƐWGğ)l9@؍S.IF;#2b.A jUtx;#.Y|Y ᕯ{,-_eh7B$oD9xl~Ww퀥qzF^۫Õ?zn\YY ]+..vi]#ʍIhvZ x:fܹcSTb>%2w 0){Slvf^HJIOLN!>15"*#.ܺgBms9 ۻmûW0_,'7}\w~m-ԌOԯ1fMCQD} q3q]Sow˧엾AL5s'_8FyQ15w ۸rc#ݗ츄GOܯܹ*J<^Hц#Gg"CʏӧgG7lY SԣKG7l\9@FfNbr:K;myځW[(:py]K~ueq^9m\y~>]JJ`uׅ=Nl]xC'.p$Jv:;6І 4N&wU3~q4<'*# N?^&BY/ ba=JLz!ebh]x5*s! LFmд2̩:(J{*1vSt]/Dd6d:۽+;Y ^kxuG1|VC>PC3W~:*^D eFfN6-EWXu3~f usы'3L u޹p@.y>l̈rrK~:g3'QZwݽݻa/^>:5SQVbXr%%)tԾMkgwVܓJJC.^iypb ʎn)PaBR߇C&"*˓g#]lm[dems՛mZcu|"ZcM|>| ٵ~B0"*wAaL6۳ܙV:b;CTY񊋸X&gSF^~?;'/.!%"*/556`bM85}=ypʊ̬e2E\S}}]()XH}QZXMUe☟(+)NyK3WdyLܺ^]H-!BHh*dP{!bVBV'zm11<gC_QPFpZSaaɏkX2ACޖd Z싗YP҃Lg!b=.ĻeBzCuO z8wfN`7M *3+L͇鹹F uKH~2m@ۀ]ζlfj43j*j*2}edr +σ F")ij捛4n14fWTGm{N'&$~[ޗ⹓NLHJ%Eyh[ƅw8kj1Ci]Ktv QϮmN{-Mu.44Ҳ/fZ\gݹ>||Y,>̙ ^!H6 (t}ي Hވ1cXLVVԈ>_t+fƢxxӥP(˫  !)uы́DCJj&}=-^pD,׽S ̙S[EB!ԬN)vl'/O{mh>rU<ˬT* xT -t=Y*NܒLdȩcM/}j2 MfU Dikil@[fek'/~?qs墩%r 9E}9yK`z1w 7޼gΙȤkki8鋀r]::2YvԷkTLӗwM}\ϮmŃt -k\wn/zד& Dv̟uU9u8PxeP}}N% ZxOLUxM;֩570U)jԜ xnz?>~2wkKɿ >c]71)M f Y,#Tg(] S4{eaDeʞr5075LȾAMsyyԴLff?NO_O9qA=n[@FuFx'`(ʞ/enm^Wi&mm,9u BqR=%fMlwӗ.Cg1= '//®itl-w8_(>Ä=o<n/nʊ,kო98  60,SI ̈Z%k;QQQA Ii߹ixHfʾ?{|:sw:*Z(!B^$J% cӃ;9OH-p|jf⹓gNieagkcv̾:H~@MU}Rv ndž'hhmcjl0zxS˅E2sMlv̧Dќj*Kd;wp`-̌ZޥRtl>n[]":W675a-̍ޠ iAy-4U})s;>fDƍJʒR*fuX\`\'YGũLKdeee/SO=ظ2A/17a6NEEYIOWk^;6w$]|VUTL]7Բ;m)''[\\n~_wے ]| x-MG6u+.NIld=[W]M%":xۯf]Kc㒄B n^=y"UU=o03(&RNը,G!$ZocM-SC~uwUL>D/۽{d3ةMbD/e'^XM! Ӝ ֟WZZ5B!B(</Vo>@_O[!B!?Duh̄xߕ>]L Tp8fbM/"B!(yѻGMl7LRSUQSB!7B!Bȿ QB!" !B!D DB!B("B!)PE!B!RA\T2B!?B!Bw B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!HQ:Oc1- YR|cxT2|1R/JL߇f7WWtjdƗ94r f_@B!BS}%J,()%LnUO/G!B!߈d7V((ee e%rY7#vt~sNt-belE5JFֿyPj#YCB!B~d1S7q>(%JF4 L * ;D(B?B!#AuWA(Q2?;n҇bIS{2W ^ZxŅQe[Qe;>˩7kwD1Ee|%ffk9,$&WڃE ]Ij;[F@Aē,'i74OAjV aWXl|^ !w)rj ;Z+c)"5MnˍIpV#clc,‰yrg7])'[^\q>P2yi)7W&*ت6`4p61;^-%X^bt;ׄB!w| JAz]g(yKSlF>+|vS8]=ˊrf&{ױ ,- w‰yͬd n棠P$#'r챱f0yasx[,hܙi pb}cU7/7NTW na@/(> b#7E%|z,}S0xB!Bݾ*Ƽ1Ɛc IDATM&()4/Éyis{vK  kYQ.KVdf{ڭ.Ɋpb^2c؞h"?7),b Aqa%j/m-iJ0dixy>9McgT2iM,9EA=\ξe貐I|yYH_h!b7/򂊭vPP*ͧXs3, qLy*ږNۯ2Ŗş)YcY*M} QB!l֒-\UZStf;12_Pb6q]A ēE/ XM|_"z`n9@.35zt!Im/ynRءlɐ9!d:ڠxaپg˸y&ָ;esˬD Yj鰭4 M *Έf_ @A{+((-ϴ}}&9eU:MS4ȪH{SڭJI?.Όea=qSlCB!DdT,[D<2Au[2` *Y,u^%9 BnJ0ޤF J((csnOcK xiBVx)),Y5ۮLSVu E츉A&Pt0԰RѪ:O;;]PRtuqŲjj: S-Y<.Du{f/5T0ꙪMge2JrT&dx)y-i5XY@,GWZ!Z1tz]f XYVN%7~~jAQf!?vwsM1 B!wX% 9dUt$ 贛K((>0 §(!өIE Y3[A@; #Ɵz_Cyw%2^AQ_VAtC78}]/dŕ4O'hn[qPbw1?^AoBq¨7Wq3JZ썚ڦu,b~K13}BAia",+6gjѬ>oRBRn@a/B!%oSZ6hk~ڽmE "ܻU, - uVݭ>/<&WND|~<>x"%([M:͒l\465 OQ SbՆ[sdLۑd/exsTMoO8?[D0C11^zdiai CE6*?Ef!+Rn.Woڰv !B!"I %!B!dSY5B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!H(B!BQB!" !B!D DB!B("B!)PE!B!R B!B@A!B!HAV2H&B!BȏDKKK2IE!B!R B!B@A!B!H(B!BQB!" !B!D xsBG^ ;v#BQ7nػw/UVK?fW\QWW/O!54hТE۷3/_\jڿQ8v옇G֭7nfgdd۷oƍ[>}:ׯ !,~~~K,L܆x{{oݺk.wuuu @̰aBBBȝ8qUTT,,,~Kh\\Ǐ'NXYKMM7n}}3gΰX,5y-Ϸc^"4dȐ5kִo^(oK>|D2B.尰_k xd!p8!!!˖- %>}yWaën˖-V-n$DhIII~ȑ#%3!l6NjmР06- Bd/5k֬BTB?ݻw K.K.eRipBG533R\ ѣ4**jŊ/_^r%[QcѲOƍ2ʕ+يES bȜ={wޚ/_'UUFM2544Ə>ߢE 77{zzlՕiu%YYYEGG6h@ '$$Ĝ8q"22[[[= ϟw^QQQΝD[9tЇ8NƍǏooo/^@7n܈ѣѣ% B~0,KtC,X,&vZj˗/l<:LSU]KڑІЂlx3N23q<ƙx$'33of<睙fLvBbcx /B $ЊvRKUW}\Qj5##Ip|U2U=u}.icR:;;ܹsΟ?/IRFFƗuM9 pBGG\ѣ߲eWD~=C6mFؼyO>yf2/ g?joFg}СCwycQmv/}K7nAx'>"M0Xk=MMM x<nٲWmxÇ?cկ#G'dD5dHOOO0,**oSO;v,3 sɧ~>"?֭[;|;q45 uuuA@k=siii۶mFY;ӧOg###===E^4?Ozzz'z)'455MCClD"wk3yyy򢮮nɒ% ?eYh4;w__yf|)))_r3E[o F7A>w޹lٲvmDžB&eԩS@y?SSSڞ{W_}U=VQ=|0jKn\7F_>>>K/__x׊ /;λCC|w}ӧOǷDaQŋ(կګ*_:;}~$IR|FGGv/}h׿m۶ }_׿oh>qDMMMBK,b DQߢ"NDэ7/Rrr(o $m޼y֭zffUbq)|3KC H'HP(p8o/?Vg){+Wl68p:p@KK^ߺu{ ^}U}QA`ڵ ANշ|>竫jjj8۾} /ceYB?ǝn742_eY ΝD"+_ hگ~0548cY8`Y"!4Qx⾾>KJtB裏rGQԷ-{{{O:%bZZQQQN3gV}vA?>a=t{{{(h4۷o߿? MJ"f)Vb x\[[ k֬o4M;Nٳ$(o߾70p缟7nz<axEQN8Q]] dOޅ8ހg8dP"Ç={ٳz>++K&/H7zh4zޡ 8Lr'O EQ^FFF@8{nڵe;C5 F&r{ΝjKH7n~q\^^O|僋ƍIxdU->񟓮o:YreEE p_uB;v%KmBHUPPhvDiڼ<ضmYuVEQfsVVɊGȞ={ѣ$6;tvSO?ڀaYvxxLO?SO=h!f)Ȟoᒹ|Wr{P^^N^`/˧zj[ꮻ/{;v?O=^'e/2;o??W:'+Dj ?}ꩧǧ=Elٲj}G_|7ӟ|>_0y~˖-SC!rssڧKwѣ<˲d'dY4;;? /z$Ejk֬n7e?={BX,fX~axǟ~_|gppb$`S 8|yy9y[\\{߹slp[?E2;vP7 uֽmmm?j%ןf⋡PHxZr%˲dK+^bEiiiccc=F7olX59i&^ u]%t:^o^^3ah.L,zWC`B!4]}}-[@:tl{%.G}nV5芶,LC!DѶ|!(B!_v;#~/JAB!@IIFIH]]]cǎ0B!شiӥqY*B(>SA!Bi !B!(B! B!Bh0B!Bi !B!(B!'j&!B!2Aq$!B!n%Y=h:p:B!BMQ!B!4 3<!BhVy"Q_L% .ȘĊ6߮E!%HQBNlf B!4DQȞb0nI~o iCbUo Ȳ#ˊܬ (z+f; B!4$}k3_qcb0A`Y6!E1k]RGкOF#Ib1F$]f1QKH(z3u3F!u|/]RTL]n|qaL n{`$!tHQԾoѶ:[;W?<_ߜXzN`bMví‰yw[놚=}0VG$)'3IؖE\ M |T6YvIȐֽ2Y~9}fl6jX,l6j%ϰEŢѨ*pЧXx.SfK2qCGH9K%I3 IDATc!jo<77T8AMFEi)id@Bh o*yͲ5VZY>:Ϸ@|ɊշǗ(S2s%D7Y_ _<J4' Ash)"IFh4:c1o]-Y101#KMW3 t}ЦޮEV̹_[_74 xrL.Ydf+Eh>=dGfjbL=I9iHu<'1)6:0)Z^λ7,p(|v߮i%< [\OBfM:t %ɒ?{ 4Z'htWX^'qdV BQ@$AX~rf{ bL:ym?0n̰%ta8 Pf8 apz:vエ -Z:@άԡ Z}c^9&l%$ i桮h$-{EӔ#+z Ƈ-^糊rsK'~Wfۣa5Iiª^79Klg|z^V^y!u|)jTݽI?Bu}|FEюs#C@EKSg㌆1%Y*֭ sлۻbZTQdϜJƁIΤeفcti_D&Cvɢ,z Ŕl_Zlq\A4=sb,_G?=纂>?U%pş{j-'pF\NrFôoYK_@Gows;EQwNax-)Bޠ7M&tWqqF9+X\|@o_gZ*WVG_qEQ&˔iE`OoS^tQQɽ_z@o4KxqlҬΔ';>En'MO(`ʁ2=5QB&2O0qFaYVѨ)%tI-iO~)s\>f` :Nl |EGJn}7fVAb:(v:wl\G5ui++8_k}o̻t gO)_[B7QSjX/^QF6QV}WݞR*&Im -5Z,ϑ)/-с뤨x!;+hk>_XmN5&|險K#ֺ,^hu0p úei>wc,YXZ07~fv.H:|nEQj>8+O 1ܵ wz=>r\~qa(ܿ{€Ws$^zoNWucefp{&9W4x&g%(h6\d6 3-5aOl47P%%g/.ƚWkI[f{ݴ1N>|nk~*E'rݸ ͅj“D!As .t@D6YU[ PU1ߘe9Ԝ ÁPWyD7[I$h8ymaY0Iiv^'4=>>2fO4+^Q̚^ !t0FIX{ym[:ƇFIbqD\NI^v"$ ;ϜW(^+.67,(^8;864Zi-9&oh*̡h=u;y:L=m -Pdaﱘ$-kIy-gɢ?p :-( 4||,(g"HJw]K;:[ ߹?)Ց6G(6:Ctfhm3[eK@um괕+h2.YZ~}S4AF_ywl4tv;pXoƢ%_EBo۹#^C^geq؜ G l6q8 KΥӧAp+ESN@є1#ic(8ѧLWD}&A1" ze4Dgt`Ⱁ ,$EV6g8)v!</*Qx`uƆ\"ˎ<#j[jN.YY0 nY:ND@XaՒcn9&;&׋[I[[dO3uFwt܃.ՐHpfv7{F==貥$ǧqfw4S |7]y,GYVՕ.zAVcr\0袡lptkvBݚ8[xۃW;B]IlIۤ`l=6~?6$<q-yf}VԷz^`ND&;ÃCq3Csg;B !26gGJXȸ ,iyf"Qb10ͺK^B#>g[=M{:z,SE˦a߽_*=yRK&SO |2f@4˳bbZLӊ,!tϏ ᨬRT;|ٸo7!F"pl~DQuu%8HQpZp599&Ǥ@')rt\An~P=OϚX06NHJ1#zJ` }&9*2IGbޘ*Bڀ|;&4'+MBѰ˧_l3f.q:rKVĸ%#I-nmvߘWg&O {Nk mu& Lƻ`x|dLh CEϓXPةׯ#n:ߘ'W';tF=[Fo7auhy=$HNsĤX|4ݯXIiHpw&֠+[S [7A_5mE]sB3ao?240y(޻y}}zz{Hyok#SqIަR;QqS@努m8>5 g'אA?:e9]!p^EI$IRCx,ZQYK?E`^)кnHΨ{Ӕd9faY֨ O.kp ZADƆF+E4C6$Io2:z4k>~&aԤfȲ<64ZTñ K Z"͙5MD'5d k)(\5\=g;+1}ݵS4V~Bk,;֞t.Hs|x(dQAU 'pYE,[h8 \"gjʢ凴:]Sv{Vo\[węFٴlɑ}O([oOvNyz2;xw3d:RSTm5n_:~LX^ul7ʲEEy-f95D+#Bfh4a=j]yꊬb) R x( y:AT^ya$>s"VgˏibEUw~z]~U pZ~}kZkϝ8-'ivu ՙW^r7~bu(3zӪ {Q4(^.YٮSx&{$tM>9WtRuMՕm-VXPW^4641(\tS >YX^va ek{֒}VE׷DQ_L7Vhj̑3LV5f,^k֮3mq~.BhF?U+u=*'ge[.Y0gĆGkHvA׃dx=iʩEQEӴpT:NKq/sInQj-D"~j{"VΧJ Qo(􊢾j {1# K{8+~;~];5p x!B E3lW{O|ۏ^H,񛗮1TK]trqƘJD^{}{]'[4}7aZu\Hk4 <2#\u8ْlMx0Y[~$ (2qy),&{EQւb˲)O=/B!p(t1;/Wo0x=5 \K)h4v-ATț1gq,ѐX=G,cOBQR4rfk֘ NE6x]S#B݂|> FaNy=>߫7S{}(axSʒ  X:wwjWA4,Kh$j !B c湐=-e~o3hs~ܟi]/E,o УYs159|4EEQMӌFN3 xe1AB!>Š#C=mNg7}?K@4qz>XQ\O q, FF@b-Ig[I&_71߮WQO&`5aBygw RgI~o0 93R|^EQbX,EQEI$I'q}iJC,˲ Ð 쉭AB!{<-JFr|^2%IR4 ƾw=YW"Mo+_?(dljQPC )Z lopldTe$grrsj(B/c#2:8<ݠb^]o| ~jhDCC`)@/ g 7$a$ MOEQfQ,ǚ^8b,+$+sM?]֛Qpd`6 :Զ3!Bh~ Y2-lʲ,b863AVx=ث@9|יWw(yfgF*˱ dZ83R4xƆ]4Mgfd?YE]Mi|PB!ne#Q {0YQ`+xۀ3!aX<)ʦC@'wNNY"CEMr_Nt_n]pDz(s$sk=լN#QƖxF|c0yK B!ARJ|hDE[= 6}RW<]?kK! KIiC߁̗`~v^w|y=R3uFCb .tv[;~;^kvu[/ZmoH@lox؁ ߾K;zՓ'Lׁ={?ڹ+hmP0X$IFG\ӻ-W^M, /n{LՑ{$EL%@WdS)=$MB(2$/uL_NAdp8p8v=99yиaH4$Ƨ )F)Sf{J2qCh$rxd}\妱t !z^Pxt!I{ ;x':Pvnz'>yw?#5MvBtLQYVbߌ" `Λ,iZ1Q`:Yu>Fbl6jX,l6fjZgبH;,Q&vI}&ɸXA]XRk^ѧx74{bXqiILr0w*?vb=&h }ֳgWYU舋㹬&̖ͺ;;ox s;Νi(*aZAj.~k6_4MzVFOׅԌtgZj,6޷M'ϞiF")i$Ruw6l{8_,p=PS7:I͞Lf4ɱ=Y{䘢??t54,hgFOvw|~f/28S}@S$詉B279~jpX^'qdV BQr3)I ,?y۩~y; .Ki:#=2,7fݸ *lx߆/m0Ȍq;ɞGس6diS IDATdy4MjILQ^\-=ށޜˎdjn_Vj!撥jʊ} }O{"2I1IZs:^?wYlVezYp&DLJzV;gkx$+?ɁWdfg"wdx`p7EQ,Ǒ"ϫ huU֐$ۻqXF[-IIΞiiz d'5=mSV˲\lin~8RG.D]z3)qMv35=5%}CnеKHQk4ɞ}zYzfp &|\grJKܱޠ_շk`d5_dOg4XmYnryHR,~_$lH8<24 /ONZ2EG&jI~%FńZ9D&O[{Fe׫ n,%;~viDZQk-htbM1OkY<^C C@+_]iHq#3;,I;q&aݥ$C}PhYj8-Δƺ]mkZOzw_}3#GPr⵩[nK\f@w(@\b"ˊwdnfu\͡UK._ ÏH&cJ@CpD:JrsM\% xR>e9d’bGwf/,X#nnʷ5 ޒ5,ǒ xYK8$!ٛ%P?dZ1im4MS2d:w'mjKMQ6>[l"JFG\b4f&r&0P( ]|zJ` }5?gs('a|p0469oɥ7svfow#ŹbjuhȏEqbQ3-u=f0,6'~)~Wܾ^^ F0BkxKv:4MO낢()@)i)%'zp< $[y}^LKv)aI"/Iw5DQpЛб!t8Rc4?`bN%']\>/.< {.tuLV-.<~  p 5J_#5d1;pfNoЏdYbuGk$GWIʲ,-. B5?$QL,#C-fDMƮNظhp nq =+ÖtKF'h sw~’bN G*+jrajj%I2M]mUK ˫+?Ӈ?9ha4*?)hB`Yz)Nϸ.=+tXl6}>\l?daXL>U[p^ 5letDP0XRQVXRڶޢ%Ų<'|5<70RWL3EQO.YLg!fF8C6Nd,lt~f6P(tD]0LIO[LT-̑DL^ʫ:R._v}`W[s}q'DQMk}w9ҟ?X^ul7ʲEE$@ݘ5HB(:$I$$|"( jnu ^teYЗhrᥢw#(w#Fx<7&ATiey(𝝊,Sw޵DhמihQA_ vk;vBEA+=)#.UK) m:u~Me-]Q-xtSV+/[VA1 jݚ^CM]$-kz{rzZ_ZӶmmi< B^a>Xlul?^oZ,%-,L1ħ4Wʹ:;nk:ٸ 1BFxߦʊɻaķ 5-Y\d6>+'=lIq*yqFqw%^;ɸ{K^ԥfv(he{e$!5M|P($Ih4a ɎR)¾M x}`Hd"+u1X }$pRO\M ^܏!BRXT9[7sr$)QhYw-fTa0x'EUd*~Lʣ$DRĨz#Q@tNq>Im/ w4&^6߲lJ;/B!|@fi4:Q 30|ZkWb4jea+4aBjOAH`7ϙoK4˲ 탚QcDN(bPp<-Fheلl~hv !Bf C-i)%{|#:d$}yujТ<4q`FXQ\O0 qZ w1 D^D!Bhqfz㣃=mu$AO7PͫU(h8l֤nxDG4!XeYa00B!|dY>=,5 ˲,OAjD5FPQ!BhQ㖉'E< Ex I(B!4aĂ\xbB!BJi'~B!Bhfx$ !B[J]uKs0B!|D?XqK :]1Wm](,+*&^a5aBy'2CnY.S uWa](XLɢ(J(I$FQ'2DS4h4 'Ja=5=0B!DPNѢĊ[l4-5v3zTLE#H8bxnbĦ7pa[P{exMa5{aB%8곧$!4yuT(nN1XT;r<{ҕ} ˢ40o(),z:X,isM;08+LR4K6橭 ·B%ΖYm fX4߮WeQd[ZX`|n)_gm  GDQTo!ex046zc~:1 {Y' F*S 'zgjd4>q{$;SWxgn ?Չ"(y=sBˁ+=$4?ID{*[kYzqlfjvdd73Hu R,lҟ ^|I$q@ϮKD)Z9}cSMz7t/u#7Vs8M~ukgΧ|k>NєonB~&eDdysg8Aң&cMF/_Tl]Z 6>~kIWV}}czQ69,',]\NœܷFqw_eðE.-v-{mNI7M9yt}WSw.,-\;UIR*(MYKiYԴʪKQ|d,2jXv=ߐkPw+Juhɿ]j2Tz`,nf{\5+K=MO9ʊ@"?7g4G>R)+k'6_௦ 05 C "(K r k4YN,Epк'ibRT]Ƨ n '[P"J+&FƔff(fɤӟ}I}sOS575s\>*=}sS'ar(zZ plB iJ*|r tF_2$YƫdtvⱗԖy\rSQ2oޟ\2W4:zcK'x +kʛ+72tGHVFxT7.OWl~xDRp.כ)h΍)trϪwE'==1xIB+חEMׯcűtAW*' ֱ_╈ k L:NUw=7K5J%IR*rRC|oċ/E@!Eƽu$˲[ kum~s:_`grXt&=b o&Lv Wܚ]jLULOys YZUnrX_U6Y V0I,8VzF2ȕ#{}mP _,aSV_Gd7% 1|)k]W99=2~kPenLKћ2r29,gMV]kQj+[k)gXBy)JNᆍslu6O(=e2$Em/0(>gyee\  BkUofaX##7.uc8(wWMt+^!eHʥ@cksck>7faSPWӰ~Ohڴ//ß7^:ST}tI}0x00hfJ{ll6dlsU9K dIN,G I Y!MI2d)dYV>7nn_<Ay@sCS;/W,?Z)> pl8F>@ϡ4Ih ðl͠ A쪽ԥkq-NArI45o~i(T#}yK4M$Էas i(`8Fh^kwIҕZ2IdW,-]RFA65Gg(Jj}lVNDȢ$q0 HfVuX3›ōͪ$ N8EԣϚ4(Byl:[:Qnj.ۍAEpŧ= bIQ,jt[**jz}&dN^U3,˒{h$Ƀ(AAi-E"h0<2vS%V"yr?ĩ9QHaa8A4C5jNj( /gWCA  {ԒQ*/NHVAD;-@08MxI xN4M ò,24$BF(BAd/" qGaX ((,ɲAa8$IIQE"Q  EI/(J1퍠)qԺO(P  N%WeQEQy^yAAP^̓Qc("BIX=P  Lki Xdag*cP pl&-`[1[t o툕$yeYa)G^(BAdoY mngqn`s;þ{| #T*eډ^G0P!.\ް _0$=GeәI 1/Tiԅ ,VaY/18Vcc-! I.VDk}EM{|%Iy>h?GP lO~vi?d2*ܳ^C\9Ħ(\:6(ރ> Z +ܞ%)FcgyY٬u8YEل p٬ ($8#{PhaYjT  !  O% 3I[!> |S!`szs K`=ϾY69>iѨjY((RfT*9a"8NPlh5IN[\5QV7.]-.E꛿~#Wld"ߘ)@A~N8ť7Fq zKw“O6,K,&-U$W%&_ON@_)_ueih6fd2F`0 dRjFtnCQ|;Cť3lJ2#k."lJѨ5{I%SťȎR0 SU"~#>.q{!|}&I5I l7YV%Y]Kn\d#"fh^[jZFQjZU@)h4J^[qwǐ$x00x(Yu- MjUuKbiniغѧB;"Z+.Bt*֨utumt`hi~]9o}܂(JR>oWO,+n [l0,]^$iyqYeW)x> J١RovoTg&?Zf̜(VzAZdUEql@<?]Z!Hᾁq.˙cG͆eBw{5 DQdX4Jܸ[֨K*;:tƭaY *O:QY[ cC#C#d`2;)[ L:i[쯨T}OuiQS'=hL?QVYkqZdurt8SLޮy.Mm-Uu5p ~څ3SgnG|θJ<'&& ~(잝toUɻ~T#vCᵟ8yz`U~Kg͍ۊ[<1~ 4N>ĦÉ߀0AqC$EQJ*JR4+T$2ZʨJ6ۭ@`6n8ҷ}NQg%奧>/b8?g_Ou2/YsMZ$bY`vK_'_>kjon{^EӇ>ot]˯/wHFǞ{K.\$u.}q$ckQY3|X owvm ~y/ڝHRaG*vt`Kf%w}zT2}˿|s\[GJE_0w3eIJ=AoWP}6=u;zsV j ?'zw1,{a ^kf&:W^ڛqw_eðE.-v-{mNIV;r7\Źk/wvJkZ$R]_tJW*i rCm-m-p9]Z;0;rAǞ=|^ HLwz;V) (RB>0[~ݙ醖入ϟUDv .g8f2m- @:$InO@*!o:iiפT]\,E)H$9N"wgPη)UfWY\){K UVfeÞIi~ Smr>*:.jy/*qfl˕m0zŸ/UQPVU7 wǷӡVM)i'r]=͵# -N'͍Ox'w~It25=<^\ K,+s 5;|n[( [Zf绯L%S鿅w_,,T¡+:pAe#s_6?nALmMiEYiEY8ug~4CCVÑ/>:WVUq*DZ~s+AQN2Tr?0,IoRƯ`vu_L$"O:aĽ$"0nOmU6f2pO!3e N IEJ$IGDNA==1xnpEDwHl&Ԅ!r)Xxam!^Iө#]ZV"(IRTq kI$IJr%F*U eYVڀtuOY 'eɤҫٱ'>e[WU5կJ A_p9lVEٔO։}NJf's˾ &fUqz}Ѵd6qK=vCMFW4:m}Ak.;. HZVIͦl&37pu-m}n!F)i$-/,K=hz׷Wd/qV7ojea^R{gV]YZkPʹ:Nȭvz ٕ(%wyE;_F]Q]9x I&r `0_+~.qJa:ݺM鈮E{zx 7={ vaة\˻i?qqoͷ)mW޲P4mO"[à#*7IE#Q pf3-a*wxp;oa_+Ioa6n\&[ ['Irl5=Q4Me3+׹lU%eJU[D%t8-&㮞,T#}-{4M+st>2$=ЕqV"QbkU"a&eۻǑ! dV{t} }%INR$E! A#L ȣ7ө-E":|won/E,mMOqlA"M HfFct:-˲Ef@vD! 8J\ZX-ۑZabo.hpiZr'Do!iaT*eYaT~h$jBA  {l9\aN4My;p7JJHEQEv;D!  O%MQ%IFP Pۡ AAy'Y %^Rr!#BZq  YJ7d3e%~,   <D!  bLn8`;| uPsx}7lŬSK6y>7~au YJ1*'q˄Cq eVǬg(P@#Q  BeId2Ε?+3[ 2}bS@ou8k7+{(3Wfgb#zZ K˕܅s+Yh(\H7Yi}U,8(Cf5AX<"(BAAJJ AlV[/U١.bS\.gpssF~N@WnkF3 IEA ٬u8YEDYAPHĆ!_Ge,JPRHiZ="1d \çCW{K]byjeә Axw!XY\zowzt7(<ޮ?dcN|& s+`8g!TpRx/_xM`- K`=Ͼ/9>ijv.vfYVUe+L8aS>4N#WcPo]Dyg/"lըX^2?|Z\<³ .VaF^AN$~o֢_ޒ V|n$YQSL&h4.kO6DQɷ^=GddFId:&(T7RMԖ>Ao 1?241MeXUT`[ܮn V=³xn:^5C(́vV ò? IeY-χ!W;;TjD`̦˗gԮ|B)K?H,B@nuZVj4ZVUFu woMEMjAĄwѦp h#0 I#p&qb;m]\= ɲi*k<5e? <㳬Z% xonYDxMs'0:_I7:[#K=7u{$ H]]Vyf`bq|8tdΤ/8"Av@$ _ <Efj{+ʌ {dzjpe,xNtWt&퍶Htf`b-2YV]UYT]x\}gK42 eoy}5M*Q_:?28>'hqUj{+|-2}n6Tx:Ti-5gqs [ļ:Pa{V9u&r0MG$1zs P4+moL2Y0L~1N ˈKq}+͍_$)+Y[pxE/uȇƆFlq+Vp@l-J3L}uM kޮp0$ f=|ޠޮImNG DZ+Cb[77='honlVb7e3/_A_ }_/$UPz6exb,2 C4MJ(dYqA`YbX'=5eu=<&Nz,-/<.wiIǎoks:H74gnzvc6#߼r\`qnKO:RF(" }'*㫱γ.hM o[ :\MUE%ߪ?K4;ZP8pS9i䴄ᕀn~س ڟ;d5{ci-􍏜pG_9 j2~yyrt3hGo~|J<@"?7g4ZMRQ]|juڵD@ׯy mWS\HBqߡOE%af{Y*2Š̻pLWbYQ3\XDiZVEsMi]b/HDJ^L>)N|gt,&4j"s!rMgP %FG+ xjf' WX@D%GY~a cUbpw;gW-J.,IS_H%Q8\#ph3ZQ"(qըk>L>'|#M TiT`;dVߝp3MF걏u }٢~jZo4WpO%SRIQkqnA 0y}$`4rATtu"WX G"(atd1+cqVڊJt*T~lOL J΀S삮X-r&ǩ 4t07,zi],$%ųӳ*rwe}7մ6s4_?d>kl 0×_ծ5:8t٧vui+1 _iUo7,{vJk欫6 b"ug(ZjܛQ$EV4ﬞ֔ԝJ$۟9m,ñ"ai5w{X:l%86T2__!Ͳ> Iplj3EOz ȓUfȤ2*g5AjU./bjo9y#xI,)/UKJ:^.Odr/󳥛&u]?dZwCbh~m>ɘJ&nEoifrZdYN,j=5Fʿm@^PN]m̤Q#J6`t&AEeu()#vdYVS?71ku۫ZjV㳙ln/1;u{2: C.IH<" L@Cm7eE^f qc6%Rpڟ+YFÜ}i;8r #LqbIAosLJGW;Gn$≉ᥪƆCz#77=c okYl6ok/0(gך<>2Z߹GƇ%nO%C854Yd3;]v M;VFCaG덆BT'|뷖PAqq f$I#=yNd2pdxVo4T \MR6C%,SR[7dYO9HfG-7lYCH"-ʤ]u6YZtFL&nԶ7.[igsU7\8qiGަ8r Ed??J1;(5ӉſRd궷=ǧPc8L*E3i귔[P578}Ph]᱾!NmYR{+fnOktH ˳ئn_Oza]JP/U9er#{鸦5moiInZ<55Kk];w5^gnk&/<)&=-]~R+fk  HT(6p]QtC[EQMtUt,3MUzO-W;+JR%HR[L4%&3nE;׳tv, (GG6 ?CG|տ9p٥!B{}.,lYmFu(fҙX$ R p.[[hN,K'n;Rގcf &#˱~B3k$*/GJLecW"Zwsʘ%ŞeA=!BE)G42cvn|#טe(^WHzdFY[B$hKm83R{j>~@n\%6B!6!^QamX6X8mAQMS,ǍY~C3]!+4EQTQ@2<$[D!BFV *ӳ}.%$ ax7 C IDATO+*Ÿo)6?7;d6 < C`BB96m"gYAGkaW/A <'LGn{3KDP@A$E, zQ9a4k(BHÌvnbpD[)^4=@D*U8 IeY`d$AvIQ,˰$Aa h !B%ZHDc ;`6 fHHha3vj(t:J>tN t6}d(.hMYS 6J'Scò$$YRg75;*$Hv8<6Q!*,[- ޢoAEAo~H$\0ou ρeo)̝ϝ"A4]q2f5ZPs,elmzoX<83ҩzN0xnyۇ!B$bqajTh+r6MRԇKgW@?s/dc`ڟڿl & 0a*fDRð EeŜNG3CH,*+*+V+*#" hnu(B!BbOT6L'T`kP|*D5Bp7n(ƕ˰YLPlgI= Eʘɪs :gYaEQ ' \ S E a%ES5  B!6P1R ~pgȄ߁0Ze}~z5h%wCx#:r|W,5 .UN;z5au_]`g&EQ$z=/,[2kZ z{Hp8Bhx=l'>fOt9̟zW[&;  [ҭ ut~ѧ7;5(Lf)#TL £ooEG bV$/XTϰ U7s:}ʷ)GFiі"iDo Ĥl.)/6c|dCOmoOrzbr!4_\V7u}2J;[M7ԴbY&`nzSR b6Z[u_&(h~ŒSjIttdc$@ZB]A0 ,4?ISe2`6W]iflEOyݽ r,IQMKӴ$I,4mi._ MƆƪښOHz^k Jp>_м$I6>H?߿~4EU;[+}Xl=Q@m~9̥3)$%D٩l&7K*{T=\nKfeמl>d`vWTWUVc>!I=>uw^#{o{Ye#=}o"Q* b6{s|d,Nͦ-MUA~}yխؾCh|Rg9vמh_>cȳO/߿]QS op9vţ1`hhiP|ͷjwۦ$?vmWN|1òOllt*m m_A$v,(ؽv ]R(q"`[❝klaD$?tޑvuϤE^O7jth;f9mWΠ? & utʒ,r_9QQൟ8tB`WGo-ݸ5:4Xklhݢmzey?OJBZ`\5D8 y8eYv)JQ$@En2wx$㉐/`q^#Aos9Bfy4Hޣ $IKKF?rAtd!dQɱg_~x7vݸzu_9WǾ D~^waWonKEŞGHW.t|LU;)_P=k}o77T(PO#g2L}NGF sPuSE􎆺֖Wv[.N[WՍLMu3-6k.Q$gGn储X4=z m&S46M'>/F)5G>ʋvCqgܘL$>~C ;-]ǃLrW.t(B؄--rC`vjYVukѡήG9ٹKg/0]oNk' u)ò5U I©s:Fm?=9 E9z^y7FC&?;Wnrjm9[8 2 ׆iah樂Ф#IRUW2EQf&՜BgqQ,Yܣucl&SZM)*^|B#hĝ3Ss3ObCMFswhio=7x ,΋()/S{'#k?}oNg3%BhJROݹ73{Sϼ UժR:aY r1nOݣ--݅ rA˱m =}f7 eLgfͰI\nOI,Iׯ\Y~gS~:BQS޺sW31l..ZWե^yh2S4=15;5 V~u;м?64LtIEY^ٹ%`^I~Y\R^;\r Οpzu߸z}r-Z#AogILbubt\ 0V3D$`YsATx~AFSG!IEqYnS#(FN,˷zƆG${um mJK[ e-L*IFⰅf- lZ.g^:>wVOߍ׫jkZ#4ҩ(͕("Gn GaQV&7$%}U :\~:1R-w}o|=SiÿJE|zBP6QUlTu#V>-f2)&J/_Ϳixu+u-10rtwvyiB_Aǯ媷/vz(f&EICu,+2=:QQ_oGQE5477rR" K:^.Odr/󳥛V L}zE&c*L&ɟTEM'#r"ж@(Z7zZnoNn6I%ͽ~3 (Y7 %EIhTǝI%Iجɱ Wqwfut&B؝S)uLIenEQH$"RC dyy5sALkʽ%^Qgs%#c ntNk$h2=E5Uj(J4xStl)s3jXl=#Lqp_AosLJGW> ШY?r{hth$OL /U56 Gs @P x?X)2YO#ٷg A@eI:/t<صY|+ä\BkojV[zM~}@qhoWD0 xbPnڷj=41:rn{9]MV5CbWh2 F!0qNJ8Lc%Iޣ75Jvu&cSkKdq+ DaekkIIoiq4rp<_-˲JRDu).+=}Qٺ}ew),)/EcΜ)+?Iu3Olί-@k_NWڊddẎM_XIRg/di^ǗηEc90sȰtwv ' NGՎje28_Yv|O?xُA0_JhqOFߍk2muz[=}$EYu Ͳ-g/S)ܶϭ >^7xsZׯ\SS0:[iTĢW^<}N0vhOʪ*.dsc aYuIϼKWzug3`hݿX{]|Em/>Уw㚒֖%moiInԄOM}m"ڹY6ZX9h2j^{(&S'T'`6?Oĺ?g#=N-dt*r>U&N mh*W d%xbjx $QT7 <~e}7GBۏ,w46!)wv7oՅe`c/b*#C eNC4V?R+qTCj7T:bz.u8&Id3ᾁDI;kip߀p% Ved{EkC!qƆFlv! A$AQT/h_7#דӓ$*˲Nw[EQRKo1,SXJ$eyM=4$Iz}g|V.a\auWB!t_,x,~BǎܚN!RG4ͲDn~t_3,+8fΟ{1ۭf&dXBt|iEYc{mbz 3f G_xΗ u& GBM;o#,4MQTAQ D!z,RK+mY)%$Iat:^0o_RSU7mGI& G,FN " B!TXL6K2m"gFdhΗ$Iaxef駇2ƴ(wtANPG, h4 <3 `KMM(Bw'Z&$ v" Uo.hQ8HdYVL&#IDiHPŲ,q::{4 B!TL6ܣo]s8 XUW]zAA$I*a(: B!жP)adY^zԢ8j: B!v[q7J.ܶjr.hB!*Xjoh5e?j`VB!ZB!f—]͉η`BB$Ӊ`4Kh+6%^Fi+֬WQY6Kb-: B!TpҳL,e:H֭A($ɒflV%Iee1ǹFR{Hh)5ŹB)j@mku`B"әXabFlM}P(et*m=FHmGEؔuXS0ں0B!PaINot+pzsuuT2WY8hg(}, _\'O-cRFT$DGk~ ( 6SOB$͕NF_w|v!)$HR06fC!PaI[eT`6>B;_E٬AqVx 38B6Z v`c$JgܭI") w6 h[6N"mcsa1(' !BmȲ"fThݥ[zW!2^ÿw@Y%.ÆG'#d1AŴR$OUfNVk_4e!0{ B!6E)NB]:4 2 ɿa@0>,kKGuY~9;j0In IDAT\GӴ&E1vkLu¿B]tZ$Q4&A^L T 5O;0 7x5xHp8Bh:z]ӖG%O3 5QA'&-`x`7Җn'u>]^٩$QdK$l@@޷]KG 6xͥ^5s8NrWp\Np {R&JSn:@XEV  K;?T"qS[B :ޠӖM*<ϴhz]r: NkڊB]d!+97oǛrެ>˕hr("+*R*> `Y*/Nx Tj̦cfl6jX,l6jUk-˔p,,Ij7Ckb(pqf%A!tc}CDx=[ &f̍NwSd?9_rr$ɬ\p7Ahk9L-ސȲ<51(Ef uơJ#zRJ-.} /:+ `z^G-1 jP\wPdEJpgF,ʊL{)uK Ҷz/#<{tNԂp`AQPXS\Svᣓ<9q^D֕)Y,.[Ý2s3C,&~_nD4~~}iES겺=Fny`j;"ֳ g|HOJrljW'Hݼ,׹ʽ{n]T{ե0747J$iGkt1o4X2NWTU':Q)쟟Dq5l#1`m._{==41s;Q4%Xufu}S/b7'Qʊq}=ScGWFCfPGt~y!5oa9nVljO 2 keS-A}}^\Y}eYCuh.0u34Y.q:wHhד{ 0qS'^;Qvlg6mybESpT̈PX#o|v99xV]N:;2y@:hH;MvK.1 /.Rh)*^С( Acb٩gii69C#ݝ]rs^`NtvOIq2P{wINq3jɩ?Op<(ʩ/ /ʲa02M9Ͼr{=Sh{HB?!`Mp|/}m64M3 Ct.bHZ6˯*pD:6A5baLEhGLb&.SoSſR(fh|g:]`yxj]5]tܓTWGm`:}3H菎w'RV>}Eds7+-*8Yٴc~6pԕTHImzP/5yeEFʊCݷ*j*v1 Vwq%ne_} (ew٣nG#c}Cy X':wyb "K*ޡ\77H%`:{gԮ`*VXߠ,JO=,qٞI-;jۤngә[Wz[UL'sp7nhmU!EyJL(;cK>圙v͊*0TNLOLU7{*jZb[cc ;oX6"g<07=_xg ß_Q]n Fa!4m([ $f@Em!P'M$",)Jl2DҔ EL$ Yk== `,.S+kJk+X=W#)EY3 ? 5vʦ h)+Π:2_d (b&[_tQE1(q;ʔ5T2vw%]42q$EfKيmx L27>:YR[TQKb{[i]ekEI50 eS#(rg]V=5FRME {`7/߰lz\7M4Ix[ZL3W u\߽D#;1 Xl\_(U惡ޮP0$(.ݲmjH6"eVOh2d"yNm"ڡBɳ` ;sK<ӽe!H"b*kXZ 0h)ڴ]?1~sxoh x/H]ZUEg\|LM O1IVrKAKmKE4X_>NT-Ns|(vk,)eiSEQ.#X"*ESp^;ӣo]QT[TC7u6הMgh.eS_ ɿHƾO (,, mޒYp;Ҋj c;5c(jR}ZذAaF/]}߇ktpȳO[6[ynWb:~Җ^ޣvz(t6 sf${6!o{GDYXSznlRw*l怶˳ {B`H߅;viby6XB~3*)ry\ep`bwǧʥ iO $ w8STQD;<9=_zwx}AT4լr}ro߅i`$ޚ2Gnos~dErFєf|\ Owh`$ɒR셩 D+M`(2?[lRו L}:+3d2H?af&*jJD|qqRvK!g-dЭ QdEQDR&q/ȍCDQEq|!:qkT-!)Jȶ$Y^ղ#2!rkަKN'Sz C.IH]}͇d<{]hj LXvWCJ=s3:f%<Ωq圸+EQD~x/:6=*vz$Qi?SoQ5*jw_<}A`dpX}4 9ms=%TJ$ @wCe)2YOk?dyhCPsm_xyYBpFrFe3Tc~PQ K# V2 eW&&3@T`o߇+w8K:AJ$&r qjh7>Cfv^g*vf1Žbm 3#N/BCo-!vLFIFznzuL*3=`8 -uW{өP@Ic4˔VMYsbw<`RƦ#icu\$\ߴoqdqUY358~3;9M炵퍚NC]7۞yU 5V90 i`1Y4ږhvm\UR^ߧ'*L^ܙ'tbUuut^x~6Ӷϙ/Ob>ԑ_{%A=~O32?ɏ-TO}PWU[mf](2u>lί-@(g f=u߹Gwd4dS ]t9WYQ.sTD,q{2<>??J1;D_N)lu۞Y pTfhӾՇroYChPCmzzw=H,:eUKmL*#=MMNPx$:P^.Sɔ'r㚒֖K~W%mhhnlh^ʊ/=wZßѺ=W2O3ǾtI<-NNG0lOjr5=Q(f2t:JrUKyD'Ck_NWT\ i%)- Y@t8!eDEQ m @|տY!Bh3w7oՅe;-#רWt*DBs׏+8u\ Nc?_cVl n48+Tv~.VgQDYXlji*vzeVbϲ ~m!B@A$IQ .C`iP>@fzBszu(ȍK'n;RÄuIQ$ز@IP{ 9g5XcAB!&ԑrM,3f{aQ#טe(^WHzdFq_I:\_h1SEEݸS6B!6!^Q|u6X8ҹvuAPEq#Cz6LtDwq4MQU8A 2'S?o52N !BEo7gbP៞կ?sA/A$I (\ZYUQ̰p}GBظeװ!Qyf,(ḆIHmhp?= :ʼivI2 # 0AE3(.P@A$E, zQ9a AB! W$a?;718۔xAw PA r$ɲA0d2YY JFR(eXy9(e@[Q!*D3/08')aQK(+(>"$Hia"10B!Bۊ^B0,ˋ}P6ZQ00B!B>$'5|(%NAm9r0mB!Bm#!B!!D!B!C[6x!|8o; !B(ZbS n=B;_EQ|9ʦH,㨭(BəD4ogģ[B;_EQ$I$)͊٬(( >($ Z0 P&ԶF[Q!*,B"lءQ0GoGB )UDQ̤өT8sL#CqYG{oůҙLFEP5Vgqؒx ϻT Aou ΉBmUOOӖnk/km&P TDQ|o|7Ӷ|K[Ե/>ty(,+R~c@|zD0,\x{'`κT”pbl6fZ-b1fYݶZjbv$IͷfhM\EFI[zFݨ$">!V7n;rAm=|wbXޠqvmN@.8|[fC>˕h֯'zEZ*WLQKK%c& 9@ `0z^QK &+v.W4dYf@@ fEM2M z]uS=Yn߸ާ|"!=9L$ޠ/wLMh$dYԖBh>447=7 6'bO,SSMϨ%b6<%^h׮ӯvoC^;R.t@S%w-Һ #;AX$z&lneFhff>M>f'N>lq~cF|ۓc[] S#QA%}͖H|wox Rݲggw}.Ǚ-%3-  f}71;5#2[=奙t-/qjSQ޶wם{%I8zrK57N &-L>p'f3YNuGEu\džGw{=]X\VVY.7KIͦm4MT&nx^OcXqǵse'&_rZM}NN#|3wFBa$͖#'^&Q^`od"R*4qZEi ޺Hty'7`w9Aÿ'n{}\.ppK11: u{mc|$7[-{) a~Cݟ8>yzh.w~~Kw'FS$uM M;[l9 IDAT%G߀ ~!I(9^gCq\VmrT:-fHQmH d0_L?ҷ~ 괧c !aбNu{wJ &b,sSo`>)Zo0Vgοa7{{hzׁ}pF|)!|%̶_*[?~Wn@ū\.w!$<@Cs3S'zX w{mfZ&G/~s^9IaU`O_fK|so:GS;}u:qm-*jwKֿJ ]ܶwaes^?òj'Ǐ*C5U%VK:q/ry\kx[5*\{g}lK$QL&˿Wj9ӷunǺo݉EGNG.dq]wHatzn}pg}V$%3m Ltv;rs7^W>S.nszܙtZŢhj}8]tzѓޙp0C JtyFƻo?^ AA \جō6_ '#<\%|q'? OE0Yaf&IorœV#&g*kſU7I%SyR-ߦ8 Oh:-A$Eiĝ9O~fqth~k#lkݱpD涖+g/Siva,Lnpf&?} 'Dde6ntWHVtȗPMc\d|ykCV;`O*-uMOJE3轾側dC`iEY,:ODMCEQtsitZ`XV98t쨲ygi9Sk4wowYge=}[[+WKZ}P ?=t;N;qPjjٽ]꾖 ߬x*j*j@SS2/8ʈVyp-wnޒ Ok ο V9ReZv>AT6 A뿀 6dyഒ'o\&멮Pwǝɡ/`s; Q/.VʹKM5jŧ~$I%bU*A/+"_P\N؜(%d]2 ds\Mv/'˫*v\6[ eJa,hU$^dtZ>5aL\;˪*Dr\q2 GΏZM(Epĝð9OzkJ YwhzqOҮR7IQvszbJ04+Ib0MJ/\##_t5k"ͅ|ɰ<ɞZ5Z̑ldT 5Iןcowoݪښm"lUJ$IRR%b1'gb%ِZvWUP4mY_[Hbϟ:=vLƥ9/I.# D‘a;=J&EQmVqd&q0x{kJ Y\.ߢV ܓNyf:VbXf{P q]{v2, .:[ls#(w3E5ee`ֵ?py/p:bp9#g8UVUC*DZO~Uju]v vߺa~ ury+ɦ3@l_ӃKi[WU6Ea _gwڹ\N٤g&m{ӎf٤R EJֽrl& GX6 xEFNꛙ$iߑUՈҕFEQM;{8_pśkj *ZW#F^$Iͦ鉩 L:rFdaĔ$ICdsy}jl)xX>mOS#cUM ãxp.0iZ t荆ch5`HEA:w|kĦ<<+T!Q|>Ϧd< [hWO"qz:m:U}LƱ)%fp.:۸}l|4qmNE5:mh.uwAdSkxrA5j9%le=vaY^֝L&kw% E5Z^٩W;߾lj'neUH( &=dw9zݜϟd(mFaw58^r+g/WWܿGO'Z EQm;o!c]RVx|لH:TJ%ҞPXL&sVg:v]y}z:w0,cWlݳً7|ً[aѓ;^4ųjٳƥ!E[jk[Or_aOգlσֶ\.'y I'yr\6F>W{'vf|9]QSe[.esh Ih,q(ţ} \6q:˱*S^ֲMjnۑL$]LQԫoAѓz:ө40%VKՖje&>ٗ(ZN_,1lb ܽ}'ɒe[_y}!S\Yey]S`@oWϾ#uzݶ*~ &֖feӎ掫s٬hhݳ3Qj^oA5:msێ- urվ#;^?i;;ppO$EV9~rKZ3]X_DU9J_ qC 5Z2Lmq6a{{eUlM}-ʑd3;ɢ涖A?]e55lkR^<55Khܾq6Wrwu~uǗN'>`a8sD>y_Edn\.L&*q `3EPL61/.}TqwCs'+ki̅rPQrU5-]ROv. z7ů AA r$I4+~QG1cJOӴkd[Bqz(AA6?%3VCk׭IX"=~%IA|>(;  K6^/ڊh gԴޯ$|^ZsZ C-۞%;24es|^?ل0:ME,sIiɧM2c2m,A&(n PO  AH$gYŊ] @|h T d\SӼ5NA+=K i-N/VrRQ;KQ4EQ.I'ꙅf@\&%Jr#ג AA BN)˩" *4_@|ſa07C[0 hp]PbCpBC9CuqFuT$ɢ \ҟQ-&tsϫA HbQT.)%r(UD.4 e랓2[Uw/ݾۅx.귟̎Lwq뛫Dȩ~"97X O'FOkMߵҥTsSKFq3n/>#S~I.-@)<;=[ƆF>MuutkOy/f0o 8 54L(پXx}`K@ /X0ڷjl626jZ,my/L8./OhFF..]cP^D=Gvl_Þf$/tQlFjTť+hJcbf8USLFM% cJɕ>ĂY+|$IVT5Ozx$$ ],f 5%]  ƴX̬h4fd2Fh0 m2Z8=( o`$JB^"/JMuEP$.4׻U}*1Y=vyU]zMY&F zZŪ|<4P4Rgqӥa).2qe^_aOsqS̖e}^(N֖fhݶtod$y4bIb@kB.IL,L~@!]]*-jZFi$ N$?2}of[H3 v\UGztOIvl+-Fz$WBv]($UPzS2Lo> T,s\içD'"qOmmD)4Jt$xHP2j`h57>'wWXqw<%cs@LX ֬DQ܃)Ϗ$IDըێ<"WιO_h"T!M\޳h+ŹI#eISsYJg˅ B]ۖX0"ԡXKE|A9ŻxhH9l QIhG񇈥G9'Fc:AJm1dj.rcCFEZ Y/TU&)rNOLˉsy1r%iK q}-#XKXPvA^V<AQ&ih5먾d%(VDmk2ZcC>=gqw7k<l>| A IDATy8S %Ѫwy.O1K¬/Sxzh BhTx4K|'av9q~94@iE=9ٲbx?L_559|[7GtvO9 `]ڍ@ǯW?6,yoߔ>S53Os:I$IO55m:EAJ<4_sxecs'AP$ ĊL*M1KnA VjuVD_bqt0XL]n#-YKIFP2[sG}%;,IQP(+LAH)8ܒܕzXt@44LOբҨOӐZ ! T.GSӮ"CatA/+CyknAQ$B:.dҙo73[QS-GP(SW㉜(r=YaR(R eIy\! ?b>eC+~(ֈD%L2]hq: fCaX BD$1xChxHQo=؊xq Y%h ,+BjQA&If5_QP =|gfI4ZKv_>Z_;z~8A(aR-CMܽϰB^u͞+8A8<5b wocTxҞ ᮁ;lN4^ZW)W!zyEyImWGmM 3m<55k@m%o,_W׽oOkťKY0 0%ƄUD|6$y]|r7T.K&zrDO@BLl@aG~:7  jh[ vXF>M~yr\2#7Mh1w* Yߍ;M$^xG%#eu2zЛd(1/D|OjDA$G/a!+ʨK;+AAAyA4MM_o\ 0FzI4M$)jqSg8ɲb8FhaIҕd)xġ0PA}{xhAA5ժְ'c `'/aA$A3̸꠱箩}we?W41 IAl p%֤aM񴧂(AA6u.Ǡ"OsS_ p#)eYN{yeUEUUhA tZeIM AAͅ00tF5>8lu9Kh%^?Uk_ )YF+j`8A4E3 ˲ CYP  fDY2y:cqhϋz;&9{I$AREQ8jCA  %QEb#r`a EP AAfa\r>}GA<蒒c'Am(BAA6  9QX\   Ԋ   O9D!  Z σz0%jaSF9Q Aٌh0N+$Vc>ob_I| iMd[Q l:s3t"iu9 cd,3,{J$ |yy^\X&Qrc("B^Y?P  ̧ʆ-k֠tl+AMuutky3ICbB]M4L(پXx}`K@ /X0ڷjl626jZ,݄q<ϿTť+Й Vj%X+Asa˯?zخ{rBa ȆhMqh 8{VR?E%Y"m|~a\9{!7}y&I5I `$$f~U% f[57fl6L&h4 `M&\k4gǕAlE1aaa|Qu[)bժzk>ژ}RQ?:,-qodn,_$E A4C$)$I7]/4e}]=3da-5;v7zp7MȊe(tvOfm-n  f}71;5#2[ js|$ʪئAG ݸ|8I;o֨=m{w|3wFBa$͖#'^{%IZUmܩ5rohhޱ7^v[ew[*~7 R-SLOLDA06[J O|~'uwNVڿi >vpa-P $≮^$za3ff#$E9=?up 86<*{@JvǮr{o!Bqal Y\[s^fzbJ|~ w͓vWywϽA.s]Njbt'>f[H3 v\UGztOIv4Xr^ Pw I%c OPeYahŬ2I YKfI#b{TfB#}{/40,nѬ73=:nu~Kc~=}ÃwثiST?G3Lq#AֆWEQ|,Οf=| zr|RU.;pAXrϔV{$DB::{nmX;hȉWҩ3tzݾMFA}yG2 =1:٭hDJ[1 ~o=_WntOUXҙ ۚ:w}a{2̩OPUm{w :-xg.:[QSܶ#~ٗoCTs_s\s[Jv~R55lkʤ_3 ?#pn_$ ði*n>xO?:Aͫ)j.kzbmN;NO.(}pc/]q1wf6 ò$]:s^Ѽ[g0;rPo9gw9-6kq!3~?(z8P;!2$IRER(rܬj3$79#4W nG2MT6֮/I3̱7^-6x?A $eҙ{P>T4Օ`Y?Yrlū۷|/{U{^GO#) nG?|b|$R]W+?`9g޽e۔Hf{lֺnJ||(o8Luex<WT;=@P9|Ys[˕ҩF0Uپ7aWGF DQlnk/VN;J /{?IGL.u8zX~p9>)GGH^߀. &| _j[.gūo6Z dU9=#(hj)QQUeno.kjPiACʸ>E(EpΕð9OmWj#EQC}nWaUH1r9V0qS^qz8RN\ <:>2x^X[|hC(lL34(c\.g0 <bVRX\]_*1k.a?keC(?! /dNu]SÞ(~S}ϋ<ȉWX@ǯW?.]9osᛜ$)J/DQ$tE};_\%™ϿbU=;z=IW_.nH$E8J.~RT8 鮎=>RrT2uKϝO}!]Qf ok(eKxh%+(+( C8M@A$d(r,L%*BQTa|Ã;^߱uNe#ЅSg B6xD))S(3MD&G\JW,<P_(pEq8<˽SӮ"pDyY-]oJ)P4DNf2tIsᛙ?(SHC=yˍ- O'WMgپo_H4Hʳ*uzJN/  09>QA6}frZ)ɤӡ@p| lrvS9]0 EQ-%ScãTzzbj|dla|x~`<>9|ElNhqZ</ ̾=VDQz^qDNE9_0I)ۀX|l< [(FL币>Vo>NVǢށEǹ\mcw  a)/MѴl"Hrj|vA~kȥ5$(n8\N >F-"m-ϜrByu(A{(jn}A6=6ttTiuګ.0Ts^71:kt\A'T;N3O?HlqV z7oݳSP _fmMC#g8հh8\ZQn*1vS.\Rq;ݬ-ͅ IV=JeҞP~LOTEMU&{3J;ܮ}GܼҾPU]=};ݱryE _>rł-0W_h~\YuՏ- uU5-aSy痧'6ϳM?5銢蟚-۲8p9.Gc|IGcyE)Q*z=PUuM m;vdwowݽ% ZPuJIڿ7 $]:}~no* 3GN|: t( IDATklzwn޾9Zeg8a;L( *ˏxn8A(aRm4 ޹˪ؚZ9C$'o߼~ N5U_>|otg_Jhu_:ן~Q&FFc8^VYsڃy#*ew[mrVKRr#N[wutv\!l~͓woݹt<.ug· 7O44-~ *( gW^<55k@m~7u#^w{.}SOf//&5yږ8.eYksJ9pkD.MNjK)Odv@Vm& P6`bOt7DA7^}3oyo7ϗ.Ξ֋g8gexړy>cP0 ?t#[Zad sj,n\~l`{ZyBo\&;60TTaUMuwWa!K|7ů AdÐ#EA`A^/~'5wۻ&7O6$@TD{\WUX ZWEZ'JںJ]U*?QPA%(2F< r*$].1OUsB)c rOWI9P5ʭ% ssgOT2&s2Јb\ S3?RU)z}%l)'JryJuʹ"5KT(˙U%* 1RJU[k|)@E4S*6Sg(x_H$HR\HTuΰOe؏MTu+RT"WET(9"u/LWcUJzrY%s~[/=9W+bT&355137hyD>4EUD~Ζ榦&OQDPDŢe^n^ܭ;jڼ+:+'=9UinfQYbLfbb*Uힴּ@Ӊr"uR\PTf&&&2N@532=o{+)TUW{/[ "rL1E1D" 3=Q4Q>l ]uH$K$r^DbHʐd2D"AC 3SMd2^߭zZG=ԻET4l` KLOw.\0 (@QE(@ (PD" @QE(@ (PD" @QE(@ (PD" @QE(@ (+tE9KfNF~M(DQAa CP!Lxx˗㭬իT*Iewg?V E@ŀ" ϟ/\"e2^/,,2eʂ Wy7 Kmݺu]vr Z*??_Qffŋ333xkf̘|qnpܸqwFH,gg甔fuƍcZsLL ?>z^uFQڻw/?2WϞ=r6\W0`@AAXnnnaa!?Z6jZBu҅-8ԩSu:*H,~… cƌ1 lୂ" "h4aaa*ԩSx>}5kƍQPPŋwĉtox=G~\.;vlJnĈh9m_lٲPlҤIGG@պ?xt>>>iƌffffff,:|7"Lq2x8{`]~}NN9߿墠@PoӧGGGG.bB!'Lo.7h9{D"D"aV߉h֬YwG'LoRYIIIU7nLD zGaÆ!Cp#9D䀧M2eަ"]H~'BBBڷo߻woN_qv8N[n]v:mڴd&,zfիwq)&uVOON:_"?hѢѣG3o@˗QFΝtqq:t(s{6}yuҥ.\`r222Ν۽{>},_7~Ν;=իsfgg/^wǏ7oZfKV/\{crN:ucG}]vܱcoӊ+Uooof_vv… {>š " e;6+11?www1bѣGSRR}2_Ǐ6lۘ1c._={Ǽ}t¾R!((o߾cƌy&EFF={twwgNO= NO2[nƍ2e b=ƍ˗/?>EEE 2dϞ=LΙ3gkΜ9SVѐ!C:DDy.]<<<6m$xSa$3g 6>4h*ܹ3a77={^7"YDD]vիW^ȍх !Ԛ@d!Ɋn@G6̗ ( +;;ҥK]v\rӦMy#k֬ ;wM V'/ƌӣG cǎ͚5kΝ-[̙s֭e˖-\g~~~ϥz~„ dӦM[l)P}LLw}qs1v4/nWAAƍ}}}׮]byRٳK.㏷oީS?ݻC.]XjUE^Ǐܹsƌ[lVSQjj}֮]3qD^/;mxL"J׮]jժǏYk׮f͚͙3gҥ龾5ھ}wPP[u,44陑1vFmݺuѢE!!!ӀaLJJ;w~cǎ^z-^K)>sL++~a֬Yɓ'{qܹDn}V~tm9ILȮӠۏk -ux>(ӏȢ.DD235PDDYmj>| d?$@fQdR3{FPI55`0:s`ܹ3m޼9;;^8p߿cǎDqitEEE?ӺuڴiCD'NxÇ}||iȑǔjԨQf|'Ǐ7ѤI6lHDڵj CΞ=\w1+++"Z`g}sΉDe˖ƍgdd,__si"otٴiSϕ؋/޽ٰŋ:ibF(񼸛ϟΝ;MMMׯQI7o޺uk" zucGzTfp8q=bL/祌מ={ Œrttܻw (//oYY+WfM777D2o޼7n0]z~Μ9:t "}) Aرc%6\:nݺu&=z9su!{{ӧcJJʞ={t՛4i988L>}֬Yޥ\Nзo_kkkGGdž 2 eľL/Z6))iĉ 4hРsY@M:U, rLFr^L/ϵ**Sd(?q7#<^$۞tkdۓ>XO~'2`1rs궗b(L3m;E@Ҹqもƍ3#&$&&lْY511iРJj'OFt:SHQzx}|U%"sssL,qqqvvv׵&MH$f͚r&tڕY%"^ήT*6Xrenϋy޽zꙚ2Fb^jȐ!K,tqqqqqi۶-`j7޽{#ƞ!ۯaÆi-Zx"5{{z}aΝ;;;;wܙ,UVBʕ bJgϞsN\\\xx=?#..Ej-6nXƉ]¬ ¬O.cͭaÆmڴ֭)݋ov\>dȐ ڵsww]]-yjQVTӝRQ>%!;لT')xckC8YD#)jQDDa3('%k2 n⾺()###""BsN:-^$xوkT*:tƆF~u.i٢^ِbqQQD"&_U*U5>61KGX,mI :NMEѶmۿ+<<|ԩ+V`ӌ f*,,wiO mwx j" .X"00pΝ "Xם/;m4رcJenZnݢEӧO8^w %XJշo߅ ⥟\۷_r%,,_~Yn7oO*ۼg{oy] X۷okZ{{{{{ׯNNNjllw4TY^VIMM}İ?2؃j 6|GgΜ^dh k׮q[ /`"##_:W*888""G_Wɓ'Bӆ+<<<99yÆ #GdJרQ5 [Mx`"##ٜǏϛ7^vp;?s۶mL~{{K?RSS׬Ycggg1~&jժ{o6|pf_eL6m469 &"@Q5gVC"":DDDD/$"*,8(S IyiDD'Rpǧ 5ř"AP1:ucǎDΞ=F ʕ+/_'˙/7666nݺ{.7cH#=z+%KDGG7jԈ|Rܶm[bbbXXXm۶ ,~ŋvZF޽{ &i&flGG:L>ڵk_Jhoo2WFEE}W'!.](J??ؐ~ecc_p!%%el|ʕ۷o9rVZ;lB)jǞ?@+hlFÇ'$$l۶-,,;D^+V\|{D81sZ[[ = N.ss .JR8~P X"ڱc;:ˡ=b+Wd/P|u23A)HGbNb1*xBKe n$bPn24T?5tz F~:값jt{Z|IڐTj'( (==ڵk}k׮ݼys}ӦM޽ҥKǎh~wiӦzyy 6CםΝ;''''D4z1clذ'--m}J$/^5hР3fxyy3^H$ZnR8q9s7oLT*|||6nܸ`R˗7lpٳg϶_t) ll2I۵kK(L&۴i30>|88C quu;wn~ݙ_-}}}'LzjؼyGg͚k׮s駟F_~W˫]v9rsVXakkKDd'N7nd : 66mx{{><44t͚5Zŧ8x`@@uPPPddѣ}||cҸqc__]vyxx޽yL&ׯٳOs6BQ=9_Pr.=MKȕ$SQ1$1k,)kPx<銻>'z|}}^:^W%=: z}R@)233---򗕕խ[#G6o+{RWwJNNR|-?j7oNzP(>ӧ3 ՒFochwM0oVq}V*/o *^rb?[ ͙3gΜ9QPn@ @ (PD" @QE(@ (PD" @QE(@ (PD" @QE(@ (PD" @7_DEte\nSԒlSo MbL^WyȕqN'|ť cq߰Z˥iWgސr.:f97> /zM˕u'Uqy<&"&ώ>?W<FH7GfQ0a^ eVDo̲fŷr&Veټ FIDATg1?*"J1Z^b'άKQT1ԲyDgۖ0:PymKVG S6Ʊ2M{=WfYMrtqnբlZͬLrOȺ~r49ZHKLj4bVmA_t{y"7w&N!6^x]a]x#/H"3ݢFu9Vj:J֝tc k쾼J"f$&'LDEu]T+DDTS1ۘHQ^VQ;S喵Lkgi&L듫c7DDu'Kz7?M}Լ/Pcey-XwD"R^yˮW9_ID%EDS?J>)X6zUgMEf 6k fe'WmiiUPP+SADz%^>y/PdMz c({fo>\yEThfV8Ex ERyw&"\bUY6F$7m EDTsĦĂTjQGVG?礟ۜ~nXfrrԬOUWo~jElRr}fUf S EOaʱ""cǜԱaDDP~:LH™Lܠ+`UmE5'6`zxb/>4H$=+f^ƍ6P%"?UCi׺һO?ܜ#.K&" +"pTrGpLk6Ć<>:\`ȹuVIQ!⎬g>H,Wʭjk3tz$l&Ħ!`;E 2$gy0_}Y=7"*,cA_')ǗZ`jۂLl>z(?ADTgtܪ6 sҟ5ߛˢIj ;/^TQ#7/;*Īv "2Ռx-q@cRTUP zݽA91gs\+MDJvbRO|js {&f:}ѓfs(?'7>Vӫƙ^Eb%}IK}Y*ӫ> LDuPDeԓ}Q!6ͲҍI>7XgTDii(*6bJ}Or\˺~/SoO?CDD*Dr+GOt>g:edjH,Uf̲3e㿷,?%IľDi. IU'\/O?iYΏWߢGlpN߹$i;xEI3#q6C|lEU5qO1H,)N6FwUDdV׵Nthv^bd5Yj=ՃeFM5 3W>w=g]qٍ ^٩0+%)௘H*Qjn|.{|{xD)vY4%(-͜> b Pkr= IV&oJL,}u3Mm[JEUǪ|Ϳj:O?E2 R~6G?LߨIL̕uu'+ec ; ""(NW#r~fu]%JKYk8rt"jYeRXR9vtŲneA fjXf*5r`ﵣր4SMSڵ+T zVC}uraTȷn0Cqύ +(E(@ (PD" @QE(@ (PD" @QE(@ (PD" @QH~mbee (PD" @QEoxsw^ 3 6"w~mDh"{Ihh>?O啿ѣGݻE"?$ᅅ5bn"4h ZOۿ:/((HEGGzf7߾}xFW_EEEs1?Ξ=,EGG?fEՍ'k֬nݺtw.X ;; .dZÙNӟ.={aÆƲ:x  +=L管IENDB`slidge/docs/source/user/gajim.png000066400000000000000000024023411477703150600173500ustar00rootroot00000000000000PNG  IHDR R miCCPICC ProfileHWXS[H #RH ^! $,*vŊ(f+be]ņʛͽΜO3+Z 1i S@ Ȁ$hewyw Ug?2832^>xiDrJD@+BZxg*6ėPrl4A= y4>C*h:Ar+bwϟA{ 03oC\nV5 j!"$;,<8BiD"X[ q83&VQk?ʺRd=j̓a>Į|nHbU,Qb[ЩN/BU6[T,)ҟJ*|=&To(&BLتP.(ͨ"!;fF*OPoq@ a *|` I`x܁a.e<#̅/ U=U<$ ʵ8E-y DZ<nN%?%)KRƉp#A4`r82$Dmur& pdpViẄ5? кY(/CZd O!Q yKOF\8x0<8^?aAMJ#$C0=n~x43q<  E?D9t@0U-2n9=`Cf\7θgOeVT2iȮd<DqGkPC3?gW}>Gh-agy(VXVbxhw=]Ʌ<|J\]\?+ S =I2M*0X `p<'wm;o;MG"8 7mu5BW\_B4C.v07@ $40F/\ `.(`9Xփ`'p\]{:Kށ>AH #b8"n @Bh$IC2lDȑ|YG"U9GڑC y|B1& :e,4 MBǣd].EJtZ@/%ڋL1gX,¤,+*>X֍}ĉ8gpG8—]x-~ ?{H%pcل)bBa04~ީKԵmKћWwLCӷ/?C0aaa 2p A> + FFFS66;o8ox ӍHL֙467 21]mzܴˌn`&2[md`1SscsV6> [dy,[R,Y-[,{̬F[ͰcMfZ Z~ockjЦ湭-Ƕȶ.n]5{=>~eAPpur9ntlw"88*n:SY΅]]]Թa5"}ĊgG|utszwȑF6|spNss^C'sB/^^R.o+ 789Ol>}| |(QQG=o`dl 4V> zƲg^KggC𐒐P,²êz=ç7G""VDpx*NOwSQԨĨQэёWc#Ul&'W?M0#l"=qbwII˒&%˓[R4SƥTO I]1fĘc.I);{dž]3sq7ێ: yMԜȝx(;37[dnyky/A.`Yʬ٫2a-Z/z9}nlԼ}j bq$IS'K%ŒɾLFIwxY}.o?, ((0%eʡSS[9L[=ܹ)i3gUݞ bΗ}hݫ_ gLOk7K_-q{_î̏g?~z73s/_p܁O 4+ 7;@}eDٿ 🰲_/j{|7 WPwY ƒg#/`wl+{Paϰ%3?Q("?ӐȥteXIfMM*>F(iNxԠ ASCIIScreenshot 1292 1236 Screenshot ./2iDOT(@IDATxٯ_Gw.EDZ(֢THRupc讲0?̓5)Ocz%jE-$j!)Q(SD=ݍD'~yi@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J %H R)@J/6cf&>J%eNO=__%U㓗S̊n>yx^6'o7_t]Lພ6 1O\-dY\u=9mc\j '2%,9ip'W9Yv)!e|p]yN;Z.ɲL ,Kzs4\Ǹ.?rANeJeY:\sӦ:u9)N r,SB.ພ6 1Owr\egrYiq]~ʿ傜,;˔˲t>EZ"li?rb LzEN'ןZOS'@?[F+D7(u[QH# MӎSx 6,ɿSˊx?/_ڟ[Vt{**z,,sJRw7w,@<߹w^ܐ!F 9]dCQV4N;7)x!xF.7 ( ӣ|s1-6;Ll%9bS99Plm(n-yK\r#x !vgv8c@s!e NMiꁸQ2`!Vd.N/퀳2Q>q4@da59rWn>[#N+9?8Oο?iswnOo챼O~0_4r9z~+_ kuy /Qo sf2ZɬV2+gq~]ccMu -B#-Q4ϿSuQZu8M|S#&Z }_l҆O/p/\yڗ/_ο\=.^wߴ@ߴ!5?nCĝX{z}b=r2-!yYY̓:oRF%2ƌY?/_ڟxֿ\rGrO֘G2!d&PJ?|SH!o{`e^~/%ғoz$SR :a{aO\rGFonx o Zns{]w>W!̋x_NSvMV0ABP%K<6!t\%D<#Ir+GMk2 qu8USG)Կ9&oIO?L;\'{\c.Vh34W0_ 1-BΫc|v7(XB,zP[&Q(iԊMT B[lKy@k^3ZC#EYOYc +qz7ӄ@@_4O=Xs֧O?ë?Gn)(FRv֏?c?i̞"\szg?5(x\m=Mfl/hx5:NY>x]It56^hۊ`)#58|&纄&9g:(3y?yۦhr7 TsnxO)ԿΎ1.u+v40:Ϻpt=$r̳vaύ`dm?p<A]Aڴx]5F^5 jB<(BvZYc1`l { S8\.+p,*z}8w8M \.4V`*-M)Կi]ƞm'׿\r\OrF_0'xOHС =_Ӿ hڰvYzpw(Gqu'=cE o>|ju;Ph3-éɿx=S6su6ٖ[E=`ea[Q+7lKЏ\Tj[-:L[/r?t">Ɠ 8qo~|MŌ?Wbǀ0\r`z(Cȯ5A_EuNM?lB>B9r{k=aX>x5OO]w{M=ǚt Z @$ $d-HaMgM:OyR㺾Cju9qׯi*;2+#Nw'ÿYmt<|RsuF> H֏i~ל:n{N'Oڟ?H3kY\{'O=bS6%ߏο?iY%^r)ۯMvr3_moG htZ8u{kzХ4E`:\Ɉ;%yy$ς%Dpx\8,k<B(JH|?ɫCY@-D3Bʠ9h oLw~)ݘG, ¢㴧@Vt7~&q8`z 4HסE\ө-Cm͝ x7y]6.iaM]NxǟrxLڞdB?WB=gsI;OW?~K;?s2?ϰuۙ}w{*xA>yuh^!B @^u]#Hiec s5.:lryKr3}Ecp>] A3㒮Mh|$M<}xOpPPiՏß hO}d.aʿG_i5,o?v? :Gr1;i!dw1o-:n^b"xK?夽A`^C}(s ݇CUw;O0y8i 8`<R-5:n!x:t)'nzVۣ7$Vi 6viK^͟|qh&ßiO)ԿnNKc{i0oIc[ߑǰCs7nN]KLѩ?}D/cuMo^]ȫuik^ƭqq9!y]%kqmu\22y 0\/;g<++i288|\7W0Kӥ.'Ǹ OK <:\(ˇ7m5) ?}M4 u%nǐGoTο?6ֺb{KF?K?\GϏ\5X\ '} 1ˬo6z?e͓8o:w'0-j}-BZjF͟턆:M9i6=BЧS&\w\<'UqBN ׭#`Xmy3:\6wO=u}QQFh HjS.tjHXG_7ru8|gYAz/d=Sl=R6^_,wF?07Vt0n=q/!N]OzI]#nAg ;nzu=wYAm \geFq=`XyСn: .*]Y]r]u-<]WV@pKqh>e_$Q0/O\rGr7?_d|Bgxrf8uqL!a]t}]_z)+u\W1O% }5v;mNCǴɛvw-:< <Χ=CHCAFVȳ񶐨ocB g]:n8F5c~]8p֬Y?[Z+,]t Λ;w9s揌\9ky3f̠"/'f_PZfPy=  hI^j-HkF#OXѴiߘ<;?K{DAܮꎒs^ϟN\8SO:uO>|o޻w,8{v K>еCq{a%Y(ey\w֡ 2Fp6.H{" sM4 mNFͳ8h RK9|O>Ь9>u9tpsgϜiΞ=Sa8ũ29k4hP:4uD3B,.nn ZWpA=&/lQ/'+hǴm^ FRC4hWL?/_ڟ(hF+7'h7geG/#Bm2 LGHG{{g#rɅ̜57~xfM 7sfiΜ9֭[Oɱw=6\\H+)-%t}-%N>!̃zৡy):#25g)f4 $E 'm8B±M\N6 4Hi6@|y_uL3/yn_W̝s'6jN?ޜJT! vY%WBA|F^ThyE͌:::?M99'{i23y_wOym9͂ K6˗/onZBn}^T_ѵ9:^|(o1m31ԳA|i\\'U!|8y) 8`z8"mCYB 4H۹EH9!yfBfZh3]O<g;:}͑G'7hHS[ NJG֘gdeXA(R8D >ʈDGt)O)JH_ο?J{[ kX\M&D\b)iv}E.atk i'\snK`eQpϚ+do7ݤSkKn+/lyk˛7ojj']=_Qtϱ_ qOitqU4}B>#xpOĩcT]62Da:"Ν∿lu2bEA 689/BaFYȱd "L-#o0oMO7(U`D{YM 6/jV߹[o~w Sȯ||<_HC? 6k!mi3.¦GN-w.ߎ/@>q.Q׏aoa+`ܗ{l`s⇓y=Ix£ct : r#K"DR{[ҁr[48K$)?OI"_ڟ-k *ן\#d+_D Bwwtya@ύu=cDWӻՖ|sJ'{ݗ_}5N+];ua:p.S!8~Ҁ}."}< hqLnyE-kEVAǍ5M:Ѥq 2't7i|gڊcxPM~Wճ_bGglj/̶o\V]k†: 1W i_H:b,:m~]nv?/O\5_ W?sy7 }6K/mVug^6Bg>)LԸ[Z hqqqvC&/_ȴO;JO䚎;ӊFq:뎹K 4.!ec/lnr#|RM濾ȑ#9|͹窆,U_;R-/NCXVC8q*[BcrJɿr YʿhڅJ[:ۜԿń%x8D[(KÄIU6$ן2yr- Wvֳ]mmEY~1#+j(6%E5 k/͌aWWv5ke^%`o/?)OE?։Ǝ-n>y,k yRN}B}Fk[]~E0]a@?i2#t-Bʸ,TȷB.@9e5 ƸʎvG8xY褹ƍg~Yl%T)1rɋGCU *6جzq /Ǥa>ɿf+C%/mio?U`^,_Dr1Rrǖ_9+nmme֭IC^2qS2q4xL&eȫڱGYXkC)reʊ|/ |5SХaq%!rwiO9`:u4q+: LC?/yGvsGJ$ >LvIRiJASqJoFhaBpD[e.PZu)Y9?3^'_lc?ʚ/6Dr)1[iYF&񿕑ljur1C'/YҬXq]t/ٳ]x'c\.G!N9!~zyC1q ;~$ e-$yTQSW4 Z Χ.v=9]~'mY-h]:k*!{?~Ǟ?qx_닞dɂGPݕ&yZ,.A6r̊jlʶ+bZBj%_!9Z(47֗\41rn&bY*G;qfI+n/7 /hO?mLUa3O0~juh]?Ϧ6u>yC碠!E@h0 yEyUvxQ)2!ߴrE]u=?KOO;o˞AƖo| t> V]1))*e /1tRJY1sk K'[G 3~sٲe[V4'O4L6R"΅%(M2cgK=p\_ C(:*>xmԽhekРR|K-p\OшӮiXi;BirQo9{)m>dfLE]jn9}V!qD^qrnE8ע0 \ޫ?!_ڟ"Zѭ2Jo?rU ,D?% I!Ci*s#?;Tba޼;W7g>=Imv;-P[:ĿCݙ ʕ5 $ 4M68 8tI&V͘1;7/|? O6`vd'>V6:E>b qp /t\n\ntoꚯS],P0 Sȣo #[(n#tpQN=hY>FM!0c.CC'l}|⯙!tm>NF<6- F-y1pٍNů#tt?D܉Jc@`G9WPIk=,?KhoN33皓?m''U 9L [QMyɏI#3g+N}NMcj#'!'ѿ9sgyg5iv~?3oNnXZ]~p{k }?{lꫯ6gdb^#[iǟ{A֯w{^iΜֲ8hW[ uof]c;^_y5퇶nOE{4%&Kshץ܄-Zx {6Gc_>gя|3cfgh7K6w~F;߿6z-ܹsy4#=_||+y)/Sg[fْe͇|Js V;Hpd^Sr;|M4!0kexH kB0RYQf'a` 85-h~yk:B @pr|B g^XjSo>JMX8J*Bs.(3 ī2s'…75(h4X:'ǎˁ%4[t#eTC多ܴh~^, $ R y;vT 9Or˴iP5FK~ܤ6J~3t]ףM< T;ϟ?'S1yjeezLj_k߰A@[[,ٶm7"q(P'ۛ`_}Ɏr{>)ܗ_yy9lc^\ ןaZcz]+.[YV9k(}_ua(;E|EC=yO'4 KnܲbC_}U8e=N%U,U!.pʊg͒ŋj`Qp2)>r9rpоXg aF)ܥRr8gQi! ]9|$݀r0{R}w% WO7NaiJ-3ڔ:z9rHp?Q%_xN'':^FE̿:rKG+ Vn_tӢfc38l{z_y'ıp=ͦͷl=ZLis3r$ן_,}FٶZHW=#z7h3dCY@IDAT_-MH5֑=?qYtohn]qk]h߾9-'FtOݧml{ C>5tih?ñ}`GX:ܦ#fIZCb2iP Jb9&VռH޸M_<Mt1U7%nO8p^} |.|57^rR:L|xRfIY\,Qpc@.AQp$uCxMuL8=sfܴhZ\^ʦ_&"F%nV%JEt#Vu-*`A  A]^_ٶb-ennݼLP]_KJ9ZϗP$T~!"t#Ž^ȯ4,+QVkp/8D8cƥ?_{Эcl(ԭֿs:QF}9nZ4|T;hV=2'Nh E`1y$p9#v󛧞 5C7<]6>Nɬ(m-$&ԓ,7Ǚ3:5RE \ ukQ;ݦ_TOզZuk]& 4,j?rs|mFRժ to'=nJ/oz8@_}~PC7=ܶRvf| Lv_0YU݇?eɎ–ո;[o[NN־d:{_8tjo\3o^zͱv'm1\bǏ|㐏_ O@9y'-B nncBbGyiyCesézrq V4 vH1=4qA %g~9{9үW8؈(8[1?Vr`mvcRv4ۯO)~&h}=:9_˗#i,mq#z8I C FD28lNyUh4aP¦LdS~;tP#O?4ZP>F"ȬJ)^ϣX|'w`=[jEWbO`le=;G>Pco#9呢qE\i09"]y QtοEoz\ҥp%{+߰aCR"mP j/#<cv?]~3w)ף^5x%վAfB n7Ĥ}66r(`|B|.<-u#@>`ti(I8㱬FUiEo%4h@ <ʸCӄu\XPp_|>-HhM3n,!jZV::uJG4?T MhU$,m]~uEȸ D&nj obxBny'H[8P>9P+{*֖7GZ77<_;jl@-Z,):} !wsGoiNmJ <[̏n|lmpz;˚]EjҜD=Bkx9A`(>|x7/̟L9ޡ ~|eEQFGΡWTuO{+_g^i 5JKi^Yr~cOS7O6]_#'>-_h^*P}>\!v>kL~H 8tB&Ƀ  jz, \1W'--~PCmN=E)—VmF#[Յҗg oѩbl8m:p>N`AKΏG[&xrEg}T+k@8Mы"̙K-/" ^`3fΌv.i>ʃ\5 w'Mwi!9>Ytq8xU2ɾP-;Nh"c]KD^?k޻Gg|lffa2hܵgJy2Gd?m3%;P=sm> 0Ο?~lx[2_HV'$Ÿ\ϒܼf-v;E{wgVk?5g0$/]6LJԔh"́YRl4c!8}piҥb͸{f0`g;Pu?iˢ^[t *}bޡOa͟:BPߗC|+ ϗGaD(Ik Oj۬pϿ-omiF֡EǦ'7 /ԺQ7m7֭[{O7/ 7RduA_A\ݬ].nG֞=b] 7/m1/$wS3ΐ`yW\an+XwaE46L7 TڛCbn?ܾ=.8(yQѣOx&|-<@wnyQ9,*hf䱡?MΠ?D7jV--rN9C!hx{pI Bə@kp8>1ޡ&?cK5ւUw4k8hMSIÏ,n܇7"Qs^f, ?N>ў,qKrwլ[V ^. Tn?7۷d[ǁs w6_\pk=.Gvss絲zo1[n 'ҵXJJm;c9濴ys+nӁzjӁs^rgF[R:ű7iWi#9[Rx!2[[j֬Y?REF̵K|4x##!Զ_l\C㦍Gŗ^jΩ??LS:~Ϛ-IE^EHݱ)r8G5キ-~,J֯׬.Γv997h8[.&~UPYyօprJKHW8*e#zΦyEo Vn[ooiܵF?)v\f&8۶_D6r ?3|5xj]ᏓOvH~28xuULޗġFq`!zgzM pPw uHPGڝ5Oʹ) 4aN.e/>1p=U mu6cnp\`Whl.e~fMK`bE:}560SgՖxP)j%:?| B=E_P|divKQqɯ_J'ۛ_2,^91oμ 2i?C Fusm]W$8A@QyQ}f/JתON֛['PP$$φ^y )'6/<_NT߆ glw˩3^xAN=~"n ΟU%<]Ï>ҍW'/7Seo~E\"j1e`U[eɂ7%Bnp=*G+C+abACdĜY:y}-u0_OnZ8Lߟ;erps@aE?C?DP8ӲoFy|}O?^,# Îu#i$_GTtin!'O졇*_x <̣8a'Xb|RnVyQNǿ:'ٿP[Ҿs%>֣v._vKs) Ϳ9s˩?mߵkEwÉg=.PVW?_9CTʟB 5.ȡVyܶoPs[CONb!hjGdGp/0cF*Bᇄ[ buC'tr戜cI(}C~9>;ؗ`U[Tm8OyNE?(= Ԙ8eNo_ߖ[N?<RB_=^;wGՖ.Tob, G>[7*TDkB`5sD6Nؿ͛>8߱jUs}ҽ1woyGǟaԉޕOi9-x~h:RoC8ѿr/:"aw?J3q;)IAYr% qWow],Ԅx2sර:l6&xX! mFu)7Iաs+ 4 |,ԩ}ۆο_:6~]`CU 'G)r\Q@n[AUEkHJ"W;:nD3MbPѭ<4.R-Ak 7Pij?2=N<H)+JF!MTfɏϰOğřS#D! @Qݐ托_2L$m??<9:xDg]Gopd~e74_>8JRmW).9%c7ݹ*>8u/eN/ԕt?m '82jSNh=>SC"U4O69QBQDޚ~> p5_s@yWzrFᠤ2zw9YK.=9{lR?a!SO0YG>񉾦(8tzAR<ȳr޵$|@qu?SM=O_4'. H8,ICMzqsr\j޶Oڵۢ /gk-M'x O1O x !!i>= \qkg.S'EOe"7q+3?mqĻpi=Z{"N}AfN Ꭸȅ\4X4ޜEOr:|SU#9MU+/y5?+8+8wŸrR,u7ݦ1zWGwk2d:A]8֪Ɏ?KoJ̋C@5?Ы{/Sʬ+̵bwrQevieNlݱR;iy.<;.iVt@:niQ.2*K4 :\ei5>ίġez䱊.#5 -n9$xYUPH-p0e("qk3Y1{PraN .%-4@B?j+3s8 C킮w-ԉ1,ю<:_Ŋ=qC06=4EfOG9"hNȁo_nM˟vvH' qs2!)ousbh/'S}P8Ʌ2)"ek3o|5Foۮt|':zɠ'6sEN !L 'hЫ8լ{}[FhQ9fȑ{-]OvE.ps'cͿezd1nG/nߥCp$Crnu+Moٞ"^Dԍ|˽z}L]%I9o߾xu " Ԯxo9oI3:8NGZlߦ00h5kˍT[M4|-㸻W;㽺\h吊/{/Zz&k^{_U ?N8ZpJz!Z=w&NPƑ; nC*_vMnݺhNhN~~04Z"&9yt`t)·aT^MX\z@/qDY}gY6юr̄#_JO㷴ӊ䨌)J$a^{( fΎLU *]jņ4~d<L5^yIJ8ԸhjjkNڥŸjCp j5 ?Npށ^|7'eMI4n]_C>DAgծ?@SJOc WLک\߽X?xHvmQ4G8MAg7:8/I/yok̿zAp5[t #rgyrL7Pt -'*pNTxaT)C4!^2Ho(gbQNGBrhh'cb,*9~ _5w͜Disq ?zB7z#9ݪ7 c|_svEsAּӁSp{!;$eF N.YLHȧN?2~O0p 3zݣqQ'~|yNB𕫾:>`wNYP𧿜H|AG :i:gvv~zV׏ p hǿHdʑ)w֖pAWV->PyM9a* '_|eݻ?;(|ŏ2T3c*?6t {GG`Dxc"cV8^N2ltZKTمӓ5|)UN5ZcH9,#NTޝ,>0B?Gq!DsμKH%#aJYf眙sg XgFfӍY,ƬFA`!Es߬W 7KD}3322"3oqs "ש ?%>M}Pb0DuQR؜>/ z?.ِx[z8 7b)6zOǕ|%!q!OO;2o䯥TT~;՛)?>&=pgwI K)(EDܱjήuRivó˷P?W,I{A>Wb ~<&x_AFӚn՚A:0o!^=nQ;N _G19LD4H#ј24?da1@c@p9~jWlZ_61!(~itVxQ'qʩxOMMAYWĖDB[CDoVl޸1ݑl:俦8{I}yC+*UۨGv:q|ڿ?>C_xENO|&{ù^B\9_)MݎQ^.LsAh+ĦMbE?J#,`wjT`ժ[dC8D:nbn('}nv_~+kGCKzO(l{Eq_Vp8ez7JA|9OK ;C U8^%H~'bda~qd[qI?' $ŖU,KV~-O"5YmRp>(Zu?O\Jж,tN?RPrG"+cZ{TdGfZ|M+[3/Z)9li:#GV~;lfqw)?ЊH-s[N,HT7k>'=U!R!Y5 5SZ혙Ne|U–!U[~>3r%ZuHXZ}l]N{rVV$RB5RSU+aw>~lնX |zJKίDf׫~]!ŵlE^ʤ >(n߰^7sj+⏼m:Sw|V}p!=OSr~>6m_(C0 _B[ڊ맹'R x.IvŠ6}/_Hqۗ("fE(T׹Bj*->"3ZjPSx0le gX[tvK[>dt\9.ĕ} 6>|VDR;Ή s壄կvT[>hI]Jj$;ny[ZY<j/.9_,s8o/\.pV|Lc:k 7F~;D۶iouVXA*'k(agϳj?"\7zYgL LRkX{M-/m[mʊ\@!=^z{k6oӡyQ &&ױD>+PY~ZŤĜycxJ7rT;|ڵkQϴB9ڗ<[WB)6bvZMKP2IXe yT;67m8i(ēNIn26埋nxCQE~?lPb>:aF=8u;_=RH#K$7Q/v[_~791,?WrJxr?an }+!ΖkWWbd|ojpݻ_ j  UxppVAKs4?x:~ij~[Dx?ɺ48"KS&E/Ă֑'؉ia[>y:& ;శ DžO=hse/N2kIј;<&It'RQg,a 1!e|N&_aE$F>ku*M?|knZĊ-G4wXDd\="ǍIYIZO;+ZnO/jy;0ed4?"/d7֖O&N.a{Q_(lOmפaHd|(UncsnA+kTةu>_=@Π2?iHX-\A/UG9#Zgj*<,:K_M !OQ .XyhKysk8O[N@`TC]?;+cRO=d<)lr/ΐ9||kw܆u)"O|8q@:iFsҬ mrf8E E *W$[&Ӗ&GqI֯t;o[O{Vӈi3;ѯ]{[?[>s$+#%ZgB'sjs"*O>g(]78T~Z増O21;2H?~Yi˧ɤUGOJ":P[>MU+xǝJ-&Maхl /mH*e{ O,wt|]o:Υk?9WOא(:T&,Pۀ??Onl淯ۘl fOI'?L*^5sOEZŦ+B8qVJJ2~aՊ;݂*jڪ>\GRf~Pi|G8G|22WB MR8e"F?eF>C-XT$Ȕx܍>|veuUn:}%B]ʞlg2gvG)e4(M"O%Th'xӹW\~Yp5P{!j.ʛ?+@ȹRJ¸EEM7.escNd &_†>J8CϏIO_E^_&E[H cכQn >O[_-g؏ҟx܍kOH*F?PXbʩ.%//0+n30QQ ֨ vy :xb:>;0 3<1h:<^8J,Gap6 &0o;&\<#W `˶m۴[\<:D8yҞ](L`sem$<[͢r<䄶;(5EUmb ɷX2M {1 ɳ.X|(6AJ#",Gle6 bE ÊȿKMRa#Ytwoj|4șTK;c +jrs ׯLmiq mR\[\c@L@IDAT=|X++&Vkz;P4ڊvտoJф 8P%87i[ݥZ%|.f1ZچP Ӕznc|o ֓--V+=&3jD83ؠ!2JZBh[n]6VSF O-uz\ t-O=+e}ݐPsl$;q>S;#HYI[>YunmUj%2t#Y%n^VKZ>^۵^ыN2U?K >R2<. e )V~xd )+ϭ(B(& Q1%.IbJa;RV.,=P/EL+)ߓ򩇹S Ru;-%Rԭ.A nRHޑ~\UypGgQq_غNN)-?}ڦ(8BĖMۤPPwOOE6xRhYU&O?"Y zE%D5A+ԤdD+Ԝ~w)EihbRZMe&]vyS$RҜN!'Q2r&Dk8V޹'g]V)oTN`)y}= pJa96[g}qTj+(Ce:V#_҇w5&SvrR6ADA, j)7_4PSr!?.k%hp K'Ppu;O{:iW9:kv׿UmN3ڶTCJ*HYaݠ!ެ<6+B V6Kq1rBm#V?7#\ CwV?!el>ͷ?TKqKܧؼ!{GJ{bLQYnYD" 5-Xב KnEw?ܳHS;Wg)B/- ]x璑x?BGDВ٠y\wNT?nVMk Im/, vp=#m&r<|^[#3q^.A' vHG{g n˳"M&l/?M%Ǝ[>7ē8svU.#VRgLΝW(Kvf0!1Ĥ]8UWscl qq@#A+Zŏ/P G`pd Wܬt;Vt#XUW}MC(KG\,|}jY~n!ƈ#Flgn*ɔdlRVlChJܜq}Q?Vra9CI+7rV ;>33q,#i5»DyeqTnӍL\(߿yᅘ(r7/a=-}cvlsRȾ}Ӛpڦ&$ɳ˥ 4Y@("BiŖ[XQ<fԿK/,|ڇM UnC*6Ѥ{6٠[@cR rJg,V/CqL[ߏb+'úaTif(֭kÃ,3<_j퍆QmIgeICJaeӏ|&k*a&LP9#}$To\U9+WO8ˏvݑ-@*:^o2@7_sՖ[: }ܹ*U=mȏw*%iY52Yh%WF8Hwƾ%RW %4mj::V^ 9=(nANoFf|Hi>gK<$v0~E+9W-|HN7yrpƼwHIk(b3}4 4Q澠C>IJ;>L3rp_ܜ2#]bj0c`5i2xCtDg;?L! 3&M"HdRr 3&?3h$AeJ`<#k#",e`IHRM!R"4 'Kx% $7ȭ9|1ϧ~'_/\?y( ի$kmcZuĄ2eK?x-ߘ8m=[tnJ_zq>YkZqZtcŸg?~P:F EM:j2,,dY~gRM~.jO+̐τUrX_ф\vwŗ+m|?&Íoh'.xP弨#zC#])N+8hPೂJ1,oEt>wZC! nٚ 5e!o->4;=HQѪ囕o]VGRg߽yY[]b"td*jePdz),y= ?c)sR-'Ʒk:.ꥵP'Eg\ Ïy߸=@ _E2+&k?D_/fNEmfp+f(O$wwbV}Vpo7Ja[x& /b@VV ]Rg-9vr/jۥ峕^~_-Gy&J_Qm|V.=ճo[hQOJ_J>G! 7?K>=))6\n3ug[!%iq%y>.?V+#pf~@>aq_"Z.s(yyƋ/Ҫ-1gu%zOG{M~.%hql%(䌧])iXV✻P"/Yek+)S?6۰-`gxj-qq}a8z&>RD[>/G p(W{Y~+XGeR>T8W*P:(QTF5S:Cҿ ClDÎa7!5p5 Y8x ?x fG3~)hz9-vL>#ñCos2lY<x8geb-+ͽ"2b' )Rc2!Ġx6rj򸾂>&J 5-I`tqj*c9AyJE}XYA|"vˏ7n͖7 s9:C_J> #xqCF3(Q Pl$TL-_j0-wpbK)Tm/ D;㜁PAB-ec gB- }Mg[%$_p2 VzM{:ۉj?ֹؚgk}{K)z#48硇 D@%[JlTſ_CrRQIcn[8%_mDvOIAw8Ca۫/\\>rp(#;a&˝Zɖp%RjjRx~&vV.`OcuG7Q ZT -wB쥗8A>yG1F%͸%W^kBHI٭|BXaI<~AT;u9U?x훬{ cjZ7UAHoK $Nn ʨ\AB3D| =|",1B%R# MM>v΁{^[-[UQ e|lgf[ҝmF-I+Gy(qV;W~ZU ?;d (vmp%3|~Ѵ!cח_*cC_;Rj;V&kKm/Ӧ7Ky!Zk9Z)}zq.%`~VN;[e}^P;LsYNԷgr\-Gt'[>#j 5)%_Չ!+r|IqZ]E?,_%_9^EP_#3o+^z^jxJ<:tHУvpc8vrhZOdo*D<ML9qΘ>0CoE ޼97p|ncVM$jC 6^i$j&#m[2JVIKPy7hU%˝(tl;CZ|[ SYtIFlEyӯ8:rHgKYJ~UȎU~O_ZQ #F}w#SFRɋ˜ZR!TI0@C馟w]x-|wV-~YI^'p̳ v,:{Z)FَzrRmcLnxu[Q+X BH E Ґ' ^: qeУ>x6gK9 lm=K_65s__RDioΑᶷuym:KJ Ln(L0qXo%|" ̳Ϩ\Vl"dP۱\R)H8o)3̿=@isMyzRq.+࣡lf&+0wM#xT۬^k;SD@RXH>Umܤ#C[k8DtZ7P9*ξejG)&J,+n*fff&nVsTEc˧NXuHLm\뉉Ɲ)%7| ,H_CCcZa7  ;.yNmܑ7BO/B}Ds$50'<- 486xwċK 6@wZ_$/XN@  > n:qOMB&(qKSX!W4L>rF>E"ҽ|"."1٥n„b ~c`Ȋ&!IOZy qKO鈚pShI_\-|8G/K.Ib_ʟS,`,+"8G'0maK?G5uWE`nžpjVA?7+rS A哕LK:7gnTc7wlNZC詼3I ZXʨ A_|M87J 7q}[~s:#+~6xH%(X}y/wY1J/n<^>̓=rdO %B/~fD>U kOE>p+JwX3$53qLW)閒Wؾ{^n~=UE],'稬[G1jZʊ( KPOvj&bxǫRpOB[a[Z)nN)ɧ΢eywL 9$:BA9 _$l?Yr(Ϸ~rf[:~spBNj 09@8N8v |P+Ō}c:Y۴B U8h"@_71qN>@*%att )1G㬜t7q&dLYIFquNq:!\&D~LΰJ nZ,Kϼgd0ww[%c#T*9qx"_J~?ӫRֶC~K^ c:)FO[nI+ 'ȋ/Tg'b%2K-=_n26yQُ.G,nV}{YuNʢL\Pfyohũ˄y ~?*+Q|O?Nɍ7|1 +r>mg6gqUkIQq^UUT]z]ڿ/X%md@艔jHo 1Kuc(Q w{ڞsg \t1cuǔ" ]Y V(n9.|yCZ?ei[wX8gNL^ K'.q,?vg(YrΡb [7>8ι?>OW>;_-XzUN]ɫD6q68OZТU]Ԡ3z <>sN>OVwGlgFf.^xvgZhϲ_m=k:sic*^73gEyuQڳc|_3\r)~e+V@Ҭ7E<[q{>Ic:yeyxXEe =#vS<08"d:Yx4yV0beMh&<8Cm>{u R=0JPiZs HFv˗в(V6~W+US{Pi\VZ:Zys>@TgBr^ '8~ժq=gݱժ*_˯:Jyc| T78|>J0d n_lfjY[d9 \Ds}<w?jUn5(PqнXfM*xvpxu6Y"iɃt9,rM##2_~ :x`e]\" a£r"q|bG;@OdbPZpv^dnA}-B́! 60!s-iȞIlM^ tRf2&KTd_90HSXGF8_*UiC?VSo?4B`X_5 n9bw7g|G?h)m\ UFg>4<{05pxbl5dt9 a@|wz> a1=n˴H{^h-04&8+ozӈ$8Lۼ(:8 3ݧK u/Q;Azm+h2H} 2Pل3RSv|ъ7O~8#eV2eeU&R? Q,%z"/VҠPo45V|2Ⱦ rbT&I+?.5Rupl";h"T8㯭[ {(%5zVp9lnFD4ӸЏZ6"Y9 >РR/t?nZDh`N8ܦߑ4g;dxq"1y(L3mDGBi{S\-hT(2\={㝎F6sMsY# ]Tgne_տjhAGƟj j=6 s^Ä\jWߚg;B]:B=~E{z*bӺ"yzZ{^-8hYI6pXz[I , IY"dJW06xA|HH?X&kA L,?kvhB m &)*?y z5As!=*$GX!# . fF%?r*T0Ts;IT/ƻP}QݩPUk)_5w9jQ[|ɶs+8}z!Tj=B7ݬ`QxY?v߁-~t\YCvh{>/ 89!aMDL’pc =XۡC/&` _JbQT $$,|lG )2"c 1NmɯWm@[/*GiOտj42f2AQ/C?k]l2ƜJs3 IͿԥ+Bmjd;<((Ci5ax#M'no;&~?q|cYip8_ pGpk  DIEĬ"ءa} vc'<Oobb <<?6ٶ߷Pcc~d& C#FMі]1b-DWlr{Lч»W TVoԾTdQQPjUOF4"5gͿAfq:C %Ş>=l&9| Dg iCӀAwL7$ 8h?&`yCo\qq$@\pb 3G"kYLa d-q,a1?n币&`t-KSG0rd,~N<=#,)@o>}t;`8i^mٶmm@5/j)vT(mcP,Bf`eN)K>@O+QUGnR?p^OY"[_5v2?ȊdBKn5̔ȣ+8ܪK (w|"3h VaZdexto}&8pz:zPia#,a GoqD8@\pcNĜ)#2c3m༭?~d ZL0Q!z[aEZOmyZ_նYCmE?iU[B~TrQO5D[L3CMCͿj"_5Sld2鱢]n;7£~j0M[Ø7<]3_,hI1S:Wo?v;SH`^!;~g<}b޽,>S@N¨?&^$}jl1Fa R_xɯWڟl/0jKQ,V[Ob5dpQ/Ɯ5TPw?b(Q@wqvSٽ|Vh 놰ۍi=ˠ 0~ح̲~5<ccwpiEM.s N@A+a#$șKxL;Nzg݄'~7qݻ +t>m ְ#I|sDxl6+k|Kɏ,wd(Qp凜E2TOySO`TSo?j>Ɵ5$ȚD;vRkB j*8C:t6V`aZ 1L+03Ac4y7eŸ֏7D\|*`z"A${7|m #Ze7J4'Ҹ?gfO >y"o6m^jb1WC/DT,pGonJqm)N%?(2* UTȄj'*C9^Pf#V*jFgUw?jfRʑ{SRsO=O4V`آbfTSo_!d'2늻 ir8wVSO?]*]gƬ"ƛɒ|QтiPzm viƙuBV -4NBkQ7~&z:r!`F S1cG_ 1܎~8hx0<0x22m?`>Ntؑm> >n$9̭L-$bH#<6/o79DUIS#c*)^Q.\^UDPP0( T E?NJdo\ZC"?iWWnDPFjIZK"RƟb6kىSPTY! CcZؽ?ӛFGxdg:k| Gxt@Oe:2HCaP"9qջyA[$ v' z+g: [.<p8ş~?ݱ}:BM  naagN^DQtdR \M 0+́\؁v$tvK2_տjIkC$J+#2Ɵ5V`KPWKVj1j"SkjjJ]^Qkp t7cml6M+A-n~<~CӛrX&aY_4g#  p#b g ۡ#ﰸy bÀ3Yz3pC؟m߾}=֙h P.-rėSXi|+iZYi~ c${9`ğspUd}xbq"v;E)x2mT uBmZ+PLimUXzkQ2nqw?jաYh*:k-Wiť'v(jXii@>ȼ8݄qMx|::Oݺ&tߴ6Xп b!G#A?;Cat|LgLV dn5Ãa{P sbzfZ !f^Qf)98!8/OQv;J~Ѝ -nUOHVSo$jQ5'`sG5,o_Ae2<+cLX'鉨eaqX ;a[^Q0DcH"Ip6:W;Z #7n򰣜|0l߱c#o#^C$ S]@InaoPi O"ƇŒT_ !T[O1\F5'&Y2j:\>_5&uW0o([-xGP38V0Q;&@㩸V?AopYGrp79˂RNipV| ay9iQ< t@ۦy|l -\ޙ`'w]4!,nD.KҥVqȴ%ErYSGK34"jY0X!)l5?S:x.籖P5V;PG9C -ep-]) ;z!p6VSӹXd?ӷ`,4tDt0q'iJE~d D v)GдZG8MA3ZE@IxhoSvv HMD#1B 2JJN", GCIx&gorQ͡2RѠ_?4Q7{%7MyiY=kQ/5 6_9Wsҙ_Ԕj vy@>Ja#-(\1XWo|9giIOYxR9BA%#X;G#i~oV0CsSWS,U'j1TSo?jUR5bsȆ CtS:CB[>dBR_aw5 |$>=U:Lo^cvAp :h-3;xC->BV9Ab7_aZfyЂi4l=8yoP[i),‚ Oq 1+;Bt"D?6Þ`ܱ 8<'ZK Q+Uˆ_WPTC[/+EjAȧ_n'јU[kYou"qGͿڜ4JD?kOy_1-4~뀊 +PTYB13+0i|ڌ&hpCk<ޱ}z$L 9-j NaOĆw#2e.QBt #(UvTSNTSo?ƨD6,G*'兌M5qäPjv7!Pf=O/gJ3@ @ݼpD_3)ͻ<[-nǧoy͞ɼ°C9儁wd0'簎4a'3 `H; 1 A>vhl[R)IDQ`{ᓐ iLTve+e% J2lHK~JAj h1lwlV9yɣJM?tjP5W5w?k_5nіϕUܹUaE@ACѱ `ѻ`xpأ6Ӻ"rN?^|v wI0% E83Α!q=tn~G `7a`"oЀA ݞ?ؾmZ1KB @$Ž%\HEq39,qO~YUT_OAMHk2UOCd3(dSt(T[Oԅ6H5&TR5U_+1}99-*ݖO (:OVPx+춟M2lr4?xZc>Ѐ>6>< /Hy " s{#3 ~4<Ѐ#Mc:egcmH}33#;*q+_,^ɶT0Pig '.~MIɯ_MT[O9zƟ1>h5# k-}LȜ̧Qj柞n25RP;R-[hH|Bqf;xtE8q~zpMtLAȫ\WЀ'`l?q'-$yc8h-i:/@sq =L+ƈּo>Y +K3RljXC(+owƴTP)Kj',:|s2%_տjHV[KzFGP_1`V?o?wL+jAEͿj]`5V;)5 Gԏ[&]E]1t(i|~s褠޴&.YgácӁ&<OXx ~LZa//YD‰!yOb0쎌 h?`7mIgvLtV8Lt:3؎ vYc܇7#fI0"pMdY_" "bCBၟi8]^Uf:MIq$ ';~5[> I!U<}h >07h 3<v ;LD 3=(҄__kuZ:UTf5ZW/&A?d5W?kG-ۈ} vt8YX GBEXLh~Be uC8S֠m^;8a/ > b^9Ni  nh_N$t8q4Cola 8CMk.E׬l8bЅk4iaNЗA AA ϒ___?Vc18bPb+Ɵk]UPieݨW?k=ŢjD}/GY~;~tř#ext]6xb @uGL 6e >`B xčpҵ/ h>~؝1: 4N n(3ަ7ӣ,c&<7L8x>|6&tx@tMˆo⪋bq43B&ahRJ2B<(3GeW(Uk~ȡ_mُƟ>oFrQ-jM͠r&'Iܹs+z?ЃRPRY)Y%A'Cx7*! `7Ks ݲ</A'ko&> `"Lx" )Gԑ#Q,<,z8!6uC4vcZGdePwIL;[Hq7vzp;tgݏe޲e]Nj ;'Ž`'|<4;[@x܎8+0/f n8>)VPc˧a$fY,7l%Rf5RaJ (r˴%_տj ՘a(0r5W?k]ߟaj+ v3B!f(Yb[An&Nd  ~.~ >#VabN''iG`l=3 x?p<@7`x =vZgzfIp% E DH .Gy'| `V!Ǜ&aLƒ4_YQA`іujO&Ya t0GHB+E棱N2u4ɲMX6t', ůWF$dZ*UơA?]%̠QO+_Z1ԕ&]ݪGjY~ǟSRv# ,i/mht29>vs=gf^S MRy ؠD6v(ah$.or i?_BUAɯPڟjQߨ#G<dLUj<TVLHW?kzajj2Νq)PC] (ST3aߙfρ.Jzp'O=8W "yh˩K!O?c~@nk@ pAr;m#梜iEptsZqݨ\N4tS~Nq=88FPkIR D0#[`DE~x2?#Z"PynR I;\Yݵa\mR#"*. L VhJjNmn_ѕnw_C`JZ%# -pօ\pB9/p!&svҁI.;41N4.ehMҔ;>iiOiRF.uF48驞zbQx"XiLʁ6FvR<)69e?g}?<g+f^J9*4@i G AqmLIumQ<ꤗO1~ {_ǝ8`~\>aqZ)XiZ&pCwK GKܦ!hGyoO<=z=|N͸×ÅXVyi뮄1q\btYܷ4p߈W 'Neu`9 | ܤbI=8.89]%108xx0SIGP,jķ̜9s,93*Vs>?6#Oц|(2PykW{/Zơh>oiuݪS?|]8 [LPQu~a? =~qgط}{ {طn N'7EZFM4) 5_I[&cgjQ9ƌ顶Nk?N+ګjߔ'ON52 +O3O͐j,ЎlltOn+ȫ>;e8F(?D9`DéKp8yʈxtgycxyqx^G*58J,-sa6e]8 NzO}ecp@sۭv(H/זO}; {FUΘF: /-QYh4=Kz$yMJ\Q)ic+~ y4,];^#Fɳr%0xBh ߧ:oFذaгGХkнkЭ[wm]¶۴sKػwwޣg>|XӧosְnB=A~&R5$dϛiZe[hqɡf.ABSXhߥcȡ?+ܡf$ ub@9p?NY?[>Gk'Lnh'9?h1a/y<<}=ffxrpʧ& {}!'TJ^ƫ$>w7C]^IE{=pqX7Mz&|SNu9bI+0:și>U4;{K*.9x@A829\ 8"x=pG3g 2[> 3r!(Ֆ?[$%zKQcl,w9k92E@GTY:m񬑮o 9%L//-‹~þbJ=s ' oaQ b),b55RGW- {XS]kLߴ1S$j"Ͽ٧Qtllu|zXi[Cm|bTXV*jz9Ԟ|6ױ߈ᄍu|& oqoױV!5d;ggG~y_m}'Q?jz'έ2}C '܅!b.hH!ϋSI2pqtR#8/rzYl(D #PJ܁ D!GpQh2 -;HENp4P+)G Hq0gJu u{dQŝVuRPH !y{8︔N 4#M ߪo*%KL@c(|L\!qUF׎婯"B4֦X2//_49Ӯgڪ.à/Y8[+uJτOm)CZ0\VǼZ^b /cvBk;/&OkKžuR(_Ro(Ar0}zk+mO9^Žmm;¶m[;eqZVi 5%B.5.C^zv͛7;NoM~m|%YR=B=lf9z764ƣ_WWOر}GرsGxkZTijHEvnm)۫wOi^oٺ5kGyERT;u GH߲ys B0[GTe1^˳X <*w87Cuo~ yeLbfZapp1χ8GLp|~ "4/HF4N4˪ˈb; AA^o& 0'O]GLz⨸I9Oy_8ĄΘ1sQu1`8p%-[eQ QB W n.9gkЍ]$/,^}9-^/uk*'Y)L.O_z1;\tхFw?rhXVDj:;?F' &wn8_hi( 5׆aՊն Z8oiZ/ <٭ gv>̳¨SN)eKs/VF&ؿK]x9O:95vQ0 omrȊA)_jjPg}N~eٵ'^&᭷ޔ^frGjfd/]4<ɓrX6>m>3U{tP>m^be ۷l7w?eHayo>7ѷRBV_K㯬!qY٤ۿ_+?߾ϟ_aPTXY1SZR88x3 " abwai`8%ڎm.\^IF NLqO㫨Z(LLs'|՞Dža\ dٳXN;(鋼 /vZ_eE?/\-_~w[>{7v2G@>l ߼isů~G;eYm.{C Ǝšv-^^-GjμyOXxp9^|Q\U{ >XxꅡSggܒ>m\jtq(.Qb\@)CN;-5Lg#f~]aۖmo24(\mGr@ P?_|%|p?yp6& fd+j.@a+<ď'zjrVz {io~ңIq?UGz毇V߻s+"/'/_7<7Ͽ'y5F[>y+r;i'H18Ĕ;irxOIu`07r4yp.6' z(JzrnD4|@#y`\ԝo;S?T 4V P HBs^ZW~d)[B?vdh9QL3ϡt]_[F~ˆ{ pͶRg„ Q>VE*Tr%݂ϗSaIK7ݻwZ|ۭ~.\e.DǴO:5 :Ԅ"TofQەCM8OPj\i|>o OS>i`[K'ieRawj+Ί* fsV K/IϵS>u(;#\WcU_, }&ܡmݺ-LF83 iBŋeǧZbLs-ISt(PU?'l8Ԓ/]̶{\|pHh@ǎq!oIN1?x$\uMsWNyK?yߘ:@M UO80;xKsg;q'x;1pbis.y',Р1(8O; ?? N+ZswfQp.x$Q 4I{}0h  \NZ͜1Öh-_RX❚z!BfDkF! &節 rﴇ5 mDrυIǘ5!Vx'>ɒEUGMzUJ}*ЧWWn{[f7d3_X+.ӖX#G=nYmF]D+B-{k3lt/tly<5'c+f^.oЅRo,Ҕ|::=ҘC˝eJx\y}ʸ\iAesqJ*B(¤8܈\x)wż M ;MY0Vfd).iq^93nq[l˧bbe%=eECy4IAyP60xhw/n>k&L KpW^Cn]DOu0jŗ~0I_4RhǷoO?.j1塣ڿݴw!tNpb8p`xjEwaDp'L7Cڝ_^.isQFpqHwrbʽ&PiY~\Wx $Ua. {WD`8=b1ԇ.) OH,s:)ipG/z,x9w?3r;86mcY.ԖO}k. }Jz_Vnk ݵnC;] _^ڰ!t\o uRlS1Z>Z}cC>-rbŊ۩'<¾Ce6.bp 5Vҩ_,pǍfǟx"lm٧WpV pB_yNp:msBr/]DP{I?[>j%5~ݺv 8]۷PfN}]lN08Saů77R4_jf\Z>?¬q/YT芶H?4&&EA7v<H'?yUGSjG?-&[>%gN\|qcp'y0h)٠qWOyE ;TzIy>1@[Ȼs Q;.Uuɻ4184G+N[d.^RමdLRJ98:+Vщd`_Ջ'ZF?_0 G?)=[]73LGNN,|+| _ ;);WXJs)Q eTƿ|~oAssBݝs?0\1gMje-wuJIH5.Lc寬(?<6N-[pqxz>[>B|IO![>J8nhzp^D>ve :s|-ku, #ޟkU&9є ZjVmj?{^$"X]N;3ktW}[ۏ,M soD?v],򙎿/"pPk:vjg;wNr|^yrSIc^,*~ <,Bj|(Z8~l~tS򧹎u?Iֿr_Mby1!?yIi λ5&+9P[.)t.wvQ)5 8/w !.RpZT+eP,+C] L@IDAT^^u)+@<7]1pr,Ð]u-S#&}9ͱ#GVj:;v?_';Q[t6m xT~XӹSRΠөV3;;`NTΧ~F{{roVU]vipI޼olfmAVIḨ9rd2e3Qkm1P 5%V{":bAj'7"/sX%w< ?1a.+}U#./}.ɼZ7m o#6ղ X#@ZZ|C-"9CȤv7IS %˴B2=Cj}b$iӦE*?wVc%Sѯ7l gK}TVs|CS0} o/l?Qȑye*^z&OdA.bmt8)QL}CM+*sυ^|Y#Tݷ![̀Şo̲BBPG3Z_G=6&W q???Oi)??h)e6l Ę_fyϾ?GB K S8ܑ<.xOK8OL:twnbHE9>Iw8TP~/ SR =O;Q/MSNc%Ky m)wA./G8d̙(זψԶ7CeuB4}E*ZZ}*86/VpCu14?0&{):QYJaCI-xf~xEhe#Fʺ Mtm-H'၆78C6e>u]ne[VV/"WJ.ů lQL_/83WT; B9NAe'v^^jaЍ (NÕ!&A biFS<~A'N? 3Ӟg,!u$݃,)¹ڢY׹yTnݺ0MylS>gkl XV7_D+P.=s:9-*.\UF_ ^fjT'mQ~p/ eFtu 3V%.0lіO _8of_?Ye3'M)2oD 8^+P<#N.p٠0 f_+أh0W_ f 2?j)dw8̿dG_nsvd}QL+K 5J}L+:)3[{o]mmU~J|=:`C/[ -ɕSYY- =߯@+zm_+1`(v^hPN7NfKf XՁ(F# 5o6nslJR9 AFpU&vĄJn4k:?cdO??i6=#ck]Jiv.hC8_rV˟~KC^=CoD#PRHtDmѿA߾c{زyK?."4h`7!!oK:k,2Rh+?Z _}uMر}y? 28;Lε[~rm~6ASu'EP5jQ2TyOȡԩiⰈX;w Р"Ə_ԋs5xL4uBE#?֮[C#!jD]qCҿ Bj*k`5 ؜z2J0f`YU5[>:Fdie-PvF-q0bJ 6mCR1+GP[-*)~QDԖeF;yGAMJ/|ghĴ_߾ǚGcfZ>];~;j8Cp<ु2QnF(F]iyh9@y}`~A x^OI t0'ʙ]TP(BQS< 9<8-Γ^|Nr6cƌls׶(]ё @څ"_ PMHj)SpQD6)Zd1`+No.uĿ1|馛3<_;|r BW]-LUoROis.{vъ]]cO\'L<[-Ɠ='= ;€_nJ$EئG?"Րb!vV5P%}o,b 7D至>O8oB'7|1Y0_ *&ɱ3Z μ'9ΐcfo>5Swm iǡN+imY%Z ]v/¶m۬~D̳|י57e|Cm-5: NM#5\?Z6HyXaA,sfo!k_bOmI)\VPKHb+^^hB!'}CaR߸=p,)i8c'ShR'?-s /ưŝ ˷ָHoZIAՀ\^Ëq:stk W+><ڿK.Mv9Ծd;̜ƿ#۴I_KI:MiѶMREQ?c|8.nvۭs7k|Tt|Pphԓ~OpRf`dtXۉvTFЫ|Pnuk u]ik֬ N FwwO9dF:aT+ u&mUƕEUd;./_[2ifןJkj$mP vݓrGq˵ *Cm"}h?r[>/RlL㄃? Z(T>fBECC=V^ei+MI\'2"win/"}%c۴Ozy_?c2;T5{^P]1_](|!s *̓r!C-J<] gu0C_kP|BM\QYP*6m ]juh7tPK/gDRWCk^ <`cgEЧ̡1t M-/jZ7ڡwEPӖOy̋f-H^*e _`6o`+5 E[U/y>._b0`bVY,V 0VA+"zyMLX.5Zlܟzcrl/~i`8cp@T9oB ^bfKA~~O7XBRiW&LmapWH/Jh a.{ej[gi="GC6n؂}N[V- 7Y[O5Zp$%W7t(C¿_߾ҾUhuHsPNʌYyvWz[?փ{uP۾mkiMXoDEjZXq˧Vpv6[m2RF+h6m wsg)xʧ4=P3_MX 7ѐԩ2l4Jl`4hC?qgsJ~<qTN||{oq =kJE8DrZW;6߹1ѭ#F 'ưu/fEZvƝŊUG&rwy:@_E[o|s)ͦ ЈO=p+6|U[[Qa /HN5m .2n^㮰u˖}^TF=[{ޟ٘ ˵BC$*LpU_Z *ELab,P7Լ#ESDdٴA|쮪?:"ה?[>W^#ZGWsoT&eg_?G3/ޥoT:]]rxq_b0i#;/HS8<N7x" ' .2Ƥ@8r]4ӥ.CA钧x|j_[>YFK%ZgBГZ#k$fn[ -q 8".+C]My%h cm/ա}Er*0Lхpwj҂?ct s4_#Ylq]z')wzo,D(WJj xSu+٨PS[nflX-%:<_Yny/QӮ.'[dR†dOyϟFuM?8o$Eice /ڌĉ:U//Z0<3t uESANϧ%KٟM6U2JdEo^*GoNc,ro)j-_UGر1K?M{jtB8:V8CV9 |kwgf%.0 /.·<\di\: N]W.e:mb4/O"<ɳx9e;N #zqq֒`I 3߈Od-1EFX~4RWNCJ˹z~}NyU]'aul$H(mO{Z$ϭ#7(}ZRG哙1&<8^13A :rp:chrymPhb?E ٰ͈Fpi΃x}((N^x yB༩u)yp]7%[ThkHq]$v1"宨( 6bI^ZQ|#&,:]A[Ƅ_D I0 i/IˀdUccNQnq R.w# uRڒ/[آr >1~x_ )}׬k3 ]vV-pD [FN QݫO暫K}&OU+91tw#;U#h(Wi'~Z[;os&V[Vaߞ?x1ǎg}HUzr?X) ^XQ(-"7DWI> NTc ׵9WdDZ?=hUW>?F o1FK_~y_.= W('µ}\$ƻdŻa#^k?ϰqƨJ />{?8}ʰ[]t+.+fżx`;+^J/N1! xGq9;ȧ|HPu+zb[2iSEp(srcj3 J8hF? ΋2^.%Gi;nC TmI|{ѵE"N.4TijƊoY&k$koT֦Z=Gj2˖- s-ݣSCtM|͵oT ;[o)4U](u]O|6,kO쓉:TIȖ MH[9}}q>.ykon},<ퟟ?5?yߡnj 5Zt3dz)s&'C؃ýLNs 8*px}GBwQ@"PL}Q )_`~g@ԅNRT㡔]4~\48M C"O i =͘9di@pi4玠˨, qkdc88wUrPJ}\A^ aa?Xćjd{ª+Z/ gӁ.vVTƘ/4wo_oUӦw?C&j 7o /}V={&/ `9[w1:P7;ul8eХNCGL|544W׬U(/hF_WÈ3+<77cQq?jz'-1Ee8y=F^wڔN 4#M ߪo (NDc-RQ( Sڱ76twg'zUׄ V^v0]`$z|yGVQ6eo[YMvl8ǟƂnt>7ȰXc:ryjdYQDPT-QvZt*̪-biVgYg0IJſz'I5x0/ 6Tںuk;®=(U?<1\'o[@pŤgvUٻOkB.q~6s\Ncׯ󅻥ǁoOwg;~Ydiu?קe!Ni]6ǩD"$TiEXbVnD\.A=0rHu|_[4o{v Dr ?/¦ aLNp+ZRa,6k(nW|<5i?rg5s/?yO|H>7Ͽyo;raK'+g͚BbsʧH*HrisQFpqHwrbʽ&PiY~\Wx $Ua. {WD`8=b1ԇ.) OH,s:)ipG,poB)S. C17o ާj}jT1,/0Yb;il??b)G_y>GoC||-zN|j*90fM/#O8v7(pSxvQ0#zIy>1@[8#iN Wv|J Nce.xL;-bpnіϓaxIj}r:FLI_jXqNlC4(kb~'N9`&R?g-CN sr\pɴiS'Jvo_l >sBWko9 ?Ơg>r]P>7^SeT]|YTJGEo>Y3zY6.`G˿~ܸH9tPW_-7) 7eևMGU}/ǟrUo9Oού{tC >;>p%p44^!nDx&3B>B !ilOyE_G>W:\θu9^2ޕ iBVDS VWڈ3m#,!ơvhqo,6,C 6ZW:G>+Đ m$BA$Ée!ĄPWlY82U.\)}LIt\O6R^<ډr['ƞWK#F߮U]*g΄_}7go\-T#K:vPJ,l?DY9kO?9lB9rpk?s|0X{h*"S?ԡVxp_0l*#{m?`?z/O_xYeRS*@0_Og;];@YC}?%~m9x М&rsЂdh2@p ![Xx:pa8BҀC;8=?9$UKA)N' WE%ޗ,Rר%aXVH!0?&G/eǿ|#OD)oݼYo~M'My~qw"uՇ/?w,qL{ϗ~4ʿ4?_>[^Π_PhzE[}Ӛ3*%ꌚM?J؞vp/?;Oο`d=!e߅Ϝw?>q^W~E]-ːy4BCxh\y֊w158Ɍ#§c}Lshʷ³!l`8\hgG%ؓh 7zI'<3iC>?d/NN={l<)LX:|կMxQ\<:63UG}@(9Sa{>C}:6~{W"?z_GmT/CW(CǏg?f8먩j#_+QZY ?egՉGrǞX\FߪG>ǃF &-}rj[:> ṭh8р]jo'8n?ir!K@ c.3>g~2Sh.p\i;H#2с @~x;mj̊Y'|wL$)/#Ed'#q*']o=UNlzk_Ͳ3sϹ(AE\2)'~Vw'ʘ*aHE} *yo?>dzz 겣aߏߍa `>~#cW4ͺC '| |S⊲Q3f8pf*>' MBplWFm^<"g^|R<``)}@ETXtU_ĂD'A8 'eJ<=ʗ?F9]>7eqaovux,(C͎2iôӄ,_\˯dy脀RM=~l7 PnOAEi2ą_q[%P!c{|@<΃uنiN#< 3$pקZ02)~0+BMA8>d'yj|rX9_r}凯7ۣeXutu1W;=hg_rz. ӗP8y 3% `q;Ƭ>!x:J犩_{f]-nV!c AFՎV8rTZ$Z<<2pYF>qK^}z ;'&?J㙝Xk1!EYQ/==/kr:99z4# uήi|hFty`?Z^z\QoI劒HW~CoOn9y[^9r?;P_j\Q3{U 2l{w?\1r?=~G j+#ar p!h#/:|o Z% 4lސQ xLprFYg'c't ?t?i+4@hCoF^#O"UO!>á͹B'#GP8BZ(JwqΩ*|sM!̋{D4|5Ĺ[}'q˓oY8 pw(,sYnZzkcYlCp}lu{y#|"C~CiӜyq& Y(a@?s& Z[pso_GhZ))*؝k[ܵe4,ڝ[lQZR[Ce^噗.Pwcãe~n]ϞN99WC]^yg_9lkל:\'?+?8p4^0(_ G]j4A3O qtei!Duc[,E8E Ƚ9rd]ְ k݄ qm>]|I?ӡo>nny6!"5;3AZa7[Yꩬb wuu0. }_EVry*GGʏKc[Tr[ee\Y1_zurN:Q>e.=&ʇ>H-ѳ3cOw_ᚧĂ!߹HiYo, +翜r/翜vwN_=:vI=}S7)ck&T6˘ ::!t^px]>҈c9;n FZp"s s^:}8tpYÎ5I! :'iа^P{2NZK7Qõaq qBsqhq,i@@β; / BL8S 3O%BwAǞ9Fhd:GHZ/qg;#NCmEie%6rk it1?tk64-_aw?/[i^sX(ȡx9rV9xeu Ǣ裚٩:2JTB-;vD7n]}sJntP?^F-;(zsu[eP{VnBr\ww0x9w?>J_dY.3Yxڸ> -44 ՗wo,i4y=[>ifG9i"ݙ"-ƹ-O犁ӬͻӶiȘ/X^p/YefvFtJBmH >4ɅĎRg}rU*)vxZ|y)9tryN0]˷0{O;:$jG}ʓ}[|ʝr;eno6Q!7t:(^Y_lխZoN6ǿpt_|V|brZgB@q1G[?>!Qvn9N8,i3˴hcrp(<t!Z'rqxZ7[OMM=I>Jf*^][-) 7l3~"EuCAB [ I/^;_Hgj;xz˱SSr4}iup- ٻTt?e=T~{N6nL?ޡVݏ{n?,9_9f?w_w. m^wӃ tB$΅,`zY&n>I)5Ўj|Z|Ggotڼ?—>+scU}T8*CMj|/`x1toyg_{q_9o\PS)ϭ'-\|LÉ8!i@ =P4qאn9EW-ndR>jjѴuC+\#!srm xp8)#̎,x,#/$#O[>]hW>񖱑 ?H iC}LpןDinJ9rszT9|<j 7W>'C(`/Y.?Qrl:7ob5Au*zlFl?ҷ\#iݾrO2H v#-Xc냰'GW@sޡ+/+jvn'_8Cg:1M94/~pQhl[B'#4xxh]7+ِQ .ȑQ )2c:4xZ>gr5?i:qqi_[^\G IEP{SR30 "Cw[VD^$dQZtn!m_ðD)ʫ|8xx#'ʉyWmBñ dQO}Siޕ֛Ӈ tJ-\Y( HF|{ھ;kÑӮ\pp0l o/_N&&Psb%m>6 n ] g@uT<#oۤuّNAxӜqːC?|n6!= 2F40g|gVּ[A yF'C|urr)ϼ43ȡ$bPu6E%vƠ,"X[ \^w;x#0VmRN><P{eÑ?CmI%>HQmN-K`p-u[A_PW=P>P^WFM\an/_?-3+snpw?w K+8Ը3*x3# ͎02/}G7y1 4_B1B @kZ0@AHs%nμ.x;݈[7&r_n]|X =—/\R p"ӌw rCxNӪ1!gY|oe6Hc7V斗3fm>R8ix Z^J4qr^@μmG _ġ&Г+N  D${N1Y(bGD?2 Q}@>wSjsei=9jZbů8=uRof#hȘ%].q N#`O{l׍^\e?v`϶np7jws{8?{uP{UwN2Zi /@GKEp^6?N=dA?鶩ڦȃ÷d𷺕\`_pf c,v)t o^s˕O8ۂny2ʱ !W녩ɧ4;3ۿ0ǻp [Q'~ʪ q*]3q]"i8/>w>Y[P0Gwq%N)Q*(Q(L WCL&p1ch&OLa>x ߯O.OM ʿС#V>LC'gZ R<[czsE#zZSv\}VuE:FE}Lt#~9(^Y~=wg߫rowN|W1==6|'.2tNI;Sź։8x  [i|Dhw8NZUHxm-q w'\HTads#^.V&|qYaC^B^|v, Z5^Zf  V.'f BSj1!BO8v>Ġ#>sJTR}eЉCm^PC_ġƥ~ʭp]8#卣ANUKֿjBLޫ/˟?4^[9Ý'ȧ #l:1(8h<|' oW|_9dB<}ȴ6Ml%#Xtgܼdnu8˒v%8!gP>-ˁģ?z]0Ée42;JwD JCq*>Z,ŲkʿQ}`_~4NtQ~e:rG p]j8ؖؒjr28"4+o,?îowoǿl;dϫ_e/k ?iM%ɮ./#ŷqxI>#=r×cǝyq3m5^,}0oV֢9861YitB4o`pYFEC>F?ش Yy}c֋rთ !NTAwLXAMtoϵ9C8"E#cl?j> +=ToW,?}~=_ox\yҍ(?_rK%kWUrԋ2őU$&x0?egՌ{i?ۿj ݸ?9GW;?i0:/hneb64Vu2^zcK+AV* ƚ}~m?43ie AyGNc\fOr[Q7 9wm/\?0\/:t5o4PPY hL[gV8f|zQ|*G'~Y'tH- gG& th3?銮6>׀A:!d3EZKfE$H寥7+-}@vKYh# rc0޺-^4 m2[3UHW pa`p !C[Xx:pa8BҀC;8=?98¡)#d}P@ ʼn. WEޗF 5GIELJixX2Gk;R퓶}s/ljZZ*?噉ѱ5~AA^/i!'R?ڑSZTZ? \a?O?9 rw?9'{W /C!bCͳV޲_8B.֧h?4|o:!<6 6Ʌi;\ ppT=1:)0q4x|{ \|C CԳgq#KR멨J5b;됗uj4F+##( _5Xm*"㦭%^>`B^3wǾW<3VN+N#G>}X7ekWK嵷˟XY\ֲ e腹+ @B^D (3jt!VQ}G?=$_r'ߜr7Gl5ռc3BhuߝX܅ o-}rG;tߕ2x3|4Et "@i@NEu9%o1ƙj [3 ?q؂)48.\\I ?<ĝAض 5EfffŬPjF=m~4[HRP" QP{vÉdRN3ۯ#NOГrI4@r ^JS5x.*}NU<eZ7B :h܊O4ow/ǟrWbIh Gva|K>l +qhlZ<2iB{8ppZ!WwDZ`,`Ck)wƠ&vV2!n~p8ŮgМ7Bx9OMNU|cRiBtt$Fy.2ZN4$+c謄kY ,"|d64jh_quZK|WT+cwaէANa5&Ӵ?X*M]77+li9f0g4 /'+ֿ>OcqU[FUmcj3eg||"+j겣F㌝?`>~#ݨ5 /B#oǺ,. hNpŅ?>)xuEpBlԅ%L%N!Ytؾ+4AO:4fIsٮ}':mY43/fL( LVF&RNp o1uAu)FXHIA~5Fd.FW|Ŋ/LFz@U"T,vk}IN-*tQx_!3Oj CYY4Pw meO]N<47]j ϜĄ_|UwQ}C4 #Yf 8Gj<~?Bh؀? >س6Z~pɳm+1O6 @el<v-Ǒoe£BO8e9 2FDrᨉG>@q|;{:(+wzaGc.D_y1h^.Z3in 퇣eZe{m+ >ݚ>4?NYȀIǚcRnE"itj0?>] >pO9W9_?q2}aUC-< t+$g֢!b:Z~;Ȅ>hv gqeqb_BЭI_M0?"Sm^W+I9Nڅu x]@Kæpu>H8o<4fffB!/Muaitv˵qʼn7gf{9gdr,;A1 `Bk\?*B^Hu=!ssJm(֊+qQ8@IDAT=k>h?;?ego?g/_j?.LO_RC 82&ƒ:ϔ 2i>A7 dt:T48>#YqnΏ}:8x\ku` 8.84Zlȁ7<<7dN(W3g3Bᬳ 1\pBݴ #^d!D^ @xQ|>T=xUuk\H<(HE:gT) `o?Rd{G.qC.?zi[XzYYu"G l/_:d'_e۷'矝2Lt%pݕ_fCc짱VIue+x8HAnX.9x ։냆~tu]!o3:EF"n{Τ0R4Cbc97:]ġGSd{vOY2|ŕeޤI>dVF8ޔ *wRi?Gi1r7矜sX?Tgs?@'Ce+M(p񐙞jߗf5mBi!n}>*Ї,týlfqd1Jp.XKob,kmH6Z'CBw>y9Ԟ3/Oaqn-JaD85·Pͽ eCGclUY\T?Ge'r7 rf=e+]6P$i/W?k4/j4paeQ Z;xW4h8qf>fgrxȂC.Bl+:ӑzH7ǀ:P vaЙ!0Ç 6I'`:4gҭ=-!ġQfgfC9~Vý]SyʂO-*`3;pGKN oPѥqOW%tЭꪵӯ#UTBr7矜s\53W?TlVWm UT!Aw9|@Pd'!v ~#c92a ]׹ӅxwNiO>t;n FZp&"s s^:}8tpYÎ5I! :'iа^P{2NZ3yF_,Q\[ E-jq#JWQVSaUs/_?s'\h(W?s;F7d."!NIWQʧ8(8\vr!G>-o.il@@βΓӱ ~,΄3 :T"tqYμmI^J3>Ȁ4y^vGP JmN7bȤj(F|䬋4LRښO480vsW\+ן Dx#_T` vo򻪷{;<'Ӌ3 G~6dࠁXih;:n:H\ ޺㠛5n͂3rҀ3D3E[s!I[OkYwm61q_űf^ AayU!)4ɅZHU:HYo7x'_-4ꚿ6 7\ }r\n8*_dfY35}YE@bt3Y}r9Y?>!dxZ;$rq-Y<]⡯ f^i0'MQx @+C N"nxz- 0?_} z | +^V]EUl@d/r(sBĵ!ţj# s責.gZǶ zC?9S[&ݜBGmTV\;b`\r?;细^wF gt;A`Q+ 042ͤ~;mǖeƛkSmLxu@KsAs̢0OQ ر@2`J|@b3zS/#˵ձŠC,-C JD)# q%[ gC~ZY? la$ǟ56g46E?r\#_?O/#:3 /kW>ɰ"'C`iLq ]Y[4>;ΐ8 R l}Qā.8 m08QgMB9Q(]6 ^<<8?@ .r SSO#_8{~%<I\=S'ޣ*HRk#O2VӾnJ.@_J?9ets/֕1Mta?G\{ńB'r{qCO>+8p[p E0 'ㄤO L\! rH['h_|ԀͣiV.SGC,px'S3gGYXG^ I4G8| ;Ј?j|ֳl>uqeI 𐍐Uad]^a;K437g<ܶhTnkj#TiM$ǟs7# V )b?[\;x/絖PV;F(y|wٹM&o 8B.)`>ůck/2qw>G p~C Bà"3Cs&WwHApxaߺE%e}mW{ZHF& r'0 W¿p)N-F^$dQZt0bJȫ|i|osLr# 矜Y^t\ԕW6uUU]r\CVY{ 9/L_RgyP&2 `'7:qq|6:ƛO[@CvC u`L#CICG3h>IÃx+k^-XW Dž^<#o!:99g^Pq >xP7`m5 F(QEX9Ll긒F5s7Q!b_SхZO3߹W?csÄޡƫtBfjUĩXB;HC#|-gVlǰGB/ǟ:FƨȸJ*YS 8Urm$_ u\0DA_IW?7r]ȹ:&3NO|ᒚtDhTg3g4tN/p%۸m9/v g6 ًz|BEjB=-!"ݨ E '>8Pf;iiߙ/wBYVڰ7cZL ڥڕgMXm4=i??_r\!矜a=ҁ&$\ s U31~s믵_:<r2_ckA ~F}8_>(2_NcbXYfqХo:"kwG's{2/cǺ돱ѱrرpQ_΍<\=MMa? z+kì0;څ /E]t/|)vŲ8t]T?`:Geg}[đиVq m;oY3l BGr%rt̲44gN[vY'DCrlBӰyqK_zȧ%2ŭ4]La1 4¨Ti48kM+7o,ssގ<_tQsՕྖ7Q!ǿ™Wr/\/׿ʵk>sLO?/,۷R#ѣlKu˚.scw;pwXӖ]P Rl-Ͳx[r+o09= WT|'Ҹu |32]&qҎs[74Wq:tBuqA?΃kV&q^i e3C0d#>\Cȣ p?z qeC}'WǭӘ}qv5ᣯ.kb*r:' +t J1^JW J*T U;X]sr'&}z_sx,~Úr'ǟr¶?v{jړ=rt+{npN{NԸFNw?G˾2Q Wذ[kiF2Fʩo#WiΝS:vI+:\vv(Q-K|^ cK˱μqV+k R m̅A:`4ݲ*H WOeTA] z?d!iÏMeG >6"̷^ԀCF1G 1~mˈ\ 3FDxrƥS=urrw3cSFv?z?g57ǟK7WnP6#r!:0g>O:uB[% SefgC _ǁ4W^YFidW[W mHz;z =fSh?9o?)« r ?9ƍ+_ z]ߥ>y-eiP9Dw>8º7O==r?\s<-pP:x<}e1Oc f ɰĒyI# <>B~4[k4mZ|k\ J!. ,.8!2dt oBN0}CHphCG'tV+C%SGPDjGKWV|#CĒ8V$/]P@BsP[ ۑ֖Սj oi>O}/h7ǟ:7XPg5+sdv̿W8x{n│w.OQ:k_Y)Gx[\m7wW; `{z[ibjΛH bh-49d.|\\>QY^2gصF4ɩ<_Pqx KV `k:x:1(MTaHVP1YIPP_] *1rwb_~͸8VCl&=h**9rz9NXk<Ӵ8^>(+rW>$sU.vӏG>ߝΫO5:'6oq?;ȹ(7(ů{{D ӯʟeG84>o Ƿc]A4Y/tqrp]@P(uaI;SmFSgJ!<|hƓ {v&!8y\hɆx|099y̼bE33EXiY"^p.6An+Ԧ}ǃVo/<4~䓻ؿa|CZ/r0TGl4&0!?9&s%ڭXfœrݼaa5ӧO"ǷDNJ}?y†QP۪Pڏ>}ތPnxl=D#]8o~QO,Tf9]w5DDPB5;h& M #Yf _> #୛8? 3{F:zob$7PnOAEi2ąsq[%P!c{|@<΃uنiN#< :v`i4!:qp~\(v)}f^ЉS;o}&|)ڗkۋoȣI}sdT5=Yv5W2ƱWvga^ URU. fI/w%+Q l5O?93]Xn/?t[:8s-~gg=G>o{NUKOQPoowIw~kKzυK pvzAr&4!>ydT82i>A7NGͺ,ku~h$+&qc <Zje#G%٩eOc!-e߼էװsqrj8ofgg:ZL~mH.z_==/hA_FBεV㲏HшCs~lqQoayx6 o(Pg.gYg'c't ?t?i:9FhCo|zB $2\x0)Y2( gT) dCo*ɕO6uߊ6r=m5?+&7Gv]{+wZ7Uro+}/ǿ.r-;xCv~ro?j 8gNG dhP_Fú0XW׿_LJ>{vǡF?qk6?( (Ce i/"mUlkjuVV@po>B;>E7n9x ։냆~tu]!o3:EF"n{Τ0R4Cbc97:]ġGSd{vOS2G^?H 58D/%F8y$lC `je ^|AQWiW?|rW>q&Bּޛys,=݁ArAl.lYDEAQUQ}zWAv%mӴ͞&m~?<959'i{g>5ȯM+ L+-WoFZ ̿K|u}k. Y@[co?o9 2؊4u$&蹲;fȁͿ#Iw9T>N7cX|Y͚gfVLFyHi7?^.t<Cy7 z;Fz'-B  V˳b<.~tZæ7 Bx4&"i{D[>r,J':pcEÅ+t]ּ`t+}CG"L\JFEC圴WwU|tK-uUCi$K۵9?v?+*/TsBm.%Ziա.%AGsnrNK| xV,;''brĆ#VedfV9GR\ T%&-% j,4ZvRC]TmǠ " {ue҂KZp+r[ ;4'q0.x"`NZ.3o\g?~0tY&a<@.pabP;T׭ Y v˿$]J6)M?@F? DH~9E`ߪ[rxSuZفK_n'VUF5(V?Q=K@|NdxPK[w 5_lmlG&|}sg,&1AYz+)cv/ҝ?!?~n7|vƖO{KugViFea^c0ni(*sr5.<gPÏ|Ao @..μ ]d?aCx'tϜÆ5IyWVOs jc:P SP^*Z m>ʯpN=rv2)ɟ2er4yb:tHڲes&?'N3:աlذ!gFO&Lf͚s}O*?nɑ&yo"u{]ӡC?;K׿>y?Aտz+ӫ^ttM7z!ѵzpok\?>re8y'E~KS/ J+?)cw[|d%V?rO|OC[>Μ- wn$hZ@?4ߟSiqS2}OL0s0onmjCu'+D[]Jʭfl k;'}^J~wξb멇 IăwZp} uZg™rqyH j?8yp-(=&/i?x+ q+غqskh.Y2) ^FH#>7{n7~|bOGە4S%KYgr{_L̟sI-?kE@oiɓ V^;rw/2Z\G[Hgk3իOO-i޽6^Vq7 ο@vD-ȯAxE#5~ AjEEWՁ]?|k]|/ӹ߽K[>fΚbx\3u^SDg@(ʿy|J^8p+aw>\8'Ұ Jy'wOM^溟kY~Zg1U\?V)#s|#_K :dwy'kYĐ]W0pā'pzs]'-ԍf N7sVѠCQ# п| C'(w" ) CV}{ҷޠ,>j`~o˷oD-GBm__"(u5?-[[>e\~xPW nCtpQ.8 gےӁ"CS+WH Dž32zy?/\󆆸:_2LOz'{mVZ@9z'Ҟ}NW\(@<ԃ}u[~|[paj?z$=isϥZ~u_OiӦs煦}t}a1lU{:*3&-Ztv4qCg%3mw{Hr=2;^:ٵ; >.[~[T+/ݷ P~.I)@?-a+V鬰Ue_7xS!GePgO׿ojm@[>( #33%W`ڕRMsNL5j~Jڿ1~6ʿoBA.||͠Z}U fg e [+F=CSj mv:G1`F" q4z5>2;S&MNK, ~ZFiᢅ!&6m?C1,8LFٖ>{vOc5/^=wEp0*LdQQ=#i<+#̦d4Í2*8&g-Hcd!ڣvR9أ=ESh$4He{'d9X񧄝in՜Y!?iظ!<@oՖOO 5\r+DU~<6,]OeқoaX/})}ߌS2P+C+Ԩc_⢋# 7\3gwdLO~6Ҋ;wˮ,ھ=?MxG:\u| >pڹcGq;^7(N5.pU_ ykdt_O̧ӦͪB•G"TU.^t͛uwϞoLd`CpݢwӴLk)G_,}߾/9g>d,9}_ߢRJ+ON7>/F|Lo0ڭ?2 [h9mo̜GrstSzʿyċ[>GM~_ʍ1߰tһdTkW!C >ջӷyrw;ޭߏ,;*~2U_z'-T̃-aq% ؄5.|k:N.a⠅]ۄj0lr>׫kUs2i년<8 ^ CtuƐ@IDATz@Ka0=# c/4z9Cygp!l ]Q6CEa$]^aI34x4Rq }JҙVXF\gyeY[di *gRϽ 52@jǝ$#^fF c)L5TCC!4IV}뮍C._%P+-ߏbϕAmo3AgٸFaa=[M3VoICLoۦj1e*@) ~ʧf}K-# O*+^~ᵯ6kPr&qBb6 .Ҳ͝._LgM_ߨlJnZKv[⨭7 ={ WK~GB&ǯ}-y矗~~>İRպU~'2o ӿ|09_QO?~7-ǿfˡ\ uiE .eQ_%˿kbU6nU=o6Q؆J=_MkɧL!(pRG$~տ"RQZZeIA~Ú oM>RJSE*OSʄ(yV[>y+0/4NIiy0 8:]gJߓ!0)JS?.9Fc, V]'"ˈoHV^?nVε5Z K_(MU/H إORgogR,5C7$ Uӳ&lAMY+ԆNs$Q6ߢePS䚵k6 X r \vF(q1.`z~%[< yyx O*1%tC\Ogc\':&қ)qd V ua:-w"hCsI[g Eۙ6l(#pҴgܑ6jcX.(9RC蜵v-eceT)3d6]#MN[x0ddT;sB^s-\fT`{ݺu3ʙJiid%]b8a&s9eK'*?0.RMFQx,]Lh;*gɱ*2nan s >,_NؾN,?+{Ѻ9 P鸔@ϙ='}B+Oߪ>`҇>Z$_m4V]yEWs)ڂt9r˧hg?4lİtБ^ǔˮH\i.%Um'氌 O%/yIzdi2+>bx?-#?7?g~Wf>ɿ[ң=* 2BY-PA CZgk2?OV|oN/{˂Vm~o/4]wo}[s\$_a#_K+K8Ɵ=n=}u)󟙳YVCǿݏ>[qE6Ɵ1 y=_5wj5G?c;sW7^ƿu)0jݔ_" ퟲdww?e.*Gyu=jC{u;´Bm0f T6&Ά0i%MU_h6=yL 9G[K:<.qĘ>c{uP"B 8 8ؐA߼y]867ohckx8pNsPuS`Hq JeWhN~GX=¯.-CF B^2tkqa`/2|TX( 'pw*?0plٲ,di֖OEҹ'MYuV3',kn|wkEZ  -HI߯"@ue\aOVƿzgΐPcHv=tmK.Iۮ|nҪ2hoB~p㒋/n}䵫CDZ.-b??~YF2/|bfO>;d\J/E/L.z?}#_~9ŇH>1m'ՉpяaW7~T ?+^;ݚk{˥q S[>oj Co_:soCc^26Jߥo=/@mKGzPII݃8dN/KuWmXo:ڥ-g k%+㈕jVU C:#;ݯ(4Q!`^0Sg_e&DƟ?bW'L>YW]0մR";ofcgU!o^mA-wCֿ޺v䌉8kّd~h4K:\0?650tA<O Y1 sȨa CkW+?h0zhZ_[bpF5!N FGe)j+*ƛ3E ~&3GAclWq`]7Э{e%҂E!GYl%dܳҜyoLiEt*}ǔ38o\ 5VP3ǵ6S 9@~mD*u³S}'rv=Z!&ۻWqUH)U,6(DVW?Cg5 ?tH٣P;*-}te#6"\w_*:CW\W^rSĵbٟ ֟ԧҝwyLҗ4}Ϥ\龤KYA|o;UW(]{m!]]/Y3$KWWX|6P#&m@+7MW~O.X1>ϧK"ƌ_ ߻O?Hh&ɾ RIPkSHnTS}XEcgšԮMLJSߗnu;nlnS߿&k .{wj~E<ϙ>ߎm;" Bm*sx jU1տXsR}-~W_zVzbz 9>F܍jb%OyZWkտ9ksFjP{R$ȑdRlCpqqdl3Z6=F= ?,xa[2_뼡 (83< Ë#,m7❆5?SH+Z(qE[q \:W\N٦UO#*MV@fONI~YЄMsW}Ǹs1ߦi;+us%`[I, U>C3%yld[V;qc]>Y0pZxBmVq[6SV hKb]IZ~n-ݢ-x+`޼i韭_e iu1sn伷9?6lx\F}D6[~Sn+2osK }W6bcg_}ݺ+sVg5Rq֘W~%ȏіX|7.6*_m$5 oQ[>=!/Q2{%/zg{%qvPcVU ٹ}]qȐЬH3\SehÐ{^ߥ/M7xcdΝܦs? d?kଫ_fO v8hxo5MnJ+OaY<}jcjP+tڥ[ &+\>4&2'cCǾ01yE5?Qgñb-?9'o5r ^r2Iү|̖϶4ȝ Ce:C-V]APĬ0l;`[ ^?xh#xe6dwŶC8/u^ %4wLӭ[g-A Y/4~ҒY\ VV"ag93M3댛;|m 3Q¡duZ tV6yի4| AQ"jT ..ns|>6 Qbbh4,UlV012;bh}pz &,YitYg~=A^9sʀ6OVNmmrgB\< 5 9i7u |X6]5 &ʰHO0I[P Ugvof em51mĉڦ:)ܱ W6z:bs&N}`{t;W*eCoGw=rֺbE"wfO?)tVC|\g`%u7ܐ^W??N_}jKG]Q;zk^B#]pV,Z~{/^Vr~~4Vc|UêYڃ{4O&C!AY}*arn0{Pe/MoAO~2VTmi4/y~7oԭ/K+?|O|U_}Ak/_'g5t?3ۥVgAm06DY{ xC!Ө3$H?}?oR{+0BO&Ϧֿ%W\~e_j8J /ԙ2< /N/^^_7ᯗc%哭??~Eկޖ>b1*A7ՎZpڹZ::MywzۯrqSJK,M/z/Ӷ<:?ӷɠ~ U.K MX{0 ox|xݟ45cǥ/~+ ?$?w=U(eX]皩4AߵDԁEWJ_O_?vk sVy9/&݌bTk}S+T$?3C|so-ȟjB1yě9^lAEEzż]Np͕/cՖlS}s~[:￵YE1,̨mDYG@|1 (8h4t'L<6z702DZӀ(~̠ܰOBXdc#7-k約xُ MݙPAg|߼pM!3Ԅ! j@ScQ7B&7h9_3C'*\sj c4Ɛ?n=7;?=CiQ։{P8 ?jt!gSiQ zԩ]#GМ`{w Ouڏ_R T ~^vq6"VV[Pg͠Mh 40|p>O>{IytzR9<-^8tN=MQ+^>sG&?0zE' r.? &>t=j0Np~uQ?Ko3ޛ 5 Wxl Óκ ??rӅYs{Q_Wb|ʲ]~?2GاȗV-@V_'k>y]Pe?xCiًCG~KU9;;fΆH5y^ͮozÛBokKSO}?Õ[>yזϏ|vi;?RМw+o'3h1jkcP*.O.Snq/c}1:< r-9nPg'7GY`G/y~1T/޹J+4i3?}]?yrSB-ȟ-C.]9[bFe[>u`wiCT_CKt! H5 w~h;Z"ii<]\+(Eꮅ0%pt*28ltHS'29Zt"?@t=2-yN{.%XAj)LX d0j1K)SxFQeDl:|#ZO+qFVtAsF<YN>+VOߏZK,NSecNݬdXG:iؐOY F$ Xt;'NFQR[I\_f@.je(;kťgΎNd6&ϣ2 Sٶ8,Iv‚t>By+M>Xj?z4sE ӤIι6Q ~H#͑GB#uآ*8ssaݐ0{V5j)͚ ɚ w/_$Uv~AW/&n=zn,@US=3q):+, 0i.ԼK." R~ @"'ҘOc;{ ;V_R2sLgn+}C~[>[>{o'R{:,]4bP.Rm[aC uR8o@7Lx"Kw^W# `Zgl/$ԅYθy9udD;ӸA Qy(m4eW.gA<UV.PIr 0 FXaU@}IE:7Be2%23sL] R>&mwAnӹg\BnےV w'95B9*,2VBjڒwn9"OHSǠ_m32h˧姣8Әc*2vMõCCS2=n€X1OnTF0V:j*O.Ν;uc ӴiJ][K穡<#z([i tDt%waG{'}3.ryZ5@Cץzd/(RN>|"ؠFxnUeu:Ӯ/ 76G?Cn2}8 qZf]Jpy:CM`k)(-ߨ-T^җ 7 'ePn7| .#jRE? prV_dʳ_:[y6_rnUg_u*|g|<<ݻw? ?1,E5tiy236NQmNrgFx`#_[>~} |O^4<8Oow=9[gkޕ 5j+k4َ[>a^)ulc2 qN-qИxT7s [cLC8hU!>L$:t.~ ە@g2l,/,n:?V :mdJJ9Ѽ" :/R-? kAc"*H#ϩHʊciRR:|@˖.YɈi==DgqֽW jF(O:,S2Fqv-.?7_2>$-?ƳK";t3u҇bBdg>|.J`۴H{5Jc'C/Āxּi1'uy#92Zu9}.?9xB-C-dS:sbVeO<)oH/9]?6H|;~-]ˠx8IjI ve}R7I/:{Qz׻~yDc|o'b;-oŠJHSOM7\CH+%ǎ"R]ct]w+4H{VO8!MoJ/|ዎ=ޓnִqGBrxck(.UjFO4Ҳ_}ۯvNo_}H.(剾e~I(fwԿrS)oh3Tx|w|f3,e%o֨E_؋/fGN[\0W,oC)H]J._W8ca){<3?n?třiXuE, ]ʴ)0`Ϲ$/9?߻^ln됂s q 5Z={0pu?L׍Y>G 6ozxbˡ* 9NK<~#gC&b5tu"B#$)57NFZr#p>ضL?4 GqP[b=+h2$tF{fCN4$~7#!arU8ԡ0nVu$ -?v4N>|hG[0倌 򏖁mx<ҵ!q4FA.,`:|H'I۵B?=;W?]]C?+b3#זkES2n*FiԳ@(!).`KJCuϞ(mOf tM=#C|3;IkoJ$ctT}im3~|=~0ߗR~J-O] T=S/ߟ)ߟBi|rF!n ĥ?f乁|{v՗̋id_woݳwg-=Ϳ o?=V25k7H%+0=3y`Y^ydN4-aWeD I cEAp:s2tݺNmd J!. $.8.i.:x`Cqo>6tV/ԒlPsr;Yo*Bѐ(\.iJ Ƌ :{Ո0x.}mr_sTg(v p)9*GU]>wu:IZy|&G?q>[_1я%U(RK/LKh~Xx9Q; Z7BMϷ|>Uc0hy߫/֙i^ /'?2mwIx( AMoOF2#k 4vHK b;+15`1?y~p4!Nx\h,Ó 6p@ P-Nx\M_UeēOhpg·xBCH{U,b-B9[HICLE!ٰ0qTD2`GtY *?-uxeP8ZӨTz2 |V}ۓ%k_Z?(m\l.&ӪB>oUzEn'AE~Of//7ԿJwZ鞼_,f̚5ͣM52[' Wg1B\mÑG46;;A{u'꿿-?_6W|=4WZ  xhᅭ8Zxp ao=ar O @0T]9㷋,hHc<0+@ ~灰ymyq<֭,jxφO*=6.Gp 5:eERP'?s|N {7C+MQ";qs 8\saxEY=@ 8 ~ӃX-rm8%yÅ8U+W-djY`PsKxMKV `s8h(Xگ;u|;d0X4eTR|)Bi;~>1B y/ӫ5uz?us_ўJZ*ܬ綡ZtTY+>-p&}(qQGݻdPS|-3 η:_3f C3?GȠ&Vqg'[Ҿࢹ"4B }roT# 4g>r#Y { ?PHUQ4.?[˴!5kkWl?>͆31CWl7" vxGZc1/qszp~Ń=:70BYQvO!YxXBg7*Q # -M;gXBaw/r2t4{l]J\%7ȂgJW3͂@@EYZ qWڟ]_i1+O>) 53fO<'~5tt1d K ٺ[>z+ӌc%Q yLO0I- (K/:S3l(6D6Ƶc2 6HO<.myGm?QG<|0w OAya2ăq~(ri,[<x@ņ7xm :C&ixl4rv++p@⁀)ceYV$1'% x]@=缀sZ烰iO -nݺ`ȥeA7nlj42 Q~ cxZE۵dZe{]kX==iG熢zRJ;Y߿ǽGO)ON㇧5Wbn 5zWEV'̾M>ߡ(%"*8H _v_T:ۜU%!_=&`Z}&895"5˖֮YC-y5xF+q-ox\@ņ3yyƴ'æI@l_j w82o Ȭizaxv䍰Ê7÷㫫VOmY[N[>M16L&tC<*p#1?P("Dy|2&"Ji -'D5տ"ԿN?)OJsjgv]ob?۴ @9c֐ο|Qm 8^W*.ax ~uDuX=! 5klA ы(l8˃Mn oivxp%o^;tK֯_WbBV뽊C;e<6*iGđ5ZB42uvEGmSц" POSLUƆ.PNYRJ;UoJSԌπw|3uʣ@nOU(5[> )fmD٠6@/W9nJyg^6I>36-# ]r Ǧ“Qc4yۧ]hpڸV,F$os~,E!x?x9NM}#P8ɘq |'[7mxp͗a7_đmOBIdֹ 2aR囩Mّ2)ܠ,#cS4:Nn,qA!"ym;E97y7-[>SP2E9W%< Hk0YaxV]Ă@U"?[>yS'Oa׏_ՐnŀzSԠֿ"Rj7?*0?P_J[?kWj0`2B/Ǯ]ĺS+fgݎ۷ootʿiċB\ܧc+"ԑw "6T'I-S:caeQ5kRz^gVZkg0-! æVx pz )޼v(?oc\%\p.X=.ϊqZ]`n$k 4 Y7?\O@s jئm(+ÍA uXnYnk YI3Aq)gs_UE-|2JU W0.o׎L,̿4S @jϦW y"ϹQ:Q)17ZP9y`+O8M tp2 4ȝCk׊(1oK dPcڵk6(@.Tdg +Όޫl,#=?\҂#-`^XVT4'-C8y{ggfLsҺpu.y:3_C 2 88Mguy٤Zn}0jsn_$ RIQTh2t! 0F)+4Vݒ3ԪW=TU\[rC=W5A'_<=O㩺?b:w 5_lml6zxa[>+ e7 5AE2߇<+>=οO3|RSmX[>۴RN+6*;-6 ck2w4T@IDATN]q^`9\ o.Ng<8ڏ|o @..Τ ]d?aCx'tϜÆ5IyWVOs jc:P Sò-U EU6©J@Ta;@1%ܐR[r&L|#תSP|i?wťJ:?a~ߥ=mݺ哑+2wle翛u'?G+-3!O[>v6ekBM1ե0By.6l ^TF,oƮ_b멇'|&ah]6y{΄3 :(x~9q;6Z&nQ0Cz" o~C>bu7H7wڸVhatcɤ4kd_C, -ԍooiɓ WmhR('E}IJ+ԿJS)j4]t3g2ӷ$g@"4#S0?F?{k)[X95'w˥2"ڼlaµ,b.T+889.tēۍoF3x¸o6p;zCָn]y ! 8Cw#q.$aNgASѬw-2Hc:~Ú]+ʕAmfMY)$T46raAzʼnJCT?SS$*4P;s;ȧe|*A?)޽;&dYh2sVѡ byU[>ɜ333m-GBmIRl2ŧTyM$1)\J0owòstU|B5H.U/a"-Cπ8U;'L.8qr#iH4ct9 fޚq2Vsf̛8#1j3<^h'=`Y֠; aؾ(u2SFÂa~NH Cgv"DHOɅWP0,T g}䉓0֓`\Ew)OJSS38=]BMg͘R_Tu3;wĜeV-W`i i0EwXR9vyGө?,[~NjӢ5ݠ q'6)-\l2q vQl<69 iNG biܺz4<@'r?P|^~-c, Y3] ҙe>A[Yv惟h t"+WZ1[>)kp>SD6z)FfC$6ЦN6 f9B!\`.Ro߹S6Ҏ4a4b8U_zԸ.Yb)j\_;$jRh Sz|s[JS? U_ԏ?vX_yuᄡqaiƴҋ43Dm/QXSLJ9,/nK?0&?z@:z5GRLgqUWq ` CpF4q 6>?g¼8K8hm6!/r:ۅOaDU$ 2i년<8 _wp8]^AaY+DJ6pl@_{PQ6l.\EaCWX i(";]#Ә1cΥ)KW+2)j@OPƿGz@8[2l>Cm&+Jw߾4f4i⤮mp߮a3ҨsҨq,О-ilǎ4!dW/jn 3lȁ ~7v'8yLg״uYF˂la9t(~nљ&MFkg(g翇ڦjڹӖW=^NO^f֊̚?F{ܓz4sWW*k˧޺ V N6q? \dم0wRc:yӺ䄡8%xZah̗pC\Yp݅ @Fm OZZA҇G?k3CG*\:x`vS5z~}tm.k a-4CyV n۟U\s5ɾW_ik XnVTmF61yÕ||;nln'm6%w6e9ðt@s}{FAƌ&(=3c_tXT&}bu@ɾC?==M'UV /?gvBI`PC6lLQ aa\ām3Z6=F=䊔nVLIO>-/u v\zEnc6NCzӚ)WA8"-8xـ .OV\7.ĜɾOa/lS/ӦVhY&49׏ m޹+gszWW_/|4a[3:ӬNj;?VmoOG1@hgNرc5?Ut|w˨v2 6:?- 5^O5VM㻝h)GWdzuMb GEQˠe|oOVs(Eh۷_g^_i J&)G/߿+߿+?O0A<ǜ/0u LB'g=4wL۟6ݧT=j'?֬Y-^pm_*C볟/0O~Mq<;؈oQDyp[̻lABk!B̐y2̅%&x./pA&t1n?ýc5+rl:P˪Ygiw- f*T'"LDZ _3SO\/ 9s߮C>:_'6}VYԿRgK+TJSU_#GLnjJtg;GfpE:ç#m4Yn2ѸfdnHFaD*mmo:m/ 24~\ ̠?-!l8wMKZ9-a+^u~Bc<.2 :[ mo&|ظ:B|2׌ CQ*1D#&.  CP_ZER!X_J[?L3UL: YOe0XE**2*/&_)-]&+6p']l!%l:یW94a˱δ ~s\+^$ 0mѝk9(Xݵ0wZXEG.ixX"e:x_is:\c{%@.̊p%C$@ޙ5Z5F1h<1/Bu9+ Zr}tM1qX,4280 r<îM:7Be?i E~ѿԿJJPRA }7׋Qe(2(2#2.5Q!՜BW#Z cl~jZx 5nl R0bw~?$REQI"}iT`]hŋler!Gq -8(;m5jԨNG>1&HE I(kir&ˑ ;xl|hHC˲qPb'ԸqWccYb!pqj0#_h 7q <xwk+ϊ?y'3IEx]🵃H{vƎ{HŸ#GLidbU% /y>8#Axx:<>B~$4<@|4tGt-bl1R* $*8!<DH /Ӂׅ `%4P=8|wu!9T2y)W HGWO E\5WR%XE'^@KY?럞jcg_3<߼`03"?+?p,?W? sl.9TQ6e^!<9Ą' MKeI158Ʉ#§#y)OxCoL|BhV k/H1'%R Q $GI˧Nhe3iJ>r>xǏuIȧcOO9* 98~<4 ~ʤ6,20gGx:%H+?׿CG+SFy'Ͽy/&4_Ggd|Bߺqb#G9w^xB\'x|9_47rA8GZdANzN>!KGdv#_)*a**+< =q)48.\TI=4ei [x 5L8ɉqFZ.&Dȓ$:+RD@L qs_ן876՟C4JTby#?_w`=b3nIgh0)N|t0$!W4BHiX(M(z-4N!ZBx|>Cxsͳ (DQKeyġP:T\:QVx2h+!y.dݣFcςZyp”~1U6"5T*VsD": -YWגQn곚UԭEy#ϿbScw׻A92YT&Ҽ;XO Okذ%;v+>OK.GQ?i|De O~#0p'Y!ߎdYNt! th~VpBT%-ʆ2үJ! ?t O|h#L: ^VlȀl׽#GZ&Ba]2QD .HW-%BXY"F-8V"DA8Ѡ&Uɪij~LTIߤ_ҤmT߼7?p7"? fl=$VPQ#OhQ{h+S59 1M(M(2e=Y|B@m)x&@|G|?QG>rj>r \q "q%<@٤eG+|8o#,Q#+5PG>vRQ/v0Foo"(BZUj*IN-y%-d qЋ[uz =G=ΫI&JG uAč(cIy;z8;1yyzklW*O0{JKqʓf :N(UKP8,1L Pr'_n rJ."Gi\a @8tB $$U]X bVx2yBDa!i t$MGq͌>}Ja!Uɒueضʟ/<'bfE yyNv~VpҼ_nKG^_1"Ͽ;gܘCMiG ƎR8䧑Veg[N< wBpyrA#y i7#-P2$<#C 2MR&* "-|`xр2bdѣ9e-zO+1uҤɞtpOY8< ] N4ώ'=n0NV %}+t "_O"̜5#ۻ9BQv=-?Uf/<7?+?ߴXy_yΝ nq_kÇw(ޡ}i8H4wN Z(RZjK;#.~)^#Nȅz&|j'KL0pf)&:-N9*!4G>ӷod#S6՟ju5 6LVFx8['ڥSժ.G3>art??}8*>O=6Ԇ9_4V@y/Wu&ݶn; ѳuΙg }ZNx:? ,_zsI?FoetkgOsJpltx8tڌ 9ب:iɉG).~(O\|ziD-x4*G[ eZ!d ɇKq6)dx9|!Q'=il\%Oz#NCm . aqTǗ;\+eG&UoJ? N%/%phF]%{*BPZ)-Ve ZO5qGٞU%~-]ԞC徻Ǵ)Q{ξ'懤Jn0#ݚUY?5?3_8%r ='8t=jԨh*$GXc9慅a.mH62ijGH_P{|?cqjSj7qgڰ.qXn0[l=ׯ>PJ3rϙ='l켳GCcm{:oo_jlA.E;vn s?< i=_^c?̎kzy|5aP[oUǰj׫-Vw>B?@2/Xa<4<>rƑ.!.!W@Ƶ#p (@& qpf2tڌ %P,ph.KkCZ{6i?iT4pd ° [,8cЇ9I-< 'PNP3Zr~UԿʿw9swu;B0?Yoޕ9# ?g6}L{kU}ºp/}ӝjW^Sd'{oc/6>_Ex}?c<׿Md;p Wj?$]i_]D6|g&c#'>!N( G3ᛁw_i|8r' L􀇏8P+;RNR YRAR*r0^q^/ɁْC  i  >hGޑF G>F3s/#t5TUi Cg@ ~?ϏKaNhD1cV맭`DpW2 )~ʿ-NKW7*m=lEvہe|՛ Ku{P|_֎:H?` tŶ%vټe"6WzAFF1ryǝC_{y 3t=w?AX; ؜aCO+80 _.Ҁp)NH E!B.א:&iEBU y)k36L,'r!q da2ၖr @'.d,@4qCOwj$AY%k}^<OPOw}aIJH.syڣ'\b{~o7i&+Z;γx kl:f?hu_{~D"Z1'tٜ>O=aO>cEE'`e-. UkG>_5v}6TՙrcՈQi"ю׿rc|+V)lCEKWM9A;veh? P)W(&a=o{pq`9ؚп*ȈCgd }(KaUhgF[ӴJI4Z;樣c{xgQB5i.79+ {2ʀWEu߮! g}eECp~;{lwo:2kZ>j9v)^ 8Pш=i]R`.^#2"CGtJbs<-kKP@0ڎxr@|G4O@ތYox#d" ]wwG鴁ir>`?Mߌc^^E?jZ/Y뮠9ps~+b~J&~_Zia=~ wņuu>'浫8vQ w{@koc*ª?w?&bo7?yO^J۰׿5\8jpǸMqK*B9x8Y.hO!4#:lBCiN O:HC#2-q&y*R8ihj.^J%[@N48.x;Ԇָj;N942(pTZ w"D'>YX,N_~B&*&ŚkMDU2k^Ovez;o:ul|{C`nM&<ԤF6?Go9`}?a5u˭b:RY*&Ś۩1jq%xidt%#9Ž;¶j?7g] Ɩ.?۠Al|kUڿs'ڐClȐ!֫W/׿eSL &/bj r+ӧ-X̞mg:UuCCWo뛚ʠwviڼn:vG w}M{umֽGE^/oFeB:t 3!WUL5KaWXUr٬>rt'6)r:B"hu #?qKpȒ B.GֽFI'U v<Zcîm!b=΀NΌ9&$dd̈ " /܇k]> Y՟?7nlɾFm|vuW]zW2)=LJUON',sjO-r'Ev޼;'oߚ$P}?Y8s;\wݾ[Zj$ۯ˗;ΡWv衕_~χ ` ۩#eE3?O;4;ÚS/5_-Z0b|su}ލWC"_#!}sO+8P_o3t;Ԏ8|+e˼߭xMFQEtS&@u|SGYVRt ?pΫ=_?y'Ͽyڹa#+XןS[OBkYOkoC-N_ afpl1]C+y?,hEqxh?q.BXE\Ń~&ò JrË8Tda;S 88U9^Y?!KМ _O_5ޗy_.?<<4͉AOWO0رc_qs'!.U I+N$ <@<@2WE xGD Oq"d-bf3$RR.p2YF\0#<\B# @e!Y&9\!-!Ǐ瞷/_q.-\q%'Ԟ3Nr9k5>!~_sIwOָqJl=~-˙>9Wag|v衇56s?>}{Yn#4'/s/م]h{WԆ$&ǀw^ޡ6sX^0}ٳ~1d_x}* 5a[3~:)M辩6rzHC Oޞ7}Lk3/<'Ͽia+禹|RXd6MdW%IiƓÉ@@'zǶU'_t_TlChAvNx`/~Bg?}[7Y#5?+ڥ}{߳]s>6^VNWBP/?_іq+ĥ`2+RN>< |)2<x!iCN@xϿ7[GAP#-d9I8W* C\{j!,Ü8m5.W> V?Uۚ f6.ѿo8d}hّgOľLܹsU'sPu/Nw;u3oltj\|}_η> *ICVv[CxhG3h=ˇ˿\8b+|Mo_w>- ]ڵ~ȢC`'" &)nDA|w-q.hpS':;* MHCOlE^4thm°*PV%r1* hBDɐS ڲ h$K‰&}Bd;j,{>͗,b@S)n2BCPJM}.>@%$%:3cߧ?VeXuWU?;ԼN81tj;u4)~^v\7M̵xeKmȵ_nؙP[_~ڿ?;7w}7ٲe ]#{䑇}٩1{?v7zrd~v-4)駞ujàԫ1NPcKO/Cv2GM7U3g|8mBS'0O}gԄCg?`;츃+̥^? r| =Z@g ċler!GqE N:N9G5j0DlpΙ?$DYKl 0kpYNhG /':YIqN,bf:T~?{Fg'K%js{hG$V{t/I+eTIvٓmђmE᧶ϝkr'[XZ-g%B|яxQo{+|)[m]x(v[wκ:?!O&Fq*OO_.?\E cQƿ&<߇J.iO^WX}&biV7;w&zfT=DWwU :^Yá7n8ԸJFhM;')ߏpQ "8Gh ['>*ߣmMUE! )80( Σ%Zz#4!e8N Ekd7:H_8Z<ol=F#xr A56kƌ߷O9#Cnm~cgh EN!# y :t? e[;bT9__Z?b;#?q|i{eO/+Ptd|;ORNIv)%('J#C8yE۔)/7ia:S.#]JokWۍ7~2Y}vi.wuvtzY95n2Sw 9tj_ZWmnJ_ώ?G΋=5ۑ\R_YοyOcK.ҞՌ9gC/3iG  lC?F??MvƔ`w5/9 ̣0_Op'%Z8|?AK+ OiBZ bf3*#TH$C!TpBx0t'^ 8/K!i@z q9!<[5C>"BQE¥bkё""ꏒ:gÇ..,>JapUu_xV_ts^ˎ=n@ڃw8|_y8}ȁN LhH=\Q#Op6~i{druYo_G <+G'O_l+/†Q/b{e&5uu?-+~y>oQiwn~ȿٶe1`yJ޲ow.җ\JTչ?f1iS˝pqoSs^}5;gζ( zm5ʿ6[׿'_Mh?u? i?b}XߚZO{-O^?C19=WU܊1WHlX*9O /C#q3F!,r O@(e9P8B.|:J:'fxRQBZ. OxO(hK<| \|섆P6#wQ' aC2)tLy<.J9k#UrBqxiܹS+20nG_N5׿TwAv#kWKsl]?c^|1. P)~s.ްܾp_C&X uY#F;Fvq_lc.SQmOl sd.ЦL}%ʟe8M7}z+馽pppSNrG~a{՝\<㓁yfcW z'Ͽ(ʻҺuhs}\_4?57ntة'_oe&G~zaw(M瞳>m';v{X{D}PSyuǝvpIa:߭tj5~-soɁa̜[}?<<3iXOu!Q,wJMj?xfژ1c )hKW\n;4/ȦN$1~ꩁ2-o=dɒ8QƇ ڰKٌM۷w5aڃ?xvꩧUi1Ņٟ}+rSVj~8?@p)'WO_7~BaSaZXRT|*Vab_)?^Edt\^ c14 D[bfߺFMOӔWFŀ~[;o\ɫ2 4ioZn*ZZUߡ69گ’'[ATe<>%C4+_8Yp J!ġG+o^@T2 #8t22Rz#O!ͅ9xC\pA g6Bhȓ w9j+J;]0_LM)h}K344L!l̙>Q7Z_wP W՗UkSwK?_Z<+`ocO;ԝ'?ul2l6/t;~ۿ̳KnA8߳GOߋ|>_B8qZ(嗻CLe4蒋lPs89U#PS/|~~i߇#Ω?l^vs ;iW_ ~`=X߿]s5s?9(t?K7|s7Rn{Bf[ҎjKV#ؼ[6lƩrCy'ͮw1g\>'4U4ҫo0ߡl>ذ%Ǝ?/ZrCwq?i@FS!K7 GߎdYNt! th REI KZF %N!d,2_B~48'Gt'%8ِmٮ{G9M|ºd@\FZJl$cg0"h1Al8H$#ɱӯo ?[?U1-m뵝0?k &C{Ov3iJbohKfϜQ'~Grҋ/>ޥ־p=Z`G'asI{gknqk7_ eM 'je:ںKOŝ>}܋:u|G?mUi77dlLH:C.mŝ_/z[TsyW8 S_{~_٩Ƥ?>l?.Jz%pbep"秭RЫD*6j #bQ"G ?yB'?DAb?jy_Zc`4)Csw O\yXjmϼsc{%hgǝRKqtQrO69s;(*Mȝ2Zp/L oGHK6qz '<2=8K@ (@x<@VąsKq<#B)+2 %Sx!l ܡ OLfIwz5zO%e*D"v,E"Dpp9g 2?uÎ׿_v?-;7޶zlᢅvwڲe6;ٽ_^F^jLd?l1_x^u]j[la =9n]5b6nµoߥsWzmֳ6ct={v mmʜ{s6ߩky~k)6vg+s\ӷ6bmG׉t_m;SC)㡇;nPi u\]sWfM~翼7Ͽ?v\[ywGsܡ3hHή<<Ǝ;j_N"M! 8<3QxCF^2Gyr'q<=.C/Jn;VgaT9 *K @|Vag!PCpE Nqxd!|EĉC /MxMn;_qj"tPD>/7BգpLjC-'qoO/~+ T<(z˭k7N1\'֦RNq? _B~rڃMv0|і2ܖ꿛GYv+}]w [d1~٭,oK4FYTzluwk&GyKn H_+^G(32{F6h1c뮍N<&Ͽyհ&{Ms;I}0CIߡ cرS<Eܐ+M3Xtt˱3 9#У‘@v/<ɂd.+Am4ɒ`B)R9ci44e(Z$J44"[<Gi''Zl59jl^&MXb,1"7Nuyȃ 4N3yTM|F fWi @П5!Yu:{ؾc.~3[_l@ާ}v&ԙ Zo;{vMѿhjӞ={'> "O#l NM5o.˯Jm)~o?Frđ Y [5oo@ym_Z(f4/y/=N1Bc(sCmGgG ħ•JqMT<|s4.B9G/CC&O A8tB j 4 HF O(CJqsܛ2yBD6i|;Ԛ0<*/Zjj1ߒwdmST<smˁZSRz؃>{4 RQrQ(޻t/b-~O`vٴSm{vuYݱ:6W^qE*7ҥovY)p$sWMdQXRyM,Gm.<(ywl 4k]?G>}z8`z^|`qAFə%?Ee,;t%_E8oqx|I&{GJ,]酀.0!SJU0Exct.rA$Tf}{;gY(J֮~܄>naz>ruA/Od =5\.&|͎`}wmÎv[Ccwn]ݰބE"s? Bh <TQU7(&,X}Tpτd^RWCtOrP/^T1T[{O:XTj%ut7yO;'Ͽyi5lr9G~s=W|Կ"qqhܸ/{x!gd{4DS8Dęp봙ei!d#]AP(^@i! pmdH՜ pe*d/ǡ@'y P>yЋNrHi !4qRY15yB(n67S ?Mb13k N ޲O zEgM=|o_?USC{Zj0cпy/;! yc=cxg;hEmV==EozmlS[~f=ޠFZoɠuQL Ҫ>U? `O)?0\AZy/J$~ܹsg2Vuv|I9?6œG>1 !tqE#>2C  ^aY_NxpU#UbdH90T3LihԅLÏ~ɓ 9d/x W=IOxwm'& \g<~ASţ}W 6;>"=’9DqB ֏d5.:_뿞r{=`ѣ8yMx9|8vrg>ϳz[me컯%ćs|;bU]q{台./oO9TmڛKؕW^Y)FGa˗%\fO_ZSN>Z*tw-̀FuǕ?WO}!>xU<:^(% eP: 6GH[x%8xɓjL'R i Ge|SεR&pqdk_[O8J ?=G |uŖ)*PZp]bcwEE=mѢEv7^=?=GYN]{u߬uzvz+;Ko\`{'82wL32mݬN:&-O~#=L[p\6vm7sv'^f㯭bS;_7wc6/?5=Mq?ht h:qmWZ:E>8$ܽ~BmP茠pԔ:*t(|^b:OY*'vD—&BP5JTPTN#%Hd6'RIV]cPuC[mˮsO>u']w.ij6tPk?S5eU JowFO5gxw.RjP;V4D;8j ڢ qQMpQv[' Or g_[F+AA{oeI{֟//?j ?yYgޜA~Bо5?v3lذ_<*bfqW{2/Ja1-㳁V H'qtK8i[8xk~$-yRb.8%r ='8t=jԨq48#ppea.m2t#=7cqjSj7l{vv]/>(_>ڜyE{>dyW{_m7#yO:⼭ޢw>Q'tܬw>֧O{s9cfVUge ,,Fwy`iqڶ?5φzP<^[a3ߡ:%XoUǰjEs1yq.n'd?M)83?*CIC#g|_xr d\{1 ǀr lG g*CͨP  ލ~"ȱ @Z<`'>Jl&#|MNNK&o2=#ީ@;gږgbLK-<@z" 5w%G ZEk^?1k7~{v6}tH[R6_۩'o]{^o[ah>;/\lMM2#QZcP7Fruͺuf umI?o^iӦ'>1=9:j-6ljvoZNG#IewՆn;z95sB'Md㯫;V)[8}߻֣η9sXNr[o=s6Oo3Nטm^t1'V?0a0?$Ukimc,l<Wǿ/y[w{_y-j ?yOb;p |Z jػƽO_Y؁FMcU3#x.|3Bn HÑ  (Dxe#+Em%$2. !Wi9-9ĹBfpy9jPy3h cpM~!Z77) Mop(Ј~cƬ߷?YJIB>A_Ua { k[+Klmӹ[w^(9m~ ki8XBI]f= .]Ɲ/o֧G='xõ@;#OzsOeڣ.ډyP{PNġ_d{jrJ@;lyt9o̶;Yϲ'[.$4"A #LC#2=Ǖ(y!F)5@ H"e~ _}UL5GB?F~/si|H j/Sw_m„ ARi?$I|?&tBW6Z `ɓ+[uXUʟlZT? wTyC5Nw%r b_(+!O!mOޡW>_wɹYO 8B.Sy4@t EGr \҅,d82^v@x@e@ T(hP d(5}r²ŏxXdr"[/6[g~"mN6u6gYO[ݖ7t.KRUO-mXV[' W߯ϓW6JXvҵ;햛oi?CvGGzꩧo} 51f mx>pI\K=sα(\)cjƞpLѫϱۃ=hSNGj:(ҹs夓c~ =0gDhӟڳ80` ;cm?/[Fي/K)D(S)k׿[l8yi$LG@>m3;#mۻ}5ӟŃO)Dm*/:8^J?10aFCTPuܼUTj9"<{D!DH_؎y ^E]a kH*KBa& 3WsA%eSqa n|. z3"8t.$7ݝ$筪v:v˥]L)Y?۝* (;-O5=j0;kjʜ7B|?a>#HqAc:Bs>`Gr#@qBNC<'^fLeey44q@ӑO]K^:,iD:>.Bgix3 !ٳ{Exﭽm Ł'{XFh3Y(à 9PDm \^խ̈Hr#CϬ c?l˿k􎬝ǥ'n٪U.Jk i:IKhY­]X롵rMkvz靟V6Xqc^xw:E]q@Zcjrц{Nцn|N1\W?,]WZp`-ZHTe9igW>{>>?rD33}9qN8Զ~t'.$ ߝn]h´ދ\p;ҭA2̽G%y'Qh=8OOQ&~yN{δ~=K@IDAT;O[iO?: o 1?HұSO\i^?Rc}@ucY|̿yRb?R?Fͦuo_߲<1Ǐ;x"^!/4ynSYbm6=L >#/'$zĴ3v_3 = Q*υrs p#ex]8vlhhu>@<j@۫|ʱ;Ct5nhNqcតxvf3.pyܫT51cl[ פS}ʾif]w^s_Z聴z+ҚJ]֦~9״+YWͿzB f2@ !w'xLhy: tg^hK;i\#  qIK܆Yq"P;րNy Wi[mN8Ӗip\^w[p ke(2(`}T<޷fO|2-YPC zZۤXj19֑)hKwFc͠iL}qiǝvN[=NO_FGVkҺk>]kILkvS7f>۫VNrKR7tS"?sH,e=_ӎC-;^WA>~rOkgqFzg\%x|O#no|ӏ>:dٶnm:#ӛ':7)Lu$믗 E6*; P.e3!vii@eȺg}wA`v Vje]reP >cxT+ Xy6ҐRT8iG>1G25g/XWɋ?iAǦ=1mݗ֬Z+áOњIbpP˙ZqUW/+Ҹ#vOI|նV;f|%ciGZFß;̮zy e| Cq5v)v<ߧ>映>'z]3P{)JK.}8$Ir-Jr/| k.˻rYߺᆼknh]YܾqjuUzU.tq3N]0w9}WD]Z{.QIŒv6ǽI&ZS__?:VLaYSE\?uuQr+M25Gs(CnϿ35r!W7K!# -5qptp>G|#2E[@osRqv6mi K!ȲXBVD\6Cwkɳ62|ȵ32y' %=ކϕM(aFDڅt,j@a% {`hm8ԖI&l[#VkAΏ=7}9ɡ"q~T[rޭ&{V/]ޯv-}EZ۽`?ZnG Iwy'U:Cu񐴗 [M{[Kӟtt8G>3'|P;T&[sN:)]{qDž#?k_c.]]8Ԏ?җ`/~2Ѵ/(M?@(Z95}k^hx ]#4d\J|,*SE`;PCxZ+Xre:WKn=Mg~z,j?iC_k%ڡ6#_Xio;ϿuO_G]ج?)ڡ֬=ng|vo޼yw f|"|~|4ĹJeHsQt2oWI;'$߼N#"}GفJBgc0qe1`xH;<\C# @X!rIqeCXy>a~6W;ԲtZf^jOI5űo-^EUr'/^(dNW>J?n$a7Ϝ#28򩁾࢛.|>X F (qO>(y@IOYiQ)u' pb\g8-ߕ@8!4v!r(m,BhёQ{˂;Ԅe}o_/6wűӥAHrP7.ɖrHf ͮ#CGlPG/TN>=n֑i&vxPҖ_ZvmQU8R[uiO} i/4wd+t)'/|Koۀ◾8WčW}+ ߗ~ۛ"Gķ\wv됃^p~:CҊءvZ#;SAyZ玻jW_7{񧎿c?E#e)1ks X u?G>y#/5a7cƬԭG5P[(ɨegG9*eZҦψ4yeBs=Gȃ_wE&p^'>_ӖV^ Sa%-@y9UډQt"GHУ:gkQ|9}KW$7[ak!Zːn7Pпhⰺƌ նoCl8siƧ]&MNC6qseZۿJLӑχδ{W'>WU}کַ6-m|Wo-O?m= ?}sK/ӎ;3[c”Ow~{:l}#O>@~P㉗L棑B+w+xAџ?'>2h"?8SNsЊ(G>wS׾n~tg2_vO'|Jz Kt*mwܑc~Y̗DiJw3ntsr"](-o?W\#qG <#e~"Nzg#;.嫤M^ [? t9c=:<,N|[翨?Zi3c ?ݝ52 V4@>Ëiq?j49Vݥ{c ҭ|8]vMf^GLc5X>wѪՑCM0|G#ivN3/mqG^IvwjɡFs8;#N?T7?Ojei|KҞ(%@gG[/^ ^ؔ7W(Fy%.x>>erwCmE:է<(#7?>Pöx%z V9?QfHr,?2lWQk/ 1@lpь!uo`P:֟uUן6iD\,ӑO=Cml5m<ءv|oV r8HԜ3X᎞@\H! (Ύ2+hr~qI++ “Yɍ@4qBm41+ VCyG$WU.~><l#k23Dhё&F7ƫߎ>V#X:릎?uz uϫl4 Ju9&eK"a[8,;55@P[5qLO|EG(D!'Fb 8E<)2r&:cАo8vb; HFgf;s);ua$msp)F#xY+8 C Bz䐏 CnX?ұ3vs"-ꟴt 11é>~Z_##UwVPvOW~?-]6n}|^7}N/vU7C?J޾;Mz6}zZbFooR#ڵ2ln0ņc|߽K? Eo러]j"=1ݮpksw3n(#Bec1WGliT%f~+V=8~9cf71e; P45(pfvϜtF,;\2ۅ P(\.q{x_TΔ:J*_8rF aQ¡֑NJolc/Bnv(lK>vMNPɦGq{xMZ}ޟvl*\6~?-Q|ݤqU_MlS8?9?]Ơ:~ylTSzhǷ+ӯ+|Q]Un _@k]vQTaO%J_l ZvEҸ;#,tgNYyWͻS|@Tᗉ&N,;Hh|gO8Gȥ :ϡyGW_; F VF i;\ȒQ Tx(ˤ% rI-w+Nhm3iۡh+`9a?&vA$d>Rw\kmrBq(.uruK.bp\ GfB4K;&*釷UzDBrF?,o[ҏva)/\{iݷOL5m*(DY~jw@Z.'<},*5G^4X8?Ow-7NnӁHW'??Qɍ0*G@^?j1E.̌7}Hm? ׿4WJus2}哾=y7S'Iq97|6=pj9M*-]|CA`#;lrs@?H( wZm< RE8s)+n>} _ɣʟP?x,Q|4fIsY-'2-VGI{-uD!",zVˆǨDAܟn8μi9m#H مBӸI`$#R-KdcZDH0aopܣj !K߸pky~keKZا \&`ݺx$kNYq4nFğ~r7*1jѴ:mO…s.sPǟ:?uy?8vm9͚9#ϛ# P 5U"XCfR*Vu*F]kW MƼÑc E0X_gR#Gzkwo͘ _cwWXzYTPRl䩭ϭS~5ϟ7oj^d9#ӄ\<`3%yռLIy\:YG ͟yEcY<2Vi<"pAnXv#mp2,<# 6tX"zE>@Xm0R4S czszC)iK|~U_S,MI11Q)[W\.4n6D.v 5~[t "ׯ, '^gjL R~(`*cY~1qp|"6K[PcL~kzPԔdljSǿ: {ُOW:l}?ydi֬1Λ%;:ߗfN e₇i!ny4TdGel7 = 0T)9/JI+K}ZfwMI֙6i ][!hCm,ȧ<܇,JacGa8 DsoCXvD!,[/|KPG BW[Lːƞ&u/?W^S-Oʅg[ߺRϿu#(:d}`KO38Q9CP+uA.t\3MgAav@'^pE752& z BhCĥ pbfل688t8謓4?,<@Gu48ԦwA25SوLO %d**)wح 4U,ёON8!MΆazo) b(BcE[l8o/t?'JG>1 !v N|e*dA_gU7@kCdNxeqqۧ@T iG'3ihLÇgv&/x 򫞤'}uj^P uaOg,X^uP d iPyޡC?v-5%x:.'w8( ;Ȉ# ? e~Esqhi"nK>xu`)6F@BO%:>縍: Ku^%;bZ8"i [Q_sotcˤj_[ܟp? %zDtPl J/ MJB[~|kW_5h"?KhbK"OS:?Xcc+ZetS#K[b'%Wb^va",Be\|N|Б/ߔN3B>iB4q%[8v8=dH#x4`oq.$iASҹ)u4+mw:E>8,ܵڡԻWtXlHSb9\X%17(|~ɋFP TڡvO4aDc%HV2ӟm%M/ϝ;hm:W_[߼?uٴ[:n۷/L5:`dj9sf"| s ,W,s\|doJaalz[8i[2xē?,xXL+C1 4G቗:JeGeCC^)8ب |❻뚞}QG h8#qŊBli=$ZrRƵuҪȢʧp{jb Wkb:oS:nw[:7l~Cv;ԶW1@CM|ls%BwF gvn"4G%/x Ɠ8ҥ2|xrlH1c@g-<.ƑÙn3*8xB!w~#n=vA6i;ؾ%ۂ^29:O,8 2ĠbKD\ʲK!c(@*gC١C-j١Q Aԟ 릖__3VO[:w4:'?Yy}bϿu)돾ơ6U_ܸ8PY.mw;d_'CW!'C`hJx8υoއ88)7i|8vqxRf"z X + +e,B9i9m9ĹBfpyӳ? ~2K9q T P;V,A ~OÁ'.Z'NґODNF3CSJYi{KZpU5`R_{ ?C?[̧uyƭoSuQ_um {].ӑO7U_ ? w񩤫~3 _Ds4`C\!͍3Al Ւ`y96aLg޲ЀË86̎0 JzxDaz;! 8t\xA8Pk /V܎ +S@T5/r sDΠ%K">A%ӬLv4hWZX_Ln) FG?Qui*Z)uoR}jy"(>[xjyޡfNUqH<yΡi뀳Lօ,d2CB @\p~`!$ -|BMIg#;IwHApx߲# tIK8/jk1ܻ`A eMseyyW h5u$ 6 4s " A[s-^Py܆ ŏ(<6Dv_뿶?zm(W12~j __׀g:72"돺Ϻ?Ocq>9 =aw,9Ԕ9oZYAa_tIs7Wf N *J[#n;&<[P@0-bdJjHv1+O ڮ2#ZkDKZ^D hdGoW[5ǿj/ujaa=οu2LCMl|2οzBu.;4j8t&;qBCHĝN3o:tg^hK;i\#  qIK܆Yq"P;րNy Wi[mN8Ӗip\^w%-8Ԅ]hB^F& ZN忝d%'Cœ5O0);ux5XGJ qhKwfc?Hs+v2᪣>j/~?`>w`$o[y)jdci7OxЀuO>T&StPoց~v%jXd'^ ;Ep=8>;|h K;ڳNor/e+98@8Z1\xۙEbFy7ٹJ;ʠouo~2ʱ BRֵ=g ZwbϹa/bS+-c#;0!'d#'2#ڲsWH)|*|\ӌ~dmmko__iMA۰jj?uO\[{ٷ#]iʔɭ5X>3ء6y. B;bYE|*]'ȲLі.d~l a);H4A dYhb,!+W"i.!`Xn o>fJ_9 fϙ/G=ȧ% UAp9tqs]Lasl0tH]xI4iVѿ_oWj?jyo):)h{h!0T#Ư7NW># s1 q;/Tw1'8,i.ďCa^h|Buq<;  -|$VR8{g+ Zx.!# p=r qeCXy>a~6W;rg9whZj{Yı&RMqHgv#aEЃ‘ B /^IXGe TƲUԳjWǟ<̱oS:wu/ϥuu}')SndWMSzfs3gȧG>1.,+\|)i N4)QLX54vzqL7[S |BG0b+i;G 7-uƍ3/iWJyBc2-!2q",hCMq+6BJvW q5IS\gasV J ,)p>DZZ? &VCZWǟ:dt6F_ȫO\W]W]=z_{|.^o P3fnyLCmh'>.;8iGT/В6}F+/nntD4|r3-r88yM[֋[z lN) VVN>< |.2<t*YF :d_t#$|i35(.5?ܢ+ MƖAD.6ZEc2̉h (9пhⰺƆ նGPkh蹕oDOч*o?ui/q6~Q:g?uοu֑׿:?f̜P/ӊ'" E q1OxC' usPO ׶zy%i:?ca4P*sEp˲\ Jz68h4hvn0@IDATA[ʀƲ 5]iBd^3{_6ݺ`AHK"I DF 7f5DDrݳ(O#xCXodl@+U_?P5W^uFQMwmW:?9uejYڧ#zg;4jwiUW5 7Cxb6N,` 0AczBS\s^T҄E>>˄0 iiUeCC %ކ#q0 _'CE[X yMagrЂ)'tJOOϾg&+r"k)$QNH8҈+N>J0e@3pwȬٺŸcq<~|A>Fآ% /y |c|x6r 9Mh1ݠ@R0…2\B'I0?/dCq'n9v9o>pfvϜҺu١)#.T b?"nKԙRG؄,4 (agN up#D>ǵU E=lLUTulS&RumC7[Mx돺G/j١Ip,?G`S }9]5NeQ˰HOC!yvO@hyᓝckpGȅOm9ϡyCo_; F VɅF i;\ ppT=q>eR`A.irW|섆6 {ca |  F2)ȻrrςkmrBq(.uz`Y|@%qI GK@Ї݈,98˫kGq3::οu1_]Q,|GrϺ|?+ NQM?82qyt>4o!c_f  xh8x<8G4[AOڲl;8x 700̅Qb˳w.h1BsHE~ dBOSmSH(L #8t6.BG C;J>@ ,vg"6\3g?oW>h`LS `s:h,Z/NL":RZxq8&Nԑ֗c?&OG۶m]߽i^O6-;\~߿M7Қuk:njy?3!O:$ !arEyuǪZ3QeMa4n7\T7uFSǟ:nuWVe}}ܜeW4>%/~qXo~3vQ ΕCIiʕSOw9oʃm=4 MCi_뿶S:PueM@5×E]D?4?#>=3kG Fԃ=3fb+wޡfGf7D7q>"eˀ+lO>!`~6>B ޲#`>gߏuG.u7R@cE1*Jc-/y#B9v&d@c,:Hp6\wj88򩆷NXTojO6_V*d̅hIbCFLZ" 4ќRw{-P7x`:s暼koMK/4k1P4'/t vQo~sTg\w?t!vQM-߭pZU_u:񷎿[jojNkġҼ#7P B|A]4y\EhR ?7o( :7|33_m!=$<0=¨2W+A':<.BCpM qxl!G>GRoooA&V~}Ldv"5tPD>7@Bopܣj !sw /Ko7ܸqhs=4?2}nw"My(vu^yBz _IoKާJGudzS8R+ҍ7(l{鐃I\NcZQ?nE}gSʟ"]k#lvܚԦW1hup:LylF:϶1PءW[bi@f͜ͿKue(pލ|q ~i4wơBzv)1-iXe8m  6^:n 80ִ':Qp4I.E#my'?rK_}~[5 ["~|`!M?#.P ^%+ 2_+NG7"W>5U-U~6B~әgI«w#-q͡qǥSN>U~-KOi =aʿ|_tmHiwHfL?MGen-ݧ]p%t;tzwXܰ5iO_]#tpaӷoAGp1ZJ;N .L˗/C2OD#uBgCە7ͻOG[k6bW5Pǟ:F>'?˖jNJηw{vϽǧq¢teHӟJGydZ#MsNє?1uwKrRzs7W^"%ngHìTHumoWo[^mO(ƣmkM]񏑯u#}4Ϻ-G>chkeo%`V;*W>F8` >lThp>8xHsAkiy"to"Ne9yl̼AB9 a!Yf0\pB˅|]v!iEoCޏ,4y>pjpH-1.7KțβJM9@"t^\N+'!Ϙ~@k0]RpQ-v!w?eL7Į2]P}ֱ鵯}Mw@Am`WL3_y3r6n,JJG³Ȟƞ%PӇ?e9瞓?L&%ٮ|oO왓fϞ^c[_O~L |l!dѶ1nm=-]UjGC:!ߦ_񷎿oY:οuݺoTGO5(=hl3~HVwy48HAnXlx'ilG?dhC$ȀN1T:0+g#oƗ.#PoZd'8_W> nSR O9![7Jƿ5\xRL.rAdU"aE 1}z-H?[ّݡ+tlV]w,8_C+駟nӧOO_tw%KM]|Fz+^.]_z5%oMO:8;Ű&-]&0cB?f7?=7~qPٞ'e:]wsMvXq~9tӏb;֭]>qeN=︐˝n)*'WG[@eCAK7h*OJ?Ϳ-iB@! ͿoS_uYui?3.ƔŞ-/'g-|W+QG 5^8.i%+4i1N#WC8 G^. E[3?q!a ,Q8d(%]8R+ƼY2J'Ygyo;kP۟LV,1s)PPt!+R8}U<"@Ae.CM%PrC?`zt$4u~Pt4`d~D(ސ~Oޮ/zˮr>C:?ut>J1YG d>W_|Hj>V}r?ϥǏ3t≼?m#?'&|zޝn%/a /LJwh?y.(p4م+yi8qvT5^<-B /`YXW44·pZ4\.):4ؐ)9 pbֲ m qqptY'iR'zY.xttIiqMkAejP~o&aNc;։jpӊ?C I3IDG>3q@i;ղܪ/Ilsn3G7oӅ_n'+~8>6pQOKo++4jܹıMn瞯t7z/8:O;q~K/JWe/{i:eGWkW7$?GQ\r%Wuȁ}wK;p?ҷFOi{W [Z"Fr4~e܀`#؆\TG\|du(n̨\ k˭eC_f睇zdA_gU7@kR_Ig<8:~G)aa:SJJH;08aNCK@/d'>g<˰c64x[qd_$M>y+P'vjZKW6{J=O;Pc@8P`m>&4fa)j蟤#jhvL˰q1sf=ID_9܈G)?Jqmo{k:򨣵C \WWQ}vj/~v=4t~8G4S:פ| uԑ,}\.|3}, )g w^zSUM:z}S_(([JU7u: 0mxo6S:s4X c=:M|7'^z? czFQpBy!> #nF@^ e~ 7y_OF/rϼwZMѡBIk#l $T".3yx; Qgyil\-z#vCmƄ$l((˝ĊL2N7L [ADòĉ:)nSBTRp["! ң-wI5өV]秓O9YuП~> ˯;Ԑ{\ԇVZ^ST?G!l:g>1ҨI9MSQ7+p +T|+GY9sH* :Nէ:_?3m?LZU[ߺ^LMC9|~KwY)OgMhewz:x_뿶OyԨwcno[r_:Pu}Ͽ˖ȧG>7˟"{u+(EhVSg븒# &#^|7WB>xB4q%[8v8=dH#x4`oq.$iASҹ)u4+mw:E>8,ܵڡԻWtXlHSyd\؆J#;R4qFPnP'O0ITF}`ީ^݃$`R|3i]w|o-Ї{?.}=}WkӴC*v}ư`CmJ =խqM}# =tg{Lzk_ڱvW|S .s TAwޡ&¡9;Szʓ=A7UWg۹o9/񏒉7-?Woj-ƍiou_vQ1j[8*Fuoi_)SC%?}[ 4W>OPp1p2Dž ;Gt9Y>>!x@{㤍o|O`a:2 \.x'^ (y q^ y\H`2L󉓏\[𚞞}5XtHP;o[wv};ߍ+ړtpZbe:Ur ]CÏ g#rYK.M֔ξCsw_Y$lyv(*i)_e нg}ӹݶԩa'? .ρS7UkӸ!6aj?u!?~돺j;F?v_Cgw_ϗ5C > 83\8Cȷs %pgATG6`nBO]1ߔP ѣlUƷZ)GnjV_:z/kyS?:񧎿ӂ_PlPwA|S3U'4Oy)ol87忐l8D sdh7Dn5TrSFt%#l6#/Ӧ:LC)D.wU~qD_yBzɋM7;Y:niv%W;]8]we}}rZv}=y I339y޹;Gpf9Ԟ[9vW>uO>uzGx:{ Rǡ5tV|{E_FFIn#ߛ6=Νux_E®_㡟_G+.2YLj+4j**׾jQS N|(kI]tqф>ҥ:tWu+jyms'9ԸJO E@|8mkC[nM\rq#l򴿢Z7F$ЭAhnrWS?#`9K棟j cNN4K:m}e<h t(LC-Oī 5@|ާoٹ G\O6 8!=WB|Oz޺?o:yrdMOd;w>yG9Ї\ v_/Wo+_??e-O5G5}[eQWY M^dȬtAEhTg0 7lBxOiL|[=>s^xv@i\҃  s f<C O^2\u~͸u;3o3 3 G ƷPt^`! @$Gz]mqD&(G5ڋbc-F09֖I*aK-La^JwOnzawX[ڕ\e)AH񷌿=ֿn(5”ᥭ>[2wтE?;ZCbԊ?miz jIo4;q&] h5X.i#>'ݬ=4-Y.MJ0l !W`da<̂nAwr%e7κ;uG؁t)S4{/n&lEb G˕A rrld@fB[v )++jԏL?cEK=?Z)#h V[_i@񧌿e)oYk`u?-i>M5p>S ء6c}9D_ avpla2q8u0^#eg.q¦, "4; ~lӽ+ 4 exǽ3H &Zx.!t# p)~>8,C^B8c3>6K;rgy2Z&?6 lU*N̐VD]W,6o)å`XiG\X ˊ?[2̿e-j.ŲZS>'N܈USO91|"k 8#vD8˖N&?t9nQ {Ay:#  󉴝c˃n͋qq㜗+YMyc2-!2q"j9jZ'6BJvW #y*#$MqcLƼ>W|2)d9NKW>֏:2 E_i񧌿1Y0 S߲(>eU_eU_/|=I|.^ C_-Fcjs)UN;f^ψ4eՍ{×cǝy8Lkgj5o#n= lN) ( 0 ]d4Ҽ邑i'2*~.iys>Bȇց>Zb;R {+ I,bӯUTH{k!,Üv%gΟVק|V ?WۊfA/߭V?uF2YG[O_;o}gg_mQ[NRdC O+V4Á  q8OxC' u3]ȵ-?qۊ&i󚞱a(T抰er1+<ф D˰S ަ x,ˡPݾm DӧwLacݳgNI LFh8%4pc_\ >Ȝ@G'^3+_y/KG;ƟJS+O(o^1*d}e-o[_fʧ١o N'"Uzejxt啱CS>fpHoաˎ)9v)gO/'Cqk^8y;|x|:Xilo·\4 yk茽ېpa` p ɃAp~M>𾐁 ơڡ#N;YZ,;\2"BR(sQ\,4oin:sU.l lJiqš LJC#̰sN w#PSRG*f26D)k2V̿͡e- <GYןsQ8ԺCMp<|J1uV1t^yJ~)i;fиB _8B.|:hgCM~ 6XbO.4JH3It -7rI7wkCNxm3iۡhMG.4o!c I7(" _ 2p"8n?i˲K@<0F5[CtC)48.\2  -n-3j̚5[j‡ 8yu$B"fI jtHYe#s7Ԇ[ ͡3Lj[/UiQ ir6PƟ2̿e3cbYW~\IϿe9 5=M01jq7ERL5%"WЗcjjs@]%ye Sh)9?J6vf9i8/__ }$jȓ~YOYQ);?Ebx0IP5Y[zUSN%AggR.;ߏf|k4Bҍ#qX xB/dq 4ICǁ>;¬@~\֫hdCM،ӚfuWe]6L nH׀c1%baDWA 8$C4|Oq??$5^gdM ƢiU!hQ*msE_U1UcM5+R0?eqϨAu2(ei8U3e14?r{FQ7>e_.+%]'F?p?TCC}6M28!e aӄ,_\_ɚ oWH[6q~xcM~pБK|@,cE1*Z1 oQ2 < eyS<dc,:Ls<3灇4G'yf*Hj򩆷LX2BdR*yT;K <(s!jI4f1h4PEsJAr;/Cmu_/VSj:J+oJ+V_@5'Mxt@IDATjӶ¡RdP B|A]44BܕZhv4{!?(BO Pi串\H q.CE4i4^m缶y9NF+#i֬Y!C`- [1`HDk:8|[%ǣڡprno ?Fs 񏹡e-^?s e)OcU?pP[OOǴ[aFdA0 G=8y7xNہeӄ P8S C ]dՏN a$FjI *T OJy `#q+D~K|j*Һ2/*iU/n e)[D+qYe?ɕG(?zZIt.7&NJJP윣`$l8MȅOeZq(4}Cm"Y90х/perί:|s4o u*__:2^)O^[ߕifG,U/Wzj2BemU3+ZN01W~Wiա3f/S]eg4& lkyu uAp p x{>lGghCdh#B 6]ภĭn /EvA=G?soho^N):{=J 4R3#ŞhN]b-0UP *,_J"̛?7ACrf,lx=/Th*K+?e-OG2VPQ_eU_/… ,WLӦM3%7ڿfv%y6/@`'qǑb-|ī.!=B 9N^n8>WZf3$yM'Y{tNsj欻ʧ8uKxіdrYC*|F Zʒ_oPaG_kK+?f/o?e)OSUU2ƌ۽V:@J*'Aw61>%;`s\ѠrĎ3n3;' X1 rHh`ChQ0ׅk*va-ތÇ :I'`:4gM}N=kvYa!7Y𩅟,g"c{֖nҧbPQY,|qf3d՟oE K˽ bVgeQQߘs5?e)O"^̿yWYNjap0W>Y~C l)#SN;(|) ! AN˘*d_*7kæ&8#St` P6Ҏ. %Nhg ġ[eرfy[qd5O$ L96jC-/{J=j½Lò:j@uJ(bD7 AW:aٜ.Sءzs#(ph_KW_2?e]WYGYGk^*?UءTJp3\N(6#g렸6 #< ~}8sǡ'GF/rs^eSo `k#l $T"tq94mmIؼ Qgy>Ȁ4\-z#v1̚}P`l38εӍ-!|mq`|Cop>qʧ%+ſj0PifKσ_J+MB?g ߆ئ 1񧌿e)OSXcy[+[]믅 ʧ|o?8qUJLNɫZ|g# P3dࠁ8LwtqMiF[s&DNUIWA7 ~pϐ̃pM%D".ƹ&+NfM۝N /5wvmPYJ/k6*)|^t$VIA $ ||seڡh?vc%HV2ӟm%/ϝ-;Pi+:W_[߼?eY[2hx8I;P=oԩከNxxej#Z\8)8 O:C LEh@ku~pilTo:]luӧC bk8#rŊ)慅0{^h{v ƵeҪy:S 5P SVRꌥ__?e)OW̿e-o[V󄾡F;j)V |.DQΈs n!h83?j?x!3tSqt9ƛkP`Ìn74hи3˕fT(qdCFC?FzX <`&%Pٳ'\< 04ȈAi"g(*ap) @8Rx7j8Ԓj˯4Dd]K+oh_k)o[2e]2Ǹ3+O ?VdrM)Wk:m4B~:q O|2tB|2 ƛFScx.|3}ÝGgqpMM_qdl0X9Yn";Gwi9m9ĹBfp/H_1}0g DN/Q5PeIHߩ L7? bNhD?6w~7^|:KP6hrԟ69[iUUEb0)5RW>e-Ӳytwq[CYWYjA[B~~?.+2oN *?oqTЕOĹŌą/i4hB 9q5qΧh44x>!jyS9BӴ~C 엩"8|,$<"A #|M~Kaى '.d,4q|Cu'޿Xq+֏:&{Nt"/9tJ]^a3(=oաt]IWw_J4m=rdzZfFj1"/ pei/tv;^XnWX"> рWR3ǬvUqc!ZƟ2sO׎t>3oG?qA4?/g%OC-wƽ'?Pt} [ 8x4EмuYGνLzkm\d,d@F1PCQhQc:4x|64C  8x/M^\G ݌FPۜ#ug͞FBYSY2ü+Zn* 6 4R!" CK)s͋br¼Cn -9FaSZ͔ɯ]WVZk{:-}Kӏ]H zr[uRvq b)_F)_[2GYgOϿc?<.yZN-^k?mj"v蜣v\N(E\yG/x)0qqH0 8!2W:m=Fv8M4+ 0 " ]yWwVxȘQi}|\5a4a¤sϧ;iN疦EрrnSh'vM 9?BK\/?58-j\oKϋ9ֹjmAkYֿeR~YW>PKj]z4s匫f̑鲃N3#`f<`Bw.O|[~ަdZix,%nì"P g YeD:?fܺlhv™Eޙ#z5!.-X^ "9N19Sđ-a&C ǎ﮿ZZҍjar-m%HV[ҍlar-ӿFv׮4&6$mṯi79Ĺ=\:?ni묽xYZO\bD`oFRݯk?}ie3O-IUЪݞLkqD%VҕhHp.[?f(h)/:H  [_uKiEZq-LO^xLCՔ Vi[MgPءXpadLS iiBz"x h5X.i#n֞uo>|K S6<CC4Yr;!hН-5,{YXGA4*N s 3qr"!W>ӏd] /ccF-gUis`zMcFN=\뮻';6y-[mYW/;Lk_j/Z~hkndCm5Pe{\W1y<לGqVA^9Oƪ7zGKQe?Sjm_\Ϣ9"M8GqȽ;UPj3f'Ct1 K!# -q8u0^#eg.q¦, !0f+i;݆9/iW'N3dZCe >Ezss5 5ǵLޯ*+cQ8uvpZ9J% 'Jtd. )T!)%+}Y U/W#^#73͝.e;Z\p9{/+W~t'M7$=O濰$ Y^oL?~yS;=ʿF9gcr:TĞq1@uY4&E<Gq{I9"-]ZȠ1fzWh4M7ݔ?:*2C74q,|7_&5B]/o_8poꧯg !*}LoH[P~Ӭ;L6=ۮN_g?>/7)3~?}Go٦<-ȸ_ s;czu]kgҜ_^7@{wߪ7rWwnK3>5a^+_Z0Y\??{+HsAk^] 4=qb|Bȇց>Zb;R A0MLJ{#**CZȜM6` 5է|R>^8PsZ#8,>|ti7a]0b-@ݺh_Bӗ^oK˷Һ?g/[CUh,;QV8 tߧ]/r/.qag.t9H~_Lc;s\pmp ~ɒ%i5=$IudĿnU[ӱ+KDCЏ~p%?&ghbH9[ޙ=#![o؂~ʿˮ;vܩ.袰N Gԅn}GA~}ӡ>w'Mn>lZpaNP邏UΝw_ ߽o]Q%WQ-j5\Vտ_k}_,1QFղ2-(__ 208vO_;\[NR52LxH:pxA@M;/q.x3B@mCk0/MenY6)?xMh@{J ;mʀDz [iBd^;}z=Kg)drl˃oe"^PSL'\ %Ȝ@G'^鐓4}}JW^VεFc7L|si1c'>[~=X"v6ѣzK{v|%s>7)-Љi٢:aπo@;*&, l'ON\rinU?pɡi9 WE6EWC |d uMޚ?jeRo~~wo:>/Pk;kzN;/tldWn$ jZorVm?q~tgv9ΎYtjXkWʪ:Tj?$ʩxp=LȡPkP@kU2_Kǿ4W1p_2c"jMYtGޟ?4P+# !Eb6N,P pxc~BS\3-Eie0i੫`!2J4s:#_3CE yyM28n{gMӧO:K|f6e%w<3E".RHlIe"# .=tC8t׏YQluxNGWR+}?maWO<3M?ǭw#I;[/鳗VFCXg1=?Kںx(@gUW]Y!4qƷK5h)$BvxJ;+eGdM%4iZN-=IΔ M.+|e8)~Cșܳ)Nֿ喯PlTUrKZ`A:$59nmӥ]ok%V~y''|"howt!/,sn](ϘqUzH۾zO;gtv- h Ewɡvȡ>gDG6uRӽۼjt9gg0ȓ-طOVECK@访R"`ˏ(_w9 ^smׇ+R#h@j(ѯz/ͯ?5Xʫ ?WƟ?/wQXlLpsPDu0Ojj:"W>}gM7.5١F t/?􏜞nGQt ץ^7`˶;4=^{%;Ԩeف+U8Fz߽3w~Ot^XPwZgU[[[]Dud7rsxbʧ/Gr4Y O;ڴ曧78=6~{,^Axϰ;癭W;Uڡ1 ޖiI5Wi Y'Ӿvצm6.G~Hu}o©YUY?^qX+QXcv.MlfSzo'>0t^{Jfÿ[i#9mu?_e$wcU[/+oomu|X?B;ԮA$߳[J<>ۈ"_}qWVRv})mAK˝MJ+OY?̴L-׿em̳1.p"$cC'Bxfxwrt?ӛcSCU3S&kc; 4|2%M^h!eO^}?A8r М&N1_3JlCR0…2\B'$n87BN0~CHphC8~fI;e¡)Lo*Be"yk QJ*Bi4 C Zƣ#x眒>ێ|BO}P;iޣG;v;tv ]o]AM޺iw=!Gt T+vWtI"F| Ƴ_b_jk/sߕVڡ^MKsvQMrcLT(7\]vp?q;l|Q믿>͜93dTD}kt ߽fXk^;}_n3zmկ}%O+ҽ}g|VV_C੧07/E8'tbX ;kGG6X(VvuHká'uO:;jl*}CMKH8|ӕ.MK_X~c40v9%/J|"C?C+M0!$Sezgwo^NeʏQrtAik_~7.J=@y*կ~uG+B/,]H 5IiR/o񇑧e-okQ8ԺC!۶PƟβOd:myeBq( eX"@!4;ČO UhpjA158Ɍ#qLs<𡫙o:!< VɅF i;\ ppT=c:eR`A.ixjkNxm3iۡhMG.җ*)<{6Z] F]%8ݿ`N8rdNz5!YgaP>{rAu[A>DnN;"{W>u{W;bB뭟xӈQ/})O%[CM0?ZA9v|KҭܚعvEy ]u'ҭ0b?~ }%U+Ǚgolo}6[;C Hnáf3N?]گ)j㰊u[Y"<}KW[tG8}#DWΓN*t\~ʘu(P3d1*m LU7_*_cն#h!J?5񧌿eѺp<ֿυ:哵ńI?XңZ fzYw~S'>[«t_-G8#/xx8<8n?i˲G@<0F5[CtC)48.\2  -n-3j̚5[ra"k(`W,$)4ڻY#qr^B|8COhԟ gCO%9'Mtt{:i㗿ȪKO?HzgG+_إ})_$9}áh|>jnѲc_w YgT5|]5eFt~N<.Pk~7]2sxSSyŒ6O>o{A}/s{ө;ᐑO$wMQhk_O?TV?_|ޮo7!tΝ7/G1h~ɤp>^A@햻޻I& *ޓO=3Ы;ˏ\?מ6e?EZ]C^2?4+hrh%aRѠ1l6pXoI WaZ!돼v`~6GpeIp^C#*Sr~x&492kYχ~xooD`,E aЈg1Y/|\\Q<Cp^;6BxنwLߌחϊ %4yLS `s:x*AL{P]zM~w+gf -)m,N!u5Ū5Gum/$.gM2ǧ3J묿^Z.m($-j7fYtizJӦ޺:o.p\xҒ'PSeq+R7ԴC-2_ݑӫ!PñA@!^ݼNfeqm0hYNPáP;XJШx[Z+q*HL$'p|z'+q=Pp t1vtOdPk'] gNU痤s?q^gi 7La|sҵ^t \,;7ء+ډ,?m+$E}d>oj.b}ﳟLӴiiՊ=;'vjY?5v}>[(?ҷ-i7^>яĖ,0o9}_MW| yq^>UrFo|9rκ+xv庑qj?W8 [U Fk:}W|UNQ$e9~FPPQA+]ΩxAJ+O?C&ƐX]믅EOꁈxXM'WhK; vta  X1H@4p<$߅dͲCOn<2f> Y\'' hy'+J]X6i(q aca:xqY@~ӬxȀi׵yg벉fBqE6Y bGA 9?r7.*􇤦PѫIXtv;^=LU= =. mw=rc\XS'Oi{HEh Nez/L]#iZ/]%CyXc|&|s?=YuUxW>ihώ,j-D[S>iUGwe#j|BU!$voQ ^5PQ]ʞQA`Sl.v6yh!cjCTvk[vEKlMxlc+4owvRҟ|0})*.aF)S>S>c8| J9r!;ԆKS&r^'?/|Ȼ S99$_E|Vى p8%Ks0~UP꿴?:C3n"0]&;4hfkο'*hZ0J#W(A62Vkg…1ġ?16In0 ZZ|> stl.0iBX1/tG~Ώy]n:? g<&?8%>hAɀ;`h .[G(<(i>Nm,0i8Ϝ6yj#qʧφdȤ#~HQCJ 2\Z81h4PEsJA;/Cmu_ꟾO5pa qeݷi٥T7_1"[#G. %=ZO}*uaO},-Z}b鰴?C \I\uUχ?nߎ 9C]oqZg03S=5Xze 3;q8Ԥ7]N:9 z.~Nz7ӜH88qs48n^ ο MoL{]v".?i>C-^^9ΔC}vg|'W xdfW>?]ӃuOӶsxN#BcJ8tʔt{ y&x ]_J|^{]{U^?/;w;Z1G7o%`ZG?\?7U? J[{+_->ǟWf2yC{(?-/w@5?ij^vXM v|@Z aFqE!Wn-p@o4gx#4/HCC/qCCJʶ<0?aT3W+s p>Ig!Фtxp8yl!|nYf@ 9P$( r5O?lH(V95_ơfȹ 7Koֲ^0* Ou_{DȻ|OVg 'ۿ=|/9KA9nN~1A1|o(YmnJ}'O$]~erxU0vD1^4:cqqAUu:̙~U4o)ʡ֨(pP樊wx;[hn8~uu))J|Z?LK ]y3NryՎ,a 6A_R9?eN3š*^x+}+:uU gb>#|~΍ߩzn7)P]}C^+١ҩ< H-}-dfǍ#CYvuVUMpnݏR?J zæ5F@IDAT[M9gje_2wo=xoC `Z^w0,sZENe9!9: wJӄ<}i4~}v)tp4qXe8m0 6NC:n 80ּw(8@٤m!Xpj3g.-Gp:h{:ZX' 5)[=|R^s|Þ.[zs".yaI]_Kyu2`Dz.o6( =>(PP5E?:wn]gvZu_[;g@V tpı/oڕ7drR`G%J_G-TG7iһکy~`?P?qC> {C-mӼ?}G//QgiP;X51n^˻f&b>EZTpOv8(bFV7\w}ޡt(0&owMƺ+voAC-gW~S5Pˀ铉|@ПOuZ ڞD'| gv^NI׾?t밆c?~N|-˵30>uQW5=]=obQG7oRN:\~v=o}\ux}SNY?߾D<~_9q"ShswܥT Oʖ5O<&vHkˏC{PFWukoEϿ6J__i&)Oao~3P@/^oQ]#]r0 W6*i88x5| ġkA`Y7v8?<3(P@!l\6e6Cf~BW NhCӊ`oB8 ~% 7p^^j6d!NraFpd6*=S>]<#SLW|LUmڟ<ӪS~9*?kY;^vfmfe;ԬE:h^|mP١w١%-)2i5~S'_"N'5Wl\G_ 8zvcƌNW|銴ZR@θ{::v=L[ä M_o]"9s뭶W&GU/~'yx\+y^?}.a?}W|ioԁn%oKɻ֚k&qw>L6IuuǝwAjkڡ#p"2{nynFmB}V,j+uED*O[j,CK])U/V=7oʧ ڜsUtΘqLyweY_D8=lkwCg%'4i6#/qH#m0|dX}6O6. "m :@X6R4kc~:'LE/q{vLٳQR ?׮xZ+L2@ `f RUc59(ՏVb/[ߍzH!u\˽W|CVDNjMq(8[PO׫;lsA T*ʇnZ|{ڙ={83˿YgmkAIc%]CHA}l\WEz=[Nѫy!{i괩JR]]oݨRn]}hwN?}#{8إfZ٢8SmR_U0N?BwtGH\.]N8ะ2e lx }M"h4qtaФ7b2!q|k:76GvPۜmqn,JC~ Xѣ& w *!u/ʧjcE~ؑ(WlU\ڜv+|5;~RkHj  OylO|yU<  wz"} v$r^{]9SFoo#?|ni:{l#Tt7?t fF9Դo2ytɥyW>og<*v@H%[ R*HBUbӕ(RԀ{'"В@H!'~ew$73gΜ33Lg y>ɠMDR G2ʯm 5VD0Q'Zsyхljc*{0ȧ@=vVz=mPo#+럶˭k_IA w`~w8=\r1^]ݗ?L_J\}VyGʛ_V$ʫdP{֡ccY ۵|SWj%wEA WvS*to2]aU]JR}% Or#߅\!Vᙘd^xT[Ttxr{FdSVrk|'? ;<)l TH g^u)=+haHZ`nPw5|L4t>>yf):7z b@Ǐ%o%x9jƑmP6!Q(l-gx ~0tЅLl"8.##<ć7hp1 3i$gxT|M$n_ HTS/O(zi3K[>!ݻW.E^EAM^Hɣ#V+Z~&AZ_U4>$b-gOiX>s  ha;ne{ﺥ]s4ɯRpM𞵽~3ձnZ/"Oʆ}>;fܸB?l7tMY_[Ru2l2;̩Z'eVf)8۷VL!w]g3r>v֙6+SO_~T| l[l|E#}톯"`)Ygi +|:ݺ0@.2 IW.vxuiA ,Zоsm'm]#$_~*n#s^Զz zy$;S?_Z1N)uX_E\e "_?AG>9C%(0}t׿n> m'}ӓfnycGi>R0w<;㌏(J6h훕__A;F+Dw:GQB#'޼Jw^KP* D3'Vh?aǿEFUM@/#u(ܹ,H[>S5g5Hf-u~}b[>1\52G8h"CaTf:u*ZAw{c .{~z+_ܶ/篡?7SZ7Ȇ*Cv^Q&ٚx\"0r];_4ZӺU4ߐ?_n(ىϪ_gkZGuV}z/R#norǿl36!濵[>-I?rfB0aTŇab R />҄}SW!txj2md"2čSGAÏHG\#ah GA u/.2 L^__!B7H[#-Q(ExL Q[ ޽SPbS -d(Uwwt}uփ'UO~mٽP6/na4OsX'G.h?뿴PC֦(^)-PlĪgm 5ũ;Sȷ&ߺP B.^Y k8y IPylZ__3w| b'XoM׷9e\JШo׻|231da*tq"}p#nl4#L^! # VpG꒸9@d?)"($HM.*LfG8d xZwV +D%eE) VX( N>}x#k*Qˠ6GT !pV0ڒJ׽oNkZeA~}ujy'}stObtd}wvHvaGmϖ,Zl~|}X%MUNU[l3Xt)م mYm1[7CkQMy(n/'-E:26!$ a'LQxe4I:p74ĕFzpa lTFГ>׮b;v"_z?G)ȞPȩgwH42dX(̘y[:KgẈ(W˟T[tW--{7ٞqѢS|W=o/(C*遐E5h‘pK lDmԡ{d9:M,apd( r`)bIY9Kh ّ V1_C$]~s62j10,KCloR'EEvՓO%zD%-\?\S[~]`wH[׿oWjQUZav#zN=[x7?i9=n|gӘq?tIW!n&CWM?n@#a8σm ?8fy>Ɔ3e?<ix2ϲ- ?P j!DPLBtAG+Wp?=  A ?;vltMq"C12"yƛ vxHN$%gu24b,`\~o6G)*]O~IhY~}kynDtU ݺ{>묃_98 Ńl/د$*ZOп:T{M/ԗ̕e }VUC1[T Qj4TA+jk;?\Z|mCׇ$4_}Ʃ@CS_&3F<)!k6r<HIm$CkQ:CO[LyC8hǍ%aB6(>a5D:yMH ?B)]؅"צ $*"g@H[.$4aOX"D2=iDǏX^@C"Ї3Ԫ|'f>!qtG=^PV0q# $]^a4Y҅dL{X,VտCn_S?O1>n:Fu)˟G_M?'?O73W[>eP 柣wMV*0q?Ɣ0pABK|G.?6EZ t.B[R~5w*&_ֿjC%4C5I_ 1Ĕj d|5" N9`P aEk MP" j(dX)˜ 8p<R\~l2a pQ4a?r }-k \rE, &Gm VT  YG8*' hy;v̘!p4qRB,B*[`I-4Q}{ے]mw2Gm륋q GEyoVf!5m:>;M* jm5ijG5YO={˖2+&ٖ|V})ěKݫ÷ΘFҶ֤\:g+?rпS=X̦LjS&OEOYu2:..]sN4-?%yȐa֨Lj,q2O8l>+j>}mK.Mu}U\G~.lذa2}Oټy ZZҿ.ҏznߣGb-dh[f̱N:;no[oۦ4E՞4[6jփlڴs:%sC>[*/shn'?dOf=C7|ֿ7g޼9}7vY_Tv*aƯG1O84 C!ceV w>vYg;\D1}4oٿyz;ɧUǃ/X&/~qׯ{޹.9 /ʸ3v{+/Դk6vفhŢO_|~BSjMnk4O i7ɸŽ;x_=G9BWmݔ_?mQѪmgl~%KFi,{og!^]Gn80(c1iSWg/yo_F$H$"7.T#Ϸ-X0xڶ>oOf̘n7mt#gIu޷m{HNڞW &AAz*BP$|ֿ-zF=//?D!7gnd?`@?ub]QZpK[>V \Fe$pр2.>hHU!~)\ҷ A{C+jxВZH .2*0t/\h~-̤j7V #Wƪ_{k@cL´(EpfPBI|".6є|.q'f9hJ;d6Co۫CmΥ/Zs:γWN/g7uQ_7ӿ寫E/JiK,O>iꪫl6g jgȠooӇ; xzmd*jcƍʨDWȫxܲ'ls\_O"8ЮJr-[b#YBH茵 "Ȉ7E-xȎ{D[`Ϙ5s]j"|ё~QۻuVh+ͱҢ-mqC7;Xֹn_l PM`Gكt79W\ jw'++a#GęP|ozh;(J=>zݼ׊s9W?{{sT駟f~7+Lj[EmQlG6i~N+V8 s,OnwmVӍ>>[{衇[n:g7o}jWlFQG:COr$jHOipY+0xBWG?zyڗTԻX] . jEs'>iv9O]|߷^~vG9)i'x_=p uGmMG>==RǍ;Vr젃O:˿H~Sٯ__;#K"|&M\t j;$~iE^[MOF7گmklrCٿ~~Y7xÆ.~~+oK9h6?ezǟ\~ժ-|o+::Tk:P/_[1{s5;Yv?k jL'v"qaO|w" y~."^(y%LzG^W# m`M,,*"2"ed|Ti @mJ aPJ& {ǎ3N\.qJ(0;N" N\J [ZqoLDL|ό3\~/m[2F+Cem=9l3WgJmluu4a]6PҚhK.mW2 `Ka7e屨;y k\3[ jl?q:V|+3z8իԿv7WsvVm`~Vzj2nك[ܳ|^Xbl mUUmsÆ:|#8O;U[=-[ƮˢjrcUW_%[ٗ, ^+u^ I~53r$~Z;2u}ӟiBl;1gwܱ1HŗkgdxJ.V<l8\r)XǶYc]wbL[p/" _߾2USҍK":%gyҺvblbsQ|XG wYFߺ.h7}_x 6}t>k˵5ݷ"VZ/w6u^v/tF ='>hQNhn&NX))ˉi%&$R kNbŋUp&%]EFtn\JHn?RYGqppM2B _ǽuns0xNkKg,3XŶl"a͖5XF5FVu^g f,mM */u^v]v#ev‰2%ʫmVqa%%67홒 6m<2۸]ߟ*=OƲ=LvK}o'?|[o[}#˿?s 7h⿞3g va'}Lx޴ l]I9OZO .:_=!?E[n%%o{~mO=Vҭ4 ;v/V` 5ZxEfFO:-ߥsTұ_蜱oxj_#.%XhM͞}[g?pq.G"yNJAv͵_>?/}Qߪ?mG1Y)AE8?Jc%Zc|+ !m]v] ϋRh8}qGMXgMHE]z]Zū(*%Jq!<1Y~Fֿr\7?sǟBujpV2|,:Vd"'CC|{72jCALbckF{ 5)R#/79Ax++Pu(h֌.w>cZηO~-ӥjS<対߶2;?z]oaCCu2]il9\`+,%ĻS{i[Ki[}^;z_TC۝߻L1_ J{wKN:$$IϨ.@޷d~,꿫V->O<]ū۔CK"EtkY~\\Jm]o_W_;)nno? <CE߿y#9]91]J0?1U9G(讏o4HPmtr JW Ss`&$<؂l2AK/x҂tBE0? 7Ht-dT @<0@4dp|<!% pG:pI1c=Rɠ%!dZ(G AW1 בR'V")ŠaAR}ܠt2^~J)ns#,Xx t;ou5p{.pFJߢ R峖ۂ'K=mvmk&-[+K,AlPk_'u%3t͛oҥ{蜳)f/?@Gwk]R1$G?z:p.̞&\]v.?noM[O~qXW(}I V]yp? {~yA|(cY[>KNv';{G:Ld0^+sY˰ 8 L7م(aPK \?q~Dgoy=OT+?T+XEFK'\=U~wv[zM+vyWKB_u_PE+ז/[;R?`x%Cuj߹V?XCs/X/V_n>XwIԌ-f(TMo'H~܈ 7@r2}BZY{!cxBh9LEf-R T(x2VWQ2 |  myU2= #ȇxBCH;aأ8ɷ| 3JƢt[?Qz71p H3=K [qDO8e ŧ= N5Xj-ݬߠѶZ}@FYg:#M5k\sʹ+mż0obqYо?Rtm{ 5Hu)v:CXr"t;on.%8̴Z 6S1'Q a6t0߆ ZaLuQMƫs_{1cƺ} iU00蹥'nĉ~'-ߺ:ԒcC}rZg_-c+]ik~?.!hu)Ûeu7P·۪6iK/[tO/o\X5rGc=){Wb >jv~ ݠelkܰI2ﲊ,?qb!]>~-ZH[:&U粀_h`R$ɿD7ĖOyj-v:yZI_n;ZFaxv|&.\8ߞSR[OYю`:Z9\ɡ_ҊmvF2PyE8ys컯w}6WF57ieÿ{8KnO]Jmbkއ`;Nߐ+Tfwm- tۻg|=OSMt۷jnPGKu0G+<(B|o{~_䩵'ndK7/,.+XD[O:ѹG< bv׵ n;qw&%HȦBk;V,Td/u%Ƒz5OUOTS;_~rݙj0:.Py_ssxf.+fU=CmQ=EN>*?q ExlJ} c0.h#h]MQm"%1C&(Xȅ( ajEhH?a,a8%. q3v(Wς-F:$Jd5l ;MK˵ZI3U##+e"tU2̜&64Tjx}wu9l[k:.ʴ:~V-J52 PM(ZMF[`-=:hg}l+w6j7w ;4_y{駽E=vd |Bi?O mt'F+nJݮ/RPS-y?J;ǖ,h$ZzЭ~6ukS~ӽ %п]@+F"*N;3.vEy|@vylymzǚ-cP[>%[2_09;VȀ;~o|:HT7%RO3!+8#h[bcld+B?2מ{y!2 jﰃ]&##7wȴ ][L1vߢ}ol:Es$R+†+'Tbᰍo6ڌ"mʪԩ鲂%eNtt*V|NuˍE:]Jj:Og|MgѪ[J $ce_nn?#@ܦFMb/*;GUnYLx2=:؈@81B—8` ^^ H.x H'o%[ `"äQX‘rFS,n]^z饶 NiӴ6;ﴳw±֥K7JJmf䫳gd_r@by=b;Lwq_?|O 5ApnecMZ .wGӗ_fv;]ƷwY#-:Ws;dS^zK:v jo HPZ&*2"W_|VkGkb >D!w}g"7\&ڎn(pqκ ]AMoE)?y?jD>'XNz 7EFuT!yW!@Ar渄*Nz~r/'tP+w&OR49-#>}m5@n{_[+_ܲ"z5N )Br !aܰa, 2: Dz6|@Hx'd_ꩽG G孄ƭ𓾜FA/<(Tđ&>7x@y^!#",Rg<2I3(ԸϷ|JotS)n(D"e&M&qM,g. jB.k]YG4؈m}ȸ2cPхZa)zYB[>;[iŽu6gZ59u}NU֤n;LFi#sgiZ7tĠF!nB dzR~VɖOAyȑK.N;/oW(6)V%T6|p "h zwZm7 ~\rjz)2i˧N;(#E.U[oOy?>닳`eo;BQj֮:{ɐjOx P'kE"Ԉ>l]z=5wCǍ;V6xw 0.2HՕy5۠/g5vyGb{<2$b8kp~A!:.[D[O%ȉW+ .~)22w% XHiBPVyI1},o2 j)~7Tr#Y~{w>VF&A+B3ԸߴB V.M+f>6_ߴ=R <w3{nI])vC̻.?M[#JoE|j+m֌Yn,a('WgwaՊ2O3:ޱ;̷xbha/{n?aA=E ,YrIn\WMZWϜaQ/;;&DmRXH8n>Gw&k5a9b*ɧeWv~',n)w^vs8Nl}/u]k>wtouGjzCnaH6??{7_n꿛kGtހ+NҘd"(>ѯ'?ir?6A `ƯQH=F߻5E=((p²A< BFZ?x\;聞NAC Mm5)Hd&|4!p+2? ~_Tax|G~KIH2{Ǝ9jNAEDTwՁȤ>)C< ZwlQFZq$<4=>SgV@DK~Ehds:u:Gmж[Ʀ23tZe[nF=W*6lyaSu?-ͷOԞնK.ȶgm?i^+_\C(G>+^y{mVh\ 6[1l䨑hB{aTwQP6RF%nggZ*\",omkgp5uKʌiֿleڈ#tΜDm1.hR~NڶI:+oז-]&ÞV\ηٳunqm rõݳ:%RƔ\*?r/.j+P"wcssJSorǟ<%j|,1eX9:CMlׅdP)SeS&${Mkx}AыS LAّ0?xSلÕ!p+?؅H @Dt)`I`Ch!ahiPa Kb&p4EGxpAKbr&;f8ϓ&M,HS1B"f|$6zI,*-8ҖO?X*-~dXS>"7κWy5ϖ4QúBzz^ + {h&gZFm?5&] E馛tK*2*Zື$!T,?%en:r_TGsc[>~7e]\J0^TV,2}`zOƶ2]*NuY,8a6 4O^RoJ:hg/G |ndU(* ȋLF|d#C ^a#"g;'e^O!d{r(+3H:Pl'qavNX:<2 Gׯ3gp{,/ Rz_gw҉TC(wүWst۲{[h=zvn[v6Pm XФUil &Mk׏_t~Q/u-_MrѣG4~_Jg՞3l+5҃ ZF4D8h~tTagquIU\ ?iZ~82b"m,A6ACEK|4Pamj@al4,yS?Pw#oh:v=Amu(G^d?ɨ&/W!i:E 4kp ~SO.ʔq -<͝%*q|a~t{&TEo׿pmG4ox,7>y55f/37Z;Gu),eNL7gZK dP~]_P@Ff)>H|Yࠏfa,#=?_:GZ xPr&I D8C8p)1eI+ o ~⁈/@2 x  \GLŠ6Ttyd sQWhT?ix/,-魛829ĶW~zQk#Y5ldT @R%kNs_nkݲv`;RSۢTQI-i9 Wڬy+;ξJQ_nJ_]"_BWYsxe'|?qdTmV_/<-Z SoOo??/k;wsǟ? +}਴n;sZ6YO.ppD:2(xA]gy6.hÏ|?'Dke!eL cX3x't"} aX<ӂUՓ0!LAm[_6Q+:l"Xnt|Z [֢u»S2,e $Uj-k-9|olj(% ůUBW}X&VD;3CL.@.%}uЖ'׿N{e[?+‹Hi?b) Ztx6&o@B62 FD <"pC&n!P=r&/i/~/_ƀ3q3Isq`~D͝F1Hu߲(TCn ѣDH"PP[J^\Bswfyy`P+n8U%j○CӊgȆj筅w$IqF#HvEފSr{\Yʑ_nk;W:“W '7R ?^sS[>2 6FRF}]pxF1da*tq"}p#nχohFC>E%_ > V]"0_B '>2Y$)EASѬ̐AbX rePJ)h(`:WJSĉ§_jDUjPҧW=@$~ ՜SE _3^׿dSKN&?BվDTauS bxƲ @|84_ݲa-c 97P>\ؖ"x Дķ A&QM$ix GeDep'c W1QAO_ ~8v!R (v9Yds(\Գ;$ZlJ8gK 'p3|gjL\FO[5)Y~?V%u![zUYs=hS,WKR͕F|;!ߥ{?nz7Ӡ̠]t ?y<7;_g}&^g_qGJl.1 3|͠"#|cA̖OGc࿐ȁ"!1yě"=Pt``A2`C2֯Θ{іH1.=?'вʯx?OUCi /wJ-'?>v*ooZ? t-yo<<4;O[>5=[>7oP㪠45ƒ-(@aD ?.a L(Lpy^CH'op7ip&$ot@K*}9צ $*"g9t\HhL0! HW' h<+ ~x@sZO0 Bp, iPAYIKX/hIxOq Ϝ5t)礒 #S?SQ!/tT<5YWv*4P̔;?yO*NӬUm98@dظ+.<pZ0rǮMOq"N^ 7h :eȂ4'y>tT&`yZI7?UCy?slVgnCk2)r]'+3GC-At@Z ̃??6xBK<!> 9.hVq#*D>x DO"AG!62'i^zk˧C۝ S?ߴ?eyW 7$NY5W1W!XZLN TGd|5,B +U d>rUB?o6GӠ'oWSrk Wr_k ǟ<̛3Oz-B:ʔfZwCk7zچ )FH"iaC0.>Z~l2a 7 z  k/d-_˼l \S3 /20f1y=>Ґ>h_eЀ\">҇Q9a@xޱc Ӥ*ob?;u¦_'--a=7#aOJ28!~Dwj fS}z3O xMyso(ZZnh)=h6t/5_noy5 _Pjwh>-7 av0l1O<vmO|WOڰEzx"/e^H[д薙HPB dUT"a<!> i#Ef#SQ#|<9J_}"]T6n!s˜VO)EE@QFqS\ħ ¦VYU94(W/`h4:s,ߧOM"S?ߴ?eV_n4/ysmThȆ?iglǏQXFvax<|#Ox0q@Ď?*"BC>i#?@\va(jipDom ;^d <2YOZ9GP`','n# wÞea|`hN>2 8O/;.|`jggȎ.-tqBKVW i!-I6ǘq}̲͠(x4DY5}Op)ܼ[ M6 4PPkQ;qQ7UȺqMW,̛7?̚1;ĦB:_BG ?oyӠż<;V? o>3'6-6`}zҥ['VM |+n"ӳa҈;یs;G;aqÝa`{9+NH6M,*+Ns0J!Xwb. eyBz%Ud8} ,Pd#Y('~4eRJpnj-gU̿8^YXIx֭)Q/w-gcf^I1fL7[>j">}foݺehѲE \.֓f͚fNG?˴JWrx'Ga=Ϳy_>o> $ߟq?-cѥk}+6aavv"~qncE[D/coJ' uJ||x| ,qp1'\θrxatWe'ݙ4`qq8(l0}%U8+ {ё-cKK KְbVaLemm|l7ֵQō@$2G iMMff-SPk~;7.]l5:uBޭK/%3iBѿ.yVFY{e˳Wtlа|0a4 {y?ƛ5*?U?_? X||>P/cXݾ5{|jk޿?cgiok+Ը R>lcrT 2{:a >6I4`ҪL`FHjql3>; /Q.& ̞F7, 7/9QNᙞ={v$w|F0Yhv Q¬([qD0 Z)\2*4ܸƥhެ >}2gbE"cJH\".eWgR$JXSm.&+c k_XpjOB*kRuʲQ2&X_,c/G!S՚NVUy9:~lolc#z!_Oߺx?Y(jy?sf϶AM6/kMK:`P )0዁ *1+f8`pà b,) /Ka/G,IsC&xq5XA#1R$ L'|8=Mq,pY:s>ض M8~`9x&= N$B c>29@K{5nm!$z{0II*kUaD?e .nӿ:,$̘67o°[_L&ӳFYNSÎXvU&koO -lIBfW/*H ASIM?uGN3, aЛ f̨q_:=i!)C+.'!5{V7 yO9AQF_ ? ֺ_cp .75+: }7Z%_w*-˗/ ^w]X&?+{sC\q_xgoJBdHvT|p5ׅK&귿3qtpە:+NrL ? |c?7O߀nFZc3>dc?՗Ϛqޔloscsfυ3]JкY'ӳh(V|EzST~n# f LY9[ @aS#{9pc8y:p^82W+,Jt paq.8>e`x֑pi'xw{9Snh:+A%!°^!X+*fE1!lP@IDATю8R-̠V_\_;M~7]zB?d̤bXLըǞ8?fX0ˠ<0s&dPPl:L~#,]$eq*e^c w4YD:0[8?#EG|zEXli,;6mIwa…IS~!/$~ꩧÏ8}-4'c*X^zk ./vmgeS9\҃YX?^i4sl,SYmBC8JrfL[E nI2s 0%N:2Ҳ'03qC4eGyb'^Km˧R $!}10Y!Ca}07Fy|'Y-͕GVS6Y#wƟ^z)<أ +Լsa jjGƚK.$WC~Wt) 0zӯ6;l6{'l{Os%̗Wo 'c5. meۘ ﻑ5mf5eȟ?-Ioe?/ fS26nd>onjfjE[>Nf;l,y8jt1ƙwŽ \ipE8+:pqǁ#Ow?PS`̘R3XG//#ϩ'@DQ'xII!+J i3fuڦot42`P~X~b_/chauhYV.Emn6h~yBJ 7k0t%4jV2s s͋I4_1hZyan˴7-7h |f='̕guǿ;3l6igڢPK8@rFI(+{uK͝;7Ɔ-*-;3XwgF<,*8U?,п6͠hj`ӟ6cDžlȱ(?g%GN<Ȱ.Ǩ;]>3LOޘux /ck#yg4`P[cSdtɠ444~p-/CǗ͛3Zj&㏕e)D:Zk IBp!EarGJ48ŸSƸI`<ތCvpǑ氤aH<0.)z`m!Trg<90Hs+ʲ 8/CIX,tpO ?򜇡={u3HgB HqIli)ELJF@CQOyEyz_!6G\ m vbUz.%?>Īm'M<5G]Vn7[+e˧\诃Wx@8-V(qiioh?x7 >;ɄTgW[F>%açf!R~;Ν:&M _|E4yRX[Ə_upm64e 9sVxwÇ_^Ν:Nk"z_$g"[v%l߭kh"zyw7f8cņ L6bsW(̐Q=ozǛc?T/gW^as?4lz\3TIcy[[hqנ~{t2q݌¸z;;ciڬ;nI:z"oicq$&EO)2?-pɎy"7͘&bwVZCG͠06ƛ|A}7LZj !׮lHS'>m8afFu݈.$)?ێ2t|yIqS0pD0B Kܙ2J!Yp8}W qG>4聋' GysNfޣGN|K|.@ LOX3ǃ-dB,o[z?8Iʚg/ldPk<-O~mB{+oDcbe"nZǠ&7mւDtL~?IJ+>hT6,UfdS^[ j1@?6׆6L؈9,Z•t'Ə]OBkܸqxdQK?W^ >|9}Cgqh^߿ry-[ M)I=t3ώuN zH7qp72.CVѳD]~׋ӥ<]2) 캳Mx:)[2L ۶ogAԎ:dA0lذXN1ݡͲˌn)HݨmcǏ W&?r8Srn|.??-9 Mo 7?'򜾃Yes/؟g*HwIC~L2%&Y@6%?iV&Mm"wMijK jY6T&?it%+hC9Yp X!F یeᕜQ28/Gq'"c[=ߪL?^<,%KVm[CR_ԖO0ѿu[R^?PRA<ː3񇕘" {3` $kCmE YsB_gIƾ.(YUzOaȓC[?QMH2J *Wr6J?k=+&b]-o5Ϳ1y˾O>02ߦuۍ[n6!Xέ7*~|u+SY4\eyaױ,<#_]8 itpfr^Wb;2{ 9ve\p^]X)C:.|\6,44/|wX͠'(Vz^\nl;N5Uͳ|h0lߖ76W߶bw`az PFh"oZ}DxVIq rmg[mOKBۦKKEe:Ӫ,:rw?)PSYn`ַ jӦmkw1HF#:"i䔄_F}2*\VtA å0-B3ϴ7(w56(Ei;0hm3 |1uzcB[Q!? 9:-l͛45,fou„fxC[ߜ@Hp!|¤F ?E/OY,-/RU@?`yHCEVs<[jW_cqƟf͚zƟG}iyVZ ""Zer+}W+dP[>ILGQgH+_$DRF3]YY7~*:Q21n;&ؕZQ7i ]ۡ_|AũH$VVkGޜ Ko'-``([?xtND[W2'bXlպMg͞mmpC#F/u;-3"1!]o[4ȧ|§?5\"f1_Z{ڥ g:8aN[ 4jӒЊ?LFV7{li_ #ۿnjo&uh14~_0ߚfT5B}c_ϼy޿_PF>I PKa>pP뙼Wikix!JQvejSXE-(Hu{?<u&'KE?%ʏ}3r?@%/+ĕ^zzw?)^UUfPSK[>JaKhgu6rݖ[jLwQ]K/b?s;7̟7?\|/?J>PƿV[n>ۚoK/'܌m۶ }_r֙^o}pV`QCwC 9Vs\gq{5XU/~2YO}-tNO?~!Ho϶յNηMl.%B˖ 02V.h x 1^VI_NpcF^n6۬QбtL\tq#/5~xh.ږsnDe+HȡK^~y~ӟFdlXmwnVo|.2>|ՏdP tFk|Q( đu8W6U$(JmB0j尠1x'j0ӂ +\% -(ȇscǦE+KX;) jQܤ3 X ȯ}nJr9Ƞ&j^_¯믿.tg'?Aڪ$5-Q7+u '>Ѫt ϙ=K~m=?W|q ~3ScƎ =h8cXM"3g  "w.8.*L"9/x__l/4aiR 6mK)i '0E?Wy_>*y'Z8>Ĺ3-qZ/mu=r4RgNR[> 'T Si8X/q'Xp8EimG9Tg ftjP+}u` B;hPcL'2')~_j+,Vu?U`#(K#g%8ƿ]hRJbIvi]6 <D|ŊMG0kzGStfYe<+ӱ-=;qV 'pw8;&\w͵,[W9䐔>+ԞsM uz'7.-pCW^}@2tfeΆ]~ëuVݿ[Ɛ8qh?X1fcWQ. }(ZK >-СlCv㤟s9aI%_Qmv3ck31g?jY ZUW\&Nۈ m-PU.%HzTgqfBNց}ιaނy_eUVv宻 vЊf)$N>)?+#s==?V{J\kE j ?~U6Gt[Դ]]i˵ 3xđmُ?ʿZej-gj"O0>hsd>\p᛺M4IS عk'e=I2zu@l|1ؖOu֭ZƟv #n2CgSOM+'L14n/"- ݜ|^ Cmg z^W-; GH9e2w!!8in"8N3K3LY`,p~l]' ]쳺#ST>[)j|-=J5aR-@{g[Nd"LTyKʬ3Hiя|CJk5^0*:6X<6,^B;X9ZI߫2( 嶊._lut]w1yyR 0͟?7\z]J@y/Ƕ^=zFJ|;V3c~:a Ct2ENKoo iZ#0е9Ci3Y/9UVO޿̭U;e2d~r)-Ʃ8ycy H8~[ a~9~ 8/?tM9/O|%]Ua_Qm]1Sij4,3 ?_HVwxByLJOI*Zg=ƨ23G) _YJ..eް5u+7|p >nA\zRLo1COo^TkX~h\RɧUWpVQ3Ajàbc>[=}X.Q.=R;H~Zp%|vs˕WqDExCZ/nSX͊Jln726֙aoviYCe}Uf$#diJ\چ1>`g۳>??PriKW^{V9\~'XmsޑhOotmH_ρ~W5]J VPqγiQ|Ub\|Qb.t&ZǸy2}Mh2fnf B4\Ikf~۵/9!t~Җ϶1t}7wƻtH/9dvjZ}w m@d`2Mj^e<2ܩsO:7$ʖ`gw jtݕKKgM;^X^fz9'L\J .9<@nĉ*Nkk#}γK??VO@rudqȳb 9\y[yKF^>O>|Ɵ9u)|+ؐ|! 1BalzXAsVy:rG:a4qZ <_N|yOYǝqZ @Y.K؅uYy~6 .pN8?Q9^qġ0cP)cJ 9)k Cz|FN Q\:oeq=fh't,!x֖~ա__>iK =1nĠ%]prG-$WuנjPUG'oe,8!\%8P[ jW|ժoXY?&9}[^9yKFG?2y|g'~ +hOpuoVQm2/pӹ1\p ' z?="r~."{?\xbR5n~ppPYaCBMg!?c! ~os=W**>wd;VFaDMn馰vۋʫ̠AߔSjq#ɹʶPj%H.x=]v;SN 7r ew.x RxܧmuqA=E{ÿO.7q z .~-0q/֯ʟ-B@t^|idx zj8p+G:uZHm\Yi&},Wi'{Am?%>/`wohۦm8饗^˭[Qp!S{_?kP*4Л 4SY< z`PjX&7U {[>\Hdܵ쎳X:|Apk;)}ൺpG?q8^rS{l5juc.X0&H鄜LIϿs8Fu̖Ϻf/r]tgVi$EY9>C `8>U@:y0m%>8ge<40aODL"8a%0ONfZ9pΓIw2|Oz: 5۪\Dh8Ƥ{䫙[!qVyIXXx&qTP~ m\m J֍>_]w^* OP 5sORgٌ.Z{3?zX+e?F)[ ?nVB誗-u9C'Vh6oT+b\'NۧϘnN|rР_2r}8t?c n37 ~[ߎzC=MT8I&-ӦO ַtx)%sѼlإիW/mMBjϱ30SK_B⥶sX%@Mb+ uηyrm*_~H:N m![3=%s(+[aӽ 1( Mi 2nFW/YGhUFЬI0le c6m^/>Qu7]tј]Y΍Kl7&}dXN? iW_{%[>.~υҏt=-/S^as1+8.l VN6hT7yr!ʬmne%2й.'ɩzdlN=ӯJ. Z٪wc^9U 長\Uf̐]Ooի\}Y{s?Px\m}u[V߆; 5ɥ1+%?fP {n B,<qr|ddiS˒Oǁ%FGں,3L9 p+B1:G>Hq  cKp[N&Ӧk{RH\ F58^Jts.%ˠ6*5oD3M~7p/?kEL]NG^2F>}3tӯ]32u)[ IǍgjX ֽ }!ba9-ҙK}.Hee୙WV_|!G 탿H.z%,!yTFɢ%ZvQ-~U.JüYƥ.<(e26iU?n[P?^0;},EsL8V?ozB?$g 5 xqSiKcV V-iN_d s;g/B֤ jM/%'i Yb$cMm)tWFޮ38& ]v55A:}] ]or/-+ϥ}"^98ϥ[>{ wg=iq!nh$g٭*+]ZY1m+ֿ_L.<2b~+L@?'Q+y ܌鎿s̱n[>od񇳮W- |xٍ]+jiā!=NŦa7e}x!Ii +ys><^O:g䄳D!;SE\H^,+,MfY=4eÚ%mVu/$Iu l+~D"ۉb>qK"T=Pj[h5v"]]k.~4BM9nL_Eqm_ W|gKD7י~l]&HvJm-3g̒]û`,.B.%pIjW[6z[Oڪ'MXկW_g=ȣ_wiwp Bkl~-}ò ApW۫2{Gk.ږײmd[d*6VPVz}QSCBgGgn ˹pU3J[ 7p%ʨ_5X׮!m"%~ <*_a.Vm!vb tPFbs+? 0x>07;&}߹ZjF+[^bpycտz[>OL`eeV9!IO3?k?6Oe=mK^tl::(ӉA G:q# gi˖!w# qC^' Gpx{>aK>4Hڳgώdq)mQ86,-W@1:l`#rGVGëI6]|*VHy oȶp]J 'uՃ`U]J`$m-c ukCΈ3fY3g.႟_ h\ΛjꊰnAWKjzGWʂOՙRE5zݬ$R(NUG/["Sgpq i WN>S؅2u_tHE>,N9唔~<(Ihgm gy =?kx5d1!,?YJCxӀg_…^6<jXdi8#>BQ~Yoyh;}tɒχCu%u-Q#ŸgQf8Z+ڵo3#JK/ ?>̮0h ܌R%5KHk鼯Yh@ c'|n _5Se'Фi"m|f3ᅿ)҃(T2M;:J "Xr AFe`^'o}*[_07FҿꪾatCN2V}^DC08e˒NYLͮ8`tCP4lAtW+&@\8 dAM?3G @,a"&HѠOy"Ӂ2R+3Z)‹ c^rQŪ8pu^ @yygC/S/!OдH jOBTGkO~ jBD|r: ㇭UX|| 0*r_~cUWV%Id+5qMHXUpGM>SJ}2hn, MG#1]f +\Uq- dH0ir3UwbN7̌4|ijzr}jVZ֮ݶCNM R=T]i+zƕ2UpGԘNs[NcNV#H|-"t),W*}_F~#СHkbzTGIs9Gm"J[;Jmڶ . 'ORt4@IDAT9(CW*XEp?oye&6z\o'9>vՑ"%S|jfPwG>q67ybOtr# g-͆lGeqBte:1d,'CNԙƳBrvqxtC:a~8q̀r={v-(mˎJial0@ 5Ey\ʒs#b8"q #" 0s mvL+b%kK_"GY]gTv|_3>xg_JFy:'fo=_+\A?6#Ko-Vby#쟅IJ2Q} jlpJM~&Si$E ,nkV"i+迅V&uϵ#z磌 㭼\G}bǶ׿Zfc"y.'Z/sj˧^8!}ػHmTvܰSoĸG80y8M(^0{9+Ľ,)6!S|_?%$4N0P㯍P|m:t捁|ݤٺZ3ps80a:C31Ƅ7$^ ga( X$xδĖ,kGҷ4{8E@şj{S߭õBw'ePs]G zJP 5>"^|⏌oDaEYQ.T;PXhTS=jdL 8> #rIiUQrZԳbHU߈T @W>C)%j(h\y_@a`dǟ||;1J02V?z{nY2?PnɭC2I8ZE1R~8q8|̏;҈|8{y8@ <9*Z% wD@wC80gሓN]e,@+?3aqѳ#njSP%̨Bdz^uA*Hcz:)~2gaE=| jH {z*Y]3Ծw|go9(`E 8yNi/bӗy{O]ygj f "gh"e=5K(Ѳ0Oek8iL綉+pcFr4'ZcѼMc)~i`|z>7%V_>/?k73whP˾;CU3؊jAO_*L n#N. ?x*IM E\ r8|_Sxz*+$D%ĝqcC07@vμpsa <OckeH2#tZnj-"0 y82<foɪ>aV 0#42)fTOn}/]l6{vPVTi}'q^v[Ya7[~Dڬ+\uiQ%p}}}LKږyT =[&Ma#!N6lM$۟2֐i8k|M q)||/o,M;Ƞ}?ߝO=$.'|7q4¼̺ a0 >Np Z9/Zp{68p: 8^r6YXΘs`ԍaq˓ ;- hnsl4~3:cAM rMF M 1dR"VM׻B+ <joѬ}( iݓ )1Tw"TAL$2~'Rb(ߺMв.~HFtwlI5͜v!"wWLeR=⠩hI3#FW:p^>"j7ڛm*v v/1cJ#eXG_TQj$ž/FlMy_>4Tm<CEqd|?7^-q矹9 T+IUPUwvPid`Pɰ 1͍Z#8>=tɸNA֝%ܨGpO<*m L:g_ap<ݘE|/Cyu|GHW =i坖 h5gT*k`-{*V^"+WLlK+2IKS8- cW|*E3|\YsN?F^7eۭ[ M6 ,ֿ„+nZ">3#8>K Df޲c;D4_Aé8p+ $p҈1a%Ã% g@eRC騱,H:'M??:^gYJɱ?ؾ1lWs/x.3l'4m>O>l,ΕV7*sPC! -5a"MYe!|`a.7)"M2ns^s:^ΛTgT I M0ʕH3#`^֙&ϙȗ0əh(,,a \=zjD1Fբh@\軸1р.RcL 0pn)t:} ߢE BC˿SZ>-ʱZ+}-k>$f- E7v …:.]81>+LZ6(!2 mT^ 5{wjklk@ 39j7>9W/g s+ ]ZV4,_<,lYbU+˖^Y8k//]WѣJ#EIek:b*GD6ߘ V6 ~[~AD}J/i/7TO{M/z$q5W_vig┓O$gp$>0n/k$λN\bMXO-o:?[O`Wk=_N_S, \MonuULW I5vӖO!l)ь֘5pa!QV'đF,<a%: L >di?>kAm]/ߍc^;,=ߕ@< } ep^v\ Ֆΐ]Z8CM鶸d%գc5'96ӌGZ l cL@쩴x3(1D<#O_r[>9Y _R"`V]^ԨCU>Ĕ.;Y_9'FϨD_-VLqiUTo͆vY4xDDǘSU?{>',\8ߠ)1?}^vePBr*h5G]K[_''{O>26ҝ_\WwMVM )g_ Qyz6Lqsq~})8Q[Cy^q@RiBe'PY^q3Q sa(K>,y"ÑG.eeX"G>qM=/;TlW;fhd>| .u&䱒,XI$zk8 6E>?mty)m5яj[sf")җRl2ՠf u-Z+XW|d-o=S_5Uӛ3FX=F1Sf#M ^hs 6IyW*ζ{ N@`wNj׽jxXf\|VaP%kBq6lؿ\uk"/_3[Ag5>sq{}~͠F;ӬI3j0m9Xz  ΋.h F͚I.XrGs7oy_ǫ~|[>׏*G]dWlP.˿w|D:0x}K0o09+qu^缂/ G8z~LI,1W3,^"ݕDIw= X\q%:7r_IJy={twg[ZjX0RL*,Aj["Id("A.IK0fVMfЖO}v'2@ɿ/sȒ].C묨sߕAMn=v14Wc5gepѰe2)ی_f'SR̰QfS%YaՂԦd5[:jաo6l.[.^6ߢmkݺM0~XjZ`0lŖVn֬"2l:ki;gvԖ&lAf:ou.) LJyXE5a6az׺l]E|0y0iİ@ˈ[KP>8@:ow!l.=Mi7~\XWV[ w>ڜ6eʔ0qҤ%/O؋-J)c*5 5,~ ;ug~Οzj~p}٪&?$עeNrb7~l0PO}hcYb2?7oRQ|fe#S6\G?^8a:FPY&l~aw lM'6]o8t-v/rŋ|ZJu:LOjqώyKNzQ]??ӺfՖO}Bm7j;CM ~!$1|a"a#@4e%tt7y4V8)|Lڕ9D@e N˹`ˆ'geM8KÍe ,iN#kSrxgϞ-LE)m_n[$j,Dq LYzbUVA2*4K G=M_H3"pF(<*JN #,g AY5KYKM=v5a?mYQd ׿)$|Vl fH|/ ]/1LKȑ#M5~Sz;e˖߯jJ>>ljŕW݌ez䑳j-Z}.:'?Q:F{cP[jk٢e eXYrFfpKw}8f }*|gV7Wc@J[Yw%,_^NіOW_/쬭-#_K8B+lՖ&1mZ4~kc't0T0  #oU5\@+__5YioEyy-_O)!yw|,c263glc'M&J(&1tyˆ1TvLV7N68O0eeI## q؛X)j 9<+X.G7&a4g8ui ;`|MhBCt7{74Vu"jCEU^O)+Lcc))7`z&ӓ2( ]ܦ! uY~I=7o°[_L&ӳFYNS;jφ.%= B$Jud[jsԫ2/ٛn>ί?>3Ԥ/ Ƒ&, SZy8a5T.V!?Dr"#j?E8X(/%@ŵ{\WJtp iرYgWNJ(=>tA3HC)XR0wE姴]|?l'}O6`4wc33eߚ(gk_'O.2RlSR8-Y~7# 5|ٮeG5׹h.2#aC;Z3\NO<1 fuY7,nMNyn )0d?Sz;Q*??cLN MYfkYNgK_<4mt Q?#DȨf1_~|յW_`[hl9Ԇ?]޿LO%{/O?ṣe#͸XϿ?ƍz.4^1Meǔ)38( /y!e/xAǧChߏeF>̇\yNZyLmhn3 ])@b. !ƒA0?&x_ ~OrIs|w񄉻Ƴ5L!XczP*i;R稶*n&b%СPkx2XzScblٍ\^"a>_?,i?|\wQUguN#n "P84%[@A?N0i+?Vt(^4$;OC5PC$j8C=1t.?C.[0߻{^:|OC=D+ҞU-xV6L`{}jlpV١&.|@+ޗ/OiWMuH̳ϤC9T_|RQ[:?H+;w[nIVlKaǴG?oS&)Bn8*pONkt~.ga9mطV{!=XASVݬ ._=5,<ҶZ~tUQGPCWmdWȊF.o . =CiM7I>9AujkP7ۛ^?~_ji7M!F_ߌB+sO t`жя}ci6  /# ~74**vSy7)6]uՕi?^- Q;+?m#rnP]+BDO$Y?@N"W{jy֡=jk+/Puy(ze)o6 .eg^8:C*7ngVU)'ӸINO(>J~xF!yvAޗ7 r>xLhSTWޅ>[4Q\4 4(,20Wy 8KtY٦j($/Av#F Y޿~RԿT$N?fӇߵzZwX΀Z9_y+M歙6vw7\5C4ܡMpp)549[8' fUzږ]pr0#T߻sObvPqނ ﯹ&3V^mt '|W{y&IZݴV=V-VGɬA>suÁxjNU^=ǟpVm]KW7iuIEѦsf#NIOWd}+׿y߱睻xM/aveGI 7-n˲ u3 GAؗCP8e n-wWw5-7eWgQ0fbYOie]0 3hCA(w}TLmNN 338Lg >1X%> ltg>W\4/Lkf:3\1h?vwt~VXMg  'qiJɲo 63ƚ5؛DoX淼9}ӟ]˾I\Ͻr(~_;#B[Y*XL1 ^9hOA9VV|gqo+9?LJƌ@-:CM׿qv/r~U:vVLK|H-+PY+8_M$钋mxKb(LGPZd:Js}b`+}!i9#-}9-}+_Y~ C?`O4dOO_0mc\/Yy(i~2\Z72'72G  pǝ`YƠ]W:Cm(C.ye N!S4@qFNj78\~8ӂCi- ]B x]6e- -E ac 8^saHsgh7=68#$϶"

?nY&c#vnbL7ZN_/#Sە-sф}M|x9_FFXTjywN tXVd=+ӾIo4~\:Q7͈Bm 7 >lü3ZmӖOj҇3D9(9ǛoE?^-?cw-u_fG>y+XON֖XVmI~SO8?}#K馛TR},?k^N_|Q}wEN믿NNPۚj 5-^E?O/Jwrnoz;`SOcL첋hbK*N>ܑh·C o}:C y3gʑFUe\H7hZrpȀmk)Ӗb;!x;ޑ g}fW:yoӗR6Z~iBGF>֍GRiW_OS\#F 5wⷚ令gܸH'х5j-ɼ#? )(yerXyeEoVݣ8tȷ>/]?9AN +D+T~8G@ J^FN6ɣ'V#5{_}]+:5Cr󏳢l=oqߌd6?TgcE/Y__҉RYáֵ};I#GjRoϡl`hu6mr85VAQWY)洂N$sYIvȉ(ÉrW> پfþ)Z 7xaWe_V6>8}_NAM[aF_Ok[ojsk}_YvDv|Nn(Al>;UgVʽoNQ6 >A;tf+{B)_9%햿 B*𚭶 ;يɖϝvz_|8ECUYjK{^?  =9!4W[~뭉zu? HoHN EˏTAR?-suF6ƟJF (/Ʈ2[u;Qw25ƍjR_WZMU7ohV#so_hV޵?A|ޯr/Jك>ĨOסg5oO_#wՕWVΑ5N[njm"=jVRWJqƛ_-RCy+Ӄ/nf-|S+RrkgVJvoRv-+ `>3K+?oH[[j%p-Rʹk;gPo}+VQM:#O=d~,s ׾\*i sWIVaoM:@:$tEaZ]iCJBW]w8fxgoOCM+o5l}+VՖs~"WqE!Z~.d?:(7h_{ Vƿ2e+yat%|#Fz@?7E4yR @H! .qEDEۚ LfVo:@0ȷUQD<jz Z'sww.aџ7U3R~]F9.8~vȜ&_A?w_W+ij%Z k~ qlMx+͔FȃpUFZq'x[C+r Ȟv|vۡK~퓞ӊ02~һnA7Vk{g{wR|`#‚VcmnZt8ŨVEIM8SlAC@.wqǴ{fe]3Ƥ7µ_rͫz:ÎHtvWtBjY^oV)&C>\K+ްoھ°o= YQˮ ^Ѳozjyՙ ?ZU#*kNF>&]uvW<8;{>{BOU(s2٫Q xocӟF]rI~V?=眳J+忉xx}%8}:ץ*p׿!ƿ+*=裈2١FGaE>_iݷ@N_iZvRps=/z>>WQ~F/_Spt8=➿Jk2?ei|(o>pPDAbǍ ؖrG`x˜!4!4vZ9Mh]yȦ"4@>8hLK84e8m_ 6^:n 80ִ':rH#R4ҖGqow#)㒉v͉:O)"űQID0R0 BT:J-#clc%3t7oju*@鯕E7Y1:fޒҙj838'*s$|J39ڄ\_9v)E7`٦ǭ`k(+6p,Nyݭ`I].jv CM9]ߒ֚!?iKQJ+%2#J?ēoUymI㾔GOѶEv[>T/vI_[ZeUuГ+=pGN#G8HW?> S?o;9W.8KǥNN+}FtTrZ7x_Q}dW~1+ ~&=<{oCvstӯ9|CMϑݿMc۶8Ԓg6ufq֘V@~l1VIa|IQSݶۧCƧ@m|B%dӗt(0Q&Gj-oR:M+ԨWz:35V Rvӧ~?XE>[>/S+P?OrDG:-l-ȡ|fv{}:x;C.gxRJs=Ou[3bueOk?]W9̺G+ R\PPUf(On:$?UƊ3b8Y觌?K]:ey)/M1#?ԝdoBy 5yrř980Eslv r}90ujg)NIjJ+&{FiKsO:_r*Gv8S &u~ק7 kV[*!9g|]q8]ȇWl r+%UKFy眧sn=WiӴ^{CLy=dp'>1s>V庚S;ԄQ%_nvevȾM68O#74}/*ݖMqxpI'y pl{ÇC:*Mtء mXd@IDAT7AgQ<ꨣ5-]6qz9V:7t0Uw6SVҐ?JDҗS>g?YM^!{ÁVS=wO;o9r^/pޓ  mR;GY? dMұ~I骟K<l}Qx}ޞ8m13n+& Oy痉Vpdž2{Dwf&58_jiV\;Wتyzk̿珼3P@^ B;ᴢ\x!D!rIA8E[>׃+P= )E4R3XHrB]Cl 2M'(š\ x$y"W?BgotUc2 u(pUtʻ<t܎pA_*na$x "/h'' +ۓzL>JjgN'4iu7GN*/ZQwjɾcOiV|n:O&om]gI'w9CkѕU[hE|3m2]-vw_=uȹrO'61)!Xe%sx[Y8c[>id(sӱ[uWsS&*O@ߪZ$$y=Q_YKA||y5]JӦߓ:樸Gj|ww}lgmY88@%++׳<;Vz@ѹzo (uuY3ͯU_s)?hIP)ř*t$<[$XFCW_i}gu)#G:y4y}2Cq1_8Ut7]3x1<i6# S4ن| γ}#g| 푠K. A !\ĭF: /E\0dA`{L~sBohoZd'8?W>Gԩӕ "d $Tum dmJ <ȚHUyّSh&"(VC?*~ӏLY~;- iG^9C;HߔіϻlKWmLqs+ Wqc6tq F#΃I~.+@̹]El!sAkm/?}JgOLWd]ppqٳ j%HK?:S szk9Sf5ˠ4ʫ~#jmo}szGӡj!Om} F}rMm5\Y hjuթf>,jZk>ۮ믻6Gz˾UV͕{ޚ}m<`kMZO3UO_;T`wori5lǟiZnivE9)@w)]u?Q|}=8oc' elg>sakgTm1M8QeA:Shk糙FG O{Y~?k?Y>Juݞ:XC~۫Lz_nֹl_gC9N:(ߑNÍukU.ڗrUt䑟O{̧>IT?g%__ӟeo?*(YiWNM[fnT?vW j-__oh1_W2ܹO!ݏg\ &G ɓ0/tihW4hBc:hF.\Q vDž=h-;OiB.!ZCOq/"4qxQJ 0h7bk͐ l:5d]Ӧ7 6 ~#$vratxuROYWAT: Gh׺})^\5V\x[ߚlzx[6CӰC{{|kKhh]9ZY5{c2jdMCsJ]?ZHibN7"MӦOK/$VaVrW|Am0W_suEtÆ3.M!0O`p?`zGbo\wݴ~4c6DX=m)bjrUGdp9| O̬{.dw >ܳϤe룪K>87_gxApHk_;Et+w{yPp1Q&t}JjsIEi5SƟ2jd2<柹:NzWݟ_NB5ǻpɓ'A-;0qE% )T/d3z6zC@.ilH pM.eoơ@g呈s>yЛrH7i6 ! H65? P\kL~2$5s§oCҼBlm`V՘.g[nK]]Wfk_ai7l4D+>R%Z>9?ϦۦyԙWk ^S+`&+^4U+zv=ϣJ@޾'tc=-J?Ơ/J9]^kڡ ;i!9XX/ي~ii0kKuw>-2tcoM_=E{v5<,T/kuRRisyҮ`<-:?< *;r c0o?+mEj3#1B;0p5i̇_yG$PvxM}r_:~#2(xiI6ގ. %Nhg ɇ-2XM" ^AYl%t|k^SP[/VkZMmdROZ_ĉj)0 ZA@\j#JgA!CmlIRB Cs#dg_3Q*2dӍ:\-Hws&eF6ۻ$? t>:P돂U\_~@ m5\#jW:q0qVm3NO\su0 Y~kK񷴿^<W_jJq]qQ 8ڌ Ay zQ }Y8J,qǡ_tGV y4 06F@BO%:>縍: 8 #!il\-z#VڧpH4qvb42]%ب"Y)Lx(0Pל_)UpYOCl'[~@G#) ^ft#rU3U#y[u{(OrV\65CCe?׈-89#"Ly15ai67e^MlEx^I_϶-/skX^2?Ke-?;w| 3Vω˱P7p&E|ght h:q5mw:E>8,ܥZ6ZjbJ͆48-CF:˅D7R. 3T3W$ڣ`(;+|/{~lOGZΪh![n\|8PY(;.3ȣi.a>RqyK0vmOOtJl7:Ɂvxwkt~ZS?ӏ/4%,W//B})Kgg,},L+(>Ǎ{W>w9!,s\|dJa1G||6C48i[2x'`Euɴ2spIS8pxS@8@hEhАהk~pilTw>qK>:^حO%ga:86CX-6 Vi{K2/YY_r?u?Z\bPygĹppo!pgAWYIx q4Gy/ƛW`zCj84le8\p8( fT(qdCg~#n=vAؑS-%QMmg"vf:2: 1(i183rKY)st%H%bo(+p%jKsYa]]>kZ29Xl=KQ[o-g(T$mοs ݵrDgBNWƟ%K+aɞ?S52w ?S2,w^P@Kt6Nh2|JS!'C`xcx.|3>_j~c<a3=ДEz^,Xb VAVjf`tqN2ˁٖC  ) `>hG '-5 tP+}3Exj)e0/:GvX|#HAO@ fCj˧Y"'~ʚrԟ6U~*gEM+ڗ"tXJV[fiHHeۃ.:JȐ#^j+ W?]G?۟$RY*$7!%hK˕^_i屢?e[3FG}c-oy(_,ϟsi˧Q'wu=ͿtjW>qn1"q:4x @YgGb>JXMR? %%:RkΑ?3 ϊn6w#-hS-O:ݷr])9ߩjq_C?SS)?*DSSgGz@ci?[h>b (O?sQjF4?g 8C-8x) 0Cg:?XI 8&v@xe@ł,P.a( 6 cO4M:ٕ4@>q-<ҾH7isYqt3N6mxSPT;*!0Ί?3~jʜ4y jJæ `' Z E@\5R;AUN[#n;&y axАfMG'ݬ=4~p,l%l !W`da<"1E#]?Cky!Yv)Z"@'˶6e7XyoLm-A,4qx1+4]0hl Le7r LD7iCo\لC >[b˧% ՈBpr<]LaT$ LB& ?ty_9W_iJ+?ee7oN-Xmv&MOOH\7C/ ] I;N, <@<2wE yGD q"C?nC 6 Z+i8{e+ Zx.!# p)z:8,ICZBN˄֮jl+- 3Y& +7VzW4U0 %ʛ5kf+} }XiGb2湰?4e(o[2<TsA/y'ȑy0CO7^[>hlTE.ښ.|>p F hLxlhjғo:/2!4"}?ȃ8 X@oZOX6ܴ5nyIՔ8!4v!r(m5nY\-cP{j‡kFX,+pp|Q0ӶOf( JE˲򫜜/\I)r`Ȑygey- Sǎcj3DW>1.r7В6}Fk^]uq">|9vܙ9#88y]97[(n et8(X32 0;߼䱊 47]0xv,Wot#$|i3(| `>^&Bn2+?W۲fv[RJ2P%-j)OZ8KQ2g?e]_ۗoz碟G=/7VPW~V?i|G/Poy% usPO ׶zyM:u~.%2W ,ؔA]I@F-5M%Zb6e@cYtV4N8a}M:5t(/.^a03X᎖˰\H! (ΊG &F\*ɬ p2x(^?fDrsU\]t%r}V|biր.:rK/?e-OP xyX.cLyqU Kݔ񷌿 7{;gn2QqB2,nš P߃ h1tә܎j3:1=2TEy7yQeΎ8Mj|'tPB ?!N*@p64xh-:S6r:Mt! 9loıBm4P&W aWa$sp)F#xY[{QߖfϜ #|d(?V9o#;CK(h?.r*E:48TC[Ɵj\uZ2,[_=`?/z.oc<(sKyPE,I*/珹sqgQhͿƎ E&+&c fLid(aMK^7!2Hc m>s:cnC3vJpa "$p7BN0~CHphCG'LutZ ;\2|D[ PpXQ4/in:Sj+8[EF af 5Cá|13,)1vv(Կj賴Qsj DóAP_2w?Gy(_ϟ¡֑j7wΣsx٠ qck'fiFi8рYj:O>pMCe*-˶ 06l4 x\vrpe"' -n-<jO<;ÂGP:" +""b VP'3gÆ zBs sEj?m=jA?__~J+?je)ZA[cF`L,_+soysu^FV")&:'"d/'Ǹj3⣺*K?@+^h1/tTYCR!ںBpfґlmDCeyt_϶ɸѲZQ_QܬU7KKS-W0W؜#YUCOy)yT(ϿKX[?:ybx1JgEӊɍKz q#I]tх5~|gcӑ o I7"ǷcYAy3?8_ <"AK8\QV’QMCS w:'M>4fIsY 6t„ ZSwWeM6t hiY"F-8Z}ĀC ^| R!;!?Tt?# ,KQ.?(JSt\QWa |XS*+SƟ2gT}?:HQ_UFYJPu24Ī2P?r{FQ7>k͝W|@w7?1w7j?vx+ jv17NG~pMz%k:'̏[H[6qzg<&=8K@-"ląsq(* ǣZm8 [?Yz[___c(?@7s?e)oyj+~Y1~?L4~1]# P qqhm48qq-Mv)i#8͋o#)㒉϶Arig"eYD/j#HUJG<d㓸"ojuūDVZ_F^WuPƟ2eaHЏϟ[柺*\4~z3wPSS9bԀccF+F8` >lT4yk#k䡋<Ӂ8y<,qγf`'Pg !2!q 3=+'\M;1B˅B8MK<)\y;lu K!$}lβJ&$ :7/2oG Zgub ۢu1q[zj9EݶJW_ 4WGӪ2ZKkK__~VƟփP[b`ϑ#Fʯ?(ɓ)3~H lk|_D6# S4 ̮]HgG>2C#A ]HDچ q#ncɷBKр&bdӣ2'O1uJ 4RsكD?+18фA 7ra[` PtM |*0k?dZ "+EYXGz+__TW_[柅?[<<篗ܹ#,};iR|gu=/3|+]k37J.x6-@`'qH j yg)B9/JI7>Wy-wMktoyNxX1ll[[>Ź(^+onY^M&'5B ͽ a,Рu/MgJCā_kK+?3S2?e)@̿1v;G%7B%IP~΂"P}'OWOG]9h䙦rĊ3j3;B/8x"7ƺq>C@.ilH pM.eoơ@gI8f|j'L0fm(&NjBeDahfk'z 2g[F-\ʠNA_꿴 +jyV[?j[ b2?eɣ#Cde-o-<珗ܹs |.ZP6n٦rZ6CO$ck2;mq ~|Z65gZO>>E0- 44#Fх sZ2}!8<3YkI 2#UO䓇5EbZV:cԳWէ."@hTq3upƔʌj-KAKe._꿴{gI2̿U?Gy(U?{~<ӖOG3ScSQkp?j3B|6lŵaqp3B.}8sqh4Ey4.= &Q6S仠|9nh#Nd: Hc oyփB}ݒIg@8˝v52;j8X2'n Ḃ@=d| P׶~T eJ? l)y+__eaנIS`,T!2?e)OӜOyX֓W-篹sS-ě |q ?>J@NɋZ_D1N/B;0G~f\|N|Б/4f|.҄ȩ*)J8v8cso#xS i'FGi\H惦I Yv:1q_Ccr]jʡ.FPڬlHSPq>gJ b8$Q+AAי+S GCCE%+A(٦\%׿?#en^nZєiA.M7ݔ~ӟV|1PUz{~~WƟoe-o_x?e0ohG '-7)#;cHS-FskE7Z f! fCj˧Y"A;͚rYR?Kڒ>lX|?H:9y(iyۥzhϑCrk\꿴51]+OPSAv'-" 8'ㄤPFȅ8N))MvhP<˩gt"C \$Q,$4"A #Cc&=L7 [E(9stm||ba2rĈц>^x!nƌ駟Ҋ+EϥUW]5m|4}4<Bl]wucƤW.,X{G F&IkM7$*7 L߂?f~,dV hl|/ح]!IDAT(SOUue/|+;{=֑VX/J˓GՈt7-WL Ս'x٭14ŗe棟2ҏ\Jǟ9(j]+* 5;x"_8Oqh8Թ5XI 8&v@xe@ł,P.a( 6 cO4M:ٕ4@>q-<ҾH7isYqt3N6OOFBYpG*'(ZSk$ xapD/)T:CmV̩גCMNKN?pc-= uUt>%~kZ:?Dn4Gέ체nAVzmd/gغQs>SS_I*uTb-C>y[ZqE|kM? 2)+v}δnC-?: aZQxq7/mb+b}{tkZ<Tqz7dG y䑇gKۑ+Pr!i.ӊ+*lg}vc; Wݼ‹*﮾:/ˬ ssREj+Usw͔r?eFDo<9e2.9ˡB-?%Zon?ǏCM&O!Gr\N(E\iLG/h)0qqH0#@qBdW:m=#p \20AICCG7iEkZMv⁏ yF+LCd„#}Z{BqĠBf2 ;FoD" \`  ;0UϒC СCnFiF2oc۴ӽ޳HN^aC:|чґ)kЍjKvXfmTnA B j׈tI_ʬԟ fMG}t|Cg%S.Gj/27TvÍyijI}6=?7Zq?LǓOrZKc'|rŔ~nA q:+K;o6 0_[u 7QUWU-GE[ ji5e-O/?^Q_H<^?e-/1ox9+ԚOz 5͵Bmhpf"# `ɳ#tMZ ]*h-WٵleZh 1>pҖ~yFjU)Ҏ;~qsxt!h3 va5p{d|UhA 97ȚwMy& +Cy+_ެ܌=uX־}ecSP3rIgիV˘ʴw9?[[?gμ֖+d*U+eP;097>Pz2UM6MOcz嶚D&L}6~ )A?#ϴ0l~_?n}߰>j\$fTBڵ+ 25 59K񨛲ФQI'Λ~C/ ܭHgm WH5Y'>pÍ76'13fox|91Mƍp Z:.q;m`n#i8ky:-9])@qu jry LSxԩaҥNl%cWLG8|I;\xE*OmX|EN|vayu]d-eտaCu#` ]?~߆_y9|  ƍٺ[f0 mj.Ll5:^za2 Ӈ~R9={ \|qַi8w]03f wꏥeҗr{ ѣFHٳd:ToU~(\;B+05Cs?mZm?9F_jI/%##MAS8[i?ǟ]G[R4dI*vjA,uaKwC [4k¤ݩ8O&Cˍe x&\l;=?a?O; y9NI?O$B/4a",>_ޮD<.<.4i. 4d.qq 9χt< ۷ [ m˧$ !Kр^L2%\xIA_^WjZܨ˸aY 5ri1WÕy0-&^JA /PV aʊ_\ݵEyvmiKlq(62tdEcI&N:SA,#Cap˥nj*tzz=-Zxi2m|:Y,a w.1鿪V{gm*Cٿ`(uϔьp?jrmrCiףYaV9sAr;.yԊ_}Ռ\[??'VP֍^z* u.a.bB)RK/R|7ߚb\Ui'gu/^ -"xft^ 0rZqy e'"<@&nFDGvd q<^w%:| 1v)h;=<8=?typs>yZЁ'i19|! .>>m96uP3 3jdV3 %8GgD^lk`z"c^Vږ- ^[>{T\5V(1gj;3 |!g9͛;B Eoܨ-:C?AM+Ŀoh]ÐCtQCs41怜ZVNyBY8qBjv \/|dyVOcPj&a`K¹_hc&_~hQxG_mwɠ,[Ng]]o31/}0iaAEMp]xH10a8F4\IgwWtp4>-@T#Oa#9'p \!Nsz\pE] <uCËN l_[YwQ+-ĕU04bۧ 5ԍшeMpG͠`x o"od+a2*/@*Mg^#@O?_~-"Ʒ휰RB+Њ+d۷5\@?!cCغ]]T7zk6:qXnrh-2m߱- 88:)Q\Dp2=^B-jGwNrb;E竝`yy_S[/P1ܭjܖMQGJ!m&L2ո>caAg .4u mHrхL܇wuykݻaEտZj{tYift%:Ui^uU777Qa{'xTR-5u?=Ds?/cYG>J/?|Ht]~lxmdK\gP['nd8qc[9w<!-8MmÖ;DžMӊ15:nWP``yߙ!4yIc4l􂑇'/'2Dx"=/y|ġ><<}2lT⾮B/euSΒIBsE&q-dEHFo~gI]3`"ͯ⢂}_zik#"f;dds(;Mos˧J+U~[UߗA QNguoG,Mt)K .aGlE)$Pà&7lH:ٶBF G ~ ۊו2g֬Y' Y8a!aK}kaРVng>C=tVѣG{y2YK HH{]EK 3g j]Ep9g ,PqZ uvshN?뮻\ja%yڳeOjRQf]y'?0eO_ݴD"?Mcc_/lYdP%Gbv:uab'!#GEIw08o09 d8,>azy<#td8gp @]I 0pqq4(n8N}̈quu6oV\iT0L*,!j HpCl7I>g hLġPZDV9Y|CϙjU+2A,1, m9S5b/?+Ժ 5 Zݧ:t]βGIVhp5!gq?xȐpWϷZ&|Iٹ j2p 4|D3Ԡ=\gpɀS{ K, wRcYA'|p(=:ڴi2XS$[SO(C׉J5og}M7fic-SoVcqTARVT_͊>x FZЯ?w Ub>- ]K"XrXo/JYW,χ?ֿUTLYhs 7?GSKF[> 5fVw|Y?|kgiѢE׉w6ApG*XgOWМ4p).֩E08M)2! :'_D!8]pyyx>/a҉wyM8Íe .07 ~]]]݆׵3)+2[$j*Dq V$1+VVXgybֽx f̈́yx+UG6O. G_}18Uc ^&*]:I 'do/bhh].m(*d\zB08_XtF6J O%Q_kҿT >׍SGa}2_-iOm!!?ifx H֭*$5vC5_sFZdc{;NߒaPރ C8vȠvLVXM68XG{>xpr<{v ! )1R!$s't)X ^x\m>N y0VjT4D{js)94"CHr7K~*W8`;7㚕uK!,=z񯨬,6ϵkr˗?RH?[3iߨjvceC}.%WWx"=,oVMꗟns@BX]2h9Qƫ]t6V rG峧T*]Jnθi.Wr(cԭn[>#濂;?&L6]+c] c{_/ʿa}\J@^3e_BUsP3w];3- rԯ/B6;/v3^i\G_OFs?7?RKOm4'6ψi_iͿZ#A,i04Ne3zxF0; >qnsF|6FL>8#z] ( Ek4|#r+g]TS͉]J ju<,Y?P ~XrCӦM]: ڵkÄlTö.^;}kJkOl*`lg:Cw u# ^Hm[p]jj~p)K?vq>|xض}.ݻArF>S hEۇvښ4lڲ)"/#ޑ < c3'[6i劕R;dȐtN[[ڛ?#G7.|teKTk&1"-gazޟF = i!CӒ%K{m۶Xjai5u4Br.][t_Zf^#ӨѣӖ+RǎT&rɨ|يʵ4ʥ2= ? E~lEp5T?eW20pE@Cvh&L8FGrMYs}@u!88,[ux`hm-8B}B#m;ȷLEev#-0F[CtA @N.Ld$ⶁemeٳgO"ZZQqrkD+h|ipD+CQ##3# DsCE240?,ٟ פ#G =h Y_KUWƟ\!m)O5P_e)o|(o3~z-^?֯]qG5~?x{^0+t--', +qyD!PS2?4NޏGgZp0e <>BO|ۦ];6<` ` y. i.(Z#9s'OeGawbLGQȳ(ByerXyeE'e.,iU78Ȱ~W i# x@CaI4*p!ں]7͚5krGmWeM6tR#=f1D,Zp:Uo+zQ0zԨ?CE3E?vU{ޓκRQ'TC,]/VPƟ]?C_m䞤Q20D]2Z: Jeq&cUSeUU񷌿$Ɔ>֮̿e١֛7z]w^!`~6~ec0:G.^[@cE1*Hc-(Gr<7Pq˲9 <АFgީrPO5vaa$A[X7t:`A ѐDc&#-" TќRwer ?Ӹq#]c,lQ0WK+8p[GF5PCOLj)͙;~-Zx4 2W#CY](v!NoAG]/ٿC Z_?\=J5P[(OX2S2?/5j~1c4 -iˬua({0p@n4;mVN"Y\!xDžbǙ ۠1-4p4"5@>ʝ&C\Pǭƚ!]@ǡ< F6iۥh-L8c/2A6H*` ~!Y4y-t9w NYLhVjx " CS˞Dˁ8Bd\88<Lm޽yfϚJY"t!1*7vU~Idž=`%CN3!B4rut`xxmŊψV?xxHXD [S7kNb*OS߽_GVze(믲U֟e]W><v3az9<}Cm"Y90х/perN 󫡎4v.L&,;v݂wK(P@!l\6<ˬ1 r'߯n vZ."i\F+G•w޼!V7ra/o9z gYp t&$ :7ooѡoub:@)ES>.SOC!lNh[K+o 4a)O^[izG,Y/Wzj24Bem3+Z7>W~9]gT̙;>(;짱4qVug[ΫkQGhy8x 7h,Oр0w#mp3Ȱ>{ mh]2w!mi.p\ĭ% /Eva{L~^P ޴%N>qxS>'a()Hwny>@bp ;=n0v6 TC]D.ŠCQ#jřEV}E5 DSٗS2(o[ߪc?WYףevx7n?t_4cx̉C K JW 剒 .M1PA G8R,eGBz;aaËRuq@=c^ˬ]`!$6i][!hnCm i+ Y/׮[wɉg uDsoC3#K~EPӡJCR\/2񿌿'2?e)OW3nj8kPM|t(> ʯ8Cܹs)y.(ptFi*H83z6 >zz:]tI?,<@tiP ׂ!d LO-d jpi[M, f^DϨQ#Blr{?ߨ=џR)(K{Atح}//j} 1)j(OS<:2D.oh篲(Gk5a|}fgFM)-)jɔcC qMc>2C 5a]_xpu#StS_6Ҏ. %Nhg ɇ-2XM" ^AYS=IO<9Ԏjmڡ댗=w5nװPE]XѢ GPŕ<#,eS*3v^cF(~EWƟe-OrUeQeZeQ<ѲZW>yv=?v?UZ .'wuP\v77 ߯Qby<>1!z3/eSgP6x*|qȀ4Z.qg=;p̗4H: Q'wڹVtcˤj_[ܟp1~%z()Bݰ%+ſPZdKσ_J+MB{>g ߚغ 񧌿e)OSXcO+;X믵kʧ|?8@J.䷛|g# P=dO>`~sCȇߍoN3B>iBTq%8v8cso#x] i'FGi\H惦N Yv:1q_Ccrݤjʡ&FPzYPyr>gJ b8$Q̕ZCmY=rDc%HV2v?۔Kw{_;[?wv)2񷌿yٷWw2̿eݻwݚu?~vѡ{;COQy*bL\ugBg#|U9zi^@]x{K'm}KtLK.Vb.8-r 0='u8t0{IP8ίxhP8[rɆ<2v xcZ*|NnF*~J,__K+O=?e)[̿e-os#jءvО3 5Np8 ǡ2(@]/8һ+-a1Jmt=]/B9e9A!r#rs8͠-H4k)g ϔ^ʉ@ 5SMAXl8r4x!8j?j^4K134)?+o-_꿴"h 1C)os+O2iYlvZ.Lgz!q la6ၖ² @'.d,4q|Cy'޿Xq3:&{NtNSWIC+,:+VHJY3Aw??/7o{tjw>G/`n׈t遥X;`{8?|5QƟ"񷌿e-ct]C ˡ'G|C ) 5;prǯBCɧ8S4tM_ud,.d!Zxxa<|\yYw [BP‡(4(q>yldWzR^lH"]%ey8 )۶`A eMf 4k KH/AlHAh&BD,_"&ʑ#Py7 ukQzmΉ~ir-Qn?Gce@񿌿2e)/9neDXeU֟e]&{j9=;COr)s9z{uú `' Z E@\5p 8!2̫h#m=#p 20AICCG7iEkZuv⁏ yF;LC|g֬ٓ޶i8b^"Pa?TLAXDZ AO_+P?z;[K_sJW߾2jq-O[eQe4x4 5Y38A]nmƏj1=ŢgYE% `ɳ#tuZ:OUІ\h6=L =G_ OH@iglf!Z+%<m81hĎ4ZmwCFܲ1W;my3OP;6)Vn SR#@zHC\&?Z( v9B_%2X:Hg@%;`?9W_iyP@YDw(?UCYV ̿e-O`9dgO߹ߞXf*B;8.h\o@UuY&!iAۂ݅tލ˖hHy׏ mٹ+|*H^̹ٗ Xgl/iG c4emK+(2?e-돲r 5ْƏX=;]$St]o*hB=|h-8Ge6t~ e[번yoLmX-A Y/4qx1+4]0hl Le7r LF%9 yN;mz+BP0T=yϟt1ͽ"jJSU"i uŊE.p_Rs+ J+?ee]nMM-dzjg|s̹Opʧwa.!n|.P E`q" vO޼N#"}Gtw exǽ3H -<|8]=r qeCX!-!:ud>֦jwh -_,X83;̑"AHfU\y+V,h2tBLEԳJWƟ<̱oS2we-/ϥeqpt'r7)`==3ʧ^;HFDɏe[EcW|9nQ y|z->{Ǵ[:bHx8pxB|wICg/Tȵ-ᇞmE^4ti/PW抰er1+ hBD˰S ں h,ˡPm DMgϚƲ NI LFh8%4pc_|.n& \{HdJt ghW: /KG;ƟJS+O(o^1*6n[2'?񷌿<=?kO=C @OD?47Դc|"^1'iZ1=).9/EiE>>˄0 iiTeEC -ux<ǭ|.|8*ڨ:,CZpQw qٓm+LpGe؋D]gGY,!qI+W]*qLO|Ew PB ?!N*@p64xh-:Sa@ ]]t6щˇ4x;䰽.| C g_]j)s);a$sp)F#xY:"%\<9:GPUȭs^G:vP,.]TRuipƇ跌?ոѵeY̿Z`ןe1`W29H9#ean)/ UNϟtx4?֮Yǝѡ9vL6=ߞ3w̚ˎ)9v)gO/'Cq%B~,4oC< <64c]pa p Bux/dCq'n9v9o>pa&Psr7 HErۣҼ^LW؄,4 C ZSG̰3ڽ~ۑQxԺ|uU~jrj&ZQr̼]0"Vkw?ѩQZgKLƖvAnNJ@>&V$H)CcX J;v51n!?e-:Ib,-O"S3FguP5 bC}Esǧݜ'%`1_0;$1C2M]15rӁ8Z'fނxBi=M%XٹEZ*D +i< 4]ec4P\hyf:ʧ0"$sHAޕsE64Q\4Z0J.b, ~骜'S>[(;6JȺ~98{|zG]~]P]7WLe~jTF;O+ځFfo꿢 YxEx;Xh֭ צ$*;vBsO/ߣGE5{=UTci?^Oyw^Zzn܄ SyXgC qN|jqEqBO:(ڰ.0Cg1`#:\\QV3y2hk!y.l g͞qgE>/tTYj^wE[WL":RZbE hFԗ5RMUOMۚk:KL2tȈ iqiCR~pql8tqQMFOG>$ .;˪֤]1ؠ!h8 ouر#mٶ#m}䑴nʴiۺiڭRNMԟ \9Γ2Eg# !LME7?RԿAiOߪE,Q:AP_=\GUW_u"ڐ+M2FUt2u7ZE);T5k׭g}CKd`s;o1$Ι3>?U]2?0i@FGR0.,8۱, xB<ә/dq|6'# hO'+J]X6n(q acaCG>@ǁ>;¬?i.UdCun5kdNkj?.h"Q!@ׄ-]1ՃXʧN+~z?NH 0ҟSFX֟L;OJFMJ#)gڄ4b4xȐON~JÑ8iA4@Q/Bd^ɉpE} YD;v!`~6~eG|ƣϾӃ#{ 7`h#A\8ΣdyyXxȀ6Xu8ixg恆4G'~]Z}iEriڪnt֏OFϕE7:!仸2񷌿zGSƟ2>{]#oC3fPKi9k7Ԫxw+|CreQy:F uz;Ȅ:;cCd~E=B7=Ғaz z|W+ A':<.BNC -k;H6B^G!B4ny< f3#;ۚ9fNBoXj| !s\ǺAҎOH&551"mܢ'm닉vgw[YSPTVT "*/`tx5ݴ]1_;oIʡ6=~ݵ+%Tn4XAn?ڜ?Ʀg?[2̿^}/5j~C-،0wlF6ti鑭;3ww}V?xbwSFViԿҽj@9ӆ2 =t=҆ڝԺmE_3kYXz{Ӊ'mȤ_a!^tAu5vkkT_p#&Eoߺ-H_S3L<&}M;zޔo8E/(={_'*g?96;]yi}қrvzs]ݺt]+QY:Wג蘭畜msvW]ue:Bt|.O W_i?@|WwKTk/k? 0]o&{zo;gb8;r&4!jLɘX#Evvhq$~3Be @2MswkA eVnvlǠ:q(Z$Z44"+F8xPf•ec4Nqݤ䡋<Ӂ7'X_Sel-y" eCȳzHLO r -zqvyN x%B8Mr,4ypj7pHlp=΍$eSAEv N+t'y'_>NwrCM;ԆK+nNڧn2UvoYEDCP@fUsZHt4]ecG N=$ C`8 ('hJjNܚ-ݘ~{d%:g4҂ڥmСYFqэdxjN*u-x\{2_H?ς2`OMʫJzh߿a9};fw/XjoyN Zf/_wߢ8'Cqx{yYzV ̺Kݞ6j)]Q9vWO5(oLOrhވPO3:DfdEsQf9Vg/mFY/?Kj:_^W>?n|=աsνO2,|2/"Mζ:].Nst p w`x#1=tɀޅAm88V8.GƒoƗuZ;Ȱ=G?sBooZ'8)0u{n9)J(x%Z5J7"D_%rV\GQӏ R>ՏM\ǘcCx9f24\v]>PG@reS|I!'gYk9t@;jH8vhz4A <0 QAá);h׵CzMwM/8j#2h@3rPڴqCmQZ]9ɡvf2YW|dJ'SN=5@hߡWLG8z׀,l/Wu>9ԪF96>ڗ:Շ^j3g;PN کew[7͍o^~fVԾ|9Ԟ)ҏ},vmoq/,:M=nǿO<1]jfxjb`2nO)l'ʎO.0#]}ү@W鷱?߾(ͣKfY*"Z}45o4Ob~ `v~_՛PmGCZRKW;SE[vu?Ǎ'?t[ӌӣΙj8p\KeqE0-4ijsGEA |X ~yBk)B9/JI VϯsŘ2!qtȫ;ɺMoBGHPkrmr{I΢v8(Kz.xcT: G3Dϯ|j:@i0 ǃJ4戙Qi0;V?6P9>Vz7 ?:|t Ǥ#!a6P|ӫjL-8 >>LcС /\nB9V ӢFm}GpiC RMwġѠDtrOx SN{oN_qbk')G1OcnjjtHwy :~ӟiOO&Mp}o钥iT\t=6hT~*7y4i4B|˖-M-NwugEI k3qȡf97b;kWiwޡڝFߺyKz[,-Nv12mЎիפ[n}î;Ԟt}L|viڴii[MZZF뀖'?I騣=U>J'iء=%s3vLzOJGO;DNo[1= Oܻq]V?BeySmJ-ݫnKK<$.9j}?ƔҒ7Gp ;)PITih@IDATE6tr{7UUHiU?=4zS2੮P_Ga^|+BeDf4+' r6՟oԞGWŕ]3i!r|ޔ6>޵Q_qmԪ IM?jL:f4N;p񽴝Kn]2]鎅8R6D;F J[6=_V-ѡzPC4t"tѯWh,P3,'߿-jݷM'kg+^ EP*=ae}L՝ar}T.Weg?@;ퟝQo84lfC5}W82EYwc=69P (es83\՝v4;(j [|r#y1?"?ݿS6?4w^>Ew0Jꊑa#)8q %Nhg ͏gv&/x ,^%t|_P;2viZ3^6zf;^SeB<!@hTq3#dMءzs#!ogGC;Զ{b+19tDZjCڸI}Mys~iQϟzd:vh9(p2aC M ut puȉ0 uH,5K Pj}'jΒ\!r޹^ULclݷ8i 9x!QLw_fMzN\WG| '~o}+.Z/K;N9v#OaÆ]&nH9rQ(|׈K.^U>FE5yj۶mbNS}2 iNuZ:/kkץ;(vYw| w3C.?v:ojOunw裦}CZi ӧ/D;x_|f:_NC?Kz_^wۂw*t QZgһ\(}e)O:+o#/17};'+PWk` fl؍v7#= vdZ7C|zi"nK>xu`-im6J$u}#qoGu/%pG8Bk[`GPck[0_R 6Ge<ʩe >L [&ծ8Ѕa 9_PF(v層jß>SLNe骍M:^}/?܀^CC?zl8Ԏ>bdک<3Ia ډMl))a0PޕkKw.ai˜ai#^9RoOVI}wiFЅS{|C 9 dӃ+ΎcYرM1.kΣӄ :~v9=XJg /zQaض#EgdK HOYU5kVzE}^xaZ*ygg|?r$~9'?Io8D7WXU?r4hqj|x/T^N_x q 4'ƋE899sKӑ/62'?RxM4,A9<ݡ]f{SC Myp!jJ6v=]|F]CrP9]JOySě!쉶s9oi޻N"'vY),"t^/W|2أ}gGOޅ^n$& :F._,&@.7O;:O]g=ue/_q_itQǽXպ^D}/?e-oTmp,Ow?}_d2Uqc)QJЮ],ɍEh&Ra\Ǖ ih4ywqep[OF\|AoN['^WBAɷQE8ӹiVixLGX\p7idrZx5z&4gJ 9W(|^xɷt2Ujzr.t+AX3grIǧoq8ySt(^\Q;ԴW.c?O^|ҴƧ'MN&s34g:Y{xKZִn>H~ڠtCkC dGIjpiF}AP{okv֊D}vq(A] ln?5Zʃ ^{!rZ{V8oXzC׾N=_/~M9tWF-'Dy4ewvk.K/xsoOܶm[Ї>$ǂ4Fm袋<LךL7Λ\C 6oz$}Beϱrj}Bλ|0T_׿ Uy{|}7rI#6}_IY5?J}:uE.׸ ~_"e 5kO:~M}FJ&Uo7 63r(9Fyي{{ñ$^1=Z}29npqg59bSE>W>WTO| 5X~_?Mתprk{W>?{d/ծK[lN{K^"gg㮥tvmV?}v{m ӘcL<~'ͻ馆CU{yJԷ|%kW 3[MNOiIeY2̿}][o k2~l?u]{?o}ޯN<51yq3J8tsF}1u=qٷd>MMO.Ļ$ie(rp(<:q2#Ëв!.8ب |#|t#aٓPؚ8?Ot+>&Ą=$Zʺx?"WO1PqSfCo{T8dhZrvm}DF{Ӿ/^sq`~ӟMzϻEC)RZn]:sÑg^?GҨlB.J߰nM:V}O7U9G;>C fgw_׭~gpU]6?5_Uv %=:hLwͿo2%C *#JCoYǵCmzªipN-r|QIֻЀ 7~Y?e DŐ𓟼2njc4NU+J헤_Q:"z@;1w[ɺ_הUg_0F-m?}EUU?2q񿪄2PS[t3d_J{t?VOC-n{X,Lo_u';|.X|8|BE *pyG OBp~;HGyƛW`zCj8le8\p8\8ȧB'4 4| EqAmP4t48@l gcc C8:RvB),$.[kdU qn;pq]v~HPº>p%86=PáT rΦ4ugNJ2}BNeqӳЪMֶӺ[c3;;oIHkfu!F-:;K֭++ =-Rj`Poi].vzFG;H?N?i8/^jiVagV0WY~P;J}9gts)]G}%'dZ﮽Y.7P>d iґԹQ~7ݘ^>1~O_Iѣ>Nι>͚}ZB~?O}:3J|_3NXPi7 ڴSH $}6?99/+tW۷oKo~qzjaC bLǡvvct?иERo]֩W|w^~p(}˛s;qW*44]FuYr_: JBܢ_\_Jc\)Ol[߮?jr?U ɞ^t}REs=w cZYQ!Nh Jxd\#_ ś8|q@XCmPYEzmGůd,+w%hA^(g: VOCoXxpl!  i `>hGYgOG@ 炰2z؏8D0N'a}G G!S0F:QG[DMJgo=Y&|Ma!үTǰ+ßPt(@}~ʇӆGDg?nJin۪T^3&J/Q(FpȠ1Teᒵ8ݺ`yZfSڴQt  Ljw%ZPSP{\<%85%?vo *os?gj^4M.DJ/׉i]9rkJtuċz^]FG38]-Jy6B?qs>ʛב~ʷPj~R/u?h}S_iE;/~K'+ H?sNlvZ.L^Hh+@fG |uzxDaz g@`qq<哧EY7#nha㊲KZ¥gFx<WyH=%I~0 y'OP{p^ܨ]RԱ?<}|2xvUMjC{ٖn]Bsۖ h:0Md wݧW>O9ԶmK|C?V2[j|P'=nvwk+_0]y^!d7ۛޤ+obeʁ)S*ttCz<_lԡ =W\r*VbT;voc]šjZC Yg<=]ӻ~M7-&[o-]v*t7>OFlJ:K[oMN6N81f;vlR?LYPWNS@[`HurE;Ժ g>]}Dw{;<9.G[)Q-q()SҩmUrDgMM3&IVTCKE# ɣn޲5ݠW=uhm{vĄiMw/ҡT?Wjz]v^'34TN/~ыP%B…_}U:5 )Mo]^_i fhwaN\ި뫮R;W_הW˿t2}w8.\N198Ёde9u.L!$rj}'q0U]W>スڇt(AvBRHtkeE^j^*vzp`djW{N\tr_IY-rg', w\{g=9! K?1}Cv7ԞS+\NrViu?d%ZG^U7P9$9rdU;(G)e_+k.u?b pzMr'ǩX%8ĿE}ןU94_*JaZt>^e/;kVˡ%jޡVV[}i1C5eΙ;g:f 6ºJ#XD4#LWO83n? 5Ө%Ӹ9߿ KO?4yˆ#ڌ)C dB`GaDxڎ:RV"}s J|_%Uo%3piV_)^ /ur|◿HGL<" _NMRG'vEm[_nx3+5~4|l,nk|^^Zώ&/ڒ~oi}3ṁ_M]+9uu*T^r%Sӳ>;v]WV͖_.|C;LCQSM|Z!mh%QU]zo}3svqw~ݿFEjECYEU3`ɳ#tuZ ]*hCMi1H  Ƀ<1 L=dwBCkdԕB 9 8 S˶O@e; qkc]·3<}CHt*`09YL%#ݠ+qEt|[+"HA9.09ovp^ dGp]Yzs:`HzpW>PЄOB'ĺƦtZy5<} Ks68 IB$gu$jZuwhOw_gQZ_tsɖrM:W>7W>Xm#l+ zj}WjڬSgEh7]*s_uzk_'U9e%K@a]_iݪqΐc=i~C*= ٪]nJgLudh%aO*EuOޮC -[^a?k,!Gȹ@f[e/Mwʮ2a7=I| qLMx.?)/{ٯ=s7~u]O_ x3N +>~Wr)Szo.}I !HHsf7ل$$73gN>"ZzB|bCvtI N8S cBI?&>#[=  ^h% 4 ^ /t&niGBqE^d1 \S>lv rlnvf{ȅfMUXc7^&(BI%XLׯOǠvmTehצ^~ 7}gYPTCJCm_9Iligc z?dG| "6;&͹"ɊGrDcWc͛kՒSG',YM5xrk㬰j[:=0LzmghҸ] =ܝJHLu?s2-҂ZDr&ME ܀w-Y ?Hq%W+;8 K23d*Y/G#ѝ3jK7eL #*:J (_=Whj.EK}&u%eL Oǟ%t[P+evnv T9#ePA+fH5RF2zI¨0a\Zx\?60ڀK8q¨O}O8_z3örVnps!˓O0H<ØE<<m#(hcE4??tYa@x'##FAIK >=%bV+(!}LLtNqf% iH0-r;8nVSJS=!;f3Ԛ| gJV? ͼT f!l{>anMցX+*eP'γo\PCdP`e<-&[Yc jhRO!Qcn*39*Wj ~Xg7R)ʿ]vꩧ؞{흄zO-_ʷ=Vg.mQvA9_>~eu$SFgme^Ga־s٪UW=*SH[y9GaMt.Y>(~⦼-?ms}m'M`כmΈsorr-G::@]vf w}o3?? [o6\$eH&O7l*|Wf||p̸CƩj"UW]e6aK+ AfyxO1_c>ͰW[ӧo͝{A / gsκ8 O/_nws{fXғ@n\`!\?vdp@+пOO E+_10f1HbflZ.42wMSq1_?Y?*?,bguڥTF[sS`Ƨnv)n svl) C [T/~pv-q񇝇0X&oI~xxDZpA-htB$! ?$U"aHưL8 |?Kq|hĨQ}X[> +PX62.1 #BVH|,zx4eup3ԲjÆj[>/ѹJieϋjzy?џC1YK*6 |q+5kֹAOSQB;m'ףhҖ25^>Mۉ7WW.:{m]&O.h~ݻl._6WF’J6ݻJe2LmsgV_WMiJƴ EW:M74#6m}Ki3l"γ#y5俩m}e jӦn,g3Oo6g GpD*Ť[^K.lͪ5:m͙3|z=e#_Bt 4H6v#Rf+ 駎\m𠁥.-`ij)۷ݻdV.v.RyOƪߖ?;ܔж_܋/_'F4K'Bl7Z]$y koli'Ǝ;Ma[aLJ0~EP^S7?|RCx0q@Ď?BBC:06"qۑG':^gd-$$EbH|ce ]$?rye@tBcYy 켛j)),MZjxYʲA^BG!\ޚ:ϭ!REUs j[^:Cmh`朥K Ԭԁx>}?ig͞tZ&jwyY[ÙLK>M(o\Se̷?;&NEѴץv`k׬Ws˧ j˴Sgm/Q~Bv_W`W_sTjOp̲fBo45tnfKd71nh]~.8\7~N2 ϵ2zق?G,y͙=3]*&yw_15?gn7寲JŁxƢ>ݤ9gK6S?/-|wYբ-h˧L \&o0aDO7&^yz.a/>H\~ЈdAy^Р!Y6]4gv >I<>pfDch&>l9a Zh x hK<ڸHF)2wCYd^⁈#ı 0V<,kXgZt>Vx:m?(#?7wbŊ]/GE+_ Q|˻`}ߩݔn,mֲ?g4xj԰R500 ^Ň~þ< ƺx<@n0㏴"/OG: h#>a7 @^YD$}#Xy]{ƽugy؄mwJPjz]ҡ-m[+Լjտ l7kbk\<]ttW?8!-dY^amⴷ-t` h~Vq`ϥWk+ƃm[Yߞl+^APӖOPT察"5ǿHx"ݲ6ԗ7o۾CTl[WqiW /x'[62➻_Q5oY% ^Y7f%36z1>_-kRh}._1S? 26Yut PWq'^ R.ƈ4hxyZ zuy".ˍᏇxl\7(/q#^ލ(D.#$"Q@2#>pr.:H6щ aa#y8V1jCUH͚1 KֈxOSY|rj^- Ł`|ߡ}G1#Cnm.7O~ӵrS<&^[>u)A]XӚhf/ٖϺ3LvGV>_{ʐvgw2A n#О "ʠ6Q jstg25uhBmPK ްY %_ 6yS'n"vϙ_Aw_IZ%nP-2moi`@IDATAk?ʤ`c;M\J@I0\87hg/$n^,p<|6q$ 1 \x npCR4bQ}1RL-8$!}iUXa,H-iq#h fD&a G%AM:8_Ӎȼ~.'y;]ݠa6 j9-_f_ h[[>2uT6΁7{ ԴBhgoU|hg^0̞K [cPToH8h}_}UV}wuK&^|v:eO oH)AN83NziHw_Ͼ2⮱˯*Jc݆ իVڕW^'?n䧬r:օ5k|nlhE+_ѐbIM? [[>t֠ϧl{'o>֖#ƃ[N < gYj>t6!CNO8d!/h%IysEf"Q8E^d@8\0у zhG \8h2!?CMI&X ,#5D3"@^FȌYA H_w3Զ~]')?el]nԖO j&Mʠv<ԨTԭwjS[=~^<%{i\[2|S*3 JO۶-OP?aVMAmn\ح[ܹs$1w{:@hNue)hHzmֶ];{U_wݵe}nڌ3 .H9$*BwRQf9/Goȑ DGs9wȑ|ݠֿ jMm|+jԛ Y%z?{2o쬔ue>*P8tg:Cm:7-#2Z֒Cr#N.ov2=7I薵njm+b^#Z7/2<X%p+UclVtڷ2F?^RoE?ѣ\]tх jǔҳ)FOӞ֓N:V,_"]ǎ. e(#1A!O)~b[ t75EKS>nFTEۺo%>w閶|񦙔o9=CdرӤ(=a"5~5${ tqa7?2EFy YC< <.@\?xŃ=~0BEA,HT>D$? 0xc@C_B'.8~h;͖OV J7D KKwuORWkìV ݚ6 mJmMS%aoH[P;sɠSyQAdE'j/Omצ-)&u^qV/%x},[83-$gp^7}%)Օ!d Oڝw\Hv !K?5O 4D?vZ-G+TYWcAmj%ϊus_Ss= 5c mRnM!d0<Ƴ0Nx:JFW%,D(9ߪ UR@4"%IXvQ2o H<}n/ڎп#cPK[>Y>dPkjBmPzl_ְ[>Ed._4{%{ٶf}UWʚNH_[ٗK ֭7 jo~ 5n:_C˿u~Oگ~k[rVƭϒYa ʤo/OO?O߾2& fMYgz߼Zvս*lҥʙ[|[2i5ٻW/2tWkݪM1S+޴˖={ZUV֕6m "*嫾O|l&c֫~)|_=*4pM>.|2d^[5lQv+Ɵ.ښ;pڥ*_b aPCQGΖ7kn[4X<_'[Uw*3gبӟ 5'-'ȵ:zX `oتի};tӬ+3$E2EllҥWRoWz[1EݮmkӺVZi ~Fun֣G{]k̙ ʭEV97{\Ê+_-? 1i)[ǟ4/b/Ɵbپw jn]wkf?{coߐ7vtsQ0\&'Jiu`3p-..x#\ቴ&M9b5Q&G֗#r($f-itgBQTj߃A- qw;O*ml=:t'C.ּi [3ԀM? 8V[dP.R+Sڽ`/Rc5joߦ uڄsmj:CmSwX-px6?G{]wN8%cU veЃO=A3: 'Ad5jd_:Hg?c*6S%cKSm:l[{=8R8T}x9sOnM ßG:KС&aHTM|'NpP1cX6PF _st}tkCsjܸ.8=PkԸI kW;~=쳞b9g Fj:^{ڱ҂q&6mBw7;ēm :w8 :tGƸU tG>[g_=ؔ+щ>R|FqA4uꛪRvo۞{ic]xQ q men#Uz?яlGh'?rݔ~ϸQbG@Zi8g o>BE_㯚F|SF&bKTOaPX?ޑ= =N~f(%=1DXx V"kZBz2 ă&hS7K4!'dD8hĶqpsz2 7ā?txEb6\G!Cل#]~+Gn^/G-dP(h#yj_60]H!H!'|Vk{P Je{/)-_|l-ì]~ַ7+Ԛ[ WJ4ZV拾Ӿپi' A U 2T:h >Y&1ϛjj:L*/CgMӚiRJ7J}QV{ܮց[qK.v4kӶ:[SngϚm_rѪ zq| >s*t27k+ߩz|ʠԙlvRЩ]{5֮xkЏkqƹ|4vɮ#>ҫ5@$D#.eP0f̚n>.B무 kʔ7k*[%lbVe[69os j3D7K 2ҊluW.ͩ9$?餓U']]ǫNȳ.?ZJ9oi.ɸG'>z[˄xy|߷ː,/+F>:T_|.+&&O_=qdKqFoIz~JuS/N/Y.1G*PY:1_1޿Ónwv$ ysK7>YCu?;]5 ^D0.y㙂^%E4&p:tB4@̠K A!qCy( xT VXC44Ev #.x-O<}ВXFџ3y4eP$orb/vYANEUq>؆ʍh@Ixyx+t&-~Y2-~tS:"5.:yjԺ޽XS]J֢j:5U9pPnPMGR\j2=/{I[>׮\JzozjgpjVt>&_Ckn{I',O>n|\޶CEƍ{nll>o컗gRS?C^sͳ{WJOA[nySw+V_ ߊ#dvš~WatX[Y*{Goj}Wճ>ۖi~)'Νn?Ӧ*/76i_K'ۇҊ;[~zVQ_O}ϒ  C(B--m5E7mb"ڦ>y]jmj¦͛Si?{uJK/~^//wotjgs.!#r)?~T-6SvukI'h+V.?o؇ou9sfۣ>m3lv/q5mC>d}PTdHG[|ڰvw~SH)+鯫_t%; j% ʟUYPa3N:'bڴVjD㵢Wʷ[sϽG٨~4[eWk)GjyEUPb)+wO1/s~H[> 41ǥ m,RTxRH~M\ *1xE t *物Z~BIMH\"!ą̼=nPd7BO<.\tG\r|%9ȉ0q #-CB $&`ٓro pK :!oO|g)+6/5Ӻ֖ZֶK?ճ,Amv6`˥v:T凌i23?z^4y8h'gnmXoתV6i\[4o5- Д=j/yڕW^Et}cӝ"c=%<Bg}<+,.w} i%^Sb%ܾmɧ_ur Cȕa"(f-٘^{;RRcck<MV«ҷ11FqB{[ W^is[ʫO 'M7㟶KvKJ0VdIZ8쪫⣄U{[WkàvM72Lyr_][FtmdA?+(^ M umyA9R G20 AR+X1K*[m+]#g kkq6wn\[>{PU!cfi!?!;Ӕ{њQqvbݮ28Aa4p̕cW-/VzODV2.Ѧjɠ׿~_%O v!jøPgr}9UmcI{?K%߶zJ A9mk;_Wab-_{w-^WdiJϗ4a"I> ڠKI {q8da: '6ဈ}׍KP+d$p$F+Ї?K|$^h!OF1""=A~sB >hыxZ|!'OQP*E7LjxKus^*<D_RZ0wжL|,uYgmozwF ^^#j;3N?_/hwj?YV\6Q[s^zIƥSʟUNG~Kﷇ{K-UqU)ЇotU3}ꩧ;ؠ# <`d\9Hƕ32Е +/k5`ujƟk\m P%u.\ ]~V^75k~-\SO=iizi$ V?GO6 Ŭ6\*3G}4O<؉ Nɮп?"Dr<@;󬳔j?.H/kܐIG=ݧT/]ww\.ڎPY~Kw~ÿ(;@VȘ7|=`/jaôQ/ZU8$ST2*Jo~{Kӟu]|?،W[s-[hJxĄq/a659ޝauG㏗A1&??'/>~Jl7 2ƎK IJ0x-W" Mu\ h :h#<}YN%#gܫ:3 g>kG8JW-CO{uY)K:#+:?c?jw}ӇAٕZ3nԪ.)S8 CٗÍ?IA?t0b#WSs<yYcV]#ɒsh5\CǟϿo烙| V:I ^G' ja] 5]Mˆ#U&ɈhZ1xoō+N<9Ǡ~Fh+mN|ɒ~δ:epzr{!lD [)/T׮EFz)X_2jo)FѱBmy8k:mi>O>nTXKo5q1py!)85?ۙ61$:(D)_RȏEӟZb4 zeb/9Oc)ߝz]h翋.%؞wBڟVzcPЦHS7T0vA~y}D 4ـAX&xz//AQnG W?OYTABC寧-zj>`RLnh _ƙTTM< uztՊ#1!y=ᳳΚi5XldޟFBhuǟx0Kvw e,w)}:R dcPonA'|;csd||]|S|߾p73`Ѐ@ʚifvqkLه~ё꬯G{̣0/R2?oƙb썒_îmFu'ω'ˠK ;8;\wT3i7-C^{9f6ck'?);F?PCݝ?ˮoc'+*չ1qڵT4r!v΃!U˖owj֬sO/P]/ʿhtS*J1 $(Ɵo1O1̿/q-iJmigi(O>K!AAZ81]Oa {M|G8hϙC[%m\І#OMLy%yȈD cX 3d'>gBF"M"u^!?a/<$Zo_6I+tdnGGŨ`/Z'*CrˇY2JE 5w%TyI;jP[fACZ9ֻGg}OUa}4KL(?Tϔj.ma jM 2t6[9o-ucFRtfkӲ͜=߯7&XeP*$OGos}Wh'7o&:z{2͝;ߦO76qں͖dVB~C!%E1ߤic㌳u*SN9EsRȩm/(0[gϞo 9sF˨)Ez|oynQ^]/8uT9\ʎ =?ڽ>Xg2Y0:vϽIDіρ:ߌxfϙGx\ye6vDU6^{eõ.EƟ*WͲs=_+utdQn}wVY>s>Ü7ޤ}ZPO=|6%'Ve_dڿ^,?BR}#䓿|t5__estAo]ƴ }++.+p[_ׄ/|T+Ԕ[n?|BeZ&_yٮꏒ0A9^?t N/<c!1xS%iO}+7<O~-_A໨x؜xZ-̿]wjk?|K L/x:`bY0e/g  zf[dᆼ bɇI'rhx6&oCC!ODEq#"_ 0N C]K\C^!B?i4 A F '7$,T %ILxA[>K_!9Uwiy/bZݬW]C =:j5^ʅ/.k ޜUJ+(T{z<BmlàR+ԴBN_Jٓyݦ>ˠnZ/[Ct)Ԫ9CmСz}f8s%ի$.>$z{W_yŮZ+9TNrƍjW?ǮaAOVuy,T}2(I)7Zr}}SS;e!'>aso2>]V:3뗊>l{;~~7"Ixv/!N{폺3&ɥsI#_-GQor?Bl񧯌Wh_2ު.[n/~oȑlEي>~8VoI߸w MQ{ڇ}Ɣ_>ysqT:pt;,.0Z*2nq^N;t?Hd|}g-thu˧ G6omΪ;*>j'pM+Բjgw_zŮl/B?nZkBߜ'_Jݺb*Ɵb-柚9W}w--?R ,^-??;?=&-'|'1zb ,zr1jp p*b 9|5fIxᾬ3X}nZj2 +k,t]Cn:4Nΐ㲇9zg~[>YY6tNv/{^FRSQ jպU־±^Љ ;~?\%C$kW^祅~.h[#[>1K/tYA %sΎ{2eB+ԖO ktZpE⯡?(?G5@SПQ;zCQ[v,\[[%y t)5%[> SAj/$7 y lB. E~6# MOF!7JT+DG"yyynȄ?/ܐ qy z#?%>V>CG/͔?Zf!}8).Rɥ8".L.9n7uQ?-j[ 3w j:knn>ZA.lXmE?Ue^m\\[>gyZVWиDo4Lk*?|jfYdm|Bm|49?d|^y'b{u֯oz.%ZQ3^Պ3VʋܸϘ9R˗y̬[٪ӧjX[gi_g7͚=ێ9'?.|Kn7L]lmOkٍ |[}{WɸG? LCJ~M]K8Cln^3d; sV_c9笠 }{C)!#ֳ gZ w ߍ9 ߻ϋAtlTHwy@vuW}ycdDzӶ\CR^xEkѢaYftҔ7A}SlĨi*S/Am OU2l>e].CE_msλ~n󵪎9X 2C<.>D 8hy{0<:a#׃]8x|4"q !&plxHqp00vO,p4Ӄ~EЃH%C~~)&O(nX0) &$RE]Z$/AB.jnqv|J?|"cfPr(Կ߷v6jUmYOv>Zֽ6Ꟛ5_>~Ft8`.% KBM 峕 j%NSdL[s+Ԩ)O)R k`+Kar )Sđ?.2Q0 ?̙<ۥ2:UK,(1:CM37/V+[jMkVU۟}~z6.Q)2,b0B}h-C-/Ș0VˠvVaPQ򉡉j)|*BK/LzDVǟX]%kٲBe+$Å2Vk.3ʮzҹ=rUW]i'L,a2ad0Z +vW\ʻԌAK زV*1s)oAZK+oNE[>Un%֗@zj?]/ʿhEXa b)b`4Ojg̠M|2J=IWIx }Bp˧j]l2q'qV/Ss.0v0+Чd'y~ 7ґb66BAT('A43ć?2CF4"ihJڟbݷ·˅z ܣ{w;3l@j[>ZkT@W8RoPxa0pwivE<ö|9 *K2\7=m/ܨΝ:j1gퟤmv'sN9PT:Í~SUuyJyK@IDAT/Wx>.qwu^Sݻ9C -SpZƯ7{`xq:]A'fYLpG:@x ,IBD0 Eb"8htKF0x?d<@8yZc %z /'l=TOt 4R<(c@FȎ>2: nC64ckg7q!It˧>8() %GNF5$:c>"ft~IΒZPuЖ7!vwt*ZZMvZֵ[7ޭm3}%YFc(T1JW,-` i` E P~ |4_17g~֓%..`mE6o5}-_4UHBn۫k;l?(.)? ^XG)8J0Pyo^*jW?[??8sF1D[>MWR' Ta4L5~>f oi.4iUC}|::Cr 7BXC#a8"/2@B0Ȫ-^!# 1h|"-a@ #\x}Hg2@ M1HD‘ Q>j&{WvE+߱YIsQ2&jC я:OD-(K2&jE+ 5?Iu}E kܢlӚfZw-[^d%k! DϹ9ȤS` pUf"[|Z:*W/Uk 4,܅8#iQ X'FkqP7wx0т"7 V{=/e1ĀCȿj<7y_f__ 92&jo1nb]hwj-vwn@R]!j3DA0Q a ¸1&F8y=ڀK8q¨O}O8_z38B`B$'`dx1:ІI~Ҵ#HvGjZ 䯮nlU[hspGh5fMZ85-)gR迬^I;/z**WːFW=Ӧ: ;_WO-r`g]Q oi+bSi?mQ?4̘WO.?RYO15.YĖ ڵKij+b 5_v}SE=`K C [4khqmOYa, ?<v?~H n^A8xi :ݼ: rH/`H'=DLE9>1,dr& >O0_6nġFaoxBբh@FvS\'KdS*3책 RMHرѿWW%Zʾj}w˗Ze6vP^3ҿ4E;r55-VsΛ@VVlK_m _64`ПJ>忽_W.oS}G1O1MEd"U[>>mi'0Ǝ;M 3VQ6?_/ǃlpT!qGA/4x\a>ą?lGHCtCxІ|!ᏕaQp$<ܐxD8/z:Bn?}hQ#rl$PKEV˴BJhd'5ʲC8P=( i`IBq2zk;ϭPyښ7__-*JӪe\c2UCDiV۳InJ.Ѹ~k,W2V<8 ij^_4wߍ_9םE_.To >=nO?v(uL*)t[4哤 \|a(8h@#=x}4h蕧'> F%|\!!ġZO8yGƒ.x G! +//Є !7\ e6_py@[>vj»J_ǣT _:+Z%s%nX1- GJ¥A C$|[]~gxlt)e\Hs1ieHcgJUm5U>Vi~1ɘ=-aXZ#* o;K"E?Eg0g\*b/b/4"_1W7m\Psn BmhS']l Cq ᆌ0-4E\mHJ%D]PPW\m\$˻,2/@đɈ^XEG훑1xxXFA6ϧ0Gg@_>K ?{Uq#*T,{=HX"hc k'jM1Ec=*"`H߽;)]xݝݝ}on˚`720 1f2<֒$&c U`I"B !0$ FcǸԕ[>ImtY}MF:wm8bXVv_c0C0_XvRZNyjӿJFCO_?/e#n7?@S_T3,/yEo*|V|.-.C]=ѸA J>yc"-zaþy%uy:>! >qO8d^8x]/ƕEEA+,E QIx0pq5TbevV8!>4ӧwW3tSa5Dɭ.)RV|]Q܄9P|ڊQvÿ(C=\?_z i0y32j_lo:eUh*Z*6v.n{7>_ǿ<  <-<_OԖO}B? 5}PsϠb-`"\#V y+.K8pz"ͫEqC:6I\8,pH^DEB0pY2<^G䋂&x V*| mea,NC^pQN`M>}:D[>5#vB0+dq_`J -Vyu>rǥhۦ0?rJR%;iʣXci+\%kEWwS~%T~RqTZ&>O'%Ŀ>ȎOO-sgDžT5D2A_fs(x9yӒ':w슷?5P)p0/1Ymb>4,EG>Ҽ X7!KT/#];Pā7hGztyxE~e<@#A8.0<#L{M''ZL> zU(|(\*ᰨq^ P*u j)*~!DA|~)vnP&N?$3?H(dEv/Eo\'i.c^%.̵fU yǃE31?&Am^2-!7`j?z{/ .%3a ; I XINmЍck0 NSiG[_Ət|puk 0!(p$FHp@8p2`e|ґxȡ`% ynOA !}iUNJ?YC33WƢ A!GxaaPn6mFR& HW w:8s7D]oBQǟ<'Ͽ x/~z?߿t'@Ν4Wh-O-m!O)400r'?.*p!񠁏 !CizG8 פ)iĆ:¨6!i . |#- C'VEpqpX.|pOZȆi!}zi@64Xڵm LzC;fmR?Tjៀ Z5U~h_k-[fM2.9DMf̱'fα79~s7F2UrPeET)S_VQ>U3yDbw >GHP CZ!iF,2VO:>.c-6a2>0ҡKk#O0!8`܊0yCBEyxNGE<8g8<Ё25ƠƖO)%S"/:AbAU7O, QahVa(!DRAtF62- ˺_T˫-ߴ\Sk֘fq?Yp3kaլʝr[˖-m¸6iOR*`ȏ>% o(㇁L`#-L/ ai'33ܖ-*d!^#¸@UC'*+* *p/ģ8.|\9 .X,Xsu( 2 ri}Y"V}LhNIU71aqY$(P6 jRn.҇]ڱw&i[q}mv--ՌiO>Q >} oVO{zlyl u_u n-#Vקk0]^ߵ;_R7U$? _|)& _jC~z3*pA=TeŠc1&Gg>;0``рpÀFaq჏|= g z:0p8aʂ !y#N(hp6p'p0= #m!z&-xOo)CŐ!d9&aCH!aYB(|y8tZ&A.GPҖySgX%ZZ+L+hLmlۖzlXU?vU4^#3v}"#)ҦmkL[Z-J3ԦNS''Z+h5yk6f7š~8&̞kOeo+c _nM *KcTzֿJUd_ː:C~;[N5P_I'ϿorKs0^gI;uԂh4zE%jAà IEG_pրC=B# v_Aw#]zA8pp7h^J]oZJ ̃i#qqpyPa KbNyy"iI \dkywk 4Wm KgRFg W[>  6$T c4N>~TP?Ozl 4q ?tqIH[>W#WZFvj!uW(H$f7yn$HAtMK,I|[> ,e¶R٘EsTrYiȺ7mb6off[3smVGrApǍFَ;hM4_|Z\v͞1î4ÞQUJ/7_v˃v1.sܞǟ"С**'c*iu`DSv<@+e*xsÛ@*x ׯ"l[m_ 8@!aw|`+9i3kwɓG.5NBEmP#VU_捊b /ϾYӡрmv֧3ht= <3dUӿ.pH>Fֿ'rQG 7MqµC?yp }'U<}/U,8ѬI,ǟ ǎ-ֳgF4/%3j3l+5kd.pxVC81P #ȏ!%h#\ tN|5ԕ0yaJ!^yf'<蕍d58/=|CNs jMs`m^qR)҄ 3WM&TOǘԡX́Mҝ\ +O[>ePV]bU^_ڮ{rl)lgcꫯ_PEo ӇX#=G7}}V鲪 mYvTN#Us͆/Ԡ._;I DV3h|۵`gs5 ;S/N?CW^{w]p~]  H/;/Ï} CPQ<^={ {1c-NAg,Eۙ??hyv 󟩍Du :5B^{u;餟mV ~饗/TKڐ#Ͼ߾'<`~?=V]jHqUߧo*~_)g8{1T/ M]?OO Uرc_eh~NJ/"{$7|sE_rUP_vP :Kvm֬]{M;l{HYxf?qw X:bn?y盭a_yg|i%u鿯Pӛaq)ALJ;P  Ay ay茸􂚌X -yȇ'tp|7d8udh+!TSGA# GZC0O; m0|Ȃ#o% <jL8Ct>@ Xv&#&7wEJ)?cL!6o7=IOåmSvER0EQY;O_G6od vAۦfv5z+x\jդJQ&+Oץ*~`Mfw7O;vi''}w̶[fI οPD9Iߓ2 ϻ j?؏?V@G+Ԟηj8"9QW+^~Xzj"{M/h%iS&OMQ\"yÝwiB [ _{ݾoWi>CgSnrG݂v4{X&ݠ]Y2E̳a}2ѿ䮑QY(fK%-.?W͔*'Y1ǟe'LЖOI4{lISvK [>x:,Va!#NZva7>:~2njPCp q\D0, It6֥Kq_^1?>Rg*m5Zjڔ퐉3mNj-Olaoa_ՖdPc'wj{pW+ܶWn]Wf=lUXeHjx {뭷ZδƛXnݬj]mΝM7^zXK+R2;W|m;۪QcԌ׺utMlW:jر޻!nY]:v8sN[Y=>Zg;{=/e'mɦxʤG?/2e%]oݺmfundOl{`mgQݶXS]b˲c-:iצM~/V$9/+`3'?.QɔePӖOwB_WUӄynX- +}Zl$m6h[Kժ ^PMu]GVH&+,mv\it X|׆O?CS-v/5&6`uW~f*R'H_/THZ1O68F~`8|ROH'L:tI0ӕ$.% W[sW(;ǀ>0цrʄjւRŐxi ) [>k3ROʟGot}Ycֶ?7?KRӫVXGVۿ&leYUe]d` `l..E8o<}4\K0ppa?*oNO\dpMmxp!iQ`20vNZi/x :C< l6t`& /rJ/tL0)a^Y9SFw| IF՜$*io+0PKEZHdrfFK>n{{/|۷ cZ^>Ѵj̺r`~?GsO;lSOr{{܆C;%_~߾ނ-պX* j==` a?bv莕CW^qXWP'N>2 5Ԣs̱6k 0\[ $`]%ߕW^3^TmeOObrQ=-pլ3֎\ɧyR߷zN=MnQ7+~vlV(nv7?Qпd[1F^=z:tG6ZYv 7zk<Zk- 7egx`7/HyK*{Rq?2bx=P_[G8A鋸uIfU+2i{nKkZv7i6h{ͷ_i/O)Oc=vZk/ګl=s, _S:OXVjdn@WE?4"_f<w |bϿ Zgh?QkɣzT̵ yK{h}SƳc~G]O΀`!" שc Y=-WYEƕ#^ F"9/ < (X9> :>tC;h. =gu@Yl'>/ S]q #XQؔb;}#FhH2zoN+D"5}47˓h±XbZ߸Kvv*ejhVR9j[UV&͛/RGϚ>H[ϦWz꾦V3nߺ2{ilp:fp)g!+kRRE5Sa/GN|%gc]q)Y޵ʝ: .$q?S&);e]5{5kt>wzH?mr:kדF\d~7S{^qں ǏWJrxc/l1AOmzm&͙Mk;r˵/Pm)-'Zq\޼@ ?Wܭ}ehK^cmrɎͳѣGBּY3]q5mΔXq\}Օ~(~nHy6p#бc5s*\XUEeNӶR_o~߆[D6c :O`뭶q0>_+_v˓elK),K.mbP)͝# _σo?; =6$,|_T58Oᴵ?t;S?cE}n> ^uƙ^GT@Υz]+b/?Wm0'kO82(E])cjE5ul2v:ExagPK|bB">!4h'p\h6!*K.rF @"Ñ/B/ ² '?axa, 9 axpq! [>V&r`\ppՏ4ԘGyɉp#D #+9vQpmt)96e]+5rEZˬq|)6uٟ=x-r:U+v-JK=Ve{n.uBR!tpmd(‹m֤OmՋβ:od5Mٗ[>YnSusX(AcԩvQ<-ZmV.ck ˯h7+'^gW~.AJS1Rg3U7tڢWUcG"Iuoo8nnZ=O3T:Yq?C/ÿ}|~< x?͖qSNeAevYgkǾ ._өߛn͜5SrW}jU;Cߵ2"o#^mJcǩ|~~\(?gʾe c@IDAT/ףm?3vhϕ,霸^v+b,'2pC*⨟磁e-ya?_v;{6yglm U|n[>2p#-: {Q~x?va{%R,,ޚc͛JmըMe+꟯٢YisdT٠G;b+.mk2Yq!UW^egϲfVGM崗R?D ɜ|:FR7 >@1Ҷn;mKB=|D[e.fV% p0p[KK*Ee0Zڮw΢"Ln1_\ț( \',CtV[\SOW! 4oM6B1bz)fZ^8_ta< jFG=|#T7pww؊:~UC`v2^/"Y+ۅŐ|+.a{MO|uy'&~`۔O/T-'r[89(kmEGqM[jEv饗 NL2q׸q7RBnA}үS=pc|b7ZcѠߒ~qDC;&ȍϷv[D}RCX^_EE׬Uj?BeAy䠒qʬT%@?_)yO4~YdPjuWczٳW{ Wb5C8p &0t4<"|O_ي8pGz]0 Rp@C\Gp`"\qZ-x xx |o{CR%riUbx֝ Ny:" \A v&6uOl`& .W_]HM.D[Z輫Su֬k[_-i2M]aCۿ\];\2UaZ_df̴me$7r*U?cQקV37|p]|_~sPj)woy}u_?PܥU;z۶ӊ2+޳3<#)fd Fݠ3RWJK O=pӟ1v*7I_ڎ>hsud~YFO=T):_L*^v>twYwMtmH._= Lk\Sm%Ɵu{:ˮ]v^=u~ۇuZmI'">9vWSu|Ͻs=~*P7Ӫh z҈T8D W^~ee]mj_xREp ~Pg>ym-٥Zƥ}8hϡZ)OF.{]wj>3fV8%Q\7don NTل8.n"ʏю}L5T}09t' ?矺c^-YR's?r/?[| -|߉zSNKVS 8@+Ԇ 3car_FFq-8`vC'p G>F[PK>>p| ~H7Pf !81p(HҢ@!|pÅэpN)F:yq4`!COpQ"#Gu ,nx <~_]L}%DȤPt^Xb(Blmr?9]K.~MeQ:V\8 JmQzF/j4o&µM{=2]DuVuF蜭gѿ+VNJB-ܳ5\=պ.-?e'76ҪtC߈`wUn+^*EC?@Ƴ哕R5ghI'dJovܭRQNirw@*kmlSNoojVMiFMHrgu;?Cl=PJVj $Y3I.tva1G+ܿlwrBwH2 u;uTz#mUjdkiKg~u3h?(㧟~\YY`8G]UwGmw)٤Wmt|l}Mssmgl ++. vХ͚7?c_wSs~`_ηп[Bm[*7Xoz?O>MۚUjA[or 9:PGJO]ѕ)W ?ڟzKs_ <'eLߩ ji(/SǨ^KK`lC0px38y%x z NE^pr#_ N% \%(Rp 47/W0n9 9i05p40 0)QrR@~*' 5kӮ:Vz )T#L(XTkEɯ*H ՈCJzCyz[[(RKY+&W@3TWCTĶrKm>yUkZ4pZ(j(I~ɺ0PW+Ԃ=gW_}u%ꪫ]V5!HH2ns?UC#_|뢐}mY1:lw"Ạ Bt[} SL֙l*b.d.rQGTGeJ-ӎ;^*!\zg[[[>ǟ-BMgt7(r j{`PK.ki;cj_Eɕ8JOu$?yd)_yk؀I N@Hlw~{-tvںV[ٺ뮫R͕V)~g@Og]p5Wy>qVk:m/~[L=IdNgq&j[o#Z1j 0L.9okt[8>9l[{uB^|N~N Fz O-S*]3zRԎ#G|`McRJUx̶9 z׻VeQrIPx0OOUWϑ&u?T]pUWT%A$e9 αBj*A?^ Ba$##ipHCq|[8|.` ?\-􉗫*x<-]˴Ձ.) -yHGC z.5`D~`O8/ h zP Bi萡Ǧ!";EH-/A=%Ir`V pa<_`[>$MX/thQw&;b}}g{?^irv>N h?k3nydc%[9Yw=ob>nYUtkUrIȠ߾[OX|瞷kdP cPT7QCU:IF}+ߗaO/:W Zs2d-xgHoqږA]w-gȰKa{1?j=$^3^5^߷O<vkY-Փ?J70mЇ_Om1zi6bpP==ڟ[>ݠ\D wB-C=v߳2`m٢IO-+1Fe?gu[C2^2p\S-L_blfD8VC+0 r׿5[a*_RWe M6;C$яD ߆((Kz>-/~F0 2|/ m 2r)Afͭ '3q">j7-_תM5߯Ce \q%ث葈.Jw7Ym_+_P;h?ME(ޫ_fM;:kL;CZ_\<ډ:*?PKEO==2dy8Vx.?y*LaWvҎ#Gcux?=j;LY!ٱ- af0l!2aqp{\Kq¤G~hL /!2vG~(.FtV%?"= 4 Rs9!: qP3fo.mYu?Ajt=LLmQfv٫;{]qS 7u{~E?ώ:}{uo֭y:ϭffR4I,AݠVPAuiCH]LjQ#9U#ΧC?vuWOvնѫƚ7ojdѪwy[:|~ENpu:nUWq^(jXuƊJhP#G~`Eo~֖DC ~ڍ$=kf/]yBl$#`\JTPɂBm 핞C3[Qq|p2\u:DryAg]q. mwI*Z-\rn|ո s5)WJ$ztn[HSF61Ɩڴ]vi$#?gkE.EzV"r+]pΊgw|I)뮻Ny-u뭷r^ 'qZeZ#X[>?V+Rr5+RP~˘XvēV-[:t[{V2fZP 5rG"YqL_~R% ? pl kU$Z'a./O-7=(_2=jt]_d%Rz_O5WIwٌ?ݵ+wA/q3V.LJ00~E@<afP Zqybq@yr9HG7؈Hp؎mP}`k;;mԹllu^-C~ܚHG0ta# ShV{B/jq>%\̞G>ߕjUBnPgH;}߲r[3r^~V.?M!eB>]A / `)cӹl2-=wtè E\j6UϖDQW^x%;W?Spʏ}ҡF~opqVʨ|*(?~۩_k ROZqYw1~g}f,#~le:Ѓ*NL*ԞV_} H #pV:ZnӦAc=h?Q~AN6߲2]qߗW_}u_UGٳg٨Q@hWJJw}1G mFaJ[>/ ״S'ߗu1N)Ar-!_JÏhZyɪTO)Ֆ[n=t4Cj?||-jLAcx~ W/hu . S"ă]Rf4FTF/AǓ5RG4UTGyQ=PyʈLe\N:YSV{~tda=S|PŨ4eF*`8F4\I`8a*x+^H=o=y>>@CBA zBEထ?G%/Ӌ0>8<A7| e!xZܯ-Ö󚂡7Wo?[RW}8>kVDMa1&"H)W0ߓ1$:D*>[>;Y]D䒈U~;4̦n=?q-أoI۷vhg{~qV*пVheGy>oaos4y=s5UoaAM*Eϳ~UM_U+.BǖUV^Y[.:)$Ag?=[mVW^s|$xg=cfMJVZӧMe0SD[E ~Į??aw~{*~ <G j!vxi'04rXBg{ťCŠ0swG/Km^5iEãg*=wE?p~g mc˧ jdP-'foV)?[>_yek۱VF:7w=3ۚkɺԂ IM7b_l.jՎnrѯ;︵Z׾^iT?!T柔!S Ok}V>Q%<7?y2?l3iK1/9+ӽ{OcPXE@]pl CT^KIV1.t" mpVRoa| BR308q<#/i"ÑFQ0XFŷH/G">q{.%X'Cts _,p|F$'~E!ϋZp'>RAc\-O1Z7+=i qC8s|VkhwVvسoL~kaV-6\.C{ӢݧweD@; gWտio}K~CN>e7+̿sZQs"fLVl˭ôV3E-/ 6a$;Z[4õcthm2_:eZ}jim ItF9n͓[~:k U[+۷IaSťO6n]Oύ63*w߿]11 (>}ʦyj^ڷoofm=)CF:;Ivv_O=MsCs SaY&o[+oT9,5`oᘡ\N񷉌| 4q{,[%us̸'?9vig s3l;[{׮];mcep #tA%wy |V[hiUZfM쭷꾗[nU+nTY 5.`%|!TmV)g-F9X[j԰08tR(.ap` Hq Y|!+xn'~ɰ0,*"Z!`.J"ChF-'h/т8!>4ӧwWVäj#8 *A"C 9L" ):t\ a(Q&0ѣF;6s/P%W/>>陡Y묧vIxg=6sjߥs#kػ#gX(&^@d5[cIl䘴*?[$/w5kM!6W>V}-ƏgoLQ<6b'^mEkl֚:uJk}A_P,ջvn6l.K]/^5⊭knk%2y1R[h?R=0,Xo\ eUرiX݇ , ;^dR.4]T1\d\ v+?}4?-'EaJƟ_?Yߘ\yJ_矪EOaV16E 5q{ 8gSwApGlXHW]4p?n,Ҋx>񐎍'hNe(+pH^DEB0pY2<^G䋂&x V*"/ y :y @ p);ymxUt+k is9  R?\c mPʨ-5գ(t騕o~[_%^{}NdBQo[ke%G̝=N9dPF_] _Q58?=g?2r*=__-Oֿ3ax ;5%#A‹>rǠj#aPރ W#*1+Q+pn X L^D^`#i<8`aބ,Q EǏt@\CScBHBN:ܠS"x2HDž'|>8āA˴aP`(+x$; ZZ7R5&CHrHO~}V9%me 0uKV>e:nA.a~EƂUSS5+hu2RMj޾ָi?#YSko{޾~;6}V.ڟMߢY#-=O`*fJqVÎ6EďWy6NΖA5\Ee3 ETfͶ~v3&15=8]w֡6ѨgVwڙg饩>TJr|mOv4}QB$XykcY?=w@ w' Ss`̰&CqT\p <򁇃&6|4# 鸀pE?xFՏ&F DEɃ@..0:qA/|a߻OnG jQ2bUr@=¥bf8#JJMP"1?H>V zό?7?jX_Y_8-ͱj6жЍS{f͐>em.:~5gҢT{Rc{5kF_G/^yR-j?x(ޕSfڛ.6T?=Yug( d,{ѢKKT\'ٕC1<7碅8oM4AI,*Qi>[ "k~}Ҫ/GTKFrsYߌHϲ~kb<-nP j*7~bw={1(ڠ{/ eh$4' WGAx{8K4~y7\#-.Nb uL0pE!¸m`TBXǣ tS8Ue#8!#CJ:tA>>Իρ0H -JACӪ-*YC33Wvc7Ja8+.x@LOAtw#Pͅ4?y+d?w:8oj.ZB}*~=s6|p{J+Dnrִ.n/d&w=M>Ѡ.MD9L=pj;bN&_r/SEM"?6y-|-{s2xZFjG;[ia-ڣGow羕T߀Nqםګǿ<</`K=A|R]4u-ƃ͆0±r+q3^ -l5 6!#r4t|A+dz8ud^`Q8^'x d8ᐁx PS`ȐB4p7 1B`4Hi@"JA2b<+ c:Z;<]T%X/*KkUUZYfZNS~}g'@mZvokG?[sl6^lG7ism-k^ )BԿMvֶ]6}c#Z??|,`NO; ο1=i￟Ouc|Us1MВY-gbѣ ⸯBa G4|DAJ\M)qb`8`q47h 86|N&MI v 6FH# ^pQ iQoxe/S$֊yy 5LNXHU'$+, UUFڸe&2jml'j>x{hSeqm*\c'Qi\Zld=<ۿrٞZִy3HqSpSE?KJu/M!NBjs/?)購Wc2|?_~ ߄:Cfi}xѣĠA g 31#^ّv#Gx! yFNЊ< 8> "_Т8PQQ4 K<* JBQ)8Nt h CX#?qૠÉCܲ\ݻe <.H !j ט\p)/Ql-rڶ'!`mRV1[Q:d&oa]f5㩧lVmܳrv}J?{`k{)ڐj+[KTö|Yk-fۿУp5/&뿴 /J1 o/ ZfJO+ 1㝌 R?O'\ St jp)Ľ.c-6a2>0ҡK.V C#O0!8`܊0yCBEyxNGE<8g8<Ё25ƠƖO 6IP'1,rUEÑo(Do$),"b3jFeYƟkvMvZjejoeMt ?lgߥ= higW5^g,ʶ[ٳikΞah3ْSgqc[mҤvg/_Q u#ײ _W *t'E:3mM73۹ӝ1Q?яlֳiӦٹ{~ӾoY3NәjEtA޺Sms}g.ҟE!=?yO`rqyk2;<P7 CGtA0fqi 瞗Q #;/KQ̛⭴75??Cs?\n11 y]?YN`PXƼj 6tOxQ!*fhē?a UqqCp|$ N8 '@l KBD4?xg.)~^Tah+I/?x#?t4n9B|ƉӁdȽ*#l`)s~H#F,HOEi˧P3"ZWV/2;1Ŗi~6}t.ٿժik]hb`ufvuT[3ҟu-isΕc뭴f5J.-zGYq/hvЛY+UXva'Hn4Yc FvbmheG?ys+pz眫[벂eZw$>ʟÅ?*_q(<_1`ђJR קwujTzE$ 5?'?Y5 ^$0.F7tɩ#5a zF+CvfE^ K+ B+@qy0 U΃|*ZaI :D.#4 Ey+8'?p5V#G 9i5`$oz/R/V9KH"-ܞ*puuġ^5:Cm+ԈgV^$$GH㤋̿oPY6x4fhQ̊ڿg7K:Vϝ=m`=\_Š6\ ѣtwŬPb/Nly 뿪E7W]رc}hBvպi([Ahk|s˯c<Ϙzwc;q4ec_?cmV֫W/kk R﹈F(Zп* C?dP[ ѣm۶Za7 dSNy֦͘^ʛ?Lod]vV6m4{'mRMx`6xkӶ+/ۓDd8PcvgT;뜻ndT v- n]o89g }ܩDjL-ްFV[X?vj]:wm ŠvOgLa=z>d,4puA?fJ[ݻmVg-i>ne]vy)Cw+{cʔTWECzP-^b/d;wrWޢdb{饗l̟?O* {jaPCCe s9A֮}[j*O#C[ŗl֜n>8oܹ'-߳WOdM.P['۴nuTۿCv6p JzviߡjjEvuWig"1cdim[hh_|JwѸ|7_}&=;)QҲ*jZrziLOll+뿡;^UMK ۊ؈+Hzߢ>Y핕nWx+e-$ui}˧ƿ>Oi8\?up:Cm 3Pa ;M؋G䲱S]y^؋H0Ok 'CL4G8 B>4_7醠M"4H?  cq<#C7b$<sPI|?3Q'MzVAFjJ i.M@IDATdTj˳b= HG3)Wo=ìȳ>?onw^BF.e9b6C?1,R0`رntn={ҝm}Զ|aQ])o?O[zAk'Z(+?ٮаP-Ž3FFMXՙ'J̙k~?ZO<ц,ξȸb.Aml|bP }% a#|ʡ+ZPu?޾mE);F;~+p2~Pôe,VW^~.;J{}Q=%Pa^xN򨼯J[im_wF-MFkyUK&4@?~ٜ\lWm_NP¯x-2uץ .Tc8oYcE6=쁩l,m6_߰{e,l_hOL+_+{s矣 {?hk}ʏAK.ѪNoR(Z T*׿]^zuu'S_~EƲeYl.) jZtƙ;6Α!p 6D+6O+Xt9gˈ1yMG7 T_*{+-;3,#P jg-6[9/g9T◿[oEe9[Q%ǟ+ԍO8Ջ.X+6s|q ]vN>TJՋ~;-Z'ԩo`yz| k-?ӏl#Gf)Y &t dIIcPcsѢ7ҭi1̼:.cT j}zo %T{D)#Iܳ'lJ+'Ox{%HrY+cas/`+M=oQ~]p2>q$]GxDOU)/u `[dZfK1;??4xKgOo^uE?#}X_ߙ3t)\WʘHRC Wg^3zZg,?V|8A 71 H'/C.ሓiAƨ `'o8 qC^ /xf 餁xAxp2n rM8ɉY5F(oBJ`T: 67~ä$Lg!LTmOZ!SCRj#oNB;`nN;ʖZv`/b3wylImk/.[Vlӥs猥Zȶv;z+ڪx cO?]V~{ϽvVmzSOAV[~nߘ:UN_͹2ӛqh0mKVU :y`xOn0~}ǯ]#<>߇*C>d_vec,Ǥ|o[e㷿m{GeKjeЇ W| #~>Nne]m#><+t. /yZ-N[>e`@?l*W_ wϚ5S4^-F2"V*?8ԊK N8c=ZNX[<@m'n]Ca\!O$jnjϗ7%=H{뉌c23NO[_4ϓNn-2~A Rj_Wӟ,cp/;㪫ՄvgZA} ʌo~[_~9/L@!R-* z B7LֿC2xPJhO*_yw&GH>gs_nWCCP?Y- IpׄKG8p"vH==Znć[WƋx7'zC>yWVe&eƤaBPa 0hMYzA# k!P=/A?ʷz&4eP+qevRj 9 xwGF-σK6TW|2U?v/<άk'uGv"$[2 [m[u NVL:$cJ+}t@ =:ۜZaKKoտj.}ߒ;Ȑ"CY{ 哦wSNNy9i 2\Zm|.^~YׯV2,Z?<}ֶ£?IF]o]3 'C?vW1 Zk &8V /ҘPι ϓgqcǞb;7YF/W(S[6To|㛾F4< VBI5I~ߵn4_tхOV>]m<_+&=ʼK\|r* 7?7Vk.Op}Cf+O"꼴31 UǿTi2;GSJmFUZiH8 "!tزșyGqu4m0dQ~ȟÇYcuJ. 腶.^vXS=/vSO=S;F[>نʯKDwb|Y[r=wNemEzB."{ZvQmJe,{ٴ]PX+C!+b=Wg*8{;ؤ12^_ Kynf'~$kR~d*?Rv~J3ϻw'=BaОףЪ .n ?yF>\l=0oEޠap[)P !B7I~p'n!P>%,y.~^A_ƀ;qӢJ}pĖdK@:R.ϒIQo<.%õYBa+#gZW_b` >Zmklٶd\[:c-1jI1|+jumju6g"[6iM\`?U+ڣo1|m\K9 3 oܥA3Z+Ηu֧O_XK=j}GtyQK}QԪ(0oގ# nig+5kFC_A_e[(yDQg}re]rJ>so`qVT ֺ 3_r!|u0ѹrs]e!o?n_+{'Ownjc]tQjBemuYQPFumAC ֙k_'3ڶm##$Yc d(): ޤc*8.9lm۴J0 h3Z)HvdZZU5<[uw.\?/ۄiUbPu]줓[cw{ ?v?}'|a:km ]!嗱:v$I:HsoEe)=_oD?\>`V- я~D3*JG_H6߉nX"*j+'ۿn_ir_wSi0瓰Flf@l_ K)CRZ[>b \+q!&-tb ?.C<.tVH4ʸרKBqe&?!~x($N/*2Ofe#<yȞ{Y>EnՂ?~.IӮ0\vW|E}ٻwb K"k=,ݧI;TgrOӶw{{01-yX3OI$ԖOVlq1C9S'6|ⓟtm_[m.?FF½c0Rt!]-'j+) U;N>Bu[3ZL]ғpc3Ujʃ;L0s*/Gm />AzVݭ!:袓#otƥpD a"x?!⥚t1]ܨs7|TrnhOFyozKޤCd׬\ 5JмV-ȋTVnX~E;._ݲavA?}Vyѽ2h+)#.lKxN9B Ab01Ox8 ̃t?4ɇn2O@tCtx{O`Q4?B9 *.$ur!Fb@R-T]MyC|* bg-=ڦ[zoN7βѽTlsՙcK- %696Ef.evҕ7i+u;QС_ Ml!n7</>!CyBM4Gsήrjn'FkIȠ6X ƵZ_TԷA2?Z??OUq.%Йb O,?g{^7 ?lwa]CFzDE裏֭ˌ3g[Eu&:CM|Y-E0T*?IwޕV͹ OW :5E7Ob,ތ:2pf컯c?7w 0ݬGK7VߋRC8@؏y܈'L\}TV.C>C<\fAdpCmx(q20vN'7ZāJ^iBy+4V@&MԖODt*tXC0i2Ur0yΔ=_b[@AB dP[u0Oe%l;w MuU= Uj iZ6gZVkӞsG{η1Eo?2p_Ȏ5*09sVnc=Wo޾oYWAO1C[8A){)~g>qh'Fp>q~rkDSj˓sAl9O{WS:}5gk5~LhK'}֋#EWt(OqnTD$wV FH‡$j뮲_{ʾ 79Svɮ:WV6p`|K;- 5vt)6E= Sw K ƶ 7;:W^YҘ b cfEh.O:<+6<ͫos޿CuZ+q-qv Ra 0˃-VT.<c!4 :0~p.x@KB7H _-. a!08Op),+h> cY- hB>j[>\Qe\I~4;5(fʋf5E1@wzKf@7rv䌬xZ⿾VƑaf;[~u|m#Ԝ:3MF5ڟtڷnI-f>8fb{ؔKGZ>>قVQGKcOs҈sV[: ?s~ ~@:;FF}שx9/2Xys93Z=vV! ?/('3: ӿ^;츣K~V&6NuWmՖZxK/0͙y]dT5iloa_&R p n{y 5U*7>Ο\Om&Zv_1 lyb[>)ײei+iE7bPG` Ǎԍo9tI:(ڏ~xGR:+9袷EᯢT(M0 jH87[k}EHZ朗Pؑ<7,WCC4o5CsRj?7G:~r[>s0n~,&8\8&C|^.Z\<9"|<iQhR"+EB \! B(t)ㅐ `& ?xq=Ჟ:Cm 'Mr!Sin.Rg? mZyJ"/Y)\,"@*& )r͇Bw|a?oQ3hǝZd#?Yv֥G_UO53|,_F2M3ؒY<2QF5z6᮹;E׳~?^FigYcOj;jctv+t-}1۴_?WܐRlEyݻ|^<~mFsjZiϿh~K/%k_T(%{%'n/m]>i޼*HZ{ڴvݏAYA/3R&u`h1 sק(N+ ʘƌ9ƻ=(w},iʥZ ^C:d]t^g]g?k΍XGys+͵"H5i3v xX+;{,N[z!k`[ɈtGZ^Z+)mϊΩvAMr& K0Xu \Ir/j<^}U^gɽ[oyQQی~ݰ-T+J@._\u+? p #2ڴi# %=hl6ý]N˜G[ bsz{F:S,x %w_df|G8ԩ 5'wߧ;u 5 5F?5Ts-~*JMH-EwEwy/xCWcWM~߿3ˠ&CZZM AM',/-4u NO8Q`U#N\hǍHp ;X[>}|B.%a5^UOr+U5(Xf }3?C2P#Yј osߞ'/?ye`ݽf͚> o 5ʹBmp012kt@4X7K+) ZWrv'p F[PK>x\ KL?6H n0%̔x! aH?h#< n68ΐ7 ӈ< ~ O[>aBDQuN9Qx43G@<~3DG ouԶznm8hu훵gY´SFhZzԇhEbٶ=BRפJ^%-dRxsM7*@ZRPwfh9@p{lْenVhm|OvpmoaE/ܩ24ӯnW}csu37Jъϳ-ڞRWѣ=4?O[;wf3Z+vKxn]𢢪`{쾇 DS[: Nƶa{k>;V`Хk;f1Zrm:miEFwtFgQފKWg]y啞Vnݻtw(c{FA{q{{hGNNb/zj=}ޫFo\oR}YL9z2Qjr8W 5^By9}nW9,W JvmJ6bTvi]rq]j:wvmeTNzIOnjr.w  .y駩\oo p7w:1(*kUen:sDxmٵ?mU_r{ ;~r_.8`\w?4NRE?TW~k`КҴSY 8 ӖOOy>6`=z0/e.p( .?*c cW) Dr`(LCNdrݣJ,X{VRjL5lF,JnzDVcAW%vGklFNnؿݴWj˧V]#}V[>bmBBZ^۹]Z1CK56ڍjL5_GP0&آEM+D:ud]s칅aᫀImڶ͔St;+77l_d}zvйB=$WZUY߻wo4h y^c|_L=eIK+/DwUZ_ ZH$_p27?ѣ$/wz%1 ǟz/4?60]F=U<#a[ i+8RapyC a,.'=?pF%+'?"Za@t'C6J+WRp_R哖ʕb=٣oq:)OtB )ٝ-J-hA;$k߲gapAˌUj۶~Xnm}p,ݢex[6{j粅uxqM^mlk{k+F_4{ ˖X3>[U;K[1{amͳkp՜?a]-Ĝ)ČU#?y'+g}f`gq/G₩MP+&Lx^8k[ n°`Bd@2y+7@4L r<QLQRdɋ|G% (X I aC$#t49J&0ElH#=?`|˧"( DMib*6Jm0:J*:Muοg.[?[u&#?̶ߢu6UB-4tKͳ92=<m\XES)|m5ooյc?O֮]{ȑ#}?}e^rf͚}]!?鋧h'|?~ - f+G1O8*h! qGED^pt\#oH n &67 q! ‡c8y]`=ܠG<n.SxcY B}bVlL!MRRqBrwS bHEuő`߃ZSa7 5R̟R;uSonk>X*<BF@04(n8A+\E;+ 鬜K3&9n(UX"C80wIdH|u'&$&eHtwS[>5Bc/i+[7R O qkVlkF*GW@]<_JPiGiJ+Cґ[?WoE1$%Onz?FXX~1o~o06cE OBGh@+&>bcH y;<1K[>5 eښyn9_?CMߟ7+ԸH T+pHסK8 zEZt0C:6I pG8WyC+ RD!(LJ |Q0WJB#>BGˠ\G(h#G'u|&4e%ttڅ A@~V _ ITˇōIY XG .\JR+iPKlۄ'|>8i_c"&B u:r0+R"CHrbHѯ(FFC񒶲 8kl)οGNSH܆n6?ԑӹ8jSGSݕXF6ؒ2 b?I~'G IuSOhQqԞ*8ǿJCV47FoS?̡ǂ<`~Μ͜1)і?p_do~Է 2<~ʤ*)9a0׷g[M0p BOޢdg0n~aD<"<#O5FFDF DpɃ@@qtp A/\#qwˈ`ԢdrDѫHE ?4xwR-|*K#}C$@nO7Hn $̿hFshhZ5~㜵Tk'KYYҖOqrːzj]Oke_QE3VPFǿHoe<'?z41gLMJ"?_{F')#-?x??1d ޣyYmS5#ϿWo=S|2۷Y'ԊauǍ|7y` ~-c[;/Noΰ.@ .GMyp ك.@J-„PeA/<Oy0Lh`$>8CA4Hv?CM' Y&@XYĂGP0I!-H)hD"L('SNuz 58g"'Jh'߻|4 ?T__yV7"3u^{q4L$<$)j⠂`x1pO$ab8l= \A ^cC6@l.FH^Q iQjEUEo(JKIw'#" ) h~@x'x# Ssg_ֿs/_Ouǟ<0̐A}6\oO 5h0\& 'u*q@ci`F 4a S}?@Z LTw9?آBM[ONTVT@T$/ᒇx]@9 .@\B\ 9.q'OȆ˖ρP8qKʷT Acx:7Ԁ(EK׵B0]S˥3\k_ GS wCߵ=a;' 'c]?0P[aCa /J ``".VO:+Vƅ6hcJNH*ܫnlTBjJ!+KGTK<$;HD-uSgEJWV/2Uj۶2vvviʺie? 瀙ړqowO"*KKǟ_<*HW/bA&MXbF!YdދT_1V+NcHVid%AF3NB8Rк:DT^Pޢxc+ԈgVrXH I=mP&>j߽:AmZ{ E ׋h?y1F_yq5_yŜ3mL+֍q)3&- ]r O*4/ ZH#'lD򺟴' ~#-"J**' WBfŏ` [7 8t \D4.aҐ][>+P#{"†Tz/$$B82|,$.nE5zZLEVUhcpY)UwB|R+߱uimX1ul֫_` }NֱC9}޹SGg,YeveBqRƟj[So./G#a#0*F0?#?_U:Qy{1O5x}xXq) ^@)>=Wf؋ll+ۅt↽4y@7 n=y/e7G8 ҠH>4q+tHD!0vC:?aI N<:q(FBg;'e.|񓎟?-uҤgh3[ =$2'W;Q+U*lTr{)\T"}*+ѣ[r&Dl|+濢KMَKkS>ZOh}Wb_ێQEؓwifd׿bZX M&OsgV41r|xRԩy硞gUK]%敚@<~,-%@ kLr' D8C8mB1)e+3m?@a ID:i^% \GŠ6\&NrDVDM0'P,e"ƑZa>ELՖO=!LO *SQ 9;>^{@-ۇ9ڷooK/'zzQjG6;rϐqmn֩s' 97Ynb[?nzW^{\$E+:^Yђw!}/ǟ?[ F22^?ɻr SK̤!d?n" .~@?x^Z$TK|򭞄I' ~v Z#=ص,WW U7udH +ޝ¯p>Ē8pcp`*+S[>W?*T[yVos Ym;XSN]>o2ndUMgΘf6z^Z+^k{ŗv"jO`$@{'V_5u?jr/?ҁU'?y9̪|O~jfi'O u:?gP*^q#T6fvP>@?kzA}N <?b) \|t#죞+24ʸ!DT"_z !|p'n.|E oO| >+ܠ6iQ> b~X͝a\+%bwK&UCb(!">]R "W(reLK?Y]>Kծmp8]9sRO}ry^(Ar?vЁl`3&>m/</\hZh)?GuժPX1g]Ctz?[ؕOY%=ָǟ<⦼[>Kg:uȟV˟뿵'A%FۜslSmԹg[W{i"')ryKOۮ mnouïuKѿg?Y'?!)0r葮s߽~׋? q ]taE[YN>e?_Z~B%|wUʿ?ʿG=]j>_ynsN=ג?ēdP۵_g^z=c6Ա;V/z _|xA*SZ۴nZWy(!]p= Ӓ_)//T/:@S /]ֿWOҫ??ZW>om̿f 5桾Bm}hPӿσ ]⣡\;Cg!Ca! Gēx%8e>yGy,ᚃn(8BM‘ cVTF6B-h'?H|2[1v_Jlίȩи>,Sbt (R<,kB(gTB d@Z?[U;{ٮW}owEƌ::lYHyXC'%l@۽ο Kmib{Yxگ jR|)#^A<I]pUUoms)wMޮvvTx}v5W7V6\4=y siDb/Q'D-% ] *KKLcT (?o~8(-߶ٙL+ Ͽ`7pڿssm~UVlm?{w_F K# j[mE~>;c'au<)ƿ~.7|^þ>{{ |`P{Hēt\tp߁k׮eW_s.3j#Fhߙf_/o[/ϼmS~)ƿb+Z'o?P]q-ZTnz1-dP|l2EziBpYHVCY4޲|#' g(ÃۢtI#X 0D|<_)S.Tb8a$ <C:i!܁0x>F3`xpQX׮mf˧'[)##cRWh /:G#eJ'0}4m*É͌K@_R+D_aӯQ]Nh`MwI^ҥK_?%' ^M-9*]QsNZe{#4s6c AN㎷Mxik~m R >(U'uYi|FA1&}]5ӆ?tPW/߆l`g}V/`]/d}ǠE7,*?Wgʠav][m/cSߡU< G}ؐw޵]~;[WἽ=g}^jVNw?FVA﵇xPIrhw_J]y7g9/_7c% Wb)biο&O?]0waPo<؂!c-2΄9lN/0 ,`\2RYV' XFpa@# t\|9COy OP񲝌 $•sA;ʔۗ_}ƺ9)1#CD~x6^}X͚SO>L9eZ)j4m-5{嶭F>Sh[hesYNgڡGE_-Am񥺖xOjyu`췕VPu lZV35ofΚiǏw} pm#goBڽWYMVa(yZ+buF &njm۶-[xO}>sN]w-F3l.Bgl֡}G| 7䬽ZLs}Z6d[ ߶1G{Znm묻oNxg۰O2.``\s:icF_|>F38Qڶic6d-Uƍ)Slĉ[oi׻ ?JFIjoHz;Ⱦj*1,*??]yUֲEKLcu%"R͝Fgԩ6a{ꁿ[w j[(}wX;k(=Љ 7rpEO}^{PYf:Wgغ#8C IoHzM[k٪V!e?c>(e}uE +OQOi4QV1/dSvJVbN߉Z@K,a 5n!3¸G a}Ym i6qVZ*Cj]mЦ?0[Q f9} P3luZٹySO?m;찃י0֔q,迫mB)׿ce:#=С⋫?9+xbwء}Nj&x:ڶMi P4;uDK#oߋln̷r 9QHohsѓUb EB {u^6Q?j;Ɯy#F 3 ixD ڿE| M7j3m}m[=?; k^cPc'6`}ߢZ~}|Sn>/d ]Ip{g: Hw_`O&%β\[wuOQ>*%|P m ,WI)L T8_)ߒ;? x&Oe(9r]|Ox8A5?PKdﲳ?T7v(23pF}1p \P|l&qO<`0p%|8#*X#pqH^ɷq"Hc 8āE8_6`t2/p<8-`qAѰ*8T >*1tlFGZerdj%\ds86m lYwEߥf=0[gmgќ[5Iv%Hퟗ?}4L&d n";ﴳ햓uqϱcoHzOE<[>EtY~?ѪgtVu΃W/;kN|}b?$! TQ#Fgad'S ̠F~.-{~ܺ|&{ΎU"B:x1۳\]V4AV`)LKMߤ`u_d 4,L߳|-&DjU"rWA9mVOcݭKWY~D2R{⁐wm#vF""ɺf~ʼK퍷߰kvpm J!PM ;V 9`{gl-Yҫj/?aw +K].ShEޟO_'//B"/]?'I"lx-Ţ:.GrX)C]: 5 jA a"̔F0!,T |Wv wS'` G9#8r8|YbJ)(TMBQ2DIx0q pTD+G:q8il ZOY\>(sPk GO}XqSB#Gv쯶)y8W 81(,SR8m`mtvo9Cߵv5[uz}>rpt!k#ڿA* R3sO)sun*_v} X/٫w UL%Ge$2׹jQ!o함#ٳ+ŀ{[6g?T+.ȶ|:C޼ynpR[] k=Rfl9.?X*ڪ՟-x}v֪e+[V,Ƙ3a/V?Q[Yƃ$JcBm.?'>x3tt~\ЧCJ!?ӦM.5_џ~Pxv\_}n[&`%i=(t9=m'_/c\SO-k;c[qgS&OY-[Jέ֜tQ+{ߓ?\ԟAˮʯUJ?3l%7dp?l\?'eCkV[nUE=z`P6~Kٛoaie6ka : ɟl|A;]7.I/_0߷1#CxU] K/e_f+s҄ .iHa{?n >=v;u>\x=*ѯ,7. {ԱB@ W?bYh/_H&4Q̿>N[$mĭ#dR˳QbsQh6SRa>fLjS8N8*.DG(3q(K^q ā ė%Ƃp_TFF:q`Á2>`0zbFy CtZ[V}AM r ~1hdR"羞Wu3 yЏ_YT,X| DrT" >X?\ȨHIJ1?B}zꩆA*U2;ᵭ~%Kb ?|6c<ͶX)s꼨yjsm3ݧǒЄVPC+pP,Ka>:]C >>G eܦOa'x5mT7 zۭѕgP! tX).da7L/Ě6of=<[ƠV!P{l+8 $-2{ny_Qp 7޲+ܹ=s 2~ :>!Ϛ򨣎inp˵V_?~mnYEq~[Bmz]=>ldp\+cvIK 3ϒý(9_/9WJ&wҬGkS\:YIt'# χ!`m?: 5G[B9CVm3q[Jof7u.\B]q%n̞mtjIx%;m׬is7ϲV2_zu_W-WniW]}UuUY: p+))T)B.h<\"ҍĊBȴ02a4`^.zIE ?fS4lKPXQL'0 Gi|\@~ǥ8}S"^ AL\JlOC*2HHa=T SVpP7^F ܩ+4O~v\zV-u~R:$>khmj{ p|UKꨕFTQV$ [Vy5(qVdmءW3boL~M,Q_5+_3l3un)*+S-gAMubma;VrW_5 r:b$YeP;l# =CN$ 2fqxFZ!.e6ڙWR1鷀&/픸κMqOݪ~s;1?O?ia[KKC}.bR'5k+pʫ?ov_?l0lY͚v `+ԮM5ݝxB7]0N8=k2"h'0 _l?! ZAo\?ƟO|fۘ1cd,V'X:ʿ 6Β1?nSY?Ă>~s \p:}`)4 pc1cF뜽6m4-@_-[Ƞ_2BuԩW vRv@mtGዺY]"8.!A2pAd.ɓZy9*Kb$K\&Մv#׮좋 6?vB1OE%e0_Я,z+Kr +_110է!-'3]j ]uVY]JxŖ0l3`08p} |ɏ c%Z)6@Q<?p8$eL~I| Sf1X0B~T,xT;ңxOL4LZ0B?Y[>@UQ(@8UT2B?䧣P*_*0 *坦b,~ӦwIl[AgȘ1gNy߼=quo2!^_O q{َ:\`^|Mȩ-ݠP;dPkF~_:o״5p=[Xj袋K?T7^ K m~;kaU? OvFX9^J /hE>Zv8xPV[ys j 5J~g=8K H,Al5rJFg%^zeV+k{'2w:*^s56kbVDyܡb}VƤb _Y69[zN 1LjqIڷ3.,џ4i{qr*3V%r]wiT#7e+D/-:CL+ԢaP[Mp0.t{v돞-1tU?PZw%PT#ci F_߶FQy^:ѯUi<|O(/zoWJTc@BGEhE()~i.3,a!ƯW瓞C'H&M8IM >'?Fayۑg'xx~ 2R"A$_H f`> >)C#O7id_7U+Gj!ҥsM;y:TQ[qwmWo Ppn]m7P ڵeko4V[I^e2o[Vt[nzX^~^ W||ӊMt,_[F&GQ/)Կ_1g' Y 3J+he1[ nW?$EU?9I1aro=-ڿCGmԤ)D%L͇40Ǒ O x &^yx.a/&0OG< ǧ@$AXڄxH>񀅹HC0>0a4g CY\W.|`ig[~qjJo}}!Q5GZ l kJ3RD фQj-Ӈ| GU ڷ4:jm: ͙kiegH&1S+?r|0ݎ3J[){O!ͣېlLzRI2l wA&:%iΝȠRo6m|. s԰ahE%lۼ@7דQsAc_Yif=eT /8O[?t 0CUymujru׳Z+^͂u(rJ/?VJU[(J+=X%=2mZ+T!zc']5m_Ի.gbB-ܥ.5ڬaFSv>M[,>?O+Yg/Fz`6u~VXfkҙs$7nudH/~XOUکmU> [dv~pSt!YB+dy-_BPC/[>h;.RwN@?ph. 3<]ps!*KqMʒ(;HMz}>Xe>5*p|Ƴ,'~Dƈx@gI ?NVbr-T+~~.ݯl_^y{G3f } ȽZ]hLEKԡ!ܨF/asW"' /#bgn՞D/4BzکK .B̢9Yk|u.V0S\dg) /(C.8HXjD>_w=[!lORy%?v\41E? >|gWlL?d(yyn]D[.Y} B|1p5P Po[ \.Ѝ3䠿_g] #ږnMx[[_|E[KFbW+zy xu?гX9ȍE*Mv{wd VUԟ;fxNGQdmlYo~Hg$>֥jɠ&FѿK/N۴#ę;cmVęemMW -Ɉ/b!CflUs ḱv޷sI~I%cʹb)>#F_хH+jnO=G7zhO~S+RKCJHI4xnJ 3%MR?{,PJ.u_/7ЕbQG* |d-[峴s]$~^A u0؉xJ5b"L~w )80o0\+q ^⅔/G8\F~J]/剅  !$Oz0i*byFL]em>Mn8# #4#|K/|B.K˒f"MǍ7ՖeJ?FnѩwW7Nj٤VmCiW_Wn:ֶm-~_'gx/;\ߺ?bP@V/Wsis ֿe/nj'zwn=o͘1C[A2@q+& ~a2Xҙ`WLjײ=v>R:OZgYcy:cͭ4Pcdi5j{I txfvR;_.Dhm`[s5WzrtY2w֮+3p)x?+ɪM<8 %9\?; %ggnF>O2?"Cѭ1?kwq;T+D%/Cʵ?9[>}Ef& 5+4f C9qc˰tN2ǿAO=gխWuK/l3ӿ7֠V?@+&O[[앗i^Mzul\nWʾ+Z?E[YC?͊_6i%I#Sȿп1;On#`He+$!wU_~$V16:ی?٘ gw=?8! R0>lc"ry~2T|E^V/G8N"xSeCud" 3Obˇ'%b<>\7<0'/QN7ݜ:|d'0k.GtB0+_z9 Ka0)L*5r044n,H/[Ѧ %. x"YG:J&km,Y -[~ط%Q% E_Kk؊jM.?UwWEݯ[ :s6_non}s4Xe".u=](kqm:# þa\V\0h.ʤ2-mx"t`͚6u|M5!]g]?`ޗD#3GWs[m{|L+ 5A8}SRJmCzUW_eN%JZJ?#G_(_;g}Vލ 5cgPA&sSkԈZ v!ೞ-:3g'IM?͟7߾V50 _!wu=?:kcw.܈z]w)&Uݺw-l:m]be1OzYkL7箻?x\I^-C޵SN=6e'%ZQ7VmmW8>2)%{l=tyo\*Ǵoe=2^`=<;j,3_yՕҫ+]yWkonTUB /ˏ]t @²?4 %鷘C"GNʲ!&HY&MJbPk]VOoZ2L^cP 10⋁ xޘ4YmN6HK) (K(G0aoyQ.#_E;AT$ L'|8#MXt#|m '= rMz>jmH`kI9@IkųnJLg+JLRRY~늪< tHB8<̾77iԱ{_%vt*NRS<'SJl3cß}gv 'vknܼ㥉 e_Xvݶ=$;j![. Gqwe(:dWSD3xh_J鋠/h$ߡ-j=o 䜰ovfmהAE >}-JKtv-A{um^:g Su+oi.g+ҊorÜ7C 3V]mUݻnT[ǟ|!.K 7r)AL9Z¦P$>RPKi2鬵1YfBͷ|:cڎʅ 7]5ɟ37nEsq LzNNivџ?w ~{ǟ?GvNƟ_÷Sv_ԩ[Ol0a*ov 'z.@=¡r!N;pƟc>ef9{ 粋!lC|nZ?ƛo1mlUo{ك>zse㜿A??@[GyijEdzmj*ҫn}qQ_%qo䳹(vlUw ] c+RJ_1\W1o1_?&NfJѥ+xϬxLO]8?;9x%qon0(؂pd@%NY2FJtʒÑM:'E9#/A3\~2Rb&2 >.*O'w<<## >q\a (Ga ڿm\%Ԣf9+*JCʥjvMVTxyp\SPKQ1*UNȾRgo4p2Ŀ ĹJCύ_|K #俶dƝ+F;L+hVUN7TP ޿o3dx)7zK'l3Zh=,0 ~Kgal'k sӿe?_ΪZ〛g [k &k;."xGZA_}*V]ҷ/`í H8H^#F~]-$흂upYz~*t׍V3vi˼qVZ!V[yOӦy=L4\1>C+jU]g1>rl;S#Lg=s>`n,;}lN[XWj~oJڬ~m}.Ы$36u6?W8'Zl!{DfE{:#QNO5O~%vqAyOZ,BW?iLzbE -柤/ȡŅs wʓAMo$szZk6#ĥ4v74'/ bN?` !3ǰ`$4|.;2B7_r,ցlI]7* aJ4('Gz~Uiyxx`)eۿk6(ǾS)af{P E|kh(2^@f`1C37jʖiSڔS+[]缭roQ rgDX @#-gJ+Vo_*Ÿ/6տr#jWe+6sL7vn_}_otk4'W:lk] ߙ-tϏW`zr˼TEY|;AcjKGƭj#*s&2^bE$.^ ' ӥ˦xϢ%XK1Lc]qy!ml^CjďNa GJ|BOLV8~g4HÎ_Ǒ#`H<0?xS֢K c.ApQ yQ<,_.24eB7 go< 6,[>3*FZ>(~a5)0YoѶr)Qdt4L[tnA_WA//K{~}f i\ }Jf=GyT+WƸFDzx3W엇 ,Z\"Y~uxz]zkL]g/3 =k/Zv./FCϾ%ބ%{pr3fazD hIչn=NN@Pibs)f4Yk2SO>sz#o6 =.ܨQSS:ӳ?7%z޻rxQtG"߰ǟ46:g_bK]a43f+jߡCG$8pHEOO|cpG#/FJҍy \l;+ʀ+q\'-p`.)XpDa AѨ,`*(a*̂#PS8qE:q1A/ aA4]KF6pk߆@}a]b1@@8"^rPґ}f3%@ǬPh|OM$;<}'QRKK}PchCY'>}8{p`~:v񉭵%mWkQf/_μS{%jР%]-m{g WMkF`T%=#T/}aZVm謵:E7/ߴmEV"svUWا~&8%׿nuKm/Uh -|&V'Kj'ITzX )p!BJ(_RlH1O1~edq°_wYyIb3Pa͈㇍cYeH+Z<(& ( OÓF>x /COAQ&8 `܊0eʃJEe LFE28gQthR.2qodȡ$"VLrWȆXT +nq\L1u;KX]}΢ZU:C{OQ|4𵴑F- װG_Î3Hc"gk׶6cf¯+5fh 59POnYߏAQ/nNo,.m}tYE~eou;veԩdXϟ׾Х߻lάYj?u6.ΐRw\ĐM]`SO__~p1-?˙|weQ8Z3ܢ![P.pE<|:`HUGy/|F y0) ¾CG^U1T KFS!aB0r& 2cH ^H%-” Ls294dP 2o_I9e%5'H30yYcB¥]S yQ;rS.Vvm7*Fs{_%jǭX4#/O暶V̝;<yapLT& 8\< (qHF8h f6| />qMRf6Jn n–TxS)GZl0#l׈-7&/ z2yGf. /P`Jsa F y0<~(*x'?n q ?RxqɋO[>[S*PxE ;ດ!L/l2#ߍe^ZI`ʅP2Kv(:5R-O˙(VZ_u~?-vYz O>ッN:]~{:dVخcVZeLg5͟?V~yk7 ֹi,-wE_ZrYzܹm&6klի|jLuAB)NmN[YgagYf/J.wAu[fP. {SUR KѥB8)'T% /v[?_vl\,ƿ?E+uRb?S_ycET[>K sHt&[.Ya {q°7Ru*V+`a#|ap&)•y_A/ҫj*e !8ia"|G8%?,0(.F12>9IzB0){n\V?Ed)sCk<(4є;$>+Il)Ts{(^\~*W~&rU2%d˖>SU ?T*[m_zfLn\?1syv`jծin Ww;?[EA༩xumnBE&>ΰK}y5==ɰa%%ߏpjBR%ų='Ɵ[f{sFU!7пpߵB< jie|; ŏ?pۮڑGeӧMK&O/e#wЪ_Ϟz';p_viO />gɾ?gY[v4iJRg j j0\`KƳ3EXA,0l0y(p0&~%ܔnUIӿ\eBQyGyz!(8>aF;/|A2eG~Trә">>-#P2*iWGyK"]igPӥ.o/QL+HxCհke^&L`om^{ٴri3mװɓm!Y]<{ <[h{lݑ p=S L-nݽ ҟV$'ӳ[?=zbm;z}Z9 FYwAm]v#i2U8|wIQ1}>`kieAAտB}0PğžW"2Iߧ몾 +Ǯ&Nr1K x]^RDZ »:1{Pt?=@0b, z^d841(Gz8q~LRq#-)x!'(C"K0P6*' yE~> .pA8?@|^qġѧh9aK)iugVi(E[>ym0&|D@G8D v9O>pFzyzyH'-`# }G8SpBKDL F 1,Ɓ3p&rϢ| aX eIՓ8A/'Zk_VCu)|a V -ŁZ@XeaVe,,jo-ߘ>>ߺ?Q5/+Iz(Z5ִn/NZ~]n2]::XfM  _y`io9ɿaÆ֠6{LcTUV6t6fhqnݺ֡Z[žb}h?}՗6o|Kn:F۶}&O&7g7slckٲ껮B-{R76\Q_m5iӵmU].YV2[tDQOO7VebVkA9҆u㢔.ڵ[ZhOg-jc9zvm啛ۜsmO3#ZN9XV8:uNlVMOfMM~1ܮ]]w`;ꨣBmқ5k.'ʹ庬ժUS+=m'jEڴ]Sƾ_Æ~8ԦLJ,_G8Py.c_ 6c^U3U_:T1FׯWGE:|XadK0 Ɵ*_;ĀTп%_Ͽt_sz[0Er1Hc'k'3+jK#ȲyjҧR}\ts#T6fvPYQLn&Mc(- %/Q>ʄr| \Ц\%Kba`*"QC/ʑ`r8a <"l%Lz :+Ԙ>CadaH͇!7wSY2) \pJ#jt)Mt'.vW(g~N4S_DW!jjϚ+Գ7LgØP_4iĚpPfm+qaw޴ s5@N[JBuٶ1qEFǤd˵YtFA].7x= kmGq HJTѦjkϞx 5d;S)*}{U$7d>Րs}`{% gz#G?_{}gCȿI'Zy8qD{[Z2"leJ(Tt0wA4jG[ ׬Y> ;s̱nŞ{i;E+6$ çhyys쏇dtƺ=zkq~" &&- #1M-տYfD}}.m{]2w<;CćlECg}efjH(GH>|*}@6ɡUg^l].(H>c=i.[1o/I?i j0(}ӏb柬r%/VY^A//ݠ_}╴4',S_E+ƟbM?IZ-bľ t.hr)O"^a<C^+iā!#N^va^IB>!=p+XJ#?*^ZG%u<3D &?" -$Åh=A3hP&C>|^kZ2}$iz 7x;%FVT0QybdK%Z67mT "-H_:߾?m4k̵mZ&lbo_MDߴT弑r,?P\4ԡ:|=h9۝K/uWE*?+=\_ATX>Z:(*^E*#dM;1\ ;^2=s2n/~i3+.ʡB ںi3Tcj?ua20/KZԹU%Rӆ gE}/F Z(rxmz&>]dcﺫ@\sd!r4@gTP.Y!MCve_{5ǿB`dxoifvU~ҚZuns %"9M.%J#\޽}/tqU=z˯Ї/6P'ŕ9=v(0Ik*VYU>

uGE[_W?ߟS'O,r PcB[yСu)A峋pAa'o,ݑ+΢gs?o\ftpy:+qGZؖ80tx@2yrrQyye'B<(O86+b# Zv:S+>-ZzX?G%`N|@G9,yUJ㎴q_rP5jNj9ҧ//}VwTnum̴m6c~]3i*ej̴5jsZILh~2yM{j ,'[7iMtW 0<56£ehj jKoճ=\s -}]e:@+sпb7|}9+kF;k=m6r_jh¶f[ܙU^|%[`k5Yi?l/_b;;B?sLD=7}luhrک%,fl]t(Ӷʿ^6K/@$y+#Vf uBS:}TV*iR}{ャm5|;<*v1kE>oFaץV$Rl=Sj5o}}+%+ +vygg}oaDF̖6yNZy‹/ٗڪhj\J0] ㎳m\ͮld+{ Ii|eA+?pG9}tS:Y+҇u^W8˵u&}O>%w'Yq{)|s8<׬y3SiQj{?hu5xo5ڵuwI?#;JCneQ_W"j\!ԗwWI*Ͽ> x"cb *b);EG0 ($j/n}u䰹(4' 3(}<igHG|Y}˥ >8>a#CQrQpfs'ZT.G=4À:k>1ڼ5l6+ цo߽1޺R2{嗵-/IGeW}av|Tjn's 2GsmvI'u/t*L[ɈtE˼;vVeQzO>T̡OnOO>F5azه2Gc^h[89迬Pld5j]x:nןϖd`,_~rݵwGگkAN;47g.Udž3aypqmQqy_ B~0LW 8*X>|O!4|CN>F3`xp:@IDATQX̖OO5a ؊{ FKQ 9YYSnJ FyaF0Bz~YD{I W(h>WYas|KZ(vgtg [n-Vko ͱ_Lcܯؼ3miVc|[\|t-%=_ævlfo|<|vҿbE "Ygdžf˃<`'ETLߤ` {풾}]kj*{OIkoկm=vwzE}K<9ߌmΞAɍdhԴ"hmK.VJ/Y{WW<٠{ePMePS=.R{㭷lϖu⊡3?weWq?v{#4ʤdBVヘmN:=ԓvV'{4 VoyGܯ=䓺aUV ~0?t rٞ~)UJ+ )jE"*vBBU"CDP|H;$@z-=wwM#f̙s̜; &o!G`\N ]b8V2RP@Pà&be|V8 pʞޏJ;︣?_~x-;S,1n! < #>]qca5U<⤁ m&$PN<ӋB.5;FDpB/s²  '?axa,s9saxpq. -sƥK?dŌy)G)CDe@wz%NӦYY#Bw˿e_kuڴklCbERe3*TV, K(* l‡Ru5ymTB[p hM$1ÇhLaesooua{’6m v5We A͜:mv{An:wR?` e WVVdP~5Ѡ.2j 6DF2)Fx/pX}u8"3X[RcJ߬yOX}6nyW>~` _CTw!tWfS>; f[KW^ O>t9Cr[y U+dx$>ZrP^Rǿ%K/~0c:~GgqM5ŨX+XV-a-ۛ~*,w 5|~O ěJktᯏ-oO vmi7}ovEG:w*?elk[]Sra=W+:Ty=|rK\ߨ,ICxlߵ#tzXD J\ O`[%OWU֩Rk|ƟBH‹_iƟkK  zȠsZ=`5Ͽ8C [>' Ql&N:49q4p^Ђ&C%V'48//9e" .\(t)⹐uH'>Ma8/qzqċa 9${ &143Zy/~? imq+IDSLjsN1$DpkA/78E S'Z;o~G2l鞺uhѢ"T-*#VhIn?YcH㬵%2-8~}nP]0}vevo[شg%裎{^xcyF27J+ ̊PSCcdT3R-K)3#VEze-q:ˍ;(cV^I\[AmXf)?Ta5cil jp P0zV-J#<*`8L4 7P1 _[3EeLR} ~LGg{1\̶Է2.H?8n:LgV~pmkcV-M6dU><裖Ԣz,֭G/KWr7 fU:Q#aP2Dr)+$ [ƨn 翬\z h߳B GW:KꟄs='lGq÷uf< K>a]lahwPcksKme]:ȼɷ jPH_e߶:g6[V&x.ͱv:SJkj]*?_iU[s>*_{;k|_#G[qyT]2?P.q¥_y2)q1UX9 hR6'TC;}4YLK%4@~C 8qpcs8p`"\qZ+x xs|_P9죚^S  f:jtO6@O6! r$*4NlV6]/*R/]{m0|wZ.tYo@ؠ_m9[5ZTXƌE,hUڢxKUmmZߝs5շzz^6QWLakFsf2pfãT\4 E/! jf@+uZuڄqc蠳Y:Cye!|@+wn6Ϋj^lV/op6dW2чo]{>5'x:wpf2*J~pVUaa%b{2,0bޞ_VcOPF5 8ti6v! &lg6lh3j(3YA Q`&G˱hB7nz͵2j^Ґ3R>;-B-[Pe?[7lVa- [ȰK!om.sn73~:p .CxLL#/m@eW #hP(HuxS͏?ȊgAPm.~\y_%xpԏc_IWYv卿 ~x\V0^% kCT (J֬'O)kOjMҤ?^KِoC/mbȺj_Cc-VPcVM5Ma"n#N^1:䜶㓇qqG>F[K>>p|1wLxX)p nHKiF7N|8R\^O'/<36y+q^:E2p$J*(BGKxHO| kzaH 3A@ Sts :k˧9A]u?{Q}uC>:.4b](0j¢iҴjMӪt޴UaA;vKW5ٲ )- =<)ӽӶqM7$ӹcP]emJ[> .b|~X:+*~}rjcPq%vd۞qROy\!N=d(Mk -]T1>cƌpO~l,WIC9 z>.|[4s)\J59h R,MVQ^?.I״od-C|Hg=TgUDskw뮻Y_L]Oo]`p͞_:J6ygp aμa/~K-2\VVsZk#-U2 4P;\jY'+ ̉&6 HOuYrY?^N:49Vma%8c>gVtc[o3qU0ԙZY}T뎿Xcoxo?mҖO7ĸ:ޱ%Azhƙj8`y Q0>'q|xOAyaܢyCu\?deG}Pj6>1,#K1'B ]:uͿ׃%KCbZ B΄5JYu 1T'ΑաWVa;jEܸBr}juZ jlaJүXg-T>?;̚0%|2sIujǕjK_ ZVZUⷷ 1Vdi-lw63v7q{~VKvɽOJLgIۑN;Nj=&Ӻ 巡0$}ݼ2\1Я&V#^}UQںd(9T֗8_+Ԏ>\i.a쪏l /10jŞge`S#u20^v9e;]g̭u1\XY'w9 |soõ ZwuwxDF5I"alwoo3{oZFy:/nvVYbљb%WϾ~[QmʊiIgq,g^t,3N;=̙;'gVTPqmkK.T0C eJ>I[I,eÎЖO [WBpfB6|wsH=76Hz~SFmen4,X{Fr7 1LvZt 4o2'*ϫJz?TL vJ5CudtjUW *_K}{z' 1T'J_BGM{ *׫O 7J,?Px{U+ޗ F2fnLÆC08# p|al2ns.qw, >KQpöt/^bs SZs7f1kO+0Ew^!SC@i yx/d+'B-73AN4д~N;B-JW%(kkjkEa7ֵsu6٬ruP\:]F9}I f-\RP۔V{u <:/Pv.}8VaB4xM74\|jIJ ‹gC wश̀?fY 5`e Xyos@?_6Q% 㟿4?J]iie|EM75I*f Һs t;٥uWl)T°`آ{& b=y:'L燖y&\y~x\"-Kesz"z b a1Xy%qt/uIsa]p$s=t4*x0n|^zhٖO( *B@yōi^.l繢VfH% ,CB t:u4ߥKτg]I?Xo6l0hQ+ڶ(T--M\9Bgq42fT[RZ* [iKBPէͩ3e+ԎxmaۙahEix "dL\+F\qE۷_#LM}QtSꩺٓmʨEya翞h-D;%)~&Iye `PaHge6OHo{ZK[`{7jxojϙr#dX,S^Znm{9F+ToA^{hؒwgbZK:nVQ:UZԓGpQHF4;2"3['q kW04h0\0X[k8C-F,Ֆ[ yZ`EgB-ĬZ*Put?:lGʈV)uRwrի^q-D"[M r*,G`m ^[>Y(3[1ZYWH NviŕW=Zg 뤡OC/#?g@?ߊRM㏏iM3 6i=tZ6V,ט1c&IC4&T?nt,&a紀s|.UI9H'z8<#KЏz}'^ob ΤX(0a_I&wzypN~<^>t aeNC\|?8)4^+bGjdMh0)=o HqFć ѱ#1u#E|,4M d[jy~a~mC^}Btc.$-UڂV3tcM^sƒ0Yal[oNZ~)CW(Ūh*lm ?I+d.Oϯ6<䷷ &p?e>^8唓b1}/Z:Z2N14mt :~=\}mp '!=n[?قn5WVxc *?On-h "Yam) VCʰ (`ˮ'*/%j>UeQUU#=g{omj} ~OE&.DԹS~1Ih]x'>3TYn6m}/y7| :wVwi Ÿzpםw9rO~b-fH:lmXpVeF}t!#8pO/ An~,P9@n]#<"lZƨM2{r[fDo߾VLE{Qp҇IlL1Ro˯3vk; ~۩hK?V( ^{ _Yڴn[jo5\uQp1Z<1QMy8b'Vl*?:_z߻oiA㰐6*t'=jթk.˞˲? ov Q4E6`LL8 #xOt F">ae"8в葧ȃ0$ EnszpuĽUa|px((4\6^7O[>7Jg n>[+)mc\,P$")W0;btF U\|6v #& ߵӒpطC蜮WZ}VaLR%l'7}.9. s?Mu 3+Šֿڟ37\`bm~;LӪM/?um?̟7[r$_GEt.ZNb[(ONB]_L*!tת$ xݺw"?J'Q~ЯƁ>2͕͌ӻw(e+>U|vkjX{$l`8UKS9֯?"0.o%_yPM<OY.A?a0mԯZYhzl4.:-7wϊ޽7Ԫ'2ꎿ}apm;p}f. RHPrf?Rqhm2:SRolK?iW ο];iy?}-Ugp;vVM^O)r!/%xn3"CZq>;aqÝap{>O J7pՅ(Xwf^4 鞗4VH#M/yxrb,"Z#x8Og&+K[y1oH㘦ۖme"D5^ &diZm"Leն2&?/GUM7j%府VHբJVfȦ[>+ysVui&|8إL/kuKY_CmS)[m??߿-ƿ?W!Ge(kP.ׂW<,,{g–%3Y%2Dn5#2P)LDVSm+Ve_u#VGpwM : uMhʲ_YYgNX02<7~~ZY>&ӿh` \߸-?Hz.Hj1?b[7!9{o:]emtVy7<㼲JO HUƦm,VvO_ia##H_ʞ̬ЖOiYؚ1,* Ϳ 5-<1Ş[>qn E#6F,ArOWМ4p).yUŇtls9ض O8}pwi/X PIfM +4ȥPN/*=D'/6[UGqFwŨ[ А_ң&:rcP*,dUfH/?mJ5ڑܡ2vfb^ *뿦,<߫칭G4CQyN O2>0K85Y׾|GugV>ԓ?ϱq_ywYV;gͬ`ӥ=31/il>5i#cmC_1si7Laq= plA8l2K2Af%tpnw‡tq軽'tݑq|y^3֛X蕂^PO'^tpp?'s z|ᄀHGL>[T( IR0Fs^/-:bʈ"\h(9 j%f?q3"#S1dge_j4OR>pۇi0йUm+jyuVM$^uxsb "rSq˧ -3y$\_J6W^v)y¼ysӓƿu[39M?iK 3DAo*X1W.? d3f2T/_' W';3q b^0|>zyyW1YXS3䅆 q7pyF%%x:ӤāW+Ⓨ]t肃|}pC6H1| bN(e鋫b sj"2ό \ȸU g j"ڹSgTK/r#5с5 ?Ҽ:RUpڿJhѲEg}UUJiQUiΟם?,[Nj(ODz?@X?My'֗iM4*'#?YW=5Fs,lS>Y^(1<[y@L568co!+ 8ϸPp8V 0:|;NSA'\v`ǑLG8¸PENυ<y;ivOW |I#͹gHD)Qn3(d#L"Lfvj+1D*[𷡍*Ti|z۶Pz.3SՑ@FrXVgVvj(iZL+_jXW $K7]W:7}G35~1{P˱{kMq*' ?Zi1td80c1C>+ɓp<^  8:-x>8 ץ)qbS aTq4 s!+ʊ<8Ca,n8'e4aCm"3C0H6c/㆓%"-;Is]g9#(a|¿s.uJ"R|eIϨk j B.U8:m' +?3a'_T+%,FR`R nj3I.g { Wt݈^HqҀa8--Hs<h`)hpFar^X.TQP…J!N~HǑpc@œ'>08_s#4-СC7H13颈.@^cov k! ?NLE>9Ͳt9ZS07JE"G`O;'-kC(kߨX5V`g/|$["Y jύj/"Iobm߬3/%SjEnZ0_?(i?YŤ^MjH# И5ê$ĩF{M}N*fͲ]J5Wk8A[>ߗ82ZmCq|a,s "9Iy~6&<QF:t 7ِDl@8#*㖇_̣z p\<S87)i^t:R!IsapqhwäVEzF:]5m4GPYbA&V"*-bˏ rn,$1Pw -:CPsjhuϙhդIN_ iIzGWEƟ4|Io^_wLO{VC^I? 51aPùы$c8>6LQi0 pq40"79-y^x1_'J$'̝#P]W1aQInrKyeyq'"^σC l„j,&H,ڊ _ȜϿQK28 CyztPZrY.aˉX.M{[lXH_V|Oo>t7Pz}#7mH4H4X5e|jqTg*?3&+- ]r O0F0a#׈/ۛ8bx{YDF(.\Ҝf'`w[7 nw9|x4.qҼ!Ԗ WFͥ2aUŊ#L7cH'E ؤ#o\dAtZL?E@6+vBVAK|rEq\RK)WnCrIJ_WҳbGLT/ߪYJ4"i]mG=bˇFf;vDa&N"*ۊxuE:|G6w8NOAsfuwi.á }A: {!] .807v!GvaIw ? +QvNiw\&0y[> **7ʔ\.x `DSrt.%Pt(.9njqAmna#i_'^񎊷P.B͔|\Y&ySہl1h{7Htau8yqN ߭12O'/·8<`iR4A^"c/wab< ]8'qRNt/zq1%ׄ8\h 3ـ߈M17jaM<<::V Y%LӖOt)!`L&cC-XLZ^ߔ:K{or)`EUu8J!S*G^SF7O{ks'?IM:L+HBCdks֬VF|ƷAsPQ2- Ɋ|"1|53&#q|e< V'wȯp`a=.vdj+2)2 .%0KMItaB@Fpq}(08={~x>|l=8|s8^6vdh+. S{A= ?G]x7;O⣨9 u_d@Ypu;=BE -am7wqƒI6k358^Jv)W;+?* cZ)%KZzxH} ̰e?0lkդ5UVY'Mta… p_XI, ʰVaqJSSwg^Y(9տҿΑ_wi_k.@@IDAT?H}H]߿[> bs\JP-[>Q<^n<CvbXQt8i>xۍfđt:a{\7蓹΅8"80.axwN'Fw΃dž6ۆKz+t%tD;fj)B7txo:H*LwJ1{QxFK8ᕧK;dr???%Cg}wg 5A[1`PӿyasYa2C:>CaGżm!Ƀ Ǎqċ|ypisᚂ+8\@M΅# cW6B w-`l4H#|ܰ!Re0^[>u"BǎKN 5SO.b3fT*A0)K2tZNQ8P 5 jAgE5,+YW_(ҏZ-VCvW̆[+:ɐ-Mon/sQwB9sF)+ZL{>uY\00{qx! URS?&O㿆k#O/,HM_Nz."3-AL~;v |l26ݑNJӰ͐w1x~GOo7_ σ#H>GWrĔF~Q#(K%9sr.t1^,Dxa/X1xv:yp1Ã|BC d|P`_^J )ap%-";Ā'C `wmZ᜿"E+rrXF ^yCO?XV͒1녗-:̝;/<î_3~7P-|\CnCe/Z.XS+{/l~TWK?/P4EKOWi5=tzāVZ"}Gw?gUh˧_? 5*-x` 80y8M(* CG{ q=w9=pN/JE=Q 6%2!|XHpEp.ܐ+\ J44ᅱ hǹ|{3J|ƽxs Z߇f#D""(&2 *9sO6"t) ([$gr1忹&G2,RVgug*nÃdVAu뿪J=SO {ӟ{60eqU8Cg-0%q]"B)WI"m}z!8dO=d ~i-щ']w5'9+[ok\vy Gc+5C 첓CN:0i\c {ﵗe8qb8ECDTbH6-_cXe]5 &N?:鄰뮻F\1"=F^>; a<4px.d]|8 OA0xiF8?ċyab:Cmc4?a a %M3[x|@/[/&8m(E?HB4&%XO2u :ʠ& e/с4 f?ElyWk'뿥*6!t3c9s3N?S[3miBۼ.oRJxJ7Ok?ۗĜ2Wö?#7Mi\TV2Q?3o]F:qߩvE,&w*ň#ë[a@o9<6z3ꩧɠsV!rdxWÙa./ћE?hs~Xtb+/=k|==(d»+9k(p^ި LP, !<׿TIRK/?sW?gu0s j+YVߕghAMcƎِ:F/F( f8>7 ]4f]p}da|{ǝ 5W=8}]LDlNН!p:qEq/V@<7oС@|;K*}Ԓb2?*ٖ9plZD>s9\3ɧʠJ.mG#:obߡ:|W7z5rvc祗_^xpqDžuڭ+Jŗ5LWx[Gm*\|%lqe>PY13Xxz}h,C;Ni_OϙZ'W|AP?Q2h;Mw!0d*CRPe2^s5a ַrN Щ󘵉;K׹a3AګۇLeLè1~3I)VU7n*7N;!R8qV *?_:72_u?_ҿ_!Y}4~oE,+ԲɈfU4uP(*P, ǰp+n#N}a3i;>xE\pܑ"lA./pIYbrm35\ 3%8]pcQ07y%]x{ဃsa >ti<σ:CmCǏ-,ldHUnݠZp OaOmyc 8e$-KM2mB5ZCqmVϺk2Z-qıa>_hzvm׉Q*+ }kz=q =_FW!<@(ֿ]ݘ9g2?a„wBm6 tG}7l!)7V>`рkjlD9bdxW{N8QewGy$)ɠ j!9^}o}3l6Q>ìZeo@w}?iZgrdm# 3w~xP,~@['ԩcWZ/_?pOozohmx|ҖO\2-c;YbiRь3p1.px'=Vx Z>G<yI+A:΃88Nxk r 挋>0u4/479n165p1+1T$eR|y& +CK<;ҩ T9j*QwP Cu΄P΢;:w6a:vUhѲe8K[?c+.XbFtBÿ/Q=eX>-YjڼE ZV%Ս/'#ck[B s;??C8kO9d[=UW^m Ka- _7|Gg ͓np^W^y%ׯ_XUWUlѣ[vfڎ;g\GO<-]j6' 4(z{aa{›oixV097bJ_W. 'Ec7QqCzN:i[0cƌK/`T>-qakzI_p}V`>wM[mOYtޙ0@iӵkW7@ڭv.]\^jY:u^ Ӵ:]88wnk8I;(_Ur%H Չ`i/VW uO5*5ǕKJN/Ϳj 5IiÇ_]XSdiM>V̬izO_VRevЊT;p@R A HGܨ8D8|2asߝ-ky:-9]ׁ\܍Y# N!:=JW&?a-7C:CaC҄rU5faB`\( f7y jPq;.]~\Ic=uw pVZ;೓V+a-֧׳}+ee1 yoEXۛCח Y^uo<Ũ.ײ#8EMoYs=/L<9/|pmI~h%1QٖOmŖԟ46ȟRVՄO>0l{[o>p9!lQ(8Ox r7p5]ޢ\[rzf΀*̘>3leXVhP 5qz "#^йx:ꨰV?1ZM W^ya^(TPe睌 @7/0_mV:SN ;tſJO?4^H ?)޼.dBJ+e.r:ztq m]v=\F/Otޱ4~| 6ѝyY?j ",ɈQRG.%PsP+c&iGRKO'W3?L-kb&[>#G|2 jBm؉9Xhl)n6aj%L:zGV)\Gt-7)"Lv{~<. ~awIewz"z b a1Xy%qt/uIsa]p$s=t,əhT2".a 𝎧ڇmSbjF#s$!= $|ɚ+Q`R2t6P jӌ.>uTߔ lY>< K{?>.kؖ=u6nPn9?_Qn|wluRI{BAaƆ-WoXלkfz+,ZQ;^ hE%\,VzJ1sfn 7ps j[9ʀ+>̫2f_xq׾0dee ]\|ɧ%2⑆l b]Ϥi|54]oAC~xxҿeIOzA6>|Z-|pdMG|@ߛ0Ej˧|܍fLs{H0Iep7zIw<({!Mzny<CMrN)\ڄ8vpu#J ^a|phM>2 < ;-|pӖύaj7^>06&rXDMa("#YJL,AHc|݀|6v||Ȅ֪ͯ&iNF3ܹ: Gsf]#O *Z 6raz/ |yp:Jik׿ҍW (|*@cJ?+~]t-]dx>=kv.#_UXsT6j|IjwkĮFqA6Cϕ뢋. |qգWbIo[o]zu駇V2 l[omSNG^1.-Z;VA:9q+oΝͿQ]xxk^zyU:_3JY ')2*^l= 5S5?N25\{5~Y>m;uǃ×ZϥtR~K/L3 䱞$aYd{ׂI8nAcSM5[>3?Vۊ92bq_tGIh!t!FUkStTXl賏߻kxyagߪ -[:˷zc ˜a~][x|zwblҌN啕aK 39ȲۿwYe6Rne/qݿ_LY5'֢UpOO;(GvOB]@0q9;VBwl;K+.!p^-g}V2d[K>nK;V.v!9=K.4[-y.%X//`ᦛ~\bm~a@dXh:7~x[|fg͜ɒQYλ ??'͠vC^W:nt;câ ڴSFGm" xEE`\:~uת]z)K¶]foE>[H>,Osw>s̜hc-#k-0xiS]PMu/-EKƟZ"޿qh/?{:*/#3K&6nWs'\ n:52 ;cDpaƩDF1p4qZ dF4ˀ  ڇL`T>D*+!j HpC^_#2P/ LtE[>5Bcʰ k7ab+l}UU^xn[T5P{&\yJUGbͨso0߳2tx^Я/1c.3e_7Gz6!nr+ݔi設]rOTk;ɺbV0_ZAĖ[?UhL,Qp>\ |.%q:L[)~~mxsV}uk/z֝w G[ p@c=nP/|[-W^52ʫ9b&t!yb0褓)kKzkUUecAT~l;j3~eP3W0mAp >LиwVY[Qm}?K?0?q5YXfc.ԯ6:k;heUb2KJw~GhӶrDdP#7>f|2f%NV/տ^ҿ|eiYe4GCO̚>|BMڛ翊Zy8'_^3Դ{LAN~s}ƈ8ΰ~єfY$.T+ZO1Iag8VODu)>R~E`eG,s&H:JI8ƣ4< <巼ehYZ̃9?t<"Մ딇G~O+vT@tC, :oJ缰o8Qgb޽γ:;T(5W‡:yUkYg|Yk:_'^\P(( χ3r94?d8pPygleWQ=o;WnK =G[> /H:L:]UdwRk8StCN(NwjPU;ס鄶|C5DX2o*/HZ?#+?̼obu"9.v*rȠ۲,yVn]tN,bE\Կa Ql_hw@x{4E} XPH?gnvMf̙sfΔ;3g.:w!rwY|rhpcf/?c2g(Qc!v ^3$n<}bvR0t][}y8]Vi#v96٧Sk\b~7@Ͼ7a|'s;jة]w͵ZǎTVjmܲ >q\q֣[7;cEԩS-Fg.AڠY=WזϬO` رGA-+߲Ɵ7}([oeO>񔟫|E+(.ɿ em^jslG=H SNF?'mmjƟ֖_+5rQ謸BqA[j,ovxZ"C>pͶ⭼ .w{.S<2N}Qgq6?ff6.voO'jW:寪.(?KkղUG:SB|3QJ^j-xO_}=SzkC=xe?xؼi*ޗf]񇄵omA]ί')9a0׷g[M 0h ,#%OڬdBǦ'^+uj+"/a0AR]ݠteSJqG>!|wOO{c=c~q1q^UtܡBտӟLޅUHö6f_y\Re?[ה߃⯾ugtP>JIˠ®:U[)5xWP gԮ\7lV+tYRJ}R2hΣJGs9Vu$B{J[oa8oiwD7k::.#_.eFǐ;kfS-ަ)*ouM5zo}^=gCvqgڻ,o92d%%~gR >jʷ,c΋r6mM?Z6O{ 9yksKt媗{q}la7S'!SJȥ:u?\GE߭{F79(RL}cjI-[crIBTCB~^W?i/_ I i Fb]zV|T bE8ӑ'\o; j5ɠko" b8C n(KTn; 6pbO4SN15NqF萛Ox\h0k,` OQhW\8HO_v .OO<7L8!o)А?3rhS|˧0"8=,H12 1.J}6r"3>Up> m8'2/ˆOc;j^adcp\?\>鬬/޵^(vvQ;/zӞRJwIS鴼j^c=tM7d:rkfjzK-{ukhXmA:E3UrYiPo *V^mmZqo>jK,/F1<#Zi]CMj2IˠF7?s>?/Ak٢_Z9h/h25wACf? Yݠ[<;_N<.!K,l6g mo~[O} J_[7.JF.%:CM[>9.<{ܢʕ=Khm ܳL{~{P\ڸ|/ >e}0%q7Љ̳\SՊG=[c؀[e宱'xBh%_*ۮ=;gy3,P/Ofx_i[ vSQ?w#xƍ)]֘Lu|~5Վx /PrAxfK ghC /l5A2 ⃧NO8x/hHKn@2L)48pC2 <q>x=~JUWO>iW]ynϤPA |՘a4r&$#3ԢP/j1cuƳVq2" w޵n];[۶mW预ghe[-lXܮS! m܅[73?8K?.A񧪺J+fȀ˗_~êgb-#T+B駟SNO!IQ('~.%CjHEeZQ~ ~O2 ~%BZ7j)o~.Ѫ!O<.~h (7T6/?O-wRWZ%:"OPCR'?(_RH1O1D_Ȇp7 ҇G6g>8g>=>wo?Vg'ˉۂ0^'U Ezh"FҁӗS2.h ,|p|Gz2Tq Ap@d2BG\0" @0AK0yÅoFՏ~gFg3 >I~jv, ejG*c"+e"tefr]E帲ĪnJΕmoY.GTY$?`_ɰAzNժOf٥ě0Õ!Q ?}yϮ?yY۷oO[ًݎ^<^YQm/Ӆ|԰NomF]wIъP䋼qےŋ]g 53O?熍;lnVa?-c2}jؕ6^gaP ;V5]JWO=mi )7k*<跃ByjY=bP{N4SOd;~uG/MXC{u[|o5d瞵3֥2$ӾuXg ϖokv7q%LQ~ؔڟgIlh'NЄ}&A=_q'ogQ$%G"Lb62=H{55(ڟ7h7OQ7?M57w $0iҤ7%=a" ~l0a0ty .FB1!iN4 <.@\Ezp{tN^`.*B#SYxP aCG<@\  y"2qx6Gُs&=wS"5W|d:h>Һl-]RNY|甗WA=U"+ yGD}CvK/zϵqc7>NbFRvTW]hW>N5-+Lqeɘs!۲ϖ.GlK-TזX%:['?9Lj|ċ^0YE|_-Y~vSO=e=/ \߶wUzTM(MRa#8¾|A;p?OZA.qn٩s'ђc 6:x ;_/?._8/0;mJ .Yc\XP.?B~k]pM7.7|;|\QON<&D_l/L; qp6yΣnj)ɿ[<+Ƌk-T/V=eʷ=.S:+H]&n|]y'k bq$_cmd5(_,:t_]#!ByK ]BnuN"O>6g(k#BEK;?E /bES /dc;'bU?SWjOo֬'ty\~)7g? S7ciazdaa#Xm&pyzKt' .R?="]G<|7(tc04!< [ !['}>G$<BFEX" 4xC{Aw-$$vp?R13jRFXĉVL7ZQ2o ;ό\~g6]MY~;vj9eب}N#Ҥ_iPe߲W3KӨ>vYSmLN+n=]I+_i'W.:Z݄o]O{aD}mZ9s޷3g،fjVK:H3{뭷ܕ+4.L8~eO=L-G*u56mڠA[Yw9Z'{v)*wu[)?ۢO:֥7{l{WƮyuB OwAį>*Btl-?l ӦMSBIb`KW\qo1.l6E [ӧ{:͇Ƭ!n{*N\Ոuე?M0Xȯ[L)u{5Om}oj0Q:(ߦO1\Ԙٻ4ݘ55Ps4 3znbu+O2 88g !O:␉84.|z@IDATy! xm c' EjH<.\ ̈yi#\Iyeg8M֊"1c@eAgD@Uxv)<& BPTjǠRwOϗ>r_GUmU3=9 G[.*+{:oao1Тh(@dS]dкN[oCVUcptAI5tq|#sODRj]ꬵ)Kl$WV'{\{m^?l+*+j|%+8;Ș?ӴpwO][EYq;)ɏ.6_WkTE[bYп/!L)??/dJSaPXsr <Rv QaiFO<bUqCp]?x\@W@xpmIiOpc$2D> A8 pO\CM8%o)]O|aqרGت-R +W76a#czxe*X@EE@K5 ; ѨԐEG6kLW-'VLY,B~GYg?RE;5ޟF?|pmkʰj{G[on˯;OtZooo>Ce3c-}mW;/ZwK-pnݸqgź-W n1}*4Oj/hE/mb]A8e(ƟGb)Ɵbah'mL+T M/ǥ{<0Ѕ- bSI"1bkh0a\# Y8>BE~B^> 2~Ь" E,0pEF y? zPP7BO<.0#:>>0q G[>$UZFr ;7Rg2!THBHorcI)\I,keܑ6Х3j%Hr7S.yO#-4[i{컿p|ҩm[Lg^IgmuNjK,ٽ?k[nMyU;>۪e {n[Q[u旾%YT6o,ͱz7={E~ZD mG}6Mw1¾y" rJ퐲4 \%O"%8XfZ(-PЖL_{YܡzY|i#3AyV O^hu1o6{N۪[b-_z_hy _r;+58PE+f)UK ?O#p67$`ҝw)Ya {ad9olӥW+h0n?0AuiK毛p@Ex7FF$=_d 2D82'}&ȋLF|d#C ^a#"g;'y^OϾduʔ"d+T//%< 윰u 0 <D_Rf͞)֥K|L,S҃jRp.v#T@V'm#~(VVH`-[k-\햡>E'>Nb j~vXOk|WҮ\Җ>o֬uэvK{H<~Lʟ -fHڵsgj˨xi؞Yqi(':dʟj+OW)_Ϳ`gi C(޿PQTt0oT>7}у#V6_32d7IR|ꞗ3l+ 6¤~( :x0yKI>S#- B>c'-B \,3F,?\#72gWSX[n:RCxrGw}XSΌr%YWSκ@aḡ$f Zn]vmwV- TtZqv  [h޴M66gM7-\VmoK}C+v/}E+ڟj(zlGj7b/b6P?Oz̛K zR 9ldCUcP-w9 ƿd1t F@C< ,}I˗..i%-pk8d9AF" -@/0DF( `0'm./8 q#3">C]$OAt<@8/p6AAmLAa3H,%"6땢?ixtt*!0/+-ҥsʆ)ʏSQ r K h+T;̧?i'vܹ]oYg-''b[VZ3X/bLSKWU㏷lĩ__ROrg62JN1o1OPlLP~$S -Z)jo)Ih .S@pM.q4uVd.hn^^.6'~#H6 d(~0EZ<'~I,ȓH=-^[= O=2m+&k;K<[^i:T^j] 2;AzOC6`%TPC~Wm\g0*d䓿\|>ZUeܯ` G|Ap. y ! D,BtBAeb4=!3d&C<ւ/B\&+!(m7TRPOȈfT"Ɯ : kJTʹBmڹDBjrE$^lEmZ-OvޮNZkw޶*Ud0[>V .]B0'V6]?)E[6}U6xgcj5ֳVeͶq| #ͺ563<| _πQ>-x’x¤}t'q%Ȋ4:\FAd1du3NqM#. cb 7^h'=r° @8]~)M-\O,B҄oHDâL GLy\xH|5 +0PKu1ZKMW|곇B5qVֳ66EۉT`ֺuj-gg;9E#LL=+5׮m8wrWTӧ[oMK0Znm<:Ӌ?\1Hԁ[*m ءuhN.U6p@۲O%ӯ?Zʼf[핧6gl{u~]\IPy-Ǵ`_ϞmnYLeK-Vj@*YX&TF֭n} E:GJix߾`BA| 'T2yz]u}$^{}KQa-[48-ʟolI߯ b7ft6/ Ayw6VsۡceWؼsyfaW_v9i׶  }tЀ >y\fZ{Q!f˧#eM[忐>G!N$Xfi؄ }Q`]~YBO4!_OB5T~qs~-TYwf2a߾X+ @ªdRFvmi5b"CR-yJܓY+kE5_Ѽg役w.藷N7k'JF6Dڽzi6t6 Js0̚=>JWiG$/[O[!ym{衇lbx8b׿nzgCڎ;DIn_p_e=Pև86| jmfWyB9*\× # UY7V˟\Ѷ^Qe?|Re<ӣG;3mVD[7ߴ2&NR+dP[,vW{;okʪ*~gPJ9צ.g8Tqhr7Nk;IB'eYwX5{2w޹|~Ahd++3-ȈY|ɇ")?vM~n*iDP*?/bfo:Jtol+_?(oW?[NX?~yZ @E ^y[hێIB(BWo^͙eWQO[[P<È~\$ K<c!L#A p&$Tt@K.Bψ[ WKEg2. xp! 204.OOh),+x=# cY^@C"Ї3ʷ|̇ =. đ,Ή}}LSN??klwZ.]>T~;y-:ٿY hΩi.m3zqn]--5DoFG/FMZw93km Ehe[8quʤ5ǀRӿΟaV+w߽v]w{ְa.}^-bN?_%dHq#G~tI~_s5a醱GȠ&R|rn[>)wA*o&]'z2_ j n{\ߺI!PDa8L58?VG?^|Y V?r.a&HŒӀ|r7{}ꔬ9uQyV#)7Qȯ+-/Ɵ1 F ;iGbuO1OC/;lv{ʠVf#7pZhe67pyOq"N^ 7hߌ^K! ^-iGI2 !PQ(hIGF"3O4yd]zå  <-~ KgJSx&\rEW )KiҋiDdBf5FL-ŜMq|`]W'(M"o㮐kWB~q]ہ2UTjV"oƦ-`W%mresklJXĺ7[l}YY$-wO-G~Y B fj-i˞VkӢfWz'~ʮJo}~㶺Q Gc07Mc=iλoޚnÆ CմӵdīVzJtɁw+ @jծ:͖4~?8$_ofϙc>qG,_ntɤj~& [mUc^+md]g=o?/ɧʫG5|'5R~O=[O}˿`|ȍ@Ο0IG d@ȧ#ԪvI'Zv2.s`o݀\KVP[>Bfĸ+d1Iuߦ8[.vF75$9DFL՚WҁbOo 2G7^G7}4J+֤?42D5ENs[JP`"np( 7Ll%ӿ/'W -}&mk3jEhɓ't~qƙ6Jo vՕʏ %/I3lKZtfs'ؒKf?^֡ RC7KQVVkv^xх~K.xίO^s?yocncR{ d5=G? ~N1c; K?v*SC{t%('$'GCDy}oT_QhhEWg:+6@N?V֟PcV% jj 5+Uʱ)La.AM2ϧQ' =ixҁ8؂"p=o O~#QѵІP"B <2q p CZZ;2(x0߈'-ij[B՞n}S;؍7\/{ڱ2?#ȏO"mvpt.BtxBux~eey&?pnzIPgy_Vo֢F<>Z~򪋌` +.-P j2xQ42$-w|l0J]H+瞶}LyÖO?M9Wi\o3?p;]^k?Ωsץ E-@~VzT}O:"V*Cܷmq\ڄb{Pƺ[sQ !V%klxn*qgKy4RnF"㖛ey^ۏ/2E!4 Lq1j\?_ߵTn2BS?E WVRv ||uȊ&?w> +V2*{77uz i5DV h@"KK# ~l2A@@Q4 '=8lKd^a@x'Q#GӔSJ>&9 WۄG e*LJ28!Nw~qtީ+Uhvfg~V><6嵐?ٹm9loY+klUE2 T˘FO_WEjJm ]a _\d[fwhW%X?/"\dSJA%[jeck{~ֲۙhU:;lwJ(^|_z;fX .ܞ!?[.? Ǐ>mңR=t[x[fnj|n {k15F?޺u9oGt%Ր /%DsYlgIŨG%u[J?o=Z<:3ŖOd8K T~|7;'KR$G8t.Q凘[EwOSV}/ʰ?r_Qúuˁ'Tp+!P۩^^OtE)gG,euC62|N jzZx'|_8u;aV? ]!?ut n ,ڟ7Q6NSG;/_zO1_?s4:E1it n?BizhR R$I!_u;٥_݅O0aEOSiWE<?GzxLޒ,ŶC8牼y # #oASgR/A/4~ҒY\ VDvZ^?}q2BmQSNVaJ+*l/RO>kZ.8C]O?]k/}(CާN9d/Zd$&w kq!*w}{xŖ]^}˧% Z&=-]̎S1zj'j!+KeP?Vjڥ]7I[Fz/_W?7;\4o2WB }$6-ѥooz4cƯG&O8*x$>xbBy>'?#m ؎yGDZiB->eד6D63ȲPVȹ)Hm CuNd~ϦHN(MYC^nީuﳝʺvזGLKU1 T/sV.Qm چ8g [K ؆goM8_>߯7E*c;7ove-u֫wOo7^ngq%U{~Nu? [7fˏ:~ٲ%vǕ+:iɠjt3Ćn]xWn|.Re~WxN7^rD~( +ΈȺr~Ozݢ9?5,z;7s;-* k0Dgw/cWa;V)W^a[S)j쮻OL17|\n\Vİ%^x͝;/.Oh?f|/;3j0*ڲUt$(cODmİ% ?w(oۮh{FPԲ+?1U\P ;/_԰O}V?;2R|i+(PTK:D1C%äV~ŵdT \|1p郸6.! WAe x*2~\7 AcCdpxВEZ¡x .t^F[>#)3Ԅo}Z ,^*Y9_q'>Kφ+YKR ؕ֫[+b٧_,B[YG+j-5+jlV=m.*VU^Jay_ώ:h'쁇9ߋs&#6V$*?1`뭷dj1)[CC7N9ٍnjhdzM+Դ㏫%t|ΖOўtIP+g6DYk+>}PAIƝ[2G(y-\b:Bk[L@jL?zg6ҟ#z|(Y83ŸeǷ`*~m=L+ۙ2-Jve;3<>_~}:OYJN9J?Jϕ]獺@U&[<&*XEf6k%ge?_f[.^b *2r(A3d{ږXjǔOǠp\s]:;Wm[~y饗:?7%p5o)޿Y_SWƟ11hl|2q  i[OJ.r!*%ta3"C\ i=M.h ?4#]ŋA[B E0JQ¢0%8 8Vff4<|b,C}^G".aC̐H]J7 N~U_?03K NHzMh!YW2'cfΞ. 3'E]Z(䯾C#Q7kߥ ضvT]LiiZ<.'1mV-S5Uܧ"{%]J ?D袋K[>W?7!Ǡ4kƖϷԳ ] ;a `Lz0x5x4Z<`Xt/&=#ڈO`m /,^<_2>DGq-P7GŠ&x+B4\x=jȾkS8>-NnVa@x'P.:^xzyLCij-3gtj˧fP5 kU۠-[i6߱JрmW||.!j]HPSv'Cbyؔi+?ag|^\ . әhOٕ: `ͯJt}j[X-O_\+J;=VYКNd@"PVc5o&ڮ{isߗ,.0J5>ðm… ."bwt' 9zIʫ@; ϊ-5Oa 0fΕJ59D@wm=.˒竮9-дS\vC5#\WUU-#D{W<0:a٪KǻK hK' ^%Zv1,0!1n7@j׾oWs*(^N[[[/>hj ~XT/iP/|Vf *-k3;_[> du 5-7}-{hA %yC#^^H-qOg?pᏇxl<4@4TFCk`FB3G`'%b<> ^F/BF('רQ;Y[>gv= iR~1I*aˇqUp&%]EF \J.;-/V5RB GSy8@u)J:2>*O<چ lc]6zZFJ[HҶ*=+JjTVK*bmyz-XTi۷W\n7&iUc텻y}KVi1WЫ/@IDAT[mVxw *y<[4s}V9snlVG_xɧ܀3|p2Vv9s9l:|;ēme-$m]d'l|:Μvr^+:tq~Ԫo}[=>e˗lcu^[ NZ /19]|V?+d{onݷlR7u\[=Wyw8_=FCo觭gJG=]>!kn?w}UO=bmd?o|R٫Ʊ(2;t젺;޷&1:ƞq7K+ނ4Յ| Yt2n K/?Kr O[vCR;9lIo '~fԊSO=U8^C{'oz϶j'G?jog(_-y*xEs O1㯺ofia19|?tN1ճ륩޿>c]I6]5&1Ym@@Gq\ȉG:\e. q؛KH/µ"B#$"S@<#>p2|./:6 aa#y:QƱBDSX1L0ګM#5q(͈IR7O~}0P OZ<_A3g.]`u/~-YL~tPکڎ7oeZQڢj!m^[;e+u˧.%f7ym{W6hnao+[&~ߢy _W%]yvזE {w_Uoѣmn=zOnoFb~EåaoFfM}F3ntS/_?Z_2r2_ԱYif}z6yOzh87p(4PO z8 /a6 \x npCR<|!g#aX[>qIƙCҪ÷PmLC ERfn"215&3=K m8'|\pWoX4qgw#PGv:vj: 5JSbٞ|y^AD3RlOcǎ*@?>bZ5ڵ/z0nrPnH7J;u o]1d`].0h (oQ_5/_z>,4qd=g柅{hLӋGӋ/)_- C=zj6fbp3TIn }Bek'M-_^7750En> F[#d8! gumI {ˆvaQO^BWo*yOzݔ濛w<GԂ2 Ujo)?} [ƫ2I顉0n4t-8xmBxl>C]7E l, A10DB.tEaĊ|H-i=8e"7 gEp!.Q#GceB13$Jd5l ;Mѡ5:Is]𔉰 9N[c]vWM)?MH{Ptu W_@ХC}lvJI\Js#R @?NB̘[B;>l؀S:wla klMfͫ^hiHS#P#\ZDD?߬y3{37PקNŋuPEOZ.AVzdF̩o%G+_5}MZ62/'Ӊ;p/ڟXbo1gx̟7~)gcMaI%0t߿sМ2Ba" Ʋˀ+X#=@9<=Q^VֲZR$ipd\)(ƭ>FA/<(Tđ&q'h"+dD\I`<4wI{ )cIC'hۄTLmL̈FX c>h0ySHA|f̜;ˠ1oogJTZ:euZԿ׵dj4[S:[Vk'Q.Cp56P]XMY#_O_5J_JiƎoCO8ʠF{o;򠘟ClAlҝI(g\l? apyRW.Ӏ#=}3JNG\|.;"MgEcnH6L eBdt' 41v8dF< H ;ϥ $@H!)RP `ҤH  T)* ; ĿI)};.w!9$ﷻ3[fwߛB.[>i̘1ΐCC!u6(9pTPeG9O5 ='OA- /F,?\|-T|ڋ)hTK勀BcRتJ1"!MA -"UZ_QAUpҏk%}K@&e?݄To[b|ǟTTFzj{*z7?yǻQуWֿàB G{!*,j4pha\6xcぞ0[4A ~I=SUcOGF0^٠ xQC(8@ބ#_z8Ê7÷ΡC[d3F[>^W7f@&1|x%u1$ (49?TH[>uZghWVOrsW`R \mZj#R׍JC6W#8K9jiw+V[/(rWeJTET-1T*=_*e/?yeH0VH^ټJR;mPSѽJ2!tߨ#'ы#ˣ/ʊ/5Pi;+C`_"- igХF~Q#$1naO)BQ+,;i"q-ykȹk!`<;vLADbW4X1_SxIтtRPޢ&O$ 5U Vi_nŊJOrZTK[O0Mojꬨ~An+vI=?uU '?ya_ƖC&OG~X5?ҖϴBoQYgM[> 0TxKAip54AAƵ2d'lD򺟸O+A@:~,"2 E**'2 e? zܨ(8nغ0xn%-|pa#/]ٛTilʱ+BH#eGoSȑ}[Z$1)ҲNdIo4RKwY~?VEcPs[iNoʅE'ZKnW<`?zVQ8PIe޿haZ/yO>nVou)ī[>U=HT3,.%9jxee"S؇DŽ&E`U6bv%Obܰ`# v 'CZtOx?Q6JT> ".p<C,aBe0 z䳝2OA\'ݺY;e)}P4ȻfG 윰cr 0E>(\X~HEWviʟssg/Ԛr_ǿx{)?xА?¨PXIТ+9_ig:duA4Y~G>I2eИSX7W]/E2x,Q}%\PӶrx(Wd^igPӥ C.嶄Bp?_{yIzx<֞x<'?yceP޿MեR*6 ^#|u) j,45j8 a"ᗷb C"X2HǏH /ܰ@A \0.xY&-l9ipeO<e?tB&aROA|i#46 1c9JV5̙E[+Bq fJ5(>x[3H_mDB. Ѥh4U~j呏XU#k\iTkg/?ńPzAzEi+ϿyMG]?GGyү9L6e3Fs/:QFO. )o$cpׄ᠉te"΂%m-+\ІOeCeEYHY00tQa 0xOēY~# k'zZ?ʷz&8䕟dP+h6{J`flXi# d#MORJ=xGHnO".rB6"-# mMކM2md"2čSGAÏHG\#ah GA u/.2 L^__!B jc_7Hj[#o4)E717dR5+h7IDzYs)Х| PWSߋ|Z),\YrcMTomY<'?yOyr?cyo^~M-򉿜= ZbR%Zw{Oc YĐ]Wqā'&.Åxb ?ь0y!0.|Jr}@# $n*Da,0O|d ?. I8AS ,Y9!4A?Ű| ژc6kn H瞐-DS![y!WN>ZiDߵsWQ&(4ՆH87U{/Ok6g֭.)#k'[K檁&7-W/سKۂVZenf؟Ϳy{?yozO?G߿O=zjiZ~CC? pSDq~ 0S6_hcY#|Q:wˆ9|?65P>O8pa[t4eRߚ$?@ds(\ԳK -qDZF;BpMO[>3@hAZ~7;>ݦ>ծm6T4s? 3Kѿ\ͩǿ< G580ip|h1pY~dEҁ# tj2-d*.68> </060-HymQ\p !h%NBhd. : (X9 }tЀ >y\fZ=dоӑ/^mJXkBჰR| ^0! 'Mq]jg$$:y6IJnY&KhY~/nY~ U~Z[GU/ek|uOrϕ\GZZZr/;ߞ;= Z6MտI_z_?t/T\"'=dPKAigsP Fu p<8'CЅuoAOpВ|t<q(/2 Etd)2ASL֥' ?\ 8. ~e?;uZ_V ;3bfʌUi +^)<6 $ )51½TD$mCs'ԔôB.q~U>F`Y'(P qj,:zsj[_v;0k߾-ЖAuNںK'ZVPy[ƵS߱A75֔mb{eH[}Om%vkxPn[|t:=Eo㟪:nD͸ Ґg帠yD)`@đ!!!,2tcrڠ L#ZiH_xF8V .p!C|Kc9T ZsdP Á(܃ R/^S| jv҈teK~_UiӦmgxmOz_.Z]ةs$ס?M{ߚ4>'uq-^0~0}C=&YӸ/ݑ tѵf/?<7?d M>?{+~5PgqVM5f&c~a#L\‚LK+) Z ' OBG[Pt.x\ KL>aD DBCB( σ81h$ iQ h|p?xC@8vF~#@9\Kgx)Z%6r";PdT?S<1< KIg91,I"#2ȃ/0W2h:gm6{,{lUɚ/??gs=Zk_Va ͷϳtБehZR'x +~?z?\_~ki )|RQl' Ta4L5~&0pA0]?6Ebtrt.B[ZR~7  ㏷[#/I ٰ m. ԩoW*ǕTl!k%K?ﶷޚXdD}oؠA<//ΝJa&~njٙgXfobW쨣Bp1B5h+{|9 C=OUU$_pisYۅn-_ZTK*fE?WR@#OSIzZY$_pbPeο scKDV1W'ǟ3}tkgP6e4viv{?)Au RV* j(dRaL 8@qD~>Y8h/ ipBfRzBdaxy10AHO!eЀ\">҇+ hy!Ǝ[i]e7b<^KaFo ю5c82eqBB':8MlLA"!?aS^ZuBVUZ4e/·j3g̴a'O~6p#m1^>8ڿnڗ뉓{وuo~BiE\+y _Qp)ŋ~Ϟ}J jiGu2ߊ*fyM7:R_sw%Xw vE좋/g~چ ffsɥ?7 @{m{giEy{~Bƿ~NCLKtyo/̍Bɖ)+\*RDymh\Yr,/0&3ǘn[ʺ yMuP5^[s_=\1Oegѽf5dHZjwPjF;z.0aZ'@@⁈6'>+eVd' -‘?O  iy z2z JrbJ$'#G"mdldрQL˛CRZ E>S\~׮,Y`i[۴.kvV[m5wc>~ᶺneEK$CL帯ںl-s;j-%'kO/7ѿ\%fm줓N"!4\YfYgi&Mq?OmTcܠ6}?V2^:bӫU'p'ʠv߽Ȟ~7m֎/d=?sϽltPeP痿ЄWWϰN۪*I.xex;K=\,&(uK?˧usYB&\5'3'XyQ'QyXt}۳| 񧿶|bȑnBW '~*xO9C#-4x\6؈ą?lGCuyuІr!X?*N^0in/yX|py?ýk~Cش1Z9,Vd]"ON=DS)U!PP@ ?ݤ'Or^]u犒H)ϲY>5Wm/%j[2>d?UN~C䒶^P͛;nv׿c=> }vEm mLhN?Ͷb\3f0.exl5:V[mioIp_? t|1P*Nq+{oBO'?I;oͽ~ӟ.ǟpmv _dOi_mɦgm#7^zF/Sy^Zh yZ5W$g9 ?IR~9;gI27O/H@jWT"_Qi+4:_zN7?AGo8_8-@i']! \ ex1/(8h4tAOx>^ez.a/*>8Y @Qj>lUh[ֶm+9|STW︃Zg&ڿ2 V?߮={˯Z/oN>̟gߘiӊпWSkNLgv0l{=lO>i\oH5-V-뮽Zتg6I6ҷȣvֺ;Cl6݄\g=#ou[oi;8!՗C~ uդo;oIBgy oi Ϗ,{ok\[f͚i3){٧v.֖ϧeP?`C[o(s̙S:wj}t3п^'¶N-Qѣp|v]>яZ ״7?6kd㍝??O=QYR=U=RXa7x#QIH5;ڏ~tipƍUjͩo_寎yOGy–eqOR/yyIy_/[>y-+ۿ@J+&[>QF0v3 Qe|- aO.&Z1ipO~{?+Dd VvCXG!#>*28lߌOeT|{=rH]% 2iAjc^xQ|Ǫ``&cc=Q"CDI_9#;m|(!Ҕɞ-E%?U{6:YVu[Ym߲gyF[.)jiu\o[bP6rj/fʘwͻڊ.5[it~:KN?&Lx%oq /c$VaPTc9Ʀ{؅^h/`:?\;sxVLy ֫zve/ޱ߶)oOK/zIL'|Jik~c 1jmCU;w,7s {KtKpuw.OcmLHDƹa;ﲋ&1*qp7nV],#,m=%S2B;qm#[АlUנ]A5ް0 ^ňDF Cc]EP 7K㏼¯LG: h#>a%As,,*"2"ed|Ti @0<(m4+\*稁& C÷Kc:aBqy⃊0 p@ĖIE:Oę@ijI'sWm\S^YyD~i@Jk7#;d?6{O뷡fuۿ凥Tv ml}(gMCNL.q\u1;o '׊yI xJw-` Wo*嗻Cbƍogy?mA\ԿO+>la}tim/eVc|SOaN8dVSsSߐC_^z?V+_}W)?uTGsZ7|pOp-[> Ֆϧ3ԶIu"FR'߆pY#2WտK:CM:Oγ[{x` 42[&e @O8`+' x}@70'-Q6 m:thbhgIo {pCPVt,ѿ48"PH!/bIISJ2sq)"/l9xe]x82)SSO?i]Tgc=Au@z_kuSg4˄m ,JlN+jOnlvWj?.³qC6^gENQ "֢%I"pzq= d1CD2RR:dMju]k_ą,[bj2:Rdr- }ߥȠֻ _KF}馛o2l24ISߙj" l⤷*SCqpuש?3Uvbk:kƞ~ibtsm1G4nvny"|E-Oى8b8{aq]JpکxQiJy!3~K;4sV_t4AwP!7xnrOU޻h{,ʏȓN+ӳ`(=GUiCyouǏwjorFeo9޿Qԇclхu&D!iSY={_QŲ5@g&s.1!>#7AO*uj#-#qA . q؛KTq.q#^K!HLAp) 8y+>BW@mDN00ȑ2oe+1j#4,+m+ )uXd-ⓣ_GBx嶲 Ձ56e$ߥsW1Cn]ηO~,ӥjS<*?Mmv~5SN9gv-#ײ?>4ӞyZܧJuX/Yf͝e[l۱]zw_' ڢmgxu¬ތ=sY?Zwn]uqul?m ]F yk{gm3me-?ٷ3_ë[7d(і›~by 7K Y~X߅@IDAT)chy˿o> N:Y[רG;c*y߱^z(Yg:׭n6̄xwjnVk㫻2p~̙Fg'v]#g%+k^ܔ  wZƖϢ_,7a2t.)UtewnjN];پ wX& S+_a[ I>/W7Omw7 Ϣh)ǿBT1_I"?:+Dyo 4^RODL__@JߟӦNG3Yz 5#5_@\#GS!z0G@a`8 Z¤%Ȉ| |g n~a~@oq &u#auQ)d" IQp\Ґ!\eDze:#|0n'CiKԢdrDW Hߌ9D$A+)L,:yLw] bGFpQ=O97/G>% Y>AKո[]zm;N7*.#ZcN;}mn ƻ߶>XF6>)i+R˶`Xƹ^ھnfLȠ~ͤڊe2Ru/8YԎ7+9+ԊSCeRE)yVq]`Y_|-*o_Q=6Ən_`;`]v[3Ԋ[?_~>TOmk_Sޱ뮽V[h[m1ګ*zک@ȧ1^zћRUcǎv)O:{Y]2%KRZ-[>BaԶ=Tg[VT/?s1Do^+.1.H9;Gi[gߗ_qV7Hf{}mOG=u]K-B8N_W9O9gvW.ܤb&-FV{eICh\҇ThC{d17N$UӤ"yytdP+z_ڬ9_SiZp/ .%@4vW@K\O4Ӑck0G}?".Hr 2fM) TRdVފq ?TBX# q˫'F G>Gڻ o}^lOaDRpf{RZbm3Ub\/%2f"3 %0 )^nBBiS哶RzXSk|,?PmYvmtm/Ti6{_3zw_muS%t|{y {GTw+ngK[naoQUNBnm 1sSreZtɪ6pH;Hhn_׹rx&2$Aj?_BRRhhW$Y~J,gio`_z;.?mT;hHЖkK :(/Zh#L8 kvmW))9lI6Tm\O[_Aʉ}k*vagAwj|`L4Ohu\r<.;WoVw~F?$#IxToYM?tO)d[Z|o\fֿWeq<.bnu4 D1 $oTOt1.v?}- {w-{X`A2rLǎ/ )W ghC /l5H2 ⃧NO8xE ~@Dʂ_dz" B0遨Lx`$H=4#.<.?CM1cƊXxNXH'Dk!.D)h^F"'%~"?i߭n8\]KvTXZtYQt-oS/Wz~ֆ3Mo*w;j͚#qٵW M~th?;uS6Τeuֱ7vs:Sw2'L[ vyF:t2̗1d{=<.muղn_+X1GRxQGٜYIOzO3;D,q*wѮNH_P7r)._8!VC͊ZjQQTno֛ y'jdpQp+w~͘1)0m/Z?+Ԟyi;Qu6ZE TZɦ_ܽ2<-iIPQHO:kZꫯЀt{쾻( 7vq{6'#Q_|]pq衇*5::5I'u7 ?1Jh;_ֿ_+#?y̿3vҽn.C펿D#FK5t-8xmBxl>C]7E56Gƈ]d `!:0ybEY9]42h3\"oyCgYB23V()4E>2AWI\*K#Jֲ|굧 ?ӟ: ݳ5fXuOC"wo2M7=~rO:' L,ԋO,Z1 ])ÐvS: _eo]}knZ SٟV7_@ý:n^ m}Iԛ u[C!ϊx=6pv:RqWN$ob'Akn?t@>'7TaV*FA9M{q|(u6[%Z]xF<]z-h%IOC 4HgmimM]v!qĈ<lUTgϭu)0LaPtuQj[G}Kf:2]w֦M[qKwyrgMy!ݟz)joGnA[>;ul|}@Pc'fգDYu7yķ޲_yZ+~zNKI DBK|\ .j+tU(Ͽ/yVOy~_[t u<.OE-b"0c;y)*ڨTMt>B9J5Ek}}(2ڿ j*;=,mlF7lD.L`Ml!? ya ezp!iKHȠ0~җ(腇8҄Xh"+dD\E,@CށG&iOV)xKN0|H-’aQ 'WZQ o ;I]>[w??'tm_EgD͚5~Sno6]n_5p}o]mR[;up==fSNvqǹNѕ҅vͩep;o{E1wu9_at8H[ ȣlV]qnۤP~VO~Ʃa:>|o `-\t2?gJ[$YwӞt9{BO<ڕVv긞|֯_JGta8UGתn #-;+`Tm=~G^Eϭ;c/ݫj;[>~o/Z+B7|SfOt{lM7QwwoW]u&[9졇sfn:To͘5C+רTBن{x=Gx+ =ʾ+d-KYym24-?gsSY{OJNŽ'?iO~?SePCzX[2%濁0iڨJ.gEC.X]0q<1k 88gx'%/Ӄ8d ~H/WW AOr>QYQQǀH~QpIc.PCfC A ./ƌ 9w/Qdx+~"22jNBP48yYAmGWm=&[hTt~C?O;h/>{u!؜hB}$Wnduv~T7r^v٥%9@wuK:kCI@o[} _Hv?*R[>eQ%NӥoEw#S.2[D+/OI'mW_Q)>}7KUkZɠE*2Gl`d?7  bY#UuK>Sb˧PܵJR+B+Y~k5Ea;k[oP{|w6jN#?|dyV$Zʟ#;ڭΞ٭v‚Ev\~hU6OԶ~QSnvO<:l_D;|m_>BA۴i-e:k~7o\;A͘ץKWN6Qeȿd"_:B+dPԫ??{[A/mc;U﫹_e}[n(ˏÎ;ط :O~^Ӄuz링߰u~YZWԴB-|\V ԺiE1Dm֑|rf[{馛I%L)ժ‹lZF2A}ߟT7ZCO.rǟPdF{d?>K_56GcJkL-QRGUE4{B=|*(/ogC1Hg5r9Ԁ0z~DFGog H kQGZx88`_"- igХF~Q#$1naO)BQ+,;i"q-ykȹk!0y; MŌBYlދXX^G#)$,D#],*-8&&O$ 5U Vi# ŊqOQ[KFV7vǎ?^[{Wu5ԃ׵?5ˎdbֱa|S:xcKcm~gڵkg.H߯gNF=z۫^;w~kΫ?_x}16s f?A[P nQ}Q>Lv6v[[R~D-o|Z"׿ln+<fQ)}-ݬCtFZ2k,I?-ȏZvZ]u1P{e%;C|cKuE{K#.?KzKFS& UhGq}S4ycѪƝ*.g}Tֵ[W;uI>XyqtAA!/۶>2Rr6=/#w>. N+e-8ptm?Q|;5ZE [{mn'2a6Bu<>6-iZz)Vss)N€.l9d OR>ty a~". ?qWN x|E>"=4˄HLB`""2q'cA  7 `.|"L|/a!pҖO/PKET/E>Y>m2Лc9)DXbE 玴iuG.%V+Q ɧzm" ҼmFgwfϞi?mp"'Kkg/6?ZUP^^{WIҫJCֲ!+\+{vԂ6]C sآ"IEk[^Xܽ{w*w-̓lĉ@皹NUCO'Cv|+r~Vz>J8h'|?fϚmlWzoi-ѹxzQ^:2ͶW_ooG$ x{2?gč?Vjm=Sϗar:t?.W.h%K:Qzʯ6pZ߫^SKj_~l}B!Ν:Y>}lٳl«tn BElG>b{n|~rÍ+1 ?Y>%|yEA9d_^?x+dѫoTu,.%9jxf"Ka ;M؋Aƶ2q4:b8 m7h p@Ex7FF$}22D82x%>2LZhסLF1""?A|sBP >hx[7&!cǾ@#n%t `DStavNX:TrUB *0y$ߥK|L,S҃ZL)?x *+phLq!:Ӻ=$Uz[5خ:(]\זYla)52r|+_^$ɧl6h[c^hL7x{_>4M֫K,?0mn֯o_mF(gaƍWO}n\OÏ8/HBX_/~b؟(#QKɧ(uOeﮕz\JPzus=?.Zh{heWUg7씓et|$s:L뎿q{yzVr)[o/ҧUsKoǟ?k!XAjbW_5soT>y[*u6,ߟWiΫjH-.ar~yZhV/  - Vx{)?BА?¨PXIPe+ǗEDYvG~_7'i2eǸ>]uaPRhމ1ZkU.%>mj@a崘Bp_PjEZ?Czv-z|nLԩOfW3S|/*phrvvSl-?_B瓟?s읻GW. 9huʀ,տ mh+m RuKkffI['jGOvM-ՊlӔ_ZnkSEֿW^z:){%}-{]t(祵n߿/v7b'hNխ['_mO/<y~IEz,~k62L>k%j;9sc5=jqV)cC3]d3G(\)>H}YǨ|xh:.4H / ! '-C/0DFƔ `0'm,8 q#3"C]$O* ≃>/xpY2m;f3Yj3H;1%"3_"JGGj!Y f|"K)Bhb*?4SQ ru`gkY{6:G~_ڂ (ξw\8K[8k=[ٝ"z敯_ahj5V_þ*]mu@w圅__pè0qjF%ZGB?pݨ:'DU xh~uqM/˃2p-&ˬ•ǟ}NemZV7^7n=}}lkL;޶yzu!ŰO9kao4ԜgJ$L;KOPLy'0&\M.q4u% /+L~??y Me!eG&Ea 0xOē/4\_o'xZ?ʷz&8䕟dP+hnlxj o& Ew+@."}jrˇQʄɇ=\>KFKPkۄ+hXG }{b6vnzfM& 3;F7?h۵G2օt<9k磻ͱvݿU~ma?E/mOyTmxȟ:U7^|ч 0vcwzmzLu]\.1qD]PY_F?mk:[u)Ӻt7ۿא|rs,_5?'__>he?pY~\=~"/j7OזOzpWs?]V 5**Vba;(H}dĊE!?Gp[O9\!tx6&o@B62 FD⣠G^#.0L0yH|~!| 57}Q\ 0lُH3kH2},T %BMz.%!PWSߋ|Z)U]~{F'=`a/|?~ ggcw?I,ξK7;;p_~~"X1jΐ1oSZ oUk6mVu1Z]ֆZyg["5fQؖE(zխT7Rc5-8KT_kYr[UO?ѧDȌ41[TGK?ӦF[>[ ZbR%v}Ԩ c YĐgqٯO<#L\ć v~7eaB<a\~+8#GtITYa 2~d\pLMY&Fr#2CiUhMGOPAM?'ڑVq~+ bxƲ @<@(e?6 倏O\ؖ"xAMO|čՉ a'LQxe4I:p74ĕFzpa lTFГ>]Yw :"fʏ) s ds(\ԳK -qDD%YB?u=Iu˧pݴE|J_-_hzk{}>sU-)Λlczu3*P_o'ؓczo`[z6]+~?o"mEL!X~9g_nǿeyoKOx#Q]e3B}+1`PӿOV\+30}8pa?*_rO4I>aST-9DV!8lE |4 "sM!&0q2P oxqQp( ͨPq|!8!' k# 0(1r*t`p,)SBr^#Q t#QPàf:C-Ԗ_>ĨSYI^˿jgi뵥ꓶ쥗^賈:ۈ[C 54mMmrp'g3l k Z-6\<^u-5Y>xaYmvu*쳯}l-m.l8V<`dm9;M> ڂH[/ƆҖ<˶(@Y.8˄LA'C!42] Er > ^:hÅxx<.F3hxH-2th_HY?HF6P%HHZTUhu9A!8 а~{ߥ|FIɦKJnYY~Zߪ򋛧ljW!(T; :Kjn?zfF}w]3Ty4tV;cZvVӹlelx_3CX/mد;SwIc^~Qêsζ6DFv6P{~6{֬ _KgeʠXa54,2o#ϿMa_W?_OK?Y~|/^3m`{-nm!CC_tWuVO[L<؂! DF„5.||^fZt - P~/R E>#Q76JTD2ΐ9t\Hh 0Etez@KaY#IY"€:<>Vk\8e%-a=,/htz@\“L@g]J9)I]IXl}[3u=c&mg5 6Go }ڒm'kw.Xbcу2|w[ 駝^X?ezlPÆ m[{}bJzY6uځXn}6<`{Hٖt֬J5μ2RP<ӈ|'kgymfx"+76Wi\&U;'TBWG*jWVy/_aOxMyK zȠAx 5@|jaȁ ~7O<ʼn8y.ܠ.d$-CBK?2>xā L&$A ZґQDLM.2Y0@^".b&M4;ɠ6M~ ,q BB w ҫkv6Z6mʹ3نKp5~_o5=pvҹÇkچUVz*wW1; j۸~ 1 =u:wdsϷɓ&[74TQm/~qw;8XD-}Ν;[mR.r9swk\Y58~;?M|ߦBa jBmY_q޿AM#Gr \ᖍP؋x :\ -_5R1?.<"p :G>"KƁ |/@Af(`@đ!!!,2tcrڠ L#ZiH_xF8V .p!C|Kc9T 3QM>Be?{WQ iz Ihi SX&M@Ӌ*EAA}P=)R%zzsLr3s9gδ{Nܚ;uD`ݠ ɧȠw^iw`SV>W6]]̟kS_7Kby_ۡ6ʶ>}Zoa[O`KDzH~j\j Y߯ e4-W^y&Md՝tB/:"^S}J*F]ed ,ZZ\oH-u>Dӧ[]QFE2@tFyWl℉ڂ[)XGAYϞEl]2Dj iQ"{״:磻 np񶽶|R]3]PAs'ԱV+~Z=t⠁u^+6{lFߟo9wܷ>}C1aY<:t Ef?"=zH=؜Ņ2Q+?ƿNT.m7oƿkwnݺ٢lΐUO޺ϫ]M>l7>&L`櫽yxlS+*P=MyZZw*u_r_xǿ<3,0[߿4w֬jEg\?\gqV% j 8U%a#NZK+Q'\ \p‘x4q1~ZIn7Pf !8vp(HҢ@!|pÅэpNF:yq4`!O}X5bv2p${PwaP+V 80g3IK͒G"dd\s~IhNSzi˧{.son oj͙ 񣶦Zk-ҭ.%joX?n]BXjkO|r7)Ai,ϿP[>l5|F_g;3߾| k׮^;ލ&_ngW]yB5Z7˾qQë(doYgj߇NgE[hӟ&Z{mh8'8QW_}-Ur'zV8l7饶C? ?ER-?tV(~Tjl%5\w <t3IiӦ۶m~bvvئlZ{Φɘˮ%K׾5OWQn_=Tפ/4Ѯ?6СN=TV[버vrN-ȀywtVʅo9ٮo*:?Z0)>Eso+5>xpOSǙߘyGzBԚǿmNA3 j*zuIZ|Ү2v$[Ohƙj8`->'q06SE^ x,[#CA X[˸C0pЋ ( G<՜8F(1p#?r8xLa@ #\x{PTQy j1F"9J -U1me29Mfn;,պYeykیlζ:5Z֣vk.gg[m`ݣ cɳۭ6jkɐѠ\WO>Kk5.c>j]vObPPy)ʷQzi{3w[`}hq24_8N=ZCzg;SZvu?jbP=CE?vl{9vqCsyVgl^fy[YO>es7J/~R_OZM]}fex p U^?5rXqf[6FsѼGMNq:%kk&ZGN-| 5eo}㛺A_ʴf^{euA d 'Cz:z3@˳n"e˯j'lH82W !OрQL2ʗ*Ძ 80L{@5)d9Ѻw`nYk{Ol5UKԄe4jZs5Kd]ZԖSk?͞QjFcOG^H__wu|1,Y`KV]p2 ѶWZVY}YKmϢuЙr\sZv̱VV0(}ߔ޴O=mZ:uE` x{cG~g=<{5iY}Ã2r0R8"FYwr9uT\p 2T@'rZeZ2dӵBM7^q-'t~[;#4ArtW^}2`hRQFݵVs=sS#$U ZdPOVz!ua'wMZ:ORb 2?,#J/Z#Yo??TZ©#oLgJh_s[3_fYf);_Co1cƼ$q3|'[aaW bDx)p*O9ED^Аt|A:$0Ʋ_! .>ȁZl#B$:K=_ _ߓ\88PCRRQg iS"NԔ)@oy!/u\^b[ozK 6n]ۂ7fkαYmyf-tFjZQ-[͙:wuo_[_ᓺ#CF>V>ۧė^R8z:7;nw֭]O<O+e Ɋ{fD.ghhYjz.MGF!Y*̇<,_ǟ )Vhikopm-bTC 0 qz%>q)&x4HP hD?2(jZoiVسu6X떴ݏаDƑָt-Wo0A76X]Z{u.-^}';ysiZv_I6:\%9jo" 9TȥFn<7s>'LxQ熝{s׋/ӊ=r;C(^o,cA!mmv=I'?+v?_f;Sl[N_ǟ^tZQy46ÓV3կ7hav'9o}q/> ǟF|8sVy ǖzvslS)(_uA?QwODw<6Vޱ3WPCyD"]w8^?Wƿ. *O=!wr?),_Y"W_,2B姬Ly3Gt?1\nǟ<G1?4Omd-oW!7:t1VMWyVN32x͈8i'h48p‘OU w N!"9\(@K`RZsXQ`!/HyIc4lߌ,'2)˂A|/> R!=9y󍿊)XLbc IBOLBqo -d>cNq+|,R6:l2euefZuֲZ]4tEȘ&-`1m\צ.̳)/v)3x'[J cHk/_mw./_hEOohjMpc9wUWR^ՖE {t{\`V;:z9|cVi )h+2v]w{~_.%PPM%Ó@g1uQw+)ܔ4{8#uv_Oi*LB[mi ]u(UE(~=Q~([UK!VBmg%>0-l>b-ֹyp0J[tݐX tٸh⋗ɟlG(=hom/?]̿yGW?E/5ǟhOǿyΆr[O#S峲sLlRKaCemnP.Z0Nă 01R(IN08uz0^ 8,>ze<7tdh+3 EA+,E $<q4eap4 Z FNȀ G߯Əw*|!JW! Q'0E8;T{+"* PZD$a:ߜ{˿(r?BcV.gݡ:TɠVp56j[4m\lKd8UoKbmy2~V=h_IkN]ul[j[guVnz|!{;^;/{/YN&߮Zq 5ȠvVH7\-Tì7j[u7οK(W~ S\4WtlrJR') qj=lȅd!`EyŶvݬφ[XnuTm\m*V峡QP[GA[9f3t97?` HkmfK^\/\6+ Ҿ+nm~9ޜt|vWѬ ,r>O!P}=&v."@3fLjŸE ;츃}Gߠ6k wnxnXg'[>qN׶q[>Y jvn}s~2[ \#dVS lnk݊!72Dx'?:Vmqsڍȕ?tώhgByGV.K RosR^I=!` ˆAZdPS?SK.vy[ ,J*m $O6=,m͜1;Yn'uA 3^A {.\9LU/X=pn XBG>Ҽ0aoBP/#]e;`| A~|TB(Iǁ4#=` V:W鸐mO80!{62jgCHk^kLlxINӯOJ##!G:yճSҡ!9['Omut.ΠZT:O)gӻ7t{]J:5̕QmV-T[BbLC\Ld lo,̲:Gv>ƌvMj@3_ͣnnTKbWҗh_G++_/C_t˦|&d~=+_UWj?t> 53u&ۄ*yiXE΅*\.zzP͝gsf϶^<7?u]M%W믷?@w‰'K DhQH Q O[F`9>#6L+ю?=wC w|]J0g dS]p# $sYD񨣾a;Lڤ;ƿ;@+nc#+B?/|i6%ۅ]lO=Bz;o͞5ǿO;]|B-\&~#6,^ ڡPp'2]?a8wqgj?AMOJ>B<|Q~eD(yUQeOzIQFq'?y>?_3gbfq7}%'F*7? |n3v2)9a0S fa&Cq`KZ!B'/86 'p"tÑqyE?2 JA( (A# 0\'\40?`'t^#0ur jQ2bB9P MzJahќˣKNA-WB.iDl&LW)jb'* tCtB>ڣmF}{ è0_EZs4S}RΖ[>Ӗ5峏k=Lv_V[?t0\:e=kx Y: xۛi2¼ v2i5:SOohK;g.l?W?>YsvGNF/?ߵ?z 5旿=Vס 6L㨋/ꂃ $B:ivDӷqLCtS>1ax;3ma\} l7x=c֩Z[A}zꥑQ[:ϰOVR7O/oVr"8~HK!"#0nіO}tFP)=$)贅qƉ^߶wRA'[wӏYޝsWeE8M6T*M[>^lٰj;jՑ 7 q«;:o}ʷ[ʺhG.߅oeP{44ֹk;oZm#oϴ>*2jbh7- 3ZP٣wߢ%nyED s%߱~t;:WKy_g&N܋/`NIemZfv٥N8DPjʜn`:SL<f|CYgmk3N].\VIͳk.* WXi[Fy'1*g~Pp.%`j]tq#dܠUry+?(JϿ OzkټM2g_O|y)L8d#OxaLI{7`@?/oٖ=ކ #;PpF^B [ 4p< ҃OyNy8qLh`$Ñ|p ă>.x8 Y<4}O FV;svV >ėmu!ΠֵBꜯJ/BGP!]xJ*nԖL[bSmdd۪=7i]hʮύz}C*Krh?7ݬR~x=AvM+;ҥ n(JNt?Fh?qJ+ :l*Y}vM?~Q[oCc+ ]gy nx!I/?o淾eGmgzډOmhk桂<؎=8?9ގډ1)VՃ[nuՏ7Yb_öao_c;C;C?/p[-x?C U<O:Iv[[8L~mv G<&wi';?ng}Tlbȭklǎ푇~.5ѿK 7FڔF̫hrOQF߿^hu2C~M+jM*J+l*NW?Y5ǰjDv/H !Wϧ82<~|F`8`q47h 86|N9MIv F-H# ^pQ iQ<<"\ B[u@- :ZdJh;NKuQc-{΄$<̦.'jZ_TLI61.zZ߾ݣ\ Zլ9j6`VG4ힲR]Bxg9땅65&#;Zߊn`,1ӬLUhumCloo駧il#_eZ„ t[gɵVdu׵5mK k.SY챗_}^8Q)2H8Umhz!|_7xUNO]] 0_+F6h[v4q?_HD $W\J|4!q ?xyx|N,ym-wJeNE XIr̿mnMvᐵ[O5M"dL[*cZ\}[>_8_2 bF?lD.h"?±x&"_QF:t 16'GPJx0nEi8輩={ʠ&JU?vR|޶۴c-[.(涌W9?'0^b<7~:oEr4J_e'N'>uI췃_Z^.~E6q+l/wRWf뮽V+ZROZG]&}GN>q[_<5_.6_yko/Ie?<2_Gbg` 5P;eIjE%[P.pE4|:pZJ#?NnF 4a s i$C~Ƒy +xU\'= EbE†Oq4xF:8`! r\`&OȆϖAPb LdM[_ jxCRf1aL0jUoj'aP rw5OoZ̿i7h;U8;׏*>Elݻbn\vnUYO 묬Ep^͕m)sޘE6}MWz'Zop$"UqҏcxpyTq@^`u6Iǟ.ϖ{hkx;̳%}{;x{3oKmaܻV6轟˟?e<% 'y__cpqN|jCҿƩaC#s옱/+|X<>"q|>hq|hA>A3+&O ,ۥ/eS !i' x4x8WN8 a6KA=^~XF~i6jV5mtGPBbVPZp`iGڤ|_ayN mj=uZUſ´R~WդpEo}}ifxW A/Lԫhp=WMUxnV&CZ9iAE̿(j_# dC4%?_{= ڄW\?v(-In-+pַO_O?ovw$Jֿ@W._DggG;YCo455g ?r/?<3tϚJ㟄&:C￱cLA F/"̌q|&3Eg5a zFx+"Fv!-Eޠx)uAh(M0~0a@q)! BQ+,A8ÇvXx',p-V玑Fx6~53 !` z/Re*Z?&Ϫ!><d!$؈ ̅ڦLpl`neOr4N}eTUj7`klu6wUkkwSܴZ6yR{E2 V~HKkdv B{2ݻٳuӥnlr__ס Xϸ m.&@1 Gk6e5bK-0S/㥚OoK#ͽwuοigZ󍸒.% MgMR[>  <06Xl0p7kqЃi,MI y_9!'yUA| q˛" ^+c}eC&ԭK% /WFgRj_2EԿDVؼ }bQޒ@oOXnHe(ko\Y\?nn\/zTE,4D_On_<5U}7̜K T~㭶u߫tCرc&*00t +iSD|Yfa,#|p|$yqA ?1 H'/.8<`.iR6AQ2(laЅ/xg' t G̏x7⁃Am0ƏVhfoBaNu;I$`rYkfaWfRf 2 ]J?a0@0x^Z$T >7RE7:&FO/\?9ecQ?WG~xҖOg3VW]+j`gxpb>62‘'> RG$E8GC/c) \&_%x)غ#C[]7B@%0"iІ<ˏ0ԅzqqd7r 57^@i`r~sgJeڍn,0xU'ݭR{ҖOfYbsLK?%Mdymѥ/k:⢡v_RS(U\no*۬E5rkf;ye5^p:J*ʹU\ǟ7gLm-s'pW?p&-[>a ?gH! P9HN:.GH<ɋ&oF3B:q|J~#dn !N̄8."LzE^$)ㅂ)hV=3x'C:ւ.۵Bm|7~2frC9< IQK-WrĦVV{-,ƿZI˟S+ZYo_yY7y}{yO8b߳fBUmnN[ߟÆ sSDq0Q /2a-0m)?)I_ e"5K f0'NQxey&cVc`COH'|0x:jԨ`ɥ5Aa_#+ !Fb>η*/\<.&O-jb5gYꌹ_o<<7Ͽy3[g}?m3s²_Qsa!ppF~9 ><<2Ͳ-0`\B0G`BB|G"+WA0|>F3pxp\}Q@f˧e Y6GqzQSǕ?t``@2aA0BCzοWom,^.Ѯ ]DUbeimk0 =dx^ myymeO5^(r߿g~d(A~SU6@|NCktWnz'-x" #Z„!|c!·cSI4p~؄R, Pi2DsG[.$8a"@, a!08Op),+p|Wd-\ǃ 0gUom C\0\ XOozrXjz5ȋ `'S=S3&*⿦^߽fmZ[k#JZO;Xwh 6L6qi=[`OI,\4"\ndy/s}֩v4OxRywäǟ7}GBhfRRG(=\-qZ cW!&|I8 ?p  ;-hC%02<8xpE|.,QQ(pɇ0 B!LN/lOG:)mx0O##^Mg jи]g(i*3Kz-qnӫG/B,ȁhMJ U*2y(zAM-ҁ4eHEWmgwnW]֪.Cyq@IDATG/.ghZ[@4zMѷF<߿g~kt4j[ynopԔ8fIRWwj1p_6Ba/NxG#< xķ44NB#88ne6C>ǃ#+#GmqU%T2|YE!9aX݉)N Bpkk:bvHSdPF޽?Y]t_v3H^Ui_Xc:inm-j[AgֹkWܥ͜>Μes&i3=w\_F[jO85I֔;~0Z8/k/x<-]˴iс^ XxRwhzX?Q蠝-r{s29\f뿓ε6T-c/ísNh"{獛Z{*ep,sGKƳ&Zn2-ƌ.l2]9k=Pjk?i s]4^0gDY_4Xuyo^PFv%}4֦Nf`go17 5_6vDuaKC [4+¤]H7&=C+e Vx&ܰE~xB2-ᐃx8-e"-"B&/c % (X I aC$w#t49 %/? r/ic~mVO)EqAբh@ pɏ⦴HOGF1M@R`ZHUV3Awk.GQk5ڰu6c't)]vVC==zJbJu-C˪(̙7޴]vGNgڕ\[rVF IvbA?ѼD`dYR&r/?5_7k _[>55T[>u5f̘$|Ɗ4at<x|FFx)^#C.hb!\UI9H'=Faiۑ''dx~o1`R.TBp Ap\pҽ8a*x{Qaq#z) }| p1>0\#=\kX%JV^ 4nB6<U[>3wW_VRJ#7|uViۧ-IV Żdɐ1&KxI .^WwN;ٔS슫UMoomvچoy:}BZĎ]o _9ۿYg.8Wȇ1ѣGaZ/?ewjIa+I/!jm7'駞f+:;r;-~lIvQGN;}D4]xs).=j;mo681X?7\<|:/+׼\g9)9~ǿǖO޿h[?xOoV+VMR2| yV0D0/lFyH+?AbÖN]x#9\hr]0X.BR308q<#/i"ÑFQ0X;Z#x8g_U Q(0 k0B1B6\22'W1;'OjBFImoP瘵jFi}CmΜ9v ;6tx~[څ-@aHڗ2h%T3!9aJ!iA'9-k/ʰ~RZu6gKvۿ~DKm-/p6lmgu%at;VNYn[F2Qo-'1\*CzЮ eiT~ 4;@dP{R[~^_+ץ}_o=GƟ;NI ?s@i8 '`n{C_r%2=dk{U6AmuCǎ\%#/j_t-jy/.8):k2G3Ԑ_4r{S߬P[9}6? ?CM G 5nąmO6XÐ <N0z" |tl<(NyXѠ8@60B *8ϓG׸vȕy2μY8z/?e.tn^:?PSֶ+/w ֵkg?{e%?!4_0n-e]l}co*EvlO[?F~}/ݞՙ6t*k?vwz3kk>h7L'zߵ(סT2?yđ^ou]M&pڗtqq(dcKcgmĈ=^{u?NcZǠkm\!&))!¤'fW^~=E~ k9$׌DzC<' V4Os[xj<Fǟ 9ض O}pi/X6xV1IV)KVKo<.i+V``klοWNSHnÇlav۫2786t[o d6mdּ7d3[^ϒl`omFK_y[Fn`mm]~m?LeZ؎:[dG4j^o^;{\>}=ctmGcݡ YQƶa\;_:e[7 UޝvQs][oݷ|Fzr+P_~͟ooy| 9[+VFԹvvhuL356:kNnYC/ӳ+G%OTOsOм&cOWn4]'x9cS.%Z[ƌ(F 0)3N-W|x \G^A&/pǦA#‘q|-pZ#cP BDaHQ|\<O"?2x ?᠃O~PQ 4(|.@j(P2\% YW\bKO S[I[TU>4 ho7Uy+O+2}lN5`_?ҞԙP\rI^ R״bmпc%s׿lnƥ6s귿mPZ' q^ҲߪO=ٶ jE_wbw9P[{+ 5pboQg{kbm!|BF.guA{SGVMFq֥k&4zb{m2|S'O{SH'=2{Ac~z嵗;_bӦ(]pN0q+u:[ZqYvO{VvWx⚖_yy_P). k_}SҪC 'Ɵ /-s*lμ{Nm} Xsb×A^VmޤiwR+dPk^w9]Mۿ*miœo!,0.H[iAX2_t_DgsqmMAonuw<7لmS䰳;WlOnrPC?b1Fzڷ6 ~A&Sj #V#o )ngvw;|_}FZbrs 5<ӆ 옣r1]tq?޶A- ҏ=mzJAr7mjo|Umuuf : ?ɠW/}j>v{還?:X/)!A)[M_t85dx-xr_~yA-yE/ 1q'iw007%< 5;@ϥW+`o-5cWE-^M{3s^%g<;;;evϞ[j~,g&Nl6HނvvMೂ8HpFZ«xH2 ⃧NO8xE ~@2L4E4 <pQH:hG\ d>xC~<Ç>5de+q?9C.BzFB Bx@bA ر_}۶K[˩[~*2,X~&U<:9c4oVt9W9gVJ}iӦMykvYWMYsh]NˮO j!j? 5e4lJkRfB' q.'xt)H[?x}B[.;8쁖2!O:y}DO? mMMOͫO>yW6=g'p7jp2)_guw8ϙ[io][GqVNm]8m^AI&?Ui7*; "wr>D=UjUVYOB^-o UlO2pD8o?RNS:]ԑ*ÚOFhǿտ7IJw+7|"2|$S~\P׿԰yȐ:!BRH=7uȢ2o)޿ҁbqAu^W?*?sjOgk O%LGN>>RR.# 0<F.4 Fp8 ^zh? DPm#c.2LQ qQ<,.@ DAc .q7\hx<߷Oپ|fLK8_6&4sU)A_v=e"tesUkr\Y^T/M)oʹ hXOJ7ֻrk,i&cv|D{G!6`\ (? οHҭ\;'Χd0KlΔIE(8}&A U*{XgaPV[me9vV'qW[i6x رcIt錺,nl{tik ::*]?.BxPgꫮ*ɧ_~e;\ ƙ*xVCj TSbVKꫯj i3몳tW\[2߰-RuzGNjSt>Y?:{o0:?m|ƟwƟAβ#G:}+?I]zɥ~uf3ϓOA._ve6V+"A Zyy[+~3)2i:S𪫯,ɏvC8{l0W+jfZll!l 7sL;萃Kr$:kI/_ϒ}_CԉϷ|vjI|2^.kn$CD0t ||- TGqa7"\<ā#m" 5]xH.x0BEE(,T>)Dd!?*0# .ǀ+Oզuk/|MswOR 5`˯ʙz:(DVM;Ti=vlk>f<2n5hȖ4C=8kn\Qvmw_A-VJFzsZϰçd[lV~mgg F-BjݤP%j.ꢂ:F[/Fp&׺k{챇B׶؛nveg(NF̍ivBlr̙μ JZjU[uڳ 2%Vli%^bZ@-Jm 厃WYyU]m'?/j 7^]v~(كNyQ C]\>8vM@ qr!R:@Dtٍ7aJwUڦq:! vT-Qdd'j2g?ڿR|衇(67j.2_N*ePC>J jGQQWF;'T[峭V-~کQ4 NLT%ADp =*UClO%LK<۲B~QI)?BQ +CD "xOV1v_9 )?Y UF 2ESr(dܩ1N˳Ty~)Ekӿl Ty אQx_[7%->8"O؆fqF LBHO\ c-~zD#/l?!#Ox_dnHȠ0~(腇8҄<}:C *" 4~2ķ|Jdp2){^_"M% ¢%Nh1" dRw1cǹV2}WmP6k3t1qm^;jlᢴF׌5g>p%<ָ\0\T//vi?giuON;Xb-)IS]p6|=+va]oVu䑶ˮ;[~<[opK.?%G# 1=3=oLW=l[k=mת;V5mE^`p_:S}Ŗ[qË/hOo&k8ܴ^i_K[K/*7Ac9wՕW!o ԭhۄV"䗷|y~iy j$Շb.ӖUtSi~Jޟ{940V{?;yQ/+{T7xKKKGv$B{w)eʍSQ/%=Z ZM+oQ%-0Z{W_KTQYQQǀH~QpIc.CfC A .s 8 >ri/Pdx~)22rN^BP4j_ƠRw9Oϗ>r-oiKSY+hž*6ɚe {i5Z4`WV)jiLGi3R[xN9&Z믰Z!ut|g?[>7܀3FGARu`!Y~ 6C+|Mx] u)/~ Gw>^g)Ϝ9Km=eI/+HJQ+([tE|ougmȐ!Ɓ^ڬByqf[TSx {5.k@FO?AMqoݨ}7yg+. 5h: LÍ茳|ctCfv\'(< 6yd:] ..8dP۴Wt]qnaDOhed.%hذV(:}啗p~j]wMljZi[-7~ ɟ3eD"_H7+_B~[RWVaG1eW?A `DhyF)|t8 ge}: BF~cぞINxp-a >#A#Si#L(hCWd6h%0? 7ȗ~'>xoV>>P@H{{^';HO5H[>uQ+l J䗄ʯmgA (+.{Ogvpm;bq_{!:DTo^{mp[l'a+(DzC⹔A:ڶvPwl({Mg:UW-5rm6KW!>k7W .LC|C=$YW?P7 1b9;<|:g)'"*=d4<+Aݔz͆@_~}y=)ښ2^ag)'zۼf+Ɵ霱5knPmu-1LqRJ/oFMl˭tjwmmH+hE?~͖ʹx/aoi)'Rdi+g5V}ucm*G{XsϱuY4>6EƻC94KWaXЅg^%[{u[|jw٤ۗvC]jq|W_^Vo)пbSb)__h|eSjbxL/;_Q !ʄ䯭3Ԙ7t>HܠFыLA~pi ##].EZ ϠK F qCx%!a4Q0j%1@@8\xGx"M#<%oz 9ۧ33b43 !dzb}@Fhr*#)vΛGƃƍ+ 5e i# Ґq-TjhoVļjO lܟlۿ=5cz[YYݻq-_ceH[}/꿨o:-91O@1?OV% )hn Dzޢu$0Ѕ- bSIJ~&~#-E\Ё {~"?!/@ ?|Dzh x"E(Dd. \gOƂ7*( |'&0#:>>0q }H[>B-!2SjjR ^ E#0ʎٍeLq9IJ+2=w`O|'`7h)GTIlɓ2,5Ě+l*liSo~sUVivmh7+|fzNIs8`e=ןذ[jK|x}6Ͳ+&ϱWt@hR<`_ 7,?~(֬a vwK/dp֥sTlu.gSʎ hw嗟rI盌0H{m%*WBGNr? |Ez-[UYп9Kc$t@1P Z)U5U.[z׷|wh!u~n٥Cee,Sa ;M؋'yc[. :l2`#~ap&Ӷ_3ဈyӍIP#(ddpdF+?2K|d#C6bDD~l> |"?InDVGHACV%R^Z&fߗ 0X{ UJ%:WTq_:,_)Ĕ|8S_* .#~'W6R}-Xq% 1YP 7 >ycgk޴ 6MTk ;WliUQGDmmVޱ}\*ײe򃘦,Gl=]1 tJχ|Ǫ]Vm;_GϺm-K <ϥ kO\A;7S7f}Z<_|pg7_&&un5D+wY@8_X6m܈`S3V.epZot Ɵt VP*c_xD4S/ {kt_R|(٪ q{'w'  gm"Ze_.yGmە*y?/?K c.~s!?dP{[! ϭt1aβ[j̈k/ R|iToQR1UBI' *dbS/PUW(jv*Eĉ_{і=P_O=^ -/#!/ R0x,Є_^6!: p!o7"=~\@]υѢB>c'-B yyQ16x] :a |pF?\#!?ˠ$0%0pR$ OKK- ]5v;?<@$+O[>+t.%Pr9.wy/ISkqmǚ??6R0Ue JGM 2o?ȶ֭Neiv-vYv{oߤj)Iy43Qi*~^X&Z]ڭ$YANŧyoTH+~I/dg}fYulMK 2 YI٠v|ƛ .d0$8OA폏>f:iǴ-DB}[\c?CmwRsSop`@BBKxz])'5ӗ~`R] ٻ)пESGuO-|5qDץhKsB?ZK dP7cJ4]d3z\dAH>z<~4pGZ x5&d9AF"r!8yH嘼 植Ga7nd?@a IDCosNӥ$c {ꙧUGwr#@َ:hAMHC .d`"_őA*coPveWM7eh·QUY[lY1VQ#uD?稽>-dzMt-owߦu;;OR/>ܞ}Y{􉧞tVݭS'cߴu)]׿GRuy*\ySh*ʢIzoS /ES_@8Eig~fi[dGZB+BS[>1\&#4ϙC3;smhn^'tG>ɻp @^H^0<"a"qah'ƳHG"O"~x y!VjK4jmOG.ĞEޝѤ?piI8(+ԐF[>X>J\O>ovj3+uֵtƟ{͞կ9XuЕ3Y?լ˪l?j\_t>V^={?ؑs&dijWiuBVQu9F>bʮq-WtXVW65j}}\]ZIBlQІ _0",2]Zo*&x{,I~@km|_FٳSwGm!c$E?d]7lb.Shkmuu2S~a'6ZʿzZݴmCm-u]trtéB{A]vVib]v1?s:_ﶥZks! + _?S?߽g|a%|9_R}ҤF(>n\l6|LZ=O ~E:'2q0yH|~!| 57@\ 0lޏH$qES a q@lCG[ӥPn-ԥ|('&]h_~@{W"_%¨d-5Rdd5\MRi{2*O9i/+qbCWS(|uG8sl"Dӿٵi©}{٧uc\m엪kh.Y,#Wy֥vnM'nMߝcxF G{Ԟ6sfۮn㗹wrQS˥PjQp?N7b|*N/? E4#Gߗ/s?9y.j.~((FTlS}[!dr)APb  :8>E|OZl7~7&/ƅ-o G|A \."0ǟB '>2Y$EA,̐AbX   FJAyΗͨD7Z5M|O JTBmUiFT ! -rC _<-ڤ2E w/^OѣK-KrPk6A9nF wG2.k:kx9t_Ƕf1m}ȃh[į*mJ:)K  1ڵU-E2\j-UC'茺ޛU͝;[`luw}^VHJ/c;nIh~8<1SQRo!V]uUT*X:'_rJƻ$xJi֢_H^GjcOQ ר/R^-޿ 1i;?VyPAϹRp},s?y1 _}v"K |kij[/,,rhCӊW$T_,>rn8Y]:C E,G\B~mڥjie ~7%N6{1=Kc nǯf{<>*RKڡB[D״3ekvh5k֨IC{O7ڻZ Y:o 8 r+wUZ]yUϗ[6un,jٲ7U|ǟMԎ'iWePk)?s#vb*lPjum-{ejͅQQ0KA˳H[b2'`&1+*!PqFC>遐Eȃ%C~@F זO::)6 N1/U.MvBR٘_{y81HsP[wy[f}[" k:ɧ-~WҜ|Ɵ/u_oL j+EcK RmmТc͢S+!ǧa8σLc 0v0Hy"<yB8 |8_(.|`0a$ >A.|;p1AD:hG>}R`2ecH="ǶI)d( MOɃKzm2cy2cru7K*__0K$_~˯jDJUܦǻb6_чnP7zf= wڢ6vԯ7ӿ.kv͵*fVƒsY}E{A-__F:ViM1}9V5fà iC1"/]GV-t E*ތ鷖Ԯi(]n5{f^BSGW?)̿ӠW_%?}/Ii|VuW~{q a 07V 4.|k& n؄-~)T1eg@H/$4aO\MӓZ J4AOzȊl ?t<x }9C|'_d!qtG#pzgd~A+ Wh|I4HJ2m$m4I#l3zopzgm칂 5v] j;7&nL4P]i+^E/}ҿe1Q7F2S MYJa4+yfY_4=O+{_s{mMJM^."KkƁ &@Af(`@đ!!!,2tc|ڠ 7O#jiH_xF8V .H>};|9T _ADTodI (aF9 &7O69har| j4C~C[=oiZ[>f[k֬n}]R[忟?v]M68RGիy^t&ۚU# '-l?w}m7ֹe׿>u, ˳>l;11McπuYf̘Eпg;k\;묳=u[\\g\HT<仌:Bj`9?b)Ɵb]Sj&M egk3 jO_jDwF0]6f4YAK|Ţ;G[Pt.x\K HTGt54!P<c@A> : n74ckg7I Etj[>a5)9Bg237?c C#&E~5^yZ[>2,v1>4bZq4ҋ6utՏL[`fSӬ~F[YmlCm^u~}ڿjOG#>L ϧ~T/(I'&={*Wewz:O9drƦӻd^tY o~nLvaGش)p|>B2uKx - qd̅?\Eh&MjWJ H>Y0.hap<}Hg`PW Q:HR`~TYV-Sя_YRfQPC~Vm&M-ପ˘!|!v(s2&jii_TU9W_O^\?/Zӌ\}mTg̮] (nD1W#޽Otz=.0` ׷By?YM00dVYYuתJ0`@(Y-kWx뮴~a2*OMRֿ䓛Wp_&Y2h=[.WdWڍ2&jk" NE?+T,疵+eLkGtbSU bz|!}0nE%@+ʠE|Yr€ .O׃}GZb l/l哖W+?a=ѱfFҤL:Nx_?QoT|VlLA4!?aS^ W^YҨ۫5h^5)z /T&y lƜ&[F*pxV9ԭOWghR?d!Iڟx j*mlqڢmOVJZDG]V6YeKO:$iOװXHW*VhigͲ1cǨ^E?Ki;ݠ`A$ uWhZTƍ}kyʯi27?/QTYҲ.Ϳx|O С}mQ1 1.C]OW+cJD C [t/| xh~#=X&oI~І(y@>GGނV7ϤV2_h% DQH&.2rưəhT2<-~ 颲q#ׯK]}˧37 Kn7E|:0)lz+fDel'`4mԾrmڴV>o mimٹu贪rV^jkz鼪umv`)ȭ,u*"◆-joֶbK Oc|uKXE7H4njkf*޵IԪiZOZgy%Nsi5:_ M<Tm*>~#>hW- uyDoK*iƌ6IKWZi%뮶i,C<<;v-[ڌi㔧eyf6SgM3׷k(=lh*߬Y[k+ۗ_~i#t&i6eYZٚ]K͞=F>߆j[>Ŭ_VBmרQc_cRgkꫯnst^?iD籫V-Pk0߾][[yUB3gJ'*n[5rUsk5jbMu\K j[ Dڧ^}+ 'iֵk7#.ڇ%JTi[Q[SoG}B~пB S/(N61J,#$ЂskҤ +jyg˿ҖOCDXpIƯG1O8*xO8""-4x\6؈ą?lGCkuy5І|!X?*N^0in/yX|py?}~}VشZ9̲Vm"B<BD)!bp>,ƍϥ%.sgv|jnڿAa ˠ0~6id#9Cĺq&֬i3k-i6mM8]DoZk(-Ŕ&%-[ϽCG |xo{ϫsշ}v}wk caY+#믳?"S'Y^8}bZzaů>Jrv_|mڶ?PM受9s߳t^vn'cl;ښgd7e_DL2 kwC;T70=*Nj=oOy h`;|=UBv o!? j==د__/'ge?`}.Gr9S 4I.QM/P {򳺠BPTw2TTQiH+tśF*?no$x=$KU t'_Q<柮sY/K:誅qs[>x F08h@@FF<=Ae F%H_$BM\dϒu&74qe;A8mQG<-Ӧxvˀ?@e5i?d&y,Y沅п}֮p]2si^uɯNT#Mޤ.W,`Iݫz/_GA(۟W~?|2_Q[>\7ϲ_knBS6K']l CTCK8fDvH a ZO7h!`Z)X aQDHK}3 F|>1Q@/# t0Gf@^_[ֺTHqk_,>XRKHcL2"[xzShGq>.TT^o%?E!éJ?)4y|޼E*ʯC;ڳhBQ yjߢ?7)'?~n,m\p_Z^ku]Kz5 jeXv"4qOv}'}w" ' uABy<. .aC? <a耠]/ ¢""+2KG%ziA S#by@YiDp`߾}:i.cS?*HYgq dawRƷ#Ҁ@$^*q&!Um\2,D+iڛMe?y]uiZNDta]6POͣϐ;ؼm-m*;&VHeuo?|-׭7ߪiY R?|T$`iNg=X}ed&oa{JCm_ʌUM?ԘHg|oUV7xVJWҍDt<џ.e9Z^T 0TVlAMo t֖#m8p`:?+]qŕ^wf]tj믫PK/^;Pq]>zi:lHoni*_xov7zkVZV]\1~z20"r^x[s5 p,+zʀHmݠB OԴUW\ɮ^窹g)sGheJPӖώZvŕW*l=G뭶c=4?lgtn}LTT4ǯκ/w§Nҏ3N!RgǪ} }nt.Pu޿O?R_f[bS|-?9b] SpVnnOmMu 5-<ޡJ||6׬0.˖0@΃M#P\8qx'i!8hJUB!,P! q?dD(~ +o1Ai?/#e Zp!#o֍t|pmLdJJjL=څ A@~VlIQ&/_x ӤɈťh,mdrkđT$bIʟ뷆vkuoj+:Z6eSh3N9X6k4'ÚͪԁK7b~ 2vlk`vYPݬzof' <^gܪӞ ee_oe^RMi=GcpKx*奛"邫wZ.7޴K/o=R-*~-Uy hO>iwnOKTW\~_E4Jr$v٥ٛoGz!F 󶧯ߠuhv~G/vwҷʨՐu֙~.;q=j>8.2[<Æ /ײ v72˞~i{Wg͙=iX=:y`@ԖOr)k*#$'2~;à<3v 'Jŗ\u)SlsO8ј=Svw)>"=":K)\ >\K ,VPPT<8ϛʵ3U7=(`ik ho؛o%^Q||(㟗y?/{Q /n/?X}X>DB(ߋ*OS?g +vX;t jP- \@OV Y혬6GWmBiOZDZp#q^e{yj>=n˻`pqHE& ahˁx  9 >c FB c:ټzhlOR)#/79 B~xdE})oݪs!|'G,ӥrS<&__xoc?ͭگqi-ɩHϫkUl^is&̶Ļa}l Jie2LG޽Bm孷h/!DN^UKRI۷VJ|d䓴RJ,__V͓a. *L?7+&LxRƚ|wۏHUww~.xV4"?ANaJMSeۤs?lV?ګ禶;-l#0ok;E_TeX?+ҦϘՓsءoQ ͚5n[y^YӴjl6T+n@F7e/N%"eȬJ'gg(/$ʿ>/=_w¾2`h9*ǡ*BTVi:BADvgK=~?bͼdUK$_~י5zhkg v&Qv7.H4kְeDk._dQqdB\S?&ͩS;_Tz|xQ##cS?, rcRT?kuu9zJa {Ndc P-ah2"_H >A.rO<xE7dFՍF@F( $# DqICp `\@a |touA-J&CzP*YS0ysdɫH(OU:(r۸A-|fSJqC^飥G:ĻO},N:Pi`vwl)'ik8mj f/cV,Sise`l6nL 6nœ [.|Jo h;k%e)iQ5)rKg~ϴԪ2c.~i;'K/Uf5gKwF3d8R L73/6=l-t4XZ y^|#+fk vA7b܀˯*EJ٬i;ZV@]|yƍ[o5jDYJ {}ؓ+M[-a~ԑG۶l_!O>n{Y`|x3.ިV~8 ` \ArۿThMV"]уT^VyL[>)9Rrc8=C=_w3if~? B'OM)?+J#*޿̍M_5_g/eÑzbg%:bȿo]ߡDA.%]>K5iM15ˣٶ74#+>O,`1<|[/4䏴tSdٞVm?eb\/*}02ni(8L̠&n[VQ&_ ˼|( N W;l:~u kؼVRϦ(1CgiHuͪfUm6w\3iUNk?nwXڟ_֥lHjڿw/1ުCܟe[;#]M-Pl}4'vM9KO|2oʎCq2wjn>{ J/^<.|Tg ʘW!|F::jRg 9a{m/m'7dշwZAlf͜KZ ϊ.V̸W^~7vǽ%%R"hM`: uE1PSO9E|+Sg5"p֘үSw տg?#l0n:ʶtjK{ˏ!CtۚkViEg-i/hq[|5gN1gǿho'(?7R|_IT-Ңb)=|L-kξ<4Ɩ=E46 |sጴࡅxB.tMtG>:=y~ /(Ld*/8E.!M)48pʄF24C?@8x!;83>| I ߼D3"4L,'%~"?\~:Cmiw9.ezrSa ,j~i֮x;Y!ͨT?VYhShuZy:smzSM"_y~\R) uS.UӠ-kwf"1A Md\s-뭭}F8y'~n ukn~I'LZTkvY6|3H|*:eܯz[V5۴MQ#,+$O Þ J^{m]pov ߺW.֊8 /dB*l2ج>8 B>+`>*lnoqV[kaUx 퐿ZT'JX7{ſ<ήcaoٺR(0Ux}l<ȽTV]G)cm>3V i[-@7R-K@Ǣ Ά[wU2)j2ў|P- j+?>fC62S|P\/пO1/2cH2s9/޿wC=3TPS!k][eZ>CSKOa O.YA cSD70.h#hck)jTq Ap@,BG\0O(˧4ЃX-rpK\ "VY|f}D闬Maɢp|kVW|4OEY *Ʃ 6mjĕ%~Ҕ}K?yֱm#[yM}pVM?K+4IU<PSes̵cf٤&mC3~]rkߕgiv){gYs-hUكG}T{!ֹs4L'JZr&[dY_X8K!.\a {]_˰* j Ia"GOx#oTAmS6Z~sL>d?KTnD m;$_ jz: je+APǠʫiqܠvbm)xh5yd~fhޞz/KվzEI]oE @C#cgІiӦz]M (^='+^:.XYHE$I'=f G?3iWi~B #[粂2'i '?CIץjd^Dgݮ3tF%%t:IۃISmE|oQW_1Ș^-WQһx.I|^CӾ?lk:oCDOO\mrOtq#=Il;++qA ,@IDAT/C-%PQQ!4 K82(~ GȏJ!Lz }V|dKY "L.\sLH"׭Ah['s|O KyOkC΅q ~Q;kwNRcLVW ,N\oY _Ԭ͊eFfj q-3ufZ6a|αz*'ϵqç[U kcɩwu) 7N? ]mԪOYw~ȑk^[Oq=D daDIgqvYfA'쿿D69r3_mg͌ΉOpcγZQFlأY}ohi c"ԏ[%e 0 c-Va[mҳ.U䳂m+2e]l-Z&k˧V_%lV >f 7 j]>!˹y\J,i<ϰc{eY^߷ֻk^C!Jl%#%`YLW%C(frLf""EIHskzLL" dOČouwut׻7o>{ν{}Z{o=﬽w<)ͣxĕޑb}X=A5= ;w_?}pBQ6-0:f%q۟Y#l25Mi9 i7ְh\Zà5:V&ZEro(L(3{Hqcܘr;5vLOq|+$Pb7D1&gjz;:!̏Ə7xc5=8K|l(x(@y<v 9CzQzؼWfr.|CA$c"\32ٻzsV|g,eSsU)t֛߸z$ћba:IR׮]uzlfYjk?_m~Y=ʭ?B7+?U P>f!K5G.C盳ot_j>5O_< 6SŵC&jJyS]_/RWyO!'<ߟ\v m Z4ޣUzS*|ܧofٿG)Z%{~%R_=)9HF%oxdЄ {>i\翿!' K,ȱj]~s srQɷνφy4N}J|>9N<8?/{z$hrBMxB{G><ͲȪG! 2X? A>094ra4?9zQWPY6߉WNz9}z]MC-v 0cqEnnF?;IE㨎P+??,D O'wo7O>5?~';?#'3s_V߶|ͯS[d :ҭDi!g1r-'~BZ eW[ozmO? {špB1E8~,&ӧUue(p8|gC> q iB+C L2$ Ӓ& !`N.¥Ti[iO!]Aǡ<7idvG(ȭe|W_P#dT7NAI|ĵ?PUj:qyc6P iȧޡO NiW*mj^|z{3eKsTQ{qlvko;+ۯ|~i7n{߳4M2 {kwѿ8%>|xo|Os6y$Czۜ;4t9P'>w1%^Ls>E*>%3O!I1zNr}}rJ/eyVxg-ʚ.\|?=}Ro/b_j\s'gnԟ9'!/o9<ﵫzzQM`k֔GWZipvzpA4!`gJ5vYȰÎŎCE#A , ^i_ Z/˂ JOHyRvjٓh9Gl g|i)OS'_yvLKZJbmnb+xȃ 9H@(xam]<O0+ޣ%._҄O폓/~Asŝ T{]{d~W=]on脚ީvN9ݸ<͞=[g>ٲuצmzohn}pïj~|š~xQjmſf=T{:l x7/~?}Q^owjn[{*1'ږ=c6}DW;CE6TקDJOD=DoTil } Qٍ.R$7ԢzrOοU[+گ?ȧ9|(RYN%>}寷??~HaU;j:.4>E8og t_yʟz'矜s7>b^q+|-wv?w7՜8q<~/96iJ븢ACA4|5Ĺȳ|EC\2d{IȅTNv=!S۳K4l'oo [ Ek<.K~@})&BBReVYIh~\ Fҿij9_!]?>PSrq柵fOG hsmO e }AF?k?kQ@eWMCu6'_[. \F^]v#e.֙ۂ wL_}:v^OcCqӦ1~ Y׹׆ Wδ|w]`Z Jjp!袠 sZ2}!8<3Yk.HeGVUO䓇:#sqBM_+ÖRn YH~ j . hFN+ I[lS*3N\~ѷЯٹ]hfAc>Wi96cCgg@_,b2 &}bE: \t" _?sN?Fo?ʜخީŶ!rrhHj?: |GzC) oyC97$ ~@u ;1X.sH-Tm$6DH>J߯G>ؾx:s%Ci'?/Z|a~,GdeWM:a2NLe>|-{2<} % _p._Cl )s Gbο}_ѧ(cP;2t=*?ן\svM| 'z-{Cn2cU@4?(S'pzyFqdJ<w<;|x8΍o4#MY"Mh#yjZㆆ0 .<‰JH.q]("m+I|tn hj8;m"Zk uP{Aj3b( ip z{>T5^DƯl~ZAWTRPL(9@~IErS+$/Ш3[.;14p&Jur4\QPKBfʓ_&w Zm`V<9+m'ߜˆ,rwsϜ6sum'#GuB `$q+~URC W,s\}|dokaX!;P[Igߒ745  dZ\98*OA>P->8-jFc~<]:uyTQ8xqmEVBpAt_.޳ Ojj S^SBSipUsBOs\Xʔ+ [NsF?C5sֿr'矜s>;԰8n,,_;s.E[ `Q >pOj~;է2|!xr.8u hq.y8YTi38xB!w~#n=vA6i;ؾ%=Yk+]4RzUf-:|h@+@8Rx6j8C8 r@֕}6?ֺL6&G_?˙ 9O (FsWK+km֡vT_\kyQw+&N9|jdǍ.!>㝧!fv76wGguz..Y^p+Z"r d.t+;|Rbuz;,:h#Dxd[q.z4\EO:"<Hycx1/$prHH\)z{;Y7r@ ї/\ ODNK_DcOX(O ?jۡX,Ͳ5㏑O,,?9Λ˂\Ͽ,L-;t#_kv]|j{rT_\KèޡƧRO[ _Ds4` !3B.א&|͋lxR`y%գC \dE[WpxP gGYИZ*I48| ;ЈC\>j|r#M`ŽH{)'Zʎ&<(% q J_t)Q(IWvuwS-3Z ֺi X*iii9i9R7''ן\r閝.7V>JH<"Zqmw9C-8x 0Cgu! Zxja<|\yY YPQP\( |\AzR^lH"]<:N+zڋj(D(Ti'0SfJc'JH4' _ NA֣l9炾d^9TrBPsҠe?2ZPNi OοWT?b6`RZ(y_ϫWP9Ɯ6Qr)sy% r\k'".4#LOT8>dBK> ;i1H|yO3ͼL2Do(Q !#4iE5ÚF}nW0j?w(N 5ZYE-)`ɳ#t5 5wNeZh wi|A./|!xBZOLG_#iDvZ+%V .81hTĎ4WZʁӍeC@cv·ޡ33ʧ[sCLI!khYĵefQ9B_+2X .+6#@z?egWr!j7 wo{M|GʡB|2*것N3ީ#` 7,!4OiL >󶻃Aq慶.244Kz,qkZ.!\ jgx5(^F4qrY@δug75!99kȄ(~3.gJ^"bGDa=a;Pp:[Jl2ިF߉WN19N+jN2v٩44!#8>;|h K;ֳNore+9 \*0\uEQngv)ߴgZ(qE84hjY:yy$9.(= ["V1Y&y 6ߌ 92pB 3J[v M|*>,~dm[ʚWH12`ߴ?Z _r'-WysSsn2QQ?w ӧωu1\o*hB=|h-8Gev@k?rYkY yﲙfhX JP!Jr#r)y]h\X2|ȵ32y' %rsc:gN~Sg<)UjDa@\ ]ݒiWS2*[99$ R/^ط.׻}/?=翴?u/_?soο̿ׯ_Gȧ=BV0)*QqʧOQ\o_*vJYx4ye!0/4|Buq<; qZ$VRW8;an8DR`x,sɇ\ӵ 葃N,š8#'s A:M??O#+ߢ_?e-ʱ7ן\r'?r㺾ȑCFq`35#RZ=IQd 8#5É5 ۶N|9nQ yA<',qZOX.i)`8v# 8!4v!r(s٠3~0nYмG>_Dg{P>|\~AhSejp@~J3!ic+_%O8?DZ?9Z?e)@USOOοXMh y+_OOZF;LcjEW>qţvD:-igD춣$!×cǝi84ė״5n^ e A8XZ+;KH+W]Ne4v]!|@g>Bȇց>󾦏I;7drxEUn2<U*ڂH[!JFq""J8_t1J}-bK\(?ۿЋ/4r >4I֟rmq7\-?_Nr=WG|4B'q?q1ԠC w+V4@>y6ۿcO78L|"@BH~艻ȫHC_27 nY.`-w#=x8h4hvA[ˀƲ 4h\Bd~9Xٳ!/Yč$WXEFhCz5ֿx}KqB|"^'i( 0AczBS]s^4҄E>>˄0 i隲!EB% .@wuOڡ]Bo^duY]^hYGNz=Y wX.fҺH! (ΉY+,!9)F\Y\y:pAG Jғ/h| ru:2? r7ןl4 W{}FL董ɵM31pz7P߃ 4ڙU?i3:ezd+2@Ë#n>Zq(z. .PB*?!N* IP@kБo]-Zj=Б4!Gkk'^qj8t#;7%lXSG($@fXy0qDA??/F?9XX0 0iW?XIw j v"y_~ #Gi<~x?<}.;ahJ ~ɳCxB %8$3 )9ϡyCo_;EaxrQB.W`OyOhTiK<>U'rBC2v9 Sr6&@$d>RPN 98(.Yr " Yy@nS';gc#SGO9ƣ|Hq0wh@Ws-ilxjEP\p^3Y{'FcK4齌#p88ӂCi- ]B x]6e-  (D0.yġs) 8^seHsDYgh7=8eТ׎3B\6Bhs^;u <>_l c0"@$U"`,!Ւj%Qmz]ڳWsvV̊KE=̑osl?6d+3GɢHsDOY)m+r'bK4==R4|?~x1H#<4Ee|;edOg:,.AЙOѮG8PVʒv.,2ߍB~348gGu'evN6d@['Oגfx-])@܅"Z*BXY"Zpm 7pAM-MRdJ_ʤy7ן\sAXo<-D? fY6[ x->C؉?weC͎2F}CLNG~p5oGH[6qzg<%>6bFbr hw,;&D0g=ULiK5 XӦAƂRr߫QWq m3:$88 l._'8?rk_9uR_4% [x%LF@WHy ~Ldy6i/99Oe!e#_%,f}u/.Dy6#?W5P"q\Nr㩝gJyi)OS'_yvLKZ_+RM81ҋxNϋ>6kt_E/8&h^2ZfM0t~&OOοy ,W?srW|ޡv^:0/>(q&8?8xHsAkiy"to"Nc}5:r\C(yQBT…!283='\v!c /`9q<-4y3z9 5{=\5T!oRq͉:R)"D!EGwc ].J6^ԟɡ9m2wʹJ+o?FE7VH7%Y+g?YZgFR/V ?O%>}]mg4&Nkg[M&!&!<2 :` zW"킃 q#’ /EjZ;pyL~PoZ'8|>OQϞ}KI7y-¦C^$L4][!.'<м&ڋ<&6Vl]vx YuY9͠AʎTm>#T.GNjw̓^%*kVAm1RbMMM5[nmol۶]mնh Obw5wtpO׸h- eLAKlSͶ[mw(lRro9l˗¡GvN1Oo<[(0[mi{n3fh?}ЎijdOMmnb';wĺWת]Pv=|Xi=g ]@Fhz(_n>pks77jg~1._ؑg?0pqsJ.F]9h䙦q8}2 ^퍱 hi/E8EG 2S;+W+ve-Ѕ!N>:.:$4Ƀt  ]#]:mBj/uvlF!3_|Z(L0hQC12ciA4u!x;tƃ3'y]>EX '(j%b\H;((qB;Ü8L_$N>|8oyaǚ$ođUՓ4䡯ȡ\P .[I=-gPTzVMMx ZA@\ڸ҅zgiS*i>CVsޝпY5V~ =#REOji6o<| p6͎;]о ff߼y#G0;w67oֵ[n / \/zrEΛYC0'po߹'v4vhN˶myJK-0?s:>7s#?Cm]z0_[7s;fJرٲes3yz*yOca9_9:9{;?w:OBj̿۷ ;!MV?ez@r_?k/?'?'Դ%28i3B|6<JKAFz.7wˏ"E7yFk|z4:X7|un_ BMBP. O#:> oGu֗8 #2 MYx-8x˳'Xlfξ!i 㨎/Gy2]^>WkvD$DHz۪e4x"ߜ_qx@IDATՄŨSo 葍ݻ[7㉝Y۳,n&MtTm+CvncO,?7[b٩={DobMο V+-00[ɶ ˟CN59KqKw_O~mͬ=VuY).nu|-٧fm9tiI?7C=6Aݻ١?S<}^4n?_w48:2O_.@4߬kUjP{1y{~ =b59$l{݆Ͽw旛?zg}_xpпjl#y/ FbnO3ǖWKJE竊2×U;:|8351:gc=t/Pߖ6hB}KF<SlکSG%%G/[2,#X?QBOAВ7𗬢[ (2._*q.rC  &,k;o ^T{-,X6W\4~O:{fj˦&!VƟi??j?ޑz-v͡C”{ϩ˗h bGSsss=+b0N?v;vԻz>^\ET1@vB|2 ;>;sᛁntb~ƇcY1[ǡjqrZ$󲬜Y ]J!>Qx]: ^4"<-8=@ .|"'Ozb bt=)=='#M%RF? G=yQzQ1V):[Ѝ_E7LQZVvy?:vWNԯ7O< mX"ϣl9U;<\10֟G+f ۜԅS-1֟2bMaS+lNm}߳oUhv~mXL?N}ZWc7ޝ/2>Z/vs_VcWV+olzjRW>qnm4\X.FG>yϼu%q `vИZ#GOC`qCq"`+cYBBUKZbEQJLx6He1=5ie5ItjHDR${uxLСѩr_t)^ZWv<_2<0Կg#ac+X3*?9HDW߃}kCqi늞"Yf_zY_ԩe9d+}u0֟ 1T&t-4~ز,aE|3jG-xDu_oQ߲Ps x[89W!!q䳝qshZ:ଣPC.d!΅8r_lp~QE EJA E0'΅' O!aGeG+;i_eyuWyٳQPjcÿp),lm" Qyus.\k}(m( nT"KY?os"$fK$>Z5Opȑüo+ aCݨ$0l'/`+Sb ?@\zEI7eg9) .ߣ33H͕Wc8y:~[;K1߯yZ|[1 W42=rI4jeE7矲ClcͿ,W5ЇGny Œ_ԟ('PSM>C \k'".4#LOLE\ Y -2:4B3e G40tg״kDž\<#o!~S#|͙^  n^EH;Po* C@m ܜ^@1*֖j:t06E?<0a9(??χO?X 7nE ki59\wќOjʵ2O?fŅ_(\A/V&v#4o_8^xUҥ1/;6g.NV&EKqBanԿZjyޡƫtBZc*&v&ώ0մjpИ^NeZh wi|A= v_3CCkdJs9 8ؑ AKܲ]x]9vlhNu8;ԞxfF_2SC'W&g}MHċ47D<'weˌneG0_Ӫlտ /䃌Qm,fK)7Ƚxr?oߜ#671s2P?6͇>:ǥ š#_#?<1, ;QG1Oʘ˝25FYeQoiGGG,iQˀ00֮֘ l29ʼJ||uDhTǘƆE !w#ty[ tg^hr :HCc %Yq"v' YenDӚ|.4;L[q{FP{8ԄShBtA# b{3E O|2'YldX;=.֓nTSb(Jb%##ihF0%Կ l#"GV:F &9Hh'3㤥<&Vm۷]L̬6s{3b?>oO-/5OWo >T_hRz쵶dK.a&͡8f06~Z~g/k)L M2^Tn3iUg DVOrBڻRCI4;q&]|dS45X.iԃ~䓮:%˅pprFGY!h~Z[nh} Y.h;Р! e~tvl׻玅-Mrld>kaBN) ȌҖ]NȧGdaRPiխ-^_ͥܨF 8=_ϸoc/Pá&_Cd*n'f ?prW'-Ϝ5 :z[lzj̗v/uw?ةi(_{w*8n̳t'?FZ9@};HW՚q/g.#o)IlljG ~Rv'@{O|h#M|#2E;]q,,6]6 k!C *d]iRXBVnD\.<!3 M [vY&D״ġC|nlBO}yG>*QP(D+WBW9J-QO{4{PqЁEWzDIgـGn/d;jaOY-J{7;o/9hU\17yP?,]X8vs"߻^~)[vǎtF5/n %Y?{zeo=濵mczsJ{wToiLx q;/T䃇EqҎ{Q P;qeBP GD q"C\>4s ԕ2΅d+H Zx.!# p=r qeCXygNz^6jKyԲxhgZgD Yj&( +_q_P6<69jTe韕06EK9 D#n;"Q52Ʊz?i9&44 ;s1H &(?WtB S|6G06GO~jڡsJ/;^k=L?%}A@W?\;He/OR `_!N%7\Tb?Z_/}jڿn;~B|jҭd*  SMdpЙ4QoWMO]CiD~O8  ,`b+9c#7-g^nd'N3dZCe.t-狨}lj‡j7`SINOV j1.؂(@΅/D[dk]6Kӏ&Ɉ?*e?mxHFjY.>uͷ˟ʲ?;h˥MF'rr,0KYx_x`߇GuH_Y1t0o\?9GƣK[|ot?wboJj:x/Mp+}38סFp8LZ1=Yps ||< `A--e*0XT%J\hxMg>W :N>iv X yMagr'ߴଣv SN=O,db39[ioZ!˩GUkzGXJslڬ?8C pq*BvfՏcr bxJ,Ey7<8;7Q7y t~ R( ⤢ E<)rcDCCr qBg93L·)atVFnȏQH8*ZMA(vCYG.i#~Hez6;~K0QvO^s [_?J-n/E? :B?LؽTTL75®0>W^O_̫m8j_$*?.^#?|QƳk/X.gQMxs%FFk/exeGCO_ԿǏ×OlxRSs0S4'_O,V=ZА/  |;| MY/2yxk iB4醆f9tPWpeW DHpt}!'C?q!$ 8C!x#k'O}fv8\3|D*H=ʕjKVZy %GiѻVH3HXP8 *˝B%rSV$rK44)p;D?(n?_rbNaa3ퟯ2cij2t9K4_d=˘._ƤGü/l5ߜ|<}7VL̲Ͽxül~#&VtqX!I?KP+J|dSyژ/#g_N`W/?۟5Wb=w0|/ ~ɳxB %8$3KM'O.<94tkzB( l\bO4JHgG#ؓhZ&&nOɧ̤]E|BC=sԫ/!9|  V2/,JkQWPͬ Sa%qV?tq []T_BLBdْ3~ Ď{Glsݻ̓&8GGR#3>Nܹy]͎tTͽ{~|pFj1jG?7X;={態W.e?uw}T_P' ٹΝ;=gnۭ1k@kH<^l_ 1M-QPN (Ktp1ObۼyKs`~͛ͽ۷9B6l/';h޿\v=?jBy˷6Җx_lW q✆: ;g R5۶omm.SmK^zM Ǵ_߻{w]v@ʼ8d__W+}UmK2tUՏLAg\[jS IG>3.| =3ggfE&qGjt}ZVDŇ7}wSOqso10ۤڵ;nʱ2+:ch^l_o͉֭cȿ`je̿O>^E?ޔ$V:M{#>ޅUR,Te8G,`(gFkR|۝)_zbכo2w&xss\ÞY&UFgX=0ڰ ̂ymZ5;{y,!C8 iBL5RkIA4mFGgZp0e <>BO|P˦q(&#8t.0b y i.(-` ZqFHF y.kNzLJ+-ZUAf5Ћ tJLe B"z:tԞ>_~AV/Ex~'2I66k~&C")5e05qϾgOg8Ԁr]c~qZ$_zkf]=rwf}vι2?g%ǡjLym jxa-sH/oԦG?|-[4[i9/wT{go}sO=} Q϶ۚs퐝;nDoͫ?OrͅmOH|SDzMvdx/]botQIh.=ڲysm۶f=2}u}9&rDMD3%GۿN\5߳}̂2 d-R"_ ??Ǐ)qzzU]vt)G +Qgqq¥P;!R ?w^qSQ7ofk<~Og6F?ek#6k8??,H;vBY? ?Y[g:-|sJm߮hyпUTzf~pd;̝\׿ ,OD艞|}[]hO+,Dqc'N0Iw(C͎2i&M2jz%;:'̏Əl7xc5=8K|l6 ߾u8U9ϕk1zտѿRO꿶?$?YnrkZ? >=waj .qEy睷߮})ܥ{+Po)ȯܤOl"Qxgq̈_\] u_%`'P_JPCڔK:4 Y%=Rg{0F,BVK9s7=oᘹwHODU٭V1S J&22+*2-"~T8ȷvǚmV{CΔJi;Ѹ@0Yg o-~¿oz:/KZqnYB8NB0q>}tzb[P0nh˔OZ/?.w57/Q/՟W&||蠬w|F}CacY)xq,$et'}jjq)-[lS}'yX 1,R?I&/>x8bAFP.x%7/_/8'Rǡ'dMgמkV?8JF?>|9]?D߲tT_P!8Cl߱cDJ:ڱoQÇG=zAq;PC|YG0X7p,O>\k$,ν׿O8CMP'"C?.j%)ȽBgϝ tZeEJ)<idZ… =Jyp^q׋vlرF8rd:d>RQdC3"40 \J?7+vV'P[>Ñ,P3(dL1!8~f꣖XHD1ҼS|-5Q9yTԟp^?}^N/uR?,"QGw=YQ{>۪N!Rʱ{QaYPQHBIJ<R|k3jYTvY/5j!9-ERԏ^˪G,jPhQ*J?g2qvP%}ua!_H[>{dA^ʏ =Rnq /_VXɟ<>|D" /I7mĭ[YAwK/V1Vgq6/Nfnœ5XuYq"B!,<V:ORR%B9JNs)Kn(L'hO)izR<3Ԥ}N]EEȔr])9V(:4ȭ3h*\4Q6S)^n.)e]믿j0/*uŹ6j$К5#nY7_z'ɉZ/#(JWt|B!\i|N*70/E3|gWn5ѺUBӭ[mKB5@~9|b+$$Dh?҅׮^fCҋFv'?iL8z `00'\|U)g#3@E8bx*CG2~⨔Z$08`~8aO:8R6[AW.ϰhЬ+ʑ>RR\NˑFV4h  1/Qx#slO/6.2+gBĭ~|5g9/ךc{/jK|QsN]ۻ'WR&L//XNE}u||K2][E/܈g.ӧ|a++S'OM~זg/S|zx/g{ 5#,2k5[R[d-q_ɿ ew.jB1XAC~^kjOO7P ZqvFeIy,VE,҉xY9w.;)T?ygSR-=/~cT _qTsa`kO[ v#¹ÕG'K/,'/c['iSb륬8~M|8?oB# &)D6qd5YR/_R' W/_\ac;/[4|3Cl R^,~u)%|!U/*d[۬c?ت3ւ50,{! 2?[~XBQ{ .99Esڞ9mP@i/,(_hТ tn1)V̄ᜓ(!bV rg)䭎,'+d9)KTY$mӸg)ˢK=B^CF de 9/Ȳ`ȲFWj?{FIo%˘YԺ;53r(@t(_hFvPc,d|y(vzqn5-`)G/ Z2&Ӄkp\q]B>jJ.t901:~ىgǗ+3;~ä .\Z7'124pO\fMk %D g BqO<Ź<xҽu 0poO%/xxaҀ7^¤#x-Ag'NdϿQX*bVj,Xj<2kWKT /zrb[XH"QM T5͆? ^~U:V ;s{v,>:*b%W,^8[ kآLRfߊ$ٙ[hZ,P<זcY?_R92Җzi3X+?,9w#y$Q=p\',[{2htc%zOHq0j$q༈ X2VaaT[,x>c[%e!buf88m|ʒ11_HKn*q.^%\<)%}~ 8饅Z*PBZΞ<}⣶޽{'>fQV'Ν9ydϥ1a_v8O.HU?hC OX׿lƯODnm;-k X6|V3Q/J&yڵ{ L0?E*m% Np(# 7`OpGi0>R6 ]+xW"geqH7=.,n 㟼JX+Hpy }ZoW$ްO:~n=~,X%k̈́u8e%+3_]G}!W99j,h!_y ~? v3 ݜ?lVkקCgPGeh]Jkۿ4.^CոiQxr*J嚒'ΰ~??Ҍ_A:)qos~#/qv A]Y0 Q¥# q^,p+daz"$$0Bnő9Id-U"3 j6 ]+E~%I& U@%A5Z- ^gEUꥋC8~K5}mע\7oa(W߳g9m-`Pr~ ,9shb)vCatGavxݕU{}='TBY6Ec7J{#eS\]g?ύ2NErmv˱E|B?bC; BMv' :8-tsWA9CK_g7odz0Ril1 K1b Y…"^- ggNHTLYt:Ɵ-"XB_rڹkbfv&pNx^&7)S[yIT(C؎͋ ǸE!,>j  f⡇'X@pbi'OʘW0nߑ^L/X2v\OiY/|-(;Y9b!esT, /EG`|twu!BJ9,6T_؞Y??G'}J129{3^PqH\\4@Pby[3tP*>-Xe}L3C>~vRv>?? QK_(J,_JB9]c q,,hABj{u4:V0,w˟RrlE L8:NPlM>(> yt |G( 8 nOG-l@1]7G%_: jK >Q7JėhgGYőس[(TY~?\/v<2)bG\EfWzـ ֪:T}ꖺGmg/UAJ~u$T8CGX xkZW:߶1mC:ڵwE?c"QLFa !pa#xxcZe ޳i N'/a!<+`$+Wvep!|1M`G847~ŏ# gX  nr"2?q,=њ>)I]k EeDfyLTdR4--9H 8cP oZ7ny׸c_ǙO:zd;޾ ,;'fgeM7=y@|$V$ZA0H#,t D+76ʖT;wn&``$]_Qch?VGew?'/eGGJ<pi:5 5?,P WbX=n[&^HrsD7.%J ulπaaF|r읻w7?Ds*?>>)eg|C GexsZ\N떝^oS$Jc΄+Tl/e|MwPFܑQ|:s Rj e?ʩg&z('{K|L{7ǐԑ-ԗc]V7wWn]JgKܦyVsB"3_Q|x.kaa&q>6вdWxjv 7H#[:쁬 =B)֜[5'g=ϿznB/`!k7m\6G8&*M|>!_)p߶Fʿ:7dv_)OD1pkaJC; Q.𠷑M†TBXAZ_TGp)a'Ok ꏅڢ.%X?}4>{Kid\'n=TtxXGZ-%I,-pp C)FQk~>^¹[(aFo#un=Rc5Mmd^POpݵa?ՙ "8:\=zvk!ln߈vVj HJ7n`|EBk(.k?D-a8+k"-Դx٢ J8,<w st/Dl 7c;t~FJeC(gejf,Բ/*gd)NKnҘn)QJtpgN Zf,ӟER:CMퟖ:Z߫̏(ߞ|Iڞg$JV.Y>3N:c7l,?,Kn4VjRqZi?fl#Q-ok2Q,i&%3U= y+kb_OVC=Yj*ǟfJS+Q=?8‹c\7Pj< oY?llE%l Vb\){  Yh3w _2X5϶ YЌBM.%+Zpb~pa;;H<~w?ۊ|Gr.xpÑyI'a`]7yWwdԕ. F$X w#l.ZuAe!%DD?Xy' ĉDz'z 1ܜnnC-^̋LT{lKAQL2]ov.7xmoګ3gΉ % &Un뙌[n}X`sP`FY/hbQ+Q0X`~\Nl!JUWߗs/P"Ro%"^Nl0g߹Cٙ(cE ʉc paQV sQK~0Z۵eϛӋãPon)B6x3 r+ ?<4ICqt] nJ. SߠP>am<;;m~OUϞ /-sV:)W:cWj&}ImX.o{dYpF}^HHe4wgߢ)ļ!ZZeQ BT:~pv;J!߳粊U[R]f-ߏ>h?JYZ?>ȈY/~Cz'nW,|JVg`~ڽ;AOΒ\* ke(iGB:B3\~u|BIhXZVWXt /4GY _ݺQ*?JX9"[>PC^/\سO =_cp͖tQ,ögOE ڧ+i[e5Z%slÑJO&>V<<V`~EN4aҜ'p[d?'e!~+Z ;sx'u( r%8?.7`v>n %Mfe6Mq1~HŚ/YS2(aʁ 6r5kt&FbbG(uJI)Դ=Lq`L/h3WǺ'©(>E 墱5gvf6=XbzOPW,]2̹'VtQgDA,Sc;h,hZߧh';s .]RKݠNuҁ>X[y8Zr,Jݹsb c;/ܚ6U?)d%w\[Fp[mA adB/פr;bjј4`j?}oUγy^T33q/Rn촻{ΫOe JJ Rf.|ungѢ(mS YP̈rn3 ̗.)mWJ˙=[>ɶ3(nb _|>"B `)!V+m-#neLМg=H4?-N.$r"(dX&tUmә][>UزRE6ݓG$Sm_#?Vgse?8t{Y֙tp ti[@aeͻMXJl{-e3(=$kP]Mi\ʹR`PRpf&y[+ı+چ|OgT.@ 0O(%_W},ZcV3(G$-gS?xpB/>_֤?[Q߮![΅8 1ZnOfqI3$9_R8J ʁb i19|e\rM?fA/oN 2f)W'R()X TVvt札:c{lN͓tTjKe:*~GCi9z(cie :_m‚p+ LdJ9)ۮ˕'VFap?_^ROvʟjtUvo4EuE[SG~y,OdzTA٘O C{ ?8[#]) 3 ϕ#0$ʗp;iϋCxax;?%Ǖ+Wΐ!5Em@QDO@WaPoGRG2qVȂg/t~9RɣXXD".X`H+ίi~$P*(eJѹ??3ŃS)He=T6 EJo;{/{2Ǣ] .Y_~-ӔuaYס!k/['7- AmLT[[zE`~ +r׿(z f!_:(qQ'Tr7LO€Yg %[(_b(qh>l5}Nl .ns?,ͺb,(^޿YN3fo̳(d-@rB*aca9;;+lCkK,\(΅%}yW[csm5 &{l;ΣQL3`{:W?Gv\/("B?rH]>/l^QLjOY8TYb B xڔ^+ⶭWȟz=)?(dMMjauℶJj'7:f&)M!,xũ\%fZݎ0 (tQh?/eX袙-znc˧jȿO2,?>q~^ݖ{Rbdc[>@^Q} k/FچxF?PA%귬?i;%-| ,Lu)|V3hVBp>%ǟC V5? w1r0,|rGO':F{Q)CZSggg҅!>|X$H%j)#+o菲bLaʓ3vJ1w٥[ONE%U&)Ԫ#dX;s4z|t~B7~9^?K[yR#Hu?o\o'Cvm#[+߿[ 4W%D@Y#'$/C?C08ĕy'/Ó8oVN4|\!&L hܤ+@ee4(~yځ8`>qG؎~ϸ@٦ӈ'cAИdĪěQiq{?!w`F}A3])F M?JT5kh>ѢXaH` 4[>Q6>ۤ(>~(Q8Н6v;f~UX4?{llD,>^* cR(;nʬém8P#Ehq6Y6A+ٲ?]ؒ5_'U]Z<OZEYg1 8pB܋FPm5 IP^ejzHL4q( ۶M·m 7Nj-tI~g0Â<eLFVA ^i'a:,e BBM֠B&$+ⳅS:j_ԟO?yŒ:~1 -iyGm-cRq =q;5E)ϸ.ev)zVTTA;>_G^;LiGO)jec-Ҋ'QXQRrK*C!K.Xئgh=N*RH/}bڸO0vjwn-32Yga<3אZΒqK d(Q@"EXiw$`7}+BM2%Q(_OCP]AK>3l3M?D蒿%]ďw)Kԋ7㳯i'n9/CmjqfDOGެPI;!ߥhs[qыzOKݼ,B?V5?]MRMW[iqԟ9gֳՏ|U]_Jٖ[ԛ7i}ٯr(mQ!'X>M8%<`Iâ|Pՙ}VVI}Xe@[_?TQ"?J>>2N FS+o[C"E߯UOl0Ot2 zvw(NJ3!/gs'qG+ȃYxJ!ˑ)}BhXgD]2\V ?7+J^01>Q~㰢OޔiX+WAM|18J9ɃsOWNB:G4#>S3hr"hAAACm+hV`R>d%$Zҟ,ګl bҋm:ޓ myGk^N`޴&+~:q@?g8Fu6ƻ\svf&TqƅACG)U+Y1WF-дkԟrN+Ne[s2~ѡRKSC5/ڟs a )(EJsoMBR=Vϋ-xHbw94,x E1gq+^QڞɊJ,¨-r,s&ش3jx]^(fB7-4-i$,Ըmi)2L wrA֭l/:'+R79o^D>ʸ_>Ko K"U_Tmxh-_D?,`֜,wKGiqTKZmB匶Qh(W.+򫿺鯐jtnj$'t?8ןARY?|V?[*ZIk<ph"#rhy-폼ȕ(ym>$de e!f܉P ;,P"Yj^)-XmaXwʯnţlكqSy'y}wP+ tR$?c+BA1BEJ9PL%iCzQE/K%7 VU?q_,%GTj ZI%m1FYtr*qɐ1?\)r!YCWf0/U`e>HCW^U6͓ڒW_Capci{5JB7S,!I>}M?[L(XӘzpWD?#cLl8ooۿ- pUwri㜆~q)1 aҝOގ8V_tAe9+Q8 Mh.<|S)`ÕCYf"؎:{-PF}j[XCp;/h?* 5m ~voܠU@^P-g9L0,?cxoRaɆEC(l# fX?c@\ːEd;w!Nsk^ԧ?/0|Ǣc\C^)ݰ**ێ(iTo/-[1H;۟÷@ ymJXaT*mܴy_ A[Sb1^Z9&P(jמB%'؎]Q7ny,Xk&.G1Xؑ3PGXo:PuAc~_luמgkl?Bv-w~Vn1G啟/aIs^Z/Ԗ;'M4pdlFNr  i8޸E}Ò8O$ ~;,o9?u9E (O *U)ZB(=OD)/D@VTӜ E%;KS׼!"^ |zc$^`-}Z G)%-[Y|ťP'56*q-ap=_Q L1e;Y"X ٣USZ4Ϝաj>|)b?8/ :ԽAo[c|R[êz^V?pSFmkdHH߶?Ln3V\P^vZ? mr=?i2RE?`~mfq8p7.Li7<)gUq7 C ad.O+xGa 6ҭD#~#l7˗![7*G5\AU:#[U jNq_tä_jXyܔH8DO]2*LO2A?.roퟖ=ud V̸  $hgP\-Xȿg aO+?˹j=bL [u ]<t7`v_S=l,S}6VӠ`XEFFsBT?"QvWTM_6+B*?-o_?r_Х6A*\-+:Z?Q߶ FPc+,k4PC3C_6(pI#ʱԀH) K~aU;+؈pG'<[z%dxVu`P[1Uh(W 0^"q1^ӵMIX~8`y_ޠϓmNyp? -ZX~\,Gr A ]p>Y/Lhr-U(#m_ K\m%M&XPoo_XSb<):rKb#+qCh=ZK Zˋ F}tS1i|.V)muQsݎGߴڠ1|?`8C#jFFBpkПn ֽb㏞8OK]߰%OyyŪ SsY6J6әƃ%XϠүZo0_'q9Sǵ_] f! .)%`xZG<=뉪a>8NiJcrF6p b5 > L((iﰢߍ^n1,p;H|lW_Ƶc߳/q{& BkzÍoB4m+E7Hg!;"˞A/gwe 5jڪL~gLDktZ٧ksv\-em柯%mߦlwquR57^sGSY9V7bDgZrrF].gXky$PST5ƵteqNd<ҵ^-\r0< {eY?(o?m'[t?<]xZB S~qVm-!~;R8~hHGCذg~;` ]ݕHV@' +W0? x+.4y]Xx(ge%,~୐s>43ƖOEP(F M%틤CzAɇk0 L;g' |П>o7ܱy۟8h|ۏ8q:|PϏong}ۂgeW[;׎q)v6h Z<?pF+rk)agaU"p\??4W'vgd9k.F~ظx?ᑍXfmMW^Q-p ~+Hw+㍋xa+}fJJX`'g:"8(ep{&vEk"TyPxm˱i7e5 œ_( E ?UՉd6j[>u,`,USIvߜ+^h>AƷtϴP;( b jY?[>< xeo A~T$UT J_6JSppQ[>5[>Y,XE+ .t>11"e t 83.:"k!|1 ̯՝Ѥ1cጏtQP~nn|y:Ohezę?sjJg.X􍍘UEӤ8*BDc iU1IWG! KRNcrv9q.[>4W\~à6kl%TۇG?z~Jn4ߨϺ_o?qbRY~ b~dpl"'~:#YM8;}KRpwq,i~\%"Ose@4~ncE#07]1h[2RWE9lW?x'p.iQ]J0e7ߐL3S2١G@BŦ&QDuSuC9E_v J? {;(ӏ5֪KGq &7*?}ʖeMMPa߸ۿl5O=g!2#8VnN;۽ oEZ^1ZB--z?Z[_k-gkb-9X:îס= ^oa B1oP3(E!?k_Khu7;ߓ 52_+#A+,#ݺ! m <2끜Hei8+L_ёC\_a\I BT,G$@IDAT+Q:K(h`gq\0N ~r nVnřW!p!ޭ׺ PM:H4Ib1K &O? 8D+P|-b],+(j{ֿݶl%Rzpi{-"!YV])7C 5ب9̴vmfG6t2=]nV /3P,ɍ>B]~ocV^5aaDP~iڹmuߖg} GnYc%Z0<#/0.2"Jxt ,qITlt_F.%-UxퟣUא?ks ߡo>۬㯻VBg4=I UAem0h =PtگzPCO Wa\VA 3`׭Ps4p\.=OӬcRT94CCYz|w&I xVFsTlTY̧J[+_ثj8$c?.HVGo`ba!r^΢<##kן)&h]vjޅG[gmZ\8!zeBO&ްL/19^b8x<kw8 g-ys^ìtUW¹2$+G!\bP!g~QY`' ~O&o(g+uvby9jT*" 8ķ^^{&LzT ͋b0B,Hd=C@;ߌ6ʭP%)1+A?2"Z6AHv !N o[SЉLo;Rr{F? ^ dq,ثW[tcB"eA' W@Omr:z#N Wv[$j!f?4B;y&~FyW[WѷGK5VT\q4 pYD'qOxpā'5q "8$l|.+oXCX˕$ G;1 L~;O5m`A3q"nޜI|Æ7&&WQix GЕ :e)2EVD? ?įJ?rЌ"$}w}/\ p.=/E g$>g5cuvF?aq| D?į*LtIQ1 V'ʡϾt'ܨwAǼd$ *'M? нGgs+lAqeQןZQ-~js \va,Žx ?at9<M2a҉ÙtJgpF?ē<8?4KĻA`. B.p6~lQFӜ8a #͊664_+g1[> JiW7U)?˜B*WJrICL,4ά`'}'jg?+ؔ.-›~Գ᱊F=Nq+X:Tv?xXRlDU)Ez;"Of*Jȕ13'&< ნ(P44mg#}#?,p&j6Wufv"FOa6O\I2"2R:mq]f^CěYIQ:o_]uMc߆F3pb4իWVd9#88sK "><mI7oܨJE/pɐ)TKlWF9xL#^b򩅌>ҏ_ !}ײJK_?Ro*OvZZf96-irg^|^*-h|'(_a0xlkF4|?;?Oa/l_LQ}U'_W'>mDlq_gp{5&—A?2x1t5 gYQnfr'zӉ0G"08Go윆[$o[wdQv.ڐIcB@<4a`<ʖiРI:yX¦X08I6B-Z X~m/e`(h bJUEHIKPqװy\ݖxС  =}ߺ-l\|o4O[ŷή|@_K|WcBje]Enu-!S0J5q1Z0|C7M^㝏'2?6 בygx* 0tAp|򆟰a)y:L>ÔxH/ˀ< 3q"tpo܊ |͛7&Ф2K47@D:q-sd# !)[?Oۿb۶m.@[&4^ʼn%|MīW&&[QCQ|z&TUn_~rbrV-yDR)m."фW?Ao1},9֭bmpl02[qcXL/SNOiIGoOClϧϟ'^x#\*Qncs>:t֡wO|u,10MFɟߖ}lۿm^돋k$9]?Vzb%~+tI|fG~e_ qBB Oq8lLaپ qI q;>c/0 kX`sqJCp'?xo~ ?\ى-Rig8*| L,Sy4+Q#%Zr"&ʼnݻLܵ3BpuUwo߽صsľ࿕/fQ---M޽{bNm[߾ؾcā[ቄgo׎ߺM~qiݻ; \bArzWǷiWc|;'>ٸe'w~$b+do%Y5LpPGS5Xֽۿ߶{n{wIg¸v}=P1QbHVv<:m8p<%q%N҈72 ,a;`x8/:n8Eqe8: T$1ODr G~JYq٨qY<,B3 `H7L/_68 ˗81yESmRb1kX4]E)*ҏF,K?Qnfm׫/K,KCzI{)z[eEt~]1cFTɓjQ?c˗+Vz۬gzh sN`iշ_G׌@١7!̶=ܱkGP*9=qR:5|(;YYH3%[aOgQ~"9=-YrgKo c)։2R}HW}JbWyFK.ZΜz:4ҖOh}xǮJJDPڧ-)۶oSU3kzKmc#'Nmko5x5Z}3.y?[>nȿlb}1%Q;oO:;g5|w ǥa >Ym|; v52q⍶xQ 1֝vum5'm3S;CxB WR2 (D??[ӰwZ6N]t~VsRT8ٕ~ǭxhP炁~:\!BHa˕!Ǔ2 ;]X?hjjs?kM>cy]P\_r<`kM8IM˂>=m3 ,`0A_տUڟoIwcq_a~XXNi@iO ׎lAB.q,w%zkoVkfdO%V-9-'9 cA Z{AKX3~5'Md ld{,5SS)XTg^s|Qe;9weT ߳5#l̖üpH|;wI!7]^_߶k0Cu\jPCpջԏE?+N4 %x: 9lhZHqTONqe;u((FK®q0㜞(\Ah2 OpäH3i9aj3@ݜBYyXO|7AXS,3)2_ 4~CRQQ,&Q)Ty7[B"&qiΩʳOzOUӏTG."{׿O_n|'x-gՖ᭓[aO\8L÷t<(S].Б- ?Tn_Vʿs#}>^ GْwϞe?e)CZoCR+[lBEFu'J`ޡ#ߍ\@훷l CB9]rzV W,]]Uh:5NUAY}͌aY{n&f#$?[񉔯!YEd8K1R7j7m?,Դ6.%Ot68P(p6²2'qp" |(s~izɃxI#߰qtO,B6wCXHt vldiU$x- e\4I  52ćE )+#G TMߥ*KҷJ۷?J۶W)`.zW?P(0VrLjWjOnpF|Nn i(V)˟qͿ?,jaca@UkALAY^_0Bc1Zې7)k"+Cr~. uה?ZrǏM \R0GԈb\okM+|SJ۵[q.Z1W忼i+PFDXEa\wF~8DZq8sH3.yoXcW:̃:O K"q.8~R@ X «wŚ“8w qtag)9qsU`B`kּ$W86n0!T3WBoB?Ք{ZN}(95u$:?j&y6Roh_]*9k_H}y…XoV| 58,Y VD% 8P'?l ay〳Y^}Ȋ$0?Wq8k]Y+#Aœ;qve'?xhD/0i+W!K "¢gMa<UV_#yW*m-[Ў?IJ tB'viXiJd#Bk_]Ѿ_g%-X1芷=QwDSsο #o5@3h]#ϥ k(dc4xr`Kކ"6QO@{!;Xv#IG0zB9B:4/ Y@[]wr dCY| 퀇^ӶezӦP[YtA,u3*VpCvMʦU0뢷E:o`kc?ùŤr#lHOo?%mq,%N!CCaX?9qgys"e/~PU;AK1~(g fHæ}(y4KdNj PI="[RF@;c* P_O* axɺbo@ge%TMls)/?I3T-XO ?&L=QPJm\\Zd^yqmH" pl6[.[8vmsz]:] )l$% B 1i\Z05rihu>/.:\i 4>'0R5Uf٤ D٘34hEFO9aagHIwd%/lҒ;dP!ٖV3#1'g'ɿs+cxo O|5 +lONIiK4|A_ۅ\ڷY#QKc(i|MDRrYt#]/z2^)<Ӌ4C|qPZQL lc|[ (\S`HL=Pl2i!#c3YF=? P#Oce$" "7&#/"BΟ9s##jb=/p2ӡoAsW>_ȃCז:[BN(=ێBGxlr {YNI1upJ R΅`6Ybd4LK?>6' `YIګF`Fئ)QGhh\Cb#Cfr2lg!KH$L&>%jL߹^$J{ 矜j%$ǥ B9ϟJv(d#V)Ĝ/$}"mkrQp}:@[vYlv-mǴpSD,dqP=A6N{]y}[b+#?U^!ĆmL}]z~|ӺP#yE/wX/c7d2c+:@>DG$50Pg!I)gBZk|kKIwf#H+g?F~#_L?Қg8S{9Pj\`K.ġsyh uSrйPbkH[Yu.5좭eu=yYK Lp%A<>?dy^r}ANz Ϗy//}?o] q+W!<`8קݘ5Z%;hC3 (*# ƎAge% <늵Fa̝%Oɿa!&3OG_9r˗/srWO~_K,7ixqpxԹ{>d0|L9ZMlX.tt| >;P[ɇvP8tg6PuKFtxBGx)]_>n27zϿ뗾œWK^[14_bDvE 9xo9du,A4thC:%籭gz_kl~qrgbCeW뿹9㴈\TkxaW=H:Su 5}MB :(Bg3<:VPLw/Ȑ#WJəg|]OAS G9ݏ=v /|~מw .lxFبRoy'm O`k?Q΃x:\x0(p(!@G9S" =lΏmӋ7e/hچiOO?G򞧞z+/qDg57HmS[K9bjfNq ́@lhhTӋ5>8 ?I_OK"#Zeo#ak\``d~*:Q^;4տw_ɷmd$Ēd D`#Zbj"ÓVcIlJgke.3YK8Aa VF E&Vf VƀR$JqZQ!珜rxdz='ɟ}ӟ(gP +/(+[&=d7 |rV<!/@AEC9>Sl3<{򍧿>.Юթ8c 6=mysW@PT4pLgő71bȩPFII󃉑5oZcC#%K@;珜re# s^]p#S{ dDx4-v@mAS__Ё)L'q6)r6ҋ-J Fzyuh8%6ѕo<_G?+?"t>1%,!aQVvahNar1} N576RYgsYd%$nE߃TxI6j,jB0'97&P`cfI̔%Hk.!ĎI??pCog]?߅率:gPWi,ɨGyJiǍ:tu 2Dcp N?#s~S͋2h }<ؠľ|lHݗ.]o~=׮]禎^⛊Sц e`K:68T6xemqty;2X@_Oo쿛 xlRs ->I/fK#H s< ~nNy'իVW>.KDcಌYV/Kn!C=վ˚s:0h 8@: F1dzزQ.lW^[Xy' M (G?Ï>?7Ngʚ 3 +w]]mlsxvJ:AgSu63YŒ3'7Oߊ#Hakd&LG͡i`r'}{}s[(U=WƄU0;p@yP=!#(%vC}:=J/t_{iw(uC,нR _ݖ|J:?O_>w[q9lcCb0ԦJ*PF_;;1 M!Rge1B怌ɇy'f[%#WV;珜r˗O䡇>/-KBdf.d?_S'%_[]+ܱT_O/efƾ2KR6 75G&fKHF8pG{~ߣ'~>Oue|K@2i U]n]2J9SKpxxPN)őm8Ѱ Kc1:r1@YU t 9lvB/t3xQV=c_rx+l+F1y'cE)y{QI;uT k6ǔ0\x>ge%$f#WŁV3w9#dΟ9}p}8y}cR?:/GB RbP :/xr\pW$:|xм-7ڱ3>Q:xrpl\رӶ%AZ㱓-ҠӐG-Pj"=䵫ÏA?y;3Ϝ|ju ̆W^ ?>trOr\)W JSwܐ}twr[-2fَ__O/Ư%pV4P?,bV\Xk&L3gϻ*~o{#o}_W=}.6LVxyh\!wwm}O:vC] B}(Qҟ%ISxмrP;ptcpl0drJY=uVB'ͪrWȠohS-WGQBlgUhjAuռKppc\1T@U2Y?۸  ?J͘T1!OdY37?r"2Y='oz-o>yo?{?]pEp.xf46Vy4T]d|w5چBG;r#-}u}Ȃw@O踴J  8(, @IDAT8Ա-eXdžCNP,o,@e@#o{?V_|ʋ'|_|j]W_:}5K`zBǁw @'t:bҐ_4/ܦ0.Q2Y ;A쿕G#Lj5[ZP'J{Ƌ?r u|'z'33z%$<^NQR,mvqPXڢmh.@]ZOe8-l2<c [ѵE r FǷ#\ہnQoQ?|/җwm%!{]][KP-}qQ@–xKtQ-{ KgCoҳ1:ZO:oYrm.!ԇMJnC:6Ĺ$a\дE{k.~#??>t+'/s={ʫE2J?Y6CK-~V?d3/F_Yd?#&fY;eUF=W9U1ϻ}G֓x.˴e?l];z`=%*^Q"6A@/ԟ`z6J}С؃L^|ljkzgΒeqG6R> lǕ%:t:(/)]]9 CW7G??ano= Ng}Z=uQ/͡E2. 2lq+r]`%#Ag3Z!oğf#&(&#篜?podq򥓇zɛ=uOq䳟ѓaz>]S *.Jf<7h)хGIS;J):S]xFݑz;}PhôIG$mFa吱OS>EZLCןm]sY&|e~*_zC?ΓN /\;'J%P~j6o߃NkMٴSenܹMӖqzKߣ3ξ%LJ+$돨 [$1\ˌ"i,J3_?G_+ W^|iP;~Ɩ;8#ݴ^tQoVRbf_Jvڃf2<.^@鼠38ڢpDYh\\9HХp˯|K/_˵}iuZz $?Y oՖ3h^mPnƷ-ʡD `bpOVg3GdMR\; 9iss+͙=ͳKׯO_{"˴xp9ʫUSW>Nj.pBl^r̻^>8w2茑k4ڠD:5@qk>(ycK-a@Xxo`o B/t\v!π{E =qF}mhr,<|BOMпw녺\{{W%&e} ҹ~ojeo~~u|߽d&4s/h5\7BC:BWŎ 6!2˕Cos^gFso`LQd Gdc$La̫s"6Yp\v*k++/KW}W7{o>ThOk:pRx<ܡxE(z@ף"?Fn; |s-mSm/Q`GOyиzà#]::j rkyM?^i{VJw?Tz~hSxփ>%S{y6<|h%t' Ѐno׏W?/oƩğdk!ǒ%J{?r7gMw?P勅gze=Oo` 47 8|romaO^|W]kC%4Cشx*e?` mv*zQҠHu?6WFܒm8hBAƲЭג~GIvw9tmE36CI2gmUFyx:%~M~BӞ 9& e?//'7O_vĹGKOX&qki:+tl ?etho6)b[=)Oydxcal;ԡ#p#-p ]KNg3N7 :$,a,O]X$2$Zbf?O<@O}<{zgqeH}J.b pb:]dŮupAԖ(y2?uPеM{3G~g~4hhDǎv {t66!;``n;݇2.ZϺ%r׆UV!g?6R({]Ed%6+tuJPҶCѿ\ ?oOֆkǵ$1zό!T{sd8;[fb91g=WsοΕ7'^IS9;ΜC~h<ʠ/ :CS:%%vѵݶIctF,q\Bׯrt<X3@CJ;KCؐ[7X}d[DW_|b>bh 9ۊ,8|J}|򵯾mYYGC3|p+Ҁ&tMm_d/?w3>X7?s,WJﲗ'OJɿK~ksnF?/g?y/]OɃ}Jl }_tէ|hB4|pME#{^B#DOA;pa]b_쮃f,r.|h<4ഥh[]vu~x:B/Nd]C؀uuz mP8n}E^ض]hz3 ̿eg͸$6Ĉe7q\<C?<$JEE_<[bs.Y@뙝=ǃ )tWUfp ACϔGP{_;@ ?'#+^qu)D,84Fۇ{udd!D}0xʬۇ>JmՑhg6|ځ/GO[v% C=KO_߮'o?;튓В%L=G?Y;rt<`'dZdWۇ< ]WK(딴g) CCvӞq?.cmۧ?lRzJ7\ AȈJ`5]%<g@9n.%~i63k|:6kA"o]} hC4`\]AvH׎&|Xfer3sgqpdؒgFs;X+{#s?}Lcv Np5|1ІLV[}lQgu7gğ+/?$J9ɿub`'ؗyw6 ;C@ڤDN6+Ǚ/`@;+t6 l:A~d_(n?<8Q ڂn62Еey؀ ^(> O{>ea-@/fȗG LcKBmꀥ}>i#OyY?Kv=;)g3cE_VŒ'9\_= s'ϱ;-/kD}M轎6\O\|A2[v.-!eRׅG8 FÖnd{ O>ysħF=%sJmӹΧ@F ..%8>;_d@Y)tw:m:,|l]'Eono'|ۋMpڂpM" OAP>w>8d83<@_xg3Yǿğ%n$EdMtEʜ x'JʿXǞ=4^P3a3x}+K]9lj]v|&u y|@:m'ud]Ե,|t k=dyԣs. ɳ@o,n:ўu:qGE@yDG*v?ӿ6cпiW@?4w:A/m6ulճ]ݏtG@&hd3ws=͝?g1?:ay-;\y>ޏ~l;z-`=hrY@kRC׎F[xߴK]h2m#>U{yhxc(Aģ|_-q탗g(yG:` ͇8?}O!P؀;䫧LGwt_ڑ.|G}nj_ry⚠#?s<Ɛ߹G9>Λ?}fW?D >ј{){:2q(/]C)ݻ)Y_yuT('ukճm^jw>];7ސ>X؃'0_b :`Gi8/:r<\h9H]{ذox M)# ]O?#tANK.:DZf;BnnCu)NrRO](Xd3%Ŀğ?ޙw7\3ɷKǒՠ$\Ne,/1uBHu|xow_b:aoW!>Sv9l#]{;Ј~S:ڥTY䴥׆m_hKڰ/ۊ]<Ի__xx>7И΃+c#e)m<2ASa0}y<rQ>%4.|`_Anǖ6?}䫋mu: ɳz?~S:6yݬ̿S*c]gd`[Bo 0p@i`RlD p W`Xux"|.z`GpFNܶ]mtyBBO2=n]ۣnK?@׿ghn[5`]Fpc_: -?ȱcf/?$2o%&v>c2?ryܸW7̯^O?i>:e;x 8=>PA@{m?pr=08Д2 :dI<,pB8ѓn϶M`[ԣ?|_2:hȭyU~+wgO?.E>8e3fJ4'o_杖?3Fw<;2@_/9/'h_Soyǟu_D?8[ ^#>,,Sv0CK7Tߐ/```, /R@]@  \nn %p ~ Mikv evHC:|kő[>m/%w] .crt[ee3Xe,sDij̱`a,(7G6jil!gO_ɿ_Tc_Ѯg;(ϑg e%__ME:$FOP_؅8CmR".|۫ Ӟ-5xoEruCĕt@(2QD>k]qX&][䢮Ǐ PO~Ш>EOZv9 ;PGWvw/ߔQHwf/sawwb!ǛHȢ-QgqC-ǧu]oI ^aˋ4Gr߾b6. 6h?y`?c]hN~d)yB-d?5g-_]?$HA$ZΟ?O±5G,C~<̋n!MsKd ?%voJ۸_ݗ~:M=)}pێ`E@)MY)c:퐥:#:һN׃P2>tm >2`G[~.zN|mmՁ 5G<m2S&s?A/APׯz|DV_GO,C>}.ce_.uC>ά]'/z׏`;s%|><_b˃_Kp%c.׃.4~v_wH?y#|#H+cd4?)~zmuu+Hz(mrڕNq ^_/z:~_ԣu([8U cK0l/y.$/90]_9i_K_%4% 'OCPQA]emk?:?}X_{D:ptGk]~tmY3sğ?gLdo~ctCܛ2wohs]%~Z?yw5[{=dd?:C h7=IGN{@GX6EN[}-pKoCie-e@]k@Vk;?|/V;aeم&Lo7<c(폶w>ݖ,wl8̿ğy_vϹZfqýqJ16_t\/e?$8>øKduzt[)w=OZ)^"=߿utю/>HUB"M?2ן>חnۏ t4?m-[_AwBOud8kg_bDό55b?--z_zl-1ތ:Аx:&!kS>:<&|}?vnC/ky<ݮx2`GO)MY˶Yjl.hR\}Wfcv2e9ǘ'̿cbγWױssN0OY5e0^s+''gB]z00FcοkF?ti> ח}@FEe s?WEFF-?6ۢm ~ˀ\>\i0^i6y! J tnZ}dCnhڴ}?u}Egp/O~_t?yΏ̿3ﮏȅK/׏|Wo'_\?ğ߮9{iǟ؟Ί?9^:BpO Oɣ2Z$5@T|>4#_t/W˿s̰u3&]YBސ 52 Al+`2m2&t@Yp^(գk:?=d,tءڳxBW6"+^i]C?ֿm~2Sb /R@׏v{{%۷ONL>{~[vn_9h?8cg\[֠u|K4w٧]'&Hs}HɿG=jɿo}M~2Y,S _/9`.]:_ZQG?Jő/2k @l]2 ZAAO.IRtcfgXն<qAW+.m׮cO.N:Hi,]`ge%8`L]Cz߱g++Kf7/q@Hq:`dָcu~;-,^m:ԕo-n4m.䁮=*S"@yty2v+*:'BX &@|Y]}_Աu(ѓQT;}Ah+'H[Ƿ)I?t=taGc~";q389?/7y__ϲXCğe8?,q5O;)|]5`~}5_!/t{QNJp }l _nd.~iG_8h-wr_: PƠ2!cA&,u;kЁ[}"-|a >DW6j9O]Y}"' :8?nڸú;Fځvw&̽5\0$iza`8\?d{n0~8˭[=yk2 (=j_%?]+[b [mnlm߿>ҭ.cփ_lŇ_QE_yA+< [uՅ.&Wƶهy@]ڥO~Cxq?,XG<@__9w*JIٝG&8❟'osk_rc?b<N݅6(]G~@:xo6{FfWx>(c`πLJ\|HݗM䁵(_YK׶/ 84Yu~:hsiCXz[73ط?E埬_VB;e=í82ˏV5?4vg8=1}:mdܼ;ۡK% @>4eu9c wyp@.ڠF>>c{YChe23u;'?YҎ:~18"-Q9Ϭ_ۉϿwܿ\9+g[>"]?ߩw8!?[R߲\s^Iow?zH?X52v3Gױr?|כgIdw+]bSG_аOk[Yubu:zm``((]:/PcV/_c˗~_ԝht~Gnyp{6cݒ2__όċeOFŵ6б]ȯ/PviY6;k)"/9uý8 HAyl8оddC 9m@^ ^hK]N]Xמz_Pg?yʶ+>h+KI=ǚb5$J{[9q?gl7y@\?\t;-Od_:?vۉտ@ Ae5pAljѓDDtJe)/#&}zCO}{o[)_3x渳-IDAT0?1?$H1YɿY~'6GoNeШ"E{owt;x@o'=L_:Yx9!Swr#C"⑇xWHh{9}2k 1c{h);g2?sO5&.k?&HwKN{{Ǟowߕ^5G*\r:럶t_WnJ^μMε/@ADRE M]p^u@C?>zS,۾9qDo{{3?3_Ond?ϒw2@_K?d<降g[of]Ӈ_8+#`ǹAgdLðoF^wA+668K]82%+Sy::gлm6 j ᆒ:G/:|p@cR=  Yd'dnF6~vWEEOysK'Ef{7rpq=}!a܍t]ԏާ p/CvcC[veRPV^uE /}A]=_rNMtŭ?>n>xh`=0?_2$//tl0Zf;d]Z G䟧Xc?9+h1y1{Pc/sLdn+0ٺ1,J'"Av1n]v< +$'=x n/C =~ȿUiS/_'c믪753)Ă%cK?Ⱥ N=7o5r_ymph_(?3/|CmYґ;|q 8m+]>6}QA&Yȿ:Nyn̓2̿e\;1cNϲXGkI`%,c35{|LY('7+a1'9NF?]lh%m~2~h=@/mrnO|/;kzOe)Y_:tk,k oi.?ovy\.bk7o1Rqr)3hw_ǵ7^Njrnj22keWw:Sf]g)̸ğğe6$$;сutc:ޮ2eyE6iCF}z;-m莆9tJկ*^3xE>ulPvX,YI)ķcB??@g3ZKI^/t,$J;xg+篋8af/o_?.%. ڃwǀcj[yМ up]zʣ&x嵐fdz̿?/'wurrt es7`#W_ɿ?;ϼ9oEumyRZ됇Gɣ̺,֝tn^,|Y_28XH%yN}E-dynֿ}mӡJ)[73>_g3g7Ns/sVw};?m~ak}E0ރ,2w;;L_x`BO]1^O6:gG 9}I?M in?v΍g΁?Ω5Al0'?&OG_9+no܍ti/`>}&fѝh?|_xR3oP{-xc+>kS"M_is+1]dğ?9HNg?nJ<{.'Gb#矜 oxr sS#s׀]ӡMGo:!YG/K4$COz'Uw_=(GG:pwKIw.5?ц%Jvrnw];? գ_wהvЪ#&*RV;NrrKkOL,I"Q BA?/oğ`nLM={sdM+g?fN9yƉho a(xk$RT-Iؖ=c#=)q~1 @ 3<Ğu6an; Y:{;11n[b2싿'-6y^׾[EC??f^U:%YZ_Lc'zskh짝g1~ƃb1;M'brO6^vfއ=44x>s|:?ߊo4ҟ|w ~OYa=>[p篫^7,,8pBl&v0k:Lgcu:v^*֣Bͅc 饿<2/,/P!PUUCב5W_Fs~HWNX=O:5o#hrd̏soL5.ҟw(1BK^erV~S)cƊoqA+.D|ʿo~:")3P67(8:f;9׺}1u{v>6(;?rm2e G0&79ʎywv &3gEB|8fŸ1d<٬$3uz7‘Mc3M_g +vnGKMaZA}音r% sMz l-=4u~l~s_ˋ@ۊ"Y̧csYs>_1/ڌI_hr!PĮW9vOW-UH3,/,1.ZU{w}PXccEEPQOsY};|c ?Ο朞^MLg~u_Z+棾>Y~"G.2OGE]e^|WثOaK7MU/ZϪ>k}K7[X$`wk|{ixڝ{ ?2-boS_3^OsqØ,??ZOxa<νƽscZ8,?j>1;=i=;E _"@Pg)JMhv^ǜAˇ͂D3QK|_(ꏫ+KQV\D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"D @"DJ2xPts:IENDB`slidge/docs/source/user/index.rst000066400000000000000000000021701477703150600174060ustar00rootroot00000000000000========= For users ========= .. include:: note.rst Slidge is an XMPP server component that can be used to send and receive messages with `any XMPP client `_, such as `Movim `_, `Conversations `_, `Dino `_, `Gajim `_ or `BeagleIM `_, to name a few. Your contacts on the legacy network are given a "puppet JID" of the type ``username@slidge.example.org``, that you can use to interact with them, just as you would with any normal XMPP user. The contact's ``username`` depends on the slidge plugin you use, for instance on signal, it is the phone number of the user you want to reach. .. warning:: Slidge acts as alternative client, logged on as you, running on your XMPP server. For some networks, that is not a problem at all (signal, telegram, mattermost), but this means breaking the terms of use and/or trigger automated security measures (account lock, etc.) for some other networks. See :ref:`Keeping a low profile`. .. toctree:: general register contacts commands low_profile slidge/docs/source/user/low_profile.rst000066400000000000000000000015671477703150600206310ustar00rootroot00000000000000Keeping a low profile ===================== .. note:: None of this applies to networks that are tolerant to alternative clients, such as Telegram, Signal and Mattermost. Some networks are actively fighting alternative clients such as slidge. To prevent being detected, you should (a) activate additional security features to your legacy account, such as 2FA and (b) establish "normal" traffic on your account, from slidge's IP. For (b), the most convenient way is to use a proxy (a SOCKS5 proxy is pretty easy to set up in case you have ssh access to your xmpp server) and configure your web browser to use this proxy whenever you access the web interface of your legacy network. For instance, I use firefox's `FoxyProxy `_ and these are my rules: .. figure:: foxyproxy.png :scale: 50 % :alt: FoxyProxy's rules slidge/docs/source/user/movim1.png000066400000000000000000031112211477703150600174640ustar00rootroot00000000000000PNG  IHDRd$ـ >iCCPICC ProfileHWXS[R!zDB 7! Jb/ ]T(X#v *ʺX+oR@}{}sϙsgqDj/.ƅ0SRӘ' 0@(vLLe{yw \h |3|W%BrbRDa:R 8K8Cw+l8@VY/AYς^ &ľCl m$Y?dM3cPʹ( 9PT M?K~l58anNcu{QkCA$PCReJ{Ԉ_9z; xA,΋P`.pE!^ ,WlNSB2?˓*}ݗ&U\>F/NH e() b:N*Qٜ,N%qBqHR+ʔǩK mqTxoavB2?X+$t)ssǞ ʼn*€8X*ɋQ¼9okAQj,TRϔ$(ċsxa1x p@ ``x < Y@UdE^A1"!("dWG-RO  ăޒcȈ+ƛw #$`nxꂳpρy|' v;;5am9\]k["\#'+dsse_p 8$SB&~L4|}U|7?9;v =pٲC 2i|KhfLq AH`pK$0 % ,@6` ap%p ܁   ! 1Ea!H!H:2 !ˑ dRA"'sHr yt#O(:1jGY( Gбh:-F硋5h5m@Okh'af#8X4ebRlVcX >D3qGCDOg |;ހWx/@#^.!ED(!NExG$6DS9ĩEuzqbD"H>hTH*!%$#]&u>Ȧdr09,&!w/?S4)V/J4E@BYBBi\tQ>S6Tj5:ZG=EK}f&RFmYjյ9ceշWFYiiBbZ $>AwsLz%~RaQQOF&EZӜYyPfCkVV"Z紞i7k~ Ϙ8!purttvj&N֭=۩YqջiᐅC\^PT^'AA2F{a$ { Z:tFQTFmF}&!'{LLMrLV56eLW3}ey5VfYlYgsD9,,L-,k-o[QXVVXNohF߆kSlSksזfg;Ѷюek=jfm_iupw9sF9LF(iNxd$ASCIIScreenshotL' pHYs%%IR$iTXtXML:com.adobe.xmp 1572 1124 Screenshot Ԡ1iDOT(? @IDATxkmUwM7V[R@@jcB@D" v@p&1؟cy8&!#!32 K"0/aPH-[-u773ksN[U֜ݻZs9k=\{:ubh@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h@{=h큣-Gimո(/O8Ư|+FT!!dCQ]l+b>zOyvο:S#_y<@{=p<tJl:}~K-r`Lڶ2,1^L?*ǼB̯DzSHgxNbqvhn|AOm*C:ڧMd%S|([eC?at3uP|([eCg0^:t1uP|([eCg0^:Տ)/o%d_vb Wƚ~-O`uO fQBe*qEiG V/o%d_vb Wƚ~-O`uO fQBe*qEiG V/o%d_n ,krm^ d)&)kLx_-#QI髎6+גqDiSI^1^j ;4(cW>yP'qDi83W<^ƐSKH@{{;OxU)/uhCDk YU9!Mxaok6zmۯ_52˫rCamW]W՗˫r؀z|?(׶U_#c*G??w7u3 DM >,mꒉ[=*7ק}Kx!\_Q6>^_ ǏqVKŘ/ 7P~+<~\gnz1E۸L?1H_駹h,NJ|s}G #1jI6:SObhHr.f94OC&G7^ICP^;X{7[F[jԽz?coq籠9_OCbSz/I~1@|S# mԥ_{wuC^ꧺ\cHW>mԥ_{glz o^:s!]S~tJ=<;b]fn{uu꓏u\䣇=}bk<'͌[:%_G_,|}_~^:x1K?Yz$f/t^Ι b<0N` L: g}BK,D6\дmmDuφM[yuiCwsc G;2o'F uz0gvȕw/hC ˚KzxC}1ú{p?İyұ7_nL`7CYXr/m,wǂAE[c Iض#98~ֱA dC97?ok? .n;~M;/sm?t4~x. A เlSrAdo98[THNu0Oy`FFuV5?Ѱ |9ӼJ\o;_zuLj3>kϟcuE\oO8k6ީCSMoR9C[Ǿc 7蹀ԵM Uy\SL: x׋<qnHmJ2owwo0Zv{mVq?#5^z??Y?]M(7B䁅 j`SGև!_PGFm[GzkЅ}mi;[%1moc=zu1:o?W?7'cm:l :!?lMG|J E AD-(Pj6=>C.|ؒ"qXw|@v~@^_?;zs:t5; l~w bXG?GN <6$WÐݶiyu,6v}۔h ^xt405ؑugN')լӮ=p?,Wś{_|p U? |UWhwZ?z;juj==7c9y[mG/xΥ/S,qܯ屠\\.ą-9H~9ji,.lQGfJh9K .p8j|?{#7fuuk{{I:t{Z~}?ɿ8-mZ?(o)>DwwAH||%;zsOοGG*g?z%bnܶ ~]=9o{ihK23Đ A΋ .٧^Tbs1>ŀW:|masn~Ykoĸ@^}&wbϪѼ;?kDn;w%Oƭ\c1kܴ5$|kP֠ LJ`"_̱1ŵ_//4\s|0[-SX >]|G^:3WY<G_ϟ t~/rQ<Ӂ G>Uיu+xwц\>uH|Ɵ_?oA]:tyysߗٷrc~乺b:hZ\8!C۶lrqR+. 30ǎ|e].J?/xN.Խh+K ۓXy@YTlhf:-'|bW>k&sQ҅.6_Ǩ[7_y7An?׽e^>v33qY<뙣Mڭ _ՆgFyXV>uh޷C.FZ\ErԡnkZۄmxV l2 R>m}q?y{Qn$>hս fN;zs/{ӼHԏ]qcsO_T+>r߳"<Ӷ:hs1-5h 4x\Rp C Dgp&9>ڢul99>}u~]?_9_`[W‡^Ⱦcʽmny'zt:;Ա'`8nx^?ZN>. ?خ}BY d[6>P`A&|9i>7c8w;Vy5gz]o6?{:Q?>s?ؓQÐۆMd!uw/6=pvDZҴcsɟW28 GU~xAFpT}U 3KL@>>(~y>ο+W/ G'1q `S,˧EPǢMcY JlTZَIy3簫j݃h__kA߹F7_;?QЍQW5~0`qA/Cꋖ0s`|/y3)ɉތo#7՛Liz[·ugۀOO! )ėmGg}9C˹W|s=)w6|\+w jOP;z:d;οY_?^tVjIwcA;(>|RGz}qE^]~`xІ*_YMEN4s77vC3(!zJƞ.9>|_,QB"7M+>ũ;T<A?:| _q3ows]^:v>s4ol{ק}߳a?84?q阱'\'0$lW|E?z΅y$?uM2sBܴzk~OYo>}} hr"ݺY ^GLr\vѶW# >_|twWF lss|ο?}9>#2SB[οuϯu"OMćy/X/uqU?/\Ģt*{?I {?ȫ_16˜.{:LOb6xo ՅIɍ2UW9~O m ޗ:Nmw v_[tsˬշگ:I[nces|7?7~jo9O?XyϿܳy>mz".44х~QGmҏmXw6Wç>u+m}1~eˎ',\ 7>u7Q>| hS^>v*><6䨋o@V}iq+~4#G?y+>>=j|Ǒoɚꚕd9mx_?WχCNtٟQlVO;,D3uŵLqP|8+x_+؆~|mX?Dobn7yy#)4o<*oPϠ6W1 ŋІroWmI`m]M퇏T׺op1 ? |ƀ_]Ozϟ䄓x|>CĕɘxǙ:Ϧlqo:U|3mstDk>}ױGg̙D YD793(s"4.m @ؠmT,%| ڐ6>|`o><ŧTQ]G\/6W2?׮M_Ͽ:w_?^l;s1<ՏjC5s|tםj?̤m?8~[⻊͍}oGV!'>J?}ޥqB5ll*L7 P$C# A_/pȠGR|J+&0En#+<-}2NćMqmg|+kq8omOq* |G\ܧX:VCnwڪنj9F/D)o?|uL':iK6?-v?x#9u~կu!lC?z6kcLS|r;k aGooeC|cbsu7mB~9ma6|?m>oR91C[gY#̅Abx3&S_&RrѧKI$7toJ} 3yeO޶~w$E )O )W}gO~yeS4퇉?3A31$7fx33ꕼWebBO9$;mķ\ 6lo"WWYd,"i>}SV%lŇTYtzŬg^c4]_^77N:Vz:QW亂w5ϐ{9G_%-ςk>d #O +&vŧ"w$1hPTy&rַ#'mQm9C: Pup~F76s82Z'}l§6ג>APy@1vɱv\mWF\Kř3#S\K> Mo^:Kο;v=W}kI]\K?ĒA?M߆}.D T_ǎϐ!1&}ڠ~|G)>%dh6MGf 8Nm@}>oԽT7 4дMV⍞ˆĤQϠX6q#GH狀7_ F wؔQ\.U|7z$r<߻>̳Ϳ`uY딺l?i5j}󟃮2)_.,uq-ቯ_x(]hA9oglhϷs~ӷn}dB49<{kpyySqui$kG:%/sv>:Es#S9sg'~T !K]Jm9be ?zkvwwGOjqG7gןA?<s!;v%vyt|}qz<惕rֿQӦG=Zglc>u ^՛ǟOՍjֵK9gs|t!խ,mlRo죔ħO-1г@: pi:_d$t*il]\o:6 *k]<:N(kQ:g^:KbU:qGϱ]*W\UԕT.$:cx)W)c۸&n-WXhW͎Z$tzw3~_'1"az_q6io?і}e׭9GSGLߗ9ڶXx)SXJySo/xO;qc /|N&Q7,o2 >6ëL||؀W|+ [珽?kGu;u_3 ~/j/6JŽсX7yӦ_mP/^t-猌իiW_|⣷nCs7?EŧO}?|N%9S98 n&eg>}Y:uGOvUr\uʊOxF3owso?{^_bF?3ʦOlSB-+ O՞/J2QK7Wȋ>0-Q'}оOcS]c:6N9S5 iϫѝ n>$ n /Xe>-ѫx\CB>֞uUW}1)rAMymlW6]M:--\~'>%=*|9Wu:v ?U[E0woz珳=|Qu%x1ǩWlO)O.u2}63gu?mnG?~x#.u1Ro9~\w7{wuB6A׿zFA^uCS/>u.H|2%h:G^YKۄ|{|toYv]MB΀ W矕:[cO?<_} ?vz6G{ƺl#iDzՅOm˫6˅ut!k]YKdůz_}QVYm)7|aY;T's*TQdžøq:>S9 . }o珝  n[|q>Cg<'N:ā74: gqA6rXep8zM@}ln-ABGcg@NTG:m#'sK}S}#oOrӧ\?ic/3˹\+~hڠ-[_8_?ѹZe^L/M99VϞ0?0^;u̺ <Pe~^Շs|p?{|&|*:ĦDR|?h;M良-d m/:Ȣos`<! @Nq8V<{SiKkπmq߶7ڣO|*/IO99"?6(]m&| qh;xBUslV6*NmW>͹o[u~W|d!ǁ=1~C&a\cK 7tlO -JHx2h'>-PWB>?2kSj?|tXOOoO֌w_o_r)T uwywῃ<;cua" ;uC|x#?ǧ_:_;\AKG:؄觎ͪ,d?:GN}m&}SB>mBO91o?b|#sC)$8vHu(K{]{C;bvG~>.}RY>OZ/bFrAiu51 Wћ'&ze{y`GvŧOJfwi{ag _0_h_#___mq|F Ͽכ_;m6:J'_| Ky!瘐:9.8bv:^珮zus9W;+nǎ]?A_'xB?;Kؓj>ڧ?{YN7?*9}Ҽ]m{㳬v*|lv|xc~9YH*O]<8.05ctޖmpk?j>>ͽŠ_چ_KkDZ֒`[*Gf~ܱ"u͍10C['b;|SD5o%ؕ_2hk=.T*ŎE?m#OQG> ǥ]@\^}q=qAw%؎:)үQڙ> q5~osOg1?z]??H|qVFZ&_!ͨは owBʎ*x q}u\r…|+_ث_.{n#t[opŘ4+n Ѿ%]t [&c_CB0yK+,WTs_ο?nc#Noc*bwwb}~W;v~C=&}>l_H1^ro;שּׁF>:['/p |s zc}GyO|}+@κtKG3']+Sry_R~JH|J}XUel;,|s|MDɁ(^ۈU}{jBLC)z协2]/xR;ȭCB947x__~7k_%o W//xJ*hdt$q>?ǣ׀.QLz|ȾTڀ!#\6?&C?^3Rxﴣ'F2Bܧ~== |KIS٥Y+Bئ'4ciLѰ;5~u\,Raaldo䉜Tf#~L4剛x1kbT.$G>W?+/_}qbsg=N>Jy mdiY#Dž tk{=_0VisA@S~W>lc񑁏^ű Ɖ7pqDZ`T7F!=s=xPG`{†:|!c|E_ůzk?a-K/}W?~.\_x~g?xO>Q3//^|Oc]Uh،$DnT˷C&墽XfpvD# w)dnp^ ~9zJ;(WM0jw;vM߅}J-_M|i2+={ &5/0[j`}cIg匬)0QgV0"7$hNGo?/cA =^DK..oziq/~o]|mǹ_o/ߡSq}$.R\Պ@IDATqp.^%ڞQ< yo <bPzvc?|d\u\Ա-'Nā'@)q7q>Ti:]l@:^׽{)!F];C#pďjvlb ]Xڡ*X,rTW꛾}w=sGyl'\<̳>^!,HTWyЊ7 -)sa+1t6oV#1tO,C쿱ϧx<4v/g=wܳG\=NP"xOϏΞ0y$qS,S yaD DDu?*O}m.|ɝ_+_E|O'?w qqxQY=BpzEO裮!ꎧ|^z\o[--o~gn'Y<gzf⋗(d0Y.y ~1?~.'Â[fV!z+DOE~>TD;zus服tW.^Η,ŝw޵wOƅx1?+kJ 0]=mK{>j9x:AS{UYmqUVzG1VMtZ]W"s"kDxN@t,<@I҉\oCA~7ן{%6ԇ_>c"!u0}lóNI?b|w/}Ã>I_ a\LM$Gu sH!/ \v#5b.> ۼ FR&ụCI&ȈY#s|h/ʥN~oBrϧyhBȿ”h'j?) ~a#`>_U398EB*`9>̓T2P˟# ˑ9 螈uV?>m_9{/O|᫾`$Sexi!/)-JO]Ai'%6,}GG3ȁwj:x ]:w@>zI|L(&,yhs!UIh!*>:1ӆs|lIR 9t7WK'3y&^`a4T za3 ɨEeISF;dS`T'uo_?{9ؾKV}N)+g|rI,A i>=}7|' %6r/oXSj;ֿ<?~ѧ:h.ySX6zr{5_uoڏ?>+_PO~1#й9mE}zY4Ha/Np'Qj_ĦϾ|5U%lNUP%2)=K;%C-BnhL„rQ6,N!|otŲ ?tz_Eb2Tڎz+B~S=}u(NdO?*_wq╯wpW=\pYRw|6˙RBy|h?XɣA%v!@]|Ͽa_|tėg?U<~ţ\H ,F$ crpdpUo(For>C[.m!Ljuql_wwL|2橧gEpR7f mE "L3`ڵ:j?.g0шV_<'zy2Ʉg::%3k:֝&HP|fi5~x?Wʗ]/`ywdR'cY?)R 㴅qdyVߊog$u8Ks=x/xSK9n-y_xơ$)'OE-}CW{e.'e^e/[/?xOltqqHϺdd%j YdH?zPV{bCl>r)Oh~yo>S6[LK~6>1I9N2mfJ䂔 BqPΎ^:3&ظNjӏQ"&??16kgsI{pG?9$Trwj,m6GWZm_cLY=|.,. !i C~/'} .\Z{̝w+Ww_/%{S󇯍*zs|N{iSg@hSB7U[(Gi9~ 7=0r֙[խ1Fl62_}_#/0ށ pr:E3㜭h0o;:0#`b㉸#^"i&o?MSNIi/e_o. ɧ~h#3zy.?|o~M|z8ȗdOX/9)0OLcE\3X7%w/{3<'K?/aL^dA^OL cML~'Mҭ-y鞗߳xo7~=YdIΚ'>u>=~Ke~[[PlK];!Wգi}BR:Gu}a:cc CNЉ1:y1\(&iunŽɘ{zCӟ@M!L{Q4?f11AvA/ +:>Jds} dPߴrƲ`, ,PJh9K;YMo?k'e9ğ.I1OW3'  _}|%/n\gΨ.Ň٨mϻLj}|_euCr(W.%/=&qq3z V?9or!)iwrl伪,AC>%߲Wуw[ z:zrBGoye RVXt&Nm4mQb>.)kY&lt|$CVP㇃X+7ڎ^:FB`?.w|Cd 9I,S2W,/zⱏ=x /DO z^`ʗ*MHſę2</6⣯CX'U0I`RmN@ep)O68nՑPzi1%ˠ.Ov튇Z"OWه1>O^ /=xr@)Դ |)߅KʆthfRĞw1QAŒ0r?`:hw7̌˽"*<_N,A;cx9sߏ?s=خ|B>J^(YOё xh\?w{o?|fƧcϖHA;eF`cvX^t ?CK/L<6A6rPHڜtKG^Ψk׾O:v /ڢFե7ZmXJ'sudB;|h+:y¯"FA']r3mLk 66l rQA X:dI{9WGb2ו!_?[zzϏ.b^@c(&ܯ2tG7-xђH~?C.s':p\oz;]8_'K E !odFAr4Ҵvq=_r~g>cոPBg,ɧj8$QjPAv])KK|d!w&bPVPB1?ŧ xŖ8nGGUN ,&s)qI# J?6ѭQoFW}lj:q'>˱:S?DC7=뫾וԣipQY s~~eyvho3bd]RZV;\KV"kKD󏩹/{N\Odmoϕk2ϕx1ӗ|g"xz.c|Kt ?}^^$';971z&Q؄zl11$ΘV9Xo|S$> s1*1qr&S l q劖Yk[Id-$3[sw{cF?Ȭ,q7GvҦpl!նOiQ"7;_/7YyόGx0L20jQKӈ) AF +ʤHf[d7x{2yD|~#>돜o?{_ b?u껸yA?>rü&?ΕeVedIaE"&I?U+R-4~_矲mt c*\; ]2|JJ-[>>!{~la=@69^~ ?ݩ`N'/=I9HVJߴ{S|B "/..^OIž 2v筈yy|]w-x,/GcYN?~qΜ;6;)q3gUλq!qy$Jeq;Թ ݊]z QbK_Ysҫ;c:INs Ͼ:9KC?&hKylPaU[:{CF+A>.H,ldLC1@_;G0o]w5}`!2 *V (x6E 2q+)?:4P_ s!(]AibRO?cth^"!DR;m YLuRؐOe{!77a^L1e K\ock[npȍ2C`xoL`R-% 0ڍ?dtdh (KP &+댭79'deL<%ldrF\Qwܾxk_x?sO|:x-Or깘}md%xS6wO6|H[e__|uӾ`j_|+:9W)#UI;=p\38L2t u>Աih+'&|Cuy;(C?Yr) :WQo6ug (qIҎE9QU{m9f,Yd8;T Z+"_!ibc1!D-M>ZbMLH#v!xrOľo{G|oO쉳ثn .؃|5ZU:NG3EvM6דm i|XCo|'D30F( ::biJ|~Od럌/_X-(eǦץ[o[|ٗv3~g~G{+gWΒ:׳S~CqV圌>}#%Gڧ YliC,ukcA~tš9ç_| coI0m.'cLȋ>H>NکmdեDO9j:d)6 6%< KC^#O?υvG57ݷv>xLW$SijD ?MH[If< tMWH6~_)f'va+z/%ɘ|7M2!'^/ߩdo%KtD+o/},f3AϦ.'`];¿/`Os|LrE֧d c.]x×~?O{"b[2ng{nG|uqlxi)9#5:ӶMΤi&H]YuqBILYtJ:^W[cқ8KT:ŷ]qV#O};]z8#Ah'*n97xP?xGOd/Ra)ym ?oJ:&;doy~yʗ!ُ2|:K}OC:%7@'TM1!CNx??1ϖp4>)OAKlʿ˾ +/̻~_<t. Xx'y♕9ON]h3xKrGů:vS:Vd m->![E%ADhDijtFG^YլX+o[*6 YYG'֑nG9x X?Yġ4_ld ٗ5Ze7U$ow"~vϴ_x؉g>Brbğ+]2o韽n^͖Nce0DS^>@ ~HPϵGL :"%O % َ?NI?Qޙ9!ԗ?W5/f8(s ds@<07~ ?3ᕯ bW0P,csă\U>gNtNWm+Mk>=`aB'sc[[ћ_9i*KqO΋~e,=rI΃t*uozu"||mTǭ-`x)>|t<_m)m>⸢tA_X*Y@]@?AaEy!l/?6V,i];F -yKzTd̕ w?{w M) E9)$lRM}|9?[x2i/ꍟJ '[;6Glg#~;ƃim8"累7 %^\2gK(j?_qߐ#?Ϟ =8(2p0I6%A`gR J0lSүm6u϶ cPFGݶ >s7'lJ+ HbN IgB!DGBGXE W_`-u+dP.!ʊK9mc~WR_PJul[h>ʃ!#GCs::!duc] 7@NGŸj  }6<d[!3N(WB0-/^D3xa6٧"IE"ƧbFO06 5Cw;z":D<ź:͗3'K7B&6qG.Zy6V}D(>n =<41 lPQRW_ e-w `X|.-΢e,,$,,$'.#9NgDﭙ{ˎWuu׹UE*s_cgޒ[MeW ƔcA.]޻Bft0P[% sGޠļ|1D:h|vAb9(.OzybLt,QG|+p#&QFtVq|[23L/:chPp0R8+;'~;O?iUr>QhLd?D}=*?eǡh#F|Pԅr77. dDыTF e_ذӘԯ}0LW4hǟM[R,WWە/gۖJ瞧[UqP灾?K?<2*ܷrpZ@̞V$' T 'gm7Gȿhbqu7޲7;?FۖX)ٖ?AT,zյ vݷNvVKF`~i9cJڛA1!H~%~̿<B>Ԓ4'8ͪN20FAY҄P4J#O7MɂqIC#xq?4 y6:Sxӂ>u076u! 4#@/< hLcrȄIy`bCHJ@hxT-P ,Eu^#S//?Yq-qSc\8^S"-aax!L[!C}7\oYJFHCNXTRy ei΃ 8+yxr8G'x&D}Ђ?ÅHI} eA7B  .~a,'|aV+c4CߤWkPnq*JCGֆfyLOύy5[X}XgTǁ)6m'VTBeK‹>BrD+%Eq5%Ŗ}Wwu 25kJN9dxtG i%M8,628>e2e)5Crl8LfĄ: tIH܀Q^B"Z1Ǎrp'&'x_NDL.eA*UrE:drI/S!HpUrDy( 2 N p/EقH\ ClezBQ#^Q68dR=|y_tqHA &P).&`*KU_ X*!--!t $` _E*\d8gɍ[ '92J[8g8pNJsJJpO!sz%` p=f-:5.h˕Z4X.2e3lWFWKy"IL3_y3P28ngE>U~Uwu2CeDp 61ٖ%.W)Rmk*/8!B^>P3xy2%7`)#Ms҄)fe:"y"bC7 qĕ]'LWr|/@A (#^ub|](H( pX@aCPye@88|?qPWP.q >|i:Z r1pQO]8⬗O>f衤 < 9HGxolψٲE S@%MRJA 3\"PSp QU's`+ /WF@X/O)%8a D9ۘ})ΐa"3bTފD6&@1b5uaPy)i1Q:>aj'|+ybBܩ7Oԑ&pA2p~N9!!zQ'S;QOtcnlZp WBHPE 4@>0JU%.^%L(p#e+:lD92yCMeKOB'&= ٧=WNoWFzvD*F!ҼYOKolIk:N>ղ6uke6r\|\hl>}N2H]!/ɍ[Z*Kֱm [zWfG̳F-_)Is7XZtc հZjs۬ly6y6cڡj$Ҵ.ECB-[^~g P-C-K9mY+T _q\t)[݉|*+\5Nݬiȝ+ܠQ\~SaD8=}O*{B)ɿ/OS\fnv-:o7U2z _2&,ѹv YށX!>L !QcRi'y;0(fS<"O9<!k&B.:ԓܐ<'F8kBπ |ʁ%O5DAAhQI>4BĔcL` A/(#M}4dxux"OeynպxuXt ^#+pU"^ȥӅ - C}5[e/: #y( &L;Y_ 6SV[KZ5-׷F$A4= 4^0>s̙uDRT+մm֯k[]5@ThEX=`MUb/}8Ǟp}-͒.DE_ZZWǚKƚhNdѷGJdY6696M#p˪"b*bSYx_~K>?uk:,Q9.q,ץa-KsdeISnJqITrn״o~gcg#\U*/9d6 b<$PN _qf=Յ9v4 GrWl.ҁ?1(Qz&Qt#uדV,k@B a $߈"CʀfX)V?a|9y˜<ҔC} tVC#d}8kU@d|Y`f nAIBl6mmz]+6AttN1#?O'MsK$Nzr'c:fl@fh;z\={uV%HqFp9uVMt:U˭) s~.qCgWgt@YUEϫ<\0eM4?@RjMjN5J3۹ mi+ش&ֶaCWl_V_oW}'e1rȹBVQ2;Ka{_^7(qOQ~f:t? 0.NPpS=վ~Fqz&ΧK1y´ycDY./ЅH$\ =TNH"9Y Ox{[ngy_U%I8^QyAB`_X)!=$׾BS{oX5g}e)br8Wd)vS3wr.9A[ֳCgk3yQ~\ýTi'"T}s/oc $?8bؒ wzlg ܡ1}zP5pL% Fٽ2md.j@OpÈxZ_-F )?GNCGtp5Pci6hJF4`Q@)}_%q2lY!;p 0NC*)j(FeC+p^<7@* zB !dH.p7<8yy~uD1,kh!mQfROcOjo]ٻsɼPlb/^>pRXQG&K0. /<q>UްS,1,1$p#KAF0 S b捎:񨇱 ^ZۑT4t*NH|<C_^⤲Rms p$c+!3ibAIˉS?n%Wo>KkܸmMO{cq^h<͘6-^{sEk1Op~3dTLT&Ͽʙ6{p t뼚ߟoC-ͦ2檤cu p|bKLfg <,|C9jР oQn?g˷ N9 Oo3lMd/ }.od%_ȿ4΁?SN?ճoF|6K mag~=ve-V;l{}c .ﷴ?gC\c^ycK@?>)9TSzOu O97V *??*.Lz5o୳BAK)@>Q rʘRF2e Y@*-a~J=8Ɍ6eQق!+0!4(|??)?!p6qAtXEB/Q e A#&.` cC( 'BA'C#xǶ #G1tЉrb\WG nyރ.mpU^ĒHKG-QU5x$9cv1>1?s'l<0KnD}Jg\=rAzN- -$m%M[zʝ*ThR@紐Ǝk56[xylCM(FkzƢYuA vi{ ~g[%QE:h7oɓW]+KlvE^} {vsJ7_eWwvZ[79Hڿ'o4deqOU?a3{{ aۤ`Տ?th[\f:U,+Ļ\25kg%E kޢl#{wmnիWW2P&Np?~ O;E-䔹򚅶NvAZӷ` d%῰Jt? CÇޟv&UBb}?nn'5/eWVֺmrD:={ྯ;~cR&`PF%C\++4п^(]xws]\*kkX; {a]tbM7+/=Ͼ+]7y]Ƒ6s;ģEfԫul~>;p:J=s*^o׮#rC*D-&K_0?vѧ8!O2)8.xCKwɝ! yd)XV$)ua_?u&tCV^lY{?Yw࿨GzCJFNfIsf2v"?ꄈ)'͌G͌[SOd^P,=%j (.xԁO:+&d.Q. ?y9/U*RR(F"ڔGIP*b ˉB0H 0BXꩃf,D=u X0tiʠpB#ʂ3'MX`pBʂ?p@=zIhQ,HzavzNnY &QnUd6HK !#Xè)<4A*sqD418q6b:# AY5^i}፬Ijg>@+^ۅRܢ2e?_NۍNm}WbO`ڶ1ͬFXIk*rV+U;RD9Iob3FWlq(,c;!}Η^7/]ecݮ7:Yl.d<}=Jц$[&- ~2;){Rl=v]wm7#;'kwV7+wy{By޽nFn~GAW3O5IַzO׭W;, >|'Q,]5 YNӵ:8kݪPΛkϾCi{[P$'=c^:݆TYyCm'k-[p?}g"Tam )]ry0OwXa;9.ɧʵ^sWǍcDF{I?qd'ۧ@':)3T:vFZi'hž㎛ŗlz JO\o߹P^aQ+dbݧXD (%K#c]޸dB ^iF|ʀ#@,IOp.BnꠓǧL)r?u<yp20THC7 fjHa~,nlf+סIBu(5?aTeI5[9J*tsZn,kGPϫD. ih4/#9Jozfĸ QTUJ۪۩a"qן-T Ŵ ⿬(5D+O2?/nqLMk=kP-۩GwS]}?1p?Ś/޹ bڔ2]]Inp9gF}6tkmQ۴ l#`[Rmjhr\f#J37X:ulU:ɁQ4oRMWr2I6⿸ ~}9r<2r۟mMaK/h-0G X=?g#9MfP_gv~:MAZe0ߵl[&\ĔmQlUr_6˞ӌSm/޳XV>A+ppgfwGnm>9{l 숿g=zCn]puS)[E+upq=#gC^6i];C|'½ƻv94:u;ڞ. 32ۂ_u/.`5-LM_ҟLsSf^Z2]? +c.)>G{F{zԔ oy}U-K\^{J9i % eI?HJzxV)Qq'?X *5?뮻%!C#/J2C}٣C)sNbL:樄 3/3 |Ex`8KX: C||d!C 1<QpO"/+8y6ey\Wԡ_- QW .Bf2nLBzP`Jۋi{)KCӿWken(DBT`Q/s$|jįLi=AO탓`[! g:: VyI00˥ӯG9AM[|Zjgfwqk~w[ck6_2pIz羆 a)͵oʈ__juƫt-]3zh}|VT̵ ::Δqsv{yS&جnJ"uCfϝc\{M֥:M_;dLJl"CX6?.JЩ#LpQ˷Wzdēx~>ܳԥ+C嗇(5w*r;_fruͷw { b[k~!sNY+dܦUbo4[^o&Qa+ہWæN&^phL1f9G]92zV`ߵV;[>(O/ʪn $\bRPdoA5O7" ½5+*OFgvpEc6Jw9giTTW*.MNɾB—_ sOL_K.. Ῥ?1E.w_bSFƶJۭ~x۴\N6Vc_dZ*_3mߎk2cK=n~M-V~_jHyXAlEccq]>{^6^q2y k)蟍?}iRJ󝷴WhLZ6V6жvcK+D9dx9dP=EĿVymMW5fN>y[CF9d8cn- SjѢK_271Ë2v-O?9OoΌ/fۭ>R䐡=WA~{Y.}}\9>>Ƽ4x&CݮoM6mUL c|gjj"¡ʷ&x>deV/8Ph!Ö)mmƏEmMɖ_OCy͚4ժUii0CZc8aP #P̶ښon=K:KjCT]CҸݜR4_9HWrThRZ0/=J\g3D[Fum 9zp` m7+|me[;^Oa#7gFYnZV|6P0ce/'A3;ɠ2G1*[OGN пACQm+3-e!mYVȄ촭ٿer6MW?=Cpr QR!+ Wm΁W_O[O(:+lȐXyG~Rw!g]a+h9:uV5f&M'yQw~uZGr@շr\8L5s~_={5Mv;1o2=vjY\<ϳNҁꌨ:nrl !vȁ{:N8>ՠ%cwYS9Kk4/:t;8ouO1C%X}V^A[#t{i:T^g=3vO8v9}Nܹժoiw}| ^i Z/6{l5E[kG}o9]xiI.YNYgsb^y-oeAZy%yE/1?vE2eG+x,%~ty8eDye]ƿ?sp0P+d-yj,{.k8!3[mUqPG`ˣ1!p'A+Sχ$xq" ^ t^#8JQ蕇 ^sǁS%&('`B#M4e|P W0ʢ>+NG/hSN"]?@94484z*<!K+hdA.8}8!CZ!2b  S(ET5:#è"+Y]JڝfDBS<@s@wG/̢tW&N6yp 'M*G $Iye@t_ 熷0}E+l+*Nlߝ3,%_>:mB/k/ҿ_Mk>jS-\ݚi5uzσo89\ϵofCm>l3yRϢ/9Uܾ[}u9$?`ߤ胐Je-Vdh |gΚ@ vu7Pm>F=>a`93bHku !o'ҪMkou_X+&s{t2W7Z{WտVNun5tIі; -z]v8VVK=*_[hl/' $N5lw7=z ; ٺ:kyѥlY2HKCBe\"8vug'#:|ۧ:GOR8-  \R]_{n0u:g9dmN!#[; 5Yر:=uG)U\M'jNRDھlqЩt٦o7* шmU9o|$I1+No\*<>LgH)hҔqݢh*U(4j?lRmSR1eڦ;~G'_raVgiVRogYt5Ҧ^pҘ`٬vΏ-q :J|uv޷uv.k[kr|lk dEVYn2$'s_Ifaljbr^y]?{t(9׎Wa "kK7MD}=EN_M0-S"gH2Beu@N aqu,Г).uG]4ʇ@v)47sΘy,믿zc7>VT:&XV-톜?1LϞiRn7CF(>8Ckp9)?]\3l3*v?Ww!`U12\Cj5mUJں51 @CwphKFvtRP\H3nr:LSkO> 0?l}~$R(!NrY:tUCp/d|̃tRF sOyХ>d Z Њ4uyuNʖ)dY…B!P"#略|4qW3 0!1|Nʣ<ʣxOĔ"<',х3SO;5m_'UW\=ΐCY!aP+mozǕze -/W$V>fP՗pܽ%8ُZ!_yoS߭kIk~gRWtJ`dkaLHqpX PAB:X%5y_TF,b9\+YL]_l2 twƨ 'N^ Z! q〉5jpիLzaZ6zt }!p'QJdTopĹV#]w.U *`}?lC;Alcݭϩ6@cO {걁֤ICw\ya n[mwIV~+dnCf?뮿]|;q*RK?7\^}\gJf ]rSrU]e/<8d>_m7,V~aOJ#qcNmX# }ۙ4vMR9٦U?x3mM޶HC̪݅]~9CaZ?:% 㯿F<9Rҋ(K /P~Uvu;#$̀P<[؝@|e!nϺ6+dN) )cY(ӂr 1ee(# rBGS0!u|?b9L#O^ÒN(NC@<ȇUiiN^S'症zDcơ>0Ѩ4#M=UAxH'J'!Jx@r n|QNІFٲDbP~ZBq eeXE*[{аvB%-Q'T8K<꓃d,XwRN# wzSmY[D ͒pbfF!&<\_f\6ޫ,PMzV Z <K*  Z=:X`F 6B/O}fnbt?Jr_$~q8*\vrC D&̵bÛoj? l/K.> RhLWPG3-2ꫜr1Ga}:|soK6ȶY!`.;Ug4B ?7D9_Nӗ_CF+5s}PDgcW뎯!ӦuK[2ﱞ6>1r&̘>CI~as.H&'Z>I+dR SRq]x nV].E9@0ў}y;|,{g/Pv{[kZ ϝw_uΐ:_g'4; ƪ`~H+Wþq.6dԱ-d?+NirC_MܕryGsp?D 1o|S9dX!+dt(:Ύs$VYeUqPD[ks3y2c&+deI)Gfijkppo_C<+TI5*\#$y"9Px{&fgȨW:I#=N. =ug{MU,mT>_gP__!#%coFaG@AH,< ͚ooOvβ\VJ- K`5Uh5H2h;V}%z•V6]SmN"}:W!e : kS6Quӭ{u=ˬVȘtZy3/7~ljT?α@Aq4CʫcέΕC@lYrrjI9<y^xq?o$m vrd+糜#72@vuh2ڲ'.6e9XÆvWSu*zΓU2i& glYڊIHM?^m3V<;h]|ӿxOǡCt//5 /ނI<D4?7(@,>ڵ& хB,p<m?ɛh%+f)Կ[ttESsRF E>>F9"D:|x4e$q!TŁWHpW!M=l^VQF:d *zb z!eć\Hh8D'<]Wſ;ꝼWOfj;_!UgN5f By.g#d$W;l)&S>P_VT?S%E\_aZv[Nӭެl뵻֝/?dc ~gak?9lw~\oK<^a;J-]m諯J, ]_GC+2+l9>(cEB+^-m҄ɚPN99ΐy\g̙Ll]5փwЃB?8+ MҫJtk-imx;o= *K*Neҍ7ޖX/@ꮿ}˂<|#?G wա4ѮFے5ǍY27I^Ìc?^sMNtmY^ﱛ8gzuXuvMC;(Ǜ@;OG^} _K4n8+VJ 謟[s49~Gmg/ör0 vC͙*)VV:s9fx1!~&n/ҫTN.;ddKDF_Sjkmxve\64܆Ae yuLG9ezf[F 2Ke!a/ A/(S\ڻX+Nb,iGsO{2\o S[Әk0m[,32+]&_*2[ KG]93yBg0SOפ㏵BHk.uZi\3djߦsFVFG _V>rx5dpnoV> sl/SuܡExy_e]nCKS?6i[L ̶,qp@aM7a^xt8.C3myW!JB|GKlM7qqCC;+d6U!#Y~| S[pRkVL~ڲ4V[? H9FEkƗk wXw!α@[pOQK Hc-O{7AM@IDATGhkxO%J~WW6FZ5KN/>Nz5жL>SL2ȿ?LϾ?Az%gߢGkh}@j")JEOU\d>XOTU F!olN+##H.=;E\Wp+IV<‹:dtwl U1m0;w^M0%r)^fzH|q%:f~N5TLO&5'|GE C)^D]/A+t>p0̷I/QeC0/qbKhUt( \4JsC0 p Qj!χP.@O>G>uf<G=uy!DOG.8ʡ9lOg4(dC 0OT1ʼ~a+4+("~04 ̗{ ܌?cuBg @Nz^-x+*'1 TVDK@9bagfypT UUn]VYoc!M+~_X}t2^{]=em K?>.< 7u556麊{薯4aԪ^]c5Yng9+/?`85Mz8ZX[ڶlAQ\[]NBL#+yӦvQ, 6m{yYoj+I`W}T2ti]-Kk_eH6Sd'Ы!'-K01LH؟sBxmiBBWzՏLɿU_w}[ouĠ$~J+R7@Jg-K\smss.aݪ7%c[e ۢfo v^ H]j*~Ȳz(…Cy'9o=_!s\̑ ҟ[snlY*_z S_S[8%^{WYE[gR2o+58WJ'K0)/ͬa"5):Wv mBwc*ԓ۔x\a{%9RcrUdrRS&b⬰7pʸCF굗޲$ôB&@*pjBVO$$ڲal[gFvũ?:ㄜ%Y!芡%5:ug\,mX3dvU0VHFn"e$+dz2ITQ]]c4y&\}ݭ:T/g*Zty9Ek,mY DO ޤM6Τʿ^=(?VEWZ"#. ?g z*d6TgԷH[նjVv)Ǻ~u%f jUjy^s,3d^syT&VW9.TYV\U- rmuYR8C#!/'[k9>CKjseI Sr4'+uL^Cl&-K%D/V'm Jg\Կb|K;Rga|(x kaL:orc cutٷq8mY*7@g|twak9VM?h4ƵA8_xojX[^3_X} :e n~0DgۗJKUO}碯.:C!Seׯ 򅨂s(ȅL~/`9}Jt#CZ0;J|=t gW]76ΐ7tCH_2&s^12CE9yhOF&'pH(WgK:C7\CG8O)Ys/pC9Qn#O)|%Xրaqt('0,|)R??O8ʁ' O@]g#.T?x_#1 p ><9TW )xB_;`UdyCG/Yλ񍰩T4 "?/EdUsz鲑kxmJګ%lSͱ 6a:gv2mM`<j ԣpۿa~KÿĺF8'YȤLa8C+rV̴>xpt˞:f:[r҉֢eSeWb~Eu-?i7rkGWVȴցto@!Pc7}`uG ^[!̜zZěH_ΠE&:K[Iafp`.g\s :L^m][O>}N֯KX,Og!V'3}_Ti!k;[&m3goƟy7Jy:)WU)F+쓗nhE.lnrV!2"*8CWȘޙƲłJE1+\mEgT༨S[+u7M+dx.K!#"g.Z!毶jL ۗ _."wh3FwY(JTo&޲Ahju>45ju sr"E5!mYWjŶZ=5i!3>;i<Ӡ$@Ur MPKw+svX遇C+F E:D+[W]Y4qfڲPz6; CWln]ſp Vy2E.ffyHnۦMCu89/<{Ӗ#,;& 6=Aq+r$t?TnJ[mN6flVyU,cV یz}O+/3dnіj5p֤3d2o2uuis#DGDrKs!+dDϐ l @`7(69t+mgIAgc=*IBH#t!rP Xh v.]\g؃)bQQԫ&W(RBBIv5Ke *aǹQ/z)eGL˜F>_PRgc(4=sCO9d?e)^:59# Qۈ&J5rzB%"X+)tB@4E罋2 7/=eAYol ,)s˭ܤ[[que[YgPhBgl 05*vfTJm ׳5iQeU=vdƕSw;m98^YZKg, !,+9=]^wvȴgȸ9IP^YҡڑBi"FO4b.j}7m.q{f=@Eq-m7N+V{]sٶIbɴ̥(ʗP,x곊~d\8׆_|'2;kvA;J$~aq`e/}^y{3Bk%§FY[o35jz4v,\Q=|?4 HQKsjyA׎9)D6>帣>/|գN8>Q>GsӓewGWCf/n3C;d-U?E^Y"ۃÖqg /`qkxkp//6_+d>ġ|eIyZxyf9ύWz4Xd<}`UD~>}.b9'="j ;n^g螢8 +Kjo}({SX_#g?;;x,L4Oю8Wuw!ۛ*Z~1/񑏚H"og.;jQFTem?:?c'1)^+F_Y._!Ęt5Fs،TL^BM8C_6@MukqLyF F dN{qb⋢!k 1`9hTi!閜}QUW6M'()C[abtZeR9xL7gS?f&.{kה~R]R7]y):W)|qP2P0#KZGˣF+jnVqlh[2E9H H;_]zƋJdRWh}U˶{[V_(weS)vay_6-\jd|v:hWOAq|<~ko < !CL'y:4N;l!WwaYk5usjxeI4ӴsgV@Emw_Y2]t>{ I],=\#s}>T-Ye ; _n9T}JyC擱 3w 5tQf|U84aUW!C ^NIEpd d-p/Td WaҔnꉂ?O[Xv_ye _jjD^lMՁ>mw7Odn\Y]vxtlP߫x]N+KhV}s9[~Y\ve M?b+KD퐙>gێxz55wIc)s衟_0Ҕk1,BaLXQA´^CfFԆ)`MTh&TV_z]Aҩ|!#ڃ/m?ުz'$>2+MoBYyӭs\MG;ZY ȿ8*[a5G>P-X73w'k҉tKIjK[lY9C֕W]]@Ikոt0vܬV]Sұ4g2_WzSS8U;n70}$?ľ?}D _K{Ia:c>=MWvۭʧ??!٧(^nLO y~xߥE$ßkRʹk=-\Mvȼ] *gv6ӍW.+/ weƲO|q0y/b B4Nl!sI_귣_mK?ӘkΐcHaMK3s |[_ڵj+K4OکMbΌU$2F{XLn<~*t Ǫ3C9CfvGS,"b?xlǟ@Ǎ|=oG,퐉ZcBT{gg أ`]f OvqBÞSG7qF@a8`:-`!xBQ6MQzۖ}Amb3GcK3 a^~X&!fIBrOHFr8R`.ddk;s#k@ly*e\a]W} cJ/>y FCsy֣'W?m?A ug:ysdQEg-~̔3Wɳud:ޗfrJchx%H>4e}7;5?EۿcGqYr)nleo|rտ%jz/^jp;BANu䪫Zͧ>;ql0?WF>O{ړ#>2C4I>׽FWC/Q:W[y[$g+KK^Pߵ˟/|_>LO)DPs->}#%Tƃ\y׳3.hybZy 9jB'~s`'>ث_v@YOЁr7jpΐ^mBfpLqte%\ -hwgֿiSW'd_Cb'b\xtj[}˃~`bɯ[@>jZeG}e)w">ыoqV&2 X?'/y#RN1 2%JBAP2AUㇾ[_E&Hj׎;YuY^IĕX&ٷ-0k̷Cfv![.Ƞ̧_ѧIs>1Z`}V5xNѫlATX5Fg>s^ĴOK1iZ_y~{dk|}׿K*ha?}SJs#SxсkK({r|?6!s <կ,v>{}̉kaA!>߻x3 5h"u 2ҮAUW"[ߩEjtvl8ď':뮡W؞QC c&w=٢XZ,\gy9:"y,,zΐϴ׺/kZtӍ7̢ fv66WU&2ł ;d7]דr K['OծG 4" ~,nQgO8xӹ`2#K}p+ӿO澦2Gy7_d_Ϝ@Ez(`sE/xr:+Qj! wx丈Cg~7ζgn r.xGضб],c!g[yCf+ d !3qxA\HPo+=ō|!a;6'퓢̋}q3dq / 'M1t'.I FBf!5 S9Ze^Bz :ȉ(G;xW_]Kcwn~J?q w\VcٯF2 D~|O/~(^L~7x^z$?qyO+N,$+zѿUasUuc<2N.oaYgVcXp]VKqƙCFWKZmZ C9뙌.b˚jo_7QMh%!}z%p㵙b:NA_B;:CF/j'í<ʧe򕥇79,,sA-Ӧt䢋/eզGx묯S.a@2L&rA&ۮwyJFdF^|e:fr`';M/}l};zKBA[|fL8/h~R+K쐩@ 27;5J7n'fW묔}(W3dIߦ|}b[Gg0ɰ gy tUU'%8{87FL끤TGN:WQ6H>/+fj퐹U2o߅ A3&:aeQ,o\/) 8;gX(L~GǮAmLdTVcХs*^ |KQۦ_!(QL7ƦWt7ۧz-EXJ.sYsI2;_ׄ[B@\Y\6[cOh}x-\!:sNVG}^7Mс]pl4VlH؅;epBNҾC ȅDHA7@,kӦ~#/z-3_B~l-i%ꕥ|@:_e\Y*$oZ!5:KYZHMUq<!d^Roחo3&(isYO>k\|~Rt?1 x&HR)ןRN:gb$K*u;2I/EgOt"_#n/Wus's%e}jҵ#fN̗Ȝ'o^1x{?66zeU'וr7փXQA?Fe/{Qh^abQ2:y2+O˻=$4I[Q|fnRn"߇2W{uuiAЫNzw'6J$| ;0fDt6|/WEeз(iS%jޘ~ӟ4Jׂ Sv۵<я.iёYg;LsY;f=jOesӟ9<44yeܜ â|;>etKH _oW~?[fak>s~coR6p֚ikr–[i1IQt*3 &B?\Q__&*'?>yvW 9+Ag14z ʃZ:j*[{~w~O䋃y\:$aZ=>r78u$>AJZRCQN7"ƯN;=;Ydnȼ;?%/ܷl}(踱r-/|{`ؘׄeg=Ln~~_B:eMb'Q[xru.7sV𲗽 8G;z}/LYnaY;5\kr͵']|ݒˡΐy.=[x+pg?CPX{wwۧVu$9Mf5 ї8T=` >-h@30noi8B'n<'i"OI|No01{}Yu k0iYn@ъDO!;lg ۂ43C72!qŦuo;-ać ta9b ?e>paZᕥqk-?ӂp00G@Q!vɛXj:ͪ֜V2M'NsҦB9HrVϼkEi3 dpO|O*>(j_;Ei_~[o.9EITȿ5\nRYirzOS"W ˵_ռrӟC\Bd?,݅~.mm[Ǖo֧wy^\[y e 'k/6*Mӊҕ?嵸YzeikA&ΐ񡾽H3u4} k(wZY% ơ@{B篫dL}G.hAFO;ʒ:c(̷& ch9 R2F>u2v?x%pO{4O꛼kʴYg>:IסP_i]AOU)H|Ս/_vġ=2E n.ryPv5#G;$^if>ų(4lMxMK/ׂvS,}oױ"Y6Yc^])rvÜq9{Xv|b)x^,< _C oz-o3^h 7ͽ̛;?KI }ISV?Yq l<Ի-!w쐙-ʌ|Ϲ1,Ƹ~UӨ|)(㸫x?ƂxZ_Y 11qJC&dv;-&"x.Njn˸!Y.:G[^~=B'lm2'Fv4xi]ow ۵-OrrDp€>2 ǍkC2<lڂ23kr1}uMs[>hy6BiBS!Nӌ'mW#4퐡chI/\ E˛i y}V4l9Mȅ0z%jlc]F4[&h-`Ì.2): B Hӄr zo,|?G>|-?G7#KXx!Ζ>A|8Cf}A0X"\ W0U.Cjx)_! gjͧ=T-l_5eI܎Q\vDAg@@gLa:j7@hdґЏQ"F1[ TYVh޾eW &s 1"V]9E ;%+S K(_wAXy5izkd ^^,Ӽ.W D!ϒu:Kme3o/Uww 2 /ied%'uNs*o08f֗ ~Zy\UV Ye*)y8L{G:C}w9;Ћ aE|,4!894.nY`?nBlXK@8}E7| ;\?2} 9o{,Xr W!;Cp2 ޅq42]Xi,z`87|c?/vH>:H۾ӼWl[y秵o]a[!3EW x_=(9~2DgOD!]F2(-" :g:ZCH~/^ F(Kt˱_?qbP1ǿx]yFbmYv츧d4 F<𓱞q{73\iۗ=Hiآ3jZdp$oT6\v|s_U=z[f+K 3^ 2*pK4XȎj>πrW?O/k/f]$e^߰|Zeu#8bp1zy.B/ڴ缄'+4^؁NrQI 8xC}nI:LǞnCڗB`A1ipنCt4:9sPI.8Xx 3Keh@|*AO 6z:E7oa2Fq4ơ:\v-Ă:? K/tR#4,@L/v >4ʓx"WrϗM&yŒt>=_ ΏIXcLZd',lFF<]Y[_saB 4:[CMe?pw*^wKv:3dtp +KaJT^! Nm?ƚ2g;d8Cb/Nvׇ,qp<1f;sڡeyϯQtnk~lu!v9<>̻m8Bm_k>8!Wq8 x?!ˍ'Xw #pXPw`֏<7֏}`X:8Bz%xӱy:ퟺ^YB3d⡌D$Qht7JYp*P[+Qj QFh?WtUo8TJJգ5;dGł~Y\!1%vj p/L}>`҄t˴fy+_\6xm<ᶸاMM;}5yZcGYV bދxۯ"nHiv;:C&ϏkKEB' ^c%ϿxeأP'St*St̯=τ 5[eGCkN^ϓIơˋ&9y>a+O%Mȅ.+q|z_.@ ]q'3J# "k,H?txaeW MCgvVb ,%R641! Á\9"9K\8a§M* +BKXT(~wi[%:WJ|i2L7秭U;n"O&;񳕾D?^Yb @)2we> wg8@sn̹tw7XPQn(1vGz=/Vt~F|}l8Owo?[ߔ\v@A 8ƹlx@IDAT :6s:S-]em!F>eYm82ܨVO/E#{rN[8M|a:w2buƂ).X0E<R\8~/^Q)I6do̝Fz[\(SF}%q:hg g)Nvܫ~C}ś(lC"z*~TQPu)}[H~En*Gm&ܽ eL!3A(E-h9F %~j\Q ǿ꫾X,ݤ"- *̋ C'E\6J\^ Eޘ.LEo|o}D'C6rxEG[l}$fhFL ~<<\.pcxٗ2noF}3/raccj#o], 3wgϹPJJ"" 寰G{r̋xE=ICZn! Hfqj&,sgBGv꺝#^֕݇XQG"uA$0%^jBF tGLu?{vS.Ȝgΐψ1% ( #}rJd($ dx+S\mz"/qn /[x<0ߏöŕNt`MxoCx qGS]vpy oga|3C"t˲7^i9O O@:z]`<e͇؆}m 9wxa~=9b|c.ˁ1"l/.:,xdC@"M22؎,!9W$Bݾ0T.{/;5?yJZ8Et.Tf+KPu$OG3$@#q>195qT4/`{~̴zgP;Y/q~аGBU4x^Esx ذ ؾK`yCjq83 i28 xtGؾy]k]gu'xjy<,8,)0'v؄U:pRˡ2 /ؖ:G~!v¾*g|}'t8 w4rOۗc@ Zxa Pؕ8xR B7`˟JJ^6R˥mJ#-M_T9z;*meɎ%Mۺ?Z{1a_ -hcfAa7hls CϤGW@Ft41{gAJ60p =5`{woo[wC1bzV⇺]H(>XXW8C戹*d+ "t#)N8^!>y晄zot~󒶭YOGێE$@@- o ?ogk>ł[,SCq 3mʈzoSYY}z.Ǩ3VS ‰`p<Ńo(r21U1((^Qm4*jr˪9н zǟX8Y0ًkJ8?, CO;l}Za6VhOiV`cyq1؋BՅ۳g\n_ d/'{Q_q!3~vŰ(s RYDl)%q eAƋ3q3EpZ# YBH⡧8ؖZ4dW4de3mzn@y6~b @/W9b4Dt&rى6/NyytB_z|ؼ  ^dix B*:@~ /`xi0aYi ƙ32ȒP4toomXޙ?:í_a{= nyWw/iP4쀳<87qȁNu}%o]`WvXwעQ]7(uhWҤ7^A$n,! Q$oVZOf)PR ({GӉ,PCgȐNЂ ;dߓ<M!x4HdQ&KUV5Y?v֑|%C}/E#N4DX`y'4py;f};DKnEC7i殄MpG`y"n x=|'/}˃ǎ/˶x,8yС_Y&TLG6x qʿnY%!*g-iЁ (裤Bgt{9DoB\Ȱǟՠb#듴S} D0=2€atO(R;=Ɠx7Zx(z Үt3CEI^YϢDV׿+7!sBW,>gj%Ͽfj U+KOW1ǥki/Txy$t.p2@8˷6yA: OӊF= Mn[ͶZ +cIic 9.߅z;e ֆ@C]xtga &<P+74:[~p]d^}xg־,+ ZɿhUvks<.1O`nRHbCam`;K0D= N˟IB}_-u%4Y3:C^ge8#F:>3\Os,'QEhL2&a/}qLȒB1^YjD$_{zc}e2i _~uڑq*ѽy쐹IZD ^L \ 1|(d92.y^hi4b4x m~oqc9V>18{Nd?6 ܺ:GhHl+Scq (԰.@1%v ^qnt#9'?o>47s|{N heloͫh?/8/,KPMP,N/2b,Ͽ0Jkf͸-#XH!HDOcYA߽Mz[oOsd%2UKm?{}$1  [n;JF2!4b :>:!li~da9nq!cNK~?̯8BĭqBtY#nN3&؉1 ҊoƓ68 N:De|]#tk -jP1!Chčvm 8wYy!xBp!ΐ J!#)?ֆrkq6Ң|x$ iHbId$Štӊ^YqT;Dz?ϝ7ƀ.V? ] n_Pf>rֆ[k/Ds0YB7'ؽW>%"g Ar`DlNj9JU&Keߺ߶ьr_.(s~~Ώ0_~JQF[2*ΐa gлx7)/zʀo#Xqxv9Z4dH!tY{Q>C0/i>$mi qxߋ >pqZ\2 D73ɧre }}ih'SY@;d,vb:70t#HI /n#!9Exȋ"PbPLg(d/InLzO|/?6'G}aV_w<3~w?c(xc*`-LLBBB&x@.G+:S{wIs>)?sB˵ fPb?|76>1%yZ;8|[hE чunp־}&qxGt8<[}h8 ow2i>un+K0ϙGb]yJOU+F"iDJ0:K!Xى tY2Yku:M@A*V䒉?cSE;j`ktяbf(;eqU x˜_Zߔ 31QX3A}QX@{#0@Q"tOd /y7}ʆ8zwiSQqY{ÙoӏXTzM0br?Q/X?)q?D?#8ÿ0zc"Ko9#_>,9 S Xh~^Ed787g5 1KJ9^FGt&}bH11G ŵ.AZH,:vtl9tB.с}0#tyi{"<A?4qA~!y2r,4X ҭ^.~28Q9,.TnZm}i!Ǿˁ44Z& ·4DG&$A Bq[",**B?<_xZ¦~]/Ր^=G1}YWMNX~apvð0ΔYU׾o|} >DOicB@+O,({# W!| ;g< 4@an㤑qo^n!8.nw&xx [iB}1/ǾݦrnC%:#i-޼xq` $pf±,;MFkCts#o:')FaG;Iq*:sQG:WTztI?*we W4C6 /`m2\Z@G<b!zgN[ /w((N'd1&iL6Ph%-g&82BIChH[ !rY'myp0/@H;n4<6[koF%YPf+KP%\E A@ AAxI C%ƩHlUWp/Z#CT /} kxz>dp5i=,Ъ$bpі臇{&;1 쟢fQ&QWA8 L,gUВl0"_*~/^_l%ᑅc@?BgÏ>qc$e q:C&3d(=08\]bq^XȂ1rif1Es^hm# 4suS4':h閵nk8 ~`\84iX&MxE#n<26ECORt:|\h훏^BZfB"C]֍M\2u9-е2mya8}?` vȴNѹ9H Ty c;鍼F#[;xp) cI3=;n?Y˿׿j'zǟ;c8KRiY 7V5i 9/TAcT1ke%Ŝ` 6\jlY:t7<y(^BI/ @kG TC~)ˆ8##cpU.Y&]32:#2 (jHsvW0sR4qH#yyׂvA:r\?xz/z!@ C} >{S X0$1 1c?U_i0shBX! "GD2A_oeAY˥?*,f\f)?%T 4tk۟P, vXs<Τ?I_0n bj&_2ĸﵗƌ@ga"2Ϣxu!B!p#G;fxX1!tˤ#>8 bc6a:r2Y^t<2iuA``I`k1mЙ%mg"FtE#Ce|EhO&}O`>AG2]@Wts>8yh'C#m)1W_oF>0>c];2>c.n8c`-vy^o jrghIFrB"D%gJ)~/^s_6p>WW!CK5A 5Rfqgn.^G -R\v0y&t9;B.tF޴ay^̂'d|"μ+`}@ :{.^샳OD^it[wG6u\ѰE1$ Jx8xˁ#wH %m=ć#g޶P9Mܲ1oN4.I`: Νg;ģ~'"уhZMn$/&cA!Z|Jw}@tT2Ϳ8ԗr;v֑sU{g75\|1f"d` 4w]:ky˙xr C?rCB/"n:ۆathO[u~lzPƓ@0i]$ $u#y v Ȥ! M'N 5.PZYx㑵ju;]B'k}ġ^veaZᕥqk X"\ZWQNꇁ!^+Fth!tt_׿z_>nm zOjb0'V?>2睌-R)/RZE;m(f{nQ3w2 TK#c?ršh8&dU3gg)B<1?]4eG/Za^-?ϼ-!F`.xm] ?ˠ4xWpG~D!MR'h3#V'xg6lzчiq3޼],v3n:zl==yw~~|i{5nvF1"2 ba#J5Sp"˚<D\Ly/)28eW8^W>}_ 1^FJ+F9xL";W^HgjD#"B 6V_Y/OESy׿hjQ6^uP3dE!dRQXmP}q74xekYLРq BmyW gN=nlV#; *q }{ H<]2Of~ x qpi-?4ӑlyn}?1b; v`^;~AsoyxKk]}x-}XGCFpV9yz`!_Ff ێCC>u[GnaPG{vH7GtsOJ6jG)W?x^4r͢!WcA`G*/dxAE2w  @@ϗe!|n}84C?<坶}㭏3u[iDZO:rp5)C0, Z;OF028XG1[7<w`G}en$-k: M&Y\C΋ȀCG+~pNk/yHFkDtN83x Qљ,K!z!Rq+ 1HX~N'{_}`# O<>EHO ɢ>Tt*lE(_y}Q]^t!9-qB;sQM, ?sQ20B.-2>8mFgBἠ8ґ.Ë}ϹY9AO8j3?QВ.xxBBj&3;H$i],>7ּm!B}ӰmP!qϖ,>+K!0wf V!8_FyaI]zJBd3r %Ŗ6O>ZoPLs$R(B¢l!`  )*UpuAp]q])KPH U@jZ !9w~7In p|3֙wʙ{fg+Kav bZ5%`WJؘ"j@Ki(ZY>T8ne6nW?z9 z?q/w C} ބ!ڱ !tМ6?kZ(ˍhN>z16}^[rYC} 0m~O09.|Â[41 72qm !;<5Kĝ~ta;n8x-k9Z|x/Y78pa:KtqK*~X(F <%W~U &d>Fd~OCG=/w&A{Fq?j#wzG=^`믡wYg52!2CIxl|!J!7!z}0=k:8xX cMxce Rփ `}q/Cۇ.XaD!]H6 >;3th~Ft~dq6_,! Or{}"tp!Witx4' lGf^e/6AP,Ʉ$An챩p(jEb1)7`G&4B HE٬W턆S?0 >4܍uUS 4I4zqTk}o&Ww{`>{3d=kxeu׳ǝ&8Ou&ȯ4}/M>pQ,6uXzm["M:VZi}^{UR\!eΥ/pusSª"F$ӭ߃Qs>Y ep}\i 2*|ۅ2{Kw̉`jG%pTΪ_=0MS_zܯw5:>YĄB̫&A|&!ҼtOƦ XeM֞a=J  Cܲu=4ˡ;eܺ8:'iI;%oW2Xy?n7tҀț|2ay);"6(GqEm }3@Mڄ%sBS X$%t\ m9%C'|YO#KC~{qGv"2иJ=}*,tp3;.^WZ^rٞn2س- 9!^ʀ΋陫Eɂ A7͡&L ฐ- d"te8n;[C|\h'oh8o@s~͇vl42lo~dߕoY낟8qp#zvcCG=ub v:/j ȠlG:0ezcy1Jam _{B>:ė~@m%z:7Z|͝4ӳRJttU:+kZmߎh uz?f1=MdFz_96?XS073)!t !,GuU)+9Cl>2|%鳸PI6Kk\B֩ gc8SC8h3uyxI۾y ݤ~6^h<4!I{^V4Jpe@', 끟8Wť)gx#mg#} +3L7Nneg:2CxaHG:?<<80p[csz_Wzw&>Jg;͈ϏnULA($.`@GL)6tfCppBE-GgL+הx351Y!̤hi7_HhU_M$FbJ nuSc-ALY|!Sǿ}3{vzIO=|/.|[qψ;kquWꅮrHo/OsA(n-i Sn-|ڇz͎HQқ,#BOc qp޸O> rf{6;F8dp<,hm n n~pngj/F1}M]5sD*ʁ-79&tC_E}Ҷ:-\ۯh U$@h uӨøA޹˓n[-0wMq-͛h5D%@G sD7^O*:COLSE]$YGzq4&hsYwsd @.7ƌO\0:!>2C'qe~:ny:-o{Ȼ\T;fl q @dt354qq4qcȘ } 8˕4xl2K]hgqhXJ238:4.<#{31c*R@"c2>WDs!.xmP7U7V&?7a)]-eD=4(,;FomGzaLz +_u;qGQ~}-=/BdAxH[~.x Ls޼Fq]#McۤM3 vƒQЭ,f:i;9dL#n~)o-o{e:gBۄߺcpcߛ#3QNChlؾZ܀y&>jXhPgAh}_q+V6x2=7ndҕwG*!LP!(`mn(m[4`tE~%}I}(dM֭s+36G "t~ҍ~J:̛?EP'c9PO=6L2"O&T) 3zl_|e >@!J$dj!iN4Yc4x]8x%}x.m|x?^m~ "nγә_+h(+(Cd44|q$2^og[mG6-K2mӡ"n݊6Έ::/,ChҡG`plDlH:jhXXes \#*Vg>T$q0!"|W Zm_s*7Eڸ?$J4d_;VE^.F'ˍ sb +1T ۹rl< 3!FT,/Lq3e R&\5%9^usjzƭyĆ>{}x݇C}ֱ$&7SjlF.٣i+2?8.dbúMWA#:.dN534Mss>4Sl.7Ȱ.Gnl]6r+C9#e8 f#Yz%\P4e㼂'nNE&x.8M<5 ~XOyf5%6=,y̙!PʔFD2^^f:ZW$Ĕz !adXۗsj vW?ojCNRݪb4hU۪c3+17f<䞚f@Wӵ1f/uӰ WO=vt2 zWYi*%wTC24J2CP,Z>/^YBgIO0,h#*Boج$Up\)xLs>~./a= yZymZlþm+:ٜGu9 qt20Ǚ6Ӗ'dt$2Ǧ `iَ, YFe[.ȁCP}ag輶 gCz"]{d:ua js8(gkEuTuW?[tL5n UO2|/HWy9]../6[%:!i62xC::|Ggu<@ڼgaqN;?7X.n{GIЧp\5n0/ J9#耯c86x3y;]ya¶Ѐs pL B[ygeHepZ!8Bl'T>{sB!ӓW!hRo85GXIyQӄ0ĢS$K_ūb5Z!s*//SVךobs$: ])HE ܆*B[A:jDx_ۯ_?:7*/aVcE= jW:|NH=+{-nO=M7RlIȇ0rvL P(C+toB0tqh2@ ~v‡NָGub!RC/qdi]ᷬ ݥ=}J]=x\'4t_6#4`jqy^gy6(oXm.yGyvv1 m]ZlE&d}jY^;ЏE" \7f_^Y-z ݣǽHM%VS1gSy~o=>7 6SuKi#3dkJ_!TהX_!|mƗit,cNc;.^O Ӗw֩h#˺i\ϛ^(K@/GK|ߦmUT QhuYα[o;ƗcN%+@ @Ky<%#d3}! 8m4; 6]vl7υuOO| P1I+ɁT$Ҩ5 #M40ᶀ$3DT +t_s8hj"Nݨmܿ4[g$UV]1#K÷&xrWzVK~d+Q&Q)n;mk뮛?)ū׎%Mr5t<.u}0/ko_'tgW? G*Ԋ2;夣Ӗ[7{)鞿-Y翪h8zf1gKm={8.]`YMo,5)9q8koo~x!W^Ug~K}X9D9:MylB'm~lfF!tđ傿Z8ƻc}@iiu3A ;$C(t9M˙V!Wʛ:@0:a~BB5 q CX-NB#Mpu,o}в~- y]x!{p}!dk .Ȋ#ģ "xʿt nyC4wT/zlBamX=}liXC XJ7zW$mZ~_SM(R,/I; [di>} coN+# c]`?Rz`vkSnuct[䗞b[^M /Nc.M=j'߲GpQtt&1ŹgT6?˛Lʴc-k:qZZI V4p֋<<֫h ֺ6eY:?|ed\赬uc^Ǯm>! zʸp{?0. l0nY1!Ga! /eGɩ= <>iW$<<tx}mT/Jɨ DH) qmd!BZ!z:[@ ?C۰k Ȓ~-o;xG2/d qr 8@ ,'d@ؒ54dAM v k%"\ѐ fMXۯ;2lDg3HUQh;Wt- ;9HwU.; Kz\5ے&tnLS&w\=}b]Ұl-(kfzi|T|a.k]$1V@Z3.;|>s(MrAiwkF>n*lC"_aWE?jiٯD9#hNl$w}"S.n2YOȔ|uYQbC&\\e]Q׳(ZuK]sz! ^\|m܎˟+9?j'dtgL!2!vZ7#zxB2cf @xn t.4tQ4L2|}h\Y7ێp4ˀwm98e}U . 0%G`M?r88܎Z7Cx;Oq&OltYfzFԏNdOȣ>8А+ՁуAdb+T,EՌ=~Dq aӎpTq5{Ydyɒ1ʅ_FkMmvⴿu>Ȉ>ֻzI tM|4}t=ܙ 0 |=}kƼnG\4AEs8tzE+mW?7e я=ެcyB& MOzjA[lvqt#iܭ(n}wiMN3S+Kl쯋5&͘$GBxXk: `mNu7>LCnAh i a^6tWoYy~tฬ4en6LS48H'qE!6+k($t_(qCe&:2B̖r!!N$Җ<8_Yubo$ t>͋,N3^%E#̇_A/:͛ 5Hl,4mhDZ"NK j_}>!!|t~cF2˿87p4h`tYf:\L/}oW}_ZkA'?=Kmڱm픎90#? Y+:[L3~)ũ^3 mM#`N&zEGTJiGW6s`@dDG}6+!BE3{Ǟ'cdT']Ä(SS"ˑ(oo_;m8wz-R~yi^k{-/ަ(y_ΐaHfy_Qn$1҆^ǐOHccI\0UZu/Z˿J++G&mxy/m W_,bwNO-ܻUECsY 믡fA</X_&"C˕i'^e9pa)v@'kY-}s>)X|68Yx4/@q_KhpɜA|L',mZ:aGC@?æPA<~ q8BwЃ)ڐ.zem` @/u Wd$JzJ#n9yUWf~B&g-*%|D %*{ ߷o3o!҈nJ,!˫k߽wMW[n#~sNkQZkͻK[mިhڊ=ؓ_L=6RZ{5ojOK=bcӱfLh˫ q?U=ѿ,9/ZS[WO}:PW_y97Wk!XJZAΰOsWbcF j*M1=imh H120bLO/w@Lг;;'I][mOiMUȃ?t6s}4*CUܞG@z֔h'5boqw)v"uJ1t]5V_}48 t{my})M|fG2~^ 'dޮO|bvJܹOOѸ5?qv0CsӷO:#wƽMabZ'xL1C3D7Ix:&[H~ gj $r,!E]ɖ/֚ljB#Slְ$ ͼ8Bdꉆ]1^tSt_"iy*xY_xx:5@WR4Blx>B0JQH0>Ҝ9R`\)gGy!4HqxJ]m=e8@ڀcӬv;guH`ul01j %\$qFyF ScIg͘ $s'>}D!M/dxh.=n餾jc^373קz_ZY2XVL;o8*PDp>TKZ|0}Y(9Wﯸ>MnbV'ΡD‡&h5{q>Z<~J~m)_l?6>,~aw۳?v++Y ]zōE- ϲ!ӒnXu4~6+x>͉k T[T(uk^Pw{Mvٙotַ3'mӈG <`cGv7gvcgl#xBNH/?Ӑf^^gRiЭxi (Vd3H*78٬zn;4tA++/EM_; /m˻]a-OA6믽M+PQ=T/}6c?,DݥWuSF3׃ҧ?WCG+Wc+K}:sLk Polذmd49?Px;Iq:J]3KC6#<'ɛUvd-Է+wT168r;.?8T2Kʟz짢}-7O=We~^:?W;޻qoĔGșyw»G,RԿk{YKWzb mU{tOfvSU{{to!9gK Ys{l K^KVj匇כ0ĝ'Wp4zXS|w<[Cqwre=Z/6y1B@eFJ3Q9sMmg#uz ^۳vwc>pF7E&i۳_l!:#K]2;=񡉏TlR#BDYxHa0L/VzLH?zH*VVG)'c6tulh_J,T/M^_¯+SiZj-~4ɘiz*|bc9<ˎ Pwx.iCL8U?Jf4%HK>#Ӿl>Su+h =QWŝ::ttyDez"rQ>72G~{?4B*?ڌ!?1)0pѝ.{ws7sꙿkY> ~/ǦCzjsΔ˫-il(RzBfd,!M7^7ϼ~i{6<4 ]~MQ<,,әמZNj2۶ɆLg49}eɯ,Ia!. %MD7=gN:;&qg7&cOIqWvU`tL.]"toFtt޹?rED6e=L، LZNSc%㄀u6 z&D2 B=B.˘C'x7C.ˑ CMÖn~>v _fFc^g`<] F<]BNmxHHo[lhfN4$l8eAuHoA 9Mȅ-o>2zBF!l7<`BO1OdRdX5C9ljkJڌ!/^wS:T=OYJob`"˻K[/sC^}=^__*K??NJ[?sgkb=u1>3vEK0O}rt{s#W`S2Z9Iu_>Dǝ&AR* }+YR` s9P _t>ܓ$섶?Wq.s.>`8%W~!qK%M.3q@eEm,1C<hSv[EQz2F:W؉$Qze6G}ʫ/Vwwmt6W)LgؐQ.0te_Zm~K04WQԨvO-(=_=ASWŲQ[S3d+㦱 4F_Yz'?k47K2wrM2ͿGIoxr:;<Q X1ƿ[ѡ+KWFlĽvJfd?>Zz,~fzusYv}tn}w ;pӖv~mRU'#xi|.!i_[޺Ґg#yfyp}2y[ߺJ:8矸m(X8e]ːW6[Ӎ#>u#yeVOSh$:NuQD^` }\1 1lt~!T[ҁWZʨ( c z鵰 F2eڐ/6d."#?J÷׍jC3Q.,zeЍٯ,!4wqʾ:ӎ̛RݺWQcS6dK۳?J7+Wݔ-UVonQ»Q4_e╪۰\uչ̘ pdz/ DߣWznf?mr!}BQmѺҋė:[~t<}Fzӌ7eVhrm3o-u P~9Ul6˿Ҷ_Y*>KUv='N}xuk?, .ΐIޫzd9?8)>mii~"8AD*}Y:#mʰBhtr>p7/:DZ)swŏcZ姎+oʡ*/Wq)7]((kjBiƬiI/7Jb~C(2\e@kVJez'/Ƀ&'\39t9OFjܠltnn|M I6c^S YhiڗI3,gKV:Sf]}ebƦ̭/9Xçgi5ߕM>3c-r1䓎Y@Fxe)7HӔ-,ʒGdQm4*E?>KuuZBRA%w`߽v+̿qZ+@y a ~~Vբ=WuK1[}ѡڐxe)gW~;({l T]0rY b7ݹ[4QG~9 K!ˣ6 fc~ u9s ӏ4t͂鱿?{Y_OB|9zoZWϴ>]z~!=ʢ)iԣ*iL͝;h}g="io1)Guz wܗ&Mz9}RZ[c\p7/7Z/?"[o;{ S-mtf'3kt*!4CHϒ9Y⋓_`z=/ ?mF7Я:*?a2x~?*0/Ց9PI{_~W$"1Ž@eCY(\UaQM8 'VpAĮ4bX>kbS&4Vb'dszB?X4_e pȑG_w? d|d,ȱ+ 3]_/w!889X~=)swmc&q ;EgߎP_9sdsUű(q|)qݤ/ :\c]. })qC:Xc%$MYF~X?eB?y Dz\&·?r9)hلi "+`vig 9B:`}e>ۄ1Y㉣m9:C3!Gǖy-G/!E >4ls!gySg01ظ6@?Op%}I7t)};6cZ?m}ԙ1՗Y(O2.O[K~kmޝ3P]ujÇkC ?|6Յ6d\ڏ Q= _ "g|t0`ur蛡QJSYh^ȑǜvmuVtAzB^YjY20'Kߞ]f[`|~ [<ね,={Գ~F0v;Sؐ[+]7pbGfN|KLsOs~i})^Ev,qܟugfĶ[lPBnz7ml/]Am1gք?f{ɓUIdzY#dk(?gW?8_[s[oV;NnI_KQ_@-b((\ Ekodzy@|,i{'hVZ9.o~'^y{ŴZOeg6MfyM6LϘxxZM4˛2S}~_4܍M7H;m<Olh9z)6]'}w%|-josع>X3haYoI/.R ـ~~w?%j;÷,柽J5w8 oQX8̺@IDATW5i|} Kr<ʿ8G8"׵"+8$mzWtq_W?;yw=2φ uS}z(_^zc%!)*I.o<@'xXDG>x|7]*[fh,rЭ׶,O?umlK~7\#aAPҭ Cvoȶ +ЁG xڞCof@7Osq㦁拢_8 2yK^Qdb5цō43!WXKՄ~pd'tCW_kJl ૿Vz2_./mܦ'eWQKR/swhLޣ-B[]~쵚jo*>J'1?ې|sxu >gK4bE^B(lgOi{us~FoD۬lAVǪ{Ni'К+NVLc٤g?懑Eh^F\1K>Y1jR{g^֙&e2塴}7I]#iy3zyi/NkZx[t[S_JM͛Ayf1G6%ZPۇ3dh4 soi:GZM n6J[ [8_nP40K[`Z|pˡJd gi`ݶF[4kON-wo=RW_hz`@E<d-z1%}Zk3fըtD[zI"4wW۝^/E#ҽiS\%iS hC#kLi~]W^=mז^]9l'lFY 3,L,SU/Uϫ8Ώ՗>iI_׸+5$h=.gȉ8]OrC?9a+|_TcֻfF5 Ar2y1LWR#~շOt>7e楫S:E_R?NOV򄌾؈xNL}z )U׍8 6#J7ѧVr*cG9dֿ[zY|OdN T {KD}[5 lwJ÷pDٶsZmQUH nG2a܄ӷ}Zx#(MWC}yX*C%#- p2d'V#nSE+C. pfٸ+4^шB=B_цNǝxOXm:o6#xv % Q(bB0>h0С21P2䑱 l_tm_e>et!x Ƈ Ze{(}}Ҕ=!*D\xQa4Mv@WQ~FPXaG&4B HE4r7Dmlaœ1໌쓏YiK}}|RfBeRKZazG;G\δ%QSwND7:!>'5Gh΄C&ӝK'?RϏWJ)@-N8p=R7 B-e ARNX)2(v^F:AY&ON5IagEi[9O{'>=0(_뿽w wL/]_M kB٬WSOB]ohzϓK~-=+]6+?_YLh&nO猼tֻp8#{kS],qkY3Ԋg`Q>21OO 26^|%rEJ4i5@pɭ 霕~iax[BB`MOmb`ztC:YSߨ'|)c/h>T(=# 2)Te:EOZ5#sI^=/};_OnNb4[;~z2BNv~q~ ɿ}쩆C6VW^Y4*m)A:К_ϳosa MZ}?vu{fbFы/MO7,&SSCX|-Y_9&w+qP7p7K>+M>;ExGO_foQ"† r#__cbI>t6e6x("+a7dt~̘ 83?GDsˤgu̹g~O7 Y>g*)CrE` z>Y4t_e} kC2BÞu?:uZ|ȡpY(}G:4ti? 2+3(3DڼҾXtܼ'mp9d,BW]y. 8dl:qk:my-, M~Qӝ *n4+UI|Rq:,s_'6:4i .ȵbWIr{:xo:|'Fwkj ]rk_{@я/}UZ@a׍A%UM8D~ZA V yլDJkmlFM[F mߨ/qL93q/{E?}/NGռڃxcOgNu-pa!^s`Y\Q- [ \XEuqu7ުЫ ٕZ#$Mws;dz൩   X_lZ!3YOsy|z'VYyJ6fTnhG ROMKzٰ~ʏ闿4ߥ6@=ř/3WGl4/>6`t꺛-:s<)ɏ͖ٱˮ׆֦C7lf7ܥ<ާlO? 8@uΌvQ0E~s$Tii PWchWZۏ+9S-*Qm/Oza3s}{-y}Igʰ2~k3 Όa%rzl}]\B-Ľ6TWD^ \VdJ#蠟d#m|׽%걣ﴃ< 랟<(珀[m{PvxlqqySźl:>#/qx.\CGwg "k

~hP},F~׷m̛GO\bMO+A(pu^V7ꛒh>PJ㪯 H+D:>3g _Jk(#6pHW,C?߁|!s}LVSd{F},;T4fqivL !+?ɹugLI_z͹t*fQMv|iyՎ'J~{QNRotKܤ﫯Wfg#"գk>:wѶš:Z;~%ڐɭ ЧWv.}%չ#İ¿Vu.!!uL㈭+-4G7ftIdu!Yk=ӝz(^^ڰ׮?E^?)%ׄQn!ZS>69k1@zK댛gՓT`5: {̙3ڦNyV!.\_^je=!c +>8 "?sצLe\V>/9T S( XU~?)_P\ʿo1'e6\?_}شn;npGYxה֓4\!U~hQTEގ 8r'tvц,sd:*#s̫88l;p(ix˫߉+K:gzC0^{=NH^- _\l4yeVe=%S*(ʿ|^@V簁uN0F?]du tn\-Ua:bӢ+E1ruz]iI/NSgi\_Yj[9EkԒ{BYڱD//_WoWN8xҩgliذhCeN!sWyt0_z@g\.;K=(^[KOҤS޽f ŌmK Ӱ ש9/rOmvޅzՊ88!|Ԧ)g"]gD[ϾOJ/u1Oߕ~~*݈ D{skÑVR+e~N[ 9IPK~ $sق/G-Δ |_\4G>cf?Ώ}S|9vwwiDηx2Fl-;ߑz{ U|;>28`\.\~%$ 4AA/ Y|#β˿<쯶Zߴݶ_Y5W=ܣG$ˣc~8͒?E_0.;zwQE0^l?LvXcujp?A̋|R|tL"U^'tcǧ\|B錩XS_ۗ~*O ؐ%?WF3ظ'rTEv uJ٢Gs:eΰ8__~S/i$1tӪ,TUnY+is *O}WH ;pZ^pX}wwN۵͖jCf-H2O>'M'd!sS7l˴!^st>ʇglGÇoHFv;cshۤ eEuo7Kwڍnq\G )OdK$4h2щ3@#Lĸ"*fSi{~So@龯NܪUn-ڔlwǟC#Uta4—yƆjϞTCw|?.n|/%m33ש b`^v[e{nU(Բ+?!^Ը2X];߭,QwuTeΓ JQZLǙJD5qj0dZ_ܽSt?+o3SߩD*ygu&q$G=k :֣DrBuQIu׶ơs8`}ECWIЭ=B#oްSe[ Cbrd;аe7` N2fXc}g@eҝ SƳl9ORqaB m |G΁–q׋_.@/b僆?r9rțg׾aC.zCOuB3F %#FEYXv_mj$<B5Xp'j}GC]2qVF&FkX g;d>GxcTw2, ɆhO?^Jӧί>u9羑/6JH:^w|cg2cq}qo) )dW G]4Q0~ݯfne:FjidQYO[wM?>Jm#6Z!å\w͌\t׾q5`ucKTk|oܠ{ڜuG ߪM^yݤza <6_'KMW]G~8P_!S^c=!{ƆcSZ}x//2l\;d.>HκY\T,*_G ?,S9cb9sSf{5\mĉ#/D=T'{ -ʗssS/M\^Ջ@WѺQ[ⶦ?cXxTwڌu<)4w6ۏȻ\76?>e&!wÍeH:xRL B^?1R@bԭ/3I%(9hN??ux< kk?)qg ]-/:嶈?ǽ^>hG_h§M.ruqK-yJ 1 u ߓ/P?Uv꯹> /EOkS}Rc:Fladl6dd+>TOj#J#V0SO8w 2֧ygZ:B4p.UG%,9=ȢO{9#X_o{v], K9?>Vtm;cVd!/=GG p`3xdIqO~OE$bJ[}_g!C?D,1ѨgdF}gwKyԭz|z|\Gnfs7Sf紺\~{~j^ן ׸Mt3}C6Y2ĒG֝`{ҽ]ڐKuyd}z<=zЋ!Wz<(=+e])|ޗ''|l_#s鷯xzY3}j<ŋJKf=v tG~h*@[A`{;)/Zbe }3ǙgXBV2a7iL&z|_&֘5_6Wީm8>yt^?x؄f4:kZ^}y.#)4cegہ溂#k۔>y;Vȸno;ȃs,Z̅pAL.4Qpf9H|4Xa2摷hwKK͚#I]wՏn(w?m/Ƃʑ xĊh^G ׿Tu^4nx!]Ga"i{&>ǨF{C#K<yL Dr1b=츋¯륾sT/jyͫfb}QݭtQw>k!v5Og-w𵤋 ={W}YkxzA[n=7sqne3rL8NaH j1p%*?k=O=iO&׹ĥK]1edmqG;+%BWou̯jTGakmEv˻ĈbmF;F]iI a(BEKtg_3IĬ6-zT([lC0/x6mEq#?1HUSi}MSP6`8Xcz  /[P %;m}5ky_ [cl=<4h;9`8ڒnާmJ qьs</Z_B/x|P.ZurDH_K.ι~WKw=Gk=3>6"_?:]SRXz'ro0}IuȃC>H9NYw6QN ۞IlZyܘ_ aiiLG\8eyse?9Ȣog6zGǁb$vH9\fǗmb 04GYгsN8ԍ_Md!.%]^tq2c*'YRo/ ֦Bz8^_71SoqJ_S&ˊg:ڂc"8y/vgKgEQrN\vC/Yǡޯ+X0RrqZRs!0y)'t;cPZekrsYZ;@_ۻ'Wm4kR]poV)]'9r!w|vRgd㑥D'zb_w?wNĩqW;/;TS[!IUU#%މ:×.8:Zg=zdiۈ?Z$/y~7KrȟzݥzK|_I-$wNi1syi.D{HY]m\ssG@mxn wͿwu@6[t{s7MG^{<^z jW=oߏ;XLS`;CK/C#tٖN뮼z^H2[kGS5't 8彿w-W}:n^N$GN_qg󆍓786khf}KCߏO:WKMkyz[Oo6b6My lSU59ITxa=8Zn-\_"w\arޣ22lW~S|_|gJ/?Q.ڪd'DhfrݛN8_wi3f$"jg/-;cD%D#&2e10 UJF/^?=g ;dx:(ܳ%A TG3)6,/4؂ :ʷ/ʇvֵq a~2#g@2c4xs=·G!`yR,<}HmOx@hY`dގ۞+ VX|~kYr@_.C&(e[6葿]9zojt~Ihg\|ݧ͚]tEs߂. 5e%ӱP`˯#V[n^Rw0)wݧ砗@֝:_Lѵ)bm,fCKy;<67c2_YzFڐ᥾)U/5?XwhOIU$+<5S6Wu qK] Hv|3xLw -{Q2;fbC\SNh4n7Q⿺#KZQ#+juIˡjc_kb!hrΰ0 UkGʆ̧r圕WKF?C7 QS:Mw\|wKUJ7*yIs ?OX]o2_gTt, ` u$ztC#Oújo'R?t:>.8aԶ-k~Q:.|9ge, Ȍ6StWr!O) +d98@ș'4t4>풷qlǶt#m@&o }C}H8Bȣ -lO:uؐW@%D+Đ*bȉ_ #aŐ\ =("U@9z6edc)S)rڮLה+F+Ֆ*d?E lxP|^7wx֜F@/J7 XmkR_O* ?_iPIc L 2oy۵)3sfyY瞧XHXWxNwQtg _Us܉SOYU!C9K}?YeCw<,e4ODC6NYC糶*m%y kKGgH/v[ӻnE/pw]?×76d|{ڞ.1 g N/ܽg_{}X*O}v)/p.e\Cw.ձY6z?{6[]ĻUd-鮺zsMӭ Ǡ*WӦŒ^[Ogos^H׵k/{K^\9oWT)쨿tmZIF _vW_si*2eLegCfvV(:7p;j$(ak?v. Y,y'3;JuTh=׋A?2?}iVBAfy q.ݻN ^z3>;cOqA"4洌_OǸC:fqQ.eSo\x^ܭXAUe<<֊-ڮNlwȼ&Z!)^p61Fx^#Aʁ `m>7O >)4R!oO:84|ٟgгOK,oБ:krC 1 .H΃zg9g;î}c a pN0tmߛ 9v!cmpʂ z9u}dۖ<ۙ{o?Ȓ K`JPWDy ƥBf!#YaB75) dy'߮Obn>׮QkS>U>T m+ dׂ,l󻹟t#oʳwjŇ6 {Mڌ6ܷK˦)"SWmr!C*@LWj|,/2u~mz|ɋ /OAuph,v1 @k;'٠<1Ц'Ly }VI{F?xdʗvʆ̒JOƿR~>Q]n[zlYoݻކ!*tS? 꿡`f^JB?'5pƆ݌ 6bYן^ow@lEI}m662 Ε;7!~z=.8A_o2MoSuyG<͚;WjeH@IDATlA7M-P{;yqiS_tw44YŪh\p7)w,F _SZO 0?wTה5\_EsZ'/euywS<O_(ug8 qBD~J\e{EWi'n*Evmy'ݽC: +uZ9.ox1ݫ?^'YT/ @>4ہ÷?4CWr ‚jPÌ\Jj*  =:;PJ( Bw˦̟鎅WaR?ّߎ'o3</ϴGEeR=~/cbD،?p ocX}B{r|#ݥU6},w6RrQIڦi!S_/ݳ6 %oKs=~O})a{nyX}̴򘒾s[P4l/BM 0Z5(~H7=Yģ o~Z"-?0=B;?V7:ƹ B{a]޿ GQA]ThȒp^[bg+,'"*H.0 7nN W?Q;dpk[r_G6jG٢<&D UArg}Ϟ\ݵY(_ A'iRԑ\/5N,;dyӃ  Cc N+2<ǥ^<`[ȁC?imC.r]!K g=-t"̳cyR cײ 68[ +Csec˲ m!Ce8E \pa]a#!?Bcy!O dY Gr ) <2K}gAD\@)ED΍w)Tx(HF|̬\p#H>Tg2s7}W݂\+vW̙x[QyjW>S;dnڔ*vsPP{bDҺ 7S}jk,/džLQtHk-'IUkB-\NK]_K†L~蓶A *)l9/$76xuɼ" ܹp8#C,?8;kR-;Q(J1}cE㏩UQdz?G>U?2 6v?œA Vz2R_9^2?\^h=ܥG._N^ ~|ao!@C6ڐKO?w'1uqQ(Gdr"L D/=_qeTvO#K 91Yg7 葇O޸7KB{惻,) 8 6>:-klʯ2?ۃN;6f.#kaYӳo"Exye\8S,OtQ>|,e-䑃O:89esp ~814Wm1Xt R.(eR$Ra@0A^^/e TYhmr%'r.Z_-??;^_ZY؞ me^L[{<-^>Q@r]JGu]O}2=[:jK:&ec=O~ܯP8t\S,,6[uyZΪDWdg(_ƿH!:ߑW?_Ϋ!+[ChPlȨ2ӵu;P+Z}['yʩ>Ѡ .4}i)VO;sΐ'z@F_(:.>mq0*B]_p%0JTrIʺ9wgr : ezH!yhto@,xwD{]1}6iԷRPO>ѡj?$bNv]*uA o͖t~>uW̚O/F+\m ݲCV[qGm.zu)U"c |}=[Oh&*&VlBҬ;MVg;J{*6_/:}|E(J7d%V_KD//k(xtU85+7O}dGP] 1PJ.FڏlORk ^k “#s!z RB5Ѭx7!Xr`<)kz[&CGp9,\,, ^clq{&nnHq,0Axds%TGmXVpYPrX_y2! ؏А>q*4x^>[;ٞc @vHr륾CFyLjX0iJa*!TE6kR`RCM[o}ȪD7OPGM5(~BOa"~LnAfm[ij _>{$BGU?ן`jl&nУiA #(2!%?u֗ <q:Xcޤr\^s CjT!λ 7,4)"|: . 炒7.>(7*mJ'E=R`:ƒn}ӱd5c R峮mC7{C:4t84oQDŽ<\~ݬ]X I"}M\$E*uV)D-<&1?x&lxX7ض%tGT( JQ4 D?@j,6wR6Q\L-LEb1(^D Wv&B$[k^{F;%1F<ֳs%f `7lL%yp87]l%LC52yۇf;868LϸtB{knp]&7mǥ <  vB B;w`u@@'o>ǿ S.|w8)eG;9M'F?Ȳ]ێm{>{=xLQ(ŢsPJ.]119_%$u]*̽Zh3jFKxDXhlцJjk[n}8_M n IVNשVK`z-&HjKm Iڌ&DSeJ?iCbTQᅁō?Ɠ{-)g'տ6_uyCi/kʮ2zdG¦ )׸T.JY{Mהm+ v ƑSq?"<|dY >]tylZVh}wiÖÁ~tdǟ_c! (ᘔ2뀻RE@C@#̶ l>.+g\ƝG cs/RB!, HHs>W2@?9D{qX #(_D?ؤ)UT2!49Yk+%BmB o!hw( l>&bat75,KKTТh3@g_ͦo,PDxJl_mYx2qi2NĘQm?YG RIBAe] zց f `YRulӴl<4Ӎd&\K6)}Y>f6e'϶з)`Ypt:l˲ vjYLC 6 n>4G]"5?MG6쓼M Yef@,lD k? #Q_Sl2{܉`x}W(JJ+H Ӥ/1 Fܘl]N]<2 *l,&'ş owLbbuh2%\nxkudž^hn_˸ߡ(i6m{%6da$GRdXCn֦C`<>I9Ǯ.6lZ.2/Q5:8c.g]N|7Oh/{=B)>ƂeF(t.lC=`H s%l4Wֺس,6t9x:nY듇ryo 'e#>z 7kEE S-b-E^"Nn Wm`mHLټ)~ZĚskK[mwj6ltJ6vϥ!lm<,v<ſACm?u_h .%mI5HwMWL鑥:ǔJ6f)ks6 I`]ˑGYYx:>|ֳrvr9L#u)Bî˄=Fm`(>le }RӰgR.e]!},#pI&, O BN,)6(BA vH->\Ge2Id( )>'w˜F<}ڎN}eͺCF,P@R DIECSՖtL\X-zUNPv/.LjG<[eG+oz){# e|Ż`?d菴;ѯUa8Г`z<6e'VFĴJH o/1j/Ng0z2wq}aL'uz!Jm]uth1NFk29s #2`͚x`S:ӕ| < ܛEV -:J=$EJXg%DZ>J7) !&tϾ]vȲVem*^#6@Ö?`LͷQ 0υܸ #b{۞Sx7C#cYRd< -/mg@-o`~ٮ^$3z6dW)rUET)H^iYȠ~R$$2G ?uEk_2}9[t;zi ^ JJ>)W뿘q-6V_^F?Ɲ6r]ᆱ|e6QGiϸ6 D?<'jzLSF^t*㺣3 (eL_~XctFkLh:oX`s=yY#}` yó}˓L1߲`*;Tv&|[A66Ĩ1@mt<<˙.Rto:PYl9j7s_Oǯ7R`c#=˃.A38t#u N꺐mR˘nY둞}Iuח^ꮢ)*G(FiC—v{6GCyL 6[ 7Iiyщߖ{饾%Z؇ CqX)4ptm}ʋ]D :@ֵ<2ƽ ?׶N zG@Ytx0'O8ekjB)ڱ0  Svnc*(rn\m/2%᷵6bn6#tNutT7E(聥Y;_r.qF!`2G 7D( WQh:foO xc}ܭKI/iEicDZ ]>Dgg=)g!kSo8aBC`C82<"E2\N9o[l lyRrl!;dxL]1e,s%.p5 rBԠii&NceW%n#\4F߬M6f͘AntC??bJ)1)=tn(ɂ)6`P+ J F?"_ t ^9ʕLo[OԗWz]X_  ܩE7'w^hu.4ld=M=RxckY Fva'Ӱ: py ԁm [qtscu~Ȼ<m<?, KE0ډ)y[߶DuG lCOuB O|t\|)˂#o_#k曶4}~Yz,b+Kջ/ NZŒ`tb:VPLF?lo6rҡ/dהhȕ̓7ѧ!rf3&~f}ۥ7eko?8|}\S5`)h1`ԚK}u,_Y:MRq%e u2ylTОn~6l|'~E cyo@}mvSֶSeCC}>k~t9+}Yе㙏>qȢc1)+KnBCl>1 q<H=:} <` `C']xX4"oyl߸;=Oݨ6nލbWHeQ/"{,Ԑm+X e-l(Zؔ'i$1joO}cmdfTuIMm5_ZO ŔNAQ?k: @_ZS׈q©3ׁ$(mx<߇^=@wǨ2>)v0O}#ɘ ZM XWWsp~d ]YơCu;yc:y֢CtR9XǢ" V v yRA`{)mܤtw=F~Ox=,NNE$\8۞Slϕ}"uGtts O CM3@ &-ֱ?nle?r9'2'x ;^a_(N`YPn Y0Q(V\7 BBEm٬oͶ6҈SIbyQ/ +oqU y5a?X݊W)\0@GDZ[.__?=x߽1mWq݌Kj\wˈ7q%cV!ɏ,7+BT;D%X q/kF6UՔ5*yxxkao`,o9t|p4N)N]lr^o:@`[fYI?.)L:$mdc<˃48(f>E`Y6#pӭq1t\EE:x}ofАyۀRY, ,cStD>|G^~LApLb ppq[Y,TuT),abزȉ_Yo͜q Ky3w軞1i1PEG%ͿAh4Wh-qƑ]?O ug}ڠ~qQa i\qB炁hIbwM[BPDݩs1oo}eVcuFSe#q C2)b{uYֵp"2g=C>yRb;R6 lˀ||ӑ>9Ƃ e Q`äȹ\`6#=BУ' (O iB{~yѥȑGy9m Sv;= 8}a~>Cf&ePr1V* \2HCPCT&L5yM?KOp1IſN6_ŽF͊]ofHK/2q,p(,+-a7mYwŋ~أݣڌyL86(tcpLQͿ܂Dԃ)[7?!f s jog3>u:Aʚ#|O<|#:Xw"GBn)y|rkϺy=.uyHa9XErGz䱳T>- "oYp 6j-k}7뻬")liYb4Rt.uH9m2./'8.?'믿Ioa˝zna _'通G".*eD\EɋĨFa["{kᢴ6]q L%pms6;E9XÐڐ[9F ֓ɇ7 Asu'X_3D,C܇屍M .':)2ʆMȺ>uї]E6óׁ^Zߺ")|', o eHF y-ȅEz(}Ry<m@7:6}A#O\CyG>/+2'rқ7d2Q2VwMABc%GF*>q*R:JS@@>`_⤐DL%l53xBh/qi/Gg Im8߸¨Sxlןؼ`ĠBmCS}8aԛۿ /R6NMRhp_RwYpҩМwͲ,8]u.%*H1lؐ)[4qϿq:#!QRr-2ZkO]?bܨ"]menXʒF328Y8klzw0|sǴuɃ#5^>L#':m@7 [uy4Rx6&؃F2fXc>|g[&'L`TN(*`-4]yc߀7$\.9|ue,B#u^h:8y2 y :6Y)OhןvmGĈ_mq\W{{k^zgY;d80L+@ueхo9ȍӱ(yl1}|w rIl[2.~ցnk9:qY'7e3H_/ l9c9MW\qaz+󣶜'E6+<7zRtgyR6Z\׼1?m_h䡝}2s:27X4z ^l FKD(/*+$Dzvl4k#qRkձ?4]tro6(+7Ϳb!:IOyw9sv9!FQ\Rt $&kq.?0@&|-=϶Fi:v@0)y{ >8λ o@Y} ouQ}wY%ml@'KAp6\ӱ낛FHAB|]``}do4#txօn7J/ڟi.0c>]6~#K\&K[R1*TcE)l$ TRi U6cBnşv6=#zVta0e% 2D"<䅴46}]#.mmH\(c|αN̿kni|^kdy6*Xczu,95 z;և^,D3 du ]]hm4Fl.p؃溁ێ pۀ[m\{M46DQ)qa iO56\'?|QBw-y~ĘSGhNxX:-y~JZcNAZ8=bDZ+qhoN9uid~!ВhġA;1=#N@K_`m=mwšҏ) W<>![gcrhlj/.|>NjۿD^㇗-尮ؽ?h!&ؼ5N!&(}lĚ'I5o,fD(/)q j:;<}Fqj:J7IkCƿEzăDMGc&t8INlSE0h@ m9QApRoαrrZ,lx@ƃyn>]-Siu[ r\]C]Ĥ6ytcyR [nƂ e\qزЀ\62dYBq` c`=N +4l9d@s _}kuC>O? {N4hXL7F3ͿNs9+og;Z[[6}J=e2' 5!Ɵ6gth"˺d^OGZR .!ұ:nGS!58mx- yB:`>YZ箏6X':-K@>=AЏțA']?hoF ˸prS,OtQ>Tؽ +  ņ Imtį7E,b@?$#/+ᑚOoܐe9hG  ŧ*)P?i< -w02XkZ'f#m4T݋CZkc8qm`=HR(viן'Gg3a5t Cw` gc{ㄵ+u4֠|d,rm)|<<{ aYG98.m2Ey66S(}`<viˁ1"Bc N;vmөO,o:)l9<>dB#B6~H ez8-KChN?T!"p6R~ ^fv0 Su)`ЖW,l4-Nws,B5ۋK[iQFO6j 8!28OҮ?O%wȰdSԸZw4&3(G:4l"Oʁ})|۲HᲰȃÛ3b]&d1g3]+m")4&\BPr R.I|jm9MVI)$>5Զ&+äKhj[NaR%4O -0)ħږder MSCmi2LJ&񩡶4Y&\BPr R.I|jm9MVI)$>5Զ&+äKhj[NaR%4O -0)ħږder MSCmi2LJ&񩡶4Y&\BPr R.I|jm9MVI)$>5Զ&+äKhj[NaR%4O -0)ħږder MSCmi2LJ&񩡶4Y&\BPr R.I|jm9MVI)$>5Զ&+äKhn;dxd81}gÏ0y&kZlǚyt8C}veHYB"N\PƅzY26QSiG L]xЭo:eM6Zl|6AOj?θs]8&9xdi| -ċ1Cbd&@1=1{Gd@'lq>JĨ! Kbh+PA%+i AOZkOm`#=0K=1{G(-9F`LtƐZS".%y6'`Tȓc-k>Xf7Ma#V"vێqRg.)~wDu e>M~7 :@Rnwn4pG@lȏs6df @2n%]Ò/1ƨq<8ſ&Gг/=Yo C E!s2@¡D ^JI=2wdzm! ǹRkjt6ȯڭG*фjˈo%d:+k ik>dzRXq^[HS{F$}/yzQT)kMh>h +ZY<)eI·l.8|dm[ 9t9l:>8>G〇/p[w;dfDCB9r 1M,<& 6-׏$B<@d0>AB6#&|hO3i6D $Dhoݏy2Ǫ7ǚXCn8,`^colYe_Ƒ훣pAp `As>@YeyDϸE`ccܤ ,,)1 "`A"6ltXPШ)_4T"h4ш- V J]>Ιжٝs=wfF$ĥ(jl?*ʀ />dNt;gc vHV!s> *l|Z1́8G#ƁnɎ  ߑӜ@- ^`qנh]sL94`bŕ>{mg ,;bJ#W (g`F 8F1YJYh-6QDt%hE 󉖂ź׵–q}zQ4?_@/\>:źzQ47 #©>Da˸>=(?g8 NO5quw(hn?jO sY|,N gc@A'{#bP |?hF쐳Sat༳չbLS<яؒ1x쑳l+rlI1M嶚+ &Td1#%UPz% 4LGӠ( "W{l4@יwuAnO?:8⸱c,/|a?y2?wiεP#ʵG4|梔9 }lcX` t ߐV*3/q8^ @Hr5$7r-:Y-.(f;. L.WAɫ#-:-pĈEj(?dCmRLv ;lA'_#K?mT[AT &miݲY FjiSOiCP_.f|>BexE?ժ_?pο7| ??q| ߿~mW~p<ϨXY+kf2֦97atl6w1ecD8KOv7^ 4 {1a'?h̩rt @-Ae.Uzjx+hR3q)h8#M?*yQmࠁ3W*HX<(Rȴ8x#N1/~)Xqp0q|Q>q#ăī̯HU5:I4#ڃ O.Ѯh`0'64XU:,X.>@1Ȕa+_jŇg@'=2bKm^mҁ^}Az=TopR}y h_Q! @5aga%X,hX 6̃3OG[>C+ ry. dZ4AΦ'c܀⊦-q-[aA#h@3WP\pz#kNs0قh4Yxub`#`k`t[+9b{T894r@,:tQؐW~4eS'&_[Qr]ȂP!fڰKaCkd:{JtT!7v&d:PVg8 矑N}tN:Vgr>>?x{Z\ ҁb/Z Vr0TtlcVr9( m+ka6O9MHFW1O[vK,ڈ~|_NtpHFRbRh@r5:Ic5N'[sSN좏J)<`SINm#W;Xt;Œ≏m=b19Ag]ZQ:) \`a8keO2i pοψo*L~]Wר" _/ cs26Ϯ>Vpܡhî-BddaWӖ|D:T6mմ%5_( 6jڒ/:]& %Bdц]M[R!\PdaWӖ|Dwւl2ڰiKjP"t*6jڒ/NZMFv5mIJpCeц]M[R!Z hî-Bul2ڰiKjP";YkA6mմ%5_(uMFv5mIJp'k-& %BNסhî-BddaWӖ|D:T6mմ%5_( 6jڒ/:]& %Bdц]M[R!\PdaWӖ|Dwւl2ڰiKjP"t*6jڒ/NZMFv5mIJpCeц]M[R!Z I?c ik~j3I|6#'"xU>١gފPгGO~CΦAk>,_G6cq_)MfjO@=e -4c>vRl/PG_m5˄ lKlNوIl@6j/;U4jX< 2ƹH݆B:Q`( Ѻ\,u-Vkmf]Yܝ(\mP 0_cn!xNLn#:_nrZaiʔ&n\a}++Bz{I=pP:Q`%x~iw}x i5H~n+6{E-~uQe)ǰ}b/*"D=PN.XJs硇J]{][3l{$M81MBZy윹Q` RKOp@#Ҭ?։%*:`bsԘ,l=96&'6-C&y'Ș_B&{bh1d>B れ[)z3s bNڢHKN xr^2嗭FYi^t,ĢK y +>;Cs@Ǣ+%rS[l:=C۠ы6y㋞5Qb;hnY*^i=PZWr44BR_W.-~y瞗~ߦٳJ .{gvE}+-ȯ?F8Gh\}O4~qx~ Isif_8tM M7|5觳xN -liO/|%nZ>I\CG5qz{KZr% CtkxE$gn \>ĀgfNKL#&>2S0~谕бȜZ4>S Wl0rƜA^xN@`[5Zۍ j< j,r&6PX0ߖcC\V2چz0 `d`K,?`|>접+~ d޲~$W0 h S^X+\,dPM.L!=,WʠDC]B4{8Y*p1A40hpT^b*"h ua 4dIޱ/H]vi5v==f1,Ce &[joEfOGE$iRy I4ԅ)DӸ SAh ?>wytߺi.㧼%KY$ .L!},WʠDC]B4{8Y*p1A40hpX^gft;+9/N蓍25ѓ)RKob%"h ua 4dTE$iRy L9OSǦ! ;p]BK]8*e*UP-\ePM.L!=,WʠDC]B4p9F՟/x^qP^mҒue62j-\ePM.L!=,WʠDC]B4{8Y*p1Aأw`,tZ~iƌjЖYf뮻#* t2(&PMN+\LePM.L!=,WʠDC]B4{8Y*p1A40hpT^b*"h ua 4ᢥC}[u G̴@ G:req- " ,hNc`^ɩFzn024&ځy.9ʣv)?q &6(B+?vA+C<@SN87-(*%4p=z:Ǣ DvC(`rH/2a#K0Mە_Dc,;r)L@jiaѠTUm#uYC9یTu,cȟwR@4j]O؏3t~L>RsSgEVc3enkʷ1 hQ}(>[>\G'gPζ-O`$`Mo|i]K!BUŨs;/}>1|%2҆$"QUj.?c;߹'\92\%oE6׬kyi>4^1w;aexIfm&Ӌ]7ۮi][ls}VzAz̰gW,]|bW|Ctʏ:l"JQTPt 8nyu^ q тڅ+'"XȤW\X/ْO-2x#AdžNHZzl#'vS.d'_aa#zA h{a\ev,ENFEEq:C~@ƂTx2}o='$;%a[4:UDuƯ4i(!F+4SiuK`CB-#L/tmWi3Y钟\.|7_m&^yUӻ蟜dꪫvo.ڪUb+Q/ v,{DݔX  ~GcLVuzoZ -ޒ}cˠBNo8,g?6Iw+.pw-O:qSyL|?<lhLk^{}[!]! OO !W! #vSO==s/#8"Ukbke_PƟ=cOp!A:aWzoj'H3fL\xA-q%0WiT|p;6` 3 2|ȯ9ukd}+b*knlV`@j*|x[b%׾vzz>RҀv|ڼm|sE$*kǷmwoq(tIT֎o#۔~+l&WmUZr7sW\hxwPC[o7=eĚ׭ _K/4 ϥ{?}{Ce]P:㶵YpH7mj=Q:Fy{љ鄯km={eEZv'd15QwaÎ;ʤXuDjDQA!v|b<Vx/"]WRT֎o#|hqHבDemƾ+X6N ,hH_£c\Z0Sz@-S<ŏG)9qgß0К#xQyA W.WamxAF`608$RŠaSR5\dl|c@G|HXȏ~JW.CLU*d(.0#6FO~#SS/z=1鄳M~z a(d!ѣI']\IST %L:L!??)C M?1Fvx.&׻]c{yj_vC{+;wq54)vN:l䢛Z, Y`(9f g&e<׾`Y.[9`[%Bz~6d,.pV4blZ, Y>-6M.-y;ln[M{8]V 0x 馛n6Uz +PbO#A ]̨9؎a< FdFz;E6[h4^r yGc=M1`Mi͑ d!̦Cdž(tjlF#bH&r1.|padi6Đ\1ء)?1v%6Q_aFbNtd7G漕Dv46CF2AM$-DmڢC|l*֟]=ӞoNкꫥH;\Lc/K}ٽj/r:]LHK/t_=d=nJׇ믳{n(# h$^;YS2~=N]tڬ"Eީ Z_W?O{bz&yjolemѓ1?G[gڂMN[du+&*lK7-OŘ /C9dai [ ?..uEዦrfU|zЭao}Muoţ4f!v!]_|ZmF U 2SNIw85mmI\׿E-ևjG|,߳w-1Ӫ?~h36̃>oՕ*e/ 9QF/_<{6lcnM~lȔ!cL<42̆&6NK̘^qo~a6KA8uHъ- ) 谧+v`WA'x)'~lvj3`OeC:⊙Њldžg,0^[MEkh̒ծMIjۊju[,>Y\UYk*Z;t)X|!,TvRmgq1BVYVgh\tE~vN"֯S_|]w|4{$Ye>g͇ᱻ2qӥbbuMEk.E8~#deu֚N7]-q|n]>񬑳k_g3 Afe[suᶙmm7tD MI뚒eMbe?ʲ:kMEk.E8~#deu֚L{_4Gw^cP|wEO9tџQHòʢKOL]zyZ}U}d5RTHY߶r5UqM[,>@`YeYMn֙ʫX:\!3ɏ_mv/MRK6fnrpu̴ 9''B搴)g?DGsQ \]4=޲GZil˜Ϥ|/xA㏥3[m]!yfOo~~?~{}jV-t;ߞzv5]>kYΎ;~>M_p$UXyɲMI?Xh .Y4+Y-nYNJ{V^ll+QKL l ) CmFD` 0 `9Z3*E0ʴ#>tL%]+ <8or3yMF D^jtVUk3|/KN~񟐶ui7{G_~./~twTWm1UoPvPMV\iן`rYM?7xc:otWj/HQ|Kѹ? 졉k#>r1^~z}<瞟>xrG^NIH8XW> :2p>#8s\} Ix ǞHzמ`yi ߟ7#&s,:?oY}CXscii]ɹDXk>3>_[얥*Pwأc `_sj;c]sAS_W,ُ^/#g'|x=g ϩy$XȃG-^ޞ^gVctoV3Ͻ?CdO&O^!h({ǽ?J/|^6VZ>;fW;//oQla`!y٫C?=ZDR;E661{?wCaSL_yn+|eVwiw+?ϒ)3;ﲫ=dهmϟ!s⨣` ] N8t-7% ;d.+dh%0Ʊ #1eLQd3d1vXϗgNO'ۭj}[l'}6&L{G.\!V{,D3ޱyKg/Uڨc_[ߵ&ٕWfH*rmJcpSm1~c裏aO+fY’9zir!F%%`;cu2wӹ$ 5ݨ 5+%+ij>2A, ghLE91:9$0r(DqC}+ioxrjG]C|s/*`틉c6yPcȿBAdl}7BytP\^Oi׽ve_}ڴS]_/H?؍D3yA I32GEx9O:t-T#Yĵ\3cwuYgϐC )S0i(MT%}NE،?WMsYXkؘvz89z2vLN+g}Z=6C}m!dNfO@ygYǐ- A9?L^noy~}Ŷ'3' :kMl\>/q8y>Yk*xfYǐ-ɚ96>8fXxŅWr&rah+n;ʭv㧸#!;b!W'_N+7r?@iف`FVH^~jC cB^{1A|?ŘF cQ@NNr!WЫ?)4XqW~#]_ؓGC,K퐯K>d䯯M7b? YJTb0/ 7QHf0br R??}oMVg49/as} ]iظXn ,?ijHUjZ}љgi;|mJb},Wah@Q*D=chAUfkrR<@u}},7l[nJ_>Xv}iQf5K-dw6ٴ1FT^^ 7"=PkLHv˒MF[y<-ɯg1i)wygm77r.5K>?FŠVB0?{=ԗBn`߯yS֛b2Ks0]?p@|Ԟtle 2o-KC?묳UWifO3f>TmJ{]Vl{}*6/h޲Il&\QAXQ'Քt®騃`oC;UHXTCv v)7>|i-T'L˅|-DRY}bx5M\i+_i o1W5i7q^T5uGRS 1?c xc"Yl`O4'.@xO#y쿷h!ʿFYs;yKi~_V\qD˶nSV X_*Su/jmW\Ť޲TWz Zvԃ&ZS _>`YGZWS zx]`>CO[26r1lc<'6j\!.I*%n`lM]lQ*22+&I^-6v@|$aɃL1҃ɯxXC&e*\fĽg{A;H/.]EWRkŒ /tΏα/ i}aE<?@IDATo|cz:P2};@<WAډtq9U2@5Hz*S̕W^i$9;ם ?saۃ|nLC/EsFPu><2k_1(Ʊ?S<.4~_~vk289Y?_&ڳn]}P+v硾WC}Oi -kk֮N:+vcX_q6eʴ[DQ6ŭ^ynYb,x0(!Wʜr遇Q]o=}i{ mCOy1GcVzUz۪B|GjWѕx]+yqkXYve{|eM+co-{ԩmJv v{+9?]8.TN-?og11A<ózt]]&걑z+0zbdķ#ix6tE_r_#/bcb>X1]P&Kk#0t ,kAQ1b k`G{S|TTFzN1Oy<@'k3ia[rT2L :\jWd(r]6b>{oLc"uǐQUW^:, iO;ӌ3m0?>qᔱs-Ov͞{x髧ja|U "0똏a"=@7Lwۮlz-imxboٺnLdng iv[̶'Vgnq7qeE7VG.cۦ7[bʵIZ;J\uM9~K8Uվկ\5 b:t=ԗuQ>*&Ǐ[1.B7M~;`g/6g/xs1a% 얥e}G؂ݲZbWO:)9쬃llW}Ee裏v߭u`| :?g?nCSsZ.3g[n۔JÚGBDJa#Jųs^= T_g2똏E,Q=C$`*۔c93@.\G`SLɕ9~^ 94rlCSG;ht,Zy-2*qOqa QC Z\I6H1:" OPlx|:@F51sb<M3تXj [@y>d˟ZXG[Gm~(.:f͚5^u? MQT,) \ %4E d0?RC(p G*p.<끗}u#̰9_4--Re]6ӓKlR8 ۿ>l8wy#u,~iM7GL/V_C(p G*p.|{_P9Q;X`bW ;찴 (R#!&lwunE+%\a G*p.|HQyK.$XgrIk>1:`]esǥuⲯ0T,}7_l+p.|HQ2ϑ<ˮj{֒ղZ(Sos+GJ(w=mFY۲}??s%2X!JS@#K8Di\k܎gLe?Û\cB~=v?6mJbxcBƮ[ny2X!JS@qɋw#o~czk^X3۔N;ٮɷ)5!i 7`vub (p G*p.|HQ2X!JS@#K8Di`Lᖥlc /b4j,@ǩ9-~<1gvvL46+?}}1?AF~H(01xQ8 j<:,~_6Ň1Pix0tЊ%@BbJ"ʃ6#KH_yy|C>ڢq2z˒ʽ0f0Ge! %Iw7,uʥ -d!J5k3SٳgcqFZS>Ou6Q"^e-|Llh,DUK.#St e! @;ӗUN>jq%_o-KEyxu6qɥ?Iʬx[Q R]Lboz9]E =<( Y,O?|ںobR"fzՖƩ(aiޗjN~C8D, ,d!JZÅMqte! @ 2W{}㳙bWDpWm-n!٥f4ޠĂkݎqgvK[bol\Wʠ+ҤѲ\,d!JZ|޲tuy,}Ğ!zPGg>f=)YM״[8-]cUYi#4E1x{D{𣶨s18j+enMk@,^T<( YEs;#ۿwo E|[L;hݦ|9?c?V|(K[j6ZVk\,Dk-Y҉HNŵ, Wot9bcc.+=xd$b lt_l9)r 6flLq)4~lGZ+l/՜{M~a#WzWl %h*X:*,*>Tʯu1ڑzȿ0o; ۉ;ԯ6!ye?7|_Ξ)`GU_EU٦%\m}=gwv&*Zۧ]yo}aQ?}a㎰3BؕSV:\}ռcc8Mn>PǜEƬ8wfo^zE +ZGHr" +ZGHϐ*kݲTKY%ގ-Y%MgUςa[ plqw*܀7?CnMGT 2Ч~=0^9c#|S|k!;[Xۚg T5aĢe"ҲjamQ)l8ZDZM顇VxcNNw٫zr&Zf/6`u]_zu²^ۢ6 ~K.$\,0lB#qXsbܔtأS tzA@4r)c. MshQ-K|"?1Phtʏ[( EbGb0 ВY}SQ ,z6lbT`ʏ^@ @";@~ʯEZ<~h1ڢn~2(h'4Jl+"*ܤo# ovm+1T{WZiθK07?{lV:餯[oҁ67쵊;xn璣E7Ykt.Jl?yJ(vO8ss"%Z ^ W(QW*MxU S{qom ;_s۶ؚGFNB3o#N9q|ϯʫI#?1ώU+t=gwe$աx+S֝>2 +dSO/goڹUSTz)ncvE1=>^!SUBnD2,Nߥ?,o=yo:{Xs-{{ԃ*YGGI}^&}1-7F,NWQ_WW[] ` ,iż=X~v,W䔇,ƗNX#cǎtbMCG1Om:b&5S4!b! TQ,C-0PPh? ,Wq ؒ 2@⻸caxe\mxȿ[ge_i?2'6j8I^~T _9=E_b>7d,BpRXg9/g,l0&_%m;MH۾ui7:>Uer}3f"*&w?m w6;Jo_z9?IގB~ի?`ʟ^0sꥼe#ӤerX7!ii׽GWpXZwʋBZҦ:U3 Z~d.vXC}W_}2wۤ~y{h"x.ogQݼvfB̶.CñV[m=ʱ>foSO|WW\9=ho2ƞbT>4?,[?ٮTҮ[rB!yRf k1:$G051oCMvN9%',6ĕ\mVv1h594C .C&L @c^N}Ȳ(vl2xO L^P^ k#h@~( ~RPLTLP\p|'[tqk`,GF?MOX9B΁N_E^2=Ta.Texލώ}KReX!%Fz.D~|_.7ivI:Lw"?Vzy?&?Lk؏6t0PJ)V' տLߵ[Sb='|tpؤY'h<ϭx ~{ERoU8[LW{P:} O/z1q)t6[9B~l{?kZ8vch[%6fEXT Wr¡>x 2W[[j渨b/xōo{?L +sF;^3]<ęc*{Y߲8KV^ў1㐟p,޲4,piUW)?6>(05O(71_0C}y96[=vp=3ܳ|b [~9}>ŝo1v&^|5_#`8^nY;6{CC:6v.6D9)`,::Yi? =_>:Ɩ21ĉ vAcC|0g㢃gd`: }@+;;x/#9)[u?hm 0tZ-t<+T2L ڢ-z+?JډHU.bA}ġ݊';y r2!.ƞ-Oqtb Õ:Um_*ѫm2,qogϷ(Xgq`X"u^~ig/{Ϻ޳Xo=iϋgGz؏WQMo m`71>+^hewcm<#XCc0x{Ek'.oƏ?O.M?:Gkf'CPZqx Q47~%5b| *^ǟˎ?Hd}5֍Q=}^3[M}-AMzQf\ve簘RmUv?>iOrv}P{J>elbMb&=v?VKW 29+Kʩ lXdcNF%Gh9*z|`#;䊋t` rŘ.yؒ1x쑳l8ȱU'4jX`@T}xʏWiB`@5uj0Nx*:\/F8F`l%aْK"cc 4 bl6ʃL>O}ݞ_@YzdX!Rh* 琿S6k1d'3Dva'{[+p|z+{(i&~P1o{-";U93ݷ:s$>iRª3 4[=ߵE1fW<&.=,j8cnP:򑮼L4z衇+/'aC'4P08hc1\/mV η+.limMI/4cJ5|iVG򗿒c_]G15rwqUWKG s9b>.aU_c/]Svy'>Ρq]3/iYa Y\ \͞!.Y81!U/V*ز3/q8^ @Hr5$7r-:Y^ &ëB8TlbĢG{hWm2ɉ6)\&;xڅ66|}OW I w4}CxG+Jw>Yw3/_{h:ضvZncn{+ӓO<0^B^.z{j?EM(V&'&Tk>}sv]k_MCr .N?y 4߫ZYU?O3U1܎ 2t\mU{c?ků즇\ .?s UE2>pcf7=cۓ &6vۼvky ]4͋\{ XO~?+g4sߓVx7fsSr ,z맗%ՃD/r]iD&Oq&'Q__1T9Yvٴk5ٳ݇ӭwjee\k} ޕVXˇ*Fj\,!;6͹b<:6 ȏȋƗ0/U<ᧃ$^ 4 {1a'?h̩rt @-Ae.Uzjx+hCl F]06-rF;|7E^l/;kL=b@.rQ:r_l_{AUQ2+)1d $Z":nӲNb*/vl"a~B ځ^ qNڭXQ8W^MMfW,U 09 x濮IaԱJYI`4_+/QۭL7~qyB/\E%vfv𪫬bAPWҶ{?KK~?via/ujੳ6m?W"[ۄc p*+7l]eN6a_*?o N.^3oLgs;g^qҹ<3#6{[fwMq-90 w?ߢ{ն(Czo`R뭟6}ɦvM ٲ:gVHM>߿9p ǟ nw-;^ufRO0BW#t;FKx`aS8>(n8tVgd)$MбM>ydxvE9AQp%N-2QA(G["W6(> '?':鑩jjS sU vv# htg3>x?׭9W?;B\́jq~^;g<2ui4.sKf9B\́jq\b KL<vp|t%!{J5Y"+_]-6`ThרQ\&>G`=vA{|SPUk(T64B\́jCs?o'ouؓmQ XGvwJWʙ㟍B! @5aE0?>7m,zShmiCg \Ad-}f#Vz$/\0@hM5OM[\[KF Ѐgί>6G$a+ X ȵh䱳8r0mGlW>rp&.rhXt!?'G:h bc+OL0E+0 2ˍ 4#^a1P6pcCk/huzG騆o8p=jq=;5.y9ofC&v]lSGGuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dpt91ց 7 ЬiѬ> Y$~>֝5=\hAgJ shs8>as5QGu!̩uɕg33v#uC&V?\>ӱ\5=y5p4qcE쏭Sx '$8q"`6R9X뉒[6B mY1p!as~: L\aK:O=%s*3?ga溜3o.b\.v?n:gU׌ܺWg\.[_oG}3kzs}}'7} w59q.c6E]WL l0kZj"GQVG 0WH7~ewt"S8W[}ɐL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL4K}IL42c?ˊo~65.~`-57dφkZqaӾ[}4>x8Υ'~>ԡ'~ 1X'|'L8`P?<=M`#dS'8Dy'ɱ)#1ݟ}5b=6? 'Z+ˣ5Ea_4uͱ!`߅㫭wHN`@;k6[?oo WZ,6vopqv5L m M$~}b[yj|#hb6ƮqC;f}#?PW1G=<ٟb1栉 G  ssgܺNĞ\ٟp6!17ܐq8Ɂ8bO Dj1glЁ,0AUφ櫁R]x 1u6%|:1`a^W5kLDYf)Ux 4^w=w=3>]x |E<bC?8XϲvFdoúR.(ᇇo#gצi}tvKZZȡkZ ظԭ9o r=ڜ9Uhwl;&<I11u<(>ry[-4>O>N6n  >78 xȱ/~G֍~f{}bgnB ajR]Go\gxT R[nN.ꪸMң7sFžxTVO&*3)ћ9rg'W]kɫLzf5jUWc&E=z3Gl5d1#w~}2yUIQ̑ggcCl/d-a@.%qyֳ~3zOA>ka46Cv!`ۃ] o/|}+|l\ljS?=r9 $xR6O]P' qXq %cGxG3>7J^c]oǜ&NM {'1uVF5!5?4c:[gAH&fLSgA2C  |͘VY]I0S>#[ߺ0Aa_C3}Fu;$azfLSg|%'8٨`& XUc! :w=ŨA.svgL.B.>l_;ByNMul Vw繁c^f'D1fEs8x0\$1)̈́Á~|Ԁۍ98#䘇&o5џkSAK~6Ca~'1lo !ntM~H?$ӡ ͌տ?Y&gP7oo;iόYnLGo=w}ģ`}DOb5e3c}~y?c?\oK|&ޞX֑~냷-Sp4q֙~S%`!z'~h|ֱ9 cmM 9>4G#hG4~DM1#f/RǶ:^38OF;hkeП @䈉6!38y& y$瘱 yMan%{O>m8 DE*-unxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9_Y0XŚWqYư 5ej .Y A8ȷy'9bs;`&yCS߽X#F#oӃh|< ĢpjC8!ƭɑvr@>j÷}\n#zU;& O5'&ɥB/MM9A0օCŝ;>h0*+lI 7Vz(]m+TW{R5^>1k\y/uqumămz&YϟQyvX^Ktn?@o7'YϟI'ߖ_Z<n<`f1liʚlṔc0ɷG&˵:>퇍X|mb~)vp"6d``Q؁:j qƬ['9\}ƉQ"M  xͷ$r.1xAa-XɯFMlr697dž̃=v'9[ =R.+y팩e =Kn`yE_<}kxn󰮿q=R.+>׉H #58hۼ"󰮿q=R.+>׉H #58hۼ"_#`()5fz!nnŶ.6lq/`u|j! 5]mQ cN?1xlBO|XMx`&p\mmc0|l@xư}C÷N&bۿօL]y`>65~E󱧚0ں}#ў|l#q쇏2dQNmU>% 1sm: 7ْ6=AXd8skc:5]*y Z"Og?'4kSIj8.CJ i:7=2i?OSMQֹYwzuo=Σw.wo~y۟ةX4N w8%7`:6TprԳ~#n8ִ?<<&Dը >L Ě襈kMzPc}%(9|ޔY @{Rű[w|!~^Gl=EX]&r\;qOZ_^I,Yϟ?6'fdy'KJZ?>eq*Z. خp-pG6V׵3$G,G9EZ?6u8qlUqЫ6 nR9n?jq꓏I{DF89`O8hkۓnȭv೟o?qs<s&67~e)'.g HOo"{;3'i_?oqk4vҏ'i[ߺCCdo'8z<@g=g=CCdo'8z<@ڞ??ٌ\``@#']9̅W}\8yö.B&ƺkx15rW?b\8#悭c<Z_G1==( a#(u4``ֵ7&Z?׏9ׇM Zi?x_sF6N-Ƌذ:+K}2(!M+noz0T}wZV~;pZM_ߺA_G Y=$*h[?WEljsFm٦pW_my_>u.?uZ?DA!>?u|FZN4/< ,ˢK8ҟ?DW$h1|:&nwf#~|4qk Wk86sj=L/7g:Y ںg3+xԚSGMk\o~G .fW ,AkKU vZRevBT]*o.UfW ,AkKU vZRevBT]*o.UfW ,AkKU vZݵOlv'ܹy0_ Om/ K#@X?7ĤOoɔk;UpZRcvBT]*o.UfW ,AkKU vZRevBT]*o.UfW ,AkKU vZRevBT]*o.UfW ,AkKU vZRevB% ~eUs& 0 VCkZj5569ko5\9h~4"֘Zp𛇶?x!*@}]S;ހ c>~ȭHJ'Y8xb4p’WGGM7*j}N ![~3fdŚ'[yZ|m4>5Xl~e~;eA"D8HenW4 &LHgƇs|>Uef&5u.z=q&Xp!ӿ{Wt_g\ذrW¿yv}zꟓw6OwCƷlw'KL,EN\-%0Ay,soܢ'iL i27E&jsak}j/ v*ssZK0a6<F'iL i27E&jsak}j/ v*ssZK0a6<F'iL i27E&jsak}j/ v*ssZK0a6<F'i !8؜ PuFG`m|Glq \|$[uhkmǀFs̬9:$g9sNWlgWLjmkrO݄A L6aPs`1vNIS1lo6=kkÏ0.1qq7L?1\6j]?}?ْH˂F }ʁYDs7 p@}X:CV5[?<`T\_b}?ϟ_O}j_B>W^֦Rl]h׸n]c}l >1v!WP"M.'p xx8ur6AGSÃS_+q lE(<'?\9Ț&IZqfcO\X^ևgk«X<&#jXy1G&˿!Ck -Tv~:&MPRLӕMk𙯳J*K"zbrz7vg4wInufXߺ2/nn 7ЬЬ5y0+̓`#LA䢉ǚWq 6-ka/rY73Z~Fȫj1j9'rrSX ߩPnbSG귡'` 6NMNZhO5mr.8\Q1! $\0}A]% 1bg,2FC68K B|W%"H>#}[![2Mk*t3OP±|oO=XG"hl: ܌nҸCRm'N<P\ֳ<:uЎ hA26T9̫t=m55j>OEGi#t7qr) !`lhŁ|7!U.y9AibM`hj0 WnC=NZ5Kk/0#F zf |d/uj!7ydMjpA9 T7: Clؙ-Nյ-VLܑw9 Tך5|#hr;e6IuF}.GxT玼3GIg=g=x`B}Ԝvқ$P]x W͚)~֏(l 4b r l8 yc /ѫ֌pl/^ǁF[[Mzs p|ֳݹsmwki'/=KeAoub5[_#6T+'O<<zIYe=w=3_7wzzړk}hH܂lx5>;TnÁg]^GolԠ<}w}0ű8z!5}˥-v|59Fk66Tj3q 0p wWn:pn.Xs_ۍ hel'L_7R&N65W>:Fm駆cOO6bF~S>8vQ) e'i{CHDÃi%{VfDډEC$-1^ViZ4o>vOLux"RN%txY2)zύSs==2땲~o763o.TIEg_]Y {v*lbrj#\6N.imNO{c'6dgX >E@q͍ONhW܌c.X;Mñu,zVy5ZZkmG ~>Yɫc>O4NSfmJ |'D}O<`rkML})p z=6T8+`8ԩ1{GQ縭U֡xs7?z΃XbXP& cւß4cT#v! _G⍲zWsx!hc?~w=N.ILm!`9|6-p70D(`mbIxbj|kE8să55cO6ObQ^Ÿc\chm=|)~ou:Dr dr3NuǍտ[s:MԘ+A 1տvu9&j )Ya52]()Y_u]#ӅMO+A 1uyX_52]()Y_u]#ӅMO+A 1'Wo^WA'6=`M f &:R&`j/\4B}n簾dXWXZ[8g9Sb]׸||t79 ڍ =YlO56.zԁoL5M~q~0~Htpp O]D8==lj/ nڐ98FGee  h#eW5;2+e\(Pu/ ͸~`{8 y(0'Ny@_ ɆWO ̖x9?Lfp \1>O۟#Ա6uQWaf6d9=uī\z$P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PG\'#{ϰ ԣuu1 ;@}:rx=N$Pv)׉3Dptu1 ;@=P\'#{ϰ ԇ)׉3Dhw@rx=N$PGcp͚Ӏ͏ME` ?\o6qOq֭O86q[?6Qu_œX\yk0æ?1 LMB8Gꀰl!X؊B'okOMk.u Ј~4AMj#r/ocCk!VsyGhi>4B~p(~jx{lfzi BmĐ\ [k{ٿf MYߺZX_ O5{{lfa}\sӲz歰qy !H,YOBp>N6#F`=SH=o|l0`7R_7dl5|S>< ȅ[s`,\ WA?6\>p2:H'fN*(>1`#:uOMA6xm6|n1_Y[829R1 }m9Cz_Қ岮kSs^u͈9oέo]uO8~ԧ9Cfz7}?xw}nxW\GyhO?6kSص|&B1qDm5A9Nm5y3yg CA[{q0qr}*&v2:h6 6ԁO&X{?RO>Fq4n4>4?'1B=M:[ql<se{Wjpw@+2Hp%keW N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>dWT N#L>*36 GhnP`#XFYsqMl&56=Gzz?\zGOzgMupor[qПyfSSߤ+y `1B6E{ A䚧|1|0ΛqloN>vLA_(ߐyEY"h1klj7=V qnWsMOrkG-ֺgG>&F.kc>c֧?BMusØ!˩Alpl8QBqd? j]mOށ9q+pܐp\o?7'm-|ƭDQKyɥZ;`rqs   f/@䙫sƆ ʍQx 4^lh(;1X؅03Pgc\qͷ9So v%LxZ]kτ`(Oan2[0AUk=w==ꃡIG%Q̑l5d1#w~}2yUIQ̑?[>x̤Go]󿟭fL^uU|0|mh=2ǹ"᭽6XŦ65.@PN'ek8u  uٟb>\11m-|G~1ss<E(O9օ_&}kwl}8wBڙSwl< Ya$LP_C3}F+ jBk~h=u$+30AЌ{lu5kW 1uO3u! &5?4c:[gd[_C&7Lk~h=uWw+KN ֘n0^1nc>~ף[«P9g|"ö#qPfo`LK=|:w<j1lzkayBCl?\4 IsrLo8=IG xݸ#8Byhl[169П;kS<46lW{FؐI($l|Ib1`X{ir/o1 qS8&Xϟatq>g=O< I,_S6<37ege>x8X1: Ag7U&b~|r&ng b=􇫐Osċ1鋍O~GsG=o=b/5klsxؘ<0l\&HD|N}ao=sgmO>0y`gO8xog9QJP'ZӆߐyhG}!@T2вo<^ ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otS2wkw]_3%] ]L Fׁ*9%{v5SunxNޭu}͔otSk% `Xy7;YA)yhj9a \_]vʡ8ҟ0 |8N{#6ڮhb714 p>Bm4BMKL?=g^@I, 6bz`'9 68|{7\M̕`|\~؈51&F{wb aC6h̺u"guM-bQ qP|A"'(KA.Řzn&Z+`ysl׉H #58hۼ"󰮿q=R.+>׉H #58hۼ"󰮿q=R.+Z^?š_S lءXlblF\˧B bX#?ص/>쏮9fgN&ć!:- nA6A: 90C'` 78|lm.k]ԕ&cSOOzw\={ [7yx[?b~?!C&ֹYS ]3Ѧp-iӳ*MտM3?^ۥr0%tnzVszrNñ695 MMj]NS2˩sӳ*a:Om՚I]q=y9\7<:.'`A;eM&P[~憏om4RGOP=q8Zr6aJZ 'A=l>cMCcrL[Ps͔MI^o֤H\997_Yp iC8ʇMX5 '!\uR{Z.5km*wUNuuXϟYϟcc=bFϟ7}Xyg󇏐{W^zzr l ײ8[ ~8lcxq]K=clOrrxꉛ#^acS1Ƕ?\j[nӐ&>8'XODnD#hj8쏶&X=릈jG >&>0JmbhhqPK>9WNqqXt/38z<@V5FCho'8z<@uo< ?DvR'i=g=g< ?DvR'cʼn*6a 4qڕ\xDžÑG]f qrWbT.>ڟuqYWLGZ+[6MĸjAOd$K}0'$L>䆫 5 9 AVE1Ec|L? !F>9&Oa?q;Uե*V~;X~|w>ry/<9i6*vMK'* vZ?_˻˯Ć ◾W\<W}xG{+]omqCW+VZRdvBT]*˫_ ?%]ԅ>U`ZK]̮bX跃R*^җ^>O];7YUҞdϮbX跃R*Xԥ*V~;h-u2U`ZK]̮bX跃R*Xԥ*V~;h-u2U`ZK]̮bX跃R*Xԥ*V~;h-u2U`ZK]̮bX跃R*Xԥ*V~;h-u2U`ZK]Tg?˃<ԣ5PR"Xz]7dWӃ,6bX4 V*dMK5kkVmr8'jrЎE1󃒵7m4 ؄G`/y`g_u~ڎZxtqN'F-y5_q`yt礱pb~u7_?cFjM6ZiMCG]E3\mr9C?`s+K,Y- zO'A*s ZK0a6pF _/pyol<|*6<ѯs/{_12/ ˕s&H`>]~7~##<[?$\_w;׽G/oǿl76d`CA#7'kL iluɅً`Ojw_]^=|lȼű!./M/ow{\|_gC|̨o}í5HM^Ňgk _ dY\윸ZV`?0ocv,^N'%0Aտn<;Uer&.oCenן?x_둇(K 7d^ 0&Z8XQl7[ܰ rx~ڎY#csXuLg3bgWlomirOMBc&`XA;hcb'\'ZN~lۇ|uw N6Xc z£}G͸O`'&FPKgu2D!r=%9cLƾ]WctX6ޟʁɦ={}]ۿw.?<5'?Q~i^o_/.xgo o圼uok7d?L@IDAT|+!W&Nl%o|_?ܮ=g?;ŕ';oy\>Ɇw} 'Y-'PK}|=ήh+bY_ߺT??|(6c}OGc=/!ͿORusTFŒ8_!&~e[up?\>f=c4o|b V⃫#Ԡ"mkakh{:ǥ5>?` 96}8_?\m0{iOfP£1zÕ9`O m-.GYkEŘ0W:V|!ߚ*&I&q_{97zj~ 摃so|[%/-o<>S=\F~enXc- dvlԹw_nW.x#Asg|W󫗧>u+Ko;}-{9)[dEaX|"OFt%x"mؐyͫf!bz2'~?OؐyՁsο7ķ3xuo dv/Snv](ەT^%~5KfXOy^zOW'tKG?rw<t{/M_t>ad h!f݌ZgIm\f "MM>֤ZߘCN᱙B &"&κ՚#6B^UsQ9ZPZNwʥ>W =YlC7Pqb%7U%'n-bj'ĶnPLŘ~fIHa~Ӄu LPqc~[ +4VG^%L80QVM6{{o/S#O#-|wڨ!*˿z.gy<_۾ډ>ϸ=})3/??]_ZG_;77dv3}6.xzlh?_>O^^o*`^>]g__yq=n 3-#jя}o_ _5./HW8>s{Oy/|Q{ ?oo G~../t,},aLcyۗ߻7_?&a=]k?u_u`Y>y>xa?otI:ԧ>j?SS[~~g>@ڕõ|o|N=+K<6dj7B͇?ۗ}wcqޟƦd;_DZQwy06X{߮/Dw~w.όyṗygp?|'wڇ)G?ˇ?x&_}n/%E{k ( $P VrVDr0D# * FQEE(QPAtMy};V{9U}o[]\wuoAfPQ8%ۿ&'w' )mZdg}k &* ׿xZIW~sw65bWoYfޟ5Yϴ%+vBf 1 5Ƿ<ďey=4O8l`[r9bC\]lmsj_Cs8>41R?d\?։l{;xΉi L`SAnYO aseN mљƁG#h]>86r9tm9m["\.v>i$ dݦ &0nuf-y#='(nkgbZLΖW|}s~sQ/| "p?UM-AO>x{ Wgm|J/v}fW7 7  ݬzo:uɲ}۷[Ej=A͞{ռ/?8q~_ꑩƀ n͛> 4}#zxN+j;7zps'H .yԑs8{wvۅ w5o?-7+Skm٦}+U8k^cu?"nVڐ >џRZv5%v.7| C2ȒL{Mn|G?R&|Вjx&an[ޫ?.Gyd'=QeR\~'ZYdM=v۳}mk8w[QAȣ5&aF/nު6Ng'?ټA}f Pa|#8yk_Q?k2lۂ9_bs!4\sU%޴_sܷ;q>Ӝ}hc^&Vlvzrhw ~3ݍկ~uWz+ݲmM]va>98K&qvqf7I3!M~M^fXeUuR6&F*Ct^>+ "?Ԗ ֆ.jƩ'ՄX-w׿xl5Mi:3 ƸU5\Iۛ]qɳWveۊ +hbʥe\Y8|0 fP <~GMvCg^l/?"s qr9,\-|⢃簟e`b Lq;y`_Y`bPE`Ώ3Srb?220л|PnC@ r20vC̓t!' ~(ȉCo),>%>C[@51])çuGӭ^3?LMӛVzR/fBoE%꿠}͎Y9K'{NsŀpЀL=Ceo2-Kt%}7k5G~WҕhuNs@ꌏמןmH(Bōh3to%JE{HN~p[ousGO_/cĢOͦm|뗐4 ^L>OiwNJVS/޺F([Bw?~q&dޭ 㢋lfs̼G6kD/Ni)8%՟Zk6yEwse:BVe_yU _a\?q}1F(Eު&lO7~{ї.,=\Ot&\;j.[ 6r'"ێ۾%n ),la.m3 O9)rE;1: ' Iy0߶288l X'7 2s>t'뱅_~)>Ia_폼,o'+Q b1Jui~ ++MRk^/#B[1 J(|C Gc5$Fxꓜjb?b͚GGj6ڨ7z=h.(^va5c2dMkõǹ眧a&+ytԟ-<Bi4S3?(@9uW)y3Q۟U(X4W^se!=^v&B8cn 4Ǜ]ڄ&s'hb?YN8<4{ih=3KLk}A$#kRtI3gWh]w]MhxDG=VJfuG1f啟^һ /(jOzy9[?gDn-OsWk\G$zx;DZ{ֳlx/|o|=^{~o:hF+6h]n^կ}y] Q/}KQ p}i69,qsKf;Q7W]u}ڿnͷhXMĊZwɆ/|C_E`kZ{ۚ|Q&Bry>/bz[c76>*Lv ?Y!SNKi޸+l.DڈܺNxt&zW{^– rXsO:t1j~Vn1aY~իM4A暫#5 &c=  zziZtӍ3Ju}Y>OiZ9 aJQ2_k&~_zVBh{&O7ъk>Q+h`晫&w><5#5g|؃%GΟu7khVSڴg&rt޾>OjBGE4z,&豴CK.)zf{Ѫg=? ͪ\-lZٴgrcM%nC-1#&RmN&*ğIյ`fn*_ &tvǟnv>kמ*ؓIT'rl߲$# ^7>x}=֘sm*&5׎2D7x%/YZ ŷ+:T& ǍXeFQ|W7$dO~349HnD+B^?>O6MҼV`xDy&/фx,K &3m=+q#:W\#e/B.k˞A7WT)>JI?}j//֥VIxTz.} 6=B οmfBniڤڥmJ[fHctQtxѷdqPw@VߑmpGѷGjVʜw6c̵0{k4UZ_V7M;Eޓ0qVģ㰭DQ̛Ɨ l; ]dۻ ;A# ~)+i˼cź|:4]#Ђ-I]iW,$\'r Ⴭ'=)qs#1uOcd_ۻL1I'?9?|ǃ=v| SycgG l!A"0 ^|n^" n,SBy0Ԝp ]~l\<ARf&mM(2wJl4/m~k?j T-JkUVmn暏tsytE)*3yxܢi< =1pk ͖[m|mtԀ^KSO-%=&(o9yK^~7i?chc뮻H jK.g3[MK 6X_.檫iGkwu8qZunm?7|3GX6 8celڼw= sùOk_=d|>90կ}yV>U\smڪ6'~ˠww5!\~6璁{iɮG-M?_Pm}oT㚷yD} _lkRq&_}ac5!60 ʍ_+UᅱCj ޭ ʟ\=XI69]1:?î&{lQ!r צ=fkB=돛Pw+ⲙ5yEu֙[lBvOz#[Gn1):ȦMI: LSlx9+ ơ`6blv.o:¹cccOsrv_tl ?dRBl N*ںp\!;vжξt^ d>`B46:cCs+:yirn$:XXn! L]!Xg;⬵E5Vmbgardn^nǚhPۣc׿+bZ!݄N|wMx@IRgvA+_ۺ1?=B?10 zxϊC#9JU!_y+^!LlR p5W 9*G&?_Kꃷ2;hwlzڵ1zԪl$=Ge7cuCkX};vԄəg-97Ox=s=Lo 940Glm2wL.V+A wW]qe#L ˪9[mT?29"_EYcl?Fb:oo|lQhT;͌&ok]-ڠ՗fڷ,k^G?rh燐Ish/.nѝ}B&YR;:sf߷i%su],2*eMߠ V&Yo>EHUqwێR[X|jSVZYjEOj֥=f1CJdM4mzӞ汏ylyEkg?2Ilc'x3GJΟ) JJA+S9BsKmC@c^7^G-m]ZuD߯wGK6jN+qBf3/}x'6~z E``oƾp^b9q2q0qqi2GЙwYO_;t{e2MlAdwye: RNb=ؕ%QA  dOPc9i8leep|xAN~&Ot#s\]lk]dCG\[n(@8=A@3>{.QX_eʭ 6 D6o+9E{DfX8s" ֭ ;=woWȸ@%s/vh@Q\9[X1P)=dNգ ڄuZV{}YׄT&M4_pA "b m{K}uwy?& ?DB2{LIz‰CܺooZk {%t{Qc"0ĴҢ![6=>[{tZ#y܃}nZa~cj34iH|7}~ÍG2͎I_aźhZ7^Nw l2n^\o}'q+b1rJs 7ꉏ~=nt&*(۴ 'z6PתOЏhaWXQe[֚Z!^\Qor>h=7m.OIs>LV'5],^qr}C$ӈذ P0V\MRz绢XԿNi*?}?n6|oyV&/k^W4yh">+KmZ5? P۩hgXE,cmFU " BaK;!H#43XWa:JCs+V9K.0@=i=N5MYX۶F Ѐg:q\zbM$`; dse]q!2l``{·qITѡs &?ȏ>\&O|@q)69s>ߴ3!Ă&db9jCgؗ1jG dbV g;-AWkRK fm|;Ziph&Ӥ~eԕ1aA\3z@yURƱ ~dvz>Oգ2kPŦh5?6:/P^{}(MLL6~?23!#`Sj_v_E{ƚ̓s^LG-앶)6ebC2Z!1OP뫯Fo9K*y6+rv gϜ Px SZK[?"V0z¤Tq:-Ow#6e3|X+d^׋ OǗ xF?+rw7c=]bkX M|k/UkWƄ r&=q4+jO~*OW_ >(?O#FoHY9M{ 9JPJB&*lW&E{:hKk=Q7}W3xRXJ4glvrkm[& WdBLLj\Nz^o".=bGۨhZ͆/~Q6m><+Է>=5W_SmW4wTtrk<^J{$] z-9G&m''<[-26.FlWBOu1gf5yuIQ&-O|^~ + 1k K-t2m'dDhR;@x>О_h`ǷKx0X[196ā&&2x:)c]n0yq]OOˉN  `DJR=xc=|ρ=C[f=Fr8#㙷1zsE19A߯ >5e0B?'M ߩ54筧-~?GeGU uɧvmU:Zt!jaͻޡnl⻚6  &e steȒVџyk9?Ҿv%KIB 7M}4о'K*oi\!S^}Y/~q^ lȫ&d({cwMVDuI*Eg{,L)w $>&.a_,{#Kޏ,n=5|}{S0IL"NL2Ԧ!㍊zGX!usV% !:ߛBF+;ֹ7W|=Dc=yziuBFʄ hL8&2oKm^fMMȼ}uzKk__hj'4S[050q Lbn};ߋHCύ}j7ɹ>U3%&ڝYNZ/|!{~{z%9j[Ww=q]xKzEcwGcniԿ6/A{ۥ.8>B':oc4+xJ:6OkFͳt<[:k_XO:exx[t%P>x(J0&b2Psϼ/Yvec^[[N;-\` zkA?sC[2aC{-J\IIP2a<J솓+e¸f$솓+ex2m '5CIWʄq6I '5CIWʄd*)&Nj m(&Nj ɴURLf7 %_)5$QLf7 %_)Ɠin8J:R&kIn8J:R&'VI1pR3t|LlD1pR3t|LOb2f(J0ffꙓN:iO}~4YCIWʄq?d+& 5&d:XΦ?2{lcm9=,g` }xu0}`8:?JǼ/+Hi14cmtx flΘ3fg/-f8@# 7@3v@_`N{ G@ Yq8hͱ၌ig۲fñ),z⚶-6+ۣ[?2@XhTZ6ea9:h>a8tilmc_䌪C"W~6%CE \&gܪǑ[*(^k븈gKS`y|sOi])|"B\ٺÿ6}|BQθ&@e_SYnĪͣWxG{tԒ8(wN~4޲*Q*j l6zDzT;#!ݶ#D_hsgDgy'mke'J{H{xs=WY" O|ڌW{q2*+H+06O~ғ7z<؜C}7^޲n˦ЪQ\R,7Q=7(/Y*hKy-[]VbF;H&׷'jZWAkѣA4G]o}8X ݦʘ,uwm˛df]wk>j-+dS+dG+d6ߴFR_4Sk=d<"_5/?5\Kӣq5xG+X~+t[2gW˿Ov6eC_k/0}xj=aϙgٰll5ҿUgӍZmUk<ߜz~r캛VDzǰ]6df5UD_% =usd?. gҪ5iUj"8ڋϪ}DiU? ,Mzؙ3F6 UD咡e``m; c+W>ח]YAT3]LjoO=tA;^@L#=cxL3NT"6M@믵3VQ`E,tȌ#+O1FڇB}V^T'o},Æs~ׁ8m>`ˉ@[ a,4#;QQ`*ɃܨrE+ƎйQCd:ˀ;dġ~.P6hEo?x|3"k'9%fB)e RZZbUmkG-,5DQ3l^7[S;"&40W^d #s30wt; cB߿1<ǁ_PQ^G;&e0/P꿯_{-/`v,5\} ԟG6О5YWqNP޲L]:Z{ZW_u;G[xnm#5eo!6=G=5|[AK>PSٟD*ǩz$%]=MTp<wct1,3\+M}#KA%׻]넌&4!O+c犝if6mnFoOE(l fxN)i\ הmӬɣquK9_N<{LsGIvkuWǾ-Iq vIw=vDde?ٷܓ|[LU迭=j׿լ_~GE4Z9p9׉oףf^ v=B<[xt(}Od%QgюG7!jI@IDATuN%t KW15[&cj+z}b- Nm:(`xci?tJGA>#cjFc8v˄Fn[Aੀ€n l]'Lpr̻0C96%m-ls L~&<ĉCn F8>.rˠ9[pºoXe+SY:-׸Z@- 6fE $u0^K͛:`N|!ަQS~kԙoOE8_ڪ'7i!ͯgu Ҷ=iU‚-+UE2L60|^NoYZY1![bGugr#h>D0݋F_Z{\aӵBθozN2ʫK N-=wadɖ[l[뜔}XX_^ivmzf:%5ٲʳkyd?+^ݫSrt[Z}V3>6B,mdf˭=묳c{^}6[hkՊ(5LN:V:cnImгoBg=ܻI0'^.5&Xsl&iޣ4qm~߿@v2&?+XgjBO.N0tly^fM2\s5Q߼eZ89\{O}_{9CV(pwR%o@r-UW]й)~zu<+h @m+طINz۟FGZ값v^}6n/k=պn@~5^N~|;ZӶlɘ!𖧏h۷q4>SwiV~C |Фځ GUD14}õɩ 2_ԡ OԕDx g1w cD+Sl:nuK/$dO]Jdө- 7|Ayg3V<_'ʯ^4k m Ԍcxc\C`8ALcC;<9Wda6\sɉpY].'9s<. ޱ(j}8?vtiYgbP9)`3GOE2i;~փL@f,F)r,qmGbXiL.IYL~:U[81n4 I|d?)bZӚ 4~? 9?Ade?5x)=eF>3YrwJwu].S,oz=&jO:vn{%i]sM[J]ڟ S27aY_:A]%+O}\WPE )ڪX3_mkkU")sL?Uc5Vh~24{]&d$t0e x!Lpaǁ JvȱcqYֻ̓lI`B :cǁL"8E抋X`d4y w%ccFl{o?hUdm;b w9f΍@@9cP09 n[Hȍ$޾q~tE#mw,d\vtE%;xRոg>}y3Qvd{XBPc6 3.6}.yCdQJdѳ1θ"<,*Y z69f>Wd֌ƤOƪDn0ѳɛo] JcO mimV|Y^kl\w'VȜZqO Qh1k:r9_{~c[XcҁJg1'Yrgl?=y#p |EN~t~ :g//2xq~R:!\n|=2`뺘&K<8#U9ȍ}'֗v+D0,υC-ÅƖ=BBLq± cRȑ;=q!ۣ= p@F~:h@e{klb 6Ηs^NR1@iϚRjXR}1l[8cw/nj^ofV}f,}Dʥ1u=c)h^/#襡u!+dm5nW^o_ju[7yQ3ѷxѻ9_jzZfV:hƑ^ؖn8qWvxǗ|`|`bρ x_h|d}X?y"3|s : s.3wK4l8][OKAS8Wȍ v[6ۘF=y;=~ ?9sq=6w Ó+29 ;۸l{,1ׅ)&]<~tTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRC۵!_>i]v9m P6&SijlC.s S5H3lv> ̏Vp JEֵzGe.U=/8V+Л~Kv.kGu-Pl>gsKَMVg(6_p."lUd]wTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vTRCgua"vT0Vdc^';9mOÓ;6v_C#lC  `{p9'"#6F&\Vz{1;?,''r0Z8(69AwÃ0?r`@c s]e-c圃h_rz,!ЙΏ 2L 1Nn9:i  >n<> ΍`qmcql뉇?:>c(cfC{>A"{9%ov^U%&ԊXE6s$ YTcϺ>(g|l?~3 RM/Xn|7^Jh&Y0%jwƯz}TbHQb$ fI.g92EdLmg9cslg? ,N{{9q1!2X;<eytw.i 8!&]P$pskBg2b{Bq9mmdR} @J:b\"C6'iBf.H)j(4uhҕuYx>1in1c[tkC_ ߵ)Rhҕ}Yx>K;'B.t%lAO`SnMWN '0c)cP&+] ['A_iuĔ[E _.\41Si4qw~^ؓ݌7CrW2x"1L-lq2`)D- m>JICch jGۮ2.ܗ}4?7OmS /Z6M%H-ʧqYUU -0l-nSy݄mU4O-lqqxtfbsu^o*'Ƣ\Q =uǎk,rk\..<˘ ecntȭ'2y0vv~tS*%$P zhbVd@. caC8"CyqLMCWd;l s!\}uÖÀ?S,a 4$H_~WIJdAwfBc~s2+|먡]h&I0c^6JWʬ6Jן.FL31"}d]z9ei sZc!- C;Dz8w,l8COXA}<%u#chǴm3}9`"A9ym cpEDž& &7q'Fn>[&cF%?9h@0sLs,>&7@~hEy>DmLRi,7&@[ԍ0d2Gm$(c?O7g|{C9u)&rqKe603~??o|-W?G{u0dRlZ>dfdē g9P8>aOc|\h㢃>;\\:j"EKd2_4ұS(qLF:q%.|H2NQ%2/X)J_D&E#8Edhc(}Qt,/J\"eEKd2_4ұS(qLF:q%.|H2NQ%2/X)J_D&E#8Edhc(}Qt,/J\"eEKd2_4ұS(qLF:q%.|H2NQ%2/X)J_D&E#8Edhc(}Qt,/J\"eEKd2_4ұS(qLF:q%.|H2NQ%2u ,m1x&>'At7`˳cVp`=mm ^ykAn?]N<mb~./~;25ϼˀo`@ £# oZdA8>鉊Jà ,v\to9erL&Zz&Av||3m#t.(G,/Vyq7e3l*Bd%=1Γ-U*l%+Q[bUh󒔕j_s]}}V?CȓO}"-/Z`Jtd%jws[oSZghjhq+d6FXzh{œ&*| w`.ef %g `z =&jx965N `'(`LaXnݢkai\pȴco&bpwOnI-2m;x-w;|m6$_2-mF'!(`q-{Lc>:@خpbӏHcahC;rY5zhۊ ޫ,#<@`ck|hrMl8̀8cGb0t%-2@R`s`ˑcXRs~b>.+]G#=r?_C]  E"KJ-]E+&l1i\[ilr}σ=^G* =#P%d9YG1/|իުvp1^7^|+#PJᮿv&m'&2tƕc2-=8&96Ga.qL|9 _h)_|˱:bMl+Q4!c! \8m fɳ ]Yx<`AGG\|bY)B lp~b[c8'IBE{l'6r/c&qH2t:.CQ9X3-;9DUI~6y <W$"|Ȳ>=a!xwx7LtvZd;xW OOXH0o;!fƘ20cIoy1l0)rE;1: ''%aے˼" 4`c6΃>Н<_~)>:S9:jUC"ۍiǗP\G\7S126xFo;Og?k"j3ݺN߿Q7XM}oU\ ƘC̫^̶Tqyp|ǁ v`1orEVpY\rY=:Ƿ=#%"Cvr\Ǵ蘖;6A)Oя)֊`g[TM7HfЋc&&c ^@&cSn7^7^)ms31c&&3~ ^ \6g>Wք_{ˌ0S '<2^dm FUi168rf;|7ec\^b1vαxz992.u"g_.ূ+9U)! dvRb@X?+D|W\dжξt^ d[`Bds=`l?۱qvpy<49\7Cvҝ//XXn!)z nxݻl8ܬ#4Z4oC%c-2^跍 <esJ['ҡ2P[t 40^+(3Tb4?UJqA.hBf3/}0yРH` ?Hyr{"1/~9q2q0q|qi2GЙwYO_;Hr{ISIC@BOADA0&*@ $$$!ȓDࡒ@ַ|{9p)odj{9{ߙhΓb6d^eڒbđ!cC<2d}3A/mF]Qv7=wݎc7G 7y© `5r{07;2vPt䜃c` [~CO^v" ݚp+_΂>[-XfFGfij/ ̺wtMUiJY9Re^ۏ~ŕ%ן~ןtZ?.:1rzh.L~d=sW,bJe1ndc Ɓgy,oPh+\مbL>k:-R5iN]raw# jkDVD1Eau@6NgY\Ovbg>t6pN) p춓bK|m>\;dBD`Xʏ0&( X^v㯟ӯg7lfh.֏L8צA꿿C`bW8]8>6B,>0A!%Evj>"CK,r0wlݜCeC6NM{/`Qxdڈc#Ze( 觀 5Ff]csGC-vbxdc S&1m66vɺ9Cp=cY,1_b9еkwхOgCNThG 03$%bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQnXďGGD#>28dEHxlܱe;:y 9]vby-9blېoGSLj!ln`3ok}vdµ0n &l0}'e~ 6܁PKvpr0 r5:xbi;8t0$8n}F.6d7_Q"uKxdƇX9Z5tgaST!eқ _:*d3pճLzSB6W=Kˤ7IARu2Tfgi&)ȧr+RlzIo|dU2MROV \,-$JP!eқ ʭHYZ&I 򕪓B6W=Kˤ7IA>[*d3pճLz+U'ClzIo|*"Ufgi&)WN \,-$TnEU2MRT YZ&I ܊T!eқ _:*d3pճLzSB6W=Kˤ7IARu2Tfgi&)ȧr+RlzIo|dU2MROV \,-$JP!eқ ʭHYZ&I 򕪓B6W=Kˤ7IA>[*d3pճLz+U'C<=bsΜ? cc ;ŜӃn:~lZO~ ;M?͇!;6yB1/Ȍm6+d tl$lEI(.7;.Bwas'..x|cm SHƒ iWInH p*mpub_<-ȄVLv}MK@mp2!{oj-^Pk#6YolEhh cT=jz,?MK0mpcT_2xkskM[Mc(_7Γ~mB庫g,p1g> ͅ|;Y:$M< 2<؅PKN`u݋m!V`s<>bolJ BGf}ZFlҏ+'MZ9"Ƹ9׏~Ͽ~orXWѯ?;oxcdhsr:m]iGgn aCv.er!3%'D}e8rarg>6a̩,.6ė>|-8"F[|H``Щem,v&7x0p_|b6pñςY8v|yCncۨO?kCf{!eRy`dl)M5EZWa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k@_ˌ;N?s;xd.9 7\dςsҜ~SӺ!plov`gKMulCM8b!`[Xo6dbCl1䃬NgMm33B&eQtkbٰClcon}ۉ/ `q '9<( '$޶ 2{U XظPۿL1@6V6kLS"G_0Ji0l}c8/)McY_?6Asm>8iSصny=ɇ niW!s@^mpg}bAd Y`1(@вA *˦,O#T[c_V ^6eyI#rf)H*U,Ȱq" ,!%:>B EfBrE̟sՈZS;yg#0rMA#W[ o-xmroe/$K#& y>y kզ~3M'Lțk\6=_?&&Mv`5tU~ ib &;0}:תM%]cCg9,cg'1Ă˓Pةl16̟ņn뫇 du!`%d^ao汾 WQ{/Ls2:ivdŴa g`I|6R,cQv ;6rӎE=`㇈7i@IDAT1v9Q ÇNn60'.ane 65C?1dpRJ22櫧vj&R#zzGi68}XϿtIz N~&n#P_oKA" ](Q#oL]lmd g*!6>;Rփ<<|̭V"fb}S:26QΆS>jB9|E]r1>LIzoMCqvhseP(O[:"3?q&hC&N|Y86mF/8C5\\ Hl.pb2,_ ^yUmu2^+ϟ-$Q6U.8] ai xpحvj͸W VdR0s#;86A [ۺa*dXGc>tmr'` ob&@pk!X/ڹ}h6#Cb1l' &PGO>ֈDjc/I*IƐtC<9Q?qc51돧O{53IWbq1ןOWo'/-R{߅t#љcӇ;>9 ́p0ÉBc[va}0U,s\m4ŭ ;On[F9`SlE&ŵKv.Nx"!q8xXb σb¼bMs'vx|m43Ïb 1؉gg5Ҟ8&JY[sLaKi clj~ix4'BXdC2yH㴲M_4Y+lן~s,~_~O?ӉRĤf!@_w7ԗ.Y6dL ,)X$Ǯsy061>lbw<8Xb=a^L5B,dlvAro~HRͼŶFNtbeB,6b$1'Ă޻v ԦVF#M4&7At2|B~<ڎ\ħ1t U2ןII^G*VO7 ⓡU_ǡq$w&^=&N"VOu7'JrGhՓMnd(bWǡq$w&^=&N"VOu7'JrGhՓMnd(bRCfღ01NSA9?<ݼ`ЕC, 9/O.1G6vdAg}x m6On?2| 5Ap cl -jM4ntЍa4?;yB,n,s^ƢnkʉAC,~w$~2R.al[=lԟ!V89g9AFNF[F>g9ppctgsxژe44AҾV89ǟ,CA},eqs1 @?CҾV898_\΋vgAE9\  ]xp>υ 䀜W_|!mⰳ9o<.'=˜N"6tw dž]?:9ذIp(.ǷLFxll%6B!,0dXssl3ņ:`i}pq 2al%G`sygJ̉Z2~w$}$,%G\ϊ;җU1!ȓoYq'Cnu9B帢OIZgŝ }oyڴs+)1L\ &CO,/#n͒}z8?|l,vw8^rj,;G6sYll ;y 㼖|<15QOC;:y 0m~t냕Q+ϵ(clܲ!&];O<#b0v?'ps"k&u]pf=w;~`=!\?.ܠCMLyd)F\3+~O"IzXuZxǟtjiy-_uZxϿ~E!IˋRoUǪ'!@ӯ?ӯ?E!IˋRoUǪ'!@ݮ?,ư}5:zXa8Cpt6 sW6ceF~C- 1/Ɔ aK 1g;d 2` lnxn\zɕ녺J&[T ~7#!CHv<6|,03X|/y)wDX3?8<>dΜֳ\9>28djCGO. w rG`9d@T S]wqTg\jmÿZf|MjEF㯟q!댗ra~_R#d]x\Z8ӿaB~ĻD+K|Er7#&.b PqرF~g*Zr3Oᢋ.;7O Pt[\2?8rPit8X|pټ6C9' -"!-Ė;[8Kmhc#K{WB+g0ѹT<ŞMlBi8-,sy O:Ru6 m4'K<ƻ4lb@~MG7GMŞMlB?Ͽ83b.Ͽ2kd'g}cя5~q'Y{C-:sYؘ⢉ 6*`ygQs8Dmff~lf^=' 972xM}y kv-9k hd1tdFF#);*%?ַQmpёʱKMYG]vv)im7d!SOX, A9/X+|[a˲!bug1 2:W3C}Ͽիrr<#,__^_!~Q{5]eۯ^Cm=ѣ{g' (p6ǝc~ g|U,62v)u #9m]ڝ_`ņXdtF=r@6CA.)[;XudIZK@RQN0Fd;7܁-̹U| Kܡ[e~pq7', ?vH cϵl =9n㈁9~28/ ѰR0) (7S.ILן8Qߏ8Y;K󭙚`7[t_?<}3v|ߝ2N\3osp8WB㷎9-g-0ccbtI,Eϼ#:D\Εc12!%^YT,ɴQ<-hgm u\``@#/qXK'vaMt"y5hglϸPKMberc[?25#Q2~B.}mI=Kcf 2CrS 0k)+ ycBl;;~Gsc' Y3qr5Cv=Xe9ѭ*/]Xٞ+ѩ*c`stOC3c+㪝hի*_K?88V~_us2xNUy/GԯM\y\)cؾⰪp79:'4.e8'xkX98<  5l̝dZPAg3.9V<}yqHBqA;c&kb!' 2:\(W B kN@cŷ0,Ylk -% 4ٸ/)f#E1°PKdqdS?׏~qׅt陮%ٸ/)f#__`BԬ- G6]d:s<&-٘?;P.f!sl ␙F#;Xb69 8r.9rmuX6's` \ג:èߤ$DCګ68q8N1eGO8t y F,NuX0!^,͋ &nnu86czNڹsgy_v60>}Phj(-}Mz}F?*Mkcye=c5~Ai׏~y婼_ω-P1tAisA^8:]sL%]/|Cf*İA!]~5nu}؝ȶ jAmu%nr^snnt䵔' 7vO0uq˼.<@N-;u!%ĆM<l{m:qش6Y'L&vC >cClnیW?>VPVQI dA)K8z4(Wzz06+GH9ɦ3-L9ˢc5ZJ\_FSADC|(?3{W{E3 9Fwc+Sb֕ecD2R#/|W6|!dHm̺6p!cGgN]>dmCUUR7~:_Ñh+v,7$lD ; qĀq{m07 XsO{kbXI>槾/5Б!#wcF 6bxL 1EsÜt.E([,LY[q}'ʸrCK,d,k._?ׯ?O!.+gO?NNo3Q}{w,g=͘ 3?X@q΍MJp+ͻW\c,_y,jfq\8\̝snΗᇰ v6Q#f>㭇ZgIN (9lvxEXsg0 Nuo?XP!/!!OY n>8ml7knu25pΝ{?Dp(7+0}d@ًuߏ?. 'J?Y1 ޯ?29u~ɣR ןKta2BwC,9K>\a‘a@Ā1:yxl:\§n[O_[/DGۤLnb= uj)Q @:Bbkp;aǰe wF x(ȱfMmsGO vxӏԲ-mSCp͏n_- =nQk\ҿ7{9̗Xu8)wy\4Gz:]h6P;iǿ (`#Q~q_912;P <.Fţ@Џr0cdvly\4GWǡ`(@'i}?=ݾ};eبĢݘ"3F<!#l1b<[suhΓb6d^eڒbđ!cC<2d}3A/mF]Qv7=wݎc7G 7y© `5r{07;2vPt䜃c` [~CO^v" ݚp+_΂>[-XfFGfij/ ̺wtMUiJY9Re^ۏ~ŕ%ן~ןtZ?.:1rzh.L~d=sW,bJe1ndc Ɓgy,oPh+\مbL>k:-R5iN]raw# jkDVD1Eau@6NgY\Ovbg>t6pN) p춓bK|m>\;dBD`Xʏ0&( X@I^7xt:Lch*}/|7>m'׿2)#r_ձ-r׿1\zO.K[ЃtOZz߮NUi^37". !  ";w5vc/c}9āQf >dsD8>8ul<6!xr1F9%k#|lk V&69u ؉eG㑍5.L kĀ} ۅ&?e|-@&ǯEo~bC?)&^^vю`fHJMQ.-_G5y(>|p?py65WlG@1$oMQ.-_Gu~傯 /bx{O,vO wC;tm+:|{qmowLuI(EL8DsӨi n:tQM$FM\pա[pj&Q4jGاا?ַm0,.)I44j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI_ʰ{47^ &oMQnXDGGD#>28dEHxlܱe;:y 9]vby-9blېoGSLj!ln`3ok}vdµ0n &l0Zw3rB-İY;d}89a[}yK\حO![ņf+Jys 2kxii&) ]+d3pճLzcݝ_p6F۷Lt~L\,-$yʷ+d3pճL'pđG gg1;ۣwO߶yϻ;2cko7T_7o2󭧼uph-oy/Qeқ +RlzIo|d x[2/~q1ʑyy ??w[O"N^?w?>G1rا>;Zb"MR/&B68񎷗7ZI To)Vfgi&)ȗE^!eқ OblzIo|Y4YZ&I To)Vfgi&)ȗE^!eқ OblzIo|Y4YZ&I To)Vfgi&)ȗE^!eқ OblzIo|Y4YZ&I To)Vfgi&)ȗE^!eқ OblzIo|Y4YZ&I To)Vfgi&)Ǣo{)//;v( YS#W=Kˤ7IA- Yy g~gN giŎƼ:vuو8[Av?:~;Cv>l#8!>?:c^bYGl:9VAH$؊P\n v(7],2FÖ9N*5]]p'xb9ؑb%HҮl;ܐANUkĢxZ 6*5oeBNZjFlV‹д va8u.cd"w ?2\ yWyд\Lo~͇??[OOԿo{[iMF7ѷ]9 u{cMZO=nw{軆c;2b=txUSNyhe֓[ߴTw.b)9r_n̳=v!r o+!O5ccd{sr::!56/6895O4w6r?2m40iC}?ע~KAOe:ۜ3I93ͧDSEwm8av_`k 2!sxlLO2n.&wb6HtH :xrʹe7y05~^ q]AX8>b^1pcC,1paеY_lٖeFb7>LGx\6,Q<>rTts`ׇn a#=mC,qgż!<'ydptR _,lyZk:q;$.sģK V~ζB@<VoKóq/ngϋOa~o~|Dt[H(-cp衇 z(Sl} sDαw18#|pʛaN}p^ɢ!:†i6nwms}pȸpv޹=6o8;wB8\oQNON#~'/=뒜ԧ7ME]T~l0p@Sq? {챇 20"!t6M!lLt6d䄨GNb lS]N>F=l921< t}|և%G$v0h/I! l:lOK;O6F~8n}868Y> '.7б>pclvml/,"~\a*hnJ6DMf֕7f 3'QǣJhԄѥ=%x[0f4a^A4$|#n}۔HaKywW\g Ȥ9ׯҗckqЪ࢝ _2V{g{//;sx> o\p޷)O}p[z{ ";>GgÏȏ{X(+0W:O _^*U⮚xUwmn`wM_}[mҗ4|C.×ykV9_{#s>gy.?8\mǎ~KYg=|5S׸p+]i×tmQ>Ïȏ J;{kR݋b8ȣ, 2OyS[å/ua\r?s>q'b ~*t^vV{,E;mǸn}w٧egEW8aLr/V]r;w@uYm{nyWp2\6^>qj9vź<*~KE35wG?amUrWrS󀨯}k.0p{8 ۆ_}jjiܕ1#g??wz7OV5x>~ұ?1S+5lZM<~-(8s7 V;yُ~qOf 3wS0DZWޠa.M2oEo=~T;c(3])+oUZf 3wS0îS22~+R_'@ac<2ϜӅl.g9iЩiK~86pηg;3ϥ&v:&z m0FحO,~s72!A'᳦c(:5N9fqt&)e q /epéX6l[a[v˹B-;X5 F -'Ązu Rj[@M)f0V6-t0*R|\E>+F/V#`[ sa~+=G ?xӟ^>eӝL/CgWڮpV!PÇ_ 1njg9V*G>GQAT=>p;ixb QRJt@žqGH%/BAd_zpQGXMz#6|;^[XzK_2<'Xe$pc~'~b%ǿ O^^+\ᇇgiÎG+j4` Gy:x8g w1Ї>,$ή"/7𛱐åglwŒ=Fe ~׸ !zpX|;w`+^a8cO>yxnƱO}GntßѡOxO:q7w%=zY unxb~8 s88?O*i `~o>A>=яO'o(sᙱHʂߜj\?*z4]?O<*R|\4(~8_ Ʀw~Um7~O>ٵf c뗻]54 ? NMb s`F.~pf̍ѳ|Yp2䕌F 7}'YLV"H-D. ³nm_やg}9Ϻ7E.e%z'Xlp  H.d?tpdl>ka'$X93-PZҙ"M(Plّ2VqKgBW܅>i/(` FoK]f~e~ f6dM(>xg߄gןʏxÇWe 2?^ep0g|Af(/QO/ėZIEY)J> 7I$`Ncs%'dx(pjS< }k̆;o{歩_?bxg8nwIO|{g>X$9dx_+fEԦ/G?1udkԁ0sιR6!\C}0^ w5_d[v7ה;m18${{)Sn~?e/_Wzȕ{1I'cq닟|;Jַ󨟣F͝ޤ"_Gtok쵆W^5}t0|1Z~>ќ47h &t6mV=sK&1y4YF<0j1j+dHI؄Z>_& CTqKgB'\s7ǝ2q9!y"ov; Ky?;C.2d~؀c/x}!#fW'i~xv8ciGA.6lP9u>71wnN۪6>)$>yib؈fC,؈4u2:lƇZ6 ZBG6=]o<6},׼0c.OVl$uNXt?)'8yIKt:R1іe(ğ;]?<:#ve)~YH|a_+7-]DWUO맇'=ח~#K/k gy3ӭ>{~ѥ嘊vrT+OEͦ,7@wkL=E̦,7@wkL1J5MYFRzƈ{k[MY2)Urf)H*U}U5MYFRzƈl4Jջ5Fg}NgmۇpS ҲE_lj]bw  ,b_c2R\?b8?v Ftsa>8r %e?zڈ1/Noh;6>^ ֑>y̵* &䀐7فP_wagѭKwxWaǣK83&$W\Tw$DBd_aw>\x_`Aĝ/=)&Movؿ2e#K 5QC&d1 ~xۆ:y?8o /kxǣ?ϊޡBx;xG~WNc+k^gům5b|p\LlIp^!s/3953wgSltp,0^}ַY]P.o/==Ӯ{B{C+;L~PGcX2q{ş8axlOLۆ{pXhW$|q$pVϋvya,=dO􅵓Ug=k8- RfSzxUq7#pYOy#KGns!ޗ 2M, !swČO=L߭x 9haױ̛^!oa++ay1>)/ g˟ߎ}#QˏxGubq.G;BI@M q"ŝ9/:EÓ N֛RXΞ__6D>\5v&P+Kw6+^ rg ~x _Xj>+دqG冟ڱύ?{}iX/;x/,r1F,A,*R^^lsawemso{up'ľYȩAQ4< +cVSHy4b!_K Govp_h:n5gϿ~ܯ?w_ƛwv_8&.Ѥ#c<4ܖf3OE3Nd|·xbo g dKL}16C|#pԑQp6쐜6Pg-3G7aZO{ku,L2C6F,$ $Fx: :7137q'F2q#ΚıAl3:#bЉo}pq% &mه B&M).ڦI"C{iV<a_U:9R<ժum-mtBkk61:DԟPeA&׈4//m^_Epkdxog~Gb-⥾M2Gw] 2G1b!X9, S^z!xeK[~p+]ԧgwI[K}޷?27M=zVik\E %;߅#o$Ko^{/rH)qw̎mK~4xCjWw -s(kgNj0ܩ8_we}^B>\0-I'w|K}Rsq㎬xmXR/e<v1$wF+/G k{ֱ0!1sg,Nh?ӟt<i~bMozceAe/yi}8ץGO_*/`c7b yӞҧ~qh:wa[ѵ5zgccu'(e'ul|,,Wւd b-ϜST㯏~seL,jWi$hy>dͽ4W?[Hm:I]yHs/jNRF~Eay̞{.2Msy52-SI<) qظd0U'U!؁ b!N'\/Sbg. BF Hul5Q6f;o|p &L-pu8N rzL.h8>d9*1om낅8b#YWn{X RNNc~|bxrA_ aCn-2/lƂQLd5ֹ#1˜uplMgEN0\=o|cQ2#B6G.9y_)]+m{8oE o8|_x+c3ҷUXcB]$._Y 8}rXX$9#̂́q7c;~T~NNƣ6GJ+Gnwwĝ pwHk`Rϝ!֘Y[D5^Z{/hӃ-lTt1Y\peRV7эⅺPJŘ~zR b!?|[NI%XNcNCҝFsNC/m:W^wv85B,ApY:'z: wb~G8`'?Y|R^{+^Q^"\G? N7Nc.wK Fhةqwxˊ6wV3]r72;vcł 5>ş wGΣ⎕_>7dʸG\>Fxؔ>H?'nvp GH2DyկMImFdNyem>MZ sny8x"!q8xXb σb¼bMs'vx|m43Ïb 1؉gg5Ҟ8&J*fCXl1-H65_Uz /%OLKٚ]R@ظH]z .sţK- ck>V ?~7^^?2\z[.WW ܙ(F#>z氽ԗ;rLC_Yx[#cާdmwDN}Nwܭ, 1'xzWCʂ@=7c%] ~?[(JO#)yR;d\^;?_?{x .k^ 2qG, wؔ?#m[[c^OxqԈF.X uIUyk/*8Q 8t3<#q 1 ^iPP4M"3f[cA'" %ql/7p{T7WU߈սϺZUujV}Æ/,˲z)_π;+K)_Y %HBx."U#yRcۍ=Ϲ1^_hPGלz{'tcC 07w<~;dj1>=ï iwsNһ)Qq~wE{˦I2f~nwΕgc1ļ~il\veўso<;dߍ [{{%o cqq'l,Yo2 ?t#]Xy3Okv-G[Ǫ]@a"lbw<8Xw{úxkXxjW6d[2v&o Sٶݐc1X!6ԆdK ϼy cզyM.| vxA"& -<5ٶFNtbeB,6b$1dž%bRZ5|Hru^=&N"VW?7'bWc:::!_ظs9B/)`=s&|u)\?/F!s 8Xu^!T8%ouWjXp>-?m|.›2g;-ѥW?ÿ,AJ-^܃V;d⾥=iG?_ʸw}W<ֻC~|KJgUؔ>KjN^-P{_Yzpx5~{+^ÕxDI}{x`czy:\~3B6A~CJwdS*N!E_bIC}ql?W5Ui N"VW׾6Ӹ)r~54#Uח;d>c;_Y|e)¸c%wP5+ks~\ zVE+9D !+_Tǯ<}'~*;tٝ.x0uSR4ww+Kf ^a_×ǯfax&wg\p+K1~eq"2u)NG\1K/ "yzHՓMnPĪ uw)1t $C+d|ڥY0a 8Xu^!{. n= PĪ ߫v)EVp{9o&N"VW^K)ka|6Ap2B]J^{ U2WRs߰M E:jRdמmd(byU"+7l'C+d|ڥY0a 8Xu^!{. n= PĪ ߫v)EVp{9o&N"VW^K)ka|.|ٌf[ŝ1l*0^K-g&7Ax2!2WMO/6il:p0P77XǢa* #/ clmۉN>ó \lyrgL BV`o=dhWkz1#06Z6A@7Ά66 >tb!ucѭ&8d665 ԤvۅXSN 2ܼbFxxo?aƆF~ X]]GV9.Xh p.hvxoR^a7#C^p:|Ck2 ]mKhI&9Y=$0ƒ7g|'rPYƣfr̵sm=-V//CجzKk_ 񕥧džUM}򕥰/򤯷;;"m$؅r}-wW^➯@3hɃCQK5ů,w᫟rmrnܿ>+<`7S<9/_pkl:|]06wK1_{xjj"_Y⡾_YjI 2֜@rHXx$7Hٸ!m \_T0k`z""}]Zg,9,73Hccx.?GC^?xcϥ9=lͿץ5|k{f vɇ41w\Y!snyP?B 7<=6ˮW!4zN$__;ِ͐᡾hWzpgP_z_!&~i偻)w3bC?#>/qQmWBײ2ϯoÆ9t~.o_4"?wܰ?J܋c*2?uw+K_WhImy;SpL_7}{;_yam ⊘p/?|_{G|U,9xs:~6_|Z@-$D[!򕻨ץ5x6d>mΔ~%@jMd<9qyNeҲӳto>bqs> ѭVA\?[8QظuC.)M9<Ϗ?o:/¬z泾US4ye|%{xnW^z{_wO,_g]XP^yՕ/wS~eV}}`mD? '?xFYK+K_Y{|e_1_d/KC kCWC/"=+Bód^A< \<^{nf[WFs{랇n|^q^{3,ݛCpbm<<{_?{yQ\;/2G~~/{_m3c2~1e ׻*u']+ xE}ʸa,d˛^ 2ȫ,YQ+K,px _qiW[Cfgˇ“_{_wu?_fh\ 󘄴 ~qWl|e `V? _Pnk9\|Sҍ0G>rw g-Fmnw,師qW_Oy2!Y6;y?|u|K㗆kfO}S8KS7xLO>_VӽcC7>th^D|eIW~46p x>qiwգuxӖu?u\*!eyQJPd Fp^??")SgKq\kc# TdXӼ@ae^>>^+S| eZ5)}xϐl aDeM3Չ[бc#'|n]%:k\d`!!|!3^\:wEW}Gll[8N"& a3v6G+>9s0%6La[̱>y @xG[`!xb>>>9'0Cn$~ ,OFSҒڟ={_Dh% izѪ٪_[W\|;ڗM+Oє$&Ds-Moz+.͋1bxU5şosWG3?3㡿O&~hW9~e*W~%aU;%/yi<+4/]:uxЃ\/iSw]֒[q7O|K#.c3WH=%ƀgH8/5~Z ]-o9<8~<|+hXvS+Px=>چF.Ve~eg}~j |Ol}dz#6^rxM|%,WupEWfΟsr%2~!lFۿ[_ۻ^wXggqy|59b7MN{' bSoIC~km?L|miq%G([7\p$(n6aux0]՘$Q%O9FSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OYFSҒ%OY0o>//~V?;|L<3a"N!ZWR%1c(bxUcA1W&ᣇi3\'p֬pO`mg<6e0Gǟ!f=#Onl{Cln 0֕Եso$V~;w@l<6 P1ph/^LEON7*r~:&>H;x9h1~>:xrgx±ubmqL9EM DJ\;)lbZ6^*DKm TkʿVvABxXjpŇ|KCm=w?8pLlpZ<_;w;p[rwo:Nw*_:TM Mi@Hfm>qG % Mwpyl(엏>pn .҆.5*bO&6[-2wUѻ_l '7ۖgDcQ2,g"15]Pؑ)w/T8 %/fCe+$QVܼ<)Oٟ]Z}gb'W]}βW9ZYn-ψ,οϦ3s9sy6o5)ey׌Ȳ7'_;Cn3Ǚ$w6myFdY'ztŒl9\jS[wM1?$j "9q=\C[6.0bC,2:B N=r@CA.)[;XudKRxE!) Ga8 c\p;X3 W1`/ r-󃃌9eY8Cr{e[q6G '{.wูDJb569 P^fj-9ZtGuwrNj VM08e $,1=|--w[Z_{szhyv<7<|BO[ȥ?m~^<8n1)`f(nn$[ jg}6&ǟ_ jnNɊsI:Ͽk5/y3?P;oX6d֘ g-5ԏu7kst:g38lvqtZEq9Xq2eڦ;2}F&'D^MN>6!.g|b͏/|$N9R N]2ٮ3=HLHa.8Zf' ϝ5|b) )>;>H< >wȺ>4|+r[U^ZY*;?`, _Y?f?c3Qr l3^+oWD^UbIH dtpɆix7!e,r~rMcRȜؑA[+.LmA}v.!XkYO>rPsZpXbɆLRN (C6#}'咹Ma1eLv&&L2Ʒ2Yl;l?ǟ?t3\:\pr||ǾKuQII Wm8q{M1eGO8t y F,盒aBXjC 2pScl`3=/~<׋ݯ =(ttn5`־}Y?_?B<⼉ys,Y^WߜSω?P%t4?렴/?C}8x `ؼOȌuS<;d~AD-=3x~pbm_kNqќ͍K9@? h@t|ⴇv7講 \EwpjYE)!6,>78lrAxdkՉæ:a*6q bbvf:8u/ 5apxjT))Ki8+thykI/~. UOfο2CIϴ42|]XJ\~_Ƽ믟S䘟g~1?Ub~Tp>?WF3Y {Fw+Sb֕`D2R#|W6|!dHm̺6p!cGgM]>dmCUUR7~HtYXi;}6"Ʌ8b=fdžL9 SrxۤFMS߇zp|ő;c 1|M c#GHv`qaH"boi9(۸{֟?_(ٞCk2(ka<)es8b>reqϟ3? 6s6QLwlמfYBlƦN%tx╍+nƈ1te`̇: 53Xc-K.97xԀC؉W ;]vۨZm3C%; #Ib1%X N<"bkN uI`|*%d0>k͇g͕桾`{ͭW}3o:{K4 @a?oGE>2 C=wb];@a%D_>+ƱqqԱr&-ֵ374jyTtqlАyoeVRg2gi ן|9PGN ןua2Fw)67'X<8s18|!6ڕ#䁈c=yxl:\§n[OX_[/DG]ۤLnb= uj)(ci !~cزxs`#GrXc&X9';q Y?k~L~|yk9ciX n`sg8vsXp,1!p֣FnfGʃsA}b <>~|˛&BA''旳!sI2,u}``Ձ[{d63O=wοyŕͥ>g^O5?#7?+QKX6dWܳvuB nX6s@;!aXp֩9XV{ BX*xycB,́.XsPn%v W_#N"Q\.hI82gMp9F?ډ9!81n`ӏ6NrzKg.1rXpS 6b\!r'Y6 n?MPY;Hs߼9 ;?_#ױAͿ~82W.'kF6"Yqb񹁁)A,kWa7Z۸dg(lX/p|p8xlC,~cr#KF8v I&69臃玆ZF#k\6oq@ Ĺf2[΁M_.LJؐ)?{rE;*o$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi n:tWEM$MQ.-_5y(v4-FƫC|^MѴh{Q7bGӢi)6+K#` "P\Ħ _<6XA<ܜnhd;˺rkNm) @IDAT7zé & 60mEƾ ;.pc`ppv f?>_ 4z< .w O{9> Bl9<:|xvsa>mcnub#mWa!z6daNVz 7o 9z֖7IA r-]oHr-]o|S*8pY[$y/*8pY[$j7TqֳtI ^n#UqֳtI MngmzFgmz䛪P!ǁ[&){T!ǁ[&)7UBMRrBMRovCn=kKכ 6Rn=kKכ T 9z֖7IAm 9z֖7IA r-]oHr-]o|S*8pY[$y/*8pY[$j7TqֳtI ^n#UqֳtI MngmzFgmz䛪P!ǁ[&){T!ǁ[&)7UBd8܀~4bG"=qbb7F?vb'?V|ϡa]k{2hC>> ?>'.7б>pclvm_Y"L|e<0\ UnJDuHB@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah"+oP0 )M;HT@3nJF&Һ 0҄A4( 4ah>ԗw~:5.v\E;8l*bYϚc(:5N96%k`?gc⁾>8|hSصny=ɇ niW!s@^mg}bAd,q,8Pt"9n,Ϻap:N~  DͅMy(r)/Գ>9bX|DrC&&d;Y ;9 qƘ!Ct3E$\PpYز#e΄N&6a֏ȣ1?|2&y`Ҍ΄N&66+ׄ|aHWyaN4Z &\w^w^5!_'::؄OlPGz3&C֕beE);8l1k:H5Î XjF ]S`loK Z͉c[ᐺ~ۦ+O"$C'7:gb c#q@\pj78xjn<~c]6gž7Evn cg|DRSy[b*MYy9.q;ٔIzOƈ];T7;'\mxޓ1=`.Np6egRޓ1b_Vw/GRzOƈ ^6eR;=ZElrI=#vz3xٔ>J{2Fh}g)}$d9*eSH*UlȰqE&,!%:>֚B7 EfArE̟sՈZS;y 0rOA#W[] o-xmr ouy jp|e52D8٨`& Xc! 2yz4#",vF3c}p>al7?Y,L}K>l <ַ=*>tji$֮!Aӎ,#O'Hp7 xSرvt7.Bl#?Dqpli28|C}r斛\ac\:dƆ 7NVZIBQf⶷iڴ\K#b֯e?_cQ8t8Dg ~MцL:2&q8ی,Y_ :q3>kV\`S3d.]?@I*.K]1fk AT]"!ͽ6W_[HWRFy iYBBUmj2cHsjUEC{mV<DU[ڥ.kz z.etǐ^+ϯ-$Q֫v)<4ڬ^y~m!!^K]1fk AT]"!ͽ6W_[HWRFy iYBBUmj2cHsjUEC{mV<DU[ڥ.kz z.etǐ^+ϯ-$Q֫v)<4ڬ^y~m!!^K]1fk AT]"!ͽ6W_[HWRFy iYBBUmj2cHsjUEC{mV<DU[ڥ.kz z.etǐ^+ϯ-$Q֫v)<4ڬ^y~m!!^K]1fk AT]"!ͽ6W_[HWRFy iYBBUmj2cH1-_Yzx(,Yb+ف bO }er@,Y qPo iMbE'Kn8>N~lk;G1 . /xmrPVހ1/gt)*3X 'AN5jHԞ$-,=S9No iWx9h8󋱉 pMy13?tbzMr:1Ͽ՘zg^+O|[_Z⡾_ 7йRDgmM~5eN6We|ƀG1zN$X2ۂͶbY+Glt-nmp!|pj}0 :]p s׎.y8}lb&<~ Q&7ṷ|!aPc|u|ovc;m K ANۢO|9щ! ؈\2;w6LRZ5$7QՓMnd(byf:y1:~ PĪ ?qRבʯՓMnd(byq%#yd :Xu^98牒zM E:МuDIu^=&N"VWhο:sy$w:orD'C+w5JWMaMהB,:M٦XtAW 2X< rؑ]"S]YG.l<`3&B!Ély2֫5Ҙ-Vx gh ~v@o~ yW2~vjR o)'n^}# 4Ȏi?C Qckq#8߼ܬٛ\x&.g4XrFA66Cap_r۞K dN,kb8qpеS57xv&á78j [.pFxl%6B!l0dXss3ņ:`i}pq 2al%G`sygJ̉Z2~ cI^HX 117ǯ,;Ɲ ïGբdL.rui *;fx#$_+zui+;o}ڴs+)G1L\fa^g^֗yɒ}zI翅>B.WzVqmb9Aw lײ8 vH? 㺖|<15QOE;:y 0m~t냕qP+ϵ|16nr~jy'Nnd;Af}9[nz ao;~`=!\?nܠCCLRft.:^Eul- ܅sF'Zocki.h_~egNY. Q2!#'{Xd9yW`9d@T S]wqTg\jȿZf|MjUFy7ϿuC0ԋü)1Ab5:V3Jp^?'.g~iB`_ijD+`=ɦ\9f}Ȧ >g;T?8k:vl䄃s_uYǍ\q ~􌳶Xl>r#'wdml/IJ rB|錝̓h,)xd%6La[ Dxdjq9ַM`!x6SO;X!ƃ:oKo99a4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- ~:\e4%- v+KWӃ(&\z5-qkqYՉxkx"Vj`7n}8 ui㑍'C!sh64S  j`bQ+rőӍNBC Ňx9h1~6A1!Y6޺pl!gXo{G[ttzE(5\I>P6sT<ŞMlBi8-Qޔ~HաJ#&9y,1%9؄91PϹ>;oD.cŞMlœs/@?#Fy{f< I'gƽ[籘o=;c/FKw<"6'pdjXGVz7Mܰ W˟wne9mCfflKbgx]bѱApFO/a͗s#ۮ5'vhD$#C4|6Oف W(@`GuG62m,V6XjZM:ڰCKNo cׇ zB`y~/Ć;3] 9|>L{x3o3g9so{:UC]Y.v3""g^g^` 1<6r[Y9;_   501qX=$j "9q=\C[6.0bC,2w i ! c:2$djRHJ08 il;9r9W>~omŖkd .1ď?s-FO[8b 8q_{ ΛC4 adirx ԄSni!}ҌFi1ok[X7AȋEG|ƅZjC,s:A^ >j!-҇6?/!sQiAf(oJl " Ӯa!4bl " ;Gwِydͬ19x%]&֐fmB!]5s'/mo\V\ MǐaGKɉh2rƇx}!rȇnMC,%:È߃$ʍ6(66ǁ#A0 sg%"X|<}cBlo 2vS΍fV:ډVyŸɱO߼|0^`SU>10̧y[pJNW)Ŵp֑9:'4ne8'xkO,ٜDmpۅ pB7T9t=m"h9ru8iSFQ"|d#ŒAb 8)nBX9#W\1ރő;]B,ֲ>"m18|19h Đ iE.+avȎ#< l|+J-ŶȦY^h<.iXf R{̎#Ʒ2Yl;loο9#_ҥ_Kq^=fǑM3?3?\0u!_jv}+J-ŶȦ{=ˆUfmҲ9cf2 51`$03^'[sAqK]rs̓mO W7"'l9waoR"!U}6N|S@Q#]Bn, :l/Z i7:X1g=|;{lyv60^}Pj(}&q>#7ο8mAԂг<>>K!?gͩȻH D'N{ awӁκ`u^t7HPd꺑bs&GGQ8labN!!~`mƫÍ_;]TRYf~gݭFpKB'2Ap`l+3dLK#SDzX-_1?T>9??q~r1Ϩ~f|?{L݄;8Tueb98ú|7ߕ xo qYShYq!}U:mկlԍík8D#vڎd~raa60nz`Ϲs!ki/yT`9+6Qv!22:2D}qxyɣRc ͺ0RG҃u#;UC,9K>\aƑa@Ā1ȼ D4L,܄p7{h칳v9B,XN}ߘ X8~Q#ǁ s#cAG9>1lP>dMs[N}eِ ӌntd:PK>00{s=2';8ul<6!xr1F9%k#|$~QAO`k^csGC-vby#|㑍5.L kĀ}8 ۅ&?e|-@&ǯMo~bC^lȔ9Q7`HJŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi47^+&oŎE(xu薯܋I;M\pա[r/j&QhZ4rWnʽɛDi4%~qFyEg Yb/wCَNHnN74|e] GεX'6dg=sZ?{xWQ = "H ҔЛttXT:@F j(RH&ҫ RvHB{{YށN93sd\h,`nK}m- hA `@pl0رAx^v|t6ݟQ~W>cK~=C _ 6p+z(d#x}TwޢD !.~|2!c6s21%]k{ Vhe``‰Z+2d0rĈz  쵴5\81=YkE ZښZ.:]! 'F'kȐ^K[S˅#Z2d0rĈd2kikjpbDt=\ ZښZ.ў"C{-mM-Nhː^K[S˅#ړVd``‰p2kikjpbD{֊  쵴5\81u.C{-mM-NhOZ! 'FNe``‰Z+2d0rĈz  쵴5\81=YkE ZښZ.:]! 'F'kȐ^K[S˅#Z2d0rĈd2kikjpbDt=\ ZښZ.ў"C{-mM-Nhː^K[S˅#ړVdH?onM@@B?bX#cg8>;!'`G2v|U>3nFQ󑌝|U(6b#0q_ӏɏM}8 TPXAX!da)Pv c# +d *<6x#Tcp /qPRzʇĦr蠊A%|F=!clR)RHc֖˜nVa6Lj@9l(]\οr)j^=D.Vg`A)]v.t_/thJMmLj@B:Rvi2> OGЉjd:Ɨ|) #C62vp8(ȊE/l̉-ᱫ / 6-_XVVv%< U\dU:&VбPb([V:v(uCAτ8Q}UGbWCFG8jF~ځ^:x>-K8Oc,>MԹ ˀ00j+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHB*h PMa.Ba>+ZEiP&$Ed" # *@Q4iHiԗV0~ d :ƸS3 :MgGcvC&iM<=\r(!'|gAqPGbW,A~Cxc(NrJנ0*`PBV~/@Tz6}4Tu,˟OF7UBP7t][qRs՝7 j\k)u_wu7z5\zppξow/ߚ u?}:6e/QVleRc.IポUؠAŏtPrCQ nßņ/cch':,f |#>)?>(<=JAu Үe5^p⫀фNOQV,t+:X.aɧ*X/`MHv0ɇz`S.Ġ'_Q⁑O=!C}# [EI\ !bfh +la7Sy1p-]~O7pG?;'V0Rw=!%S4t0aSޭx3E תp|0_F¡#L.XtCCO1>ט:XFʯʡ#*J,&{FoO(8U`*2gA E_tj ?MuHX`#D'86B.SC>(/ztG'P&Z81C>oNffF_jo"*/QykS F_稊|I\N#lޮ!jUNk|e1Cr稊|I\N#lw{>U{R\N#lrOEUS\yQ'e1v-t^TEIq:e]{>U{R\N#lrOEUS6!D>(`1Ǘk2 5a/<JMxceSzPdO!?:KV;Y9^d =t1bLlb''X>|@W;ARPE'*.d"!v8)?6>NF|adCV,tz lE/&J+ڡxf*qO:||FyӦɯQwt{m7V _M4cRӆdQD&]Rɒ7 &:?HL6ڔN.?*lab?HL6ڔNοp01~lٵ)e,]&^0M46믻R Az`fצu3\}YbLi|`IƱ|dT6<'k<Ϻ~@IDATfuùFOEDtw3q]wKvgH@ؖ˄q*< ;LT1Q_A+Ag 1&LJNy\a_O^d(XtёP>)WTrRaS.3GV5@rj:J+X:HacO+8d(W<`O#?>'x)'~|(Tgd`OAƏx!rhr %'bd0!3/*3B@ʺMEMbn%g1Bg]\ڪiM4k בIr,]ԭ[qMbn%g1Be]"K1Ւ3Zs|ӥjəbPYWgvRmLwq1F\D;t)Zr#TYk.os[-9]\*57]i..uu֚h.VK4~cʺ:kEMbn%g1Be]"K1Ւ3Zs|ӥjəbPYWgvRmLwq1F\D;t)Zr#TYk.os[-9]\*57]i..uu֚h.VK4~cʺ:kEMbn%g1Be]"K1Ւ3Zs|ӥjəbPYWgvRmLwq1F\D;t)Zr#TYk.os[-9]\*57]i..uu֚h.VK4~cʺ:kEMbn%g1Be]"G67`PyU4فN2>J MNȆ4GO, CʃETN|w뱡h76(5.) &=9 E'?c{ )*:DvSWr*;xV^Sy!+ӞD!GLȎM:X RDSy(P0 ?mFPa#CXWL1׸C̆`9,P] ĀSygWt69Mi&chNLLSQOMi6Mi5Ӱmr >";N(.֪Ϳ]W=:}¿=lM]WkY?@BJ Y謂b3EC?_x{lfw{G{uTw%xni 5̝q$2cst!ˎY6QzS1\!b`xd(@C/x0 :]P~0蠪c#STGScqMV|mSQC<]Ld x|)rȮK2u3&d0P"Y'@5͖h/|a g7&d^4W?loch1#{{Ua)Ӿc4F׉ue+[CP8e)o:/Mx) >+4־S-%(2ߔk'NL34S%,MR.3թ;oΟx E32~1_>(2w~'{*~gK0YqꡚfK0BYҝ>Oĉj-^foʿmϯx&> ƔzLX*`u;88Yq 7uqqĢ8ʍ^c_x)?4NuVXx0X/DG ŏ(:OiY0J姊QtL@!`8z,_dqW8xtLjPГ\U/jrU\ou jxTz>lbTCGz hTRVIrF 0IbE5D-)eIGמ}Y9(L RU,c'B>li1{\•Rq4\ Rh &NJ{n~/#FWY%=:}|6Ȫ2&&~P Cs`^vdbA"JxʏoQFS(6\GGvxb kX/g7Yb[zA^9)2m5ߧ*=s>gzƩ#L7.-Ē-ګ1 ZA1&8v^ߟ'ZPR{1duK9(ֶv9v̏cq?oLO~:x vtoS E!0Ymlե|Ǻ&՜ln1EOKBD!0Yo*~tץUWHZ`Ԩi]- |kNaeihOjV0nغMO AW eX`ob{Ѹx_b0>FV&aQaO+K8dc<ġѸ^uYC?)+ʵ+B pNRGmǟF!š`P(1+'y5)"l NtJ~M S%:>#K&+~XuR=iًhiţЫ)1鹱'Vi iˀl7k_)jϣKyugMJYՏ '%kp̈́?qy9|\3 iėM7ݘ?|gyY՛WS-\_tiikpHǿVf+JGqDZz٥ӷ)>rz5\3g8ɘkƶdai5VOUg`2+)@},dc$'M_0Tq0ҶkQ:ض;)[,;btGF/9*|S|䇧^bQ?Lޢ`c`ɠ 㣢(*M<&8L>.*y>䈗J<N%?)ށPbSLS >Ã'7B1yQGrgˀX/TvVzݐvt݀xo)rVDN?;ӟ8/Ϗ'Ujx\69g4rUn>77?Ra{ SOMMo[n;`7xcb-?:Mk.3K!I5u[*9tG%2,l2s5g<}@V*?-ܜ6tsui 6>ju9t~ w8jםNc'D9'ނ%W9twU'{,]}͵xXZ}ӨQ\^eik0dR*~90iBcjEz U<Ŕ^yQC&?QLp#OڠǾkUg{QMUoX XKM vUbM9E=2-{$T)XV[~ynN}{=V3Pu1+Hì? /&O{0IVM&󯯼jybKW_K38Cqo=}4yO.umxVŗ^c06&H=lkf+y"3y啗V,}ߗ3i?fx^x3žU,/sy$s,y'&.AO|09֦L3YSO=X: 3쑩-_9%CtVVIV BK3 U+ /ع>d}g~#!/Y?F?X`tWI/OJ*> [8555 ?kihEd̟ ,IW\ո:8QpGn>>*q}00%Wo6zigk6cvAY&=h kʫhkfvut6cLL^JGMmYZ6 >` ɑo}8נˀyW?wO?դ[`҉^5k9ƝswN;OBkVK.Ģ :ؾ~tӗvٸ4) /N SO99=JUuxyM/!~y/=?~Z*ӨF!¿8|׻G{4>lWO.k|!o˯rJ0 wOZfuV2&Mz9}l_m6O~ʎq dqkeo0q]|Ul|r:Ύo;^{핶n{\TkDbFNsLz&0uMofL..ǟ#GLm}aY?4]wO9Ρ6Љ'XΖN9tg9Ī!JCuPސ~sܞ&=1{s;츣Ldt\g=qW1?|Aߌ@=GU76JE yik]-LUk/~GK;}%SΛwڥʽ\-LUk/ڽ߷w9> [/?2' %^}]:f PE{DdrkMȰBf+09!R]bAKF7M%kN6ƺ/XWq+xclJOuf:)>:A<:JySƺC\~<O/jl._FV߾*A*J O"J#Ce57RGGV~^qē_u dxaEzNg҃~~ q]ۏkc}[ZwmIEQ3$&[lM,=fb [νc_s(?<6ʏdx(JEJHMU*}S*T:|1c,3S~*Ġ耪b.G+&KƏN&&vQj.M쳯?*ć>.+dlU /6b󴜭y;Fc%DQ{ď9i="}LaSN6q_l ן}9i.}{0Z:يcEI+&"qojN,Ѹ94Mf1>Wc(mi嗳̜2l4~iϽXs [?k$N?4_d)U+=?iG;FTjY|1رgm?K.!cI;fM?z9ĝf}/noKg.;,|R'd>>b>}u=<.ǞF^~,nU=ۙeLz|~i(q̧?Oyđ=8.2˵ŒZ҂2S?*@887YJdcqg^2cԃ6qZӵr[QF-t?O_\s_8=e> -1VPTʏXta*?d 8&S(BC+b ]/r?:>[dpqbGqc*>ؐ)X'š})XG-|L:GȪ y4@ˎR+TE-=ʃ&A :Q?Lˇc`'qiqa _J7xEg{Jδ ؿ\1'D٢K.籘;溞 lOþ}XƟ-? :ꨣ72s9/ M?mŬ˶‹-yYx\>h_iWGИT62se=dsO~w~ċhЙg5iQ[88> ґRA/lm^o 2LPrþ-ǧļ*}}&lXۤ u=V]:ΕO~Bo s/5É BҦxMBٵp~awO2ou8?!)?ˉ-Y푥-KDz:=yگ==8l0 /o m_ VhN}Y5B7YRf Dx06k0pY&]df?߶7hϿCVȬh+dVQtӍbKSO͈}lZs+]6 KmU iPQZ݀|>'<1MK][ur>XV`vm_>~X`Ac{uI\ w@h?2MXM$lƎwEUPZm5lif d.x}f=f6}`cp@:묳 =Åu i `¾;=ڣ66evMWJ|h_ڊƔ׾u[E*+[}?m5Ӳ#w~$Tsa/η푱 =~#6^eѤ7L hln==joI~Vm`27X#dҖO&w ŊDѢTXks@7pJ Z%vcrgkt =@,Z%vo?gGԝW7&~mU{RF7X_J 3mmaA<*؜I 27CMGe?%>cc zHU''.6bH%a(q#ƁWnw 8d), NWTIa AA A|`Xx:xP qE^Ġ.bW~2F®ފ d%tP| 8ƺ1E Vc*]}B*%h2; *4(3n(n(_=6]VdsWc0D?7GGJW\~mG(C2ՄmBfLY2j\q#؄2L XTM74Fl0=dVbg̘]˄|=B 'Vg{Ȝ3 Ziロmk]=bΒϳ=N \{Fb?q~쫳`ZllcP u9^ VElf`2<Ş5ԹEKiwy qkӼhOǀ9ף٦7ۄ+\7O7l.scYFU2Gul-)&F[[ƗǍ 5.=sC]0WJl 7؄~\EJnIMl̞9*j ;o_mm^-ckQ{>h/ƶZ ,Lir0O-Kc = fm޹}Gk袋y~6>#62v[n=61 ,پ8[~uz3yַ^˕M?lʽҋ/sv~z}lc |gӷDT>j_+h5!6;0|Z螮cqK g_s3Dl>1Q]S 8 _M?~p焏4{T>z_1 /̘_ӇI |$ 6-:cx1*v|#T?1+4s%/1bn `+zڤfr,>#M}A)?EM)J>KM*'\?JC(:R+(6U+N C;uPb&L\ 8>SWlP:S>lgvW4\Af2f W>5yuzr+?fu{`+d GLz}ڦ7oƣ>9A[:k;_)-jW^ux/ϫd+d*d)c`2Zd<ޮ:fWiqyVAuow62=ۦ6!4nB|j1K[&dݕxT/T}_yՕi1Lmc8{{ٷ_Q5<6isӍL q6Y3ڎ˅摏;SIl/{+['<;5wgt{#6v(`5WsBWLY_hTj`==dOc\=M4z䠦JJ(lqۄK_<ǘ5+m}No;5/=VULu?\pA>HSʐM9V)纜g>v!]a1G-lsd#l"q[τ/7b26I6!ːOᅮk_ͱ/١9Ҵ6K{?_/Q3*ʑT$N0D' U/ȉXMCϸklizMn3YTBiԤ{4@iN耯xo(h_}޻[Fr\O-?2{,T-0Ĭ;W?x' ަ{C~Bf]ۇ7s_8оv PYmC=CRl.2 1c1!/M7tȒYiE]l2fmZ礣s%|BŴ98췿xmkľ3 d-WنJ lBf-"KV|c_@ wy6 PϾ: ?X_~6a,&X;?{؛(zzp/V;>au 9Fc"7y\r(6ߖuicO|b.{tf&Mrlh pX!'qIҗKW2vΠl_~-{^Vx~&{o3K̯#畭Ya8[W=ktM~`+}MZGb2/ZV[=8TeCy*+ƳL=,2z :&a2;&gVy|is{6Y>jB&{\cs9뗯V':3ptw)W3k1) Uw^.H:U<o6N?4uqﯽҲ3GcndMYi26>ŠeaDEq*pWO^ut1u[zQK'?lٲe*xKC4D`%ŊD*BOT9|h}.8u>:x|K\ALz|(WxI2Q|3T~(RqĎxp>ڇmL}T@"4K Et8ރ~il{؄L=Uc#տWu"06lKPq(ކkNN70|**hu6L^أ}ޚg-h2/[-Z0f|JaN2-E xÇq("|+(x3ST_6±ccxKV=SK(__5є' +V@Gx5j #|;TDW;P!8Ѧ\PNXQ8W?-*jVK=l.0o,^ cGm!)oYA=zk71uTp_].y`i,5^"!?&ˊ ěm?ˍmf{|A%1ezJַm/ k'GSO vyԧ>tQ9 {U=#Kcc)_Y6LP5Jn49^^S 0tU~`3q2ġFq鈣6"MJ~Z C"$jAV}Q$O_^D*HCܮP*F0t.2sAOt(|蔓`^u6SdLJgMvt$"A)P+>ڂA>ݞ&-TJCQR {opoӟs4UPZCN&`R;l&dj{*hK^Jj]2襪 }]?OB -&+ yf7Iӄ~;{~\;lP+/YPbn| qzO C{ms{;*F&lPUs`p6]wudI{f %Gل ?+oL[E3 GWe:6 ܧ\osk#[?|R/߿kI[CW##9*+ 3!CVȜ]i*z^+k:Lz>>džیəPذqɱ_h@Os_O`hn־qng7N5(]~nGFkp JϑƉRu:\7<:s03&ccJm_ )dw-K7&=h)>icg ~Pc/{C#vBטK)+W=Ugb)Σ,c~.6[pzBr8( :565l#X(u`-' &Nx8L;ثz/* yl;Yޑ~_Igl okw9P׏ڄ ev`rȡ>҄tma(Lp iO=m{Lo^w~H| ;J@>DJ2Z鷊{5 C#qFސCapX5o{Ѫ~W/lMi6]cTf;C8z &(7]vUWY%=S?|p Ů?49萃= ,`:oڣaJ3<+zʩi+k{qakԙȘgyr[V Kn)75ql7MK-Tz^~ۯoKxf־1+okkZ->SG6qw&v1?7!1o{+|Oo'P/}mU2I@qF#e36'dlR 9+dlj uT:u&gԨQz#6AM7M};{7q!M;x4.uة[RW ӵ?L/weJwkot^Q oSԡz|M3ݴL5!YReʸ"&d2W!E1g&?Ce8o7CÚXr}S()ā'f燢W dW1O]zCK,^4~9Ӕ H .&WHoF# E^DNQ<5N8}dWz|ORɎ:@ѫdE>ʧU<Šb'?#%'tLi*D)`d, 7W\2dTm'L:bi<ԷD2vl2!@$- { x(Z~mEJbh17\%2a'GP̊߃Yʑ6sхE]jmv3VȐ=r~V#x eluCkm=Ri޲L>W~tail`MMbؤV-Kf(o|oRU1G~a3Wlo~ ft:+OxmJˣY&WIgڊ >ڧOI6A _\}d o*+dW!u{jο%P:S|r,lR'6)dYTa3 Ixr(h|޲{49YO*j7o}Xb &V?pkSO&>pF6 +b=UUoB99QUWܬf~Sο/ W=]v}DpC_ߟ_ooSSbeLu+`T}<+d3&u&0QS4)/0'=X٠䁪>IoۉzWQ~C1j!h{\Njӟ} lP*m@@QeI"?*UMsuc2=Ą ǏGӻvbUJ1Vy}kN:c=9yY?U?_=r=^xSU(ϲM묳sU+d&*9xۯ愓NL?P4ڪϧ1v{ǼW.mvx +O:ӱ>!c}V\y%P9G KHw:|u*7b% jo|߽yh/+p[WlԈZj;#.:DN;`NXj?pʝ$]|M>̲3κoI׬x%>)Fۯ90~Jwڄ7in֬BwI"dMYsV]:΋0y.Oڞ:Y\Yte0&7Sr)-Z56U$f̢-s%k`m*IJQ E[J TU 2̕$)E.4dm+1X+lSUIR \h6,2WbVئlYe` MU%I)*p h\XaJRTBAfі56U$f̢-s%k`m*IJQ E[J TU 2̕$)E.4dm+1X+lSUIR \h6,2WbVئlYe` ,FO[ɘ—tEB`m*IJ ̸Y؇1*HMt$ؠ0B]xtXAD=2q( 7~򓉢1>|! ^|#C &2 :} vMv£oS =EM9I)885*lPVu耐< .6@=E [r+2x|;8daSO'bW~N!bK&.:bGugs b(qO3 .ؒ*mM-N\  쵴57edhe&3¦6f *>cuaiM@Bɯ^Y[k"F4~g``‰1:IeFX2Tu߅ԆʣVT+dluѶ C{-mM-Nx?&MM:q%CPZښZ.QC3dVTd2v~654>;!'G2v|U>CE G2v+z>%'<1GUq_d,?Ǧǎ2~р).V=%VYXx >TCHYE _u5DHaK0ū!&)¨:nP"QOȘ*53E @o,i֊ Mէ3Ci]KOg W:L hkoY ȍ\ }X uS*ַ9^a+d^j4oYyE1cDuo?{ՙ"tPJ3t!3Nhֶՙ"tPJߏ?{:s~kY[BԺ|;rt_wٵʟ"wxԽD4R[΅W_NOk^] SW]ÆBfk0g<Ʌ& ոt/i12Edx}'61)1vx 8X)2&zL`z%_)@].򗯩]RY:-5+V_vx*FC6Q|(8lL>怏Ɩ8eCOL}T(!>Yq<=c"' g̷Om?7s%=}O/uŽ/ؐ뿯)G_}??+a}.|zbo^I;(k 59qnӏ v-_159,⨉_LQGMr|S[Mk81YSM5qL84~~5&9_1DŽN\cs aai;XNL8abш5Pzr169pφ8y~!hֱ6~:c?_WH4~|'Ə=]^N#i_u2$c__w /K8LE蜌8o~d)l_|G5* p *Il4K}I iWxHn5/ʴ:%c[8;i_uR%c[8;i_uR%c[8;i_uR%c[8;i_uR%c[8;i_uR%c[8;9O? N/H-F2=ou&HHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp v%ӾJHp vU/[z?q>ָ|Yns>>Ik xMOަ:8.ShY/`۟\b'n>0’C=bԷJ,FȦhOp"\ԕ/OcSG)b{}5bGMcrS'Z+GjQ˾h W!_I\;}'_'rLݞ6E6ת5P'1Sl* 3gB촰´-^*:?7O_!Vv>7p:~̿s۹>/L[W?#n`V=P_q_ߚgE"KW>gs֜y?^iI_,}r}Bfˁ⥯;Tղv?RzxKK?1 }c3~dqg=ŬS5.ę.sMOrkG-ֺgG>&F.kc>c֧?BCusØ!˩Mlp \8QBql'М88nH8.xն>u%Ԁp98LGOq~j US9kCD`( Z~o0|5P*Nfo_?/o)8U4M?_Wf6( ZTO_h/k5+aU?Řo'o]{Smf)0oF9ׄ cl.}Wv#Ux  j~ΟO(A9 zclw}_'f]Pz3X؅0U~ؐ8ֳb*?~kh֕nHˊR~x9g\Z<~0RsYkP9wM]kq5A㵇]#DZjmdW% )66gA.>O<ਅGI 1ߓ>kiV~ ~#`kxWϘ|4-kݘ|6Y~L('O{:x# cF欬=#_$?AĻ:{#dgMD|nYϊ78Þt= XBdAuU|/9osVCruU*fm\=2X]'!I߮W#!*^D36Gn9[~xͤhF?gkW]=l a5}#a?L^uUfR4os5ɫLfmܞs0yUIь͑7gcC2M7X%61֚řB7 $"z֯f쩟:`9g-F?B|`ڎ>l{P dsE\[6XŦ65.@PN'ek8u  uٟb>\11m-|G~1ss<E(O9օ_'}95q;6ƎϾ? />0'1ݪyBH&~fL=Swk]ʼRjK?$;gnb}=LK(z__M+sv믯&]_͝[}O?ϟL`=.z'hL=SwkGX##L-61daajc,$<S|G5ȅWp9O|"ö#qPfo`LK=|:w<1lzµv30zW!gc 698z"5zE_j1?\"9: ?51y` 9hm L3110|F#z'џ}`#Ϟq 3`!rɣ5͕dO !K}!@T2вo,T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[]~dO` :k^|Vf}JnNö6<`r8g- ">!ӞŦ6M M}k6>cCPOj.© m'G>qpNy6 s9aWxM ۟6N={hñ1hcf7Ff 9!f[O ,)*VS8|Xi'QQ]ڜ\윷(?NIN__}]˪>7G7Q?{^y󷟿mwQ,v7Q}~_Z ,O"hyb6KsCŵYCC=é>c~7kC=-vk#ZF|I61;V7| \200(@H\8@c֭AO>Ĩkoj&nhxͷ$r.1xAa-?XɯFMlr9bCoAiNC-xcit9u1:M 86͓H=gH'N 86Hu"R;CKH'N 86H_s[׉H -=#58pڼ"}yo]'"3xci7硯u3R.+ۺFg CaM)6Ckqusu,u`_x#S 1xLlcGW Zup3'zChc|FN{z+qrwNyG  Bu6.dG𱩁';.➏=օo#yxG?b~>!C&y][ \1yУU.8m,Ӈ$7@a_};;9ϟ9y\h5~ȩI$u3һq~O?ɧ}g0kQh7=ƃqcn\Ʀغj&蓇uxu\Ovlw !Ɓ8658)h/`{ >Izq0RCʵNz8|ǚ䀙sU)X1q@Z#yp9b |d %dDn(o>+k6ba+I[8|_;gH}+-tF.&r;$m?OH;Kq%߄B_YI.i֧˭&kp5\o-8্5u-yãc=FG'xmÏMz`p|koCCT;B x5NS}8'XODnD#hj8쏶&X=릈jGx>ER'N|'`w<|rƏ,ĥ I;,rڙq=I^ Hݿ翯?ouk tD{IA￾[·i'{@ϟ~g= :"{IAC{̳8џ& :AcFθ']9̅W}\gb|M/̻XڰlrO:zl0`ֵ/L~ؕM=sRϏ?@4Y~A+lZp`5uGd9ePB4Wܴ`<ZV8pZGE͡y=}xxpO?}K $OME򼖎+Jsh^A?;/['~(e8XO)U?|3i&g?28g6~|DGsP_}ǍZجqq9~ʳbT.>ڟuqQCIGxpU7>Gq6@-H ?'I4`rNH>|4qk 5 9 A3poq|g3\črX5ݨ9il4\zhl]bϘZkgh0kWl}W\Џ8'+KaOȂ>EꍆqʼL` ,ώo7f_|9Uef&\:Y}>ץ&qv0%0A_}#v|+1S%0A׹yp30eMNqlN:a5z7Mܰȅ˷wo1Z13kdl>%+6s¯8l|7kZNM@0)6# z6&vb#4Nr"c>䃭#cprclz ևa\b4㶿o ~brlպTbC{CH\Ŧ|ᴟk2JO|++>_}ݟ>G|jr2*ϟ~?y΍X+bo?_>!gG?.n86Pq+Ǭ]c}'f biM.>?B j#R{cp\]_<O +b㇫ ףT$ã1zÕ9`O m-.GYkEhĘ0Wuj/C̳5UMLM5qr,nԼs 摃sw6FK!Gd +iwdv.}szk۩^M2~e5 %t]CUXc#;dv~SﳼK&~'?o*߻srnY7srh֚<XWGFౙEG5酯7f/80mxS:$^䂉nf|!WkbrNk)9ԱGĦr)oCO &lu 'E8qkS[?ְ'}u`pG-Ƅ|?xW^'9r 9CL#Qq8c1`27G_Oe>(Kd( -¡_}$~>7,/3&o?DG?}?׆Onf Z-?81ݬͱl )Ӎ4kq2:p^by969Pç&aL`l}bu ö'y‣b`8{PDL:HIq5yL5p̓. ԓKL|&&`~Q;>X?1D: 1DžbQm;)P_aM8yoyosuܮҘul6_Gt^Wyͨ￘8nS}g>7^o~v=󷟿ˏ+f ?qIqYHڜMM7cCMɓGzրPCqy>Hֆ 6yu!ԱF>Q|D>r_ I)V&/^iNPX?.ź•??|SG ?ph:zQ셶NCM6d$O,xH\@uyP@ya{TWZ&K5ys<7z^Q^nq#s__}>ʣg=K yTW?ϟ~υyoq<׆OfZ۔es? bf`{fj6<\Tp1UkF8k5җ6|\1xN֌η&|?4nQ шi1'IsXωD[OMcӚ6c5.v\yKKSO1Q9p_k3sɵ5p9+1ic}KzE?06~|#SB"o'=pl57w wFWn7:=[ḟC}_?ַB<6 ['oniwlgX >E@q͍ONhW܌c.8Kñu,zVy5ZZkmG ~>Yɫc~O1NSfmJ |'D}O롭ڕ#Aȁc'u<mBL۱ҟs`}m'\$0>D^8_ |L*9 ;XG{7Z|1`AA3=8w9X89 B8>j˱wLk ͷ>̯q?_7a@$@6GO[rkw9o9sv6Qkz_#h@sM½A[>?\ .kzBp/GЖ7硯q1kdP ½A[>ߜ0B' "|mH_søF 嚞P/܋c =O+K?Nlz :Lu26~L6^Ԫ9rýI_ֹh>4a}pԵ\5yԲ?y`|p#'Κs>1ĺ׸||19 ڍ =YlO5.zԁ_k.Qą;`:ZПA |Z|j6dxkC EԳ08Okh=32+e](Pܾ')o+O?Y7xOOޟ/=ڐ?㞵bn(Cpl栭ta\h֩cw}?\CXn, G8fM:`8?5k 3k7ApO=ЌjS?\I2ɷ5bh@IDAT8\<9yWs\Y>Af >㤦b 8x֒v9ϡW5=# B@@,0 `׮o.9}<}k ypĬM MǁOOm8+_yxoI[FNq8O< 8hO._`s Wr⸰T4珿r,^5wӅ>q9ĉa<6dƟ\d'N(FrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,j+&~'qFQufH -VtA'VǦOn=ಮE\k/Ɏ l쇟86pj0#]qf\?\0S{n9łp:6 ,sl'{A3 9/#GSɷ_Pua!G~3v:֦.>jq=;E.y9_!))6О+^ +&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ&#gى&59=N$Pߺ.Ǥ<cOp͚ӀÏME_6~m9ȵ<[!qm~v=lA6u)2az>~\}­90 k䠉.bl}G?`89x|`Ɖu GΣOP*F}âkyfUO.oP4.;n:Oz5#8?9___<:iЫo3??g} w59q.c6E]WL l0kZj"GQVG ~KZ&W0sB̞6][0 0NksrlJp>Ex/ھCbha\|9>`7c$Vk9AM4>jxQMDc*6d>p50GVi]Z?/m.c֚3ܛ?&ʚˁ⥯^]A۽v]b￾mw K?[h{(O/w Z,6vOpqv5L m M$~}b[yj|#hb6ƮqC;f}#4?PW1G=<ٟb1堉 GQˆj{ ͉S_ㆄg9Qmk3n]'ZbK.O CHn8lx@1{"\56d@Q K`{@8. @sR'\Q,K`_P f)Ux  ^~w<ꃡql ϓc_ >fs3o_>я E⇚+܁kHuUl|}=UI4os^¥_q<\|0|mh=2ǹ"}cvbS R }L(\'5~:p} :IwO1.qö>#?Q^"u¯>朚8cgߟ/"CC{c٭/$+ jBW~h3u`0AM͘zB2C  ͘z! &+?4cꙺ[g￾+wH ͘z￾0AaC35}F~YbpjlTtu,񪍱tL 8b ^]Osl<Y?:׎P>qC0/yn`Dhİ9} .۽(L?v~hA$ 墙0p#'5v"`|9桉x V%?YZacړ6b? !nLkC~<$ӡ-vFk/_LCyܔμqIp>v K#~O?'C=(ykʅwFvc\OK :O}`m$T0MuT /ȧ/6.>m0>́Qsp DjًԬc~Est~j`c rp 9g9bb9aFȷ!Nɣ?9iG=@9fl?B`G=k+Aɞ`kOCBhO]_eeMz.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[]~dO` :k^|Vf}JnNö6<`r8g- ">!ӞŦ6M M}k6>cCPOj.© m'G>qpNy6 s9aWxM ۟Vs~\WsspTG:%9}}w-+Hߘ2yF~'zI~o׷FyF~[~/k].ֳ`?pl,eL QgL>G{ݬq<}XoÇku|`~;&oSXD-p9lu #pYN1s<EL>jq6n J҃úqPc1&5ɡ 8|(r ϿC9duZ 3R.+TF[_H>/rF8 xG[w5o5g C\FN[K4F g?^ץr0%t^zW}9=9oFNM'ҧy]8,CJ 9/KLsu )JumUϿ_?>Oq="(waǍqc몉˯8O~~7 8rЎjrYC#և#8Ǧ>ETlbb`8)TO0܀C@`RւqP}X0anQj0<8r<#|~2%k"&\kRs$.B,Aq-\oeͦT,x:I#r G/k y^NވyUz'Mbq?9?zg<=~[+K??_z?rv kY|?bB渮1o}x`} '9b</zq]c^-whqp@iO>~'ȍpr(qM ''}[g?Hj̱R8n`#ԒCSt< }_?{gEN;3' -nΛ贓~=I^ H}_P9wOR?ϟ~BAC䴓z=I^ Hhϟy6c8~'8؄a:X#hl66wㄵ+ ÑGW׺Tg}xN*!h^0B0<0S=NҜ, Jм_gY먨94/ݿ翯A0o)qzHqEiKOO<e”x}_,}I6wF6M̐GƏhh뷯1QQ 5.q#B=Ǐ]y6ZlG].7n?jrh?*~W(9d<:RL. &n-Bca3#:9ps."1lПqB} >u80b`]b௾OawqVե*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*XzE~BYi1=BGA+Xo%h5569oo5\9h\3?BY ~Gc;΀Cx6ڞo|UkȭHJvB<18Plq̫r#ˣ>'XCK 3RkbMlmr<fͷ/yj;1Ye)YgH0nRW5"_ 3k35l9_>`=1Q}ܺ:Wμ&믯XwĎw`e7faJ&y_:}30;bƼɉY2? U9l41ֲXb⦉6p:#F_k;4Bcfa}|g$WsNAsP57>㦿:`rWkש}00f`AYAcNlƉRZN~lۇ|uw N"6Xc 8zڟM>#KfwpOL.ZYl boiؔobw=3cMYo{'vO 5~Z]WF2ϟ~g>չ}ueT,_'d8>3~  5pkrxĬA 7WGAMDJ<`t Kk܀c)!}?b0~ElpSzJ|"Pdx4F3@ORr5Ivs5kf 5_e}xy&cI?&^ōw`WPSߘ~ruܮ~;<q،q9}9.).`Y`s3If yH38yȷW[}rYqC;.0z9X;P056:c|֨ug|7 O0^L#awO+hgиoxߏvWWsR}g~{bXj+R\;'%_7ւR_e| ` >5>?;d庩BuyuwSz6q8C`|b֗v|1.8պ֔GŞ)-b?\Cɺ`ͳ.FN(nL.178ljI|uO dp b0"1V[>0 Rs^.Rs;&m8b7}OIF##GǏσ|cJ_7S8v0ޖfn_(CgKٌx￾Vfsxr6p Oxm??? 쵷ka֧(i -|91rEri8C#q5X Rs 2=~ׇ1=k}0yul3~؏I>IqL~M!CX1sIɴ/<NZ 5q :5f/#j:wbzxn>|>r"I[ oJ,ҌQ5Jdڗ B@{㑀xW}n't?dr\y%]3XHW_y87X`￾`\̫f꥓IOӜ~YsF?̏`aBH*|C g 8G<4g=uXqz:9p䣎 Vi;VsY|fhK< I 'BakhOW[+™<&|7(q'\98c'?| 7p񣧟~)e'qЉM:=XӂYN#k>Z5Gn=:G&9:.>z#3֖&Z'`Y{'X}o>&4g`rDq߃A'FE[:p ppE=xp>R'\kAr5qnp'.81džׁSDI(P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]SmďY^$֨0sج .q 6RAtCɭ\ֵk|p:ٱoFN|p䣫!، j ?ǣX1W'F|&^=0G>?~hj0 j6|r;%>!+6d993uū\z%Ф&#gى[嘔{,;@Ф&#gىsݒѭCZ3/8Yu Ȉnǖ ,E`0#"TVrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrLk=rzH^nhR^ӳDrL31ցhִh֏]~l,7֝5=\hAgJ shs8>as5QGu!̭uɕg310|X5|jo\sчF9hjRx|SpZ\? ƾKFH冧wDaS;c5Hkm#t[k{9_3:@Z_\ }|2ح;c5sQLK?I?V|\>==cCo| ֓<( O|/<|9`Ԧ&RkO7:؍ 0GMp=TOB>r|b|s5r ?>Ec0 <\Iq0DlIrp%G#fl\'~c8Cu8&8/ u<:{+K| Q7g?(>ga<37z?(K_7nM sߜ믯mr?Ϗ4yfU׌~\??!_-rMz>جM|`S̚y$8+Fq4n4>4?'1B=M:[ql<s'WjpO@H"keW N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~dWT N#N~*36 GhnP`#XFYsqMl&56=pGzz?\zGOzgCupor[qПyfSSߦ+y != rSW>qNM NyBE<|Olm4=ˁO0y3v;NbV GjQ˾h !܅wNN`@=mlUkxئ2l9ýzcT\/} |}ϟ/}o=$ /o?olϟЗ%7d~kqֳZ۸MUeA\l0114zCӓQnu𑏠Ʃk@]}0fr!r*&~&N'P9D-;04'N} Dϸu(j=/?5CkB1bn"!Spѓq셟Yo9HDDq8jxqB$Zh_78<7&|6'!Ǿ|3&FZ7f;/5W-א365{=$WWūhFȽη?&Kq<\2lt&~g c}M&kq r,(#nȳk1{|Y b>cqԀ|{G\>;VrM x)>'Ϲ@IDATID?u>FB$x;`}'Ɓ8Bra[ ᑇC\(o/zʺGu׿ csNMw ОĘzrv #a1Lݭ32 Xa$LPC35}F?PuC35}Fz{+wH ͘z￾0AaC35}Fr$LPoЌgnM+KN ֘n0^1nc>~ףG«P>grra[s‡8nrc7\> l;6zbwYǮp9s\4>BkxRQ~l7.x'c8 GG Mm8:.2 ՞İ)6dq21J*6/n5_#tXL'>Y&翯⿾bx8X1: Ag7U&b~|r&ng b=􇫐pċ1鋍O~GsG=o=b/5klgؘ<0l\&HD|N}ao=sgmO>0y`gO8xog9QJP'Zӆߐ *SWhYӷ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=e_Yu,Ybͫف rЬOCS cֆS5 `x@ۇ[bQ8 ;9I ۾b\7Gc=lϹBaGMs#ƉarpċcSGj}uo.o/;2 `pE~m~5ay-# _W'u~&)\cʛx=Q<L~x'cT7ݰǽ99O?xm?KKQߍlج#Y3m?15?MYjY9pAF%>X9va8ЎU.kڃ؈c4 ?BLvcܚpiyoos؊'g >o?}]nno?rAa=)ǦZ\5~qm}#xPpXcq]ڇu18|zVk7cM8ŎM$Æ L1 ;PRm0!ИuD381ڛZĴ᣶A㠶wDNP2%:=7]1ܨMW#G}l|㏟ p sb O@vq 8m^?ζ3C/ dǀ1yL ǀߺNDjgh dǀoC_:g']NW9}DvO@vq 8m^<ZzFjl:~0b 86'p\mmc0|l@xư}C÷N!bۿօL]y`>65~E󱧚0ںp}#ў|\w#q쇏2dQm% 1s]: oْ.Ac2z^m^r¼y]䜆195 KG[wן ag(1<:/2iߧ)(u^zW=Nϧ>o#(waǍqc몉˯8O~4 8rЎjrYC#և#8Ǧ>ETlbb`8)TO0܀C@`RւqP}X0anQj0<8r<#|~3%k"&d59C!FBW8\KF.ᷲfS*f b [w!9+{;oʝSē_^I,~?c?1#[,5VǕԟ??|eq*O-[Ml`kZp8#O+kZևG `{##PO<"x:8*8Ur9wjcq9iO'GpmM~{MՎ|MR'N|'`w<|rƯ,ĥ M;,rڙq=I^ Hݿ翯?ouk tD{IA￾[·i'{@ϟ~g= :"{IAW{g3qMu0fcq7NXr p8yö.B&ƺkx15rW?b\8#悭c|Z_w=2==( a#(u4``ֵ7&F?׏9ׇpL Zi?x_sF6N-Ƌ8:+Ks2(!C+noxf0TsZV8pZGE͡y=}xxpO?H $oME򼖎+Jsh^??AП?;/>G}Ke8XO)U?|~2i&g?2?8g6~|DGsP_}ǍZجqq9~ʳbT.>ڟuqQCIGxpU7>Gq6@-H ?'I4`rNH>|4qk 5 9 A3poq|g3\črh-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`ZK]bXR*Xԥ*V>h-uU`o+K5Y6 hMִYf&|+XcG(kohlp}RS;^Ͼpm6I8qN'F-y5_q`yt礱pb~u7_?cFjM6ZiMCG]E3\mr9C?`s+K_,=Y# L7M* >FdK0a1pF<;ߘ}TM gsyz\g!ZK &L~ϭ~+*aK0a/uG~VfcdK0as9/f`˚%!8؜ PuFc-k(-na ffi:bc@#9fg|KrWl>_p4u]s3n&z6qG9aSl F46mLFh(DǶ}[GL$b;ƀ@죭?¸hm >0"pucCW{CH\Ŧᴟk2JO|++>_}ݟ>G|jr2*ϟ~?y΍X+bo?/ߐ#zcWr}z59Qp"?~`)`z=J%>(J2<'?\9ȚB;9rԚVF ?qocW{Yb bmdjǏ{>`cqc0MC l/9AHV/_]4<,׀%S6dvj|K2ʫ$"FOzۧgy-] LI:_>ם1T%?osrnY7srh֚<XWGFౙEG5酯7f/80mx[:$^䂉nf|!WkbrNk)9ԱGĦr)oCO &lu"Њ|k> B]0q֣cBIHa~ӃJ>b&Xe819WcppsXa9 $\惲DQ?[ܒ!_>IO>eƄC󷟿o}o?ڐ}ͬ19xЬ%]kM!s8!_=@s.c1n^W^%c`lc3u9?|jf]o,'V@=l{'8z({ E4[$XI Waud%\b6qߘw|Ա6~9b'uS˝fb ڞ5o"vSšq4n?'Q86P_AMu]O}ť1m輮FQMu1/}qqܮ|n>V}5/Wzo?_WqIqYHڜMM7cCMɓGzրPCqy>Hֆ 6yu!ԱF>Q|D>r_ I)VMqXW{!cp9߃씡Qo?cjc0G=K?ڐCجYzlN`XBL~@# lQ .9b[jgpͣ&\RֆsZ::ɚ\B#Ԥֳ?Gc0-JA0?==iNo(OVS1h8NoJÆ rm]|<9`D7>g=OCӟ=HrJb g%3s=}-4#.yUЃ9)Ͽ3u?۟?=1_kA_66b0"1V[>0 Rs^.Rs;&m8b7}OHF##GǏσ|kJS8v0ޖfn_(CgKٌx￾Qfsx>r6p Oxm?2fY"lƧM'l|+n1MlʥX:=+\ּc-\K-ε6|@#׆ ?_~h^ձY|a?' ')b3u6\O>'p`9̵&q&Ӿ8Նk=*`0Ԙ#s֪~ypFY?|3Mc<ֿ "Ȫ]D}y $ݿ翯? 7JyBK+gΕW=z9Q}YsFo U˼Zk^:ɘ~;͉Oy87X`?_a7d~FTR|nN8>롭ڕ#Aȁc'u<mBL۱ҟs`}m'\$0>D^8_ |N*9 ;XG{7Z|1`AA3=8w9X89 B8>j˱wLk ͷ>̯qw{_Fփ0 ` O[r19;u5/܋c uu9& z^Ϡ-?.ql5=^3h@߸5](z^Ϡ-oC_bv\{>| 9}a\#ۅrMOE 1ǯ~eqЉM:=XӂYN#k>Z5Gn=:G&9:.>z#3֖&Z'`Y{'X}o>'4g`rDq߃A'FE[:p 8梉ۏuxDJg>G\>#|b}qPQ %5n19 i<ꚣ4h8za?`s Wr⸰T4珿r,^5wӅ>q9ĉa96d?{—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:VM%Fa#1Xa1.XF:ښnhT?˺qX';6ͳ~h©|t8qpOx >(0'Ny@_ 8}ևdë'gǏM &~C O.c ZX[p+J԰uɃf~ 8cNaNL]j'W^74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&59=N$Pv74)Yv"u9&@Ͽ 4kZ4GO?6qNxi.~s ~n%x9z9M#VǺʳ\Gl69W`l %)(?R-`{0zbc+ -qrw>5Z7RԁC#Ŏ45q\b M.׆LXic%Umr;]❱C\ }=Zy BӇF/Vo>|RZ(y+w>.!7d?Ihnçvf>֗`>؈l|jSg񍛏 fF L&~}߇c!Wpk>1{k9hdž[я"1Nv$8q"`6R9X뉒[6B mY1p!as~: L\paK:O=%(3Q|3ǰhZz{~K=˥Sg^u͈9o_qG}<3kF??oo?xǏݐ&g=`vE~l֦>kfMKMb'6? 'Z+ˣ5Ea_4uؐzOV;z'_'rLݞ6E6ת5PtyQlts֜|=1y\_*k.{umumϗ>|7~󷟿ϷYsQ6_ߎ?2õ8Y-fm\ߪ j.6X=Ib Z:Gem]ÇvGh5b>zx?9b`9 nA'@(  ssgܺNĞ\ٟp!17ܐq8Ɂ8bO Dj1gmЁ裼,0Uφ᫁Rq7]x t:=q5?NL&|3X؅0U翯3>?Sf&j󷟿x&CyLf /a'6d,kgM <8+ˊR~x9g\ZL5Fj.=k jq#k 87R෿5qksr8Vm4mܱJ$fl"x8Cql ϓc_ >fs3o_} E◚+܁kHuUl|}=UI4os^¥n.xL2osVCruU*fmܾUWk&E36Gn9[~xͤhF?gkW]=l a5}#a?L^uUfR4os5ɫLfm 6:~?賉K&Z8S`7[Y̘=SL>؈{G1 _1ڇ8mjwL#qkxk+Ԧ~zsH Il` \d#N<>@̇K!F}9ưÏ!f.~}nae]#Ǻ߄9&Nq߄}ahObL=9Ud0AM͘z0& _Sݚ>#ݟ?HVf(a:a_Sݚ>#==};$az~fL=Swk_I0_Sݚ>#}_C&7LW~h3u{%ƁFkL7YXr I7O1р#F rE(\?F3c9C7?Ա.XROF gpbݻ,Obcᇋp`9IbR. 5<)O?sC䳎kkUAg8Fŧ G?9#j{Hͷ1{ї?u׳H|O lLyBN6A[.$L"GL >' >9ɳ6y'GSL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=ӭ=u}͔ou]S2|@ U{JO5SȚuxO֞fJYӷ.T)>SL kVׅ*{=%ç[{)dMPedtkO]_3%[] nk5}B=2\ׯ,: aUg9h֧䡩1lks} v)Kp0<z C8I\lj8jqw/k3=5.16y-(Nqyr䃝$hmp 1W#ȱ}ՎDɣ90B8ű#5>^uƺp7}7Fe?i08"?[Ԛ]E b?ޓ:?{PrKkăm{&x@?}1* kn^@ǜ省~`|\~؈51&F{wb &oaC6h̺u"guM-bQ qP|A"'(KA.Řzn&Z+>6dO??89i1hBKH'N 86HPgSXz͡ci6 <cؾ!['ms_B<0q~z "SMm]8⾑h >}?;W8̍pG 2(Kjvj.lIUib t1\LGFK6/9a^KszrNߘNҥOһqj]3@Rs^mUd4}^S?:/I~|~x\Z;MߍܸpݍM uWM'?fWE9hS5jObcSF*|?Gc0'qSKn!u l0T\k8gG>pixyL07Q5j9|>p5K 2ךɃˡ#!+KP.%#r G[Y) ^N҈㭻wΐW[-\==}M[NyɩwI/n$G?ϟϟO+Jϟ>B~ɿ8\m֧˭&kp5\o-8্5u-yãc=FG'xmÏMz`p|koCCT;B x5NS}8'XODnD#hj8쏶&X=릈jGx>&>0JmbhhqPK>9WNqct|q9̸$/ן5:oN=p$y _}BAC䴓z=I^ HO? N=p$y =̳8?& :AcFθ']9̅W}\gb|M/̻XڰlrO:zl0f`\]ZSivGSTC8&M-xִ} 9``x`z#E{XM9YN͡y77<39{-+M8fмtA?4qĿ3C3?>j9߾FEPG-lָɇ8|?v|ch}*uϺܸɡ#<8_8 X$d$KI0'$L>k}͜Z xӋԷc8>泙BA. 9&VÕfE1#7m4 8G`/y`g_u~ڎZxt8j'̼/8b}Ss&L<=u.}p&qv0%0A_}#v|+1S%0A׹yp30eMNߐ]lN:a5z7Mܰȅˏwo1Z13kdl>%+6s¯8l|7kZNM@0)6# z6&vb#4Nr"c>䃭#cprclz ևa\b4㶿o ~brlպT珱!󫈽!YbS~U~pό5g'ݿ翯O #>5jw]O?ϟf=c>1kCMkrUjP_605=,7~KHu8_?\m00% Гd{Rhkhp9jZ+B#ƄٟB 7Ա⫽1քW6yL25G=0˱S1G&˿!CKߗC $+NW/iwdv.}szk۩^?M2~e5> %t]CUXc#'dv~SﳼK&$?o*w}C7997Ь994kM|\ +̓`#LA䢉ǚWq 6-ka/rY73Z~Fȫj1j9'r5rSX ߣPsbSG귡'` 6NMNZhO5mr.8\Q1!_ s$\0}A]% 1bg,2FC19B0t윆x.AY"C-Nn{/$|@Tߎop'o2c¡~>_}@Tߎop󷟿G{m>f֘eh֒o&֐ƉfmM`HO/Ϟnt9\S7ց+101`LM>5c.g~7p`c=x=I܃"bAwNycᤅ+0Oºz撃PO.1YoL|S;>X?1D: 1DžbQm7;)P_aM8yyosuܮҘul6_Gt^Wyͨ￘8nS}g>7^o~v=󷟿?˯+f ڃu$fmΦ&1!&|#^oe=[kCSǡp`<$kCü:NXYy(B>O[/ωKAN{`E+ Arͫ= kESXW^rAv7]\{PXO1j19 8rɆ ɓq.ot(/8̑9|A˄y)S:a@uAb8-v8sdC믯x`BygIu\9O?P5|#}sm!l֬uMY6's~@!&?nfk y`֨`QEcy^fQ.}Q{k9enpd |kjRW1N sp4' 7zN'zjba+`֩\4'7aÄ|Ҷ.>0hӛc|ೞӧOki$ yi%1m3M\Ǫ]Uߜ|__}䙺?ϟZ튯WI? qu~o1^\7U@.u#ַ|njP>>XyOю>RuxG;Zך5[(8;@lbk~78Y7yvhz ׍%M:vQ<|ᓇ͸!<;6桿ԯ.*69Ug/cp찼g)dt2) 7o榯qlief863K1篟SϿϟqxJo\ 1j?(?{&kanx:85Nbr98Qúz÷~+1y`جO?1ƅ316i3Vbǥma>OQ-4=:s5N- M=Ý<'\kx9X/\6>֧ZaˣvA>r5%_)@ x҃KGy? oVs3yp/nk{vӳl|x<_}g(cxl9qO?gbs7c䘋&6Npf G.xk^kZez kÅc4F/z`جgœ| :`RC' Q8cZ8i_x jõ|σ j0 uj^hG9nkUu<8^kkܬ>}zK &1xZxSbf `dծQ"ӾgzWs<%3+BoҜŹ#}]e^-5S/dLR?Dʼd,0ϟ/z0S2 ҃ u#o)>7'k5K1ZaƑa :||6Z!X9_@gy] cSsyM//>'fM= O _og>x0RݠƙϞL\s;mg!f5X;5F[sw׸M#AC0Q|'-m[MԚE 1?ẺmuCp/gЖyøF 嚞P/܋c }yo\ .kzBp/gЖ7硯q1kdP ½A[>ߜ0B' "|mWO?|8ĦiY'cW`5A#7[ܞeFMw K]kUG-|0b=gC>qǷ I30h 8ד[#ࢭGh\sG:'.GIl~П lП=b`MSka&bِ 3ntTVQ #.pW>X>8(ޒ4puQyq4t=0\x+I|LJq\pm _lXr[İ2㟽\d'N(FrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D .KZr0>—Y]S-9 V,ҩz ehrweTKN=G2K@t%#|%ZuY:ՒSπa-P,jɩg_f(ww]N3`X}/D vUy Mn f!Dg dL#MDI\T\R:#3 HAJL̘@R IpfA0,Yo9םQ{UgZ>=<'wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvN=`X}f(wvNb?d9qFQufH -VtA'VǦOn=ಮE\k/Ɏ l쇟86pj0#]qf\?\0s\/(0'Ny@? }ɆWO ̖x9?Lfp \1>O۟#Ա6uQWas?2qeNavvOj'W')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uNx=N$Pv')7ϑg؉S蔛3DhwBr39zH>uN&:3p͚Ӏ͏ME⿋u'N6#F`=SH=o|l0`7R_7dl5|S>< ȅ[s`,\ WA?6\쏾p2:H'fN*(>1`#:uOMA6xm6|n1?YS82>9sB>s 675Cz_Қ ErY~ǵ[w5Cf'qo=:irЫx.7dxEY#Fq4n4>4?'1B=M:[ql<s|wƟ,jpw@d i_u2$cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83’i_uR%cS83ªK}Y֏ܠFt,qMZk#n?lz7`6~ֹďqٟ:Dz8?q!IW8 bl'5O]b996%8y"f/ھCGs|8onI уh|/:|rlw;j}pa=9PNO"kj(^V~QLӴ}Ɯ|kcl(^^/ɷuϣ}=$4/{} M5;esçc }.Q>!s_Y"h1klj7=T qnWsMOrkG-ֺgG>&F.kc>c֧?BMusØ!˩Ilp 8QBq8 j]mOށ9q+pܐp\o?7'm-|ƭDQKyɥZ;`rqs   f/@䙫sƆ ^-%&hj|5P*vx`a^@5q5NL:e /aƫ֚5[_{&Cy3p)Ux 4^w=w=3>[ &xxĆ _{_gY;#n2T^qa])\Vw̑skS>~zHͥgA-|5ul\9wmNǪF;6U^i|+<I11u<(>ry[-4>O>N6n  >78 xȱ/~G֍~f{/?;;$?jjp&&U{֜-"*D=zk|DŽpn..ꪸMң挄#U$[s8[;x̤Go͑8[;x̤Go͑8[;x̤Go͑8[;x̤Go͑8[;x̤Go͑8[;x̤Go͑gcCg/l/d-a@.%qyֳ~3zOA>ka46Cv!`ۃ] o/|}+|l\ljS?=r9nH$ID?u>FB$x;`}'Ɓ8Bra[ ᑇC\(o/zʺGu9GM ]1uV #a&?4c:[g!Xa$LPfLSg|!Y ͘VY]I0oC3}Fu;$az͘VYߺW &?4c:[g%ƁFkL7YXr I7O1р-F rE(\?>grra[s‡8nrc7\> l;65f30$ӃMXic\_K%~<#?.ֶMNAYgI#_'\~hY瀃X5* b|b5=@O[K:Z$^G{6&ҭĢpjC8!ƭɑvr@>j÷}\n#zU;& O5'&ɥB/MM9A0օCC=xaTV' 'XGtZ=P~^ I{PrK=ѺysXcg=3Fay-v5q"Üqg=''Zi/8\͓u$6ks|ư6)kB-k3>69hhrGp >8ڱe\{qӵGюp[>m8Vb\?ɣD0@'$'-Ǟ:6,-GLmX8# uMܚ5q5'5N='K!0VS|iGtTޣ2jOj.v(ձquo_V`m*|}s'Juz=V/|w=_ǯFiyR\PupXrdJ)'f>11D_\k[u0>9ԣ?3w><سba]L=61^o?lk=;Nc{ɧ!SԁTep4f:q3N1mb? qP|A"'(KA.Řzn&Z+`ȹ?6d~ߙ@PŠAj #58hۼ":ΘZc&] vW̓HgH'N 86Ț>׉H #58hۼ"󰮿q=R.+>׉H #58hۼ"󰮿q=R.+k?{l|0֔Rf3Tp_77Xb[8`Ս0X>j}6ft1[ &qKb6uneKJkao̭~%'k<պpFNM'iGs׳Zן ag(5mri$;AXEK[gqRyTϟ|h;qO$֢QnzP n<7ƅnlj &.j>y9\7<:.'`A;eM&P[~憏om4RG]/P=q8Zr6aJZ 'A=l>cMCcrL[PsɔMI^? sI]ϑ< 1rro>"pq7՛R0kHjS8Vѝvΐ8#6Б"pq_󿮿mVSn;HZ_^I,Yϟ9>6'fdyw+Õ~#rspϏO>jjl`[õ,>ւÁ1X!^s\RϘ>ۓ\lqzyk=Aฮw ƱWAڿ4GqpǁP^T|lc|N'"7{45d|G[ߞuSDn#!>0JmbhhqPK>9ONqqpvq,r38{<@V5FCǛh'8{<@uo< :>DvRρ'i=g=g< :>DvRρ''<1'8؄a:X#hl661 kWsU1Gq#𰭭G.ƇA0vͥ0hƏ&`kj1>ǦVY,vtq kam69'bM=6a`\]ZS&Z?׏9χM Zi?x_sF6N-Ƌذ:'K}2(!M+noz0T}wv65e_󿮿uŃ 3>ڃ?g=}K $OMErWfӼO<zo ]-}%ʿ8XO)U|3i&3Co=?>j9߾FEP[-lָɇ8|?v|ch}*uϺܸɡ}#8_86@-H ?'I4`rNH>|4qk Wk86sj=L/7gR .bf +xǚSGMc#z%;3]Uu0U`T]*oZK]̮bX.UfW ,ۃR*XAkKU ԥ*V~{ZRevB=h-u2U`T]*oZK]̮bX.UfW ,ۃR*XAkKU ԥ*V~{ZRevB=h-u2U`T]*oZK]̮bX.UfW ,ۃR*XAkKU ԥ*V~{ZRevB=h-u2U`T]*oZK]̮bX.UfW , dq f?arcɚ|j{~`KC=Wc &")@dbf^͗C>X5ݨ9il4\zhl]bϘZkgh0kWl}W\Џ8'cW $/\-%0A8#OyTM gsx׹8E5~.%0uܸ:W7%4 &Lu/qG|V \L`9óź3p1;u y'd Ul41ֲXb⦉6jp:#F_k;4Bcfa}|g$WsNAsP57>㦿:`rWkQ{)Iae= F;j'J}j9o18`j1`i6=hÏ0.1qq7L?1\6j]ÿؐyBH\Ŧ<ac2V>WbkO 5~ZYWF2g=g= ~:7bOӳez'd8.Y  5pkrxĬA ׭WGAMDJ<`t Kk܀ c)!}?b0~ElpSJ"Pdx4F3@ORr5Ivs5kf 5_e}xy&cI?&^ōwy hFXk`{64Bt%x4Nf眥5k۩^od'.^.h*T?koKfX5Y%~KW&uywFSy'dsCͺC8<6D.8}I/|1{1iëұ&"Lu35G1lZs"7\-mO1ȡ] Il*bh^6d }``BƉQ<|RZ?o {bۇ\7A &zbLcG~zC.Rk>1z3j#v!6;qsX9 $慲DQ-vn6_ߺIDz>oXq_fLzzo}X>Z?6 [5qbYc#<8S˳he,ͫuʫpr m ?sS.OM>˜˙lmO[/4=.bpFuÝ`k<k8i'a]t=YsA'X?C'cms 9W<똧+ڋ.G=cjD Nzk۩8^'ızjW5\i]qikc{&uC_Wyz1/8]O[z~8>`{PS11qw=߆/1oh֑9674nƐT<'|{<'lN>ás l8]O[#hMc=gZ|̓G}ȽQHz$qr) !`lhŁ|(nB\p՞5ᢩX+/\Cq?;ehy.[FX'5y^hl9dCOɂN8r.ot(/q0[f]kZ&K1y ̑s5kyGu;8fl@uo][\(,`@uzzυ7E2 5k]oS 'PذhA59p8sQ%_lWyԄK_jsppY[?\9Y3B:Zhr~z)6 ӸE)F#5+͉b='o=510~T G.aÄ|Ҷ.>0hӛc|ೞ}C=Ծ7 O0^L#ɍV9Ʈ]VfnjM\xUЅk}Ru=?<]zh '%_X 򥾿WGkLQS/CV*p Y׺[[q75mmc]Lq,<^v'f}hGr:<`_kMyњ Z` B 61y 㥮`p5?\wLpnX~IF##GǏσ|gJ/S<8fm0n-S8«uōon=tL)qd^<uo=ƯB<6?7<[dNO{m6<~gؐ6c-a57>m:a/6^q3FhbTv z`ֱpYy"k:^jv^BO6\8;Fcg&zR<`8)Nɯ)5:|~= 1f536\W^I,W:+rqnȺ7]_-5S/dtz;͉Zϟ:+qnz<3?!s_@֗l L |R_H9_;`\򈙇FkW6\S!u|C 1mJ΁:ˍz\pڞCs<mz[ᑤr q'qGFj8'\9`;c'??܏M>;u17Д}4Q{8 Ax4c kpwy'st{g?)G䃍|0jpOq{җu.>l7MsXu2\|F+f,u-WMOO .k\>IHz$9 ڍ =YlO56.zԁ?k.Qą;`:ZПA Z|j6dՆy7:*+gh08W5g4ʮkgdNWʸPNu/9'dLf&CsqS\kW7}<}k ypĬM MǁOOm8+_y4puQùq4t=0\~:&9>qaé5#hYbWk`S o}rؐiu"=Qu&G1 ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKZr0>wD ;vKJ?{8X¨:f~$+l +HcSQ[ 'pY"dyOM_8VMyc3.Q{n9.łptlX '< N l>ևdë'fgǏM &~C O.c ZX[p+J԰uɃf~ȹ 820@x+Pꔛ3Dptsv"z;Nx=N$PGL10|X|j?HR;NsԤ6"rCulhmr_26dj.O,m BԇFhϖOmoLo A1d_ºV-u{m BӇFº'lm AX1Ŵz}y+o\^==cCO<;'yQ@ u'X_y`#rMM7n>6u)2az>~\}­90 憫堉.bl}G_`8qp$ yP'Z_OzX ubkpTlk:᧦ s~`7c$Vk~AM4>jxQMDc?6dp~>0 (}E5j5/(iڍ>[cpO51y\V\6/[MWt[ߺ>|]2zx1|(/g5q~ˎ8`bhchbЇ'95k3V#A#1vS1&p9aCTL$68vM(N8rl'М88nH8.xն>u%Ԁ 98LGOq~j US9cCD/Qx 4^|oh(;e /a?fƚosR'\Q2X؅0AUku=ꃡrրx6.uk~[k6'GcF#*4Xؘ: MO<y'M'|ORXK l^O[d<||h\ [?`|G5W W=zkw\]J5G~cBϿ]7OuU&mz[sF‘xT9rf_L^uUq oc>y`J.6p97 G Il` \d#N<>@̇K!F}9ưÏ!f.~}nae]#Ǻwǜ&Nq}ЮĘsfd0AM|1uO3ҿ0& oC3}FVPufLSg_󿮿r$LPo7S>#[ߺ0AafLSgo+wH |1uO3p]kdSf5,c9Wm'`꘏h".~F3c9C7?Ա.XROZ zbwXǮp9s\4|K__cߺb}piяhc'RG^fmu-Ufpvրɡ? ÷@VUvsJnk}@[)du:Pe7@VUvsJnk}@[)du:Pe7@VUvsJnk}@[)du:Pe7@VUvsJnk}@[)du:Pe7@VUvsJnk}@[)du:Pe7@VUvsJnk}@[)du:Pe7@VUvsJnk}@\,=3 aUg9h֧䡩1lks} v)Kp0<z C8I\ljjqw/k3=5.16yVbQ8 ;9I ۾jb\7Gc=lq'GR !nǦ& h{ !\0*+l[,#oKkQ(]m/T{O}x{򓟼˻s?~D}G>y{w}515V~غ?/W1&dzחay-GLFi^x[ϟXk %p6?!l֑ج6ۘĚlṔc06N={hñ1hcf7Ff 9!fSOc )*VS|i;۾1;8%1Ǽ~;?x{,+:ծGu* _<+4PRz:]׼5'>gƆ<=ßgd,uX'Ju<_c:{DGFk'>:Ͽ=csߦG(ձuA$Y߯/.Pn(ձ7=˗2u8ֳ`?m,K QgH>Gg,Bf |rcvgڇu1|zCo?lk=;Nc{ɧ!SԁTep4f:q3N1mb? qP|A"'(KA.Řzn&Z+`ȹ?6d~86O'vHm{G^~?Q_؉}=W=޳;:|WҿIhN/cCc嵯6ddQO6zIq4m^_+_NXGZ_98&_ vW̓HgH'N 86Ț>׉H #58hۼ"󰮿q=R.+>׉H #58hۼ"k?^?;0\ ,=3RSs㯛c [ q],Z5clo`׾``?bx瘭SC 9F8#n?0r2G8ZI6ρ1>6 <c@÷N&bۿօL]y`>65~E󱧚0ںA=/ٿ#q쇏2dQ.mU׿n{Zl4a9{җtB 97[k?{宲HW-.i׳8 giH׾66^z#6^&6jچ j/{`{9ՀJ`?_wXv 7q^.mU#eZ]y|u?_V@>,g迴uzV䜆Q^=v]wן%XRMߍܸpݍM uWM'?fW금;-x&51<4Bm}bcSF*|?Ke0'y2bj ؤ *k-8j|ַyL07`Q5údJ&$F/EL֤.6C!F'KP.!-r 2O2J=IOluS?_cwf9cZ#61u50'K1]yl<1OϮ)S|Bf:f#Tטx2nS8~?~eǚu>2Q9c[{)7rx$o⮘g#&~ZŵQetl`[õ,>ւÁ1X!^s\RP1=FG'nxmÏMz`p|koMC~$w58M>=z"r#'JASIg5I_7EV;M?qs<s&67d)'.W H1p^oO{Ssf˿|?yoy[EVbOlK!~_|C$~5o~Wr_}gö޿5 ?fɟ{}ޯmcNM7q{+_W> ǻG??Ϳ{xd?ۿ'oͿ9ݶ}ȇ<~]Ɵ&K+?-SCf׿GRޞ{{0۽?'sۓ>Ic鱃.:̿ԣlwmyI;s{<1g!s]]-Njh%u=I Hovt,r38{<@V$'hLgOҚ5W/fx΁'Hf /ʼn>76a 4rqڕ\xǢ#8xVE#Xv  zC\jG `\uWcS ,;G6ly` 0|0uivGSTC&M-xִ} 9``x`z#E{lXM>YNͦy5 p #'KOd鎸ɿ+ɟo?ڧ>8꿴O3/}"/SK˶w}wiKK[Đg?}EoM\_/uܱlO|&.Gc|=ɟd$<壞ϼվ{W|xQ=lu٤< Oz_5/ψ/xy|̇?i{Oqo?'C}mMoOľ꫾kMA/?st{KbNejyf8ÈGi!G~7Id8l?[jo^Wyn Mz:7G\O|(ҳ`=ɦZ/h73"nQkƒO59p0s=.t:O]2mCf_}gvc~(3b#cmsn_u_6۟?wg.Oذsc`^<~2wM}Wo <|*oz?ML})+/s_=j5l o8|l_f/s|߲{;yb/{{Sgğ,џrkm)q=!A#Wr:cԯ|jQQshn={C1gHF5 B7O̵61h|恫f;kҏ8'cW HFf-}C&diO~S|j۫UcP/| W?ݷگݾ{?Gh{gŧY؀߾E/#?o3F!OxBX?ms?7nO}Vl ^zx7\\IϡOD7,76d~&6dTQwp?`>&ojdП7db%e,li;3?3)D?pdB{b/km{[ߺ=C[?Sz1'/8<6G0,G.\-%0Aտ70~ce#֥?:/ w-4UlJl-Ħ ׿~{:!g}؇;[ް{ƟѰqW|JOjlG>~8ۧ|ʧ0"+zI׽GZKsG[bs;͖'|~oo^}>6G w|G9}g}fp^#?=KRtxx+ɝmtl?l|_+G| |o[ֶկީU;;Zq{gGO|Z/ޚ?Yj_'z'd>3>߼}䓟o_^i oF? ǜ x+WU_B},^柿?Y5&VW+bYg++gκVX֚5}?织׉ڧYWF2g=߿];ONԱfS2wuzJ 4k\1qXS%f bn\E?NDJ<`t ^]_<lO +b㇫ ץT$ã1zÕ9`O m-.GYkEŘ0Wuj/C̳5UMLM5qr,nԼ#Aw|4Z7Cf"dl!8)|k}۷}ɕѝ~}_x-oEn<4'f>> KqkMOy}S_NH|Ӟ"~?}Oxڶy?1/xa|9 <9!{7tR}^l|ԟ鵯}uφUP+c[ַt#~<n?/ ⟽~!a@Kb/׵BM?G~۷ƼhSM,hOͦO֤ZߘCNOX\{ &"՚#6B^UsQ9ZPZ.$6K1}4~z>n00`ĨK?X>)‰[ ؟ɷ=C k=j1&1q#l=!5o`zP.>b3j#v!68V- M?C]$hRߧ=Ѹi>PhS>n{.>hzm! 3kvOtn?m3 ^>YӾ#'J9Ͼo_S= w۾˾bK_5ۋ*mdiws<>y_WnΧ^H8^O_'п7/?YMgGOF =_zv߳-o3%//>c~_к0}G ao0,#߈M8\ :ɐʯ=6X96.۞4駟>ċ"ORO؏X˼}W7}7pxx;<~8-_?MW!G0c_ӂ~B,j=XލS_JLQ~[ь2Y~%} 6Ӕ6lWƫy`T7\_7;dexv^i'U7\?g?O?*YY9^Wډ?Wb۾왱y6#!8ss tn8]!":q'NY'l΁*3.om8jdGy |a* Ns<8q$=6umnHlX;DXd4N_29y!)ѦNb^llpF}Vfd8O>rP'aq Đi @.+aN_Y7 S/BwL"~)#u3 s- ,]?aaH,o.9 NŠuq‰ܾ}mo;={?/OPG? I->m// B5qm4Iey? qYg?{8s"zt,L}4H}UZwXH OWO0PMIƯ,xBLJ׿uaK]ÝxX -ƀ9?[>]zo1~10 aB78'Y(搯ZŠm#ziW| B#PddSǍ.bH+ab[Ȧㄑ 3Kq^SG6ǯ~\yVY| lLXd'>fdkt0qK>x=اZ9g[GN%G;}\<dp|s!YZZgpH{>'nR@Q#]Bn, C?RrBR|aק> 0f 6]tС]h۴6ߗARxW &+K$Qw<w<7^Kt /EfAᕯ|5/H˱` [=< Kc!煗nXZFs3)ƀ~ÿ1r8"1yѾ? <3bш#jsyaX0[Ee?m$89cy/?o'>:w^ZTdLuM:k{ɘ>I}_=ͮsp>e I?Ai7?q6~(}4 2!Fqt" sMES/bƺ ϼ.6B5x'>ΏCld[lq!ᶉ:Œyx}99Fsj77:ZH D'N{ a?a, s.NN-;u!%ĆG}l<6iÝt{={??'I 4W/?}`\8\}եAxBzr0hЍ7?~M' o7OX߫=!CΉc,aX{Fo~^?m )/M~< SeiǷ_Ca7OW?>| Ç?Q8צ<\r%1 78\ =WCcԆ#RYT Ou5+q_2壏G{fbNl1`\s[3 cC&^A9VmR좏KzE=d8d>tdȝرL 8:x`.X\IyBfk*, Os,5RJLϼ%Cj <9]= eqEBǟ7=])]t8bcʨǂf }̅s{9|~;``knQ9?2qm3zkNu$`C i'ߎXd15'~Ӻ$0Yk>r2dɍvPqV+Sþۅ@ G5S.d'dW*h!F^?<4.ށ򗿼,(pxٛh<7D_ydžww.pc_yl^;O/|x׈p+M'Hs W r᫆k?}MO~ ==;r+K/|Ͽ>1TW ?''I'fx#9d}[St*ԛP~pꩱ OJkQ?39,* 3c_3g O = OEmcMϲxhؖBe<<Oyp|<1`̻F V&~ ]qbsmB,um6Hűp$KGHlc-n'n o0l1Pwxld6X`~t`>'>mo2Vo~tmQĬ%Dp!hfH+^a|'S'>Qޫr/a[,xbطop%N:_WW~#嗟N<<(b`g>Sj19i)0y8m?^!s=ƴra5h>yU޽{{8͈1HɎ1&wҢ@GY,MY|_Gս|vi'< Q>uƣlFw ,_ǡ>2Q yAţ@}3}dl84GWǡeg(lG' 0Jga,z9-2`d̓ѱ'E!s<2r0֤.\8Dm.`g3dب W乶X9q>qg _/ی:Qv7=wݎc7G 7y©b o=j8panvd<93}c` [~CO^D095:9W6=-XfFfijx} <2y(Mi{>Q8ӎC_Q?ꎲjv~~gHW?-ۯ?lx 9柝w}pd,Q ]Pw1% ȴ8d<<2XjS!V K|րS"2ul[>9%ksm7Fdb׈͈b";4$lxΉ&XZ|lSgm'9}%Ă3sۈO}6 ,̩ 1 y"YV@8m8+0MP!>Wi _?g+l?bȄ7_ R_6B,>0A!%Evj>"CK,r0wlݜCeC6NM{/`Qxdڈc#Ze( 觀 5Ff]csGC-vb"⑍5.L kĀ} ۅ&?e|-@&ǯEo~bC(d^vю`fHJMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FMLOgD#>28dEHxl<e;:y 9]vby-9blېoGSLj!ln`3ok}vdµ0n L g`=!F/gZi/Cև^CAO,m\حO![ņfw!/qb.dA&6^j% ɟUO+Rl \,-$JP!W=Kˤ7IA>[*dcgi&)WN YZ&I ܊T!W=Kˤ7IARu2TU2MROV YZ&I 򕪓B6zIo|*"UU2MRT 1pճLzSB6zIo|deқ ʭH1pճLz+U'Cl \,-$TnEeқ _:*dcgi&)ȧr+Rl \,-$JP!W=Kˤ7IA>[*dcgi&)WN YZ&I ܊T!W=Kˤ7IARu2TU2MROV YZ&I 򕪓B6zIo|*"UU2MRT 1pճLzSB6zIo|d2~zl.@™?w~bg?6бS9=XƨFgފS|lacm9G y!Y+:`8fЩϱBƏB'φD? br"Cn4lC|X%~ba*69/RiCڕm1p6rc{ R6:诙dB+&J[%68D}\w/ڵ7}"4m }P=jz,?MK0mp}P_=3xkskM[M~>a~㤟ۡϿt 2#sDq{r}K/Tݥ+WrXg?ۯ7~_|uAӻZ䜜8IEmљBؐgli Q_?$7@l8|`z#sjeb3yA 5e?2~`}1_KD$v0h/I! l:lOK;O6F~8n}868Y> '.7б>pclvmlWTa*ݔ&l5EZWa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k@_ˌ'N?s;xd.9 7\dςsҜ~SӺ!plov`gKMulCM8b!`[Xo6dbCl1䃬NgMm33B&eQtkbٰCl ߴ_jQplp.1 |b, qDrh rYu;ot$l8[ʼn ~:PR_bg}rW1LAGM6vvr@⌕1ӂ M)"؄˚׆bˎ::؄^?F Fc/c&QUԙ &\_?9!M'V5 ~~rN'tF΄N&6  2ؘ2w\dȲOcüR,(am#fW'i~x~|ؑK͜\l؈s `ɛc[^kX߹91bl:RoWTfdp6dA,"Qllb!\plAv:vR}K6C-}O-tڍ#~b]f<6},׼0C=.8p2z$"IQS[IT(ջ9fct[MY2)Uc{c@_5;O6e z7LKtgSLJջ9FlU5MYFRz7Ljl4Jջ9Flh}eST1b/Gk/*˦,O#Tcz?dXxFl賈Ke~&sqbP\lg>\5Ndوg. G\>D}lmuC x'[ ;^}Û{nm%NMll# ) Æh' FYHȃN?x _>tsa>8C`] %e?zڈ1/Noh;6^/p[G3r/$K&6!>y kզ,!؄Fv`5tU^jb򀅼>y kզ#MLفйVmz׏~#MLفйVmz׏~#MLفйVmz5kl,T0ty,1tbXpy ;m#f3c}p>al7?Y,L}K>l <ַ=*>t6j/iN&[' v8`l6,~:F3`|`C0Žണpbk!bg/بO{CV'7_n02!6dpRJ22nڝLMKi{_c1 tN8iM?LF_?oĩ_ܖ$Td0?LT A|؉'zpְx@cnM}16C|#pԑQp6쐜6Pg-3G7aZO{ku,L2C6F,$ $Fx: :7137q'F2q#ΚıAl3Og}1đJ@ZMdsM ٳ|/zS\TMI"C{iV<DUTu2^+ϟ-$Q6U.;G1 . /mrPV^1/C5x 2CŰOL١rU1}&_Ƥ_|'y|'CҞWwXG!_Mf#?>Ӟ1J*+] X G?c?~Τ7-/-Rcs:gjtͱC׏])sB.s3<:1pp IJlvX 6m\9נ>:dsqkSVoN0% 2oq蒝vH18}!k`X0/erSɇ2v7u0GC}t#Xn vb#YMN,y9ɆF6^V4aܛm!@<$mqZ& Ev,H6O?1wo?/7mw:Pv,H6o??C1ea>ˆ)!E8eKؕsn3&#|m\.րkCl:̋Fsulַ:dcMQmmBW4 d$6$X@}O6kmrSLJ.ha=ƛ1i`3/>pl䁨a.t-羑rٮI̅ CؐԪh$&^=&N"VOׯG4_6At2|B8)H&7A|2|B}8oOī'PĪ CDIMzM E:P8oOī'PĪ CDIMzM E:JgMaN)Xt M1 EbdyxrA9#;E>ó \lyrgL BV`o=dhWk1G"06Z6A@7Ά6 >tb!ucѭ&8d65 ԤvۅXSN 2ܼbEx/-aƆF "Hku#Z}#=rDA<|\ λ9?6Cags<.'=˜N"6tw dž]?:9ذIp(.ǷLFxll%6B!,0dXssl3ņ:`i}pq 2al%G`sdJ̉Z2~ cI^HX6 117➨xVɐ.~ST-JƄ"OIZgŝ ~|uߝ#$_+䛤%xVǿM;ґ!&KP?Zd~ibDfWKŞԯ?m3csW*έ\, \xsaG^?8%>}p`~lMbŢCCS.?ÎNz۠`%llsm>e[6ಟģkG^v0wDlَpYnNd֤"bBج$ǎ:lO 7acSd)F\3t8ǿ]E,,X4$?( Z X4$_?B'ޠUKOBB'ޠUKOB?Ya!:Xa8Cpt6 sW6ce;F~C- 1/Ɔ aK 1g;d 2` lnxn\zɕ녺J&[T ~7#!CHv<6|,0e_SazkTC \i=%pٞc!#C6D}d䢽p,'O:X h]'b-3& 5"j|}׏8yC91ԓC?󯗔AbYD_}iGGOO_neR\"?.Q~eؘO(Wؼ2h"! gCǎpp6k>PВ 9.~CD>ۏq6^\:g^zdS_]02Y=KaX9!>tA4BtH<2pBY-,|" <2\K~&uco<)ԧB.FsO912>?u~Q sSҒ[%OY榤%1&KMIKboM4kPB9B}uq βօc39C=☠'K;+DJ e2rt1O'gPNK&\^)?C8T4MlB?ꨮ1MKr6 }c??榽cu#GFŞMlB׏#b.ُ2kd'g}c5q'd'dΈ Zt8>ձ1EwElUbr{gQs8Dmff~lfo/՞evyE<<Ć5_΍lصHD# &Y Qp}lJGO.;ClS&BPK}&dvo"tY6pU_ci̫ס{>}g 9veUY??S<6rϦ<#,o'dv=c<8X@96?f>c>>sUbC "nsad7ִ Kk SBڨG.F}:%ecL= Zk)HJ08 il;9W>~_mŖkd .1ď?s-BO[8b 8q_{ Σ!V 0{@Cz GSnӢCf>m/Ko4jحʵBQXK“ޤB?O>Q2ooo!QwOXHd#!%w A]hnn89hyA.z[Y)IKȅlC'.L%5ϖkgK 9/ܷoN"M,R ,?q[T^ t9^x<CLJX|4d=3gkF}׏~B/9=k ח~~ʽf=Kr{YƉ~[N()?.<+/9TXT" >!6{>< [8dA#ݼcsbr?9rmuX 2gJM沖c}-Yx3MJBd8jsQp~)s(v.!c7O~).ċyA Cͭ> 0f 6YE{-/NrUT&9ϰF$^n{Y%ꂑ<RVxsLT D+#ո^}#<~?ۯ?YP~q?Vqn(~?V_۷ǜ#bI{n2[dabN$G X6&K!?gͩk)'[On"68a*E:K.Ȝ˼.uI|o 㯟9~B-Da(;F_Ǒ_۷vrs'9=6&cnι]A-M'+cgcD2R#.S9!CY׆8d̩v| ‡8|ȐUW]_k8mF#vڎd~raa60.z`Ϲs!ki/yT`٠+6Qv%22:2D}qx܈F /n߾N7" =Mbu sYtMP) /$ *cK ^PFQ~F?/SΗӯD~Y,[xw^;`.O`SgBR7ac 7jg G,d{}̅s{9 ~;``kf}|ԢfΏ/|[}-Yd3$'c Sl9AC7OEXsg0 Nuo?XP!/!!OY n>8ml7knu25]x޽{)P=C1ytCbOnǓ<odM}*fa(*1}׏~LϿzѯ??W#[ eأ_xqu!x g088f>yp|<1`̻F V Yn;'9I!g{AR.#Q @:Bbkp;aǰe wF x(ȱfMmsGO vxӏԲ-mSCp͏n_-_w\ǯ,QxIa8Tw4F[LayJXԉa8Jxj}00}ٿbxД#s!]ʙ"x"_gE΍ CfP?a_I={+KόÁEd7 2~Y`?#+Lj oMR QG&<,6jCU-y-VNO260#CϜ>;V` _ODБ4L,܄p䗁FcϝEbg8bzlfGʃsA}bOo?!C^&BA''_/ݳcǎ~FZ`7@\ɺW_Ǚ@l"꽾Ʊ?{ {L?[.z STǦ_G ~93:O{,1H]wBرCv1<9삇q~)+3%-66|bR19 YN}t6pN M?mc) \bSm#?$2&lPRQ20p{sH9qz/t6pq-+~u>z6RgO_?Pbp^E17UO:hq@^Pn 1'̙|A&XL™cs!2vrQXdnlm!Οs<}،퀃Ǧ=`Qxdڈc}8(MF!A;L^cĮk~XddcXldnt0Y7vNg,%Xl969~.E/ڷo.vAH"@&y +0DEQ Gyx5MƩ-xŚ^L?xLϿӯqps1:ph7~N }uOOgƄ3j[2|pep.~xbvt'rs ,WF9>r¶7TYpꖫnpȜʶxx&0m~`/}ζLƍ:B8>~|p'e~ 6܁PKvpr0 ˟@Cj@*c}=88$_/n IsT?b=οx:}d̟ ppX9lб3B<y+>~-`S|la/GpC|^uǼĊX#3%8sɳ!1x~ Eh2IeƣK .2xo[Tlr9_Xc+Ncl$7$*mpub_ sԷj49S=xUåRŖL#"Hml,cŦb \.Q߮kD>'>}_ʢzD;7~?8%p \g.GCQqRm۶;w 3cc)qs6N#1DK[@c^t6&'sWKł<؅PK vĻY_;mb!m`ȡW K \;9 A!tm[C`eM<|,A% "=wԼra7:{!pClyh2~n:Y|1o-m&8ddD޷{`bŘ/lyG ۡLG'3;|D 6|ҊR>JpH ;JCOjk_!^X<߿?^X|A|{?q46{xwK#NJxRwǿe}_?׏οx/tR3ljZ䜜8EmљBؐgl䄸Qɍ#'16۩.'xh2~|loS+?8sXdApbm}|և%G";`mŗ 6Z6Hbw`rÉ' a y'Vm#?>Y8v|yCncۨO?kCf{޽$ o `dZcm(Lq|Ba{j%r߻o}l'>1\OFqӬ 6#Cp9]" x|[ c||;\j @IDATbcOj©g `7>~!b!d}p>kj 9 0. #X ?cQC$eڻu5dn}85ˆMbk_ַrP,c#;uۖ )ݼЉȔE$Y,56.Tp`gQxJ!b @1H-}h?^Yy{cv7 %qo~ǿ?^9;&׿~~~ןrf^wļl9g> c뗻S54 ? NMb s`F./F<G,sc'6m֦=\y%cQMCe+dhE C˂y aYz.Nd\ׁ"K=,6zdžE$d :8jAg|`) 2;N8a8pcO k-) @#Cd!>G3KH?s*ZĄ?]91X9B+?zbkkC(z ׿4_1^ǵ-?W_}p;ޑ=k$s;?|^~O?֛~~?oGLȶ(᥼g7̝!gN`%8㉼#fW'3=5ذáK;rra#Ωs01Ė?`9ְssbVu8߶鯨Il$ɐ̓NYD؈fC,؈4u2:lƇZZBG6=XxlYhyazbѱ]p:'dxK(5ē-rJy%0S֕V0[ {hXo)OX!E1O}jx{;|O'exp]N.m.펏c=0e?[^킷z c SS cp<ǭݻYxFlٰKe~&sq؈E&~q3U#jMA&xptبQd<2xuh5Nv6 j{nm 9<2ۆHD<$AYԆ%tsa>8r %e?zڈ1/Noh;6^p  W%</hS/Կ^]k <𔛴/.Qrpxl٥ /:7i)5^;<)Oŕ ?wy77.?;?Ozҷ翠|WPzܢ?ӟr𬳞]cy<~[84}?v_?>Pv7?+GaQظcS>t\혧"`gJ 2>N<ԃ㷆32skꃕYCEŦzԇaԷP7>kQ>yz[+`air6` 1g 1aO>1ڐSG&q$ gYN~s賆+i5NmCx~6Odh2q-T GOjlilQGrw3jPןd}}kx̽J??y Wnnz_n򰇆m8=^śZGݜi_vY,J?.S8Xz=WeЎ#֏~~w 7ԗŌy%;3?%!>t`$!1lX3!c$:AӚ(Nn!p|0ʯLn^ojO÷}ۓk Kc<Í7.1u!W^m;?7qSt/`Q}iÛͱPX@$~oOOmK27K?O ה]?wN}ғxO/o]|8xpxb,ZsgqpgšgX}c^?_}x' 9ॾ`:w:Ht&uǎOͅ9Y\!98a8qXbC`l 6n;ܶekPRӹa)m7'l`vt@@d;$K5y0C,X]2cn:s࣍>:~sx}]A7;1&aNIr0&ZdgyD϶KMFQ7!I<rt.밾7{%ǺVJLJ /mx9H/aaKyw~-05Vm<|[_JE_9 Ozbi>yx[.-OKxBqdlo2/(􌀚kL ^5~?~/ќ{ozky9~} a\P?&f Ν;Y$yxW\.1eCɔˢ\2%Iɱ+gFB< G &]=auo O-|6d[2v&o Sٶ]!+`AL2Pud,l>g'V~|6Fn4AnXj86@0O}sȉN lWF$ݰH"* 2 u8a,BE w'nە}qKsOŎr]<K◆(B˩T}6gqAx`&;v<{x{/|C÷<Ԃ?;9yw'>@#6ZU|/}7l{[zHTDZ<7߿]sяwُ~ݚ޵'dΌnQi!tnx`oA9F\^`n^tѕC,6rLpd8ֶ 18epn\3&BAprC,o-ј#-Vx gh~ v@_~ yW2~vjR o)'n^" ?<:{WQ9I$p f"PBEb JP4A:*%P@H"B(!${k9oI= {%5N۳̙=?]|z65Afڵg?] 5!#i }C~P|gO99y(J`vzw?Ç'KT29Oy2 ?hx j?O9ܦ|V^g>zM.-[baY!,ͦwt}e5xў.+ܒ8:avyJ%%RvVȰ &-<ClؗeN|N] G2Gb4a[ÿw?k7f{mK/4}+.{Qpi҂7"MCWX?]3e}y,<9A?#n~i:qx\4O5@Wm2N\sB\ǩi'n:x3}2  pg>@ 24'Mpl;BhNFZ |B{8-AECGMs,>5b X ڪ `bU%08#ߧIox%HSi&ϯk6kjM=Sͳ. SJs9nAKibM_ZKW^I7pc Q4*I+I'-{^uUnUZAēNKJn'g}v:3eliO|n'x;|{O?G Ckc/jٕcRHO]o[oOVz[oO?;g w}w^$q7Cb'MpWdž&Ơ/?ߘ2O}Vo;6^Y M6Iuޤɖ4վZlN4cti6Dm>sqoϯUjugϿFG'>zM_ޯCN٫T%S&aHC88"o ]JZՋ#c9#n1.Fcxp_v%4p|X<-8|:pmLcGJVOљfd |_/ ;qȓ`|]t hKy }\!vmڟ+8p |l^B_BwN&ӊkcںQyQ[Df58{@," lW+!8U "?tuUӗfDv⋧" lZk)jA?>Y 5!sJBmAsKoLAFM |S+GŎN>1 #|F'Kj D銫Nʊe?fVN7fa7g?_iqKdi|5fW7_C>Ys`V>C9|Ѷ: rLI'/4:;߰V,^}t>YZ]XdӉtaCJ^AiU:$=CRL)S7-bۗ:Eq`{ _w# )K5;h{+ꫯbEG kM+ra([M+LOo8*%B9V방4d)Ǎ2|+d ^zI>2m:Pd#U,[oUćk͹VkqozmT>XQQOBFw73)fV{ P5ӄ[*${]dӎ;Ï<,3J:(”ڑu iJA#׾￶"*ֈhW=BQ}?---ֳ',e>RJj\L|0ғ x1-zAhzǘqt\R۷Cd-Ct5}d[ ;B#=Rv^>סH94^`"G١ ĉwƅ6J}lr$ڣ'p$ ln[tM&Zl|&AOh?ڰ]ַ_Bh/eB)SBF1onrjDv\/Av[ Y;3PFeFF>:RRh?A|Vpjs?sЗ7X_,MVȰ9Q>&pG}щi mńO+jkM{_-i/Mz^!SF׈ޒ8r:jo~3iVKKJˡ:#jV,/|@e=,5EmJl6!aBo<[~HgN)1!guSOfW$U?v>&>3G䨆}~z}Ӆ\(tQ Jos7xe[x}|ͿBcn-t3 -CE#NCc^NS;4NUOc mlmA_WW\i/,QnH\ORyq;f>Y@,ńV8TiIguNMOXrK&dXAVP Z4c/LsmKsBPh<9 #éLׁђB@2zbBК #ϫR.⊴{^Lm?iK{_&Vl-D_% k!#+dֈ?N{hS_( pN:Ä 4Wm?v&B%&d$* T7W;CLHwU9LwoPM5V?\x~ p=8TK?j]u?:i?6Y~ЫߺouA+Xkһ-M@vU8t/ =&ZDd4qx8u7QART#bFӕ-߅ /hK-TڄNUg@;&qFz".S%V0A'dZҭK֙g oHE]R42lnՖ>Y/ u:eiNY?a̞?#S̘1цySO =ʜmg?8{8EzR<8#8U)ϧ,1!φO>Tv+%ȥ@U^y<;6ɵYw1ioT%iW 4lh 0;D'S)뭷شՖZ9AZ!3 啗'U?jrQeT 23ҳ<7K iy_hh7>QHI*-u { Mg,bW|9ƈ3dUi`wK{[?W3*ִ[(_<tkWjD27췾6r=7 o}Qwv7v\"Vȴhfߴ뮻.{ 00꟣OO:g5n?歴_ ԧ%^>Gڵ=#V,GGI6e5Ȫ"HIQob)>g?6\nt/lj%Ԋ=d{Ȉ'KjU gS!ӇJ|wꈌc_=dp0OQb裍C_~ONZkoCBg^U%}Gm?@ Bo*us ߿+}]#8e)4T cL.FI%?2iø|hw36' [-L}zcqWAr/2:8 MCθA<>2 #n!XLˆq4]F8rQBQa΄.:, x]1BӇۆ>LGrc=xN \eizO1C^62"@Qq=li u471+,dG^~ͫk2ݞs]?i}wG}D];v/gK1 zqL1_#7z0`#IΈ{׉DQE7V2„L^!tKl;pT[>Ԥ ,?Y&Lȴӗ $/c/PwKtoZ#Kikoyߖw I 9GO<Ϧ|["Ѫɚ?+%͉{t-TI#<2Vܐ>Z]WdN<ڌS!WO:n6il۾3 Ħ?H+^8c+Cw QQ"Uq!!1BʜI+ac6hzsO¤Κ9D>oIFRMXX__tQu/nuz0VիݟC!8cm_(26g˺'i8zև[з/3GrvtFt#B.^4Ai/m(l} 7 NفiYg=p_-Khp]A= #еN*M bиOz·}៙299b Ӈ =x'+aL#D ܾmGh`SNY=AF b NiЪ1CHaUC,~T_,#1@ǬiA2|t^u>qz1Q?_g?uJ;T*u}iԗSxZcB͌#,ǞwYA@B2?Vl }F#/K.eLQA n ߹QR.f_t:jZ+dO{ذ1NK-tw/׉G:FZ_´:ZQ w!Giv9TCr}jGǍׄfy7%3t͖Í7n>w6!&&\5!Oܝ}`(KOimk1y 9&]y?V"G7ճ\Ӵi=C =3mT_̳L÷:+N? ZlNk*WѥQ uhOճ&ZO;ɇGGjBfZ9cVƗ й.d!D6h>g tE])&Lз,]h\r\Lm o.fhg.}Q4*{BU#+BKooӛo#R*W+ 3۹`^6=Md2uA_S39sua^3]rōߠ_>)'{}NmʫTKZ/^{8zMd#g{0řѦd[l9k^&DdyOJI(kVkij{oi% l˵&dr $iPYO?=mFBi97ˠm>Ki]i\2Gҙg4Smi"uW2grJ >^7.m92-Z!>eJorPh裎N|/GݤɘeMcweرVS{N=X:唓Ֆ'5Ϟ9'1I;(&0=xഃ6)^zO= ߠ 7|(V5s1ǢQ鳚_ΏINeS~]u,vk*\$.hnuttOOg{. q'{/{:jLBBnR֓*p3.'۷G8|9̓Qk÷@>@!-O4|bp'tJi96MmBi#1@mt'8<˙.Rt&2-6r@ YxGυ ^ ypit=hc刓.ˠϺBe8t^8uʔ^ p_J5@0IRPo,+xd?2Ԃmktc19t tV ?"e_a;[|HUm>}Gy?5=PgCLKH}oIS^ziZve!)Kҹz uz>E a;EhSX!'KiSagI{gV\{͵QO1QČtG]mgB) p \ꬾjGi# |`s7?ߊMiVTxIhtGvbo~zOdtCve'^'KgvWQ8'*}$5tChIG OwnOEfhf3_G?dHݰ3(!+ +z${t>[oK/,K,ʟ:Yo?eKn~[Ouz-_훖_ay^ѭ[wöEU2y17qO$[֡HA'n^ա tc{C "9e48f:<|MOhr. #Ȗũ3팕 )yNDtġ'=S䆜 8>Ů`{"Y.Ե82=c&?pBKy| k 5eEkv}p`["l1c`. 9D9 &fŋ, 1^,7xH˴?D%J$柕>E͜"]_F^tqzT>] #3Jgݾ>1&3. ,IJ(˿U յ׎fشo_maQoY!ӿ)nW?GT_'t ۭjqZFLǃqZer 'Dp6Պ)KqhʫfQ$5u6C޽{^GH:3?갏V ImFLhf1_ +2Nqq2#|z }tVۼVV\wuG64OyN;pcC;X㳮}?~ FIFjn_&Ka/K:Ek ._Q`)ڀEnIGi57N7CgYٜ~o}^sRТLBsS]vR{?'Rֻiз:z]z`.U2z 3>XyB877qBˣo:С$,c]BxmJ=䭏R]Ez%p^1v.m#'6tGtӠs?<|G^6۳CLjYl"5dhm avnk)LE΀LGw>PoG;%Ͼm>zNmtm y=h=yrا?#Q%34/dA~!I9E㾞ԉJ3'[ʿ]?_5KY!ç6ο6;n[)#?ʔGtmP;#>1mջQ+/)oO&+z.ǻc ҉' kgQg_Oub6|G'iS[Y!t<~>񙜖Guܴ4sͷ &$}JVZyآ-5'Osie*e:%?OnW  %#ǟGej_q6r u^9i˟n |ᅤ9Ąw+ YwݴV7{~x0@IDATa 8l9Ai.ƗH[N~$kȑ#'W^9MYdN9 g?_~Ğ;l[=L:%.7tt'qwۓӆhbt̀iXqT`J? ]P?WU>?+-O߾}jLUw[Wh8, E7ƤF1-!'DcO mȃ_sa @ixɷOFܓ&йldda]㤥k[!zز!>8`H acC@xvsW '옐DC/3KnBCld@㢠  P:e3tB]IL' :uO86 o^ޓHmް$q m" pc&~浦_ݡsxAǵB$W)l<7W&M|SZs2H-)Or uI:&[|?1Wb{iS . }١ji˴QMm?ݎrrHO81 80JH{|]\*}I'iu|rV[w1ct"L T_D9?ǧYD;UnI4:.ø7ϟڿZuXcvUu_?:|V{o1wX5E]W2˻ Q(t D]cWCKh l^:jQs1E[E ="`쉥 v yBA`{Im:݄t_w>F#Ҭg8Et 0]v$1gysY9sOJ5]=\P覙Oi tb؟C9nYe@~hb}( D|_eoY fqTbP>A!ߓ]a/46 gcMwPg;C҄Hu%} A/ \?I:_uRfE ?]tQV 0Lc2I$dJB x'%g[.8L6ƞCd{췴a9a 8}අn)?!~B䡙.aG9Rn]AB2;}иGtC/e,RZiiI*[>  YPgO{GߗZj'Lx]70^~'K{=TB`*qD#n`0XB(ı8MOhtt Ync|wnֳ?7z=m'΅ K0i3td7t;dY 8r8<'4|xp>6remd=?!6aB}q%<rt'vlqB/Sa?EypMs&IxmS .3)BnE|D̊ Nչ&n5j2ڛJ/LͽHiÇto:9e ɘ;2%MƄp@4=鑶vB[60Y ޔigM/0V -}WԦ}|n~JUQ]nJwhO0?1Owub+X^}H͐[ܷ!CCjb 0??Sޏ]yU uW`?t??;4}Zi՘@N_b~{W^ Ҿ10# Bƴ32@'NK!3D8tBskϸMAq=\N8"D|XrAvvl]N9b?<8|xqt Ȭ>-W&:P&eHX ˗4nY78]Xi)h6HZ;w:CȅMlqziT:mK|ihB7rkx yStJijlXD+\rW]q('|6wז!5$tWH=^ffd+hڍ7D+ckP~gϔʇxNBRbf-;Щ]kU[v;w.Yͺ[YO߫R=m4_?lҍYY!3DFyqpH9!h؄c?%ݍiA4t-ٿ@Z/do]BG?a`"#Do>8 ##L-'ȉEzQt g;xKe ;̓M_NzrЈ?6pƅFzх mq~4'r|&jNXA/{  B|-m ~&8Z5P,:L+QA( y𿂎h:^O瞭6#m,He8zҝwhe:?tg(moH<N ]u=OswaWuSmOO.wߺ?{/,-?YR%̟yLx NIL'N@XıA A$6MthNуW R8|x\<26mن!8Mq-_<ؿe?a. d;oN$0p r܉)>v(XB6쿙 vgYHz \O|v&NxtBcġ;}ȁCi?n8'Oj1I ʪZ$_ё#8+FwUW CWyn/e~ԄP4ZG=s8xu[i„2^i9"؏ό3DL9klG-.ŭbh+},]p~/-ϥy_L<@܋$XNa]?]׃S$ԫWOoK`BUƏ @c yp ⮇'g Mᡢh s7'cÄwEk} &41qEZZHcCniEon]3?2Y|9;vjjuW?nOeψYuS?u[?o1nܸY?L` R'n[? OwՋOX,@h1=U5d >Gh!24l1-+@C .cc%{fL+6<]uLÇ/xI: ̳|*lgYf٘Dʃjre:l?_['q˟n{uWu7ߺ?ygJy2h4'lp޿ի"ELxEh/cXWaBhc >ٞ헶Fi:vB0!q`}pw>@>|.o_M#im7*Qv؀n;ȃsu X0@IDAT_E?7 ɽKS:DRE}w_i6B忢k VW] E@Wݿ+*5@@Q@roH;_%a晧Ϝ:ϝ3'-FKdm j[,zc#`C7Y؄#.˘Gݶ9.t? }؞֮"?V:Kwlf?8ؓ/&D=Dhp78hCoL[R Qפ;K1/I OUjwΡ6h㰛# V:'},G?Znm[$l*  -./fݍ]Fq(х 7jiŽۧ}}E꡾8W_+2gr_8+^ןzS>̻gkqE~6bYJGH<7(,Gz{{ı< }Pt= 5l _!4Vg=*|$=?@ɘͷ:Oo_ O`1h*P2>ENNGݶ:`lY3FG =q':ƭKw ۔h7P2cȠo]9oޓ㉆^ 3P`HNV Dr*0LLlE4o (PD ͞^L -]q~r[gfC` rѢZⴠ4wfXȄ Ҷ?(?/mԄ4n!I1o<"fόJc䙈ʶ^2KkczuO V^7>Kz[?CK/4-Ԭu4c~LifL9x?3wx@L恿Y`I yԑy.&‡g)%aJ|E y a>e+[cx,N @gұ"-|ǃ 4ב 6Е) %vnSZ:<'@p7rq!gбOʲ3$,lec?m>ևݛ=,ZfI[B#W$Df`3F)33K/,O qQ?OIGR/Ow~2c=Ci jf,'͎YP*'Fߛ՛_v[s-/(t`OZ6iN*3NBf.|d{F$]WV\@wk=9T+u+z.NWϿ| =OqOhb+2j 73cDҸqHXWgEgӂ{fHPZ8\B7^ڶ}F$)it'kGp<2Q.`}|y :8[coqNJ`%  cvͳݲ#Ykk؂: 1T]d* :*a^ __='0ח^צ;\0޳ RX% Ei7Gr7&~*E9$4Nt鍯@ l֑Θ/~5^i?K>7yimMkߕ]Niۿ{GR]{V8+vof|b00I!LCF5E!:16?l2.I,I{Ƨ =36`Ap+J$areU E3yJQI@8PID`#n(Oͦ#@ZG 1yL^2L'Hwͼ+tAiM7+‹.Jӧ_gt)wg̘;{v޹ BKN7ts4qb:#"8sh!uiHϿJ4M>hÚ6m-ozZ9Wzo-^Fr3ft3d&KmV$ N<05s:Ա@],_,xoʸ;Cx)ѣmu17<6S4@)o} 6ؕ)"ʆ;xhlc˲B@H0P24 {/4h۴9tuGl9h#h"46`wumn#zȲcև~> ~ n %B$Upۘn>xC"L/ҕ_`/!#ʚOJ /ƀ/\0=Ҝ1rC8\1.$tj_W-hALVBF !)dXG;=CymCh]wljNv8-Y?YK sbɓ}/Ϙqm:3q:t-K[k?g-tiA#8"3vo%۟B 7rKۆإ-ۿ"?RYm-ǿ_s_=uDx RZ왇}`1u,dYxr~,Gt1O5@cWm(^2nx @4S7|8} ح  Ć;S&_'E,[fI.4#(ͧĿ7lYei > |e Z1,^,r~+r#I ϥalPMtWhQ^R5;d k,P:~մYmp&ḣ,hjƗh{ҎYg֒Ѽ֔a_E_t_M6wnwmo;:X;EJ\̐r,]ԪaCff̸N3daU3>+J6~'1hA2Yv_t>;tM!3)qđґ% ns9a5;'?U&&Mm Cm*ץirWk_m^Wzc;y%XVH?}8m$ab"J$6(|'N԰2+iS\k7pݶ]"c0M /\Ery@<$_<2 n۔el!O@}`cK Caѷ8@ts# `?.K;Fjz)o:%l9w*y}FlB6~( ȕR?8rnқ]b܁qUR8V[\H'l$Pz BASw0y))l\Kc> lB$G8)% hPN˞(8^g7ݒ&N+K$ZǺJ#i&؎b9~.:Pu㿞s ϝz_sf3 닯,Mxq[կ)1:3@IMJ۳M:-:# ؟eroq)ea]׻o:tFe6PƸe'Z4S·-ˊ6 ɜ <8؜A|h[d ]t6C;.q,uۅ>'cmE@I uKDe~:2L"S"g=iڭ3\| ֍ᵭя_#FFH9RJ(JPz̤UӛDmS֛eQ߳/Q^ϐVu;!ғhS?D׺뮛6&c M#o1Z`ZHw֬Yifl6iҚ33-Yܓ|rh/H_/{to-L;N Bt"&hOM{oY=mi6vD/ph )cH<`ZSiM6I;L!mXb #Ӝ9s҆:ͩFUw߽z L;C~+t^Y2y L7`[ouz3Nní*~űeNJo~ݴӭ- I*xXKhOoQZp}iSN\Z/p(?%)KgM%g/gh`>ȇNfš5W?PWFS2^zZsl !$_s]^>p<4I?x+Kic͒I|&'A( =۠4c::l!}D2qQYqdGi߀߲^[nڶt@~]"_s4tK[PKw(!e(nCKyl:QQڧ)'~$N .@-])yLy!gpH8:}_ ,=/Hfo2RB_׵>я'x""s=ӆmMs}"jSs{k&ͽ.;46 ਷5UnPqY̾[D[ƶB"͐jZCXZpScx3wnӬw=]w}ĜeS %+K+KzI%2^H8@SL36v0y~;͖?ᜠ'*!sGj8Oԧsղ^ 7Jw($t򕯤;T?@BZ}}>wrDwb/gcD?N:郱DIEﴈqs Z"fX-h_ZpwOT?ןzX3ÊuMڱ :G?l8*0;}'楉NLo| 8+E}'|)J"mY3RE}ex}淿IƿFv1lDzӋ_[oe+2J||V_Yb|a&$q>#{鐿+*Y&͈lҤ%]qŕ>>}n/}>Jh/~K,NHvSG|I^DBFߎΛ2Whfc #1ݱNt~9̨KWjͷ60}ⓟ8yϨCA+x |u`4G<ޏ_zOrl xɗ 7z)_EOtMz}v>a9ZB2)'AUY7sP2+SB,@F2,%|JSڷ?FeuHll>3+m:蕶JxrXVСOB;+`py`Ři8/vRw0q7veD l c9n?8 ]p  9K9U': @_  ̃+Sh 3ILB_t4 Qb1 *ilƋ 23m:ȣݚ?b+ώY+ 굥 vF?fĘ?]1-!jDm<[%Xcxsرy  jL'bbXe/{Y"ER"K/n-5S^Yz1ַi /|!ͼ)aw^g{׻!b敖xeI935{bV:D o #]W6,i`S鎻g3i[̈́՟7/}C'Eye/}Yo+pUzR.ߍ׿>^Ֆ3֔G롌.UѾk.Wl{ %Wr~?Mӟ징[Gt]w* S$.SNI{O?Y43Moys}/]vei#>?hz\~U3d F^Y?7m= '?CmrjٜW?׵/XVe_&ן?kSz^zߵ1gFT=1d)$GJ6!͇q7cszr [>ᓒcqJAr/J `As 8?'4pۇWƀ=q]ƺ2E #qTiHJ[!`4F.eX`ϲCwvCcg&g;sil!(} VݾiO͟?~f-B q5(P]I*ns >% #m@n?d%z^%fSX;F[Nȸ]*cԪxbneh|$-Эԇ}ݴ4nX͎Ѣ%WrO% ~dvOn&6بkھW*G>+-oI~^L'xy6ǞZ[g=CKyk$x5u՚O}F3K֩5sfN2K<{/yG,?}%g}-gK)0q#J6>s똠Y4JmCvf;ԇYFЧ4 {뺾DAZ BuR%Y;Ix,xзNRbXl%|GɔQZ}t 8%b=> y6pM-?CSYg6INA(nMD܌ )0 LWk>})132Wia 8CRx5:>hkZO хR&fȯ7Wu]!3&"?]T?/ˈ'|?_XO3`^% ^YbB6 '}T!23`=7^skhq?N$F>uڧ?y1+%֌ϴ㋨SBf=}Gf . 4Q?|gTgqҺ)XC|!$d0C_2ZwZ2؈slwC^sp$$&||zhb:Bq,G}J'BCٸ;Ir r2zeVE^:b=8&Sh:xeifp1 $DBfv9W_2o[w/~!tD8ژoH9?Z@um- ?Ut}[SSS>?+o2ǩ),n l$q B< mi&<6|:qA=G2nG &t/G>6J߮#6 Xv#6 Ymp_zWLͷQ SZ'Y[ICN=w \C82Dv{~HoY|n,nۮSl2:lǢɃܤe!p$)^f/Ebc$ JK!DѬ̃N-~ n¡oDZI?>{- QW2`F0j#/7L" FY Eh}#ʏ5%}aI }ڳR(eu%GI n}w>8f11Sy-%!w];NPBf*Nzԩ"epg+Kzh:X-ӫ=kMOZ3dC5<4>kVƯ;> BL=ajsTԺ2zeʫUbEߊq馛nl߫~o.p #ҹR {VN|i8+_~=hQCJ|S4̑rtwFg(!z?jԜaaݳzN ʹ$gQYgnZ?ע]o]B=bWiJM_Yz2֐4CfЇCB_W ͐9Z;:2ŧʗA4zA?Yoz^˥7^o5Xw1zg)ڸEIN h.=녧RIdvk;-oC~\7B[p,=_<<۷<ÏņBOJi9h6uP&`>܆LwYt;@cpԝx@uJ|?uw(8~HsÝKyp]GpL#R,GCxڶc֣@b 5Ca ^r! BIBX+dԅamPwa@Nf@(УD鷿q'H03F83E/Z1Cba ֠>GE*h,+rǥWoA:;o ]XJ\?x?e{ޯEn+Kg?/ iQ_͐?GOC+0T@S;.fs!3K3=\ Oޏ5^__oO}*,SN99=ZԠ>%{j|\ī7,N#893}_KO̙w }\͐kȰ#9Yԗ^ﳷ?ݓh˕X#o IJ'JG~xxDg!d)!oy(}M=b173yrW>ݮ9\͐ = q鋚!CǝD\f}V??WZ\u[?g↤k U|wˬg3z}^Gi#ؼdpt7; I cKxȔu  a(e{C(g@<X1uӐ8tꌩ/Jv)愥<!7^+Nj ;lYnXHs%[ K)r+·mOulO#wL#cIǿ-SG]C hOQ?0WZX@}"4D@3[HH@OR(gDF0ChyJ MXU{zP3d,k<5@σaΑ!ĒP^"c0?L: iM[=Ei:$aWh*ؕr$:4+O=TzfB+Ip)$dbJ_Dh~unt֓/Oe)ZpPtzW5d4c#V-nW_L~eEpe0͸fF:O >i wtzDN8^,p}H,M略ұ@WͯG7j>qEoL_SץE}ZCIW) .,}߾)%@ ׹ dZ+MlBXE>K9NjJc2^1v.m#OlCSBGyqBh棋^Yw7+SDc,_lذ>}7\h1Ϻ δ_ Ȕudmy 6<=K9n*鶃m)m=hӴDo K.,Q"`$th``p("(dBryV|aM5wcugVƃ E%H]ӣU#&`Վz?Om75cv E14 /w}jL%֐WhQCQZ ~W+KJ]wޥ9}^vzҤH{՗;+M7f}]h'tAk1SJs]8s ~|eק5I }:,tqSpFk~K_6l62睧5dgD?._(|_ Si„ z ֯?ZhzeH͐Qw]eW>Q^qǢ{M'OMo|cDNO-H??q]ќCa%q}y睴N|eiC>WdcA͐Oz_ʒ8wP,p֐ُE}&Z4#f ͐Q򪷏|j:_kۜ\2pvBr˻6ٌ[?S?W?/3dXm/9y]ĉOӡ99BG6.zG Pf{ؕcY;:n7vC\4sݱ60?ˆ8d 1 m.İM|9S)k%@i}ۧ@_u>.p@ DhDMDvj yN ?)3T>u]SR`uk6=Th'9H^(Hh"FDCXų0u:ɠ~o|ӄ-b|ZK.l +9b9sҀ4 mOi壎Ң$dp s9G}}]u}'% d}+K4C !K, +Kϕ/sb!gqJBMo _ bqڽ%Qib`zӛߤOD,ZJ,roo_x x@LT')N$;O3d4̈h^3+G Қ6mac޻?hvhѹurf̙DpI.t'}$ni_f#f9_tit,L~|e77}C ,m5d9|;! $qh?K>qzտ:z^,k8BSdeDXYG=Ϲ7ߟ[z|ei6A#2:4ƴuJy^Wo~!t?N_~y4 X9O2ff9xeXP-r rxeI hL,+O(P3Vr?nF-G5]Kӭ.:/nWcy{qiEN Kxh  $/[ne?oxCr̘W)g~G>"B>c:_pC^]m2f(]OR0va\|q|ܥw[yq8=PrD*$/NYQiNH߯O;p'L,XlwNԚ4oW\qEw_}6hO<2II8o5koy[Z$&%?Ws%ĞDp5Zh|pg-- &@IDAT_zϪj/wzߵ 滍_ąk( Q:"49rlNxXY)y8]oM)7Xى v؄<%t۠n=tmb۔؂ ;nШw 8)rSpw"u4`gys YqOJ5]=lS香O1PBw$b薵=-> /K|" E}RaAS`Qإs4Khs$Vr?أM`lvӧ$`ZĜ'ZSnCm/k\M6,\#T#Rc2@q._mUyn6Wz1iczOtOcm̐ 1dBT%cT؜xca'0NJ   Kd{췴a9d3m[c4%~%L!mdc7ؿifٶD`YPm G7:.*reGxǃ[z" :2e6z)KJYh ꦹo.HۇoKBWu |nyPW:Ͽr =. hE G MubC~GY[ӣ<o{'8%o+DLBJF ,Z{Z6vb*`=LG}d\GJ.ZL?] ^>chT683a???㯶?z7=PyjvPXY]L=8F|q!3EƓ7' Asq'h/2P,Cܛ"X req)/2tdjX5 ]dK}x6ۅ+RP·O4 D|p!$NŖrB/n>%rl:zT!<8F9Cl m=e 8,eb"%b _AMun)N<XFF8#h(M@( /d_#X?Pb}ٳHO}">y<_.\z3kѧǍԼuzOlaz6:2M{J֒!gB_44tCN=?ysb&L߸ Ӧ mif{l4!2-ϊQ* ݷjڿ*̣i'-N.3}khχ89&W9S~2=[jaޝwEG3g3_} 3Wa5^oƭ'~:?,i W<&g<PKt1th˗863&&:4K5 FƱ CpLȱ-Xw2s)tI@xyaw)  a`ccltpbϲz)O y$NxntJcԡ;>ێO;EzeׁWtdHT! kfj 2-W|q(Pi1s2z&iAI'bjIHnJ̠&e0'VFv_=_sWtkH/3N?2q:1.tJ3t,ȓ񘴴|O)!6tƹ؏'41D?m8z5.`٧ie)V!ensYG>#)Ah;AKiփp|JćyAs)F'Җ-lRBÆ `y2M#i9LeĮTe 2(!C7:}O8BWѵ,1TvM2%I DCKT]BnP~~B.Ad!l)-yu͵Wۃ??O}~m<֘W8Y8cl]:Y54 >M.upxQ+7(`McRμ4Jx26^1fXv 1 ̳t-S;Xp⎢Ɓa)˺(i8 8!Ḑsr4m-neg@BЁI$'dJ>2ԑ'8tlK!cHȌ3&-|J瓣B 4W$JaN34P4SԤC#Hzx=C04H&ͬLh)}"hW"$rk$Fj̟E uѪlCmu=Pz?_=wdq5dXXmt΀ %NdagYt[zs EYXf8ul1Ҳ}rv rl)R8Jb=.m 6 m:6qac:%,UxY`9Hvxطym C: t_X6EYc^"]]hm]m4qC" nǿ4 v -KIÑAmQСa9ө;q!б%| pۀGfC؀Wey[ز%2$l :` QXV xmY‘nL<͈%`Y͆?%Q/BSv?N_q[x??ۮ|dcWiϙqg}Pgclȇq*82%|ƙ"X}tG >}Ʊȳ!ck#k@f?AЏy:%\?SA\mp J}ۃg_f韺X_`l;F w6 d ,c4yA:%۞ugs{'zlrggl_eXqY86zOt[:=)nxw.!O,p&tG6%4 ]Z(2C;po{nN@)G޾WZб= O`æuó -A?%|@i_evAߺs޼' fzBCHΈkE)94IX0I:GǥKpן暫k,izqR?-G}ϟ?8 Fu%ޔ?~ @? y"5;.lnybChunO և,:<td5SZx!c ԁupYy[v<8xtM3v[\ Nh;ǁm۾$H6ۅv|ؖg8,_ 4t 6?зxP@F^%*>i _b CĆ:*ae(ۤhajտ:D@S?'WϿzzq}.¸oo}_3n [w:٣Xk( -em$>x˯) :БB|xerq,uۥ uBV&^NxTi[б9 ^\"S8r؂mxP7;딖s#4uR.ص8|I :>qw{%: w$ۅ<~XIȐ Y+KDV$Ysp g&2Xun&71V,p5{Fz/΃zkе!cR?:/W|n.V>3$iSgsBxv<5*}|ei6$BC=^Z! Bcf`1{6+K=[g&`xc9S|JoFqpr%Yh7leneP%66MO:)bٲ.v4Nr >~9m؆Gi>%`r,3Mk7_Y5C@,E)٠5)# U79(h Ѯ.Z_##9|:氩_=^AJ![?΃椨ϟb{䉒2u/g|mw@s60\Il47%N0ve.%>2=1c\ :JmߴRѥD.mS1vUC:J. 6$'S{0xiu-PG؎ v]vm%6Q/MDsׇ_hءriCi@:7pEބ;Μ9sy2L2@ppâMP)Ą%==-(P'T 4؎"B=W?$rm\.T?`o}Pg}㏑RvaG!x$qmи5%Ƙ^g>xhPG:4lRڞmnQYqX{গroq)ea=ŎٮJ MKeٲ|( ƒNv=7.@9Q· ˊ6 ɜ u) طDŽ,`?CC$SON ?qc+t{eaT j[. +VPrYX$-GږJ'h!>2Զ\V:IE@ 񑡶岰I*jZ -NRQ+B|dm,tZ#CmeaT j[. +VPrYX$-GږJ'h!>2Զ\V:IE@ 񑡶岰I*jZ -NRQ+B|dm,tZ#CmeaT j[. +VPrYX$-GږJ'h!>2Զ\V:IE@ 񑡶岰I*jZ -NRQ+B|dm,tZ#CmeaT j[. +VPrYX$-GږJ'h!Lkxe8m1P'W4cɘ=۠4c::l!}D2 ^ykjm!zOIq /J}pcw聳nץH\w 薶`0 KwJݸVԷ |-M'*J4:%QmtN@iDmO7RBxYG;:4e SGLW @*Z$! Q:X )8A/-"!J_EB~):E[zw^s|uvv!N fH=W?CgD'yQ+%nRp^0[EZkCGdCB N fHkmC㈢l]H zli QzhQ )8A/-"!JW#R!w!'E[6DqDQJ6.`hֆ(]=4(JɆ܅mE)ِ٢-Zt8(%rRp^0[EZkCGdCB N fHkmC㈢l]H S fT5˚:4ԝlq BY/m~i1P9fl@?ޢkz[6Aw)4J6z ǿcp'RKQhntS;nd %T?윣Lxlĭi'zPg M>[o)QC= םodɲ(qK_oA+Doq.Y%nz]xMKL=Wiz'B1"c#ҧ><ۀX.ײ,4d ơ_rzy mq5|p g ?l96`#:8 `7) Ѝ4]6R܍-Jw4t6dJ-G?|6P eۣAl" )xTE HD)W*^ E!Wzg7{vM Ww'33;3Y)bbiDF@#TMDvOw:+dPZ`Јb.iܠXuEQ!^mʆ]6_wf;Vڿt_r+žڠxw_n U=Q%r _өx=nxߊr|Mx};QKjVU&&2tƕc2-=(&)6G!# ]\r(7SSAcEtRDVN‡8/pV%d$r%TeUhqӅeW,@-9ʃ&A [#ehg_ ƐrAM(d8Б =e!tU3lps(c6ybP}B p>_!qku_yD5 tϟU|;Fo_&ca?jBf]MƘt}0cIoy14flKp@r)&:H/[\m !rhq2a͸_:#V|t ăWND WiB:"D4 ÏFP 5S%++_|vx0FGF?M?;őlsVg#iBfZ&b=?/VtWJ]~k=5rߨi6reXv?:GO?s4lb5Lw=_Wl2Nz[Ƒ9<7&i4y?\FǗl@X*46s0vzBC~O+9G(b8m?@2;$4+P!eK@*@뀗-X)uQ4 .BNjsL94`bPŕ>{g ,;bh\CYOvA1sP&+CL&dR{gzڤDZiO*rF=aZ60QԵCC(zt-AuOQDQ]Cԣk Z{"u\Y-[QDQ?[oU>ˍR'O{vs?tG͍R_ ;KcL6FU/TqyP|Ł v`/r,{t/{GeK Ít=XqSv*bJ}!g =(I£&r@I*lDžăɥjPhj"bNjlbF*(?dCeRLv ;lA'_#K=lTb &m?¦e/76!FjiSㄳS?4JMv_/T=hy񛬾B~jJ~{T9ߟO??ZW~p?Ϩ'e̟ڊXXQuqVD^ȋƗ0.U5%c"7MPuy194b6^ukٍRM P[pi0m>S1':?ߟ?pʭm?3?Շxۄ>{ی0S 'Cs+ rq. d4AΡ'c܀⊦,q-[aA#h@3WP\pz#kNS0ق:(4XYxUb`#`W>rL\ѡc &?ȏƆL蠱'. ^9Uxb/Z񅙐1 %YPJRcmh6 {:lkzLOO;Jl')W u?{ y7J\>!ԏտjB&㞱&,D#ׄt`M9`ŀ:ք84X5DCYʚXrTf)qɣ^x*)bUn0y\qUO#]f$#)v1)@r5*IaN'[rSN좏J)<`&SINe#W9U,FbFXDzპl Çkk@+ J`` CCN0טla2'J/>/W^Q8ssϝ1"oNo{[|ϟ\pAQʟ\9]eUi6F ԵsoX5oZ?_=}o7d˘Uoc Vl[1# "`ƻ`tx0u@`hR_h]| {al_g1dhXF[`t``IneOll#:C _280DH = TUƦ_ac5r!t+?[I6ث|8 +rLG[&K/"1%F/&]⣗ ztgDp1hH6E' [ϧ?=#GM3̒Vpʹ}o"tVm&o.f2]zWZxӘ1 r 0 Qb4e',コBjᖺb6m*NBaR mSTq ˸ଐZؠ lPXg-um eU2.8+n+6h)ۦ$qY!pK]AH6E' [ @ʶ)8 e\pVH-RWlRMQI(,コBjᖺb6m*NBaR mSTq ˸ଐZؠ lPXg-um eU2.8+n+6h)ۦ$qY!pK]AH6E' [ @ʶ)8 e\pVH-RWlRMQI(,コBjᖺb6m*NBaR mSTq ˸ଐZؠ lPXg-um eU2.8+n+6h)ۦ$qY!pK]AH6E' [ @ʶ)8 e\pVH-RWlRMQI(,コBjᖺb6m*NBaEqgu#:hL+V(' Јr|-Z@c|蘋S>&2ld zMzɱFƪr}&L(0;U zt`c{Xcݟr(?r@ĠȒ_[Ł_ʎ<6IXȕQlEF,h*;_Q"?i|NcB :ĄJ`dmIJ\l2a)2t=)SXYYFnzLf2{ r4;cgb|FZ$M# ArBvl2a-Bڥdh^M[R!r P %BxCjڒ/.-hմ%5_(CfnWӖ|Dw?d &z5mIJٻhմ%5_(]n^M[R!١g܊PKCxX#vđ qFWvʃCӎ* =:8C'b@uPv J*X蠱&r0 )[@20 hS>`bK'E/ \jB:Q`( Z[-u\6H.tǝwzVØԝR`W'Kn\la6bȍ`i %\2-+"qUu xץDS.?M[Z@s7}sϧ7|@=Sin$ mZj]'4BppJwH w_VآupX20UiX! "Q(@LXNluȞjAyhH!1}c嗼`73ɰ!|_u,+|t %S~ٺA<0"MCJuc 6*T |lNJ*⃱C8:1tlTx0A.rbe@2P?^q(4zF:/?x|3F8O,rb+K ʙֆRV ͯ䖣X[H*W?&v[f`yld-wlep8(\.Xkļ!dXheK|;ϜD[H*=,tYTUC^_hG^.?|j;V[-]k3fW^%mi/~1a{ws In}O׻N{S IG/!~tcxIs1&dzHcr<44"MdG _4z@X~@r]5H,4ztC:2Ml)aLq`UY/ѫ/t  VQv!)< O.H \  ?qhX0ߖcC\V2ʆz0\gƒLT?`|U>접+ꣲz {|e a6`}C0 PCxbY*07tS;8? rEGZi &j B[W ϐVobx~ yʿRKW\I!י-L!YVE$i܇ SAh SqN+\LePM.L!}8Y*p1A40hdTE$i܇?)mF񩏦ǮN=rjc~ӟ5H7{Al?餓uB|a !!,W8T,WʠDC]B4pT^b*"h ua 4Ry I4ԅ)DӸ'K.2(&PM>,WʠDC]B4pT^b*"h ua 4Ry I4ԅ)DӸ'K.2(&PM>,WʠDC]B4pT^b*"h ua 4Ry I4ԅ)DӸ'K.2(&PM>,WʠDC]B4pT^b*"h ua 4Ry I4ԅ)DӸ'K.2(&PM>\֦ ~#f1E=4=cNM $ LhLc`^ɩFz|024&ʁq.9ʣr)?q &2(B+?rA+C<@SN87-(*%p=z*Ǥ DSvC(`rH/2a#K`ԛ+ʉ.2/AL02bS(/8UlBfx?&ųRU3d4l] nz6}詧J7(&O%]'1U3n̰IJ `>)cJ\'<¥߾@ZmUu6d}xj@IDATC*_ǭc9esOc==Lk>Pxe#3?G闿eܬk#B.v113.GjZu Y/6,ߦ߹P J0Fu,6/8?>w G2,k`3NmQC餓OJ_s(2?s~ŗ^^j:eА:nN|f@o}緫ʹoϨwgAOt^uۨ][Nzv3Ueumkfei3X,[zaMzhU 7zF.ɉ%#ch'] _QOMND^Ij(bVdK>'t:M"iB&걁ǎʁN|| 2 3U`WB]eAq}_y EZgvV$?D\'dU5'̇g=3@at/go~{\sʹEe. B4ŘTp֧'ONlRkN9WNN{?K 'i 5tbDmw5צy:;ψuIY'u/Fyrt͘p4%pȽK.b=*-rJE 1d8tA{Fլkl . ~=/pbZ^nᆴg?y 7HwBui=9ܴՄ%lZ/ ,Gnvwx[^^g[*Yr]~k{4|% Y?k k B4DhJv,Ek&dؔw;0v4izFPŗ[]vȱCM[/Xt]51Ł kLc`č>ȕ_1QyC56G6*x0 ^zMl$(v*$Vx $*_d ?@&Dv,`l?Npnؓ GC+IyQ^dG&Lqb|+#;usa3yq> Q醑3Y۶;o~y^´OŜ"o5xQ:l{K~}(u$QY;svۭ|ƋH-)*kgn#ۮۭ|ƋH-)*kgn#ۮۭ|ƋH-)*kgn#\X`X$&X%<:ƚũ& fHdOcsJNh9g, 4'@~Cc/^eTr{l 9d|*v^~*툁\q086IAd*lc"!6%-ze#b!#?vŲ/rhd(^T3U|᣸سG>m*eG+<#ojt٦E?P ,;wK'bUϧn:RmR'X9҃?hvuBi_\3NJn/ i/=Dd,D.j9%@ M V.vdkMoۨ? &UjJRLȰȑ_j8 &MJo?|Bт6Mi_O>ŸY^CɣsӦɕ@FoYJޛX_| -?;И|'Nk4d[V{)*pS 7V쵷1`2B}_k}MО^zi5 Z]OUOJG_'\%4?tbO׼]]ck&^ ?_xnﻦoj_W#4-< 4 CG&O#A ]1?ĈqH/5˦2` -&Kd1(lh*Sg&(XSZs$(pȱsб` )[0 F#bL2b\kRl!=:b)= \p@F~bCcZ@%{Fuņ 9M`oW演Dv4ՋZ(MN1 V[hq}z | ̺ Ü}`i]3n)/9x B Y 9U]71is[ljz)i71-{ӼΓwgLKos4|QXuE(&{9{p=ԴbKاY*xI] ֛ҨQ#o꛿.`z饗Ӄ>Z܅\c鶋֦.i#?~aife綾^J/rz +6dtygxcVǥJ)S*|ȗj;B,2 .h9VX_d?᏾~ĈiZOw4{;C{e_Yj4?{4ftni6iE?MouǫV=`Poz4b?/j_ˮvT%h!EMnP_GkiQ QE?p8tx]P]+QZTӢ Ø=sIxfQ7M[Tbsϛ;P3d, shq046=?RH/c|F`=r@Kr+Ly\`''/<[d⡑`0U@WA'x)'~v*3mz33ӟaU<_/>69pïgm gWMꍋ.&j&MMy){ӗ_]W]R/=c;'Ʈ=6*(-c~bwꫭ>Gvxz_?xj,bYqi喳~yrt&crz{gO[Y;14>~7NJwmJ~ӡ_>&Xަ"WMjQ_MlI#vMd^&ǟpb:QW[-+&U&nЕ^;5_Uf~u# ?4([ٜbˢ 2)=:n꫱+3Ϥv=]ςZ%t]žB^Y;'F~niMeT"gW:2=`bjMo?nI7#*yET 3gyeeCnZN7bsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtӥbsq1BVYVghtE6d2ac^&;)lOC+6vy}!r`K~qʉh7:05 yYS W.lINb@A ^T9tjN`*+b W+0[@4rbʧG'^#)*0z +zR\lSf̘> ?ݪ&f%5k۸CLs=ԓRۤQ"# zij󗔘_JajHÈM Jdna=VNrOf6K 7Xht}_si?MsQ_|?ßx ;r =dlO Ӷ&jq;m_ifޜg6)A\9҇ȕZ؀vX {QkOlsoE+_Π}y-l7Wm=`-l :%f#ps{7?_/-vh@ܩ#L:xK'_SdrXN>Ã0?@r`@c SUXe-c嘃(_rj,!Љʏ 2 f 1J.9@N:i  N>j<3}=F4ɣ9 tuF1QF!?d긙ߚP6h#? ݤ+?k S%;Vz\k_:nE9v՝UVY6}:-oM={iZkIԗmY;85hl{on%}K?Ww!WxhP-3ʿC/7`zPI6 gWYXGI +Gt[!q~}ZG'r2ymVlVZi%{Ue޴kCZ׷~sS:3ӯlO}iQiү'v%9Vv&[]tmZxEt}K-~9"VwLOSO;ͬm#l"r S;>=}QGH?py9RuYN׿^~%ljtAm k40GscS}% &6tXN}`_j!1ְ66e[sWsX|#X,V!_Y2uY,ӾퟶcB*Ҿ;^)cCNL~}w!푶*꘯[v̳I'X;묳=?`C33ѷ~dlj)_?fh]ҷrߟIJ Q_ZDItߵ?ѬOR>]S~"zL{ LlU&c],V"`ىf{nt0r1>#?6Q&]TԓyrH  ZP G2^<:+rSʊmȸGQ A* yhƑqXģCFlMhp<* _rp(.:8CVY OLx|r2l9m>`'5 9tP T㬉BBYpr <|RVrTuÜ0  zKVZ.+!Y p2ڴ%-yeCfcLui1iO}ƾWmj_WPm@͗>@گWVzpC}K,.2kz5iԴ+s=K}fѶ\ od_Yڂt‰$ֆ^L0=Vnk+8'|Rzw'c@:5X=;6=l{pُ$~99أO?~+d%6{Fe&~&}^!R^A6Yp?0t)N4/}f.y۞r6=t|2o1)M &n=lr&f0~s[tءV!mNUַWI 69ѥsF$ɐ>gO<3G9ӗ9&뤺t}v?J,}҇}WȘÔl匭lcbR[4M(/Bzm gpE*ݼϿ诖c} i?+PLfVs}6] F]]l2z=n68{G *L&0hگЅ '3p:m!dNf3 .NsKsD} ! p2󜁮s;t'B5Κ/t!dNf3](aP& ]Y t/?y^ok4\ ~1IϥcK}dهd+.F:-^dWDЗM7b |y`+B̔k/#u7`=@O6Yk1& 3#2KMO+{o*;%ɷV,Yof+SXmc}oLo=4vۧ6?jyu@:6M?a|NiϿѶїm:naJKԧmվ03K,Ge\S7Z Wo6`LE~a~cƌNW_}uJ_=;bjaZV\d+g#}&&;SΨ}&!p]3k+kԚ=д}݈†(IMmI˯۬e+N3)]c@?4L`3G{ؖa+gwL}M%wt';0&E+{F>b??Fp1MW K> U`LnV`r1"TUJUժ.S"`E>,UuN{d>نj\ l"l8yVj ؀91~ַmR|cنumSfk{ت >f[ƿjG2 g){i~ی&|я?6^ r29&r ,Ur_mc[VB0ɡrU/}@l5kkz,|Q {\{GN_(6Ͼ  hX{=iݏ~cz6lʩ#?] -TbWc;//-Mou0VOW^^oZvb7BV|׈0ى/?UM??lP[[^ImZׂ k.s(yS߽]3&[ 2ZU4^Q҄ 'h{AOލ9.@EǶBfI_!3h<զv-{i2RۼBV۪+꿁Y^'wCf|̘muU`VdS~_aA.׺j۹G~4 !d!܎ _jmutӷmܶڿE5n%]&bQ:ѵvM:%]qvhO'gm,[~0  MmS`xi܄2uM2gis.'hɗ#q/] |^9hk){mhm^?4k˧K Fyj&f+dvM;30&dذ m22&ye]n=>!X!?qɲs9רlkqv|R6NfRMowx}t_y ++\>!#sk~mm>tMTFiD'?Zr% {eio{e`e:c1^Y`D'wޑF$#\rMX~ؼ"jr*E1/] /MT7}~;C64=عUоb\!- T;l/^I1 3RkaA|1UJqMQ6]*6I/~vo6_zb\f=쎿zb&c8^4x;;2Ll^']9]GFv`xtF1<t*>C6ey|Ѐ|/S> ֫: Zv<>UDqTh1`AzW1uaG{S|Ԩ翊Ho@0SLS(>&7@~hEy:tZ8啥Xj2L :nJ.+Pl5>[mkc'&V8<+;h<2Tg]Li*U`>wv[9  %.!JAcN572%|oG3gSk&]bVIO=TZK5l6l7vgeӴm*;[KosRqo+dnyef[5ȨѶ^xd+d|+,0!7؄~XU${_k![!09̏֨WV_}uov=s~eU6 w}^`x]kؾ/l'&w~l{[E30{2~sͥ/>4?Gۤs̱Ͽ=%-' >|B=djC?ɏ%OÎiOLu.z缚ƫnLM}lVߠ}ZhCT?>{簹&Z;.k<`2Zs{esզؗ;^;2VO1|ι?L[_jeX&dP-l{ri`?,3O?8l{EK4zb5ۻ.o/~ s lz~oU?xrZkۧO;Q :愨yW^_66?M |Vסkڤ~\tϿ_KrlkI&ݑ%&>3@Ù##&XSrEΡ<뱃 1.4rlCSG;ht,Zy-2*Q>y-Pnxhŏr:&@z!2llD_ 0zŒӎ?Á8>:&&g%L#=2d )&VA{㣲@Uipkpd_k8($Ǡ,* lFqq :GQ|fETf%[ 7ǝw'y Vn첳 MlsW@kNeڦ'Lp y%Ak\[ַu:v}Z۫ftR+d|B5߳LUVLXҼ3`$*|!{Am)lZ1WSm02!:, } L21{eA>E} 7+I|ԟ.7ي"^򉱞 Y0&B~ls}?|b2V$1mL[o!?){o^9<8{okFUW|O zMpgmO/8]w7rfXل ,Y]ƍ>x/oWd1T3n+a.^A{k1sN:)$W-ɢY1\1j42=X!JS@#K8Di`>{b (MQgT,) \ 쑊%4E d0=RC(p G*p.|HQ2X!JS@#K8Di`>{b (MQgT,) \ 쑊%4E d0=RC(p G*p.|HQ2X!JS@s-k.?ox`/&b4j,@O9ƴ)XǘU<>ar c+?2@`x7ZVU^#K=$WlWOd ȨZzUQᑑ@ /lQǏDEO*)':@rhrKNŔIFؑ:R|Emq݈s+Kg+<n@ ZY`%ȓ-&q;&Bj +%H.L<'C}rwτ2^uy VɽxU2QfBec_!ҾjB&k7h##WMW.w+m `ҦV9&dL~tlbgV9Bv[[i>~15 >Y5u+o<Үd_ncƌd` YgZ{,{}Yg9Mzk_32+J|eMlSzo->?HLC:(a1iCB?A0%<( Yo5ZV>"׸<( Y.5}n&]p@ѲAYB"4Ѝ=˃Z{i9tt Yuvل +d :a:FXVzhkL6F/<^?V~*L)2 _#-ʏ\H`j =&Vblh  (@J P(S҉VÚ+J|b!+8ɯ2e+\e4s*? 9@D)@#4D `DPK~Mmn i'52"Gx0³ܠyREo1l0/Y6b ӧQR3#_ɲ5+Vo/Aʿ3ψį_IyXѫl,ml+d>6!y[!ҏz _9䐒f^Y;`{L0כΰVC3Qvj#1b^Gvs^Kgyӭ,Mי6l3xadBRjhg3Ւ{Z7/d_پíZC!+KՄfS_^Yf 6Ѡ`̏lr]5,r.?zmwm˾/|Mygqˎϔ&͈ۧFUKo+'g=γWVXa>}Z[̯zuj KlCY7hYg:KgJ9u[4i${̮iL;zʒYWm yW`Bf;ŬUV2j 3&ѷFTgT{ן^M}7nz౶90;}ZV_g&؀in-뀽ta< >uD-G|n?^ִv #--Eeꟿ[hiYv_߿z՚8ZDZ]w#Ԏ:M8ZDZ]_C2EU^=+ `(!9 Ì5&GX:|):@v̈́ [h1hm"%^~u00F9UbD9?%S=%7ixꯟ##LrP~h∇&@7p4l(*;)*)Q%KcJJԞS~UY\*3v_1xhdbG'&:h1ʢ臏#>?鬐AjyB#lR l7(Vf]ц~o45CzJ<"ghT_I$)+p伪d UEFJӕW^}V0ɪ,5y_ C YlבVZiE{-yU6%x_I8f65&Lpݴilb[o J?^|+[is)y &(-ޒVOi]yG$6serbyذuV( s=W=zI<\kMsn}.mn6e & ,>|kM^bB5>Yc26!ӆwV\6V6#+dֵW,%VN ;Pmra1騣Ln>xMĜ}Hk'wľZaZjIZ~ޮݤLOe/c1ѥoWDO;fiA"+WܑkUq^B!86FA6im*%0-}9H'68:k2,zm_zJ_xYgnW\yxռʬ+ A^3o#T8*Vr,.{s6_oΥ.Bf/ۈgQZYKZr*_?v[<|.ݫ{NZ!0V` q%zLd FFȕ=49K :d) C2a{5*Ep ȧ0aD-R!W٦ *xm_Δg>k7b)a'&ͰeF^-Hz&y'ƺ4@Q,3oLʒbdf,҅*V 3VTk9eui2,9)>Es}Ҏoifxߐt皰?ɰv:_>mJ9L }xeiʔrVW>{]=f+dV2PgJE؄cOYu'+Kf{LT?86,ίyӗ|M؜/J͕Hhug ss}.SUo蜿uԑ6"S wj!Ís,7 ntM}h=o}NEKi_i8\rŶ15{ e}pr I_z:־"v~i}[}TCn >}?P l2Oe+5a1/[D_B0]w`o+lwm{Vk_cՍ9f?󧚐g,cL`ƒRzdw36G H2SNl `, q%Wb|l]%{94C .C&L @c\N/N+>X+'~\`r4!z Pav=.*XY'[tqY. 4r*q9)u@IDAT:@6G~²rt*;_wɣBEr&b= L#gEdl:1'uɪJ3wVUWXN1O;",b/n"ѓ~n>#eDIU=A۔ߠFyMpjp?P9=d&L0}}Dtx)[5dbxHd1yQx[- <0k_Quwܱ>; jEi|Bk\ǧ+V?}dn ˧ 7 m%FcDzBfJZuhOLc.W߆}^wbu sZ]5W_ |! x]Vǯ7<뮾%Kϱ&ML.UW_-n 'CVu8sך;ba0_~PTo9`p ~U,&"vu^4W}MJq]d-uqWc_QFĉL8?-ث`oDW%gQ y={ϋaΛkea{Uh>;[y|ySZqx˹j@h^`%M8`ϼ֌p"CM]#Yl?ߏOi_ WJ]ojd2MA=}'6s?<XߦGS* ,\) ˸kv`ٶ A&&:Q3k$ ~~@?z.숧\FǗql@X<Ɔ`~lTOs/1 |@+;;x/CNRBxق*XTZ-tx1|Ae@`bPŕK\]9/W6eM,b+x!13|uՋv]F7ףɲ̃V {ߝkY^=W?s'ߝFGu~y?׆Ə2|/_@v J>WX.X*"L1?->|iOQi)^xtaO\A{;ﰾ;ʾ 70a[_|9`}JNW?|~u\ &hIEuM,7`8Z.룈g~pѵ-=]GE]_w?^gW&pֶWGo<{-AM(*K5!3xƬ g:Yd@h|?hƨF+.>ҁc"W=s9u VeP.4jX`@mA)?}&^A  $\Sɮ(JEx@#WHc#[06Z%DlE}7㼝xK!U f:(ՓՎ|/|2.@ ~GťɠsNwE}p Ӊ'/m/8C8_S&o8F Ht\:D j3" [f3oq)s~؝2n4}鑠<5'FJ虉5=UyT< | P@gߴ}ã1/ЎcYaگ,\|<;ױϏx9wc䖑m\ůnG;9R)! dİ hwȠmc}=yl{ 1hz46ĩuvad2HJ ɇ__i0:0NԒx8FGi_Ě2?qKA,tӴ5G_V2;қ'4ۅf]qq{l c;t`8̙5=1}z,[bo}?;fo׆UIĕɎ? P:f3[yP`ȟ)0Ӎο]m8 H->AA;[T#>0=zCx$*rhO{A1(~vP}sr~Os>^C's+A's{ ? gi(Lw7 W_ʆO'(I;T]FQy]J :_nY3eG9MA~e]yL#G )ak9'*tl<[% p< R#@< 4y6aF1O[vK,ވ~ ~TNtpI=K$8;1zl9}r|ٰ`|tˬwNbzȰs,;m'?[jLNlC20Bh(|#)<3WWE kJ&ayFsΛoyהzq7х60*λk-%iԌD3 M Hm6iE 9|5מPjΠUZ3 p?G[NPn:zdji4y0 -P~"hyϿ8 c. "C:{E |x ec~~v:1s0A;0:0y0,Y/zbccӵ=-Ïm `nPDpkc|G5z˾xhگE{Լc ymKj[du xˌ[ =:U8^ w5.ATm-bMqV|C.Xi˸*i$sy/)U)V_mi1矟-q4)ş |˯)V{FnXiOX%S&NaU Vc -!iۮ,4qYaqOݰ"m56.8+6Vmg=uVڊmWp۸ରڸnJ[BcV [i+Ҷ]QYhlコj㞺a+mEڶ+j8 m\pVXmS7lHvE g {ꆭiۮ,4qYaqOݰ"m56.8+6Vmg=uVڊmWp۸ରڸnJ[BcV [i+Ҷ]QYhlコj㞺a+mEڶ+j8 m\pVXmS7lHvE g {ꆭiۮ,4qYaqOݰ"m56.8+6Vmg=uVڊmWp۸ରڸnJ[BcV [i+Ҷ]QYhlコj㞺a+mEڶ+j8 m\pVXmS7lHvEk_;r SңYlD{)ƢD@DƌECiOb'^Bsy*\wMuc<l6G~F(YIF Y|2o r!ky_2a\l2DM_2aYdrÉ e¸Nأ5}Iʄq/gf 'j/ :a&N%-_(ƽ5M&7KZP&=:Ln8Qӗ|Lrl6p/iB0l2DM_2aYdrÉ e¸Nأ5}Iʄq/gf 'j/ :a&N%-_(ƽ5M&7KZP&=:Ln8Qӗ|Lrl6p/iB0l2DM_2aYdrÉ e¸Nأ5}Iʄq/gf 'j/ :a&N%-_(ƽ5M&7KZP&=:Ln8Qӗ|Lrҹ睧=`[mV-Ř(}2mB0v86s ;hs3-G~>rx4{6;GN2b"yu>ۡg3}0:b`G_v8.slqtO~t+hxL '"F1l_wҴmopGm(dHڗ8M؀m^NTغm`-Ȉ ?6UҌ+K&g!un0Qȶ_]DPlM٦) `ϖҥ^nqЈd`*̚XwN8?\M)_Y2Q4r\r預CE.@nKWLMX&u9ƭ1Ȋξh(\ec PmlkIP|(\efsa8ʥWJNgi_A2 p\r) _3NҬkEDZ:`  ']wusP1͌j ͌Y1♰+ WYX1Cfgm+{[?L2?]Y*!#$[n^SZU$3>hק\a`jyi.Y<<-kl<8 i4o`ѱ3ib1;mŽ #|A l'7 8xrAX}96%m-my L~ p>s;#wF8F~2h/%y{c3`@I.L! /LW]}u Wަb̺1yC ɱ{K/iI'Sa ѱ8zɯQC!Hم%BUB̜ 6r* 37F˴[q7nLs;aK?$)Dk8 eK5.6(.Q]-׸ڠDG]BtGpt^bj"ua 5y Ktԅ)DxgK5.6(.Q]-׸ڠDG]BtGpt^bj"ua 5y Ktԅ)DxgK5.6(.Q]-׸ڠDG]BtGpt^bj"ua 5y Ktԅ)DxgK5.6(.Q]-׸ڠDG]BtGpt^bj"ua 5y Ktԅ)DxgK5.6(.Q]-׸ڠDG]BtGpt^bj"ua 5y Ktԅ)DxgK5.6(.Q]-׸ڠDG]BtGpt^bj"ua W1ה1[hf̪13yK6TDG]BԦ#R%^ԗ' "y~@ 9;:紬k㎢C`w3McQ2i~փLeCf,FiXbێ##O r 2S@6N4NlS5tn}]~/Kݏ51̀ 9buO],mmRӦ̻ٳg \L e4x}?A>bOC0'1 (@3IJUm0gPL fH4E_IAc@*"\vщ?&%pSmA hG\GeWL̸}F:Ց\7Q Di?xhT((?pxMM`(D1fVƺ^OiwGGeӬq?:Hc O ^&-iYK(3%-+Lzi뭷ֻ 95E|4ion|?Wee\aWs4dɹ*+ʮk_׳.z`3*sUV֕]! v5707\qOo e0sj*B"jn_1wn7PS)y߿_I]<;.2Դg`dž Opŗ1!Y6}ybqp|p=C/9bCL]ĭ};c:۸y6U's# mx(26|"#~;MIXb#]p߰'{}59j+*2fJeVJB~ŘqٳTO3;=4Hi S5U) :LZѵVH .`aӂ -Tq&*jT9~ݩ4Ɍ8Ѣ ϖ8IQ&YE#ԕmT#Ⱦu#Ԣ2knz\jd*knc|kƿZ T-bPM1p εHv61xۑ4s0 ~բnGTƶG+WjISY;wG ^-g3civGSnĨBJ:vc EXD%<:5y' BHpvusZNhY &ur3JvFb4(IAkx xZl%6h^_:/f\+_ EɅNFYǂ*gŌX4{CK]- ܛǠ&^hr-9{^^O-B*,2b,a4Gy_rtŸCmГ8tڭ=C8*D=`8to-wV׵ru3O]Lo/ɮٵr׎_ss~| IZVV!5uٵe53%BSEجuxM#G_[Tla3y|A:.6v`mnh@VuڛTǓ ֕INf9|8؂xa4 i[0#cx$ 1.|]Yڃ 1GG,w d'64vͻoO tul/qM,lWl(8_<' 7E+4͋fgMN1X7"Z( -c1|2kc:0,΢Ij@ŦNUQZ%d [*a"dI.1fDAfx6Yi:3ӟC{:g%LiElf̰kxvLԟ9V0/I[hHǰb{rL{е?g3J#In!s=ieMӧO/BGc^z˯r O -me{%O^C׽ Ao\Sw^;?z1C,+uCz[!r7m4Hgs`hl0z3=SEd~s&?s2Z+N -Y2Z /Пn49/s|5*Qo:KQ3ERu]g\[eYj렻.Eϸʲ6kKAw]/6q/."lemt‰'.H K(na+yi1_פu^7xc:Ôi,u1Zi wqTL*Y>E'$H/BYۖ 6|ٲֵ+iTk;IIĎz_\D*ڬ-U[u Qbx9+y .v sۀy>L 'wlhGp;%?رΉi7:0] D0{x6ȝ [t`908(6uB'cw{v^lAnO/eL#'}zta$ pیysw,}w}y1Z_r yKFa":~+yI7xZBl5P6&àUTsJJgC9%Nυ (?3wǬWU m%"W92Y+]qț OWB/59__5PTйψ ~ V΍'OjɜA~8#P`Q߷isxx#y6Gf3W1)1{x06< -lظ-vAv[m˳rh_rYܹC3 v[d  ;wl;g h ;d;xNd8'  DžibDZ'r{3cC~xY[x cĖWABUVqzCV[`7tSяVD)$w.Un>KB%A#;9p)^1933ƒǞ{HM/8Ϩs|_^V z_|3s}N'>Uݥ sQd^̐O>p:SWfUXz珂a dg}-xSNN_ޖy:mSto!s2!07\9tlaIgFwL(͹]#UMΪZ0?f̎ij.ϳlО"2"`ۙX=wt1rul>#<V2$ [kp{w>uw':;<mytwni ` F4 `vCݐqh>n"ȉ 6p{z|V{3/ƍyxa4=;`zGdy;c=Bvwhb"C v'OoCna_K>dog0]wbȠ)2ؓ^wusu)ȗbI41VFF]x)( %F"琟fx^]JL'ݹƷ HCxC;"mn8r 6ai։EQYK&>CC_ oanUWY%-ȢoHzkŎך>=Wã?믻>iVH묳NZtEsJn0ŹbO[!=kg~pɹik׽uoVJv[V{?~L_kzӤ 4GUW]nq{ZiӚkZzDb bZ_%=9F!=CZ^c~pc1}j ߌųZMz ]uZ^_9ƚi%T^{5{M/xZQڑ{%]Ekc|8FۦO_+A!$Δ#?6]Ý.O{ZZ |ҥ5C /L/yU-yWOk],G'rxP6}mY5땥#QH߀mij p91Fz3UmC3f+~eꪫb׵݈kN3},|c㥗Z*f|-*RLkF,M/΃;f!-ܲxzZfe;|eဏ#_IG)h08Wi."#<\OwQu#9$M|pytۯ?m/63dަ61Fu% #xҦ9-qBQ&~iI[-=C-ze/K*ќfm}}.tK۴%޻vY=xZw,} O?HNEw9!?W_mO﵉P]mt >O~կTs"N;_qSg̙֒3UиMұ׿>mCM|Kz7J^Tѣο ~_H];yʱe,ޫX]@}66`ޚ6x4g]޾nQHScҾW'EeɯkZNHpN̺뮛ڟt 2 Ҧ #]Z8xЁ*V,iNpW~ E/_*ut! <x>3U$Ͽ&~iq?Q)x _SQdƵtk)C3A] |١0mu-շ u%TFLTɭB3A] o8okR&U:\& b%_q2Wv:*<3l Exϲ;6lGV><89u!ϝӶCtu<s=2w~֊:e16n\!֓2[O#@@AF`DŽ9뢈mk^dd=y="K[l=.IJ26b<(BF,VbFyP1~( Op!GnxI̸ hx1HW,Յq̑9zmiJw/+}*}'&UTNUOV n段sҎ[`Y2 57r!Q2!;?d 9ˤ>H?f[CZwK/M~Z V\i OJQ'pb-~_ƌ7ӴpK"3tՕWn1R?Ծm^h&lg!~ψ|zyg}g70zj: ?PQdV)Ӣi,6Xh\ӟX<7/r/xf}<z0ckcI3/fg7KN[2]veAۨ`կNRQ;W\Y"QȺ+M7k#Wlڧ7ա#=m=/S3M?C WNIAY殻:(=j>p~yqcc8>vL/ao|3=_o}[>IwcŔO{al8|O{"wU~:XN|1()-!#VT?H/%Ÿ%d4ry0'DI;8_9'jb;Q1QR[BF?\#9K'Q/gDDI1n  S{`<6ƿ" ` Oa .f_jb=[=bd'w|j_rg 5/c6UXu>&j7Cׁcm;h;8n4(0p0(>Xo\uLcb` -cyPC/2 c:  ?4vm=h/؛ȠS^Yʃ ̎PDPdn!X$_2N3ž '$04hDEa(+[f12s4u*b{,ݵ̹$uNDhLn Kv` McXt|A}{U4X@#tfD#[4&H5rСzfT=`~#I=ᙼ96h;MW8#}K3N0>B,5gLKҪzH~ ~3)Ԏo!mo|]kȜ3gxϞ ŗ\Ek ]C&x͉'4b= yUsT:6W[]L T{EgjF!nX4˿KZnYzX|~4yghķNV#L'8S矗Qg}?Vk1qۿ~|OԖ_s9@c4C"=9IVsO|hZkqX4>,Y;.)kX"ʋ`#?tv{"nʭRxeBb^Ym7Lݚ!/dV\׹ӿ/~˴fUȠ3}$}_?B1GsTZB_Y"4׿|HGU`9TAQue򬗃zwz׌CwEAm;8߷4C,͐q^)|3kDW> Ew 11m(#i:?q . /n`8fa&+b O LauBsr8)HMTP&+As= ? D ;{m>9c6')BM]dQP4q̠~ȑxi"gs=yƅF-vؖxn?vqq>5? ˼687j#u}}zk,Hx9i\Ŝ 6 ֹA,镟y峣hrǫo~XeUWȯp XW]va㕥WAF3C1|{BozS|-z9Ŏ%>{MMs{@803pp!̴=z-Ks|;5SA{G1;O;UIAIW3dcq'2+KR{֤D뮻*׽`׽:j4vXʪ+t,*JWTqYXU*2?ұ(]QUde>cWQ|H2tEWe\E*"+#˸U\EVG:q+t,*JWTqYXU*2?ұ(]QUde>cWQ|H2tEWe\E*"+#˸U\EVG:q+t,*JWTqY?V53dxeiWmlasckX/ ;?@^cOlr@uwc/`"q6H›Yj۠vtOC;.:6uL -i=Eo"K욶nhx"ʒl^gwS3 P[6"4wu5naG!}yɮE}A(|(BBH"W9z@3d(,/C)YE*_QB 0Tri)x%G,, %-! tr$ oL[2#" [wySN}MlA!1kkO6a%U̬Lߜ)+Kgk& 2mK]pu B6C~j}5EZS2Y[ze=뭷^z^{acqb7nڪ[@aSB tw^Qi},*I'3O~}m,Qy _./8~Z6)+"}szVF1'lE5wiݖ5cq]!VX1h7hq#%c5(|D>p@ShGҢ7C=4z>5dJ1Sr* Dg:ZyoT8#',p23^JfuvZ6,8+Y#(0]4.<]Ŏ_Zzۈk::p y,d!JV2Ǣ3vœ9Rk$݁-QhB^) Y$Ֆx(4!,DJFfhjFψ /[/}qMpEիi[if",R@A"d^XTŝ kH'__Wڜ7ޤ.Z tQDcTXL_jG(i-C^YZFk}{K?aCM<+^j-jntf5d6RA 2:@;,Ƭn'> sEZm co9HІ|j,ieδm'ڢm9?0?(\_s5m{70  ?L#~&Gus t2C4r2i'ؼxghtkѻ@Vd|"?1phtΏ[78 ;!( Ŏ`NZ.QidձgÖYǒ*t 7u[չ;~մydbGw׹zj~W֐AxEBu!7Y0(Vn"*D'e]~ %:63NDL:y4rg@H,l}ꍙ" ZG%kňD.|نaۏ![kuN88֐ ʘ^cM5;(c;WY!aWOҬiyL1+]Z0_ E鵟Wu5KaR҂SאN/9mNA\75ClcOĻ͆)q2Y"{` xfUVm:F8=!e(E}O (h&K/Ni,޸}=C&;K:_Bt_ׯc1_/~ BpsW(p :EtſMt.9 1>FēNJ+K/2(׬!#ί,]s5*0{hN~Yl:чi WKhn! h?]Z[7iagYzin://U|U98v/Y_D/^Y7^Yzo~Kfj؞{!o^ջ-AD`4M,|XFNj[YFZ,CPh?Xfqؖ <B\h:h?Qcm'6r/PǰrAM?"Cdž4zB,C'jEq{!p!meQ{(: ?\@m5+Ohfp_A"dI_nsҵ/mՖ T9+h^Yb,0Sf̺륻+qsQ@~|eOo. A?әHmO3O_Gq}>su"|_Hק8 ꕥyK۵n pwkV~AWxV {cD*u?yeIm=FbX>DϚj.#zYj)ֹ(}'SIɶC4,u^Y>{}+t9&!sq?(cЂ:Zh􎞎7@Ͽ:9?:{;E?_sqE+dj%}CX ?ko 2q]4t orh6LJ&&@\㹜:A;> ăwNL\`r7&QH%.~ 8Mr ם/>lt? aVVWj_W: ;1h[pl[* Ͽoq=.я5dW#fvHz-i:Vܬq,1Cfcf!sgs鼲T3dW/Y5d*ȯ,^EרZioǿj_1wYҠ0C8xa_oh3n,p> pF]a?7#0~ <<\;k# `0?HP='y%><1ĩaƆ`6}2<,y1$ 18}?2;)4/p#m ޶`qAq]m ]<Ff9[01h"c\w~콑Jv#su.l6ld& 10l~#2|IA<u& zlk2BQ|P#@uQ/9rZwg}5R̐XXKf~kN9?<2*,,~ ʼn9~vyK-q/e*ў,nt|93L<2xlIrn_iEyҪ?,X{AuL_zUz?Yi knF( ǟ̊O;~',֝W_s>EZ$1?wOK!ֵ%y#5CfD5O}*]v͢:/||A-|r_Wz?9mfaW2>1)|Ǽ/k` }W}2H4Gk4tǵU!*͐,#!E>}k=5EC~x5qWYh{ۚPԢaοQR&z!Eg9Ta|K:# Nt'!2Ej;[5Glwwtfy-Q!?3eD^F rILQÑ {i!>-*p)c1Mōf,cASiv *;V\0bS3fZTf@@.i:EE}{tX΍<靱pΏ{K-l|GZSsSHRաH:oeY6pZZ֐נ4[*n_Y1\;|y>:VGhN]tj6C XCFwQhYeN~"ie~_Zq"{ߧJ=/>3T\2= &FjzSYBߩ! nd)ҳQ1fE2qxd?;GH/9i(ڠ,@~72(ƨ`O ٹQ%BR㟿ttC+2N8!-rjW|ߥ2}zٚ-t| $,]~QZyW ѝ5ֿ8'kX>W%/-SVt qu@AHta@E>: Hu)J|\f rwBHJ@!o޽'!9֪]_uz CC̹UQmI:,ֹt?tXt_YB_ 3\ 25-)x?|#׏ĴL`{JTrvJz$(?Z}~|kwIr__X'\Lrˏ|[XtޅeAfO~l 2tX}2j`yVIT5 tZ/=^\ljEmR&}A= uj)(ci !~{lb#C9 rYk,vbil@cEmX}pn u| 3cD6"(PgH;^av K]|(_w_ɫ ]AeM0%\se _yBfsns&xa{05;2vPt䜃vyd}.095:9W6 ڐ8{ԑnEt!0+=%n}p^ ?uweR6 ;5K7>Fӗ%MnvӲ䧰%r8;`:o%cWRGJC>~ o5ny; NL5ؐ G߅.\¶,K 3eߘôeY;~9W9mDgoυV/ D8ôeY;#Qo\_\#Uoސ_Y7֮;!e Vvc>Ddض 5iN]ra  [55" E0u1o>9~!9qcX<àkӏnm'9%Ă3sۈO} ,̩ 1?+NB6lz+e# l#۞9\L>LC3.&ᤗƳK.,·«]j+0'L',Nrؘ"0p9Sas}yaCo.֡'\k~'jōf"~Iab#&),wĦLt_tvBr bFy?+_+0T]rm!y(7͚FƫC|M4kfCAB<췪=l 'W:ٶ![8aǏ.+B <N[C1:yö+Jy,6d 5tgaKT!eћ ߩ*dXgĔዞ]2<͘?d_㪯/[~Ml0|M <|_s68jк$y |]ڲMRoV -$w]ڲMRl-$w]ڲMR>:Dj׳,zc@Y[I Cv=kˢ7IA>!?Peћ [}:Dj׳,z8>a8܀~4bG*Vu98[A?:~;Cv=l#8!>?1/Ȍm6+d t%DI(.7;.Bas'..x|cm Sb%HҮl;sܐ$MUkĢ?{ِ 6.5oeBrZjflV‹д6|Ч^͗RLltH :xrʹe7u)2d|´X9m_`76L%`!67I34lx6x4 y\6ƂŞ;Jl><>rTts`ׇ86tg{Cې+XtЉyClyOn5!%$6!d.=ôOIԓVY/.R&%͌^/Bظ7ap;&Ǟ|u:lg?>?˿u9ݐa"!t4nhӎ†Z>@@fMKN!q$f;+΂I@IDAT8|fYS+9B7X]l/g}&>k-n$0qԲA6NhqgÃl|D>0Mۭo;\7X5 F'uۖgņ̉A*`RsBmw,1@6v6I:ӒHٮO)o H$ƱjlQ^_wsmvt1b8s(-re̿1͠\Xg8^|W/;} u53~ 60!sZYBpenzxW e9v/$2vmbqrlCp|IJ6F~afmU?W2F5d1YƿCq8Pt"9n,Ϻap:N~  DͅMy(r)/Գ>9bX|DrC&&d;Y ;9 qƘeC x7@gH&6浡ز#e΄N&6aԏȣ1?|2&y`Ҍ΄N&66+ׄ|aHWqa4Z &\w\w\5!_::؄ωOlP`=r! JIJu_\ aGr,5srq`#s07Ă%on}sc{a}ĈpH]m_Q H!듛Ƴ`Gq͎Xri8 Iu.u8، W'Ŭzȫrw|+D/!'Vmz>ο,F:1daˡ?s}|yLc#‡L,r Y?  durs>9vsM.`ذ^2Y!>?\VZIBQf⮷iڴzĨ_/(u3̿88b&]pΛ&/;M3?,f&G(o\w|ĥ`|xwʤ9d{~3oK\oo,H`m$Td0?LT A|؉'zpְxֱ9@cmM}16C#pԑQp쐜6Pg-3G7a&ޚxCqvhseP(O[:"3?q&hC&N|Y8mF/8C5\ Hl.pb2'0LE?j.RF+ڬ+k[B{Ru,!ͽ6W_[HmH]>fk AT-U)܇4ڬ^y~m!!"et^+ϯ-$QնT].rkz ږEC{mV<DURu2}Hsj[.RFiYBBUmKE"!ͽ6W_[HmH]>fk AT-U)܇4ڬ^y~m!!"et^+ϯ-$QնT].rkz ږEC{mV<DURu2}Hsj[.RFiYBBUmKE"!ͽ6W_[HmH]>fk AT-U)܇4ڬ^y~m!!"et^+ϯ-$QնT].rkz ږEC{mV<DURu2}Hsj[.RFiYBBUmKE"!ͽ6W_[HmH]>fk AT-U)܇u0(ּ 889ܜЇnnp]v!d;R05 !$FXtrw/59[,> 7q!Ȥ`rA 8 gGvpl@6ruTXn#|֕۞& O9'%/M1ԁ0CO5x 2Cp# &PG\w\ULWkD`?*XL1VDY矗L<|9i\qcUgtbzMq(Q\k7?lO7-/-PߍtTt֑ͱC׏]OSsB.s3<:1pp rlvX 6mZ9נ>:dkqkSVoN0lvt@@d;$K5y0ClX]2cn:s࣍>:~sx}ݬA7;1bW,_YҶY[s,aki slj~Oٜ6n+a c!i7ߘ͏& EvެH6ϸ9D>;˟D)byv 0}MeBeⰞ@Δ˦\2% 9vA!d05Aǚbb!Zmxu|Էv۩l[l C&m :2A6Py@3M?>Z\a#Am wDL@Z,58̋y j;]l>oD'\+b#F}'>rtel#q3'! W0Z7q/GDW@[%$Yyk~}H32u</&8ʵb\ qͭŸϟWk9.lr=믫_=_>ΑSn|c9=6>kJ!v0P=onE7/:t-o Cœ ">pٵ/2uYepf͓ۏ 8cB,DMl:8~!CkZ+9bՍq6ao n0y!ㇰa&.ǚrbo7nSg~o[n06ܭ6/wE5μgz#,\ ͼͼg90pp9:Oqi\"N{64 +ц M]{6z/{AceS8AvL0~xy[7u3A8 ]_~x:r]~AZ3MݍܸpݍNyg&;fۅXb'sbYÉCֆC8G'6IeQ6Ʉ0(wcЏL.! a!Sƚ C,6cN냃cpcC,92G\c;SB,dN|ԒkNGrH؟e ŽP츓!}-Q(2i *w2w|=oָq'1cOvn3e/&k8dןqןec\bDߟ}d_f|#8\{k5˩&kpds\`8C6V׵IXt~|/|q]o[Z~-pGq놐\STxtk?t授 w Q26͉ݚuSDl]|S w"8lb2'ȁk-!@Gu"v^c_N"O໎]K/B7οqo($i}Y ص4"h\g\g($i}Y ص4"_qf ǥMu0Ggcnv0\1q~0Snn9X?u16tXXjA&.>!cC}dXdssr>G'W.lS-=H #h~ykNoLzN>sPׇb@8zցK= CGLmE{!YN:X y'l]b_zkQp^F1c/.qP. 0?GJL!wM"ΥՌR-3> uP)>">?_Y:=:zMrbM7A| 1v~p8t  v곎-Y'rnptb!)#Onl{Cn l֕n͹7 +?r㣀 EWx1!##9?FAڑ͋i3sbNlPnb קtr [:#GADG+nC,5Ϧdum!ڥ ַ`ÆL=eb!lٱ!sB LeC*=6bu-w3nG1cojuī\.ψ,ןqןq׃+(˽Ȳqq^y W8]pl9\jS[wM1S9AVJ!rӯHZ55m,6"{6pP2vI#SOB&e0Iaj1v n.,GΙs0VlAYߜಬNLN!9~=ײ-nu#g"DJK&*гLM"v0-:Q48Q?_KN<7?5jןV峠LyffIx`OE0cY;K󭙚`[t8<}3v|Ν2Nnsp֚< +Í CL[ǜ–q ]:$Z"gLrN".1嘈 S[S\Đ\6G#%6g, (~|%7;E踹ɭxsX:ĺ B^d`G.]#pg\&1r@91ح+Qq2~B.}ms2(;3G ӰϞoY`h0ŊMc_"zyqiW߰}1w\w\ D" ;G;o<8f֘ep֒oZkH\w6GǏ sIM.Κnt9\7.+.Rۦc#sgdrBC u9Cl~| C&q!Zpjl6aAe Fjs A S{yṳO,>ut1!7;~Gsc'™8ؚgNYϊN ߊVnٞ;cT10c;i̿ungOvkhի*_8;iןzx9_:XTo ̸2;vӸpJC}?2Vu:6gBdt⠬g<~owz6'Cv!s,*v6G |9:NIk#v/t4bIH dtpɆix7!e,r~rMcRȜؑA[+.LmA}v.!XkYO>rPsZpXbɆ w"ʕ°RȆ#< VZ"mÑM~2`y\/c+J]b64?PŐ^v|+J-ŶȦ1o\0u!]zkI6+updӸϸ|]ȗM RKxm8^lOU6d~0֬yMZ6's~wb̀C !F5Fv&Xbd[jnus9R:x,ku`k9UI.[g}dMgpH{z(89PvC'`8x)&ċyq@8 Cͭ>9 0f 6Ys>\뇆ݯ |iڻn0@k߶Iܨȍ/Orqļߏ9,ω6L^o̿qy|\w|DYauP>c-C}/yzlxD^sl'dƺB uy΋2ywS|Qׇql }_qTts@u8"S׍69g9?q~r1Ϩ̸ ?{h&kan|ba*]̺2lHa]j>q< Y8d謩v| ‡8|ȐUW]5" KQ;mrCFd?0G 7= s9ؐ5񴗼|a*vPo(c~^sQ8rg溜/钏}tK.shծv1xҵ|ᵧ~|E7b.F;6KZ\l?&1 4:WN qc,MSi;F QwNwnL2_ 8vO=DZ9tȟ4c-b576uF.xecy1b't`̇: 53Xc-K.97xԀC؉W ;]vۨZm3C$; #Ib1%X N<"bkN uI`|*%d0>k͇g͕桾`{ͭW}3g]~h#~$o-yX#2^tRz4,JU3.~mMuǫsSa|7ο\/Se0 (]V/3ϙ%|궕B].x"6)>l6HZű~p4dHGHlc-n'7\n1؈c9ج5mN}6O |ZEmX}pn u| 3{'rgAD ƛxPο;7 WoyLiՑ09+kn6}W|Eᄴnv uyEQ:1jvc^Gxh1e29Myx1/H]< 4_1d(s(c^Gxh̿:cPH7Q Ǽ"u(Иu+̑nlyEQ㫧w}pTbӃJiY#g ֞!s<2r0w~kRZp:4a~`Q22m!?vmm&C,2\ `YX-$FK 4N5w6A#M}0y>ّC@C9>1n;яN׹uZ6o"}/t%?U]TCt|4\%qRdF`ן?qj͟qzs7o,ve )ct@@$c0v8+:|cw}=L̇B[!n,C"5ԇȃLbñ]2>m17véK.A!}&FD(\4q'?d>;'Κr~bk=sCpbtmm$wXp#7zn1!95`#g +e#r&p3Ai'BVO_p^)k)gҵʌ(~.i_>"!F7nwO7э "[}_O"RϹ?c]oG<W|^uy7}7}ӷ~۷F[7O {5,[eskvm\fa~檏˘X4i,81b\iׄ1>S/s;dNe ~"Yqb񹁁)A,kWa7Z۸g(lX?ԱpشX G8ql(MF!A?l 1r0[ ؉G6ָ05mal:;3e,]v7]9qQ~:DvT!)IۛfM\p9oz<0t1| G[Ðb;%6SJlėcË <0Q7ethmn=_$I4[o5rWn=ɛDi47^+&oY(xu\|8=..)I4Uo5rWn=ӟ/~q̝CկqB71D+Tn^4^9}w~όgZi?y'Zq~뷦//O|rկ^ڞ*TLgX˦<%7~7+*.bg!T9-oΐ_>~i=4>}O_Xօ>k-Bu5MՓ7b{Ӭi n:tWYM$MQ.-_g5y(7͚FƫC|M4k{V7b{Ӭi n:tWYM$MQ.-_g5y(7͚FƫC|M4k{V7b{Ӭi n:tWYM$MQ.-_g5y(7͚FƫC|M4k{V7b{Ӭi n:tWYM$MQ.-_g5y(7͚FƫC|M4k{V7b{Ӭi)E%~qFyEgI Yb/wCَNHnN74|eGεX6dg=-99gok}vM23:F8>~|pbi:?x\@%rX;d}89|r5:xbi;8t0$8n}F.dί(:%<2C̳ؐa"mimY&) ׾v(4WZbYL^PXT*/Ԙv 15bnp;.Lͣ +d?p׳,[鱏ytmF`7i{{9P6.u-$B|uw G l";IQWSz֖Eo,3sVi[Ol"]as$~-}K _rV*MO~cCM/xoww~7~cǷ#뤋ޤϛǗ/}tfZf׳,zg~f=櫻j@*ʮgmY&)w6Kz֖EoU*d?p׳,zeBw=kˢ7IAު Y[I ݲR!eћ ov -$nf]ڲMRjBz֖Eo|lT~gmY&)[]Bw=kˢ7IA[Y*d?p׳,zڮP!eћ -,Y[I VmW]ڲMRm -$y+T~gmY&)w6Kz֖EoU*d@ 8kZ8GOC,vt>Np)1sk=qY~u7vۇ\D5r|GBXqk}d6g}|2~|:yw'@PnXd݃sϝTkox̗'?b΃?bw̑s8eTx޺nAi {?ؐ32SٌaCwD«O:9udzs;L)W26kI`G?:Ɔn{ۂ}k_w"=Dԧoyyp^җ2rjr+yдi|Wj:/O_˸LM)<"h5yJ-B6+?̿q.3Tcq׿zuv84p̳!9=,n&wf6֗L~tH :xrʹe7u)2d%b 6㳾}- Ɔ[,d}tmևoMgiغ`m$iAf *l=w|y!|s <;9=lq8l!Wơ؟kC+K܅I˝3lt)Zj^QFhOw?76YHubů'ſˑ# GFjfhas-BIqE[y̽˗fVyE'73{[5q'ݿ{eխn5S_ll1MTP!Wù*wEk{*hs`9Gs|p ٞ?*1+?2nW\i.ڴ6!299Yđ2~Hn9Nu9qEb~ȚZz ck[>|H`1_7Cl8tj u݁ '< ,2v0%Xm8pplpA}6N>]"oc}6ڐ9ɯ,|\,/0W@=Թ҄=QYgzPQVV=;Jd/Ca- ݰŁRGf19fD &Œn9_5mjV ~H]B4"s9}w>Cй-Ei]y h^MiBDZW2wS7=a+K}㋵)MHT@3BvWuCf+K}tSe>E"(\zc1?9]k3R^=]Ux_&hU?,v;dǗ>l|N--u +zs7t.wqn(볏 4C/t4oh"+oP )MHT@3BnJz&Һ й҄A4C/t4oh"+oP )MHT@3BnJz&Һ й҄A4C/t4oh"+oP )MHT@3BnJz&Һ й҄A4C/t4oh"+oP )MHT@3BnJz&Һ й҄A4C/t4oh"+oP )MZFP_֏ܠ@g 67Y4zԴn%?8YR;de}PN=X6#x'h X[ 냓YS[3s`2\@G 3^<~:ǦIʴw!­k6kY?pj#<>0Mۭo;\7X5 F'uۖgņ̉. }ܸPc}\K ͶM3*WA˜'T7X(VeWC3eT'rWS;qo=Ǜdm{_[K"ŋ+KA}㦇A?n'w'f oEFcg/;d3d_K'u5O?ny/s$~ojW~E t{ņLD0~.Qە_?[7Ӎno~*+c{]Z^.qT-'/8to0}y<^L?W9w3%Tu/R%gs,H22RHXϮ6?\:zml_L}3;WFqן⁾2Z,Wf_w09s0\_>I d/F<G,g?m6*d+O >,! 8fE(:Z7\g08ϫ0nH.pַY76!rȥlR b!bqԇ[_?1=ygd?MzLi|H9[K_JtML=ayzW ŎL~9M\+`w;<͵7K-K?~tǯOdƯYg=;#LJ]Lkbv2}:Tu#z%VR%eO]x9獱!3?ԗ,=-A;/_݊F *E/ztxk5W=ǝ4ޱ wO{iOChqI>?I\9c xӸA׋;P ?7լ%|X,OvoCxjUWLό_u[WHoo<`zR<3]*wW(ؐ9^t֌j.uzo=-o~m"ذ"_HW[vhhg??~e1Q((!7d7qE}?n=/gCֱK"l|/y &^ܰ.1oXϝY/b\zK'~y;V6{SD΢lr4z4ܯ]Emlr4z4CgK˦,/#Tcֹ'"Hbz-Ǖ{R"[B4Hltu{,r%)-./EYj2d/x)'n+C9HT҇27pIl~f![:]JhYώW_H,>7=Y%ۮ}S;1?>!&xy}_ԡ髾+o\zǦWg69Ot6QO$3Bк3ό;:!O({ynv/鶷/)vȏh{,_ӝt(G=ƃ?wO?7_%>lz:2F捣_'O_?}˦qϖ3_^?+^zgǝ3?Q/,+K!s~!s2WCX+;0}k9W/j}ܐϊ|92/4^Ml*l=W1Ӎ? fH'Lc-cӏQ96bh9٨`& P97Ocl#G۞p:ԓz!Aӎ,#O'Hp7 xSرvtrnx1nlԧ=!S ȁ/K|[nrÆrM|ϊ A^VZIBQ{X&̖6a׽ӛX~9M^[ǓvZ?l̄Pdtd|q\ERfB'R?قLOmF>X*Qj 霡~ 19!tt54DŽ>3Vr+O|1l\?6Tř7ܙq o^WL?hOWP S<=sP7ʯԯpwk_:MoM<׼/ġX3] ;gP@tҵ⏧S5lPY69y_L|<=Dlqg&?O*)Oy>_{TwKlE+_g5qR21o~[]>|Cbq́_͙9ޡR(G?;ٌ`3;;>k5$^F=b+Lu|&DooOw݊xObgpԡ!3P؜7ݭW}u>u],lzPt,{ԣU<'j4ѿo/_-?7d>y7DMWO71?/K:o3K^鑏|d{WQbѶƿz1{?v`5F}?_̌gm4{7ߘ|ߖ5~(.#-ӬS@p3S%Ća'X[zpYǂ5J@جS xbSGF=9Cr@M(ǛK\<Ƈin{k&!nC}<oppxę8 8udgM8 pi}17>kV\6tgȜr C꯯\^ЙCx㡛?{IU & /Q)ʂ ltDQ V ECP5 `R.U}߹3. |~νg܉Gs>qu$~R4%rިKCK/ʖUZDnm" FZ[nҊ+)GDFxW be\ꑥrk=aRz衇b\\lZG8#Zchǧ͑z|xL$CN'rV2rP%|6`GJo~#=yoY(d|MK⛗?X z\~{#G>)at'?IO>񳟧]vU_pvZqcǴe!?}J6aS)Qqo|}Eo8sbVZ(= =Oougҩi鑟kn$L+Xg^:P_{p}2ץl~d\9ޫ+jBEO& +I Zx2޲(LrcCn=;z<Cجrb`diUև:HdWΛ}tސ7G局s='o?^:rlbןۖԶ u)}"VY6ߟg\ʲa;\P_-Aw/g\ʲ6kKAw]/6qY\D*ڬ-U[u)|gqa,kTmtץbןERu]g\ʲ6kKAw]/6qY\D*ڬ-U[u)|gqa,kTmt%D#K;`X15؁lf~L 'wl0:| #_df} c9.6]Lm*0csT{jչX>E|ړbB$D2 ^ u 3ߤBQ(SO 1y ahLD/D_1mV$_딟Ld^VWO)+Hou=ܘߦ; /'\rq3NOf͊x3x} Y5c~VS^ʿSAִц/g% Ś*WgY!"x{3<3\8ʜ=>3L_k_C?j[<2v}^_=mQVէo鱴 CғN =u׳BfO46NfpuWq)};77 r9ІK.sER}~>0򦾗_Ֆ[*=bbIN1:o?ҫ^IqN]l]zNףMq+OuPڧrkcw83D}}dz'HÃ0?r`@c Cg{b8?6ۖ}I~xm/zʼn Й1Afc:@urht<> =@qmcql뉇?:>chc֡t![YZ RaȊNxĤH™o"6u &^h}!3)anș1dlDxM5)-;1lԃg!+ iYZAɤB!8$]=_3nO?4kV4GbҗW}ZQ߲nBGK7XWj&q{77 y6 _QW*IӌD/| UѬ`ǕW\^Vq1[og߼s* LUzj(XA_qJljMӑG, +ZG6#DpO#\K,TvmfuUki5ɷNLBFMGx˒գIGؓh"W'j e[5+dQu^Xlz[f\ߣy9 EwLX\Ɔbq߲[?644>c1VT咥=i+!_zKVC6?#PpzHʙ3\G>12`8 Z0-$;7\ӯ82EdE3M~c#/Zf!w{޴uu>"? p;d{ȱLYqO\X'a#<4-@tqztunb3] BĶ@b_rp8.:8ñmu߈ .!`cUYlB)GC&o.ŶЊƜ7&ۼj<6ԧfΌGNԛxf8Ywg s}i(|s:TЇ?[i+܎uVlVZ!F/;&KV$;n-=o ߳j 7HPji+Q ;nOGqjKm G_7Lm6gSN*mz$ s<,rq}qF]vYRG1y>nm|L1!ަ͎8VfzMTEc5-rI'}D=7a[63'~=ߜ |S![|H/O3fp a":k|u[6dtÍ7kyҸW_Gk&Umz|lUVy|Ia7홶r2~ޒ^>149Jy>F#3e.*Aqƿ=OL{Z5Ѕu%2|a8ĔGYS ][W 3'0y=OL{Z5Ѕu%2|[Ͽ{~n)|0̐(xX*06vuqy, oZd 4OKڞX1aٹCok;b!sn?46谳r"̓A>'y4ƍyxa4=;`;w:.ĵ4zg$I.nz9;./2"YD@>tvCFv H cy E9{' y&ihb)h3064z:DR:CQ<*F}boz;n|ttl`a%9o^#?zx#&u/}KO|Bډt 2֘%Dng[beMw4ynz#?"m =҇_=-Kx6~g.zAϼi-Ic-l~tKoxt/hc_z[<%-EgtМ_6˕* 6ۨozK_ZSXY9FplSa)6H$wQ+VۯtG0=S{=d׼Qh 2slXzk-KGqTԧ?uW溽fVZWģVgu<f=OobȒӟ, fKQT}Kc!}.e*򱼅 2%bc¡ۨґCa{GΑoV I3m8ff WC4 ዤ}Xwџu7w^:Mo+dvQlEJ o + 92h5Ff,r`jînzGd؂ݞ˜p_{b:DZ @mO\ڎ/r<8xmnlq7;cXu!jm :A={ȬƬGW4?5z0Ɋ^ƛ|nƛF⍯C:Y[ Fzg}{ޥ7Hi#9{37W4߈qO~)~`5 K <Mcϟ)zz\ִVePLb5HjKV)EZaE֛j/־)Kh>{_cZ RVnv_]U/ʛFU=WDի˧M#G8#,&o El<^ Ċ+;zzl(oI^b_V2[7,>jGYMDO~'k | 7F9`7/i7mLO>KEhI鈺L𭮥vQW!NC2+|k]hFԕ`aٔkR&ľpJ0ϟ}aܯkowsQlaŜya[S1M/5]_}ۆ=)Ԧt JVȨ i%T`%GCnbDqrV~0h'|\_^[ou:VA"Ã>My =Wq?Xu>`,Au`X8M:2(>Xo\uLc1r|`9>vT ;t>4@~h zb^A<Cl`>Q@!oCQWҹ$G>ɗ‰L 4I""U0d!"Q B-\b;nqϿ"QJB4(ۙ v<{#Ko#Kѥ0OL|õޤGΈtliz̓)9/ϥ{/gEl6gö{vz[΅1No滬PYb%ӏ%]L*_gM4N:Dd6lNWJ_pQ;*짯ԗMyҊzt4ǻt^{ 7rgG.+g. qfK//x;3Lb?#W\|/a}M*mةobϖeY&Gblyh}ب8dvX!xShD:C?E]=Hj?_}?#KFormͶivK3g.~u?5ڷ~ր~AA):-߃>$mޜ7X>Q:.Ph󌧓HoUGz6bѯT?Wreeĝz}6r*' mCѽ ߜ+ '/.GV[&+A3q}x8_ `Q w&+A| 1p=ן}Hn:OR-2_~L9tX/2|f?ˑxi"p{GO~hn_mցM;Om&qq͏2/ ܶI6?wD_0zDzDp[(p  w~ [yG)v 8&v蠏;wbzcq+܏~t7#iQuıcdB\ fUH*#s*LV[mޚs);Q?>WYueK: ˢT:ۿ6_VȌ6z̥(hIKd 5^M]О00Go?~!HQ#O}hjDÏ3[oȒ%5>VS/06}o_׼Nxa[^{.(")￱W[8Ӈs9GE Uζ)UM[_ezlkXz44gJ==^E?1}ǴM7ۃH@34񯙯S1tcdfu̜{?2뮻Da%I]Kě2L /(N?jc bJ^ۄDZ;P'+?+ 4m#uYGꉴv;*lɪ*K.o2vEW‘e\E*"+#˸U\EV G:q+t,*JWTqY/XU*2_8ұ(]QUdepcWQ|H2tEW‘e\E*"+#˸U\EV G:q+t,*JWTqY/XU*2_8ұ(]QUdepcWQjޯe++ۅ&ĸ U\EVו:~h G\k9-~cj߹ +2ObT%6v~0)2v^;6k}ϼeֱQ##j006٢_Ӆ:>ˡto9Zz ;>>D5m#tK <DZ[g<2@m( Zdj6͓FAd1&p̌]km&c me9E hi;$ no=UB"8e¾}ICPlڶeJĊ1e/Ѯ) =7c(CܥGTdPmFn׋~yQIdL{YrEJ(/K.^i=H//M[B!?ks[tUIQk[ƽgҷJTh!m{-M$vBczkf)G屩J NeE.Ӣ.U3>(D={M'L uM~?~?>XB?q|g5ۥ}ˡ M=Dg{V<'BʹBԣ>|'~oY!vțjccV^ѷUN͆6^{[seVi￟V2Qr4Q鼹r8Թ> kY;|;aC^BDhI+ҝx vnMJYBd_E=# ېWBb pw=~2WBb8otWDk믏}7~iY4jo! QJk ]uP eF\zhȘ»⢉ 6R/yqcl:s @ayM۟>!7YjsCcOo,2|w~vq߱@C < Ixnli,|by ;86xmkm9#Ey[]uȠg_[ Au\c?.ΗU&U52?ނ ZoM?$)J'㟻3<F.̋iQ@W[ι(ײlieŪV]5 2x)Z)sw*뭷L+ϹMuMzDkږ 6m.Gs=FM+) `K$5-(36k"a/cz9mx NN{kLrCZkb9n-_ul;]-3@{'-i9IFף55?mhwHgDnnԏ̴'ZVߩhi6^-ZZ+MN3g.V]uum&-[G1\uiK˯B/zիb o$X- DzGfZZMEԩ')qƙeړsGY#k欩ڢmlghi['Ɵ{L}6Xڢm9CqF?CocMFqmQӶ˽ܠa7!5Y{3kVp9921\M `9@ .'?"e~̅}xn/IkmȠћe# ?< 9g&X gH F>رhr1XRs~bBVdu.;&v5m?hu.Ņگ=dP.аH("V)FXI.4KV)u (rB QL乙R?kW$ 87j&wsl& Q9U/iO"YeA_Ӯ ɳ֙MǎF2!>0OsnbKͯ}>Y*_}f9ˮ;H yu0.yDկy':笧@\ ܂ojܐ5Oa/=vl-b{:F 8]-baHsf*gp ן%(DƲ\~更Oii^)4Y6T+ef^ގ=oV̤\3oFG2 %z(w$:uӲmDzcùFϼ1mc>1DvN‡8l,ImK0H^˝Нwc m=:/"tܱ';s;.=G,vvb#'>ru w .#.2tA-IJ=|{h\៌~z<ןp ,ce~ƻU~|Ѵ~{ WCոQ5.vcb9x8b<a˿uK w2g(cM6$͞5 :opƍҬxq%ڥeMY8(>3)tɚ45s5/Ǘyl=@.hl`/@\t L:8qT׼eN 8=uo[ {NF/ k42L 8={Rvb?220л}nC@ r20vCL 2htQ5qB?QQc#y AbdCi5c4"@L+on4<އ}X Bv)b(xttB!&K-tzvE#z9lyUcu=!ECnW_B=fl{gUכc|k Eձz??;nk6e`ǚ}}NHc[ަ I뼟܅/meGQο]tv1Z4u6Y=Ua )E-'Vʜv#|$VlY^)C'=5E?6GD~4=$ep>(N `F:ۋ,2n@A=>r:L8WS[m{[mpSyc9}9'z% 4'Ezf.snxaUqXP͛Ɨ‰q:ԫrl6ּe9<_I_v-:heZo2o?`+v86%; unD'rჍ)q#1uOcԾwccE'7u,Aǃo;bȑ><>^7S`et[T(U!|%%Q V"spN6)LK^s]ĥɣM&sF3k'T%>3 N$O?Z3Yv-?1ieĮu2QNӵs:piC2?tÌKɯ(]I'f?\2 Q?0=ÌKaP0laflc_0UqVʖIgi_2ðơ4y`Ϙ(YzSfo^8&D (Т"&Gl1s߉)2H!0e +믾G11\ w7ύpy?w^K3I;lCZtflYN Oɚ2d~n.$R%I.N` {b~`l+#χlWdĩeul 063ﶒ>0vlC ]4A'P?0x4mi !$փ w YcI]|I .P98lm}p|xAN~':\nnSmkd}Zs}5h0@GPUf<˿VT(i)@MK`À;l;DjifS+.ġ @%&˿+džc _U){hTl?U-12M~/=dQpij @ώy;ٌIĭP`ȟǡ@5jIĭP`<HDiGhqk(Tο<'C#IĭP`88 _ qtNfx&Bua?5_ag//b L:,=8Ӡ}0fZy2.CFt h D f18-z <+GVʈ_iV[o-;d"s  r$FCfnjD2XjpObk?fK:{t &5\~b ՘MlVcya{5wanMzI1-3pAd\@`(/箎ܾ@_h`o1liXֶq;#\dmOllc68eqc,`0?F"$pq ؐ/ھغMC]Լc ymKj[du xˌ[ w|A8ע+]tGAGP1i[㬳ҕW\FBaZiOCC1u1oVYeV8{׸tSBƘy8׻`Eϔ 4v.GƊx0vL4j9-SFs|:G |ÃɋMKl2\66-yo9ƂUJ8Y(80;wL=:ǞƮΏp~01|Yb`8K۱{; v8mȈmgg(b8qX 2\cKN }}Iʄ]Vldd}P1% /HHd#=f۴S jܚ-F6 qv'?c@~VeIOs?o\c|H޴kBӦ֥)*exx% AZo#V^ʚF e¸vdrQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2aYdrQM_2a\'drQM_2a\kOӘ? 1/bsW7mB0l2P@;p̜"CC:wbdc>#ly+:@a=9}0:b`G_v8.slqtO~t+hxL '"A1l_wҴmopGm(d|/q,v; &۸RMc북 #.D6(m -Ht)'7&QoɍLj? ,#E^GZruΥh;9WLIw}gQlQT|ؒ 39&q}<ǵ ؾ"l91x˜߶aPذ4 #;Qt: x[uG!wx:t=!Anrbۀ;dġn6hEo?x|S|q\%S9摥=d XW" }#G1"9+_)Uvk#ħ(B~,[lb?e^qЮg*f.FaSt+1iyf> 2y{u)wFJVOOMK=Vri*;s1}zlDL58D(xTB7];'a 8_$8+N"?o CaoP?^?cjkb~qO=c1*'?:k '-_qхcAg2hk[xho=`l?b s;[̩M;px#m]F>8m9?x,`h@`nI n< n,rLpp϶6Ѓ9<F&?S8qй }A#wm#?@n4ǿ"\6 S+ՀA[0(?7Tp[T6A!#C{Tc!yȓ/jK?* r͟ɝK<;W7Ήb(i?U7=oTk8A3+rUT1aK[#=s-Ti>cFܔ)DQ8wڮQMtԅ)Dm:kr).Q]1-oo|( Ktԅ)Dx gK5.6(.Q]1-׸ڠDG]Btpt^bj"ua 5y Ktԅ)Dx gK5.6(.Q]1-׸ڠDG]Btpt^bj"ua&-ܚ~9c4mz[ ` G&(}L6W\ye}1j/xn+sQ?w]GHcC1Ȇ}9ȣ]PguεFݑ p 3$_=pY-ޒN?馛֫h4s XVsq4sl]N/<4:u`ta2bibױ _xneK?`:65~`DH⁢~w00'$.\yBfze-":t." S뱁ǎہι}}ڂ Ў*+ʮH?@׿34`1M!;V17! \IiO[i̅^#KÿIKA$#RЉ"?M|'Ta )1I鎒 \ԁ 9OӞh ^}(M_dF1mz]֧ KTpu:MAiV ;v? _?=kK6fu‹.NkeČ]wߕNѾQjzK^2OdI7F e#TA梴v S̙۴.}8עStҷNT)ӟtZbۀ#=Tcȅ_e4d?E-[?&A-:Vv6W:1εHv6}!XVch̬Mշ~s-6m?ZZTHvh?fjQM#i*kocaE5ݎm::vԁ" ,"c~ &sq _ht[lx_9āƟ`x`hͻ΃wb`-s![mYƎ{hIG׃@@9c09n[($ԃ$޾q~tE#mw,d %;xRg>}'اѓmȜgIR1g.WQzu%Nbi?9)=FD1jDZ#yv(rmhCF3; - xd)Kb;2F>4wϗ 7MA!MdъpQԿW7䀴 W\ؒ-t~ehb7\Ygws~C=G(w%)&xdiB"S9VȬN5y6SצՍXuױBUZ!u8\eSFOo=}TBcM+FAf"uAzvgK v6nuQz´wxV?iScmrk<sig7﹇ ")18;u(7MK63gq-1)_V!/Ldr̬3ή].ˬO8/r`DO&:̚B"m='GB.YTqX_cxM#G_)*@N~t;gE_dse""wnۀ-n/9y6u-G`Y_79`7nBF̀c1.\,֣#S w d'64vͻoO tul/qM,lWl(8_<_QA{'V9@iQ<&wVZnn 1řʝimx [r݂(" Uc>UiBcx~G֛Je2 >ß`lFF>9wrs.8h8%Α/2j'Juu)ZrЛ6xZ>#MգKJլiFMs?[o%yNP/|QGh)mCJ7tcQf-rk]lfcW_uuߦLԧDklc<_~yߥV\>ZcvZf"?Nv^!}M^uiWOFHo{| |߽(Ǖ;&i+ju{7]);=iumx>]'BZF{l} Um],X9P{|LBfwo[eU9 |IqoN_wmZeUҚkN_c`V젂LòD1_'nA}㶚6~VxPjEv cKCiŕWFkEٴ%`H_<=̐79cUҭnyfֆOMz+5*<j$UY[R}"VYfm:KQ3?[eYj렻.Eϸ,."lem֖u_l>UY[R}"VYfm:KQ3?[eYj렻.Eϸ,."lem֖u_l>UY[R}"VYfm:KQ3?[eYj렻.Eϸ,."lem֖u_l>UY["A(f줃Õbkp#?01\ޱ9`bng. 9;~/<=7:0] p[%rDآ#Qh~ bsC{p8~Ȉ {v^k[x;|yݞ~E%?~Ĵ0:09j` 0 hM5eRF)`~E|*(*#1q6iaIU~gG7)QJL>ĊʊU{ǰJF*5j`$JF.&rj bRCnߌ3_6Yi 2Ӧ%mیZ8(>]pͪڕa:,-q%,5Yڀg%JkZo$,/~!]|PW2(#d[:g(l?+ LKH"Z"VȜ*+eY6ח5ysY;[U0YRiG5>i%ޤ;6%7\FmSG}(}EŒ+͞=?>?OLGX~FSҽݟw+]>dWuj =|V|si4˘JtF0X? o8}˪\g\c\78{ pI=>SzlM}wቿ )G3Bf3 sL+r t}x_hldn e\ ?<6ڗ;7vtncccb<6c{'΃mg  vlgv։ ׃`1Ml8Crq tq1kCXos Ң(ZRaȊuS#&`"ݢU4^/sG~I(thdُA‰+Ce *ĐK[Rk_ҝƷ G;U@!vOKH;ci`9nr(U $ 2{NF950ެk[nf,Ï8Bz +ߦ{;ULtPzʲ|QJI:KTjlE6eK/IW_s->쓞gm5 b~ښidzb!vZstG =B93m*s|(LIf۴֭{ȰU*ȰBT__v)q8h$pQKLM}sJZYr[7j%^lqzwwhSxd)(ȬB?Toz9c ~ZFnh l;%cl<#ɅO6rro~b 1Rͼk.gm>oD'\+Ĵ#99dv@+& iMm>$&<.n7~z,-eEXȁ)UkC,QLBH葈a +{>WvgJ^51,eLd_tG&'b-Q'c8ro;mKł*A׈V6MI.Bnc<ߊ-o>|d,46WK<яEW|MsL^ /ߍ] ñWzJ]{)`ngD^^7%;|X/~Lwy;//(Uю;q7EȭWڿ{ MkS_0洿vWiY׼k|.,QSOoO?t \ s,썫y^WMqW] Y,s/y?g+:?yhyq%C-~qqīX죿o| ,=]/-K<ԗ?/d^k {+S yn\)th, >OM >0BjKimW.ƾ܎]=qR^QD y9 jWta.Y ‹!Ŧ 1&\ěŐbmO1OculDCM2~:I%bH|BOU'D6pr߱]\ )6O$S; !Ŧ ?Usa|vAp1t>!㧪T"N9..'dTuJd)ŐbNRl90c Rl:SI* ?wlCM2~:I%bH|BOU'D6pr߱]\ )6O$S; !Ŧ ?Usa|vAp1t>!㧪T"N9..'dTuJd)Őǂ bcTLKM)  1 Eb 2t*IͅǤ#バCg}x m6Om?2|K$g{#z`ެ\bՍq4 ?;/?x󄘤n,k^ƢnkʉACL_$~Υc[m06ܽ6OWEڪk2X2q6/bᡀb4jxG@ &l큾f>7|qG O* 1lkJ>!ZLW<}/u ]qu٠ZG>|]3o]<\AwyБ|>Y>֫0vEoǤMq~ w=ٔZg q[ I_x ~i1r\}OMo|ۿ;Nt+ cN%Qx};*Oۃc}*pX靈|!Hpp6?ގb΋[O1}!ypa,,.6zL-K'՟EYN.j_[ԧ?54c-U'w·[C}cjoY:*;yo ?{g~7OV?jgܑ,'3oZLo:Eg;nYq}X\ l|YqЫ_W\1"fBw9haÓ0T\!e)[$Ň?+]i_ոr,3g}itjlǃ/megh٪kmnZ?F>MѾU89+E<)7ڵĥqsVs 7r@p{+2+d:*,&\%{ϭ93s4]$6G+dG2WIeg|;,+dgo8|w'395޲t\e)dY7}WQ҂K8 F\#6,a~W 2.-s9y[W)#eaL\vhGG__WePovws,?0{1zaِg/l:ơN}x2/\7d~z q/bxɋ_"G ˿o uY8/{ˆKI\a[/L/{ٟ/}NPݰĂ ]I [玅If9o&g}W37pn1oYH{ohe7 Q?^C}ۂ -Y\o)j$^{q w[>oz@p[M~g-K<<7 q=q+730D, 2ω>%!>Ca[⎽,-Ȝ>|w,ܭ,:#WЦ'?m+oDZ#DŽ/ozV(=liɆ;5'˓R?;eQ ƻv]}Xc Y؀CK '2،Wm8K?qC7,BǼsxd7|9; ^c!XcA!c mܖY8Y &l,HL#U!1Mkx;F +Sg~p*}!fgNY. Q2!#wn!,'Oe C$H59+t9socwWބgCϜ$ 2EB ,e\FBjmVڸ'.9$quXeOϢ (-J .&|@ǏP=I7xKB&2{;cy2Y}4ȕgy gsrpCb!S7˂̝~j cek~5Sީ*^zU;o|xs1C}2?ϜK-9/?=<*nzgfNsjϲyEyb:+t䕎֥3?w1ÇM|\t,@_{;-_Ws ϐ;⡾署Oz:[ǭXwh_K˚ψ%|KoY[{X b7AA9d®pBo=5[Á|ظo:bSΰ▥gܼ+ZO,sc"Q^ؐQq?|O[~?̸B Xyr>,<TXX qtC<ќcdžPv&ø?偒f>o=<0&zdžPNM,qJ;ƳDbc>ɢ\] mJ1} ~0vl䄛ϜڭЩ,d=+֩Xl䵾MXϼx{}dv)?ov:cg K~db!:$x8~s Su9l @ĸGn}~lg1Xȅbh9'0C>90;%*Y^禢S|?sM),}0UF;žOҝ2jBs<Ob!hqy5|3["ʍsquk ەE2YC#\ 9b 7[\.åai\n3>yܲ23]QĻ^|moޮnYp%;֘%]9YM?Cõ+Ҟ2FEM'g|u%dt)+yO4U/|_13o5z뛇,H<bG1e:b Wq{cZ׺V>oۭ^קy'_\nɋsn7|4Nx [<89G(WW@yLܲqTA,vkba冹2O gX9l~@凝?9tS[!3=5 ϊ[N}oB̸BomzJ<,>%ӼIE+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"xM?Ae鞱1Sr# Bg[\Kӓ9-qkqYՉaxk3?M qpS_nk<ȶ7c]9ZO]57Na!Cpc ŋi)œӅNmҎl;a7^;jNZ̩EíC[=w.qU'\[Bo•3UbJO'bBVo^+'.W0DK@FOkr (}qR\--KჇ} &YXP '̟hgݲ疏9 XbR<;chYiڐC}㖥@FOŕ׻Oba~sN X(xӇ~[k_뛆+ mxj+<\k[9^}GI[3b~p.oHK{" +īy Cv21?].\7,~Cg[? .7.bc W-KwlxWg<*.&}~!xX27ʷ/nobɹx5m,C}ϐ-Kgpt=acnXWs~M2wz,,=9n;(-8؅hi6se-- K{qv dYױ>bųS@ڋ]&:u,(S^]:1PiVy)=i/.vם,k:ew^]:1??榽cs#w%ӓl1 CM~dulG]lqpe,X^`726!l3sdt6c36{'SZ<<ęXsۮ%'v+8, htd>e6\Qˁn}ler#㳍!fMYG]vv)im7d:TR<ύņf5f*3yَcidviYNS-+1 QL>|5Vc=q RLhj>|7 7fcEX. SB0yf^?sJ{v<З}uVYԾ x ĭ5-Ko5!zgX !?ydBA~nu[Fw>o:y3cū(y}㞐C}?1vKB;6m4_σyC<\2͇"d{^w]Uf1獼-ov<mB8z9'nO|3=k>2aĄvx[oy̼N=8=$WL [fXio^:1?w_9ϼ4G/?)Sѿ0-uo?|,T}(m`/ycbkBX]sg:ո۠~?|Ee.{xMw]!spXySC}>d ĂsAf`n=:q<PlqMo3wNqN}g|aVRӹH\95m? ڙ#kCLݫQ\ԧ! 㓔:2$djm  )( vR{z#k 2qe9kp>~_mVkd 1ď?k-B Tm0Nן! ˂!Ň$=ԅHL_wxdAE.d;> !(ͳOX7f+dF:VAlܶ WV\ .upGl˝Q8nD?zB ›`}wCk]<;} ~4vo-Kn۩W2ሌH+ǂcҩWYS+Xxp>7k_ 籼Na8_7s 3?nwvd罼AW[zqN8(Ixv9~0h#o*b'9@[y#"#⊝C5$#"-P-$^^Vxsur^}[eݹoПB=uquK  X 9 1 bA摏: P_m,NYJWxEǢ #`t g=o;7/Gp{'^{}#Jb9mc5Ww{Q[_<$OѦaq阯=:<;!{xʓB&^ '"0!s챁Y OCܲ=[BMv ?/^xu=%'@=smo=GIu`O|>=:;+dNwv ̛LðJpB8S pp1'1^cNjYXd̛i_i=ێWs|rLĆ)cT&11D‹#%6W, (~|%/+E踹ɭxsX:ĺB^d`G.8+.ԬI lPa vGyycÇ ?m!>ȶyqUȡ8HW03$g7-Bzg2DÖ৿mLE2`MJ `*pWcF̿괎Ř]|.޶di>vۆO::DB)/X7횷n{ώTFs`QQ/;Fp.~y;ío}Y ኔ%,``.@%/2>32Qy4GsӝzeD}]2g[E/@tӛ,*x;C9m[V-}?f'?u^~\q]/n:.i-iʈtrqE{%8D{'qŠ[X3]_|#vL>8_·6ۋ^Wxq%mV :?W'iW7q/.w*nxö C}Wˇ~_EO\׼5Vwƃ /Z829t>C<7P9|/#|6Ѽk8䓇nzwWusU֍?_-|_|m s9Û韆s>'I޾~kN`bg*&b6P?o3ʘso[2]v0 1h>.r}yJ,pҮ@DgV豀|t;bT8+q+eJÜqoʊsU?.UXя~4o;2n!;O"0T5ü< 9 ?|#_0IT|fAWۓXh^K'|{}죱g`6OvC:7UO?gkέ'w-db1:WC]K O1^ Bͫ*-8_Up8ݰzY?z_W@k^Uyǭxu1o_솓bc/ccA<37gBwiNSX5~p9?kgCn㴁21 9 `$0Ⱋ}pOS֪9 8`KZ[H!׸pMa\C|Y ?3 c/ϱ?Sw^Ŕ xt7[M׊+d.=-o^}x '0RWc4!qXِ4ey@WczA"ޱ,OUz2$_ɿ?]\σOѩ,L{ɑ3Tź ϼ<|r8NvQ B>/n_,9g׼.֜ͳMϼ3fQOn" :>qÔE:9y]x :ZGw@BJu+Fui'mN&vC >cCnیWN,FVI dA)e~̓)[L3y#.=Ѻv ۙ. 3E|8m<=2L?v WQծvp10KtO=}v5 5rby{W?2\o:$u\|ᖡV?2$}?c_wwxyox'ct>(1?1%v}?k6z}1^?_n^-z\970WaÔrV]X6N$028ô^ xo qљS/hM&.O zQmqۧ:D# KQ;mjCFT?0G = s9ؐ5񴗼|aJ;X6ƊM`]1?ṗ U+67bmJǏy> iŅ!" <~6 ) wc>xι:ߏdž!_|g?0AѹC9d}ءz e+ijNҕ#A{b&n[K´+|O=|xjÑG^{o8}k e&nS&ahKtܬO3CξUۄYΙpgX39K,d,mWڿqyӻxE{^=}\uk^#!׬L; wΏ3lj3bN;~kO e4;̔͸ w/C㿹`S3e3`}P0l` ZNP;ljXdM3,p5)ѨSȼDV3,„%bXsaA&ߨVnO*ık;>239_03xhx:|~7l=a=Wge'} \x>Y+d.n,@]HJ~8^=$X7sWJ"ycc>uJ}z!ljEmR&}1l:ӱpqT14ZD?N݈A6\a1؈c9ج5m1`~t`>'>mo*Vo~tm_>9q2$v"Dp 43no0>gK\ Y3.Ф-gXa}cr+bbr9eX!~ٱc^R~rm%.(NMA~h4Yt;!&n>sxf{0u;2vPt䚃vxd}.055:9W6 cq eHpHm(Ҕ>R>ez߾xRUbM"ڗ]d%coxy—yIf4_kGz}4=} 2%}L} e!N삇q`ÙCfcK[aJ2 m+ D>k:-ñ]2>m17véK.nA!&N(FQp(=LNƉ7?X8k1ṄaеG p춓^b%Fn>6bSM rjF O0!Dx%lpq%!ܚ !_*DmFx?=J\ÂM8!djFD}Y?]ݺ8㟣`Ao=Łn;G:z rq^UN/zH Ư9# pp|l.<>0A!%Evj>"CK{#x9;n!2`!؊(Svcbœ1,Y_qlcJGo2 9)`yp0PN,__<ƅcmdT!8^,bB͏_ ~|ϋ|uȅZfQ4j;o{TbQ(ysnWdÚgB 9/("M&\?C#fQ4j;o{TbQ(ys/ܣZE;7FΛC|-عi4w oMQ.-_Gx(vn5r7n=[DsӨi 9tQ-"FM\p͡[pjQ4j;o{TbQ(ys/ܣZE;7FΛC|-عi4w oMQ.-_Gx(vn5r7n=[DsӨi 9tQ-"FM\p͡[pjQ4j;o{TbQ(ys/ܣZE;7FΛC|-عi4w oMQ.-_Gx(vn5r7n=[DsӨi 9tQ-"FM\p͡[pjQ4jfLܲksT:G|pep.~ظbvt@rsQ ,Z9>r Ͷ![8aǏ.+$ln`+ok}vd­0 L g`{{Oe~ 6ځP3Y;d}89|{9<:|xvsa>mcnub#m-Jys2 2[l–@YRˤwIA.t oeһ RWS - c7"q̖lHoW|{;Ԧgi.)7[dggi.){MAvnzI|l4M2]RjB ,-%fnieһ 6YZ&K Ͳ ;7=KˤwIAޫm 3pӳLzeAvnzIWdggi.)7vK ,-%y)4M2]RoYZ&K ^mSheһ ,- 3pӳLzڦ ;7=KˤwIAY[dggi.){MAvnzI|l4M2]RjB ,-%fnieһ 6+d?56 i;? 16^Np)ɓ1k=q~6u7v6ۇ|Xj"y_B1/Ȍm6+d tHt`jCAb!t7!vRY킋LJ ^?10Mk,y@vei ^vTe N,sd7{SZjC {kt7kzAbm鳇е п6|Z ^GMގgеo=b_X~(_;3xkskMݛB&cQ z]Ͽq~(vv9)6&'Py'ؘ_"sAĠ#'79Y&~ёB jX=yb 㳾}- Ɔ)c>6÷0ҰeH#0:ATr  {(u3?>CuPbσ]vr pA7mCƯbơ؟kC斥g Y<*Z͢δU>_{:Jqe#.O=#kzξ/W®?9sy<~poGh׈?/ _9'g>M;:sSs* 9-9!+ƑT8Q}dNL ~p08<`!8v&Go/[  6FxN-nc;0ēC[ N}<‰}gKc=t2vF}]2۳xm@,Q*]#t\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]h"+P0ftaޢCt\҅9x&Һ saJ-H;T@7̅+]hYˌ+N?s;xd.9 7\dςsҚ~SӺ!f~86pηg;3ϥ&v:&z 16#x'h X{ 냓YSیW̱PhpYnh'xet&)e q /epéX6l{7mWs_8jօo[N_`RcB: ;0ih8lI23MR~)f4*m1<Ii˔co=hv̶.7p! XϿw=n?f5#df[ߟ-?\Y\!s؜3̱]v;6Ƣ#㫛685́]V̍ѫ|Yp%Jh>‡,7@@$ "U6 N/qAv\7E.e%z'XlpdžE$d :8jAg|`dw+Y" ]H\mH[uMܯ ]XX?vsL=utu :z9e=02Z.$jw=zb(g&Y.8Ă {lg;C.2T٫^a^)XfⰃö4?^]c>PfA.6lP9uX71wnN۪6 U>)?28I2d}rx(66b3qbT_RP~]o੅_!tdsOCuK،ǦO<Zf!9۳簃ce$47,<ҼLQKt[S&?FlOҤ*w@w΃fiR>PTBPߴ4}G*w@w^u&(TBCޒҤ]hc:xb(o[RԾTS; ͻĂ 'ƜEXB%:>IL(C.3kjj'2l3C P>6xd:!tkd⭅BͽG7ٶNnr`&X6d  ) Æh' FYHN?x _>tsa>8C`] %e?zڈ1/Loh;6>'ux l7\q> ؅Nv`-t5@‚bj@;ف铷йlzݧ`uX;ف铷йlz_ǿ1wI;L;Mok5z[[rb c=ֿŨzțyne92вPEl+ǎ 2yǎ82 3&"y'l&6YB%6l۞p|aIC$,6;X8GˆNⳑb _85ac#8.\F<~Y 76dpW%>=7az&>t}.0&[ƍşU: a uW Ӧ7f,aZ?cu[s=(iY׿?ֿֿ?۹ߊBi2i7};gzaO;ax5WʀY' ϶S,6udlԣ> ;$} ԄjYc|ޖx[ L쐃 >Q8t8Dg ~MцL:2&ql8ی,Y_ :q3>k^\`S3d_>@I).8ENRE<YCBlSI!ݽ47^?{Hm:Is8 81QYQ5K´u6 0֨¸7B؀T:9uHߺʾta=r(xf!l@a=8.w/Ov%Ţf!l@a=P_v(8gِ2%\Sf<1>H]6?`!l8bG}0զ/iw<8X3^0/&!&O-ؐo=t؍nī㣾5Ne vo8Fi2HlCmHՑ!4[16Xm§] n{l7 $bcf^|Q\^b[}#':1]!Iɱ {_<f.ldCZSI>B krCMZqEL|Cǯc Rl:kimgT Rl::moOěŐbúMxT Rl:кqXi?Q;BorDCMZ67'JrGhS]bH|B_/k^2 49):M٦XtAW1mur^,\9clm}*#6l~d0b5Ap cl -f-4ntЍa4?;/?x󄘤n,k^ƢnkʉACL_$~N>78aƆF "H[u#V}# d$Xh?]1G9:1uwqDƑٺ9<}Lð9}h |iߪkz-wA,eqsV1 zχ!}qs㠮zKtٻgp:_{}b.yXƒq}.\8F'ؼr+iy̓o{BXpksn>6MRCod­0vccӏL.!&ՆPbα! u d3G`syeJIG-I_$}$,[\g] ojEX՝䛤%(=bXW9B帢OIZҳ.u_aӏr숩Gqxm b1#_+Ş|%,#6'|^zre76~m +kZG$FY,:?D>4ć8t5vt@`+acV_kMblܲ!W&];O<#b1v?'ps"k&u][p'a_;~`=!\?.ܠCML޲z' wn"IztlZ:x_<ħC#A;|ӱiIzztR($;tӱiIzY?g=L'"-O"KC7}Ya0:zXa8Cpt6 sW6cU;F~C- 1/Ɔ aK 1W;d 2` lnxm\zU녺I&4 ~7#!CHv<6|,0e_^SaYg~p*}!Μֳ\W5>28djCGO. w 1e9y-Km2 AhםUk`vWˊބZdTMZu[8yCa=_Rb=dUugYߟ8?o Yh??,Q޲tRl'Y+^6Y4qagCǎpp6k>P̅?!ہ "G8klF^3/o=roN JƆljL23, &ć:ƒXG&\bÔDebNGy b#S䷾m YG?6ƳB}A, 1n1'8c[!cy!gϞ3ϟUy07~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bh.y27~`%T"撗,sSъX&K^ME+bǿ]+de鞱1t# Bg[\Ko2%p59:1lom9X1p&؜3>\=G!&QH5xr?dK2vk` Pt{\2?8rPQit8X|pټ6C5' -"!-Ğ[8Kmhc-K*p% A9:ғbd3Z͑jCUF]9Yc9[[LR]:1P??榽cs#GNi/.vao=byVzl^]Bɲu[bZip%{l,N~X:6rϦ",r==r ̭G'G (p6ǝc~ g|5,62v)u #9m]ڝ_`ņ2W i\ ! c:2$djm  )( vR{z#k @cgVs\Jf}9 Pۊ2?88\Չcɉ;$oZŅlq@p hX avd69< @r7uᒔN3>J_Nu@Gz* rd}/ O >ztuY?J㭛`{t3Q?9ql8sAμkbscW`^nXb $:_ML]W1'l"?fWsZ|q5WG.Dl2nMerCsaJ$8Xix[΢"G^bR .Yډ75ѭC E|MС#B͚`~dj[cG-d\#3d- c4죧;`T0StWx}bÖkuo=$O67aX? t_FL1z]Ͽw='j0q|i 29&_[nkbn!tN lIYӅ8sq<`Z`2>ly69mBf^/Ĕ͏|$N9ĬV2VgDB8p1pԿ0;a^x@Oy_LKAƎ؉gAbc\lj]pwlU'ejoA|tʳ3";~|tKG?p.ႋ.>8~rȥ=>|p_>+ Wҕ:WT٭+/7bڴYܟgW* .u8m|?;Uex~^'Uwܖgo=ovTUe=~/?p.b'Fq6#!8ss tr88q#W=ԞX5~<  l̝rӂ :qΧ$18IvmEJ.lX;DXd4N_*~y!Ih 'm1/66E8~w>+epq{Kk-0p<|𑃚gƀgMւ'Č!' 2\;T+aN-j@'3A- 5#?}Y}?:6F`}Aq_g^:0^!1ZzjG={cu*#m+֏jClqĶqi⨦uw} °P/F dTzz?q#16 B]Ͽ_f_qA枡3gmv'1ddbA2sT@H`a7/1ڬUsAqR:xl,su`k9ÕXCロ>d07) }6N6NGρ'Gm:H_V@hngupƙg ?}++.7LwͻlRk_1_{po|}֏2uL Ub$:1pl;Tի9[d=ڠ/zN{]ZUz[1[v msA{a|3'N]dab"GfX X6}9{YϚk}π]mStڿR+3e1-P4tWy1G/jAOw`w|9gN<)p‰i3P3@~Z#{ ¿n-FHCqc?` 2z\X lSxp^b0Q8CffG,Ƹc.K.17xᇰ/,vٰQ1?2qmʧxCKd_IN>6XE! xdu급. #b?D`V,v!!O\CS+ڕW%SC}S>l1q,%2G>`'tG|u I>,P;zd[]wܙ&ڂp[<WLTzNUW\nZY!/Ri T*e}f~ {J%{άLӾ~K7|s?v~)[z:k_Z}g+fyf)8tWg1En*[޾?]Xb Ӧ~usy0޾^a4g?Zrç}o\w Zw192ۘR_R%'an,N9(>!lW,i>zĀQ^=O> ᓮR>0Vnh>N,rldr&zt$ܒ0)5XU]1ȊW.3lı C1^ hcSMb&z]~bSE~)bCp+?zv,ՇhN(Pe3F|mRzꩧҍ7ޘzIp#`dd6dlXY K~W}]00QۘV]m4I&>jis6ވGJ}&??g.M_ҋ/fmִە=L|t,csr; b(Pi3A5 H]wuxBMDME=Zx}^(wOI\jo}l5ְg,s(.d,ęP|t]imMO+ c6̟^v?a 7/ȑ#cɪuCg)c:+ _H]qyzMƍK32K5^SWѣ}Af1ciVM7,zWW[,v뮻 /1ǏXb6>lZguӶxV[>loSf QW_k}n=uǟYjZ,񋊹+NHre|p"@ʁpcCϬMq`eˇ4)dH`m{uqPpp8>5\ա/lpz<1lR}89z&G<:CdW.O!(tb#6(CuKxdƇXa" ֋$A\}x4~e-ver ب;6"=3e?r6xR3W+!\ʮi7UE/Q)ږZjmlz.r[i򕯤9D^fa9bdtNi-9# 馟!:K\=CL38fíP>܂?Ks9gZhsݾ*l枿t+>(7`ߞ[Zh{zd<Ї~8_c=xhЪ&^L?Hz<6ތbVz/sXH3L7Ǻ {;ۮ<85v=nY7z,.Xs /"@y;tce{C=W,da>S3~mf/u6Ӷz$=CzږZ/P-f^ORE . i[jHC!{=mKIxh3dp`m"Ib -^$ Aϐ֋$AN<[?y򗾜曟[v&[Lkaϐ9Wó{q㟯rOw)}_J-Ix]beBh=[߶R,]g]}'yK3RNJ_cՎ;ٗOI}r#kWdWdj,&1e`4@#W4 E @PW+hמi,^`n? :S45uh5uc$?2lWlk~#iB>l>0l/!aБ OnrB1wx k&W}t˦$`_iX`HGatŃ8RXcGStX9*rX`;9Aడ?]yh2~&8tb&񏥹yL~;\|:t z7lSf &Eۛ~ o2c=lrDzI{h0>_|}lb5k>yŋ.?e8`3{[_PFlbmv7q5פ^[i ?n*+3<+DŽӖo~g9rD Ҝsԋ9%!oY:{gҰmGfn1[  _P@gCfBNC#&;N萰 8է69dbU86]XS8T_`&Xdx4b@C'F bĆO):߶!/#k@U N}<‰O."ǯ<]S[+Q~` d޲Dp[M'wnL85E) Q|E&" t@;* /jmŃ|iY(7*}pdnts97YE\*7(SfyMP`IkZmv`#nۍ)}vߴNvf̆ƛF8ȣۚ>;y矟8e:UW|Иc~:i1cӁ~nU^t)v՘1c+Dvn-xk_@CjU(J]Bvs J_۰BvߍDw_8dtS!BBv_wuǟ?P_~ #\ 6#CpsV1Yќ4 SSuMpl4&ځy.Cv>y 6(FIů\ X&@NO5ekn8Z P2\* WhUxK16dUWq oUN e&nbi_v⋹L/X8j)TNHx4[%g@JU3t/@$k-n-;YE(c:K\%R+&&Zwx@oaY2ܪ"fT{j0V)Wįmq'סcô-*U O|y˒;&m[qq[=ĭFǙ?qZ}16ikϚ%_ڨ#5Ǭ9G0:p?gvG}TЮ;B>{w|W]}M5ja2jbʰRK۳V.Fzg+,o@桇m/~1]r饾+ soXxyҒƭ6tP02WǬmfӢ,܃tWX}̫je/Y/>1UkWO/bZx#~=r߽gΨIk99KvLVlغmp=~ݽ*wgɧw>;ۃj'kغOws{/ 2\!m3c/E]UndžUM685lbq<؈enWe\Nʡ#5ɧ! e==@ HD.G]Wt"0ZP3\]o\/a6bD >6|ZD҂LAGM6HG p9 +b*(MW"8N6q˘Zl?175 ̯3"&:3TdkQAo[>^Tm6-:*l͆t:L9t\`TBị?Oq1ooY|-}9^{AsAf>k\YdtUWOW^-WΟw%{,8SZ5k7.nK3w[{ςlA5<6t&]siv}ی6LܲUSf}t[=r l;SW4iV:Gyt,tw_x>9|QFғO=mϐ3d]B}0oЛ|uԨQ&) k[.bsYs CWckux{ Di_[qG{)kj}/@֋4&?'g=`9?XKDZk>wpyp~To[jZW߾ }L C:JH ΨYW"8.j;? (Ƥ3E(pl` K,?5[;F_:uQ>PfA.6lP_skccrknN0jt8$]~M I2dĕƳbc#:nc#RQ:)K:lěZeW<~Y9OCbaS<6ТƅW~]veletRD3rs'tE&:|e3'/˿0e9L 6zJI2WLJd㆜MM?}&Ou1ud]uG_G>hWH\r .L؂FΝ;4<,rnYr-<+dځydWFn 6|ǟ~W+Op=Ү2y*5 #?[y{A&T~`-*ؕ ϞvRs_^r%y*ܗʗ]EG:۠tV[صVLy{ ܿJC^џ!c 2XoYc{>rln[¸s]wg\>Ɵÿ6t3ϭ ^gu|y=5!Yg9Fb&~^Q>殰G c*9g?|O}e<駵ѮgcMQn\ޡ1uTzhrIRu{\hrIR- ^4EII;4Fnۣz(#)){ۍ{\3xz$%eao}/\#l7rES둔Ccvos%:_v,hD_c\.̄?OckNdوg. GAG/]mTj&īvl-`8d2v<%@"A !T@9hp* K qL-+V1 R #ra>8b);26-W-CUl(/LQL㧾F۱6 1cM#3Ÿ|J7q5n#58d_΄ Pʪ.9?`8t<7N{㒧KyJFEO<5l qI?]AmV[ڢ!;'oy9F 29CMEdeT1kڛ#SO=՞#sr_Yy'|sXBLァJ H]_i\?w{ I9K{=^jĎ$^._Yfٴ&'Sfí~3-K{g:˾|u V-T+y6 3v[J]aҩʁ:2YwBf꡾{XDsRG@:wܹ}]mP5 |YEj f/xijʓ#zRٺiIXuu+BkL>nшם(GF9u!0&f&xmj&O}nYb ղPK,L\>:- GرC&:`M9Sv s7IɚZD2HdGΦ#dž(|jp/G jvtr(VA?D.gFبOndpMɁ o`XQX:?-x#pɚW%៾ŌԄR"J&O>d-1.psx Oo#7C+`ۉ6ϕaLo}y~w㊓Xu9@IDATn85OI{wf[2}޲qݲdo_agu^{H=ɃJ+= Sy=ww6mfi {;N{Ps*tg\ |A,{Y$喥b޺,*aWvqG˨yҋ}Y1CCۘva{ti,>8%m=?Å/BVCYeO0veŹ2{؂ 3drWg=ԓi,Ɲ;jm]r,\xm{Cf}y!t0\]T\z)W K+,yi=}礪Gt]|Qʗ=l>lBY(a↓+?jGDWF9hۼxn뎿_%'_cǕ-aH;<4ΦE&` 8~智RDO)>N<ԃW N p<<vH[SJL&¦:n.:,6jˆ6PʇOKX]yo$`oNܯ L! 6\V 0g a$O8t8D+bQِL>T86ڌ _9S -DV.p¨mϐ&=)\l^ AIn3ەʗ`Apӫs5>Q~MXQ^{4bemlg^hOV[dG=K܎p/Cv=Cf { z/ڭJKۇз|yYy|Rz饗Ӓv0v /һl!&vf~smz=C&ޚ3dY;'2C7ߜ]_ձbB,=RZDVr3fbPVWvRmg 1AlRD )Yz񳄘 TUk)]nw,=YBL*ꪵ.7Cmy,!&muZh!6K<~ʶj-E͐ng?K Be[]"fHq3%ZKr3fbPVWvRmg 1AlRD )Yz񳄘 TUk)]nw,=YBL*ꪵ.7Cmy,!&muZh!6K<~ʶj-E͐ng?K Be[]"fHq3%ZKr3fbPVWvRmg 1AlRD qYmc 9HؔA8S| NS;d̅@ب :Ajj#YּD G\^MtB5!0U,>٩A>؟ EJ &T8!kp8 q+?xV]bE|+Qq%bAp6|3Z&:mCZKy`S,S5Қa֍C?b/ۭ00aEC#Pl VkP~]5&%OeߒR9ܿB̴?b4;Y`>>t뭷M7ԯ`"C6h5Oҷ-;'pRG3t([`3d*6F`WltQjrwy΃i:P_>_g[*w-27>Ə?8Q8էA.gCn؆7>WzXw@ȕ|ET߬e~7sECwE`p gl9;vϯ7-P߭mӂ9RљG3&篩rB.p03 K,xڂMmW;T 6*,sXڨXjj.C$V#r&L_0)AU\vt:N>  DVS|&zj`XP^ta$:-?CLJ]t0ʁ6*+g?2;S5rL8j *mQM|ʉ\ʫ^o1LXV=o7 +)6êd)d+^`R ʘ)gK{jceX.f%{2 q% ٳP[n)lxҟr+.Oeo^b~aCq?4?{=?_; m&y'Z._'ns< ӗy'|ބdo` gڂ _=-Kk7cźmنŊL{BC}=c%7'JmM:ojon|^kBh[0+xӮ>O?!\qʶ&sғO=o@^{@}=\!c՛p]m;aǝmq:;Go5Gu]AWȰGm'IRWmU-1001eϊ9wA Ӿi ynZӇh[ ]6לṡrHkϽK,f9r{ }|%%^M[+s_^}w/~!)U zĵH4tߍ?miF|aw*"tEƩ@;;8Z S s]Ngِue(N2%Ɏ.9V~~aC>ꃉ6v-ր=cuaOjxjჰ!Cʁ]&㣾j`W;%`{HGePAL2jHԑ!)o|QcWɅO:>lւ_vG_ $RMyc#D bqYmOxĐK2m`Dœf 20YfW 5|HJT"WŸt=2=` yEEXW`A䡀ep?BTkz+bzߒ<(VDˬAY<&8I=;ۛsG|ϟ^ytϟNwl[+` \Jkcڰ-zigNF2̀]wf-;5ym[:LoX,WL|u_ V[}8E{nt=t|s.-,z}c ~nDFE.b/C$qPͳ'\Ŭ u_nIgOBY<zϯd>*zq%t*8d}u1F<]yщEl 2Ԏ“ ">p̑U;2>:><ȅMmV~d0)D'jbÉ_LJWA@WFcCؘ ^yLtXtՏyW8d65 Ԥv Õ~}p l]jb]amr!.cś,Wo0MR!#6@^xLc4!60i1^b%*9JD|B#k|(]ve駟1IU0,Le}^g䷍ru_v.g4s v/K{FȼްvKՄv>{CRo~/"ϛ11Njw~W+15<7wjjo?-y0,-K6ÈڐǧzSvx_c'{u;7ϐ,Y<`P\>N:,#,@_뿼='Ο)f"%R]hWf[Ѷ"v,Ty *Ю튡-A/GxB@s=Kz.eϐ^}Ůp\xw?lDPēWXᨁ&'%͍W㱩3%ņ)b a#aï:18 bM#C=bcE%'>j$O_bb 0xgapc!Cl}ć?uT'x?6S^b&ř0᪀9kUڗ%QqBO#FسINEOyp{[oc¡MTuF]yt]yr_`ʙϧM7Į~֩&?yOسWl1ğ+TAcJW u/얤6|(;|mҎ6/1n:o%a߲v۝a >{lQ(eCPƌ_[n\QXj<2g۽>v'" k{ԑv[U_詳~r Y9.h hF{Ǯv:m}qϚ͞!c\ ^p8NHOxcIzW]u{G>w\:ʞ!3w ^ ~#o(_05 BvRw0t{P#$׈ѽ(I w=+unn9tPjsO;Iwu]>5"Fz݋ZjWl",د2f7a":~td\~Æ CO>taȡj> "?D>ny=6GW} acVX[6'EƵBpOQلG'N#š` cS}r"ˮբQ76ӗ$?u=&\~8pK8llœf 20leï j'1K'fXXyյx ka99c* oT3ja^SpUؔ_6"RդCYj_,l 4iH2knwI ?ۗ^{i!g9'ڤчM/rZd"iwiHO<=tM9W|RV 4T;{ x{΢,J8)C[s/>&<0!cw؂[x>E-J]^m= 1a[BZ$4L뽷{KlkQ{nus9v SB^LtݞUo'k 4iRԂSvofXv"uSޣ;N)N:U,hcУpCWnq~bl`+l1ِubUȐb/W\dl1t!CĈQǬ/c^'8z4!7N9UOu"pc!#C6D}dE{LtY<喥_Yb0`LS7P~˘X㏧UWY!vՎ(jGgojv{@ۙ*+lq1{ΟO'fK#dχŮ)9pBWB(7K-rZxwߍٛ;9;w?j v=׏B@,x3&5ʴ?,Q޲m'YKƦ_N,hDϙNg,zON)쪋Mu 9.2v K>!S|pɪȫlGWz?nRY:P$Ąu6bɏL,DG&_5u9sb> <2ش86TG~lb iVF<Ĩ-ȐbŇ|=Cd{FUHvdŇ]f(dE9gp!F%wplC:_mјfW̤,QVqj3sdt6Ǧ998:x|Q %ۇ"!rp:-R\YЪ6$0š2z>9QRd|"ɪtdꉐ՗"042 AU.lŜ1ǀ>~9 }j+XKAS}e1ďǯ c-ĸvG 'NW!sryA*Tyy|OLكc)B k |u3YgltXL῜e:tـWa,+2 'm3{Ln}38ilYt/&LG9ŕ(cq[ϔfq>2X O)H pLiG̲B ǯ:I-l1|Ӳ*0&Q|rRM#gIbNS!b\a1pՔL.bȣ\ 'E**,dx:PG (KX>e'^9T]u"yU>r&h&qzMbeb`W n† rɇYm=Cf&o˘28g×P!@z/o#~| +7R^sg8`cWSl܋ ,t[qf| O5s |OZ{u j{#ɭ.?*oݶ%-&*"gsޮ~n;cXw gLλo1]x~t+e}u{ ?ظ}iQ T{NΧߟjAf;k sL6g.mM!ǦywWG6ᰉdN5RΦ8yeWb~.֒ 6#)?29!l?m j'DF_RN3FI (L S';Xb Or;:~}1&/;~<ʍx6$L?(N\؜!vew-b;ۤ!RŽ=xƵO;Ѫ>_ݒL +Ueu|t_ 1bDήyQ_gmEXguY1W_}Uz^ܯDYb}r|a Lomқ\,^y{hP]BzEm ]}G,^Tnm`xj=| hR 3GWEgWeG^=JTz,r:~m13e+eojpqzxذڶQp&e9:pHCD u8G%?gcyb;d]`gc?ŇΦ8O+ȣ|Q'Id.A"5RXu8dmEj /E"9~y!&;,EyAZW}ڨJ\8ⱩĪd8(pA3c)/8D!' 2\S+֐>hҁ~ ^,V\4X03C>#mB#صǦo!{},ρar 2c3c6GU}ӧҦO阑88fmkTL -an&?^TBeh>h9,K5Zqn㈦ngaGeh>hn SKnuLG4uxӟtW5 3Omiq&,qa>h_9Mgۤeq;5Y obq)1ȪA2sTĠ#}f_II dZӧ ׏QpRObW>q|ġ+O C?:ZZCW^llv$Gr> 0M1C'NZjZn}l=>uMLJgs-U ,oU)\%)0$-M#l1fc?<]}k(0y " > .`~ӄ2[՟HpkUO̊ëfmN[n5YH&d9+`_Mv¨GcRϮ~wԍu_Z(w矾y;v_-Q3O1QrckoM묽NZ= rpimyJokM,5#PElx1/vHy]xA>ppՑ.v͏M,AԂУ<>QG~aɡy}1r 6*ʍܗb~I@ÕHv5p ] $k@ 84䈃j(8lCmR3M8t 1kbvMኃj ԫ"̔G<8=@wιĞk[-c8>NFsfQ}SYYh4vZi-P֮pȇ%F"yc2ޯx_Zh&'s3HW`c)߶IJpl1iM-KVtuCerM*(t:*GD;gw壥;'4{[> s(Bwȅ1QOV6*X.}yD>ƿtnŎ_1?f묛a"m]y9[[U4F":MXq3)&ꒉecDO}=W pxشaL[:`h:X\XRE^)]~bcX41A™ Z[ftcǦdd;==y^aIbN_OOɾ} X,_@~x2E֓Vv[{ *ڏhoѫL!P=~==_n4^|$,MK#gCiz]nώϿGQ8J?;2㯜 8QISPzc{ܥh7e(n{wA,}ˠ=`1^w :̞{9ܮa1fԞ6'~3KScNãlMe 3?X@ќt4tKV 6]a 罪8CfG,Ƹc.K.17xꐟ a'^:X0!)^Q1?26#I*ҴIbcC0`lrQ0)V93,8|TA;'T 6v+W+CUnpPߔioL8هF5Y#`X(x0 C =cylOHٓULIZD[n_YۮF&Lgy&wހ\rIE"E߰@7kdhۢ ,6_\\mL/xZCcAs+^YNs73qle=һh;t;}/"S_Y8rӝqοy?omJpnS,`WT8:iX]!/xqBWp*Ŧ MtN9(8|Cpٔ<]Y8b. "G}$.']m>}`z&VN2 $ꃉ^]I'dw?L"F ̳W'4h4 YL,1؈c r㱩&Vة lO y DⱑxE~X\ʏ`kG|78 C *a+Şa|J7r/d,T8 ĆiιY؂ 0Wؽ_Gld2U[5Qn!ή1ZYiK/3oq E+mäǿUh_Wt_>J~h( 0 uju#ck$F< ywG;J5<1׀IHx8  ]ݵ[ɷ)UM92oY=h6Lː3OFǮ[C#c5sïe #M)`QR^ɴ%ε'\O260#C9L~|r AM(NÄ+!\ صh챳8v0ѱp#X8ŘX5b{4pf.vd81'F>'ǯ>dKsNN8%+8 204#^#`9Tv4;{t٥L4> Υ0S [f5zi9P7^6Հ Cdm&z}8v@)A}ڢpN]raFdDWb"kd7S4RW>q`Cʧ bN):x8'F)ǧ6U,&:NW>mU ! DP.UT^A]STUWY]}u_EA k@|ν;g'q΁yNs33l""&lPd3dLn,#Ƙ臘k"_9dfGv%[(s{Eۤ*+RܞZ>YQ&IPs> "/x&`d…6ba,zƧݕ6;͛Ѕ,*HF&ū8M[?E}={F3ٶr$;)SR??\2tE7Pfi?0moW'A]ssx3/pοv"?Gcv6%M1/XDhc_Ĭٻ2.&Lt&0A!MJkWîXdkc4Q}]?n!\Cjr p|ppd7“b$GCn hDaۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;om&gwQ7¶MEQ\ʳCn񎻨Daۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;om&gwQ7¶MEQ\ʳCn񎻨Daۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;oq#[mL{z/!(Am*55*MuK2|pEL@ +PWNMhD;q%#k|XH\PzzGSLeSlW,2.W߰:8L(1:O f?>n1;`^6> XrUtvp`CdW.O!(tb#6(CuKxdƇ%1N u-^% /O7ħl &EhLf0`+e^/j2?Yʽ.МrYX2ٲ j8*-ەJsV$KwY@BbS8F~WY 4YiM+N[%-*ۖ fy'-y,ƿ=zj t-^% M)CG=]KWIHƐ! pu/v3ԩq,^"\m^o}k>1M eh nvFq t-^% =ueʐQOU b=5oםбSX슑;'~[A~6|>d]+V#8C|0GyNu>26+4VAK$X'pء ta!tm4CI*]vLJ ^~bmcv%H68$dS1p6rF N VmK'+̈́inRGz]Z(cM\̝77׿a- 3ƚ&D*b/N ?6R{ +^j‹ҬYv˒#JQ t7̶V,p}6ߢg2We|"&|uOuP 6z^!_!r &d Lzl5~Mqj,*Etn!V5uj ~;3z!tuYssMlMlV{<tŗ]w%5yؿ<9hvzKgmGSs;3l~V U 0?&O=#CG&Uk[^Zy0pq2|WaE!MX=.;9 A!tT_X-4ڄGWtt t,QW~88ʃk AEWka'|఑=CېK6uš5QbT2, t+7| 7܁ }#G(3O~zⵉ &T3 c[~K8Jn 9=w@'S$"V0JL< ݿiΤr}/dcB;w3\0wMvM/y~iʔm{L~"*;wn;֤Yo`TmѨdG*6c!bByQGdwޑ=0 YC9tl'Ї;s颋.I ܊%o!8o}G3tZqCJ'>|:v2vQ6!`_Ϋ>,ea͜ UP]*aR iɋd`oX Y!uUɔvP!+C>Hl--GGJKE #x9&%bٰXߴ]N|19ȡBu䁄W[fڄʌ էJƙZW'xF71Z6;n; Y|R&hh} w]*` MX'lX|z<ņ OD[7i=7Jk'^nb~ 2t+קsyzG)/g/لc}Þ&|32]xa:_B z?nRѯϊC;'?He)?s6!sEvn喥rO.Đeӯ_*ordlv0šo8μ?1~oYz{zK[~qMzhU .~:Xtd|p&@ʁ]6rqq<؈=ɇ 6&C)F6jhOC&GHF@ HuD.G]Wt"0Pir"ʅM~@K%,T`D >6|D҄LAGM6HZ XqQL3!CDg*Vqhg3MJZKϟ~ٓo ҆H-oN7xC9i5LoqZ} ״+.tӧtXb6dFZ}5<7l[Wlf>hj 7\n<ϙV[}u_)u3b47pMȰBpGu˄M7dcoZ)=*rxq zi7J_kzkyLӦG 7OӧMKO^=JB*c<|WyK:Næ}t[ΞW=W^?ÃsfN~'ھ(nc1ayקFutq1Z?`@ۢ#dpU㢦'VU 0o ^ߚ^@1\ ߿7~N('Gvxm\tq i!Lpa/vp}1tE W׀cb,5cra#9&1+1jj͉Fm˯ɟQK"%F Y>qAL"؈Ŧ؈ih@NJz8:~BGVEM>LqbzDױ`im҈$Lg&Nh3&~~w=@顼iqZVbanV}k_K?\'i~^/xa'cy/ڭOLNШX^Iv`zA=?<^cd޲v|3{ئS68LWXR?bn>sWfK.I&8=Fw2?&dY79S9"=o>kYr~8r_w`yoKLȼKeU8oҚy֖=cz`>`׹3r &rip19kV/t4&>٣1LH{i%~X|:{Ͻ~oM6Io;loWsg.]v'8qE㗌ih{)M&IٻdhrIR.#P;Z ^4EIIٻdwG(7#)){vh3xf$%e1-{/܌]2FazESK؍]0ѱmtcE,&%:>5YJ #ra>8b);26MW-CUl(/LQL㧾F۱IvnWU] Ϙ2> LeDM"ن&3ծ-e3?1s&dnGkV,Xtg_\!VC;fVoFm^;idS,VȔ1\]/_7XbʛniV\~M.z؇w*]o+F.R×%_mEj=6cx;V[Vg\/NoҳwNoDZiV[ntO$?w;|MVܝ.eom+[z^r#9xNˡ~uVKӍ7đi:OX-K#~)ڄӶ{^o)i6q첫,-N۳9J&oi [=tŕnɎeِr@'d'V|cUߦwɏe{'>eVvu_k?/xWlE6!sIm{oOx3t[]uW ?*3|5pv-C4>oL_\5qv琶V8?zs,eIkqm-%|${8m-?g矞[*Ɏ$-6t&*$Pb AY:Pb7sM.0az&>tHOrN$L9;S*pDWJ]zi[}KO~ {e?f:kxEĶno?ÊޝWlbG_>qD}'m܇~gMdj:c+?}1Dz_^fΉ'. #g:̟!s^{3d sl6i}[ґGaыK_76|Ro/9nׯ~>tiy_moKyZ'&U)lo&QȆk"WUhb3y\2'2|w؃`[rݵץ}_r,쓞L78X}UmerOGUP]]໑ZNS`N_-[6b?p3Ia_pi(a8:zm6b8\򃝯'ב\KCl\LS@pJ+;R_5TN<ױ@PL Mu`O]t8XlґQp68PW>|E]rʣx3ޜ L! 6\V 0WtMGWr!U_8'ȆLtdıAft&GO8!jhr &rFmCx yUF6"=)hT絑"eKuN TUT h{K|R< E(՚"NWb'X!c 3W6!CM< <~1QQNi^8[2y=ٽ6?)uI;Kao ZF;h4myMȐp7Fη,I%n]i|꓾FkL.,5L,NG}|kl53;NG0y6ǽ&d2db[vy׽ۓ+,Qgj?^eoQ,GyW̴2-Ka}‰ɱH~6.O'?*N@ʄ<=DMYz񳆘 T5U)]nTw,=YCL*ۚ.Cky!&mMFh!5K<~ʶj#Eg?k Be[S"vHuw35Hr;fbPTmvR]g 1Alk6RDYz񳆘 T5U)]nTw,=YCL*ۚ.Cky!&mMFh!5K<~ʶj#Eg?k Be[S"vHuw35Hr;fbPTmvR]g 1Alk6RDYz񳆘 T5U)]nTw,=YCL*ۚ.Cky!&mMFh!nLf6~q k^&;)0pOC+78]_#c!v>` dplԀW|!E5XtrW6'1'Z`N rÕ~$RR0 @OÇQ4^U,Xa!p`GfS]q;bPS,9 #7>7Im@p¯\ bhB JLt&2Cs~ȕ|w+O^O#F}.[ ɷoRbA+ir.26co1 #cϐ&diTO[LgŬ 2״`ܳ2odϛ1wǴ|e$޴sd,'o[jk,]2Cz~γ[m-Z^Qޛ]>/g| Im5G^ ]nؽ&n\ԟ7o^])"~Gv-/w9P_{P^^%Q4Vw˞]*U;ȭN/ۄϯm/>מŗ?k֬O~~W|q3Xa~㟆>E |u;Ъ}fhoCW?U,c81n?K8rd8pοcԳ凌A[kSHHt.ɇ.?vt dr\ʁO1`zN$,l!d0j 6]P}0j\+v7smbkq'> 6`z H "ⲣypBVSO  ?Ą #QnɋR<>ՄKjPWN#~dM֠+Fç&j喥8 VVMXWDCQL̾&?DtNYLxGkYkA2Fpf%7,L 1!s*g#"9mwj. g+%c JVI i4e7-_49\yت&o}[NƟm]r[>gD^~̷;=㯾jxiKx׾r[?;jRZ&dbr[8^)t[mB'dʞse:aW%@v՘|R-K't~8}ovB༗=ً$> kDQ(S4`/.h#ht5}YϏq1kH4 p ߿c ?4k LYHuõlZbOd 1 qcn\aC>ag&j<ڣau.&^5LtRŨ&?>6ILJܚ`aW{r+^5A"Aj,5ؔ86@P.&g}#':1RLt1"afڄt&(Wv 5|HJ*WA`pqqy^>|RŪ2Wm&f0[DNl2av &["rrIy~B{/e#6)s&Ȝ7S>dRncmI%˱3l]jb]a~BH,^],Wo0@dP0bBK.oYUD1bF6m&Tc?j4V-7pҜW{&R?N{茗EQ9,.7ig277}ymYQ))5Y.i%z!jOZxU1MB{J r}Ks/M{^Mg>tLl19dy,n^Ӑn^v ~u~eiϽZg]uux,+3l"ze[`wrrwW:gC}*>-Kz.^mkļeik:쟥-2zc e,e8joCm<фO-K[koO:CC]7g|/mn:VL^3Ř}RT:1n[K۬f~`hunU07h$|P0~6ppA[0]{.cm֓C}h8k7T@12fkxꘚa8FUARxޫXx"i5;bұc1dWV؏oUaLxpoE4q֣†L|GM8ln(¯1p@5188Dne>xr vM$E G dKJ4#bxldr kSl "V96t3y18 bM#)/vL1I9QK$vblX6>b 0xڤDe !!\4QdtJ IiaYV7,y3ªV2 %{ۦg'R$@'>/D 4d姞)m8coI4{ÐgƘ}>= Cm7}L7)?#wR{m`7e %M?cbO~]!0z[Z^<^ W;ྯ|r?  w0!'e_!!ч`5XX`! Ho|iWNl8zA^i+>m!ӆ g̰ 2cOȘ_V kY?;nyG8~_8!^{Z{5}~]M#qO&jby졾uې迹+g,͜i2ۄ1Z!cϐY>2dB[^3duL?V*5)c0geV#E?{F0Bwrψ;!#34F37|_kS[2] ßS\ئ:d ?4fۄg$ БCײ+6}+ck'!$,>\q$>C;:y 0WGW}"ll+esxi1j\!Mxtّ~:;"+YɡAƦpD]5Iann'l$?u=&ֶ\~85qK8ll̴ |umFH1a\R7IWA,V!mm1 D)md}Am>#=jftC쯾^J#n[ZZ4/]jZ^|Lpk;(?r>-fݮoXV%{~=bs#DqxA1k~~+>Vޥi6qJ ʌwBf>jo`D8&DM@:}EN̤ZO\7ts:>/N/}Kt7O}߰7Iplbᨴ*^>r%/xLyK|Ӽ͞YzݎM̝77o_[Qs]wgk=WU%;sz2_|[[3l yS}`Bfwч3|fV[Ю_֖UvϳT_{}=CV]oYZp;ֱ gurnC}*|?ec;cmb, IRM]]8:zw; n {4?M  _o ?;9Lwj1jF0ѻW:xbI!8:pG7ł6ap`!:>\\XjgC&|ԱCAV=#CEVnxlc_X<:b=SGIF=ق_cb項CЉ#ʣF vǼ)Np6NiPCo灓 rE= CGLmµ8y-Ky'' (vݑv t̄ λJ[sTF#/bS1"Ĉ?NTbB* *X33Ê6!6=mJ~Ok*$>Ԧ\©N=i[{ÒML _A&Au Ll/='lQ|?w˭[CcIKkT_k>qM[q@Y%8}~xGgl4#z˭yޖ=+Kl28{ }WzNpFӻ}̯ l"L`,uUv˒MXc>nɫ^}r [T-'?_|gc=?oY:Cbe{/d4~qWOj׋}{ oY"|g{~9`6mւi{Sվ/:cΎK9o}ߧ-COƜjX1j얥}ҬyT[oC?Og\`qۿ<| Y>Gۊ24XϲE+ 竡b8pѷasJ/_,hQk9);X˿W|||0ǣk޲m\O2 ʤ&Av(7<ǡcFN)쪋Mu봃 k\d`!CX#KVF^&yTOyq"FGVh9ŖD+AL;Qg *BtHxdKX39QCma2'#<1#SM3`ɯjXHu^LPv ibmQNp#Nڂ )V}P^|3By2FU<$i+r{饗z1}6ݟۤ-,J+Jgp1mKv~i3zL0%Kh-sCLJ [?uW}kZnrir6!cL]C?HJU߿tH\K+V_8d|ͥ&ěn瞈6`e{Q&o_OZ9b-<<x)aLm`9#}eK;Cz]j{'>i9V,}m/Gd/uV:3ӭj tu햢]| g[#*VAv.&ZIJsvcWw<+<z=\g@-\惉3~ߺK k7KjorugLrݦJ+߲4ĴJO}ϰ2]f~8o~i1х0Dy+xH6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-lr,mSЂ&*x6-?e ,kWJڈЙ`"FME5-q=q*6pWmq8$I87?]sxdœ[CV{M]58XOl+ T ?5Pt&ָ/ ~H#&*b~: )Ɏ+^v ŜvX3 B}Ě;ʊW]86!GXPvkLfDTe(9\x 2BY=&=8Xt- /0r]X+ >7Rt`A2)NcyN&ˣd272L,^d^n]`%嫓,elXE4Bu-uXN nUJD cx>mM0D"U`9?H;e)i+ۅd9|*na'q꫷҂bsʙ7o^Z{`?7Y*N7޴ L:"2۸aϿYn鱉6 *VaﱕFj\Ӻ\aP{tcwqOd9o]~×7x2tL̵}a65'ox/ۃUK펲wxDW=n*VaoőՌH[.#XDs=n*Vm,C8ag0ps1zIxUv 4߈\aEd1bjշƬymLNP2rS]I~dظ@d &lLXޕH+>Vqj3lʏMy+{O:6F^]s#7b/FV^4 :2DOFG'Yk.(ɥC<_i"L颬G4ԇWWj]6U~l\lz5\k"@$aJ>1W`ו2e"{۽W223@'?6f#?%TCF|nXܪ$=9C: n6cӞg?w2}NwͶ(}3Gw& ٕѡa?Vȣ?p cſ'p?K +DXvґ/R---GQHJCQNITc)r5Ŝ1ǀ>~9 P[ ũrtdrWZj&zb\ #ϐ &ɇd8 ]jRnC _UI fB[j7f,BP`3¤ 'EIOzRh]vRU?hyt3I{>m6ma)M-J WI˳cl{gZ .C/ްveǴfp/tYgcWKv"skʙYG Qo|&oe 7F 8p}?7Y3Fbp^M~߿Bfrp`hBu3kMl*r]8:HX8~8~QNjaS-0&UW('lE,2~i_̩zCb >riL5US2!ra%.TTXFhWAu]&Pd8XVq傋Tܲ:j*hGFEx@IDATqzMbeb`W n† rɇYm>nsey{ ZߧRT neamL0koSL䲂eymJOK/S݊Y4[v&le {(G`(w옏-#jY!P?N;h=9@%{ni}JS7)x;Hq9/ۙnҙMaOBkod1·py?eBf;2dε$W,pnkR~l?2&6©&:l'/m_q1Xb-`!ɇM}F&'D^Mx6!s]/.+?&qMZpj:͈_I$S(6Rv3AQcc8Р0uByᱳ%"$w׎1d>(7v |0}8Q9CO`uoMbćo g먹و3.%<|t^8뜭jwe='͹w?d#g=Pwڴ"ZZ OHkٳOx:_O 92[&fnoVuo w_sSz! VNO\uOKeZo|"f&cXEæxgltO#U<d<w~6w=lSԴYx}v$,~s|qvi|Z/NדP񛁬/xK7r>?v w|/CPph8ć K>Ɔ_Ol<1aSM{Qd8בks& tME8'N8④njO,׳1~<.d0䇳qfBMqẞV39Ghr<N$IǧX^4R#UCֆ.,\i)q灇WMv8X89hbc4ǯQ2pkZ`yh d% !8x6dՂ+CN&dcb d$'eɜH>:$J>/4u~Mpnz $+o oBm6#cfu[֎zzjNט L4 qcG4 ?MQIDp?'ߋg(25kf#,pp7||r!s t0"0aW^b2JbNs:1Ϗa|%d1L[Hg~1ynMu(nl>83$2>^בe_?as0~.ӷ> z8Ŝ+_xG%Mt?\yS8Q9eWnt^򓀆+j :>d7vM:YrA͋pR}t (2u5bbq#YUMvrMc& N!bM)^:\q_P7Bj LٞD\xJWڶiǨ>+dސ&j4z/hM2bA?fXZkO)FޒmqP.:-{g<940 wd'?Fps=r뽬 t͍I+cVL=ꒉecD<\2)p'< 6F]6pCƎ5u׎vA.p;AKV+N V}>a UCGP!Eiu,6$Ԉ'vFcn4si!^g&ebjt05CzzpHБ! GǎM1؈6%ǜ# 6ӎŅ{wTWʒƍk?nsV@y06 >Ab^cc~Hl3g,CR" ;~Fg㟓@s*;vpϿ,Z甖27Eo m3z20⮃4ˉ Zeք(&=xɊ+Fp|ޫ(2^`qBjo>c{ŵs ^ԀC؉ vlFE͘6S:4#Ib(9UA_7eaSrg0UL*x r"d0>Ղ+~H\qjrEP_*tdjoʇmEMhF-2;6P"f|hP;zdXN6&\&ي&^KF#ya?o_ 76& &ԉVpMज़D,8X)eS>pđFyld p>j+תg]]$ꃉ^]I'dwt?,"F#$VcUD~:GF L5q <25AAn<6d*lA';<ɏM}P[W"V>8W/غѯd N_ D 2j0>[Zw#mƥ^Z[短6cnKZ(P?Cv17Hxh<H@)cl,Ejţ@a8`cu16"Q0~01:PcnKZ(pq??i(ex17"q룽,S˜H#6iFu2:vRYF#[~դ.׹p:k;:,6jC+kmaʼn# Ȑ_}&?>Hy]9"Fa•h4YturX,1&:VpG ;D~(:rQ` d6cfrR> q%7؇L>ՀS"2u6>rX>mQn .k#i}5U"hID1E5Iq+8~!SSMp1F~be'V <lSNrjS.aĕOzl1& .rjF O3dLn,#ƞ`G  3pzig8:_#cW5n_?_ٻ|fd2Ag8׻p|lx0}4)A,]bSxq0gՍ9#`HCV.b#VJ8>8u8xl~ F1#YF>^$~%A~ y#1cGMu;xd*L6 ^]`!8L(_b9e/&]?>SlB_{mrEuTZQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;om&gwQ7¶MEQ\ʳCn񎻨Daۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;om&gwQ7¶MEQ\ʳCn񎻨Daۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFq+;om&gwQ7¶MEQ\ʳCn񎻨Daۦ(.p!x] mSd[.jQضh2 \yv-q5x(lT4<;︋Am*WrwE 6MFqeI|e^cר "7u%C1/<6VCюNH\95 ,׵c-6dWaǏ.T'l&`wiq`@q|j0u{n~ 6S=>vH`UZ_'CO'raW}GؐEC^#3>dBƶ֛TFq t-^% =ueʐQOU b=d]+V#8C|0GyNu>26+4VAK$X'pء ta!tm4CI*]vLJ ^~bmcv%H68$dS1p6rF N VmK'+̈́inRGz]Z(c֖˝V}g wj 6|P;~j{oOz]Zcaկg)Zۚ}gP0(e8_ Uοtw Vm\OӦ|ud6/9C #kbli+`MP?NȘ9UT'uT[X1ExŚcU,販>uuV$^~dFGAa5㠢+5vSA:S{Е!lC'?/kbͣ6&8dnYbN/5+g:tVGfQf<#dori5pN'~rn8/<7|/:lҏU0TKF$`UUH1aơSK bĆO)w`K>aemᇳiU N}&<ĉO."ǯ<]]yll'%'S9M̙Pe U˜,WZT фT] B]*=+^TC[hR6GRu+Tjh -wUhBx P mJMHP-UBܣ *@5*Uh{4!UWBrW mp&WZT фT] B]*=+^TC[hR6GRu+Tjh -wUhBx P mJMHP-UBܣ *@5*Uh{4!UWBrW mp&WZT фT] B]*=+^TC[hR6GRu+Tjh -wUhBx P mJMHP-UBܣ *@5*Uh{4!UWBrW mp&WZT фT] B]*=+^TC[hR6GP_G`@q<2kNM`$ x&|tMsïzT]=?8]oOu.5Cv>y 6(FUXE;_qؐlb!DTS%åpuHXʼnGdt&L{R]*]Y~é!lMcoڮj'TQplAp@«-3ox~33ѕ9;Kx"W^d:os2' GSm_}\?2¿M/KzTN@J tl ~6^zQA6DAIBUBQiRDWKPY{@ς՞{{fgJݣ??o7D?7Αߟǘ_ >c,sNZY.kl5须juXtd|pk NMb .֍8blCp|rmWe\Nʡ#5ȧ! e@ HuD.G]Wt"0Pir"ʅM~@K%,T`D >|D҄LAGMHZ XqQ̉vOxC:CDZ37KGGȘ1pE,B_F F?Wnc&QYW"8.jxbg98aEp\oϿ (YW"[y)Np=˵3I(p,`X.[7F_:Hǻk;2cs15uF5ѱ1U_9Q{UCumN0jt8$]~M k EJN$|ʃNDPGM7sр(^%6MqC}O-t+?'!Fu)|ÙhQ_ +c;9sZ`<$gcMQ@dؘ6v}Hp4E$){7ڍ7$8fccwG!MQnGRRh>d)HJޱ1-ׇ ^4EII;6F~(#)){ۏw\2xv$%eaC/܎#l?r}ESۑccǿ;Zhr;wlOۄ ;5%(`1ѯ/qɵ8]Є\P4")̕#rMɃL< \ ! P>6H५BW r2/}t xt!Ģة eT4/"quJC jl48H˦X Ń#0+6#;b˦~(J^Փ :rWh;6=!`zpFgL8, X`hv`Z6yQ"GO5RX8rSر蚸0!b\9Q Ç,,`O|]›&`QX:߱ F8ɚNVn'wȮT#݌R58c0M87Ev^#O­j0?SA$:Qc!\#ߧ. ԄbS-3GWśi8 8ܛ+!8uH W,$ Haʧ)7q'F6d#8$6T_tȧ&W Rj"+8aI;d&RHEzq=zT{zmvY;Yz,[Z[ڪb)Yzq]BL*ڪ.!5K` dp,ԀW S5XtrD GɯL!';5 Ǧ8 X`bAЀȯ|5N@6rڪk&' p(ꊫ=I>qT vH~|0lb u j Cbk9h8#CD742(+}u\Y 5,qyZ,b~gr?}¿1},yy~돿c'¾a7!s?ݟ=VXbX_>h?~ y~=K}wEx@gE:kslˏ]>qzLK9)<:b\É%< Fm\mkXڨXjZ\aIF~M:`,R"u.|  8LՄ1aH&7u[~a`mTW/~dM֠+;1Ī&XYz´!t[qa]i Mh*}gm"4{s˸uH4c __sYU"ǟErtH4cX%ӟoprtH4;^ŵ.;׳,Ⱥ3D ' qcn:xQLgfk<8HQM?{u1a⩥kulȪzr`WŨ&?>6ILJܚ`aW{r+^5A"AJ,5X8@P.颶'|9щ!eۈ sM,<22f|5$75k4I\Z=_" .f5G[#&.D1lR|_'Iy:{E>\:k<'5B-Ϟ/r<'5B-Ϟ/r<'5B-Ϟ/r<'5B-Ϟ/rz??{L|)T`{:]yK6mquȁȪY׾AOQG.lj#N1&:Q,N(6 ul k3/F +]j]qjacB]?x1IbU?L^CؘԀSZ.j W^o!x4/zv5w2Db elxr*([1c֓Ӹ'P׬ѰY .`Mۥs]Ɔl0w_ߏ099<eid.ck֟59Yv H Y==Ie(*1!>tFXR" !ᑉW.aDnɜ@hL-M΀%M`!Ցx3B}A,bPG9'81j 2XAy!8gΜSg菟4!Cm Z|DYjSЂ'*xR?QC %Ԧ1OT.6->r,)hA Kr,)hA Kr,)hA KgЏ?g@{Dr9Ɗq=n"m-}8e؋0;ԏK"l =8X?{d;dv !rS]ZV~dظEd&M4ac.<ؕG2\1SFFgQ~lʃMutx+{O:6B^]s#7`/FVءp^D# &R1tdF) O\QK]UxdL}A,8vDLz@#]6 ݪvA.6dI&DPK}|&d2DEY!#6ʳYhi^36r3ww9k ArQ5.o@`I`^2ˤ' 6a)֌rLOWվӳןOߟXGvȿ-~ >,Eq]cX>$\e6"!rpG+kad-VMȲYX]F@#B}Hu$>vґ/R-($( f*1\h,`YbΘ\cT6ڊ-R~pT_9EY:q 29c?c-E=1w|,Dü d8 TRnCwݕn4xDL:M 5ZriL5US2!raJ$$Ѯ,CM00#/qڰ)BǕ .R}rNr&I"|MгlG|ę5!9]u#Sz$ >|BO[%~dgycf1RLuFU`JQf'&CY!y[oN?v8ofn3L|sղ ser 9xD:sZUW[=dde#ɖ=4l@H~?:?ۓ٥u)qԟO)Dߟoȿ&7EŘ+ &vi&dvq8ג\?ykH麛kst:E8l"مSMMtYt-N^"bZRBf;2LN8mB溜!^>]V~| CWM$JJ6iFZH"B  (Le U,1'6e cǯGჄSʼn 3.v,QWNlAD.Uܻ1pڌG><ͅtucI(Aass%U~WET|wM"{BLJK/TZguҒK/f3П'MS<}f4? ̙f͚;*i_s{b-6մo?ۿj~8^dT30߿J6;Qd8Eks& ],i8()<#^LO,׳18<.d0 2:yWm¦8ພV39GCA c.F>bp"5RXu8d-Ej\tZ"6"V1nhl+7Ey@W}ZL xbǪ>nLwƀgS=Lp'%PdƎ0 qDtՀ1\}馛nXĽ.fkpPFlbf&-Dg@<ldǙURW_}ct껡Yyn͉5wazۆ850lC?tY |4mѫۣߌ;Ln29cқ& e57_OCԏ? a5:Gm#?8aB8h;j3M.utY;ZT,pp7AA&Y5C?1`D`î;{[+4w 䈵Bj@>jW.Xj||}(PWR"!ٳV8q: NE4P(vGDp0Y%a҅xa%]y@XAdWnpS:MTwΝ/Տhx8:n &%Wtʹ SaL0yE2(LJۤ7OsNfX|ӆm5-QnCLjTvi?L}K>~1Ǥ%mwZC:;S'm[쮰{M7Ivgpf-_?iŗs{|d^*[ޏ\xA:#=⨣N+RurbzmL_~yiŇvhzfuF,'~%nG#0dHͲbnkW36E=Mrdׇ4C8y S?ew9"K<܌S=}ǟ] R߇ml ,\aG]/lդ X<1/vHy]xO>qՑ.v]XHmQ;Gx|/<\’Ԫ*'~& Oy+*GODk 'kҁΒ Rn^tM<k@ &84䈃j(8lCmR3M8t 1kbvMኃ`3 !̔GyzB|S漞xG:)F>cze?%Y,>v`mB/MMc<̻֭NGut|s2Mvz:lr5Xq9&RiZwS}?.}8̴h\OM}ku_gd?5*t'ßƵ]skF'Z|,ŠĉdbY8)ץ'q4ޕ x^mlk!˦8|Ȑ|Y:m_%]q?Wp$TUձؐS#\iG Mz`UdžLrO{ɫ>(«MH֤S_/U.!!CG/#;6-`#ǔ߳qup+j04(񸺤K{jsE%Ans2@IDATa.\I/m`&Kn41Hry iUWguN 1PR)rowf[w&+ /aX9srʔ)i}5H:kYim. ~_{R:OD__~pcx?Ӷ2!3mf?y޼#9* N;5tA&KsNZ}\.di Tu]e_Vb~SP[GAhO L?cO 5UXpr(p\rRuLUJ3 Z2,ngڡ`\(<&kHjgr-)Ig/Y1t&cQ, |\f 1.%17x]Ra'^:X0!aW5c~db۔O>ɡN3$*&|lCX5$;q]F>*'~SuDW?P!/BCS-v+W+5jrKK|N3w<F2k9D>V1GWo1]k1 5s([3&\ƛxpKkQü0y @G7\+Z vʼ )Ǫ޺5\0&Wы'덵}޹c>=ڣV[mo}&dwK9.ewni/:fZ@B㭟UMWZ9liS#mᆶOk3Ljyl6ԳJ묵niH'>Į;d$ԩ2uOKyo${yi5a.mQ-`RuN;.ZktggL+cSQ;G1yHU< c*`!U(P_?C3>F@5nc[<*?C?3>R(!U(Pq?|vfxbnx8<ލ?,b #ͤ#kZd.ːs =C#Yc5sWMR QG6tM`gQ~]'FmHy%Ӗx-8qR}␱!R}\+0 ODм4LXi6>=v]Ǯ&:|6 8>GgbGs0A}bkQ;}zw-@{p"}UO89oXP7d +t8(Zx?Oj2K%&d|j'_4}N/G^ 0pSYJn_'$G;+8؝/~Ӳ-gDڴӒ/xaZaHQkl>9%;<̙iNtoNΝk^PxmM Yf($̱ٳf:&F?;Oyᴪ=6aBkx;.´Z}ُi)FM'of}Wi!>{ݿ{z:c|[el82v'N`\&f?~sig=3iG4!V;d26% m{.mG,Hzs+]gΜ?%ibz35?HG;eT)>J] }}7l簝i^ǿL/V/-Џ??ڏI=)c ߟfBIlQ׹vbӂY)S1ȚPXAX#YvE5VĒlE׮Adm&:]9EGrP(7\S\صY4a5"h,EŢTu@j',~H9T\XىU=Cg0ɏ6],&:NW>m>,ȩ 1!crKxEd 06G0Cxhygsh#DM x^ayK s}~mμ@Gg~mŖKj+6ݐ\`cvo~L&C_>ϥ38#P3 |Aþdl\ŗG>/]^[5o|^{T5{VngfR}7wԄ7tSq m({=mRftء_:iI>y1WPaўk6=gG}ki |X'>Q>cN[G29tq'.e EǤ8ҷ}b{J}O}Si_X8q3V8ҤIA⊟ڋlom쓔f[~9f$qmz L0yoMԧ"^P 7X?Yi׾6xuG9ע/=PG5ThEOݡ;={(?;>"sc?f<_J$&zE&O &%EֵaW,2Ե1d>n!\CjrTp|ppd7“b$G%No閴݉Bw?Ǩ?M?v~{OK:kٹ&K&o:q`L?ͶۦYu{2^js]v܇j;8rzmP;:|u31TjQ(j7mU\P(pmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4.<;5x(lmj4ŕɮ6u[F'!>dp"&] +PWNMhD;q˟쐮c-VېU_q?:`b.Si#<<jEvCfL(1:O f?>8wuhP< .vT'EC'5TR_'C'>xʅ]i;DN^lbA֢%rys"2k~|tCPE !-~}矫uA{Dmdt-^$ 7C7+xdC@[iOSϘXx珐+>߽0]uՕi)}Mfw襾2|{n2iR2.g3~~ cI{s]vmvigu!6r= fǪLq 3qz,W:]k׻}#iO}z$vQ?5kw;.hDwZؤV[mmw>-~q3e8\bΟͷ<-T}+>[_s5]9?SLv>hZH55.~G{ g>|[ݫ*19Y2WG6o>a p宩__cX7<6?bh8y摥9cW45.vn6ٶs<=zb#^[{k|}.ow~iֶbi?^,mFeM8u]ꫮN/cS"6gt^$ %vPȐсՋ$A|ldAOE ^ 2:pӵz$- 8ZZHKA!CFzV/Œ!=]KIx6(dAOE >XX2dtࠧki"I/ 8ZZHKt-^$ ڠ!=]KI`bɐсՋ$AT2dtࠧki"I,[,2:pӵz$jBt-^$ e%CFzV/RmPȐсՋ$A|ldp]r-sM OMt;:~.xNp)kz#?vbUO8\,S_}ȺVGpxE1+UqTN}|+d XߍqE?  kaNJV" .<>dxLns!U,y@!%ܐ0j/;6dj\:n'dLs>HBP68DFke߿qXdSO买OqKQ*-.klRl7;\e1rkBZe|7K}% ,3\}uz}6qq!l88*m6CP{]>sʙi{vwȬGy$/ #vE.?55GJ\{tYg{1=&Ob-ʻN ZI0SĈ<$Ɉ?tɧ8Mل5?eC=,l푩0#6p~c |g=#"4&8E֗e fml)O2K7x}z"j(Jn-|Ksf΋7|3xv7pw;o6C&Om/w54g=;k69d1e Zym[,x/v&T2>{m_ >{9o{:\7ΙilfۣE3|˼;#vW<&|.mow05GҞkOl/ٻr]O՝Ϲj+HBD68~+Y)8&uP _پP菿|fЙjkuh-Uw947;dv D#Z4Qxj'Rdb΃Q^tt֠fvԑD5o,ZT_vS}(0pŚ1p˦: (0k]|\Ad0R!cGW\+:{]9L,yˇZbF+mC/DN,~&_ĒGmPMp=2p*ce\O`Z2mfRMwΥ7LU[:C+8.򗿰.Y7mb׼z~'? 5׶#oJ[omX'>|Sg/tK;TV^ioLS!c װ sJGQ߇|/p{&K.4-?qywo4b;>!$<`Z%-ev.pt#ca/{_@bT+SzKV2m$v^|$M\qE{EiA^8/76]+6 d#K!C%j$ڝwΰmYmB2g]4{Ko QIvg駟"۞+kl{qG\۞3vkU֟o~{'MLj#! M==Zk&]h8:qm aCֵ| sGNC#'16S8q"B=#Ԓɍr8Hm ^#WT_|JE$`UcTH-hN-5H]NWl>/^ɑWCǮCƮ/Yzɥ‹c_{ZxA.þ7ˀ8(i]~K/J~vW"=۾s㍿)i;fLxV](/*%!2Eax {[nIf߷.i'z}>#[_^vK^e/xt@6|MSN99}6FLZ[w4+BtUM'ӮBڮx&~;ew9}Qh5wlG~6fK}7 mb=y#jwpMJ^ziZ;k;tڄ9v /Hm]y/ӄE&_&uWLLp2'Zzo&@e˭|_#i=t<G-w5}$>c/H`I1xw%Fb]"!+^C-TфT]jr5x&P (EC4!UW@(ZE)B  *@1B.JjMH/P*wQPhBx P PR_h+2M,1TjV(.4@*N<%O瘔6dUWq UN e&nbi~vW;\c#v Յ^m9&d.IƙGdt Y|R&r~o֔Lє.4qn;dxd0zMVgBfMȼ&d>9[G) #=/76o96.TQ?޹?.kis"&hV_mUC!^ļMP0Sy;DɛLv26!>-I>ʕ1jv&d #K̈́ƶMkfZ}5m{滖|ܚ!`/{,{ ;d#FD3f'`cy0_akȞ ǧُ;TrT@ee˕m}}>_#2TZTc#g4K{dӟF;sMKUw5Plѵ8׳\2kCwm bё㋋lpj)ve}N\G?fd7rh"F ->1 |D1PDE CKQW08'&$.pzra_y5P䒬~ K='Xlp&:m>\凃NMHZ XqQL;!CCiLg"q3펆"F+d̍bhqo0.#}C\)+]1'GIWl[nYMm[03 3οd/͟>2o { f" ۤ÷ kеU(q}uª}QhYM6I{gv h/{dqn$2nNo6XoGֳG_l1{_B߿&(x/}r  k_;ZN%{L(!5lfqkL[OI IZsL0)Lx)58Yf\uG?Ob\H{On=4&Y˖ifJr;d^D&~3OKr5wWL4#cwYmox]33f;dN9:w/nQs_ĻgٛG6zk< ܟI+.z9WBศmț=c=*qpCZ_cΥQghW"88j_d tq q}* { 8\O,~7 K'Cxw 8p(Ҏ\,؈>vsQM,c vXW5T ^?3*I2)8$|ʃNDPG;nQ u:ū5āz8A⨅_vūMʡ~]^D \ʏx3glft^D.rsa?b-t3#`B.Yr+ڜkJgmt 30ɏIo%nUR[΋;%;,%i(,8qt?) ?.y Su 1_;iw䘱3r'O5×qS3BdqB綳ۋg} >64;א,WCw侴=Zy^LZ{y2)2|zUk%R7cJw2C#˶K8.HGؗ|]r%i>!cwȠ,Wxgd;dxT/A_SYfv3ԋ/iw%-*{q4c%'w ;vy?>:/wGKn3oE>4]d\r']Ai2EԪMQ."dؘ~L:rIR_1Xi\[KZ(B)~Pĭ6d)HJޱ1ۿ;Zhr;wlwG!MQnGRRh>d)HJ.ƮtcE,&%:>55 biROc.PA0ij!c ^ڨ!|"^Ce`8d2v>H`C/|ʅmǧX4Q^3Wɫz;aTG1]jmǦ'#$Ln`H>񌩵ZᏙٳJWQ{'K$mx؝ c*CE+ t}l yp>m"m49 X>mpڛe,]#vGgcyeeyc_d3\ӦmqdH~tHol_wN.ȿ17} ,2w1oI&LgS_WڻYN=&X3XD1)5qD;c38{)%w6k#Vf=dyODŽmBb%\mo{[Sˏ綸ط~o6Ld:cS،g}3;sʴ߾m?L)i2YH̵>{}9gsOz쥾vMN;igYm|?OO:ɻJ&/ Wu 6t'>W*ւL<{~vivvkw9廒TW0]u3bX_B0i񌩵׏?FkF_'qThu_CYDǂ$]dᢊEQM GرC&,vXXlʣU Rn"d꫽æxdQs|fIjkC$TLv8`,j<2Xt),cQC9)Nvtr(]J3Wlԧ=!K _lf.ɥ88&lT/ć)wlB~8$6*6;yrw 0jZ^ʗPD5D8P<89qț~Glt 1mMXnٸsj"`B ң =`~by?WU|ezU =![{wrGwݻ׿>si]7]ܵ[>C=g9an:mŸ̸́tݩ nsvO~KL g>(YeU|/R{I[nU:Go{xhjY2zĦn#i { A_w]Zumݖ?=0E]V4,!Ý ʘ'df϶GUSN 7RĎ=mBρ;; i'e'ݴt>{}WxT=䟽v&dcfOJz< +`/%>ZuW{W;t~K#K)]zml{yX{^؞[| oxcOgپV3}?ޚ77zcOt55#?MUF:X=ݱGǟ dž~ENU%hAO=-1Z-ZBgږu*2Lݩbba'X:< H~ٕL&¦:n|Stdlԣ>;$N}Am&jQ>(LIޜxX&ƩCl` a@HWppxW>'N>1!'|ĩ&q,8SA'rȧ\1H\Q!!3!^0aTc!uPC{<9,5.f'j`Ad(}\UMH.^פ .0o_=_2鍯CNױ5K%NMNȇti\ P^uN$FbM^| L8x"t8j';5 W>HIĂ*W>uG c+?xV]b8b#Ä~,dTL׏O^#TD'M?cU r)/lsL5xz Ϳ 2_ړB.֖o6 k0 `fαd|&۝1Ӕ " Uu'M¥aXr˥mI^x}'> edק3N0 ּnWoP$쟽&dU;d<}\g}h^yJԧ #mɊER_{AvG?Qg6?Zfui=/锓NIteGo;Y~tѤIg_a=o;)b}S&< y?2j/}*Yt 'VHM%JwMF|fcgᏛQCǟ鑼o֭n[+K;dC?sӯo.ǟu9gw f0MtC>g> 6#h>(V׎Ma%>3Μ9iUV/;xxzY$)i}fodZP#a|}} ~8ĠܟӟmoL: 'g;ӥ!m+R?ٗx]&V\GG6ǎ. t bpAB`S ˵rA}tHmT,5u-C$V#r&P0)AU\vt:N>  DVS|&zj`P^ta$:-?CLJ]t0ʁ6*+g?&k|abU,,= GKBY8ڰԁsڙ4Ì8Le%f{}p%k#R2Y/qB_ Y2G/>!G~ȝ2KBf鲴WW¼q5m+ekQ_~kӥ^8bL#~61\t'/~k9>kD$NSeG}鮻o.j⋥ 7zaOФ.A+C6MHʾ0wӍ@Ml${oIEMn^zȧ>y"LHG뭟.&@Rng>@9?!;s]fo?^7HQ o6LV5Y۬bjy;ӕv7цzU:S3`LګWnq4KKosi)DŽ~̈́ q+Ƃ;d&nW}78͞5;|e-bn汼e/{Zʹǡ/;qNnHȳ.,=wh 7ܘ`bg?峾q#{N\S𪫮ǵZ]ôJ+u a 2dTB؂$eN ei /BP( O/^rwmV/PN8ъ;dvhΦ\rⴿ|A֝)&\8LıK%/6;1>hnd xpڃ.YXbU jჰ!Cʁ]&㣾j`IV[c. 0dX UC q(Ɓ5XG^&>[l jAn+?10ȊBr19'yQU&Ԅ@/ IBտ (KQ W]AJŮ&H]*zCI~9ww{/ ΁ysy~Ν7bC,H#͉v3lIFU70PCrS3GkBD}mio]qx^AOLh쌮B~7L>X|:hF? 9fSjLmHL1X;X)sgKFgd*^}L[sCU~xHj;Q²qD} ! p2>PzӲ\Ќk;QŠT㬉'{b-`d駟W[ߒ_|4nf;*ޜsMLUL_pAO]1C: {t= ^iuh'UrqjG{eڣ_n0v]q֧Ym! o_l1̛oecɳO?n1lqyNa@IDATg]XO9%\sΑs?Vy;εkc6}B' Ʊ h#]'^h'v9tǾ䁗NƎXfʼnv1ҁh`GqW>hgi1lhي"O 1dL@넠^qt/_xq1&Ș\.r  V\HD/3!R;k2x"%pfl%gM0ڔ|y'khH5igX(ubTQdl *Z[UT1g{_:_2#_!+ZE=jd5K*:WjҶmTVB0SNi7B]q̶UGLX9ol/)ʗ)4й*^FĒ͵i^@c~&_k-1}xѥ/pD*D}l/2vsU |~0ch(_lVb{i{9 sCQ3:mf3N7DUL{&Z^ Q*̊QO̠TorK)X%puןSjj() k9/aT?onOMSQ5yn{4;1onuM͎K{7 ̜1^ao{>%&=h 4tи#}dCf]%vW{t1bK/9':^7˰Qۣ9A(v\GΈ&l/6 Vsl̏ 7}Ł5탍lke\u䫘F6 ,a梮5= 7ګ.olz4ymKq@lz;eo4Uk( +3$5%;VlvC}iXq8?o6pƸGlα]:5= aiӦu6H6OGƏ୹k^{nV?'|4y`u ItM:F~#Gi2淚&OenmMl>n瓕MX^k~{U?:JMkzx3#T[?im۔OO񤃗 1+glء'!;xc<Ѹ^m^ Ws1ƳhƵB 좞^r:ƎZE"#S~bBKդl#ojdʧ$=y=FXz05qKv8dsM4Vf0Ѱu̴I_h1FG>k]kFXh ۳Gm2m#=8Yy|m=h7i#!{U5$ZhAr ߴ !_d뢆8} ۢh_/cm9R)߳&?%LD7|s{m aKUM~?q9 +P5矟nI?0bֹ#l~Wkw?h֧\36qu9q/UJʌQ//:U){+)U}F~?ڕd&QF2 ]G'MzMLl$x6&ahNnpLl^']9]q=[[=q12xuc< }!h|bC~h@Њ m)Ċd zc-;h:8j4`d0 p:1ࣽ`)>v**y|Hb*Ey4CKO, a),bdu"Vr]>%rJz֛#6̛8aLɏ,OlJya ]f<99Xm)𸥖Lz;*sf;G*sǒ AX| Yݹb=0{%2+e8s-w>c{K]{CىhdŸB?uM=d+f=Av>7Hm8guf{xS~ZZm|U h${kx[APL>b{Www]QH/^SXuG{)߿%1)S=DR. ~UoEC$`#SC4$A ##&XSrEΡ< dqc %ڏ>At`mW`/Q>[ZЊuLY tFE/W,ٚȁj 91с=4849˟XGG6D9Wx Ch8tw̤\(B!ʴOϔ sJ$6 e=UL#-KLƜo} ~kVn*o}KQۏ HE d0X!JS@Bw}uiϧ˧ Ӽcm810;g0)KwL) \ HQ$y禧OLƌp> !~Î;ÿ~DZ~ 0FC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RC(p ##K8Di`>2RCʤ(_Y2iҤ444_ ) \ ۑQ-vl2Rҁća$XcZ,yc*_ +20 Z~`ã''Y!r`|%7F_@FE"ЫC`DGF5T6`lE?S1>튧ɡt/9b̰4\,L<0Ď+/X5:|[58Ѿ-KƇjbE 7`z ZfI'hs"ċƒJDL-S4a" &Sst뭷zB!Bv<'38mBͭ;tםW-K2(? rdqoKÃȅ,#ǿ6ULm<( Ytc-z@mQW%?+Sosꫣ״\,Dwu_kF4o`^>%AYBגos;VP2mu'MNCCC=%L#r&dX!LN&bcc,+=xdG5٢IMؘ}A80C)2ƏCH˟>!6EsCcO/lJ0}h@bC4Dh,h_X*rxWG4 ZF#='vbP!hh0V~F.2hsMkYn iOd=1<|\R<[+lVsU-ܲi7uQ! J֛eMSL}gRgDvާAYvTRu!ZDZ/^bs5e1ge!Wѕ$GH˲ՍqQ'ZXWS/ewu_wu߿|???<\)W\ye|k9[|03OxNJ_RtވrY!a6Ut2`0$9qɑ1>t]bp9)c, CchTtIJ5zh4<'O @#V<4%aCQ1ؑLIMT}SQ ,zl9bT`ʏ^@ @'TmEs)>v_1x(21#ѫ?c.E=ѯS2(_0OhR)NQ2mO!cnADs0S5dE3N2toJWop|fP^nFm߻*COG7*OFJ{T[/j1ϗ_/pT)B,Ftk}ʷ%PQxw߿c_OpT%rlN/o#߿o-Sŗ\@͙go[?-ҳ^_B*@` ÐV=X~vLW$_:aa双C6F;$7rFϸŘʧXTٚ})XGTQ,CM0PPh?XVq ؒ WRL5)2ߋ?#O41d?tZ6^9m/(X_ h@~(@~K{l0uBqu' lщnǁGcd9)ѫ}QlsS 6G9Щ]v:hD[6VAjW܍aK538#k ϫKr$Ϙ!)eymReeK79 H\{M{x}k t{>04\*Zrɴ&gOBb]x7{jjs!Kl Vouyaow_k/hˍ~_^v?~"\} wXa:z}~Nɘ.xz8e^ʘ+ct mbdӖm{R0@Ct0(͙t4X$ ~~@:); v||\.hl`쬛q&d LX(qס8ml`$/ف)[u?h 0tZ-4z<0|\P$Wl01h"4F ?3ej'vk\ĂVCOv&d% q01~c h1[lmtK/MM(lhXxctwʔ2S"G-4{iuMUP1OL(z׿2U2;Ϙ%t0,`"@A3FE<6ld\qP@G?EO[<9[ATS|R.R>S}A*M( HN]?LGIQ<JEx@#WHc#[06Z'%aْK" 4 b6ʃL>OНlxM}uRs3>bb׷%3łҕW^e "m5u2ϓ>c뤙&gi}Zh^t~E-ӦلiEbo[^~ȚOVx-l49c2,fZy7{Q Dϗ=%W? (r]}տ?7tEBwNW} _kFu&EG}C<1]x*_R^[/O 3֑IftHZ^(Fդ 5Aq^FO(\-}e6F&}ڤW  #Ӂ2|xL ]c#7MPf),U!MqwNʓΚg63DY0SVǘc&B1Ӌ/,tyImAلdɔ}0ngq0Iߛ?nsր> v [TfWf6տ_E6ߨW _OyOw?~)_A?l|LG]vR ՟* h\S ׽G]ں|e {?ʘ~{ƴS=l֠G2aZ*i;0S JA#;0 y*A˟_@~QX8|cl5%@xlA (T/M|}A4! d VRbV @H?:D|uHe#|=T^D[^`BDr= ,?[\qȯ:-,!;q̙c|o\ &?x QZ';p}9gZ\U<59׼ kʹ &ɇc糕2\m/uCALe7FV͠6Wq1.]~X}SXrX4 WYfٴR1(ma|w[}uu_˪*L&9/WҶr.v۾f/{s9'͘aW[gmBJgLF~, d|C'k"*t&'o)6X2+cW&4&6OFA`:j+k3RHB6&`dS{% fJ"=XPǐE~ FrR8l\m6ボLGFLr-ҫMV:0ë/QOǰcV@ "f|oH_~EQ`Z&]*l(|OʰRf㴠M\dD~m< ,- Ku}^wMJ1MVDhN=ok<~ iP%h8>n%s5WZeUuGޯL{4-bir9jakP ;շk(T64@\̆j]sHBch6 /סbkqT14@\̆j]~15ҸP fC5axf;/MMi"?_J~R5gG?G[vܘa4zș` =XG&n蕓 )d"#7iKkV?b)?~ȰP~g 'V_iv@r&[iN:c0A~|סv0y:h|KS'&_ Qrb66lᘴ|*?Fpg[ro+U=d100_g{|::]s5i=+6{ ߶jMm^J+5vvyoؗ|=qt{+Bjz/ $U>simW2%Q]w#Ӂq8b@s;4䇭`9&rP< 0VĒ L~8Qیt{0rŀ(A~ڢ`L^b!AhHDWYH]L HnF'i ^cPŃ I6*#Nd#xcAO~ELNlChk@+ J`` C=t7NRL*/QI<X+4M,/6\rꜟOD'2dC${;nhOH뭷^Or˂PdپFM-9.3w^Ւ hkz&d>{L:=P"mbv#] x9tѤ*rfKxe`j^ҏ% j^&_{B.Ed~>SU8T 'p2_<(2em -@v2.8+n+6h)ۦ$qY!pK]AHFW\N:$o Jy)n!m꒬bw7zOIbˬ1,)mp ,"+V|^z闿e` MߊPR K/z v[4KRq [DVH-,mAH6E' [ @ʶ)8 e\pVH-RWlRMQI(,コBjᖺb6m*NBaR mSTq ˸ଐZؠ lPXg-um eU2.8+n+6h)ۦ$qY!pK]AH6E' [ @ʶ)8 e\pVH-RWlRMQI(,コBjᖺb6m*NBaR mSTq +%^{QXZ 쑱brxŠ (7224ƇҨmD?=<>4 (hك#`^m^rlڈ/(`_ ucS<@{FcO0i/#L |Fz*oK(Ш,QKj'L{ީ C}_2:VȌ˯V(uB{l2ذWӖ|Ddda-BGX!3=#7yuLhKVL m)jbf %BO^`^M[R!d}p6lثiKjP"( 6մ%5_(Jg %BO^`^M[R!d}p6lثiKjP"( 6մ%5_(Jg %BO^`^M[R!d}p6lثiKjP"( 6մ%5_(Jg %BO^`^M[R!d}p6lثiKjP"( 6մ%5_(Jg %BO^`^M[R!d}p6lثiKjP"( 6մ%5_(Jg6d33~D.G;x$Ә^#4G=';[zɯx9>hKNh0:b`GmQ\|e<*?4uTT+hx :0+@O@"be a-[ .Ac/=>W[M2a1'R%6d`@rj|$6 @'U4jX<'2ƹH݆upP20@ȩMM]paX3ª8B$x+=LEޔ3egD\,Ƽ]wݴ+XHM6ifS:RK7x}zgq}Ї>d(Kŗ[u lЌtm[nYtavD&z8};*G(B>Xxꩧ,&Gxmݞ4c r.n/2ir;#=o8#dobhֲMt{Juwz6}wEQ {;mUrvdldTye gih(\0 ty:볫$]עy՝"9 ~ED႑d`u?^߿j52!MC&RD K&&d@%{k"E/&[u(A/ bNڢHKN xr^2嗭FYiu^tLAhl"U\ac\qtcXTx0A.rbm@2?^qh4zF:/?x|3F8Olrb#K A"ZK*ZY5p3δ=_e  ;f1# R}_Rbc,Ô 5Qe&ٰ@-3\_w߽oA8ɏ~c|^+e6SҦn3_> h+dĎ?S[|&JnZ[{n=PvMCC+c&S:ī|gM{$Ni5t_MܙV_oHO?}7`?@:[?oMmTk$Gdk)?ma~xܗ>խڤI҇>a?λ'ws qni{!@+ on~fm<{^&rDų6홴d{d2~V#Uw])4.V5''7M [\fs]skĉC&Oz=p:K/$oO;a޴ksyT(;sWfW>8~dT-?+-Zja;f~52#IZvפlBFOC'J;nyR *UV[%-mj^~$Q/E&Lx+c?y[]cƤd02,kz[oYg; xÊOMLwuG?yshFoaNʯv,;r(L@jˉ6!3&JU5X [>4dnsJ]{]CLbGlp&$tT5zevW&xvd4 2_&DִK?kS'Hԧ ?M0D/jP_Fm>o1̳Lz=C|"}}>sg)E޲d~򓟖U&~=VӶjfM6-3_|хSo(Pa% އ]Ya_98i },7Z~]OzM@6Nns+x }s?YRZb%< }rӷY&H6*Co9"쳏Ymf؄g?9*>Qb +ǥ=dl~ǩ|ktR|m?~;8o'Foe~E_d+jN\ժ % ]xn8 u&ۇ~i_Oٹzz&_rn;jFNfBwT16hk_ӟ4լe;7Iֱ5[oj?6!o^ 7rU†ױ5{S_?F+Wf^0s죻z*gV }./oN+qaumkCdi;K8Y,1šЪˁ|сd`r(rɈX7I ×1|XmLrS9( #9tHA&{@ z$*D,%G^Wtl4!vaʧɉ+2W"hKS~b` ,H|q$&dx 91Wxȧ!ھ0Kev,E_ꗾw(U+D̍873N`G dv#,Lo݃rgo?CWpbf8![i1ahb5&6~T:&REzGҧ;ۣCtg>~6]*#5LNC?L Vb|2|Kz*oNG]&'^UBo 7|?y:묳|51p :Z9uH5}&wn\0L\i+oVg>wj:6s 2ye1aϖ1= k^{#K:ڪG]luZ{u~+pXVÑGk.7e=$|T[!euIܔ1^'Xdv0q7ei",d:y 쑥5J%!7<ΜKi~=|=UΣJ6WY|(9eQѓa` Y.U V]v{MbaY*u+d!.r]w_{B1;Jw,NV+d!.rMȰ)vv0b h!L.v0-e~>i)>891Ł kLc`YGʯʡ#U</&} $(vj$Vx$:_d긑 ?@& "uR|;x0X'87^rC=(/r2#N`&Z81C>H瑝0}ƌyydv@2,lFQFd-0)^({@IDAT5oaħN o:gMQ|=jL X͏I&t + Oު򆺞Ӣv6KyxɰB&& y9*aߎEY4}K*`~<&7lM~7ھ/販8Χ/Oh橷 Y ?㶱Vo{JZﶛ]vG(=+dnlj>fb#L0sTyGӰmr{?#9ƍǬ2w%Ogz6a}o 8ti!VO3O>>I& RZ`ҤI&cNc3![#KWoY" V"Є{U G٣@]}M~=!Njuӓ7;M>)6mц?Fr <({715Y-c{ K.Mk2;ﴓ1Lϱ~epsO;C0gz/Ф&l&׶vsE .kgmS{E#Yȶ9"]Gm]-/"]WRTF]rO(u%Eemdտ]-/"]WRTF]rO(u%Eemdտ]-/"]WRTֶml D6v(`1Ǘk2' (l)~=rNɉ?9 ?2@؋W=[B`/}h jd1+cH"%P &;%Ue HƖXW>ʏ?tėtێNȡi{R?T%Ib?_yɯvd{=_sbPc6M.iJ(d&N5]xy{Q*jVʥv6NLȬa{t:v]?bt@LV&;C6#G+d.G+dL+\3\vMowd]epƙvN== ZYjk!jM]?]WSk4믻kзf<~76M?>,1F9`IUG,8G _Fr G`;{E~(1b\l$3blW{Ɂ,U֡6xn55G`I[0FЩS0N86j9CE2ˈv #K{!舥x`Lp :h]lM\b &l/D(wlB{83$apfk4;0jZm٦_*[B,cFz^wdieXvX%SRrX2+ʡqbdgWV9CVn6_=jsfmo{k^!s<Yӑȃm z|)EZ6Ɉ%[a}=[VkVjl{Ȭo lld5dyG:}𾨽{yM__&ZtA%cVȰ]eeF>!sב=T2XgqϘ13M]ym܏ٞ4=L#K&Ml +dqۯ} 60mV~6e#Olk{nugB*GM}eRu~nm98&ovo;*nּN>;զGvt6Ųy6Vf2S:7R|*Aa !MnZoV>-yOەjlչM[TӢW&n*h\tןSmEEvqO5\#C( R+U2vxǗ|ŗے3Ş9x_neF S؇tj;A'x)'~vj3<#?!rhrLJNhN6j<2cYqAL|S`YeYRS馋lЈ-ǕI.7Q/1S8l⛽-s6Y(U3noZuUX~TQ$38~M} !D֓te5l]v9dH؇푥[ercI{<>k0(+RK5%@-?-WkXYVj[&#K#Ku%S=&<ְIa ،2G}cmeF;&dv~w>*siCBzdI2UI0xy'ӛ޴R{ymM$k+_!c2ڄ FSNI]tM@-(2Y|]że2h?/6`Zo)擽%{~;J['>o Vhsa:>:q{tV*X\!C3ãcl;rogoW3!VRYk*ZMT(̒ɦ"ukՀB+rWί5mJTm;Ċտ5Q_SMI}Tm;B V뮿ƂLflmr1g+d?0149-2xNC;hllX;rWE9-_xbW2D&dc"o {ڂ=@<`ʅ-:Al0((61+:ZQTS|^[@94 =I3u?S>0:b)&G)j0z +zR\lgΜamMEHT2A/d 3)do6U*F@~[?4\3$mWM|ɗL hD02>3zV Og60R)G҈yd铟WC/?q7䧼hTѲk'SO=Ѓk0Q|BfG8&OM}?n4rH*;K/tC66o[`剁ս+WR z}>m{]CgY̺S{xiYCx82G/0&drXN>Ã0?@r`@c SXm-cCcw6ʗ+7vtjc#bbL<6}YDK/P{ huHvQTll0c1LX(.lD<-=qG`(ڣ8}S1=&kS #9呥yPnReV.+ڭMDA#pe[0szOj]g&*;08$VHR5ܵ:OWvP~sc{,_l+y̺gF3k3`KOf򔽲1ۘwҮn/z8s}Xϯl^~C|J<*}^{=g}V~ay|tzy睗x[푧->pC6'OJ[sMߴI ޚDY!^&2!6Mlq]v {doEϛsľ"#?6Q&]ԝyø0AB V~Mʁ\b2QT%a#Djt N>jK2edsMȌ$l!C%@5Κ/t!dNfO<?peWb mٲ椑ڏ}`|v(x4U |d[3n{e^(M0(K.De}nZ;V$^9<6]w$r_=L'Y6y{;Czq?4,F3|O~6]rɥmJQMFFoH}ӕ'ZVx0᧌.k_^}u1aS-K/t7x)Ҹ#͇|幎M}mult2 +3>n{؄]&ʌ푥!Y{?E-mДɓ E8H{DV{ش キ\OtW?!c2G؄o}#Gԗ<%_S<6dګj7Yoo,maֹo?Xu/4&G{wI;kXsM1HڹvnxXDM,vU;%->/[h C=d496jzAK'`FVH^~jA cB^'=cx+1qe@ƤBvWShbF^'~aOȇYl!_S|_//a(go0 %p1hW뮳,y!}V$%|!^ oPWIkFw{1:ķ~{bglUyz}'{)~JIlEǦn.=3}&8Ver+dl`.ѥ>WM>G/ڄң6sx ?*cUe]6ߟVYuU% \ va.I|8mݶiޝU`{*?GFM6'?<̴ɯO?=SFɼ[ms.#KUIƹ:̳mӹ&oMXV\i%ޛV,v%-TD^7NUͅ#62Gu<:6F{y;7󰂝}wrw}T@ RIVB0_ou;}f4QݪzTSjjjBEp[ޗM]*J"~fڋ)Fao{lk!c *5tиNJ+^$r]lzH=˘ d_cntȥ'2x0\vV~t}A*MHq5Ē!L0D 2ʃb*?v(4#bh!ڃL5BƤx#KLt&N|8t(&ydpM:—_>`YGZWSҁ m}e'}7-oGl K,D~@: b =ZWS6ScsZ-?=iaرm}kT+cfj_J[q| =VS=_o\: gVs`+u),%TCG]wkS2Ц[t?+I* U?)[vC.r&bc[c6-2 18bh,{†9 =tGIG|Cs}-:?D>d/}86#_zx72JsKAQ !vQORd/9t[?VcGdkbMl=&L -rW")N0rAYJlt` <@,!㐍?d|f4lNL[v6_zbN>Ci6=cVx'ǑϽR YMQ^um2Zy啛{U迚]z ɀG޴r~KE~M0:};v-`ڐ7}?я~luSZu2fb ~׫ jJLW/j4*535#6UJ_]oRԛ_^EׄߎCXGwI83zM0v/vQV zld~--GǸ:p%l>@+B+68_,ڧ|+3W%u:x|c1b Ę烏ة!7 &v|` ?4v-=h/XN Zl~, ,df i+xPǜGNmAŇ|D>~y"Y禤%1$3'|_x@/+ps4&xLLeWWN̷[D@sSҒ[%OY榤%1&K|,~q#, 1-u3zء5&@֕TsSҒ[%OY榤%1&KY>C)=/~p^?Zȵߚh.y27%- 5\enJZ|k)ܔ$&DsS)iIL撧,sSҒ[%OY榤%1&KMIKboM4)A6n}8:~KzG6`+k#6HP3,vxlbкx11xrQitm75Aڑm>kPɪǜ>:xrgx±ubmqL9^RUp% B9:טؓM( %e.<;Y3Έ~߿*v Y偲i*2USS/˥ew{r6 dꩧŃO<9^W/a6VTYc!Mݙ=9؄:Y6#9ώ9˸@K_Un_4$M؄\mtVؓMcv{4vͼT|ŞMlB>}Έα&dh\c*bO&6՝,~6MX]c#P% N_ OoC8؜`djXGVz7Mܰ WyܶgY#syYǷXY6>aXtlnClXȶkɉ]KwG4aБ!e> קtr [:#GADG+nC,5Ϧdum!ڥ ַ`ÆL=eb!l.kcCf3] {gz4Lf<#,_?V_櫧ܱZeۯۯz ѯ?,tgDEp>+n{_=ߘl9\jS[wM1ܩ qUXl`%e9Wԍc-ښviw}~Abb tpP2vI#_g1*$( vR{Z#s @cˑs\*>%r@[e~pq7', ?vH cϵl=9n㈁9~28 ѰR04z kf;kZh!ӤiZ4Ԅs=آCߋOQX;K Ie\-:~ן~όYү?s Β6%9OEЯ?ypL8kMl\YWB㷎9-g-0cbtI,EϺ#:D\Εc12!%,*d(6:n008XK'vaMt&y5hWϸPKMbrc[?25WG-d\##2{mdc6죧`T0S4W׮$!xȶC+yu҈)ޯۯ^ m. Яy0F=WX;ng3kLZ g-5ԏu7kst:M.Κnt9\7.+.R;m#sgdrBh2rƇx}!rȇnMC,XK&[ #~(S(7R{K'66ǁ#A0 sg%"X|<}cBlo 2vU奫+|]dG]0NUyǟٱ2Qxx-u^zUqce>Sk^WΗ |t7_+_<\ЌN)Ŵp֑9:'4ne8'xkXֳ98<  5dPA0.9V<}yqHBxSGt Zp<|𑃚gƀMւ'CN6duP:A8l/ ˊoaX%8ׯZ,Ki,q^SG6=E1°PKdqdS}?..=ӵ$:ŬqdSO|]ȗxb[Ȧs{=Cg͚ۤes;| >! 8dbA2kTcH`agR%FNVV9Ǒ,uɑkAV7v>3\:\pr||Ǿ,F&%!2^Ɖ[(89PvC'`8x)&ċyq@8 Cͭ>9 0f 6Yߑ;v(Ck4 m}[ eoI\/OrqļXs,Y1J{׿>畧~3?'@z5[ 7ւ<1; ֘Kͻ^X7U.yC7xnj:>mAԂг<>>K!?gͩk)'[On"68a*M:9yx :ZGw@FJ y3Fui'mNM:|Ɔݶ7~x3Ȃ0SvpxjT8))Ki8+LֹI%eiP&p`l+3dәFeϱ-%./F_ϩ|rϿ??UTp>?3Ghe냣=\s cwqt1rq"uo+>2$6f]8㐱^.6!CVuڪ_vg?[H+v,7$lD ; qĀq{m07 XsO{kbXI>槾5Б!#wcA 6bc# 9Ņ!]tPvM3e5n/Nq{eY=YLY"\Ͽ~_?lV?<6Xf93e5`?O)v*pڜώOޱ!^{f} :W6w#X8ҕ12X08b!ۛ㲏pn/X;]/Sa'^,vmm>jQ3G&.|[}-ɵ0$lQr #!,F`Z&`~Bځ_BC|!q\n;pej7a;;)?Dp(;7ւGE>2 CSSѺtKϿ|VqSƙSʙ4Z<~QűACϿ4(:[9NCTP3́ן<*u86hP#U9$D֗l@  ܩܜb!X7kW6\"ycc>uJ}z!:zX&erۇ[SK8/vGLmEÖƛ+-q <2ݠ 258b!>]?1ة O?6rR˶M7?}~|x|opvӈZC4B(̐g+Yd>,ׯƹNy 4`7,8lFw iǿL2Gfe``7,Ce292(`f(Pu+̑D'؍4G_ e&8<6'nY< t>?z}v~e8Ħ=X"F:_)BG֟9ȕcĆy&uY!hCw;]'Fmȼʴ%ʉ#CxdYg _/یQv=wݎc7G 7y©b o=j8panvd<9'P㷾 `r.tktr©l~92jC4cu7:2KP}``=kJS^tL&ʊ=Ͽ~ŕ鹥iZOsg:QVO?O]BnLqC&e=kW7,fJe3ndcƁgu,oPh+ZٍbL>k:-R5iN]ra  jkDЮb";4$lxΉ&XZ|7Sgm'9%Ă3sۈO} ,̩ 1ϐ y"YV@8m`)6AizO \ǿϿ~Oϟ wbȄ5i_ﯟ̿?C`fW8]8>7B,>70A!7%Evj>"CK,r0wlݜCeC6NM{/`Qxdڈ Z+~QAO`k̺臃玆ZF#k\6oq@ Ĺf2[΁M_.LJ~dlȔ9Q7̐$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7Fb?^kTZt֏%6] q :tC#ۉXֵk|\umCqÎN]09Wymӯ,2%o9֒ :ølΎl=!F/gZi/Cև^_!X'COn.֧y̭N^l@9C^#3>L<)JP? W \,-$JP!eқ ʭHYZ&I 򕪓B6W=Kˤ7IA>[*d3pճLz+U'ClzIo|*"Ufgi&)WN \,-$TnEU2MRT YZ&I ܊T!eқ _:*d3pճLzSB6W=Kˤ7IARu2Tfgi&)ȧr+RlzIo|dU2MROV \,-$JP!eқ ʭHYZ&I 򕪓B6W=Kˤ7IA>[*d3pճLz+U'ClzIo|*"Ufgi&)WN \,-$TnEU2MRT YZ&I ܊T!eқ _:*d=0ցpִp֏]XY$~/֝бS5=XƨAg݊9S|9lacm9G y!Y+:`8fЩϱBƏB'F"? br"C4lC|X%~ba*697RiCڕm1prc{`m\X~k_ oC+7*@&l+ !{SHi,zߑ&Ͽ~S^b7I2?$m*0%SӼ۶94\"XyQu'8̠CbБ=ēP]-^ȬH~ސ X'b 6{)}@_`76LpG aеYu-Fd6XsG͇‡=@΃n[vÆNlyh2~n:|1o-m&8d]8kisƋVM9EʹU>sGL>3:ϟ|6Ghv\}t_?'~t~zVyן~;o=?^= .~59q.ڴ6!299YӒ2~Hn9Nu9q3Ggck~dbYH`_6Cl8tj u݁ '< ,2v0%Xm8pplpA}6N>]"oc}6ڐ9^ͯ,=:y+҄ QYdǿϿ8H gw? BQMit )ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k1@IDAT4֕7f 3wS0DZWޠa.MiFi]y h0s7 sMu *ݔ&k4֕7f 3wS022=ԗ8#07(!lqGYsqMl&9:5b6lvֹYvY<ԄS|! o.A}aC&V9C>$|60Mۭo;\7X5 F'uۖ#bCf ˯j 办b0l+W5If)Wׯb6L2u>1xLsK}9#ѯ?MW~elk&W٘ϟMgg4EO?6͠ٹ6N[sxo\q!qg= Yc뗻]5L;LWcёC@\u3N_΃xXO>lp۬M{ Jh>‡,&W+@@$ "Y6 N/qCvܜȺ7E.e%z'Xlp Hnd?tpl>ka'$X93mPZڥ3E$\PpYز#e. pMlB#GLeLU]::؄Z>}\!]QJ&6֯ۯ[ (Uܥ3MĆ =$ֳ!7]/8K,+Jqa[ƈY@1vd(R3 6b:c{C,X71wmN۪6^)+28I2d}rx6(6bK.86 ;Mx;̥yþ:~Бa?i1.a3>p6ZkޘxXtls#vFwG$UjfSgT1S]5ٔ)R#ihiR@eS ջkL4ZE6PٔhB}c(fK&Mlr4zwi>b(fK&Mlr4zwi>b(fK&Mlr4zwi>b(fK&Mlr4zؐa~qd kMpÀXd!7[ę9W5xY !Qd<2xuh52F_!^ƣl[E'79S,2F;"q;esh  yoYdчn.lGv >cksZO1 cc?mmfq ֑>y̵M!o's%DBdO^CZ7zצQdgDM3Ͽ>̶ShI^V3AzZ_?Ͽthb#Mv`5tU~ ib5_Yb @g5,c9g'1ĂˋPةl1𬟉ņn뫇 du!`%d^ao汾 WsP{/Ls2:ivdŴa `9I|6R, 9vlݍ[{c? F}ڃ :9Prć&qp0lX/ću!>?VZIBQfⲷiڴ ^0lpX_ Cܤ N=qyӄesD f#ׯ??R?Hgʤ.9d 2-qV=X֑ږI֩` 8~֙ީb჌O,a=8csښ`%b l)xGtu!o>cgnO6dԑG5ft`!bЉ#9Y͕':!JWpQ6]^')<iYBBUmSI"C{iV<DUTu2^+ϯ-$Q6U.tss}S 9 @8q&1Ƣuw%7'{5L3K3Ns,Q?qc51돧O{55IW\ G?cү?ן~?O|[_Z⡾ߍt#YcӇ;>9Mr1`%< ƶ``mXʹ!h,5][v2ܶZ~sf- s׎.y8}lb&<~ Q&7ṷ|!aPc|ۯӟD)byV Я˄ba=ˁ)!M8e6K3Ar9YaC>6}a.v7kl5)6s[#BS˵:6d[2v&o Sٶ]!+`AL2Pud,l>g'V~|6Fn74xAnXjp8@0w}sȉN lWF$ؐgؐԪh$&^=&N"VWׯG4_6At2B8)H&7A|2B}87%#4&7At2B}qo'JrGhՓMnd(by4Oī'PĪ WǡϿi(WO7 UvCCfユkJ!bls ~ysu,y+Xlyr],\9clmȮ},#6l~d0b!jbA<[Z՚^intЍa4? ;7?xXHXt缀+?M ;5v?֔ 7/xXp:xK=|rlnQCH,Z]V9 yJ49KE^ #?#qK0qG>g}9eV89Ku!jli_kwx~_g9;{lu#(gaqc?Mv5. mOIJ&ȭ ];]sq`׏Nl:x㱋%uD6~dr Pn 2\`bsZd@bɑ9280=#lޙb!s⣖7Xs>CG D~+K@l.gŝ oEɘ]7IKP񬸓׏nsqE|ϊ;oyڴs+)1L\ &CO,/#'Kb&ϟ>B+K⟷޻U\[/XN56A]#õ,6Áҏncq]K>}ٚ(Eȧ\ "]·<A?J8b ;l9e?EG׎O`p7  c>ܜڭI]7Ef=܅Y7I?uٞ[[ɍN}7n!r!|e)F\33:黊X&Y-bձjiIв>ߊ׿1~ÿ[:_$oca}wg~fbp_b~!Ff[8S5Қ3ԲKW-?9CZ&Y X4$њӛM\ X4$QO3$^ X4$>O($Z X4$g3q?qT{9=؀Cp܍֮e _O}`!:>ЩK 1g;d lnxn\zɕ녺J&[T ~#!CHv<6|l0f_SazkT!XN.p洞ul1!S>~r^GE}eC$Hp^0I;uGuem`m_ˌ/ C͵ȨZ8/g}p' _W}kC3SڸHEi^_}_p^n•8Ͼ6j[N\\=x_?iԯ?ӯ?IqBk)/tqEU->ty?W$pe>94qCĭqرn>sj.vc:k\d`!!|!3^\:wIW}x,Y.p 'ć<ƒXG&\bT:͜@xG3`o}~lg3Xȍbs'~ *O榤%1ϓx) wpɧSp{$&H2 I9 OfMW_UH'ܔ$&DsS)iIL撧,sSҒ[%OY榤%1&KMIKboM47viuH3ճo 21$ڶ-qJۄ풗kWKWrOIk!S+lbZag|k/v$^=צS%]O.qKLM).ߺ^}y.0ƻTb=9؄vݟ,}XmwCMh9Y֎pyx=9؄%9$6'C-:kYXM%,K2`726!l3kdtc36$$V{! knd|97Zrbhd1tdFF#);*%?ַQmpёʱKMYG]vv)im7d!SOX,A9/ؐ;| LeCbljT}=beux3lZ R)66]Tkr%S,ϦKݧ2Aϐ }rϦUYϿ~=؃o<9@sqT7:\c}\O59AV/SJ!rt+R*khkچ x[_`ņXdtF=jPԇORv}:2$dj \p4NjSkdA&A!ˑs\*>%r@[e~pq7', ?vH cϵl=9n㈁9~28 ѰR0TFOa l}5rWԧ~XM'\v\L[ҏ5lh8n <h䀰NT-9F@_w\ /H/cv[ΎspfBaV<^W}CN֋-G,^z'9 ~c>2tܗ ]S+_x`z;^@|Ãҕ>oMqun6ۆw?s]@p^#}97=7zǑgxڌgFa, _;1<߲i~?Ki)r5SV;d7bsCκpar&h8q`~djׯDaǁZi GGƢt8n1)`f(I*_A!_@ *O'>O]/v8,lO}kȽO?涷.O{paݱ °?yRO|blb=([6Yy|/f0?Ls7k>+.ӯM5ߏ7n&kLF8kI׷|5~|YGЙ4IY,5ɫ6+.R5Vvd#"/&'dX3> mnMC,XK&[ #~(;iGQ Љea0;a^x@OyoLMAƎ؉Abc\lPk]p #M(GtvcϦȎO/#Xl֏uwFIkȔQ͔E0 ̠%׻k=N5cozӛ W/-v(]h\xSf;>j C͇Djr'68xdkb!'2S&A_rƑMkRpc\>Yz/Vs]+CFX})ѡ1 wbJ256rMxS| ]A)լ OyS~pLCƑM?9m{qdS_d8°PKXqdS>,tHϮ(3D[6 S&naG+/#ո׾uk]󚑥0\_K`ŵ݌_Y8g<c{>}þm0|pjl\2;d2rCMTNYkLD}^D77 &o'_Y-"϶ɱ!z}tv\'we?,q t>c`_y_W?vRՠu١gy+ο6dk$?_Toӡ _4G|D?R |8jm׏ n8y69w03Mx9/vȼAM [G]v!6-8Zzg~pGb:<۾לl9y-dI@M=L@g\02/$PS(2uH a`#z#^ۨN6M SC][?6Ag_RYfʮ O|@7tyJʚ<#.>?/kRPOSl%K;P7yg v%:R,~"UoUK}4&yG?r'_eZG>û}{+Wᛧ:+_YzTxǸv]gÁ ]ha _8sv p 3%qO?sYgu۰9 3d}ѿŘRuիp~&es71~x䣆G>p^?X4`~^z}FnI_>!MNI>8p͍&L9+C!VE֕`D2R# C+>2$6f]8㐱Y.6!CVuڪ_vg?[H+v,7$lD ; qs[3eP9ؐO{kbXI>><éG 2:2qxp|LFR[;f~u3]݄)mH@x09R1t !BcÕ|:-V/Yc)?~j%/o;:`g ׹é_gǕ~?|oc"wrP5䥵q:z⧒1<Opש^ ~#/xr~C>eg|++^e/{pgNOx=Cqy~ߌs C!oʗϟk;Fρ=,ztHl8N91e;db hRЁ;p83k^j_9gGu+_b)!1&׈=,Ї㭇ZgIN (9lvxEXsg0 Nuo?P!/!!OY n>8ml7knu25)rP3}g|֥31iq{󮲧"fh"| eÅt5 ?uبv|e,T?p꩙Zq4\կj@_1=1~x[xF˻]CO3ڌGմCq_W3ËE _π6Wbl}6 }4cvo͒wtN[5|;)S-e_~_RC|e)' 76d9Csl8#ZfGfxa[ψgzѵs~ugl|ߨ3>FlȌ% w]]J%[:\:ש `8w mQ=zWFo1Dzaʍ%FUK[ןigZ?>}S8]_Ώ_O\m;d 'L7S567'X<8s18|!6ڕ#䁈cu6 uOݶR>в^~qbsmBlqN-Xe 2 #$v [opl1Pg@Xc&X9 'g§9e[ۦƛݾ`[g?wŠghB(̐vxO"=N;֙pӲv䣹T1 KY$||ł- @a^E ?9FygԿԥ.9i=%=LѼ)nqPGko|㛔cSY6X<"x/?>qGPP׍E{pz_0/;nwJM4;k73+]J2<-GO"kZǕ޼&' {3ᲗpI_ O-nq#P?>x%=,áߋK]j8&Dz "9),G--spxO|Rx9{÷bf7yܥtBxۇw <1~ _Wwë__k:'>a`ӄv܅|HG-;dogp-a p_a\?wGvxf/ $H̺wg(P{΀؍4Gz:1 <v#Q>uPllyFţ@}qL2Gfe``7,Ce292(`~e1Mf)᳟쨅ޒ/dIx)F4/+g!տ51\/6w!<x?!m5wy1rxͶxrCu2g?ë^QjW7i^?K_nwە1yI|Pu{{yf _rIq/LJwphl.0_όگ^-}گ\uxgl0Gl<8rfw%n%d7SOIc0v8T|cw}=L̇B[! n,C"5ԇȃLbñ]2>m17véK.A!}]M|QrQd@{f1o>9~!9qcX<àkӏnm'9l1z.|b!ş95`#2!OW" G?\`Z0"8c v-h/vWο_Qk#./ZPOL92ӏe H8kT8>7B,>70A!7%Ef! -m 䓃>Y7Fy\(i'7 Ǧ=v1F9%k#7e(䛤?vcĮk~MxdcXldd!8Ǟl/ci~bC?26.'֨vюQM$F-{~O=6 8+-4ҹVVa8ʢkqPE ώ5Sۢm$èn͛f]{T7ēN91w;_ju߽?nK,n]|s7|߾_{Au!U(#g=_/ަ17kpx׸A3+KGņ (9PySRby _Xs"Wj^_x8.!`hG?V~(K*ínyރnuկ|U-e/{ynarӸ+ ^gV(2|u,&2~e /wKc{46d^[??)wnwk-p\ns2Wtxbbu_oʹ'|l𕥝;w ~JY*5yuw=O ^g=t3qÍot#_GoogQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQ.-_G5y(vn5rWn=ɛDsӨi n:tQM$FM\pա[pj&Q4j{T7bQ(xu/ܣI;7FƫC|Mعi47^ &oMQn{2 8X<Ԣ'9>28dMHxlܱe;:y 9vb!9Uۆl}㬇?:e22:Lm~`/}αLM`ppv f?>8wuP{< .w O Cև ?6:xbi;8tCഛ i;Ds8=l^a#uKxdƇ#ؐa"/,Io5 $Ѣ1Vˤ^5hzTyX#5:^7;"ե%K1Y&I  yk<կ|E3}?~3ӆxaG8d)w5l7b冱Nw^⿈/ wS|%fSnh~F<(!t?Uw7WI3|mu6@G=ښuUͭo~* ,ߏ^R|[ݿ*s@IDATz=>*8p,W=gȌws{°mǶu?ln)'M<-/A"߿ ?3M ַ5x; o?moĆ Ne׽a^[yc/K.ws|sKPoeW \,-$X!eқ 7@YZ&I =U6W=Kˤ7IAo:DjճLzZ,-$y#Ph@zIo|c>mU2MRY?77 i> ` ;,vcc zn99S|9lacm9G ԇK9yGl:9VA~1P\n v(7],2AÖ9N*5]]p'xb9x#K068]v '!1I ֶՉE!Z1mUj"4-!ʄ̭~pz<ȷ6;&Jڨ)yj.6oUG1Hܱ9<_2~wK4YvbULP +/[%68m8ㆻ3Wh_Y)k ?qGH"zȡG>ڸ;}.]E _Ygc?_Yڛ}y73IRozӛWuoz WO/8w]~3ï_Z_ ߊo\~)Ý>}{O<105+>msڗrAPߨ'+K,xr k~e68"G};MY8Sk2"fC^aQG>;Ⱦ +KCΣcC߸/}qk_quxFPKmsZ?WӎԿN~=3( G(7SF_Jg:v-BӚΐ: KT3z^E' -HP]EQ] @U* XPbŲJQE"lAD7C R w>sϼBsf~Ln&N)A,^ց_پP菿|f:{؂ WjHi6 2rAX챣+ppؕ‡=@&DNg̟'MvkYP?[ {@7mq:{+ө3M| Ϫɤ~o|3N&f⡶\JgW.a񬜟gjkςQFiݛ.Ro4{m4pd;;n4i%^U9\1g\swcj}9o)x 7 l wu7w#<^&Qx}'4,qL~qiwIS|a'0P^kmn>ssxR>YV7BVa)~|{ |ꧮW<ֺLjן??{K7ndm4'\Ѣ '0l!&w6d  I9!K+@Nk&EƏzGԒɍr8Hm ^#WT_|$F$`UcT|4b@C$]Ů '< ,R!/ΦW}868Y> '>|:v2vQ6![k'^$O̙PeE)Bū[]1' XY!ud CF;A9Y_LKI-ոŐޢpqh"j"ݮx2o~':5UDNmؙR;:j꓇p)Nj`]ů\ X&ANO5exW(.4@*N<%OX6d;!ՅA?(W}85# }|aډ/2տ`AN86rhP]8y ՖlAf|^πOjgjשapӁ6HFғOd9i涀~Y9?۹9ԗXŸ]Ds-ڪvݘ; qg؃뇦OE'yB{{튦w|8Q㳇~6=6 }'93ַqiŵ!Vebw5m(9Eph_9!꓊isttg`$o{8>i :~dx(.1a i!ʺfpO]ᰃ6#̠_:8*?8vCGb,5cbذC}ͩc0j%oA TC57'FU:.&FO ωI2dĕƳbc#:nc#RQ:)}0t8؈7 <ˮx:r.¦xl gE} 3&gfZ,+8щ$_]*( [5 IѿzXNmN#c9kJ,vN_rsnyL_}J얝0۸(_{zkOsMwt嗧=3=%͹ašl1洴+޲dV  eʔ)ZcFx-uHva_쪏_ Avkˉ6wb߿]ʯY6w!nϹnu_~729Cwi@UcY졾ﶫY}҃h{vL2L|+U^ L\rI]O6#-Y￿ Q9H]s}+C4vxZ.Q ~*z4A)u63DMQn3I9cnuGSLwazESۑsۏh1xv$%e1?8Zw ^4EII;gMQnGRR#l?zEfK/,nO8qb;?lBN+E-Kt|5YL7H̔?OckNdوG'&6QxdF9W-x+D[*`5RX89v o撛\aFbM|6͚NVݘcZ۹GVW܂ݞs}86ax=V iۦ-&m)AϜaWʜwnJm\4mmMJ:Rl }?\D4eƟ[x,V]uTmAUb>˿_og{f-K't-e-*e^:qǞHw}-,i㹊nUqlݶkA7E{-LW^ɞ9BJ:E-CFaO"ku8N?klo{Ѵ8k P{UW]\դkG쾥3L&NL6 mL스/,-E{Ѫk5bsМ:{GMk񗿷~t|1qm9L?5jl ËNYLnjl曧; :li'[ k{pecnn<s8֕*&>H1vK=8~P=8L5C>⩋M:26QΆ6PʇOKX]yonۛwPai: $d|0xW}S ~┛8#2qґGjSYQ?U_t+|N 2sI$5F6O!Be[=K7\=D^3ۇ%\1"]| ^:f*+9aCƸ.s޹CT.n٬6$6 ?GRb Yz3 y~rR܃f陷կL{ܷM{#B향O<|#LDuk+D6nNtwMm\^=$L {[ 5 4Kytw>5 nB=˓"kH{k{p/2ϰWmOڭ^@ށm}Qo%+,X񳄘 TU[)]C{,=YBL*ڪ.!=hy,!&mmVh4K<~ʶj+EuHqg?K Be[[":3%JrR܃fbPVmv)Ag 1AlkRD\Yz񳄘 TU[)]C{,=YBL*ڪ.!=hy,!&mmVh4K<~ʶj+EuHqg?K Be[[":3%JrR܃fbPVmv)Ag 1AlkRD\Yz񳄘 TU[)]C{,=YBL*T.{1ES/EmQy6O'N Ɂ*s,[ >EK6p[3[v4?b+b6eNGC+78M퐱CR;R05 U8HTMb$+ܚw7>85!rbW}dg0))XP4 +:G<G pIj&CW]qXR;9C>ΆOW#TD'M?cU r)/lٳ5c ۥG ˔,]\jSi /쁫My!5٢E[@LjA7)y*kv{*MZ/,69abU_y3 frk}o2k û:b7|skҍ7ޘ[u2JFƧ tCg[dkpA }k_Zo=<&/cecly^ꯠ>#/WXI{nE:|C? ^s mN/<~z})3wD?wu G^>ǟ0:`x?EOk>8K^fb$e&m&9fwil&Zx@HEgNwɇ.?vt9r)>ŀGQ 08HXbC`ljڡ`Va+GFRSsq'> 6`: H "ⲣypg h :$b4x3cT?Ă #QnɇR<>ڤQ|Qy_9c|XS`e\7Uːݖ8sr9i} s _QX 7U0[B]Y^OLMA v*6MhϪT;0heemx7CC] zN89v:o7QwuB9dNndn[YXƆl_j A!O|s1Ǥit?4y䶿6+>h6S޾^ގuvmc7nϣ;{imkՇXν0_NbC }onp"X_ۏ?G胉Co{ G1cm&5H\!SӢ>eN˗+S( ';xıK&6t~ʋN &3ۉKdcīH1aCV}GⱫMKG}PKV[  D*H(jHԑ! 8H Oy@SOl#j t|ȭ jAn+H1HFʥ+]}#':1RLt1"aN#6yび5Z LW:xDM83#> %r"<<9"E* _tvEwVyͅK,dZdfhXQ}H^em26; |'XZ%^*mof0nv5Z$qVE.Ŭ ͩan>6lvLos(>[R=O7۴Ȣ<诣G}կ1ޞU>"@k-klmkcmhW)bl!RcXs_>{kV%C9$W!j RYR|l"32~9_E>\:8I#?'\Ŭ ǡDFE.b/C$qP˳'\Ŭ _;ه\45=3˦[+e6ls[Yӿ}/zWY`W䧟nS2u cccѕ dF.G+< rY#バ‘G.dYybbLt6YjcW{,_'јg#0jFj86 <&:IW,Ǽ+2~vjR څ_QMqb ^MtH:x>l]jb]a~{ XD Y^0D#&O?eǵR8 +,1!_[`l:f#E%G@0ԭҕٖg-pa͹>?C6~TǥS5+ϭJ]vݺDz+^6|mR]SnJw_Z?nTGXsT%@ |?q 1_Te D<(u?=YO|׶PwC7C W#>B6Kc㯝5+^v19,c$C.ck֏.ދ{=AkNl?7?wgKD2;[+l1Za,x`ƒiBntr@+8~#&v6͛5Kۅ_1cjO2'-O>6򣓃 H:xcȪiFŎD"ErslWf/cpŚ9"GFAgS^,H7G-d Rr &G F% y@2g ᏯbBoJzɟ~YhN,4v3z<},G0ߐиqW,y)M h'zKԋ.NwmJ:)ޓysfh*QYDwuW!jҚAr:8(J8 o_i+gVĹg M9#eTL}3ӟϨp?{7tz//7[ly8qM7CxOCwN 4e oY6M)B ͭ4Et0ʡ,66 ;$?r y-#0\5c"d7؇p|Ѽ򣫾 +G-sF 6࢟ .;x:ƎkA&7CMʉ,jRW"FNTO_8vXJn|p :D.ᰱ ,^HW ]Fleï U3w{_dRaXdT^um6NQj 4¨Y؈{4vdlZ^W=iҤtϯL7 D Rhk'M2]hߺ;Qj #IM:cG=& "udy2MjÖRJG۰E??<2p+;o^~owפ믿'&|H S:ͰB" #`kfH>U,hk&_%7~mS!:>5Wn8>61RS8dbNQ)Y rcrz:LJ6HK-2NHQ5|,0e x̫aCx>&z8)\.c 2D}dpԆ,?h/\.ܲ+pbE?n ߵ+d8b%AO~dpq\7*y5Җ[L˯H i69UʪƍŘs wb B x^n]wҒ+Y Ri߃?҇`+9}~o~~ ~?O!W:?۟Do8-d2C?p=Y b:?,o(SWؤl츴[l_vVߞ%[vYpLشghE=g?;NCǎpSNU;N 9.2v!,~O%Nb#kIS^]>Hё?څuN9JNg8%?2x8~LN̡@mŦW} ,:cxSO;B6('8'FmA>(/>uYИ/Ҩ$i$|SO1K@0˔e_qt!h clW^\!V TP??V8GEݼƛ'" /3gy,-LŚ re5hAbh %Ԧ1OT.6->r,)hA Kr,)hA Kr,)hA KMQ71YV^K?dW 0+>֓.17BÀ_k@xlPCC]dD?ɩNvSM|j>슗C1' -)? ԇ1L,xՅcSrԉjh[[L(%W0!<% -ffY<Ō֦lk4ί>i~su鮻nr-{kZO,X3ϮNωj6'+Ce59K~€=ZYjI{;`ɋ(b򐘳r-"o=n"dk-Bl&=8XtVh "1w,bJY)arۃE(ZKg,"s=n"dk-Bl&=8XtVh "1w,bJY)arۃE(ZKg,"s=n"dk-Bl&=8XtVh "1w,bJY)arۃE(ZKg,"s=n"dk-Bl&=8XtVh "1w,bJY)arۃE(ZKg,"s=n"dk-Bl&=8XtVh "1w,bJY)arۃE(ZKg,"s=n"dk-Bl&=8XtVh "1w,bJY)l[n_/luZuMk&%%[kEdc0+\!m,NTʏ,Qt-hD 6Xt^y+dcn63GFgS~lʃMuc=ʊOM,qQ&i7MXU_UI4وF@:2DCO@哬%VO. vtW⑕G2 :F&8rtٰCK2v>y!c2$ e!(><ߵ7"L颬;Fֻ){96ɗNHbE VUV!m9i]uB% >}zYgC߿?zK˘X2֮ڮYŮith3QQpe.fDzS,4ǕLe~|\srp̏5yO9Ae-&$c)c.MshTKδM6Yv]FUBQ\Z!A.JGZ] pNnN -挹>L U[ZR+'(K'A&'~8~{h' Aq@p4~28h4)AYA7W^qe=,sn|(TLGߛ2˸}^<&<UsK {V}뷧~^b;ME_ާDЏ?WGOr&hQqzMbeb`W n† rɇYm=CfQo\\F" m<7קcfyM"mK=tc_Hb=< O-KY,Ȝ}Ϫcl> 1Cgffbi^|jemlz+)h×6Pl&}:eC.` YT3T&WBD}~c#iZBOQϿߟ|pƔF#"/&Ngʏ/|IddN3FeP8A94hf*_:YOX|cbRWȣ؉gAtGq > ,[տlaq @qKn l-|ڴis)uK k}e`Y;-vۦ%Zй>ʺZo-1P4[a1b/VzgJzXaEIO-mc#ΎMK H|W_؉ #8/sK=iv*l,>H3)Yt4Y" q1PԉS<~G8④njO,٘?Dj2٘;Y΋Mq5V39Ghr<ȃq‘xH6;*lF  ]XH 8h_"/𑃚όFT <&z 9YJYdX j pDt`}߷Dܲ3[ڌ&~'5Wlii{X. yn}>d~UN1fWY/ʰbg[g=yƮԢ'ߖ3йYv |riɧN>B whrrf] Gii)(vu8IO@č00 G4:,K3  ǹMǐo0z: #©=Dx@mc:ԟsu?;1gli]m5^m=Lqua@m!h_-bAfwۘ6iYtE88|›XlAA j,1`D`î'۶ViR9Ƒ,ukK>e< r˱+Xj||TiFpH>5N.NGKQ> :].BƮu~Wnt{`@IDATǜ,Z8-l~+O3ZIVYO٢̒K-.^2 8ꋏ1/֧|5 n 1L~fvzb#V1Č^NlY /d",9kex-¶8-|[, ps{# RZgr_hؚUqkǮur0|_k8lΙr PQrt IɃR>O?:fo[I u_dr%7̮_+;r'=w5rWh]\daڃMYWpDUAǼ!UtעUG|5?6ڢvQ B>_xG%&ż)ڨ+7:r'd]I@ÕHv5p , ̋pR}t (2ubbq#YUMvrMc& N!bM,)^:\qm!b<yQ@3e;O) Û<2/˜Lk,hP'{9g@MWd⋏ہMv͆D,@<9Ԑv1=i[eWӓASDc g!꧴Rһ79J 2 /=Kf\c 3yҢ $ Oiϲ^kiF =o%ZІTXZ0W5IO|*MXsʹ (T΢7>^ {N.X` 09K}}뷻gzȣ>j_!ZnEee7Ҋ3c;f/]?=dž!Ӈ>Fg=ekwg2Msnt-b:>6a]dbX8)S)8F68xOml)٠5!|yr;AKV+N V]>a ÀB!XlHO4Lv{̭f,h!c'W}P>35vcjt0CzzpHБ!rG<8i#1ܦ?R8ZPc0BD3J'HzǕ MiZۨ+eƧ%^";>ʋ0I#?YZ] mY1<EVgÁەf1/iCV5ʖ& @a><8)һ^s-pBv?-,U22;YP$#Z$;Tܪo"V3f>QG:kg}V9+@0.E\$2 rc&;M6 La{5iVKni.Zn_|E~VYy4 =?9n{ڞiE9OJrc8SkrVJ4vx<矾~?Tz:G^+%<.E/CP?bJrAxT&sa (6ip቗l0uC(G`YԌ8b!7Es^1w)f;=a'^:Xځ.vQ>ji'6Sw:4#Ib(9UA_7eaSrg0UL*x9h~2Dj?$8[]yqP{[:\257v]511j$޼tC|u|Nv Dsk_ovX<<]ojZŢ !}E=!\ό1Wb])U1&?3%oLZ<ϴ{iX][/zSsƤ2Oڂ86[4ޓz0sR.1d>1c 2&7b-Ǿ ݠ`@X<dv)m&^8 /=ymF? ) *)S4ܣ CxX yooiC{WV2oh<,G8tAs琘_Gzctޏ?8Yo<۸_-߃X렳?ظ] ]Ld~M'!ZHJaB8 <8Xp)\ypFyM< <6H:\OJ}ZMl~Nd"6I&`AW{Aҩ%"*"?\k#Ye8vd(3ij cSMb&xdH~bK}35>jjSW~t`|AU!˕E4`'24S a|Fwܑ.+ Rډ\vk]_8_oA9b22NZ 4ܰ2[I\- ߟX@YCO'/>5Mߵ*Qۖhqwœ ng G>͂LG!ӂ?x. d[ 2vLs5ݖ;xiBzWlM(fW|ʪ)S]|sV?f/O>D:>{eY!UT(PPg0>G1gxǡ]jQ-Y*?C?3>R(EQ~3>R(EQ~3>R(EQǜC}wJ,zP YsZde'c-E!l1š&u!ȆElʯy2XlԆW2msmaʼn# Ȑgί>A9VAFa•h4YturX,bLtbU=83;2vPt䘃PW}2x%USA''_ 4#嬩v4_.}vzBy%qtߖN*%4x)QFòϜJ 6%vҲ]UMӑImc@Ӎgj NFĕ_v ui[WbۖlQƞ\ܵHb$WpRJoumYJݶD|AaBM 79gaN+d:hJޱ].ۜWX]<|fi?rFqmz?_ clNZ_>_?͂Le]`!3Rdvg!g.8CfcK淲IPh+\Y Kn|>Ddm&z}8v@)A}ڢpN]raFdD׻j+DМb"kd7SIq+8~!SSMp1F~be'V <lSNr*\ˆ+6bSM\B-eX^|Ü;[nI_zr1 L!)| 7qx3<1JkܦWnjUM%X%WRrv{M-=Of%XjOCo9M𡴸nJ+u]z>Þ!QLJ+dֵ[D=X%heM3gδg@ŭiIWtYYnY:A}qͯa&9Ƥ;n--"iu׭cLc[nM7|sZvVK+hZiŕ a}/bDCu^/nE^ a0?_Aroǣڱ(Rǟ?n}KG_21Mb1A-3߅cƒ>Em xq0<R؈WMv/<(Fr#T_6IDѯd O5@ bt5F~8x쨩n'/B_mW`U2,˝K=a)#]榫t980_wn7=;;>я{}2kMֿ/&싥~򓟦k뚴- v =xޟz#״6Ov-4ܑ6uȣYL;[nUmo+lT?-06˥|V`cntS噸3dgctM= o:G}ǷqƖ_ot 3 .Lgyz<H^פK.$a=\NW^y_q3s֧L{mn迒ڵݡ6xÍ;v[lEz[ޒo믿_"ބ5s?E!ibm 'xUQh2 \xv->n d[|ݨDakS(.p!Q7֦FQ\³CnwoM&gF [MFq ܍A65r5x(lmj4.<;p7jQh2 \xv->n d[|ݨDakS(.p!Q7֦FQ\³CnwoM&gF [MFq ܍A65r5x(lmj4.<;p7jQh2 \xv->n d[|ݨDakS(Lծ׻D䦺Δ \28d.ct(+4ظefikWmk~{zGSg!唬v ~M~"cvNRNG J SY<UB=< .vT'MC'5TR_'C'>xʅ]i;DN^lbC֦OP!d@/"C[ Vx<$wmQ\_׾d I3>Fo~ 4O&@_[22k$=coTb1mHfc&I>qjUKxڱJ/J;o:%--l5ݺDަ7UAz%S!묷NZmߖ~{o+9@{2e wꑙ3fW-A￿~{/D_^e'4>/ZXz_lW,_[7)olZ?W~wmbf8⪫ö`}ϧ^ʤ0%_NS_g-ؕ)7[졻B+divy򖥸 sGj,|߳1i%%/~IzOo~s' 'c{ASrA]~Y:}[/bd1+{lly[:e>t1Ǹl^{bfվWm$oHo{_J²gC;q !>k 2wbĂK6^>&]+gg$1.lǍ6J?Xʾz„5lr(UoB&q!=V/n͐сÞAKIx_C4@ {-^$ V3:p3hi"I:yFaϠՋ$A|h ZZHu>PÞAKI?5z$cmZ33įnGϏC:vFEsz#?v6bUO8[AilS_}Ț+V#8C|\0ʣ :`UqTN}|+d tJ$'pء ta!tm4l0𱓒ULBP68DYhDfk6ۂX0rjvp _´{ګN7~aƙv+e)Oo1Xoo;-ԓ˯ht[|1i7>ZxI*G#LK"1y h+d&8_Ȍ&BL68/$M<[?ׅlp~m_r(!aG?LV[,^]t=lA16&̲iB>lwȜ|!aБ OnrB1wx⠒K "=vTyŕCqPѕĒ|ɩ-8lj6dMt]q_65QbT2,qN'WE9Y+t~#Gլ1>sFjX¿c ' ul$+|XV3agK -؛x9<7A9N3"  !KiWS [+d+*_rC9Ԟri=(\3uŖcvzK^t_:F|`Cr"5sbnyʞۺWӫM=gܞ޲[ҶlӤ{MLJ^rO=_aefwksʩME}Qogܓ^jW%ԩ8ƀ@-K<=[F/:-؂QGngZf$M>`4qDS{UOOr:蠃wnf1ڭQkdd! mꇶ]9sakN{˟?sbOved}_z+_9b;,>\!mOl +hW,{;ؿO}*`s\c]vw}v#d#fkm x3n1"~F;ߏu?hF*#??_`OЂ xuk!tNZtMvt6dLt6d&q䄨/?$8raS;$?>6WSK&7~pʡz .ڪzя_}P}a>?XVQFx5Zjt5&6x0pH9TV6F~8\g,?"Apʣz>pؕGQۨO?ˆvd!͜ UF]"ChӟX GM$  5 Tla$k9.U%S>$rsyA,Y,ەfە~zVgs}4='ab̬(O\&lrقB3d2ϝK2gO}l^җqeg[D=5a^{o^k瘴׼եr˭H[ `amirk =Wϥ ;g5a7;8!x;Җ[nWm|oӚkL=4^{@v+o|ȕz/MxFi=҇v 2yw+d6+dhvqǝ`W,>꫾\qOLK.T◾0JoYZn:x_yŕ}SN{tv=smziGmjAHs L씃BfꖥJ튣H]fWऴK/|:DX^H` bhU E)B&N(ZE)B ЄT]jr5CRu Tb]"MH/P*wQP;4!UW@(ZE)B ЄT]jr5CRu Tb]"MH/P*wQP;4!UW@(ZE)B ЄT]jr5CRu Tb]"MH/P*wQP;4!UW@(ZE)B ЄT]jr5CRu Tb]"MH/P*wQP;4!UW@(ZE)B ЄT]jr5CRu Tb]"-"mn_W0 ts\!9pM,Yќ4_Щ&z~86poO[xȭ?V3aϤ?V8 BUyƛl^Z%<񼘽Xah ,t嶤Ab(fÝ|I+.CwE~ҝw ni|_H7ۛ]:!"Rcva>o8t1(_ (pmM78կlci݆C}-Kv˫N88[HW`? `VSO˷,MBHgɏ2jvAn˕SSN=}![oﶻ6<ۮ$R}O>x?4,t?|[IiI{mA&omC cc~"[*{e\ss4@cL+y_o~m0̧Yx3nr>OgˣՎΨXr>O/Xum6<4\!m3c/E]UCY64WM685lbq<؈{o]VwުV2ɐ S[2Z^"q CATHLpQim{i/VI66QۡD0mZ!*C1S%C }9{z pvս{9Ͻys/̍ѳ|Yp2!Jh>‡,&wdc5PāɡmeAx qK`\]o='n.l@K~\=Bc" 25 ہZ3VN>0*=S)"؄˚lgɼShCJ%|]X;I. o}(Na1,73x-Z2rܫqZ1KG^> 1xE(S>n :RУnX~U?(oʱ_×r&Tax 1g<w7]_B_M}]޷<}/Oz7,{7O<E_wȴ&W}a~ǂL70ݎ/6lA}͸I;/||l"GoWgų[@___x]-K gy]~Ϯ>|6}1/ w*PQ ,ʺ gqo'_CqfA&yn9=t2%Yٵ=u;٨[O-;zU:ՙ &\,9*wb5 Y??Όy1 jW0M(YןyG&X᡼IsgE,{ 860LcMvpض1b~urB5Î XjbF S`loK Z͉c[ᐺ~ۦҞ§ H!듛Ƴ`G͎Xri8 Im.u8،  O_);ycW~;d~'^.gA3d~;:?c+Ӟ_}C&zN>GKSXwP%3[?y^<|}]7::Ă㡾,ȼqgң'K_ҋ^t2eAɑb!!iK).=s2gS{&=#c̦,7@tLRG MYn&Tc(>}*MYp="[jH]mlr4zO4D}lmuC xF_!xtmkmȁ ⡾_C@-o^H?hwOo WňoxcC}cA;d;dO~0\-oŌ\oƿ[x˯b=yAϊJ~=4d4j:~5k<{K_L<ύ;|;{ -7/wy~2!?3P߲t>~JGO c sr^wT@} ?K'K!Â0%k33؄pjY1סy j3_6XMIv`5tԪM9~c,ML'فQ6=Gw:9<7: L\daRŦ?s}9 2yǎ82$ة{3c}p>al7?2m/7dX٨L6ZfHYL;znvp6 ?g#0>p0!jNaFp]x1nlԧ=! Ɂ/K|[nrÂrM|cA^J22W>&thUmj?`#2Eq1+aHƖxQΚnŬm.d)U@sdg|?c;ly-:%#cq|p6yJP3V@S~qwu;.8rNj?w<=^uͫ-W]urx0,=XԺdO"!=}/l,FãqL/YFE/!yγC}iyC?3]\wP??".* K//}C'?9"ކD_8>?s/W= ˝>Nїs{M9 r [ 2G2mxqЕӞ-˽uۇ%1&?Η.KĿ?< E1IK_/zQs"M7ݴ|wyWy5Oz7,w|߾h6~yF~_9,_Ń~߻Wfw~Wϊ޽u[ңdVjCO= <%lcq/d3Hspnʱojk+~)ʬ_04Gp$uc|yՉ:Po^⤋g^7 ?g>rSd;Hؘ < <;UBl8|9v≥5'y,x60[SD :;}S:26QΆS>jB9|E]r1>L$[0q49pm 3e|0xCC[|'Qmĩ#8k,OA'9Yŕ'ƶCRN֨ӡ+cA5rs'{g'\MnK_ٺDf~ϖbM"$X4/_*A|D3>7%tA5%s_?`Q=W% 0'3?)wD}jMK_bcFz{Ϙ2Wk;ܮSdެ1&]zy,\HXzC7[S׽a;? ׾>%>sU~|yU"//#B:cp1wP~N~Ąȧ`X|کAnB<&@ICvplc~ֶ.XX.#;2u嶇 ` e4f'wqC,dCn-2/lƂX,#±˰#Zpõ5UL+ENo`RI&%7<èY)exϐ޼tay%o w$0@ D)5K ,?|poq/ŝu!c(>^Rfz륿Ktw%xSBѷ}G!s0;84y;+On8F׼5#kk|#>ƢtmyFɍW+cLX.w]zc>bB,ҿWז=e_e7ݟ[w?&=(?7 wpy iϲ|WE'xR;~ HoZzйRd#>tMsB.s3<:1pp IJlvX 6m\9נ>:dsqkSVoN0vt@@d;$K5y0Cz̋.F1~aLJM9FP9s>1؉g`ҥ8:?kؚ̒m$?.edy z *}Re^-Sy(9r,$ +^|o\@MWLE9Q.ON;{/oO.$b.+? X9\s//]|r&)煑gbg|[YbIU7-zԣ[prWY`oZ>9pʃ[HY*7S]\ջ+̯ⷋ.9`۞>fSKЛ 7E]nwőX~x^/ >@IDATx/ϝO~vK|gy [N Y8Vc郱v饛v%H;H6g9/\ 0Ͽy7R1~=)1-CeSpl䁨a.f}ȶE7rC.b#&".l!UÇ$7QՓMnd(bC'_~yxiAqE=ė<}:?l" #er}_&x tX8w˻ J_5 'ti}>;+.QX$1GP|2/YZK͞UA 7DWW'PTqjxUIOI], k㮐G}[R E:{h[rl#7lH"V=d|ڥYms5n˿+|ז6Ɲ- y)/_mkkll2!{"+{ Ug߫v)EVp7l'C2WR9o&N"V=d|ڥYms߰M E:{^K)۾0a 8XujRd}a|6Ap2!{. nmd(bC]J= PĪU"+{ Ug߫v)EVp7l'C2WR9o&N"V=d|ڥYms߰M E:{^K)۾0a 8XujRd}a|6Ap2!{. nmd(bC]J= PĪU"+{ Ug߫v)EVp7l'C2WR9o&N"V=d|ڥYms߰M Ez,pUA*L)Ģ 2ccϋc͋]9b /3%'D|#[;ra2uYepf͓ۏ 8cB,D}l:8~!C[ZӞ\hH8FcCX@7OԍE~ C'rԤvۅXSN 2ܼbAx-aƆF~ XC]GV9JE]47u)k0.eyܕzJZ&t0e'P,Ġ3^|xU=)˯+_;1'K,?Sz"LT;dqXeA׸NAm5rmeG>r=I<^M}-<NwmJ+\Ba1_Tgu矧Xː 636mt1`~nMbŢCCS.؉CןaG'ymЏn}66j_2mBpOQmѵ#oi;;"6 lGCpr8ج7'vkREYw!lCcOl'VrS߅t\ⰱ&dx G#6Vo!{]ж>*׽޹oy{]7«?'?%;.]2"񬑻ne䖘9%(_Z,7ۗҮ;\I]Wi?a˽(?KI1LǬ.{n7͇MxrC[}._G,/oMFh;w+\YCa(I;A id7ޱ4pwk}Cs/+{#+72dʭ>9;Gq<ăj[RyO·Vocoi.BOyp6%_0'IS|Ó 8]8aʦ\њ8ԇ[c^ }i˱L,\|ֱC [Ȑ6}N\/=ln$FΉACЉyl4X``A<5&J='9C \i=%pٞc!#C6D}d䢽p,'OR, Hj0IuGuEm`j7a6U gm ,ΥԲ(S9H c,ʅl.<&V^?L^͘ԋ]e"_Ѝ֟yyQ_9-g^?~E+y8|}ŝ>CYpe#â >g? e;6rgNf~HЙ"c YO,l?Cg>u2yϼ\̫~#Gll[8N#& Xa"\Ǭ9-q9ǜU6pֵCǦ 8/Q79xr?dbѵ[us=um j`bQ+rőӅNBC Ňx9Yh1~AI%8c͍Oxu86㐳j9G[tkDԷ,^Q% x W0e<0O'gPNKewm__ʅ fJ;k₆!vL [1ϖvT@eW]MlBM|x_;ѫ)ihw}:P&l2籘90bO&6a1P\v98{r6 nVHY3r5ؓMc?sH]yqI&/ig8ӄ\)Ey|l,N<Ԣ1Տ(-pl.؄Xbrxyܶg#yYӋXY6cXtlιnClXȶkˉ=$"0b2FGSv`U:J~r9ѭoͣL} #c!g6Rnn0aC2Xr^?XY C"tY̝" RcDm#Q&\k8CK*U[YkAe}?yH|~137zy?K.=v=8s\ajc1? qU'62vDJ8nΡiȁ56YlEF.!m#a>dd쒲UGLC#I Ga8 c\p;X3 W1`/r-󃃌9eY8Cr{e[\q6G 'kϐyk!PVOa l}5[tqłSAkWϒ dXYY %!|*%G'??>'_?ӷWg=WU"<0Jmڵ/עr%xk&ܦNpazc8f8 ln+Í CXL[ǜ–VĄtp.sRZ"gLrNvt+#c"6L%nMerCsa;$^,*d(6:.008?XK'vaMt"y5hq`~djןDadžZi G/g\ZWoW gb)N&@3S4GXG_}Y04*١څauSa?~N|,_owuo9b~˓?>yXQϕt(chCc]yB9&G)[~R?>#C 8lvqKM8sqjM9?y˵g`k>lG&'D^MN>1/g|b͏/|$N9R NC2١3HLHa.8Zf' ϝ5|b)o }(>;l>H< >`˖uwMܦ;x"9ծ*/]}mo\&'_BBի*O]Y? C*@գfny gUVvKvZ?[Up\)7ϼ Wٮ+BSןxϼ| ]RyE'Uq g 3p8(8⭧7?gsy.'A Yym6O#Lc>fmpx6Iƪ8 kCvC l܁4v_"ÃCS M-o}ngv#;]B,"#60>3<[EyB,1dA;yJQJaܨVllD' K6Z"M~2`y\ xG6_7|^S y6<ɦyןy\@n6O:80i^?6^ ZɂccΚ|,*`gc" >!6|9AqQ]@#ݼ;VV9ǁK]r౳k9Uj:7K|g dCgpH{Fw(8?9PvC'`d?\ERKݼ plb!VSl`3Ν+_|]:qn5`cY?8ΏCld[lq ᶏ:Œyx}99Fsj77:!dG$&nB]t..s݅ée}t.ذ\pɑ=mmT'vr&넩ġ.p`glmpW }@6_YowQ⚧,p0VR}IY? Jc3riidX4Ͽ2VT9'c^׉tnpd84rQ?()_=e7[O|zlԡ:X0#0Dyb 9Fw|*c+S䲮L, 'yOq6lCȐxۘumCƎΜzkG |ڌÇ Zi~eۥn0n#_Ñ4v,7$lD ; qĀq{m07 XsO{kbXI>槾5Б!#wcF 6b')궎 #Ņa}P74}݊_o;>2 y0<'<ڥƼ/'XKv6kCo`Am׆a92Sع۠ 6s\,Xf̅B,8ƦHl 6^q1Fp|\9!3C͌#9. s 25 va׆65s~drgN:HrXL|nE!`loC,}ƚ?i]p5x 9h~  yZpnsey8^sÕȧ[Ν`f9B(c(p(Gd^yQc oaP vhY|4DO>yɣR ן[ua2Bw)6'X<8s18|!6ܕ#䁈c#yxl:\§n[O_[/DGۤLnb= uj)vB14ZD?N1lob#C9 rlYk,vb,@c#'l~۔xlAW/F @!7{VKC_ǭs 4ģ@~.c4 v. (:sP@Y' 4ģ@0r0cd8P v. (<8 ux` HC< 4:+C9Fe`';}?~zzP'F%=Ɯy02~I2~H<F#6̃ߚe Elw !*Ӗ<+'\'!goɏ2/|+gxlt!8  7!܅n4{,n n>S&Ă5zq!CyБs?O>'o}<]͇Xdhk^Οs<}"6cTc;iœ1,Y_qlc;$QB~ ;@^c`;jlqajX$.t0Y7vNg,%Xl969~. bA:DvT!)I;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(xuoܫI;VM\pա[qj&QhZ5rWnƽɛDi47^&oŎU(7S,Oxbc "P\Ģ _<6XA<ܜ.hd;y˼rk1Om7zé & 670mEƾ ;!Ɠ:B8>~|pbi6?x\@%Y;d}89|r5:xbi;8t0$8n}F.6d7[a!dbaNVz 7 9l-]o|W*d޳tI ^n'Ugkz仪P!'MRr;BN=[Kכ U 9l-]oIr2pZ$j7TgkzN{&)wUBN=[Kכ vR {7IA r2pZ$y/*d޳tI ]n{&){T!'MRvC {7IA 9l-]o|W*d޳tI ^n'Ugkz仪P!'MRr;BN=[Kכ U 9l-]oIr2pZ$j7TgkzN{&)wUBN=[Kכ vR {7IA r y 9-#~!;:~&y'8tsN1k=q~6u7v6ۇ|XGpC|^uc^bYGl:9VA9Hp'@PnXdݍmsϝTkbolJ BGf}! '.7б>pclvml?[8~Ta*'nJNf֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>DZWޠawS04֕7fݔ&Mu *Fap7 #@i]y hQMi>22}/3>8a ts\!x.`s< >IspNMXñsM>ہy.5Cֱ]'53_ly0n}bvPqؐUŐ>8 5 <F&eQtkbٰClߴ_jQpl.f<[K},y#h8Closk/gEݼBw lg~|pwL<Зgrcs.|+3/wûj8s/o$2vmbqrlCp|27F~afmU?W2F5Y|>d1Yƿ# "HmD. ³nm_やg}8usao^\K,O!">\~0|NHr1/P!L &\n(Hx3MchC>2&y`US pMlBemy\!]Q8uph%\PpYyy-ׄ|aHW*Lkb>&? CyYΐ Ypl`W%8mclb wט;2cs 1wNs!,ys v뛃k ;7'FmUCmJ{ F&g#IOnt"F,6;bFdo'շ`3>R?/p7Bǯx:9'!ƺ%lc'B} 3ore腈$񣦲[b*MYy9])=Rw?Cm~98x)eІ][t)=R;h`)}$t9*eSH*UsU˦,T1boGMY#TcߎV/GRzOLj ^6eR3 Bcc}Q\` /1d.`@,2J-gFԚɃL<̅kԇ 2m~xkaksxsoэCĢة Dt!"q;esh yoYdчn.lGv >cksZO1 cc?mmfݫqW!HbF-@hbr@'فQ6=` &䀐O'Vmzf}HFMIv`5tԪM9KgHOO'Vmz7Ͽy3M'L'فQ6=<&&kU'K̑!ƆBsLYDz\IOcK=8~kXNXu!v.:,6udlԣ> ;$} ԄrYc|I౷&>*`air6` 1g 1aO>1ڐSG&q$ gѽ8A'|g Wj" l`xۇ(W"ࢪ_^EC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐޚ+jUEC{kV<[HWRFy iYo!!^K]1f󾅄 z.etǐbZP2ŜWr' 5N1lX3!c$:AӚ(Nn!p|0 x:"'vŧc3.=:L &|vxd9a#7\?xk[7L8ḃn].đ~|b\rA_ mrPV^1/r*3X Ҡ`E| ~UŴOZ#I ିLTyO7t#7?α8C> #0?>1J*}򕓮͹ymd^#0?3\I|?k ޴C}йR3DgnM~5eNWe|ƀG1|N$X62ۂͶb+Glt.nmp!|pj}0 :Cp s׎.y8}lb&<~ Q&7ṷ|!awSc|;t1;H6_C}9r0eCΔˢ\2%9Ar9aC>6}a.vkl5)s[#BS˹:6d[2v&o Sٶݑc5X!6ԆdK ϼy cզyM.| > vx@"& -l<5.E7rC.b#&d.?\mlHmjh4DWO7 Uguh;br1t Ug?qRב&7A|29u׏%#yd :Xu<8'JrG&7At2yq_?NPՓMnd(bC0~(Ϋ'PĪRY))RE {^`n^t0![^xA'D|#[;s_d><ȅ6' >pƄX du8q16Cz=bՍq6ao n0y!ㇰa&.ǚrbonSg}o[n06ܭ6;d"W>Z}% 2 Zy ZRs`/zƥ qW>y9 ,=!6'|ޭzre76~䣅9y-y#cdk,"r1N;:y 0m~t냕Q+ϵB16nr~jy'Nnd;Af}9[.z a;~`=!\?.ܠCMLRf8t>:^Gllw- ܅s)O"mO{]<7ϿyBޠ{]мϼO($i{ w- ܅}]Yacc` Y؀C[ ']ٌm[?qC7,BǼsxd7|̇s,5  u1Ă>2d,ṍGh+ uO&{sbu!tb$;bM>0X|/y)R=g~p*C,'8sZ:p \d QY?h/-"~T!Z8;LqҮvQq ,2KoPsm2n֟?yŅ 3^ʅ^g^J$&"K#Jpvg~3aBXijDyU1dQG}Ȣ >g;T8k:vl䄃_uYDž\q ~􌳶Xl>r#'wdml/IF rB|錝̓h,)xd%6La[X Dxdj8ַM`!xSO;\X!ƍ:knUg距Uy0?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2#Q@IDAT?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2?h.y2hw'Ӎ("\|9-qkqYՉaxk3y$Vj`7n}8 ui㑍'C!i64S  j`bQ+rőӅNBC Ňx9Yh1~A1n!Y6޺pl!gXo{G[t6~toY J$`(;y`*bO&64t(Y~Pmbhv<Q=璜Mly\?:G߈\'{r6 <g(_!lbڸw<ێ#PƥNd;d82p|e#cc>b&.؄Ă;`726!l3sdt6c36%ڳl<}. 8ys#ԗذ˹mז{H/D4aБ!e> קtr [:#GADG+nC,5Ϣdum!ڥ ַ`ÆL=eb!,y sYN!LeC*=16GYr9&bTbT&11C"ȢbInA;ncyN:n.d}rk'D.?X6A $X6(0u#S$ >6|BO[ȥ?m~Am"h9rm8iSFDЅF%!%qP\5 H!sbG 'm10b[Y)#wXbe}8Db>qA3c&kb!' 2:\(W FG6y%A|F-vȦY^h<.iXfQ{́#E1η1lYll<7?..=ZF1lןyןy႑ Rsۀ7j /G6Yd:s<&-٘?;P.f!sl ␙F#;Xbknus9R:xl,su`k9UI.[g}dCgpH{Fw(8?9PvC'`8x~( &ċyA Cͭ> 0f 6Y Ν;W뗆ݧ%X5Ȣ=I`$9_&\`ˊS >hW_YÏoe0:;H!r$G'Ae_9tPY@|Ua_9 7)End9Hc16N1evxX=eyѣÆ,EnOnOZo 29&:_dǦtZ ! 0j*Qf0uro0ӆ9w?06pa?k?ۂi?.?~;ϕ=ra,J+dtrh|@ؗ_Y<2 cW ËDK.X+tJ9vG?Hqt)H :4NDFY|K,ɱCwb{K{:_ ON) C?//0?e4?z|x5%0F\u5h'k Am;t2͓#(#.{,[1>ȮBl ]u f9g2]:?<|WⓉ%M6O<!AsN,xI8A&<lЩSdYl\c&.L7TyMQXp%#~!wph1RûgXVZoO*\-/aW;WtۿQgοs1_siLƮqqׅ\t) U_rx4h<l@^|Љ-;/wup0? =O}<2 K9׉Wd{tgs wPd|@|n?.NUlDr.6W6;4h3d ~]zGo0.q?*QdӅ`lO14\7>FG>~&D|O=!})'f',~~ٳgoZТTЃ _9A]M!mȻIICosi?}-3%\!jX譡ycG_Y"ẃEh(D0v;t^ENlB@%~ S8(0 f{P2U !cQD~sV3?:,Qo^0y9tA?ytɔn7p1058Do"Xz8iVady঱ 2{>ogXjT;s5ןsC\lچn_{5n~UG1bJʖdp ze)Љ'Wmna]tyr<|h]8+ؤLN6GnJKJ\w%?tñzDxP}~mn⣵crdi+49P 1!袁r.Xэr y'CӏMk/|჌nlƒAgl`|$_ jh N{ɘ?ZڅWLiͬF*9_wkjUϑAd(ipa(}j{g79w?s\\%kfW/'i+Ku"vVrP@}oUP?uKE7-V_.%>| D@a1%<$>rT`lTP?Vx?L,~=6r| \Ƀ~G9~&]d+N-×-r9$&=K}=ӾFĹYsiW湾t8KlIf\$ق)+oa zO[`nO[kˬcXSNF[Ϲ}'<ץƟMWak;=wn9Ɂբǁ0.ᅯrHBD Mc'}4oϙw=y)En]^%|\1A$GLX+ArLϖylC^,1ۇG}(AT@*Q?4}rs(/ZJ\8qcN|Xغ󁩷?y>'G:E';{Uk)q~yIKn'j +i3'_Ї9jt@N6=vd3o1f֨g;9\\?Stck[0{쿊kx;oJ- 4J bP+CJeSROȭ%72oR6gO=-f2]49F9Ĺ/ж>Ϲ~?Z}}Ya;es@G9꥾^lAF:Os&l%=u#h]y|Q5\0m1˅FGtgdMN|IٛӏNrM)Gw8$9CG?ʒw r*^d8iا)J;W8R7b){rS>l?{Ȣ;hI]ǃG9o-S=[r77CW:"r=.~ˆS1^ct?UbsPf763lo3ߚR3߹hk+uCWB k@ßgIA/8)v^Y4{s]WrBR5^@EsIr^VbGO99. yD'>t{^9vZv?Ѯc#p(E/Axt9H/M⓹@2r|Oݘ;Ylx9(XGhx᱉_;a'6\&w}c=zmzNSeI,6NcذLF9\FqVu_EShS&^&ggB2RVghas\WǹΘa?%#,Rqǎs , fꗲC{T]tG;.'hzc-,@_|2.?l'S.V'Ƽ_7_8I1&2W/g맯}E/,4ݴu:z ZͶMMQ*7GtꪸgnhoygD2 sqοs1_m;ןP5@w5:Q|GϾlv ~?;VR1<첷ed`r(@CK.JcI:>D'4}+qAl!g3<4KLv.@/9+ vevGdÕRb^tRs#r8r|Ԋ$ hz.1@H&tlξ&So2$bӗ } F0?钅/0^슼(] 0 _*Irivx|ÑO-VF7#ğr'CEG1Or &8N&~鰏-C@/(婘" X5}|CbF4пTkК8`\קv84k*7qP[YgIkJywοs1_sٖsÕOXL)kḗQW69xP}#"S_9`A _A00;].M'%t]{1 9V> 'ħy|K΁@R#+Ry8zDBыM^dE6Ą ,W9:'#Ğ ?WtL#2y|sX|6dQ侘t#_0+KuC}ϖdUIДW|Xr2Ҡs W@Wٚ~;矾}bοsQ=aJ-kl\} `י_-1箫td?!՟M 'Sl"pB;,/0=IxEVÚT?$&O"^,2NS{|9觌'~b'ɕ +#%$D2ЕHdIlË,e2ɃHS*%_y%Dn|*+yxlѹ>~v+IUZ^R7:\ik}|x5ՍPR?/^;)\&3VFC"qoOt:fƯֳWâ9s;9\\n~?֣meZJf?6^G|ɇ]"6'djtK=eTd+O 9xatCx d_}d/Ziz|%G!")@rSdc_FMj|1O^ObAK?l|0ϕY$җt7m?TP{yj@Gڟ~ySylDGeodSF+6fт;`ihua?.gjֆZϭ$+1￵Q>j gοs68f mlcf5\XvlKؘWhGDLS5_YiU'O\z/w%iУv#&~oEU5o8KYzc^ɧȦ F֞f/>}>FYDܛ}/~NrUh1K8Gcœ<^*ߘF.x䉓rZܱ.8h "I}]$"7dbB=Dž]I1s CpWØvR< <-H_au7IW<赢r6q}8hrZ}DG5I\^Tg}Ǎ_dt_:ͲqY&I4<9\G[sW/#mMV.篏[z1guQ?x~eu<;.A1a >~:X!m2Çrb ]e@1O#+d_"WN|x':7"%nxK>[Z\21"G=e=4@ 2"<:6Le2K;swbDC,ؠ gox _c<UB %Cc8eCe6T$~4|"g0>L/Q/8pm ^ml偗UdԩS1=u Z7ӜbgZn_ U×k*R:-*P۪3~oo>=L_-Vqv}$?[X,Wgɉ.Ha` rf"G`6 bz/:9p̙7*w`&DzjL$D?m4;D_ld!s6@s锿A+'ϲ'NXl]}ZQꯟz?]G?Xߣ'O]O;rꛐ^v 2 peOˎv)sft'D&nlBI>h+c,F+xLrxlGDic䌃%a'KJEnv}tȁr92d_`%>#w">8En큎N@x.Xba'S~\=!soKVY1j=y{ڥUpE4r$~iyhksS-Ao92OtѼc4=9gοsF\Us긵IM"?eY[O>ӧO{Bmu9 FN~T95D_F?t £cp?ұ?n̞Onя<;l^$$2H$t,tm4r'{t?9 OEr'ßr:|/4[@APbӏbOy'(TR}r$w蚸W_Ell|t#Cb_h;ĩ~ƿ|pafûW?y;ێd|uؕ=n6nٓӳ?Φ. RK@h܆1_J0:WЉ@}1^A"?$?:馌/@R1=a kKrARp|ih|]sU&`OMxcbIoSfܕk@貃'䠇a+< ٥;d83=R 63Pe-"8it$Ma"[~ldMsY(ĉ[?7y'l;9Aa[W':r?{1Q7E_z22o];NWu'ٳ7a`L\tla|<) 0}:&>GXt|+҉OĊ.}sKʀ|>60_fa ^|E>r,<G~&$#CAёNáwl#1mA4(n%'P/m ~#&ѣ{V'+Q=XrKl^SӞt)rm ϰ4?ki nx$ޗV~ko.8R~Gűqxa|0ud[sE='hhοsѶ_S_K2VC?MQa[=ȼe%a_kw/n{ser4 :ztG/1ń]ً~r#~1VhFv?O䉶/>#+O6rb ]dq فb$@cUN=v!>h>T"~ᱲe.Y]?`>4>y'ٻYAz&э2~Y\c9RJ^ wHi5bec/4۶sIE/~щ `?qRUmg79woBO=;sJmHϹ%c:yj.aۛ;tY䐆m^9Rd)ώ>~<ƒ˪z<X :>W(ED-8<b IF7bΥ]84d|t!F]iD66o/ &O|wR<X/GG0[̇ B'?EnO8$OL BYgXLiBu퐹Xȇa^UR3PzodX$B'^P?{gbyP (3ȶ57gοl?k?{?j^ @uٳ_Zar]@!~!= ~K v ӅD_ ȓ>x`cNߥtsXY3J,>I% yrA_wVQzP/ _0;?~FNta:i|(c&+VK|F:/ 8x;:h@+K+wx\קm C&FuNQ-u9\-̎Eǰ.P+Kg:^e?sOMmhοm%2sUC[c}:c?֟i\zh.S0vq=YmB8)Goĉ<|d^doK~|+A s 8 ? *E/b5ϡsׯr iP9H)r%GG}tM) ?4~bG>6E9yۏ3L?F)S֕IGMf]NIe6ymJҫ6ZHtܸMv㟸فL=!Q5ۿuyƄ9'O<0>w?k?kLo+sQлĶ~;Ε=raҜd52Y)2.':=vmV+<>=8ã;4r` '#+r;HKˉ7/}g==ygOs+r`?v sq;O{?{Dmo&sNS[NPD'>"?Geɉ\Ga}'+9[1>OC8$8At$ ?щ,I1^XO=!:4~FYbtDgR&~|ޝ~_;:sޮ1ԦYph;T2F3n/!KwFS~O4A7a3t u⟨,W?{}X~B:yΧ> e+'vc9Hȓ2'ӱ76<:l4.&Og˂藌7!2<2 K9]dvM?pԕ^hS&D&/~ϩ7:.{+Kh a& O (r̹<E|9>&\t^g_+Kg_M s;}5_a?[+-[1sqW@K}Q.'ᇇaLdx(KnlsG}tFWtǽvtWl@>{''g /KFӈt!$Kr*E6]8?YtOxd|ibm|4>}8 \#1O=D})'&]4:Hlj~'d&e =VϴQ_* 02hBs=ٓ5v }'m[2df79w?s돹jK}snl #A7.jOn~![gԚN`ta);\ۋvUle.&6~ 1Āls1?6>ėK|é,._AEA16`t)LPzcPt bd@~%E/16 mş> `6=ȕ֦\\n_Á~G]#+k8\ ItlM3́ȧ?^t AG8mċ2=q۪0.:чG]4.N]B(,*0 az8Yvx@N{#ػk|MgWƕE6H|8G!=M"~bǏ2}rY>˧ON&'Hxt]\=e @?u|o\-C& o5PhU cV|K?Y=Ў{<]s"8oe7Q|@IDATFHhlW[ˆ6ǟ>TT{w?s돹O#>AGy 'dR}.:e8"}wcӂy -<Vqlŏd@Wܕ2ymOOt؏U'~ŏ^M|t&vOMN(pp) 0zc`LH9hKb>?V2ttcH^p#gZ &}䃌/r~A|O<:36ɡDMN|:C/'ٿ~zz?{t5@23 ۏW*ѫL LŃVY?/mRz<8>]RGLJWF_Vٕ|OؕJO߱'~L.8En?Maő78WhI8ÃQJBl]0=Qdc بQ?2|>''%~Ɠ>]/b‰?!uRt~r<)JjAP.jB~yJB3jW1:pʪ9}o3߹륹Ϲzj_ 2o[7f9ُ+{gI7|bCӏAL@8~_v@1MjӋ.vxorzxٗ'GwȕSďćʗM&wXI($gϏAF|$.T[,؉ ҠG'Wwp:I#>ŏ,Vӧ80{ȓ/|{.GJp;l)ptڃ,"9Wv+vݦ'3߾mg79@kP:ߵ=ZTgοs*Ɗ/3ן\o{CV?{}dmjVt)E98 ȶo'68>l#^7A@|!-?uP"D]>8zK6u1 3b4ST(|c<ꇦO!B[ą >ćt'qy6lq;_ll6S{6<1R:kUlXQ˃v<= mŌ*{zW5plK[?k7ǟ9g?s돹ϹNv:}'d+:{V.yao8<~У?{&~lGD2 >2C~G &<1rEl7::#MH +C!6&Wc9X ɋ^'^9r|Ei(B^X. x9Ae9&n3:Eǁxɋb'^dwzj_6ה A؏ڙ̹ʬkf-<[CF# i56yg˂uو9#bޝo5IC;돹>\tk<Ͼ~o:w9x9L)CGw4 \tuyR&rH.qwnKNw[ywfäRK /yz9]yt#Z_N.q@/9CG?/ s+^ !EhXFT2҈O;_nqĀ;t0^$ C'N[xC/Z O2$6VR '뿹d_8X9.}NfjAdf79ҽ`p+m)z?lskhmt)j;9=zBGRi0U(u CUdx+G0FNFĴ9%74o@2}bņ~x+[k;42߉?ѮC $@|$K+AHU?b/r@L9O/vdArF'^V92Y?DK;7g%?е@)Y-&/\G5/+UHgO]7F t'ne-f~>2߼3_d?s돹Ogs=Y)u'R;̞r}$L#_:''SK^../vctRl^1ħŏ,XbO?\iWg{%A@)Wç $FY.|tGW@}1/S|nI/#Xta؎nr$/;H?e>qS, MSjS9+iiCAٕ}fu@}(ilf7hcgk2Y'""9WX2Noz\\:ҙøp 2pκ 9LAe*}=e7&%dtR~p|cO7Ȕdx)G_<1`>?>|> yl(x. |JhS!$$] 5Nh28Ҡt'~]dx% %f5AL`>Ïrle>A|ѣC7!s35whYiK*Ty e'HLь~D=!sԷZko?9:M,a0_w\\\n@qWқTw4faٮSf}>~2Z2|h0ڄG.Y;Zz.:ي%rGe1Kd`NO2 O|O a+Bt)H0:cGrd4NK#ŋm;qF7؄O7qM3YbE Ƈx1\!Nl9uD;s{Q&Xh_0,}]M ~s j, 5T-i'^di?LؿkBmec\Ϲr?m}K}ZsX>MB$t\9(ɕE3+h'8+/@ȕK|<+GGx|ڗGr-9,4,DtRs b&xI,ѩPbC<+cc Y)1cK9>˨wr^aC $xO]G̙3*˚(^gSܪ2mOо[nLzHS*}Y'd4ljby@߿at|9`!Kx>-<=CGޅRpgʱ?1N/"[||װC'W80L&~b6C@b&L$D2l`d;6YlƊ9Yʉ."> ȒG,6E&E66΁ :u ;y.toeW'tЉn?u@w{[B`r4p)KounHק\`ݣEn&,E s|^?<}3G|wyzO|]oE5ǪƶmPb?{5qT/Q5Պs%--oy=MGjrl_-ƶ6]fX $}uK˧?וs5_n;y[kzP׎-Qhp@G'gڕ~I|2e@#m\ENL>/AWlŦ_G آ]A>U$s9rdA9vILr/GN?~lr*LS7zh|#"SN^0}2WYL];e)_ۡ8d\y*&yW\/Oso -Hjrr$bz:*QfSGOʆ?ωoNo7߼+K]s5^zJ9˿G}tۭkw!f!,=etk^ˋEOj{G>7W$b8ϋ_ˋ_ܼċmbЏckO_VV]~/r<>1ن6|ٛ\ VÕEn1~#_b $N94_DH'u㋎x䉓rZܱ.5:EO D'%e:iԛ?l#u+@7Q"ӁM'd zz婍ƭDUZzkj`C??_wuw|/7tϖ?̟|U㷶x,/\F|gi떿{}~y_뉄_?j_Z~9ZO_;dʼnr(idki%} /vϯh_?Rculֻ_|ז??i 9bn?OG_ZKoY~>Pߏgt׿fng6 w5eǿhm/·@x`T? z~euA:X1؇tE]ev t[RF Nx>EzY ZSN&.ЍQ9W$^ʉ_xF0aʗQ'&Q.hxW+gEG=ʕF#M$X\trP?'?GG=^eCćzq {2f3.|}ho9J!&ŶD?= DU=wyӛ޴=ޑ哿o~7*V}e2ij tiUV׭Om5EӍ7-o|O,oX_wB;tW;,w?5l]v}0r=ᗗ|3Oj;,olߺrsozB{>W̿76WU\<?~}*.˯/~ѽ޵S{:ԿM*3~O}V) ? ZC[2_L=V< nQ]"˧H7ztAĉ,6&qN5.l2 xٻMNr9MB %CcD\M K]?%K9`7 g0Cb/>zI|zl6e v1trg=ŏ F#u S?{] k%im$xrju۶l@Aq:)Q&'7ayK^ZZ>[:x_|sCK@[kg_WX?߾TsK&{{ᇗ_>wt;,;j=Pe?4^.?} w?Ϟqa㯹~/GhX/7qy ^|c/yG>__i }*yJm?_z+/=~}X_>{OW4/(; :m?kod?=~d>V?{];d-i>ymIBsҵڧⱋN[Ll)Ydac7艅vH'Ψǟrb)K}$et6,إ 8L* Tz-Pb5𣏟Ôa@oA9<.#f0}٤-#G/) ]>aW"P锿R('4WWy&tE8]grݵ_w~wOyV2CbZǰ<7~|+m=ZJ1?>W۰8OktWzy5ht2%?g+zx9^&_ڴlop$W8&`ydz?̇?zPA[՟E/Z~G~d/t_m}lk8w{P|隍s>F{O[n7}G_<}roo+z[{N;iTj_YfVCqOwx-ܒζ}˃_VN-^U]O;* ~2k-T'nw8?yksM.z§_wh}˷|rS]}<r|ϻcXІ?m|f9[+-5^6Z+v5Y^=߳/c׽޻e?}3??|o<__~dG}v|RZ0M~cVcʥǿGwՓ6fR||yB}.d~𷖏cX^s-?)6!;dگ,>>G~x; 9D'?dnCtRc ']\^+4كiqZ[Sp[H']@x9X`H$2,t>mW"LGOhC+/ѻ|uL'&;nr٠o:.=Wbm@zz(ǷX|H4~ tM RއG}D K0 X[ : N3<,4HbMNaK79'+~ܪ؞l\L8p~bO7`v9Dp=gL/kN4:j|m=3Ec`9f^z^g+K,>X~_8qK}[ԟi_O/}-7_#m=Rͼ{|+_Ulwˏq`2gG~OWTxӛ~<{/\1}w.ջVV;~iׁR Պo{C_z-Z+VlW]C*(ӈVzbժO,)yg^9yߩK?Oju~dyp?~wC[(z|ܯ,_u?V2:Mz[ 2d= dliȡrgXy$2@r,\s9>\~b||ā٥Mوm/q]p'`IahPtx8h*# $>Yl#cVL/~r*r$h/ڕ {gsO>G䄾n"c^zY>u=5AjsjK&j 7u} r|ˉ783~k ru 9߷Ԛ3G+^GkkGWŘ߯j,e [厷u+Okٳ˧?GG뉘xmo{n_^җ>Z;qu9V.7>Ymr_sKu/y.;Ote}[xC˝wo9{s7׿Ƥ̱R=e߇+K^++^v 3οtO:~t?=?&*-{LW-:W/>ns SŤ'+tOtؗzDaقM|/N}dE6:Ѐʉ @$Ih$/vK N=v!>T.H#).~ƶCb?AAӏ<|H'|OG/腟\J͘GI6WOȴ/ރLue/]uZOc߱VMmy2u'NpE1ץ=^|K^zw_1Ǯ~vy_}H9[nU|[(VkIp$?|t؞;?՟k0x#|3RZX{S7Z8S/^':pM?~;=yO==`9Gz澅^?/-?ojƿu8~?,n>mܗ<-}|FWէ-.n_Y:|+C@m祾d.tG]i|H9f+|wrv̅ *];ɯʉfb`)v0f_{v yyė(^dE.I2*BK9p > u"ǘ3ʉ b >G[ <8?.|;6bE/]$yy_Yr 5JrG6vb_xу˭Wl\>tĞ/9]6ЩKr-V_|. nr%O@HŌ{^Տ*W8{__coy:g3sC1v\fmO_RY{u 5W>CCL\[z֗ZuJVyYN;dw*Qg4`\Whe!n]t> D?e.רo#gdʡȊl1<2qVq16?ʻt|/z BH}:>4؇>7WڵAIW22.;Q]mCS 4y}ɊEYʋ]uKy1?Pt2=p~Dp: yx&/ AL'p>-K$q>{HlW|> 'K!wPIU__uX~wB/?wnu+2T4lfTWjXm_u~o Y3 '֭etF,jP!Jc~׮K}3.LUHsngJ? z4C&}񜮿sLO{^9ԭW?oRwu\_|5|76zKFvp W1:cck˿K}tl|wYc46k%};d_F&o[?;dx?̇"5&  .CfBOF - Yl el6&_ܘ$LYx-많y@DbhO#dũBu 5 j>k raƇ6(> [4ӕuc%ƁptHёP2S%GFϬXqA0L#Wb#e+z%JJ^^W䑭X%p >tҥQЯ_wu{6cOT,YV[=_liZiܸRPQ:8_@wRzW&i̶kI>n?Q#p:O׫`Jwnr_w/b3gn+׿?G>f_wߎ[oMwڟVtŗ&fq嗧w/_ih/>r?;Gc7v,Getn}ёXleGN_-gwQĆ ߱JcEgXpI6'SN&t׮p`322M>tiN6qؠ>^ ]#-Y['R2`տ<|_6( <.3LjΔ)65htxWkCG9pd,e1<ö31MmX7SjYpȁ6o8h W26˭+W1u5mXgr<=oxdN0b!_\nf' !Y蕻b 3ߚXɨ7?~'^t뭷~;?Q_,NӓYq?Ѵ$ާp+_Ǻt=?=rqO=XEc$.+XvaO?QOw_ Ǟ6RmͻOm:ƟW<ɣWm<>s;e.tGnҷc?m0GA3g5KK~=cE]Jҗ<\0ۿ?CS$jſ2;>ϖ)[c>!w3 QoތT?&iS2=PυAo{+s?mcƿ;n Dύ.):z_rjn;C&rT.@ٍW榋n((O@LRW9֣4/-eŽkeqr rxb6`ziifyƱMǾqBWOgu)p lB\}mױ8GPYlڗ/i_sxG<ؕu?8!qeps}LEaӤ Y ~ -g)96y/W~{;}_6d#KVAL?ta~+~DEG$ .׉w'o!^wQdSsX^p:Crw3yw̟ }S|Ձo֌#h;7.Oו3tMq?͞{/Ķiַ_v {8<27͋1=a}7zڇ^c`tI.,8LY#K,Z59$ЁF>)m lsYt4HN@u g6( }aꘑn`eyNGʠ3 _F(P.<P:?/}{QMjxAn˶&?N}S6d&{OTgu[ِ_w7pcv^'c'޾7oJ@Cڗt;\7it /ϛ6mtJ,ϋ/2eK2WvG LOw1g]w&/?{ꩧrݮ~Zzސf͚+zЃ+s/D? as呥ƆLeh_tє?{ݽC~F_-/'~\"6x] \-;`g/?:-ohY)怷 ??+81d?$0‹l)Tt2 swȄ5($HѬYR''X֔8knAYl`jG4KkwcOֵc1d Pۃ,qS&@3.:ڂ.Xn#OH!I=B{TcɁ!# rՏ6Q\ݺA/ʑ׍, , ^p\踁ukƁ, GȰQW0/폔 <2a\HY` ݜ1BfFv^JN&ւa^פ +K8vsgx͂K gCf^"_(qD•JFŋ̀?bѢx`'v q!!VO6?/tqϔ}k_i{?._tae Yߟp/Wb,L_⨿@K_LC?Ik֭Mx"X|DZeiGDhq1-mݺ5}_MO!lF9;ԐD@@Ę;dxnt]"̺5}bku!hnjS2@OټuS\ 1n]?etqx™esә g҈NW~Wpȡ9dvwԬW!?Y/,҆x UlIlؙm%otſֆCHZR_ 6dCs\& s g N5VM=t/OдS uu/~[卛z'6kyp> _$5_c8ز꘣;^0ɭ P| Ժ4L-N&!זƦlmNmcXFY}Cȥ+OB9^}:Yx|4F (veCN~Va_qЩ.4iiu!C3/OH\~awA^`"EyC&:N{uzӛ߼EL|fǿZw0@ܹ==;v?3'Ը񾃗eN?JlOw}O:bL_ۑzW_v-P|Kxăoml$~ؐ !3k֌ؼesWƦ󺮛 "TaQ6bv _ypzKܟJULgA!l. R2v "_o1h??d!=_'n)H":`3fHOy3MцUJ H_ՙ;wy]N;7|cktMO>tK?ﮡC;GKk6_Yr?+9uqo6l̈l۶%}_xM˗N8>6eFxoδchzGOc|;^\خaQzl>IK!"4ȰR(%L7pvy*??{$y wȰ~Hn@$g Oy^ :92ɡ Ӄ6SXb)9?чGkWʩȡce#g{PF?lC6H692ڴ @FR@{{mPx5m~) tqmaeu㑥K>R;8q];Y ~^:MM삅^ϻOwT}د wƆ?w| W{/tE?_7|0Ck<6ߟ'??l}LG(W#>,Ư>- v~0cU"7yCyt2x4%GƣIK;)m['%n#7io}K:sdGJlܷjUp7k6d^,g[&Yjo$oE3L؄q6?޵涸ȜѦyv(olLzR#oG^;3#LO%{>SNؐEbLur@NhmKe3Ӯ2A-#K<8t1RQFDXKYA:OY:@Y "aؤl6qqǿwe􀮿{o Uz?\?-8x^:3ҩS>m302 ?1n^veiGL<[{q\^ Oe]z krZ Lx8v\ƿٻ^G=<ΗoN'{zy$?:?Ba__9Og߁?&zU^qCru6z2U A7H"@豈&Ν2k Se!Oxc-m ?ص]deX_[NC<]Aǻr d8qa`pe@tƀφ Y׶OYQ~A)=}#g|f#@Y;j:~<>ܧ >wyP h8c!À=h0Q@!Pj/#E1;L61!nڴ1} {DA4uДjZ #Kar2Ddž;/,_YR<,]q_~d))WS;wcܕ2ϱag"x߁^|%G1;\sMqX҉xe!:y6Gw׿K=Z/ǒ1vsΏ#گ\۟K8,?tE1c w͌=cu=u]E}y/^(Sw:`{kNOo< .ޱKsN^ͱ>8u|!OwZxfE X!VϼG>@cNMݏo\؆q7 \:4/rx$\v6|o;@b,Gvp, F*vDXƨF |*j eQz0*6o9 &UK2uU'o`EMn1T5GX]|}GW\RcE357>G~ÍH|[ 6 S$^7MΛRzrt7slxq00 7茏?X\_:%?c*}=wySNNUz^6t)t?&}t%xQ/Zǻ@t= ǽn^3~ 4b> ۿT!Rx̎3fg[;cCooI?ݖ{߿$;j/z_g~rcGI3F~.=Ʉ?Vo\?#Ή2 m By?O{UWEbM ]۲Ɩn taE6h NBלЮj\_hhbh!܄=temL@S=h]|䌝_xLgXI" `vPWz]&PCNű/ fm#?t -HGG{7:_AՃ e%'M|C i2R0&"X,R.g d@܎ _yɞK?~ij|Kn X]iu|!C#NwM٠ǟfC2G㑥+>оE=4tߍCReduutB^Q]S);~;ٞfBqx/Cc<y6m1T݌'/"m}&h?FlܙxYEG{dSw$}T2eQϴ4_I Jmv<@ت>x(sK~?Gwm^)uLx 0:wxY~7,b/_B `ϲ10v˹;C&¼2\tܠp|H e)SUrMdI(+ xt@\"=m# 2>ee-2a{ƈ]v]?уV(~[?EȞ@c̵CLQKيmCCg^ <@?epU=F+2N Ǘz3J]m#mcW ;dbV 9 NW` Ƙ_KU]&%Ca0@a96vW|x9?QlLǡ`P⥼Jvg (pqb|3!wT2" OBTWI&A9)œїȳ\&d۷FiMhگvCǾ<;?6;K'>ڿ|1n/R P˹H&~S( e,}.ƿ-[4Kn??^X(D;*#mc|Uibv>@vw mBX9蜷C~҆OJ "T+] s~iֱ'lfEoHzx۰wyPo;dUW_DXgx,c ecBÓO=|'aMky@y RSvYmˇYݵ`gSчmq$v6KjԱ؄<i袧]>z'ne\b>_2бUW[T1/z]Ccb)2J# KY|q$ZUk^E I/<6d.]LGcbW&?,q 7%΁j7?Ogun߳o56o/1&^x=2y5)XBܱϿGy\s;@;[kx?'ú嶷 rrҜsߕF\BAM7dYfY;EsL&xl&|uJ{/+K\~)kv*Jb 7Y8 r+t7.rf]y$+\/Y\rG>8ȑ%׮u‡ YS/ sq) }'G:4}Sm11ĪcΤS,9YydI6 w1nLqƿqkq?a~TeoE/o9i۶x\/_J^x9]L>7=iSn\#zzB>^{|,#,b6F#" <1bZ9!mR6nOZ䈲!ڿvXq5_`z_:e1 qLͦe'6{Aڶ}k]ʼ;YE99s^Zfe܋?Gn8e) ȸohxdw˵v4x1rmfi]'f] )mg+e_cO G$8V]9-;dz:;-YY?_Wd_ދ஺:ԗwȰ6f4f2ỡA[$<thMdmcW[_uu6~k[jBF[deɑ1:S\z-]ቓ&2,#3.h`\f5PZY mT#@p+CrPRbBе!nlБ#^S&a-rWzav#OiC=(kld +WZvVYKQ)k!W-UH<_l˲x&7skh:X 1>]mIn_زu[ڴicz:IC#u%6EZmtvcpoYD`x\<{l8??ѽ) i]>mmTm{NIb2lrĢצ~?i6 ~7+ӌEG'צ{qY 6d.˽?u/ lQ</xd!zywR(dlLϻĈ?!F!Ԋ55wn#G@{[iKv`;dԅ|pçL@,.a]>qh9<}չr>r ] @u0lR8.tp1G߲.z6\6 ~ qeY}(g` tyڡb4 }Eԇlv9<`Z!+M'({ٻ9Yw,o/C^A IFiTpRXb脈(V5ȈB]zе e%K}R\V8noǿ߮'@Sl׮^FrE#KtM?jy:G#=+2hλmZ|KBC,2?X|e)돒φsÊcrQ\>f.^+IC15,p,Sߓ^ur5=aCXl2nA |xi T8kyZwmh:k[&rx3ڧ6N_4uSocZq[ҡrG^-ic?2߅ɻ_lg=FOxAxo]Z+(Aw3}2';vlk"^h6[X7BNY[Hѡ9Y)kS}yr?6M8rÖ!\?ǎ;Ю8'T@F V0.d[s j j>N!oë£,An\Pvm LmA N6`lj?3Ǎmc_.r-BW]͞5caw2;v2\dKHRah<;=^` Qԍ<" 2HHӚh4Ggmà?mv鮷qNW:w@Gb̝1#qi{o["@Lsxk,GSiM_IzS:84m̴轢o 2⥾L㎚4͘p۟h\mIO~bQC.IX|abN|+ӎz͵(oP77РGQ&'H:N\!@ =aYHW#Y@XNβPgJK®N0|wȬCf?)|[iqSVv?#A?ulD{D#qɉ<}216n7aJ9"8k_6bMrZ}gK 9krLW@s|6emC'GOCWy":CON@l,ꛣ/p4tk{P' !9a @!j}#/ [ǡM6*Iة7&ȈW e|e8?M_d,-q1nA^ۈboSOac0@N9n'.2U&[͔i1J͞9\8!Æ̠}[~$:}*C[_Da4OH4+6]GտlDp쮔}ia5'gE`yoZ5#j_}UsG$},s\C^4}Qi5u/+ 3;;;nz"xy.9$z^֮]}wsGbcYAyMȲ QZ_>4@;^iA 8W`#Km#?r͑)#Z\Ј򥓌Řx6 DAShƑ!c-+Xe  Yte{F 2E;A#NiփZ}`-9Q6>zi|S}~3Jgt2H@1¢g&~CNg)mh`(Mu2bC/Zh_Mvi2"G's;υh<^zS_2/ZΛ}E(͢ySQU1RΒEut Jzs;7W:a?.J˖՝~}#%W-r&eXnf}.#+OZ6z#BW4pRK3@ǿkilkwm+'0PpLNVRz kpбw > 6՗F =드2`N= |'έzyo8(eI>}?_7-7x(Jn`f!4OBj1!,JSG<b=? f”ؐy|ukEg_dο1SΓ6O{ZoWKKw¼-:)$![0Z#?GH+;,2iq{xcmm3?47>suwH4^Mtly|")\"ү1PF2ԿmaLbVM@\5ecFV<\r | hl *7aDG$A:ez(G#Vd%Cy?zP7 ھnhGEY=r?sMuH#&c}i`gCcjժ%^[TQ)`b|"3hTVn(s8bC|o^?>2YxZiڿvخ?o۷Gm{tg\ qޓY22}A.x/BM26pbC^G޹g`cIbG6?gsǥ"OܹGMۿ/?煾U*4ړ6'MQݜAGV{ ہ}#,K]@}m M@v )t ,KCwҳA*A7MQ\00<T WM 4Cm;=m*]lrrh3L,|ˇ&he˝0tt~I7Efvݺ BMX]2.Z92QhL x ~@q0k:&gS#ִ:DUtKrr9Ɵ6ƹ5Ӯm_q]?YËk&?g<=|FZlN:c᜴0^Bߘb惿g C-fUatmǶnYd;zK.]x}~2*Hlr1hκQfF!Bn!:V'i28|j au:$ЇqUZ\=Ɗ̕,ȣ<+Oc66c5$}QMn:XYlhSCS=[~ݽ E d>Pۆ7\V'rmïoY}bxlwwΛ9s!>XzO濬#`0!"l&t l)?9{f]w _ C&c4Fn6i_?‹8rhO]ͿY$P6JS(ޭbbɸ=ylliΝO}_+it#fK!w f`w,eR^2@ n@WFkVp7aΰ3G䓳 h.cC#gЭj[Fz߱{ɧrҦ#?o&oFX )ԙɆIR,gL Y#̼PF 3L$%KVOO?v׿[o??b?ϮLYLY^Ҹ;fAcʕ?wGGbY2kvphl^eeQWhQXz\^=[ȒKtpk(fԫ7bk8-#gy2ձm6.qLyh6NQQA@3(#o&|tK aJiW4 8@(u6eGNܴƭd+1A{u%:h֒xvPvM1Bct[!6;O\wt,@+QTL *Sxdi5S|[oo;?Um3nzg^6g=!9k]kgWDZe耛=Xeڦ  ?8n]nQW>*HWX~YYl:Ҵ.8W>'޼yק~:ܙP~   pM~ YE ( 6ŦjS_Zƿ63.\͛;o@8ծzsY¥L#sXF7JCsmYc ]@}b gKY[ĥ>82jk@3`PB=h%پr p_ 8bʒk܃dE8O gFǃo%ǿYq%x't=iЁ,>I ,<=dIG}d㨠]p ',I:y˦yC`ͱ)fu|߇&Ot fS3)/C/r;dؐOߎ@[o*^Zmk߾Ÿ͘9榥y槻k=@fH+9Y;]A#Q]SVVА%Yt8tl'tmޭ79~ p ֳttG~ǖ ëu ]la+Z83W>}渀2lhV@et]Qdԭ!@qAxӗMtd7;nA28`F8BI fH;ilÑ<2Ƣ]h>9{Nm!]ev]/Ʃ]}cO;8)'ɇH'K^W;Z@IDAT/|J[bS'ٹsGދ qJƋ$R⥿?ynfdZ }+ ? K#Km2?Տ?=-0E?t~pAY R?| [O?z'^X9[CPG@eeҨ6k^WRE.LtCG4V6sS Bm}B.2ꙇhoStZ锳>g91)ٲySzi3[H} L(RWp&b1{8?o <R*^iSU ߗ7Tp-Oo*r<߇vO`:hvwA|z~~Wmۺm?zXѧ~H8 XLY3Ί { GY=rh hӲ4e+[ԲQБ̑C2Iؐa,jrcKY|\Py\fG4,>edmH6< Z{ژ Ҙ (ʀ_4$mS/ yG.r n\h2s`-2ƋM(WHW7IJ>2ʩm]c_g ڸ:'pCQW =+T7ÃKnd(^r '#^ځ-r~mvnX.~M|g}NٶyLH'k<ȍG/_>}EY %s@VG S G7e?Տ>o׿=1%vZnX@m_ĺX|almyw;#mڕ%_b}Z gy18|z2ύp@[YkCe+Gyprl?2j9ľert#n8XG˗ hۺazdgyva +@@GV=rx|X97_K;5 ~m̈Z8jyƷаA墘>b`l𕓎<ڎ4t7^+ܸ?:qN͙rӋ-w,X0s֬YsfΜ>7wg̋ oyoDTQ4ܥґ32hOޜ !v(7m q /./\oX8DLߎם`mk?Ɯ]]vpdq;vl۾}6 SO=5=;?顱 *y4HFbӊhg(l0/Z;Q@Fz>8kTinzS=/ezu2} _>zƏA{  J^]yxuFWF2l*([WV=dC^ vo@ǖ@FV2:LmWx!Nt]f?n\h#mЕ"!3'ᑖD:*6 / yy2!etNtbUV۴6~VW#[꿯FO>bοcT;Ǹgp^poo'b׮?sk2\8 5Dԟ {.eӃ&ȖHOEަ5#K?91~x 2Е 4dFnmQc V4u3c>,k]cĮ9Ru::ƃ,1[ֿb CvbSR-ȇ7o%9Pǥ e ]@s=ȉ p@ȣ sdI чO94bj[I׆91?x4g]>1'{c$N.=tI.'j{r.2о,y@_l,XAӿױ=`ȋCW]񏾀Mc%WN_kq+WO<Ќq m` ԃ~pcGo_;S{ߎm\lvi6(6*O!Irɹ/4xdZ:4e,@ړXϸ_:G_cG?8 I߁fGF ȐhZWVNDBC?4YS=}An/ucYˁf=i3&#/ K>tipm܄γtfe(A$*e7 N;\]qG^)htX0?:[I}x$`呁&r_~a=0@{o_}t0ĥl:~F`J3vh <@Yu1֟.8(+>4,9=)'Y@]yڿ#?Dv{;ukƟ]6k kL}ޮ?N#]_,пkxRW?3)l`JiP)cK>F u4Ƀ_Y"lMG<4~pOOykŽ|cK=}do2<>6ӧu#GJ. y@ >F0V`@A!KFK>PGJYVz_SF믎͘CGGд { ay7X͘C4o`>glvxiKAGcA_{$llC8m'rčc GˀPAvA\#OƯ 6c8O@3nIu,ܘ?q%tE[Slڮy)S:z2:$x5a׿u_[ÃfSֶtW`{%  X#wgێHfq+ O v梨A FV.&a7gzO`XՇ3/r|O=ˍpVpQ56+N it?6+edrX//гWcU;a4'b\$a#ljY5)(f3?y\Ĝ՛˱imNÚ9=&^ak$f91m98Đ [_Ow޿ۻp{{N~t0їWzz?Pߟ;'Ϙ[9#FV~1${s8gK8.<^#C>y_~/3R-M&V-3klZU8 QĸY>/$Wтj q|ob](ώǏՑϼʕFya=x Xw6{>s0eSUp-g,]4Al>6:7KYq[=a&gg'՚ȧ+7 +|C'c>kM7⏻54%1_ճL[08M>?'ˋ|ÊϾﱰI&?}4'K3-~gbCowO̼3'Ӌ0/ӝ:g,?'?{3nߖmfX#q-(KbQR/it?7IC,bW8zwSq5'[wRչ\S+I1F8l$qѫqqֽ'wJ9|[8F>q .689LQ\Boty̚K6168/<w?{߯zog@/|4=s}u}s{vgqsն\<:RЋ=~/_Kȉ.6Ω;9ŋQwx0g2_"Ɨ؉A>`WqJ}7or\{rapŔO,[^_><9]}-FߖYAV,tݘϘ9ר~0Z&{K}s3Z_Ioo3Y]tW1&^^_~Ko/T+g2/xÊSLc.6 Ze_Ǵ e|=~$uF;mO;Μ|?WD{xϗl;w7+~q!^đ9~|?yvY>{qrˋq.Z`%xg3lf5%ږzC\/&xRU?qS|-y[{O_s'_% c,|Ed1$<1t.c_aC͏fqd).lͻab]b\X.u~L|.f,v'Ul"_g,UT;̸^/??6"~%Փ^-uǓ,?b[Nog³{_;MM^yz?/~l~cY9_8s<ݷaətJ&wbd$fφ?W;?>]?/?Qbk-wpHN5Z! B'ϸ ?BØ&&jA;VFy5FyEfKvz5o%q$;r?W5ْ?^ֳWN5d?sadpxDμqa1'fFqFqmj`'FWb˩9$Lrv~b:L.4?Pƞr3s9߷s8g|+oOv7l[.n^rIwK׼gRN6֗1êj*/rX)>b1'8zK$3~vZI=1ro䯟l&~qǗOʙğMlXpZ+9)mzK??E4 y;qFWj_Ko^c1y _ GցTѕrY)?T? S}ȧ>gb 8k>[GFF r[Xql|մ+<~Xx¤вLO=2?Hqdڟ L?xV}o߳|<98WYZ}|lt3zꋏe>%08|//k+>~uU^qo[qGXrH8jG'p/z鲛n8r/VR:#~u_{a~_C}vx<g9׾y?an>sp>x/}?;s3ONy}ƭ)by/0\N_q0iLl/i1|Əkib7D,_9l_gg􆳹X8͍0&Κ^sW_~ssu}[T*\PW&7'ZH4Ol"bÚ줛.bˋK„EM 8>a=bߚzֲ[?~Sg$ZH{>)d9{Ӛ/^U[tazjl*n=Ʒ/Nxa[9+?<g9iK7|?_N9G::3d -i'|]Z7_1f+/rïf|a=+Onkcy'>y϶gTƿLv5Z[?ی&~dz%5/sG2[kk!FM$^#6ƿ#Œ]6G MW# Zta'3&}61[C7 }+n+Hv_begܚsGm_kr^~Nڲ57jyG}㧋3fO7je㫮%&<#_}\%r-vdϼg+xyy g9ַ38ߟf~{矞98_:?1| _'|7Ɍ3oa܈_\TyK׼Zg/=_,~&g>3*Oow7cN>W_14'q 7{r- 0\w 38O\sR/%Hj4c..%&[Ӌ ,z{|0V=6>>ysF?_Kjd#q17NfjxeFybʛx_w]#aĵ;g]%8?}9Wsz~Lyuܺ?W,φHޏĺfl8l]8]eNvw7_a,Nrѷʕ^7r\1 zs3oK^5l$|c1S=_~9ߒ@‹sp5kxn|<{}%6xվԋc!<Y:嫡^%r5au5_\?+c|qȋeǥ'\:o|t1'q'kz:7N~04Ob&G{֌ӻM>_y{_ޜ=m8!qf\/ggr=l)#œ'׳ępÓ s5|1L~Ñkd&z>V\2_yFaWo;k'⋧\32_TXHEf3֐nje[7Cb]5qxjF71b㪶n@c G>6|r`e+9tg.ZwYr7gq'?U{V-^m8G#{a/82Y5Տ7̓ɟmo5 r:GxzK?zl?g/oJ{O?ǟ|UanIyc1b8{w!l/G ^l.3\/=^cx?ꁱgo^]g#bӼ?yݏ˰=_8Sٍ?ِ`V<Hj?÷>~R>=?_?)X>O/wq鏿61qɉkb35w9i?/89aOLɿߝ?wory?g=ߟqv?xw_ϟ=K}o=k睵Uf|Ybk3ߘ%?و˫fzr>Ry"嗛{k[_9baz ?W,72ݟ>&?{jYU/ J e!Xq5-%aa5P߃f$8gwլ^eb Yꥫ}MGg'ɡ71e'>ꖋǘ0>g8񯔋#'o￰Cm+B`bJۛV3g|uͱ86J7ԃNgnRx|kiLwg#X&pp>_Ö7y^+~58é>{zZ?xsܱDžQKWSXY>ؓW}{^c=ˋoc֛{8UNgy<+g=g|n{?{}}>ο}WԳ%}2~~Iي'v9'M p[ l̸NdO~x0;\q0`'?=V&cv7/U]!>HW7 }T&[hc8w}ƋIލu@#}W F#S\/=b&xoM5ιS?;%{!WW㹸skyny>yjs{rϮ3Wc-7zr&$_~x s[:)y1Ǎ|laOf4'{;l6V +wڗ.b≿^ _K-A nf?TyDcfXtޛB'+1l>o/?\?\\~nR gp[)_|<'yꣷŰ?W,_~uL2sץ^U5uW8rH7a?oo߿9s=^qN=vG|)?.Wߌ+c t‹ٸ0+gGrg.}iaO~Esy-hoc~{\~|;96g^Lu;$qѝIb1'l>t6!;yDC7#?1~8;\W.щ$Übv⪧1O~_8jŶn 7'86U~`K7goƱY&^2]qG^ke'MnFv,_oy3jz /R߬qZvpv10[>#aK5|[gsu<wgy|Ogsk_99/zGߝ=d 8Gu!Q}4Kf?x0书w[X?q'X.W?y9{:sjߟά3ȳzv~sn tÆ[_,l`0đZ9/m55n+VZ5.aY__C7VR/ \{H`Lzc,89k.5F[x~$@~*ZDRb*>x7 _ )ϕ1c[cd+\Kq0εDž7Ϸԋ&O\F: /F3?^l=KaM~X {9rf~-7wo\%擿gsqBo'~|~쿞 sq?{=}ѽ$?cӳY;~#|qT?ox&^:Ly}Hߜ.gVuɋ_C~O͍l$h)ZrBǖnLV {|gg矉- ۂ,һ,P58p7Aj2p7ˆ+~s~x0ş캜~+x7lņW|3~֔=W/c}Y%l&5ğXĕSǷ?nYVX͉U}8\_2]":-~6xlW\;yu䟶WÉ,??yvo\[%g}|fOCL|G#9;$ߞ߱y$F\~Xkg$C.l^lK>2O'8󛇿W3You 8ڪ߸x_ \~=K?/1{b]oWݓz읿W39w{x&wۧWs:쿷M>>x׿9v0zs&~zFt9|k\WL7<#o`$rv~pV;[bH|13>rg<5K7;qby%ǜoe3O!aVL{,~9q$l "Mi5[ll Rzj<:` ˆ%;zj+Ý7kZ'jCçF#a {Zo7eNs#c'rUmW? s^0Zg9q\Wm'//_a/_ gzg˂@IDAT<#gyGC쿏u߿?=r3F=io}H׼o|a_۳^1|a'"#[=4~8dgNwM~0'5p/F\#]e'(|: `5W2]-ZV=g2/_mŰwb1]^͍/TD^Ŷ1lͻ|Kt~vRSrVy͚|4k_DWw?( 0_oW2{?sg'caOan7,k+'|8s2f>#zHqz8$55oN§0qå⯎?>wnޏ~|?}%?3sgձbr6:l37pZ1nWL~G/})gOad36=g??̙\C&/^'d3vn%LV7R)AԊK`9'-Xc21aSG3oxb?tbt[RI9OHns#;|ɟvd/|77Y{9l{6#'I/yꟹZOθ7$9=~ ߐ0:ќ5o!b9FR/&F|MgH78bz=P|&YR\ [<{]._\k痛1`ƉXl$>9ca.: l|U?5_/!LWt·_-bM~N{_8y}>s{c|?_ggϿw~2:띙lw6'|KtO~~\a-L83ʩ8l+|Fų{f -.‘۵{]qVaOՖOxb&?{-&ͫ(+w/Ə~?g>3o9δgܯogK:6x)FkAwnSyױ^=?Wj˛clrg$؄X><_Kό z3.>~$llIk8]ӷ_lf#%Wbs 15Sxg/<);|gg9=?}ϟU<7~+~byv^^db=O.?/f`#6=~O_7rIt"_0NO/>#?OWK[9kOo#gPMB9-Lcj%Ԉ/lcXL9㫞ע~%a]bw'NK&?8r6Yq3k߼Y{ =,FF_NaZ c/Fxa6U[Kd#a|j /7z^Nlyys۷û@B߿9s93OgwLltyJ~;#e^>{[1VL|?{Szxspߗ>s_\;=l )6;8Ϝy'g9q7>l.O.1z/O/\R}L;[uQ\\g_-_Q<ۜ{U>o}Ʈk (\Z&tI5F\qw|u[\k~ 魫5c_ϊ3!&ibB5/iB3lbveC#xGĤU)1~~k zq/6k$ta_+8?{bꉃ> X\1t_?=?=.}֚?lW+3&qq:|5ރޭ^{_Xy]gф!g=ߟ=sӹo1M1jQnpd_,~0|\1D|gaW \#^~ckxm=/&OF8[uk'1'Xi֑u^ ?׈C-{nSi㛶x6lC {'9WD5M[XR=XlqỎ~fN!k9|[N`Og+.f#v-\>'NpZXWܼlr^ˋgv{.o]~]}<;m϶{:t?{{ !o^+O?7<{Բ?l;OϭZg$m+NǏ#~36OƵϺ͓YGr&Zv=[ķ\5k47N5dgQ!S4EYpcGOj"[13G&sqt t`Q2‰'.8%6V7S9p'?/v/e9qő-IFF~&?okbe <|xȳ2fn?a_οNjy'g]Y~qxO';o߰;9u|,V6N|v65Agcs?{}1ZŒp&$;[9M~Xdg1,:5Wkkn2̚&c3^) MNR/_g ["ZM }64g:k89 4=??uL/|rc2_k$5Ϻ_'݈g%׼Çj^Zku[FWW|[ǎNUw~yum?\~od<;|ct;ן}ľ:::;u7:76G×<~6qZsŊO+s|:Z]%qo<ΉO㧋W\~_6[]o=Ӎ.-ׁ @MυiXM4wkNy=tRnl'8HYq/Xa\w~3y9 W Zn|$éo9/oQˆ?cs' {[S~|qF5-<oy;s;Do; >RCbs2u,7NK{hj1p1bWw$~s5]I㜸[xqqO~s/_|u~žꮎ~_2_<5ͫk]qptʼnmي/VǼqSny6"[vS#m>oNr5()1Z6"/|/\X.R -oTklbã"prMXzkG{fƷz|xtyP~9w~G~[Cyex5{xj/%l!x{{i|H{F<>ީއmgu쿷>|u?}}}l[U3Uky/xbZ>{ǎx|bc.ol?~>c'̸y>h^.<r_Xbg?rWaǞp<gy|gP8ߟyz?s8{2k^۷wk_Ws\c&W|_~sV~_|j>yD=~8vV|~*.';یe|F.9typTo&^y~zfC&e"-3-\C4F$-:fr8sN>z7?u;^q|=^xoN:Ȭ;jhOzS |lŰ<3v\?\9.v̝^RO ?l9/.Z3N~wÀw}ҧsy{ػռwسSyoϐ?giiloi9Izv9?zNlDo| inzV]-\_?--$0=T/]{pf-aR/}͍8\ꥳɫO͍S'^8o>}qf#? "inZqcΦ3rΏSF/Wn~(_=5^o$[_ ]>?? aheD\t2Zf|Þ~q._,'6?ya'?}_Wկ.2ןީ\ԓ<;ns>t?mշU?.g{: VF=.G~5oCw5[ )W\\51& {ΟՊ=W{&@{oMa|&;[xx?{g'wjgWmbZSX_?z1z_OvA#z77_O'uZIJ3>X.2]g8=䡓t9lp{>Xo*;<&ĻAg9<(ޕ}|cva<̷t뽿:1q?3cD|Űӫ yq7.ӅG7]W9pv~-MɎOaT Kύbӗzm'oG]=~ o_ 0w|uE&yy)J*n.W7P>fi[7Xq]f\XWWqɥQnR/~XcᲑ,.jem\Gq?[>a+~o'&6rɋwgql?+K3{~=~{|<{sw>W|߳|oowog;GMp2zf`Z3N'bIWxK0]mh8-Hv8bBOk`5ZLaŸgk rRdK‹)N⧳U=jތI/b(>ilaakη!u/79?:ּqNq9;?k~[zG޿ϭً s<=#}gnOb<7ΟIz.׷Wdɯ)QO{kvlȣl?,=Kyً>:?T^=<~9柱|/UGSydO*.WhZhFF2O_>O/ο\-N|-~p6oj;X '?f8FgEg^֥OW(.ak}.=~:~k-->~ŇO&?|~^_rsѾ}q?{9?ݧο}|#6kz?<#v. j~?+cd>.~K_'>|81=.~c"6nOr[\bMUkzMOIMYkzgN\nd-֜x0nߍ#1ᄻLqgs<3;^1lě].}\q{/5Wx\;f [\ƤOňG8|a:b1'a>3~kQ-Ŋ!_<;ob߇C7|㜿AmGվ8}wbew;C?r>ew%8r'?$^l͗5wR;(7iMxI0#~s_NyU\10áWמ|an||US"{CZp iٚE~zMMP3n byX]E +.vLbvSSX\& W8b&Κ^RGˣOܤߺ&~ŖW3jr)iqqF]cݸnb7O`$~;Opv\/F~~syxY%W{}i;?I?s9{-3EwwR׈'7$i !~Ƭ}5 y61]|ѫ#} b'6vR~'C+fƉC'+l/7<OX3޹.;:眿~yrl:Θx%⦍NMFb{ײ/smJ$^.?Ӛҍ<_ݳdzt?ޝg쿏=|'ޓsy/,D7NlܞcX/U#͍oxZa-<"V=oK6uÃotŷ7ʭtc=[^mxWGWLb5w;y/J,xXR,_78a4.%k|OBv/GƒYL5zٍlm$rL{5V/_~zyy>9F!6p_N/%㊿'q;^u#g>|8$w%,xzuޙӛԞӾh!g9ϳ|~{߳ދO}/ZwݚOi^qЍjgx0?caߟt6Ü5gK7&pUg&X{~x|b]bv子73_|k&? 1/>+ ZZEOsZμٌ5xjn?F'bŇ[W7qW3~l0&?.68?Wpxz>g WkX_ĸʁ{_{ 3;[|H<<]O?G8?nܻ7;ӷ}'y|^;/ni γ<_9 c wɑG^o|˼_|x?=ߘЋg+'~lƝM?'3F\x ǘ=|Ų|t ?oQ_!bKڪ&ϛM/uzF/r]qFO,c|1_;r&y0lo=nyWq'aQí;NO=r?wKu}D -+-ٲˮk"BE$!ܻQ|_AƘx3Vyki={Z?ݺ~lG'~\?r_cO]ѢϘ?? "p"(S/ clx .}1 I SƋc}d,F:Gdڏi+J|ዧq?Nwy&j3ugNߞg5=ɹ3g\9'?g~ۘ >kRF+T3>|`]a w 9_=8Ϙa+>z?9!c|@K{}/!bf0,h[>-c>v%cƇ|f_ϼ!եu~W0ܝo 9w|,m^ \˼Y79/&}0o>[ӏ &2mӗ!OӦM;lĠ}7_ZZ{mg|?~;53ߏ!W(kɌj{ } \\Z}!?1g 1=|f| |u l{ ?עoc/#m3oݥO_>CH|#Wt?\碮h.ږ' 2G'Cl^K{e?tr=>!CϏ bOҧI];\,xÃK|x>r1y;liGi%^C@̑>}i#F|mcX]ZH_ŧ}^x_GsD~oEߺ_ZZ{lw d<{w_I_yb>x/?t@g_gxM}d ď}dp^hI}#w=|l5Dn;HGD?۹Bo=znKh={]^%o☛y?"Yc7L| IzsczGG[t`]1vo ~c O[jy}uou^?ɞ?wﬧ6nm=[j2~o23vb1g?/2t~H||aѢX=Y~߀}-6cZu d{E|}؉7 1v}03U;1Kc %op}Ġ1Ge1-Kҗr&xkGsw|x·~XƿcK,>%5j2h֗ s4\yF1Av,fC'_E^#\{&!zpM4qh}Fo7i?}7 zr>~#|\13λ7w? ?=hP.kf`^3@p z'eE&|>^ '/ K(G_|t]%w]gi!iH\[E?k]o^/L1G_}\zC7_YͿv֟}w~4c#c}|x{B![G |>S~~7WOZc+>-zs=9|:8I|xs_8Ѽ\dϘg0 */zIVtL[^6k.}m-| _|KO#I6nb>{tZ>z~iw[Ƿ\Gw=ZG;'ZkSy `99{#З/ؾ:=|E iGk V9v^3>%m/-I$!|&-|.msxħϘ՞rt]'_ʱӷ}u?"VI_V^{Ekq˹Ug?i?sE9f\g| Յ'1z ||_98b {mS#{GyHgsןwn]?ӧ30zd|1ċ;wH&t>c.6hyLBL|)Ϥuʎo cc.sžÅAn 7'b AϮlB?\ х~맯sG/?* 2ecfܫ??=Q7kwkkףosV<3^3싽/H|e3?c!Zk;2ySy%s[?|l }G#QQv.W-!~ oJ<j3dloCZ{[O;{| f1?~eq庴K|.D:׏_\g|/сs _`0'w#f=q^ u9_K.kgqn@ }hI vÇHƼCȠ.ʑ'Ż?L;t BNrp 99wG׏}^ky-}vGog>hK !cN_ߗ1lm]?0=dj!#okmZ?zlOu38Z3_>y<=u>:o/_Ƚ]?}vC<Z~}r;@c= @#n36#0/ֹPT66^? >r@tAvg|beBgw]B|#sF[xX^si3o& wsS^?3(a 9WXY߸/>_gYw׾/7{ +8c9ܻ-IC_[]~tڈA=|⃓<;HlisO(?TO]+[?28O]߆\PLeP (}ZH:W\/Ox$ $y._59{n^?<~o᫃}.*yşBڋ#/팟:b_+^,m߱WZE\?ڴo5wԞ֟jK9饗"*IDAT[k{6Ѿz ַmW'|mW6)e;' I{ ]0?݌/6Xe7+[o1D͗O )#yl^0d'?!1g||C|t̍I}dWWlQ.lc1ao׏_U__I؋um뿇x=|(7=Ƹzyhin{|?z}vęDn=}q(sMÃw|Qc.q?;O]o?K|}|]#c?k7hC-ȅ}Lb%H^qe/_a҆%G'1'x;-;_l߹gCc !X?3cK5j~>ߒw'K}_kKG > ͯXxI/1D‰=|VcץL|Z(MbwL\}>$:jo!OmU`!r~2?KG@#)GWƿwoiޭzr3bQ?7gt? #9ww֏ G]y7 1SYL3~Wo\?:\J̜>г?g~ʿM ~w9'0suJ9}>$&>2xsS=L`-x?ZPȍ.ъϘ>X/O{f5krח{뿇GӾkFOA`}h5-rHybk_}xϬ_|8#|D9k}IN{zVΕњ}H8c^M2(dϏ&.|x0FiY7v} %&Ȁe/ -//dB^S=^O}Ǎ!Ɂcyo.%>_بzy ѦW%{wξy~g+^l}D^o}}~uw9k8PSzOϋ۽3?>9_z3>|x'~{7gg|l yg~C/qG>-_rSm[m'8Mh^"I_ 4D3G/ N'fӆvWЌ~:G-)Ʈ>r61.>C՝[t'w\ 8q/sC{VٷVd3U[=so_d-5/[|Y|[gz<cu{5;ƭwξjoOo_d}mrA?b1I~#>$/ ª2qL|Y F[a;ry|3>#q kC;cGCt/#ħ6gg?'7}0ן=.oeֿ֟kM{?W|t_Ϲ#K{Cѽ[ >cnm|woΓg%N?Y-~AkCx=D /k2G̶$B=[|-:P˻O<%5؊%>c翇O<󋽺߹k3@WWE Z=|0 |Q|¿{޻cuo>uj}OϟtkM{#?Ԫه64A!k_#>-:r+WF|9]H|xH|u:7ZH/&> u^Mx&}_%/:\PM&{_ <>;??gMӽ&wѹ~]8όooQ||w}7Ϳlh[CϿ?9/{:q߾w mN[܈wal?tW|3е>}}Gu0Ƿ?F3W|evGG K|,rca?Hƙ-rawki.pzlţz}kc~gQ8gйW99޺ /O3} }Z>|W7/K?lyq,ȗ,_rsSF_??rc|l/-63ķ>𕧾?su{~Vw? !H,/Ts;yo?g?=ޱ\^#>Qx>؝g|p][HOq˜xKً҇bwm{G ȏ$}!5!եBRX=7<}7B(WG_7F:WחkUgׯ6%>6ꉏ\G x5>sįsׂ|޷άo3YrS7oϖKOϟf]gGх/3ru e?:.^.ظ#|t3f0fڌ%( 2QD}aY| >C96lBLJioa ϤFfŵr,-||}6)43VO.378#*^ϟ9Z[[[ke]'3|߱2l֏&z@.-==|}!K|Bou~vrQO>lbCcEX/T䋦f}$ws }~l~t/$1:wSh?Nz>>]?>%:Z⟉_7__ϿOߞ?{Bmo맏>to[=|Gg/FΟ066/~ۯĥw==GGoϟ?=~oULm9wqݫ q?Z?vPWz0W{;? Lr^(k5ah}$a?~?]|6xa{o?=z/~y]|掾x{?^2w[̃?.2&?C_|!ɗh86Y;<^p#kj??F,pWGJ_nV3><ם6rNBG74[#u Cc!Ήr2ln,j~PF^/~&6$-v#C}}62n>Zڠ9F&"ɿ>GE3>v /?M1t_ Fsw{fP/mo{o]iYk1>zR?=g4}F u=v̘s{g^?s`r-֥#|ϵ~l>RD Vl^Xo'/\> SCˇ iM *%)IN^=c">w:v~1#|`'s@F{#LOU/~ouuuwܻ$__q#|/*tC~k!?Ʈ>X|ʴI|}o"Fx2Bg IF_||G ] b_k W:9_ox6Ϳ֟_N+ߞ?-kD~rqst?ƀ5δ >~~)W_\[hyǯ#WHʗKX&Ib17QI2.&5iI_;G=L/~>ڋ-|_?<7\B's.Ϳu]y"ȕ!#E¿VGYݟ3G|03Xrs%yOFVK}~]KYXb2(1IyF-II/a|>~=|e_<kwζ1k~d3>>_?͜y+|#7ͿmOtmNzo?8[~^-{Y~3xw'~~ۀɘ$:>н>&ţA9gce=ؿH$IKg&q!i}L9j=:s#S|Ŵ'񱑟z} Gg ~8yҗ[S#?:Ŧ-5丹B_z6[3y?o=mEkYW=.d>Pe̓ٲ;?|s1_?ߘG~?A|~~7 ?EI&clHHɘdR7Ǖ1s.=CsS?6Y?'v?~>z?shqaWvzEVÏd M~w￴×Ct֤i:С'ssD2`X^ 3!Mnx5H NZV3~.b,]6oZ1ԻW3o$Ϳ֟_&= z={/?Wk5??Y>b>?z!xSss>Hq~}#Hcb\oC$<cL_.}&&-y!,>6Ç6=|dB.u>O|}d[?sϜ Oϟ?f~VS|~<|tGV<|Y2 6‰AE-lņV&5 ǣ/AWʑK-|MӇ'>ct=| _wL9#w?cUƿ@_Ozj«=d;_^]X5?OxďyˣUbc?MS/IcӒ7yMduC><ԵÇE%q諟OC ||xqu;s{>>!cA /Nً_|~=\Wƹ{'{uHb_[нvoG1e\/@wh<:M>yi}7Y|m0'N| ?f?A{Z?H[ |V5ó֟v._Ͽ?=z!~Ky}(lKltKv5wOOINX1bcc-21ċn#g3_~n(qů >w_/>oo_u9^?A9vn‡r=`_s|(#`BђP(2>H DV/>:݇)BhFw |%x;e]/V7ض7X;E{r ~|>k[s|x>t1ϾD4E 9:"0'(b8hť/׏/3Ci%G;{V7[s0aPË$ֳcm2a'9o$w#h %_Ҷw?~eހ=_ZxFw!yZg sFwG?7Ϳ?GO/|8{?=~A}|tAŤOp.|˃C!V_cs/N__ 녵vZM+ݬ'5V=z'r?~qK.dXk 2&*Sϟ5E2^9}Z 9$n_c^[zO&"D/n‡hs&gǏG+J쫛#eaK_4s7Wm>_3h+8vb-uW䙛}J={#3+L Y_|]E6b2{ G!lXȵȼ0*|EaYw[?s\?k( ~o%vt>P/z;?z$nK׼!WO`}?Qzc,o)&1fE 2lk3V>X>g^2KQsu?k}o{!cG[;G_nޕ76~a}}sHW⯞D 7}7%pd?ٰ\&M\;W|evK|c(·rcc(·r,eͿ_ q֟G?GuUO/ q?}PW;/\FMdg8vS?_cCH|n@y}g|WYą53wjſski=86w=׺A|G_~C^xnxpYk\7?ek#/a `|4Pl@ZHv>zZ}gҏO?5n|a;o*~[?rPqooogW{irF`'A"籘(1cf__|cpLF?̫ͯ\^B1m:f\3>ƿg~Daˍ|WKRPcAc7jQ./Co~oͿxWryod6"`ѧxzX"ǘO૫lXC>^hOVj!"sgwMo .?Z8\1OqeK1>֜bl^Whd?/[x"l_e?QXrceK16Ϳ5ĂS?{YjO\K,|՟~gLb\+bC2N{Yqa⾒N_O_'=zn2ZGсzƪ>2KlqmGҟj/bc|5>gxeFxXY[dga}mʷ[եMo$~I߶̏g1dg~"?Z5b"0_!pXcf>ceڶOGO돵|Hj]~8ߌ_@##f3sBSh?؋/ '_dž@Z9VjDoϟ۽e#P%Vk9bMg7,o޶h~I81ꭒguRCױOo bh-.smU~yA;zуSױ>L9WBLq1Nc}hr-ƅ8;cJc,士Rbo 6kg\IͿ5Ɔ6H[wm%"[ 5+k&䙛=x}o#4OFy[䳏̖#/ܱKʊ }co9FLpewϹC̑53xc2?}s~ZblldLZZ9YȍeA^jFDŁdQJ4ԇ.ԧM=d(g ~r)-J<1Zu?={33iCCPICC ProfileHWXS[R!zDB 7! Jb/ ]T(X#v *ʺX+oR@}{}sϙsgqDj/.ƅ0SRӘ' 0@(vLLe{yw \h |3|W%BrbRDa:R 8K8Cw+l8@VY/AYς^ &ľCl m$Y?dM3cPʹ( 9PT M?K~l58anNcu{QkCA$PCReJ{Ԉ_9z; xA,΋P`.pE!^ ,WlNSB2?˓*}ݗ&U\>F/NH e() b:N*Qٜ,N%qBqHR+ʔǩK mqTxoavB2?X+$t)ssǞ ʼn*€8X*ɋQ¼9okAQj,TRϔ$(ċsxa1x p@ ``x < Y@UdE^A1"!("dWG-RO  ăޒcȈ+ƛw #$`nxꂳpρy|' v;;5am9\]k["\#'+dsse_p 8$SB&~L4|}U|7?9;v =pٲC 2i|KhfLq AH`pK$0 % ,@6` ap%p ܁   ! 1Ea!H!H:2 !ˑ dRA"'sHr yt#O(:1jGY( Gбh:-F硋5h5m@Okh'af#8X4ebRlVcX >D3qGCDOg |;ހWx/@#^.!ED(!NExG$6DS9ĩEuzqbD"H>hTH*!%$#]&u>Ȧdr09,&!w/?S4)V/J4E@BYBBi\tQ>S6Tj5:ZG=EK}f&RFmYjյ9ceշWFYiiBbZ $>AwsLz%~RaQQOF&EZӜYyPfCkVV"Z紞i7k~ Ϙ8!purttvj&N֭=۩YqջiᐅC\^PT^'AA2F{a$ { Z:tFQTFmF}&!'{LLMrLV56eLW3}ey5VfYlYgsD9,,L-,k-o[QXVVXNohF߆kSlSksזfg;Ѷюek=jfm_iupw9sF9LF(iNxd$ASCIIScreenshotL' pHYs%%IR$iTXtXML:com.adobe.xmp 1572 1124 Screenshot Ԡ1iDOT(E1<@IDATxmYy߷} $-P! -Y" qbG<ɓɓȲc )kp$ 2I4 |9}9uUo;}kS. $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 $@2 2GV,]b(o8~æ\^h<}%>`U'>g1##oX7xݱ'o?`)!#yW?w{椝 $/1+CD-Ճ06Hm=ɿ帳t)w2de*1]_帮gWcw'槎#s/ϟ!3yad Hx|E:Omm% u}ɫ+߶6VP<3rpde8&|^QP yg֟! K֟?%:_Wmmԭ:ԡ?CgȅcgtT $2x0zUC'}i#R^N Y4i.e pY47ϟ<\/X=X3y<#yd0 ӢyaXI(HyG*\(YY,iCdЖ|H9^ }RR~hSz^ÃM9%ҜGkr7%d\fMp\Q:#my5V(/2o7J6)ḢtGkQ^ domr7SqEX3Xȼ9(!2o:ȇұfiS2Fy!'ysQB+G2 W\<]\T.]DW]JcU{RWLxof6z?䨎Ǔ3W$/拺GV.%̿!Ou8̠ƈ㥎!N-GcWWxU˫zȡ mrWb\^C!Sn2daokͥryUP?ͱ^oͫFGJLJひ0?ί:j.˫z22?05i;꼪T.|P|Sm\\^C%3d b9aQW:E6uɅ[;7Kx!^(OJcǏ㬖mc7\?0 ycq2?γZR̿?D阢N\qh;,Nj|c{*L@t̐qՒml2| ☢P2 bXLrhW~]}¯W]4?cVl~:7*z_֟>{3<[|RB5&cjs8NƇ! <q%P2 \f\0tCE~],q/.8UJQ!%QsdMק?\ 3h3 kTdO[;yG޿'cBeaUh/s_d/:sH;`rڐ%/.Bᄦ?mhU7=i-mR[仍_qg9WoqgAj ~_&%o;'Pe c53O<9QYC ˺/b|1Dw:fwYxhCa_î{us7%t2v= Ku&^}Y̲U'uNVdLX^T=%>Xm>:P÷\%[1SϿ?#&/oZKde\pAyLLs%y~97Z?kE? a)"Lxω&<'mJ.He\S>~+>':c| Ƹ~6:͊__M-K 7䂜deqt]e\fra>ya,1\KXC|*S>:yq^1.\o~ONL>6z--#Ƅv?Lr͂#9;')9њ=_'Ni#7a[ғmJ/̿? Ygϛi3}3d<~G?qx?xNp=kWh2 .?npRl2> utAݶuW)~*/]Gn{O۾`O*,_<;2XOyW?aJaa|g[Ag[Cu7 ϕeuh duс ED-ե(I(Utu 6$X8?} Y~7[]_v%51ZFϿɲ=zk^cݟ?s|.!K_x%OccdgF4`|AYuL9A=XS\˭O>q$ h÷NS|tb_/O lGn̿<(__֟y ς<YսgK t߭_m?s=kWgQ_iɞzhFL QX-I%C-62m$~ Ao[4KCm,|Џ>ħ>dc;dڎս/?n?//Q]oXj>{V'O֟?ȕ^V繵g(>հ!m[90sćʉ|&9f2kP@#gPlRbσ|)aVoNtGlVğJ|sD,:oܖeoA9-33}wfdy],{sԺˉ|oŬE>3SDzCcVL 0$'%E z҆^ i[yijb; u]o؋cxUk$>^0f'> dg޿w??yHW2vתZ=߽"YEh㋽U7wH?O_w:32Ϙӵ7Ͽ?y?c}?^= mgAߍrK|'`oxԷ~h&ADv9AulkWl?l/¯EyW𑫳,>z<|⯾g7?tp1GV[F@?ydYW8L3~E^kgzq.3G27Mk:d? A`QrAAF:ഩ<=ln&T'2{xIhEL{1>ᡣ}$>zƯ=|ʵ_>ek|ԃ?Kܟݸ,O|gxW[Woy=x}z ٟ͋__c=-g5CEϺlšo_'Y N_/pRϴ'>[oC&.#on'u1\W?\gd(k=}7W4$@ nAb҃WNէ.]'O|oIm_(!t Kwӷ [l?UY E?x8rYUf}J*̿~|uZw>?ywN?+7~/u_%<}x +foͅD]rd>dRJvl_`1]. ͢wvoߜe7¸rL/3խe_cuim!uv___,?ɝsv\$e?#׎r;r{/V_6zI Wn: B  dwlo-^ǿk>^:q`)z[kk;Ҿ/*OOÌ:?v矶!o7o\='ϿiXWYc!uk/ei~dٯ/r.5;yדv0;sv*fW|l^ჹtng';(o/x}}3C$㹓kt%*||cj >X\"|aN׿zl[bGt֒ t〃>bz)lujMl+ovgmϠCo|'SS3ͬ=eN|e_Os+|v?/o_֟~fޅvϼgAY|?/v2'u~i(~-"^pϻ~ U?"_6a@0k;;p|2%$8's߃%J\ y~f>"|q*nێ_|l]EoO~Y&> ~ycN,]7o߬=/ϟ<c|~x^n?qlwO?븴{.9c]X+BP|H49kN1qc!nZɵ)GVI?+8᫧-__vT}唋z+WV񭣿 >_|lWG̚ |3F?%xsSBYf׾bO'|˘?=mruޛbubRr)!Tj G=*~=>z<||UQB<ο6_fe]}Y&1>+EfM-f' X}/_g?EAK#>?ڨ[[4y5Gצm̝z!!WGY/8?HҺ8x 4d\n|>sȣMzSቃV`Aj?}؏i~4'7G*~t7n4Uۏ<ug s_uʇmgg kڐYdku%U|O;,'e-e~l;E˾-;VyErׇ%ODn7Xn7JzcwP`AY_[.ڷ&}GE$Ⴣl񉿭ѓ*?Gk\8^*~?ˎ__֟yS?Y+ϟ~}Ws n?`J;wwS :D(eC^yۼ'v\7d_׾:G>4́uY=Wr.2 qf{u{:ys$A@逡1YOp`GR|1Vye_k/>bߒ9uJ%>RϬO4oO/Ɔf=]&͊џ R)Yv :qC nxX C][ Wׁf-r'G/nAbMŧxW|5; Й/q-k,;Ƨ=+~'].|؉ߪu|3 #߰V&?Y7ZXϽzvoq9 ǼAsA>RBF)e]E<|Z|RG:PBѦ.e7F@M#nM4F,mՇȭb3ʰ &DaуoO[VƯOݾQǯ"f{.Q」m*g-:gX3Tl#^ǃl뷱?+~C|Y7^:uXl;*7E??'n=f>/v޿W5?-nS*~qEGE[ŏ/T۞Ͼ kgiK̿mJT-A#EA@M\&Rr!ӏIǗUf?_ԡO)!*>~Пw.KpS##OUn7/~73?AInfZ'ꕼ*r2so:=__g1YħV=JH݊{/>Yo7Obſ,߳q7]ȉOr_V)9d5)O!_'V9迴,> _SK >ԹbW|J.n['1hP_UZ_$GQmݘ{X|Ap 7ryS\Kd,:ůyW}xZ"}můE#EV`k)>>1>{?/'?3/_K)o߬Yw<XWuϻ|-k?\˼7A?1 y/ǿ}}GDZ!1'2}PR66yiWد?HA!:@8orHT 8n @:OV=mzLJH4`S?vk~7^|⋉c'#_n;șx7)/A֟?˼沆X<V!}3׎6|cz濶QӼvN$>u0W|OY#÷K97gc|l!o,m|RjoXR6}ܷDG|!|c\,Q}NU[1WmŪ_.uu(kK'9+:eg_?L9׌ǖuuWѮDӹȚ'/֬߹z'i7ϲ9м?}7F߿hK5~bec9L|tk?Ϳx%x`[կx)SͿXJynV.ԵxN;qc /|N&QwC]]J/O?Qs>ߚ=>Y`V|?> 8~uG^u!7~~} \/-Q\*kį!1D^I,ßl+>ĄercmU?9!Ϫ߿=_WeZltZYM~ ļ?mduO5$ft׮ç=G^o͞o]|f?XNY߾胶!o־$ڗkrOo7 O~rJmcWo*6y=/q(w` pWς1_6NB1xX\ʣ.> į#'N|/j||WOdi>Rćg[olA_؛wK|͛bk_c|xH~%o7ݾz(aMFCa &Q*nTG,|x\KV3 re,|?gcKk g>2c?%>G^㟕tODh8GVs~c/_/)+>2ϕ?O327ϟ<=_]ݣ7%dݲ}xWQD{~/DݜCWԕSe㟕i_ x f_uuđ4U&I|HTuV|nb×md[bWZsoq[b6կ ")׸o>ki~SǮGlK&lk>6;ۉOIW?c^_6O3[9dWB.!YmsOy88Y93x<|L裮+mySK[>5߭= O !g5*om-G ?v?R#?|Uo4N.7AB_PC K O_ƉmM$4V/WO_ŇuW|}T|_]!ƿ >>N[eG߸8Mߠ?S/>V}kgyK7Ͽ?f<f9?Yf]X!*YYn3mxFg|bq,|O|uEߨCc|cp?65b[e0? Xdóԍdc?dGK JHuGto{㒔V:|sA;"|tЗ~kn=u j_g,k>mdEy-?ǯ7ڒ2ڵNWR?yģcqgwCn2 pe!o}z?yܱ'}/vfk|z—+?{B$}Sҿq]LW[!pG_^_.lժ}[HZW]]^UW_co)پ̾PD ƙ8e&Ԥ'_w9_x~S]ZG]||VzjV|]D <+2S|k͞j֍ h߶zAWy5f2>vݬ{ yE+w4lp3d-eLښ߰z_g/yL 3!ϟ{0v2ZP :Ϯ9nhX,]Oq[Eĩ^=A>U>mdoǏgсS}m7F69=1i$ ƪC±C7MUOMmb6 :Jx<$xڴ%S7zXAL͍W1>mB|kL<|n^g`C_|qj6 |¯W> ~79=:32~.ZKkly ーg}`<1 qNݶc}Gg`߾,of] c|pߪr&|*?zbSC)>v ].H= [uj <{i к?~Gjz7ДԵ 7βFS\ħZ'nLƀuDRذloLуġm m@Kc|cz웶/O}]OF+v%u,d u*:!m;f'ϟ:r%}N?urI+>S^= Q,uŵ}~O>YGG:qЯ/Sە/}[5f0.d?G1FcM_L%>yơ[)!E i_d hnlG: ay-\|Q/2gU{;0noMr՗k/go's=yKOoO֟?G;陼E޲fe-]}Uya]Cv׺yEOaM5~O9c|7~p(W)G:r|Bȩڣ )FYi/DF)>%TӶ/!W{a]{i%:@$ ! lJ _mil=P!}1gb⣏܁ >eޘWv CYh+&m!'(¯ z5ƈٗY7gVLJe2}k{ cʎ?___O<q޿|Gq;ᅲ7}<5{+oƽC`X?l%l)>|}\'DA{O{?zPկ~Wۊ/q-YŮ#OP͗ ޼O}?.y8,W3$O<f{eÓnܷdfyܳt][e?<|gYT|raSGfߑAc_aP><GBfJF|?m:\Xu5oj[WW{1>*>!lk#[5~̿?Z<0ꜫ?,gnmgw7F<R>*>v\icJV?O@ֱ~{'i ؔE5MuZ|]%Q:R 2l \&nؑ)JL_i-]=~Oůz]6zulO~)omu{^ծucnNK,/<7ȸk^ZOg_2dޮZ>ծv_/S(]Zu0^q^]>Dvk?~/>V|6ƨ26~!uAܲW&x/1$X$TvL$с dk ~}jcú2mW[Hݡ5xd}GЮq;^| r˭tǮS\{է':yرcG?qѣ/$HB 77Yp0(l>Iq澕 *- =Mn(__֟y ψ>/\vxx^ml)`Ll7tg~m$XOӦ9ԏ֝aݍ~ps>}ѳ>3zػ_->譧^~뷼N;B[(. Ō?=70Y[BFE3lS|-jkK,M^Reáx\6wi/8ЄjNh8m15=Ov ?ߡmU׵QXSc3[-|^KXk?豣]GdgK_ѧ/=ZSv}]̰Ø} sN!wY[tq,Y}6.$KS7>=2ġn6{N_)"A&&ę"3!&{lv>w`PGρ /E|DxСW;U.<|%~^ٮ>~;;q{{g;=vV?{lwܹP2 $@2  Mh $ 2IE-`I ^<$:>Bo?~;~xwꫯ^p ^OvW]uUwg?Oj͖_iWy5jyrtOG_VAІ6c|o>|cS¯qڗ&{y ?_Es @)I¬A*Iɴ^-> `Iuo2|JȾQ׏Y }n\G|Ţ%Tg{goyvUW?Ƨz{AݓO>=f8x %@2 $ze`aL{m׈Bf $rp2^DWɓ'<{@IDATunx:=3/ޏA~]lgu*徑 \oAOQm$~-#vأS>I/_%:(K`0I0SB$ˤc&`&ml%Ag@R׿ȰQ.vIL7G|Q?Uvo{MO<{Ǻ'xk6P2 $@2 Q+"ިkʡd H.':NO1^6U挓5ӧM7]{͵?}i].m1{e$%ek={Yu)ݧjWm{[u'#jc(oj3~ó $ !y&@ρ@˦ғIz ߛll?&K|}h1|ѡ/g9"o^o;yݣ 桇:R %@2 $e`aL}pA tlLd` MnX 3ECoy$WZ _v0җ}o7>ӾNZ~-\I#uo2ѧd?{VARG:T|lYڣ?딒|.l7Vmo@z^8pd(%& {m$K%|cm.ttlM^GX#_>ħKrLa]O~_W{g|LKM(Hd X L[?~"lN>#"Q%{aS1KPNeC) O_>-/Y/~}gv^/g͠GWU/~ᇺڷ+_ۊ[{Fqև/OV K}yZҁ"L An˜a#WIOۛߛukkG ~YmHΥL?oՉ\}쑣_j >'U1hcG!|U~-o7|_v>Pb^RJd H7a'cJ7AozXId@2pe20Z8Ms 3N2|̋_e}ccg%׮^ѽ$Fѕ^92)% eSxG9)SB/D ?2[LWg]}t,|7t};<ȣvg~G?pgn;c|;c4J $@2 $뗁aL{ ^(۫DkpC`}d@d࡯>bC'c8qۓO̿)믿;}_G~=${DJw?ViF U>.rda['e4~w}VO|tŧ^㇯Vįvc|NvbׁX\&ߺ76u7hSBdqBG=JeMסfW]a'P:ؐCHԉR^mql7D7w?~?7ΞSgd Hd X Ld8|? &1ow_>=ģ뜒Ė $ W{ ^yovw099~*J9Y|N8ѾewwG?O~?i{I>!^ѽ6<k"W}&:PZ׷o}Y~,CkG)7|$5Fm.5ChLIdFA˃o~i[ƣy̸ ቉ G̪'o>_|uK[j[/|c=?d Hd`30p^ ڧa 'cՁoG9Yfn_2w}tG]2~B/c/}7xCw~O<>%_}]se}'$&6>(-ٷl\C4 jP(I2&O=}pc =P CV |(v׊ ?ZoZ'1iP/^o;oy'Ѿ9azHd H5O\访;71kxR2pP2̫}w#=rWAL>%[ȟþ/m}yG? G!OUuG"hRciC)U;"BooYū o1Ƨ]b v8_{v_Y2Hd H.>O)Oȴ_Y1_ᯤd rUw4L1O/nlyⱇ?_i?.>Þ%%[SE}.mdCu k_>K<16_?£׾ϲ'6#>Q?lvnRM AmK'}>K}dN &DCJ!KNuI|S}W7|$qLɢZd;2=ED? QƩܘO o|ow<~]{P2 $@2 w/|/sO}w/w]2  ÿOØM2&+Q^[k9og|ԺbE_f 'E%D%o6ut{1r#>v/oSRZ`6(Ƞ)IA$JA& }0Ķz3j}~_17Xeh#2jO!?pwo|ᇻ{`g2 $@2 }bRȬHˀ2K̦OɰUj2r7n}gڬ~]-!K;z>{Txj $݇ڦ=H{Q_{LV ?\)S}{g5Iʱ7YhOm|}-L'>}=ŇJ%[=o~ŷ uGɟl9 %@2 $k]Ĵ_Jāv 2k?`2p`20='@XL;akJnLx{믿{7v_g?o]G-?Lږ/){NxP0U{ꗺ_c\b3oI qOY6I! uxQn? 8|%xoDɡ  6j[u.; 0lL/]\=u5M !ʟW~׿ѝ9D/ȏd Hd X @foK19Y]nyiw_VRKge~:5V'| įDdA ޯ[U %@2 $g`ӁLLזoR>!c %-dR)idTL>!-}_Wt?>`3|׆{M)dI=f%2mί,OVz+V35'dk\(yuw/}+sg + $@2 $랁2u/8x2;d_Xʁ'׼UO>y߯ފo Qu/F&rd!ثDq[qO[xb꟒=/o-{^{𢬱6/i1LJAOŷNr""|s 8&` $$ 6RbxMG >%< K!}H|mGNÏVx'ܝ;8 %@2 $g 2~_2~ȁǻo}ͷtg>د</vP2 $@2 {r w8%뗁Ȭ;vo=Kӏ7omWS$,YgmS'W>u Uz=q+:>]oZ׭B.ڢRdN7Mp@ՅȨ+kվutԷ-z\ 0 ]Y"^çu[ >>(@1~GO=v>;0g2 $@2 gr 5Q%뜁Ȭ;}'>MgsÅ|d%uhAy:T}}1r >TF)m\ձDw"DtHmRח~H'^h7A;;\8ͧWWNb=tĿ?v]w}P2 $@2  @䄘 Yr zm@ 7Aޑľb? >tGNݲU'VD|y%CŇ_?<9SȴOO@# $@2  @P Ur y{;+';~?[]_"c$|WÃCu[})r+>ugF_CWPVjO*չUL  u0)IPؐ|OZu\C;ʊOPx674k[:>%U;lhW|QRd;!̯,@2 $g`<;t']?r ]]8=wLw=ekEߘD $s3ّ+KkJSRgI酌f!ϵ Wݫ"tЗߪ|ʨϥOlį^l ڡ/%}C&YdBՏաԖ~o#A.)|Ԏ@y`L-M*H~l,j2&‡ǎK |.GEW||"&_'>}):_YjI%@2 $$@;N}Wwu}'}jF{cݙ_!K 3Hjr Sq+K|Bޕ=i%LѮ< Ξ-%j/rR=|l۪}4׏|+j;\_kpOȘ@2 $g`rq#ڽ蚿]u/=PggܿWF>/I J+:ŷ_JX#~m"7_* 92{^P]B' <:k0!(@da%>>Z rl)2]Ʌ>p'>уQ ^O8U~mG.rJ.?}?kx !% $@2 & {| sw7O}Ue /tG.߬D $}r ځpm^l_Sϴw}Rg_ֽ$2.xo$g - e=4?c~+>u}~ݧ#uB/x_8AI|P*o^B>eZyO: іGA*3 YGRG[gM6AЧUG>mvŧnĦ.m O O_^sUw7lgu_{lwדuw>L'㟯xWnXv7_w W?u_:k^y;5^r;ǰ:7wOs;ݓ_`xd H58~V^.\hew?ŕtȑɓ鮻e?-ynm0F'۝?}W8I}Ȭ 2eOȼ]%ٛVPCrȭsp6m^ذaT MC{->m7_ਯ>[}G2~ t.&ˤR7@tNzԹ K$]HJ@} aFN۱llT{]bַ<@q\_|0{++K/~^_ӝ>ֺʟ{" k9~ Owx7x{>q}xymg=9r{#lo?q+W^COאn+G?LǟhGd HÝAϴvɫ]Z׻.ߝ;k&ddȼ=;_ϻ{oݡȼ](dS~>m jx֫u.d8nR6uIt̺vmP:mzTUfʩ6rN@&B$@h<6v _]J1ķM||cNO>$.uyTV G+ǯ}EQR+W}{n{)~R 9LKC{j)_L_Zxۮ[oh1 F*{=twrt__4 sot8Ы!}t,}mI}stG2A}goh Źt'Zm/%Wwg_x.ᬶ $;Mo*eTfdv4?ww{z? $뙁v {F|3?}wSʯ,ȰE}$ r%豟tӁD6Cvx G_JH|Aڀ U3o5Іzc6ltN E`$:ʬcI?ʊ ҿ}ͫr=|·X=wt$YOG`=>Ȝ?{_GnyAwh낑ЛVcpx9C85ϵƣ앖?ׯ~NMpF['|LvAϝl;Nu/[tGqPTh綏;ܔ.>}q9Uw龋'.bZEjëióu A~nϜp{}vPd X @f~[ֶ2+YWuJ=TMc^ }/S[7vO`;노7F:}o][I=/aS||*v[%\2@f[;\]JozCB(z6&&DC?xJoʊOӪ=Ƈ(Gŧ }&-xM >Gd 4ШuE^Ul -{~|Rf#w8s+N2_}ǻ7|+g;}·/c/ؽg7\]/:}g,?~]J@AAQ;~vwv7v`acMJÒowe򎾹瞾 3s{{SOJ4ij֭[flhzZXmD`Y-W}dڵ>(Ӕt^XL3=~  _U絅O_^|; VͰ)6eH3P nʶt}{dGU-o S9e~ ]/>]5C].Z݂水4䙧J梏-f\]vѥ L#9̊hёʿx$#𤣘%҉@l*[Wb4ѕX#Obpn䤯DOP"JBС_žE+yш4pYq+dZg~f̉:Xעf$h25/'C!ܤ0?Ҡ#&O+3e83洝[*$a87RfP*\ggpf"}X6PXq)/X 1deOFfH< 28"ӊi pZklzt/&nםR"aKN:'oR_5ivanVì?5 s+̘>ݦNТEskM~UU?*|c&شiT%JV2*\q2;/4J.׮UнO]5j߾EL(tMx_%͜qMӒ[e" Y6!o1gYJ]Zsx/2W*nV[e蟓A۷`UGgwm. 3SϷ_Gnsf/v%UܿV?BGQQ rl;ŗXܝyѶ%2URPZff#d8v2`˒掚Sby3,/4tiB%}>!?с MP2C\z %6+"IZdQV֨ g.^[<BƴvQUqR FfL+Q-veяoZӒ^&{dPru7۸n+pqGY_=0QEne>h{8/t*yf-m~/p|῎1Gf)M Çj~hC +KnWUݕˮkUv-?o_n,lo,3f]y%_t}{+R)u =j~B2sfN/6qj tz,_oС}1AK.\I}ͷ*hv[gM~5*WZ {ꪣKU2} 1yAABuֲY0?Y onl;%xw=?7b;.?|{%x\A3JA5 xuui]{㕗e}]qͭ"gue'wo<1K@!c^>Gbf^*Ocݺ>`g;Av<$5{"fi_\V _W_mN*CGe$7 Mb2-mj-K2ܺ&>rb>fBP 9yGԗd%h):}b>@G.zbV!BP9hžⒼ˯t\$5ܠjdĕ4&XZ~\XРԒ0-'R|JlJ?Ƀ|.  {؂}" _E7_P 1@>w@ m}u$TmLqÙ*jw3踼#Gߏ#{#TH_~|Q& ,hvIM4tXGbhǚx|*@ FX!HL[ǥM؟֢KJ=[ۖ%=^u6Å+O%&өfafF>xwKv_~jw},J}vɖ[.fO?|Yal]vtdqSD>Ԟy.YZ=l=n t;Hq^m x~eH^~LamlUVbx^fl.'t(Jy~|љk'_> ju~ji w>μ{J ky+_}=WT0үMBKeEw1qYu9ԗϹG}sHcR |8-?&Óit!@Iɾ>ҍc}deG-s[#ȁ#ёtidG-!, =&M4*T Np,%E$H|  Y\ؔlus>7Avؿ!` }p#OↇX2'v.ǂ|ȿ L-KA/"bDIGG 2L7CmeH>b={9v -u!\zu ˌ(ACQT)Y^M6ni]ioQ8L31zLG5PoM^\R@YQ+^?MlNXQm^[cNu|w=Z5=1mxv'_#*0*0?#lyg{>蓬=Ch>=LV7YշYr?ho^k޼[U5e |mߝȔ)OXu[$И{%YB$U-,P;}eA2UA_{!/K}G 0.(Tym_XK.`[wod` [ur=?[ a+>A d`2+5k .25d1B(CNY!x=;/yi[V[юF\ekb !=ej~񣒩G#xQ!)}4Sydd!YOr _q",tlї @_>4َɿ{?W$ @W2'i4 8Iң X_ɒM@O4by"#'}@c|~n:j ,(zQ'?'3}ѓ휟F%^oK[׶Q^2o ~A#9[K6>\ fqu=_rE2@' G\B';S헻fZh sVYb5Zb z-u, TpҕmܨiPy7iW𷑾*5Ub~%&'wqP n8"k' ÏK.Y4GvP( @̀o$mOmS'vf*Ȱ箻҅+|x"2Plmlu{nv{rʵT!?mYIq?8$kڔ d(XQSǎv#~[qAh,xªy fse|;v̸ʫB1n *_0GS_V|qD|f*EC]it xL2To{-HSdBc]~64Sfvv?ؔiOTr.e7_wq67k+賲[ <5^xM*؞~GelYlO<7O nvns;CB&M=q9[;yX& >BZcPkXqQՈ V^f ?CՋc7^{Q3/3nf ÎlV0PࠀF\~u }}gϿ eZܷN7@'[;ExmQ#ΕV[2}{_x9h@vm^{t{i 5?h5k~A3!q^⢌h U! Ԃ2c| oMifD&~fylRt٢EK~8ZȮZhؒdd5^Ńl #twRv|>VBjV# TA(YjfP%Jx \7[J% |"=ЅK^ME&֑O>+G  c @3d1?(sn@=u@BRI\2G^qobpC~US~%R/p'rGCuCNc5ORXe Kalư׽̆Ά_^G3;Xv)"L <B!b12AF?寙6_A%}32Тֶh;([kBl#|u̽J2ةd-yJnMlԋ}2*vKڪA^ VưBxf9{$ ܋/b tَ< dZo,؈ ++P8Ã;/V!܎$)`#>EN8X-s߃=Eԧᦾ~_1)뮻?h dPs]mJyQ*BL9$LL^?7V _']|lb=^0{"oy]~&R&M^d?y̘1aR`w&l\MhQIZkib_ueEݬmY×?#t+_N>u=B 0m mԺ5JeW+ۃc+Di-츣ϕN9>teLvY(T~ :r_IVl߽v#9,o Vgk<}(}T,`ӎ5=֞{UE w\cv_>K/| XbY^/>lE-Z"Y\2Y<<|s<>[TTnϿFalw G{F8Oϋvw^6`m; ==*[tҘ{FqF+=Ei]/ӽ2πg=d@IDAT7;w rC1K?L??às+xZB= Ijh@ g8X{uy3crI 2]?Y!á~1)h9M|],+'g\؆ -\:6 7]1JIW,K'ێfB;Qw V ^\ua<ܗ Ŋ>&CIˬ !r:OzⓁ-?^\<5gG?VL:#PрΘ2E($)~pgU|+\95ipIn$&Ł;/v8 T:}}q?B:g"Xí儫levCܷQw {nX}u*JG}ư mo.uNID9a/Hy+;vpxPL;me PZ {`dXӭ*PD+>8.Ȕ:\s 5pv9F~]iUl.܏0b {zO0mO-?ӊWזƟoE#ޠ?"Av[ڄ mіZr1_)1YeWd S zםϘu6Qٖ+n yD}] *ªB}x,;"% / ԧ#(X1I p͗~o>{r/grmن~!4 A Ogm}S&cgq|Bc* w-Eb 2ϦǞx}z(>kXX;[s278Y4d@Ϗ|PI:׬(Ϥdq:^ o*I{e vIgG4@"z5?+.Jܘk"@'xtLYp!'Փh#pͥїW夬[Ŷ<䰯xN07_cIm` i@J @^C}ك']zOPl\O|/.zGG>8c]Zh!OvB'?ʮ^_!CWȼV]{dY4fB;~CY,B)GnU˂uOHcHl!DdlL^fQJWtFEx nOM18ʼ wZڙ3jm5y']lm՗X4ٛ# |Se1Z2We[?fFawnޫ;X+|Aʏ͛n=i2v2alȐYQ&{A7o߮=j={.^[dXmAߓ-_dcˇHR݂ x&oP,OƷ*Bѣm%4;K+/x "OcZ{-V){a,!҉d<d 8!oN<\Bdz.gjk7E{_AlO lP@N 29dkF%*uBdPL3oo@ZtNRӇQI3؞/'TU}+-&l5ɅcѾE[g*\vn~֒{0Пx;gKqcr zoy#y{;#B!ґ^1@\e0;k?GVoCʋ!VG <dxt(]zM Kw 9cklYZƷ,wlԥ8Wn?С$9 a2xF'SF[2i4{t%*Y;{ǖ3Y~-wd`U 8yA_|eO{:df` e5W6(B{cs vSiݪLVF5KUKۣƁF)J`h+cVgbwA֪ټ!ePۭ PA1j=zS>Ӧ~fdest%UtY`YVBZbp~:r~w_&< hCm/ 2P2O3/Y5W_N:p8_?O;Kk-4<ȷ$W>__ۉ~io9 ÿv= W f;4vY-wAəD'~}[{bP ʒv;˜{bU:HY`ϲǀS~'o`֤ zky;9Xl·}-baq! 3*tV6і%VϔD  2qQ>zaG7Jh eX/L g҇Ƿ\Bg WCż @ /9is?qࢁ+VvR%{ :q_|&5|HmA6AFAIJz+O )IAMl"#ےRnbj?te[^ia/Au /2V۰&2è\DS .yeg!ΘIvΏ[q .07\2_Ih쭯MxM_=pk 2kgp #[^x1U 2˷O]dq*9[[N;0ko)'x 20ơO"O .З^Ur|.Fo|q/ =WP:N5 _#G3xoyx +*c3cD(wAY ;˟T6ŶfAST 3o_lS6׬lN%EV rƅm-nL jwuc/TkU!T>JW_R< [Td8Y݂m\yWkτ 9 5k[G6G|O<B8$.@vK/<#-T 'dÃws,.v~I 2_q XmʷCh9D|^}Yњd8@?"kKb=es qLJ4U1&>3F, vZE9(n_z+t2!s;sZ86%slp Yt.ɀf&$/qRVG=.cO2)NO}Ҙ6C68@_.}'[A cṾ\{qp,rI 7VTC>+!;;ry>K?O|< pٌ|xth8hE~L1޲{^@\ / ]d5l^?T>lOwB0ySB{-PP{]R7 Z8Ȍ?O?oRkWCMac5jcĸ+ƬҊd Y% 2Lr?Cm]{xė_6WL 2:3fώL\-nRWbrA\EdvYa9/d'5mOl-PlnPJA&(Pl羇 ع-|-viP$yoO :n,&q+\2s0+(ZQ%.(h(SFMVE TNbF+mLK[ΊL~`8JHu 2(:puRl.௿f~v~v9V seu&Mx  XGl|۽-ƺ_c<̂[ozґ[<eKҮt&@w~dUq$ӔrmV 3 I` S*AִP.bLm 2m8lWMېm'1 19&f*PE9 Ϯ(VY)K.)K9B2Ǚ_2)s^E =c>c%לY|?^pǮt_9.ɡ#9| tӢ_eW}ѯ` ZڥOĥK~+ mHbcYx6_*?ȃ/bGC}jLIY+7x5E.,Ӻ]d'k_E9,**&$2 >y;6hԬ^MenjWZ:@) RO\| kBIxDvKzmnl&Q|}\ppbReGاԘSq6ihݞ6 x5lLU[ĦO^.XmڸId6!oWg-:Vȃa%',UP_mIJĞ{ŀ/ =믿Ëi}GUӶlW|ÏV0pcr!.T7sс ${|bi)૝Ce⤉a}(&)JAF|.,wAqv־!eeSI+{Z|6$+bYᥓۘSgLu 2\ω;^eK,=Ǘ3ն ʪ4[uj\#)ȰϬ ~+\c'( 2W_i':i!Ö b Kg"d 7yp\; MzzmseVѓt僖)؃;_dmb]6iї=ZWq%VxxSD(yj%b3Ƒ$+IDB#NdKtg..Yg7c 478CB}?e@>mݱu6iВ.F!9(xD<: /5Ѯ,O5=yb۾ ΒB)rⴌ hGI,=TźVQb(q'jkRI2{Slc$ u^VjuXb+v['>Y|[ZjLldgN[Ik_ix*ywWTS\821nY]la [Gtg̮ 5pddS:{d(rFTmD$>!B/d`*rˏXmVAGW 5澀p3xx.F+_yQPy7jd:vϺtD" 2*TV96TQ 2uִJfڵc; B {q/'o/>nnA ɪ2w[xj>&M d%^y ae{1c+,Rvⱇcf]v}x TYHW\?[WmkϽ< Ω9OCut} y@O̢-X|ιo [ǝt}W@+|O.:ij+Nv{Zu 2:?H~{!{ۺk^ zD=}ϲ>P<䉅ù?@u 2*TV/wQ&-(uFg0YY/所Z`)DYO ‘ W<#z٫VѺ+{Iu#ģonSAЅ';(Ib'_gB-#:6d+AtБlhOa{A搖t{ >kSd'v`]N<%sko Bֹ6y[C Vz>-6]8`b~ g@b^GDsOk+{mك>t V]r 1lC;ċ* Xz@Rl$@`q4(ZEx|ъFK”dtj+?SU<؇`Ozx.YŇ |A/lC M _="dSᦃ2zAz:#`~h2 Hu2 9ʎ@*M:N'<|=;nWwf ;f"Mr n@ bznxĞ{G:]l]wAJBuDdpb;b o-r\$nݬS sFm`>ݣ̖Z7mmO1܆};drܱGI*o _6hJB&٫^-+VYneϮ;rF˕WW!|~{>{lnd`ĪΝko(ȴ%Ob' }6t0aw]O#瞗/&:^jɰ ;ݣ4 Qa_@Q?JgN6=-ãz%3j-!K>x&q^8Zn[~M2)2[A+G_/0}LauװW#$?-; 2麋lexZν|2K-/.⦀7ċx{>4{.׮CYejYkn^Rgq991j 2uY'؆믭p qA37\'~W۞C"Nu 2wvq.q}WֱپiP$ݲޯ#NJGc Ou#| ^7iR;Zdy#܇v)5PՂ S2K N[bUPHZVf+KM+d;';WdΨb援Ug@㒾h\#8|haxĶ-z*&9b_& Ctş_%: ,@@JM82*B0 %)'nzq~nLF sCbO|n=B]6'\19d.!SfClYG綶ς>FI x^4@-?m|-Xg";`3/ʐ%OqXuRAd-Abs#)o溇6).[xQ&Y<6H \RD&-K2%0] &嗟BWd(X{lTn?>'> 2A`~iLf+{z!^ OJ^QfwKVm)ϐO{Ҷ۬y>yV5ZkRa5M^ b0C` O;Ȳ++{]tUV=P֣.;-O۲tsf}p%HI%y%+;𤋼b#'}l@ ]/-Sugv6OA熛VXU΃&yU QuqAk?rPyik,dA3@}pH2't}% [Zy=^*3l8`CL U!'w~U)-r Z_1{1O$kڥGVnܭٌOԤ .+9x3Nū ŋSN<ºJ;*Lc~}Lʏ;@c5 fc`e3.ꖘ=YzQs|^3?U{w }o,zm;vE gstԚ5kW#YRP]r>9?GBoޚv'\_lw:h= LۉmZooy() E]LV/F6[m<,G:\?Viƽ&'?cXrEE_|V).ƠvچϱcƅŶt<- [|OYb~ MҴL'rsq_j Зoh/-肋hN|q_3CN}Z\-?2_q.ّ tO<5  v)`K'$2[| <|a[6㤐TY:S\l.?/!e{\Z[=|HןC}4xɿdX!SOYrmly[ZYCyIߓ̘Yy`F+/%8 i?/+O/̘xr)a"kr kZL`ʁ?v;2cJttJzfLyrmgx񥑍(ibiмmkP x*P T SKWs:gtxCeGͤ%.e"4in3_VlJ C@QcƻB0u63:bddB˲= B;wwت{dhyR"a}S,8ŕ{tmۄ9b 3_<h>‶ZM|+ {xT8FiaI]|;ǓSd P$(QFW66-aLA@2n8XYv }xOp!:)i*[&v4w$#lh^ O5?F{ic̉ёM`ЀX%Q|OLlɗ'I T*!rq▁c8Q`eW&G|U->?WOI>rBϕ/yN 4i_Otɞde e 1H-}EJQSġ|5Вii~u:xυ^X[v^<5̆AK3?x$vśYKzEq0mLGMuMq?ښϥ h v:h +?yRc=|֥V[o[' mW/+?yf#cx缼&nL]Kw絡96O>2dXXl |+y;FR S-KLP_1$c \_OB sXVthD =@6%#\W3lBk 4Z.r:>#yኗ|b+փϳl9pԏZb$ .ȥ9Z $#+=h>-bCz':#.}.GCᅾ.b'XV8m_!ӊF#4i '3d?1.(iZe3g2Љ̎}mo4OkJG)syo&7U)yk@L 25HZ=qQf>ڷqΥ(SP(ed~.4OΫɍyn9%Q+9'sf.c~)]eY~b{EǞ=}V~GPf/:2.Zl'f~_|x5rFISH`Е,ġ Е4<JFHN ЊN"kK+yp]-*sT:2Bxh5$ϻ ?t١C=]x+ fډA44QB4Ɉۄ4؉.{ tx~̓l?)ۊ.Ņ>qV;\߷,zN!@4i ΀ !XRO׺$~ztV ?l&Y aZ!S7v%%`yai CmzHAyg0_ԥ >2T{GU@<@hCɏW+]a^Z@|ٕ_säЗ}JVE"AVcgE 22(-xRH3f @4sG愂2ݬ~j֪Q۰۶THֲI<tmJ_Oi`+cGR  .[XEg-2):0wr1gy&< ..|頏-#.V˧Ѱxk.YO?i%}GNL>X_CV.5 Plb  '\ \DW6(H O<|d:x;J0X+:?T'{!zt'Ǟ0&pAt ^iRA+>BeRH3f @4i 2J3f3d6QA-K'2we> k 97}6icYpͻ _@`䟹l_e!VZYylaWbGh'5~+8@cn% @є0Z%>xpt'1߻7tIK'8:ܨ=r4j,l@$ E/E([< )H3f @$iAf.0 2dfF[X!e:SCA䐹#tZ.5# @T@@@^ p{%>D?+8Nlj1b]a =&eGq1CXJ5_ejı Ѐ@ـ  dă.nbmIdEm#C;pZMhXF>/[ee/nBƳB4i K2d3(iAnofLAF技ZEb>4椀 )‹pّt >sXhMsbWb:? G\4B>ʦW.!dW00LpTLpRl._ D}|JÇǮF+9G>DU<؋Lj]_aOl.KN|'<=th>||}Zu4>\E{ ȿr%*)H7[1>ig>4tcS'eA%dʯlBWeO<وuq kL@‹efd?\iAlf @4i 솧M3@Zۛd^Bf+KkNJEy#LoӇ9d%tm +>: =FOŋ*"ls?v^-K-`[HS i H3f`@Z;s:4 ѡd9V0ϤYCcdjBZ]xHYt .΅:J>иdGbd1\5HM v$!ӗ<'k '=H_xE?C2\]:N r}h?~>bEi{me$f @4i FL3@ZۛmY)0W "%Gjh_2T>r3i5U.lh.,Y\.涴}h"|yBM^s!!/z3/{R@u@Εx'EA21_C<A_OI#!)NZW+*N D&9;FoFtkGy 2-^{&RH3f @4sQ҂\tӡd -\z% QAsr*`k\} R %OlJvG> $}W,}p4&}h/yЃdR1] H3f @! 2ӠJJ-̴ 37 1ebAƋ1^ Řƍ| S;d ġ):0 ́E|SN14p⢏_lY}  }B}N >2бM_𤧸-Iz𹀸b_:'9G;z\ZMtZ{J?O ـ&@9ly1:ZŅ'.G6 MȗCM+_c.|cRJ"i0+q𱼒Yۥ GzCaCJ|hˉGGDo\Hӗ}Gͦ xX1I> {!tL5@4i 5E2_au +dزtbCeiy;M30g se)ٶlW tRo#Or3=ULQAsYZ.d39Gy']/{!%fu5>]#=X?$65wMtCch3{7 8/{?ӇW*< \@p8h& @>| -6:ѓ>8ѓmp&r%/[!Wy.d%C=l? A-ƌSK^lOvύQ!ǼPc_sjx@#0_/YG=ydd˿xBCO%@22C91 x:$A #.GOHh hJJA~ ydZŏm89+c" -z C@h]1>AA3dC i H3f`n@Rc(Ĩ(+dzou5/nmC>{٦L;$g4shZ+ofJ&ڇ/VquӂLo g>+d8w;3TCE3o"ɦ5$5zmVv'vlȮZxe=.tdC}d>:̯eC4Z@+[ҥ%@WEGF`]'49DS!Gtق~L H3f @% 22K]hIl/疔L3f`@+ؼ]F|o?|1(SQ!F:9*d ΐay4.*8- 9'82q߻"lACF}QUOs_@V1BHh]:CO"⢏--W^z# eFD1(P@p@<`+:4D4髈!9/7x# 82"bDtwR\:6p)<ԇ/G]1?t>~~LK~+dHK i H3f`ɀ 242Z};ddD:4sb:ؾ}9h ѼhŧX?^P28rǢ}ѡ ZPP,uIOheSbdT?}dɀkA1b۱?t%x ?C%J H3f ܔlA$dB R0jezoΓ5@9' b c3ݳd g^⚻E.sZ}W }d2!>|- /ueSGFx`wpUޮ! 99(IQ~˂U]eП.§ +* HHAҐ00|;ϝot]G߮SOs917a͈Oαq1~'_e<[ q_Y_MC $XcCbŢcQ 4!-^f< l_8G2 cnQ 'T-pGN2?tgoyΈԗ:Toh6eh܈_#f7e]yڛ_=:0jT[+P+0Vɧ/=x ˄xnLl_r3q{R|s2͘>-KaS[8!~ʆ{KqsfW2Ir|Cc\\O9IAa;~V'Q;1M92gH`ψ^OXmX: ԔIo Mjb chta^KyF6 1x ,?g29{_Ji>-t2y>klep}J]L[9iRW?E{PwnN# AI&or&\>{<6Q!OFmB̵+GtC府3bk|#y;Gqc.Q"ss|ke)QVVVVV`VِaA '&L.Pg]VVV`*ۘ` \ҀM9:5t8 ϐ6>CsXg/CŕM %l؄YE/ySyj=/:qè=80~_~RϥoxjCՙ΍ "%1HD&^yW D s|66q5><6 E$o4}FHI$}0x|雘̹Oy@`ecFx},MDQOPJ}2Ԃ0_gɿt̩V3g^ZZZVm˜ L͖fCʼnTqs* g?/޲k aNb 1a/qsx!A~ʌ/%=ʽ82+,~'ËEE|mC ̉e8!jְ䍇 ?'(fOL#A8zwQ$eB.= (}m3K ,5㒴SH| X#zCւQ'2.3Bҁg?C>ԗUh6]r׍cFca|Lk@W ܝMv80|ihL/Cl?=G?.~Se!Fث:ǎ {mB̑:{#2m(;9fXf_L|!#>S1ӗ9*cF964X0-DQ X)#>sanGsR%c \o`_Ɯb~Zzs@g`[LAO|9أ:|2c ܐ,E5* ߘAͯM MekjjWv&-sdD=RylɆ1oY #_8#K~sk!wC!F.|06>{u1չ/_-DN싉8Lu$p9z. {`s|B.Ș|9#1::*Cޫ\,$1I "!cE]s"\?[sxm`N:.󄗌/lC<|-K Q_{MI* X֦RF~534&k@@@*'SQАS1-Ie)oY~>}%#{Z[|``V{*H8c͋Q;tsys^;u7.Y~Z?h څ(r&Cʜ+ö_hndbqȂ x/b%cn"mBxF6DM2|`Wڂ ؚJy%"/'a$sF_\! % XZZZZZZ9[%ݬ@=j-Kr:!аgf~ړ"с׆Ѧ#L[yy?E]_!jڶcc'}6q28̵/} ~ЁAoo @^8g7f:GX08]Da\;([*侼9vg̘K?1Go|BsANȹ [ scwcQ2Gbe.c~[t֢=Q֑rdNeBnCbO6 ?-9d<1ĥR?xtg܋9ȸ#7~駽Oc=6~ ߸JsjCC` /G㏸#&-Jc! oYb^#d#Æ 2p칹 0!0~#+yi'e1Չg.|h^gDU* ,r޳D樞d#rGWÈ΋`!` U `C`smүs`o$\7~9=WGaGe]$ 3Lzt~lN?7QYSox^B.JbN\q9a9N,<Ĩm&t 8F!31Npjjjjjjjjjjjjx pXǝûg9#Lny\l휻LJ g BaNcv:c1\%wp\-vGo<sC~LXo&\,<܋}/ )e|>@|#<<#6\`ЗҖr| Jd,/l&>9jC&PV`W`EM/DZd h_UͿVVVV`4VgY3_HϘfңq%5Z]▥򄌍4#7qD=2/6lly xe+g$&>ǯŠ/?6'xee>b]~!|A׹1WƴJH ^ԕdWh`CSrx8"w1+q6ĵյC|Tfo58ǟ5lԹ=6>CԱV`V`KE3f(;]+P+P+P+0+gKk!r)qؐaHS"\g)(<a-r_N;|ٹ-y}9\=s(e6`cn=xo#W9{K:5\dྚ9wJIa`r̉F2D| c+lӷΘZܢhP α˃GO6_~J<:G2~'^"b*u+ׄ d~OtFC&i>7QV`UEVMVVV`Wu=Y 7X7M0!^{mOؘ`? 1X`TpQin<`~T#T0:''!+qLjqK;27})7 a[0:qЕ<>T 4(x=!| va6lGNꑙyLʉ)_ B%9da=s}3>lψO=6#&MPcZVV`4TgLYfѐjͱVVVVV W'ϔ =ŤH렸7/e^+2:p\"Co;e {R6b*G3r!#0/s98.519Ԯ7 T'gÜm&,2.l˅4bÐ7NsĮ 2~9>!A6@Й8964[ %cGvʡ1h=!CY* 1sY ̩@=%3  ޒ+e}*rF4V뢇l܈SN_6qń(f1}/s}8Ef X}۬A< #>/׀OCL2c<!oY:cD{F6O+cO}.UQy;Aʙ5xq{[c>!l g=k^?/1>w]GF Y;lY4EqŠ!ቁolĠO`iW\#| #>M0<:0/2ac|N` C#.s(NoY,5Q Ԇj zY,_iB4o2c_)&{\x̙ј1Ȉdb$F}҄1-0!n^_';[n\ZZQPڐRMVVVVOjCOIV`UhP_6Ps"gU#spSpC dF_ s FU1ы` :di<V;G|{@a_uAE:(G$LIsC) » qڀA`!Gyel[_oLm!|c)bԣC`ψFȜI#ӆ96 O-K>K@mȌ&[+P+P+P+0!SjzEC}x/>$M )!j6VԱ焧T> }5f <2.lC_m]{}a 1坋adό=y0?$μ2|31/w rK[!4 Y f"Kr oYt[xlb 61Ww <6Ʒ CWߞt!> 6d2ƩѐHJc@W6dz3 @mI ,  ޲<žgs6^;Ӭ`~`9^w˃c3X02~8^1#3vYyG6D=GYY~!\{%ɲXY$A!1wCg`3!AscN\G98Fף/Gl aW6Tb,sFmKv;V?ธ>7PV`U6dF'V!Sjzmn'6͏bѦGCِ>W!6ؗwsxń(ʼ^8ĥ:c1de!Ycg|Fc'Vk߀@8h! 9ĂD(@I%M d<쐕>ǵ fA92ccSgo|9so<}9޲D* Ԇj ԆLN 'dGV}#ߢgdN#9Sb܇G\?fd+b.Vg؏;7$cɅ-X[^؂QϜ8Ϙfr 7fWscDQ&”gN2^ToуX"C\(acd΍rĦl0'$^>5>1gtm Q N{;%nY⡾w0T Q!kjjjT6d jX =$.0'2P(Gxfl)g9T`!|A~='r._xl/Gq]sύx`\SF_t.wBnj!qs],Xƞcc۾P☛%m0O{cx9#1l3g1Ȱ#>2쫑1GN~^X7$cЗd뿔E(fO !:St:beq,z}qaQ!pe̹ l×|)#?H;F9svqnM흃iډ/xD'd(WZQѐyŚ=^ser.t{(YiXF֊uBĺxb<UU+P+P+P+0 ׾|dfgt7vW+0**P9[n3)U(Ms4ɳӟ+~sKkj3kyA[oiSlj/~2ݯOOK;N/ҳ MNLZ=ݶJV= , Ԇ){ Z+б!-K!NsdžG!c~ؽ2#rFtɇKG%'{Y0=3Xuf%l |+1'Wl&+e_m͍(y#!c5fm_QD8%Qp$H,^y$KxŨ/F ˥O#s% ư': [WqN9vh,d >iG|/ bgN'd(KZUlЈȞ%M8S$̯0yky-LOM2+Z>|X:ٴtgFe~=|cnV[[|truդkjZ+P2ZY2}ܷxAqvȾ d%ϣB671W/ڰE =Ĉ vC ~б< 96 6G9;i#o|Uڀi!s#%2r"ǜ0B6((ztౣ84XCcDW_ #d)ؖbrR\ӵʣS_Dؕ-Y=!XxaWEC;bqlZ1Y _'+[o<)1n̳5\.<CV]8 #" F ?~zY͓# 8HB&N/o}Ȝ͘/~{YQӸи*ZṊ7#q3٬'?aMӽ  ԆLo>d|,nӿ} .kCaXnY:$b{^##Mȑ؛ó[.͌s0ꌉL zt3W6ӏ>@~0+Cy'xs8d)UeD72/''&喥%>󩏤kj>!>ùҺ묕؀03y5h7.෥UWm<8-tSKOe-t!)1|H4b~ux`?r4x`}4 )O R6p)8?xUZf̉n-1%YiSoM;'LJmW#Zڐr9 /",^[Y>o?_~ ߯X el'O8o~p\!)wd9{M7ڕs3#NCoF} K||_^[l*sҎ˵"ҏIb+W $Ų{1(#ɉ+31F P%mo~Nf/$ck'z[G-K4c=͓ zSL!Gy=sO=t+MS mڬc>!p믷N\k_89silƠ_{jC曅Xۣ=o˹y{.;m;ec[Hq܏{2x1ߋ[C[xX~̶[o'vJf?^{a0useEWL笰QYy+ѫҢN;l[l܌ӥW\?Gu-70-པ<>k4qvg>no}0U06*P29ֽӿK/2\ti߯Q[aat ׏iw7M~~~NYwumGܲI?fF="[F̕yKPZ24B{bz`Jщoz =CŢOsҾ̷v,qg(KN93 ,Jz st┇(r>-sC\f|tBW~̽mbM#rp̉ư rm83R/0ʘ>O4d&Aޖ6#N͘o|9%fzSfLhJ+ط_Xhp}q:I~AAc|w갟FQmȴ1Ys3rjΏֳԐy+LK,X5kV/״~l¤gYz]/ƃS߿vi)K5\x5ǟ!3I7iO|(LSf\Kc{6[7Nuܐm=]a^Fѳ`CüĢcX44ݷROF8ż37ٴOsȅO0YÓw vO>Է6c2o3cOڌifL02N!S>Ἃ9Ґ6Z@oX\$~^gW2}+32/ pCo}\q'hVJFkjCf}r4Y̷lΌ}ܿϵ)3ɸ-KGdh޽+ANa2vQ-X0c^B_.;C9w\;"ܜFdx`ʜB0w 4)*1$B\hzF+DMd[4t= 푁5Y#bMxAcXBiC4[ ʘcؕ,k፽D3|XmʔMyP駿Oa,4dʓ+y;H$1;剢jLtu {t󖢁p\ *Ր  GFm _<> :tZ4ޮ2s2Xo,2c6pkew?-nkӵqz bE<'zYw5Ӳ,~kKKOY*MYjrZdE'|*xC_4O^ϝ_:qRJԐqt\x/S~on̺C;bHG-}6^sӔ*>s+2e=ql=5#zCy(">ن(6-zd!ݳ*+mK'{db__/;|ilii=oelCԔGB͍21 ].Ydx?~0flcFua\RLj_r.}9—,rckm clcY#yl+2GL^e6cSfM3N;>V|aKѮ;9 @IDAT?u,4dx|;m(~k|K=%CYwCE4h<;-KlB[>I-KԸ!Wn)F~o4dy|KΓOyfRKNJ]xR?㖝>9׼2M9S-fnv~1h$|b/^i!ւ̙ݿr)i9>Qenxiuʵ #vWOyooW4I#;>+൯lIZ*nu6/oFb<4t 7ǟxJxfl13YcUKf)|=Nx^jȔo)3PCf B4dhUjeZ^^W![ KdG <\ ,>Ύ6s.A?8}":x7rec̓ˈ2?:F񩅾͛<{ʺN15 MAC0+›,d ?G`Csq Qrx|6^ztχO,11F썥%4uN21 ڌKMAs`2.|HDkO'^O=\%}o-NۍYֈJC[+KSk<6n )*oY^ Kuh ` -iU H /Ƞp6{޴ioZZstg梕WZ>rM³k0O?3=1S]^U~s3ԉ6`Ȫ8]qM}`1'ߣL?^u3Ά k9NLO[mI MK/6`n;FӧѼʶ2ל_pels:fWoE`n@CW.7?_IsG+牢 sr뭝?NM2͘c4}3]x'?oʫw C*C}9!s m b`<F<)Q؃}+̈́ r4~קcg l[+_ciCbliތ sMZ@TuF"A}Y_m플zN ŗ4$(dC8@/B2Wߤc."CbԃUυOdgNLmnY͘FJSCޖ{r<胩W[py-\26KԾZ{8>)4~5X-{06a ?vYͿ-Y19$QmܒXŒۖZ)UiݗO։n-1%4t780)ʶi5ujtM }>hXLx'a8]= ܋3 4m#>rU7mOcsȓ_h,ZhBpۚlƇ26~,%οoC˫fWޘ|\fםMn\]riZ/6{S眠)2hp'N[nQZ-@{Lw>oa!3sA|.y]_J3}q]fL4^ݨ30^m`AOC+7jY\I{%WN>cCZ߶~h(m6牢 έ)ө!3^1~e&>ٯ )3qzy,OWu; ^7%l#?O8@aG~cx1-++G? {a?3*6ȍH|6}ʛ %+ksu$vT(1#8 \@S{oƳc6g@?H,eCfzZ/>T򅏧u^yѥW\?gG{W"Z8t2q=Ma0Y4?,V\aN04~h\=?SՉ ]tk:>(Mgk鳿)˟jŷ;ZCjʴ7d{3ywM̿O{Uǡ;:!~4yaɈN -F[Jș>xpC?6=s 7>Y?^ ?轴e 8`6;c舧oïyWK[qaGu2eb^JYɣǡ2APss/k!\XK(6ڛ+zd2?惍s)V=1&@S+;7{!sҏ<2Fܭuiǵ??O_t᜿R%?_>;BUXVCUe8nI)Nuld< DLNZýAoզ|A! >=_SW闿fy}ҡ2Ϝ?1%};íGgsA/We5 7dCy4믝6x|T[To厖ԒQ[qe>N`(Mڐ{qkʔ ^=˹rR鋟D!| ;%f͇%%N +:5YBl68a_Zb7'٨0ba,1%&<]x|>k]~c>kLdHF_r1Wf|X~XJp%J <(/\~~CeQ]6`ЃSlSʉaC\XfSE_N 2x!-K΂eHvi$;Z o#E9뜑 Օ8~!'jWo}ȼZ:K/7jۈÜ[vh&xbHQِ!:T[ƭ*fQm>h,l|ݘ!nZ}|Fvݾvn-h mNpmY7V׿slϟy?T+N|+k3oק^Mn܋⶞F;l<Ng> ޲$ ahf'mI4d &ačۥ^ʧeT2ݮh2{9f+g-K["95 Oz4d8xؐaiOg +|y)ga/a/! Ke`Gon!Vd"G.Uz]\KF8&H{2o ˄,r cu7R?Rf,8k@= pN߮9x~קr}>{B4dyݐi0qpHv?_[Z5q$g& 問|oz̑Y2},ƪů.Nn6fS RWT m/}]p|F>7_N~ޕ1>Of@aPڐgƭ?h6NkrV~W4_̔'spw] !|`X+Pܲ\:$./ed9j4N܇qm܀ŇT XsI`kl\8x-sly7N'>E?m\I茩e,-oPFt$05v%^{x{KM[6pF @M܌ki,og }2e$ľڽ?WFSrP" /ś<# ǿ k`ߐ42④y9> zƇx0r;bxͷO)3rXiP1{wC;bXhӇn6dؼey_bՆ* RKfzO~mڛ2\hrk3E֙TZ9zЌ9q͛oa0)z!r9~o5Zcv[o+yfV4ƨぶW^AWWCs;~:~fknNNp6dvW]o_o0. {h~BjJY1M{$7kV?x2Ux q9 Ԕ͘Ӄ9R1s{+NPރb?f_ WhD=Xl ~]9rՇsq`l'I`9bA}7#6j_vP+"3Issa3dqa܅}1"rı ڻHC|%>9:!v6@Й99#2k\l~fMte\ wo2g|LO>l<[]>'ok9-K=h ec;oܵyZW[6;job[ dp6dVSCoܐyaߞn=C6\3^.,on#7}}Q_1Cŏzyם_-w-~FmnYb^#M 6Gc1r|G9rudXljgn2/!l3/`^=|Șs9o<0r1'zlԗMWmEж[m1#OOK=sk/7) g724!;wcʆ N:=D|PvƳxtyNe< ˦I<h(\{}مluH[lcOz4 ]oWTR Y4o)XՆ$_Nv0͘GGr6[o?`>4#6ض[#=/#dL` x~@2C|Y9.b- 㱯K8!tؙ38ytsrƯ2p:ѹbB3'w3B` ,:~ 3t HןCoq6ڪolゅks6Ṍh>48Ѓ,ԮG'3C`C!ї\cx$2G#|QmWXoX!˼͓{:!<̇Ҕ)1oׂܐrH nSՙ-F90| 7\'mZ}* HO>=ƃ vRU@CSrUxX9̊f%]ͣ']w&-lZƪi-7i¶'hx> XR4 Jz wm5%yS.4Lk.i2_~vNY:nU}BG%$[ g6dZ3b>2GPÛG}9cN\8~C߯  N;./m<0fsûE |\Qf ̇-rb@pCė?=~س8sAG'h70ȜH: 4:(9œ .lcLƲ!e.Fѷz!,s^`!}4fs0e,60ʗ dlAX▥^zo؇ʍq}%v (/mE c||'ӟ@;M7|gn\}+7d W6d<ݶG;q g_e:5Vu)mi<w.˵YQMgWmO< _T[Wl~xo9[|ij^hGZtLYգo3I}0{nxǙg.>nfOξѐ7ܒ%ߜuYِ+̛hiR2޲fwт>!&~H}E|~KJ\t逡YUh Mw&{&Ͼy0\\!Ɔ=1Cč1!3"ċ2'k+X'ts}@^ԯxǾEF?;Ȁ8 f"b2Au- NVzt569G ߞ^Fn>a&> m_t`+/3;cum`[#ئ7'dHjow7ně Nys~C k>gN9}Hϔ?}1 5l|/sλ(vY}}|A⺙HԆLQci=fV˾|vVAW-\z|▛%퉇6O6ϵ=ktlȔo)YqȣOiK~ENB5 @(?=]|i&Ioxޘ0=㥗hF==RUݐ)JS.>˧@{|}o}S׾|_j:T\VUhpB=qWò'9)c쾔(re}s1cÃC'~<8Y?xt 61{;76 !1З8FtdF`LZsM"0H= Pǜū` Uӯv !!ǖX q-;vsC\y=2x:yaAM|U#jCf<4chp\̱yB溛N2]wzez!o'mP}o;Q'#4 h?)C#єM;4m͘PݦOѐ)h!H+\_οRc !S2w9vÕc[+@F6͗r]6=hxDubB9s\;ү{m}GE|F:£7`3?P9/Cs@踐g.dsFk\1#鰣28*n\<LF/0, e|dcӼYK聇5*^4oV]y}Uok1kؑG!ÚVX~tx4x&k7\OQsҡ Дm)CkLyt͵7+[v]ŧe|S+m9(xbCfVJoI.̙.GwL{w*3X  *.0Nݜ'JV2ܗ޸lfo `3B)c\f rd\ \CɅ }m9s1p.l;Q!P_C?Ȍo~+!,F%LzROPe+ocoA,Xx198-r[88xGxl_qm!Xtڈ3r[x!T+0*`x8 jNBƼ+gpf?OǹR^F 86 2ObE2QwR#V`6&3ly{Nh<'|kR29k7X0k̃9K<l R!:{4 >c ܡW3|z#l+sԣe"ˆYZd@ slc޿'j{v}'#uC=[@]4;<(C~@yd eC_NtAq^|6Anby_Y^^?3&a!3`UTq dnKmV7k ss YzXz̖蓭>6 g_x0H=^444\|؞@y?ۣoΩ|A[tbڗ[v!$g %_r%j=ȝl9?:=I7@ӡa 0 %>>`鮙|&7. 7~t7Pccr9>ɥ[>t+z`U`U ****p+2񪭜 !qN =$Ϣaҏx lSa r.S\ģG򏗟9+LgLAfZzn4r .! HzΗ3V=nzrzcX>d ' #Rѭzbx:An}wXm/@ufC 2'0,vI6 ]k9M`']rścȮ7)2]P2Kr@M+z,XeX[vV[@fCX90y2~oƇx-}.<@0 ӧo67OV,:|ӛ'[X/+w{@>b ̗tl?l@1+]:'rO_/q N=h7_pP|G1ö!/9~-&g-ڙාDgO&=\'y-GO ܆ mJ+UUUU ~E~UU`#K1-!?†6zxzs{r4 z+U/or#?|MoMWm2|4_ ֗d9g_Lvуbb&yh$vHdg;z!mvC-_d~ɻ0]4>yo| |Y?_)Z*pK*2B4WVVVNT` dNcmVkK Kj>DH ! ;mz첷gY O}yă MG ت淜ă?o~.!xٶS8J2ݲOJ \]d.zxl6˼0] =`ooRo>n>À\`1@1 og;mC}Uc-ܲ ]XXX*2ªͩwF8!s ޑu t,AntCzxl+|ta:M%/|~lGsro9]z~C9Dn}~`'_oW|PLS0wRVrJ A&cg3t҅T.}bo+>lN?b[7;:h@C}ÜG ܊ ̭L+UUUU ^AvU1VG0nXzT =&zu^rm&A,D_8gh_ v`??}I.p~^93~avvH!9Q'K/`mak^{XWP R%kQ>˷۳Ë_AnٳF\4Y ?3~2 VVnC@6\~@f"k**PwL­zn0zU@f&&ד&÷ N:Ǯ>w+'=>{0gƷ/>(2'ɆrM]^d;=΂IW݁DfYIr%Vrl4}swz6og`m|m/t|?=ZlaW@5.XX 7_ۑrU)ۿ-Sܞ!^M8zL{8}CaN>O~:z>, x1=/{ |˾dbUlG}gNBANr_qICs\AtK䣤:N'Y$W a‡<h:̲btD3pX-x/O3~C}G5 ܒ |闽k_[JsU`U`U`U`U`|E>{3UUURoYzHKjxl0?}:AdW?]|ŇӻeN~ytdx=/7AAngO~-z)0-}|GoQ8fuK ۷ؠ`l)< (ط+l7<d:C$/YMX?y{Y_{я UUPWK|W~mHu*****U_>;~fM4-KKhQjoia}0\l[ w2tm'}b+ ijoho忞. r{t*>;4'󳵪 w-F, P>eCXB+0,i?t Cg.}YSkG\tC|6[aE*~ CI} _5\,*-XEXwܢR]XXx+y ?ԻA4>^ 0Ş@wdWm> mc g#WW.W X|Zr>|/| ~t?l7EWu␒%~tA:\zŤ7$gmgoчF%/<4lɦxOwn\|+Hobc#ҫfl|u0,Ny>tA㱳l0`L|.@70C$l]lO/?< oOgΏ?o_:h t GF ;enE[) <%Xw<%zVV`Ȓ~XzTvhz;TOdg@c泝]}-Gϱ.~v'ť3 Zt҇gC^nEΆoi0(1L9{%bӛ0||(~{lNϞY@Ћ/ov$<,tQ~G2_@#'c`U`U6V3e^yf}m+UUU'M![όy:ʓWi ɱ@zZXH^:ȍoOrbGo%iO.~C+~z~? ?e^qɮdrhr2`ϩ g9 9!t%}χN7{ɾ\kg.dCăAStQܞ@fTaST#Kyn,~RjP@4_ {t+}Wgo7HȌn)̗sAn-0Y;\:W:pl|>Wt7)μǭ 4$ dsQ*!6.|Ϗ= ßcc;?l9>?rCGrggϖ%8tKIJX%Ծd+̜8{~(>Onaƃ73u>?xGSn;~^rѫ.>,F2^x\0{˓l5NO >a<>GQ\~x3롾`U`U`U`U`U`U`U`U`U`U`Uɯ۾-w^WyXYG뱓ztW~"ofk&d?_zY/=g@IDAT9xp9Nj?DA6h%+>2@Ng< x!Hr9O×tHļ^V(;WxpAn"k@fӱ'ʃX| G'~W|n^}XY3h0BϢL~3=|zx6U],<6S> 7]~gMl)0hܒך/Yz%:Y8?7Dx[>thq*H0Yl󀣳˞<~)6K>lYx?mϼdt!, VVVVVVVVVVV Lw4h2ȭ'k آaKzAa{L@| >M}9tb{>;t|1ZhTtp<C %'AH/x'@t$:dkPBX#C[xlK~&Lq◛ QOQ|dI 􊖽A!>J>d͆'h~ǯ&-=.9x)~!ڝ 7snŦ/x m2{K}urvʮq ~N.lŷop1]>t'l @\|/4=2t{-:A6`|LxsL2{oY_UUUUUUUUUUU2[~~?5R]VOE0>;U#ٳ&/F`X?t68c/{qaxx[ ,~g(1l?b>?aHC -T]JG|Dw`šg_6N|6ڣcWLvW®س/ɊpebE/t^c/XXXXXXXXXXXXx+] 1ɱ`7h؁GVO^&k8A> <芯,1@a:K1Dgkw} 70s7 (:/@0t7 JgAN PAñGW/vqklm8BM.! gg6 9Y: /+09mpL~ϖ/,XXXXXXXXXXXxz*0 d<]c7x׷#x' GiW>Ȳo@l-M\ʽ൧s(91Ƿ:<^2 !AtJ/G/ 7bs1ȁE~Ӊ[|'dOjO'dȑ퓑gφ,<ݻZ?? <okyCw|\^R?k3ePN/ڰޓ gxħ3 oXC(b9}1Abի㡋_<{)d<˕3KD=Е(d AMɳbE֞ .~I:Ceg+_2z?@|uK$K>l*An<6A:/3_?d <0/;dCcJzX|:ٕdDHf!An>[~)j|1//S04_&B8 #Û*lxOu,XXXXXXXXXXXx*--2c cɆ Ǔ57LAf},7E^>+`w<|sd~r{>r/Y?YOr?rxGKJ+An0'b0ì/:gAnk?NwÔσY,4^r烤;;t%0p>)nCtoW.R\.W n_YlY EX***********T`CsF? NVft_N'=>D>aؓ-2l+F:hpdfKG|4ox/=_s}r>1zvܞvzh`& )i \ Ço>]aų^ELJ?ȭ0_Y?۠hzh|n r,ƂUUUUUUUUUUU2=CF?i(G Mr肙'|/.Uxz\4>]Pt+YM}288.ח'/^~l; `vE̖4[@Jw6GشŲЭXŁK?{/h¦5G0:ζ3 >Wj!" VVVVVVVVVVV Lw{ǪυmG~SOnӳgc+ntӁx0H=^4tvpa{rqlΞ·.An}b7L>7R1r Rd} wv}: lP1wh{. @|t~/g04hgrC ramɢoec?ɥ[>-K VVVVVVVVVVV !ql =UeG44i`3D-]}j~ g?.+g=Ί?;~x;淞M_&/~==$ b_IW8Yte[/{t~/dN7_b}<| hXB'C5O߻wuCQOA!^|E=z綹3z\4>o ww !^j^ݪ.f9_9:WCŶw~6^g_C Zxs?f=;E;U. zfc`vo=CFU llE' rwQO[ sH/?مӽ~ Bך寧{gٟPY************W`zᅪ>7~2|֛:{}eCv 'g{۝9g#\^h:Öޙ-.\?ڀz.=N؃x=%nbn٧ %K..JCY=_<}6e^|.̇\g0y7)cߝ.lE ~䒿s1-9 7>b*,XXXXXXXXXXXx+ /` ޱs4,0=C@>ؠGmbO'~~$AfAv1nOrz-bs6|ZV_B 3yN9D IYɥw;(.J*T_}LI.}b/YA|t ^G#KC}GF5~-XXXXXXXXXXx4_YÌ~nxyn, 5}2A\ʃ 2dO.?qG} 79\~|Wo{ARH΁sҋ?X7tp؆ kXŷhq rKրb1}Go9gr*`mEl;~e~߽{25|`ڪ#\{}װ CnμƷ NK;+# }`o eGCXCUYUUUUUUUUU]59LϐkmS`Rύ^i>:^iN0]t~tĜ, w}ӯ_|c[sf7疿W>C$tȓWvsR *٣;|NlIťЙtGs!MYV,8 ]ykGPnG3~QgȌj kS%^XXXXXXXXxr*4\˽oYz/[@ *zTrA'[v@qGjpT?`G'xt@{8 k_;xrM~-z)0-}|GoQ8fuK ۷ؠ`l)< (ط+l7<d:C$/Y7 oYgy~Ȍ2,XXXXXXXXXx*4hgx{҇zg[zZX&'tkN>ۣ-/Y^Lq0`Ͼ dx|Fe xtأAl-@~}zww!$`Iχ /]:sRl > 梣%7}']$06vg_yŏ@_E_T}di `ƿ'*4 h%}޵85ȍnbZ GvtT2e_6ȟ\^W X|Zr>|/| ~t?l7EWu␒%~tA:\zŤ7$gmgoчF%/<.AnzJ'snlŷf,&<6xOGU^**********p <Ù顾y dVAnd>h(]?l`S>ңYW<L/ OM'Y?;KΖS8 fyt`~ =LrAօ£n/?< ۞μ>5,߬7&Ņ?y:d<2^?ϐYCq \OpfC^g,=^wcdp4=t`{0|6ИlEW_ s,}r]aq̾v萗[t[!y r d%lx@NAnIf/_0_A>Ǟ>[ӳC r=x|YV%>/;haҽ11xRz aU]**********p-xR32^?X `=-$?Ʒ'SXNzzzzٴ'Ƿ/=r}+&ړ?|ٖL=~a?M8tVr^|j&' 7'E}?2:'CO&r&}f3oNGE[np{ d f\'g>m}__C_W߱y;_e_v狿xz qKw]ooU n^\O;_%_r5y͝׿w;O қwVF~G]}NSnwM3Kt-yG!Xՠ /\iEZiұGO0yE?yh @n>v|9'M3ȥsg;xOwS̛iqJ߾@LBfx0bo80=29z.}> ֜=<~|#74y }vl _;ȝ3PLzhYϐYC/p4Ë\gC׺9lM__n/TۋC{swOtk,ڷ|˷l.~>nS̎nzّYuF=u4rz=`OwI6䝡>+ݒ.~%g/V /Y 3'ΞA>ϧ/Ɠ;rتŇ` <78 d?)=~C΁moY|d"͹,p}'> bO7ΛN~aOO+گ;ׂ_v'ey^w绾|W~eM%+4_p \sml_Nݿ9f:rQ^rѫm Em4o|hTuh*~jǩ'?c7M~|}8җ?i_No2Q{:MθU~2>c'7EM' ~<^{rr6G~lJV|6d L?x@2C s_}/14(/z/^Éy/P|Ewt+>t܀ E,cOOL YeN6@Ƌa _5x>} ngԧ>]7c[A< Ϳ˿l/^=dKZms]>ono{۶ai ~>p /Иc;u8}B-۳fኟCw3nx@s57pY2Pw\0Iz29[Ig_ޞwгćl+/ Գ:;ȝ`S(~9tʵ= '/ |^IrNΏ :(rZ }l=Lw-pYQCŹL#>45w]*]_!!cɷw(w^gq߇ ]'gA;S~4`_ړ5l^0-/cbA3x {4Xr`_ >}x oo\t-u*P:8С oǓA\$zpY {䀌t O|zl5(_Α!-^<6??8crMx}߽{g.꯮r3Cǯ[?zGÌoonw2̝M??}!]L2mv\a6-esM/c0vϵ4z7{ c|M'ay98/|dI 􊖽A!>J>d͆'h~ǯ&-=.9x)~!ڝ 7snŦ/x m2{K}urvʮq ~N.lŷop1]>t'l @\|/4=2t{-:A6`|LxsL2{{ ߲tۇ=n5wڝsVnv| e{{-KseC:rߢt]+;|F}!x S59N5y\?N\]10۝dwU׍h==!u/qʟKo{Koԧ\ٝ*鑁gيx0{},}ȟZ|W10]h<ć-|3`Yso0xXzf\pI烮#t;0г/rvf'>xhv+&; +g`aWtdh2Tv1^{:!z?.L#}]9\m2g),^_3xwW^Y3=O,|x At$y:{t)o89bw66݆#Ͼp\|v|f|or Fklң6b}p{x 9t YCʃd{yz{Vsy?X΢)3xaw^$ҹ`~Q=Uw iJ߹u\yhپLyx2sM>{-аk.sEa>m dq\r0;v} A;fַ]ˣz\6YywU^jכ%Oo,ͧ _ ˆ=L'X? 鲵4r+(><\+1ķ嘭ŦȢr->מAp` >pz+(l*dl O'oq?|=~=|1?ۓsOFnXc >| DL>ZC:nj^J3x {p5|i?r{gjq*sҡ1BsyL:5(-?x?Sʛ/wYo։pױww|-%+pفkhmv` d.yNQkz+ reȜr뀻L] >o}[/[l*An<6A:/3#nsz6]=x~X ǭY~4i`f>xsLm2W7HE2dLs}h- q {쏳m5y؄##Kyn,=Ґ^ ө zgo=ȍ7^Р8}d.z_|qiz|s~te3 0;P>#/sK=퍦yCo}7vÐ9^x]cE = <<ԏ'kpQm?o<x[X@o΋|W>3f'x,|Xn_~*4惗<W`NdU/:gAnfOz\xG;tt|TdD/ EҝC^Ha|rqg\&7|g}di 9yp*0Yq_zW;T%c|,8~ f9ﶡ̿ Yїگޑed~z?? 512.eݓ\u(2w=珕y`"?0|g˞X;d>7rxX0I}0 6`/opw7r:1%zL_g_ng[1Aӟ 4[:A|s|ɯx탐%9-ѳC{6AOIgb>$~ -/*>>TAn GC G'KpkYz\W!oܴ:E}ȷ|8U>94!H}ap-[=̾3 y13t \σ}ϭljBNBċ \2¸@P3]GMoz%2[lʀw7#~.׷,]Rי48#EN> z7۸ț?,_-ot2=CF?;gī64isfzA肙'|/.Ux~I_zhřu-0gǀE-pn*.(_/~A6'= w!d^?߲8S5<}%39^Io&ES#K\Оc:̈́=bC|(j~2]ϝwX??8,!+@5>=.+07w\/!>@jϯ#pw;p{GG#>s^5t,w\XO 1҆ p@'lgmƢWb eM\|؞\@y?ۣwmx ӟ㵏vMv=q<%j=ȝl9?:=>T 2_2> ZD'leox١nϧxr&,֋$Kávr G]XI8Å{h8h M>?9f͗w5oxq cc|ρF; W p6su;Oa2c8\;+;ҝ4~'{wԂUഁy;L~̕9=q"o]N:t0_،ӷowNg2lrt{<} 7[.v~ g?.+g=Ί??<ȝL[ύ/oÃl??G䯤+,²|U|=:?CEG/An1e>>W4,Cnŋf z4!?wlw2p^Cu_>Xr6zYSCN&0\i{Js^( d X׆E`]Tr:~&O.G>avq̂W2~YgwGz4xg_*t7-]u#ůUrGj/;_> 5تǍ?XX?^ONO E ww_n೏HN/nC+~5y砃Nwm.lx|5Dn{4Aڟ N%JO`X2`풜m \-s5DLO7?=]Io۳Sd>AXҠg!;6fW 4ce{S%q1esM|[ Ǖ.[崼pw/Wh}{{??؞+Slwmb.39+gWy. ̍ݻ|dV:Tp d^-kG;1a4ww+x1G?2oƇx-}Ύ. S@09L^|7OV,:Y r7ߥOVآPoY<6dk!㫚;X \h/yPt9'/`/q0((:9캰q|A>Ŵ/ۆ /9׎E7[L6l-08h1#QxdXhr]ο{ t'p=܏AV^pXZs?sǡ3Q 'EC|\qrUm:^CWc7}\JNV/bu+UVW^ ?>|ܽ{Áb,`}w~1-Nr|?7% %=ŋ~f.06Sbܡ6ۦ*2p&ϲ7͗Kpi[L?7k1ydg3;6ݦ\ϻ,"EcxL^p[scW-ÇhX `W6a؀y.{rvGg}~<兦?l~ ?=>SyPv v}pPb4 uţg1^Ň|%xh~K|28=xma@G.K1 [.?ls!(by>xs蛐Ci 47p4}wO;yֵԧ>}k?rZՎ3xC˵v-k}wĽ '3ܽo]/;0YwxfrAKvPz]U0;.L9]9Nt>xO= _l )^6xA|di{Žjg1?(b=ZG]{iMZk3z?}ױo8c._~f3rg|ۣBr$%hO^6\_*}E e;]>|˱=;|Sqkg//ztӻJ@IDATe;9-pvݻ2^xu8sistw/zpaGa3_ /rC<_䴏/ߌkଏ(_2 œ/_z7e>9g߾<~.mz܀Ga~<-5a?ofoN[3>b4=3F>n桳{W͹v+W?KN9ϏzgnExkx~yA2`_ch07c G7[rC퓳% 咼fd0Ͽ}g۷=}2Jp>K,gɲ?~m4ድ嶜iӵڿ]ٽS(Zszh&Ϟ7Øx%캺+*8N^X8u&~g[^^Jemϊu&qwzoYzXPCL{KO d{>R`g{`e?ˋ)>l74_Loh̽vav|{4(>?/ON@.]69,i?t Cg.}Y -Gt\tt⳱o𤋄&Æ.^1;=ȼľ,]P:|3c\Wޯq^XXXX D**(*0#Yϒ]%i 3dIߩwk rwkm>٠xdG7>O%C[Xk,m?>ŀ)An0~>͇r7\ް|AwAnC1v,LPzsPt?X')Pr/LNåWLzMr/}l=^rr˓b䦗tk?Ɔ\|+Kobc#g ұ'|gs n&p<ΪYy4RcP_wtPu!lu@:3`ǟu K7>7tq!O9/מ!sfFsJ}O@ÅһCx^wlMwGxl  ]@ܱbf=? 3kA=>zO.7t+>=,.nע><rK.w6| p0O;AAaz >3-}|kC7]>g+wzt^|_|/ *wߢGqeG>l^72 +]$釵U*p;ĻI,P_OGAn|{r=;=gӞb[d>jO.~Cg[>0I<~6->Z9;{ɪ s*ppsޜ >O [|t N M?9u6^|B-?t@<ď.l`OA:A5nڿ5 d98iqS<:qŽb|U`U`U`U`U`U`U`U`UJ8ɜ<8ٜ?<1}ϻC湱4I=gA^0Ocڃtѭ̾s^O=Z Ey 3O|83_A^8fZbҷ/,E嫄A?L?? jA'5@F+ M=m=[r×r9G^c p;d5\8i|: β9Kv?8۲YXXXXXXXXi8ᬳ,i>O9#K_l oEq`m/g{ Mg0;2eGfl#?}~?9&,GwCV| P>8tKIJX%Ծd+̜8{~(>Onaƃ# N:Y0y~g_~<&sC[[>@XCƗpY18WVVVVVVVVnz5`wdp|gP_}ew (>95doSO:G^<{1;?G~犉S^GLX rr&}%ϗ<'6z;@dŌw'{ 䌨ptٻp R(>eiFVF)L"+4.G1,/\ФKF4#,KSXxA;}{y=cXkXsfxfO %< w{B˛(N#_Yx!9ok/c-g'0) o-/X2W)Sr>;%糅k}o1YYYYYYYYYYVP\)SrgtJwJ~*Uyo]ߝW<}ՋgNnU5Q~3_<NӍG2XN>v8dLWo9YEB1?LtCٌ4UPw $PN w- vlH.v/_É/YV(;l+l\!RQNNht"doX<6/p{; oXwu8qZl"ۋ{1GvO幞MYYYYYYYYY7bp5^"cvOZcSqu=zצO_4dza`̖~3;rvd[lx3^]S<`s!c 6w!W[qGO-GdZab5<():]8x7DD|:\cd@'-OɿƊَ {xt1:OS^,t-8}rɞ dz{2 .]]|#!N{ez:o<|5yWq02ۨ{="zc ,t̮TYȣGΟ|0}9Dzw[6ʳ ˋْţvCkţ+bC+: bUlWM4{'̾<0_Cϯ /-rsvo8y? =]6p Bk ӃGXeC/\yCf;Z`X£mJGѤgffffffffff nˏ/? dlo׷x9Y:|zr|~+b.xM>~`lZ YkogClc5+gxّhMb!g ΃}ɓN.{@t']>]\} Šg-? '?Qn5فSN:@>/B\dA?yk>Nr**ŝZZ0[h y4`kq+X|Z:q]#E}G&i"ˏ1ӅӍ}ڧ>?8ijj^>?@$>|4n"dЋ'z,: oP?E}7^ P\0}#ɲ#w7cq]xg|0? \znA/ y[hh'Gk!? l̻ćm=S|+d貝xV`V`V`V`V`V`V`V`V`V`Vި^Oz4e|xo[ݖ9%;; d޼>s9 a` 6`78ѻ:e7[=Madzxt~?ʑ /ȓ||ȏ k܋e}ck̷WC\wdz |N8-Z</C^m͇]EC_ȵXYQ>hvhbY/ܱ+[pVnma=%/y}jԧ\ơHtԂ-oO; d/pB٣v 6Fk1q`pmj-0[P7L: Vt1ٱ'̇M9 8p-cta=:Ȍ՛P$-6]t0Y~_)T-Kn\3l}0]8]&)*YqʃH>;'BFZzn4{?o{.XE`@$x4 |+X_G'ZPlq!ל2OF+C@#C5~/ o^CBÑsVvO6|e~ǯ[jɷ6ٞ—[s>sz;c P`G=nEtǰ^=]1@vW+dlhr "^Q]к_wl.JJ(|b5!?(<.?9x|Z] Nb;Ił+4c9ZTNOzA[+٘@~/&bғ0}&s=v 0귻L{~{21GHex/},k x|-o1[G{t2 z{^.i #xғ9|Ǐ m_Vyxeӡ_GF[oGԑYqM@6{v/Wrе.7 [w~/;ŠM}asilGE$*2R@ƺ#ݚٜSt.-~a #77tx}eC~y~ˎ?z~vAב n]h63_ .ޑ_2~hq'N18_v= .m'݁Zlq4mg)KŰ9@C;7)ߕ. XKCNW`t,#U6atY8'e{t5́e_i7+0+0+0+0+0+0+0+0+0+pOU@4Gz[Q<軥[-ܲ~ YY2\=o! 'GC] yC@1GmgɎ|\cLG?lsdz'w8z␳m *ZmjY}x'߅*a A+Y]n;Qv(ūP(yt8#&[@y Lg+W|q#4H^xX~My!+^^%-> 5c'>yB~@.u銟=I 7-fvɋGl ^-O]EyC'p۸r_A6HYȣ-]-Xz[cd|l dȼ_{z7z;/9Iȓms7x,g<+}+' %~\pIye@rNHN|!W:t^1.`3lǾ0P:4qF]уp~X8W[x8Zέxd/Xtox0P^{QG'/@f[ 0<:N=c9xxn)vGC,1 ?)>|;_̖LlPhk{l*??4hP~z=?_tٓNplnp"`&O߉c!W[xŀa3=} /$GlXttw,9Ldžc,v|bG?l .io {Q6|F||dU]5uLG=}}->q˒SZMNֱ+0o֟n>MV(Z^*X~+,?-Ok[&/>(bo-ņ[7,X;υ\tGtH&)1)$_DgNb@˾xazx\vd7盜owGѓǷuU, +pǵe7b9ِE-K6릾eɞV7#7~[z+^zfffffffffff ?w/% KŦk#?{\@2-g4L/X 9<,kA؃|# 2=-0?wh}0h}bm0,Oٓ%_3b'zGO!38F}$t滷މo~-Ȇ}#sbٌ|1G[Ò⍶dc |0}.^gCO2߂/=i@g {QG'+9V~vXG*ovگ+><ʶ4W$oyVW2;{ԣuG+9]ibC>{2W(Njl1 [/֘Kϯ|xX^6c]Ml[[l[ܹ;vEx Z== .zbO`aWuC˷#vvx6t{.yo@bNjK&=EIˏ=Z}".l#?Ҝld{7 [4 9E[kOǏٸB,S zJ;l'w-_vV:PLxևηtb'aa).öhul姫Vhz:$p ݸ@r0.-;,l#$.8{:4|oh^l Hѭ3! MЋͶ|/\!yav`Ȝz|V`V`V`V`V`V`V`V`V`Vު@Z4d슑|.ᖥ7/ۖC㯟u4(# wF/zL<^1"^!{ ȣ R2 Q֜n!WHF0];k)n6p rOVlWGl-L%uD ƢՂ7\|yq|́/B㐧#c\;2q_ w!q:q dvҥi8qi{xEoo_W`ffffffffff 62d[:وz2[5懁~?\?zц.ɒ ]/?bh=@hzOL>uƇѱ.?:|d:=b-ɳsҍz4}Pl@E|H-/W of\8q(VF\MfmaG/?Lo!I@8Ó>vht>M~A e >e+d,>WVwY>;<:}6s sK0r d@C=:lGzeM}]qc62=.9{4s6!kt|x9˻k|]x\+'9(O*8ro!Wh !dO^~Ų#C^ȣxr&;G3 c*) X;9C 'ȂhݞPyGʿHA^`:X !&L&Fo[=dOq@A-鶲N򕯤0+0+0+0+pWo~~[*WY﷍qŬ}i3Xê0{)9_^~~M}Hc7^/M#,'P dbuGtc27Əz֜,Z I&GGtlF*(U;T('UBx;;q6 $Z,}q+XѝW_ dpBĄgg'r7,|ց_7 x ANn+=<ʢ+ρ̩Wcgffn /{ {^ŏ~8xۿ} ^,Mh④+C,lZB>/[O65[[+VOσkӅxCN_5LtT;mxhkqѹ O<zG>%'K=lҹ!WE=uLN[8#?ʢ__]o@:|l.Q?|wa{7 nl[_ǠgZo|~T>{Fv>?]]_K%kk5[AЃï5H9F7+_WnK[p.iO{~۵YYYYWc?c !rO<% ( Xr/tzM&k҃-c+Wr&_/ ぜd@h<_1سE'sZ{ZX;1ȋ8 ."(Q aaٕg a,c79@l٤L~vƵK&kP¾\G1n).qO6k'+/Csҍ=<]tAL{r =|W|~4<o2~}~\ٍlA߆WዿgWLqx_~>?l#oPlȢuq:f8׏./;-۵;d?~Qω nƵ-:@z#)YT{o%KGvx4[k gQNL v .6>L~ACNj`#t㺲_b1)ߘ@[H+>m<1ꒅɁ̏؏>>S4#䏬 /%>w|w򔧜ItQg|k }J>W d4ÿw$O~)r_n\X؍3W(u_uYqſg>q;# s_9dž,ϭK|˼ͬU`4 %,7Ystz/ @LrK.x0Y~ y50: fLX /xB*?|ӳ/wyق|v|/ozCE+f>dvЉ +n/ -LFy\.b Yz2 L܃l(Cwߗ2oo8>VZLS9>C?̷se_˯y47nzpf}6tCWGz;WJbdz_F=Bo2Mt5+0\q==;;p׳ֻ2ÍˮJyc.__x##ˬ/x/ſx&8;< dʸ y|zwY!Zab}ɗ|Igfff{ DcEa2@%|EzS=07Ӗƿ^ߊGdbU |a6aȖ=@i-du Z+9ǃ֘fGZ~6Ɇ,.08 0b%O:y8tѝPvT/d]2mԧ>YmXg'3+0+0+0+p Ȁc EDb:pJ~E{g6,z[Vrű!I$Aݕ) E K=@A6c /;iR-NNuCa cp(Q U}n~;yT!ޖ2o/sn9pBvn;;dtv_vp .-foo<-{yu _+౺#|ڧ}6Qwֆ>zz$?kR$Yy_x*^˂Z_WW{UIIk1{}??y׫\\p=W[moY6W&4{W'O u'й^ZޗQw5R{9v2z=}9{ ^>yߎF 7w##캟.5{un3n2ky#xU2nY{|k_~>g{q++XvAOÏ 꼫}7|O4ۏI \_ܪS.~#0>v t[%×-^T?c!ׁym`B$b2tqdُ$ˎQ,`7΅ $8b8ʿGf/>p_{e㉷xN&Zl7.ĀaѶXlC<ّї'~68hw!#Fc#_!+.KBcD o1q[G@)/ٻedNɳM>O&o#2cd5 #V#y^7ki㎓!\җޕ3O|:ƜjF -# t[~6=4n蛊li'd;GW<я'F' a?ށB=γu@f|ohP|+m@ \/ۦ>>̭ cBV{0nz_Onvc~I6EN-~s/~c~/ÿ|ˬٓE/zqGtWҒ d簸|˷W __}og{[9vw]4gҳ*p@tćǁIt _^~s2>c9j5-F=ޑ.F,xl =zoB?׊Nl9St=/z,?çl[Cz|dclW"mZv!b&wҝx". p1)oClG~Q@V^09žh(@F|NvaG<g _£pP50t#ÃT \g.;pH1W$?jGy,7/v\C?W0`[}#_vLճɎ^~_0[@tb2z_t]Wl|G.h/ƽXW>x| {5j }1p<'DӢ3`b>qY|>9,^UTi9ʿka|v|ArŲ^c!W:,΋oM٠cClGH{xx2n[덒]-Khxw7F+c {ڏUS{i?_@PEuUf?|zWAܞ秹J:+5Yo?k47t < "0dse@Z ''%]&)nuL\q-_wo-庚1iqi>9t u>'n'kv5_f yy//KwlܨaI ׼wj+(]i}ԷdM{*jY_"4` P$>y以Wcg,ݶIC8ZHVomhĭNr5{213yya/ qlAo\h?]іLӗ{xq;Gƣ?ʳ]dA;y'ɎEW|4_G鋕"ZAmb3 - ۊZ`+}ف6Rǭ<5r[0(rI_IMJDf^*{oYN͌\q|oWg~g쩁FK3JO ~qroRް7AlpQ^oM۔p^=?Fqλj2 [0˰.3W〷nK1$qxQ6XEe=S>R㠘X]bp`#8W{9᪎ooM[z$e|@{UX$كfD 27sX\|}l{a:wF{s`9YYYY@/PWyn0[^)[܊xM7 dUnYzrzJ60ї6:'g`Wrf˟, :?0^@ޑ[B4>y9f?OwM A9\AZ<-G?zP|tvb6;i<t 9t'f04h)fzC,/bE]ϿYK5;ܲXC dѝSFHǟF}źY2Ccxmd|ʧ|,>,1еq7gm$Fpv@jWcu0{ߤ3t?m#Ub785[XtEN07@I[ {~1{WxH40xh#dpW$#lfx;ڋk(wc/e|u" {W]4GG!ؘ>z76ug2>8C |nlۧojp5_4{ yLxێ=g+zay}n{WXCsgm O{*` 7W/@yO`>yK.]!03xaW˦GǓG [`6 u"!/N45_k#'+YyG:'/o_En yFvn][w^,,( ,x-EWEbUr|]h[CEģ ƅ\s'>9hغߺ٠ӑ qg-K38o Ӡd ~LJŏxK㷇oߞ. OݲtAט-xiG>1-hmCl~`pg[W^Uջ:٫Qy.{Vxxz2{ 3kvՐlf!ޚgN>ۧxi05 /}陰U=cnACU%zhng8˕ ߗOj F02,ӯ+?AE>)zԫw{4O}uYtYE;'d} {27sx f;Hv^FオqJ>[ \{|ac ><s;I`dٌ lh9ڦMf  n=zz= M~?^=]1ݢZ- [ ;lZWk΃ :ۅ\ixj C~P4y]~rhrşŝNb;Ił+4c9ZTNOzA[+٘@~/&bғ0}&s=v 0>fhv-nqW2nϯ I<ll:g~z[+$̓FsX7b Jהj:??|F~Xi6d+PDa:;~7MW]w dN[ Bp؜^mt[QqQv}t1޳#v޷7/{n-ˡYw5>xzW] &ލ\? yttH=Yn@~1n j=peω=096f׋E=\ʹ27sxoxfb_JlngS'k>1GC?YFVǏt ,BQ_@7zM}ƍΏ-v) [/O1"+,g7^SLrE뛭oY>>txo5ЉUM]D\hS,]"(i%|ŷy0((:=za])ċ;9b_ AEӳ-X ĎhkN>|`yyr%sБEtv%C [ȟGGZ̽SVuOd\Q[h^W\/#$-܈=#'h ژT .%au}K]Y>^\bğ87@f(]޻Zܐ`{{x  ok$mc?a[E5߱>alw5߫-Z7z Ξ0]җԽ]츲ϕ3#\zh mjnu#? rv`xlÛ7د-oy''W eƁ lF:N=zkMa-KXtzL~K!whח:# gWNr,q1>ٍMGv-&\}[ȕ.>ݸk.X](خrw]K(bG?v|ق^NxN񲥋,EAӷ>qM@6{v/WrеGw~/X}C7)h~h i62L#q2֠܆yC5cKw*{{hr6ֽkS\1WxIﮁ̵<%:k 7;o [}+㔫Rh/46{,|Mkۄ۾H^-ܓ dSlc9 ds_t[J@WE2{`z7{okqvao}$XS7+0+0+0+p07˗Dݺ$L_Xw l[Vӗ/Rvm9Çh֛:40׏f= {W;ƠH.4aޙ/9dc]AS 8r/ adNA̶g - p !F[4}/ %/6,zg!IYdǿ+]r-X'u/EƐӕ0ců8 yfS߆ =Y:xԏ#Q_7@gzJla ߬~G~U 5{KMܲ4tہo5[qw d+MM5(6TsՄM]G2n7d*oƁǺw~n^nE؞;6A~|ƫWmߛ5pEJ@ #yZ!tmi u̕ULqk^\4ٻŐh7X dnjyPk/~UW}n73cLzV`V`V`VW e+eȺR ᑶe|K㷰'[֞΁3u ! ̎.`rP1&yyУ{bٓ;[zqٶrsQ|ՇlW~](r/h$յe׋R dyt8#&[@y ]g+W|q#4H^xX~My!+^^|Knpr #U՝Gy4CRɌBlk{ 2x ޓ^4rM]BfoM^9/o6]\{,hb|6ȱwK)Deoߖگ=x*8qkFh4[NZO < g{X{WxE ]#uO dn11 h[k2Zu2{W鸎s {늛^/ȸrT"}c27sxoS_^{wqxғt!߬h Iۡ J&[vO6 +ٟ{:2W蹖M}}urC!^,hPחqAq>}C OG^Gh- G{g>yg+F}Bzq]k,fbѻ03H/(y e|@6\(6.\WP RhK׀b1}GϏ,x_0ixۍ>?̒ 9)(3A.=Ӱt5Ҩ~O{]y11[pztjrVFT)n@f ޿c^'<`{'pxɊǫn.z]4ٻMpwߚM{b9Wyo]aS/WmV̉gfffzapG_Z]xU{#uyOw<(l%;q!Ez-aЃ|]E|dhov[Dٷx6 }/K>4 G{''CFK;EO-ɲF~Nv9oƁu}~ gcK5xހNOмU'z|~p@d筷޺68>`566-#?#qex{Qܾ_׬ DPk (h-8m_qt_'??>x=>Cκ|֚{2<ي߇[5ԧ^a ܥ Lh83^)#J{xm=,A6!?5za)0@&cGCG lآf{z7z;/9IȓmVˇ߅NrWdُ .ٶ2?_ȕ&]ŤW ،<7۫*./zJķ2ۻomL:v[==;!ÕnUr+ޭHYۿg;(L7^1Z3yo5yWk Ɓ$G= yN;2{WSXxd!^JO{Ә_W ~l7=ޕ=B]4ٟ7}7]v//_Wq+d+Y֠}~6v>r{kOey"[1|L*47R/u`&\ܑ\!EhxlJ %k8>~!W`ϮlG dŃw58aS=8Cd [A]\=qqMbw 2/v`p6-։bK$`|tZDG2~ wlʩpl%o yG|ĕWt:BhMm:߹mG}oYm!EGS2rr}W|Aӹv88E=R4F`g/Fk۳XޭC4(<# ҟ (aU5:]bȜ:0P={a[~ήꯅqڧ r'~YJ;r^tw@?tz{0j/Vz ]!@{*̚/3ާY8Q3-O`{OK6lq`ĕ3n[&YS*ӵ|nΞЇzzZ>MOƓT= {tkvїS^` O2?z@U\-L蟓eøuضy 푢,_~[q-pw dRkqys([+7bOKQ]QQ߰ۻdny>dވr⪖Gl}__{fb=o9YYYYV!\խ0ˡd2+lqJ>钅Gd@Qߩwwh(Ya ^3t&th>mEB0+7XZbArɋǷ[K +y8s!W~/'8InLљ/^-=(^']9ٍ>&[aQd&obvBv&\qm|wlٍXN6d|=d|W4XwNGx'5nY5*#]ھ}h{(h vWѣ=:(2~;ly0 &QEH?70߽ŤNT||m@6[fA`O>o%c7tB>z:z2=;D?=9)=mu0A!֤nO:/aDWΨKoW]sbI+Zh c Y}vyqn4^O`r{Y] ܝRm}no8ffff 1MmJl&;#1Ȁ喥7/ˡGՌxaW^sFGҴ-. }dX59@c1l='64W>rz<,c]Ml[[l[P]"Ec /d$A֙SlM&Efx.h7#4e[yCa}h(ӕ :Xۛ趌Vl@_pkMAjz~FetO[|+7簫 [*Gߗ_s Uv0p2|x`Cf Cܲ ۖC㯟sv4(# wF/zL<^l1ǣ R2 $gg[s\!_awye绐\  dv5d8F_QgN`,X-`K>hqŇّ豨b,1y:r1;5б#|q M+/KqPNvh,!S<و4~+s0]pXxֳuͥ׊M' \nݲ/ҵ3g~g^}x۾ێ%l7> ,{OɶwжUf:ep; ]nֿ?d߾}^޽IԮUrzﮮ?8l17kהs] m+Жϓsu^HBFܘWǁX4IBW4xbni)&vEZ(N\l9;d='^Ӈ ^+"`8A6~#5Hq|ApX%ɰ^dC6zJ['խhŅ-<~\tp0[ x#08  /$%|I@/hqRBKzOIȀݟ>Cŕq"~c&Aĸ@y1g7hIILZmÏ?u>C}%\>A]JbC H=ǖv=48:a?^|kV* 襬뜤@pw(~=4}<|e+ -eNg鷶ډWmuն#J46rPį2me_YJeiwCh)GfVɎČkLO7EX—sq18l|$դE`=C9dӋT/UU,~\_Gg\-n}#cǎɑGG#ߪخqG`+W|@RT:՚|%DI> ի|hYe%v/{p'X\< L#j6|ftxxu M'衱%֭rU;hXCS͡ޱF‹:x㯃8XM$qo[qt> S82IH]?B ?v3PlO"_AMz6gK) 'NOH; dLtUB&1!cS}ڧR}bg8Wf|"68\;Rcm͍@.fM+/Oɥ.uFгmM>}%/yEf7ת}}>ꨣU|}osG`q622I!PndÏz-6̐jcEy6$Pz!g##,]fɡu"Vkb8|Vl😺 vhelI{#ӉLښ~.<ri$cRƎK"h6hx6[J4z 't2ԕd|Wmk#}#ϗ:~H?NТ^-kMG>;^+K˾,~*/ɔ[/ZFn^9&d2c98lOqp88^G`Y#L*J6<\ի|hYe$dv3d$:61G,KuP6ȓ M_AbGUg t>඙@h0\!HCBg7 M% |Z@<#zlO: ]В(!_Gu N쒯kOt%>i 7L':b)$viY'&Y嘐#;#08#08#F`v<JwiF ^˾W1_Y:#M]Bcd¯ex$OD=h6tȢo.;zǚ tmo= ;tSݷבBSKqzm:Y-$^Yɐ|F:Փi{DЉ_ 4N:62W#'2z](ZϨizʢx;Ml)̬uV)NJ|qfftTDCO?%~|ğ8 =5dtZ4B}~ՕdRǿ҆R> |{x/տzD# C 9czpK24:6o@daCN] E^lNhpz٣l\ڬ,?2؋#ɕNd[":u,Y%!D @h)у -cB&8#08#08#F`o'dP¥*ʓu̸-&/yIv%L]|JOY4&"Cd pr6>@H_N誳Cm%~)O. mHk䵅<#u%`=Juz 4bLuc/qzhl+'ۑD7I{2'QzlFF~ҶU0K~tɿ`_$d%|z-WG`qG`qG`+ lڟDɬrH;zUW9kȀ.!cQu[$ŭHu9ZxᣫR<+l^tȫ+DG)Vk#d=2hʴ5bu6FMWO&$:A 0b%dNĬNbB>_'Z!ZxC嘐{e{fE,)Nq+P{WG`qG`q#2vBMԝR%IvˉuŒY<3S:%E .YB^Rc_LrtLב=@xӏm}?AlG=m|xzb+3]Җ"_Ʀ:Үm4:ȴ^Kv,R&1үrr_vJOCVɻ9Xg>8~Lvn/ɭo}fCج=~G=jO}jjmo{=>"#08lւ!enWp W?Z7MLNrL7'<lуo#ʊgAwׇ+KfmbH1$MSu1Lb]ǪǮ:}h5#`m|}Q/'Z;p2xӡ Dԕ@;h.j՘E@&lO#3K4I@gmzt%nMRzڅgJ:pe쒏mHeE;VەvDcOfDVK\$@IDAT䑏|d} ^0y#1Ѕ.4yk_;׼f<ɼu\"iYJx`Ox;6|3??O'e{۾O|[Vo#a7i}3p=ӟM_&':щqy䑓s۲?%[BJWE/z^}{_f;ϯɍntus)︂vaO~;g>'ysS{&o^uG;&dj^b&ZLHܮĢ$=:%<Гx$C@_౛-E&Vv=ԒXIO M=tsx6ٰNi{~#mA 18K4>vh U6蜍FÏ2@ FCZɑa+AIޡSx|dǦxt>K+K2Ic{Fn.sL^nvg>󙆿\jN|W'\h>2n&jo|Nv/~lP|eo~Z׺V{}4xяN%<<r!~-oy[NW# Lgj\*ˈ2{i%IePfvNȸ>NIیg|#͵{L{Y _ M\\#WYkcX$d86d5C|_64%H%.>_l2tnqcf$sJ'FbY4"cCÇ+U'q-{6'U{z:;Lz4N0ZIxT46U>E^=txO_ڑv mld_$E"[Ze'D-i[S?uVl񕥓t=R)kYȠխғQMo: Aӝ|~wĘ[ɓ<=1^DtBkH^q 2&XNmqS:&S:8fW לwݍ.8L?!`Kn=čծv5y  c3`~h;P2rnT&T?9vG`ɱے JoNhI합G?CD{ ̲gGFnLd$}if}2"u$IBjL>^|gfz& #ĆI]m%Wib>Lc;%YhU*#_ :pdc[Y8dK⏼:[_W]1ax5::@:;i4{ vC)Lө5U>t%{O. -; -r gK{:)9o7o*UHBOSNKSvxeiMVKȘ%b=1׽Ϙ9')Sef=$Dn-wWʔL֐OJ ([K$ C]IT^l!@Sɂ,{}Uʢ/vS*;x _m%g yIg F}8]CWVd;R6ɜj;N6plIΐ`-J ~K>)k :٪MrIILlwT7}]m5+rL"e-l?dzo}[瓋^O,Ank at}nvvMzֳN귡_?Ig=3}پ귩3T7As,e>~tRg4ZWg5łƌ^r [Wdh1G?01~޹}ɩO}&?Xնf sk,?|Tlb1&^11%q(!s^w= 8.qKL/2$d~c6if e2\ x{Ώ}-9tSkeu?qfXfm>MN~/RlZs3ײ!9f/c<4/_2ԆYU3ë!kFѳ,s9=Cg8f,eKusǟ&=ري.~=L˘/smfOcl .ǝv=4^֐\_[g{?cX>kS_dB.о5ޮ׬2;ѐ %d6r}R?z/U۵W:nl9R-eCk?}oH/rWイthGJSćD4IeO'ĴЫ5u:6r)FFv(AxG N=e+wZ賝mz)WFƟBO9e$ UޡS|ȱDEJ ]<臮͠ڔhE+NڮxW=x?mɘ$d ~e IBKzzK6)WvI̺ nuƹ[2|-+ K0KǍu^ycpH' Y=S?Xƻƪ)"YNUy{^[|}h[lf=1eX[<ןbKSBp;iXfXԯd{Ue^R!vRfڌv`X33܀򕯜<я^6m?X'gܘD_~ӟ&f%|#3.̲2*`uT:F2V+džcd8N4FX&7|_l-I:DLF ᚐ_gDZܷ윬jBF>5h&d>m\W"t.'NU>x4qr} wkD kn}иak>eAY_~կ~BK_ҭ Q5կ?xh1Sk87s JJ5~roZ/@?P?=Eօh~s7}?gJs\[бf-9ō>q_Ivmu$yvڹs皶o^ַ\cc59yƮ~)2I+眆k,:^rgKOJ+ɓ@ @xJ/ 3d6 A^-j+IJSG'٢DK¦C.YqjNpeA bdu[ZBx=eN*ή,vCV_@#L4:hu2؎:>[t ڐATG6%zءggAN.pRJ =<48A.켸 N? z+C_;&deokBf7(XHլVz7{#`d9թN5uSo"H?!d77*P2dk$՟Ki,xbs^BfXFAX'e +|cHx:ZAuq'YzCXlB@u$1PCAiM"6Fa~N(hЍ>: ;yh# ?u$yx-,{fTA}*r1Y`<VՄ̬E>2zzsh͹/_2d#U$#c?XD?#u1]'ӜC|4,>^2dW+nY 9K$F !7y@&S+_m ӹ3C&:zY`6~'nR%5-9Y|OdeZo t[rLS<7mCӾ&dL,iD\7-OM? ̨IWƼԄ:χug1_ǵ,1jBFҒ_Pg}m$!-Yԛс$Y6狶]z3AwMv1W6iF㪟3sjuz $d!kٞH2\/5??ڹ|1slYm@|9yfv\<{f [^+z;fDJXk8^sK69_95Iks]B?_uDK/ "7IfI\MHtAkً^I7AXVI1^6B@ͫfxczaAO2U7κdz}Wyf?cm^jl{~BMs Bޗ\ؙU3˽-qmJq3RM*WF,P$S@dJMЪLli\tmE/n־j3zVc+cَt-: 04ya:.X3EtXgI&in~CuL"U dqZi;A}تcUo}{Rp/`s#\*AՄ/9~̱: K _B WI)yS$d @b!@d0 nw[Kd& Z]C=9:hƕk 5c3ײt͚!v>YgkYp:@?'qkGybkP$kA`A>k-KÎM' k]ڐRĖx-RA$+dr]\6!# F~"fS׎)׆z Hnd,dןjr:?"I#DYW/cm^joZn:>KmC $˧&%niSOl䪯 n}8]CKHƇ~xǫm`O=>K #gc$x zkHc9!6ȓaD*kg۔"oG?;C;mDfH.v2,veeߋx2!u:J!aOdb=|1s"%( k^9jBtfB_ guY4c99FD}%1cqu}p|.<̵lO$d6zy%`!q5?[vϡ;C hyYxCep'˿L3?&?Ä{}â?8<zSO%$J..S~]ħkk0YD,^K8oL'oU$Um3ןEڪvg# C⚕ 2|U6ͪ啥?vCM h|L|K:It@Ӌ>x4rUc |NmGhʴ N}ML9z)6jvbP<;ATRLȦC# a BltxUtiǶ m]4HC/Sv>d ?#ݱG0)񥌝m:lE}$z+ևf2! Cc$ Y7kn>|Uڼ蚐xՅכD9 $O'0&x4-lE(!s$","Z&Mrf?T2nhJGMTSOA2}|^BƓՌZS"t2mL @ aPDV#^C^,SԼCO.\UǪ͸eJ3$Q2ۄvO * 5\#MϾkaxzK߬R|3jBf^hߡ#3T fH8vi5!#(5θ,1!ׇE퉄F˼+7?ةKš$8@2elI[|_"7qj\?ǰ5?>|u߰ C.+nZZr l6!(k0ϳVYzA?vBIPckD_ BR?~SBu=:̡&fPR&~dQË|NiP<tA.FMBLLcNxJt_&zylTߩG#6Wlv3q~lW2CڋAAfG ?F+Ajkyi\J%{t=RScd"$G„~dJ=vl-2pzlo ?hAR[wo͐qSWf 6c'3d}J&ͺɘu6#fnWM82zJ_vl%d2 Mw)O-a%253o-|,3~$nܽ۞h OeNSݼ.P_!׈WwuZO XWeny>W=5ȭI iy՟% Q}8xjBqӗ,dnǎmmϛmֺ(LxԄL=v6^ X,"=fe1!*!'龑c83oVIe2^tH29v$U̸jdfU7?>vdrtRκoX6/!«UY'f29%ռ,{Uo^b_%dY])AIߺp}@}7VeܞN0,x:f}ZYxt&YĚh)"IjnG>$5#CO|ܡSH[z/^yb?ʴ#ˆ8}om؅jl cw%|4^x"継[|$qlB,11OT?1.VO~+%d3, <6';," ,]2,s ǗvΏ(-JȬrYt/O~61LI!^ȇ֯t06pIˆC-)zptI vĥ9zbWPgpyCC]]Lݧi=<8oWmW][v#MqD&XmHqt$^mgGetc>^Gjt6Pu#6N&x> -QS*v'._%dNDtiS/"S>fswl:fH8q~$qؗ26<3k#^o$!S}?9ꨣ}I3+!3t\VYE`Y3d$>ڗO,^لLnjS l*C,s ^u| IfcYkVBf3ןEڪ:s<ٗ C\0zIXln'H$FK]@zډC6>^m[E?ҞN|-kg$"_"!mxd‹nl% Sdc|!†vdةR>H;*=v828[YCf3d@@)1++h2k}B&dxHcǎvc7V[ɡYIOn"t+&dYly֢u?. 8{kVBFn=kU=Yҗ`TORuVkbP" )Rn%f-{aEW٨Uj|W= hOdW%jɓmɓY 7]bM )3ciꫀ1!p f̪͆!kڼ/19WfꬪYz/| J̵~$* Uu]$ }э5 JBxZJųfe;> :f%dTi{YɉL*LeVR?mNY_e*2q`Y Uo^d_dj"F7Df?U_{/M2fml@ D*bT$'ȧޡ ȓ 2ѥx_SƎU(0;>D फ़$ m vС͏zڃRg;xc,*ilL'[iFA7m}}4pd6wT§.'/|!m ?m)2+l^YJ_ >TVZW 5n=1iFzջΌ*|QB~z.P27g?{3&оJO6[B@! 8)nY`.FFWXͿ`8&بO97ub3|>qSrM|M"^'FW7a 5XϧG8ׅWen4&}n^Ck k 0 58R혐S5vp2 /}Kau!M☐pIdR[:Nd34m32O%v@kYhfYS 2ue ?kh%Z:3vq'D+$If 5:Su0lԡE+R5PP \?Ws?-%IzNfvhB^/tLk|Lu͡ھIoVB~Ū#zBkߝ;W$Йg#F_{/lBx:ïeţV+>$w/{NF*!Ngp|q:z^)ïp'y ?>nS~BSOɢ b73mf+;hzɊ>H[}#(-γ҉AiHv|Smb 61dǏ?<[ړHS:؃WIiIvRxpjjK=>u629Y$FZTzŋT:JC*DŽLI pa֦L`d3 gn`wn,*!gk[Vo8g'z}SPq DW zh0*_ڱcGV3n|NSEIiӫ1S'yp30Wb-#՚3v$piW fp#oP" ӦfkXDV87.)&a&ےQbn $ m`ձZ?>k^'GI>8VDy ԓu=|;&d>WzO vm象=Ķ2Vz6vgO)PqJ:gBHk:1 |'b/\|: nŞ(WOi'-щGlٔdk'4:I6Ha*U6x~O2cBdRprC [_7?OkL2 t#Qov Y&b_%d\R-6v)z0 y.vjBF[ML^J2^V#\ >xlѭ%CI-]#6@FiI`T~qA@:ntRjLʡ];";UY4i Ķ:Z t*+Y{UP-%~Iؑ|ޯA }-@i#&t<ϼq)ƞ}븘+'Vkz5T糞3?]&]E= i2omӘe*H:>9~2EwfjݑGO& 8%-(;vXU/DFUeG,G`kvKnf2t_<)̖DZĝhKSDF-l ]cÏ:\D 5!UMtOT6ߡ BK6|%^ڒ~EFm:tZG ԟVZ5lW`iz_G=]ԕ FyE1f#ld1ވ=G`<fH($u3aW1׶Ytx;q#0+!ϙ1eK=eMV͓1VJ2ƼZ\<In,<|PŴlピc@K;SOIl䢇g-}SO.6bP@CG/@x0 Fy$ 8uҠX Lm8}v Fde/i=|-J4%^;_$}K%~ğ:zG;Oƿ~=Kȴ^YJoЯ#VZU]- =1!v3qz,=~G` -W@Mo*J},G`{vJȔW23Nďb$(<(#/L-I>I rgvhD#x]':+S_uh!vɣ?]C:'֔Ux WрÊSe::"U>8y|yů2~⿏%ѵi0O;񪭮vp`SFU"KȜDt!kq#`AV |B##08},5>E.7#G`;%d^n4g2;IzdVxԆ/ތ:On)b=4ĺU.j>PW>{hʴ9;VNh|d /m /t6@䢛=22FkqR#~~- K>)<8#p\_0X3;vh}/F`;%d$!ӭ!cQރM<+vI2T<^Ȉ+#KW9trh}8:~.[64:'6Ȥd٭:tt"Kr<b\<(.Z:ޡ͖HɀD?Lo+u%*UHz:<6O装yDKZ3muv3dN0dFG`qG`qG`qV햐nDA&HC[|'/0 k$["{_m3tvmJu4}8y~|+H }}Vl'Y6- SqiHiDB$rOC<>Y|~dS-4CdZ%+5щ]uMN?mvRkd]3!G`qG`qG`ˏRR"WC,Z՗ʒwe/FbED3Iq-ZIى>zm<6;lo@-.:,zmMVNgKbl-'1gơUxxmFᥑU0;LGld:RvQOC!FxlŞ_iF@IDAT 4نÃ?x]SlI_H_b}Jzl2cBfG`qG`qG`{vJ* ?tĖP%"J|qffthW{ď1.\I_KކbOl??G_u%Yh񯴡i^|f;Ha.C\ҡ 2 (2xu|SW{^p'= 6g :=b#HrȠN2 #18#08#08#ئ ɌvXQ,^@KL/ hM.5Du0EG&8v졃gKLlicU?<|x5A'û̉g1DLJ[BW?LNyS#08#08#F`;%d5d$IjcY23SH11{v싃c d*-I֐OJ=8yx#. :|%VG쫃@>zt2 xںp<ơc8 MCjjHË:xI>^xhl'a=iя$H"ӉLu.9;L%׾NCN 2/֐9DȱvMh Ohy=oD7MKknmo{)OyJӟɹu m|7y{37 {aqvJ>{}H91R&VI&1mщ\MnccWzmxőg GΟC|}Q/'Z;p2xӡ @|v?\C?;R~'jҎv?4 y |x62C׿u M^:Ѽ=x3[ |SJu,}QlK?䲗lk=yωmcFy{o~1":׼5<׽nr\d*)"asC)}rQGMqk4_2{^Dw%&x򕯜p<#'89Z?яNGONpLr+5`:uw׿5p#؊q#72F” -*<|]EœIzthKx'x<$.wnJ9t[fq, Wۅ^SKbb%=%`;4"ygC_ [w贞WW9>OҙlE6jCU6蜍 ?~RHoK FCZɑIm.zNm^vPtc: k d?/ݛ,Մ gIDf$dܜ l@6&~~M.sL-vkAY8%}Byы^tM־XdWGZ+.~O|pLhW27ٚrk]=]][AƓuٹsf-^P89ߴ7: nL,M%d>L~_M$j3[l򸜐G<[V{z~O6U2y ^0kdujo$d~޹ئ _Y}y1UC[dŜ ᱑X͆{dl |Pb$e/<#F<>`/x#t?SCWgI\F?|)"FZ+ /*4ȫt:X;َ= 2Z+cz|Hdkc7@줔=:m+x'q؊-2tW^YJre&dkW$ht[`] ~N<,w'ݒ1_Y^ka9yZ?ns۬ɰVW ԗƝ>'?ɭ)cg:әi֖JxOOӯi"Y j^y9, eGjr2^3[QR9ڃrB_Azd';^x풗d+~%ڰYשjyrTGXy U6Le˪SWSG?$aē`TP>?-*M 瓉>zl$ :M=1<<xX8]ekDN p]xl+kli_Wg!svQ1 S'98PHGb'fOΐ|?e)c:Oʇd/ePPwh@%[b3G WU ϖ*uhSsx̉Ƣ$bNҥ_V޾Jhހ#8%x~K_:/7l+$UVh옐Yawn }yy yzxcBfl7&d67~5?|=j>u63^ScBf3:#wF`&d|enOJ ([K$ uf: *MefnŸpt "^ڏ_xO<~,/.?b7S[Vr6j0יtbtه:y8}%~lE#5o#m̩(d#˖ Vڢ$ G/¿v X$GtvNuӇŃ22cBƐ :oR~גI~f dE=%^O4֫_ɹ}`Od6mO}'^Iַ5^@'}~wv3'_W~puۢlg;_6.y]f7a{XɸdOcf?Xo* _e?m̫koֳu]{1V[+Qqk^s>g艽~ۆe]DƳ[gϟxk,GE裏g9Yy sgF;?-<_6!þ ]c*0QT˟'.c !>h9yu3fw#IוE[/kwfHO2IP{;ٚk*Pa5بk_ښ} ?X^fw۟mV|s'CL _[jh3 =_9cǎv_1OS&dr}ircxqX2ln +KnĹJ1%K|HRQěbZzNUO̚:d#L; '2><Іh|zhtm)DhJ!Sf@x<P:իITT:$ bO @C~ M _e֡S~*Ѣu-uL!Pir|#%~hjDՁغj!Ν;w6\rҗtG.O֒\ŕE8뇜gݸvaT$tω<]U:.7qS-WB~3 }ﷁ gcmFvMz׻6u}>hIY"_ys3ox׽u'luUc+/1>FL>prx| 2 =DIEq0&5c'_܂<]bkfxZۍsBUǘ@iqcuH8?}cki[ė&>5_M*bhm c`|~cA`0/~Uu☿Mo:}5}u9?Fך;{Y1ԥ gg':թ)A}Wjic__12.y$B=_ xo}[ɾv˚arrU;p_yԣ]+PA{g |1ZW1Z, 8'"g\?p?n}ׯz^9.t UvK.ڷ0+7zE+]hg=yשE e$lC^#1q치8[iuBXD{3NE}͐9$'쪭ˆOvדlQ%aӡM85vc'2vڠf1rh?pz+}GO١S+& '5|6\ԁR?&G _pDP?!;/ V.HUvTZDvLc?Z\ !(nsF۴YP2b Aj< +^* }V)6رc"alWz{ ݋C /v+)O/~񋧋csVYY25H?(iѧk?7h%dd`|kR3ykdfDkdHsy '7 g I0u̕tɋ^i;$c {*Pz %$'d@qyjP=k1;wyG1vJ_ѪzzAUL|ՄL9ߪ܇>̫]?u&e1[ ]"y8 ~$|K>TJׇ;CvCw^mouHgѫ/9'\g?|q:5/!SǍ!(!Sr o丐@_&9:ԧ6햐1^CTS(% #McxbrrcX<^lE#`3H=0<[bLҮ_dІgPh? t *8$(er+50 #MYu2 FVmV[ X١i+Z@?6UXjy݉'?v%~hلSr`1Cū[o|̚IԄ硤 X˫U<5!_ܬ}fSV$(38[t؉-A`pid JtV='  ]zٱfxl)vc#>n [D*W|ҡkFt.  -lO_2&dј͔ _: -+=T}mZqcL AkfR`j_f$S5~ClIYLk|{Q #%*#X=FE틾oSc]g7I@HL:^};zYRݓSa-,z0I/&$_U7\GrC3dIp:&d˂㜬3YB"Մ3f$Mw+^1e(@W$*H:~OzͺBfNJvf1pMW OR*9 척D1$J"# \%\ E%S2&d,!3$}j/!+.!ĪYFٻ`۳cHb '8@T&$%bTt@@ŹKJ-Aq `%`!** 8T Q^>k%{UZ{{sνwkg>w}q pxs ;t\}qg+ K [rtaִQC&ǫVԕ^?bve9Mt7cEd ?OoO;9.z#X'!g']|cMvc+ O􂵈sF%>cL O7>{ Ovtozyz]^dN&oSo=xei3'jfx-A`[!2`o?|yJl ayZ2;5Wgl22 뼎}b8*r@{|nVl+ G!s*v?2u\5V>|vWos*9Y,ixγ>@us#٩l؀sf[(RM7,;KC*=pS%; њ"V4M#o^DْOX?u2o/8>w.+{:j~jJTeNTϔ; v4\nӨ9Z=>Ov̏sY s~^L[w ,nԹuueřE <^2p5dma.͇hVk:40'W=:v yă?ldƮOp~O>twgd8۳*h v.t᠉%!]l{`6/~a>%x.P&/>.uʚ'=x1yc#Nx0}Xp~6rs??]Pe/W}&fS[qSkȐwwh|}뾇_}gp[xe{! j &*nE\Cƹ&ێAM5 ʝ|v1=?'suݵMn9ޅw7>W5dne !s_}2{!BCF}eZkءobp6YԚxat^ԒS ].~A~c~у8~F4v@,`I.ws>'ySt[ s`9_ҋv`5y׸ao\BHȃ.Y LG7ؘ^|>Sq6KؼaCFցܲo gd+ՐpnWQttH6|| ?<Ǻ!c+~O q~[:f]5aԡ̕nG4NK;+N<>@do>擉 #Cd7G7, S^<6?k<QALG {|-iOnYm>gh߲4or 3bwtk#ՐN ;^'S6 6na˓ȹ>/zы'DLٷL^v <9Ea6g]uxyovls2/nkG| |VΆlڽR3&la2c9K7)S vW/>c%f7疿|6[E,9ႲJ -;NlI.ťЙc:4T0M)+?rήyk#~yhn G CfK9x,vȘK9E/yKv(zcِvC(XfGvx>v9AqOf#W_}<pp$;Ր;P\Cy,;$61O5d\nܸqWaMS:w WsW|WB*N 6n)9!3oQih7dӒnlO:z5dsztp,{&vž3>ӹFIz*~yUk؜㹆!sݼ/VCf⋾2p6dyv/4F*jToN쪇ّΏUzN~Oi p@ָo m>t٢眢n 1n>x1ѻ`4Y $9"`.66x$ wSfߜ7`+ KK4u>ƭ~1^,mI o+Ez>GUooɎga0h^ ?lRfs<=;4ZFg̟ Guul.&0oh}ٗ}- o؛\2gx4[c.#;'w c1n) _gk;-׼5[4|l'xȘ߲!vlͳEyY{}ZoųCk"G%6zuƍ,9'ދ8u?/W}&5dnkߡ~_=|7>r`e{!#FC5=ԉ.5-Z&W'wKN>.My1U@x5M]N'6o}l|#woϞ?{@s)O`t GMajMėt2?~J0tf|J:=}h~ϦC M_3} ̓-K[&u!/egQ`gN46lW(\}ۜLaeW@0~oYRQ2[(+O}Z-Gvk̹q~ {{8S1z}>tF/m8>= \ g_h8K$u\wgAIdM\AE0W\UUl$v/y]!X k[aO:U5BmIGzֳFO|?0yy+dz!?Mg`6d΂QȻm~W!c~rO>"mw>Q5M=p!#ˊxu֜<7w[O%ȏxS?4z9owt5h!3ϜFwy;>go4 w Y;k=2{ߣ54444~n(g7vhygGvRhB~pkwAVSO#,u?/W}&C5=ּs ;4}u}_XeE+ ܣ y˒Sx]d)|է٠kxdG7>N%CԱoog`?5v_1`?hqF0~>͇oyun>>.7K@u"M4&K+&im|˟> `6/qs'b]/_x΍ .ŤFohجC}%k?\ՐoéZˆ{hOΝ݆BEu6^2Cqِ,e_qO=Or8Ukr!>Wi54 .۽o`SP^l2XN]wy,1nI|M Mv2߱7e.n7Φɏ :M!;3tks`?ޏWu>/W}&{sw{\Cn]^B'cgh\x|e^kb~~peRͨ^z&sx]V 5%آ]-ԴtYqDACd8#oN6S?;-pLy* gZ0لٞLrB 6ҡ\ t8~Y7} ǯ?y:dc-g`6o>c?af s4d8ǷxrEBЭ LA"ve/O9>W*ڿU|OP)]}}|F1y vۿTqgcum )8/:Ю;2f#N2YeyoYdCkl&˜7vީ:s Wr?۱Ѯ(IOz@u_u;woɋ92v0hPՌs?t [SgHsnnɯKۚ+ 7ώs _=VI^xӞF}y Wrی\͵g>s9A#h7vЩRܟķÊ/;3&hzk~?k;-<>xOx<+OϲZ>yl+>;;>k\2N?;nxcce+:wN\l4tڝ:א1;tr/ĺ\^xֱl[uъ;;썽}-;Ƿ;J_n|npġ@1mk)첸+q#~ļ>OOw;gQc\Tn4/c>z$={uk79{}nfwkչ[&q^7nܸҽ\_ngm}iqۉ{'#{*__й7:av ~?7+w  f_WKԩs;㩃/Iw}{5d]5 `5-~$>ȝoLt(1².~6]l T?|WCWg|`2>؆/ChylOVde@azs`N8]402cEFqмɾn&>fg6OA:כ5ns5~jlYX22pW8Ր+wh9U5xu{&<:avFZcxȽ5ߕG*vh)+o/]Xx2vcjh2316'q vܿ] F^S3O}Vct]'or~T?yhupgCf>鶞L9'ⱥ&+~k0֕F6p|>+~M8J߸@SfBy0Wbυ7~~I5F'k΁?|cnh%|F9bCe >)IOɏL?oWj׏G# ,x0\]==y c]ClWĕ7 8JC\2v56> Kݾ\CaUǁbK7 jIaBiGpv|xͳq#sl#?q~?l䭡> K&]I<0hrb5M̉Gba_̍9*Ň`9_5>?x͏??oЮ2OeI \+!sݵM1(7n\8oWVVVVV t˒lkNx tᄑ!cMBCDm~kP<5.>}4WsָB_çtG^(N*>?bo#wh_|2ɳCȃ bƻO[GQA`.<[ |T\b$c oa~^067Ok/pzb ћ0? ҭ!$ڵCfϕ;jy) `;|ԓ<>5?R񖟕P_Sz\S&ੋp }Pn>ճVc'hWzEM' ~<ԺS/dlSܜD;A6JV|6d Τx! %xB|` ?SCy+~jNqDݺ|Ńol\d5jL9czb@ +> lf5ddk]e6<]a~W2>O#rH <u=je`e`e`e`e`eN2PC}5ILAxO{#_WevCy]Y30vsQWV>=ct.]?`ڊ9}c#~5An/~>4bڜM:͵1 'onoj_9$9C' 5Z(-^ hqJH-0ݩ eL]R@IDAT?{r`G46K>ZgcF<}m%iY22222222222p ̆')SSG L;S=N22.5G Kc2Z\}YÀ-ZIjO_7-nƌZ67bA.`_,|@?j|gnl_L.,P 8IhQGog 3IoO%d8_xceF bmc_LdsonW\ u̖+++++++++++wㆌHMܾ 5Oc6IO=|;j5zs7]j̚,XW򉓩7kzt#wlt?S}3[8?ok͛m9bEh/x 7Mˌ]bXBnq xN.jh.r$kC5.60:6 ^|o>hzd|ٜW`:6ś1ɌA_= ~927~ \'22߻Ib wxƏ|\s}u7KmlׇKmT\N<葁?|lŃɋQ<:ȟZ|WH^\c.^c4x.|541Ys7O4xZzf6T&҉&oC싟l囝l5FǮ\^s6n c̎|$+F͕M/z5 CF& E5d7)?}JM0> ͌.:X jvUS6VjN37k4>H>htW hz.1@aZK1DgkwuFa2$\7-B|.Y|1ȇ tfkq%ɕvx|klӭ9BMn>MώlArt$/@^hnar0mpf&tgKsWCFZ ]5d7ght۽yh@ e:3IO=| ~vUx0V#xɌ' GW>Ȳo V@l]Nskͣt暮Zy4lŬ/6=|@ 7IO:x A&_AKF_"L݂˦KM1 ra_t'ٓw52s̏5kgC%&ݗo \+++++++++++'Qv+i0Slvt'=ン!Ij]zօngFM8hjO2Əf=6Ogm_>=|Igu1blًU.~A>oN7&3}0C3hc4kr &LY6Mmd/dx|7wAΦrÕ_2z.~jKsI\lj^t;CI ҲWS?Swwٟ}nnWg]?'wOOxӟ~A:k}<{ae`e`e`e`e`e`eqCfLfL;֔&Sor5۔6raTn-6d5ί1y F.6>-qÓ^sϜ?l6r114 7\tto%8&f2O$3!ܡqş~)ij|1o^䭧a6h8􋿑n]qx/ oΫyd_;ddU_UO+_ʋz+~}|̾q;.>'~'^ciߺSo_{^|}ܮ_+m Yak+++++++ffCAox .)[Nǣo\xAcL]|Ӎ\xs%GF0'r\N|ѱ8ȝG^:,>=%"ۍ}LGWڙ;L (|udlg%isPT+?>s?s=hW?z~Ou?!|1Nn2]u_oTᣕњۊ2222222x@ ;higq3ɝbx?驇?2vUۭRm}UsjWQ-碟/:.|}B>mkK:|&:]c@3'tCr \b3v;|L&w<>M7>8&>[(9(xŇ_Lqk;Ǜx"k>y+dprX7ƀ\xғz$&|w/`K/]<ϾM{[W_uxwIIw?g]7w10Nn2_ˋ[?܅?\gVKM.gƜj0Sl}'=ǡod.M:088Q7yjc9 ȁ1ϸE6bf ? 4[:A|s|_5mÇC.!=7 ZH~4 ^ ͇7^lg<^IC=0_Yɟ6h >Zl >:9_ wmNyv[cC}e/w|~cg/j}7~W^\#54)VC2222222ˀ/jݦuY3ɝb+8 ?驇?2tv'5h5#^IMΙ!.|<>3~q]š_=gEKO>d'3u-C>?|~o܊ϓyғGw7xwO5r?xk_{~w~wxw:~mmqO?lȏlooi?g?7Ƨ|ʧ\/9?\wNXu{S}=`.ᔎw7nܸxw|]\ϙ\?_Oi{O1{2^;iO{yz7\C?D%)K~_v9o>9ey~u.hna dM5-'S4fWb`,{h:GgwC7ߍi+n8)G9p id4Q~a*//?Baby O)xwyX şS[[n<#f/ſxauCn1(ݢ1QQh|~~q͞Zi3?~;GGGl54">>ӊ>?e/{VC_ӞbQas>t//ܽ^ݦ~\I1wȼ%/|^OO?~ovb9ѨG/=O{}{#?#VOO_#5;>l:k=5[XXXXXXxs΀[|I15cj.:3: ›h!ҜPN 1L(Kn5RӤ&mS󃟟h89]sV#ٹO:{konVs雷m??ٞ L L$MDɢK,ےW7.~qأ-~s(Itq#iW4l܅O/-4Om,=q2pI8? i|~]k{V} _A>(57l!32u_ui[N&f6ej_K֐̋8vluiRh// QJx+Ι|y|zu3G'k}3vſ'.:I 7\h5[Oa+uz5ZXXXXXXx@R0V} 3cq˒[\ͨqoC}&>բAһ9G7|-RS WkAF%>]ɂm?(V[TEЀS'0l-2:Lrڠٕ|]t]v<6W+@v'I7f'|0yAXͥFϴ;Cvl̮|퐑T#7|u2>sa3@_닧>xa:vX`<(C|]|O9ڇ5J+M$/-vxۜq\CN-f\h}7* |@(.~o-м4~mcfp܄N!K~/wJ=O0W7|72/!/x.SH!Na:nry/ۥ%ߞhv)\g.2222222𖒁2떥/8ٝੋ00>ֹv2ٮVkuCՕpvt1҅aS,?YdJg#czsN>ٺMn6xlȌkڐUNX \h/UPt9'7qj0H(:9za|A>4.ۚ /sn~m=&.0}d_rvKL,4ݜ?o5dl&|ɗ|'|'ۋy X9n!hخaaW}Wa8of”e6dcMΛ9`ny3qBvȀS~?Y3g2noo1]==\CԺ{lq?Wx>8y[4M;s;ܸ3[Ӥ  W|;c{\?zl|ڧ}ů`Д3x5v՜gᕁ K [^#5K%Տ29^uܘ եZinz3V4}q@ѮZ^>Ft3.&cbnL.NK@vM 4iG$>] " fm?K,؏1y/F^4>y'] sJ/?مӽf UuYt{v̓m[gȔ#n>L f?`?v8{r?c▛c+/|o ِ؏؋-cgmcۣhoMi?Gf:pup!!1`fөCx=א9tzc|f @s?g:MِhjICyEǯlhX1eVBѹZ}{c7tvW]p96t5i6`eOM=z?[uA7/4a-9x6܁8?boc{]M2e.41$âkBM]4y/ f37̇jzŧ%NY5ƃ"o~t;db B F6|*DS<[i >f@mDn$c )5)Aӡ[ܢlȸu4)4 4H z9ny5qm5U2nqk80 \CܺO:0n"D# TF4)fCvQSwې~ݝ\(;`62222222{!Z_?Ԑy2YgTw5Y`zdo䁧%`.;ƅ1Nze Ś>񋃞v1c7jm&urY~㟄n9 7Gʚ\zE_j 34ta:ցEq4Lا+V=W:h?ߍav:/#s~!ep9DQNir7 `4:N gzs&Wn~*!cJ{-Qop[OKA 5UfCf23ϽvlɆ93Β`5dnU`e`e`e`e`e`e`e`e K ,il;ߴR&Z/n׋ZrT/>o~O^S?zpq'ï>4>YӇ8C=n}Ǻ$Ov7Y~ѷtvArL /;24HvstslɣWZGpOz%8" -͉LYrMɱS.>|<4|7_~[C6Χ6ԘNtM֣ⱁA`=uc-K[&.'<|W~ӞwېNu/{:Ng99ߤ'L8wkx}E;wQ__X[#CSMTCFCsq>>l)>R;dJ=I؞|Q`5dVC+++++++G2=Z}SR͍Xi >:vԌI'[.:?aX:bN=ns|o|rϾq6db9͹/5MVhrt9'KNmx >{t ⿅oN,|Kfqtn >̃<@gʊk_ob9Cfq8$s7)Yvهtkݭ2b_84'^8,_rK'nC8nH9uحx/7Mxؽ31+RS8dmwsFӹ3dzʔ]P} {O`9We!s5^XXXXXXx=ڐnK!k$ةFū9AF@^>dˮz(.^]5ԲN~Oi p@ָo m>tmsN|7<И]pU0up|<䰉x Û6x$ } >)>lFkȿqr6kx|\7 p7n-xSA떥-Q]=|gŇ~臞U+2T$eƺ鋾vlxt7>u뼥ƹ1d7ɳoo^^۱ۈ>>W_q6d8G/`8կ~گڋ}۷E3?3/>3>cyu<אiQH?^5yO?qp>j%MG!1,!))Q>uoZxe`e`e`e`e`e`e&ZCF:TjzTgԴ:MN6רP{}tiv'`&qM|W m.N7̎Ca'Wu<̅m1@*.5 4\q Fp Cg&>y\-GtL::]rw_ />LF bc>a֐yeih-Gck2Cl.x3\S3lȼu\윟bƀ&F;h _F Ƴ / xNC'2;u ~_ŇGL#97qbrkf.2oy&'i.y~z^}|;:Cp+wM>~8je yiݗK}!3>>팛2eyᕁ&hCf޲TVGumNL1>]#;au*ڥe_}c?sjK;c8m#w}?AC|s7ܼaq:7rIvF`қA% ƺe&\ %kq޴I6>E0Iܸ9͓vl䮗tk<Ɔ\|W@bc#˷f:WN[4CkU0oѹLw6d=apxڒDOOb1s; /<.#5. _ 5d5#ŏ`.4'`^wֹ< ۿ6wӐ_7vgp֐~ʢWVVVVVV k unl5ԋVdj`[tkEc~N񫟋;}G'Z :_lx&i0}y>tعom0`s&PIH>Ђ&dçW^^xtM]<63O]͒M]8^<~M/d!'3{vl8u,PHkP\_mb}ISNrb07=S<ȭ3г?;PgIdN02 vf]!v=9ϙ⳴&k΁ss!cz-͇7n} ^\CԺ?Ce /| MHl|ɗ|Ʌ[<-5jm^`•>>hߑ3e2yy;-Kܡ`L}Z]xe`e`e`e`e`e`e-=ZCݲlU;6G28:tdzcL1? v^tյ=c w뢓><ysK.6|II<Z8Y< '?}ɦ7 w{6|Q[sgY@Ћ/;'ߍ˅jGqeG-?l^nx'>?;w_s _Wĭ8nݹؐS}w'x)oQ)sYy[[[];k9?\8D55{-].7w9r71k_}_#e.!~fqo6oHXVVVVVVVV{!FQkm GoW XM ɫO7r) gMcr|Gn%6z~-~6->Zsvⓕ+490,ppsޜ >2N LGsfo4/8}24lokɧ/dCăA oN&El_2[΀i5+g>֛[ ~X~͗S93eXw\XXXXXXXX{!cۗvܿ] F^S3O}Vct]'or~T?yhupħUd#[~[78ۍm|>0Wtw#ɛqܕq̄-L`&&ş o80=2=j l^lfGF7\F5u4rz=`Ow$r4yk(~:dŇO嫀AIx&4 XMq/1sX|:nդ=Y܍xk\1AqW/Cs&}%ϗyOmFlŧ3-x*"8@Έ nvM4%,{N#[^x#YyM_pOŅ7n '27~>>2f,&[rՁuW8Z<\ӣ]5Qz3[c4 &W# <76.g-q~xք7snŦ7_Z:?o>h>nQ ,>]t]I$Ӆ% N>#- zk\la>t'l5@|Ѝv977_t4l7c|{<~퐹se`e`e`e`e`e`e`e`e`e`e:2P7nk}pԖv}ԶjMu*ՙTȃ~gV<Ń٫cŧxۏd55F'>a[CsLK\>g|dNC7 P`vhkA%n+A/ h2L8aˆ]ى&]c45gv+~:Gb\T1^c:ΐY;ddb]dmhf|vE5oPLVs.qU_A:A+Z@s/;Z&:[c7r Y'溱n7@0t7F>(]93$_c.9M$NNX;dnzlg\p9np\|v|f#|Gs i#50>[2Ҳ`e`e`e`e`e`e`e`e`e`e`e2p/5d 3d1U_Xj4V%3No,ͧ _ ˆ1L'X= uh:7kjC|cZY4\O'y>5s(\9~-N/}0At J/G/F6ńg2ȁE~Ӊ[|'dO՘N>1?s'#G׬1 Y>6t_5dz=Zj \?RC-K`{ʒ&s6&Գ.ue;Sh51@W{0~4|__<@ әd{׬߼jM=㨋c#wFjufԢɚ7o=%6ۍ}LGWڙ;L (|udlg%iX22222222222pwG2vy*8]5&q58:ղx.ѧ+ӦdÇظl5;cqߘ@9$7.>%֌?ccWirޔ /}c9{ns!nW|8Hsw+^/R\泑M'ůqc Jϕ,=i^K‚kf^kXv6҄QOlcy+[z=I۔;Lc2u1q5ý0xLJ?*ۇ{axŁz?m@|4=@|trȝ o gɜɟԝ;t/ȏ\*Wj^c{!)KoKSV3U[kr**'O8_\|WqWqtAO=4YL]<~.O^6>7C|tPlct'? v$t-$f?-a|vGs̙tG9Cd5.(_ko+lOz <ض2)Kұ`e`e`e`e`e`e`e`e`e`e`e2)KnYzvUjJ p@TӲ~O;5kc6.z-vn:ɲNj.>lL.~ O{t|>tȝ6_ x㱝ҁqD3&O&@5ȃݴO5*6 G ? O|&F67Ytas S?.4~2ٺMn6xlȌkڐUNX \h/UPt9'7qj0H(:9za|A>4.ۚ /sn~m=&.0}d_rvKL,4ݜ?oݲec]f^jȼ뻾Cgh<]jLzVKVQC&'Vԕ^?b]KXthW2|5Z^=0'\~'ȝ?ٜd{,XII@s7!)Mvc' (isٲK}ǘf#/ o|.@9p3Y{uq{vғ=h2`e`e`e`e`e`e`e`e`e<SP/:ZCƊ[~pCo7K!VGXm1jҰl˞]zs ٪grϜG<>3˜g#w>C}Sdgf&:|يkĄN2۷DٽԐq˒C mujH |QGFxP26bCQk 複_6`|P8iG.93vc.kV>P|n]:?_0(o+>I(ILS0stɥw [(^0;IIc^GÄ}b5/ s_lSlW<2,2[6 R8@IDAT e |Lk]tcԐq˒''i{.ul5v%nM.@_]>855q2<@siXx?}<]>7rogz1m> )x7Acok:Xlͅ|5AcXP5R6KVd1>ͷ96fw'PeWA!$1 A ((" T!Nyz Qp * *Aq@QQq@dT s}}_:guj߫w;|Sqkg./ztؐe;:-pvFCKaSsm؏Ilf̏q?nj̨ԣ5d7n±{F{n0H7LFgG'A~K;{W0# }`onO eGfgs&.ٵ⟒8N6'2Jb/cC컘}ԫxh`/_~[Ck5l~95]'w|A@lظ0ѐy_:xȎ0 f~`<@Wz׏UUUUUUUUUUG*0Fׄnt~G(mӔ1~4? {hmx1鳏+5c&O},1g=|f7|w}o 웗'>Ƕ%n-|yh繏8` U%G [ 7:d\1K/3~PC?XpA8Ϗա|=d Q׹}|шiğ/m\3!)ctrF3q5mxc_64=d'UQjNo> lٵfG?8?G֐>hdU|k. \i?=wkǼ|@s7`)Y d ny :| £l_=^1n<-'g/ K%y9ͺ`gւx]4 `U`U`U`U`U`U`U`U`U:W& \#'a4_mf4d>՘yl"]?/׭!7n C5=g}0>K>:l6̾gy1ŵυx5MO7o\vav|{4(>=?[2_xO@^8lr8( Xb[8~>,~/ ɋGf(Ths'>5Ozdt 8Wc',~t2^,*-XXXXXXXXXfct,zֳS,Cn4f5m̗,wڻk rk?]#;aT2a˾m &r5686Mχy?_./?9m~*& ]ֹEJ\ %kqlm|˟A0Eܼ`S,N8ؐotXL:xlx4dvzҨƂUUUUUUUUUkT4fF I?'=+l1jO< \׭!y6nQnҞ~0QajJEw~h)~>ңm /x&)>^AN6ѳ>:(~?[[??|ۗ/9Qo[o&orx7{3 Sn ُK<1Qug< :'wb|s~@854f>yеs,rC?ᓛҙ}x :3!/'!'y4 r kd%y@Nwy +6yc(>>(>̇o]g+wzt^|_|7/(wOQ8V6/K^t QQ|Co oc|ٛk?7dtOg9 7ǖnrA.+8An6p|>+~#ř7㸑yf-L`.*_%şpaz1ds \T| ?>92zxX_GnhAnْkwG?gΡ.Yz?릾Jɮ ^4~OOO=<ϽTw) <c 8_k*˿|)wv/-!uN****p{Јs3͐' k'cd靮eLש!vov z}c'5]77x3͇Mg0;2eGfِG~0~sLY&o OI|p\1tIJkRrbPů0sQ:{Pbn<6*xazM|6`4d9dI8||?] S'}'ÆyQoͿ>}:^=g{g~g49m^GGwwp{~WVVVniJшD's'elvOX{ʼۻ۵i\/ynWL|P*>?boC~0[SْKgf=ȣ8b;gsD9#*($`! ³~4}rӔxDwO'1?/u˽I6NO >a<>zS_./ g2gŹ?S;'S/o'׉97dͿ7JQOO;|}&%^?Jo1VJ)xɌxk^XXy1N0֌qx˂ 47K=[;iGԐy۷}qSG1ڋ8;yGjA rldp2l`1ـ|^wK6c`2泜?< Q xb4  C3䏂@zنjȨ͘,MGB_e 5åJNx?'?׽uۥx[[o7:湖>Gy3d%M#|B.voVo||F3E:bS~O,sr{o|&:|~nqۿ};Q3&멛ޒ&7z9aֽoTZ{\g.xNpv5_b):/҇jut =4C|UW~]ۭc;si⩛'j}R>[r?NjH".~\G[ VVV^*oVYoo*tZ=eSC%K>{ȸ)c; 3$=}elçw&9_3gV_S> gM٤Sau+`~/T_җn>s>眩M#d~C[bVz _xO'<=P//X/`Me_|Kܜߞᦴ^S ;~_jn//;~/l Agd׍q{z__5n>?5~?4g֠{|'/~F?4Guw?xxw|Y{Y3Sߝ:||95YXXx*Wg n  {_*'}w/q''dZ{]/]ڎ2aOadh{qlfKz| ~7栞@|<=~r,>9(/1O/>ZA|ͳC+[s/&]m\."D[$0^r%dȞ.9 ?d!?^q:7DGŋ&{'=&W\ +ՐUB'c)_+Է$o6Ն̋_|l\l?4[ܣh F;I6__OpLS_Q5 F >l?'!f=7d\u4C_KQI'R;⢆s~Qտz-秈FO5d~ݯu^8ƞ_OOGooӚDnNt/}(=q-zU`U`UXk!W~nFҥKiSܟ~֕~ySCgMOY!+ 5Yc3NfYOG`K,/>76}g-y~|kͶÜ[^kC3D{oCs @ӅG5-$Ӆ N>#- zk\ `r /x>ė9 JaS9&9q_ \?Gq7(#Oѭ4d<^}f?|2پͼFїeG [kkx_kS .[w:KG8b3R%D=CHJ nVO^Md7Df >>l뽪ɣba8& 07d̽f޿l]7k_***T@-}t?J9S21wڐ a*\kWk_<}407m1StLnIA@?|lŃɋQ<},}ȟt68cH^\s.^s4x>AL0珬X97OuR<-=s|*@١%ׂ*6\@鈯 h2L8aˆ]ى&]s4\su?svHV+C/z锛9puBW͝KOsu2+]R~ӶTQ`@f?%866.%sCf%3xD5m[u w/6S1Ckߩ%In@:@m>#?rg?lT7s/17dS{~h΍Ssf#/}˖\v?xz܌ߝ}e9N84{=82:仕Kj j$<ۥ9q>x)KRޣjT>to 7//h?.}Eէ \B~?G7znhW=M~NUUUzjhw:\UG}6ѐqJgNn7o ۵}"iCF3c+?5;S6?e՜goz 9 .Y|17@fS:sty+NUvx&_٦kɟyq7Qk˖ll +|rr F<_-/\ eZ|o6\`SDI' ;ƽ&F=/9м M3ܐ97N817zpby8@pZc4MmP| Fo}sFuw?u16ifiLUd{<7d>;BA9׼5EIᘸ|/[ʧH57NDM.pQ2{t]h4dҽ޻5Bͤe5W{Ԓ\ {Y-;97dzgϗ95`U`U`U\ ko/~x;U_ 9 sZ!#;m\˖SCƩ_0Nȸ ǨaP܆ܛ^/yr|da|jltav ]Fh:2cQ|:xp}co1[17h\O'y>5s( r0c_d-NW [PzTM|nCH:8$gN5J\|h:IS]Nj jZ3o饹!1iAWk5_XX|/4dit/|IiK;j6jkujȸd K5I&g{m{I/ɔAnM؞ 5K{0~;dž?̼dk/)&/ 7^x?{rJ9/r(HQJ %Z" J`΅ ˦KNo"kNw /~I:Ch+_2z?@|iΠ%Y4gWy r yȯ{T'{=^ u']Oܐ! #s^̿DِcíYpk8bl]W4`Qh>%?.> TJ'P}TZ͖rY3Mu/iVcՐ.:[j!ӫY|6?]rS\JԐqY6dU[|SCfF]4mnO= ِ X1ƛ/hо8}d쑋n铎>胶%y:en5d\c0fn!3!;;tS[r ۆPG5H4Z=V O >?{{6 kݞR!+Қ WL7w̭&}2 !!g=ؐLd$pBm2 ЌAo4WYVz|^gjx҃coJ{=NמSļ=8:e cS٦}-OcbN#Eo#=?|s~ܼt)nMtoW^0>8x3c|akܘ3q뒥Q+vvqPl:2I- sQ/O]7gŦ0x<2}... <&~,lG4Ϗ&x"2:a؟٘!v}X7>ݑ=O ݆ ߰x K_ӧ!?4?٣|?nn }o4WCf{֏UUUc|C_~ O%ƾU/=-fKMQ=Nv;xE07afz?sEchO\c{y{W#[z3t/yt90'/3on϶bf ?A6htGlO_5GC-978FώN -$?%͟C!yxp/0aOr+ |8p@o6h >Zl >:9_0?{lX7U:rM7Pu 5d| qbD<϶ѽ6nl] wxwDgCf~쵓N8rґ49<4@ܩ?~Ͽh>6? \5i![q=Hf*?g*2[tI3'<*ڇ-'jtO"2ޯiTMt}o<XC7m>Ҹ76Sw .M?q~W{cs'jl%\?VVV7d:!v_h D“ݟ4dS|xєqK{Y=܄Ԑư[k>33O8_\|8_zhřu-ɋ0;G6G槻ans0;lb"f?-a|;XQ.9?6 h3t+VtAq^3E|y ji>'̆Nym %Co'd\=զS5ḑ*lm 2?Blxw~<&x6%6oF/EgٟʑxO4_n!i{?Ƹ5d7{+^}X.mFys˼4c78V7'!#|˘>c?yK0oO598N5==qȗKwjߋgHDN>D=roE^z⒛ХCn ['>jL)zh]zjleX?VVV7d|Y K>'(W \b׼1?!coo}Ֆ{=dd a'e͘vN?u }{̆=%0Ј/ d`O.pΞ9}M.$/:;9 0=߭]<#~17L<7R:2vy +H P}:gQ1hsWL2]dǗ?}j3&pq4y==Z'jbnhl'd%KWt=ٍ0m;JCZ>Ðӥ n~{ӫ^nxw`N11>~Aj\)mٙe6ןx7zX'ՐOoHv/f]=_2?[%54doN}O|g}\2˭`>Exfo֐߽.z ;FSD[ UX***H憌v'd|Ftb--Q"/|?!tݜk-]U覾^NU䢦L.z;h8!s3m&G7dz5հfK>5?} ؕ=Έ??<ȣL۞M_&/~ =  $ b_IW8Yte[/{t~/dN7_bKy<| hX@'COWK>d넌"\qpMc7}F}۷m`q%Pck(|;$ۼ>wB Qx脀KZ>>o/>w e~z& hN:}ڧ}ƞ/Mfʯ"nhz?x=?g_C\X+~>tsv'9>4\jvc`vuBFYHڴkt< j(%ۧ>s?w3q)[[\d~ >}zG=hoD%'w|E}`Ӗ"kz{r5_XXx+07d|ɣ 2#˰ 8c4d?E~=eem.C𪬹<憌^/S` YU<;td|1}c!7x PJr8;L0yq)?Ydg3|J'lbE7oY<6dk!㫚;X \h/ePt9'/`5/qj0((:9zaU|A>4/ۚ /9c? [>/9Z ~_<2 M.VCfTcV//8o+u.r:h}H]J|#'+S2/4k?!A^{5d4c|JsVw[y2t )547\Eu]lm9gzß!!"l/=drh`n_jO5:`8rInzsh mf4_ rBۗd9g^Lvуbb&yh$(%;chu|ZD~yٲKcN 3㋂'/?~d Sz.dfkq3qXZ**Uf5`_Ƿ|r',%5.ywzd%.(sa㲮Gj8!kx<䫧 ֌N8]کԋw==NTCZ5eN;)>n~6ӏ|\3]ڡC4l `{sM`n_Y`9٥=蟭3./4aޙ-.~:?'.=N؃x㱽]%n bn %K.^ͺţg1nta>_<ɋO!NN r-4 K#CL'a0}],8?ln}?3gݙ};~? GǂUU[sʯs 0tN$MN݀׽{{(8َ>]r>@^=?tˡXM}~`P-''<alv^VrJ%/L<@g ӱt/GÄ}b5/A|7t ^G撥̨ƂUU'|\&!ľ nDɟtnkѝzuk5}/>o~O^S?zpq'o<#KybYF,'NtϞ^7N&/ nLs ůd k:Xlͅ|x9,V+(Z)<|E[磜3X/=syѣÆ,Aׁ_n7_62sVCfTcR8xܵG{’¯:ң$ٓ\ ^VVVVnq-:%~> (cO1)NǸ^t]ǩKh8r%ͪ'9e:{ȼp 7sׄdFa2:<8 c_ٻT}<~vh|s{=L^ /;24Hv6;5yty5Ϯud͠@tE9YV/|:5=g^C{7Xf˩9>K<4 g` W:Z)3=tx뒥ڋ^XXXXXXXx}>;%!ԅS2K F?#ECɘ1/͹(A7(47dj$7c.?}.3g=!c ۟ x1Gg镚1d E'L>}Ptes|;Ͼ_|c[sf7疿W>CsNNhx]˓Nqnhy=@kXsq8ݏ|]35dx9ћ`e0딬p\Iؼ}hN`A G7[rC͓_%k 咼fd0Ͽykn.YZ*********p+!S2"n뉅2 qRƥBh }7~SK+ӕO5d4ʼ5G:o$]l 3gzא7n 4=g}0>K>:l6̾gy1ŵυx5MO7o\vav|~On ӳ<}s2.5J4XAnp Cg.} -Gt\tt1o_ />LF /zl=|G?!L뒥QW)%M/iD??_۩g=Y[3%͘.W߸9p}۩it25sSMnK,}3g=!3_di81ȍb4t ƇSІ},k+Xb~ 7}?AC|.oX\[ dG`t3LPzsPt?X)Pr/LNťWLzMr/}l=^rr˓N rW:5scC.Х7c1יTW@IDATѐѰ9‚UUUUUUUUU]%) ~LX.5Gve.o'*4IF̾r%'CwU2%mܬS]n[w6K<r{FEdFAnd>)5٢gOK?Lퟋ;HN}0(?tκ <`xyNo:Dⷶt`L|.@ 70S$O-llu@:63`ǟu5K7>tB<r2~! QW{ S]i2.ir ;;o͙;O 0߾=25b˔w}F㊗1I>胎OT҈roOSau3t<:!aj;1dp4=tĊ9p>kh|k_ X RngW<|rsX\:1A'}x:<>]46|$p0/ZFAaz-5.?}Ŧ7/`L7{ŇP slNϜY@Ћ/e)J|_v/yK:!3`U`U`U`U`U`U`U`U`UTLqRƨ)1֐!34Ng?{;Ⲣyη0:{͏Kn ͘2p1}_U.YR;tՌi!s+Y=Cfzs=lO~5 `{Z 79=w3ǷlOO?GnJQ~ٖL=~a?M8tVr\|j&'\ 9LoN  }?/2:̓'CO&r&}x!͖:  Gg6' &E[npsX Q׬F225okaХF55Yjh0=cHM}S'Y'S> ư񷟴l( ԌD n1G7O0><<{s}p̘n>鶞|9'M5ȥug;xOwS8fZb77/,iE嫄B?L?? jA'1@F+ M=m9[r͗9%KO .YRק5R:)Rå1p'djС뒧2 5djԔ \Æs3ƻ zߔ:5u7G.qɒC=TCF]ZԞ~ۋt4ڞO ~49g'W覗!^kfCN/C1f95?'eqŠ[')yaPrbPů0sQX{>~K7ؑF/>#5NZY0y~g_~i=&uC_02x] VVVVVVVVVY榌Fܘ as; M2ө׬Iש!ㄌ΍^?0 `_ىG{>sָB_ç=y, 70}8U|~Ą 7(`/>[|C|ly^@Vx|NH gD[,]x֏OnqhT\pa15ƒ<n ؐd5jr:4@y ^ad2`U`U`U`U`U`U`U`U`UW`n̍M15cjȴ| NԔt0;#N n{Π&LwꅞAǾ2]ۤ7}tq5 f[1g|xloO=Stm6[7gN6ALk) J3tp~%B x-|/hT[d0YLs˜<~)6K>Zƌx?mqBf{zʒ,XXXXXXXXXЈ5gjhԀ7tFKNkMl^O\io {J͏,dd5 آ5A͖יYasPO@| >My9tb<;t5|1hBtp<E %'AH/x'3@t$3:dǫQBX#Ct[xlK'LqcrMxՐYY*********T1S5Vܠ 3[pn%덧,?^?{d`hkTcd58f4?&-=.؜x)~!: 7snŦ/x m2sC콿:9;}dւ8o>'O6JM HLV0/8@|(|<>7q1c>t'l5@_>hzd|tl+>+;Md ^?uB0S5Z==띛-5`:nu{07m5StLnIA@?|lŃɋQ<},}ȟt68cH^\s.^s4x>AL0珬X97OuR<-=s|*@١%ׂ*6\@鈯 h2L8aˆ]ى&]s4ټ,슟9;5W1&:_kN=d X*********@ˋlsc&x~״!1yxd)۟jN37h|}Ptŷ 1@aZK1DgkwAna2$PuN_.a'~o/A>( rJgX <-=\E`7~;Xdnzlg^p(d(._c(09mpL~ϖ,XXXXXXXXXx4bXM}ENϯSCM}p65[#'7Oͧ _ ˆ9L'~fekh:WQ|:xpk<cb/6=|@ k$'tNAfL̓BH݂˦K6ͦ\ ra_t'ٓ7#9y>gd5! Ťhȼ?ʳ`U`U`U`U`U`U`U`U`U`U`U*p2.Y2$mG}e'Sh{b2ƧoilzlK6f y4}Ǿ}1A6bWC? S/>]gϔw㴀CsqwAP9ڊa䗌#_t)dkӜ _5ȍ&H2%_4 ~SCfj5>!)LiN{A!'5c=ȍ77^Р}q|>#-y3M/rϜ?lxf'?xC;e@m}IVJL2O$3!An<[~)ij|1//S04_&[}qܼG7U1넌,XXXXXXXXXXX \ӆ ǰ1ȭ_x&O ^zFfX@o΋|W>3fkO ՞> 3ayzb Ë8^xFr4_r9 f|ѱ8rN>Od8hv|^'SA> {']# c?{WKaU6d>rAo8/$!>[(9xŇ_Lqk;χx"+As N_,=c%K`U`U`U`U`U`U`U`U`U`U`U+p29n^4&}056`8w57Ar:w=|r`Nf_g 6mHNll鈏٢ 9%7k7=Zr!op7 ZH~J? /C7^a3^ECV@/z,^q̟m@|4=@|tr 7a~yuS_UX***********pg OYz` IM8ڞIM3C]07C0=..(^?&>]Yۗ'/^~$[L-hG`m Gh42jЭXŁK?{/¦1G0:Ѷ5 YOYRw^֐OYrҋh SG02M{Zv=ks6;L7ɲNj.>lN.~ <;ȍ6_ c;&?BJGƎ g %O&@Gh7ۧCN6 G ?rO|& 7v,:0^vy>)\!YOYX***********p 'dCsžqca2{x5[j԰͖}j~ g?.+g{ds#x+& v3o{n4}yf.)$H28(9H$%]!dmB8*9:0~9rI/MPayr -^4[@W#h+>S~^1.YzKaV5d,sCƥGX= lǍ?Xx{rzǰ(}|fgxth|]"<;{at=t1ˡⷿ&otr;%O rO6y1ErqrvR8 N?ǢlG"䣢 kf.Yޏ9y/ / 7>{tN'pgbܛ1[%K2@ VVVVVVVVVVV׭!c㒥C4l `{sM`n_Y`9٥=蟭3./4aޙ-.~:?'.=N؃x㱽]%n bn %K.^d.z~l0]5=`oSǾ.lE ~䒿sg M}SdW:5d\ U۳m{H |QG x㟄frAyYɥw [(^U0;.L:н(s 0}d ;b/9Tk_|4OxG(`泌>YӇO>|.o>+|Ϸ9$N)$@9_d k:Xlͅ|x9,V+(Z)<|E[磜3X/=syѣÆ,Aׁ_n7_62x] VVVVVVVVVVV׭!3Cc5aCn<74Nؗ/=v`>GFA<ܞz' Cˎ rM]^ͳk?%;qt3(tm-lNd, _b%džNMw1Wl^ Ƨk6rjN'O& =5qa!uҨĂUUUUUUUUUUU5m@=7^s{Ls8}uzfL:dt ӷ#G/f/_γo}ؖc2Ĝ͹/P?-<,9ႲJ@| F,|+fqt9o*|ȃ<@g 9gW:obṇ:!3 `U`U`U`U`U`U`U`U`U`U`Un*pM2t `Y#I{TrA'[vّΏQa~;:=5Yr쯋7-tڏE9E1/<МMA2uJB8.ق$[l`>_uch0נcoWL-~9r!yȒ㵆rI^Nn2߼gy7^,B-XXXXXXXXXXX \޸hinhrds.)Jw=O02>l47>. rU|vh<:Ѡl @~==;.{!\$`Iϋ5o /0t'/P٢A|4@EGK!>5Ozdt 8Wc',~t2\, -XXXXXXXXXXX \ӆ|ɒ}k{h| 7f9? hl5<O%C g"W`\cic|ASna|coaqonb@0AA `[d@ɥ09]r^16g[aQx/O>;2M/_|΍ F@ތŤFGCFfW a[CM}_;%3j&/~&3j< r AM  <{Za:o\GztAvd8Nj?s|&zGŏ{nze\ngW<|rsX\:1A'}x:<>]46|$p0/ZFAaz-5.?}Ŧ7/`L7{ŇP slNϜY@Ћ/e)J|_v/yK9( f:YBs~.n7'E>ҟnAy铡'g} >f3oNzF-79[W̨‚UUUUUUUUUUUukX|ʧ8!6dxxh{Ls9>|ٛk?7dtOg9 7ǖnrA.+8An6p|>+~#ř7㸑yf-L`.*_%şpaz?{iU}پbEXT,Fk5*&ƊTE)J@)%Mп J7w7Rdy;{ٹgbx6tTxcA91]m CFg=ơ6hAV^?x|3|1n]`N젹eiZ޲D;ف@v ;dفw`" dcz̒%Kpn"x΢])l ,ĀgAs%&@~iX?b Ny1~N?t,!sA1ol0rmۜZ=4z`~mЙ<0f`k&,O. X6-X0` ʱ!.UFml#38qrxqFncmgȕAΎ̬yMHdف@v ;d;ف4inY򡾜+#gL2θȱstp! |<1Co>xr7 ;ijsɉ0u8xAV Eo, ?zA+dC< ̩} ʤ`7@lV?qk/=z6Piܐy`AZ=`r×LdWv['6V֎##Z΋̼Bלdف@v ;dvi ΁=8Y,挭^ëj8sX]ɉ` ʈYSA?F/gcVORg~|蠵iicc(6,X&00' s8B޸6XK["Yt,tȴzl#' tBN @;}FЭ]R~eoQO}2ee܆{v ;d&|&@[3dx(8rv2W`†sr9v}35C lc {nc`cAV[>o |l6*[U`gV'6<3DpX"sAX`dn<6D7)?Kmkl8G\<1'c^2̠Žao +ܸBf|] l^j@1LneT?V=._{vAr;݁_dYS\uS9ڏ?ӿZ&ͨwTo8[6 UzwkJKz)_<+~R{edm˴߯L^pqmUl^lÕ%GeUt 4mmS&GӢv`_1&֠`d2wo/ʆ_lYL˽˴65ѷ22|_E?e6|rHQv ;dm CO ϋř! K| 8KBGzav3~џ8Ÿ0p&@~F?7r{e[cmAv3qf m6`cPv&pmc6).c~t,@l舯:xc!#?v942%؛}/T]\)Ǹطτ1>=ڑL>C&2\‡w .caYe-|O|Bڧw':d:sy6e;?Sqn*'GYt۲}T9n8;eMv/L>,36~G4~Ϝ+X36p%LNd BN@mw+[6+ʖV)ݯ^)qyoM?~\󒺫 vxӅ`X%n B/ ˦p@4+ATĪ+e,kRv9meUQݰgfsw56D&k Q=cC E#/ȱѶش M`Cms,&`؛ۼ!Gx[6g]~ԇxNK, X= 2}?'2e|h2};e'ŭW*S2 i{JY-ׯ(ݮ>2u{v1 oKKbس!>w*328҇+?>sKYזIY|E=~sYYoT6zqo{V]`}wK'5}|eRr%QgDv ;dL  up Us$̃֊>zc`L`ؚ\Es`سy vCcSd1'mzqᵑ&6yx!wcc thã7f=4>Ag rbL^Ӛᰭ}{:G2Έ:&k>NS27Vuw/z y͜)ܧڬk?)u6+VV񠞘[U?eY6\qiYyҡ=)Sʢ3Gk^pW熲OƉHޫ#7YwwcX6Lew.>N2 [z].G=z,}c˖⪟y'}oUzھ{ Y4S1L]6]yYa8#qzp1+Ny`dvش2u!rc^Zys1Ї\ՑA|1ܚGG~s ֞t5!Aj!- `Kq3ntX<ԣ#N2b;Bn=Al `v,k(m-oFLx|e]AV>63df3dlXw(n=8L/w2hB^>P$Zk=S28cr`c=zѮn*qΰ՞X1aF&:[;b!fCc;}@Nd<?z A'mRV^ YŠ1p@co + o6.=Bn]ݏ9@z?HžtںC_͇y ]#1~o=#-. |o b?WFv[ͤ w},qEȥ5;<7RV7o olw;ˏ#nYLmwC7}:S˲cֽfÞP~~}ZV3e͇Z@;}L$w}D黵$3|U3۳~}˰o ÉyoDr;y{pq\a2 `^"nv]'{yWSL^p*& ګ8C[_qz<6_}zl]<[w_2} 'KW?k۫.8xLo@f5)+^u8]GC[aũ/ 3Urkd1+[M*GV5y{Bݖ{3@'<;>~CLoۘFl 5gǕASge[Jp(8ۙޮ*qv ;dt Xq8=8 C\ | IȠ+F}ʴCL.փxb|9VXб'Avv3 PqGRM,m€Xذ9d764?v~4dbh! GDG.Ax~@8,x[ymځ̸VqBo8pLl 4ZCxὂeO}FI;ݝʂ7^m^NܚCFSͫ /<*o_w\eA~[v7v 91h1۳u2l C]uhV׆/X1[2<$glY̌xnѪlϻ-/lՏבr9.)χ[ηWxZgBZúgtQ#9J7>h~s+>ڴ?dف@vVہ :Y =4g^s#ÖX9cm ϲX荅 9z`}C}S 18Ûݓē6rxxV@"WͭmoZ,صz*^96ۍhڍİ6&rsס- $F<Ȭ'ȮVb  vX𖥙ki@&1O󂓺gȴ5eY' N*|aC2n3d26 c bہLT6^/| ֕݁LYky~%F d/ 75Wו >]e{u康Pqq{UMڄ40ﭟ6md0ӣW$@IDAT;&´G+dhٱtY y'}h9iz_WGO>X9#eeʽT^_D׾ekd^9xΦۇdm X| ص ?4vX vYi1q>ϐ/.ctkʥwc;lV\pAfہ^}T7w{fE@:me^5222/zX|7c4zבQ40a2͞g}jYl=e9[L2>wҔ03gu?!ioQV]}SqIzĴ]w4v0ܠL Ȗ*k WnyHWa\]yJNaޤxо*yOUMqg>:ް7A醗8>h!oZpWȬ,V1RfX_? vS2wWU_HYɸ'`px#oi~ZT?7ʪӏద3J";dف[m&@4C4gFdpɭNꃬrJl mVm$7@ͳ76Q_C !7!@0q|\Cp[e-`l󀍋 SOjcb=q`c ;_`\t |@fo}=5,}!VgcW*-5gQ0ڡxu31+^,}#޲ݚ^ #W4o7x͕eű5)ŭc(,Ri?>,B}(=,副} ~9=^s\=8>{ Wv)!nҁL܊ nwcH8UiѲxM[ߓq[O{ 8ۨ:*>=n}z\("9xW?<%}G>V*o1pՓy+,;i1X x+cϭt{xBp՗Ώg#Q}xS~@x?Z_y'G}lkv ;dm 90޲-KL 3`ù!X@y3-~+o\von1ڀ 46GO~-COlmAvPnl+ƾ' 664z7! Mbk֞*ll3':@9uCrژ%oL A$ꃟF'<~-lݿ1G-@fze& `wSdڇ7peA `ȴ:کMet u@f$/׾ 2 J5e( _O~qlY Qwu_w_eUϑrGa# ؽjG2-[A<^#ܔ?llw,#w[o_Gܮ#cNBS_|"e'/2ƫGWb*;[]tF|ۊ_-K75_/= eoE˼#Ot+|M_Xm2i²}+9G3! Ͼ[W(sJ*^M?Ag2+^ѝdفpc3c<gY8;l#s`dŖsqG~e|dAfhX[ZG.kizƞ/7? ;(t{@Cb<`u6_O,85YsbG,y!KL'z4ruȠ'/-6.qҌ-K4aܜx,= @0;2!cܽiL8H:xsͭ~23geqrs dںH;'e0ځǤ3)y%SH>wec Ǽ5՜c/1/ֲr]TV.y^'sz| nZ`l!ӾҶ{Kݲtx{?}ކye{^Z64*#w޳;l}ꔲKXC}Nj˰g'4d܈ÁxL׃z=[C΢=3d| >5y&~12yooe:ZW<=~k}뿜0AUO]Ad2b crd#g`-Q+OyY6t;ǜUޮ6eίiZ&ޖ4 ?fBʾ,s#+k͇{78|Qͪ|U<{|O|+b'+²_oي~S϶ޒ˫_nF \q>5Ϭ_'LmP#Sׇw>C(yW%dفi xǸe+ds-gUΘ`G̅ J`zk11Ʌ\菌M/\sn6Aj}˞h64! nL-[ y0Ph舋,Wqc[90'yuB\hX_[s/ hcSn^yȜ;6 Wo|=sS0gc2&~<gT&eێQ ~V_ kL{}0wf94_2}i@v ;dn}H=#o>C̡8c8/9Kr~cR=2ݜ?rKY!kgN`lK-k`]K[,uȡYq215As.? G@[`@ffЄt ^ɼLv;}eݥ-SSdف@v`v` dg], s(6-/D0RigW*:h@{W\?h @L?tЀUu>#жDhKR7BZEzb!0 qggAk x!rlY@뫽5c#!"40<4@~#G>ml=/@ _:ȕke_[7PpC!$8AL}[I 톈ƃ65&zi^lZ[a>: ՙ l 92 L  s2ځ~*sNE[!`<j8+;XB x 4y-jFn xxbluK,.@ +?,'pصImmbxbآ3'vz|k>`|h2Fn* O g bbrb (2 l4!!;dف@v ;d&@fkbn3#΋.CU=P_hϮC/40(L{~6oC;l9m,|[[`t`X{dʃ'66HЂ"{Ӷl ZHdԷ pGbԃmXAC_k {C M9f[[%km1+wi|ڠG__{WD'ف@v ;dفDȰոe^3*g[yΏA ]#xӁF+Ƿ]rY{Ctc'3>) 0*@[ĭ4C 6 wpCW?di`ɡ_2q]}|`oXX;r~SL@{k9/23 ^skv ;dف@v ;L^8>سgqγX3zC1+#gN][=񐁭YPU0 (#KA֦яQ Hb@p` bnl6$ ;D z(bI/mg~b` ,d|б9Dr 걁ǎ,:Й 91}r Cف@v ;dفDpx =8Y΀CXp_Λ!٠6zyby6O{u lc {nc`cAV[>o |l6*[U`gV'6<3DpX"sAX`dn<6D7)?Kmkl8G\<1'c^2̠Žao +ܸBf|]Hdف@v ;d;ׁ6aq LK| &gqΗ 欉pآyr*'4,9 gȏ {yk4z[s!W^Z'61[4k\h{@ nưĵpe mv걷`eG=ڨ72crt"W{sㅪk>e0g'Q;22gDCف@v ;dفDpȈoY:8ȂgP! X8#<d_ "'`m [s~q/nOC[oCޠc~ U=/D`~V  0r8؂Y- =Dgڂi860B)wpdW6ڣ1zC-Ol6'.C&~`lؘ͉0ӶlْW_ف@v ;dفNu`" d%Hgq9sW١o!_ћ|`9b0gkc+ 3Ou-2yhd#?=X9xE^b፣õ?hsC6liC~h#{6yxeC?sfx`Gsw56D&k Q=`C E#/ȱѶش M`Cms,&`؛ۼ!Gx[Ǐ ԣӆ @o*/&F>j0}?'2%!;dف@v ;ܸL 2e @v ;dف@vD hΐ)M)sEg |Уkc tU^ :8#9g_h3?#2k6N[?46' D@˃8Л{+lhmHQ z'Q7= obL\jI.օS4ظ؛?Ȫ/{Ceˇy ]+'tҸve>}66m*/˅^'o:\dف@v ;q;0A2 _YC \x'ظbڷ=L;,͜cں[zYOL  [rbaO t,2o9` n⑹ibik -†ͱG_{Mn wqeug?{\_js+oz;,zЃ~ƍw{oۗ/eٕ Wpҏ*K;}ف@v ;dف-߁6CPߵ$` ` pve]+ :F;ަxt/ ֗@ ־@Ѐ5E}Xm`ǂjz.CGp#Ʊh1``^5&j>^9xΦۇdm X| ص ?4vX vYi1q>ϐ'/,a[m;\Ws5rp@9䓇Є+_Uꤳف@v ;dف[m 90޲-K 3`!X@Y3-~+o8{-Vuu#Z?/[h7ńyطF߁F`@6⑑BYlAEMÃxD(tWN@A13!?j',2 +"G?ZN8jU?ݖGY˳i~ۖլeف@v ;dnLЁ Wg[y0:βqwF&-T 74X6532<Ȍ<ԏ\d =u_dUo~cw(P" &(xm#m,6X69̓?qo 6 Z[1rk ĎX =+CP4O,hAO_[Am\c?,㖥+y]apS dxp=5 6lP>wtAeɒ%sK}v ;dف@v ;p3w` dhG6J{2p04:Aȱ&M3 (ؑLnRy"[hlгe1X:f~1?PkE2>v~7&v--M&&zF=` ( n_׼5{!/>?OW_]ujvl|K.)+WϗYjU53\a3m3@v ;dف@vف4YxqmUܲ2ϋցs3r0gMd.Ρ+уch FFoc Y_K ;*c297S_|kc>eO QSXXȆ&Ֆ`HMf-<h(ztS*7X0?o s›_ ąFEMakl͉,r&q% ::rAbCm-Kэq!գq=1ʏ}cu{'?<`x{R+[hQ?ANktv ;dف@v ;pv`" dOg094gLE0gIϷpTs7gsxs1[hscnZj9X`^dm<# & z (q:nfFQ^FnF[06m%o\d,;6Ѐrc˃OnyxC-KyLtMP_R/_'=~7RozK_ZO:dف@v ;dف?^&@[ BF9pD&ΒClY۸^{5<{>kr{tl}Q-1<Y`kS;k4rcC60p)@xt)Qجcƅw@y0oC %M=Z<~ȔÚ*t/|n df[h 2὿k6eC?Ws\kһ[᪛A *~ы^T9Aف@v ;dف[m 8@6g;; _cOu QcZ_Ii>'5kG92>@fZ޲!<Q?r`,nG>Rm.rQGU^z饅j>qclRdف@v ;r_{ٖ0Sgnd1/2^q`tBG=XlaX/:m21r9\֨\lCf3ϬtxG'G?$dف@v ;rޠtp,Η.gH \ 僬=vF_<0/2きٕaaca2`=p6_Vm< $Mldo=ybKW Ų[Iԃޅ Q ? (1̜4[}[së9#s֢ޚZ[u` AVhԎ ȴC{qǕ޻]\|N޴/nwlڴ<(?p[ҝ|Wezv=,^xa1> hGف@v ;dفMԁ6aۣ=4HΡ =AC\?6>t?_b2w1f* .sÆ 9yNKƵAK U2յr{޳%dف@v ;:0A2-K;9r#svOМOvSA8Vy*Z  9Mk ڃ^0 ?l` 83ˉ$#)vmRh00gZ0GOrx)y(0NsL-r}#gk>snExFϒGO~!gYa}GC}qvњ'?:{OqԮ-9-4ڷVxڣ^=>[kLPK[>`1 hc|Jckm`y|?BBv ;dف@v ;h׿+d$gNtĞ;F | ڥ= { @&9<YAت3{ X X91xe׶_4le-MavioVGlÃIJ |l5`@6AvqCNLW[:Yq t_ _d5;hnY,ю@v ;dف@v ;H=أ^!d239qCe9Ƞ=˷41YМi _= ֏ȬS^:l|c@IDAT#gji|cg `[\6'V=_tZx˂-i ǟ84 rlK접XreЬc 3+p޲Dف@v ;dفNv`" d[|/J8<3.r9 sȂ=Ϥm Л #69\r"c]'93^8Gn~|:ȏ^?dJ0?v:s*íqb2) POKc1`2Az7d^yCV6?e!ƾ։gNmͮ $DTQKrHҝ3VQK- qD:MyC N P@!Ip'{IZj5{W;9>&W˓k!y Z2tOsؘ+k̎٥O6x˳?]S<`s!CGf=]a+C^brٜO6Ֆj+ݠ*R0tpqDd_.#LtiyjH]22VC=[\}2wž燮Vx vMR%(F$“)"ao+8Y?[z@'~6"?r._d-Jؗ{oq˗Oq)O>^jS;Yy-d2!Ӂt`:0LӁtA:p2<_Yzlی ̊ޢŒْq:f87.Љ]\\w؛d?~w"ͷ^[٢+YwCճT}on.} ׅ%Kߋ#g ;fл$]Ef k { G19[\,\`z x1W:^lbueb)ߞ~֬iNE09'Uy/G`XP<`I_^<̖,M&Cwjţ+b ^.Aׅj6\`Aȯ h:.̾<0_Cϯ /-rsj g'^1ҕ29D]6dlCfщt`:0LӁt`: ˌcV4ͼA2fx)?[2|5_A6Au,9rtr/vs"0{ %^-0l!6L"e',A I_.]s*&#NϾ]Ͷ;>×7\=KO|Azl4W,@_CT[>\lBF[Ӂt`:0LӁu&-dwxio7͑x9Y:|zr|y+b.xM>yh6BVQ~6dpfkVsȏ՘fGj-?dCZ`p7(ٗ<9>pvk](|jt<| ͠g-=zc._E\dA?y5eO'9ـj^B|fK0`Wh<UtAOMNrEOG&v yTI6+S\:v8@bK~x>bU" ';dwԖӁt`:0LӁt:p2?{ŇLהyfXr6ʹ͇n_ncųG/4h.^, ;3r;hy4;Ȫ8{h6tYrt<TOqˇt{6> *L1 г/"_=.cqC2K @.\E}惆˾<}pb/#ߥ>#ѕt`:0LӁt`: ˗c5OX o t͕#0z:;4 Hq*Z< C>vaϮC_@X,v,_y.}@~4;@~tz wyaqZyk_ړw\|w]`Gdo~U׼5{ꡧӁt`:0LӁwxnBƯ,=R635[[=34Tr213yyɝo&ÛqlAw;4]d\|ō/s~tPn<<Kv7H_=`w.71_| e+%:@b ai9öfU ..-SW {q|{gP\:ʒv LӁt`:0LӁM[Șׯ,+i΅͔mWZyLo|3qؕac2˟, :?0^@ޝջȃ'/Yy|ϐYpAsאBы<Ѓ⣳Eߥ0[P<Ёґ\`iR V+ٞ@~/&bғ0}A﹪EwYC~|̯?{M1Ot`:0LӁt`:0ܤ#z1c:E,| lGɎ,H]9[tNjImv{htg@\# jBEt×_"\BU.!}Gh"/) ŨiKtDqeK}O&>qMt 슓_8۫W9  [gߓ+K/KFt`:0LӁt`:0ܴk,a1:Ѱ9fsK7W?=w~|ͳ{ z ^G2lć3_ ./<]%:@S=/;Ad8 *2.g +.>t-z[0[X -=`oSgǿ']y ⨥xщ!;c;j*\E>b/_YҒ?]8t`:0LӁtFuEzoBW[ WYy i9A-Y`vt/,3ҁbAڌ&`yqAA~ҋgO[zqVCTms~`P/BU.a A+v]ewvQv}(ūQ]xa:~M^f-=}({ _\2sAŎlCYG+KK} Ս ECLӁt`:0B% Wo\1K&YS/fݶ [7qAq>}K OG|3TKurcOW'}b4/շ-fvXƣ=uPxW .KtyKmP˸r_CHYٖGconjG\j*bdž]<߃?g-dZx 0 ;LӁt`:0Z 2/[fN37%99ن|4cq\Z]T{;|24bWݡxKt:`;D)XaP<<ȟػ=9Y/_2ϝ,N LӁt`:0LӁMR~,l>(dfL<=|zzeL6tl 7Dzscm~,+y;yY @W'9 OƷ%oxz%o\e\R^VH݅[dݡZWnpwK]ߋ+KQӁt`:0LӁtA;p2{륾/_9ң9ias0ޜ'+E.~G;b X]_Ny͹0 ҄)~s2[2AqղنU~~h26Ѡf~t%' 8 $c˧pM_ɋ.^1`` `O_>:GEh> ؛ΖC~>-OȒtlbW?a WVӁt`:0LӁt.d,;ͮdEt8hi>MSЎ9m%:xrC+,?-O-ɋǷpuE=y;dۓk@%풊}lA\vd盜ow&N1{e] /~NݎdCGBf^ Ӂt`:0LӁt>;p2^uo]h`^ysZ<,еAK f23-07?whs0>t:`dylAsnٞ1/D5!ހ.,n>abGnKa_}d(6OZo%c铷tA>z:t`:0LӁt`:0xܴ,뗯cF5xo~١K>{2O.Njl4\jC?ay{YMl-}rhKȝk*N((0̮U={y}f/_NAa14|eGonN<#wY= ?V%u v:˃bU|hqŇّ轩b,<:vd_64}">?<_z˗.5W;_YҎt`:0LӁt`:pI ~xB敯|e sc_-j&7YKx) C7xmMUg|s#fh>Qv_&vIݡЕ l+E*XT^ X_A1)g#x&S?zة`}H~@L_ɫ8ݧwdhB W4a`:0LӁt`:0gnBfR/5Wĉьق̸↬% { f=;,"0mܜ+'9(Ou_9a8ϗXꐟ>?24E}gЕ3mx7Mq(`T%\d_xfOr.Ȃh^^py#忤H! /\̽^c-CLLQd_-OCUst`:0LӁt`:0ܤ̋_9K}[wn78ڌ>ңj̣y3_<N','P dbuwt{2k6zjN|(Fq|IW~>tN? J4-$5 J|0ش.v/_ˉ/Y(W_ dp"CеDj!+ |g250LӁt`:0L7i!+K22^:Y3hɰ=ac̖y3;rvd>\j.tMGW=XlwyzQfs>Tk< W[+wH+9|ɺ"X0?Х!wX0~G}˅O?==çYd鲇-Z뾘}ydy>ʒ. LӁt`:0LӁM[ȸzBƢ)-?Z,/tfM_Ylɮxc]y\L., A;d 4j,=C lr4Z\Nڹ#07K\ O@Iҳds@l٤L~v|ɓ(a_Q:-_2>e<\bzMd嵐wȬ LӁt`:0LӁMZs~euo320+:x 3fKs~̛-OOp6RdbKoqs=l|-@| _=hvtx6Aוc¦|{N:<(SgU_t`:0LӁt`:0ܤRoYu̖Ȟ;f[9̞TYَ#W>c;l@ 'y0[x4|9AN\'%2^{%V\pEe#lػ0À=bO24xx,ʟ xHW+],vِųyBF'Ӂt`:0LӁt.d,3^Yl^4-;)ͧ`lf|&ٰV~0f/?]':_݋r5_|<(yՔ}< gJz7 -Ђ \hVqwo]>7Yz:q-]<-4|@Sb_$,Nq99Փ.Ւ.nbU]_|M,5rW})g../A݋<^ #몎|LyBFWӁt`:0LӁv.d,_^YƗOtx}֟xxqjH/?ۀ̑kϿM]"]/iїz.dgt5p1)oKlw~ +_R^YV-n@ȜlCքt`:0LӁt`:pi \/}BX*Ga|v|ArR/YAyjCy}kO%smwy[^WW_쩇LӁt`:0L :.K8HlmirPĄW%wþy oEVt˳ے[~sy7:Al, }]e|G.=?L_l/cTe8O=Z. ۚU-0[PL_+|:{Lv aSE}Cq2+K10LӁt`:0L7m!c\+KX96SG_aj tM3-=NaWrf`.h6xzyw~G/vCW"d 'Ǧ?CFgA9\C*N G/gCN{|lA`@KGr5=EK1[d|MYqʃ$݉<ȳN̍fnË<|ӗz/P8(  Rp˷U#/tq密Gg&E9ىe+VwšwEl-.ΫWt%MLӁt`:0Lفq_=z+-P`7cx39;1leOW :weK6&bW3 ;嬆J|M=ؠ]A{J(|bǑMDmtbՓl-qEv-x/(i%|+F NO'.>XOxb'_r÷%hzKMW{{|CCܾEFɅWXՍt`:0LӁt`:I ~t22/_njad-3dzn9 R'; yvl;%}~{N:rӝbqݏ, m.yŧk_N~ы;p2_ǼX>DH-\ْv_Ş518{Bv|tx'dpt1q'N18K'{Gt/l?t-TPZBcsnɒla1R\2Ͼ#MtY[.6heG'a﨩\pqyyw~eIKӁt`:0LӁw&-d|eyźu͐ђfG"2s((t9ͨ-@l6ّO:kI^Ǟ-{x;5m5Ku6 O~-Z/h$nU\vaeׇREɋi6l݇0៭\%s;'4H^xX~yg+Fs"=\}{bfWL^l|24bWݡxKt:`;D)XaP<<ȟػ=9Y/_2ϝ,N LӁt`:0LӁ ]Ͷfa)@i&7cGC+-cc.N9{Pluf^{lbz@?:y2՘N.9h~{m˿|ճo.y-,ʶ@r.$~_Ag.bkfylvmw 1Aوb_pݭxd!3OȬF\oxNWnW9Mo:L^_~wuz=x:}ڧ}]ߵφꦆM|7}y< ާzLӁt`: ]1_v,E'Ųd-'/&_~@yɊǐb0kcSdt@<W`."Zik;,ȃ2/v^T\%Iw οXKt!hk(ҟ6_j^ WxdݡZWnpwK]ߋ+KQw?KKO?;_:xy'N}N#?>8S~ñ9 77#7x >{6exwyy=9 oO/ӗ}ٗVLJ|ȇ~O)ɇLӁt`:ҁ[/}"͙͡xL hzs2ܢ@vg{t%&cw}9 |[;oNfK&6(nZ'Ͽ:`G/\5_T |IzN) .Y!5u\ffo{9S,G zaRO݃^\uc}bG?l!5?<K'ğxX}7|z?>? C~9?opy%wDUA*:?=wE^Ao?xzk_{mx"j`:0LӁ;[nBfʒjd:tW fW~|G~&ͩth˿6P?2ǣ+K>_/ſקWgcGqOӿ٧菞̹SCeh;BrO?i_rnLӁt`:NӁR_n?~C03Z&lޅ̦cd& ],|=YEf/6|w{tPd[gn &QMH7 OuŤX ےlWg ﶖ%mda-]|ŧφ?е_Y~5~W}Ձ`V띾2ouN~:{N_^OO?׺=<=裇ޟW qt|}q^YYO^dwO?9^~t;_7y{~|Q;T?k@|'~kkI>_mo++O6e%xꩧ_<l Hՙ)6Ȧz#Cf[mp<_f!p+寜 $Oo|{G~G^}!CO\-Cv[Oxf9MYdx¢"(q?>!mx_X yd_*1|7xJS}j:o-<_ Ojkc!~8iAڅ +=ac)~/G?w;}l2-e{fdwxJ'_++΋45xǿE/޿#@IDAT tٿY>orς˿ˏ,>S>X]WȦӁt`:0ܴ^2c7O9;- ͝dK;ً-&c_Ib><"E~c%?y>-s6b /wPKq]'>Y= ?V%u v:˃bU|hqŇّ轩b,<:vd_64}">?<_z˗.5W;_YҎa_(5p1lO?gCACZTAgg̟9OX=ּsW.O6{]?capƯտ~*Xݿw"K/HFV⧑H'>Z~=2 &u}'~O;ē_zǾDLRh %OX}iusOo>W긇P~7IZgAl˾ ?LӁtA:p2?zZ5,%YrwZ 6ӊ 䏦Ȫ3>̏mv9uG3u4zv(;/[ghPl_  ]", */WW,y / 忔Wl=dv$Iw~x/NM^Smy2k|%Mcvh,cɽ3SOx c`_?{->{Boo9wb!~ԏQyk2iq1?g#?5- g̟y6bu_nq^kW~ {W{{,%1Y?XzOxWPO>]>L>}!#€+E}5m_~ݑʿO,z+^ﵗ1t`:0LӁwܤ^kq٣ YK>ͤ{ vYEa2vU9WNrP*8roTCqٓ/}!?}~dhы<.+gn|Q@J w6n͞,%\꽼PyGIˑC^>{IZg'0(/,ȾZ\ '>WļB/5yRT_Ϲӻ^g!G-Df\ώ{w^K{̏1#d۰S)ikS!S?S+wO`O|e}SK^.}&jYgOOaqx[.dek`iS\%#M{GeMOӁt`:0I s9K}[o78ڌ>ңj̣y3_<N','P dbuwt{2k6zjN|(Fq|IW~>tN? J4-$5 J|0ش.v/_ˉ/Y(W_ dp"CеDj!+ |g2u/eO]_ǃr@~ /:e2K|=J}#-g!ƗO\.dZП" tE= ec{U< *'ld!#W%OW^OnYZˊ=[f.oK)( ˁt`:0LI sR1ϚAK?89l̕ώu>\2ޟa`s!CGf=]9C^brٜO6Ֆj+ݠ*R0tpqDd_.#LtiyjH]22Vz|4אBƻVzR`NW2^V@^ްY Kxc9㉠}]ܟ{a//<}/ ,A%O'U/QY\+K剬s'q 2VERg5r[nտz{{9;߄VΐӁt`:0L):p2CEK1SZ`Y1_̚fqe h&=hْ]Ǻʙ\4Yƃv@hXz/"쓹+h/?>?tŋ-lsG`t/xoB*A1*$L {إg_O:I/6t'kQ¾\ݣxKu[d|~'LyWk!3Y XV}g}m&/~.dZx!?~܏x*_uq. -裏lC/c!.. x_a<=$_׎|=_se>-,O<#/Z˞ޚzwT;m!sYO}ܗ:bOO?xk2^*V{ŋ_}'Mԧ/S.gi,;%ע;#MSE/'dO`t`:0L;p2#waFfEoQalbu8y ZɛGyˎ.`K.em|;|_2|qT;alZSYCw侌DCnB%őGu]"5Ά=]L v-.yMO~0)^ 2ՃfGa#t{]/9Xl,lʷÃ?x=O\5溿/}ۇ?ɻWOO;/} Z5-N6bb_+u`":=O\;rlt.{v5.vLuMozmK/?K_ 'OzU=\ (3xk2bO/" ,_"ĖZ՞}@o=?x:0LӁ;[nB 5--su̖Ȟ;f[9̞TYَ#W>c;l@ 'y0[x4|9AN\'%2^{%V\pEe#lػ0À=bO24xxo9=˟ }1ҕ29D]6Ն!3O\}^~ 2RX$O=>n^ O^ }fʑL,ez1~OD|/DIO<{*Y,K{M&/?'O'=x_ozۗdgU"_KI)B`_mR^g[/;aO\Z&'_,<%dd=̿8/=Mv~/mIyYӁt`:0L:pC2/YǬh6?ydt͔S~0] dbkFl؃`+Y @sٗ./^lqU{ fZ`jaċrK.py=0͞0@_.G9W7=r-7ۖ#rL|̇ ذƷ+"0=}pU\6dіg=e,o|ɋt-0Z2!^#}N%~?!'{;5}kyPS8ޱ,Iro ^pX/>7߰3Ӂt`:0LnznBK}}^uZx[Hٜ,>=9>]r>tXm9>23/YvN `~'H衇-|~̟g]|++ο u^~>MӁt`:0L7i!+K`=oI҃yb{207<ٛ= Lͧ7 #lvY%>-kWX{s1r,|jV'CxP ɫ)x:ADWo3[BB+dр@+|*ot[,x:2[hȫ:ο-HY&g_9ұsr']%]y̐fʾȃ7ȑi]m>tr/. >zl_Asqb1ؙMnE˃OW~xىEVGc" a~zCF_>4䯤_ TtWdMWad] }qy@|qYjr9".S04\\_{yGGU.9o'dBF1ޏ,ZM,dtf@ _O|܌§t`:0LӁt.d,_^Y;z ^!Xçl!=l2G=;36wYvɲ'G_t/Rm]bdD'/-E>|}HarzyȪgZŦ#s+K-=rb˾ˎNqq fK/y5{z y3LӁt`:0<:p2Z^ƅ^%y ` .-N̮NvR&;_0[@tb2|3}W~>@{\k%<LӁt`:0L :IK8HlmirĄW%wþy oEVt˳ے[~sy7:Al, }]e|G.=?L_l/cTe8O=Z. ۚU-0[PL_+|:{Lv aSE}Cq2o_Yz& ׿E-|z/R.2_Ww:0LӁt`:0G!n_"sBӫEtjܷWV3 8E/:yBE_P򵥯گ=|v'd-b:0LӁt`:0xt&-d~ӭwXȼ|3c^͒ͷl̐隻xz4KȂٕE4qff+@;NwF u?0&\[AnA<|9E/q-Zwh/2R;~PDK// ^tїq}0<(hr@ή8puv=ˎ2Atzy |d2\{{|K,d̟t`:0LӁtYԁѺ7,:U!6Glnʖ4/yvAHWcv ĥ;%DS;{AEfۅ;l`e߇nE{?CpKf 9L_~m"rA@/;:1ݐt,x;WEbozӛN__|^X|؇}{̗|ɗ0 U"$ݭ(Ȥ`H"A( K@twD 4W #BNEѨ LFM !$dNwzۧnzK*9{S{=}uTWVVVVVVVVV.K@#KkY~[i8o Y`zdrӇ|AZĞN@'=Ib>񋃞%cݞ> t[mA>xfrAiYɥw ;(>U0;.L9}(s>xg_l )^6x+Gn~҈̃+_ ^LXxU`U`U`U`U`U`U`U`U`UT* d<׺g,}ʩ8ҳ=:lȲ}{8;Yc soG;^+L5QWm 3C橣zV=}C}( љI}icw<2 g}:^vdhdwkj].1^A+h`s"$f9_+96tzϾ9{f3o6>] ̶S{: }/~/͗xh OY_G \^җn/ym0afz d«Wt ^N}t6x^iN0]t~tĜ, w}ӯ_|c[sf7疿W>C4t܇wsR *٣;|NlIťЙtGs!MYV,8 ]ykGPnG2憌oY݋_2 a'n_{7qnlEX?VVVVVVVVVV.U@7(=ex Tѣ5~Ч:ٲfG?8?zWa~;:=3Yrk:lsN|wAnq;zp#uJA8.ق$;lb>_ch07 G7[rC퓳% 咼fd0Ͽ}g;?4@У>24>G>:l̾gy1€M<&V -N7̎Ca''Y>)`t#. J4|XAnpCg.}Y -Gt\tt⳱o />LF }/z<|G?l sǭ|diĺL|ٯj,XXXXXXXXXX%}޵85ȍnbZ GvtT2e_6ȟ\^?6%py9$`қ+@:uHɀK?ar .bқm϶xfۗ_|ve^ r.I-{/_( af5ʹ~ \ \̵>8Jzϵ Nߵ'< r`[tk<=- 럋;HN>u0,Ny>tA㱳l1,ϙ@!\o6aǒI;(_ f7tmOgΏ?oś} o?y:d^2 VVVVVVVVVVVnWm %_{}XzTvhz$Oύ{0|6ИlEW_ s,}r]aq̾v萗[t[G!Gy r d%lx@Nx WlzvU||P|/ {V/?n/_UEˎ>Z}<xmnE/lS@tjO.~Cg[>0I<S~6->Z9;{ɪ s&pp= 09A|0'd.ط$v#fo>~r6ٗ`m0n`?<->锯_>hgk5Uַu7ywXbݪ+۹ ~y{Z;w|+wWݟʊ*******pWm Oݐg,~RjP@4_ {t+}ɳGmn>v|9'M3ȥsg;xOwS8fZbҷ/,E嫄A?L?? jA'5@F+ M=m=[r×r9#K_,) ^ݗۿ}n=s!!OO}'|\e*p |w}?wvv?g̋;m ~կ=ϾVVVVVVVNW* d'm7d2-'׏{hCx]/?|[h=-@hrΎO6<ۇّM/;2K<ΨfCN/C1f93?GA ~䁁AًUBK̉Gaa_Ǝ*xazųx+|}8U|~Ą 7(`/>[|C|l^@Vx|Jp@Έ w 8H7M %=?s˽I6NO >a<>(.H\=22'y4?ۻ?S?Sw{{o7e˾lcƣysv{omm-7~\ vOI?i_Ӯ@r|lǀAx.XXXXXX8UG}ൗ=NP/p;yGj~3[{4 &W5bU`U`U`UrV* d>A[2}s7=7a'^W/SܟykvC=�^휟3~}}NƋ7nO{v?&?|yȻkV _>O@?#?NS~(ߞ-on+!_g&\> ^Xxʗ<˻Ga׽u;Kw_W~~hw~8~[޲[2gEig쟽;^yia^9}]wm:?;t˰ov #?Οq߇١˕=|IB[gs:TQ"-xE~ +>KO'}ħ!?^c_:싓!&W\Qol wwHoܚ]#?#v5moO?^_|⇿~ݾF y򓟼~ "<7>3>B/z V;> {??njf2~O3K/oo?gvuOW|p{q`zQs>svY zO#d>%?17})3{ww}kkv|ah~տW[~o{kky|a̷}۷>>{{k#~=z?ځOiڰ?;~~oGW l,>:,1[[g<|V]*y4]!J2]X|t1ѡ߾ I&7O|鑡m9{`:6ścك?0k œc =2|}'~vb}};Pm55Θn7+bs>?Ez:?a|kʭ%C;C`f/̟iK W_C8k h>>g7u;{@;n&XzK}^[z[ԧ\MA@?|lŃɋQ<>EOo->݀ +?'wr#+|`KO P`vhu t>*P:+H'/={/~!govⳉfbr7OǞy>PD^:fCfݐ9Ņ~7J)σ{y;`bX_5~ ۻ y45n{צGַheДkv}wkB??=f-MgMFPsz I5OyS>[?GgϭU}7~?0SPMnf y>?a݀cs`U`U`U`U`UWt c`7h؁GVO^&k8A> <芯,1@a:K1Dgkw} 70s7 (:/@0t7  93,@I]qJ"NN Mc?@c9|f-tYAl#E˖\?'!Dc|Q{Jf! /n`oX0 0(=s}91VV{M7J0LhP#ߚ=׃nJ7; Քk>꫾z6ٍa뮻N=Z15lYgnib62i0QM;0lS#r* n  5w%̃2swz~DZ5`;k srhxd~?[25Chc:>w c8fce+^/%dS- %>S>eQ(C<q10Wn[ 0? t8"oY>&73'z*,XXXXX8^4p`}X5 H{9^2|lt0;.[>@)r/5]C|{PيY/^lz,.IO:x ׃̘~N/} MţlM1"D-N'o29G|{| kgCAI >A쥕c0gz֝t z{w7_/<8)!f&C5l 40@g;7zA6zc~o`r ÍyhpJ{Vc/K07Ln- fzuXJcS,ig0 aw_~}n͟ :ॵ8k 3?/gt5181 D6M,# ';݄qxg )tV +d,~2AKiφc =;d*p/ؿ1?E`mt#=MǗ{kIшd d$Ay\< b8ԝndh݊F}{ADz@欺η#ʷP5`rA{7ţ63#^q+{CͮfݣS)\!ZM[UG]5s vfA|鬗{w~wnϻIxjpށ\|h8/˷Z`~.t=u]A`pοOsNͭjr3~~k s\tVهsc Wi szJCS{=,Lm&yϯ=[An ` N}%Y+1[L@IDATɑ<̾~~Al?ozhrg ;O1lp~\^ ޜWyd;xx2(c@Ypy25\7Ⴝͷ ފtE2Ϊ?/̷*g&w̏꿭2ޯ#_ uzaN!qƠ ;B>C>ds޷:N3/w >j,XXXX 0c=srWt cԱɆ Ǔ57LAf},7E^>+`w<|> a}zb ó8^x&t7'b0ì/:g7#/NgNq 73Fӣy'XA4}@е26NzdI.m 78٫m djx 1K}v F=ћ+Mq(C/)`s~!81Sc7dm>nԸf/enӷwYE_E۷[@=s%%iiۋ]O5ic+-2awu6xYݽ@ƭ*~|vזXy Ӡ m=oIƭ]# gݬ\E \ x/ٷ ?8{e+:{}'zCĞ=:xyX 9`|/Q^Lv䀿1~g|oS_>}<+sx|#uJ0>rAo}(gp>HCA GE+>OtbP$y?)O.^ r+dprX7t|cYR Yu]۷bv7ģ?'s dW¿2ѝBO׸ [ "0@m}}7-wo:o}7d{z?A Q#h_m i=1.ʷ,y5eɷy ­7n}:~ܣS7;y3}{Xo75_",<4߬2O9=M[@A791|zdI% ܪUƨUy6!~R `{ ]l<=衜NzzL_g/"7ۊ-٠'>d7rH 1zvܞvzh`& )i |ó|`~֏zU|[axŁz3Ab/Fy`C 륾p1ox&v@/D&ҋZ_jj?#>bC]hBa3~w$|~fww^l f;wX̦:z x5ݾ)Ϲ\n̷_Ƿ*54q#gXkg_";s^@f)ҷu>o@ ޅtF<فg~g21 [<\\E*vCqo6[|@o^:륾n:==Z*ۣ7;j5߲tXIC8_R Mr肙'|/.Ue^Ƨ .O>ɲOGgocx㣃bۣ?ݰ`׃9vxs8Ph0y ?Q.9?6-h3t+VtAq0E|y i>'̆N %Co7dn dbCs:xܠg d^ #Ŀ(pàĻ2z/xſï6LJ[d3Uelh5At<"cR_,Cy:&=,5 5Sx؋x}}s cl|\yH@E (9 ɠ1^V[e]#`0gƁAE 4?t/xW`0f7 ^x>y_^m_?3LCG9'3wdp_;^P_mhϝlc}G R_j1c_AOo)h??qowxYxC8Q6Q,=m\XO 1҆ p@ӲğX[0tA頳ۓ;g{t|w>trasξҞq@3W'D!GgٿCtA`@K}|9٧AK>Mk{3}qaxr&vCf}ҨƣŨnh⽣BMC ھ<MϿ_mmaL3ߊ qk{Gyx E|ލw{w7 v?~wni ޟ%lgcve#[fk 93>c{L^fAY_L/N-YLí<`u˛ϝ7O>7z!<5Dqo2̏ Ώ2}%~ܦys]V d|Vޟ梁;<׏UUUUU \t(sU1}E2n=ᄞqca2lrt{<} 7['~]0`Wzd{+x+?~x{淞M_&/~ = H $p ˶U!_D_nŤǗ(Nx@^ѰO/-Om Exk4}Y. '4asr /;C0M܂9q?w韾ON5 됞[+:Yӡgc{>4-WUF' 7=usi|CAw[C#1ߍb픋d_ P4*nn!2? Ɨ[+ ͎{ѭzbCyů&trՅ /h& O=XOYچS`?X$g4/8Eך}ξh)X>@hͱOdW|қOr|&p[eIcٟmޟq، >_h54scu{f'~C gsģ Ă躙O\s]@OdUUUU*pU8UnM}^U a&^Ύ. S@09L^|7OV,:Y r7'[Xlf>dž}6d|Utk~w84] > aKF֯duS;>|Ӿ8l&?< D7[L6l-08h1u>~,2<4\JF.箁̨-M(& h{1׋A {i^|Uma,Mn<go|{ٱa-ǭqANtnuGzyﮦ OxvVGw ĹU Q~YXު8h3/zhlx|<wl N!${^gw] =yhyz֝EtVVVVޱ+pPa㓻J'=Iߍ d1-"5C&'֛ۓJ/^1tV8rInzsh muf4_ rBՇ}Ant/&An`1BΎ I@s{_S|T`"|l%>cO3'/?~d cz.dBך㑥;c113=`x`/\**p0 CC Qq;6@ F ! ;{varvGg}~<兦?lق9<˳~:O*~>Ǐמ,[Qp#(te.,PBͺÛcf3T6'>.̇\`?lȋO%Itf[À/=2>tF|ӷł3͆#_:Pat=s2s*pk*зxcaݬpc-pnM˪;cʼ# c|Wi ^@i#}=o! -! L,AyP26bCQ\ 複_6`|P'~qг}|̱Ƿ^=?tˡXM>?_0oW|PLS0;-+aׇ A&cg3t҅8ec`>]t ~m:/#RuCfTc;Y׻Tf wu_Q4,zU`U`U`U`U`U`U5Q* dYӇOQ>M7c>}fgs /A{ B>7x=,V+(Z)5|E[磜3X/={yѣÆ,Aρ_nٷ52YR2X/q>MKss>sޔw# +p2n(]{S!­zn0P@f&3- ?N [9xdx١ԇ|2y2t d/>Α=\cG׃IWDfYIr%Vrl4}sz6og`m|m/t_^қ/0ȟ=߳>>^ /72_,J,Xx'f./Bj/靼<7za)0@kǴg ^WƤ-LF0}},1g=|f7s|g}ؖc2Ĝ͹/P? 4,9ႲJ@|F,|+fqt=*|ȃ<@g 9gW:obty̺!3 `U`U`U`U`U`U`U`U`U`U`Uf*pE2A-lঊ܀>|ɖ]0;2P\ѻ1?!}<ڗΠ. \өgs3 rc_>x=ћSq$9!`t ;GپxT8 o ',9^g(4&}; ޡ,E둥Q7[6qk/wPCL{KO d{)Jw=O0W 67o\^;0;G=\ >gyxэ@>8lr78( Xa;8~>,~ ɋGf)Ths~ƾEG|Ó>$06ξ 둥Q7Q+:Yw]끣Z)|٠xdG7>O%C[Xk,m_1`?hqm> l!\ 7,._-~НsXL=0&(9(S (&K+&&9l?{> `6/}9gX+pϹ! қt؈ج*‚UUUUUUUUUUUGY6Rqvd=a~]j0M  %آ]-t_\Gztb;`2X)}d=룃cgc ׃Y3*Bll%>vPAn/?< ۞μ>5,߬7&t췯^7dF% D@Q#Kޱz?Ё H>+`l1΋XrC?ҙ}x-:3!/qg÷B J AnIf/_0_AtcO! z_|~ݞ_<,t}}y3|^{@tt l7H|F-7=[笁̨‚UUUUUUUUUUUU8ӟt7dK㯟s; #h==HJo>~)|<řrN> ⱥ&+~gKJv 0Hqʹ8no_Y&a!3Ogk΁?W>;{/Os(&=GYRWi '=i!s77hQ=~Eċo7xzBiGpv|x>̎nzّYuF=u4rz=`OwI6䝡> oW %]$% J^j_fN=? |O_ҍ'7vU{H|@L=4~~:O֗GaUL,R_}e7NzOO n7'}#/ƣW_ys)#&,^A9>~ْKgf=Ƚ b;gSrFTPHA@مghg(/၊ 7;=Ct.|&l+/ Գ:;M`S(~9tʵ= '/'ZO9Wc/yCg|x@d!jӝv?8:}gOaS\/{d-üG?x7dnø߲7i׽n7y{뻤`U`U`U`U`U`U`U`U`U`U`UT dnܐ1xXzJÏ,K{2^\-ZIϾN,NbbA3x {4Xr`_ >싅sVPC{֞o>Ťk3ҍ(x< $E~ +>KO'}ħ!?^c_;a!&W\K׾ۭ+@f| VVVVVVVVVV.U@#K{G!@hT1c3Nlx'{An2># c-`g@ym0Vl-~)~ &>7X!g:g`VɣZIV C .χNMv0<xM ݞo|y?{|e1)׏7l7d܆1y[޲݊Yn\XXXXXXXXXJ饾oռo,>[VOE0>*둁gيx0{},}ȟZ|W1~$c/=L=OВ@.|UtWN4}_z0`_eC.gͮ=?vdgzl vOǞHV+Ce/z൧2̃>{_ b$9c놌,XXXXXXXXXXLÌ'WAȲo@l-M\ʽ൧s(\r0cotx8dB^6^An6ńbڧͷ8N>Ȟ ՞N>9'#G7 Y>y*&@ kr x%nlY?VVVVVVVVVV.Q@#K`|˒!Sà 1f C'>iNh|zO2ƏfO|:3/`o5ˋs}1b rlĪWC? S/>]'܃)J %Z" J`΅ ˦KNo"kOw ~I:Ceo+_2z?@|uӥ\?O{6| 7 gwn~_1 ^$ȨɂUUUUUUUUUU]nOq2_{}ϨROcJzX|:g?Q.gBRh>xY|; D fuE,?ȍG^:,>=6An>fGFxL$+&6vFvt `S~dɷ)xm̓1c5ÂUUUUUUUUUUUTސ-Kn[t |YE+M}-O}1Dk7}=?N}}9$/>k?NwÔoN&Y.hIw:J`>*2^|S܆"!x}Ha|rqg\&7|g[n#K129ʬ,XXXXXXXXXXLj/}he`'zW+[z3Oz(y)ѳw. db Ço>}0lg?LJ?*LJ?ȭ0_Y?۠hzh|n r<0! FfF:鞅}w~ӏ\E \ \сoYg,gī64i{f> f>pVqqtAg=4Y̺x__xm }|tPl{tg~z<0v.bJ&W>0gǀE-pn*.(_/~A6'= !d[=$/k " VVVVVVVVVV.[@F8e#KO>SG052'lgmƢWb`,{h:=ߝ]h+n}œK5 d2,{Ti2`U`U`U`U`U`U`U`U`U`UUdܐ{, =vdzxQ- M fKWDϾYlo/?xAt΄nFӗAnɋlDoG䯤+,²|U|=:?CEG/An1e>>W4,Cnŋf ~ŧs#K c҃>1a|ݡ|ߢk>g_C\X+~>t{v'9>w4\v9d ^+A!>PfCf+**********p*p2wyVȒ2OoƇx-}Ύ. S@09L^|7OV,:Y r7'[Xlf>dž}6d|Utk~w84] > aKF NN/>X7E<_pP|G1ö!hrKN@zcdlC#B˅dr~cC=ݎ96tyGaUh>·f֗d9g_Lvуbb&yh$(%;ch:D~ٲK}ǞfO^~@:]8'1 5;߳#Kw@g qt}b/YA|t ^G摥 ׿׼5#8w?&kwl%\?VVVVVVVVVV.Q]]G7d.zGWCCvXyȢ PnC/> 5}2A\ʃ 2dO.?qG} 79\~|Wo{QSH΁sҋ?X7tp؆ c+_q% ]5@F \ ȼ-ow~~(rlؙ9<̟Y2Orp۾!>Yh|$ȏ4ӻV}>~vh|{=!L^ /;24Hv;5ytyϮsdl@tCw9YV/|: =g^C{7Xf˩=>K<4 gb 獁| _ )h@9ӳ̏>/yK&y?k̾XXXXXXXXXX$؏k_M2s[.g] b3Ɵ2}u^X P b>:n4I'[.:?aX:bzAn|;Ͼ/>-db9Gs_+~:i;9)Xsen >ȍN'Y$W {9 Ty,+?rή5#~u(|l̛杯 ]Dk * VVVVVVVVVV.S>>nȩዡmݶ{ijoho>. r{t*>;4hP|r=g >gyxэ@p npߒ4|XAnp 3M7[xbT0=0Mox">y;>>D:/bv{>a6ڷ#K~woxF*'plB%:^UY**********p*K}=ѥ@uPNC٦|Hg̏,; ,($ r@OC7Ȏn|XJo++7XG9W X|Zrχ}?_./?9m,&]]֩CJ\ %plm|˟E0)x/O>2M/_~΍ V@ތŤF@mRW7mX*;B8K/E7$yx~_>3 ouG{ Ewa2 7Y 2@4`w~hp)~sqgщC+`qM%~gKw)\fyt`~ =LrA6YC,o{:>v~Yװ$.>^<~Cd!'߾Է F @J,**********Py =nk^]C'ų5"7ҙn{ңug<^HGC. yxn؃o糁g;/ZPc ]슇OnKg5@ܒǧ?ĝ : 9<*̳`Pc^'+ar2{%}蟾bӛ0||(~?|͏=}rgO,~w{~Эr-J|_vx3n3yы^'3=+e/۽YzϬxzdiWVVVVVVVVV.C2# e1Ǘ b;l~<;zjia#y 7=cNzzzzٴ'Ƿ/=r}+&ړ?|ٖL=~a?M8tVr^|j&' \9LoN ٧-H>dtOM:X/L>A-?t@<ď.l`OA:뗠5n?ȼ/=596tl:dx5Q22ީ )c(36G71Oݐg,~RjP@4_ {t+}ɳG?dvOg9 7ǖnrA.+8An6p|>+~#ř7㸕}f`.*_%şpazds \T| r?>92z{ɩ0Ȕ 3"g),$Jzzg1l;ۑ /_{ĉьق̸↬% { f=;,"0mܜ+'9(Ou_9a8ϗXꐟ>?24E.+gn(`T%\d_xfOr.Ȃh/T^pa9u‹|/̽^c-g'0(/,ȾZ^ |'d3ds:0LӁt`:0xB7.Ox]2ɯb]ӻc-dTk< W[g`WӅW%B%suE`2~KSCX0~G}˅O?==çYd鲇-Z뾘}yd'd?^)䗗Ӂt`:0LӁtӁ}!|,@)]]]t2_(֏l}EX~`Y1_̚fqe h&=hْ]zxΙ\4Yƃv@hXz/"쓹+h/Z\Ny @IDAT u9#i3Jr)5VE%Ґ Ejܛ\5DU.E)" fbR*mT516A$}{}O쳞k={yݥᎋȟ,k,ϖOo^r ߖޟ[prxAG?^:鳉+xB=t@/]h~#/doAvNviO:'Z~oͿ 0G|chl2C=ۙȌwؽؽ/ݽkHdnc|;46mG@=yu\_W,kM $6?^69+d䡶c=<8;飣<8]8^@<@._X9M8@o57xvxӷ񽵷.]Lׄ9?dm& N<#M  r+\,~]OK_KFdOn?ـ@JX>W_K>?6tPsh |Q?{xѿ~v_H! 91=5m +@/ p<&L>?~>lN48p哞 k`d鱗x}³E.dxV81\r 2df33333333333WES䬭ɦHQ}((fk#d Zco'mʕGƘ.&µ5dg Z}2i%A&@ %N#oAW|j`'6?َ:H>lcv^F?t:xXЋ|}R/cDGx6◒I/|z)x oτ7}7g@>58ew ^: nx-xhlWаO`;W Ifa.9;l)KǹOƵ+NwU~ {d]Tt Q0#0#0#0#0#0#0#0#0#0#p5EYx; 0xSm=[BRBeSC){LiA׾L9^77'+m,Ayql6ɑ//'Zc8~8; Bt?%nrp| q7|ҁkK> ۑE/ᡍj.?4 ??qW8Ym'm l~\E cHYR@mɎ2GBPQ|yE%g=t-xWx(Wᢼ[BwS~ZDKV,q75+'Ggdtq #"cr6+ ??dr = l [892l5bʔ]!x|tnsGt>S#K)CE^]; 22aF`F`F`F`F`F`F`F`F`Fj9-G,nms*藃óQ.f#-26t~?ײdؐNx>`/|%,Ȕ7'][—Qmi÷|&lj$2&ldk O~+$; hk'բxt?VWl%fKƿ,u_GS^WE88ԞDC͂1#0#0#0#0#0#0#0#0#0#pEdny+˦#Iƅ 'rW[Gt'~)vmcxb4rxx|X#8]mcK8Hm8C/-ݛCnι@oc>O:98Ч44h Ňlֶc՟(]^T=>]e\6m@n:p?o?<>[ƫm[e;dp%^[_lmlG<YYCj++k M>WK|GpoW˸8fffffffDiAw'rFrkE g`^6ݖhr\8:Ydk/xh_^?٭]Aɮ-gA 41 L(y8}-~]H+XsF{tU![,ȏ6WL8V贍6ɑgl/^9dl,ȼ/_WIŗ-bJd^?&N/*^_{}Vlo;;woݵ^{9um2@_{?=8Eԟɟ\ .N{yb?Xi'~' 'xsN0#gL^ٷg|/ɥ\i{ݹ{#s ; C v\<ۛ[AF#K\\>z"6oivzrtlZhhZ/}p2;G}xl7?x]׏֒ՏFw O4x<~FdAɱYboZ {Zó~tcMlƗEK>XMAf]!s%ei,U2#m~Lx-+Uwy1=ȏxz'' e~~a>h=yy'_K/A;99B~^T:sR?uz׻՜]։I[[yu8g_~tn|+__|;Vw9zrH>|}DZ|}- w3?3Wdv'ct׾D|x}4~Ŗ&l֪KVkm7-טe-?pz#9.^o-熓7nktՍlPK  AN@נ 6^x[*}xvo Il-zc\'9S\pZ?;68z<48tY.yzz%W(8Y+\+#-pAOMggܽ˻*;?n9{9{{v˯7@x{W~v_5_n=c_޽ۼJ{U87W~W~yk욃#×}ٗ9^Ind6A-~󁣏KQo;-5ƕ^!_}Ac,̌89qR+d\苾hoZ/[{c{GVU#0&韾.Ul:{;<1ݲ|}Yz֊_"v87u[th~d/U~dт'6p :o͂Q 2<9<_2ϋvrodOA#GyزVʛѵrM6y_Y)dZd/~|Î͖U:٤k]8]m>7u*&.UGp谓- 2&` xkZ)bg;:lS??t+ '= xQn>еF:nD [)#K"ˡGů= 2|/O~2t$dT??8C;;2>p,/-V 2yeIsܕfof7~7/y{J{۷}۵/1~*{7"mi,ݿ.!roo,_~;;6s>={7}ӝoo^^oo;'u)B{{`Fp61z7ɍ:pc/5[₹oG^?~7~cZ/;8ۛ{$yc;??ٹ}{%࣌뻾*Cl=qo/| ^ 9>?*}kk>#K۷kYg?ٻ׎x˖!;~xo?G]S܀GdݱwofS8Rzx4/|N;fOz\j2ǸG:vX kcٟoo^k\K}*7cfYYH߾/vy1չs^y=o9#Nj/;u=ݶrNK_o3Kߝ(l؀&oRswwm?G읳\}c~{nmlϲ]3OsZ 81?ZqrqcoCTdþ>pNv}|z¹ƹ5ɱQl>KlɡѓgM.K]chlrp-:֜lv>0&\|[>8I/|AW_Z>B2"~#Ai9z;IdW;N6]:d·v1 )ptNH\vҫM >m7'.,ͅ_{F#=I{!=+B}ɗ|z?\O|W weZ@[&~X. Ւ}۷}J&Ln$ͬA n[[w??Ew7|7ydA O(|}'|zsh)J£}r?n弣K|Mfxz?H"y{4G7aʍ9)na<1p %{7ѿqMJF_%QǾY{ZAf#*Cpؔ@%'i~)կ;k,o'dβ]C!!kA~wA=nE4|U{cϟ{Ե,h>3nybЎ!{y+Ȉr\yu&)>k@+7Wti:`K^rcӽ㲍6;8i Z‰禂-=ZGKwvg|d\~h҅loOV40%N7ǝGOg%H%eXfl?6Uʴo˂"v%{0-y<_,R_/U!0HK${/~dG^邌_ ǯK_ H߯MСn/ :bǂf҅*Iƫ0/n^~-– w1Qr/vN{Ÿ5 >?V5xs/R\ͽX\$a]@’nȚ@/d璖bsųh@M5`U= (b䦿G7٫odCƼ5N_UԎ`,Ih_7~KYx#|Z²]؜O雾z4D+?A?tؿVec,yũBX{ӕpm^WeSƺAl=aJ Vh\NĿ ν -6l=qi-99V)&).ۓ1!C> ۊ{A?t& cNkkE\п9i}/Ć]$rJ r`>8ƝU2~؞d\'$mn[X߭۞O*wsnO/*x?duX {~ 2fYأ`蜣׹}R;s+]c!8D =>8ewcϝgΙ1'Ӯ{kcEݓuokYu@ǡ{FYQǧ{bp}*|>SAy=G.ᕳ/m6"^ C;p:|ЃQ $]: _MG=d}'n3r젓m ZedX^B2b2ׂz\rZ%N^2YZ{Tcw}wdTjEq?68Ǘ桂HSStu< Yb!~'oYm|l&=0;$P\zEe dskW_@Z_@VHYн,^Qxc}zhhLYH+-9}"G^ zcK6=.[&= 2ca/e,-THv`Wp 7 2ϚrqŅ__jfHZ,!l)Hq8.9LU(8 0&~%1ї8+:7gMM7n+F<2&׍ 3ƛkf70w7_8xxxU?L. 3&R7^=Za, hF W^ xShy8- Yoi|E6cP9:ƍ#^O9޻riه_zߝlՎ֎n-h0d*xfs][ϗ +?T!cv/7h?c>fŷcbX$]WyӞ/(0n l8AηB:p:&]c`,2x.%t19?ُsܵ5v]P{7C(XePHidOkۿ}wd?yC5pBG/up,Ƣ=t#~~^sVgV+z ?4yq9d{Cr !ъǸ?8p 2 ˽!<|ma.$i .5Wtm '^r`\GAqG#]_NɗkvmS2ǂX>ܼO{NKdnyx=4<Ʊ\NAfc5V\APtq,yAkJV"$RBZR>Tig)c 2Guxpwa'q 6<vyg{~; xorkdƕQ:֬8uM=^ ǜ;85|po۞ƹw{1]!kK+z܎[_{fox]_4ҭ]orƮw߭UQsCeۋrIxnGn!nɋ7v;#(d宐9Tq;vtp8ho*d~"6[gIJdƸ\z2A&רVspsU6wiFokġstNO[g9h7xj?rm8qɡd J?ʸ;>y0WS{N 2V\l˶' VQ*N+' 2KO Ѳ͎UH. td< 7VA~]瞌8p}~^~2 D :hF}o7A|:x⣱WcߘFxZM?m?ѿYu2wFwïւ̸4%{[@77~%/Mڭ]lQqC#KgY!c1Fu[/: ܨ ']3%%g9;~|1V\s{cW+Y! ,^y7-fK}Ɏx ow+dcLx\jX'< #dj*e܊=EEYp_s9m %}8¡NUʙ;-S1"]z;~WOx:9ǍXaع2>9b8Ti?_<;;jqUUhqc:w㑥qɻ,9u\kzwt/I9ۿIPAf~9{!옜{#Kbq~9{Yjd~ Hw]@t8>vK 2zۈzjV?걛Z'sݸ܂Lƺm-Ξ2>(;U qsW=o1񥾏XPy<&˃d}t '$?ml:.>Dӧ_Nhd 71N[G#C/oŊLv[|u(] 87dP;fd8z6tf/Z1d` :|?mC񤝔îqm賩?< 2wY/˸עHZ 2/dc 7.A˛cdϠ6,Ȍ7}nE\nA r+&E̍XhIv|O)⍿UM\ܜtsf_ X$Mez#/Ax q(/n$M,Ջ?1bw ŕ(=&=zgٟc2!ލ 2xcPw+ c;~}+_.ɶOzod%܊Oד`Lj};fkhԥ;[ےuu)ĮRb dz _酸轈vaK$1{ڞìbg>:mY 2ql KVuػjC1w,E|,(_?z";m@'9T&Y+ndβO:g[{8>IWqy>R]Pήι,eZvepg8A!klq~{.acϝg '3?c!49lŪ[fof//>8m,ۂsksIuiǸ( u\svG9џJ/:1z[ e+;A| .d6ӁG66&_~}!=|ʕ+,B|hil kѳdn67lk_؁7]|iF}!]4I .krܨnt˞hzA8TlI'GO6{[>ɠPxXt`'x cl3_>$O^bbA(ȌŞ !҂ *{Z0Agê7|C0>nGbL2Z.9; ƥ'u -[YǒdLlEy+?o\uHJdIbItc:ȴ#m{ZAf|lg7 2g|{ߍ#|;I$@B8&[V/^/>zj\n>$V-Q> s$wkX\J[rq8+d­/džUp6o ^}4ƶ9T!*fz#~ւhg利G1mɜuwaס=b ?vւvu _͸  g|o9cǞ;rv9Sj g>g|cϟoyP8 t+cL3ny{)1l8nj 'n쫮s+ }'bpݍ!3*&\H.rT}<[]s x<(A.Džg.lirZd^~GɕoӑQ?8Ѣ/ElI'|FsKv8 F~8*4avө{h0&Vvx'ɐo|h6> t5QV${,hC?Z-~.vd_j^!a 7=_xCcxy_Ό​U VAyME/Up jU猉.mS;&0ŏ~bQI:^Ɨ5vq> xQc c:t{ U ѵ`/4X }i\N$|\- ɡ4>y/ij߇ 1jstƕ6n(4 R#VWEIXt(t),@6E9qEq lb!6% n7l;~^h\5\8 t0R<$'nEij}7ɾ{=NGƘXӘ* ;7;ks>10I`:o?ƕ㿃]y+6wuRcBs- {+Ȍ+Kw,Ƕy9s"cs1{̾8&9?=s8|ZMw~;:qXO;gѯu\<>:qw\GcTqǞd{sX0>9꺊%]#O{tgx'kZbpzp<|ؽ5Lp93ڳ\z.(*7pӟFD;؞6c|,ȸ+Ot1 8\Kpr/Թ1HIFܶ?}%dd׏X69\wlڗ?iE?y4+VHgfH;nd嵠>'76x:_2Z#ol񣓅on;<\'M@ ZrM+ie 6qKwէc?:ȿ >{=蓧k rѳcd>hlm]9ayyr %C0G|gin7ZIy]hcǷDWe_)MG] )F. Lz|ŨS&Ho+ _sʇf$]qR<͝_h[=֦C~ݟcߒc8<OXڱg1gt= {w&\1Wq:_{<圇̽}uwPBR>+)+N;L[)8s,K>Qgݏ _B48йRMvx%®~4QV9w,sؘҞv>C5>xpG9v?EvK7?\sz+8>gD`4g0q`~pf`[ɏ O6}qiǃO'ƺVZ-hÎL6]vȀhZ=q6H:کd>]'͂ *-#S ļs~zlj Ŭ~m5aI8O1XG/㊮%i^.cYn]0#0#p-'n$I9g[N4גCώ>C}ǠgcAv㡳6;xclp]W|4q $ҝ#Kr~^V[?/x Ye/ӽPA(.M1mъK[33Wk\ǗsniuhVgl{kzҕ>jD<du{+dqd=ZTN.rъ.ѢM\~зlAmzlh~-=<ɥg9ʩFvZdGx# ^AA|;H  NTqX-Ft2첗l4ckѴ{?;Go|i>@}RY阏,-јpn"%ϸ{JBIyx%ݟk_cA{w<X#;JR[qyh}|㱗g}jzD<dGz'G9f }&E'n*W)'mϟ>]վ\6{]':O?;|jނH=te㧇{@xvQ; _t cX960lz|8y|S0yvB7=4-_-jt?>;̼=otljȯ|cyR\!s!8|EVɜbFɸr-q{p露r{ct$ 0{.5%fffwSAKĝo|K@.ǎ_[ѣU5QIK@IDAT|3]}86oܢi-(oFhʍG>{h-Z! ҉G[xdFf1n$pRL0b+ڱTh\򟿊c?[h[ o^6Ȣi/ tlx*Ȍ|2Ɓ/t6@rֲG&Y 333333333330 %2^{gΠ"Èꅜ2YɡmuUquMQ[64:S64]eu :7/7Lc7OO kfG(.Z_ՖhdZ]F/}}x6/z>Z -u,G> ў |dI&\^[A,2 [69G]K}<\\~Y.\*$쏶.h\8}}VT҇y~h ᳽OX:dmpۉ@R&l46h0\| 8Zl }dg rtѣU(!摽htK~|'L~ٶ76cG˯| 33333333333$ %/K.#M_BYEk?O줏^>+ oFF^LEN/@[a[8=NZy6Ő7c g< $dZɐ|dIA.~ݏLNzZ|;<h>627+VzZ2 6}?y;ٟ\!s!.sF`F`F`F`F`F`F`F`F`F`F"p 2K}`&GC&kSd_JG{Gmd@ #}~d_kC7'/~g't<̽`Ӄ\*   #oCN_ ?{6=Dӫg^>Ƭ zOF{و+'<[AO;d 0#0#0#0#0#0#0#0#0#0#0#p "pN 2]6^2JX>[->u|쒡.ςLe8O/;dV_A_ު/ԗӏV>My}-ta-=,]y'X{?4mcMV<_4t,79tm'?d9N -G6xM(t x S;(XdW?pv|Axmdÿ>~6Gx5Ƃ^쓗̝^ ̈́ˏy*xd ,Q$yڹ0!+[BDQȉSԢvE?o!5[G?bX\ lޘ>ɀzLxCwsh`pn dY 0^v@Fn«v ;4~d.⑳V+]KǹOƵ+NwuCvF`F`F`F`F`F`F`F`F`F`F"p 2n‡RNcJ di"|!Ϯ>]icʋg GNotx/x׎89svKgAW kƓ4:|̥LN~ )@4x|Yꧫ.av4E '_ 䳖\]_Еߎ6/xy/mWHwa?WȈʄ[sZQ|yE=t-xWx(Wᢼ[ P~ZDKV,q75+'Ggdtq #"cr6+ ??dr = l [892l5bʔ]!x|tnsGt>S#K0aF`F`F`F`F`F`F`F`F`F`FEd#M 䍭V)޶9A(E:?TXukً~2lȏI'}nD]{xZ6 2ZلG'E {jAk< +xZ+C%_^ „ˌy+ȘRW,#MF>)Z} -+]mir-Lr=DN?۵d>Ǔ.j 8]2Alk1e|#o2vs ~p}>dA`g(>įfGZO/@-[䲙hr#}ԁ|2^mۂx-;OO;{<!Hn-OTzֳ offfffffffffnRE0GX6◒IvXy31BF˒GlZy&+|SEm'הD7ߵdqѴ ^hdi?wn~ƻ+=h%?h rmix4PÓcBhߤdADg= F -ٌ¿6?tto{ϯ-=اk?c)&6;/|dI&\fiA kMqB8Kwk[*TYX.YyjvгM^c#۲٢x^{gN޸]U7~C-݃`9ч^.xng@?? 1$}xqAWR Wָ  _8]@V!h?Cv]\!#fffffffffff.3 c;dj,zFңzǑcIhginlGhO|F`F`F`F`F`F`F`F`F`Fjy*^÷>ۧ+ymvqD6.863].-hl,;z1BD@R mBmj5)!FY鬄iѵdlKvl?W)&G. \ 1%wƐSYU_dxЃ2z׻^^Da[{vyCysv]{ӟ{7~=o"3333333333; %?Xyy9o!'mGE-9tOlЁ\ZDL@&9҉Em>'>yt9glcW6*[--(m̅? µ żܡDɵSW(zjp8ddN(OErd zk-#KK} %'X8yC[V /xO-/ |g}>bvF`F`F`F`F`F`F`F`F`FG<d< X~ݲC"\ު([Q vAv|~<Cci_@~Od(_Еm|l&=0;$P\zEe dskWZ_@VHYн,^Qxc}zhhLYH+-9}"G^ zcK6=]v>$'X!_wzw:(}ZA}>6{6&qF`F`F`F`F`F`F`F`F`FEd<)q;dD[.Vέ_F l6 ;%GO U9hx$Oh[:qn6/!{ iQ7TLxEKz\ '(Fkd4YZ"q l 2=w|3waiDdr[(s՗ckZW*$,<;dȃK6'˖yM| o=/>Gpzز~"~14ɋ7g$gɏ)dNބo K7_rO6{䛇 Ɓɰ3>Mqgkgx]_Gsw2K NmAc؛Vy+_{+^м/گv?ooK-~~kwmok{ x^u^gy~> y1%/y:ūq*Mxu9-kM~٦xTqVq|]>o=&₱9977>vԧFOW8c?c/Y@&>#0#0#0#0#0#0#0#0#V_*zgZy0_P!IIvA>`?/_Z@'~Et[˓ɢ n,죧86+6BO?bEhg Ppގ#K'Zj _@@)cȌ ?<[)p iOo<=hC:O8OIoxpі~>>Zó_ sS̻ۻ>?q xNw plA替woo~v87ȵ^fFEſ p[N1F$P`~VOOEYoʯwofffffffff^U#pN 2#KN+Zy*&_~}!=cr K+a`-8?mAWy-z6ƒmmz;湠kO=P: 8^/&i0%Z| {M.|uӍN7ӷ-A_1h'bYU.[f86:ƖO2htRY _hcA)Oy*G?z{)(rLzE˿'=s~n]5x]eGG}'*GGG"|}#?#5\|˷ܽ/=OU6V??Y5껾־┹{TW~W}?+3333333338o_e\w3*&VųUxXЕW%].lirZd_FrdEwh4-y zC>d-"94& 3333333338o`Y1olrT.ݵ/ӆ. ~hV胑fNwkA9>|%Onl?t|}-dF[Kw62kG G' G߶ v2xOBA8^VyNN=< =[7vN볋Ɩ ayyg|g2K4NmAӟ H雾i}odN/ջU2|Ǭ^nxXu<䱤C`Ϯ,l{mF__X.h/|b߻{7yQeG~d}I_| f0#0#0#0#0#0#0#0#0#*TG #~UiG]rJ/wG{ӓEO>>n×`|[}|nk|t r$C?,݋dnr!86+Vp|<Ήi`r8 d@6'lA&'Nu!ZцlѴ zxLGkcmLuS66m}O:Oxo$*W?_]NA}j*<{a<z|h?J~^£Z{:VxW}f M/sU(C-T@_ }x[l F(7O_.XYMt'?#ݘ/ 蒍s0敌6]ut-ɮG#m s4 DƠՀD[}-9<| ~6to=:mȡc~G?;_е>]|ŗ.NǐOrp,jdjQTqCέ*}}hoVo#0#0#0#0#0#0#0#0#*TJ 2-*'}hEh ^.?lrZ66=6Yóte#;~v-zOoO/ڃ@Rp ۠sW` 8pd#[:vK6 xhZ=vĿIsk~{@7>rp4ƿyG۾j)miG^򒗬 ' #n޳}+@Q>pߖ}~vۏ㥿dyU_VhŒCSAfxd+DGm }&E'n*W)'mϟ>]վ\6{]':O?;|jނH=te㧇{@xvQ; _t cX960lz|8y|S0yvB7=4-_-jt?>;̼=otljȯ|cyR@ոB bc^W 1WcA#:=-_}lA%Jy !dSA=<Ɨ^rq,І˱VhU|Ԇ/LWhZ>lGcK;Zkfg?{5;>juK}=r<~%NzﳞKKVxy*xdIAfy^lY30z!g##L|39trh[dlGg\]=`s ;=  vb$Jy[ qX %Iw'GT̂␤Ӊ8]q@]" 3tN W{۷^U~8{S{=}nݽU=Nrw/~>ؔo1_o&rm-UkA$gdKYl: 7_0;Сũ wtgF_,{ٓ{t>:|?/YAKg3 =2c,eC{~a)]\kf_=w AY#2۰[8Quտv[Ӟ;t|1ZhTtp<C %'AH/x'@t$:dkPBX#C[xlK~&Lq9◛k sS?u??x%_%OO~JMf_g?%E/z^nqq>+C{??qWro'7Wǀ=yٹۿw_E_u>v"VVVVVVVVVn \z,3^=2ЌYz̆,X+7~tآf h&C- Ր:g`VɣZIV C .χNMv0<xM ݞo|y?{|e1)qmO2x;g4x$:2tK7]}ݷwA <6y3mP?:g߿{_>??~w60*`">3>9M\XXXXXXXXe*p2C}/|#5h:&gv zd >{(^Kߢngc/؋kkO|a;C9 fK\>~Cǥ'(0;:PņK:t(MF߁= ?ِ7;Ck]1Y^9w cώ|$+FÕt GydE>f̼2O@ƿ~^yڗ}W&ggUxQ73U닿wyk,̿_VǛ~^uuJOOڹRŕ/|ٗ}veL:qK;#{pZS re=}']xU`U`U`U`U`U`U`U`U֮%f<,>X vSןNų7k4>H>(k.4=K ~q؁RL6o sN2f ֖ L_.a@Xt7AJׂҙ*m6&_٦p|UpQf-"VxY7Rnar0ۈba-=ͽgCtFsŐʕ-梧^hx2ߞs^C_49ks-UUUUUUUU[i 㡾` WY6a:aHE ^G嚮^yQ>^Y4\ŧzA^eCN67c=ȍ7^Р8}dGűOV|xWG! 7@xhp?N_%s)tg_> 93{Ag־_&x_Lˋ3 ~{!E_c oΫ<}<<+dݪ$/k " VVVVVVVVVVn \ҁ ҋ'z rx7xOࢾ۞P0y3&÷ޜy|fVO UMY?gg=1Ti /y<+9t n8_ȋC'ŧSf;njU>x+SA> {']+ c?{W=U,UIB ^@FE le龱@*և8]=!}=8:x|ѱAr lS__2Cl_L6z䀿1g|oS__O7Ěϱ0`)D7}|Cw ^AGE+>OtbP$y?)O.^ r+dprX7t[<@o~Hc$7R3r Rd} v}: lP1wh{. @|t~/g04hgrC ramɢoec?ɥ[227%*ʬ̨UUUUUUUUUU[t {23xl=LMnoФmtO4`VW4;+LgLAfzn4}yf.1(H,8(=H$%]!dmB8*=:0~9rI/CPayr -^4[@ h+>c~^1nY}n-K{vox[ ˪R6Q kg4@zǰ^>Y>@z']nj^ݪ.f9WkA 7ھW~,AqAtG: 1N`?X$gtWh|5} &S|^rścȮ7)2 Lysri3~ۿ&Wȼ=­VeU`U`U`U`U`U`U`U`U`UV@ڹ%ч8E𬋞xx3}swnq˒+d^0V}mXߌ5Z$ =.L'bNxV:W䓭U,hr}fcCf;f2IŌ ?; MxvXˮ֕"/8(>磘a_49%'V zgbakG6!GoDYdbW2rt9F޲7i]i_EX**********pT@=yGg=Y;c?9Ĵw'ųaqs-}C_iad29Y}ܞ VzxA٠Kf?tӛcEdh3:^=Z_> rO6}1ErqrvT8-N?lG"䣢 f.Y{^A_4>yo| OvtO< +/oּ|[y.7 ׿#Xϐ xs{޶ : F8"xEp qһw,e #77tʆ40eO.=y?{lrϜG~8݋Y=Lǃ+ uѐG < |AZĞN@'=Iwb>x=ѧ+.s-gW>rLM>?_0oW|PLS0ZVrJ%/L>@g qt/AHbg_< 'N// 5}2A\ʃ 2dO.?qG} 79\~|Wo{QSH΁sҋ?X7tp؆ {0gƷS#Cd'\GW:Gpz] D']A;te%1—Xɱ]U|<4|/!Z`MӉn{HoC h ~z|x-6xlԙ2z[sc鳏WƤ-LF0}},1g=|f7s|g}ؖc2Ĝ͹/P:vsR *٣;|NlIťЙtGs!MYV,8 ]ykGPnG27 ޔF @J,**********p+T`ƒ[7W{ 5vR`g{`e?ˋ)>ƒMA&V -N7̎Ca''Y>U,. J4|XAnpߕa@IDAT4\#*[4f耹t ?gcߢ#I/^|~{g_yŏ@~ -K]ߵgO̜÷^r`U`U`U`U`U`U`U`U`U`U`U঩^P35ACW̼dgfs0},i 3߲ԻG S@OA7Ȏn|XJo++7X b~ 7}?AC|.oX\[; d{`t-LPzsPt?XWR2_.9_Kfl-^-0(z']2M/_~΍ V@ތŤF@?w dTyZ2>#[g(atһ+r=a~]j0M  %آ]-t_\Gztb;`2XU|&zGŏ,}W>v~y1tS3=OWGxl  ]@\bf=? 3kA=>zO.7t+>=,.nע><rK.w6|(p0O;AAaz nMo>n򵊏>~A7?ʝ=>?_ŗ;'BQ8V6|~nmDt LJ3?z2s**********pU@!qaLbu氆3g=;s3dzJwҳ{Ϧ=9Ŷx[@}0՞\[凮ζ|`2>y0W/Chu,'Vhr2`ϩ}h`ߒءCF}P^pdhd_/dCăAS zQܞ+n@2%C] rOq8i`p99<̟Y@||+dK㯟s; #h==HJo>~)|<řrN> ⱥ&+~gKJv 0Iqʹ8no_Y&a!3Ogk΁?W>;{/Os(&=[nyoYz衇vސy?nmx dkyUUUUUUUUURb Ao{۶,)=xf8}Y:jzK^@FZTOzц.M^~zZ> l^y#^vdxxQO͆^>C.ӝcr4yg(~:dŇ峀A ~䁁AًUBK̉Gaa_Ǝ*xaWL|P*>?boC~0-y!>yvxhуO/ +f|`#pnvY?>J8< {xN#[ /<}~^0{˓l5 |x|MQ\~<02wWxxNJX***********pT@+c--T>6!ų`t>c>f;2gzq, =F?j;Xh28L6xl@>ם~~sCA>?dOV!dtfqP 0P8 U@xwNW5K WtJWAn? ؐYd r:Ĵ@y ^ads2ޘ7mt }mn^XXXXXXXXXX)*>wMA |aLÓb3g=|,2+qXY3h0]BϢL~3=|zx6U],<6S> 7]~gMl)0hܒhM_^IrNΏ :(rZ }l=Lw-싅sVPC{֞o>ŤkSY\U|H|~K< "KN%d>_xceA bu o/0_nr+ ÿ{ӛ޴s?%X ˪-R,݆2 cb;s!_x3}%C+e!#eoPlȢ8~ G`K,/anl-g-}~~gͶÜ[^gC3D`] 9خq ~N.l>-^S`r}fW rGg+L^-: zkl^q60]h<ć-|3`Yso8x\zXCKUlAW_@:d~qÀ} 9|M<4h蕳}W+~:Gb4\*|K=ϐaWȼ}۽ ow]!Ua}YXXXXXXXXXU*@y\}>'u3C>ԙ2K/yxd`tMҡʃza:M_vMt|wr 9'rB|.Y|1ҵ t أ+NUvx|A6& 3|, c/&8./ۓ_ |: \O_S>`3KD=Е(d AMɳbE֞ ^ )tV +d,~JrI|6>U^xltϐpFx+SA> {']+ c?{W[5Ty衇d@FE \ \ҁtXz^ojzCľzY<~X 9`|/?/&t=r__ҳO>÷Щ/ŧYbnY0a"|ЛK>>Pޡ;|tx"':~1m( ׋'|ϕo289,~{WzxV:cCoY2q˒ȨUUUUUUUUUU^6Q?Уh,C>0878ѻZқyzC9ȻM}t9'ã[daVtl=>͖h-:1_+}{xcx%'q{顁=䧤3`b>$~f[_UT>TAn GC G'Kpk g`ܨ޽Ȓ8>nYJ DeޱpФ!Hϙ!.x|g[š_?gEK'd'3[|}y1~1?k|)9p)y2J>z{>rtz|6;=Ld >:dǗ3}3! Oٰ6dٷCΟORM- _˱[fqଁJ,XXXXXXXXXXY+pI2g, =vdzxQ- M fKWDϾYlo/?xAt΄nFӗAnɋlDIأDWN]aV|*}hˡ"٣ 2i+w!Et f?㖥ۇ)e#<{["%:2*`U`U`U`U`U`U`U`U`U`UfeȨx[^pzFتǍ?XX?^ONO E w;ţD>ѭzbCyů&trJ(x|5Dn{xZ0+S1=a vOrAw(8_pƷZm2+>y-W9V슟Oz3ݞ"I?*=aٱ0k  [DH ! ;{vٓK=zޏ?[3./4aKKfoe`mO}h'A؞ ΂LCӅ%~/JCY=xl6^|.̇\g0y7)cߕ.lE ~䒿1-9 7>>eI2 cz׻vo}[@Fq \ \[_W2/ֳp?䞧%`.;=Nze Ś}=ѧK=}|O!tjY}f? >*9 'e%1zQW:(~d4~~fN0@q Lا+VYw{X|-@8xxdnY{\!P_L=ؼ/XXXXXXXXXXXy*p2nY{%/ytTޱEFwՋ^rmB_|اO_<=KybYF,'Nt&o1m> c 9x◠=Yzkp!~-QNaC //;inYj C15J Dl2/e œ4h' tÃ>epQN r4H}(_2/_z7e>9g߾<~|ɖ]0;2P\ѻ1?!}<ڗΠ. \өgs3 rc_>x=ћ `)Yd } :| £lC{b*ly{{4HΆ_ OYrP.iM8wCYރo[8`կ~5rMS6Q+oPCL{KO d{n)Jw=O0W 67o\^;0;G=\ >gyxW@^8lr78( Xa;8~>,~/ ɋGf)Ths~ƾEG|Ó^$06ξ 3o[ e bleX_VVVVVVVVVVn \ҁ|˒SZF7L?#;a}*Dz?XF@`ic/|ASna|coaqobѵ@0AA `]uHɀK?ar .bқm϶xfۗ_|v 7|_976[]z3 ΂UUUUUUUUUUm s塾uJm Ewa2 7Y 2@4`w~hp)~sqgщC+`qWM'Y?;KΖS Tff{,mv.H~Y t}>aIf]ه}0y.^CNfųĂUUUUUUUUUUU'P6qq˒{}XzTvhz$O+`l1΋XrC?ҙ}x-:3!/qg÷B J AnIf/_0_AwtcO! z_|~ݞ_<,t%>/;ha~*=ȃ`+떥ڋ^XXXXXXXXX*p2wyc5tߞ\ONzzzٴ'Ƿ/=r}+&ړ?|ٖL=~a?*?e^q圝d MN98sޜ >O [;|t N M?9u6^|L7[~x0]|t7A/j4r۳Ȍ*,XXXXXXXXXXXxl/W;_?l5( wF/zL{=>| 1ۣ Rğ2cħy3|cK79LV Ε 78>??Ma̛iqJ߾@LBfx0bo80=29z.}> ֜=<~|#74y }vl _;Ƚ3PLzh,ݶnYRx.@9yvK^2n-'׏{hCx]/?|[h=-@hrΎO6<ۇّM/;2K<ΨfCN/C1f93?GYqŠ[?LR*%_a0Gt/xrcG[0,ێ78 d?)=~C΁mt d9eI |L閥ꫯ=G8}=g, |Igȋg/fqoW\1Aqʫ AnP_|Fro(>³UrFTPHA@مghg(/၊ 7;c>fP{ƙc'7EM' ~<^{rr6G~lJV|6d L? 8 0! _bi Q^_>_x[\W|>2!RYNǞ(b6l@F|ݿbIIߧpG=>(l|VVVVn =w qyeȸe@f 7]~gMl)0hܒhM_^IrNΏ+vP<-^ c:8$W{?[yeO?{r`Gx%K6h`F<{Bv_Tws3^?}~9odr6?|OԟS7ͻr]j{_?gv_;{~9iz׻vݿzgP~F _Wv3~_t+ ~nR ?g݇|ȇ\(^~n6qO /Wടk^s^,o{W3yyo>ڝ}oݫ^a?lo\[Om oo߽/Oɻ_~yM7=97.\g9`L<xM ^,x|y?{|eoIf%~o]!sR}=m O)?ek~ͯC//߽oBZUo/ſ9\Xyˢ{~w__?aGGJeȸO?N?ݽ޻͞qgd/`)) D2k^-? ſy"~olq 2{ܟsۦ99CFv񍾙^jilL顾:x,>R/ -^S`r}fW rGg+L^-: zkl^q60]h<ć-|3`Yso8x\zXCKUlAW_@:d~qÀ} 9|M<4h蕳H,~: W>&:_)7{2 Z:d?p7a/Kmt충C?Ct k ~/>׵ ǚ)r~ށ̱??e1݊0.2nrXફ#o]''y\{=o'CUSzQf59+q@uW?y]??|֪ \ҁa+zޠaY=e{);]<|ӫFt+^@ӳ,d=݃|dI,\*}bŧK_ axt-)9`*Hu8Srpr.`mm Gɟ}qs8D$'KGq[6X/ll dt23F[CkN?ẇzhQQc6ToͿ}|ϦΰGmK ?'m _sw_uC SO};o7=(k bk窒n!?'~ď/U>ߞ~ɇ}׾vaɟ;Êv.MK+_k{Q?߱:ኊ~U5*ݿwgK7rF~-M{7n70l%@_ :_51?$YG^?M&_F˭9/۳<>ö/^>Z>r%Y,^Ӟ w 0}?ח}Aۭ=cgǾ~O[gͳ&p ~Ov%g'r}+sO5<{|y3>q[Z ga~l8ŕU6x:? O2=cMާkt9}{۸|{vSaRr9fpʗ~ ?{jpvzw jC=}?GW8yi༞_ Wv~~fkw0g* ?[C|~o~>x|;_n=-7E2n͑sxsw|NLf8{}{{5|8{~ۘ˪9+p2 = Ǫo`oc%Oo,:+|,0l`0:X~BˣtNW<codsŧ<kO(P` :<> @eS*6][铃9E~҉[|'dOVL=|O~ėy>A'97 Y>yUL3|̇c0gz֝tsE6o-o_5_}0ӗ=yۇ Kuڍϗe탈 ZS\)<bPr>A,?OE_Q#'ğ7C>( |+h_Km=`WSGC|YW_̗P pIf@F ~1/x vC\Қk?Ϳ77uWut;kGI#p{O?}g9 i]= _oo&8{>C`W_F⬞5gkrπ[~9{7_:{az7;{ԡvZPK/N6~P@+z^cYY`ư"WOo"@w}keq>fsGx{%^_?C{Ͽ4@iC{'?v߱\^77_W73Xi$s- ^6Gټ{yF|_\v ˾lcm~7,X@*p2wyvԐWΫg-tW rE{0~;z\cßCgCSL2?/ 7^,2.~A>/ۓ_ .W'(Мq\%2р@ &\lJmd/dx|7Oy}ـtيa嗌/t$K>l*An<6A:wzW@ EÇ!oh:n\k 35Ec<?x4/~|Ҹw߸H>h/a |C3%@5>82hPo]_o|@Ϋɘ2HǚcKdK\{>34|xf%>k|2:?6Xc_Z{.fxN4wƆL^"j)/ĺ@_/<s0 Wsn+>t}&$j|+r RCGlh +^' zޟgccEs " $&ЗF~nԴ#_؛ޏ Y{+^!w{/U^ .r+B64p6?<1r9{#y]]-n ~4wx?!w:69UIc9\3K{<]uh;ʱg?Ӹ"F Nø{3r1]W?{Eދ3_ Ҿ'S! |>leU{+al=93{Ag־_&x_Lˋ3 ~{!E_yGC'BFUs>?G>Ŀ rAFZ ,n9Akt5- }/9>@,*sksL=s&_Kvɟ}\w/>ݺ}g?c_u̹+j2_xy_ߏ5WEͿ7oW_e)|@\Pooݾ7\1=۱>ckb!.2Wy6_q{  |MsT0չ&gok ޡoRa+zڜ|y ðꜳ~v\nc?#xsfj@evw6_*su<|xvLWXN;?8 :5ѰnXL{w yOޏǜsaȠD}O~'a' J/+`w<|> a}zb 8^x&t7'b0ì/:g7#/NgNq 73Fӣy'XA4@е26NպeINk d{˽DϿpobsj'MhWC\+s§4 =㞋 cdq\dtՊ=]+1jFoMo~ 4@'T< ۳S{=/ſxh_[2|(r]>&ۆ5 CkCp_z;E2h 胳 {I0_sMKh~],]t #+ѺE-"5^Sx}e]۠"7j{YghX?᳉E'WFY}?9z߃>od_g䬁^xSK:ydF}JrC{z|Ycc٦dÇؾ%c|I>ߞ@ܾt f;Lz\xG;tu|TdD/^t?)F>W n_Y/KSmCC~f~E0<mp˵2WWs=mD]~ZjE~xh4_u_Q}|i6/} n5/@fný0~LJ?*ۇ{axŁz?m@|4=@|tr 6ru%" (5@0NtNǁOWχjjw\_;{:b7-ӆ`g/Z>JS@mL 46=GȸDMu뿷`Mۣ^Uf70|.e3[R@M~M2ls<+snsWl/2u;9|Y1+u@f~쪭o2I}yjjoUa+ d _nҀ]5n?ct d S{/Z3u?Ns3CV79}ݟUpU {U;߉7}j L /nJn=Kg=={?𻹁:9@bd P512`7Ho 3~6]zW&e7y/2yHjp*Pn@g' h=#^IC3C]0x|g[š_?gEK'zhřx__xmofO~;N<aw;-hGASb31iGe~K.~9IG?{4Xa'fS.hmg/zBP9IOɏ {w dN7//> \0x?T5zn 4jUGF[??.7og'|?zJMc&Н^!c5W&u@ҫP}j/nrKp y62^3=e~"N] (/s4\an͋g#zh6|WR[97>ui3de0?k>We65nR'WWҼj~.ӯvFU7 [ nC1&{p|񻢁|M~v#ynrwlH|4l@;߫ۿ]]S1h^#?>r{^{Mz?f59=Ϯauc9{u2~3׼c ەst&Gc^u~`O;1~&7=smyM2}}^F+BѾw>nDϸz6ߋo32mO/}a&O٢W[6n^ecj > b ALe8ӳgcѣϾadEAg'?w=:{;|7xŠS0}<7R:2NWDӁ9t.4?lP1;=\+&.2L-Ln">\67ϩp_00;c}YAn@2npB80^69= 7llnKWDOvG.8hvVIg/F&ͯ.o%9]^Yx7XP0{4tJ 'h?(|U|=:?CEG/Ǎc}<| hX/o:hdxhAӯyKMs0}Lt~FK{xjlO$wy5gm~36۳51H+p{(|8mO YOyЭl^Wnl7>WȜ>ۧFy1~էdWdGo|Fvpv[]7_cv {>}Έ&?; _nҀx}ʔ:wOӏ 4ש5SqCh7h$L@>hT uMc}h^2[r:LwSj >Wz0@f>4fg23)*s;;]M glN1T}/{M4ŏu^8mZӭGѭzbCyů&tnNwP(?.>>hXga*S1=a vAw(8_pƷZv<+ތz ޤ۳Sd>Agri3NsȎ U+djJ'y6?P#e:MsMfstA^&[47, b ݫ>?$R.<ü?WUblF y:ʙy\`1Q%͡f*Mg5|WtO#1?ŦlfcL㊓^|Ϧxjel=0+=~An*`W#Yi s{tʾd8p 3jNr'nsQuYM?ޤfW]!ÇA/;>qmd\99=4uw+AM^ƔMޣ {Weuv3񹟑syUṘOd`Ϝ~n\M׃\]Pv2ǧ{Zj1oKk}&N?OnqGW>9gJ~4sp3]=s >L2o{}n?܍]^ދ}xM{LOz~{=|ueM~oLrO1{+kT$\蟂}~o8~>5:g Tv뾗]&r5]6 tƅW[4ܻBm6^Wa H&ɋOdŢ=FU:dk-\,xlkڐUMX \h/ݠrO/`/q0((:9za])}>i_ AE_rAz޶cak#␣#r+9v djـ]Lͽw}X}/i׭Mn>u{ӛ޴uVV4q˧ȼf;5C&'֛ۓJ/^1trIC7+ndh3ku>0{ '\}'ȝ?́?b=,YY$oq-L2>d=v/X/<-_dѧ~{a6ˏ|㳷H^~ {"Yy}еy~m>?;7tOp˪3?z3t- @[0jyFN?\YN}:ȹ>ʱ*q]^Wfz+惺B]"uAvn*0_W^9|=oKm ڮ{|CmK3h>D֛:|}eCv {varvG=g>?3xpy[zg`4C<n_?[ ړe> %C8@Ӆ%~/JC/ތͦC>ta>6qÆ[⛔ѕ.0G.K1D|ӷł󳑻 C}MF]z„ɟtҽs2ڋ~*0?>z&l9?uz)ƏgXNlו__?TlsL6/[y+_7W6 d ZOd^^:m=4d鑥G> zt:M2k/zѧKnjݞ> t[&ujY}㟅n9 ˲KvPz(P?a2v??.L9н(3 tj_< гAn//-KC}2[5|Uso\e1Qp/ 12࡙}齈c41ڗeH+UUJ DP𥳐<׷zVOsUl<;Jlx/+Ѓkt2M,ȼ oxbc҇!^/mB/>o~OΖpq'ï?#Kyb)O'y|ԇo.ogzGBr$%hO^~CmS 8hU| AFupTl>fѧ>|˱=;|Sq6K^^!v#/_ d;^ >n+C?C4rP@7gUh,**p?T@sa~<9{s̸g;r <m Wo CnG['A~K̫r4H=x١ԧ|2y2t b/>Α=\sGwIWf"SVSJ w17Q惽| X;mԞNtCCz%G{a북 -K[% | ȶmS`Rύ^i>:^iN0]t~tĜzAn3_7e>9g߾<~㜓?.(tKg@wteOr,.a‡<4~XpA8GPnG2  VVVVVVVVVVV> ҁOPzt[˖C6Hp}z-avdxw58s<~:Cx=/WA]sM~-zwg=}|G/Sd-HrC-6󵱎6x } >Npt/sȿ}r6x\M8wS)A떥P VVVVVVVVVVV> ܶ>Pl>У>`4>[XG}tiL_S^Lq0`Ͼ dx|FeU|vh<:Ѡz~d )=]Fw{!$`Iwp|l /0tfRl > f~ƾEG|Ó^$06N_yŏ@ŷ%ό>pH VVVVVVVVVVV ҁ̼eIߩwkmN7L?#;a}*Dzvȟ\^ؾ+,>-Nm>|/| ~t}.&  7֥CJ\ %p޴I6>ޢKn_0~yU,+pؐotM,&<6uzpÏOW_Iy}S6y򡾏o|" dVe 0}P-5٢)OOK?Lŝ>ң۩+`q#/M'詏%~gK) wPB£ntmOg`ǟ%xӇ}0y.^CNf|{Z[**********p_U doe^f[zTvhz$O+`|6И|sׂz|O.7t+>=,.kע><y%O;urxV1:AAaz n=Sl~B015^en@桇m ~ն@zZXH^;ߞ\ONzzzzٴ'Ƿ/=r}+&ړ?|ٖL=~a?M8tVr^|j&'\  9Lo&fB颁}Kb>ҟN7{ɾ\7 O^t l7H|}F-7=[܏w{r\EX**********p_W d˾\!ض4I=gA^0OcڃtѭLgo7Hdn)9F-0Y;\:W:p|` K7iqJ߾@S&a!3 l^y#^vdxxQO͆^>C.ӝ1ɦMY@nqŠ[?MR*%_afQX|:n<# ^|mGt·`yM|zȼx-Kk5 LJz^eU`U`U`U`U`U`U`U`U`U>mȌ[z+N􏚲OO n7'>^nƣW_ys)#&,^6rr&}%ϗ<'6z#6 Ȋʗ' 䌨pt Ohg(/遊 7;w{A5YWZ_VVVVVVVVVV ܦ[ dgx(ACIw =2]ڤs*oW}tq5 _멧:廑.`S(~9tʵ= '/Zw+I!Bųu}x@d!۟jӝv?X'|tN).~=^a:3F{ٮy%wiWifk : VVVVVVVVVV ܶmWt<-=GK{2^\-ZIO_1fo @|<=~r,9/9+}vrek7bҵ֕@n,B*@>J$>%O H/x'@t$:dkPBX#x|&Lq9◛k >C0Pt4^C5QSn@-K/x G:ꑁ^Ѳ7c6dZ'Nlx'{эe|G476}3[>?ogͶ̭tZSDƺ 9}jׁ8o?ç [%k;$YI +} F>:>7c>t'l @_>hzd|[tl>+;MfL2{n{B 1` d2/i 3ܯۖRUVOE0>+U6Gٳ&/F`X?t68;c0]h<ć-|3`珬X9'uRHNȋ3/&Fk`K}59wD㯁,XXXXXXXXXX*p2 gx﫷U^jכ%Oo,ͧ _ ˆ=L'X? 鲵4r+(><\+ۃrVzbdpN|kO,P`botx8dB^6^m bڧͷ8N>Ȟ ՞N>9퓑gφ,y)&ݷn돽~?#?"K*7t߶̟s?>]ܓ)J %Z"stXK0Y~g!Ȳ)'9括= 4@Se$l*G[1ѳb+]%YמO{6|F<:A:oޞ!aǽ|U×Sm߼c_n_x Qv7⋿nvK**********W6 dN>:zH=e)mөK6dsϯ=yszX~ AFO\l|t/ZdŇ'M/rM^6xf'?xCw|݀NI>ٕdDHf BN??Cl?R7=493b_^䝧a6h8􋿑\^ ̫@q~OL⥇o7cy|&f'x,|Xܧ~SOrrxG%g%GF01`0uE,;8tY|z ;A+lM_W2e#w'XA4y/P3kdlgu%*|d/?%/y?O4õ` dU*********Td|kƮV>9 Aų狎 VȧM}-O}1Dkw%==?N}}9$/>kƟ1)D7K>>Tޡ;d73)2pe~)J*)TO)?eΌ_g >=LJ~{R'~'>̳߇/θs**********Pn@Ƨ,=-gī64isf> &O8_\|8h|xWSM}288S˓/Owǂ `:ag8Ph0yX;\ s?6-h3t+VtAq^+< VشOz n.zB&}3떥n>SG052Ӳ~O;=k{6;L7XdEAg'x6bq~ڣw#w>~1?㵏v&?BJG A8\AJLȣݴO * G ?rӧAK>d#wv,:0^v߶)\e _!s/>e% YCp|^d 9R| \g MMeG#+` xw|'~ezCAL{5:O|MTg d\!03Nm=LMnoФ&mS󃟟h8`VW4;+w&F&o=7on.9,Hn pP{4HJBɢ+,ۊW/~qأ-~9T${tar=&= =@q]|~-4~2|Ӿ8l&? D۶{L6l-0}d_r:xd\.|%#Gޫ̻K6-<|}V?'jux ^pψ|O?93rC}=ƧWy^ 1̙W3vEW ol?׿;ou??Mg1VVVVVVVVVVytLխu9{>srk6}=_6zdzs{r4חZ+fW.ɳ~7cEdh3:^=Z>mNl}1Eo ,pc8h&cRӎ;EGEXC͖ /]S?0y|QO[ sN/?مӽpYZs޼ݲbÑ2>5-4mȸg>x@7xſ~7@~x߾Uspn V0K/ſxtt 3o]ؕ8ݖ_/S_XXXXXXXXXGݺ\5x#[tۖ~2|֛:{}eCv {vٓK=z?[Ayă M;[Ńv'cAڥ {}`x !YL^|-MuFŢ/=2>t% olnP_?^\ n@F>~ۯB@gt҅8eiXɢ'?`ŶeG-KC}+d "ܦ}ۘ&U諿?#d~yw$ d?_K~ɮy6__:M ~@IDATOiGsk_oU`U`U`U`U`U`U`U`Us ӕ2>,#0 9ʐ58.6n:ѓm-xb}l[z9dk] UZp͏}ْ.Ndyr)v@,`?e>\~$OpML?0SH΁sҋvo y׾ao_A6Hȣ.Y,Go9gr*yˋ6dn}{8;k̃~oY򜕆0EW{|?|7|/2_M2=<`s`|~t@C|_/-_UUUUUUUUUU;Wt c8%C}ޡ!u,=h'=O!Mns7ч2+0Ʒ NK;+Wh>{0COddxّA^ɣ˫}v#{dG=D$/|: =bn^C{v>] L˩=>K<4 gkcm|oY'8|߶24H{Ӂ_>W_\MlOv˪*pn 2!'!+e e&//Mg dz[sc鳏WƤ-LF0}},1}Pt2̗/M~@ٷ/O|m9&Kn斿W>e萗O8`τ *٣;|#w:d\1K/3tGs!M)+?rή5:obty̺Bf+U,]oͿ/1_U_uxӛ޴|OZ7Os|||3Kԝ{M=ܞIӭL2y+^qx;ޱ~7ۮ0ݪG=_k|+weU`U`U`U`U`U`U`U`U`Uj CӇ|<S)]_ ?S1+nK2<z Tѣ5~#wO/teW?̎ /p~Gvt{OgH*3诋{ԏEϜ DZ/<О]p`ꔬp\Iwؾ}6£o@ǩ=^1n弑{O_|@3Kr`g΂wj?=ݲt[!--~V>>_uO?u8%/yɮ{ӁKoMnG}t~́?x}ݗnc@/?_u>'K-XXXXXXXXXXxzȸmK$OO9<8pݰf1z2ɇf#R:꣓O{`bυx M'7Z.N7̎Ca''O)O0 GM%K־c#w]83 @xdBel0NC|6-:"ŇpWc',~t2/\߲t[?eѷ?mox5|Kit @wqr;np:_:|W|M3fX%\XXXXXXXXدhw YЇa =ە cOϻ錁!#۪ԻGK;0Ş@3tƇ mcX;ϦzK_1y8嶑> ŗK sn? JoEW!%J.钃uIo$gmgoчF%/<*N8gnlŷ&ߺ d lߌ ᵞxX&|L_&Z,}~g}Χ]}ɗ|o0bBQ?'(.OIߟ'>勾AϹ ]c?'.O_~+/ \T 1 IN1oE?驇?nY׏=)3&\4kXkOf5xȽnA胆lvƛ<=-F?wHN> <`x7>tYwt`s&PEH> Ё&\2s+[/D/2:.H~Y t>v~kX7} o?y:d^||[ 87t!i?~^\ ~ - S?Suz׻??pxK_zO{^݅+p ^Wyد[>3>c_OU`d -mJ/zы{7Sww0n: {mQo}$C<}O_lݞU||P|1Sg+wz/?n/_U>Eˎ>Z}ؼyWf4$`'=&?zbrd0exs?dxf3oN&E[np{oWb ]D{_b/xn7##[~>G?zo2Nr5w|w>>mnU`U`U`U`U`U`U`U`U`UYLW[Z|nCn`|t d}ٗBmizV2p}a@ {t+})ۣ Rğmħy3rN;cK79LV ΕF6p|>+~MZbҷ/ДIA fQ*!6.[32zx8_Gnhٳ%7|FtC1ݲB?-K 3Ȱ{>!S#׿s`'[M؟y7Y^XXXXXXXXXx*WbMIպ0Mt9M}'=ݲڞA@FحEq`m/'͇ ď&dʳ}#zh6z>?tI6hPtȊw+ݒ.i%g/V /Y 3gϏ ?ᗿtɍ9lUxn;3u>?xGSn;~<@~oYCi]@f/ó$?soKdmJɟ|飭 BN' NiC=-K=W_'G=f {ŧpz7x YԓNȋg/fq7rѫ~ْKgfG@dŌw OKrFTPHA@م~4}r3pxD@Ņ().>,F2^x#y[/Ok/pzb 7Eqa~@m {q|\ W)8/yK?p^!^**********U-KGLØ$%sAOޤOuӖ|{?zq, =F?j7G`bǗ^w%~~Ã9^MC>?dOV!dt&M4(O2P8 U@x;|>L: $ʋ^pb|#wP|Ewt+>t2!R)cOOL YeN6t c?ch@˴`U`U`U`U`U`U`U`U`U`UKz^?D <Dbg:O|MTg|쵇>-4dtWг+eLީM:|Gg^]?`ڊ9}ezS;m6[כI\à}rK~5 |' ,{"tP<-^thq*H0ݩmseO?{r`Gx%K6hs0#o 0OYӏȏ-,XXXXXXXXXXR2㖥/ѧ8MEOCIꌏ6xd[zJÏ,dzM[^4lI/.,.bbA3x {4Xr`_ >싅sVPS{֞o>Ťk+݀YU|H|~K< f%_Of2If/<tȲŏנ~:G6o/Lds/7@=C3d e@f{ mȸeOYzt{@hT1c'o6# s-`gD3faVl-~)~l]fo}oc]]>@ŷӅGӵ$Ӆ N># z\l1:6 ^|/4=2t{-:A6Jc}Lx3&=[~ȽBFp!`*>ei/**********pU6 dz衽ڞ!ۖRUVOE0>+U6Gٳ&/F`X?t68;c0]h<ć-|3`珬X9'uRȲo@l-M\ʽz_ܲd(3ylݲR VVVVVVVVVV ܦ[ CGS *"@OL &cßt&/ k_g}1blوU.~A>/ۓ_ |: \/'_S>a3KD J`BeSl%'|'㻁|ʃ߀$Mh+_2z?@|uK$KO{6|F<6A:oޞ!Aǽ|L={N`򒭁L/«Kn@cjhSvF{=,>zڍdCN6Xӱȝ7/hP_>_2zzb}'+>#//dtJ:ٗdEϮ$C :@2^rgk_2S,"< A_彐p/f^&>yBF2,n]k * VVVVVVVVVV ҁ˫O6}~@q~OL⥇o7cy|&f'x,|Xܧ~SOrrxG%g%GF01`0uE,;8tY|z ;A+lM_W2e#w'XA4y/P3kdlgu-Kui dTgm-lK ]Ro}s׃Q/g $GOZcbN#EKzɧ?|{~rHn_|%֌?ccwSrAo}|Cwyt7QAGE+>OtbP$ݹ;)O.^l1W n_YrR~w]؇4'F2{֗UUUUUUUUUUm t^?nF? N NV&Oz*y)ѳw. db_UT>T E/+Лi⣓%_;3d^q+x{T0^Y**********p?Ud|cO zFzkC =g`ŷC~^Ƨ .O>d'3u-0gc`Ӣj8Cb L/ Sė I>?a6tGΐ_2~)K}M~zx5QSn@F}ʒ[^\XO zZvOigmƢWb`,{h:GgwCF}b7LkM~q<%jGi9?:=>TLm/~ O|&7F67YtasmS>W4,Cnŋf M?--Kl2?~x߽2^m 3dzȓ3V=nuǰ^>Y>@z ]nj^ݪ.f9WkAF}W Xm/@u9N`INtWh|5}N_h)X>@/hX+~>M=;E;*=4]?~BO_L[+db VVVVVVVVVV ܦC=ne2loƇx-}Ύ. S@09L^|7'+t67'[XlfMcCfM_ӆjmbFņ|MxO~;}ɈӀAAe JQLm/np+=So1ٰ ~qb-QxdXhr]_{ݲ2nc5K GM<< lKiad-=drn=9kAlՋ+M?tӛV2|ՙ|~/ /W6rO6sϾ7rqrvV1[D 4E)iG"䣢m ! f.Y{^<(h|'-@9^xAV^p,t|onYzavLC_Wدj.XXXXXXXXXX*p2ݲۖ~2|֛:{}eCv {vٓK=z?[Ayă M;[Ńv'cAڥ {}`x !YL^|-MwFŢ?r_zd|yg#w>/d2D=<2=C!ՂUUUUUUUUUU]W M,鵞ȼvuzH |Kh#K#<}(t1ء @t/d0>(_O=}|OM ղM? >+ܘs!/ve%9zQW:(~d4~~&M']s{Qf>xg_l )^6x+[Wx0{oחUUUUUUUUUUUi cHzVǶC ^U/zɩPEva/~͏ED.Ndyr)v`?e>\N$OpML?0SH΁sҋvo y׾ao_A6Hȣ.Y,Go9gr*yˋ6dn}{8;k̃oYZЂUUUUUUUUUUm 3!꭮zV=}C}( љh|$ȏ4ӻyU}s?;4O&/@$]U|<4|/!kӵMӉn{HoC h ~z|O}|<68 6ymeiw]! VVVVVVVVVV ҁL{]o֟znzL{8}uJØt#ԣʗnvSw/>-db9}gS 2,pAY[x >{t⿃oN,|+fqtnw>A3ełGٕw&??Ww{8ZΖ?<ϐYWlX***********Td|ң_ A+Ux 'l#Q0?۵;E+9Jz3po"62*!\EBs DaUR$7PH;=^ҳog,spųtOxt v;N`'  7\> xHH~pb8 \.X<0p_=>Ǡ%d9H C'ozG'ز|b8Ua'~eLhi' v;N`' x %Nol#^r:r fǽSȎw޷ُ(zE-.W \}NàG C}7.,rST,JnُKj\\q7s.z8zzfb9V1gorw"&WS p26K} ai' v;N`' | /_ >$K{dtoS՝ʼnIFGKU|:g~a)'yƓg݊qEB !@[N\c(r -oƲb¦zwkYތelq-] /~̙N`' v;N`'#&2z^oToɏzޏ|x8rd+t4t8(f i;XZDєӞq.0?,N`' v;N`''2o: ?;M{?>=e{SޝŲȭ^qޭ|w0ί~xN{[?8 qp?G}'\UGlXkVd~>DBQ ; ы%#zGcK&[>=/x>rʯclqYlpĠl8ʞ\LبA?d˥.dv;N`' v;9q~>!9ޓޜ|lޝlޗdoL:*N&O'{HQ.dza>ՙv=?E͏U;{{+C숞^m)^ӰY̡ª!9H,hqqACq{>v868W?pFO>⥗Goo8gPMqd_Y~e8v;N`' v;'SZ|_>!onEɽ[KtoSF-?etM ̏3=.Olq9uGod9G#Κ|O/$ea7ydaPstj(fl\>Qտ lzǏ; 8oq{T^tzS=س~2_=|dK;N`' v;NN)-dWޕ}y㲋'#xo7l-Y[&GWGq6qw:U}8jwċ!F՗>/,7x"jf{w(4"3>YWnrv!cZK;N`' v;NSZʒ2~)xz; SS/1ޕ,]}N1tX3?]d4s՜69bW,ܙ^0o5\N1!jGQj9_<-(#.N)KcbgQsīE^>?S]||-u.f//'d^cؿd K;N`' v;NM-d|BƢxSZ~`9ykz{_0KG-[ /P3;|G0NGG곡:^ "bG⳹+ }>͖'d8=!W_$]c;5O$_MH0; q-.xGLHNy8xl,> W|ÆU.¦z&ۗ?xο1L73oֶ v;N`' > |߸ 3mr,UAiO?]??9ޑ}xۺY 3oq|._z85<< x[/6تs?KŲSwQ\Pj_ԅ9)>?VȚB 0P1*&O 8:W?r-O}9US3Oȫ~1tyW+'Vb2 X v;N`' v'dx)W\[;{ћ7jכ2TrB,=lq>Ī-,9j#ujI.w/l=p׋xF:`W_,_v5`l$(P1`H|ré 6_|{L[lqr£W7^?K՗vÅ?G3GWk.`#\c2Ʋ v;N`'  <_B/9=[<н[ޑtos||q0-W`Wx:.{Gd1VQ}1lxN0t0g~>~rz9r8s5,dk?яn_8Me1!\YzN`' v;N`'O)-d|BdIK`1=xW!"ț{{Q=YN{ˆɁiw̗e,Նŗ{oAR '΁OK`#^69Q1F|' v;N`' | <ݟ~қ)ҽa=s9|sK.|6/dԻxX8oj{ա>>eq9?Y rxlC^52SŦ_ ^^iY@t|~ 9Eҫ?q-̏,5jf\W|xAK:~y/RmU SY v;N`' vB[xzO8`o{<_tqq'Gيcwz7{"q/~&MlzsG|{~: G>˟qj )y6'?VGh6b0iƆ%xWGL3{ cr81ꇭɔ#^&ZQ2?rÄu">9HNgʒ1,v;N`' v;?nOt!,}o^اUz[b9-Aaesć%aGɏ-VgNZx^M9ґ8~/2xt8HLzO؈Qk֟ьF`~EONw.8/Rqb]02[0٫nKb~UR_9W|x~\7t86eW ai' v;N`' | aU\rrŨOFgazf~L颋##S,0,'n~0'GqxkPx %.UM!#j#ap~p~WN`' v;N`'dx es'-doF֖&-A=3E/ew#0.UXx?|Ó3cV߻<M[}rTm:9i/}`vys'E.TVtb3$Ar:j9-g6zŢ/2E}}E-Vt&8prĄ}[nw|}Bfʒq,v;N`' v;O><eWsN\ܛ-> SK<7qNW?Rw3\>G#^2={5ت?g;oTp'K7SFGbQ`.MŢpćc/?Ĵh 3%-Ox+7lqS?u0K3;2'd,i,v;N`' v;O8' 7ϱfy'y[4ias\WXpI˟#yLwg :dyN)N# 77YՏ_(ie pT:i$nx+jWG>9d!WMq,=Puҳ#}%>~̞^\$"h/1<_Yz+K v;N`' >B5գoeoF 鍛nc⼏qoQ|aX6Q2;H-Lf=WC #^2O eSV8:d(bEdXP5%ӭəCRxXxfw:sb3G0P?zeʫ~⦜.ϐa8!ZҢgPk~ןOȘN`' v;N`'dxJ 7xʒO|޵-pfv[]ɏ'ё8T,ΏW'L&~j))7]Nrj%{7obVOɛX3V3)ɰ Q2h)>Q'E\<,<>è&:r[% ?zՔ#AՀW>z\st v;N`'  <׿g ox/ޒoxCmN'#wS[jp#Y+Y:c';ݙ u?|'wwKo^MyGjj/[X{:?#~(dv6|T؄@IDATb x5 ?ԻWoo 3N`' v;N`'I'2yaoX>$ޑ6t@tʖ44uy+N>yG#^_d1qY.Gwˆ.0&>8 Qlr_H^F5YlGЋţ5W|?|3<xr.X^esPK;۔.Gr%#8z 8>j񎞪s+K},N`' v;N`'xJ _Y!sko`w-Ypq|fCa!WC]L$8pQ&&{u3OX~vxvz.q]Ql>lRuc@qA{Ws=]T\?Te/'Ùbq1A2X/Vp$)T+K/!s v;N`' >BW,d~u:;t.Y5>-9c[uxˎ ^|I7B~xXÏxaWafN~&ch<5H+.1]e[.yKo WJVEo|-869OzLǖF=U.[qt}##^oq2${=կ~oo v;N`' ^ <2:C榷E|Nq>1SOfw,NpK+N+aȨxh#ӽ|B|lQAkJ/{W1-GQhb髉Ů#Į1>Yn ;wL]fn.&OKzRd~qg<;[Gp7~_<kwwN`' v;N`'L.d׽mOJontoL:^rJ˘bĒÉWly-<<^dGc>Ԝdyʯ^K>o@bφ+*`C哻.~K.&_afuEb.6#8W-<<~/Úpov:Fw y?!Tuv!s~rK;N`' v;+5'T]p]-zAĄ Nw( _z;k1gOɰ:aC鰓/?^F3f]pV$?%pzG&h淠qVM[.{=!|z~9[ lݡ^ӌ͇#݅>EW|2淿m S] v;N`' ^ 7\> xHH~pb8 \.X<0p_=>Ǡ%d9H C'ozG'ز|b8Ua'w}~ksz߅!,v;N`' vBf~eɻ۵7p2;[爗2A>-£+mc(<^rpUW!SoGq0QxrP_/a =x仑"7@czA5W|xq~(.W\5͜r˭^xtG<] `\\a~>j_a8ꕯ_8 ìo ջ8H<=, ;.,ܩwE FuO69%Ǐ؟#SAӞq/wy,/; 9w;N`' v;WaOi!o\﮳~sZ@޴##^v:7_ )ٛ^,{9ʇz;pspp•[\uVl\]}fECt8/$E X2w4vQdrӣ‹#/z=~Ŗ G ʆY_CM[ox\_ __s. kN`' v;N`'&2~'d:{қӢ-޻2IGɝaDA|d)υQ/Lvݧ:ӮG(\qwtbrxa xѳU+`SMYc;+4}v>˃9TX5$e 7>.=:c5s0xa3{Gr-_= )+K_<)?}S1 ?N`' v;N)M)-d_o~wc_-M=ޢ-]e{"6ro)à;doZHd~/Vqy|b+Q;zS'F8=g/v~2w~1|%/# ClMWI# UC5`f,¨] \xfӛ<~ip}H}#xztGfԛW,c,efzS3ʒ,v;N`' vBf|e_]'N1[P7.x2‹lq֒EOo҉!zt5{ g{^}{ɎS_Շ&^xGa^}aC}rxˁ/.f ~qh 0]@.-/>󻜥#(Y.^Pxq5uGz~}M^?Ԁ?pPywǠr1.d?;N`' v;+4׾;_coqY'{c珷S5ޣft2'{6X޺3.ap>džsuQFdSr)*!E !Xħk wq%k91۠`%wbի> lx/Dj!3btP}U*8<1|n Ǿ/컐1N`' v;NUSZʒ2~)xz; SS/1ޕ,]}N1tX3?]d4s՜69bW,ܙ^0o5\N1!jGQj9_<-(#.N)KcbgQsīE^>?S]||-u.f//'d^cc_UGx]ȘN`' v;N`'M-d|BƢxSZ~`9ykz{_0KG-[ /P3;|G0NGG곡:^ "bG⳹+ }>!>i|鞿Rftl v;N`' ^ <,}K_,y荌<E7fKX'7[{\q|Xv{,eS݉ շj%Gd[w#S9GwԾ;)h]Xvl],r:lG0f_ Vy ͚|tΟEg ~F^|w!c:K;N`' v;4goK>Awm5Sb'UxCON0t0g~>~rz9r8s5,dYw~c2N`' v;N2%tʒ%7/ń]'SX ob>DfIGqd9a.'b-1_5 Vzx],Gz>^>{=Χ~5Šz}[VPcd`kFNFb5j0_s|ԼlV_:~@SI1'䖫.8R#>R/wIxD|}Z2/;W ~w;N`' v;/B^uo Mהxްbz~KCO>e2]\<,O7rɽ}U8Xl,O\9GHM6DN zdt?Xǿyb˯Ɇ@/4Ï, @>z?~r՟GGl]M닿T3.+G %^6rVM2Ot}]I#1N`' v;N22&yg,yRئ|32/:[{{Z\@Í?9?Vӻaq/9sowz}M<FϮ~onaOE8|R:мxͳ9ɰ=E I36,1. :b¬8T #^ʼnU?l͈O0Ԋ&(OAr_>_}_Y5/څLX v;N`' |&`!Vɟ<{oKEL{8C̹[/ en*yޜ 0z˲9㰣+3w-|}LbH?|?O 2?ϞWRǖ.-]w!c"K;N`' v;6p2&w3J,}Ž)⣯0#>7qNᐺSaw?r٫VݸY/=܉n;_r<5JO>-oÏ'Ewi:.#>.{&EK-Aoyr+G^a%GQ^ ݱ>!i~څLX v;N`' |&/|u)$yRFLK?ϜjxYXxy7Gt[6?9{޲I rzN2^Įɫgod>[8l!sO|wb7e~Wn]ǸGI/#M H#t%7X V`W:$_ N.6^Gje遪+w;d|ld"AW}kB{+.dv;N`' v_ q~/%ɟ~/['&yRXlSAw7/h%ټ =[Fm_J/[ ]zA{!{KDWV8:d(bj?gs0.nMr4#֙.U?+۬8T^7ty }֪=3ヌ \^5ǧ '~܅/v;N`' v_ ̗̅g>);ܮgArlqs_e1oƕ~'dyNw3;He *Zl?_Ĕsě.n~J'LN71'M{LW^dXr(|UX` vYzͨӂ@".UvvaT^-AņKOWɟqGjʑ렉Q{uj+Ql|Zd~OY}esT̩[̼/5N`' v;NUBrw ~{.[Ɍφ}ex/ޒo-m!6ݻ)-^\5[tNoqz_[ܬ,VDv򱓝L[\>ٻ|;%7{G#^p5u#HB|JOBv;N`' v;/[X~X#'%OXL[s}Yһ'sWˇd;]ْF.|~y'O7|޳}d,>x;EpNyaEpƒ!D>ʖM I˨&BzxTcoƒO<_exߖKljW_|G}28}WnĐqG/C#Spx~~ei2ƾ v;N`' |Q'𢅌|_3%K4Sy7o!c!sY{r~dě;!\ ydo t1ÕgG՚!^|8@ES]F&jN|klda/1]vS>/ K=2£l9lraBie?3(E؝MqN`' v;N`'L~!22&[ݧ}3},d7)@͖Ia+-c)K'.;VLu;[y ~ay;Ola ~.?=\|j9|ʯ^"[?ЀXl5\QY.܅sK.&_afuEb.6#8W-<<~/Úpov:Fw L>!XW:%豥 OKx/_K;N`' v;N\E?ϯ,@_SWr?W'Ōx_v,7dT([ GH0Ĕ+<v8= c68ݡx6G|;x_W݋=.<{JxաJ|9215"k".;ruL6yOF3|jrWaWgfl>O.l/̾X; `v;N`' v;Wb-dΘ_EK_ƴr+.bDbM69,d,l>_??`}҅2MqN`' v;N`'L51G-cZ|\}ڧL~*cOX/@9ި޺ҽdq/'VhpP<.w޵7>y2z#WquL^S<>I .~ϻ(_tqq]-p=x7/p+_S}vT};~_v8tr./ezGpN!w_QQēGf9$9 gд'{d}Sr5: 9w;N`' v;Wa2-cZ/b=c}Ɛ'67xz_!g紀iqGާGt~oJػS۳7Xr+߻wݩ?rr!N1+ꈭ><|͊χp^H>7dG!zdDhyb˧GG_NzL-?1 - Gٓ܁ S#臚,t,2'?9eE΂E1_|2N`' v;N@כw4[|X򤹄} w2o=iQi}IƤbNcESM"E9.tis#^M8_^W1xGr0<Ď٪_0)pڦ1> *IJϋ9TzGcٙ=c~CGod#^zyt/}fG/g%-cE6vg2N`' v;N@ -Rz=q%OM9+KVov }78{tɖmȽ ia"QG=M,_\Cp"YoݡU7 5]&5, jV ,{ˇc8 v1pMoqWg#-N_c!?O=So{6B櫇&_Y?xCR/;Fv;N`' vB'do1zWGt.9[ܰdoӛtb]ކW޹jWᨉ/pųW_.XP<6#rꋋ9>s)xЀbˋd.g)-JW<6T{9?^}\|roޫ_|85`l08T||,d^!>! ڟC/ZZB v;N`' bxJ }k'eB'd<,“[zQ{\:χ͓ WS =,ooG8\occ9[(prW}9|\̔?D}" pt S58HL W굜zXlmPWUX6<sZ"~1tqj:>Wnr>̻{}JF1N`' v;NO)-dƟKy<{drz,Vfqs]'ĀQ7SGb!~Qrt篷Q_?W5oEl]&uiuH]2+;z՟ &^-:FԏGl+hs1xlq>!gW?)iE|%Z v;N`' ^ <ٝOXt|oJˏ,Gޗt>oMoqrޚeKq?2jfC騝H}6T>Y|z=VѫCX$ZH|6wEþϧGWt0됝9.U0j$;ϦA9$qp9|~1a/N;W.{%=;n W0)_ozgg;dNk+K2/ZoBTv;N`' v;WiOi!+K~e3God-*1[x:' |ޣG|0IJߛǎxgH.=NlU[,9"_غ9q]>=1=O=o@BPŲgsbqb~bq#Fbg!.;o))M}a;bԇ7*oذXTo䣣p-9>Ohwy_bEK2Fv;N`' vBfR_s-#N%A8wfT9-?UFpޱ1(\C/=-89ѫ% ˃Y;WBտ z󣙣«50ኑ_y/d.dv;N`' v;4K}2~J{g{i+ rq1ø,,¥ [cGwńpԧ9[a)g!^/\~_?]ٯ,]v;N`' v;WhOi!+KKo^ YǻOA|.ۓr;8\6$NOjA`;MhLj`p ʩy6,t>6-4>`~ bN-W '\>qFX}ҥ^?.V}ɉ2[v|Cf.^.ŋF2v;N`' vB^ui Mהxްbz~KCO>e2]\<,O7rɽ}U8Xl,O\9GHM6DN zdt?Xǿyb˯Ɇ@/4Ï, @>z?~r՟GGl]M닿T3.+G %^6rVMW2-[~_d~@ ;D|rWWZկ9z|B v;N`' Bx ex">i B N,#>,1;O~l?szjI.#xɗS=cAbzC~zFlZ]fL7C+xzv_KwybCѐ٪I^Mu[;㾈zo그#Xű9+Ks\~Bfi' v;N`'xxj C=þs%w088vu7mޠ~15%qb?y{Ó;|F1dxSgGQ%_azG0aσO0`NU>P K\իCFőF/9%0G?-@ ۔_S/2oOɯ v;N`' x es'-doF֖&-A=3E/ew#0.UXx?|Ó3cV߻<M[}rTm:9i/}`vys'E.TVtb3$Ar:j9-g6zŢ/2E}}E-Vt&8prĄ}[nw|}B+Ks3eMZӴBdv;N`' v;WiOm!xʒ,}޹7%"[| xM+odyެrqխv\l11ϖ,\^}ί~g>|ݏ\GdzjU7.~K&wbߨNo 5ϧ@ғx˛lQ1]Ee'^~v=if~K[ʑWn|l呧~afBw,dO|ei.\O"7~}jo2& v;N`' Jx y oɏz8l~r:(eKK6uN =d]8Wt'|pتCdr?-op{s[qF_F@ GF«Knr>N~uIV\lxU'=;W2w%Eb-&׎ʒ j9{w}Zoo/ v;N`' ^ kKwCyr.]U ^ v;N`' ^ <oq|e'dyNw3;HHe *Zl?_Ĕsě.n~J'LN71'M{LW^dXr(|UX` vYzͨӂ@".UvvaT^-AņKOWɟqGjʑ렉Q{uj+Ql|Zd~O [ v;N`' ^ <׿g ox/ޒoxCmN'#wS[jp#Y+Y:c';ݙ u?|'wwKo^MyGjj/[X{:?#~(dv6|bH@IDAT x5 ?ԻWjᱟ9Z v;N`' vpOm!+K9ޫC2posKDlI#ASW>䓧~\ى>?{wkUwDiUI|5$Q!wcU$E%ЈfTRT7A"x)z]gyaƐo26%Qk /2,Gwˆ.1]$O)C 'x-=؏$A ۅđ;xTcCi 7ÃG⋹ ۲㰸z)/ZzՇoS']xV #yRp|ry;c=U /9_:;d4_Y2N`' v;NusZʒ Yz7hɂ?ʇ!\ qdo t&{yQfNg<,v:<aj.lrY6ǘqI{WsqREG2LX=Pf aJ//0dj;:lŰz|tR̙N`' v;N`'xN _Yyw;}йddԋĶTuwˎ[z-5ëoT/!Wx?Ïxy˙tCKeA:_t{KmPjU@H9 ׂa1>tql娧e Gbx{Yȼq~ev;N`' v;s[Ȍ!seoNonzKP焍L=ݱ8]Zp]T<"ԳGf{Sي#|Oړ^\(# 'Q`*ڥldjbbX͉i>sWoF[;.;flzJInS~%RHprO<;[G p~eLbi' v;N`' x uo[oaSdқ-\ [^iX\8w,<ޮGA`&Owφ/^w5LqSWzt/?Yf+8#\|&hƷ>[,{=bշ#\gO>S,Gp /_M N 70lbYX/5N`' v;N`')'2Rwuzތ ދG|-0S՝9${#KĪp>\ٲ%0$O<9~6q-|Mj>j&?r\lqˆMz9kYRe9[A? ? 3N`' v;N`'GL-d\|eɟ9ި޺ҽd8rdg:v<(^.vXZ8|͊χ|$Iq/a ; Ò%O G/z=9 + ʆY )7 S#臚 [oxX_BLai' v;N`' xn }}Bs<'9;- ;-#ؼ/ޘtN[N4s?Y>/Nn\}3zċ͏U;{=gɇ=[^Oi$Wh4"|srՐ>/Zxq8AC㈷<qGod#^zqt/=-O=T+K_ܯ,N`' v;N`'xN }k'dy2ލ}78{tɖmȽ,!{ʉOG3=.6\q|zlћ:Y ?\9E8{Yoݡa?$W +~ߤ摅Aժeo0q,Q{;ͦ7qڑ'݁G!<գ<ݧwld翞W߯,N`' v;N`'xN ~we8~lAAGl޸dlq֒§7_==g-_}{ɎS_՗GM\z(#<{KF|[|p_5'%ǝqUp'󻜥#(Y.^Pxq5uGz~}\G~Ԑg(˃Oo qw *ޟ;d v;N`' > |_/x~GYgPUIi_|B[Ex7vxK>U=xt2'Tlry#O1}c9[㺨䍊ɦF'_!>r)*!E }CrUz1`ZH\rbb_%Wr ^eӏx>%R 釡é(\q`̵x[H0e ۔_ pS=_;'N`' v;N`':L9}BW,dKyu3j0>xW>&̽?]ՙ)d4c՜9rĨߛz怩#^Xyg {!~Qbk:['WᏣդd|5oEl]W.M4d}tvz՟ ?{N.GԏGlų [t׹QGt: /3_Y2N`' v;N`'&2nyʒE7hr}Ila ɏZ+_g!w{ tN@}>* ^GjWgsW*.j:d# U$B@9j$5M sHpk8[T<,??L>zԇǎ`e֢Zݣ|u[lb ?'Luכ٪k!C di' v;N`' 1xN ~ת+K:FFފݢ%w|ޛ-O{Op|,ytཟ"lT?nwbC-ڰ䈬~~;j;d/[?кdb9vXܩy2|570?pxF9;p-.x&ݡD3||RWY^1|}J@ő5ׅ6^50 ՁH|W rS_L6t|)ApL,.NrF˕$ . [:!1N`' v;N`'GL.d,3oE`Eoިe_otSq8_ X6zz_QxT{ 2 _qTSLrt{wʍ'.z=zGjHNeWCn#~>J 3 V/_On85ıɍ燯vuböS>zusD, cr!_OьQ՚ ˆ/BXv;N`' v;7紐K}_szx{ҽ#lޣ\|qbpa\ +ցGdza> ^ag Ux_2^Շ_NtqT+uy<\>x +;SM|YX&Ynuʝ_>vT<{'|z,tr|5b/ՄY|o޿dy-_:-4>.-H-V |pW}{ܷzl'dLei' v;N`' x ˗o-=ƒC|-.zw@?9?f__\71.Grgg77rċɧVr:$ų~ev;N`' v;7gW}7/n*yXoNKzopr9z˲9qQ~ ]+_>}LtǏK p3;]ӻӫ9jv؋&n|]p^$q0.ʏp92[rWSݖ"a~UR_9W-nHplNe鍣6N`' v;N`')'2y~{}KI`-6ptoq 7mޠ~p}M ^8,Gt>b6zoxr{>cC:;*,F}2*\n|(Ok;ꇩdqP .\9W<CFj#/9%2_b_k K;N`' v;NM.dz[[#vMN|^]v:޸dvXT?đχ'Wgb䭾wy7?atr=8_˻LC,V~XBqr=ԿQ/93y\^>͜-o ~˓#^1-7_r8[q9K3;2'd~זѴOyb=p#~pN`' v;N`'x]&L2>!s,'<'?|elޣ--MZ Z$#qLwV9٧\;ǏxV?~+6]G\CHH"t2HjA؆/W`W:IV6^Gje遪+ww%EA3җodK;N`' v;NSN-d\_=Wf@޸ُ>M}{{ҞlQ2;Cξ"^0P_{_wr#^2O eSO8٣æՋj= |T0¸dc59c] /ޠXg朹X#9P?ze~9ᦜ.ΐgGqCfEό81?{ͱ1N`' v;N`'&2ou%9k[츷&[;Ի/e )Zl3j9M)XZb9'\3Ofr\lIWQEJMi`׌:- O^q`}R˅G՗;rT^-A%ÖO.=!_?qGj렙ՐD9|j"W>~r=+Kދ_W/o\ai' v;N`' >xN }k/22>^Ľ%{xCmN'#w--:p[F^/n 7k%êN>vӝr"yݏ-.]n>7{^MqGjj<=t#Gdfُ6qp  n?.Q^|^X1H|y`xOrw Wa22=z͟\_u?p}U&]F&_.v՜3w5kld_yC:hƆt>W_[.8*'ijuİ 9 /N_YK{NgcoB v;N`' Fx },Pzs{cq~bJ˘0|caG<ޮGA`&Owφ/^w5LqSWzt/?Yf+8#\|&hƷ>[,{=bշm˞$9컐Fv;N`' vBf~eɻ۵7p2;[爗2.*îr `ީ|d;V|tQ(zE-.G$_5p#\gO>S,Gp /_M N 70lbYX|n_SO~f2~"K;N`' v;ss[ȼK}=3~9{3Z&x/"{sZ<C𨥄XroKF6oZ8L{?Ww\.pug~ėr<9w7q4%S!UD?xrM, _l*7f`GOeI&mggoY~0|^K)oawN`' v;N`':M-d|eɟ9ި޺ҽd8rdg:v<(^.vXZ;IW|N{[?8p?G})OyņlXkVd~>D#I#~ l?/xzT_xx>2|~11]8Xy`P6eObpGNQA?dzZzt#ԛ׳W}O~B v;N`' < ؽsdGթˣ&^xQ=b˥c#M>-F>T}^N@ɸ*w  [\|]R[ Unqlr~:l#soW>f^?pjȉA{g!g ~>jrOȘN`' v;N`'N9-dկ|~̴,“[zQ{X:χ͓ WS *{6u'.&|b/^ٲEh oTL65:/LCT97(Ҡ\IԻ|..B_S/[6(WX/,~ċ9|-ZL? NM_@ኍSgyOW:;GGwWΟx~D'{GLu#-Z\̨_#^:_O|+Kd) S_ v;N`' \&2~>!cs)-?Zz_5Ž/[%{k-W)fvy8Pφ'çcur"bG/}<8r-HzI<Х*P.ogӠE\~V}>ˏ^.6ı#bٳ(V(qV/ Sb9wZ|fCw݋ǧi|#[g?!sn v;N`' ^ <,} _,}y9t o̖,ޱNyo9E&с~IJS݉ շjÒ#+[w#s\}^l@BeaqɰKdXaRlre8HLq8x9.||-Wq8MfM>:*ϟEg _/~] Awm5S0*GP3xը.;ށAV6تs?īKal꩏;(~w5ь/_ԕs֧czL{?*Paqdut9` ($0|u8_/,.,js\=ꇡr@n5 Öw|fJ Bv;N`' v;? _}NO'oP^/Ht^}XjȍwQIaf 4r 86ծ1]Tlؖ#pbGn~(W_Ű|a W._p8?1 ZsS^r!zh2MbN`' v;N`'0 ׷/oq[ ^ag Ux_2^Շ_NtqT+uy<\>x +;SM|YX&Ynuʝ_>vT<{'|z,tr|5b/ՄY| >8 ۔_ ˿ :>O9[gv;N`' v~$LKys=:> X$5)g|tʒ%7.ń]'S@,E71"+NGqd1.wpyԇ||[ח\YǻX|5xQ1jVg#WtT)|:`Ad7VPcd$:j`ASb瓷rB~C߂$́bpȃ( ^w.F:e2oXt1xt2߅L?;N`' v;2OK/7={<'K$o4l8ݟ~->{S5#^7,;Lo#7ޱaGlsBF˅F6;/YzS3'sċdCt\W'W3"^|M6zq5~f×K ,o82?b@j^_ݧq1d?w/xu^>=[=6?O?}_J=ZpxIg߅,v;N`' vW¥Od蟄O8)׫0c!csE'[zZxx=Enqy'G±;}_z뫼jÑ t9(G_<{85{H%|1HLg+K?O^7Diw;N`' v;? K}_],I>j)3r 32ҷاUz[ޜ res㰣GӻV|^M1aɗ|?g>v<w}=W6bsԚg~ML7"+-<=;/ޥHa]r4d$g-EN/b^?8;:ls[rq[Б\؜0Ge{+.dv;N`' vh!6r_rKoe|$O*۔1w|9{;t [x:M7&_S;ϻX5Ύ!Q%=>ʥa*ه=O8:D]<5-C[x?vգO|v\5T}}GWU}Ɛd8H}r~wxqy~̗Կ?cO &|' v;N`'s\| _~,H2-NP>cn*X[xOzތl-MZ{fz#X4lr+g;Ձ=F%âꅕ'W|><:&oW 'Gզ?a/Q̄]eb" O+1]}z̙ȃtj9-g`Vన:xyዷLQ_+b:3'-;|}Bh S1Bv;N`' v;? /d|}RWOM9>S}e;;Dd4M+n>Y8pխv6 }̆|ų%Ðë?>{Kg[u^z637 wxy>j|[܌ÏO'gߥ8,*Pvry؋Ϯg4sZ3%-Oxň+|ŧlő~TO/̈́X\S]Ȝi/v;N`' vxGNPK|3J0)U oc98Q/ONglii渮XX'/~godS~U,>݉=~[ܔM׷#^_?bF_E@ GFW ϗ`6|;O.OАqzI{²FH9TzqNojC}e}=`ax>%M=?㨏CfNV/"&@Rp ُ!w)\xfw`s:g`@@m*?*儛r8C}֪=3 \\5ǟ2;dLyi' v;N`'sG-d{kK$o/ߖ+-P^/,}o^~'dqNw3;NjEtg8uʩ̟Z0ų9axt)֩X2wf#q3׌ᓫ=+f\b)CUTѰeS|+e5N %W\?XqrQ.դWGlKyKOWOQb:h(:d5+Ql|Zd~ȕ\g+KgK;N`' v;-d~\_WiaIy \}ex/ޒo-m!6ݻ eW[\^/n 7k%êN>vӝr"yݏ-.]n>7{^MqGjj<=t#Gdfُ6qp  n?.Q^|^X1H|y`xOrw Wa2$ޑ6t@tʖ44uq '<9S\6t9"y'O9f}\8Iģl~$ zd.$ܡţ5JK%<_exߖKy>|G}2<>r+Z0dɣCZxyxyR_S%W}څ, v;N`' vB/~җK@$|bO^y79ެ i9x?Zp|x *r5đQ[a"pf!89aՕcNgw[<Н'@IDATy\w~p,W}Tc_R 8쫹pC)_GdL&,~(jApN2^t\}SbW=>_Y_bϽEv!}, v;N`' vZ1/n>siyIsiı7;uw\xk}[rb[:޻|eG-=|I7B+xVGxͼ W^n:!d%x /\c˶\(}^z qOodu[kѰCxrc8rSu鲅 F _G݃ފONj,d8O%PO<_Y v;N`' ^ Xe ?|\ ?zw|ཅ;-aC63dv$*wi‰{˟aGd^:Nggas2_??WN]i24w;N`' v;k1ה:iyIsiıL)@͖IËO.O S,K.O;F͉G [y ~v{/ˁdGc>Ԝfo+zs/S|&$ ?5'w!/9LboՅ`[>a">#2~EWN?/م,v;N`' v.ߵiӂt}b}bȓ6{ z~ޡ37-LN[Tx{= O?Ɏ\r>TW-Foiw2,ܨzoqqrU_ F<Uߛ_ӬxEl5_*RXqMһ8{9xaW <iptّhr<db-<͎{XoEy+X 9|Gթ#^x|bˡ^ʍ7\y&ߍ})(,JnُKj\aQ\j͘b^bgi󦅏Vsugp0EĪp>\ٲ%0$O<9~6q-|Mj>j&?r\lqˆMz9kYRe9[A? ?Y|BByt9ܖ.1.dLei' v;N`'x&2fw|~oToɏzޏ|x29tAó M;]/g i;w-O\OՅz[&<> g%<>tG]8\a~>j>?a ׯS}vT}\ïoË;/Gp^}#yʝ./\SՑW<||-9~ݖ$=e{SޝptvoŤ;b߻w/S'j‘}e鋟WTO~r}J棖/⻐1N`' v;NusZ2XN ƾZԛ{ѽE[dN6ElSiD'xqrAl8>G=M,C"=7dP0|H[oR jPzͲ7ٸxy GF^fӛ8~ip}H}#_^So{6_B+_Y?|_Yم/v;N`' v}i!3/'ޏޘ-(ޜ-nZ[&9GWGlpv\5Qu򨉫W#^TagXrC}bxU.fS4P2]@.dx~py%Bū[>F>[[կO8rlrEuqyPzYȼy|BF3?^$% _YFv;N`' vB_:c-=do}{,N|dM.o݉7G8XocdžsuQ9?MNC|0SUC 4(@ >._c/WŖ JVˆ"1|_K2CSAW-v9Pb.d,W}ui2~K;N`' v;ssZȌ?{~YogԒa}΁ +{3;}L{\3?]S>hƪ9s尉Q7SGCLVńt篷OG%Ik9bٺ\88ԥi w\8? ^-:\1e؊ghs1|xlu>!e,ͅˣ.M'dLei' v;N`'x&2fw>!cs)-?Zz_5Ž/[%{k-W)fvy8Pφ'çcur"bG/}<8r-HzI<Х*P.ogӠE\~V}>ˏ^.6ı#bٳ(V(qV/ Sb9wZ|.Cpүt[$w!cK;N`' v;:紐/| oz##oEnQْ;?y>͖'dyg=z'X8]<:bX6zy~\;aVmXrDV~cnT~Th]H2T;,<wI 8 !.绐1N`' v;NM9-d/<;O}o[oMT2 3o8>T.^z85w`PՇ \O>qXt2z;AM4W-u)Sާď TXYs]a5]X 0|. _8:W|+8d#K''W>"a+Gj\9[Mr°2'd/~M߯,]v;N`' v;ht!cx+z{yF-;zS{ZN ̎U[ᜧCݥbݻWn=p1;N:L9X-kŰ+_ g?jO ?N`' v;NYL9-d|e ۖ$߸޳weLA|IGqd1.wpyԇ||[ח\YǻX|5xQ1jVg#WtT)|:`Ad7VPcd$:j`ASb瓷rB~C߂$́bpȃ(Wt|䤋xDaw~#<)%;~eN`' v;NusZ ->!)/7_ Xzy0#^x!Bry#Wۗ=_)Va9Ej!r:.OyGFuWLMM/& ?b@sċҋW7Yj v5b/S͸2^^Ļ:~y/RmU'dZhRטBTv;N`' v;mt!csE'[zZxx=Enqy'G±;c싿~W?&HlrG|{~Nx}⋟85#h^ٜdوä-9:0>*p[`f'SxQ9Ԋ[Ny8X'TxyE7/]ȘN`' v;N`'M.do͋Jo{֛\޲l|`vh?czʗO0rxӫ)&9,#/'=NFlZF䀛~Eg'Żt {7 #\V}THةElgW~xUn>oq ?mޠ~p}M ^x=#:w17<=Lj!OC >K.7>{|KՃ5T{,3q:t82HyjZ> ? ˇ񙷜~0'G|kPx  իM!#pdr|/1?{j9>剛OaO-dN`' v;N`'L.dz[[#vMN|^]v:޸dvXT?đχ'Wgb䭾wy7?atr=8_˻LC,V~XBqr=ԿQ/93y SK<7dqެbV;>fQْaGN}Ɠm8]ur; C4t#IӤ/"2j))4٦ (_NמF*i:F-@ŜkL^ zNlt3 Qzoy3~>8-*&Kq(<pϮg41-Zo ~˓#^9 /t<ԏz?LL莅 ,YĠ]\cv;N`' v;|t!2oc96Q/ONglii渮\ީᰇ?G[uS.ߝGMnonx}?╛aW4*R84^M7<_ra5vz#NАbxī8X:ؑq}WCfFV/Y.k4qW||%CX v;N`' vrm!w7޿7 ~LxorqǸx0PqOS,(W&wzCW+{k!\/ާG}T2{\}vd"Zi> *Y¸dc59s]  oXgbN|V}ZlVPySܔ2L~v>dY^Z̼ʓpy SX v;N`' vni!o^<_Y oӻ̎{kuC+":gqꄩW-19񦋛 SS-d~fM#ykj&ӕW38a=$ ,^.KuZ0(9?\y`}R£.?jҫ#%\2` *83WM9r41a?Y }%S̯X+KgK;N`' v;NsZȼ/22:^Ľ%{ߊ׻ۜOFtR8({qlс;%yGlqVXuNv3&~lqHOdr>xxH=t#HB|J{ձ|Hƽ#mnޕ-i!9hW|ԏz×=;1G6b㎷\ =1]'<8aL}^q@l~$Izd]HC/j \PZBXG˹ ۲bqz A-=poS'Oʭ2%`y;c;z/ݿd$K;N`' v;NO9-d|e1B/έY{r~dě;!\ ydo t1ÕgG՚!qQЋR8:{FRZ>}/?u w/?^71ai= @eA:_qُ".r!{\zZէ7P-RxkѰa̡xrc<0:tيK9|vz+?/˳%CX v;N`' vrm!3~7ϕ;-aCSlOԓ(r#2*^>z6to{;9[y|dI{?8b]l62}515VsrĴ`<[\g# C~v鲋u-$ _~IoXq/gga}2_ܯ,I,v;N`' v;?`t!ӟm-},Pzs{c'֧WZS.'N\w5gxTb˛>o/,o-2 ď';b&'˛W~焿L]ep +~6\QY.܅w#^r1 0+.3u*0jxyִ~s߰d5[xl~~B bi' v;N`' !x As/;EOx~ċċ CLzCe 0$OLlpCl(U}w^=.<{JxաJ|9*15"k".;ruL6yOF3|jrWaWgfl>O.l/ޯ,A-v;N`' v;?tm!Ro;ңw&ŽtRT1g>فM,WS]\Foi;Y,ln^[ظt-d_Y:Z v;N`' vBf~eɻ۵7p2;[爗2A>-£+mc(<^rpUW!SoGq0QxrP_/a =xjHǑbE 1tI͠+>8X~^+fN~V/<#GCϖ^8{}S,G*&^MN$VjaBf!,v;N`' v;O9綐y{WfL^D.x8kģrɽ]c/m޴b81aɝ!W>plُR1|^'d$v;N`' v;0綐q%[xzN~K~Óő#Kx6XiA0[hLyzעY;/zt\]1uO_o%<:.`Q0.հ?{x5}/ް ׯS}vT}W}p]] _${ CԻ:p'~;?;!⏦ Oޟp73K v;N`' xN 7|zŽ{)R`NqtvbIgwV8~V>;IW?=/x>rʯclqYlpĠl8ʞ\Lب~b OK㵐Z٧U?ms!?;N`' v;N൘׿_3 M3mr俯,UIi^q?gyIoN>xN6&0C'w M'˧[?2G0vLlrҽ=╃g!vDVbO6eOiE,PaՐ$}^4x8p舏} ϡ8 =;Oz8z#'ˣ| 73{8,}1ܖ"ZMU1>\\'dW v;N`' ^ Qտ lzǏ; 8oq{T^tzS=سr2_=|dK;N`' v;NSN9-dWޕ}y㲋'#xo7l-Y[&GWGq6qw:U}8jwċ!F՗>/,7x"jf{w(4"3>Y<7O6\M9( lug\&|r lُ0?ɦF'_!>r1SUC 4(@}C*O#1-$K\rbaAJ^ŪW}b#^$kBfũ; T\qxbمi-v;N`' v;?pi!+Ko~)[xz; SS/1ޕ,]}N1tX3?]d4s՜69bW,ܙ^0o5\N1)jQj9_<-(#.N)KcbgQsīE^>?S]||-u.f//'d̰ev;N`' v;s[ȸE7hr}Ila ɏZ^)fv8d|oa gCŧcu0ċEŎgsW>|zyzK ]MH*<Х*Fd[4h0$.V}>//&=,6cGbe֢D|Gxuí^69ኟ:s~Mlյ!s v;N`'  <,KP2~[x##oEnQْ;?y>͖'d8=!W_$]<:⽟"lp~\;aVm䈬~cnp_?ux2 CNͱŝ'u]& \xF] G#x~$<< 6CGNQca*caSYw/+F ;N`' v;NL9-d/չ8;O}o[oMT{gI#P]\pjT+V_l$U2WeK'>ݡD3?3 V'7k`WtQŶ''cj8vėkS [$(W<;=xq]r|Gra# p$V'Q|t1a1a|5rqėjY|i,v;N`' v;O?紐%t~%7.ń]'SX ob>DfIGqd9a.'b-1_5 Vzx],Gz>^>{=Χ~5Šz}[VPcd`kFNFb5j0_s|ԼlV_:~@SI1'䖫.8R#>R/wIxD|i,w;N`' v;sZύ->!)/]Lo#7ޱpbGlsBF`F6;/YzSÙ+Ɇ#^^.W0R{}wjpW$޽ī"f_Qq걩1N`' v;N`'N.d,_ydK#^ Mz/~r~8vww,7grĸ<F7xpě^3F$QkOU/X0Laq9^Glfš l˜,N akFl}2ɧV0vGybOSzd K;N`' v;N3]+K:Ǜy7iXoNKzopreaqQ~ ] /b`xӫ)'X:Ǐ%_Oq]_9jv*15w qӯh~.E=[ #FCf>&9{5m)Rԏ"CV?G Ϗ↎`,}g ai' v;N`' | Y8[bpc6+-Y <8_Hݩ|r~x`n\MQA7Ý/9@jO'7Gᓋ٢b4NzFӢ% <9#ܰO#OTO/̈́X\B{_ߞ֗v;N`' v;3g αfy'y[4ias\WXpI˟#yLwg :dyN)N# 77YՏ_}H}i)UNF /m;ՑO'YzhHtrqz+K_?v;N`' v;k3綐1;d|ћwzf?{78c[T<_|')Ml̎`+Ry ;Y5C.LSBԃ>=J>tzY4,Na\]21ݚ9.7hvG31'q]>>WY+|q)n &?;w2U/-zf}Awk2?ϮOd]-v;N`' vB7߼Fw28wm ܻdxzWEt$2 S-Zbgs9M7?\ZrՓGG&L=+f2,9pb{H_E-X6ŧ]^3`0Pr~>GՇ]~դWGnKd~U qgQr:hb^~pJç_/_Yi˘7_Rf2' v;N`' VxN 7xCB[xc:ދd[1ސzw{Ɉ]e/-:p8\/o-nJ";Nw&Dpݏ-.]n>=WS^Z)cIVh6oCٙ'N!ЎAKǡ9Bf?8aw*Q\8ŋ}BxW_xaL}|e'd}?OnӦEL|2秹 v;N`' Vxn ;_Yz^u,qH{[: weKyH'j񎞪s+K}_?.d)?;N`' v;ssZʒ_z7hɂ+7w( Cި-@b"1ű+'Ύ51٫CyØų; [=vP0eaxgH~<$({]T\?Te/'Ùbq1A2X/Vp$)T+K/}>!:Mާc5g' v;N`'x&2m;wF9as'oRE{O\vn8Ԁ'Nz>GQԇ<OxO~ˏ z 3{tC`x@kW\c˶\@:V n#bZp4ls3\.-;z1]'F_G݃O,dr+K/EOxv|2XN`' v;N5s[Ȍ!3FoNonzK1]oթ'u,N" 8yޮh~*O<"qĕGf{S9[y|dI{?8b]l62}515VsrĴ`<[\g# C~v鲋u-$ _~IoXq/gga}2_SeWՋ:휦e.b-.dQ?;N`'c@IDAT v;k4g^>E(ҽ1xa+-c)K'.;V3N<_M{Gg[Rs+zs_. 8?bk eOB/|ow bL_QkQ9ox-<6CO M?KK]ȜN`' v;N`'&L2[x_v,7dTFek9!>rLJQa8HptP:׫}]#^{\.y ;C6;r#U4cjElE]wn9l f~ l48§î %=|8]_'ʒe\Li,v;N`' vB :wGLM{}cœ| &X鯦޹8҄ wX6(d̷vqy/&F>Uߛ__1rX}0qÏ[|O%MAs 3|-c5g' v;N`'x&L2+Kޝޮ:G)t8( i9͎{XoEQ􊼕[,] Yz;A“z o\]X8yKTF>ST,JnُKj\\q7s.z8zzfb9V1gorw"&WS w26/}U%}xIg߅,v;N`' vB_қ2{޻8ሗZJ%v \2yӊ~N${#%wƪp>\ųe?KŔc-K›l-Οdѯ?{̣*~v!cK;N`' v;s[ʒ?{sQu'?{?ȑ _;>w[Cl8tr../ezGpN!w_QQēGw|٧N~޾6t/4l'w_?(l~eiN~N`' v;NusZȼ,d7-􈗝M{w{@^N:?#z{N:;G=\n|0)F?%pW'c=GWYG#~l^,;(~^2Q}S~eO ?b˅#eQ,w`FԯCM[ox\_)2"SșK)2f2& v;N`' nxn 2o=iQi}IƤbNad >twpB&Sis#^M8_^W1xGr0<Ď٪_0)pڦ1> *IJϋ9TzGcٙ=c~CGod#^zyt/}fG/~_YzOΆkG[tC88գן82{8ݧwld翜W̾J21N`' v;N2紐_YzWGot.9[ܰdoӛtb]ކW޹jWᨉ/pųW_.XP<6#rꋋ%>_r)xЀbˋd.g)-JW<6T{9?^}\|roޫ_|85`l0Euq8zY|o?яFv!sa v;N`' >'xN }k/oAoqY'{c珷S5ޣft2'{6X޺3.ap>džsuQFdSr)*!E !Xħk wq%k91۠`%wbի> lx/Dj!3btP}U*8<1| _/~ӟo?!sMa v;N`' >/h!ſZxXz I%O۔c|esgQK)qblqs]'ĀQ7SGb!~Qrt篷OQ_?W5oEl]&uiuH]2+;z՟ &^-:FԏGl+hs1xly>!e,?u%2 v;N`' |^&`!?WR2'U}3BOXt|oJˏ,Gޗt>oMoqrޚeKq?2jfC騝H}6T>Y|z=VѫCX$ZH|6wEþϧGWt0됝$A"s]aHv5M sHlsPbbbS_IJU[,9"_jͺT΃ܩw/S`/[?APŲgsbqɳ1Md?5hHt`ԗz##?p~ ?0ԇMGNnՇ7*oذtW16՛5(?{Eˑ?'d?wBI, v;N`' >w] wib)/gI%OM9o^Z81s-#iLp~>r,?U篆Kqޱv\)G [u.˖NfSO}aGqC=f~x|ROczL>>?*PaydutbD}@$waX@xW?r-O}5奓ɫ<gH~1tyaF˕rIK\1FG~g cޅ),v;N`' v ̯,ͅK6? W'WŌeƟ `Goިe_otSy8_ l&bģD稁<])'\:Gq> cz)^/8^}|Հw@̂h r <6x~ծ1]Tn-G ^x/QW_rQ~b xQQ+\1lB'?ɋ_zh2MbN`' v;N`'y@׷|u)$(/9Ո;dRoÿݻI6gGN rV|刧bqyXxDS/l^ÆkʳtT[_2^?L1lb&_o$wixʩ&>,,¥ [cGwńpԧ9[a_){g!%~pԊI-Oئb/~k]cu?;N`' v;N5@ }-IտW_˖I۔c|BdIK`1=xW!"ț{{Q=YN{ˆɁiw̗e,Նŗ{oAR '΁OK`#^69Q1=C+!G-]Z$dz㻐1N`' v;Nu\>)ӟ֢Rg }!WOŇ77e_S:{òM{ėrr;.] =m.^Ȩwq >qf'%CW}|`s8>q"59'6ѽdjU$M/& ?b@Ňsċ˥W [Yj v5b/S͸2Ļxu^>=[=6?OG?z; z?B v;N`' F_X;կ~/b~eK#p|9ޢi-=x-<[<|{`?=>x/~r~8vw:H싿~ëzp;=;ÞéozgGy6'?VGh6b0iƆ%xWGL3{ cr81ꇭɔ#^&ZQ2?rÄu">9HNg+KEGK|l]ȘN`' v;N`':MBRD-I|RRfϜ `SbB_Y9޼ȻOzsZ{-KÎ-VgNZx^M9ґ8~/2xt8HLzO؈Qk֟WьF`~EONw.8/Rqb]02[0٫nKb~UR_9W|x~\7t86e+Gei2 v;N`' |^'Q Wg_2oͿyD}9bɓ}1w|q%w088vu7mޠ~15%qb?y{Ó;|F1dxSgGQ%azf~L颋##S,0,'n~0'GqxkPx %.UM!#j#apw~̗ؿw2 v;N`' |^'Q {]2$/(Z\R)8W>{ROfdmmi3ÏؑX4l0^]v:{ѽqbQW3W~><:3 nW/GꓣjO{W(˻LC,>Y.rq=ԿǨ˜'OVi9#a .UW|).j"31ʼn#&#rC|2 ^1N`' v;N:G i_1dr+~>~8޹7%"[| xM+odyެrqխv\l11ϖ,\^}ί~g>|ݏ\GdzjU7.~K&wbߨNo 5ϧ@ғx˛lQ1]Ee'^~v=if~K[ʑWn|l呧~afBw,dO_Y:X v;N`' vr 1-F1?)^9NgX[XNx3N~KeټG-[Z9+Wwj8$OzzF;᳅V<'w'oySۛ,^Ǐx~>$4*B'#W ϗ`6|X ^'~=4$:8{=)Nz6vd\ՇKZM\?srO?~G_ܛ/~yi' v;N`' 6 g?՛kJ_җ.9'3J%_=ތ(71y.#/ >Tܓ&6JfG0`ɝլ{!{K)ljQU%W]l:YCJ0.nMr4#֙.U?+۬8T^7ty }֪=3ヌ \^52~/K. |' v;N`' h!w?-Zd2'.[ŧ}37߼L8wm ܻdxzWEt$2 S-Zbgs9M7?\ZrՓGG&L=+f2,9pb{H_E-X6ŧ]^3`0Pr~>GՇ]~դWGnKd~U qgQr:hb^~pJç_/}V_Y[_X v;N`' v,ddwKhÏ>)9'W}e['-2?_nos:?ѽKآUEMZbAoNw&Dpݏ-]n>=WS^Z)cIVh6K5;301~`]"\|^\9^|8t~0GP'lvC< xOO5 ?,bw,}էU؟?LxO yY v;N`' ^ |_"eL˓O]Ogһ9ޫC2posKDlI#ASW>䓧~\ى>Ew"|tlx0"8 c"eK&#Iҫ&BzxTcoƒO<_exߖKljW_|G}28}WnĐqG/C#Spx~Bw] &|' v;N`'qQЋR8:{FRZ>}/?u w/?^71ai= @eA:_qُ".r!{\zZէ7P-RxkѰa̡xrc<0:tيK9|vz+?/˳,1.dLdi' v;N`'L_/ſ}Z|X򤹄}{ wzs[x">8';'Q8ޥ'OGdT|4lv>}!>r(ߓ5r}=ǫwqT!1*ڥldjbaki>y̷F|e렙[|=Inް#(<:_֑&/,d迨W~߾я~tʞ>?uw\v;N`' v;h-d+K-_<'3< ~4l8XghOolSdқ-]:O\[^iSL8Xr8qޱbSؑ[x yb #vgG:SK͉O曽W~焿LyHŊg*Pv9.?#^r1 0+.3u*0jxyִ~s߰d5[xl~g  hr-]-c]ȘN`' v;N`':M.dX o >rB|//. 1=,U-<<oWp<1>ųtO3ʒe!c) Z v;N`' >7xn ~ޡ37-LNbO?,hbѧzHN6zKv'b`p2<2dT}~o~|>.~/Ol/W oqPe]=#^xxa3 z|AKFr4N.C}9-O!eȁpbfO?n!UY,ƟgQɧ\ [g?!sn v;N`' ^ <Ӆʒwkodv/e vO!(nv;x}. RMoKWW \}NàG C}7.,rj&?袰O.ˆ_l .fa#geIx3mbгt ~>g?OX?>%Q˗K˚1.dLgi' v;N`'x&2fw^oToɏzޏ|x8rd+t4t8(f i;XZ=e{SޝޞŲȭ^qޭ|w0ί~xN{[?8 qp?G} '\UGlXkVd~>D`qAv4KF1L.|zT_x|d_trAp=>Q1?P.W̩?_ė3Fv;N`' vhm!c+K>!9ޓޜ|lޝlޗdoL:*N&OO'{HQ.dza>ՙv=?E͏U;{{+C숞^m)^ӰY̡ª!9H,hqqACq{>v868W?pFO>⥗Goo8gPMqd_Y+Kxyե]ȘN`' v;N`'&2o{wZx7բ-%[v)b#2 COG`Al'غ7u~qa#pbgM'wW}! ~nbkMjY]JY3`qFb+6N>ΆkG[tC88գן82{8ݧwld翜W̿ˣ.Btv;N`' v;4紐_YzWGot.9[ܰdoӛtb]ކW޹jWᨉ/pųW_.XP<6#rꋋ%>_r)xЀbˋd.g)-JW<6T{9?^}\|roޫ_|85`l0Euq8zY|O MWzOOkI kN`' v;N`'&2_^xK}:c-=do}{,N|dՔ`[w8l'ۘ>zΖ. ܨljtU_#3eQ>xߠHr7Ի|.?BUz-'[pUzՇ! O?ErH-d_ ]j@ŕ'b!cwg?ۅN`' v;N`'&2d!s~_9޳Ψ%Ôԋ8Gwer7cvS̽?VOׄ>\5',M'= wW? 9[6SL(=*qFyK.eGp6yK@Xԫ\pt7jϏ0~T=b+-_EKwćKg /3|eIIL{ql+K v;N`' Nxn ;9ޔ-Xx/|ޚޗ- 䒽5Q˖ b=e<,ߑ-Q;l|z?WxH~ؑlzÇ}O/\ra)!;I^ET¨pk<-_٪#>䣇Ŧ8yH _ZVn&'\wT{ί~靭2!sz-Z,\~_^}){e SY v;N`' ^ <,} _,u荌E7fKX'7[{\q|Xv{~é~qĆ[n[Œ# é~;j. U,;}6.wj,wI,70?p1at)v6[\֏p~ ?0Y9#F}| <\MfM>: {^ђuLKw}O~}&.d~N`' v;N`'L9-d/ՙ8;O}o[oMT{gI#P]\pjT+V_l$U2WeK'>ݡD3?cj8vėkS [$(W<;=xq]r|Gra# p$V'Q|t1a1a|5rqėjY|Oge׿~ٯOBTv;N`' v;mi!+K%Ko\ YǻOA|.ۓr;8\6$NOjAd;MhLj`p ʩy6,t>6-4>`~ bN-W '\>qFX}ҥ^?.V}ɉ2_x~LK&w{{]ۿ|' v;N`' xN ?{Ň77e_S:{òM{ėrr;.] =m.^Ȩwq >qf'%CW}|`s8>q"59'6ѽdjU$ Y:DJwhPᢀ48h $E@2L6BK^X8v߫enQ[Ĥ"IUT}?zԩY=<ާyȚϯC, @:|/= r|27;4= rE^]ݧa>h?M w/^ \Wu;|d_OXXҁkt!cq \ɖ-ӵhƋ~ YvNs9뢯Uόa~0;>XC/SAjs4_ɜhs! 3̶brx&/bOs 3Fc#V#LŤ+ʷvG~lSɿZB[[վ`u`u`u`u`u`u`u`u`u`u`u+^'}5q+-z_Yz8f^`nifCYh%sCң-VgZ҉c|9Dgxћ`>/`\_ g9rn`B`7%M=>9Pߥ|lzo&.&:y9m)dE CV=*6 _YeGԹхW['do||K9x []|23衞Mv}Ladfxt<}~A>hlG|ņR_Ś ,DB=g]8-!}x[p/pgxgWSrk ,bh ?Zn ?:X;8!sZЇ `h3lkVlAy2E~u Z1ٱ'̇MťCoOxm-ddS1_Y򑥗Ӝ )⣏0:󦙖'gf+old0H?Y4t~pa<3?wO^c!!O'AGg'f9Kax0ť#?Ӣ-Aoy2͇_Ŧ?&=_LRO̵+K%MX::::::::::pu]xBXNg<؍̲dQ|˖&-ljekN-yq~lF;'+Yy3;iq-xo0ߣˁ"AIhUtEX5_A/y-5$<:0y5rN,KPd@]Ѱ/&OG//`k4Ǖ8Yy2`u`u`u`u`u`u`u`u`u`u`u)vZ[ȸ=z+-P`7cx39;1leOW : wbK6&bG3 ;嬆J|M=ؠF{J(|bǑMDmV+ٜ@~/&bғ0}AϹEwXC~|̯m?{MхOB{.9> \B67fM9\I;-LӗrӕMdN6t)_4Y}s~sهNzm>p9#NȎf+X2gy ,bit /vr('<|[g[<|n[N>|0ȇ4`u`u`u`u`u`u`u`u`u`u`uivZZL?{m!q̘y6K6߲1Ck6o.u# gW,.Z7aݜ+<#G;-&rn]|=-,Q(Qwh.2R;~P6DK// ^tчq0ܿ(hr@9fWٞD8AW]pow׎,W2C VVVVVVVVVVVbk,=4нWˇh `+[|˟_v3?}|9=g#\]h6Îٙ/ n.m N)}藝 Ad|N̶ CwA˾%l_<>,9C-%s@K;۔IAnb8j)^vtby15 . 71/]% VVVVVVVVVVVz,2/6r1?Z^f.?M&;Ir1˃سcϞqf␳rs6b^9^ɏB*0}AD wZWq.ʮx5/Lɋ3lٸeca?[KvN6hr;My!+^|diRƂՁՁՁՁՁՁՁՁՁՁՁ؁ki!#K2wc,fM0^̒mK`o.㒃}ⱃx{YǞ'Olh䦇o[쪱ɋG91(@+O]!ڀ˶\(a\|aʏhy[ roKׂf17}GWo5#K.F5g6YvxucÇ.AA^m [^Y҄OB'%.~Kƕfn|Ks(s ӱhr$(x32: f>y~hrPN.@N;NxGpv] J&[It u1"WXa҃|]!|24bWݡxC:`;D)}q@IDATIoCàxx4?;g{r2>pޱq}ditb5gmS`Mo·WZd/L]0{s,9g;z7sb웗|ud|1\rڊg.yZ' %~.2? ٤˷Y^vg[4`u`u`u`u`u`u`u`u`u`u`uvZ[ȸ/} ͙͡xL dMSЎ9m!ڠxrM0WX~ Zjfo1WK#{rKdk@!:uIŀ˾xazx].r}Mη|;adW=2ͮXل?Ƈ^~'`nr!#Bf}&,XXXXXXXXXXXx2q{/^h`^ysZ< rӵAK f23-07?whs0>tζ:`dy^l6D9wl`L~n@70cŤ]T||m@6쫏Ƴbg[˒Ͷds |0}.^gCO~z=!3:`u`u`u`u`u`u`u`u`u`u`uitZ[ȸȒ~8fTG:8:t' r8 \|ؚkA3>zeN6t+9=kud'g&?ݍ9 <.`QP`]`z: r+0>{f7_`zbh~`[<{jggCj;^\2t{y_w 3 W?IKNՁՁՁՁՁՁՁՁՁՁՁgB{毱1?8- `3-l~o>&ӛ)})Lϖ<"Ey!3-&9SY _awtl|rOVl7 gL+LN:T*`K>_ȋtsT|1C\\;2qܯzQ>z/Kq}#K7,iǂՁՁՁՁՁՁՁՁՁՁՁށki!p{Bk!cnEq7tIo6dfʹbA8?1c]~t|dL͇]19f=;?G@K~X⁅A媠%1sXr6⊗m2񣇝^~ ǎ8tݏ<)ءɋ}M~ Ovk,dnx}dI<\K #K}'N̏fx@f%gpf7d-Y[41ї/gyŇ5oW\9Ay WAnP !dO^~RFr#(?NQ@J w6l͞,%\=PyGHˑC>/{I7g'0) ׎- ?WVVVVVVVVVVj ^9tܹY< h3vpK1:|h:8L7d|@1ȓe֝q?#9Yڠ I&G']:t63MJpFqQ Ir 88,H3+ vq6-$]rbE5J|-? 9t-Zz6xvr::E..߰xlY Z:::::::::::4;p--d|dBf|/}8Y3h0=ac̖y3;rvd>j.tMGW9XlwyzQfs>Tk< W[O/`WӅW%B%suAn`2~KSCa`󂣻 O<zG{Oye[ty1#r^3`ʒ.,XXXXXXXXXXXxj2n9x8fJˏ,KC=[\}2w>燮Vx vF&tB[h07]ztȟ-=?tbώ9`C/y%=7TKƧ)O>^jS;Yy-dwȌ,XXXXXXXXXXXx:2>t…~efd`Vtf̖,X'ә7[ɟytN ao~ S߉ T7smfWxd ]WPm:#a!: ;-}B%őGu]"5 Ά=]L v-.MO~0d͎/F~1溲}lXؔoII_֟OBfRw~8fKsdO}YӜf`zsfO roG+L_ͱ6xfk|Yy6#y0[x4|9AN0\9'!:מ>ЊB5bՠlM4{f_vx/Wl~I_99]5{_res.x6Cf=! VVVVVVVVVVVFхegcV4ͼA2fx)?[2|5_A6Au,9rtr/vs 0{ !:e@xI^~t&ogѥl%!kN~dbٗCAٶaǧxp"p9HO˿ <.׼).іOBƗ2%4x[Hٜ,>=9>]|1S9پ~,dnZ?{5 VVVVVVVVVVVz,+K$/ޘ'Y\ٓ)L,Eٛ= Lͧ7 #lfY!-kWXss1r r|jV'CxP ɫ)x:ADWό{ C &pṾ[Ł LWܹtT.n>Ӄdّ;XnzW=3k&@db5styzN}Zυg |'sңv̅X009O'#K-Q,,|-dn/zu`u`u`u`u`u`u`u`u`uj5+K/ ̍=l}5sZh%sCң-VgZ҉c|9Dgxћ`>/`\_ g9rn`B`7%M=>9Pߥ|lzo&.&:y9m)dE CV=*6 _Ye^-Q,,|-dn/zu`u`u`u`u`u`u`u`u`ujѿq,a̓`-6`poqbvue7̠z61%vbz3:0~#4_8' Ek<K}ck7سP }G`8uvhtTx ^ ˇ9n1ý0mˇ]MLJ?ȭXYY>hvhb r~|Mo|SMp kɒ.<ˣ=/pabe;DZ,sx|:r9-g֬jقeeXәcc/NbrKޞW[Y6Y}%).Z^3ӁmrpsG2+K>qsa3%@[| p@g4$̬|v-wm6 'f.??wgtbw?trAV09_|2sl=d5$P| ~6lQ1wi<t 9t'W3cZ3%-O˷tdg~)Z ޱٞyӛ4a4p(;_ f{xK2WVVVVV{kJ\vw~_=ݽ>.|={vG?zu7ݼw>E~\wW}NɋJx_/8%…]Cn>u^^wÍ{OՇZb.wcwc>ukݝ_\vnW~?{/}5<#eq?>أ '1_w>3vId{۷ݻίkߏ''㏼k}w 1x|ȇo:ӓCC{o^=l/dxȓ';7Ì0Y6=:߲I |ٚSC^h89v5_5NɊCV4?'LNAfZfn4{u曾.1((r qP<(x]#t5oF_D_ 5 6L^r'>9PW4Cӑ Zqg,Gn'%-Z,l-d«O;߿{{\onxٽ~]wݻwo=/ɓG?e? 1`8o.<{^^/%>QE]]Uov^>n;sɷ~gh/׳^Wwo/ٽW~S3w7Ǝ]vn=y|KvO{;^}/WQcN0WemRXO?]l箵Vѣ_lvq~>673fQtŠٝp'dlhr }D*>?0 ]jfdN>ȍ O8Ƀ򓳍G+ O).2*İd!9\,F;l9k6>V+ٜ@~/&bғ0}AϹEwXC~|̯m?{Mх\;p>W2o/}e8o~p"7 r^伷2_>kOrPO}su7߼[~l41-dyӿ= Wm?zL'cx5~{1 \H{c|q,K?q)7Fg \w7sgS\Ȉl箥=ܳ{|dwȼx8-`or؛c5Wv!aS.9~rɟf{e (&_\hzsCXX$!|rFGb/%VdvY|ӂAC I/_QN|yM϶xb (;`|:`yyrG:j+=jҨon,XXXXXXx83XA ~%?2ǿn;9/U_g|щ=;lncw7iNjGuZ<{]>{ת~=wiU]~|֏>!冏]cv?o!-__'ZTǎ& Ņ-]a<}/ / )69Ёl'p'NrUܛ3#KZz\MOȌZBF2k/{% j^G߽{G6x^v?,ӗZ<{=^>O|y~?]dW;dz}@~+Z<=O/ױ o?{_1e'Xҏ﮻ݻ~vo?nP^e¨0G~iWcm|zܻ{qG-t,COp:ɍv_Ds':\ km!#K t8U!6GlnJ<3/zG`z18sBvaqN~`@S9ܛdO\t9l?t-T\-!f[4}/ .ʒC-%s@K;۔U';d1,}Qw1$L2.\An>bn_kKd!̲C:R#v,h9ex2ݡO{l!#72u'p~ɆnO]Y'_?%yz|˟=ٱ7 '{c_;bB?vo{g>s?sOɶ/>*tWhw}fs 3&ƎP[^}orm廇~o?|/;3~Ͻ'2=~t`g=ܳ_2/:3y;ђfG 2s(]~h3*=d+,m_xBf?dY Y::::::lt Ouqq'd.69~a7멇H/psg\ȼџ[ }+6֗rwGF.L6o.+>%_c~fs=_l0g^۞doqFGwo߽{;^Ȝm|$ǻE>ĵ%}q{1Z1ч^̒mK>`o.㒃}ⱃx{YǞ'Olh䦇o[쪱ɋG91(@+O]!ڀ˶\(a\|aʏhy[ roKׂf17}GWo5#K.F5g6YvxucÇ.AA^m wBftc/< _{?ګ?#?|9~O|ީX7~b>E~g>tϿsY&vSZ<{=^y¯xi_@X/;[~2k=jQ,:d2}nK?>})_Ɓ7>7/0w7S1n퓎we:bxߣ_{NŻߙ_V:Þ>tgsw|4g^6=w-d?\c@V.Bf]EzVq|ش csv5 ͷ+nw(mr}x6-}/ GNٞY_8 X/{#K#߹OѤ#JGtqv@'7y?/"=|B<ǎPd/L]0؃e߬3 gvco^rIȓtrՇ|͵/B<B)Ys%# ]|M|Ieyc=,TP}f#ά+\ύa+=b}_{g;Ɠ `{{d!~>qݝw/9{դy:s.~h ܷ}wo=ϞXpG?twuSmsrwG|&{7'N4/~H)| _ˁ-St,Þ1\Lv{]ȼk/;-OǣE'Ųd-'r&_~@yɊjq@L=jVk6|sMbwAnyCŎ06"WlI]w/}i<9'+Ʊ͗G|ĕХ'Ւft0/>QZh‚ՁՁՁՁՁՁz`}% ocww%/=c$Sf/#mݶ<=>GUBfוrW|kݗ~Wzɪ{v}_w\]^v!Q;mOO}wEzw+_FNpؓF׽ظ+}ܵ܋_AC-=̍莙mFӛrA?kvO)\0' SddrFE|* 7lE?[3M(pH ǖOau e(85[^6sӗΩ7D{9ts t?:,Oz=U9/&,t2zjҨu[%Y::::::t;pf#>ܰ}玟%M9]B!pǓoE?v7}w?;R '{3,1|qipLu-ws׮#o݃_S^׍/}weǞy'a~,r6>BfȒjd:t8ho|-(bȯb +yGtIN$e7'E׀Ctꒊ}lA\vd7盜ow&N1{e] /~N݌dCG׏`BFgmؿ28G#{/{w>ßĞ`hY>yK/d鳡o?{/zBftc/螅+KǾ/s583~=3T&PݿwoyO4<|k??-x:3/#ڽ ƲԳpB?3A3/c^1 a|=4co3j\ݝ_gm_ݣǿ.5߿Kv/:\-km! ?{q̨fv͏tp4;tЛ9}XYBcět.sr˟_lA8!}OM~s xT9]0.NWt:zA>^)?9(?,>)wq^<=`XW;⒉;(أk_WƂՁՁՁՁՁՁ_@w?SO -_{~k_/w=t ӓ9WnO޽m~kOپ/rS>v7O8%Z2W{ZZs=ۧBY㴀ʹtOoNvxrofzӓ;|˗4;ܩ>tpӉNlS\ٕmX姫Whz:\R@/`vs`.-; ;|lgOfO:D,L?Bf8l@2$|`GLA6MЋͶx׭‚ՁՁՁՁՁՁ_83,.ՀÅ*puy`w#K qO~F%scg1}.d콞L/r3̻Q~tW'wd74r A;>o;o!|g7oOμG߳2=> {1'Ӣ,iAfD1 do3̿tJq3g$jC?jw'y{m_ȗZȼ;/:=ꗞuzsMBBjxϝyîw)6̺0`}Yk!ù5ޛKx) C7ϴxmMUg|s#fh>Qv_&vI7ݡЕ> / ]", */WW,y / ?Wl=d>v$Iw~x/NM^Smy2]c!sLz!gBN/::::::T:pf>ϋ\<ηcGyB?7i6Cvo>l}62 xyec w}.v:y^BF_G|GwoO7|oٯ>yOE36?~< q|9yTjZȨ~]K {g.}J 12?̸↬% { f9;, 0mܜ+'9(Ou_9a7 8ϗXꐟ>?24A}gЕ3)<R0 (UI.ӻ Z*/\@pa9uƒso:X^&7Eyaq@ڱŖR_b$dppCPNM!<?x!s]>!+ux&G:%ޗ~X|Joퟹ{½[v񇿆?];dA!ʓٽ[T>SwO]̙>b˽vP,_8| (]{%g~ݻO ŮG/=ݎg2#Pw%<ɹ)Ɯg9O/f;F?q3}t28 ^/ YGӇ[zTѡWHx4O2XN>2v8dLl՜,PmP$|:?%8(F$V Ůk91"K_%VtV |H-df=<;9Pt"doX<6n-XXXXXXXXX=b /#3wki!#K3;dl_:y Z2̴y\-_P/Vstyk9`s!CGf9>C^brٜO6ՖjSKAU`toEd]|[,Ԑdaxl?-n'ϟS^,t-u^Ǿx׌'dn&ԯ,! 1fKL\xu`u`u`u`u`u`u`u`u`ujѷENj1SZ~`6_̚fqe h&=hْ]?:8ə\4Yƃv@hXz/"쓹+h/?>?tŋ-ls.0<7K\ O@Iҳds@l٤L~v|ɓ(a_Q-_2>e?'Ly9Wk!|̈_ڴh9&K%Cycx-dtmԁgr!G>,.,]p_Yz1#0cd1:gμ-NͣtbeG%6>{3_ _Ndk+7[tz#n┿zj i(i\ ,9~.-T{9>֬iNE09'U/G`XP<`x-Ya 'GW.yŜ㋓ kOKPb~hu Wt1jP6k& /;< x+6?$CGǯ=¯Wth2L9b Y<!=!-3<2`u`u`u`u`u`u`u`u`u`uj5q̊`7hAFLo>ӵ`K/6k49Ȇ=@IDAT4;G}yRN>nsO:Xv:D1ϖ.bd ,0͜5$}tͩLl8=rw67ۖ#_p.Q.??1!\}7`0e?_T2YBle˝9WVVVVVVVVV< }8Mr_ 2%4xHٜ,>=9>]|1S9پ~,dnJ~Z( Y<:,>B/::::::::::pt.dZOt.9dg3 &oYþĊfc#W:ǃbO^M_r6 ZOf| ޳%TZ0+Bf *T`΍˧&KO'nŢ#zC߂$a).;G GzҥZe?ML<kOkwbq%!#HKβC]lg<2urՁՁՁՁՁՁՁՁՁՁBI'e|~ޡ0C) 7 KΦv|9_\<| 7ټxAbб3#싖L3׏fC.An '@@|hpȟH?s9`S_5_)Y@tt^z?e,nvhz@f唋O9|pqٗfW-/ѓ9Ut)^2#Ƕ\ Z_L\xu`u`u`u`u`u`u`u`u`uj5|y8fQC[KAn Z\4wKw鳟1;,;r uW}3k&@db=sp?=_}B~.<Wx VvdN6~e?YR CZ؂ՁՁՁՁՁՁՁՁՁՁBFƗ><DZ1O؀ʼn64z>>_0[@tb2|3<-ffm}rXⳫP5]1W8`7g-Rص_dd/fgf27W*h0Y.Lc}Bfkcu`u`u`u`u`u`u`u`u`u*B…q]{b%]9m g6;T =Kޞ񗷅 EK |I ?3c-d«Wkr~,|4L#L-Aμi':gfg+ol͸d0H?Y4A-??wgtu?trAV09_|2sl=d NEr7gCՑ-*.) @rtqOf0Ǵh)fzK rtgPaC&ASK=;22Ґ%Zt6鳙q2urՁՁՁՁՁՁՁՁՁՁBIƇólϳ26/r8n9PW6⩻;0,_4_"S,Yyg qZyB6̺3R#K[+WQ,db>v~hs(WY]?]x>G/hcf@fCǰ4t w'qh^>"O,4C@'o.&lr=%L>?ʃ&O6-_(̆ NbX]2틜}] .\s5TNOzAlU|v Ld1CAS--z>d0^S\2|Z)pv3l эWk-dZL{e]e}bƇg=ܳ_WyB4o9c5WCYlalͧFY=[l<)buʕ-Yj9|`dXٲ9^J`$%|c4L\~v;9b_-A4M϶xb (;`|ʏcöE?\xzs=O#K#~т-䇲ÅK6YBP-XXXXXXXXXXXj:KOȴ{z=]Bg?,tw׎,jL>!3r03l˗lg2:`u`u`u`u`u`u`u`u`u`uj@ +3VնQwc #l6txseK~ ˎ.zgӛg8sBvŭ>bO};e3T?L 3"B|pPaEi A7ۢ=&(KPKq/?6epv;vlbId0{].8?i}P\, $ Gó,z-d«JB'W{.!L#K毋 3/6Ml_32 >_`e.Rnk!S^XXXXXXXXXZ:p5,d[Ѭ; Y̍o HdmٻXg؉iMiPz hPl61=P빅q-_6;ķNqX)&K_7.4:z o.d҆4|2/U"c{Urf]ȴ٥^+Bf.;˒t7.dםmMppa'bΘ|:rbjCi&rb8x"N`9gf1,q)Nċmrzj⫾~C%[9G#ճ{!f0媍S2 L6>Å s'l<3W/:>yҜUn6CK}Y.lҥʝwbaS|]ȴ^ox/dJ~8z\ȸoFAkg.q6N-Vpuq98r%{1%_İ5" j^Ll櫏kK׈x6]P׋Sjśy3O?q]b8뿙bˆW>l67Ky5"o=-G.Syr.61q m/Bf^+>t9O+K_=Ή.=gKg~|HLNc\dra8Κb[̓Wy6jpŪ^yg3^)QtM4oj"8hexyAm\|<}[ ~2Ξ.?W{la OO.~=aމ8]ȼu?#K.IS[,]_Y?4#^XXXXXXXXXx-l yɌy#=(Dž%NgWG"yTO]a yQ,:6˗c{S\[o$>pO Y >s6x")ơY{Z_\=xؽfrh)lnŷS/ipKʓzp5W[xj,Xy~3͉E Lx0bsx6<`ۅ]Y*)]>.˳.dlnki%}{}'(>~d'd~/&Ό. ç}=Κ<|..W9qS?ċorMp0ٝ)ډf^f SMijS׻d#Ө% VV{4LyEqU?vM |b7fqn ih)g{tr9K} {ʼT36s]UnƳi~i]ȴť^+p!3ao+Gų{9c{?>>>!^tFvVtM Xv[*Ij ;~$ lCh}\ٳ9|398)0V8l}wJ-x\DLr @^!z-x꿙'?݅EǏZ4l<8՚#}^^N?zבzxvO9|Hovr'd'f.HXvOH?矺1kkkkkkkkkkkc~om2]쵋.Gg8sq{饗NnXE]v>yx3O6s_ ٧]ov^-F8\8}Ch<0=փV片<0>[v復-/GxΊ$-$P#2e~Kf+NcT߬[KOq'89|XKoơ{Ӆ̭A֥ Ŏ=x]um`m`m`m`m`m`m`m`m`m`mU?'_U_u.[ژŲzy';=օW|BnKggK1G.:wʋ9[G>|$<-GWOq5=[m||ogf7 ]f^G XÞKh 7\*RC`ƛOg\*?ͼ/'>iuNfl1e!Fĉ8v\4lz3O=zWBo,Gz}O苾咵W}]zeˎǠųZ>9gϚ#l{饗N=.df{49t;{w&=|xp)Gώkxǣ:ubwV#G< x3\y|P8O 5A~$.W7xc]dQ}|z [l=-t8.8iqι#p;Iu >f=̡{ǧ6>%zML{=CȤلz⸫KO|6wXsBqw#?OIO;fo)F7۴Ȼџ_f0jy g .K{!W~nqoO%kkkkkkkkkkk>?'ЇNCDN b4is'J~Wo.'suglqN ~aoq<.=Y?W|p#);lR jcي'=jSj[fpH IĠ ~ \5C{q Li.ׯK%V>+,X#l7c}Nj S[/25é&Ӆ~Z$y:zs?g+?V;ݿW|W/YXXXXXXXXXXx6O뻾_uxvZ|κiϺ/iw_=΍ sb8MawAY)SG>x`?94. L\|0Ոyi5j'-y.̾F/_5$5$2vt<|2b^|3O\:K#6{\4o }bwRZcb$?9v}6Oyե .Rփž훾'd4%;HQۅL?Gȓ//Z66666666666I77?uJ]=dnͅOϺb.d#K.Gr{g]M~g\qgKR%r]lz6L+4~gٯ\dʼnشuǗ˿|N+3滿O?4/Q/iCɧ'ežOպ!'d>gFg.Sbiӹҏ=ݽ^Q/5],gqg3O~r|gbbӃqI>>9qj;k)tAq e I5ǂYEO ǧGַ|?FqI~7sbơ?f|I\p0VOC EXʑ?sGv1zڸ?T.wɗ}ٗ=տW~AǗp#t_;c>y-}cOV̶j^?8:Կ(3f}?:.mO}s2>U 89FEf`=΢qvRnKsϬ;w;{sO~y>b\0wq.oez85|0b0ar F05l͋7\/O'- Fe ?!O\XEdkIW7n6UO]X6٤EYz .b?c'3}H}))~+׷`oy666666666z%nTs=g{Sm^jM]nÝ˩饗NwK}ΡqE˗b^O 1糝O:ɯ&>W|X謽'[II3ϗӿ&C |܂54Xd4hLMl X.޹j+/޸멮K r\86@ѧjS v ws.08{Of^ C+ 66666666{G5ky0b3MO;37cӞ5d}gˉ,qttɐ\myL1D]b:p[蔯֙s,QG1/eއi7r7>jMa= l7w\%K}n}>9\4\rdXj}_>} w-kkkkkkkkk?7pҠw;W_=g}~?򫹔g|B_Y僃Ρ΢',L3]H0Ŝc/#q|rba<_ϰU}l)} /?w$CS__ͼ1Ow&pApͼy]PUK²ðg'&猳aI]O/vL%bA7Maԯ>Rt~d (ӗbafϚbS_^rG}-m`m`m`m`m`m`m`m`my60/#QYfn>3>gdrE/6.d>v{tB;ؠ =aÉ8†/_M}ŝ!j-LƳL5 +&tկ|iq~;mg%g3wl{\Cs!4ęG+Ng$oZsv3gxwaϼdj<{&{Cb'_f~{>3X~#!'ns_ ̱j~~n|[8/arL={?\?Gzޯ勍 ekyҙvfvi'O=/$\}<~MW \ۧhs3MN/ӮĊ,+?;i>;{Қ]L^4%;_-Bin菳xx.SQ7gSq18h29jE'#8ROqԟQ雾-GyBf_jd^ L6?})fjCgM/Ff~HCGVlm`m`m`m`m`m`m`m`m൰.n!jb،/?'n6/]ȰGv{MivyN`Oz8jHjz͂ Ϝ-Okgf|:|S]zoSO> ]v}[ͤvrKΐWnQ p FU]gh]ӰdSNT7wAqѽͼ{KՓ=}z7O!'98g~ڗrY߲Ŧf%})wK\Zsu+666666666@ץ-=?c/^l{rOcą93~Gr'djpN%c0vcxpNf!#׹V&_nR5+zRi&vL<Ֆ6Z^CRlbϦe R"z.f꣞OP>f};֯X=ǮW.|e Ǐ,AOH;YO<^rI=.?C^XXXXXXXXx=n`^Lװ3?m>3P>O=퉛q6w_f܅3{?z{FO8:ov<ޙι+8F:X|OIs FlJb3}T[V"8nu{ʹbkkkkkkkk5{H|{}ؙ#K~ gLg.=󳜳j:C!8]$j9⠝qW4Üy{]yq_=-VMlgC$][\YO5 46H/N B/WGիS-^7f#aEfQOp[Ξ3MƉCLRx##[XXXXXXXXx=n`^:sq%ܑi]l}Oʧw_gl}.?Yy1۹љӥ [g]|S˦;J)BGF9q.jI89q`P_n3Ovrl_Oͼ+P";LnA$!o6쬃S%_]8;Ξ=ŝ5]w#'luh>= ؉ /-lRQfׅŕԻ6\\ ie?#ؑ>ջ9?GG9,666666666<8w p+cbͱM飘M;X2c.?>K/n~]qfmgIbrΫ.]+]pNe#N\Ոy=a5<vwi-t|=za=D)ۙhbV#6-~(5>Ln"dӤti/ 7%Eo\δa&{ԫ>NԈŝv3j+{#NšEwȼYO^~s:<ͧb6Gͺe \nTw{Ǧ}Iӛ_r]s~qz_{)7~G7Wo3 9דM{\8GŝEg]\//GӰj<+VΌ$rly6<]n3O=;^NcbȞ})dO%^v#0xD/:x^EƹGqܾAF_xsnr]j[NxX>Lg=jҥ3+ l32b.Ր]}.ŦfYw@/yv=>K+666666666<t!p?s>nͶ{{bOw+7snOgnQ$b.@hP/.֣."_}aq=v,$q9^fg_9'x>z`cVfy~Z.cd_s{ޮĖ;vbn׼۫=66666666p~GF֙oglڶN)fM&eC?{e;cvf'b=sh'\gkz,^o9|iy=yYX}ulg87TOU'_Nl_9O#ճ{!f/qq19˫k.ag< {ٌGA/S& 7izxV/Bg>!/7Sg~qK_k5ԳK0{=9>s~nҁy1ܗj.Z>=/>/>]3L}T'd+u ~\IK](w{ '_ZX <Ԧ#$p<]l3;g%S<4)Vͼ^㤓;{kf>X?Yɬw&W뱋fS#F-t /(&DX5C{)})_ć.|cZ̜^/'VwR.G˦}Eb|87;Y仄', kTBGz#K򂽗K}n㚱#{&~Ƴwɾ˹{9֟㏩s,m`m`m`m`m`m`m`m`m Kv~t,'ۙ; 'QK3Sl~uεl|<$X}6dSxL՟֣w{. %Lll̆! ><<'j΅ֿɜcΩgǥWgȫyo;yę=D1P.GQK+pWV{4Lymbٿ Yk1/7x#:jf NaW39R'x8= /=[^.]\!/rƫ;za=ڥ^+tipnYskܴ͟=åůՎOgQ!F8~< "y'fqä)OwQGVYr`ŝ-h0=59t}Sap5W~SGus&loD1pj7gp_<`W#GK<հZX!Nr᡹#M>?iÊ#9"X񵁵sv?Ww ץs-w{QRysջqx{Ӆ3c٤8RxfrXx6'aj=Ç̧z;;~jFؓmf̳{If>+5~6bM|ל0h8^ S‹u![֟_8>}f\뉯l̓&͸>D]Loo.d޶髟v9K}nMbwgs׸q}m^kkkkkkkkkϻˆO絾ޟ3fl K[0G>_=ΛΐΏ.D8]h8s¥3[zuB'83i37-sv=7t95CiE^v0yjs>nƎnõxw{,}8IBwu.ItqR G GmY4a=L\Ӯ3يm֣݃OZgc¨k>$<\OӿDnb}5e|yT]<ǟ'7|-֌tfcE\뻅NxԨ'|\]TK{7.X\~ p &N\L٧ ͽM.]8<4whG3>9S{ķ~_XXXXXXX[=3a{(19uK.Lw87vA%x~ȹR\领 C{a␋W8ɎW-\|qÊ=xL>,#_3<wr5fMF_;DWCdrx2yv}ǗV7?'"?M4oǜO„O׫6nx*.B-2rjjVw7Wr-ܭ\q>F2zx=8H9˫gaxaWf8^^ꛟ&S\t3M>$ф #5+ΟÉ/zZ\C]:j`L9|j ?/GڧS|c-8S2M-va`cBM~Џ,_tSK0G#ة_fʹ'foߊ{QuϞwkkkkkkkkk/b8l5k^ <<Ễ}h~Q(>} s)W^!2wo3sbyңsiyͼ`uYׇ__#Vl3u tO; :ݬ[Mゃ'z7w %0d0D>^N<%l橦\ó frY t6|tЈwFĭ#V~r_ދSB^UGwȼ?<b=ɥKKo|^?o~ǿyT"bV1XXXXXXXXxn16{(w+} s)W^^o2~ څsz9~<̜3( o\׷q.c3O}hu&!6q'0$, sjY!S|8y6~y>MOszs 5u\ugoӹՊgϙğ7hLM4Ҁg{]PlI-.Ϟظ}`,bjPi-t_V!Vp5K0b0i9#v!M\ \793?퉙(6k} |y\kkkkkkkk?7؋l^`}=OO̴O}?͚KK'd~φsG~g_|y,*9փW-:qҟvU~ֈ Գo=wxa̓%1.f'lkÏ3&L/P2k0.Um/^X8'ˎ<$7߬l.UQrߣ_7 o r!R/prM&7{ }\|q>g 'tRnbe\a;mXYq/8ziYS\"C o|[ٓYEqr~pr8.Nd?[kkkkkkkkk?7p%=={ \y;B_臿ٟ0xwt(3K ~g`yyQ} v8돳vڝsq'GϮO'fNIٸ+oS^aI\ILL yMf^foI₏{rg?9k-MMm~'fW[0|#qa?}?K r%ܥ\0ե/.?ҏs(=>tTw)7g;;k[9Lż?[[~{\.7ou7tN-.|*ց2Ą'8/o:>sſڅ6ߣ>.tqq)W=} w-??|L2cf_5kkkkkkkks%y9_Npr/Eķʊ'?Byӥ@g.]tOUÑyïN^}gW8~R_~5ŜesUϹ-W-tO ߼Uw|r$;kygW$|PNKԖ.- GN/˩w13&4|B.^̼L3/^魖M`Ȭ9~۷y?=wRn~ {)7y..&Rns\zkm\l:%K=%乄KK=7=>O?- 霘g.!h"6?R=< :yڞSe;|g|8鉩?-^l/vrKΐ^Adٰs8]T=4=/&&f= _P?aǩo Kpeinq8ɱO7;//տWO>򑏨*\"܂%\%ܥsK~w\xVnm`m`m`m`m`m`m`m`m^,5ky {)9 w>'| byv<)Nʕ{`/lEE<'V Ǭc]3rslSyX䆩> K|guvlyO>x=hO:kbqR3> Ϟ}ذ4?!bHՋW,|O=q[f=F|!Y7+ ߣ_;B'ѿ7x~o o񋶏=?xJeµ%+ŃZkl?Y};ߪ[XXXXXXXX8G]s-u )JZ~iiO_[_x#/m;C$ 8;:_yφ-x*ߥ8upx<ѳy[O:?\|blϜesO"g޹1s6i~]y<"N\`_$/IiL,5lujZv7P^`rV_VWo!&gW#4G"y^rlEz5W#OFLNcۧc~)/ɟ'??nkN]]3-5`&k~=Zem`m`m`m`m`m`m_kt`uZ>kkx]Ƹs}}} مHg`qOjQ|5qН{OWɏ#N`坕iɗsU_W7O=fY)&ո3n~F]^ yo_+_n3OR9?SKk<Չi{oP1R=3kW/5l|_~/o/}c{c?c(ya![o弆o[q0㫱sm`m`m`m`m`m`m`mx=[g״/'>ޗ~\o~?'?΀ߺa`{1=7)_vy.>|rڧh;CgΠ38z)byUja8.I9l$qϸa7t{b?%=|lT rsAz׻>_//v?u}Ts)w/\[kn_4+tm`m`m`m`m`m`m`m5\ :˭9讀K1K5rcj.b~/%Oٟ }8?ǴGKgKL]MՅ=xrvκ`x{VާxϮ')VOS{V] Kvt`ƥr-/jԽ4.<.pw B|5&'85qn)Ƈ׿Xf$,~I6'blW|W|| ?̃7YG| fp -?Rl/Zfx^?8: jI߂??ӟ|g|ƓGo6ܷlgδΑ|gMs$ S 'Yh8:Nlꉫ\}/81sv=Ҹ̓Z=<qV/'<ѫq5wWmq3Ҡ$^0l5\80\sQKS7g('6iQY:K&-W-[\5l8&7|uϯ0__+~ߢ3?[on߂{6[/$?;+9fO'ٟ}qoo?C?n=ΕΐѝbYr^t9q[<'|ݼ9>tX~R~J&S QRSK^GŪ??}{,\,0͒'[nONC}E>\;-?lׯ~lCmo{ۧ?ܿKf=ɭP·pgr,{m`m`m`m`m`m`m`m`m`ma8wpxn}+V5ns^.befTKO)}~.~Rv_|yikN;nx2///;~n<%yECj䖚[0wl~}hCv|E~>ﻴ&^XXXXX_{z;??sK-~HCQ1t9}:{{gO|IgMgv3ˋwg8QXc;m? փ3ngrn^5[gi=."aIqsc٪l打~FZ35YxQS%a@o(/d!̓)W[_Z[a\M Nx01 74Wtiqφk&_?>%__g ɇ>'iK˭[qu+.C3\,8rcw/fܭ ӌyo9֛Tw"lwv/ήÆ6g#FC6|9@yZ1[)..#,Mbb{لݻO]$y֬0o G5RV6Ebz_i]2)VO<̣}K+}ʭ 7pRy~(Cs[komM?m?uo8:?;CޞΑbΑΕb|2/JIXg[uz#X5Şy1R/;*{z6La;SoIpvՉ9o[>^)QtM4bai͐"^M{a6fm)G[husO~v٤{>MF{OIXԅN|OO~1b_/G?zOO?s)s~c/n3g9PQsW\_XXXXXXXXxm`"(CGq .c7>Cſo_#/9C:ΈΝ-)vgu~8`=uFřO6 ?[?xλba§Kq_Rï$,wчO=>+@D3M#\xf.S<.o=oQ_Gz 3lN\Oiˏc =L7__.e_W_^j^:<?=~|G=bVߣYVlm`m`m`m`m`m`m`m`m &wki;cÒg8L#r-@(o^T;Hē53m07\ʉ|6 & /6K'| գbI//RW>''/ensQsCHzq/666666666Z Er>15Gw+ɘOO;}:'c?_Q|oOqggܹ#Ɇ']qu!Ύc[7.QxZNzix/olpG_k~[~f?~ʜD88.C|K>^k78FG|\Zo=}*El2?YיY'e4&[,a{?=C7[3hu4_ή_|gS812yĚ=G:q.LD]xQdov._)ApjD@l/S/=pӟ5jß_O9 GܸH|vqu=b뒅be.A9q'm~]o_'f_c~;g1yLlV·m.7t|j[f<[[>o^ZqU'QN^}ZkVvlSOً]S-:~xqٞ_}~oMӶ%~r_eraqY/xαpkkkkkkkkk?6pE197/|q 7_Q;@տ~y{7k{t&tt R8 Ly G&_ClIYu*RGK .eGC|6)3__<363֟Nfm1Z|/ٓ7\3&.)3Kiq6?ҫNۧMi?3?7].?-&Wt|:ttXz_G~z[BGKqҿ=|~:|>]ttX:`1v?].?-&Wt|:ttXzeў}_M0Slg~oOޞw W޹g&\^rͻ???;g>|60p{|ef!CgAB1[.f$.qgyW5rV +F]plW_~3W3 _#y9|1d{7| })ng@/>0ͽ^z3cl-ؾ̺x85,wᙸ=<{oquHɇ+SorSL-toqa/m=_?3?|gԷo>Acroc;+dӯVXN n_n_q#sK6KE?U|/Y\]LO/3W>߿ KOT>=sc׿oIUoE_7.3snΙ]>Z# ;˖~GܿǶ_w{~lOٞ7Oħog0~4oExQ{6XK:=~^/\#Q||=TܫpЇsXn0ȧ>L]Gob}q!9[|>YZ~X_rb'>{>_zձ8ӯ/ƪwn;wk*FyV?ЍWN\Ɨo'jY%Fyb}\ŋ;8ۦ,a/(~uJ]5꧆x=._T_oFq1W5]eb2?U7`үӃMb8|5+uӠgåX}z˽f~xb =,]cOw;y~=g|/s޿<_wHOoRfO}LƟb]}ÙO 3ʋ3 ]\sT:'G0{i[O0ϑya^7֠~$2XG糺>sя36_UW\]=:/^.}O~W,c>pâڴ>6"5n.|/nޏ ˽O԰z5ssM9aXxS8Y?}9>W۟Wn\;fz32caW9Nz8aW/>a},-ԯ+y/Ȼ8?g<w3G=~60gX߿/9M4y?.rMz+tq{pϞg~s\a۟z3g0f~ngm-sWJ>ֵzxʧ+P3szۣ)OWջڳ4[3/cMABLL3-L0)X$W5gߢcK_:#,0L9y|ap-O(#fK^?`뫫4pO+z^L cܬQ蠗_ϝcq#r>MF<~ͻӝ)~x?} wc=[<ڛ<{k=t?v?q}}ڷ-z'Z95w78ÅQw:5‡^ ˽FVzodb MXOiC_K\4خo:[{Z}0O_\]i!G}S~k˜zrb͍l]\OY | ` 4f{MA+h5?7m+PӦkGcr+th. [8^Ƽ㽰j$FX\,}XyG˽,YCAY>|Ei.h{|1ɹoLZ#nuxSqcrvsϗ㬑k }&J>&g3֣nOۍsusws?zmŻ}>kS7WW3맱/l..'LӇ ~+u8b<yՔb˽jcՏDWQ?C>~ #^üX>G;}54>_/G׳;}/y}{޿>w-Q|<_wgC}f{W=ko՚3O?j`hs1z㗛\ዛ0E-g,K|0,4:җ5MkOoY_}f덩~#0xs<ç_-Ùkdtŧ~|Y3"5"4-701X8:ȷH<'F޼r/luN~Zwg.> SL?1kzpgiqz`Fj:h⩦~O|㑛8y/q êg7X~si]zjXc9}M}E7oJ_fyϜ{?ׯ߿>a}ok#V|k\ϜBnr]X5a] i6OÚ⬞a;%}x{cpZ3IJoS;|{"^.< W՘;Xͧ|>凛L<Q#Olkz_PhAm89臉ʷxXǾxu8qx>ʙWߘJ]X<\CW\>hQnnA.̧~˷/s驍w]Zꭡ:3W]d`^\uC|,V.΂\fN1}˷W3nԧ>#~o<9lkx_]{{~'Nx?sš7y:_?׽}}^:&k~| ]Ô + ҃W ⌯$;ANm'#|tX~{(8׾3?<}~{ų #ƪ19^Wīo,U Om6Wq.nXM x,Mǧ6la/>gu㫶lĕ~jy?^n qcriqЫ~V貰ő>\a` 35mOkʉOlꛧ?} ]ɮo~Ԝ?yy<{?ׯߟwߟ}6y/܇>ߏ ՚wԫ`qèą-\I70ʋ;7K==9Ya0om{2y||}nyV-l6JϜïy6$?8fC jŒS^'3o#F cކ6K7 ~H_-ߑejXZ9SOms# Ï#-s~{ ~c5՜GZja{1+I#aXjaWSVN8Wc..' /ظ⃩ޮX_~U~yڶ~أL\^ws?Oއ9oר,\q8V˩er}3ΎԲ0~`szxf_~}7ҍ[>ܮ<4)nyf3N}LM8έyHUf}vkNYïOZؗ _fs6pmE ;ymR4\_?i;pXdž ˪saap"ׅ'n?ĻΘө.n?Y]cy#S֜>14;3?y07ҴoX_ 5V۾L7O,,?1S[+}\g9_ۍggyy%޿מּ*>0~=Ll}oMc0fݘG?xwNՇyF\˽ioL<S?O1^@IDATک/fmjK?6+s&# Ӟ돫s~Wo+F)/yĭ+mk&XNf5b]$a{Y;x1NV|G0a,W ?;x||5pէr=GS_ү]7q//ԳůgjJ_^s vZ5rC \CL>ho.9},m#I1͏զ}u?gw[{q?W{};{yOy8g>L9Ͽg>zޏ a6F{Z~.ã&=>lӗKwj;>cv/S+?va;9ŧnup{Ʊ':~|r,mu|Ԥϯvmok1kqsn kNB.h}a5Sߘ+gLV2=fS/ czWWlWyqcOmo}}w7~zHרw:86U؏j1sft'vS-3x97y\s]Gp:<ͫ']pS>) 8ê?ׄk\;ϟIqy<ߩ{s\=_ý_ {OKw=wxio;l އ37o5}v?.5Y7^N^S,=M~_avLx7o[jXky+ЉGՋ>ԓ9_ܜH_5_x8?qw F_q&M/&hA;WFumrFu6TEW/j<}gxӁei8jҚ\Zbԟ\S?WS^5P!C{q25eĥ؞o>9p=p Όaˋ5,Njv}q<^~i~@ϔy^3{?|Wߟ{߿޻{z&Wl+|?>nw=̪)2q<=?+ٿzo[p/3^NWOey1F..ĥُ98_/_+ta.?&;cS[{4aծJ]ZkqZ?կУ_ڴr|pyG?)nT`v~_~#2#G$O\5Ulk|n|&͓O'm^۬`‡K_}}ogVчcxqb.^4/=99>K/}7Oo^/|L1gv:pG\jX#\Y=3kӧ5wN'FlO?8xBo4i܈c5{KC<# coZ 5mՏ-$<>1y#<1Y+:)qт.Fq~r/7/prq/?9qoM}Xkͫ <˽?ן>4k9}~>:yjyK;|{}c?O_}c}fSobYs`=wkrqӏo-V]_x2q=Nmk1/3K^_f_lIhm,1wȷ^f҈C?\&0zbY&G&2|v-T}'\apעq61sI5thq'opSgA/vwaЯN>}xŧ:[GMp,lcZW>W~XsG_ϳoq1c|8c|#W1\0^z4gSֽҟ|_ѧsyߝS<~nsM7Oy{|8|?n`4,Mw{n3oqVH.l2 oL_|G"w9/Ʀ~uY|YWgL\:{[C1s<闿N04gixjm$8w徱n$Opԇ1gsj?u+Va|/NWond9?}tv>:|~<8sϫyвy~?|u~{g̴nݯ? 7Q]|bGbGf˽)p{ψSF>a4y[} WuS_WJvkn rap3#ΝW~s4goK]=vm'Y0OS\y5߲UWQX [T la]B[`06޼_}Q^ޗ{i̜=`!AC\-ZW,Sf/f|?hܫ6 i%~>]Wnz|3giS1s]s>^iO}~_]kM=[7_ŹϾ_<yyψ}|3~Z_?O}V:GO - zׇ(׵Y9O~hO̾ʽgS?x_Ns 7׿WkarjԵo;{çKZ&3coDj,&ېN Vl_m5whWMOlӣ4:iM:q'_N,،˩߅U,|sK95r~o6bqO}>^w7OOKw=bpl{G=SOo֙gōS؎o5}Eڴ珽jۍ=5$vwjﺶwW?s?r \'wlOfW;}uG>?߹Ͽ8ϟ=8_OxGg޿o]kgn7S}O.qz8y<,g//]O_]=WQշ˧e.+Ư3\Sþ:5}3lo  =9ꦾxzY/_OagFֆ\,\߂V6Ƌ cW,i1/hW2y5]dzׯ]8g79eF}c,/U}?W:za}r_-|F8fGo4U`8_[_[wq#Nѿ\=;~~o{޿}3}۵ڵ?|߿8^=`޻:S+>}> WXUj=AnU:r_ѷ^76aryxk7V'W#WmΓ*4߯?y_>iwm|`ٌFY1}S̉9iSY㬛|0q$'WF|'*}1F.|\IxhV?V7u{z`i_׭7ǝ+>G5积fյgpOpCĻ_c{;p_W1x7/pWtkw?kncϕ=_yys~8/Y=z5Rs/}1t׏ _'w5~W]7&&~Á?>ÁsO?YqOjЇ_䋿W}>:WFG MwZ>j&徱-1uX?xOx˥\'&{tgqɷLU;+0to0ʷWXq~"rVIGr'j<vnE1fXt>6oji6W7/ ῖ~O}oÈ#V.>~}lr/l)7NcknqA_op'}83jX͗+7+{j}=~;=5o;w߿}>]xwwX7+߷ę9`DŽ+BwSC#>y9GdW]Wlozl.x87.3/f"tBu"056Z8lwBz72_a29~tӏw/-nb鏉3ܝ?,L\USЙi/8}/ګoL|ES,?l}؝;gg;ϟ.{|}^Cs?;7?W_?7;_=pU\ [~y\C^z6=up8B|YOxf>0ōb+tY؝Cvf7S-|rW՟Gߚ;iPkt0>S[}y;bY+f>%mU8X' ._-J ٌ.r3GL/]G^8;Nx œ}_Fqۛ]_徫AߑU[Kߞӏުokf^9'^MS_~+闯>Nq,_ogXǜx<~7烝?߹>gէ6=>3w01lyfW?i׎k>r(hc,Hېyj.<x866 Z~7oĉYqWBnk~m}Y(ԯf/-GϼZ{:G/WZ/o& 4o3>{jY]_~k>j8^=}?<>%z<1O|ZW?ûZ׏}=eSv 8r1iV9gŕ>|89fX:т|Nt_Z?W @_tZ 6F5.^UǷM [[HMENfGoYoڸFMkodr3S>y]簿Ę+&^kO}g%7;FDM Æ%:|0$~җA(W};_L2#/l\ثCxb2?c_-,߼==pf >`߹KO8f}zkOfOީoxs<^??s?9>ߩ}盵oy͈;=]ܡgqxqG+ZF[tҏOZ̘~ub1x]X=gk״p!f}q~u X}Z1smҗey#15c5_sk##CSEoSj;Iڐpշ\s5F66Mjp:X͵'qŜ~?.Wy^F1up,t7cg}9|?+tǥ+^ =zc1ʇޘ~=OLk_=+F\ج~{s}`.w;l>=SOzm+y>گ{ uq?6ϼ{%3;~w؟+ӵԷvF9riԓ?ot&_>Ny/ߜփԥfð觓fsK5i„oլc0iEz 5߸ ۈИE8^~j0Nk-(4X38JƸWI'}#!f6Sx_%84r/S7! /}8}ime:qLߜk>g+t|}ևXbWOsխeS/Op9Ďs<>{nGe|==b_(~?{?s;$۟]C=gzx]_=`Ԩ˽F98#jp0Ŗ{Yu3dI'Fq>~(1c8ur˽NKL][W5Sw\KXlҢϯ\}? k]W_`1M?j7M}d6Xq[$㷨FNTʏ/_FAO3 8/VxcS_ .}> oJ_5g }4ҏ߸xV~__57vqO-{O{ׯ=};_{^SWu?{}oz߾a_7?~yMxQwu;*i_sPu|닋[V>3e;L{z_U_Γ5skx։Ե6t0rz42g<}Z4729q&qqYW:1wW~Ojiٯ_ _4UܮfA.n+r=gžgw9ϟ!|vuy3\ |<1ړ}>zytcsX۳_ljk Mi6odxԙӯ4YkM?^èko˽ppY_+tz1~kIOXTC_<U_oa;1 W~s#K "?5 1MW-Fy'Qn//FŪ1ox'f/d0S_ G8Ӈoov{!?kgcWaϟq-nE7.k kt3z5çB}ο~Xgg? ~kR֚7gqO4> {O>~l^ۍs<<gws޿_~=_h[Uۜ_rSF\ŒO>qs0K?ϏgaȌ0Ō,zbQLm>Ҝ 93/l}gӗS?ڣP_z^MɧXϭ>-^+_}כ~kPW} u^үfW׸s/^өt^״~O?=/g8ןދ?soy=}~`z?y?[C~2O\]c+tyF7k”S\^ի6>=9&(ÔO[xrճޫ?nc50O_?Y~֪k=;q]oYsxr?2ZL_y ar6?{N16]wW:Aጰa]PrMzg.^z E ^nrW+Vi#fiL}8-i3wKO |s#|b>L7.0ripӔKQ=smutgrx<=WkNwGzbwƹ>sϿ9{޿?=Y]߿U_]k.}\|1Z~}-Z/igTS?x&W:]3qusϜp'Lr8,.~Wo}y_,z4Ұ~5^7^}3jsscxfWʦYPlhAY0:fW\l}7urS?6ҏ8nZu>~T_KgWպ`-vWڎ>JǑ~:q[OtrS__ķkO:X6_X~5+\矆8s>|y #G$sЙ lcLx[az7?kE[OO>^~ظʋ֑"1kk}3K_'_t{99wGOԇi=WZ?ɧw|jqM ' .}s|b|?d^/F{+u{qk?_{s/\);ϟ=}~;o?^uOҙ}ŗןf~;~+yfT/oH~0y~:w_}f?p;҇Q>^w},c>,}uԷ}&|ɳLJa8>?}kj^/r>ZS_|'Nys:?axfC Zhs٘6emĔۘ~7&.:Yc-zZGSS/Z> ߱aeS_ N\|nvz`q3ۗjҗO'W+}8}g\=]eS?KS_ݟǫypyvy<~wXޑz||oѳcr g=\Oj31&xs =¤'ϪÇjx|Pa~VaNx7ZOr&W>kW\orB6}j/,N<^eQOéMb\;_ o!p|'@N`'C~߸z,Z>6A1$//_C&kqkΚO}~pw1.\G`l_n>TE'߳{~~Vjn#l՚q-.vy}ҟt`rs7߳^s~ww;ϟ}^ {?r޿~~{}.k~ws-.o3G.r8goWquS?u:G3J_uFgy|˽8p0{}<ޙZG85qrL}t2y_xէayʭv07\k$5ӇFLEd-^[+flBS3n0b; baЁc5L~kJ\_pg]blۋgNˡ&Y#M_^g~/qƧ~aIC?;mta fgeӟ\8c=ǯ\}н5k˼Üomy؄a=s|ﯟ}nlca{_RߨxFoɇӹxK_,5_ c;`_l]/ ?9F&:181k7<+6tS#p>֣xk}kOAwۈ-N&uԧ7ceb]O}s<.z.XV]Xbi̙`sIET/j>xHS/~;+Óy~bV/i˅_L{Yb󜫛|G;}W_?slݟ={k {?W_g8q|c?ןK|i3}yV=Hy:Չ'BH_/_i-}ٷy6WW7w}s\sMX\qvӗ^'&Xqs8jw>^px=9y;_|>>׷=={v>ƙW/.8brbyO/Q3/,tabդ#6q]_,ƸT\ySfW7դ/WX~5J'.15˽rW_lWGe-Eѱ ̆#&6g:p51b5O}y7ҙ}5oOs~7TH.ZF֟jOMzO}jw}'}WlJw}w?9r\;ϟ==iYi?{+ogu{6+6R©+o\q7W|X߸ ;l8[J_/.4ʛ1Os;zR+^ ]XlחcOo(r|)'ob8"=HB: 0&4wk'3u]|VmbitrSO\}\ma72u3N}W+Ї4g8WPJ#2=/',a'_ŋغ͞ڳ[?l||\ OX_>oq[o=07_G^O{?'?ϟwG=??_ޣa{e}# _&Ead~,>^=ɇkO9]w;/~\x?]?.#S h_[?lj32jX\N^/x☵+g-?z3:mNsc X~'] k‹;`&xJ?tfԇaj4'_b/^'E!fE&mcC[WNM +ϪS&~#ԟk*zcef|"9oN}80iW+}:x돿ڣo'sf/ڟϽhOw9zooR G]ڵsżk~^]<ɺ^GX+oΥX||_Ҋ|bu_'09&wz,(ay?1?k*ի;? >Ə~'oFܐ<1-lLXrqY _ON=8xh?] 0ÄŌpX4O?x4~q֨I/zus-♮8k)V?rħZM=j|Z{F\>yޑ_s?oݏgau |Zs\ī/>e}ӼؑQ64Vʇ/8ncKS>+}XIozϦ~kjU#ּުM_/Ń;K<^Tq~B-B"ܹ@~f0bqɵauҫY%mSYҗӯq/>- _xYzqB:zCN,}<>F_?}}'L\kL:xz5W<Ļ+}qӗ]_^{Z}GL8w=|.e|ڏy~/yv:{8_/^/}>zƞ}5zߕ{YQ1bS F<0sΟoÎ\[cpY8q&.>FZ+gPW7I]ӫ.}s\0XW7m1&65'?7y&g Z5r9uBFafI/f]aNnǽ܋\}_I7LyS8>OY{ׯО\w[ͻ];aw\Os?=g{4wd];aq.C<c0οqZ].8V_iOK˽ggFvNJ畾զ̥%v;MPB1N5v!(nw T;_NN.68M1~/@IDATjFp׸{qҪVҷz3zyƯ~W>}+t\|6p6_US}3{竷8IzLLO}\L~r}EU}?=չ>]:?Ws<{<o}~j~zocpiK`7pebL ^8sV7,cZpMᾢZ~sh; f}}+}~ybS_]_^ >j=1b/"f[ my}w2l SV /?7^{qO}9.YX_p+ךP~X?~tc.wj7O/qٓ[y1}čֿ}>K~+tpwVx翜1}Sh>{}^?߹ 7ثyy 0zcxڍ+tɱuTgׇno9G;6W֟><G}S'~15˭ԕя',z :yNBMm,= 7J5Sw^XcMc f=O<.5iŻsԧWZ[g=bO8t{0TkO]sL<{JVM+{=~{^s,ϫ<<_=WWyc߿=߽ܣ~n1}3=4w#O\S|oi t~֯sO/=ϗcS_i.Gvg~pvӯx˽;.?nnѢ_iZ8kFけwB?o60ͫ3͇q/Ժ(KϘ Ç6,}XӪ8&&}I#|#\n_N4oXS|o+uԗccb>hZ?{o]LMukTxMp\WWG_ϧ!7//ZK|Q>~W7'3sGr>[{.zn8=yoޑ/6}?#Sﲸ ?￾7\~㘼[5FM萷~,}Yöxw\ 6mOj[^sa곿M5xX}\46m_d&'l9s׊u"ذ欍w#N\ ]Ɖw:3~n;[n-{u}uw,ErcS欅$κ}vpϾyЇuY|~rmoaCWZN͝,dֆ9|Þ1ͧ=|c/f>KtPcŗ;ogp>gȞ?=?{H׻G|t=tu2yw-{FsO7Ƈ肫;o:kFOyO5|o%g]dxƟo^gϦ[>=> [ ЯԞR%yM>Yɖ,)/>˜~ \Ovl{s.썳3Fم>oL/{ {tՏ/I_s3?3㟋~uOs^aOg׼W?M|y /?xlKÏM___{o;ן{7ܿ;+yo)w&;?sgkͫϹ=||: y'lȚ;'37msK/Й~ᅲ}?;la5򓼸&ͮ96Ϧ}K WQ75 _b ?}/v//#yqzg~/eB/"kmnp'גG?,hQ1Ug~3p81\ћh}o^yi㢏G'|vlwt~0i%=|xcވ ~g|Փ|Ƣ֕-_~Շc?w'g==Ԕ{ps#O^޽?tat_Zl|sk̏>~>:_tas#dsqƧota;|­~vɦ~})|-‹3yd݋{ޤ2?>&^L61ҁ^|yC6b3~/~:aj=y—7hƁ\1y?Fr1ǙY&N8U`ZkDyztK^z ^IN|OM]2|9Nt/VçW0s.rc1|>3l]Ίhb>sE򯑴?c~l/yƧ¿?63S}ms?ǟllkg?=cϟ=9Ns7/=s=ܻ!~[}yt_lm_ۜ/UMk׶ho6/b>owOe|k}Kn1M|wK'[m:բ[]/_E*`KĢtzAAڃu%=޴O Šy8lz(|ٴ MŃO/y?D/x"l&96_Fy+is.DF'}w<ءGӞ~P7|y_k'7=}ރv;gw;(NϬٚ;?? }9˴si|? -}!>^c(|r|b_~ؘ_g4_sƏݡ*/Kb |&|呜YN5'~|%R~ȎwƧl&!;t,J/??t/zx|]o9:1KsϏd/3z/|2P88{/3>=|GÈ~?l]yo:;v#'?s疿lϟ{rZ_Q0гޜq<~ݟQ>f#|?Æ׏ G=$+.} cx}=/26O/ixN|GXpm%Kx ћSMwVT[KG%Rȳ%/x{A/l=ado(]~'+gfg'Y>$OlovGalܜ.^[>;O'k?6?8iQ_#ych}'_lo?s=޿ܖ{bimG{vQw<9dtp/>0›#̔7Ӝ&1]rTx'?l-(>04ϴ/3~s슟O˟_?D}cR1;H$I@i>қFfSrEÆovɼ83| 腟хNOABn|&>}drB5 c6t }DV&>~`c |!辊1~xY;_]>oǙ΃g5?{g?(5/gܼ$kN}4>_؆/޾m1{Ќ1\-J yCoG_?X0ȋAό|5<}|͑=2c}yr6|z=/iO&~xdgk=-|vӞ~9*guo(t͏̿Ѣ79n.lڜ}>͋gҌXw?Lm}?X#|l-xi> _;}ώ|_;mf>رin⋭͍#wwvϿ?Wwjg?GW43U7l?~ݝ~p|?B0M#:Ɍᗇb:!>_b8:۾?cw]r([r6ѽ3c93'OZ*_zE+Zsї B%tt1z^HMdxpz99ΞO>>~l'>+\N,>7c}s8߼ξئMz'/n-?ޡ1d}Ote~ø9Aբ|_s-%͛Qk՜bu?;F3kd_z~@_W[xb Ϙ.tWINx0&>1 W;}Տo-Ona7vx'~:=d|…Y< H+bL/Y8I.*7.)%6֖lR!{Ȳ!CaSқ/~&~x7'B=oރ϶/_!>p9C9񓱛fda7oc} =O MhGag%g΋{X'sylϞ]cgl"{oUoTo/oT}$ ţ74ngO'[`qƧ<˿q|u?!=|c2z!>~c3|sGsdL_*Sl0/Vօk=S]NƧ?Dއx.~snKmn۩ C >[~[y_ Ӝg'~2v3&مwt/d\|ٕ]'k}5I^F )iX7֢^RWhNrcm<[݋=|Gœ3<҇U<3t˶1yLOKL/?C1'}>=Bt31]㗯<>^_[]\o8ą^bߵouaɞ:"" _;1K?نO_߃ot0s961:7;⿮_v\Ow=ܩx{;gCh%oOl{a?׿1ڣ{Os8gyvm^ ~:3G 3p#|s>˓>?1xxJ'|hC33+{aWJ9q/O f׋灉ņ,?ᇑaR$ç[˔翸7Z] ?ZZ~qt?[v3s??, Sn'>8Ԗz?Wǿ6߹mmm{??_}3l{9m1t1?_~w> |Ԙ_nx֟Mo'˧ԆFa;o9>7>߼}tܒTOol3%ћDXT^OB]̦v bo>l6l[_%̹WD 6 gx#|>QanKv1s`cqù|Ϯ~bO)9kI8}y=޿gоA!=3|f_۞s?[ -|zͅn>ھ&>ٙ?d>t>m>&m5º~oNcR6qNIb xN=(ut˖Vh?#|+|c\< W18ߟt=??,-5kͪo?bϟ"9sYѳwr7Cg+gPgQY?ǾYz_;1 ?Uz՛(gPPO>]ND ݋¯4Ϧӛ|-A[׵\.Z3~gokiϟ=74{{j[xL%úҙcOFo&yO)V ۘ0EO#?k7{OofwƟeG${ s_ %>r ?d}O|?9۷e7[Vmi+\s1aoF^vvu-m53om{>ʽ~oOza3&~~aEyI??}C!~_&>J  H r͵RK*l&/[l/G铧לسA?!=?Oo) 7Yx{=O֜W=M-=76?g M9||53N6/_i[jmU}ws{~g߿{>sм~:~sx7[4)gHQf~R{Ù/Ca'擞9|s_?L/O>|܋3CLhL*Eexk{ѽ| '|||Kڄι~1G!ٝNj?/ ?~~s~U׼{y׏~gƽi?s;ſȽ\loZA[=oGlokg]uߏ?/Ω^IU#gwk'>\E$|/~d)9~ԾO?HY3Y6ɲ#119^%T}Yz]_!9t_0|!!>]4&~1O}a ''sؠ9acwU ^>|*lOMxg{|4w]߹w]sc܍v}|~?پ4Im>?{͗qQ+P_?̥/nGg'_٧{^sM|?q?gwǦx##ܙĽ ^n xdl{ Ӌ0*^ڡR"Btf!N~Eg|:M6go|;+o&iO!̛9cdY_-s_g߮>vgnk{?o|j[pߟ}@}-?}hnaܜ?/L|:_vb4ģ> -"o3~ٳ/§GITb%E~/)^_A5ыc5'^1Rpᤇ?kGŕ,|-1g;/?'l' :6/5ЌO]hs_t[dNߴ{_|oumﯿ5{iw͠9g+}p???LngokH>}}]u?aTu=ᅬﯹF?1Vҽ{/1=/jd>Gy7Gs>0m=_漋S;CW x/c׊~sɤ&*2ġyQ(^^~/b7S_0¯8/|k~E?Ga/;m&7sqQ>/N_ÿWދ9.g[(g~zz-nH͟?m?s9OcO>b۽3#>?sa<}63>>>nmz|yo?\img|6(w3?;飙p"'/;WDM/:D3 #әsF{OotƧ?u~:Z7c\(r˧>Oҹ6o?j S^9/EuW{Wv9ӾZϟ빳dsz]Y[f?{D|;q8V7|_s|ƿjOode~-_AWLvLүsܣpuaM ϗɮv`P{u-|2~[O@x:8|ryi;|d(r/ ~'CFk? 9~:8?xYn{gvﺧc{Ϯ°ڷ>ݗO>9=cDoڝ}ð6~^SsƇa~~ŧ g^x<&x~Qj 4Hζdtg "{|N[:h{ _*2 p*|-۰7nYg>]ܳ9 n>K {0koןo۹o_D~cϟ=G,նG:W}4a-#شG>9Z<|ɴ͇[ߟEW?%#|Gܴ(n+/龚{$ޣ_Q+pԥK>Ttzh+&}?Z3?';/F{av3bb0y[_rӘ3~y|+Uޛ/׿ٰ߮u=ߞ??{;\twïݢ{|Lglͅ#|s([|O#/p Ql N^zYyqazƳC^KξCxt<~p [KO~O-|F_c 3񧛿n+_tӿ?٣8?D3>{o;}ni=nG={βW=JV|g~W,eSn/ЋP/l٧WL?? Nϯ\|js?Z6gd>}'Wf͵yߋ?[g恊U??vM| KE\Nov{ݲ;6IޘO<<+3X]7oFS~ybx.b1𒗷Ysw?Ͻ3_ڷWK_1셈 +Xt^Re^ o!LyͰkdsmsw*m:gfg^?,~917G~9{z&o̮Yo߸sw=go?j+?w{>=?O/gu8͂?}o=CF3~/gst/l>ݙ36$f^P""͗(0NqP{\8|>ҟ0M[5-|>K"Eg|N|6rQ?_qs;w߅_}[w3 u/_ ~>7N^|40<[l[{.dV65'vF'=T.6ڿP/'^֋2"'YB[g ?/ L|>&~ i殏DoW>=?⿗ſ߮vg=߿8 =įX~?~~SO_ɞŧOz_+7g~\⸎ҿNi+^e+dG/U6(@>!7&'NZ4cq-|sO^_G{O1//F>3oe?^ϣϞѣwϟ={|kSߟ#OyLyo|=GCӾKb=;8ȸ]1ߏ^ZE@Q>%Kc}+d-g>5Ó1637>6{J|]# =[_7[vwϟ=޿ ~]|s޽:m.Gwy[Ao^W9ygL qE,vROjm͚sogڹwٙgƻsg_{&ڱ[C;߽ >]Ʌ\?՟9ll;ќykj*G$WaA]^h/Q[K6ɮ>92F6|M} Lŧ5ΆjLEp~z3>{t7eⳙ>`Ho$3y/~6g|6(#il7[5c?׽X>c?z{g4j:Ӣ]ΜZWji_s[ٯ3ۏ6XڗwMg3Lh9UtPx)N]ĩzx:񐗏dž~cmdR)PN/>LO?;><'=wߋo?Uo߮]׻¼wgw}UoC?(;x>^6}W{G6W5?~l&~>ß Ī"+~ a癅Xҟ->yE>_1F:GO<~kAoOn<ϼoI>7[v׉vփ}|Onw߼5Ovo7>5gڔ|~ݧ"pk=|7~ϑWHL/W_.* aWo^J_EÃ5M|>,bĶ>L_*|q[7?|{ձ[ߚ?f֟}n7ID֜Z?=gm~Lv >|:as3|jա'x(?Jڨ\T ZOEN'>2FLMIO(ïi|]ߣp'9񍋽9/d6*׼/wOX]sgw=lֿ{u7Cww:/tZ?rQ|v]s'3IERPn( M^?LJڋ~\H|_/M8Mwⓡp sS?ky/g~,~q];_0AW+gϕיzʽvG߭ծ=0.SvUßx2g>Z|G??s 'ov)*Ud"pʍA+YLs^" c" ä́M&oN0ӟgy?ڏ6{rW߮]Ϟ ?{W7[?w.0wvV'C߾7{o~s̾|k3/:}$LAE^U7==t7(9Sk+zm>ϘBQ>ZOzgNS<V Gt׮]ow=n'Ξ?\?em~1bߟ[kݣ|D)t->*DrD;}+~{ǯ0{!Y|6gB;.vG3_瓯goN*o wϟ=z3Uۥz?~_y3tG ,lNˇ`Obbͦ~E<%'W<[au! ߘns߆(w󅉚3y;gw]{=K _uv?qk?OO|/O<3xڷCbS63|k=K@/ac懅)W v+ 9|'|8|rśyNs/{k~7=5=o^׿;Wܿ|#Im~ŀ} ދ9~<5:}W߿K@Iބ,F9T c\kL-~Zg;7o>}(|;)q?-~s[k7[GH0IDAT5m=g57v!_3Olc7&6~o|Ex~eg~x1sSt[Ix'xũpU ٽAM.[_t>H/_.poESW/oοvg5`-Xwwwwk75vwwϟ)٫%{7Ubs?|X.Ϝ~tLISJ\AˣB>/gwߊ+|~\d6GAqs+Cw߂<*[jm__uR6vg߽zNw_{({>wW|w ufG'};1IC^J{z+6΅Ln[t+T|}|TAh+| #j|rEJ!wլYsܺ:>}oo߮?<_{{ۢ7uȌF<1̀/}.lj *<7f41"W->M>_syi;^·ϟ/__}[oo߮=wǽ?7G"8-ߟ៿A/2Q] Ts,Wbw'CGL>7FS=_?tx9г+狍9G{g,~ceV?|?{ׯs,W{-(ZP|Խ=`$}k2Pk-p=/ïf@4#yuq?;2{<.=r]Qߍ_k 桵ٺjlݠ}_ :{?ܿwVpbޝ|OOVdr')r$-ْHWŧ}{h6EÏ6|`WkѝI\51\/<wU ^kpP9}|WuVѯEt"r7l~|3{we@◾7OU*fo=kO6 >ɺ0UK~=|ߵo^Ύތ_ $_iN?t]K/wg={c_V޿gwNyo-yzgKߘ6oXק wPN ٖkG< |>ѽşsiͿ/&vwgBϊqg=޿߽]I-޿=ǖ|?(ݯĿz޿ߞ^޷-~28?A%ѿ6IE~^L?~{&MM~\=5>1F.~)_ߣ޿6~_LqU;/wa-ۚТ]_xwŽ]uпw~-7fNȁ|D6ds?l;Jŗ5V.Yosu5}%7@dk'G)33M=>dV3ƭf.Ǜz|]̷'/3NxSm7G:5exS5w3 kۼj;$k3֏~_g&.,\i9Ou6[l yw•s̓Jurloןz@jʸOgןl\\s[_xyo2_Ռ:~c6[|ȅTSƿz\ l~AqF07^|_~7o3I[kF|{tYxX>Z/gziYMXJԻj\u蓥6O/ݩwX|/)_ >ϳ謹-m6+Vmdύ$=w31gqlwzRޠ&{Fe;f@ bvx6@{@zy?_m#:7r6[vi<vo9%kH{*?9ƵJwsv3G2:Xh~wxNw~4mqgo 3/rXʛS.C#:oGf<՗'TQ53GnuU^PSMUs2>5G6&5Q]jҦW.0x[9-wM)fq>I79-E]9ze|klMNkyrWNrnv#t7f`36~ѱd' ^ǟgfZ+d5F;[:oo5qnwZ>ܽ{y=]}n6Of >9o}d6Ǟxl|S?y0GfnVcr:Y3[s7|x߮jZ|Tcxvz~Ֆ:/vm*'Ӟ3cC l ؠh ^1zdt|/_͠s=zuoI5eyNw{pf}{Fg={6ߑ6>7_p1'u3SWÌ_N>_ͿZw]Wn{G1kEw{wf`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l6f`3 l#w]9z2&IENDB`slidge/docs/source/user/note.rst000066400000000000000000000013131477703150600172420ustar00rootroot00000000000000.. note:: These are the generic user docs for slidge. For :term:`Legacy Network`-specific docs, follow these links: `matridge `_, `matteridge `_, `messlidger `_, `skidge `_, `sleamdge `_, `slidcord `_, `slidge-whatsapp `_, `slidgnal `_, `slidgram `_. slidge/docs/source/user/register.rst000066400000000000000000000022661477703150600201310ustar00rootroot00000000000000Registration ============ To make it work, you must "link" the foreign accounts you want to use with your XMPP account by registering to the slidge server component. This is done either via the "Register" :term:`Ad-hoc Command` or :term:`Chatbot Command`. .. include:: note.rst Gajim ----- In gajim, go to the "accounts" menu and select "discover services". .. figure:: gajim.png :scale: 50 % :alt: Gajim service discovery Click "Command", then "Register". .. warning:: Clicking on "Register" work sometimes, but it uses in-band registration (:xep:`0077`) which is very basic and won't work for all legacy networks. "Command"→"Register" should be preferred. Movim ----- In Movim, you can see the gateways in settings->account. .. figure:: movim1.png :scale: 50 % :alt: Movim gateway discovery .. figure:: movim2.png :scale: 50 % :alt: Movim registration form An example registration in Movim. Other clients ------------- Other clients might have different UIs, but you can always fall back to the "register" :term:`Chatbot Command`. In your XMPP client, start a new conversation with JID ``superduper.example.org``. Type "register" and follow the instructions. slidge/pyproject.toml000066400000000000000000000123211477703150600152520ustar00rootroot00000000000000[project] name = "slidge" description = "XMPP bridging framework" requires-python = ">= 3.11" dependencies = [ "aiohttp[speedups]>=3.11.11,<4", "alembic>=1.14.0,<2", "configargparse>=1.7,<2", "defusedxml>=0.7.1", "pillow>=11.0.0,<12", "python-magic>=0.4.27,<0.5", "qrcode>=8.0,<9", "slixmpp>=1.10.0,<2", "sqlalchemy>=2,<3", "thumbhash>=0.1.2", ] authors = [ {name = "Nicolas Cedilnik", email = "nicoco@nicoco.fr"}, ] license = "AGPL-3.0-or-later" classifiers = [ "Topic :: Internet :: XMPP", "Topic :: Software Development :: Libraries :: Python Modules", ] keywords = ["xmpp", "gateway", "bridge", "instant messaging"] dynamic = ["version"] readme = "README.md" [build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["slidge*"] [tool.setuptools_scm] [project.scripts] slidge = 'slidge.main:main' [project.urls] Homepage = "https://codeberg.org/slidge/" Issues = "https://codeberg.org/slidge/slidge/issues" Repository = "https://codeberg.org/slidge/slidge/" "Chat room" = "https://conference.nicoco.fr:5281/muc_log/slidge/" Documentation = "https://slidge.im/docs/slidge/main" changelog = "https://codeberg.org/slidge/slidge/releases" [dependency-groups] dev = [ "coverage>=7.6.10", "emoji>=2.14.0", "furo>=2024.8.6", "mypy>=1.14.1", "pre-commit>=4.0.1", "pygments>=2.18.0", "pytest>=8.3.4", "pytest-asyncio>=0.25.0", "ruff>=0.8.4", "sphinx>=8.1.3", "sphinx-argparse>=0.5.2", "sphinx-autoapi>=3.4.0", "types-pillow>=10.2.0.20240822", "utidylib>=0.10", "xmldiff>=2.7.0", ] [tool.mypy] check_untyped_defs = true files = ["slidge", "superduper"] exclude = ["tests"] [[tool.mypy.overrides]] module = [ "thumbhash", "configargparse", "qrcode", ] ignore_missing_imports = true [[tool.mypy.overrides]] module = "slidge.slixfix.*" ignore_errors = true [tool.ruff] line-length = 88 exclude = ["xep_*", "tests"] [tool.ruff.lint] select = ["I"] [tool.coverage.run] source = ["slidge"] [tool.coverage.report] exclude_lines = [ "if .*TYPE_CHECKING:", "raise NotImplementedError" ] [tool.pytest.ini_options] log_level = "DEBUG" asyncio_mode = "strict" filterwarnings = [ "ignore:The object should be created within an async function:DeprecationWarning:aiohttp", "ignore:.*pkg_resources.*:DeprecationWarning", "ignore::UserWarning:slidge", "ignore:coroutine 'XMLStream._end_stream_wait' was never awaited.*:RuntimeWarning" ] [[tool.uv.index]] name = "codeberg" url = "https://codeberg.org/api/packages/slidge/pypi/simple" publish-url = "https://codeberg.org/api/packages/slidge/pypi" [tool.git-cliff.remote.gitea] owner = "slidge" repo = "slidge" [tool.git-cliff.changelog] header = """ # Changes """ body = """ {% for group, all_commits in commits | group_by(attribute="group") %} {%- if group == "cfix" %}{% continue %}{%- endif %} ## {{ group | striptags | trim | upper_first }} {% for commit in all_commits %} - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ {% if commit.breaking %}[**breaking**] {% endif %}\ {{ commit.message | split(pat="\n") | first | upper_first | trim }} \ ([`{{ commit.id | truncate(length=7, end="") }}`](./commit/{{ commit.id -}})\ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} {%- for cfix_commit in commits | filter(attribute="group", value="cfix") -%} {%- if cfix_commit.scope and commit.id is starting_with(cfix_commit.scope) -%} , [`{{ cfix_commit.id | truncate(length=7, end="") }}`](./commit/{{ cfix_commit.id }}){% if cfix_commit.remote.username %} by @{{ cfix_commit.remote.username }}{%- endif -%} {%- endif -%} {%- endfor -%})\ {% endfor %} {% endfor %}\n """ footer = """ """ [tool.git-cliff.git] conventional_commits = true filter_unconventional = false commit_parsers = [ { message = "^feat", group = "🚀 Features" }, { message = "^imprv", group = "✨ Improvements" }, { message = "^fix", group = "🐛 Bug Fixes" }, { message = "^compat", group = "💑 Compatibility" }, { message = "^refactor", group = "🚜 Refactor" }, { message = "^doc", group = "📚 Documentation" }, { message = "^perf", group = "⚡ Performance" }, { message = "^style", group = "🎨 Styling" }, { message = "^test", group = "🧪 Testing" }, { message = "^chore\\(release\\): prepare for", skip = true }, { message = "^chore\\(deps.*\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, { message = "^build\\(lockfile\\)", skip = true }, { message = "^chore\\(lockfile\\)", skip = true }, { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, { body = ".*security", group = "🛡️ Security" }, { message = "^revert", group = "◀️ Revert" }, { message = "^cfix", group = "cfix" }, { message = ".*", group = "💼 Other" }, ] slidge/slidge/000077500000000000000000000000001477703150600136065ustar00rootroot00000000000000slidge/slidge/__init__.py000066400000000000000000000033351477703150600157230ustar00rootroot00000000000000""" The main slidge package. Contains importable classes for a minimal function :term:`Legacy Module`. """ import sys import warnings from importlib.metadata import PackageNotFoundError, version from . import slixfix # noqa: F401 from .command import FormField, SearchResult # noqa: F401 from .contact import LegacyContact, LegacyRoster # noqa: F401 from .core import config as global_config # noqa: F401 from .core.gateway import BaseGateway # noqa: F401 from .core.session import BaseSession # noqa: F401 from .db import GatewayUser # noqa: F401 from .group import LegacyBookmarks, LegacyMUC, LegacyParticipant # noqa: F401 from .main import main as main_func from .util.types import MucType # noqa: F401 from .util.util import addLoggingLevel def entrypoint(module_name: str) -> None: """ Entrypoint to be used in ``__main__.py`` of :term:`legacy modules `. :param module_name: An importable :term:`Legacy Module`. """ main_func(module_name) def formatwarning(message, category, filename, lineno, line=""): return f"{filename}:{lineno}:{category.__name__}:{message}\n" warnings.formatwarning = formatwarning try: __version__ = version("slidge") except PackageNotFoundError: # package is not installed __version__ = "dev" __all__ = [ "__version__", "BaseGateway", "BaseSession", # For backwards compatibility, these names are still importable from the # top-level slidge module, but this is deprecated. # "GatewayUser", # "LegacyBookmarks", # "LegacyMUC", # "LegacyContact", # "LegacyParticipant", # "LegacyRoster", # "MucType", # "FormField", # "SearchResult", "entrypoint", "global_config", ] addLoggingLevel() slidge/slidge/__main__.py000066400000000000000000000000451477703150600156770ustar00rootroot00000000000000from slidge.main import main main() slidge/slidge/command/000077500000000000000000000000001477703150600152245ustar00rootroot00000000000000slidge/slidge/command/__init__.py000066400000000000000000000011451477703150600173360ustar00rootroot00000000000000""" This module implements an unified API to define :term:`adhoc ` or :term:`chatbot ` commands. Just subclass a :class:`Command`, and make sures it is imported in your legacy module's ``__init__.py``. """ from . import admin, register, user # noqa: F401 from .base import ( Command, CommandAccess, CommandResponseType, Confirmation, Form, FormField, SearchResult, TableResult, ) __all__ = ( "Command", "CommandAccess", "CommandResponseType", "Confirmation", "Form", "FormField", "SearchResult", "TableResult", ) slidge/slidge/command/adhoc.py000066400000000000000000000244751477703150600166700ustar00rootroot00000000000000import asyncio import functools import logging from functools import partial from typing import TYPE_CHECKING, Any, Callable, Optional, Union from slixmpp import JID, Iq # type: ignore[attr-defined] from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0004 import Form as SlixForm # type: ignore[attr-defined] from slixmpp.plugins.xep_0030.stanza.items import DiscoItems from ..core import config from ..util.util import strip_leading_emoji from . import Command, CommandResponseType, Confirmation, Form, TableResult from .base import FormField from .categories import CommandCategory if TYPE_CHECKING: from ..core.gateway import BaseGateway from ..core.session import BaseSession AdhocSessionType = dict[str, Any] class AdhocProvider: """ A slixmpp-like plugin to handle adhoc commands, with less boilerplate and untyped dict values than slixmpp. """ def __init__(self, xmpp: "BaseGateway") -> None: self.xmpp = xmpp self._commands = dict[str, Command]() self._categories = dict[str, list[Command]]() xmpp.plugin["xep_0030"].set_node_handler( "get_items", jid=xmpp.boundjid, node=self.xmpp.plugin["xep_0050"].stanza.Command.namespace, handler=self.get_items, ) async def __wrap_initial_handler( self, command: Command, iq: Iq, adhoc_session: AdhocSessionType ) -> AdhocSessionType: ifrom = iq.get_from() session = command.raise_if_not_authorized(ifrom) result = await self.__wrap_handler(command.run, session, ifrom) return await self.__handle_result(session, result, adhoc_session) async def __handle_category_list( self, category: CommandCategory, iq: Iq, adhoc_session: AdhocSessionType ) -> AdhocSessionType: try: session = self.xmpp.get_session_from_stanza(iq) except XMPPError: session = None commands: dict[str, Command] = {} for command in self._categories[category.node]: try: command.raise_if_not_authorized(iq.get_from()) except XMPPError: continue commands[command.NODE] = command if len(commands) == 0: raise XMPPError( "not-authorized", "There is no command you can run in this category" ) return await self.__handle_result( session, Form( category.name, "", [ FormField( var="command", label="Command", type="list-single", options=[ { "label": strip_leading_emoji_if_needed(command.NAME), "value": command.NODE, } for command in commands.values() ], ) ], partial(self.__handle_category_choice, commands), ), adhoc_session, ) async def __handle_category_choice( self, commands: dict[str, Command], form_values: dict[str, str], session: "BaseSession[Any, Any]", jid: JID, ): command = commands[form_values["command"]] result = await self.__wrap_handler(command.run, session, jid) return result async def __handle_result( self, session: Optional["BaseSession[Any, Any]"], result: CommandResponseType, adhoc_session: AdhocSessionType, ) -> AdhocSessionType: if isinstance(result, str) or result is None: adhoc_session["has_next"] = False adhoc_session["next"] = None adhoc_session["payload"] = None adhoc_session["notes"] = [("info", result or "Success!")] return adhoc_session if isinstance(result, Form): adhoc_session["next"] = partial(self.__wrap_form_handler, session, result) adhoc_session["has_next"] = True adhoc_session["payload"] = result.get_xml() return adhoc_session if isinstance(result, Confirmation): adhoc_session["next"] = partial(self.__wrap_confirmation, session, result) adhoc_session["has_next"] = True adhoc_session["payload"] = result.get_form() adhoc_session["next"] = partial(self.__wrap_confirmation, session, result) return adhoc_session if isinstance(result, TableResult): adhoc_session["next"] = None adhoc_session["has_next"] = False adhoc_session["payload"] = result.get_xml() return adhoc_session raise XMPPError("internal-server-error", text="OOPS!") @staticmethod async def __wrap_handler(f: Union[Callable, functools.partial], *a, **k): # type: ignore try: if asyncio.iscoroutinefunction(f): return await f(*a, **k) elif hasattr(f, "func") and asyncio.iscoroutinefunction(f.func): return await f(*a, **k) else: return f(*a, **k) except Exception as e: log.debug("Exception in %s", f, exc_info=e) raise XMPPError("internal-server-error", text=str(e)) async def __wrap_form_handler( self, session: Optional["BaseSession[Any, Any]"], result: Form, form: SlixForm, adhoc_session: AdhocSessionType, ) -> AdhocSessionType: form_values = result.get_values(form) new_result = await self.__wrap_handler( result.handler, form_values, session, adhoc_session["from"], *result.handler_args, **result.handler_kwargs, ) return await self.__handle_result(session, new_result, adhoc_session) async def __wrap_confirmation( self, session: Optional["BaseSession[Any, Any]"], confirmation: Confirmation, form: SlixForm, adhoc_session: AdhocSessionType, ) -> AdhocSessionType: if form.get_values().get("confirm"): # type: ignore[no-untyped-call] result = await self.__wrap_handler( confirmation.handler, session, adhoc_session["from"], *confirmation.handler_args, **confirmation.handler_kwargs, ) if confirmation.success: result = confirmation.success else: result = "You canceled the operation" return await self.__handle_result(session, result, adhoc_session) def register(self, command: Command, jid: Optional[JID] = None) -> None: """ Register a command as a adhoc command. this does not need to be called manually, ``BaseGateway`` takes care of that. :param command: :param jid: """ if jid is None: jid = self.xmpp.boundjid elif not isinstance(jid, JID): jid = JID(jid) if (category := command.CATEGORY) is None: if command.NODE in self._commands: raise RuntimeError( "There is already a command for the node '%s'", command.NODE ) self._commands[command.NODE] = command self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call] jid=jid, node=command.NODE, name=strip_leading_emoji_if_needed(command.NAME), handler=partial(self.__wrap_initial_handler, command), ) else: if isinstance(category, str): category = CommandCategory(category, category) node = category.node name = category.name if node not in self._categories: self._categories[node] = list[Command]() self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call] jid=jid, node=node, name=strip_leading_emoji_if_needed(name), handler=partial(self.__handle_category_list, category), ) self._categories[node].append(command) async def get_items(self, jid: JID, node: str, iq: Iq) -> DiscoItems: """ Get items for a disco query :param jid: the entity that should return its items :param node: which command node is requested :param iq: the disco query IQ :return: commands accessible to the given JID will be listed """ ifrom = iq.get_from() ifrom_str = str(ifrom) if ( not self.xmpp.jid_validator.match(ifrom_str) and ifrom_str not in config.ADMINS ): raise XMPPError( "forbidden", "You are not authorized to execute adhoc commands on this gateway. " "If this is unexpected, ask your administrator to verify that " "'user-jid-validator' is correctly set in slidge's configuration.", ) all_items = self.xmpp.plugin["xep_0030"].static.get_items(jid, node, None, None) log.debug("Static items: %r", all_items) if not all_items: return DiscoItems() filtered_items = DiscoItems() filtered_items["node"] = self.xmpp.plugin["xep_0050"].stanza.Command.namespace for item in all_items: authorized = True if item["node"] in self._categories: for command in self._categories[item["node"]]: try: command.raise_if_not_authorized(ifrom) except XMPPError: authorized = False else: authorized = True break else: try: self._commands[item["node"]].raise_if_not_authorized(ifrom) except XMPPError: authorized = False if authorized: filtered_items.append(item) return filtered_items def strip_leading_emoji_if_needed(text: str) -> str: if config.STRIP_LEADING_EMOJI_ADHOC: return strip_leading_emoji(text) return text log = logging.getLogger(__name__) slidge/slidge/command/admin.py000066400000000000000000000140251477703150600166700ustar00rootroot00000000000000# Commands only accessible for slidge admins import functools import importlib import logging from datetime import datetime from typing import Any, Optional from slixmpp import JID from slixmpp.exceptions import XMPPError from ..core import config from ..util.types import AnyBaseSession from .base import ( NODE_PREFIX, Command, CommandAccess, Confirmation, Form, FormField, FormValues, TableResult, ) from .categories import ADMINISTRATION NODE_PREFIX = NODE_PREFIX + "admin/" class AdminCommand(Command): ACCESS = CommandAccess.ADMIN_ONLY CATEGORY = ADMINISTRATION class ListUsers(AdminCommand): NAME = "👤 List registered users" HELP = "List the users registered to this gateway" CHAT_COMMAND = "list_users" NODE = NODE_PREFIX + CHAT_COMMAND async def run(self, _session, _ifrom, *_): items = [] for u in self.xmpp.store.users.get_all(): d = u.registration_date if d is None: joined = "" else: joined = d.isoformat(timespec="seconds") items.append({"jid": u.jid.bare, "joined": joined}) return TableResult( description="List of registered users", fields=[FormField("jid", type="jid-single"), FormField("joined")], items=items, # type:ignore ) class SlidgeInfo(AdminCommand): NAME = "ℹ️ Server information" HELP = "List the users registered to this gateway" CHAT_COMMAND = "info" NODE = NODE_PREFIX + CHAT_COMMAND ACCESS = CommandAccess.ANY async def run(self, _session, _ifrom, *_): start = self.xmpp.datetime_started # type:ignore uptime = datetime.now() - start if uptime.days: days_ago = f"{uptime.days} day{'s' if uptime.days != 1 else ''}" else: days_ago = None hours, seconds = divmod(uptime.seconds, 3600) if hours: hours_ago = f"{hours} hour" if hours != 1: hours_ago += "s" else: hours_ago = None minutes, seconds = divmod(seconds, 60) if minutes: minutes_ago = f"{minutes} minute" if minutes_ago != 1: minutes_ago += "s" else: minutes_ago = None if any((days_ago, hours_ago, minutes_ago)): seconds_ago = None else: seconds_ago = f"{seconds} second" if seconds != 1: seconds_ago += "s" ago = ", ".join( [a for a in (days_ago, hours_ago, minutes_ago, seconds_ago) if a] ) legacy_module = importlib.import_module(config.LEGACY_MODULE) version = getattr(legacy_module, "__version__", "No version") import slidge return ( f"{self.xmpp.COMPONENT_NAME} (slidge core {slidge.__version__}," f" {config.LEGACY_MODULE} {version})\n" f"Up since {start:%Y-%m-%d %H:%M} ({ago} ago)" ) class DeleteUser(AdminCommand): NAME = "❌ Delete a user" HELP = "Unregister a user from the gateway" CHAT_COMMAND = "delete_user" NODE = NODE_PREFIX + CHAT_COMMAND async def run(self, _session, _ifrom, *_): return Form( title="Remove a slidge user", instructions="Enter the bare JID of the user you want to delete", fields=[FormField("jid", type="jid-single", label="JID", required=True)], handler=self.delete, ) async def delete( self, form_values: FormValues, _session: AnyBaseSession, _ifrom: JID ) -> Confirmation: jid: JID = form_values.get("jid") # type:ignore user = self.xmpp.store.users.get(jid) if user is None: raise XMPPError("item-not-found", text=f"There is no user '{jid}'") return Confirmation( prompt=f"Are you sure you want to unregister '{jid}' from slidge?", success=f"User {jid} has been deleted", handler=functools.partial(self.finish, jid=jid), ) async def finish( self, _session: Optional[AnyBaseSession], _ifrom: JID, jid: JID ) -> None: user = self.xmpp.store.users.get(jid) if user is None: raise XMPPError("bad-request", f"{jid} has no account here!") await self.xmpp.unregister_user(user) class ChangeLoglevel(AdminCommand): NAME = "📋 Change the verbosity of the logs" HELP = "Set the logging level" CHAT_COMMAND = "loglevel" NODE = NODE_PREFIX + CHAT_COMMAND async def run(self, _session, _ifrom, *_): return Form( title=self.NAME, instructions=self.HELP, fields=[ FormField( "level", label="Log level", required=True, type="list-single", options=[ {"label": "WARNING (quiet)", "value": str(logging.WARNING)}, {"label": "INFO (normal)", "value": str(logging.INFO)}, {"label": "DEBUG (verbose)", "value": str(logging.DEBUG)}, ], ) ], handler=self.finish, ) @staticmethod async def finish( form_values: FormValues, _session: AnyBaseSession, _ifrom: JID ) -> None: logging.getLogger().setLevel(int(form_values["level"])) # type:ignore class Exec(AdminCommand): NAME = HELP = "Exec arbitrary python code. SHOULD NEVER BE AVAILABLE IN PROD." CHAT_COMMAND = "!" NODE = "exec" ACCESS = CommandAccess.ADMIN_ONLY prev_snapshot = None context = dict[str, Any]() def __init__(self, xmpp): super().__init__(xmpp) async def run(self, session, ifrom: JID, *args): from contextlib import redirect_stdout from io import StringIO f = StringIO() with redirect_stdout(f): exec(" ".join(args), self.context) out = f.getvalue() if out: return f"```\n{out}\n```" else: return "No output" slidge/slidge/command/base.py000066400000000000000000000322071477703150600165140ustar00rootroot00000000000000from abc import ABC from dataclasses import dataclass, field from enum import Enum from typing import ( TYPE_CHECKING, Any, Awaitable, Callable, Iterable, Optional, Sequence, Type, TypedDict, Union, ) from slixmpp import JID # type: ignore[attr-defined] from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0004 import Form as SlixForm # type: ignore[attr-defined] from slixmpp.plugins.xep_0004 import ( FormField as SlixFormField, # type: ignore[attr-defined] ) from slixmpp.types import JidStr from ..core import config from ..util.types import AnyBaseSession, FieldType NODE_PREFIX = "https://slidge.im/command/core/" if TYPE_CHECKING: from ..core.gateway import BaseGateway from ..core.session import BaseSession from .categories import CommandCategory HandlerType = Union[ Callable[[AnyBaseSession, JID], "CommandResponseType"], Callable[[AnyBaseSession, JID], Awaitable["CommandResponseType"]], ] FormValues = dict[str, Union[str, JID, bool]] FormHandlerType = Callable[ [FormValues, AnyBaseSession, JID], Awaitable["CommandResponseType"], ] ConfirmationHandlerType = Callable[ [Optional[AnyBaseSession], JID], Awaitable["CommandResponseType"] ] @dataclass class TableResult: """ Structured data as the result of a command """ fields: Sequence["FormField"] """ The 'columns names' of the table. """ items: Sequence[dict[str, Union[str, JID]]] """ The rows of the table. Each row is a dict where keys are the fields ``var`` attribute. """ description: str """ A description of the content of the table. """ jids_are_mucs: bool = False def get_xml(self) -> SlixForm: """ Get a slixmpp "form" (with header)to represent the data :return: some XML """ form = SlixForm() # type: ignore[no-untyped-call] form["type"] = "result" form["title"] = self.description for f in self.fields: form.add_reported(f.var, label=f.label, type=f.type) # type: ignore[no-untyped-call] for item in self.items: form.add_item({k: str(v) for k, v in item.items()}) # type: ignore[no-untyped-call] return form @dataclass class SearchResult(TableResult): """ Results of the search command (search for contacts via Jabber Search) Return type of :meth:`BaseSession.search`. """ description: str = "Contact search results" @dataclass class Confirmation: """ A confirmation 'dialog' """ prompt: str """ The text presented to the command triggering user """ handler: ConfirmationHandlerType """ An async function that should return a ResponseType """ success: Optional[str] = None """ Text in case of success, used if handler does not return anything """ handler_args: Iterable[Any] = field(default_factory=list) """ arguments passed to the handler """ handler_kwargs: dict[str, Any] = field(default_factory=dict) """ keyword arguments passed to the handler """ def get_form(self) -> SlixForm: """ Get the slixmpp form :return: some xml """ form = SlixForm() # type: ignore[no-untyped-call] form["type"] = "form" form["title"] = self.prompt form.append( FormField( "confirm", type="boolean", value="true", label="Confirm" ).get_xml() ) return form @dataclass class Form: """ A form, to request user input """ title: str instructions: str fields: Sequence["FormField"] handler: FormHandlerType handler_args: Iterable[Any] = field(default_factory=list) handler_kwargs: dict[str, Any] = field(default_factory=dict) def get_values( self, slix_form: SlixForm ) -> dict[str, Union[list[str], list[JID], str, JID, bool, None]]: """ Parse form submission :param slix_form: the xml received as the submission of a form :return: A dict where keys=field.var and values are either strings or JIDs (if field.type=jid-single) """ str_values: dict[str, str] = slix_form.get_values() # type: ignore[no-untyped-call] values = {} for f in self.fields: values[f.var] = f.validate(str_values.get(f.var)) return values def get_xml(self) -> SlixForm: """ Get the slixmpp "form" :return: some XML """ form = SlixForm() # type: ignore[no-untyped-call] form["type"] = "form" form["title"] = self.title form["instructions"] = self.instructions for fi in self.fields: form.append(fi.get_xml()) return form class CommandAccess(int, Enum): """ Defines who can access a given Command """ ADMIN_ONLY = 0 USER = 1 USER_LOGGED = 2 USER_NON_LOGGED = 3 NON_USER = 4 ANY = 5 class Option(TypedDict): """ Options to be used for ``FormField``s of type ``list-*`` """ label: str value: str # TODO: support forms validation XEP-0122 @dataclass class FormField: """ Represents a field of the form that a user will see when registering to the gateway via their XMPP client. """ var: str = "" """ Internal name of the field, will be used to retrieve via :py:attr:`slidge.GatewayUser.registration_form` """ label: Optional[str] = None """Description of the field that the user will see""" required: bool = False """Whether this field is mandatory or not""" private: bool = False """ For sensitive info that should not be displayed on screen while the user types. Forces field_type to "text-private" """ type: FieldType = "text-single" """Type of the field, see `XEP-0004 `_""" value: str = "" """Pre-filled value. Will be automatically pre-filled if a registered user modifies their subscription""" options: Optional[list[Option]] = None image_url: Optional[str] = None """An image associated to this field, eg, a QR code""" def __post_init__(self) -> None: if self.private: self.type = "text-private" def __acceptable_options(self) -> list[str]: if self.options is None: raise RuntimeError return [x["value"] for x in self.options] def validate( self, value: Optional[Union[str, list[str]]] ) -> Union[list[str], list[JID], str, JID, bool, None]: """ Raise appropriate XMPPError if a given value is valid for this field :param value: The value to test :return: The same value OR a JID if ``self.type=jid-single`` """ if isinstance(value, list) and not self.type.endswith("multi"): raise XMPPError("not-acceptable", "A single value was expected") if self.type in ("list-multi", "jid-multi"): if not value: value = [] if isinstance(value, list): return self.__validate_list_multi(value) else: raise XMPPError("not-acceptable", "Multiple values was expected") assert isinstance(value, (str, bool, JID)) or value is None if self.required and value is None: raise XMPPError("not-acceptable", f"Missing field: '{self.label}'") if value is None: return None if self.type == "jid-single": try: return JID(value) except ValueError: raise XMPPError("not-acceptable", f"Not a valid JID: '{value}'") elif self.type == "list-single": if value not in self.__acceptable_options(): raise XMPPError("not-acceptable", f"Not a valid option: '{value}'") elif self.type == "boolean": return value.lower() in ("1", "true") if isinstance(value, str) else value return value def __validate_list_multi(self, value: list[str]) -> Union[list[str], list[JID]]: # COMPAT: all the "if v" and "if not v" are workarounds for https://codeberg.org/slidge/slidge/issues/43 # They should be reverted once the bug is fixed upstream, cf https://soprani.ca/todo/390 for v in value: if v not in self.__acceptable_options(): if not v: continue raise XMPPError("not-acceptable", f"Not a valid option: '{v}'") if self.type == "list-multi": return [v for v in value if v] return [JID(v) for v in value if v] def get_xml(self) -> SlixFormField: """ Get the field in slixmpp format :return: some XML """ f = SlixFormField() f["var"] = self.var f["label"] = self.label f["required"] = self.required f["type"] = self.type if self.options: for o in self.options: f.add_option(**o) # type: ignore[no-untyped-call] f["value"] = self.value if self.image_url: f["media"].add_uri(self.image_url, itype="image/png") return f CommandResponseType = Union[TableResult, Confirmation, Form, str, None] class Command(ABC): """ Abstract base class to implement gateway commands (chatbot and ad-hoc) """ NAME: str = NotImplemented """ Friendly name of the command, eg: "do something with stuff" """ HELP: str = NotImplemented """ Long description of what the command does """ NODE: str = NotImplemented """ Name of the node used for ad-hoc commands """ CHAT_COMMAND: str = NotImplemented """ Text to send to the gateway to trigger the command via a message """ ACCESS: "CommandAccess" = NotImplemented """ Who can use this command """ CATEGORY: Optional[Union[str, "CommandCategory"]] = None """ If used, the command will be under this top-level category. Use the same string for several commands to group them. This hierarchy only used for the adhoc interface, not the chat command interface. """ subclasses = list[Type["Command"]]() def __init__(self, xmpp: "BaseGateway"): self.xmpp = xmpp def __init_subclass__(cls, **kwargs: Any) -> None: # store subclasses so subclassing is enough for the command to be # picked up by slidge cls.subclasses.append(cls) async def run( self, session: Optional["BaseSession[Any, Any]"], ifrom: JID, *args: str ) -> CommandResponseType: """ Entry point of the command :param session: If triggered by a registered user, its slidge Session :param ifrom: JID of the command-triggering entity :param args: When triggered via chatbot type message, additional words after the CHAT_COMMAND string was passed :return: Either a TableResult, a Form, a Confirmation, a text, or None """ raise XMPPError("feature-not-implemented") def _get_session(self, jid: JID) -> Optional["BaseSession[Any, Any]"]: user = self.xmpp.store.users.get(jid) if user is None: return None return self.xmpp.get_session_from_user(user) def __can_use_command(self, jid: JID): j = jid.bare return self.xmpp.jid_validator.match(j) or j in config.ADMINS def raise_if_not_authorized(self, jid: JID) -> Optional["BaseSession[Any, Any]"]: """ Raise an appropriate error is jid is not authorized to use the command :param jid: jid of the entity trying to access the command :return:session of JID if it exists """ session = self._get_session(jid) if not self.__can_use_command(jid): raise XMPPError( "bad-request", "Your JID is not allowed to use this gateway." ) if self.ACCESS == CommandAccess.ADMIN_ONLY and not is_admin(jid): raise XMPPError("not-authorized") elif self.ACCESS == CommandAccess.NON_USER and session is not None: raise XMPPError( "bad-request", "This is only available for non-users. Unregister first." ) elif self.ACCESS == CommandAccess.USER and session is None: raise XMPPError( "forbidden", "This is only available for users that are registered to this gateway", ) elif self.ACCESS == CommandAccess.USER_NON_LOGGED: if session is None or session.logged: raise XMPPError( "forbidden", ( "This is only available for users that are not logged to the" " legacy service" ), ) elif self.ACCESS == CommandAccess.USER_LOGGED: if session is None or not session.logged: raise XMPPError( "forbidden", ( "This is only available when you are logged in to the legacy" " service" ), ) return session def is_admin(jid: JidStr) -> bool: return JID(jid).bare in config.ADMINS slidge/slidge/command/categories.py000066400000000000000000000005371477703150600177300ustar00rootroot00000000000000from typing import NamedTuple from .base import NODE_PREFIX class CommandCategory(NamedTuple): name: str node: str ADMINISTRATION = CommandCategory("🛷️ Slidge administration", NODE_PREFIX + "admin") CONTACTS = CommandCategory("👤 Contacts", NODE_PREFIX + "contacts") GROUPS = CommandCategory("👥 Groups", NODE_PREFIX + "groups") slidge/slidge/command/chat_command.py000066400000000000000000000256211477703150600202210ustar00rootroot00000000000000# Handle slidge commands by exchanging chat messages with the gateway components. # Ad-hoc methods should provide a better UX, but some clients do not support them, # so this is mostly a fallback. import asyncio import functools import logging from typing import TYPE_CHECKING, Callable, Literal, Optional, Union, overload from urllib.parse import quote as url_quote from slixmpp import JID, CoroutineCallback, Message, StanzaPath from slixmpp.exceptions import XMPPError from slixmpp.types import JidStr, MessageTypes from . import Command, CommandResponseType, Confirmation, Form, TableResult from .categories import CommandCategory if TYPE_CHECKING: from ..core.gateway import BaseGateway class ChatCommandProvider: UNKNOWN = "Wut? I don't know that command: {}" def __init__(self, xmpp: "BaseGateway"): self.xmpp = xmpp self._keywords = list[str]() self._commands: dict[str, Command] = {} self._input_futures = dict[str, asyncio.Future[str]]() self.xmpp.register_handler( CoroutineCallback( "chat_command_handler", StanzaPath(f"message@to={self.xmpp.boundjid.bare}"), self._handle_message, # type: ignore ) ) def register(self, command: Command): """ Register a command to be used via chat messages with the gateway Plugins should not call this, any class subclassing Command should be automatically added by slidge core. :param command: the new command """ t = command.CHAT_COMMAND if t in self._commands: raise RuntimeError("There is already a command triggered by '%s'", t) self._commands[t] = command @overload async def input( self, jid: JidStr, text: Optional[str], blocking: Literal[False] ) -> asyncio.Future[str]: ... @overload async def input( self, jid: JidStr, text: Optional[str], mtype: MessageTypes = ..., blocking: Literal[True] = ..., ) -> str: ... async def input( self, jid, text=None, mtype="chat", timeout=60, blocking=True, **msg_kwargs, ): """ Request arbitrary user input using a simple chat message, and await the result. You shouldn't need to call directly bust instead use :meth:`.BaseSession.input` to directly target a user. NB: When using this, the next message that the user sent to the component will not be transmitted to :meth:`.BaseGateway.on_gateway_message`, but rather intercepted. Await the coroutine to get its content. :param jid: The JID we want input from :param text: A prompt to display for the user :param mtype: Message type :param timeout: :param blocking: If set to False, timeout has no effect and an :class:`asyncio.Future` is returned instead of a str :return: The user's reply """ jid = JID(jid) if text is not None: self.xmpp.send_message( mto=jid, mbody=text, mtype=mtype, mfrom=self.xmpp.boundjid.bare, **msg_kwargs, ) f = asyncio.get_event_loop().create_future() self._input_futures[jid.bare] = f if not blocking: return f try: await asyncio.wait_for(f, timeout) except asyncio.TimeoutError: self.xmpp.send_message( mto=jid, mbody="You took too much time to reply", mtype=mtype, mfrom=self.xmpp.boundjid.bare, ) del self._input_futures[jid.bare] raise XMPPError("remote-server-timeout", "You took too much time to reply") return f.result() async def _handle_message(self, msg: Message): if not msg["body"]: return if not msg.get_from().node: return # ignore component and server messages f = self._input_futures.pop(msg.get_from().bare, None) if f is not None: f.set_result(msg["body"]) return c = msg["body"] first_word, *rest = c.split(" ") first_word = first_word.lower() if first_word == "help": return self._handle_help(msg, *rest) mfrom = msg.get_from() command = self._commands.get(first_word) if command is None: return self._not_found(msg, first_word) try: session = command.raise_if_not_authorized(mfrom) except XMPPError as e: reply = msg.reply() reply["body"] = e.text reply.send() raise result = await self.__wrap_handler(msg, command.run, session, mfrom, *rest) self.xmpp.delivery_receipt.ack(msg) return await self._handle_result(result, msg, session) def __make_uri(self, body: str) -> str: return f"xmpp:{self.xmpp.boundjid.bare}?message;body={body}" async def _handle_result(self, result: CommandResponseType, msg: Message, session): if isinstance(result, str) or result is None: reply = msg.reply() reply["body"] = result or "End of command." reply.send() return if isinstance(result, Form): form_values = {} for t in result.title, result.instructions: if t: msg.reply(t).send() for f in result.fields: if f.type == "fixed": msg.reply(f"{f.label or f.var}: {f.value}").send() else: if f.type == "list-multi": msg.reply( "Multiple selection allowed, use new lines as a separator, ie, " "one selected item per line. To select no item, reply with a space " "(the punctuation)." ).send() if f.options: for o in f.options: msg.reply( f"{o['label']}: {self.__make_uri(o['value'])}" ).send() if f.value: msg.reply(f"Default: {f.value}").send() if f.type == "boolean": msg.reply("yes: " + self.__make_uri("yes")).send() msg.reply("no: " + self.__make_uri("no")).send() ans = await self.xmpp.input( msg.get_from(), (f.label or f.var) + "? (or 'abort')" ) if ans.lower() == "abort": return await self._handle_result( "Command aborted", msg, session ) if f.type == "boolean": if ans.lower() == "yes": ans = "true" else: ans = "false" if f.type.endswith("multi"): choices = [] if ans == " " else ans.split("\n") form_values[f.var] = f.validate(choices) else: form_values[f.var] = f.validate(ans) result = await self.__wrap_handler( msg, result.handler, form_values, session, msg.get_from(), *result.handler_args, **result.handler_kwargs, ) return await self._handle_result(result, msg, session) if isinstance(result, Confirmation): yes_or_no = await self.input(msg.get_from(), result.prompt) if not yes_or_no.lower().startswith("y"): reply = msg.reply() reply["body"] = "Canceled" reply.send() return result = await self.__wrap_handler( msg, result.handler, session, msg.get_from(), *result.handler_args, **result.handler_kwargs, ) return await self._handle_result(result, msg, session) if isinstance(result, TableResult): if len(result.items) == 0: msg.reply("Empty results").send() return body = result.description + "\n" for item in result.items: for f in result.fields: if f.type == "jid-single": j = JID(item[f.var]) value = f"xmpp:{percent_encode(j)}" if result.jids_are_mucs: value += "?join" else: value = item[f.var] # type:ignore body += f"\n{f.label or f.var}: {value}" msg.reply(body).send() @staticmethod async def __wrap_handler(msg, f: Union[Callable, functools.partial], *a, **k): try: if asyncio.iscoroutinefunction(f): return await f(*a, **k) elif hasattr(f, "func") and asyncio.iscoroutinefunction(f.func): return await f(*a, **k) else: return f(*a, **k) except Exception as e: log.debug("Error in %s", f, exc_info=e) reply = msg.reply() reply["body"] = f"Error: {e}" reply.send() def _handle_help(self, msg: Message, *rest): if len(rest) == 0: reply = msg.reply() reply["body"] = self._help(msg.get_from()) reply.send() elif len(rest) == 1 and (command := self._commands.get(rest[0])): reply = msg.reply() reply["body"] = f"{command.CHAT_COMMAND}: {command.NAME}\n{command.HELP}" reply.send() else: self._not_found(msg, str(rest)) def _help(self, mfrom: JID): msg = "Available commands:" for c in sorted( self._commands.values(), key=lambda co: ( ( co.CATEGORY if isinstance(co.CATEGORY, str) else ( co.CATEGORY.name if isinstance(co.CATEGORY, CommandCategory) else "" ) ), co.CHAT_COMMAND, ), ): try: c.raise_if_not_authorized(mfrom) except XMPPError: continue msg += f"\n{c.CHAT_COMMAND} -- {c.NAME}" return msg def _not_found(self, msg: Message, word: str): e = self.UNKNOWN.format(word) msg.reply(e).send() raise XMPPError("item-not-found", e) def percent_encode(jid: JID): return f"{url_quote(jid.user)}@{jid.server}" # type:ignore log = logging.getLogger(__name__) slidge/slidge/command/register.py000066400000000000000000000151041477703150600174230ustar00rootroot00000000000000""" This module handles the registration :term:`Command`, which is a necessary step for a JID to become a slidge :term:`User`. """ import asyncio import functools import tempfile from enum import IntEnum from typing import Any import qrcode from slixmpp import JID, Iq from slixmpp.exceptions import XMPPError from ..core import config from ..db import GatewayUser from ..util.types import UserPreferences from .base import Command, CommandAccess, Form, FormField, FormValues from .user import Preferences class RegistrationType(IntEnum): """ An :class:`Enum` to define the registration flow. """ SINGLE_STEP_FORM = 0 """ 1 step, 1 form, the only flow compatible with :xep:`0077`. Using this, the whole flow is defined by :attr:`slidge.BaseGateway.REGISTRATION_FIELDS` and :attr:`.REGISTRATION_INSTRUCTIONS`. """ QRCODE = 10 """ The registration requires flashing a QR code in an official client. See :meth:`slidge.BaseGateway.send_qr`, :meth:`.get_qr_text` and :meth:`.confirm_qr`. """ TWO_FACTOR_CODE = 20 """ The registration requires confirming login with a 2FA code, eg something received by email or SMS to finalize the authentication. See :meth:`.validate_two_factor_code`. """ class TwoFactorNotRequired(Exception): """ Should be raised in :meth:`slidge.BaseGateway.validate` if the code is not required after all. This can happen for a :term:`Legacy Network` where 2FA is optional. """ pass class Register(Command): NAME = "📝 Register to the gateway" HELP = "Link your JID to this gateway" NODE = "jabber:iq:register" CHAT_COMMAND = "register" ACCESS = CommandAccess.NON_USER SUCCESS_MESSAGE = "Success, welcome!" def _finalize( self, form_values: UserPreferences, _session, ifrom: JID, user: GatewayUser, *_ ) -> str: user.preferences = form_values # type: ignore self.xmpp.store.users.update(user) self.xmpp.event("user_register", Iq(sfrom=ifrom.bare)) return self.SUCCESS_MESSAGE async def run(self, _session, ifrom: JID, *_): self.xmpp.raise_if_not_allowed_jid(ifrom) return Form( title=f"Registration to '{self.xmpp.COMPONENT_NAME}'", instructions=self.xmpp.REGISTRATION_INSTRUCTIONS, fields=self.xmpp.REGISTRATION_FIELDS, handler=self.register, ) async def register(self, form_values: dict[str, Any], _session, ifrom: JID): two_fa_needed = True try: data = await self.xmpp.user_prevalidate(ifrom, form_values) except ValueError as e: raise XMPPError("bad-request", str(e)) except TwoFactorNotRequired: data = None if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE: two_fa_needed = False else: raise user = GatewayUser( jid=JID(ifrom.bare), legacy_module_data=form_values if data is None else data, ) if self.xmpp.REGISTRATION_TYPE == RegistrationType.SINGLE_STEP_FORM or ( self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE and not two_fa_needed ): return await self.preferences(user) if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE: return Form( title=self.xmpp.REGISTRATION_2FA_TITLE, instructions=self.xmpp.REGISTRATION_2FA_INSTRUCTIONS, fields=[FormField("code", label="Code", required=True)], handler=functools.partial(self.two_fa, user=user), ) elif self.xmpp.REGISTRATION_TYPE == RegistrationType.QRCODE: self.xmpp.qr_pending_registrations[ # type:ignore user.jid.bare ] = self.xmpp.loop.create_future() qr_text = await self.xmpp.get_qr_text(user) qr = qrcode.make(qr_text) with tempfile.NamedTemporaryFile( suffix=".png", delete=config.NO_UPLOAD_METHOD != "move" ) as f: qr.save(f.name) img_url, _ = await self.xmpp.send_file(f.name, mto=ifrom) if img_url is None: raise XMPPError( "internal-server-error", "Slidge cannot send attachments" ) self.xmpp.send_text(qr_text, mto=ifrom) return Form( title="Flash this", instructions="Flash this QR in the appropriate place", fields=[ FormField( "qr_img", type="fixed", value=qr_text, image_url=img_url, ), FormField( "qr_text", type="fixed", value=qr_text, label="Text encoded in the QR code", ), FormField( "qr_img_url", type="fixed", value=img_url, label="URL of the QR code image", ), ], handler=functools.partial(self.qr, user=user), ) async def two_fa( self, form_values: FormValues, _session, _ifrom, user: GatewayUser ): assert isinstance(form_values["code"], str) data = await self.xmpp.validate_two_factor_code(user, form_values["code"]) if data is not None: user.legacy_module_data.update(data) return await self.preferences(user) async def qr(self, _form_values: FormValues, _session, _ifrom, user: GatewayUser): try: data = await asyncio.wait_for( self.xmpp.qr_pending_registrations[user.jid.bare], # type:ignore config.QR_TIMEOUT, ) except asyncio.TimeoutError: raise XMPPError( "remote-server-timeout", ( "It does not seem that the QR code was correctly used, " "or you took too much time" ), ) if data is not None: user.legacy_module_data.update(data) return await self.preferences(user) async def preferences(self, user: GatewayUser) -> Form: return Form( title="Preferences", instructions=Preferences.HELP, fields=self.xmpp.PREFERENCES, handler=functools.partial(self._finalize, user=user), # type:ignore ) slidge/slidge/command/user.py000066400000000000000000000300171477703150600165550ustar00rootroot00000000000000# Commands available to users from copy import deepcopy from typing import TYPE_CHECKING, Any, Optional, Union, cast from slixmpp import JID # type:ignore[attr-defined] from slixmpp.exceptions import XMPPError from ..group.room import LegacyMUC from ..util.types import AnyBaseSession, LegacyGroupIdType, UserPreferences from .base import ( Command, CommandAccess, Confirmation, Form, FormField, FormValues, SearchResult, TableResult, ) from .categories import CONTACTS, GROUPS if TYPE_CHECKING: pass class Search(Command): NAME = "🔎 Search for contacts" HELP = "Search for contacts via this gateway" CHAT_COMMAND = "find" NODE = CONTACTS.node + "/" + CHAT_COMMAND ACCESS = CommandAccess.USER_LOGGED CATEGORY = CONTACTS async def run( self, session: Optional[AnyBaseSession], _ifrom: JID, *args: str ) -> Union[Form, SearchResult, None]: if args: assert session is not None return await session.on_search( {self.xmpp.SEARCH_FIELDS[0].var: " ".join(args)} ) return Form( title=self.xmpp.SEARCH_TITLE, instructions=self.xmpp.SEARCH_INSTRUCTIONS, fields=self.xmpp.SEARCH_FIELDS, handler=self.search, ) @staticmethod async def search( form_values: FormValues, session: Optional[AnyBaseSession], _ifrom: JID ) -> SearchResult: assert session is not None results = await session.on_search(form_values) # type: ignore if results is None: raise XMPPError("item-not-found", "No contact was found") return results class SyncContacts(Command): NAME = "🔄 Sync XMPP roster" HELP = ( "Synchronize your XMPP roster with your legacy contacts. " "Slidge will only add/remove/modify contacts in its dedicated roster group" ) CHAT_COMMAND = "sync-contacts" NODE = CONTACTS.node + "/" + CHAT_COMMAND ACCESS = CommandAccess.USER_LOGGED CATEGORY = CONTACTS async def run(self, session: Optional[AnyBaseSession], _ifrom, *_) -> Confirmation: return Confirmation( prompt="Are you sure you want to sync your roster?", success=None, handler=self.sync, ) async def sync(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str: if session is None: raise RuntimeError roster_iq = await self.xmpp["xep_0356"].get_roster(session.user_jid.bare) contacts = session.contacts.known_contacts() added = 0 removed = 0 updated = 0 for item in roster_iq["roster"]: groups = set(item["groups"]) if self.xmpp.ROSTER_GROUP in groups: contact = contacts.pop(item["jid"], None) if contact is None: if len(groups) == 1: await self.xmpp["xep_0356"].set_roster( session.user_jid, {item["jid"]: {"subscription": "remove"}} ) removed += 1 else: groups.remove(self.xmpp.ROSTER_GROUP) await self.xmpp["xep_0356"].set_roster( session.user_jid, { item["jid"]: { "subscription": item["subscription"], "name": item["name"], "groups": groups, } }, ) updated += 1 else: if contact.name != item["name"]: await contact.add_to_roster(force=True) updated += 1 # we popped before so this only acts on slidge contacts not in the xmpp roster for contact in contacts.values(): added += 1 await contact.add_to_roster() return f"{added} added, {removed} removed, {updated} updated" class ListContacts(Command): NAME = HELP = "👤 List your legacy contacts" CHAT_COMMAND = "contacts" NODE = CONTACTS.node + "/" + CHAT_COMMAND ACCESS = CommandAccess.USER_LOGGED CATEGORY = CONTACTS async def run( self, session: Optional[AnyBaseSession], _ifrom: JID, *_ ) -> TableResult: assert session is not None contacts = sorted( session.contacts, key=lambda c: c.name.casefold() if c.name else "" ) return TableResult( description="Your buddies", fields=[FormField("name"), FormField("jid", type="jid-single")], items=[{"name": c.name, "jid": c.jid.bare} for c in contacts], ) class ListGroups(Command): NAME = HELP = "👥 List your legacy groups" CHAT_COMMAND = "groups" NODE = GROUPS.node + "/" + CHAT_COMMAND ACCESS = CommandAccess.USER_LOGGED CATEGORY = GROUPS async def run(self, session, _ifrom, *_): assert session is not None await session.bookmarks.fill() groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold()) return TableResult( description="Your groups", fields=[FormField("name"), FormField("jid", type="jid-single")], items=[{"name": g.name, "jid": g.jid.bare} for g in groups], jids_are_mucs=True, ) class Login(Command): NAME = "🔐 Re-login to the legacy network" HELP = "Login to the legacy service" CHAT_COMMAND = "re-login" NODE = "https://slidge.im/command/core/" + CHAT_COMMAND ACCESS = CommandAccess.USER_NON_LOGGED async def run(self, session: Optional[AnyBaseSession], _ifrom, *_): assert session is not None if session.is_logging_in: raise XMPPError("bad-request", "You are already logging in.") session.is_logging_in = True try: msg = await session.login() except Exception as e: session.send_gateway_status(f"Re-login failed: {e}", show="dnd") raise XMPPError( "internal-server-error", etype="wait", text=f"Could not login: {e}" ) finally: session.is_logging_in = False session.logged = True session.send_gateway_status(msg or "Re-connected", show="chat") session.send_gateway_message(msg or "Re-connected") return msg class CreateGroup(Command): NAME = "🆕 New legacy group" HELP = "Create a group on the legacy service" CHAT_COMMAND = "create-group" NODE = GROUPS.node + "/" + CHAT_COMMAND CATEGORY = GROUPS ACCESS = CommandAccess.USER_LOGGED async def run(self, session: Optional[AnyBaseSession], _ifrom, *_): assert session is not None contacts = session.contacts.known_contacts(only_friends=True) return Form( title="Create a new group", instructions="Pick contacts that should be part of this new group", fields=[ FormField(var="group_name", label="Name of the group", required=True), FormField( var="contacts", label="Contacts to add to the new group", type="list-multi", options=[ {"value": str(contact.jid), "label": contact.name} for contact in sorted(contacts.values(), key=lambda c: c.name) ], required=False, ), ], handler=self.finish, ) @staticmethod async def finish(form_values: FormValues, session: Optional[AnyBaseSession], *_): assert session is not None legacy_id: LegacyGroupIdType = await session.on_create_group( # type:ignore cast(str, form_values["group_name"]), [ await session.contacts.by_jid(JID(j)) for j in form_values.get("contacts", []) # type:ignore ], ) muc = await session.bookmarks.by_legacy_id(legacy_id) return TableResult( description=f"Your new group: xmpp:{muc.jid}?join", fields=[FormField("name"), FormField("jid", type="jid-single")], items=[{"name": muc.name, "jid": muc.jid}], jids_are_mucs=True, ) class Preferences(Command): NAME = "⚙️ Preferences" HELP = "Customize the gateway behaviour to your liking" CHAT_COMMAND = "preferences" NODE = "https://slidge.im/command/core/preferences" ACCESS = CommandAccess.USER async def run( self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any ) -> Form: fields = deepcopy(self.xmpp.PREFERENCES) assert session is not None current = session.user.preferences for field in fields: field.value = current.get(field.var) # type:ignore return Form( title="Preferences", instructions=self.HELP, fields=fields, handler=self.finish, # type:ignore ) async def finish( self, form_values: UserPreferences, session: Optional[AnyBaseSession], *_ ) -> str: assert session is not None user = session.user user.preferences.update(form_values) # type:ignore self.xmpp.store.users.update(user) if form_values["sync_avatar"]: await self.xmpp.fetch_user_avatar(session) else: session.xmpp.store.users.set_avatar_hash(session.user_pk, None) return "Your preferences have been updated." class Unregister(Command): NAME = "❌ Unregister from the gateway" HELP = "Unregister from the gateway" CHAT_COMMAND = "unregister" NODE = "https://slidge.im/command/core/unregister" ACCESS = CommandAccess.USER async def run( self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any ) -> Confirmation: return Confirmation( prompt=f"Are you sure you want to unregister from '{self.xmpp.boundjid}'?", success=f"You are not registered to '{self.xmpp.boundjid}' anymore.", handler=self.unregister, ) async def unregister(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str: assert session is not None user = self.xmpp.store.users.get(session.user_jid) assert user is not None await self.xmpp.unregister_user(user) return "You are not registered anymore. Bye!" class LeaveGroup(Command): NAME = HELP = "❌ Leave a legacy group" CHAT_COMMAND = "leave-group" NODE = GROUPS.node + "/" + CHAT_COMMAND ACCESS = CommandAccess.USER_LOGGED CATEGORY = GROUPS async def run(self, session, _ifrom, *_): assert session is not None await session.bookmarks.fill() groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold()) return Form( title="Leave a group", instructions="Select the group you want to leave", fields=[ FormField( "group", "Group name", type="list-single", options=[ {"label": g.name, "value": str(i)} for i, g in enumerate(groups) ], ) ], handler=self.confirm, # type:ignore handler_args=(groups,), ) async def confirm( self, form_values: FormValues, _session: AnyBaseSession, _ifrom, groups: list[LegacyMUC], ): group = groups[int(form_values["group"])] # type:ignore return Confirmation( prompt=f"Are you sure you want to leave the group '{group.name}'?", handler=self.finish, # type:ignore handler_args=(group,), ) @staticmethod async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC): await session.on_leave_group(group.legacy_id) await session.bookmarks.remove(group, reason="You left this group via slidge.") slidge/slidge/contact/000077500000000000000000000000001477703150600152415ustar00rootroot00000000000000slidge/slidge/contact/__init__.py000066400000000000000000000002771477703150600173600ustar00rootroot00000000000000""" Everything related to 1 on 1 chats, and other legacy users' details. """ from .contact import LegacyContact from .roster import LegacyRoster __all__ = ("LegacyContact", "LegacyRoster") slidge/slidge/contact/contact.py000066400000000000000000000546211477703150600172560ustar00rootroot00000000000000import datetime import logging import warnings from datetime import date from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union from xml.etree import ElementTree as ET from slixmpp import JID, Message, Presence from slixmpp.exceptions import IqError, IqTimeout from slixmpp.plugins.xep_0292.stanza import VCard4 from slixmpp.types import MessageTypes from ..core import config from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin from ..core.mixins.db import UpdateInfoMixin from ..core.mixins.disco import ContactAccountDiscoMixin from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin from ..db.models import Contact from ..util import SubclassableOnce from ..util.types import ClientType, LegacyUserIdType, MessageOrPresenceTypeVar if TYPE_CHECKING: from ..core.session import BaseSession from ..group.participant import LegacyParticipant class LegacyContact( Generic[LegacyUserIdType], StoredAttributeMixin, AvatarMixin, ContactAccountDiscoMixin, FullCarbonMixin, ReactionRecipientMixin, ThreadRecipientMixin, UpdateInfoMixin, metaclass=SubclassableOnce, ): """ This class centralizes actions in relation to a specific legacy contact. You shouldn't create instances of contacts manually, but rather rely on :meth:`.LegacyRoster.by_legacy_id` to ensure that contact instances are singletons. The :class:`.LegacyRoster` instance of a session is accessible through the :attr:`.BaseSession.contacts` attribute. Typically, your plugin should have methods hook to the legacy events and call appropriate methods here to transmit the "legacy action" to the xmpp user. This should look like this: .. code-block:python class Session(BaseSession): ... async def on_cool_chat_network_new_text_message(self, legacy_msg_event): contact = self.contacts.by_legacy_id(legacy_msg_event.from) contact.send_text(legacy_msg_event.text) async def on_cool_chat_network_new_typing_event(self, legacy_typing_event): contact = self.contacts.by_legacy_id(legacy_msg_event.from) contact.composing() ... Use ``carbon=True`` as a keyword arg for methods to represent an action FROM the user TO the contact, typically when the user uses an official client to do an action such as sending a message or marking as message as read. This will use :xep:`0363` to impersonate the XMPP user in order. """ session: "BaseSession" RESOURCE: str = "slidge" """ A full JID, including a resource part is required for chat states (and maybe other stuff) to work properly. This is the name of the resource the contacts will use. """ PROPAGATE_PRESENCE_TO_GROUPS = True mtype: MessageTypes = "chat" _can_send_carbon = True is_group = False _ONLY_SEND_PRESENCE_CHANGES = True STRIP_SHORT_DELAY = True _NON_FRIEND_PRESENCES_FILTER = {"subscribe", "unsubscribed"} _avatar_bare_jid = True INVITATION_RECIPIENT = True def __init__( self, session: "BaseSession", legacy_id: LegacyUserIdType, jid_username: str, ): """ :param session: The session this contact is part of :param legacy_id: The contact's legacy ID :param jid_username: User part of this contact's 'puppet' JID. NB: case-insensitive, and some special characters are not allowed """ super().__init__() self.session = session self.legacy_id: LegacyUserIdType = legacy_id """ The legacy identifier of the :term:`Legacy Contact`. By default, this is the :term:`JID Local Part` of this :term:`XMPP Entity`. Controlling what values are valid and how they are translated from a :term:`JID Local Part` is done in :meth:`.jid_username_to_legacy_id`. Reciprocally, in :meth:`legacy_id_to_jid_username` the inverse transformation is defined. """ self.jid_username = jid_username self._name: Optional[str] = None self.xmpp = session.xmpp self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare) self.jid.resource = self.RESOURCE self.log = logging.getLogger(self.jid.bare) self._set_logger_name() self._is_friend: bool = False self._added_to_roster = False self._caps_ver: str | None = None self._vcard_fetched = False self._vcard: str | None = None self._client_type: ClientType = "pc" async def get_vcard(self, fetch=True) -> VCard4 | None: if fetch and not self._vcard_fetched: await self.fetch_vcard() if self._vcard is None: return None return VCard4(xml=ET.fromstring(self._vcard)) @property def is_friend(self): return self._is_friend @is_friend.setter def is_friend(self, value: bool): if value == self._is_friend: return self._is_friend = value if self._updating_info: return self.__ensure_pk() assert self.contact_pk is not None self.xmpp.store.contacts.set_friend(self.contact_pk, value) @property def added_to_roster(self): return self._added_to_roster @added_to_roster.setter def added_to_roster(self, value: bool): if value == self._added_to_roster: return self._added_to_roster = value if self._updating_info: return if self.contact_pk is None: # during LegacyRoster.fill() return self.xmpp.store.contacts.set_added_to_roster(self.contact_pk, value) @property def participants(self) -> list["LegacyParticipant"]: if self.contact_pk is None: return [] self.__ensure_pk() from ..group.participant import LegacyParticipant return [ LegacyParticipant.get_self_or_unique_subclass().from_store( self.session, stored, contact=self ) for stored in self.xmpp.store.participants.get_for_contact(self.contact_pk) ] @property def user_jid(self): return self.session.user_jid @property # type:ignore def DISCO_TYPE(self) -> ClientType: return self._client_type @DISCO_TYPE.setter def DISCO_TYPE(self, value: ClientType) -> None: self.client_type = value @property def client_type(self) -> ClientType: """ The client type of this contact, cf https://xmpp.org/registrar/disco-categories.html#client Default is "pc". """ return self._client_type @client_type.setter def client_type(self, value: ClientType) -> None: self._client_type = value if self._updating_info: return self.__ensure_pk() assert self.contact_pk is not None self.xmpp.store.contacts.set_client_type(self.contact_pk, value) def _set_logger_name(self): self.log.name = f"{self.user_jid.bare}:contact:{self}" def __repr__(self): return f"" def __ensure_pk(self): if self.contact_pk is not None: return # This happens for legacy modules that don't follow the Roster.fill / # populate contact attributes in Contact.update_info() method. # This results in (even) less optimised SQL writes and read, but # we allow it because it fits some legacy network libs better. with self.xmpp.store.session() as orm: orm.commit() stored = self.xmpp.store.contacts.get_by_legacy_id( self.user_pk, str(self.legacy_id) ) if stored is None: self.contact_pk = self.xmpp.store.contacts.update(self, commit=True) else: self.contact_pk = stored.id assert self.contact_pk is not None def __get_subscription_string(self): if self.is_friend: return "both" return "none" def __propagate_to_participants(self, stanza: Presence): if not self.PROPAGATE_PRESENCE_TO_GROUPS: return ptype = stanza["type"] if ptype in ("available", "chat"): func_name = "online" elif ptype in ("xa", "unavailable"): # we map unavailable to extended_away, because offline is # "participant leaves the MUC" # TODO: improve this with a clear distinction between participant # and member list func_name = "extended_away" elif ptype == "busy": func_name = "busy" elif ptype == "away": func_name = "away" else: return last_seen: Optional[datetime.datetime] = ( stanza["idle"]["since"] if stanza.get_plugin("idle", check=True) else None ) kw = dict(status=stanza["status"], last_seen=last_seen) for part in self.participants: func = getattr(part, func_name) func(**kw) def _send( self, stanza: MessageOrPresenceTypeVar, carbon=False, nick=False, **send_kwargs ) -> MessageOrPresenceTypeVar: if carbon and isinstance(stanza, Message): stanza["to"] = self.jid.bare stanza["from"] = self.user_jid self._privileged_send(stanza) return stanza # type:ignore if isinstance(stanza, Presence): if not self._updating_info: self.__propagate_to_participants(stanza) if ( not self.is_friend and stanza["type"] not in self._NON_FRIEND_PRESENCES_FILTER ): return stanza # type:ignore if self.name and (nick or not self.is_friend): n = self.xmpp.plugin["xep_0172"].stanza.UserNick() n["nick"] = self.name stanza.append(n) if ( not self._updating_info and self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza) ): self.__ensure_pk() assert self.contact_pk is not None self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"]) stanza["to"] = self.user_jid stanza.send() return stanza def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str) -> list[str]: """ Return XMPP msg ids sent by this contact up to a given XMPP msg id. Plugins have no reason to use this, but it is used by slidge core for legacy networks that need to mark all messages as read (most XMPP clients only send a read marker for the latest message). This has side effects, if the horizon XMPP id is found, messages up to this horizon are not cleared, to avoid sending the same read mark twice. :param horizon_xmpp_id: The latest message :return: A list of XMPP ids or None if horizon_xmpp_id was not found """ self.__ensure_pk() assert self.contact_pk is not None return self.xmpp.store.contacts.pop_sent_up_to(self.contact_pk, horizon_xmpp_id) @property def name(self): """ Friendly name of the contact, as it should appear in the user's roster """ return self._name @name.setter def name(self, n: Optional[str]): if self._name == n: return self._name = n self._set_logger_name() if self.is_friend and self.added_to_roster: self.xmpp.pubsub.broadcast_nick( user_jid=self.user_jid, jid=self.jid.bare, nick=n ) if self._updating_info: # means we're in update_info(), so no participants, and no need # to write to DB now, it will be called in Roster.__finish_init_contact return for p in self.participants: p.nickname = n self.__ensure_pk() assert self.contact_pk is not None self.xmpp.store.contacts.update_nick(self.contact_pk, n) def _get_cached_avatar_id(self) -> Optional[str]: if self.contact_pk is None: return None return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk) def _post_avatar_update(self): self.__ensure_pk() assert self.contact_pk is not None self.xmpp.store.contacts.set_avatar( self.contact_pk, self._avatar_pk, None if self.avatar_id is None else str(self.avatar_id), ) for p in self.participants: self.log.debug("Propagating new avatar to %s", p.muc) p.send_last_presence(force=True, no_cache_online=True) def set_vcard( self, /, full_name: Optional[str] = None, given: Optional[str] = None, surname: Optional[str] = None, birthday: Optional[date] = None, phone: Optional[str] = None, phones: Iterable[str] = (), note: Optional[str] = None, url: Optional[str] = None, email: Optional[str] = None, country: Optional[str] = None, locality: Optional[str] = None, ): vcard = VCard4() vcard.add_impp(f"xmpp:{self.jid.bare}") if n := self.name: vcard.add_nickname(n) if full_name: vcard["full_name"] = full_name elif n: vcard["full_name"] = n if given: vcard["given"] = given if surname: vcard["surname"] = surname if birthday: vcard["birthday"] = birthday if note: vcard.add_note(note) if url: vcard.add_url(url) if email: vcard.add_email(email) if phone: vcard.add_tel(phone) for p in phones: vcard.add_tel(p) if country and locality: vcard.add_address(country, locality) elif country: vcard.add_address(country, locality) self._vcard = str(vcard) self._vcard_fetched = True self.session.create_task( self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard) ) if self._updating_info: return assert self.contact_pk is not None self.xmpp.store.contacts.set_vcard(self.contact_pk, self._vcard) def get_roster_item(self): item = { "subscription": self.__get_subscription_string(), "groups": [self.xmpp.ROSTER_GROUP], } if (n := self.name) is not None: item["name"] = n return {self.jid.bare: item} async def add_to_roster(self, force=False): """ Add this contact to the user roster using :xep:`0356` :param force: add even if the contact was already added successfully """ if self.added_to_roster and not force: return if config.NO_ROSTER_PUSH: log.debug("Roster push request by plugin ignored (--no-roster-push)") return try: await self.xmpp["xep_0356"].set_roster( jid=self.user_jid, roster_items=self.get_roster_item() ) except PermissionError: warnings.warn( "Slidge does not have privileges to add contacts to the roster. Refer" " to https://slidge.im/docs/slidge/main/admin/privilege.html for" " more info." ) if config.ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK: self.send_friend_request( f"I'm already your friend on {self.xmpp.COMPONENT_TYPE}, but " "slidge is not allowed to manage your roster." ) return except (IqError, IqTimeout) as e: self.log.warning("Could not add to roster", exc_info=e) else: # we only broadcast pubsub events for contacts added to the roster # so if something was set before, we need to push it now self.added_to_roster = True self.send_last_presence() async def __broadcast_pubsub_items(self): if not self.is_friend: return if not self.added_to_roster: return cached_avatar = self.get_cached_avatar() if cached_avatar is not None: await self.xmpp.pubsub.broadcast_avatar( self.jid.bare, self.session.user_jid, cached_avatar ) nick = self.name if nick is not None: self.xmpp.pubsub.broadcast_nick( self.session.user_jid, self.jid.bare, nick, ) def send_friend_request(self, text: Optional[str] = None): presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True) self._send(presence, nick=True) async def accept_friend_request(self, text: Optional[str] = None): """ Call this to signify that this Contact has accepted to be a friend of the user. :param text: Optional message from the friend to the user """ self.is_friend = True self.added_to_roster = True self.__ensure_pk() self.log.debug("Accepting friend request") presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True) self._send(presence, nick=True) self.send_last_presence() await self.__broadcast_pubsub_items() self.log.debug("Accepted friend request") def reject_friend_request(self, text: Optional[str] = None): """ Call this to signify that this Contact has refused to be a contact of the user (or that they don't want to be friends anymore) :param text: Optional message from the non-friend to the user """ presence = self._make_presence(ptype="unsubscribed", pstatus=text, bare=True) self.offline() self._send(presence, nick=True) self.is_friend = False async def on_friend_request(self, text=""): """ Called when receiving a "subscribe" presence, ie, "I would like to add you to my contacts/friends", from the user to this contact. In XMPP terms: "I would like to receive your presence updates" This is only called if self.is_friend = False. If self.is_friend = True, slidge will automatically "accept the friend request", ie, reply with a "subscribed" presence. When called, a 'friend request event' should be sent to the legacy service, and when the contact responds, you should either call self.accept_subscription() or self.reject_subscription() """ pass async def on_friend_delete(self, text=""): """ Called when receiving an "unsubscribed" presence, ie, "I would like to remove you to my contacts/friends" or "I refuse your friend request" from the user to this contact. In XMPP terms: "You won't receive my presence updates anymore (or you never have)". """ pass async def on_friend_accept(self): """ Called when receiving a "subscribed" presence, ie, "I accept to be your/confirm that you are my friend" from the user to this contact. In XMPP terms: "You will receive my presence updates". """ pass def unsubscribe(self): """ (internal use by slidge) Send an "unsubscribe", "unsubscribed", "unavailable" presence sequence from this contact to the user, ie, "this contact has removed you from their 'friends'". """ for ptype in "unsubscribe", "unsubscribed", "unavailable": self.xmpp.send_presence(pfrom=self.jid, pto=self.user_jid.bare, ptype=ptype) # type: ignore async def update_info(self): """ Fetch information about this contact from the legacy network This is awaited on Contact instantiation, and should be overridden to update the nickname, avatar, vcard [...] of this contact, by making "legacy API calls". To take advantage of the slidge avatar cache, you can check the .avatar property to retrieve the "legacy file ID" of the cached avatar. If there is no change, you should not call :py:meth:`slidge.core.mixins.avatar.AvatarMixin.set_avatar` or attempt to modify the ``.avatar`` property. """ pass async def fetch_vcard(self): """ It the legacy network doesn't like that you fetch too many profiles on startup, it's also possible to fetch it here, which will be called when XMPP clients of the user request the vcard, if it hasn't been fetched before :return: """ pass def _make_presence( self, *, last_seen: Optional[datetime.datetime] = None, status_codes: Optional[set[int]] = None, user_full_jid: Optional[JID] = None, **presence_kwargs, ): p = super()._make_presence(last_seen=last_seen, **presence_kwargs) caps = self.xmpp.plugin["xep_0115"] if p.get_from().resource and self._caps_ver: p["caps"]["node"] = caps.caps_node p["caps"]["hash"] = caps.hash p["caps"]["ver"] = self._caps_ver return p @classmethod def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self: contact = cls( session, cls.xmpp.LEGACY_CONTACT_ID_TYPE(stored.legacy_id), stored.jid.user, # type: ignore *args, # type: ignore **kwargs, # type: ignore ) contact.contact_pk = stored.id contact._name = stored.nick contact._is_friend = stored.is_friend contact._added_to_roster = stored.added_to_roster if (data := stored.extra_attributes) is not None: contact.deserialize_extra_attributes(data) contact._caps_ver = stored.caps_ver contact._set_logger_name() contact._AvatarMixin__avatar_unique_id = ( # type:ignore None if stored.avatar_legacy_id is None else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id) ) contact._avatar_pk = stored.avatar_id contact._vcard = stored.vcard contact._vcard_fetched = stored.vcard_fetched contact._client_type = stored.client_type return contact def is_markable(stanza: Union[Message, Presence]): if isinstance(stanza, Presence): return False return bool(stanza["body"]) log = logging.getLogger(__name__) slidge/slidge/contact/roster.py000066400000000000000000000237351477703150600171430ustar00rootroot00000000000000import asyncio import logging import warnings from typing import TYPE_CHECKING, AsyncIterator, Generic, Iterator, Optional, Type from slixmpp import JID from slixmpp.exceptions import IqError, IqTimeout, XMPPError from ..core.mixins.lock import NamedLockMixin from ..db.models import Contact from ..db.store import ContactStore from ..util import SubclassableOnce from ..util.jid_escaping import ESCAPE_TABLE, unescape_node from ..util.types import LegacyContactType, LegacyUserIdType from .contact import LegacyContact if TYPE_CHECKING: from ..core.session import BaseSession class ContactIsUser(Exception): pass class LegacyRoster( Generic[LegacyUserIdType, LegacyContactType], NamedLockMixin, metaclass=SubclassableOnce, ): """ Virtual roster of a gateway user, that allows to represent all of their contacts as singleton instances (if used properly and not too bugged). Every :class:`.BaseSession` instance will have its own :class:`.LegacyRoster` instance accessible via the :attr:`.BaseSession.contacts` attribute. Typically, you will mostly use the :meth:`.LegacyRoster.by_legacy_id` function to retrieve a contact instance. You might need to override :meth:`.LegacyRoster.legacy_id_to_jid_username` and/or :meth:`.LegacyRoster.jid_username_to_legacy_id` to incorporate some custom logic if you need some characters when translation JID user parts and legacy IDs. """ def __init__(self, session: "BaseSession"): self._contact_cls: Type[LegacyContactType] = ( LegacyContact.get_self_or_unique_subclass() ) self._contact_cls.xmpp = session.xmpp self.__store: ContactStore = session.xmpp.store.contacts self.session = session self.log = logging.getLogger(f"{self.session.user_jid.bare}:roster") self.user_legacy_id: Optional[LegacyUserIdType] = None self.ready: asyncio.Future[bool] = self.session.xmpp.loop.create_future() self.__filling = False super().__init__() def __repr__(self): return f"" def __iter__(self) -> Iterator[LegacyContactType]: with self.__store.session(): for stored in self.__store.get_all(user_pk=self.session.user_pk): yield self._contact_cls.from_store(self.session, stored) def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]: if only_friends: return {c.jid.bare: c for c in self if c.is_friend} return {c.jid.bare: c for c in self} async def by_jid(self, contact_jid: JID) -> LegacyContactType: # """ # Retrieve a contact by their JID # # If the contact was not instantiated before, it will be created # using :meth:`slidge.LegacyRoster.jid_username_to_legacy_id` to infer their # legacy user ID. # # :param contact_jid: # :return: # """ username = contact_jid.node async with self.lock(("username", username)): legacy_id = await self.jid_username_to_legacy_id(username) log.debug("Contact %s not found", contact_jid) if self.get_lock(("legacy_id", legacy_id)): log.debug("Already updating %s", contact_jid) return await self.by_legacy_id(legacy_id) with self.__store.session(): stored = self.__store.get_by_jid(self.session.user_pk, contact_jid) return await self.__update_contact(stored, legacy_id, username) def by_jid_only_if_exists(self, contact_jid: JID) -> LegacyContactType | None: with self.__store.session(): stored = self.__store.get_by_jid(self.session.user_pk, contact_jid) if stored is not None and stored.updated: return self._contact_cls.from_store(self.session, stored) return None async def by_legacy_id( self, legacy_id: LegacyUserIdType, *args, **kwargs ) -> LegacyContactType: """ Retrieve a contact by their legacy_id If the contact was not instantiated before, it will be created using :meth:`slidge.LegacyRoster.legacy_id_to_jid_username` to infer their legacy user ID. :param legacy_id: :param args: arbitrary additional positional arguments passed to the contact constructor. Requires subclassing LegacyContact.__init__ to accept those. This is useful for networks where you fetch the contact list and information about these contacts in a single request :param kwargs: arbitrary keyword arguments passed to the contact constructor :return: """ if legacy_id == self.user_legacy_id: raise ContactIsUser async with self.lock(("legacy_id", legacy_id)): username = await self.legacy_id_to_jid_username(legacy_id) if self.get_lock(("username", username)): log.debug("Already updating %s", username) jid = JID() jid.node = username jid.domain = self.session.xmpp.boundjid.bare return await self.by_jid(jid) with self.__store.session(): stored = self.__store.get_by_legacy_id( self.session.user_pk, str(legacy_id) ) return await self.__update_contact( stored, legacy_id, username, *args, **kwargs ) async def __update_contact( self, stored: Contact | None, legacy_id: LegacyUserIdType, username: str, *a, **kw, ) -> LegacyContactType: if stored is None: contact = self._contact_cls(self.session, legacy_id, username, *a, **kw) else: contact = self._contact_cls.from_store(self.session, stored, *a, **kw) if stored.updated: return contact try: with contact.updating_info(): await contact.avatar_wrap_update_info() except XMPPError: raise except Exception as e: raise XMPPError("internal-server-error", str(e)) contact._caps_ver = await contact.get_caps_ver(contact.jid) contact.contact_pk = self.__store.update(contact, commit=not self.__filling) return contact async def by_stanza(self, s) -> LegacyContact: # """ # Retrieve a contact by the destination of a stanza # # See :meth:`slidge.Roster.by_legacy_id` for more info. # # :param s: # :return: # """ return await self.by_jid(s.get_to()) async def legacy_id_to_jid_username(self, legacy_id: LegacyUserIdType) -> str: """ Convert a legacy ID to a valid 'user' part of a JID Should be overridden for cases where the str conversion of the legacy_id is not enough, e.g., if it is case-sensitive or contains forbidden characters not covered by :xep:`0106`. :param legacy_id: """ return str(legacy_id).translate(ESCAPE_TABLE) async def jid_username_to_legacy_id(self, jid_username: str) -> LegacyUserIdType: """ Convert a JID user part to a legacy ID. Should be overridden in case legacy IDs are not strings, or more generally for any case where the username part of a JID (unescaped with to the mapping defined by :xep:`0106`) is not enough to identify a contact on the legacy network. Default implementation is an identity operation :param jid_username: User part of a JID, ie "user" in "user@example.com" :return: An identifier for the user on the legacy network. """ return unescape_node(jid_username) async def _fill(self): try: if hasattr(self.session.xmpp, "TEST_MODE"): # dirty hack to avoid mocking xmpp server replies to this # during tests raise PermissionError iq = await self.session.xmpp["xep_0356"].get_roster( self.session.user_jid.bare ) user_roster = iq["roster"]["items"] except (PermissionError, IqError, IqTimeout): user_roster = None with self.__store.session() as orm: self.__filling = True async for contact in self.fill(): if user_roster is None: continue item = contact.get_roster_item() old = user_roster.get(contact.jid.bare) if old is not None and all( old[k] == item[contact.jid.bare].get(k) for k in ("subscription", "groups", "name") ): self.log.debug("No need to update roster") continue self.log.debug("Updating roster") try: await self.session.xmpp["xep_0356"].set_roster( self.session.user_jid.bare, item, ) except (PermissionError, IqError, IqTimeout) as e: warnings.warn(f"Could not add to roster: {e}") else: contact._added_to_roster = True orm.commit() self.__filling = False async def fill(self) -> AsyncIterator[LegacyContact]: """ Populate slidge's "virtual roster". This should yield contacts that are meant to be added to the user's roster, typically by using ``await self.by_legacy_id(contact_id)``. Setting the contact nicknames, avatar, etc. should be in :meth:`LegacyContact.update_info()` It's not mandatory to override this method, but it is recommended way to populate "friends" of the user. Calling ``await (await self.by_legacy_id(contact_id)).add_to_roster()`` accomplishes the same thing, but doing it in here allows to batch DB queries and is better performance-wise. """ return yield log = logging.getLogger(__name__) slidge/slidge/core/000077500000000000000000000000001477703150600145365ustar00rootroot00000000000000slidge/slidge/core/__init__.py000066400000000000000000000001041477703150600166420ustar00rootroot00000000000000from .pubsub import PubSubComponent __all__ = ("PubSubComponent",) slidge/slidge/core/config.py000066400000000000000000000174341477703150600163660ustar00rootroot00000000000000from datetime import timedelta from pathlib import Path from typing import Optional from slixmpp import JID as JIDType class _TimedeltaSeconds(timedelta): def __new__(cls, s: str): return super().__new__(cls, seconds=int(s)) # REQUIRED, so not default value LEGACY_MODULE: str LEGACY_MODULE__DOC = ( "Importable python module containing (at least) " "a BaseGateway and a LegacySession subclass" ) SERVER: str = "localhost" SERVER__DOC = ( "The XMPP server's host name. Defaults to localhost, which is the " "standard way of running slidge, on the same host as the XMPP server. " "The 'Jabber Component Protocol' (XEP-0114) does not mention encryption, " "so you *should* provide encryption another way, eg via port forwarding, if " "you change this." ) SERVER__SHORT = "s" SECRET: str SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)" JID: JIDType JID__DOC = "The gateway component's JID" JID__SHORT = "j" PORT: str = "5347" PORT__DOC = "The XMPP server's port for incoming component connections" PORT__SHORT = "p" # Dynamic default (depends on other values) HOME_DIR: Path HOME_DIR__DOC = ( "Directory where slidge will writes it persistent data and cache. " "Defaults to /var/lib/slidge/${SLIDGE_JID}. " ) HOME_DIR__DYNAMIC_DEFAULT = True DB_URL: str DB_URL__DOC = ( "Database URL, see . " "Defaults to sqlite:///${HOME_DIR}/slidge.sqlite" ) DB_URL__DYNAMIC_DEFAULT = True USER_JID_VALIDATOR: str USER_JID_VALIDATOR__DOC = ( "Regular expression to restrict users that can register to the gateway, by JID. " "Defaults to .*@${SLIDGE_SERVER}, but since SLIDGE_SERVER is usually localhost, " "you probably want to change that to .*@example.com" ) USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True # Optional, so default value + type hint if default is None ADMINS: tuple[JIDType, ...] = () ADMINS__DOC = "JIDs of the gateway admins" UPLOAD_SERVICE: Optional[str] = None UPLOAD_SERVICE__DOC = ( "JID of an HTTP upload service the gateway can use. " "This is optional, as it should be automatically determined via service" "discovery." ) NO_ROSTER_PUSH = False NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically" ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK = True ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK__DOC = ( "If True, legacy contacts will send a presence request subscription " "when privileged roster push does not work, eg, if XEP-0356 (privileged " "entity) is not available for the component." ) AVATAR_SIZE = 200 AVATAR_SIZE__DOC = ( "Maximum image size (width and height), image ratio will be preserved" ) USE_ATTACHMENT_ORIGINAL_URLS = False USE_ATTACHMENT_ORIGINAL_URLS__DOC = ( "For legacy plugins in which attachments are publicly downloadable URLs, " "let XMPP clients directly download them from this URL. Note that this will " "probably leak your client IP to the legacy network." ) UPLOAD_REQUESTER: Optional[str] = None UPLOAD_REQUESTER__DOC = ( "Set which JID should request the upload slots. Defaults to the component JID." ) NO_UPLOAD_PATH: Optional[str] = None NO_UPLOAD_PATH__DOC = ( "Instead of using the XMPP server's HTTP upload component, copy files to this dir. " "You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure " "an web server to serve files in this dir." ) NO_UPLOAD_URL_PREFIX: Optional[str] = None NO_UPLOAD_URL_PREFIX__DOC = ( "Base URL that servers files in the dir set in the no-upload-path option, " "eg https://example.com:666/slidge-attachments/" ) NO_UPLOAD_METHOD: str = "copy" NO_UPLOAD_METHOD__DOC = ( "Whether to 'copy', 'move', 'hardlink' or 'symlink' the files in no-upload-path." ) NO_UPLOAD_FILE_READ_OTHERS = False NO_UPLOAD_FILE_READ_OTHERS__DOC = ( "After writing a file in NO_UPLOAD_PATH, change its permission so that 'others' can" " read it." ) IGNORE_DELAY_THRESHOLD = _TimedeltaSeconds("300") IGNORE_DELAY_THRESHOLD__DOC = ( "Threshold, in seconds, below which the information is stripped " "out of emitted stanzas." ) PARTIAL_REGISTRATION_TIMEOUT = 3600 PARTIAL_REGISTRATION_TIMEOUT__DOC = ( "Timeout before registration and login. Only useful for legacy networks where " "a single step registration process is not enough." ) LAST_SEEN_FALLBACK = False LAST_SEEN_FALLBACK__DOC = ( "When using XEP-0319 (Last User Interaction in Presence), use the presence status" " to display the last seen information in the presence status. Useful for clients" " that do not implement XEP-0319. Because of implementation details, this can increase" " RAM usage and might be deprecated in the future. Ask your client dev for XEP-0319" " support ;o)." ) QR_TIMEOUT = 60 QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation." LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND = False LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND__DOC = ( "If the legacy service does not support last message correction but supports" " message retractions, slidge can 'retract' the edited message when you edit from" " an XMPP client, as a workaround. This may only work for editing messages" " **once**. If the legacy service does not support retractions and this is set to" " true, when XMPP clients attempt to correct, this will send a new message." ) FIX_FILENAME_SUFFIX_MIME_TYPE = False FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = ( "Fix the Filename suffix based on the Mime Type of the file. Some clients (eg" " Conversations) may not inline files that have a wrong suffix for the MIME Type." " Therefore the MIME Type of the file is checked, if the suffix is not valid for" " that MIME Type, a valid one will be picked." ) LOG_FILE: Optional[Path] = None LOG_FILE__DOC = "Log to a file instead of stdout/err" LOG_FORMAT: str = "%(levelname)s:%(name)s:%(message)s" LOG_FORMAT__DOC = ( "Optionally, a format string for logging messages. Refer to " "https://docs.python.org/3/library/logging.html#logrecord-attributes " "for available options." ) MAM_MAX_DAYS = 7 MAM_MAX_DAYS__DOC = "Maximum number of days for group archive retention." CORRECTION_EMPTY_BODY_AS_RETRACTION = True CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = ( "Treat last message correction to empty message as a retraction. " "(this is what cheogram does for retraction)" ) ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200 ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = ( "Some legacy network provide ridiculously long filenames, strip above this limit, " "preserving suffix." ) ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS = True ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS__DOC = ( "Send an invitation to join MUCs when adding them to the bookmarks. While this " "should not be necessary, it helps with clients that do not support :xep:`0402` " "or that do not respect the auto-join flag." ) AVATAR_RESAMPLING_THREADS = 2 AVATAR_RESAMPLING_THREADS__DOC = ( "Number of additional threads to use for avatar resampling. Even in a single-core " "context, this makes avatar resampling non-blocking." ) DEV_MODE = False DEV_MODE__DOC = ( "Enables an interactive python shell via chat commands, for admins." "Not safe to use in prod, but great during dev." ) STRIP_LEADING_EMOJI_ADHOC = False STRIP_LEADING_EMOJI_ADHOC__DOC = ( "Strip the leading emoji in ad-hoc command names, if present, in case you " "are a emoji-hater." ) COMPONENT_NAME: Optional[str] = None COMPONENT_NAME__DOC = ( "Overrides the default component name with a custom one. This is seen in service discovery and as the nickname " "of the component in chat windows." ) WELCOME_MESSAGE: Optional[str] = None WELCOME_MESSAGE__DOC = ( "Overrides the default welcome message received by newly registered users." ) slidge/slidge/core/dispatcher/000077500000000000000000000000001477703150600166645ustar00rootroot00000000000000slidge/slidge/core/dispatcher/__init__.py000066400000000000000000000001241477703150600207720ustar00rootroot00000000000000from .session_dispatcher import SessionDispatcher __all__ = ("SessionDispatcher",) slidge/slidge/core/dispatcher/caps.py000066400000000000000000000037541477703150600201750ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING from slixmpp import Presence from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import StanzaBase from .util import DispatcherMixin if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class CapsMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) xmpp.del_filter("out", xmpp.plugin["xep_0115"]._filter_add_caps) xmpp.add_filter("out", self._filter_add_caps) # type:ignore async def _filter_add_caps(self, stanza: StanzaBase) -> StanzaBase: # we rolled our own "add caps on presences" filter because # there is too much magic happening in slixmpp # anyway, we probably want to roll our own "dynamic disco"/caps # module in the long run, so it's a step in this direction if not isinstance(stanza, Presence): return stanza if stanza.get_plugin("caps", check=True): return stanza if stanza["type"] not in ("available", "chat", "away", "dnd", "xa"): return stanza pfrom = stanza.get_from() caps = self.xmpp.plugin["xep_0115"] if pfrom != self.xmpp.boundjid.bare: try: session = self.xmpp.get_session_from_jid(stanza.get_to()) except XMPPError: log.debug("not adding caps 1") return stanza if session is None: return stanza await session.ready try: contact = await session.contacts.by_jid(pfrom) except XMPPError: return stanza ver = await contact.get_caps_ver(pfrom) else: ver = await caps.get_verstring(pfrom) log.debug("Ver: %s", ver) if ver: stanza["caps"]["node"] = caps.caps_node stanza["caps"]["hash"] = caps.hash stanza["caps"]["ver"] = ver return stanza log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/disco.py000066400000000000000000000037261477703150600203470ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING, Any, Optional from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0030.stanza.items import DiscoItems from slixmpp.types import OptJid from .util import DispatcherMixin if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class DiscoMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) xmpp.plugin["xep_0030"].set_node_handler( "get_info", jid=None, node=None, handler=self.get_info, ) xmpp.plugin["xep_0030"].set_node_handler( "get_items", jid=None, node=None, handler=self.get_items, ) async def get_info( self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any ): if ifrom == self.xmpp.boundjid.bare or jid in (self.xmpp.boundjid.bare, None): return self.xmpp.plugin["xep_0030"].static.get_info(jid, node, ifrom, data) if ifrom is None: raise XMPPError("subscription-required") assert jid is not None session = await self._get_session_from_jid(jid=ifrom) log.debug("Looking for entity: %s", jid) entity = await session.get_contact_or_group_or_participant(jid) if entity is None: raise XMPPError("item-not-found") return await entity.get_disco_info(jid, node) async def get_items( self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any ): if ifrom is None: raise XMPPError("bad-request") if jid != self.xmpp.boundjid.bare: return DiscoItems() assert ifrom is not None session = await self._get_session_from_jid(ifrom) d = DiscoItems() for room in self.xmpp.store.rooms.get_all_jid_and_names(session.user_pk): d.add_item(room.jid, name=room.name) return d log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/message/000077500000000000000000000000001477703150600203105ustar00rootroot00000000000000slidge/slidge/core/dispatcher/message/__init__.py000066400000000000000000000003371477703150600224240ustar00rootroot00000000000000from .chat_state import ChatStateMixin from .marker import MarkerMixin from .message import MessageContentMixin class MessageMixin(ChatStateMixin, MarkerMixin, MessageContentMixin): pass __all__ = ("MessageMixin",) slidge/slidge/core/dispatcher/message/chat_state.py000066400000000000000000000033071477703150600230040ustar00rootroot00000000000000from slixmpp import Message from slixmpp.xmlstream import StanzaBase from ..util import DispatcherMixin, exceptions_to_xmpp_errors class ChatStateMixin(DispatcherMixin): def __init__(self, xmpp) -> None: super().__init__(xmpp) xmpp.add_event_handler("chatstate_active", self.on_chatstate_active) xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) xmpp.add_event_handler("chatstate_composing", self.on_chatstate_composing) xmpp.add_event_handler("chatstate_paused", self.on_chatstate_paused) @exceptions_to_xmpp_errors async def on_chatstate_active(self, msg: StanzaBase) -> None: assert isinstance(msg, Message) if msg["body"]: # if there is a body, it's handled in on_legacy_message() return session, entity, thread = await self._get_session_entity_thread(msg) await session.on_active(entity, thread) @exceptions_to_xmpp_errors async def on_chatstate_inactive(self, msg: StanzaBase) -> None: assert isinstance(msg, Message) session, entity, thread = await self._get_session_entity_thread(msg) await session.on_inactive(entity, thread) @exceptions_to_xmpp_errors async def on_chatstate_composing(self, msg: StanzaBase) -> None: assert isinstance(msg, Message) session, entity, thread = await self._get_session_entity_thread(msg) await session.on_composing(entity, thread) @exceptions_to_xmpp_errors async def on_chatstate_paused(self, msg: StanzaBase) -> None: assert isinstance(msg, Message) session, entity, thread = await self._get_session_entity_thread(msg) await session.on_paused(entity, thread) slidge/slidge/core/dispatcher/message/marker.py000066400000000000000000000045531477703150600221520ustar00rootroot00000000000000from slixmpp import JID, Message from slixmpp.xmlstream import StanzaBase from ....group.room import LegacyMUC from ....util.types import Recipient from ..util import DispatcherMixin, _get_entity, exceptions_to_xmpp_errors class MarkerMixin(DispatcherMixin): def __init__(self, xmpp) -> None: super().__init__(xmpp) xmpp.add_event_handler("marker_displayed", self.on_marker_displayed) xmpp.add_event_handler( "message_displayed_synchronization_publish", self.on_message_displayed_synchronization_publish, ) @exceptions_to_xmpp_errors async def on_marker_displayed(self, msg: StanzaBase) -> None: assert isinstance(msg, Message) session = await self._get_session(msg) e: Recipient = await _get_entity(session, msg) legacy_thread = await self._xmpp_to_legacy_thread(session, msg, e) displayed_msg_id = msg["displayed"]["id"] if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES: to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore if to_mark is None: session.log.debug("Can't mark all messages up to %s", displayed_msg_id) to_mark = [displayed_msg_id] else: to_mark = [displayed_msg_id] for xmpp_id in to_mark: await session.on_displayed( e, self._xmpp_msg_id_to_legacy(session, xmpp_id), legacy_thread ) if isinstance(e, LegacyMUC): await e.echo(msg, None) @exceptions_to_xmpp_errors async def on_message_displayed_synchronization_publish( self, msg: StanzaBase ) -> None: assert isinstance(msg, Message) chat_jid = JID(msg["pubsub_event"]["items"]["item"]["id"]) if chat_jid.server != self.xmpp.boundjid.bare: return session = await self._get_session(msg, timeout=None) if chat_jid == self.xmpp.boundjid.bare: return chat = await session.get_contact_or_group_or_participant(chat_jid) if not isinstance(chat, LegacyMUC): session.log.debug("Ignoring non-groupchat MDS event") return stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"] await session.on_displayed( chat, self._xmpp_msg_id_to_legacy(session, stanza_id) ) slidge/slidge/core/dispatcher/message/message.py000066400000000000000000000360001477703150600223050ustar00rootroot00000000000000import logging from copy import copy from xml.etree import ElementTree from slixmpp import JID, Message from slixmpp.exceptions import XMPPError from ....contact.contact import LegacyContact from ....group.participant import LegacyParticipant from ....group.room import LegacyMUC from ....util.types import LinkPreview, Recipient from ....util.util import dict_to_named_tuple, remove_emoji_variation_selector_16 from ... import config from ...session import BaseSession from ..util import DispatcherMixin, exceptions_to_xmpp_errors class MessageContentMixin(DispatcherMixin): def __init__(self, xmpp): super().__init__(xmpp) xmpp.add_event_handler("legacy_message", self.on_legacy_message) xmpp.add_event_handler("message_correction", self.on_message_correction) xmpp.add_event_handler("message_retract", self.on_message_retract) xmpp.add_event_handler("groupchat_message", self.on_groupchat_message) xmpp.add_event_handler("reactions", self.on_reactions) async def on_groupchat_message(self, msg: Message) -> None: await self.on_legacy_message(msg) @exceptions_to_xmpp_errors async def on_legacy_message(self, msg: Message): """ Meant to be called from :class:`BaseGateway` only. :param msg: :return: """ # we MUST not use `if m["replace"]["id"]` because it adds the tag if not # present. this is a problem for MUC echoed messages if msg.get_plugin("replace", check=True) is not None: # ignore last message correction (handled by a specific method) return if msg.get_plugin("apply_to", check=True) is not None: # ignore message retraction (handled by a specific method) return if msg.get_plugin("reactions", check=True) is not None: # ignore message reaction fallback. # the reaction itself is handled by self.react_from_msg(). return if msg.get_plugin("retract", check=True) is not None: # ignore message retraction fallback. # the retraction itself is handled by self.on_retract return cid = None if msg.get_plugin("html", check=True) is not None: body = ElementTree.fromstring("" + msg["html"].get_body() + "") p = body.findall("p") if p is not None and len(p) == 1: if p[0].text is None or not p[0].text.strip(): images = p[0].findall("img") if len(images) == 1: # no text, single img ⇒ this is a sticker # other cases should be interpreted as "custom emojis" in text src = images[0].get("src") if src is not None and src.startswith("cid:"): cid = src.removeprefix("cid:") session, entity, thread = await self._get_session_entity_thread(msg) if msg.get_plugin("oob", check=True) is not None: url = msg["oob"]["url"] else: url = None if msg.get_plugin("reply", check=True): text, reply_to_msg_id, reply_to, reply_fallback = await self.__get_reply( msg, session, entity ) else: text = msg["body"] reply_to_msg_id = None reply_to = None reply_fallback = None if msg.get_plugin("link_previews", check=True): link_previews = [ dict_to_named_tuple(p, LinkPreview) for p in msg["link_previews"] ] else: link_previews = [] if url: legacy_msg_id = await self.__send_url( url, session, entity, reply_to_msg_id=reply_to_msg_id, reply_to_fallback_text=reply_fallback, reply_to=reply_to, thread=thread, ) elif cid: legacy_msg_id = await self.__send_bob( msg.get_from(), cid, session, entity, reply_to_msg_id=reply_to_msg_id, reply_to_fallback_text=reply_fallback, reply_to=reply_to, thread=thread, ) elif text: if isinstance(entity, LegacyMUC): mentions = {"mentions": await entity.parse_mentions(text)} else: mentions = {} legacy_msg_id = await session.on_text( entity, text, reply_to_msg_id=reply_to_msg_id, reply_to_fallback_text=reply_fallback, reply_to=reply_to, thread=thread, link_previews=link_previews, **mentions, ) else: log.debug("Ignoring %s", msg.get_id()) return if isinstance(entity, LegacyMUC): await entity.echo(msg, legacy_msg_id) if legacy_msg_id is not None: self.xmpp.store.sent.set_group_message( session.user_pk, str(legacy_msg_id), msg.get_id() ) else: self.__ack(msg) if legacy_msg_id is not None: self.xmpp.store.sent.set_message( session.user_pk, str(legacy_msg_id), msg.get_id() ) if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]): self.xmpp.store.sent.set_thread( session.user_pk, t, str(legacy_msg_id) ) @exceptions_to_xmpp_errors async def on_message_correction(self, msg: Message): if msg.get_plugin("retract", check=True) is not None: # ignore message retraction fallback (fallback=last msg correction) return session, entity, thread = await self._get_session_entity_thread(msg) xmpp_id = msg["replace"]["id"] if isinstance(entity, LegacyMUC): legacy_id_str = self.xmpp.store.sent.get_group_legacy_id( session.user_pk, xmpp_id ) if legacy_id_str is None: legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id) else: legacy_id = self.xmpp.LEGACY_MSG_ID_TYPE(legacy_id_str) else: legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id) if isinstance(entity, LegacyMUC): mentions = await entity.parse_mentions(msg["body"]) else: mentions = None if previews := msg["link_previews"]: link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews] else: link_previews = [] if legacy_id is None: log.debug("Did not find legacy ID to correct") new_legacy_msg_id = await session.on_text( entity, "Correction:" + msg["body"], thread=thread, mentions=mentions, link_previews=link_previews, ) elif ( not msg["body"].strip() and config.CORRECTION_EMPTY_BODY_AS_RETRACTION and entity.RETRACTION ): await session.on_retract(entity, legacy_id, thread=thread) new_legacy_msg_id = None elif entity.CORRECTION: new_legacy_msg_id = await session.on_correct( entity, msg["body"], legacy_id, thread=thread, mentions=mentions, link_previews=link_previews, ) else: session.send_gateway_message( "Last message correction is not supported by this legacy service. " "Slidge will send your correction as new message." ) if ( config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND and entity.RETRACTION and legacy_id is not None ): if legacy_id is not None: session.send_gateway_message( "Slidge will attempt to retract the original message you wanted" " to edit." ) await session.on_retract(entity, legacy_id, thread=thread) new_legacy_msg_id = await session.on_text( entity, "Correction: " + msg["body"], thread=thread, mentions=mentions, link_previews=link_previews, ) if isinstance(entity, LegacyMUC): if new_legacy_msg_id is not None: self.xmpp.store.sent.set_group_message( session.user_pk, new_legacy_msg_id, msg.get_id() ) await entity.echo(msg, new_legacy_msg_id) else: self.__ack(msg) if new_legacy_msg_id is not None: self.xmpp.store.sent.set_message( session.user_pk, new_legacy_msg_id, msg.get_id() ) @exceptions_to_xmpp_errors async def on_message_retract(self, msg: Message): session, entity, thread = await self._get_session_entity_thread(msg) if not entity.RETRACTION: raise XMPPError( "bad-request", "This legacy service does not support message retraction.", ) xmpp_id: str = msg["retract"]["id"] legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id) if legacy_id: await session.on_retract(entity, legacy_id, thread=thread) if isinstance(entity, LegacyMUC): await entity.echo(msg, None) else: log.debug("Ignored retraction from user") self.__ack(msg) @exceptions_to_xmpp_errors async def on_reactions(self, msg: Message): session, entity, thread = await self._get_session_entity_thread(msg) react_to: str = msg["reactions"]["id"] special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith( session.SPECIAL_MSG_ID_PREFIX ) if special_msg: legacy_id = react_to else: legacy_id = self._xmpp_msg_id_to_legacy(session, react_to) if not legacy_id: log.debug("Ignored reaction from user") raise XMPPError( "internal-server-error", "Could not convert the XMPP msg ID to a legacy ID", ) emojis = [ remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"] ] error_msg = None entity = entity if not special_msg: if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1: error_msg = "Maximum 1 emoji/message" if not error_msg and (subset := await entity.available_emojis(legacy_id)): if not set(emojis).issubset(subset): error_msg = f"You can only react with the following emojis: {''.join(subset)}" if error_msg: session.send_gateway_message(error_msg) if not isinstance(entity, LegacyMUC): # no need to carbon for groups, we just don't echo the stanza entity.react(legacy_id, carbon=True) # type: ignore await session.on_react(entity, legacy_id, [], thread=thread) raise XMPPError( "policy-violation", # type:ignore text=error_msg, ) await session.on_react(entity, legacy_id, emojis, thread=thread) if isinstance(entity, LegacyMUC): await entity.echo(msg, None) else: self.__ack(msg) multi = self.xmpp.store.multi.get_xmpp_ids(session.user_pk, react_to) if not multi: return multi = [m for m in multi if react_to != m] if isinstance(entity, LegacyMUC): for xmpp_id in multi: mc = copy(msg) mc["reactions"]["id"] = xmpp_id await entity.echo(mc) elif isinstance(entity, LegacyContact): for xmpp_id in multi: entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True) def __ack(self, msg: Message): if not self.xmpp.PROPER_RECEIPTS: self.xmpp.delivery_receipt.ack(msg) async def __get_reply( self, msg: Message, session: BaseSession, entity: Recipient ) -> tuple[ str, str | int | None, LegacyContact | LegacyParticipant | None, str | None ]: try: reply_to_msg_id = self._xmpp_msg_id_to_legacy(session, msg["reply"]["id"]) except XMPPError: session.log.debug( "Could not determine reply-to legacy msg ID, sending quote instead." ) return msg["body"], None, None, None reply_to_jid = JID(msg["reply"]["to"]) reply_to = None if msg["type"] == "chat": if reply_to_jid.bare != session.user_jid.bare: try: reply_to = await session.contacts.by_jid(reply_to_jid) except XMPPError: pass elif msg["type"] == "groupchat": nick = reply_to_jid.resource try: muc = await session.bookmarks.by_jid(reply_to_jid) except XMPPError: pass else: if nick != muc.user_nick: reply_to = await muc.get_participant( reply_to_jid.resource, store=False ) if msg.get_plugin("fallback", check=True) and ( isinstance(entity, LegacyMUC) or entity.REPLIES ): text = msg["fallback"].get_stripped_body(self.xmpp["xep_0461"].namespace) try: reply_fallback = msg["reply"].get_fallback_body() except AttributeError: reply_fallback = None else: text = msg["body"] reply_fallback = None return text, reply_to_msg_id, reply_to, reply_fallback async def __send_url( self, url: str, session: BaseSession, entity: Recipient, **kwargs ) -> int | str | None: async with self.xmpp.http.get(url) as response: if response.status >= 400: session.log.warning( "OOB url cannot be downloaded: %s, sending the URL as text" " instead.", response, ) return await session.on_text(entity, url, **kwargs) return await session.on_file(entity, url, http_response=response, **kwargs) async def __send_bob( self, from_: JID, cid: str, session: BaseSession, entity: Recipient, **kwargs ) -> int | str | None: sticker = self.xmpp.store.bob.get_sticker(cid) if sticker is None: await self.xmpp.plugin["xep_0231"].get_bob(from_, cid) sticker = self.xmpp.store.bob.get_sticker(cid) assert sticker is not None return await session.on_sticker(entity, sticker, **kwargs) log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/muc/000077500000000000000000000000001477703150600174505ustar00rootroot00000000000000slidge/slidge/core/dispatcher/muc/__init__.py000066400000000000000000000004151477703150600215610ustar00rootroot00000000000000from .admin import MucAdminMixin from .mam import MamMixin from .misc import MucMiscMixin from .owner import MucOwnerMixin from .ping import PingMixin class MucMixin(PingMixin, MamMixin, MucAdminMixin, MucOwnerMixin, MucMiscMixin): pass __all__ = ("MucMixin",) slidge/slidge/core/dispatcher/muc/admin.py000066400000000000000000000063711477703150600211210ustar00rootroot00000000000000from slixmpp import JID, CoroutineCallback, Iq, StanzaPath from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import StanzaBase from ..util import DispatcherMixin, exceptions_to_xmpp_errors class MucAdminMixin(DispatcherMixin): def __init__(self, xmpp) -> None: super().__init__(xmpp) self.xmpp.register_handler( CoroutineCallback( "MUCModerate", StanzaPath("iq/moderate"), self.on_user_moderation, ) ) self.xmpp.register_handler( CoroutineCallback( "MUCSetAffiliation", StanzaPath("iq@type=set/mucadmin_query"), self.on_user_set_affiliation, ) ) self.xmpp.register_handler( CoroutineCallback( "MUCGetAffiliation", StanzaPath("iq@type=get/mucadmin_query"), self.on_muc_admin_query_get, ) ) @exceptions_to_xmpp_errors async def on_user_moderation(self, iq: StanzaBase) -> None: assert isinstance(iq, Iq) muc = await self.get_muc_from_stanza(iq) moderate = iq["moderate"] xmpp_id = iq["moderate"]["id"] if not xmpp_id: raise XMPPError("bad-request", "Missing moderated message ID") if not moderate["retract"]: raise XMPPError( "feature-not-implemented", "Slidge only implements moderation/retraction", ) legacy_id = self._xmpp_msg_id_to_legacy(muc.session, xmpp_id) await muc.session.on_moderate(muc, legacy_id, moderate["reason"] or None) iq.reply(clear=True).send() @exceptions_to_xmpp_errors async def on_user_set_affiliation(self, iq: StanzaBase) -> None: assert isinstance(iq, Iq) muc = await self.get_muc_from_stanza(iq) item = iq["mucadmin_query"]["item"] if item["jid"]: contact = await muc.session.contacts.by_jid(JID(item["jid"])) else: part = await muc.get_participant( item["nick"], fill_first=True, raise_if_not_found=True ) assert part.contact is not None contact = part.contact if item["affiliation"]: await muc.on_set_affiliation( contact, item["affiliation"], item["reason"] or None, item["nick"] or None, ) elif item["role"] == "none": await muc.on_kick(contact, item["reason"] or None) iq.reply(clear=True).send() @exceptions_to_xmpp_errors async def on_muc_admin_query_get(self, iq: StanzaBase) -> None: assert isinstance(iq, Iq) affiliation = iq["mucadmin_query"]["item"]["affiliation"] if not affiliation: raise XMPPError("bad-request") session = await self._get_session(iq, 1, logged=True) muc = await session.bookmarks.by_jid(iq.get_to()) reply = iq.reply() reply.enable("mucadmin_query") async for participant in muc.get_participants(): if not participant.affiliation == affiliation: continue reply["mucadmin_query"].append(participant.mucadmin_item()) reply.send() slidge/slidge/core/dispatcher/muc/mam.py000066400000000000000000000054051477703150600206000ustar00rootroot00000000000000import asyncio from typing import TYPE_CHECKING from slixmpp import CoroutineCallback, Iq, StanzaPath from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import StanzaBase from ... import config from ..util import DispatcherMixin, exceptions_to_xmpp_errors if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class MamMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) self.__mam_cleanup_task = xmpp.loop.create_task(self.__mam_cleanup()) xmpp.register_handler( CoroutineCallback( "MAM_query", StanzaPath("iq@type=set/mam"), self.__handle_mam, ) ) xmpp.register_handler( CoroutineCallback( "MAM_get_from", StanzaPath("iq@type=get/mam"), self.__handle_mam_get_form, ) ) xmpp.register_handler( CoroutineCallback( "MAM_get_meta", StanzaPath("iq@type=get/mam_metadata"), self.__handle_mam_metadata, ) ) async def __mam_cleanup(self): if not config.MAM_MAX_DAYS: return while True: await asyncio.sleep(3600 * 6) self.xmpp.store.mam.nuke_older_than(config.MAM_MAX_DAYS) @exceptions_to_xmpp_errors async def __handle_mam(self, iq: Iq): muc = await self.get_muc_from_stanza(iq) await muc.send_mam(iq) async def __handle_mam_get_form(self, iq: StanzaBase): assert isinstance(iq, Iq) ito = iq.get_to() if ito == self.xmpp.boundjid.bare: raise XMPPError( text="No MAM on the component itself, use a JID with a resource" ) session = await self._get_session(iq, 0, logged=True) await session.bookmarks.by_jid(ito) reply = iq.reply() form = self.xmpp.plugin["xep_0004"].make_form() form.add_field(ftype="hidden", var="FORM_TYPE", value="urn:xmpp:mam:2") form.add_field(ftype="jid-single", var="with") form.add_field(ftype="text-single", var="start") form.add_field(ftype="text-single", var="end") form.add_field(ftype="text-single", var="before-id") form.add_field(ftype="text-single", var="after-id") form.add_field(ftype="boolean", var="include-groupchat") field = form.add_field(ftype="list-multi", var="ids") field["validate"]["datatype"] = "xs:string" field["validate"]["open"] = True reply["mam"].append(form) reply.send() @exceptions_to_xmpp_errors async def __handle_mam_metadata(self, iq: Iq): muc = await self.get_muc_from_stanza(iq) await muc.send_mam_metadata(iq) slidge/slidge/core/dispatcher/muc/misc.py000066400000000000000000000077441477703150600207710ustar00rootroot00000000000000import logging from slixmpp import JID, CoroutineCallback, Iq, Message, Presence, StanzaPath from slixmpp.exceptions import XMPPError from ..util import DispatcherMixin, exceptions_to_xmpp_errors class MucMiscMixin(DispatcherMixin): def __init__(self, xmpp): super().__init__(xmpp) xmpp.register_handler( CoroutineCallback( "ibr_remove", StanzaPath("/iq/register"), self.on_ibr_remove ) ) xmpp.add_event_handler("groupchat_join", self.on_groupchat_join) xmpp.add_event_handler( "groupchat_direct_invite", self.on_groupchat_direct_invite ) xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject) xmpp.add_event_handler("groupchat_message_error", self.__on_group_chat_error) async def __on_group_chat_error(self, msg: Message): condition = msg["error"].get_condition() if condition not in KICKABLE_ERRORS: return try: muc = await self.get_muc_from_stanza(msg) except XMPPError as e: log.debug("Not removing resource", exc_info=e) return mfrom = msg.get_from() resource = mfrom.resource try: muc.remove_user_resource(resource) except KeyError: # this actually happens quite frequently on for both beagle and monal # (not sure why?), but is of no consequence log.debug("%s was not in the resources of %s", resource, muc) else: log.info( "Removed %s from the resources of %s because of error", resource, muc ) @exceptions_to_xmpp_errors async def on_ibr_remove(self, iq: Iq): if iq.get_to() == self.xmpp.boundjid.bare: return if iq["type"] == "set" and iq["register"]["remove"]: muc = await self.get_muc_from_stanza(iq) await muc.session.on_leave_group(muc.legacy_id) iq.reply().send() await muc.session.bookmarks.remove( muc, "You left this chat from an XMPP client." ) return raise XMPPError("feature-not-implemented") @exceptions_to_xmpp_errors async def on_groupchat_join(self, p: Presence): if not self.xmpp.GROUPS: raise XMPPError( "feature-not-implemented", "This gateway does not implement multi-user chats.", ) muc = await self.get_muc_from_stanza(p) await muc.join(p) @exceptions_to_xmpp_errors async def on_groupchat_direct_invite(self, msg: Message): invite = msg["groupchat_invite"] jid = JID(invite["jid"]) if jid.domain != self.xmpp.boundjid.bare: raise XMPPError( "bad-request", "Legacy contacts can only be invited to legacy groups, not standard XMPP MUCs.", ) if invite["password"]: raise XMPPError( "bad-request", "Password-protected groups are not supported" ) session = await self._get_session(msg, logged=True) contact = await session.contacts.by_jid(msg.get_to()) muc = await session.bookmarks.by_jid(jid) await session.on_invitation(contact, muc, invite["reason"] or None) @exceptions_to_xmpp_errors async def on_groupchat_subject(self, msg: Message): muc = await self.get_muc_from_stanza(msg) if not muc.HAS_SUBJECT: raise XMPPError( "bad-request", "There are no room subject in here. " "Use the room configuration to update its name or description", ) await muc.on_set_subject(msg["subject"]) KICKABLE_ERRORS = { "gone", "internal-server-error", "item-not-found", "jid-malformed", "recipient-unavailable", "redirect", "remote-server-not-found", "remote-server-timeout", "service-unavailable", "malformed error", } log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/muc/owner.py000066400000000000000000000063541477703150600211640ustar00rootroot00000000000000from slixmpp import CoroutineCallback, Iq, StanzaPath from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0004 import Form from slixmpp.xmlstream import StanzaBase from ..util import DispatcherMixin, exceptions_to_xmpp_errors class MucOwnerMixin(DispatcherMixin): def __init__(self, xmpp): super().__init__(xmpp) xmpp.register_handler( CoroutineCallback( "MUCOwnerGet", StanzaPath("iq@type=get/mucowner_query"), self.on_muc_owner_query, ) ) xmpp.register_handler( CoroutineCallback( "MUCOwnerSet", StanzaPath("iq@type=set/mucowner_query"), self.on_muc_owner_set, ) ) @exceptions_to_xmpp_errors async def on_muc_owner_query(self, iq: StanzaBase) -> None: assert isinstance(iq, Iq) muc = await self.get_muc_from_stanza(iq) reply = iq.reply() form = Form(title="Slidge room configuration") form["instructions"] = ( "Complete this form to modify the configuration of your room." ) form.add_field( var="FORM_TYPE", type="hidden", value="http://jabber.org/protocol/muc#roomconfig", ) form.add_field( var="muc#roomconfig_roomname", label="Natural-Language Room Name", type="text-single", value=muc.name, ) if muc.HAS_DESCRIPTION: form.add_field( var="muc#roomconfig_roomdesc", label="Short Description of Room", type="text-single", value=muc.description, ) muc_owner = iq["mucowner_query"] muc_owner.append(form) reply.append(muc_owner) reply.send() @exceptions_to_xmpp_errors async def on_muc_owner_set(self, iq: StanzaBase) -> None: assert isinstance(iq, Iq) muc = await self.get_muc_from_stanza(iq) query = iq["mucowner_query"] if form := query.get_plugin("form", check=True): values = form.get_values() await muc.on_set_config( name=values.get("muc#roomconfig_roomname"), description=( values.get("muc#roomconfig_roomdesc") if muc.HAS_DESCRIPTION else None ), ) form["type"] = "result" clear = False elif destroy := query.get_plugin("destroy", check=True): reason = destroy["reason"] or None await muc.on_destroy_request(reason) user_participant = await muc.get_user_participant() user_participant._affiliation = "none" user_participant._role = "none" presence = user_participant._make_presence(ptype="unavailable", force=True) presence["muc"].enable("destroy") if reason is not None: presence["muc"]["destroy"]["reason"] = reason user_participant._send(presence) await muc.session.bookmarks.remove(muc, kick=False) clear = True else: raise XMPPError("bad-request") iq.reply(clear=clear).send() slidge/slidge/core/dispatcher/muc/ping.py000066400000000000000000000031741477703150600207640ustar00rootroot00000000000000from typing import TYPE_CHECKING from slixmpp import CoroutineCallback, Iq, StanzaPath from slixmpp.exceptions import XMPPError from ....group import LegacyMUC from ..util import DispatcherMixin, exceptions_to_xmpp_errors if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class PingMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) xmpp.remove_handler("Ping") xmpp.register_handler( CoroutineCallback( "Ping", StanzaPath("iq@type=get/ping"), self.__handle_ping, ) ) xmpp.plugin["xep_0030"].add_feature("urn:xmpp:ping") @exceptions_to_xmpp_errors async def __handle_ping(self, iq: Iq) -> None: ito = iq.get_to() if ito == self.xmpp.boundjid.bare: iq.reply().send() session = await self._get_session(iq) try: muc = await session.bookmarks.by_jid(ito) except XMPPError: pass else: self.__handle_muc_ping(muc, iq) return try: await session.contacts.by_jid(ito) except XMPPError: pass else: iq.reply().send() return raise XMPPError( "item-not-found", f"This JID does not match anything slidge knows: {ito}" ) @staticmethod def __handle_muc_ping(muc: LegacyMUC, iq: Iq) -> None: if iq.get_from().resource in muc.get_user_resources(): iq.reply().send() else: raise XMPPError("not-acceptable", etype="cancel", by=muc.jid) slidge/slidge/core/dispatcher/presence.py000066400000000000000000000144251477703150600210500ustar00rootroot00000000000000import logging from slixmpp import JID, Presence from slixmpp.exceptions import XMPPError from ...util.util import merge_resources from ..session import BaseSession from .util import DispatcherMixin, exceptions_to_xmpp_errors class _IsDirectedAtComponent(Exception): def __init__(self, session: BaseSession): self.session = session class PresenceHandlerMixin(DispatcherMixin): def __init__(self, xmpp): super().__init__(xmpp) xmpp.add_event_handler("presence_subscribe", self._handle_subscribe) xmpp.add_event_handler("presence_subscribed", self._handle_subscribed) xmpp.add_event_handler("presence_unsubscribe", self._handle_unsubscribe) xmpp.add_event_handler("presence_unsubscribed", self._handle_unsubscribed) xmpp.add_event_handler("presence_probe", self._handle_probe) xmpp.add_event_handler("presence", self.on_presence) async def __get_contact(self, pres: Presence): sess = await self._get_session(pres) pto = pres.get_to() if pto == self.xmpp.boundjid.bare: raise _IsDirectedAtComponent(sess) await sess.contacts.ready return await sess.contacts.by_jid(pto) @exceptions_to_xmpp_errors async def _handle_subscribe(self, pres: Presence): try: contact = await self.__get_contact(pres) except _IsDirectedAtComponent: pres.reply().send() return if contact.is_friend: pres.reply().send() else: await contact.on_friend_request(pres["status"]) @exceptions_to_xmpp_errors async def _handle_unsubscribe(self, pres: Presence): pres.reply().send() try: contact = await self.__get_contact(pres) except _IsDirectedAtComponent as e: e.session.send_gateway_message("Bye bye!") await e.session.kill_by_jid(e.session.user_jid) return contact.is_friend = False await contact.on_friend_delete(pres["status"]) @exceptions_to_xmpp_errors async def _handle_subscribed(self, pres: Presence): try: contact = await self.__get_contact(pres) except _IsDirectedAtComponent: return await contact.on_friend_accept() @exceptions_to_xmpp_errors async def _handle_unsubscribed(self, pres: Presence): try: contact = await self.__get_contact(pres) except _IsDirectedAtComponent: return if contact.is_friend: contact.is_friend = False await contact.on_friend_delete(pres["status"]) @exceptions_to_xmpp_errors async def _handle_probe(self, pres: Presence): try: contact = await self.__get_contact(pres) except _IsDirectedAtComponent: session = await self._get_session(pres) session.send_cached_presence(pres.get_from()) return if contact.is_friend: contact.send_last_presence(force=True) else: reply = pres.reply() reply["type"] = "unsubscribed" reply.send() @exceptions_to_xmpp_errors async def on_presence(self, p: Presence): if p.get_plugin("muc_join", check=True): # handled in on_groupchat_join # without this early return, since we switch from and to in this # presence stanza, on_groupchat_join ends up trying to instantiate # a MUC with the user's JID, which in turn leads to slidge sending # a (error) presence from=the user's JID, which terminates the # XML stream. return session = await self._get_session(p) pto = p.get_to() if pto == self.xmpp.boundjid.bare: session.log.debug("Received a presence from %s", p.get_from()) if (ptype := p.get_type()) not in _USEFUL_PRESENCES: return if not session.user.preferences.get("sync_presence", False): session.log.debug("User does not want to sync their presence") return # NB: get_type() returns either a proper presence type or # a presence show if available. Weird, weird, weird slix. resources = self.xmpp.roster[self.xmpp.boundjid.bare][ p.get_from() ].resources await session.on_presence( p.get_from().resource, ptype, # type: ignore p["status"], resources, merge_resources(resources), ) if p.get_type() == "available": await self.xmpp.pubsub.on_presence_available(p, None) return if p.get_type() == "available": try: contact = await session.contacts.by_jid(pto) except XMPPError: contact = None if contact is not None: await self.xmpp.pubsub.on_presence_available(p, contact) return muc = session.bookmarks.by_jid_only_if_exists(JID(pto.bare)) if muc is not None and p.get_type() == "unavailable": return muc.on_presence_unavailable(p) if muc is None or p.get_from().resource not in muc.get_user_resources(): return if pto.resource == muc.user_nick: # Ignore presence stanzas with the valid nick. # even if joined to the group, we might receive those from clients, # when setting a status message, or going away, etc. return # We can't use XMPPError here because XMPPError does not have a way to # add the element error_stanza = p.error() error_stanza.set_to(p.get_from()) error_stanza.set_from(pto) error_stanza.enable("muc_join") # error_stanza.enable("error") error_stanza["error"]["type"] = "cancel" error_stanza["error"]["by"] = muc.jid error_stanza["error"]["condition"] = "not-acceptable" error_stanza["error"]["text"] = ( "Slidge does not let you change your nickname in groups." ) error_stanza.send() _USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"} log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/registration.py000066400000000000000000000061231477703150600217520ustar00rootroot00000000000000from __future__ import annotations import logging from typing import TYPE_CHECKING, Optional from slixmpp import JID, Iq from slixmpp.exceptions import XMPPError from ...db import GatewayUser from .. import config from .util import DispatcherMixin if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class RegistrationMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) self.xmpp = xmpp xmpp["xep_0077"].api.register( self.xmpp.make_registration_form, "make_registration_form" ) xmpp["xep_0077"].api.register(self._user_get, "user_get") xmpp["xep_0077"].api.register(self._user_validate, "user_validate") xmpp["xep_0077"].api.register(self._user_modify, "user_modify") # kept for slixmpp internal API compat # TODO: either fully use slixmpp internal API or rewrite registration without it at all xmpp["xep_0077"].api.register(lambda *a: None, "user_remove") xmpp.add_event_handler("user_register", self._on_user_register) xmpp.add_event_handler("user_unregister", self._on_user_unregister) def get_user(self, jid: JID) -> GatewayUser | None: return self.xmpp.store.users.get(jid) async def _user_get( self, _gateway_jid, _node, ifrom: JID, iq: Iq ) -> GatewayUser | None: if ifrom is None: ifrom = iq.get_from() return self.get_user(ifrom) async def _user_validate(self, _gateway_jid, _node, ifrom: JID, iq: Iq): xmpp = self.xmpp log.debug("User validate: %s", ifrom.bare) form_dict = {f.var: iq.get(f.var) for f in xmpp.REGISTRATION_FIELDS} xmpp.raise_if_not_allowed_jid(ifrom) legacy_module_data = await xmpp.user_prevalidate(ifrom, form_dict) if legacy_module_data is None: legacy_module_data = form_dict user = self.xmpp.store.users.new( jid=ifrom, legacy_module_data=legacy_module_data, # type:ignore ) log.info("New user: %s", user) async def _user_modify( self, _gateway_jid, _node, ifrom: JID, form_dict: dict[str, Optional[str]] ): await self.xmpp.user_prevalidate(ifrom, form_dict) log.debug("Modify user: %s", ifrom) user = self.xmpp.store.users.get(ifrom) if user is None: raise XMPPError("internal-server-error", "User not found") user.legacy_module_data.update(form_dict) self.xmpp.store.users.update(user) async def _on_user_register(self, iq: Iq): session = await self._get_session(iq, wait_for_ready=False) for jid in config.ADMINS: self.xmpp.send_message( mto=jid, mbody=f"{iq.get_from()} has registered", mtype="chat", mfrom=self.xmpp.boundjid.bare, ) session.send_gateway_message(self.xmpp.WELCOME_MESSAGE) await self.xmpp.login_wrap(session) async def _on_user_unregister(self, iq: Iq): await self.xmpp.session_cls.kill_by_jid(iq.get_from()) log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/search.py000066400000000000000000000063401477703150600205060ustar00rootroot00000000000000from typing import TYPE_CHECKING from slixmpp import JID, CoroutineCallback, Iq, StanzaPath from slixmpp.exceptions import XMPPError from .util import DispatcherMixin, exceptions_to_xmpp_errors if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class SearchMixin(DispatcherMixin): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) xmpp["xep_0055"].api.register(self.search_get_form, "search_get_form") xmpp["xep_0055"].api.register(self._search_query, "search_query") xmpp.plugin["xep_0030"].add_feature("jabber:iq:gateway") xmpp.register_handler( CoroutineCallback( "iq:gateway", StanzaPath("iq/gateway"), self._handle_gateway_iq, # type: ignore ) ) async def search_get_form(self, _gateway_jid, _node, ifrom: JID, iq: Iq): """ Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS` """ user = self.xmpp.store.users.get(ifrom) if user is None: raise XMPPError(text="Search is only allowed for registered users") xmpp = self.xmpp reply = iq.reply() form = reply["search"]["form"] form["title"] = xmpp.SEARCH_TITLE form["instructions"] = xmpp.SEARCH_INSTRUCTIONS for field in xmpp.SEARCH_FIELDS: form.append(field.get_xml()) return reply async def _search_query(self, _gateway_jid, _node, ifrom: JID, iq: Iq): """ Handles a search request """ session = await self._get_session(iq) result = await session.on_search(iq["search"]["form"].get_values()) if not result: raise XMPPError("item-not-found", text="Nothing was found") reply = iq.reply() form = reply["search"]["form"] for field in result.fields: form.add_reported(field.var, label=field.label, type=field.type) for item in result.items: form.add_item(item) return reply @exceptions_to_xmpp_errors async def _handle_gateway_iq(self, iq: Iq): if iq.get_to() != self.xmpp.boundjid.bare: raise XMPPError("bad-request", "This can only be used on the component JID") if len(self.xmpp.SEARCH_FIELDS) > 1: raise XMPPError( "feature-not-implemented", "Use jabber search for this gateway" ) session = await self._get_session(iq) field = self.xmpp.SEARCH_FIELDS[0] reply = iq.reply() if iq["type"] == "get": reply["gateway"]["desc"] = self.xmpp.SEARCH_TITLE reply["gateway"]["prompt"] = field.label elif iq["type"] == "set": prompt = iq["gateway"]["prompt"] result = await session.on_search({field.var: prompt}) if result is None or not result.items: raise XMPPError( "item-not-found", "No contact was found with the info you provided." ) if len(result.items) > 1: raise XMPPError( "bad-request", "Your search yielded more than one result." ) reply["gateway"]["jid"] = result.items[0]["jid"] reply.send() slidge/slidge/core/dispatcher/session_dispatcher.py000066400000000000000000000061141477703150600231310ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING from slixmpp import Message from slixmpp.exceptions import IqError, IqTimeout from slixmpp.plugins.xep_0084.stanza import Info from ..session import BaseSession from .caps import CapsMixin from .disco import DiscoMixin from .message import MessageMixin from .muc import MucMixin from .presence import PresenceHandlerMixin from .registration import RegistrationMixin from .search import SearchMixin from .util import exceptions_to_xmpp_errors from .vcard import VCardMixin if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class SessionDispatcher( CapsMixin, DiscoMixin, RegistrationMixin, MessageMixin, MucMixin, PresenceHandlerMixin, SearchMixin, VCardMixin, ): def __init__(self, xmpp: "BaseGateway"): super().__init__(xmpp) xmpp.add_event_handler( "avatar_metadata_publish", self.on_avatar_metadata_publish ) @exceptions_to_xmpp_errors async def on_avatar_metadata_publish(self, m: Message): session = await self._get_session(m, timeout=None) if not session.user.preferences.get("sync_avatar", False): session.log.debug("User does not want to sync their avatar") return info = m["pubsub_event"]["items"]["item"]["avatar_metadata"]["info"] await self.on_avatar_metadata_info(session, info) async def on_avatar_metadata_info(self, session: BaseSession, info: Info): hash_ = info["id"] if session.user.avatar_hash == hash_: session.log.debug("We already know this avatar hash") return if hash_: try: iq = await self.xmpp.plugin["xep_0084"].retrieve_avatar( session.user_jid, hash_, ifrom=self.xmpp.boundjid.bare ) except (IqError, IqTimeout) as e: session.log.warning("Could not fetch the user's avatar: %s", e) return bytes_ = iq["pubsub"]["items"]["item"]["avatar_data"]["value"] type_ = info["type"] height = info["height"] width = info["width"] else: self.xmpp.store.users.set_avatar_hash(session.user_pk, None) bytes_ = type_ = height = width = hash_ = None try: await session.on_avatar(bytes_, hash_, type_, width, height) except NotImplementedError: pass except Exception as e: # If something goes wrong here, replying an error stanza will to the # avatar update will likely not show in most clients, so let's send # a normal message from the component to the user. session.send_gateway_message( f"Something went wrong trying to set your avatar: {e!r}" ) else: self.xmpp.store.users.set_avatar_hash(session.user_pk, hash_) for room in session.bookmarks: participant = await room.get_user_participant() participant.send_last_presence(force=True, no_cache_online=True) log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/util.py000066400000000000000000000131721477703150600202170ustar00rootroot00000000000000import logging from functools import wraps from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar from slixmpp import JID, Iq, Message, Presence from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import StanzaBase from ...util.types import Recipient, RecipientType from ..session import BaseSession if TYPE_CHECKING: from slidge import BaseGateway from slidge.group import LegacyMUC class Ignore(BaseException): pass class DispatcherMixin: def __init__(self, xmpp: "BaseGateway"): self.xmpp = xmpp async def _get_session( self, stanza: Message | Presence | Iq, timeout: int | None = 10, wait_for_ready=True, logged=False, ) -> BaseSession: xmpp = self.xmpp if stanza.get_from().server == xmpp.boundjid.bare: log.debug("Ignoring echo") raise Ignore if ( isinstance(stanza, Message) and stanza.get_type() == "chat" and stanza.get_to() == xmpp.boundjid.bare ): log.debug("Ignoring message to component") raise Ignore session = await self._get_session_from_jid( stanza.get_from(), timeout, wait_for_ready, logged ) if isinstance(stanza, Message) and _ignore(session, stanza): raise Ignore return session async def _get_session_from_jid( self, jid: JID, timeout: int | None = 10, wait_for_ready=True, logged=False, ) -> BaseSession: session = self.xmpp.get_session_from_jid(jid) if session is None: raise XMPPError("registration-required") if logged: session.raise_if_not_logged() if wait_for_ready: await session.wait_for_ready(timeout) return session async def get_muc_from_stanza(self, iq: Iq | Message | Presence) -> "LegacyMUC": ito = iq.get_to() if ito == self.xmpp.boundjid.bare: raise XMPPError("bad-request", text="This is only handled for MUCs") session = await self._get_session(iq, logged=True) muc = await session.bookmarks.by_jid(ito) return muc def _xmpp_msg_id_to_legacy(self, session: "BaseSession", xmpp_id: str): sent = self.xmpp.store.sent.get_legacy_id(session.user_pk, xmpp_id) if sent is not None: return self.xmpp.LEGACY_MSG_ID_TYPE(sent) multi = self.xmpp.store.multi.get_legacy_id(session.user_pk, xmpp_id) if multi: return self.xmpp.LEGACY_MSG_ID_TYPE(multi) try: return session.xmpp_to_legacy_msg_id(xmpp_id) except XMPPError: raise except Exception as e: log.debug("Couldn't convert xmpp msg ID to legacy ID.", exc_info=e) raise XMPPError( "internal-server-error", "Couldn't convert xmpp msg ID to legacy ID." ) async def _get_session_entity_thread( self, msg: Message ) -> tuple["BaseSession", Recipient, int | str]: session = await self._get_session(msg) e: Recipient = await _get_entity(session, msg) legacy_thread = await self._xmpp_to_legacy_thread(session, msg, e) return session, e, legacy_thread async def _xmpp_to_legacy_thread( self, session: "BaseSession", msg: Message, recipient: RecipientType ): xmpp_thread = msg["thread"] if not xmpp_thread: return None if session.MESSAGE_IDS_ARE_THREAD_IDS: return self._xmpp_msg_id_to_legacy(session, xmpp_thread) legacy_thread_str = session.xmpp.store.sent.get_legacy_thread( session.user_pk, xmpp_thread ) if legacy_thread_str is not None: return session.xmpp.LEGACY_MSG_ID_TYPE(legacy_thread_str) async with session.thread_creation_lock: legacy_thread = await recipient.create_thread(xmpp_thread) session.xmpp.store.sent.set_thread( session.user_pk, str(legacy_thread), xmpp_thread ) return legacy_thread def _ignore(session: "BaseSession", msg: Message): i = msg.get_id() if i.startswith("slidge-carbon-"): return True if i not in session.ignore_messages: return False session.log.debug("Ignored sent carbon: %s", i) session.ignore_messages.remove(i) return True async def _get_entity(session: "BaseSession", m: Message) -> RecipientType: session.raise_if_not_logged() if m.get_type() == "groupchat": muc = await session.bookmarks.by_jid(m.get_to()) r = m.get_from().resource if r not in muc.get_user_resources(): session.create_task(muc.kick_resource(r)) raise XMPPError("not-acceptable", "You are not connected to this chat") return muc else: return await session.contacts.by_jid(m.get_to()) StanzaType = TypeVar("StanzaType", bound=StanzaBase) HandlerType = Callable[[Any, StanzaType], Awaitable[None]] def exceptions_to_xmpp_errors(cb: HandlerType) -> HandlerType: @wraps(cb) async def wrapped(*args): try: await cb(*args) except Ignore: pass except XMPPError: raise except NotImplementedError: log.debug("NotImplementedError raised in %s", cb) raise XMPPError( "feature-not-implemented", "Not implemented by the legacy module" ) except Exception as e: log.error("Failed to handle incoming stanza: %s", args, exc_info=e) raise XMPPError("internal-server-error", str(e)) return wrapped log = logging.getLogger(__name__) slidge/slidge/core/dispatcher/vcard.py000066400000000000000000000120121477703150600203310ustar00rootroot00000000000000from copy import copy from slixmpp import CoroutineCallback, Iq, StanzaPath, register_stanza_plugin from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0084 import MetaData from slixmpp.plugins.xep_0292.stanza import NS as VCard4NS from ...contact import LegacyContact from ...core.session import BaseSession from ...group import LegacyParticipant from .util import DispatcherMixin, exceptions_to_xmpp_errors class VCardMixin(DispatcherMixin): def __init__(self, xmpp): super().__init__(xmpp) xmpp.register_handler( CoroutineCallback( "get_vcard", StanzaPath("iq@type=get/vcard"), self.on_get_vcard ) ) xmpp.remove_handler("VCardTemp") xmpp.register_handler( CoroutineCallback( "VCardTemp", StanzaPath("iq/vcard_temp"), self.__vcard_temp_handler, ) ) # TODO: MR to slixmpp adding this to XEP-0084 register_stanza_plugin( self.xmpp.plugin["xep_0060"].stanza.Item, MetaData, ) @exceptions_to_xmpp_errors async def on_get_vcard(self, iq: Iq): session = await self._get_session(iq, logged=True) contact = await session.contacts.by_jid(iq.get_to()) vcard = await contact.get_vcard() reply = iq.reply() if vcard: reply.append(vcard) else: reply.enable("vcard") reply.send() @exceptions_to_xmpp_errors async def __vcard_temp_handler(self, iq: Iq): if iq["type"] == "get": return await self.__handle_get_vcard_temp(iq) if iq["type"] == "set": return await self.__handle_set_vcard_temp(iq) async def __fetch_user_avatar(self, session: BaseSession): hash_ = session.user.avatar_hash if not hash_: raise XMPPError( "item-not-found", "The slidge user does not have any avatar set" ) meta_iq = await self.xmpp.plugin["xep_0060"].get_item( session.user_jid, MetaData.namespace, hash_, ifrom=self.xmpp.boundjid.bare, ) info = meta_iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"] type_ = info["type"] data_iq = await self.xmpp.plugin["xep_0084"].retrieve_avatar( session.user_jid, hash_, ifrom=self.xmpp.boundjid.bare ) bytes_ = data_iq["pubsub"]["items"]["item"]["avatar_data"]["value"] return bytes_, type_ async def __handle_get_vcard_temp(self, iq: Iq): session = self.xmpp.get_session_from_stanza(iq) entity = await session.get_contact_or_group_or_participant(iq.get_to(), False) if not entity: raise XMPPError("item-not-found") bytes_ = None if isinstance(entity, LegacyParticipant): if entity.is_user: bytes_, type_ = await self.__fetch_user_avatar(session) if not bytes_: raise XMPPError( "internal-server-error", "Could not fetch the slidge user's avatar", ) avatar = None vcard = None elif not (contact := entity.contact): raise XMPPError("item-not-found", "This participant has no contact") else: vcard = await contact.get_vcard() avatar = contact.get_avatar() type_ = "image/png" else: avatar = entity.get_avatar() type_ = "image/png" if isinstance(entity, LegacyContact): vcard = await entity.get_vcard(fetch=False) else: vcard = None v = self.xmpp.plugin["xep_0054"].make_vcard() if avatar is not None and avatar.data: bytes_ = avatar.data.get_value() if bytes_: v["PHOTO"]["BINVAL"] = bytes_ v["PHOTO"]["TYPE"] = type_ if vcard: for el in vcard.xml: new = copy(el) new.tag = el.tag.replace(f"{{{VCard4NS}}}", "") v.append(new) reply = iq.reply() reply.append(v) reply.send() async def __handle_set_vcard_temp(self, iq: Iq): muc = await self.get_muc_from_stanza(iq) to = iq.get_to() if to.resource: raise XMPPError("bad-request", "You cannot set participants avatars") data = iq["vcard_temp"]["PHOTO"]["BINVAL"] or None try: legacy_id = await muc.on_avatar( data, iq["vcard_temp"]["PHOTO"]["TYPE"] or None ) except XMPPError: raise except Exception as e: raise XMPPError("internal-server-error", str(e)) reply = iq.reply(clear=True) reply.enable("vcard_temp") reply.send() if not data: await muc.set_avatar(None, blocking=True) return if legacy_id: await muc.set_avatar(data, legacy_id, blocking=True) slidge/slidge/core/gateway.py000066400000000000000000001113461477703150600165570ustar00rootroot00000000000000""" This module extends slixmpp.ComponentXMPP to make writing new LegacyClients easier """ import asyncio import logging import re import tempfile from copy import copy from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Sequence, Union import aiohttp import qrcode from slixmpp import JID, ComponentXMPP, Iq, Message, Presence from slixmpp.exceptions import IqError, IqTimeout, XMPPError from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation from slixmpp.types import MessageTypes from slixmpp.xmlstream.xmlstream import NotConnectedError from slidge import command # noqa: F401 from slidge.command.adhoc import AdhocProvider from slidge.command.admin import Exec from slidge.command.base import Command, FormField from slidge.command.chat_command import ChatCommandProvider from slidge.command.register import RegistrationType from slidge.core import config from slidge.core.dispatcher.session_dispatcher import SessionDispatcher from slidge.core.mixins import MessageMixin from slidge.core.pubsub import PubSubComponent from slidge.core.session import BaseSession from slidge.db import GatewayUser, SlidgeStore from slidge.db.avatar import avatar_cache from slidge.slixfix import PrivilegedIqError from slidge.slixfix.delivery_receipt import DeliveryReceipt from slidge.slixfix.roster import RosterBackend from slidge.util import ABCSubclassableOnceAtMost from slidge.util.types import AvatarType, MessageOrPresenceTypeVar from slidge.util.util import timeit if TYPE_CHECKING: pass class BaseGateway( ComponentXMPP, MessageMixin, metaclass=ABCSubclassableOnceAtMost, ): """ The gateway component, handling registrations and un-registrations. On slidge launch, a singleton is instantiated, and it will be made available to public classes such :class:`.LegacyContact` or :class:`.BaseSession` as the ``.xmpp`` attribute. Must be subclassed by a legacy module to set up various aspects of the XMPP component behaviour, such as its display name or welcome message, via class attributes :attr:`.COMPONENT_NAME` :attr:`.WELCOME_MESSAGE`. Abstract methods related to the registration process must be overriden for a functional :term:`Legacy Module`: - :meth:`.validate` - :meth:`.validate_two_factor_code` - :meth:`.get_qr_text` - :meth:`.confirm_qr` NB: Not all of these must be overridden, it depends on the :attr:`REGISTRATION_TYPE`. The other methods, such as :meth:`.send_text` or :meth:`.react` are the same as those of :class:`.LegacyContact` and :class:`.LegacyParticipant`, because the component itself is also a "messaging actor", ie, an :term:`XMPP Entity`. For these methods, you need to specify the JID of the recipient with the `mto` parameter. Since it inherits from :class:`slixmpp.componentxmpp.ComponentXMPP`,you also have a hand on low-level XMPP interactions via slixmpp methods, e.g.: .. code-block:: python self.send_presence( pfrom="somebody@component.example.com", pto="someonwelse@anotherexample.com", ) However, you should not need to do so often since the classes of the plugin API provides higher level abstractions around most commonly needed use-cases, such as sending messages, or displaying a custom status. """ COMPONENT_NAME: str = NotImplemented """Name of the component, as seen in service discovery by XMPP clients""" COMPONENT_TYPE: str = "" """Type of the gateway, should follow https://xmpp.org/registrar/disco-categories.html""" COMPONENT_AVATAR: Optional[AvatarType] = None """ Path, bytes or URL used by the component as an avatar. """ REGISTRATION_FIELDS: Sequence[FormField] = [ FormField(var="username", label="User name", required=True), FormField(var="password", label="Password", required=True, private=True), ] """ Iterable of fields presented to the gateway user when registering using :xep:`0077` `extended `_ by :xep:`0004`. """ REGISTRATION_INSTRUCTIONS: str = "Enter your credentials" """ The text presented to a user that wants to register (or modify) their legacy account configuration. """ REGISTRATION_TYPE: RegistrationType = RegistrationType.SINGLE_STEP_FORM """ This attribute determines how users register to the gateway, ie, how they login to the :term:`legacy service `. The credentials are then stored persistently, so this process should happen once per user (unless they unregister). The registration process always start with a basic data form (:xep:`0004`) presented to the user. But the legacy login flow might require something more sophisticated, see :class:`.RegistrationType` for more details. """ REGISTRATION_2FA_TITLE = "Enter your 2FA code" REGISTRATION_2FA_INSTRUCTIONS = ( "You should have received something via email or SMS, or something" ) REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link" PREFERENCES = [ FormField( var="sync_presence", label="Propagate your XMPP presence to the legacy network.", value="true", required=True, type="boolean", ), FormField( var="sync_avatar", label="Propagate your XMPP avatar to the legacy network.", value="true", required=True, type="boolean", ), ] ROSTER_GROUP: str = "slidge" """ Name of the group assigned to a :class:`.LegacyContact` automagically added to the :term:`User`'s roster with :meth:`.LegacyContact.add_to_roster`. """ WELCOME_MESSAGE = ( "Thank you for registering. Type 'help' to list the available commands, " "or just start messaging away!" ) """ A welcome message displayed to users on registration. This is useful notably for clients that don't consider component JIDs as a valid recipient in their UI, yet still open a functional chat window on incoming messages from components. """ SEARCH_FIELDS: Sequence[FormField] = [ FormField(var="first", label="First name", required=True), FormField(var="last", label="Last name", required=True), FormField(var="phone", label="Phone number", required=False), ] """ Fields used for searching items via the component, through :xep:`0055` (jabber search). A common use case is to allow users to search for legacy contacts by something else than their usernames, eg their phone number. Plugins should implement search by overriding :meth:`.BaseSession.search` (restricted to registered users). If there is only one field, it can also be used via the ``jabber:iq:gateway`` protocol described in :xep:`0100`. Limitation: this only works if the search request returns one result item, and if this item has a 'jid' var. """ SEARCH_TITLE: str = "Search for legacy contacts" """ Title of the search form. """ SEARCH_INSTRUCTIONS: str = "" """ Instructions of the search form. """ MARK_ALL_MESSAGES = False """ Set this to True for :term:`legacy networks ` that expects read marks for *all* messages and not just the latest one that was read (as most XMPP clients will only send a read mark for the latest msg). """ PROPER_RECEIPTS = False """ Set this to True if the legacy service provides a real equivalent of message delivery receipts (:xep:`0184`), meaning that there is an event thrown when the actual device of a contact receives a message. Make sure to call Contact.received() adequately if this is set to True. """ GROUPS = False mtype: MessageTypes = "chat" is_group = False _can_send_carbon = False store: SlidgeStore avatar_pk: int AVATAR_ID_TYPE: Callable[[str], Any] = str """ Modify this if the legacy network uses unique avatar IDs that are not strings. This is required because we store those IDs as TEXT in the persistent SQL DB. The callable specified here will receive is responsible for converting the serialised-as-text version of the avatar unique ID back to the proper type. Common example: ``int``. """ # FIXME: do we really need this since we have session.xmpp_to_legacy_msg_id? # (maybe we do) LEGACY_MSG_ID_TYPE: Callable[[str], Any] = str """ Modify this if the legacy network uses unique message IDs that are not strings. This is required because we store those IDs as TEXT in the persistent SQL DB. The callable specified here will receive is responsible for converting the serialised-as-text version of the message unique ID back to the proper type. Common example: ``int``. """ LEGACY_CONTACT_ID_TYPE: Callable[[str], Any] = str """ Modify this if the legacy network uses unique contact IDs that are not strings. This is required because we store those IDs as TEXT in the persistent SQL DB. The callable specified here is responsible for converting the serialised-as-text version of the contact unique ID back to the proper type. Common example: ``int``. """ LEGACY_ROOM_ID_TYPE: Callable[[str], Any] = str """ Modify this if the legacy network uses unique room IDs that are not strings. This is required because we store those IDs as TEXT in the persistent SQL DB. The callable specified here is responsible for converting the serialised-as-text version of the room unique ID back to the proper type. Common example: ``int``. """ http: aiohttp.ClientSession def __init__(self): if config.COMPONENT_NAME: self.COMPONENT_NAME = config.COMPONENT_NAME if config.WELCOME_MESSAGE: self.WELCOME_MESSAGE = config.WELCOME_MESSAGE self.log = log self.datetime_started = datetime.now() self.xmpp = self # ugly hack to work with the BaseSender mixin :/ self.default_ns = "jabber:component:accept" super().__init__( config.JID, config.SECRET, config.SERVER, config.PORT, plugin_whitelist=SLIXMPP_PLUGINS, plugin_config={ "xep_0077": { "form_fields": None, "form_instructions": self.REGISTRATION_INSTRUCTIONS, "enable_subscription": self.REGISTRATION_TYPE == RegistrationType.SINGLE_STEP_FORM, }, "xep_0100": { "component_name": self.COMPONENT_NAME, "type": self.COMPONENT_TYPE, }, "xep_0184": { "auto_ack": False, "auto_request": False, }, "xep_0363": { "upload_service": config.UPLOAD_SERVICE, }, }, fix_error_ns=True, ) self.loop.set_exception_handler(self.__exception_handler) self.loop.create_task(self.__set_http()) self.has_crashed: bool = False self.use_origin_id = False self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR) self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]() self.session_cls: BaseSession = BaseSession.get_unique_subclass() self.session_cls.xmpp = self from ..group.room import LegacyMUC LegacyMUC.get_self_or_unique_subclass().xmpp = self self.get_session_from_stanza: Callable[ [Union[Message, Presence, Iq]], BaseSession ] = self.session_cls.from_stanza # type: ignore self.get_session_from_user: Callable[[GatewayUser], BaseSession] = ( self.session_cls.from_user ) self.register_plugins() self.__register_slixmpp_events() self.__register_slixmpp_api() self.roster.set_backend(RosterBackend(self)) self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME}) self.pubsub: PubSubComponent = self["pubsub"] self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self) # with this we receive user avatar updates self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify") self.plugin["xep_0030"].add_feature("urn:xmpp:chat-markers:0") if self.GROUPS: self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc") self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2") self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2#extended") self.plugin["xep_0030"].add_feature(self.plugin["xep_0421"].namespace) self.plugin["xep_0030"].add_feature(self["xep_0317"].stanza.NS) self.plugin["xep_0030"].add_identity( category="conference", name=self.COMPONENT_NAME, itype="text", jid=self.boundjid, ) # why does mypy need these type annotations? no idea self.__adhoc_handler: AdhocProvider = AdhocProvider(self) self.__chat_commands_handler: ChatCommandProvider = ChatCommandProvider(self) self.__dispatcher = SessionDispatcher(self) self.__register_commands() MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__() async def __set_http(self): self.http = aiohttp.ClientSession() if getattr(self, "_test_mode", False): return avatar_cache.http = self.http def __register_commands(self): for cls in Command.subclasses: if any(x is NotImplemented for x in [cls.CHAT_COMMAND, cls.NODE, cls.NAME]): log.debug("Not adding command '%s' because it looks abstract", cls) continue if cls is Exec: if config.DEV_MODE: log.warning(r"/!\ DEV MODE ENABLED /!\\") else: continue c = cls(self) log.debug("Registering %s", cls) self.__adhoc_handler.register(c) self.__chat_commands_handler.register(c) def __exception_handler(self, loop: asyncio.AbstractEventLoop, context): """ Called when a task created by loop.create_task() raises an Exception :param loop: :param context: :return: """ log.debug("Context in the exception handler: %s", context) exc = context.get("exception") if exc is None: log.debug("No exception in this context: %s", context) elif isinstance(exc, SystemExit): log.debug("SystemExit called in an asyncio task") else: log.error("Crash in an asyncio task: %s", context) log.exception("Crash in task", exc_info=exc) self.has_crashed = True loop.stop() def __register_slixmpp_events(self): self.del_event_handler("presence_subscribe", self._handle_subscribe) self.del_event_handler("presence_unsubscribe", self._handle_unsubscribe) self.del_event_handler("presence_subscribed", self._handle_subscribed) self.del_event_handler("presence_unsubscribed", self._handle_unsubscribed) self.del_event_handler( "roster_subscription_request", self._handle_new_subscription ) self.del_event_handler("presence_probe", self._handle_probe) self.add_event_handler("session_start", self.__on_session_start) self.add_event_handler("disconnected", self.connect) def __register_slixmpp_api(self) -> None: self.plugin["xep_0231"].api.register(self.store.bob.get_bob, "get_bob") self.plugin["xep_0231"].api.register(self.store.bob.set_bob, "set_bob") self.plugin["xep_0231"].api.register(self.store.bob.del_bob, "del_bob") @property # type: ignore def jid(self): # Override to avoid slixmpp deprecation warnings. return self.boundjid async def __on_session_start(self, event): log.debug("Gateway session start: %s", event) # prevents XMPP clients from considering the gateway as an HTTP upload disco = self.plugin["xep_0030"] await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid) await self.plugin["xep_0115"].update_caps(jid=self.boundjid) if self.COMPONENT_AVATAR is not None: try: cached_avatar = await avatar_cache.convert_or_get(self.COMPONENT_AVATAR) except Exception as e: log.exception("Could not set the component avatar.", exc_info=e) cached_avatar = None else: assert cached_avatar is not None self.avatar_pk = cached_avatar.pk else: cached_avatar = None for user in self.store.users.get_all(): # TODO: before this, we should check if the user has removed us from their roster # while we were offline and trigger unregister from there. Presence probe does not seem # to work in this case, there must be another way. privileged entity could be used # as last resort. try: await self["xep_0100"].add_component_to_roster(user.jid) await self.__add_component_to_mds_whitelist(user.jid) except (IqError, IqTimeout) as e: # TODO: remove the user when this happens? or at least # this can happen when the user has unsubscribed from the XMPP server log.warning( "Error with user %s, not logging them automatically", user, exc_info=e, ) continue self.send_presence( pto=user.jid.bare, ptype="probe" ) # ensure we get all resources for user session = self.session_cls.from_user(user) session.create_task(self.login_wrap(session)) if cached_avatar is not None: await self.pubsub.broadcast_avatar( self.boundjid.bare, session.user_jid, cached_avatar ) log.info("Slidge has successfully started") async def __add_component_to_mds_whitelist(self, user_jid: JID): # Uses privileged entity to add ourselves to the whitelist of the PEP # MDS node so we receive MDS events iq_creation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set") iq_creation["pubsub"]["create"]["node"] = self["xep_0490"].stanza.NS try: await self["xep_0356"].send_privileged_iq(iq_creation) except PermissionError: log.warning( "IQ privileges not granted for pubsub namespace, we cannot " "create the MDS node of %s", user_jid, ) except PrivilegedIqError as exc: nested = exc.nested_error() # conflict this means the node already exists, we can ignore that if nested is not None and nested.condition != "conflict": log.exception( "Could not create the MDS node of %s", user_jid, exc_info=exc ) except Exception as e: log.exception( "Error while trying to create to the MDS node of %s", user_jid, exc_info=e, ) iq_affiliation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set") iq_affiliation["pubsub_owner"]["affiliations"]["node"] = self[ "xep_0490" ].stanza.NS aff = OwnerAffiliation() aff["jid"] = self.boundjid.bare aff["affiliation"] = "member" iq_affiliation["pubsub_owner"]["affiliations"].append(aff) try: await self["xep_0356"].send_privileged_iq(iq_affiliation) except PermissionError: log.warning( "IQ privileges not granted for pubsub#owner namespace, we cannot " "listen to the MDS events of %s", user_jid, ) except Exception as e: log.exception( "Error while trying to subscribe to the MDS node of %s", user_jid, exc_info=e, ) @timeit async def login_wrap(self, session: "BaseSession"): session.send_gateway_status("Logging in…", show="dnd") session.is_logging_in = True try: status = await session.login() except Exception as e: log.warning("Login problem for %s", session.user_jid, exc_info=e) log.exception(e) session.send_gateway_status(f"Could not login: {e}", show="busy") session.send_gateway_message( "You are not connected to this gateway! " f"Maybe this message will tell you why: {e}" ) session.logged = False return log.info("Login success for %s", session.user_jid) session.logged = True session.send_gateway_status("Syncing contacts…", show="dnd") await session.contacts._fill() if not (r := session.contacts.ready).done(): r.set_result(True) if self.GROUPS: session.send_gateway_status("Syncing groups…", show="dnd") await session.bookmarks.fill() if not (r := session.bookmarks.ready).done(): r.set_result(True) for c in session.contacts: # we need to receive presences directed at the contacts, in # order to send pubsub events for their +notify features self.send_presence(pfrom=c.jid, pto=session.user_jid.bare, ptype="probe") if status is None: session.send_gateway_status("Logged in", show="chat") else: session.send_gateway_status(status, show="chat") if session.user.preferences.get("sync_avatar", False): session.create_task(self.fetch_user_avatar(session)) else: self.xmpp.store.users.set_avatar_hash(session.user_pk, None) async def fetch_user_avatar(self, session: BaseSession): try: iq = await self.xmpp.plugin["xep_0060"].get_items( session.user_jid.bare, self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace, ifrom=self.boundjid.bare, ) except IqTimeout: self.log.warning("Iq timeout trying to fetch user avatar") return except IqError as e: self.log.debug("Iq error when trying to fetch user avatar: %s", e) if e.condition == "item-not-found": try: await session.on_avatar(None, None, None, None, None) except NotImplementedError: pass else: self.xmpp.store.users.set_avatar_hash(session.user_pk, None) return await self.__dispatcher.on_avatar_metadata_info( session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"] ) def _send( self, stanza: MessageOrPresenceTypeVar, **send_kwargs ) -> MessageOrPresenceTypeVar: stanza.set_from(self.boundjid.bare) if mto := send_kwargs.get("mto"): stanza.set_to(mto) stanza.send() return stanza def raise_if_not_allowed_jid(self, jid: JID): if not self.jid_validator.match(jid.bare): raise XMPPError( condition="not-allowed", text="Your account is not allowed to use this gateway.", ) def send_raw(self, data: Union[str, bytes]): # overridden from XMLStream to strip base64-encoded data from the logs # to make them more readable. if log.isEnabledFor(level=logging.DEBUG): if isinstance(data, str): stripped = copy(data) else: stripped = data.decode("utf-8") # there is probably a way to do that in a single RE, # but since it's only for debugging, the perf penalty # does not matter much for el in LOG_STRIP_ELEMENTS: stripped = re.sub( f"(<{el}.*?>)(.*)()", "\1[STRIPPED]\3", stripped, flags=re.DOTALL | re.IGNORECASE, ) log.debug("SEND: %s", stripped) if not self.transport: raise NotConnectedError() if isinstance(data, str): data = data.encode("utf-8") self.transport.write(data) def get_session_from_jid(self, j: JID): try: return self.session_cls.from_jid(j) except XMPPError: pass def exception(self, exception: Exception): # """ # Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception. # # Stop the event loop and exit on unhandled exception. # # The default :class:`slixmpp.basexmpp.BaseXMPP` behaviour is just to # log the exception, but we want to avoid undefined behaviour. # # :param exception: An unhandled :class:`Exception` object. # """ if isinstance(exception, IqError): iq = exception.iq log.error("%s: %s", iq["error"]["condition"], iq["error"]["text"]) log.warning("You should catch IqError exceptions") elif isinstance(exception, IqTimeout): iq = exception.iq log.error("Request timed out: %s", iq) log.warning("You should catch IqTimeout exceptions") elif isinstance(exception, SyntaxError): # Hide stream parsing errors that occur when the # stream is disconnected (they've been handled, we # don't need to make a mess in the logs). pass else: if exception: log.exception(exception) self.loop.stop() exit(1) def re_login(self, session: "BaseSession"): async def w(): session.cancel_all_tasks() await session.logout() await self.login_wrap(session) session.create_task(w()) async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq): self.raise_if_not_allowed_jid(iq.get_from()) reg = iq["register"] user = self.store.users.get_by_stanza(iq) log.debug("User found: %s", user) form = reg["form"] form.add_field( "FORM_TYPE", ftype="hidden", value="jabber:iq:register", ) form["title"] = f"Registration to '{self.COMPONENT_NAME}'" form["instructions"] = self.REGISTRATION_INSTRUCTIONS if user is not None: reg["registered"] = False form.add_field( "remove", label="Remove my registration", required=True, ftype="boolean", value=False, ) for field in self.REGISTRATION_FIELDS: if field.var in reg.interfaces: val = None if user is None else user.get(field.var) if val is None: reg.add_field(field.var) else: reg[field.var] = val reg["instructions"] = self.REGISTRATION_INSTRUCTIONS for field in self.REGISTRATION_FIELDS: form.add_field( field.var, label=field.label, required=field.required, ftype=field.type, options=field.options, value=field.value if user is None else user.get(field.var, field.value), ) reply = iq.reply() reply.set_payload(reg) return reply async def user_prevalidate( self, ifrom: JID, form_dict: dict[str, Optional[str]] ) -> Optional[Mapping]: # Pre validate a registration form using the content of self.REGISTRATION_FIELDS # before passing it to the plugin custom validation logic. for field in self.REGISTRATION_FIELDS: if field.required and not form_dict.get(field.var): raise ValueError(f"Missing field: '{field.label}'") return await self.validate(ifrom, form_dict) async def validate( self, user_jid: JID, registration_form: dict[str, Optional[str]] ) -> Optional[Mapping]: """ Validate a user's initial registration form. Should raise the appropriate :class:`slixmpp.exceptions.XMPPError` if the registration does not allow to continue the registration process. If :py:attr:`REGISTRATION_TYPE` is a :attr:`.RegistrationType.SINGLE_STEP_FORM`, this method should raise something if it wasn't possible to successfully log in to the legacy service with the registration form content. It is also used for other types of :py:attr:`REGISTRATION_TYPE` too, since the first step is always a form. If :attr:`.REGISTRATION_FIELDS` is an empty list (ie, it declares no :class:`.FormField`), the "form" is effectively a confirmation dialog displaying :attr:`.REGISTRATION_INSTRUCTIONS`. :param user_jid: JID of the user that has just registered :param registration_form: A dict where keys are the :attr:`.FormField.var` attributes of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable. This dict can be modified and will be accessible as the ``legacy_module_data`` of the :return : A dict that will be stored as the persistent "legacy_module_data" for this user. If you don't return anything here, the whole registration_form content will be stored. """ raise NotImplementedError async def validate_two_factor_code( self, user: GatewayUser, code: str ) -> Optional[dict]: """ Called when the user enters their 2FA code. Should raise the appropriate :class:`slixmpp.exceptions.XMPPError` if the login fails, and return successfully otherwise. Only used when :attr:`REGISTRATION_TYPE` is :attr:`.RegistrationType.TWO_FACTOR_CODE`. :param user: The :class:`.GatewayUser` whose registration is pending Use their :attr:`.GatewayUser.bare_jid` and/or :attr:`.registration_form` attributes to get what you need. :param code: The code they entered, either via "chatbot" message or adhoc command :return : A dict which keys and values will be added to the persistent "legacy_module_data" for this user. """ raise NotImplementedError async def get_qr_text(self, user: GatewayUser) -> str: """ This is where slidge gets the QR code content for the QR-based registration process. It will turn it into a QR code image and send it to the not-yet-fully-registered :class:`.GatewayUser`. Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is :attr:`.RegistrationType.QRCODE`. :param user: The :class:`.GatewayUser` whose registration is pending Use their :attr:`.GatewayUser.bare_jid` and/or :attr:`.registration_form` attributes to get what you need. """ raise NotImplementedError async def confirm_qr( self, user_bare_jid: str, exception: Optional[Exception] = None, legacy_data: Optional[dict] = None, ): """ This method is meant to be called to finalize QR code-based registration flows, once the legacy service confirms the QR flashing. Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is :attr:`.RegistrationType.QRCODE`. :param user_bare_jid: The bare JID of the almost-registered :class:`GatewayUser` instance :param exception: Optionally, an XMPPError to be raised to **not** confirm QR code flashing. :param legacy_data: dict which keys and values will be added to the persistent "legacy_module_data" for this user. """ fut = self.qr_pending_registrations[user_bare_jid] if exception is None: fut.set_result(legacy_data) else: fut.set_exception(exception) async def unregister_user(self, user: GatewayUser): self.send_presence( pshow="dnd", pstatus="You unregistered from this gateway.", pto=user.jid, ) await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid) await self.xmpp.session_cls.kill_by_jid(user.jid) async def unregister(self, user: GatewayUser): """ Optionally override this if you need to clean additional stuff after a user has been removed from the persistent user store. By default, this just calls :meth:`BaseSession.logout`. :param user: """ session = self.get_session_from_user(user) try: await session.logout() except NotImplementedError: pass async def input( self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs ) -> str: """ Request arbitrary user input using a simple chat message, and await the result. You shouldn't need to call this directly bust instead use :meth:`.BaseSession.input` to directly target a user. :param jid: The JID we want input from :param text: A prompt to display for the user :param mtype: Message type :return: The user's reply """ return await self.__chat_commands_handler.input(jid, text, mtype, **msg_kwargs) async def send_qr(self, text: str, **msg_kwargs): """ Sends a QR Code to a JID You shouldn't need to call directly bust instead use :meth:`.BaseSession.send_qr` to directly target a user. :param text: The text that will be converted to a QR Code :param msg_kwargs: Optional additional arguments to pass to :meth:`.BaseGateway.send_file`, such as the recipient of the QR, code """ qr = qrcode.make(text) with tempfile.NamedTemporaryFile( suffix=".png", delete=config.NO_UPLOAD_METHOD != "move" ) as f: qr.save(f.name) await self.send_file(f.name, **msg_kwargs) def shutdown(self) -> list[asyncio.Task]: # """ # Called by the slidge entrypoint on normal exit. # # Sends offline presences from all contacts of all user sessions and from # the gateway component itself. # No need to call this manually, :func:`slidge.__main__.main` should take care of it. # """ log.debug("Shutting down") tasks = [] for user in self.store.users.get_all(): tasks.append(self.session_cls.from_jid(user.jid).shutdown()) self.send_presence(ptype="unavailable", pto=user.jid) return tasks SLIXMPP_PLUGINS = [ "link_preview", # https://wiki.soprani.ca/CheogramApp/LinkPreviews "xep_0030", # Service discovery "xep_0045", # Multi-User Chat "xep_0050", # Adhoc commands "xep_0054", # VCard-temp (for MUC avatars) "xep_0055", # Jabber search "xep_0059", # Result Set Management "xep_0066", # Out of Band Data "xep_0071", # XHTML-IM (for stickers and custom emojis maybe later) "xep_0077", # In-band registration "xep_0084", # User Avatar "xep_0085", # Chat state notifications "xep_0100", # Gateway interaction "xep_0106", # JID Escaping "xep_0115", # Entity capabilities "xep_0122", # Data Forms Validation "xep_0153", # vCard-Based Avatars (for MUC avatars) "xep_0172", # User nickname "xep_0184", # Message Delivery Receipts "xep_0199", # XMPP Ping "xep_0221", # Data Forms Media Element "xep_0231", # Bits of Binary (for stickers and custom emojis maybe later) "xep_0249", # Direct MUC Invitations "xep_0264", # Jingle Content Thumbnails "xep_0280", # Carbons "xep_0292_provider", # VCard4 "xep_0308", # Last message correction "xep_0313", # Message Archive Management "xep_0317", # Hats "xep_0319", # Last User Interaction in Presence "xep_0333", # Chat markers "xep_0334", # Message Processing Hints "xep_0356", # Privileged Entity "xep_0363", # HTTP file upload "xep_0385", # Stateless in-line media sharing "xep_0402", # PEP Native Bookmarks "xep_0421", # Anonymous unique occupant identifiers for MUCs "xep_0424", # Message retraction "xep_0425", # Message moderation "xep_0444", # Message reactions "xep_0447", # Stateless File Sharing "xep_0461", # Message replies "xep_0469", # Bookmark Pinning "xep_0490", # Message Displayed Synchronization "xep_0492", # Chat Notification Settings ] LOG_STRIP_ELEMENTS = ["data", "binval"] log = logging.getLogger(__name__) slidge/slidge/core/mixins/000077500000000000000000000000001477703150600160455ustar00rootroot00000000000000slidge/slidge/core/mixins/__init__.py000066400000000000000000000011631477703150600201570ustar00rootroot00000000000000""" Mixins """ from typing import Optional from .avatar import AvatarMixin from .disco import ChatterDiscoMixin from .message import MessageCarbonMixin, MessageMixin from .presence import PresenceMixin class FullMixin(ChatterDiscoMixin, MessageMixin, PresenceMixin): pass class FullCarbonMixin(ChatterDiscoMixin, MessageCarbonMixin, PresenceMixin): pass class StoredAttributeMixin: def serialize_extra_attributes(self) -> Optional[dict]: return None def deserialize_extra_attributes(self, data: dict) -> None: pass __all__ = ("AvatarMixin", "FullCarbonMixin", "StoredAttributeMixin") slidge/slidge/core/mixins/attachment.py000066400000000000000000000473641477703150600205650ustar00rootroot00000000000000import base64 import functools import logging import os import re import shutil import stat import tempfile import warnings from datetime import datetime from itertools import chain from mimetypes import guess_extension, guess_type from pathlib import Path from typing import IO, AsyncIterator, Collection, Optional, Sequence, Union from urllib.parse import quote as urlquote from uuid import uuid4 from xml.etree import ElementTree as ET import thumbhash from PIL import Image, ImageOps from slixmpp import JID, Message from slixmpp.exceptions import IqError, IqTimeout from slixmpp.plugins.xep_0264.stanza import Thumbnail from slixmpp.plugins.xep_0363 import FileUploadError from slixmpp.plugins.xep_0447.stanza import StatelessFileSharing from ...db.avatar import avatar_cache from ...util.types import ( LegacyAttachment, LegacyMessageType, LegacyThreadType, MessageReference, ) from ...util.util import fix_suffix from .. import config from .message_text import TextMessageMixin class AttachmentMixin(TextMessageMixin): def __init__(self, *a, **kw): super().__init__(*a, **kw) self.__store = self.xmpp.store.attachments async def __upload( self, file_path: Path, file_name: Optional[str] = None, content_type: Optional[str] = None, ) -> str | None: if file_name and file_path.name != file_name: d = Path(tempfile.mkdtemp()) temp = d / file_name temp.symlink_to(file_path) file_path = temp else: d = None if config.UPLOAD_SERVICE: domain = None else: domain = re.sub(r"^.*?\.", "", self.xmpp.boundjid.bare) try: new_url = await self.xmpp.plugin["xep_0363"].upload_file( filename=file_path, content_type=content_type, ifrom=config.UPLOAD_REQUESTER or self.xmpp.boundjid, domain=JID(domain), ) except (FileUploadError, IqError, IqTimeout) as e: warnings.warn(f"Something is wrong with the upload service: {e!r}") return None finally: if d is not None: file_path.unlink() d.rmdir() return new_url @staticmethod async def __no_upload( file_path: Path, file_name: Optional[str] = None, legacy_file_id: Optional[Union[str, int]] = None, ): file_id = str(uuid4()) if legacy_file_id is None else str(legacy_file_id) assert config.NO_UPLOAD_PATH is not None assert config.NO_UPLOAD_URL_PREFIX is not None destination_dir = Path(config.NO_UPLOAD_PATH) / file_id if destination_dir.exists(): log.debug("Dest dir exists: %s", destination_dir) files = list(f for f in destination_dir.glob("**/*") if f.is_file()) if len(files) == 1: log.debug( "Found the legacy attachment '%s' at '%s'", legacy_file_id, files[0], ) name = files[0].name uu = files[0].parent.name # anti-obvious url trick, see below return files[0], "/".join([file_id, uu, name]) else: log.warning( ( "There are several or zero files in %s, " "slidge doesn't know which one to pick among %s. " "Removing the dir." ), destination_dir, files, ) shutil.rmtree(destination_dir) log.debug("Did not find a file in: %s", destination_dir) # let's use a UUID to avoid URLs being too obvious uu = str(uuid4()) destination_dir = destination_dir / uu destination_dir.mkdir(parents=True) name = file_name or file_path.name destination = destination_dir / name method = config.NO_UPLOAD_METHOD if method == "copy": shutil.copy2(file_path, destination) elif method == "hardlink": os.link(file_path, destination) elif method == "symlink": os.symlink(file_path, destination, target_is_directory=True) elif method == "move": shutil.move(file_path, destination) else: raise RuntimeError("No upload method not recognized", method) if config.NO_UPLOAD_FILE_READ_OTHERS: log.debug("Changing perms of %s", destination) destination.chmod(destination.stat().st_mode | stat.S_IROTH) uploaded_url = "/".join([file_id, uu, name]) return destination, uploaded_url async def __get_url( self, file_path: Optional[Path] = None, async_data_stream: Optional[AsyncIterator[bytes]] = None, data_stream: Optional[IO[bytes]] = None, data: Optional[bytes] = None, file_url: Optional[str] = None, file_name: Optional[str] = None, content_type: Optional[str] = None, legacy_file_id: Optional[Union[str, int]] = None, ) -> tuple[bool, Optional[Path], str]: if legacy_file_id: cache = self.__store.get_url(str(legacy_file_id)) if cache is not None: async with self.session.http.head(cache) as r: if r.status < 400: return False, None, cache else: self.__store.remove(str(legacy_file_id)) if file_url and config.USE_ATTACHMENT_ORIGINAL_URLS: return False, None, file_url if file_name and len(file_name) > config.ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH: log.debug("Trimming long filename: %s", file_name) base, ext = os.path.splitext(file_name) file_name = ( base[: config.ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH - len(ext)] + ext ) if file_path is None: if file_name is None: file_name = str(uuid4()) if content_type is not None: ext = guess_extension(content_type, strict=False) # type:ignore if ext is not None: file_name += ext temp_dir = Path(tempfile.mkdtemp()) file_path = temp_dir / file_name if file_url: async with self.session.http.get(file_url) as r: r.raise_for_status() with file_path.open("wb") as f: f.write(await r.read()) elif data_stream is not None: data = data_stream.read() if data is None: raise RuntimeError with file_path.open("wb") as f: f.write(data) elif async_data_stream is not None: # TODO: patch slixmpp to allow this as data source for # upload_file() so we don't even have to write anything # to disk. with file_path.open("wb") as f: async for chunk in async_data_stream: f.write(chunk) elif data is not None: with file_path.open("wb") as f: f.write(data) is_temp = not bool(config.NO_UPLOAD_PATH) else: is_temp = False if config.FIX_FILENAME_SUFFIX_MIME_TYPE: file_name = str(fix_suffix(file_path, content_type, file_name)) if config.NO_UPLOAD_PATH: local_path, new_url = await self.__no_upload( file_path, file_name, legacy_file_id ) new_url = (config.NO_UPLOAD_URL_PREFIX or "") + "/" + urlquote(new_url) else: local_path = file_path new_url = await self.__upload(file_path, file_name, content_type) if legacy_file_id and new_url is not None: self.__store.set_url(self.session.user_pk, str(legacy_file_id), new_url) return is_temp, local_path, new_url async def __set_sims( self, msg: Message, uploaded_url: str, path: Optional[Path], content_type: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, ) -> Thumbnail | None: cache = self.__store.get_sims(uploaded_url) if cache: ref = self.xmpp["xep_0372"].stanza.Reference(xml=ET.fromstring(cache)) msg.append(ref) if ref["sims"]["file"].get_plugin("thumbnail", check=True): return ref["sims"]["file"]["thumbnail"] else: return None if not path: return None ref = self.xmpp["xep_0385"].get_sims( path, [uploaded_url], content_type, caption ) if file_name: ref["sims"]["file"]["name"] = file_name thumbnail = None if content_type is not None and content_type.startswith("image"): try: h, x, y = await self.xmpp.loop.run_in_executor( avatar_cache._thread_pool, get_thumbhash, path ) except Exception as e: log.debug("Could not generate a thumbhash", exc_info=e) else: thumbnail = ref["sims"]["file"]["thumbnail"] thumbnail["width"] = x thumbnail["height"] = y thumbnail["media-type"] = "image/thumbhash" thumbnail["uri"] = "data:image/thumbhash;base64," + urlquote(h) self.__store.set_sims(uploaded_url, str(ref)) msg.append(ref) return thumbnail def __set_sfs( self, msg: Message, uploaded_url: str, path: Optional[Path], content_type: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, thumbnail: Optional[Thumbnail] = None, ): cache = self.__store.get_sfs(uploaded_url) if cache: msg.append(StatelessFileSharing(xml=ET.fromstring(cache))) return if not path: return sfs = self.xmpp["xep_0447"].get_sfs(path, [uploaded_url], content_type, caption) if file_name: sfs["file"]["name"] = file_name if thumbnail is not None: sfs["file"].append(thumbnail) self.__store.set_sfs(uploaded_url, str(sfs)) msg.append(sfs) def __send_url( self, msg: Message, legacy_msg_id: LegacyMessageType, uploaded_url: str, caption: Optional[str] = None, carbon=False, when: Optional[datetime] = None, correction=False, **kwargs, ) -> list[Message]: msg["oob"]["url"] = uploaded_url msg["body"] = uploaded_url if caption: m1 = self._send(msg, carbon=carbon, **kwargs) m2 = self.send_text( caption, legacy_msg_id=legacy_msg_id, when=when, carbon=carbon, correction=correction, **kwargs, ) return [m1, m2] if m2 else [m1] else: if correction: msg["replace"]["id"] = self._replace_id(legacy_msg_id) else: self._set_msg_id(msg, legacy_msg_id) return [self._send(msg, carbon=carbon, **kwargs)] async def send_file( self, file_path: Optional[Union[Path, str]] = None, legacy_msg_id: Optional[LegacyMessageType] = None, *, async_data_stream: Optional[AsyncIterator[bytes]] = None, data_stream: Optional[IO[bytes]] = None, data: Optional[bytes] = None, file_url: Optional[str] = None, file_name: Optional[str] = None, content_type: Optional[str] = None, reply_to: Optional[MessageReference] = None, when: Optional[datetime] = None, caption: Optional[str] = None, legacy_file_id: Optional[Union[str, int]] = None, thread: Optional[LegacyThreadType] = None, **kwargs, ) -> tuple[Optional[str], list[Message]]: """ Send a single file from this :term:`XMPP Entity`. :param file_path: Path to the attachment :param async_data_stream: Alternatively (and ideally) an AsyncIterator yielding bytes :param data_stream: Alternatively, a stream of bytes (such as a File object) :param data: Alternatively, a bytes object :param file_url: Alternatively, a URL :param file_name: How the file should be named. :param content_type: MIME type, inferred from filename if not given :param legacy_msg_id: If you want to be able to transport read markers from the gateway user to the legacy network, specify this :param reply_to: Quote another message (:xep:`0461`) :param when: when the file was sent, for a "delay" tag (:xep:`0203`) :param caption: an optional text that is linked to the file :param legacy_file_id: A unique identifier for the file on the legacy network. Plugins should try their best to provide it, to avoid duplicates. :param thread: """ carbon = kwargs.pop("carbon", False) mto = kwargs.pop("mto", None) store_multi = kwargs.pop("store_multi", True) correction = kwargs.get("correction", False) if correction and (original_xmpp_id := self._legacy_to_xmpp(legacy_msg_id)): xmpp_ids = self.xmpp.store.multi.get_xmpp_ids( self.session.user_pk, original_xmpp_id ) for xmpp_id in xmpp_ids: if xmpp_id == original_xmpp_id: continue self.retract(xmpp_id, thread) if reply_to is not None and reply_to.body: # We cannot have a "quote fallback" for attachments since most (all?) # XMPP clients will only treat a message as an attachment if the # body is the URL and nothing else. reply_to_for_attachment: MessageReference | None = MessageReference( reply_to.legacy_id, reply_to.author ) else: reply_to_for_attachment = reply_to msg = self._make_message( when=when, reply_to=reply_to_for_attachment, carbon=carbon, mto=mto, thread=thread, ) if content_type is None and (name := (file_name or file_path or file_url)): content_type, _ = guess_type(name) is_temp, local_path, new_url = await self.__get_url( Path(file_path) if file_path else None, async_data_stream, data_stream, data, file_url, file_name, content_type, legacy_file_id, ) if new_url is None: msg["body"] = ( "I tried to send a file, but something went wrong. " "Tell your slidge admin to check the logs." ) self._set_msg_id(msg, legacy_msg_id) return None, [self._send(msg, **kwargs)] thumbnail = await self.__set_sims( msg, new_url, local_path, content_type, caption, file_name ) self.__set_sfs( msg, new_url, local_path, content_type, caption, file_name, thumbnail ) if is_temp and isinstance(local_path, Path): local_path.unlink() local_path.parent.rmdir() msgs = self.__send_url( msg, legacy_msg_id, new_url, caption, carbon, when, **kwargs ) if store_multi: self.__store_multi(legacy_msg_id, msgs) return new_url, msgs def __send_body( self, body: Optional[str] = None, legacy_msg_id: Optional[LegacyMessageType] = None, reply_to: Optional[MessageReference] = None, when: Optional[datetime] = None, thread: Optional[LegacyThreadType] = None, **kwargs, ) -> Optional[Message]: if body: return self.send_text( body, legacy_msg_id, reply_to=reply_to, when=when, thread=thread, **kwargs, ) else: return None async def send_files( self, attachments: Collection[LegacyAttachment], legacy_msg_id: Optional[LegacyMessageType] = None, body: Optional[str] = None, *, reply_to: Optional[MessageReference] = None, when: Optional[datetime] = None, thread: Optional[LegacyThreadType] = None, body_first=False, correction=False, correction_event_id: Optional[LegacyMessageType] = None, **kwargs, ): # TODO: once the epic XEP-0385 vs XEP-0447 battle is over, pick # one and stop sending several attachments this way # we attach the legacy_message ID to the last message we send, because # we don't want several messages with the same ID (especially for MUC MAM) # TODO: refactor this so we limit the number of SQL calls, ie, if # the legacy file ID is known, only fetch the row once, and if it # is new, write it all in a single call if not attachments and not body: # ignoring empty message return send_body = functools.partial( self.__send_body, body=body, reply_to=reply_to, when=when, thread=thread, correction=correction, legacy_msg_id=legacy_msg_id, correction_event_id=correction_event_id, **kwargs, ) all_msgs = [] if body_first: all_msgs.append(send_body()) last_attachment_i = len(attachments) - 1 for i, attachment in enumerate(attachments): last = i == last_attachment_i if last and not body: legacy = legacy_msg_id else: legacy = None _url, msgs = await self.send_file( file_path=attachment.path, legacy_msg_id=legacy, file_url=attachment.url, data_stream=attachment.stream, data=attachment.data, reply_to=reply_to, when=when, thread=thread, file_name=attachment.name, content_type=attachment.content_type, legacy_file_id=attachment.legacy_file_id, caption=attachment.caption, store_multi=False, **kwargs, ) all_msgs.extend(msgs) if not body_first: all_msgs.append(send_body()) self.__store_multi(legacy_msg_id, all_msgs) def __store_multi( self, legacy_msg_id: Optional[LegacyMessageType], all_msgs: Sequence[Optional[Message]], ): if legacy_msg_id is None: return ids = [] for msg in all_msgs: if not msg: continue if stanza_id := msg.get_plugin("stanza_id", check=True): ids.append(stanza_id["id"]) else: ids.append(msg.get_id()) self.xmpp.store.multi.set_xmpp_ids( self.session.user_pk, str(legacy_msg_id), ids ) def get_thumbhash(path: Path) -> tuple[str, int, int]: with path.open("rb") as fp: img = Image.open(fp) width, height = img.size img = img.convert("RGBA") if width > 100 or height > 100: img.thumbnail((100, 100)) img = ImageOps.exif_transpose(img) rgba_2d = list(img.getdata()) rgba = list(chain(*rgba_2d)) ints = thumbhash.rgba_to_thumb_hash(img.width, img.height, rgba) return base64.b64encode(bytes(ints)).decode(), width, height log = logging.getLogger(__name__) slidge/slidge/core/mixins/avatar.py000066400000000000000000000177101477703150600177030ustar00rootroot00000000000000from asyncio import Task, create_task from hashlib import sha1 from pathlib import Path from typing import TYPE_CHECKING, Optional from PIL import UnidentifiedImageError from slixmpp import JID from ...db.avatar import CachedAvatar, avatar_cache from ...util.types import ( URL, AnyBaseSession, AvatarIdType, AvatarType, LegacyFileIdType, ) if TYPE_CHECKING: from ..pubsub import PepAvatar class AvatarMixin: """ Mixin for XMPP entities that have avatars that represent them. Both :py:class:`slidge.LegacyContact` and :py:class:`slidge.LegacyMUC` use :py:class:`.AvatarMixin`. """ jid: JID = NotImplemented session: AnyBaseSession = NotImplemented _avatar_bare_jid: bool = NotImplemented def __init__(self) -> None: super().__init__() self._set_avatar_task: Optional[Task] = None self.__broadcast_task: Optional[Task] = None self.__avatar_unique_id: Optional[AvatarIdType] = None self._avatar_pk: Optional[int] = None @property def __avatar_jid(self): return JID(self.jid.bare) if self._avatar_bare_jid else self.jid @property def avatar_id(self) -> Optional[AvatarIdType]: """ The unique ID of this entity's avatar. """ return self.__avatar_unique_id @property def avatar(self) -> Optional[AvatarIdType]: """ This property can be used to set the avatar, but :py:meth:`~.AvatarMixin.set_avatar()` should be preferred because you can provide a unique ID for the avatar for efficient caching. Setting this is OKish in case the avatar type is a URL or a local path that can act as a legacy ID. Python's ``property`` is abused here to maintain backwards compatibility, but when getting it you actually get the avatar legacy ID. """ return self.__avatar_unique_id @avatar.setter def avatar(self, a: Optional[AvatarType]): if self._set_avatar_task: self._set_avatar_task.cancel() self.session.log.debug("Setting avatar with property") self._set_avatar_task = self.session.xmpp.loop.create_task( self.set_avatar(a, None, blocking=True, cancel=False), name=f"Set avatar of {self} from property", ) @property def avatar_pk(self) -> int | None: return self._avatar_pk @staticmethod def __get_uid(a: Optional[AvatarType]) -> Optional[AvatarIdType]: if isinstance(a, str): return URL(a) elif isinstance(a, Path): return str(a) elif isinstance(a, bytes): return sha1(a).hexdigest() elif a is None: return None raise TypeError("Bad avatar", a) async def __set_avatar( self, a: Optional[AvatarType], uid: Optional[AvatarIdType], delete: bool ): self.__avatar_unique_id = uid if a is None: cached_avatar = None self._avatar_pk = None else: try: cached_avatar = await avatar_cache.convert_or_get(a) except UnidentifiedImageError: self.session.log.warning("%s is not a valid avatar", a) self._avatar_pk = None self.__avatar_unique_id = uid return except Exception as e: self.session.log.error("Failed to set avatar %s", a, exc_info=e) self._avatar_pk = None self.__avatar_unique_id = uid return self._avatar_pk = cached_avatar.pk if self.__should_pubsub_broadcast(): await self.session.xmpp.pubsub.broadcast_avatar( self.__avatar_jid, self.session.user_jid, cached_avatar ) if delete and isinstance(a, Path): a.unlink() self._post_avatar_update() def __should_pubsub_broadcast(self): return getattr(self, "is_friend", False) and getattr( self, "added_to_roster", False ) async def _no_change(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]): if a is None: return self.__avatar_unique_id is None if not self.__avatar_unique_id: return False if isinstance(uid, URL): if self.__avatar_unique_id != uid: return False return not await avatar_cache.url_modified(uid) return self.__avatar_unique_id == uid async def set_avatar( self, a: Optional[AvatarType], avatar_unique_id: Optional[LegacyFileIdType] = None, delete: bool = False, blocking=False, cancel=True, ) -> None: """ Set an avatar for this entity :param a: The avatar, in one of the types slidge supports :param avatar_unique_id: A globally unique ID for the avatar on the legacy network :param delete: If the avatar is provided as a Path, whether to delete it once used or not. :param blocking: Internal use by slidge for tests, do not use! :param cancel: Internal use by slidge, do not use! """ if avatar_unique_id is None and a is not None: avatar_unique_id = self.__get_uid(a) if await self._no_change(a, avatar_unique_id): return if cancel and self._set_avatar_task: self._set_avatar_task.cancel() awaitable = create_task( self.__set_avatar(a, avatar_unique_id, delete), name=f"Set pubsub avatar of {self}", ) if not self._set_avatar_task or self._set_avatar_task.done(): self._set_avatar_task = awaitable if blocking: await awaitable def get_cached_avatar(self) -> Optional["CachedAvatar"]: if self._avatar_pk is None: return None return avatar_cache.get_by_pk(self._avatar_pk) def get_avatar(self) -> Optional["PepAvatar"]: cached_avatar = self.get_cached_avatar() if cached_avatar is None: return None from ..pubsub import PepAvatar item = PepAvatar() item.set_avatar_from_cache(cached_avatar) return item def _post_avatar_update(self) -> None: return def __get_cached_avatar_id(self): i = self._get_cached_avatar_id() if i is None: return None return self.session.xmpp.AVATAR_ID_TYPE(i) def _get_cached_avatar_id(self) -> Optional[str]: raise NotImplementedError async def avatar_wrap_update_info(self): cached_id = self.__get_cached_avatar_id() self.__avatar_unique_id = cached_id try: await self.update_info() # type:ignore except NotImplementedError: return new_id = self.avatar if isinstance(new_id, URL) and not await avatar_cache.url_modified(new_id): return elif new_id != cached_id: # at this point it means that update_info set the avatar, and we don't # need to do anything else return if self.__should_pubsub_broadcast(): if new_id is None and cached_id is None: return if self._avatar_pk is not None: cached_avatar = avatar_cache.get_by_pk(self._avatar_pk) else: cached_avatar = None self.__broadcast_task = self.session.xmpp.loop.create_task( self.session.xmpp.pubsub.broadcast_avatar( self.__avatar_jid, self.session.user_jid, cached_avatar ) ) def _set_avatar_from_store(self, stored): if stored.avatar_id is None: return if stored.avatar is None: # seems to happen after avatar cleanup for some reason? self.__avatar_unique_id = None return self.__avatar_unique_id = ( stored.avatar.legacy_id if stored.avatar.legacy_id is not None else URL(stored.avatar.url) ) slidge/slidge/core/mixins/base.py000066400000000000000000000013541477703150600173340ustar00rootroot00000000000000from abc import ABCMeta from typing import TYPE_CHECKING from slixmpp import JID from ...util.types import MessageOrPresenceTypeVar if TYPE_CHECKING: from ..gateway import BaseGateway from ..session import BaseSession class MetaBase(ABCMeta): pass class Base: session: "BaseSession" = NotImplemented xmpp: "BaseGateway" = NotImplemented jid: JID = NotImplemented name: str = NotImplemented @property def user_jid(self): return self.session.user_jid @property def user_pk(self): return self.session.user_pk class BaseSender(Base): def _send( self, stanza: MessageOrPresenceTypeVar, **send_kwargs ) -> MessageOrPresenceTypeVar: raise NotImplementedError slidge/slidge/core/mixins/db.py000066400000000000000000000006721477703150600170110ustar00rootroot00000000000000from contextlib import contextmanager class UpdateInfoMixin: """ This mixin just adds a context manager that prevents commiting to the DB on every attribute change. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._updating_info = False @contextmanager def updating_info(self): self._updating_info = True yield self._updating_info = False slidge/slidge/core/mixins/disco.py000066400000000000000000000071261477703150600175260ustar00rootroot00000000000000from typing import Optional from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo from slixmpp.types import OptJid from .base import Base class BaseDiscoMixin(Base): DISCO_TYPE: str = NotImplemented DISCO_CATEGORY: str = NotImplemented DISCO_NAME: str = NotImplemented DISCO_LANG = None def _get_disco_name(self): if self.DISCO_NAME is NotImplemented: return self.xmpp.COMPONENT_NAME return self.DISCO_NAME or self.xmpp.COMPONENT_NAME def features(self): return [] async def extended_features(self) -> Optional[list[Form]]: return None async def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None): info = DiscoInfo() for feature in self.features(): info.add_feature(feature) info.add_identity( category=self.DISCO_CATEGORY, itype=self.DISCO_TYPE, name=self._get_disco_name(), lang=self.DISCO_LANG, ) if forms := await self.extended_features(): for form in forms: info.append(form) return info async def get_caps_ver(self, jid: OptJid = None, node: Optional[str] = None): info = await self.get_disco_info(jid, node) caps = self.xmpp.plugin["xep_0115"] ver = caps.generate_verstring(info, caps.hash) return ver class ChatterDiscoMixin(BaseDiscoMixin): AVATAR = True RECEIPTS = True MARKS = True CHAT_STATES = True UPLOAD = True CORRECTION = True REACTION = True RETRACTION = True REPLIES = True INVITATION_RECIPIENT = False DISCO_TYPE = "pc" DISCO_CATEGORY = "client" DISCO_NAME = "" def features(self): features = [] if self.CHAT_STATES: features.append("http://jabber.org/protocol/chatstates") if self.RECEIPTS: features.append("urn:xmpp:receipts") if self.CORRECTION: features.append("urn:xmpp:message-correct:0") if self.MARKS: features.append("urn:xmpp:chat-markers:0") if self.UPLOAD: features.append("jabber:x:oob") if self.REACTION: features.append("urn:xmpp:reactions:0") if self.RETRACTION: features.append("urn:xmpp:message-retract:0") if self.REPLIES: features.append("urn:xmpp:reply:0") if self.INVITATION_RECIPIENT: features.append("jabber:x:conference") features.append("urn:ietf:params:xml:ns:vcard-4.0") return features async def extended_features(self): f = getattr(self, "restricted_emoji_extended_feature", None) if f is None: return e = await f() if not e: return return [e] class ContactAccountDiscoMixin(BaseDiscoMixin): async def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None): if jid and jid.resource: return await super().get_disco_info() info = DiscoInfo() info.add_feature("http://jabber.org/protocol/pubsub") info.add_feature("http://jabber.org/protocol/pubsub#retrieve-items") info.add_feature("http://jabber.org/protocol/pubsub#subscribe") info.add_identity( category="account", itype="registered", name=self._get_disco_name(), lang=self.DISCO_LANG, ) info.add_identity( category="pubsub", itype="pep", name=self._get_disco_name(), lang=self.DISCO_LANG, ) return info slidge/slidge/core/mixins/lock.py000066400000000000000000000017171477703150600173550ustar00rootroot00000000000000import asyncio import logging from contextlib import asynccontextmanager from typing import Hashable class NamedLockMixin: def __init__(self, *a, **k): super().__init__(*a, **k) self.__locks = dict[Hashable, asyncio.Lock]() @asynccontextmanager async def lock(self, id_: Hashable): log.trace("getting %s", id_) # type:ignore locks = self.__locks if not locks.get(id_): locks[id_] = asyncio.Lock() try: async with locks[id_]: log.trace("acquired %s", id_) # type:ignore yield finally: log.trace("releasing %s", id_) # type:ignore waiters = locks[id_]._waiters # type:ignore if not waiters: del locks[id_] log.trace("erasing %s", id_) # type:ignore def get_lock(self, id_: Hashable): return self.__locks.get(id_) log = logging.getLogger(__name__) # type:ignore slidge/slidge/core/mixins/message.py000066400000000000000000000172461477703150600200550ustar00rootroot00000000000000import logging import uuid import warnings from typing import TYPE_CHECKING, Optional from slixmpp import Iq, Message from slixmpp.plugins.xep_0004 import Form from ...util.types import ChatState, LegacyMessageType, Marker from .attachment import AttachmentMixin from .message_maker import MessageMaker from .message_text import TextMessageMixin if TYPE_CHECKING: from ...group import LegacyMUC # this is for MDS PUBLISH_OPTIONS = Form() PUBLISH_OPTIONS["type"] = "submit" PUBLISH_OPTIONS.add_field( "FORM_TYPE", "hidden", value="http://jabber.org/protocol/pubsub#publish-options" ) PUBLISH_OPTIONS.add_field("pubsub#persist_items", value="true") PUBLISH_OPTIONS.add_field("pubsub#max_items", value="max") PUBLISH_OPTIONS.add_field("pubsub#send_last_published_item", value="never") PUBLISH_OPTIONS.add_field("pubsub#access_model", value="whitelist") class ChatStateMixin(MessageMaker): def __init__(self): super().__init__() self.__last_chat_state: Optional[ChatState] = None def _chat_state(self, state: ChatState, forced=False, **kwargs): carbon = kwargs.get("carbon", False) if carbon or (state == self.__last_chat_state and not forced): return self.__last_chat_state = state msg = self._make_message(state=state, hints={"no-store"}) self._send(msg, **kwargs) def active(self, **kwargs): """ Send an "active" chat state (:xep:`0085`) from this :term:`XMPP Entity`. """ self._chat_state("active", **kwargs) def composing(self, **kwargs): """ Send a "composing" (ie "typing notification") chat state (:xep:`0085`) from this :term:`XMPP Entity`. """ self._chat_state("composing", forced=True, **kwargs) def paused(self, **kwargs): """ Send a "paused" (ie "typing paused notification") chat state (:xep:`0085`) from this :term:`XMPP Entity`. """ self._chat_state("paused", **kwargs) def inactive(self, **kwargs): """ Send an "inactive" (ie "contact has not interacted with the chat session interface for an intermediate period of time") chat state (:xep:`0085`) from this :term:`XMPP Entity`. """ self._chat_state("inactive", **kwargs) def gone(self, **kwargs): """ Send a "gone" (ie "contact has not interacted with the chat session interface, system, or device for a relatively long period of time") chat state (:xep:`0085`) from this :term:`XMPP Entity`. """ self._chat_state("gone", **kwargs) class MarkerMixin(MessageMaker): is_group: bool = NotImplemented def _make_marker( self, legacy_msg_id: LegacyMessageType, marker: Marker, carbon=False ): msg = self._make_message(carbon=carbon) msg[marker]["id"] = self._legacy_to_xmpp(legacy_msg_id) return msg def ack(self, legacy_msg_id: LegacyMessageType, **kwargs): """ Send an "acknowledged" message marker (:xep:`0333`) from this :term:`XMPP Entity`. :param legacy_msg_id: The message this marker refers to """ self._send( self._make_marker( legacy_msg_id, "acknowledged", carbon=kwargs.get("carbon") ), **kwargs, ) def received(self, legacy_msg_id: LegacyMessageType, **kwargs): """ Send a "received" message marker (:xep:`0333`) from this :term:`XMPP Entity`. If called on a :class:`LegacyContact`, also send a delivery receipt marker (:xep:`0184`). :param legacy_msg_id: The message this marker refers to """ carbon = kwargs.get("carbon") if self.mtype == "chat": self._send( self.xmpp.delivery_receipt.make_ack( self._legacy_to_xmpp(legacy_msg_id), mfrom=self.jid, mto=self.user_jid, ) ) self._send( self._make_marker(legacy_msg_id, "received", carbon=carbon), **kwargs ) def displayed(self, legacy_msg_id: LegacyMessageType, **kwargs): """ Send a "displayed" message marker (:xep:`0333`) from this :term:`XMPP Entity`. :param legacy_msg_id: The message this marker refers to """ self._send( self._make_marker(legacy_msg_id, "displayed", carbon=kwargs.get("carbon")), **kwargs, ) if getattr(self, "is_user", False): self.session.create_task(self.__send_mds(legacy_msg_id)) async def __send_mds(self, legacy_msg_id: LegacyMessageType): # Send a MDS displayed marker on behalf of the user for a group chat if muc := getattr(self, "muc", None): muc_jid = muc.jid.bare else: # This is not implemented for 1:1 chat because it would rely on # storing the XMPP-server injected stanza-id, which we don't track # ATM. # In practice, MDS should mostly be useful for public group chats, # so it should not be an issue. # We'll see if we need to implement that later return xmpp_msg_id = self._legacy_to_xmpp(legacy_msg_id) iq = Iq(sto=self.user_jid.bare, sfrom=self.user_jid.bare, stype="set") iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0490"].stanza.NS iq["pubsub"]["publish"]["item"]["id"] = muc_jid displayed = self.xmpp["xep_0490"].stanza.Displayed() displayed["stanza_id"]["id"] = xmpp_msg_id displayed["stanza_id"]["by"] = muc_jid iq["pubsub"]["publish"]["item"]["payload"] = displayed iq["pubsub"]["publish_options"] = PUBLISH_OPTIONS try: await self.xmpp["xep_0356"].send_privileged_iq(iq) except Exception as e: self.session.log.debug("Could not MDS mark", exc_info=e) class ContentMessageMixin(AttachmentMixin, TextMessageMixin): pass class CarbonMessageMixin(ContentMessageMixin, MarkerMixin): def _privileged_send(self, msg: Message): i = msg.get_id() if i: self.session.ignore_messages.add(i) else: i = "slidge-carbon-" + str(uuid.uuid4()) msg.set_id(i) msg.del_origin_id() try: self.xmpp["xep_0356"].send_privileged_message(msg) except PermissionError: warnings.warn( "Slidge does not have privileges to send message on behalf of" " user.Refer to" " https://slidge.im/docs/slidge/main/admin/privilege.html" " for more info." ) class InviteMixin(MessageMaker): def invite_to( self, muc: "LegacyMUC", reason: Optional[str] = None, password: Optional[str] = None, **send_kwargs, ): """ Send an invitation to join a group (:xep:`0249`) from this :term:`XMPP Entity`. :param muc: the muc the user is invited to :param reason: a text explaining why the user should join this muc :param password: maybe this will make sense later? not sure :param send_kwargs: additional kwargs to be passed to _send() (internal use by slidge) """ msg = self._make_message(mtype="normal") msg["groupchat_invite"]["jid"] = muc.jid if reason: msg["groupchat_invite"]["reason"] = reason if password: msg["groupchat_invite"]["password"] = password self._send(msg, **send_kwargs) class MessageMixin(InviteMixin, ChatStateMixin, MarkerMixin, ContentMessageMixin): pass class MessageCarbonMixin(InviteMixin, ChatStateMixin, CarbonMessageMixin): pass log = logging.getLogger(__name__) slidge/slidge/core/mixins/message_maker.py000066400000000000000000000136111477703150600212240ustar00rootroot00000000000000import warnings from copy import copy from datetime import datetime, timezone from typing import TYPE_CHECKING, Iterable, Optional, cast from uuid import uuid4 from slixmpp import JID, Message from slixmpp.types import MessageTypes from ...db.models import GatewayUser from ...slixfix.link_preview.stanza import LinkPreview as LinkPreviewStanza from ...util.types import ( ChatState, LegacyMessageType, LinkPreview, MessageReference, ProcessingHint, ) from .. import config from .base import BaseSender if TYPE_CHECKING: from ...group.participant import LegacyParticipant class MessageMaker(BaseSender): mtype: MessageTypes = NotImplemented _can_send_carbon: bool = NotImplemented STRIP_SHORT_DELAY = False USE_STANZA_ID = False def _make_message( self, state: Optional[ChatState] = None, hints: Iterable[ProcessingHint] = (), legacy_msg_id: Optional[LegacyMessageType] = None, when: Optional[datetime] = None, reply_to: Optional[MessageReference] = None, carbon=False, link_previews: Optional[Iterable[LinkPreview]] = None, **kwargs, ): body = kwargs.pop("mbody", None) mfrom = kwargs.pop("mfrom", self.jid) mto = kwargs.pop("mto", None) thread = kwargs.pop("thread", None) if carbon and self._can_send_carbon: # the msg needs to have jabber:client as xmlns, so # we don't want to associate with the XML stream msg_cls = Message else: msg_cls = self.xmpp.Message # type:ignore msg = msg_cls( sfrom=mfrom, stype=kwargs.pop("mtype", None) or self.mtype, sto=mto, **kwargs, ) if body: msg["body"] = body state = "active" if thread: msg["thread"] = self.xmpp.store.sent.get_legacy_thread( self.user_pk, str(thread) ) or str(thread) if state: msg["chat_state"] = state for hint in hints: msg.enable(hint) self._set_msg_id(msg, legacy_msg_id) self._add_delay(msg, when) if link_previews: self._add_link_previews(msg, link_previews) if reply_to: self._add_reply_to(msg, reply_to) return msg def _set_msg_id( self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None ): if legacy_msg_id is not None: i = self._legacy_to_xmpp(legacy_msg_id) msg.set_id(i) if self.USE_STANZA_ID: msg["stanza_id"]["id"] = i msg["stanza_id"]["by"] = self.muc.jid # type: ignore elif self.USE_STANZA_ID: msg["stanza_id"]["id"] = str(uuid4()) msg["stanza_id"]["by"] = self.muc.jid # type: ignore def _legacy_to_xmpp(self, legacy_id: LegacyMessageType): return self.xmpp.store.sent.get_xmpp_id( self.session.user_pk, str(legacy_id) ) or self.session.legacy_to_xmpp_msg_id(legacy_id) def _add_delay(self, msg: Message, when: Optional[datetime]): if when: if when.tzinfo is None: when = when.astimezone(timezone.utc) if self.STRIP_SHORT_DELAY: delay = datetime.now().astimezone(timezone.utc) - when if delay < config.IGNORE_DELAY_THRESHOLD: return msg["delay"].set_stamp(when) msg["delay"].set_from(self.xmpp.boundjid.bare) def _add_reply_to(self, msg: Message, reply_to: MessageReference): xmpp_id = self._legacy_to_xmpp(reply_to.legacy_id) msg["reply"]["id"] = xmpp_id muc = getattr(self, "muc", None) if entity := reply_to.author: if entity == "user" or isinstance(entity, GatewayUser): if isinstance(entity, GatewayUser): warnings.warn( "Using a GatewayUser as the author of a " "MessageReference is deprecated. Use the string 'user' " "instead.", DeprecationWarning, ) if muc: jid = JID(muc.jid) jid.resource = fallback_nick = muc.user_nick msg["reply"]["to"] = jid else: msg["reply"]["to"] = self.session.user_jid # TODO: here we should use preferably use the PEP nick of the user # (but it doesn't matter much) fallback_nick = self.session.user_jid.user else: if muc: if hasattr(entity, "muc"): # TODO: accept a Contact here and use muc.get_participant_by_legacy_id() # a bit of work because right now this is a sync function entity = cast("LegacyParticipant", entity) fallback_nick = entity.nickname else: warnings.warn( "The author of a message reference in a MUC must be a" " Participant instance, not a Contact" ) fallback_nick = entity.name else: fallback_nick = entity.name msg["reply"]["to"] = entity.jid else: fallback_nick = None if fallback := reply_to.body: msg["reply"].add_quoted_fallback(fallback, fallback_nick) @staticmethod def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]): for preview in link_previews: element = LinkPreviewStanza() for i, name in enumerate(preview._fields): val = preview[i] if not val: continue element[name] = val msg.append(element) slidge/slidge/core/mixins/message_text.py000066400000000000000000000176401477703150600211170ustar00rootroot00000000000000import logging from datetime import datetime from typing import Iterable, Optional from ...util.types import ( LegacyMessageType, LegacyThreadType, LinkPreview, MessageReference, ProcessingHint, ) from .message_maker import MessageMaker class TextMessageMixin(MessageMaker): def __default_hints(self, hints: Optional[Iterable[ProcessingHint]] = None): if hints is not None: return hints elif self.mtype == "chat": return {"markable", "store"} elif self.mtype == "groupchat": return {"markable"} def _replace_id(self, legacy_msg_id: LegacyMessageType): if self.mtype == "groupchat": return self.xmpp.store.sent.get_group_xmpp_id( self.session.user_pk, str(legacy_msg_id) ) or self._legacy_to_xmpp(legacy_msg_id) else: return self._legacy_to_xmpp(legacy_msg_id) def send_text( self, body: str, legacy_msg_id: Optional[LegacyMessageType] = None, *, when: Optional[datetime] = None, reply_to: Optional[MessageReference] = None, thread: Optional[LegacyThreadType] = None, hints: Optional[Iterable[ProcessingHint]] = None, carbon=False, archive_only=False, correction=False, correction_event_id: Optional[LegacyMessageType] = None, link_previews: Optional[list[LinkPreview]] = None, **send_kwargs, ): """ Send a text message from this :term:`XMPP Entity`. :param body: Content of the message :param legacy_msg_id: If you want to be able to transport read markers from the gateway user to the legacy network, specify this :param when: when the message was sent, for a "delay" tag (:xep:`0203`) :param reply_to: Quote another message (:xep:`0461`) :param hints: :param thread: :param carbon: (only used if called on a :class:`LegacyContact`) Set this to ``True`` if this is actually a message sent **to** the :class:`LegacyContact` by the :term:`User`. Use this to synchronize outgoing history for legacy official apps. :param correction: whether this message is a correction or not :param correction_event_id: in the case where an ID is associated with the legacy 'correction event', specify it here to use it on the XMPP side. If not specified, a random ID will be used. :param link_previews: A little of sender (or server, or gateway)-generated previews of URLs linked in the body. :param archive_only: (only in groups) Do not send this message to user, but store it in the archive. Meant to be used during ``MUC.backfill()`` """ if carbon and not hasattr(self, "muc"): if not correction and self.xmpp.store.sent.was_sent_by_user( self.session.user_pk, str(legacy_msg_id) ): log.warning( "Carbon message for a message an XMPP has sent? This is a bug! %s", legacy_msg_id, ) return if hasattr(self, "muc") and not self.is_user: # type:ignore log.warning( "send_text() called with carbon=True on a participant who is not the user", legacy_msg_id, ) self.xmpp.store.sent.set_message( self.session.user_pk, str(legacy_msg_id), self.session.legacy_to_xmpp_msg_id(legacy_msg_id), ) hints = self.__default_hints(hints) msg = self._make_message( mbody=body, legacy_msg_id=correction_event_id if correction else legacy_msg_id, when=when, reply_to=reply_to, hints=hints or (), carbon=carbon, thread=thread, link_previews=link_previews, ) if correction: msg["replace"]["id"] = self._replace_id(legacy_msg_id) return self._send( msg, archive_only=archive_only, carbon=carbon, legacy_msg_id=legacy_msg_id, **send_kwargs, ) def correct( self, legacy_msg_id: LegacyMessageType, new_text: str, *, when: Optional[datetime] = None, reply_to: Optional[MessageReference] = None, thread: Optional[LegacyThreadType] = None, hints: Optional[Iterable[ProcessingHint]] = None, carbon=False, archive_only=False, correction_event_id: Optional[LegacyMessageType] = None, link_previews: Optional[list[LinkPreview]] = None, **send_kwargs, ): """ Modify a message that was previously sent by this :term:`XMPP Entity`. Uses last message correction (:xep:`0308`) :param new_text: New content of the message :param legacy_msg_id: The legacy message ID of the message to correct :param when: when the message was sent, for a "delay" tag (:xep:`0203`) :param reply_to: Quote another message (:xep:`0461`) :param hints: :param thread: :param carbon: (only in 1:1) Reflect a message sent to this ``Contact`` by the user. Use this to synchronize outgoing history for legacy official apps. :param archive_only: (only in groups) Do not send this message to user, but store it in the archive. Meant to be used during ``MUC.backfill()`` :param correction_event_id: in the case where an ID is associated with the legacy 'correction event', specify it here to use it on the XMPP side. If not specified, a random ID will be used. :param link_previews: A little of sender (or server, or gateway)-generated previews of URLs linked in the body. """ self.send_text( new_text, legacy_msg_id, when=when, reply_to=reply_to, hints=hints, carbon=carbon, thread=thread, correction=True, archive_only=archive_only, correction_event_id=correction_event_id, link_previews=link_previews, **send_kwargs, ) def react( self, legacy_msg_id: LegacyMessageType, emojis: Iterable[str] = (), thread: Optional[LegacyThreadType] = None, **kwargs, ): """ Send a reaction (:xep:`0444`) from this :term:`XMPP Entity`. :param legacy_msg_id: The message which the reaction refers to. :param emojis: An iterable of emojis used as reactions :param thread: """ msg = self._make_message( hints={"store"}, carbon=kwargs.get("carbon"), thread=thread ) xmpp_id = kwargs.pop("xmpp_id", None) if not xmpp_id: xmpp_id = self._legacy_to_xmpp(legacy_msg_id) self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=emojis) self._send(msg, **kwargs) def retract( self, legacy_msg_id: LegacyMessageType, thread: Optional[LegacyThreadType] = None, **kwargs, ): """ Send a message retraction (:XEP:`0424`) from this :term:`XMPP Entity`. :param legacy_msg_id: Legacy ID of the message to delete :param thread: """ msg = self._make_message( state=None, hints={"store"}, mbody=f"/me retracted the message {legacy_msg_id}", carbon=kwargs.get("carbon"), thread=thread, ) msg.enable("fallback") # namespace version mismatch between slidge and slixmpp, update me later msg["fallback"]["for"] = self.xmpp["xep_0424"].namespace[:-1] + "1" msg["retract"]["id"] = msg["replace"]["id"] = self._replace_id(legacy_msg_id) self._send(msg, **kwargs) log = logging.getLogger(__name__) slidge/slidge/core/mixins/presence.py000066400000000000000000000173231477703150600202310ustar00rootroot00000000000000import re from asyncio import Task, sleep from datetime import datetime, timedelta, timezone from typing import Optional from slixmpp.types import PresenceShows, PresenceTypes from ...util.types import CachedPresence from .. import config from .base import BaseSender class _NoChange(Exception): pass _FRIEND_REQUEST_PRESENCES = {"subscribe", "unsubscribe", "subscribed", "unsubscribed"} class PresenceMixin(BaseSender): _ONLY_SEND_PRESENCE_CHANGES = False contact_pk: Optional[int] = None def __init__(self, *a, **k): super().__init__(*a, **k) # FIXME: this should not be an attribute of this mixin to allow garbage # collection of instances self.__update_last_seen_fallback_task: Optional[Task] = None # this is only used when a presence is set during Contact.update_info(), # when the contact does not have a DB primary key yet, and is written # to DB at the end of update_info() self.cached_presence: Optional[CachedPresence] = None async def __update_last_seen_fallback(self): await sleep(3600 * 7) self.send_last_presence(force=True, no_cache_online=False) def _get_last_presence(self) -> Optional[CachedPresence]: if self.contact_pk is None: return None return self.xmpp.store.contacts.get_presence(self.contact_pk) def _store_last_presence(self, new: CachedPresence): if self.contact_pk is None: self.cached_presence = new return self.xmpp.store.contacts.set_presence(self.contact_pk, new) def _make_presence( self, *, last_seen: Optional[datetime] = None, force=False, bare=False, ptype: Optional[PresenceTypes] = None, pstatus: Optional[str] = None, pshow: Optional[PresenceShows] = None, ): if last_seen and last_seen.tzinfo is None: last_seen = last_seen.astimezone(timezone.utc) old = self._get_last_presence() if ptype not in _FRIEND_REQUEST_PRESENCES: new = CachedPresence( last_seen=last_seen, ptype=ptype, pstatus=pstatus, pshow=pshow ) if old != new: if hasattr(self, "muc") and ptype == "unavailable": if self.contact_pk is not None: self.xmpp.store.contacts.reset_presence(self.contact_pk) else: self._store_last_presence(new) if old and not force and self._ONLY_SEND_PRESENCE_CHANGES: if old == new: self.session.log.debug("Presence is the same as cached") raise _NoChange self.session.log.debug( "Presence is not the same as cached: %s vs %s", old, new ) p = self.xmpp.make_presence( pfrom=self.jid.bare if bare else self.jid, ptype=ptype, pshow=pshow, pstatus=pstatus, ) if last_seen: # it's ugly to check for the presence of this string, but a better fix is more work if config.LAST_SEEN_FALLBACK and not re.match( ".*Last seen .*", p["status"] ): last_seen_fallback, recent = get_last_seen_fallback(last_seen) if p["status"]: p["status"] = p["status"] + " -- " + last_seen_fallback else: p["status"] = last_seen_fallback if recent: # if less than a week, we use sth like 'Last seen: Monday, 8:05", # but if lasts more than a week, this is not very informative, so # we need to force resend an updated presence status if self.__update_last_seen_fallback_task: self.__update_last_seen_fallback_task.cancel() self.__update_last_seen_fallback_task = self.xmpp.loop.create_task( self.__update_last_seen_fallback() ) p["idle"]["since"] = last_seen return p def send_last_presence(self, force=False, no_cache_online=False): if (cache := self._get_last_presence()) is None: if force: if no_cache_online: self.online() else: self.offline() return self._send( self._make_presence( last_seen=cache.last_seen, force=True, ptype=cache.ptype, pshow=cache.pshow, pstatus=cache.pstatus, ) ) def online( self, status: Optional[str] = None, last_seen: Optional[datetime] = None, ): """ Send an "online" presence from this contact to the user. :param status: Arbitrary text, details of the status, eg: "Listening to Britney Spears" :param last_seen: For :xep:`0319` """ try: self._send(self._make_presence(pstatus=status, last_seen=last_seen)) except _NoChange: pass def away( self, status: Optional[str] = None, last_seen: Optional[datetime] = None, ): """ Send an "away" presence from this contact to the user. This is a global status, as opposed to :meth:`.LegacyContact.inactive` which concerns a specific conversation, ie a specific "chat window" :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism" :param last_seen: For :xep:`0319` """ try: self._send( self._make_presence(pstatus=status, pshow="away", last_seen=last_seen) ) except _NoChange: pass def extended_away( self, status: Optional[str] = None, last_seen: Optional[datetime] = None, ): """ Send an "extended away" presence from this contact to the user. This is a global status, as opposed to :meth:`.LegacyContact.inactive` which concerns a specific conversation, ie a specific "chat window" :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism" :param last_seen: For :xep:`0319` """ try: self._send( self._make_presence(pstatus=status, pshow="xa", last_seen=last_seen) ) except _NoChange: pass def busy( self, status: Optional[str] = None, last_seen: Optional[datetime] = None, ): """ Send a "busy" (ie, "dnd") presence from this contact to the user, :param status: eg: "Trying to make sense of XEP-0100" :param last_seen: For :xep:`0319` """ try: self._send( self._make_presence(pstatus=status, pshow="dnd", last_seen=last_seen) ) except _NoChange: pass def offline( self, status: Optional[str] = None, last_seen: Optional[datetime] = None, ): """ Send an "offline" presence from this contact to the user. :param status: eg: "Trying to make sense of XEP-0100" :param last_seen: For :xep:`0319` """ try: self._send( self._make_presence( pstatus=status, ptype="unavailable", last_seen=last_seen ) ) except _NoChange: pass def get_last_seen_fallback(last_seen: datetime): now = datetime.now(tz=timezone.utc) if now - last_seen < timedelta(days=7): return f"Last seen {last_seen:%A %H:%M GMT}", True else: return f"Last seen {last_seen:%b %-d %Y}", False slidge/slidge/core/mixins/recipient.py000066400000000000000000000024701477703150600204040ustar00rootroot00000000000000from typing import TYPE_CHECKING, Optional, Union from slixmpp.plugins.xep_0004 import Form from ...util.types import LegacyMessageType if TYPE_CHECKING: from ..gateway import BaseGateway class ReactionRecipientMixin: REACTIONS_SINGLE_EMOJI = False xmpp: "BaseGateway" = NotImplemented async def restricted_emoji_extended_feature(self): available = await self.available_emojis() if not self.REACTIONS_SINGLE_EMOJI and available is None: return None form = Form() form["type"] = "result" form.add_field("FORM_TYPE", "hidden", value="urn:xmpp:reactions:0:restrictions") if self.REACTIONS_SINGLE_EMOJI: form.add_field("max_reactions_per_user", value="1", type="number") if available: form.add_field("allowlist", value=list(available), type="text-multi") return form async def available_emojis( self, legacy_msg_id: Optional[LegacyMessageType] = None ) -> Optional[set[str]]: """ Override this to restrict the subset of reactions this recipient can handle. :return: A set of emojis or None if any emoji is allowed """ return None class ThreadRecipientMixin: async def create_thread(self, xmpp_id: str) -> Union[int, str]: return xmpp_id slidge/slidge/core/pubsub.py000066400000000000000000000275741477703150600164270ustar00rootroot00000000000000import logging from copy import copy from pathlib import Path from typing import TYPE_CHECKING, Optional, Union from slixmpp import ( JID, CoroutineCallback, Iq, Presence, StanzaPath, register_stanza_plugin, ) from slixmpp.exceptions import IqError, IqTimeout, XMPPError from slixmpp.plugins.base import BasePlugin, register_plugin from slixmpp.plugins.xep_0060.stanza import Event, EventItem, EventItems, Item from slixmpp.plugins.xep_0084 import Data as AvatarData from slixmpp.plugins.xep_0084 import MetaData as AvatarMetadata from slixmpp.plugins.xep_0172 import UserNick from slixmpp.plugins.xep_0292.stanza import VCard4 from slixmpp.types import JidStr, OptJidStr from ..db.avatar import CachedAvatar, avatar_cache from ..db.store import ContactStore, SlidgeStore from .mixins.lock import NamedLockMixin if TYPE_CHECKING: from slidge.core.gateway import BaseGateway from ..contact.contact import LegacyContact VCARD4_NAMESPACE = "urn:xmpp:vcard4" class PepItem: pass class PepAvatar(PepItem): store: SlidgeStore def __init__(self): self.metadata: Optional[AvatarMetadata] = None self.id: Optional[str] = None self._avatar_data_path: Optional[Path] = None @property def data(self) -> Optional[AvatarData]: if self._avatar_data_path is None: return None data = AvatarData() data.set_value(self._avatar_data_path.read_bytes()) return data def set_avatar_from_cache(self, cached_avatar: CachedAvatar): metadata = AvatarMetadata() self.id = cached_avatar.hash metadata.add_info( id=cached_avatar.hash, itype="image/png", ibytes=cached_avatar.path.stat().st_size, height=str(cached_avatar.height), width=str(cached_avatar.width), ) self.metadata = metadata self._avatar_data_path = cached_avatar.path class PepNick(PepItem): contact_store: ContactStore def __init__(self, nick: Optional[str] = None): nickname = UserNick() if nick is not None: nickname["nick"] = nick self.nick = nickname self.__nick_str = nick class PubSubComponent(NamedLockMixin, BasePlugin): xmpp: "BaseGateway" name = "pubsub" description = "Pubsub component" dependencies = { "xep_0030", "xep_0060", "xep_0115", "xep_0163", } default_config = {"component_name": None} component_name: str def __init__(self, *a, **kw): super(PubSubComponent, self).__init__(*a, **kw) register_stanza_plugin(EventItem, UserNick) def plugin_init(self): self.xmpp.register_handler( CoroutineCallback( "pubsub_get_avatar_data", StanzaPath(f"iq@type=get/pubsub/items@node={AvatarData.namespace}"), self._get_avatar_data, # type:ignore ) ) self.xmpp.register_handler( CoroutineCallback( "pubsub_get_avatar_metadata", StanzaPath(f"iq@type=get/pubsub/items@node={AvatarMetadata.namespace}"), self._get_avatar_metadata, # type:ignore ) ) self.xmpp.register_handler( CoroutineCallback( "pubsub_get_vcard", StanzaPath(f"iq@type=get/pubsub/items@node={VCARD4_NAMESPACE}"), self._get_vcard, # type:ignore ) ) disco = self.xmpp.plugin["xep_0030"] disco.add_identity("pubsub", "pep", self.component_name) disco.add_identity("account", "registered", self.component_name) disco.add_feature("http://jabber.org/protocol/pubsub#event") disco.add_feature("http://jabber.org/protocol/pubsub#retrieve-items") disco.add_feature("http://jabber.org/protocol/pubsub#persistent-items") async def __get_features(self, presence: Presence) -> list[str]: from_ = presence.get_from() ver_string = presence["caps"]["ver"] if ver_string: info = await self.xmpp.plugin["xep_0115"].get_caps(from_) else: info = None if info is None: async with self.lock(from_): try: iq = await self.xmpp.plugin["xep_0030"].get_info(from_) except (IqError, IqTimeout): log.debug("Could get disco#info of %s, ignoring", from_) return [] info = iq["disco_info"] return info["features"] async def on_presence_available( self, p: Presence, contact: Optional["LegacyContact"] ): if p.get_plugin("muc_join", check=True) is not None: log.debug("Ignoring MUC presence here") return to = p.get_to() if to != self.xmpp.boundjid.bare: # we don't want to push anything for contacts that are not in the user's roster if contact is None or not contact.is_friend: return from_ = p.get_from() features = await self.__get_features(p) if AvatarMetadata.namespace + "+notify" in features: try: pep_avatar = await self._get_authorized_avatar(p, contact) except XMPPError: pass else: if pep_avatar.metadata is not None: await self.__broadcast( data=pep_avatar.metadata, from_=p.get_to().bare, to=from_, id=pep_avatar.metadata["info"]["id"], ) if UserNick.namespace + "+notify" in features: try: pep_nick = await self._get_authorized_nick(p, contact) except XMPPError: pass else: await self.__broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_) if contact is not None and VCARD4_NAMESPACE + "+notify" in features: await self.broadcast_vcard_event( p.get_to(), from_, await contact.get_vcard() ) async def broadcast_vcard_event(self, from_: JID, to: JID, vcard: VCard4 | None): item = Item() item.namespace = VCARD4_NAMESPACE item["id"] = "current" # vcard: VCard4 = await self.xmpp["xep_0292_provider"].get_vcard(from_, to) # The vcard content should NOT be in this event according to the spec: # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224 # but movim expects it to be here, and I guess it does not hurt log.debug("Broadcast vcard4 event: %s", vcard) await self.__broadcast( data=vcard, from_=JID(from_).bare, to=to, id="current", node=VCARD4_NAMESPACE, ) async def __get_contact(self, stanza: Union[Iq, Presence]): session = self.xmpp.get_session_from_stanza(stanza) return await session.contacts.by_jid(stanza.get_to()) async def _get_authorized_avatar( self, stanza: Union[Iq, Presence], contact: Optional["LegacyContact"] = None ) -> PepAvatar: if stanza.get_to() == self.xmpp.boundjid.bare: item = PepAvatar() if hasattr(self.xmpp, "avatar_pk"): item.set_avatar_from_cache(avatar_cache.get_by_pk(self.xmpp.avatar_pk)) return item if contact is None: contact = await self.__get_contact(stanza) item = PepAvatar() if contact.avatar_pk is not None: stored = avatar_cache.get_by_pk(contact.avatar_pk) assert stored is not None item.set_avatar_from_cache(stored) return item async def _get_authorized_nick( self, stanza: Union[Iq, Presence], contact: Optional["LegacyContact"] = None ) -> PepNick: if stanza.get_to() == self.xmpp.boundjid.bare: return PepNick(self.xmpp.COMPONENT_NAME) if contact is None: contact = await self.__get_contact(stanza) if contact.name is not None: return PepNick(contact.name) else: return PepNick() def __reply_with( self, iq: Iq, content: AvatarData | AvatarMetadata | None, item_id: str | None ) -> None: requested_items = iq["pubsub"]["items"] if len(requested_items) == 0: self._reply_with_payload(iq, content, item_id) else: for item in requested_items: if item["id"] == item_id: self._reply_with_payload(iq, content, item_id) return else: raise XMPPError("item-not-found") async def _get_avatar_data(self, iq: Iq): pep_avatar = await self._get_authorized_avatar(iq) self.__reply_with(iq, pep_avatar.data, pep_avatar.id) async def _get_avatar_metadata(self, iq: Iq): pep_avatar = await self._get_authorized_avatar(iq) self.__reply_with(iq, pep_avatar.metadata, pep_avatar.id) async def _get_vcard(self, iq: Iq): # this is not the proper way that clients should retrieve VCards, but # gajim does it this way. # https://xmpp.org/extensions/xep-0292.html#sect-idm45669698174224 session = self.xmpp.get_session_from_stanza(iq) contact = await session.contacts.by_jid(iq.get_to()) vcard = await contact.get_vcard() if vcard is None: raise XMPPError("item-not-found") self._reply_with_payload(iq, vcard, "current", VCARD4_NAMESPACE) @staticmethod def _reply_with_payload( iq: Iq, payload: Optional[Union[AvatarMetadata, AvatarData, VCard4]], id_: Optional[str], namespace: Optional[str] = None, ): result = iq.reply() item = Item() if payload: item.set_payload(payload.xml) item["id"] = id_ result["pubsub"]["items"]["node"] = ( namespace if namespace else payload.namespace ) result["pubsub"]["items"].append(item) result.send() async def __broadcast(self, data, from_: JidStr, to: OptJidStr = None, **kwargs): from_ = JID(from_) if from_ != self.xmpp.boundjid.bare and to is not None: to = JID(to) session = self.xmpp.get_session_from_jid(to) if session is None: return await session.ready item = EventItem() if data: item.set_payload(data.xml) for k, v in kwargs.items(): item[k] = v items = EventItems() items.append(item) items["node"] = kwargs.get("node") or data.namespace event = Event() event.append(items) msg = self.xmpp.Message() msg.set_type("headline") msg.set_from(from_) msg.append(event) if to is None: for u in self.xmpp.store.users.get_all(): new_msg = copy(msg) new_msg.set_to(u.jid.bare) new_msg.send() else: msg.set_to(to) msg.send() async def broadcast_avatar( self, from_: JidStr, to: JidStr, cached_avatar: Optional[CachedAvatar] ) -> None: if cached_avatar is None: await self.__broadcast(AvatarMetadata(), from_, to) else: pep_avatar = PepAvatar() pep_avatar.set_avatar_from_cache(cached_avatar) assert pep_avatar.metadata is not None await self.__broadcast( pep_avatar.metadata, from_, to, id=pep_avatar.metadata["info"]["id"] ) def broadcast_nick( self, user_jid: JID, jid: JidStr, nick: Optional[str] = None, ): jid = JID(jid) nickname = PepNick(nick) log.debug("New nickname: %s", nickname.nick) self.xmpp.loop.create_task(self.__broadcast(nickname.nick, jid, user_jid.bare)) log = logging.getLogger(__name__) register_plugin(PubSubComponent) slidge/slidge/core/session.py000066400000000000000000000674271477703150600166130ustar00rootroot00000000000000import asyncio import logging from typing import ( TYPE_CHECKING, Any, Generic, Iterable, NamedTuple, Optional, Union, cast, ) import aiohttp from slixmpp import JID, Message from slixmpp.exceptions import XMPPError from slixmpp.types import PresenceShows from ..command import SearchResult from ..contact import LegacyContact, LegacyRoster from ..db.models import GatewayUser from ..group.bookmarks import LegacyBookmarks from ..group.room import LegacyMUC from ..util import ABCSubclassableOnceAtMost from ..util.types import ( LegacyGroupIdType, LegacyMessageType, LegacyThreadType, LinkPreview, Mention, PseudoPresenceShow, RecipientType, ResourceDict, Sticker, ) from ..util.util import deprecated, noop_coro if TYPE_CHECKING: from ..group.participant import LegacyParticipant from ..util.types import Sender from .gateway import BaseGateway class CachedPresence(NamedTuple): status: Optional[str] show: Optional[str] kwargs: dict[str, Any] class BaseSession( Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost ): """ The session of a registered :term:`User`. Represents a gateway user logged in to the legacy network and performing actions. Will be instantiated automatically on slidge startup for each registered user, or upon registration for new (validated) users. Must be subclassed for a functional :term:`Legacy Module`. """ """ Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent by the user. This also applies to 'carboned' messages, ie, messages sent by the user from the official client of a legacy network. """ xmpp: "BaseGateway" """ The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not session-specific. """ MESSAGE_IDS_ARE_THREAD_IDS = False """ Set this to True if the legacy service uses message IDs as thread IDs, eg Mattermost, where you can only 'create a thread' by replying to the message, in which case the message ID is also a thread ID (and all messages are potential threads). """ SPECIAL_MSG_ID_PREFIX: Optional[str] = None """ If you set this, XMPP message IDs starting with this won't be converted to legacy ID, but passed as is to :meth:`.on_react`, and usual checks for emoji restriction won't be applied. This can be used to implement voting in polls in a hacky way. """ def __init__(self, user: GatewayUser): self.log = logging.getLogger(user.jid.bare) self.user_jid = user.jid self.user_pk = user.id self.ignore_messages = set[str]() self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self) self.is_logging_in = False self._logged = False self.__reset_ready() self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()( self ) self.thread_creation_lock = asyncio.Lock() self.__cached_presence: Optional[CachedPresence] = None self.__tasks = set[asyncio.Task]() @property def user(self) -> GatewayUser: return self.xmpp.store.users.get(self.user_jid) # type:ignore @property def http(self) -> aiohttp.ClientSession: return self.xmpp.http def __remove_task(self, fut): self.log.debug("Removing fut %s", fut) self.__tasks.remove(fut) def create_task(self, coro) -> asyncio.Task: task = self.xmpp.loop.create_task(coro) self.__tasks.add(task) self.log.debug("Creating task %s", task) task.add_done_callback(lambda _: self.__remove_task(task)) return task def cancel_all_tasks(self): for task in self.__tasks: task.cancel() async def login(self) -> Optional[str]: """ Logs in the gateway user to the legacy network. Triggered when the gateway start and on user registration. It is recommended that this function returns once the user is logged in, so if you need to await forever (for instance to listen to incoming events), it's a good idea to wrap your listener in an asyncio.Task. :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'" """ raise NotImplementedError async def logout(self): """ Logs out the gateway user from the legacy network. Called on gateway shutdown. """ raise NotImplementedError async def on_text( self, chat: RecipientType, text: str, *, reply_to_msg_id: Optional[LegacyMessageType] = None, reply_to_fallback_text: Optional[str] = None, reply_to: Optional["Sender"] = None, thread: Optional[LegacyThreadType] = None, link_previews: Iterable[LinkPreview] = (), mentions: Optional[list[Mention]] = None, ) -> Optional[LegacyMessageType]: """ Triggered when the user sends a text message from XMPP to a bridged entity, e.g. to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com`` Override this and implement sending a message to the legacy network in this method. :param text: Content of the message :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat, :class:`.MUC` instance for groups. :param reply_to_msg_id: A legacy message ID if the message references (quotes) another message (:xep:`0461`) :param reply_to_fallback_text: Content of the quoted text. Not necessarily set by XMPP clients :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for 1:1 chat, :class:`LegacyParticipant` instance for groups. If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None. :param link_previews: A list of sender-generated link previews. At the time of writing, only `Cheogram `_ supports it. :param mentions: (only for groups) A list of Contacts mentioned by their nicknames. :param thread: :return: An ID of some sort that can be used later to ack and mark the message as read by the user """ raise NotImplementedError send_text = deprecated("BaseSession.send_text", on_text) async def on_file( self, chat: RecipientType, url: str, *, http_response: aiohttp.ClientResponse, reply_to_msg_id: Optional[LegacyMessageType] = None, reply_to_fallback_text: Optional[str] = None, reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None, thread: Optional[LegacyThreadType] = None, ) -> Optional[LegacyMessageType]: """ Triggered when the user sends a file using HTTP Upload (:xep:`0363`) :param url: URL of the file :param chat: See :meth:`.BaseSession.on_text` :param http_response: The HTTP GET response object on the URL :param reply_to_msg_id: See :meth:`.BaseSession.on_text` :param reply_to_fallback_text: See :meth:`.BaseSession.on_text` :param reply_to: See :meth:`.BaseSession.on_text` :param thread: :return: An ID of some sort that can be used later to ack and mark the message as read by the user """ raise NotImplementedError send_file = deprecated("BaseSession.send_file", on_file) async def on_sticker( self, chat: RecipientType, sticker: Sticker, *, reply_to_msg_id: Optional[LegacyMessageType] = None, reply_to_fallback_text: Optional[str] = None, reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None, thread: Optional[LegacyThreadType] = None, ) -> Optional[LegacyMessageType]: """ Triggered when the user sends a file using HTTP Upload (:xep:`0363`) :param chat: See :meth:`.BaseSession.on_text` :param sticker: The sticker sent by the user. :param reply_to_msg_id: See :meth:`.BaseSession.on_text` :param reply_to_fallback_text: See :meth:`.BaseSession.on_text` :param reply_to: See :meth:`.BaseSession.on_text` :param thread: :return: An ID of some sort that can be used later to ack and mark the message as read by the user """ raise NotImplementedError async def on_active( self, chat: RecipientType, thread: Optional[LegacyThreadType] = None ): """ Triggered when the user sends an 'active' chat state (:xep:`0085`) :param chat: See :meth:`.BaseSession.on_text` :param thread: """ raise NotImplementedError active = deprecated("BaseSession.active", on_active) async def on_inactive( self, chat: RecipientType, thread: Optional[LegacyThreadType] = None ): """ Triggered when the user sends an 'inactive' chat state (:xep:`0085`) :param chat: See :meth:`.BaseSession.on_text` :param thread: """ raise NotImplementedError inactive = deprecated("BaseSession.inactive", on_inactive) async def on_composing( self, chat: RecipientType, thread: Optional[LegacyThreadType] = None ): """ Triggered when the user starts typing in a legacy chat (:xep:`0085`) :param chat: See :meth:`.BaseSession.on_text` :param thread: """ raise NotImplementedError composing = deprecated("BaseSession.composing", on_composing) async def on_paused( self, chat: RecipientType, thread: Optional[LegacyThreadType] = None ): """ Triggered when the user pauses typing in a legacy chat (:xep:`0085`) :param chat: See :meth:`.BaseSession.on_text` :param thread: """ raise NotImplementedError paused = deprecated("BaseSession.paused", on_paused) async def on_displayed( self, chat: RecipientType, legacy_msg_id: LegacyMessageType, thread: Optional[LegacyThreadType] = None, ): """ Triggered when the user reads a message in a legacy chat. (:xep:`0333`) This is only possible if a valid ``legacy_msg_id`` was passed when transmitting a message from a legacy chat to the user, eg in :meth:`slidge.contact.LegacyContact.send_text` or :meth:`slidge.group.LegacyParticipant.send_text`. :param chat: See :meth:`.BaseSession.on_text` :param legacy_msg_id: Identifier of the message/ :param thread: """ raise NotImplementedError displayed = deprecated("BaseSession.displayed", on_displayed) async def on_correct( self, chat: RecipientType, text: str, legacy_msg_id: LegacyMessageType, *, thread: Optional[LegacyThreadType] = None, link_previews: Iterable[LinkPreview] = (), mentions: Optional[list[Mention]] = None, ) -> Optional[LegacyMessageType]: """ Triggered when the user corrects a message using :xep:`0308` This is only possible if a valid ``legacy_msg_id`` was returned by :meth:`.on_text`. :param chat: See :meth:`.BaseSession.on_text` :param text: The new text :param legacy_msg_id: Identifier of the edited message :param thread: :param link_previews: A list of sender-generated link previews. At the time of writing, only `Cheogram `_ supports it. :param mentions: (only for groups) A list of Contacts mentioned by their nicknames. """ raise NotImplementedError correct = deprecated("BaseSession.correct", on_correct) async def on_react( self, chat: RecipientType, legacy_msg_id: LegacyMessageType, emojis: list[str], thread: Optional[LegacyThreadType] = None, ): """ Triggered when the user sends message reactions (:xep:`0444`). :param chat: See :meth:`.BaseSession.on_text` :param thread: :param legacy_msg_id: ID of the message the user reacts to :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``. An empty string means "no reaction", ie, remove all reactions if any were present before """ raise NotImplementedError react = deprecated("BaseSession.react", on_react) async def on_retract( self, chat: RecipientType, legacy_msg_id: LegacyMessageType, thread: Optional[LegacyThreadType] = None, ): """ Triggered when the user retracts (:xep:`0424`) a message. :param chat: See :meth:`.BaseSession.on_text` :param thread: :param legacy_msg_id: Legacy ID of the retracted message """ raise NotImplementedError retract = deprecated("BaseSession.retract", on_retract) async def on_presence( self, resource: str, show: PseudoPresenceShow, status: str, resources: dict[str, ResourceDict], merged_resource: Optional[ResourceDict], ): """ Called when the gateway component receives a presence, ie, when one of the user's clients goes online of offline, or changes its status. :param resource: The XMPP client identifier, arbitrary string. :param show: The presence ````, if available. If the resource is just 'available' without any ```` element, this is an empty str. :param status: A status message, like a deeply profound quote, eg, "Roses are red, violets are blue, [INSERT JOKE]". :param resources: A summary of all the resources for this user. :param merged_resource: A global presence for the user account, following rules described in :meth:`merge_resources` """ raise NotImplementedError presence = deprecated("BaseSession.presence", on_presence) async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]: """ Triggered when the user uses Jabber Search (:xep:`0055`) on the component Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS` :param form_values: search query, defined for a specific plugin by overriding in :attr:`.BaseGateway.SEARCH_FIELDS` :return: """ raise NotImplementedError search = deprecated("BaseSession.search", on_search) async def on_avatar( self, bytes_: Optional[bytes], hash_: Optional[str], type_: Optional[str], width: Optional[int], height: Optional[int], ) -> None: """ Triggered when the user uses modifies their avatar via :xep:`0084`. :param bytes_: The data of the avatar. According to the spec, this should always be a PNG, but some implementations do not respect that. If `None` it means the user has unpublished their avatar. :param hash_: The SHA1 hash of the avatar data. This is an identifier of the avatar. :param type_: The MIME type of the avatar. :param width: The width of the avatar image. :param height: The height of the avatar image. """ raise NotImplementedError async def on_moderate( self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str] ): """ Triggered when the user attempts to retract a message that was sent in a MUC using :xep:`0425`. If retraction is not possible, this should raise the appropriate XMPPError with a human-readable message. NB: the legacy module is responsible for calling :method:`LegacyParticipant.moderate` when this is successful, because slidge will acknowledge the moderation IQ, but will not send the moderation message from the MUC automatically. :param muc: The MUC in which the message was sent :param legacy_msg_id: The legacy ID of the message to be retracted :param reason: Optionally, a reason for the moderation, given by the user-moderator. """ raise NotImplementedError async def on_create_group( self, name: str, contacts: list[LegacyContact] ) -> LegacyGroupIdType: """ Triggered when the user request the creation of a group via the dedicated :term:`Command`. :param name: Name of the group :param contacts: list of contacts that should be members of the group """ raise NotImplementedError async def on_invitation( self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str] ): """ Triggered when the user invites a :term:`Contact` to a legacy MUC via :xep:`0249`. The default implementation calls :meth:`LegacyMUC.on_set_affiliation` with the 'member' affiliation. Override if you want to customize this behaviour. :param contact: The invitee :param muc: The group :param reason: Optionally, a reason """ await muc.on_set_affiliation(contact, "member", reason, None) async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType): """ Triggered when the user leaves a group via the dedicated slidge command or the :xep:`0077` ```` mechanism. This should be interpreted as definitely leaving the group. :param muc_legacy_id: The legacy ID of the group to leave """ raise NotImplementedError def __reset_ready(self): self.ready = self.xmpp.loop.create_future() @property def logged(self): return self._logged @logged.setter def logged(self, v: bool): self.is_logging_in = False self._logged = v if self.ready.done(): if v: return self.__reset_ready() self.shutdown(logout=False) else: if v: self.ready.set_result(True) def __repr__(self): return f"" def shutdown(self, logout=True) -> asyncio.Task: for m in self.bookmarks: m.shutdown() for c in self.contacts: c.offline() if logout: return self.xmpp.loop.create_task(self.logout()) else: return self.xmpp.loop.create_task(noop_coro()) @staticmethod def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str: """ Convert a legacy msg ID to a valid XMPP msg ID. Needed for read marks, retractions and message corrections. The default implementation just converts the legacy ID to a :class:`str`, but this should be overridden in case some characters needs to be escaped, or to add some additional, :term:`legacy network -specific logic. :param legacy_msg_id: :return: A string that is usable as an XMPP stanza ID """ return str(legacy_msg_id) legacy_msg_id_to_xmpp_msg_id = staticmethod( deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id) ) @staticmethod def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType: """ Convert a legacy XMPP ID to a valid XMPP msg ID. Needed for read marks and message corrections. The default implementation just converts the legacy ID to a :class:`str`, but this should be overridden in case some characters needs to be escaped, or to add some additional, :term:`legacy network -specific logic. The default implementation is an identity function. :param i: The XMPP stanza ID :return: An ID that can be used to identify a message on the legacy network """ return cast(LegacyMessageType, i) xmpp_msg_id_to_legacy_msg_id = staticmethod( deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id) ) def raise_if_not_logged(self): if not self.logged: raise XMPPError( "internal-server-error", text="You are not logged to the legacy network", ) @classmethod def _from_user_or_none(cls, user): if user is None: log.debug("user not found", stack_info=True) raise XMPPError(text="User not found", condition="subscription-required") session = _sessions.get(user.jid.bare) if session is None: _sessions[user.jid.bare] = session = cls(user) return session @classmethod def from_user(cls, user): return cls._from_user_or_none(user) @classmethod def from_stanza(cls, s) -> "BaseSession": # """ # Get a user's :class:`.LegacySession` using the "from" field of a stanza # # Meant to be called from :class:`BaseGateway` only. # # :param s: # :return: # """ return cls.from_jid(s.get_from()) @classmethod def from_jid(cls, jid: JID) -> "BaseSession": # """ # Get a user's :class:`.LegacySession` using its jid # # Meant to be called from :class:`BaseGateway` only. # # :param jid: # :return: # """ session = _sessions.get(jid.bare) if session is not None: return session user = cls.xmpp.store.users.get(jid) return cls._from_user_or_none(user) @classmethod async def kill_by_jid(cls, jid: JID): # """ # Terminate a user session. # # Meant to be called from :class:`BaseGateway` only. # # :param jid: # :return: # """ log.debug("Killing session of %s", jid) for user_jid, session in _sessions.items(): if user_jid == jid.bare: break else: log.debug("Did not find a session for %s", jid) return for c in session.contacts: c.unsubscribe() for m in session.bookmarks: m.shutdown() user = cls.xmpp.store.users.get(jid) if user is None: log.warning("User not found during unregistration") return await cls.xmpp.unregister(user) cls.xmpp.store.users.delete(user.jid) del _sessions[user.jid.bare] del user del session def __ack(self, msg: Message): if not self.xmpp.PROPER_RECEIPTS: self.xmpp.delivery_receipt.ack(msg) def send_gateway_status( self, status: Optional[str] = None, show=Optional[PresenceShows], **kwargs, ): """ Send a presence from the gateway to the user. Can be used to indicate the user session status, ie "SMS code required", "connected", … :param status: A status message :param show: Presence stanza 'show' element. I suggest using "dnd" to show that the gateway is not fully functional """ self.__cached_presence = CachedPresence(status, show, kwargs) self.xmpp.send_presence( pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs ) def send_cached_presence(self, to: JID): if not self.__cached_presence: self.xmpp.send_presence(pto=to, ptype="unavailable") return self.xmpp.send_presence( pto=to, pstatus=self.__cached_presence.status, pshow=self.__cached_presence.show, **self.__cached_presence.kwargs, ) def send_gateway_message(self, text: str, **msg_kwargs): """ Send a message from the gateway component to the user. Can be used to indicate the user session status, ie "SMS code required", "connected", … :param text: A text """ self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs) def send_gateway_invite( self, muc: LegacyMUC, reason: Optional[str] = None, password: Optional[str] = None, ): """ Send an invitation to join a MUC, emanating from the gateway component. :param muc: :param reason: :param password: """ self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid) async def input(self, text: str, **msg_kwargs): """ Request user input via direct messages from the gateway component. Wraps call to :meth:`.BaseSession.input` :param text: The prompt to send to the user :param msg_kwargs: Extra attributes :return: """ return await self.xmpp.input(self.user_jid, text, **msg_kwargs) async def send_qr(self, text: str): """ Sends a QR code generated from 'text' via HTTP Upload and send the URL to ``self.user`` :param text: Text to encode as a QR code """ await self.xmpp.send_qr(text, mto=self.user_jid) def re_login(self): # Logout then re-login # # No reason to override this self.xmpp.re_login(self) async def get_contact_or_group_or_participant(self, jid: JID, create=True): if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None: return contact if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None: return await self.__get_muc_or_participant(muc, jid) else: muc = None if not create: return None try: return await self.contacts.by_jid(jid) except XMPPError: if muc is None: try: muc = await self.bookmarks.by_jid(jid) except XMPPError: return return await self.__get_muc_or_participant(muc, jid) @staticmethod async def __get_muc_or_participant(muc: LegacyMUC, jid: JID): if nick := jid.resource: try: return await muc.get_participant( nick, raise_if_not_found=True, fill_first=True ) except XMPPError: return None return muc async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10): # """ # Wait until session, contacts and bookmarks are ready # # (slidge internal use) # # :param timeout: # :return: # """ try: await asyncio.wait_for(asyncio.shield(self.ready), timeout) await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout) await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout) except asyncio.TimeoutError: raise XMPPError( "recipient-unavailable", "Legacy session is not fully initialized, retry later", ) def legacy_module_data_update(self, data: dict): with self.xmpp.store.session(): user = self.user user.legacy_module_data.update(data) self.xmpp.store.users.update(user) def legacy_module_data_set(self, data: dict): with self.xmpp.store.session(): user = self.user user.legacy_module_data = data self.xmpp.store.users.update(user) def legacy_module_data_clear(self): with self.xmpp.store.session(): user = self.user user.legacy_module_data.clear() self.xmpp.store.users.update(user) # keys = user.jid.bare _sessions: dict[str, BaseSession] = {} log = logging.getLogger(__name__) slidge/slidge/db/000077500000000000000000000000001477703150600141735ustar00rootroot00000000000000slidge/slidge/db/__init__.py000066400000000000000000000001511477703150600163010ustar00rootroot00000000000000from .models import GatewayUser from .store import SlidgeStore __all__ = ("GatewayUser", "SlidgeStore") slidge/slidge/db/alembic/000077500000000000000000000000001477703150600155675ustar00rootroot00000000000000slidge/slidge/db/alembic/__init__.py000066400000000000000000000000001477703150600176660ustar00rootroot00000000000000slidge/slidge/db/alembic/env.py000066400000000000000000000031521477703150600167320ustar00rootroot00000000000000from alembic import context from slidge import global_config from slidge.db.meta import Base, get_engine config = context.config target_metadata = Base.metadata def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output. """ url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, render_as_batch=True, ) with context.begin_transaction(): context.run_migrations() def run_migrations_online() -> None: """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ try: # in prod connectable = get_engine(global_config.DB_URL) except AttributeError: # during dev, to generate migrations connectable = get_engine("sqlite+pysqlite:///dev/slidge.sqlite") with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata, render_as_batch=True, ) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() slidge/slidge/db/alembic/script.py.mako000066400000000000000000000011731477703150600203750ustar00rootroot00000000000000"""${message} Revision ID: ${up_revision} Revises: ${down_revision | comma,n} Create Date: ${create_date} """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa ${imports if imports else ""} # revision identifiers, used by Alembic. revision: str = ${repr(up_revision)} down_revision: Union[str, None] = ${repr(down_revision)} branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: ${upgrades if upgrades else "pass"} def downgrade() -> None: ${downgrades if downgrades else "pass"} slidge/slidge/db/alembic/versions/000077500000000000000000000000001477703150600174375ustar00rootroot00000000000000slidge/slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py000066400000000000000000000016101477703150600310710ustar00rootroot00000000000000"""Add Participant.nickname_no_illegal Revision ID: 04cf35e3cf85 Revises: 15b0bd83407a Create Date: 2025-02-22 06:57:45.491326 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "04cf35e3cf85" down_revision: Union[str, None] = "15b0bd83407a" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("participant", schema=None) as batch_op: batch_op.add_column( sa.Column("nickname_no_illegal", sa.String(), nullable=True) ) # ### end Alembic commands ### def downgrade() -> None: with op.batch_alter_table("participant", schema=None) as batch_op: batch_op.drop_column("nickname_no_illegal") slidge/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py000066400000000000000000000020111477703150600301560ustar00rootroot00000000000000"""Add n_participants attributes to Room Should have been part of another commit, but I messed up some rebase Revision ID: 09f27f098baa Revises: 29f5280c61aa Create Date: 2024-07-11 10:54:21.155871 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "09f27f098baa" down_revision: Union[str, None] = "29f5280c61aa" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("n_participants", sa.Integer(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_column("n_participants") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py000066400000000000000000000055571477703150600322310ustar00rootroot00000000000000"""Remove bogus unique constraints on room table Revision ID: 15b0bd83407a Revises: 45c24cc73c91 Create Date: 2024-08-28 06:57:25.022994 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op import slidge.db.meta # revision identifiers, used by Alembic. revision: str = "15b0bd83407a" down_revision: Union[str, None] = "45c24cc73c91" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None meta = sa.MetaData() room_table = sa.Table( "room", meta, sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=False), sa.Column("jid", slidge.db.meta.JIDType(), nullable=False), sa.Column("avatar_id", sa.Integer(), nullable=True), sa.Column("name", sa.String(), nullable=True), sa.Column("description", sa.String(), nullable=True), sa.Column("subject", sa.String(), nullable=True), sa.Column("subject_date", sa.DateTime(), nullable=True), sa.Column("subject_setter", sa.String(), nullable=True), sa.Column("n_participants", sa.Integer(), nullable=True), sa.Column( "muc_type", sa.Enum("GROUP", "CHANNEL", "CHANNEL_NON_ANONYMOUS", name="muctype"), nullable=True, ), sa.Column("user_nick", sa.String(), nullable=True), sa.Column("user_resources", sa.String(), nullable=True), sa.Column("participants_filled", sa.Boolean(), nullable=False), sa.Column("history_filled", sa.Boolean(), nullable=False), sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True), sa.Column("updated", sa.Boolean(), nullable=False), sa.Column("avatar_legacy_id", sa.String(), nullable=True), sa.ForeignKeyConstraint( ["avatar_id"], ["avatar.id"], ), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), ) def upgrade() -> None: if op.get_bind().engine.name == "postgresql": return with op.batch_alter_table( "room", schema=None, # without copy_from, the newly created table keeps the constraints # we actually want to ditch. copy_from=room_table, ) as batch_op: batch_op.create_unique_constraint( "uq_room_user_account_id_jid", ["user_account_id", "jid"] ) batch_op.create_unique_constraint( "uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"] ) def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique") batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py000066400000000000000000000020741477703150600310360ustar00rootroot00000000000000"""Store contacts caps verstring in DB Revision ID: 2461390c0af2 Revises: 2b1f45ab7379 Create Date: 2024-07-20 08:00:11.675735 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "2461390c0af2" down_revision: Union[str, None] = "2b1f45ab7379" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.add_column(sa.Column("caps_ver_bare", sa.String(), nullable=True)) batch_op.add_column(sa.Column("caps_ver", sa.String(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.drop_column("caps_ver") batch_op.drop_column("caps_ver_bare") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py000066400000000000000000000021561477703150600276130ustar00rootroot00000000000000"""Store subject setter in Room Revision ID: 29f5280c61aa Revises: 8d2ced764698 Create Date: 2024-07-10 13:09:25.181594 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "29f5280c61aa" down_revision: Union[str, None] = "8d2ced764698" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("subject_setter_id", sa.Integer(), nullable=True)) # we give this constraint a name a workaround for # https://github.com/sqlalchemy/alembic/issues/1195 batch_op.create_foreign_key( "subject_setter_id_foreign_key", "participant", ["subject_setter_id"], ["id"], ) def downgrade() -> None: with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_constraint("subject_setter_id_foreign_key", type_="foreignkey") batch_op.drop_column("subject_setter_id") slidge/slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py000066400000000000000000000025171477703150600314720ustar00rootroot00000000000000"""Store room subject setter by nickname Revision ID: 2b1f45ab7379 Revises: c4a8ec35a0e8 Create Date: 2024-07-20 00:14:36.882689 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "2b1f45ab7379" down_revision: Union[str, None] = "c4a8ec35a0e8" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("subject_setter", sa.String(), nullable=True)) batch_op.drop_constraint("subject_setter_id_foreign_key", type_="foreignkey") batch_op.drop_column("subject_setter_id") # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("subject_setter_id", sa.INTEGER(), nullable=True)) batch_op.create_foreign_key( "subject_setter_id_foreign_key", "participant", ["subject_setter_id"], ["id"], ) batch_op.drop_column("subject_setter") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py000066400000000000000000000025621477703150600264750ustar00rootroot00000000000000"""Add Contact.client_type Revision ID: 3071e0fa69d4 Revises: abba1ae0edb3 Create Date: 2024-07-30 23:12:49.345593 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "3071e0fa69d4" down_revision: Union[str, None] = "abba1ae0edb3" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.add_column( sa.Column( "client_type", sa.Enum( "bot", "console", "game", "handheld", "pc", "phone", "sms", "tablet", "web", native_enum=False, ), nullable=False, server_default=sa.literal("pc"), ) ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.drop_column("client_type") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/45c24cc73c91_add_bob.py000066400000000000000000000023411477703150600232010ustar00rootroot00000000000000"""Add BoB Revision ID: 45c24cc73c91 Revises: 3071e0fa69d4 Create Date: 2024-08-01 22:30:07.073935 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "45c24cc73c91" down_revision: Union[str, None] = "3071e0fa69d4" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( "bob", sa.Column("id", sa.Integer(), nullable=False), sa.Column("file_name", sa.String(), nullable=False), sa.Column("sha_1", sa.String(), nullable=False), sa.Column("sha_256", sa.String(), nullable=False), sa.Column("sha_512", sa.String(), nullable=False), sa.Column("content_type", sa.String(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("sha_1"), sa.UniqueConstraint("sha_256"), sa.UniqueConstraint("sha_512"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table("bob") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py000066400000000000000000000036711477703150600303770ustar00rootroot00000000000000"""Lift room legacy ID constraint Revision ID: 5bd48bfdffa2 Revises: b64b1a793483 Create Date: 2024-07-24 10:29:23.467851 Broken; fixed by "Remove bogus unique constraints on room table", rev 15b0bd83407a. """ import logging from typing import Sequence, Union from alembic import op from slidge.db.models import Room # revision identifiers, used by Alembic. revision: str = "5bd48bfdffa2" down_revision: Union[str, None] = "b64b1a793483" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: try: with op.batch_alter_table( "room", schema=None, # without copy_from, the newly created table keeps the constraints # we actually want to ditch. # LATER EDIT: this actually does not work, I should have copied the # schema in here. copy_from=Room.__table__, # type:ignore ) as batch_op: batch_op.create_unique_constraint( "uq_room_user_account_id_jid", ["user_account_id", "jid"] ) batch_op.create_unique_constraint( "uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"] ) except Exception: # This only works when upgrading rev by rev because I messed up. It # wouldn't be necessary if the constraint was named in the first place, # cf https://alembic.sqlalchemy.org/en/latest/naming.html # This is fixed by rev 15b0bd83407a log.info("Skipping") pass def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique") batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique") # ### end Alembic commands ### log = logging.getLogger(__name__) slidge/slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py000066400000000000000000000026251477703150600263430ustar00rootroot00000000000000"""Add MUC.history_filled Also drop caps_ver_bare column that should never have been added. Revision ID: 82a4af84b679 Revises: 2461390c0af2 Create Date: 2024-07-22 07:01:05.352737 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "82a4af84b679" down_revision: Union[str, None] = "2461390c0af2" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.drop_column("caps_ver_bare") with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column( sa.Column( "history_filled", sa.Boolean(), nullable=False, server_default=sa.False_(), # manually added ) ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_column("history_filled") with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.add_column(sa.Column("caps_ver_bare", sa.VARCHAR(), nullable=True)) # ### end Alembic commands ### slidge/slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py000066400000000000000000000022721477703150600305270ustar00rootroot00000000000000"""Add vcard content to contact table Revision ID: 8b993243a536 Revises: 5bd48bfdffa2 Create Date: 2024-07-24 07:02:47.770894 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "8b993243a536" down_revision: Union[str, None] = "5bd48bfdffa2" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.add_column(sa.Column("vcard", sa.String(), nullable=True)) batch_op.add_column( sa.Column( "vcard_fetched", sa.Boolean(), nullable=False, server_default=sa.sql.true(), ) ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.drop_column("vcard_fetched") batch_op.drop_column("vcard") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py000066400000000000000000000114021477703150600320000ustar00rootroot00000000000000"""Rely on DB to store contacts, rooms and participants Revision ID: 8d2ced764698 Revises: b33993e87db3 Create Date: 2024-07-08 14:39:47.022088 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op import slidge.db.meta # revision identifiers, used by Alembic. revision: str = "8d2ced764698" down_revision: Union[str, None] = "b33993e87db3" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "hat", sa.Column("id", sa.Integer(), nullable=False), sa.Column("title", sa.String(), nullable=False), sa.Column("uri", sa.String(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("title", "uri"), ) op.create_table( "contact_sent", sa.Column("id", sa.Integer(), nullable=False), sa.Column("contact_id", sa.Integer(), nullable=False), sa.Column("msg_id", sa.String(), nullable=False), sa.ForeignKeyConstraint( ["contact_id"], ["contact.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("contact_id", "msg_id"), ) op.create_table( "participant", sa.Column("id", sa.Integer(), nullable=False), sa.Column("room_id", sa.Integer(), nullable=False), sa.Column("contact_id", sa.Integer(), nullable=True), sa.Column("is_user", sa.Boolean(), nullable=False), sa.Column( "affiliation", sa.Enum("outcast", "member", "admin", "owner", "none", native_enum=False), nullable=False, ), sa.Column( "role", sa.Enum("moderator", "participant", "visitor", "none", native_enum=False), nullable=False, ), sa.Column("presence_sent", sa.Boolean(), nullable=False), sa.Column("resource", sa.String(), nullable=True), sa.Column("nickname", sa.String(), nullable=True), sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True), sa.ForeignKeyConstraint( ["contact_id"], ["contact.id"], ), sa.ForeignKeyConstraint( ["room_id"], ["room.id"], ), sa.PrimaryKeyConstraint("id"), ) op.create_table( "participant_hats", sa.Column("participant_id", sa.Integer(), nullable=False), sa.Column("hat_id", sa.Integer(), nullable=False), sa.ForeignKeyConstraint( ["hat_id"], ["hat.id"], ), sa.ForeignKeyConstraint( ["participant_id"], ["participant.id"], ), sa.PrimaryKeyConstraint("participant_id", "hat_id"), ) op.add_column("contact", sa.Column("is_friend", sa.Boolean(), nullable=False)) op.add_column("contact", sa.Column("added_to_roster", sa.Boolean(), nullable=False)) op.add_column( "contact", sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True), ) op.add_column("contact", sa.Column("updated", sa.Boolean(), nullable=False)) op.add_column("room", sa.Column("description", sa.String(), nullable=True)) op.add_column("room", sa.Column("subject", sa.String(), nullable=True)) op.add_column("room", sa.Column("subject_date", sa.DateTime(), nullable=True)) if op.get_bind().engine.name == "postgresql": op.execute( "CREATE TYPE muctype AS ENUM ('GROUP', 'CHANNEL', 'CHANNEL_NON_ANONYMOUS')" ) op.add_column( "room", sa.Column( "muc_type", sa.Enum("GROUP", "CHANNEL", "CHANNEL_NON_ANONYMOUS", name="muctype"), nullable=True, ), ) op.add_column("room", sa.Column("user_resources", sa.String(), nullable=True)) op.add_column( "room", sa.Column("participants_filled", sa.Boolean(), nullable=False) ) op.add_column( "room", sa.Column("extra_attributes", slidge.db.meta.JSONEncodedDict(), nullable=True), ) op.add_column("room", sa.Column("updated", sa.Boolean(), nullable=False)) def downgrade() -> None: op.drop_column("room", "updated") op.drop_column("room", "extra_attributes") op.drop_column("room", "participants_filled") op.drop_column("room", "user_resources") op.drop_column("room", "muc_type") op.drop_column("room", "subject_date") op.drop_column("room", "subject") op.drop_column("room", "description") op.drop_column("contact", "updated") op.drop_column("contact", "extra_attributes") op.drop_column("contact", "added_to_roster") op.drop_column("contact", "is_friend") op.drop_table("participant_hats") op.drop_table("participant") op.drop_table("contact_sent") op.drop_table("hat") slidge/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py000066400000000000000000000025571477703150600243420ustar00rootroot00000000000000"""DB Creation Including a migration from the user_store shelf Revision ID: aa9d82a7f6ef Revises: Create Date: 2024-04-17 20:57:01.357041 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op import slidge.db.meta # revision identifiers, used by Alembic. revision: str = "aa9d82a7f6ef" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### accounts = op.create_table( "user_account", sa.Column("id", sa.Integer(), nullable=False), sa.Column("jid", slidge.db.meta.JIDType(), nullable=False), sa.Column( "registration_date", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False, ), sa.Column( "legacy_module_data", slidge.db.meta.JSONEncodedDict(), nullable=False ), sa.Column("preferences", slidge.db.meta.JSONEncodedDict(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("jid"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table("user_account") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py000066400000000000000000000053331477703150600320610ustar00rootroot00000000000000"""Store avatar legacy ID in the Contact and Room table Revision ID: abba1ae0edb3 Revises: 8b993243a536 Create Date: 2024-07-29 15:44:41.557388 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from slidge.db.models import Contact, Room # revision identifiers, used by Alembic. revision: str = "abba1ae0edb3" down_revision: Union[str, None] = "8b993243a536" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() room_avatars = conn.execute( sa.text( "select room.id, avatar.legacy_id from room join avatar on room.avatar_id = avatar.id" ) ).all() contact_avatars = conn.execute( sa.text( "select contact.id, avatar.legacy_id from contact join avatar on contact.avatar_id = avatar.id" ) ).all() with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.add_column(sa.Column("avatar_legacy_id", sa.String(), nullable=True)) with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("avatar_legacy_id", sa.String(), nullable=True)) if op.get_bind().engine.name != "postgresql": batch_op.create_unique_constraint( "uq_room_user_account_id_jid", ["user_account_id", "jid"] ) batch_op.create_unique_constraint( "uq_room_user_account_id_legacy_id", ["user_account_id", "legacy_id"] ) for room_pk, avatar_legacy_id in room_avatars: conn.execute( sa.update(Room) .where(Room.id == room_pk) .values(avatar_legacy_id=avatar_legacy_id) ) for contact_pk, avatar_legacy_id in contact_avatars: conn.execute( sa.update(Contact) .where(Contact.id == contact_pk) .values(avatar_legacy_id=avatar_legacy_id) ) # conn.commit() with op.batch_alter_table("avatar", schema=None) as batch_op: batch_op.drop_column("legacy_id") def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_constraint("uq_room_user_account_id_legacy_id", type_="unique") batch_op.drop_constraint("uq_room_user_account_id_jid", type_="unique") batch_op.drop_column("avatar_legacy_id") with op.batch_alter_table("contact", schema=None) as batch_op: batch_op.drop_column("avatar_legacy_id") with op.batch_alter_table("avatar", schema=None) as batch_op: batch_op.add_column(sa.Column("legacy_id", sa.VARCHAR(), nullable=True)) # ### end Alembic commands ### slidge/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py000066400000000000000000000166141477703150600305130ustar00rootroot00000000000000"""Move everything to persistent DB Revision ID: b33993e87db3 Revises: e91195719c2c Create Date: 2024-06-25 16:09:36.663953 """ import shutil from typing import Sequence, Union import sqlalchemy as sa from alembic import op import slidge.db.meta from slidge import global_config # revision identifiers, used by Alembic. revision: str = "b33993e87db3" down_revision: Union[str, None] = "e91195719c2c" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( "avatar", sa.Column("id", sa.Integer(), nullable=False), sa.Column("filename", sa.String(), nullable=False), sa.Column("hash", sa.String(), nullable=False), sa.Column("height", sa.Integer(), nullable=False), sa.Column("width", sa.Integer(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=True), sa.Column("url", sa.String(), nullable=True), sa.Column("etag", sa.String(), nullable=True), sa.Column("last_modified", sa.String(), nullable=True), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("filename"), sa.UniqueConstraint("hash"), sa.UniqueConstraint("legacy_id"), ) op.create_table( "attachment", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("legacy_file_id", sa.String(), nullable=True), sa.Column("url", sa.String(), nullable=False), sa.Column("sims", sa.String(), nullable=True), sa.Column("sfs", sa.String(), nullable=True), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), ) op.create_index( op.f("ix_attachment_legacy_file_id"), "attachment", ["legacy_file_id"], unique=False, ) op.create_index(op.f("ix_attachment_url"), "attachment", ["url"], unique=False) op.create_table( "contact", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=False), sa.Column("jid", slidge.db.meta.JIDType(), nullable=False), sa.Column("avatar_id", sa.Integer(), nullable=True), sa.Column("nick", sa.String(), nullable=True), sa.Column("cached_presence", sa.Boolean(), nullable=False), sa.Column("last_seen", sa.DateTime(), nullable=True), sa.Column("ptype", sa.String(), nullable=True), sa.Column("pstatus", sa.String(), nullable=True), sa.Column("pshow", sa.String(), nullable=True), sa.ForeignKeyConstraint( ["avatar_id"], ["avatar.id"], ), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("user_account_id", "jid"), sa.UniqueConstraint("user_account_id", "legacy_id"), ) op.create_table( "legacy_ids_multi", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=False), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), ) op.create_index( "legacy_ids_multi_user_account_id_legacy_id", "legacy_ids_multi", ["user_account_id", "legacy_id"], unique=True, ) op.create_table( "room", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=False), sa.Column("jid", slidge.db.meta.JIDType(), nullable=False), sa.Column("avatar_id", sa.Integer(), nullable=True), sa.Column("name", sa.String(), nullable=True), sa.ForeignKeyConstraint( ["avatar_id"], ["avatar.id"], ), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("jid"), sa.UniqueConstraint("legacy_id"), ) op.create_table( "xmpp_to_legacy_ids", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("xmpp_id", sa.String(), nullable=False), sa.Column("legacy_id", sa.String(), nullable=False), sa.Column( "type", sa.Enum("DM", "GROUP_CHAT", "THREAD", name="xmpptolegacyenum"), nullable=False, ), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), ) op.create_index( "xmpp_legacy", "xmpp_to_legacy_ids", ["user_account_id", "xmpp_id", "legacy_id"], unique=True, ) op.create_table( "mam", sa.Column("id", sa.Integer(), nullable=False), sa.Column("room_id", sa.Integer(), nullable=False), sa.Column("stanza_id", sa.String(), nullable=False), sa.Column("timestamp", sa.DateTime(), nullable=False), sa.Column("author_jid", slidge.db.meta.JIDType(), nullable=False), sa.Column("stanza", sa.String(), nullable=False), sa.ForeignKeyConstraint( ["room_id"], ["room.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("room_id", "stanza_id"), ) op.create_table( "xmpp_ids_multi", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_account_id", sa.Integer(), nullable=False), sa.Column("xmpp_id", sa.String(), nullable=False), sa.Column("legacy_ids_multi_id", sa.Integer(), nullable=False), sa.ForeignKeyConstraint( ["legacy_ids_multi_id"], ["legacy_ids_multi.id"], ), sa.ForeignKeyConstraint( ["user_account_id"], ["user_account.id"], ), sa.PrimaryKeyConstraint("id"), ) op.create_index( "legacy_ids_multi_user_account_id_xmpp_id", "xmpp_ids_multi", ["user_account_id", "xmpp_id"], unique=True, ) try: shutil.rmtree(global_config.HOME_DIR / "slidge_avatars_v2") except (FileNotFoundError, AttributeError): pass # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index( "legacy_ids_multi_user_account_id_xmpp_id", table_name="xmpp_ids_multi" ) op.drop_table("xmpp_ids_multi") op.drop_table("mam") op.drop_index("xmpp_legacy", table_name="xmpp_to_legacy_ids") op.drop_table("xmpp_to_legacy_ids") op.drop_table("room") op.drop_index( "legacy_ids_multi_user_account_id_legacy_id", table_name="legacy_ids_multi" ) op.drop_table("legacy_ids_multi") op.drop_table("contact") op.drop_index(op.f("ix_attachment_url"), table_name="attachment") op.drop_index(op.f("ix_attachment_legacy_file_id"), table_name="attachment") op.drop_table("attachment") op.drop_table("avatar") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py000066400000000000000000000030271477703150600313600ustar00rootroot00000000000000"""Add source and legacy ID for archived messages Revision ID: b64b1a793483 Revises: 82a4af84b679 Create Date: 2024-07-22 21:06:35.020569 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from slidge.db.models import ArchivedMessage # revision identifiers, used by Alembic. revision: str = "b64b1a793483" down_revision: Union[str, None] = "82a4af84b679" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # since we don't want source to be nullable, we drop all rows first. # This is what you get by using alpha versions! op.execute(sa.delete(ArchivedMessage)) if op.get_bind().engine.name == "postgresql": op.execute("CREATE TYPE archivedmessagesource AS ENUM ('LIVE', 'BACKFILL')") # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("mam", schema=None) as batch_op: batch_op.add_column( sa.Column( "source", sa.Enum("LIVE", "BACKFILL", name="archivedmessagesource"), nullable=False, ) ) batch_op.add_column(sa.Column("legacy_id", sa.String(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("mam", schema=None) as batch_op: batch_op.drop_column("legacy_id") batch_op.drop_column("source") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py000066400000000000000000000016451477703150600256600ustar00rootroot00000000000000"""Per-room user nick Revision ID: c4a8ec35a0e8 Revises: 09f27f098baa Create Date: 2024-07-12 06:27:47.397925 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "c4a8ec35a0e8" down_revision: Union[str, None] = "09f27f098baa" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.add_column(sa.Column("user_nick", sa.String(), nullable=True)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("room", schema=None) as batch_op: batch_op.drop_column("user_nick") # ### end Alembic commands ### slidge/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py000066400000000000000000000012001477703150600304730ustar00rootroot00000000000000"""Store users' avatars' hashes persistently Revision ID: e91195719c2c Revises: aa9d82a7f6ef Create Date: 2024-06-01 14:14:51.984943 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "e91195719c2c" down_revision: Union[str, None] = "aa9d82a7f6ef" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.add_column("user_account", sa.Column("avatar_hash", sa.String(), nullable=True)) def downgrade() -> None: op.drop_column("user_account", "avatar_hash") slidge/slidge/db/avatar.py000066400000000000000000000175411477703150600160330ustar00rootroot00000000000000import asyncio import hashlib import io import logging import uuid from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from http import HTTPStatus from pathlib import Path from typing import Optional import aiohttp from multidict import CIMultiDictProxy from PIL.Image import Image from PIL.Image import open as open_image from sqlalchemy import select from sqlalchemy.orm.exc import DetachedInstanceError from slidge.core import config from slidge.db.models import Avatar from slidge.db.store import AvatarStore from slidge.util.types import URL, AvatarType @dataclass class CachedAvatar: pk: int filename: str hash: str height: int width: int root: Path etag: Optional[str] = None last_modified: Optional[str] = None @property def data(self): return self.path.read_bytes() @property def path(self): return self.root / self.filename @staticmethod def from_store(stored: Avatar, root_dir: Path): return CachedAvatar( pk=stored.id, filename=stored.filename, hash=stored.hash, height=stored.height, width=stored.width, etag=stored.etag, root=root_dir, last_modified=stored.last_modified, ) class NotModified(Exception): pass class AvatarCache: dir: Path http: aiohttp.ClientSession store: AvatarStore def __init__(self): self._thread_pool = ThreadPoolExecutor(config.AVATAR_RESAMPLING_THREADS) def set_dir(self, path: Path): self.dir = path self.dir.mkdir(exist_ok=True) with self.store.session(): for stored in self.store.get_all(): avatar = CachedAvatar.from_store(stored, root_dir=path) if avatar.path.exists(): continue log.warning( "Removing avatar %s from store because %s does not exist", avatar.hash, avatar.path, ) self.store.delete_by_pk(stored.id) def close(self): self._thread_pool.shutdown(cancel_futures=True) def __get_http_headers(self, cached: Optional[CachedAvatar | Avatar]): headers = {} if cached and (self.dir / cached.filename).exists(): if last_modified := cached.last_modified: headers["If-Modified-Since"] = last_modified if etag := cached.etag: headers["If-None-Match"] = etag return headers async def __download( self, url: str, headers: dict[str, str], ) -> tuple[Image, CIMultiDictProxy[str]]: async with self.http.get(url, headers=headers) as response: if response.status == HTTPStatus.NOT_MODIFIED: log.debug("Using avatar cache for %s", url) raise NotModified response.raise_for_status() return ( open_image(io.BytesIO(await response.read())), response.headers, ) async def __is_modified(self, url, headers) -> bool: async with self.http.head(url, headers=headers) as response: return response.status != HTTPStatus.NOT_MODIFIED async def url_modified(self, url: URL) -> bool: cached = self.store.get_by_url(url) if cached is None: return True headers = self.__get_http_headers(cached) return await self.__is_modified(url, headers) def get_by_pk(self, pk: int) -> CachedAvatar: stored = self.store.get_by_pk(pk) assert stored is not None return CachedAvatar.from_store(stored, self.dir) @staticmethod async def _get_image(avatar: AvatarType) -> Image: if isinstance(avatar, bytes): return open_image(io.BytesIO(avatar)) elif isinstance(avatar, Path): return open_image(avatar) raise TypeError("Avatar must be bytes or a Path", avatar) async def convert_or_get(self, avatar: AvatarType) -> CachedAvatar: if isinstance(avatar, (URL, str)): with self.store.session(): stored = self.store.get_by_url(avatar) try: img, response_headers = await self.__download( avatar, self.__get_http_headers(stored) ) except NotModified: assert stored is not None try: return CachedAvatar.from_store(stored, self.dir) except DetachedInstanceError: # This is an awful hack to prevent errors on startup under certain conditions, # because we basically misused SQLAlchemy pretty bad in slidge.db.store.EngineMixin.session(). # cf https://codeberg.org/slidge/slidge/issues/36 # and https://docs.sqlalchemy.org/en/20/orm/session_basics.html#session-faq-threadsafe # It may be related to threads as we only have reports of this for slidge-whatsapp and skidge # which are the only implementations that uses threads. # In any case, a proper fix implies a major refactoring in which we spawn and close SQLAlchemy # "ORM Session"s with a reasonable, well-thought lifetime, instead of how we do it now, where we # basically just brute-forced our way into having something usable but with poor performance. # Databases, asyncio, and concurrency in general are hard… :( # Oh, and getting rid of the convoluted mess that this giant method is would also probably # be a good idea. stored = self.store.get_by_url(avatar) assert stored is not None return CachedAvatar.from_store(stored, self.dir) else: img = await self._get_image(avatar) response_headers = None with self.store.session() as orm: resize = (size := config.AVATAR_SIZE) and any(x > size for x in img.size) if resize: await asyncio.get_event_loop().run_in_executor( self._thread_pool, img.thumbnail, (size, size) ) log.debug("Resampled image to %s", img.size) filename = str(uuid.uuid1()) + ".png" file_path = self.dir / filename if ( not resize and img.format == "PNG" and isinstance(avatar, (str, Path)) and (path := Path(avatar)) and path.exists() ): img_bytes = path.read_bytes() else: with io.BytesIO() as f: img.save(f, format="PNG") img_bytes = f.getvalue() with file_path.open("wb") as file: file.write(img_bytes) hash_ = hashlib.sha1(img_bytes).hexdigest() stored = orm.execute(select(Avatar).where(Avatar.hash == hash_)).scalar() if stored is not None: return CachedAvatar.from_store(stored, self.dir) stored = Avatar( filename=filename, hash=hash_, height=img.height, width=img.width, url=avatar if isinstance(avatar, (URL, str)) else None, ) if response_headers: stored.etag = response_headers.get("etag") stored.last_modified = response_headers.get("last-modified") orm.add(stored) orm.commit() return CachedAvatar.from_store(stored, self.dir) avatar_cache = AvatarCache() log = logging.getLogger(__name__) _download_lock = asyncio.Lock() __all__ = ( "CachedAvatar", "avatar_cache", ) slidge/slidge/db/meta.py000066400000000000000000000034471477703150600155030ustar00rootroot00000000000000from __future__ import annotations import json from typing import Union import sqlalchemy as sa from slixmpp import JID class JIDType(sa.TypeDecorator[JID]): """ Custom SQLAlchemy type for JIDs """ impl = sa.types.TEXT cache_ok = True def process_bind_param(self, value: JID | None, dialect: sa.Dialect) -> str | None: if value is None: return value return str(value) def process_result_value( self, value: str | None, dialect: sa.Dialect ) -> JID | None: if value is None: return value return JID(value) class JSONEncodedDict(sa.TypeDecorator): """ Custom SQLAlchemy type for dictionaries stored as JSON Note that mutations of the dictionary are not detected by SQLAlchemy, which is why use ``attributes.flag_modified()`` in ``UserStore.update()`` """ impl = sa.VARCHAR cache_ok = True def process_bind_param(self, value, dialect): if value is not None: value = json.dumps(value) return value def process_result_value(self, value, dialect): if value is not None: value = json.loads(value) return value JSONSerializableTypes = Union[str, float, None, "JSONSerializable"] JSONSerializable = dict[str, JSONSerializableTypes] class Base(sa.orm.DeclarativeBase): type_annotation_map = {JSONSerializable: JSONEncodedDict, JID: JIDType} naming_convention = { "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", "ck": "ck_%(table_name)s_`%(constraint_name)s`", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s", } def get_engine(path: str) -> sa.Engine: engine = sa.create_engine(path) return engine slidge/slidge/db/models.py000066400000000000000000000333361477703150600160400ustar00rootroot00000000000000import warnings from datetime import datetime from enum import IntEnum from typing import Optional import sqlalchemy as sa from slixmpp import JID from slixmpp.types import MucAffiliation, MucRole from sqlalchemy import ForeignKey, Index, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from ..util.types import ClientType, MucType from .meta import Base, JSONSerializable, JSONSerializableTypes class XmppToLegacyEnum(IntEnum): """ XMPP-client generated IDs, used in the XmppToLegacyIds table to keep track of corresponding legacy IDs """ DM = 1 GROUP_CHAT = 2 THREAD = 3 class ArchivedMessageSource(IntEnum): """ Whether an archived message comes from ``LegacyMUC.backfill()`` or was received as a "live" message. """ LIVE = 1 BACKFILL = 2 class GatewayUser(Base): """ A user, registered to the gateway component. """ __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) jid: Mapped[JID] = mapped_column(unique=True) registration_date: Mapped[datetime] = mapped_column( sa.DateTime, server_default=sa.func.now() ) legacy_module_data: Mapped[JSONSerializable] = mapped_column(default={}) """ Arbitrary non-relational data that legacy modules can use """ preferences: Mapped[JSONSerializable] = mapped_column(default={}) avatar_hash: Mapped[Optional[str]] = mapped_column(default=None) """ Hash of the user's avatar, to avoid re-publishing the same avatar on the legacy network """ contacts: Mapped[list["Contact"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) rooms: Mapped[list["Room"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) xmpp_to_legacy: Mapped[list["XmppToLegacyIds"]] = relationship( cascade="all, delete-orphan" ) attachments: Mapped[list["Attachment"]] = relationship(cascade="all, delete-orphan") multi_legacy: Mapped[list["LegacyIdsMulti"]] = relationship( cascade="all, delete-orphan" ) multi_xmpp: Mapped[list["XmppIdsMulti"]] = relationship( cascade="all, delete-orphan" ) def __repr__(self) -> str: return f"User(id={self.id!r}, jid={self.jid!r})" def get(self, field: str, default: str = "") -> JSONSerializableTypes: # """ # Get fields from the registration form (required to comply with slixmpp backend protocol) # # :param field: Name of the field # :param default: Default value to return if the field is not present # # :return: Value of the field # """ return self.legacy_module_data.get(field, default) @property def registration_form(self) -> dict: # Kept for retrocompat, should be # FIXME: delete me warnings.warn( "GatewayUser.registration_form is deprecated.", DeprecationWarning ) return self.legacy_module_data class Avatar(Base): """ Avatars of contacts, rooms and participants. To comply with XEPs, we convert them all to PNG before storing them. """ __tablename__ = "avatar" id: Mapped[int] = mapped_column(primary_key=True) filename: Mapped[str] = mapped_column(unique=True) hash: Mapped[str] = mapped_column(unique=True) height: Mapped[int] = mapped_column() width: Mapped[int] = mapped_column() # this is only used when avatars are available as HTTP URLs and do not # have a legacy_id url: Mapped[Optional[str]] = mapped_column(default=None) etag: Mapped[Optional[str]] = mapped_column(default=None) last_modified: Mapped[Optional[str]] = mapped_column(default=None) contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar") rooms: Mapped[list["Room"]] = relationship(back_populates="avatar") class Contact(Base): """ Legacy contacts """ __tablename__ = "contact" __table_args__ = ( UniqueConstraint("user_account_id", "legacy_id"), UniqueConstraint("user_account_id", "jid"), ) id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped[GatewayUser] = relationship(back_populates="contacts") legacy_id: Mapped[str] = mapped_column(nullable=False) jid: Mapped[JID] = mapped_column() avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True) avatar: Mapped[Avatar] = relationship(back_populates="contacts") nick: Mapped[Optional[str]] = mapped_column(nullable=True) cached_presence: Mapped[bool] = mapped_column(default=False) last_seen: Mapped[Optional[datetime]] = mapped_column(nullable=True) ptype: Mapped[Optional[str]] = mapped_column(nullable=True) pstatus: Mapped[Optional[str]] = mapped_column(nullable=True) pshow: Mapped[Optional[str]] = mapped_column(nullable=True) caps_ver: Mapped[Optional[str]] = mapped_column(nullable=True) is_friend: Mapped[bool] = mapped_column(default=False) added_to_roster: Mapped[bool] = mapped_column(default=False) sent_order: Mapped[list["ContactSent"]] = relationship( back_populates="contact", cascade="all, delete-orphan" ) extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column( default=None, nullable=True ) updated: Mapped[bool] = mapped_column(default=False) vcard: Mapped[Optional[str]] = mapped_column() vcard_fetched: Mapped[bool] = mapped_column(default=False) participants: Mapped[list["Participant"]] = relationship(back_populates="contact") avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True) client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc") class ContactSent(Base): """ Keep track of XMPP msg ids sent by a specific contact for networks in which all messages need to be marked as read. (XMPP displayed markers convey a "read up to here" semantic.) """ __tablename__ = "contact_sent" __table_args__ = (UniqueConstraint("contact_id", "msg_id"),) id: Mapped[int] = mapped_column(primary_key=True) contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id")) contact: Mapped[Contact] = relationship(back_populates="sent_order") msg_id: Mapped[str] = mapped_column() class Room(Base): """ Legacy room """ __table_args__ = ( UniqueConstraint( "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id" ), UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"), ) __tablename__ = "room" id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped[GatewayUser] = relationship(back_populates="rooms") legacy_id: Mapped[str] = mapped_column(nullable=False) jid: Mapped[JID] = mapped_column(nullable=False) avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True) avatar: Mapped[Avatar] = relationship(back_populates="rooms") name: Mapped[Optional[str]] = mapped_column(nullable=True) description: Mapped[Optional[str]] = mapped_column(nullable=True) subject: Mapped[Optional[str]] = mapped_column(nullable=True) subject_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) subject_setter: Mapped[Optional[str]] = mapped_column(nullable=True) n_participants: Mapped[Optional[int]] = mapped_column(default=None) muc_type: Mapped[Optional[MucType]] = mapped_column(default=MucType.GROUP) user_nick: Mapped[Optional[str]] = mapped_column() user_resources: Mapped[Optional[str]] = mapped_column(nullable=True) participants_filled: Mapped[bool] = mapped_column(default=False) history_filled: Mapped[bool] = mapped_column(default=False) extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None) updated: Mapped[bool] = mapped_column(default=False) participants: Mapped[list["Participant"]] = relationship( back_populates="room", primaryjoin="Participant.room_id == Room.id", cascade="all, delete-orphan", ) avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True) archive: Mapped[list["ArchivedMessage"]] = relationship( cascade="all, delete-orphan" ) class ArchivedMessage(Base): """ Messages of rooms, that we store to act as a MAM server """ __tablename__ = "mam" __table_args__ = (UniqueConstraint("room_id", "stanza_id"),) id: Mapped[int] = mapped_column(primary_key=True) room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) stanza_id: Mapped[str] = mapped_column(nullable=False) timestamp: Mapped[datetime] = mapped_column(nullable=False) author_jid: Mapped[JID] = mapped_column(nullable=False) source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False) legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True) stanza: Mapped[str] = mapped_column(nullable=False) class XmppToLegacyIds(Base): """ XMPP-client generated IDs, and mapping to the corresponding legacy IDs """ __tablename__ = "xmpp_to_legacy_ids" __table_args__ = ( Index("xmpp_legacy", "user_account_id", "xmpp_id", "legacy_id", unique=True), ) id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped[GatewayUser] = relationship(back_populates="xmpp_to_legacy") xmpp_id: Mapped[str] = mapped_column(nullable=False) legacy_id: Mapped[str] = mapped_column(nullable=False) type: Mapped[XmppToLegacyEnum] = mapped_column(nullable=False) class Attachment(Base): """ Legacy attachments """ __tablename__ = "attachment" id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped[GatewayUser] = relationship(back_populates="attachments") legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True) url: Mapped[str] = mapped_column(index=True, nullable=False) sims: Mapped[Optional[str]] = mapped_column() sfs: Mapped[Optional[str]] = mapped_column() class LegacyIdsMulti(Base): """ Legacy messages with multiple attachments are split as several XMPP messages, this table and the next maps a single legacy ID to multiple XMPP IDs. """ __tablename__ = "legacy_ids_multi" __table_args__ = ( Index( "legacy_ids_multi_user_account_id_legacy_id", "user_account_id", "legacy_id", unique=True, ), ) id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) legacy_id: Mapped[str] = mapped_column(nullable=False) xmpp_ids: Mapped[list["XmppIdsMulti"]] = relationship( back_populates="legacy_ids_multi", cascade="all, delete-orphan" ) class XmppIdsMulti(Base): __tablename__ = "xmpp_ids_multi" __table_args__ = ( Index( "legacy_ids_multi_user_account_id_xmpp_id", "user_account_id", "xmpp_id", unique=True, ), ) id: Mapped[int] = mapped_column(primary_key=True) user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) xmpp_id: Mapped[str] = mapped_column(nullable=False) legacy_ids_multi_id: Mapped[int] = mapped_column(ForeignKey("legacy_ids_multi.id")) legacy_ids_multi: Mapped[LegacyIdsMulti] = relationship(back_populates="xmpp_ids") participant_hats = sa.Table( "participant_hats", Base.metadata, sa.Column("participant_id", ForeignKey("participant.id"), primary_key=True), sa.Column("hat_id", ForeignKey("hat.id"), primary_key=True), ) class Hat(Base): __tablename__ = "hat" __table_args__ = (UniqueConstraint("title", "uri"),) id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column() uri: Mapped[str] = mapped_column() participants: Mapped[list["Participant"]] = relationship( secondary=participant_hats, back_populates="hats" ) class Participant(Base): __tablename__ = "participant" id: Mapped[int] = mapped_column(primary_key=True) room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False) room: Mapped[Room] = relationship( back_populates="participants", primaryjoin=Room.id == room_id ) contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=True) contact: Mapped[Contact] = relationship(lazy=False, back_populates="participants") is_user: Mapped[bool] = mapped_column(default=False) affiliation: Mapped[MucAffiliation] = mapped_column(default="member") role: Mapped[MucRole] = mapped_column(default="participant") presence_sent: Mapped[bool] = mapped_column(default=False) resource: Mapped[Optional[str]] = mapped_column(default=None) nickname: Mapped[str] = mapped_column(nullable=True, default=None) nickname_no_illegal: Mapped[str] = mapped_column(nullable=True, default=None) hats: Mapped[list["Hat"]] = relationship( secondary=participant_hats, back_populates="participants" ) extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None) class Bob(Base): __tablename__ = "bob" id: Mapped[int] = mapped_column(primary_key=True) file_name: Mapped[str] = mapped_column(nullable=False) sha_1: Mapped[str] = mapped_column(nullable=False, unique=True) sha_256: Mapped[str] = mapped_column(nullable=False, unique=True) sha_512: Mapped[str] = mapped_column(nullable=False, unique=True) content_type: Mapped[Optional[str]] = mapped_column(nullable=False) slidge/slidge/db/store.py000066400000000000000000001341461477703150600157120ustar00rootroot00000000000000from __future__ import annotations import hashlib import json import logging import uuid from contextlib import contextmanager from datetime import datetime, timedelta, timezone from mimetypes import guess_extension from typing import TYPE_CHECKING, Collection, Iterator, Optional, Type from slixmpp import JID, Iq, Message, Presence from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0231.stanza import BitsOfBinary from sqlalchemy import Engine, delete, select, update from sqlalchemy.orm import Session, attributes, load_only from sqlalchemy.sql.functions import count from ..core import config from ..util.archive_msg import HistoryMessage from ..util.types import ( URL, CachedPresence, ClientType, MamMetadata, MucAffiliation, MucRole, Sticker, ) from ..util.types import Hat as HatTuple from .meta import Base from .models import ( ArchivedMessage, ArchivedMessageSource, Attachment, Avatar, Bob, Contact, ContactSent, GatewayUser, Hat, LegacyIdsMulti, Participant, Room, XmppIdsMulti, XmppToLegacyEnum, XmppToLegacyIds, participant_hats, ) if TYPE_CHECKING: from ..contact.contact import LegacyContact from ..group.participant import LegacyParticipant from ..group.room import LegacyMUC class EngineMixin: def __init__(self, engine: Engine): self._engine = engine # TODO: we should not have a global Session object but instead build Sessions with different parameters # depending on the context (startup, incoming XMPP event, incoming legacy event). @contextmanager def session(self, **session_kwargs) -> Iterator[Session]: global _session if _session is not None: yield _session return with Session(self._engine, **session_kwargs) as session: _session = session try: yield session finally: _session = None class UpdatedMixin(EngineMixin): model: Type[Base] = NotImplemented def __init__(self, *a, **kw): super().__init__(*a, **kw) with self.session() as session: session.execute(update(self.model).values(updated=False)) # type:ignore session.commit() def get_by_pk(self, pk: int) -> Optional[Base]: with self.session() as session: return session.execute( select(self.model).where(self.model.id == pk) # type:ignore ).scalar() class SlidgeStore(EngineMixin): def __init__(self, engine: Engine): super().__init__(engine) self.users = UserStore(engine) self.avatars = AvatarStore(engine) self.contacts = ContactStore(engine) self.mam = MAMStore(engine) self.multi = MultiStore(engine) self.attachments = AttachmentStore(engine) self.rooms = RoomStore(engine) self.sent = SentStore(engine) self.participants = ParticipantStore(engine) self.bob = BobStore(engine) class UserStore(EngineMixin): def new(self, jid: JID, legacy_module_data: dict) -> GatewayUser: if jid.resource: jid = JID(jid.bare) with self.session(expire_on_commit=False) as session: user = session.execute( select(GatewayUser).where(GatewayUser.jid == jid) ).scalar() if user is not None: return user user = GatewayUser(jid=jid, legacy_module_data=legacy_module_data) session.add(user) session.commit() return user def update(self, user: GatewayUser): # https://github.com/sqlalchemy/sqlalchemy/discussions/6473 attributes.flag_modified(user, "legacy_module_data") attributes.flag_modified(user, "preferences") with self.session() as session: session.add(user) session.commit() def get_all(self) -> Iterator[GatewayUser]: with self.session() as session: yield from session.execute(select(GatewayUser)).scalars() def get(self, jid: JID) -> Optional[GatewayUser]: with self.session() as session: return session.execute( select(GatewayUser).where(GatewayUser.jid == jid.bare) ).scalar() def get_by_stanza(self, stanza: Iq | Message | Presence) -> Optional[GatewayUser]: return self.get(stanza.get_from()) def delete(self, jid: JID) -> None: with self.session() as session: session.delete(self.get(jid)) session.commit() def set_avatar_hash(self, pk: int, h: str | None = None) -> None: with self.session() as session: session.execute( update(GatewayUser).where(GatewayUser.id == pk).values(avatar_hash=h) ) session.commit() class AvatarStore(EngineMixin): def get_by_url(self, url: URL | str) -> Optional[Avatar]: with self.session() as session: return session.execute(select(Avatar).where(Avatar.url == url)).scalar() def get_by_pk(self, pk: int) -> Optional[Avatar]: with self.session() as session: return session.execute(select(Avatar).where(Avatar.id == pk)).scalar() def delete_by_pk(self, pk: int): with self.session() as session: session.execute(delete(Avatar).where(Avatar.id == pk)) session.commit() def get_all(self) -> Iterator[Avatar]: with self.session() as session: yield from session.execute(select(Avatar)).scalars() class SentStore(EngineMixin): def set_message(self, user_pk: int, legacy_id: str, xmpp_id: str) -> None: with self.session() as session: msg = ( session.query(XmppToLegacyIds) .filter(XmppToLegacyIds.user_account_id == user_pk) .filter(XmppToLegacyIds.legacy_id == legacy_id) .filter(XmppToLegacyIds.xmpp_id == xmpp_id) .scalar() ) if msg is None: msg = XmppToLegacyIds(user_account_id=user_pk) else: log.debug("Resetting a DM from sent store") msg.legacy_id = legacy_id msg.xmpp_id = xmpp_id msg.type = XmppToLegacyEnum.DM session.add(msg) session.commit() def get_xmpp_id(self, user_pk: int, legacy_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(XmppToLegacyIds.xmpp_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.legacy_id == legacy_id) .where(XmppToLegacyIds.type == XmppToLegacyEnum.DM) ).scalar() def get_legacy_id(self, user_pk: int, xmpp_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(XmppToLegacyIds.legacy_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.xmpp_id == xmpp_id) .where(XmppToLegacyIds.type == XmppToLegacyEnum.DM) ).scalar() def set_group_message(self, user_pk: int, legacy_id: str, xmpp_id: str) -> None: with self.session() as session: msg = XmppToLegacyIds( user_account_id=user_pk, legacy_id=legacy_id, xmpp_id=xmpp_id, type=XmppToLegacyEnum.GROUP_CHAT, ) session.add(msg) session.commit() def get_group_xmpp_id(self, user_pk: int, legacy_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(XmppToLegacyIds.xmpp_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.legacy_id == legacy_id) .where(XmppToLegacyIds.type == XmppToLegacyEnum.GROUP_CHAT) ).scalar() def get_group_legacy_id(self, user_pk: int, xmpp_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(XmppToLegacyIds.legacy_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.xmpp_id == xmpp_id) .where(XmppToLegacyIds.type == XmppToLegacyEnum.GROUP_CHAT) ).scalar() def set_thread(self, user_pk: int, legacy_id: str, xmpp_id: str) -> None: with self.session() as session: msg = XmppToLegacyIds( user_account_id=user_pk, legacy_id=legacy_id, xmpp_id=xmpp_id, type=XmppToLegacyEnum.THREAD, ) session.add(msg) session.commit() def get_legacy_thread(self, user_pk: int, xmpp_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(XmppToLegacyIds.legacy_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.xmpp_id == xmpp_id) .where(XmppToLegacyIds.type == XmppToLegacyEnum.THREAD) ).scalar() def was_sent_by_user(self, user_pk: int, legacy_id: str) -> bool: with self.session() as session: return ( session.execute( select(XmppToLegacyIds.legacy_id) .where(XmppToLegacyIds.user_account_id == user_pk) .where(XmppToLegacyIds.legacy_id == legacy_id) ).scalar() is not None ) class ContactStore(UpdatedMixin): model = Contact def __init__(self, *a, **k): super().__init__(*a, **k) with self.session() as session: session.execute(update(Contact).values(cached_presence=False)) session.commit() def get_all(self, user_pk: int) -> Iterator[Contact]: with self.session() as session: yield from session.execute( select(Contact).where(Contact.user_account_id == user_pk) ).scalars() def get_by_jid(self, user_pk: int, jid: JID) -> Optional[Contact]: with self.session() as session: return session.execute( select(Contact) .where(Contact.jid == jid.bare) .where(Contact.user_account_id == user_pk) ).scalar() def get_by_legacy_id(self, user_pk: int, legacy_id: str) -> Optional[Contact]: with self.session() as session: return session.execute( select(Contact) .where(Contact.legacy_id == legacy_id) .where(Contact.user_account_id == user_pk) ).scalar() def update_nick(self, contact_pk: int, nick: Optional[str]) -> None: with self.session() as session: session.execute( update(Contact).where(Contact.id == contact_pk).values(nick=nick) ) session.commit() def get_presence(self, contact_pk: int) -> Optional[CachedPresence]: with self.session() as session: presence = session.execute( select( Contact.last_seen, Contact.ptype, Contact.pstatus, Contact.pshow, Contact.cached_presence, ).where(Contact.id == contact_pk) ).first() if presence is None or not presence[-1]: return None return CachedPresence(*presence[:-1]) def set_presence(self, contact_pk: int, presence: CachedPresence) -> None: with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(**presence._asdict(), cached_presence=True) ) session.commit() def reset_presence(self, contact_pk: int): with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values( last_seen=None, ptype=None, pstatus=None, pshow=None, cached_presence=False, ) ) session.commit() def set_avatar( self, contact_pk: int, avatar_pk: Optional[int], avatar_legacy_id: Optional[str] ): with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(avatar_id=avatar_pk, avatar_legacy_id=avatar_legacy_id) ) session.commit() def get_avatar_legacy_id(self, contact_pk: int) -> Optional[str]: with self.session() as session: contact = session.execute( select(Contact).where(Contact.id == contact_pk) ).scalar() if contact is None or contact.avatar is None: return None return contact.avatar_legacy_id def update(self, contact: "LegacyContact", commit=True) -> int: with self.session() as session: if contact.contact_pk is None: if contact.cached_presence is not None: presence_kwargs = contact.cached_presence._asdict() presence_kwargs["cached_presence"] = True else: presence_kwargs = {} row = Contact( jid=contact.jid.bare, legacy_id=str(contact.legacy_id), user_account_id=contact.user_pk, **presence_kwargs, ) else: row = ( session.query(Contact) .filter(Contact.id == contact.contact_pk) .one() ) row.nick = contact.name row.is_friend = contact.is_friend row.added_to_roster = contact.added_to_roster row.updated = True row.extra_attributes = contact.serialize_extra_attributes() row.caps_ver = contact._caps_ver row.vcard = contact._vcard row.vcard_fetched = contact._vcard_fetched row.client_type = contact.client_type session.add(row) if commit: session.commit() return row.id def set_vcard(self, contact_pk: int, vcard: str | None) -> None: with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(vcard=vcard, vcard_fetched=True) ) session.commit() def add_to_sent(self, contact_pk: int, msg_id: str) -> None: with self.session() as session: if ( session.query(ContactSent.id) .where(ContactSent.contact_id == contact_pk) .where(ContactSent.msg_id == msg_id) .first() ) is not None: log.warning( "Contact %s has already sent message %s", contact_pk, msg_id ) return new = ContactSent(contact_id=contact_pk, msg_id=msg_id) session.add(new) session.commit() def pop_sent_up_to(self, contact_pk: int, msg_id: str) -> list[str]: result = [] to_del = [] with self.session() as session: for row in session.execute( select(ContactSent) .where(ContactSent.contact_id == contact_pk) .order_by(ContactSent.id) ).scalars(): to_del.append(row.id) result.append(row.msg_id) if row.msg_id == msg_id: break for row_id in to_del: session.execute(delete(ContactSent).where(ContactSent.id == row_id)) session.commit() return result def set_friend(self, contact_pk: int, is_friend: bool) -> None: with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(is_friend=is_friend) ) session.commit() def set_added_to_roster(self, contact_pk: int, value: bool) -> None: with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(added_to_roster=value) ) session.commit() def delete(self, contact_pk: int) -> None: with self.session() as session: session.execute(delete(Contact).where(Contact.id == contact_pk)) session.commit() def set_client_type(self, contact_pk: int, value: ClientType): with self.session() as session: session.execute( update(Contact) .where(Contact.id == contact_pk) .values(client_type=value) ) session.commit() class MAMStore(EngineMixin): def __init__(self, *a, **kw): super().__init__(*a, **kw) with self.session() as session: session.execute( update(ArchivedMessage).values(source=ArchivedMessageSource.BACKFILL) ) session.commit() def nuke_older_than(self, days: int) -> None: with self.session() as session: session.execute( delete(ArchivedMessage).where( ArchivedMessage.timestamp < datetime.now() - timedelta(days=days) ) ) session.commit() def add_message( self, room_pk: int, message: HistoryMessage, archive_only: bool, legacy_msg_id: str | None, ) -> None: with self.session() as session: source = ( ArchivedMessageSource.BACKFILL if archive_only else ArchivedMessageSource.LIVE ) existing = session.execute( select(ArchivedMessage) .where(ArchivedMessage.room_id == room_pk) .where(ArchivedMessage.stanza_id == message.id) ).scalar() if existing is None and legacy_msg_id is not None: existing = session.execute( select(ArchivedMessage) .where(ArchivedMessage.room_id == room_pk) .where(ArchivedMessage.legacy_id == legacy_msg_id) ).scalar() if existing is not None: log.debug("Updating message %s in room %s", message.id, room_pk) existing.timestamp = message.when existing.stanza = str(message.stanza) existing.author_jid = message.stanza.get_from() existing.source = source existing.legacy_id = legacy_msg_id session.add(existing) session.commit() return mam_msg = ArchivedMessage( stanza_id=message.id, timestamp=message.when, stanza=str(message.stanza), author_jid=message.stanza.get_from(), room_id=room_pk, source=source, legacy_id=legacy_msg_id, ) session.add(mam_msg) session.commit() def get_messages( self, room_pk: int, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, before_id: Optional[str] = None, after_id: Optional[str] = None, ids: Collection[str] = (), last_page_n: Optional[int] = None, sender: Optional[str] = None, flip=False, ) -> Iterator[HistoryMessage]: with self.session() as session: q = select(ArchivedMessage).where(ArchivedMessage.room_id == room_pk) if start_date is not None: q = q.where(ArchivedMessage.timestamp >= start_date) if end_date is not None: q = q.where(ArchivedMessage.timestamp <= end_date) if before_id is not None: stamp = session.execute( select(ArchivedMessage.timestamp).where( ArchivedMessage.stanza_id == before_id ) ).scalar() if stamp is None: raise XMPPError( "item-not-found", f"Message {before_id} not found", ) q = q.where(ArchivedMessage.timestamp < stamp) if after_id is not None: stamp = session.execute( select(ArchivedMessage.timestamp).where( ArchivedMessage.stanza_id == after_id ) ).scalar() if stamp is None: raise XMPPError( "item-not-found", f"Message {after_id} not found", ) q = q.where(ArchivedMessage.timestamp > stamp) if ids: q = q.filter(ArchivedMessage.stanza_id.in_(ids)) if sender is not None: q = q.where(ArchivedMessage.author_jid == sender) if flip: q = q.order_by(ArchivedMessage.timestamp.desc()) else: q = q.order_by(ArchivedMessage.timestamp.asc()) msgs = list(session.execute(q).scalars()) if ids and len(msgs) != len(ids): raise XMPPError( "item-not-found", "One of the requested messages IDs could not be found " "with the given constraints.", ) if last_page_n is not None: if flip: msgs = msgs[:last_page_n] else: msgs = msgs[-last_page_n:] for h in msgs: yield HistoryMessage( stanza=str(h.stanza), when=h.timestamp.replace(tzinfo=timezone.utc) ) def get_first(self, room_pk: int, with_legacy_id=False) -> ArchivedMessage | None: with self.session() as session: q = ( select(ArchivedMessage) .where(ArchivedMessage.room_id == room_pk) .order_by(ArchivedMessage.timestamp.asc()) ) if with_legacy_id: q = q.filter(ArchivedMessage.legacy_id.isnot(None)) return session.execute(q).scalar() def get_last( self, room_pk: int, source: ArchivedMessageSource | None = None ) -> ArchivedMessage | None: with self.session() as session: q = select(ArchivedMessage).where(ArchivedMessage.room_id == room_pk) if source is not None: q = q.where(ArchivedMessage.source == source) return session.execute( q.order_by(ArchivedMessage.timestamp.desc()) ).scalar() def get_first_and_last(self, room_pk: int) -> list[MamMetadata]: r = [] with self.session(): first = self.get_first(room_pk) if first is not None: r.append(MamMetadata(first.stanza_id, first.timestamp)) last = self.get_last(room_pk) if last is not None: r.append(MamMetadata(last.stanza_id, last.timestamp)) return r def get_most_recent_with_legacy_id( self, room_pk: int, source: ArchivedMessageSource | None = None ) -> ArchivedMessage | None: with self.session() as session: q = ( select(ArchivedMessage) .where(ArchivedMessage.room_id == room_pk) .where(ArchivedMessage.legacy_id.isnot(None)) ) if source is not None: q = q.where(ArchivedMessage.source == source) return session.execute( q.order_by(ArchivedMessage.timestamp.desc()) ).scalar() def get_least_recent_with_legacy_id_after( self, room_pk: int, after_id: str, source=ArchivedMessageSource.LIVE ) -> ArchivedMessage | None: with self.session() as session: after_timestamp = ( session.query(ArchivedMessage.timestamp) .filter(ArchivedMessage.room_id == room_pk) .filter(ArchivedMessage.legacy_id == after_id) .scalar() ) q = ( select(ArchivedMessage) .where(ArchivedMessage.room_id == room_pk) .where(ArchivedMessage.legacy_id.isnot(None)) .where(ArchivedMessage.source == source) .where(ArchivedMessage.timestamp > after_timestamp) ) return session.execute(q.order_by(ArchivedMessage.timestamp.asc())).scalar() def get_by_legacy_id(self, room_pk: int, legacy_id: str) -> ArchivedMessage | None: with self.session() as session: return ( session.query(ArchivedMessage) .filter(ArchivedMessage.room_id == room_pk) .filter(ArchivedMessage.legacy_id == legacy_id) .first() ) class MultiStore(EngineMixin): def get_xmpp_ids(self, user_pk: int, xmpp_id: str) -> list[str]: with self.session() as session: multi = session.execute( select(XmppIdsMulti) .where(XmppIdsMulti.xmpp_id == xmpp_id) .where(XmppIdsMulti.user_account_id == user_pk) ).scalar() if multi is None: return [] if multi.legacy_ids_multi is None: return [] return [m.xmpp_id for m in multi.legacy_ids_multi.xmpp_ids] def set_xmpp_ids( self, user_pk: int, legacy_msg_id: str, xmpp_ids: list[str], fail=False ) -> None: with self.session() as session: existing = session.execute( select(LegacyIdsMulti) .where(LegacyIdsMulti.user_account_id == user_pk) .where(LegacyIdsMulti.legacy_id == legacy_msg_id) ).scalar() if existing is not None: if fail: raise log.debug("Resetting multi for %s", legacy_msg_id) session.execute( delete(LegacyIdsMulti) .where(LegacyIdsMulti.user_account_id == user_pk) .where(LegacyIdsMulti.legacy_id == legacy_msg_id) ) for i in xmpp_ids: session.execute( delete(XmppIdsMulti) .where(XmppIdsMulti.user_account_id == user_pk) .where(XmppIdsMulti.xmpp_id == i) ) session.commit() self.set_xmpp_ids(user_pk, legacy_msg_id, xmpp_ids, True) return row = LegacyIdsMulti( user_account_id=user_pk, legacy_id=legacy_msg_id, xmpp_ids=[ XmppIdsMulti(user_account_id=user_pk, xmpp_id=i) for i in xmpp_ids if i ], ) session.add(row) session.commit() def get_legacy_id(self, user_pk: int, xmpp_id: str) -> Optional[str]: with self.session() as session: multi = session.execute( select(XmppIdsMulti) .where(XmppIdsMulti.xmpp_id == xmpp_id) .where(XmppIdsMulti.user_account_id == user_pk) ).scalar() if multi is None: return None if multi.legacy_ids_multi is None: return None return multi.legacy_ids_multi.legacy_id class AttachmentStore(EngineMixin): def get_url(self, legacy_file_id: str) -> Optional[str]: with self.session() as session: return session.execute( select(Attachment.url).where( Attachment.legacy_file_id == legacy_file_id ) ).scalar() def set_url(self, user_pk: int, legacy_file_id: str, url: str) -> None: with self.session() as session: att = session.execute( select(Attachment) .where(Attachment.legacy_file_id == legacy_file_id) .where(Attachment.user_account_id == user_pk) ).scalar() if att is None: att = Attachment( legacy_file_id=legacy_file_id, url=url, user_account_id=user_pk ) session.add(att) else: att.url = url session.commit() def get_sims(self, url: str) -> Optional[str]: with self.session() as session: return session.execute( select(Attachment.sims).where(Attachment.url == url) ).scalar() def set_sims(self, url: str, sims: str) -> None: with self.session() as session: session.execute( update(Attachment).where(Attachment.url == url).values(sims=sims) ) session.commit() def get_sfs(self, url: str) -> Optional[str]: with self.session() as session: return session.execute( select(Attachment.sfs).where(Attachment.url == url) ).scalar() def set_sfs(self, url: str, sfs: str) -> None: with self.session() as session: session.execute( update(Attachment).where(Attachment.url == url).values(sfs=sfs) ) session.commit() def remove(self, legacy_file_id: str) -> None: with self.session() as session: session.execute( delete(Attachment).where(Attachment.legacy_file_id == legacy_file_id) ) session.commit() class RoomStore(UpdatedMixin): model = Room def __init__(self, *a, **kw): super().__init__(*a, **kw) with self.session() as session: session.execute( update(Room).values( subject_setter=None, user_resources=None, history_filled=False, participants_filled=False, ) ) session.commit() def set_avatar( self, room_pk: int, avatar_pk: int | None, avatar_legacy_id: str | None ) -> None: with self.session() as session: session.execute( update(Room) .where(Room.id == room_pk) .values(avatar_id=avatar_pk, avatar_legacy_id=avatar_legacy_id) ) session.commit() def get_avatar_legacy_id(self, room_pk: int) -> Optional[str]: with self.session() as session: room = session.execute(select(Room).where(Room.id == room_pk)).scalar() if room is None or room.avatar is None: return None return room.avatar_legacy_id def get_by_jid(self, user_pk: int, jid: JID) -> Optional[Room]: if jid.resource: raise TypeError with self.session() as session: return session.execute( select(Room) .where(Room.user_account_id == user_pk) .where(Room.jid == jid) ).scalar() def get_by_legacy_id(self, user_pk: int, legacy_id: str) -> Optional[Room]: with self.session() as session: return session.execute( select(Room) .where(Room.user_account_id == user_pk) .where(Room.legacy_id == legacy_id) ).scalar() def update_subject_setter(self, room_pk: int, subject_setter: str | None): with self.session() as session: session.execute( update(Room) .where(Room.id == room_pk) .values(subject_setter=subject_setter) ) session.commit() def update(self, muc: "LegacyMUC") -> int: with self.session() as session: if muc.pk is None: row = Room( jid=muc.jid, legacy_id=str(muc.legacy_id), user_account_id=muc.user_pk, ) else: row = session.query(Room).filter(Room.id == muc.pk).one() row.updated = True row.extra_attributes = muc.serialize_extra_attributes() row.name = muc.name row.description = muc.description row.user_resources = ( None if not muc._user_resources else json.dumps(list(muc._user_resources)) ) row.muc_type = muc.type row.subject = muc.subject row.subject_date = muc.subject_date row.subject_setter = muc.subject_setter row.participants_filled = muc._participants_filled row.n_participants = muc._n_participants row.user_nick = muc.user_nick session.add(row) session.commit() return row.id def update_subject_date( self, room_pk: int, subject_date: Optional[datetime] ) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(subject_date=subject_date) ) session.commit() def update_subject(self, room_pk: int, subject: Optional[str]) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(subject=subject) ) session.commit() def update_description(self, room_pk: int, desc: Optional[str]) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(description=desc) ) session.commit() def update_name(self, room_pk: int, name: Optional[str]) -> None: with self.session() as session: session.execute(update(Room).where(Room.id == room_pk).values(name=name)) session.commit() def update_n_participants(self, room_pk: int, n: Optional[int]) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(n_participants=n) ) session.commit() def update_user_nick(self, room_pk, nick: str) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(user_nick=nick) ) session.commit() def delete(self, room_pk: int) -> None: with self.session() as session: session.execute(delete(Room).where(Room.id == room_pk)) session.execute(delete(Participant).where(Participant.room_id == room_pk)) session.commit() def set_resource(self, room_pk: int, resources: set[str]) -> None: with self.session() as session: session.execute( update(Room) .where(Room.id == room_pk) .values( user_resources=( None if not resources else json.dumps(list(resources)) ) ) ) session.commit() def nickname_is_available(self, room_pk: int, nickname: str) -> bool: with self.session() as session: return ( session.execute( select(Participant) .where(Participant.room_id == room_pk) .where(Participant.nickname == nickname) ).scalar() is None ) def set_participants_filled(self, room_pk: int, val=True) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(participants_filled=val) ) session.commit() def set_history_filled(self, room_pk: int, val=True) -> None: with self.session() as session: session.execute( update(Room).where(Room.id == room_pk).values(history_filled=True) ) session.commit() def get_all(self, user_pk: int) -> Iterator[Room]: with self.session() as session: yield from session.execute( select(Room).where(Room.user_account_id == user_pk) ).scalars() def get_all_jid_and_names(self, user_pk: int) -> Iterator[Room]: with self.session() as session: yield from session.scalars( select(Room) .filter(Room.user_account_id == user_pk) .options(load_only(Room.jid, Room.name)) .order_by(Room.name) ).all() class ParticipantStore(EngineMixin): def __init__(self, *a, **kw): super().__init__(*a, **kw) with self.session() as session: session.execute(delete(participant_hats)) session.execute(delete(Hat)) session.execute(delete(Participant)) session.commit() def add(self, room_pk: int, nickname: str) -> int: with self.session() as session: existing = session.execute( select(Participant.id) .where(Participant.room_id == room_pk) .where(Participant.nickname == nickname) ).scalar() if existing is not None: return existing participant = Participant(room_id=room_pk, nickname=nickname) session.add(participant) session.commit() return participant.id def get_by_nickname(self, room_pk: int, nickname: str) -> Optional[Participant]: with self.session() as session: return session.execute( select(Participant) .where(Participant.room_id == room_pk) .where(Participant.nickname == nickname) ).scalar() def get_by_resource(self, room_pk: int, resource: str) -> Optional[Participant]: with self.session() as session: return session.execute( select(Participant) .where(Participant.room_id == room_pk) .where(Participant.resource == resource) ).scalar() def get_by_contact(self, room_pk: int, contact_pk: int) -> Optional[Participant]: with self.session() as session: return session.execute( select(Participant) .where(Participant.room_id == room_pk) .where(Participant.contact_id == contact_pk) ).scalar() def get_all(self, room_pk: int, user_included=True) -> Iterator[Participant]: with self.session() as session: q = select(Participant).where(Participant.room_id == room_pk) if not user_included: q = q.where(~Participant.is_user) yield from session.execute(q).scalars() def get_for_contact(self, contact_pk: int) -> Iterator[Participant]: with self.session() as session: yield from session.execute( select(Participant).where(Participant.contact_id == contact_pk) ).scalars() def update(self, participant: "LegacyParticipant") -> None: with self.session() as session: session.execute( update(Participant) .where(Participant.id == participant.pk) .values( nickname=participant.nickname, resource=participant.jid.resource, nickname_no_illegal=participant._nickname_no_illegal, affiliation=participant.affiliation, role=participant.role, presence_sent=participant._presence_sent, # type:ignore # hats=[self.add_hat(h.uri, h.title) for h in participant._hats], is_user=participant.is_user, contact_id=( None if participant.contact is None else participant.contact.contact_pk ), ) ) session.commit() def add_hat(self, uri: str, title: str) -> Hat: with self.session() as session: existing = session.execute( select(Hat).where(Hat.uri == uri).where(Hat.title == title) ).scalar() if existing is not None: return existing hat = Hat(uri=uri, title=title) session.add(hat) session.commit() return hat def set_presence_sent(self, participant_pk: int) -> None: with self.session() as session: session.execute( update(Participant) .where(Participant.id == participant_pk) .values(presence_sent=True) ) session.commit() def set_affiliation(self, participant_pk: int, affiliation: MucAffiliation) -> None: with self.session() as session: session.execute( update(Participant) .where(Participant.id == participant_pk) .values(affiliation=affiliation) ) session.commit() def set_role(self, participant_pk: int, role: MucRole) -> None: with self.session() as session: session.execute( update(Participant) .where(Participant.id == participant_pk) .values(role=role) ) session.commit() def set_hats(self, participant_pk: int, hats: list[HatTuple]) -> None: with self.session() as session: part = session.execute( select(Participant).where(Participant.id == participant_pk) ).scalar() if part is None: raise ValueError part.hats.clear() for h in hats: hat = self.add_hat(*h) if hat in part.hats: continue part.hats.append(hat) session.commit() def delete(self, participant_pk: int) -> None: with self.session() as session: session.execute(delete(Participant).where(Participant.id == participant_pk)) def get_count(self, room_pk: int) -> int: with self.session() as session: return session.query( count(Participant.id).filter(Participant.room_id == room_pk) ).scalar() class BobStore(EngineMixin): _ATTR_MAP = { "sha-1": "sha_1", "sha1": "sha_1", "sha-256": "sha_256", "sha256": "sha_256", "sha-512": "sha_512", "sha512": "sha_512", } _ALG_MAP = { "sha_1": hashlib.sha1, "sha_256": hashlib.sha256, "sha_512": hashlib.sha512, } def __init__(self, *a, **k): super().__init__(*a, **k) self.root_dir = config.HOME_DIR / "slidge_stickers" self.root_dir.mkdir(exist_ok=True) @staticmethod def __split_cid(cid: str) -> list[str]: return cid.removesuffix("@bob.xmpp.org").split("+") def __get_condition(self, cid: str): alg_name, digest = self.__split_cid(cid) attr = self._ATTR_MAP.get(alg_name) if attr is None: log.warning("Unknown hash algo: %s", alg_name) return None return getattr(Bob, attr) == digest def get(self, cid: str) -> Bob | None: with self.session() as session: try: return session.query(Bob).filter(self.__get_condition(cid)).scalar() except ValueError: log.warning("Cannot get Bob with CID: %s", cid) return None def get_sticker(self, cid: str) -> Sticker | None: bob = self.get(cid) if bob is None: return None return Sticker( self.root_dir / bob.file_name, bob.content_type, {h: getattr(bob, h) for h in self._ALG_MAP}, ) def get_bob(self, _jid, _node, _ifrom, cid: str) -> BitsOfBinary | None: stored = self.get(cid) if stored is None: return None bob = BitsOfBinary() bob["data"] = (self.root_dir / stored.file_name).read_bytes() if stored.content_type is not None: bob["type"] = stored.content_type bob["cid"] = cid return bob def del_bob(self, _jid, _node, _ifrom, cid: str) -> None: with self.session() as orm: try: file_name = orm.scalar( delete(Bob) .where(self.__get_condition(cid)) .returning(Bob.file_name) ) except ValueError: log.warning("Cannot delete Bob with CID: %s", cid) return None if file_name is None: log.warning("No BoB with CID: %s", cid) return None (self.root_dir / file_name).unlink() orm.commit() def set_bob(self, _jid, _node, _ifrom, bob: BitsOfBinary) -> None: cid = bob["cid"] try: alg_name, digest = self.__split_cid(cid) except ValueError: log.warning("Cannot set Bob with CID: %s", cid) return attr = self._ATTR_MAP.get(alg_name) if attr is None: log.warning("Cannot set BoB with unknown hash algo: %s", alg_name) return None with self.session() as orm: existing = self.get(bob["cid"]) if existing is not None: log.debug("Bob already known") return bytes_ = bob["data"] path = self.root_dir / uuid.uuid4().hex if bob["type"]: path = path.with_suffix(guess_extension(bob["type"]) or "") path.write_bytes(bytes_) hashes = {k: v(bytes_).hexdigest() for k, v in self._ALG_MAP.items()} if hashes[attr] != digest: raise ValueError( "The given CID does not correspond to the result of our hash" ) row = Bob(file_name=path.name, content_type=bob["type"] or None, **hashes) orm.add(row) orm.commit() log = logging.getLogger(__name__) _session: Optional[Session] = None slidge/slidge/group/000077500000000000000000000000001477703150600147425ustar00rootroot00000000000000slidge/slidge/group/__init__.py000066400000000000000000000004021477703150600170470ustar00rootroot00000000000000""" Everything related to groups. """ from ..util.types import MucType from .bookmarks import LegacyBookmarks from .participant import LegacyParticipant from .room import LegacyMUC __all__ = ("LegacyBookmarks", "LegacyParticipant", "LegacyMUC", "MucType") slidge/slidge/group/archive.py000066400000000000000000000125051477703150600167400ustar00rootroot00000000000000import logging import uuid from copy import copy from datetime import datetime, timezone from typing import TYPE_CHECKING, Collection, Optional from slixmpp import Iq, Message from ..db.models import ArchivedMessage, ArchivedMessageSource from ..db.store import MAMStore from ..util.archive_msg import HistoryMessage from ..util.types import HoleBound if TYPE_CHECKING: from .participant import LegacyParticipant class MessageArchive: def __init__(self, room_pk: int, store: MAMStore): self.room_pk = room_pk self.__store = store def add( self, msg: Message, participant: Optional["LegacyParticipant"] = None, archive_only=False, legacy_msg_id=None, ): """ Add a message to the archive if it is deemed archivable :param msg: :param participant: :param archive_only: :param legacy_msg_id: """ if not archivable(msg): return new_msg = copy(msg) if participant and not participant.muc.is_anonymous: new_msg["muc"]["role"] = participant.role new_msg["muc"]["affiliation"] = participant.affiliation if participant.contact: new_msg["muc"]["jid"] = participant.contact.jid.bare elif participant.is_user: new_msg["muc"]["jid"] = participant.user_jid.bare elif participant.is_system: new_msg["muc"]["jid"] = participant.muc.jid else: log.warning("No real JID for participant in this group") new_msg["muc"]["jid"] = ( f"{uuid.uuid4()}@{participant.xmpp.boundjid.bare}" ) self.__store.add_message( self.room_pk, HistoryMessage(new_msg), archive_only, None if legacy_msg_id is None else str(legacy_msg_id), ) def __iter__(self): return iter(self.get_all()) @staticmethod def __to_bound(stored: ArchivedMessage): return HoleBound( stored.legacy_id, # type:ignore stored.timestamp.replace(tzinfo=timezone.utc), ) def get_hole_bounds(self) -> tuple[HoleBound | None, HoleBound | None]: most_recent = self.__store.get_most_recent_with_legacy_id(self.room_pk) if most_recent is None: return None, None if most_recent.source == ArchivedMessageSource.BACKFILL: # most recent = only backfill, fetch everything since last backfill return self.__to_bound(most_recent), None most_recent_back_filled = self.__store.get_most_recent_with_legacy_id( self.room_pk, ArchivedMessageSource.BACKFILL ) if most_recent_back_filled is None: # group was never back-filled, fetch everything before first live least_recent_live = self.__store.get_first(self.room_pk, True) assert least_recent_live is not None return None, self.__to_bound(least_recent_live) assert most_recent_back_filled.legacy_id is not None least_recent_live = self.__store.get_least_recent_with_legacy_id_after( self.room_pk, most_recent_back_filled.legacy_id ) assert least_recent_live is not None # this is a hole caused by slidge downtime return self.__to_bound(most_recent_back_filled), self.__to_bound( least_recent_live ) def get_all( self, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, before_id: Optional[str] = None, after_id: Optional[str] = None, ids: Collection[str] = (), last_page_n: Optional[int] = None, sender: Optional[str] = None, flip=False, ): for msg in self.__store.get_messages( self.room_pk, before_id=before_id, after_id=after_id, ids=ids, last_page_n=last_page_n, sender=sender, start_date=start_date, end_date=end_date, flip=flip, ): yield msg async def send_metadata(self, iq: Iq): """ Send archive extent, as per the spec :param iq: :return: """ reply = iq.reply() messages = self.__store.get_first_and_last(self.room_pk) if messages: for x, m in [("start", messages[0]), ("end", messages[-1])]: reply["mam_metadata"][x]["id"] = m.id reply["mam_metadata"][x]["timestamp"] = m.sent_on.replace( tzinfo=timezone.utc ) else: reply.enable("mam_metadata") reply.send() def archivable(msg: Message): """ Determine if a message stanza is worth archiving, ie, convey meaningful info :param msg: :return: """ if msg.get_plugin("no-store", check=True): return False if msg.get_plugin("no-permanent-store", check=True): return False if msg.get_plugin("store", check=True): return True if msg["body"]: return True if msg.get_plugin("retract", check=True): return True if msg.get_plugin("reactions", check=True): return True if msg.get_plugin("displayed", check=True): return True return False log = logging.getLogger(__name__) slidge/slidge/group/bookmarks.py000066400000000000000000000147001477703150600173060ustar00rootroot00000000000000import abc import logging from typing import TYPE_CHECKING, Generic, Iterator, Optional, Type from slixmpp import JID from slixmpp.exceptions import XMPPError from ..core.mixins.lock import NamedLockMixin from ..db.models import Room from ..util import SubclassableOnce from ..util.jid_escaping import ESCAPE_TABLE, unescape_node from ..util.types import LegacyGroupIdType, LegacyMUCType from .archive import MessageArchive from .room import LegacyMUC if TYPE_CHECKING: from slidge.core.session import BaseSession class LegacyBookmarks( Generic[LegacyGroupIdType, LegacyMUCType], NamedLockMixin, metaclass=SubclassableOnce, ): """ This is instantiated once per :class:`~slidge.BaseSession` """ def __init__(self, session: "BaseSession"): self.session = session self.xmpp = session.xmpp self.user_jid = session.user_jid self.__store = self.xmpp.store.rooms self._muc_class: Type[LegacyMUCType] = LegacyMUC.get_self_or_unique_subclass() self._user_nick: str = self.session.user_jid.node super().__init__() self.log = logging.getLogger(f"{self.user_jid.bare}:bookmarks") self.ready = self.session.xmpp.loop.create_future() if not self.xmpp.GROUPS: self.ready.set_result(True) @property def user_nick(self): return self._user_nick @user_nick.setter def user_nick(self, nick: str): self._user_nick = nick def __iter__(self) -> Iterator[LegacyMUCType]: for stored in self.__store.get_all(user_pk=self.session.user_pk): yield self._muc_class.from_store(self.session, stored) def __repr__(self): return f"" async def legacy_id_to_jid_local_part(self, legacy_id: LegacyGroupIdType): return await self.legacy_id_to_jid_username(legacy_id) async def jid_local_part_to_legacy_id(self, local_part: str): return await self.jid_username_to_legacy_id(local_part) async def legacy_id_to_jid_username(self, legacy_id: LegacyGroupIdType): """ The default implementation calls ``str()`` on the legacy_id and escape characters according to :xep:`0106`. You can override this class and implement a more subtle logic to raise an :class:`~slixmpp.exceptions.XMPPError` early :param legacy_id: :return: """ return str(legacy_id).translate(ESCAPE_TABLE) async def jid_username_to_legacy_id(self, username: str): """ :param username: :return: """ return unescape_node(username) async def by_jid(self, jid: JID) -> LegacyMUCType: if jid.resource: jid = JID(jid.bare) async with self.lock(("bare", jid.bare)): assert isinstance(jid.user, str) legacy_id = await self.jid_local_part_to_legacy_id(jid.user) if self.get_lock(("legacy_id", legacy_id)): self.log.debug("Not instantiating %s after all", jid) return await self.by_legacy_id(legacy_id) with self.__store.session(): stored = self.__store.get_by_jid(self.session.user_pk, jid) return await self.__update_muc(stored, legacy_id, jid) def by_jid_only_if_exists(self, jid: JID) -> Optional[LegacyMUCType]: with self.__store.session(): stored = self.__store.get_by_jid(self.session.user_pk, jid) if stored is not None and stored.updated: return self._muc_class.from_store(self.session, stored) return None async def by_legacy_id(self, legacy_id: LegacyGroupIdType) -> LegacyMUCType: async with self.lock(("legacy_id", legacy_id)): local = await self.legacy_id_to_jid_local_part(legacy_id) jid = JID(f"{local}@{self.xmpp.boundjid}") if self.get_lock(("bare", jid.bare)): self.log.debug("Not instantiating %s after all", legacy_id) return await self.by_jid(jid) with self.__store.session(): stored = self.__store.get_by_legacy_id( self.session.user_pk, str(legacy_id) ) return await self.__update_muc(stored, legacy_id, jid) async def __update_muc( self, stored: Room | None, legacy_id: LegacyGroupIdType, jid: JID ): if stored is None: muc = self._muc_class(self.session, legacy_id=legacy_id, jid=jid) else: muc = self._muc_class.from_store(self.session, stored) if stored.updated: return muc try: with muc.updating_info(): await muc.avatar_wrap_update_info() except XMPPError: raise except Exception as e: raise XMPPError("internal-server-error", str(e)) if not muc.user_nick: muc.user_nick = self._user_nick self.log.debug("MUC created: %r", muc) muc.pk = self.__store.update(muc) muc.archive = MessageArchive(muc.pk, self.xmpp.store.mam) return muc @abc.abstractmethod async def fill(self): """ Establish a user's known groups. This has to be overridden in plugins with group support and at the minimum, this should ``await self.by_legacy_id(group_id)`` for all the groups a user is part of. Slidge internals will call this on successful :meth:`BaseSession.login` """ if self.xmpp.GROUPS: raise NotImplementedError( "The plugin advertised support for groups but" " LegacyBookmarks.fill() was not overridden." ) async def remove( self, muc: LegacyMUC, reason="You left this group from the official client.", kick=True, ) -> None: """ Delete everything about a specific group. This should be called when the user leaves the group from the official app. :param muc: The MUC to remove. :param reason: Optionally, a reason why this group was removed. :param kick: Whether the user should be kicked from this group. Set this to False in case you do this somewhere else in your code, eg, on receiving the confirmation that the group was deleted. """ assert muc.pk is not None if kick: user_participant = await muc.get_user_participant() user_participant.kick(reason) self.__store.delete(muc.pk) slidge/slidge/group/participant.py000066400000000000000000000425021477703150600176350ustar00rootroot00000000000000import logging import string import uuid import warnings from copy import copy from datetime import datetime from functools import cached_property from typing import TYPE_CHECKING, Optional, Self, Union from slixmpp import JID, InvalidJID, Message, Presence from slixmpp.plugins.xep_0045.stanza import MUCAdminItem from slixmpp.types import MessageTypes, OptJid from ..contact import LegacyContact from ..core.mixins import ( ChatterDiscoMixin, MessageMixin, PresenceMixin, StoredAttributeMixin, ) from ..db.models import Participant from ..util import SubclassableOnce, strip_illegal_chars from ..util.types import ( CachedPresence, Hat, LegacyMessageType, MessageOrPresenceTypeVar, MucAffiliation, MucRole, ) if TYPE_CHECKING: from .room import LegacyMUC def strip_non_printable(nickname: str): new = ( "".join(x for x in nickname if x in string.printable) + f"-slidge-{hash(nickname)}" ) warnings.warn(f"Could not use {nickname} as a nickname, using {new}") return new class LegacyParticipant( StoredAttributeMixin, PresenceMixin, MessageMixin, ChatterDiscoMixin, metaclass=SubclassableOnce, ): """ A legacy participant of a legacy group chat. """ mtype: MessageTypes = "groupchat" _can_send_carbon = False USE_STANZA_ID = True STRIP_SHORT_DELAY = False pk: int def __init__( self, muc: "LegacyMUC", nickname: Optional[str] = None, is_user=False, is_system=False, role: MucRole = "participant", affiliation: MucAffiliation = "member", resource: str | None = None, nickname_no_illegal: str | None = None, ): self.session = session = muc.session self.xmpp = session.xmpp super().__init__() self._hats = list[Hat]() self.muc = muc self._role = role self._affiliation = affiliation self.is_user: bool = is_user self.is_system: bool = is_system self._nickname = nickname if resource is None: self.__update_jid(nickname) else: assert nickname_no_illegal is not None self._nickname_no_illegal = nickname_no_illegal self.jid = JID(self.muc.jid) self.jid.resource = resource log.debug("Instantiation of: %r", self) self.contact: Optional["LegacyContact"] = None # we track if we already sent a presence for this participant. # if we didn't, we send it before the first message. # this way, event in plugins that don't map "user has joined" events, # we send a "join"-presence from the participant before the first message self._presence_sent: bool = False self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid}") self.__part_store = self.xmpp.store.participants @property def contact_pk(self) -> Optional[int]: # type:ignore if self.contact: return self.contact.contact_pk return None @property def user_jid(self): return self.session.user_jid def __repr__(self): return f"" @property def affiliation(self): return self._affiliation @affiliation.setter def affiliation(self, affiliation: MucAffiliation): if self._affiliation == affiliation: return self._affiliation = affiliation if not self.muc._participants_filled: return self.__part_store.set_affiliation(self.pk, affiliation) if not self._presence_sent: return self.send_last_presence(force=True, no_cache_online=True) def send_affiliation_change(self): # internal use by slidge msg = self._make_message() msg["muc"]["affiliation"] = self._affiliation msg["type"] = "normal" if not self.muc.is_anonymous and not self.is_system: if self.contact: msg["muc"]["jid"] = self.contact.jid else: warnings.warn( f"Private group but no 1:1 JID associated to '{self}'", ) self._send(msg) @property def role(self): return self._role @role.setter def role(self, role: MucRole): if self._role == role: return self._role = role if not self.muc._participants_filled: return self.__part_store.set_role(self.pk, role) if not self._presence_sent: return self.send_last_presence(force=True, no_cache_online=True) def set_hats(self, hats: list[Hat]): if self._hats == hats: return self._hats = hats if not self.muc._participants_filled: return self.__part_store.set_hats(self.pk, hats) if not self._presence_sent: return self.send_last_presence(force=True, no_cache_online=True) def __update_jid(self, unescaped_nickname: Optional[str]): if not unescaped_nickname: self.jid = JID(self.muc.jid) if self.is_system: self._nickname_no_illegal = "" else: warnings.warn( "Only the system participant is allowed to not have a nickname" ) nickname = f"unnamed-{uuid.uuid4()}" self.jid.resource = self._nickname_no_illegal = nickname return self._nickname_no_illegal, self.jid = escape_nickname( self.muc.jid, unescaped_nickname, ) def send_configuration_change(self, codes: tuple[int]): if not self.is_system: raise RuntimeError("This is only possible for the system participant") msg = self._make_message() msg["muc"]["status_codes"] = codes self._send(msg) @property def nickname(self): return self._nickname @nickname.setter def nickname(self, new_nickname: str): old = self._nickname if new_nickname == old: return cache = getattr(self, "_last_presence", None) if cache: last_seen = cache.last_seen kwargs = cache.presence_kwargs else: last_seen = None kwargs = {} kwargs["status_codes"] = {303} p = self._make_presence(ptype="unavailable", last_seen=last_seen, **kwargs) # in this order so pfrom=old resource and we actually use the escaped nick # in the muc/item/nick element self.__update_jid(new_nickname) p["muc"]["item"]["nick"] = self.jid.resource self._send(p) self._nickname = new_nickname kwargs["status_codes"] = set() p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs) self._send(p) self.__part_store.update(self) def _make_presence( self, *, last_seen: Optional[datetime] = None, status_codes: Optional[set[int]] = None, user_full_jid: Optional[JID] = None, **presence_kwargs, ): p = super()._make_presence(last_seen=last_seen, **presence_kwargs) p["muc"]["affiliation"] = self.affiliation p["muc"]["role"] = self.role if self._hats: p["hats"].add_hats(self._hats) codes = status_codes or set() if self.is_user: codes.add(110) if not self.muc.is_anonymous and not self.is_system: if self.is_user: if user_full_jid: p["muc"]["jid"] = user_full_jid else: jid = JID(self.user_jid) try: jid.resource = next( iter(self.muc.get_user_resources()) # type:ignore ) except StopIteration: jid.resource = "pseudo-resource" p["muc"]["jid"] = self.user_jid codes.add(100) elif self.contact: p["muc"]["jid"] = self.contact.jid if a := self.contact.get_avatar(): p["vcard_temp_update"]["photo"] = a.id else: warnings.warn( f"Private group but no 1:1 JID associated to '{self}'", ) if self.is_user and (hash_ := self.session.user.avatar_hash): p["vcard_temp_update"]["photo"] = hash_ p["muc"]["status_codes"] = codes return p @property def DISCO_NAME(self): return self.nickname def __send_presence_if_needed( self, stanza: Union[Message, Presence], full_jid: JID, archive_only: bool ): if ( archive_only or self.is_system or self.is_user or self._presence_sent or stanza["subject"] ): return if isinstance(stanza, Message): if stanza.get_plugin("muc", check=True): return self.send_initial_presence(full_jid) @cached_property def __occupant_id(self): if self.contact: return self.contact.jid elif self.is_user: return "slidge-user" elif self.is_system: return "room" else: return str(uuid.uuid4()) def _send( self, stanza: MessageOrPresenceTypeVar, full_jid: Optional[JID] = None, archive_only=False, legacy_msg_id=None, **send_kwargs, ) -> MessageOrPresenceTypeVar: if stanza.get_from().resource: stanza["occupant-id"]["id"] = self.__occupant_id else: stanza["occupant-id"]["id"] = "room" self.__add_nick_element(stanza) if not self.is_user and isinstance(stanza, Presence): if stanza["type"] == "unavailable" and not self._presence_sent: return stanza # type:ignore self._presence_sent = True self.__part_store.set_presence_sent(self.pk) if full_jid: stanza["to"] = full_jid self.__send_presence_if_needed(stanza, full_jid, archive_only) if self.is_user: assert stanza.stream is not None stanza.stream.send(stanza, use_filters=False) else: stanza.send() else: if hasattr(self.muc, "archive") and isinstance(stanza, Message): self.muc.archive.add(stanza, self, archive_only, legacy_msg_id) if archive_only: return stanza for user_full_jid in self.muc.user_full_jids(): stanza = copy(stanza) stanza["to"] = user_full_jid self.__send_presence_if_needed(stanza, user_full_jid, archive_only) stanza.send() return stanza def mucadmin_item(self): item = MUCAdminItem() item["nick"] = self.nickname item["affiliation"] = self.affiliation item["role"] = self.role if not self.muc.is_anonymous: if self.is_user: item["jid"] = self.user_jid.bare elif self.contact: item["jid"] = self.contact.jid.bare else: warnings.warn( ( f"Public group but no contact JID associated to {self.jid} in" f" {self}" ), ) return item def __add_nick_element(self, stanza: Union[Presence, Message]): if (nick := self._nickname_no_illegal) != self.jid.resource: n = self.xmpp.plugin["xep_0172"].stanza.UserNick() n["nick"] = nick stanza.append(n) def _get_last_presence(self) -> Optional[CachedPresence]: own = super()._get_last_presence() if own is None and self.contact: return self.contact._get_last_presence() return own def send_initial_presence( self, full_jid: JID, nick_change=False, presence_id: Optional[str] = None, ): """ Called when the user joins a MUC, as a mechanism to indicate to the joining XMPP client the list of "participants". Can be called this to trigger a "participant has joined the group" event. :param full_jid: Set this to only send to a specific user XMPP resource. :param nick_change: Used when the user joins and the MUC renames them (code 210) :param presence_id: set the presence ID. used internally by slidge """ # MUC status codes: https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes codes = set() if nick_change: codes.add(210) if self.is_user: # the "initial presence" of the user has to be vanilla, as it is # a crucial part of the MUC join sequence for XMPP clients. kwargs = {} else: cache = self._get_last_presence() self.log.debug("Join muc, initial presence: %s", cache) if cache: ptype = cache.ptype if ptype == "unavailable": return kwargs = dict( last_seen=cache.last_seen, pstatus=cache.pstatus, pshow=cache.pshow ) else: kwargs = {} p = self._make_presence( status_codes=codes, user_full_jid=full_jid, **kwargs, # type:ignore ) if presence_id: p["id"] = presence_id self._send(p, full_jid) def leave(self): """ Call this when the participant leaves the room """ self.muc.remove_participant(self) def kick(self, reason: str | None = None): """ Call this when the participant is kicked from the room """ self.muc.remove_participant(self, kick=True, reason=reason) def ban(self, reason: str | None = None): """ Call this when the participant is banned from the room """ self.muc.remove_participant(self, ban=True, reason=reason) def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None): if self.contact is not None: return self.contact.get_disco_info() return super().get_disco_info() def moderate(self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None): xmpp_id = self._legacy_to_xmpp(legacy_msg_id) multi = self.xmpp.store.multi.get_xmpp_ids(self.session.user_pk, xmpp_id) if multi is None: msg_ids = [xmpp_id] else: msg_ids = multi + [xmpp_id] for i in msg_ids: m = self.muc.get_system_participant()._make_message() m["retract"]["id"] = i if self.is_system: m["retract"].enable("moderated") else: m["retract"]["moderated"]["by"] = self.jid m["retract"]["moderated"]["occupant-id"]["id"] = self.__occupant_id if reason: m["retract"]["reason"] = reason self._send(m) def set_room_subject( self, subject: str, full_jid: Optional[JID] = None, when: Optional[datetime] = None, update_muc=True, ): if update_muc: self.muc._subject = subject # type: ignore self.muc.subject_setter = self.nickname self.muc.subject_date = when msg = self._make_message() if when is not None: msg["delay"].set_stamp(when) msg["delay"]["from"] = self.muc.jid msg["subject"] = subject or str(self.muc.name) self._send(msg, full_jid) @classmethod def from_store( cls, session, stored: Participant, contact: Optional[LegacyContact] = None, muc: Optional["LegacyMUC"] = None, ) -> Self: from slidge.group.room import LegacyMUC if muc is None: muc = LegacyMUC.get_self_or_unique_subclass().from_store( session, stored.room ) part = cls( muc, stored.nickname, role=stored.role, affiliation=stored.affiliation, resource=stored.resource, nickname_no_illegal=stored.nickname_no_illegal, ) part.pk = stored.id if contact is not None: part.contact = contact elif stored.contact is not None: contact = LegacyContact.get_self_or_unique_subclass().from_store( session, stored.contact ) part.contact = contact part.is_user = stored.is_user if (data := stored.extra_attributes) is not None: muc.deserialize_extra_attributes(data) part._presence_sent = stored.presence_sent part._hats = [Hat(h.uri, h.title) for h in stored.hats] return part def escape_nickname(muc_jid: JID, nickname: str) -> tuple[str, JID]: nickname = nickname_no_illegal = strip_illegal_chars(nickname) jid = JID(muc_jid) try: jid.resource = nickname except InvalidJID: nickname = nickname.encode("punycode").decode() try: jid.resource = nickname except InvalidJID: # at this point there still might be control chars jid.resource = strip_non_printable(nickname) return nickname_no_illegal, jid log = logging.getLogger(__name__) slidge/slidge/group/room.py000066400000000000000000001405531477703150600163000ustar00rootroot00000000000000import json import logging import re import string import warnings from copy import copy from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Self, Union from uuid import uuid4 from slixmpp import JID, Iq, Message, Presence from slixmpp.exceptions import IqError, IqTimeout, XMPPError from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0060.stanza import Item from slixmpp.plugins.xep_0082 import parse as str_to_datetime from slixmpp.plugins.xep_0469.stanza import NS as PINNING_NS from slixmpp.plugins.xep_0492.stanza import NS as NOTIFY_NS from slixmpp.plugins.xep_0492.stanza import WhenLiteral from slixmpp.xmlstream import ET from ..contact.contact import LegacyContact from ..contact.roster import ContactIsUser from ..core import config from ..core.mixins import StoredAttributeMixin from ..core.mixins.avatar import AvatarMixin from ..core.mixins.db import UpdateInfoMixin from ..core.mixins.disco import ChatterDiscoMixin from ..core.mixins.lock import NamedLockMixin from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin from ..db.models import Room from ..util import ABCSubclassableOnceAtMost from ..util.jid_escaping import unescape_node from ..util.types import ( HoleBound, LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType, Mention, MucAffiliation, MucType, ) from ..util.util import deprecated, timeit, with_session from .archive import MessageArchive from .participant import LegacyParticipant, escape_nickname if TYPE_CHECKING: from ..core.gateway import BaseGateway from ..core.session import BaseSession ADMIN_NS = "http://jabber.org/protocol/muc#admin" SubjectSetterType = Union[str, None, "LegacyContact", "LegacyParticipant"] class LegacyMUC( Generic[ LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType ], UpdateInfoMixin, StoredAttributeMixin, AvatarMixin, NamedLockMixin, ChatterDiscoMixin, ReactionRecipientMixin, ThreadRecipientMixin, metaclass=ABCSubclassableOnceAtMost, ): """ A room, a.k.a. a Multi-User Chat. MUC instances are obtained by calling :py:meth:`slidge.group.bookmarks.LegacyBookmarks` on the user's :py:class:`slidge.core.session.BaseSession`. """ max_history_fetch = 100 type = MucType.CHANNEL is_group = True DISCO_TYPE = "text" DISCO_CATEGORY = "conference" DISCO_NAME = "unnamed-room" STABLE_ARCHIVE = False """ Because legacy events like reactions, editions, etc. don't all map to a stanza with a proper legacy ID, slidge usually cannot guarantee the stability of the archive across restarts. Set this to True if you know what you're doing, but realistically, this can't be set to True until archive is permanently stored on disk by slidge. This is just a flag on archive responses that most clients ignore anyway. """ KEEP_BACKFILLED_PARTICIPANTS = False """ Set this to ``True`` if the participant list is not full after calling ``fill_participants()``. This is a workaround for networks with huge participant lists which do not map really well the MUCs where all presences are sent on join. It allows to ensure that the participants that last spoke (within the ``fill_history()`` method are effectively participants, thus making possible for XMPP clients to fetch their avatars. """ _ALL_INFO_FILLED_ON_STARTUP = False """ Set this to true if the fill_participants() / fill_participants() design does not fit the legacy API, ie, no lazy loading of the participant list and history. """ HAS_DESCRIPTION = True """ Set this to false if the legacy network does not allow setting a description for the group. In this case the description field will not be present in the room configuration form. """ HAS_SUBJECT = True """ Set this to false if the legacy network does not allow setting a subject (sometimes also called topic) for the group. In this case, as a subject is recommended by :xep:`0045` ("SHALL"), the description (or the group name as ultimate fallback) will be used as the room subject. By setting this to false, an error will be returned when the :term:`User` tries to set the room subject. """ _avatar_bare_jid = True archive: MessageArchive def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID): self.session = session self.xmpp: "BaseGateway" = session.xmpp self.legacy_id = legacy_id self.jid = jid self._user_resources = set[str]() self.Participant = LegacyParticipant.get_self_or_unique_subclass() self._subject = "" self._subject_setter: Optional[str] = None self.pk: Optional[int] = None self._user_nick: Optional[str] = None self._participants_filled = False self._history_filled = False self._description = "" self._subject_date: Optional[datetime] = None self.__participants_store = self.xmpp.store.participants self.__store = self.xmpp.store.rooms self._n_participants: Optional[int] = None self.log = logging.getLogger(self.jid.bare) self._set_logger_name() super().__init__() @property def n_participants(self): return self._n_participants @n_participants.setter def n_participants(self, n_participants: Optional[int]): if self._n_participants == n_participants: return self._n_participants = n_participants if self._updating_info: return assert self.pk is not None self.__store.update_n_participants(self.pk, n_participants) @property def user_jid(self): return self.session.user_jid def _set_logger_name(self): self.log = logging.getLogger(f"{self.user_jid}:muc:{self}") def __repr__(self): return f"" @property def subject_date(self) -> Optional[datetime]: return self._subject_date @subject_date.setter def subject_date(self, when: Optional[datetime]) -> None: self._subject_date = when if self._updating_info: return assert self.pk is not None self.__store.update_subject_date(self.pk, when) def __send_configuration_change(self, codes): part = self.get_system_participant() part.send_configuration_change(codes) @property def user_nick(self): return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node @user_nick.setter def user_nick(self, nick: str): self._user_nick = nick if not self._updating_info: self.__store.update_user_nick(self.pk, nick) def add_user_resource(self, resource: str) -> None: self._user_resources.add(resource) assert self.pk is not None self.__store.set_resource(self.pk, self._user_resources) def get_user_resources(self) -> set[str]: return self._user_resources def remove_user_resource(self, resource: str) -> None: self._user_resources.remove(resource) assert self.pk is not None self.__store.set_resource(self.pk, self._user_resources) async def __fill_participants(self): if self._participants_filled: return assert self.pk is not None async with self.lock("fill participants"): self._participants_filled = True async for p in self.fill_participants(): self.__participants_store.update(p) self.__store.set_participants_filled(self.pk) async def get_participants(self) -> AsyncIterator[LegacyParticipant]: assert self.pk is not None if self._participants_filled: for db_participant in self.xmpp.store.participants.get_all( self.pk, user_included=True ): participant = self.Participant.from_store( self.session, db_participant, muc=self ) yield participant return async with self.lock("fill participants"): self._participants_filled = True # We only fill the participants list if/when the MUC is first # joined by an XMPP client. But we may have instantiated some before. resources = set[str]() async for participant in self.fill_participants(): # TODO: batch SQL update at the end of this function for perf? self.__store_participant(participant) yield participant resources.add(participant.jid.resource) for db_participant in self.xmpp.store.participants.get_all( self.pk, user_included=True ): participant = self.Participant.from_store( self.session, db_participant, muc=self ) if participant.jid.resource not in resources: yield participant self.__store.set_participants_filled(self.pk) return async def __fill_history(self): async with self.lock("fill history"): if self._history_filled: log.debug("History has already been fetched %s", self) return log.debug("Fetching history for %s", self) try: before, after = self.archive.get_hole_bounds() if before is not None: before = before._replace( id=self.xmpp.LEGACY_MSG_ID_TYPE(before.id) # type:ignore ) if after is not None: after = after._replace( id=self.xmpp.LEGACY_MSG_ID_TYPE(after.id) # type:ignore ) await self.backfill(before, after) except NotImplementedError: return except Exception as e: log.exception("Could not backfill: %s", e) assert self.pk is not None self.__store.set_history_filled(self.pk, True) self._history_filled = True @property def name(self): return self.DISCO_NAME @name.setter def name(self, n: str): if self.DISCO_NAME == n: return self.DISCO_NAME = n self._set_logger_name() self.__send_configuration_change((104,)) if self._updating_info: return assert self.pk is not None self.__store.update_name(self.pk, n) @property def description(self): return self._description @description.setter def description(self, d: str): if self._description == d: return self._description = d self.__send_configuration_change((104,)) if self._updating_info: return assert self.pk is not None self.__store.update_description(self.pk, d) def on_presence_unavailable(self, p: Presence): pto = p.get_to() if pto.bare != self.jid.bare: return pfrom = p.get_from() if pfrom.bare != self.user_jid.bare: return if (resource := pfrom.resource) in self._user_resources: if pto.resource != self.user_nick: self.log.debug( "Received 'leave group' request but with wrong nickname. %s", p ) self.remove_user_resource(resource) else: self.log.debug( "Received 'leave group' request but resource was not listed. %s", p ) async def update_info(self): """ Fetch information about this group from the legacy network This is awaited on MUC instantiation, and should be overridden to update the attributes of the group chat, like title, subject, number of participants etc. To take advantage of the slidge avatar cache, you can check the .avatar property to retrieve the "legacy file ID" of the cached avatar. If there is no change, you should not call :py:meth:`slidge.core.mixins.avatar.AvatarMixin.set_avatar()` or attempt to modify the :attr:.avatar property. """ raise NotImplementedError async def backfill( self, after: Optional[HoleBound] = None, before: Optional[HoleBound] = None, ): """ Override this if the legacy network provide server-side group archives. In it, send history messages using ``self.get_participant(xxx).send_xxxx``, with the ``archive_only=True`` kwarg. This is only called once per slidge run for a given group. :param after: Fetch messages after this one. If ``None``, it's up to you to decide how far you want to go in the archive. If it's not ``None``, it means slidge has some messages in this archive and you should really try to complete it to avoid "holes" in the history of this group. :param before: Fetch messages before this one. If ``None``, fetch all messages up to the most recent one """ raise NotImplementedError async def fill_participants(self) -> AsyncIterator[LegacyParticipant]: """ This method should yield the list of all members of this group. Typically, use ``participant = self.get_participant()``, self.get_participant_by_contact(), of self.get_user_participant(), and update their affiliation, hats, etc. before yielding them. """ return yield @property def subject(self): return self._subject @subject.setter def subject(self, s: str): if s == self._subject: return self.__get_subject_setter_participant().set_room_subject( s, None, self.subject_date, False ) self._subject = s if self._updating_info: return assert self.pk is not None self.__store.update_subject(self.pk, s) @property def is_anonymous(self): return self.type == MucType.CHANNEL @property def subject_setter(self) -> Optional[str]: return self._subject_setter @subject_setter.setter def subject_setter(self, subject_setter: SubjectSetterType) -> None: if isinstance(subject_setter, LegacyContact): subject_setter = subject_setter.name elif isinstance(subject_setter, LegacyParticipant): subject_setter = subject_setter.nickname if subject_setter == self._subject_setter: return assert isinstance(subject_setter, str | None) self._subject_setter = subject_setter if self._updating_info: return assert self.pk is not None self.__store.update_subject_setter(self.pk, subject_setter) def __get_subject_setter_participant(self) -> LegacyParticipant: if self._subject_setter is None: return self.get_system_participant() return self.Participant(self, self._subject_setter) def features(self): features = [ "http://jabber.org/protocol/muc", "http://jabber.org/protocol/muc#stable_id", "http://jabber.org/protocol/muc#self-ping-optimization", "urn:xmpp:mam:2", "urn:xmpp:mam:2#extended", "urn:xmpp:sid:0", "muc_persistent", "vcard-temp", "urn:xmpp:ping", "urn:xmpp:occupant-id:0", "jabber:iq:register", self.xmpp.plugin["xep_0425"].stanza.NS, ] if self.type == MucType.GROUP: features.extend(["muc_membersonly", "muc_nonanonymous", "muc_hidden"]) elif self.type == MucType.CHANNEL: features.extend(["muc_open", "muc_semianonymous", "muc_public"]) elif self.type == MucType.CHANNEL_NON_ANONYMOUS: features.extend(["muc_open", "muc_nonanonymous", "muc_public"]) return features async def extended_features(self): is_group = self.type == MucType.GROUP form = self.xmpp.plugin["xep_0004"].make_form(ftype="result") form.add_field( "FORM_TYPE", "hidden", value="http://jabber.org/protocol/muc#roominfo" ) form.add_field("muc#roomconfig_persistentroom", "boolean", value=True) form.add_field("muc#roomconfig_changesubject", "boolean", value=False) form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch)) form.add_field("muc#roominfo_subjectmod", "boolean", value=False) if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled: assert self.pk is not None n: Optional[int] = self.__participants_store.get_count(self.pk) else: n = self._n_participants if n is not None: form.add_field("muc#roominfo_occupants", value=str(n)) if d := self.description: form.add_field("muc#roominfo_description", value=d) if s := self.subject: form.add_field("muc#roominfo_subject", value=s) if self._set_avatar_task: await self._set_avatar_task avatar = self.get_avatar() if avatar and (h := avatar.id): form.add_field( "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h ) form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h]) form.add_field("muc#roomconfig_membersonly", "boolean", value=is_group) form.add_field( "muc#roomconfig_whois", "list-single", value="moderators" if self.is_anonymous else "anyone", ) form.add_field("muc#roomconfig_publicroom", "boolean", value=not is_group) form.add_field("muc#roomconfig_allowpm", "boolean", value=False) r = [form] if reaction_form := await self.restricted_emoji_extended_feature(): r.append(reaction_form) return r def shutdown(self): _, user_jid = escape_nickname(self.jid, self.user_nick) for user_full_jid in self.user_full_jids(): presence = self.xmpp.make_presence( pfrom=user_jid, pto=user_full_jid, ptype="unavailable" ) presence["muc"]["affiliation"] = "none" presence["muc"]["role"] = "none" presence["muc"]["status_codes"] = {110, 332} presence.send() def user_full_jids(self): for r in self._user_resources: j = JID(self.user_jid) j.resource = r yield j @property def user_muc_jid(self): _, user_muc_jid = escape_nickname(self.jid, self.user_nick) return user_muc_jid def _legacy_to_xmpp(self, legacy_id: LegacyMessageType): return self.xmpp.store.sent.get_group_xmpp_id( self.session.user_pk, str(legacy_id) ) or self.session.legacy_to_xmpp_msg_id(legacy_id) async def echo( self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None ): origin_id = msg.get_origin_id() msg.set_from(self.user_muc_jid) msg.set_id(msg.get_id()) if origin_id: # because of slixmpp internal magic, we need to do this to ensure the origin_id # is present set_origin_id(msg, origin_id) if legacy_msg_id: msg["stanza_id"]["id"] = self.session.legacy_to_xmpp_msg_id(legacy_msg_id) else: msg["stanza_id"]["id"] = str(uuid4()) msg["stanza_id"]["by"] = self.jid msg["occupant-id"]["id"] = "slidge-user" self.archive.add(msg, await self.get_user_participant()) for user_full_jid in self.user_full_jids(): self.log.debug("Echoing to %s", user_full_jid) msg = copy(msg) msg.set_to(user_full_jid) msg.send() def _get_cached_avatar_id(self): if self.pk is None: return None return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk) def _post_avatar_update(self) -> None: if self.pk is None: return assert self.pk is not None self.xmpp.store.rooms.set_avatar( self.pk, self._avatar_pk, None if self.avatar_id is None else str(self.avatar_id), ) self.__send_configuration_change((104,)) self._send_room_presence() def _send_room_presence(self, user_full_jid: Optional[JID] = None): if user_full_jid is None: tos = self.user_full_jids() else: tos = [user_full_jid] for to in tos: p = self.xmpp.make_presence(pfrom=self.jid, pto=to) if (avatar := self.get_avatar()) and (h := avatar.id): p["vcard_temp_update"]["photo"] = h else: p["vcard_temp_update"]["photo"] = "" p.send() @timeit @with_session async def join(self, join_presence: Presence): user_full_jid = join_presence.get_from() requested_nickname = join_presence.get_to().resource client_resource = user_full_jid.resource if client_resource in self._user_resources: self.log.debug("Received join from a resource that is already joined.") self.add_user_resource(client_resource) if not requested_nickname or not client_resource: raise XMPPError("jid-malformed", by=self.jid) self.log.debug( "Resource %s of %s wants to join room %s with nickname %s", client_resource, self.user_jid, self.legacy_id, requested_nickname, ) user_nick = self.user_nick user_participant = None async for participant in self.get_participants(): if participant.is_user: user_participant = participant continue participant.send_initial_presence(full_jid=user_full_jid) if user_participant is None: user_participant = await self.get_user_participant() if not user_participant.is_user: # type:ignore self.log.warning("is_user flag not set participant on user_participant") user_participant.is_user = True # type:ignore user_participant.send_initial_presence( user_full_jid, presence_id=join_presence["id"], nick_change=user_nick != requested_nickname, ) history_params = join_presence["muc_join"]["history"] maxchars = int_or_none(history_params["maxchars"]) maxstanzas = int_or_none(history_params["maxstanzas"]) seconds = int_or_none(history_params["seconds"]) try: since = self.xmpp.plugin["xep_0082"].parse(history_params["since"]) except ValueError: since = None if seconds is not None: since = datetime.now() - timedelta(seconds=seconds) if equals_zero(maxchars) or equals_zero(maxstanzas): log.debug("Joining client does not want any old-school MUC history-on-join") else: self.log.debug("Old school history fill") await self.__fill_history() await self.__old_school_history( user_full_jid, maxchars=maxchars, maxstanzas=maxstanzas, since=since, ) self.__get_subject_setter_participant().set_room_subject( self._subject if self.HAS_SUBJECT else (self.description or self.name), user_full_jid, self.subject_date, ) if t := self._set_avatar_task: await t self._send_room_presence(user_full_jid) async def get_user_participant(self, **kwargs) -> "LegacyParticipantType": """ Get the participant representing the gateway user :param kwargs: additional parameters for the :class:`.Participant` construction (optional) :return: """ p = await self.get_participant(self.user_nick, is_user=True, **kwargs) self.__store_participant(p) return p def __store_participant(self, p: "LegacyParticipantType") -> None: # we don't want to update the participant list when we're filling history if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"): return assert self.pk is not None p.pk = self.__participants_store.add(self.pk, p.nickname) self.__participants_store.update(p) if p._hats: self.__participants_store.set_hats(p.pk, p._hats) async def get_participant( self, nickname: str, raise_if_not_found=False, fill_first=False, store=True, **kwargs, ) -> "LegacyParticipantType": """ Get a participant by their nickname. In non-anonymous groups, you probably want to use :meth:`.LegacyMUC.get_participant_by_contact` instead. :param nickname: Nickname of the participant (used as resource part in the MUC) :param raise_if_not_found: Raise XMPPError("item-not-found") if they are not in the participant list (internal use by slidge, plugins should not need that) :param fill_first: Ensure :meth:`.LegacyMUC.fill_participants()` has been called first (internal use by slidge, plugins should not need that) :param store: persistently store the user in the list of MUC participants :param kwargs: additional parameters for the :class:`.Participant` construction (optional) :return: """ if fill_first and not self._participants_filled: async for _ in self.get_participants(): pass if self.pk is not None: with self.xmpp.store.session(): stored = self.__participants_store.get_by_nickname( self.pk, nickname ) or self.__participants_store.get_by_resource(self.pk, nickname) if stored is not None: return self.Participant.from_store(self.session, stored) if raise_if_not_found: raise XMPPError("item-not-found") p = self.Participant(self, nickname, **kwargs) if store and not self._updating_info: self.__store_participant(p) if ( not self.get_lock("fill participants") and not self.get_lock("fill history") and self._participants_filled and not p.is_user and not p.is_system ): p.send_affiliation_change() return p def get_system_participant(self) -> "LegacyParticipantType": """ Get a pseudo-participant, representing the room itself Can be useful for events that cannot be mapped to a participant, e.g. anonymous moderation events, or announces from the legacy service :return: """ return self.Participant(self, is_system=True) async def get_participant_by_contact( self, c: "LegacyContact", **kwargs ) -> "LegacyParticipantType": """ Get a non-anonymous participant. This is what should be used in non-anonymous groups ideally, to ensure that the Contact jid is associated to this participant :param c: The :class:`.LegacyContact` instance corresponding to this contact :param kwargs: additional parameters for the :class:`.Participant` construction (optional) :return: """ await self.session.contacts.ready if self.pk is not None: c._LegacyContact__ensure_pk() # type: ignore assert c.contact_pk is not None with self.__store.session(): stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk) if stored is not None: return self.Participant.from_store( self.session, stored, muc=self, contact=c ) nickname = c.name or unescape_node(c.jid_username) if self.pk is None: nick_available = True else: nick_available = self.__store.nickname_is_available(self.pk, nickname) if not nick_available: self.log.debug("Nickname conflict") nickname = f"{nickname} ({c.jid_username})" p = self.Participant(self, nickname, **kwargs) p.contact = c if self._updating_info: return p self.__store_participant(p) # FIXME: this is not great but given the current design, # during participants fill and history backfill we do not # want to send presence, because we might :update affiliation # and role afterwards. # We need a refactor of the MUC class… later™ if ( self._participants_filled and not self.get_lock("fill participants") and not self.get_lock("fill history") ): p.send_last_presence(force=True, no_cache_online=True) return p async def get_participant_by_legacy_id( self, legacy_id: LegacyUserIdType, **kwargs ) -> "LegacyParticipantType": try: c = await self.session.contacts.by_legacy_id(legacy_id) except ContactIsUser: return await self.get_user_participant(**kwargs) return await self.get_participant_by_contact(c, **kwargs) def remove_participant( self, p: "LegacyParticipantType", kick=False, ban=False, reason: str | None = None, ): """ Call this when a participant leaves the room :param p: The participant :param kick: Whether the participant left because they were kicked :param ban: Whether the participant left because they were banned :param reason: Optionally, a reason why the participant was removed. """ if kick and ban: raise TypeError("Either kick or ban") self.__participants_store.delete(p.pk) if kick: codes = {307} elif ban: codes = {301} else: codes = None presence = p._make_presence(ptype="unavailable", status_codes=codes) p._affiliation = "outcast" if ban else "none" p._role = "none" if reason: presence["muc"].set_item_attr("reason", reason) p._send(presence) def rename_participant(self, old_nickname: str, new_nickname: str): assert self.pk is not None with self.xmpp.store.session(): stored = self.__participants_store.get_by_nickname(self.pk, old_nickname) if stored is None: self.log.debug("Tried to rename a participant that we didn't know") return p = self.Participant.from_store(self.session, stored) if p.nickname == old_nickname: p.nickname = new_nickname async def __old_school_history( self, full_jid: JID, maxchars: Optional[int] = None, maxstanzas: Optional[int] = None, seconds: Optional[int] = None, since: Optional[datetime] = None, ): """ Old-style history join (internal slidge use) :param full_jid: :param maxchars: :param maxstanzas: :param seconds: :param since: :return: """ if since is None: if seconds is None: start_date = datetime.now(tz=timezone.utc) - timedelta(days=1) else: start_date = datetime.now(tz=timezone.utc) - timedelta(seconds=seconds) else: start_date = since or datetime.now(tz=timezone.utc) - timedelta(days=1) for h_msg in self.archive.get_all( start_date=start_date, end_date=None, last_page_n=maxstanzas ): msg = h_msg.stanza_component_ns msg["delay"]["stamp"] = h_msg.when msg.set_to(full_jid) self.xmpp.send(msg, False) async def send_mam(self, iq: Iq): await self.__fill_history() form_values = iq["mam"]["form"].get_values() start_date = str_to_datetime_or_none(form_values.get("start")) end_date = str_to_datetime_or_none(form_values.get("end")) after_id = form_values.get("after-id") before_id = form_values.get("before-id") sender = form_values.get("with") ids = form_values.get("ids") or () if max_str := iq["mam"]["rsm"]["max"]: try: max_results = int(max_str) except ValueError: max_results = None else: max_results = None after_id_rsm = iq["mam"]["rsm"]["after"] after_id = after_id_rsm or after_id before_rsm = iq["mam"]["rsm"]["before"] if before_rsm is not None and max_results is not None: last_page_n = max_results # - before_rsm is True means the empty element , which means # "last page in chronological order", cf https://xmpp.org/extensions/xep-0059.html#backwards # - before_rsm == "an ID" means an ID if before_rsm is not True: before_id = before_rsm else: last_page_n = None first = None last = None count = 0 it = self.archive.get_all( start_date, end_date, before_id, after_id, ids, last_page_n, sender, bool(iq["mam"]["flip_page"]), ) for history_msg in it: last = xmpp_id = history_msg.id if first is None: first = xmpp_id wrapper_msg = self.xmpp.make_message(mfrom=self.jid, mto=iq.get_from()) wrapper_msg["mam_result"]["queryid"] = iq["mam"]["queryid"] wrapper_msg["mam_result"]["id"] = xmpp_id wrapper_msg["mam_result"].append(history_msg.forwarded()) wrapper_msg.send() count += 1 if max_results and count == max_results: break if max_results: try: next(it) except StopIteration: complete = True else: complete = False else: complete = True reply = iq.reply() if not self.STABLE_ARCHIVE: reply["mam_fin"]["stable"] = "false" if complete: reply["mam_fin"]["complete"] = "true" reply["mam_fin"]["rsm"]["first"] = first reply["mam_fin"]["rsm"]["last"] = last reply["mam_fin"]["rsm"]["count"] = str(count) reply.send() async def send_mam_metadata(self, iq: Iq): await self.__fill_history() await self.archive.send_metadata(iq) async def kick_resource(self, r: str): """ Kick a XMPP client of the user. (slidge internal use) :param r: The resource to kick """ pto = JID(self.user_jid) pto.resource = r p = self.xmpp.make_presence( pfrom=(await self.get_user_participant()).jid, pto=pto ) p["type"] = "unavailable" p["muc"]["affiliation"] = "none" p["muc"]["role"] = "none" p["muc"]["status_codes"] = {110, 333} p.send() async def __get_bookmark(self) -> Item | None: item = Item() item["id"] = self.jid iq = Iq(stype="get", sfrom=self.user_jid, sto=self.user_jid) iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS iq["pubsub"]["items"].append(item) try: ans = await self.xmpp["xep_0356"].send_privileged_iq(iq) if len(ans["pubsub"]["items"]) != 1: return None # this below creates the item if it wasn't here already # (slixmpp annoying magic) item = ans["pubsub"]["items"]["item"] item["id"] = self.jid return item except IqTimeout as exc: warnings.warn(f"Cannot fetch bookmark for {self.user_jid}: timeout") return None except IqError as exc: warnings.warn(f"Cannot fetch bookmark for {self.user_jid}: {exc}") return None except PermissionError: warnings.warn( "IQ privileges (XEP0356) are not set, we cannot fetch the user bookmarks" ) return None async def add_to_bookmarks( self, auto_join=True, invite=False, preserve=True, pin: bool | None = None, notify: WhenLiteral | None = None, ): """ Add the MUC to the user's XMPP bookmarks (:xep:`0402') This requires that slidge has the IQ privileged set correctly on the XMPP server :param auto_join: whether XMPP clients should automatically join this MUC on startup. In theory, XMPP clients will receive a "push" notification when this is called, and they will join if they are online. :param invite: send an invitation to join this MUC emanating from the gateway. While this should not be strictly necessary, it can help for clients that do not support :xep:`0402`, or that have 'do not honor bookmarks auto-join' turned on in their settings. :param preserve: preserve auto-join and bookmarks extensions set by the user outside slidge :param pin: Pin the group chat bookmark :xep:`0469`. Requires privileged entity. If set to ``None`` (default), the bookmark pinning status will be untouched. :param notify: Chat notification setting: :xep:`0492`. Requires privileged entity. If set to ``None`` (default), the setting will be untouched. Only the "global" notification setting is supported (ie, per client type is not possible). """ existing = await self.__get_bookmark() if preserve else None new = Item() new["id"] = self.jid new["conference"]["nick"] = self.user_nick if existing is None: change = True new["conference"]["autojoin"] = auto_join else: change = False new["conference"]["autojoin"] = existing["conference"]["autojoin"] existing_extensions = existing is not None and existing[ "conference" ].get_plugin("extensions", check=True) # preserving extensions we don't know about is a MUST if existing_extensions: assert existing is not None for el in existing["conference"]["extensions"].xml: if el.tag.startswith(f"{{{NOTIFY_NS}}}"): if notify is not None: continue if el.tag.startswith(f"{{{PINNING_NS}}}"): if pin is not None: continue new["conference"]["extensions"].append(el) if pin is not None: if existing_extensions: assert existing is not None existing_pin = ( existing["conference"]["extensions"].get_plugin( "pinned", check=True ) is not None ) if existing_pin != pin: change = True new["conference"]["extensions"]["pinned"] = pin if notify is not None: new["conference"]["extensions"].enable("notify") if existing_extensions: assert existing is not None existing_notify = existing["conference"]["extensions"].get_plugin( "notify", check=True ) if existing_notify is None: change = True else: if existing_notify.get_config() != notify: change = True for el in existing_notify: new["conference"]["extensions"]["notify"].append(el) new["conference"]["extensions"]["notify"].configure(notify) if change: iq = Iq(stype="set", sfrom=self.user_jid, sto=self.user_jid) iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS iq["pubsub"]["publish"].append(new) iq["pubsub"]["publish_options"] = _BOOKMARKS_OPTIONS try: await self.xmpp["xep_0356"].send_privileged_iq(iq) except PermissionError: warnings.warn( "IQ privileges (XEP0356) are not set, we cannot add bookmarks for the user" ) # fallback by forcing invitation invite = True except IqError as e: warnings.warn( f"Something went wrong while trying to set the bookmarks: {e}" ) # fallback by forcing invitation invite = True else: self.log.debug("Bookmark does not need updating.") return if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and existing is None): self.session.send_gateway_invite( self, reason="This group could not be added automatically for you" ) async def on_avatar( self, data: Optional[bytes], mime: Optional[str] ) -> Optional[Union[int, str]]: """ Called when the user tries to set the avatar of the room from an XMPP client. If the set avatar operation is completed, should return a legacy image unique identifier. In this case the MUC avatar will be immediately updated on the XMPP side. If data is not None and this method returns None, then we assume that self.set_avatar() will be called elsewhere, eg triggered by a legacy room update event. :param data: image data or None if the user meant to remove the avatar :param mime: the mime type of the image. Since this is provided by the XMPP client, there is no guarantee that this is valid or correct. :return: A unique avatar identifier, which will trigger :py:meth:`slidge.group.room.LegacyMUC.set_avatar`. Alternatively, None, if :py:meth:`.LegacyMUC.set_avatar` is meant to be awaited somewhere else. """ raise NotImplementedError admin_set_avatar = deprecated("LegacyMUC.on_avatar", on_avatar) async def on_set_affiliation( self, contact: "LegacyContact", affiliation: MucAffiliation, reason: Optional[str], nickname: Optional[str], ): """ Triggered when the user requests changing the affiliation of a contact for this group. Examples: promotion them to moderator, ban (affiliation=outcast). :param contact: The contact whose affiliation change is requested :param affiliation: The new affiliation :param reason: A reason for this affiliation change :param nickname: """ raise NotImplementedError async def on_kick(self, contact: "LegacyContact", reason: Optional[str]): """ Triggered when the user requests changing the role of a contact to "none" for this group. Action commonly known as "kick". :param contact: Contact to be kicked :param reason: A reason for this kick """ raise NotImplementedError async def on_set_config( self, name: Optional[str], description: Optional[str], ): """ Triggered when the user requests changing the room configuration. Only title and description can be changed at the moment. The legacy module is responsible for updating :attr:`.title` and/or :attr:`.description` of this instance. If :attr:`.HAS_DESCRIPTION` is set to False, description will always be ``None``. :param name: The new name of the room. :param description: The new description of the room. """ raise NotImplementedError async def on_destroy_request(self, reason: Optional[str]): """ Triggered when the user requests room destruction. :param reason: Optionally, a reason for the destruction """ raise NotImplementedError async def parse_mentions(self, text: str) -> list[Mention]: with self.__store.session(): await self.__fill_participants() assert self.pk is not None participants = { p.nickname: p for p in self.__participants_store.get_all(self.pk) } if len(participants) == 0: return [] result = [] for match in re.finditer( "|".join( sorted( [re.escape(nick) for nick in participants.keys()], key=lambda nick: len(nick), reverse=True, ) ), text, ): span = match.span() nick = match.group() if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION: continue if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION: participant = self.Participant.from_store( self.session, participants[nick] ) if contact := participant.contact: result.append( Mention(contact=contact, start=span[0], end=span[1]) ) return result async def on_set_subject(self, subject: str) -> None: """ Triggered when the user requests changing the room subject. The legacy module is responsible for updating :attr:`.subject` of this instance. :param subject: The new subject for this room. """ raise NotImplementedError @classmethod def from_store(cls, session, stored: Room, *args, **kwargs) -> Self: muc = cls( session, cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id), stored.jid, *args, # type: ignore **kwargs, # type: ignore ) muc.pk = stored.id muc.type = stored.muc_type # type: ignore muc._user_nick = stored.user_nick if stored.name: muc.DISCO_NAME = stored.name if stored.description: muc._description = stored.description if (data := stored.extra_attributes) is not None: muc.deserialize_extra_attributes(data) muc._subject = stored.subject or "" if stored.subject_date is not None: muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc) muc._participants_filled = stored.participants_filled muc._n_participants = stored.n_participants muc._history_filled = stored.history_filled if stored.user_resources is not None: muc._user_resources = set(json.loads(stored.user_resources)) muc._subject_setter = stored.subject_setter muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam) muc._set_logger_name() muc._AvatarMixin__avatar_unique_id = ( # type:ignore None if stored.avatar_legacy_id is None else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id) ) muc._avatar_pk = stored.avatar_id return muc def set_origin_id(msg: Message, origin_id: str): sub = ET.Element("{urn:xmpp:sid:0}origin-id") sub.attrib["id"] = origin_id msg.xml.append(sub) def int_or_none(x): try: return int(x) except ValueError: return None def equals_zero(x): if x is None: return False else: return x == 0 def str_to_datetime_or_none(date: Optional[str]): if date is None: return try: return str_to_datetime(date) except ValueError: return None def bookmarks_form(): form = Form() form["type"] = "submit" form.add_field( "FORM_TYPE", value="http://jabber.org/protocol/pubsub#publish-options", ftype="hidden", ) form.add_field("pubsub#persist_items", value="1") form.add_field("pubsub#max_items", value="max") form.add_field("pubsub#send_last_published_item", value="never") form.add_field("pubsub#access_model", value="whitelist") return form _BOOKMARKS_OPTIONS = bookmarks_form() _WHITESPACE_OR_PUNCTUATION = string.whitespace + string.punctuation log = logging.getLogger(__name__) slidge/slidge/main.py000066400000000000000000000146111477703150600151070ustar00rootroot00000000000000""" Slidge can be configured via CLI args, environment variables and/or INI files. To use env vars, use this convention: ``--home-dir`` becomes ``HOME_DIR``. Everything in ``/etc/slidge/conf.d/*`` is automatically used. To use a plugin-specific INI file, put it in another dir, and launch slidge with ``-c /path/to/plugin-specific.conf``. Use the long version of the CLI arg without the double dash prefix inside this INI file, eg ``debug=true``. An example configuration file is available at https://codeberg.org/slidge/slidge/src/branch/main/dev/confs/slidge-example.ini """ import asyncio import importlib import inspect import logging import os import re import signal from pathlib import Path import configargparse import slidge from slidge import BaseGateway from slidge.core import config from slidge.core.pubsub import PepAvatar, PepNick from slidge.db import SlidgeStore from slidge.db.avatar import avatar_cache from slidge.db.meta import get_engine from slidge.migration import migrate from slidge.util.conf import ConfigModule class MainConfig(ConfigModule): def update_dynamic_defaults(self, args): # force=True is needed in case we call a logger before this is reached, # or basicConfig has no effect logging.basicConfig( level=args.loglevel, filename=args.log_file, force=True, format=args.log_format, ) if args.home_dir is None: args.home_dir = Path("/var/lib/slidge") / str(args.jid) if args.user_jid_validator is None: args.user_jid_validator = ".*@" + re.escape(args.server) if args.db_url is None: args.db_url = f"sqlite:///{args.home_dir}/slidge.sqlite" class SigTermInterrupt(Exception): pass def get_configurator(from_entrypoint: bool = False): p = configargparse.ArgumentParser( default_config_files=os.getenv( "SLIDGE_CONF_DIR", "/etc/slidge/conf.d/*.conf" ).split(":"), description=__doc__, ) p.add_argument( "-c", "--config", help="Path to a INI config file.", env_var="SLIDGE_CONFIG", is_config_file=True, ) p.add_argument( "-q", "--quiet", help="loglevel=WARNING", action="store_const", dest="loglevel", const=logging.WARNING, default=logging.INFO, env_var="SLIDGE_QUIET", ) p.add_argument( "-d", "--debug", help="loglevel=DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, env_var="SLIDGE_DEBUG", ) p.add_argument( "--version", action="version", version=f"%(prog)s {slidge.__version__}", ) configurator = MainConfig( config, p, skip_options=("legacy_module",) if from_entrypoint else () ) return configurator def get_parser(): return get_configurator().parser def configure(from_entrypoint: bool): configurator = get_configurator(from_entrypoint) args, unknown_argv = configurator.set_conf() if not (h := config.HOME_DIR).exists(): logging.info("Creating directory '%s'", h) os.makedirs(h) config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare return unknown_argv def handle_sigterm(_signum, _frame): logging.info("Caught SIGTERM") raise SigTermInterrupt def main(module_name: str | None = None) -> None: from_entrypoint = module_name is not None signal.signal(signal.SIGTERM, handle_sigterm) unknown_argv = configure(from_entrypoint) logging.info("Starting slidge version %s", slidge.__version__) if module_name is not None: config.LEGACY_MODULE = module_name legacy_module = importlib.import_module(config.LEGACY_MODULE) logging.debug("Legacy module: %s", dir(legacy_module)) logging.info( "Starting legacy module: '%s' version %s", config.LEGACY_MODULE, getattr(legacy_module, "__version__", "No version"), ) if plugin_config_obj := getattr( legacy_module, "config", getattr(legacy_module, "Config", None) ): # If the legacy module has default parameters that depend on dynamic defaults # of the slidge main config, it needs to be refreshed at this point, because # now the dynamic defaults are set. if inspect.ismodule(plugin_config_obj): importlib.reload(plugin_config_obj) logging.debug("Found a config object in plugin: %r", plugin_config_obj) ConfigModule.ENV_VAR_PREFIX += ( f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_" ) logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX) ConfigModule(plugin_config_obj).set_conf(unknown_argv) else: if unknown_argv: raise RuntimeError("Some arguments have not been recognized", unknown_argv) migrate() store = SlidgeStore(get_engine(config.DB_URL)) BaseGateway.store = store gateway: BaseGateway = BaseGateway.get_unique_subclass()() avatar_cache.store = gateway.store.avatars avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3") PepAvatar.store = gateway.store PepNick.contact_store = gateway.store.contacts gateway.connect() return_code = 0 try: gateway.loop.run_forever() except KeyboardInterrupt: logging.debug("Received SIGINT") except SigTermInterrupt: logging.debug("Received SIGTERM") except SystemExit as e: return_code = e.code # type: ignore logging.debug("Exit called") except Exception as e: return_code = 2 logging.exception("Exception in __main__") logging.exception(e) finally: if gateway.has_crashed: if return_code != 0: logging.warning("Return code has been set twice. Please report this.") return_code = 3 if gateway.is_connected(): logging.debug("Gateway is connected, cleaning up") gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown())) gateway.disconnect() gateway.loop.run_until_complete(gateway.disconnected) else: logging.debug("Gateway is not connected, no need to clean up") avatar_cache.close() gateway.loop.run_until_complete(gateway.http.close()) logging.info("Successful clean shut down") logging.debug("Exiting with code %s", return_code) exit(return_code) slidge/slidge/migration.py000066400000000000000000000030051477703150600161470ustar00rootroot00000000000000import logging import shutil import sys from pathlib import Path from alembic import command from alembic.config import Config from slixmpp import JID from .core import config from .db.meta import get_engine from .db.models import GatewayUser from .db.store import SlidgeStore def remove_avatar_cache_v1(): old_dir = config.HOME_DIR / "slidge_avatars" if old_dir.exists(): log.info("Avatar cache dir v1 found, clearing it.") shutil.rmtree(old_dir) def get_alembic_cfg() -> Config: alembic_cfg = Config() alembic_cfg.set_section_option( "alembic", "script_location", str(Path(__file__).parent / "db" / "alembic"), ) return alembic_cfg def remove_resource_parts_from_users() -> None: with SlidgeStore(get_engine(config.DB_URL)).session() as orm: for user in orm.query(GatewayUser).all(): if user.jid.resource: user.jid = JID(user.jid.bare) orm.add(user) orm.commit() def migrate() -> None: remove_avatar_cache_v1() command.upgrade(get_alembic_cfg(), "head") remove_resource_parts_from_users() def main(): """ Updates the (dev) database in ./dev/slidge.sqlite and generates a revision Usage: python -m slidge.migration "Revision message blah blah blah" """ alembic_cfg = get_alembic_cfg() command.upgrade(alembic_cfg, "head") command.revision(alembic_cfg, sys.argv[1], autogenerate=True) log = logging.getLogger(__name__) if __name__ == "__main__": main() slidge/slidge/py.typed000066400000000000000000000000001477703150600152730ustar00rootroot00000000000000slidge/slidge/slixfix/000077500000000000000000000000001477703150600152745ustar00rootroot00000000000000slidge/slidge/slixfix/__init__.py000066400000000000000000000100421477703150600174020ustar00rootroot00000000000000# This module contains patches for slixmpp; some have pending requests upstream # and should be removed on the next slixmpp release. # ruff: noqa: F401 import uuid import slixmpp.plugins import slixmpp.stanza.roster from slixmpp import Message from slixmpp.exceptions import IqError from slixmpp.plugins.xep_0050 import XEP_0050, Command from slixmpp.plugins.xep_0356.permissions import IqPermission from slixmpp.plugins.xep_0356.privilege import XEP_0356 from slixmpp.plugins.xep_0469.stanza import NS as PINNED_NS from slixmpp.plugins.xep_0469.stanza import Pinned from slixmpp.xmlstream import StanzaBase from . import ( link_preview, xep_0077, xep_0100, xep_0153, xep_0292, ) def set_pinned(self, val: bool): extensions = self.parent() if val: extensions.enable("pinned") else: extensions._del_sub(f"{{{PINNED_NS}}}pinned") Pinned.set_pinned = set_pinned def session_bind(self, jid): self.xmpp["xep_0030"].add_feature(Command.namespace) # awful hack to for the disco items: we need to comment this line # related issue: https://todo.sr.ht/~nicoco/slidge/131 # self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) XEP_0050.session_bind = session_bind # type:ignore def reply(self, body=None, clear=True): """ Overrides slixmpp's Message.reply(), since it strips to sender's resource for mtype=groupchat, and we do not want that, because when we raise an XMPPError, we actually want to preserve the resource. (this is called in RootStanza.exception() to handle XMPPErrors) """ new_message = StanzaBase.reply(self, clear) new_message["thread"] = self["thread"] new_message["parent_thread"] = self["parent_thread"] del new_message["id"] if self.stream is not None and self.stream.use_message_ids: new_message["id"] = self.stream.new_id() if body is not None: new_message["body"] = body return new_message Message.reply = reply # type: ignore # TODO: remove me when https://codeberg.org/poezio/slixmpp/pulls/3622 is merged class PrivilegedIqError(IqError): """ Exception raised when sending a privileged IQ stanza fails. """ def nested_error(self) -> IqError | None: """ Return the IQError generated from the inner IQ stanza, if present. """ if "privilege" in self.iq: if "forwarded" in self.iq["privilege"]: if "iq" in self.iq["privilege"]["forwarded"]: return IqError(self.iq["privilege"]["forwarded"]["iq"]) return None async def send_privileged_iq(self, encapsulated_iq, iq_id=None): """ Send an IQ on behalf of a user Caution: the IQ *must* have the jabber:client namespace Raises :class:`PrivilegedIqError` on failure. """ iq_id = iq_id or str(uuid.uuid4()) encapsulated_iq["id"] = iq_id server = encapsulated_iq.get_to().domain perms = self.granted_privileges.get(server) if not perms: raise PermissionError(f"{server} has not granted us any privilege") itype = encapsulated_iq["type"] for ns in encapsulated_iq.plugins.values(): type_ = perms.iq[ns.namespace] if type_ == IqPermission.NONE: raise PermissionError( f"{server} has not granted any IQ privilege for namespace {ns.namespace}" ) elif type_ == IqPermission.BOTH: pass elif type_ != itype: raise PermissionError( f"{server} has not granted IQ {itype} privilege for namespace {ns.namespace}" ) iq = self.xmpp.make_iq( itype=itype, ifrom=self.xmpp.boundjid.bare, ito=encapsulated_iq.get_from(), id=iq_id, ) iq["privileged_iq"].append(encapsulated_iq) try: resp = await iq.send() except IqError as exc: raise PrivilegedIqError(exc.iq) return resp["privilege"]["forwarded"]["iq"] XEP_0356.send_privileged_iq = send_privileged_iq slixmpp.plugins.PLUGINS.extend( [ "link_preview", "xep_0292_provider", ] ) slidge/slidge/slixfix/delivery_receipt.py000066400000000000000000000025261477703150600212110ustar00rootroot00000000000000""" XEP-0184 Delivery Receipts The corresponding slixmpp module is a bit too rigid, this is our implementation to selectively choose when we send delivery receipts """ from typing import TYPE_CHECKING from slixmpp import JID, Message from slixmpp.types import MessageTypes if TYPE_CHECKING: from slidge.core.gateway import BaseGateway class DeliveryReceipt: def __init__(self, xmpp: "BaseGateway"): self.xmpp = xmpp def ack(self, msg: Message): """ Send a XEP-0184 (delivery receipt) in response to a message, if appropriate. :param msg: """ if not self.requires_receipt(msg): return ack = self.make_ack(msg["id"], msg["to"], msg["from"].bare, msg["type"]) ack.send() def make_ack(self, msg_id: str, mfrom: JID, mto: JID, mtype: MessageTypes = "chat"): ack = self.xmpp.Message() ack["type"] = mtype ack["to"] = mto ack["from"] = mfrom ack["receipt"] = msg_id return ack def requires_receipt(self, msg: Message): """ Check if a message is eligible for a delivery receipt. :param msg: :return: """ return ( msg["request_receipt"] and msg["type"] in self.xmpp.plugin["xep_0184"].ack_types and not msg["receipt"] ) slidge/slidge/slixfix/link_preview/000077500000000000000000000000001477703150600177725ustar00rootroot00000000000000slidge/slidge/slixfix/link_preview/__init__.py000066400000000000000000000004421477703150600221030ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from .link_preview import LinkPreview register_plugin(LinkPreview) slidge/slidge/slixfix/link_preview/link_preview.py000066400000000000000000000007001477703150600230370ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins import BasePlugin from . import stanza class LinkPreview(BasePlugin): name = "link_preview" description = "Sender-generated link previews" dependencies = set() stanza = stanza def plugin_init(self): stanza.register_plugin() slidge/slidge/slixfix/link_preview/stanza.py000066400000000000000000000051501477703150600216450ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import Optional, Type from slixmpp.stanza.message import Message from slixmpp.xmlstream import ElementBase, register_stanza_plugin class LinkPreview(ElementBase): name = "Description" namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" plugin_attrib = "link_preview" plugin_multi_attrib = "link_previews" interfaces = {"about", "title", "description", "url", "image", "type", "site_name"} def _set_og(self, el: ElementBase, value: str) -> None: el.xml.text = value self.xml.append(el.xml) def _get_og(self, el: Type[ElementBase]) -> Optional[str]: child = self.xml.find(f"{{{el.namespace}}}{el.name}") if child is None: return None return child.text def set_title(self, v: str) -> None: self._set_og(Title(), v) def get_title(self) -> Optional[str]: return self._get_og(Title) def set_description(self, v: str) -> None: self._set_og(Description(), v) def get_description(self) -> Optional[str]: return self._get_og(Description) def set_url(self, v: str) -> None: self._set_og(Url(), v) def get_url(self) -> Optional[str]: return self._get_og(Url) def set_image(self, v: str) -> None: self._set_og(Image(), v) def get_image(self) -> Optional[str]: return self._get_og(Image) def set_type(self, v: str) -> None: self._set_og(Type_(), v) def get_type(self) -> Optional[str]: return self._get_og(Type_) def set_site_name(self, v: str) -> None: self._set_og(SiteName(), v) def get_site_name(self) -> Optional[str]: return self._get_og(SiteName) def get_about(self) -> Optional[str]: return self.xml.attrib.get(f"{{{self.namespace}}}about") class OpenGraphMixin(ElementBase): namespace = "https://ogp.me/ns#" class Title(OpenGraphMixin): name = plugin_attrib = "title" class Description(OpenGraphMixin): name = plugin_attrib = "description" class Url(OpenGraphMixin): name = plugin_attrib = "url" class Image(OpenGraphMixin): name = plugin_attrib = "image" class Type_(OpenGraphMixin): name = plugin_attrib = "type" class SiteName(OpenGraphMixin): name = plugin_attrib = "site_name" def register_plugin(): for plugin in Title, Description, Url, Image, Type_, SiteName: register_stanza_plugin(plugin, Title) register_stanza_plugin(Message, LinkPreview, iterable=True) slidge/slidge/slixfix/roster.py000066400000000000000000000032501477703150600171640ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING from slixmpp import JID if TYPE_CHECKING: from .. import BaseGateway class YesSet(set): """ A pseudo-set which always test True for membership """ def __contains__(self, item): log.debug("Test in") return True class RosterBackend: """ A pseudo-roster for the gateway component. If a user is in the user store, this will behave as if the user is part of the roster with subscription "both", and "none" otherwise. This is rudimentary but the only sane way I could come up with so far. """ def __init__(self, xmpp: "BaseGateway"): self.xmpp = xmpp @staticmethod def entries(_owner_jid, _default=None): return YesSet() @staticmethod def save(_owner_jid, _jid, _item_state, _db_state): pass def load(self, _owner_jid, jid, _db_state): log.debug("Load %s", jid) user = self.xmpp.store.users.get(JID(jid)) log.debug("User %s", user) if user is None: return { "name": "", "groups": [], "from": False, "to": False, "pending_in": False, "pending_out": False, "whitelisted": False, "subscription": "both", } else: return { "name": "", "groups": [], "from": True, "to": True, "pending_in": False, "pending_out": False, "whitelisted": False, "subscription": "none", } log = logging.getLogger(__name__) slidge/slidge/slixfix/xep_0077/000077500000000000000000000000001477703150600165455ustar00rootroot00000000000000slidge/slidge/slixfix/xep_0077/__init__.py000066400000000000000000000005051477703150600206560ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from .register import XEP_0077 from .stanza import Register, RegisterFeature register_plugin(XEP_0077) slidge/slidge/slixfix/xep_0077/register.py000066400000000000000000000242771477703150600207570ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging import ssl from slixmpp.exceptions import XMPPError from slixmpp.plugins import BasePlugin from slixmpp.stanza import Iq, StreamFeatures from slixmpp.xmlstream import JID, StanzaBase, register_stanza_plugin from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from . import stanza from .stanza import Register, RegisterFeature log = logging.getLogger(__name__) # noinspection PyPep8Naming class XEP_0077(BasePlugin): """ XEP-0077: In-Band Registration Events: :: user_register -- After successful validation and add to the user store in api["user_validate"] user_unregister -- After successful user removal in api["user_remove"] user_modify -- After successful user modify in api["user_modify"] Config: :: form_fields and form_instructions are only used if api["make_registration_form"] is not overridden; in this case form_fields MUST be None API: :: user_get(jid, node, ifrom, iq) Returns a dict-like object containing `form_fields` for this user or None user_remove(jid, node, ifrom, iq) Removes a user or raise KeyError in case the user is not found in the user store make_registration_form(self, jid, node, ifrom, iq) Returns an iq reply to a registration form request, pre-filled and with in case the requesting entity is already registered to us user_validate((self, jid, node, ifrom, registration) Add the user to the user store or raise ValueError(msg) if any problem is encountered msg is sent back to the XMPP client as an error message. user_modify(jid, node, ifrom, iq) Modify the user in the user store or raise ValueError(msg) (similarly to user_validate) """ name = "xep_0077" description = "XEP-0077: In-Band Registration (slidge)" dependencies = {"xep_0004", "xep_0066"} stanza = stanza default_config = { "create_account": True, "force_registration": False, "order": 50, "form_fields": {"username", "password"}, "form_instructions": "Enter your credentials", "enable_subscription": True, } _user_store: dict[str, dict[str, str]] def plugin_init(self): register_stanza_plugin(StreamFeatures, RegisterFeature) register_stanza_plugin(Iq, Register) if self.xmpp.is_component: self.xmpp["xep_0030"].add_feature("jabber:iq:register") self.xmpp.register_handler( CoroutineCallback( "registration", StanzaPath(f"/iq@to={self.xmpp.boundjid.bare}/register"), self._handle_registration, ) ) self._user_store = {} self.api.register(self._user_get, "user_get") self.api.register(self._user_remove, "user_remove") self.api.register(self._user_modify, "user_modify") self.api.register(self._make_registration_form, "make_registration_form") self.api.register(self._user_validate, "user_validate") else: self.xmpp.register_feature( "register", self._handle_register_feature, restart=False, order=self.order, ) register_stanza_plugin(Register, self.xmpp["xep_0004"].stanza.Form) register_stanza_plugin(Register, self.xmpp["xep_0066"].stanza.OOB) self.xmpp.add_event_handler("connected", self._force_registration) def plugin_end(self): if not self.xmpp.is_component: self.xmpp.unregister_feature("register", self.order) def _user_get(self, _jid, _node, _ifrom, iq): return self._user_store.get(iq["from"].bare) def _user_remove(self, _jid, _node, _ifrom, iq): return self._user_store.pop(iq["from"].bare) async def _make_registration_form(self, _jid, _node, _ifrom, iq: Iq): reg = iq["register"] user = await self.api["user_get"](None, None, iq["from"], iq) if user is None: user = {} else: reg["registered"] = True reg["instructions"] = self.form_instructions for field in self.form_fields: data = user.get(field, "") if data: reg[field] = data else: # Add a blank field reg.add_field(field) reply = iq.reply() reply.set_payload(reg.xml) return reply def _user_validate(self, _jid, _node, ifrom, registration): self._user_store[ifrom.bare] = { key: registration[key] for key in self.form_fields } def _user_modify(self, _jid, _node, ifrom, registration): self._user_store[ifrom.bare] = { key: registration[key] for key in self.form_fields } async def _handle_registration(self, iq: StanzaBase): if iq["type"] == "get": if not self.enable_subscription: raise XMPPError( "bad-request", text="You must use adhoc commands to register to this gateway.", ) await self._send_form(iq) elif iq["type"] == "set": form_dict = iq["register"]["form"].get_values() or iq["register"] if form_dict.get("remove"): try: await self.api["user_remove"](None, None, iq["from"], iq) except KeyError: _send_error( iq, "404", "cancel", "item-not-found", "User not found", ) else: reply = iq.reply() reply.send() self.xmpp.event("user_unregister", iq) return if not self.enable_subscription: raise XMPPError( "bad-request", text="You must use adhoc commands to register to this gateway.", ) if self.form_fields is not None: for field in self.form_fields: if not iq["register"][field]: # Incomplete Registration _send_error( iq, "406", "modify", "not-acceptable", "Please fill in all fields.", ) return user = await self.api["user_get"](None, None, iq["from"], iq) try: if user is None: await self.api["user_validate"](None, None, iq["from"], form_dict) else: await self.api["user_modify"](None, None, iq["from"], form_dict) except ValueError as e: _send_error(iq, "406", "modify", "not-acceptable", "\n".join(e.args)) return reply = iq.reply() reply.send() if user is None: self.xmpp.event("user_register", iq) else: self.xmpp.event("user_modify", iq) async def _send_form(self, iq): reply = await self.api["make_registration_form"](None, None, iq["from"], iq) reply.send() def _force_registration(self, _event): if self.force_registration: self.xmpp.add_filter("in", self._force_stream_feature) def _force_stream_feature(self, stanza_): if isinstance(stanza_, StreamFeatures): if not self.xmpp.disable_starttls: if "starttls" not in self.xmpp.features: return stanza_ elif not isinstance(self.xmpp.socket, ssl.SSLSocket): return stanza_ if "mechanisms" not in self.xmpp.features: log.debug("Forced adding in-band registration stream feature") stanza_.enable("register") self.xmpp.del_filter("in", self._force_stream_feature) return stanza_ async def _handle_register_feature(self, _features): if "mechanisms" in self.xmpp.features: # We have already logged in with an account return False if self.create_account and self.xmpp.event_handled("register"): form = await self.get_registration() await self.xmpp.event_async("register", form) return True return False def get_registration(self, jid=None, ifrom=None, timeout=None, callback=None): iq = self.xmpp.Iq() iq["type"] = "get" iq["to"] = jid iq["from"] = ifrom iq.enable("register") return iq.send(timeout=timeout, callback=callback) def cancel_registration(self, jid=None, ifrom=None, timeout=None, callback=None): iq = self.xmpp.Iq() iq["type"] = "set" iq["to"] = jid iq["from"] = ifrom iq["register"]["remove"] = True return iq.send(timeout=timeout, callback=callback) def change_password( self, password, jid=None, ifrom=None, timeout=None, callback=None ): iq = self.xmpp.Iq() iq["type"] = "set" iq["to"] = jid iq["from"] = ifrom if self.xmpp.is_component: ifrom = JID(ifrom) iq["register"]["username"] = ifrom.user else: iq["register"]["username"] = self.xmpp.boundjid.user iq["register"]["password"] = password return iq.send(timeout=timeout, callback=callback) def _send_error(iq, code, error_type, name, text=""): # It would be nice to raise XMPPError but the iq payload # should include the register info reply = iq.reply() reply.set_payload(iq["register"].xml) reply.error() reply["error"]["code"] = code reply["error"]["type"] = error_type reply["error"]["condition"] = name reply["error"]["text"] = text reply.send() slidge/slidge/slixfix/xep_0077/stanza.py000066400000000000000000000045521477703150600204250ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import unicode_literals from typing import ClassVar, Set from slixmpp.xmlstream import ElementBase class Register(ElementBase): namespace = "jabber:iq:register" name = "query" plugin_attrib = "register" interfaces = { "username", "password", "email", "nick", "name", "first", "last", "address", "city", "state", "zip", "phone", "url", "date", "misc", "text", "key", "registered", "remove", "instructions", "fields", } sub_interfaces = interfaces form_fields = { "username", "password", "email", "nick", "name", "first", "last", "address", "city", "state", "zip", "phone", "url", "date", "misc", "text", "key", } def get_registered(self): present = self.xml.find("{%s}registered" % self.namespace) return present is not None def get_remove(self): present = self.xml.find("{%s}remove" % self.namespace) return present is not None def set_registered(self, value): if value: self.add_field("registered") else: del self["registered"] def set_remove(self, value): if value: self.add_field("remove") else: del self["remove"] def add_field(self, value): self._set_sub_text(value, "", keep=True) def get_fields(self): fields = set() for field in self.form_fields: if self.xml.find("{%s}%s" % (self.namespace, field)) is not None: fields.add(field) return fields def set_fields(self, fields): del self["fields"] for field in fields: self._set_sub_text(field, "", keep=True) def del_fields(self): for field in self.form_fields: self._del_sub(field) class RegisterFeature(ElementBase): name = "register" namespace = "http://jabber.org/features/iq-register" plugin_attrib = name interfaces: ClassVar[Set[str]] = set() slidge/slidge/slixfix/xep_0100/000077500000000000000000000000001477703150600165305ustar00rootroot00000000000000slidge/slidge/slixfix/xep_0100/__init__.py000066400000000000000000000001531477703150600206400ustar00rootroot00000000000000from slixmpp.plugins.base import register_plugin from .gateway import XEP_0100 register_plugin(XEP_0100) slidge/slidge/slixfix/xep_0100/gateway.py000066400000000000000000000104131477703150600205420ustar00rootroot00000000000000import logging import warnings from slixmpp import JID, Iq, Message, Presence, register_stanza_plugin from slixmpp.exceptions import XMPPError from slixmpp.plugins.base import BasePlugin from slidge.core import config from . import stanza log = logging.getLogger(__name__) class XEP_0100(BasePlugin): name = "xep_0100" description = "XEP-0100: Gateway interaction (slidge)" dependencies = { "xep_0030", # Service discovery "xep_0077", # In band registration "xep_0356", # Privileged entities } default_config = { "component_name": "SliXMPP gateway", "type": "xmpp", "needs_registration": True, } def plugin_init(self): if not self.xmpp.is_component: log.error("Only components can be gateways, aborting plugin load") return self.xmpp["xep_0030"].add_identity( name=self.component_name, category="gateway", itype=self.type ) # Without that BaseXMPP sends unsub/unavailable on sub requests, and we don't want that self.xmpp.client_roster.auto_authorize = False self.xmpp.client_roster.auto_subscribe = False self.xmpp.add_event_handler("user_register", self.on_user_register) self.xmpp.add_event_handler("user_unregister", self.on_user_unregister) self.xmpp.add_event_handler( "presence_unsubscribe", self.on_presence_unsubscribe ) self.xmpp.add_event_handler("message", self.on_message) register_stanza_plugin(Iq, stanza.Gateway) def plugin_end(self): if not self.xmpp.is_component: self.xmpp.remove_event_handler("user_register", self.on_user_register) self.xmpp.remove_event_handler("user_unregister", self.on_user_unregister) self.xmpp.remove_event_handler( "presence_unsubscribe", self.on_presence_unsubscribe ) self.xmpp.remove_event_handler("message", self.on_message) async def get_user(self, stanza): return await self.xmpp["xep_0077"].api["user_get"](None, None, None, stanza) async def on_user_unregister(self, iq: Iq): self.xmpp.send_presence(pto=iq.get_from().bare, ptype="unavailable") self.xmpp.send_presence(pto=iq.get_from().bare, ptype="unsubscribe") self.xmpp.send_presence(pto=iq.get_from().bare, ptype="unsubscribed") async def on_user_register(self, iq: Iq): self.xmpp.client_roster[iq.get_from()].load() await self.add_component_to_roster(jid=iq.get_from()) async def add_component_to_roster(self, jid: JID): if config.NO_ROSTER_PUSH: return items = { self.xmpp.boundjid.bare: { "name": self.component_name, "subscription": "both", "groups": ["Slidge"], } } try: await self._set_roster(jid, items) except PermissionError: warnings.warn( "Slidge does not have the privilege to manage users' rosters. " "Users should add the slidge component to their rosters manually." ) if config.ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK: self.xmpp.send_presence(ptype="subscribe", pto=jid.bare) async def _set_roster(self, jid, items): await self.xmpp["xep_0356"].set_roster(jid=jid.bare, roster_items=items) def on_presence_unsubscribe(self, p: Presence): if p.get_to() == self.xmpp.boundjid.bare: log.debug("REMOVE: Our roster: %s", self.xmpp.client_roster) self.xmpp["xep_0077"].api["user_remove"](None, None, p["from"], p) self.xmpp.event("user_unregister", p) async def on_message(self, msg: Message): if msg["type"] == "groupchat": return # groupchat messages are out of scope of XEP-0100 if msg["to"] == self.xmpp.boundjid.bare: # It may be useful to exchange direct messages with the component self.xmpp.event("gateway_message", msg) return if self.needs_registration and await self.get_user(msg) is None: raise XMPPError( "registration-required", text="You are not registered to this gateway" ) self.xmpp.event("legacy_message", msg) slidge/slidge/slixfix/xep_0100/stanza.py000066400000000000000000000003501477703150600204000ustar00rootroot00000000000000from slixmpp.xmlstream import ElementBase class Gateway(ElementBase): namespace = "jabber:iq:gateway" name = "query" plugin_attrib = "gateway" interfaces = {"desc", "prompt", "jid"} sub_interfaces = interfaces slidge/slidge/slixfix/xep_0153/000077500000000000000000000000001477703150600165405ustar00rootroot00000000000000slidge/slidge/slixfix/xep_0153/__init__.py000066400000000000000000000004331477703150600206510ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from .vcard_avatar import XEP_0153 register_plugin(XEP_0153) slidge/slidge/slixfix/xep_0153/vcard_avatar.py000066400000000000000000000007121477703150600215470ustar00rootroot00000000000000from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0153 import VCardTempUpdate, stanza from slixmpp.stanza import Presence from slixmpp.xmlstream import register_stanza_plugin class XEP_0153(BasePlugin): name = "xep_0153" description = "XEP-0153: vCard-Based Avatars (slidge, just for MUCs)" dependencies = {"xep_0054"} stanza = stanza def plugin_init(self): register_stanza_plugin(Presence, VCardTempUpdate) slidge/slidge/slixfix/xep_0292/000077500000000000000000000000001477703150600165445ustar00rootroot00000000000000slidge/slidge/slixfix/xep_0292/__init__.py000066400000000000000000000001421477703150600206520ustar00rootroot00000000000000from slixmpp.plugins.xep_0292 import stanza from . import vcard4 __all__ = ("stanza", "vcard4") slidge/slidge/slixfix/xep_0292/vcard4.py000066400000000000000000000005461477703150600203060ustar00rootroot00000000000000from slixmpp.plugins.base import BasePlugin, register_plugin from slixmpp.plugins.xep_0292.stanza import NS class VCard4Provider(BasePlugin): name = "xep_0292_provider" description = "VCard4 Provider" dependencies = {"xep_0030"} def plugin_init(self): self.xmpp.plugin["xep_0030"].add_feature(NS) register_plugin(VCard4Provider) slidge/slidge/util/000077500000000000000000000000001477703150600145635ustar00rootroot00000000000000slidge/slidge/util/__init__.py000066400000000000000000000004551477703150600167000ustar00rootroot00000000000000from .util import ( ABCSubclassableOnceAtMost, SubclassableOnce, is_valid_phone_number, replace_mentions, strip_illegal_chars, ) __all__ = [ "SubclassableOnce", "ABCSubclassableOnceAtMost", "is_valid_phone_number", "replace_mentions", "strip_illegal_chars", ] slidge/slidge/util/archive_msg.py000066400000000000000000000032741477703150600174320ustar00rootroot00000000000000from copy import copy from datetime import datetime, timezone from typing import Optional, Union from uuid import uuid4 from xml.etree import ElementTree as ET from slixmpp import Message from slixmpp.plugins.xep_0297 import Forwarded def fix_namespaces(xml, old="{jabber:component:accept}", new="{jabber:client}"): """ Hack to fix namespaces between jabber:component and jabber:client Acts in-place. :param xml: :param old: :param new: """ xml.tag = xml.tag.replace(old, new) for child in xml: fix_namespaces(child, old, new) class HistoryMessage: def __init__(self, stanza: Union[Message, str], when: Optional[datetime] = None): if isinstance(stanza, str): from_db = True stanza = Message(xml=ET.fromstring(stanza)) else: from_db = False self.id = stanza["stanza_id"]["id"] or uuid4().hex self.when: datetime = ( when or stanza["delay"]["stamp"] or datetime.now(tz=timezone.utc) ) if not from_db: del stanza["delay"] del stanza["markable"] del stanza["hint"] del stanza["chat_state"] if not stanza["body"]: del stanza["body"] fix_namespaces(stanza.xml) self.stanza: Message = stanza @property def stanza_component_ns(self): stanza = copy(self.stanza) fix_namespaces( stanza.xml, old="{jabber:client}", new="{jabber:component:accept}" ) return stanza def forwarded(self): forwarded = Forwarded() forwarded["delay"]["stamp"] = self.when forwarded.append(self.stanza) return forwarded slidge/slidge/util/conf.py000066400000000000000000000154671477703150600160770ustar00rootroot00000000000000import logging from functools import cached_property from types import GenericAlias from typing import Optional, Union, get_args, get_origin, get_type_hints import configargparse class Option: DOC_SUFFIX = "__DOC" DYNAMIC_DEFAULT_SUFFIX = "__DYNAMIC_DEFAULT" SHORT_SUFFIX = "__SHORT" def __init__(self, parent: "ConfigModule", name: str): self.parent = parent self.config_obj = parent.config_obj self.name = name @cached_property def doc(self): return getattr(self.config_obj, self.name + self.DOC_SUFFIX) @cached_property def required(self): return not hasattr( self.config_obj, self.name + self.DYNAMIC_DEFAULT_SUFFIX ) and not hasattr(self.config_obj, self.name) @cached_property def default(self): return getattr(self.config_obj, self.name, None) @cached_property def short(self): return getattr(self.config_obj, self.name + self.SHORT_SUFFIX, None) @cached_property def nargs(self): type_ = get_type_hints(self.config_obj).get(self.name, type(self.default)) if isinstance(type_, GenericAlias): args = get_args(type_) if args[1] is Ellipsis: return "*" else: return len(args) @cached_property def type(self): type_ = get_type_hints(self.config_obj).get(self.name, type(self.default)) if _is_optional(type_): type_ = get_args(type_)[0] elif isinstance(type_, GenericAlias): args = get_args(type_) type_ = args[0] return type_ @cached_property def names(self): res = ["--" + self.name.lower().replace("_", "-")] if s := self.short: res.append("-" + s) return res @cached_property def kwargs(self): kwargs = dict( required=self.required, help=self.doc, env_var=self.name_to_env_var(), ) t = self.type if t is bool: if self.default: kwargs["action"] = "store_false" else: kwargs["action"] = "store_true" else: kwargs["type"] = t if self.required: kwargs["required"] = True else: kwargs["default"] = self.default if n := self.nargs: kwargs["nargs"] = n return kwargs def name_to_env_var(self): return self.parent.ENV_VAR_PREFIX + self.name class ConfigModule: ENV_VAR_PREFIX = "SLIDGE_" def __init__( self, config_obj, parser: Optional[configargparse.ArgumentParser] = None, skip_options: tuple[str, ...] = (), ): self.config_obj = config_obj if parser is None: parser = configargparse.ArgumentParser() self.parser = parser self.skip_options = skip_options self.add_options_to_parser(skip_options) def _list_options(self): return { o for o in (set(dir(self.config_obj)) | set(get_type_hints(self.config_obj))) if o.upper() == o and not o.startswith("_") and "__" not in o and o.lower() not in self.skip_options } def set_conf(self, argv: Optional[list[str]] = None): if argv is not None: # this is ugly, but necessary because for plugin config, we used # remaining argv. # when using (a) .ini file(s), for bool options, we end-up with # remaining pseudo-argv such as --some-bool-opt=true when we really # should have just --some-bool-opt # TODO: get rid of configargparse and make this cleaner options_long = {o.name: o for o in self.options} no_explicit_bool = [] skip_next = False for a, aa in zip(argv, argv[1:] + [""]): if skip_next: skip_next = False continue force_keep = False if "=" in a: real_name, _value = a.split("=") opt: Optional[Option] = options_long.get( _argv_to_option_name(real_name) ) if opt and opt.type is bool: if opt.default: if _value in _TRUEISH or not _value: continue else: a = real_name force_keep = True else: if _value in _TRUEISH: a = real_name force_keep = True else: continue else: upper = _argv_to_option_name(a) opt = options_long.get(upper) if opt and opt.type is bool: if _argv_to_option_name(aa) not in options_long: log.debug("Removing %s from argv", aa) skip_next = True if opt: if opt.type is bool: if force_keep or not opt.default: no_explicit_bool.append(a) else: no_explicit_bool.append(a) else: no_explicit_bool.append(a) log.debug("Removed boolean values from %s to %s", argv, no_explicit_bool) argv = no_explicit_bool args, rest = self.parser.parse_known_args(argv) self.update_dynamic_defaults(args) for name in self._list_options(): value = getattr(args, name.lower()) log.debug("Setting '%s' to %r", name, value) setattr(self.config_obj, name, value) return args, rest @cached_property def options(self) -> list[Option]: res = [] for opt in self._list_options(): res.append(Option(self, opt)) return res def add_options_to_parser(self, skip_options: tuple[str, ...]): skip_options = tuple(o.lower() for o in skip_options) p = self.parser for o in sorted(self.options, key=lambda x: (not x.required, x.name)): if o.name.lower() in skip_options: continue p.add_argument(*o.names, **o.kwargs) def update_dynamic_defaults(self, args): pass def _is_optional(t): if get_origin(t) is Union: args = get_args(t) if len(args) == 2 and isinstance(None, args[1]): return True return False def _argv_to_option_name(arg: str): return arg.upper().removeprefix("--").replace("-", "_") _TRUEISH = {"true", "True", "1", "on", "enabled"} log = logging.getLogger(__name__) slidge/slidge/util/jid_escaping.py000066400000000000000000000020521477703150600175530ustar00rootroot00000000000000from functools import lru_cache JID_ESCAPE_SEQUENCES = { "\\20", "\\22", "\\26", "\\27", "\\2f", "\\3a", "\\3c", "\\3e", "\\40", "\\5c", } JID_UNESCAPE_TRANSFORMATIONS = { "\\20": " ", "\\22": '"', "\\26": "&", "\\27": "'", "\\2f": "/", "\\3a": ":", "\\3c": "<", "\\3e": ">", "\\40": "@", "\\5c": "\\", } @lru_cache(1000) def unescape_node(node: str): """Unescape a local portion of a JID.""" unescaped = [] seq = "" for i, char in enumerate(node): if char == "\\": seq = node[i : i + 3] if seq not in JID_ESCAPE_SEQUENCES: seq = "" if seq: if len(seq) == 3: unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char)) # Pop character off the escape sequence, and ignore it seq = seq[1:] else: unescaped.append(char) return "".join(unescaped) ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()}) slidge/slidge/util/test.py000066400000000000000000000334671477703150600161310ustar00rootroot00000000000000# type:ignore import tempfile import types from pathlib import Path from typing import Optional, Union from xml.dom.minidom import parseString try: import xmldiff.main XML_DIFF_PRESENT = True except ImportError: xmldiff = None XML_DIFF_PRESENT = False from slixmpp import ( JID, ElementBase, Iq, MatcherId, MatchXMLMask, MatchXPath, Message, Presence, StanzaPath, ) from slixmpp.stanza.error import Error from slixmpp.test import SlixTest, TestTransport from slixmpp.xmlstream import highlight, tostring from slixmpp.xmlstream.matcher import MatchIDSender from sqlalchemy import create_engine, delete from slidge import ( BaseGateway, BaseSession, LegacyBookmarks, LegacyContact, LegacyMUC, LegacyParticipant, LegacyRoster, ) from ..command import Command from ..core import config from ..core.config import _TimedeltaSeconds from ..core.pubsub import PepAvatar, PepNick from ..db import SlidgeStore from ..db.avatar import avatar_cache from ..db.meta import Base from ..db.models import Contact class SlixTestPlus(SlixTest): def setUp(self): super().setUp() Error.namespace = "jabber:component:accept" def check(self, stanza, criteria, method="exact", defaults=None, use_values=True): """ Create and compare several stanza objects to a correct XML string. If use_values is False, tests using stanza.values will not be used. Some stanzas provide default values for some interfaces, but these defaults can be problematic for testing since they can easily be forgotten when supplying the XML string. A list of interfaces that use defaults may be provided and the generated stanzas will use the default values for those interfaces if needed. However, correcting the supplied XML is not possible for interfaces that add or remove XML elements. Only interfaces that map to XML attributes may be set using the defaults parameter. The supplied XML must take into account any extra elements that are included by default. Arguments: stanza -- The stanza object to test. criteria -- An expression the stanza must match against. method -- The type of matching to use; one of: 'exact', 'mask', 'id', 'xpath', and 'stanzapath'. Defaults to the value of self.match_method. defaults -- A list of stanza interfaces that have default values. These interfaces will be set to their defaults for the given and generated stanzas to prevent unexpected test failures. use_values -- Indicates if testing using stanza.values should be used. Defaults to True. """ if method is None and hasattr(self, "match_method"): method = getattr(self, "match_method") if method != "exact": matchers = { "stanzapath": StanzaPath, "xpath": MatchXPath, "mask": MatchXMLMask, "idsender": MatchIDSender, "id": MatcherId, } Matcher = matchers.get(method, None) if Matcher is None: raise ValueError("Unknown matching method.") test = Matcher(criteria) self.assertTrue( test.match(stanza), "Stanza did not match using %s method:\n" % method + "Criteria:\n%s\n" % str(criteria) + "Stanza:\n%s" % str(stanza), ) else: stanza_class = stanza.__class__ # Hack to preserve namespaces instead of having jabber:client # everywhere. old_ns = stanza_class.namespace stanza_class.namespace = stanza.namespace if not isinstance(criteria, ElementBase): xml = self.parse_xml(criteria) else: xml = criteria.xml # Ensure that top level namespaces are used, even if they # were not provided. self.fix_namespaces(stanza.xml) self.fix_namespaces(xml) stanza2 = stanza_class(xml=xml) if use_values: # Using stanza.values will add XML for any interface that # has a default value. We need to set those defaults on # the existing stanzas and XML so that they will compare # correctly. default_stanza = stanza_class() if defaults is None: known_defaults = {Message: ["type"], Presence: ["priority"]} defaults = known_defaults.get(stanza_class, []) for interface in defaults: stanza[interface] = stanza[interface] stanza2[interface] = stanza2[interface] # Can really only automatically add defaults for top # level attribute values. Anything else must be accounted # for in the provided XML string. if interface not in xml.attrib: if interface in default_stanza.xml.attrib: value = default_stanza.xml.attrib[interface] xml.attrib[interface] = value values = stanza2.values stanza3 = stanza_class() stanza3.values = values debug = "Three methods for creating stanzas do not match.\n" debug += "Given XML:\n%s\n" % highlight(tostring(xml)) debug += "Given stanza:\n%s\n" % format_stanza(stanza) debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml)) debug += "Second generated stanza:\n%s\n" % highlight( tostring(stanza3.xml) ) result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml) else: debug = "Two methods for creating stanzas do not match.\n" debug += "Given XML:\n%s\n" % highlight(tostring(xml)) debug += "Given stanza:\n%s\n" % format_stanza(stanza) debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml)) result = self.compare(xml, stanza.xml, stanza2.xml) stanza_class.namespace = old_ns if XML_DIFF_PRESENT and not result: debug += str( xmldiff.main.diff_texts(tostring(xml), tostring(stanza.xml)) ) if use_values: debug += str( xmldiff.main.diff_texts(tostring(xml), tostring(stanza2.xml)) ) self.assertTrue(result, debug) def next_sent(self, timeout=0.05) -> Optional[Union[Message, Iq, Presence]]: self.wait_for_send_queue() sent = self.xmpp.socket.next_sent(timeout=timeout) if sent is None: return None xml = self.parse_xml(sent) self.fix_namespaces(xml, "jabber:component:accept") sent = self.xmpp._build_stanza(xml, "jabber:component:accept") return sent class SlidgeTest(SlixTestPlus): plugin: Union[types.ModuleType, dict] class Config: jid = "aim.shakespeare.lit" secret = "test" server = "shakespeare.lit" port = 5222 upload_service = "upload.test" home_dir = Path(tempfile.mkdtemp()) user_jid_validator = ".*" admins: list[str] = [] no_roster_push = False upload_requester = None ignore_delay_threshold = _TimedeltaSeconds("300") last_seen_fallback = True @classmethod def setUpClass(cls): for k, v in vars(cls.Config).items(): setattr(config, k.upper(), v) def setUp(self): if hasattr(self, "plugin"): BaseGateway._subclass = find_subclass(self.plugin, BaseGateway) BaseSession._subclass = find_subclass(self.plugin, BaseSession) LegacyRoster._subclass = find_subclass( self.plugin, LegacyRoster, base_ok=True ) LegacyContact._subclass = find_subclass( self.plugin, LegacyContact, base_ok=True ) LegacyMUC._subclass = find_subclass(self.plugin, LegacyMUC, base_ok=True) LegacyBookmarks._subclass = find_subclass( self.plugin, LegacyBookmarks, base_ok=True ) # workaround for duplicate output of sql alchemy's log, cf # https://stackoverflow.com/a/76498428/5902284 from sqlalchemy import log as sqlalchemy_log sqlalchemy_log._add_default_handler = lambda x: None engine = self.db_engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) BaseGateway.store = SlidgeStore(engine) BaseGateway._test_mode = True try: self.xmpp = BaseGateway.get_self_or_unique_subclass()() except Exception: raise self.xmpp.TEST_MODE = True PepNick.contact_store = self.xmpp.store.contacts PepAvatar.store = self.xmpp.store avatar_cache.store = self.xmpp.store.avatars avatar_cache.set_dir(Path(tempfile.mkdtemp())) self.xmpp._always_send_everything = True engine.echo = True self.xmpp.connection_made(TestTransport(self.xmpp)) self.xmpp.session_bind_event.set() # Remove unique ID prefix to make it easier to test self.xmpp._id_prefix = "" self.xmpp.default_lang = None self.xmpp.peer_default_lang = None def new_id(): self.xmpp._id += 1 return str(self.xmpp._id) self.xmpp._id = 0 self.xmpp.new_id = new_id # Must have the stream header ready for xmpp.process() to work. header = self.xmpp.stream_header self.xmpp.data_received(header) self.wait_for_send_queue() self.xmpp.socket.next_sent() self.xmpp.socket.next_sent() # Some plugins require messages to have ID values. Set # this to True in tests related to those plugins. self.xmpp.use_message_ids = False self.xmpp.use_presence_ids = False Error.namespace = "jabber:component:accept" def tearDown(self): self.db_engine.echo = False super().tearDown() import slidge.db.store if slidge.db.store._session is not None: slidge.db.store._session.commit() slidge.db.store._session = None Base.metadata.drop_all(self.xmpp.store._engine) def setup_logged_session(self, n_contacts=0): user = self.xmpp.store.users.new( JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""} ) user.preferences = {"sync_avatar": True, "sync_presence": True} self.xmpp.store.users.update(user) with self.xmpp.store.session() as session: session.execute(delete(Contact)) session.commit() self.run_coro( self.xmpp._BaseGateway__dispatcher._on_user_register( Iq(sfrom="romeo@montague.lit/gajim") ) ) welcome = self.next_sent() assert welcome["body"], welcome stanza = self.next_sent() assert "logging in" in stanza["status"].lower(), stanza stanza = self.next_sent() assert "syncing contacts" in stanza["status"].lower(), stanza if BaseGateway.get_self_or_unique_subclass().GROUPS: stanza = self.next_sent() assert "syncing groups" in stanza["status"].lower(), stanza for _ in range(n_contacts): probe = self.next_sent() assert probe.get_type() == "probe" stanza = self.next_sent() assert "yup" in stanza["status"].lower(), stanza self.romeo: BaseSession = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) self.juliet: LegacyContact = self.run_coro( self.romeo.contacts.by_legacy_id("juliet") ) self.room: LegacyMUC = self.run_coro(self.romeo.bookmarks.by_legacy_id("room")) self.first_witch: LegacyParticipant = self.run_coro( self.room.get_participant("firstwitch") ) self.send( # language=XML """ """ ) @classmethod def tearDownClass(cls): reset_subclasses() def format_stanza(stanza): return highlight( "\n".join(parseString(tostring(stanza.xml)).toprettyxml().split("\n")[1:]) ) def find_subclass(o, parent, base_ok=False): try: vals = vars(o).values() except TypeError: vals = o.values() for x in vals: try: if issubclass(x, parent) and x is not parent: return x except TypeError: pass else: if base_ok: return parent else: raise RuntimeError def reset_subclasses(): """ Reset registered subclasses between test classes. Needed because these classes are meant to only be subclassed once and raise exceptions otherwise. """ BaseSession.reset_subclass() BaseGateway.reset_subclass() LegacyRoster.reset_subclass() LegacyContact.reset_subclass() LegacyMUC.reset_subclass() LegacyBookmarks.reset_subclass() LegacyParticipant.reset_subclass() # reset_commands() def reset_commands(): Command.subclasses = [ c for c in Command.subclasses if str(c).startswith(">> addLoggingLevel('TRACE', logging.DEBUG - 5) >>> logging.getLogger(__name__).setLevel("TRACE") >>> logging.getLogger(__name__).trace('that worked') >>> logging.trace('so did this') >>> logging.TRACE 5 """ if not methodName: methodName = levelName.lower() if hasattr(logging, levelName): log.debug("{} already defined in logging module".format(levelName)) return if hasattr(logging, methodName): log.debug("{} already defined in logging module".format(methodName)) return if hasattr(logging.getLoggerClass(), methodName): log.debug("{} already defined in logger class".format(methodName)) return # This method was inspired by the answers to Stack Overflow post # http://stackoverflow.com/q/2183233/2988730, especially # http://stackoverflow.com/a/13638084/2988730 def logForLevel(self, message, *args, **kwargs): if self.isEnabledFor(levelNum): self._log(levelNum, message, args, **kwargs) def logToRoot(message, *args, **kwargs): logging.log(levelNum, message, *args, **kwargs) logging.addLevelName(levelNum, levelName) setattr(logging, levelName, levelNum) setattr(logging.getLoggerClass(), methodName, logForLevel) setattr(logging, methodName, logToRoot) class SlidgeLogger(logging.Logger): def trace(self): pass log = logging.getLogger(__name__) def merge_resources(resources: dict[str, ResourceDict]) -> Optional[ResourceDict]: if len(resources) == 0: return None if len(resources) == 1: return next(iter(resources.values())) by_priority = sorted(resources.values(), key=lambda r: r["priority"], reverse=True) if any(r["show"] == "" for r in resources.values()): # if a client is "available", we're "available" show = "" else: for r in by_priority: if r["show"]: show = r["show"] break else: raise RuntimeError() # if there are different statuses, we use the highest priority one, # but we ignore resources without status, even with high priority status = "" for r in by_priority: if r["status"]: status = r["status"] break return { "show": show, # type:ignore "status": status, "priority": 0, } def remove_emoji_variation_selector_16(emoji: str): # this is required for compatibility with dino, and maybe other future clients? return bytes(emoji, encoding="utf-8").replace(b"\xef\xb8\x8f", b"").decode() def deprecated(name: str, new: Callable): # @functools.wraps def wrapped(*args, **kwargs): warnings.warn( f"{name} is deprecated. Use {new.__name__} instead", category=DeprecationWarning, ) return new(*args, **kwargs) return wrapped T = TypeVar("T", bound=NamedTuple) def dict_to_named_tuple(data: dict, cls: Type[T]) -> T: return cls(*(data.get(f) for f in cls._fields)) # type:ignore def replace_mentions( text: str, mentions: Optional[list[Mention]], mapping: Callable[["LegacyContact"], str], ): if not mentions: return text cursor = 0 pieces = [] for mention in mentions: pieces.extend([text[cursor : mention.start], mapping(mention.contact)]) cursor = mention.end pieces.append(text[cursor:]) return "".join(pieces) def with_session(func): @wraps(func) async def wrapped(self, *args, **kwargs): with self.xmpp.store.session(): return await func(self, *args, **kwargs) return wrapped def timeit(func): @wraps(func) async def wrapped(self, *args, **kwargs): start = time() r = await func(self, *args, **kwargs) self.log.debug("%s took %s ms", func.__name__, round((time() - start) * 1000)) return r return wrapped def strip_leading_emoji(text: str) -> str: if not EMOJI_LIB_AVAILABLE: return text words = text.split(" ") # is_emoji returns False for 🛷️ for obscure reasons, # purely_emoji seems better if len(words) > 1 and emoji.purely_emoji(words[0]): return " ".join(words[1:]) return text async def noop_coro(): pass slidge/superduper/000077500000000000000000000000001477703150600145355ustar00rootroot00000000000000slidge/superduper/__init__.py000066400000000000000000000002211477703150600166410ustar00rootroot00000000000000""" An example legacy module for slidge. """ from . import contact, gateway, group, session __all__ = "contact", "gateway", "group", "session" slidge/superduper/__main__.py000066400000000000000000000000701477703150600166240ustar00rootroot00000000000000from slidge import entrypoint entrypoint("superduper") slidge/superduper/contact.py000066400000000000000000000017051477703150600165450ustar00rootroot00000000000000from slixmpp.exceptions import XMPPError from slidge import LegacyContact, LegacyRoster from .session import Session class Roster(LegacyRoster[int, "Contact"]): async def fill(self): for i in 111, 222: yield await self.by_legacy_id(i) async def jid_username_to_legacy_id(self, jid_username: str) -> int: try: return int(jid_username) except ValueError: raise XMPPError( "bad-request", "This is not a valid username for this fake network" ) class Contact(LegacyContact[int]): session: "Session" async def update_info(self): profile = await self.session.legacy_client.get_profile(self.legacy_id) self.name = profile.nickname self.set_vcard(full_name=profile.full_name) await self.set_avatar(profile.avatar, profile.avatar_unique_id) if self.legacy_id != 666: self.is_friend = True self.online() slidge/superduper/gateway.py000066400000000000000000000043201477703150600165470ustar00rootroot00000000000000""" The gateway """ from typing import Optional from slixmpp import JID from slidge import BaseGateway, FormField, GatewayUser from slidge.command.register import RegistrationType from .legacy_client import SuperDuperClient from .util import ASSETS_DIR class Gateway(BaseGateway): """ This is instantiated once by the slidge entrypoint. By customizing the class attributes, we customize the registration process, and display name of the component. """ COMPONENT_NAME = "The great legacy network (slidge)" COMPONENT_AVATAR = ASSETS_DIR / "slidge-color.png" COMPONENT_TYPE = "whatsapp" REGISTRATION_INSTRUCTIONS = ( "Register to this fake service by using 'slidger' as username, and any " "password you want. Then you will need to enter '666' as the 2FA code." ) REGISTRATION_TYPE = RegistrationType.TWO_FACTOR_CODE REGISTRATION_FIELDS = [ FormField(var="username", label="User name", required=True), FormField(var="password", label="Password", required=True, private=True), ] GROUPS = True MARK_ALL_MESSAGES = True LEGACY_CONTACT_ID_TYPE = int async def validate( self, user_jid: JID, registration_form: dict[str, Optional[str]] ): """ This function receives the values of the form defined in :attr:`REGISTRATION_FIELDS`. Here, since we set :attr:`REGISTRATION_TYPE` to "2FA", if this method does not raise any exception, the wannabe user will be prompted for their 2FA code. :param user_jid: :param registration_form: :return: """ await SuperDuperClient.send_2fa( registration_form["username"], registration_form["password"], ) async def validate_two_factor_code(self, user: GatewayUser, code: str): """ This function receives the 2FA code entered by the aspiring user. It should raise something if the 2FA does not permit logging in to the legacy service. :param user: :param code: """ await SuperDuperClient.validate_2fa( user.legacy_module_data["username"], user.legacy_module_data["password"], code, ) slidge/superduper/group.py000066400000000000000000000037021477703150600162450ustar00rootroot00000000000000""" Handling groups """ import uuid from datetime import datetime, timedelta from typing import TYPE_CHECKING, Optional from slidge import LegacyBookmarks, LegacyMUC, LegacyParticipant, MucType from slidge.util.types import Hat, HoleBound if TYPE_CHECKING: from .session import Session class Bookmarks(LegacyBookmarks): async def fill(self): for i in "aaa", "bbb": muc = await self.by_legacy_id(i) await muc.add_to_bookmarks() class MUC(LegacyMUC): session: "Session" type = MucType.GROUP async def update_info(self): info = await self.session.legacy_client.get_group_info(self.legacy_id) self.name = info.name await self.set_avatar(info.avatar, info.avatar_unique_id) async def fill_participants(self): # in a real case, this would probably call something like # self.session.legacy_client.fetch_group_members(self.legacy_id) for i in 0, 111, 222: part = await self.get_participant_by_legacy_id(i) if i == 111: part.role = "moderator" part.affiliation = "owner" part.set_hats([Hat("test", "test"), Hat("prout", "prout")]) yield part me = await self.get_user_participant() me.role = "moderator" me.affiliation = "owner" async def backfill( self, after: Optional[HoleBound] = None, before: Optional[HoleBound] = None, ): # in a real case, this would probably call something like # self.session.legacy_client.fetch_group_history(self.legacy_id) for i in range(10): part = await self.get_participant_by_legacy_id(0) part.send_text( f"History message #{i}", when=datetime.now() - timedelta(hours=i), legacy_msg_id=f"{i}--{uuid.uuid4().hex}", archive_only=True, ) class Participant(LegacyParticipant): pass slidge/superduper/legacy_client.py000066400000000000000000000122161477703150600177130ustar00rootroot00000000000000import asyncio import random import uuid from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Optional from slixmpp.exceptions import XMPPError from slidge.util.types import Hat, MessageReference from .util import ASSETS_DIR, later if TYPE_CHECKING: from .session import Session @dataclass class DirectMessage: sender: int text: str id: str reply_to: Optional[str] = None @dataclass class GroupMessage: sender: int group_id: str text: str id: str reply_to: Optional[str] = None @dataclass class Profile: nickname: str full_name: str avatar: Path avatar_unique_id: str @dataclass class GroupInfo: name: str avatar: Optional[Path] = None avatar_unique_id: Optional[str] = None class SuperDuperClient: @staticmethod async def send_2fa(username, _password): if username != "slidger": raise XMPPError("not-authorized", "Use 'slidger' as the username or GTFO") @staticmethod async def validate_2fa(_username, _password, code: str): if code != "666": raise XMPPError("not-authorized", "Wrong code! It's 666.") def __init__(self, session: "Session"): self.session = session self.user_id = -1 self.__task = asyncio.create_task(self.__incoming_messages()) async def __incoming_messages(self): while True: await self.on_contact_message( DirectMessage(666, "Hey devil!", id=uuid.uuid4().hex) ) await self.on_contact_message( DirectMessage(111, "Hey!", id=uuid.uuid4().hex) ) await asyncio.sleep(300) await self.on_contact_message( DirectMessage(222, "Ho!", id=uuid.uuid4().hex) ) await asyncio.sleep(300) async def login(self): pass async def send_direct_msg(self, text: str, contact_id: int): i = uuid.uuid4().hex later( self.on_contact_message( DirectMessage( id=uuid.uuid4().hex, text="A reply", sender=contact_id, reply_to=i ) ) ) return DirectMessage(text=text, id=i, sender=self.user_id) async def send_group_msg(self, text: str, group_id: str): i = uuid.uuid4().hex later( self.on_group_message( GroupMessage( id=uuid.uuid4().hex, text="A reply", group_id=group_id, sender=666, reply_to=i, ) ) ) return GroupMessage(text=text, id=i, sender=self.user_id, group_id=group_id) @staticmethod async def get_profile(user_id: int) -> Profile: return _PROFILES[user_id] @staticmethod async def get_group_info(group_id: str) -> GroupInfo: return _GROUPS[group_id] async def on_contact_message(self, msg: DirectMessage): contact = await self.session.contacts.by_legacy_id(msg.sender) contact.send_text( msg.text, msg.id, reply_to=MessageReference(msg.reply_to, author="user"), ) async def on_group_message(self, msg: GroupMessage): muc = await self.session.bookmarks.by_legacy_id(msg.group_id) participant = await muc.get_participant_by_legacy_id(0) participant.send_text( msg.text, msg.id, reply_to=MessageReference(msg.reply_to, author="user"), ) await asyncio.sleep(1) participant.send_text( msg.text + " (correction)", msg.id, reply_to=MessageReference(msg.reply_to, author="user"), correction=True, ) user = await muc.get_user_participant() if random.random() < 0.5: user.set_hats([("prout", "prout")]) participant.set_hats([]) else: user.set_hats([]) participant.set_hats( [ Hat("12", "gloup"), Hat( str(random.randint(0, 256)), "gloup" + str(random.randint(0, 256)), ), ] ) _PROFILES = { 111: Profile( nickname="Baba", avatar=ASSETS_DIR / "slidge-mono-black.png", full_name="Ba Ba", avatar_unique_id="baba-uid", ), 222: Profile( nickname="Bibi", avatar=ASSETS_DIR / "slidge-mono-white.png", full_name="Bi Bi", avatar_unique_id="bibi-uid", ), 666: Profile( nickname="The devil", avatar=ASSETS_DIR / "5x5.png", full_name="Lucy Fer", avatar_unique_id="devil-uid", ), 000: Profile( nickname="🎉 The joker 🎉", avatar=ASSETS_DIR / "5x5.png", full_name="A guy with emojis in his nick", avatar_unique_id="devil-uid", ), } _GROUPS = { "aaa": GroupInfo( name="The groupchat A", avatar=ASSETS_DIR / "slidge-color-small.png", avatar_unique_id="slidge-color-small", ), "bbb": GroupInfo( name="The groupchat B", ), } slidge/superduper/session.py000066400000000000000000000026041477703150600165740ustar00rootroot00000000000000""" User actions """ from typing import TYPE_CHECKING, Iterable, Optional, Union from slidge import BaseSession, GatewayUser from slidge.util.types import LinkPreview, Mention from .group import MUC, Participant from .legacy_client import SuperDuperClient if TYPE_CHECKING: from .contact import Contact Recipient = Union["Contact", MUC] Sender = Union["Contact", Participant] class Session(BaseSession[str, Recipient]): def __init__(self, user: GatewayUser): super().__init__(user) self.legacy_client = SuperDuperClient(self) self.contacts.user_legacy_id = self.legacy_client.user_id async def login(self): await self.legacy_client.login() return "Success!" async def on_text( self, chat: Recipient, text: str, *, reply_to_msg_id: Optional[str] = None, reply_to_fallback_text: Optional[str] = None, reply_to: Optional[Sender] = None, # type:ignore thread: Optional[str] = None, # type:ignore link_previews: Iterable[LinkPreview] = (), mentions: Optional[list[Mention]] = None, ) -> Optional[str]: if chat.is_group: assert isinstance(chat, MUC) msg = await self.legacy_client.send_group_msg(text, chat.legacy_id) else: msg = await self.legacy_client.send_direct_msg(text, chat.legacy_id) return msg.id slidge/superduper/util.py000066400000000000000000000006071477703150600160670ustar00rootroot00000000000000import asyncio from pathlib import Path ASSETS_DIR = Path(__file__).parent / "assets" if not ASSETS_DIR.exists(): ASSETS_DIR = Path(__file__).parent.parent / "dev" / "assets" if not ASSETS_DIR.exists(): raise FileNotFoundError def later(awaitable): asyncio.create_task(_later(awaitable)) async def _later(awaitable): await asyncio.sleep(0.5) await awaitable slidge/tests/000077500000000000000000000000001477703150600135015ustar00rootroot00000000000000slidge/tests/conftest.py000066400000000000000000000041241477703150600157010ustar00rootroot00000000000000import hashlib import io from base64 import b64encode from contextlib import asynccontextmanager from http import HTTPStatus from pathlib import Path from unittest.mock import patch import pytest from PIL import Image from slidge.util import SubclassableOnce SubclassableOnce.TEST_MODE = True @pytest.fixture def MockRE(): class MockRE: @staticmethod def match(*a, **kw): return True return MockRE @pytest.fixture(scope="class") def avatar(request): path = Path(__file__).parent.parent / "dev" / "assets" / "5x5.png" img = Image.open(path) with io.BytesIO() as f: img.save(f, format="PNG") img_bytes = f.getvalue() class MockResponse: def __init__(self, status): self.status = status @staticmethod async def read(): return img_bytes def raise_for_status(self): pass headers = {"etag": "etag", "last-modified": "last"} @asynccontextmanager async def mock_get(url, headers=None): assert url == "AVATAR_URL" if headers and ( headers.get("If-None-Match") == "etag" or headers.get("If-Modified-Since") == "last" ): yield MockResponse(HTTPStatus.NOT_MODIFIED) else: yield MockResponse(HTTPStatus.OK) request.cls.avatar_path = path request.cls.avatar_image = img request.cls.avatar_bytes = img_bytes request.cls.avatar_sha1 = hashlib.sha1(img_bytes).hexdigest() request.cls.avatar_url = "AVATAR_URL" request.cls.avatar_base64 = b64encode(img_bytes).decode("utf-8") request.cls.avatar_original_sha1 = hashlib.sha1(path.read_bytes()).hexdigest() with patch("slidge.db.avatar.avatar_cache.http", create=True) as mock: mock.get = mock_get mock.head = mock_get yield request # just to have typings for the fixture which pycharm does not understand class AvatarFixtureMixin: avatar_path: Path avatar_image: Image avatar_bytes: bytes avatar_sha1: str avatar_original_sha1: str avatar_url: str avatar_base64: str slidge/tests/test_adhoc/000077500000000000000000000000001477703150600156165ustar00rootroot00000000000000slidge/tests/test_adhoc/test_access.py000066400000000000000000000204601477703150600204720ustar00rootroot00000000000000import pytest from slixmpp import ComponentXMPP from slixmpp.plugins.xep_0050.adhoc import XEP_0050 import slidge.command.adhoc import slidge.command.base from slidge.command import Command, CommandAccess from slidge.command.adhoc import AdhocProvider from slidge.command.base import Confirmation from slidge.util.test import SlixTestPlus class MockSession: def __init__(self, jid): self.logged = "logged" in jid.user @pytest.fixture(autouse=True) def mock(monkeypatch, MockRE): monkeypatch.setattr( slidge.command.base, "is_admin", lambda j: j.user.startswith("admin") ) monkeypatch.setattr(Command, "_get_session", lambda s, j: MockSession(j)) monkeypatch.setattr(XEP_0050, "new_session", lambda _: "session-id") monkeypatch.setattr( ComponentXMPP, "jid_validator", MockRE, raising=False, ) monkeypatch.setattr( ComponentXMPP, "get_session_from_stanza", lambda s, j: MockSession(j.get_from()), raising=False, ) class Command1(Command): NAME = "Command number one" NODE = "command1" ACCESS = CommandAccess.ADMIN_ONLY class Command2(Command1): NAME = "Command number two" NODE = "command2" ACCESS = CommandAccess.ADMIN_ONLY class Command3(Command): NAME = "Command number three" NODE = "command3" CATEGORY = "category" ACCESS = CommandAccess.ADMIN_ONLY async def run(self, _session, _ifrom): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): pass class Command4(Command): NAME = "Command number four" NODE = "command4" CATEGORY = "category" ACCESS = CommandAccess.ADMIN_ONLY async def run(self, _session, _ifrom): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): return "OK" class TestCommandsDisco(SlixTestPlus): def setUp(self): self.stream_start( mode="component", plugins=["xep_0050"], jid="slidge.whatever.ass", server="whatever.ass", ) self.adhoc = AdhocProvider(self.xmpp) self.adhoc.register(Command1(self.xmpp)) self.adhoc.register(Command2(self.xmpp)) self.adhoc.register(Command3(self.xmpp)) self.adhoc.register(Command4(self.xmpp)) super().setUp() def test_disco_admin(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) def test_non_existing_command(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, use_values=False, ) def test_category(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ category """, use_values=False, ) self.recv( # language=XML f""" command3 """ ) self.send( # language=XML """ Confirm? 1 """ ) slidge/tests/test_adhoc/test_confirmation.py000066400000000000000000000226421477703150600217250ustar00rootroot00000000000000import pytest import slixmpp.test from slixmpp import ComponentXMPP from slixmpp.plugins.xep_0050.adhoc import XEP_0050 import slidge.command.adhoc from slidge.command import Command, Confirmation from slidge.command.adhoc import AdhocProvider from slidge.util.test import SlixTestPlus class MockSession: def __init__(self, jid): self.jid = jid self.logged = "logged" in jid.user @pytest.fixture(autouse=True) def mock(monkeypatch, MockRE): monkeypatch.setattr( slidge.command.base, "is_admin", lambda j: j.user.startswith("admin") ) monkeypatch.setattr(Command, "_get_session", lambda s, j: MockSession(j)) monkeypatch.setattr( slixmpp.test.ComponentXMPP, "get_session_from_stanza", lambda self, stanza: MockSession(stanza.get_from()), raising=False, ) monkeypatch.setattr(XEP_0050, "new_session", lambda _: "session-id") monkeypatch.setattr( ComponentXMPP, "jid_validator", MockRE, raising=False, ) class CommandAdmin(Command): NAME = "Command number one" NODE = "command1" async def run(self, _session, _ifrom): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): pass class CommandAdminConfirmFail(CommandAdmin): NAME = "Command number two" NODE = "command2" async def run_admin(self): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): raise RuntimeError("Ploup") class TestCommandsConfirmation(SlixTestPlus): def setUp(self): super().setUp() self.stream_start( mode="component", plugins=["xep_0050"], jid="slidge.whatever.ass", server="whatever.ass", ) self.adhoc = AdhocProvider(self.xmpp) self.adhoc.register(CommandAdmin(self.xmpp)) self.adhoc.register(CommandAdminConfirmFail(self.xmpp)) def test_confirmation_cancel(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ Confirm? 1 """ ) self.recv( # language=XML f""" """ ) self.recv( # language=XML """ """ ) def test_confirmation_do_it(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ Confirm? 1 """ ) self.recv( # language=XML f""" 1 """ ) self.send( # language=XML """ It worked! """ ) def test_confirmation_fail(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ Confirm? 1 """ ) self.recv( # language=XML f""" 1 """ ) self.send( # language=XML """ Ploup """, use_values=False, ) slidge/tests/test_adhoc/test_form.py000066400000000000000000000437531477703150600202060ustar00rootroot00000000000000import pytest from slixmpp import JID, ComponentXMPP from slixmpp.plugins.xep_0050.adhoc import XEP_0050 import slidge.command.adhoc from slidge.command import Command, Form from slidge.command.adhoc import AdhocProvider from slidge.command.base import FormField from slidge.util.test import SlixTestPlus class MockSession: def __init__(self, jid): # self.jid = jid self.logged = True @pytest.fixture(autouse=True) def mock(monkeypatch, MockRE): monkeypatch.setattr( slidge.command.base, "is_admin", lambda j: j.user.startswith("admin") ) monkeypatch.setattr(Command, "_get_session", lambda s, j: MockSession(j)) monkeypatch.setattr(XEP_0050, "new_session", lambda _: "session-id") monkeypatch.setattr( ComponentXMPP, "jid_validator", MockRE, raising=False, ) class Command1(Command): NAME = "Command number one" NODE = "command1" async def run(self, session, ifrom): return Form( title="A title", instructions="Some instructions", fields=[ FormField( "jid", type="jid-single", label="Enter a JID", value="user@host", required=True, ), FormField( "option", type="list-single", options=[ {"label": "Option 1", "value": "option1"}, {"label": "Option 2", "value": "option2"}, ], ), ], handler=self.finish, handler_kwargs={"arg1": "argument 1"}, ) @staticmethod async def finish(form_values, _session, ifrom, arg1): if form_values["jid"] == "bad@bad": raise RuntimeError("IT'S BAD, WE'RE FUCKED") assert isinstance(form_values["jid"], JID) return f"all good mate, {arg1}" class Command2(Command): NAME = "Command number two" NODE = "command2" async def run(self, session, ifrom): return Form( title="A title", instructions="Some instructions", fields=[ FormField( "str", type="list-multi", options=[ {"label": "Option 1", "value": "option1"}, {"label": "Option 2", "value": "option2"}, ], ), ], handler=self.finish, handler_kwargs={"arg1": "argument 1"}, ) @staticmethod async def finish(form_values, _session, ifrom, arg1): assert isinstance(form_values["str"], list) assert all(isinstance(f, str) for f in form_values["str"]) return f"all good mate, {arg1}" class TestCommandsResults(SlixTestPlus): def setUp(self): super().setUp() self.stream_start( mode="component", plugins=["xep_0050"], jid="slidge.whatever.ass", server="whatever.ass", ) self.adhoc = AdhocProvider(self.xmpp) self.adhoc.register(Command1(self.xmpp)) self.adhoc.register(Command2(self.xmpp)) def test_form_ok(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A title Some instructions user@host """ ) self.recv( # language=XML f""" value@value option1 """ ) self.send( # language=XML """ all good mate, argument 1 """ ) def test_form_bad_option(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A title Some instructions user@host """ ) self.recv( # language=XML f""" value@value option3 """ ) self.send( # language=XML """ Not a valid option: 'option3' """, use_values=False, ) def test_form_exc(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A title Some instructions user@host """ ) self.recv( # language=XML f""" bad@bad """ ) self.send( # language=XML """ IT'S BAD, WE'RE FUCKED """, use_values=False, ) def test_form_bad_jid(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A title Some instructions user@host """ ) self.recv( # language=XML f""" bad@bad@bad """ ) self.send( # language=XML """ Not a valid JID: 'bad@bad@bad' """, use_values=False, ) def test_multi(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A title Some instructions """ ) self.recv( # language=XML f""" option1 option2 """ ) self.send( # language=XML """ all good mate, argument 1 """ ) slidge/tests/test_adhoc/test_reported.py000066400000000000000000000071011477703150600210520ustar00rootroot00000000000000import pytest from slixmpp import JID, ComponentXMPP from slixmpp.plugins.xep_0050.adhoc import XEP_0050 from slixmpp.test import SlixTest import slidge.command.adhoc from slidge.command import Command, TableResult from slidge.command.adhoc import AdhocProvider from slidge.command.base import FormField class MockSession: def __init__(self, jid): self.logged = True @pytest.fixture(autouse=True) def mock(monkeypatch, MockRE): monkeypatch.setattr( slidge.command.base, "is_admin", lambda j: j.user.startswith("admin") ) monkeypatch.setattr(Command, "_get_session", lambda s, j: MockSession(j)) monkeypatch.setattr( ComponentXMPP, "jid_validator", MockRE, raising=False, ) monkeypatch.setattr(XEP_0050, "new_session", lambda _: "session-id") class Command1(Command): NAME = "Command number one" NODE = "command1" async def run(self, _session, _ifrom): return TableResult( description="A description", fields=[ FormField("name", label="JID"), FormField("jid", type="jid-single", label="JID"), ], items=[ {"jid": JID("test@test"), "name": "Some dude"}, {"jid": "test2@test", "name": "Some dude2"}, ], ) class TestCommandsResults(SlixTest): def setUp(self): self.stream_start( mode="component", plugins=["xep_0050"], jid="slidge.whatever.ass", server="whatever.ass", ) self.adhoc = AdhocProvider(self.xmpp) self.adhoc.register(Command1(self.xmpp)) def test_table_result(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ A description Some dude2 test2@test Some dude test@test """, use_values=False, ) slidge/tests/test_attachment.py000066400000000000000000000402541477703150600172470ustar00rootroot00000000000000import os import shutil import tempfile from contextlib import asynccontextmanager from datetime import datetime from unittest.mock import ANY, MagicMock, patch import pytest from conftest import AvatarFixtureMixin from test_shakespeare import Base as Shakespeare from slidge.core import config from slidge.util.types import LegacyAttachment, MessageReference @pytest.fixture(scope="function") def attachment(request): class MockResponse: status = 200 class MockAioHTTP: @asynccontextmanager async def head(*a, **k): yield MockResponse with ( patch( "slixmpp.plugins.xep_0363.http_upload.XEP_0363.upload_file", return_value="http://url", ) as http_upload, patch("aiohttp.ClientSession", return_value=MockAioHTTP) as client_session, patch("slidge.core.mixins.attachment.uuid4", return_value="uuid"), ): request.cls.head = client_session.head = MockAioHTTP.head request.cls.http_upload = http_upload yield @pytest.mark.usefixtures("avatar") @pytest.mark.usefixtures("attachment") class Base(Shakespeare, AvatarFixtureMixin): http_upload: MagicMock def _assert_body(self, text="body", i=None): if i: self.send( # language=XML f""" {text} """, use_values=False, ) else: self.send( # language=XML f""" {text} """, use_values=False, ) def _assert_file(self, url="http://url"): when = ( datetime.fromtimestamp(self.avatar_path.stat().st_mtime) .isoformat() .replace("+00:00", "Z") ) self.send( # language=XML f""" image/png 5x5.png 547 {when} NdpqDQuHlshve2c0iU25l2KI4cjpoyzaTk3a/CdbjPQ= image/png 5x5.png 547 {when} NdpqDQuHlshve2c0iU25l2KI4cjpoyzaTk3a/CdbjPQ= {url} {url} """, use_values=False, ) class TestBodyOnly(Base): def test_no_file_no_body(self): self.run_coro(self.juliet.send_files([])) assert self.next_sent() is None def test_just_body(self): self.run_coro(self.juliet.send_files([], body="body")) self._assert_body() self.run_coro(self.juliet.send_files([], body="body", body_first=True)) self._assert_body() self.run_coro(self.juliet.send_files([], body="body", legacy_msg_id=12)) self._assert_body(i=12) class TestAttachmentUpload(Base): def __test_basic(self, attachment: LegacyAttachment, upload_kwargs: dict): """ Basic test that file is uploaded. """ self.run_coro(self.juliet.send_files([attachment])) self.http_upload.assert_called_with(**upload_kwargs) self._assert_file() def _test_reuse(self, attachment: LegacyAttachment, upload_kwargs: dict): """ Basic test the no new file is uploaded when the same attachment is used twice. """ self.run_coro(self.juliet.send_files([attachment])) self.http_upload.assert_called_with(**upload_kwargs) self._assert_file() self.http_upload.reset_mock() self.run_coro(self.juliet.send_files([attachment])) self.http_upload.assert_not_called() self._assert_file() def test_path(self): self.__test_basic( LegacyAttachment(path=self.avatar_path), dict( filename=self.avatar_path, content_type="image/png", ifrom=self.xmpp.boundjid, domain=None, ), ) def test_thumbhash(self): self.__test_basic( LegacyAttachment(path=self.avatar_path, content_type="image/png"), dict( filename=self.avatar_path, content_type="image/png", ifrom=self.xmpp.boundjid, domain=None, ), ) def test_path_and_id(self): self._test_reuse( LegacyAttachment(path=self.avatar_path, legacy_file_id=1235), dict( filename=self.avatar_path, content_type="image/png", ifrom=self.xmpp.boundjid, domain=None, ), ) def test_bytes(self): with patch("pathlib.Path.stat", return_value=os.stat(self.avatar_path)): self.__test_basic( LegacyAttachment(data=self.avatar_path.read_bytes(), name="5x5.png"), dict( filename=ANY, content_type="image/png", ifrom=self.xmpp.boundjid, domain=None, ), ) def test_bytes_and_id(self): with patch("pathlib.Path.stat", return_value=os.stat(self.avatar_path)): self._test_reuse( LegacyAttachment( data=self.avatar_path.read_bytes(), legacy_file_id=123, name="5x5.png", ), dict( filename=ANY, content_type="image/png", ifrom=self.xmpp.boundjid, domain=None, ), ) class TestAttachmentNoUpload(Base): @classmethod def setUpClass(cls): super().setUpClass() config.NO_UPLOAD_URL_PREFIX = "https://url" @classmethod def tearDownClass(cls): super().tearDownClass() config.NO_UPLOAD_PATH = None config.NO_UPLOAD_URL_PREFIX = None def setUp(self): super().setUp() config.NO_UPLOAD_PATH = tempfile.TemporaryDirectory().name def tearDown(self): super().tearDown() shutil.rmtree(config.NO_UPLOAD_PATH) def __test_basic(self, attachment: LegacyAttachment, url: str): """ Basic test that file is copied. """ self.run_coro(self.juliet.send_files([attachment])) self._assert_file(url=url) def __test_reuse(self, attachment: LegacyAttachment, url: str): """ Basic test the no new file is copied when the same attachment is used twice. """ self.run_coro(self.juliet.send_files([attachment])) self._assert_file(url=url) self.run_coro(self.juliet.send_files([attachment])) self._assert_file(url=url) def test_path(self): self.__test_basic( LegacyAttachment(path=self.avatar_path), "https://url/uuid/uuid/5x5.png" ) def test_path_and_id(self): self.__test_reuse( LegacyAttachment(path=self.avatar_path, legacy_file_id=1234), "https://url/1234/uuid/5x5.png", ) def test_multi(self): self.xmpp.LEGACY_MSG_ID_TYPE = int self.xmpp.use_message_ids = True self.run_coro( self.juliet.send_files( [ LegacyAttachment(path=self.avatar_path), LegacyAttachment(path=self.avatar_path, caption="CAPTION"), ], legacy_msg_id=6666, body="BODY", ) ) xmpp_ids = [] for _ in range(2): att = self.next_sent() xmpp_ids.append(att.get_id()) caption = self.next_sent() assert caption["body"] == "CAPTION" xmpp_ids.append(caption.get_id()) body = self.next_sent() assert body["body"] == "BODY" xmpp_ids.append(body.get_id()) assert self.next_sent() is None assert len(set(xmpp_ids)) == len(xmpp_ids) self.juliet.react(6666, "♥") reaction = self.next_sent() assert reaction["reactions"]["id"] in xmpp_ids self.recv( # language=XML """ """ ) for i in xmpp_ids: with patch("test_shakespeare.Session.on_react") as mock: self.recv( # language=XML f""" 👋 🐢 """ ) for j in [k for k in xmpp_ids if k != i]: reac = self.next_sent() assert reac["privilege"]["forwarded"]["message"]["reactions"]["id"] == j mock.assert_awaited_once() assert mock.call_args[0][0].jid == self.juliet.jid assert mock.call_args[0][1] == 6666 assert mock.call_args[0][2] == ["👋", "🐢"] assert mock.call_args[1] == dict(thread=None) self.xmpp.use_message_ids = False self.xmpp.LEGACY_MSG_ID_TYPE = True def test_multi_moderation(self): session = self.get_romeo_session() muc = self.run_coro(session.bookmarks.by_legacy_id("room")) muc.add_user_resource("gajim") part = muc.get_system_participant() self.run_coro( part.send_files( [ LegacyAttachment(path=self.avatar_path), LegacyAttachment(path=self.avatar_path, caption="CAPTION"), ], legacy_msg_id="the-real-msg-id", body="BODY", ) ) stanza_ids = [] while (stanza := self.next_sent()) is not None: stanza_ids.append(stanza["stanza_id"]["id"]) assert len(stanza_ids) == 4 # 2 attachments, the caption and the body assert "the-real-msg-id" in stanza_ids part.moderate("the-real-msg-id") moderated_ids = [] while (stanza := self.next_sent()) is not None: moderated_ids.append(stanza["retract"]["id"]) assert set(stanza_ids) == set(moderated_ids) def test_reply_with_attachment(self): self.run_coro( self.juliet.send_files( [ LegacyAttachment(path=self.avatar_path), ], reply_to=MessageReference("some_msg_id", body="a body"), ) ) when = ( datetime.fromtimestamp(self.avatar_path.stat().st_mtime) .isoformat() .replace("+00:00", "Z") ) self.send( # language=XML f""" https://url/uuid/uuid/5x5.png image/png 5x5.png 547 {when} NdpqDQuHlshve2c0iU25l2KI4cjpoyzaTk3a/CdbjPQ= image/png 5x5.png 547 {when} NdpqDQuHlshve2c0iU25l2KI4cjpoyzaTk3a/CdbjPQ= https://url/uuid/uuid/5x5.png """, use_values=False, ) slidge/tests/test_avatar.py000066400000000000000000000411411477703150600163710ustar00rootroot00000000000000import unittest.mock import pytest from conftest import AvatarFixtureMixin from test_shakespeare import Base as BaseNoMUC from slidge import LegacyMUC, MucType @pytest.mark.usefixtures("avatar") class TestContactAvatar(BaseNoMUC, AvatarFixtureMixin): def setUp(self): super().setUp() self.juliet.is_friend = True self.juliet.added_to_roster = True def __assert_not_found(self): juliet = self.juliet self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) def __assert_publish(self, rewritten=False): h = self.avatar_sha1 if rewritten else self.avatar_original_sha1 length = ( len(self.avatar_bytes) if rewritten else len(self.avatar_path.read_bytes()) ) self.send( # language=XML f""" """ ) assert self.next_sent() is None def __assert_publish_empty(self): self.send( # language=XML f""" """, use_values=False, ) assert self.next_sent() is None def test_avatar_path_no_id(self): juliet = self.juliet assert juliet.avatar is None juliet.avatar = None self.run_coro(juliet._set_avatar_task) assert self.next_sent() is None self.__assert_not_found() juliet.avatar = self.avatar_path self.run_coro(juliet._set_avatar_task) self.__assert_publish() juliet.avatar = self.avatar_path self.run_coro(juliet._set_avatar_task) assert self.next_sent() is None self.run_coro(juliet.set_avatar(self.avatar_path)) assert self.next_sent() is None self.run_coro(juliet.set_avatar(self.avatar_path)) assert self.next_sent() is None juliet.avatar = self.avatar_path self.run_coro(juliet._set_avatar_task) assert self.next_sent() is None juliet.avatar = None self.run_coro(juliet._set_avatar_task) self.__assert_publish_empty() self.run_coro(juliet.set_avatar(None)) assert self.next_sent() is None self.run_coro(juliet.set_avatar(self.avatar_path)) self.__assert_publish() juliet.avatar = None self.run_coro(juliet._set_avatar_task) self.__assert_publish_empty() def test_avatar_path_with_id(self): juliet = self.juliet assert juliet.avatar is None self.xmpp.AVATAR_ID_TYPE = int self.run_coro(juliet.set_avatar(self.avatar_path, 123)) self.__assert_publish(rewritten=False) assert juliet.avatar_id == 123 self.run_coro(juliet.set_avatar(self.avatar_path, 123)) assert self.next_sent() is None assert juliet.avatar_id == 123 self.xmpp.AVATAR_ID_TYPE = str self.run_coro(juliet.set_avatar(self.avatar_path, "123")) self.__assert_publish(rewritten=False) assert juliet.avatar_id == "123" self.run_coro(juliet.set_avatar(None)) self.__assert_publish_empty() assert juliet.avatar_id is None def test_same_avatar_with_different_legacy_ids(self): self.run_coro(self.juliet.set_avatar(self.avatar_path, "123")) self.__assert_publish(rewritten=False) assert self.juliet.avatar_id == "123" assert self.juliet.get_avatar().id == self.avatar_original_sha1 self.run_coro(self.juliet.set_avatar(self.avatar_path, "456")) self.__assert_publish( rewritten=False ) # FIXME: ideally, we should not publish here… assert self.juliet.avatar_id == "456" assert self.juliet.get_avatar().id == self.avatar_original_sha1 def test_avatar_with_url(self): juliet = self.juliet assert juliet.avatar is None juliet.avatar = self.avatar_url self.run_coro(juliet._set_avatar_task) self.__assert_publish(rewritten=True) juliet.avatar = self.avatar_url self.run_coro(juliet._set_avatar_task) assert self.next_sent() is None def test_avatar_with_url_and_unique_id(self): juliet = self.juliet assert juliet.avatar is None self.run_coro(juliet.set_avatar(self.avatar_url, "someid", blocking=True)) self.__assert_publish(rewritten=True) self.run_coro(juliet.set_avatar(self.avatar_url, "someid", blocking=True)) assert self.next_sent() is None class MUC(LegacyMUC): type = MucType.GROUP user_nick = "romeo" class BaseMUC(BaseNoMUC): plugin = BaseNoMUC.plugin | {"MUC": MUC} def setUp(self): self.patch = unittest.mock.patch( "slidge.core.mixins.message_maker.uuid4", return_value="uuid4" ) self.patch.start() super().setUp() def tearDown(self): super().tearDown() self.patch.stop() def _assert_send_room_avatar(self, empty=False, url=False): if empty: photo = "" else: photo = f"{self.avatar_sha1 if url else self.avatar_original_sha1}" self.send( # language=XML f""" {photo} """, use_values=not empty, ) def romeo_joins(self, muc: MUC): session = self.get_romeo_session() self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) assert self.next_sent()["subject"] != "" # assert self.next_sent()["from"] == "room@aim.shakespeare.lit" def get_muc(self, joined=True) -> MUC: session = self.get_romeo_session() muc = self.run_coro(session.bookmarks.by_legacy_id("room")) if joined: self.romeo_joins(muc) return muc @pytest.mark.usefixtures("avatar") class TestParticipantAvatar(BaseMUC, AvatarFixtureMixin): def romeo_joins(self, muc: MUC): super().romeo_joins(muc) self._assert_send_room_avatar(empty=True) def _assert_juliet_presence_no_avatar(self): self.send( # language=XML """ """ ) def _assert_juliet_presence_avatar(self, sha=None, url=False): self.send( # language=XML f""" {self.avatar_sha1 if url else self.avatar_original_sha1} """ ) def test_romeo_join_empty_room_then_juliet_joins_then_set_avatar(self): self.get_muc(joined=True) session = self.get_romeo_session() session.contacts.ready.set_result(True) muc = self.get_muc(joined=False) self.run_coro(muc.get_participant_by_contact(self.juliet)) self._assert_juliet_presence_no_avatar() assert self.next_sent() is None juliet = self.juliet juliet.avatar = self.avatar_path # no broadcast of the contact avatar because not added to roster, # only the participant self.run_coro(juliet._set_avatar_task) self._assert_juliet_presence_avatar() assert self.next_sent() is None self.juliet.avatar = self.avatar_path assert self.next_sent() is None juliet = self.juliet juliet.avatar = None self.run_coro(juliet._set_avatar_task) self._assert_juliet_presence_no_avatar() assert self.next_sent() is None def test_romeo_join_empty_room_then_juliet_joins_then_set_avatar_with_url(self): self.get_muc(joined=True) session = self.get_romeo_session() session.contacts.ready.set_result(True) juliet = self.juliet self.run_coro(self.get_muc(joined=False).get_participant_by_contact(juliet)) self._assert_juliet_presence_no_avatar() assert self.next_sent() is None juliet = self.juliet juliet.avatar = self.avatar_url # no broadcast of the contact avatar because not added to roster, # only the participant self.run_coro(juliet._set_avatar_task) self._assert_juliet_presence_avatar(url=True) assert self.next_sent() is None juliet.avatar = self.avatar_url self.run_coro(juliet._set_avatar_task) assert self.next_sent() is None juliet.avatar = None self.run_coro(juliet._set_avatar_task) self._assert_juliet_presence_no_avatar() assert self.next_sent() is None def test_avatar_forbidden_emoji_in_participant_nickname(self): self.get_muc(joined=True) session = self.get_romeo_session() juliet = self.juliet juliet.name = "juliet🎉" juliet.avatar = self.avatar_url session.contacts.ready.set_result(True) self.run_coro(self.get_muc(False).get_participant_by_contact(juliet)) self.send( # language=XML f""" {self.avatar_sha1} juliet🎉 """ ) self.recv( # language=XML """ """ ) self.send( # language=XML f""" {self.avatar_base64} image/png """ ) assert self.next_sent() is None juliet = self.run_coro(self.get_muc(False).get_participant_by_contact(juliet)) juliet.send_text("prout") self.send( # language=XML f""" prout juliet🎉 """ ) @pytest.mark.usefixtures("avatar") class TestRoomAvatar(BaseMUC, AvatarFixtureMixin): def test_room_avatar_change_after_join(self): self.get_muc(joined=True) self._assert_send_room_avatar(empty=True) muc = self.get_muc(joined=False) muc.avatar = self.avatar_path self.run_coro(muc._set_avatar_task) self.send( # language=XML """ """, use_values=False, ) self._assert_send_room_avatar() def test_room_avatar_on_join(self): muc = self.get_muc(joined=False) muc.avatar = self.avatar_path self.romeo_joins(muc) self._assert_send_room_avatar() def test_room_avatar_with_url(self): muc = self.get_muc(joined=False) muc.avatar = self.avatar_url self.run_coro(muc._set_avatar_task) self.romeo_joins(muc) self._assert_send_room_avatar(url=True) def test_room_avatar_with_url_and_unique_id(self): muc = self.get_muc(joined=False) self.run_coro(muc.set_avatar(self.avatar_url, "id", blocking=True)) self.romeo_joins(muc) self._assert_send_room_avatar(url=True) self.run_coro(muc.set_avatar(self.avatar_url, "id", blocking=True)) assert self.next_sent() is None slidge/tests/test_backfill.py000066400000000000000000000103411477703150600166600ustar00rootroot00000000000000import datetime import unittest.mock import sqlalchemy as sa from slixmpp import Message from slidge import BaseGateway, BaseSession from slidge.core.session import _sessions from slidge.db.models import ArchivedMessage from slidge.util.archive_msg import HistoryMessage from slidge.util.test import SlidgeTest class Gateway(BaseGateway): COMPONENT_NAME = "A test" class Session(BaseSession): async def login(self): return "YUP" class TestBackfill(SlidgeTest): plugin = globals() xmpp: Gateway def setUp(self): super().setUp() self.setup_logged_session() self.xmpp.LEGACY_MSG_ID_TYPE = int def tearDown(self): with self.xmpp.store.session() as orm: orm.execute(sa.delete(ArchivedMessage)) super().tearDown() _sessions.clear() def test_empty_archive(self): with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_with(None, None) def test_live_no_id_before_backfill(self): self.first_witch.send_text("BODY 1") self.first_witch.send_text("BODY 2") self.first_witch.send_text( "BODY 3", when=datetime.datetime.now(tz=datetime.timezone.utc) ) with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_with(None, None) def test_live_with_id_before_backfill(self): now = datetime.datetime.now(tz=datetime.timezone.utc) self.first_witch.send_text("BODY 2", 222, when=now) self.first_witch.send_text( "BODY 1", 111, when=now - datetime.timedelta(hours=1) ) self.first_witch.send_text( "BODY 3", 333, when=now + datetime.timedelta(hours=1) ) with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_once() after, before = backfill.call_args[0] assert before.id == 111 assert before.timestamp == now - datetime.timedelta(hours=1) assert after is None def _add_back_filled_msg(self, legacy_id=None, when=None): self.xmpp.store.mam.add_message( self.room.pk, HistoryMessage(Message(), when), archive_only=True, legacy_msg_id=legacy_id, ) def test_pre_backfilled_no_id(self): self._add_back_filled_msg() with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_with(None, None) def test_pre_backfilled_with_id(self): self._add_back_filled_msg(None) self._add_back_filled_msg(111) self._add_back_filled_msg(222) self._add_back_filled_msg(None) with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_once() after, before = backfill.call_args[0] assert before is None assert after is not None assert after.id == 222 def test_pre_backfilled_with_id_and_live(self): now = datetime.datetime.now(tz=datetime.timezone.utc) self._add_back_filled_msg(None, now - datetime.timedelta(days=5)) self._add_back_filled_msg(111, now - datetime.timedelta(days=4)) self._add_back_filled_msg(222, now - datetime.timedelta(days=3)) self._add_back_filled_msg(None, now - datetime.timedelta(days=2)) self.first_witch.send_text("BODY1", None) self.first_witch.send_text("BODY2", 555) self.first_witch.send_text("BODY3", None) self.first_witch.send_text("BODY5", 666) self.first_witch.send_text("BODY6", None) self.first_witch.send_text("BODY7", None) with unittest.mock.patch("slidge.group.LegacyMUC.backfill") as backfill: self.run_coro(self.room._LegacyMUC__fill_history()) backfill.assert_awaited_once() after, before = backfill.call_args[0] assert before.id == 555 assert after.id == 222 slidge/tests/test_chat_commands.py000066400000000000000000000174651477703150600177270ustar00rootroot00000000000000import pytest import slixmpp.test from slixmpp import JID import slidge.command.chat_command from slidge.command import Command, Confirmation from slidge.slixfix.delivery_receipt import DeliveryReceipt from slidge.util.test import SlixTestPlus class MockSession: def __init__(self, jid): self.logged = "logged" in jid.user @pytest.fixture(autouse=True) def mock(monkeypatch, MockRE): monkeypatch.setattr(Command, "_get_session", lambda s, j: MockSession(j)) monkeypatch.setattr( slixmpp.test.ComponentXMPP, "get_session_from_stanza", lambda self, stanza: MockSession(stanza.get_from()), raising=False, ) monkeypatch.setattr( slixmpp.test.ComponentXMPP, "jid_validator", MockRE, raising=False, ) class CommandAdmin(Command): NAME = "Command number one" CHAT_COMMAND = "command1" test_results = [] async def run(self, _session, _ifrom): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): self.test_results.append("yup") class CommandAdminConfirmFail(CommandAdmin): NAME = "Command number two" CHAT_COMMAND = "command2" async def run_admin(self): return Confirmation( prompt="Confirm?", handler=self.finish, success="It worked!" ) async def finish(self, _session, _ifrom): raise RuntimeError("Ploup") class TestChatCommands(SlixTestPlus): def setUp(self): self.stream_start( mode="component", plugins=["xep_0050"], jid="slidge.whatever.ass", server="whatever.ass", ) self.commands = slidge.command.chat_command.ChatCommandProvider(self.xmpp) self.commands.register(CommandAdmin(self.xmpp)) self.commands.register(CommandAdminConfirmFail(self.xmpp)) self.xmpp.delivery_receipt = DeliveryReceipt(self.xmpp) super().setUp() def test_non_existing(self): self.recv( # language=XML f""" non-existing """ ) t = self.commands.UNKNOWN.format("non-existing") self.send( # language=XML f""" {t} """ ) self.send( # language=XML f""" {t} """, use_values=False, ) def test_other_destination(self): self.recv( # language=XML f""" help """ ) assert self.next_sent() is None def test_command_help(self): self.recv( # language=XML f""" help """ ) self.send( # language=XML f""" Available commands:\ncommand1 -- Command number one\ncommand2 -- Command number two """ ) def test_input(self): fut = self.run_coro( self.commands.input(JID("user@whatever.ass/x"), "blabla", blocking=False) ) self.send( # language=XML """ blabla """ ) self.recv( # language=XML """ reply """ ) assert fut.result() == "reply" def test_confirm_no(self): self.recv( # language=XML f""" command1 """ ) self.send( # language=XML f""" Confirm? """ ) self.recv( # language=XML f""" no """ ) self.send( # language=XML f""" Canceled """ ) assert len(CommandAdmin.test_results) == 0 def test_confirm_yes(self): self.recv( # language=XML f""" command1 """ ) self.send( # language=XML f""" Confirm? """ ) self.recv( # language=XML f""" yes """ ) self.send( # language=XML f""" End of command. """ ) assert CommandAdmin.test_results.pop() == "yup" assert len(CommandAdmin.test_results) == 0 slidge/tests/test_config.py000066400000000000000000000167631477703150600163740ustar00rootroot00000000000000import logging from pathlib import Path from typing import Optional import pytest from slixmpp import JID from slidge import main from slidge.core import config from slidge.util.conf import ConfigModule def test_get_parser(monkeypatch): class Config: REQUIRED: str REQUIRED__DOC = "some doc" REQUIRED__SHORT = "r" REQUIRED_INT: int REQUIRED_INT__DOC = "some doc" MULTIPLE: tuple[str, ...] = () MULTIPLE__DOC = "some more doc" OPTIONAL: Optional[str] = None OPTIONAL__DOC = "not required" SOME_BOOL = False SOME_BOOL__DOC = "a bool" monkeypatch.setattr(main, "config", Config) parser = main.get_parser() with pytest.raises(SystemExit) as e: parser.parse_known_args([]) assert e.value.args[0] == 2 # Exit code 2 args = parser.parse_args(["--required", "some_value", "--required-int", "45"]) assert args.required == "some_value" assert args.required_int == 45 args = parser.parse_args(["-r", "some_value", "--required-int", "45"]) assert args.required == "some_value" assert args.required_int == 45 assert args.multiple == tuple() args = parser.parse_args( ["-r", "some_value", "--required-int", "45", "--multiple", "a", "b"] ) assert args.required == "some_value" assert args.required_int == 45 assert args.multiple == ["a", "b"] assert args.optional is None args = parser.parse_args( [ "-r", "some_value", "--required-int", "45", "--multiple", "a", "b", "--optional", "prout", "--some-bool", ] ) assert args.required == "some_value" assert args.required_int == 45 assert args.multiple == ["a", "b"] assert args.optional == "prout" assert args.some_bool def test_bool(tmpdir, tmp_path): class Config: SOME_BOOL = False SOME_BOOL__DOC = "a bool" TRUE = True TRUE__DOC = "true by default" configurator = ConfigModule(Config) configurator.set_conf([]) assert not Config.SOME_BOOL assert Config.TRUE configurator.set_conf(["--some-bool"]) assert Config.SOME_BOOL assert Config.TRUE configurator.set_conf(["--true"]) assert not Config.SOME_BOOL assert Config.TRUE configurator.set_conf(["--true=false"]) assert not Config.SOME_BOOL assert not Config.TRUE configurator.set_conf(["--true=true"]) assert not Config.SOME_BOOL assert Config.TRUE configurator.set_conf(["--some-bool=true"]) assert Config.SOME_BOOL assert Config.TRUE configurator.set_conf(["--some-bool=false"]) assert not Config.SOME_BOOL assert Config.TRUE # for the plugin-specific conf files, we use the rest configurator.parser.add_argument("-c", is_config_file=True) class Config2: SOME_OTHER_BOOL = False SOME_OTHER_BOOL__DOC = "a bool" TRUE2 = True TRUE2__DOC = "true by default" configurator2 = ConfigModule(Config2) conf_file = tmpdir / "conf.conf" # false conf_file.write_text("some-other-bool=false", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert rest configurator2.set_conf(rest) assert not Config2.SOME_OTHER_BOOL assert Config2.TRUE2 # true conf_file.write_text("some-other-bool=true", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert rest configurator2.set_conf(rest) assert Config2.SOME_OTHER_BOOL assert Config2.TRUE2 # true conf_file.write_text("", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert not rest configurator2.set_conf(rest) assert not Config2.SOME_OTHER_BOOL assert Config2.TRUE2 conf_file.write_text("true2=true", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert rest configurator2.set_conf(rest) assert not Config2.SOME_OTHER_BOOL assert Config2.TRUE2 conf_file.write_text("true2=false", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert rest configurator2.set_conf(rest) assert not Config2.SOME_OTHER_BOOL assert not Config2.TRUE2 def test_true_by_default_file(tmpdir, tmp_path): conf_file = tmpdir / "conf.conf" class Config: TRUE = True TRUE__DOC = "true by default" configurator = ConfigModule(Config) configurator.parser.add_argument("-c", is_config_file=True) conf_file.write_text("", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert not rest assert Config.TRUE # TODO: fix these cases! # conf_file.write_text("true=true", "utf-8") # args, rest = configurator.set_conf(["-c", str(conf_file)]) # assert not rest # assert Config.TRUE # conf_file.write_text("true=false", "utf-8") # args, rest = configurator.set_conf(["-c", str(conf_file)]) # assert not rest # assert not Config.TRUE def test_false_by_default_file(tmpdir, tmp_path): conf_file = tmpdir / "conf.conf" class Config: FALSE = False FALSE__DOC = "true by default" configurator = ConfigModule(Config) configurator.parser.add_argument("-c", is_config_file=True) conf_file.write_text("", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert not rest assert not Config.FALSE conf_file.write_text("false=true", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert not rest assert Config.FALSE conf_file.write_text("false=false", "utf-8") args, rest = configurator.set_conf(["-c", str(conf_file)]) assert not rest assert not Config.FALSE def test_slidge_conf(): args, rest = main.get_parser().parse_known_args( [ "-c", str(Path(__file__).parent.parent / "dev" / "confs" / "slidge-example.ini"), "--legacy-module=slidge.plugins.dummy", "--jid=test.localhost", "--some-other", ] ) assert args.server == "localhost" assert args.admins == ["test@localhost"] assert args.secret == "secret" assert args.loglevel == logging.DEBUG assert len(rest) == 1 assert rest[0] == "--some-other" def test_set_conf(monkeypatch): monkeypatch.setenv("SLIDGE_USER_JID_VALIDATOR", "cloup") argv = [ "-c", str(Path(__file__).parent.parent / "dev" / "confs" / "slidge-example.ini"), "--legacy-module=slidge.plugins.dummy", "--jid=test.localhost", "--ignore-delay-threshold=200", ] main.get_configurator().set_conf(argv) assert config.SERVER == "localhost" assert config.ADMINS == ["test@localhost"] assert isinstance(config.ADMINS[0], JID) assert isinstance(config.JID, JID) assert config.SECRET == "secret" assert config.IGNORE_DELAY_THRESHOLD.seconds == 200 assert config.USER_JID_VALIDATOR == "cloup" def test_rest(tmp_path): class Config1: PROUT = "caca" PROUT__DOC = "?" class Config2: PROUT2: Optional[str] = None PROUT2__DOC = "?" configurator = ConfigModule(Config1) configurator.parser.add_argument("-c", is_config_file=True) conf_path = tmp_path / "test.conf" conf_path.write_text("prout2=something") args, rest = configurator.set_conf(["-c", str(conf_path)]) configurator2 = ConfigModule(Config2) configurator2.set_conf(rest) assert Config1.PROUT == "caca" assert Config2.PROUT2 == "something" slidge/tests/test_db/000077500000000000000000000000001477703150600151255ustar00rootroot00000000000000slidge/tests/test_db/test_store.py000066400000000000000000000060031477703150600176710ustar00rootroot00000000000000import pytest import sqlalchemy as sa from slixmpp import JID import slidge.db.store from slidge.db.meta import Base from slidge.db.models import Avatar, Contact, Participant, Room from slidge.db.store import SlidgeStore @pytest.fixture def slidge_store(tmp_path): engine = sa.create_engine("sqlite+pysqlite:///:memory:", echo=True) Base.metadata.create_all(engine) import slidge.core.config if not hasattr(slidge.core.config, "HOME_DIR"): slidge.core.config.HOME_DIR = tmp_path yield SlidgeStore(engine) def test_user(slidge_store): assert slidge.db.store._session is None with slidge_store.session() as s1: assert slidge.db.store._session is s1 with slidge_store.session() as s2: assert slidge.db.store._session is s2 assert s1 is s2 with slidge_store.session() as s3: assert slidge.db.store._session is s3 assert s1 is s2 is s3 assert slidge.db.store._session is s1 assert slidge.db.store._session is None def test_delete_avatar(slidge_store): user = slidge_store.users.new(JID("x@x.com"), {}) with slidge_store.session() as orm: avatar = Avatar( filename="", hash="hash", height=0, width=0, ) contact = Contact( jid=JID("xxx@xxx.com"), legacy_id="prout", user_account_id=user.id ) orm.add(contact) orm.commit() contact_pk = contact.id contact = slidge_store.contacts.get_by_pk(contact_pk) contact.avatar = avatar orm.add(contact) orm.commit() avatar_pk = avatar.id with slidge_store.session() as orm: contact = slidge_store.contacts.get_by_pk(contact_pk) assert contact.avatar is not None slidge_store.avatars.delete_by_pk(avatar_pk) contact = slidge_store.contacts.get_by_pk(contact_pk) assert contact.avatar is None def test_unregister(slidge_store): user = slidge_store.users.new(JID("test@test"), {}) with slidge_store.session() as orm: contact = Contact( jid=JID("xxx@xxx.com"), legacy_id="prout", user_account_id=user.id ) orm.add(contact) orm.commit() contact_pk = contact.id slidge_store.contacts.add_to_sent(contact_pk, "an-id") slidge_store.users.delete(user.jid) def test_unregister_with_participants(slidge_store): user = slidge_store.users.new(JID("test@test"), {}) with slidge_store.session() as orm: contact = Contact( jid=JID("xxx@xxx.com"), legacy_id="prout", user_account_id=user.id ) orm.add(contact) orm.commit() room = Room( user_account_id=user.id, legacy_id="legacy-room", jid=JID("legacy-room@something"), ) orm.add(room) orm.commit() participant = Participant(room_id=room.id, contact_id=contact.id) orm.add(participant) orm.commit() slidge_store.users.delete(user.jid) slidge/tests/test_db/test_user.py000066400000000000000000000015241477703150600175160ustar00rootroot00000000000000import pytest from slixmpp import JID from sqlalchemy import create_engine from slidge.db.meta import Base from slidge.db.store import UserStore @pytest.fixture def store(): engine = create_engine("sqlite+pysqlite:///:memory:", echo=True) Base.metadata.create_all(engine) yield UserStore(engine) def test_user(store: UserStore): user1 = store.new(JID("test-user@test-host"), {}) user1.preferences = {"section": {"do_xxx": True}} assert user1.jid == JID("test-user@test-host") store.update(user1) del user1 user2 = store.get(JID("test-user@test-host")) assert user2.preferences == {"section": {"do_xxx": True}} user2.preferences["section"]["do_xxx"] = False store.update(user2) del user2 user3 = store.get(JID("test-user@test-host")) assert not user3.preferences["section"]["do_xxx"] slidge/tests/test_feature_restriction.py000066400000000000000000000033711477703150600211760ustar00rootroot00000000000000import pytest from slidge.core.mixins.recipient import ReactionRecipientMixin @pytest.mark.asyncio async def test_no_restriction(): x = ReactionRecipientMixin() assert await x.restricted_emoji_extended_feature() is None @pytest.mark.asyncio async def test_single_reaction_any_emoji(): class X(ReactionRecipientMixin): REACTIONS_SINGLE_EMOJI = True x = X() form = await x.restricted_emoji_extended_feature() values = form.get_values() assert values["max_reactions_per_user"] == "1" assert values.get("allowlist") is None @pytest.mark.asyncio async def test_single_emoji(): class X(ReactionRecipientMixin): async def available_emojis(self, legacy_msg_id=None): return "♥" x = X() form = await x.restricted_emoji_extended_feature() values = form.get_values() assert values.get("max_reactions_per_user") is None assert values.get("allowlist") == ["♥"] @pytest.mark.asyncio async def test_two_emojis(): class X(ReactionRecipientMixin): async def available_emojis(self, legacy_msg_id=None): return "♥", "😛" x = X() form = await x.restricted_emoji_extended_feature() values = form.get_values() assert values.get("max_reactions_per_user") is None assert values.get("allowlist") == ["♥", "😛"] @pytest.mark.asyncio async def test_two_emojis_single_reaction(): class X(ReactionRecipientMixin): REACTIONS_SINGLE_EMOJI = True async def available_emojis(self, legacy_msg_id=None): return "♥", "😛" x = X() form = await x.restricted_emoji_extended_feature() values = form.get_values() assert values.get("max_reactions_per_user") == "1" assert values.get("allowlist") == ["♥", "😛"] slidge/tests/test_mam_archivable.py000066400000000000000000000026661477703150600200560ustar00rootroot00000000000000from slixmpp import register_stanza_plugin, Message from slixmpp.plugins.xep_0333.stanza import Displayed from slixmpp.plugins.xep_0334 import Store, NoStore, NoPermanentStore from slixmpp.plugins.xep_0424.stanza import Retract from slidge.group.archive import archivable from slixmpp.test import SlixTest class TestArchivable(SlixTest): def setUp(self): register_stanza_plugin(Message, Displayed) register_stanza_plugin(Message, Retract) register_stanza_plugin(Message, Store) register_stanza_plugin(Message, NoStore) register_stanza_plugin(Message, NoPermanentStore) def test_marker(self): msg = Message() assert not archivable(msg) msg.enable("displayed") assert archivable(msg) def test_retract(self): msg = Message() assert not archivable(msg) msg.enable("retract") assert archivable(msg) def test_hint_store(self): msg = Message() assert not archivable(msg) msg.enable("store") assert archivable(msg) def test_hint_no_store(self): msg = Message() msg["body"] = "boobobo" assert archivable(msg) msg.enable("no-store") assert not archivable(msg) def test_hint_no_permanent_store(self): msg = Message() msg["body"] = "boobobo" assert archivable(msg) msg.enable("no-permanent-store") assert not archivable(msg) slidge/tests/test_mds.py000066400000000000000000000164511477703150600157040ustar00rootroot00000000000000import unittest.mock from slixmpp import JID from slixmpp.plugins.xep_0356.permissions import IqPermission from test_muc import Base as BaseMUC # from test_shakespeare import Base as BaseNoMUC class MDSMixin: def setUp(self): super().setUp() for domain in "test.com", "montague.lit": self.xmpp["xep_0356"].granted_privileges[domain].iq[ "http://jabber.org/protocol/pubsub" ] = IqPermission.BOTH self.xmpp["xep_0356"].granted_privileges[domain].iq[ "http://jabber.org/protocol/pubsub#owner" ] = IqPermission.BOTH self.patch_uuid = unittest.mock.patch("uuid.uuid4", return_value="uuid") self.patch_uuid.start() def tearDown(self): super().tearDown() self.patch_uuid.stop() class TestMDS(MDSMixin, BaseMUC): def test_add_to_whitelist(self): task = self.xmpp.loop.create_task( self.xmpp._BaseGateway__add_component_to_mds_whitelist(JID("test@test.com")) ) self.send( # language=XML """ """, use_values=False, ) self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) self.recv( # language=XML """ """ ) assert task.done() def test_receive_event(self): session = self.get_romeo_session() # juliet = self.juliet muc = self.get_private_muc() with unittest.mock.patch("test_muc.Session.on_displayed") as on_displayed: self.recv( # language=XML f""" """ ) on_displayed.assert_awaited_once() assert on_displayed.call_args[0][0].jid == muc.jid assert on_displayed.call_args[0][1] == "legacy-1337" def test_send_mds(self): muc = self.get_private_muc() participant = self.run_coro(muc.get_user_participant()) participant.displayed("legacy-msg-id") self.send( # language=XML """ http://jabber.org/protocol/pubsub#publish-options true max never whitelist """, use_values=False, ) self.recv( # language=XML """ """ ) slidge/tests/test_muc.py000066400000000000000000005156421477703150600157130ustar00rootroot00000000000000import datetime import unittest.mock import uuid from base64 import b64encode from pathlib import Path from typing import Any, Dict, Hashable, Optional import pytest import slixmpp from conftest import AvatarFixtureMixin from slixmpp import JID, Message, Presence from slixmpp.exceptions import XMPPError from slixmpp.plugins import xep_0082 from sqlalchemy import delete from test_shakespeare import ClearSessionMixin import slidge.core.mixins.message_maker import slidge.group.room from slidge import * from slidge import LegacyBookmarks, LegacyContact, LegacyParticipant, LegacyRoster from slidge.db.models import ArchivedMessage from slidge.util.test import SlidgeTest from slidge.util.types import ( Hat, HoleBound, LegacyContactType, LegacyMessageType, Mention, MessageReference, MucType, ) class Gateway(BaseGateway): COMPONENT_NAME = "SLIDGE TEST" GROUPS = True class Session(BaseSession): SENT_TEXT = [] REACTED = [] def __init__(self, user): super().__init__(user) self.bookmarks.user_nick = "thirdwitch" async def wait_for_ready(self, timeout=10): return @staticmethod def xmpp_to_legacy_msg_id(i: str) -> str: return "legacy-" + i @staticmethod def legacy_to_xmpp_msg_id(i: str) -> str: return i[7:] async def on_paused(self, c: LegacyContactType, thread=None): pass async def on_correct( self, c: LegacyContactType, text: str, legacy_msg_id: Any, thread=None, link_previews=(), ): pass async def on_search(self, form_values: Dict[str, str]): pass async def login(self): pass async def logout(self): pass async def on_text( self, chat: LegacyContact, text: str, *, reply_to_msg_id=None, reply_to_fallback_text: Optional[str] = None, reply_to=None, thread=None, link_previews=(), mentions=None, ): self.SENT_TEXT.append(locals()) return "legacy-id" async def on_react( self, c: LegacyContact, legacy_msg_id: LegacyMessageType, emojis: list[str], thread=None, ): self.REACTED.append(locals()) jids = { 123: "juliet", 111: "firstwitch", 222: "secondwitch", 333: "not-in-roster", 666: "imposter", 667: "imposter2", 777: "offline-guy", 999: "weirdguy🎉", } legacy = {v: k for k, v in jids.items()} class Roster(LegacyRoster): def __init__(self, *a, **k): super().__init__(*a, **k) self.ready.set_result(True) async def jid_username_to_legacy_id(self, jid_username: str) -> int: try: return legacy[jid_username] except KeyError: raise XMPPError(text="Only juliet", condition="item-not-found") async def legacy_id_to_jid_username(self, legacy_id: int) -> str: if legacy_id == 999: return "weirdguy" try: return jids[legacy_id] except KeyError: raise XMPPError(text="Only juliet", condition="item-not-found") class Contact(LegacyContact): async def update_info(self): if self.legacy_id == 777: self.offline() if self.legacy_id in (666, 667): self.name = "firstwitch" return if self.legacy_id == 999: self.name = "weirdguy🎉" else: self.name = self.jid.user class Participant(LegacyParticipant): pass class MUC(slidge.LegacyMUC): def __init__(self, *a, **k): super().__init__(*a, **k) self.history = [] async def available_emojis(self, legacy_msg_id=None): if self.jid.user != "room-private-emoji-restricted": return return {"💘", "❤️", "💜"} async def backfill( self, after: Optional[HoleBound] = None, before: Optional[HoleBound] = None, ): for hour in range(10): sender = await self.get_participant(f"history-man-{hour}") sender.send_text( body=f"Body #{hour}", legacy_msg_id=f"legacy-{hour}", when=datetime.datetime( 2000, 1, 1, hour, 0, 0, tzinfo=datetime.timezone.utc ), archive_only=True, ) async def fill_participants(self): if "private" in str(self.legacy_id): first = await self.get_participant_by_contact( await self.session.contacts.by_legacy_id(111) ) # first.nickname = "firstwitch" second = await self.get_participant_by_contact( await self.session.contacts.by_legacy_id(222) ) # second.nickname = "secondwitch" else: first = await self.get_participant("firstwitch") second = await self.get_participant("secondwitch") first._affiliation = "owner" self.xmpp.store.participants.set_affiliation(first.pk, "owner") first._role = "moderator" self.xmpp.store.participants.set_role(first.pk, "moderator") second._affiliation = "admin" self.xmpp.store.participants.set_affiliation(second.pk, "admin") second._role = "moderator" self.xmpp.store.participants.set_role(second.pk, "moderator") yield first yield second if "weird" in str(self.legacy_id): weird = await self.get_participant_by_legacy_id(999) weird.affiliation = "owner" weird.role = "moderator" yield weird # second = await self.get_participant("secondwitch") yield await self.get_user_participant() async def update_info(self): if self.jid.user == "room-private": self.name = "Private Room" self.subject = "Private Subject" self.type = MucType.GROUP return if self.jid.user == "room-private-emoji-restricted": self.name = "Private Room" self.subject = "Private Subject" self.type = MucType.GROUP self.REACTIONS_SINGLE_EMOJI = True return if self.jid.user == "room-public": self.name = "Public Room" self.subject = "Public Subject\n\nOn several\nlines" self.type = MucType.CHANNEL return if self.jid.user == "coven": await self.set_avatar( Path(__file__).parent.parent / "dev" / "assets" / "5x5.png", blocking=False, ) self.name = "The coven" class Bookmarks(LegacyBookmarks): @staticmethod async def jid_local_part_to_legacy_id(local_part: str): if not local_part.startswith("room") and local_part not in ("coven", "weird"): raise XMPPError("item-not-found") else: return local_part async def by_jid(self, jid: JID): muc = await super().by_jid(jid) if not (x in jid.user for x in ["private", "public", "coven", "weird"]): raise XMPPError("item-not-found") return muc async def fill(self): await self.by_legacy_id("room-private-emoji-restricted") await self.by_legacy_id("room-private") await self.by_legacy_id("room-public") await self.by_legacy_id("coven") class Base(ClearSessionMixin, SlidgeTest): plugin = globals() @classmethod def setUpClass(cls): cls.patches = [ unittest.mock.patch("uuid.uuid4", return_value="uuid"), unittest.mock.patch("slidge.group.room.uuid4", return_value="uuid"), unittest.mock.patch( "slidge.core.mixins.message_maker.uuid4", return_value="uuid" ), ] for p in cls.patches: p.start() super().setUpClass() def setUp(self): super().setUp() self.xmpp.store.users.new( JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""} ) self.get_romeo_session().logged = True @classmethod def tearDownClass(cls): super().tearDownClass() for p in cls.patches: p.stop() @staticmethod def get_romeo_session() -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) def get_private_muc(self, name="room-private", resources=()) -> MUC: muc = self.run_coro( self.get_romeo_session().bookmarks.by_jid( JID(f"{name}@aim.shakespeare.lit") ) ) muc.session.cancel_all_tasks() for resource in resources: muc.add_user_resource(resource) n = self.next_sent() if n: assert n["subject"] return muc def get_participant( self, nickname="firstwitch", room="room=private", resources=("gajim",), presence_sent=True, **kwargs, ) -> LegacyParticipant: muc = self.get_private_muc(resources=resources) participant: LegacyParticipant = self.run_coro( muc.get_participant(nickname, **kwargs) ) if presence_sent: self.xmpp.store.participants.set_presence_sent(participant.pk) participant._presence_sent = presence_sent return participant @pytest.mark.usefixtures("avatar") class TestMuc(Base): def test_disco_non_existing_room(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, use_values=False, ) def test_disco_group(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" http://jabber.org/protocol/muc#roominfo 100 0 Private Subject 1 0 1 anyone 0 0 """, ) def test_disco_room_avatar(self): self.next_sent() self.recv( # language=XML f""" """ ) self.send( # language=XML """ http://jabber.org/protocol/muc#roominfo 1 0 100 0 e6f9170123620949a6821e25ea2861d22b0dff66 e6f9170123620949a6821e25ea2861d22b0dff66 0 moderators 1 0 """ ) def test_disco_group_emoji_restricted(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" http://jabber.org/protocol/muc#roominfo 100 0 Private Subject 1 0 1 anyone 0 0 urn:xmpp:reactions:0:restrictions 1 💘 ❤️ 💜 """, ) def test_disco_items(self): session = self.get_romeo_session() self.run_coro(session.bookmarks.fill()) self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) def test_disco_channel(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" http://jabber.org/protocol/muc#roominfo 100 0 1 Public Subject\n\nOn several\nlines 0 0 moderators 1 0 """, ) def test_disco_participant(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) def test_join_muc_no_nick(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, # the error element does not appear for some reason ) def test_join_group(self): muc = self.get_private_muc("room-private") now = datetime.datetime.now(tz=datetime.timezone.utc) participant = self.run_coro(muc.get_participant("stan")) participant.send_text("Hey", when=now) muc.subject_date = now self.xmpp.store.rooms.update(muc) self.recv( # language=XML """ """ ) self.send( # language=XML """ """, ) self.send( # language=XML """ """, ) self.send( # language=XML """ 0 """ ) self.send( # language=XML """ """, ) now_fmt = now.isoformat().replace("+00:00", "Z") self.send( # language=XML f""" Hey """, ) self.send( # language=XML f""" Private Subject """ ) # empty avatar assert self.next_sent()["from"] == "room-private@aim.shakespeare.lit" def test_join_channel(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """, ) self.send( # language=XML """ """, ) self.send( # language=XML """ """, ) def test_self_ping_disconnected(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) def test_resource_not_joined(self): session = self.get_romeo_session() self.recv( # language=XML """ am I here? """ ) self.send( # language=XML """ You are not connected to this chat """ ) self.send( # language=XML """ 0 """ ) assert self.next_sent() is None def test_self_ping_connected(self): self.get_private_muc(resources={"gajim"}) self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) # def test_origin_id(self): # """ # this test is broken because of slixtest magic, but the behavior is actually good # in real conditions # """ # muc = self.get_private_muc() # muc.user_resources.add("gajim") # self.recv( # """ # # body # # # """ # ) # self.send( # """ # # body # # # # """, # ) def test_msg_from_xmpp(self): muc = self.get_private_muc(resources={"gajim", "movim"}) # muc.user_resources = ["gajim", "movim"] self.recv( # language=XML f""" BODY """ ) resources = muc.get_user_resources() tpl = f""" BODY """ # language=XML resources2 = set() for _ in range(2): msg = self.next_sent() resource = msg.get_to().resource self.check(msg, tpl.format(resource=resource), use_values=False) resources2.add(resource) assert resources == resources2 assert self.next_sent() is None sent = Session.SENT_TEXT.pop() assert sent["text"] == "BODY", sent assert sent["chat"].is_group, sent assert sent["reply_to_msg_id"] is None, sent assert sent["reply_to_fallback_text"] is None, sent assert sent["reply_to"] is None, sent def test_msg_reply_from_xmpp(self): Session.SENT_TEXT = [] muc = self.get_private_muc() muc.add_user_resource("gajim") fallback = "> Anna wrote:\n> We should bake a cake\n" stripped_body = "Great idea!" self.recv( # language=XML f""" {fallback}{stripped_body} """ ) self.next_sent() sent = Session.SENT_TEXT.pop() assert sent["reply_to"].nickname == "Anna" assert sent["reply_to_msg_id"] == "legacy-message-id1" assert sent["reply_to_fallback_text"] == fallback assert sent["text"] == stripped_body def test_msg_from_legacy(self): participant = self.get_participant() muc = participant.muc participant.send_text("the body", legacy_msg_id="legacy-XXX") self.send( # language=XML f""" the body """, use_values=False, ) def test_correct_from_legacy(self): participant = self.get_participant() participant.send_text("body", "legacy-1") msg = self.next_sent() assert msg["body"] == "body" assert msg["id"] == msg["stanza_id"]["id"] == "1" participant.correct("legacy-1", "new") msg = self.next_sent() assert msg["body"] == "new" assert msg["replace"]["id"] == "1" assert msg["id"] == "" assert msg["stanza_id"]["id"] != "" participant.correct( "legacy-1", "newnew", correction_event_id="legacy-correction" ) msg = self.next_sent() assert msg["body"] == "newnew" assert msg["id"] == "correction" assert msg["stanza_id"]["id"] == "correction" assert msg["replace"]["id"] == "1" participant.correct("legacy-willbeconverted", "new content") msg = self.next_sent() assert msg["replace"]["id"] == "willbeconverted" assert msg["body"] == "new content" assert msg["id"] == "" assert msg["stanza_id"]["id"] != "" participant.correct( "legacy-willbeconverted", "new content", correction_event_id="legacy-correction_id", ) msg = self.next_sent() assert msg["replace"]["id"] == "willbeconverted" assert msg["body"] == "new content" assert msg["id"] == "correction_id" assert msg["stanza_id"]["id"] == "correction_id" def test_msg_reply_self_from_legacy(self): Session.SENT_TEXT = [] participant = self.get_participant() muc = participant.muc participant.send_text( "the body", legacy_msg_id="legacy-XXX", reply_to=MessageReference(legacy_id="legacy-REPLY-TO", author=participant), ) self.send( # language=XML f""" the body """, use_values=False, ) def test_msg_reply_to_user(self): Session.SENT_TEXT = [] participant = self.get_participant() muc = participant.muc participant.send_text( "the body", legacy_msg_id="legacy-XXX", reply_to=MessageReference(legacy_id="legacy-REPLY-TO", author="user"), ) self.send( # language=XML f""" the body """, use_values=False, ) def test_msg_reply_from_legacy(self): Session.SENT_TEXT = [] participant = self.get_participant() muc = participant.muc second_witch = self.get_participant("secondwitch") participant.send_text( "the body", legacy_msg_id="legacy-XXX", reply_to=MessageReference( author=second_witch, legacy_id="legacy-REPLY-TO", ), ) self.send( # language=XML f""" the body """, use_values=False, ) def test_msg_reply_from_legacy_fallback(self): Session.SENT_TEXT = [] participant = self.get_participant() muc = participant.muc second_witch = self.get_participant("secondwitch") participant.send_text( "the body", legacy_msg_id="legacy-XXX", reply_to=MessageReference( legacy_id="legacy-REPLY-TO", author=second_witch, body="Blabla" ), ) self.send( # language=XML f""" > secondwitch:\n> Blabla\nthe body """, use_values=False, ) def test_react_from_xmpp(self): muc = self.get_private_muc(resources=["movim"]) self.recv( # language=XML f""" 👋 """ ) for r in muc.get_user_resources(): self.send( # language=XML f""" 👋 """, use_values=False, ) assert self.next_sent() is None sent = Session.REACTED.pop() assert sent["c"].is_group assert tuple(sent["emojis"]) == ("👋",) assert sent["legacy_msg_id"] == "legacy-SOME-ID" def test_react_from_legacy(self): participant = self.get_participant() muc = participant.muc participant.react(legacy_msg_id="legacy-XXX", emojis="👋") self.send( # language=XML f""" 👋 """, use_values=False, ) def test_mam_bare_jid(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ No MAM on the component itself, use a JID with a resource """ ) self.recv( # language=XML """ """ ) self.send( # language=XML """ This is only handled for MUCs """ ) def test_mam_form_fields(self): muc = self.get_private_muc() # muc.user_resources.add("gajim") self.recv( # language=XML """ """ ) self.send( # language=XML """ urn:xmpp:mam:2 """ ) def test_mam_all(self): self.recv( # language=XML """ """ ) for i in range(10): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 0 9 10 """ ) def test_mam_page_limit(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 2 """ ) for i in range(3, 5): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 3 4 2 """ ) def test_mam_page_after(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 2 5 """ ) for i in range(6, 8): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 6 7 2 """ ) def test_mam_page_after_last(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 70 9 """ ) self.send( # language=XML """ 0 """, use_values=False, ) def test_mam_page_after_not_found(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 2 12 """ ) self.send( # language=XML """ Message 12 not found """, use_values=False, ) def test_last_page(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 3 """ ) for i in range(7, 10): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 7 9 3 """ ) self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 3 7 """ ) for i in range(4, 7): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 4 6 3 """ ) def test_mam_flip(self): self.recv( # language=XML """ urn:xmpp:mam:2 2000-01-01T03:00:00Z 3 5 """ ) for i in range(9, 6, -1): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 9 7 3 """ ) def test_mam_flip_no_max(self): self.recv( # language=XML """ """ ) for i in range(9, -1, -1): self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML """ 9 0 10 """ ) def test_mam_metadata(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) def test_mam_metadata_empty(self): muc = self.get_private_muc() self.xmpp.store.rooms.set_history_filled(muc.pk, True) self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) def test_mam_with(self): for i in range(10): self.recv( # language=XML f""" urn:xmpp:mam:2 room-private@aim.shakespeare.lit/history-man-{i} """ ) self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML f""" {i} {i} 1 """ ) def test_mam_specific_id(self): self.recv( # language=XML f""" urn:xmpp:mam:2 2 4 """ ) for i in 2, 4: self.send( # language=XML f""" Body #{i} """ ) self.send( # language=XML f""" 2 4 2 """ ) self.recv( # language=XML f""" urn:xmpp:mam:2 2 14 """ ) self.send( # language=XML """ One of the requested messages IDs could not be found with the given constraints. """ ) def test_mam_from_user_carbon(self): muc = self.get_private_muc(resources=["gajim"]) now = datetime.datetime.now(tz=datetime.timezone.utc) user_participant: Participant = self.run_coro(muc.get_user_participant()) subject_stanza = self.next_sent() if subject_stanza is not None: # inconsistent behaviour whether using the debugger or not assert subject_stanza["subject"] user_participant.send_text("blabla", "legacy-666", when=now) now_fmt = now.isoformat().replace("+00:00", "Z") self.send( # language=XML f""" blabla """, use_values=False, # necessary because the third has origin-id ) self.recv( # language=XML """ urn:xmpp:mam:2 1 """ ) self.send( # language=XML f""" blabla """ ) def test_mam_echo(self): muc = self.get_private_muc(resources=["gajim"]) self.recv( # language=XML """ HOY """ ) self.send( # language=XML """ HOY """ ) archived = list(muc.archive.get_all())[-1] assert archived.id == "id" def test_get_members(self): muc = self.get_private_muc() # muc.user_resources.add("gajim") self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) def test_room_avatar(self): v = b64encode(self.avatar_path.read_bytes()).decode() self.run_coro(self.get_romeo_session().bookmarks.fill()) self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) self.recv( # language=XML """ """ ) self.send( # language=XML f""" image/png {v} """ ) def test_join_room_avatar(self): muc = self.get_private_muc("coven") self.recv( # language=XML """ """ ) for _ in self.xmpp.store.participants.get_all(muc.pk): pres = self.next_sent() assert isinstance(pres, Presence) subject = self.next_sent() assert isinstance(subject, Message) self.send( # language=XML f""" {self.avatar_original_sha1} """, ) def test_send_to_bad_resource(self): muc = self.get_private_muc("coven") muc.add_user_resource("gajim") self.recv( # language=XML """ """ ) muc = self.run_coro( self.get_romeo_session().bookmarks.by_jid(JID("coven@aim.shakespeare.lit")) ) assert not muc.get_user_resources() self.recv( # language=XML """ """ ) muc = self.run_coro( self.get_romeo_session().bookmarks.by_jid(JID("coven@aim.shakespeare.lit")) ) assert not muc.get_user_resources() def test_recv_non_kickable_error(self): muc = self.get_private_muc("coven") muc.get_user_resources().add("gajim") self.recv( # language=XML """ """ ) assert muc.get_user_resources() == {"gajim"} muc.remove_user_resource("gajim") assert self.next_sent() is None def test_recv_error_non_existing_muc(self): self.recv( # language=XML """ """ ) assert self.next_sent() is None def test_moderate_by_room(self): muc = self.get_private_muc("room", ["gajim"]) p = muc.get_system_participant() p.moderate("legacy-666", "reason™") self.send( # language=XML """ reason™ """ ) def test_moderate_by_moderator(self): # muc = self.get_private_muc(resources=["gajim"]) p = self.get_participant(resources=["gajim"]) p.moderate("legacy-666", "reason™") self.send( # language=XML f""" reason™ """ ) def test_participant_avatar(self): self.test_join_group() v = b64encode(self.avatar_path.read_bytes()).decode() session = self.get_romeo_session() self.run_coro(session.bookmarks.fill()) muc = self.get_private_muc() muc._participants_filled = True self.xmpp.store.rooms.update(muc) contact = self.run_coro(session.contacts.by_legacy_id(333)) contact.avatar = self.avatar_path self.run_coro(contact._set_avatar_task) self.run_coro(muc.get_participant_by_contact(contact)) pres = self.next_sent() assert pres["vcard_temp_update"]["photo"] == self.avatar_original_sha1 self.recv( # language=XML f""" """ ) self.send( # language=XML f""" {v} image/png """, use_values=False, ) def test_presence_propagation(self): participants_before = self.__get_participants() contact = participants_before[0].contact last_seen = datetime.datetime.now(tz=datetime.timezone.utc) contact.is_friend = True contact.away(last_seen=last_seen, status="blabla") dt = xep_0082.format_datetime(last_seen) self.send( # language=XML f""" away blabla -- Last seen {last_seen:%A %H:%M GMT} """ ) self.send( # language=XML f""" away blabla -- Last seen {last_seen:%A %H:%M GMT} """ ) assert self.next_sent() is None def test_add_to_bookmarks(self): muc = self.get_private_muc() self.xmpp["xep_0356"].granted_privileges["montague.lit"].iq[ "http://jabber.org/protocol/pubsub" ] = "both" self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=True, preserve=False)) import slixmpp.plugins.xep_0356.privilege o = slixmpp.plugins.xep_0356.privilege.uuid.uuid4 slixmpp.plugins.xep_0356.privilege.uuid.uuid4 = lambda: "0" self.send( # language=XML """ thirdwitch http://jabber.org/protocol/pubsub#publish-options 1 max never whitelist """, use_values=False, ) self.recv( # language=XML """ """ ) slixmpp.plugins.xep_0356.privilege.uuid.uuid4 = o def test_bookmark_extension_preservation(self): muc = self.get_private_muc() self.xmpp["xep_0356"].granted_privileges["montague.lit"].iq[ "http://jabber.org/protocol/pubsub" ] = "both" iq_counter = 0 def increment(): nonlocal iq_counter iq_counter += 1 return iq_counter - 1 patch = unittest.mock.patch("slixmpp.plugins.xep_0356.privilege.uuid.uuid4", increment) patch.start() base_stanza = ( # language=XML """ thirdwitch{extension} http://jabber.org/protocol/pubsub#publish-options 1 max never whitelist """ ) pinned_stanza = base_stanza.format(extension='') not_pinned_stanza = base_stanza.format(extension="") not_pinned_stanza_empty_extension = base_stanza.format(extension="") invite_stanza = ( # language=XML """ """ ) iq_confirmation_stanza = ( # language=XML """ """ ) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=False, pin=None)) self.send(not_pinned_stanza.format(counter=0), use_values=False) self.recv(iq_confirmation_stanza.format(counter=0)) self.send(invite_stanza, use_values=False) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=False, pin=True)) self.send(pinned_stanza.format(counter=1), use_values=False) self.recv(iq_confirmation_stanza.format(counter=1)) self.send(invite_stanza, use_values=False) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=False, pin=False)) self.send(not_pinned_stanza_empty_extension.format(counter=2), use_values=False) self.recv(iq_confirmation_stanza.format(counter=2)) self.send(invite_stanza, use_values=False) request_bookmark_stanza = ( # language=XML """ """ ) bookmark_result_stanza = ( # language=XML """ thirdwitch{extension} """ ) extension_not_pinned = "blabla" extension_pinned = "blabla" extension_not_notify = extension_not_pinned extension_notify_never = "blabla" extension_notify_always = "blabla" extension_notify_always_mobile = "blabla" extension_notify_never_always_mobile = "blabla" c = 3 self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, pin=None)) self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_not_pinned), use_values=False) assert self.next_sent() is None self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, pin=True)) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_not_pinned), use_values=False) c += 1 self.send(base_stanza.format(extension=extension_pinned).format(counter=c), use_values=False) self.recv(iq_confirmation_stanza.format(counter=c)) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, pin=True)) c +=1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_pinned), use_values=False) assert self.next_sent() is None self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, pin=False)) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_pinned), use_values=False) c += 1 self.send(base_stanza.format(extension=extension_not_pinned).format(counter=c), use_values=False) self.recv(iq_confirmation_stanza.format(counter=c)) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, pin=False)) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_not_pinned), use_values=False) assert self.next_sent() is None self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, notify="always")) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_not_notify), use_values=False) c += 1 self.send(base_stanza.format(extension=extension_notify_always).format(counter=c), use_values=False) self.recv(iq_confirmation_stanza.format(counter=c)) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, notify="always")) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_notify_always), use_values=False) assert self.next_sent() is None self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, notify="never")) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_notify_always), use_values=False) c += 1 self.send(base_stanza.format(extension=extension_notify_never).format(counter=c), use_values=False) self.recv(iq_confirmation_stanza.format(counter=c)) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=True, notify="never")) c += 1 self.send(request_bookmark_stanza.format(counter=c), use_values=False) self.recv(bookmark_result_stanza.format(counter=c, extension=extension_notify_always_mobile), use_values=False) c += 1 self.send(base_stanza.format(extension=extension_notify_never_always_mobile).format(counter=c), use_values=False) self.recv(iq_confirmation_stanza.format(counter=c)) patch.stop() def test_bookmark_chat_notification_setting(self): muc = self.get_private_muc() self.xmpp["xep_0356"].granted_privileges["montague.lit"].iq[ "http://jabber.org/protocol/pubsub" ] = "both" iq_counter = 0 def increment(): nonlocal iq_counter iq_counter += 1 return iq_counter - 1 patch = unittest.mock.patch("slixmpp.plugins.xep_0356.privilege.uuid.uuid4", increment) patch.start() base_stanza = ( # language=XML """ thirdwitch{extension} http://jabber.org/protocol/pubsub#publish-options 1 max never whitelist """ ) never_stanza = base_stanza.format(extension="") always_stanza = base_stanza.format(extension="") invite_stanza = ( # language=XML """ """ ) iq_confirmation_stanza = ( # language=XML """ """ ) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=False, notify="never")) self.send(never_stanza.format(counter=0), use_values=False) self.recv(iq_confirmation_stanza.format(counter=0)) self.send(invite_stanza, use_values=False) self.xmpp.loop.create_task(muc.add_to_bookmarks(auto_join=False, preserve=False, notify="always")) self.send(always_stanza.format(counter=1), use_values=False) self.recv(iq_confirmation_stanza.format(counter=1)) self.send(invite_stanza, use_values=False) patch.stop() async def __fill_roster(self): async for _ in self.get_romeo_session().contacts.fill(): continue def __get_participants(self): muc = self.get_private_muc(resources=["movim"]) self.run_coro(self.__fill_roster()) participants_before: list[Participant] = list( self.run_coro(self.__participants_as_list(muc)) ) for p in participants_before: p._presence_sent = True self.xmpp.store.participants.set_presence_sent(p.pk) return participants_before async def __participants_as_list(self, muc: MUC): return [p async for p in muc.get_participants()] def __test_rename_common(self, old_nick, participants_before): muc = self.get_private_muc() p = participants_before[0] self.send( # language=XML f""" """ ) self.send( # language=XML f""" """ ) muc = self.run_coro(muc.session.bookmarks.by_legacy_id(muc.legacy_id)) participants_after = self.run_coro(self.__participants_as_list(muc)) assert len(participants_after) == len(participants_before) nicks = [p.nickname for p in participants_after] assert old_nick not in nicks assert "new-nick" in nicks assert self.next_sent() is None def test_rename_participant_from_participant(self): participants_before = self.__get_participants() p = participants_before[0] old_nick = p.nickname p.nickname = "new-nick" self.__test_rename_common(old_nick, participants_before) def test_rename_participant_from_muc(self): participants_before = self.__get_participants() p = participants_before[0] old_nick = p.nickname p.muc.rename_participant(old_nick, "new-nick") self.__test_rename_common(old_nick, participants_before) def test_rename_from_contact(self): participants_before = self.__get_participants() p = participants_before[0] old_nick = p.nickname p.contact.name = "new-nick" self.__test_rename_common(old_nick, participants_before) def test_rename_from_contact_with_forbidden_char(self): participants_before = self.__get_participants() p = participants_before[0] old_nick = p.nickname p.contact.name = "a forbidden emoji 🎉" self.send( # language=XML f""" a forbidden emoji 🎉 """ ) self.send( # language=XML f""" a forbidden emoji 🎉 """ ) muc = self.get_private_muc() participants_after = self.run_coro(self.__participants_as_list(muc)) assert len(participants_after) == len(participants_before) assert self.next_sent() is None def test_non_anonymous_participants_with_same_nickname(self): muc = self.get_private_muc(resources=["movim"]) participants = self.__get_participants() for p in participants: if p.contact.name == "firstwitch": real_witch = p break else: raise AssertionError assert real_witch.jid == self.run_coro(muc.get_participant("firstwitch")).jid p = self.run_coro(muc.get_participant_by_legacy_id(666)) assert real_witch.jid == self.run_coro(muc.get_participant("firstwitch")).jid p.send_text("Je suis un canaillou") self.send( # language=XML """ """ ) self.send( # language=XML """ Je suis un canaillou """ ) assert self.next_sent() is None assert real_witch.jid == self.run_coro(muc.get_participant("firstwitch")).jid p = self.run_coro(muc.get_participant_by_legacy_id(667)) assert real_witch.jid == self.run_coro(muc.get_participant("firstwitch")).jid p.send_text("Je suis un canaillou") self.send( # language=XML """ """ ) self.send( # language=XML """ Je suis un canaillou """ ) assert self.next_sent() is None def test_illegal_nickname(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) self.send( # language=XML """ """ ) self.send( # language=XML """ weirdguy🎉 """ ) self.send( # language=XML """ """ ) def test_illegal_nickname_quoted_fallback(self): op = self.get_participant("weirdguy🎉") replier = self.get_participant() replier.send_text( "reply", reply_to=MessageReference("some-id", op, "quoted text"), ) self.send( # language=XML """ > weirdguy🎉:\n> quoted text\nreply """ ) def test_group_rename(self): group = self.get_private_muc(resources=("gajim",)) group.name = "prout" self.send( # language=XML """ """, use_values=False, ) assert self.next_sent() is None class TestRoleAffiliation(Base): def setUp(self): super().setUp() muc = self.get_private_muc() muc._participants_filled = True self.xmpp.store.rooms.update(muc) def test_role_change(self): part = self.get_participant("a-new-one", role="visitor") self.send( # language=XML f""" """ ) part.role = "visitor" part.online() self.send( # language=XML f""" """ ) assert self.next_sent() is None part.role = "visitor" assert self.next_sent() is None def test_affiliation_change(self): part = self.get_participant("a-new-one") self.send( # language=XML f""" """ ) part.affiliation = "admin" self.send( # language=XML f""" """ ) assert self.next_sent() is None part.affiliation = "admin" assert self.next_sent() is None def test_affiliation_change_new_part(self): part = self.get_participant("a-newer-one", presence_sent=False) self.send( # language=XML f""" """ ) assert self.next_sent() is None @pytest.mark.usefixtures("avatar") class TestSetAvatar(Base, AvatarFixtureMixin): def test_set_avatar(self): muc = self.get_private_muc(resources=("gajim",)) with unittest.mock.patch( "slidge.group.room.LegacyMUC.on_avatar", return_value=1 ): self.recv( # language=XML f""" image/png {b64encode(self.avatar_bytes).decode("utf-8")} """ ) self.send( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) self.send( # language=XML f""" {self.avatar_sha1} """ ) assert self.next_sent() is None @pytest.mark.usefixtures("avatar") class TestUserAvatar(Base, AvatarFixtureMixin): def setUp(self): super().setUp() session = self.get_romeo_session() with self.xmpp.store.session() as orm: user = session.user user.avatar_hash = self.avatar_sha1 orm.add(user) orm.commit() muc = self.get_private_muc(name="room-user-avatar-test", resources=("gajim",)) self.user_participant = self.run_coro(muc.get_user_participant()) def test_user_avatar(self): self.user_participant.send_initial_presence("romeo@montague.lit/gajim") self.send( # language=XML f""" {self.avatar_sha1} """ ) def test_fetch_user_avatar(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) self.recv( # language=XML f""" {self.avatar_base64} """ ) self.send( # language=XML f""" {self.avatar_base64} image/test """, use_values=False, ) assert self.next_sent() is None class TestMUCAdmin(Base): def setUp(self): super().setUp() self.muc = muc = self.get_private_muc( name="room-moderation-test", resources=("gajim",) ) self.user_participant = self.run_coro(muc.get_user_participant()) self.user_participant._presence_sent = True self.xmpp.store.participants.set_presence_sent(self.user_participant.pk) self.user_jid = self.get_romeo_session().user_jid def test_moderation_not_implemented(self): self.recv( # language=XML f""" This message contains inappropriate content for this forum """ ) self.send( # language=XML """ Not implemented by the legacy module """ ) def test_moderation_success(self): with unittest.mock.patch("slidge.BaseSession.on_moderate") as on_moderate: self.recv( # language=XML f""" REASON """ ) on_moderate.assert_awaited_once() muc, legacy_id, reason = on_moderate.call_args[0] assert muc.jid == self.muc.jid assert legacy_id == "legacy-stanza-id-1" assert reason == "REASON" self.send( # language=XML """ """ ) def test_set_member(self): with unittest.mock.patch( "slidge.LegacyMUC.on_set_affiliation" ) as on_set_affiliation: self.recv( # language=XML f""" A reason """ ) on_set_affiliation.assert_awaited_once() contact, affiliation, reason, nick = on_set_affiliation.call_args[0] assert ( contact.jid == self.run_coro( self.get_romeo_session().contacts.by_legacy_id(222) ).jid ) assert affiliation == "member" assert reason == "A reason" assert nick == "a-nick" self.send( # language=XML """ """ ) def test_get_owner_form(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" Slidge room configuration Complete this form to modify the configuration of your room. http://jabber.org/protocol/muc#roomconfig unnamed-room """ ) def test_set_description(self): with unittest.mock.patch("slidge.LegacyMUC.on_set_config") as on_set_config: self.recv( # language=XML f""" Slidge room configuration Complete this form to modify the configuration of your room. http://jabber.org/protocol/muc#roomconfig A new name A new description """ ) on_set_config.assert_awaited_once_with( name="A new name", description="A new description", ) self.send( # language=XML """ Slidge room configuration Complete this form to modify the configuration of your room. http://jabber.org/protocol/muc#roomconfig A new name A new description """ ) def test_destruct(self): with unittest.mock.patch( "slidge.LegacyMUC.on_destroy_request" ) as on_destroy_request: self.recv( # language=XML f""" Macbeth doth come. """ ) on_destroy_request.assert_awaited_once_with("Macbeth doth come.") self.send( # language=XML f""" Macbeth doth come. """, ) self.send( # language=XML f""" """ ) def test_subject(self): with unittest.mock.patch("slidge.LegacyMUC.on_set_subject") as on_set_subject: self.recv( # language=XML f""" Fire Burn and Cauldron Bubble! """ ) on_set_subject.assert_awaited_once_with("Fire Burn and Cauldron Bubble!") assert self.next_sent() is None def test_kick(self): part = self.run_coro(self.muc.get_participant_by_legacy_id(111)) self.next_sent() assert part.contact with unittest.mock.patch("slidge.LegacyMUC.on_kick") as on_kick: self.recv( # language=XML f""" kick-reason """ ) on_kick.assert_awaited_once() contact, reason = on_kick.call_args[0] assert contact.jid == part.contact.jid assert reason == "kick-reason" self.send( # language=XML """ """ ) class TestJoinAway(Base): def setUp(self): super().setUp() self.muc = muc = self.get_private_muc( name="room-moderation-test", resources=("gajim",) ) self.user_participant = self.run_coro(muc.get_user_participant()) self.user_jid = self.get_romeo_session().user_jid self.juliet = self.run_coro(self.get_romeo_session().contacts.by_legacy_id(123)) def get_juliet_participant(self): return self.run_coro(self.muc.get_participant_by_contact(self.juliet)) def test_online_contact_joins(self): self.juliet.online() assert self.next_sent() is None muc = self.muc self.muc._participants_filled = True self.xmpp.store.rooms.update(muc) self.get_juliet_participant() self.send( # language=XML """ """ ) self.muc.remove_participant(self.get_juliet_participant()) self.send( # language=XML """ """ ) assert self.next_sent() is None def test_away_contact_joins(self): self.juliet.away() assert self.next_sent() is None muc = self.muc self.muc._participants_filled = True self.xmpp.store.rooms.update(muc) self.run_coro(self.muc.get_participant_by_contact(self.juliet)) self.send( # language=XML """ away """ ) assert self.next_sent() is None self.muc.remove_participant(self.get_juliet_participant()) self.send( # language=XML """ """ ) assert self.next_sent() is None class TestMentions(Base): def test_mentions(self): muc = self.get_private_muc("weird", ("gajim",)) session = self.get_romeo_session() with unittest.mock.patch("test_muc.Session.on_text") as on_text: self.recv( # language=XML f""" I am {muc.user_nick} I want weirdguy🎉 to kiss me """ ) on_text.assert_awaited_once() muc2, text = on_text.call_args[0] assert text == f"I am {muc.user_nick} I want weirdguy🎉 to kiss me" assert muc2.jid == muc.jid mentions = on_text.call_args[1]["mentions"] assert len(mentions) == 1 assert mentions[0].start == 23 assert mentions[0].end == 32 assert ( mentions[0].contact.jid == self.run_coro(muc.get_participant("weirdguy🎉")).contact.jid ) class TestHats(Base): def test_hats(self): muc = self.get_private_muc("room-private", ("gajim",)) participant = self.run_coro(muc.get_participant("i-wear-hats")) participant.send_last_presence(force=True, no_cache_online=True) self.send( # language=XML """ """ ) muc._participants_filled = True self.xmpp.store.rooms.update(muc) participant = self.run_coro(muc.get_participant("i-wear-hats")) participant.set_hats([Hat("uri1", "title1"), Hat("uri2", "title2")]) self.send( # language=XML """ """ ) class TestNickChange(Base): def test_user_nick_change(self): muc = self.get_private_muc("room-private", ("gajim",)) self.recv( # language=XML f""" """ ) self.send( # language=XML f""" Slidge does not let you change your nickname in groups. """ ) assert self.next_sent() is None def test_user_gets_away(self): muc = self.get_private_muc("room-private", ("gajim",)) self.recv( # language=XML f""" """ ) assert self.next_sent() is None class TestMUCRegistration(Base): def test_request_registration_form_unknown_muc(self): session = self.get_romeo_session() self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) def test_request_registration_form_known_muc(self): muc = self.get_private_muc("room-private", ("gajim",)) self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) def test_request_remove_known_muc(self): muc = self.get_private_muc("room-private", ("gajim",)) with unittest.mock.patch( "slidge.core.session.BaseSession.on_leave_group" ) as olg: self.recv( # language=XML f""" """ ) olg.assert_awaited_once_with(muc.legacy_id) self.send( # language=XML f""" """ ) self.send( # language=XML """ You left this chat from an XMPP client. 0 """ ) self.recv( # language=XML f""" """, ) self.send( # language=XML f""" Not implemented by the legacy module """ ) slidge/tests/test_name_in_constructor.py000066400000000000000000000036541477703150600211750ustar00rootroot00000000000000from typing import AsyncIterator from conftest import AvatarFixtureMixin from slidge import BaseGateway, BaseSession from slidge.contact import LegacyContact, LegacyRoster from slidge.core.session import _sessions from slidge.group import LegacyBookmarks from slidge.util.test import SlidgeTest class Gateway(BaseGateway): COMPONENT_NAME = "A test" GROUPS = True class Session(BaseSession): async def login(self): return "YUP" class Roster(LegacyRoster): async def fill(self) -> AsyncIterator["Contact"]: yield await self.by_name("some id", "some name") async def by_name(self, legacy_id: str, name: str): return await self.by_legacy_id(legacy_id, name) class Contact(LegacyContact): def __init__(self, session, legacy_id, jid_username, name: str | None = None): super().__init__(session, legacy_id, jid_username) if name is not None: self.name = name def use_contact_info(self, name: str): self.name = name class Bookmarks(LegacyBookmarks): async def fill(self): muc = await self.by_legacy_id("some group id") muc.name = "some group name" class TestSetContactNameInConstructor(AvatarFixtureMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() self.setup_logged_session(1) def tearDown(self): super().tearDown() _sessions.clear() def test_set_name_in_constructor(self): contact = self.run_coro(self.romeo.contacts.by_legacy_id("some id")) assert contact.name == "some name" muc = self.run_coro(self.romeo.bookmarks.by_legacy_id("some group id")) assert muc.name == "some group name" def test_participant(self): muc = self.run_coro(self.romeo.bookmarks.by_legacy_id("some group id")) participant = self.run_coro(muc.get_participant_by_legacy_id("some other id")) assert participant.nickname == "some other id" slidge/tests/test_resourceprep.py000066400000000000000000000016051477703150600176320ustar00rootroot00000000000000from unittest import mock import pytest from slixmpp import JID from slidge.group import LegacyParticipant @pytest.fixture def muc(): muc = mock.MagicMock() muc.jid = JID("room@component") return muc def test_unassigned_code_points(muc): part = LegacyParticipant(muc, "fiesta! 🎉") assert "🎉" not in part.jid.resource def test_control_chars(muc): part = LegacyParticipant(muc, "leet hackk\ber and I have control chars in my nick") assert "\b" not in part.jid.resource def test_control_chars_and_unassigned_code_points(muc): part = LegacyParticipant( muc, "I'm a leet hackk\ber" + "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉" * 10 + ", I have control chars, emojis in my nick and a ridiculously long nickname", ) assert "\b" not in part.jid.resource assert "🎉" not in part.jid.resource slidge/tests/test_session.py000066400000000000000000000577231477703150600166130ustar00rootroot00000000000000import unittest.mock import pytest from conftest import AvatarFixtureMixin from slixmpp import JID from slixmpp import __version__ as slix_version from slixmpp import register_stanza_plugin from slixmpp.plugins.xep_0060.stanza import EventItem from slixmpp.plugins.xep_0084 import MetaData from slidge import BaseGateway, BaseSession from slidge.core.session import _sessions from slidge.util.test import SlidgeTest from slidge.util.types import LinkPreview class Gateway(BaseGateway): COMPONENT_NAME = "A test" class Session(BaseSession): async def login(self): return "YUP" @pytest.mark.usefixtures("avatar") class TestSession(AvatarFixtureMixin, SlidgeTest): plugin = globals() xmpp: Gateway def setUp(self): super().setUp() self.setup_logged_session() self.xmpp["xep_0060"].map_node_event(MetaData.namespace, "avatar_metadata") register_stanza_plugin(EventItem, MetaData) def tearDown(self): super().tearDown() _sessions.clear() @staticmethod def get_romeo_session() -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) def test_gateway_receives_presence_probe(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" YUP chat """ ) assert self.next_sent() is None def test_avatar(self): with unittest.mock.patch("slidge.BaseSession.on_avatar") as on_avatar: self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) self.recv( # language=XML f""" {self.avatar_base64} """ ) on_avatar.assert_awaited_with( self.avatar_bytes, self.avatar_sha1, "image/png", 5, 5 ) self.recv( # language=XML f""" """ ) def test_avatar_unpublish(self): with unittest.mock.patch("slidge.BaseSession.on_avatar") as on_avatar: self.recv( # language=XML f""" """ ) on_avatar.assert_awaited_with(None, None, None, None, None) def test_user_send_invitation_to_standard_muc(self): self.recv( # language=XML f""" """ ) msg = self.next_sent() assert msg["type"] == "error" assert msg["error"]["condition"] == "bad-request" def test_user_send_invitation(self): with unittest.mock.patch("slidge.BaseSession.on_invitation") as on_invitation: self.recv( # language=XML f""" """ ) on_invitation.assert_awaited_once() assert on_invitation.call_args[0][0].jid == self.juliet.jid assert on_invitation.call_args[0][1].jid == self.room.jid assert ( on_invitation.call_args[0][2] == "Hey Hecate, this is the place for all good witches!" ) def test_link_preview(self): with unittest.mock.patch("slidge.BaseSession.on_text") as on_text: self.recv( # language=XML f""" I wanted to mention https://the.link.example.com/what-was-linked-to Page Title Page Description Canonical URL https://link.to.example.com/image.png Some Website """ ) on_text.assert_awaited_once() args, kwargs = on_text.call_args assert args[0].jid == self.juliet.jid assert ( args[1] == "I wanted to mention https://the.link.example.com/what-was-linked-to" ) # kwargs = on_text.c assert kwargs == dict( reply_to_msg_id=None, reply_to_fallback_text=None, reply_to=None, thread=None, link_previews=[ LinkPreview( about="https://the.link.example.com/what-was-linked-to", title="Page Title", description="Page Description", url="Canonical URL", image="https://link.to.example.com/image.png", type=None, site_name="Some Website", ) ], ) def test_juliet_sends_link_preview(self): self.juliet.send_text( "I wanted to mention https://the.link.example.com/what-was-linked-to", link_previews=[ LinkPreview( about="https://the.link.example.com/what-was-linked-to", title="Page Title", description="Page Description", url="Canonical URL", image="https://link.to.example.com/image.png", type=None, site_name="Some Website", ) ], ) self.send( # language=XML """ I wanted to mention https://the.link.example.com/what-was-linked-to Page Title Page Description Canonical URL https://link.to.example.com/image.png Some Website """ ) def test_mark_all_messages(self): self.xmpp.MARK_ALL_MESSAGES = True self.juliet.send_text("whatever", "msg_00") self.juliet.send_text("whatever", "msg_01") self.juliet.send_text("whatever", "msg_02") with unittest.mock.patch( "slidge.core.session.BaseSession.on_displayed" ) as on_displayed: self.recv( # language=XML f""" """ ) assert on_displayed.await_count == 3 for i in range(3): assert on_displayed.call_args_list[i][0][1] == f"msg_0{i}" def test_movim_sticker(self): sticker_stanza = f""" Un autocollant a été envoyé via Movim

Sticker

""" self.recv(sticker_stanza) self.send( # language=XML """ """ ) with unittest.mock.patch( "slidge.core.session.BaseSession.on_sticker" ) as on_sticker: self.recv( # language=XML f""" iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg== """ ) on_sticker.assert_awaited_once() args, kwargs = on_sticker.call_args chat, sticker = args assert chat.legacy_id == self.juliet.legacy_id assert sticker.hashes["sha_1"] == "4b97ce7f0f06a0e05999f3c719cd5b4f3da992a7" assert sticker.path.exists() assert sticker.content_type == "image/png" # this time slidge must have cached the BoBd ata, so no bob-fetching IQ with unittest.mock.patch( "slidge.core.session.BaseSession.on_sticker" ) as on_sticker: self.recv(sticker_stanza) on_sticker.assert_awaited_once() args, kwargs = on_sticker.call_args chat, sticker = args assert chat.legacy_id == self.juliet.legacy_id assert sticker.hashes["sha_1"] == "4b97ce7f0f06a0e05999f3c719cd5b4f3da992a7" assert sticker.path.exists() assert self.next_sent() is None def test_movim_custom_emoji(self): with unittest.mock.patch("slidge.core.session.BaseSession.on_text") as on_text: self.recv( # language=XML f""" fdsf :amogus:

fdsf :amogus:

""" ) on_text.assert_awaited_once() args, kwargs = on_text.call_args assert args[1] == "fdsf :amogus:" def test_bob_not_found(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ Bits of binary 'bogus' is not available """ ) def test_carbon_retract(self): with ( unittest.mock.patch( "slidge.core.session.BaseSession.on_retract" ) as on_retract, unittest.mock.patch( "slidge.core.session.BaseSession.on_correct" ) as on_correct, ): self.juliet.retract("some-id", carbon=True) self.recv( # language=XML f""" /me retracted the message 1269564719166132224 """ ) on_correct.assert_not_awaited() on_retract.assert_not_awaited() def test_new_thread_from_xmpp(self): with ( unittest.mock.patch("slidge.core.session.BaseSession.on_text") as on_text, unittest.mock.patch( "slidge.contact.contact.LegacyContact.create_thread", return_value="legacy-thread-id", ), ): self.recv( # language=XML f""" I start a new thread xmpp-thread-id """ ) on_text.assert_awaited_once() args, kwargs = on_text.call_args assert kwargs["thread"] == "legacy-thread-id" with unittest.mock.patch("slidge.core.session.BaseSession.on_text") as on_text: self.recv( # language=XML f""" I send a new message in the new thread xmpp-thread-id """ ) on_text.assert_awaited_once() args, kwargs = on_text.call_args assert kwargs["thread"] == "legacy-thread-id" def test_multi_correction_caption(self): from slidge import global_config global_config.USE_ATTACHMENT_ORIGINAL_URLS = True self.xmpp.use_message_ids = True self.run_coro( self.juliet.send_file( file_url=self.avatar_url, legacy_msg_id="original-id", caption="prout" ) ) self.send( # language=XML """ AVATAR_URL AVATAR_URL """, use_values=False, ) self.send( # language=XML """ prout """, use_values=False, ) self.run_coro( self.juliet.send_file( file_url=self.avatar_url + "--NEW", legacy_msg_id="original-id", caption="prout", correction=True, ) ) self.send( # language=XML """ /me retracted the message 2 """, use_values=False, ) self.send( # language=XML """ AVATAR_URL--NEW AVATAR_URL--NEW """, use_values=False, ) self.send( # language=XML """ prout """, use_values=False, ) self.xmpp.use_message_ids = False global_config.USE_ATTACHMENT_ORIGINAL_URLS = False def test_multi_correction(self): from slidge import global_config global_config.USE_ATTACHMENT_ORIGINAL_URLS = True self.xmpp.use_message_ids = True self.run_coro( self.juliet.send_file(file_url=self.avatar_url, legacy_msg_id="original-id") ) self.send( # language=XML """ AVATAR_URL AVATAR_URL """, use_values=False, ) self.run_coro( self.juliet.send_file( file_url=self.avatar_url + "--NEW", legacy_msg_id="original-id", correction=True, ) ) self.send( # language=XML """ AVATAR_URL--NEW AVATAR_URL--NEW """, use_values=False, ) self.xmpp.use_message_ids = False global_config.USE_ATTACHMENT_ORIGINAL_URLS = False slidge/tests/test_session_2.py000066400000000000000000000210271477703150600170200ustar00rootroot00000000000000from datetime import datetime, timezone import pytest from conftest import AvatarFixtureMixin from slixmpp import JID, Iq from slixmpp import __version__ as slix_version from slidge import BaseGateway, BaseSession, MucType from slidge.contact import LegacyContact from slidge.core.session import _sessions from slidge.group import LegacyBookmarks, LegacyMUC from slidge.util.test import SlidgeTest class Gateway(BaseGateway): COMPONENT_NAME = "A test" GROUPS = True class Session(BaseSession): async def login(self): return "YUP" class Contact(LegacyContact): async def update_info(self): self.is_friend = True self.added_to_roster = True self.name = "A name" self.online("status msg") await self.set_avatar("AVATAR_URL") class MUC(LegacyMUC): async def update_info(self): self.name = "Cool name" self.description = "Cool description" self.type = MucType.CHANNEL_NON_ANONYMOUS self.subject = "Cool subject" self.subject_setter = await self.get_participant_by_legacy_id("juliet") self.subject_date = datetime(2000, 1, 1, 0, 0, tzinfo=timezone.utc) self.n_participants = 666 self.user_nick = "Cool nick" await self.set_avatar("AVATAR_URL") class Bookmarks(LegacyBookmarks): async def fill(self): return @pytest.mark.usefixtures("avatar") class TestSession2(AvatarFixtureMixin, SlidgeTest): plugin = globals() xmpp: Gateway def setUp(self): super().setUp() user = self.xmpp.store.users.new( JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""} ) user.preferences = {"sync_avatar": True, "sync_presence": True} self.xmpp.store.users.update(user) self.run_coro( self.xmpp._BaseGateway__dispatcher._on_user_register( Iq(sfrom="romeo@montague.lit/gajim") ) ) welcome = self.next_sent() assert welcome["body"] stanza = self.next_sent() assert "logging in" in stanza["status"].lower(), stanza stanza = self.next_sent() assert "syncing contacts" in stanza["status"].lower(), stanza stanza = self.next_sent() assert "syncing groups" in stanza["status"].lower(), stanza stanza = self.next_sent() assert "yup" in stanza["status"].lower(), stanza self.send( # language=XML """ """ ) def tearDown(self): super().tearDown() _sessions.clear() @property def romeo_session(self) -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) def test_contact_init(self): self.run_coro(self.romeo_session.contacts.by_legacy_id("juliet")) self.send( # language=XML f""" status msg """ ) self.send( # language=XML """ A name """, use_values=False, ) self.send( # language=XML f""" """, use_values=False, # I do not understand why this is necessary, related on test run order?!? ) assert self.next_sent() is None juliet: Contact = self.run_coro( self.romeo_session.contacts.by_legacy_id("juliet") ) assert juliet.name == "A name" assert juliet.is_friend cached_presence = juliet._get_last_presence() assert cached_presence is not None assert cached_presence.pstatus == "status msg" assert juliet.avatar is not None def test_group_init(self): self.run_coro(self.romeo_session.bookmarks.by_legacy_id("room")) self.next_sent() # juliet presence self.next_sent() # juliet nick self.next_sent() # juliet avatar muc = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("room")) assert self.next_sent() is None # self.run_coro(muc._set) assert muc.name == "Cool name" assert muc.description == "Cool description" assert muc.type == MucType.CHANNEL_NON_ANONYMOUS assert muc.n_participants == 666 assert muc.user_nick == "Cool nick" assert muc.avatar is not None assert muc.subject == "Cool subject" assert muc.subject_date == datetime(2000, 1, 1, 0, 0, tzinfo=timezone.utc) assert ( muc.subject_setter == self.run_coro(self.romeo_session.contacts.by_legacy_id("juliet")).name ) def test_set_user_nick_outside_update_info(self): muc = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("room")) assert muc.user_nick == "Cool nick" muc.user_nick = "Cooler nick" muc = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("room")) assert muc.user_nick == "Cooler nick" def test_user_available(self): self.run_coro(self.romeo_session.contacts.by_legacy_id("juliet")) for _ in range(3): assert self.next_sent() is not None self.recv( # language=XML f""" """ ) assert self.next_sent() is not None assert self.next_sent() is None def test_leave_group(self): muc: LegacyMUC = self.run_coro( self.romeo_session.bookmarks.by_legacy_id("room") ) self.next_sent() # juliet presence self.next_sent() # juliet nick self.next_sent() # juliet avatar assert self.next_sent() is None assert muc.jid in list([m.jid for m in self.romeo_session.bookmarks]) muc.add_user_resource("gajim") self.run_coro(self.romeo_session.bookmarks.remove(muc)) self.send( # language=XML """ You left this group from the official client. 0 """ ) assert muc.jid not in list([m.jid for m in self.romeo_session.bookmarks]) slidge/tests/test_set_name_before_fill.py000066400000000000000000000032761477703150600212450ustar00rootroot00000000000000from conftest import AvatarFixtureMixin from slidge import BaseGateway, BaseSession from slidge.contact import LegacyContact, LegacyRoster from slidge.core.session import _sessions from slidge.group import LegacyBookmarks from slidge.util.test import SlidgeTest class Gateway(BaseGateway): COMPONENT_NAME = "A test" class Session(BaseSession): async def login(self): await self.contacts.by_name("some id", "some name") await self.bookmarks.by_name("some group id", "some group name") return "YUP" class Roster(LegacyRoster): async def by_name(self, legacy_id: str, name: str): return await self.by_legacy_id(legacy_id, name) class Contact(LegacyContact): def __init__(self, session, legacy_id, jid_username, name: str | None = None): super().__init__(session, legacy_id, jid_username) if name is not None: self.name = name def use_contact_info(self, name: str): self.name = name class Bookmarks(LegacyBookmarks): async def fill(self): return async def by_name(self, legacy_id: str, name: str): muc = await self.by_legacy_id(legacy_id) muc.name = name class TestSetNameBeforeFill(AvatarFixtureMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() self.setup_logged_session(1) def tearDown(self): super().tearDown() _sessions.clear() def test_set_contact_name_before_fill(self): contact = self.run_coro(self.romeo.contacts.by_legacy_id("some id")) assert contact.name == "some name" muc = self.run_coro(self.romeo.bookmarks.by_legacy_id("some group id")) assert muc.name == "some group name" slidge/tests/test_shakespeare.py000066400000000000000000002171531477703150600174160ustar00rootroot00000000000000import asyncio import datetime import importlib import logging import re import tempfile import unittest.mock from copy import copy from pathlib import Path from typing import Any, Dict, Hashable, Optional from xml.etree import ElementTree as ET from slixmpp import JID, Message, Presence from slixmpp import __version__ as slix_version from slixmpp.exceptions import XMPPError from slixmpp.plugins.xep_0030 import DiscoInfo from slixmpp.plugins.xep_0082 import format_datetime from slixmpp.plugins.xep_0356.permissions import ( MessagePermission, Permissions, PresencePermission, RosterAccess, ) from sqlalchemy import delete from slidge import * from slidge import ( FormField, GatewayUser, LegacyBookmarks, LegacyContact, LegacyRoster, SearchResult, ) from slidge.command.categories import ADMINISTRATION from slidge.core import config from slidge.core.mixins.attachment import AttachmentMixin from slidge.core.session import _sessions from slidge.db.models import Contact from slidge.util.test import SlidgeTest from slidge.util.types import LegacyAttachment, LegacyContactType, LegacyMessageType received_presences: list[Optional[Presence]] = [] text_received_by_juliet = [] composing_chat_states_received_by_juliet = [] unregistered = [] reactions_received_by_juliet = [] class ClearSessionMixin: def tearDown(self): super().tearDown() _sessions.clear() class Gateway(BaseGateway): COMPONENT_NAME = "SLIDGE TEST" SEARCH_FIELDS = [FormField(var="leg", label="Enter the legacy ID")] SEARCH_TITLE = "Search for legacy contacts" GROUPS = True LEGACY_CONTACT_ID_TYPE = int async def unregister(self, user: GatewayUser): unregistered.append(user) class Session(BaseSession): @staticmethod def xmpp_to_legacy_msg_id(i: str): return int(i) async def on_search(self, form_values: Dict[str, str]): if form_values["leg"] == "exists": return SearchResult( fields=[FormField(var="jid", label="JID", type="jid-single")], items=[{"jid": "exists@example.com"}], ) def __init__(self, user): super().__init__(user) async def wait_for_ready(self, timeout=10): return async def login(self): pass async def logout(self): pass async def on_text( self, chat: LegacyContact, text: str, *, reply_to=None, reply_to_msg_id=None, reply_to_fallback_text: Optional[str] = None, thread=None, link_previews=(), ): if chat.jid_username == "juliet": text_received_by_juliet.append((text, chat)) assert self.user_jid.bare == "romeo@montague.lit" assert self.user_jid == JID("romeo@montague.lit") chat.send_text("I love you") return 0 async def on_composing(self, c: LegacyContact, thread=None): composing_chat_states_received_by_juliet.append(c) async def on_displayed( self, c: LegacyContact, legacy_msg_id: Hashable, thread=None ): pass async def on_react( self, c: LegacyContact, legacy_msg_id: LegacyMessageType, emojis: list[str], thread=None, ): if c.jid_username == "juliet": for e in emojis: reactions_received_by_juliet.append([legacy_msg_id, e]) class Roster(LegacyRoster): @staticmethod async def jid_username_to_legacy_id(jid_username: str) -> int: log.debug("Requested JID to legacy: %s", jid_username) if jid_username == "juliet": return 123 elif jid_username == "new-friend": return 456 else: raise XMPPError(text="Only juliet", condition="item-not-found") @staticmethod async def legacy_id_to_jid_username(legacy_id: int) -> str: if legacy_id == 123: return "juliet" elif legacy_id == 456: return "new-friend" else: raise RuntimeError class Bookmarks(LegacyBookmarks): @staticmethod async def jid_local_part_to_legacy_id(local_part): if local_part != "room": raise XMPPError("item-not-found") else: return local_part async def fill(self): await self.by_legacy_id("room1") await self.by_legacy_id("room2") class Base(ClearSessionMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() user = self.xmpp.store.users.new( JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""} ) user.preferences = {"sync_avatar": True, "sync_presence": True} self.xmpp.store.users.update(user) self.get_romeo_session().logged = True @staticmethod def get_romeo_session() -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) @property def juliet(self) -> LegacyContact: session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) return self.run_coro(session.contacts.by_jid(JID("juliet@aim.shakespeare.lit"))) class TestAimShakespeareBase(Base): def loop(self, x): self.run_coro(x) def test_jabber_iq_gateway(self): self.recv( # language=XML """ """ ) self.send( # language=XML f""" {Gateway.SEARCH_TITLE} {Gateway.SEARCH_FIELDS[0].label} """ ) self.recv( # language=XML """ exists """ ) self.send( # language=XML """ exists@example.com """ ) self.recv( # language=XML """ not-exists """ ) self.send( # language=XML """ No contact was found with the info you provided. """, use_values=False, ) def test_jabber_iq_gateway_on_contact(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ This can only be used on the component JID """ ) def test_from_romeo_to_eve(self): self.recv( # language=XML """ Art thou not Romeo, and a Montague? """ ) self.send( # language=XML """ Only juliet """, use_values=False, ) def test_from_romeo_to_juliet(self): self.recv( # language=XML """ Art thou not Romeo, and a Montague? """ ) text, contact = text_received_by_juliet[-1] assert text == "Art thou not Romeo, and a Montague?" assert contact.legacy_id == 123 m: Message = self.next_sent() assert m.get_from() == "juliet@aim.shakespeare.lit/slidge" assert m["body"] == "I love you" m2 = copy( m ) # there must be a better way to check for the presence of the markable thing m2.enable("markable") assert m == m2 text_received_by_juliet.clear() def test_delivery_receipt(self): self.xmpp.PROPER_RECEIPTS = True self.recv( # language=XML """ Art thou not Romeo, and a Montague? """ ) self.next_sent() # auto reply in our test plugin session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) juliet = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) juliet.received("123") self.send( # language=XML """ """ ) self.send( # language=XML """ """ ) assert self.next_sent() is None def test_romeo_composing(self): self.recv( # language=XML """ """ ) assert len(composing_chat_states_received_by_juliet) == 1 assert composing_chat_states_received_by_juliet[0].legacy_id == 123 def test_from_eve_to_juliet(self): # just ignore messages from unregistered users self.recv( # language=XML """ Art thou not Romeo, and a Montague? """ ) self.send( # language=XML """ You are not registered to this gateway """ ) def test_juliet_sends_text(self): session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) juliet = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) juliet.send_text(body="What what?") msg = self.next_sent() assert msg["from"] == f"juliet@aim.shakespeare.lit/{LegacyContact.RESOURCE}" assert msg["to"] == "romeo@montague.lit" assert msg["body"] == "What what?" def test_unregister(self): assert len(unregistered) == 0 self.recv( # language=XML """ """ ) # this creates a session self.recv( # language=XML """ """ ) assert len(unregistered) == 1 assert unregistered[0].jid == "romeo@montague.lit" def test_jid_validator(self): self.xmpp.jid_validator = re.compile(".*@noteverybody") self.recv( # language=XML f""" """ ) self.send( # language=XML """ Your account is not allowed to use this gateway. """, use_values=False, ) self.recv( # language=XML """ bill Calliope """ ) self.send( # language=XML """ Your account is not allowed to use this gateway. """, use_values=False, ) self.xmpp.jid_validator = re.compile(".*") def test_reactions(self): self.recv( # language=XML """ 👋 🐢 """ ) assert len(reactions_received_by_juliet) == 2 msg_id, emoji = reactions_received_by_juliet[0] assert msg_id == 555 assert emoji == "👋" msg_id, emoji = reactions_received_by_juliet[1] assert msg_id == 555 assert emoji == "🐢" session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) juliet = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) juliet.react("legacy1", "👋") msg = self.next_sent() assert msg["reactions"]["id"] == "legacy1" for r in msg["reactions"]: assert r["value"] == "👋" reactions_received_by_juliet.clear() def test_reactions_fallback(self): self.recv( # language=XML """ > huuuuu\n👍 👍 0516cd85-4d78-4d29-bc87-378e33d820b3 """ ) assert len(reactions_received_by_juliet) == 1 msg_id, emoji = reactions_received_by_juliet[0] assert msg_id == 4940890112 assert emoji == "👍" assert len(text_received_by_juliet) == 0 text_received_by_juliet.clear() reactions_received_by_juliet.clear() def test_last_seen(self): session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) juliet = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) juliet.is_friend = True now = datetime.datetime.now(datetime.timezone.utc) juliet.away(last_seen=now) sent = self.next_sent() assert sent["idle"]["since"] == now def test_disco_adhoc_commands_unregistered(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) def test_disco_adhoc_commands_as_logged_user(self): self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) def test_disco_adhoc_commands_as_non_logged_user(self): self.get_romeo_session().logged = False self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) self.get_romeo_session().logged = True def test_disco_adhoc_commands_as_admin(self): # monkeypatch.setattr(config, "ADMINS", ("romeo@montague.lit",)) config.ADMINS = (JID("admin@montague.lit"),) self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) config.ADMINS = () def test_disco_adhoc_commands_as_unauthorized(self): self.xmpp.jid_validator = re.compile("nothing") self.recv( # language=XML f""" """ ) st = self.next_sent() assert st["error"]["condition"] == "forbidden" self.xmpp.jid_validator = re.compile(".*") def test_disco_adhoc_command_register(self): self.xmpp.jid_validator = re.compile(".*@whatever.com") self.recv( # language=XML f""" """ ) self.send( # language=XML f""" """ ) self.xmpp.jid_validator = re.compile(".*") def test_adhoc_forbidden_non_admin(self): with unittest.mock.patch( "slixmpp.plugins.xep_0050.adhoc.XEP_0050.new_session", return_value="session-id", ): self.recv( # language=XML f""" """ ) self.send( # language=XML """ 🛷️ Slidge administration """, use_values=False, ) def test_disco_component(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) def test_disco_local_part_unregistered(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, use_values=False, ) def test_disco_registered_existing_contact(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, ) def test_disco_registered_existing_contact_bare_jid(self): self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, ) def test_disco_items_registered_existing_contact(self): session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) self.run_coro(session.bookmarks.fill()) self.recv( # language=XML f""" """ ) self.send( # language=XML """ """, ) def test_disco_restricted_reaction(self): session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) juliet: LegacyContact = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) LegacyContact.REACTIONS_SINGLE_EMOJI = True self.recv( # language=XML f""" """ ) self.send( # language=XML """ urn:xmpp:reactions:0:restrictions 1 """, ) LegacyContact.REACTIONS_SINGLE_EMOJI = False def test_non_existing_contact(self): self.recv( # language=XML f""" DSAD """ ) self.send( # language=XML """ Only juliet """, use_values=False, ) def test_attachments(self): a = LegacyAttachment(path="x") ids = [] async def send_file(self, file_path, legacy_msg_id=None, *_a, **_k): ids.append(legacy_msg_id) m = Message() m.set_id(legacy_msg_id) return "", [m] orig = LegacyContact.send_file LegacyContact.send_file = send_file self.loop(self.juliet.send_files([], body="Hey")) assert not self.next_sent().get_id() self.loop(self.juliet.send_files([], body="")) assert self.next_sent() is None self.loop(self.juliet.send_files([a, a, a], body="")) assert ids.pop(-3) is None assert ids.pop(-2) is None assert ids.pop(-1) is None self.loop(self.juliet.send_files([a, a, a], legacy_msg_id="leg")) assert ids.pop(-3) is None assert ids.pop(-2) is None assert ids.pop(-1) == "leg" self.loop(self.juliet.send_files([], body="hoy")) assert not self.next_sent().get_id() self.loop(self.juliet.send_files([], body="hoy", legacy_msg_id="leg")) assert self.next_sent().get_id() == "leg" self.loop( self.juliet.send_files([], body="hoy", legacy_msg_id="leg", body_first=True) ) assert self.next_sent().get_id() == "leg" self.loop( self.juliet.send_files( [a, a, a, a], body="hoy", legacy_msg_id="leg", body_first=True ) ) assert self.next_sent().get_id() == "leg" assert ids.pop(-4) is None assert ids.pop(-3) is None assert ids.pop(-2) is None assert ids.pop(-1) is None self.loop( self.juliet.send_files( [a, a, a, a], body="hoy", legacy_msg_id="leg", body_first=False ) ) assert ids.pop(-4) is None assert ids.pop(-3) is None assert ids.pop(-2) is None assert ids.pop(-1) is None assert self.next_sent().get_id() == "leg" self.loop(self.juliet.send_files([a, a, a, a], body="hoy")) assert ids.pop(-4) is None assert ids.pop(-3) is None assert ids.pop(-2) is None assert ids.pop(-1) is None assert not self.next_sent().get_id() self.loop(self.juliet.send_files([a])) assert ids.pop(-1) is None self.loop(self.juliet.send_files([a], legacy_msg_id="leg")) assert ids.pop(-1) == "leg" LegacyContact.send_file = orig def test_gateway_message(self): session = self.get_romeo_session() session.send_gateway_message("Hello!") self.send( # language=XML """ Hello! """ ) class TestPrivilege(ClearSessionMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() self.xmpp.store.users.new( JID("romeo@shakespeare.lit/gajim"), {"username": "romeo", "city": ""} ) def test_privilege(self): assert ( self.xmpp["xep_0356"].granted_privileges["shakespeare.lit"] == Permissions() ) self.recv( # language=XML """ """ ) assert ( self.xmpp["xep_0356"].granted_privileges["shakespeare.lit"].message == MessagePermission.OUTGOING ) assert ( self.xmpp["xep_0356"].granted_privileges["shakespeare.lit"].presence == PresencePermission.NONE ) assert ( self.xmpp["xep_0356"].granted_privileges["shakespeare.lit"].roster == RosterAccess.BOTH ) session = BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@shakespeare.lit") ) juliet = self.run_coro( session.contacts.by_jid(JID("juliet@aim.shakespeare.lit")) ) juliet.send_text("body", 545, carbon=True) self.send( # language=XML """ body """, ) juliet.is_friend = True self.xmpp.loop.create_task(juliet.add_to_roster()) self.send( # language=XML """ slidge """ ) class TestContact(ClearSessionMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() self.xmpp.store.users.new( JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""} ) self.get_romeo_session().logged = True self.get_romeo_session().contacts.ready.set_result(True) with self.xmpp.store.session() as session: session.execute(delete(Contact)) # db.presence_nuke() @staticmethod def get_romeo_session() -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@montague.lit") ) @staticmethod def get_presence(ptype: str): return f""" """ def get_contact(self, legacy_id: int): session = self.get_romeo_session() return self.run_coro(session.contacts.by_legacy_id(legacy_id)) def get_juliet(self) -> LegacyContact: return self.get_contact(123) def get_new_friend(self) -> LegacyContact: return self.get_contact(456) def test_caps(self): juliet = self.get_juliet() juliet.is_friend = True juliet.online() self.send( # language=XML f""" 0 """ ) def test_caps_extended(self): LegacyContact.REACTIONS_SINGLE_EMOJI = True LegacyContact.CORRECTION = False juliet = self.get_juliet() juliet.is_friend = True juliet.online() self.send( # language=XML f""" 0 """ ) LegacyContact.REACTIONS_SINGLE_EMOJI = False LegacyContact.CORRECTION = True def test_vcard_temp(self): juliet = self.get_juliet() juliet.is_friend = True self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) juliet.set_vcard(full_name="Juliet Something") self.next_sent() self.recv( # language=XML """ """ ) self.send( # language=XML """ xmpp:juliet@aim.shakespeare.lit Juliet Something """, use_values=False, ) def test_probe(self): juliet = self.get_juliet() probe = self.get_presence("probe") juliet.is_friend = True self.recv(probe) p = self.next_sent() assert p["type"] == "unavailable" juliet.online() assert self.next_sent()["type"] == "available" self.recv(probe) assert self.next_sent()["type"] == "available" juliet.is_friend = False self.recv(probe) p = self.next_sent() assert p["type"] == "unsubscribed" assert p["from"] == juliet.jid.bare assert self.next_sent() is None def test_user_subscribe_to_friend(self): juliet = self.get_juliet() juliet.is_friend = True sub = self.get_presence("subscribe") with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_request" ) as mock: self.recv(sub) mock.assert_not_awaited() p = self.next_sent() assert p["type"] == "subscribed" assert p["from"] == juliet.jid.bare assert self.next_sent() is None assert juliet.is_friend def test_user_subscribe_to_non_friend_accept(self): juliet = self.get_juliet() juliet.is_friend = False sub = self.get_presence("subscribe") with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_request" ) as mock: self.recv(sub) mock.assert_awaited_once() assert self.next_sent() is None assert not juliet.is_friend juliet.name = "JULIET" assert self.next_sent() is None self.run_coro(juliet.accept_friend_request()) p = self.next_sent() assert p["type"] == "subscribed" assert p["from"] == juliet.jid.bare assert ( self.next_sent()["pubsub_event"]["items"]["item"]["nick"]["nick"] == "JULIET" ) assert self.next_sent() is None assert juliet.is_friend def test_user_subscribe_to_non_friend_reject(self): juliet = self.get_juliet() juliet.is_friend = False sub = self.get_presence("subscribe") with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_request" ) as mock: self.recv(sub) mock.assert_awaited_once() assert self.next_sent() is None juliet.name = "JULIET" assert self.next_sent() is None juliet.reject_friend_request() p = self.next_sent() assert p["from"] == juliet.jid.bare assert p["type"] == "unsubscribed" assert self.next_sent() is None assert not juliet.is_friend def test_juliet_send_friend_request_user_accepts(self): juliet = self.get_juliet() juliet.name = "JUJU" juliet.send_friend_request() p = self.next_sent() assert p["from"] == juliet.jid.bare assert p["type"] == "subscribe" assert p["to"] == "romeo@montague.lit" assert p["nick"]["nick"] == "JUJU" assert self.next_sent() is None with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_accept" ) as mock: self.recv( # language=XML f""" """ ) mock.assert_awaited_once() assert self.next_sent() is None def test_juliet_send_friend_request_user_rejects(self): juliet = self.get_juliet() juliet.name = "JUJU" juliet.is_friend = False juliet.send_friend_request() p = self.next_sent() assert p["type"] == "subscribe" assert p["from"] == juliet.jid.bare assert p["to"] == "romeo@montague.lit" assert p["nick"]["nick"] == "JUJU" assert self.next_sent() is None with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_delete" ) as mock: self.recv( # language=XML f""" """ ) mock.assert_not_awaited() assert self.next_sent() is None juliet.is_friend = True with unittest.mock.patch( "slidge.contact.LegacyContact.on_friend_delete" ) as mock: self.recv( # language=XML f""" """ ) mock.assert_awaited_once() assert self.next_sent() is None def test_send_several_subscription_requests_in_a_row(self): juliet = self.get_juliet() juliet.online() juliet.send_friend_request() assert self.next_sent()["type"] == "subscribe" juliet.send_friend_request() assert self.next_sent()["type"] == "subscribe" def test_correct(self): juliet = self.get_juliet() juliet.correct("old_msg_id", "new content") msg = self.next_sent() assert msg["replace"]["id"] == "old_msg_id" assert msg["body"] == "new content" assert msg["id"] == "" juliet.correct("old_msg_id", "new content", correction_event_id="correction_id") msg = self.next_sent() assert msg["replace"]["id"] == "old_msg_id" assert msg["body"] == "new content" assert msg["id"] == "correction_id" def test_retract(self): juliet = self.get_juliet() juliet.retract("old_msg_id") # msg = self.next_sent() self.send( # language=XML """ /me retracted the message old_msg_id """ ) def test_presence(self): juliet = self.get_juliet() juliet.is_friend = True now = datetime.datetime.now(tz=datetime.timezone.utc) a_week_ago = now - datetime.timedelta(days=7) juliet.away(status="Bye bye", last_seen=now) p = self.next_sent() assert p["status"] == f"Bye bye -- Last seen {now:%A %H:%M GMT}" assert p["idle"]["since"] == now assert p["show"] == "away" juliet.extended_away(status="Bye bye", last_seen=a_week_ago) p = self.next_sent() assert p["status"] == f"Bye bye -- Last seen {a_week_ago:%b %-d %Y}" assert p["idle"]["since"] == a_week_ago assert p["show"] == "xa" juliet.busy(last_seen=now) p = self.next_sent() assert p["status"] == f"Last seen {now:%A %H:%M GMT}" assert p["idle"]["since"] == now assert p["show"] == "dnd" juliet.busy(last_seen=a_week_ago) p = self.next_sent() assert p["status"] == f"Last seen {a_week_ago:%b %-d %Y}" assert p["idle"]["since"] == a_week_ago def test_disco_bare(self): juliet = self.get_juliet() juliet.is_friend = True self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) def test_disco_resource(self): juliet = self.get_juliet() juliet.is_friend = True self.recv( # language=XML f""" """ ) self.send( # language=XML """ """ ) class TestCarbon(ClearSessionMixin, SlidgeTest): plugin = globals() def setUp(self): super().setUp() self.xmpp.store.users.new( JID("romeo@shakespeare.lit/gajim"), {"username": "romeo", "city": ""} ) self.recv( # language=XML """ """ ) self.get_romeo_session().logged = True @staticmethod def get_romeo_session() -> Session: return BaseSession.get_self_or_unique_subclass().from_jid( JID("romeo@shakespeare.lit") ) def get_juliet(self) -> LegacyContact: session = self.get_romeo_session() return self.run_coro(session.contacts.by_legacy_id(123)) def test_carbon_send_file(self): orig = AttachmentMixin._AttachmentMixin__get_url async def get_url(self, file_path, *a, **k): return False, file_path, "URL" AttachmentMixin._AttachmentMixin__get_url = get_url juliet = self.get_juliet() juliet.send_text("TEXT", 445, carbon=True) self.send( # language=XML """ TEXT """ ) with tempfile.NamedTemporaryFile("w+") as f: f.write("test") f.seek(0) self.run_coro( juliet.send_file(file_path=f.name, legacy_msg_id=446, carbon=True) ) stamp = format_datetime( datetime.datetime.fromtimestamp(Path(f.name).stat().st_mtime) ) self.send( # language=XML f""" {Path(f.name).name} 4 {stamp} n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= {Path(f.name).name} 4 {stamp} n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= URL URL """ ) AttachmentMixin._AttachmentMixin__get_url = orig def test_carbon_ignore(self): """ Ensure we don't react to stanzas we send to ourselves. """ juliet = self.get_juliet() juliet.send_text("TEXT", 123456, carbon=True) self.send( # language=XML """ TEXT """ ) with unittest.mock.patch("test_shakespeare.Session.on_text") as mock: mock.get_id = lambda *a: "msg_id" self.recv( # language=XML """ TEXT """ ) mock.assert_not_awaited() class TestUserGetsOnline(Base): def setUp(self): super().setUp() self.get_romeo_session().contacts.ready.set_result(True) self.juliet.is_friend = True self.juliet.added_to_roster = True self.juliet.name = "Juliet" self.send( # language=XML """ Juliet """, use_values=False, ) def _assert_send_nick(self): self.send( # language=XML """ Juliet """, use_values=False, ) def test_user_online_without_caps(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) self.recv( # language=XML """ """ ) self._assert_send_nick() assert self.next_sent() is None def test_user_online_with_caps(self): with unittest.mock.patch( "slixmpp.plugins.xep_0115.caps.XEP_0115.get_caps" ) as get_caps: get_caps.return_value = DiscoInfo( xml=ET.fromstring( # language=XML """ """ ) ) self.recv( # language=XML """ """ ) self._assert_send_nick() assert self.next_sent() is None def test_user_online_with_caps_no_nick_notify(self): with unittest.mock.patch( "slixmpp.plugins.xep_0115.caps.XEP_0115.get_caps" ) as get_caps: get_caps.return_value = DiscoInfo( xml=ET.fromstring( # language=XML """ """ ) ) self.recv( # language=XML """ """ ) assert self.next_sent() is None class TestUserPresence(Base): def setUp(self): self.patcher = unittest.mock.patch( "slidge.core.session.BaseSession.on_presence" ) self.presence = self.patcher.start() super().setUp() def tearDown(self): super().tearDown() self.patcher.stop() def test_user_presence(self): self.recv( # language=XML """ """ ) self.presence.assert_called_with( "cheogram", "available", "", {"cheogram": {"status": "", "show": "", "priority": 0}}, {"status": "", "show": "", "priority": 0}, ) self.presence.assert_awaited_once() self.presence.reset_mock() self.recv( # language=XML """ away I use gajim, yay! """ ) self.presence.assert_called_with( "gajim", "away", "I use gajim, yay!", { "cheogram": {"status": "", "show": "", "priority": 0}, "gajim": {"status": "I use gajim, yay!", "show": "away", "priority": 0}, }, {"status": "I use gajim, yay!", "show": "", "priority": 0}, ) self.recv( # language=XML """ """ ) self.presence.assert_called_with( "gajim", "unavailable", "", { "cheogram": {"status": "", "show": "", "priority": 0}, }, {"status": "", "show": "", "priority": 0}, ) log = logging.getLogger(__name__) slidge/tests/test_stanza_link_preview.py000066400000000000000000000107641477703150600212000ustar00rootroot00000000000000import unittest from slixmpp.test import SlixTest from slidge.slixfix.link_preview import stanza class TestLinkPreview(SlixTest): def setUp(self): stanza.register_plugin() def testSetLinkPreview(self): msg = self.Message() msg["link_preview"]["about"] = "https://the.link.example.com/what-was-linked-to" msg["link_preview"]["title"] = "A cool title" self.check( msg, # language=xml """ A cool title """, use_values=False, ) assert msg["link_preview"]["description"] is None def testGetLinkPreview(self): # language=xml xml_str = """ I wanted to mention https://the.link.example.com/what-was-linked-to Page Title Page Description Canonical URL https://link.to.example.com/image.png website Some Website """ xml = self.parse_xml(xml_str) msg = self.Message(xml) assert msg["link_preview"]["title"] == "Page Title" assert msg["link_preview"]["description"] == "Page Description" assert msg["link_preview"]["url"] == "Canonical URL" assert msg["link_preview"]["image"] == "https://link.to.example.com/image.png" assert msg["link_preview"]["site_name"] == "Some Website" assert msg["link_preview"]["type"] == "website" assert ( msg["link_preview"]["about"] == "https://the.link.example.com/what-was-linked-to" ) def testGetLinkPreviews(self): # language=xml xml_str = """ I wanted to mention https://the.link.example.com/what-was-linked-to Page Title 1 Page Description 1 Canonical URL 1 https://link.to.example.com/image.png 1 website 1 Some Website 1 Page Title 2 Page Description 2 Canonical URL 2 https://link.to.example.com/image.png 2 website 2 Some Website 2 """ xml = self.parse_xml(xml_str) msg = self.Message(xml) previews = msg["link_previews"] assert len(previews) == 2 for i, p in enumerate(previews, start=1): assert p["title"] == f"Page Title {i}" assert p["description"] == f"Page Description {i}" assert p["url"] == f"Canonical URL {i}" assert p["image"] == f"https://link.to.example.com/image.png {i}" assert p["site_name"] == f"Some Website {i}" assert p["type"] == f"website {i}" assert p["about"] == f"https://the.link.example.com/what-was-linked-to {i}" suite = unittest.TestLoader().loadTestsFromTestCase(TestLinkPreview) slidge/tests/test_util.py000066400000000000000000000140021477703150600160640ustar00rootroot00000000000000import re from datetime import datetime, timedelta from slixmpp import JID from slidge.contact import LegacyContact from slidge.core import config from slidge.util import ( ABCSubclassableOnceAtMost, SubclassableOnce, is_valid_phone_number, ) from slidge.util.types import Mention from slidge.util.util import merge_resources, replace_mentions, strip_leading_emoji def test_subclass(): SubclassableOnce.TEST_MODE = False # fmt: off class A(metaclass=SubclassableOnce): pass assert A.get_self_or_unique_subclass() is A class B(A): pass assert A.get_self_or_unique_subclass() is B try: class C(A): pass except RuntimeError: pass else: raise AssertionError("RuntimeError should have been raised") A.reset_subclass() class C(A): pass assert A.get_self_or_unique_subclass() is C A.reset_subclass() class D(metaclass=ABCSubclassableOnceAtMost): pass # fmt: on SubclassableOnce.TEST_MODE = True def test_phone_validation(): assert is_valid_phone_number("+33") assert not is_valid_phone_number("+") assert not is_valid_phone_number("+asdfsadfa48919sadf") assert not is_valid_phone_number("12597891") def test_strip_delay(monkeypatch): monkeypatch.setattr(config, "IGNORE_DELAY_THRESHOLD", timedelta(seconds=300)) class MockDelay: @staticmethod def set_stamp(x): pass @staticmethod def set_from(x): pass class MockC: STRIP_SHORT_DELAY = True class xmpp: boundjid = JID("test") class MockMsg: delay_added = None def __getitem__(self, key): if key == "delay": self.delay_added = True return MockDelay msg = MockMsg() LegacyContact._add_delay(MockC, msg, datetime.now()) assert not msg.delay_added monkeypatch.setattr(config, "IGNORE_DELAY_THRESHOLD", timedelta(seconds=0)) msg = MockMsg() LegacyContact._add_delay(MockC, msg, datetime.now()) assert msg.delay_added def test_merge_presence(): assert merge_resources( { "1": { "show": "", "status": "", "priority": 0, } } ) == { "show": "", "status": "", "priority": 0, } assert merge_resources( { "1": { "show": "dnd", "status": "X", "priority": -10, }, "2": { "show": "dnd", "status": "", "priority": 0, }, } ) == { "show": "dnd", "status": "X", "priority": 0, } assert merge_resources( { "1": { "show": "", "status": "", "priority": 0, }, "2": { "show": "away", "status": "", "priority": 0, }, "3": { "show": "dnd", "status": "", "priority": 0, }, } ) == { "show": "", "status": "", "priority": 0, } assert merge_resources( { "1": { "show": "", "status": "", "priority": 0, }, "2": { "show": "away", "status": "", "priority": 0, }, "3": { "show": "dnd", "status": "Blah blah", "priority": 0, }, } ) == { "show": "", "status": "Blah blah", "priority": 0, } assert merge_resources( { "1": { "show": "", "status": "", "priority": 0, }, "2": { "show": "away", "status": "Blah", "priority": 0, }, "3": { "show": "dnd", "status": "Blah blah", "priority": 10, }, } ) == { "show": "", "status": "Blah blah", "priority": 0, } assert merge_resources( { "1": { "show": "", "status": "", "priority": 0, }, "2": { "show": "away", "status": "Blah", "priority": 0, }, "3": { "show": "dnd", "status": "", "priority": 10, }, } ) == { "show": "", "status": "Blah", "priority": 0, } def test_replace_mentions(): mentions = [] text = "Text Mention 1 and Mention 2, and Mention 3" for match in re.finditer("Mention 1|Mention 2|Mention 3", text): span = match.span() nick = match.group() mentions.append( Mention(contact=f"Contact{nick[-1]}", start=span[0], end=span[1]) ) assert ( replace_mentions(text, mentions, lambda c: "@" + c[-1]) == "Text @1 and @2, and @3" ) mentions = [] text = "Text Mention 1 and Mention 2, and Mention 3 blabla" for match in re.finditer("Mention 1|Mention 2|Mention 3", text): span = match.span() nick = match.group() mentions.append( Mention(contact=f"Contact{nick[-1]}", start=span[0], end=span[1]) ) assert ( replace_mentions(text, mentions, lambda c: "@" + c[-1]) == "Text @1 and @2, and @3 blabla" ) def test_strip_emoji(): try: import emoji except ImportError: return assert strip_leading_emoji("🛷️ Slidge administration") == "Slidge administration" assert strip_leading_emoji("no emoji") == "no emoji" assert strip_leading_emoji("no") == "no" assert strip_leading_emoji("👤 Contacts") == "Contacts" assert strip_leading_emoji("👥 Groups") == "Groups" slidge/tests/test_vcard.py000066400000000000000000000170761477703150600162240ustar00rootroot00000000000000import pytest from conftest import AvatarFixtureMixin from slidge import BaseGateway, BaseSession from slidge.contact import LegacyContact from slidge.core.session import _sessions from slidge.util.test import SlidgeTest class Gateway(BaseGateway): COMPONENT_NAME = "A test" class Session(BaseSession): async def login(self): return "YUP" class Contact(LegacyContact): async def update_info(self): if self.legacy_id == "has-vcard": self.set_vcard(full_name="A full name") async def fetch_vcard(self): if self.legacy_id == "on-demand": self.set_vcard(full_name="Lazy") @pytest.mark.usefixtures("avatar") class TestSession(AvatarFixtureMixin, SlidgeTest): plugin = globals() xmpp: Gateway def setUp(self): super().setUp() self.setup_logged_session() def tearDown(self): super().tearDown() _sessions.clear() def _assert_broadcast_on_demand(self): self.send( # language=XML """ xmpp:on-demand@aim.shakespeare.lit Lazy """ ) def test_vcard_in_update_info(self): self.run_coro(self.romeo.contacts.by_legacy_id("has-vcard")) self.send( # language=XML """ xmpp:has-vcard@aim.shakespeare.lit A full name """ ) assert self.next_sent() is None def test_vcard_outside_update_info(self): self.juliet.set_vcard("Another full name") self.send( # language=XML """ xmpp:juliet@aim.shakespeare.lit Another full name """ ) assert self.next_sent() is None def test_fetch_raw_iq(self): self.run_coro(self.romeo.contacts.by_legacy_id("on-demand")) assert self.next_sent() is None self.recv( # language=XML """ """ ) self.send( # language=XML """ xmpp:on-demand@aim.shakespeare.lit Lazy """, use_values=False, ) self._assert_broadcast_on_demand() assert self.next_sent() is None def test_fetch_raw_iq_empty(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """, use_values=False, ) assert self.next_sent() is None def test_fetch_pubsub(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ xmpp:on-demand@aim.shakespeare.lit Lazy """ ) self._assert_broadcast_on_demand() assert self.next_sent() is None def test_fetch_pubsub_empty(self): self.recv( # language=XML """ """ ) self.send( # language=XML """ """ ) assert self.next_sent() is None slidge/uv.lock000066400000000000000000007723641477703150600136660ustar00rootroot00000000000000version = 1 revision = 1 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version < '3.12'", ] [[package]] name = "aiodns" version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycares" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/84/41a6a2765abc124563f5380e76b9b24118977729e25a84112f8dfb2b33dc/aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72", size = 7823 } wheels = [ { url = "https://files.pythonhosted.org/packages/15/14/13c65b1bd59f7e707e0cc0964fbab45c003f90292ed267d159eeeeaa2224/aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5", size = 5735 }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } wheels = [ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, ] [[package]] name = "aiohttp" version = "3.11.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/1c4721d143e14af753f2bf5e3b681883e1f24b592c0482df6fa6e33597fa/aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8", size = 7676826 } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/98/be30539cd84260d9f3ea1936d50445e25aa6029a4cb9707f3b64cfd710f7/aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180", size = 708664 }, { url = "https://files.pythonhosted.org/packages/e6/27/d51116ce18bdfdea7a2244b55ad38d7b01a4298af55765eed7e8431f013d/aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed", size = 468953 }, { url = "https://files.pythonhosted.org/packages/34/23/eedf80ec42865ea5355b46265a2433134138eff9a4fea17e1348530fa4ae/aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb", size = 456065 }, { url = "https://files.pythonhosted.org/packages/36/23/4a5b1ef6cff994936bf96d981dd817b487d9db755457a0d1c2939920d620/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540", size = 1687976 }, { url = "https://files.pythonhosted.org/packages/d0/5d/c7474b4c3069bb35276d54c82997dff4f7575e4b73f0a7b1b08a39ece1eb/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c", size = 1752711 }, { url = "https://files.pythonhosted.org/packages/64/4c/ee416987b6729558f2eb1b727c60196580aafdb141e83bd78bb031d1c000/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601", size = 1791305 }, { url = "https://files.pythonhosted.org/packages/58/28/3e1e1884070b95f1f69c473a1995852a6f8516670bb1c29d6cb2dbb73e1c/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98", size = 1674499 }, { url = "https://files.pythonhosted.org/packages/ad/55/a032b32fa80a662d25d9eb170ed1e2c2be239304ca114ec66c89dc40f37f/aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567", size = 1622313 }, { url = "https://files.pythonhosted.org/packages/b1/df/ca775605f72abbda4e4746e793c408c84373ca2c6ce7a106a09f853f1e89/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3", size = 1658274 }, { url = "https://files.pythonhosted.org/packages/cc/6c/21c45b66124df5b4b0ab638271ecd8c6402b702977120cb4d5be6408e15d/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810", size = 1666704 }, { url = "https://files.pythonhosted.org/packages/1d/e2/7d92adc03e3458edd18a21da2575ab84e58f16b1672ae98529e4eeee45ab/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508", size = 1652815 }, { url = "https://files.pythonhosted.org/packages/3a/52/7549573cd654ad651e3c5786ec3946d8f0ee379023e22deb503ff856b16c/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183", size = 1735669 }, { url = "https://files.pythonhosted.org/packages/d5/54/dcd24a23c7a5a2922123e07a296a5f79ea87ce605f531be068415c326de6/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049", size = 1760422 }, { url = "https://files.pythonhosted.org/packages/a7/53/87327fe982fa310944e1450e97bf7b2a28015263771931372a1dfe682c58/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17", size = 1694457 }, { url = "https://files.pythonhosted.org/packages/ce/6d/c5ccf41059267bcf89853d3db9d8d217dacf0a04f4086cb6bf278323011f/aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86", size = 416817 }, { url = "https://files.pythonhosted.org/packages/e7/dd/01f6fe028e054ef4f909c9d63e3a2399e77021bb2e1bb51d56ca8b543989/aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24", size = 442986 }, { url = "https://files.pythonhosted.org/packages/db/38/100d01cbc60553743baf0fba658cb125f8ad674a8a771f765cdc155a890d/aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27", size = 704881 }, { url = "https://files.pythonhosted.org/packages/21/ed/b4102bb6245e36591209e29f03fe87e7956e54cb604ee12e20f7eb47f994/aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713", size = 464564 }, { url = "https://files.pythonhosted.org/packages/3b/e1/a9ab6c47b62ecee080eeb33acd5352b40ecad08fb2d0779bcc6739271745/aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb", size = 456548 }, { url = "https://files.pythonhosted.org/packages/80/ad/216c6f71bdff2becce6c8776f0aa32cb0fa5d83008d13b49c3208d2e4016/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321", size = 1691749 }, { url = "https://files.pythonhosted.org/packages/bd/ea/7df7bcd3f4e734301605f686ffc87993f2d51b7acb6bcc9b980af223f297/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e", size = 1736874 }, { url = "https://files.pythonhosted.org/packages/51/41/c7724b9c87a29b7cfd1202ec6446bae8524a751473d25e2ff438bc9a02bf/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c", size = 1786885 }, { url = "https://files.pythonhosted.org/packages/86/b3/f61f8492fa6569fa87927ad35a40c159408862f7e8e70deaaead349e2fba/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce", size = 1698059 }, { url = "https://files.pythonhosted.org/packages/ce/be/7097cf860a9ce8bbb0e8960704e12869e111abcd3fbd245153373079ccec/aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e", size = 1626527 }, { url = "https://files.pythonhosted.org/packages/1d/1d/aaa841c340e8c143a8d53a1f644c2a2961c58cfa26e7b398d6bf75cf5d23/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b", size = 1644036 }, { url = "https://files.pythonhosted.org/packages/2c/88/59d870f76e9345e2b149f158074e78db457985c2b4da713038d9da3020a8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540", size = 1685270 }, { url = "https://files.pythonhosted.org/packages/2b/b1/c6686948d4c79c3745595efc469a9f8a43cab3c7efc0b5991be65d9e8cb8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b", size = 1650852 }, { url = "https://files.pythonhosted.org/packages/fe/94/3e42a6916fd3441721941e0f1b8438e1ce2a4c49af0e28e0d3c950c9b3c9/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e", size = 1704481 }, { url = "https://files.pythonhosted.org/packages/b1/6d/6ab5854ff59b27075c7a8c610597d2b6c38945f9a1284ee8758bc3720ff6/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c", size = 1735370 }, { url = "https://files.pythonhosted.org/packages/73/2a/08a68eec3c99a6659067d271d7553e4d490a0828d588e1daa3970dc2b771/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71", size = 1697619 }, { url = "https://files.pythonhosted.org/packages/61/d5/fea8dbbfb0cd68fbb56f0ae913270a79422d9a41da442a624febf72d2aaf/aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2", size = 411710 }, { url = "https://files.pythonhosted.org/packages/33/fb/41cde15fbe51365024550bf77b95a4fc84ef41365705c946da0421f0e1e0/aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682", size = 438012 }, { url = "https://files.pythonhosted.org/packages/52/52/7c712b2d9fb4d5e5fd6d12f9ab76e52baddfee71e3c8203ca7a7559d7f51/aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489", size = 698005 }, { url = "https://files.pythonhosted.org/packages/51/3e/61057814f7247666d43ac538abcd6335b022869ade2602dab9bf33f607d2/aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50", size = 461106 }, { url = "https://files.pythonhosted.org/packages/4f/85/6b79fb0ea6e913d596d5b949edc2402b20803f51b1a59e1bbc5bb7ba7569/aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133", size = 453394 }, { url = "https://files.pythonhosted.org/packages/4b/04/e1bb3fcfbd2c26753932c759593a32299aff8625eaa0bf8ff7d9c0c34a36/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0", size = 1666643 }, { url = "https://files.pythonhosted.org/packages/0e/27/97bc0fdd1f439b8f060beb3ba8fb47b908dc170280090801158381ad7942/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca", size = 1721948 }, { url = "https://files.pythonhosted.org/packages/2c/4f/bc4c5119e75c05ef15c5670ef1563bbe25d4ed4893b76c57b0184d815e8b/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d", size = 1774454 }, { url = "https://files.pythonhosted.org/packages/73/5b/54b42b2150bb26fdf795464aa55ceb1a49c85f84e98e6896d211eabc6670/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb", size = 1677785 }, { url = "https://files.pythonhosted.org/packages/10/ee/a0fe68916d3f82eae199b8535624cf07a9c0a0958c7a76e56dd21140487a/aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4", size = 1608456 }, { url = "https://files.pythonhosted.org/packages/8b/48/83afd779242b7cf7e1ceed2ff624a86d3221e17798061cf9a79e0b246077/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7", size = 1622424 }, { url = "https://files.pythonhosted.org/packages/6f/27/452f1d5fca1f516f9f731539b7f5faa9e9d3bf8a3a6c3cd7c4b031f20cbd/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd", size = 1660943 }, { url = "https://files.pythonhosted.org/packages/d6/e1/5c7d63143b8d00c83b958b9e78e7048c4a69903c760c1e329bf02bac57a1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f", size = 1622797 }, { url = "https://files.pythonhosted.org/packages/46/9e/2ac29cca2746ee8e449e73cd2fcb3d454467393ec03a269d50e49af743f1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd", size = 1687162 }, { url = "https://files.pythonhosted.org/packages/ad/6b/eaa6768e02edebaf37d77f4ffb74dd55f5cbcbb6a0dbf798ccec7b0ac23b/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34", size = 1718518 }, { url = "https://files.pythonhosted.org/packages/e5/18/dda87cbad29472a51fa058d6d8257dfce168289adaeb358b86bd93af3b20/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913", size = 1675254 }, { url = "https://files.pythonhosted.org/packages/32/d9/d2fb08c614df401d92c12fcbc60e6e879608d5e8909ef75c5ad8d4ad8aa7/aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979", size = 410698 }, { url = "https://files.pythonhosted.org/packages/ce/ed/853e36d5a33c24544cfa46585895547de152dfef0b5c79fa675f6e4b7b87/aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802", size = 436395 }, ] [package.optional-dependencies] speedups = [ { name = "aiodns", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, ] [[package]] name = "aiosignal" version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, ] [[package]] name = "alembic" version = "1.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/57/e314c31b261d1e8a5a5f1908065b4ff98270a778ce7579bd4254477209a7/alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7", size = 1925573 } wheels = [ { url = "https://files.pythonhosted.org/packages/41/18/d89a443ed1ab9bcda16264716f809c663866d4ca8de218aa78fd50b38ead/alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53", size = 231911 }, ] [[package]] name = "astroid" version = "3.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/33/536530122a22a7504b159bccaf30a1f76aa19d23028bd8b5009eb9b2efea/astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550", size = 398731 } wheels = [ { url = "https://files.pythonhosted.org/packages/de/80/c749efbd8eef5ea77c7d6f1956e8fbfb51963b7f93ef79647afd4d9886e3/astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248", size = 275339 }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] [[package]] name = "beautifulsoup4" version = "4.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, ] [[package]] name = "brotli" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, ] [[package]] name = "brotlicffi" version = "1.1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192 } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786 }, { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165 }, { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895 }, { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834 }, { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731 }, { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783 }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } wheels = [ { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "configargparse" version = "1.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817 } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489 }, ] [[package]] name = "coverage" version = "7.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] [[package]] name = "emoji" version = "2.14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617 }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, ] [[package]] name = "frozenlist" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } wheels = [ { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, ] [[package]] name = "furo" version = "2024.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "pygments" }, { name = "sphinx" }, { name = "sphinx-basic-ng" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } wheels = [ { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, ] [[package]] name = "greenlet" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } wheels = [ { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] [[package]] name = "identify" version = "2.6.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } wheels = [ { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "lxml" version = "5.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz", hash = "sha256:773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1", size = 3679948 } wheels = [ { url = "https://files.pythonhosted.org/packages/84/b8/2b727f5a90902f7cc5548349f563b60911ca05f3b92e35dfa751349f265f/lxml-5.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d61a7d0d208ace43986a92b111e035881c4ed45b1f5b7a270070acae8b0bfb4", size = 8163457 }, { url = "https://files.pythonhosted.org/packages/91/84/23135b2dc72b3440d68c8f39ace2bb00fe78e3a2255f7c74f7e76f22498e/lxml-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856dfd7eda0b75c29ac80a31a6411ca12209183e866c33faf46e77ace3ce8a79", size = 4433445 }, { url = "https://files.pythonhosted.org/packages/c9/1c/6900ade2294488f80598af7b3229669562166384bb10bf4c915342a2f288/lxml-5.3.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a01679e4aad0727bedd4c9407d4d65978e920f0200107ceeffd4b019bd48529", size = 5029603 }, { url = "https://files.pythonhosted.org/packages/2f/e9/31dbe5deaccf0d33ec279cf400306ad4b32dfd1a0fee1fca40c5e90678fe/lxml-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b37b4c3acb8472d191816d4582379f64d81cecbdce1a668601745c963ca5cc", size = 4771236 }, { url = "https://files.pythonhosted.org/packages/68/41/c3412392884130af3415af2e89a2007e00b2a782be6fb848a95b598a114c/lxml-5.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3df5a54e7b7c31755383f126d3a84e12a4e0333db4679462ef1165d702517477", size = 5369815 }, { url = "https://files.pythonhosted.org/packages/34/0a/ba0309fd5f990ea0cc05aba2bea225ef1bcb07ecbf6c323c6b119fc46e7f/lxml-5.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c09a40f28dcded933dc16217d6a092be0cc49ae25811d3b8e937c8060647c353", size = 4843663 }, { url = "https://files.pythonhosted.org/packages/b6/c6/663b5d87d51d00d4386a2d52742a62daa486c5dc6872a443409d9aeafece/lxml-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ef20f1851ccfbe6c5a04c67ec1ce49da16ba993fdbabdce87a92926e505412", size = 4918028 }, { url = "https://files.pythonhosted.org/packages/75/5f/f6a72ccbe05cf83341d4b6ad162ed9e1f1ffbd12f1c4b8bc8ae413392282/lxml-5.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f79a63289dbaba964eb29ed3c103b7911f2dce28c36fe87c36a114e6bd21d7ad", size = 4792005 }, { url = "https://files.pythonhosted.org/packages/37/7b/8abd5b332252239ffd28df5842ee4e5bf56e1c613c323586c21ccf5af634/lxml-5.3.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:75a72697d95f27ae00e75086aed629f117e816387b74a2f2da6ef382b460b710", size = 5405363 }, { url = "https://files.pythonhosted.org/packages/5a/79/549b7ec92b8d9feb13869c1b385a0749d7ccfe5590d1e60f11add9cdd580/lxml-5.3.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:b9b00c9ee1cc3a76f1f16e94a23c344e0b6e5c10bec7f94cf2d820ce303b8c01", size = 4932915 }, { url = "https://files.pythonhosted.org/packages/57/eb/4fa626d0bac8b4f2aa1d0e6a86232db030fd0f462386daf339e4a0ee352b/lxml-5.3.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:77cbcab50cbe8c857c6ba5f37f9a3976499c60eada1bf6d38f88311373d7b4bc", size = 4983473 }, { url = "https://files.pythonhosted.org/packages/1b/c8/79d61d13cbb361c2c45fbe7c8bd00ea6a23b3e64bc506264d2856c60d702/lxml-5.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29424058f072a24622a0a15357bca63d796954758248a72da6d512f9bd9a4493", size = 4855284 }, { url = "https://files.pythonhosted.org/packages/80/16/9f84e1ef03a13136ab4f9482c9adaaad425c68b47556b9d3192a782e5d37/lxml-5.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7d82737a8afe69a7c80ef31d7626075cc7d6e2267f16bf68af2c764b45ed68ab", size = 5458355 }, { url = "https://files.pythonhosted.org/packages/aa/6d/f62860451bb4683e87636e49effb76d499773337928e53356c1712ccec24/lxml-5.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95473d1d50a5d9fcdb9321fdc0ca6e1edc164dce4c7da13616247d27f3d21e31", size = 5300051 }, { url = "https://files.pythonhosted.org/packages/3f/5f/3b6c4acec17f9a57ea8bb89a658a70621db3fb86ea588e7703b6819d9b03/lxml-5.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2162068f6da83613f8b2a32ca105e37a564afd0d7009b0b25834d47693ce3538", size = 5033481 }, { url = "https://files.pythonhosted.org/packages/79/bd/3c4dd7d903bb9981f4876c61ef2ff5d5473e409ef61dc7337ac207b91920/lxml-5.3.2-cp311-cp311-win32.whl", hash = "sha256:f8695752cf5d639b4e981afe6c99e060621362c416058effd5c704bede9cb5d1", size = 3474266 }, { url = "https://files.pythonhosted.org/packages/1f/ea/9311fa1ef75b7d601c89600fc612838ee77ad3d426184941cba9cf62641f/lxml-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:d1a94cbb4ee64af3ab386c2d63d6d9e9cf2e256ac0fd30f33ef0a3c88f575174", size = 3815230 }, { url = "https://files.pythonhosted.org/packages/0d/7e/c749257a7fabc712c4df57927b0f703507f316e9f2c7e3219f8f76d36145/lxml-5.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:16b3897691ec0316a1aa3c6585f61c8b7978475587c5b16fc1d2c28d283dc1b0", size = 8193212 }, { url = "https://files.pythonhosted.org/packages/a8/50/17e985ba162c9f1ca119f4445004b58f9e5ef559ded599b16755e9bfa260/lxml-5.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8d4b34a0eeaf6e73169dcfd653c8d47f25f09d806c010daf074fba2db5e2d3f", size = 4451439 }, { url = "https://files.pythonhosted.org/packages/c2/b5/4960ba0fcca6ce394ed4a2f89ee13083e7fcbe9641a91166e8e9792fedb1/lxml-5.3.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cd7a959396da425022e1e4214895b5cfe7de7035a043bcc2d11303792b67554", size = 5052146 }, { url = "https://files.pythonhosted.org/packages/5f/d1/184b04481a5d1f5758916de087430752a7b229bddbd6c1d23405078c72bd/lxml-5.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cac5eaeec3549c5df7f8f97a5a6db6963b91639389cdd735d5a806370847732b", size = 4789082 }, { url = "https://files.pythonhosted.org/packages/7d/75/1a19749d373e9a3d08861addccdf50c92b628c67074b22b8f3c61997cf5a/lxml-5.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b5f7d77334877c2146e7bb8b94e4df980325fab0a8af4d524e5d43cd6f789d", size = 5312300 }, { url = "https://files.pythonhosted.org/packages/fb/00/9d165d4060d3f347e63b219fcea5c6a3f9193e9e2868c6801e18e5379725/lxml-5.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f3495cfec24e3d63fffd342cc8141355d1d26ee766ad388775f5c8c5ec3932", size = 4836655 }, { url = "https://files.pythonhosted.org/packages/b8/e9/06720a33cc155966448a19677f079100517b6629a872382d22ebd25e48aa/lxml-5.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e70ad4c9658beeff99856926fd3ee5fde8b519b92c693f856007177c36eb2e30", size = 4961795 }, { url = "https://files.pythonhosted.org/packages/2d/57/4540efab2673de2904746b37ef7f74385329afd4643ed92abcc9ec6e00ca/lxml-5.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:507085365783abd7879fa0a6fa55eddf4bdd06591b17a2418403bb3aff8a267d", size = 4779791 }, { url = "https://files.pythonhosted.org/packages/99/ad/6056edf6c9f4fa1d41e6fbdae52c733a4a257fd0d7feccfa26ae051bb46f/lxml-5.3.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:5bb304f67cbf5dfa07edad904732782cbf693286b9cd85af27059c5779131050", size = 5346807 }, { url = "https://files.pythonhosted.org/packages/a1/fa/5be91fc91a18f3f705ea5533bc2210b25d738c6b615bf1c91e71a9b2f26b/lxml-5.3.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:3d84f5c093645c21c29a4e972b84cb7cf682f707f8706484a5a0c7ff13d7a988", size = 4909213 }, { url = "https://files.pythonhosted.org/packages/f3/74/71bb96a3b5ae36b74e0402f4fa319df5559a8538577f8c57c50f1b57dc15/lxml-5.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bdc13911db524bd63f37b0103af014b7161427ada41f1b0b3c9b5b5a9c1ca927", size = 4987694 }, { url = "https://files.pythonhosted.org/packages/08/c2/3953a68b0861b2f97234b1838769269478ccf872d8ea7a26e911238220ad/lxml-5.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ec944539543f66ebc060ae180d47e86aca0188bda9cbfadff47d86b0dc057dc", size = 4862865 }, { url = "https://files.pythonhosted.org/packages/e0/9a/52e48f7cfd5a5e61f44a77e679880580dfb4f077af52d6ed5dd97e3356fe/lxml-5.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:59d437cc8a7f838282df5a199cf26f97ef08f1c0fbec6e84bd6f5cc2b7913f6e", size = 5423383 }, { url = "https://files.pythonhosted.org/packages/17/67/42fe1d489e4dcc0b264bef361aef0b929fbb2b5378702471a3043bc6982c/lxml-5.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e275961adbd32e15672e14e0cc976a982075208224ce06d149c92cb43db5b93", size = 5286864 }, { url = "https://files.pythonhosted.org/packages/29/e4/03b1d040ee3aaf2bd4e1c2061de2eae1178fe9a460d3efc1ea7ef66f6011/lxml-5.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:038aeb6937aa404480c2966b7f26f1440a14005cb0702078c173c028eca72c31", size = 5056819 }, { url = "https://files.pythonhosted.org/packages/83/b3/e2ec8a6378e4d87da3af9de7c862bcea7ca624fc1a74b794180c82e30123/lxml-5.3.2-cp312-cp312-win32.whl", hash = "sha256:3c2c8d0fa3277147bff180e3590be67597e17d365ce94beb2efa3138a2131f71", size = 3486177 }, { url = "https://files.pythonhosted.org/packages/d5/8a/6a08254b0bab2da9573735725caab8302a2a1c9b3818533b41568ca489be/lxml-5.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:77809fcd97dfda3f399102db1794f7280737b69830cd5c961ac87b3c5c05662d", size = 3817134 }, { url = "https://files.pythonhosted.org/packages/19/fe/904fd1b0ba4f42ed5a144fcfff7b8913181892a6aa7aeb361ee783d441f8/lxml-5.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:77626571fb5270ceb36134765f25b665b896243529eefe840974269b083e090d", size = 8173598 }, { url = "https://files.pythonhosted.org/packages/97/e8/5e332877b3ce4e2840507b35d6dbe1cc33b17678ece945ba48d2962f8c06/lxml-5.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78a533375dc7aa16d0da44af3cf6e96035e484c8c6b2b2445541a5d4d3d289ee", size = 4441586 }, { url = "https://files.pythonhosted.org/packages/de/f4/8fe2e6d8721803182fbce2325712e98f22dbc478126070e62731ec6d54a0/lxml-5.3.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f62b2404b3f3f0744bbcabb0381c5fe186fa2a9a67ecca3603480f4846c585", size = 5038447 }, { url = "https://files.pythonhosted.org/packages/a6/ac/fa63f86a1a4b1ba8b03599ad9e2f5212fa813223ac60bfe1155390d1cc0c/lxml-5.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea918da00091194526d40c30c4996971f09dacab032607581f8d8872db34fbf", size = 4783583 }, { url = "https://files.pythonhosted.org/packages/1a/7a/08898541296a02c868d4acc11f31a5839d80f5b21d4a96f11d4c0fbed15e/lxml-5.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c35326f94702a7264aa0eea826a79547d3396a41ae87a70511b9f6e9667ad31c", size = 5305684 }, { url = "https://files.pythonhosted.org/packages/0b/be/9a6d80b467771b90be762b968985d3de09e0d5886092238da65dac9c1f75/lxml-5.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3bef90af21d31c4544bc917f51e04f94ae11b43156356aff243cdd84802cbf2", size = 4830797 }, { url = "https://files.pythonhosted.org/packages/8d/1c/493632959f83519802637f7db3be0113b6e8a4e501b31411fbf410735a75/lxml-5.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52fa7ba11a495b7cbce51573c73f638f1dcff7b3ee23697467dc063f75352a69", size = 4950302 }, { url = "https://files.pythonhosted.org/packages/c7/13/01aa3b92a6b93253b90c061c7527261b792f5ae7724b420cded733bfd5d6/lxml-5.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad131e2c4d2c3803e736bb69063382334e03648de2a6b8f56a878d700d4b557d", size = 4775247 }, { url = "https://files.pythonhosted.org/packages/60/4a/baeb09fbf5c84809e119c9cf8e2e94acec326a9b45563bf5ae45a234973b/lxml-5.3.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:00a4463ca409ceacd20490a893a7e08deec7870840eff33dc3093067b559ce3e", size = 5338824 }, { url = "https://files.pythonhosted.org/packages/69/c7/a05850f169ad783ed09740ac895e158b06d25fce4b13887a8ac92a84d61c/lxml-5.3.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:87e8d78205331cace2b73ac8249294c24ae3cba98220687b5b8ec5971a2267f1", size = 4899079 }, { url = "https://files.pythonhosted.org/packages/de/48/18ca583aba5235582db0e933ed1af6540226ee9ca16c2ee2d6f504fcc34a/lxml-5.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bf6389133bb255e530a4f2f553f41c4dd795b1fbb6f797aea1eff308f1e11606", size = 4978041 }, { url = "https://files.pythonhosted.org/packages/b6/55/6968ddc88554209d1dba0dca196360c629b3dfe083bc32a3370f9523a0c4/lxml-5.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3709fc752b42fb6b6ffa2ba0a5b9871646d97d011d8f08f4d5b3ee61c7f3b2b", size = 4859761 }, { url = "https://files.pythonhosted.org/packages/2e/52/d2d3baa1e0b7d04a729613160f1562f466fb1a0e45085a33acb0d6981a2b/lxml-5.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:abc795703d0de5d83943a4badd770fbe3d1ca16ee4ff3783d7caffc252f309ae", size = 5418209 }, { url = "https://files.pythonhosted.org/packages/d3/50/6005b297ba5f858a113d6e81ccdb3a558b95a615772e7412d1f1cbdf22d7/lxml-5.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98050830bb6510159f65d9ad1b8aca27f07c01bb3884ba95f17319ccedc4bcf9", size = 5274231 }, { url = "https://files.pythonhosted.org/packages/fb/33/6f40c09a5f7d7e7fcb85ef75072e53eba3fbadbf23e4991ca069ab2b1abb/lxml-5.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ba465a91acc419c5682f8b06bcc84a424a7aa5c91c220241c6fd31de2a72bc6", size = 5051899 }, { url = "https://files.pythonhosted.org/packages/8b/3a/673bc5c0d5fb6596ee2963dd016fdaefaed2c57ede82c7634c08cbda86c1/lxml-5.3.2-cp313-cp313-win32.whl", hash = "sha256:56a1d56d60ea1ec940f949d7a309e0bff05243f9bd337f585721605670abb1c1", size = 3485315 }, { url = "https://files.pythonhosted.org/packages/8c/be/cab8dd33b0dbe3af5b5d4d24137218f79ea75d540f74eb7d8581195639e0/lxml-5.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:1a580dc232c33d2ad87d02c8a3069d47abbcdce974b9c9cc82a79ff603065dbe", size = 3814639 }, ] [[package]] name = "mako" version = "1.3.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/4f/ddb1965901bc388958db9f0c991255b2c469349a741ae8c9cd8a562d70a6/mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac", size = 392195 } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/83/de0a49e7de540513f53ab5d2e105321dedeb08a8f5850f0208decf4390ec/Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", size = 78456 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] [[package]] name = "multidict" version = "6.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/4a/7874ca44a1c9b23796c767dd94159f6c17e31c0e7d090552a1c623247d82/multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8", size = 71066 } wheels = [ { url = "https://files.pythonhosted.org/packages/97/aa/879cf5581bd56c19f1bd2682ee4ecfd4085a404668d4ee5138b0a08eaf2a/multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46", size = 49125 }, { url = "https://files.pythonhosted.org/packages/9e/d8/e6d47c166c13c48be8efb9720afe0f5cdc4da4687547192cbc3c03903041/multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932", size = 29689 }, { url = "https://files.pythonhosted.org/packages/a4/20/f3f0a2ca142c81100b6d4cbf79505961b54181d66157615bba3955304442/multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf", size = 29975 }, { url = "https://files.pythonhosted.org/packages/ab/2d/1724972c7aeb7aa1916a3276cb32f9c39e186456ee7ed621504e7a758322/multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf", size = 135688 }, { url = "https://files.pythonhosted.org/packages/1a/08/ea54e7e245aaf0bb1c758578e5afba394ffccb8bd80d229a499b9b83f2b1/multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc", size = 142703 }, { url = "https://files.pythonhosted.org/packages/97/76/960dee0424f38c71eda54101ee1ca7bb47c5250ed02f7b3e8e50b1ce0603/multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1", size = 138559 }, { url = "https://files.pythonhosted.org/packages/d0/35/969fd792e2e72801d80307f0a14f5b19c066d4a51d34dded22c71401527d/multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081", size = 133312 }, { url = "https://files.pythonhosted.org/packages/a4/b8/f96657a2f744d577cfda5a7edf9da04a731b80d3239eafbfe7ca4d944695/multidict-6.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98", size = 125652 }, { url = "https://files.pythonhosted.org/packages/35/9d/97696d052297d8e2e08195a25c7aae873a6186c147b7635f979edbe3acde/multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633", size = 139015 }, { url = "https://files.pythonhosted.org/packages/31/a0/5c106e28d42f20288c10049bc6647364287ba049dc00d6ae4f1584eb1bd1/multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e", size = 132437 }, { url = "https://files.pythonhosted.org/packages/55/57/d5c60c075fef73422ae3b8f914221485b9ff15000b2db657c03bd190aee0/multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d", size = 144037 }, { url = "https://files.pythonhosted.org/packages/eb/56/a23f599c697a455bf65ecb0f69a5b052d6442c567d380ed423f816246824/multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4", size = 138535 }, { url = "https://files.pythonhosted.org/packages/34/3a/a06ff9b5899090f4bbdbf09e237964c76cecfe75d2aa921e801356314017/multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2", size = 136885 }, { url = "https://files.pythonhosted.org/packages/d6/28/489c0eca1df3800cb5d0a66278d5dd2a4deae747a41d1cf553e6a4c0a984/multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d", size = 27044 }, { url = "https://files.pythonhosted.org/packages/d0/b5/c7cd5ba9581add40bc743980f82426b90d9f42db0b56502011f1b3c929df/multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86", size = 29145 }, { url = "https://files.pythonhosted.org/packages/a4/e2/0153a8db878aef9b2397be81e62cbc3b32ca9b94e0f700b103027db9d506/multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b", size = 49204 }, { url = "https://files.pythonhosted.org/packages/bb/9d/5ccb3224a976d1286f360bb4e89e67b7cdfb87336257fc99be3c17f565d7/multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4", size = 29807 }, { url = "https://files.pythonhosted.org/packages/62/32/ef20037f51b84b074a89bab5af46d4565381c3f825fc7cbfc19c1ee156be/multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44", size = 30000 }, { url = "https://files.pythonhosted.org/packages/97/81/b0a7560bfc3ec72606232cd7e60159e09b9cf29e66014d770c1315868fa2/multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd", size = 131820 }, { url = "https://files.pythonhosted.org/packages/49/3b/768bfc0e41179fbccd3a22925329a11755b7fdd53bec66dbf6b8772f0bce/multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e", size = 136272 }, { url = "https://files.pythonhosted.org/packages/71/ac/fd2be3fe98ff54e7739448f771ba730d42036de0870737db9ae34bb8efe9/multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c", size = 135233 }, { url = "https://files.pythonhosted.org/packages/93/76/1657047da771315911a927b364a32dafce4135b79b64208ce4ac69525c56/multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87", size = 132861 }, { url = "https://files.pythonhosted.org/packages/19/a5/9f07ffb9bf68b8aaa406c2abee27ad87e8b62a60551587b8e59ee91aea84/multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29", size = 122166 }, { url = "https://files.pythonhosted.org/packages/95/23/b5ce3318d9d6c8f105c3679510f9d7202980545aad8eb4426313bd8da3ee/multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd", size = 136052 }, { url = "https://files.pythonhosted.org/packages/ce/5c/02cffec58ffe120873dce520af593415b91cc324be0345f534ad3637da4e/multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8", size = 130094 }, { url = "https://files.pythonhosted.org/packages/49/f3/3b19a83f4ebf53a3a2a0435f3e447aa227b242ba3fd96a92404b31fb3543/multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df", size = 140962 }, { url = "https://files.pythonhosted.org/packages/cc/1a/c916b54fb53168c24cb6a3a0795fd99d0a59a0ea93fa9f6edeff5565cb20/multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d", size = 138082 }, { url = "https://files.pythonhosted.org/packages/ef/1a/dcb7fb18f64b3727c61f432c1e1a0d52b3924016124e4bbc8a7d2e4fa57b/multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b", size = 136019 }, { url = "https://files.pythonhosted.org/packages/fb/02/7695485375106f5c542574f70e1968c391f86fa3efc9f1fd76aac0af7237/multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626", size = 26676 }, { url = "https://files.pythonhosted.org/packages/3c/f5/f147000fe1f4078160157b15b0790fff0513646b0f9b7404bf34007a9b44/multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c", size = 28899 }, { url = "https://files.pythonhosted.org/packages/a4/6c/5df5590b1f9a821154589df62ceae247537b01ab26b0aa85997c35ca3d9e/multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80", size = 49151 }, { url = "https://files.pythonhosted.org/packages/d5/ca/c917fbf1be989cd7ea9caa6f87e9c33844ba8d5fbb29cd515d4d2833b84c/multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16", size = 29803 }, { url = "https://files.pythonhosted.org/packages/22/19/d97086fc96f73acf36d4dbe65c2c4175911969df49c4e94ef082be59d94e/multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e", size = 29947 }, { url = "https://files.pythonhosted.org/packages/e3/3b/203476b6e915c3f51616d5f87230c556e2f24b168c14818a3d8dae242b1b/multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817", size = 130369 }, { url = "https://files.pythonhosted.org/packages/c6/4f/67470007cf03b2bb6df8ae6d716a8eeb0a7d19e0c8dba4e53fa338883bca/multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc", size = 135231 }, { url = "https://files.pythonhosted.org/packages/6d/f5/7a5ce64dc9a3fecc7d67d0b5cb9c262c67e0b660639e5742c13af63fd80f/multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1", size = 133634 }, { url = "https://files.pythonhosted.org/packages/05/93/ab2931907e318c0437a4cd156c9cfff317ffb33d99ebbfe2d64200a870f7/multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844", size = 131349 }, { url = "https://files.pythonhosted.org/packages/54/aa/ab8eda83a6a85f5b4bb0b1c28e62b18129b14519ef2e0d4cfd5f360da73c/multidict-6.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48", size = 120861 }, { url = "https://files.pythonhosted.org/packages/15/2f/7d08ea7c5d9f45786893b4848fad59ec8ea567367d4234691a721e4049a1/multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0", size = 134611 }, { url = "https://files.pythonhosted.org/packages/8b/07/387047bb1eac563981d397a7f85c75b306df1fff3c20b90da5a6cf6e487e/multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f", size = 128955 }, { url = "https://files.pythonhosted.org/packages/8d/6e/7ae18f764a5282c2d682f1c90c6b2a0f6490327730170139a7a63bf3bb20/multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de", size = 139759 }, { url = "https://files.pythonhosted.org/packages/b6/f4/c1b3b087b9379b9e56229bcf6570b9a963975c205a5811ac717284890598/multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02", size = 136426 }, { url = "https://files.pythonhosted.org/packages/a2/0e/ef7b39b161ffd40f9e25dd62e59644b2ccaa814c64e9573f9bc721578419/multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d", size = 134648 }, { url = "https://files.pythonhosted.org/packages/37/5c/7905acd0ca411c97bcae62ab167d9922f0c5a1d316b6d3af875d4bda3551/multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e", size = 26680 }, { url = "https://files.pythonhosted.org/packages/89/36/96b071d1dad6ac44fe517e4250329e753787bb7a63967ef44bb9b3a659f6/multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2", size = 28942 }, { url = "https://files.pythonhosted.org/packages/f5/05/d686cd2a12d648ecd434675ee8daa2901a80f477817e89ab3b160de5b398/multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7", size = 50807 }, { url = "https://files.pythonhosted.org/packages/4c/1f/c7db5aac8fea129fa4c5a119e3d279da48d769138ae9624d1234aa01a06f/multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b", size = 30474 }, { url = "https://files.pythonhosted.org/packages/e5/f1/1fb27514f4d73cea165429dcb7d90cdc4a45445865832caa0c50dd545420/multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e", size = 30841 }, { url = "https://files.pythonhosted.org/packages/d6/6b/9487169e549a23c8958edbb332afaf1ab55d61f0c03cb758ee07ff8f74fb/multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025", size = 148658 }, { url = "https://files.pythonhosted.org/packages/d7/22/79ebb2e4f70857c94999ce195db76886ae287b1b6102da73df24dcad4903/multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd", size = 151988 }, { url = "https://files.pythonhosted.org/packages/49/5d/63b17f3c1a2861587d26705923a94eb6b2600e5222d6b0d513bce5a78720/multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7", size = 148432 }, { url = "https://files.pythonhosted.org/packages/a3/22/55204eec45c4280fa431c11494ad64d6da0dc89af76282fc6467432360a0/multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af", size = 143161 }, { url = "https://files.pythonhosted.org/packages/97/e6/202b2cf5af161228767acab8bc49e73a91f4a7de088c9c71f3c02950a030/multidict-6.2.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331", size = 136820 }, { url = "https://files.pythonhosted.org/packages/7d/16/dbedae0e94c7edc48fddef0c39483f2313205d9bc566fd7f11777b168616/multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c", size = 150875 }, { url = "https://files.pythonhosted.org/packages/f3/04/38ccf25d4bf8beef76a22bad7d9833fd088b4594c9765fe6fede39aa6c89/multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b", size = 142050 }, { url = "https://files.pythonhosted.org/packages/9e/89/4f6b43386e7b79a4aad560d751981a0a282a1943c312ac72f940d7cf8f9f/multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151", size = 154117 }, { url = "https://files.pythonhosted.org/packages/24/e3/3dde5b193f86d30ad6400bd50e116b0df1da3f0c7d419661e3bd79e5ad86/multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019", size = 149408 }, { url = "https://files.pythonhosted.org/packages/df/b2/ec1e27e8e3da12fcc9053e1eae2f6b50faa8708064d83ea25aa7fb77ffd2/multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547", size = 145767 }, { url = "https://files.pythonhosted.org/packages/3a/8e/c07a648a9d592fa9f3a19d1c7e1c7738ba95aff90db967a5a09cff1e1f37/multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc", size = 28950 }, { url = "https://files.pythonhosted.org/packages/dc/a9/bebb5485b94d7c09831638a4df9a1a924c32431a750723f0bf39cd16a787/multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44", size = 32001 }, { url = "https://files.pythonhosted.org/packages/9c/fd/b247aec6add5601956d440488b7f23151d8343747e82c038af37b28d6098/multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530", size = 10266 }, ] [[package]] name = "mypy" version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } wheels = [ { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "pillow" version = "11.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } wheels = [ { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, ] [[package]] name = "platformdirs" version = "4.3.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pre-commit" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] [[package]] name = "propcache" version = "0.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243 }, { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503 }, { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934 }, { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633 }, { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124 }, { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283 }, { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498 }, { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486 }, { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675 }, { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727 }, { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878 }, { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558 }, { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754 }, { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088 }, { url = "https://files.pythonhosted.org/packages/54/6e/30a11f4417d9266b5a464ac5a8c5164ddc9dd153dfa77bf57918165eb4ae/propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", size = 40859 }, { url = "https://files.pythonhosted.org/packages/1d/3a/8a68dd867da9ca2ee9dfd361093e9cb08cb0f37e5ddb2276f1b5177d7731/propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", size = 45153 }, { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865 }, { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452 }, { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800 }, { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804 }, { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650 }, { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235 }, { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249 }, { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964 }, { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501 }, { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917 }, { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089 }, { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102 }, { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122 }, { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818 }, { url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112 }, { url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034 }, { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613 }, { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763 }, { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175 }, { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265 }, { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412 }, { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290 }, { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926 }, { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808 }, { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916 }, { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661 }, { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384 }, { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420 }, { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880 }, { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407 }, { url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573 }, { url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757 }, { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, ] [[package]] name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[package]] name = "pycares" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/ac/05e32fe0abecf98b58cb28646d59280ebad8d79b9785ad6771fa830a458b/pycares-4.6.0.tar.gz", hash = "sha256:b8a004b18a7465ac9400216bc3fad9d9966007af1ee32f4412d2b3a94e33456e", size = 822080 } wheels = [ { url = "https://files.pythonhosted.org/packages/58/75/8efeb3466629ca8fc6fc4b95a1a5c2d22df040161057de3d4abdbd8741a2/pycares-4.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44973963fa4a3900b2b6c155b2b301456a2b514e5afd93139cda3cfca27e71d0", size = 75589 }, { url = "https://files.pythonhosted.org/packages/24/08/5658710a195401b095bcd10a734edf125919574d320dfb14542a54c3ca37/pycares-4.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:889e671a7d1a164585597186b0bf445589f4766f96e3b465da64d0239ba20863", size = 72079 }, { url = "https://files.pythonhosted.org/packages/a3/58/79e0b1133757bef9eacdc4e6bb4603fb1e79fc18b816c1087d02d92269fd/pycares-4.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19701de492309b1dcfba44897987fd98571400f964ca5b64377b552f6771ae12", size = 289884 }, { url = "https://files.pythonhosted.org/packages/ec/f6/d8caf4e6c4920db71079964f68c2d520f7e318392cd411737983f416c522/pycares-4.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b873b6547de2d8ced4ed17fda6af3a1666d6cdedaf9c47092351aeeac3d1b207", size = 304540 }, { url = "https://files.pythonhosted.org/packages/fe/4b/1af8217ae68f0a76f07fb55de1269b081dda48f09b4ad39575e6a101223d/pycares-4.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84bb8599fa9e983ad863debdb86fc7d2af44bc910e0e68c3d26b3a346e5ee175", size = 293981 }, { url = "https://files.pythonhosted.org/packages/e6/62/155535b4899753a3e7ad6afe2c3a3ac34a0422fea7205fb25cc4ef99617c/pycares-4.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2143f0387df6b0ebf8d5a0b6a10f80dacf4f8ef74babc2e683b9f6ed7b95d4f", size = 289320 }, { url = "https://files.pythonhosted.org/packages/a4/66/c8e16d5fe4dce3613d30f4dca3735ceaf795ce6174d70f62166f86f9c055/pycares-4.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d77e15b41f3ae8ab2196adc5eb0ce99c3bc3a73cc169cee0140b874aeb57bd0", size = 270622 }, { url = "https://files.pythonhosted.org/packages/9e/86/b9045875839a7b696ca997b90085aa5cb98a7091740c31fef171668abfe2/pycares-4.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8513edc4392746bb34d963b75df9df6a337ed987476fc0a019546cc4b46283e5", size = 278605 }, { url = "https://files.pythonhosted.org/packages/bb/e5/415c6c462ccf382dc8bd80bbd2206984208675be6a060555afa5f20861b5/pycares-4.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d30ba8ffb8f8ded2704a9bef79aba738050426691c369d38c1f36d53d6528890", size = 264442 }, { url = "https://files.pythonhosted.org/packages/68/e2/c8fd6291e4c828f46891c90016de0366468094d3fb2bdfd882ea9e43d9de/pycares-4.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:063275e0f59a8ddc19b56a6a28b6e50290f6f209d7bfa9debeab3ee59c098212", size = 298226 }, { url = "https://files.pythonhosted.org/packages/ca/60/2e303ac0733997ed6a380ac5d338d15ebacce54759c18b713eb4106b94ed/pycares-4.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8ceebe4a96c1428d197008bbc3e6ce10a279e70084aed165e37ca4cc4da581c5", size = 291274 }, { url = "https://files.pythonhosted.org/packages/05/a9/8f499198dac6cdb4709b22b2c1da2f9b293e032b12a4b54be112c284275b/pycares-4.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb2107570ffd414184705525a638bd310a6504816221672f3dec5049be634eaf", size = 280726 }, { url = "https://files.pythonhosted.org/packages/fd/e7/6734bbb833a81fc16bbcddb1e2ba4b50f26559025a60256949dfa2853073/pycares-4.6.0-cp311-cp311-win32.whl", hash = "sha256:982e31e560d3c03b0c836e3f8a3041a68b15436a4849451d4f685149130fac57", size = 62239 }, { url = "https://files.pythonhosted.org/packages/db/18/d631b258a1b0f42e44a14c15af388fee42fcbbfb8d98c670e244c829892a/pycares-4.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:36d2f38411a9863e4332b2583b12db4cf669fbdf9e74fa5114e1f235c73d5fae", size = 77438 }, { url = "https://files.pythonhosted.org/packages/bf/25/0da39434c0cecbd309628b8a3bb88b26ec2391d84cf282034bfcd65b0cfa/pycares-4.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21c3254ccbeb0111f0d3e570e55ae1bdb7bc7e4a547b664b33aeff701c794cf6", size = 75676 }, { url = "https://files.pythonhosted.org/packages/fe/3f/3335bc34b46c4ad0de3d0a363d6e809e985bab09d24ba5464e8d034914b5/pycares-4.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86f443dacbd31b9bd0b4b2124c9268fa6697856b44b5b4a4c7bab9f81615fe5c", size = 72089 }, { url = "https://files.pythonhosted.org/packages/13/9b/0b37f124b621921bfb13558989d22aaba6723621d49dc12f93b755ae76ce/pycares-4.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3813815990345deaaee9e8db5e42f0c9cd2f46154ff5cb43d765f1c4891e1cee", size = 291100 }, { url = "https://files.pythonhosted.org/packages/80/4f/4308a2765e102ddac0413c74a9446c0794685c7b64f5deed770b8a5351be/pycares-4.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c49887eaf68c33beb88f5c3454efa9165753df0b27d3cac9ae0f8fa4cc377be4", size = 305062 }, { url = "https://files.pythonhosted.org/packages/34/54/f73c6940905269b2967b8c0c637649f16089dfc728b2370bc412e1e39d90/pycares-4.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee60a544e8d6ff2a3d5e1518e5c9597d528848413cc330aa46cf3fc62f48d9e0", size = 295043 }, { url = "https://files.pythonhosted.org/packages/5c/7b/dfddb506936ed27ab0dac6f53f5882e2094d5fc4ca1e41c1ed5ae738c316/pycares-4.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44224d486b5b03804a2b575f22feae3df22cd07fde2be0ac055c752c17133542", size = 290642 }, { url = "https://files.pythonhosted.org/packages/9d/05/76ab86b13684f5e752634030511270264d5f8dc99e23a8bf24af6dc66fb6/pycares-4.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59989759114d68ec4a16d2c2b020ec702b5076e351e9d40f7a4629945f9d56f7", size = 271377 }, { url = "https://files.pythonhosted.org/packages/09/21/d032d9c9aa1c124a3443fa42e0b0311555d4ebd33c4b5cb1c16e180ca9f5/pycares-4.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e2e65edf4a2c84eb61340b7698573dd36f58182641db3d61f44abe142ba39b83", size = 279317 }, { url = "https://files.pythonhosted.org/packages/18/84/927f45d2b3c8a7cf52c1d94c1d418950f04261443eedea30e997ebe782d5/pycares-4.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b0e02b71765f0ab1a8dd97a126cf4e674f2e98ef9e5108c00ff577bed29c439c", size = 264481 }, { url = "https://files.pythonhosted.org/packages/f3/a7/5c54f36ba75464e7789d973f7467fbb9d1fe7b3b778c35b85ba886e167ed/pycares-4.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7ce93fcde4a35dfc53b32333bee7b2a131bac9a605263e76342cbb37eff3af04", size = 298494 }, { url = "https://files.pythonhosted.org/packages/af/36/a30565c579e92d0bce14afad31ed202374fd824e89be87bc40e1cbe86f7e/pycares-4.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:178287b3a3761024cfd492e1c4293c1ed886cd84ac128278646b317dba082613", size = 292394 }, { url = "https://files.pythonhosted.org/packages/33/f5/803674db3ef0f7f0340b4f7faf49ee3b151cf2b3a53ba3605326ea351d0d/pycares-4.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bae8cda2af0c0f3325bb035fe7484e1d88d21698ae7043ad920d5c330a4f694e", size = 281916 }, { url = "https://files.pythonhosted.org/packages/af/8b/5b0cbb202e72a75ce43efa2c721f479d163b6eacee7907a3602ada68f0a1/pycares-4.6.0-cp312-cp312-win32.whl", hash = "sha256:efc8bc00eb1d0a84aba1d2edf3f9f96bf3b7b9f076d6bce1394ff895b7618eb7", size = 62248 }, { url = "https://files.pythonhosted.org/packages/a9/6c/5a680fe0402f1420c8c4ec235a44ae5fb3d2fc5642eca69f39d0f44abbea/pycares-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:6430b996761fe61bf86577183d6d342bc619e75e5cd7251b5827f2c56aae5aca", size = 77474 }, { url = "https://files.pythonhosted.org/packages/7a/ea/2cee3369713fafc04f4ac56b13f709879e652a55528c23a88a65eba3abaf/pycares-4.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8687028455f243dc3fe047e2e4b4b98d77811ce0e1e83d1929baf4a2f60bcd0b", size = 75674 }, { url = "https://files.pythonhosted.org/packages/59/21/bac9ebdfe03d3b07456fd44042ae2cb95fd552f6f1050618077957bb6303/pycares-4.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02587f22cd7540dbf608dcc942d3c6eee4246bbb76c9cc8449d307188ad26441", size = 72095 }, { url = "https://files.pythonhosted.org/packages/3f/46/fa7e5d053040c4e8e214864a0b2709eb6ad903b332eb107af34103c8a716/pycares-4.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f07d1558503ac72475f9bb2ce6116c18fc868ab083b8e67c6433ca0c7b09d3", size = 291073 }, { url = "https://files.pythonhosted.org/packages/2e/22/aae38c9e676904138535bde04d8315361aaad6afd4da66ec273a38e4aab9/pycares-4.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d39bb28bebb98a4be0438f06adfed5f4c12b115af531b48cd27692b8704eec02", size = 304994 }, { url = "https://files.pythonhosted.org/packages/5c/96/18330188d540be9307f9cdce5ba6336de8590792496af0d2d9e0b23aa870/pycares-4.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e63b5c955b0ade059e147334efc0aafe76ba513f4cf6dd802af9c2450dc4f6ad", size = 295017 }, { url = "https://files.pythonhosted.org/packages/33/0c/e7cc479a7fb6e849a500985944a5a1a3d8b4fd05b9d4aa98c2e01048e2c2/pycares-4.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae9c72108784d725d2fa38eedbf702a6511cb67e24e27714b35c930e4d3bee6", size = 290624 }, { url = "https://files.pythonhosted.org/packages/34/c6/803bca58a4a381f60f1654c5fcb06df83789a03f546a6fc883d640eafec4/pycares-4.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30ca7df82180edffd0562ebc22228f8dd348f91810d37ac0389ebc73e4a7d723", size = 271348 }, { url = "https://files.pythonhosted.org/packages/27/a1/c45fce9580658021b96a788cd6e054d24af834fe910ec7067f49d1f713ac/pycares-4.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:74dd022766d987eb6ea64ef5db45821849508b1818bdbd5ef8d0ad198ab25d02", size = 279333 }, { url = "https://files.pythonhosted.org/packages/ff/14/fac01d5a2d8401357c1c38fa5053f5d808a79cf853e3eb104bc4af17df2b/pycares-4.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9716e6c1041569fd391aeb71a75b18038cb5e796a93408a88c27cc48dd076035", size = 264468 }, { url = "https://files.pythonhosted.org/packages/03/6b/ff6cc8b671f7742f9054bf1d50ba3188b1ae8b1a188610dbc9515eb11ca3/pycares-4.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a1972b3a1473d473e52038f8153b0a9b49860b7dffd183d8bef2fe481e850ce5", size = 298489 }, { url = "https://files.pythonhosted.org/packages/cd/a6/20d1d368621e8327cc2b178982b1a1990608bb0cb65ec7b822faeee41475/pycares-4.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c2b69622fa6fafbda109f8a79bb92d785d38546c81dfd94207fb0ead4d237a36", size = 292391 }, { url = "https://files.pythonhosted.org/packages/df/29/5209f3ec6bc20d0c6f81c9b444027ffc346f2b603d5b764abc2bc1153645/pycares-4.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63012f6194be62a9d6a62bcb57fa3df90bfb58b81f613f548df0289ce6b53067", size = 281924 }, { url = "https://files.pythonhosted.org/packages/63/f0/c3c0a927d26235929d8ecfe2b4c95a5b701c19d61c7bfd41121765110284/pycares-4.6.0-cp313-cp313-win32.whl", hash = "sha256:e8689563fb2148feecdf4e03b8845f79dc3115df0fefad99ece718ff0f319004", size = 62249 }, { url = "https://files.pythonhosted.org/packages/9b/3f/6b976c4fdb43249f72e071b2dc132a829a5aac616a7cbdc56860670c1de3/pycares-4.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e82fe5e269359fbecdff3ed026f491485e92012f8c8a7386d3aa1aa830f1936", size = 77473 }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "pytest-asyncio" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } wheels = [ { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, ] [[package]] name = "python-magic" version = "0.4.27" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] [[package]] name = "qrcode" version = "8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/d4/d222d00f65c81945b55e8f64011c33cb11a2931957ba3e2845fb0874fffe/qrcode-8.1.tar.gz", hash = "sha256:e8df73caf72c3bace3e93d9fa0af5aa78267d4f3f5bc7ab1b208f271605a5e48", size = 41549 } wheels = [ { url = "https://files.pythonhosted.org/packages/29/e6/273de1f5cda537b00bc2947082be747f1d76358db8b945f3a60837bcd0f6/qrcode-8.1-py3-none-any.whl", hash = "sha256:9beba317d793ab8b3838c52af72e603b8ad2599c4e9bbd5c3da37c7dcc13c5cf", size = 45711 }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] name = "roman-numerals-py" version = "3.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, ] [[package]] name = "ruff" version = "0.11.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } wheels = [ { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, ] [[package]] name = "setuptools" version = "78.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } wheels = [ { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, ] [[package]] name = "slidge" source = { editable = "." } dependencies = [ { name = "aiohttp", extra = ["speedups"] }, { name = "alembic" }, { name = "configargparse" }, { name = "defusedxml" }, { name = "pillow" }, { name = "python-magic" }, { name = "qrcode" }, { name = "slixmpp" }, { name = "sqlalchemy" }, { name = "thumbhash" }, ] [package.dev-dependencies] dev = [ { name = "coverage" }, { name = "emoji" }, { name = "furo" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pygments" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinx-argparse" }, { name = "sphinx-autoapi" }, { name = "types-pillow" }, { name = "utidylib" }, { name = "xmldiff" }, ] [package.metadata] requires-dist = [ { name = "aiohttp", extras = ["speedups"], specifier = ">=3.11.11,<4" }, { name = "alembic", specifier = ">=1.14.0,<2" }, { name = "configargparse", specifier = ">=1.7,<2" }, { name = "defusedxml", specifier = ">=0.7.1" }, { name = "pillow", specifier = ">=11.0.0,<12" }, { name = "python-magic", specifier = ">=0.4.27,<0.5" }, { name = "qrcode", specifier = ">=8.0,<9" }, { name = "slixmpp", specifier = ">=1.10.0,<2" }, { name = "sqlalchemy", specifier = ">=2,<3" }, { name = "thumbhash", specifier = ">=0.1.2" }, ] [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = ">=7.6.10" }, { name = "emoji", specifier = ">=2.14.0" }, { name = "furo", specifier = ">=2024.8.6" }, { name = "mypy", specifier = ">=1.14.1" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pygments", specifier = ">=2.18.0" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "ruff", specifier = ">=0.8.4" }, { name = "sphinx", specifier = ">=8.1.3" }, { name = "sphinx-argparse", specifier = ">=0.5.2" }, { name = "sphinx-autoapi", specifier = ">=3.4.0" }, { name = "types-pillow", specifier = ">=10.2.0.20240822" }, { name = "utidylib", specifier = ">=0.10" }, { name = "xmldiff", specifier = ">=2.7.0" }, ] [[package]] name = "slixmpp" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiodns" }, { name = "pyasn1" }, { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/79/0ceacbcdc5f84248a36c98e11c126494c4c262902bba69eb9bd536771f76/slixmpp-1.10.0.tar.gz", hash = "sha256:46bc5d01507cb4285c8255ce5c717c0b5f68e54dfc1f1712883998d6d722578a", size = 705681 } wheels = [ { url = "https://files.pythonhosted.org/packages/f8/a2/f2d170330477b50a71ed8720c048a2de775ebd99f5afd072695e4db42279/slixmpp-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a46a2dee7c4531f176b648e70c624b3c5e07401c7b19b3bb3dc5fb7a21bce2", size = 925509 }, { url = "https://files.pythonhosted.org/packages/06/31/fe90303e8facd62eb9cc5c4656a2c4ad1fc6227075465d17a8f670c60821/slixmpp-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9e5c4a48637588f5e9e9bd2cc6d28107a5262211968c46d159ec6f6b71c39e", size = 929715 }, { url = "https://files.pythonhosted.org/packages/29/24/93f36273dce1beaebfb19bcef11b9423e9164951c5a9e3fac7df72d7cecc/slixmpp-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e11e308bb9bdb28f6753184d29c5cf03d7056e32bebebe6aff8d25532f15f8e", size = 925576 }, { url = "https://files.pythonhosted.org/packages/e6/bb/b4ed1601c1a51efb439e38767d1e71bdff6d234d5a9dcdfd57361c8f8d4d/slixmpp-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38c8babf6771f8ac03380efa566e09469b4d967fb38934ea0649e1481ae5af23", size = 929685 }, { url = "https://files.pythonhosted.org/packages/73/f4/1eda543f5520e98a63df2a56e2600e652ba437b5e6cd16b0c4b62ec52413/slixmpp-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6187313f255fd36a2a23e2f9fcbef4afeb9dacbac11ab666557d189d0913158b", size = 925269 }, { url = "https://files.pythonhosted.org/packages/79/ed/35fd9ca70c8e8545a7a06fd45571cc6ef41af5080506e57296b24ec00787/slixmpp-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb7fd7239df14636c01bb22034f78349d1a408080d4a1398862ef20ef6c9986", size = 929193 }, ] [[package]] name = "snowballstemmer" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, ] [[package]] name = "soupsieve" version = "2.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, ] [[package]] name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "roman-numerals-py" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } wheels = [ { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741 }, ] [[package]] name = "sphinx-argparse" version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/21/a8c64e6633652111e6e4f89703182a53cbc3ed67233523e47472101358b6/sphinx_argparse-0.5.2.tar.gz", hash = "sha256:e5352f8fa894b6fb6fda0498ba28a9f8d435971ef4bbc1a6c9c6414e7644f032", size = 27838 } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/43/9f0e9bfb3ce02cbf7747aa2185c48a9d6e42ba95736a5e8f511a5054d976/sphinx_argparse-0.5.2-py3-none-any.whl", hash = "sha256:d771b906c36d26dee669dbdbb5605c558d9440247a5608b810f7fa6e26ab1fd3", size = 12547 }, ] [[package]] name = "sphinx-autoapi" version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, { name = "jinja2" }, { name = "pyyaml" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7f/a8/22b379a2a75ccb881217d3d4ae56d7d35f2d1bb4c8c0c51d0253676746a1/sphinx_autoapi-3.6.0.tar.gz", hash = "sha256:c685f274e41d0842ae7e199460c322c4bd7fec816ccc2da8d806094b4f64af06", size = 55417 } wheels = [ { url = "https://files.pythonhosted.org/packages/58/17/0eda9dc80fcaf257222b506844207e71b5d59567c41bbdcca2a72da119b9/sphinx_autoapi-3.6.0-py3-none-any.whl", hash = "sha256:f3b66714493cab140b0e896d33ce7137654a16ac1edb6563edcbd47bf975f711", size = 35281 }, ] [[package]] name = "sphinx-basic-ng" version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, ] [[package]] name = "sqlalchemy" version = "2.0.40" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 } wheels = [ { url = "https://files.pythonhosted.org/packages/77/7e/55044a9ec48c3249bb38d5faae93f09579c35e862bb318ebd1ed7a1994a5/sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e", size = 2114025 }, { url = "https://files.pythonhosted.org/packages/77/0f/dcf7bba95f847aec72f638750747b12d37914f71c8cc7c133cf326ab945c/sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011", size = 2104419 }, { url = "https://files.pythonhosted.org/packages/75/70/c86a5c20715e4fe903dde4c2fd44fc7e7a0d5fb52c1b954d98526f65a3ea/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4", size = 3222720 }, { url = "https://files.pythonhosted.org/packages/12/cf/b891a8c1d0c27ce9163361664c2128c7a57de3f35000ea5202eb3a2917b7/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1", size = 3222682 }, { url = "https://files.pythonhosted.org/packages/15/3f/7709d8c8266953d945435a96b7f425ae4172a336963756b58e996fbef7f3/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51", size = 3159542 }, { url = "https://files.pythonhosted.org/packages/85/7e/717eaabaf0f80a0132dc2032ea8f745b7a0914451c984821a7c8737fb75a/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a", size = 3179864 }, { url = "https://files.pythonhosted.org/packages/e4/cc/03eb5dfcdb575cbecd2bd82487b9848f250a4b6ecfb4707e834b4ce4ec07/sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b", size = 2084675 }, { url = "https://files.pythonhosted.org/packages/9a/48/440946bf9dc4dc231f4f31ef0d316f7135bf41d4b86aaba0c0655150d370/sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4", size = 2110099 }, { url = "https://files.pythonhosted.org/packages/92/06/552c1f92e880b57d8b92ce6619bd569b25cead492389b1d84904b55989d8/sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d", size = 2112620 }, { url = "https://files.pythonhosted.org/packages/01/72/a5bc6e76c34cebc071f758161dbe1453de8815ae6e662393910d3be6d70d/sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a", size = 2103004 }, { url = "https://files.pythonhosted.org/packages/bf/fd/0e96c8e6767618ed1a06e4d7a167fe13734c2f8113c4cb704443e6783038/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d", size = 3252440 }, { url = "https://files.pythonhosted.org/packages/cd/6a/eb82e45b15a64266a2917a6833b51a334ea3c1991728fd905bfccbf5cf63/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716", size = 3263277 }, { url = "https://files.pythonhosted.org/packages/45/97/ebe41ab4530f50af99e3995ebd4e0204bf1b0dc0930f32250dde19c389fe/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2", size = 3198591 }, { url = "https://files.pythonhosted.org/packages/e6/1c/a569c1b2b2f5ac20ba6846a1321a2bf52e9a4061001f282bf1c5528dcd69/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191", size = 3225199 }, { url = "https://files.pythonhosted.org/packages/8f/91/87cc71a6b10065ca0209d19a4bb575378abda6085e72fa0b61ffb2201b84/sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1", size = 2082959 }, { url = "https://files.pythonhosted.org/packages/2a/9f/14c511cda174aa1ad9b0e42b64ff5a71db35d08b0d80dc044dae958921e5/sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0", size = 2108526 }, { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887 }, { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367 }, { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806 }, { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131 }, { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364 }, { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482 }, { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704 }, { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564 }, { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, ] [[package]] name = "thumbhash" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d1/17/fc026b10f448e25413e4cc692c9ca00cc8ef0e4022a1dd8d7b90761c9be9/thumbhash-0.1.2.tar.gz", hash = "sha256:ef4e6398f93f3b5ad480dc8e3a3b3a200813c86a8857fb069f8478be809f8247", size = 13766 } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/64/0b5d39612708e892ca67934a0d98b2a2b06aabe8261be800b5db54f9702e/thumbhash-0.1.2-py3-none-any.whl", hash = "sha256:5dbddde2349c35c65d70af11637e9c89a97a2b5a134f3e14bcf2a5dd82522a22", size = 5112 }, ] [[package]] name = "types-pillow" version = "10.2.0.20240822" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/4a/4495264dddaa600d65d68bcedb64dcccf9d9da61adff51f7d2ffd8e4c9ce/types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3", size = 35389 } wheels = [ { url = "https://files.pythonhosted.org/packages/66/23/e81a5354859831fcf54d488d33b80ba6133ea84f874a9c0ec40a4881e133/types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d", size = 54354 }, ] [[package]] name = "typing-extensions" version = "4.13.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } wheels = [ { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] [[package]] name = "utidylib" version = "0.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/60/3b/7e5d9f84fc7abc031290b8c277ee55a7009f9f3f3c8e15ab8be76a01c266/uTidylib-0.10.tar.gz", hash = "sha256:95fe538c6cd1ea08d7df99d56a5816cb0571a06038189c03518024064edee60f", size = 16959 } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e3/54156cd94f0c8b80c3a6432fdb49b0ff73458c6f815080d17c187b567cd7/uTidylib-0.10-py3-none-any.whl", hash = "sha256:672cd4d5ec28d5e3e4496c0e104569e4342ff32f9fd28b74871174fae66eee7c", size = 10101 }, ] [[package]] name = "virtualenv" version = "20.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, ] [[package]] name = "xmldiff" version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml" }, { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/d8/b31a4d75bc72d5c3a5adf5012405118169b56d163ebf407e4fc49a575e96/xmldiff-2.7.0.tar.gz", hash = "sha256:c0910b1f800366dd7ec62923e5d06e8b06a1bd9120569a1c27f4f2446b9c68a2", size = 87596 } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/23/4c026139010faa8a48636fac190e9f8f830f850edca8e85f74123a58ed92/xmldiff-2.7.0-py3-none-any.whl", hash = "sha256:c8020e6aa4aa9fa13c72e5bf0eeafd0be998b0ab55d78b008abc75fbfebaca27", size = 43994 }, ] [[package]] name = "yarl" version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/4d/8a8f57caccce49573e567744926f88c6ab3ca0b47a257806d1cf88584c5f/yarl-1.19.0.tar.gz", hash = "sha256:01e02bb80ae0dbed44273c304095295106e1d9470460e773268a27d11e594892", size = 184396 } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/df/5fa7cd75e46306e0f9baf38a7c8969ff6730ea503b86232e85cb740304cf/yarl-1.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:163ff326680de5f6d4966954cf9e3fe1bf980f5fee2255e46e89b8cf0f3418b5", size = 145126 }, { url = "https://files.pythonhosted.org/packages/2a/be/c1b52129cd2166ab7337f08e701a61baa7c260c7b03b534098cc8297aecc/yarl-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a626c4d9cca298d1be8625cff4b17004a9066330ac82d132bbda64a4c17c18d3", size = 96691 }, { url = "https://files.pythonhosted.org/packages/8d/39/ad62139b45515f9bf129c805aeaaedf86fd93ae57ffe911f4caeabef3e74/yarl-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:961c3e401ea7f13d02b8bb7cb0c709152a632a6e14cdc8119e9c6ee5596cd45d", size = 94505 }, { url = "https://files.pythonhosted.org/packages/be/be/04e3202cdc9bb5f81761e327af7095cffb0d81e32421a6b87f926052d2ae/yarl-1.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a39d7b807ab58e633ed760f80195cbd145b58ba265436af35f9080f1810dfe64", size = 355485 }, { url = "https://files.pythonhosted.org/packages/00/7d/1463203663ca1ae62af8fb9ebc9601dd07f04dbced7edb1df3141a2cb2fe/yarl-1.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4228978fb59c6b10f60124ba8e311c26151e176df364e996f3f8ff8b93971b5", size = 344569 }, { url = "https://files.pythonhosted.org/packages/b0/1b/5263203017348669e637bb73856fb9632110538e92d5e9f8214fcc764da9/yarl-1.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba536b17ecf3c74a94239ec1137a3ad3caea8c0e4deb8c8d2ffe847d870a8c5", size = 371426 }, { url = "https://files.pythonhosted.org/packages/78/59/90ca5f16d56b7741e5383951acc2e065fce41920eb5d8fda3065b5e288dc/yarl-1.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a251e00e445d2e9df7b827c9843c0b87f58a3254aaa3f162fb610747491fe00f", size = 368102 }, { url = "https://files.pythonhosted.org/packages/84/f2/5e33aa0251ffd2c2a9041bf887e163eeefdc1dca238fdabac444d9463c3f/yarl-1.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9b92431d8b4d4ca5ccbfdbac95b05a3a6cd70cd73aa62f32f9627acfde7549c", size = 358740 }, { url = "https://files.pythonhosted.org/packages/22/9e/ba92d234c81cf94495fc01eaa0b6000175733f76bd63e60ff748bce22c81/yarl-1.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec2f56edaf476f70b5831bbd59700b53d9dd011b1f77cd4846b5ab5c5eafdb3f", size = 346965 }, { url = "https://files.pythonhosted.org/packages/8d/0b/d4f53136ef12ddad540855a886d7503a6cc17cfabb9a03ce0c179f3b9e51/yarl-1.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acf9b92c4245ac8b59bc7ec66a38d3dcb8d1f97fac934672529562bb824ecadb", size = 368547 }, { url = "https://files.pythonhosted.org/packages/31/4b/35ec8622908a728f378a8511f0ab2d47878b2c0b8cbe035f2d907914a5fc/yarl-1.19.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:57711f1465c06fee8825b95c0b83e82991e6d9425f9a042c3c19070a70ac92bf", size = 357610 }, { url = "https://files.pythonhosted.org/packages/c1/71/1f39f7c55b0684834d945a2bcfdfe59e6e02ca2483a3d33c2f77a0c3b177/yarl-1.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:528e86f5b1de0ad8dd758ddef4e0ed24f5d946d4a1cef80ffb2d4fca4e10f122", size = 365331 }, { url = "https://files.pythonhosted.org/packages/2e/13/57675964de5c8ccf6427df93ac97f9bb7328f3f8f7ebc31a5f5a286ab1c0/yarl-1.19.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b77173663e075d9e5a57e09d711e9da2f3266be729ecca0b8ae78190990d260", size = 378624 }, { url = "https://files.pythonhosted.org/packages/d4/c6/5868e40f8da041ed0c3b5fd8c08cece849d9f609e970e6043308767fbb60/yarl-1.19.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d8717924cf0a825b62b1a96fc7d28aab7f55a81bf5338b8ef41d7a76ab9223e9", size = 383981 }, { url = "https://files.pythonhosted.org/packages/f4/3f/e40124c986d96741d3d341ffac35be42b6df82ef8c18b5984ca2e7d838dd/yarl-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0df9f0221a78d858793f40cbea3915c29f969c11366646a92ca47e080a14f881", size = 378868 }, { url = "https://files.pythonhosted.org/packages/01/eb/caf2774c770288bd87a818b11f3a56ada6a855f1987d93421aae01a175bf/yarl-1.19.0-cp311-cp311-win32.whl", hash = "sha256:8b3ade62678ee2c7c10dcd6be19045135e9badad53108f7d2ed14896ee396045", size = 86446 }, { url = "https://files.pythonhosted.org/packages/4a/97/d4fe6168c1bb789507ffeb58c2e8c675a7e71de732dc02e12bda904c1362/yarl-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:0626ee31edb23ac36bdffe607231de2cca055ad3a5e2dc5da587ef8bc6a321bc", size = 93121 }, { url = "https://files.pythonhosted.org/packages/b8/70/44ef8f69d61cb5123167a4dda87f6c739a833fbdb2ed52960b4e8409d65c/yarl-1.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b687c334da3ff8eab848c9620c47a253d005e78335e9ce0d6868ed7e8fd170b", size = 146855 }, { url = "https://files.pythonhosted.org/packages/c3/94/38c14d6c8217cc818647689f2dd647b976ced8fea08d0ac84e3c8168252b/yarl-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b0fe766febcf523a2930b819c87bb92407ae1368662c1bc267234e79b20ff894", size = 97523 }, { url = "https://files.pythonhosted.org/packages/35/a5/43a613586a6255105c4655a911c307ef3420e49e540d6ae2c5829863fb25/yarl-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:742ceffd3c7beeb2b20d47cdb92c513eef83c9ef88c46829f88d5b06be6734ee", size = 95540 }, { url = "https://files.pythonhosted.org/packages/d4/60/ed26049f4a8b06ebfa6d5f3cb6a51b152fd57081aa818b6497474f65a631/yarl-1.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af682a1e97437382ee0791eacbf540318bd487a942e068e7e0a6c571fadbbd3", size = 344386 }, { url = "https://files.pythonhosted.org/packages/49/a6/b84899cab411f49af5986cfb44b514040788d81c8084f5811e6a7c0f1ce6/yarl-1.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63702f1a098d0eaaea755e9c9d63172be1acb9e2d4aeb28b187092bcc9ca2d17", size = 338889 }, { url = "https://files.pythonhosted.org/packages/cc/ce/0704f7166a781b1f81bdd45c4f49eadbae0230ebd35b9ec7cd7769d3a6ff/yarl-1.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3560dcba3c71ae7382975dc1e912ee76e50b4cd7c34b454ed620d55464f11876", size = 353107 }, { url = "https://files.pythonhosted.org/packages/75/e5/0ecd6f2a9cc4264c16d8dfb0d3d71ba8d03cb58f3bcd42b1df4358331189/yarl-1.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68972df6a0cc47c8abaf77525a76ee5c5f6ea9bbdb79b9565b3234ded3c5e675", size = 353128 }, { url = "https://files.pythonhosted.org/packages/ad/c7/cd0fd1de581f1c2e8f996e704c9fd979e00106f18eebd91b0173cf1a13c6/yarl-1.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5684e7ff93ea74e47542232bd132f608df4d449f8968fde6b05aaf9e08a140f9", size = 349107 }, { url = "https://files.pythonhosted.org/packages/e6/34/ba3e5a20bd1d6a09034fc7985aaf1309976f2a7a5aefd093c9e56f6e1e0c/yarl-1.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8182ad422bfacdebd4759ce3adc6055c0c79d4740aea1104e05652a81cd868c6", size = 335144 }, { url = "https://files.pythonhosted.org/packages/1e/98/d9b7beb932fade015906efe0980aa7d522b8f93cf5ebf1082e74faa314b7/yarl-1.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aee5b90a5a9b71ac57400a7bdd0feaa27c51e8f961decc8d412e720a004a1791", size = 360795 }, { url = "https://files.pythonhosted.org/packages/9a/11/70b8770039cc54af5948970591517a1e1d093df3f04f328c655c9a0fefb7/yarl-1.19.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8c0b2371858d5a814b08542d5d548adb03ff2d7ab32f23160e54e92250961a72", size = 360140 }, { url = "https://files.pythonhosted.org/packages/d4/67/708e3e36fafc4d9d96b4eecc6c8b9f37c8ad50df8a16c7a1d5ba9df53050/yarl-1.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd430c2b7df4ae92498da09e9b12cad5bdbb140d22d138f9e507de1aa3edfea3", size = 364431 }, { url = "https://files.pythonhosted.org/packages/c3/8b/937fbbcc895553a7e16fcd86ae4e0724c6ac9468237ad8e7c29cc3b1c9d9/yarl-1.19.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a93208282c0ccdf73065fd76c6c129bd428dba5ff65d338ae7d2ab27169861a0", size = 373832 }, { url = "https://files.pythonhosted.org/packages/f8/ca/288ddc2230c9b6647fe907504f1119adb41252ac533eb564d3fc73511215/yarl-1.19.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b8179280cdeb4c36eb18d6534a328f9d40da60d2b96ac4a295c5f93e2799e9d9", size = 378122 }, { url = "https://files.pythonhosted.org/packages/4f/5a/79e1ef31d14968fbfc0ecec70a6683b574890d9c7550c376dd6d40de7754/yarl-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eda3c2b42dc0c389b7cfda2c4df81c12eeb552019e0de28bde8f913fc3d1fcf3", size = 375178 }, { url = "https://files.pythonhosted.org/packages/95/38/9b0e56bf14026c3f550ad6425679f6d1a2f4821d70767f39d6f4c56a0820/yarl-1.19.0-cp312-cp312-win32.whl", hash = "sha256:57f3fed859af367b9ca316ecc05ce79ce327d6466342734305aa5cc380e4d8be", size = 86172 }, { url = "https://files.pythonhosted.org/packages/b3/96/5c2f3987c4bb4e5cdebea3caf99a45946b13a9516f849c02222203d99860/yarl-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5507c1f7dd3d41251b67eecba331c8b2157cfd324849879bebf74676ce76aff7", size = 92617 }, { url = "https://files.pythonhosted.org/packages/cd/a7/222144efa2f4a47363a5fee27d8a1d24851283b5a7f628890805fe7f7a66/yarl-1.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59281b9ed27bc410e0793833bcbe7fc149739d56ffa071d1e0fe70536a4f7b61", size = 144789 }, { url = "https://files.pythonhosted.org/packages/72/4f/3ee8de3f94baa33c0716260b0048b1fd5306f104b3efc6e1713693e7063e/yarl-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d27a6482ad5e05e8bafd47bf42866f8a1c0c3345abcb48d4511b3c29ecc197dc", size = 96685 }, { url = "https://files.pythonhosted.org/packages/3e/7c/fbeebf875c1ededd872d6fefabd8a8526ef8aba6e9e8bcdf230d895d487b/yarl-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a8e19fd5a6fdf19a91f2409665c7a089ffe7b9b5394ab33c0eec04cbecdd01f", size = 94307 }, { url = "https://files.pythonhosted.org/packages/f3/ff/b7a9c1d7df37e594b43b7a8030e228ccd4ce361eeff24a92b17fe210e57d/yarl-1.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cda34ab19099c3a1685ad48fe45172536610c312b993310b5f1ca3eb83453b36", size = 342811 }, { url = "https://files.pythonhosted.org/packages/79/e2/9e092876b2156c1d386e4864e85eba541ccabf2b9dcc47da64624bad0cc9/yarl-1.19.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7908a25d33f94852b479910f9cae6cdb9e2a509894e8d5f416c8342c0253c397", size = 336928 }, { url = "https://files.pythonhosted.org/packages/71/24/648d99c134f2e14fc01ba790ad36ab56815e00069e60a12a4af893448b83/yarl-1.19.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e66c14d162bac94973e767b24de5d7e6c5153f7305a64ff4fcba701210bcd638", size = 351021 }, { url = "https://files.pythonhosted.org/packages/0c/ee/7278d475784d407d1990a5939722e66a0fef057046fb5f1721f0a6eb156c/yarl-1.19.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c03607bf932aa4cfae371e2dc9ca8b76faf031f106dac6a6ff1458418140c165", size = 354454 }, { url = "https://files.pythonhosted.org/packages/15/ae/242546114e052a7de21a75bd7d4860266439f90bbc21c5e4dd696866d91d/yarl-1.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9931343d1c1f4e77421687b6b94bbebd8a15a64ab8279adf6fbb047eff47e536", size = 347594 }, { url = "https://files.pythonhosted.org/packages/46/2c/35f4347f76ea4c986e9c1f774b085f489b3a1bf1503c67a4dfc5d8e68e92/yarl-1.19.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:262087a8a0d73e1d169d45c2baf968126f93c97cf403e1af23a7d5455d52721f", size = 334113 }, { url = "https://files.pythonhosted.org/packages/20/89/3086bc8ec8d7bd505531c51056452d7ae6af906d29c427374f1170ac1938/yarl-1.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70f384921c24e703d249a6ccdabeb57dd6312b568b504c69e428a8dd3e8e68ca", size = 361037 }, { url = "https://files.pythonhosted.org/packages/a1/5b/2c9765524a70d1c51922b41c91caa30c8094a416734349166e1a3d8de055/yarl-1.19.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:756b9ea5292a2c180d1fe782a377bc4159b3cfefaca7e41b5b0a00328ef62fa9", size = 361025 }, { url = "https://files.pythonhosted.org/packages/ca/f8/c4a190bcc3cd98fb428d1dd31519e58004153dc7f2acd1236ecae54e3433/yarl-1.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cbeb9c145d534c240a63b6ecc8a8dd451faeb67b3dc61d729ec197bb93e29497", size = 364397 }, { url = "https://files.pythonhosted.org/packages/6b/fb/f65b1347be8e12ac4e3e37a9bb880e6b9b604f252aaafd88e4879b1e9348/yarl-1.19.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:087ae8f8319848c18e0d114d0f56131a9c017f29200ab1413b0137ad7c83e2ae", size = 374065 }, { url = "https://files.pythonhosted.org/packages/1c/c5/102cc3b9baad1a76f9127453ad08e0f5bc9c996c18128b1e28fe03817d6c/yarl-1.19.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362f5480ba527b6c26ff58cff1f229afe8b7fdd54ee5ffac2ab827c1a75fc71c", size = 381341 }, { url = "https://files.pythonhosted.org/packages/f7/ce/f5dc0439320dfe59fadab8cdd24ac324be19cf6ae4736422c7e2a510ddf3/yarl-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f408d4b4315e814e5c3668094e33d885f13c7809cbe831cbdc5b1bb8c7a448f4", size = 376552 }, { url = "https://files.pythonhosted.org/packages/a9/4a/4833a134c76af987eff3ce8cb71e42932234120e6be061eb2555061e8844/yarl-1.19.0-cp313-cp313-win32.whl", hash = "sha256:24e4c367ad69988a2283dd45ea88172561ca24b2326b9781e164eb46eea68345", size = 85878 }, { url = "https://files.pythonhosted.org/packages/32/e9/59327daab3af8f79221638a8f0d11474d20f6a8fbc41e9da80c5ef69e688/yarl-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:0110f91c57ab43d1538dfa92d61c45e33b84df9257bd08fcfcda90cce931cbc9", size = 92448 }, { url = "https://files.pythonhosted.org/packages/a4/06/ae25a353e8f032322df6f30d6bb1fc329773ee48e1a80a2196ccb8d1206b/yarl-1.19.0-py3-none-any.whl", hash = "sha256:a727101eb27f66727576630d02985d8a065d09cd0b5fcbe38a5793f71b2a97ef", size = 45990 }, ]