pax_global_header00006660000000000000000000000064151423277050014520gustar00rootroot0000000000000052 comment=066503c0ce5a38e46c543caf2574fa1a41b60827 offpunk-v3.0/000077500000000000000000000000001514232770500132205ustar00rootroot00000000000000offpunk-v3.0/.build.yml000066400000000000000000000022211514232770500151150ustar00rootroot00000000000000image: alpine/latest oauth: pages.sr.ht/PAGES:RW packages: - hut - uv secrets: # see https://builds.sr.ht/secrets - c4b4edb9-6d07-45fe-ac31-5d3ac6a27a8a #~/.pypi-credentials mode 700 environment: site1: offpunk.net site2: xkcdpunk.net tasks: # The following, contributed by Anna Cybertailor, will automatically # upload the package to pypi if it is a release - publish-pypi: | if [[ ${GIT_REF} != refs/tags/* ]]; then echo "Current commit is not a tag; not building anything" exit 0 fi rm -rf dist uv build ~/.pypi-credentials uv publish dist/* - package-gemini: | cp -r offpunk/tutorial public_gemini cd public_gemini ln -s ../offpunk/screenshots . tar -cvzh . > ../capsule.tar.gz - deploy-gemini: | hut pages publish capsule.tar.gz -p GEMINI -d $site1 hut pages publish capsule.tar.gz -p GEMINI -d $site2 - package-html: | mkdir public_html cd offpunk/tutorial python make_website.py cd ../../public_html ln -s ../offpunk/screenshots . tar -cvzh . > ../site.tar.gz - deploy-html: | hut pages publish site.tar.gz -d $site1 hut pages publish site.tar.gz -d $site2 offpunk-v3.0/.gitignore000066400000000000000000000024511514232770500152120ustar00rootroot00000000000000# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ offpunk-v3.0/CHANGELOG000066400000000000000000000774501514232770500144470ustar00rootroot00000000000000# Offpunk History ## 3.0 - February 9th 2026 Changes since 2.8 Important changes: - "opnk" is deprecated and replaced by "openk" - Default image size now 100 instead of 40 (can be restored with "set images_size 40") - Links to available feeds are now displayed at the bottom of HTML pages - "root" has been modified to go to the root of the capsule/userspace "root /" for real root - Images are now displayed in Gemini. This can be disabled with "set gemini_images false" - "ls" command is deprecated Features: - Offpunk is now translatable (by JMCS). Translations in ES, GC (JMCS) and NL (Bert Livens) - Add "unmerdify" tool to extract content from HTML if "ftr_site_config" is set (by Vincent Jousse) - new "xkcdpunk" command-line tool to directly access your XKCD comics - new "websearch" command which default to wiby.me - new "share" command to send a page by email - new "reply" command to send an email to the author of the page - "cookies" command and http cookies are now supported in netcache (by Urja) - "set default_cmd" now allows to configure what happen on empty lines - "view switch" changes between readable/full view (by Andrew Fowlie) - customize prompt with "set prompt_on" and "set prompt_off" (by Andrew Fowlie) - "help help" now sends an email to the offpunk-users mailing-list - "links" command now display all links - new "blocked_link" object in theme. By default, blocked link are in red. - new "theme preset" to allow switching between multiple hardcoded themes like "yellow","cyan" or "bw" Others: - In "lists", only consider files ending with .gmi (bug #46 by woffs) - support for new links color in gopher (by JMCS) - fix an off by one bug in "cp url XX" - Ansicat: Fix incorrectly identifying RSS feeds as XHTML when there was no entry - Fix sync hanging on gopher interactive prompt (by JMCS) - Fix crash when trying to parse HTML without BS4 (reported by Hoël Bézier) - Automatically replace space by %20 in img url in HTML - Ansicat: HTML Render img with data-src if src doesn’t link to a valid image - Opnk: fix a crash when trying to cache-clean uncached url (such as images) - added mime-type "application/pgp-keys" as to be opened by TextRenderer - "redirect" has been refactored to acts on the netcache level - ansicat: in HTML, the link to a picture is only displayed if different from the picture itself - added "wgl" and "wde" shortcuts to wikipedia GL & DE (JMCS) - "bugreport" allows to send a bug report to the -devel list - "theme" now accept "none" as a color to remove part of the theme you don’t want - "info" now explain how the HTML content was cleaned - history now consider multiple successive views of a page as one visit only ## 3.0-beta3 - Released as 3.0 - added ftr_site_config output to "version" - added "wgl" and "wde" shortcuts to wikipedia GL & DE (JMCS) - TRANSLATION: updated GL and ES (JMCS) - BUGFIX: successive visits of the same URL are not added to history even in different modes - TRANSLATION: NL_be (by Bert Livens) - Added manpage for "xkcdpunk" ## 3.0-beta2 - February 6th 2026 Changes since 3.0-beta1 - "bugreport" allows to send a bug report to the -devel list - "theme" now accept "none" as a color to remove part of the theme you don’t want - symlink opnk.py to openk.py to handle deprecation - BUGFIX: history now consider multiple successive views of a page as one visit. - BUGFIX: do not urlify mailto, gopher and local urls - BUGFIX: In "openk", self.last_width was badly reinitialized after cleanup() - CRASHFIX: in "ansicat", build_body_and_links was crashing with empty links - CRASHFIX: "openk" was crashing when used in a shell pipe - improve Gopher selector code to be more robust (by JMCS) - TRANSLATION: Spanish - ES (by JMCS) - TRANSLATION: Galician - GC (by JMCS) ## 3.0-beta1 - February 2nd 2026 This is a beta release before 3.0. Its goal is to find bugs, allow for a "string freeze" period for translators and give packagers time to adapt to the major changes: adding translations to their packages, adding xkcdpunk and unmerdify. Packagers are advised to build the package to report bugs but *not* to upload to their respective stable distribution. Beta-testers and documenters are welcome to test and document new features and new tools. Important changes: - "opnk" is deprecated and replaced by "openk" - Default image size now 100 instead of 40 (can be restored with "set images_size 40") - Links to available feeds are now displayed at the bottom of HTML pages - "root" has been modified to go to the root of the capsule/userspace "root /" for real root - Images are now displayed in Gemini. This can be disabled with "set gemini_images false" - "ls" command is deprecated Features: - Offpunk is now translatable (by JMCS) - new "xkcdpunk" command-line tool to directly access your XKCD comics - new "websearch" command which default to wiby.me - new "share" command to send a page by email - new "reply" command to send an email to the author of the page - "cookies" command and http cookies are now supported in netcache (by Urja) - "set default_cmd" now allows to configure what happen on empty lines - "view switch" changes between readable/full view (by Andrew Fowlie) - customize prompt with "set prompt_on" and "set prompt_off" (by Andrew Fowlie) - "help help" now sends an email to the offpunk-users mailing-list - "links" command now display all links - new "blocked_link" object in theme. By default, blocked link are in red. - new "theme preset" to allow switching between multiple hardcoded themes like "yellow" or "cyan" Unmerdify alpha support (by Vincent Jousse): - Add "unmerdify" tool to extract content from HTML page - if "ftr_site_config" is set, unmerdify will be enabled in "ansicat" - ansicat will fallback to readability if unmerdify fails - "info" now explain how the content was cleaned Others: - In "lists", only consider files ending with .gmi (bug #46 by woffs) - support for new links color in gopher (by JMCS) - fix an off by one bug in "cp url XX" - Ansicat: Fix incorrectly identifying RSS feeds as XHTML when there was no entry - Fix sync hanging on gopher interactive prompt (by JMCS) - Fix crash when trying to parse HTML without BS4 (reported by Hoël Bézier) - Automatically replace space by %20 in img url in HTML - Ansicat: HTML Render img with data-src if src doesn’t link to a valid image - Opnk: fix a crash when trying to cache-clean uncached url (such as images) - added mime-type "application/pgp-keys" as to be opened by TextRenderer - "redirect" has been refactored to acts on the netcache level - ansicat: in HTML, the link to a picture is only displayed if different from the picture itself ## 2.8 - November 13th 2025 - New option "images_size" to use with "set" (idea by aelius) - Automatically use newly created certificates in Gemini (by JMCS) - Fix a rare crash when listing urls in a non-existant page - ansicat: Trying to get render preformatted text correctly in HTML - ansicat: html should be inline, not a separate block like
- ansicat:  is rendered as 
- fix "up" not working correctly on gopher (thanks JMCS)
- gopher: implement support for item type 7 (by JMCS)
- fix out of range index on page without links (by Alexander W. Jans)
- User-agent now includes URL to avoid blocking (suggested by cks)
- Adding gemtext rendering to message/rfc822 files 
- ansicat: support for  in html rendering
- gopher: consider .xml files as feeds (by JMCS)
- gopher: detect image types in gopher to open them internally (by JMCS)
- "reload" now delete cached rendering, online and offline
- ansicat: remove soft-hyphens when rendering any content

## 2.7.1 - April 9th 2025 - The "2.7 was not (completely) a joke" release
See 2.7 changelog. This was "real" release ;-)
- Reverted the tutorial back to Offpunk. The name was a joke but the rest of the release is real.
- added "application/xhtml+xml" mimetype to be rendered as html
- according to html spec, 
 content should be parsed as HTML.

## 2.7 - April 1st 2025 - XKCDpunk release
APRIL FOOL: Offpunk is now renamed to XKCDpunk. See
https://ploum.net/2025-04-01-xkcdpunk.html
- Introducing command "xkcd" to display a given xkcd comic.
- (April fool) Switching website from offpunk.net to xkcdpunk.net
- blocklist: marked x.com, twitter.com and youtube.com as blocked to save bandwith until we have a viable option to use them with offpunk. We plan to block everything but xkcd.com.
CHANGES:
- "shell" (or "!") now works even without content (so you can run it on startup)
- offpunk: improved "version" to help debugging on the mailing-list
FIXES:
- "reload" on a too large content will actually fetch it
- Consider mimetype "message/news" as "gemtext" because file is sometimes confused. (reported by JMCS)
- ignore annoying warnings for LibreSSL users (by Anna cyberTailor)
- ansicat: workaround what seems to be a BS4 bug where elements after a 
were ignored - offpunk: be more explicit about how to unblock a blocked URL - ansicat: if a file identified as html starts with "260 char) instead of raising an useless error ## 2.6 - February 24th 2025 FOR PACKAGERS: dependency to python-pil (or pillow) has been dropped - NEW BEHAVIOUR: not rendered ressources (like PDF) are not opened automatically. The user is prompted to type "open" to see the ressource. This allows the ressource to be part of history, be bookmarked, etc… Offpunk: - new "--command" command-line argument to immediately launch one or multiple commands - "add" now accepts link number as a second argument (suggested by JMCS) - "url" now accepts link number as an argument (suggested by JMCS) - "url" can be piped to any shell command: "url|" or "url 121|" (suggestion of Stephen) Ansicat and rendering: - new "preformat_wrap" option available to wrap even
- PlainText rendering has been vastly improved with wrap, margin and link detection
- fix wrong wraping in gophermap (patch by JMCS)
Opnk and opening files:
note: "opnk" will be renamed "openk" in 3.0
- "opnk" now supports following link like in "opnk $URL XX" where XX is a link number.
- "opnk" is now reading the offpunrc file to use predefined handlers
- "handler" with now automatically add "%s" at the end of the command if not present
- "handler" now supports file exension or full mimetype
Deprecation and removal:
- removed support for chafa < 1.10, as announced in 2.4. python-pil is not used anymore. 
- removed the "cat" command as it has no purpose (you can use "!cat" instead) and there was a potential crash (reported by Stephen)
- new "feed" command to replace the "view feed" (which is deprecated)

## 2.5 - January 30th 2025
- "abbrevs" has been replaced by "alias"
- "alias" now allows custom command to be aliased
- PEP8-ification by Vincent Jousse
- changing "datetime.UTC" to "datetime.timezone.utc" for retrocompatibility with python < 3.11
- checking if "grep" supports "--color=auto" to supports OpenBSD (reported by Dylan D’Silva)

## 2.4 - November 21st 2024
NEW WEBSITE: Official homepage is now https://offpunk.net (or gemini://offpunk.net). Sources are in the /tutorial/ folder and contributions are welcome.
NEW FEATURE: This release includes work by Bert Livens to add gemini client-side certificates (see "help certs"). This means you can browse gemini capsule while being identified (such as astrobotany)
- Deprecation warning if using Chafa < 1.10
- introducing the "tutorial" command (which is only a link to offpunk.net for now)
- netcache: use client-certificate when going to a url like gemini://username@site.net (by Bert Livens)
- offpunk/netcache: added the "cert" command to list and create client certificates (Bert Livens)
- "open" now accept integer as parameters to open links (suggested by Matthieu Rakotojaona)
- fix cache not being properly accessed when server redirect to same host with standard port (gemini.ucant.org)
- fix crash when expired certificate due to not_valid_after deprecation
- fix crash in netcache when a port cannot be parsed in the URL
- fix parameter "interactive=False" not being sent to gemini redirections
- fix problem with non-integer version of less (Patch by Peter Cock)
- Gopher: hide lines starting with TAB (drkhsh.at), reported by Dylan D’Silva

## 2.3 - June 29th 2024
- Wayland clipboard support through wl-clipboard (new suggested dependency)
- Xclip clipboard support (in case xsel is missing)
- offpunk/netcache: fix IPv6 as an URL (bug #40)
- ansicat: display empty files (instead of opening them with xdg-open)
- fix escape sequence warning in python 3.12 (by Étienne Mollier) (Debian #1064209)
- ansicat : fix crash when feedparser is crashing on bad RSS
- netcache: fix spartan protocol error
- opnk: fix a crash when caching returns None
- ansicat: remove the treshold argument when launching chafa (strange artifacts with new version)
- netcache: moved the certificate cache to the filesystem instead of a database (by Bert Livens)

## 2.2 - February 13th 2024
- cache folder is now configurable through $OFFPUNK_CACHE_PATH environment variable (by prx)
- offpunk: adding an URL to a list now update the view mode if url already present
- netcache: solve an infinite gemini loop with code 6X (see also bug #31)
- ansicat: added support for 
(by Bert Livens) - opnk: added "--mode" command-line argument (bug #39) - offpunk: support for "preformatted" theming (bug #38) - opnk/netcache: added "--cache-validity" command-line argument (bug #37) - ansicat: consider files as XML, not SVG, if they don’t have .svg extension - offpunk: fix "view link" crashing with link to empty files ## 2.1 - December 15th 2023 - freshly updated gemtext/rss links are highlighted ("new_link" theme option) - offpunk : new "copy title" and "copy link" function - offpunk : new "view XX" feature where XX is a number to view information about a link - ansicat: added "--mode" option - redirections are now reflected in links and the cache (bug #28) - ansicat: avoid a crash when urllib.parse.urljoin fails - offpunk: Fix a crash when gus is called without parameters (Von Hohenheiden) - ansicat: fixed a crash when parsing wrong hidden_url in gemini (bug #32) - offpunk: offpunk --version doesn’t create the cache anymore (bug #27) - ansicat: fix a crash with HTML without title (bug #33) - netcache: gemini socket code can crash when IPv6 is disabled (mailing-list) ## 2.0 - November 16th 2023 Changes since 1.10 - IMPORTANT: Licence has been changed to AGPL for ideological reasons - IMPORTANT: Contact adress has been changed to offpunk2 on the same domain (because of spam) - IMPORTANT: code has been splitted into several differents files. - IMPORTANT: migrating from flit to hatchling (patch by Jean Abou Samra) Major features: - New command-line tool: "netcache" - New command-line tool: "ansicat" - New command-line tool: "opnk" - "theme" command allows customization of the colours - "--config-file" allows to start offpunk with custom config (#16) - "view source" to view the source code of a page - introduced the "default_protocol" options (default to gemini) Improvments: - Reading position is saved in less for the whole session - Rendering is cached for the session, allowing faster browsing of a page already visited - "redirect" supports domains starting with "*" to also block all subdomins - "--images-mode" allow to choose at startup which images should be dowloaded (none,readable,full) - Support for embedded multi-format rendering (such as RSS feeds with html elements) - The cache is now automatically upgraded if needed (see .version in your cache) - Images of html files are now downloaded with the html (slower sync but better reading experience) - "--sync" can optionnaly take some lists as arguments, in order to make for specific sync - initial tentative to support podcasts in RSS/Atom feeds Other notable changes from 1.X: - "accept_bad_ssl_certificates" now more agressive for http and really accepts them all - Gopher-only: we don’t support naming a page after the name of the incoming link - Gemini-only: support for client generated certificates has been removed - "file" is now marked as a dependency (thank Guillaume Loret) ## 2.0 (beta3 - final 2.0) - Released as 2.0 Changes since beta2: - bug #25 : makes python-requests optional again - --disable-http had no effect: reimplemented - introduced the "default_protocol" options (default to gemini) to enter URLs without the :// part (fixes bug #21) ## 2.0-beta2 - November 8th 2023 Changes since beta1 - IMPORTANT: migrating from flit to hatchling (patch by Jean Abou Samra) - "--sync" can optionnaly take some lists as arguments, in order to make for specific sync - "view source" to view the source code of a page - initial tentative to support podcasts in RSS/Atom feeds - new PlaintextRenderer which display .txt files without any margin/color/linebreaks - default URL blocked list is now its own file to make contributions easier - prompt color is now part of the theme - improves handling of base64 images - fixes gophermap being considered as gemtext files - fixes opening mailto links - fixes existing non-html ressources marked a to_fetch even when not needed (simple and/or confusion) - fixes a crash with RSS feeds without element - fixes a crash with data:image/svg+xml links - fixes a bug in HTML renderer where some hX element were not closed properly - fixes input in Gemini while online - fixes a crash with invalid URL - fixes a crash while parsing invalid dates in RSS - fixes hang/crash when meeting the ";" itemtype in gopher - attempt at hiding XMLparsedAsHTMLWarning from BS4 library - chafa now used by default everywhere if version > 1.10 - ignoring encoding error in ansicat ## 2.0-beta1 - September 05th 2023 This is an an experimental release. Bug reports and feedbacks are welcome on the offpunk-devel list. - WARNING: pyproject.toml has not been updated and is currently non-functional. Help needed! - IMPORTANT: Licence has been changed to AGPL for ideological reasons - IMPORTANT: Contact adress has been changed to offpunk2 on the same domain (because of spam) - IMPORTANT: code has been splitted into 7 differents files. Installation/packaging should be adapted. Major features: - New command-line tool: "netcache" - New command-line tool: "ansicat" - New command-line tool: "opnk" - "theme" command allows customization of the colours - "--config-file" allows to start offpunk with custom config (#16) Improvments: - Reading position is saved for the whole session - Rendering is cached for the session, allowing faster browsing of a page already visited - "redirect" supports domains starting with "*" to also block all subdomins - "--images-mode" allow to choose at startup which images should be dowloaded (none,readable,full) - Support for multi-format rendering (such as RSS feeds with html elements) - The cache is now automatically upgraded if needed (see .version in your cache) Other changes from 1.X: - Images of html files are now downloaded with the html (slower sync but better reading experience) - URL do not default anymore to "gemini://" if not protocol are indicated. (ongoing discussion in #21) - "accept_bad_ssl_certificates" now more agressive for http and really accepts them all - Gopher-only: we don’t support naming a page after the name of the incoming link - Gemini-only: support for client generated certificates has been removed - "file" is now marked as a dependency (thank Guillaume Loret) ## 1.10 - July 31st 2023 - IMPORTANT : new optional dependency : python-chardet - IMPORTANT : Gopher directory index filename changed from "index.txt" to "gophermap". To update the cache to the new format run the `migrate-offpunk-cache` script (Sotiris Papatheodorou) - "set accept_bad_ssl_certificates True" now also used for Gemini expired certificates - Add missing chardet module (Sotiris Papatheodorou) - Fix merging dictionaries with common keys (Sotiris Papatheodorou) - Fix Gopher requests (rewrite URL parsing code per RFC 4266) ## 1.9.2 - March 13th 2023 - Switch from setup.py to flit (Anna cybertailor Vyalkova) - Bump requirements to python >= 3.7 (Anna cybertailor Vyalkova) ## 1.9.1 - March 8th 2023 - Fixed crash with archive without GI (thanks Étienne Mollier) ## 1.9 - March 8th 2023 This is a bug-fixing release. - We now have a man page thanks to phoebos! - ".." as abbreviation to "up" (by Sotiris Papatheodorou) - Fix support for UTF-8 domains in Gemini (Maeve Sproule, fixes #5) - Assume UTF-8 when the header answer with an unknown encoding - Default handlers have been removed (not everybody use feh and zathura) - Fix a crash when subscribing without GI (reported by sodimel on linuxfr) - Fix a crash when trying to access a link without GI (Ben Winston) - Fix a crash when rss items don’t have a title (eg: Mastodon rss) - Fix a crash with badly formatted links in gopher ( #7 by xiu) - Fix a crash were some HTML content is seen a bytes instead of a string - Fix a crash when displaying embedded CDATA html in feed. But #10 is still open. - Fix error handling assuming that requests is installed - Ugly fix for a rare certificate bug (fix #11) - Improve compatibility with python prior 3.9 by replacing a dict union ## 1.8 - December 11th 2022 - Official URL is now https://sr.ht/~lioploum/offpunk/ - SECURITY: Avoid passing improperly-escaped paths to shell (fixes notabug #9) (by Maeve Sproule) - Add support for the finger protocol (by Sotiris Papatheodorou) - "restricted" mode has been removed because unmaintained (code cleanup) - "set accept_bad_ssl_certificates True" allows to lower HTTPS SSL requirements (also with --assume-yes) - Accept "localhost" as a valid URL - Better feedback when --sync an URL which is streaming - Removed cgi dependency (soon deprecated) - Fix: crash with some svg data:image (which are now ignored) - Fix images from "full" mode not being downloaded - Fix a crash when ls on empty page (thanks Marty Oehme) - Fix: A variable was not initialised without python-cryptography - Fix: "cp raw" was not accessing the temp_file correctly - Fix: ANSI handling off arrows in readline (by Ben Winston) ## 1.7.1 - November 15th 2022 - Correcting a stupid crash in search (thanks kelbot for the report) ## 1.7 - November 15th 2022 - New "search" command which uses kennedy.gemi.dev by default. - New "wikipedia" command, which uses vault.transjovian.org by default. - Aliases "wen", "wfr" and "wes" for Wikipedia in English, French and Spanish. - Autocompletion for the list/add/move commands (that’s incredibly useful!) - If a link is found in plain text in a gopher/gemini page, it is now added to the list of links for that page. Useful for gopher. - Create system lists when needed to avoid failure on clean system - Solve a crash when parsing wrong URL (related to bug #9 ) - Solve a crash when loading webpages with empty links - Solve a crash when trying to load a wrong URL into tour => gemini://ploum.be/2022-11-15-offpunk17-sourcehut.gmi ## 1.6 - October 12th 2022 - Support for base64 encoded pictures in HTML pages (opening them full screen only works offline) - A list can be added to a tour with "tour $LIST_NAME". - Check for timg > 1.3.2 to avoid dealing with old versions (bug reported by Valvin) - Redirect are now honoured also when --sync (bug #15, thanks kelbot) - RSS feeds are now automatically downloaded with a webpage (bug #14) - Solved the bug where an invalid URL would break correspondance between url and numbers - Considers .xml files as feed by default to avoid false-detection as SVG - Replaced default libreddit.com redirection to teddit.net (bug #12 by kelbot) - The "beta" option has been removed as it is not used (update your config if needed) ## 1.5 - August 4th 2022 - Removed optional dependency to ripgrep. "grep --color=auto" is good enough. - "open url" to open current URL in a browser with xdg-open - "redirect" now replaces "set redirects" to improve discoverability - "redirect" now allows urls to be blocked. By default, facebook.com and google-analytics.com are blocked - Fixed a bug when trying to download base64 image => gemini://rawtext.club/~ploum/2022-08-04-offpunk15.gmi ## 1.4 - April 25th 2022 - Making python-readability optional - Removing "next" and "previous" which are quite confusing and not obvious - Archiving now works regardless of the view you are in. - Fixing a crash when accessing an empty html page - Not trying to display non-image files to avoid errors. (this requires "file") ## 1.3 - April 2th 2022 - Removed dependency to python-magic. File is now used directly (and should be on every system). - Removed dependency to python-editor. If no $VISUAL or $EDITOR, please use "set editor" in Offpunk. - Images are now downloaded before displaying an HTML page (can be disabled with "set download_images_first False") - Introduced "set redirects" which redirects twitter,youtube,medium,reddit to alternative frontends. - New behaviour for "find" (or "/") which is to grep through current page (ripgrep used if detected) - Default width set to 80 as many gopherholes and gemini capsules have it hardcoded - Streaming URL without valid content-length are now closed after 5Mo of download (thanks to Eoin Carney for reporting the issue) - Gif animations are now displayed once when viewed (instead of a still frame). - Restored some AV-98 certificate validation code that was lost I don’t know how. - Improved clarity of dependencies in "version" - Fixed a crash when the cache is already a dir inside a dir. - Fixed a crash when manually entering an unknown gopher URL while offline - Fixed an error with older less version - Fixed bookmarks not being automatically created at first "add" - Call to shell commands has been refactorised to improve compatibility with python 3.6 (with testing from Pelle Nilsson) - requirements.txt has been contributed by Toby Kurien. Thanks! => gemini://rawtext.club/~ploum/2022-04-02-offpunk13.gmi ## 1.2 - March 24th 2022 Very experimental release: - Completely rewritten the HMTL, Gemtext and Gopher renderer. Tests needed! - Removed dependancy to ansiwrap. We don’t use it anymore (which is an important achievement) - Lists are now accessed via the protocol "list://". - "view full" can now be bookmarked/synchronized as a separate entity. - "view normal" introduced to get back to the normal view. Small improvements: - Limit width of --sync output - Solved list names becoming very long in the history - Fixed a crash when trying to save a folder => gemini://rawtext.club/~ploum/2022-03-24-ansi_html.gmi ## 1.1 - March 18th 2022 - Perfect rendering of pictures with chafa 1.8+ and compatible terminal (Kitty) - timg is supported as an alternative to chafa (with a little glitch) - "cp cache" put the path of the cached content in clipboard - "cp url X" will copy the URL of link X (suggested by Eoin Carney) - "fold" has been removed as it doesn’t work well and can be replaced with "!fold". - Improved clipboard URL detection an fixed crash when binary in clipboard - HTML: renderering of
 has been improved
- HTML: links in titles were previously missed
- Fixed crash when chafa is not installed (Thanks Xavier Hinault for the report)
- Fixed crash when python-readability not installed (Thanks Nic for the report)
- Fixed some gif not being displayed
- Fixed some URL being wronlgy interpreted as IPv6

## 1.0 - March 14th 2022
- Default width is now the standard 72
- Content and pictures now centered for more elegant reading
- "less" has been renamed "view"
- "view feed" and "view feeds" to see the first/all feeds on a HTML page
- "view full" has been improved by dropping inline CSS and JS.
- "up" can now take integer as argument to go up multiple steps.
- Fixed a crash when accessing links in list (thanks Matthieu Talbot for the report)
- Fixed a crash in "info" due to a typo in a variable name rarely accessed.
- Removed dependancy to python-xdg by implementing the logic (which saved lines of code!)
- python-pil is only needed if chafa < 1.10
=> gemini://rawtext.club/~ploum/2022-03-14-offpunk_and_cyberpunk.gmi

## 0.9 - March 05th 2022
- Initial Spartan protocol support
- Http links with content above 20Mo are not downloaded during sync (except when explicitely requested)
- Improving subscriptions with more feedback and better detection
- Avoid deprecated SSL methods (thanks Phoebos for the report)
- Links in to_fetch are fetched, no matter the cache
- Fixed multiple crashes
=> gemini://rawtext.club/~ploum/2022-03-05-offpunk09.gmi

## 0.4 - Feb 21st 2022
UPGRADE: Users who subscribed to pages before 0.4 should run once the command "list subscribe subscribed". Without that, the subscribed list will be seen as a normal list by sync.
- New list command : "list freeze" and "list suscribe"
- Pictures are now displayed directely in terminal (suggested by kelbot)
- "open" command to open current page/image/file with external handler.
- "set width XX" now works to set the max width. If smaller, terminal width is used (thanks kelbot for reporting the bug)
- RSS feeds are now rendered as Gemlogs to improve consistency while browsing
- "subscribe" will detect feeds in html pages if any
- "less" will restore previous position in a page (requires less 572+)
- Improved syncing performances and multiple bug/crash fixes.
- "version" will now display info about your system installation
- "info" command will display technical information about current page
- "sync" allows you to do the sync from within Offpunk
=> gemini://rawtext.club/~ploum/2022-02-21-offpunk04.gmi

## 0.3 - Feb 11th 2022
New Features:
- Gopher supported natively (early version, might have many bugs)
- support for RSS and Atom feed (you can subscribe to them)
- "less full" allows to see the full html page instead of only the article view
 	(also works with feeds to see descriptions of each post instead of a simple list)
- Option --depth to customize your sync. Be warned, more than 1 is crazy.
- Option --disable-http to allows deep syncing of gemini-only ressources
- Vastly improved HTML rendering with support for images (you need the binary "chafa" on your system)
Other Small Improvements:
- Disabled https_everywhere by default (caching problems and some websites not supporting it)
- Modified --sync logic to make it more intuitive (thanks Bjorn Westergard)
- Caching more problems to avoid refetch
- Offpunk has now an User-Agent when http browsing to avoid being blocked as a bot
- Changed XDG logic to improve compatibility (thanks Klaus Alexander)
=> gemini://rawtext.club/~ploum/2022-02-11-offpunk03.gmi

## 0.2 - Jan 31st 2022
- config directories have been moved to follow the XDG specifications
- support for http, https and mailto links (https_everywhere is enabled by default, see "set" command)
- support for HTML pages, rendered as articles
- Mutiple bookmarks lists and management of them through commands list, add, archive, move
- Subscriptions have been moved to a separate list with the subscribe command
- History is persistent and saved to disk
- Copy command allows to copy content or url into buffer
- Search as been renamed find, in the hope of implementing a real search in the future
- --fetch-later allows to mark a content to be fetched from other software.
- --assume-yes allows to choose the default answer to certificates warnings during --sync.
=> gemini://rawtext.club/~ploum/2022-01-31-offpunk02.gmi Announcing Offpunk 0.2

## 0.1 - Jan 3rd 2022
- initial release as an independant software from AV-98 (thanks solderpunk)
- Including contributions published by Bjorn on Notabug (thanks ew0k)
- less used by default for all content with custom options
- online/offline mode
- content is cached for offline use
- bookmarks are cached and subscribed through the --sync option
- tour is persistent and saved to disk
- reload while offline mark the content to be fetched during next --sync
=> gemini://rawtext.club/~ploum/2022-01-03-offpunk.gmi Announce of Offpunk 0.1
offpunk-v3.0/CONTRIBUTORS000066400000000000000000000012351514232770500151010ustar00rootroot00000000000000Offpunk Offline Gemini client
And the Offpunk tools suite: netcache, ansicat, opnk.
(C) From 2021:  Ploum 

Derived from AV-98 and Agena by Solderpunk,
(C) 2019, 2020: Solderpunk 

Unmerdify from Vincent Jousse
(C) 2025: Vincent Jousse

AV-98 received contributions from:
  - danceka 
  - 
  - 
  - Klaus Alexander Seistrup 
  - govynnus 
  - Björn Wärmedal 
  - 

Offpunk received contributions from:
  - Maeve Sproule 

  and many others who were not added in this file
offpunk-v3.0/LICENSE000066400000000000000000001034641514232770500142350ustar00rootroot00000000000000                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

Copyright (c) 2022, Ploum  and contributors.
All rights reserved.

 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
.
offpunk-v3.0/README.md000066400000000000000000000202541514232770500145020ustar00rootroot00000000000000# OFFPUNK

A command-line and offline-first smolnet browser/feed reader for Gemini, Gopher, Spartan and Web by [Ploum](https://ploum.net).

The goal of Offpunk is to be able to synchronise your content once (a day, a week, a month) and then browse/organise it while staying disconnected.

Official page : [Offpunk.net](https://offpunk.net)
Development (repository/mailing lists) : [sr.ht](https://sr.ht/~lioploum/offpunk/)

![Screenshot HTML page with picture](screenshots/1.png)
![Screenshot Gemini page](screenshots/2.png)

Offpunk is a fork of the original [AV-98](https://tildegit.org/solderpunk/AV-98) by Solderpunk and was originally called AV-98-offline as an experimental branch.

## How to use

Offpunk is a set of python files. Installation is optional, you can simply git clone the project and run "./offpunk.py" or "python3 offpunk.py" in a terminal. You can also a packaged version:

- [List of existing Offpunk packages (Repology)](https://repology.org/project/offpunk/versions)
- Please contribute packages for other systems, there’s a [mailing-list dedicated to packaging](https://lists.sr.ht/~lioploum/offpunk-packagers).

To get started, launch offpunk then type "tutorial".

You can also consults it online: [Offpunk tutorial](https://offpunk.net/firststeps.html)

At any point, you can use "help" to get the list of commands and "help command" to get a small help about "command".

## More

Important news and releases will be announced on the [offpunk-devel mailing list](https://lists.sr.ht/~lioploum/offpunk-devel)

Questions can be asked on [the users mailing](list https://lists.sr.ht/~lioploum/offpunk-users)

## Dependencies

Offpunk has few "strict dependencies", i.e. it should run and work without anything
else beyond the Python standard library and the "less" pager. However, it will "opportunistically import" a few other libraries if they are available to offer an improved
experience or some other features such as HTTP/HTML or image support.

To avoid using unstable or too recent libraries, the rule of thumb is that a library should be packaged in Debian/Ubuntu. Keep in mind that Offpunk is mainly tested will all libraries installed. If you encounter a crash without one optional dependencies, please report it. Patches and contributions to remove dependencies or support alternatives are highly appreciated.

- PIP: [requirements file to install dependencies with pip](requirements.txt)
- Ubuntu/Debian: [command to install dependencies on Ubuntu/Debian without pip](ubuntu_dependencies.txt)

Run command `version` in offpunk to see if you are missing some dependencies.

Mandatory or highly recommended (packagers should probably make those mandatory):

- [less](http://www.greenwoodsoftware.com/less/): mandatory but is probably already on your system
- [file](https://www.darwinsys.com/file/) is used to get the MIME type of cached objects. Should already be on your system.
- [xdg-utils](https://www.freedesktop.org/wiki/Software/xdg-utils/) provides xdg-open which is highly recommended to open files without a renderer or a handler. It is also used for mailto: command.
- The [cryptography library](https://pypi.org/project/cryptography/) will provide a better and slightly more secure experience when using the default TOFU certificate validation mode and is recommended (apt-get install python3-cryptography).

Dependencies to enable web browsing (packagers may put those in an offpunk-web meta-package but it is recommended to have it for a better offpunk experience)

- [Python-requests](http://python-requests.org) is needed to handle http/https requests natively (apt-get install python3-requests). Without it, http links will be opened in an external browser
- [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup) is needed to render HTML. Without it, HTML will not be rendered or be sent to an external parser like Lynx. (apt-get install python3-bs4)
- [Readability](https://github.com/buriy/python-readability) is highly suggested to trim useless part of most html pages (apt-get install python3-readability or pip3 install readability-lxml)
- [Python-feedparser](https://github.com/kurtmckee/feedparser) will allow parsing of RSS/Atom feeds and thus subscriptions to them. (apt-get install python3-feedparser)
- [Chafa](https://hpjansson.org/chafa/) allows to display pictures in your console. Install it and browse to an HTML page with picture to see the magic.

Gopher dependencies:

- [Python-chardet](https://github.com/chardet/chardet) is used to detect the character encoding on Gopher (and may be used more in the future)

Nice to have (packagers should may make those optional):

- [Xsel](http://www.vergenet.net/~conrad/software/xsel/) allows to `go` to the URL copied in the clipboard without having to paste it (both X and traditional clipboards are supported). Also needed to use the `copy` command. (apt-get install xsel). Xclip can be used too.
- [Wl-clipboard](https://github.com/bugaevc/wl-clipboard) allows the same feature than xsel but under Wayland
- [Python-setproctitle](https://github.com/dvarrazzo/py-setproctitle) will change the process name from "python" to "offpunk". Useful to kill it without killing every python service.

## Features

- Browse https/gemini/gopher without leaving your keyboard and without distractions
- Customize your experience with the `theme` command.
- Built-in documentation: type `help` to get the list of command or a specific help about a command.
- Offline mode to browse cached content without a connection. Requested elements are automatically fetched during the next synchronization and are added to your tour.
- HTML pages are prettified to focus on content. Read without being disturbed or see the full page with `view full`.
- RSS/Atom feeds are automatically discovered by `subscribe` and rendered as gemlogs. They can be explored with `view feed` and `view feeds`.
- Support "subscriptions" to a page. New content seen in subscribed pages are automatically added to your next tour.
- Complex bookmarks management through multiple lists, built-in edition, subscribing/freezing lists and archiving content.
- Advanced navigation tools like `tour` and `mark` (as per VF-1). Unlike AV-98, tour is saved on disk accross sessions.
- Ability to specify external handler programs for different MIME types (use `handler`)
- Enhanced privacy with `redirect` which allows to block a http domain or to redirect all request to a privacy friendly frontent (such as nitter for twitter).
- Non-interactive cache-building with configurable depth through the --sync command. The cache can easily be used by other software.
- `netcache`, a standalone CLI tool to retrieve the cached version of a network ressource.
- `ansicat`, a standalone CLI tool to render HTML/Gemtext/image in a terminal.
- `openk`, a standalone CLI tool to open any kind of ressources (local or network) and display it in your terminal or, if not possible, fallback to `xdg-open`.

## RC files

You can use an RC file to automatically run any sequence of valid Offpunk
commands upon start up. This can be used to make settings controlled with the
`set`, `handler` or `themes` commands persistent. You can also put a `go` command in
your RC file to visit a "homepage" automatically on startup, or to pre-prepare
a `tour` of your favourite Gemini sites or `offline` to go offline by default.

The RC file should be called `offpunkrc` and goes in $XDG_CONFIG_DIR/offpunk (or .config/offpunk or .offpunk if xdg not available). In that file, simply write one command per line, just like you would type them in offpunk.

## Cache design

The offline content is stored in ~/.cache/offpunk/ as plain .gmi/.html files. The structure of the Gemini-space is tentatively recreated. One key element of the design is to avoid any database. The cache can thus be modified by hand, content can be removed, used or added by software other than offpunk.

The cache can be accessed/built with the `netcache` tool. See `netcache -h` for more informations.

There’s no feature to automatically trim the cache. But any part of the cache can safely be removed manually as there are no databases or complex synchronisation.

## Tests

Be sure to install the dev requirements (`pytest` and `pytest-mock`) with:

    pip install -r requirements-dev.txt

And then run the test suite using `pytest`.
offpunk-v3.0/ansicat.py000077500000000000000000002340351514232770500152260ustar00rootroot00000000000000#!/usr/bin/env python3
import argparse
import base64
import fnmatch
import html
import mimetypes
import os
import shutil
import subprocess
import sys
import textwrap
import time
import urllib
import gettext

import netcache
import offthemes
import unmerdify
from offutils import is_local, looks_like_base64, looks_like_url, run, term_width, xdg, _LOCALE_DIR, find_root, is_url_blocked, urlify

gettext.bindtextdomain('offpunk', _LOCALE_DIR)
gettext.textdomain('offpunk')
_ = gettext.gettext

try:
    from readability import Document

    _HAS_READABILITY = True
except ModuleNotFoundError:
    _HAS_READABILITY = False

try:
    # if bs4 version >= 4.11, we need to silent some xml warnings
    import bs4
    from bs4 import BeautifulSoup, Comment

    version = bs4.__version__.split(".")
    recent = False
    if int(version[0]) > 4:
        recent = True
    elif int(version[0]) == 4:
        recent = int(version[1]) >= 11
    if recent:
        # As this is only for silencing some warnings, we fail
        # silently. We don’t really care
        try:
            import warnings

            from bs4 import XMLParsedAsHTMLWarning

            warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
        except Exception:
            pass
    _HAS_SOUP = True
except ModuleNotFoundError:
    _HAS_SOUP = False

_DO_HTML = _HAS_SOUP  # and _HAS_READABILITY
if _DO_HTML and not _HAS_READABILITY:
    print(_("To improve your web experience (less cruft in webpages),"))
    print(_("please install python3-readability or readability-lxml"))

try:
    import feedparser

    _DO_FEED = True
except ModuleNotFoundError:
    _DO_FEED = False

_HAS_TIMG = False
_HAS_CHAFA = False
_RENDER_IMAGE = False

# All this code to know if we render image inline or not
#Do we have chafa >= 1.10 ?
if shutil.which("chafa"):
    # starting with 1.10, chafa can return only one frame
    # we thus requires chafa to be at least 1.10
    # output is "Chafa version M.m.p"
    # check for m < 1.10
    try:
        output = run("chafa --version")
        chafa_major, chafa_minor, rest = output.split("\n")[0].split(" ")[-1].split(".")
        if int(chafa_major) >= 1 and int(chafa_minor) >= 10:
            _HAS_CHAFA = True
            _RENDER_IMAGE = True
    except Exception:
        pass
#Do we have timg?
if shutil.which("timg"):
    try:
        output = run("timg --version")
    except subprocess.CalledProcessError:
        output = False
    # We don’t deal with timg before 1.3.2 (looping options)
    if output and output[5:10] > "1.3.2":
        _HAS_TIMG = True
        _RENDER_IMAGE = True
if not _RENDER_IMAGE:
    print(_("To render images inline, you need either chafa >= 1.10 or timg > 1.3.2"))

# return ANSI text that can be show by less
def inline_image(img_file, width):
    # We don’t even try displaying pictures that are not there
    if not os.path.exists(img_file):
        return ""
    # Chafa is faster than timg inline. Let use that one by default
    # But we keep a list of "inlines" (possible commands to use)
    # just in case chafa fails
    inlines = []
    ansi_img = ""
    # We avoid errors by not trying to render non-image files
    if shutil.which("file"):
        mime = run("file -b --mime-type %s", parameter=img_file).strip()
        if "image" not in mime:
            return ansi_img
    if _HAS_CHAFA:
        # -O 0 remove optimisation and allows every line to be the same length
        inlines.append("chafa -O 0 --bg white -t 1 -s %s -f symbols --animate=off")
    if _HAS_TIMG:
        inlines.append("timg --frames=1 -p q -g %sx1000")
    image_success = False
    while not image_success and len(inlines) > 0:
        cmd = inlines.pop(0) % width + " %s"
        try:
            ansi_img = run(cmd, parameter=img_file)
            image_success = True
        except Exception as err:
            ansi_img = "***IMAGE ERROR***\n%s…\n…%s" % (str(err)[:50], str(err)[-50:])
    return ansi_img


def terminal_image(img_file):
    # This code will try chafa first and, if it fails, try timg
    cmds = []
    if _HAS_CHAFA:
        cmds.append("chafa -C on -d 0 --bg white -w 1")
    if _HAS_TIMG:
        cmds.append("timg --loops=1 -C")
    image_success = False
    while not image_success and len(cmds) > 0:
        cmd = cmds.pop(0) + " %s"
        try:
            run(cmd, parameter=img_file, direct_output=True)
            image_success = True
        except Exception as err:
            print(err)

# This function returns a MIME based on the gopher selector
# List available here:
# gopher://spike.nagatha.fr/0/phlog/2025/2025-11-11-07-07-ChatGPT-tells-me-about-Gopher-selectors.txt 
def get_gopher_mime(url):
    parsed = urllib.parse.urlparse(url)
    if parsed.scheme != "gopher":
        mime = mimetypes.guess_type(path)[0]
    elif len(parsed.path) >= 2:
        itemtype = parsed.path[1]
        path = parsed.path[2:]
    else:
        itemtype = "1"
        path = ""
    if itemtype == "0":
        if path.endswith(".xml"):
            mime = "application/xml"
        else:
            mime = "text/gemini"
    elif itemtype == "1":
        mime = "text/gopher"
    elif itemtype == "h":
        mime = "text/html"
    elif itemtype in ("g", "I", "d","p"):
        mime = mimetypes.guess_type(path)[0]
    elif itemtype in ("9", "s", ";"):
        mime = "binary"
    elif itemtype in ("r","X"):
        mime = "application/rss+xml"
    else:
        mime = "text/gopher"
    return mime

# First, we define the different content->text renderers, outside of the rest
# (They could later be factorized in other files or replaced)
class AbstractRenderer:
    def __init__(self, content, url, center=True,redirects={},**kwargs):
        self.url = url
        #base url is used to construct relative urls (see  in html)
        self.base = None
        self.body = str(content)
        # there’s one rendered text and one links table per mode
        self.rendered_text = {}
        self.links = {}
        self.images = {}
        self.title = None
        self.validity = True
        self.temp_files = {}
        self.center = center
        self.last_mode = "readable"
        self.theme = offthemes.default
        self.options = kwargs
        # self.mime should be used only in renderer with multiple mime
        self.mime = None
        # The library used to clean the HTML
        self.cleanlib = _("No cleaning required")
        #url redirections
        self.redirects = redirects

    def display(self, mode=None, directdisplay=False):
        wtitle = self.get_formatted_title()
        if mode == "source":
            body = self.body
        else:
            body = wtitle + "\n" + self.get_body(mode=mode)
            if "linkmode" in self.options:
                # Avaliable linkmode are "none" and "end".
                if self.options["linkmode"] == "end":
                    links = self.get_links(mode=mode)
                    for i in range(len(links)):
                        body += "[%s] %s\n" % (i + 1, links[i])
        if directdisplay:
            print(body)
            return True
        else:
            return body

    #Return True if it should bypass less and access directly the terminal
    def has_direct_display(self):
        return False

    #Return True if it is able to render the content.
    #Return False if the content is of a format not supported by ansicat
    def is_format_supported(self):
        return True

    def set_theme(self, theme):
        if theme:
            self.theme.update(theme)

    def get_theme(self):
        return self.theme

    def set_redirects(self, redirects):
        self.redirects = redirects

    # This class hold an internal representation of the HTML text
    class representation:
        def __init__(self, width, title=None, center=True, theme={},options={}):
            self.title = title
            self.center = center
            self.final_text = ""
            self.opened = []
            self.width = width
            self.last_line = ""
            self.last_line_colors = {}
            self.last_line_center = False
            self.new_paragraph = True
            self.i_indent = ""
            self.s_indent = ""
            self.r_indent = ""
            self.current_indent = ""
            self.disabled_indents = None
            # each color is an [open,close] pair code
            self.theme = theme
            self.options = options
            self.colors = offthemes.colors

        def _insert(self, color, open=True):
            if open:
                o = 0
            else:
                o = 1
            pos = len(self.last_line)
            # we remember the position where to insert color codes
            if pos not in self.last_line_colors:
                self.last_line_colors[pos] = []
            # Two inverse code cancel each other
            if [color, int(not o)] in self.last_line_colors[pos]:
                self.last_line_colors[pos].remove([color, int(not o)])
            else:
                self.last_line_colors[pos].append([color, o])  # +color+str(o))

        # Take self.last line and add ANSI codes to it before adding it to
        # self.final_text.
        def _endline(self):
            if len(self.last_line.strip()) > 0:
                for c in self.opened:
                    self._insert(c, open=False)
                nextline = ""
                added_char = 0
                # we insert the color code at the saved positions
                while len(self.last_line_colors) > 0:
                    pos, colors = self.last_line_colors.popitem()
                    # popitem itterates LIFO.
                    # So we go, backward, to the pos (starting at the end of last_line)
                    nextline = self.last_line[pos:] + nextline
                    ansicol = "\x1b["
                    for c, o in colors:
                        ansicol += self.colors[c][o] + ";"
                    ansicol = ansicol[:-1] + "m"
                    nextline = ansicol + nextline
                    added_char += len(ansicol)
                    self.last_line = self.last_line[:pos]
                nextline = self.last_line + nextline
                if self.last_line_center:
                    # we have to care about the ansi char while centering
                    width = term_width() + added_char
                    nextline = nextline.strip().center(width)
                    self.last_line_center = False
                else:
                    # should we lstrip the nextline in the addition ?
                    # nextline.lstrip() is breaking AsciiArt and I don’t remember
                    # why it is there. Trying to replace it with a "rstrip"
                    nextline = self.current_indent + nextline.rstrip() + self.r_indent
                    self.current_indent = self.s_indent
                self.final_text += nextline
                self.last_line = ""
                self.final_text += "\n"
                for c in self.opened:
                    self._insert(c, open=True)
            else:
                self.last_line = ""

        def center_line(self):
            self.last_line_center = True

        def open_theme(self, element):
            if element in self.theme:
                colors = self.theme[element]
                for c in colors:
                    self.open_color(c)
                return True
            else:
                return False

        def close_theme(self, element):
            if element in self.theme:
                colors = self.theme[element]
                for c in colors:
                    self.close_color(c)

        def open_color(self, color):
            if color in self.colors and color not in self.opened:
                self._insert(color, open=True)
                self.opened.append(color)

        def close_color(self, color):
            if color in self.colors and color in self.opened:
                self._insert(color, open=False)
                self.opened.remove(color)

        def close_all(self):
            if len(self.colors) > 0:
                self.last_line += "\x1b[0m"
                self.opened.clear()

        def startindent(self, indent, sub=None, reverse=None):
            self._endline()
            self.i_indent = indent
            self.current_indent = indent
            if sub:
                self.s_indent = sub
            else:
                self.s_indent = indent
            if reverse:
                self.r_indent = reverse
            else:
                self.r_indent = ""

        def endindent(self):
            self._endline()
            self.i_indent = ""
            self.s_indent = ""
            self.r_indent = ""
            self.current_indent = ""

        def _disable_indents(self):
            self.disabled_indents = []
            self.disabled_indents.append(self.current_indent)
            self.disabled_indents.append(self.i_indent)
            self.disabled_indents.append(self.s_indent)
            self.disabled_indents.append(self.r_indent)
            self.endindent()

        def _enable_indents(self):
            if self.disabled_indents:
                self.current_indent = self.disabled_indents[0]
                self.i_indent = self.disabled_indents[1]
                self.s_indent = self.disabled_indents[2]
                self.r_indent = self.disabled_indents[3]
            self.disabled_indents = None

        def newline(self):
            self._endline()

        # A new paragraph implies 2 newlines (1 blank line between paragraphs)
        # But it is only used if didn’t already started one to avoid plenty
        # of blank lines. force=True allows to bypass that limit.
        # new_paragraph becomes false as soon as text is entered into it
        def newparagraph(self, force=False):
            if force or not self.new_paragraph:
                self._endline()
                self.final_text += "\n"
                self.new_paragraph = True

        def add_space(self):
            if len(self.last_line) > 0 and self.last_line[-1] != " ":
                self.last_line += " "

        def _title_first(self, intext=None):
            if self.title:
                if not self.title == intext:
                    self._disable_indents()
                    self.open_theme("title")
                    self.add_text(self.title)
                    self.close_all()
                    self.newparagraph()
                    self._enable_indents()
                self.title = None

        # Beware, blocks are not wrapped nor indented and left untouched!
        # They are mostly useful for pictures and preformatted text.
        def add_block(self, intext, theme=None, preformat_wrap=False):
            # If necessary, we add the title before a block
            self._title_first()
            # we don’t want to indent blocks
            self._endline()
            self._disable_indents()
            # we have to apply the theme for every line in the intext
            # applying theme to preformatted is controversial as it could change it
            # We wrap preformatted text if requested or if it is set in the option
            if "preformat_wrap" in self.options:
                preformwrap = preformat_wrap or self.options["preformat_wrap"]
            else:
                preformwrap = preformat_wrap
            if theme:
                block = ""
                lines = intext.split("\n")
                for l in lines:
                    self.open_theme(theme)
                    if preformwrap:
                        self.add_text(l)
                    else:
                        self.last_line += self.current_indent + l
                    self.close_theme(theme)
                    self._endline()
                self.last_line += "\n"
            # one thing is sure : we need to keep unthemed blocks for images!
            else:
                self.final_text += self.current_indent + intext
                self.new_paragraph = False
                self._endline()
            self._enable_indents()

        def add_text(self, intext):
            self._title_first(intext=intext)
            lines = []
            last = self.last_line + intext
            self.last_line = ""
            # With the following, we basically cancel adding only spaces
            # on an empty line
            if len(last.strip()) > 0:
                self.new_paragraph = False
            else:
                last = last.strip()
            if len(last) > self.width:
                width = self.width - len(self.current_indent) - len(self.r_indent)
                spaces_left = len(last) - len(last.lstrip())
                spaces_right = len(last) - len(last.rstrip())
                lines = textwrap.wrap(last, width, drop_whitespace=True)
                self.last_line += spaces_left * " "
                while len(lines) > 1:
                    l = lines.pop(0)
                    self.last_line += l
                    self._endline()
                if len(lines) == 1:
                    li = lines[0]
                    self.last_line += li + spaces_right * " "
            else:
                self.last_line = last

        def get_final(self):
            self.close_all()
            self._endline()
            # if no content, we still add the title
            self._title_first()
            lines = self.final_text.splitlines()
            lines2 = []
            termspace = shutil.get_terminal_size()[0]
            # Following code instert blanck spaces to center the content
            if self.center and termspace > term_width():
                margin = int((termspace - term_width()) // 2)
            else:
                margin = 0
            for l in lines:
                lines2.append(margin * " " + l)
            return "\n".join(lines2)

    def get_subscribe_links(self):
        return [[self.url, self.get_mime(), self.get_title()]]

    def is_valid(self):
        return self.validity

    def set_mode(self, mode):
        self.last_mode = mode

    def get_mode(self):
        return self.last_mode

    def get_cleanlib(self):
        return self.cleanlib

    def get_link(self, nb):
        links = self.get_links()
        if nb not in range(1, len(links)+1):
            print(_("%s is not a valid link for %s") % (nb, self.url))
            return 0
        else:
            return links[nb - 1]

    # get_title is about the "content title", so the title in the page itself
    def get_title(self):
        return "Abstract title"

    def get_page_title(self):
        title = self.get_title()
        if not title or len(title) == 0:
            title = self.get_url_title()
        else:
            title += " (%s)" % self.get_url_title()
        return title

    def get_formatted_title(self,linksnbr=True):
        title = self.get_url_title()
        nbr = len(self.get_links())
        if is_local(self.url):
            title += " (%s items)" % nbr
            str_last = "local file"
        else:
            str_last = "last accessed on %s" % time.ctime(
                netcache.cache_last_modified(self.url)
            )
            if linksnbr:
                title += " (%s links)" % nbr
        return self._window_title(title, info=str_last)

    # this function is about creating a title derived from the URL
    def get_url_title(self):
        # small intelligence to try to find a good name for a capsule
        # we try to find eithe ~username or /users/username
        # else we fallback to hostname
        if not self.url:
            return ""
        if is_local(self.url):
            splitpath = self.url.split("/")
            filename = splitpath[-1]
            return filename
        return find_root(self.url,return_value="name")

    # This function return a list of URL which should be downloaded
    # before displaying the page (images in HTML pages, typically)
    def get_images(self, mode=None):
        if not mode:
            mode = self.last_mode
        if mode not in self.images:
            self.get_body(mode=mode)
            # we also invalidate the body that was done without images
            self.rendered_text.pop(mode)
        if mode in self.images:
            return self.images[mode]
        else:
            return []

    # This function will give gemtext to the gemtext renderer
    def prepare(self, body, mode=None):
        return [[body, None]]

    def _build_body_and_links(self, mode, width=None):
        if not width:
            width = term_width()
        prepared_bodies = self.prepare(self.body, mode=mode)
        self.rendered_text[mode] = ""
        self.links[mode] = []
        for b in prepared_bodies:
            results = None
            size = len(self.links[mode])
            if b[1] in _FORMAT_RENDERERS:
                r = _FORMAT_RENDERERS[b[1]](b[0], self.url, center=self.center)
                results = r.render(b[0], width=width, mode=mode, startlinks=size)
            else:
                results = self.render(b[0], width=width, mode=mode, startlinks=size)
            if results:
                self.rendered_text[mode] += results[0] + "\n"
                # we should absolutize all URLs here
                for l in results[1]:
                    ll = l.split()
                    if len(ll) > 0:
                        try:
                            abs_l = urllib.parse.urljoin(self.url, ll[0])
                        except Exception:
                            print(_(
                                "Urljoin Error: Could not make an URL out of %s and %s"
                                % (self.url, ll)
                                ))
                    else:
                        abs_l = self.url
                    self.links[mode].append(abs_l)
                #for l in self.get_subscribe_links()[1:]:
                #    self.links[mode].append(l[0])

    def get_body(self, width=None, mode=None):
        if not mode:
            mode = self.last_mode
        if mode not in self.rendered_text:
            self._build_body_and_links(mode, width)
        return self.rendered_text[mode]

    def get_links(self, mode=None):
        if not mode:
            mode = self.last_mode
        if mode not in self.links:
            self._build_body_and_links(mode)
        return self.links[mode]

    def _window_title(self, title, info=None):
        title_r = self.representation(term_width(), theme=self.theme,options=self.options)
        title_r.open_theme("window_title")
        title_r.add_text(title)
        title_r.close_theme("window_title")
        if info:
            title_r.open_theme("window_subtitle")
            title_r.add_text("   (%s)" % info)
            title_r.close_theme("window_subtitle")
        return title_r.get_final()

    # An instance of AbstractRenderer should have a 
    # self.render(body,width=,mode=,starlinks=0) method.
    # It returns a tuple (rendered_body,[list of links])
    # 3 modes are used : readable (by default), full and links_only (the fastest, when
    # rendered content is not used, only the links are needed)
    # The prepare() function is called before the rendering. It is useful if
    # your renderer output in a format suitable for another existing renderer (such as gemtext)
    # The prepare() function output a list of tuple. Each tuple is [output text, format] where
    # format should be in _FORMAT_RENDERERS. If None, current renderer is used


# A renderer for format that are not supported
class FakeRenderer(AbstractRenderer):
    def set_mime(self,mime):
        self.mime = mime
    def get_mime(self):
        return self.mime
    def get_title(self):
        filename = self.url.split("/")[-1]
        if not filename:
            filename = self.url
        return filename

    def is_format_supported(self):
        return False

    def render(self,body,width=None,**kwargs):
        gemtext = "\n"
        gemtext += "File %s is of format %s.\n"%(self.get_title(),self.mime)
        gemtext += "It cannot be rendered in your terminal.\n"
        gemtext += "Use \"open\" to open the file using an external handler"
        r = self.representation(width, theme=self.theme,options=self.options)
        for line in gemtext.splitlines():
            r.newline()
            if len(line.strip()) == 0:
                r.newparagraph(force=True)
            else:
                r.add_text(line.rstrip())
        return r.get_final(), []

class PlaintextRenderer(AbstractRenderer):
    def get_mime(self):
        return "text/plain"

    def get_title(self):
        if self.title:
            return self.title
        elif self.body:
            lines = self.body.splitlines()
            if len(lines) > 0:
                # If not title found, we take the first 50 char
                # of the first line
                title_line = lines[0].strip()
                if len(title_line) > 50:
                    title_line = title_line[:49] + "…"
                self.title = title_line
                return self.title
            else:
                self.title = "Empty Page"
                return self.title
        else:
            return "(unknown)"

    def render(self, gemtext, width=None, mode=None, startlinks=0):
        r = self.representation(width, theme=self.theme,options=self.options)
        links = []
        for line in gemtext.splitlines():
            r.newline()
            if len(line.strip()) == 0:
                r.newparagraph(force=True)
            else:
                if "://" in line:
                    words = line.split()
                    for w in words:
                        if "://" in w and looks_like_url(w):
                            links.append(w)
                r.add_text(line)
        return r.get_final(), links


# Gemtext Rendering Engine
class GemtextRenderer(AbstractRenderer):
    def get_mime(self):
        return "text/gemini"

    def get_title(self):
        if self.title:
            return self.title
        elif self.body:
            lines = self.body.splitlines()
            for line in lines:
                if line.startswith("#"):
                    self.title = line.strip("#").strip()
                    return self.title
            if len(lines) > 0:
                # If not title found, we take the first 50 char
                # of the first line
                title_line = lines[0].strip()
                if len(title_line) > 50:
                    title_line = title_line[:49] + "…"
                self.title = title_line
                return self.title
            else:
                self.title = "Empty Page"
                return self.title
        else:
            return "(unknown)"

    # render_gemtext
    def render(self, gemtext, width=None, mode=None, startlinks=0):
        if not width:
            width = term_width()
        r = self.representation(width, theme=self.theme,options=self.options)
        links = []
        hidden_links = []
        preformatted = False

        def format_link(url, index, name=None):
            if "://" in url:
                protocol, adress = url.split("://", maxsplit=1)
                protocol = " %s" % protocol
            else:
                adress = url
                protocol = ""
            if "gemini" in protocol or "list" in protocol:
                protocol = ""
            if not name:
                name = adress
            line = "[%d%s] %s" % (index, protocol, name)
            return line

        for line in gemtext.splitlines():
            r.newline()
            if line.startswith("```"):
                preformatted = not preformatted
                if preformatted:
                    r.open_theme("preformatted")
                else:
                    r.close_theme("preformatted")
            elif preformatted:
                # infinite line to not wrap preformated
                r.add_block(line + "\n", theme="preformatted")
            elif len(line.strip()) == 0:
                r.newparagraph(force=True)
            elif line.startswith("=>"):
                strippedline = line[2:].strip()
                if strippedline:
                    links.append(strippedline)
                    splitted = strippedline.split(maxsplit=1)
                    url = splitted[0]
                    # We join with current root in case it is relative
                    abs_url = urllib.parse.urljoin(self.url, url )
                    name = None
                    if len(splitted) > 1:
                        name = splitted[1]
                    link = format_link(url, len(links) + startlinks, name=name)
                    # If the link point to a page that has been cached less than
                    # 600 seconds after this page, we consider it as a new_link
                    current_modif = netcache.cache_last_modified(self.url)
                    link_modif = netcache.cache_last_modified(url)
                    # Let’s see first if this is a picture
                    image_displayed = False
                    if (
                        _RENDER_IMAGE
                        and not self.url.startswith("list://")
                        # check if images are enabled in Gemini!
                        and "gemini_images" in self.options.keys()
                        and self.options["gemini_images"]
                        # Check that it looks like an image
                       # and link_modif  # There’s a valid cache for link target 
                        and url[-4:].lower() in [".jpg",".png",".gif","jpeg"] 
                        and netcache.is_cache_valid(abs_url)
                    ):
                        ansi_img = ""
                        try:
                            # 4 followings line are there to translate the URL into cache path
                            img = netcache.get_cache_path(abs_url)
                            renderer = ImageRenderer(img, abs_url)
                            # Image width is set in the option to 40 by default
                            # it cannot be bigger than the width of the text
                            if "images_size" in self.options.keys() and width and \
                                                width > self.options["images_size"] :
                                size = self.options["images_size"]
                            else:
                                size = width
                            ansi_img += renderer.get_body(width=size, mode="inline")
                            image_displayed = True
                        except Exception as err:
                            # we sometimes encounter really bad formatted files or URL
                            # we fall back to normal links in that case
                            image_displayed = False
                        r.add_block(ansi_img)
                        r.open_theme("image_link")
                        r.center_line()
                        theme = "image_link"
                    #theme for blocked URL
                    elif is_url_blocked(url,self.redirects) \
                         and r.open_theme("blocked_link"):
                        theme = "blocked_link"
                    #theme for recently updated URL
                    elif (
                        current_modif
                        and link_modif
                        and current_modif - link_modif < 600
                        and r.open_theme("new_link")
                    ):
                        theme = "new_link"
                    elif r.open_theme("oneline_link"):
                        theme = "oneline_link"
                    else:
                        theme = "link"
                        r.open_theme("link")
                    startpos = link.find("] ") + 2
                    r.startindent("", sub=startpos * " ")
                    r.add_text(link)
                    r.close_theme(theme)
                    r.endindent()
            elif line.startswith("* "):
                line = line[1:].lstrip("\t ")
                r.startindent("• ", sub="  ")
                r.add_text(line)
                r.endindent()
            elif line.startswith(">"):
                line = line[1:].lstrip("\t ")
                r.startindent("> ")
                r.open_theme("blockquote")
                r.add_text(line)
                r.close_theme("blockquote")
                r.endindent()
            elif line.startswith("###"):
                line = line[3:].lstrip("\t ")
                if r.open_theme("subsubtitle"):
                    theme = "subsubtitle"
                else:
                    r.open_theme("subtitle")
                    theme = "subtitle"
                r.add_text(line)
                r.close_theme(theme)
            elif line.startswith("##"):
                line = line[2:].lstrip("\t ")
                r.open_theme("subtitle")
                r.add_text(line)
                r.close_theme("subtitle")
            elif line.startswith("#"):
                line = line[1:].lstrip("\t ")
                if not self.title:
                    self.title = line
                r.open_theme("title")
                r.add_text(line)
                r.close_theme("title")
            else:
                if "://" in line:
                    words = line.split()
                    for w in words:
                        if "://" in w and looks_like_url(w):
                            hidden_links.append(w)
                r.add_text(line.rstrip())
        links += hidden_links
        return r.get_final(), links


class EmptyRenderer(GemtextRenderer):
    def get_mime(self):
        return "text/empty"

    def prepare(self, body, mode=None):
        text = "(empty file)"
        return [[text, "GemtextRenderer"]]


class GopherRenderer(AbstractRenderer):
    def get_mime(self):
        return "text/gopher"

    def get_title(self):
        if not self.title:
            self.title = ""
            if self.body:
                firstline = self.body.splitlines()[0]
                firstline = firstline.split("\t")[0]
                if firstline.startswith("i"):
                    firstline = firstline[1:]
                self.title = firstline
        return self.title

    # menu_or_text
    def render(self, body, width=None, mode=None, startlinks=0):
        if not width:
            width = term_width()
        try:
            render, links = self._render_goph(
                body, width=width, mode=mode, startlinks=startlinks
            )
        except Exception as err:
            print(_("Error rendering Gopher "), err)
            r = self.representation(width, theme=self.theme,options=self.options)
            r.add_block(body)
            render = r.get_final()
            links = []
        return render, links

    def _render_goph(self, body, width=None, mode=None, startlinks=0):
        if not width:
            width = term_width()
        # This was copied straight from Agena (then later adapted)
        links = []
        r = self.representation(width, theme=self.theme,options=self.options)
        for line in self.body.split("\n"):
            r.newline()
            if line.startswith("i"):
                towrap = line[1:].split("\t")[0]
                if len(towrap.strip()) > 0:
                    r.add_block(towrap+"\n")
                else:
                    r.newparagraph()
            elif line.strip() not in [".", ""]:
                parts = line.split("\t")
                parts[-1] = parts[-1].strip()
                if parts[-1] == "+":
                    parts = parts[:-1]
                if len(parts) == 4:
                    name, path, host, port = parts
                    # If line starts with TAB, there’s no name.
                    # We thus hide this line
                    if name:
                        itemtype = name[0].strip("/")
                        name = name[1:]
                        if port == "70":
                            port = ""
                        else:
                            port = ":%s" % port
                        if itemtype == "h" and path.startswith("URL:"):
                            url = path[4:]
                        else:
                            # some gophermap lines include a selector without a leading "/"
                            # gopher://some.domain/1phlog/ is valid
                            # this is perfectly valid, and offpunk shouldn't modify the selectors
                            # if not path.startswith("/") and itemtype:
                            #     path = "/" + path
                            url = "gopher://%s%s/%s%s" % (host, port, itemtype, path)
                        linkline = url + " " + name
                        links.append(linkline)
                        number = len(links) + startlinks
                        protocol = ""
                        if not url.startswith("gopher"):
                            protocol = " " + url.split("://")[0]
                        towrap = "[%s%s] " % (str(number), protocol) + name
                        # If the link point to a page that has been cached less than
                        # 600 seconds after this page, we consider it as a new_link
                        current_modif = netcache.cache_last_modified(self.url)
                        link_modif = netcache.cache_last_modified(url)
                        if (
                            current_modif
                            and link_modif
                            and current_modif - link_modif < 600
                            and r.open_theme("new_link")
                        ):
                            theme = "new_link"
                        elif r.open_theme("oneline_link"):
                            theme = "oneline_link"
                        else:
                            theme = "link"
                            r.open_theme("link")
                        r.add_text(towrap)
                        r.close_theme(theme)
                else:
                    r.add_text(line)
        return r.get_final(), links


class FolderRenderer(GemtextRenderer):
    # it was initialized with:
    # self.renderer = FolderRenderer("",self.get_cache_path(),datadir=xdg("data"))
    def __init__(self, content, url, center=True, datadir=None):
        GemtextRenderer.__init__(self, content, url, center)
        self.datadir = datadir

    def get_mime(self):
        return "Directory"

    def prepare(self, body, mode=None):
        def get_first_line(l):
            path = os.path.join(listdir, l + ".gmi")
            with open(path) as f:
                first_line = f.readline().strip()
                f.close()
            if first_line.startswith("#"):
                return first_line
            else:
                return None

        def write_list(l):
            body = ""
            for li in l:
                #making sure we don’t write ".gmi"
                if l != "":
                    path = "list:///%s" % li
                    r = renderer_from_file(netcache.get_cache_path(path))
                    size = len(r.get_links())
                    body += "=> %s %s (%s items)\n" % (str(path), li, size)
            return body

        listdir = os.path.join(self.datadir, "lists")
        self.title = "My lists"
        lists = []
        if os.path.exists(listdir):
            listfiles = os.listdir(listdir)
            if len(listfiles) > 0:
                for l in listfiles:
                    #We only take gmi files
                    if l.endswith(".gmi"):
                        # removing the .gmi at the end of the name
                        lists.append(l[:-4])
        if len(lists) > 0:
            body = ""
            my_lists = []
            system_lists = []
            subscriptions = []
            frozen = []
            lists.sort()
            for l in lists:
                # we don’t do anything with home which is ".gmi" thus ""
                if l in ["history", "to_fetch", "archives", "tour"]:
                    system_lists.append(l)
                elif l != "":
                    first_line = get_first_line(l)
                    if first_line and "#subscribed" in first_line:
                        subscriptions.append(l)
                    elif first_line and "#frozen" in first_line:
                        frozen.append(l)
                    else:
                        my_lists.append(l)
            if len(my_lists) > 0:
                body += _("\n## Bookmarks Lists (updated during sync)\n")
                body += write_list(my_lists)
            if len(subscriptions) > 0:
                body += _("\n## Subscriptions (new links in those are added to tour)\n")
                body += write_list(subscriptions)
            if len(frozen) > 0:
                body += _("\n## Frozen (fetched but never updated)\n")
                body += write_list(frozen)
            if len(system_lists) > 0:
                body += _("\n## System Lists\n")
                body += write_list(system_lists)
            return [[body, None]]


class FeedRenderer(GemtextRenderer):
    def get_mime(self):
        return "application/rss+xml"

    def is_valid(self):
        if _DO_FEED:
            try:
                parsed = feedparser.parse(self.body)
            except Exception:
                parsed = False
        else:
            return False
        if not parsed:
            return False
        elif parsed.bozo:
            #print("bozo "+str(parsed.bozo_exception))
            return False
        else:
            # If the second element is ")[1].startswith(" 0

    def get_title(self):
        if not self.title:
            self.get_body()
        return self.title

    def prepare(self, content, mode=None, width=None):
        if not mode:
            mode = self.last_mode
        if not width:
            width = term_width()
        self.title = "RSS/Atom feed"
        toreturn = []
        page = ""
        if _DO_FEED:
            parsed = feedparser.parse(content)
        else:
            page += "Please install python-feedparser to handle RSS/Atom feeds\n"
            self.validity = False
            return page
        if parsed.bozo:
            page += "Invalid RSS feed\n\n"
            page += str(parsed.bozo_exception)
            self.validity = False
        else:
            if "title" in parsed.feed:
                t = parsed.feed.title
            else:
                t = "Unknown"
            self.title = "%s (XML feed)" % t
            title = "# %s" % self.title
            page += title + "\n"
            if "updated" in parsed.feed:
                page += "Last updated on %s\n\n" % parsed.feed.updated
            if "subtitle" in parsed.feed:
                page += parsed.feed.subtitle + "\n"
            if "link" in parsed.feed:
                page += "=> %s\n" % parsed.feed.link
            page += "\n## Entries\n"
            toreturn.append([page, None])
            if len(parsed.entries) < 1:
                self.validity = False
            postslist = ""
            for i in parsed.entries:
                if "link" in i:
                    line = "=> %s " % i.link
                elif "links" in i and len(i.links) > 0:
                    link = None
                    j = 0
                    while not link and j < len(i.links):
                        link = i.links[j].href
                    if link:
                        line = "=> %s " % link
                    else:
                        line = "* "
                else:
                    line = "* "
                if "published" in i:
                    # sometimes fails so protect it
                    try:
                        pub_date = time.strftime("%Y-%m-%d", i.published_parsed)
                        line += pub_date + " : "
                    except Exception:
                        pass
                if "title" in i:
                    line += "%s" % (i.title)
                if "author" in i:
                    line += " (by %s)" % i.author
                if mode == "full":
                    toreturn.append([line, None])
                    if "summary" in i:
                        toreturn.append([i.summary, "text/html"])
                        toreturn.append(["------------", None])
                else:
                    postslist += line + "\n"
            # If each posts is append to toreturn, a \n is inserted
            # between each item of the list. I don’t like it. Hence this hack
            if mode != "full":
                toreturn.append([postslist, None])
        return toreturn


class ImageRenderer(AbstractRenderer):
    def get_mime(self):
        return "image/*"

    def is_valid(self):
        if _RENDER_IMAGE:
            return True
        else:
            return False

    def get_links(self, mode=None):
        return []

    def get_title(self):
        return "Picture file"

    def render(self, img, width=None, mode=None, startlinks=0):
        # with inline, we use symbols to be rendered with less.
        # else we use the best possible renderer.
        if mode in ["full_links_only", "links_only"]:
            return "", []
        if not width:
            width = term_width()
            spaces = 0
        else:
            spaces = int((term_width() - width) // 2)
        ansi_img = inline_image(img, width)
        # Now centering the image
        lines = ansi_img.splitlines()
        new_img = ""
        # What if the picture is smaller than requested width?
        # We try to measure it with the number of "m" or " " in the longest line.
        # when not optimized, there are always 2 m per symbols
        # Yes, this is a naughty ANSI hack
        longestline = 0
        for l in lines:
            linelength = l.count("[0m")
            #print("DEBUG: linelength "+str(linelength))
            if linelength > longestline: longestline = linelength
        #print("DEBUG: longestline: "+str(longestline))
        newspaces = (width - longestline) //2 + 1
        #print("DEBUG: newspaces: "+str(newspaces))
        if newspaces > spaces and longestline < width:
            spaces = newspaces
        #print("DEBUG: spaces: "+str(spaces))
        for l in lines:
            new_img += spaces * " " + l + "\n"
        return new_img, []

    def has_direct_display(self):
        return _RENDER_IMAGE

    def display(self, mode=None, directdisplay=False):
        wtitle = self.get_formatted_title()
        if not directdisplay:
            body = wtitle + "\n" + self.get_body(mode=mode)
            return body
        else:
            print(self._window_title(wtitle))
            terminal_image(self.body)
            return True

class HtmlRenderer(AbstractRenderer):
    def get_mime(self):
        return "text/html"

    def is_valid(self):
        if not _DO_HTML:
            print(
                _("HTML document detected. Please install python-bs4 and python-readability.")
            )
        return _DO_HTML and self.validity

    def get_subscribe_links(self):
        subs = []
        if _DO_HTML :
            subs = [[self.url, self.get_mime(), self.get_title()]]
            soup = BeautifulSoup(self.body, "html.parser")
            links = soup.find_all("link", rel="alternate", recursive=True)
            for l in links:
                ty = l.get("type")
                if ty:
                    if "rss" in ty or "atom" in ty or "feed" in ty:
                        # some rss links are relatives: we absolutise_url
                        sublink = urllib.parse.urljoin(self.url, l.get("href"))
                        subs.append([sublink, ty, l.get("title")])
        return subs

    def get_title(self):
        if self.title:
            return self.title
        elif _DO_HTML and self.body:
            if _HAS_READABILITY:
                try:
                    readable = Document(self.body)
                    self.title = readable.short_title()
                    return self.title
                except Exception:
                    pass
            soup = BeautifulSoup(self.body, "html.parser")
            if soup.title:
                self.title = str(soup.title.string)
            else:
                self.title = ""
            return self.title
        else:
            return ""

    def get_base_url(self):
        if not self.base :
            if _DO_HTML and self.body:
                soup = BeautifulSoup(self.body, "html.parser")
                if soup.base :
                    base = soup.base.get("href")
                    self.base = urllib.parse.urljoin(self.url,base)
                else:
                    self.base = self.url
            else:
                self.base = self.url
        return self.base
    # Our own HTML engine (crazy, isn’t it?)
    # Return [rendered_body, list_of_links]
    # mode is either links_only, readable or full
    def render(self, body, mode=None, width=None, add_title=True, startlinks=0):
        if not mode:
            mode = self.last_mode
        if not width:
            width = term_width()
        if not _DO_HTML:
            print(
                _("HTML document detected. Please install python-bs4 and python-readability.")
            )
            return
        # This method recursively parse the HTML
        r = self.representation(
            width, title=self.get_title(), center=self.center, theme=self.theme
            ,options=self.options)
        links = []
        # You know how bad html is when you realize that space sometimes meaningful, somtimes not.
        # CR are not meaniningful. Except that, somethimes, they should be interpreted as spaces.
        # HTML is real crap. At least the one people are generating.

        def render_image(src, width=None, mode=None):
            ansi_img = ""
            imgurl, imgdata = looks_like_base64(src, self.get_base_url())
            if (
                _RENDER_IMAGE
                and mode not in ["full_links_only", "links_only"]
                and imgurl
            ):
                try:
                    # 4 followings line are there to translate the URL into cache path
                    img = netcache.get_cache_path(imgurl)
                    if imgdata:
                        os.makedirs(os.path.dirname(img), exist_ok=True)
                        with open(img, "wb") as cached:
                            cached.write(base64.b64decode(imgdata))
                            cached.close()
                    if netcache.is_cache_valid(img):
                        renderer = ImageRenderer(img, imgurl)
                        # Image width is set in the option to 40 by default
                        # it cannot be bigger than the width of the text
                        if "images_size" in self.options.keys() and width and \
                                            width > self.options["images_size"] :
                            size = self.options["images_size"]
                        else:
                            size = width
                        ansi_img = "\n" + renderer.get_body(width=size, mode="inline")
                except Exception as err:
                    # we sometimes encounter really bad formatted files or URL
                    ansi_img = (
                        textwrap.fill("[BAD IMG] %s - %s" % (err, src), width) + "\n"
                    )
            return ansi_img

        def sanitize_string(string):
            # never start with a "\n"
            # string = string.lstrip("\n")
            string = string.replace("\r", "").replace("\n", " ").replace("\t", " ")
            #now we replace the rarely found Start of guarded area
            string = string.replace("\x96","–").replace("\x91","'")
            # remove soft hyphens
            string = string.replace("\u00ad","")
            endspace = string.endswith(" ") or string.endswith("\xa0")
            startspace = string.startswith(" ") or string.startswith("\xa0")
            toreturn = string.replace("\n", " ").replace("\t", " ").strip()
            while "  " in toreturn:
                toreturn = toreturn.replace("  ", " ")
            toreturn = html.unescape(toreturn)
            if (
                endspace
                and not toreturn.endswith(" ")
                and not toreturn.endswith("\xa0")
            ):
                toreturn += " "
            if (
                startspace
                and not toreturn.startswith(" ")
                and not toreturn.startswith("\xa0")
            ):
                toreturn = " " + toreturn
            return toreturn

        def recursive_render(element, indent="", preformatted=False):
            if element.name in ["blockquote", "dd"]:
                r.newparagraph()
                r.startindent("   ", reverse="     ")
                for child in element.children:
                    r.open_theme("blockquote")
                    recursive_render(child, indent="\t")
                    r.close_theme("blockquote")
                r.endindent()
            elif element.name in ["div", "p", "dt"]:
                r.newparagraph()
                for child in element.children:
                    recursive_render(child, indent=indent)
                r.newparagraph()
            elif element.name in ["span"]:
                r.add_space()
                for child in element.children:
                    recursive_render(child, indent=indent)
                r.add_space()
            elif element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
                if element.name in ["h1"]:
                    r.open_theme("title")
                elif element.name in ["h2", "h3"]:
                    r.open_theme("subtitle")
                elif element.name in ["h4", "h5", "h6"]:
                    if not r.open_theme("subsubtitle"):
                        r.open_theme("subtitle")
                r.newparagraph()
                for child in element.children:
                    recursive_render(child)
                    # r.close_all()
                r.close_all()
                r.newparagraph()
            elif element.name in ["code", "tt","abbr"]:
                for child in element.children:
                    recursive_render(child, indent=indent, preformatted=True)
            elif element.name in ["pre"]:
                r.newparagraph()
                r.add_block(element.text,theme="preformatted",preformat_wrap=True)
                r.newparagraph(force=True)
            elif element.name in ["li"]:
                r.startindent(" • ", sub="   ")
                for child in element.children:
                    recursive_render(child, indent=indent)
                r.endindent()
            elif element.name in ["tr"]:
                r.startindent("|", reverse="|")
                for child in element.children:
                    recursive_render(child, indent=indent)
                r.endindent()
            elif element.name in ["td", "th"]:
                r.add_text("| ")
                for child in element.children:
                    recursive_render(child)
                r.add_text(" |")
            # italics
            elif element.name in ["em", "i"]:
                r.open_color("italic")
                for child in element.children:
                    recursive_render(child, indent=indent, preformatted=preformatted)
                r.close_color("italic")
            # bold
            elif element.name in ["b", "strong"]:
                r.open_color("bold")
                for child in element.children:
                    recursive_render(child, indent=indent, preformatted=preformatted)
                r.close_color("bold")
            elif element.name == "a":
                link = element.get("href")
                # support for images nested in links
                if link:
                    # First, we transform any space that can be found in that link
                    link = urlify(link)
                    text = ""
                    imgtext = ""
                    #normal link, not image
                    normal_link = True
                    #display link to the picture?
                    display_this_picture = False
                    # we display images first in a link
                    for child in element.children:
                        if child.name == "img":
                            recursive_render(child)
                            display_this_picture = True
                            normal_link = False
                            #print( "%s same as previous link %s ?"%(link,str(links[-1])))
                    # we check if the link is the same as the image itself
                    # if so, it is the last link in the links list
                    abs_url = urllib.parse.urljoin(self.get_base_url(),link)
                    if not normal_link and len(links) > 0:
                        last_link = links[-1].split()
                        if len(last_link) > 0:
                            display_this_picture = abs_url != last_link[0]
                    if display_this_picture:
                        imgtext = "[IMG LINK %s]"
                    if display_this_picture or normal_link:
                        links.append(link + " " + text)
                        link_id = str(len(links) + startlinks)
                    if is_url_blocked(link,self.redirects) \
                        and r.open_theme("blocked_link"):
                        linktheme = "blocked_link"
                    else:
                        linktheme = "link"
                        r.open_theme(linktheme)
                    for child in element.children:
                        if child.name != "img":
                            recursive_render(child, preformatted=preformatted)
                    if display_this_picture:
                        r.center_line()
                        r.add_text(imgtext % link_id)
                    elif normal_link:
                        r.add_text(" [%s]" % link_id)
                    r.close_theme(linktheme)
                else:
                    # No real link found
                    for child in element.children:
                        recursive_render(child, preformatted=preformatted)
            elif element.name == "img":
                src = element.get("src")
                text = ""
                alt = element.get("alt")
                if alt:
                    alt = sanitize_string(alt)
                    text += "[IMG] %s" % alt
                else:
                    text += "[IMG]"
                if src:
                    if mode not in self.images:
                        self.images[mode] = []
                    abs_url, data = looks_like_base64(src, self.get_base_url())
                    # if abs_url is None, it means we don’t support
                    # the image (such as svg+xml). So we hide it.
                    # But we first check if there’s a data-src
                    if not abs_url:
                        src = element.get("data-src")
                        abs_url, data = looks_like_base64(src,self.get_base_url())
                    if abs_url:
                        ansi_img = render_image(src, width=width, mode=mode)
                        abs_url = urlify(abs_url)
                        links.append(abs_url + " " + text)
                        self.images[mode].append(abs_url)
                        link_id = " [%s]" % (len(links) + startlinks)
                        r.add_block(ansi_img)
                        r.open_theme("image_link")
                        r.center_line()
                        r.add_text(text + link_id)
                        r.close_theme("image_link")
                        r.newline()

            elif element.name == "video":
                poster = element.get("poster")
                src = element.get("src")
                for child in element.children:
                    if not src:
                        if child.name == "source":
                            src = child.get("src")
                text = ""
                if poster:
                    ansi_img = render_image(poster, width=width, mode=mode)
                alt = element.get("alt")
                if alt:
                    alt = sanitize_string(alt)
                    text += "[VIDEO] %s" % alt
                else:
                    text += "[VIDEO]"

                if poster:
                    if mode not in self.images:
                        self.images[mode] = []
                    poster_url, d = looks_like_base64(poster, self.get_base_url())
                    if poster_url:
                        vid_url, d2 = looks_like_base64(src, self.get_base_url())
                        self.images[mode].append(poster_url)
                        r.add_block(ansi_img)
                        r.open_theme("image_link")
                        r.center_line()
                        if vid_url and src:
                            links.append(vid_url + " " + text)
                            link_id = " [%s]" % (len(links) + startlinks)
                            r.add_text(text + link_id)
                        else:
                            r.add_text(text)
                        r.close_theme("image_link")
                        r.newline()
                elif src:
                    vid_url, d = looks_like_base64(src, self.get_base_url())
                    links.append(vid_url + " " + text)
                    link_id = " [%s]" % (len(links) + startlinks)
                    r.open_theme("image_link")
                    r.center_line()
                    r.add_text(text + link_id)
                    r.close_theme("image_link")
                    r.newline()

            elif element.name == "br":
                r.newline()
                #weirdly, it seems that BS4 sometimes parse elements after 
#as children of
for child in element.children: recursive_render(child, indent=indent) elif ( element.name not in ["script", "style", "template"] and type(element) is not Comment ): if element.string: if preformatted: r.open_theme("preformatted") r.add_text(element.string) r.close_theme("preformatted") else: s = sanitize_string(element.string) if len(s.strip()) > 0: r.add_text(s) else: for child in element.children: recursive_render(child, indent=indent) # the real render_html hearth # We will transform the body into a "summary" (clean-up version) summary = None self.cleanlib = "" # if mode full, we don’t clean anything if mode in ["full", "full_links_only"]: summary = body self.cleanlib += "Full as requested" # let’s try unmerdify elif "ftr_site_config" in self.options.keys() and self.options["ftr_site_config"]: ftr = ftr_site_config=self.options["ftr_site_config"] # we want to unmerdify only if there’s a rule if unmerdify.is_unmerdifiable(self.url,ftr): try: summary = unmerdify.unmerdify_html(body,url=self.url,\ ftr_site_config=ftr,NOCONF_FAIL=False) except Exception as e: self.cleanlib += "Unmerdify CRASH - %s - "%e if not summary: self.cleanlib += "Unmerdify failed - returns empty html" else: self.cleanlib += "Unmerdify" if not summary: # if no summary from unmerdify, we try readabilitty if _HAS_READABILITY: try: readable = Document(body) summary = readable.summary() self.cleanlib += " - Readability" except Exception as e: summary = body self.cleanlib += " - Full (Readability failed) %s"%e else: summary = body self.cleanlib += " - Full (No readability installed)" soup = BeautifulSoup(summary, "html.parser") # soup = BeautifulSoup(summary, 'html5lib') if soup: if soup.body: recursive_render(soup.body) else: recursive_render(soup) # inserting available feeds at the end of the page (if any) sublinks = self.get_subscribe_links() if len(sublinks) > 1: r.newparagraph() r.open_theme("subtitle") r.add_text("Available feeds: ") r.close_theme("subtitle") r.newparagraph() for s in sublinks[1:]: title = str(s[2]) mime = str(s[1]) # we remove the "application/" part of the mime if "/" in mime: mime = mime.split("/")[1] text = title + " (%s)"%mime url = str(s[0]) links.append(url + " " + text) link_id = str(len(links) + startlinks) r.open_theme("link") r.add_text("%s [%s]" %(text,link_id)) r.close_theme("link") r.newline() return r.get_final(), links ## Now the custom renderers class XkcdRenderer(HtmlRenderer): def printgemtext(self,source): if source: gemtext_renderer = GemtextRenderer("",self.url) final,links = gemtext_renderer.render(source) print(final) def has_direct_display(self): return _RENDER_IMAGE def get_xkcd_number(self): path = urllib.parse.urlparse(self.url).path #We strip the leading "/" to avoid empty elements in splitted splitted = path.strip("/").split("/") # Custom renderer only works for pure comics which have alphanumeric paths return splitted[0] #Custom renderer should return false when they can’t handle a specific content #This allow fallback to normal html renderer def is_valid(self): return self.get_xkcd_number().isalnum() def get_images(self, mode="readable"): img_url,img_path,alttext,title = self.xkcd_extract() return [img_url] def get_links(self,mode="readable"): img_url,img_path,alttext,title = self.xkcd_extract() links = super().get_links(mode=mode) if img_url not in links: links.append(img_url) return links #return [image_url,image_path, image_alt_text,image_title] def xkcd_extract(self): if _DO_HTML : soup = BeautifulSoup(self.body, "html.parser") comic_div = soup.find("div",{"id":"comic"}) if comic_div: img_element = comic_div.find("img") src=img_element.get("src") if src.startswith("//"): scheme = urllib.parse.urlparse(self.url).scheme img_url = scheme + ":" + src else: img_url = src img_path = netcache.get_cache_path(img_url) alttext=img_element.get("title") title=img_element.get("alt") return img_url,img_path,alttext,title return None,None,None,None def display(self, mode=None, directdisplay=False): info = " (XKCD #%s)"%self.get_xkcd_number() wtitle = self.get_formatted_title(linksnbr=False) if not directdisplay: body = wtitle + "\n" + self.get_body(mode=mode) return body else: print(wtitle) self.printgemtext("# "+self.get_title() + info) img_url,img_path,alttext,title = self.xkcd_extract() #now displaying if img_path and netcache.is_cache_valid(img_url): terminal_image(img_path) elif not _DO_HTML: self.printgemtext(_("\n> Please install python-bs4 to parse HTML")) else: self.printgemtext(_("\n> Picture not in cache. Please reload this page.\n")) self.printgemtext(alttext) return True # Mapping mimetypes with renderers # (any content with a mimetype text/* not listed here will be rendered with as GemText) _FORMAT_RENDERERS = { "text/gemini": GemtextRenderer, "text/html": HtmlRenderer, "application/xhtml+xml": HtmlRenderer, "text/xml": FeedRenderer, "text/plain": PlaintextRenderer, "application/xml": FeedRenderer, "application/rss+xml": FeedRenderer, "application/atom+xml": FeedRenderer, "text/gopher": GopherRenderer, "image/*": ImageRenderer, "application/javascript": HtmlRenderer, "application/json": HtmlRenderer, "text/empty": EmptyRenderer, "message/news": GemtextRenderer, "message/rfc822": GemtextRenderer, "application/pgp-keys": PlaintextRenderer, "application/pgp-signature": PlaintextRenderer, } _CUSTOM_RENDERERS = { "xkcd.com": XkcdRenderer, } def get_mime(path, url=None): # Beware, this one is really a shaddy ad-hoc function if not path: return None # If the file is empty, simply returns it elif os.path.exists(path) and os.stat(path).st_size == 0: return "text/empty" elif url and url.startswith("gopher://"): # special case for gopher mime = get_gopher_mime(url) elif path.startswith("mailto:"): mime = "mailto" elif os.path.isdir(path): mime = "Local Folder" elif path.endswith(".gmi") or path.endswith(".gemini"): mime = "text/gemini" elif path.endswith("gophermap"): mime = "text/gopher" elif shutil.which("file"): mime = run("file -b --mime-type %s", parameter=path).strip() mime2, encoding = mimetypes.guess_type(path, strict=False) # If we hesitate between html and xml, takes the xml one # because the FeedRendered fallback to HtmlRenderer if mime2 and mime != mime2 and "html" in mime and "xml" in mime2: mime = "text/xml" # We will also check if the first line starts with 0 and (not renderer or not renderer.is_valid()): current_mime = mime_to_use.pop(0) func = _FORMAT_RENDERERS[current_mime] if current_mime.startswith("text"): renderer = func(content, url,**kwargs) # We double check if the renderer is correct. # If not, we fallback to html # (this is currently only for XHTML, often being # mislabelled as xml thus RSS feeds) if not renderer.is_valid(): func = _FORMAT_RENDERERS["text/html"] # print("Set (fallback)RENDERER to html instead of %s"%mime) renderer = func(content, url,**kwargs) else: # TODO: check this code and then remove one if. # we don’t parse text, we give the file to the renderer renderer = func(content, url,**kwargs) if not renderer.is_valid(): renderer = None #We have not found a renderer. Use a fake one. if not renderer: renderer = FakeRenderer("",url,**kwargs) renderer.set_mime(mime) if renderer: if theme: renderer.set_theme(theme) if redirectlist: renderer.set_redirects(redirectlist) return renderer # This function should be removed and replaced by a set_renderer()/r.display() def render(input, path=None, format="auto", mime=None, url=None, mode=None, linkmode="none"): if not url: url = "" else: url = url[0] if format == "gemtext": r = GemtextRenderer(input, url) elif format == "html": r = HtmlRenderer(input, url) elif format == "feed": r = FeedRenderer(input, url) elif format == "gopher": r = GopherRenderer(input, url) elif format == "image": r = ImageRenderer(input, url) elif format == "folder": r = FolderRenderer(input, url) elif format in ["plaintext", "text"]: r = PlaintextRenderer(input, url) else: if not mime and path: r = renderer_from_file(path, url) else: r = set_renderer(input, url, mime) if r: r.options["linkmode"] = linkmode r.display(directdisplay=True, mode=mode) else: print(_("Could not render %s") % input) def main(): descri = _( "ansicat is a terminal rendering tool that will render multiple formats (HTML, \ Gemtext, RSS, Gophermap, Image) into ANSI text and colors.\n\ When used on a file, ansicat will try to autodetect the format. When used with \ standard input, the format must be manually specified.\n\ If the content contains links, the original URL of the content can be specified \ in order to correctly modify relatives links." ) parser = argparse.ArgumentParser(prog="ansicat", description=descri) parser.add_argument( "--format", choices=[ "auto", "gemtext", "html", "feed", "gopher", "image", "folder", "text", "plaintext", ], help=_("Renderer to use. Available: auto, gemtext, html, feed, gopher, image, folder, plaintext"), ) parser.add_argument("--mime", help=_("Mime of the content to parse")) ## The argument needs to be a path to a file. If none, then stdin is used which allows ## to pipe text directly into ansirenderer parser.add_argument( "--url", metavar="URL", nargs="*", help=_("Original URL of the content") ) parser.add_argument( "--mode", metavar="MODE", help=_("Which mode should be used to render: normal (default), full or source.\ With HTML, the normal mode try to extract the article."), ) parser.add_argument( "--linkmode", choices=[ "none", "end", ], help=_("Which mode should be used to render links: none (default) or end"), ) parser.add_argument( "content", metavar="INPUT", nargs="*", type=argparse.FileType("r"), default=sys.stdin, help=_("Path to the text to render (default to stdin)"), ) args = parser.parse_args() # Detect if we are running interactively or in a pipe if sys.stdin.isatty(): # we are interactive, not in stdin, we can have multiple files as input if isinstance(args.content, list): for f in args.content: path = os.path.abspath(f.name) try: content = f.read() except UnicodeDecodeError: content = f render( content, path=path, format=args.format, url=args.url, mime=args.mime, mode=args.mode, linkmode=args.linkmode, ) else: print(_("Ansicat needs at least one file as an argument")) else: # we are in stdin if not args.format and not args.mime: print(_("Format or mime should be specified when running with stdin")) else: render( args.content.read(), path=None, format=args.format, url=args.url, mime=args.mime, mode=args.mode, linkmode=args.linkmode, ) if __name__ == "__main__": main() offpunk-v3.0/cert_migration.py000066400000000000000000000040051514232770500165770ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2024 Bert Livens # SPDX-License-Identifier: AGPL-3.0-only """ A script to migrate the offpunk certificate storage to the newest version. For each new version of offpunk that requires changes to the certificate storage a migration function should be written, performing a migration from the immediately previous format. """ import datetime import os import sqlite3 def upgrade_to_1(data_dir: str, config_dir: str) -> None: print("moving from tofu.db to certificates as files") db_path = os.path.join(config_dir, "tofu.db") # We should check if there’s a db that exists. # Warning: the file might exists but be empty. if os.path.exists(db_path) and os.path.getsize(db_path) == 0: os.remove(db_path) if os.path.exists(db_path): db_conn = sqlite3.connect(db_path) db_cur = db_conn.cursor() db_cur.execute(""" SELECT hostname, address, fingerprint, count, first_seen, last_seen FROM cert_cache""") certs = db_cur.fetchall() data_dir = os.path.join(data_dir, "certs") os.makedirs(data_dir, exist_ok=True) for hostname, address, fingerprint, count, first_seen, last_seen in certs: direc = os.path.join(data_dir, hostname) os.makedirs(direc, exist_ok=True) certdir = os.path.join(direc, address) os.makedirs(certdir, exist_ok=True) # filename is the fingerprint certfile = os.path.join(certdir, str(fingerprint)) # write count with open(certfile, 'w') as file: file.write(str(count)) # change creation and modification date of file first_seen = datetime.datetime.strptime(first_seen, "%Y-%m-%d %H:%M:%S.%f") last_seen = datetime.datetime.strptime(last_seen, "%Y-%m-%d %H:%M:%S.%f") os.utime(certfile, (first_seen.timestamp(), last_seen.timestamp())) # remove tofu.db os.remove(db_path) offpunk-v3.0/debug.sh000077500000000000000000000001731514232770500146460ustar00rootroot00000000000000#!/bin/bash XDG_DATA_HOME="~/debug/data" XDG_CONFIG_HOME="~/debug/config" XDG_CACHE_HOME="~/debug/cache" python offpunk.py offpunk-v3.0/hatch_build.py000066400000000000000000000035771514232770500160540ustar00rootroot00000000000000import shutil import subprocess from pathlib import Path from hatchling.builders.hooks.plugin.interface import BuildHookInterface class TranslationFilesHook(BuildHookInterface): """Compile the GNU gettext translation files from their po-format into their binary representating mo-format using 'msgfmt'. """ # Command used to compile po into mo files. COMPILE_COMMAND = 'msgfmt' def _check_compile_command(self): """Check if "msgfmt" is available.""" if not shutil.which(type(self).COMPILE_COMMAND): raise OSError( 'Executable "{cmd}" (from GNU gettext tools) is not ' 'available. Please install it via a package manager of ' 'your trust. In most cases "{cmd}" is part of "gettext".' .format(cmd=type(self).COMPILE_COMMAND) ) def _compile_po_to_mo(self, po_file: Path, mo_file: Path): """ Compile po-file to mo-file using "msgfmt". """ # Build command cmd = [ type(self).COMPILE_COMMAND, '--output-file={}'.format(mo_file), po_file ] # Execute command rc = subprocess.run(cmd, check=False, text=True, capture_output=True) # Validate output if rc.stderr: raise RuntimeError(rc.stderr) def initialize(self, version, build_data): print('|==-- Preparing translation files. --==|') self._check_compile_command() pkg_path = Path.cwd() for in_file in pkg_path.glob('po/*.po'): print(f'Processing "{in_file.name}"') out_file = pkg_path / 'share/locale' / in_file.stem / 'LC_MESSAGES' / 'offpunk.mo' # Create folder for output file out_file.parent.mkdir(parents=True, exist_ok=True) self._compile_po_to_mo(in_file, out_file) print('|==-- Completed. --==|') offpunk-v3.0/man/000077500000000000000000000000001514232770500137735ustar00rootroot00000000000000offpunk-v3.0/man/ansicat.1000066400000000000000000000034331514232770500155020ustar00rootroot00000000000000.Dd November 25, 2024 .Dt ANSICAT 1 .Os . .Sh NAME .Nm ansicat .Nd terminal gemini gemtext renderer for offpunk . .Sh SYNOPSIS .Nm .Op Fl \-format Ar RENDERER .Op Fl \-mime Ar MIME .Op Fl \-url Ar URL ... .Op Fl \-mode Ar MODE .Op Fl \-linkmode Ar LINKMODE .Op Ar INPUT ... .Nm .Fl h | \-help . .Sh DESCRIPTION ansicat is a terminal rendering tool that will render multiple formats (HTML, Gemtext, RSS, Gophermap, Image) into ANSI text and colors. When used on a file, ansicat will try to autodetect the format. When used with standard input, the format must be manually specified. If the content contains links, the original URL of the content can be specified in order to correctly modify relatives links. .Ss Positional arguments .Bl -tag -width Ds -offset indent .It Ar INPUT path to the text to render. It defaults to the standard input. .El .Ss Keyword arguments .Bl -tag -width Ds -offset indent .It Fl h , \-help show a help message and exit. .It Fl \-format Ar RENDERER renderer to use. Available renderers are .Ic auto , .Ic gemtext , .Ic html , .Ic feed , .Ic gopher , .Ic image , and .Ic folder . The .Ic auto renderer will heuristically guess the format, either thanks to the MIME type, or from the file being rendered itself. .It Fl \-mime Ar MIME MIME type of the content to parse. .It Fl \-mode Ar MODE MODE to use to render to choose between normal (default), full or source .It Fl \-linkmode Ar LINKMODE LINKMODE to use to render links. Available are .Ic none (default) and .Ic end . This option will not effect render mode source. .It Fl \-url Ar URL ... original URL of the content. .El . .Sh EXIT STATUS .Ex -std . .Sh SEE ALSO .Xr migrate-offpunk-cache 1 , .Xr netcache 1 , .Xr offpunk 1 , .Xr openk 1 , .Lk https://offpunk.net/ . .Sh AUTHORS .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu offpunk-v3.0/man/netcache.1000066400000000000000000000042541514232770500156340ustar00rootroot00000000000000.Dd November 25, 2024 .Dt NETCACHE 1 .Os . .Sh NAME .Nm netcache .Nd local Internet cache handler for offpunk . .Sh SYNOPSIS .Nm .Op Fl \-path .Op Fl \-ids .Op Fl \-offline .Op Fl \-max\-size Ar MAX_SIZE .Op Fl \-timeout Ar TIMEOUT .Op Ar URL ... .Nm .Fl h | \-help . .Sh DESCRIPTION Netcache is a command-line tool to retrieve, cache and access networked content. By default, netcache will returns a cached version of a given URL, downloading it only if a cache version doesn't exist. A validity duration, in seconds, can also be given so netcache downloads the content only if the existing cache is older than the validity. .Pp Netcache can be forced into offline mode, in order to only fetch resources from the local cache, otherwise it would always refresh it from the version available online. It is also useful for mapping a given URL to its location in the cache, independently of whether it has been downloaded first. .Ss Positional arguments .Bl -tag -width Ds -offset indent .It Ar URL download the URL and output the content. .El .Ss Keyword arguments .Bl -tag -width Ds -offset indent .It Fl h , \-help show a help message and exit. .It Fl \-path output the path to the cache instead of the content of the URL. .It Fl \-ids return a list of id's for the gemini-site instead of the content of the cache. .It Fl \-offline do not attempt to download, return the cached version instead, or error if absent from the cache. .It Fl \-max-size Ar MAX_SIZE cancel download of items above that size. The value is expressed in megabytes. .It Fl \-timeout Ar TIMEOUT time to wait before cancelling connection. The value is expressed in seconds. .It Fl \-cache-validity CACHE_VALIDITY Maximum age (in second) of the cached version before redownloading a new version. .El . .Sh EXIT STATUS .Ex -std . .Sh ENVIRONMENT Set .Ev OFFPUNK_CACHE_PATH environment variable to use another location. .Bd -literal OFFPUNK_CACHE_PATH=/home/ploum/custom-cache netcache.py gemini://some.url .Ed . .Sh FILES Default cache path is .Pa ~/.cache/offpunk . . .Sh SEE ALSO .Xr ansicat 1 , .Xr migrate-offpunk-cache 1 , .Xr offpunk 1 , .Xr openk 1 , .Lk https://offpunk.net/ . .Sh AUTHORS .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu offpunk-v3.0/man/offpunk.1000066400000000000000000000051341514232770500155300ustar00rootroot00000000000000.Dd February 2, 2025 .Dt OFFPUNK 1 .Os . .Sh NAME .Nm offpunk .Nd command line gemini client . .Sh SYNOPSIS .Nm .Op Fl \-bookmarks .Op Fl \-config\-file Ar FILE .Op Fl \-command Ar COMMAND .Op Fl \-sync .Op Fl \-assume\-yes .Op Fl \-disable\-http .Op Fl \-fetch\-later .Op Fl \-depth Ar DEPTH .Op Fl \-images\-mode Ar IMAGES_MODE .Op Fl \-cache\-validity Ar CACHE_VALIDITY .Op Ar URL ... .Nm .Fl h | \-help .Nm .Fl \-version .Nm .Fl \-features . .Sh DESCRIPTION Offpunk is a command-line browser and feed reader dedicated to browsing the Web, Gemini, Gopher and Spartan. Thanks to its permanent cache, it is optimised to be used offline with rare connections but works as well when connected. .Pp Offpunk is optimised for reading and supports readability mode, displaying pictures, subscribing to pages or RSS feeds, managing complex lists of bookmarks. Its integrated help and easy commands make it a perfect tool for command-line novices while power-users will be amazed by its shell integration. .Ss Positional arguments .Bl -tag -width Ds -offset indent .It URL start with this URL .El .Ss Keyword arguments .Bl -tag -width Ds -offset indent .It Fl h , \-help Show a help message and exit .It Fl \-bookmarks start with your list of bookmarks .It Fl \-config\-file Ar FILE use this particular config file instead of default .It Fl \-command Ar COMMAND launch this or those command(s) after startup (including config file). .It Fl \-sync run non\-interactively to build cache by exploring bookmarks .It Fl \-assume\-yes assume\-yes when asked questions about certificates/redirections during sync (lower security) .It Fl \-disable\-http do not try to get http(s) links (but already cached will be displayed) .It Fl \-fetch\-later run non\-interactively with an URL as argument to fetch it later .It Fl \-depth Ar DEPTH depth of the cache to build. Default is 1. More is crazy. Use at your own risks! .It Fl \-images-mode Ar IMAGES_MODE the mode to use to choose which images to download in a HTML page. one of (None, readable, full). Warning: full will slowdown your sync. .It Fl \-cache\-validity Ar CACHE_VALIDITY duration for which a cache is valid before sync (seconds) .It Fl \-version display version information and quit .It Fl \-features display available features and dependancies then quit .El . .Sh EXIT STATUS .Ex -std . .Sh SEE ALSO .Xr ansicat 1 , .Xr migrate-offpunk-cache 1 , .Xr netcache 1 , .Xr openk 1 , .Lk https://offpunk.net/ . .Sh HISTORY .Nm is a fork of the original AV-98 by .An Solderpunk and was originally called AV-98-offline as an experimental branch. . .Sh AUTHORS .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu offpunk-v3.0/man/openk.1000066400000000000000000000025651514232770500152010ustar00rootroot00000000000000.Dd November 25, 2024 .Dt OPNK 1 .Os . .Sh NAME .Nm openk .Nd universal open (like a punk) for offpunk . .Sh SYNOPSIS .Nm .Op \-mode Ar MODE .Op \-linkmode Ar LINKMODE .Op \-cache-validity Ar CACHE_VALIDITY .Op Ar INPUT ... .Nm .Fl h | \-help . .Sh DESCRIPTION openk is an universal open command tool that will try to display any file in the pager .Xr less 1 after rendering its content with .Xr ansicat 1 . If that fails, openk will fallback to opening the file with .Xr xdg-open 1 . If given an URL as input instead of a path, openk will rely on .Xr netcache 1 to get the networked content. .Ss Positional arguments .Bl -tag -width Ds -offset indent .It Ar INPUT path to the file or URL to open. .El .Ss Keyword arguments .Bl -tag -width Ds -offset indent .It Fl h , \-help Show a help message and exit .It Fl \-mode Ar MODE MODE to use to render to choose between normal (default), full or source .It Fl \-linkmode Ar LINKMODE LINKMODE to use to render links. Available are .Ic none (default) and .Ic end . This option will not effect render mode source. .It Fl \-cache-validity CACHE_VALIDITY Maximum age (in second) of the cached version before redownloading a new version. .El . .Sh EXIT STATUS .Ex -std . .Sh SEE ALSO .Xr ansicat 1 , .Xr migrate-offpunk-cache 1 , .Xr netcache 1 , .Xr offpunk 1 , .Lk https://offpunk.net/ . .Sh AUTHORS .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu offpunk-v3.0/man/xkcdpunk.1000066400000000000000000000017401514232770500157060ustar00rootroot00000000000000.Dd February 9, 2026 .Dt XKCDPUNK 1 .Os . .Sh NAME .Nm xkcdpunk .Nd Display XKCD comics directly in your terminal . .Sh SYNOPSIS .Nm .Op \-offline .Op Ar INPUT ... .Nm .Fl h | \-help . .Sh DESCRIPTION xkcdpunk will display a given xkcd comic and its alt-text directly in your terminal. Without argument, the latest known comic will be displayed. The argument can also be "random" to display a random comic. .Ss Positional arguments .It Ar INPUT XKCD number. Besides a number, "latest" and "random" are accepted values. If missing, "latest" is assumed .El .Ss Keyword arguments .Bl -tag -width Ds -offset indent .It Fl \-offline Only acces already cached comics and do not download anything. "latest" will fails if no comic cached ever and "random" might fails if not enough cached comics. .El . .Sh EXIT STATUS .Ex -std . .Sh SEE ALSO .Xr ansicat 1 , .Xr netcache 1 , .Xr offpunk 1 , .Xr openk 1 , .Lk https://offpunk.net/ . .Sh AUTHORS .An Lionel Dricot (Ploum) Aq Mt offpunk2 at ploum.eu offpunk-v3.0/netcache.py000077500000000000000000001410611514232770500153520ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import codecs import datetime import getpass import glob import hashlib import os import socket import ssl import sys import time import urllib.parse import http.cookiejar import warnings from ssl import CertificateError import gettext import ansicat import offutils from offutils import xdg, _LOCALE_DIR, get_url_redirected import offblocklist gettext.bindtextdomain('offpunk', _LOCALE_DIR) gettext.textdomain('offpunk') _ = gettext.gettext try: import chardet _HAS_CHARDET = True except ModuleNotFoundError: _HAS_CHARDET = False try: from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa _HAS_CRYPTOGRAPHY = True _BACKEND = default_backend() except (ModuleNotFoundError, ImportError): _HAS_CRYPTOGRAPHY = False try: with warnings.catch_warnings(): # Disable annoying warning shown to LibreSSL users warnings.simplefilter("ignore") import requests _DO_HTTP = True except (ModuleNotFoundError, ImportError): _DO_HTTP = False # This list is also used as a list of supported protocols standard_ports = { "gemini": 1965, "gopher": 70, "finger": 79, "http": 80, "https": 443, "spartan": 300, } default_protocol = "gemini" CRLF = "\r\n" DEFAULT_TIMEOUT = 10 _MAX_REDIRECTS = 5 # monkey-patch Gemini support in urllib.parse # see https://github.com/python/cpython/blob/master/Lib/urllib/parse.py urllib.parse.uses_relative.append("gemini") urllib.parse.uses_netloc.append("gemini") urllib.parse.uses_relative.append("spartan") urllib.parse.uses_netloc.append("spartan") class UserAbortException(Exception): pass def parse_mime(mime): options = {} if mime: if ";" in mime: splited = mime.split(";", maxsplit=1) mime = splited[0] if len(splited) >= 1: options_list = splited[1].split() for o in options_list: spl = o.split("=", maxsplit=1) if len(spl) > 0: options[spl[0]] = spl[1] return mime, options def normalize_url(url): if "://" not in url and ("./" not in url and url[0] != "/"): if not url.startswith("mailto:"): url = "gemini://" + url return url def cache_last_modified(url): if not url: return None path = get_cache_path(url) if path and os.path.isfile(path): return os.path.getmtime(path) else: return None def is_cache_valid(url, validity=0): # Validity is the acceptable time for # a cache to be valid (in seconds) # If 0, then any cache is considered as valid # (use validity = 1 if you want to refresh everything) if offutils.is_local(url): return True cache = get_cache_path(url) if cache: # If path is too long, we always return True to avoid # fetching it. if len(cache) > 259: print(_("We return False because path is too long")) return False if os.path.exists(cache) and not os.path.isdir(cache): if validity > 0: last_modification = cache_last_modified(url) now = time.time() age = now - last_modification return age < validity else: return True else: # Cache has not been build return False else: # There’s not even a cache! return False # get_cache_path gives a local path unique to each URL def get_cache_path(url, add_index=True, include_protocol=True, xdgfolder="cache",subfolder=""): # Sometimes, cache_path became a folder! (which happens for index.html/index.gmi) # In that case, we need to reconstruct it # if add_index=False, we don’t add that "index.gmi" at the ends of the cache_path # include protocol will include the part before :// in the path # xdgfolder is the XDG folder to be used # subfolder is the optional folder inside XDG/offpunk/ # First, we parse the URL if not url: return None parsed = urllib.parse.urlparse(url) if url[0] == "/" or url.startswith("./") or os.path.exists(url): scheme = "file" elif parsed.scheme: scheme = parsed.scheme else: scheme = default_protocol if scheme in ["file", "mailto", "list"]: local = True host = "" port = None # file:// is 7 char if url.startswith("file://"): path = url[7:] elif scheme == "mailto": path = parsed.path elif url.startswith("list://"): listdir = os.path.join(xdg("data"), "lists") listname = url[7:].lstrip("/") if listname in [""]: name = "My Lists" path = listdir else: name = listname path = os.path.join(listdir, "%s.gmi" % listname) else: path = url else: local = False # Convert unicode hostname to punycode using idna RFC3490 host = parsed.netloc # .encode("idna").decode() try: port = parsed.port or standard_ports.get(scheme, 0) except ValueError: port = standard_ports.get(scheme, 0) # special gopher selector case if scheme == "gopher": if len(parsed.path) >= 2: # we remove the selector splitted = parsed.path.split("/") # the first split if empty if len(splitted[0]) == 0: splitted.pop(0) #now we see if the element is a selector on not if len(splitted[0]) == 1: path = parsed.path[2:] else: path = parsed.path else: path = "" else: path = parsed.path if parsed.query: # we don’t add the query if path is too long because path above 260 char # are not supported and crash python. # Also, very long query are usually useless stuff if len(path + parsed.query) < 258: path += "/" + parsed.query # Now, we have a partial path. Let’s make it full path. if local: cache_path = path elif scheme and host: # shemepath should not starts with / but finish with / schemepath = "" if include_protocol: schemepath = scheme + "/" if subfolder: schemepath += subfolder + "/" cache_path = os.path.expanduser(xdg(xdgfolder) + schemepath + host + path) # There’s an OS limitation of 260 characters per path. # We will thus cut the path enough to add the index afterward cache_path = cache_path[:249] # this is a gross hack to give a name to # index files. This will break if the index is not # index.gmi. I don’t know how to know the real name # of the file. But first, we need to ensure that the domain name # finish by "/". Else, the cache will create a file, not a folder. if scheme.startswith("http"): index = "index.html" elif scheme == "finger": index = "index.txt" elif scheme == "gopher": index = "gophermap" else: index = "index.gmi" if path == "" or os.path.isdir(cache_path): if not cache_path.endswith("/"): cache_path += "/" if not url.endswith("/"): url += "/" if add_index and cache_path.endswith("/"): cache_path += index # sometimes, the index itself is a dir # like when folder/index.gmi?param has been created # and we try to access folder if add_index and os.path.isdir(cache_path): cache_path += "/" + index else: # URL is missing either a supported scheme or a valid host # print("Error: %s is not a supported url"%url) return None if len(cache_path) > 259: #print("Path is %s characters long which is too long. \ # OS only allows 260 characters.\n\n"%(len(cache_path))) #print(url) #return None # path lenght is limited to 260 charaters. Let’s cut it and # hope that there’s no major conflict here. (that’s still better # than crashing, after all. cache_path = cache_path[:259] return cache_path def write_body(url, body, mime=None): # body is a copy of the raw gemtext # Write_body() also create the cache ! # DEFAULT GEMINI MIME mime, options = parse_mime(mime) cache_path = get_cache_path(url) if cache_path: if mime and mime.startswith("text/"): mode = "w" else: mode = "wb" cache_dir = os.path.dirname(cache_path) # If the subdirectory already exists as a file (not a folder) # We remove it (happens when accessing URL/subfolder before # URL/subfolder/file.gmi. # This causes loss of data in the cache # proper solution would be to save "sufolder" as "sufolder/index.gmi" # If the subdirectory doesn’t exist, we recursively try to find one # until it exists to avoid a file blocking the creation of folders root_dir = cache_dir while not os.path.exists(root_dir): root_dir = os.path.dirname(root_dir) if os.path.isfile(root_dir): os.remove(root_dir) os.makedirs(cache_dir, exist_ok=True) with open(cache_path, mode=mode) as f: f.write(body) f.close() return cache_path def set_error(url, err): # If we get an error, we want to keep an existing cache # but we need to touch it or to create an empty one # to avoid hitting the error at each refresh cache = get_cache_path(url) if is_cache_valid(url): os.utime(cache) elif cache: cache_dir = os.path.dirname(cache) root_dir = cache_dir while not os.path.exists(root_dir): root_dir = os.path.dirname(root_dir) if os.path.isfile(root_dir): os.remove(root_dir) os.makedirs(cache_dir, exist_ok=True) if os.path.isdir(cache_dir): with open(cache, "w") as c: c.write(str(datetime.datetime.now()) + "\n") c.write(_("ERROR while caching %s\n\n") % url) c.write("*****\n\n") c.write(str(type(err)) + " = " + str(err)) # cache.write("\n" + str(err.with_traceback(None))) c.write("\n*****\n\n") c.write(_("If you believe this error was temporary, type " "reload" ".\n")) c.write(_("The resource will be tentatively fetched during next sync.\n")) c.close() return cache def get_cookiejar(url, create=False): parsed = urllib.parse.urlparse(url) basedir = os.path.join(xdg("data") + "cookies/" ) cookie_path = basedir + parsed.netloc + ".txt" if not os.path.exists(cookie_path): if create: os.makedirs(basedir, exist_ok=True) with open(cookie_path, 'w') as f: f.write("# Netscape HTTP Cookie File\n\n") else: return None jar = http.cookiejar.MozillaCookieJar(cookie_path) jar.load() return jar def _fetch_http( url, max_size=None, timeout=DEFAULT_TIMEOUT, accept_bad_ssl_certificates=False, force_large_download=False, cookiejar=None, **kwargs, ): if not _DO_HTTP: return None def too_large_error(url, length, max_size): err = _("Size of %s is %s Mo\n") % (url, length) err += _("Offpunk only download automatically content under %s Mo\n") % ( max_size / 1000000 ) err += _("To retrieve this content anyway, type 'reload'.") return set_error(url, err) if accept_bad_ssl_certificates: requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = "ALL:@SECLEVEL=1" requests.packages.urllib3.disable_warnings() verify = False else: requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = "ALL:@SECLEVEL=2" verify = True header = {} header["User-Agent"] = "Offpunk/Netcache - https://offpunk.net" with requests.get( url, verify=verify, headers=header, stream=True, timeout=DEFAULT_TIMEOUT, cookies=cookiejar ) as response: if "content-type" in response.headers: mime = response.headers["content-type"] else: mime = None if "content-length" in response.headers: length = int(response.headers["content-length"]) else: length = 0 if not force_large_download and max_size and length > max_size: response.close() return too_large_error(url, str(length / 100), max_size) elif not force_large_download and max_size and length == 0: body = b"" downloaded = 0 for r in response.iter_content(): body += r # We divide max_size for streamed content # in order to catch them faster size = sys.getsizeof(body) max = max_size / 2 current = round(size * 100 / max, 1) if current > downloaded: downloaded = current print( _(" -> Receiving stream: %s%% of allowed data") % downloaded, end="\r", ) # print("size: %s (%s\% of maxlenght)"%(size,size/max_size)) if size > max_size / 2: response.close() return too_large_error(url, "streaming", max_size) else: body = response.content if cookiejar is not None: requests.cookies.extract_cookies_to_jar(cookiejar, response.request, response.raw) response.close() if mime and "text/" in mime: body = body.decode("UTF-8", "replace") cache = write_body(url, body, mime) return cache def _fetch_gopher(url, timeout=DEFAULT_TIMEOUT, interactive=True, **kwargs): parsed = urllib.parse.urlparse(url) host = parsed.hostname port = parsed.port or 70 if len(parsed.path) >= 2: itemtype = parsed.path[1] # this is unreliable on "selectors" that contain "?" for example # (which is perfectly valid) # (urlparse took things after the ? as parsed.query) # we should just get the full selector as is and use it # selector = parsed.path[2:] # so, we get everything after hostname:port, minus the 'itemtype' ([:1]) selector = url.split(parsed.netloc, 1)[1][2:] else: itemtype = "1" selector = "" addresses = socket.getaddrinfo(host, port, family=0, type=socket.SOCK_STREAM) s = socket.create_connection((host, port)) for address in addresses: s = socket.socket(address[0], address[1]) s.settimeout(timeout) try: s.connect(address[4]) break except OSError as e: err = e # gophermap lines can't have a query included. # if there is something in parsed.query, it's because an error # or a rogue "?" character in the selector # if parsed.query: # request = selector + "\t" + parsed.query if itemtype == "7": if interactive: user_input = input("> ") request = selector + "\t" + user_input else: return None else: request = selector request += "\r\n" s.sendall(request.encode("UTF-8")) response1 = s.makefile("rb") response = response1.read() # Transcode response into UTF-8 # if itemtype in ("0","1","h"): if itemtype not in ("9", "g", "I", "s", ";"): # Try most common encodings for encoding in ("UTF-8", "ISO-8859-1"): try: response = response.decode("UTF-8") break except UnicodeDecodeError: pass else: # try to find encoding if _HAS_CHARDET: detected = chardet.detect(response) response = response.decode(detected["encoding"]) else: raise UnicodeDecodeError if itemtype == "0": mime = "text/gemini" elif itemtype == "1": mime = "text/gopher" elif itemtype == "h": mime = "text/html" elif itemtype in ("9", "g", "I", "s", ";"): mime = None else: # by default, we should consider Gopher mime = "text/gopher" cache = write_body(url, response, mime) return cache def _fetch_finger(url, timeout=DEFAULT_TIMEOUT, **kwargs): parsed = urllib.parse.urlparse(url) host = parsed.hostname port = parsed.port or standard_ports["finger"] query = parsed.path.lstrip("/") + "\r\n" with socket.create_connection((host, port)) as sock: sock.settimeout(timeout) sock.send(query.encode()) response = sock.makefile("rb").read().decode("UTF-8") cache = write_body(response, "text/plain") return cache # Originally copied from reference spartan client by Michael Lazar def _fetch_spartan(url, **kwargs): cache = None url_parts = urllib.parse.urlparse(url) host = url_parts.hostname port = url_parts.port or standard_ports["spartan"] path = url_parts.path or "/" query = url_parts.query redirect_url = None with socket.create_connection((host, port)) as sock: if query: data = urllib.parse.unquote_to_bytes(query) else: data = b"" encoded_host = host.encode("idna") ascii_path = urllib.parse.unquote_to_bytes(path) encoded_path = urllib.parse.quote_from_bytes(ascii_path).encode("ascii") sock.send(b"%s %s %d\r\n" % (encoded_host, encoded_path, len(data))) fp = sock.makefile("rb") response = fp.readline(4096).decode("ascii").strip("\r\n") parts = response.split(" ", maxsplit=1) code, meta = int(parts[0]), parts[1] if code == 2: body = fp.read() if meta.startswith("text"): body = body.decode("UTF-8") cache = write_body(url, body, meta) elif code == 3: redirect_url = url_parts._replace(path=meta).geturl() else: return set_error(url, "Spartan code %s: Error %s" % (code, meta)) if redirect_url: cache = _fetch_spartan(redirect_url) return cache def _validate_cert(address, host, cert, accept_bad_ssl=False, automatic_choice=None): """ Validate a TLS certificate in TOFU mode. If the cryptography module is installed: - Check the certificate Common Name or SAN matches `host` - Check the certificate's not valid before date is in the past - Check the certificate's not valid after date is in the future Whether the cryptography module is installed or not, check the certificate's fingerprint against the TOFU database to see if we've previously encountered a different certificate for this IP address and hostname. """ now = datetime.datetime.now(datetime.timezone.utc) if _HAS_CRYPTOGRAPHY: # Using the cryptography module we can get detailed access # to the properties of even self-signed certs, unlike in # the standard ssl library... c = x509.load_der_x509_certificate(cert, _BACKEND) # Check certificate validity dates if accept_bad_ssl: if c.not_valid_before >= now: raise CertificateError( _("Certificate not valid until: {}!").format(c.not_valid_before) ) elif c.not_valid_after <= now: raise CertificateError( _("Certificate expired as of: {})!").format(c.not_valid_after) ) # Check certificate hostnames names = [] common_name = c.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) if common_name: names.append(common_name[0].value) try: names.extend( [ alt.value for alt in c.extensions.get_extension_for_oid( x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME ).value ] ) except x509.ExtensionNotFound: pass names = set(names) for name in names: try: ssl._dnsname_match(str(name), host) break except CertificateError: continue else: # If we didn't break out, none of the names were valid raise CertificateError( _("Hostname does not match certificate common name or any alternative names.") ) sha = hashlib.sha256() sha.update(cert) fingerprint = sha.hexdigest() # The directory of this host and IP-address, e.g. # ~/.local/share/offpunk/certs/srht.site/46.23.81.157/ certdir = os.path.join(xdg("data"), "certs") hostdir = os.path.join(certdir, host) sitedir = os.path.join(hostdir, address) # 1. We check through cached certificates do extract the # most_frequent_cert and to see if one is matching the current one. # 2. If we have no match but one valid most_frequent_cert, we do the # "throws warning" code. # 3. If no certificate directory or no valid cached certificates, we do # the "First-Use" routine. most_frequent_cert = None matching_fingerprint = False # 1. Have we been here before? (the directory exists) if os.path.isdir(sitedir): max_count = 0 files = os.listdir(sitedir) count = 0 certcache = os.path.join(xdg("config"), "cert_cache") for cached_fingerprint in files: filepath = os.path.join(sitedir, cached_fingerprint) certpath = os.path.join(certcache, cached_fingerprint + ".crt") with open(filepath, "r") as f: count = int(f.read()) if os.path.exists(certpath): if count > max_count: max_count = count most_frequent_cert = cached_fingerprint if fingerprint == cached_fingerprint: # Matched! # Increase the counter for this certificate (this also updates # the modification time of the file) with open(filepath, "w") as f: f.write(str(count + 1)) matching_fingerprint = True break # 2. Do we have some certificates but none of them is matching the current one? if most_frequent_cert and not matching_fingerprint: with open(os.path.join(certcache, most_frequent_cert + ".crt"), "rb") as fp: previous_cert = fp.read() if _HAS_CRYPTOGRAPHY: # Load the most frequently seen certificate to see if it has # expired previous_cert = x509.load_der_x509_certificate(previous_cert, _BACKEND) previous_ttl = previous_cert.not_valid_after_utc - now print(previous_ttl) print("****************************************") print(_("[SECURITY WARNING] Unrecognised certificate!")) print( _("The certificate presented for {} ({}) has never been seen before.").format( host, address ) ) print(_("This MIGHT be a Man-in-the-Middle attack.")) print( _("A different certificate has previously been seen {} times.").format( max_count ) ) if _HAS_CRYPTOGRAPHY: if previous_ttl < datetime.timedelta(): print(_("That certificate has expired, which reduces suspicion somewhat.")) else: print(_("That certificate is still valid for: {}").format(previous_ttl)) print("****************************************") print(_("Attempt to verify the new certificate fingerprint out-of-band:")) print(fingerprint) if automatic_choice: choice = automatic_choice else: #TRANSLATORS: keep "Y/N" because the answer has to be one of those choice = input(_("Accept this new certificate? Y/N ")).strip().lower() if choice in ("y", "yes"): with open(os.path.join(sitedir, fingerprint), "w") as fp: fp.write("1") with open(os.path.join(certcache, fingerprint + ".crt"), "wb") as fp: fp.write(cert) else: raise Exception(_("TOFU Failure!")) # 3. If no directory or no cert found in it, we cache it if not most_frequent_cert: if not os.path.exists(certdir): # XDG_DATA/offpunk/certs os.makedirs(certdir) if not os.path.exists(hostdir): # XDG_DATA/offpunk/certs/site.net os.makedirs(hostdir) if not os.path.exists( sitedir ): # XDG_DATA/offpunk/certs/site.net/123.123.123.123 os.makedirs(sitedir) with open(os.path.join(sitedir, fingerprint), "w") as fp: fp.write("1") certcache = os.path.join(xdg("config"), "cert_cache") if not os.path.exists(certcache): os.makedirs(certcache) with open(os.path.join(certcache, fingerprint + ".crt"), "wb") as fp: fp.write(cert) def _get_client_certkey(site_id: str, host: str): # returns {cert: str, key: str} certdir = os.path.join(xdg("data"), "certs", host) certf = os.path.join(certdir, "%s.cert" % site_id) keyf = os.path.join(certdir, "%s.key" % site_id) if not os.path.exists(certf) or not os.path.exists(keyf): if host != "": split = host.split(".") # if len(split) > 2: # Why not allow a global identity? Maybe I want # to login to all sites with the same # certificate. return _get_client_certkey(site_id, ".".join(split[1:])) return None certkey = dict(cert=certf, key=keyf) return certkey def _get_site_ids(url: str): newurl = normalize_url(url) u = urllib.parse.urlparse(newurl) if u.scheme == "gemini" and u.username is None: certdir = os.path.join(xdg("data"), "certs") netloc_parts = u.netloc.split(".") site_ids = [] for i in range(len(netloc_parts), 0, -1): lasti = ".".join(netloc_parts[-i:]) direc = os.path.join(certdir, lasti) for certfile in glob.glob(os.path.join(direc, "*.cert")): site_id = certfile.split("/")[-1].split(".")[-2] site_ids.append(site_id) return site_ids else: return [] def create_certificate(name: str, days: int, hostname: str): key = rsa.generate_private_key(public_exponent=65537, key_size=2048) sitecertdir = os.path.join(xdg("data"), "certs", hostname) keyfile = os.path.join(sitecertdir, name + ".key") # create the directory of it doesn't exist os.makedirs(sitecertdir, exist_ok=True) with open(keyfile, "wb") as f: f.write( key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) xname = x509.Name( [ x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, name), ] ) # generate the cert, valid a week ago (timekeeping is hard, let's give it a # little margin). issuer and subject are your name cert = ( x509.CertificateBuilder() .subject_name(xname) .issuer_name(xname) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(days=7)) .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=days)) .sign(key, hashes.SHA256()) ) certfile = os.path.join(sitecertdir, name + ".cert") with open(certfile, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) def ask_certs(url: str): certs = get_certs(url) if len(certs) == 0: print(_("There are no certificates available for this site.")) #TRANSLATORS: keep the "y/n" create_cert = input(_("Do you want to create one? (y/n) ")) if create_cert == "y": name = input(_("Name for this certificate: ")) days = input(_("Validity in days: ")) if name != "" and days.isdigit(): site = urllib.parse.urlparse(url) create_certificate(name, int(days), site.hostname) new_url = "gemini://" + name +"@"+ url.split("://")[1] return(new_url) else: print(_("The name or validity you typed are invalid")) return(url) else: return(url) if len(certs) == 1: print(_("The one available certificate for this site is:")) elif len(certs) > 1: print( _("The {} available certificates for this site are:").format(len(certs)) ) if len(certs) > 0: counter = 0 stri = "" for cert in certs: stri += "[%s] %s \n" % (counter + 1, cert) counter += 1 stri += "\n" stri += _("which certificate do you want to use? > ") ans = input(stri) if ans.isdigit() and 0 < int(ans) <= len(certs): identity = certs[int(ans) -1] else: identity = None if identity: new_url = "gemini://" + identity +"@"+ url.split("://")[1] return(new_url) return(url) #return the same url, no "cert" attached def get_certs(url: str): u = urllib.parse.urlparse(normalize_url(url)) if u.scheme == "gemini": certdir = os.path.join(xdg("data"), "certs") netloc_parts = u.netloc.split(".") site_ids = [] if "@" in netloc_parts[0]: netloc_parts[0] = netloc_parts[0].split("@")[1] # certdir does not contemplate ports, so we should take it out here if present if ":" in netloc_parts[-1]: netloc_parts[-1] = netloc_parts[-1].split(":")[0] for i in range(len(netloc_parts), 0, -1): lasti = ".".join(netloc_parts[-i:]) direc = os.path.join(certdir, lasti) for certfile in glob.glob(os.path.join(direc, "*.cert")): site_id = certfile.split("/")[-1].split(".")[-2] site_ids.append(site_id) return site_ids else: return [] def _fetch_gemini( url, timeout=DEFAULT_TIMEOUT, interactive=True, accept_bad_ssl_certificates=False, **kwargs, ): cache = None newurl = url url_parts = urllib.parse.urlparse(url) host = url_parts.hostname site_id = url_parts.username port = url_parts.port or standard_ports["gemini"] path = url_parts.path or "/" query = url_parts.query # In AV-98, this was the _send_request method # Send a selector to a given host and port. # Returns the resolved address and binary file with the reply. host = host.encode("idna").decode() # Do DNS resolution # DNS lookup - will get IPv4 and IPv6 records if IPv6 is enabled if ":" in host: # This is likely a literal IPv6 address, so we can *only* ask for # IPv6 addresses or getaddrinfo will complain family_mask = socket.AF_INET6 elif socket.has_ipv6: # Accept either IPv4 or IPv6 addresses family_mask = 0 else: # IPv4 only family_mask = socket.AF_INET addresses = socket.getaddrinfo( host, port, family=family_mask, type=socket.SOCK_STREAM ) # Sort addresses so IPv6 ones come first addresses.sort(key=lambda add: add[0] == socket.AF_INET6, reverse=True) # Continuation of send_request # Prepare TLS context protocol = ( ssl.PROTOCOL_TLS_CLIENT if sys.version_info.minor >= 6 else ssl.PROTOCOL_TLSv1_2 ) context = ssl.SSLContext(protocol) context.check_hostname = False context.verify_mode = ssl.CERT_NONE # When using an identity, use the certificate and key if site_id: certkey = _get_client_certkey(site_id, host) if certkey: context.load_cert_chain(certkey["cert"], certkey["key"]) else: print(_("This identity doesn't exist for this site (or is disabled).")) # Impose minimum TLS version # In 3.7 and above, this is easy... if sys.version_info.minor >= 7: context.minimum_version = ssl.TLSVersion.TLSv1_2 # Otherwise, it seems very hard... # The below is less strict than it ought to be, but trying to disable # TLS v1.1 here using ssl.OP_NO_TLSv1_1 produces unexpected failures # with recent versions of OpenSSL. What a mess... else: context.options |= ssl.OP_NO_SSLv3 context.options |= ssl.OP_NO_SSLv2 # Try to enforce sensible ciphers try: context.set_ciphers( "AESGCM+ECDHE:AESGCM+DHE:CHACHA20+ECDHE:CHACHA20+DHE:!DSS:!SHA1:!MD5:@STRENGTH" ) except ssl.SSLError: # Rely on the server to only support sensible things, I guess... pass # Connect to remote host by any address possible err = None for address in addresses: try: s = socket.socket(address[0], address[1]) s.settimeout(timeout) s = context.wrap_socket(s, server_hostname=host) s.connect(address[4]) break except OSError as e: err = e else: # If we couldn't connect to *any* of the addresses, just # bubble up the exception from the last attempt and deny # knowledge of earlier failures. raise err # Do TOFU cert = s.getpeercert(binary_form=True) # Remember that we showed the current cert to this domain... # TODO : accept badssl and automatic choice _validate_cert(address[4][0], host, cert, automatic_choice="y") # Send request and wrap response in a file descriptor url = urllib.parse.urlparse(url) new_host = host # Handle IPV6 hostname if ":" in new_host: new_host = "[" + new_host + "]" if port != standard_ports["gemini"]: new_host += ":" + str(port) url_no_username = urllib.parse.urlunparse(url._replace(netloc=new_host)) if site_id: url = urllib.parse.urlunparse(url._replace(netloc=site_id + "@" + new_host)) else: url = url_no_username s.sendall((url_no_username + CRLF).encode("UTF-8")) f = s.makefile(mode="rb") ## end of send_request in AV98 # Spec dictates should not exceed 1024 bytes, # so maximum valid header length is 1027 bytes. header = f.readline(1027) header = urllib.parse.unquote(header.decode("UTF-8")) if not header or header[-1] != "\n": raise RuntimeError(_("Received invalid header from server!")) header = header.strip() # Validate header status, meta = header.split(maxsplit=1) if len(meta) > 1024 or len(status) != 2 or not status.isnumeric(): f.close() raise RuntimeError(_("Received invalid header from server!")) # Update redirect loop/maze escaping state if not status.startswith("3"): previous_redirectors = set() # TODO FIXME else: # we set a previous_redirectors anyway because refactoring in progress previous_redirectors = set() # Handle non-SUCCESS headers, which don't have a response body # Inputs if status.startswith("1"): if interactive: print(meta) if status == "11": user_input = getpass.getpass("> ") else: user_input = input("> ") newurl = url.split("?")[0] return _fetch_gemini(newurl + "?" + user_input) else: return None, None # Redirects elif status.startswith("3"): newurl = urllib.parse.urljoin(url, meta) if newurl == url: raise RuntimeError(_("URL redirects to itself!")) elif newurl in previous_redirectors: raise RuntimeError(_("Caught in redirect loop!")) elif len(previous_redirectors) == _MAX_REDIRECTS: raise RuntimeError( _("Refusing to follow more than %d consecutive redirects!") % _MAX_REDIRECTS ) # TODO: redirections handling should be refactored # elif "interactive" in options and not options["interactive"]: # follow = self.automatic_choice # # Never follow cross-domain redirects without asking # elif new_gi.host.encode("idna") != gi.host.encode("idna"): # follow = input("Follow cross-domain redirect to %s? (y/n) " % new_gi.url) # # Never follow cross-protocol redirects without asking # elif new_gi.scheme != gi.scheme: # follow = input("Follow cross-protocol redirect to %s? (y/n) " % new_gi.url) # # Don't follow *any* redirect without asking if auto-follow is off # elif not self.options["auto_follow_redirects"]: # follow = input("Follow redirect to %s? (y/n) " % new_gi.url) # # Otherwise, follow away else: follow = "yes" if follow.strip().lower() not in ("y", "yes"): raise UserAbortException() previous_redirectors.add(url) # if status == "31": # # Permanent redirect # self.permanent_redirects[gi.url] = new_gi.url return _fetch_gemini(newurl, interactive=interactive) # Errors elif status.startswith("4") or status.startswith("5"): raise RuntimeError(meta) # Client cert elif status.startswith("6"): if interactive: print(_("You need to provide a client-certificate to access this page.")) url_with_identity = ask_certs(url) if (url_with_identity != url): return fetch(url_with_identity) error = _("You need to provide a client-certificate to access this page.\r\nType \"certs\" to create or re-use one") raise RuntimeError(error) # Invalid status elif not status.startswith("2"): raise RuntimeError(_("Server returned undefined status code %s!") % status) # If we're here, this must be a success and there's a response body assert status.startswith("2") mime = meta # Read the response body over the network fbody = f.read() # DEFAULT GEMINI MIME if mime == "": mime = "text/gemini; charset=utf-8" shortmime, mime_options = parse_mime(mime) if "charset" in mime_options: try: codecs.lookup(mime_options["charset"]) except LookupError: # raise RuntimeError("Header declared unknown encoding %s" % mime_options) # If the encoding is wrong, there’s a high probably it’s UTF-8 with a bad header mime_options["charset"] = "UTF-8" if shortmime.startswith("text/"): # Get the charset and default to UTF-8 in none encoding = mime_options.get("charset", "UTF-8") try: body = fbody.decode(encoding) except UnicodeError: raise RuntimeError( _("Could not decode response body using %s\ encoding declared in header!") % encoding ) else: body = fbody cache = write_body(url, body, mime) return cache, url #fetch returns two things: #cachepath: the path to the cached ressource #newurl: the real URL of that cached ressource def fetch( url, offline=False, download_image_first=True, images_mode="readable", validity=0, cookiejar=None, redirects={}, #blocked is empty by default to allow blocking rules having been removed blocked={}, **kwargs, ): url = normalize_url(url) newurl = url path = None print_error = "print_error" in kwargs.keys() and kwargs["print_error"] #Step 0: we apply redirect and/or block #Let’s add the blocked list into the redirects #This will not overwrite existing rule for that domain for b in blocked: if b not in redirects.keys(): redirects[b] = "blocked" redirection, key = get_url_redirected(url,redirects,returnkey=True) if redirection and redirection.lower() == "blocked": text = "" text += _("Blocked URL: ")+url + "\n" text += _("This website has been blocked with the following rule:\n") text += key + "\n" text += _("Use the following redirect command to unblock it:\n") text += "redirect %s NONE" %key if print_error: print(text) cache = set_error(newurl, text) return cache, newurl elif redirection: parsed = urllib.parse.urlparse(url) parsed = parsed._replace(netloc=redirection) url = urllib.parse.urlunparse(parsed) newurl = url # First, we look if we have a valid cache, even if offline # If we are offline, any cache is better than nothing if is_cache_valid(url, validity=validity) or ( offline and is_cache_valid(url, validity=0) ): path = get_cache_path(url) # if the cache is a folder, we should add a "/" at the end of the URL if not url.endswith("/") and os.path.isdir( get_cache_path(url, add_index=False) ): newurl = url + "/" elif offline and is_cache_valid(url, validity=0): path = get_cache_path(url) elif "://" in url and not offline: try: scheme = url.split("://")[0] if scheme not in standard_ports: if print_error: print(_("%s is not a supported protocol") % scheme) path = None elif scheme in ("http", "https"): if _DO_HTTP: if download_image_first and cookiejar is None: cookiejar = get_cookiejar(newurl) path = _fetch_http(newurl, cookiejar=cookiejar, **kwargs) else: print(_("HTTP requires python-requests")) elif scheme == "gopher": path = _fetch_gopher(newurl, **kwargs) elif scheme == "finger": path = _fetch_finger(newurl, **kwargs) elif scheme == "gemini": path, newurl = _fetch_gemini(url, **kwargs) elif scheme == "spartan": path, newurl = _fetch_spartan(url, **kwargs) else: print("scheme %s not implemented yet" % scheme) except UserAbortException: return None, newurl except Exception as err: cache = set_error(newurl, err) # Print an error message # we fail silently when sync_only if isinstance(err, socket.gaierror): if print_error: print(_("ERROR: DNS error!")) elif isinstance(err, ConnectionRefusedError): if print_error: print(_("ERROR1: Connection refused!")) elif isinstance(err, ConnectionResetError): if print_error: print(_("ERROR2: Connection reset!")) elif isinstance(err, (TimeoutError, socket.timeout)): if print_error: print(_("""ERROR3: Connection timed out! Slow internet connection? Use 'set timeout' to be more patient.""")) elif isinstance(err, FileExistsError): if print_error: print(_("""ERROR5: Trying to create a directory which already exists in the cache : """)) print(err) elif _DO_HTTP and isinstance(err, requests.exceptions.SSLError): if print_error: print(_("""ERROR6: Bad SSL certificate:\n""")) print(err) print( _("""\n If you know what you are doing, you can try to accept bad certificates with the following command:\n""") ) print("""set accept_bad_ssl_certificates True""") elif _DO_HTTP and isinstance(err, requests.exceptions.ConnectionError): if print_error: print(_("""ERROR7: Cannot connect to URL:\n""")) print(str(err)) else: if print_error: import traceback print(_("ERROR4: ") + str(type(err)) + " : " + str(err)) # print("\n" + str(err.with_traceback(None))) print(traceback.format_exc()) return cache, newurl # We download images contained in the document (from full mode) if not offline and download_image_first and images_mode: renderer = ansicat.renderer_from_file(path, newurl,redirectlist=redirects,**kwargs) if renderer: for image in renderer.get_images(mode=images_mode): # Image should exist, should be an url (not a data image) # and should not be already cached if ( image and not image.startswith("data:image/") and not is_cache_valid(image) ): width = offutils.term_width() - 1 toprint = _("Downloading %s") % image toprint = toprint[:width] toprint += " " * (width - len(toprint)) print(toprint, end="\r") # d_i_f and images_mode are False/None to avoid recursive downloading # if that ever happen fetch( image, offline=offline, download_image_first=False, images_mode=None, validity=0, cookiejar=cookiejar, redirects=redirects, **kwargs, ) if download_image_first and cookiejar is not None: cookiejar.save() return path, newurl def main(): descri = _("Netcache is a command-line tool to retrieve, cache and access networked content.\n\ By default, netcache will returns a cached version of a given URL, downloading it \ only if a cache version doesn't exist. A validity duration, in seconds, can also \ be given so netcache downloads the content only if the existing cache is older than the validity.") # Parse arguments parser = argparse.ArgumentParser(prog="netcache", description=descri) parser.add_argument( "--path", action="store_true", help=_("return path to the cache instead of the content of the cache"), ) parser.add_argument( "--ids", action="store_true", help=_("return a list of id's for the gemini-site instead of the content of the cache"), ) parser.add_argument( "--offline", action="store_true", help=_("Do not attempt to download, return cached version or error"), ) parser.add_argument( "--max-size", type=int, help=_("Cancel download of items above that size (value in Mb)."), ) parser.add_argument( "--timeout", type=int, help=_("Time to wait before cancelling connection (in second)."), ) parser.add_argument( "--cache-validity", type=int, default=0, help=_("maximum age, in second, of the cached version before \ redownloading a new version"), ) # No argument: write help parser.add_argument( "url", metavar="URL", nargs="*", help=_("download URL and returns the content or the path to a cached version"), ) # --validity : returns the date of the cached version, Null if no version # --force-download : download and replace cache, even if valid args = parser.parse_args() param = {} for u in args.url: if args.offline: path = get_cache_path(u) elif args.ids: ids = _get_site_ids(u) else: path, url = fetch( u, max_size=args.max_size, timeout=args.timeout, validity=args.cache_validity, redirects=offblocklist.redirects, blocked=offblocklist.blocked, ) if args.path: print(path) elif args.ids: print(ids) else: with open(path, "r") as f: print(f.read()) f.close() if __name__ == "__main__": main() offpunk-v3.0/netcache_migration.py000066400000000000000000000021301514232770500174110ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2023 Sotiris Papatheodorou # SPDX-License-Identifier: BSD-2-Clause """ A script to migrate the offpunk cache to the newest version. For each new version of offpunk that requires changes to the cache a migration function should be written. The name of the function should have the format v__ and it should accept the offpunk cache directory as a string. The function should perform a migration from the immediately previous cache format. All migration functions must be called at the end of this script from oldest to newest. """ import os import os.path def upgrade_to_1(cache_dir: str) -> None: """ Rename index.txt to gophermap in the Gopher protocol cache. """ print("Upgrading cache to version 1: migrating index.txt to gophermap") for root, _, files in os.walk(os.path.join(cache_dir, "gopher")): for f in files: if f == "index.txt": src = os.path.join(root, f) dst = os.path.join(root, "gophermap") os.rename(src, dst) offpunk-v3.0/offblocklist.py000066400000000000000000000015311514232770500162530ustar00rootroot00000000000000# The following are the default redirections from Offpunk # Those are by default because they should make sens with offpunk redirects = { "*reddit.com" : "teddit.net", "*medium.com" : "scribe.rip", } #following are blocked URLs. Visiting them with offpunk doesn’t make sense. #Blocking them will save a lot of bandwith blocked = { "twitter.com", "x.com", # "youtube.com", # "youtu.be", "facebook.com", "facebook.net", "fbcdn.net", "linkedin.com", "*licdn.com", "*admanager.google.com", "*google-health-ads.blogspot.com", "*firebase.google.com", "*google-webfonts-helper.herokuapp.com", "*tiktok.com" , "*doubleclick.net", "*google-analytics.com" , "*ads.yahoo.com", "*advertising.amazon.com", "*advertising.theguardian.com", "*advertise.newrepublic.com", } offpunk-v3.0/offpunk.py000077500000000000000000003207271514232770500152600ustar00rootroot00000000000000#!/usr/bin/env python3 # Offpunk Offline Gemini client """ Offline-First Gemini/Web/Gopher/RSS reader and browser """ __version__ = "3.0" # Initial imports and conditional imports {{{ import argparse import cmd import os import os.path import shutil import sys import time import urllib.parse import gettext import ansicat import netcache import offblocklist import offthemes import openk from offutils import ( is_local, looks_like_url, mode_url, run, term_width, unmode_url, xdg, init_config, send_email, _HAS_XDGOPEN, _LOCALE_DIR, find_root, ) gettext.bindtextdomain('offpunk', _LOCALE_DIR) gettext.textdomain('offpunk') _ = gettext.gettext try: import setproctitle setproctitle.setproctitle("offpunk") _HAS_SETPROCTITLE = True except ModuleNotFoundError: _HAS_SETPROCTITLE = False # This method copy a string to the system clipboard def clipboard_copy(to_copy): copied = False if shutil.which("xsel"): run("xsel -b -i", input=to_copy, direct_output=True) copied = True if shutil.which("xclip"): run("xclip -selection clipboard", input=to_copy, direct_output=True) copied = True if shutil.which("wl-copy"): run("wl-copy", input=to_copy, direct_output=True) copied = True if not copied: print(_("Install xsel/xclip (X11) or wl-clipboard (Wayland) to use copy")) # This method returns an array with all the values in all system clipboards def clipboard_paste(): # We use a set to avoid duplicates clipboards = set() commands = set() pasted = False if shutil.which("xsel"): pasted = True for selec in ["-p", "-s", "-b"]: commands.add("xsel " + selec) if shutil.which("xclip"): pasted = True for selec in ["clipboard", "primary", "secondary"]: commands.add("xsel " + selec) if shutil.which("wl-paste"): pasted = True for selec in ["", "-p"]: commands.add("wl-paste " + selec) for command in commands: try: clipboards.add(run(command)) except Exception: # print("Skippink clipboard %s because %s"%(selec,err)) pass if not pasted: print( _("Install xsel/xclip (X11) or wl-clipboard (Wayland) to get URLs from your clipboard") ) return list(clipboards) # }}} end of imports # Command abbreviations _ABBREVS = {# {{{ "..": "up", "a": "add", "b": "back", "bb": "blackbox", "bm": "bookmarks", "book": "bookmarks", "cert": "certs", "cp": "copy", "coo": "cookies", "f": "forward", "g": "go", "h": "history", "hist": "history", "l": "view", "less": "view", "man": "help", "mv": "move", "n": "next", "off": "offline", "on": "online", "p": "previous", "prev": "previous", "q": "quit", "r": "reload", "s": "save", "se": "search", "/": "find", "t": "tour", "u": "up", "v": "view", "w": "wikipedia", "wen": "wikipedia en", "wfr": "wikipedia fr", "wes": "wikipedia es", "wgl": "wikipedia gl", "wde": "wikipedia de", "yy": "copy url", # That’s an Easter Egg for Vimium users ;-) "abbrevs": "alias", }# }}} # GeminiClient Decorators # decorator to be sure that self.current_url exists def needs_gi(inner): def outer(self, *args, **kwargs): if not self.current_url: print(_("You need to 'go' somewhere, first")) return None else: return inner(self, *args, **kwargs) outer.__doc__ = inner.__doc__ return outer #red warning to print REDERROR="\x1b[1;31m"+_("Error: ")+"\x1b[0m" class GeminiClient(cmd.Cmd): def __init__(self, completekey="tab", sync_only=False): super().__init__(completekey=completekey) # Set umask so that nothing we create can be read by anybody else. # The certificate cache and TOFU database contain "browser history" # type sensitivie information. os.umask(0o077) self.opencache = openk.opencache() self.theme = offthemes.default self.current_url = None self.hist_index = 0 self.marks = {} self.page_index = 0 # Sync-only mode is restricted by design self.offline_only = False self.sync_only = sync_only self.support_http = netcache._DO_HTTP self.options = { "debug": False, "beta": False, "timeout": 600, "short_timeout": 5, "width": 72, "auto_follow_redirects": True, "tls_mode": "tofu", "archives_size": 200, "history_size": 200, "max_size_download": 10, "editor": None, "images_mode": "readable", "redirects": True, # the wikipedia entry needs two %s, one for lang, other for search "wikipedia": "gemini://gemi.dev/cgi-bin/wp.cgi/view/%s?%s", "search": "gemini://kennedy.gemi.dev/search?%s", "websearch": "https://wiby.me/?q=%s", "accept_bad_ssl_certificates": False, "default_protocol": "gemini", "ftr_site_config": None, "preformat_wrap": False, # images_size should be an integer. If bigger than text width, # it will be reduced "images_size": 100, # avaliable linkmode are "none" and "end". "linkmode": "none", #command that will be used on empty line, "default_cmd": "links 10", # user prompt in on and offline mode "prompt_on": "ON", "prompt_off": "OFF", "prompt_close": "> ", "gemini_images": True, } self.set_prompt("ON") self.opencache.redirects = offblocklist.redirects for i in offblocklist.blocked: self.opencache.redirects[i] = "blocked" term_width(new_width=self.options["width"]) self.log = { "start_time": time.time(), } def set_prompt(self, prompt): key = "prompt_%s" % prompt.lower() # default color is green colors = self.theme.get(key, ["green"]) open_color = "" close_color = "" for color in colors: # default to green 32 if color name `green` is not found ansi = offthemes.colors.get(color, ["32", "39"]) open_color += "%s;" % ansi[0] close_color += "%s;" % ansi[1] # removing the last ";" open_color = open_color.rstrip(";") close_color = close_color.rstrip(";") self.prompt = ( "\001\x1b[%sm\002" % open_color + self.options[key] + "\001\x1b[%sm\002" % close_color + self.options["prompt_close"] ) # support for 256 color mode: # self.prompt = "\001\x1b[38;5;76m\002" + "ON" + "\001\x1b[38;5;255m\002" + "> " + "\001\x1b[0m\002" return self.prompt def complete_list(self, text, line, begidx, endidx): allowed = [] cmds = ["create", "edit", "subscribe", "freeze", "normal", "delete", "help"] lists = self.list_lists() words = len(line.split()) # We need to autocomplete listname for the first or second argument # If the first one is a cmds if words <= 1: allowed = lists + cmds elif words == 2: # if text, the completing word is the second if text: allowed = lists + cmds else: current_cmd = line.split()[1] if current_cmd in ["help", "create"]: allowed = [] elif current_cmd in cmds: allowed = lists elif words == 3 and text != "": current_cmd = line.split()[1] if current_cmd in ["help", "create"]: allowed = [] elif current_cmd in cmds: allowed = lists return [i + " " for i in allowed if i.startswith(text)] def complete_add(self, text, line, begidx, endidx): if len(line.split()) == 2 and text != "": allowed = self.list_lists() elif len(line.split()) == 1: allowed = self.list_lists() else: allowed = [] return [i + " " for i in allowed if i.startswith(text)] def complete_move(self, text, line, begidx, endidx): return self.complete_add(text, line, begidx, endidx) def complete_tour(self, text, line, begidx, endidx): return self.complete_add(text, line, begidx, endidx) def complete_theme(self, text, line, begidx, endidx): elements = offthemes.default colors = offthemes.colors words = len(line.split()) if words <= 1: allowed = elements elif words == 2 and text != "": allowed = elements else: allowed = colors return [i + " " for i in allowed if i.startswith(text)] def get_renderer(self, url=None): # If launched without argument, we return the renderer for the current URL if not url: url = self.current_url if url: # we should pass the options to the renderer return self.opencache.get_renderer(url, theme=self.theme,**self.options) def _go_to_url( self, url, update_hist=True, force_refresh=False, handle=True, grep=None, name=None, mode=None, limit_size=False, force_large_download=False, ): """This method might be considered "the heart of Offpunk". Everything involved in fetching a gemini resource happens here: sending the request over the network, parsing the response, storing the response in a temporary file, choosing and calling a handler program, and updating the history. Nothing is returned.""" if not url: return url, newmode = unmode_url(url) if not mode: mode = newmode # we don’t handle the name anymore ! if name: print(_("We don’t handle name of URL: %s") % name) # Code to translate URLs to better frontends (think twitter.com -> nitter) parsed = urllib.parse.urlparse(url) netloc = parsed.netloc if netloc.startswith("www."): netloc = netloc[4:] params = {} params["timeout"] = self.options["short_timeout"] if limit_size: params["max_size"] = int(self.options["max_size_download"]) * 1000000 params["print_error"] = not self.sync_only params["interactive"] = not self.sync_only params["offline"] = self.offline_only params["accept_bad_ssl_certificates"] = self.options[ "accept_bad_ssl_certificates" ] params["ftr_site_config"] = self.options["ftr_site_config"] params["preformat_wrap"] = self.options["preformat_wrap"] if mode: params["images_mode"] = mode else: params["images_mode"] = self.options["images_mode"] params["images_size"] = self.options["images_size"] params["gemini_images"] = self.options["gemini_images"] # avaliable linkmode are "none" and "end". params["linkmode"] = self.options["linkmode"] if force_refresh: params["validity"] = 1 elif not self.offline_only: # A cache is always valid at least 60seconds params["validity"] = 60 params["force_large_download"] = force_large_download # Use cache or mark as to_fetch if resource is not cached if handle and not self.sync_only: displayed, url = self.opencache.openk( url, mode=mode, grep=grep, theme=self.theme,**params ) modedurl = mode_url(url, mode) if not displayed: # if we can’t display, we mark to sync what is not local if not is_local(url) and not netcache.is_cache_valid(url): self.get_list("to_fetch") r = self.list_add_line("to_fetch", url=modedurl, verbose=False) if r: print(_("%s not available, marked for syncing") % url) else: print(_("%s already marked for syncing") % url) else: self.page_index = 0 # Update state (external files are not added to history) self.current_url = modedurl if update_hist and not self.sync_only: self._update_history(modedurl) else: # we are asked not to handle or in sync_only mode if self.support_http or parsed.scheme not in ["http", "https"]: netcache.fetch(url, redirects=self.opencache.redirects,**params) @needs_gi def _show_lookup(self, offset=0, end=None, show_url=False): l = self.get_renderer().get_links() for n, u in enumerate(l[offset:end]): index = n + offset + 1 line = "[%s] %s" % (index, u) print(line) def _update_history(self, url): # We never update while in sync_only # We don’t add history to itself. if self.sync_only or not url or url == "list:///history": return # First, we call get_list to create history if needed self.get_list("history") # Don’t update history if we are back/forwarding through it if self.hist_index > 0: links = self.list_get_links("history") length = len(links) if length > 0 and links[self.hist_index] == url: return self.list_add_top( "history", limit=self.options["history_size"], truncate_lines=self.hist_index, ) self.hist_index = 0 # Cmd implementation follows def default(self, line, verbose=True): if line.strip() == "EOF": self.onecmd("quit") return True elif line.startswith("/"): self.do_find(line[1:]) return True # Expand abbreviated commands first_word = line.split()[0].strip() if first_word in _ABBREVS: full_cmd = _ABBREVS[first_word] expanded = line.replace(first_word, full_cmd, 1) self.onecmd(expanded) return True # Try to access it like an URL if looks_like_url(line): self.do_go(line) return True # Try to parse numerical index for lookup table try: n = int(line.strip()) except ValueError: if verbose: print(_("What?")) return False # if we have no url, there's nothing to do if self.current_url is None: if verbose: print(_("No links to index")) return False else: r = self.get_renderer() if r: url = r.get_link(n) self._go_to_url(url) else: print(_("No page with links")) return False # Settings def do_redirect(self, line): """Display and manage the list of redirected URLs. This features is mostly useful to use privacy-friendly frontends for popular websites.""" if len(line.split()) == 1: if line in self.opencache.redirects: print(_("%s is redirected to %s") % (line, self.opencache.redirects[line])) else: print(_("Please add a destination to redirect %s") % line) elif len(line.split()) >= 2: orig, dest = line.split(" ", 1) if dest.lower() == "none": if orig in self.opencache.redirects: self.opencache.redirects.pop(orig) print(_("Redirection for %s has been removed") % orig) else: print(_("%s was not redirected. Nothing has changed.") % orig) elif dest.lower() == "block": self.opencache.redirects[orig] = "blocked" print(_("%s will now be blocked") % orig) else: self.opencache.redirects[orig] = dest print(_("%s will now be redirected to %s") % (orig, dest)) #refreshing the cache for coloured redirects self.opencache.cleanup() else: toprint = _("Current redirections:\n") toprint += "--------------------\n" for r in self.opencache.redirects: toprint += "%s\t->\t%s\n" % (r, self.opencache.redirects[r]) toprint += _('\nTo add new, use "redirect origine.com destination.org"') toprint += _('\nTo remove a redirect, use "redirect origine.com NONE"') toprint += ( _('\nTo completely block a website, use "redirect origine.com BLOCK"') ) toprint += _('\nTo block also subdomains, prefix with *: "redirect *origine.com BLOCK"') print(toprint) def do_set(self, line): """View or set various options.""" if not line.strip(): # Show all current settings for option in sorted(self.options.keys()): print("%s %s" % (option, self.options[option])) elif len(line.split()) == 1: # Show current value of one specific setting option = line.strip() if option in self.options: print("%s %s" % (option, self.options[option])) else: print(_("Unrecognised option %s") % option) else: # Set value of one specific setting option, value = line.split(" ", 1) if option not in self.options: print(_("Unrecognised option %s") % option) return # Validate / convert values elif option == "tls_mode": if value.lower() not in ("ca", "tofu"): print(_("TLS mode must be `ca` or `tofu`!")) return elif option == "accept_bad_ssl_certificates": if value.lower() == "false": print(_("Only high security certificates are now accepted")) elif value.lower() == "true": print(_("Low security SSL certificates are now accepted")) else: #TRANSLATORS keep accept_bad_ssl_certificates, True, and False print(_("accept_bad_ssl_certificates should be True or False")) return elif option == "width": if value.isnumeric(): value = int(value) print(_("changing width to "), value) term_width(new_width=value) else: print(_("%s is not a valid width (integer required)") % value) elif option == "linkmode": if value.lower() not in ("none", "end"): print(_("Avaliable linkmode are `none` and `end`.")) return elif value.isnumeric(): value = int(value) elif value.lower() == "false": value = False elif value.lower() == "true": value = True elif value.startswith('"') and value.endswith('"'): # unquote values if they are quoted value = value[1:-1] else: try: value = float(value) except ValueError: pass self.options[option] = value #We clean the cache for some options that affect rendering if option in ["preformat_wrap","width", "linkmode","gemini_images"]: self.opencache.cleanup() def do_theme(self, line): """Change the colors of your rendered text. "theme ELEMENT COLOR" ELEMENT is one of: window_title, window_subtitle, title, subtitle,subsubtitle,link,oneline_link,new_link,image_link,preformatted,blockquote,\ blocked_link. COLOR is one or many (separated by space) of: bold, faint, italic, underline, black, red, green, yellow, blue, purple, cyan, white. Each color can alternatively be prefaced with "bright_". If color is "none", then that part of the theme is removed. theme can also be used with "preset" to load an existing theme. "theme preset" : show available themes "theme preset PRESET_NAME" : swith to a given preset""" words = line.split() le = len(words) if le == 0: t = self.get_renderer("list:///").get_theme() for e in t: print("%s set to %s" % (e, t[e])) else: element = words[0] if element == "preset": if le == 1: print(_("Available preset themes are: ")) print(" - default") for k in offthemes.themes.keys(): print(" - %s"%k) elif words[1] == "default": for key in offthemes.default: self.theme[key] = offthemes.default[key] self.opencache.cleanup() elif words[1] in offthemes.themes.keys(): #every preset is applied assuming default #so we must apply default first! for theme in [offthemes.default,offthemes.themes[words[1]]]: for key in theme: self.theme[key] = theme[key] self.opencache.cleanup() else: print(_("%s is not a valid preset theme")%words[1]) elif element not in offthemes.default.keys(): print(_("%s is not a valid theme element") % element) print(_("Valid theme elements are: ")) valid = [] for k in offthemes.default: valid.append(k) print(valid) return else: if le == 1: if element in self.theme.keys(): value = self.theme[element] else: value = offthemes.default[element] print(_("%s is set to %s") % (element, str(value))) elif le == 2 and words[1].lower() in ["none"]: if element in self.theme.keys(): value = self.theme[element] self.theme.pop(element) print(_("%s reset (it was set to %s)"%(element,value))) self.opencache.cleanup() else: print(_("%s is not set. Nothing to do"%element)) else: # Now we parse the colors for w in words[1:]: if w not in offthemes.colors.keys(): print(_("%s is not a valid color") % w) print(_("Valid colors are one of: ")) valid = [] for k in offthemes.colors: valid.append(k) print(valid) return self.theme[element] = words[1:] self.opencache.cleanup() # now we update the prompt if self.offline_only: self.set_prompt("OFF") else: self.set_prompt("ON") def do_handler(self, line): """View or set handler commands for different MIME types. handler MIMETYPE : see handler for MIMETYPE handler MIMETYPE CMD : set handler for MIMETYPE to CMD in the CMD, %s will be replaced by the filename. if no %s, it will be added at the end. MIMETYPE can be the true mimetype or the file extension. Examples: handler application/pdf zathura %s handler .odt lowriter handler docx lowriter""" if not line.strip(): # Show all current handlers h = self.opencache.get_handlers() for mime in sorted(h.keys()): print("%s %s" % (mime, h[mime])) elif len(line.split()) == 1: mime = line.strip() h = self.opencache.get_handlers(mime=mime) if h: print("%s %s" % (mime, h)) else: print(_("No handler set for MIME type %s") % mime) else: mime, handler = line.split(" ", 1) self.opencache.set_handler(mime, handler) def do_alias(self, line): """Create or modifiy an alias alias : show all existing aliases alias ALIAS : show the command linked to ALIAS alias ALIAS CMD : create or replace existing ALIAS to be linked to command CMD""" #building the list of existing commands to avoid conflicts commands = [] for name in self.get_names(): if name.startswith("do_"): commands.append(name[3:]) if not line.strip(): header = "Command Aliases:" self.stdout.write("\n{}\n".format(str(header))) if self.ruler: self.stdout.write("{}\n".format(str(self.ruler * len(header)))) for k, v in _ABBREVS.items(): self.stdout.write("{:<7} {}\n".format(k, v)) self.stdout.write("\n") elif len(line.split()) == 1: alias = line.strip() if alias in commands: print(_("%s is a command and cannot be aliased")%alias) elif alias in _ABBREVS: print(_("%s is currently aliased to \"%s\"") %(alias,_ABBREVS[alias])) else: print(_("there’s no alias for \"%s\"")%alias) else: alias, cmd = line.split(None,1) if alias in commands: print(_("%s is a command and cannot be aliased")%alias) else: _ABBREVS[alias] = cmd print(_("%s has been aliased to \"%s\"")%(alias,cmd)) def do_offline(self, *args): """Use Offpunk offline by only accessing cached content""" if self.offline_only: print(_("Offline and undisturbed.")) else: self.offline_only = True self.set_prompt("OFF") print(_("Offpunk is now offline and will only access cached content")) def do_online(self, *args): """Use Offpunk online with a direct connection""" if self.offline_only: self.offline_only = False self.set_prompt("ON") print(_("Offpunk is online and will access the network")) else: print(_("Already online. Try offline.")) def do_copy(self, arg): """Copy the content of the last visited page as gemtext/html in the clipboard. Use with "url" as argument to only copy the adress. Use with "raw" to copy ANSI content as seen in your terminal (with colour codes). Use with "cache" to copy the path of the cached content. Use with "title" to copy the title of the page. Use with "link" to copy a link in the gemtext format to that page with the title.""" if self.current_url: args = arg.split() if args and args[0] == "url": if len(args) > 1 and args[1].isdecimal(): url = self.get_renderer().get_link(int(args[1])) else: url = unmode_url(self.current_url)[0] print(url) clipboard_copy(url) elif args and args[0] == "raw": tmp = self.opencache.get_temp_filename(self.current_url) if tmp: clipboard_copy(open(tmp, "rb")) elif args and args[0] == "cache": clipboard_copy(netcache.get_cache_path(self.current_url)) elif args and args[0] == "title": title = self.get_renderer().get_page_title() clipboard_copy(title) print(title) elif args and args[0] == "link": link = "=> %s %s" % ( unmode_url(self.current_url)[0], self.get_renderer().get_page_title(), ) print(link) clipboard_copy(link) else: clipboard_copy(open(netcache.get_cache_path(self.current_url), "rb")) else: print(_("No content to copy, visit a page first")) #Share current page by email def do_share(self, arg): """Send current page by email to someone else. Use with "url" as first argument to send only the address. Use with "text" as first argument to send the full content. TODO Without argument, "url" is assumed. Next arguments are the email adresses of the recipients. If no destination, you will need to fill it in your mail client.""" # default "share" case were users has to give the recipient if self.current_url: # we will not consider the url argument (which is the default) # if other argument, we will see if it is an URL if is_local(self.current_url): print(_("We cannot share %s because it is local only")%self.current_url) return else: r = self.get_renderer() #default share case dest = "" subject= r.get_page_title() body = unmode_url(self.current_url)[0] args = arg.split() if args : if args[0] == "text": args.pop(0) print(_("TODO: sharing text is not yet implemented")) return # we will not consider the url argument (which is the default) # if other argument, we will see if it is an URL elif args[0] == "url": args.pop(0) if len(args) > 0: for a in args: # we only takes arguments with @ as email adresses if "@" in a: dest += "," + a send_email(dest,subject=subject,body=body,toconfirm=False) #quick debug # print("Send mail to %s"%dest) # print("Subject is %s"%subject) # print("Body is %s"%body) else: print(_("Nothing to share, visit a page first")) #Reply to a page by finding a mailto link in the page def do_reply(self, arg): """Reply by email to a page by trying to find a good email for the author. If an email is provided as an argument, it will be used. arguments: - "save" : allows to detect and save email without actually sending an email. - "save new@email" : save a new reply email to replace an existing one""" args = arg.split(" ") if self.current_url: r = self.get_renderer() # The reply intelligence where we try to find a email address # Reply is not allowed for local URL (at least for now) if not is_local(self.current_url): potential_replies = [] # Add email adresses from arguments for a in args: if "@" in a: potential_replies.append(a) saved_replies = [] # First we look if we have a mail recorder for that URL # emails are recorded according to URL in XDG_DATA/offpunk/reply # We don’t care about the protocol because it is assumed that # a given URL will always have the same contact, even on different # protocols parents = find_root(self.current_url, return_value = "list") while len(potential_replies) == 0 and len(parents) > 0 : parurl = parents.pop(0) replyfile = netcache.get_cache_path(parurl,\ include_protocol=False, xdgfolder="data",subfolder="reply") if os.path.exists(replyfile): with open(replyfile) as f: for li in f.readlines(): #just a rough check that we have an email address l = li.strip() if "@" in l: potential_replies.append(l) saved_replies.append(l) f.close() #No mail recorded? Let’s look at the current page #We check for any mailto: link if len(potential_replies) == 0: for l in r.get_links(): if l.startswith("mailto:"): #parse mailto link to remove mailto: l = l.removeprefix("mailto:").split("?")[0] if l not in potential_replies: potential_replies.append(l) # if we have no reply address, we investigate parents page # Until we are at the root of users capsule/website/hole parents = find_root(self.current_url, return_value = "list") while len(potential_replies) == 0 and len(parents) > 0 : parurl = parents.pop(0) replydir = netcache.get_cache_path(parurl,xdgfolder="data",\ include_protocol=False,subfolder="reply") #print(replydir) par_rend = self.get_renderer(parurl) if par_rend: for l in par_rend.get_links(): if l.startswith("mailto:"): #parse mailto link to remove mailto: l = l.removeprefix("mailto:").split("?")[0] if l not in potential_replies: potential_replies.append(l) #print("replying to %s"%potential_replies) if len(potential_replies) > 1: stri = _("Multiple emails addresse were found:") + "\n" counter = 1 for mail in potential_replies: stri += "[%s] %s\n" %(counter,mail) counter += 1 stri += "[0] "+ _("None of the above") + "\n" stri += "---------------------\n" stri += _("Which email will you use to reply?") +" > " ans = input(stri) if ans.isdigit() and len(potential_replies) >= int(ans): if int(ans) == 0: dest = "" else : dest = potential_replies[int(ans)-1] else: dest = "" elif len(potential_replies) == 1: dest = potential_replies[0] else: stri = _("Enter the contact email for this page?") + "\n" stri += "> " ans = input(stri) dest = ans.strip() # Now, let’s save the email (if it is not already the case) tosaveurl = None if dest and dest not in saved_replies: rootname = find_root(self.current_url,return_value="name") rooturl = find_root(self.current_url) stri = _("Email address:") + " \t\x1b[1;32m" + dest + "\x1b[0m\n" stri += _("Do you want to save this email as a contact for") + "\n" stri += "[1] " + _("Current page only") + "\n" stri += "[2] " + _("The whole %s space")%rootname + " - " + rooturl + "\n" stri += "[0] " + _("Don’t save this email") + "\n" stri += "---------------------\n" stri += _("Your choice?") + " > " ans = input(stri) if ans.strip() == "1": tosaveurl = self.current_url elif ans.strip() == "2": tosaveurl = rooturl if tosaveurl: savefile = netcache.get_cache_path(tosaveurl,\ include_protocol=False, xdgfolder="data",subfolder="reply") # first, let’s creat all the folders needed savefolder = os.path.dirname(savefile) os.makedirs(savefolder, exist_ok=True) # Then we write the email with open(savefile,"w") as f: f.write(dest) f.close() if "save" in args: if tosaveurl and dest: print(_("Email %s has been recorded as contact for %s")%(dest,tosaveurl)) else: print(_("Nothing to save")) else: subject = "RE: "+ r.get_page_title() body = _("In reply to ") + unmode_url(self.current_url)[0] send_email(dest,subject=subject,body=body,toconfirm=False) else: print(_("We cannot reply to %s because it is local only")%self.current_url) else: print(_("Nothing to share, visit a page first")) def do_cookies(self, arg): """Manipulate cookies: "cookies import [url]" - import cookies from file to be used with [url] "cookies list [url]" - list existing cookies for current url default is listing cookies for current domain. To get a cookie as a txt file,use the cookie-txt extension for Firefox.""" al = arg.split() if len(al) == 0: al = ["list"] mode = al[0] url = self.current_url if mode == "list": if len(al) == 2: url = al[1] elif len(al) > 2: print(_("Too many arguments to list.")) return if not url: print(_("URL required (or visit a page).")) return cj = netcache.get_cookiejar(url) if not cj: print(_("Cookies not enabled for url")) return print(_("Cookies for url:")) for c in cj: #TRANSLATORS domain, path, expiration time, name, value print(_("%s %s expires:%s %s=%s") % (c.domain, c.path, time.ctime(c.expires), c.name, c.value)) return elif mode == "import": if len(al) < 2: print(_("File parameter required for import.")) return if len(al) == 3: url = al[2] elif len(al) > 3: print(_("Too many arguments to import")) return if not url: print(_("URL required (or visit a page).")) return cj = netcache.get_cookiejar(url, create=True) try: cj.load(os.path.expanduser(al[1])) cj.save() except FileNotFoundError: print(_("File not found")) return print(_("Imported.")) return print(_("Huh?")) return def do_go(self, line): """Go to a gemini URL or marked item.""" line = line.strip() if not line: clipboards = clipboard_paste() urls = [] for u in clipboards: if "://" in u and looks_like_url(u) and u not in urls: urls.append(u) if len(urls) > 1: stri = _("URLs in your clipboard\n") counter = 0 for u in urls: counter += 1 stri += "[%s] %s\n" % (counter, u) stri += _("Where do you want to go today ?> ") ans = input(stri) if ans.isdigit() and 0 < int(ans) <= len(urls): self.do_go(urls[int(ans) - 1]) elif len(urls) == 1: self.do_go(urls[0]) else: print(_("Go where? (hint: simply copy an URL in your clipboard)")) # First, check for possible marks elif line in self.marks: url = self.marks[line] self._go_to_url(url) # or a local file elif os.path.exists(os.path.expanduser(line)): self._go_to_url(line) # If this isn't a mark, treat it as a URL elif looks_like_url(line): self._go_to_url(line) elif ( "://" not in line and "default_protocol" in self.options.keys() and looks_like_url(self.options["default_protocol"] + "://" + line) ): self._go_to_url(self.options["default_protocol"] + "://" + line) else: print(_("%s is not a valid URL to go") % line) @needs_gi def do_reload(self, *args): """Reload the current URL.""" if self.offline_only and not is_local(self.current_url): self.get_list("to_fetch") r = self.list_add_line("to_fetch", url=self.current_url, verbose=False) if r: print(_("%s marked for syncing") % self.current_url) else: print(_("%s already marked for syncing") % self.current_url) self.opencache.clean_url(self.current_url) else: self.opencache.clean_url(self.current_url) self._go_to_url(self.current_url, force_refresh=False) @needs_gi def do_up(self, *args): """Go up one directory in the path. Take an integer as argument to go up multiple times. Use "~" to go to the user root" Use "/" to go to the server root.""" level = 1 if args[0].isnumeric(): level = int(args[0]) elif args[0] == "/": #yep, this is a naughty hack to go to root level = 1000 elif args[0] == "~": self.do_root() elif args[0] != "": print(_("Up only take integer as arguments")) url = unmode_url(self.current_url)[0] # UP code using the new find_root urllist = find_root(url,absolute=True,return_value="list") if len(urllist) > level: newurl = urllist[level] else: newurl = urllist[-1] # new up code ends up here self._go_to_url(newurl) def do_back(self, *args): """Go back to the previous gemini item.""" links = self.list_get_links("history") if self.hist_index >= len(links) - 1: return self.hist_index += 1 url = links[self.hist_index] self._go_to_url(url, update_hist=False) def do_forward(self, *args): """Go forward to the next gemini item.""" links = self.list_get_links("history") if self.hist_index <= 0: return self.hist_index -= 1 url = links[self.hist_index] self._go_to_url(url, update_hist=False) @needs_gi def do_root(self, *args): """Go to the root of current capsule/gemlog/page If arg is "/", the go to the real root of the server""" absolute = False if len(args) > 0 and args[0] == "/": absolute = True root = find_root(self.current_url,absolute=absolute) self._go_to_url(root) def do_tour(self, line): """Add index items as waypoints on a tour, which is basically a FIFO queue of gemini items. `tour` or `t` alone brings you to the next item in your tour. Items can be added with `tour 1 2 3 4` or ranges like `tour 1-4`. All items in current menu can be added with `tour *`. All items in $LIST can be added with `tour $LIST`. Current item can be added back to the end of the tour with `tour .`. Current tour can be listed with `tour ls` and scrubbed with `tour clear`.""" # Creating the tour list if needed self.get_list("tour") line = line.strip() if not line: # Fly to next waypoint on tour if len(self.list_get_links("tour")) < 1: print(_("End of tour.")) else: url = self.list_go_to_line("1", "tour") if url: self.list_rm_url(url, "tour") elif line == "ls": self.list_show("tour") elif line == "clear": for l in self.list_get_links("tour"): self.list_rm_url(l, "tour") elif line == "*": for l in self.get_renderer().get_links(): self.list_add_line("tour", url=l, verbose=False) elif line == ".": self.list_add_line("tour", verbose=False) elif looks_like_url(line): self.list_add_line("tour", url=line) elif line in self.list_lists(): list_path = self.list_path(line) if not list_path: print(REDERROR+_("List %s does not exist. Cannot add it to tour") % (list)) else: url = "list:///%s" % line for l in self.get_renderer(url).get_links(): self.list_add_line("tour", url=l, verbose=False) elif self.current_url: for index in line.split(): try: pair = index.split("-") if len(pair) == 1: # Just a single index n = int(index) url = self.get_renderer().get_link(n) self.list_add_line("tour", url=url, verbose=False) elif len(pair) == 2: # Two endpoints for a range of indices if int(pair[0]) < int(pair[1]): for n in range(int(pair[0]), int(pair[1]) + 1): url = self.get_renderer().get_link(n) self.list_add_line("tour", url=url, verbose=False) else: for n in range(int(pair[0]), int(pair[1]) - 1, -1): url = self.get_renderer().get_link(n) self.list_add_line("tour", url=url, verbose=False) else: # Syntax error print(_("Invalid use of range syntax %s, skipping") % index) except ValueError: print(_("Non-numeric index %s, skipping.") % index) except IndexError: print(_("Invalid index %d, skipping.") % n) @needs_gi def do_certs(self, line) -> None: """Manage your client certificates (identities) for a site. `certs` will display all valid certificates for the current site `certs new ` will create a new certificate, if no url is specified, the current open site will be used.""" line = line.strip() if not line: url_with_identity = netcache.ask_certs(self.current_url) if url_with_identity != self.current_url: self.onecmd("go " + url_with_identity) else: lineparts = line.split(" ") if lineparts[0] == "new": if len(lineparts) == 4: name = lineparts[1] days = lineparts[2] site = lineparts[3] netcache.create_certificate(name, int(days), site) elif len(lineparts) == 3: name = lineparts[1] days = lineparts[2] site = urllib.parse.urlparse(self.current_url) netcache.create_certificate(name, int(days), site.hostname) else: print("usage") @needs_gi def do_mark(self, line): """Mark the current item with a single letter. This letter can then be passed to the 'go' command to return to the current item later. Think of it like marks in vi: 'mark a'='ma' and 'go a'=''a'. Marks are temporary until shutdown (not saved to disk).""" line = line.strip() if not line: for mark, gi in self.marks.items(): print("[%s] %s (%s)" % (mark, gi.name, gi.url)) elif line.isalpha() and len(line) == 1: self.marks[line] = self.current_url else: print(_("Invalid mark, must be one letter")) @needs_gi def do_info(self, line): """Display information about current page.""" renderer = self.get_renderer() url = unmode_url(self.current_url)[0] out = renderer.get_page_title() + "\n\n" #TRANSLATORS: this string and "Mime", "Cache", "Renderer" are formatted to align. #if you can obtain the same effect in your language, try to do it ;) #they are displayed with the "info" command out += _("URL : ") + url + "\n" out += _("Mime : ") + renderer.get_mime() + "\n" out += _("Cache : ") + netcache.get_cache_path(url) + "\n" if self.get_renderer(): rend = str(self.get_renderer().__class__) rend = rend.lstrip("") else: rend = "None" out += _("Renderer : ") + rend + "\n" out += _("Cleaned with : ") + renderer.get_cleanlib() + "\n\n" lists = [] for l in self.list_lists(): if self.list_has_url(url, l): lists.append(l) if len(lists) > 0: out += _("Page appeard in following lists :\n") for l in lists: if not self.list_is_system(l): status = _("normal list") if self.list_is_subscribed(l): status = _("subscription") elif self.list_is_frozen(l): status = _("frozen list") out += " • %s\t(%s)\n" % (l, status) for l in lists: if self.list_is_system(l): out += " • %s\n" % l else: out += _("Page is not save in any list") print(out) def do_version(self, line,print_color=True): """Display version and system information.""" def has(value,color=print_color): toprint = "\t" if value: if color: toprint += "\x1b[1;32m" toprint += "Installed" else: if color: toprint += "\x1b[1;31m" toprint += "Not Installed" if color: toprint += "\x1b[0m" return toprint + "\n" output = "Offpunk " + __version__ + "\n" output += "===========\n" output += _("System: ") + sys.platform + "\n" output += _("Python: ") + sys.version + "\n" output += _("Language: ") + os.getenv('LANG') + "\n" output += _("\nHighly recommended:\n") output += " - xdg-open : " + has(_HAS_XDGOPEN) output += _("\nWeb browsing:\n") output += " - python-requests : " + has(netcache._DO_HTTP) output += " - python-feedparser : " + has(ansicat._DO_FEED) output += " - python-bs4 : " + has(ansicat._HAS_SOUP) output += " - python-readability : " + has(ansicat._HAS_READABILITY) output += " - timg 1.3.2+ : " + has(ansicat._HAS_TIMG) output += " - chafa 1.10+ : " + has(ansicat._HAS_CHAFA) output += _("\nNice to have:\n") output += " - python-setproctitle : " + has(_HAS_SETPROCTITLE) output += " - python-cryptography : " + has(netcache._HAS_CRYPTOGRAPHY) clip_support = shutil.which("xsel") or shutil.which("xclip") output += " - X11 clipboard (xsel or xclip) : " + has(clip_support) output += " - Wayland clipboard (wl-clipboard): " + has(shutil.which("wl-copy")) output += _("\nFeatures :\n") output += _(" - Render images (chafa or timg) : ") + has( ansicat._RENDER_IMAGE ) output += _(" - Render HTML (bs4, readability) : ") + has( ansicat._DO_HTML ) output += _(" - Render Atom/RSS feeds (feedparser) : ") + has( ansicat._DO_FEED ) output += _(" - Connect to http/https (requests) : ") + has( netcache._DO_HTTP ) output += _(" - Detect text encoding (python-chardet) : ") + has( netcache._HAS_CHARDET ) output += _(" - restore last position (less 572+) : ") + has( openk._LESS_RESTORE_POSITION ) output += "\n" output += _("ftr_site_config : ") + str(self.options["ftr_site_config"]) + "\n" output += _("Config directory : ") + xdg("config") + "\n" output += _("User Data directory : ") + xdg("data") + "\n" output += _("Cache directoy : ") + xdg("cache") if print_color: print(output) else: return output def do_bugreport(self,line): """Send a mail to the offpunk-devel list with technical informations about your offpunk version. You will be prompted to write an email describing how to reproduce the bug.""" report = self.do_version(line, print_color=False) firstline = report.split("\n")[0].removeprefix("Offpunk ") print(_("Found a bug in Offpunk? You can report it by email to the developers.")) dest = "~lioploum/offpunk-devel@lists.sr.ht" body = _("Please describe your problem as clearly as possible:") + "\n\n" body += _("Include all the steps to reproduce the problem, including the URLs you are currently visiting.") + "\n\n" body += _("Another point: always use \"reply-all\" when replying to this list.") body += report title = input(_("Describe the bug in one line: ")) if title.strip() != "": subject = "[BUG REPORT %s]"%firstline + " " + title send_email(dest,subject=subject,body=body,toconfirm=True) else: print(_("No description of the bug, report cancelled")) # Stuff that modifies the lookup table def do_search(self, line): """Search on Gemini using the engine configured (by default kennedy.gemi.dev) You can configure it using "set search URL". URL should contains one "%s" that will be replaced by the search term.""" search = urllib.parse.quote(line) url = self.options["search"] % search self._go_to_url(url) def do_websearch(self, line): """Search on the web using the engine configured (by default wiby.me) You can configure it using "set websearch URL". URL should contains one "%s" that will be replaced by the search term.""" search = urllib.parse.quote(line) url = self.options["websearch"] % search self._go_to_url(url) def do_wikipedia(self, line): """Search on wikipedia using the configured Gemini interface. The first word should be the two letters code for the language. Exemple : "wikipedia en Gemini protocol" But you can also use abbreviations to go faster: "wen Gemini protocol". (your abbreviation might be missing, report the bug) while it's not added, "w" is still an option you can use: "w en Gemini protocol" will work as a shortcut as well The interface used can be modified with the command: "set wikipedia URL" where URL should contains two "%s", the first one used for the language, the second for the search string.""" words = line.split(" ", maxsplit=1) if len(words[0]) == 2: lang = words[0] search = urllib.parse.quote(words[1]) else: lang = "en" search = urllib.parse.quote(line) url = self.options["wikipedia"] % (lang, search) self._go_to_url(url) def do_xkcd(self,line): """Open the specified XKCD comics (a number is required as parameter)""" words = line.split(" ") if len(words) > 0 and words[0].isalnum(): self._go_to_url("https://xkcd.com/%s"%words[0]) else: print(_("Please enter the number of the XKCD comic you want to see")) def do_gus(self, line): """Submit a search query to the geminispace.info search engine.""" if not line: print(_("What?")) return search = urllib.parse.quote(line) self._go_to_url("gemini://geminispace.info/search?%s" % search) def do_history(self, *args): """Display history.""" self.list_show("history") @needs_gi def do_find(self, searchterm): """Find in current page by displaying only relevant lines (grep).""" self._go_to_url(self.current_url, update_hist=False, grep=searchterm) def do_links(self, line): """Display all the links for the current page. If argument N is provided, then page through N links at a time. "links 10" show you the first 10 links, then 11 to 20, etc. if N = 0, then all the links are displayed""" args = line.split() increment = 0 if len(args) > 0 and args[0].isdigit(): increment = int(args[0]) elif len(args) == 0: # without argument, we reset the page index self.page_index = 0 i = self.page_index if not self.get_renderer() or i > len(self.get_renderer().get_links()): return if increment: self._show_lookup(offset=i, end=i + increment) self.page_index += increment else: self._show_lookup() def do_ls(self, line): """DEPRECATED: List contents of current index.""" print("ls is deprecated. Use links instead") self.do_links(line) def emptyline(self): """Default action when line is empty""" if "default_cmd" in self.options: cmd = self.options["default_cmd"] else: #fallback to historical links command cmd = "links" #if there’s a default command, we first run it #through default, in case it is an alias if cmd: success = self.default(cmd, verbose=False) # if no alias, we call onecmd() if not success: self.onecmd(cmd) @needs_gi def do_feed(self, *args): """Display RSS or Atom feeds linked to the current page.""" subs = self.get_renderer().get_subscribe_links() # No feed found if len(subs) == 1: if "rss" in subs[0][1] or "atom" in subs[0][1]: print(_("Current page is already a feed")) else: print(_("No feed found on current page")) # Multiple feeds found elif len(subs) > 2: stri = _("Available feeds :\n") counter = 0 for s in subs: counter += 1 stri += "[%s] %s [%s]\n" % (counter, s[0], s[1]) stri += _("Which view do you want to see ? >") ans = input(stri) if ans.isdigit() and 0 < int(ans) <= len(subs): self.do_go(subs[int(ans) - 1][0]) # Only one feed found else: self.do_go(subs[1][0]) @needs_gi def do_view(self, *args): """Run most recently visited item through "less" command, restoring \ previous position. Use "view normal" to see the default article view on html page. Use "view full" to see a complete html page instead of the article view. Use "view swich" to switch between normal and full Use "view XX" where XX is a number to view information about link XX. (full, feed, feeds have no effect on non-html content).""" if self.current_url and args and args[0] != "": if args[0] in ["full", "debug", "source"]: self._go_to_url(self.current_url, mode=args[0]) elif args[0] in ["normal", "readable"]: self._go_to_url(self.current_url, mode="readable") elif args[0] == "feed": #TRANSLATORS keep "view feed" and "feed" in english, those are literal commands print(_("view feed is deprecated. Use the command feed directly")) self.do_feed() elif args[0] == "switch": mode = unmode_url(self.current_url)[1] new_mode = "readable" if mode is not None and mode not in ["normal", "readable"] else "full" self._go_to_url(self.current_url, mode=new_mode) elif args[0].isdigit(): link_url = self.get_renderer().get_link(int(args[0])) if link_url: print(_("Link %s is: %s") % (args[0], link_url)) if netcache.is_cache_valid(link_url): last_modified = netcache.cache_last_modified(link_url) link_renderer = self.get_renderer(link_url) if link_renderer: link_title = link_renderer.get_page_title() print(link_title) else: print(_("Empty cached version")) print(_("Last cached on %s") % time.ctime(last_modified)) else: print(_("No cached version for this link")) else: print( #TRANSLATORS keep "normal, full, switch, source" in english _("Valid arguments for view are : normal, full, switch, source or a number") ) else: self._go_to_url(self.current_url) @needs_gi def do_open(self, *args): """Open current item with the configured handler or xdg-open. Use "open url" to open current URL in a browser. Use "open 2 4" to open links 2 and 4 You can combine with "open url 2 4" to open URL of links see "handler" command to set your handler.""" # do we open the URL (true) or the cached file (false) url_list = [] urlmode = False arglist = args[0].split() if len(arglist) > 0 and arglist[0] == "url": arglist.pop(0) urlmode = True if len(arglist) > 0: # we try to match each argument with a link for a in arglist: try: n = int(a) u = self.get_renderer().get_link(n) url_list.append(u) except ValueError: print(_("Non-numeric index %s, skipping.") % a) except IndexError: print(_("Invalid index %d, skipping.") % n) else: # if no argument, we use current url url = unmode_url(self.current_url)[0] url_list.append(url) for u in url_list: if urlmode: run("xdg-open %s", parameter=u, direct_output=True) else: self.opencache.openk(u, terminal=False) def do_shell(self, line): """Send the content of the current page to the shell and pipe it. You are supposed to write what will come after the pipe. For example, if you want to count the number of lines containing STRING in the current page: > shell grep STRING|wc -l '!' is an useful shortcut. > !grep STRING|wc -l""" # input is used if we wand to send something else than current page # to the shell tmp = None if self.current_url: tmp = self.opencache.get_temp_filename(self.current_url) if tmp: input = open(tmp, "rb") run(line, input=input, direct_output=True) else: run(line,direct_output=True) @needs_gi def do_save(self, line): """Save an item to the filesystem. 'save n filename' saves menu item n to the specified filename. 'save filename' saves the last viewed item to the specified filename. 'save n' saves menu item n to an automagic filename.""" args = line.strip().split() # First things first, figure out what our arguments are if len(args) == 0: # No arguments given at all # Save current item, if there is one, to a file whose name is # inferred from the gemini path if not netcache.is_cache_valid(self.current_url): print(_("You cannot save if not cached!")) return else: index = None filename = None elif len(args) == 1: # One argument given # If it's numeric, treat it as an index, and infer the filename try: index = int(args[0]) filename = None # If it's not numeric, treat it as a filename and # save the current item except ValueError: index = None filename = os.path.expanduser(args[0]) elif len(args) == 2: # Two arguments given # Treat first as an index and second as filename index, filename = args try: index = int(index) except ValueError: print(_("First argument is not a valid item index!")) return filename = os.path.expanduser(filename) else: print(_("You must provide an index, a filename, or both.")) return # Next, fetch the item to save, if it's not the current one. if index: last_url = self.current_url try: url = self.get_renderer().get_link(index) self._go_to_url(url, update_hist=False, handle=False) except IndexError: print(_("Index too high!")) self.current_url = last_url return else: url = self.current_url # Derive filename from current GI's path, if one hasn't been set if not filename: filename = os.path.basename(netcache.get_cache_path(self.current_url)) # Check for filename collisions and actually do the save if safe if os.path.exists(filename): print(_("File %s already exists!") % filename) else: # Don't use _get_active_tmpfile() here, because we want to save the # "source code" of menus, not the rendered view - this way Offpunk # can navigate to it later. path = netcache.get_cache_path(url) if os.path.isdir(path): print(_("Can’t save %s because it’s a folder, not a file") % path) else: print(_("Saved to %s") % filename) shutil.copyfile(path, filename) # Restore gi if necessary if index is not None: self._go_to_url(last_url, handle=False) @needs_gi def do_url(self, args): """Print the url of the current page. Use "url XX" where XX is a number to print the url of link XX. "url" can also be piped to the shell, using the pipe "|".""" splitted = args.split("|",maxsplit=1) url = None final_url = None if splitted[0].strip().isdigit(): link_id = int(splitted[0]) link_url = self.get_renderer().get_link(link_id) if link_url: url = link_url else: url = self.current_url if url: final_url = unmode_url(url)[0] print(final_url) if final_url and len(splitted) > 1: run(splitted[1], input=final_url, direct_output=True) # Bookmarking stuff @needs_gi def do_add(self, line): """Add the current URL to the list specified as argument. If no argument given, URL is added to Bookmarks. You can pass a link number as the second argument to add the link. "add $LIST XX" will add link number XX to $LIST""" args = line.split() if len(args) < 1: list = "bookmarks" if not self.list_path(list): self.list_create(list) self.list_add_line(list) elif len(args) > 1 and args[1].isdigit(): link_id = int(args[1]) link_url = self.get_renderer().get_link(link_id) if link_url: self.list_add_line(args[0],url=link_url) else: self.list_add_line(args[0]) # Get the list file name, creating or migrating it if needed. # Migrate bookmarks/tour/to_fetch from XDG_CONFIG to XDG_DATA # We migrate only if the file exists in XDG_CONFIG and not XDG_DATA def get_list(self, list): list_path = self.list_path(list) if not list_path: old_file_gmi = os.path.join(xdg("config"), list + ".gmi") old_file_nogmi = os.path.join(xdg("config"), list) target = os.path.join(xdg("data"), "lists") if os.path.exists(old_file_gmi): shutil.move(old_file_gmi, target) elif os.path.exists(old_file_nogmi): targetgmi = os.path.join(target, list + ".gmi") shutil.move(old_file_nogmi, targetgmi) else: if list == "subscribed": title = _("Subscriptions #subscribed (new links in those pages will be added to tour)") elif list == "to_fetch": title = _("Links requested and to be fetched during the next --sync") else: title = None self.list_create(list, title=title, quite=True) list_path = self.list_path(list) return list_path @needs_gi def do_subscribe(self, line): """Subscribe to current page by saving it in the "subscribed" list. If a new link is found in the page during a --sync, the new link is automatically fetched and added to your next tour. To unsubscribe, remove the page from the "subscribed" list.""" subs = self.get_renderer().get_subscribe_links() if len(subs) > 1: stri = _("Multiple feeds have been found :\n") elif "rss" in subs[0][1] or "atom" in subs[0][1]: stri = _("This page is already a feed:\n") else: stri = _("No feed detected. You can still watch the page :\n") counter = 0 for l in subs: link = l[0] already = [] for li in self.list_lists(): if self.list_is_subscribed(li): if self.list_has_url(link, li): already.append(li) stri += "[%s] %s [%s]\n" % (counter + 1, link, l[1]) if len(already) > 0: stri += _("\t -> (already subscribed through lists %s)\n") % (str(already)) counter += 1 stri += "\n" stri += _("Which feed do you want to subscribe ? > ") ans = input(stri) if ans.isdigit() and 0 < int(ans) <= len(subs): sublink = subs[int(ans) - 1][0] else: sublink = None if sublink: added = self.list_add_line("subscribed", url=sublink, verbose=False) if added: print(_("Subscribed to %s") % sublink) else: print(_("You are already subscribed to %s") % sublink) else: print(_("No subscription registered")) def do_bookmarks(self, line): """Show or access the bookmarks menu. 'bookmarks' shows all bookmarks. 'bookmarks n' navigates immediately to item n in the bookmark menu. Bookmarks are stored using the 'add' command.""" args = line.strip() if len(args.split()) > 1 or (args and not args.isnumeric()): print(_("bookmarks command takes a single integer argument!")) elif args: self.list_go_to_line(args, "bookmarks") else: self.list_show("bookmarks") @needs_gi def do_archive(self, args): """Archive current page by removing it from every list and adding it to archives, which is a special historical list limited in size. It is similar to `move archives`.""" url = unmode_url(self.current_url)[0] for li in self.list_lists(): if li not in ["archives", "history"]: deleted = self.list_rm_url(url, li) if deleted: print(_("Removed from %s") % li) self.list_add_top("archives", limit=self.options["archives_size"]) r = self.get_renderer() title = r.get_page_title() print(_("Archiving: %s") % title) print( _("\x1b[2;34mCurrent maximum size of archives : %s\x1b[0m") % self.options["archives_size"] ) # what is the line to add to a list for this url ? def to_map_line(self, url=None): if not url: url = self.current_url r = self.get_renderer(url) if r: title = r.get_page_title() else: title = "" toreturn = "=> {} {}\n".format(url, title) return toreturn def list_add_line(self, list, url=None, verbose=True): list_path = self.list_path(list) if not list_path and self.list_is_system(list): self.list_create(list, quite=True) list_path = self.list_path(list) if not list_path: print(REDERROR+ _("List %s does not exist. Create it with " "list create %s" "") % (list, list) ) return False else: if not url: url = self.current_url unmoded_url, mode = unmode_url(url) # first we check if url already exists in the file if self.list_has_url(url, list, exact_mode=True): if verbose: print(_("%s already in %s.") % (url, list)) return False # If the URL already exists but without a mode, we update the mode # FIXME: this doesn’t take into account the case where you want to remove the mode elif url != unmoded_url and self.list_has_url(unmoded_url, list): self.list_update_url_mode(unmoded_url, list, mode) if verbose: print(_("%s has updated mode in %s to %s") % (url, list, mode)) else: with open(list_path, "a") as l_file: l_file.write(self.to_map_line(url)) l_file.close() if verbose: #TRANSLATORS parameters are url, list print(_("%s added to %s") % (url, list)) return True @needs_gi def list_add_top(self, list, limit=0, truncate_lines=0): stri = self.to_map_line().strip("\n") if list == "archives": stri += _(", archived on ") elif list == "history": stri += _(", visited on ") else: #TRANSLATORS parameter is a "list" name stri += _(", added to %s on ") % list stri += time.ctime() + "\n" list_path = self.get_list(list) # We read the whole list with open(list_path, "r") as l_file: lines = l_file.readlines() l_file.close() # Now, we write it back, with open(list_path, "w") as l_file: l_file.write("#%s\n" % list) l_file.write(stri) counter = 0 previous_line = stri # Truncating is useful in case we open a new branch # after a few back in history to_truncate = truncate_lines for l in lines: # Removing duplicate lines of the same URL # when there are in a row if not l.startswith("#") and len(l.split(" ")) >= 2: previousurl = unmode_url(previous_line.split(" ")[1])[0] currenturl = unmode_url(l.split(" ")[1])[0] similar = previousurl == currenturl if not similar : if to_truncate > 0: to_truncate -= 1 elif limit == 0 or counter < limit: previous_line = l l_file.write(l) counter += 1 l_file.close() # remove an url from a list. # return True if the URL was removed # return False if the URL was not found def list_rm_url(self, url, list): return self.list_has_url(url, list, deletion=True) def list_update_url_mode(self, url, list, mode): return self.list_has_url(url, list, update_mode=mode) # deletion and has_url are so similar, I made them the same method # deletion : true or false if you want to delete the URL # exact_mode : True if you want to check only for the exact url, not the canonical one # update_mode : a new mode to update the URL def list_has_url( self, url, list, deletion=False, exact_mode=False, update_mode=None ): list_path = self.list_path(list) if list_path: to_return = False with open(list_path, "r") as lf: lines = lf.readlines() lf.close() to_write = [] # let’s remove the mode if not exact_mode: url = unmode_url(url)[0] for l in lines: # we separate components of the line # to ensure we identify a complete URL, not a part of it splitted = l.split() if url not in splitted and len(splitted) > 1: current = unmode_url(splitted[1])[0] # sometimes, we must remove the ending "/" if url == current or (url.endswith("/") and url[:-1] == current): to_return = True if update_mode: new_line = l.replace(current, mode_url(url, update_mode)) to_write.append(new_line) elif not deletion: to_write.append(l) else: to_write.append(l) elif url in splitted: to_return = True # We update the mode if asked by replacing the old url # by a moded one in the same line if update_mode: new_line = l.replace(url, mode_url(url, update_mode)) to_write.append(new_line) elif not deletion: to_write.append(l) else: to_write.append(l) if deletion or update_mode: with open(list_path, "w") as lf: for l in to_write: lf.write(l) lf.close() return to_return else: return False def list_get_links(self, list): list_path = self.list_path(list) if list_path and os.path.exists(list_path): return self.get_renderer("list:///%s" % list).get_links() else: return [] def list_go_to_line(self, line, list): list_path = self.list_path(list) if not list_path: print( _("List %s does not exist. Create it with " "list create %s" "") % (list, list) ) elif not line.isnumeric(): #TRANSLATORS keep 'go_to_line' as is print(_("go_to_line requires a number as parameter")) else: r = self.get_renderer("list:///%s" % list) url = r.get_link(int(line)) display = not self.sync_only if url: self._go_to_url(url, handle=display) return url def list_show(self, list): list_path = self.list_path(list) if not list_path: print(REDERROR+ _("List %s does not exist. Create it with " "list create %s" "") % (list, list) ) else: url = "list:///%s" % list display = not self.sync_only self._go_to_url(url, handle=display) # return the path of the list file if list exists. # return None if the list doesn’t exist. def list_path(self, list): listdir = os.path.join(xdg("data"), "lists") list_path = os.path.join(listdir, "%s.gmi" % list) if os.path.exists(list_path): return list_path else: return None def list_create(self, list, title=None, quite=False): list_path = self.list_path(list) if list in ["create", "edit", "delete", "help"]: print(_("%s is not allowed as a name for a list") % list) elif not list_path: listdir = os.path.join(xdg("data"), "lists") os.makedirs(listdir, exist_ok=True) list_path = os.path.join(listdir, "%s.gmi" % list) with open(list_path, "a") as lfile: if title: lfile.write("# %s\n" % title) else: lfile.write("# %s\n" % list) lfile.close() if not quite: print(_("list created. Display with `list %s`") % list) else: print(_("list %s already exists") % list) def do_move(self, arg): """move LIST will add the current page to the list LIST. With a major twist: current page will be removed from all other lists. If current page was not in a list, this command is similar to `add LIST`.""" if not arg: print(_("LIST argument is required as the target for your move")) elif arg[0] == "archives": self.do_archive() else: args = arg.split() list_path = self.list_path(args[0]) if not list_path: print(_("%s is not a list, aborting the move") % args[0]) else: lists = self.list_lists() for l in lists: if l != args[0] and l not in ["archives", "history"]: url = unmode_url(self.current_url)[0] isremoved = self.list_rm_url(url, l) if isremoved: print(_("Removed from %s") % l) self.list_add_line(args[0]) def list_lists(self): listdir = os.path.join(xdg("data"), "lists") to_return = [] if os.path.exists(listdir): lists = os.listdir(listdir) if len(lists) > 0: for l in lists: # Taking only files with .gmi if l.endswith(".gmi"): # removing the .gmi at the end of the name to_return.append(l[:-4]) return to_return def list_has_status(self, list, status): path = self.list_path(list) toreturn = False if path: with open(path) as f: line = f.readline().strip() f.close() if line.startswith("#") and status in line: toreturn = True return toreturn def list_is_subscribed(self, list): return self.list_has_status(list, "#subscribed") def list_is_frozen(self, list): return self.list_has_status(list, "#frozen") def list_is_system(self, list): return list in ["history", "to_fetch", "archives", "tour"] # This modify the status of a list to one of : # normal, frozen, subscribed # action is either #frozen, #subscribed or None def list_modify(self, list, action=None): path = self.list_path(list) with open(path) as f: lines = f.readlines() f.close() if lines[0].strip().startswith("#"): first_line = lines.pop(0).strip("\n") else: first_line = "# %s " % list first_line = first_line.replace("#subscribed", "").replace("#frozen", "") if action: first_line += " " + action print(_("List %s has been marked as %s") % (list, action)) else: print(_("List %s is now a normal list") % list) first_line += "\n" lines.insert(0, first_line) with open(path, "w") as f: for line in lines: f.write(line) f.close() def do_list(self, arg): """Manage list of bookmarked pages. - list : display available lists - list $LIST : display pages in $LIST - list create $NEWLIST : create a new list - list edit $LIST : edit the list - list subscribe $LIST : during sync, add new links found in listed pages to tour - list freeze $LIST : don’t update pages in list during sync if a cache already exists - list normal $LIST : update pages in list during sync but don’t add anything to tour - list delete $LIST : delete a list permanently (a confirmation is required) - list help : print this help See also : - add $LIST (to add current page to $LIST or, by default, to bookmarks) - move $LIST (to add current page to list while removing from all others) - archive (to remove current page from all lists while adding to archives) There’s no "delete" on purpose. The use of "archive" is recommended. The following lists cannot be removed or frozen but can be edited with "list edit" - list archives : contains last 200 archived URLs - history : contains last 200 visisted URLs - to_fetch : contains URLs that will be fetch during the next sync - tour : contains the next URLs to visit during a tour (see "help tour")""" listdir = os.path.join(xdg("data"), "lists") os.makedirs(listdir, exist_ok=True) if not arg: lists = self.list_lists() if len(lists) > 0: lurl = "list:///" self._go_to_url(lurl) else: print(_("No lists yet. Use `list create`")) else: args = arg.split() if args[0] == "create": if len(args) > 2: name = " ".join(args[2:]) self.list_create(args[1].lower(), title=name) elif len(args) == 2: self.list_create(args[1].lower()) else: print( _("A name is required to create a new list. Use `list create NAME`") ) elif args[0] == "edit": editor = None if "editor" in self.options and self.options["editor"]: editor = self.options["editor"] elif os.environ.get("VISUAL"): editor = os.environ.get("VISUAL") elif os.environ.get("EDITOR"): editor = os.environ.get("EDITOR") if editor: if len(args) > 1 and args[1] in self.list_lists(): path = os.path.join(listdir, args[1] + ".gmi") try: # Note that we intentionally don't quote the editor. # In the unlikely case `editor` includes a percent # sign, we also escape it for the %-formatting. cmd = editor.replace("%", "%%") + " %s" run(cmd, parameter=path, direct_output=True) except Exception as err: print(err) print(_('Please set a valid editor with "set editor"')) else: print(_("A valid list name is required to edit a list")) else: print(_("No valid editor has been found.")) print( _("You can use the following command to set your favourite editor:") ) #TRANSLATORS keep 'set editor', it's a command print(_("set editor EDITOR")) print(_("or use the $VISUAL or $EDITOR environment variables.")) elif args[0] == "delete": if len(args) > 1: if self.list_is_system(args[1]): print(_("%s is a system list which cannot be deleted") % args[1]) elif args[1] in self.list_lists(): size = len(self.list_get_links(args[1])) stri = _("Are you sure you want to delete %s ?\n") % args[1] confirm = "YES" if size > 0: stri += _("! %s items in the list will be lost !\n") % size confirm = "YES DELETE %s" % size else: stri += ( _("The list is empty, it should be safe to delete it.\n") ) stri += ( _('Type "%s" (in capital, without quotes) to confirm :') % confirm ) answer = input(stri) if answer == confirm: path = os.path.join(listdir, args[1] + ".gmi") os.remove(path) print(_("* * * %s has been deleted") % args[1]) else: print(_("A valid list name is required to be deleted")) else: print(_("A valid list name is required to be deleted")) elif args[0] in ["subscribe", "freeze", "normal"]: if len(args) > 1: if self.list_is_system(args[1]): print(_("You cannot modify %s which is a system list") % args[1]) elif args[1] in self.list_lists(): if args[0] == "subscribe": action = "#subscribed" elif args[0] == "freeze": action = "#frozen" else: action = None self.list_modify(args[1], action=action) else: print(_("A valid list name is required after %s") % args[0]) elif args[0] == "help": self.onecmd("help list") elif len(args) == 1: self.list_show(args[0].lower()) else: self.list_go_to_line(args[1], args[0].lower()) def do_help(self, arg): """ALARM! Recursion detected! ALARM! Prepare to eject!""" if arg == "help": print(_("Need help from a fellow human? Simply send an email to the offpunk-users list.")) dest = "~lioploum/offpunk-users@lists.sr.ht" subject = "Getting started with Offpunk" body = _("Describe your problem/question as clearly as possible.") +"\n" + \ _("Don’t forget to present yourself and why you would like to use Offpunk!") + \ "\n\n" + \ _("Another point: always use \"reply-all\" when replying to this list.") send_email(dest,subject=subject,body=body,toconfirm=True) elif arg == "!": print(_("! is an alias for 'shell'")) elif arg == "?": print(_("? is an alias for 'help'")) elif arg in _ABBREVS: full_cmd = _ABBREVS[arg] print(_("%s is an alias for '%s'") % (arg, full_cmd)) print(_("See the list of aliases with 'abbrevs'")) print(_("'help %s':") % full_cmd) self.do_help(full_cmd) else: try: print(_(getattr(self, 'do_' + arg).__doc__)) except AttributeError: cmd.Cmd.do_help(self, arg) def do_tutorial(self, arg): """Access the offpunk.net tutorial (online)""" self._go_to_url("gemini://offpunk.net/firststeps.gmi") def do_sync(self, line): """Synchronize all bookmarks lists and URLs from the to_fetch list. - New elements in pages in subscribed lists will be added to tour - Elements in list to_fetch will be retrieved and added to tour - Normal lists will be synchronized and updated - Frozen lists will be fetched only if not present. Before a sync, you can edit the list of URLs that will be fetched with the following command: "list edit to_fetch" Argument : duration of cache validity (in seconds).""" if self.offline_only: print(_("Sync can only be achieved online. Change status with `online`.")) return args = line.split() if len(args) > 0: if not args[0].isdigit(): print(_("sync argument should be the cache validity expressed in seconds")) return else: validity = int(args[0]) else: validity = 0 self.call_sync(refresh_time=validity) def call_sync(self, refresh_time=0, depth=1, lists=None): # fetch_url is the core of the sync algorithm. # It takes as input : # - an URL to be fetched # - depth : the degree of recursion to build the cache (0 means no recursion) # - validity : the age, in seconds, existing caches need to have before # being refreshed (0 = never refreshed if it already exists) # - savetotour : if True, newly cached items are added to tour def add_to_tour(url): if url and netcache.is_cache_valid(url): toprint = _(" -> adding to tour: %s") % url width = term_width() - 1 toprint = toprint[:width] toprint += " " * (width - len(toprint)) print(toprint) self.list_add_line("tour", url=url, verbose=False) return True else: return False def fetch_url( url, depth=0, validity=0, savetotour=False, count=[0, 0], strin="", force_large_download=False ): # savetotour = True will save to tour newly cached content # else, do not save to tour # regardless of valitidy if not url: return if not netcache.is_cache_valid(url, validity=validity): if strin != "": endline = "\r" else: endline = None # Did we already had a cache (even an old one) ? isnew = not netcache.is_cache_valid(url) toprint = _("%s [%s/%s] Fetch ") % (strin, count[0], count[1]) + url width = term_width() - 1 toprint = toprint[:width] toprint += " " * (width - len(toprint)) print(toprint, end=endline) # If not saving to tour, then we should limit download size limit = not savetotour self._go_to_url(url, update_hist=False, limit_size=limit,\ force_large_download=force_large_download) if savetotour and isnew and netcache.is_cache_valid(url): # we add to the next tour only if we managed to cache # the ressource add_to_tour(url) # Now, recursive call, even if we didn’t refresh the cache # This recursive call is impacting performances a lot but is needed # For the case when you add a address to a list to read later # You then expect the links to be loaded during next refresh, even # if the link itself is fresh enough # see fetch_list() if depth > 0: # we should only savetotour at the first level of recursion # The code for this was removed so, currently, we savetotour # at every level of recursion. r = self.get_renderer(url) url, oldmode = unmode_url(url) if oldmode == "full": mode = "full_links_only" else: mode = "links_only" if r: links = r.get_links(mode=mode) subcount = [0, len(links)] d = depth - 1 for k in links: # recursive call (validity is always 0 in recursion) substri = strin + " -->" subcount[0] += 1 fetch_url( k, depth=d, validity=0, savetotour=savetotour, count=subcount, strin=substri, ) def fetch_list( list, validity=0, depth=1, tourandremove=False, tourchildren=False, force_large_download=False ): links = self.list_get_links(list) end = len(links) counter = 0 print(_(" * * * %s to fetch in %s * * *") % (end, list)) for l in links: counter += 1 # If cache for a link is newer than the list fetch_url( l, depth=depth, validity=validity, savetotour=tourchildren, count=[counter, end], force_large_download=force_large_download ) if tourandremove: if add_to_tour(l): self.list_rm_url(l, list) self.sync_only = True if not lists: lists = self.list_lists() # We will fetch all the lists except "archives" and "history" # We keep tour for the last round subscriptions = [] normal_lists = [] fridge = [] for l in lists: # only try existing lists if l in self.list_lists(): if not self.list_is_system(l): if self.list_is_frozen(l): fridge.append(l) elif self.list_is_subscribed(l): subscriptions.append(l) else: normal_lists.append(l) # We start with the "subscribed" as we need to find new items starttime = int(time.time()) for l in subscriptions: fetch_list(l, validity=refresh_time, depth=depth, tourchildren=True) # Then the to_fetch list (item are removed from the list after fetch) # We fetch regarless of the refresh_time if "to_fetch" in lists: nowtime = int(time.time()) short_valid = nowtime - starttime fetch_list( "to_fetch", validity=short_valid, depth=depth, tourandremove=True, force_large_download=True ) # then we fetch all the rest (including bookmarks and tour) for l in normal_lists: fetch_list(l, validity=refresh_time, depth=depth) for l in fridge: fetch_list(l, validity=0, depth=depth) # tour should be the last one as item my be added to it by others fetch_list("tour", validity=refresh_time, depth=depth) print(_("End of sync")) self.sync_only = False # The end! def do_quit(self, *args): """Exit Offpunk.""" self.opencache.cleanup() print(_("You can close your screen!")) sys.exit() do_exit = do_quit # Main function def main(): # Parse args parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--bookmarks", action="store_true", help=_("start with your list of bookmarks") ) parser.add_argument( "--command", metavar="COMMAND", nargs="*", help=_("Launch this command after startup"), ) parser.add_argument( "--config-file", metavar="FILE", help=_("use this particular config file instead of default"), ) parser.add_argument( "--sync", action="store_true", help=_("run non-interactively to build cache by exploring lists passed \ as argument. Without argument, all lists are fetched."), ) parser.add_argument( "--assume-yes", action="store_true", help=_("assume-yes when asked questions about certificates/redirections during sync (lower security)"), ) parser.add_argument( "--disable-http", action="store_true", help=_("do not try to get http(s) links (but already cached will be displayed)"), ) parser.add_argument( "--fetch-later", action="store_true", help=_("run non-interactively with an URL as argument to fetch it later"), ) parser.add_argument( "--depth", help=_("depth of the cache to build. Default is 1. More is crazy. Use at your own risks!"), ) parser.add_argument( "--images-mode", help=_("the mode to use to choose which images to download in a HTML page.\ one of (None, readable, full). Warning: full will slowdown your sync."), ) parser.add_argument( "--cache-validity", help=_("duration for which a cache is valid before sync (seconds)"), ) parser.add_argument( "--version", action="store_true", help=_("display version information and quit") ) parser.add_argument( "--features", action="store_true", help=_("display available features and dependancies then quit"), ) parser.add_argument( "url", metavar="URL", nargs="*", help=_("Arguments should be URL to be fetched or, if --sync is used, lists"), ) args = parser.parse_args() # Handle --version if args.version: print("Offpunk " + __version__) sys.exit() elif args.features: GeminiClient.do_version(None, None) sys.exit() else: for f in [xdg("config"), xdg("data")]: if not os.path.exists(f): print(_("Creating config directory {}").format(f)) os.makedirs(f) # Instantiate client gc = GeminiClient(sync_only=args.sync) torun_queue = [] # Act on args if args.bookmarks: torun_queue.append("bookmarks") elif args.url and not args.sync: if len(args.url) == 1: torun_queue.append("go %s" % args.url[0]) else: for url in args.url: torun_queue.append("tour %s" % url) torun_queue.append("tour") if args.disable_http: gc.support_http = False # Endless interpret loop (except while --sync or --fetch-later) if args.fetch_later: if args.url: gc.sync_only = True for u in args.url: if looks_like_url(u): if netcache.is_cache_valid(u): gc.list_add_line("tour", u) else: gc.list_add_line("to_fetch", u) else: print(_("%s is not a valid URL to fetch") % u) else: print(_("--fetch-later requires an URL (or a list of URLS) as argument")) elif args.sync: if args.assume_yes: gc.onecmd("set accept_bad_ssl_certificates True") if args.cache_validity: refresh_time = int(args.cache_validity) else: # if no refresh time, a default of 0 is used (which means "infinite") refresh_time = 0 if args.images_mode and args.images_mode in [ "none", "readable", "normal", "full", ]: gc.options["images_mode"] = args.images_mode if args.depth: depth = int(args.depth) else: depth = 1 torun_queue += init_config(rcfile=args.config_file, interactive=False) for line in torun_queue: # This doesn’t seem to run on sync. Why? gc.onecmd(line) gc.call_sync(refresh_time=refresh_time, depth=depth, lists=args.url) else: # We are in the normal mode. First process config file torun_queue += init_config(rcfile=args.config_file,interactive=True) print(_("Welcome to Offpunk!")) #TRANSLATORS keep 'help', it's a literal command print(_("Type `help` to get the list of available command.")) for line in torun_queue: gc.onecmd(line) if args.command: for cmd in args.command: gc.onecmd(cmd) while True: try: gc.cmdloop() except KeyboardInterrupt: print("") if __name__ == "__main__": main() offpunk-v3.0/offthemes.py000066400000000000000000000112501514232770500155510ustar00rootroot00000000000000#!/bin/python colors = { "bold" : ["1","22"], "faint" : ["2","22"], "italic" : ["3","23"], "underline": ["4","24"], "black" : ["30","39"], "red" : ["31","39"], "green" : ["32","39"], "yellow" : ["33","39"], "blue" : ["34","39"], "purple" : ["35","39"], "cyan" : ["36","39"], "white" : ["37","39"], "background_black" : ["40","49"], "background_red" : ["41","49"], "background_green" : ["42","49"], "background_yellow" : ["43","49"], "background_blue" : ["44","49"], "background_purple" : ["45","49"], "background_cyan" : ["46","49"], "background_white" : ["47","49"], "bright_black" : ["90","39"], "bright_red" : ["91","39"], "bright_green" : ["92","39"], "bright_yellow" : ["93","39"], "bright_blue" : ["94","39"], "bright_purple" : ["95","39"], "bright_cyan" : ["96","39"], "bright_white" : ["97","39"], } offpunk1 = { "window_title" : ["red","bold"], "window_subtitle" : ["red","faint"], "title" : ["blue","bold","underline"], "subtitle" : ["blue"], "subsubtitle" : ["blue","faint"], #fallback to subtitle if none "link" : ["blue","faint"], "new_link": ["bold"], "blocked_link": ["red","faint"], "oneline_link": [], #for gopher/gemini. fallback to link if none "image_link" : ["yellow","faint"], "preformatted": ["faint"], "blockquote" : ["italic"], "prompt_on" : ["green"], "prompt_off" : ["green"], } yellow = { "window_title" : ["red","bold"], "window_subtitle" : ["red","faint"], "title" : ["yellow","bold","underline"], "subtitle" : ["yellow","bold"], "subsubtitle" : ["yellow","faint","underline"], #fallback to subtitle if none "link" : ["yellow","faint"], "new_link": ["bold"], "blocked_link": ["red","faint"], "oneline_link": ["white"], #for gopher/gemini. fallback to link if none "image_link" : ["yellow","italic","faint"], "preformatted": ["faint"], "blockquote" : ["italic"], "prompt_on" : ["green","bold"], "prompt_off" : ["red","bold"], } cyan = { "window_title" : ["blue","bold"], "window_subtitle" : ["blue","faint"], "title" : ["cyan","bold","underline"], "subtitle" : ["cyan","bold"], "subsubtitle" : ["cyan","faint","underline"], #fallback to subtitle if none "link" : ["cyan",], "new_link": ["bold"], "blocked_link": ["red","faint"], "oneline_link": ["white"], #for gopher/gemini. fallback to link if none "image_link" : ["blue","italic","faint"], "preformatted": ["faint"], "blockquote" : ["italic"], "prompt_on" : ["green","bold"], "prompt_off" : ["blue","bold"], } bw = { "window_title" : ["background_white", "black","bold"], "window_subtitle" : ["background_white", "black","faint"], "title" : ["bold","underline"], "subtitle" : ["bold"], "subsubtitle" : ["faint","underline"], #fallback to subtitle if none "link" : ["bold",], "new_link": ["bold"], "blocked_link": ["faint"], "oneline_link": [], #for gopher/gemini. fallback to link if none "image_link" : ["italic","faint"], "preformatted": ["faint"], "blockquote" : ["italic"], "prompt_on" : ["bold"], "prompt_off" : ["faint"], } themes = {"offpunk1":offpunk1,"yellow":yellow, "cyan":cyan, "bw": bw} default = offpunk1 offpunk-v3.0/offutils.py000066400000000000000000000416351514232770500154360ustar00rootroot00000000000000#!/bin/python # This file contains some utilities common to offpunk, ansicat and netcache. # Currently, there are the following utilities: # # run : run a shell command and get the results with some security # term_width : get or set the width to display on the terminal import io import os import shlex import shutil import subprocess import urllib.parse import gettext import sys import cert_migration import netcache import netcache_migration # We can later add some logic to decide this based on OS family/version if needed? # With "None", the defaults should make this work in debian and RedHat based systems at least # "None" would default to sys.base_prefix + "/share/locale/" # (i.e., "/usr/share/locale") # sys.base_prefix is always "/usr" # sys.prefix however, is either "/usr" or the path to the virtualenv we're in # this next line makes i18n work if offpunk is installed with pipx for example: _LOCALE_DIR = sys.prefix + "/share/locale/" gettext.bindtextdomain('offpunk', _LOCALE_DIR) gettext.textdomain('offpunk') _ = gettext.gettext CACHE_VERSION = 1 CERT_VERSION = 1 # let’s find if grep supports --color=auto try: test = subprocess.run( ["grep", "--color=auto", "x"], input=b"x", check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) GREPCMD = "grep --color=auto" except Exception: GREPCMD = "grep" _HAS_XDGOPEN = shutil.which("xdg-open") # We upgrade the cache only once at startup, hence the CACHE_UPGRADED variable # This is only to avoid unnecessary checks each time the cache is accessed CACHE_UPGRADED = False def upgrade_cache(cache_folder): # Let’s read current version of the cache version_path = cache_folder + ".version" current_version = 0 if os.path.exists(version_path): current_str = None with open(version_path) as f: current_str = f.read() f.close() try: current_version = int(current_str) except Exception: current_version = 0 # Now, let’s upgrade the cache if needed while current_version < CACHE_VERSION: current_version += 1 upgrade_func = getattr(netcache_migration, "upgrade_to_" + str(current_version)) upgrade_func(cache_folder) with open(version_path, "w") as f: f.write(str(current_version)) f.close() CACHE_UPGRADED = True CERT_UPGRADED = False def upgrade_cert(config_folder: str, data_folder: str) -> None: # read the current version certdata = os.path.join(data_folder, "certs") if not os.path.exists(certdata): os.makedirs(certdata, exist_ok=True) version_path = os.path.join(certdata, ".version") current_version = 0 if os.path.exists(version_path): current_str = None with open(version_path) as f: current_str = f.read() f.close() try: current_version = int(current_str) except Exception: current_version = 0 else: current_version = 0 # Now, let’s upgrade the certificate storage if needed while current_version < CERT_VERSION: current_version += 1 upgrade_func = getattr(cert_migration, "upgrade_to_" + str(current_version)) upgrade_func(data_folder, config_folder) with open(version_path, "w") as f: f.write(str(current_version)) f.close() CERT_UPGRADED = True # get xdg folder. Folder should be "cache", "data" or "config" def xdg(folder="cache"): # Config directories # We implement our own python-xdg to avoid conflict with existing libraries. _home = os.path.expanduser("~") data_home = os.environ.get("XDG_DATA_HOME") or os.path.join( _home, ".local", "share" ) config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(_home, ".config") _CONFIG_DIR = os.path.join(os.path.expanduser(config_home), "offpunk/") _DATA_DIR = os.path.join(os.path.expanduser(data_home), "offpunk/") _old_config = os.path.expanduser("~/.offpunk/") # Look for pre-existing config directory, if any if os.path.exists(_old_config): _CONFIG_DIR = _old_config # if no XDG .local/share and not XDG .config, we use the old config if not os.path.exists(data_home) and os.path.exists(_old_config): _DATA_DIR = _CONFIG_DIR # get _CACHE_PATH from OFFPUNK_CACHE_PATH environment variable # if OFFPUNK_CACHE_PATH empty, set default to ~/.cache/offpunk cache_home = os.environ.get("XDG_CACHE_HOME") or os.path.join(_home, ".cache") _CACHE_PATH = os.environ.get( "OFFPUNK_CACHE_PATH", os.path.join(os.path.expanduser(cache_home), "offpunk/") ) # Check that the cache path ends with "/" if not _CACHE_PATH.endswith("/"): _CACHE_PATH += "/" os.makedirs(_CACHE_PATH, exist_ok=True) if folder == "cache" and not CACHE_UPGRADED: upgrade_cache(_CACHE_PATH) if folder == "cache": return _CACHE_PATH elif folder == "config": return _CONFIG_DIR elif folder == "data": if not CERT_UPGRADED: upgrade_cert(_CONFIG_DIR, _DATA_DIR) return _DATA_DIR else: print(_("No XDG folder for %s. Check your code.") % folder) return None #Return a list of the commands that must be run #if skip_go = True, any command changing the url will be ignored (go, tour) #if not interactive, only redirects and handlers are considered def init_config(rcfile=None,skip_go=False,interactive=True,verbose=True): cmds = [] if not rcfile: rcfile = os.path.join(xdg("config"), "offpunkrc") if os.path.exists(rcfile): if verbose: print(_("Using config %s") % rcfile) with open(rcfile,"r") as fp: for line in fp: line = line.strip() #Is this a command to go to an url ? is_go = any(line.startswith(x) for x in ("go","g","tour","t")) #Is this a command necessary, even when non-interactive ? is_necessary = any(line.startswith(x) for x in ("redirect","handler","set")) if is_necessary: cmds.append(line) elif interactive: if skip_go and is_go: if verbose: print(_("Skipping startup command \"%s\" due to provided URL")%line) continue else: cmds.append(line) return cmds # An IPV6 URL should be put between [] # We try to detect them has location with more than 2 ":" def fix_ipv6_url(url): if not url or url.startswith("mailto"): return url if "://" in url: schema, schemaless = url.split("://", maxsplit=1) else: schema, schemaless = None, url if "/" in schemaless: netloc, rest = schemaless.split("/", 1) if netloc.count(":") > 2 and "[" not in netloc and "]" not in netloc: schemaless = "[" + netloc + "]" + "/" + rest elif schemaless.count(":") > 2 and "[" not in schemaless and "]" not in schemaless: schemaless = "[" + schemaless + "]/" if schema: return schema + "://" + schemaless return schemaless # Cheap and cheerful URL detector def looks_like_url(word): try: if not word.strip(): return False url = fix_ipv6_url(word).strip() parsed = urllib.parse.urlparse(url) # sometimes, urllib crashed only when requesting the port port = parsed.port scheme = word.split("://")[0] mailto = word.startswith("mailto:") start = scheme in netcache.standard_ports local = scheme in ["file", "list"] if mailto: return "@" in word elif not local: if start: # IPv4 if "." in word or "localhost" in word: return True # IPv6 elif "[" in word and ":" in word and "]" in word: return True else: return False else: return False return start and ("." in word or "localhost" in word or ":" in word) else: return "/" in word except ValueError: return False # Those two functions add/remove the mode to the # URLs. This is a gross hack to remember the mode def mode_url(url, mode): if mode and mode != "readable" and "##offpunk=" not in url: url += "##offpunk_mode=" + mode return url def unmode_url(url): if url: mode = None splitted = url.split("##offpunk_mode=") if len(splitted) > 1: url = splitted[0] mode = splitted[1] return [url, mode] else: return [None,None] #This function gives the root of an URL # expect if the url contains /user/ or ~username/ #in that case, it considers it as a muli-user servers # it returns the root URL # except if "return_value=name" then it return a name for that root # which is hostname by default or username if applicable # if absolute is set, it doesn’t care about users # if return_value="list", then a list of all the steps until the root is returned, # Starting from URL at position 0 to root at position -1 def find_root(url,absolute=False,return_value=""): parsed = urllib.parse.urlparse(url) #by default, root is the true root name = parsed.netloc path = "/" subpath = parsed.path.split("/") dismissed = "" if parsed.scheme == 'gopher' and len(subpath) >= 2: # remove the type, add "1" (root is always gonna be "folder") later subpath.remove(subpath[1]) # now "subpath" has the same number of elements as gemini and http if not absolute: #As subpath starts with "/", subpathsplit("/")[0] is always "" # handling http://server/users/janedoe/ case if len(subpath) > 2 and subpath[1] in ["user","users"]: dismissed = "/" + subpath[1] + "/" name = subpath[2] path = path.join(subpath[:3]) + "/" subpath = subpath[2:] # we will thus dism # handling http://server/~janedoe/ case elif len(subpath) > 1 and subpath[1].startswith("~"): dismissed = "/" name = subpath[1].lstrip("~") path = path.join(subpath[:2]) + "/" subpath = subpath[1:] if return_value == "name": return name elif return_value == "list": # we gradually reduce subpath to build the toreturn list # we put url in the place 0: "up 0" is keeping same url toreturn = [url] # we loop while: # there’s something in the subpath elements # we didn’t catch the root path newpath = dismissed + "/".join(subpath) if parsed.scheme == 'gopher': newpath = "/1" + newpath while len(subpath) > 0 and len(newpath) > len(path): subpath.pop(-1) newpath = dismissed + "/".join(subpath) if parsed.scheme == 'gopher': newpath = "/1" + newpath if not newpath.endswith("/"): newpath += "/" newurl = urllib.parse.urlunparse((parsed.scheme, \ parsed.netloc, newpath, "","","")) if newurl not in toreturn: toreturn.append(newurl) return toreturn else: if parsed.scheme == 'gopher': # root is always going to be directory path = '/1'+path root = urllib.parse.urlunparse((parsed.scheme, parsed.netloc, path, "","","")) return root # In terms of arguments, this can take an input file/string to be passed to # stdin, a parameter to do (well-escaped) "%" replacement on the command, a # flag requesting that the output go directly to the stdout, and a list of # additional environment variables to set. def run(cmd, *, input=None, parameter=None, direct_output=False, env={}): if parameter: cmd = cmd % shlex.quote(parameter) e = os.environ e.update(env) if isinstance(input, io.IOBase): stdin = input input = None else: if input: input = input.encode() stdin = None if not direct_output: # subprocess.check_output() wouldn't allow us to pass stdin. result = subprocess.run( cmd, check=True, env=e, input=input, shell=True, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) return result.stdout.decode() else: subprocess.run(cmd, env=e, input=input, shell=True, stdin=stdin) global TERM_WIDTH TERM_WIDTH = 72 # if absolute, returns the real terminal width, not the text width def term_width(new_width=None, absolute=False): if new_width: global TERM_WIDTH TERM_WIDTH = new_width cur = shutil.get_terminal_size()[0] if absolute: return cur width = TERM_WIDTH if cur < width: width = cur return width def is_local(url): if not url: return True elif "://" in url: scheme, path = url.split("://", maxsplit=1) return scheme in ["file", "mail", "list", "mailto"] else: return True # open XDG mail client to compose an email to dest. # If toconfirm=True, the user is asked to confirm that he want to send an email # If allowemptydest, then the mail client will be used to choose the destination def send_email(dest,subject=None,body=None,toconfirm=True,allowemptydest=True): if not allowemptydest and "@" not in dest: print(_("%s is not a valid email address")%dest) return if toconfirm: #TRANSLATORS please keep the 'Y/N' as is resp = input(_("Send an email to %s Y/N? ") % dest) confirmed = resp.strip().lower() in ("y", "yes") else: confirmed = True if confirmed: if _HAS_XDGOPEN: param = dest if subject or body: param += "?" if subject: param += "subject=%s"%urllib.parse.quote(subject) if body: param += "&" if body: param += "body=%s"%urllib.parse.quote(body) run("xdg-open mailto:%s", parameter=param, direct_output=True) else: print(_("Cannot find a mail client to send mail to %s") % inpath) print(_("Please install xdg-open (usually from xdg-util package)")) # Take an URL in UTF-8 and replace all the characters by the proper % chars # like " " becomes "%20" def urlify(url): parsed = urllib.parse.urlparse(url) #do not urlify local, mailto and gopher links if parsed.scheme in ["", "mailto", "gopher"]: return url else: #we need to unquote it first, in case it’s already quoted newpath = urllib.parse.unquote(parsed.path) #we only quote the path part newpath = urllib.parse.quote(newpath) newparsed = parsed._replace(path=newpath) return urllib.parse.urlunparse(newparsed) # This method return the image URL or invent it if it’s a base64 inline image # It returns [url,image_data] where image_data is None for normal image def looks_like_base64(src, baseurl): imgdata = None imgname = src if src and src.startswith("data:image/"): if ";base64," in src: splitted = src.split(";base64,") # splitted[0] is something like data:image/jpg if "/" in splitted[0]: extension = splitted[0].split("/")[1] else: extension = "data" imgdata = splitted[1] imgname = imgdata[:20] + "." + extension imgurl = urllib.parse.urljoin(baseurl, imgname) else: # We can’t handle other data:image such as svg for now imgurl = None else: imgurl = urllib.parse.urljoin(baseurl, imgname) imgurl = urlify(imgurl) return imgurl, imgdata #if returnkey=True, we return [redirection, matching pattern] def get_url_redirected(url,redirectlist,returnkey=False): parsed =urllib.parse.urlparse(url) netloc = parsed.netloc if netloc.startswith("www."): netloc = netloc[4:] matching_key = None match = False keys = list(redirectlist.keys()) while not match and len(keys) > 0: key = keys.pop(0) match = key == netloc #We also match subdomains if key.startswith("*"): match = netloc.endswith(key[1:]) if match: matching_key = key if matching_key: value = redirectlist[matching_key] else: value = None if returnkey: return [value,matching_key] else: return value # Return None if not blocked, else return the blocking rule def get_url_blocking_rule(url,redirectlist): redir,key = get_url_redirected(url,redirectlist,returnkey=True) if redir and redir.lower() == "blocked": return key else: return None def is_url_blocked(url,redirectlist): if get_url_blocking_rule(url,redirectlist): return True else: return False offpunk-v3.0/openk.py000077500000000000000000000416601514232770500147200ustar00rootroot00000000000000#!/usr/bin/env python3 # openk stand for "Open like a PuNK". # It will open any file or URL and display it nicely in less. # If not possible, it will fallback to xdg-open # URL are retrieved through netcache import argparse import fnmatch import os import shutil import sys import tempfile import time import gettext import ansicat import netcache import offutils import offblocklist from offutils import ( GREPCMD, is_local, mode_url, run, term_width, unmode_url, init_config, send_email, _HAS_XDGOPEN, _LOCALE_DIR, ) gettext.bindtextdomain('offpunk', _LOCALE_DIR) gettext.textdomain('offpunk') _ = gettext.gettext less_version = 0 if not shutil.which("less"): print(_('Please install the pager "less" to run Offpunk.')) print(_("If you wish to use another pager, send me an email !")) print( _('(I’m really curious to hear about people not having "less" on their system.)') ) sys.exit() output = run("less --version") # We get less Version (which is the only integer on the first line) words = output.split("\n")[0].split() less_version = 0 for w in words: # On macOS the version can be something like 581.2 not just an int: if all(_.isdigit() for _ in w.split(".")): less_version = int(w.split(".", 1)[0]) # restoring position only works for version of less > 572 if less_version >= 572: _LESS_RESTORE_POSITION = True else: _LESS_RESTORE_POSITION = False # _DEFAULT_LESS = "less -EXFRfM -PMurl\ lines\ \%lt-\%lb/\%L\ \%Pb\%$ %s" # -E : quit when reaching end of file (to behave like "cat") # -F : quit if content fits the screen (behave like "cat") # -X : does not clear the screen # -R : interpret ANSI colors correctly # -f : suppress warning for some contents # -M : long prompt (to have info about where you are in the file) # -W : hilite the new first line after a page skip (space) # -i : ignore case in search # -S : do not wrap long lines. Wrapping is done by offpunk, longlines # are there on purpose (surch in asciiart) # --incsearch : incremental search starting rev581 def less_cmd(file, histfile=None, cat=False, grep=None): less_prompt = "page %%d/%%D- lines %%lb/%%L - %%Pb\\%%" if less_version >= 581: less_base = 'less --incsearch --save-marks -~ -XRfWiS -P "%s"' % less_prompt elif less_version >= 572: less_base = "less --save-marks -XRfMWiS" else: less_base = "less -XRfMWiS" _DEFAULT_LESS = less_base + " \"+''\" %s" _DEFAULT_CAT = less_base + " -EF %s" if histfile: env = {"LESSHISTFILE": histfile} else: env = {} if cat and not grep: cmd_str = _DEFAULT_CAT elif grep: grep_cmd = GREPCMD # case insensitive for lowercase search if grep.islower(): grep_cmd += " -i" cmd_str = _DEFAULT_CAT + "|" + grep_cmd + " %s" % grep else: cmd_str = _DEFAULT_LESS run(cmd_str, parameter=file, direct_output=True, env=env) class opencache: def __init__(self): # We have a cache of the rendering of file and, for each one, # a less_histfile containing the current position in the file self.temp_files = {} self.less_histfile = {} # This dictionary contains an url -> ansirenderer mapping. This allows # to reuse a renderer when visiting several times the same URL during # the same session # We save the time at which the renderer was created in renderer_time # This way, we can invalidate the renderer if a new version of the source # has been downloaded self.rendererdic = {} self.renderer_time = {} self.mime_handlers = {} self.last_mode = {} self.last_width = term_width(absolute=True) self.redirects = offblocklist.redirects def _get_handler_cmd(self, mimetype,file_extension=None): # Now look for a handler for this mimetype # Consider exact matches before wildcard matches exact_matches = [] wildcard_matches = [] for handled_mime, cmd_str in self.mime_handlers.items(): if "*" in handled_mime: wildcard_matches.append((handled_mime, cmd_str)) else: exact_matches.append((handled_mime, cmd_str)) for handled_mime, cmd_str in exact_matches + wildcard_matches: if fnmatch.fnmatch(mimetype, handled_mime): break #we try to match the file extension, with a starting dot or not elif file_extension == handled_mime.strip("."): break else: # Use "xdg-open" as a last resort. if _HAS_XDGOPEN: cmd_str = "xdg-open %s" else: #TRANSLATORS: keep echo and %s, translate the text between "" cmd_str = _('echo "Can’t find how to open "%s') print(_("Please install xdg-open (usually from xdg-util package)")) return cmd_str # Return the handler for a specific mimetype. # Return the whole dic if no specific mime provided def get_handlers(self, mime=None): if mime and mime in self.mime_handlers.keys(): return self.mime_handlers[mime] elif mime: return None else: return self.mime_handlers def set_handler(self, mime, handler): if "%s" not in handler: #if no %s, we automatically add one. I can’t think of any usecase # where it should not be part of the handler handler += " %s" previous = None if mime in self.mime_handlers.keys(): previous = self.mime_handlers[mime] self.mime_handlers[mime] = handler def get_renderer(self, inpath, mode=None, theme=None,**kwargs): # We remove the ##offpunk_mode= from the URL # If mode is already set, we don’t use the part from the URL inpath, newmode = unmode_url(inpath) if not mode: mode = newmode # If we still doesn’t have a mode, we see if we used one before if not mode and inpath in self.last_mode.keys(): mode = self.last_mode[inpath] elif not mode: # default mode is readable mode = "readable" renderer = None path = netcache.get_cache_path(inpath) if path: usecache = inpath in self.rendererdic.keys() and not is_local(inpath) # Screen size may have changed width = term_width(absolute=True) if usecache and self.last_width != width: self.cleanup() usecache = False if usecache: if inpath in self.renderer_time.keys(): last_downloaded = netcache.cache_last_modified(inpath) last_cached = self.renderer_time[inpath] if last_cached and last_downloaded: usecache = last_cached > last_downloaded else: usecache = False else: usecache = False if not usecache: renderer = ansicat.renderer_from_file(path, url=inpath, theme=theme,\ redirectlist=self.redirects,**kwargs) if renderer: self.rendererdic[inpath] = renderer self.renderer_time[inpath] = int(time.time()) else: renderer = self.rendererdic[inpath] return renderer def get_temp_filename(self, url): if url in self.temp_files.keys(): return self.temp_files[url] else: return None def openk(self, inpath, mode="readable", terminal=True, grep=None, theme=None, \ link=None, direct_open_unsupported=False, **kwargs): # Return True if inpath opened in Terminal # False otherwise # also returns the url in case it has been modified # if terminal = False, we don’t try to open in the terminal, # we immediately fallback to xdg-open. # netcache currently provide the path if it’s a file. # If link is a digit, we open that link number instead of the inpath # If direct_open_unsupported, we don’t print the "unsupported warning" # and, instead, immediately fallback to external open if not offutils.is_local(inpath): if mode: kwargs["images_mode"] = mode cachepath, inpath = netcache.fetch(inpath, redirects=self.redirects,**kwargs) if not cachepath: return False, inpath # folowing line is for :// which are locals (file,list) elif "://" in inpath: cachepath, inpath = netcache.fetch(inpath, redirects=self.redirects,**kwargs) elif inpath.startswith("mailto:"): cachepath = inpath elif os.path.exists(inpath): cachepath = inpath else: print(_("%s does not exist") % inpath) return False, inpath renderer = self.get_renderer(inpath, mode=mode, theme=theme, **kwargs) if link and link.isdigit(): inpath = renderer.get_link(int(link)) renderer = self.get_renderer(inpath, mode=mode, theme=theme, **kwargs) if renderer and mode: renderer.set_mode(mode) self.last_mode[inpath] = mode if not mode and inpath in self.last_mode.keys(): mode = self.last_mode[inpath] renderer.set_mode(mode) # we use the full moded url as key for the dictionary key = mode_url(inpath, mode) if renderer and not renderer.is_format_supported() and direct_open_unsupported: terminal = False if terminal and renderer: # If this is an image and we have chafa/timg, we # don’t use less, we call it directly if renderer.has_direct_display(): renderer.display(mode=mode, directdisplay=True) return True, inpath else: body = renderer.display(mode=mode) # Should we use the cache ? only if it is not local and there’s a cache usecache = key in self.temp_files and not is_local(inpath) if usecache: # and the cache is still valid! last_downloaded = netcache.cache_last_modified(inpath) last_cached = os.path.getmtime(self.temp_files[key]) if last_downloaded > last_cached: usecache = False self.temp_files.pop(key) self.less_histfile.pop(key) # We actually put the body in a tmpfile before giving it to less if not usecache: tmpf = tempfile.NamedTemporaryFile( "w", encoding="UTF-8", delete=False ) self.temp_files[key] = tmpf.name tmpf.write(body) tmpf.close() if key not in self.less_histfile: firsttime = True tmpf = tempfile.NamedTemporaryFile( "w", encoding="UTF-8", delete=False ) self.less_histfile[key] = tmpf.name else: # We don’t want to restore positions in lists firsttime = is_local(inpath) less_cmd( self.temp_files[key], histfile=self.less_histfile[key], cat=firsttime, grep=grep, ) return True, inpath # maybe, we have no renderer. Or we want to skip it. else: mimetype = ansicat.get_mime(cachepath) #we find the file extension by taking the last part of the path #and finding a dot. last_part = cachepath.split("/")[-1] extension = None if last_part and "." in last_part: extension = last_part.split(".")[-1] if mimetype == "mailto": mail = inpath[7:] send_email(mail,toconfirm=True) return False, inpath else: cmd_str = self._get_handler_cmd(mimetype,file_extension=extension) #TRANSLATORS translate only "MY_PREFERED_APP" change_cmd = _("\"handler %s MY_PREFERED_APP %%s\"")%mimetype try: #we don’t write the info if directly opening to avoid #being verbose in openk if not direct_open_unsupported: print(_("External open of type %s with \"%s\"")%(mimetype,cmd_str)) print(_("You can change the default handler with %s")%change_cmd) run( cmd_str, parameter=netcache.get_cache_path(inpath), direct_output=True, ) return True, inpath except FileNotFoundError: print(_("Handler program %s not found!") % shlex.split(cmd_str)[0]) print(_("You can use the ! command to specify another handler program\ or pipeline.")) print(_("You can change the default handler with %s")%change_cmd) return False, inpath # We remove the renderers from the cache and we also delete temp files def cleanup(self): while len(self.temp_files) > 0: os.remove(self.temp_files.popitem()[1]) while len(self.less_histfile) > 0: os.remove(self.less_histfile.popitem()[1]) #After cleanup, we set the current size of the terminal self.last_width = term_width(absolute=True) self.rendererdic = {} self.renderer_time = {} self.last_mode = {} # Clean only a specificif url cache def clean_url(self,url,mode=None): def delfile(path): if os.path.isfile(path): os.remove(path) # First, we take the full URL if mode: url = mode_url(url,mode) # Unrendered element, such as picture, are not in the dictionnaries if url in self.temp_files: delfile(self.temp_files.pop(url)) if url in self.less_histfile: delfile(self.less_histfile.pop(url)) url, newmode = unmode_url(url) if url in self.rendererdic: self.rendererdic.pop(url) if url in self.renderer_time: self.renderer_time.pop(url) def main(): descri = _("openk is an universal open command tool that will try to display any file \ in the pager less after rendering its content with ansicat. If that fails, \ openk will fallback to opening the file with xdg-open. If given an URL as input \ instead of a path, openk will rely on netcache to get the networked content.") parser = argparse.ArgumentParser(prog="openk", description=descri) parser.add_argument( "--mode", metavar="MODE", help=_("Which mode should be used to render: normal (default), full or source.\ With HTML, the normal mode try to extract the article."), ) parser.add_argument( "--linkmode", choices=[ "none", "end", ], help=_("Which mode should be used to render links: none (default) or end"), ) parser.add_argument( "content", metavar="INPUT", nargs="*", default=sys.stdin, help=_("Path to the file or URL to open"), ) parser.add_argument( "--cache-validity", type=int, default=0, help=_("maximum age, in second, of the cached version before \ redownloading a new version"), ) args = parser.parse_args() cache = opencache() #we read the startup config and we only care about the "handler" command cmds = init_config(skip_go=True,interactive=False,verbose=False) for cmd in cmds: splitted = cmd.split(maxsplit=2) if len(splitted) >= 3 and splitted[0] == "handler": cache.set_handler(splitted[1],splitted[2]) # if the second argument is an integer, we associate it with the previous url # to use as a link_id if (type(args.content) == list) and len(args.content) == 2 and args.content[1].isdigit(): url = args.content[0] link_id = args.content[1] cache.openk(url, mode=args.mode, validity=args.cache_validity, link=link_id,\ direct_open_unsupported=True, linkmode=args.linkmode) else: for f in args.content: cache.openk(f, mode=args.mode, validity=args.cache_validity,\ direct_open_unsupported=True, linkmode=args.linkmode) if __name__ == "__main__": if "opnk" in sys.argv[0]: print("WARNING: opnk.py has been deprecated in favour of openk.py") print("******* Replace all your opnk calls by \"openk\"") main() offpunk-v3.0/opnk.py000077700000000000000000000000001514232770500162222openk.pyustar00rootroot00000000000000offpunk-v3.0/po/000077500000000000000000000000001514232770500136365ustar00rootroot00000000000000offpunk-v3.0/po/README.md000066400000000000000000000175011514232770500151210ustar00rootroot00000000000000 # Brief how to to translate and keep offpunk translated and up to date This is a very brief document, showing merely the commands one would need to keep offpunk translatable, and some notes for potential translators on how to add a new language to the available ones As a pre-requirement, make sure you have gettext installed in your system. We recommend you also install poedit, a user-friendly and easy to use editor for po files: ``` # apt install gettext poedit ``` (instructions for other systems are welcome) ## Quick TL;DR If you are interested in the details and reasons for all the commands, keep reading. Here's a quick summary of the steps you need to contribute a translation for offpunk. Have in mind that, since offpunk uses python docstrings to provide user help, we need to do a couple extra steps compared to a typical application using gettext. Mostly, we need to extract all the docstrings we are going to show the user, and write them to a temporary file so the "xgettext" command can find them. In order to make it easier for translators, there's a script in offpunk's repository ("po/create_pot.sh") that would scan the source files and create a "po template" file automatically. Just run these commands: ``` cd cd po/ ./create_pot.sh # this will generate a "messages.pot" file msginit -i messages.pot -o XX.po # XX should be your language code #it will ask you details like your email poedit XX.po ``` poedit will create a XX.mo file that you can test in your system (see below for details) If everything is correct, you can contribute the XX.po file. We'll be happy to accept it! Keep reading now for details :) ## Creating and updating the "po template" (pot file) In the gettext system, all translations start with this file. It's by default called messages.pot To create it, this is the command used (from the root folder of the offpunk source code): ``` $ xgettext --add-comments=TRANSLATORS *py -o po/messages.pot ``` but, because we use docstrings for offpunk's internal help system, there's a previous step: we have a small python script (extract_docstrings.py) to we use to extract all the necessary extra messages and write them to a temporary file. This is all done automatically for you if you use the "create_pot.sh" script. We encourage you to look at those scripts if you are interested. xgettext will "extract" all the translatable strings from all the python files (*py), and use po/messages.pot as the output file (-o) The "--add-comments=TRANSLATORS" part of the command tells xgettext to copy the comments that the devs left for translators. These comments will give valuable tips to translators. See the next page for more detail: => https://www.gnu.org/software/gettext/manual/html_node/Translator-advice.html Advice for translators (gnu.org) in the future, if new "translatable" strings are added (or the strings are modified), this same command can be run again. In fact, if you are going to work in a translation at a given time, generating a fresh messages.pot is always a good idea. Remember you can do this simply by running: ``` ./create_pot.sh ``` from the po/ folder. ## Creating a translation for a new language If you are an offpunk user and want to translate it into your language, you can do it with these steps: first, make sure you have your system configured to use the right locale: ``` $ locale ``` Then, follow the steps above to create the "po template" file (messages.pot) Ideally, your system would be configured to use your native language, and ideally that's the "target" language you'll translate offpunk into (but this is not strictly necessary) . Enter the po/ folder: ``` $ cd po ``` and then run this command to create a po file from the 'po template' file: ``` $ msginit -i messages.pot -o XX.po ``` XX should be the language code of the language you'll translate offpunk into. Examples are fr_FR, fr_CA, es_ES, es_AR, and others If your system does not currently use that same language (locale), you can specify the lang running instead: ``` $ msginit -l LANG_CODE -i messages.pot -o XX.po ``` (you might want to check 'man msginit') ## Translating the messages Po files are technically text files and can be edited with your favorite editor. However, if you are a new translator, I recommend using poedit. ``` $ poedit XX.po ``` XX.po is the file created before Then, you can click on the different messages, and input an appropriate translation under them. When saving, poedit will create a XX.mo file (this is the binary format that your computer will actually use to show offpunk in your language. It also has a menu option to do that. If you were interested, you can manually create this .mo file by: ``` $ msgfmt XX.po -o XX.mo ``` ## Testing your translation After you have translated the whole file (or even some of the strings), and you have a XX.mo file, you can test it by: ``` # cp XX.mo /usr/share/locale/XX/LC_MESSAGES/offpunk.mo ``` (these are the paths in a debian system. Not sure how universal this is, but right now it's more-or-less hardcoded in the .py files) keep in mind 'XX' in that path will match the output you see when you run "locale" in your terminal then you can start offpunk.py from the source code and check if any of the strings have to be changed NOTE: if you current LOCALE doesn't match the one you are translating into, you can test the language anyway, tweaking the environment a bit, only for offpunk. For example: your system is in spanish, but you also speak german, and are now translating offpunk to german. You could test the german translation by: ``` # cp de.mo /usr/share/locale/de/LC_MESSAGES/offpunk.mo #this should require "sudo", or be run as root $ locale LANG=es_ES.UTF-8 [...] $ LANG=de ./offpunk.py ``` ## Keeping your translation up-to-date Every now and then, new messages will make their way into offpunk. New features are added, some messages change... In those cases, you can incorporate the new messages that appear in messages.pot (that you can generate with the 'create_pot.sh script) to your language's po file by running: ``` $ msgmerge -U XX.po messages.pot ``` But, if you don't want to have to remember these commands, poedit also has a menu entry that would let you, while you are translating your po file, "Update from POT file". You can find that menu entry under the "Translation" menu. Then, navigate and choose the updated messages.pot file and you are done, new untranslated strings will apear in the poedit interface for you to translate. Translate, compile the .mo file, test your translation, and you're good! If you get into translating free software into your language, you can explore poedit's capabilities ("pre-translate" from "translation memory" will soon prove its usefulness), and other translation tools and maybe decide you like some other tool better than poedit. Poedit has been used as an example in this guide because it is powerful enough and easy enough to use that we can only recommend it as the perfect starting point. ## A note to devs Ideally, all strings that are shown to users should be translatable, so offpunk users can benefit from it and use the program in their native language. Making the messages translatable is not too difficult. As a general rule, if a message is to be shown, like: ``` print("Welcome to my program") ``` it would be enough to surround the actual string with the "_()" function, like this: ``` print(_("Welcome to my program")) ``` You can also add comments for the future translators that could help them understand tricky messages. Translation software often will show these hints while the translators are working on the messages. ``` #TRANSLATORS: this is a verb. Like in "open the window", not "the window is open" print(_("Open")) ``` Take a look at this link if you are interested in the topic: => https://www.gnu.org/software/gettext/manual/html_node/Translator-advice.html Translator advice offpunk-v3.0/po/create_pot.sh000077500000000000000000000014511514232770500163230ustar00rootroot00000000000000#!/usr/bin/env bash cd .. # get all "normal" translatable strings xgettext --add-comments=TRANSLATORS *py -o po/messages_xgettext.pot # get only docstrings from offpunk, for interactive help pygettext3 -K -D offpunk.py # get rid of indentation caused by docstrings' nature sed -e 's/" /"/' messages.pot >messages2.pot # merge regular messages and docstrings xgettext -o po/messages.pot messages2.pot po/messages_xgettext.pot # delete temporal files rm messages.pot messages2.pot po/messages_xgettext.pot cd po # previous version of this script, included in case # someone has any trouble with the current one. # Just uncomment the following lines: # #./extract_docstrings.py > ../docstrings.py # # #cd .. #xgettext --add-comments=TRANSLATORS *py -o po/messages.pot #rm docstrings.py #cd po offpunk-v3.0/po/es_ES.po000066400000000000000000002161771514232770500152120ustar00rootroot00000000000000# Spanish translations for Offpunk package. # Copyright (C) 2026 Offpunk'S COPYRIGHT HOLDER # This file is distributed under the same license as the Offpunk package. # jmcs , 2026. # msgid "" msgstr "" "Project-Id-Version: Offpunk 3.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-07 10:12+0100\n" "PO-Revision-Date: 2026-02-07 10:14+0100\n" "Last-Translator: jmcs \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.8\n" #: offpunk.py:3 msgid "" "\n" "Offline-First Gemini/Web/Gopher/RSS reader and browser\n" msgstr "" "\n" "Lector y navegador Gemini/Web/Gopher/RSS \"Offline Primero\"\n" #: offpunk.py:324 msgid "" "This method might be considered \"the heart of Offpunk\".\n" "Everything involved in fetching a gemini resource happens here:\n" "sending the request over the network, parsing the response,\n" "storing the response in a temporary file, choosing\n" "and calling a handler program, and updating the history.\n" "Nothing is returned." msgstr "" "Este método puede considerarse \"el corazón de Offpunk\".\n" "Todo lo involucrado en descargar un recurso gemini ocurre aquí:\n" "enviar la petición a través de la red, parsear la respuesta,\n" "almacenar la respuesta en un ficheiro temporal, escoger\n" "y lanzar un programa manejador, y actualizar el historial.\n" "No devuelve nada." #: offpunk.py:463 msgid "" "Display and manage the list of redirected URLs. This features is mostly useful to use privacy-" "friendly frontends for popular websites." msgstr "" "Mostrar y gestionar la lista de URLs redirigidas. Esta característica es sobre todo útil para usar " "frontends con mejor privacidad para sitios web populares." #: offpunk.py:499 msgid "View or set various options." msgstr "Ver o configurar varias opciones." #: offpunk.py:562 msgid "" "Change the colors of your rendered text.\n" "\n" "\"theme ELEMENT COLOR\"\n" "\n" "ELEMENT is one of: window_title, window_subtitle, title,\n" "subtitle,subsubtitle,link,oneline_link,new_link,image_link,preformatted,blockquote, " "blocked_link.\n" "\n" "COLOR is one or many (separated by space) of: bold, faint, italic, underline, black,\n" "red, green, yellow, blue, purple, cyan, white.\n" "\n" "Each color can alternatively be prefaced with \"bright_\".\n" "If color is \"none\", then that part of the theme is removed.\n" "\n" "theme can also be used with \"preset\" to load an existing theme.\n" "\n" "\"theme preset\" : show available themes\n" "\"theme preset PRESET_NAME\" : swith to a given preset" msgstr "" "Cambiar los colores de su texto renderizado.\n" "\n" "\"theme ELEMENTO COLOR\"\n" "\n" "ELEMENTO es uno de estos: window_title, window_subtitle, title,\n" "subtitle, subsubtitle, link, oneline_link, new_link, image_link, preformatted, blockquote, " "blocked_link.\n" "\n" "COLOR es uno o varios (separados por espacios) de estos: bold, faint, italic, underline, black,\n" "red, green, yellow, blue, purple, cyan, white.\n" "\n" "Como alternativa, los colores pueden ir precedidos de \"bright_\" (\"claro\").\n" "Si el color es \"none\", se quitará esa parte del \"theme\".\n" "\n" "'theme' también se puede usar con \"preset\" para cargar un tema existente.\n" "\n" "\"theme preset\" : mostrar los temas disponibles\n" "\"theme preset NOMBRE_DEL_TEMA : cambiar a un tema disponible" #: offpunk.py:651 msgid "" "View or set handler commands for different MIME types.\n" "handler MIMETYPE : see handler for MIMETYPE\n" "handler MIMETYPE CMD : set handler for MIMETYPE to CMD\n" "in the CMD, %s will be replaced by the filename.\n" "if no %s, it will be added at the end.\n" "MIMETYPE can be the true mimetype or the file extension.\n" "\n" "Examples: \n" " handler application/pdf zathura %s\n" " handler .odt lowriter\n" " handler docx lowriter" msgstr "" "Ver o configurar comandos manejadores para diferentes tipos MIME.\n" "handler TIPO_MIME : ver el manejador para el TIPO_MIME\n" "handler TIPO_MIME COMANDO : configurar el manejador para TIPO_MIME a COMANDO\n" "en el COMANDO, se reemplazará '%s' por el nombre del fichero.\n" "si no hay '%s', se añadirá al final.\n" "TIPO_MIME puede ser un tipo MIME real o una extensión de fichero.\n" "\n" "Ejemplos: \n" " handler application/pdf zathura %s\n" " handler .odt lowriter\n" " handler docx lowriter" #: offpunk.py:679 msgid "" "Create or modifiy an alias\n" "alias : show all existing aliases\n" "alias ALIAS : show the command linked to ALIAS\n" "alias ALIAS CMD : create or replace existing ALIAS to be linked to command CMD" msgstr "" "Crear o modificar un alias\n" "alias : mostrar todos los aliases existentes\n" "alias ALIAS : mostrar el comando enlazado a ALIAS\n" "alias ALIAS CMD : crear oreemplazar el ALIAS existente para enlazarlo al comando CMD" #: offpunk.py:714 msgid "Use Offpunk offline by only accessing cached content" msgstr "Usar Offpunk offline accediendo sólo al contenido en la caché" #: offpunk.py:723 msgid "Use Offpunk online with a direct connection" msgstr "Usar Offpunk online con una conexión directa" #: offpunk.py:732 msgid "" "Copy the content of the last visited page as gemtext/html in the clipboard.\n" "Use with \"url\" as argument to only copy the adress.\n" "Use with \"raw\" to copy ANSI content as seen in your terminal (with colour codes).\n" "Use with \"cache\" to copy the path of the cached content.\n" "Use with \"title\" to copy the title of the page.\n" "Use with \"link\" to copy a link in the gemtext format to that page with the title." msgstr "" "Copiar el contenido de la última página visitada en formato gemtext/html en el portapapeles.\n" "Úselo con \"url\" de argumento para copiar sólo la dirección.\n" "Úselo con \"raw\" para copiar el contenido ANSI tal como se ve en su terminal (con códigos de " "colores).\n" "Úselo con \"cache\" para copiar la ruta al contenido en caché.\n" "Úselo con \"title\" para copiar el título de la página.\n" "Úselo con \"link\" para copiar un enlace en formato gemtext a esa página, con el título." #: offpunk.py:772 msgid "" "Send current page by email to someone else.\n" "Use with \"url\" as first argument to send only the address.\n" "Use with \"text\" as first argument to send the full content. TODO\n" "Without argument, \"url\" is assumed.\n" "Next arguments are the email adresses of the recipients.\n" "If no destination, you will need to fill it in your mail client." msgstr "" "Mandar la página actual a alguien por correo electrónico.\n" "Úselo con \"url\" de primer argumento para enviar sólo la dirección.\n" "Úselo con \"text\" de primer argumento para enviar el contenido completo. PENDIENTE\n" "Si no hay argumento, se asume \"url\".\n" "Los argumentos posteriores son direcciones de correo electrónico de los destinatarios.\n" "Si no hay destinatario, tendrá que introducirlo en su cliente de correo." #: offpunk.py:817 msgid "" "Reply by email to a page by trying to find a good email for the author.\n" "If an email is provided as an argument, it will be used.\n" "arguments:\n" "- \"save\" : allows to detect and save email without actually sending an email.\n" "- \"save new@email\" : save a new reply email to replace an existing one" msgstr "" "Responder via email a una página intentando encontrar un email correcto para el autor.\n" "Si se pasa una dirección de correo como argumento, se utilizará.\n" "argumentos:\n" "- \"save\" : permite detectar y guardar un email sin realmente enviar un email.\n" "- \"save nuevo@correo.electrónico\" : guardar un nuevo email de respuesta para reemplazar a uno " "existente" #: offpunk.py:944 msgid "" "Manipulate cookies:\n" "\"cookies import [url]\" - import cookies from file to be used with [url]\n" "\"cookies list [url]\" - list existing cookies for current url\n" "default is listing cookies for current domain.\n" "\n" "To get a cookie as a txt file,use the cookie-txt extension for Firefox." msgstr "" "Manipular cookies:\n" "\"cookies import [url]\" - importar cookies desde un fichero para usarlas con [url]\n" "\"cookies list [url]\" - listar las cookies existentes para la URL\n" "por defecto se muestran las cookies para el dominio actual.\n" "\n" "Para obtener una cookie en formato fichero txt file,use la extensión cookie-txt para Firefox." #: offpunk.py:999 msgid "Go to a gemini URL or marked item." msgstr "Ir a una URL gemini o elemento marcado." #: offpunk.py:1043 msgid "Reload the current URL." msgstr "Recargar la URL actual." #: offpunk.py:1058 msgid "" "Go up one directory in the path.\n" "Take an integer as argument to go up multiple times.\n" "Use \"~\" to go to the user root\"\n" "Use \"/\" to go to the server root." msgstr "" "Subir un directorio en la ruta.\n" "Acepta un número como argumento para subir múltiples veces.\n" "Use \"~\" para ir a la \"raíz del usuario\"\n" "Use \"/\" para ir a la raíz del servidor." #: offpunk.py:1083 msgid "Go back to the previous gemini item." msgstr "Volver al elemento gemini anterior." #: offpunk.py:1092 msgid "Go forward to the next gemini item." msgstr "Avanzar al siguiente elemento gemini." #: offpunk.py:1102 msgid "" "Go to the root of current capsule/gemlog/page\n" "If arg is \"/\", the go to the real root of the server" msgstr "" "Ir a la raíz de la cápsula/gemlog/página actual\n" "Si el argumento es \"/\", entonces se irá a la raíz real del servidor" #: offpunk.py:1111 msgid "" "Add index items as waypoints on a tour, which is basically a FIFO\n" "queue of gemini items.\n" "\n" "`tour` or `t` alone brings you to the next item in your tour.\n" "Items can be added with `tour 1 2 3 4` or ranges like `tour 1-4`.\n" "All items in current menu can be added with `tour *`.\n" "All items in $LIST can be added with `tour $LIST`.\n" "Current item can be added back to the end of the tour with `tour .`.\n" "Current tour can be listed with `tour ls` and scrubbed with `tour clear`." msgstr "" "Añadir elementos del índice como puntos de referencia en un tour, que es básicamente\n" "una lista FIFO de elementos gemini.\n" "\n" "`tour` o sólo `t` le lleva al siguiente elemento de su tour.\n" "Se pueden añadir elementos usando `tour 1 2 3 4` o rangos como `tour 1-4`.\n" "Se pueden añadir todos los elementos del menú actual usando `tour *`.\n" "Todos los elementos en una $LISTA se pueden añadir usando `tour $LISTA`.\n" "El elemento actual se puede volver a añadir al final del tour usando `tour .`.\n" "El tour actual se puede listar con `tour ls` y eliminar con `tour clear`." #: offpunk.py:1181 msgid "" "Manage your client certificates (identities) for a site.\n" "`certs` will display all valid certificates for the current site\n" "`certs new ` will create a new certificate, if no url is " "specified, the current open site will be used." msgstr "" "Gestionar sus certificados de cliente (identidades) para un sitio.\n" "`certs` mostrará todos los certificados válidos para el sitio actual\n" "`certs new ` creará un nuevo certificado. Sino se " "especifica una URL, se usará el sitio abierto actualmente." #: offpunk.py:1208 msgid "" "Mark the current item with a single letter. This letter can then\n" "be passed to the 'go' command to return to the current item later.\n" "Think of it like marks in vi: 'mark a'='ma' and 'go a'=''a'.\n" "Marks are temporary until shutdown (not saved to disk)." msgstr "" "Marcar el elemento actual con una única letra. Esta letra puede luego\n" "pasarse al comando 'go' para volver al elemento actual más tarde.\n" "Piense que son como 'marks' en el editor 'vi': 'mark a'='ma' y 'go a'=''a'.\n" "Las marcas son temporales hasta salir de Offpunk (no se guardan a disco)." #: offpunk.py:1223 msgid "Display information about current page." msgstr "Mostrar información de la página actual." #: offpunk.py:1262 msgid "Display version and system information." msgstr "Mostrar información de versión y sistema." #: offpunk.py:1326 msgid "" "Send a mail to the offpunk-devel list with technical informations\n" "about your offpunk version. You will be prompted to write an email\n" "describing how to reproduce the bug." msgstr "" "Enviar un email a la lista offpunk-devel con información tácnica\n" "acerca de su versión de offpunk. Se le pedirá que escriba un email\n" "describiendo cómo reproducir el error." #: offpunk.py:1346 msgid "" "Search on Gemini using the engine configured (by default kennedy.gemi.dev)\n" "You can configure it using \"set search URL\".\n" "URL should contains one \"%s\" that will be replaced by the search term." msgstr "" "Buscar en Gemini usando el motor configurado (predeterminado, kennedy.gemi.dev)\n" "Puede configurarlo usando \"set search URL\".\n" "URL debería contener un \"%s\", que se sustituirá por el término de búsqueda." #: offpunk.py:1354 msgid "" "Search on the web using the engine configured (by default wiby.me)\n" "You can configure it using \"set websearch URL\".\n" "URL should contains one \"%s\" that will be replaced by the search term." msgstr "" "Buscar en la web usando el motor configurado (por defecto, wiby.me)\n" "Puede configurarlo usando \"set websearch URL\".\n" "La URL deberá contener un \"%s\" que será reemplazado por el término de búsqueda." #: offpunk.py:1362 msgid "" "Search on wikipedia using the configured Gemini interface.\n" "The first word should be the two letters code for the language.\n" "Exemple : \"wikipedia en Gemini protocol\"\n" "But you can also use abbreviations to go faster:\n" "\"wen Gemini protocol\". (your abbreviation might be missing, report the bug)\n" "while it's not added, \"w\" is still an option you can use:\n" "\"w en Gemini protocol\" will work as a shortcut as well\n" "The interface used can be modified with the command:\n" "\"set wikipedia URL\" where URL should contains two \"%s\", the first\n" "one used for the language, the second for the search string." msgstr "" "Buscar en wikipedia usando la interfaz Gemini configurada.\n" "La primera palabra debería ser el código de idioma de dos letras.\n" "Ejemplo : \"wikipedia es Protocolo Gemini\"\n" "Pero también puede usar abreviaturas para ir más rápido:\n" "\"wes protocolo Gemini\". (su abreviatura puede no estar presente, informe del error)\n" "mientras no esté incluido, también tiene la opción de usar \"w\":\n" "\"w es protocolo Gemini\" también funciona como atajo\n" "La interfaz usada puede modificarse usando el comando:\n" "\"set wikipedia URL\", donde la URL debe contener dos \"%s\", el primero\n" "usado para el idioma, y el segundo para la cadena de búsqueda." #: offpunk.py:1383 msgid "Open the specified XKCD comics (a number is required as parameter)" msgstr "Abrir el cómic XKCD especificado (se requiere un número como parámetro)" #: offpunk.py:1391 msgid "Submit a search query to the geminispace.info search engine." msgstr "Enviar una consulta al motor de búsqueda geminspace.info." #: offpunk.py:1399 msgid "Display history." msgstr "Mostrar el historial." #: offpunk.py:1404 msgid "Find in current page by displaying only relevant lines (grep)." msgstr "Buscar en la página actual mostrando sólo las líneas relevantes (grep)." #: offpunk.py:1408 msgid "" "Display all the links for the current page.\n" " If argument N is provided, then page through N links at a time.\n" " \"links 10\" show you the first 10 links, then 11 to 20, etc.\n" " if N = 0, then all the links are displayed" msgstr "" "Mostrar todos los enlaces para la página actual.\n" " Si se introduce un argumento N, se paginará mostrando N enlaces de cada vez.\n" " \"links 10\" mostrará los primeros 10 enlaces, después, del 11 al 20, etc.\n" " si N = 0, se mostrarán todos los enlaces" #: offpunk.py:1429 msgid "DEPRECATED: List contents of current index." msgstr "DEPRECADO: Listar el contenido del índice actual." #: offpunk.py:1435 msgid "Default action when line is empty" msgstr "Acción por defecto cuando la línea está vacía" #: offpunk.py:1451 msgid "Display RSS or Atom feeds linked to the current page." msgstr "Mostrar feeds RSS o Atom enlazados en la página actual." #: offpunk.py:1476 msgid "" "Run most recently visited item through \"less\" command, restoring previous position.\n" "Use \"view normal\" to see the default article view on html page.\n" "Use \"view full\" to see a complete html page instead of the article view.\n" "Use \"view swich\" to switch between normal and full\n" "Use \"view XX\" where XX is a number to view information about link XX.\n" "(full, feed, feeds have no effect on non-html content)." msgstr "" "Pasar el elemento más recientemente visitadopor el comando \"less\", restaurando la posición " "anterior.\n" "Use \"view normal\" para ver la vista predeterminada de artículo en páginas html.\n" "Use \"view full\" para ver la página html completa en vez de la vista de artículo.\n" "Use \"view swich\" para cambiar entre \"normal\" y \"full\"\n" "Use \"view XX\", donde XX es un número, para ver información sobre el enlace XX.\n" "(\"full\", \"feed\", \"feeds\" no tienen efecto en contenido no-html)." #: offpunk.py:1522 msgid "" "Open current item with the configured handler or xdg-open.\n" "Use \"open url\" to open current URL in a browser.\n" "Use \"open 2 4\" to open links 2 and 4\n" "You can combine with \"open url 2 4\" to open URL of links\n" "see \"handler\" command to set your handler." msgstr "" "Abrir en elemento actual con el manejador configurado, o \"xdg-open\".\n" "Use \"open url\" para abrir la URL actual en un navegador.\n" "Use \"open 2 4\" para abrir los enlaces 2 y 4\n" "Puede combinarlos con \"open url 2 4\" para abrir las URLs de los enlaces\n" "vea el comando \"handler\" para configurar su manejador." #: offpunk.py:1557 msgid "" "Send the content of the current page to the shell and pipe it.\n" "You are supposed to write what will come after the pipe. For example,\n" "if you want to count the number of lines containing STRING in the \n" "current page:\n" "> shell grep STRING|wc -l\n" "'!' is an useful shortcut.\n" "> !grep STRING|wc -l" msgstr "" "Enviar el contenido de la página actual al intérprete de comandos y \"pipearlo\" it.\n" "Se espera que escriba lo que va despues del \"pipe\". Por ejemplo,\n" "si quiere contar el número de líneas que contienen CADENA en la \n" "página actual:\n" "> shell grep CADENA | wc -l\n" "'!' es un atajo útil.\n" "> ! grep CADENA | wc -l" #: offpunk.py:1578 msgid "" "Save an item to the filesystem.\n" "'save n filename' saves menu item n to the specified filename.\n" "'save filename' saves the last viewed item to the specified filename.\n" "'save n' saves menu item n to an automagic filename." msgstr "" "Guardar un elemento en el sistema de ficheros.\n" "'save n nombre_de_fichero' guarda el elemento de menú 'n' en el nombre de fichero especificado.\n" "'save nombre_de_fichero' guarda el último elemento visto en el nombre de fichero especificado.\n" "'save n' guarda el elemento de menú 'n' a un nombre de fichero \"automágico\"." #: offpunk.py:1654 msgid "" "Print the url of the current page.\n" "Use \"url XX\" where XX is a number to print the url of link XX.\n" "\"url\" can also be piped to the shell, using the pipe \"|\"." msgstr "" "Imprimir la url de la página actual.\n" "Use \"url XX\" (donde XX es un número) para imprimir la url del enlace XX.\n" "\"url\" también puede \"pipearse\" al intérprete de comandos, usando la pipe \"|\"." #: offpunk.py:1676 msgid "" "Add the current URL to the list specified as argument.\n" "If no argument given, URL is added to Bookmarks.\n" "You can pass a link number as the second argument to add the link.\n" "\"add $LIST XX\" will add link number XX to $LIST" msgstr "" "Añadir la URL actual a la lista especificada como argumento.\n" "Si no se le pasa ningún argumento, la URL se añade a 'Bookmarks'.\n" "Puede pasarle un número de enlace como segundo argumento para añadir el enlace.\n" "\"add $LISTA XX\" añadirá el enlace número XX a la $LISTA" #: offpunk.py:1721 msgid "" "Subscribe to current page by saving it in the \"subscribed\" list.\n" "If a new link is found in the page during a --sync, the new link is automatically\n" "fetched and added to your next tour.\n" "To unsubscribe, remove the page from the \"subscribed\" list." msgstr "" "Suscribirse a la página actual guardándola en la lista \"subscribed\".\n" "Si se encuentra un enlace nuevo en la página durante un --sync, el nuevo enlace se descarga\n" "y añade automáticamente a su próximo tour.\n" "Para des-suscribirse, quite la página de la lista \"subscribed\"." #: offpunk.py:1761 msgid "" "Show or access the bookmarks menu.\n" "'bookmarks' shows all bookmarks.\n" "'bookmarks n' navigates immediately to item n in the bookmark menu.\n" "Bookmarks are stored using the 'add' command." msgstr "" "Mostrar o acceder al menú de marcadores.\n" "'bookmarks' muestra todos los marcadores.\n" "'bookmarks n' navega inmediatamente al elemento 'n' en el menú de marcadores.\n" "Use el comando 'add' para guardar marcadores." #: offpunk.py:1775 msgid "" "Archive current page by removing it from every list and adding it to\n" "archives, which is a special historical list limited in size. It is similar to `move archives`." msgstr "" "Archivar la página actual quitándola de todas las listas y añadiéndola a\n" "los archivos ('archives'), que es una lista de historial limitada en tamaño. Es similar a 'move " "archives'." #: offpunk.py:2010 msgid "" "move LIST will add the current page to the list LIST.\n" "With a major twist: current page will be removed from all other lists.\n" "If current page was not in a list, this command is similar to `add LIST`." msgstr "" "'move LISTA' añadirá la página actual a la lista LISTA.\n" "Con un detalle importante:la página actual se quitará de todas las demás listas.\n" "Si la página actual no estaba en ninguna lista, este comando es similar a 'add LISTA'." #: offpunk.py:2091 msgid "" "Manage list of bookmarked pages.\n" "- list : display available lists\n" "- list $LIST : display pages in $LIST\n" "- list create $NEWLIST : create a new list\n" "- list edit $LIST : edit the list\n" "- list subscribe $LIST : during sync, add new links found in listed pages to tour\n" "- list freeze $LIST : don’t update pages in list during sync if a cache already exists\n" "- list normal $LIST : update pages in list during sync but don’t add anything to tour\n" "- list delete $LIST : delete a list permanently (a confirmation is required)\n" "- list help : print this help\n" "See also :\n" "- add $LIST (to add current page to $LIST or, by default, to bookmarks)\n" "- move $LIST (to add current page to list while removing from all others)\n" "- archive (to remove current page from all lists while adding to archives)\n" "\n" "There’s no \"delete\" on purpose. The use of \"archive\" is recommended.\n" "\n" "The following lists cannot be removed or frozen but can be edited with \"list edit\"\n" "- list archives : contains last 200 archived URLs\n" "- history : contains last 200 visisted URLs\n" "- to_fetch : contains URLs that will be fetch during the next sync\n" "- tour : contains the next URLs to visit during a tour (see \"help tour\")" msgstr "" "Gestionar la lista de páginas en marcadores.\n" "- list : mostrar listas disponibles\n" "- list $LISTA : mostrar páginas en la lista $LISTA\n" "- list create $NUEVALISTA : crear una nueva lista\n" "- list edit $LISTA : editar lalista\n" "- list subscribe $LISTA : durante el sync, añadir nuevos enlaces encontrados en las páginas en la " "$LISTA al tour\n" "- list freeze $LISTA : no actualizar páginas en la $LISTA durante el sync si ya existe una caché\n" "- list normal $LISTA : actualizar páginas en la lista durante el syncpero no añadir nada al tour\n" "- list delete $LISTA : borrar permanentemente una lista (se requiere confirmación)\n" "- list help : imprimir esta ayuda\n" "Vea también :\n" "- 'add $LISTA' (para añadir la página actual a la $LISTA o, por defecto, a 'bookmarks')\n" "- 'move $LISTA' (para añadir la página actual a la lista y quitarla de todas las demás)\n" "- 'archive' (para quitar la página actual de todas las listas y añadirla a 'archives')\n" "\n" "No hay \"delete\" a propósito. Se recomienda el uso de \"archive\".\n" "\n" "Las siguientes listas no se pueden borrar ni \"congelar\" pero pueden editarse usando \"list " "edit\"\n" "- list archives : contiene las últimas 200 URLs archivadas\n" "- history : contiene las últimas 200 URLs visitadas\n" "- to_fetch : contiene URLs que se descargarán durante el próximo sync\n" "- tour : contiene las siguientes URLs a visitar durante un tour (vea \"help tour\")" #: offpunk.py:2215 msgid "ALARM! Recursion detected! ALARM! Prepare to eject!" msgstr "ALARMA! Recursión detectada! ALARMA! Prepárese para evacuar!" #: offpunk.py:2242 msgid "Access the offpunk.net tutorial (online)" msgstr "Acceder al tutorial en offpunk.net (online)" #: offpunk.py:2246 msgid "" "Synchronize all bookmarks lists and URLs from the to_fetch list.\n" "- New elements in pages in subscribed lists will be added to tour\n" "- Elements in list to_fetch will be retrieved and added to tour\n" "- Normal lists will be synchronized and updated\n" "- Frozen lists will be fetched only if not present.\n" "\n" "Before a sync, you can edit the list of URLs that will be fetched with the\n" "following command: \"list edit to_fetch\"\n" "\n" "Argument : duration of cache validity (in seconds)." msgstr "" "Synchronize all bookmarks lists and URLs from the to_fetch list.\n" "- New elements in pages in subscribed lists will be added to tour\n" "- Elements in list to_fetch will be retrieved and added to tour\n" "- Normal lists will be synchronized and updated\n" "- Frozen lists will be fetched only if not present.\n" "\n" "Before a sync, you can edit the list of URLs that will be fetched with the\n" "following command: \"list edit to_fetch\"\n" "\n" "Argument : duration of cache validity (in seconds)." #: offpunk.py:2418 msgid "Exit Offpunk." msgstr "Salir de Offpunk." #: ansicat.py:60 msgid "To improve your web experience (less cruft in webpages)," msgstr "Para mejorar su experiencia web (menos relleno en páginas web)," #: ansicat.py:61 msgid "please install python3-readability or readability-lxml" msgstr "por favor instale 'python3-readability' o 'readability-lxml'" #: ansicat.py:100 msgid "To render images inline, you need either chafa >= 1.10 or timg > 1.3.2" msgstr "Para renderizar inagénes en línea, necesita chafa >= 1.10 o timg > 1.3.2" #: ansicat.py:203 msgid "No cleaning required" msgstr "No se necesitó limpieza" #: ansicat.py:515 #, python-format msgid "%s is not a valid link for %s" msgstr "%s no es un enlace válido para %s" #: ansicat.py:601 #, python-format msgid "Urljoin Error: Could not make an URL out of %s and %s" msgstr "Error de Urljoin: No se pudo crear una URL a partir de %s y %s" #: ansicat.py:929 msgid "Error rendering Gopher " msgstr "Error renderizando Gopher " #: ansicat.py:1068 msgid "" "\n" "## Bookmarks Lists (updated during sync)\n" msgstr "" "\n" "## Lista de marcadores (se actualiza durante el 'sync')\n" #: ansicat.py:1071 msgid "" "\n" "## Subscriptions (new links in those are added to tour)\n" msgstr "" "\n" "## Subscripciones (nuevos enlaces en estas se añaden al tour)\n" #: ansicat.py:1074 msgid "" "\n" "## Frozen (fetched but never updated)\n" msgstr "" "\n" "## Congeladas (se descargan pero no se actualizan)\n" #: ansicat.py:1077 msgid "" "\n" "## System Lists\n" msgstr "" "\n" "## Listas del sistema\n" #: ansicat.py:1258 ansicat.py:1319 msgid "HTML document detected. Please install python-bs4 and python-readability." msgstr "Documento HTML detectado. Por favor, instale 'python-bs4' y 'python-readability'." #: ansicat.py:1741 msgid "" "\n" "> Please install python-bs4 to parse HTML" msgstr "" "\n" "> Por favor, instale 'python-bs4' para parsear HTML" #: ansicat.py:1743 msgid "" "\n" "> Picture not in cache. Please reload this page.\n" msgstr "" "\n" "> Imagen no cacheada. Por favor, recargue esta página (con \"reload\").\n" #: ansicat.py:1826 msgid "Cannot guess the mime type of the file. Please install \"file\"." msgstr "No se ha podido averiguar el tipo MIME del fichero. Por favor, instale \"file\"." #: ansicat.py:1940 #, python-format msgid "Could not render %s" msgstr "No se pudo renderizar %s" #: ansicat.py:1945 msgid "" "ansicat is a terminal rendering tool that will render multiple formats (HTML, Gemtext, " "RSS, Gophermap, Image) into ANSI text and colors.\n" " When used on a file, ansicat will try to autodetect the format. When used " "with standard input, the format must be manually specified.\n" " If the content contains links, the original URL of the content can be " "specified in order to correctly modify relatives links." msgstr "" "ansicat es una herramienta de renderizado para terminal que renderizará múltiples formatos " "(HTML, Gemtext, RSS, Gophermap, Imágenes) a texto ANSI y colores.\n" " Si se usa en un fichero, ansicat intentará autodetectar el formato. Si se usa con " "la entrada estándar, el formato debe especificarse manualmente.\n" " Si el contenido contiene enlaces, se puede especificar la URL original del " "contenido para modificar correctamente los enlaces relativos." #: ansicat.py:1966 msgid "Renderer to use. Available: auto, gemtext, html, feed, gopher, image, folder, plaintext" msgstr "" "Qué renderizador utilizar. Disponibles: auto, gemtext, html, feed, gopher, image, folder, plaintext" #: ansicat.py:1968 msgid "Mime of the content to parse" msgstr "MIME del contenido a parsear" #: ansicat.py:1972 msgid "Original URL of the content" msgstr "URL original del contenido" #: ansicat.py:1977 openk.py:371 opnk.py:371 msgid "" "Which mode should be used to render: normal (default), full or " "source. With HTML, the normal mode try to extract the article." msgstr "" "Qué modo se debe utilizar para renderizar: 'normal' (predeterminado), 'full' o " "'source'. Con HTML, el modo 'normal' intentará extraer el artículo." #: ansicat.py:1986 openk.py:380 opnk.py:380 msgid "Which mode should be used to render links: none (default) or end" msgstr "Qué modo se debe usar para renderizar enlaces: 'none' (predeterminado) o 'end'" #: ansicat.py:1994 msgid "Path to the text to render (default to stdin)" msgstr "Ruta al texto a renderizar (por defecto la entrada estándar)" #: ansicat.py:2017 msgid "Ansicat needs at least one file as an argument" msgstr "Ansicat necesita al menos un fichero como argumento" #: ansicat.py:2021 msgid "Format or mime should be specified when running with stdin" msgstr "Debe especificarse el formato o tipo mime si se utiliza con la entrada estándar" #: netcache.py:134 msgid "We return False because path is too long" msgstr "Devolvemos 'False' porque la ruta es muy larga" #: netcache.py:322 #, python-format msgid "" "ERROR while caching %s\n" "\n" msgstr "" "ERROR mientras se cacheaba %s\n" "\n" #: netcache.py:327 msgid "If you believe this error was temporary, type reload.\n" msgstr "Si piensa que este error fue temporal, escriba 'reload'.\n" #: netcache.py:328 msgid "The resource will be tentatively fetched during next sync.\n" msgstr "Se intentará descargar el recurso durante la próxima sincronización ('sync').\n" #: netcache.py:362 #, python-format msgid "Size of %s is %s Mo\n" msgstr "El tamaño de %s es %s Mb\n" #: netcache.py:363 #, python-format msgid "Offpunk only download automatically content under %s Mo\n" msgstr "Offpunk sólo descarga automáticamente contenido de menos de %s Mb\n" #: netcache.py:366 msgid "To retrieve this content anyway, type 'reload'." msgstr "Para descargar este contenido igualmente, escriba 'reload'." #: netcache.py:406 #, python-format msgid " -> Receiving stream: %s%% of allowed data" msgstr " -> Recibiendo flujo: %s%% de los datos permitidos" #: netcache.py:572 msgid "Certificate not valid until: {}!" msgstr "Certificado no válido hasta: {}!" #: netcache.py:576 msgid "Certificate expired as of: {})!" msgstr "Certificado expirado el: {})!" #: netcache.py:605 msgid "Hostname does not match certificate common name or any alternative names." msgstr "" "El nombre de host no coincide con el 'common name' del certificado ni con ninguno de los nombres " "alternativos." #: netcache.py:661 msgid "[SECURITY WARNING] Unrecognised certificate!" msgstr "[ALERTA DE SEGURIDAD] Certificado no reconocido!" #: netcache.py:663 msgid "The certificate presented for {} ({}) has never been seen before." msgstr "El certificado presentado para {} ({}) no había sido visto nunca antes." #: netcache.py:667 msgid "This MIGHT be a Man-in-the-Middle attack." msgstr "Esto PODRÍA ser un ataque Man-in-the-Middle." #: netcache.py:669 msgid "A different certificate has previously been seen {} times." msgstr "Un certificado diferente había sido visto previamente {} veces." #: netcache.py:675 msgid "That certificate has expired, which reduces suspicion somewhat." msgstr "Ese certificado ha expirado, lo que reduce de algún modo las sospechas." #: netcache.py:677 msgid "That certificate is still valid for: {}" msgstr "Ese certificado es todavía válido para: {}" #: netcache.py:679 msgid "Attempt to verify the new certificate fingerprint out-of-band:" msgstr "Intente verificar la huella del nuevo certificado fuera-de-banda:" #. TRANSLATORS: keep "Y/N" because the answer has to be one of those #: netcache.py:685 msgid "Accept this new certificate? Y/N " msgstr "¿Quiere aceptar este nuevo certificado? (Y/N) " #: netcache.py:692 msgid "TOFU Failure!" msgstr "Error de TOFU!" #: netcache.py:789 msgid "There are no certificates available for this site." msgstr "No hay certificados disponibles para este sitio." #. TRANSLATORS: keep the "y/n" #: netcache.py:791 msgid "Do you want to create one? (y/n) " msgstr "¿Quiere crear uno? (y/n) " #: netcache.py:793 msgid "Name for this certificate: " msgstr "Nombre para este certificado: " #: netcache.py:794 msgid "Validity in days: " msgstr "Validez en días: " #: netcache.py:801 msgid "The name or validity you typed are invalid" msgstr "El nombre o la validez introducidos no son válidos" #: netcache.py:806 msgid "The one available certificate for this site is:" msgstr "El único certificado disponible para este sitio es:" #: netcache.py:809 msgid "The {} available certificates for this site are:" msgstr "Los {} certificados disponibles para este sitio son:" #: netcache.py:818 msgid "which certificate do you want to use? > " msgstr "¿Qué certificado quiere usar? > " #: netcache.py:903 msgid "This identity doesn't exist for this site (or is disabled)." msgstr "Esta identidad no existe para este sitio (o está deshabilitada)." #: netcache.py:968 netcache.py:974 msgid "Received invalid header from server!" msgstr "¡Se recibió una cabecera no válida desde el servidor!" #: netcache.py:999 msgid "URL redirects to itself!" msgstr "¡La URL redirige a sí misma!" #: netcache.py:1001 msgid "Caught in redirect loop!" msgstr "¡Atrapados en un bucle de redirecciones!" #: netcache.py:1004 #, python-format msgid "Refusing to follow more than %d consecutive redirects!" msgstr "¡Rechazando seguir más de %d redirecciones consecutivas!" #: netcache.py:1035 msgid "You need to provide a client-certificate to access this page." msgstr "Necesita proporcionar un certificado cliente para acceder a esta página." #: netcache.py:1039 msgid "" "You need to provide a client-certificate to access this page.\r\n" "Type \"certs\" to create or re-use one" msgstr "" "Necesita proporcionar un certificado cliente para acceder a esta página.\r\n" "Escriba \"certs\" para crear o reutilizar uno" #: netcache.py:1043 #, python-format msgid "Server returned undefined status code %s!" msgstr "¡El servidor ha devuelto el código de estado indefinido %s!" #: netcache.py:1067 #, python-format msgid "" "Could not decode response body using %s encoding declared in header!" msgstr "" "¡No se ha podido descodificar el cuerpo de la respuesta usando la " "codificación %s declarada en la cabecera!" #: netcache.py:1103 msgid "Blocked URL: " msgstr "URL bloqueada: " #: netcache.py:1104 msgid "This website has been blocked with the following rule:\n" msgstr "Este sitio web ha sido bloqueado con la siguiente regla:\n" #: netcache.py:1106 msgid "Use the following redirect command to unblock it:\n" msgstr "Use el siguiente comando de 'redirect' para desbloquearlo:\n" #: netcache.py:1135 #, python-format msgid "%s is not a supported protocol" msgstr "%s no es un protocolo soportado" #: netcache.py:1143 msgid "HTTP requires python-requests" msgstr "HTTP requiere 'python-requests'" #: netcache.py:1162 msgid "ERROR: DNS error!" msgstr "¡ERROR: error de DNS!" #: netcache.py:1165 msgid "ERROR1: Connection refused!" msgstr "¡ERROR1: Conexión rechazada!" #: netcache.py:1168 msgid "ERROR2: Connection reset!" msgstr "¡ERROR2: Conexión reiniciada!" #: netcache.py:1171 msgid "" "ERROR3: Connection timed out!\n" " Slow internet connection? Use 'set timeout' to be more patient." msgstr "" "¡ERROR3: tiempo de conexión agotado!\n" " Conexión a internet lenta? Use 'set timeout' para ser más paciente." #: netcache.py:1175 msgid "" "ERROR5: Trying to create a directory which already exists\n" " in the cache : " msgstr "" "¡ERROR5: Intentando crear un directorio que ya existe\n" " en la caché : " #: netcache.py:1180 msgid "ERROR6: Bad SSL certificate:\n" msgstr "ERROR6: Certificado SSL incorrecto:\n" #: netcache.py:1183 msgid "" "\n" " If you know what you are doing, you can try to accept bad certificates with the following " "command:\n" msgstr "" "\n" "Si sabe lo que está haciendo, puede intentar aceptar certificados incorrectos con el siguiente " "comando:\n" #: netcache.py:1188 msgid "ERROR7: Cannot connect to URL:\n" msgstr "ERROR7: No se ha podido conectar a la URL:\n" #: netcache.py:1194 msgid "ERROR4: " msgstr "ERROR4: " #: netcache.py:1211 #, python-format msgid "Downloading %s" msgstr "Descargando %s" #: netcache.py:1233 msgid "" "Netcache is a command-line tool to retrieve, cache and access networked content.\n" " By default, netcache will returns a cached version of a given URL, downloading " "it only if a cache version doesn't exist. A validity duration, in seconds, can " "also be given so netcache downloads the content only if the existing cache is older " "than the validity." msgstr "" "Netcache es una herramienta de línea de comandos para descargar, cachear y acceder contenido en " "red.\n" " Por defecto, netcache devolverá una versión en caché de una URL dada, " "descargándola sólo si no existe una versión en caché. También se puede pasar una " "duración de validez, en segundos, para que netcache sólo descargue el contenido si " "la caché existente es más vieja que la validez." #: netcache.py:1242 msgid "return path to the cache instead of the content of the cache" msgstr "devolver la ruta a la caché en vez del contenido de la caché" #: netcache.py:1247 msgid "return a list of id's for the gemini-site instead of the content of the cache" msgstr "devolver una lista de id's para el sitio gemini en vez del contenido de la caché" #: netcache.py:1252 msgid "Do not attempt to download, return cached version or error" msgstr "No intentar descargar, devolver la versión en caché, o un error" #: netcache.py:1257 msgid "Cancel download of items above that size (value in Mb)." msgstr "Cancelar la descarga de elementos por encima de ese tamaño (valor en Mb)." #: netcache.py:1262 msgid "Time to wait before cancelling connection (in second)." msgstr "Tiempo de espera antes de cancelar la conexión (en segundos)." #: netcache.py:1268 openk.py:393 opnk.py:393 msgid "" "maximum age, in second, of the cached version before redownloading " "a new version" msgstr "" "edad máxima, en segundos, de la versión en caché antes de " "descargar una nueva versión" #: netcache.py:1276 msgid "download URL and returns the content or the path to a cached version" msgstr "descargar la URL y devolver el contenido o la ruta a una versión en caché" #: offpunk.py:66 msgid "Install xsel/xclip (X11) or wl-clipboard (Wayland) to use copy" msgstr "Instale 'xsel'/'xclip' (X11) o 'wl-clipboard' (Wayland) para usar 'copy'" #: offpunk.py:95 msgid "Install xsel/xclip (X11) or wl-clipboard (Wayland) to get URLs from your clipboard" msgstr "" "Instale 'xsel'/'xclip' (X11) o 'wl-clipboard' (Wayland) para obtener URLs desde su portapapeles" #: offpunk.py:149 msgid "You need to 'go' somewhere, first" msgstr "Necesita ir ('go') a algún sitio primero" #: offpunk.py:158 msgid "Error: " msgstr "Error: " #: offpunk.py:337 #, python-format msgid "We don’t handle name of URL: %s" msgstr "No manejamos el nombre de las URL: %s" #: offpunk.py:381 #, python-format msgid "%s not available, marked for syncing" msgstr "%s no disponible, marcado para sincronizar" #: offpunk.py:383 offpunk.py:1050 #, python-format msgid "%s already marked for syncing" msgstr "%s ya está marcado para sincronizar" #: offpunk.py:446 offpunk.py:1393 msgid "What?" msgstr "¿Cómo dice?" #: offpunk.py:450 msgid "No links to index" msgstr "No hay enlaces para indexar" #: offpunk.py:458 msgid "No page with links" msgstr "No hay página con enlaces" #: offpunk.py:466 #, python-format msgid "%s is redirected to %s" msgstr "%s se redirige a %s" #: offpunk.py:468 #, python-format msgid "Please add a destination to redirect %s" msgstr "Por favor, añada un destino para redirigir %s" #: offpunk.py:474 #, python-format msgid "Redirection for %s has been removed" msgstr "La redirección para %s se ha quitado" #: offpunk.py:476 #, python-format msgid "%s was not redirected. Nothing has changed." msgstr "No había redirecciones para %s. Nada ha cambiado." #: offpunk.py:479 #, python-format msgid "%s will now be blocked" msgstr "%s será bloqueado ahora" #: offpunk.py:482 #, python-format msgid "%s will now be redirected to %s" msgstr "%s ahora se redigirá a %s" #: offpunk.py:486 msgid "Current redirections:\n" msgstr "Redirecciones actuales:\n" #: offpunk.py:490 msgid "" "\n" "To add new, use \"redirect origine.com destination.org\"" msgstr "" "\n" "Para añadir una, use \"redirect origen.com destino.org\"" #: offpunk.py:491 msgid "" "\n" "To remove a redirect, use \"redirect origine.com NONE\"" msgstr "" "\n" "Para quitar una redirección, use \"redirect origen.com NONE\"" #: offpunk.py:493 msgid "" "\n" "To completely block a website, use \"redirect origine.com BLOCK\"" msgstr "" "\n" "Para bloquear complatemente un sitio web, use \"redirect origen.com BLOCK\"" #: offpunk.py:495 msgid "" "\n" "To block also subdomains, prefix with *: \"redirect *origine.com BLOCK\"" msgstr "" "\n" "Para bloquear también subdominios, use el prefijo *: \"redirect *origen.com BLOCK\"" #: offpunk.py:510 offpunk.py:515 #, python-format msgid "Unrecognised option %s" msgstr "Opción no reconocida: %s" #: offpunk.py:520 msgid "TLS mode must be `ca` or `tofu`!" msgstr "El modo TLS debe ser 'ca' o 'tofu'!" #: offpunk.py:524 msgid "Only high security certificates are now accepted" msgstr "Ahora se aceptan sólo certificados con alta seguridad" #: offpunk.py:526 msgid "Low security SSL certificates are now accepted" msgstr "Ahora se aceptan certificados con baja seguridad" #. TRANSLATORS keep accept_bad_ssl_certificates, True, and False #: offpunk.py:529 msgid "accept_bad_ssl_certificates should be True or False" msgstr "'accept_bad_ssl_certificates' debería ser 'True' o 'False'" #: offpunk.py:534 msgid "changing width to " msgstr "cambiando el ancho a " #: offpunk.py:537 #, python-format msgid "%s is not a valid width (integer required)" msgstr "%s no es un ancho válido (se require un número entero)" #: offpunk.py:540 msgid "Avaliable linkmode are `none` and `end`." msgstr "Los modos de enlaces ('linkmode') disponibles son 'none' y 'end'." #: offpunk.py:591 msgid "Available preset themes are: " msgstr "Temas predeterminados disponibles : " #: offpunk.py:607 #, python-format msgid "%s is not a valid preset theme" msgstr "%s no es un tema predeterminado válido" #: offpunk.py:609 #, python-format msgid "%s is not a valid theme element" msgstr "%s no es un elemento de 'theme' válido" #: offpunk.py:610 msgid "Valid theme elements are: " msgstr "Los elementos de 'theme' válidos son: " #: offpunk.py:622 #, python-format msgid "%s is set to %s" msgstr "%s está configurado a %s" #: offpunk.py:627 #, python-format msgid "%s reset (it was set to %s)" msgstr "%s reseteado (estaba configurado a %s)" #: offpunk.py:630 #, python-format msgid "%s is not set. Nothing to do" msgstr "%s no está seteado. No se hará nada" #: offpunk.py:635 #, python-format msgid "%s is not a valid color" msgstr "%s no es un color válido" #: offpunk.py:636 msgid "Valid colors are one of: " msgstr "Los colores válidos son: " #: offpunk.py:673 #, python-format msgid "No handler set for MIME type %s" msgstr "No hay un manejador configurado para el tipo MIME %s" #: offpunk.py:699 offpunk.py:707 #, python-format msgid "%s is a command and cannot be aliased" msgstr "%s es un comando y no se puede usar como alias" #: offpunk.py:701 #, python-format msgid "%s is currently aliased to \"%s\"" msgstr "%s tiene actualmente el alias \"%s\"" #: offpunk.py:703 #, python-format msgid "there’s no alias for \"%s\"" msgstr "no hay ningún alias para \"%s\"" #: offpunk.py:710 #, python-format msgid "%s has been aliased to \"%s\"" msgstr "%s es ahora un alias para \"%s\"" #: offpunk.py:716 msgid "Offline and undisturbed." msgstr "Offline y sin molestias." #: offpunk.py:720 msgid "Offpunk is now offline and will only access cached content" msgstr "Offpunk está ahora offline y sólo accederá a contenido en la caché" #: offpunk.py:727 msgid "Offpunk is online and will access the network" msgstr "Offpunk está online y accederá a la red" #: offpunk.py:729 msgid "Already online. Try offline." msgstr "Ya estamos online. Intente 'offline'." #: offpunk.py:768 msgid "No content to copy, visit a page first" msgstr "No hay contenido que copiar. Visite una página primero" #: offpunk.py:784 #, python-format msgid "We cannot share %s because it is local only" msgstr "No se puede compartir %s porque es sólo local" #: offpunk.py:796 msgid "TODO: sharing text is not yet implemented" msgstr "TODO: compartir texto todavía no está implementado" #: offpunk.py:813 offpunk.py:940 msgid "Nothing to share, visit a page first" msgstr "Nada que compartir, visite una página primero" #: offpunk.py:879 msgid "Multiple emails addresse were found:" msgstr "Se encontraron múltiples direcciones de correo electrónico :" #: offpunk.py:884 msgid "None of the above" msgstr "Ninguno de los anteriores" #: offpunk.py:886 msgid "Which email will you use to reply?" msgstr "¿Qué dirección de correo usará para responder?" #: offpunk.py:898 msgid "Enter the contact email for this page?" msgstr "¿Introducir la dirección de correo de contacto para esta página?" #: offpunk.py:907 msgid "Email address:" msgstr "Dirección de correo electrónico:" #: offpunk.py:908 msgid "Do you want to save this email as a contact for" msgstr "Quiere guardar esta dirección de correo como contacto para" #: offpunk.py:909 msgid "Current page only" msgstr "Sólo la página actual" #: offpunk.py:910 #, python-format msgid "The whole %s space" msgstr "El espacio %s completo" #: offpunk.py:911 msgid "Don’t save this email" msgstr "No guardar esta dirección" #: offpunk.py:913 msgid "Your choice?" msgstr "¿Su elección?" #: offpunk.py:931 #, python-format msgid "Email %s has been recorded as contact for %s" msgstr "La dirección %s se ha guardado como contacto para %s" #: offpunk.py:932 msgid "Nothing to save" msgstr "Nada que guardar" #: offpunk.py:935 msgid "In reply to " msgstr "En respuesta a " #: offpunk.py:938 #, python-format msgid "We cannot reply to %s because it is local only" msgstr "No se puede responder a %s porque es sólo local" #: offpunk.py:959 msgid "Too many arguments to list." msgstr "Demasiados argumentos para 'list'." #: offpunk.py:962 offpunk.py:984 msgid "URL required (or visit a page)." msgstr "URL requerida (o, visite una página)." #: offpunk.py:966 msgid "Cookies not enabled for url" msgstr "Cookies no habilitadas para esta url" #: offpunk.py:968 msgid "Cookies for url:" msgstr "Cookies para esta url:" #. TRANSLATORS domain, path, expiration time, name, value #: offpunk.py:971 #, python-format msgid "%s %s expires:%s %s=%s" msgstr "%s %s expira: %s %s=%s" #: offpunk.py:976 msgid "File parameter required for import." msgstr "Se requiere el parámetro fichero para importar." #: offpunk.py:981 msgid "Too many arguments to import" msgstr "Demasiados argumentos para 'import'" #: offpunk.py:991 msgid "File not found" msgstr "Fichero no encontrado" #: offpunk.py:993 msgid "Imported." msgstr "Importado." #: offpunk.py:995 msgid "Huh?" msgstr "Huh?" #: offpunk.py:1008 msgid "URLs in your clipboard\n" msgstr "URLs en su portapapeles\n" #: offpunk.py:1013 msgid "Where do you want to go today ?> " msgstr "¿A dónde quiere ir hoy? > " #: offpunk.py:1020 msgid "Go where? (hint: simply copy an URL in your clipboard)" msgstr "¿Ir a dónde? (pista: puede simplemente copiar una URL en su portapapeles)" #: offpunk.py:1039 #, python-format msgid "%s is not a valid URL to go" msgstr "%s no es una URL válida para 'go'" #: offpunk.py:1048 #, python-format msgid "%s marked for syncing" msgstr "%s marcado para sincronizar" #: offpunk.py:1071 msgid "Up only take integer as arguments" msgstr "'up' sólo acepta números como argumento" #: offpunk.py:1126 msgid "End of tour." msgstr "Fin del tour." #: offpunk.py:1146 #, python-format msgid "List %s does not exist. Cannot add it to tour" msgstr "La lista %s no existe. No se puede añadir al tour" #: offpunk.py:1173 #, python-format msgid "Invalid use of range syntax %s, skipping" msgstr "Uso de sintaxis de rango %s no válido, ignorando" #: offpunk.py:1175 offpunk.py:1542 #, python-format msgid "Non-numeric index %s, skipping." msgstr "El índice %s no es numérico, ignorando." #: offpunk.py:1177 offpunk.py:1544 #, python-format msgid "Invalid index %d, skipping." msgstr "El índice %d no es válido, ignorando." #: offpunk.py:1219 msgid "Invalid mark, must be one letter" msgstr "Marca no válida. Debe ser una letra" #. TRANSLATORS: this string and "Mime", "Cache", "Renderer" are formatted to align. #. if you can obtain the same effect in your language, try to do it ;) #. they are displayed with the "info" command #: offpunk.py:1230 msgid "URL : " msgstr "URL : " #: offpunk.py:1231 msgid "Mime : " msgstr "MIME : " #: offpunk.py:1232 msgid "Cache : " msgstr "Cache : " #: offpunk.py:1238 msgid "Renderer : " msgstr "Renderizador : " #: offpunk.py:1239 msgid "Cleaned with : " msgstr "Limpiado con : " #: offpunk.py:1245 msgid "Page appeard in following lists :\n" msgstr "La página aparece en las siguientes listas:\n" #: offpunk.py:1248 msgid "normal list" msgstr "lista normal" #: offpunk.py:1250 msgid "subscription" msgstr "subscripción" #: offpunk.py:1252 msgid "frozen list" msgstr "lista congelada" #: offpunk.py:1258 msgid "Page is not save in any list" msgstr "La página no está guardada en ninguna lista" #: offpunk.py:1276 msgid "System: " msgstr "Sistema: " #: offpunk.py:1277 msgid "Python: " msgstr "Python: " #: offpunk.py:1278 msgid "Language: " msgstr "Idioma: " #: offpunk.py:1279 msgid "" "\n" "Highly recommended:\n" msgstr "" "\n" "Altamente recomendado:\n" #: offpunk.py:1281 msgid "" "\n" "Web browsing:\n" msgstr "" "\n" "Navegación web:\n" #: offpunk.py:1288 msgid "" "\n" "Nice to have:\n" msgstr "" "\n" "Estaría bien tener:\n" #: offpunk.py:1295 msgid "" "\n" "Features :\n" msgstr "" "\n" "Características :\n" #: offpunk.py:1296 msgid " - Render images (chafa or timg) : " msgstr " - Renderizar imágenes (chafa o timg) : " #: offpunk.py:1299 msgid " - Render HTML (bs4, readability) : " msgstr " - Renderizar HTML (bs4, readability) : " #: offpunk.py:1302 msgid " - Render Atom/RSS feeds (feedparser) : " msgstr " - Renderizar feeds Atom/RSS (feedparser) : " #: offpunk.py:1305 msgid " - Connect to http/https (requests) : " msgstr " - Connectar a http/https (requests) : " #: offpunk.py:1308 msgid " - Detect text encoding (python-chardet) : " msgstr " - Detectar codificación del texto (python-chardet) : " #: offpunk.py:1311 msgid " - restore last position (less 572+) : " msgstr " - restaurar última posición (less 572+) : " #: offpunk.py:1315 msgid "ftr_site_config : " msgstr "ftr_site_config : " #: offpunk.py:1316 msgid "Config directory : " msgstr "Directorio de configuración : " #: offpunk.py:1317 msgid "User Data directory : " msgstr "Directorio de datos de usuario : " #: offpunk.py:1318 msgid "Cache directoy : " msgstr "Directorio de caché : " #: offpunk.py:1331 msgid "Found a bug in Offpunk? You can report it by email to the developers." msgstr "" "Ha encontrado un error en Offpunk? Puede informar a los desarrolladores por correo electrónico." #: offpunk.py:1333 msgid "Please describe your problem as clearly as possible:" msgstr "Por favor, describa su problema tan claramente como sea posible:" #: offpunk.py:1334 msgid "" "Include all the steps to reproduce the problem, including the URLs you are currently visiting." msgstr "" "Incluya todos los pasos para reproducir el problema, incluyendo las URLs que estaba visitando." #: offpunk.py:1335 offpunk.py:2223 msgid "Another point: always use \"reply-all\" when replying to this list." msgstr "Otro detalle: use siempre \"responder a todos\" cuando responda a esta lista." #: offpunk.py:1337 msgid "Describe the bug in one line: " msgstr "Describa el error en una línea: " #: offpunk.py:1342 msgid "No description of the bug, report cancelled" msgstr "No hay descripción del error, cancelamos el informe" #: offpunk.py:1388 msgid "Please enter the number of the XKCD comic you want to see" msgstr "Por favor, introduzca el número del cómic XKCD que quiere ver" #: offpunk.py:1456 msgid "Current page is already a feed" msgstr "La página actual ya es un feed" #: offpunk.py:1458 msgid "No feed found on current page" msgstr "No se ha encontrado un feed en la página actual" #: offpunk.py:1461 msgid "Available feeds :\n" msgstr "Feeds disponibles :\n" #: offpunk.py:1466 msgid "Which view do you want to see ? >" msgstr "¿Qué vista quiere ver? >" #. TRANSLATORS keep "view feed" and "feed" in english, those are literal commands #: offpunk.py:1490 msgid "view feed is deprecated. Use the command feed directly" msgstr "'view feed' está deprecado. Use el comando 'feed' directamente" #: offpunk.py:1499 #, python-format msgid "Link %s is: %s" msgstr "El enlace %s es: %s" #: offpunk.py:1507 msgid "Empty cached version" msgstr "Versión en caché vacía" #: offpunk.py:1508 #, python-format msgid "Last cached on %s" msgstr "Cacheado por última vez en %s" #: offpunk.py:1510 msgid "No cached version for this link" msgstr "No hay versión en caché para este enlace" #. TRANSLATORS keep "normal, full, switch, source" in english #: offpunk.py:1515 msgid "Valid arguments for view are : normal, full, switch, source or a number" msgstr "Los argumentos válidos para 'view' son : normal, full, switch, source, o un número" #: offpunk.py:1589 msgid "You cannot save if not cached!" msgstr "¡No puede guardar si no está en caché!" #: offpunk.py:1612 msgid "First argument is not a valid item index!" msgstr "¡El primer argumento no es un índice de elemento válido!" #: offpunk.py:1616 msgid "You must provide an index, a filename, or both." msgstr "Debe proporcionar un índice, un nombre de fichero, o ambos." #: offpunk.py:1625 msgid "Index too high!" msgstr "¡Índice demasiado alto!" #: offpunk.py:1636 #, python-format msgid "File %s already exists!" msgstr "¡El fichero %s ya existe!" #: offpunk.py:1643 #, python-format msgid "Can’t save %s because it’s a folder, not a file" msgstr "No se puede guardar %s porque es un directorio, no un fichero" #: offpunk.py:1645 #, python-format msgid "Saved to %s" msgstr "Guardado en %s" #: offpunk.py:1710 msgid "Subscriptions #subscribed (new links in those pages will be added to tour)" msgstr "Suscripciones #suscrito (nuevos enlaces en esas páginas se añadirán al tour)" #: offpunk.py:1712 msgid "Links requested and to be fetched during the next --sync" msgstr "Enlaces solicitados y que se descargarán durante el siguiente --sync" #: offpunk.py:1727 msgid "Multiple feeds have been found :\n" msgstr "Encontrados múltiples feeds :\n" #: offpunk.py:1729 msgid "This page is already a feed:\n" msgstr "Esta página ya es un feed:\n" #: offpunk.py:1731 msgid "No feed detected. You can still watch the page :\n" msgstr "Ningún feed detectado. Aún así, puede monitorizar la página :\n" #: offpunk.py:1742 #, python-format msgid "\t -> (already subscribed through lists %s)\n" msgstr "\t -> (ya suscrito a través de las listas %s)\n" #: offpunk.py:1745 msgid "Which feed do you want to subscribe ? > " msgstr "¿A qué feed quiere suscribirse? > " #: offpunk.py:1754 #, python-format msgid "Subscribed to %s" msgstr "Suscrito a %s" #: offpunk.py:1756 #, python-format msgid "You are already subscribed to %s" msgstr "Ya está suscrito a %s" #: offpunk.py:1758 msgid "No subscription registered" msgstr "No se ha registrado ninguna suscripción" #: offpunk.py:1767 msgid "bookmarks command takes a single integer argument!" msgstr "¡el comando 'bookmarks' sólo acepta un único argumento numérico!" #: offpunk.py:1782 offpunk.py:2029 #, python-format msgid "Removed from %s" msgstr "Quitado de %s" #: offpunk.py:1786 #, python-format msgid "Archiving: %s" msgstr "Archivando: %s" #: offpunk.py:1788 #, python-format msgid "Current maximum size of archives : %s" msgstr "Tamaño máximo actual de archivos : %s" #: offpunk.py:1811 offpunk.py:1952 offpunk.py:1971 #, python-format msgid "List %s does not exist. Create it with list create %s" msgstr "La lista %s no existe. Puede crearla escribiendo 'list create %s'" #: offpunk.py:1823 #, python-format msgid "%s already in %s." msgstr "%s ya está en %s." #: offpunk.py:1830 #, python-format msgid "%s has updated mode in %s to %s" msgstr "El modo de %s en la lista %s se ha actualizado a %s" #. TRANSLATORS parameters are url, list #: offpunk.py:1837 #, python-format msgid "%s added to %s" msgstr "%s añadido a %s" #: offpunk.py:1844 msgid ", archived on " msgstr ", archivado en " #: offpunk.py:1846 msgid ", visited on " msgstr ", visitado en " #. TRANSLATORS parameter is a "list" name #: offpunk.py:1849 #, python-format msgid ", added to %s on " msgstr ", añadido a %s en " #. TRANSLATORS keep 'go_to_line' as is #: offpunk.py:1958 msgid "go_to_line requires a number as parameter" msgstr "'go_to_line' requiere un número como parámetro" #: offpunk.py:1993 #, python-format msgid "%s is not allowed as a name for a list" msgstr "%s no es un nombre permitido para una lista" #: offpunk.py:2005 #, python-format msgid "list created. Display with `list %s`" msgstr "lista creada. Muéstrela con 'list %s'" #: offpunk.py:2007 #, python-format msgid "list %s already exists" msgstr "la lista %s ya existe" #: offpunk.py:2014 msgid "LIST argument is required as the target for your move" msgstr "El argumento LISTA es requerido como destino para 'move'" #: offpunk.py:2021 #, python-format msgid "%s is not a list, aborting the move" msgstr "%s no es una lista. Abortando el 'move'" #: offpunk.py:2080 #, python-format msgid "List %s has been marked as %s" msgstr "La lista %s se ha marcado como %s" #: offpunk.py:2082 #, python-format msgid "List %s is now a normal list" msgstr "La lista %s es ahora una lista normal" #: offpunk.py:2122 msgid "No lists yet. Use `list create`" msgstr "Todavía no hay listas. Use 'list create'" #: offpunk.py:2133 msgid "A name is required to create a new list. Use `list create NAME`" msgstr "Se requiere un nombre para crear una lista. Use 'list create NOMBRE'" #: offpunk.py:2154 msgid "Please set a valid editor with \"set editor\"" msgstr "Por favor, configure un editor válido con 'set editor'" #: offpunk.py:2156 msgid "A valid list name is required to edit a list" msgstr "Se requiere un nombre de lista válido para editar una lista" #: offpunk.py:2158 msgid "No valid editor has been found." msgstr "No se ha encontrado un editor válido." #: offpunk.py:2160 msgid "You can use the following command to set your favourite editor:" msgstr "Puede usar el siguiente comando para configurar su edito favorito:" #. TRANSLATORS keep 'set editor', it's a command #: offpunk.py:2163 msgid "set editor EDITOR" msgstr "set editor EDITOR" #: offpunk.py:2164 msgid "or use the $VISUAL or $EDITOR environment variables." msgstr "o use las variables de entorno $VISUAL o $EDITOR." #: offpunk.py:2168 #, python-format msgid "%s is a system list which cannot be deleted" msgstr "%s es una lista de sistema que no se puede borrar" #: offpunk.py:2171 #, python-format msgid "Are you sure you want to delete %s ?\n" msgstr "¿Está seguro de que quiere borrar %s?\n" #: offpunk.py:2174 #, python-format msgid "! %s items in the list will be lost !\n" msgstr "¡Se perderán %s elementos en la lista!\n" #: offpunk.py:2178 msgid "The list is empty, it should be safe to delete it.\n" msgstr "La lista está vacía, debería ser seguro borrarla.\n" #: offpunk.py:2181 #, python-format msgid "Type \"%s\" (in capital, without quotes) to confirm :" msgstr "Escriba \"%s\" (en mayúsculas, sin comillas) para confirmar :" #: offpunk.py:2188 #, python-format msgid "* * * %s has been deleted" msgstr "* * * %s se ha borrado" #: offpunk.py:2190 offpunk.py:2192 msgid "A valid list name is required to be deleted" msgstr "Se requiere un nombre de lista válido para borrar una lista" #: offpunk.py:2196 #, python-format msgid "You cannot modify %s which is a system list" msgstr "No puede modificar %s, que es una lista de sistema" #: offpunk.py:2206 #, python-format msgid "A valid list name is required after %s" msgstr "Se requiere un nombre de lista válido después de %s" #: offpunk.py:2217 msgid "Need help from a fellow human? Simply send an email to the offpunk-users list." msgstr "" "¿Necesita ayuda de de otro humano? No tiene más que enviar un email a la lista de offpunk-users." #: offpunk.py:2220 msgid "Describe your problem/question as clearly as possible." msgstr "Describa su problema/pregunta tan claramente como sea posible." #: offpunk.py:2221 msgid "Don’t forget to present yourself and why you would like to use Offpunk!" msgstr "No olvide presentarse y contarnos por qué le gusta usar Offpunk!" #: offpunk.py:2226 msgid "! is an alias for 'shell'" msgstr "! es un alias para 'shell'" #: offpunk.py:2228 msgid "? is an alias for 'help'" msgstr "? es un alias para 'help'" #: offpunk.py:2231 #, python-format msgid "%s is an alias for '%s'" msgstr "%s es un alias para '%s'" #: offpunk.py:2232 msgid "See the list of aliases with 'abbrevs'" msgstr "Vea la lista de aliases con 'abbrevs'" #: offpunk.py:2233 #, python-format msgid "'help %s':" msgstr "'help %s':" #: offpunk.py:2257 msgid "Sync can only be achieved online. Change status with `online`." msgstr "Sync sólo se puede hacer estando online. Cambie su estado con 'online'." #: offpunk.py:2262 msgid "sync argument should be the cache validity expressed in seconds" msgstr "el argumento de 'sync' debería ser la validez de la caché expresada en segundos" #: offpunk.py:2280 #, python-format msgid " -> adding to tour: %s" msgstr " -> añadiendo al tour: %s" #: offpunk.py:2306 #, python-format msgid "%s [%s/%s] Fetch " msgstr "%s [%s/%s] Fetch " #: offpunk.py:2359 #, python-format msgid " * * * %s to fetch in %s * * *" msgstr " * * * %s para descargar en %s * * *" #: offpunk.py:2413 msgid "End of sync" msgstr "Fin de la sincronización (sync)" #: offpunk.py:2420 msgid "You can close your screen!" msgstr "¡Puede cerrar su pantalla!" #: offpunk.py:2431 msgid "start with your list of bookmarks" msgstr "empezar con su lista de marcadores" #: offpunk.py:2437 msgid "Launch this command after startup" msgstr "Ejecutar este comando tras arrancar" #: offpunk.py:2442 msgid "use this particular config file instead of default" msgstr "usar este fichero de configuración en lugar del predeterminado" #: offpunk.py:2447 msgid "" "run non-interactively to build cache by exploring lists passed as " "argument. Without argument, all lists are fetched." msgstr "" "ejecutar de modo no interactivo para construir la caché explorando las listas " "pasadas como argumento. Sin argumentos, se descargan todas las " "listas." #: offpunk.py:2453 msgid "assume-yes when asked questions about certificates/redirections during sync (lower security)" msgstr "" "asumir 'si' cuando se hagan preguntas sobre certificados/redirecciones durante el 'sync' (menor " "seguridad)" #: offpunk.py:2458 msgid "do not try to get http(s) links (but already cached will be displayed)" msgstr "no intentar descargar enlaces http(s) (pero se mostrarán los que ya estén en caché)" #: offpunk.py:2463 msgid "run non-interactively with an URL as argument to fetch it later" msgstr "ejecutar de modo no interactivo con una URL como argumento para descargar después" #: offpunk.py:2467 msgid "depth of the cache to build. Default is 1. More is crazy. Use at your own risks!" msgstr "" "profundidad de la caché a construir. Por defecto es 1. Más es una locura. ¡Úselo bajo su " "responsabilidad y riesgo!" #: offpunk.py:2471 msgid "" "the mode to use to choose which images to download in a HTML page. one " "of (None, readable, full). Warning: full will slowdown your sync." msgstr "" "el modo a usar para escoger qué imágenes descargar en una página HTML. " "Uno de ('none', 'readable', 'full'). Atención: 'full' ralentizará la sincronización." #: offpunk.py:2476 msgid "duration for which a cache is valid before sync (seconds)" msgstr "duración para la cual la caché es válida antes de sincronizar (en segundos)" #: offpunk.py:2479 msgid "display version information and quit" msgstr "mostrar información de versión y salir" #: offpunk.py:2484 msgid "display available features and dependancies then quit" msgstr "mostrar características disponibles y salir" #: offpunk.py:2490 msgid "Arguments should be URL to be fetched or, if --sync is used, lists" msgstr "Los argumentos deberían ser la URL a descargar o, si se usa --sync, listas" #: offpunk.py:2504 msgid "Creating config directory {}" msgstr "Creando directorio de configuración {}" #: offpunk.py:2536 #, python-format msgid "%s is not a valid URL to fetch" msgstr "%s no es una URL válida para descargar" #: offpunk.py:2538 msgid "--fetch-later requires an URL (or a list of URLS) as argument" msgstr "--fetch-later requiere una URL (o una lista de URLS) como argumento" #: offpunk.py:2566 msgid "Welcome to Offpunk!" msgstr "¡Bienvenido a Offpunk!" #. TRANSLATORS keep 'help', it's a literal command #: offpunk.py:2568 msgid "Type `help` to get the list of available command." msgstr "Escriba 'help' para obtener la lista de comandos disponibles." #: offutils.py:153 #, python-format msgid "No XDG folder for %s. Check your code." msgstr "No hay carpeta XDG para %s. Revise su código." #: offutils.py:166 #, python-format msgid "Using config %s" msgstr "Usando la configuración %s" #: offutils.py:179 #, python-format msgid "Skipping startup command \"%s\" due to provided URL" msgstr "Ignorando el comando de arranque '%s' ya que se proporcionó una URL" #: offutils.py:388 #, python-format msgid "%s is not a valid email address" msgstr "%s no es una dirección de correo válida" #. TRANSLATORS please keep the 'Y/N' as is #: offutils.py:392 #, python-format msgid "Send an email to %s Y/N? " msgstr "Enviar un email a %s? (Y/N) " #: offutils.py:409 #, python-format msgid "Cannot find a mail client to send mail to %s" msgstr "No se ha podido encontrar un cliente de correo electrónico para enviar email a %s" #: offutils.py:410 openk.py:140 opnk.py:140 msgid "Please install xdg-open (usually from xdg-util package)" msgstr "Por favor, instale 'xdg-open' (normalmente en el paquete 'xdg-util')" #: openk.py:38 opnk.py:38 msgid "Please install the pager \"less\" to run Offpunk." msgstr "Por favor, instale el paginador 'less' para ejecutar Offpunk." #: openk.py:39 opnk.py:39 msgid "If you wish to use another pager, send me an email !" msgstr "Si desea utilizar un paginador distinto, ¡envíeme un email!" #: openk.py:41 opnk.py:41 msgid "(I’m really curious to hear about people not having \"less\" on their system.)" msgstr "(tengo verdadera curiosidad por saber de gente que no tiene 'less' en su sistema.)" #. TRANSLATORS: keep echo and %s, translate the text between "" #: openk.py:139 opnk.py:139 #, python-format msgid "echo \"Can’t find how to open \"%s" msgstr "echo \"No encuentro cómo abrir \"%s" #: openk.py:235 opnk.py:235 #, python-format msgid "%s does not exist" msgstr "%s no existe" #. TRANSLATORS translate only "MY_PREFERED_APP" #: openk.py:309 opnk.py:309 #, python-format msgid "\"handler %s MY_PREFERED_APP %%s\"" msgstr "\"handler %s MI_APLICACIÓN_PREFERIDA %%s\"" #: openk.py:314 opnk.py:314 #, python-format msgid "External open of type %s with \"%s\"" msgstr "Abrir externamente los elementos de tipo %s con '%s'" #: openk.py:315 openk.py:327 opnk.py:315 opnk.py:327 #, python-format msgid "You can change the default handler with %s" msgstr "Puede cambiar el manejador predeterminado con %s" #: openk.py:323 opnk.py:323 #, python-format msgid "Handler program %s not found!" msgstr "¡Programa manejador %s no encontrado!" #: openk.py:324 opnk.py:324 msgid "" "You can use the ! command to specify another handler program or pipeline." msgstr "" "Puede utilizar el comando '!' para especificar otro programa manejador o " "pipeline." #: openk.py:363 opnk.py:363 msgid "" "openk is an universal open command tool that will try to display any file in the " "pager less after rendering its content with ansicat. If that fails, openk will " "fallback to opening the file with xdg-open. If given an URL as input instead of a " "path, openk will rely on netcache to get the networked content." msgstr "" "openk es un comando de apertura universal que intentará mostrar cualquier fichero en " "el paginador 'less' después de renderizar su contenido con ansicat. Si eso falla, " "openk intentará abrir el fichero con 'xdg-open'. Si se le da una URL como entrada en " "vez de una ruta, openk hará uso de netcache para descargar el contenido de la red." #: openk.py:387 opnk.py:387 msgid "Path to the file or URL to open" msgstr "Ruta al fichero o URL a abrir" #: xkcdpunk.py:33 msgid "xkcdpunk is a tool to display a given XKCD comic in your terminal" msgstr "xkcd es una herramienta para mostrar un cómic de XKCD dado en su terminal" #: xkcdpunk.py:38 msgid "XKCD comic number" msgstr "Número de cómic de XKCD" #: xkcdpunk.py:41 msgid "Only access cached comics" msgstr "Acceder sólo a cómics en la caché" #~ msgid " (empty line to send)" #~ msgstr " (línea en blanco para enviar)" #~ msgid "" #~ "\n" #~ "> missing picture, please reload\n" #~ msgstr "" #~ "\n" #~ "> falta imagen, por favor recargue (con \"reload\")\n" offpunk-v3.0/po/extract_docstrings.py000077500000000000000000000010761514232770500201300ustar00rootroot00000000000000#!/usr/bin/env python3 import ast import sys f = open('../offpunk.py', "r") #filename input module = ast.parse(f.read()) class_definitions = [node for node in module.body if isinstance(node, ast.ClassDef)] for class_def in class_definitions: function_definitions = [node for node in class_def.body if isinstance(node, ast.FunctionDef)] for f in function_definitions: if f.name.startswith('do_'): docstring = ast.get_docstring(f) if docstring is not None: print('_(\n"""'+docstring+'\"""\n)') offpunk-v3.0/po/gl.po000066400000000000000000002147141514232770500146110ustar00rootroot00000000000000# Galician translations for Offpunk package. # Copyright (C) 2026 Offpunk's COPYRIGHT HOLDER # This file is distributed under the same license as the Offpunk package. # jmcs , 2026. # msgid "" msgstr "" "Project-Id-Version: offpunk 3.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-07 10:12+0100\n" "PO-Revision-Date: 2026-02-07 10:13+0100\n" "Last-Translator: jmcs \n" "Language-Team: Galician \n" "Language: gl_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.8\n" #: offpunk.py:3 msgid "" "\n" "Offline-First Gemini/Web/Gopher/RSS reader and browser\n" msgstr "" "\n" "Lector e navegador Gemini/Web/Gopher/RSS \"Offline Primeiro\"\n" #: offpunk.py:324 msgid "" "This method might be considered \"the heart of Offpunk\".\n" "Everything involved in fetching a gemini resource happens here:\n" "sending the request over the network, parsing the response,\n" "storing the response in a temporary file, choosing\n" "and calling a handler program, and updating the history.\n" "Nothing is returned." msgstr "" "Este método pode considerarse \"o corazón de Offpunk\".\n" "Todo o involucrado en descargar un recurso gemini ocorre aquí:\n" "enviar a petición a través da rede, parsear a resposta,\n" "almacenar a resposta nun ficheiro temporal, escoller\n" "e lanzar un programa manexador, e actualizar o histórico.\n" "Non devolve nada." #: offpunk.py:463 msgid "" "Display and manage the list of redirected URLs. This features is mostly useful to use privacy-" "friendly frontends for popular websites." msgstr "" "Mostrar e xestionar a lista de URLs redirixidas. Esta funcionalidade é maiormente útil para usar " "frontends para sitios webs populares que respetan a privacidade." #: offpunk.py:499 msgid "View or set various options." msgstr "Ver ou configurar varias opcións." #: offpunk.py:562 msgid "" "Change the colors of your rendered text.\n" "\n" "\"theme ELEMENT COLOR\"\n" "\n" "ELEMENT is one of: window_title, window_subtitle, title,\n" "subtitle,subsubtitle,link,oneline_link,new_link,image_link,preformatted,blockquote, " "blocked_link.\n" "\n" "COLOR is one or many (separated by space) of: bold, faint, italic, underline, black,\n" "red, green, yellow, blue, purple, cyan, white.\n" "\n" "Each color can alternatively be prefaced with \"bright_\".\n" "If color is \"none\", then that part of the theme is removed.\n" "\n" "theme can also be used with \"preset\" to load an existing theme.\n" "\n" "\"theme preset\" : show available themes\n" "\"theme preset PRESET_NAME\" : swith to a given preset" msgstr "" "Cambiar as cores do texto renderizado.\n" "\n" "\"theme ELEMENTO COR\"\n" "\n" "ELEMENTO é un destes: window_title, window_subtitle, title,\n" "subtitle,subsubtitle,link,oneline_link,new_link,image_link,preformatted,blockquote, blocked_link.\n" "\n" "COR é un ou máis (separados por espazo) destes: bold, faint, italic, underline, black,\n" "red, green, yellow, blue, purple, cyan, white.\n" "\n" "Cada cor pode, como alternativa, ir precedido de \"bright_\" (para facelo máis claro).\n" "se cor é \"none\", quitarase esa parte do \"theme\".\n" "\n" "theme tamén se pode usar con \"preset\" para cargar un tema existente.\n" "\n" "\"theme preset\" : amosar temas dispoñíbeis\n" "\"theme preset NOME_DO_PRESET\" : cambiar a o preset especificado" #: offpunk.py:651 msgid "" "View or set handler commands for different MIME types.\n" "handler MIMETYPE : see handler for MIMETYPE\n" "handler MIMETYPE CMD : set handler for MIMETYPE to CMD\n" "in the CMD, %s will be replaced by the filename.\n" "if no %s, it will be added at the end.\n" "MIMETYPE can be the true mimetype or the file extension.\n" "\n" "Examples: \n" " handler application/pdf zathura %s\n" " handler .odt lowriter\n" " handler docx lowriter" msgstr "" "Ver ou configurar comandos manexadores para distintos tipos MIME.\n" "handler TIPO_MIME : ver o manexador para o TIPO_MIME\n" "handler TIPO_MIME COMANDO : configurar COMANDO como manexador para TIPO_MIME\n" "no COMANDO, %s sustituirase polo nome do ficheiro.\n" "se non hai %s, engadirase ao final.\n" "TIPO_MIME pode ser un tipo mime real, ou unha extensión de ficheiro.\n" "\n" "Exemplos: \n" " handler application/pdf zathura %s\n" " handler .odt lowriter\n" " handler docx lowriter" #: offpunk.py:679 msgid "" "Create or modifiy an alias\n" "alias : show all existing aliases\n" "alias ALIAS : show the command linked to ALIAS\n" "alias ALIAS CMD : create or replace existing ALIAS to be linked to command CMD" msgstr "" "Crear ou modificar un alias\n" "alias : amosar os aliases existentes\n" "alias ALIAS : amosar o comando ligado a ALIAS\n" "alias ALIAS COMANDO : crear ou reemplazar un ALIAS existente para estar ligado ao comando COMANDO" #: offpunk.py:714 msgid "Use Offpunk offline by only accessing cached content" msgstr "Use Offpunk offline accedendo só a contido na caché" #: offpunk.py:723 msgid "Use Offpunk online with a direct connection" msgstr "Use Offpunk en liña cunha conexión directa" #: offpunk.py:732 msgid "" "Copy the content of the last visited page as gemtext/html in the clipboard.\n" "Use with \"url\" as argument to only copy the adress.\n" "Use with \"raw\" to copy ANSI content as seen in your terminal (with colour codes).\n" "Use with \"cache\" to copy the path of the cached content.\n" "Use with \"title\" to copy the title of the page.\n" "Use with \"link\" to copy a link in the gemtext format to that page with the title." msgstr "" "Copiar o contido da última páxina visitada en formato gemtext/html no portapapeis.\n" "Úseo con \"url\" como argumento para copiar só o enderezo.\n" "Úseo con \"raw\" para copiar contido ANSI tal como o ve no seu terminal (con códigos de cor).\n" "Úseo con \"cache\" para copiar a ruta ao contido en caché.\n" "Úseo con \"title\" para copiar o título da páxina.\n" "Úseo con \"link\" para copiar unha ligazón en formato gemtext á páxina, co título." #: offpunk.py:772 msgid "" "Send current page by email to someone else.\n" "Use with \"url\" as first argument to send only the address.\n" "Use with \"text\" as first argument to send the full content. TODO\n" "Without argument, \"url\" is assumed.\n" "Next arguments are the email adresses of the recipients.\n" "If no destination, you will need to fill it in your mail client." msgstr "" "Enviar a páxina actual por email a alguén.\n" "Úseo con \"url\" como primeiro argumento para enviar só o enderezo.\n" "Úseo con \"text\" como primeiro argumento para enviar o contido completo. PENDENTE\n" "Sen argumento, asúmese \"url\" .\n" "Os seguintes argumentos son o enderezo email do destinatario.\n" "Se non hai destinatario, terá que introducilo no seu cliente de correo." #: offpunk.py:817 msgid "" "Reply by email to a page by trying to find a good email for the author.\n" "If an email is provided as an argument, it will be used.\n" "arguments:\n" "- \"save\" : allows to detect and save email without actually sending an email.\n" "- \"save new@email\" : save a new reply email to replace an existing one" msgstr "" "Responder por email a unha páxina intentnado encontrar un email correcto para o autor.\n" "Se se inclúe un email como argumento, será o que se use.\n" "argumentos:\n" "- \"save\" : permite detectar e gardar o correo sen realmente enviar o email.\n" "- \"save novo@email\" : gardar un novo email de resposta para substituir a un existente" #: offpunk.py:944 msgid "" "Manipulate cookies:\n" "\"cookies import [url]\" - import cookies from file to be used with [url]\n" "\"cookies list [url]\" - list existing cookies for current url\n" "default is listing cookies for current domain.\n" "\n" "To get a cookie as a txt file,use the cookie-txt extension for Firefox." msgstr "" "Manipular cookies:\n" "\"cookies import [url]\" - importar cookies dende un ficheiro para ser utilizadas con " "[url]\n" "\"cookies list [url]\" - listar as cookies existentes para a url actual\n" "por defecto, lístanse as cookies para o dominio actual.\n" "\n" "Para obter unha coockie en formato ficheiro txt, utilice a extensión cookie-txt para Firefox." #: offpunk.py:999 msgid "Go to a gemini URL or marked item." msgstr "Ir a unha URL gemini ou a un elemento marcado." #: offpunk.py:1043 msgid "Reload the current URL." msgstr "Recargar a URL actual." #: offpunk.py:1058 msgid "" "Go up one directory in the path.\n" "Take an integer as argument to go up multiple times.\n" "Use \"~\" to go to the user root\"\n" "Use \"/\" to go to the server root." msgstr "" "Subir un directorio na ruta.\n" "Acepta un número como argumento para subir múltiples veces.\n" "Use \"~\" para ir á raíz do usuario\n" "Use \"/\" para ir á raíz do servidor." #: offpunk.py:1083 msgid "Go back to the previous gemini item." msgstr "Volver ao elemento gemini previo." #: offpunk.py:1092 msgid "Go forward to the next gemini item." msgstr "Avanzar ao seguinte elemento gemini." #: offpunk.py:1102 msgid "" "Go to the root of current capsule/gemlog/page\n" "If arg is \"/\", the go to the real root of the server" msgstr "" "Ir á raíz da páxina/cápsula/gemlog actual\n" "Se o argumento é \"/\", ir á raíz real do servidor" #: offpunk.py:1111 msgid "" "Add index items as waypoints on a tour, which is basically a FIFO\n" "queue of gemini items.\n" "\n" "`tour` or `t` alone brings you to the next item in your tour.\n" "Items can be added with `tour 1 2 3 4` or ranges like `tour 1-4`.\n" "All items in current menu can be added with `tour *`.\n" "All items in $LIST can be added with `tour $LIST`.\n" "Current item can be added back to the end of the tour with `tour .`.\n" "Current tour can be listed with `tour ls` and scrubbed with `tour clear`." msgstr "" "Engadir elementos de índice como puntos de referencia nun tour, que é\n" "basicamente unha lista FIFO de elementos gemini.\n" "\n" "`tour` ou `t` sen máis, levarao ao seguinte elemento no seu tour.\n" "Pódense engadir elementos con 'tour 1 2 3 4' ou rangos como 'tour 1-4'.\n" "Pódense engadir todos os elementos no menú actual con 'tour *'.\n" "Pódense engadir todos os elementos dunha $LISTA con 'tour $LISTA'.\n" "O elemento actual pódese engadir de novo ao final do tour con 'tour .'.\n" "Pódese listar o tour actual con 'tour ls' e vacialo con 'tour clear'." #: offpunk.py:1181 msgid "" "Manage your client certificates (identities) for a site.\n" "`certs` will display all valid certificates for the current site\n" "`certs new ` will create a new certificate, if no url is " "specified, the current open site will be used." msgstr "" "Xestionar os seus certificados de cliente (identidades) para un sitio.\n" "'certs' amosará todos os certificados válidos para o sitio actual\n" "'certs new ' creará un certificado novo. Se non se " "especifica unha url, usarase o sitio aberto actual." #: offpunk.py:1208 msgid "" "Mark the current item with a single letter. This letter can then\n" "be passed to the 'go' command to return to the current item later.\n" "Think of it like marks in vi: 'mark a'='ma' and 'go a'=''a'.\n" "Marks are temporary until shutdown (not saved to disk)." msgstr "" "Marcar o elemento actual cunha letra. Esta letra pode despois\n" "pasarse ao comando 'go' para voltar máis tarde ao elemento actual.\n" "Pense nas marcas como no editor vi: 'mark a'='ma' e 'go a'=''a'.\n" "As marcas son temporais ata cerrar offpunk (non se gardan no disco)." #: offpunk.py:1223 msgid "Display information about current page." msgstr "Amosar información da páxina actual." #: offpunk.py:1262 msgid "Display version and system information." msgstr "Amosar información de versión e sistema." #: offpunk.py:1326 msgid "" "Send a mail to the offpunk-devel list with technical informations\n" "about your offpunk version. You will be prompted to write an email\n" "describing how to reproduce the bug." msgstr "" "Enviar un correo electrónico á lista de correo offpunk-devel con información técnica\n" "acerca da súa versión de offpunk. Pediráselle que escriba un correo electrónico\n" "describindo como reproducir o erro." #: offpunk.py:1346 msgid "" "Search on Gemini using the engine configured (by default kennedy.gemi.dev)\n" "You can configure it using \"set search URL\".\n" "URL should contains one \"%s\" that will be replaced by the search term." msgstr "" "Buscar en Gemini usando o motor configurado (predeterminado, kennedy.gemi.dev)\n" "Pode configuralo usando \"set search URL\".\n" "URL debería conter un \"%s\" que se substituirá polo termo de busca." #: offpunk.py:1354 msgid "" "Search on the web using the engine configured (by default wiby.me)\n" "You can configure it using \"set websearch URL\".\n" "URL should contains one \"%s\" that will be replaced by the search term." msgstr "" "Buscar na web usando o motor configurado (predeterminado, wiby.me)\n" "Pode configuralo usando \"set websearch URL\".\n" "URL debería conter un \"%s\" que se substiruirá polo termo de busca." #: offpunk.py:1362 msgid "" "Search on wikipedia using the configured Gemini interface.\n" "The first word should be the two letters code for the language.\n" "Exemple : \"wikipedia en Gemini protocol\"\n" "But you can also use abbreviations to go faster:\n" "\"wen Gemini protocol\". (your abbreviation might be missing, report the bug)\n" "while it's not added, \"w\" is still an option you can use:\n" "\"w en Gemini protocol\" will work as a shortcut as well\n" "The interface used can be modified with the command:\n" "\"set wikipedia URL\" where URL should contains two \"%s\", the first\n" "one used for the language, the second for the search string." msgstr "" "Buscar na wikipedia usando a interface Gemini configurada.\n" "A primeira palabra debería ser o código de dúas letras do idioma.\n" "Exemplo : \"wikipedia gl protocolo Gemini\"\n" "Pero pode empregar abreviaturas para ir máis rápido:\n" "\"wgl protocolo Gemini\". (a abreviatura para o seu idioma pode faltar, informe do erro)\n" "mentras non se engada, tamén ten a opción de empregar \"w\":\n" "\"w gl protocolo Gemini\" tamén funciona como atallo\n" "A interface usada pode configurarse co commando:\n" "\"set wikipedia URL\", onde URL debe conter dous \"%s\", o primeiro\n" "usado para o idioma, e o segundo para a cadea de busca." #: offpunk.py:1383 msgid "Open the specified XKCD comics (a number is required as parameter)" msgstr "Abrir o cómic de XKCD especificado (requírese un número como parámetro)" #: offpunk.py:1391 msgid "Submit a search query to the geminispace.info search engine." msgstr "Enviar unha busca ao motor de busca geminispace.info." #: offpunk.py:1399 msgid "Display history." msgstr "Amosar o historial." #: offpunk.py:1404 msgid "Find in current page by displaying only relevant lines (grep)." msgstr "Buscar na páxina actual mostrando só as liñas relevantes (grep)." #: offpunk.py:1408 msgid "" "Display all the links for the current page.\n" " If argument N is provided, then page through N links at a time.\n" " \"links 10\" show you the first 10 links, then 11 to 20, etc.\n" " if N = 0, then all the links are displayed" msgstr "" "Amosar todas as ligazóns para a páxina actual.\n" "Si se introduce un argumento N, paxinarase amosando N ligazóns de cada vez.\n" "\"links 10\" amosará as primeiras 10 ligazóns, despois da 11 á 20, etc.\n" "se N é 0, amosaranse todas as ligazóns" #: offpunk.py:1429 msgid "DEPRECATED: List contents of current index." msgstr "DEPRECADO: Lista contidos do índice actual." #: offpunk.py:1435 msgid "Default action when line is empty" msgstr "Acción predeterminada se a liña está baleira" #: offpunk.py:1451 msgid "Display RSS or Atom feeds linked to the current page." msgstr "Amosar feeds RSS ou Atom ligados á páxina actual." #: offpunk.py:1476 msgid "" "Run most recently visited item through \"less\" command, restoring previous position.\n" "Use \"view normal\" to see the default article view on html page.\n" "Use \"view full\" to see a complete html page instead of the article view.\n" "Use \"view swich\" to switch between normal and full\n" "Use \"view XX\" where XX is a number to view information about link XX.\n" "(full, feed, feeds have no effect on non-html content)." msgstr "" "Pasar o elemnto visitado máis recente polo paxinador \"less\", restaurando a posición previa.\n" "Use \"view normal\" para ver a vista de artigo predeterminada nunha páxina html.\n" "Use \"view full\" para ver a páxina html completa en vez da vista de artigo.\n" "Use \"view swich\" para alternar entre 'normal' e 'full'\n" "Use \"view XX\" where XX é un número para ver información acerca da ligazón XX.\n" "('full', 'feed', 'feeds' non ten efecto en contido non-html)." #: offpunk.py:1522 msgid "" "Open current item with the configured handler or xdg-open.\n" "Use \"open url\" to open current URL in a browser.\n" "Use \"open 2 4\" to open links 2 and 4\n" "You can combine with \"open url 2 4\" to open URL of links\n" "see \"handler\" command to set your handler." msgstr "" "Abrir o elemento actual co manexador configurado, ou 'xdg-open'\n" "Use \"open url\" para abrir a URL actual nun navegador.\n" "Use \"open 2 4\" para abrir as ligazóns 2 e 4\n" "Pódese combinar con \"open url 2 4\" para abrir a URL das ligazóns\n" "vexa o comando \"handler\" para configurar o seu manexador." #: offpunk.py:1557 msgid "" "Send the content of the current page to the shell and pipe it.\n" "You are supposed to write what will come after the pipe. For example,\n" "if you want to count the number of lines containing STRING in the \n" "current page:\n" "> shell grep STRING|wc -l\n" "'!' is an useful shortcut.\n" "> !grep STRING|wc -l" msgstr "" "Enviar o contido da páxina actual ao intérprete de ordes e 'pipealo'.\n" "Debe escribir o que irá despois da 'pipe'. Por exemplo,\n" "se quere contar o número de liñas que conteñen CADEA na\n" "páxina actual:\n" "> shell grep CADEA | wc -l\n" "'!' é un atallo útil.\n" "> !grep CADEA | wc -l" #: offpunk.py:1578 msgid "" "Save an item to the filesystem.\n" "'save n filename' saves menu item n to the specified filename.\n" "'save filename' saves the last viewed item to the specified filename.\n" "'save n' saves menu item n to an automagic filename." msgstr "" "Gardar un alemento no sistema de ficheiros.\n" "'save n nome_ficheiro' garda o elemento de menú 'n' ao nome de ficheiro especificado.\n" "'save nome_ficheiro' garda o último elemento visto ao nome de ficheiro especificado.\n" "'save n' garda o elemento de menú 'n' a un nome de ficheiro 'automáxico'." #: offpunk.py:1654 msgid "" "Print the url of the current page.\n" "Use \"url XX\" where XX is a number to print the url of link XX.\n" "\"url\" can also be piped to the shell, using the pipe \"|\"." msgstr "" "Imprimir a url da páxina actual.\n" "Use \"url XX\", onde XX é un número, para imprimir a url do link XX.\n" "\"url\" tamén se pode 'pipear' ao intérprete de ordes, usando o 'pipe' \"|\"." #: offpunk.py:1676 msgid "" "Add the current URL to the list specified as argument.\n" "If no argument given, URL is added to Bookmarks.\n" "You can pass a link number as the second argument to add the link.\n" "\"add $LIST XX\" will add link number XX to $LIST" msgstr "" "Engadir a URL actual á lista especificada como argumento.\n" "Se non se pasa un argumento, URL engadirase a Bookmarks.\n" "Pode pasar un número de ligazón como segundo argumento para engadir a ligazón.\n" "\"add $LISTA XX\" engadirá a ligazón número XX á lista $LISTA" #: offpunk.py:1721 msgid "" "Subscribe to current page by saving it in the \"subscribed\" list.\n" "If a new link is found in the page during a --sync, the new link is automatically\n" "fetched and added to your next tour.\n" "To unsubscribe, remove the page from the \"subscribed\" list." msgstr "" "Subscribirse á páxina actual gardándoa na lista \"subscribed\".\n" "Se se atopa unha nova ligazón na páxina durante un --sync, a nova ligazón descargarase\n" "automaticamente e engadirase ao seu próximo tour.\n" "Para de-subscribirse, quite a páxina da lista \"subscribed\"." #: offpunk.py:1761 msgid "" "Show or access the bookmarks menu.\n" "'bookmarks' shows all bookmarks.\n" "'bookmarks n' navigates immediately to item n in the bookmark menu.\n" "Bookmarks are stored using the 'add' command." msgstr "" "Amosar ou acceder ao menú de marcadores.\n" "'bookmarks' amosa todos os marcadores.\n" "'bookmarks n' navega inmediatamente ao elemento n no ménú de marcadores.\n" "Os marcadores gárdanse usando o comando 'add'." #: offpunk.py:1775 msgid "" "Archive current page by removing it from every list and adding it to\n" "archives, which is a special historical list limited in size. It is similar to `move archives`." msgstr "" "Arquivar a páxina actual borrándoa de todas as listas e engadíndoa á lista\n" "'archives', que é unha lista histórica especial limitada en tamaño. É similar a 'move archives'." #: offpunk.py:2010 msgid "" "move LIST will add the current page to the list LIST.\n" "With a major twist: current page will be removed from all other lists.\n" "If current page was not in a list, this command is similar to `add LIST`." msgstr "" "'move LISTA' engadirá a páxina actual á lista LISTA.\n" "Con unha diferenza importante:a páxina actual quitarase de todas as outras listas.\n" "Se a páxina actual non estaba nunha lista, este comando é similar a 'add LISTA'." #: offpunk.py:2091 msgid "" "Manage list of bookmarked pages.\n" "- list : display available lists\n" "- list $LIST : display pages in $LIST\n" "- list create $NEWLIST : create a new list\n" "- list edit $LIST : edit the list\n" "- list subscribe $LIST : during sync, add new links found in listed pages to tour\n" "- list freeze $LIST : don’t update pages in list during sync if a cache already exists\n" "- list normal $LIST : update pages in list during sync but don’t add anything to tour\n" "- list delete $LIST : delete a list permanently (a confirmation is required)\n" "- list help : print this help\n" "See also :\n" "- add $LIST (to add current page to $LIST or, by default, to bookmarks)\n" "- move $LIST (to add current page to list while removing from all others)\n" "- archive (to remove current page from all lists while adding to archives)\n" "\n" "There’s no \"delete\" on purpose. The use of \"archive\" is recommended.\n" "\n" "The following lists cannot be removed or frozen but can be edited with \"list edit\"\n" "- list archives : contains last 200 archived URLs\n" "- history : contains last 200 visisted URLs\n" "- to_fetch : contains URLs that will be fetch during the next sync\n" "- tour : contains the next URLs to visit during a tour (see \"help tour\")" msgstr "" "Xestionar a lista de páxinas en marcadores.\n" "- list : amosar as listas dispoñíbeis\n" "- list $LISTA : amosar as páxinas que hai na lista $LISTA\n" "- list create $NOVALISTA : crear unha nova lista\n" "- list edit $LISTA : editar a lista\n" "- list subscribe $LISTA : durante o sync, engadir novas ligazóns atopadas nas páxinas da lista ao " "tour\n" "- list freeze $LISTA : non actualizar as páxinas nesta lista durante o sync se xa existe unha " "caché\n" "- list normal $LISTA : actualizar as páxinas na lista durante o sync pero non engadir nada ao " "tour\n" "- list delete $LISTA : borrar permanentemente unha lista (require confirmación)\n" "- list help : imprimir esta axuda\n" "Vexa tamén :\n" "- add $LISTA (para engadir a páxina actual a $LISTA ou, por defecto, a bookmarks)\n" "- move $LISTA (para engadir a páxina actual á $LISTA e quitala de todas as demais)\n" "- archive (para quitar a páxina actual de todas as listas e engadila a 'archives')\n" "\n" "Non hai \"delete\" a propósito. Recoméndase o uso de \"archive\".\n" "\n" "As seguintes listas non se poden quitar nen \"conxelar\" ('freeze') pero pódense editar con \"list " "edit\"\n" "- list archives : contén as últimas 200 URLSs arquivadas\n" "- history : contén as últimas 200 URLSs visitadas\n" "- to_fetch : contén as URLs que se descargarán duranto o próximo sync\n" "- tour : contén as pŕoximas URLs a visitar durante o tour (vexa \"help tour\")" #: offpunk.py:2215 msgid "ALARM! Recursion detected! ALARM! Prepare to eject!" msgstr "ALARMA! Recursión detectada! ALARMA! Prepárese para evacuar!" #: offpunk.py:2242 msgid "Access the offpunk.net tutorial (online)" msgstr "Acceder ao tutorial en offpunk.net (en liña)" #: offpunk.py:2246 msgid "" "Synchronize all bookmarks lists and URLs from the to_fetch list.\n" "- New elements in pages in subscribed lists will be added to tour\n" "- Elements in list to_fetch will be retrieved and added to tour\n" "- Normal lists will be synchronized and updated\n" "- Frozen lists will be fetched only if not present.\n" "\n" "Before a sync, you can edit the list of URLs that will be fetched with the\n" "following command: \"list edit to_fetch\"\n" "\n" "Argument : duration of cache validity (in seconds)." msgstr "" "Sincronizar todas as listas de marcadores e URLs da lista to_fetch.\n" "- Novos elementos en páxinas en listas subscritas engadiranse ao tour\n" "- Elementos na lista to_fetch descargaranse e engadiranse ao tour\n" "- As listas normais sincronizaranse e actualizaranse\n" "- As listas 'conxeladas' (frozen) descargaranse só se non están presentes.\n" "\n" "Antes dun sync, pode editar a lista de URLs que se descargarán co\n" "seguinte comando: \"list edit to_fetch\"\n" "\n" "Argumento : a duración da validez da caché (en segundos)." #: offpunk.py:2418 msgid "Exit Offpunk." msgstr "Sair de Offpunk." #: ansicat.py:60 msgid "To improve your web experience (less cruft in webpages)," msgstr "Para mellorar a súa experiencia web (menos morralla nas páxinas web)," #: ansicat.py:61 msgid "please install python3-readability or readability-lxml" msgstr "por favor instale python3-readability ou readability-lxml" #: ansicat.py:100 msgid "To render images inline, you need either chafa >= 1.10 or timg > 1.3.2" msgstr "Para renderizar imaxes en liña, precísase ou chafa >= 1.10, ou timg > 1.3.2" #: ansicat.py:203 msgid "No cleaning required" msgstr "Non se requeriu limpeza" #: ansicat.py:515 #, python-format msgid "%s is not a valid link for %s" msgstr "%s non é unha ligazón válida para %s" #: ansicat.py:601 #, python-format msgid "Urljoin Error: Could not make an URL out of %s and %s" msgstr "Erro de Urljoin: Non se puido construir unha URL a partir de %s e máis %s" #: ansicat.py:929 msgid "Error rendering Gopher " msgstr "Erro ao renderizar Gopher " #: ansicat.py:1068 msgid "" "\n" "## Bookmarks Lists (updated during sync)\n" msgstr "" "\n" "## Listas de marcadores (actualízanse durante o 'sync')\n" #: ansicat.py:1071 msgid "" "\n" "## Subscriptions (new links in those are added to tour)\n" msgstr "" "\n" "##Subscripcións (ligazóns novas nestas páxinas engadiranse ao tour)\n" #: ansicat.py:1074 msgid "" "\n" "## Frozen (fetched but never updated)\n" msgstr "" "\n" "## Conxeladas (descárganse pero non se actualizan)\n" #: ansicat.py:1077 msgid "" "\n" "## System Lists\n" msgstr "" "\n" "## Listas do sistema\n" #: ansicat.py:1258 ansicat.py:1319 msgid "HTML document detected. Please install python-bs4 and python-readability." msgstr "Documento HTML detectado. Por favor, instale python-bs4 e python-readability." #: ansicat.py:1741 msgid "" "\n" "> Please install python-bs4 to parse HTML" msgstr "" "\n" "> Por favor, instale python-bs4 para parsear HTML" #: ansicat.py:1743 msgid "" "\n" "> Picture not in cache. Please reload this page.\n" msgstr "" "\n" "> A imaxe non está en caché. Por favor, recargue esta páxina. ('reload')\n" #: ansicat.py:1826 msgid "Cannot guess the mime type of the file. Please install \"file\"." msgstr "Non se pode averiguar o tipo MIME do ficheiro. Por favor, instale \\\"file\\\"." #: ansicat.py:1940 #, python-format msgid "Could not render %s" msgstr "Non se puido renderizar %s" #: ansicat.py:1945 msgid "" "ansicat is a terminal rendering tool that will render multiple formats (HTML, Gemtext, " "RSS, Gophermap, Image) into ANSI text and colors.\n" " When used on a file, ansicat will try to autodetect the format. When used " "with standard input, the format must be manually specified.\n" " If the content contains links, the original URL of the content can be " "specified in order to correctly modify relatives links." msgstr "" "ansicat é unha ferramenta de renderizado para a terminal que pode renderizar múltiples " "formatos (HTML,Gemtext, RSS, Gophermap, Imaxe) a texto e cores ANSI.\n" " Cando se use nun ficheiro, ansicat intentará autodetectar o formato. Cando se " "use na entrada estándar, o formato deberá ser especificado manualmente.\n" " Si o contido contén ligazóns, pódese especificar a URL orixinal do contido " "para así modificar correctamente as ligazóns relativas." #: ansicat.py:1966 msgid "Renderer to use. Available: auto, gemtext, html, feed, gopher, image, folder, plaintext" msgstr "" "Renderizador a empregar. Dispoñíbeis: auto, gemtext, html, feed, gopher, image, folder, plaintext" #: ansicat.py:1968 msgid "Mime of the content to parse" msgstr "MIME do contido a parsear" #: ansicat.py:1972 msgid "Original URL of the content" msgstr "URL orixinal do contido" #: ansicat.py:1977 openk.py:371 opnk.py:371 msgid "" "Which mode should be used to render: normal (default), full or " "source. With HTML, the normal mode try to extract the article." msgstr "" "Que modo se debe usar para renderizar: normal (por defecto), full ou " "source. Con HTML, o modo normal intentará extraer o artigo." #: ansicat.py:1986 openk.py:380 opnk.py:380 msgid "Which mode should be used to render links: none (default) or end" msgstr "Que modo se debería usar para amosar ligazóns: 'none' (predeterminado) ou 'end'" #: ansicat.py:1994 msgid "Path to the text to render (default to stdin)" msgstr "Ruta ao texto a renderizar (por defecto, a entrada estándar)" #: ansicat.py:2017 msgid "Ansicat needs at least one file as an argument" msgstr "Ansicat precisa cando menos un ficheiro como argumento" #: ansicat.py:2021 msgid "Format or mime should be specified when running with stdin" msgstr "Débese especificar formato ou MIME cando se executa coa entrada estándar" #: netcache.py:134 msgid "We return False because path is too long" msgstr "Retornamos False porque a ruta é demasiado longa" #: netcache.py:322 #, python-format msgid "" "ERROR while caching %s\n" "\n" msgstr "" "ERRO mentras se cacheaba %s\n" "\n" #: netcache.py:327 msgid "If you believe this error was temporary, type reload.\n" msgstr "Se pensa que este erro foi temporal, escriba 'reload'.\n" #: netcache.py:328 msgid "The resource will be tentatively fetched during next sync.\n" msgstr "Intentarase descargar o recurso no seguinte sync.\n" #: netcache.py:362 #, python-format msgid "Size of %s is %s Mo\n" msgstr "O tamaño de %s é %s Mb\n" #: netcache.py:363 #, python-format msgid "Offpunk only download automatically content under %s Mo\n" msgstr "Offpunk só descargará automaticamente contido de menos de %s Mb\n" #: netcache.py:366 msgid "To retrieve this content anyway, type 'reload'." msgstr "Para descargar este contido igualmente, escriba 'reload'." #: netcache.py:406 #, python-format msgid " -> Receiving stream: %s%% of allowed data" msgstr " -> Recibindo fluxo: %s%% dos datos permitidos" #: netcache.py:572 msgid "Certificate not valid until: {}!" msgstr "O certificado non é válido ata: {}!" #: netcache.py:576 msgid "Certificate expired as of: {})!" msgstr "O certificado expirou no: {})!" #: netcache.py:605 msgid "Hostname does not match certificate common name or any alternative names." msgstr "O hostname non coincide co common name nen ningún dos names alternativos do certificado." #: netcache.py:661 msgid "[SECURITY WARNING] Unrecognised certificate!" msgstr "[AVISO DE SEGURIDADE] certificado non recoñecido!" #: netcache.py:663 msgid "The certificate presented for {} ({}) has never been seen before." msgstr "É a primeira vez que se ve o certificado presentado para {} ({})." #: netcache.py:667 msgid "This MIGHT be a Man-in-the-Middle attack." msgstr "Isto PODERÍA ser un ataque Man-in-the-Middle." #: netcache.py:669 msgid "A different certificate has previously been seen {} times." msgstr "Un certificado diferente veuse anteriormente {} veces." #: netcache.py:675 msgid "That certificate has expired, which reduces suspicion somewhat." msgstr "Ese certificado expirou, o que reduce en certa maneira a sospeita." #: netcache.py:677 msgid "That certificate is still valid for: {}" msgstr "O certificado aínda é válido para: {}" #: netcache.py:679 msgid "Attempt to verify the new certificate fingerprint out-of-band:" msgstr "Intente verificar a pegada do novo certificado por outra canle:" #. TRANSLATORS: keep "Y/N" because the answer has to be one of those #: netcache.py:685 msgid "Accept this new certificate? Y/N " msgstr "Aceptar este novo certificado? Y/N " #: netcache.py:692 msgid "TOFU Failure!" msgstr "Erro de TOFU!" #: netcache.py:789 msgid "There are no certificates available for this site." msgstr "Non hai certificados dispoñíbeis para este sitio." #. TRANSLATORS: keep the "y/n" #: netcache.py:791 msgid "Do you want to create one? (y/n) " msgstr "Quere crear un novo? (y/n) " #: netcache.py:793 msgid "Name for this certificate: " msgstr "Nome para este certificado: " #: netcache.py:794 msgid "Validity in days: " msgstr "Validez en días: " #: netcache.py:801 msgid "The name or validity you typed are invalid" msgstr "O nome ou validez que escribiu non son válidos" #: netcache.py:806 msgid "The one available certificate for this site is:" msgstr "O único certificado dispoñibel para este sitio é:" #: netcache.py:809 msgid "The {} available certificates for this site are:" msgstr "Os {} certificados dispoñibeis para este sitio son:" #: netcache.py:818 msgid "which certificate do you want to use? > " msgstr "que certificado quere empregar? > " #: netcache.py:903 msgid "This identity doesn't exist for this site (or is disabled)." msgstr "Esta identidade non existe para este sitio (ou está deshabilitada)." #: netcache.py:968 netcache.py:974 msgid "Received invalid header from server!" msgstr "Recibiuse unha cabeceira non válida do servidor!" #: netcache.py:999 msgid "URL redirects to itself!" msgstr "A URL redirixe a sí mesma!" #: netcache.py:1001 msgid "Caught in redirect loop!" msgstr "Atrapados nun bucle de redirección!" #: netcache.py:1004 #, python-format msgid "Refusing to follow more than %d consecutive redirects!" msgstr "Rexeitando seguir máis de %d redireccións consecutivas!" #: netcache.py:1035 msgid "You need to provide a client-certificate to access this page." msgstr "Precisa empregar un certificado de cliente para acceder a esta páxina." #: netcache.py:1039 msgid "" "You need to provide a client-certificate to access this page.\r\n" "Type \"certs\" to create or re-use one" msgstr "" "Precisa empregar un certificado de cliente para acceder a esta páxina.\r\n" "Escriba \"certs\" para crear ou seleccionar un" #: netcache.py:1043 #, python-format msgid "Server returned undefined status code %s!" msgstr "O servidor devolveu un código de estado non definido: %s!" #: netcache.py:1067 #, python-format msgid "" "Could not decode response body using %s encoding declared in header!" msgstr "" "Non se puido descodificar o corpo da resposta usando a " "codificación %s declarada na cabeceira!" #: netcache.py:1103 msgid "Blocked URL: " msgstr "URL bloqueada: " #: netcache.py:1104 msgid "This website has been blocked with the following rule:\n" msgstr "Este sitio web foi bloqueado segundo esta regra:\n" #: netcache.py:1106 msgid "Use the following redirect command to unblock it:\n" msgstr "Empregue a seguinte orde de redirect para desbloquealo:\n" #: netcache.py:1135 #, python-format msgid "%s is not a supported protocol" msgstr "%s non é un protocolo soportado" #: netcache.py:1143 msgid "HTTP requires python-requests" msgstr "HTTP require python-requests" #: netcache.py:1162 msgid "ERROR: DNS error!" msgstr "ERRO: erro de DNS!" #: netcache.py:1165 msgid "ERROR1: Connection refused!" msgstr "ERROR1: Conexión rexeitada!" #: netcache.py:1168 msgid "ERROR2: Connection reset!" msgstr "ERROR2: Conexión reseteada!" #: netcache.py:1171 msgid "" "ERROR3: Connection timed out!\n" " Slow internet connection? Use 'set timeout' to be more patient." msgstr "" "ERROR3: Esgotouse o tempo da conexión!\n" " Ten unha conexión a internet lenta? Use 'set timeout' para ser máis paciente." #: netcache.py:1175 msgid "" "ERROR5: Trying to create a directory which already exists\n" " in the cache : " msgstr "" "ERROR5: Intentando crear un directorio que xa existe\n" " na caché : " #: netcache.py:1180 msgid "ERROR6: Bad SSL certificate:\n" msgstr "ERROR6: Certificado SSL incorrecto:\n" #: netcache.py:1183 msgid "" "\n" " If you know what you are doing, you can try to accept bad certificates with the following " "command:\n" msgstr "" "\n" "Se sabe o que está facendo, pode intentar aceptar certificados incorrectos coa seguinte orde:\n" #: netcache.py:1188 msgid "ERROR7: Cannot connect to URL:\n" msgstr "ERRO7: Non se pode conectar coa URL:\n" #: netcache.py:1194 msgid "ERROR4: " msgstr "ERROR4: " #: netcache.py:1211 #, python-format msgid "Downloading %s" msgstr "Descargando %s" #: netcache.py:1233 msgid "" "Netcache is a command-line tool to retrieve, cache and access networked content.\n" " By default, netcache will returns a cached version of a given URL, downloading " "it only if a cache version doesn't exist. A validity duration, in seconds, can " "also be given so netcache downloads the content only if the existing cache is older " "than the validity." msgstr "" "Netcache é unha ferramenta para a liña de ordes para descargar, cachear e acceder a contido da " "rede.\n" " Por defecto, netcache devolverá unha versión en caché dunha URL, " "descargándoa unicamente se non hai unha versión en caché. Tamén se pode especificar " "unha validez, en segundos, para que netcache só descargue o contido se a caché " "existente é máis vella que a validez especificada." #: netcache.py:1242 msgid "return path to the cache instead of the content of the cache" msgstr "devolve a ruta á caché en lugar do contido da caché" #: netcache.py:1247 msgid "return a list of id's for the gemini-site instead of the content of the cache" msgstr "devolve unha lista de id's para o sitio gemini no lugar do contido da caché" #: netcache.py:1252 msgid "Do not attempt to download, return cached version or error" msgstr "Non intentar descargar, devolver a versión da caché ou un erro" #: netcache.py:1257 msgid "Cancel download of items above that size (value in Mb)." msgstr "Cancelar a descarga de elementos maiores que ese tamaño (valor en Mb)." #: netcache.py:1262 msgid "Time to wait before cancelling connection (in second)." msgstr "Tempo a esperar antes de cancelar a conexión (en segundos)." #: netcache.py:1268 openk.py:393 opnk.py:393 msgid "" "maximum age, in second, of the cached version before redownloading " "a new version" msgstr "" "idade máxima, en segundos, da versión en caché antes de descargar " "unha nova versión" #: netcache.py:1276 msgid "download URL and returns the content or the path to a cached version" msgstr "descarga a URL e devolve o contido ou a ruta á versión en caché" #: offpunk.py:66 msgid "Install xsel/xclip (X11) or wl-clipboard (Wayland) to use copy" msgstr "Instale xsel/xclip (X11) ou wl-clipboard (Wayland) para usar 'copy'" #: offpunk.py:95 msgid "Install xsel/xclip (X11) or wl-clipboard (Wayland) to get URLs from your clipboard" msgstr "Instale xsel/xclip (X11) ou wl-clipboard (Wayland) para obter URLs dende o seu portapapeis" #: offpunk.py:149 msgid "You need to 'go' somewhere, first" msgstr "É preciso ir a algún sitio primeiro (con 'go')" #: offpunk.py:158 msgid "Error: " msgstr "Erro: " #: offpunk.py:337 #, python-format msgid "We don’t handle name of URL: %s" msgstr "Non manexamos nome da URL: %s" #: offpunk.py:381 #, python-format msgid "%s not available, marked for syncing" msgstr "%s non dispoñible, foi marcado para sincronizar (sync)" #: offpunk.py:383 offpunk.py:1050 #, python-format msgid "%s already marked for syncing" msgstr "%s xa está marcado para sincronizar ('sync')" #: offpunk.py:446 offpunk.py:1393 msgid "What?" msgstr "Como di?" #: offpunk.py:450 msgid "No links to index" msgstr "Non hai ligazóns para indexar" #: offpunk.py:458 msgid "No page with links" msgstr "Non hai páxina con ligazóns" #: offpunk.py:466 #, python-format msgid "%s is redirected to %s" msgstr "%s rediríxese a %s" #: offpunk.py:468 #, python-format msgid "Please add a destination to redirect %s" msgstr "Por favor, engada un destino para redirixir %s" #: offpunk.py:474 #, python-format msgid "Redirection for %s has been removed" msgstr "Quitouse a redirección para %s" #: offpunk.py:476 #, python-format msgid "%s was not redirected. Nothing has changed." msgstr "Non se redirixiu %s. Non se cambiou nada." #: offpunk.py:479 #, python-format msgid "%s will now be blocked" msgstr "Agora bloquearase %s" #: offpunk.py:482 #, python-format msgid "%s will now be redirected to %s" msgstr "%s vaise redirixir a %s" #: offpunk.py:486 msgid "Current redirections:\n" msgstr "Redireccións actuais:\n" #: offpunk.py:490 msgid "" "\n" "To add new, use \"redirect origine.com destination.org\"" msgstr "" "\n" "Para engadir unha nova, use \"redirect orixe.com destino.org\"" #: offpunk.py:491 msgid "" "\n" "To remove a redirect, use \"redirect origine.com NONE\"" msgstr "" "\n" "Para quitar unha redirección, use \"redirect orixe.com NONE\"" #: offpunk.py:493 msgid "" "\n" "To completely block a website, use \"redirect origine.com BLOCK\"" msgstr "" "\n" "Para bloquear un sitio web por completo, use \"redirect orixe.com BLOCK\"" #: offpunk.py:495 msgid "" "\n" "To block also subdomains, prefix with *: \"redirect *origine.com BLOCK\"" msgstr "" "\n" "Para bloquear tamén subdominios, use * diante: \"redirect *orixe.com BLOCK\"" #: offpunk.py:510 offpunk.py:515 #, python-format msgid "Unrecognised option %s" msgstr "Opción non recoñecida: %s" #: offpunk.py:520 msgid "TLS mode must be `ca` or `tofu`!" msgstr "O modo TLS debe ser `ca` ou `tofu`!" #: offpunk.py:524 msgid "Only high security certificates are now accepted" msgstr "Agora só se aceptarán certificados de alta seguridade" #: offpunk.py:526 msgid "Low security SSL certificates are now accepted" msgstr "Agora aceptaranse certificados SSL de baixa seguridade" #. TRANSLATORS keep accept_bad_ssl_certificates, True, and False #: offpunk.py:529 msgid "accept_bad_ssl_certificates should be True or False" msgstr "accept_bad_ssl_certificates debería ser True ou False" #: offpunk.py:534 msgid "changing width to " msgstr "cambiando o ancho a " #: offpunk.py:537 #, python-format msgid "%s is not a valid width (integer required)" msgstr "%s non é un ancho válido (require un número enteiro)" #: offpunk.py:540 msgid "Avaliable linkmode are `none` and `end`." msgstr "Os linkmodes dispoñíbeis son `none` e `end`." #: offpunk.py:591 msgid "Available preset themes are: " msgstr "Os temas predefinidos dispoñíbeis son: " #: offpunk.py:607 #, python-format msgid "%s is not a valid preset theme" msgstr "%s non é un tema predefinido válido" #: offpunk.py:609 #, python-format msgid "%s is not a valid theme element" msgstr "%s non é un elemento de 'theme' válido" #: offpunk.py:610 msgid "Valid theme elements are: " msgstr "Os elementos de 'theme' válidos son: " #: offpunk.py:622 #, python-format msgid "%s is set to %s" msgstr "%s está configurado a %s" #: offpunk.py:627 #, python-format msgid "%s reset (it was set to %s)" msgstr "%s reseteado (estaba configurado a %s)" #: offpunk.py:630 #, python-format msgid "%s is not set. Nothing to do" msgstr "%s non está configurado. Non se fará nada" #: offpunk.py:635 #, python-format msgid "%s is not a valid color" msgstr "%s non é unha cor válida" #: offpunk.py:636 msgid "Valid colors are one of: " msgstr "As cores válidas son: " #: offpunk.py:673 #, python-format msgid "No handler set for MIME type %s" msgstr "Non hai manexador configurado para o tipo MIME %s" #: offpunk.py:699 offpunk.py:707 #, python-format msgid "%s is a command and cannot be aliased" msgstr "%s é unha orde e non se pode usar como alias" #: offpunk.py:701 #, python-format msgid "%s is currently aliased to \"%s\"" msgstr "%s ten actualmente o alias \"%s\"" #: offpunk.py:703 #, python-format msgid "there’s no alias for \"%s\"" msgstr "non hai ningún alias para \"%s\"" #: offpunk.py:710 #, python-format msgid "%s has been aliased to \"%s\"" msgstr "%s é agora un alias para \"%s\"" #: offpunk.py:716 msgid "Offline and undisturbed." msgstr "Offline e tranquilo." #: offpunk.py:720 msgid "Offpunk is now offline and will only access cached content" msgstr "Offpunk está agora offline e só accederá a contido na caché" #: offpunk.py:727 msgid "Offpunk is online and will access the network" msgstr "Offpunk está agora en liña e accederá á rede" #: offpunk.py:729 msgid "Already online. Try offline." msgstr "Xa estamos online. Probe con 'offline'." #: offpunk.py:768 msgid "No content to copy, visit a page first" msgstr "Non hai contido que copiar, visite unha páxina primeiro" #: offpunk.py:784 #, python-format msgid "We cannot share %s because it is local only" msgstr "Non se pode compartir %s porque é só local" #: offpunk.py:796 msgid "TODO: sharing text is not yet implemented" msgstr "PENDENTE: compartir textos açinda non está implementado" #: offpunk.py:813 offpunk.py:940 msgid "Nothing to share, visit a page first" msgstr "Non hai nada que compartir , visite unha páxina primeiro" #: offpunk.py:879 msgid "Multiple emails addresse were found:" msgstr "Atopáronse varios enderezos de correo:" #: offpunk.py:884 msgid "None of the above" msgstr "Ningún dos anteriores" #: offpunk.py:886 msgid "Which email will you use to reply?" msgstr "Qué correo empregará para responder?" #: offpunk.py:898 msgid "Enter the contact email for this page?" msgstr "Introducir o correo de contacto para esta páxina?" #: offpunk.py:907 msgid "Email address:" msgstr "Enderezo de correo:" #: offpunk.py:908 msgid "Do you want to save this email as a contact for" msgstr "Quere gardar este enderezo como contacto para" #: offpunk.py:909 msgid "Current page only" msgstr "A páxina actual soamente" #: offpunk.py:910 #, python-format msgid "The whole %s space" msgstr "O espazo %s completo" #: offpunk.py:911 msgid "Don’t save this email" msgstr "Non gardar este enderezo" #: offpunk.py:913 msgid "Your choice?" msgstr "A súa decisión?" #: offpunk.py:931 #, python-format msgid "Email %s has been recorded as contact for %s" msgstr "O enderezo %s gardouse como contacto para %s" #: offpunk.py:932 msgid "Nothing to save" msgstr "Nada que gardar" #: offpunk.py:935 msgid "In reply to " msgstr "En resposta a " #: offpunk.py:938 #, python-format msgid "We cannot reply to %s because it is local only" msgstr "Non se pode responder a %s porque é só local" #: offpunk.py:959 msgid "Too many arguments to list." msgstr "Demasiados argumentos para a lista." #: offpunk.py:962 offpunk.py:984 msgid "URL required (or visit a page)." msgstr "Requírese unha URL (ou, visite unha páxina)." #: offpunk.py:966 msgid "Cookies not enabled for url" msgstr "Cookies non habilitadas para esta url" #: offpunk.py:968 msgid "Cookies for url:" msgstr "Cookies para a url:" #. TRANSLATORS domain, path, expiration time, name, value #: offpunk.py:971 #, python-format msgid "%s %s expires:%s %s=%s" msgstr "%s %s expira:%s %s=%s" #: offpunk.py:976 msgid "File parameter required for import." msgstr "Requírese o parámetro ficheiro para importar." #: offpunk.py:981 msgid "Too many arguments to import" msgstr "Demasiados argumentos para importar" #: offpunk.py:991 msgid "File not found" msgstr "Ficheiro non atopado" #: offpunk.py:993 msgid "Imported." msgstr "Importado." #: offpunk.py:995 msgid "Huh?" msgstr "Como?" #: offpunk.py:1008 msgid "URLs in your clipboard\n" msgstr "URLs no seu portapapeis\n" #: offpunk.py:1013 msgid "Where do you want to go today ?> " msgstr "Onde quere ir hoxe? > " #: offpunk.py:1020 msgid "Go where? (hint: simply copy an URL in your clipboard)" msgstr "Onde quere ir con 'go' ? (pista: pode simplemente copiar unha URL no portapapeis)" #: offpunk.py:1039 #, python-format msgid "%s is not a valid URL to go" msgstr "%s non é unha URL válida para ir con 'go'" #: offpunk.py:1048 #, python-format msgid "%s marked for syncing" msgstr "%s marcado para sincronizar ('sync')" #: offpunk.py:1071 msgid "Up only take integer as arguments" msgstr "'up' só acepta números enteiros como argumento" #: offpunk.py:1126 msgid "End of tour." msgstr "Fin do Tour." #: offpunk.py:1146 #, python-format msgid "List %s does not exist. Cannot add it to tour" msgstr "A lista %s non existe. Non se pode engadir ao tour" #: offpunk.py:1173 #, python-format msgid "Invalid use of range syntax %s, skipping" msgstr "O uso da sintaxe de rango %s non é válida, ignorando" #: offpunk.py:1175 offpunk.py:1542 #, python-format msgid "Non-numeric index %s, skipping." msgstr "O índice %s non é numérico, ignorando." #: offpunk.py:1177 offpunk.py:1544 #, python-format msgid "Invalid index %d, skipping." msgstr "O índice %d non é válido, ignorando." #: offpunk.py:1219 msgid "Invalid mark, must be one letter" msgstr "A marca non é válida, debe ser unha letra" #. TRANSLATORS: this string and "Mime", "Cache", "Renderer" are formatted to align. #. if you can obtain the same effect in your language, try to do it ;) #. they are displayed with the "info" command #: offpunk.py:1230 msgid "URL : " msgstr "URL : " #: offpunk.py:1231 msgid "Mime : " msgstr "Mime : " #: offpunk.py:1232 msgid "Cache : " msgstr "Cache : " #: offpunk.py:1238 msgid "Renderer : " msgstr "Renderizador : " #: offpunk.py:1239 msgid "Cleaned with : " msgstr "Limpado con : " #: offpunk.py:1245 msgid "Page appeard in following lists :\n" msgstr "A páxina aparece nas seguintes listas:\n" #: offpunk.py:1248 msgid "normal list" msgstr "lista normal" #: offpunk.py:1250 msgid "subscription" msgstr "subscripción" #: offpunk.py:1252 msgid "frozen list" msgstr "lista conxelada" #: offpunk.py:1258 msgid "Page is not save in any list" msgstr "A páxina non está gardada en ningunha lista" #: offpunk.py:1276 msgid "System: " msgstr "Sistema: " #: offpunk.py:1277 msgid "Python: " msgstr "Python: " #: offpunk.py:1278 msgid "Language: " msgstr "Idioma: " #: offpunk.py:1279 msgid "" "\n" "Highly recommended:\n" msgstr "" "\n" "Altamente recomendado:\n" #: offpunk.py:1281 msgid "" "\n" "Web browsing:\n" msgstr "" "\n" "Navegación web:\n" #: offpunk.py:1288 msgid "" "\n" "Nice to have:\n" msgstr "" "\n" "Sería bo ter:\n" #: offpunk.py:1295 msgid "" "\n" "Features :\n" msgstr "" "\n" "Características :\n" #: offpunk.py:1296 msgid " - Render images (chafa or timg) : " msgstr " - Renderizar imaxes (chafa ou timg) : " #: offpunk.py:1299 msgid " - Render HTML (bs4, readability) : " msgstr " - Renderizar HTML (bs4, readability) : " #: offpunk.py:1302 msgid " - Render Atom/RSS feeds (feedparser) : " msgstr " - Renderizar feeds Atom/RSS (feedparser) : " #: offpunk.py:1305 msgid " - Connect to http/https (requests) : " msgstr " - Connectar con http/https (requests) : " #: offpunk.py:1308 msgid " - Detect text encoding (python-chardet) : " msgstr " - Detectar codificación de texto (python-chardet) : " #: offpunk.py:1311 msgid " - restore last position (less 572+) : " msgstr " - restaurar a última posición (less 572+) : " #: offpunk.py:1315 msgid "ftr_site_config : " msgstr "ftr_site_config : " #: offpunk.py:1316 msgid "Config directory : " msgstr "Directorio de configuración : " #: offpunk.py:1317 msgid "User Data directory : " msgstr "Directorio de datos de usuario: " #: offpunk.py:1318 msgid "Cache directoy : " msgstr "Directorio de caché: " #: offpunk.py:1331 msgid "Found a bug in Offpunk? You can report it by email to the developers." msgstr "Atopou un erro en Offpunk? Pode informar aos desenvolvedores por correo electrónico." #: offpunk.py:1333 msgid "Please describe your problem as clearly as possible:" msgstr "Por favor, describa o seu problema tan claramente como sexa posíbel:" #: offpunk.py:1334 msgid "" "Include all the steps to reproduce the problem, including the URLs you are currently visiting." msgstr "Inclúa todos os pasos para reproducir o problema, incluindo as URLs que está visitando." #: offpunk.py:1335 offpunk.py:2223 msgid "Another point: always use \"reply-all\" when replying to this list." msgstr "Outro detalle: use sempre \"responder a todos\" cando responda a esta lista." #: offpunk.py:1337 msgid "Describe the bug in one line: " msgstr "Describa o erro nunha liña: " #: offpunk.py:1342 msgid "No description of the bug, report cancelled" msgstr "Non hai descrición para o erro, cancelamos o informe" #: offpunk.py:1388 msgid "Please enter the number of the XKCD comic you want to see" msgstr "Por favor, introduza o número do comic XKCD que quere ver" #: offpunk.py:1456 msgid "Current page is already a feed" msgstr "A páxina actual xa é un feed" #: offpunk.py:1458 msgid "No feed found on current page" msgstr "Non se atopou ningún feed na páxina actual" #: offpunk.py:1461 msgid "Available feeds :\n" msgstr "Feeds dispoñíbeis: \n" #: offpunk.py:1466 msgid "Which view do you want to see ? >" msgstr "Qué vista quere ver? >" #. TRANSLATORS keep "view feed" and "feed" in english, those are literal commands #: offpunk.py:1490 msgid "view feed is deprecated. Use the command feed directly" msgstr "'view feed' está deprecado. Use a orde 'feed' directamente" #: offpunk.py:1499 #, python-format msgid "Link %s is: %s" msgstr "A ligazón %s é: %s" #: offpunk.py:1507 msgid "Empty cached version" msgstr "Varsión na caché baleira" #: offpunk.py:1508 #, python-format msgid "Last cached on %s" msgstr "Cacheado por última vez en %s" #: offpunk.py:1510 msgid "No cached version for this link" msgstr "Non hai versión en caché para esta ligazón" #. TRANSLATORS keep "normal, full, switch, source" in english #: offpunk.py:1515 msgid "Valid arguments for view are : normal, full, switch, source or a number" msgstr "Os argumentos válidos para 'view' son: 'normal', 'full', 'switch', 'source', ou un número" #: offpunk.py:1589 msgid "You cannot save if not cached!" msgstr "Non se pode gardar se non está en caché!" #: offpunk.py:1612 msgid "First argument is not a valid item index!" msgstr "O primeiro argumento non é un índice de elemento válido!" #: offpunk.py:1616 msgid "You must provide an index, a filename, or both." msgstr "Precisa introducir un índice, un nome de ficheiro, ou ambos." #: offpunk.py:1625 msgid "Index too high!" msgstr "O índice é moi alto!" #: offpunk.py:1636 #, python-format msgid "File %s already exists!" msgstr "O ficheiro %s xa existe!" #: offpunk.py:1643 #, python-format msgid "Can’t save %s because it’s a folder, not a file" msgstr "Non se pode gardar %s porque é un directorio, non un ficheiro" #: offpunk.py:1645 #, python-format msgid "Saved to %s" msgstr "Gardado en %s" #: offpunk.py:1710 msgid "Subscriptions #subscribed (new links in those pages will be added to tour)" msgstr "Subscripcións #subscribed (ligazóns novas nestas páxinas engadiranse ao tour)" #: offpunk.py:1712 msgid "Links requested and to be fetched during the next --sync" msgstr "Ligazóns solicitadas e que se descargarán durante a seguinte sincronización (--sync)" #: offpunk.py:1727 msgid "Multiple feeds have been found :\n" msgstr "Atopáronse múltiple feeds: \n" #: offpunk.py:1729 msgid "This page is already a feed:\n" msgstr "Esta páxina xa é un feed:\n" #: offpunk.py:1731 msgid "No feed detected. You can still watch the page :\n" msgstr "Non se detectou ningún feed. Aínda así, pode monitorizar a páxina:\n" #: offpunk.py:1742 #, python-format msgid "\t -> (already subscribed through lists %s)\n" msgstr "\t -> (xa está subscrito via as listas %s)\n" #: offpunk.py:1745 msgid "Which feed do you want to subscribe ? > " msgstr "A que feed se quere subscribir? > " #: offpunk.py:1754 #, python-format msgid "Subscribed to %s" msgstr "Subscrito a %s" #: offpunk.py:1756 #, python-format msgid "You are already subscribed to %s" msgstr "Xa está subscrito a %s" #: offpunk.py:1758 msgid "No subscription registered" msgstr "Non se rexistrou ningunha subscripción" #: offpunk.py:1767 msgid "bookmarks command takes a single integer argument!" msgstr "a orde 'bookmarks' só acepta un único argumento numérico!" #: offpunk.py:1782 offpunk.py:2029 #, python-format msgid "Removed from %s" msgstr "Quitado de %s" #: offpunk.py:1786 #, python-format msgid "Archiving: %s" msgstr "Arquivando: %s" #: offpunk.py:1788 #, python-format msgid "Current maximum size of archives : %s" msgstr "Tamaño máximo actual dos arquivos : %s" #: offpunk.py:1811 offpunk.py:1952 offpunk.py:1971 #, python-format msgid "List %s does not exist. Create it with list create %s" msgstr "A lista %s non existe. Pode creala coa orde 'list create %s'" #: offpunk.py:1823 #, python-format msgid "%s already in %s." msgstr "%s xa está en %s." #: offpunk.py:1830 #, python-format msgid "%s has updated mode in %s to %s" msgstr "O modo de %s na lista %s actualizouse a %s" #. TRANSLATORS parameters are url, list #: offpunk.py:1837 #, python-format msgid "%s added to %s" msgstr "%s engadido a %s" #: offpunk.py:1844 msgid ", archived on " msgstr ", arquivado en " #: offpunk.py:1846 msgid ", visited on " msgstr ", visitado en " #. TRANSLATORS parameter is a "list" name #: offpunk.py:1849 #, python-format msgid ", added to %s on " msgstr ", engadido a %s no " #. TRANSLATORS keep 'go_to_line' as is #: offpunk.py:1958 msgid "go_to_line requires a number as parameter" msgstr "'go_to_line' require un número como parámetro" #: offpunk.py:1993 #, python-format msgid "%s is not allowed as a name for a list" msgstr "%s non é un nome permitido para unha lista" #: offpunk.py:2005 #, python-format msgid "list created. Display with `list %s`" msgstr "lista creada. Móstrea coa orde `list %s`" #: offpunk.py:2007 #, python-format msgid "list %s already exists" msgstr "a lista %s xa existe" #: offpunk.py:2014 msgid "LIST argument is required as the target for your move" msgstr "O argumento LISTA é requerido como destino do 'move'" #: offpunk.py:2021 #, python-format msgid "%s is not a list, aborting the move" msgstr "%s non é unha lista, abortando o 'move'" #: offpunk.py:2080 #, python-format msgid "List %s has been marked as %s" msgstr "A lista %s marcouse como %s" #: offpunk.py:2082 #, python-format msgid "List %s is now a normal list" msgstr "A lista %s é agora unha lista normal" #: offpunk.py:2122 msgid "No lists yet. Use `list create`" msgstr "Aínda non hai ningunha lista. Use `list create`" #: offpunk.py:2133 msgid "A name is required to create a new list. Use `list create NAME`" msgstr "Requírese un nome para crear unha nova lista. Use `list create NAME`" #: offpunk.py:2154 msgid "Please set a valid editor with \"set editor\"" msgstr "Por favor, configure un editor válido con \"set editor\"" #: offpunk.py:2156 msgid "A valid list name is required to edit a list" msgstr "Requírese un nome de lista válido para editar unha lista" #: offpunk.py:2158 msgid "No valid editor has been found." msgstr "Non se atopou ningún editor válido." #: offpunk.py:2160 msgid "You can use the following command to set your favourite editor:" msgstr "Pode usar a seguinte orde para configurar o seu editor favorito:" #. TRANSLATORS keep 'set editor', it's a command #: offpunk.py:2163 msgid "set editor EDITOR" msgstr "set editor EDITOR" #: offpunk.py:2164 msgid "or use the $VISUAL or $EDITOR environment variables." msgstr "ou use as variables de contorno $VISUAL ou $EDITOR." #: offpunk.py:2168 #, python-format msgid "%s is a system list which cannot be deleted" msgstr "%s é unha lista do sistema e non se pode borrar" #: offpunk.py:2171 #, python-format msgid "Are you sure you want to delete %s ?\n" msgstr "Está seguro de querer borrar %s ?\n" #: offpunk.py:2174 #, python-format msgid "! %s items in the list will be lost !\n" msgstr "! Perderanse %s elementos nesa lista !\n" #: offpunk.py:2178 msgid "The list is empty, it should be safe to delete it.\n" msgstr "Esta lista está baleira, debería poder borrarse de forma segura.\n" #: offpunk.py:2181 #, python-format msgid "Type \"%s\" (in capital, without quotes) to confirm :" msgstr "Escriba \"%s\" (en maiúsculas, sen comiñas) para confirmar :" #: offpunk.py:2188 #, python-format msgid "* * * %s has been deleted" msgstr "* * * %s foi borrada" #: offpunk.py:2190 offpunk.py:2192 msgid "A valid list name is required to be deleted" msgstr "Requírese un nome de lista válido para borrar unha lista" #: offpunk.py:2196 #, python-format msgid "You cannot modify %s which is a system list" msgstr "Non pode modificar %s, que é unha lista do sistema" #: offpunk.py:2206 #, python-format msgid "A valid list name is required after %s" msgstr "Requírese un nome de lista válido despois de %s" #: offpunk.py:2217 msgid "Need help from a fellow human? Simply send an email to the offpunk-users list." msgstr "" "Precisa axuda doutro humano? Só ten que enviar un email á lista de correo de usuarios de offpunk " "(offpunk-users)." #: offpunk.py:2220 msgid "Describe your problem/question as clearly as possible." msgstr "Describa o seu problema/pregunta tan claramente como sexa posíbel." #: offpunk.py:2221 msgid "Don’t forget to present yourself and why you would like to use Offpunk!" msgstr "Non esqueza presentarse e contarnos por que lle gusta usar Offpunk!" #: offpunk.py:2226 msgid "! is an alias for 'shell'" msgstr "! é un alias de 'shell'" #: offpunk.py:2228 msgid "? is an alias for 'help'" msgstr "? é un alias de 'help'" #: offpunk.py:2231 #, python-format msgid "%s is an alias for '%s'" msgstr "%s é un alias de '%s'" #: offpunk.py:2232 msgid "See the list of aliases with 'abbrevs'" msgstr "Vexa a lista de alias escribindo 'abbrevs'" #: offpunk.py:2233 #, python-format msgid "'help %s':" msgstr "'help %s':" #: offpunk.py:2257 msgid "Sync can only be achieved online. Change status with `online`." msgstr "Sync só se pode facer estando en liña. Cambie o estado escribindo 'online'." #: offpunk.py:2262 msgid "sync argument should be the cache validity expressed in seconds" msgstr "o argumento para 'sync' debería ser a validez da caché expresada en segundos" #: offpunk.py:2280 #, python-format msgid " -> adding to tour: %s" msgstr " -> engadindo ao tour: %s" #: offpunk.py:2306 #, python-format msgid "%s [%s/%s] Fetch " msgstr "%s [%s/%s] Descargando " #: offpunk.py:2359 #, python-format msgid " * * * %s to fetch in %s * * *" msgstr " * * * %s para descargar en %s * * *" #: offpunk.py:2413 msgid "End of sync" msgstr "Fin da sincronización (sync)" #: offpunk.py:2420 msgid "You can close your screen!" msgstr "Pode pechar a súa pantalla!" #: offpunk.py:2431 msgid "start with your list of bookmarks" msgstr "comezar coa súa lista de marcadores" #: offpunk.py:2437 msgid "Launch this command after startup" msgstr "Lanzar esta orde tras arrancar" #: offpunk.py:2442 msgid "use this particular config file instead of default" msgstr "usar este ficheiro de configuración en particular no canto do ficheiro por defecto" #: offpunk.py:2447 msgid "" "run non-interactively to build cache by exploring lists passed as " "argument. Without argument, all lists are fetched." msgstr "" "executar de xeito non interactivo para construir a caché explorando as listas " "pasadas como argumento. Se non hai argumentos, descargaranse todas " "as listas." #: offpunk.py:2453 msgid "assume-yes when asked questions about certificates/redirections during sync (lower security)" msgstr "" "asumir 'si' como resposta cando se lle pregunten cuestións sobre certificados durante o 'sync' " "(menor seguridade)" #: offpunk.py:2458 msgid "do not try to get http(s) links (but already cached will be displayed)" msgstr "non tratar de descargar ligazóns http(s) (aínda que se mostrarán as que xa estean na caché)" #: offpunk.py:2463 msgid "run non-interactively with an URL as argument to fetch it later" msgstr "executar de xeito non interactivo cunha URL como argumento para descargala máis tarde" #: offpunk.py:2467 msgid "depth of the cache to build. Default is 1. More is crazy. Use at your own risks!" msgstr "" "profundidade da caché a construir. Por defecto é 1. Máis é unha tolemia. Use este parámetro baixo " "o seu propio risco!" #: offpunk.py:2471 msgid "" "the mode to use to choose which images to download in a HTML page. one " "of (None, readable, full). Warning: full will slowdown your sync." msgstr "" "o modo para usar para escoller que imaxes descargar nunha páxina " "HTML. Pode ser un de ('None', 'readable', 'full)). Aviso: 'full' " "ralentizará a sincronización." #: offpunk.py:2476 msgid "duration for which a cache is valid before sync (seconds)" msgstr "tempo durante o cal unha caché é válida antes de sincronizar (en segundos)" #: offpunk.py:2479 msgid "display version information and quit" msgstr "amosar información de versión e saír" #: offpunk.py:2484 msgid "display available features and dependancies then quit" msgstr "amosar características dispoñíbeis e dependencias e saír" #: offpunk.py:2490 msgid "Arguments should be URL to be fetched or, if --sync is used, lists" msgstr "Os argumentos deberían ser unha URL para descargar ou, se se usa --sync, listas" #: offpunk.py:2504 msgid "Creating config directory {}" msgstr "Creando directorio de configuración {}" #: offpunk.py:2536 #, python-format msgid "%s is not a valid URL to fetch" msgstr "%s non é unha URL válida para descargar" #: offpunk.py:2538 msgid "--fetch-later requires an URL (or a list of URLS) as argument" msgstr "--fetch-later require unha URL (ou unha lista de URLs) como argumento" #: offpunk.py:2566 msgid "Welcome to Offpunk!" msgstr "Benvido a Offpunk!" #. TRANSLATORS keep 'help', it's a literal command #: offpunk.py:2568 msgid "Type `help` to get the list of available command." msgstr "Escriba 'help' para ver a lista de ordes dispoñíbeis." #: offutils.py:153 #, python-format msgid "No XDG folder for %s. Check your code." msgstr "Non hai directorio XDG para %s. Revise o seu código." #: offutils.py:166 #, python-format msgid "Using config %s" msgstr "Usando a configuración %s" #: offutils.py:179 #, python-format msgid "Skipping startup command \"%s\" due to provided URL" msgstr "Ignorando a orde de arrinque \"%s\" xa que se introduciu unha URL" #: offutils.py:388 #, python-format msgid "%s is not a valid email address" msgstr "%s non é unha dirección de correo electrónico válida" #. TRANSLATORS please keep the 'Y/N' as is #: offutils.py:392 #, python-format msgid "Send an email to %s Y/N? " msgstr "Mandar un correo electrónico a %s ? Y/N " #: offutils.py:409 #, python-format msgid "Cannot find a mail client to send mail to %s" msgstr "Non atopo un cliente de correo electrónico para enviar un correo a %s" #: offutils.py:410 openk.py:140 opnk.py:140 msgid "Please install xdg-open (usually from xdg-util package)" msgstr "Por favor, instale 'xdg-open' (normalmente no paquete 'xdg-util')" #: openk.py:38 opnk.py:38 msgid "Please install the pager \"less\" to run Offpunk." msgstr "Por favor, instale o paxinador 'less' para executar Offpunk." #: openk.py:39 opnk.py:39 msgid "If you wish to use another pager, send me an email !" msgstr "Se desexa utilizar un paxinador distinto, envíeme un correo electrónico !" #: openk.py:41 opnk.py:41 msgid "(I’m really curious to hear about people not having \"less\" on their system.)" msgstr "(interésame moito saber de xente que non teña 'less' no seu sistema.)" #. TRANSLATORS: keep echo and %s, translate the text between "" #: openk.py:139 opnk.py:139 #, python-format msgid "echo \"Can’t find how to open \"%s" msgstr "echo \"Non atopo como abrir \"%s" #: openk.py:235 opnk.py:235 #, python-format msgid "%s does not exist" msgstr "%s non existe" #. TRANSLATORS translate only "MY_PREFERED_APP" #: openk.py:309 opnk.py:309 #, python-format msgid "\"handler %s MY_PREFERED_APP %%s\"" msgstr "\"handler %s O_MEU_APLICATIVO_PREFERIDO %%s\"" #: openk.py:314 opnk.py:314 #, python-format msgid "External open of type %s with \"%s\"" msgstr "Abra externamente os elementos de tipo %s con \"%s\"" #: openk.py:315 openk.py:327 opnk.py:315 opnk.py:327 #, python-format msgid "You can change the default handler with %s" msgstr "Pode cambiar o manexador por defecto con %s" #: openk.py:323 opnk.py:323 #, python-format msgid "Handler program %s not found!" msgstr "Non se atopa o programa manexador %s !" #: openk.py:324 opnk.py:324 msgid "" "You can use the ! command to specify another handler program or pipeline." msgstr "" "Pode usar a orde ! para especificar outro programa ou pipeline manexador." #: openk.py:363 opnk.py:363 msgid "" "openk is an universal open command tool that will try to display any file in the " "pager less after rendering its content with ansicat. If that fails, openk will " "fallback to opening the file with xdg-open. If given an URL as input instead of a " "path, openk will rely on netcache to get the networked content." msgstr "" "openk é unha ferramenta universal para abrir que intentará amosar calquera ficheiro " "no paxinador 'less' despois de renderizar o seu contido con ansicat. Se iso falla, " "openk intentará abrir o ficheiro con 'xdg-open'. Se se lle da unha URL como entrada, " "en lugar dunha ruta, opnk fará uso de netcache para obter o contido da rede." #: openk.py:387 opnk.py:387 msgid "Path to the file or URL to open" msgstr "Ruta ao ficheiro ou URL para abrir" #: xkcdpunk.py:33 msgid "xkcdpunk is a tool to display a given XKCD comic in your terminal" msgstr "xkcdpunk é unha ferramenta para amosar un comic de XKCD dado na súa terminal" #: xkcdpunk.py:38 msgid "XKCD comic number" msgstr "Número de comic XKCD" #: xkcdpunk.py:41 msgid "Only access cached comics" msgstr "Acceder só a cómics na caché" #~ msgid " (empty line to send)" #~ msgstr " (liña en branco para enviar)" #~ msgid "" #~ "\n" #~ "> missing picture, please reload\n" #~ msgstr "" #~ "\n" #~ "> falta unha imaxe,por favor recargue ('reload')\n" #, fuzzy #~ msgid "The" #~ msgstr "O" offpunk-v3.0/po/nl_BE.po000066400000000000000000001335201514232770500151610ustar00rootroot00000000000000# Dutch translations for Offpunk package. # Copyright (C) 2026 Offpunk'S COPYRIGHT HOLDER # This file is distributed under the same license as the Offpunk package. # Bert Livens , 2026. # msgid "" msgstr "" "Project-Id-Version: Offpunk 3.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-07 13:50+0100\n" "PO-Revision-Date: 2026-02-09 00:09+0100\n" "Last-Translator: Bert Livens \n" "Language-Team: Dutch\n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.8\n" #: ansicat.py:60 msgid "To improve your web experience (less cruft in webpages)," msgstr "Om uw webervaring te verbeteren (minder brol op webpagina’s)," #: ansicat.py:61 msgid "please install python3-readability or readability-lxml" msgstr "gelieve python3-readability of readability-lxml te installeren" #: ansicat.py:100 msgid "To render images inline, you need either chafa >= 1.10 or timg > 1.3.2" msgstr "" "Om afbeelingen inline weer te geven, heeft u chafa >= 1.10 of timg > 1.3.2 " "nodig" #: ansicat.py:203 msgid "No cleaning required" msgstr "Opkuisen niet nodig" #: ansicat.py:515 #, python-format msgid "%s is not a valid link for %s" msgstr "%s is geen geldige link voor %s" #: ansicat.py:601 #, python-format msgid "Urljoin Error: Could not make an URL out of %s and %s" msgstr "Urljoin Fout: Kon geen URL maken van %s en %s" #: ansicat.py:929 msgid "Error rendering Gopher " msgstr "Fout bij het weergeven van Gopher " #: ansicat.py:1068 msgid "" "\n" "## Bookmarks Lists (updated during sync)\n" msgstr "" "\n" "## Bladwijzerlijsten (worden bijgewerkt bij synchonisatie)\n" #: ansicat.py:1071 msgid "" "\n" "## Subscriptions (new links in those are added to tour)\n" msgstr "" "\n" "## Abonnementen (nieuwe links in deze worden toegevoegd aan de tour)\n" #: ansicat.py:1074 msgid "" "\n" "## Frozen (fetched but never updated)\n" msgstr "" "\n" "## Bevroren (opgehaald maar nooit bijgewerkt)\n" #: ansicat.py:1077 msgid "" "\n" "## System Lists\n" msgstr "" "\n" "## Systeemlijsten\n" #: ansicat.py:1258 ansicat.py:1319 msgid "" "HTML document detected. Please install python-bs4 and python-readability." msgstr "" "HTML document gedetecteerd. Gelieve python-bs4 en python-readability te " "installeren." #: ansicat.py:1741 msgid "" "\n" "> Please install python-bs4 to parse HTML" msgstr "" "\n" "> Gelieve python-bs4 te installeren om HTML te parsen" #: ansicat.py:1743 msgid "" "\n" "> Picture not in cache. Please reload this page.\n" msgstr "" "\n" "> Afbeelding niet in de cache. Gelieve deze pagina te herladen.\n" #: ansicat.py:1826 msgid "Cannot guess the mime type of the file. Please install \"file\"." msgstr "" "Het mime-type van het bestand kan niet worden geraden. Gelive “file” te " "installeren." #: ansicat.py:1940 #, python-format msgid "Could not render %s" msgstr "%s kon niet worden weergegeven" #: ansicat.py:1945 msgid "" "ansicat is a terminal rendering tool that will render multiple formats " "(HTML, Gemtext, RSS, Gophermap, Image) into ANSI text and " "colors.\n" " When used on a file, ansicat will try to autodetect the format. " "When used with standard input, the format must be manually " "specified.\n" " If the content contains links, the original URL of the content " "can be specified in order to correctly modify relatives links." msgstr "" "ansicat is een terminalweergavetool die meerdere formaten (HTML, " "Gemtext, RSS, Gophermap, afbeeldingen) weergeeft in ANSI-tekst en -kleuren.\n" " Wanneer gebruikt op een bestand zal ansicat het formaat " "automatisch proberen te detecteren. Wanneer gebruikt met " "standaardinvoer moet het formaat manueel gespecificeerd worden.\n" " Als de in inhoud links bevat, kan de originele URL van de inhoud " "gespecificeerd worden om de relatieve links correct aan te " "passen." #: ansicat.py:1966 msgid "" "Renderer to use. Available: auto, gemtext, html, feed, gopher, image, " "folder, plaintext" msgstr "" "Te gebruiken renderer. Beschikbaar: auto, gemtext, html, feed, gopher, " "image, folder, plaintext" #: ansicat.py:1968 msgid "Mime of the content to parse" msgstr "Mime-type van de te parsen inhoud" #: ansicat.py:1972 msgid "Original URL of the content" msgstr "Originele URL van de inhoud" #: ansicat.py:1977 openk.py:371 opnk.py:371 msgid "" "Which mode should be used to render: normal (default), full or " "source. With HTML, the normal mode try to " "extract the article." msgstr "" "Welke modus moet worden gebruikt voor de weergave: \"normal\" (standaard), " "\"full\" of \"source\". Bij HTML zal de " "normale modus proberen het artikel te extraheren." #: ansicat.py:1986 openk.py:380 opnk.py:380 msgid "Which mode should be used to render links: none (default) or end" msgstr "" "Welke modus moet gebruikt worden bij het weergeven om links te tonen: " "\"none\" (standaard) of \"end\"" #: ansicat.py:1994 msgid "Path to the text to render (default to stdin)" msgstr "Pad van de tekst om weer te geven (standaard stdin)" #: ansicat.py:2017 msgid "Ansicat needs at least one file as an argument" msgstr "Ansicat heeft op zijn minst één bestand nodig als argument" #: ansicat.py:2021 msgid "Format or mime should be specified when running with stdin" msgstr "Formaat of mime moet gespecificeerd worden bij het uitvoeren met stdin" #: netcache.py:126 msgid "We return False because path is too long" msgstr "We geven False terug omdat het pad te lang is" #: netcache.py:314 #, python-format msgid "" "ERROR while caching %s\n" "\n" msgstr "" "FOUT bij het cachen van %s\n" "\n" #: netcache.py:319 msgid "If you believe this error was temporary, type reload.\n" msgstr "Als u denkt dat deze fout tijdelijk is, typ dan “reload”.\n" #: netcache.py:320 msgid "The resource will be tentatively fetched during next sync.\n" msgstr "De pagina zal worden opgehaald tijdens de volgende synchronisatie.\n" #: netcache.py:354 #, python-format msgid "Size of %s is %s Mo\n" msgstr "Grootte van %s is %s MB\n" #: netcache.py:355 #, python-format msgid "Offpunk only download automatically content under %s Mo\n" msgstr "Offpunk download alleen automatisch bestanden kleiner dan %s MB\n" #: netcache.py:358 msgid "To retrieve this content anyway, type 'reload'." msgstr "Om deze inhoud toch te downloaden, typ “reload”." #: netcache.py:398 #, python-format msgid " -> Receiving stream: %s%% of allowed data" msgstr " -> Ontvangen van stream: %s%% van toegestane data" #: netcache.py:564 #, python-brace-format msgid "Certificate not valid until: {}!" msgstr "Certificaat niet geldig tot: {}!" #: netcache.py:568 #, python-brace-format msgid "Certificate expired as of: {})!" msgstr "Certificaat vervallen sinds: {})!" #: netcache.py:597 msgid "" "Hostname does not match certificate common name or any alternative names." msgstr "" "De hostnaam komt niet overeen met de algemene naam of een van de " "alternatieve namen van het certificaat." #: netcache.py:653 msgid "[SECURITY WARNING] Unrecognised certificate!" msgstr "[VEILIGHEIDSWAARSCHUWING] Onbekend certificaat!" #: netcache.py:655 #, python-brace-format msgid "The certificate presented for {} ({}) has never been seen before." msgstr "" "Het certificaat dat wordt voorgelegd voor {} ({}) is nog nooit eerder gezien." #: netcache.py:659 msgid "This MIGHT be a Man-in-the-Middle attack." msgstr "Dit ZOU een Man-in-the-Middle aanval kunnen zijn." #: netcache.py:661 #, python-brace-format msgid "A different certificate has previously been seen {} times." msgstr "Een ander certificaat is {} keer eerder gezien." #: netcache.py:667 msgid "That certificate has expired, which reduces suspicion somewhat." msgstr "Dat certificaat is vervallen, wat dit wat minder verdacht maakt." #: netcache.py:669 #, python-brace-format msgid "That certificate is still valid for: {}" msgstr "Dat certificaat is not geldig voor: {}" #: netcache.py:671 msgid "Attempt to verify the new certificate fingerprint out-of-band:" msgstr "" "Probeer om de vingerafdruk van het nieuwe certificaat out-of-band te " "verifiëren:" #. TRANSLATORS: keep "Y/N" because the answer has to be one of those #: netcache.py:677 msgid "Accept this new certificate? Y/N " msgstr "Dit nieuwe certificaat accepteren? Y/N " #: netcache.py:684 msgid "TOFU Failure!" msgstr "TOFU-mislukking!" #: netcache.py:781 msgid "There are no certificates available for this site." msgstr "Er zijn geen certificaten beschikbaar voor deze site." #. TRANSLATORS: keep the "y/n" #: netcache.py:783 msgid "Do you want to create one? (y/n) " msgstr "Wil je er een aanmaken? (y/n) " #: netcache.py:785 msgid "Name for this certificate: " msgstr "Naam voor dit certificaat: " #: netcache.py:786 msgid "Validity in days: " msgstr "Geldigheid in aantal dagen: " #: netcache.py:793 msgid "The name or validity you typed are invalid" msgstr "De naam of geldigheid die u heeft opgegeven is ongeldig" #: netcache.py:798 msgid "The one available certificate for this site is:" msgstr "Het beschikbare certificaat voor deze site is:" #: netcache.py:801 #, python-brace-format msgid "The {} available certificates for this site are:" msgstr "De {} beschikbare certificaten voor deze site zijn:" #: netcache.py:810 msgid "which certificate do you want to use? > " msgstr "welk certificaat wilt u gebruiken? > " #: netcache.py:895 msgid "This identity doesn't exist for this site (or is disabled)." msgstr "Deze identiteit bestaat niet voor deze site (of is uitgeschakeld)." #: netcache.py:960 netcache.py:966 msgid "Received invalid header from server!" msgstr "Ongeldige header ontvangen van de server!" #: netcache.py:991 msgid "URL redirects to itself!" msgstr "URL verwijst naar zichzelf!" #: netcache.py:993 msgid "Caught in redirect loop!" msgstr "Verstrikt in een verwijzingslus!" #: netcache.py:996 #, python-format msgid "Refusing to follow more than %d consecutive redirects!" msgstr "Weigering om meer dan %d opeenvolgende omleidingen te volgen!" #: netcache.py:1027 msgid "You need to provide a client-certificate to access this page." msgstr "" "U moet een clientcertificaat opgeven om toegang te krijgen tot deze pagina." #: netcache.py:1031 msgid "" "You need to provide a client-certificate to access this page.\r\n" "Type \"certs\" to create or re-use one" msgstr "" "U moet een clientcertificaat opgeven om toegang te krijgen tot deze pagina." "\r\n" "Typ “certs” om er een aan te maken of te hergebruiken" #: netcache.py:1035 #, python-format msgid "Server returned undefined status code %s!" msgstr "De server heeft de ongedefinieerde statuscode %s teruggegeven!" #: netcache.py:1059 #, python-format msgid "" "Could not decode response body using %s " "encoding declared in header!" msgstr "" "Kon de response body niet decoderen met de in de header opgegeven codering " "%s!" #: netcache.py:1095 msgid "Blocked URL: " msgstr "Geblokkeerde URL: " #: netcache.py:1096 msgid "This website has been blocked with the following rule:\n" msgstr "Deze website wordt geblokkeerd door de volgende regel:\n" #: netcache.py:1098 msgid "Use the following redirect command to unblock it:\n" msgstr "Gebruik het volgende “redirect”commando om het te deblokkeren:\n" #: netcache.py:1127 #, python-format msgid "%s is not a supported protocol" msgstr "%s is geen ondersteund protocol" #: netcache.py:1135 msgid "HTTP requires python-requests" msgstr "HTTP heeft “python-requests” nodig" #: netcache.py:1154 msgid "ERROR: DNS error!" msgstr "FOUT: DNS-fout!" #: netcache.py:1157 msgid "ERROR1: Connection refused!" msgstr "FOUT1: Verbinding geweigerd!" #: netcache.py:1160 msgid "ERROR2: Connection reset!" msgstr "FOUT2: Verbinding verbroken!" #: netcache.py:1163 msgid "" "ERROR3: Connection timed out!\n" " Slow internet connection? Use 'set timeout' to be more patient." msgstr "" "FOUT3: Verbinding verlopen!\n" " Trage internetverbinding? Gebruik “set timeout” om geduldiger te zijn." #: netcache.py:1167 msgid "" "ERROR5: Trying to create a directory which already exists\n" " in the cache : " msgstr "" "FOUT5: Poging om een map aan te maken die al bestaat\n" " in de cache: " #: netcache.py:1172 msgid "ERROR6: Bad SSL certificate:\n" msgstr "FOUT6: Slecht TLS-certificaat:\n" #: netcache.py:1175 msgid "" "\n" " If you know what you are doing, you can try to accept bad certificates with " "the following command:\n" msgstr "" "\n" " Indien u weet wat u doen, kunt u slechte certificaten proberen te " "aanvaarden met volgend commando:\n" #: netcache.py:1180 msgid "ERROR7: Cannot connect to URL:\n" msgstr "FOUT7: Kan niet verbinden met URL:\n" #: netcache.py:1186 msgid "ERROR4: " msgstr "FOUT4: " #: netcache.py:1203 #, python-format msgid "Downloading %s" msgstr "Downloaden van %s" #: netcache.py:1225 msgid "" "Netcache is a command-line tool to retrieve, cache and access networked " "content.\n" " By default, netcache will returns a cached version of a given " "URL, downloading it only if a cache version doesn't exist. A " "validity duration, in seconds, can also be given so netcache " "downloads the content only if the existing cache is older than the validity." msgstr "" "Netcache is een command-line tool om netwerkinhoud op te halen, te cachen en " "op te vragen.\n" " Standaard geeft URL een gecachte versie van een gegeven URL " "terug, waarbij het enkel wordt gedownload als er geen gecachte " "versie bestaat. Er kan ook een geldigheidsduur in seconden worden " "meegegeven opdat netcache inhoud downloadt wanneer de inhoud in " "de cache ouder is dan de gegeven geldigheid." #: netcache.py:1234 msgid "return path to the cache instead of the content of the cache" msgstr "geef het pad naar de cache terug in plaats van de inhoud van de cache" #: netcache.py:1239 msgid "" "return a list of id's for the gemini-site instead of the content of the cache" msgstr "" "geef een lijst van id's van de gemini-site in plaats van de inhoud van de " "cache" #: netcache.py:1244 msgid "Do not attempt to download, return cached version or error" msgstr "" "Probeer niet niet te downloaden, geef de gecachte versie of een fout terug" #: netcache.py:1249 msgid "Cancel download of items above that size (value in Mb)." msgstr "Annuleer download van items boven die grootte (waarde in Mb)." #: netcache.py:1254 msgid "Time to wait before cancelling connection (in second)." msgstr "Wachttijd voordat de verbinding wordt verbroken (in seconden)." #: netcache.py:1260 openk.py:393 opnk.py:393 msgid "" "maximum age, in second, of the cached version " "before redownloading a new version" msgstr "" "maximum leeftijd, in seconden, van de gecachte versie " "voor een nieuwe versie te downloaden" #: netcache.py:1268 msgid "download URL and returns the content or the path to a cached version" msgstr "" "download URL en geef de inhoud of het pad naar de gecachte versie terug" #: offpunk.py:66 msgid "Install xsel/xclip (X11) or wl-clipboard (Wayland) to use copy" msgstr "Installeer xsel/xclip (X11) of wl-clipboard (Wayland) om te kopiëren" #: offpunk.py:95 msgid "" "Install xsel/xclip (X11) or wl-clipboard (Wayland) to get URLs from your " "clipboard" msgstr "" "Installeer xsel/xclip (X11) of wl-clipboard (Wayland) om URL’s van uw " "klembord te kopiëren" #: offpunk.py:149 msgid "You need to 'go' somewhere, first" msgstr "U moet eerst ergens met “go” naartoe gaan" #: offpunk.py:158 msgid "Error: " msgstr "Fout: " #: offpunk.py:337 #, python-format msgid "We don’t handle name of URL: %s" msgstr "We verwerken de naam van de URL:%s niet" #: offpunk.py:381 #, python-format msgid "%s not available, marked for syncing" msgstr "%s is niet beschikbaar, aangeduid voor synchronisatie" #: offpunk.py:383 offpunk.py:1050 #, python-format msgid "%s already marked for syncing" msgstr "%s reeds aangeduid voor synchronisatie" #: offpunk.py:446 offpunk.py:1393 msgid "What?" msgstr "Wablief?" #: offpunk.py:450 msgid "No links to index" msgstr "Geen links naar de index" #: offpunk.py:458 msgid "No page with links" msgstr "Geen pagina met links" #: offpunk.py:466 #, python-format msgid "%s is redirected to %s" msgstr "%s verwijst door naar %s" #: offpunk.py:468 #, python-format msgid "Please add a destination to redirect %s" msgstr "Gelieve een bestemming toe te voegen voor de de doorverwijzing %s" #: offpunk.py:474 #, python-format msgid "Redirection for %s has been removed" msgstr "Doorverwijzing voor %s is verwijderd" #: offpunk.py:476 #, python-format msgid "%s was not redirected. Nothing has changed." msgstr "%s werd niet doorverwezen. Niets is veranderd." #: offpunk.py:479 #, python-format msgid "%s will now be blocked" msgstr "%s wordt vanaf nu geblokkeerd" #: offpunk.py:482 #, python-format msgid "%s will now be redirected to %s" msgstr "%s wordt vanaf nu doorverwezen naar %s" #: offpunk.py:486 msgid "Current redirections:\n" msgstr "Huidige doorverwijzingen:\n" #: offpunk.py:490 msgid "" "\n" "To add new, use \"redirect origine.com destination.org\"" msgstr "" "\n" "Om er nieuwe toe te voegen, gebruik “redirect herkomst.net bestemming.org”" #: offpunk.py:491 msgid "" "\n" "To remove a redirect, use \"redirect origine.com NONE\"" msgstr "" "\n" "Om een doorverwijzing te verwijderen, gebruik “redirect oorsprong.net NONE”" #: offpunk.py:493 msgid "" "\n" "To completely block a website, use \"redirect origine.com BLOCK\"" msgstr "" "\n" "Om een website volledig te blokkeren, gebruik “redirect oorsprong.net BLOCK”" #: offpunk.py:495 msgid "" "\n" "To block also subdomains, prefix with *: \"redirect *origine.com BLOCK\"" msgstr "" "\n" "Om ook subdomeinen te blokkeren, voeg een * als voervoegsel: “redirect " "*oorsprong.net BLOCK”" #: offpunk.py:510 offpunk.py:515 #, python-format msgid "Unrecognised option %s" msgstr "Onbekende optie %s" #: offpunk.py:520 msgid "TLS mode must be `ca` or `tofu`!" msgstr "TLS-modus moet “ca” of “tofu” zijn!" #: offpunk.py:524 msgid "Only high security certificates are now accepted" msgstr "" "Enkel certificaten met een hoge beveiligingsgraad worden vanaf nu " "geaccepteerd" #: offpunk.py:526 msgid "Low security SSL certificates are now accepted" msgstr "" "Certificaten met een lage beveiligingsgraad worden vanaf nu geaccepteerd" #. TRANSLATORS keep accept_bad_ssl_certificates, True, and False #: offpunk.py:529 msgid "accept_bad_ssl_certificates should be True or False" msgstr "accept_bad_ssl_certificates moet “True” of “False” zijn" #: offpunk.py:534 msgid "changing width to " msgstr "breedte aanpassen naar " #: offpunk.py:537 #, python-format msgid "%s is not a valid width (integer required)" msgstr "%s is geen geldige breedte (geheel getal vereist)" #: offpunk.py:540 msgid "Avaliable linkmode are `none` and `end`." msgstr "Beschikbare linkmodes zijn “none” en “end”." #: offpunk.py:591 msgid "Available preset themes are: " msgstr "Beschikbare vooraf ingestelde thema's zijn: " #: offpunk.py:607 #, python-format msgid "%s is not a valid preset theme" msgstr "%s is geen geldig vooraf ingesteld thema" #: offpunk.py:609 #, python-format msgid "%s is not a valid theme element" msgstr "%s is geen geldig thema-element" #: offpunk.py:610 msgid "Valid theme elements are: " msgstr "Geldige thema-elementen zijn: " #: offpunk.py:622 #, python-format msgid "%s is set to %s" msgstr "%s is ingesteld als %s" #: offpunk.py:627 #, python-format msgid "%s reset (it was set to %s)" msgstr "%s reset (ingesteld als %s)" #: offpunk.py:630 #, python-format msgid "%s is not set. Nothing to do" msgstr "%s is niet ingesteld. Niets te doen" #: offpunk.py:635 #, python-format msgid "%s is not a valid color" msgstr "%s is geen geldige kleur" #: offpunk.py:636 msgid "Valid colors are one of: " msgstr "De geldige kleuren zijn: " #: offpunk.py:673 #, python-format msgid "No handler set for MIME type %s" msgstr "Geen handler ingesteld voor mime-type %s" #: offpunk.py:699 offpunk.py:707 #, python-format msgid "%s is a command and cannot be aliased" msgstr "%s is een commando en kan niet worden gealiast" #: offpunk.py:701 #, python-format msgid "%s is currently aliased to \"%s\"" msgstr "%s is momenteel gealiast naar “%s”" #: offpunk.py:703 #, python-format msgid "there’s no alias for \"%s\"" msgstr "er is geen alias voor “%s”" #: offpunk.py:710 #, python-format msgid "%s has been aliased to \"%s\"" msgstr "%s is gealiast naar \"%s\"" #: offpunk.py:716 msgid "Offline and undisturbed." msgstr "Offline en ongestoord." #: offpunk.py:720 msgid "Offpunk is now offline and will only access cached content" msgstr "Offpunk is nu offline en zal enkel gecachte inhoud weergeven" #: offpunk.py:727 msgid "Offpunk is online and will access the network" msgstr "Offpunk is nu online en zal het netwerk gebruiken" #: offpunk.py:729 msgid "Already online. Try offline." msgstr "Reeds online. Probeer “offline”." #: offpunk.py:768 msgid "No content to copy, visit a page first" msgstr "Geen inhoud om te kopiëren, bezoek eerst een pagina" #: offpunk.py:784 #, python-format msgid "We cannot share %s because it is local only" msgstr "We kunnen %s niet delen want het is enkel lokaal" #: offpunk.py:796 msgid "TODO: sharing text is not yet implemented" msgstr "TODO: het delen van tekst is nog niet geïmplementeerd" #: offpunk.py:813 offpunk.py:940 msgid "Nothing to share, visit a page first" msgstr "Niets om te delen, bezoek eerst een pagina" #: offpunk.py:879 msgid "Multiple emails addresse were found:" msgstr "Verschillende e-mailadressen gevonden:" #: offpunk.py:884 msgid "None of the above" msgstr "Geen van bovenstaande" #: offpunk.py:886 msgid "Which email will you use to reply?" msgstr "Welk e-mailadres wilt u gebruiken om te antwoorden?" #: offpunk.py:898 msgid "Enter the contact email for this page?" msgstr "Voer het contact-e-mailadres in voor deze pagina." #: offpunk.py:907 msgid "Email address:" msgstr "E-mailadres:" #: offpunk.py:908 msgid "Do you want to save this email as a contact for" msgstr "Wilt u dit e-mailadres opslaan als een contact voor" #: offpunk.py:909 msgid "Current page only" msgstr "Enkel de huidige pagina" #: offpunk.py:910 #, python-format msgid "The whole %s space" msgstr "De volledige ruimte van %s" #: offpunk.py:911 msgid "Don’t save this email" msgstr "Dit e-mailadres niet opslaan" #: offpunk.py:913 msgid "Your choice?" msgstr "Uw keuze:" #: offpunk.py:931 #, python-format msgid "Email %s has been recorded as contact for %s" msgstr "E-mail %s is opgeslagen als contact voor %s" #: offpunk.py:932 msgid "Nothing to save" msgstr "Niets om op te slaan" #: offpunk.py:935 msgid "In reply to " msgstr "Als antwoord op " #: offpunk.py:938 #, python-format msgid "We cannot reply to %s because it is local only" msgstr "We kunnen niet antwoorden aan %s omdat het enkel lokaal is" #: offpunk.py:959 msgid "Too many arguments to list." msgstr "Te veel argumenten om op te lijsten." #: offpunk.py:962 offpunk.py:984 msgid "URL required (or visit a page)." msgstr "URL vereist (of bezoek een pagina)." #: offpunk.py:966 msgid "Cookies not enabled for url" msgstr "Cookies niet ingeschakeld voor URL" #: offpunk.py:968 msgid "Cookies for url:" msgstr "Cookies voor URL:" #. TRANSLATORS domain, path, expiration time, name, value #: offpunk.py:971 #, python-format msgid "%s %s expires:%s %s=%s" msgstr "%s %s vervalt op:%s %s=%s" #: offpunk.py:976 msgid "File parameter required for import." msgstr "Bestandsparameter vereist voor import." #: offpunk.py:981 msgid "Too many arguments to import" msgstr "Te veel argumenten om te importeren" #: offpunk.py:991 msgid "File not found" msgstr "Bestand niet gevonden" #: offpunk.py:993 msgid "Imported." msgstr "Geïmporteerd." #: offpunk.py:995 msgid "Huh?" msgstr "Hè?" #: offpunk.py:1008 msgid "URLs in your clipboard\n" msgstr "URL’s in uw klembord\n" #: offpunk.py:1013 msgid "Where do you want to go today ?> " msgstr "Waar wilt u vandaag naartoe?> " #: offpunk.py:1020 msgid "Go where? (hint: simply copy an URL in your clipboard)" msgstr "Waar naartoe? (tip: kopieer een URL naar uw klembord)" #: offpunk.py:1039 #, python-format msgid "%s is not a valid URL to go" msgstr "%s is geen geldige URL om naar toe te gaan" #: offpunk.py:1048 #, python-format msgid "%s marked for syncing" msgstr "%s aangeduid voor synchronisatie" #: offpunk.py:1071 msgid "Up only take integer as arguments" msgstr "“Up” aanvaardt enkel gehele getallen als argumenten" #: offpunk.py:1126 msgid "End of tour." msgstr "Einde van de tour." #: offpunk.py:1146 #, python-format msgid "List %s does not exist. Cannot add it to tour" msgstr "Lijst %s bestaat niet, kan deze niet toevoegen aan tour" #: offpunk.py:1173 #, python-format msgid "Invalid use of range syntax %s, skipping" msgstr "Ongeldig gebruik van de bereiksyntax %s, overslaan" #: offpunk.py:1175 offpunk.py:1542 #, python-format msgid "Non-numeric index %s, skipping." msgstr "Niet-numerieke index %s, overslaan." #: offpunk.py:1177 offpunk.py:1544 #, python-format msgid "Invalid index %d, skipping." msgstr "Ongeldige index %d, overslaan." #: offpunk.py:1219 msgid "Invalid mark, must be one letter" msgstr "Ongeldige aanduiding, moet één letter zijn" #. TRANSLATORS: this string and "Mime", "Cache", "Renderer" are formatted to align. #. if you can obtain the same effect in your language, try to do it ;) #. they are displayed with the "info" command #: offpunk.py:1230 msgid "URL : " msgstr "URL : " #: offpunk.py:1231 msgid "Mime : " msgstr "Mime : " #: offpunk.py:1232 msgid "Cache : " msgstr "Cache : " #: offpunk.py:1238 msgid "Renderer : " msgstr "Renderer : " #: offpunk.py:1239 msgid "Cleaned with : " msgstr "Opgekuist met: " #: offpunk.py:1245 msgid "Page appeard in following lists :\n" msgstr "Pagina verscheen in de volgende lijsten:\n" #: offpunk.py:1248 msgid "normal list" msgstr "normale lijst" #: offpunk.py:1250 msgid "subscription" msgstr "abonnement" #: offpunk.py:1252 msgid "frozen list" msgstr "bevroren lijst" #: offpunk.py:1258 msgid "Page is not save in any list" msgstr "Pagina is niet opgeslagen in een lijst" #: offpunk.py:1276 msgid "System: " msgstr "Systeem: " #: offpunk.py:1277 msgid "Python: " msgstr "Python: " #: offpunk.py:1278 msgid "Language: " msgstr "Taal: " #: offpunk.py:1279 msgid "" "\n" "Highly recommended:\n" msgstr "" "\n" "Ten zeerste aangeraden:\n" #: offpunk.py:1281 msgid "" "\n" "Web browsing:\n" msgstr "" "\n" "Websurfen:\n" #: offpunk.py:1288 msgid "" "\n" "Nice to have:\n" msgstr "" "\n" "Leuk om te hebben:\n" #: offpunk.py:1295 msgid "" "\n" "Features :\n" msgstr "" "\n" "Functies:\n" #: offpunk.py:1296 msgid " - Render images (chafa or timg) : " msgstr " - Afbeeldingen weergeven (chafa of timg) : " #: offpunk.py:1299 msgid " - Render HTML (bs4, readability) : " msgstr " - HTML weergeven (bs4, readability) : " #: offpunk.py:1302 msgid " - Render Atom/RSS feeds (feedparser) : " msgstr " - Atom/RSS feeds weergeven (feedparser) : " #: offpunk.py:1305 msgid " - Connect to http/https (requests) : " msgstr " - Verbind met http/https (requests) : " #: offpunk.py:1308 msgid " - Detect text encoding (python-chardet) : " msgstr " - Tekstencodering detecteren (python-chardet) : " #: offpunk.py:1311 msgid " - restore last position (less 572+) : " msgstr " - laatste positie herstellen (less 572+) : " #: offpunk.py:1315 msgid "ftr_site_config : " msgstr "ftr_site_config : " #: offpunk.py:1316 msgid "Config directory : " msgstr "Configuratiemap : " #: offpunk.py:1317 msgid "User Data directory : " msgstr "Gebruikersdata-map: " #: offpunk.py:1318 msgid "Cache directoy : " msgstr "Cachemap : " #: offpunk.py:1331 msgid "Found a bug in Offpunk? You can report it by email to the developers." msgstr "" "Een bug gevonden in Offpunk? U kunt hem via e-mail melden aan de " "ontwikkelaars." #: offpunk.py:1333 msgid "Please describe your problem as clearly as possible:" msgstr "Gelieve uw probleem zo duidelijk mogelijk te beschrijven:" #: offpunk.py:1334 msgid "" "Include all the steps to reproduce the problem, including the URLs you are " "currently visiting." msgstr "" "Vermeld alle stappen om het probleem te reproduceren, inclusief de URL's die " "u momenteel bezoekt." #: offpunk.py:1335 offpunk.py:2223 msgid "Another point: always use \"reply-all\" when replying to this list." msgstr "" "Nog een punt: gebruik altijd “allen beantwoorden” wanneer u op deze lijst " "reageert." #: offpunk.py:1337 msgid "Describe the bug in one line: " msgstr "Beschrijf de bug in één regel: " #: offpunk.py:1342 msgid "No description of the bug, report cancelled" msgstr "Geen beschrijving van de bug, melding geannuleerd" #: offpunk.py:1388 msgid "Please enter the number of the XKCD comic you want to see" msgstr "Gelieve het nummer van de XKCD-strip die u wilt zien in te geven" #: offpunk.py:1456 msgid "Current page is already a feed" msgstr "De huidige pagina is reeds een feed" #: offpunk.py:1458 msgid "No feed found on current page" msgstr "Geef feed gevonden op de huidige pagina" #: offpunk.py:1461 msgid "Available feeds :\n" msgstr "Beschikbare feeds:\n" #: offpunk.py:1466 msgid "Which view do you want to see ? >" msgstr "Welke weergave wilt u zien? >" #. TRANSLATORS keep "view feed" and "feed" in english, those are literal commands #: offpunk.py:1490 msgid "view feed is deprecated. Use the command feed directly" msgstr "“view feed” is verouderd. Gebruik rechtstreeks het “feed” commando" #: offpunk.py:1499 #, python-format msgid "Link %s is: %s" msgstr "Link %s is: %s" #: offpunk.py:1507 msgid "Empty cached version" msgstr "Lege gecachte versie" #: offpunk.py:1508 #, python-format msgid "Last cached on %s" msgstr "Laatst gecachet op %s" #: offpunk.py:1510 msgid "No cached version for this link" msgstr "Geen gecachte versie van deze link" #. TRANSLATORS keep "normal, full, switch, source" in english #: offpunk.py:1515 msgid "Valid arguments for view are : normal, full, switch, source or a number" msgstr "" "De geldige argiumenten voor “view” zijn: normal, full, switch, source of een " "getal" #: offpunk.py:1589 msgid "You cannot save if not cached!" msgstr "U kan inhoud niet opslaan wanneer die niet gecachet is!" #: offpunk.py:1612 msgid "First argument is not a valid item index!" msgstr "Eerste argument is geen geldige itemindex!" #: offpunk.py:1616 msgid "You must provide an index, a filename, or both." msgstr "U moet een index, een bestandsnaam of beide opgeven." #: offpunk.py:1625 msgid "Index too high!" msgstr "Index te hoog!" #: offpunk.py:1636 #, python-format msgid "File %s already exists!" msgstr "Bestand %s bestaat al!" #: offpunk.py:1643 #, python-format msgid "Can’t save %s because it’s a folder, not a file" msgstr "Kan %s niet opslaan omdat het een map is, geen bestand" #: offpunk.py:1645 #, python-format msgid "Saved to %s" msgstr "Opgeslagen als %s" #: offpunk.py:1710 msgid "" "Subscriptions #subscribed (new links in those pages will be added to tour)" msgstr "" "Abonnementen #subscribed (nieuwe links in die pagina’s zullen aan de tour " "worden toegevoegd)" #: offpunk.py:1712 msgid "Links requested and to be fetched during the next --sync" msgstr "Aangevraagde links die bij de volgende --sync worden opgehaald" #: offpunk.py:1727 msgid "Multiple feeds have been found :\n" msgstr "Verschillende feeds gevonden:\n" #: offpunk.py:1729 msgid "This page is already a feed:\n" msgstr "Deze pagina is al een feed:\n" #: offpunk.py:1731 msgid "No feed detected. You can still watch the page :\n" msgstr "Geen feed gevonden. U kan nog steeds de pagina volgen:\n" #: offpunk.py:1742 #, python-format msgid "\t -> (already subscribed through lists %s)\n" msgstr "\t -> (reeds geabonneerd via de lijsten %s)\n" #: offpunk.py:1745 msgid "Which feed do you want to subscribe ? > " msgstr "Op welke feed wilt u u abonneren? > " #: offpunk.py:1754 #, python-format msgid "Subscribed to %s" msgstr "Geabonneerd op %s" #: offpunk.py:1756 #, python-format msgid "You are already subscribed to %s" msgstr "U ben al geabonneerd op %s" #: offpunk.py:1758 msgid "No subscription registered" msgstr "Geen abonnement geregistreerd" #: offpunk.py:1767 msgid "bookmarks command takes a single integer argument!" msgstr "" "het “bookmarks”-commando accepteerd slechts één geheel getal als argument!" #: offpunk.py:1782 offpunk.py:2029 #, python-format msgid "Removed from %s" msgstr "Verwijderd van %s" #: offpunk.py:1786 #, python-format msgid "Archiving: %s" msgstr "Archiveren van: %s" #: offpunk.py:1788 #, python-format msgid "Current maximum size of archives : %s" msgstr "Huidige maximumgrootte van de archieven : %s" #: offpunk.py:1811 offpunk.py:1952 offpunk.py:1971 #, python-format msgid "List %s does not exist. Create it with list create %s" msgstr "De lijst %s bestaat niet. Maak hem aan met “list create %s”" #: offpunk.py:1823 #, python-format msgid "%s already in %s." msgstr "%s reeds in %s." #: offpunk.py:1830 #, python-format msgid "%s has updated mode in %s to %s" msgstr "%s heeft de modus van %s aangepast naar %s" #. TRANSLATORS parameters are url, list #: offpunk.py:1837 #, python-format msgid "%s added to %s" msgstr "%s toegevoegd aan %s" #: offpunk.py:1844 msgid ", archived on " msgstr ", gearchiveerd op " #: offpunk.py:1846 msgid ", visited on " msgstr ", bezocht op " #. TRANSLATORS parameter is a "list" name #: offpunk.py:1849 #, python-format msgid ", added to %s on " msgstr ", toegevoegd aan %s op " #. TRANSLATORS keep 'go_to_line' as is #: offpunk.py:1958 msgid "go_to_line requires a number as parameter" msgstr "go_to_line vereist een getal als parameter" #: offpunk.py:1993 #, python-format msgid "%s is not allowed as a name for a list" msgstr "%s is niet toegestaan als naam voor een lijst" #: offpunk.py:2005 #, python-format msgid "list created. Display with `list %s`" msgstr "lijst aangemaakt. Toon met “list %s”" #: offpunk.py:2007 #, python-format msgid "list %s already exists" msgstr "list %s already exists" #: offpunk.py:2014 msgid "LIST argument is required as the target for your move" msgstr "Een LIJST argument is verplicht als de bestemming voor de verplaatsing" #: offpunk.py:2021 #, python-format msgid "%s is not a list, aborting the move" msgstr "%s is geen lijst, verplaatsing afbreken" #: offpunk.py:2080 #, python-format msgid "List %s has been marked as %s" msgstr "Lijst %s is aangeduid als %s" #: offpunk.py:2082 #, python-format msgid "List %s is now a normal list" msgstr "Lijst %s is nu een normale lijst" #: offpunk.py:2122 msgid "No lists yet. Use `list create`" msgstr "Nog geen lijsten. Gebruik “list create”" #: offpunk.py:2133 msgid "A name is required to create a new list. Use `list create NAME`" msgstr "" "Een naam is noodzakelijk om een nieuwe lijst aan te maken. Gebruik “list " "create NAAM”" #: offpunk.py:2154 msgid "Please set a valid editor with \"set editor\"" msgstr "Gelieve een geldige editor in te stellen met “set editor”" #: offpunk.py:2156 msgid "A valid list name is required to edit a list" msgstr "Een geldige naam voor een lijst is nodig om een lijst aan te passen" #: offpunk.py:2158 msgid "No valid editor has been found." msgstr "Geen geldige editor gevonden." #: offpunk.py:2160 msgid "You can use the following command to set your favourite editor:" msgstr "" "U kunt het volgende commando gebruiken om uw favoriete editor in te stellen:" #. TRANSLATORS keep 'set editor', it's a command #: offpunk.py:2163 msgid "set editor EDITOR" msgstr "set editor EDITOR" #: offpunk.py:2164 msgid "or use the $VISUAL or $EDITOR environment variables." msgstr "of gebruik de omgevingsvariabelen $VISUAL of $EDITOR." #: offpunk.py:2168 #, python-format msgid "%s is a system list which cannot be deleted" msgstr "%s is een systeemlijst die niet kan worden verwijderd" #: offpunk.py:2171 #, python-format msgid "Are you sure you want to delete %s ?\n" msgstr "Bent u zeker dat u %s wilt verwijderen?\n" #: offpunk.py:2174 #, python-format msgid "! %s items in the list will be lost !\n" msgstr "! %s items in de lijst gaan verloren!\n" #: offpunk.py:2178 msgid "The list is empty, it should be safe to delete it.\n" msgstr "De lijst is leeg, het zou veilig moeten zijn om ze te verwijderen.\n" #: offpunk.py:2181 #, python-format msgid "Type \"%s\" (in capital, without quotes) to confirm :" msgstr "Typ “%s” (in hoofdletters, zonder aanhalingstekens) om te bevestigen:" #: offpunk.py:2188 #, python-format msgid "* * * %s has been deleted" msgstr "* * * %s is verwijderd" #: offpunk.py:2190 offpunk.py:2192 msgid "A valid list name is required to be deleted" msgstr "Een geldige naam voor een lijst is nodig om een lijst te verwijderen" #: offpunk.py:2196 #, python-format msgid "You cannot modify %s which is a system list" msgstr "U kan de systeemlijst %s niet verwijderen" #: offpunk.py:2206 #, python-format msgid "A valid list name is required after %s" msgstr "Een geldige lijstnaam is vereist na %s" #: offpunk.py:2217 msgid "" "Need help from a fellow human? Simply send an email to the offpunk-users " "list." msgstr "" "Hulp nodig van een mens? Stuur eenvoudigweg een e-mail naar de mailinglist " "van Offpunk-gebruikers." #: offpunk.py:2220 msgid "Describe your problem/question as clearly as possible." msgstr "Beschrijf uw probleem of vraag zo duidelijk mogelijk." #: offpunk.py:2221 msgid "Don’t forget to present yourself and why you would like to use Offpunk!" msgstr "" "Vergeet uzelf niet voor te stellen en waarom u Offpunk zou willen gebruiken!" #: offpunk.py:2226 msgid "! is an alias for 'shell'" msgstr "! is een alias voor “shell”" #: offpunk.py:2228 msgid "? is an alias for 'help'" msgstr "? is een alias voor “help”" #: offpunk.py:2231 #, python-format msgid "%s is an alias for '%s'" msgstr "%s is een alias voor “%s”" #: offpunk.py:2232 msgid "See the list of aliases with 'abbrevs'" msgstr "Bekijk een lijst van aliassen met “abbrevs”" #: offpunk.py:2233 #, python-format msgid "'help %s':" msgstr "“help %s”:" #: offpunk.py:2257 msgid "Sync can only be achieved online. Change status with `online`." msgstr "" "Kan enkel synchroniseren wanneer online. Wijzig de status met “online”." #: offpunk.py:2262 msgid "sync argument should be the cache validity expressed in seconds" msgstr "" "sync argument moet de geldigheidsduur van de cache zijn, uitgedrukt in " "seconden" #: offpunk.py:2280 #, python-format msgid " -> adding to tour: %s" msgstr " -> toevoegen aan tour: %s" #: offpunk.py:2306 #, python-format msgid "%s [%s/%s] Fetch " msgstr "%s [%s/%s] Ophalen " #: offpunk.py:2359 #, python-format msgid " * * * %s to fetch in %s * * *" msgstr " * * * %s op te halen in %s * * *" #: offpunk.py:2413 msgid "End of sync" msgstr "Einde van het synchroniseren" #: offpunk.py:2420 msgid "You can close your screen!" msgstr "U kan uw scherm sluiten!" #: offpunk.py:2431 msgid "start with your list of bookmarks" msgstr "start met uw lijst van bladwijzers" #: offpunk.py:2437 msgid "Launch this command after startup" msgstr "Voer dit commando uit na het opstarten" #: offpunk.py:2442 msgid "use this particular config file instead of default" msgstr "gebruik dit configuratiebestand in plaats van de standaard" #: offpunk.py:2447 msgid "" "run non-interactively to build cache by exploring lists " "passed as argument. Without argument, all " "lists are fetched." msgstr "" "niet-interactief uitvoeren om de cache op te bouwen door de als argument " "doorgegeven lijsten af te gaan. Zonder argument worden alle lijsten afgegaan." #: offpunk.py:2453 msgid "" "assume-yes when asked questions about certificates/redirections during sync " "(lower security)" msgstr "" "ga uit van “ja” bij vragen over certificaten/doorverwijzingen tijdens de " "synchronisatie (lagere veiligheid)" #: offpunk.py:2458 msgid "do not try to get http(s) links (but already cached will be displayed)" msgstr "" "probeer geen http(s) links te volgen (wanneer ze al in de cache zijn, worden " "ze toch getoond)" #: offpunk.py:2463 msgid "run non-interactively with an URL as argument to fetch it later" msgstr "" "niet-interactief uitvoeren met een URL als argument om deze later op te halen" #: offpunk.py:2467 msgid "" "depth of the cache to build. Default is 1. More is crazy. Use at your own " "risks!" msgstr "" "cachediepte om op te bouwen. De standaard is 1, meer is compleet gek. " "Gebruik op eigen risico!" #: offpunk.py:2471 msgid "" "the mode to use to choose which images to download in a HTML " "page. one of (None, readable, full). Warning: " "full will slowdown your sync." msgstr "" "de modus die wordt gebruikt om te kiezen welke afbeeldingen in een HTML-" "pagina moeten worden gedownload. Één van (None, readable, full). " "Waarschuwing: full zal de synchronisatie vertragen." #: offpunk.py:2476 msgid "duration for which a cache is valid before sync (seconds)" msgstr "geldigheidsduur van de cache voor sync (in seconden)" #: offpunk.py:2479 msgid "display version information and quit" msgstr "geef versie-informatie weer en sluit af" #: offpunk.py:2484 msgid "display available features and dependancies then quit" msgstr "geef beschikbare functies en softwareafhankelijkheden weer en sluit af" #: offpunk.py:2490 msgid "Arguments should be URL to be fetched or, if --sync is used, lists" msgstr "" "Argumenten moeten een URL zijn om op te halen of, wanneer --sync wordt " "gebruikt, lijsten" #: offpunk.py:2504 #, python-brace-format msgid "Creating config directory {}" msgstr "Configuratiemap {} maken" #: offpunk.py:2536 #, python-format msgid "%s is not a valid URL to fetch" msgstr "%s is geen geldige URL om op te halen" #: offpunk.py:2538 msgid "--fetch-later requires an URL (or a list of URLS) as argument" msgstr "--fetch-later heeft een URL (of een lijst URL’s) nodig als argument" #: offpunk.py:2566 msgid "Welcome to Offpunk!" msgstr "Welkom bij Offpunk!" #. TRANSLATORS keep 'help', it's a literal command #: offpunk.py:2568 msgid "Type `help` to get the list of available command." msgstr "Typ \"help\" om een lijst met beschikbare commando’s te krijgen." #: offutils.py:153 #, python-format msgid "No XDG folder for %s. Check your code." msgstr "Geen XDG-map voor %s. Kijk uw code na." #: offutils.py:166 #, python-format msgid "Using config %s" msgstr "Configuratie %s wordt gebruikt" #: offutils.py:179 #, python-format msgid "Skipping startup command \"%s\" due to provided URL" msgstr "Skipping startup command \"%s\" due to provided URL" #: offutils.py:388 #, python-format msgid "%s is not a valid email address" msgstr "%s is not a valid email address" #. TRANSLATORS please keep the 'Y/N' as is #: offutils.py:392 #, python-format msgid "Send an email to %s Y/N? " msgstr "Stuur een e-mail naar %s Y/N? " #: offutils.py:409 #, python-format msgid "Cannot find a mail client to send mail to %s" msgstr "Kan geen e-mailprogramma vinden om een e-mail te sturen naar %s" #: offutils.py:410 openk.py:140 opnk.py:140 msgid "Please install xdg-open (usually from xdg-util package)" msgstr "Gelieve “xdg-open” te installeren (meestal van het “xdg-util” package)" #: openk.py:38 opnk.py:38 msgid "Please install the pager \"less\" to run Offpunk." msgstr "Gelieve de “less”-pager te instaleren om Offpunk te gebruiken." #: openk.py:39 opnk.py:39 msgid "If you wish to use another pager, send me an email !" msgstr "Indien u een andere pager wilt gebruiken, stuur me een e-mail!" #: openk.py:41 opnk.py:41 msgid "" "(I’m really curious to hear about people not having \"less\" on their " "system.)" msgstr "" "(Ik ben heel benieuwd om te horen van mensen die “less” niet op hun systeem " "hebben.)" #. TRANSLATORS: keep echo and %s, translate the text between "" #: openk.py:139 opnk.py:139 #, python-format msgid "echo \"Can’t find how to open \"%s" msgstr "echo \"Weet niet hoe dit te openen: \"%s" #: openk.py:235 opnk.py:235 #, python-format msgid "%s does not exist" msgstr "%s bestaat niet" #. TRANSLATORS translate only "MY_PREFERED_APP" #: openk.py:309 opnk.py:309 #, python-format msgid "\"handler %s MY_PREFERED_APP %%s\"" msgstr "\"handler %s MIJN_VOORKEURSPROGRAMMA %%s\"" #: openk.py:314 opnk.py:314 #, python-format msgid "External open of type %s with \"%s\"" msgstr "Extern openen van het type %s met “%s”" #: openk.py:315 openk.py:327 opnk.py:315 opnk.py:327 #, python-format msgid "You can change the default handler with %s" msgstr "U kunt de standaard-handler wijzigen met %s" #: openk.py:323 opnk.py:323 #, python-format msgid "Handler program %s not found!" msgstr "Handler-programma %s niet gevonden!" #: openk.py:324 opnk.py:324 msgid "" "You can use the ! command to specify another handler " "program or pipeline." msgstr "" "U kan het “!” commando gebruiken om een ander handler-programma of pipeline " "te specificeren." #: openk.py:363 opnk.py:363 msgid "" "openk is an universal open command tool that will try to display any " "file in the pager less after rendering its content with " "ansicat. If that fails, openk will fallback to opening the file " "with xdg-open. If given an URL as input instead of a path, " "openk will rely on netcache to get the networked content." msgstr "" "openk is een universele command-line tool die probeert elk bestand weer te " "geven in de pager less nadat de inhoud is gerenderd met ansicat. Als dat " "mislukt, valt openk terug op het openen van het bestand met xdg-open. Indien " "een URL als invoer wordt opgegeven in plaats van een pad, vertrouwt openk op " "netcache om de netwerkinhoud op te halen." #: openk.py:387 opnk.py:387 msgid "Path to the file or URL to open" msgstr "Pad naar het bestand of de URL om te openen" #: xkcdpunk.py:33 msgid "xkcdpunk is a tool to display a given XKCD comic in your terminal" msgstr "" "xkcdpunk is een tool om een specifieke XKCD-strip in uw terminal weer te " "geven" #: xkcdpunk.py:38 msgid "XKCD comic number" msgstr "XKCD-stripnummer" #: xkcdpunk.py:41 msgid "Only access cached comics" msgstr "Alleen toegang tot strips in de cache" offpunk-v3.0/pyproject.toml000066400000000000000000000042401514232770500161340ustar00rootroot00000000000000[build-system] requires = ["hatchling>=1.5"] build-backend = "hatchling.build" [project] name = "offpunk" description = "Offline-First Gemini/Web/Gopher/RSS reader and browser" authors = [ {name = "Solderpunk", email = "solderpunk@sdf.org"}, {name = "Lionel Dricot (Ploum)", email = "offpunk2@ploum.eu"}, ] maintainers = [ {name = "Lionel Dricot (Ploum)", email = "offpunk2@ploum.eu"}, ] license = "AGPL-3.0-or-later" license-files = ["LICENSE"] readme = "README.md" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Topic :: Communications", "Topic :: Internet", ] keywords = ["gemini", "browser"] requires-python = ">=3.7" dynamic = ["version"] [project.optional-dependencies] better-tofu = ["cryptography"] chardet = ["chardet"] html = ["bs4", "readability-lxml", "lxml"] http = ["requests"] process-title = ["setproctitle"] rss = ["feedparser"] full = [ "cryptography", "chardet", "bs4", "readability-lxml", "lxml", "requests", "setproctitle", "feedparser" ] [project.urls] Homepage = "https://offpunk.net/" Source = "https://git.sr.ht/~lioploum/offpunk" "Bug Tracker" = "https://todo.sr.ht/~lioploum/offpunk" [project.scripts] offpunk = "offpunk:main" netcache = "netcache:main" ansicat = "ansicat:main" openk = "openk:main" unmerdify = "unmerdify:main" xkcdpunk = "xkcdpunk:main" [dependency-groups] test = [ "pytest", "pytest-mock", ] [tool.hatch.version] path = "offpunk.py" # read __version__ [tool.hatch.build.targets.wheel.hooks.custom] path = "hatch_build.py" [tool.hatch.build.targets.wheel] only-include = [ "ansicat.py", "netcache_migration.py", "netcache.py", "offblocklist.py", "offpunk.py", "offthemes.py", "offutils.py", "openk.py", "cert_migration.py", "unmerdify.py", "xkcdpunk.py", ] artifacts = [ "share/locale/*/LC_MESSAGES/*mo", ] [tool.hatch.build.targets.sdist] ignore-vcs = true [tool.hatch.build.targets.wheel.shared-data] "share" = "share" [tool.ruff.lint] select = ["E"] # Never enforce `E501` (line length violations) # Never enforce `E741` (ambigous variable name for `l`) ignore = ["E501", "E741"] offpunk-v3.0/requirements-dev.txt000066400000000000000000000000231514232770500172530ustar00rootroot00000000000000pytest pytest-mock offpunk-v3.0/requirements.txt000066400000000000000000000001451514232770500165040ustar00rootroot00000000000000chardet cryptography requests feedparser bs4 readability-lxml lxml-html-clean setproctitle timg file offpunk-v3.0/screenshots/000077500000000000000000000000001514232770500155605ustar00rootroot00000000000000offpunk-v3.0/screenshots/1.png000066400000000000000000002457521514232770500164450ustar00rootroot00000000000000PNG  IHDR usBIT|dtEXtSoftwaregnome-screenshot> IDATxw|e3@Ez$rTTE(z+r?r'z"Q,@:B.Mlll$g63*n\B!Bqs{GYC!B!(1ckNB!BQ,B! `!B! !B!,8:B: + ZBHB!t( #AeZx0d2MzXtp*t|4A#N ˨Gmz\]5+b4`;lQNae!I~}R1P4S$,, 8,.sb qm:Q̽58 Dž]'Njp4wi]G 8Ss$z<';.20 9;,p:àf1;sa]{;ǹbVMeދQs!a`LkC1`,sS?[U_ƎF~~t{r%n^}av뢨؉i[k"%-WZkj^7MfeY54$܅6NKRY(R|˾ Iàx4c丆Jet c͂}I)2$%0'4g+xRԚyLo; (A61٦ 9yY{crïkӷ=45Q Vh T F)[C EՙYfje0 9X`j)?:bhnQO|+yf額RY630PК@8Նp>c65=>J" h]VRj͇3` )8$hy,k vq8oN0Mpnz]QnsCso 2s0f(,ΧM\?;d[MS3GJiàR5k{Q**_F{N- $L]k:HX\,J'4AR55Ks(#\_=vik D0k'OMH֗竦I}79{_VkjM/(hi2Ԥe4wf&oauУ$hE\Y< fJ2l~ PVw֌d`U RNasد55PTw=&[4m#~DoY}o?kmso1S넝U :JFssQrQRw*pDkP<&awv 3 >0M4z cg_ uW|i|:2)w\0 1 :(H)\bj(tR[L]"줧)4m~ Sk$a\3sGv^T;0ȑ:J1?tmܹ+\, \`*e:Z)z)pc޼ة`[MF泾@ EaA'x ub}\#/&/ LOZS?8& K2|tPԾEo6itE$ !NAJK<&gOnWv˹x&+B\l,.H5 l3۴; NJSSQYٽ5;^+ПDoj" xX6nGqE+?Z\=]35{IOhZ֡xc(׏8ky-Vk`%7L>m/s 㡭F+?׶Nzn43-nYŝ{/;3u*yxaWC1ɱS6Ԥby.Jnņ{< veb*Mz>ʀc9Z> xL".ê+$@d1 |eT>754Q'r|Vk$ddynܮT u=.~V48S!gHX~hjS@|&kqwMA ղ3(^نG8֔B;ur)5zZ\gʦ #@i\y@_mA'aP xfmM3j[/cÐߣ";<PS7=Q<+5Tp$ka**_N9ZNkz<~i[v4zoׄ(S~k(w8آř~#\0aOC2NNu )S lD^ؾSñV>|^^h&yUDA[`Gּ͠YX]1Js!mݷTKYKXaàrMb3֏۽^m#awoYq6i8˹|~gEcyaB+5 nOZ kDJX[5/ \ 4њ  4;x`9! 4v¼oǎ@s 4M"1 C{:>ۻ07_u/x="-V)`.~gBza95-wrkya0PkM[説EWQJ#Hs0Ћ vN9ۉ'z(]DMBd(so&XW<],pàV=]aG]YWkq(M 3GG &ULدцu[;أ) f9ڡ1Ozm 3XL5X5ι׮gfxL0-ShtxL0FNo&obVB)n|VSY}QB*JZֳ ZXSSXi9 "oLg ŷNwf3aXvkXɞ|v7Ԍ3_;cl' 컵 SMyWMNú7g۰w;akiR0mma9ddz o^y \knPy? }3sNf;cG 9_w! [0hk<@OCu&/L+fi2jXqdpR3iXaOxǼSOck?hMEe~Pk|;R7+Z`qRq}(t!.A xaP)dy..$#_Bp])m!EZ`IY* LckXQr4ٔcʵEkZS""(ۦt~K@ !J[kkt|rA&*%iMELFB!B\,B! `!B!XQupm>s]eT%B!DQdXߧoP)Rz>_Ʃ*fKzH<yl'YJ89p"ڭ/B!.l%ʈ8* :/LU\axl$u?P; .p6Q2. cL0pT!bL_pZޚ d%/!u[aB'pHl_"GVdg"ٽo_@gZGT&83GՏ9Bp^eS@!3;_<גdm䔹sB Ǘc=*Ts.Td]ٍd]QVQX(0s.}!:Ő˛Fk\-o'ý=S8'3hǩxWt Hu£Υvhg_ٷqG1OM^\`X{˺%B%`Q3GQ9ÉQ!}k8z{"N[:$V& \[ V<6%ꉳN[ZRzʚZmC m<ոZ韕GPQVٓcWSX_<ڿM[aN=iՁ2Щ'HyWZ+ZgwlǕ36LW1(~</e8-X(,B-n;(̭ Y h-cu r\tvu<BQ2,ʖ'Q#HQt5F^Z&c\þ5[뛲j8Fm'yA5*ŨX˻R"g7_*"H:^k1mw2NxWΣ9ݚ2;,ei\<x$' a gën<@TxlʳQ5b \<}(w4%4Uź8Jڷ3ŝ|TT;DЙiއ#g\fkB!JtEK_:pnE )QխVMW)wߧxe`Tirpfj/_\#Nm1OFz^kk#ۋ̓>sp6F]9~"$fBQ> 㬯sbPje*_5ǛdMHywmJOk 0k!R=Q n6_$1Jkw QBܿlыݥ/!BqEB!B\,B! `!B! !B!,HX!B!eA:B!B!. B!BqYB!B˂tB!B\,B! `!B! !B!,HX!B!eA:B!B!. B!BqYB!B˂tB!B\,B! `!B! !B!,HX!B!eA:SF n7-Z(^z.&-vvy:9Ha$$$v8qb1qDOxKҤIx_aEC-Fu"Ǭ&UX8B 'UDYChŊ#v(7Y {!Cyر̝;d^uf͚u^LMM/ĉv\#X\Z>2N(MRpuqӯ_?KFFFPDBBB8}4niӦߗi ;?_|Ƅ 0`@߿#.14GA2}g:6 'Oqd&~GڂBMBF FT]27'y¥O%=~*߆i !.T%jڴi̝;N: 6rSO=UbBKwWra6o\I ?YfQ\9zo͠A_,M9m 6ウgϞoҊŨTƢ!Mt⼲ }kg1O@ )N=NɰOP9)'Ȉ.Cy۳eA@uanʾ}dnɓ'PojOTTǏ뮻W&M|[xEȊ+x7ڏ46mڐkFBBGweݺuyvm|w$&&?3tP k7J)Feؾ};?} |-[/||eZ*&MbժUlݺ8z*ͺh߾=3f`͚5l۶o}'2sLyV\ɚ5k3fLp9c$$$йsgضmlVzQTNnˆfBW:Yɋ|ӇmL]/zz^qӞ1;)˗FV!.qj@6Jt+0LBB8pyFv-Zk׮zǎnիWױzޗ46mhۭ{衇 nׯ;Mawu޸q۷[+}4O'&&o]ׯ__5JnݺukO2E~6|=bĈG_|18?kݥK]^=ݫW/Wi7߬[h֭^۷oP<'NIIIz0 ݹsgvu|aJ 8PoٲEϙ3GlRGDDv cU_=^hQqĉM /6mۏ-11Q5J:$$D|:99Y;6}M4I'%%^xA7h@vmz۶mo }QvZ}ƍ>@'%%zm&L(g͚'L_xᅀ"QSG{GGjML{HtDFuk| zo,X4~}zɶ{ݨQ#=eCCC}_䫰/;S'%%:uUVusꫯ^6qDO?O?C^few8pvݺe˖~z/n].g;`{ Wiu/_K~nzǎ:))Invŋu5פIUoٳg嫴:W֏=u͚5urrrl&LAmܸq0w}NHHNSN:.>:Gr:TVZEVӡW?TqYP8xlN{|dobƬZÇh"/_ƍo$=,HKKCk @XXԨQcMHII zzׯg}֭[ǚ5kxG\tؑӧOfƍAWll,{e畯Ҭ(N.]Z*+~̙3DEEJ Tfff' c:'ރq|Ν;1M@||'\q}X"۷/V\%Grat8 Kݻ{/&&Ʒ8in:vJtt43f̠sδjՊ5k8WJJ aaa~eիGڵ `+Xua]v?+WСC稒8dHIIaСQF6mΝ;}O:E} àf͚NkA?~zދ$:::}E(ѣ-ZiӦ|Wςv3y_+A`S=hGX SE#`QWNLL u.111Nٵk^{-ф n{w&MPfMn$%%+Ho=È#hذ!111 2RX?cN5j-[dƌc͚5 0˗si=5\ס $_֭k!66Uoػw/ܹ3ugAW04MvM׮]1 ɸqXb@i fWi'[a^TS>}:W_}yرc7x#`]2dO<.;w2rH/@f;c٤#ۙ9sfӞ#ąJ͟{ !Bq) ::3ϝw\>³{ Bʄcz-SB!=@cԳXQtFcB\?B -B!D_-:P2VL#cŴ FL2,B! `!B! !B!,HX!B!eA:B஻~0 ׯϺu_~Y'T~}iРAm۶$$$Z#Orr2|MY'rHLL믿/nUqʄB#`QǢEضm_5]v 8'x\RkiҤ ?<p\[,]ݻw3p@nJŊgϞl߾Ν;XR^=^{5VXAbb" .n+/_iӦosN*T 99-[t[W_-߿_5^{m3O[tYd\jL+;vl2|I nnF?L۶mׯ_bҮ];BBB駟 UVlܸ=O[oMJ B!D(Q\s ?Ϲ[ټy3^FRݻo4hP?'|f͚{0o<۷/|ӦMy?~<+V(t}Qvm~i>c"""$=ٞz)N:ٳoٲ%J)\.͛7{84h@TT֭+0i[kXxݻ7=%K;KIVGݛ3f0bĈB/LnŋSHII)}]L Ɔ M~p\>^?>}}|yξi;C[_U_˰YfzڵN:pŋo;wnY6dM6.mzeX֭[zjkiݺuq5lؐVZOƍT}ѰaCyzZj111nݚ~~_|/r1g4h ~K0deeގ;Ȱ{hx p8V*zaÆ<Ӿ0u!**N:Ӯ];n7O=4dYt̄ڵk@-)ɓ'ӰaCf̘իiذ! 6e˖~u-55r+?\.͚5cӦMy>ɓ=3p@>/̕W^IDD[fȑtЁ;vݤIBBB. `K}rP>^:tNHH~}#۴iPZ;=uԀOS'''u)ΈЫVҏ?xѣGݻ+Wgݺua駟{tM2/Zjn~xxޱcСƍu5,Y קOvW^{[nn/~iۭuo:_}3*W7mڤ{ 3i$=e;vwL~X'&&jө 63gk׮vUT޳.]hۭ+VX)j_v4XWǎ]v-0`WnnӦy&lvock}n!L)X##gϞr{e˸;y@}7<{|糈jԨQl2{@sʾgWϞ=9s&`Zjn_o5ӑH.|DFFe>L-XbyUTZj1 ̙3y>kٲ#ֈ 7@ڵBVٚ5kFFFcӻwo;ҥKON-Zȷ D}֢E PLL=˗禛nO>gM6-bHQc&;wQF<~+egz^ZjQJ.]JRR{/ys)|ڗ: 񕖖V#U_9}L4)h !2Ҷm[FIӦM?~<~?,Ynݺlʔ)Ӈ6mн{w}<O~oV/_έZ"y#߬Y >|={0x`4h@۶myXjo:wVViҬY3ڶm#<ݻF … yiڴ)ݻw^M}|a7n qJII_fʔ)̝;3gp=Vƒwpnvgǎiӆ9sgRSS_Jff&3f ..~Āe˖8q"OwqQW_ 2HMp)kHiz]Rr!uA+h߯rZW@&dChinTEuf>(q:ϙss3b{_T6ys}%O_7oW_}09rk֬1M5tP c1(Q}Mv&::;wƌ3Z;v/<<xw[5WWWl޼ˆo6?ۻU`1ko{j7lP[bĈ 0Uc1|x)UUUpvvn0),,DaaakaCv1coc 6C`1kGc1c f1c1.1c1X`c1coc1c f1c1.1c1X`c1coc1c f1c1.1c1X`c1coc1c f1c1.1c1X`c1coc1c f1c1.1c1X`c1coc1c fN"::ZT^;;;ggft:A  kpZRĺu!Zmk$RjKǶmZ; me=lkBcBh4yu^koYٴiaX_Zd27o5zh888 ))Ci5/2M9sW\Ƴ>XdddDttቨ1frn:$%%!''qqq1bDk%^okaВa[k{KzwjY{Zxc.uk]6l@TT  ą W^\͛7o'k>˗qFሉ#G[o:t()z+'IJep%x{{c0a[; -aM]{cQfsdgg#;;+p pPPΜ9T,_k蝻C!''Oƍ/z=\$%%ŐgԩǏJ)e777 558tv9bV(X`#G`ʔ)FyZ-A {{{CI{g,,^?Ξ=;w/ ~+G*ScUCRaݺuDJJ hZbժUHNNj=۶mCrr2Ν;>/^k&WWW㣏>^ǢE4եKƚ5k gΜY'6ScŬrmo#ɓ B uVT Ezz:z=bCḼRTLy41X;5ĩY/L CsZ233)44 B#Ahذauّ \眇Є gϞD-jTEv"h4sр͜9222ˋiȑti3gNvAaaa#I&?9;;=͞= MRۥl*y{{SNNM6GAAA$=3uNkTٳ4Ƥc*rNJ SRnn.M4IRɣj)??|||HT{diiI4sL?\t9$7z-i错IJRX.^OrQC9Kζ˱w1spر$i40`%%%;,9((h̘14p@ںu+S``>Ӯ$f5էbc68qD"811:u5DLJ233I49^___\\\=HFuK)o> `o)22NަlH$ԡCϧ|JJJ=Ƥc*IM/1)<IIIƏ?CAR9ը2kyy9бcGǪ̏ (sj.#Zԩrrr$q?S׹sѣ \ׯKGJBj5X)cGoYPUUUXĺz*ƎÇcذaâE0~x\vMrLw1z}0@|˕ZNdd$u>ΝCuu5nj&sm+fSjʥICRrjrWS~26LΝ;oooh4t W^?\tIR9*Gc9>rK SmozZ֭C>}aL4Q1׹1 ]M!z[[cG2{TVV"!!2e v 777Yv._,OޙZRlقd\t 7n@>}5v1R\\ GGGc8w\˾ߥK?Om1/)Je=dr )JKKqU /B0jEEꦽnkTshJ[rIplܸظq\CII ]f[[[tMR9R%FC}Zk2c?~=2&L=z 99%%%4i>mڴ >\#Gacc?(oZZ̙'''\r%%%(//UNMM J+WD׮]%]J̦| CJJ ~GL4 ...xpE ::~!vލr>)))FuiMٵkz˗ϟNJ+&11{l,Y?P(O?aŭRrXD\\ʰi&k5`1{T*ddd`ҥ}UkaӉf1k.6lQQQܼycK#X1cDTGNΝ1Z c1LΝ;W_}`1`c1coc1c f1c1.1t: ñm۶Vرc!4ټvvv-YqZh^ k[YGv/,(h@珍*bdh@VX8{4ZѢ;: @%{Tl۶ Zĉѣ233ت1)J[z !T*,YA0v`ڙ2|7[;{0،UϧT ׶E2E(]P+Wr[-Vu.|qu|9VS#a55/Cc`WG\Ũ^_dd$Ke;GaSܼ鐛ãIM4 EJBppa.ZwENNN/cڴi3g\]]zfK׷o̱7PSS+++V`ڙ7oƯڪqz Ea~.NBQ-j~?/AFί#ebPs9wOoCE.,G-KGaa25j֯_={W_ٳgg_~*+V@^^:gϞVaʔ)߿?akkYf!%%DԪq/_ٳgq-j<kkk 4[laذa?vh8fRtz81BERR\\\  ,@BBpL2Ũ>8\}jz˨샨+~-z(t߀*J߿6&z5ǟDqA3c3&o{<:Ռ8q OKL9a9j)>B;n9ǠĵVS>Dߔ\\* YQ}) 5WqPP*{]w7| ^ݻ8((gΜw̋;bb̝;ؼy3rss n¬Y$ǣRqFlذQ%$$ǧaįcχNCNNN>7J1k\]]>84Nl]999SG{<S}6lmmaaa۷o剈@LL?z:ebj.Nb겵Exx8RRRxxzztb(k֬1ܞ9sWX[.vʕ+رc:w ggglذAr{$}M8qjDiii4f8p mݺ)00(/eeeѮ]Ņ4 =s4`@ޔCӦM~QPP @< VKFQHHQ|!RI$ 6̐gɒ%?' 4o^'pϧP0`M:rssiҤI5sL ///#GӧiΜ92&MDL4{l*,,$777Ccǒ hhDN}bggG ssb>5F)F,=+$3 ia{e,_\ԨvIV,_!e~%){8s{@V>!UԃTvNd5CRzNY/-_rTn2SR|4Ei^"UgIaeMn}q交$REUh浭|;+Y8{5*si}'F .; a󙙙JC ybNmڱc{N|hٲeFǢhΝ;C}!m۶I'|B~ah[<<<&L@={$'''ZH\SJ иq護"AۛONT*U׶mH9.10"&i4:J"[[[ܹQ|||HPСC)33Ǝ+y<9000g3wѣ~1bۗ&NH]z=J[꒣ŎUFcgJEݻwot9qzt"sj֔LK,1~ǩ Ro9~zc~-EFFn;v(ϱcp̙3tR^zQaaa pJJ T*ñ7yj]`tߟ [[[# #F-T*v:eFGGSNM1SGj6>~PU@]@)mIRXvF?214gRXvª,\e9]r:A䟠$ %4K9U IDAT!Ot&E~πoscX8M?jZ{ꫯ_w˗ yPSSc}uØnNWT*ܹ/޽;m 55ܹ3ƍOOO >%%% "ddd=ztHLLDFF8WJIʚiJuu5PQqo'۱cGYgRNun~kRsSb[Ο?"їkb*cxZ޹sIhG}?Qwꫣ62WVVEjjoHt |Ν;jlݺ޶[} 0i$\tl2kC̝[I Ψ?$+,U]Һ7Ëo,Jjf\*F1kcET_1tx (,ݟJT `xBPlAi;/⩉(?&X%Tt[B\peee8q"222P\\ Bwc ݜJܺu 666ݺuõkDSO]P=%1cp۷3_DTTF . 33\:vU;ÇǰaEa&ell{\kcec%v-;coYŜ9s#,,w[iӦ_~ bbb y2220tP 8 C.]UןgJK,w#imm{ ԩS1~x|ڴi^u,Xpttܹs B[SSbxxx@TBVcʕڵkeVWWcҥpttŋ5^86xj]@^Ԗ*wTvOP}5.% TU|T))*C0^]젴z',_?EP e>Ptb6[jn_w @BCaɨUg]T^wkP;Be =PXI͛'O~YBqT^~5O QF Fas]xgpB 2k֬F:yeeeFl-PZZ A$T 11"٨Q `؄ 0{l <z¬Y@DKcƒ.1q-8oooxzz/0k;wB;K'Oʕ+pB,^ ߿w6[ʙ3g'Nt:yzC1XII Ν`XXX .uE<ƵSNþ}plܸ6km+\1֞y?#aԨQX~="""p1̝;}&NbIebٲeە͛x}X{T.cMfP(`$$$ //G)S PXXÇ)))PTcSNw}?~P*/z=\$%%(_PPΜ9T,_蜛bbb\:t^^^FyP\\BBB_$̝;ؼy3rss n¬Y$U^^lC*((\En#χNCNNN>7JKLe.s%cnɹӭ[7b„ F8q"`cc#*.]Ƅ5֬Yc=sLbbExx8RRRxxzz֩|jŪU\o]L9* HOO^GPPt:@SsG>yj]r1<6UrĮ?bgbnjS)0FY7<'1GB|fIޔCӦM~QPP @>>T*ݽN)nj)'((h̘14p@ںu+S``!}aj>r%/)ucbr^sIL]r1/&b(1놘$b1pDԬ)>>֯_otoHYf^'ZMwuD 0*ߟ9Q5PL3f̠|ݻwuZ;ftcǎƘriɒ%ۏ?8=I/m]TL]R SsI旔L#:y}%1uէRƼdnlnnyN"fmis$N<̚U~{ncprr2>x /ĉb7;ctԩSb_sCsuɵn=whh~UrĬ?ss\K>mꘗ%#vmcVADߺu 'N+bxЊ37[;wUUU9V[DFF[np9TWWc֭F?Z's*++q- ::Ǐpﻂ׮]kRٿ;.\}Wbر>|8 ???,ZǏjx-sK"ݻ1c DEEaصkɹRbsw<_ s<ؾq ~Q5}u{1ի2KKKѱcGj4A4*tPTxO?ۥKVVVf˵O<˗/7*.s*++HL2]vZ\SL+ƌm۶vjT\L0gѫW/̚5 D|eZBLpΟ?-[􄟟_cj~75Ĕ9sGXXXrij!ǺQեK9kkkYp{̙IHHO°}vbbb\:t^^^u3|t:ظqVcaaիW#==z-GP`HHH@^^9)SjŪUT,_NYSNw}?~P*Mqs%%spwwG|||q???@R֮nݺ!77&L0:>qDFtY3g;~ t˵էG}dXVkTB7G sYΘŲExx8RRRxxzzΛbc@LL!?z(kTS31&85k#Aٹ;v젰&,f<_~%]؞={(44Qn]6m۷۾};m޼YR?;Z233)44 Bͫ3~ 1./Ĥ+ @ƍzA ooo>}:effRܧ=/̍TGiĈԷo_8q"͟?_sDD @>>>P(hСIcǎ<ߛz'N-SϞ=>Lh'YZZR^^پ}($$pLJ233I4gΜKnՋ 6~z}ijرcFy;Fۉ`ߟ<({|}}Irqqc Dv֬YIVݻwM\|ٳ' [[[ϧ#GJ1sGRbba#N:e4~ 1./$96jI޽!>s}4c ϧ޽{7sDD8q¨?bbbD_\c x8 N8qjɴ<  EΝ1n8xzzb())!##`mm1PTڵ+ e=ztHLLDFF8WK.޽;rrr ~7ܸq(_~{ncprr2:믿ݾ}6666CHHBBB򕖖Y S%/r^c6 /'N`8.9ە/bԩ«˗/#!!AR9bNQQjjj _n4~ ~oQUU @yy9бcf˦, IDATܒ1;99u֗bU\\lTNqqLJ^g<c4GB^^n߾gy/"0j(\p'B֭>;wغuw^cb6lh"?׮]O퓭*㕕_B-Bao`` {Q7xRܹsd\r\c6֭[8q^yٸ8y9EDؽ{7f̘(L>vz"v<8/o8P?l EeSק9bJ2:/4ua\7c`6ju^AZZ<<<`cc駟Fjj*{3[lArr2.]7nO>uʫDBB"##1etnnn)))ѷo_ñΝ;ab8::sttĹsDu5\|#F}&/J)--5zԘ2kaܸqpvvƐ!Co>9UkϞ=ӧΝ ٳQ17űrӐrA9cB߾}O4Gα1h zꩧ u}xݸK.ݻ751ƚoY?cѰ1RSSDڵk5ja\SSbxxx@TBVcʕڵQ9&Lٳ1x` f!??_R<9?gϞP*XdI6}´iЯ_?111ڴi^u,Xpttܹs-Al_EIKKèQ[[[Ó?17u<QSS 6 33EEE˗rJ$&&~sGyQKSZj.98#0`֤<PUU1߇% 00nnnF/;99!;;[T.)̍ ( XXX`СHOO7P(駟bȐ! ŋ;K/Ν Yd JJJ`aaaO? **m5j֯_;v sg}'X),,IJe +++%ݿ9ܼyovkڹ8{lkDػw/a0=q_N%Saa!6ښ֮]KO?g}FO}6n(nnnCK"///IetҥuE9skj)::>qqqbt֖)%%)>><== ǎK F!dooO'O0رmB PBBё#Ghʔ)^Ч-O%1hzBJKӑ oɒ%$]֐GOIIIKɴ}vԩ!Z`:ueggӑ#G(((jQ}zA ___\ΣqJMM%A -$$ARb}gdmm-{iǎ;:t#5ɥ4N4*D)!ӌkGBrI1KHcLCiR)#r-"tY2.EGqS2f=^{eB/^Lb1̤۶m~`iiIwA322hff&ݿ?uuuU:_,‡ePʦ@3sB*3>iӦ7ol޼"v˗! лwoPJyuA$!99Qծ]B__ 1`BFLLLgXf =zbl$ aeeׯz9r$<<}Ta%}v믿ưaðzj<|P|}!H0`@^ 麦L4\zUm/^Dii):wP[~eͲQҷo_\|;FFF,HD4޽{trttDEEƌ]!.\e˖/D N4h̙3vvvYfaر 'z }CBB~z|g1cwNӧcልw}[[[2?r{Q_ _~={Ă 0j(DFF̬سg>09_7o6mڄ#G"117o|l8g|XhЯ_?2U>!!!Dذa  Q:Xp!&O#** )))[{n:IIIj/,[Vaaa(++CBBn߾]:wi(bɒ% D" ,@EE$''ݻpqq%JFm<==!QZZ#Gbĉ5SSSr g5:_ \ȈB)P3JKsss}ֻ 6_rӼ<,ٳ֛.8ٳg3Ώ9SSSS7|#7N+wΝ;5%z]6Hŋl|PBusskqy:w\J/]DP___NyyyGN'7jGuuuiLL ]r%ݹs'Ԥt=s̩udiƍ󥮮N !tԩ/ƆY&''+e#WWW_޽{SWWWotgFB!wn˖-4..N.7%puΝ;;| s'NK;Ҽ#477vڵtd>_~4--uɝ_͛˗ .]|Gg֬Y4%%ptt4%Й3gR@@noؘt7nм<+)]tU&իWSB|2%(G.SN@ͬBJT]]Y5vZG%ٳgSB]~='+44Bh\\ 6d>ܰ(Rlx lڴ j69=z4__QQ!铚z쉸:u666u5n޼)yyyjFhƏKݧ][P׵W^Gff&wܜӧUVVӧOc5jΞ=ÇwԍN:SN~:nܸ:{Fn@0jۧOܻwŸu^|[nnnnP(TNOOd󥦦fT˗+|HNN;H$HMM*%+??ܱT*ŠAUVV6yMݻw_x}}}@vСC >ē'OINNRRRp9\~R,߃ ###Bo߾ɣTZ[[P ]]]dgg7*nݺ!!!A\VVl>HNYiI<'''|7CZZXYYAMM zzzrYFnf9rD~e #p%ڵV2$˗E.]`ii^ E5Y=߸7opGS^ ÄuF:[^^\n-0`@Lݺun|vQhmllw6mu@utt`ff֨>}ϟ?^Z?mR?~aذaXx1|}}kj_ӧ())AAAlmmѧO$&&9BÆ ѣG޽{qYP\\,7wHJJjPee%?CCCDGGɓEllimebb;i۷o_.o߷|2?P}Q&_ Äu͆sѭ[7s::ujٳ'***p֭&j:EEEv:n:t耎;r@P(;7=YHII m|QJqU\z;vG}{0ѻwoqB"`011%[[)YGh``-:ý{{ mmT@Y222`oo B 0{U===<~ׇ ;N (e栴=5?022BJ"55;v@zz:lmm !舀Q׼]ϟ?[-ХK87oɓabb{+СCx5-[`r#@͌ --Z/((Pܹא KKK3ڵa7FUK55GE5|}}d899A,#''߿ۯ_?sMoZزe qY :W D!C@SSxב|Ň,b̘1ؿ?*++sssBZ l\xXz5`eePJrR>ĪUЧO6i׮`ffMMZDjn޼pcǎ022{@7o8QVVYfk׮pssŋQRRt :;FFF022KN̗@ @xx8 Ν;o߾Õ+WOӧ]#H NmFF***0w\XZZ⫯·~۷s#ٵݗ*UVVJ.ݹO?cΜ9ի_~E!qi 2Dڿ/=+~TWW#>>^)}o7n|9ٽ{7MaÆO>AXXQ0e]t(x>yptt@ :BBBо}F~m8;;J׍ׯPSSӡ'СC(,,Ddd$`jjaÆaԩʬƂ `eey)\OOOFFFrcƌѭ[7wܔY6:u@ }TOΟ?K.ŨQ℆_Ŋ+m${yTTT$hiiȑ#6RvBCMkѢEӧ1sL  2L{"22*kɒ% Ř1c|*44 [U/_'N?L>])26h6^xp;:KR֋cƌ ] pmb5Ǐǘ1cphiiƊ+xǙ9s&-vڡcǎ׿===,{QVVMӧ9s`޼y Wo!$$xӱd5|9w455rlݺVJFtt4c7=[NTT:tM6qif[0gG,X+Wx8;o Xf Ξ= MMM,]֝رc[O> ^~$N Э[7=xv)!FAAF M0c TUUa͚55k˗ҥKr???B$''cڵ*״iЦMK.Pr&++Kei AAA=n߾ͭVe _ 4 ,4-Bzi5_,_A(қ7o ,4_ܣGVׅX`hֆijjb7jùskٳg8w\k`0 o3Jҭ[7O< mWCB `ԩXr% >`0 X$_z0 `0 l,ʠlh`0( IDAT `=```0 `-`k10>lX3a>`0F)z};x8ڵk1cƌVG"=zX?RRR@!aaa{߅QF[CS|رc2'̙3ҡ9tf%!_|Ek`0ށ3R̛7666@FF [[A$A,}mmuZk8;;bbbT"O"Cll{ehOO?;?oF-P`0:m۶ĉoZf;x7nƍm0 )ЌfG6eJH$ի@]]8{, u?Ȧ{OSNVۈN||<!Xx1b1233m6pqX7n@xx8lقk׮!<<> b@vEEEqrtttj*?R.]Ν;k޽{C,͛ؾ};ڷo]M+̟?Ychh~YYY8v|}}AAJJ GޱcG\r= B>6mW^-ig[[[ڵ W\T*ő#G0f̘F222† ,8pÆ 㮫aHMMENNb1g+ׯrssqFH$̝;۷oGzz:f͚۴ivŻx"fΜ(ǍG";;'O/&KOO+o}}}^;-J5j?999044T*_M رcLmmmdgg#??:u899!66/_Fnn..^m?&믿FJJ csqvl$$$X)PU[>RBHMMT*1i$YeP𡡴~*|ѹ1mO:tJ`0T3Z0!!!ouHJJbٲeܽb111x)VLL =őJAjjjxzzrSF'r4݃v튇ˋkʩOǏ#&&J &&FpBL<GTTRRR`cc#jV\gϞÇҥKժUpvv˗/S,^X!*ÇXd ((">>> PQQ_y;uTL>ÇGll,;*%G[[ z `ԨQæM0rH$&&brB!b1pB8qk֬\rȐ!H$Ō3Ç+'V\H1aaa1e! PRRPxϞ=ӧ;?a$''JMFcŋQRR t`oo6mڵk(.. 48}4~GMy6.\cccYǏUf]hL|>@*|Yٶ ((ɓ`0Z(Xh H(!.]T5664==PTWW޸qqdB]\\LoرBcbbSBٳ)!_ॏ7%Pwww:c JϏB|5,D"J9::B̙3@ + <(@ !T*Ruuu PB)!Y4//S ]BhJJBM-wtՔB/_L !LJƷZε$=477vڵ8֭;믿͛7S_~Bթ%Џ>vܙB!@7mDO:%'gt׮] iаZ9w;K>\k}nqttSN522ɩѿÍ7RB8q"@CCCz)gddDԨ.555ΔBO8Aco;uT dw>:[ZZRBͥ&&&(!7균 `kkK !̙3 הmɡyyy?twwS}o6T7dvOVCOC3 66Bh\\%Dھ}FےXPMXA)[h19"w\YY +++AOO t YYY*EȳgGkii)o~~~HKKC\\\~C… (//6t邢533@ @AA'OTJ333\t v->0x`A(}PJ5 q:t r粲`mmWWW _z R5u^FAAn*< ѹsg#88XZYYYǸq}v;Mal?w_~:_uuu)<#==___,YNNN w . 99Y)=fСHOO-tttPYY7ZѮ];Px.{P733)h]BCC[l6Ξ=C"<<WKY t?XQ7ob011FOޥVVVr第rRݻC(гgOQafQFx1x`^ׯ_׺)rG޽{1|@$a޽ TիW8s ܸM577P(DYYnqȑMC &{uY,,,32?e`` VKMTUU )=c> ;L>dTU_i qܹy`0uJQQ0zh9r.\@ @=dhhh' 44@Z>U|PVVرwANNhhh(͑Rnݺ ΨjRxƌ8p 7MPٳ'ϟ*[\\8;;#..9994NeS-N>YfA, ػw/j);yptt!"6_:Yf!22{.z޽?#,, iiiz*ѻwoZJd%1110` y)KOOǴi`mm^lݺVÇ!}۷o‹/PUUuS>u6$$Ν e˖ AEEttt0eܿL2vvv$%%999ʂ //&S|B={c£Gr^ϟ?Gtt4|}}g\v[6uV(W7jwTVCrTY_i ڵkطo<<<o3vf:غu+?~ ŋiӦLJIҥ |||=>UUUXlܹ'''}… x F3gBGG;w[*C6Sh _VZ###hiiԩSXv-wm֭H$D߿?,]gΜG}sssDFFryU3f@UU֬Y sL˗/FWKyٳ'Ο?d|iiiJ)//ǤI[رcXpV||<`ѢE7n e8{,ڶm$a˖-r3wƥK/v`|8x bbb0bܼySANDD Oռ[2rGFBBձo>~ɓP(Daa!R)wÒ%K7I0oBrw>:۷/{s-ܹs~GUbÆ W^Ghhh] ;L!TY_i d_Cg0* MR)=>^ZvZ̘14kYf))) VC  <<DDDJMbÆ 󾖅ѣ]cVѣu"ddd*gggD"?UǏǴiЯ_?\UH$n4ײP̓ O?СCVr/&(n>}ESS?3> 2)Ќ~VP | qƍV"LMM۶mÉ'ZY&o> 4˖-̙3q}ﭭ{-Ua0`F@={YYY^l$$$XAԩS ]ի!vqqxBHMMT*1i$dSRT D^z4j]vʕ+J8rƌ#'""wq%\r-+W"##sm6}bdd 6 -- YYY8p ]WSSٳb=ZNFDD%&&"88;޴ivڅ7B"ŋ9sft7n=lz[V]>!66yw *7!Xbnܸplٲ׮]Cxx8' |2rssqEDDDm۶\xBxbbdffb۶mё;v@FF233~*mÆh;ro|;v;lSN7_|Pm+uttj*?R.]Ν;T򡸸YYY(**#++ %%%(>i+pz^p[lLJpttę3gpwuʺur$%%l2.͛1`BKKV/JZy1bbbDLL bbb[x1 .k֬k2$$$ MSNHMM1|pテ\A!//9s`gg]3gK,Z^^^ׯLLLM/={b5j"##affyi&9ؼy3C D"-f̘ >\)Xr%"##1b믿Ɣ)S'_!!!Dذa ! TZ@u!}?Υ_RRPxϞ=u3f """www򤪲dܽ{...rvަ. OYW#^OW^wеkW<|^^^ AOƏ?'O૯BPPB:(--ȑ#1qD)R\rFFFmúhͻmsCuE;wۣM6vOCRu)߶r…i+p>>!!!X~=>3̘1 :5el߾siV0$X`AؘtCP]]]z Gutt%%\jbbB~Bh||̱cRB5MHD !4##C᚞ɡyyy‚BO:%W"PB]ty f[RR #""'8qrǗ/_ ,tBYTAsssi׮]sn:s+ݼy\ޣ$&&i&[~=ݵkB1114,,V}Ν;GÇ+w>YfєF >z{{7nlذQPȝ۲e kNJ?T*|Ի "w^UۛB;1c%PWWWG !ΎFFFTMMRSSSL !r%>>B2{lJׯ℆RBmڴ@ <߼7T|̷6nH !tĉr9s&o_;)ӷmR[[ FGGs]m: >BYie|ԻO[gQ&MpeBS (RlXYYAMM zzzTޭ[7vNNݻ8~8UH$:n߾ͽ=~8(055.^|)wϑ#G+++U>0x`A(} ܽ{Wŋt>===|G߿>45 ~nݺ!!!A\VVֻ@ޞܹ3W@YYR2_|nRu#??ܱT*ŠAxʲ!ӑ/_BWW]te>*++K\c}OEE***888 00[,iPXXxMsՓ~K͛7\wmTʻ]6VFmvU^GŸq0tP$$$`СMc>R)<'''|7CZZҟV63d!%%Νq!r4 a`FS*/)):>ص$jjjJµׯ_7kڛ7o֮];wQQQ ޼ypzWUU]oLg>|͞WtА; } ;k,?~}X{4D}u>էuCrx;*![/vQjj]bƍ _q9oP(T:GQƆŻurߵGjj*0x`@$!''wܑ)y;_|W|طonݺѣGc1bFٳgsj4G>|8aggSbܹpssãGx 0zsڵXXX@[[Z*C 555Bn; IDAT`ff sD5ݻ… HNNV^@GGGҥKj>cFLJKKڵa7oɓabbKAAYYY=8>ݻwӱK. wPȍ +a4EEEHJJѣq\p=z􀦦&dee Ά%K@CCj> M!(++v؁;w ''x9={ڵk4٭[6*pvvFUU7oޠ8~8BBBо}{^5ZGzZJCa֬YDDD޽=zwt CZZ^ wwwV\~ӦM!>}:ғn`6l͛/==ӦM5>|Rz @MXj>|X MMM 8xOZe}6q!xUUUVn]}􁛛DYȘ2e `ii HJJBUU/*ENN***)SXdIdEGGc„ lj'sssoU RWApssv}*V5577GFFJKK/зo_4ܩS'r-,,]ad.>m%G5 :tKPZZ wwwPJT0XѬG QTT{rq~zm۶Edd$L6 mڴᎻt5ҲpUU-[  B\aTTT`„ pttDQQ~G߿Q`5 ,ʕ+qyz @ZZrmCCC9r%%%8z:ԧ&MBpp0n ]]]ܺuKС-Z###"((qj$x"11QngϢm۶HJJ˗/ePwƧ~8 44NW^'͛2pvWK>Xf Ξ= MMM,]Eo8w455rlݺVe!#,, ~~~?Xv-wR?uQ\\aɒ%wߏ (-^^^ -qmaC~^Y+w>-'O(,,T**6V^pݺuѣGaΝصki5̙3rj>!(>i+L>>>>> nݺ9sAU4> c& _mmmɓ'pqqF-˦M ??V!HQF5 ;`0bQef0-444u~ `0l 4HΞ= Hj0 `0 ) `0 aS `0 u `0 񷀭f0 Ž%a0 {f;.9sUb=zp%pߡd_H$BУGZߩr5 7U`[ot&FB(|6嶂5"8vvv8wnlذ?C:w Bzڪ4>))) Sr {3{O?í 7olll~ jmCD"BdYZZZ Eaa!T}Vs˝װ Q+=a+'lP @M.^=HUo/_Fnn_MS^^ӧ͞VKҒgggD"?ɺ03l 4ƍqlmm矷: cjj ضmN80 gԩ6n܈V烥%Tݹr F=$NjByzj!M߉JTU?BMKdvZDFF*γgϰxV㽅هf4+:::XjΟ?TK.aΝB"55RǏǤIJ;v;lSNuuuٳʂX,?Օԯ_?Oĕ+Wm63;w!ܹpB[L6 DEEaٲex"R)6n8w!߿?w?!JI6믿FJJ csqP&JH$ԬXB`cc@ BVX7n <<[lkprrBll,7rEDDDm۶\xBxbbdffb۶mё;v@FF233~zzȧ2޽{C,͛ؾ};ڷo oy5ĉAAbb\W^! O^@X\3׮];n`TTRy8z5UL6T? :_EA]K_hGf5Bh/"hZ FS^Оo Bt^131ڣ7TihTbB}5]U4zFg5gJ!^܈W*95]FKIIUBDZZGDD ::Z\bb"zzzoE__W===ҥGrK,ޝʴ6l@ZZp 6L.θqpQdggɓ@ X D8pRϟG޽ylkk]vʕ+J8rƌ}ڽ{7/_K.ʕ+XhRv100T*ŨQɁRւu… 1ydܿQQQHII\a6.\cccYǏŋQRR t`oo6mڵk(..[(//bٲeJ qqq8p N>k׮aȑ{.CjcǢ=o9N3pq>}dffzɥ5sF. 99wޅ BBBk0,, eeeHHH۷ňj &&FSlݨ+-#s=xyyk׮x!`ll 4hqix +) XR9'N䮙">>...(--ŕ+W`dd>6lʋ}ʕ+3TWWcXtR{C˗/accH2dڷoH$^<~111\G111AJJRym%%%}uŬlzmO@\Thw6cЦDf/C|(Ϡn>j:\h 5P&*sA_BGhs-?Ud'{7uֈPhC+JQsQ***pM888XQyQQQ1c ""pww}! PRRPxϞ=J钛 HGGiikk_~AϞ=`5 033xzzbʕĈ#SLQ bٲe߿? :w >}:X|wm B^^9sΎm>} X̽ē1a$''Ǽe1ʁ} ,4K:sL*(AB!@hNNˣuwwz)9YBϜ9SkZ7n:qD ri4??S 7nм<\CA]]?B=w%P RBh@@%а0uVΎ8;SB]z5@WXA !tŊJD":uT D"JR+eCKʥ!w|AJ...r畩M !ݝΘ1B+jggGP###Fuuu)uvvz N^||<%P___ Ξ=B8zG۴iCP@AUOy_;dQִ;)!.\6l:gBfی ~SO?Bj džޝ7Udoӽ,,; **0*:3(?72** n"HYҰR(tO$9?jҦYM&-_ ɽ'眻sBs]˔j DKԺz翄ejPw!w/B M? ˅2 !PC"PK $f͚;wbժUؼ]BVرclذB$%%!,, pc0|pwӧ "##-u,g.rj[Z-׿mRn>:ݺu_j֧W^ܹ3L曒m ]vٳoux_SS#ʪFuu됐ڮC ܹs7N 'OP{)))^jSN9s}vݧ}vL&,9n#I0n8M7Պ/Bv}Rl-&r|gxچMSَ3f((衿cgmj#331ʶIeX<0B_W_ް^:c,(Bz2]G@XjP׷Q+++Z:wE7iAJJ NhvtL!}^׮lݺFCEеkWĉ銊\Ų)99@ۏ9 ЦM|'nrիT*Ο?oאlϞ=OBV7ߔN ~mf{WFcR(ر#^uT*|ضm3@R9}EoW_c7Gzؖys[w[طorrrЫW/<#֭[q9[<##]vL@.QUQcقOPA7hP|X 0hPxGO5zA@g111t_&XkxlKnNrjkFqjI?ѣGc0(Vnn.{l(P;ՙ3gdGGII ^{5uEii)8>>3f̀l;u>ŃSQQΜ93 >#YM7݄l\wuts( hdT*F=}СC8p Ə/555P(HNN`qX%yسg^T)]JFF{9wS}lj5 @ӕ'Oh4SNP0I 7 : ?CѢK0 B aBtճIuPtB>j{6=z +Wt\#22>Wv * ܳgO=zԾݻwW*h׮˼|\-_:{rWXX :TV,:+J 6 <v\ѠSN~Rh?7L>]vu^%j Szg{ GRc̙Oo> 4tcW9DEEaʕ8t ~GX?>u֮]qoۡT*ѣGt: :T:eee —_~G]v߿?~a]t FW]uT*222GN ԶyWxSkAϞ=k׮l6ÛcrssQ]] ^Sٳxꩧ|+==wy' HNN֭[346tv;kutꫯOBӡ̡՛;wFzN8\Q0ؾ};FT{kLO/DE!'wBZyX %Pi`Z|%!f(T: TM;i RED퀈h 0ktw}Qh@_ Xm)Ν;#11 l&L… Ϻl ?D1zh̝;׾|6mu3fp1p X~=a6`-k={6yG=СCEK_?@kE\\ouZ8~8  6@Tb^={`ڴiHIIQVVJdffbضm4BaZ1n8<b {E᭷Bee% ggߧgSSS9s 77 ӝF=駟PSSJ'O"''aytR\x7x# rN.]½ދ;vĘ1cо}{]wvH{i~ۧ|l=DglڴI`DEEARaƍxW {l4VAAy9rO=͛s'OĤIyfDGG۷Onކl_| Mʒ:Oy=,6n}띺˭lO?'N`Ĉ>}cQ7n2~gJ 侨޹5*}?({J η#LŨ.g?~wrt7B}$Tߝ } GB}=(n* PifM0 Qc""(XNzg:dzT<ЄJqw`Νs&""\`ʵm6t:YҥK7ؗXz5222i&DEEa#o!&&ȭKIOO`m^2 .%زL&&O\,]=q^V¼y0vX|1j( xc=޽{#++ 7nDEEvyo+WĎ;j*/.gAVc>AT?Lg֗6o=N1) H z}lu{QQQAOޱ<)d<܍&~WqW{+==]|A..iʔ)";;[tׅ'o&Mtx1x`t % IDAT5 ã>={w< ϟAy܊ZMKXBe>QszN:_?ZDcǎxg}K@`Lw}0#ˑ^Պ'vaƌȲt\2Çv31HMMmt}\Yx1?aӦM~(P֬wDr^Au`LDDDDDD`""""""jS@$&&`0p` Zh`W.-- -ST_Fvv6 ^F0K@{ss7`0@7*L  ,Xc:k_\.q\x77;e: ) L& *Ԋx㍸;0m4 0/Odggcʔ)~]׺ntYYYiwfKssӝw:@Bff&rrr_cذa-Ш ]/oÖ,iz\& Q@GAW\oJuoZwfKs Xz5~<Ξ=3g8myzW_ŋ?bx0vX?~>dž?5sՇLMGWv0`#;;se˰g̞=۞nXb=/x)S ;;C u됓,P(b֭?q?| 4!{;wJϻw駟0sL(}݇L>|۷oo68p VX]v!''~-Əu>R/عs'~a宺zYsHrJI듔͛7cP(;|8 yѳgO1c |Lrm={l񜝝-Lr:~L]6:7|Sk.YF<~7, "99Ydee'|K>8ł r={a޲eĪU'}>zy/Lk<;Z/oas89qj鉽B0I*VբO>`0X{86mrk&VX0oʔ)`0~Ǻuī0/K,4ijZ;vt!m61k,|fΜ)ɓ'^vZTδh"sNRx Fr!͏?(fΜi?qD':v>iiib۶mܖ-[3٧)PzI?r o&O.wJ_RԩS/nݺ7|S N'rssc6`sN#ȪgK>8lP9痿Ӿ*5=wQSSjZ qnMٳGWKbƍ^5|peaM{/omjpS˜85^!x0blFUUB@HH=]?UjjjgԥK|g:o ,u]M6nɓ'w^@\\1o<`47n܈Yf!33۶m~z\pAj8ƬY0tPCR!**^ob`^;///Gtt}JJ N<键Gj_t!9@^r?x =_Rvڅ5jFA B߿Iꓞ0>|iKX q(r 9՜غT*TTT̙3F*X,ZsEYYYSTծ979 ĹLTSV~U*r r~@ii)6mڄ[oYƩNgƆ y|4h{x3gFBu[d bbb+ĉX,X|_ioWھrjl6K+97O%_Hmer\y>|8-[믿OƁ/SN?[nΝ/{~z6q(ݾy!BXYVSSR!==? &&ƫ}޻woÇqM7~ssے\&j ލN-FݻףZ?~}qקO8qaޚ5k0j(gϞXv}Yaa! 0tPjjjuV,YƍCTT(JÆ ;#;vܹs(..FNdQ_naϞ=qQR;߮];9x :w:TOoݧTUUeA-o@ 6Zسg 8XC Ak׮&ox7?/ol4Պ/jO}}۲"##ѱcG6qo޽=T*j۷)ڽ{7F[o>۷+nV 9ac7Y.sW ň3/߿?nݺAP`ƌɓxw0d$%%aȑ{}'r?;v 7p~! @n]vaضmPXX믿!nX,>} )) @mPԧOo?*)2LN,6L00 .{C47Bqؘ@Fc\n2/Tzz:FaW_9ѣG{ז-[kעo{@6mO >>'Owٳ!]UU{L8/S>VBee%>(F#rssrJ{2L>s΅FѣGCy} /,TVVbݺuعsWyl۶ :k֬dҥK7ؗ:t(222PVV5kָoRdɓ1o<,]aaa8zh4Kزe t:Oe^H܆+ΝM62331x`dgg7y}N:{-BVVv˛:\zªU? ;Sչ!W"E z0}tw}0o<^گem޼C=G}3g;t49s9w^6۹L ź5m v=(-__߱cGCDD~ZlL2%ep ڵ+R9.dff"''_5 usεǏ>Ʉ/EEE>Rx㍸;0m4 0/Sm94i|s#Gl6%ѣGc̙xGpUWᮻKdggnx*GA׮]_`߾}5-Mtꫯb1}t{;v,?U^xkjj|WII +@AA~7i}ZvnNRRRp!7flڴ ~ӕdDGGcϞ=MZ\ܲ뇵k6y}b ڵ 999o1~x򈌌FFGGc…s=]ZZ>f̛7aޔ)S!C`ݺuAVV',nv|w8|0~'̜9JOӧcǎx뭷~4iyUVVСCȑ#^ѣGVdw]s>dffؾ};x 1/݋l̙3e:}z IIImP 퓖+WÎ;k.<ixn3aرc8y9sn:8p{ouK, Ǝ-[8bo)))y7tj;n::t /^P9sK,oL 4ȫ:kZ`0[oرco߾^mmW_EVV>M6aڴi?8 6l`0~( {hKصkۇ{:tp(KVØ2e -Zݻw~SO=%{Gu0$''c.-gQ3nj&n1sL*ԩSE~~8pOegg)S\&YF̛7aޔ)SEFFׯꫯɲ˲M~X`esؿ?~HJJ#F۷oӦMv޳gx-[LZʫ|Ν+~7-v)}ѽ{w땘( HMMuZ6l0q1f RRRĜ9svow#Gݻw|giΞ=[dff}D^^pz˫}Rċ/(.Ā/^{5o-u&~7Gdc{oY Fp,_!g}Vdgg'$ѿ /B! 1rHѫW/_?ܧs'**J 1d_|!/_.z%ڷo/F)ƌs="::ZlذA"%%EK|"##!]>}`}7nIII./5y%>ʲMC "**É'N^!TA[>Cu]k׮  K/d] }Q[Xf ԩS?s=XbQՈŋqm_ԩSQXXnݺy1|8qm۶Ŝ9sѣGouN:7ohDAA:׿bʕ< l&PԩSO۷oqW_k9ZO?HHH@AA1|p^b瞳?u~'_v+O#??VpQTVVyyy]SRRAa{m6{jk|'qjKѿ<9smY?8tCKヒ>Z}饗uV@YYN:%{G>ʲ߿?N8er5 IEGGc֬Y:t(R{^555?z#..7oSlײlT****pTVVBxƍgggc֭8q"z-Է~YfBff&mۆcpWDll,>lwYr2^YYY8s nv,[ &L@AA=NӦMСCFpݻ7V+ۇ$a׮]pڷo_o߾X|Iw^{Jjj*/P{/٩S\]rh͛d3[l7|O zUV9qAV[ouXV]]0WJJ rss y oɭTY6w97ۇԤ,Y+8q, /_$ !+Vug϶cTjjjPZZ8㧟~P{Ofaaa.**ӧѹsgT p7cРAkpbΜ9=zW1f;v`}jmxbcs0qD,[ wy'222:bccn:߿?Xo!}߶GBBCY{%۽{ü}ߗ]H 0#G?̜9rqޥK,wPո.ҷo_T@[l`m""j>85RaÆw;p9SN>YUUw(]vMRhDHHB`СHLL^{ulGRꫯvjѱcG{܉F٦[nŒ%K0n8DEEa?c .]r#""v67go1o矣SN>}:v?ܫzr-#<FJJ5o߾ϦڃHmѣGCRJn111h߾=M\u>B޽-ѭ[7:ݻ7qQlwА_r,$$$8o#wԯ^""j: XV?~Æ RZsǎ 7܀8t:T*o߾֭ f̘&)fϞ=둒x`ҥ_|]vE>}0}t|`„ ضm&LS]ફC=={b…G1c6o7|ƍÀpM7߇bAFFOd̘1:u*zvaҤIB //Ov<>#EBBJ%9soisxf<PPPL̟?۶mٳggyy9Z-ڷokpBX,A;[P}fV{\uUxGqqVǏM7݄$ : ./G||C Umz=t2}J%^~e <+1k,ڵ ϟw/"" ;wNsH_DѹsgtMp.99z/x%>ʲ'C ABBڴic_&wx%"aLMC޽7;w94`˖-8t&Nh_cMӏO9e٬\;vU/;/[j͛c믿Ƈ~QFQ8}Ouݼy3ϟ IDATk֬A1sLP(0||'xQXX;la0]+ |w4eee[駟bÆ 7nz!1{uV|ظq#ۛ}ꉜ|<ެc>ZիZXv-V^իWcӦM{a9rUUU޽=r Fxᇑ &`߾}NO[O`Xp!;f_޷o_X,`SyG޽{CT lmo&233dٳ3gtoÆ ؽ{N>}dߏX֭#<Ç;WJJ L&6_r,Bm۶//}yQ[Z6`׃UKOOGDD 2e x :ԫ)=Wp,`S8p׏!jnбcG<Q(f3RSS] F[x1?aӦMXtiCDDD0ŬY]""""8 U`LDDDDDD`""""""jQZDDDDDD*0&"""""V0 U`LDDDDDD`""""""jQZDDDDDD*0&"""""V0 UPDD*&#"%T9oXF/}Z,!ud>DD[9VcLDDCu0] ""0Q3fkZ$gZS' M4N @M.o24NчPRV版`"""7 EPfKm!.oBx ""jPң-_vn%5qJjO|[5j13-4>4l-$= B ЅiknH݈­EycR)@M5CWXL(9_ʊjL""-DDD͐L•U yln c$]RI)-n'"bLDRסҿc#Ț4?vE*?EXΉLF|ۅI\s*H5iZ]8;~uS:qB{Wps)c )}d އ m]iVjc5խ^""jLDDԌ72ο-*!:hC;E%҉du(w?DDh %tH3/q+ߚs: LSRtmQ ]-DDD1&""r O[Hͩ7,:?fZV@ ct"j<==JjiT (+ \0Z_0UHoX vMDD hDDD DDD̀bi6DDD+v&fIJQއD5(.Hqs 8Oa1KQI{FǏ;l1ZFK7(Ƈ;c\oT̥G=~6O>+LISf]\Z2MٺCM&k">X.`(u';c Qv]]6g C4uҥDZu9vt|iByZ>.^DQVgz+:, A)4]:t @M-DD-T0[P($WZ`jfol|8\s%`P(DIiI%`,7",Bz$f""jLDԂR"DFaU]YkaFDD[Y*<#ݢ1L2M}Cݔ ܔN_p!Eƫ_waˊʊoc?M)sc\Uٿ( k0T{w!&"V'x @moX(?:q>Ε=9:3/~w?W|"0XK$(}KKg [:F,n*9C{uwxsDV>g IЅ8_(@z9MㅐvE/z = RYہMBѢv}rs<}ж}첪+` sj:W8>G;EHW| 9cLDtT+4Tע5Ÿakp_o[hݵq"\똯T+\ŗevCx <jM2FH̪ҡJ uPtC{} v޷;컴Orxv OtzgD\g__]C:HӾ,YqGU^:'Ya_)|c>ҏܬ{=rN>J4_(M2eU%uxyDQw֍ӦTPW^RsV\=_}?DD՛_VWrr)>:{y#)etQْb jDD`"FRiTZ|&1`"FhըcQ;DD`kVDDDD\.k%.nJl@ZFFɿD4 LS24}[%#Iu$ EDD`""WeJ옅 ?Bٌr)JYV_QGM+ץK`Fx= 8PL#*[9Uny6*(6 FdhuVsOBqE`C(7qyޕ4͒iVwi5GEֆecLD40RDJ1@r@|a DhEDD`"j9ͻXy}YY""[ˍ&O0[<`G&*(C]ze.;9>[uI;k3tn )/uj?u?_Uy69MDDKZ+/J? S%Ӝ=qKǫ-/WnuU |0, H<ݿ[6Iέ_rWTX$ApMsA%aLD-VYH )T*R[[ W޴6|%@l[@Ukpku)>%Hz`""/D"\8*h]<*x~+/<{ReX.PVYUA+v1Ŵyq""1&"[~+p Zvd)%,=?QHRUaMz/_jg%"0LͩWzAR@S)TVG;^I+jlU J+*wy@m%>p:5^$|Mjx& xѱ>Ԅi0&"FHHMgʟ/6ԉ=Zj9xQKHaͦ|.D,' ϣF{3iì_kuum5.Jvk>3cWekymہ_S^!NՕUV׈H4%Œ\wXopJsٜ!FkLC(&hNJtL Qz?'9I2MHHVsM%N2OAluy9|(n[(+6_?RwI)-wL>S s4]J?wO$Ӝ:^|em]쪛_Z&}tuX/Խ,PL`mV2'b`nFŅK'"jiLDDDDDD`""""""jQZEDDj+[L z5 DPu|v}ᄅNT~NruU nłh5Z|uaZ] ]NB٧!?i`LODt9bLDk;nGN N:ٽ}_*9]&ݮsx?>'{t"I2ɹsg+ 1;+pָR \{6A;s(q ة^:A~V{08Driuj(!** DJ0&VI~)VX}_ӼN&䎫V`&F9VZm"U()OYJPjI}цh gz6l&ք0]=+JJXV`?m#Etޮ^eEDYj{+p[m[; | J!y{䝮±j^Wݡ媶z"\vynwTyy4T*a,2ݭ2VT!:(UhSwEuEC [5aLDR eԼldkzɿ?ʢk{?pf^ߛSҨ'| ~Ue8%8XJ%!.ZA5f(e4 [|rbd t&""rQ`"j~?,=BoѱBkwWBۄֽv\2{ tzh.ӜnGzc9 q"mHd}B\/$$}XT4I%.LI5VT{Vv]^~%(,kpjd}:H =y0DcAnW\$Fk;/ڷ wfӮRmHvѵvt7{*m{un6ç?]tѵhU _aًu_7nWo:,4Z>!sU,{~\&IֈK'$Ӕ];~*mDՍ@^c0,S{keM, Qْ Ķ>""jILDZs/IOСDKj^jjױ60ZP-M0kPN] RɈP}ADDM0J_a.DRl*c-ja+0eQ T<[Rds5}EQv Ů%տ﷩J󇉈e`LD͒BIf/"uY5V*H=:XJe~fuVV+FKV!RcQsȸTPe{ _t0TG#PTV6 ,L6mpl)F"P3MZj%:ף{g[5j%: ED?<r(mUt-DV>Meը=ZŠr̕P(S S$NDDAKD,) "M$BtPH5_NF Z@.KZJkHHZDQjUe5Vde_猰Zk/ĘVNcǾyWVKR^E{s>eI2;\` 7GAA,hKjg+l=R+VjimS -ee?d yVެKVH{Sa`]eؽf5%g]7e:,0=(%5w"F+f}9K28>gL|>~}x a=]U*]‰X'L.;[1Oioqt_P߲Oz2:{\mf\2e؝U`嗶8, @j;vW5~g^AD-L(%LĔM*{wKkQwFZRV WcUj5T,O<9(sei~_hmG{ #K1L]cU`_@U`RZ*JoaDD 0%=w[ T IDAToVrG <&]nvM΀_7KhݡU,UZXpJ5tjT `"JJJ~UB'nMtٵ#(u11*7}e;fS@k9SjZFnJ0%'DIDiMIΘlԝCJ }Vdfb6""=&D$g|zz RH. '#"JUMDI2`dVav٠Fyr~1c/sǂ<xɓ0bl6aJ!"?|**ln?Cjj0Azk5cXfSttygNQmܙͺu]Bwvw?sEs|eYv>bs[h>ŀۥ?ﺯW#O{-n8/=<4Ɨ_ϳ%h{_YL]^KXB_Ɩ@1Yls\KI,a>f>aX&kkwь_|vs._`t=_XR路tqDD`""6C~v4cF磭ݒfE 0 [n^!Wj)}M\#Z ""1&"aqWQ_5:3Ѱ%9}aa-ߜKsiq QbLDDc73 P_Fk"*&DDQׅ1 '.CKb/<1oG<*+B6t(p 0E/V(5;]>,Hdj).)9ra^VM-:%k˩aso^x{'"RO+n?Y/i<s5L*] i8'Y=]-,*cGR#[1a̻Fs&Xk+|_}3< MlbRWQ C5/qR`"\XXm2EgVLZ=Sg"ZtywbwR zW{:u$95yww ""ejsk.+#Y<4-InT%"",DD$ę3S2V.+DD$ıbBb`?tQcLDD!XM?m^Q*cLD)kec"-IoFcۚ UEpD7(8Bfή~V((|nNt;|XmÝ='LJ?qV߰H#k1]wcjf-Ɯ:uDS5avsg~/pY8K ́~kϾ^//^릭Q\t:{ǁ /+˅:~:B0\4/~3˜iBj/MkLQ;BJ++1meV09Y~{?߾-ΤK1۞TCD8( 4%$T'ekؑcIz\661V-Nӕٜ&`""J8SFsuhna9.6mBwb\_ĘBcLDI郏 c`⚊TtI;vng⺿re$y_[r."",DDGTeYpjFhٿ6P 8RIf\KD4L%u˜2/)\&$EQTpN˜ҢL_^ +udά,u*%D/dƃw JOVl? %&٪e#],˜T搆ٷ( 0&"JSz~qN\MIf.]ދЫ',s̺DDR "snjMN֦&O(7""_=)AZ'm =˜ޯfmmj c,Y>t'F_;ByESpctaZ_ Qe񝽸cEkvۭ-=KFyn[-\>- Z HA&t`|ʻʼnE.a p7Շ}|~Cs=k8|UdY5BtraL^|-:1>nk'a X}y_ߞ w Qz 0хp\PҷKDDN]$A(%0&"", LDD"8u#QM~X>qƋSʂe2 c}C("LHA] $]$X7zh4P %'5Kg +CD<Qj >k[&; 6 t8G"&l;$w]xTxbq YQh͠U[ -˜O טvSq00:_ Ԋ. ECo0e,#"#sKg/SBWGw[D±g4|I:;ܢQ*Re @R"??0 .7 0QTL(wzOz̽..Ve{M:yxs}ǍVb n1=E˜_8 טw+xriʁD87Wn++DQQJ8rK}bq>y}Z4|N r|/ߞ˜҂Q˜>?HRj 6X 5bpL0}?_I\=nٮ,.шꀉslLMD(L"t@;N*0#U|Љ lb7$R !!(ژH@@Us, j' Z?T]0fa5X$' '-Zv!93GD awlAna̭Vp҃ܜ\:>%||Qc09F5-_KD!%zc6:Wul=z2ohL|(1&"J́UmTmLb%՟$g{4846˘,=^GGlqjQrbLD@v#Mɤu5%g5Nޘ S2"&""JLbDe.WADDD_ah"JJnq>BC ШAc}ͧ7*gyY] J˽غJi94&'?:|=_W#G|&L[0"qnyE>1 d}OҊ;ɫ:++2v j.9=ԁ;}WK8f׳n5cTAުܶm$lLQtcto7b59t5>S bZYCϝo(zUT+QgYuY!4刈( V #z8i4(}8\3xŊlmb67^g"Q@jξM"cCoOWԎGfv@[ ̦n犫Tˉ2`""Fs&J*0rUvW>_Sf2%Q&ci: Ɀ1.c<6r-=nrgw7JG;j1;:V_u~\:`"JY*MN,,Õߥmm =,c{U+ rF]N$x8߶aLVÉ`u.QzrDjhCt39zӤdJ8XU. _P~ ,LK1VgDiMO st7+GD)1L.IN *_9.p,R‘sPâԐc5w(&L 0%K*}<rʰJ,ۺe(*, xCְh*qq'(6%DJ'cWqc]uX`w"堯uN4J*pSX`"tRZ_ULTSTiKLnw|x}UFv(v >m'&p4X|Gmw7k0Toa!)n=2>c/oߑL|j0&Rxw&"JWL(~?/ūJ%z8۷e^sc8nQ&bLDI)P`CEExu_CKc˜ cNj_sF ,9Z{`/IePQ&bLD)aHo&wBN|++1Z1t'c׍U`"4B_c;tg~T"ssFhDOfs+i*]fNg&L 0CFoaHT-*pOv[,|壅1{4ǡ%'U+\uc2 +D6$H%iCUdJ~)R _7V(LDT8z߅8ǘNst9Oܕn˜]CK쀾46MZwOJŁIcݜE+DDLv&RVfCrʐlFIꯛ)Ù3Q$X&ƨ%qU\-vaLNxWƜ^Sd7{̭f]bzysU{njܞv˥Ю@Zk5w܇'ecne8g${(1Mo\B,'/ss[| ;*Bhe4YB_m{|iѳʄ:qjξ!%#Ce ]s2V20^CaIv&""J7e<䞝6њLpQL=t/O6 cj'gtz8 0QqWv,b9Y&""J#L2Lni͉nFhn0ttaA12`" +˲4US3Qp 0Y\,9В~',fksKr Cv`4@?n },t[e\f˜VKKDD Q9l E;H ű]eT*% r޸'DD9Q/wmmmjxt&J#^o9k/\!ģ hk'BD8;g` ^[xdS1w#U>Y<3|?kV%+&DDB+l72e- tq}-:jj߾NmV'P"iADDpL(uv &3`Y+xiTR `"4ȱ|v5:KW5ynO"2v'L=tYdhBLDQR_3M-!v9hNXjW cØ7ҹ^}W:w衉o,zdSuxa̡'"aLDIISQ46 6 enʙ/ݫ7S5' DD RwBUoH1&,neB]«[Ug}V(b5TzH*pio1jOt3^)DD"LT$_A'AUশ䟡81%"xaLD#݄G4댕M.CD7¤Lt(6}v!͉>MHDDDICps;"2cekkk""""JʬQf`LDDDDDD 0Ewߍ+W ,@UU#mnnFUU>a2W $IXp!֯_w̙3/Ga؞~UUUczPn'y0uT\}ظqcLs̙3;vW\q0VᦛnBAAOXb.\a@ꫯ[oEMM jjjtR۷'f8=g~޼y3/^/3fm݆]vtvv'? L27p>È8---;Q[[e˖ܹs2G d2oE{",ihhO?I&%k׮Ŗ-[''īy睸[bzSO=QF\0f̘aƼy3`Æ t;pIOp{NFe˰qFlܸGqQOC=ݻwcݺuضmJ,_=wq|I̙3{… _"Qf(v\oe˖h4=W;;;ӟԳpTWWG?fΜ_׸{PSS 6x;͙3=$aŊxQTTw8+9>#X|yX >S̝;Gii)n&;}YDKe]1cq|;ի}Lip+8~<@\~;}Q̞=o_^{-L =\.O / `ʔ)˱bŊ̟?gƺu`{b¹}vDKoe]Yfa͚5p\سgtbǎ1{L IDATl3?яP^^-[9,P(P(m>l\EoCss3-[ٗ:!??Wlvm8Ih"֭[?0֯_|ڵ 3gľ}qFt=wP(pEßm|z ?B+qjkk=ɓ'S:zhDkXr TWW;c=*gx7*?V ~^J_~9Ǝzg7pF㮻򩢟={ٸꪫ0rHL6 ?tZv-&L믿޳/~>q1}tlذ6mBMM K//H^{ 'Od ysRؿ?6oތs***<ݳNy;w˗/։T*j'č;qcǎ 8n0c2__x'8COOoߎ{{_|Bᩬ7֭[_ o&V\UVaǎa}zTVVݎD:68''477㗿Ow5k֠kEEEX`~@[[[ڼi&TWW{>3صk~Cx{݁Hcƌ[oW^y\s V\ӧO{/ F㙼j˖-TF6@8Ǒ$ [lwߍ\lڴ -ݔy*SI߰a=ƈ#{'|?m۶8m}ͳ]VV0nڵغu+^z%#½@N$gFje]믿?<֬Y` A޺u+V+, F 68q>(/^^x7ވ,\>hD#"""VԩSؼys8V<\'N$IPTV&K{.Rl߾'Fի1rȰT*QSSݻwwq`ĉ?{^NoRD___ {kZqY̙3'KuzO=ڊ1rH|!`¼y0o<;jjjo߾F*??_؍͚5x7+}">;r\Z~z=z=Ο?]vᡇ 4 vѣG^f?1?IDDDD'xXd5n8ر_=rrrV= V:;;c߰~{xg=:UUU>:Zoywn&FcΝXn]KVcx1ydTUUa߾}ضm瞈}/pW`ذaCgԭ/ÇFQ=}݇UV-f޽{сx={6F#~m( O3\$رcwvvȑ#^[nO? Պ#G QZZu ,I>,Z믿Ǐ̔gØ2e ۱vZLq?dYFJF,DDDDCܹGY(+WƕW^ ͆۷cAg6䭷ނbwcG FT`0gAKK PUUg}VK/t7oz! q|I<ܹslٲ* """Ԣxʋo9uuu[0m4?yc~X,~ƫ-B֞LrJl۶ [qu%I?;DDDDDat&"JeMDDDD@]#()eYNtbJvLt#b%9t\nQLI~()IN8\.\. ,()BR=ڳ'IENDB`offpunk-v3.0/screenshots/2.png000066400000000000000000002422021514232770500164310ustar00rootroot00000000000000PNG  IHDR usBIT|dtEXtSoftwaregnome-screenshot> IDATxyxEwL3BVNAEna9VnDXtuEqYqdF#rQ&$@ 7$C& Iw2I|^LSS]=juJB!Bqs!ĭRu:(]܁! |;0lEG"_4R e`z`2A'[&.`-aԜ~_i.vt0C?qaNkaP=5f;0/5.[Ձ/FMBToC `ày>cm(N?a,T^1 9Xax] w:ؐϮGwg~Sue˭cu]Z5#"%ٳE՚&K[GCq'M,^185R`^_43)85uaK:\5-\<q;l՚JgqaXEaP42yerNC`1C@=JRK$n ]be$ 0k *T`62ٯ}ALLM﬽2rSi:a,cop|^Z?V)躬Գ|lLㆢ !nvr(T*3MǕ vh48ц  _jU:}_;P0e5 цby4`(&LMoCԔU^)6w/s=aa0P@OMe`xPLs򚒻Sl+a`ּ2v|'w֚q.@# E p wG}jbj9b-2Sѐp+3Rj6`v6){mEMBk,3Eۡ5 iKs0Cqإ5*?A6扩(9̄S6) 4RBpw+x0(rw:pOz84 Bq_wP2SS[AaA1aY yj+ePLuP.SV=f(1 rDyT)t2ٛTMڤ{ɃJ`VDhOŕho&m%S 4)ųScVXXC9M(Ħr/bfs5)L}l/FR40 /LϡMU-Ni@25oh\%y+Տxi]yճI⴦tHXLef= [&Y].k4ц.W^Yx4+ETWS6NrS4ͫ1 ?{)< f2֚ZS)Z;r_BdbaА=ARjxLI~֞6?3T}@&[ <9Ak\5CQ p?pZkw0+t3w)29g%h3@iW)`VXY :c-|R7+Wmi Ka4Ud fuS I.2GFl_Z8)]ZV)&# w+SKwG,m5Qi#8UiRP9}­f``j4|q>W;p0K'Pdޏ=4VU~TDu{FJe:~ fwNfi7WTSnµ4-cϻLv6Y9j'T`6 R].[.glZ's)IF2iaݡ߬5urc̄!KϥYt𘡘gjS\Bknr[Wf7ɏel7M2c"`Q$2?FSe^e>n Q P>=GD 7rwB|uLZSݔ܏&PtVK.,_ _[9 mvZ20oji3W< px@GWO;4 ^X)kʥ[֕nYu-9rڷ3Z 0agClhmk&?iFxRtwCKsϪTǬ@&?}%fj 4<2 }, v};O= ^O}8߆9 Hsq:OxѺֳ"!nvhQ$2~ڧr?Q18餓1[ 2{cƓ3;mƔr {qI>Yɣ/2FR*^[m=àB*i2eA69LW^OGo1hѾA!isS?t+Ip9ɓ. ǧeVُ"ן+9S ʺ.r[#SrGEkҕmX>K>f1(X,d:nYT3=là HHȡ"=[;6Sà9?hy0W+~՚˸әG~k{T] @|uxDk+FIx6O9~a9^2Nc%ͧˆ(B{t'x+eʺA`#.+˴fa[+C+Żscqi>ķPO;u6Pcx Ϲ4 J@)RО\f;7M,S4xDZW}qe~"yNO˃r`jN;PוD''>Fm]V'x.+l%;۽rF:7 !(2MF(wy?SL^0Mӧъc &)^) eL~bV"YaZaj::ݳt뵦J~०fMtjMdaP[)ks9: ~@Kg{UC)fv%S(xKN,EOMsOBQh9 qے(T]Ҳ>T!B!LB!BQ"HX!B!D Sj}4iÊ;ۖԳB!Dђ(V~zR?=͋'cu xr!guDF2ԣti8G)\%O[Jc՛Q۫Y!zB!(Z쨕wv0Wϑvl7)yä^Q.gG7\ A>e+c^8Nʮ&[R6qTm^\'[NsuY;Y]b(EQ3CQ+ɗB!-O:Hc!FPutW<+1Wb,t3r]Щ emP!(z:TtjiqҪ e<1[ϩkI]2N:n7Q.T ˧1u'HM*|fg!BD:Hl0 )j*0*VǼpZ6kGTN%Q5F\]6ױێTѤF"i[?ҏN&%r~&-v-*@^tn hR'[8ʯ ^GT@t%\$};bqv*kJ~Gz:?Qϟ;k 4Vj8Lvz x3wOqU*${É8 Rc֒k!ibI;\5AJ2Rv~Iw_oJa*X.YwqP+puӸrnFH3k8F;I;.Xܸ 諿qe`@ O]e/i Q>:V<+1&z<@pwʵ9xB!J y (bm*{Yc0*Tt䏶#pTktf1*TG_>_~y3$y˻gˣTq?iRv- 6U_qR;1k0A R,C_;~}h{fO^VX3_Ӿɗp~Mx>+)j%=ي'/iq)WqTmqG[JW(J_3y$λ;c6=XaӨT __G&A5R~(nyճv=H]~Õ~giYjtx̋'-/O6Ϙlؕ2XR"Q*'e;/ v-aF:EMpH˿f|*[r݋({WsTJo%i~.4V~ٍ5ꉳv\ӦZHʮԫqgC k|SJPvJ5DʏlJNFkFIaWEJGD.#5jeGQ6QJ1~=1*T$m?67sD.!g)6 :5NK&i䧞oB!DI!`Q$;B_=Gm8CF`81;y0 PO;p+Tk@9-g#:m8RFe7_IAe!/oytETA /O UV5>|xUj5$@dNu~.0ĨR?Ϙ3:upl Q:dׯ˔)K,uŒG!V `Q$@ZBntT>ujߎ8k<8̵o^rO @_93{$Εzfìr{ݣaש}t_#;ЗNgOiS釿LU[.O.FƤ}玀<ϴ+ EN=EoʿߊwZLE\'p"(rXuXNǹנ+kk pO5mQn+晃u!+ї=974WREJ@Z?# ;K+iϵ\cv3 =QҤ/"``aK>4[/zB!IXs 褋89zɗHٽR-ħNYARÕ#m~ Z'bu2RO9,KK&uJ~=_QwsM `8=2k}b{NF);b)@Uꡜ<˕Y |ԃ9_k8cw`Yٕy.Ju|Zz/mk˧=I{13I.zB!IX\\[wJw~G8jvUИ3 y3'#Ѯ];&Ō ؽ{7UTsζZ*۶mw<ㄇ,~ 60p@֭[ylڴgʯ]v|Ӈ￟3f5k,1p@&L+ݻիGeϞ=;}/K/]we+V6lgƍL6?4-ŜW۸x"S+vܦ^zTV{衇Xnmڴu,Z~]vَߊҥKW^yҨQ#._ yW2e wnݺۤ`1)Sŋ?iҤ ke˖$$$OҶm[^}B)6f\VoߞW_}^z&Mxb>ƍ}s^VU?y_VDDDТE /^-Z~zڡ}(ۼx|z)vc=Ǝ;<׻wo֬Y岔r"uTsc·mԪU$nJbb"?Ϛ5kOرc|#*Ub̘1mۖ +V5jbUXX'N_~̙3}i=WC=d+:u|rbbbhҤ ˗J*x>?}4Ν.4HJJBk ;bVbߵ +|M_^_|J*+T?ݻ7+WZjL4)ۭ W^s&M8zhtY:tkܹs.{n1ݶHHH:ѼX9/VAr;(kQ|yuF.]hݺ5.]BkMTT'/|vZMFyDzT(u{y!J[B̲#믿ҵkWZnMV>|8=ݻw̙3y 䭷ȑ#\.Ν[(hY|9dΜ9 0e˖Cͩ k]V1[iR4'9q<_r%u;6ϩVbΫOakre07vcʭ~촟˗i޼9:t`Μ9tԉǏ k7S1ŋ|t3ZW.fkB$2Z.^Hr< àzҥm6{=zMŊiٲ| à]v|GرSNqyjժc\W^s$4+VVZ9zbŊ\{аaCT$$$иqc7n̑#GtgΜE TȀUF@@XWj=Sn̙3$&&Ҷmo>j׮{ 1_{Fe`jU Qeʔvۏ 4M"##i׮+Wfiӆߧς6 V7b|Lnhڴ) 6oW>Vh߱s}yB#`QVJƍY&>a4n(>ׯRQFQB4=z`ذa4hЀիO1Mڵka8N&OLŊs+22N:ѤIr<&11&OLhh('OU7*UīwM~޽;-ϢEӧN:?f͚y=c…1.]Pn]M@߾} o߾*Vb62>|Ν;Sred(mz5kӟ7nգqƌ9j׬YѣG裏hӦ 5k֤K. ><_qZicy_4h]t/̖Fb֭o>_oXO 2d:u*|A9fވ-[0M~h7w\_3hQFȑ#=?c&Mʕ+-wѶm[-[ƥK&66+ͥK9r$/~~~:tg}Dj%_0Xz5;w1 rd2eJZI|r}Q[uUhh(s5f͚ڵkmżl2Tĉ ѣDFFz~ܹTR3gz5i҄l*_<Ǐwb%f+m#Ì3x7GX6?>;v޽{,lK,!))~ &pUXp|]Ɛ!C4if͢lٲ:t(_?oVژ/㏔+Wo+WweKW}(i?Vӂn]v /C=DDD|˾n137Vϧ|w 8ӧ+;1c¦+5 ,2tP=]v+=F٦%{̙3ܹs=a,",ELܣ Qʖ-K57n˗/zͪf͚jՊP3f .\N-[?"-6vXvBq#͜9~Yfw8yIKK#::#Gfن46mZLQ[qJj;,d !:x~πC!B!(4hy B!BA:B!B!J !B!(,݌3?~qa˭ͪZj㉏gڴiE\1zwO; qm)|HUVk׮gZ_V㋲g"@2e|B."СC; qvV~YΝWaaaf%m)|&O̊+8uT* ,J/vŁ3fLaÉf͚^wؑxzB[$`!Dp/Kq"-WU]veɒ%ʧ(㏯ʾd "h"xW=1uT+~BY%`Qh*TZR%x gK?~xvExx8'NСCM6^FfPJ1n8mF\\6lw^y4u988I&y^;^{5CDDǏ'$$cڎ97۶mcȐ!9~6m4>3-Zp}]"""x3g^ӲeK,X@xx8gݺu+ x216SOBll,۷oϖ4V8ONtt4;wd^՞1c .dԩر#6>xٿ?=zzgϞQre{cƲey yX=nʾce{zׯ">>}(+pfVfMnʴiPJC;X=y~B@@;wi+йsg:v[t`UPP;w$&&իWӥKV΃ڦBVԀ,g3f^ 6ԣFUV^ۧ-[5k>n AXݿ]N=~x7o{'My=~x}Q]~}=w\}=vX1|_Wq,c-t||֭=z׃  0 ^zgyF7mT׬YS6Lp~w߭߯{e=[,3fC цa6mdkVA_3k,gygٳg{^}5k;۷#F<+ǍⱺYwl/;ec~ڵkWw߭/ ZV6XV+V_@'eʔ)zӦMF:::Z?Cܖ2eM6oVmV׮][S?S4V΃ަ"K=Z)\x7x8ϟω'h޼ytK7$**WC}BZfϞMTT#Fٴidʔ)Q|#ԯ_>2eϽKxxږ-[ضm7o&44Xf | ;v/ykZj֭[ILLd߾}|^o%Uϟ7СC|W_aÆu1/^il߾6}ЊKҡCV Gc:tҥK=i&Lٳ رc̛7VVk*kkɓ޷z~rO34lؐ%Kxb~|Jrȋղ6ԩSԪUP1sLXb111,_<ՋZj1vX8rk׮e޼y4Vσ76v&`qS8tiz^;wJ*eK޽{ṣN:xCݺu-Q|yTӧ9w\cpիG֭]tuw}hY_r\ @RRKRJƎ;hРʕi,!6i&Ξ=KHH3gdȑNcq\N:+xe6}Ъ0N8A~۷/cʕV&M=t qjKST2{y:r.sZ[_ڵ+[UV >{ݻsi+ko{ίJ)΂Z|rȜ9s0`˖-ĘرcٸqoEE;vp)Ο?_qW^s˕!55m۶{ѻwo*VH˖-m~8 zM̫=gv샾gŊԪU#GR^=VX̙3$&&Ҷm[Ky$+ ܊iذa+֬YOBB׾ <_bu{*TF{`վ}]6wuWyP;Etܙʕ+/">}пԩi֬ ,𤉊~(5j*Tg…1.]Pn]Mh/3dBCC=_:uWi2ۗP[(e2Mڵka8N&OLŊgdd$:uI&.rуaÆѠAWO<֚JcUJxWׯݻwgѢEϭ 7>x aɄft֬YObܸqԫWƍ3rZx7s+ڲe <@Sa\.?<7f„ F|u(vTV3j(nJ -V_}b͚5=z>6mPfMtÁ9 !|G:H̘1@~Gbbb8pױl2>C&NȆ ׯ/"4}+Wdٲe|TX1as믿f̙\(IIIy̻v\r|y ˗>/OFƦMr ;wJ3| 믿&>>go…ر%KO?ѿli\.]#K.eƍݛg}kTJBCC'88ɓ'3k,֮]J{p샾g8NV\%K0i$zɷ~_|AnطorܰOn;EO+WеklY9Fi?v;vSҺukweʞ;e_ߟ;w"/;׮]cȐ!1k,;q O+A!D1AEáۧvZb?^bE!СCuDD/X2/q2d7hT"۽e]v|cEYdLܣ<K,j֬IV %991cp[e˖.P|v-eRF ƍˋao9v,]+rwXkժŻロl#!i,x/k_֑z…A,r};ws7J"]YdZ&Z^3!B!v5q` !B!(,B!DB!BA:BBjՈiӦ7E> !>>xMVhB!ޤ,n; :[ݵkXj[0o<"##dܹu]ѹsgիGXXXbB!HXq[p/KӣGΝ˺uܹ3;w&>>W !B'`Q肂xwعs'111^.]x>WJ1n8mF\\6lw^y̘1 2uTvAxx8'N|^BϔJ*ox^<ؓe˖,XpϺuӧ+/-ZDDDs1g"##;vW~~zbccٲe < aoE=[p8xسg?[Z? 4.[)W^2}tƌ9<ϟ_ɴi3g`O?G]V fҤIЪa^!B+e}2eM6oVmV׮][S?S4 ұN:z:>>^7oܓfƌzȐ!0 ݦM[jmzС9ӫW/3M5kaÆ-[ڊʺZZhunѣu||Aћ IDAT4h0`ֆah@}5k;۷#FZ9e:22R?裺~zܹzر4yjd,ժUiӦSO9sh@+WNO>]o޼Y?Æ Ӏ9sC J)}}hݵkl/i ϟwzҤI+0xdEYdEYsGkW^ԪUcƑ#GXv->|8ZfϞMTT#Fرc,^4پ}; 4oV!::cǎ_#}sy^[-W^cGAɓ'QJѭ[7z͡C{^inݺxn}B!tE]п3M3[yy 䭷ȑ#\.Ν~KJ)OƎƍ e=Q9ɜ/e7Kki'J)O?X'uGN`p1Y^G!7QGڵs7n^ƍ9rH֙55ak׎>;vp)Ο?OZlǜ׺|̙3$&&ҶmzK.qʠ 鶾,WQ9qjBky{k׮^iuF {FVŋ)WaT^=?xQeʔzύ%BQ, ՚5k8z(}mڴf͚t҅Ç{,Z>}пԩi֬ ,:>LΝ\2i$$$Ю]; t2yd*Vh;KfOƍ^z4nܘ#G~:97 .dĈt҅u2m4Ҽ\E%44=z0e*T͛:u*?)))*UW_fРAt҅/2[tԉ&M_>J)FE ]QFuVڷo3+N>ڵCLL6sgggEڕCwtgsSL&ٳ1vXرaaaO-+55-[ݻQZZX2v(DDDD#MDDDDDDy .I.I.I.I.I.I.I.I.I.I.A#??JG6HHHH0Hن4o<(JT*T*T*DFFj8+WW_}Uc;05ݻw{ؽ{?^^^ OOO;,""""F `2||صk]^HH{={[nJjee>H=~xy &&6lUP(p)L>]+)S %%9998y$bccݾJ¥Kk.ǣhӦ2LLL0sL!//Һe[e2"""P*꾙={6֮] Ԧh$&&bɒ%8}֘VŁGbڴi05&uLAJ۶m\1B#GD^^U{)̰|rdee̙3 9n)nffKܹsP( AJJcJDDDdL\_J߾}Ѯ];`͚55kwޅ#QRRŋn*9OCPo߾x0gxxx4|>kx`}7o LիWO>:mffC!>>?pDEE!$$D0t 9sh)?˖-_|Ç#22SLĉ6P[W:tƍxرcqa/:7h oFtt4̙///ʐ21~x?Nr]|GCBB]M"lٲB\pG@@z=qqŋ ˓LMMɓ'#???B&aΝ(,,Ě5kI&ѣG8rr9,,,жm[ʩǏɓ(,,PY2 EEE8q֯_[nŐ!Cо}{ RyII PPPoP/2Ç#??/RPPǏo߾ +++UXXޮ /J%gܾ}[aXvv6:wS9ÇhB^}}Z[[ۚU*,X;;;)mOOOǵk AqqzѬ|TVVsss5{VЮ];>>uGb>\.TΓشiV{bb"&MwwwtqADDDԘh2:KKKٳGݹsg >ߪOшBjj*ͱpB/ -Ojj*Zlݻw8xz{04o5kVWRbb"z쉤$XXX`ؼy3 99ڵCxx8lllp̟?>;Q#!014Njǡ#ZL&aaaFo[SO b RP(5/ $%@Srrr0j(cӨ"''/^Yf۷/V\iz,--ѣG̜9۶mSKtcfgg1cƠ}qrcFDDD$o1k,ch!..a~q^{ ǎӌ)l2TTT ++ AAA(++3vhDDDD;""""""" ?'-DDDDDD4pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDDDDDDMDDDDDD$pLDFrJ|Wz+* ...ZRRRRR)̔-Fc255ˡP(RS]h1U5P*8p@is]]QS0ŋ/lOc;$BFSNCzv܉_Uk߰at#DfX3f@jj*rssqAxyyU_ǘ1c0i$bٲe:ѥk4bĈj?999Zy2|J!e,Lmu_qXQDd͌}رcqu\vء_̝;wa4̟?8s |||駟֭[8}FΝ;.\xjyRHbԩۏ?bnjc`߾}abb>69FD`2bdgglDGG#11K,ӧqYT+J%VZBٳgcڵPնm[bĈ9r$`mm->ٳ8<.\m۶aJHHxL&CDDc8prrrpQL6 )S޽Uּys,[ ΝS!]5< ľ}~z\pQQQt&M J NOO,%RB.帐SrssuP(0`ٳHOOG޽yq*u`Æ z)L>]Κ"-- &L6H[Nr6#eLA`Aĉ8yR-RI=ԇG~43gLR9RIyݑ2u233ҥK} _ɠCT*ѩS'}BT &SSS1`R]]]JÇӧO*JcNJ,ajj*8n:[NYF?^\xQ[Q\R(Jxbv%$$hܼy({bҥ:PE뎔,eI9G o]x_/ +(a˖-x1N3f %%rػw/n޼S}Ƕ@vv6u.+??\߿1uxg5ޢX7[5R ۩Z3u=v=zG7T+,,ڮUX={K.EV0|p_~wx^M}̟G[j#\'ۇH <ǎèQp;wNrS|EMVZ]vVqm!"꾔FoB`۶m7n֮]c"99kSCO*`|wO-͛@~@̞=uqJm!Ǽ>ԗz***4^uq*u5o\cLѧO 2k׮ū~ YYYZwԇixK):}x];v oz_]Ǣ.Q|E]ޓ(Ihjro{{{۷o׹BSmaa{{{#%%|rSdl2kD^еkW`hݺF89s&䄠 o1b&NݻCxw RԊİa`mm sss7M6cƌBBBлwolذAzh"tĦM.222ꫯ666o6mڄo;wnO }D9'OF^h"t֭Nc!UM}/RƦ91www|Rٳ0arzqꫯt\S=||| S7R IDATA):}xQ<~+V@VVuR^wjI&;wFddUu"jx 4/رcmۆ7x1lݺ?<>C4k IIIZW<"99݃L&$%%SNEhh(ʐDu{!((aaah޼9 0k֬j%::QQQHMM9.\|VNrr2ڵkpʕ+?>222tG.2 <@\\ۧ/0tP].]FDIIIŋyfkkk߿%%%8pgK pӦMhٲ%N ˘7ooSM}XDq/Ujchٲ%vލRRGXXv B25]UV~NolRuRÇ8x ƍ˗kեXtnS|v!&&3~7egb BdAbb-b21=jc111">>16 on8jNƜfffŋSJ`jR,--ѱcG̜9۶mO!~4k }{gPr9>|3fΝ;$k8v5QdhcQQQcA)l2TTT ++ AAAF">"dl=qL9o&""""" `""""""j&"""""& `j4LMM|r( T*DGG)OCJQo ;^)))PTPT.[[[TFe4Rb&RRRslyʕꫯCm2Ơ1mgCFFlmm% >>EcyEdLh23f 666|2w^|:ƌ???\rF}kذa7Z׃sNk@=Hi{CƬP(7o6x]T?|+z饗0o<8qƍj7MDU& 3g~)nݺӧOkܹ3q…'%5;wg')jl1SaXÆ p!L>G^bƴ͍S4MDUx 4T`` ۇ… ¥K0i$uhT*,Xvvv[-GJlذVBS0}txL&CDDFD,YOٳg^c;pqDFFĤN}4}ņ pYbScbb3g"-- yyy8tFU>Kݻ@NN=iӦTSvIP(0`ٳHOOG޽yLɓUkݺ+++|G.lllrJ9sسgk?P*ctXv-222ԷIh޼9-[sΩ36 ˗/GVVΜ9RyJY_ch"\x8u.\$+5#ukL78Bj\L>]T*'Ǝ+<<.Ν+~.u&֭['JS?ʕ+RK.]t"77WxyyHg)sUJ8qbjG<טJ=o8\2]_-[+VԘsR'4q!Rڥ|}ǘ>)LgJJJ`mmm:e!p߿u.8y$ ѧO|=z@RRlق+V Z݋BQQ6n܈lu&dعs' fdffj\ojk{hh(֬YL"8qׯ7ؕRZh(dff ~ǏGqq1.^>hzyyHOO˗wwT:=4#G aaamJΣ" 8r.]EJ>5|.))ATT 78qz~2WĬر#ڵkoP*P*HOO)@\Bg)"U:uׯtR:.tUSy'2~ F߷KQXXݯ_?˹zaee/!!ѹ0c 8666033C6mpܹ:\l۶Ml8;;-RSۭakk[Lc{ԩx5SSS 5ѣ~Ø1cRRR ˑ{͛&θrV_?IDEE>|(//oqѺuk<QRRնR磲RsmsUJ̺UsѣGx뭷ocԨQCq~BqÆj{sssՅ.5ë<}1 jjD0/ZRӷ͛kli,īNO/?zaoo+V8YWF۶m˨D||VRc1Uᆱ;vرc\E͛@~@̞=uAzϓ2_㸨BcGӐsC_.8R*%f]Ƣ&U{N]R8fч[ y~8_҇ vX`Wׯ5ë<}BCҮ?yx 4Lii)T*\]]5wuu Rgn`ff~4 ߽{-[ToCuo޽Eaa!bcc5baa[[:) /Oƍ7PRR{{:\B899i<˗/>t/·nBqq1Xkޒ/~5}zҰzj=mڴA߾}=|͚Cx":uyj븸wn߾N:kժUѐ:7|k׮=zouԥkRbe,jr 7|j}ac"8Rj>r"++ =z}{<. `2D9'OF^h"t 6l0H}VVVXht???믿VD^еkW`hݺuꬬļyP>>>ѹǏ )5k>mڴWOi&x{{c̘1ppp@HHzm pvvAqqqxw1sL8:: AAAu2bL8ݻwG;隷fя?aÆ!>Mܕ+W_`;yjشiо}{bܹT9شi,eJY_ccȐ!O_ ՟ J}窾PyLG⥗^yo'4q/x@o jӦMhٲ%N ˘7ooKjj*Zlݻw8xz1p@$''޽{dznQQ,Y+W"==]}ZOu*{޼yXlQ^^={̙3:h׮acc+W`0H}$ٳ'`aaŋ$!!CUݵkҥK5%%%SNEhh(ʐD6F1߻wAAA CQPPYfi} [DEE!55Xp!nݪ0a"""KKKhMC}9~(**s\,annL ..SR檔5r'Nܹs~z }T_H9Nw*<&h"xxxo:U>! }1"}3#!Fy5vDKKK̘1ءhHHH@V0n8cBM!5&L ÂV9+DԬY3C&󐈚[M6bcCD DTQQcAM!5%?ڵk5R\_FXXC """"FMDDDDDDMDDDDDD$pLDDDDDDMDurJ|WC')))PTPTy xxx@R¢|nnnxg(2""""jH\SXjrss1m4ǧLR-[j<ާOT* 8PrVVVX|9ӑLol9r49lr??S9Æ # R~K `2f͚0b}q>}Zr=׿0h 'RSSn:888󄅅… c͚5x1nܸzѐ,Y???Z >>>xq-X[[;$L8 `2A tAիŐ!C43dRR͚5ðaW_Ӹ|2VZk׮V{饗 q ܸq;wDaa!^|Eeff˗#++ gΜAHHz_۶m9r$tZzzz/޽{T*e˖!33S3gDZZp!=Z65o˖-ùsP(0{jѣG1m4jNP(0`ٳHOOG޽111ذaVZBSNaeDGG#!!A1Lg"<|zܹW^y-Zxjq!44111x7!ɰzjG6͚5 FBxx8yjj=~x,[ _|HL2'N*E?K/9sB@߾}{aΜ9)f߿?.]`Μ95kܪۥKlݺCdd$7\x 9""""jܸ&233N88vСCppp@ǎ @EE9R<|?Ѿ}{a̘1ڵ+}YuXl߾RDBB-ZǏܶDEE| 8ܺu+ lll0dlݺUz.\>}1uTuU!W׬YLL4I#GҥKXh֕PY2 EEE8q֯_kעE DEE!33eeeP((((P/**–-[ ~<~'ODaaa=z )) [l+Zލ7`oosDDDDԸqLտj r ^z yO?2reeeeM:JRxtDD~7`ؿ??~ ???̛7FBtt4>:qvnnq5|||P\\49u3fw}W^yEFΝ%Ӻukz.\xjZOTW^ؾ>%$$m۶mNyy9?LDDD_ۈ#`ff3gΨ355śo7;|0 $ 2k׮*kǎ8vz/_ ???XXX7oq uŋcݺuػw/ // BBBRBl۶ ƍڵk1vX$''k,ȥzӑ8_s̩? UUފ T188X-ꥥ5yfffX]" :8|rcŊҘOj۶-n߾ԸωW`LMM?x뭷i޽=p@G*DW)++͛7ѡC rRlܸгgOL8cǎ#M1c!!!ݻ76lؠU^FF^}U8;;Fc1i&}055ܹsqqqxw1sL8:: AAA~ruC???믿VD^еkW`hݺ<̓BCCw 50yd?~\ۈ-d0(..F~~iii011ÑEѣG1n8|'u.:w -- ׿PZZaaaѶm[ܸq} 077L&Ã}i۶mx رN:vF4o?_$'']v \###CDIIIŋyfkkk߿%%%8p4rL:(++C^^un[jj*Zlݻw8xz1p@$''޽{d~~W",Y+WDzzM3gΜRw=DDDD BF1o8L/cabbbbbbbbo ?'%:v숙3gb۶mxCZcժUP( `"kcǎ!..999zŚ'=bXcADDDDDDd0.I.A#??JG6HHHH0Hن4o<(JT*>SSS,_ * u<<|iӦaÆ F~~Sz IDAT3g~)nݺӧOkܹ3q…'%OS֐}xW$]-6lC0}tܿzύ-""j ELLZөh BBB/Ξ=+[n-T*Ui111bÆ bժUBPSNӧk4e"rrrɓ'EllSd",,N{!駟j<~kOu튎)W'11Q,YD>}ZkR Edd0115113giii"//O:tH=Z]êqFYm<ݻwxKs> |}}ŁDNN8z6m055]Rb B {"==][ұ#8#vu>tuuJR}L̞=[]Vddd`~lllʕ+ř3gDvvسgpww\2kk>ǝisBp Is='T*թh%.]*z!&O,T*pssʫP(Ք'&&FT*1aabb"z%:ϠAĥKĈ#D텳={SRT*,rV`Rc5v `R)&L LMMŀp.]Dzzx 2GDNN3fppp!!!BR>}RMdkk[b>?~N :T,[1p@ѩS'1rH1eU)Pgܙn ?'0Wܽ{}򐐐k׮O>(**–-[ ~ۣǏGqq1.^>Lq<X[[>ZǏqIV;^=z@RRlق+V Z!ɰsNb͚5ĤI RևXf d2p _Ǐ7b@-LAP@;;v\v JJ|RѣHKK9rrh۶NqI=. Fzz:._}azKj6TۉgQ;w.~N_TPPǏo߾ +++}VXXݯ_?Ç1c @.#33{͛7^˓KuUW;^ DNNuHm۶i< gggթ55lmm|eeeSN￯jG=s uTy:z o6F@zVVVB]Vyy9~_BqQ_θrV}788}]رǎSo_~]AV؁;v[=ZTjԡK> Dž>4֥DD4hj,Xcb„ PTÇh֬Ԗ[n033So ZWF=z4^GF6mзozGPTpuuxϟk]UjkݻwѲeK):tP݋X"66V*oI1'''\|NIQVVA]ܺu 8p`yKJJԿR54ǎcinܸ\OͣK\x:u?_kUCa}Ɲ6./^ ,[ prrlmm V?aÆ.jceeEK.󃻻;k#F`ĉ޽;:twyG}I1rHL<z¢EЭ[7lذAu=33zB׮]abbɓ'u̓BCC@.ǧNoڴ 3f ޽{ ##*acccp\\}]̜9prrBPP^rH9kcȐ!O_蟱ػw/\/ sU}Xq'">%{뭷`ii/B-[`ѢE3::QQQHMM9.\['55-[ݻQZZX$$$UV7nC!"""F(-0*++兜BCC^x_ D5k֬틕+W;""""jx 45Yf;F+..qqqO...9MDDDDDDy .I{wŕ lhq(Q# '\₠"F ` W ɘ 2.-L4iѠF% P(FehCǶQ4ŗl!BZ&7p NS@W$_PH׈k3l樽;*メ,<'m&#Sx6y[>$G:JPLَiY[n՗U,B!֊.&MJj SPyd5*++a⎽=mǯCeq Cܡzx =Q`O5OU?jKo66mB!V&Uqp?8HآwfCY;޶<OCx'FW$]͈Ő&Dm QBuv,:DLa4?AM)rGhg?+гm[i7>*FuƏ`{{&mQqmGYoP6kۼ;zC> RSoz :݃-P[Խ:OvZM@y*DbTg“ħgNE0,v?Cۏ'6:'fBy"C3Ԗ\G>gBoTkc vy3>ĝd0\p@K: 3!BHkA`ҬDFD`T$]:V LH, Gmހ,U>-~?-kVQ_7pXodps;h''yv:@Y G#P7,xo{텊#~P̄ذ#$GiI{C/_2+Hc8j\CM $=lP{5`Vj @y= #<@/8#򗯡GhԖ\VzTX6-ޠiK'ڻ}oE^?~&Bi hҬ.CQ}嗧 RCx}Gm\9 ,ڇ7P eH/!5"F:3#*ecQ&j Qua/9TO~ۗ+q y,n ߓeqn׈}x0(Q{ zCf .-V }p .xWOQ} ՗<=ۗgҥo?B!t4K 9]DO+Uy_<4G5ABc(Ćf|,A( ϣQunQ}ڎAG߀+%ެB|P潅0tU0/ {jq=z]P[zK}}UDmMED6K9y[τB!MIз]ASQ%\{vP5WQ~{(τ;t'h3e>Q] "s/b z`}Y Ljx~&BihG{B(? 4+oeA3C}IP=|c@"7 k'|DbH{]P=]R Ħwêu>? ^Ë5)x/v $]ws?Exn zɕ aC^o>B @ڶo~&By4)}7x&*6zm!~7,!2~CS[$w;Eۏ kG'c-RoEmI={zU,Z_ADDM9Gj럜َ6oDawhlro|" НdCot1DLw;)>@o@ !3 "U0zoMC +V&GBb=R}~&By%ФIQImyu!T5O#~*~\R> {I*F۱0Z V59ǠVu@uO>@ͥuJq`Օg&f神<;?j {bVi߆B!u&Qdl<iVfBo}- >x>C[ۏCg OKB!B'%ФK!j' lL jcqhӳ/!B!*>5x*zJۉ!cI:[B!ݡ 0!-zpk[:B!B4%ЄB!BZB!BihL!B!U 0!B!V&B!BZB!BihL!B!U 0!B!V&B!BZB!BihL!B!U 0!B!V&B!BZB!BihL!B!U 0!B!V&B!BZB!BihL!B!U 0!B!V&B!BZf|bʔ)Z\pooogH!yZXX8 4j޶B\\Z: Rblܸ8Z4;;;piB*hLÇaiiÇ-wvvFLL 222pE8pFxpp0,--uFZZҰw^h͚5 qqq#G`kk+mP>|Pmy{{8U[˳Sp8t1|GW}ȧ-ɓUIP;ߪrGjپ}C|~m*++aii sss-*T9st4yprrʕ+!`ذa{Fqq1PZZJb^'|?79&LرCp=b*T ^R Ի^P0oooell󙫫j\.gSH$ٳWP=Wɧ>lL[LPll1 ߨzƌ8c2Lc]`` l… G-Ғq* E8Μ9Â\m9::Ln:xb3ooovmllXDDpalԩju鱀RSSg}☛Z?GFFuֱz>}:;vfOf/fbXퟗ >8Μ9äR)֮]~UH$b˖-cʕ+ĉlʔ):;Nh/'N`!!!jbcc֭[|J`` ;Ξ=˖,YRxvB`Vt1㘁w㙿?DG)Tʮ\œԖ:uJmU*TPi&8;;C$!##366H$½{Tˆ ϫ^+JcȐ!&>BiLmK&!55Νѯ_?A<IIIt NNNزe 1tP\d^ hkk ???w"??_p.㏘>}: رcj͑ggga߾}.wwwɓO?ڵF[ÇG^^F+Vh#q-¼ym.x<~&Mw<<<CP`/hjqB&!--MmyZZ~mk???d2lڴ ׮]L&L&5jRSSaccWWWXvvv>vէO8p?3ݎ6'H Q]]ɓ'ǚSAAkm}ٱ,\:|i7EVZ*C>;1H! m>)77p9=zK.oYfaܹvjyuu5JKKaff\ӧO:t耻wGH믿^ߺuK|zփp 1eB|aذa:t(ϟ> &LxiiCmEGG ֭ں͛7CQXXRp޺+/?Suuss{|]HC/ `ee u=i̗8X[[-n3S[D"Q0`YRcK.ZwQjm!ssyy9ܹ.]`ԨQHNNֈ1H!E`Ң$ lقS#GĦM`ii)^Y֮]9s ,,,T1v;wwwX~= %>1|4.϶`ʔ)G}۷CT"::ZP[u7'b޼yD.]駟1::U[[B,C*۷W۳g`nnX OOOܹsl2d2XYYa…Qv!cmm]vo|w-/ՖٳSNŌ3ЫW/,_V :ӦMCRRM|8###Ě5kЯ_z.b͚5ӧf͚cb޽8p Hggg4MR ///XYYC{|yo9svvvعs'n޼h$ƢKIEYY<==annjp?~\M4  S[5kxW???ajO  >"~~~С>|4HP[\$~ &xz&t…^ www\zm/// 99sbn 333Ƣǎj߿pqqqDFFbl155Żᆱ r9ՖEGGSNAΝq5\nܸ|W{쁑\\\Bxyy!%%EP=|%$$FYY>{?#G"::=Baݺuشi}>|QSSswި@bb"? !1D1lԙ- GYY]B@||t8}tNtR$$$ ''Ǐ}qcƌ3`X[[# @MCG%;;[5aHOO1~x}}}x{{#../_FRR]v|ZBEE:t* D쬶|HKK=?2 vSqttqP-H$8{_K9st%2w+Wʕ+Mֆ= 777TTT ""}UŌ=| ~L6 YYYضmz%>1|*]Ovp}PPM +W\.aÆiBii)*++y?B4q׮]0n8̟?lقÇ֯[fw}iӦ/ݻwaff&(FW/[4Wڵk1sL899!##˨%j!^ @Jp>>jﱳc1uޝ3&t9Nzggg˗-ڵkUSSSK|ӧcǎlvixb&UmllXDDpalԩju鱀RSSg}☛*Rm4H#g>P"J5Sl|l;qqjϳ G$e˖Dvv 6eD6{zg;vP[{n?|>m#d֭c/fXpp0V|Ŷ[[[\߁>_biii211qѱџ[T&+WDZcǚ]cccD"ܻwOlȐ!8RDzz: "mbTz^III2 iiijo^A&aӦMvd2d2Q bŊÑ#F`ŊpwwСCӧ8`nG"SX[[k\ L C777L6 +W#,p'>ΗH$1qD̙39991 aP(ؼyJMMu[[[… 3~}ʧ-~GL>x5>?t56ԣmH$8qԩSذa/_X,_]E%%%Xv BS4&-ǤIZ^KoooѣT ܻw'OFZZwSN6yMt4o,))V> '''멭ř3gPPPP7oVPڎq"uFhh(_HիWعs'\\\Tb U>زe  ;w*ѣضm233QTTݻw3f* 8uf?jhΟ?vɓ'kB$ nvG9s栨W^7O( :tزe 222` T_!!!h׮ѿA`>O٧|+33R>>>HLLDYYz] >rO>DStAP=|BtG ֭cǎشiѣfmF'|'D"AYYn޼J GH K Mft!$$!!!ZW7VAA7nd>~qr܆6>Mq6fٳgУGoV?{,> :CŴi WWW;ww.133VZUV\Xt)FΝ;C"}HOOcǎj֭[())yqhllN:˪߿/O>v-Z-[AAA1 z¿o˗/㭷tW_ƍرc1l0;VP(:] >rەJ%jjjTWxTVVi۶>;4&-jܹ3vءZ&>裏p Yfaܹvjyuu5JKKaff\ӧO:t耻wGH믿^ߺuyY[BUPwiv7uLMM'Dm"^XV߄~ƍѣ[۫M"tHK {.rss D;v`񨨨PTWW#99 Ν;b 46}_~ul޼:t_B(JPQ{_uu\BקⓏ}ǎG`` L]W\Ǐ1d׿ѣGƍTDeϳw 2$$$[njt16:t"HP=ڎ1BhҢ~77&MR/4iuަ/fΜٳg8jg$ }]\xQP=|cJJJT?qƷú̧֣K&&&֭ڵk'eee8N;kkkѕ~A"^0@pii)Tb1tu{GEpp0 6|}ic{}N8r,^11\~]mhM>wEqq1Fb",, )))}6JJJУGỤGp}SظI.޽: ;w©!|o> ޽{͛7 >VPP+++eVVV'"-- 033CDDF~˟?|)svR\Wc\}ơ 0iQj2PQQrpMvc]sA@@ `ee+++XXXbvڅwyx7~z **JP=|bU>DZ޶m[ 2LP[uQFiH|pvvfO'uk֬A>}0k,;{Uѷo_D"8;;ĤQm*Jxyy gyѾh㴱y1rA{nbС߿?͛3gԩSU>;w.-[L+++,\>>>N `kk X T ???o^={ _X`Ǝ޽{_]|4նhӦكSbƌի/_}\pgFRRj6zz'iii=z4z-tYm2˧O |imuaff}}&; i $ҪL4  S[5kwwwxxx8,^X,z|S!bbbTzqիZ= ###ҸQW`ddÇ 8~j1rHDGGѣGP(Zݿ"[6mBrr23~*~G`ذa~W|X|9p T|ߏJrڃdTVV"&&F^[ ())cPTT#՞O?0aN:!((HuI[o%|>ۮ;w` üy{n^FNΝ;ڵkXrړϟ?ooo'qqq>|8RSS5ꋌD~`ڵطo?|>Ӗjl9]} 6 !!Xz5= mSi )`*T yPRE"K.1;;υ Sv*T-;(J#;;'O֪dggc>4Bnu3f̀9LMMrJ<|III-Rkާy !KIpwwot A6BŘ?>PSSL,\P!-5ּ턐?b3[:B!B!3z 4!B!ց&B!BZB!BihLExx8󑛛)ShUrssqu!i b16n܈Tp@b8AO\i4 pAt*F\\Z: A^ǜ !>4&ðÇՖ{{{8`XZZ"++K4a޽ш5k␓#GVp=|CѵkW/inK.EBBrrrpq7f̘1 ,5y44~pvvFLL 222pE8pFjl{w o4 6>Osss7L#ƒfjj*̙Ө:ȋM8Qo%qn!B`JɓUɩIڱGbb"<==憊 DDDo߾ѣGo?iӦ!++ ۶mC^']Gvp}P䄕+WB.  a4b{bdeeZżN>k.EEEرc47`ذaطoJJJd̟?Ǐ 3n޼q=&LPO=|_|_Y\\\^N>S"K @JpT:oooP(xץP(N266fUL.}^K$vY+mb+MOCȑ#,001 ###faaz'XHHڲXuV@qFy6w>1AAA,""}w,55={-YD@&5Ub"##ٺuXJJ pQ{80{,>>3HԤOJ,//-XE{LL ;xD"[lKLLdW\a'N`SLQO/_Ο?_OΎ;ƲӧŋX,V3gKMMe#F`111,''%''7j<_6lԖ-Yqڷo0KKK>hРzܹ3۴i;w|2acǎeI qQqEX\\fgΜaƆEDD .˦N16:X@@KOOg>cqqqMP>'NdDZnݺ4O47g]3*T4gIgWL&Cjj*Ν;۷_~Ү1D"ݻZ6d?^ZT"==C T61i|^...HJJ| dHKKS[~[2 6mµk '(FT+V܇<1+V;Zol>}pcw;ڌ"Zјnaa+++O/spp0~x( l޼YcSt NNNزerttD@@0n8cѢE7oF^m۶yX?BUTTd/$]v€光'",, ={]VZ϶?+annD8;;q{ICDz;&O |駰F׮]ꛆ>1|rU?BHs l^i IDAT0iq󃳳3V^N:!::M޶7qQT* ݻɓ'#-- ݻwѩS'h篠cǎqbIII:BQQCVVbccdZ9sNJ|M߿QQQoy~!77pssƗ͡[n^ mڴAnn.rss?> :lٲX`Z}|iii)֯_+W@.͛j[lB@QQ`Νpttȿm۶ذa222P^^T\zUg &L47oތ:믿Faa!J%5~YuBOᛳB]M^9ڵ+~___̜9gqH$xzƔ]#]P[-ݺuCv ZmFJ~ HT vFFFb]tѺG"88Vk/bֿSܘ~m`/+((2+++7Or]cȑXXX4wUҥKٳ'{V1Brnl?BHK 0iq!!!2e GaP*y[k׮Ŝ9sXYY ]vwށ;|M_T>tD޶mVVVdbpvvF||ֿ)?8p ֬Y~!""Bbjj5k֠O>5kƎ{gdd`۷/D"abbҨ6J%`eeA3~h_H$lٲSN5FM6R߿k?CbРA>|ڙ={`ԩ1cz˗cc}Ghh(Νe˖A& .V7m4$%%aڴiZ_W=k׮!,, #F@1vX̟?_#LJ~333}O?O8̓%tO?Tu_(((-b1R)о}{9ٳNNN077X,/?|b䬋~&@@':ÇԊt֤I`hh0QQQXf 󃻻;<<|"&&Fw7n^z*.g(,,RRR(++Cpp0?Zq9x V>֭æM{֢ 077Guu58ڶ7$̛7عs'D".^Att4:ut׮]ʕ+5>c <<1|ɧ]v>BCCQXXuTUUa׮]puu+c_mT*رck.L8iv>_|ׯhNNNXr%r9 6L#w(..FVVJKKQYYUϱ̇>[t!??ݻww]ݑ Ϋz\4Ƕ[ZZׯ̙3q)ON!&FqBIKxx8 w]hh(۽{7 I^,??ro>kDΞ=|}}գML}Eh>Ga:T؂ -###faac,99<֥K,22[ .01"-[%&&+W'N)Sh#5Uuܙmڴ;w]|İcǪ[ZZ2TeРAuiK"T|r'Nel֭j```gggD"ddd4Y;...HJJ|u'Nį{ҥK0aFÑ#F`ŊpwwСC888AAA?~< 6oެվhv!**  &N0S Le iӦ=.j -4/Xd>0 2 QDPD3)@!p!(J* O-eD.JC#C\:D("_GK~>Q{ež 6l'''L0 ]]]Bd22 ;P;vBB3I&k׮nFÇȑ#8~8BBBxݩY粪--^T(077?0a SSS( I`bbDFF @FFaggÇoaoﮭeG7sꫯoӓ8/: '''"** gFHHVXe˖OSS7n۷+gBQ'nX|9e̛7Gqq1RRR| p]888@&aܸq 8y#GB.իG/o=@ Ο?8w\ š.\@iiiGGu\]]!HpQ"22yyyX|9;jFVVpq۷Omb8s  ^mZ=OXSSccc9s/a'NE80j(#..۷oFg˪|B @ & >>&L@iX[[-[눉͛7sJJ كW= ;ty HKK۷q5޽W8/ԥH$o>899 X 8;;Bz/ndBAAߏt8::vj֭Ì3G_ 7oDCC+:ly7eӞjQVVygΜAXX R{\ڵ B?JO:dlmmSsn߇!:(((PSPP騭cǢ\-ז>LLL ̄W|PZZjcbbb`ddBm\n#/r* hiiǑ#G0~x 2uuu}6[tuuv644ħ~ D,dggҲ ʙ3gp]HR bty.033ƍٷN+ lڴg~m̝;=K ۣGPWWeee>}zGGG,]F111mZFFFOD;wutZ:?}Jgxq;w MMMnsu;wSLɓꊏ?sϺP΋z{{sɈ#P]]9 0v <חK>6mڄ?έ@nn*^u/pSSSzᢣڢ٩j``_~Ԅb޼ylIIIi6UZZ H4O$)]ѨUbnڵk:t(gqw0vXvi/B|+++U.F @N3F骰EbB,+sg W/r2 _2 Ԅ J԰?Ç766"##;w+ɩ8/}UUU}6?T8q֭|!nw! ÇŋakkD%K 44:::DD033c߿Qe ..W.u@ `D Ă `eeӧcǎTzPm=iii~oɓ'ǎS؟>}'OfSbXp!,,,fL0`aܸq1b444mQQQ6m YfU]b5k 6q{ƍCPPF}AAA>|81k,:t]}񴦦&A$ח׺\l:t(rss<^ ^/jnnFii)lll ---W^i3gڼܹsl2XZZbXx1a᪫ !lmm1vXv`8""K.ի! !憀Vumllj*@![IKII<<<駟~Ö-[͛]]]DEE)͏CPP -- 񁯯/ ,qå\Err2;=l0̞=%%%ߊ܌:[ @cc# |}}q).s۷[}ȀfϞxذaKN:ӧ#!!݃D"Q7nDDDtuuQRRX։Qzs?q+011Axx8{رcy?SzA6mBYY+WCRR UugUTT`رc*\e.٢.-W2PdeeJ6<-,, [nEzz:gz4s޽{pss?郒zd#]}^OݣGF||3gB("++K-rrrdɒv}J!t$]`===Cuu5"""}֭[\ӧc?r/b8|0tuuHŋl[naʕJuJ>QQQ8vB!qQ888S?'tJ재+iWBtɒD*ZpNuܘF[[WK^Lqq1΋a>N ŋ̦MxQN[o>ǎc^xQ(;;Ȉv~z|sIIMMeMMM:+V`R)SXX\pJ---BǷw&77a֬YHRۛ27of+W0nff3SLQu IDAT\tťtO棡^`_Μ>}?~1{&$$eaaaLLL899)Ѐ4ܾ}׮]ݻy̙3(..Fpp0444Zի@\\q&%%aܹztaeeD~jorGEii)"##˗SW:)!gI ._֭[3f࣏>£G͛hhh@>}xS~tzzZ`bL<_|jjj Jyk.ڵzO;x `nnoooRN[ϋu5^咏((((رcyU欎}J!lhLz!C^ĕ?tRQ[[ ccc 55`dd*qILLĹs[nOGk`ݻ}uXfJ`u\.\.@ ޽{lF`W0`[駟Ν;w`gg)S`puu9s樴fNS[[sa޼y髭z>0 +kjvMV栎}J!lhc,[ wmwP6m‡~ggg(Vsss1evZ _/+:555Jp>}O>\tVGj~ss3;000U^YOF}}=zD>FFFFff&'۷oc؈ ܹ+맴ݻ*+022$ fϞcԨQ~ x~3RD"y"eeebףo߾mS[[ ===vZSSS=|-[|r&B2_~pttDBB?~icɒ% D"D":5|||0j(lٲ:::J%så\l}"BW[y1Ο?˗c֬YE0gp4̘1C0 bbb4ӧ۷2bĈNF{gXTWWc߾},]WP(H$:sŲe`iib`rW.6lBBB:KMMEss3oߎWDipۧsA,X .֬Y &ړdرcajj4øq0bhhhm믿b̙0666~əB!h#,Xzzzv͛]]]DEE)͏CPP -- 񁯯/ K.䣫dvzذa={6JJJ`ggǹ-.q>/*++{VN5k`֭* +|^^^^DZZ*p||<___J/w>}>>>JU7n`Μ9h <<MMMرcz!N:Ea۶msW\iG$$$055Eyy96lL֪nll,Fx 88:u ӧOGBBݻD6 ֭[mmm|g웠S>9B!DF$qXawA^rѨ ǒH$HKKCxx2#w˃_?n@!ғ2t 4MMMGaaa3fL/w!6d,\ !6l؀?}&B!7+:t(vPǏիغuJB!2 0!wwB!=M!B!W0!B!^B!Bz.br̟?_~~~P(j9%BH Ji< ttt;ĶmېB0^uR)  BBBm ǏWw7!t.KKK$%%)@dddgϞgb1,--r-BBBd2d2:I&T";v 666pm4hb1RSSe .qݑ\8uT ]\\a`ѢEHOOǮ]0eʔVu ۷o#??hhhP'>o*O!I$2P̈́,!!ٽ{Ҽ~ٲeK% ﯖbӓ>|ŋM6J |;ƄGKKq|rcmmm FP0JTNkhh0Wf222ׯ3Ofϟ.`l#$$ٻw/B :T-222b]Ƭ_^%KfڴiLrr2STTdee1&LwLxx8s믿frrr/2+W]0'Od TˋdӇ erss?fR)kmݺUi`V\( W^w)cҥKLAA̚5վa0C aҘFCCS Bfqrroq$&=A]m>}ٵkҼ'N0׿g:-gBBBRiǷ27of+W0 *TP5% a 0v/^5 x7ꫯ̙3޶>444p]vĉqev 8q"8i*tCCCyyy*@ff&z-5g,Zǻ D;w0''VVVmkee+WtZn\[}k׮t3f@NN&MOOO]vvv899!44QQQ={6BBBb ,[`Ű AԲmȑz*kW Rz>RZ DGW\ad1vB~ѣG08p`}֭[z䠤@}oQQQ80 |8q..."228<''' bccqYܸqAAA|455!bnoosssx{{#++ eee8~8תQF!>>qqqؾ}2.}J\{{ ɭ5rrggVs555=󢹹.\@ii࿧t ~~~ppp@XX ̜9S]nf̘///~x7eӞ⣬ ̓3Μ90 6LXvP(O?g888J-,,PPP4ʕ+ ٳ1k,L2ƍ0*_V7aci}9\G}oQZZj;cll 333lܸQ6m!C߿? nBMM.sssr\~ɨƚ5k\>vXsz3&&FFFJs{W߹x!'ټy @ֶ:*=v߿> B;Bw^o`͚5&_8::bҥ(//g766ƈAjj*UUU𩓘sαӷnR9=\.\t )))Xj6mڤRum/\T-^~ĉoo-ի 0}VWWf^Z?VhiJy֭[XbPYY:N=gg۶m077aooJSp+v*\.çGC~lu9/!t-Ltuu[onnF~:M6?3 E幹Jox/õNMMҕzᢣTo`'W*E"}sswWFrk0tPN!@,b\BK^=(**»n=uUWWBY|+++>븪G߾};->cBH0V?|r̚5 Xh̙)-ƒ%K D"D"ukkk`ԨQزe ttt+:\pG  CCCD"B^muG  22 ,O;vҒ׳OswwGZZZ`,\Xf &Lu\gggdff޽{- GHcԨQx6MmOapttĬYp!^u"""tR^B"nnnP &֭[4V'.}OIIAyy90m4 2f͂k1H___^}o믿b̙066o/ޑx7W_a?~d2lmm1vXv`ϱA!-Ф}'_|###TVVb݈R{[̓nqqq !00>>>B0.qK>HNNf ٳg՛|Qfaݺu0`P(SNqn%$$055Eyy96lؠ˗/ߟE\*bԩʕ+Nw7"11WիW HKK?Or;COOIIIX,nO;xxx~:bcc:׿`ll'N'ODEE><36n܈ꢤbq+**yfرYYYaaaغu+ӡ>כ;e˖aݺuطo444/( lڴ eeeCvv68|bшqaO~-oþq̙ù >!Χ,Id|y\tt4±$ !ŪU^!BHO-Фk455pppP)/ 1f/!B!t 5dC!BhB!B!/=B!BHA`B!B! !B! 4&]"::Ő?J1 ˡP(sJ%JP(P(騝 Əݩ(;veGaP(-KRx{{!C `ffƬ"9qHˋ$%%IIIJ m6deeӧOoX,%UaѢEHHHL&L&áC0iҤV!JQTTcǎƆwmqQ> X,Fjj* TjKu.̙3! ؝-''K,i΃pQ]!qr ?N: yyt/666X~=̙tݻjoXn8p#F`/?سgR>\p|jDDDL%Rضm`ʕpppٳg9?| ~Nbt|aggN3: yyt+---̜92|׸y&>#?X~=ouܐH?Gmm-/^+:\pɧ_|~G׫y8QW[-`ffmmmֿ}6 PQQWPPr9k?Ʒ~ Ltk\xx88ֻx"V\RN|N\ԩSq L6 k׮&O P(P(DMM #G1r9Ba[^[6l&LA~0`dddvvv8|0^ʪeVVVr =‚ kޞw߸۷/֯_?Xv-߿ǻ D;w \ncc@|Wxb݉ IDAT\OG9s㐋v0|p9rǏGHHm`ŰR\~6q]sWA`ҭ>|K, pB1Q\\OHݻpppL&øqPUUqT,U!"##pYdffBGGFFFl ŁaĉpqqՎ/"##!HPQQc߾}JW\5NK͸pJKK;#̙3(..Fppҕ)RRRg\zQPPwys+W^1vB~ѣG۹[nEII ɓk}b֭C}}=rrrPRRvH$8z(JKK<,_WЀ4ܾ}׮]ݻ}:Yqcv5j۷+-S熋 bccqYܸqAAA^СCq֭6q]sG^Zݝ!7n_,455Ɖ'  ;uaƌ裏#` ͛hhh@>}xS~tzzˮ]k׮Nm ?Ç }JKK+--Ŕ)S8cll 333lܸ7nTZ5Vϧݿs棽abbvjbժU>}:LMM!+< R[\ٳ1k,L20㵝Naԩ5Nccs_gaa?J 0vXgΜU J<Ν;\rV'u=㻝cbbB000@ߺu}Dݳι#]KQ/nWVVGGG@WWw}:MpttҥKQ^^ollDmm-T8S'11Νc+9|:^[W(Â@ u7~'y*mΝ;add(++CSSy]͹~:߿'7ķ~ [[[իWCPmk?/N]]]ۻ=-Dws0eL<1gTUU>UY8{1w;o۶ ؾ};+r=~Xi*qι#]KQ/s3ftOlڴ ~!|!GnnU?@_ 8\(pٿs͇|-{PUU^SSS[̹Ԅ JܜWfd2ik#FP:6FSZZ H4O$oeΝ;1|+쳗q=㻝SRR QZZ XO_Cuu5ׇ1^QF}9B^^4&^Ò%K`ee;;;۷7oTzIcɒ% D"D":5|||0j(lٲ:::K.#}!D"ȹQW[-ݑ3f 0dO"H[ Çf¡CZՓdرcajj4ҥKzjBD"~C_1sLC[[[i.X|9f͚aÆ!$$477666Ԅ+ʕ+pvvFff&;@UrO|̙Iq< `…5k0a8pWΝe˖ŋ0 r9W1۹ ~~~DU9>>Bҕ@.qK>HNNf ٳg[QW[rJ}'ϖ'&&򊕞===$%%bNjU/66GF||cp ̙3s011Axx8{رcl!44YYYhhh@rr2.]+\|mRSNENN[v̄6$ }PRR+>ۣ㐋v+**yfرYYYV׿ccc8q5558y$Ss8u`ggcǎ)-S׾PW]yBI$@Jh&<<\-$ }rp&::x@ `]u{.TP$%%1ݞ *Tzo et&ۣ*Eaa!ƌ%/Z!3d,\ !6l؀?ݝ!nuw*^n&]cDDD ""B B:&\]]Ǐիpss/[#tf|ݝ!@#Y8, B!B:M@.Co&B!;B!BH@`B!B! yMMMl۶ 999P( SNWBN&OLׯ2#B!砷@^gժUpqq)Vy,\(//ǣGTӓ\|rVX,t!BRt*...ذabbbh"c׮]2eJÆ ۷Z444TDzeˠݩB!ҥ 0U\]]qq۷,_0|G: @uall;w`̙hllľ}w߱녅 u1b|R .pjA8;; ǏWYfa;wnA?'~T!H$2P̈́wXرcLXXX111Çi@\xٴiSq$ ﯖbS|G:m}0Lnn.|njT*eS\\L2Ei=WWWҥK@ `}ɓ'B&55b455aaaLll,yf&;;r .700` EI)NLLLcƍJ,Y0ӦMc"&++0a0+V`R)SXX\pżAFF沐f޽ RǏWwY|r̷|LHHskvB *T]r'tSSrss1qNo[__{ VU괥+߶444D<|P6=<>&M \\\I]PۙB!G`iiiwޅd2ƍ*tz(..FJJ lU<+*m曰@\\ZsC ϟ;w8{,nܸ hj*D&%%aܹ <VVVH$l___DF{\Tu? 0T>Lѵ@ W1A[m5-jmixTxI6!y)/"\ZB#D6BppNOPx0Q]].]yn___L4 +V@MM4ՏOOOjɓ=>rrssw}k׮Uر666g N> x̘1xٳ'ܸqeee:|W066ƤI<׹ X[[#00P_AAAӧN_zb6p858qnBbb"BBB+++I5?666011#Ncذaχ磾^|mY1cttD-Eyy9,,,'OQZZfy0|,^EEEz{HCRRvƍz9sSL:Jgj)JL6 { lW[[\^^$ )) 3f9//'뿝=X ugTVV6g888`7nꫯbgW\;w0j(L<}Lxޗ7c H9r3>GBFFƏ]VTxgp…6sb…h4zR?bct,UUUUrZt)nݺx'ѭ[7*J888>%$$h/۷ѯ_?v&&&hԟZ#AWii)JJJ0qD=i^rrt]T*ѻwoIK)))ؾ};fΜ=zHٮzسg&L#G}ss#*6Z/vqLNRvvv044`ccٽ{7ƌooo 4[lQWqF,Z7o6kkkIGLb3rY8)<<X~=F+V@z\\\`llpذaz#&F 1grYн{wĈ-L>%%%BGGG?92=zōw;ynzLL TUUʕ+ҫ`lݺNB׮]oht|1L8ZֹY,15WTT~~~ҥ -9w'&&gEZZ6&22R_|ի^-** FLL qF8p|c(8Q8aƬ]EDD~~~OV#99!!!풏3^Mn;rAvv6 `""""""`""""""48&"""""N`""""""8&"""""N`jCnn.fΜfy֮]\h4v%sF;Y=Jm6|']\Qzot3џ 'nK/YO 44'OFApppm@hh(lmmq%7obcct߿cǎm7|$&&"''~zĎ`ȑػw/.^/"&&ݻw;T*4 4 rrr͛7ʪw?/Ε?~%-- -jUcƌATT233? 22M';rGYǹGxq \|Xxqj""8׭[7ܾ}aaa(,,;F.HII/p]ٳL2.:ٳgҥKѿICL=r\8p~Y/!y[+<<gƶm0f|033k믿^Ï?U?~Q?@YY ǎkӉ\Qzotq5lقso[oǷ."N'^'`ck!wa!881jZv!//OX|v]ddpJ{!((HR?4դ#f [+,,Lػwlݻ B׮]%oRF#xxxhי /_֭[]gkk+h4m>|x+Bbb-|wBhhh+++a۶mٳg,!>>^:u`!22{600PrbWpp%™3g ! W///Icرž={ 999‘#GYfl۶M~mŋٳgիW( aժUBJJp̙3Wsljj36m+>>^ks=*dgg 'OVX!(J0zh!77Wx4W_>#!==]g:b-5Ǣz8Z=F#o IDAT̝;W:C&j  nݺ]7j(;wN\WW 5JR?4Ez%&ׄ pDDDܹsǂ ӧOkjo/~йt;77666M^ׯǿ/Wb„ Xf 1n8^^^={6#F'OЫW/8p75555kf8;;k_7o|||iӦAVc就 ʰqFEcmm ;;;|ƹb#6mڄW^yK.ƨT*?~XnlݺWR?rSDhtq-R@ɓKDpL???!!!```SSSܺu 3f@zz: RXZZGߘ[>200@Ϟ=|rddd`ٲeÖ-["k=O?wޒ۷/\|tbѷo_xyy!55ꫯk.sѨw} vލ'N //7nB'!!1.^bݻYYYWVV["??GL`ܠV矣;wDff&-[ƓO> ~:/ rssTmv Zb|صk\]]u;y$RRR| N> ###Kc*ǢtqnT* {{{,\w菂`fbҤIXbjjj@~JJ\~ҥ~xzzjɓ'k_ӧ6Wٓt#++ wƩS0|jڱclll_}SA<;qnݺDݽôV/(&Ν;{MLL`ii,?n߾-9^uj̙3m􀴼-yZ`9s&zs˗ѯ_f-//9T*\bcZ:tvKEJ%3g͛(++C߾}8P{V GlcggsFL8*77o"''ӦMݘR$1B}9r]#`p* vvv!`ggI1rٸq#-Z͛7HZ{n34hl###DGGKGLbk ڿ?Lwww:u*ܴ16lBJ%&F.QQQXlN`ӦM>QPP{{{(J`ѣGX333lذO=̙ӧc߾}ۇYfᥗ^Bzj1{ƈ׮]? tUg-;#ɓ'_ƍÇg9 ŋcժU/)c!< rʎ.K#>>^<`8::"??cccc묏Ɔ X~=F+V@R?bbS\c(&WBB_?#lق/RtZ +V͛7>eee###u2٫Wb uwwt|x{{뜙{..\@ ;v 'NDll,***VS\bbDHHC=gڵؼy3RSSQ]]x={QӧѵkWjܽ{aaaꫯBQQ3 uV:u ]vo!IЧOҥK]vAP… ĠAUU\(y\9rѣ%oGDD(qŒYs;@ee%%ZFrr2BBB%QgRkp2"""!hjuuupvvFvv6f̘fy|||!C4`"O>}K/W^033C@@~W&"""(<LDD-ׯ߿/b֭mr1Q[x0g2Z@Q 0u Q 0!773gl>^.\ >^:uu[[[Ah!&J BZZzj?~\رcκ#G~N==+&&$$Dسg iii/oc>}¦MBѦZk>cǎ#?^9"̚5KK.͛W_}UzZjBNN䤳_\"XXXh͙3G8z-3, :Tr-:t(d\&&&DVV~ oߖCY߸%b cccdggK!gYfffXr%&N+++T*:q͍)z3&7n@YYYӒT\~sG}ٳg))) X[[#00:VUUUOmm-.]$yҞ{og""9qL???̟?/FQQQ䪭Eyy9,,,'OQZZW-!))I| i\jjj 8{,rJIRpY:RiӦa޽u}x&&) Kb:&&k~I6\Zjnn̬ngxxRtXo}ރ3n޼)V?rؾ};;uuuh4F͍O߿_6% >üyGaܹGC^^^eqj-Js LD$'^M ̝; .F\:gT*y\pArbbʴɨhe\l=bU.Ro'Dn$oT*>...ږeЭUPP;;;uvvv:O-//Gujݻ\/_F~R ij4mIee%4 F~zWx駡RC 9+,ױhP 44T'R>_\T*aoop9s7oDYY+ ܾ}[tɗUOCo߾pww :}%%%8qb1-ra/f 0u8J;;;fff6fƍXh6o ###mɵ{n34hl###DGGKGSq#JΝ;1k,='NĶm`kk=HNNƤI$o;n8XZZ:ǏcܸqRo>̚5 /իWcĈسg6&33Æ P(SSSɹPTTpL0}ԩS&.l2L: Mϋ/ 6 6lO?3>r233Æ SOa:u*߯}]c[uuuXv-#i[1}.ףP*000ѣGI<\,YzR?Y 11ׯӧuv0,^V 6%b[䖞)S`СjpKLD$7^Mڧ:FGGcÆ #&Wrr2֯_ooo@`Ŋ:g#~G8ˑE^P[[ F;vLtL>%%%YBGGGȖ/66cǎaĉEEEj^޽{ .D`` `ll|jc"##u_^* xDD,--sС߷owOOOk׮ř3g$#֩Sн{w|嗨DhhPcbضmRSSEϲr}׮]͛7#55Ո׹@?8rpQKGz|gx赘TWW>>>•+Wf4Gwܢ0x`7nā"fdfk&wjr0˗/ ^ﵐ!""`㶆mo=:Lƌ3,1dȐ6yQgէOKի_UdmG1 UV>ý{:$""j'څw CXXX"LJ%ܰyfܿ/^;I!!!x琔fu2xu0c܎g| 4uQ 0u Q 0u Q 0u Q 0u Q 0u Q 0u Q 0u Q 0u Q`-ƌ;*!""""GQ 0u Q jۚFjj*V1qD< ɓ'hW QkA 1JO<J- BtF555ؽ{7!\]]駟b֬Yɑ_>}+WUϻヒcݺuy&ϟO?NNN(((OnpmG"""""j %@Q kp2p.(/i[nbfϞpCPCh4h4033Ö-[ˮ8BUV!%%W\1sLNV#00PgݢE & >>999HMMň#D׽~ٳ8wT*1~xcR{nݒy|'8s x%z _|!"""""jf']O??tr!::ϟ?q666(++ƍ͛7>>> iӠV}v5J n:1cfܹsGBLڵk8z^U*J%jkku߻wOҤ菤K {>{访WZ4{Bw9# }k׮W_E@@\\\`k8u݋#G4j5>sΝ; /`ٲeȐܟ!nݪI駟FBBT*tI}L<...x%op=XhPZZYfaӻ_"""""%Df%?-gj:tɓca6lbZпdee€꯶.]jUMpqq… q KgϞضm^uTTT@h/ȑ#׉DnnM}`͚5x饗P[[L<F>ݻ>vvv:PXX].//O?]V*ݻHT*add$:~κ{555ظq#JJJ$PUU*&M»ᆱzYY$KDDDDDZ} @$ lǣwXz5N8Vy5ա}ܿ_zAh""""""z)BRSn+3IENDB`offpunk-v3.0/screenshots/decvt220.jpg000066400000000000000000006712771514232770500176370ustar00rootroot00000000000000JFIFHHExifII*bj(1 r2iHHGIMP 2.10.382024:11:20 23:48:41 http://ns.adobe.com/xap/1.0/ ICC_PROFILElcms@mntrRGB XYZ  0acspAPPL-lcms desc @cprt`6wtptchad,rXYZbXYZgXYZrTRC gTRC bTRC chrm4$dmndX$dmdd|$mluc enUS$GIMP built-in sRGBmluc enUSPublic DomainXYZ -sf32 B%nXYZ o8XYZ $XYZ bparaff Y [chrmT|L&g\mluc enUSGIMPmluc enUSsRGBC     C    xz@P0(!9R  +B!ABHĸqR*Ƴ3'x*ZLVf& s}_$bz7o5y~CTJ S))%@((D0T(B @$HT#㕈PK6>6jTD2yc!VJ1MKo;_?$OS [yk PTQu!0V!QJ 1 @!AR@!B %R#@ al|VbLJ򋊲pZPmn|\?yF+ %U 'E]2@Z C b@r8T@ PY RD2D鑊&F<1`(M51jc Soy7W}?$XbB?( * P zZFE GB `!3I!C B)@!`2U"14B1 $DaٵvVbI˷A4Ő\cmGk37is];yƣq?#( (A RG[-V1@ ]#@1 B1Q@Ej T\l0.9@R0dzDLtH rPG, bIyW͗ju_3oqO R@%U*^?&ZhV 1 !5+  R  AAB)HPQc'RI[ %CyqȈC E#y@1*/(c!*63¯=}_9/Zo=OWhV HJ/["T :Ar 1TT:$(05BF@(! AdDTc5 A !&. 1Rk.T1H(GUW5k T)iQzZ(PpX#U!M@((bAA@(!DH**eJ!F?Ll=\cJb%k^~X$L(ld'gmYJ;?CY*JBetЦCBWjBW1:)C!(H`0!P(!@*DQ@AjeP(nxPAqċBm"@EY9|f󝾅>_E[bGQB:];,j$T *pQ DAZH` PB "EVm2%EQ-(A@ *ُGB*1YB$+&fslY^O_Cq]O!(%@?ݥ0C00 ZA@AL0F:cFP@ %PC 1ԉhŖR,)kIJ&]h++,3#.<훴򥞑_GWtC$10Md4@ (` (A)b#(HtqL)PTˋCz~yޞTEQ1] 0 "<㌹NB|,np~꽟@D!J JP zƍ@âAL P)T!D3pd%D%*H 2JȢ#L"=SG~Y^u -3\`J ȏwC|{mĤ#JJWo\8(L! : h"(כbi+:bJu$`:, T"Tр•0%`h WcsΒ1%VYQ <6};I#bߟSRCAP Mvp `: D1 W.ńդa2TaRc z1 ` F $)\ޥY=uq[1+WgoKE}E)5{뱉@ @?eL( $ DZ VQnTIXԕR% Ӧ4@BH(*C!BJ g3V*RɯcT#T5i}gdž74z5_݌C@ @CM1J 1 D`01 PJu^nƤI5cPT*#*@d!}1@5QFtHd/>Ϗk }W頻+yu?l_a(J K&Fʊ 0b!Ju:[Yo8F $(AH0  HŚ]ZXLtX/"1J FB[`+9]3}*(0 &Zql_H},Y4T T) RɿѲJ 5إt  ZrdK%e5uk:h142fl\v91áU $ JnTB"XbZy,g mps"`sqBpw6wu>v05w = @ /9!Z)`E:TE` 3]΋>vXͮyW]i4ږƦ%@ QiPk,HdfeGgoBIevYHˌL`W s$o?MmC)CDߔ*)Rɿ Z( ;P8RJny;ssG8]I1}sT."3ioy^Pߧρyޙj`UV5 Œ5g,njQZ[l݌gK[W6Yj|(ԴٌXIfHb"+<\eoџ͍HtjCVL`*TAh%:/ C)`0֨hJ!h8CcC Wϥ9Pu牼bי>A6idXTٛNci/G|nNgs-o:%-۾l7;m*jiX7Xץ RRF}=;gYfɕt824d kcXC*VW9k7_izMwL( PT$H=@)@C  B 8p:x^XȒid^Ǩkju=rkcgUqx=/OߊcIŅiq|zY{/=.R}UŲֽG޻:sLkq LR꣛#"+jvY?~t @I@Twɿ2$! @dDU8B2Vϯ::b~L%XIoĞ̫̼sLl-l%4 s.~yс=s߷]k/(h}5k[) %cy}w_S2u:VkIE^t]baPņA;y|׻=O~c1V#Q*Qwɿ2!C J)(01(B9աO)!/]|9KR;-~CzMSSj;&4G^x྄-l{|S݃~f߹Ϥ]W1o5J+XO'3Kw8GRc|@:bVŚ͒^_YЎ:f_O _Vgv4|"PAjy$()@ RZD1`%%F"Θ#.@+EAhn  JAJPD `D`F5j 銷;urbˋdLl $9ı:HՌ$ Hj$l -0n*+]z?qV q"M5hsufWv\eVO4> E~C[@4JRhs'0 PF@!ræK*^LN{| u׃ Cr03ynUZ[$*$DDWUua5=}ԷP/=~UV$ %ΙtI[-l̓(< :[_`$ JR]ZQ(DC5*W=Z4d+W< ȋ2$"lXٶkD(XTHJºŪJz{Gq`Qߞۿ*+Y%ƲWl2c <_sFO?Xk>og(1P1 @7Ҍc@ pANP@F#@@!#E|a$ t:e'Ν<rsr\gf_6ӝ~?Z+X^*q"")"@EadHiuUIOgn-KߖƖC ]Ձ$2 byusz3x};6tk]8PP!)H'J`Y"C ADS1+ChZtO:y37Ii|9to_OvNzj:GgkN5%EB+H$$j5$J -\k: _'+I"#RG-nx BJ4IMj+f[j夓Fv^{=zߛ8w 7V@PQcʆ"aYvޛ>^wd CPrՄU.m/=Ofzȼ*Dz.CuxNRB$Jȑ$RWZ():Oܾaz?1?!HB OtRGB!laXo7fqK)uu.=^z)POY]=37Il%XC~ [u~3MT۷ߣ|u7w[sTl@*"DUupk<+RZlFYz_bjb#j-, "zu~䎯巜4Y` P(DP AOt (R5HP/U˙#bM_Ln=\Œb+9y4b$Լ,z>׾Ք?%T)(Jpe,`(!D0/ן M37ߑG.~}9ƴuϣӜ".|7-I{>We)5t[7fsOTsΗLG·vDƳDDHTj u.-c{˿ܹ)ǫF,XԜiqі\ZzzW7>w}[r@, U@* J!j(JA fӌ>]nszĆpi|u婳=v&Q'Al3\ש^<_;'f|wB.gY|.q.!¾Ykhs(-#PMz Ս^}˗쎤SMWsJܪX 5 Dˋ6ri/5fn'qJ=>w?=p@*R Q.0JĈ)^ 5:ǃeU&M\Xɉu^>8nkVLϐtw&#GIi3V;c3O_]@C$JjeuY=zgO{yM'^]Wva-9Û]BR'mɒM9x%~g¾ _ƝÔd TA@(U+A.w`()B]R/~Bzsgf-볽} e34qKJYn|'e3g<f?Gb᪟Kk|vυN_3>|Q\ 4Q VUek =zOmёY:S*j&BD-LUrٷ?;;Ӡf0}?+\X P s0 QdeKMj =3&ѓqydJj!'=u%,<|||/,Iqߓ\yufm3w)t IߠԔwL DY *\=\%(`]r+'CYU{=8H3P+s"jH-YR#cg3Oؾky}25k~5\P Q Jr@@Z H\S9+8FX̤B.zyYXM;o1;7K IW8e^:`h(@ "$UeUXmc״y}z^dk1[vzy%KYWR'S.L'1{ߤctLn9|p` AY?0C T  D)V\C-UƻZ9]!\WN^=dTټ>wO/͢iyuW 4K%EjĠ %uZUXag}ۜUKэUd,R0AQBI$οwy綟`g \*ז2x߆ z|䆸;@h%,K,[ JBRK t\nYq5vƎŬ\N>ޓK 3|7>9Y~xSkW}+3zss+JSX RBe\{i߼LYwz#:jD"4q&A'ApSwccg<ϟvJRʇ  7~}[WWO ]<tտ*U/  :(!SXDSkq*嵜vmpU+5`E:}6eQԮ7L|{6Dl9U'=Ŧwy9_$ϯy~^6} F>~mf>PL)#("V11Uu5vINߏw竌֔!N~^5JUMRzOz'_7syT5k.V"UaB$/*姧ϯ~Mw?j!B#_WHf$ªgѳ\uqEf7xu+Yo l38ɍc=9&{oermR '\oc^Iɦ}h( FXjSۋn:/x}in&62E$8$FJ2*KΚ/Cϭz3s9fN9Y9L"w.@ @H*(._e E  G/n gX,k@%ܬt,+;7.$=BIk5\>Lflf51vȜ\U;͌7V5Lk<{`()#Qeױx^Q&wz3r²K,eR,2*35,e[y2SZGm]Yukd哜d9_sI΃@* GϮ ` 5K"m⮚947g5DRjd:sʷGVͩ{ux_1Ϸv59}n_>u+=zYzws4SD BBVWm+b]ާnVf&9.ܻ\m% Gs2XdԫurS]YUfٙWXibjv]n˭μ2vdKYT^\gn~173Y 5&f,UI9L9h)a`:B = 2̵2*5X>]]ynímd_D QPWȽj;Ky@%4XVknY楊BƿA{neekZMJޣ)Ԟj+h;s:Ηm.H8i1Nk$gMWoVfwSsrۢfQ+$4lXC !5k(;U3""bۥK "#ɻpc*m+c%u<-]ȶɑII˭b籍f8(P+ Р U} (@ "@\y-W4gWyX""Z^a9LW4E/)5Op*8cWdFHӂ啎t T#KUu:T9߆rvE6uL4*(bʳ2&IyXRQdSos2P\$b ) 4J @_|>@+ #~cKo5H[;ʾ+η!pjqkNz$M\*aa0L!/F,*$,` ")@@tM#^%^$R)+ ە[ 2 2K75\h; ﮱY 5ƺNg,b oվKR%B)$2E_TKbf2]rL,L/kkȐyrk7a~>>nd\/ċ%iM$=4]ەy11ӲZJp (#G:P# ۥO cX  XeU;2MYsVWYɟVKئ}Hh4yUCUHDV"@ eRIEgzxjϵ嗟o?uVm\xtwy׼=Cʥʝel+!+uϞwN{GTt-I,a@DscPNPmL]"W>1e:AJ`BH`5Zn,VeyZ32 QH&v:lSg/=hI,&/7Vy0:{w^ "bjea`dK &"/]?>g[?x#{bC2zY3w.<8U+%hMˬ"Rq"q=~@%٨XjBPJrI DTc֛L+#Zcʻ񲬨B**Y VQqXI3w=+p媱ds-V7f\ҫ8Ck#"P3.+4LI;pì19.-[>XrJ֧Eξx×ZcȽr3 uǯy aLu+m4``QPȀt(81؁M}5CO/ÝgdH %U҇D!Ii8ρe^[^ӧ5ozqFdYt#gPd/NgHsl6F:ﮚ4N4`V לq3pylq/&)|55Ν:z&Olaβ"u6[ K帼[bŰ,$HjR:`1(WUJlwXLD #`C*J[z/~yVxVLr$Dm#"k,T Wp ZW_o #2 K2(q4c=ƴ~_eAybz?k4[l6vY );JSYlg) Q*eU=6rHeR*5W^vq\qܺtMay!ϧ_۔2M67zsԞ\.=/yJ3kʫSϥR:sK罞fibMl5, n#x il,1,RʀSPBF (Q@t(K;scb`Z+ʪ4dceVr`RF*7Iqy}so7WLrGYYu.S(z c.)]49 r8SFx&s 2_:g=2,My>זtDL|c&zrY8s>5N y.ދl2.y{rLG+Ϧ0'[ۖNיWIV[/˧Y]]:>Z{Su|+6=1qVo=oc>7\me;syVbBc6`ۏYAL=fF2m x62#{ǧe ksK Ac[D`Is=cޗRf fV9#bVuk7Su[͒vY1m|k5ĝhgirL3ιm2pV]d"뎇7n^~;Y/쎷y0zr=-:>wYȖ'wˠ@yy^ghJ˫1쮱Ւ+/1 HەFgfuW-2ANgxkI&]0I5U&k-RG\db(:ku*Ugc^.&[HW㧥k @qoGyAsӞY^qfeH#.@ 7\PqwȺob+ΰcI,'K70lIJE%dJ,1V ^ԙF94϶ޭrK^Y1e⸡n_R *riyGwj*<} 3%&^>js]E:rxeG.7}5gǠ#x~ L 3`)TDDLŬ4覯ΈԘclazl܉t^[,АS]zˢg2˖n +EgVYFѤ 9뢳>8VuĀKg^hGv;κw 1MkNxؤNNjk6ۙdy.t뗗uQtVuSe:y{ms|ˡ:=ɍR1.4Xs9=2͑0[3bTؖE!eڔ5Ea9^^L}gxm1?~[N܄J:3"k7"Ĩz,%' Ue2R^8Ιb:}Pk$:eؑלK„loα'c~s~sƻfzX8z2=m6㔮N=!2GadRO}q^YHO:G{XEy /[]\z7vJ<=^ldfmL-1©צVwF7q57~Jw䵌prWEi ,V%g95Хu sS[14Β΅Z l̀[6s,sԱ1KLe˗SG\neɲi*㳮ug]Y`Rr},WXQ{_y0}5 ~_Gk7]-~=\I uQuQVq3ܨ=gRY>|ln#1{ vjgEeňB;9=CdNu%T)])}bvejf:LcPzJM.7s! te,M{ *Z*%5*fw\*&5"5fKW#l vu[B<;u2Yp,ʗlVDKנ=ˣagu6{I.:Yf]Ku[dcL7yJ<3cԦ̘̎75ʉDcW, t:jϗqwOoe]o~.嘦:D2^v]z_Xk5+糭iLdrMYg-iXW,F^:BvpYS 7oyԻeLo1d`Vk; gsfu賭y;J랏&D*g[ek.ZP53ajk a-ɐcVHF:^ ÷8i]Mm*+ٻ}MvhEgHN*5yYc.@%FnNuHS/Љ6gRDλ7L={gc:G!]Iwqs[tVu-9j<Ì)ّjku63A~}1VJ㛟?ohlN=k0Me%kM-ʢ`"F..u!*b Q :Uε=9Wst=1UDl,ѯ1˧c>zs8d&M"1U۟Sהu61IC48rs::-`U巛mg6# Y\nVIݜ=W@UrȬufhtf%LX Eerь( `Ix%z@^8ƻ@ss[6UHP4j !+DDFPUN-H" f/[e 53\oIfPcr\(;c}~noyIR&4i3٘+o@hQB5ޮ xUz[VW]6YJ*rΘ2!T"`:0z'.785bc@ X AbyY=goAN"\q)XŮ;ä|}~j.*1m+m^~A̰P!Ufs_ߜ,L2Kps]vQUD!M=JaS\^ *RZPVt@18\6w,J)9,eDЌ#ymb%$邢 Ǥu器P j3tZpgKcIt[Yy굙['<p a$©Yt[d J@V@ĭB.-IQ )+K*$JT$%$2Tɜ.YX3Nu$G( j"qe4VDBX"JJꕌpT $P Fք1Hr'S$jdD+, ]`JȀHeNE2-KȲȲ).*F<vQ1JBJ"$J(uǧ8_TmHƁ%"ST`V,&,c&XH*6 cF P4%ac e"$1(d+&0XդB%Q *c!#-KPT`#+*Wè  C $F$H7~Y^o3`L%D*!2Rc(dH"V)cJtC!#$1k0VI"CLt:H.2RьDEi]D` 8Qc|h,±5&A4ES0J QYm(ɗsUZrC"HED˖2IJ" ib2@MBK֩ndK 8yf!,MdHe fJ r# ȗ*\$(]VHk!(j @বpC֘]3>6Y6H6(b7iWYBsKVd 9ޮzVnךLe9\*aq]15VTɲE1a%ċS(:*Ǎi#<75i]ѡ1fA5P`!XYDK8e%p)C,dC',DLdD% r"tdk;Ι-e2tXF),2#LhNKt<ĺpeY3tyNtk9զZ/-Ksr9r,u5Y#ιt T"(-cBd' %dEeà$"!QI+%*CHB(VDFa"E}g/L30%3PVt6z xmdN66i ,52nُ`^6>i\w OQNG3(;c]سۍx!1Pf%QɃv?DWV/r^%v?cD?/Ό'Ϥ#ۣ =&&НP?MAx2?Ѡ7!Y ,)>}%c'\蜎.e ZnsOHbF?+Ub^8 “QG'I_ǸG'vgY߆$|XOC OS>k}ՁeJk=G!}%'$135Y?>4R}ޒYtV7lW$?+AcXnz>O )>INPl.h"=t?BIFSqeVBt?hw߱*phEI&qߠB]E#]%:G OSb~+gDvݣFUQL!x&>ToOػߪv~]-9[h|xۍ@nveztZ-հ2\\\rrrr尠Ҟî-ZG'ؿ:jT"J&t+^j Ñqae?_ǿh2_k^VUܵZVUVⷕˑ˕˔EX֐eY FKvMF%y#{?⧳\tN;M?BrSPY9/ϚR*ήCV"R}}ZiN!\U9%TeFPo5RTӉ؇evw>r*'}}gUtW/Аt;#&<|^~SHMZ=MhVhVm+BZZZDn@ Mrj'U&l>~zM}~b)Asuk~:5\Ƅם:^ry^p^j^fN^d%y%+W1+̊2yUy^rl/:j󶡝j/׷Uvcrrكȼefy\rKkP`1Q k ,C2)R6Dy2d.G!iy738^z3g^yEu]Es4EF5Q0&5SLrk(+?wu Vwj;a+|,d}]?OF_w߷ҤrY򸋌B4WfP}s:28\wk GuXp~K!zhT'BQ{5콻0Qj$h:!J1VS]ecZ&ߙi9L\ %XxrKEC]'}V}㗃uZ h^JZJ݁W՘Jܼ6i$ڬP1␾hd+H"R2>ZO@ז@ڰFZ85|*yUq\e森\ՊJ pN Es5^ILrk?\v;BWX:'{N1c^(GݵXÀ=:OUG}!0-15xZ,Tn6L<-]p">ݖf4UC D=7㫾W&[4>NV^ߑVeȘ55ٲf_*xlR9J)7۫kAE}#:Nҵ+XR7+ CC6Ƹc#'ᬣ:v-fdQɔZ.*"׍bQ^j nQza~KkJ@ +*l,{9.KgV%k5&#-LefɷR!b;ݍ,-8S?Mz3t{tPXف wrw2ʹ^_Hdq]Ԇ>g}Ϥ-FJrzr( wtk-|ɪ&*7:c۶;zksCny)j2c1Y ^,JL, 6!tX2k?)Ȣ&A躩OVRzM}±UуrlX܍YrI6Mnmz&ÜN3q@2_m/b쌍y@~d]c2rv/ֳ6ud2$uZE=9W~z.}Sh:z\~xB>{87c5/;$7[]^ZsYVj1v|oϫjIes*-0 sn+g]QMAc:h9rp566:7isE=&|qF߷ZjX$1-Ч"~uoh^ GT?AVIWwۏU%>}~e$-k&7;Zܔqmd2.Q1:SIo1Z!HFVè)~F*$ Nq$cp8WB05QYDܳu{hBh>\%Vħ"!@Z"[ _j?G~_ĬS5>c?FK-hU qoTph%|ѲFj VR'Z9K 6iQtiXS|}A*,[i".s1:㰦;?!u8*]8j^JԓIf`zQ^nsS~Tzq97]T} O~~hGEbGaԕ^0J"gh2_$%8gƛBfW21nڻЍPv 7~OOGE'嫑ڒfdgsv[6c"c'ZS#dSE@8g >s* ;WZ"OzޖmC~;xiXiȍiB1#r QM[^5'Fb62].ƥ֧-@1>]67asc^ApGip,<3SIICb?qU!!C8QhZ? rF+Q]qKP :6|a 9?SQ'=nL1y:Kjs&*O/ XU QG?O}QGuC{,4\:rTWdDiK2㪅OC\g-6 d̕56`eiy՞[3Nf[hM,BC;/X9-M0%,Đdl8r+sV+̥e=QNNXrdB Hm9Ѓ$e?P<k¨˕vH^**WBܓrC(bmDrow{5p o=u{m@:i՟rS2>-zjWtUa O^?'p[Qb>"GRiC;.}ZTJ? j_ո<p<b>̅o0Yf#ɵv9{m_g{Wtܹw]pl:BVn[UnZV_VMYb#4l`臷)Oί ]j>=LLw@lI?i6̍MN̡38s9 \5gGY(v-‡黮Zؗ1J=9wJkɾ]PPmTlc\ᑱeCjr 14d]jm 7hZ6Q>Ez޹/WQde9:7/)ǣ&9y=y9y9y9y9~ƯM]v0 uNSF4jhZr?MCh(Tc5$F:'1iMVrm3>3p?OUB@zx7EӜKQCZ^jdW WI-e@(e>MoC',\+{^uZz߭hZ@B,+ˍa[Vűpꥃb築]J?*'ze{խtN:{K&}!)W1Hڈƿ}^-$v J622f~R3U-aS)ʱA7qh^ ?5n+y\\\\\\\\\\sW1\sW1\sW31\ssssw.E"sH9J6we#fE& K, |oGBN{8QEfבz~g=9>eR RjF!h~6<7EF\bݎ9FD6G]ɢ}eS# t'}[ S_hZ-E*[S!a2vz8eF-tYiw0(3C{'kU? ]LZ7;J؀h 2x,7O֭L'Lnm{z0nRAl/%6eإX]$־??Kc96iܮNXy]tH('կ*{]:Zː(ܙ哗tvͬ11NnFe4 y iF]0NOmAzQXA~=g"3FTU5\ 8N;~fWm+ѧ-_mzf`\45۴oͶ..6 3+NelOplm=Dz)!6U h$EM k#L|ϗq>-݆,5P݆NI4x$erAc_,6hmPI {8>;1Lyg7pO~yߩ-1 ']˷.)`Yoalq5>z}F]^x6 A:6 k ӹ甄+ 5\LC#cVߑ9||a)' *͙%B`A,8z`%wE.PBj3g)"ZڕF7=n7"4؃Ecbhm̟uT#("A(1m|N9,1mI-p!| Vqq)#,;p#sܷ-rܷ-VUk)DGtjD:zB4|^EKS6ql: !R-;vk#Pwe#F趭t\&NpA@Փ}J`)[vЫ܆rmM2q k}#s K+ˋ3A4ظ՗)&dڪ!fI}exhJtHrɩkk$sJJl$m?>4Sk'UjZZUjM:&jQjZzzwz^u[FjY W+W,,RW:4˰ȋˆRXa\R-(@YT\5x@),w?m*0c$e1ԷHf2>,}Wr]Cod1H=֐\V!GGQӺoYSJ]LW;sy oP^nwwXqdz* mt,Ӱr#,5LB KRXf2)w-^Z]=Y8Ijq7rIUe8L1#T-Tב-أdYkt::-GdOg$E EMy$B /,ch-?nJiImab'w8|!rЅEjoZ^}qB)!cDz"85ƵÍݎ-d3sL1(XyM+P6J{F$͈#VAܯ`g#Fm7_>ȃw6(ct2X7]~4tx^8B bm-bn_+/xNmB\kbض.űh.5m[Vŵl[ŵm bض-hhZ LM(u?BoN 2O̽G\ݨy#آ4+r:F4GiԞաrR90wܷ-r.[kD\ٻ4;!=Ȕ<9܍m7"oat6|vBH*WZI`fiΒQ>.Y5çge.ReھHajrVgEm[VV71gy9>  nDX-5hբEh4[VEhZ-oơУӳE:" Mt[A5ѕVe["\):h^)-O7#ף4@n pq l~[֣o+-e2L-}\m\L\»*tzy&=4w 3_+$aׁc+XaN6~/x 1Hi_=oAԗmQi?keoG3s#G4{Ӝ?QEeQO .s~!̸CY` Wጚ>Ʉ|;r!-8m#)尭jk45\^ Vқi$k aj]TjArDNoL,A>Rh"ޛQw2޵^ׅ؛}֋E-N-W ev.hVGQM" S=rx6>epKXcE&j~?)/E9ЊnA'JS޷r.DMw*Ir~ȯ Z|USm4өdn:$nKU3^OvӠvX=rAg >G,Z-:VJf,RtEVo%,-{.sSRrȺn7%79\KwUks{+^(NZ6<5sXCV!QM7 F*>NgG3ZZf(PV z4Z~((ypQ=ώQdc+1"9ftK]FR/`7;owU̗ VU|0C^Zdpcy^Vqn=0Kf_zkD.RV^Kce.%w;9M<6x,_JA9w.]]]]ӖGcQxOqVnG%|VĬr( ᫝j'ls5IjN@^*IjhVբЭ("SGt6 ԅl+.4:V  է5AQqoW3\zrLJ$1rg_AW9VO$6v'}B{c𤙻Φ;dg|vv1/3Uo0QZ?b𻿦jjY̙NF@'xN!-n rvnͷ-vZI{zdiB6N5rbTCQZZ-DUx%08 Q^!~3gރ7,KMZDvX;7~RZq4Fw#f T5zd錅 Wl lNj QyEoErK)ZИ.͊PYfepGƺTsȲ#=Ҷ>{ծڵd4i3Azt|u:ic9rr玾2 ]>[g.S߾muV˛4Z/E+wÆo.QhO)hOn谬䳢cy nry0رw9oz%xDZS,}b28C.swH{1Tױ,_598WȚv#UcDr]|k2_ {eCM-۪Qaэ׆~5m>jCO)~|9 'K+R6f)lG]ysӧG]7Eb9$^oP$@#8,"M+^ѣTb5.Zt[]LPCjjsrW:OQtl`AGeJ'o˜ }DƝdaYw-.:B2,_t{ dE}KֹYA㡋qhG㱝2o(IXkgUל5Slyʬ1P! p23aHi`GZF=jmۚҌ5 4'G 6E+b$?C&Y؂9D2VwKp5iKK3mΒkHcd}<4dY[/WmUgm lr:\TlTȋK=-xF~!+$RXdjQ|9Zvm+B=-4Ztk>$9S,EoPtV!dQ L63,ȱ>ͳcA\W#-qqGx;76\L~Qq+˕[U;/cdO!hX̐a̅ oN#6-0 Md^6;R@)u}I*c:ީER#7.NI ٍ*)d;vQ6MMs1d[Jdžb뺽=:W`gO@6t  óڇcUfljbo'hC/ 3v7E>8ԙY@^<]sSo.W@٫V0=wYikIp9xr^ 7Tg+FNfcY0W|/P:-@+$:hl Gj8]ѭbm{ N,=hl7 ۣ. XjʷH)GB:c/b;f ƿz.#0^#o.=*M\Yƈot3n55Tʬ}Hk xWO {u_"xek]Wh}VѯOb5%Ub^!.rOHD{FcٍqZm ]KhEJWnw)X9k$̵AaprːAA!,D4f㢑ɬyw2x iՠ `m$;o)xQ )*iqtjhNn7=kG v@RBǶ0Pb5 m@#"4 xRV[ Rf]RV? b=׺5|=]xjXYꥏ6O(:g c?xV0Mp &2^ƛwͪJݳЏWVn)o$m\o]6}*e&s%,wXҌzs|9Uӆ֓R4w ML9YCJ`y!{>gI;LSu)솵^.f+}TqnIrxfZ1v],>Qo #h{|=\i*e]! v!Ū6Mرs-vK:ί06jM",g&rp*Eek(|vcLCK1xx>G4FZT&\d]ֶla [!%MNü7 ]j S>>*O8ѪMgJݿ.-wɠa⏕! ^++$ve ^RJ^˲+񐗂0+˪ *.du9W0\ 5^'q%nOpйۯY!I7nj՚Yj$ P񗤯W;ڣO`{[ix÷ u+XNMŮUjF6gU9.jYOq\m00*~W$q}QN1Eۓ5: @:6鉌I#t!t4ج-kgfGKþlrylK=t[TdLOE=Cf <VۆЄ4\L'#uO wNZ#nOk6ٍ-uNDEA4i@cb5Ҟzah e`hK B#:-{ f趯!,KuZ-:x۸v}8:xk7ӓP'$P7l]2vjcf]nr9r9 J;W!\_gz 9\6keJ]YAR ȰE"^#=Ju)+ Șa}aY;7sꎭw+L8 6E?Gj[9ji/k@$;r-yqT+Lc}]Nvj%t_T{lE4rW j~{BիTSݣf e)jϿU`Y|̰T#;d. VRD~W47tZ%%kmER&5Z5qY -xk jw?'tn h5\^rs܊wwwLx{Vn&ѹl+-]b٣=؋ 5m[VJ3ǎf*Y\=>X;Sd,2\u/k iFig;jصC \s\LRտ>{-/dJܬ`('b*e쾕kM~E9)Q:G @|Kx[ ϴ85[³Ř@⩼-GMd7V]LLY4Y5%2X7VhiƝ4OMs8gVflv7hZtC-Stk65 f3?1qcwBvh-Wc%cc Qd!\՘Q55^`+(lnHɫۂ0rrƦJU2}eahzJ(땨 b8(\*E;+Fi;Z-rǚLEy4)趯WZ-X̰fޚnas :748sԭo+q[ԭJ%3w:91MEs)Wɫp൝ԚONȧbu3Տd/mlV)ǏVZcfc.G/dC\,ht21ۙJ6גOac)W88hЙ-(+bm6V1f7)/~(n pE+h)\BW(7iMSIuqUr9'mh Bf߭m `[ zqL0l[#kPض-7Dqűlc@GtZ-cn3v s73 2HKqUl[F!2mIYKHfF _I1oau{X|vJEL\sYkDB!= kI[&7ѕnV7rWܩ[:&whN2IimM$rDȷ&E"|yrQ3%څcx;Y-mFarWmD^pM(lVEi':Y 6;IqSDW5!2]Q9j+G%pX v*nz7 Y3r.@@Mڻڹ uzd{G7 p{)E:ۤ6x`bq0^6lgH\Kq. . +5 ,\0v OGp%ZH@1CgC$hZ-?q|aҁRtWg~Jj%jZ׮MV_tm[VյjhZutih$M?aO4X^DQ+U@VnZVUjZתjthAjhEhբhl+a\Eq"+ \eq.5ƶ"ZVյm[Vյm[Vյm[Vŵm[Vյm[Vյm[VյhZ-Eg!Fn1vLjjZVIDVU-rܷ-rjZVUjZ5ZrjZuZVUjZVUjZzj:{t-V4 5;z{uC_H[Ət%v AolA;PZUN{kl_t pbxb -B_~=5ZVuZWUjD;{`宋勹bX.勹b.ዝb6\\r(\r.Eo[n[n[o[o[n[n[n[n[n[n[n[ȹ%\rT&r.D$\zBUȷr.EȹW6rrBrBrs.eʹ&z["\r.Eo[o[n[o[Uܵ[j-rE>#6;nDv}ןBz/<7VG'M;!MwԗyMwwQwuWuYwUsa]+w,]Wttt.˼zdB싾w.u/0y6Y^aey6^cmyv[^imyWY^meyWX^q:󙗜̼eR:yܫ$^y"ׯ>r/>+ϗ/ _DB/?+/<*՗^i]y `^a ]Kw5ƅ!e\s(\r.En[z޹"\r.Eȹo[o[o[n[o[W -Dh5H 2cR"4^7x]{&Z= /\("3ʻdBw*d h؍5yw1w27!"99YWrr沔C+*) 9YBPLGʛef$^u*PHsS1ydgerǔss5~ z4B99?ț3%~&zJ>xfkCD8gdF,cFZ9Mʰ42- 1C;ՏVc}Q $PYHή]Eՠ}.a5$k {Hlƚۊ757uzn5l[{Y͝%MY93=눡 (4#5^U[Vˑ˕N"d]̫wL2 LBe3/1y^i2YLe/8y^y2ٗLze//3/S/s/S/s/s/s/ē!I9']r4uH9VY' 1k`qݳq!mL;KݽD ~Ӯ!qكlSMfCN\tLw+ոȢ,ccγ#D}(uxc_ZxuIɾnnj6o$&vZ,f!NC#^GJ #gǴS2m@1E..9jI'rY i\xf#4a*qYIqsEiGZuw"y"B\W!_)&O^ZqWQmho6հ-hZ--m `[ض'@ֳh[Єzô[,Aq#\K.:.%ĸW 1q5ĸűq%ĸƶ-b\km[5\(W @{tAfnKԘ!Lb^ELR~n΋f]I U89];y\}~U:;Z_mfWdh$pʺrlulyXWg€ YA3nՆ㤚lnCF^ .<ڐ*6i&J,b,2X/Mq' -E+MGid"H 3E,,%4GPiÝSԛ+ bZZ٫#)Yb,S_dEч#5Vԭie.w(Nper&ć6j i-vZ5D-:-uF|t hY#j9B-h:SMM-M-Z--zmhW`e갶Y% -_tK-olm鈢26uJi-5ybg=P+tpuE 3wZ[8s_$lq.d0p$lu >Gcrg7(nK1FLlKS&-bEG &E܏V۔nW*GBB_;ࢸ[jZvlPXt9͍d-A5oE{4`׹7Y*̐pL Iؑ|JLf0d_exc>r1cr\OE 7'٢۪(|/q:BшEQgV ھQQUצu_!ѭ}E@WM>V6*DZ:(7t,m |T l 6;G+|qK#t_jlC&yfYj5];K(dsHsGbbffv7܀4O1b<̍^|lG#<8YO q_Cr=)1mĎ/,M 5\6VMMrEHGfWK-hetdirpkΎX0a(!lD!;+F^RB&.f^c姏!VlxzZvfG 8kvܥ,UX{6FTcn+C:8@}bk$f 0hmcm[boOO lܪ!l㳮}=<6nUƻi\z ~N2gtg]٨NmySBL,ӼduSO7tumSw9ɳ(PB (PBB… xx3q6~ɢhW2G΃1`mkm˪xZ-ʠrmɦj7}t `ܫ]}MZ?ov[f~Ck]:NΣNϠJ*TK5[Ž[ս[ս[ս+zzzxzx5dR*TvoIp;GP:At%J*Ts"(ދj; :*o/~=N0Y*TRHt qa^&X3߳,go1(BOBV x,t9'ZRFqY;7j{WkYkC1>)r$zj5[ʵ ͥT`D}ibF~*Rg[4!5ņ ҪIlvUm*u[K~u-囜9qLs?lTjiKi6\b}V׳^Tǒh孩8+|A~N*ڻ*涥O5mowNkeV}0G>߹-n+o.G{p+cTVѮ׎ ܸ>k-Lh57j%mXO&RsDIkQ.싪\\CO53 k~2<,*TT7ᐙUgcO)e6Jg2G}[l'*6kϚgib{Hw#)XԨfU;7{ǪggM9Nʲ̳i26‡>.G[vL 6ϙ'< [Kfv1 C=mP: DMR9*[Sw<-)wj]' ?]KK u{ U-F]2큄TNgSMQyEBslmS}kZF󪥲-j^iΆOl3Q*'+!jD!k\͇UΗUŅ[Jt6kc/0UMUA(;P8kNp|ѼgM6GtH']Pӳy?'Wu*B:EJ0>1p祸ʠi}@U zuw^-+RTsw[IhgO'Dw]]lt['x8-B݄Jhdtqēl^Sv`|]ü%T-UGZߡDB- ܊iGx&|]ü%:?2?ӑ_hX}uw^+hd§ꡆuWhYSz*MOvc]sK~χK]_hEHXki'WuuPʑ(mۖ00!2ǒ;Nb8(mzzdWucڞiVVf\ڶuzڞ]?mqt5_3j[|]vroi5C G% +Qզi9Ϣ.)&xѧYDm-,7x]LBGQr]5:٩f_УLy (S:sTNOW^l<7VwRnZު_x#*H*THrGڌuKGJu\?+5^HT+SGVy:y~K^O]ᛈ%)?לϔyV⛉rXE^5.h ^e6s^T=ANCOV4߹$ϸGm6Sd.}(8EV2='n.Fɴ\&~% }|(-V'Q,+Vre/ONKq `5Qw~1pxr?2kʼnt2k@N+Ljٷծ%%;bh̡quɃKjm菢+grt?.eLPu7-ߡX"!5|fz]:oRoDL8zy3m Z6N^B|Z>h5uyIһnXz&mZ5 I1lQy-}M`[Pӌ>/]/EWC-?7ށ)YfTg»]_UڪqTURWU#Ui,wB ijGUsܰ5]6E>d'XP}-nȴoF-GUͷvTPj̫:70j΢Fɵ> }8ρ:4IMu;u^K_~*JO7't?o{\aLړ]Q@bU;z {OχRƴNAq^k-^\@0%TwHW  Ì ͏N+fa7gtm*T6mß5:s]c_]N}gS-IROgOG @GPͩ:K$ftuOb U=eESً.;`^2L[o'qOʻ~HsLU..9G+լYTEWu|*jPh0~Wn`.(Pc '>c=GBY1n4X\֩WQѼJh΁&\3Q{科^kO6Wsm)uRFk0 Ȃqo|U]U{^:+Û@u5oۺ ۆIڝf3 ҵ:#m9վ}zRr.Li}>IJ!Ρ 'Wkrm7k Я54^](F'[T%">ZCݝУMAc6m _t?/me T5-AR:yy.@`U]xoIf[:5Y-%RzTMj4i8nLKzUc6!cn*s óhlAUY`l{gln-泧~޼g:BQmu ʟ?tl^Ȉ G6ʨ>2CGT.)r+1u<.pUmYų "U;\KYB,t§m?(Oj=n]V o]UKT(*hҶ<~;oB] ފ3̪;he=%Fo xӺ[lU,t!k`tùUml{aVj4_-6[wt|D[ҷx=SաoT|/]待cp:JϚzS၄ ^RsorhTl3!k_VׁS5&L'ѹ#FcE<[k}cV̻kȩ(s[IkRιl7wUB[#-ưSoU[-[вiQISh;}~>PPwq(!Iӕ(* zoS[z.E;}ggggg5?Ei!mO5pB(4O׸wd(CS•(;Szp7%Fݭݬ ݕXbV%б     ( ;wG9BG' ,ʔ iԝӊp-X, vkvkvX,(<25e;hB,Z}>WGxz;PHRRQnfY3'0%So>JhJ<[6 oDkxślCb-X5`ՃV X5=(Ħ0|*@U.'ET!BJ5ψuU>PT*]x;܎h>i4`!ܟ]:X_(PG r*CF淋xxzo 0Ow)fA%z<<'QYB:4 (Q቟(PLn-ڂ<3ʕ%sQJ)u4k$,̬[ż[oYYYY!HR!HR*TRJ!HR!dAdAdAdAdadE=h4 ;#U646r}S&4X L8$J-SY,sJ+mhn\ysT;FjFVENJ<{)fZ`B#FH:nN| MxTOǽ̡!OyD.Y,JVK%\Y,t,K%+%Lʕ:J:ʕ:HRIR/+hyxf=GPNQ:*TDJTR> RIRIRJ*T=)#SjBaFFT((QOԣx{B4Q5xr<#ANcCLOpB (4<#Ǎa ù67(wb+ B kx5HDpP8B(PB (PF(Q;7#T(4Gzq0Aa1PB  (PHPB (PFXQi <PB (G=C@P(P( @P(PbPXV+bXV+bXV+ bXT(PXT4   (PBB %8GpH߅i w.lh>&"J*TRg?PӚ湮k湯y{C%.kɞ>pFGpB!B (QBQ1߸HfY,K %*TRJ*TRJ*TRJJ*TRdRJ*TR) BHRJN'#G <1J*T*TR+"+"+"JdVEdVEdVEdVefVefVefVefVefVefVefVef+5f[ŚfxxYk5fYk0 0 0 0 %dY,K%*TRJ*TRJ*t*u>|dY_*TR+"+"+"+"+2+2+2+2+2+xVY-oofYh g@!G %5\ϋ&Gԓӆ,;/s)%-AmAAAFQ3S]OAv}25瀣 } t]Da GS,EB!FjK,\_ه]^U2I8888!Qj-EGUv(SRY:ԾNkeF'%rI=Jc]qosq=NrLpryF#U( ..+t*tAMIw*tMT n_U"* p=jq3~~_<ӚDr:/ QUJ.{٩G\r\OR/rjNy/pԦRRjR~}[WwyE<ӟE$٭=gSQG=R-- C djPiʓO,"f覛<05Y%@#PAk!#}_5w}250ROF$ELn?7ܖsQd~TIXkp~C r&S04 %3\:{QBYs*\}tf TcDeuJ/`lG\̎.dy丒JOjͥSZ%?!Wm,3%=1 !P\AE=dqUz~'Sn?KE#g~%D Ik9D!= ?/W|#MGL`'Ie$?:zEY&l5p. CFD~uǡF4n]"'u5AhI|ǡGoX,vӞ}. &=<ױtQs /r9*cHb{O_QSZeTjbT1mb oE=?He/]ZaUTH7$j22Oo>r5j?0#KRA[99T:DboE=?@ QslɄahI|*H⠂A m[O7%ܒJe6.#zo:ڑS1Ȯ .../CV3Ab%\9RB7xHQNX0O0I97ȹ"\rr.6Ut`"$CGyx׆2ө>'{-EvUlHaRR=3]g d{NQRx~N"ʯbTaؽEEU=S==jN'bIW޼?Dʿb?b?HĩO&|)W S0ŋCTwo#P@%(bRJ*ؼOBy>SE\ԙ=cݳ{^i= ԡsڲ(8=( VH"וpDxKb=gĕRgt{>ǿ"ً_=.%'[Vazڽ}w Ny5)bX c6A Xk(ןƣë|I9"E[Bqz jKZltǒ[䵯a;$XqjSnyPYz~ė:;kO2I$9\ѫR{/W S/QGX]'&X c-~CRDxbU #O\K;hbtW 2 t%MU\i*~Eo5| ؽMBL^{ŏKhwسW/Eȹ!Zd3&;i$Ie3]E_9&wWbI9I9$˙s.e̹2\{/e\˙s.dY,[i|'ǧǵdEyX)$| 7S~H>I.E'At'9'$A ؜$)؉$͂2)eNᔶ=OJdOidqp"KEB*2L(ʥ$nFifh&4^*)tWBGa:f#F4P P*vTA 3C͸|UI$|dQB\JU&5^zt0AdOj$y"!<2iMh4#Hf46iM2ZYk-e2!d2 C!ZYk-e2Xc,52O9WK['}U1kU=IIF;}ht2(PZUIJS\ {d1PņBK}8q;+,{,#뉋X3:4-DeUVUw FH_dJjLmsJH  :8T=*٪Nj$#&t;u\b~効0;^Q?pĦQM.e z/_I}%"\DQ(Q>G(v%~Ȓ_Q=dEIdYs%K%e̹2\˙{.d{/e콗^{5Fjjjz/E^z/Eȹ\r%%$N3Y,˙s.bhSZ 5i11.PT1sҧXKب,E#Lt F,3L4 3L-y Yk-d Yk-dZY  8uJ[)Rg= "[!QAiii2 Yk#.݋0뻇8{lBJvI;lм;$$I$$uCJ!M2ZQj,M":Ij,FhPX:?D3L4KBL4bEA: l*df ek,ej22גRAk >+=F*r:u.2BխlîTy8*^]EcAeO|Qw ]Ϊdj3>n-SP%{X1m]>k*^IVمW^zBؔ ɎyqlK\>ʔ=U+d| 9{Uc-[1Iyyy';Yr.Eȹ*Y;3+FYߢĮvP&|{XWyRy'<ږ:LOm~#Wm>"9=,Ur+lHK1"YZifeH1t^ږj'>%UZ?[>D죤Sknva(bU; [IKjֲ'5hlï!oįmI$~B6:wؼ1d/zy:s\U=\NJr$I _Eqr*sr.EȔJ%Dy":kY'CYT bS}ZTxؗW)dkcǞ~LlIuQ$IqqqvRNRI$$vOBr)BY,I$2\ˋ$U}͕_݉WۣV1ӌAZZAt3> dII')')$I$K$K$I$I$IvNsԝ8{4 2XYc,e2ZYk--e  2  -- --  -    L8:yG!ݿLW[SU>xfwY;\ui[ؤ.9s9:]KRS~]94;}0(4ͤLaۛnUy2. ;Osǐf8nMcD ?ڎ[ y|}Yo v]PMUNgUj|Gqq9 1ީlƈ4ϛU/fw t+5xYuW?i9wP~Ky9Uٜu jz xQ͔D`;O\iIY[oO;?0Towuup˲cEbU͞I+_<O3z{wWWWͻY asL\0rO;Mm=mss΅2&8W.9$~gC~?Mm=jȗs.ǻ}6G나R]ɇ֞~>|Ppڬ)?~jTZNvg?NYY[=[Cښ&Uyo:G͔DߧOb}5HCZ>"\n'C7%C+?sCi+aws|z<^??~+Ѳ`Wd&?57~+Ѳ`W@Oz<\aӾ+s'3+C++f8+#ܯO؆%?%L*\O~ T?#r/G{Hgrx+vm йҹv'VX`irhnfUy(:<!4v5P-r'W;+\p\Vg{`\%X.b'~*W;+jVxٙQK]JJR( vr~(rwYn7' . BFM&WRgJU*79.6#4g{+r;|=icM6RenU. YCt؟M{_"bҁ͙.F+Z[H9!E|T9QAf)DlH5u]0=ΩDŽH(b0 'itjNtBXEN۱CT6ա䟫 kL2{a"nY`|BuH6,ޡ@ :O5\Xi Tu{k(AEd"XQd;LS+INY>MݳȾOV{Ӄ@ņ}䲟' KRBr@5wYH u ]q<NvToDZ[zǕN3Nc`9Qxik3qMZ7(+-99$O +MCc2H3Hri91oYp9$A!ɢ#gb:nŻrkYeɘn/P7ڵ*@krw͝8k(srWKPO=Wb&'Cdrv,&'7_W3$tvۗ|?|ȒJ8rY?eҦp:(r85p$y^s=Ͽ\λs\*+wEqo ɝ``v4Va6s0q(9-O]h ;W}DH^ֽsZ둹qnM柚Ȍ54%QG8+U" 0;TRkL qVvNs7C/_=;&FӄתƟ-& ?D |pjV=2 <ЍQڭb+_rv <,UQ4qxZ$m، |0= D C)ϩ2a8]hL9:d5Y8n6FA\|5!sx;[R{~NfQ1"8JBFn7Nw.f!:fz~5g}Õ13DfIʄmce>"IœYP^!0h@8ܥ}YVvKO!Ml˧>kH@wYCh.e, V)P$HuUEx\JZF dgV8>Bd >(d~)+r`Q]'Dc9NDy2ͮ? _á){oYLD*ױݚFh L[fywk7'i廱9-%o7Yvq^WVS)bЙO,Ua? ugr 4.>I-[Z =i$LMExh').჈>$6m] ɷpw2~YQ'JR S@n;>&yVg;eY;}|dFynQHAɶnaӞ#7;@yn}?k 51)D~gEu/.D̞!Fi{R5663O{dn.Ϡ7O-1VVGU梣;噝go>W>pcl  '?um6$|S6\Wc7vŰPpLZpdzk èd0aHy2'&pDoYQ7z3rQsCYrrMIJ }eM-Hu{y^W?9OL5s&9L 1E`oG+@0c +ƽ xw"eAa'PwOo/*_s^j\ Dvs~8&NDu,)2$8mhǔFpeS0 b>bZ4C@Pa~Pa f' 'i9ظxf4=9Xb=M3)n qLxM;'1t/)όI É4<H;޹{es.?TAq*$~Y [X6C*'g/&<"~sLlI\^5 Ot7mQ=tv i[%Xf W ,!;}Gz{'ajgZH.~YM^ JV\B>e٘&H TNWN)D<J"w݇[ LKq8N<'aSX78ZoTm-&;\&PAŃ[,uOM@ 4늧O͓<'0XÀe"I[>G &AH}K:5"xQsz^\C깧! 3;?5B+6,E}^@H5z! pS~X_~`Q4345(")N8o֨\"xG4ޫ ߠޠȮuN9<='Ԋ#Vm3YK~wf&I?qvf C/*'/(,VV5i4#I 4#J-5O_ !m;_%7AXܩA ]ԣ)`6q_DhwտbiC"_cZ_Z 39Kd&ZuQ=K[}8c=V)C1C_؆h%j` #,V#Q }T[j9*֞尷UK5ԑ՚Ƞخ]ugwnhl(k,a'g''?\rz=ܰ8HqАTiupI\[ b,bxn+a'NIlj'QsBJxdG0?h4iqs9XkD:OrK hēh+#yd" gVq\rw׭a1dXhm1fzy߿uLV|ʘ3(OhS8>W7Ugr-L3GZs@8Q ʴWT3WRW[JvdEiɚ2߉< R0d6z߿.(rP?'\=c{7 'zbIt?dO}xNfe60\qOT4vG# Sd7q Tl}NͽvM*m%\ TbTHU ǃٟj8@8S>ryNjGШU[[OjDm\?PpC^O,:U'༮KPvbIHo|V޶zG \x|:G_)8Z?-Y∏pN9!*/,wE>46ül&C ë\F#[$ȇ'n+eL޲hd6h~"t ,-0j,d!f遬-{ r@0l8$CZȦs"3i5O 3G/2J.L0^|Bd_kPRIA ۄGl$5X0i!Q%}hO6"Zس+Z6&=!t7sarj\nLHpb#mOU*#L7}d׷V+5V9h#ʰܻBGbpUDZsm{ Wjyli1 ZZA'Bfa M9mO1Iwn`5Yw}S/$N[9 Eȑ ay@@-)[mOH(ZƘ3 ĮN^U7fgj!5ҫb'k2e(Fx9{19'ĆZ/E5HM9֯UuI+f>`=+j}ekCW-U{[bprՈHGǪ=R|6IkA* eEO[g5T6Caq<&]aN GyX@]궼 #3{S48fdaa;b鵈aVJw.XwpjԍîcWZc"D>+ yW?rõUU$-zW+h[4wc :w-z;9ˊs j=ýmϵT4h~W\[0ض%UZ R'C wOUMk8ΘX[JDCuy.>}`ta*x 8 EQWLS <x5hs.EhExy[ U*9jM^L/A0AWA^ z/PB^zezazO0'^EL?5S&?HLʶGCsـ'*YvԬ.>`߹Q*+'Y{zC]ܵxP[BZхnL)<9tɍ+ħpd^;7s&hPVi ۉn*DL5Em,Jc5jUbuS:ᅮ+fW+Ͽ~Y>mpd-r֊CGF{; 򁱇SZ%s2 БBݨAl24\@K N& Aʁ c@nI7^;tHݥj) 6^P<%G"fNh~m|V[Kkܯ wVo|UUV YYVo|x-W[Cj-xxx{cG>^DMМ}pkHܧ/7&{zIGVd;Zw}i9eP)ܫBKwP:o$6 .{Caٶ_~@'ޟ/qvR&TUU'H]K _zF# *ga[C~]*)r[;Mn*d٬ \{q5#xDVjL&JGd c$ L^j$FhqZ0ͲVaŅ=.`%%"W]q)u8'GorhW-z+[Z{$> Yv1wyP%.DNR!oR8|\Xz쭓$s5 R7su-S1Z6M>MHlTObfP :q"sacg.Β&e]42꽗qD6y+sq6Utָf' fo2\e 1:70TOSk.)chG]Jle,&o($IEnoL8Hzp =]R?Ά& u <'4^f30 =Ĵ؜p7j9„O9@V7 f2*X}!J^a&XDꋍh#g+#yCdAa5ӫ5gDVbӫzw,Q% הhc~=Y }x8 Mqt ZK7Ьr&{)\ ҕL;ET '9n7j<8:W_C[sl书S\pEɱ`ߐ-E#xg>*_';GHboH[W>-32WĪuyA;$6Ϫz>=y\N*٢ՅeʑⷶelwSĔ /(f|i5~|>.5wWK^jyd0 uz#%n]1TN -4bFxo%aɲ@eJk9͹<@"Le)Q5sPxSlR%HJ\:9ѷHxPp0_pUdA4Khg jY ]A|7ޑo`E895Y뫻j+lx)EJ,F.7E.)9L3Dnm33M{ly{HCpA(q&?MČ_H[r"JbMR뺍AbvC. erHڙWr&R"%?T܀ӋnQ3 `_7=h2T80v71TPxk!؜!Ҫg0p\Bgf{aC*3=Z#UrhX1؉h?m<*]M^鐽鐽pP00\o;rtYXC=K? w6nO)d 8Ҙrʙ[3ND>ND Z!!cjI)F4MvsRnURZ&o t˨P.mtXspvr:g%)NH<E=LdͦFl'OQZ޽L̫8DKude6}?A.@cˋwhc=aQ5zՌ&KZE ktXܝMw0(G)~T ⡱cF!I;x^EgjI}w!7UV'<4os->P6j׉(L&Pëކ౺#\=˜w14 s7MkFoS. BHjK=[5*ӸCyUCMB"'mV'\ ՊONK> cq曚,<䧙NJi* e!ka`UbKMJ&MJXnzPcNv1kF2%l7B&4a5sIs7fCýaR4*mZ) *࠱ Sc LZ\! Y9-]qԤD_"*_7d+,9@iYŹ.89A _)kGb?;'U`Ћ4ARuSOZ5"h=n-BXiw9MlXcª\)U)*ollV%F̥œt6[Xa+ 't8orHYpj@lFJ B q_Jc6Y02i>vmwdNZ#a2;bsD[֢5 |UF%_hqy~DX^ܩHǏ跽,m6+M7OinOSd J$S0H0k 00tB#V>Ba kV(&\eC)$FGǢ$ vc _dp]u][JB`ʢnpKDmJǨ;v6FK˃g|1 L)@1fZl1<qlD*3zl񒟼X p?F't6OYN1N$w1Ã$%9Xp=_F;/h5;)'?`MnsӕUIV_YG]]\8d73n>bc/^Nnqm!8{&b~giMzM0zv$0:Хk`:V7SRATC (Fr!04&%-Nb"e}6O5nAsD>ʖ\lO,gr?!|0'Դ{ǭ2$HPgQñGiii |W9˱q\W˥Ps kZ եȝuvX^9NWvj/%r|'BKsppVNjf{*ʢn$0O )n78;1j J#&ehcpՃ#'=azv>J@ه G$֕#ܝwC'Byhb7 RqO9LO8lͳk5|ޣCeD_>D _>޿EqpHoXd Xχ*PWMեS^ܞ$HǤּ'2V6'יNU9dSQw)ysIkr([CA谴Qd%[ \|2esrH? РL/A L/@/AA&x`v8,ky(,0ΪcG|W\W5+b'm%e.)ozFQ߂qY _*e/S/קO^^ e0}럁~ [y?~ =_/msl[ߊOoz^io ޽/((+вqWG^+?[Ucr3YYYQTfQ.3EԾi!$Ȏh+]Ǭ򪮨@3$mZ$Cm+OK+|4Wh>^azP?8^EοHqC6rDK̓KOل(,.R\)i&3V*[e_G2c3pµ^ز"׻ =N$qtPâL Ϲn[?!qBCqVXN0͑̚+$'򒲲n.NI"0VʲŁ2`"x^&'.taC&o[щh5?n Dke1cmp(1 WW@Fa& {`%0+h-^yWHCa>< #gİX^}B ݌`JJ~P ^릴P,hbSzPfh=AAC[bV 9zX q"US-3!d'bv@3 nKpފ#Lg%aUe='0LИaf+ԜP!`O UB/ݙ̈ec6 kqV,]A8H+s]B sp;7 6G |HsXNh̊TBfeEIù2!>,S2EM 'vnF7c{nLbS+;a THޱިzŒ0ޠf6ȬvE&T*-LAaM> 疹$-rV Cɱwܦ,Fb%\gb~J 7<4NM nKO쏊kLs^sLS[Ǎ&m*PSJHhMCȰ SڇO_(G bZRNNfQ\љs}ᄩ  <>ߜIV98^ɟ4ښz; :j]ܝDv9^TރoDD ))_xlǍWO xIOhEtt>X]qy?yFipF$'͂fA1R&a$NM͟8hl~+,G'?+/w#SZ hHM 6p:!4aܚ QIIKS9H )IB&@:Q{dVL3hhxNo)qMfzǑLѩG1Aԡ7|s40wɲQx@A<(yqY$/Vo9}B3w"Kog՝e ?UgtVR$lՍ6_L^"hd%`V ,%~L5N;[* IK-]YXUYmã]]1ӤO&{n?FƯzA?MSNY{"DŽ;ZH_L=Rd)/N8[ /{,:&tH,N40DCBi/I{zXylYK┓Ȃ[RvQ[[6Z@E#]w{[bwNKC>,OCl'Xǰ 8RH.pU(# pV|||E 1- 7=gޝsz{ż1+o[ӄ1,G9ʼnw--'3d=7+lW8{9\:-PAN<[aeJ1m5m5m5dyS47'ߟ(Ƀ2Z+xlD.ll cC)x6W6VSB3{ ,6$W6;soYtS ́8fs>u a d+4ȼ73]_ C4[@g/ r{X fTph1 q!> d'q bR2ÊCEFQ}ȗ4C`% e1wP# 3ttEp {-{FO*lYt`b^7y46 b^KNzQH)q6(KѸs#4BHn$ΚZl<Yoa\@0Bohܡ`Z%󎯧,<|6esLPv0sal{Ǽ1̛\$u chXxǜLohT@c> .sܹ-֜)r"5 TPk '⩚YE0FzL8& N+B}|CذeQ>WfK{ N&P%ha`an5gX\D ;$1i`=ȘQvO?h,fI=3d%؛ (]93>Y t`m2vo0۬G_▇bb4yH5,HI`,k=٩) rCtgzlxN5=j927zS:#`E-N?TN~HwI:*y։0p2:4;K apte[0wʋG Xk>g-vPpAZHo!ͧ4⽰Mʡ8\6ZkTkF[MAsSsԀ6*#%ݬP'Lk64E, |' KђY2"uP:m0YhX@H9ؐN8;'LSkbd]Zݿ6."t{tW֍_kC ':Q6?̱S8@Z|P]]KD6 e.lN Q.ENH 8MRbu6XDD+(ajl73[,:p CJvZyx4EAIJD@1nrbW5u'N)m6i1ZZd#=sq ~Ν?Sϧa,BhvxMkLq'#Q֙ũgGv˔]{,"1T"$9UL9Bҳɼݙ)L3"qpBlТB|$&XaL7H\wxob_wϳќkXQmQ!kwr:UUqz6 h;֫Hp`woY ,Rr܆'vy@om 4ꒇ(rWœ7oHn>QLe1vYHf,y$sO`#cp?Ň''|64 1hB #S{[;%. 6pX +fݮsYY9el&WU VURkeٙ, 49g%GGq)UoFIo8G |dʇ2ZEDh4WQYoܶ\#U}Q<%خz{}Ho xBɧvͧˡ|C9Zb !Xx|Pvn|*`- }+:rP"tݡDzmk,YB0h ):4ڨ^EB*f vLn%ܱg)Ԝַjy5'ݫɺ]NZEa#ɼ'evq3܀;ܜ]^$GL (gvϋ8DCJ]]Yt.ϾI C&PE]m3ҡ@UKuܛ͗&VvP~(a *zqU)lR8M4xWW*fU,؅tiCԵdU"Qf 9q*I17ٷ⟢䭕CN:61.d;T@BKK%a3;g>=Mм@??4=&tM 8&Zh"%"jC4hٜM'ACX'37I'!9+1:Zge +ءj(!yEA1u0𮲼DrjJ-i}S$D-@ؘVk%pq B]KKԎX+,+4k_f u{ Rd~њ#=a$!]/>76#JbW9p-V#:IQ8̀gu!e7(3bd,aBZzbaDtaeS("o{aOS$b ca-sw512#Dm<|/jc3⛸ӱ]m*4b>!WT7Orebut%8zP,-@,&WWRBd)KS9Q wKdJ-S^2ŐaF',e,=ˊky&rP\P&A| `*x}l*6S!2X9:R-R0NRk羨hQ!(K p0=K{gj SeEEy'`FQ))8oG4X'e@SE#)O6::_zDait׍iQdB*=<^e߇!]scpPTN\ ɢWE'n+KCô>+Ɍ|Krb2sg'Fn_v W3{nIJwv4_?kMS@dtXnŦNEe3UeDn,x]052[H."bS qܮZMEd8)#"(h{[v) R2aao= Ѿ)xEe!(0 a͖AcY6{HMDImB3/A f{%?gܛȆ:ur]QwA iX^ N}=H0 . P)A$oR}N rf(?hj+#7 k)^byiΉ4 hx-l額ΊEek4\o*oφl$mG\"8:2wދ&fZ9[2p*J 9 (ȅDôb❕JlM(AN=L2ПMCnWݭ+DF;8FÆ}kr.gà4Sc5* G%rynQ)&?d#Ǖw܉rP.<5U3" bwZkl9Hq59iDثpP9䣻Jx0+++,$$Z}v#)>NV8)+HN#&zw9&+0)ta@i>Հ;W \IYQMbFE&Y,^?0m;)`IV0k&A|Kg⛦tiv Ɖ Sgzޭ,Sfڣث,\su*onsA0Eܥfl<$Y> o8Jp$pq fDRѪ<3ۂҳYÂiW8~y0 ; s'Lǵkȵ[1/)'@mb!qbZT8jLlo#)n1<ܰfHaJka8>{kLapYD&Ěӽ]OH$9"yp Kel,C4 ȩ 9VVGf 5. FN"Anz72r$#knWW͖uDLrr@NZ J1ܫ,el;D$& O֞#;(Y\=SəCx-qbM6ًk) 7ƵACg.=Im& ՚Ÿcavk5ws⡾u'РcHNEG3ņ5?(mmc,^ Vs Bh\VO!CbrÇNwI!D|'2i4ƗEZ] O]nժ$ 4m;9oAnCa9b7}y KxPl:wRkYMeq;z9Δ^yAxNĚx#%G|TXn%T]ac[kU'#>)lvģsދxoG5ԊsP-@]\ b1l:&d,jj6jq^'&acpEQ! /a؏Mm1urbE eFh.*!KKRW쩸ϻ4(#JMWȚ>$]o8ʹLwW-;c ϝbZni:T0rCP߫44J͌/wR> ٭r6 (f ܷ-vfL7#AQimWW +Up]n~?8 54x!Ȟ1m(N>cEBAAE7޴Kח|#n[-[-V}eslW5S94) жBz-{Ki+m+:r\O\Bsڹ\?57:c\}sW8em+m i iWbCޮv]+kބ&= wɢ~"1T`A.b%C么1" ,1!x"~K濽s_޹ ๟ <,)CJAٓI`ޠ8mY5o[%N]]]]  SQ =J#pmnS" yW- wMCcmRX+`-d*1䗴yVR\w⛌\iC%--p($mh!OoHY9C8 ݚ^֫E~E)XNSfiT7\ML\+kTxI^jyU܉b[Kim[AmW 7WWWW"3UnV X-Vͽo[[Es\<Ϲs^.^N^W{{m^l^0\%A\@[pj1rx ,"1sxfML~MI &ѭ3 ƏD|=#.wA ;2􄎴#~1ςv(`UV("n%g;r|IaŻ5..(ὑܹlwCe+[ZtӚGrs/qUqqobП#s' wKذՇ;(5Qa;Pu]ŻSK"$fyP+"@4'e IvX@ A[Aq.qZHU y,{z.XHǕՆ#l:m$Ԇ\Q`F&n6SRu ,x\ 0/;t6{V,&#>pSt;,`=!X[2-Z/zl;P5qSa}'r,D5ͅKdFۏz{ZqPcCQ9.J&š`$/ޏcmKs`M v2B p7՚FL0v,F_%t ͜V,RDvJhnSaDt/s̔-H&9pڦkX,z}p*p;Jf߹7'aF#v|\=1vSDo;:n)0ޙ?dONS1UiqXC(NCE4!*_Xt0ƝL =ñFC={QʏWk@~2 {6! ke)8A;%!~ s鋣Z!#G#eOq;fV+8A1Νn SQ9f_|%v'| ?|SRug&Όl~eYk[nIJHDɹsZ3F LM47YD:PUN͔XZ)eإ!b h00Ih  L\ lähpOdV>dj{ v6aN'NH~ibWOQaꨌKEiSԢpalDIlTG8u@\]ڟ@Q8 LDJ&P82ܝ7WHLb#KnNc oCv:+t4cAX.)Mm6UzP)[6ۮJ3΋*!1AQaq 0@P?!P:b}I*ֺT__Z\'ڏQW#ij9B>"h#EQn53 uzSqoax}e>Czt_Eu=~I_ۥ}pL'ڌӯxba5+_]tV^MTц,`u C^8i*&On3!A#(F2zR+J+;¿1ӏ}ZJt*Ous 8qN]|]WzA(^m/˂kA.jGKxJfuo^~>\¿_@믨&K?OYm= 73j~ a#ԣlL'WWmsCI^?ɩtW~J'|şt!`MtMy~ yJ_O_]6ש33X?PQôvӴcN!֗wOH2΅QX5z *>_YsuUc;t?|go4e0}: wiӧ?O!Ja{hS9{yghYϖ(Bu;@s84$:\_O5m{g~?A6~?_M uc} ?gt~6K 1Gי L0ܾb4&~aD5YR`/?1O!}:y=zz̵1=>CoRLǼh9-*G=";m6ĠM%KNaW鸴>avwoorko`..^MXYD@ȭ#TKUk*-Z=_wh7j:'|~z+'M _mh>=7.Txwׁkaѭͺu+˺TxM7Y4i-ʫɩOAy+,S"F{ǧ 5im 4}1Sxv}4 ^ӦwS^ѷ_YforVنJ5z3n҃#TuqCӢ0m);=PM'?ZֿK~cs=J?U^ !5wz^ m/TVҗE+lt5Ma"on|Nj2œFN"Y}4dgԘ,pm; g dP )~Hs~,4wϼ{А,&?O⢁)*TIje-fi{Kb2K˼b@Ĥ15&bח0P8#l*Y>3Ѵ=C=Z /:4|A`^%_? -==-/̾ڽ4sB..^r^L_ /ic2_Mhh#K4BOE"RIC=Y3,k=cZEt*i9N `赿FuVYEyO㴾ӡ54Y6M!_Pu}ʞ%^&oIG\Ϟ׭ǥxYQ{izKj4Օ˷2TB?F!S2LI[ϬN{VJcb[MbM2 ?Y_mK͸멉}F44̾B5}/XKGvP:kM} gHҚ ,YyCt9cA"Ĭ6.weZMo 5YQ3++2/k9 51|}Zu}s :8WQ]>Czm*V{tezM:T5YK1gߡ؊5EwAsK AT^{& /vӁ[2we~z1=H}~/W30iX)αSTiulul.e`(z3)uۧoRL+Gp%շPʓCu}[}wzϖi禐.8=n\]{]ڂ+2COQi ,y?o]7c Rܾ`by L\f =`ɹFN? '}?S8`/~y?/F&@t5}l |}^_72,B=T]b.SvÖ/y#`Yzms1ʽ 7MtyQjMsV{ܤ; wu}l?MԻK %YpgOt[7c,)XA,&閃TRJ*TLŗ\G > MS@zɘ#XHSӱe"CC`W)/-8xiy̺BzɬV-z;A݁M Ez̃C꾗ܹKGOƽ44|t::;S5Z:d7@#% WH B*Kz\^e 5J4',&Q=_!XkZj9n1w.x]+ʾ!^DިS /4s$TljkisAi.^5<8/|i1ע: h̰r㴨`/Vyg0-=Aa?vU[:dV߯i~} zB#%‹1FKeK/A=۞ߐCCS[_bODQbՏM{Q?ifP׏L/Fٙ-Jhvyk|  ƑdЯ.[:mz5ѩGx*]^~! DP[wE]"AJ1\Rd[O\`M%^n{O0ݠ`MO;4|CO$FA<YS(m2𮎙Y=%+f^aOUʋ7K> ʎ Kj LdR:0϶\]. ^;NG2ƪoD1?f,ϣo!gDJ`[5^%4SGYGM]b`3N_)6om++Vka}nT6s!)aT@CIb8oRh)}"# _QӉOt"qko>08G5;srOL"ٞ(Y?1ˏ̤Frcc/~ :F=&Ϲi-8%A־D/u>:# c=B{³X8_![.-v{;A ye&xNtJ-xCMim!aS9QJU4E\D \g*"~HZ.![&ӱspB9yGam b3:4m[Ҭ*s8nZ5d˞YӋ̱We8 TA e 0(9V_4GL 3CVVݗ?v"ӵu4KTe._?ᬻ2!%KbEM>a;\״ZP@_>7]H.a:4*m 9ws* S|qg=q3Or!ZII_&=]mC1qPVx.4{S$щ`߱s 8'g1fؼ1f^'_3Wit2|:}91!/O"׼+5&:Us<fM-O)]/Ox3Tm9'dOhLf ,PD^ TNĶ]׈_%/y{+PIS%~c\C͕w_ZCm3pi8 ҅šXG,ei +T1۱P{45Lz|eF|/E=}!d/',5*fr1+]zKtn*්u;ъT¦Fş}{wn̩8jږ7ʪt_N 0SQ!.Bu.kYPxԸw%!0wWtZ}ksl0]zJG߇5&ӫm/ٞιߧc\'9ԃ}j.OHMa Ɉ5-S),Է3EXB_d M KZ%0Ir6S.Q&]yG!LgjVh w@oJX^[Qt\"w]i/mV|=ͫiE̠ i(y)bfʻ˾UPL>\Jw# 3`:0MxR#jKM͂C"FLbiRF C;LGY2PM EbmzF|vY.[>ehUa,sWSNt:XtǪ05u[ ~l6hnwBb>gV%vLk3+jhpm>_yqf dVØѳ*X@!z6+i5c$N?,?Ekb] l"޽GE/2qg*JC6_@/)* cWP6B4w JT^X} col0GN` G!ΝТo k0ύ,0ď">3=q@>͍'J:hqQ[l.eSlF~HQha4 ʸ՞Wӹ1ZzOi}&C{|$4X~K]Kۦu%^uχnO> `x!P0 ]Aս+m }`COV|ۥn )ChCwi,_aFhZzK>"É! jʺ-.N˗v1@F?`mh= s7}zk3WnJ/>LG}F( SsYU3DsZ~,. UN D(-Svci2 xW"/cyU 2TQXPVŒ5hS[fZ {\˗=c5)GV: nye&+Q\5fj6see{sL Ӫ1i{ e]>>&5:kP[s^/z|ӧn{%]Ken]37zcc_QS$Քhw^`ܸbR=,\~11CL$^lv^Gi4S70VLbiIʼnKo>#'wI*Y(u鞍sq'Ii0-}Or,_/ xC]4mVGQQWL- R,$׳A zS)W=]ቻeэFS:{LTSфHŗ.\:2/F0zAcџ&VwD,V7 Yyy[ө4tTYYjF'}Ieq7MKfmOIth&3=:9l%"5t]-443I3>QtY|lRk3=hD5Ǟ?%)\o~szqO=v'noYw7!AQ#p+,Y{n|ijaYV)auM^K8q8)CCo |59RLkSSQfN.ܯ:NT*| Z_K̸+onbcϼѓ:ˣ{jJUWvT vOG0Ϡfx+?1 F2\o`58/g:7,Qsa2cϯ6ҽVд[>K>\q.cߥ9o %AfgRhydqMC mݍYwmNюM2ml59DH-8\1ZQĖJmVηMSxܬf@) :M]k_t#.W2蟻B,u [1?Ħ$u{?)qn` lۈj);s/TW:5p\\״UG>R2a} ǥ˄cN_K4fT+8i QHNuڒ eX@w"Ӽc71Tǚ Rknz'JhKfWKOI%FFwԥ]W}] t3_x:qƚSg·$ ʰ-/Wmjq i^JRfz1͢hS96M!*OH2>S)!՛Lg~/&`!P6^j&+Sa_BZxgk*8VAu8fELuo6~ "d5U`~bSLhڽ'W1W/#DMTKfn_A1~74=^G] kѼCK6nb25x;oH}#"a1efePg'ЁPž*k ?:_ӯ}>7]{EhꙩRi΃s;KJ:[M@6F*7e9_T3b1f>sǒ\ֵ$0DM {EgF>a~%05ɔ>n sSCY/G5ۍ#j6N%wDŽ(f, 뷼nd"0۳J3Gl336RZ,1aGr]YGV:=({<97~@m[eю݃ lz5ti%k%i*N>`F#q3yEߦN.|Di9r.=eeb˩t_L#^%߭VOis[SMbJs&J}ʍ.v^8J&s۴vl sGPV ;Rz::@ X+LYklr\2KV9N?L i,fVhS\`0f'ΧMEfۓmuAFW;,]ƐCV'm? hcaJhӗ ibR,fa2)r˗. qz1XOC)Wtk<V?hCV;Bs/ha, xiI͆#qG!XthJOflAZ/Uͦ~5.syK|uh ڋe֑ZQ,=m\!~IO:4[lJ=NWD/ :Կ7ݪ;U]-a q+KMx# t@)@kIn%jo)- Uֽvtt6Wc wXGS"c7Avpѹ} E-9jh9ڞ\07Hg83.h~XO]\:85nZF(3'inw ~e|iMy)/ӫUX_ va7W܈Y [sq-`:^fL?!ktA Fi`no, ؏),r˗/B~zp}#JDRӳ;ͫTۯhhF Qy:Gb M+8g^ШhLb4zƱ{4)3j-^?߈8o_%4Ǝ{ӭW_,m-40/Tg,i(7t\5f[3Wepq?<˴x]S[h:Bk:bPOX` 1+Wf$#-WϽ-.TRJW~e7^X >^H; Z|+AIH ?1bz#jÿykpo9m5f%mᢥZ2}r3U:c9n@p;*a iX1mPP{jtO:}#FPy9U"P>[f /K3Z~?~Q~}ACgc,"[J/ADY=JBJXos|!+~ 2qSpr'{ѠvωVNks]F6a7\w)jn,s?W / g^5O*]CDk$[*3Zě,Z'@eJF,c}Vz ` sPN@%`b)ڡ>Rj&ȳEeq4}Qƃ>jxM%/,6wz/yy_1TE ? GgqO1?G 5ozŴ.mzuI(ԹhDfPAEQAg+GmcCEsXU6eRVg6F9KMaM3WJ߈(B`xVN%۸p5 4a|2.q|:um^%G]\`uļQJdkhk5Vչ;n\ۨχGW յc8Qwl>9veez5hKyvw1MPd];GP\]1VoQM]+O aJuXc{s{8ykW*H&bˣx +r]XXK*䎰R^xfҗJxq)u膣bKiCD˗yy\h_ ‘5(Jg\,&=%]Gϴt5Vb؃Jڕ?i15Gȳ^&'yrj3 *qk(к 2=jj߉f09yT`o<Ѕm0[L`1mJ/ױ!R  \,~WИ/i]:5sFݛL/j[G2Eą1cz~vD0r,n :6_Ȏ'2 >>Q#/B u}x*П z5(ȒC\|F{>j**]}5K.\Yr.>e߆_}-'KIL_i.ؗ[7ġ72 }e K'4~ kT.dL,s 8kq /CN'h}FBTjNAef;1GMjyLQ|q At1՜Yi.SS]鵮PqwuLI`n^e<RshNl2S'$ Fkk,c5h$/r1bD^ã~>;&]{:GKFDE&c[v3TQYQ~ dn?c+_2 |ի(vQ}5sP.|+_3$ _/C~&?_)7шPhEŋw.i .}/ /D8%Ѡ-O[i@i(S=T` [7R9<*%bU߼.hyvr VO& EgHho,nO+x1n4.P13v7bI;» 8ٽX`NܭenZ)FŅ 2^w ݓ I-A#1j8Z/X-h-Q+$`Qb1Q0 ˘T&C([t?e543({ŋ )ti-l 0u%eŕt>DT.}u;S݃7~R4SaEm{$HWW}]&Y%w#/Cc}t=]46˗].\:o/wľ_yrĹr<;MH8}-]%po-m/y̥kUNϒł̪;'2YvKV/I}i> 9޳#ox 5z2zK;Ar̻4ofaHҔ=rZ( p,kA@R:iabY0DɟU:kzIp`ofLBF)yC6щ´f|p~ MlѵaʦEWsCl&O =K6obXe>%;YNl|SCChYyK?U0[lPrη1Y,34,reŊk\UYK0h Ku\bsP=~0#.h=Q@^zQn\o??Q/%+*;|w^?Frzt[R<>y}S#?2P`qBY` 2?<ն";X; 洎Αt4uM} E}7*rx]>̺"!Pŏ/N }n ȴ2Euu.cJ̼q.*t Oۍ%m13浍:̰>-SwF'z{F0|VfN,2o\q`#n/"-|T}AhBB::hc&|]&İň aV/bHU7rZwGu9L9i ŒjoG"y"s?t%{MgK_>&1X[|GX}4*hFNJy)n08:JO-<1:M˶?ٟG 6}=~N~ 8L=W䔭O~W-KoLGoi&x)Z[n!8Y>#;I~nH'ˋ![,чJf3Ī(LLmf.kme\0ETD^Re >Wy*؉Kҥtr[Me]j3ٱ<ϿGC,l̷!4\ څK0@5_\az[`-iBZL^"Dۧ+ Lm~3*`|2RajJI}3qKYLݸr\R=qnǝw?]g3fO?=xv>J^RisKG?w%e\=S5tqо%Rs~" _xw{"nܸ5M,b8x0 ۆVw]TFc}CɁ ?kLt]/(gF.}_pqKw0g^.__fá\3`eW1%MFiOY@!)دc/AvGLD6- s!^+QU1q_5}["q*0rO&ځ2YO\}ƉNf7o̥/~~!._r;'ӟ}^@EJ˗4W.o\I].NUI 1*.'i5hX1kBAX-6,sT;*YuviLJw!؆gϡ5灿tѼn_K}t\o/LkmY;^פUE9q 93ـW%E&`~jf [wkQQ=v(}'rwo_iu072@S Q]k[>.AZ\EZiaL}ןJOxV]K+k 4SƭYb-fhE˄.\r˗}\tsD1bh.IJw:GQ&L׺,و`ee}_b*F~GfP5wpLn4u @75; vЅ8! 6]4aۉC}.\%._K\NGP;1$l.zzE.0q5uǪ"t)tīejS4yVzur@NfBB%9[2ۘNIձ|*v ͤn zVH,E`/&liahZZ [ q hWlI}ウ[jMFs/F+CީCú[4czU"RieW':ӻ2\Y? H*iK;Sl h- U X=ӈuc@F13ig$ƌh ~#'SmcGS=3kE#C j ƥxƴb4xjۢ˗.jQ_EZ˗&˩r1Pq՚aXv%ٳю0͎_LTIiPmxWh.Cq}"=b& 1.'ņSD~>-lʽ/lqwB!P[n.ס5w*C-KL`ׁ OK-2.˙ְtԱ TC0Ѻ88xux%dc"ۖAze:פկ0;2mxjvUںZ!X\Flߘ?A4aѽF{h&] eLJU 3?ÉOs?ѿ8RJ+4K_kX?p%-z!`:>RTr~z ,dEUJhB;3T#I-j4/.@mHj1*81D Y]SƑ_bܳ$TVU4S1ѕKt\MGFXX{t1zŀWآZ•]!dzp.cWMRc@ba Њ`]^iTz@]ye`qZl_^B.ᵲK$7RB='wRU 9 Xs\ |3Ym3b`VhFz̪ P"Ca}e@r7%LK8q,K:),q,KK yZ^CrC+jb]ΙM'tBG"96:ip2,Ġf}c}6Igvvɓt*G\„H9QV'Kr]&8./@pO0X[өZc+j/@1j"Y@`ʹ^غ1G\i3~pkpq4Քq ޼-7(㏛}BS9aة ^pU nuK6Pu%'t2%bRNIO8ahe\za*Î!$p0zNJ&,/踲ҺK YF[F.}}F ?`0CcQft sNnWS+_aYsRQh x勠4DB@UzvhPIxSApKa6xf ` R[p:%}D${,X5;0QlTa[u,˘ZzeQ)w8avtfC V(Pi71:6[鮃b!U1Ȇ7XoK iog"CRZ SsRQb`qcec 6id{L E-{;F<[1br> 1z~ L%}S+ H4_v]4RnHbYdMX\]\6-/ߴ/Q=ZJ+/OĽBl=$ђ5?xb$k YDf6k- L*TRJ+uRԮ+JLFa$c*i־28 ;[seka_`mP6Tts LjYI/躍`:0@QKRRJwSӘ0kZЂ%&^^dOP%bݗ!ψ ڠm0-/d{ =QBTk7p{6|>rRY(=o,|y"-P,Hǹ0 90Swg/9Qmל`̻yXVS<6}0~3j{RikH0օь*9^'4S_4BaiYS(T.U#e=u6u>g|;%:kޢqeʯ. TӯY(JNĬDHxH5J.#fߪH+riW xߙ~Ri !)Sd3brjbbJtU"OY )~cS `Bˋ`QyXV`82 Z9Yfl;2 ]ܬхߏW!?)Viw{Xz脣5_IJ$[B?`aTh[l[D)Y͆*p,Qie9ʴ h  X|{H2ԙ>3.*ǹف ADlq zR#[?__ywڥsJ;_C'\Kec7&1dP>{f˝i~juA#%FTUYR4%Z&4f0vn#Em-9z 甈kΐZMkG9cYB_1EF,3/u"}Kg=.oiv(L My9]eҎ#]. <\TS0U~d+qQ/W_5,O#6Z "?̣ܙqOĊ^gfylK}q[h6b m~y;^ߩǪ?~dJa;GOQ3`.w0h=?셿 >f+;t &}&jߧQx?q g;KY?Tu@OHn$7d۞jd¹ry,LOr$ DžmzKMNX6#YEaAuP۝#UstwGvK.;- Mᱻ0ظ99 tL}j =g< TPMԯ^J);IE:=1OE[߷Wo׈*R4kbY7 ;B@o:Qс3F. OhC`8ࣥ}5H!Fu=rȉ1k"f=@Y-v*s 9s&7Yq.P}us^H7]$H̿}b*tfv[J {I\U+q1[KJ9>Ɩiϓ= 5fZ!/rFzy#sΦΨq3&SJ;:$t*ۘv 7zr=ôͅga;H,)& WfIO*o3sFZߡv=ɤ 8b~w&b|FT6&pv%$|w$Dlz=J/N=2ree˗z\̼ gqT8D74}nDeEX/S3bQ҂KUƁGW_Ev`T@Ns\[;(lW/s 0u+wMTĺs6Tx+AQFܯkOv4WuN|dEnސVqvU9>oy&%O&m(nMtQt0)ƗpV6U}v:#plkuWK.a9wLhyoxQEbAEM{Lw GD$úy/PH&K 4bT/>jV6=&-0CrKRwCF]>lY\cd·L[QC.4n{5͂hV ^&wk|D^Zm>*ӷb,Xtg~X={K[CWԃ D{%e)0擅>$8nY+c6'a*0^fO@'ӛBf+{Y_m~oN,Ws)_a2QT)v֫Ip̹z_f'm5q/[=h٢7jĺwAshlKr<=p|B ŖEvi5jd)ʬ=eO/f=O9V!lҼhm|nnVT mKe<$|̒SHSV*x}c<4UZMֵl M lDW5%)Ho gȄ]F5nR3u̽ʎ٘ ^ ɖ6lhghx8 CxiW}=[f "w*#5#lq8jKԐ8;GX*Ih dUs6hN*QToů>3UfvhzbKǕ:kv^\Ɉ{QP 4 BGQv7s*Q"fy4&͙{ۖQy%d?oJ!w TzЌ~%\l饏Owm`y]:ijkv0gvF{%َ7)eixo*Q?ؗ o=A%@y%do~M3SUi򂱛p,٘N"Shcldٍh*gkxux(ЏlǑgS6nY~ _Tsv>gu׺<"W@߭:Y9UbwfWQ#O?2*P.gp5g^]?o?_-+,Ǽs T\Zi/5p+JC, n-!; ؆DR~f.1' gte9B} Q`Jv}QₘVĤnY'a;I&9%m~Bυ:a} -P:o^_oyzz͗MN~pD]B`]N#y7fl4 9)@z-l`m$3taU؟e;\O5 -4>YcJ 81CIx| xQ{Q i ^ق 1нy 4 3I,b@F܁x23f2+˸~#(8;<{t_l% ?l%X mX.[;^%%N {|2#=ܞH*퀦WE_0mBпgTnݶ?FuP8K-Cc|UDivA L -MZr (vTAId>U)*8:ksRl*b]cVmqpX`({Rx̗92JۈO5ri dD}Rq$Sڕ^ EUeTև,Bk!. m)[ K{&H-]- Qneކ4|7CGĚ9ywzt]_IED(v>LO~@J+SJcHq?0 my'@M ;#FsE|C*y6i6eDYpT\[H25(xҡXXrhyB>-#:AZGWPF.jd$\dx˛aB_]XիaP w22Qb(9 ue5콑{*dկn6 4&ܔ~/mt-3bflCEn5@Yw !{|yA%5RF%EAc5 '%J],&psizc0w]L~+ZjzJ' αl' KN R5qf:L,Sp<֥ƢDĻ_LcVjhiA<} h8"A /,qDDz~X#2߽l6S+/6ȯIU ;#> +(X%zJFtL8.X=W@8EB#~۽v:ھR2'-@%Wr{&h(g@ ݘ"jT8e&vi=1Vf8LXvhF6z%W4"tOߙn3^$c% nJVԽ3O)bQG r%Avb}ⵚ^ _HkOZ4sNVw_h7٬Jf\g~u|2Q E_('5; KFLϰ(;ڧ5{T1%?T7OYLճױw[zi+XS(sB8p6ѳغѪFl0= xu<ͅobЉ WbYRǵ}W4RE{Ǝ1 w&;MMVK=2C0٬RYcDq7+GuTFRs 5BٹPuwYV-rÕrWg) пwae^푸۠%]r;?:0mqNe?T[GujʬRN^cfTEPDSýCv۽޶O̪]"{!qu^%/Qsa/k-\`.wk!fo?K92n_XFN cՏbb <˻W]y%x6ө7vWZi徣v9tkvjoDL ks'ŠfBMe|LI GI@cKuY.w]]M#̪awTJ;!JNöSw4K^\KC_a?-7k5^ٔRa`GH][! JS8]Ԇ.rfA]9u&=Z  BnZNj$+.6}HƩ.hX[z0T^`2n.ӧAiz3YAJU i 7&pOIBA,+xEocW'jNTim%j; Rf!Rlq]'b8Jpc5>^/%JFlG[H4 PMAZU&HkLfV1^7T3 Kkؔx^oĴķEuyXP,0ݡ;W}$630pԔ7yh}pCķr `2lB t3\}d۰SD4l)EfMѻA" F~!gi-kV* Hc?0:@ڣfli%e9miy^NH))4g4ʕVh,H ]`5jsG蹸kki1jjbP1}0 ciG7аoh8 hIu./Gf;2_^*ow*9s$`+"KE'myz : n`X.":_BQ{%2V AJ]r26 i-6w+F35T\ӧ!ŁIV!wrcZJ"3 m|g67sB ̬ YJNc 3R `GѝCM4%rޞҖuʻNf"a5V"EJ{8X 9j3>"Pr]730Gs ZٙƐJePjÁ Gy)tdjj '3Sҽ8WHh""J?iڂTz"$qGJsP>*$Y,!k(JTJկ~m x7Nܹ|_7)Roj+.e'*rc:K^`/_dК#&&W5w p/h4C5mӦGl"; h;+ys|N|46AAS4-a6:"ZSJw%b#&׾NN 5EQCs՞?h笒UB_,ўfRc4n K1OCΐɒ V-F4~c3;B '0dRќIPPߴȥ;v Ʒ0PpyVjFSWM" `ab[+SR6)l@?-ғgpUGiyܶK33e0io5)2ˋCxLpސ{G>yNmya_dmC7Pf)=oh)PWpo;x}-=\fQ`70m0l啼08QS -?_u!QI5AD,'%ݣ̹qt/t#J'7: uF#MЕe[A2.xL{zV2{CG;f~q"߈tf+dVCi3ĆMWJ<l10)z&4DCRʶ! >h*`:PWBklNwR*39[jwlp1l%͟dw/r>T7xeq1L,H(Sn(A4vxvcKM71 G<-w 2JpFݛ0fiCRg,eh Z;Mq e@ Ӡˌ FG> n;Kbi'5qk1a=/e>!=()=h2Eqz{3q'%+UBdo _{KC'u&RGGlfˏ\rJ*toֳkHDOkm] fRyaIH=1?Mq+az£s3'sSq:<1cP}a%`1X[T0eE8iygzw;Ӿ􋵛ϿE|d`lKvтNɘO FDnJODUYhxaG4zk(&V>1xbg5i|bV75*#+wJ q-z=Fj$axXosFjЍB3+ KRx:ylAkE04 vm~xg:z+Ψkn+E usQ;)NvS$cUR^İf*PdP SYD7)$W@&cZ7GGVU0B&,Vΰ q]H۸ Q :;rmcU {KC%]7- KsW0)iqh7'=o` K3ja=x!؈-;$ZnSX.a# :+CC4hY),H)nJ"3PPD_~QLF4?~'_Y/}HXX$"hG ]n`+Жsc-s~P3iNBo♁R-E8J^,V>/i^1|z[lKb%Pi3ڕl{'" Dwbmb:b4+윣=h׏iF0EG*TP.t*TIRJҥtTRJ*TRQSj#: O9(|D{>0L1U,7R]QpEi9'_Zhz} r*-:"d#fˏXAȦ'Aq"ebLSGY6ҕSiyN 4Y Qhp\%F8k2@wd nT\n%%O yUr]莃3\T}76Eeѣh͘Qx )w髨z jw1bU]Nd*aXӰ~2`ݒ:KX''u;N;QgNWw^{OOu/`W@(gr ZZ&eZlcOO8#du%PcSFcCJ UPzI $Ԑʺ៱6T*Җ̵F\'31,ԃS+n9$>:<ˮ6N4ܘ Rv<.`% `XnRm ]a%)HQ}*Va#- OQn Q:]uQ򂷕)qO83O̞\0k8e\pQ"JuN55{-"W!Kt-йb.PC@::!P0*X*  Y")58K \! x7td(ʡZ}X'$SД=@嘽Q[\D*},LG;Cq5fy'wVQϴ3;5&Lu.vSϦV+n=\2P|Xn%*05K.:WC긱}XzWJR*T@CcRJ2*Wi_EJRIPX ߂ߪJRFQ=?Bk*'J-IgN)+W7~d<ܳqĶTڇ/D~ -] |0zx3)oHʰ\)'ರ%J*TRŗA XW*T:ocS־MP*0 $?E._K]_[5*Ux蝝INo KKR QA,=+}.\pUǠ˗._K}/WMJ*T?**TRJRJRJ@C:F mMg<@2\r*Pf0`˃о.\\}._Kլ)PljM6iamDIc2q~"ͥv'kWv7cr9h-9h]`5iq~_Dk17S+}eU)ز!RvrYz!$_;q_SD~sH:IjH6^m{K% 1Z 2<Ǔ,>uţy,xEʊiei* !;AܧE+Gf h{Lr n= &.ъRb9pPl]\Og}x羐yKDp{Ŋ|o 2>34W0HՇ0'EYy9DYR#,e#[wRtB iW-˰4Jt Z?O$쥻}8%y%ygSϠ$^&G:DcX}T} =s әAޢ%u{M:̷ctǙi0747IFGK:˛o--m.o/y"̯yG(^9[uϔUe>c$@V']]0|ˑf{8ۮseGqr2B)3_p;bu}ໞcNXӿL7xF2{_@'zvXlޝ3ÉSX7IqׯL`iђ=Y36=ԯwOz_Ӕ`?5?;Ӽ 9z6?:q~C0~t};?.O3izq>&׮z;RTO?KA6{tsM!Rޙqk7uN'%;}=a;y^IK#c;gz7gϫѓ#VX8Uc.4ߙNz+w x^H-X~BE5A꧘ YSQ(<@()"=S扂ig)b_)G|T8Q9on~҉Bn_0mf8IÈ ܔ_Ia&gNhY,f->)˸g$&8T_,m%`V~*;5H݊H oQF,ƛsP /v&s%,]#Be^yWrxqm=E-! M0"Φٟ~&k <{hleޡi G 4p/ nn/"OW}eWXsVJ7Y#x_"!po*WwIQ="j=)[F}fЋ`sN~˖-̸3Ñ{̀?`$??څV{Gǻ?ܟJw­s9tw{Òp}};y^xxy?ؔ<je1hYy,)l;wMK&齚]6Bq47;~~T]J5T()KAǒmpk0A1N5<é=`ϠAt S= QUҭ;Bn?6kIWRG[|!,kDxˢovc>`꺦e^ <] =F @# N߉E/B.eTp&^ b%x8Qlc29m M Zt+$-N9&kG%P_t_t8hJڕW]$/Hh)R)]!u%t/Mvk_ĨT(c>|2h;Q A1(<<~' uaS^8[h%Tܗ{w-0o 64ixm(K,:oݙRYkT-4 PwCtc9XԶ]^+fK~аQQ $FQj; LLP[ KEPibXAbhVlV*y1wVl\ c1q"";Fzv= 2,O3caEm(#[,YR,iEi6ﳈ|{f `3IENA'Sm~IhY>>ID2I`H 4m4i$!}(/ģot)z )-v@,H XiX!M'!W&DKmE?l=舒I|hK(4i!Xտ}ʘD?kd1&ՃMIrf| d{Ra20h6M{# Rj@S{]섴Tzg[`4H2@aTyT6O}?u"=- 5}@q-ر@``Xf&I4l XIUdkauhJVL&  H i&eM!<}]h-aޞ[-Z;&2k}֓EA$6XIA{[(mrx4{oAH'Z  %R`L4bd]뙓hr[lYK0E.=f-&Tmx mMgQb-0tN[ -b7dYD>th>[Au&IiHކ\k1I\c$I6đ~q@ KQ.7M$$=-Æ"[ykǏ޶K&ͱ{23@u;oE/Z')D~u^߬tTvwx|f%@LX `=0&gGGofrX$ kDR E.}?LѨ-oڃ~Z{nw?̫6^}^[cn0j$`A|m/~~d8S?΅>ʫ/On\& &*� ƕZMl'o09m>@C'ՇovK6e_m`kl7i{ުaO5[@Ū%+'oݯS}J7e( >ĝcȏ)?m\1B&d%@K/m"_8%EĊT[?ۋUb%ku2ztpH&k^M} ,hDJd9 愦ҵ UA\ W@$m\}ka_H̺MSdop&`vx 6Jjdf:r2-mheH \ȯ[}^tnr_ߥ˶![[+5ft`Jᶶ˴ L ~n o|nSKҒ*iHIn/LA+#q$m~oH[h2!^S+mc^PWwBly6Ft8Dȯn[br^siR$U5$7~ f6{~T_{i,J&?}.I |nSB>m~:)(3Ia!t;*PlVɴg87 r)QgKamĪ68952H;ƅA,>yVCN/ #dUڼE$ߒˠ~?yI)0ooHa#k>I2A%˶-FF FmI=Fpb $l_2p;<02}VyxZFoKp:E嬼LIݳpAft#_Asx'ji_=n0a1ΐ$[V3C䇭p  }P~fq>(Csy$[$N-5:jU ;+@$@wX6cBOM*Em6]z }|@m̯A$IKl߆"nӡ~pM 6J%`;M$: h?zd@`~ z} +i&uQۡ^,$xeF8'۩Sx.ʦmvFcR[bp4$WL]?I"`0rIs wUys=:QRcoDωI|nIA $Q eM5#a_R-]qAiMJތor$-{{bò+o[СX(mс[hH$ @8ifT4mAiL\$Y6>Fp{S*li4`H[HbZ v?<뵭FfVs kr6m$@"# M(C,56^߄=Y3hy g pl4hHum6m7[cn'oqi>ζ]VOMBmCXmc Q4fe<[Kaoi4κcd~IhA~L6qď6:Q4ePM`Wa2ġ֦,km$ ]\ՀyH ' ʭ5^xd ?*LT>t[W&0kv$ek{&KBvnx ]9Gt&n8NːA$I^_CNd:x7{,[,dԇ %.;IkH+։dٞ1B߫,On[o8#oIm|eؽen2 PA$@EDo?mhD+IcO MWXUJ Ҥ$O3riP`Kꥍ:hֹ)@gPb}- @yԝ{W M$kf)cIm~ʰ;f>HNd¶$ۃ_P*AY$  6lOhFSmi/^[uK.؊ڠ&x|L(4@ C@&Ij3o̟4w{DКsI_ ! E (C%I8]LjJڙ\AY-q)K@&=IO fI%[fKl,na ĕ mjA|}:m&#|^-E(aa@L~/KY/ % [{eY[ ]jIh[`-=P޿7H6C̒#f'#D@GYk$+ɿ'?}q˿ۿwh)0( FAMoݓ~fLire_l)i׸r@s[$`boM6/ a)*,PtʛX $QrjRbD@,Dfu[}oI6F2H$4Se@"TCǸqKv5Qg4 -`" 2e2B?rLJ߳V;w_͟_]/<e Kl]z21'ov_O +:f1E `vpWMec-}I39d6De;% $iHbS=}ܰ{iϺ~L޾^!%$ eK%֫PI'?rI S(?v/ֹq )n }i[+!Iv[G%JLTJN/<0i`Ri5:*X v^QާKCFykatu۳`UT)oᙍ̅/'tEQ9t*&ƓYTeփ qQނoA0aeoaN.PL>nJאokpɚ8Ȩn,nK:%ɜҺ(}o,תK*=#`Sw4e#󧋪 exAs+FyMw֕:$I=ªE^IDrIt@cyNMYٽ˾hUoiF V8x@LUY]3m[%6~,>O2 7X;n U1ZBs3/#/wd$T+~c~}"ʲ:AZ${^a {,z6}.p8l)=kSNAëz _6x݉m6<ޕd[14bf ďylvl^R!"s5o_Wݵi 7dnv*! 1A0Qa@qP?.)؆/Pui籼< X(n)] #Y2aYbjT_>a X6'y/6!hod^} OC~^%~r^7ˠ.E=7C;.YX'+ɋ/O|Enj^lo!БWǡ*|?雕Bp18>/"wؐ1q5_ ^Ѽ#ܬNq a}忾 Y^2&V E!}~?iZX+ʇ=pb0b.+վ^_-}T\'}[侉 ^83?+9&_lҮsiJ_\]}Yy|y>cGĆHj_{GXLO\aFFQEQEQEqHm%dddBC+иydD?aFgD?V\Q!Iz0X3yYxxBq;aj$|,1ܿZz(}v$9mp@7{Bb?vHlٷpK/1N-zNܧ73 t&5Z7w(l\o~`|t-{:>1ȏ,Us&.a=AwW\gOD~?!?&6RH~qNOBfblÎn1C|(E`(('}Q$D*f>ncOđ;D|:."}"yn b.MB..bŸ.WLBeۍXc| =Ar QE3DhٲNE(DA,̼Qq3f}gύ}1zkWy\Pݍ/x>  $,hmA:*bDD^-8K <>B|y,> |ň<0:.WC.(L>K2 ݊!oWV //& ۺO+ZeLRV9Rd+"{Y)8"OrocjJ +]M:Q-"64j}1i8EXc a7} ~51~߭q}EXIN @yh41x> oJ?z  .nvavڋs<ؔ)Zi o:w~$xӺ7\qgmY1:3əD=~?%EKI\{Nu7]UTe²W_|Sn}%E(o%ಆ-{^| ;"vmtw{RaMx Ԧm]{<,]ֺwۿ[o8&w|0 !2Z ?/JrCE6SJs[+~Q{?3x^LjO^> O*Msٿ*H7$muA)ZE֡ueinцX_gxc#}(lwl]o~ug g$|,h  "b5},\/,Nse1;%gߝW9_H<ۃ.6][gormIsBBQї=!uuňj>b>bE/J_Ez0^,c)Z٭7N[SnGo(OWvkftknvHkEoaCh;C_dZj&mMyr*:۝_ȚVyocI=l)c:j붯_[`ѵ |-Tw`fν81{O/0 nItY"g~oخJPV_ۤ_%RG' +TzTѭ+M]Z5HOO6ȅoO`n{u4R_Ӿ {J5Ө{o>}uRZl_k$Ŷ)i6Qv-"A(~m*~ DU7\8&ߏ-?ot6}Oȷi~(ac/,x4v_'8 ,_71(O-yڼ_)RGo_> )׺= tՊF4BMma1uә{K"5ij_^kg2V'_]o}N_-&_gGmiyMMMM9&OK?)cT:m6_fu>!٪nʒW{R{|oq-i5Wkӵ# SPDƓܗr{x_rlBpe cyz/=B$xP?3i?CJ&__R|%YxGTM>U%+РI&4DBa.\py}8,_c^{:pk"Z$J jbHH%\!B,! *=f JYz!h82Lh&um}/[OCVz,vgIE&Um};b F?g } [I4^v)D>*qFU*W|i{{ލr!ziwkpcCs ; +cSk0$ B<JWSoѿL?Wp"m!85نu#Ռ`40mݵhQuV<0A&~<~XH7b~G_hv׋OشMO?B?~J:͂w‰kYBŎ5a\X}ry[~c٨NtE7ЛN$b8A7|.15 4RUɋ/>ćv51 Ak,Cl-b,:DCkQ} x>PBx lݡ"qQE𑐙]ebaultBLE[v'p‘=<>by\/MpR_!^6"XEoyxG C&&l%ׅ#]thXڳX#t&nJ{C/䛃sԷBm/=;imS:3می<+H!MugmQn-v4TX)v!S178]pc1^}(ro'@U{mKC^w%Yhz39z)I'qM.cwdđONuG mk[oNFT?uU4USs!+[-,>}3VǓ~{èݫm}% +ą:e4e6TLɕ]1p5sK!Zu3I8ܖB5N.>g^};jyUԛ]uEuςT̢$?'Rzbj[RH҉Olڱ˅o $lzO+=(yxcsV|\/1ڟoBt~i?qߧqKO& 70$,ŬdH CPjQ Kcm].{v'Vk,CM[NEKÿ·iTRMi?JګB,[U/ .c;4HK۪* m}Ǎi~w&vxD!1 |,HaKء6d򆚗뗄/A2%j&KHi[gߑ"/xaT4!.kS- kW e}ocF]yAM?%NN{}Ոkmѱ14jJ_Ѡ_wт; F K/ yB >B:>МCzKvHkzKpkqcvZZ)?,|ޛq&>%26PiEèc.^oĠϒg9,BLF6oےvPIۈ{+a <;b_(_;cu>~i7dwy4G3*I2 co<&xxlcy辣<o}O4!vko 7:Ц!:WٱØS!?C2B> /bi Z<2ٷǑ]φPSI _o#n{uؾ]kGQm>$E_+U]{1QU &鏥i9ZJDhƖ;"QQlQv{J^T3zvKg3A>5(dQ!7ǢM~5 sW 1z:k .У#kB<1AtG/!14'm~G?8KŖ>J.t…1A`}YU :~^GHR~I#.ľ?CH/OV &SJDiu+w>qf7iG]NĿOǰٓ zk)S/3N҉%$hKE"dz{m&!=Cx?n$Oth7E6R;##.WЯE L\tQmB]5vt#LkI}bjZQmkB@*i&ըa;]73R5Yozb@лR"@64]/I78ѱz#haL +d臇mR.>&#!B|0H1;I%{.o,(6VT;"Sz~,xb v2:RWcZ\(Lx(v `N'ףG|'/l>>g|'|G|GG"h },.?H )?Vb&RkCi@m -BH$ԇQe,"dd*D{+;Ǯ s!3be!x#\.3Kɔ[z>@tGMzl/pɈhĭNL%lvZQ76:D5 hiybB'ҢY׋<|ދ䨨O|_Y#(bGd+5G6Q!ܯc{pVFjxN #؟a*G\B$!BIf΋aAq\Zն+m_' M/{sx !Q:!{. >čh򆘷Br^a8]_Ee.PƢ6>=v!;)p}Tq~aypy- \9X #)F أOb>!}E_999 >c>c|G#܂#܏s>C>c>C|/x~$j0RXy l6N1hj yXQ,FRbB ;RcPBKx.HܦŦEMe-JRno ''Δ~جlO{jΈ 롛d!"&o ʌ(P -'A[d!u5I5: -Ne8|S1kZ%l7 Y!1.zA, ,,~|C/شQE!DdcCD!2fY1EAᱼ”OЀ!z+,>_F DDDG8.Bi}RLjCxԢ|(A!>hTj02pBxXTe>DkX1[d%xj$KI>IoED@)J5Cw Z&'%.e~J(B no)A ZYC@tj}ei9seF+(6ٻct6([h5tQE^17[=8EC=>D JP9;O4DABhKXADXCkBPIb.ZCLLZ,,v;2CZ1"ǞL'Pֲ44%snH7d&1=bEqpv>2. 2Ų+5"f"" CJ/a2KG꥔H$EQ! !F A$A+a7thQE  0,!AuA!CyǕ<]K} I"B3!9BbЄ!Ef/LXqX<]*Co3K❓4L LLB!B4B!BaBD\#O E\i,:6DQgCE,OLJRh'P$`SOބ4x$@MQ ^AE;Cg|wLHh ]ލ)J^m5\Sȏ=^1.,hoaXR:lVZ((A MCU+ܢFYNƈjx WGQGp7^`Nu\'G:KCб d bBWoL$CCl x =L2,]!b; wT1)\9ig?FⓎ~ 7 2EhM"6"vk1 ܳ]y]-Xy5Gxxthji YB &Aͨ054#m/9-QQa a#b[;)b*5eP謤hg.'buTQ=)D2aVF LzcyC뇗-OS BE0j$=| l$5Y1v- :FA!i(}b`: +1#84A 1 E.X]ptvc'S,ke>+Kyx7^.:Eb#F"0cDLx&uB-!hxVQ̖-<.lhD3P6 <}.8&Np)qJR81JQ87qJ-Lҋxn.\&$N3 )jeEo-m˄Jq{!_CBD>9v[D> J,Ě(!#iє6C  ᩔx,đ " CD!<!B@22(iL/"!L%=2jeFב ($ZrxX\-mieYeQb,Ye /4F,#( EzץE”.(2u} ;ʗ:RDDADBrBG91 19B%/A WԿMf)yx\g0|'4N0's3xCC7e)J\QB)sJ7ƔE\zK&'ʋx_R攥.)JR R@*)qKJR/~)JRb $\> ;)y#!OJ&[c7f͛6W&L3\X 2<//N*Fh*.tT\W;Y'8Ab!GCCb{DXAaB!8BBBB!4$BHA$|}$}#܏r= >4IA8 y>C# s#܂rʽʽϸܫܫܫ"!B!D!!BD! !B!La3 L> '\((,Ut¥+e^),|g>I$I'v|wO{Gqq}Ye\R䥥lΊsV37иl(ؘؘ}\R:7LBߡfl6JBb(U^T(>‹,Yho MF LFН:fSbtl8h {a跢h>6'Ey 7AvSAb)1)'B!8uB ""p'GBh,F/䄅^(Ѫ'SoUB7a1)6-,ŢoXK>$Co C\ /B 3 pxqx>mQ71-r5K;%$(dhLjBkhOlKu bbа߄%SlZط, /$$tXY.,Cn.;6Qd[w]wؐНO4$}ga: ?8q {Nλ(|GBh=ͽZ-4Iq[X/Rpe).:SB&faYKDE,d,y4cn .}Tg4YtДΎB: 2ב:Мpc7>Kߠ_7.k?*! 10AaQ@qP?X3J5 ?d )a IF!&;o'Bx\ =Ⓜ=6Ə\ ͡7 .WC_~T>FϼaE=xqE3/yRx]ً-D5mfBR)RBgN]BfaG7!<X01y<\/NYZJwyd1+qrx\2Ok~.T>+:|!i) Ɖas|!g,#̮(ЏbYz\,Yx=򜐎x,Ev %bĆ#qY/B䎊\\1|,K/q׊yL|=pr䰘,?.?'|W_\Bap^(.0 Jo l>W! e//efyvoyRo5ej%=G'>Gy4F2(]ѕ^ЏA(AQ E?a垼|0Ҕ-?baJRJ͊liكA!Xo&Y DGX987v{7x~GЈ6_o ^Kący+áy7tz6_:?7ХnVA[Sxbn&_PnbA%ء}=׋^( 0E?\D86B f~cy{/;.i&hBp0 Re]~:{?lΆcVGAn6v &b_H'ćG(+Q:!8VVv,Av.Μ}0:)H{׋T~gOL_L\SWA iu#_Y~?F^؟袞V\L!r]e (,ltp\,+cb,~{9p}<Hxez)ZÖ8&Q#ZqQ V=4O{hTLLjlFZI7T\} +j<)($-L}v1'2A$}x#v1$=B84KP}4&JzPMz1]ɾZ =f;VDZ )KpA x\;V͋7 XxBBVhXQ;˒QOP0Wa7𵎹^C_6؛:PCԖؙ"1FQV6t:WEUZ'-`(i4c! (Y]|%7+/ J$n;bL571 xk vLB!".ī?0a Rb&|; <`.,Q2r!9,abYGcRE㣷6B[/oCēhGuQ&91M'pB;#\/rH7;cbh"μ/ͬ |18*A& `Z.Ueg('qBdz(YBmBaq\} :qs_r1&͆>xc_H6hl/BNVB hH!}J%ڭЩ!70N~tƢ*RX/ cMM:͆߂: Ccm#- >7Tm] 6}PG]|.+ie +,$!0.)!L&1>ȸ^ݔBf͏ЖA YfBF=-BCw!'_h^ބ#_Q! Bs:,{<.YcӃDODlEQF/}_!q@K3gcBq"Ӷ; "#lBq4!12UG %BME=M2Zl0O Y಺:.Wʔ~ةF5'$No|0![hܩ?%o64X{.(=B 6p^#J@Z6cK>apjiKňZ$_ą妨?d-:$k yB|O/BIzٳ!za 'ȔMV\ ДĒ1f0]!-fe^Ġ},TG7ލav]jJ/Dl9?K32砲#ܣepC蔯cDa9~2tu3;٭S4:I"f+Tv&ټ,.xB ,lh5d2i2 ⅍]Aaa`)‹"/_4NķMm2m !J:?KmwwM"V&e'b|)Q>zlXc譤%[)ؑ3D &%*ZBJEB#gA6f^.1faqB7XEƥѤewq?F%?D%7fĮ!5 hP]:^>Jlxc;rMpO.,1DjFEٖ- zjIAƒU*ĝ 5U6LZ#,Fg51pxB~ BN|NgQṘMbEYYgj`~p,JNG[x]g= EzshN"ބiAtuDo )K'\DY./% _HYKtSo516tސvoQIWjPԷDRwQ\Hg|SOc2&FэT= $7t%z'A&!S\یB{ vYeX; a5:vΉ{Dc&l =7üa!t<14-1fV{2;eW.bwFJۧugD%9リqhĦv%I:Kpn %شR#W\+_5LB Ee1V&Qt͇/1/cHGcCޘ?!4z#DEa ݷA!61 E]67 6hIg"آn;oKEKeI;lN3!,oAq!5R,Q2e(t'EX&&,O؇A?cnF}kyJtױ*!`pC+:e:eC]_X(1 B =! >-輖yQf)QJ(A !4Tlm+~! :eoOH(e /OgsCgS}O/S6JhK]#'>hcXFNTRM<.୻h]R<& . ”R4A(fеt,&Ѕ53)sKQގ7\RX}I4}!贏oc8R! YXآҲ#T. \^^/dB7*/c4ӂn ĐƊJGhR(y,$VI' ΋ U}fa\!Ao4zɦػl!QJ4+} AziOK*( 17{DM|{!FK^[ -wCt7D>9d=bLTаGZ0-} 킀RLShb:hm]ģv͐𰟁r|zLgȿW_|>E|>'H{1 ĘނS+C7 S]# 7Ʋoþ)lu1(91,sd RHކa 1C" ~\!Bx.irqKuxL6]1a!t/ȷf፧8N ThѢ-3d#tPYjAC\PKf_ , /G+Q.{g DDJR[Jea4q VJMe\'O1a*p(R7d=14ևCo7s2\FQBfY$"knR"$YD!6LM_+TLCX踼V! exز K(>&Ŕx*{Q~0K ccAAA4ͅY+”9帢tYqk+)s3yiN0HC e~>R.)JR)JR)J\R;n.O&˚QfaB<2c}OCds&{>L?ЃGPJR)K1_'LT_%/sJ\.('S:( ุjEЏҢ)JXR)Dג┥)HA PctH_?}ҿ}>J}K^pe&9Q. %kČ&!)\axx!>b?FޙG~ceؾ6^ƢYȢȯ|E~C%1Gɔ^Pd;&nf^Y<Dt\Rp~B99xRP0jXmz= S5hi,Es=Q0ƣ,!6C%#=H4?ρf 2I$I)D{?q;Ŗɉ v4& q(If3bXhB, \4PR{\2)p 3FTRL.0L2d!iD1*::g}zWB.$85Hv("#1\&1c% ΎD9JU]e5@Dc.awg lz4EeE4ɡqw;= F'jai_~W_SDxٲNS5.IR*H S]gؖL$J&Qt鞂5H5u(`K,L%%!%$WدB?%v[?C,#TN&b"5# ?+mD^7%xkLAzbe<"xYۯtz" OE(e1 Sغv=h7q} D'hDBdE%XA"cSbAByhLg\΅lŬ쌰k4,š pZu5 vvm:γ`ˆMFى*-plE+ij؋~ pb+|{.X+cw%R,xcE;}j͈Cza2xcD5ĆH>(c} m4hEɌt4btBY XxX1p K)M.ǡ.EQ)JR*)V**ᬱlZ)NL:|wcyxM5ɫ'sM ,mL]#O1 p|(ĩr3pXy/ ^л&I]JRi(ثW`XxbxXzK\$1b'5eТ14]T\#** HTUTT7^=aQQQ EXO5xg1qO\ez SZJ] f̀t6RHmeC?aObE|X+Š?OaOG3O@( xA^Uq5A$A8 I1I j~#QB XlY9߆&:<1?/߰,C&g_üoC}~PL!a)QQϼ׃^y_/9YXl\K4]g 8B ^B'(BD#!8FB EI:AbpDBNO/ d##!0QEYe$AD2hBd!LBcd!lOD6X!0NsKKk!<YDB5GkЍ{5B]K &'B*"CΊ_;)qpTS.(dd#de0b#9BGGxJSE(+)Kl/rVhVQe(QYU)pX.xxl7, J/YEW³kE3܌&r+ q{R(f>b3dlpRKR\VR.xҗBbb"!A "  _.1M0_ff x*! D$ y! 0YEQ*Yz>GȢ(7EQ~^+ƆGo¿Bh?" Ѣ#XTǶjB' &SNyFLB&H*Hi͒ERFxؘ_͡" >b>86y*K@Jk-c,̬LB &ireuhJw;lV/qLиS/LIe)5.iJR)JR◅+)JRaJ\2Kɔl. - M4B3Й!pdP[> ?x^ƚBcPOc'Є %DR"alK-A)sr\?./2K|ShhNj!p{{ -CpM>Ɛ0?Mj#BFDZƶ-!h K  48%IhxE/zJ\CYe)i r)#6z,;Obɱ"Ʒ-l{Eke ~RcH :C؜;H7p(L_*cSd'\qb,5t> =HxexDu :vcC Jj' 0>l\x`lXg'!1AQaq ? W2ꨯoPQeinؔɮe]\`|(p޾U[ԬfUOaƂd!WAܦJZ@KLո6bW<Ԫ^;b~MKdV3 ޿1[5Pp2丘|u3}=Cur+ /w{mn_ύCHbXٓ omZ|;xDJ('*b+k8#Xa˸~; G$ϩ oQCs <(}Ƌac%>|™Z\Īܬk>lhWxl,qJmvcqtv] KsLxp1HKnM8Q]԰g͵u,JJwGsXil7Kɫ5 _^5VVh4!Y xqK.;>Si'u3޻nRޥeW9-&/ COPP>1䘑]-\aAi/'FnZ gks/e&3/5YL]wz:oQFg nt(;G|ƽFfTW\W.RfdbPW_Sopd*33#v+)<?PAqQT?SI_Y]LoL׉N}w+S0?5 sIҥ"VC)@lQ`: aЬVd53~7 9pF ii&7==jڂP^+r+Hqt00'Fn_0 Fؠ^[U?"\ns%*&XM±BFLUXiĻV%L37XM4o*Mj'e*W4/nkv{&B7Ԫ_ 711mvU_]Ol]u 3>;S-s7߉}abYFeV@e_Q1&{_ՌN{P1W̫-5ze5q;pE!>@;sXET08-̺TkfRVb<љczw; c1 ;fʽOO:Sc5cWܪwU0KnCuZ]b\ 瘮gpBV-%#ZzaSD`7%a쪯SycqLHE{AzF̢V737+ﹹ~jZLL^bpD.sY ˜QPb2{M*fݰkk:sTe,3˛sAB*:}.J7%Y_]TyĻ"Nj]Fs83hBT3N ѳ/-*8spkĪX>Cns]8 tLrR@׎c3c[Jm,rU*1s"Wo,P9ޒ o8 UfhZ0Q#Em$-UܽC6TH.K(FsXZo 2:-|&3D7P.:`֓^~'7 RN wUBg%p@o y#--<\i11>%D5皛0Yǹ̾:w5{G?Q S#5O,H\YeK_ıV2: RƸ"ոL.OeGNBA8\[ ˰YַA/ _#ʉ({(@gnn:?-JQyEŊW2 TYE PUܮwlexs䖻̾3`8 c9L\^ MLKMV 򞙴aLVS̝ÜYV0c`_#qtrR苅3[@ ȮVӥl] K,Wej6oW\Aq]PE7 ݮVjq }3ms08{1»?'k nz3hJ_~5-fM`1Ay ˷ J KCͱ8rLJGq(w/W2_3[0BDd1{^S^ & kWQqΧP/%CX~kQ-NsQP]%N Xi1*GX+b r]s/|awh+㩕xKg0שrcxSej_JQ!:TV/o _QC#Wz5pvOr*՜E0=+Wlj cM ZRcYa1h%\2K\GMX]>ǷVj[j/pp*r5wEn%5,RU̷?0p:ʌµ Zn (|K&p8yC!SfyefUV32+57hscR:/|G"5~fS.o_5B}Ŵ*>`7Y 95vjJo^;͌Ʋ3.ܼ Ӌ`@JIerÿ$Jz1odWWw۹؜UF%U FTՃS;P7P0P"أʲ_*9sg{F;dA=JBfD 6?q.A@Nx8N3{HK El6X. 9߉vjkمCl*wWԺʼLAb~#*#6`q1P>Rˎb$q NO #qra2Z`sfcjfç-qx-?rEruJ)kt@_އS|' =S}̖^(ae9fajevkwM6יczgAn@)!5])(P^9U.F`ܪuӹ>n6yPa/fGv5pȜM 1j%Dͮ{t˽"n(X\Gxy%u*؊G5foD*?.quQbC26ԭ.<(=яQ 8Ĺ) KLj;q-w4:%7f2 jn{o3#zoF+!?Wi[Ea?RDTuRCĪ?_D6CY&ŋy_ܽ5?/utV75 \1j%vC}Ӓ\ъ7ܪum{~.h!a,Aze-[s$9aơ{Ե5CPjɊT c#nm?'Yre͘,o+ke13(:|b5qfR8zsY|OU]߈`- ӯRɪx >g7^%5g**HzPe9ܼqaf8qac^bb-L2>뀪`rkyz 0Lj.*UĀ}J.e)#6`YYrA2s L/a^bВ+ Q*Z>?9?l?0vd :jUܧ?>éSK^%-0fx{K e5,}w)g\KY98̫KKz!˩y) 8ex  =FU{ܼ* sAȟP}q-~:xDKsP\ݥuXƛf,9ķʣܳt\WX%-/SF/>BUZ*Um+&gz` h8e垙x Z&UΙx?a85,˧xi4cIgT#b>Ux%4!?/?Š+%ˈ$GrkCQlbS iv-^]nzöٟAZr\-JЖZ69iZ%@͓#8 לGb{+Qh;Al2HoŦqδkX3œ3(b8&yy@*U9Gn<14WdۓS @Ŏr>PbWoMUn.%0eKMsuwcfc5Y  @\>AxxҠA&@;rퟸ47"7lKY҂-e<MP /-y\|BGU- yܳgU^+[h"O@)Aޖ"%r{wQ" Q9 LwR O㨗*1Ʀ'K]^u巨c7 ѿ0i0Ke/P%p\u=wkp[wNqo|U5-xFꖭ,{(]:Vq2S@uB"onkQW0uy4ly%MG&l^/Ole,N0| ާ8H_ re9%uR7Uwi[5|<ļuqLwYZnivY?).Pu Wl_"’}s2ïr^6_'e9 1R˚b *򛃽]^Vw6%e5p.dōw,-j턆FDN`<X uH..E'5ء1#R JSe c9oLid(!E֎fT̎?.WrrwPҖXjT Y+JQ(j,)9"׾--c`1C `,X1zf֢0("He*SoA, Zj%o{1_%nVG~QreUkԡ<=E=1p%mHqpXyω a;EfUCP,Pӳ/)9DZ,ieIΥ%U ݴajVW;yu P]JEAG?)Yŀ)솗5L?2+~uODPf]59q18:f ~+N+`;ụtyy-~:Ϙ*:Ou ĻjKxsU)ٰG=['˴UD(p{QKWPcy=JXuZTnkz`AR~^ &11=eK{Ldl/jWz#PY2#]ѻVc/칋.9x!pἩzU&5bVezg~U4ȇT5hpF+y,Q+.PjH6iwR^%{ H3pN% AәQvw0 ϸDbr׏pdW{x} ,1ڀXrkG%\Sƅ_Q LURngM qyˆExHU+ _1OmBRPvP,>xpT/;7X1<SJo/DSX|R$=7.CSEs8csGQĭKy< |f]d/R>*s2?:wȴRr׹/plPĽe e0@\;ؠ!âaAUip4mLx=an]Ej3ԫ85Mo'K"R-%$ (}t#^>Y?;_*WVJp^"6t XaݳȀQ,%ZE¡|?iuW.UvmĔW1A'ҳg^`^j]X}Yxqk_`'ͯg8]~gʠNrf<@}ֿ OO=HD?eԭ/rӯ(;牣P2^].,6+}©ĹԶ{`@vX%Yx &{|^%G*LEӚT3!Rg4=FpRlf|P۹Jb !mJi6TA%-rZ2 WG"qE{AŚ㩧GsFK:e?̧RrЬSϹBg+ C(c8)N\KwNq .b&_ר`p(hSr_xwn V OX*3!qf~e x ^5kw]Ҡ;԰c'[eXwJv. bb6^-ڹy^ ՠFc_0")q lgw1UyfK<3fjaqhյHUu܍~X] ̰-yLᖃPE9Z e 2@,x# X.NB& 0rx^395 g_r̽Ĥ2۔w(`h t+Z@{&/6<о-ٜ /%Lլg< @c9 skzQ6W= ]i rmx-=WtineYKx1nuqYY)/$6 kXU cR16 *d[M堆KE V{įa^5 B:|Դ.'^D[mbU֙aϙ\X]Q{wpK22YGug: #ɀkQEm` ,b.X,@)~˲rpYkleWljW6¾|VWP5k%`B6[lq# _`p>ª-fKřA{pYI̥ki)Sq?jRn?y?I#Ăᛈѥ-lt6]Xa)}s~ f*F ʿq A69{܍NBb܀ /̌ {~EHW':LBx RiDp8WPc DR+Զ][yb#_ZոGKz-l3x >J-Jב.X}Ji9*0Uwr2U"u#U-xsGĪl7K׸y肕 ;QZ8#R`]K=KϩPUx^#sHGY?%b''ĻroDߊx+.ωq]b˔1ٗz/.̪2iij^l?1obQPU/qT hcͰr4`^-q(b:(٧.WKɪ]( M5nUV/Y*kpW:z3ᗹYK~&,B5sF4B&r"+M ov@m@Fzz2FVt4Z@lVH2=u KWXx:OBHD*%eL(K#L6q';"T]+[*k72ʰ[)ZFn*-U"b󂪟S' VTw p":83ra`+eodW}<Y`v4VZΞe+/h u rvqdR7C*\*[.T>XAORYO3SJZ+3JINt֬_Po 5ڬ,Ὁ'%bv" jBuu8)DQT3Y's}!{W U_0(`?Έw. ZlA oDZlMczOC:3A<#B$ ]̄A*6tX/BP U츴XA-F-N)Zz͋PpJ3r(,,ZEuMl勏B$q2-DGl;G\Q$VO'ІŠaA&nz._oÙSRĩX[ rb 1Faz3_iYL@` <Zk72o !ciEjuuø5|\RSC;s/.(^xϨX׊-?ppHprp(UL9N"l[qlU\/&cg?u<Fka%'*΢֮658fu3,5 VkD BXrˮR. ` jXn $. Y9S(sGbjm*!-.a.lTm TJ+\H%9\P[rg>3B4Sgʈqgi||\/u?f99̻uUd}#,KU@y&w.\ TO{Iha(ll%8޹H!!C NA9`[Ba5}J(,b8'RHz)4(&[JřsN TN]p99hTT1hxn*d MRVh|0 BLs0\9ib*Gz n04`72y'73i~b kyMpaaz4 JGCff>i2Tnsø6P{Yh42rmZn"Nu3*v~eiaBʆ\ `ӴASBcinYY2mrc RS" nmk>[k1 (Ե/axGE冦q7vKl<#6-d: ^wKPVeG Aߘ^`H+CxTbZacEztCNN吵 b͋ս&D(R,õ^csl  %䪴"A|\K|K<-DE@(*Hn;?f+\[st|&GY~jUY" J΍Mpa?}iHҦA5J&m -%3<Y*f!9j·)WaFa6YvF-3׈fmLVfE&5 Z,k,lZqKKk<ѻbS7a779هQsuuP4KVtf(5`Uz0ӟ2t.\o*~`* `+0\CAw'B)tÂ~'^gMMʏ"M#9P5²Xl%`/疏ieԢrurG*Étg+U/yV);J){SV^/EIy%h}+hgliαs.@.q]+&Q3{>%lrYLCxJ4kƉ}*EDVB,YĈZ_ɘ!xjSU>)Y$B(zqFjhՐ!YȀ m [$^EQh ݒ$BcoXR@b`A^j]5iub.YM剈Xpb*c&^?*RҪR7نh!ߩX2a'Q^^m^F ',h0ؘ 0%kp?N [\IS%q?SPz˫Yw(VCE#4(䚘ҷY/7o ]pzy=ƃ93|!HgY,93e4dPUyx#H-nXݸF+M_ܺ Njs]TJ4$5q.ͭQQlμLO*7k]b~`䲫A&G/s ~%eE>UlfcGLpR=J'XR1̥˜/cى4`n/<鐟'U3.&sjc;xK45~hW2f#U SA8C|N+#S6L3$,VT7KyqI!6"E/q $ ;B3Yu]NbugR gc;p_dTx=OS&WJ}[MDŏᘄv=9%aJfjdxy|̇?QyH> 0oR~AIYp7XLhB`O>-Vzn\vɼS Grs{MJ Y-+Tz^`9';w2 BOt WM#Gm@tV#V-`xQ |&Ee˻f~qRR]D啙S98>[NewqkW0e֗ 0nK,/j nQ\LaQNkun#U̫by 5ҽJ&GSY3>@J%RJ8P 'MBV11 d`()[ ^b/Lja9b B0S|!BϹfz\9?sP'Xdc-Uj5]?/J[_ƚfۙ2fQ dT1ʜ]8)\|ܺ8^cfsF~ܯ/8WgY*ro S]qUP+nrZF +%Сj]rA1Vm374 YЖXYB)TYnm5/gk[7UXA+vȕA`#t,y|vpr[auL f[G'Jp-4Jk »W20X{ bicum[qJ4eQh7T-e٩g6I ۘ`Գm hs*]"?(. B2&X-hL)b(*NʔouUiX9"P+,VÜN^U ۘLm@s6-c? _è:k\z[4@P1`gJf]M@āD]X?2vI#xqq|WPO(-SQo.;n2+[]J2mĴ5;#-75L^ʭĽmS67W#"AEXGjeQ /*=WPb:]]FV JgsƄ DkڑғcAS)GC-[#e`Ucr8]"&+ Joz+Bi![ `i`ceA'JXc B m+w1$*x'_BMV`ljX|{-R 6brLW!J%'ɉJ@JW:dk~ I_B{mCD@, \4LKX.52nsd!p.V`Nf>39pl Fs ~DmՇ\ ?ێ"%?E xꥰRh|E-hp o|E 89u/{Q1e3 o(&I4_KS6kS| l@^U)cn l2S0/J?Gj~#*f?VW8~p`Uqe%ff5e-mz| f".oW-Z)6*RTs4K@[34K5 c0#y+^@lFJ6~DyX2i\aژkf[&v2d"nW@^Zya;6ZL[R4:ăPNCÌ>L e;f8U)%WFW'ԯ5h.V"$e0E0]ge-9Ӄ]*oK)iʬ<][uVP(cF؆m9\ :fk2se Գ1Z TjbR׼lpx2s{ GFՏC)!Vr ]>CG\(:`+8]/p@MenkJCa|q-1ruM0 UFܘ0[+8izvN|Owc;l.%]?Ep8e0˙vY`p.B-BmXwa{LQ %sOB:,ABN]YGyV0{rkmXV 7s@B4╕jst23pa2C@5,L(yʢ墨RH,i{e`SA]*;Ot l2CD!m3 ?CW DŽYk3%aOV<7N[\vror(HٲlhWJpjYHuXUkY`Յ gs5*Fd:Q\j2`+2)d닅OVYE¢G8%(m7P1.>(8rYy`#7Q'ḷQ/+subxOǩ2?K{.& J-|u %x\g}EN1r8a~eQ5Iumj:A3Ը!v]P fhUJбEA+Aܠ3mkj+cTXnX6fAvєfZh+0HЗΰA!>CvjŖ @ָܹ1ƴg&;im ۸"WˉƵQo~AiVl7 Z_ݱBUঠUP̬7[$_=D70V_8e V樻}Z.<`t!&! Pۂ]+8B4r1z'hF n Bs Pًb(3WKEVwdP*7Xn+s}(TxG쭊R+BPm@]̀5TeԂ.~Xr{ <jAeb:-s6Qq e#X}K&S) ;dxcT =zٜ>`rKĶ,F.^av+ xf RTiW?u,k-"q QW{/b;E^Pi oK˜xXqV|_dMlY`W7S+ k8rVִ\pyblJTzq)T\su/ K0K,*AtT09-PY~i0`IM L(׹?q@VSE ~V9._Io{ɑ8wv970I%qRޜ*]IXC@wkT@p lJ*Ԯ&a }K"9:ئhUi~5#FVzTgGs -D,+rmWjb|V?Tw7pq:{kXz\M CQb x,0 VיF\Ik. l  :V:SQy` jP [0: , Uz+U. RC["ͱRl4ę( (<9BPL10ګ(,%+;X4:#}\@!Xl˛-=yaXj⋈euuew7@bT?p}W6n3#cr]~Wc]# 4>g溗HUٔb}Ĵ@iI^ 9U* s{xn6lv+"B)[ SEk䔖T(Ʀa6'Or(nCXT ~B k6ffNYܡ'0jٯWD?Sיԯ:yG~%{a rU PP 8|gf%aQC|FJ+Z +\EBQj'{9x# z!EbwqAZm=q\ڃX-b/~F%svK@.>Ǜٌ ԛXT Z2@ Ti ^[@2cF) @`/,lR@  5RW4Mlp&{?X7DT1-ЭoLAedW xiL<_wӋ{A3Ut*rīӌSV.)TAôteG6GVs_1QsE/`%Et`x9k$jP @2[3 - q\]dyc3+Tį⿎Qx ﺆ^۩f+.U=tˡA˽D} ]Y]^CUo)iW'; VWz7ܜRxS~7(%j$ R GKM\5Q[WBP;h91C--Mqq!ɿe(Y,Ap/o_~Kc2ŌlXӍ 3Îa,XgvQtjcB(^ ( fG%U=N D P sW-O+@r<T5SJ(X )̳g9ai>(r5ZB& %&[lP^eӟw0hTQL^I1񹀼Ŗ;Y'7剪So0G w54ȴQBc1G$/S2 E S Bߙ@AV^&9K5 b%m7\۸u:Ҧ(LDz??DP{ *hpv f^&! .bYM|UͲ?j?)QϙwJǾsq=OMHb93c,iRX8`۪d0I oo Wlֹ?G$f u[S"hK-(9kjkW6͜P@JhNeŪRw#@PJ1OLy-{~be{@S"Z_*̰|8t b؋՟-c0&jg۩ӸkV|G3O`)>֠yX戂G4A2E.à,[7uXM /rȅˆ^qq%#vb;ckP`WrJgZ~P=,B_^OW Jղ*wq L))u(UlmAZ0۾@qtjX̃W1nr2c 9%T/Ur0e *ʁBpGAi/V5#XϦo[XʾIA.v%/숤HR -2tLHhSG."/˽Կ?Q,uR2׀Գ;WqȦ`#YW>H#U-ObsS\..Q_N9qnCo> v/Qwje` CT? v*p킊6+.'^O(]Wh;n8* +R. @(oܠ|3e_> \0v_'q nZu[ElgL5|o~QK@Ym SxDv[blPs, !8; S.ɸR券m* rXdϒ6BRlM-o6i(uzKE"w`*lJڇ,OQ; eCjx, kZUԴ@ @BG0ʼn֣+}|bVyXG)nRٵ]`;&Efbn S_1IQ²0aUe|SWPK%z(1c(hb)BֿU%őb\yzuj鈃գɣW̨ J%{%i.erWf/j0KPX/*ɓtiO/膃$Q+`{2JE3_5\3<'msdtYq ;"aMm.> 4enXZ:]F@_]X טJh[FdfmaQa=L{ qkPq Hq^ċz [D?&Mq˿)eK09Kn7$aSַW%w|0U/0ϿfX9 ER7 q3׃?x-5 8Kgp!vQv+cvq\ 4T]T/RXQST'Ji^ `݄΁:V](i؍-\l)Ű]łUAZ-΅B+(˃NNeUw8ZV"k݈DQm2#W04hThWS, 'W赅`3n-Wq~3duY)Tr0͙ҷ3Ht!< U:G =4]a.0bMijsŊAɻU<S2x~řj vR,-,*6ISW_Re]xԶ(URK{u[+0e ejja">Ibu"12c jj5#{\|a%2u+̺ e߻rȊ6㚚0>䍇2JYwT0A7YK5슣FK;Ǚbg6ծ\>~ 0|X^0yvZKQh&_CC-[ķh^ a7QB9cªZT`Nh {=[٢S ) ad* [jvS4lSS\y׌j%Wx-YԼ)~)H.9"5A>)IvqVkW:4^?0Qk ?B)/Z P );+6̸qFcD !QF>0 n-j,ZkF#+u&[+YfHpPnt"Map-kEv NcJT5Ga Xݰ.{Yt\Ȅ/jY3cMo _cKyJW"U;d?0 eUs@w*b ^{8'ݜ1,Y]𺊢ΦGw_UX8?i%H nAzAFqB.E0֖ z8@aY2-^ o> иU+"2sAnϲ,,xm_LlvVx1HxJT(@ >h|'Ү@.XpꅱF4o5T/\i#C̉> 48RCf!d{"uEiӸ 4r~w+Vp~7۳"&kwXi^ګ3$΋@1voпs~Bml]uLDw,k*˕q* `k/xqX]wf;qƑzh&/N'3<`V A+(d̙~qj\J0S? h6sQ؅U/'{95̲\UlnU_ҁfyu,5UA| 8qYa#4rƮffio;nY1`XWf;s>gd슩Dz/( 4a0L > b O9Fo/def TiWpv U@ O^d0+eXX4m[oe/.TI4rAs^v# 6Fbt‡@͊χ P+S먴"Np XSХ9b̿٤-=;6 7'}6qufTX{Y1[_ģF1.~Qo\?"6SӖ[{[] T+hgËpƒ] q;4|w( l*9b0k{Xq3ms~YVJ&-20*a _Qֿ36_ptOEm{}fj}M k2QaU~bGض`|N,*Яpˌm̦İ,oԻA%H1 .6>_Vh?$ʂ]ռS*:U@3K51 T|!\BSEqP!eVk~Jlw %t:ֵ|h.i_qY2|Ŵ /M(-M8̻1En WNhq_-h"> ,e!zR , ??e?pTV j:r :qcPr h,mYaps<i|iGu*Q~⭺^(٪ (ͷRMUT/bt=nX5$4p[odٲz/y-b:FC*/XcP?!> 6ʔ@@b-7mJ_"c\OCPS=[<'.jM]/gGǺb r W-t{NgQ2rF-[_q.1Y坕U{^2a6hf1+A1Z  au^x[-  K.H*K8\K2,2MJ4*MD%E(3R_Q6TİL^l=k*V^~,Oh(-8?UܬO2  CpxıX%v[WZ/ PQ[0 T C0hEfߎ&RωE4i΁ڬ%Ak/XOjno/q2FE5Ba0G7G cRPk?9jZڔ1gr("1y!^vBczU@+|nM:6ՒXMJ{hSTGɩYS[[oLaK :*iq7;F > 2]zZZ9+21{!%z5k* U06sf 4kßmFעKls榺/(q(㚽V?^K=٘*ֵiqK5-/wC7i,hBi=|?[A8):us0-&psbϮeV| dR%mZ c]H}2؊%9^Z(]eˡ㌈oOeYM̫ DJ5VQH] ?Gsm)@=uo2D$y1iPܧk8r~a.VPciWC v ľX -á 73dj_=BB49t]B݉]*-Ӑ pJ6Fð:b^UTcc)V*X84KUbAk~8_%*=KB\ftca`UB1/ %8 vc(=X^%8%[҇ ̋JԦCY#RLmT2r|]j׈bbA&d1uٖg`x?="CI_yaX+5:4_+ ݵ,?(*]VpGQ|%_R^q,?S# (e$m^fQ?SM̕o=e~{[݋~G۱yYM/>Ĺ2V5g5׸ŘQNvZR{(#m`,^^X).!wB6bRPZ?ψ(s/QH+Yh#p(;x? `Js}@M,:NJ֬q Q&Kp|KOQ[Z8S#q|ƾ*31._E (y7~n_j_ZNeyy!- .@ IeaFZܶtE0ڷp]ӯ+X٘6Wh7+k49q-x1=[^3;[J'S@ 6=S5Je0WowU^nrEgd #@[kE1  ̬YbmjWjP= ķ|#ҵf}^ŕ`=;U2zD| L{e8~!33ʭb%qˌB=AaGNq-]ܢ?. d??x^g̜yhUmJẐY%Azd=Ҿ!ؽ@[gF845֕sj]d ԦX!#a}E/рKp*gO_r叽+Nz; r6^ ʳv |&~hS])}-!d̖-H#5fKUYȺ ?5e֣~ߘh ̑͠+?|4!No*ԫm&mlLjqn UZ*e NP1U4c|h4X]ļ-(XZ#X V auLZ3<N` 'c&!.ZW6z<3K]a[YUC!B֡[4A+*?P1֓!iT)f`x%3R/>Xc Ԭ]׼ĿUrye^&Nਭg80-wpkn8L9]11=%E+|~ ˗nYz`ZZPLhQD5Wm2Y|1$M`SQD%'5,ǏPMVZ2fn [(T;te ›S*LpxtR$0JkςV;?$\T e²Jspw̍B}TSlub|?"WGcm&K@9# Yx@3~q6f{Qݨn^e@)T~=?G"?xXV{0MJhq˱kaM]''zKvLymotPt hL-h(ZޭX   xh>8J8 өS?rœ*aV\e|EM=L ߉w_6l<jDm]9Zj1K>9A%g";gCpLLӓ^"ܡ1_MV`͘0r@Y߉`1iJ;$Z!)mC8_ Jz4LD6 qiC}p WX*_ULR\ H}9oL]ԁF3rOVk=%+] ϒj銳j3![ڧQ~,r~XO=, *.nA{ ޕ?M tqs,GR`N~ۅwfHƟ}9*|*3qBbٯE*ּ)S5y4ۭU0k[]4U6*"2bk4ذј 6 )Vۛ,~\ pʭZ ՕiƮʃéZj,`\uK`h ۶;45 t+44D'W XA <6Q2$ĩpcrCd'j?7l0̭Ν{ߨ֫0uC"qn]mLƺ /ĺ qhBg*%F1H.[ъ|^ys-2(p'8.W8(scF(6%5ҞC+z.07럸GV^`y͚`J7w qNFJQHq]p;emEīq-ഖd- LYRR16rnCs@˱$ܼT~4?'G]?vJIm {lqˆGI)U|^S XJ!mIsNulge+DXj+B@Ù\>p:_{^ QU׸P( %  TrՄ;1(jD֔<߸~>)ށ6Z ]# r%Z&JhnB 0¸ɍS1|8:@-XwԠT;ZSCBi?z̨/}C|ͫķQc|W%$tJlXM%.+I/yh C>Ȁ̢GoCh\-hƥ[r 5 r֏<<-Y&Qj!`?XЬ>&ht`|5,CF) T.jZW|կ?+~ȿzhzPΞki'o?^?HN, g 7ae#?>(}3*f'l<2cIm~Ay,)4E*ë92XSC`A><fc(A2JWfUVˏU3!hpWU2 ($t~飈[Vj\)b6JEJ!KŨT}j x?=C,VTLRYʡd7}PD27)A1_Vs-yU80g{qkljiqB-U-z62Dt@XWY{4fSC?x[,´k+/q_ ۭ)Fg)0W0_̷cը{jqw l Lqsn4l IL*G!p,[Uz 8,3ܔDQ {Z"fZX8dJYopCkD>-MG>RSRS3h>EpJG>:>\ROhWl {'9PCY`Xbp67d?$Ww)~;e-Wig7; rmhGa&Ef)So' ]@KtY^I,<̨>Jes|~{m*xXZzM @F=+0{JpJbqh24/7ܢ Rr< )Y[RϩO# ĭ;Ð1Z|b1t[^8IZ&uyL,?X*dV&|UhArQ^lcHmYN])`ϒc]ra֯7eRFKaьV̹ \/Rv|4(dX{ /P EȨ 0AGW1;B`Ka džl.SuETRqm-oZ5 y#52mFlUPrT+u4`8\Ds+S&(3]1 ʭCLCTa_bWR?0*$8l&W*R=p~*Sq(Љ"b冀n;a}K0*j>I}?c"^({%U}q+pTOil hs#ʪ)k&e:q̓u1&PWqp~z%Ⱥy~R:=}_KY[f// js<~/L#o[v,{}ʮ|Lt<@2m0[`AZnDf`i@Htn0h^Pظr`JTxN8 D`%8%"|S(π I,ir1DDh/^idբ1z e]gCXģ4"SPӢbBǞ;XAgXg cfFᓩh'IĽdH՗˓*`kp&X#U* zK\Tf+ y 3/W䉀!>?qz2˗˗r .+/` aMcXJ1XGfab5S/:_ ktTR_a>fJ0Vx`03:+M!>8È.qFC~cr{@H6YGJw0cg"*H%G3py6Ctj}3:@: `CV46S NV\4pʨ R4޵y#]!|BڴӯGDeTcM(iT=* "}x-.M#NP^9WnPTܦ8 N)o#`'k< YЬVs%?DO]+s{%(ȺbB!|_$AFc+8rw>]!`b0Ŀt ֱUwM3.寢2mR Ǧ}(vUUvNSPfh,-OJ:}žաy.4#*#A`ƃ+73 YJQT_XU5ЗsN/KN>16yeoI-`BtKc ,# Bw4+,K(f8ȧqm/&AeQ^TQi<|#HqJ1P*rmD j.O%kmͥZ Zd Lq3׋w)cIkKMwx8IIqU+?f)p$cuF.,#~pMӧQD<,(Sb.><#Y 7Z~_W(ƍ#¹|xcR/PRWhZmcGF8b.Jvܡ&Qy+k*qY( \uuՐc onELѨc EBkimfb}>%yf )p.,G^xe)|pvjFvGrL5#*1 `ALt;|j. ([~]ܧAo &.K#l$Aw_QusU%:?1_xSw'mRTtmdOQBt?+=$U67a1B/DHa1h=%A)n&1_E,2<"մg,Yy8 ŦPAEC+= Pa?\^HnNufkH88MQA,fM"DE:ueي"0-7b\؉~Hi[ )]C%@2gZUf nU0.UUNEffzhw\+fO.U\^%d j\UaU@Ш8"PÉyâ05TmFH9Sjn+QQ"f ArXE\~`aKՓΥ~VXsevAP~|e¯rGaVޡf_ VvBU19){Ym2\ܤH@O>B/_ةuRWL;X0cQ!]NPTkoSS6eJ> &/ØULoQ'}Kf`IP_Z((I`cR2:XZ^c*#PU=C-X3xx2̐ҵ[Ry ^}oBgs!Y!VL UFrZ<[cUΉI,V_rVf4˙h[t聪Ct^Ŏ_-34`/-9T|b4:L!]9!ZTZFMU bl-C8 :ְlJ 2ToK_hn/zA \: Y-%mLq*\]La] ) 9R/P'#A`c3Tt-KaipgEދ%/5v?\fVR?oAV">" du}+K`b;G9QZ"(m߇/$q5 =[XchvP;×Yn,@!K4~Œo+\#~IoK8SRi3[D\Q<ʙ*8u"/ Ea,ȁ$<6%ehTTSOD| zfN#f]SR5j+Aby@m c ^]r J'?pUHw- ]Xh1])ơLv{ R|XSaشp/j5-o1!Zt{ֿdP{\- rVm58>6GRݨUu"2h0 ^ GS"yz71,&4S@!M1Xv^6$*xj8Q_k n]-־%Ko\KNiDgAKraPZkDP8OPt頨b닁nJzXʐ*g09ʷkA:ZcAn?!"o6Q"9?L}"ԲBr&0Q~Fr3l3AQn 7z ,ofo̪'Y4iodcY: iQ=Pk{d< nOt"i6{M+!4`DgAhv_ ,:r% iYjqt` ̋Gx:Y#AIʾ^gcl.iP PƅA<' `=Q8tSsp33>bY 7 w]zicgtʧF  TADp.#h7lΊ~cA%+z˶u!jƗQwJԯS3S3cd`jp2/ԝyD"c[8/0![P*1saWDG)+w%!!.[5#(e8X˜DF6ZեTL7{2Wg`eK\lmx4a(xH Z׹x3/?7%0)LOo,sbeJԩShb~%r2 D-@+FrLG~*1309s.4ˍV('Si"1twʕa1pUwuԨ7vYZʷtz1)Uvn19\Peo(5.)j§-FQUA8` uz<[i2``ٺy[Ą<(+Pp5]x ?L<_nS_iJT5pҧ3%=)|>5ԀXUW/ =ϼccY Ņ.riTUY9nO֜c̺?>9"| ?\ݼ@ {6;GĨYuqϩ%a0l}!+w^EH`oUZF-gv;L3MC@ H? *mpk>?L[v7;"Cqrs*4)G-!oUq>#0n--i? CR5._LJ]cwCxe *!EqQl1s?u.|ۈ>CMĖIoQ7]|$;&$Jܮls(㈴ԹC㹙݈ Gp.X`\A\Yq?b0=z@iC̃J%x᪎Qt~bH!hpL@Gb,^Y`@⚳gBDO"b5[A'7fqɑV:H9 D>X[t_*] (,h;J!jՀi_e&`Y8yPW<řf,b( ɎAC5|7+|A#1+LSNF5Di&ٱP׀GJJ40 a;˖8Wά7^u}aւ5)Oj>a"ekOV"$YE) <|4X sǬؿp)Vp0Ig53qYcxޥ 6O y%XZT/&?R7~ 'tnrRj!99|K,b|b ? AM`n\2Ⱦ8EPfl튨o gdi|WrPjڿ 1 (M0  Qkc0e}z,ʷ~bg80tֺKX1QZה׍8mHSqTSnmD3 J{x-7WXx_X4<# bp -v%ANScPTZw&"(1`F Z0,@EHU_;!p` uydRm]+Vj_,*W1f:[j/4`=_M6P? +e̠HcPT֧j)em-T Q߸LO̹n~4Pv-"]7,gg\ZaDS]ͭO&HHՅU}!x_8} &NXJt[~R yKK"/q F_ R iKNu cN|˅}8^u;)8R*7MFk P1,4zw0 * s̿ 6*bٜS)4A`vQj|A>(.tk&' 2z#xŁ1d )w0M4\>0bHw1*v tUU@Jk44foqe[.ʪ3q!lʲuj)]i'k7` x<^ ^ \x7%cse?3C{ w#b~n-y3 Uh.,50nɤR\o- Aq~WЅ/1@!L#0_6~£b@@)(_f%>+7C]Hbٹ4kBYHMs`,_"do, ӯs|A?eȖNn6 B|?@ɋ(9u.BO f9,BE&>'H͞FA)_PgS}xl(i~"1B%>h2dž$L˲nmw#eFh]P5 [X$X1EGO9Ux^%P+C¼BGF(tJ̠*b`l C9nEРliF%C+]L 䩐h(rEͱSI `TŚ^X3!#{|ӄna-5@3L6jIWp^" P[{e.vCPRUFp_#{@ s~#u/r v{Yϧ8(>g5=/oQo:O[P븑._A|$)06tԵK_=rY>AA9SJ_^cTsQǃ?WYܧ/b/2J Qr3svA#BK O"7R x#ՁsF{6w"fL|9Q~-R" (f zPXE[x&J@| TU4)<1|Hk?҄.V@욇-}[rJV"&YZf2+|9ZX-4ah}׆l._D4jp!+FcBtָl '' ~r׌l垉OCz < K \ŭv uGSj*` L2ƴi1**?'[Z!(6zͷA@Oȗ#9F1(E]HYD[2R1 x1xt6;kP5gQW,: Xy4a_/IEq@eOpPÄa4_yeʀQЯhQ!6,@^O8bpb!DDO" eՆ B%6YWFxP4e)!"J4AAC YbS12 b˷.>d%,8)XHnbT FWsqrCyAYIA Y s!q?g%iC&+`o @p"9ijW,{0uܧdij=G$w*\4HwxsoPqsFaV W9J [f ;~cC.%ʷs9:9r̠\:re)BI?So}]Sq,͔X7 <*/*s9^^ܝ: q\zM=AM‰J>3[S=.[}ib"$-x-=\O@$EeLL,s_ fV)Ѕ1bk-b3%^26}E(5YJ[К}ED, >!cDZ?`ǝ.7;%vRʮ-=SjƘU&ne@w YoʥZ; $WhEW. o$Htr/aIIe;25U\1+!^6OUUt&:U= `N{SO ,Zex#7(*wkn>|Duk0d,:%rejRĦy/-13P_ Gpr\ġ;i3+?1AFv\7X[2`|NE7W Ϗ*Ṧu/3G~0iܰY1!\Kq_=~!S(x.'3'+=r+{sm|-,Z8O%O,U7yc-co+=FJg Dj0~`VeJӋ9X" AqYɁ^(Z$8LnuQ_K;;<+%FhXs$ pDŴցA}LtڰUЁ[U3JcF&] Ѩ^ -rW_4KuS rk6Yx43zvbYqsraÌO(`px_xz?~2+yFѩU=C c3uq2u'aF$!u7ʫfQZ?u<YK[ﴌefDljIT-Ov}B0;r@r Hj՗̒A MÜnߍ^e@tYt#2,},R[:^DXyQ{TN ?EaEf]beci\[QӠ)9 Ql5p%R,Ow`8, B\ d]KհUf؝1 }MiejmMRԤP( x"8 }j)L?c @4~,Ee4Z"vW: =A!;K%"k?4Et@Uv-alA_De+G|׶1 ەt_/˾ܰ$t`Exp*P]sHNì2/p&ό܊<ǰ/5#}ʴzi^klQܙm37 +`OW~"-[J GH$oofs@W*2cUݝ=ʦ YZ6=lDv\i\uFޞ~#>LfpX Ltj9程2wn ٷϙO .̐yMyõvн@DQW =:N.p_7HoiEBf&%n'Ռ4TYB"w f3ELxs`A.hyT>a HЗnٴeH~BWܫI2*v m@>]Xh(YsXB ˰`o8mg5EeHDP 85ʞ#~BLa&'c|^@4U }) ; ztGC ~b/qV=GrVa+%#XܭusE9)oQER0^{-RqdD'Ro1 uiJk1!R(Oչ_),-D#2)F" EOA)eƙ,e\" G) [?7:'l;).Q@(k8p]+ǥ ӷ!kSo*jEI^%薶7FXUUjֿ*K >4tsfF)#~i*SU,qBxe쯕>)?RJK6*MG|ZҊU<2 Ξba˓Kl]fT-I50g͐N3iD|n"t\π\c[mֈ bx>zh3 Ŧ8J^h ̪a,VAAAik")+\4|ޟRK1b\Ko0U1+ Pre64ٌbxGnǟqY!7xqMKI"XAfa83.+-]$4Ųު^wlz>*d=2c-i’ܽP؆ZEOFN* Lě^25-2azk!kçrm /_$"|Po9jvM~f;cefaZ~Ysx9aP[!sqEsnW-5r2k\7qfrpi1E@0z4D׸ySOܭᘷj:0o¾X,o7|?aJ@ܥ+ĺ=RN6 ]{FU0P&lGx('7o4!X< RmƟDBPE9‰ >b!Ы}ޡº5gi/+J輹jׂ$;+|؅oGm4Ei0qЛ'@`o*0xu6{3K7)\󾳤H05¯d+][+`Dɂ^N_('ȃ=*= hnԩ _}Cǀ<,0%+^ #64" po6 jY{j qk]įZDUoD)P_yqqq`~bޡ}BUPyfgPpO&N,OoZH7:WO5~h8,ZJ:A,*a ziS+㱏@2J>/CߧphNMLA'L{*&5x[UX0Е|ƛqKy &a-ƦǒPלȈpG?:SnQ6 zGa_GC\~\JuXEi%\A pU-HմVWG5Ϩt؛=F`l\s !C1Z>|dޥ ME*5pjFxizuޫ M3rk@!aøW[R qmlhAxn$cp !;%@ '[2<ⅼܳRL%B;h32k;(HPa!+wON>F$,§hph]c"F C}ߙ[հ `q-!xfDO܋G$xZ[yF?,L&A<r@N@2`Bd*|FK5Ayѱi`3-UkAM!Eƫc"nIpiU]] gl cL #ˆt w)cDV'zk* o3l:kjq%TKw#q1(!kpxim9Č @wxLhUv+Q+q *YҸapTK4EvfwĪ^; גay\Es g{,nInԑӷVP{&xG ᨴ2 Hs___29?x͙ؐ) (!bg[)] '_M#p5Ix3[Q `_YEHsrih^tp @:M̬l4"S] .OjS\aQ& ܣAU ? yeNO7) DV|`& Lvjꑌzb,*_Pt,a~@M󄨨E@bH.l+Ll  qRPSX k'"EDؕibeO~5Z?o?R͡Mo}VȾK* /< T '(mbjzn} K ;<c דg,w.b5m*(3(JrcVdN3{?P%ws'+qU OJ,0 lXP 3[xQ8:f+^8]2{԰|h- %'R|EB[~%fZ]~aa9kb4 @s W|dן;縕*`{^TP%SAo2PSY~!Jyݵ;o>r+dT=j7SX{'X%~&F*p9MaGk?1wI[gXj MK4X+sKQZndYVD/'s%< jum_q{Ŀ# / c\)%EKJkQlΈ ! 4Ŭ NL$MA1B W/i} _jL1{` +Z^`2!Eȍܡ ʮo(# X(/žퟤ-/8MWAX5)u[w:bc_N5 21Y{9A0Drtm Jkg@n6 ,ĶrnGV]ޢû| k,{r<݄Cu@/\* WI`sohTGPBG÷,(F3 v*!x5k1QBFQ^Ka8l,lqlޗ/wxsר6`@8Sdx1ӫ3AFW{+i~_DYu0V Qn&\7Z)GMa.]:HW\( 벡2څ[ Ҋ9b_0 $ZaA*~ !q}VI r4p5f0(<Σ!bu]:-S=< J12EˁN",nQR+8d_Kr9,y(mٛk-z̸3ng rpÞTL"vPΔ ȍSz:C/rB3oeQ(]/QJw1]T]kbC+K 7T ((>7,ҦP>IJ&>V?< g*6/@GdYv"(Zvؾ_2!_2{sd2ʭWST[e@NFA;%(p!ߡ a&͟,ۀ鈛AYm٘NA40ʈJ.3:>s,*l2%Skrk:w 9!4jֹf~,~ued#)pkl2X%F_c'LuqYizA?R6xb1o7X/"Y7p =A8 8PZ ̐k=% O1׃k4k;Џ̢6]Ecx1.)oeR~V^ +-8q@$y\{, ۲̀ΐXJWePg)wxLhvyPU'̥|?pak-LU1"pOrg!X WduU D1n}npFWRķSpݎ#&+dgu ^.U)E[ Z0Z:fZGLzԧ耀4{\ 1WysN}Gn<0ժ,(ʰ$R #- i:P=_c2p obU~jn=Qc]̵y}U~j,SZL*rjʼnwG#CR%-)1@ vMW>t 1lGQl|Ko4 7GF%[y?l7LyhĥP/{vfd&V+S_OZx-dHߙa.&Oj=%Y/SR6f`2xP HA7 &-\uʉ 8u HKRĒ˞f7smOF΍l<` ? W|}y#& ~DTGDhϜER oG--9ZD)K]e{ @yxw=&.?eH<Ա‘͡LKRJʗwE8Lj8!½ޡhsY>a7 S r3>&E|SӎٟX.SX[3G+Y0cכbl['(fp7z j،Ty,2YYxj=U۳j$˙@7_ ⢹g[i_d-4?ZVLkJ ES4_>{^~]JPWzg4V&6 }C̬[h.q9jriNcXK>uQ:Խxqu,ķ%hGR0fO;p|쁰.\t|ĖUY ?^8LoEX:јu5u Fwa=Eoq@o\N'H>eɷ2A>`g%t_B.< E{>NZ&<*+hSǃ3⺉5SD1uї`{, (?P 4GC\@3)+280AAf5+hFRo]3C=߉NWE=J7(<f.DNF;It"c;zE_0q)&qzb6 Z``u=Cߟĸwr/j:ɇS>vW@d XAed'@*I7;!7 HW@sw SRgwcer˼G0'!>hDvd#TEO.ustU;&Toq[οWd^%oB50/5cP,E% F QwyM(e ͊O p.9JoS*%fSz`Z.Ĩ%qU_̪+eoP/S rvSWYV "H+QW8* |LB* `KLGsgoE̳Y)x15재E~@Y7ĢPA*B@]Jatjt72af_Aru)Sʻ(,\BW>IE:k9FdюȆp^ܬxP*ʪpPEQ8_ bU(9]y,bd9f)er0\#q `!Կx92yIJ-A^uKGevJls0i]8U*5VejZ"TK%&2z 1b::dyC#&eL(^ձ:rs.zJCF̿vnX0lkYpU 3`b,)#KPhq].wp[w8 ۲.|q;,ks ͪ"bcœ[[}z SɉE4N9ŁD:`KC{ )L]/37%C#R^:e/.db`0`c* U ^v}X1`FBxx k7kJąYFy;.RP^%@i, ? JepX&T~a`HS dSbT`Lsw(lo70 .-"}pk~`ZG[P W!Zv d1T|*Z#Hf@iӛ@. Ƭ-/;ϨDbIE 01{8e;8ļ$ B<.74x/7vV,nAkB-ca0 Ib[+DSd1U*0ɰջ`Zr aȮR֋bZ74 5Epu+V]b\xX`, ܩuI  - s2Y at#@a/7TE$<]1 Qp3B+8 蒢  T|-[AL͢9_C?:YeVCEt.)]Ezfr3ʕ6.}@4Olk&&$Q SBڶhYrj-},둬B-TpW"yF +S E/uAK\x(~KXɘс/ @p¯N?Ys_*rcJ 5L,vnj~J?pE? ʯ̨ʛ^?/'?ϯ]Jk 7PxnDQif` /V__Rpgr{o&L'=idPP31zk7.(P;8xBk󄷕Y=R@⸂"'U,?^PJe|b kaXe+~e%K)r]0QRٞzK3wveM Lj5 vafgPOgBfC2Lv >XdK#Us#B@K8ĸpg"qBd8X?3x%DK4VͱRE>TuS,D.r nq ^n 0v_gY~"C;oy'my%Z6 Wj"{HU Z'/2ci)ڵ ]6pM{h| AYR ?쿸=L',>f ?)^5]m,eBHH.-hb ,?06>fk#/\RٷJn&Р!zTD^GxrK U -]>xNN%.xh~z(ն=YOL0@Ԙdxs(ظnsOSy1%ލu4=xyV>-vsQiO!sS0[ffq(_%nS- 8Wj/FpwE}ty Gg89LS?p"7vJavup?7h\CKZ>AM=Eq[iat2*i)aY,ݟ1S |)23ufL򖏖o;Nl2.>wqu&:4is)ˊqX7aYj-XcR/p8(w tYI BZVz{DzJwzb=BlN=B[tN#4XMCO,0,ΞLA6lS~_q駟;e.Ѱ #pמ%獖9G2]mWzVk[* Vh*U'L$hoz1d]gHJТwwu6aAŜ d9jJUwWX'5a@_Ro >Y` :Nk[4 .3" aѣj<勔3۴bg#C ل2t!Gvc*sŇd[|𕎇>7^N }\v |^N+F2HhBNh}mYw|Em͜Xp-*ƶBU"kJYA:%P̓H9 vlMٳY4Dzn-p\<0{R,g/,$]P"۪g Q~߼&J3P 00/Cľ:1;bnNKΌV՘鋴xN+o-z{*ori,ù~EQjE=%̢GZc#3P Ao̭IiLUYJԥǩJUX@83G5Ù(䀋)= ӾQUG*zt>":M,h !3#L+pG"'&H{95w+l|>z`y29)2@L)< BF HcǙsݻc+[uz#@ }[8XdQcYj {_ZB!Gk*/HТ%3S hԿKZyP`Ƒ(P⫆6vm=ux7~"0ɵ@ b !qc6V;9t!0C֗! a* =L١pUԧ*[דIo*|s+Q[0$zmj%o<í3Gky%w"Ϥ2\|1.%-9ޥJJrgdm*X^ѿ0f[}0q]BᠻSF܋5牔JEK?<(uh'FD+ccbХb_,T,t17ap+;։ɋ`'#cC?`&4 ZEyǩ_HM-r1Uڽ H4Ab,fg6 Y-ܡ00qJ*rjj_̨Ιjf ,ay"rIى,<7>QbYQ9zgxza1cd=sBᜎ ]eT#I [V;P*ntq0q(Lwj)ERn/1Ҧq 0Ad׸eV-g q7tQwxDD;g>:\IδW ׏s,%8;@f1SyEԿ1fm#w%~yy vJcY>ɖYXG/uL]n?b8:ZNM_*`>W+Hx hi=UrjzbS߈E]{ʩsI*R|>8cKPVǻYnrUjh~pKPDt<)gL(Dly?6&X)ߟ %ڥTg̵4 4Fj0S2gpɚ%׊& G2.Q&CkvZkXނ<<0Ȯ#" Rc'*b,3a\t#F[b<+5° kZٖ( TSwD{a%ab7ѵF24=ر3%Td~FnlĻ4x'(W99OAC`.Yt6^یitV7"S *v*^y,ճ^]0+WeT*d_ĵFL(a.Crt\NetJTK30q/wrDsɟ9a03 (2Z[fNdsgԱp~FhיU>|HaY> Sḽd9DpXX<Ņb2ѸXY&}Zq,4TiJݬDUuipSWudY<^$_(r`7et¸ݐ dX,,!J5t.V5+WJlJAa4`4Epmna1--v 7($2eS"=FQ$NM{y(s:ʼnӮWw:bSCBم.` @Z(҈9c0#hK/34aelP_  Ŗ8U>INn|c1Sň^ߟȡn0rH =Ph;\bm%pN: DT1dI#@ M#;*?D] .d!SugDfRfJ1 1Wp jhM0JqБ4@PJeU[D\s9(9]Uu+yگƀp5'NC5w^9 6!Cnugdq=ڠJJ; hhǻcS0îQ@"TLQ$p"_j9}T@  JKb7*KQt˩WY0uj/B!@lZ9`/P.S u3 ;2쎇$h`88nDAOSH Bs.l\O5.U T`E 9 Ī;&LtuCPz#g4LKe瘨 \eY/PU@jnoffpunk-v3.0/screenshots/decvt220bis.jpg000066400000000000000000011701721514232770500203220ustar00rootroot00000000000000JFIFHHExifII*bj(1 r2iHHGIMP 2.10.382024:11:29 10:17:24 http://ns.adobe.com/xap/1.0/ ICC_PROFILElcms@mntrRGB XYZ   8acspAPPL-lcms desc @cprt`6wtptchad,rXYZbXYZgXYZrTRC gTRC bTRC chrm4$dmndX$dmdd|$mluc enUS$GIMP built-in sRGBmluc enUSPublic DomainXYZ -sf32 B%nXYZ o8XYZ $XYZ bparaff Y [chrmT|L&g\mluc enUSGIMPmluc enUSsRGBC     C    ^>AP*!AP"!Ah rFAB D„ Le)FB8}8-TnGGA8nPe_Zd0DTBh؂ -Zt5*(2o/0 ?Gʆ<,֖ښΧCB4dt@ FB"VBM!pABh BsJ0*Ch4 ӇpȂ ;*; (i(̊sCNy.u>}fsYY+|t2r]MnZV[~~pPidR ! -K@(S$3 " !!!d dRΐPH#MShNWʻL[P"*Zy %R(҆s -6'R@*9"mlfؚ&t.UgSÇAP@0H 3! D!*T"@3H ABLxMS`!#"V*p)T.8@+6γxtԪby р:YEZYXƓfݳSX:ZM/wX!B ҁ jR$BPWJ!D D!  B i"TkJHU>S:Ob 2N: (JEgc܆#+qg\P@G NȎn6͕sO~SU.\ܲWB!BT*R$ZR $AB%J BBf"l,pE5Rh+w(TeV 4ԡP(EB#oŀtF4cb7pe~lq45tͪu(T( h` ! Ej!P J@!R!- @hDV#6IHlI i_ dbb=Ea@\,$"@%k2"cS.JjYKiklɫg8 *AB(HT@ @ @H !B 5! @Ji2ҏCUéˆeB\ST B!hp^zak:PG.g786lQdͭES7]0^jXCS DT"B؀"@ *T!BZd@֐@d(nIVBVq:+}&M4!SD "D(TB bcL4_`ʊ3^;:VڥjHWQm.Fu`I%mMIh{<"JaB0- [2PT%BYCD +@EkU(MBY eChtH2npAM(ZCC+I\i} ""e ]p^[:-ű.k:w6毗jB dB E! %jE!tdJU!@&8.fZ8@T%jd)@@PJ ɇfףB8D1DBD!C%Skz9GzӤxKÀR[ѩ5pVY.U4.nD T@S"bsTmUHTB@B#B%BTThB,خW!Y,j#8Q^!i @ J%8*F=6kg{NjyW ᢊU[5p[P.9sj%K؄AHD K!R!Ԧs*RA@!**%B@h@ !Y:Wӡ&YY@B8 !"eD)U"@"dg@y= 4U*Y9rYbp R2*jR۹i*^uM$w4U!2!Q`Q]2 TAL))H!p@CDP%@!*4~WKqw^P +M%(!AAR#:[t^6""UT5Dą[z]3BY%:YunoUjiGAJ-tAJEt0tA!(P%Jr!sMG T(%hD!CE @Fi;4I4daΫЎ1^" %Bj%rsZ&"-!)e^G @<ȖEښjRVij[B~?BR ڠ̡ZT ]$2++Rȕ*@J98HJ@ (Sj`ڔHIiq^nhaQ4C)P*D!"棧^[~5Q1H(^{6EHX[s;Kkx+PAjWJC2ZD]**dQ*R0lЄܸ BbɢClXZmR4kו#dDZ!!2U8p!&VuG4[OUxΠLpعDIR@Ԝ4ngt,>M ʑ%BEI@E24 !Dh%lAu9%HP4#@ T2 @iA6 :YRCj(+jBAe(GJQAO:Vv_aÎ ƞJ BQPre\orԚɍJ(D!mʇ ]jJ@8( GB !PjU B, 0@" !@b֧o-ي+kYqᔳ)D DSYb\--Z_Եd=+S*B Zf( ̠ ,P!1 `BBQ2PVP Õ)Ш@X*HD66h4UP,v2i`"Ly>-b4mR hV^qEu .WMVKZ'Ijvr@B pBԊeA+ d^yhfS Àѧ" A 6#:ED#C ABSD 6 0r}#;c; XՅ! Xw6eY[,۹Թd Mw HDBB !D%PDM8#.D}*! BpGHu :!H#(TPjR4t 0A(A:P%j ?!!KfX=.jjeĔ\"DBP$BG9_ɔpuhrB pҠ! !@ #a"J4(4B)(Ґ *nK#,!é?! yK9Q+ĥ&-seiäK:_f\{/̑)D!HHl@%R"T|^st:&u>!u @D:!p@Q H: rQ@u4B@Qh"Â?Iu8fVG+UQJi$Ҿ_ֳ~ɵ]dw;eRD%Gb"!+\XGv"!"J4pG4! BGQhA B7$B*b6}}I}_#tpӓS(TRj3ZWDZ-mn[$>wڎ+*JDBA8pGyIY}D8 *r! "(\D@ @  SOk<_-ԇ4`j0 $&<طW5v-w;bU*a%BB!"&Â8XLD"DB 0B-EI4ЅD(hP2H"DdRŒ < C4s}O&VMy(_AhhDyKcFKΦ.lV:AQ•"a"DBQ0xGY?DѡV4jQFQP#XڎiR * U5ZPZ)]kZ8y"cpYUĉ19,IO5,1 cGq4NI}O%mBTk1 ĹPF@E2hؒDйFX~o A!P"D>%TTUW^h 6~\[hHQ :e\s39pZ7'Q R$D4TAT 4 +B!( ԭiZ[e K$MX;k&4+|IFJ"XJQҢؕDԖ:ԹAJ"!WFCNVƲ>4p[sS(UskKD%bθ)Z>u5\ $D@T R (PWji˦@ kE5#n!6L|ޗ;޼u-%~1ʄ:,)3EƥKXKX!T !BHPB$8^?D"W9\lnIX{21#ݾ[HZr9#t{(!YBPCh+oU U6 EMXF6 V3[>[ѩ^v.wj%GGWQsgTӦ$HѲRٖBrED1-ho7ѲŖu'#=zΩ!P#CFir9 'yzxft p׏ԧ.W;2b69[ o,mTYY[x>{Z{rzc<ޙҥ( mDY E:=læGKٝSi+*8,iLzy^˷zO+XƏϞ_%zUtlm ԑBU#d!GSB֚Zz/ DAr"B! G9"Ƴ<89IR.t /k[b s\\@*# +t$j<ŸpcKt֥%Hs>O* m:B Av4D KAパ^xT3=q S6dM313:YUzynV87T ;?_S7;Ǐ_v\?7o{})\^n|?{/z=~oo/WOoS"7oo`Oχt u|zVֺX-*5~Owy}v2mɯim-jKUcx層\DCLʖD&&.K^/knioQJc.Zm ȏGQZ-TC(nuKJxd狯3FGO?+13cx3jKS:r3{._y`3M/ï{C4;~|Ύ׋ }LmXjWA O˦؍j1Xo?]5:ܝk3Zmk";5>gsͿF.YjKZ}O-g4lʀRE&Kw5u ʵ[Cr-~WՂKr"AB !d ͩ GAUWM/;.f,If-fj]ϙӿJ0'q.kkc_:ff\ugǛ~7^c?WN$כ;>ZEq] ϣyV~ hFu AKL뼭ܻhUhԊ[=E||ċe5:KHZf\lK*ʑEBBIJ^,Ȇ2̲k75/fB_+}M$B h`ihCЈ42Y!Ka‡Cϯ^j>3fIy,E̬hgJ2'C6i$YVZʹҹ, DVFlcpK/FLTv4bemJ:fkFV]n`:.>wҰH ϣ5]94"S0eGī+6aD6hТd7wv[[#Z3~M^VgKŮFr3\Y.$DL>IU:ZK:A,TM1ӬԊ|aQyFd JЎQHFl<B9,CךVO49) jDtիo{Ͽ?3XYfo̽r4('۔C!5ᖸk$jdy9=Zp,IVg{qCSk7jz;AOBz:bYGaQD 5L?7YG!K>ؙ%y3>R"y1sL^_gѱ3|\So~ frgћ_>ms=d9oc~c_mω=>'9?=S>sY}^?>{M{'LjjX"5];M,]FQ~V=xaxf5ҥ!K 9)dȬX̹P[bgGM{'ԱN!8쒐D(Tu)#D+FGQEC )%:u}!%?>̱<6"1~?9'}]|}ijOC?c~lοϯ3ޞwr׽ދa-sWYMjk62gk|~/</5nv[UՊXlkY_/Tד`RT5%69d,1`-iw6L hk;S`t.f*cs>Ӫl0eBY6uDԅ CB)=PeVZq'h[??7y[1w4C¡WeH!vSIʱ@Ȅ5C):yש&֣gV^ukP k~_ך$]y?Zu$q)ÜceL)1`kY\Dq P. "-j]cY8ϵ>wZt0a PiCeER Q:;ZLKc.0l~MY1QxVo/6Ec)A9pCB"?@Cdq~.g3ݿ<ݙ)h&i'̵b$jIP2`T I\B*,gv T44nzXzjgYVJ-Vg֟/NH=/Ļ,n޾|!ᦹ, 9i!e_7i0^ZOSy٭iBUaZY^bm4tm)4zXvVF :W+sX9 C'oOqy"R`NeêX'8\GZ.d[k5GRk&txg~|bSf9ξ|m39z=f[` *P^~fdݳ_)4}d\zkǭ7dIXK zo_=Sz_=>_齿pܿo\s2-mӚt4T)6RxqW##!YB#YnYVnyk]{yRRnOZjowCɥ<^S, (5$BC~+Fmk7e]I>{ycG{BJ|֋E:>}|5i桖 m-@6X:mLFLizvcY[⾬e,vy\&磛L ȩ[~hγv0ĥ\@A,rj/Vfhi|͐~|iEJCr|>.v.O>ܔfl|C]~3!Q|zG ~tl56[7Y& OνxX)Iz·_B*SPq?j߉n|i>îcZ<˚VFcFBX5Hh dCwZ ں&bcY[V Y}#o֟)I-usyg/'D9!= 8JGtyz(<ϞwÙƻ/.\38\;I8!Ľ3_#OcO=x$'RRY%zF2iꈮcu˭m HMnT8hf7F@YFRʫ?Pϩ[79#::ۻ|xޞjw˩runp^_s#, HB6Vl 5=25}+ѳֲ+k,y\ِ $&ZOWIy 5he]߫$fy7.g7ڵ@Y;<{2LI,JLfiÝлwR"À z098Go0|_Zy:$#ZZoy}7#J[*~ߝݼC.bidGrHEuvtW062jI`@*A@`Ў0eEQC\άm/Vb^3$K~#Ye46-OB;q5HNY\|:4+E 8/*&|~}jT*[}3|[[r{<>/ۏa_<>7xߝ=v]_R._o\-X 堡ΘdVq6Sd}K%Y7<2gT`S~w͓6 nk!Bٸ%?C~g]ǧq9JMj/g>v#GK^?P:(Zq&>)x|̙%I$fv{kK/湹gW i3w~gǢX_N=FT\KXƒ‚hܙ]aP$T)1j#\ڍAUGMUbnFԇ;WWI"5昼D8}h<ʬ+:6^sCNCgfg'Njj%haR7~ץL|'^sť7jnluz&mx4|xr==tXưiϣw2Ԓ\3,>Dtw}-ϡ}:~c]&qwG5^O{o6;~} Q* 3CE` JdudEܽjUkk/??;]7Fd^/^G*}|8FXt&W76}XmU^w!RDnǑ͈&5-s|}x7iD[(lBթ oӜ!ѰTVjgc!Cf>no\ F,̕nK#ٙ'b:|8}^:MYNK?ƀ0u-/y0$>$Ǣ ӽv:tϽ~Sѡ8x\HZrCUҘi(E`E$wQ$h-b[ Hl$ :}y9-=0P]{xPf1|hI im'ѥƱ3Lu lF=] ld:f/ۍ鏁Ù%bi:,/~еQ7z,Auyt\~MϥOO=9i !(R VE +kaXCZuV[|}_Ix>@!k=l}0}EKtmZC.c+B!Q֩MVװO$ž~4k=")L yy1a-(' UK).:Z8(^JD%2I'鷋a#mJ yx65(v.]u36P]f.יUkͿDFh C!ϧ6Vs${2J\x=_yJ{q y/Op_<ާTǓȧ8My|?o3EtW;Ew%ǗsךrDFj4 ʎ*[GVW|C|VMqs$ZIzs"8pv5+ bKLf/,YbMs[MZfy;Un^~}0%.^ueY$,2n=MP꒖g\ls#:_[4J vί/O:I^2z=$s4ֿ:;y2'r/>Ӥ#}Z3Vz2Irkr9ܩ`zyP -iRLx~yםo=GZP¢r+~5CCV"r^3"(,օιt`Ė6zN^aۇkǿr/ dh0%>[.w/ⁱS\筎'_FFv>9,sC,gahIg76Yԗ2K, Y"Pì*eD%bBh4m2 6JKvU4?{V{\ dzmtF=sPR/DV3_MyKMn*%zY2~}Rk?+z5z{AsX k r3~[C 29֚zGy@c#c7kfdÍ}x5/{1/d>&]o>p]ϖ7\~OmCjh P!) LKpY^ʋFzSW}ܱj{}YO|>y=swH_y Y-KŪ85(͚5K7Tfs8+}^7j/fyFQխWYZy6]ǣ:8%Dt{?ObP~vU~5dbY=q־ѯ;(dם,ׯC2NfG|15m'}nG?~F<{w@Ba4GQ4`ʌ`ҶoM}ȖXG=חɞ Fq\Y-օYҴf$lիZUZ$Ǖy5ZJP >g~7pӱ0G:q};9jhګ6 ђm6zgb!\=[ht7q>72槜3$ϲ=?rܾQRk~y3>WrO?~>nw|.ށt-H`[D@!DEM0j5#DAdU-mKj\}Jx~d5f~T@98ܾcy) ^H,ۡ-ѲΑ~mu&Vjԥ9̫E"u~=?q[cxX_/m=3m6FUq^O}}{rY!ۖs3O.S \ʳuᕮtn3+S.\Ԋ6DrRCєe26@Qk  Q eZm6|MOİT7X\ZAgȽ<±@hXKzlfJUx[:=`bryXJdt~x xW/E,ibIkK;Pr7μ~ ^}p;y:Ǚ|:Iu=]+1SAS[-Y2dBFCq$mRHYmS+j$wUuZwMFjG$!Xn#4 9sP5tȕ'<5ߍR0`h5Bdl=n<9؈V9$+'/o\} 6&iwטCSޔۜsY\ES;ugc-'oزhg{YcƍBecI"NNJG!%C:F[M! UB$6:iШ^mYl3A"+!Үm<U3}SbO=~o.ʹɀ,k4BVts:["HwoYѺ=CBVC3מI5="5ۣƪfyߛ8>kƾwu&ϗ}o9DoTp24DJJN @TJM *%He^o=e`FA֙ё6A427*Ϙ}~N yF)Ape-FֺUK Hl#WOgخ~>6̼ bN].=p5Cٻ͡fk./wx~Cc? y(N4}!X<'IQ!(@ J%!+PPB6DhdIуFeC[!&,VtLF)Gɾ')‘ A%Tj @TtKujײ@2tmNyٽgN=^1W;ܺdpߗz}138|.~SisOr`g+_}Kq|ΰP 6!ՆV I#xp4B84@A(A"rl:ɵd#+b6RՕ Uow H(8QRi\H$+O VLUd,Q#2j :_Ou[ϒ[ų<+-?+M~fo?Hm<3snjo"^W쏯ϻĆT"X*LBWBЈz hD8xlT`PY ! tGXU)F}aIeI_]ώJLQ#F 2,0heb2IJVJFgIbyc~y= ԨnxܼҚ/!gKpr˿xa;]ǎ\|kpu<{\cϕy_E=~ӽVs}O(Eu|֌ H,,YZr[%QP#lH"èӵ7bEH! bf" ) :0:H2Z;䂙pBQAp@rab :?D|YFOn~%Y]μwq/ϛ]uX>=ZxG><~󛱙GW<<,$U=ff,?rf<ĖK}&!EjC RN|Q) Cq`DH܂#5(?39ڏ̐r9Q$!(Dc!p`$,tW2­cYUôdgL#- Ü8Lƺ}y`-]nxޝgzzH|99ry]|_G5׿y PP2|Wˋr05~oK}2V; g+,nza6>U'LIӚ "EMhQ.4AT*D A,Wˉ3[”]I$H/8e4A:#I#DRBI^WZ(s> 6br*1X;6ԏ^7(:_93|vyPtۯcrcoV=w8q>]\yc/!! z d|~_r ?{~?PRFn=2:s7],ACFCz ȀET%J?7}'$d|FNch&Y9G$veP u HlIJ$I"gj~qJ|3iwr;ͅ[\?z=VSrDs=mG/7CI].{'~+}ZnK):KKCRЪ+V(΄ D3DVTR*;["CJz$QGҼQq@!B!=–)st$S=>^cYd  @4vcɑ(^c+ fDFKW#CºS$=Ϣ0γ7V3絞Ww <9gS //s~-z:zǟlؤ U"GRDPBMGt"(43Aф"4ԦJRY&McW)+y~AۄU([)M+B#*&e$B#fk)7hĤ?ǯmSk3<\z\waqן1IocK 9:kZtNk\dzx=K}]gԮ{;iL^Զ՘VHWx 0D!A6֪B!h !CA8t!Cu9(f)rmtzx>ܼ*1L#XhB;0-5#"΁$[+Z >;o=6Oc1ϵ{0>ner߳G0c3wZ&ݹJIaVJV\/Y=Qf=zRpF2;:c") i"** FBT R @EY4Hhܛt0DC)SJæ\M $2edR R[Zz:_OjY/UOS~__\/z\OvM캚0DJ]J9yyjun{$eP@ 徇{:CG1-"C*T TBQUTB@!BPBpe#XժU+YBiZe G*DDb)[2k2V췥\ig]_}f>f[WYU:MN-^"Dvl>Z9t:3tLʹ! J7&|Ha!PTʡR(t*haT@ABq bV A(TMd"Si$y.tQꩣb;X2f"=Q8ޜtSdgF;Lorb.Y{Fq\٩uV umTGVMUˎũ+ƨgqb ^"N e!D(4B @ ((!B&,.B6AȐJۤh2tnrPGH ZiAs=ʏmZYYKOmqS\637z^<${^˩`ҩz9$W,c^C׎β1YbHmQRH4Gs/yfx(*A"!:TSP F< kFBDmB]HCI$f5.YlZ33vqrZ4_?WbڱW#V3z%r+f.IT Q;/*uuTJ!QƠΣ* ؈>"b2@*?P*vj:W, b0SW.#"ԳH)Z21@@m>WJ@#fD FБT$$.-*@U+a]s@6d:JJԃI,VMh^Cz|u'|鿟'`kNt͔:j{_N~5&)\juuvU)Ι]EyEszEi8%R@%k$A 5Ypto>RV+]X}AQ$9etY6"D VhB"c%A,ӡ@4*2eJ!B 8*fM$R(@B# ZXʛUC_Y FqRڋYxl, YflƮ0e\\skq%Kf4`#(!,tG/OE_!чD4y#2U$:ik,`8! O`K',C,cMGca%rۡQ]W dRAh+ U2H2,+Hi\orKoLDܒu+)רǔfK\.]erufyuJyy.[vZ !)[PY$ 57nw ya !Y)BDZՎJˤc ƑB !S LUD8ZJ:ѢE$htnrTa)pG)DeR$q:/gUlmt3xYmN]VߔWkvRe6z"bG4InE^<U$ėyд泳icYQ a43ݱ}9N*0`I!Oe:f&Dt!,eB! +đRE 5 sh4-U(9&ETV\1ՙ-JYuFtΆ5&M~-qYĽ ;uS]n`ٳm0lFyXܩG-XurHy| qpYYhܙ::%2&c>O>sε%DZ4؞iFՉRF4p3Mi=6 ,WJbic %G,#1.B,- iA2BgOYF#Hڵ-lJEsH9|fN3NfѮo6en8X\tՒ$#W!*b RT{Yq: )GDVOk* ZYHi`hVJ% :% K2P­C&X TDȺ3*9@FB@cBFM__"$Vg~~Ko䄦o4RP!C)$QB: ڤ0BĠ6$lmҐH` dh V5d0 BVHi F[ 쎚4ЂEQ 2+ `,E7O|Q #H3cІPY D-B$H5TKPF"HP+T@A^4֢k,tZd#+t5L:iˆ G$j 2CaBXBl%jGT|$JD8jFΞZ£!TB!L_.B*"! @i!!*Hex 7! 1"02A#3@$B%4&6P5`cяN>> S~>"Su9B-Ⲫgs6 r콅[P~,j8N1ӅO&׍Ǩmc|S~;INN8Tmmg 2×/:` xwb_qsn]`qvDn]$nU;~Of8@z9'ctÊ.g;m@<\SA@ð쾎U7ނ2a'jaHyqw̄Cl}S (_h?fq&dʠst'X8+큳Z1@:Vc#LvFlL`S4krO\}ec~ޯE9]/wr28#S"uHi t`,,F0$aӴdwGD"̑g*w4gU A ivB#?hQlQNU{[](Jpd@<7V23Mjȏ.yf24HNr9LfBb ocwx@"yh;хz=o~4剿R6 =`C Q4;w51hWI^o ?h!TNGYuj@ޒåwꗏN# x\BkHNskZY`E P GNh PN48Bxcho<1UCJS/Go#fϨ'ZۚmFecJ{v ևa!654Nྒ :LQ{RGk_A|nLOL}я^> WOoS^r(zYjڹ!ds/PG\&QFȗ%.(gEeT5PEF *w/aq,9E.-Hdyvp㷭r'#}''dϙTbwIFK#TuON{vX;gjb1P\(A:IQ@uC\>oA]]x ]Hn?}E9;jcq̮OŧL䈦|-dqdls;'!/5 5Nj@pQnrOmz3vwoqTߗGOAr!k\q`H}#{`,aqZ3ťG" h`(8L1 ,q8c?dvzq6bYeSث=;|8JT̒O#Udx{˞>Wԝ/N~nM-jlF1h2c6'6N6;nw>zBO>vly?QNqgW@.yXwrʼ;u/rBv3>0~.R{2 u nbb`~;sbg6)^wϯ;eUTe kd{\'_!+kVr,KqHxLnT|z\.q{Ɏ^P:딄n8~6 ?wl7qNN>[eeeeeglJwՔOj| nZ r/94ֵW;3.%,w &v~}X0hpPzaEy˄1|8۳l}9C{O+(;,YYܕVP(&)-)glI s3Jʎp;쳝99YD4I1{& xJZlqtN7>vl(Qէ]XV1N+BKļkFWjm^i͂Ay{0뮰]`]f.KFOV$f a]hV%Սuc]hZ,c]XR%Չub]hWZ4]_EDhe [דmֲj_[|/5|QӒJviv=qFrFr 7גҝ:)p[[${\pkYl@\F\;.Y+.۱-˻{GJeF*FSLٺslqRW+@\eerܰ,EN\VVVq>A~#ݲYۖQˬ̫@Ϛyɐ̼Bde5<飦+9L湑<Z uwjvOT}QLGby2ct?z1X0C uF]?hzq@ӝ{.g^o~Y++v=&e6"uL6<rc;.DW&ܜB\ J9j Y Bu3?kvnK~>=cln>}8#Oul}oFVVVWxE/5&1Ռ zy$;]}ƨAk('jJ$ԹG Y3}]#UgцAc|, XX^ X;c׍8;Ӎ} lDHYp._V'{F>5̮˿ kc^U!6Jn.o(ւdє}XoFO0Fr/mm6}X#l X+cloc| cc]~IY3 X]w]ty=10'(E<%, M5 ,II|cӝjaX+ⰰ6XX qXXX+\\V,,,.+ ⰸmaaacr.Eevtmd!1v"|{pp EJjg Sw]cI+ .lx18ҍaxo85auY4,. \VX\PprW˃J^]7.Mˉ\FHYrsu!]HQԁu \\YbWeߐ\JP.+V \N%qXXXX۶(.H=ee7c`d>aG79x'1dper8YR. Mvyop䋓'<0atOh>X] 6qYloN#qԯGS<+zTu=6U|l_4ڊξdl|~ʍʍ̾7e_5|nWlk6%ݐ/e2|nҾ7j_ڗ(ެ6d/5Չ|f¾1`B`__4:ӫ >,_(jj C!%BR ȡ|!x.ځ|^|8GRĎ{XGSY4Ia*CR҇LTN&vٽe`'rÚ.@!{3ۓ|O)˕VS4&5NS&sż.|'KTcQy8$SYJ؊1?s)g`.]RO̬vXYTg8Ivbj :|ԨN*$]w3z uT$MJA]w'TљYeFbΌD,"6!9=H*r)흧(? ^Ɏɯ ˕'2UAUa A1G(;7NoONȪհ2iVR:Ǭʆ>GH lm<`7wzrxY:ƙSSS[K)~]d(GK5$.YP#+h5EAR@(j;e\TRM1ʪR3&nsJidGRH~*OWPw%(BleˠYn=7(muSP['UC+9UCM; UB=9^\MQ~{e59_7xnmj 9T=g.}wMnKS=ʈtW<~NF*'șmsX 6<=S=TI)]O};w}w}&(UC%э}]#d[iהm'h':o}'iO!XRWMu^>O#SN=gQ-u{VVf9t[|På3; -5\T6=„`ۅ}#Q] YiꋈӑCkGP# 4ͨJe* /sC,8!Ѳ;ZXPRQE.8- !EOc+˞ Fp50&ۺ=0I[hE3d>][P1I&ul ǧ`1mA5550rt}EҬG5nmHz5MO\jRتM-pe5 MڲK}Ŷ:% lTwijA]iz]YI`|v j;=`t;ۨitTWGwW1t z]Eq MD}u]an%%eo\Y`L~\% v$drJXd2)ԡH('/ B ?ȿ{=NJs]%h2T21FQ. K+Lnt'")>0 pn۰~bJ찆pkɉ5V[w3r~JjRf^lx[XfAS̯'6j)ڊ*% o[*ktrS>O,n1[ڊ:Nvj+oNQ|X[=.E_ =r]54l21WWǨQBZ[Qk(4znԝZv_Ԕ5֘-TeYr!>TFa.).X=`l6=>/r8͉&jaL8A҃VVVPzГiL\n..s\zPr.K湮jJ$r\PQOS(S2z(0Ao;D=a 锪i?!}(!03.E /w]g^4L<\{8{sGo6NwYY3&wYp!+,!qP^hp%k0reeeg %آRTNEg/ 5A}/15Tĩ YA 0' )M&.&>o˝X FCK{\~òCF}|GߎjfL$iMA5:UiYcmZ3\4l/i1%PP٧hi m-m- ikKYkT5z+YY;g`+++(;QzKL=9S+|=Gs95t2R!T9de5 aQHQd?TqMlm1k cL1Ǩj=9n=$*?a_o}o+G5MMTKm*}_7(:bUwpu%j*}UmZeUUXjj KwKoY>0d"gwE R)ONNE=9xm6>ck9bG\TN3vt\]#k,1!leb oߦإ(^)29l6fJ'qѲG=ŠK}b 5Nh@,. Y'9gV~˔E2 ȯ fPQgzvy^\XEp5U8*cMA5F?9N=§ۑ-J:q &^s5$W"e kL׮ޗ쥷X-w]MKe[(lڇLZh!j'Vz/㨴Ͷg|Isy7YHmu_Y/ vs1A4 ź++5FhVakmXPAHG@ۍm])klߩxkdH(§ iw2"ܬ}%e"NNɩ(S=~pZZqcqP*IM@C {wtpQ^14FT=HfkOAO$b&ۊ0stVL'r]Nr`ZKTj:9~hUsTVu5Jn:-Ih^ٷMҥV%N5.^JoC*) W"58ȬCS=XOE 悔(}e=HSӗ55%ss=ޭ)a)};S{? }-̍ɱ=BA]"וu lO;q rTs!0TilMUMO5;AMYMo[i)nr,6\VӰjr,U*ID^%eeg|eer_roq[sx:IVhjQn:jm3b6o>lKIx:f^u@2WI/.o&{lUؘAMP].A/^:TM \],p\ ܫa#+)So|w˴PD餑q2.2'9eeeegџIYܢv%8NT觢oMٞ)*maڢB\P@ Q0w]NѼNLbA2)&C9Ej** HT&U;֕ԵN[(&yQZ]=S[ɚ[|fejYdmPw s-ʼe$>߶G`5 ( PXA͂(9s\%䳰;gsr)LR'''njGٙ(={GȊ؄mp-USJv4I+pIN 4F683u"p5Kcָ:IJy?˹`3QU$Wh⪞EEi-NP<%R emEwV^ ~umDiuP[ WkZ)u]xwJA4cޖ#Aj[MA4?j[-ԐTZ`Qicn̴hʖ[wJi9F_zKGlX.vAqM5Jm~{Lї! @glF++(Q;7B9Wb!?ߣNs?52VclSPLQrvR{xL򥄙$k QjARH{{{oUEvEk{j4VÖU>igv+UoYEjI\֞ړLIhMe+h)NhxC]Fj ik ZKҞ}GGjbU:NIWT봺U,ZEUpZtEnt(_>I6ol mVVvϣ+++(푱EJ%*Hzr\AG}Ϧ؋KW#wVQ3Le!qN$<ѸFa ʡ6N$s0qzƥWT<;g6Օ2JrYUt-ck%Vju쥏j'ژ\*U]|}E=5E|UK!Sf-vWKS$6M?cLAYMZ z-ʹjX fָtVe|/8[h-50(#1њMG,,'R(ix4si0+.Sڬj43dzW+tm?h(J'iu"r)G(,NKq[wPtՔL9Y (С d`{d*,[꫞N8$VW-.Lem+j%TQKO`vWNӖm FSPƛm0ȣ^NO5U)Y['[NZ?,tރgHm$]/"bA7HgePKA{]VkЍWK|\u\O53RsM\iő1xvyw6@m֣cshYEg (cORԉȧ'/ F[D/Mqp.ʭpdM)9KTniD4 (( XDǫ`R)ߺv퓾5K~O%ZccP*:f56ɭsR)u6Wxm&ݵeӨ(-(Sg& te)?46͜u^A5Mnt~MhGo4+ohaTجV-akOI%cY3Xez~EEM|Hʣ@Y6%D[F驪e5_NVv +>VvQESԪe"r)X] '8vx!\)W$ݚTJ31K:rkۉ +{r]nAtL*Ԏm.S^+MOl&J!qvJckT՚O(#ѐ*-G񢟥[oΡE Uje}Hmu\@q`@l@Mtn3Pjj[enݫD:EX/gJۮDꋍ[MNʨ=7UQaf:bOjJۓ.3)uUm+֢}D4MIsG}6dkE[$ݟNVv|Neeel咜S)NOZ-tj }'&,msst=25LH=2TF^[E& 1ha~SFSJN"90Zx+-PȮfCQiENcݱ;k+QGղiJ{6xlR4bv sPA Xh+.>+OS{VY.v/MwVםQh]jv4UTTB-S|YV AN_g.lڂII5˽+jϨԱ +E=-=YMe_A驩A]K4tV"u[m}u ] ylu&Z竸]TϤS8v&پ~AjxyxI9FBnJ9odTr= Ԏilh|ctmn=8n˺$O ؔWOi_N/!r1ui9ʪ@oOv i&`A7e+miZgjKƈS$!TjKeລ[UD$vy}ڶr( 9ȝ#D5H]S#[]R\6 I+X➤ONNZ@ \NvMΟP;aE4&A3"+cr9cd8樝N.JcMթ)m|jWg2Ŋ(.uwު\{=?hӕ7|4EɕI!Ovj.@]?8m%8Axv'ǥ'/h6٨ Y!r%ˢt4uV{|P4&j =ӓ)n4sse3IYYfolBEwҖơw"aiXd0hJCm6{[aҶd^F[d0ڪ%Lz:Z+] v8clzQD"zzrw 0A57ѝ2-$$ iuMx`LQ ' *g=+OaP0 D{VR#;"Q_-ƒ+W,ۢҴٟHju \.9$bVe^_nN+heZM--C]kTz8 Y^K} P9$B&ᆗjj9rN  n;&uOQXj>[3l^PY*G>ߠx{_g7T ~')]I%k=|WTJ .ZWRYh|v|4k=/sn>=k> (Jz!N*O/GbgV.=% |D6eG M5l9,%5ɅFS=Mw#ߤqHAHt!McZiØ5egY(ޜ#(G,]D bQL%.T/0? E.CvGq8MPOr64ʪwSK50>ܑw 㦫)$dly.+++JD|85C45IR(j Mc4WCuC-1i‡k2Ѐ\$i*J뢗 ::4e@M=5;{?zOjoB;Rx{aNҚ"5%dK W"%s+Qq#l .erYYY\Q+24.JdD%%L\J_șּWVIj0D,Dvh>-ƅEႻgwPR5,*ij|uKYzVA)iPbW*m;zyܒOY /]TYi(*A wf1ҿJ`j`|'RHҭU7nRtsT[Aqs@Xe W%s\Q ^6B'P[dOvT^1Gí |>z#=NgංGm !I_)?5hWv_4/M[D/ ބ_úg? 4 ^ez(!:kOӡobF,Z XRA葉:bcn\ּ!q%{,aqEai.F7LwSq7P* ƲQMU]qS;`誩73P.98UQx.6y*S\vs*aޮѺWC~hW8@RFYEgҼ>G#\Facl,z YݩA˒䲹.e#? 'LZDg( | N:%$)ǿqG d.Av(WvbAqpRR:RުQUJqOZՒVRa9P4&jzlW%!GyԵ)j Cj" 8SNl+-JU-z? `Y,߮4nsO4Ԗ My3UQpuUL*l-8۝4PJ׽*a)S:XVm=g[- 'S=ΏYK9ErYrZtt6hP7ME|ZH_EM]j3ۧY VQS^+lM*TײFx)p-457WSpAt5f9-u_o QE8}aaacaq l,,,aaavX  ǣl\aG2!" ABʹ{Q 8oB)Y\A\r`{v$,"HD _,o)3lo2D]N++nJgGGuE+#ZiC$)|qq$zbv$]Җ+nH3GJڊXTw|֋nk=A+KU^7JO0ku=rJ(WIEM;ԷˣaO;QSF2+B]ZPhlY^ZRˠO\6ǫ \B+\Bⰰ\V6ǣ acUBSL@mBlNwۑظ#{ SdmwdX@,ҚȊ;_ZeS]&S94 O|C-Tn+MⲪi5յuη@ Vy-CӦXjLBj2<BYݱ-WUjѦ2 8 R17[ҧg|.Q:7Zpm^jD$McCVP%cʪ7'y:I#Z+e  Ji;X]}XӏF6LSR(Mֻ-e`me˽> hMY8N^!Hdlmtmlj[Ljzg  o7*̺+]Ivs :{ruνTI9eLys\͍rgVqmlUZMM;]emZЩUeeeer\VVW%r\W",K䲲.K+YX;0)QAyg>ߣvGomʉ),jtzEŽgfWɶi:bZੀS>U3fSDjjk&K0SRѺAWT$S(VT[,o<8!LHpzOiqRIF-Φwedc|,mN>6#эQF\q/$n1iAqjEZIi r}yȕԴv?'QkjsE]pYQ֦j嫈۩P[\};E=VJ ezGl_ckVIu}V3iV7嬒KJѾ6 Gtz䢜6 -% {|}S{=Drv " l 6(Nx/v{6v)dlVU[7;}ŔM ,6ּ"Uԯ @5U7YzKPV %{M[)89,Nf_GHT;vY:5-wCQhQhӋ7 Gi.kE-~/\/P[5oNud6ئ1Md[îizSt["VW{:30Oһ/m찴F덑iI6^ zXXXN>7W,e5Ɏ,cbㅍ{c`;U?5-c QӾ,6r5 Ѻ/X!)CIQ?CSR]k̜崔mXM{}Ou}}-=TܴcAr؍ʊ[E%,J+~;}k%W m=:*jΏKTP+-{ڕMnqtZ s\ؔ eUCbn#kH/.#>XxY):ө.["qNɃzn\2prBW$p%Į]7..\ tܺN]'.r4y~Ÿ(ЄMl'3\w)f{v(q6;@{lGv9diiCxș5<3rF)nY]9bkUxYャor\U\TQ+R-` Nt U6?&U4T]Su VL 5R.k5%ՏGyӺ:]A촺O]n6)teGA4cKj4+5 _/vtk'N5f ˢs\=A945S0j }UmZaˆZ].kpb,^F Dn iN8]x)lmz!і pYwCS\w]v,qE:56>丌7g썱$V #)G2a:QO)cS[C5|n%[(k(kdM=^ˉdk0XQUg+-+0Q K=u@J-3[O|[g:E.SXSGOqtmzn[#Uv=Zj5² Yߩu&Xl%:+Ʉ$lxpMFWnӂz}t#sMVj Cn:z.\DL>>а(,.$&1p4SQÐQof. V8؞._[Pri88YeVVH]uEciX-QƠ (#QX> =;X-XguH #$D=#z1Tr|ਿG$=U=< JW/)i)( =M\9jk}%JQ>Ӫl3'eS_'Pڴe5;ԧE~H[5͔La[n֘ ګc5%=EH7w;m}ePu]Ig[SRiKeX۾Mf>u:6JH }<:mq{iZ$_VydG7쇿B*[-'lնM:f%$<;r.E7X")-A'1{.%V0e.yik^ ]kkjҹvk@;pnf샆8'q7$6;F{,}>NbN.cAw@fPL8{ S\Zl=Xk8nn:V9Y{oRcհ^4Ӭ։8l2ZYH(W[n&m):~[i|z^umQZt5NUjUk\T:Ju%FCrThtG[`PZS0)YG/^k 5mߩn;zQ4tճH%RՀ88qA%4a}%t\F]`h-qo4ZN[u=Wh.|%ؕ c~WkN0.7,4d4ev+\;qn~pawbk[5vl697m5ͭ}15sWx[hnJ*߭ݚerNji,%K\ rHX;Og"Ўyy6tgk8lGb _PCI].H} ]o}ۀ %ǹ8Q3l&HKWܲ@FCtmw N{JajƲmSyk*"mi:GQ |2v>YMOԆ|FzQ/V],QSQMmsGqEruZY`VEOz;e-?_dQy]mĥ3f[4˳.[.ZRڊۢ ]EQmp4]gjamŮ*͍M7 W`~ѿÛK_xp-v2(Jƌa(+m5G|qL<)}ק;.=T4K$F2HܗDz[9\܋WIYCN\pJh@ .@NjNpxDas_XX] 'a %5avǠ#1+jy(=r=PV+QeR fiwb@/uU+eIzAvĴ注Zߩ(P|e.0 Eg~eMnTN_]Aj)zBCYԫm%7KcIoz^t۩-ֱ4vmY\VQNqo[L RPk)mZ<+Q5US\qHnda3AaX y0zcr8Jo=x`No›/!$$ 瓂Tr9qCS(J^ t\X-%rErdz(i{8<joMc e֌E0+YYE5e1,3c2F8rx25G}1ezzښYm) uܩ]j<e}u-] nf_I`\.:uڒln§UQ۴궺eB UOi9x&MG5M,,I|ë(ۧ t7M;qV-Z+FTzv+ƪ0OJ]u>j:Z*}-auufuk-Rk'K9UUdRS-37.]j,l` :E)qF, i6֡ xj1SIYl#p8 O %00#ɀ,45 \Wp'Ic'0Kse~n`kۑJsE;V=;&/dvMS ZMQY*O&oPT|qASeSS{3vǣo,njdԑTZU] (NpVM[- #@Ʋ:J+S}Xu3*'T4VvߪUr~Z-Qގ5ކS ktz+2 !Q%ڷSV2y\IyT۪#6VYpk ïZ?OWZ)Y(Tj SH9Y3/^I$KM Ie VŲLˣobv߮rä^]O jMGYj.Wk{Ŷ%^[mKIYU[U_GSgת%}ûuug9ώh0t:u-k\ qG_'+@Wau 5ntÛ'0=0[94t8pǙa鮟4F@F-@,9qri\HCL9fFp%pFAxXJRda7 B>6 qd`'4%\4[#[ݪ$j!~%V> *4] emJmܯ 욞KGwvY5 u ,{fz ]>rԕ4:QbWU_7m[GyZWlvM]hՐSQGOٯzj姩ZRY6*Q)~sƛH3[4Us,JuS"Ӻo_,uڂJj4T5VM-_ڜ֩ͪ/ýG4k.5$s;昽ܾM8DjIz#.NCMjV;Fz^–rȧ0:N8nV:WYQWjWԌ/j 2#\c 98 \\! 0$XsS^'E#Z"P tNx#WE.b$斟t91f71Bk>'>Ǥ21V2Ǹl.ˈX qjŋlkkӍt]B+S-/-+/+*)WjAy Ay%%y/ ^ȼHאy ä_ |6eWe_|>E#jC7nrvtm]/aa  tZwz$xO>ht< asKNm*_6D_tw1K| G'+X=\A>kW&WuOX'x}ZGI5:WRƦw-3_:a45~䑄YmAؠ ;Sݔ Y+++;glH"Gu2S۵ROWIiY^%j[8uƺΗQeYVP>@핐.X `.-\5tغQKļ @1^F|:|6|2|.|.|*|*|&#_ bBJ6#kz6Þ*# ^FU^JE䞼גzc_ Yi zS7OȝIyGir tyNF;]QH| wIx;> 1`GK<#ॹS_sf_K^/|CNvQn俈.<=9sr̠(x]P]V.UYA>3V} eeegl핝,e$'+>mv#C)nOp#-(Eqf8SYE5tZgфa1SFW (܍ Kіtk#o|62_l#mב(בyא>F%"_y/ cF-^EȅZ^D/"^A qA S}RwBGǗv %Gݠw~A m6ߡ.یzXt1,.q 淜 $xMy倚H/$D;6F((Y{:cGoO}(}g3oߣG! 01@AQ"2aqP3#BCR$&S`b5r?c9'O=8*wW)<(P˯Zt}O?o!7Lrcs G^y:<|J9##yeOHEO(x玤!Btr;ɻ<tC51yPgӸv=MznoPj/CHta>Z֎A?;(#vr7yxnj˞n`S;Jӻ8QT(t*yCw~Q*wNwP DύXp%5 XҞO1C,y!Sٔ1yȼH]D( :m t;God;$rGn0O@%O,+2*T%JfSͯBTTS<7Qq>}7@NS7ĢQt"fYefYRJӺT*TsOyygq** .y3(.I;9Ly]熩RphwmNRjq8__=ɐӺ1;jc~VTe7$}U< JcN]% X*[:nrց{4SstE©{\_g4H aVϡ.toMo; پ .h&r)W̐~Ϲ_g}fuFm88eI\}<=@GgrÀoE̮ !|@"9Ongyw};0PQ`Xu|p8 N ,[H`E{KA*0@*xL`;Kk>;± r޵J~V3QjEvͥ aƮ _Dͫn6IñdڮUɕ\ vYݱ/(흫UՇRkZ;촠ԝUM܋ (byB~մj2_~ui_ȆۻaW-[U%p hYA\~( ,^3ShOf!'Sm4FTlgaV9=n=g̷;qqaU-çegj6\#*X,BĦX]OȮjb-4*VwlL|/`*JL!; ]/K/Ne :9S:ƚXYT;~!T8}qRub8)3L@ub8&(OѶxY8)~.M@nakaYK1jGSY8hؑ"86%j?X.g@+_r!{y {p>ΒЎj0ZB?ʅei{/ Kxz~ˁOpY \. W j5ppppZ-\WóxZhP`)7yym]Bvǐ;)G_8Au(e8MF8h\QL[]QڴLU+Sbxg +;=q=PpwdRΞ "~7 M݊+g͜`JR\zG^"ĦUPKJXй}U'kyn+1k3&oEimfa2O迍ضZ|DJ\Z *VY▗YЪ״(5}B>#F 3i,,oŧ+Љ̎?`i"P12> yD6s,3ZP?)F1*B Gx^{vV͖#GA*8{۱w koЭ{r?o~w*Zߛ5Y#Tɰ!eoL$u[.5tgx'Ш" oı5 ЕyqS9ә ͯYkk&p=+_ !ĝ2V׺uTuVըm'0= ㉓R~S3+k(a MñVٳD}%;2M9s߲LQNB`(UxtaW+X ԰0e4gOTlL Ū׭i~҅ˍ19+O}AU51W"rKi,uz Ŀ6,JgS )]#)~+E?vbKe`*^mU[kDF1:T~-FvebT7Xq%'9e2_כTv}$~}X k$c4Dؖ:GON^ y vij=4;t);v#>B<(wnCۜajp\a2>~ZfQ*;=ax VټV nwHN ދQ"GXV77<Uފ V!micZC@#ʷ[#wMٟc=%l+@թ?m-m4?\ j]RhͣSs?n`duk\${!1gV7X,{{eBmF <*[E\ MvĎcceħ@4TtHT6 Lntj5;1{abKJږĂ^Gx;^Z]6j_CwMv. -JeӰ_h\e~}ݎ!Ƨ~!;~z&@Zm%#W;Kf-GyUq)T?opi?e}aeANuUMNi=~#U1ZI@'7h(+*X!Z8!ao(T2R{Fޕn P7;q|t0R&Srsąsxojf-}rQE8UϚLPu؁S%؏tݕ &G-Ƭ/ '╶X'VRLs^e GCcMgOVöciU|%cuz)mէ-B#V gESfv6',=6BOI^E0?BKCpD;{{ZN[IpPz+ĮnZ:@vB[7Jͥ!L|PUbw9Pl,lSxЩ[AGe]ƫ\fxc!]`{( /El1gwn)]l-[lS-{r0;r4Dxs(qoEػh/l=Fu3Lg/ GDH-ʬvN曮8nOO٬E0P&N_u-s~ڭgf쿒vb|7QoL dz!B*^nxazͧ?!w7@YB@PT7+ T[mʀPP,dPVP@,d iFڃVUB .P-7)SDvR 9ېz~Ixq>:M2*3 +E!2:V\:* D UkѢ ^ڦ> JZyPѺ<+r]ўeG!O{t wesIM#-Llll|w.[aN\տqF؝;YMkD8?d5>5㑕w?Bb8w]~%A5%m.)qZ'R?UNĭp}\4mī>*t~wbl0e  _tٛxSU]FrCÕ:##ݹ*awt8AHLؼ!OXi mx-Ged-| 濔y՞x; O4ݘ6gWx}Q!Pzjُ]Q{^jߟul n/?UWgpљl!p՝H؞m9@P yTryWi莄n=CwdwH(shrG G+yT;r{/N+ڻLV:974u"G ;_~a',̀N-uJg9+mV:}uR ԁ1zX~W1ǚ#oTjbQMB6р9́GkiY*yc_Lvv! (PB5]\OZESqY=!܎9we[A^ s :C@s%m=4DM>f1f4y_=?yXgfLVl<7M N`8~jeOV=T_,c kkrs7UMŭlWg1~?eoWNVG ߗܦ 7B  :w~jNIPBք+WKpMRt9JĮ1:Dm()8JE<'j0?ZZR 6Def5*3 X?eHq;V4G_θh`qim?mv5rBI%\3#=;*kI@ͳÈծ_|;8-p1/ٶC7ŽHXVw~j{rʘ:೐D\+y #{s܇KNj,}(FFs<uqm]{#6Xײ2Fn_Ulhܪ\\Ѩ3>/6{ 6kuAQ\̝wX {1Бb}\cp^js1,cogpZ3q*4.k^>lUln0N WfonJ&yH;@m^SߐaҸl/ &D 0ɬq:.!Me0r71=s@(n=bk^CC.yrOP(&zFBhӑ[ޡS*#s{y2x] jTYY*Vp+ +t?D״[% 2񏋷Qp#,YJ#g4XVvLIsAE" *|!3o.l9?蛪섵!q%5ݹ*mqvZTY!AM('N߿trʯ9j%3iq>TqlQO_e_^T9dIy K }?e}nVS(MZ6SĒF=qGbshsF]^{ $l,M}Vٷ\G->V]5qopz,s76Tm/fUo/H|L?7 Jx} d JDTayQϓ dBoTU3VN+ fjmjQm2oܬF̐E#F m):#mLGQW,n+Ϧ j>}E 'Z-,&#**ὲd#f; 1}p"g? êU5HI&8khBr1eJ{)D{WXu0YZ40|]GJt nXO0 GRNxMR`kA!rS가0;*UUs6A> ZQׇ4Tּ TysS⧡*wʟ-=?J(9DW rq D@U i Y兲%J.Ns^D @Bsߐ, j6Cg蝵xîž_~]*xǿ+mǐ*e6\Aj+Vsx,& t+Pf*6$ߒ|ouiiIis ~оֽzOˤ鯰Tݲ-{k: DݰihJkNfU: qio;OڶQY谌DbyYU]˓C.U{S}"u|jec7qGҭ$>ϼh/W:dtݵ j5hm>_,@N?U<Ʉwk?Rd{&z'4);PQ&tǢ1db4ZP~ɠ&e2ֻ>pi+zoAxs>'6Ƕo N49O+Lj_+NjT햣Uy?eֶ*1vV=̪9kqoJ_9 ? *|"33,GeuJo 1+eieiZ:PJ}PdJ"@T˪tr?$,i:\KeR‹?uq[>ˢubLO%OVp;lXRU{> w}̜u{++>ڟ>nO oe:SFUM)Ԍ݄kksLub`ڷoUZ \ݤ3_?l#syE³^cϽ}[|&eVWT%WִnhB !H}PO W 'X&1}yucx%{*O 3v_HoX\:ѕ yǖw)U>_)vSqfơ F嵾R۴ERq20'8s?EWhvk~зѲ?+B~/zXNSrԔM:E>e 7˵66u<ܪ=*b i+ ͆V[5}pv2>BZaT&#_a쿇aMkuqi{5~ xnpbpln]S0\)͆Tfb}[ 5V177N$1)87D,+ّ'xS[Tҙ4(7eEgؼfY#V ٗ'7TL o!CuHr<~^Q$sJh:!OL}MZsCTWwS<:til} ʁ>7++$~_168pO[F۳cا&M:~(()V:teyR򫛛]_eSO FWۉV>s=c7mvS{\/wljլei1nAw䞥1 ݷçdvPX+!QM%ICw9G}_Mq"T+.fJ3!B4i0=-dzlBS.5AЦ]0,m[֩UiAB{;x;ې[~;tېn;af F`oQН9 9ϙANn@}&LUT(kqa[aykD0W[]שJwŝa#P&_w2 Cjѻ1d~J.q'9YMN 6͕>af*S8gZhjPO*+HĬ&;/|hODZz-nGk[Ns\>GX?%ʩYyɎmHNLVchD4l3'#m={eâmlW(kЬ~> V"xi}}V画\Pĉ aUڜVI=zJ;ak.=!*TF(r9THۊJvNAۘݹ-JtmVض^Ӗ5v ʵMvboH91J -iOf[BD/@Sl=ӟD֫VR kuU)lfQԡ`|aèV)3.]5+; ĪJ,0;*i}p+{fPnٻ- Gt lC{}U͞f&w=ե j:}ݮV(5X~6.>'?)cYZ\q)v֏c*]YbW.WƧ;K \J΀^зj0пXS~mW=S^47ʟ!h>#yJrg|3w=)e wO!^.%klGa ZE*X6 z1Sur2 pexli_X]Z5:C+[qhqʬygEPt83_D,z :5YvmZN z}:sAkBN4sK=U/ ns +A{coUC;o;w?UeIlxX6qR4qwv֮ jj¯OՐt?V82\-iwmf>b6/mV h虅S2`~Hv.pq4'Qp;tM1m`z6M>(ֿ<;*dqjN(M;esZ7! 'c^N.U, Ԝu 0S 3V`{q,)\՚PSE<СB 9h6)S)Ôy7UԠMw qw)9Hג1G~O]5Z6wex/_Tqt/'A7k1)U@C5k 9Z鏢}u6w{z)uʦ<>@UcWfcl(֭>D6ɟ(ߴOTF fޛvQXAMiX&b4,j^ }bj$Od67};'7fq sO}?̶Oik:lK1?趿|q\I氭i=tuiwX=Ӭ!#4o;|bx^ݯ9L|Wnw|ЪXvrޭG\u]CZ Lz,3jmN8xFy(уymx09%䓼{.$&TŌO;;y*R0jtͷvm XNɻo<Pka'qZWbFAoEC׏Dhv Zޛ߇R=eʨv-hC1ʆ/ecpNq8b/^B f f,V Z[Z{O]}՞-G {X']}OS -5ܺ56soOSee_nY]O/0lBpgcO7e;m qNتé e7cX׫Rc-bIaX}<6L"\+Ⅼcj *7AM7S!v@z%yehUՠ"Z(v|*.vH)5fYVo6GsHP&= So?טoATPL'JbN?%4&҉Z+)LQJoB.Wwu..)O1uu*NSЎMv`%,G,(Thrʓ'Bi  z%7G^wJw$( &2pB9IG9oG>첟sMIXvV죖y N!]1B iT@(H*T(aT;BF*9涬]J{"wjoӭ*w5ٷ9teO<~ZrJcfN;7zn< ҁ S]qo>hu!FӐy2GB7J;|;(瞱 a59?>Bw9G1ǠyNn^ۏ_ENVB&>wϓ<c>TrЈu(J% }fDGZ9!GNz1z nr|}һǚ7Pw)<GTYtaBaBǙۧoYe!e+)PVS*9"TBכ^HHQϧ,(ž*yQHz ʲejb3pY \/j^/6x`2xb1^᜼;r^(9p\\. r4o\7,\7 ,Y 听e+)PTFa4(EBGPGyW.\".\' rp܋e* tǒzS7¨H,CPBŽn (ח^MT%J*TR,Ve) Ve!HR´_ ,YX1ebՑ#W bpX \. b5xv ᚼ0^/ uᇺ^{ ^2!nycd+r{[^TWUf>y,^n{P-]>Y,KW^,e{+{؏qvβE[*[hվVźcEd>^˹ 򭗾ز_]}űs]E^+c_[{;gEWR䇛Ӿ~Kyke^+٬}~7+xnk&>WN=:/ɬH~;H}G,D<]~OYaw,>< [DxF=5Y1~~]{wܿR?dC%Ye켞_^,Ye֯w,܏l~4"8CqN!8Ⳋ+8VqgFjfQ#Pj/>g2.g3ll[,Yl[-lӲ-Z-Q#{m#om^Ȳ8ӉEi|m_SV=3Zk. s+O_^A oObzK,EgYTVH=zG,heYY{^Ŕ=?>慔{l~d1=%m{9)(Hi4i4M%J(J4i4#B4 &\4M&ES)s9g=^ǚQosŒMA!QEneb؄|ڽ ѵ䈪BYeb=r=?bIyӾ(Bʆ9Ym #ugEtjYe^V^wӟrAs+*(hhPpppdpdpp8hDM4`i}m#Go /gFxfE,L_Nbl~P*F3F papWpdpfpfppphVM{b,wܻ֭yVKJ+;/+FYԍFQj53S-66IͱBكFjfjfQj5FQYej,yR4D>`E%t=C]76Y?NY=w>IŕI iƙYfSEIR4άwg2V()d[aj#>3~m^mkr.ryHDjwEhLf/'cp0n?RbM 1$U"2q8#+1?rR촢JI. &+Sb}]/Cն~5dw!9PԬ&;GȢM?cMCCcc1a3 !FP&ã#E=K.[e?ƶWbo{omeyVv^K:Ft\erBfp5Dz~˅P5@k . q5ĸ\I83TjQ5\MqCGmԷay,΍%lM^K;ʊy˶a~ԢW[Vۇv[Y-}f8ö槚K~7툶_nlDEbz s5رUs8'4ч8B$ʼn,H5a3cRٮ-'$bLX*8x_3^KQĉjFN2LS"r,eqR*Bu4K $brغXKnE=-}} C^_=ŹfXv}7}E{lв7ڈwQ><[+et,}4Yf}J}%Aw'vGH­DMo‰ÉO()L\nN#TLRh&8.tj{o-x[|8ڳ}_}Gܟ 5ً/l;n_At_={.-w%ߣLᑎ<:D`ʹV:Т$[|Kv/=}j܆=8a88Ql.[w'ߣqҍ_m)s,>$SCJLjQij\ZE\$Z]^6jeS5B{+ص v7=^~FuHkJ"T8Fe{olKO}O7ҾJƅb4!pHҨi籒Lt{ $--[etemMqhypK!6J6pS' ,:-Y7omB\'qE\rXr~?jYPA^H/%hBl~my>t)F;Enzy"K9bw:|;(HVr)Q>DҶAY̽\9QDʲKۤY!n?Ċ(dE Kc SS<>ΌHt\M0qa>L96sy-4"]Ug[]_5:M%.tPK5!l]?N]'^r05W"ut?W#;ى9kc`ڴSCd\ĩ%e=e}Vh[_Y,л~}gy1dIm{wsű)d_>Bx7#%w%}ɾg3B%HU%WE$$>S|.5c\V(#M?١л"F_'\e-&(%{'mY-/[} ۧ]vR5Se3K9{Q YbzEvؿeQJQ۰+}g3\r2#ȋQfebn YۗU :[,[y8@qf $,) 9"~]GV8+>HS) 6KծCGW3E+ʝذT VrR( Kbl/ك.&KeE%2$ܾ8 EE"9<5#d%}hqh}[Bgts+"Qĥ;L|b]sU%eyc3-C￳/:(y5e!c.1>lM*g%Lrc ۢJѵOW-V(h[(]ɻhެr>2Y_b ]ЎY.I}E{0,}>tٽ^ղ;QcHyػY|u{Vo{޻=jxm٭jk_O7],2!|Fʨ9j\8o;d.eKQYAǚWs'el]b\+.Up-ˣYw|wQt{{>[#%c9<O8}&%Vǐhد#K>IE)b%B٣UԡVo9`[=;Aft[qXkeo⿓b"ھ 08Q1!Dds/gTŒgOJ,uʉqd T$ēYq٫yާ~tmS8wSġ,RMSβt9Rz(GnlIvZ}68hxlP#TE4SZ9|-cjK1eG㦑-1Z"Ǘ3KEf<6,ѱ?b LK,s b.Yv,.F %hԋL(٬b1gq5.qkee\Ejk~?GV}R+*+:(*]iawJc.r}GQH]\THD֙ltDr,5qq>>!Ǘ<~65V x_gf}#pp‘‘ÑÑF~™R4#D4ci_g~f}Hh!I|[c7˥hyǰ=<+OieX!88<1#;87q01/811yS6 gqq>E=b{yaCK4'H#q1u r;XQ'lO%L~/QEQEQH^VYlLYG+;l/rj9?" v.c\G!4jæi2D"x,5ɢ$"%cGK~(IE5}UYPeW˘]Vx.g!% etq#bzM PMJlhB =EWn$myGr<~3qŋ,H}3R,$QEQ4]k.͔Es/ceB,𻗓8F${ AѨsRG"HRd`GL#c_o/]gP⯓3O)v<_FE2\&'&*程1e s?l#3~D[SE%E0ܾ i] $ Jf\<qZZFuVCKNrу%ljUIz:4ʐ5|6͘^Ո}mY#b;p_ pݨ/AC+ʶ|J[0K64N!/^ $)Dr!~HrX$]çkmFpp~+yL#aJ_g$'/tƺ"V7/mm[PEr+*l9T)d4'F-Ɔr"zLDJ\iipP0dգ"Vq!k5_Il=km[%*D*&QDdd(! O r#& 8ynFb3\{$yxZ',/3 Op>4O^]4S4ACޥ+$)OH22Ib"#D9Q>faƲQ/ fTU%uiQ?Gp-yXVW?gggy\AlC_b}Opq>'8S83838S8s8S8S8XG Lb} C^gyiZG_ʯ/ʯ888g CLJEu{k:(_2/,$,hHQ<6ؼ ͊&4 ֑;YYAOZ}&/q1jTabOI~6Œڑ}|7kL!m|ŝVTVh[{3_W}[>Yx#܄n(L*Yv3^; rFĎjF9QEVQ1;];ΎMyRD,lnh\xs;%*6`M~GpܙjHS\Q1Y]K/`ez^Օe,xX (h(l>GvEPl,U2mP/g{P_F/+yPGj?ǾLY4i"Xzâ0(ElB]{#:5lY{l2(YQƆ4>})*#*{םY^;^4%xHiʳR];VPU{g<,R_]~t/^YJ7FXU BYЄ^[E &8Jɧ.+/mYc{SuΊ/+,z8RcAʊI][m<Pժ\FZlY4WNY]y^/މF4YY'sQ߶~+;/5C+*yVۭOrpFss Cںgȼ{YDJ^V5Fu5ЭVύt˱{k+ʊK{'|QVUnyW-'[;uY& Լ !:#=[B/u^,$>q$~qm[lljeYe$\J3=)3Yٮ_b,6'F/!1>!q5!xyyyyy_Wybp3yf8_gaH}X}H}f}jFZ/u^I "SI\F2'K;,r9G#NG#)QЍAh43AL#.e>XfOq1N&!G|Cyg8glY0XoɪXYYh.C0fӑ+~$ƛP Z-҉0nn}Yحrk[% 7r]f}JA5eͰZtW 2:6^V ,Mڐ.-_8.7ր9[q9X_W2,ϵ_ +AjuZy0uu{\."]Ҳ]]y&C\ۜn3X_{_E7o5r1X>RˆK 皶&D_4MXz9,9-@+%e՚/Ըaĵލ -C̸Re5ŎPerF/R_ֆ {Ea&KN]YUi*\oY\V+0ńi:UЈQ .KZVJ++,~Qϡer G@czB⽆jt.eYc-+;]2,TJN7< Vݷگ|~Of]5K y_ղh]sVFlt#@9. 7us𰫗#"(> F64돤?-w5.}\Wasz4X7v.Xq®dўEl*\bZOr&UX1睺 Ns\#{ZF Vf 6M bshѢ :9-mՀ"ebto@w+^ōX"]:jkɢµ\jti}Q{}X]͌܇fwa\6[%t"ӠY{ʾJµp9=:.V'4t+GXo˘\'NLfrMصN ƶ9"L8 luX|[pysN?%丹kwng&ՠOԬG0\NjYZEe{,XU\U#MrBi>}HM \;Vs)*s -F 72}jwݿ"􋞹˜W8JZ-4\\Y1s.b,1sW5hZ{VQֲaYsVM+`\3ܽz!^{/F g+śJ|uץze=bj~õ#?UGⲭoY,V2جգOs^{UY{Yҹ~jbOQzU=HzP.2&Վ,o#ۅ`_rVYkgҵYZ܃ 8S.o`n Hiwm\@.6G|9;]~Fw.^^&f94זjדN_E5Zy:+-G';UȮry[o]zBd\rY16g4nsb -ؑ;?zOzrWչt/rּXU܅¿r<]+36U?ؔwTv ?Fg+6Z 5]s`2ih 7^\G.&kU.~^|ҴZr^Z,k_s\{E ٬#䷑r\[.j[#ڮV+!4F0!"\X,C \?B]ou˗?ۗ1d|tsזܚik:y.3jN;UO ;2G/#O+"Y+YfxGZȆ"DKHQcd$&`,;rތ[sq@ڃM,C._ihf}]dZǥ8;XA˥fVgkKYsVY,+ܗy tbVe;?4Xf럢ܱɻV76[ۆ%H__V3˗h3嵖,~^_ ϕ8ͽ9up/nMVNYW.\%qZvPx 4tsJN.g=E>u4J |n`~ m&|i6};lXKXCEڲ.UU~\V8lNJ{:}IѐA&~I*tV~k.VƄpRe']. Ctr+YYih#eNO޹ڳzFWozA^Yza^YzvWgzVe\\Zmן&˒-V_ 㜷q%Śs^m`.;ZYdV.Nwr\hdy1'WgSNE`uMoQV558۫Av"oALlpF_ Ȑ|]|YepkZ.KK ْTZ7MLcsz!(d+? >r ?Wf,.l+[CG|'ڟ Aq~ lοmgT?'f=Mߊi[\"&=REA)R/cս%#rHw࿑࿑ԟ/h_/o__g_g_;gEQ#>GP{>#6_o۟$!U;_ֿ_1R/*ݴ޿OWTwmiGuq2ʯngYmm?_ ;^i{HwҴڲ׳WOpxًj?֡? ~;/ ?YmCb? m ?dJ}w3rSj7&-gzm[~yҦ }&;_4$pkǃ>,,Z^M|M_jUs_7NN'#%ǫ"@LgUp]~/.KdurW6ݷK">rY ?\¸jd+iT_KuUw,]3w?I~tlm' ^EȳfzH_~՘uF囖FdVvo&=j˜0jnKFgz;4i`  'E.+||g|b1oV:-?JӐ8dF! vq'ږ _ޢ+ G\sW!eymu&G^LՕY@aP`[Ԇ8b-)ڎC4,6~ȓoβӓV@*^Qɢb.jm. \B6GܭRo[[In.mJ0u*|FueOOqm<#-edɯ&k%^Y+b&Kz13[aui^4RuYMwϺ:a+6/n\ O#O#UוDy/ q70ΤymEUXΒZyAa$d'M&F;t-k[{rnО6ͱJ*_N8\u-$. w*9ŧցħ҆WBTWbnz&sC޲3Feejj7|ٚTKk8Ѣq1FgN L172.RzdenyBD0$"5C^װ˫NE:s[M#i@x8[OjkvKOƲٍ;˱?M5L}Ee0ť&{{,xp{] g=g'%61ɂ}rw<; } 2*V=c*ZrFG.xs>#]0dt-WM QbiT6xrF&;J8< Dn;Z Tu z:w퍆p zMM6yLݻup*] u'7'6UAO1Y rS4owᅮr%xnW+Y92V 93ԮbpeR-Nb:-z|<|y5דEwC;NQݹ̾]R>ϚxJ$'' Ypb+4~0։0I#|=:Z0qC!{_ܣ Sxnd:}iW۝ ̆q%[;h6z-^g6f +mhevW˦ޱwԦ7R9]&yt*x+g \10K2QaCm%`nnlW+f٠-]{<G&aءu} 5jVЎ6]Ѽ-k{TuMtNކYZt&K:%ѣp!=8Ρ7+l{Ȏ9Dmp~D{颈>x1>,lLbǼ^% G>7Xד^K_.L*q5z>L},;zsXpUB|y3Z][jҳF 0MNxr+^4LӝK>ՈhVV_ rUɧ!鯛gO(wz0{k ۴?^ >} VWSD_R1h7oR`o M3u?Tj| x.f=S{jawQv>Rs6σX!6^uz8 Hln>70};Ri{!wvYؤxd>'Tø~dtnڝ]ek("ḓp/Ntm+S1فذSlǤ(.'ӿh8cc}ӺV7 ˷v|] BC,-#t5j? _.۔ )ci{[!;h;kŻt[ik,ל vV ~U{rye~[㙆VS7o|?0r(ZBiϓ=+zzYbkfb܂ֻk*Qsϭ`tq(h ?%Xq8۷kɪ+NMrh;~&_R6jW{W2atx,r/ŝl6򜵑ˣ_ԼU4sLdྈҳdF[څLP5}ڌAq N1?7.٧/Sa=0'4Ř鿨!$/h{|9jtnv8|صJ90T; `"ªF0\gra~-vvT-Щwc"7EEDAZh!06hf#瑱c-0=~PDӍq=a.mE#hyZLҝ4Ǽl2h p|S=>FD],' pkH$[4.p9em|RM#q[FQC+ffG uS 4#_Yz?/m`k;{Ro DnN݆H1σś%gM|Cq>51BG텮Լ 6o7Nݳ6˹m PDk0j-RqśnEpțHap˞5lT6 j0nfR130̯C1:PyN dْrXld彖/+_&K&/|W Ks&z;. p]7{"X ~ .i)p۷+O/E&|iɧ&\,@Tw|8!0úT%?8Zt#[x-|>`sy6}F9pgTd2>90vfW4-,Eҧ&PX-3iC@lo ldok 0ePTJv>*w&K-/i4O4H7pkqi|N}67?zj<5.l8xnutW<9x:օq . eoZ L67<֥eTR} {}Z=:"Fۢe=QV8p8C\uQ38sges.BbpPTT8 dx%dpGF|*oo;oJ#42;[n!W VBKt*j|- ueZ3![Tb%zeir౛TdSK{j645a-uF0!cu 4 ,qn7 inv3@[|YiةʹD\1i^5\\e$npRgy$a-4RZ7V M$]<|"9.0e˷.k.Gߔ;urBoH >^n#bK ]|m!n[ HSnM~%h]\{)~%\n kݦR ?|Mz/8 #ֈڰb6DYrYXYX-Vk5g=I qXGmde_ FRuk-u\tuZy'NA#m#MX|:j=1gz dd62& :hG[m.=C*yy2wLqZ+V7rWRϗ_#NLϓ%X >ň;e֞Pq_L7M$m-tw G׉HCr_Տe_ȿ|޺~ gDxP3අ4FNqy] 9$#IGk>k xnD_+0-U&N1s@XDbdqKR}RS x[UgN|f9b[>Tk]WUH7BoSM!suiw8볤ߥPlgI`4,=1J]w8TOC,-nGjjx\45Aɓgye[ђ=GyYM|.y4ܳ*(.;+5]?Xruk/7=˵!ҽ vN E܋z~ O=Z:$ v[wf6If(H7x{kidu"[;[%A7ݷK[D[R.-^K=XM:B ntu*v#h6L20mu7T3RYJj*or[D$ e;r6T!Xcs6܁|@LǁA#֟bťE<է&]J[İ ~V:+^Y\7ɫ^зbV-g&5F%msurV^NF1˟&gӗ_$#"O[qP{qsZYr˯ KHbh9'h%;@ ݢYuB:zVo~fGNY~&rYk~{uRIQ)vct1û;&k,Nu;\{h. @{{FC6&c#.X[($Hb=IJ9 J*Y~=r=_zg:L.x't`|l:]CQO)y]3E3DXQpl|+URNӜ>a{)[UXg,/wg;i;^U+ kN+f%cE\_by2\udvӆ 9+GP4W]DgvZ2|vʣqHg݀alDy0ֹ_LQ-+"<`hrFZsT,{8]1o/^S˧/Ȯru]F8dvqNFƻXiڡxlw .,lxweEia7 g~ޥ/Ldy#{SzWFhH3]6"zQ?xub|ѿs ƈld>i7Hk#pn,w)kcp8b"˧ܳ NLs6I7/_ -2G75ee\+!yk'<]+1}s6](Ikn9QlO tEYmG[صe[ .~=4nX 5pnI۝5->۰5ZL K=ΐJ5Q8&8^tyWȭ$?FHdxj;ѥ6y<↟hG+M b:/';[3Nql]yҵʎf3Cfu,)%KxR<.a"=9TR1F]؅_ɺsL]n+i"Fa>GL^XcxnMV-VG;|`==KD Uȭ95Ҳ亳8& ^b 8<6hS'<1Om:ȷ#"b.iTQtnsFmӫ%|1յos{g՝졖X &>&5[>7I$mrW)s !_ʟo k?Pt֥ۻCO?aG_IC>Ѕ⒐==.TT4GQ;x5vsݦxCn]<Ҷ [>K.4 77uM1 ۼ6>)Gq?r54"k=J_cv猹W{Cl) ٗeDo?E(_ֻ n{V2۵9ɧ.^E3<%f ){x_qgNi:՛DyrǕfj0g{"%ϑdͧ'Q_neU@Y 4ػ+n* yMGqL/RRj6J7#}KŇ8$[!hmZ8801WYGiIQ/fXb1lZe{wK#uX-֚lm`:ĭB2R,58oԝO!N4m۷F0}%s}n61C| >Zړ.!|Ձ̑F9I1vK ldZ@uٛCZUŽ:!429o&.=VLsk62xGbkyN>IHM1ղ-]HR@m5TtI7MC fH P-;XkD$ BlFMwQY w9S{w^+q֮_֜{:˗.^NYǗXl̫67=n"-ڰ,iYzw;'|0\t'!kɒ:g&DڇRHpldrNzr'?`FzHF8seU$- `#dßh 5TDZ2K]{A e(?gӺ't[t3u ;Sb[^ͥA+%nK4Q7V7@A=@?,tʩ渭Щ{p޶'5k[KNp]TzuVH?-E, /ճENvpHV~E+o⍦Y/-c kڙi0E4Z?qQhL1w3{槣Op0P֓ث^hh'huG@m5#ڔF t_[1,s^Z*JTI3o%]jEUmhMt]':fwE>u 3XpOVfѲ`MV >d䤬_?o)'V[ZH~)21+@ilMK*Ͽ;Eh3ߓ>J]nۤ-Q[)ۉF_&vl][vGEV n+h[q(Y)zޟZSLB򥯔y[;/3N.;{iu+K$v?͑Mr7.4t+yFY=>m/nZt@yӟZ6 eDNeK 1eseo荣?\;屟5aċLCԲ=NY,s*ܗO&w+2yzEmD/n |7YqtF5^K\EdMc\R;ˬu])B.%{7>̔/d4b8HzoQEK$Ndn9[<= z2Q3 #$ط&./F/a%Tm*YIwvpsElbF1Fq[P6tUnfٮ%nV6qǾ6oVogl|E UIe*iEfˁzevмbaw6R mmW+!rV8 %MVXeP vxjv KA*k-O6);/aͲvׅ`Mg0Yہ8XT45M# W_?ZxZ\.".x74ޅ]LH;ZsUe3}8@[UL,uD1ҹ͊sz[{DҶJTsY-E"c (<'j%3 qG)ޥvQ<""ɄNL=A%CA<~K)|x[kM[|uymg+ON $E^UoˆӐ]bu yzc9#wnEXY'DK@k8.u 9eɩZ8z\h:Gd5OcepCRk s4Wϥ 3E[dn>fl+7# eC7Kf(ò3vAN[DMdIqz+U{K ݏXh`c-ѣf3.@7CC(#_Q>l&:-,97nHi%+s̈ui.;\b1xy s7zjI $L 0]ڨ[JqD {zlSӵM{bv^gB4GԿ+^(?>j+h!x6Q<zPe)daToZ{dl.S{{ ~jNcKͽ)6⑱qU_)(k57jfJ\#v,mndF 9h/Zhm[g/3bA rTE3 u[֦`?#\bc{RǎsԜ QYkuregZӐqb˹|dLe#59p>,7s+ɯmG22$Ҟ߈{ b.k>G 5->сxҌ?U:]#)rzw<}Zv @Î]%ɫ~+ .-IޯUSոL-Ӎ[e7"߶oਞ?b7|dX=3wfa ͭ18\Bߋްl[>J6<+|=w;RM&mn]o`'2)16;N _3\tfU>רRLoq]:-3f{pLf(Ml7عKFґ,Z;;nl\ 8a Y~Ҫ6m $8{P}yޱCOb ea<\,_j t~l?U7 =Dxص;TW5Cq|} -` fxjeqQÙ6JccKZw ezEB6Ѵ}~JFHj p骧iK"2dMŐNa J":mXOtqhߢ|q֎!һNx#Y" eڍ2p :}z8MCՒ--а:ζz#p,rY+rɟPr;ΗGJ.7Z_ܶTӷi*kuM*:ZGpQ;>+Խ^F˗  $zJf]hi`uT&Xr ۱GTڸȚ& 1媩lVv~ٷ=9*2p .1._TlYO=ό8rwla9іd[> Q7mTK؝\,,3NgP@eǐ9:FM&#OܣڲN oUIIGBѽ} ͺRʹe&l)doln~Ɍ!n:ˣZ8^#՛jڳrr[̪z9p4n]ZZ@puy<]du,}z@vޫ8&i\GiBWŀ!:Va`lF 'ܺ.,`Y|H4'Qe~ǐ-*iFP̽{R@d0.y{Kx[噶EK$~ZTumXP0]b\9窲O3O$]"o[| rs ;sK^P䕁`hے/khڳYMy2%\u8m[[;  {d wKT@aAŒjnmX.?g%޹ܗ) OR7PssyU#^C:f8;["ZX'VصO|o`?֫2R.ȶHH:$!om}1[{&.mEw9i{Zؔ2(-?=_K]Y?;ʒF"nh`-\,LvH:Xoɋܱ2պ OʸqdpSeqkt6Ca6ZyZdY˪ȭV,Rx[{lss3 .j{ x,p5$o)Oua@$xxc^6ˁssS{\MnZXnn(]˜F|+5C_*gafWIJnqkj˓UbJY[ #,Ӂt ^E:UQ,gӍnK,JǑ$n~K\19ﲒ gSOFWELܰӣY^Ò7[/ώ)*+*OgFgUV CPߵVziDwF#VuDnk/irX: m]mbk2loH+cBGMmTWJ٘]#49z}i~*Zi-&>\9l8t[bx]* Ƿ#uQI+%^MߵG86L/`|U%MmcOmGҺF79;3s'UӼT ⽲VEN%DD5M:[Q:/-e1EC,fsj:>&&~8mS'`mpKRj1v|(2)&qn\,RK@AiŬpWsUfuJsVṇ|c"ر~:5NLt,i+_+/*(><R?*lH:vN jlFflFP>˸t'myw?,j-U(dzJYiWA9ΰ1ZY`.*ygzr6NeTSm7A:Y..RZkݍ8˳otY%sMiwF^h goS,d_.6Û'{{*ڸ7lSIv衦et4n"m)K_} Q3RPP9?c{2nje~&Vt*mWP,\èR%`̷>M+Kas,gޝ]Z:l>K|!x [M>0 Tb ŗmGy)i{GXcxvl,[$?0'lׇ]VN"#}ꡑU3ZlmL0x=:FiϠ/с?KU|*H)n8Ij*ڦCyňeW+ixզpu[e1P. UTb79Hj/7*z=Ѷ79mMZl#Q,lucg,APV[K=+GuoJ٭dK [oU)s梑ur6dl,t9(CYOmKn}EEɅo[xp0 znyN%!/᧑nKrK_7ba@JùQge\1px'?ybk<܍ֈg{F!ImA$dj1ӂ#doqG<}p;Chɵ~ӒJc*]~=1X_أuq$[:hI$08tZG**GGڵZ(ceYԲ[yu<'Uqdy.ڮuLu]qOy\V|=58֍w Oc䞲F;owZ7Q[nMy5˔Uܮd.8c8|=K =6:fݾ;D㲢h۬S7Ъelt,/ue䑝G 0x3;M.!|Ooq剛rTup^8Yɦz4rԲ΃GM:VzgMOH; e*VKOmN!ޡ;7nҊ@k5ܗ\Kfz \2Da\=(k$[ˇUЍ'K5.xԂ]z"|ZW;f; [3uY yf|{>& )cy׫?m>+m9ԲfɒiL)ڲqPTd@Kg1)^cC% >ӦHm. _ڪv6uDD~[%f*mm֨)$6WO87Ur9Ӟ|ys0:b!Jb7I6v)ef@ع=khD%9ָx|\cnukb]ńv ⨙A|M"Zte&5snZb Bn=ت\CSaVZ8HQxA8s߲EQh)moGiYݶhDAoE7Pj+LOt-7t yũ9g6Vn \(v#.U6͕F,Za7ߋCҫᦅ/֠A޸\ w9n+_Ԫh70qIN 8Emd<T͈r+lnvtDIVfmXd ۱nm+@>.\#C=,7V|y&tNçZeI[Òtm2, vˬv,{Iw3֩V': b~^g7 3oܤl_Ж):>6=&<{ߧ~L#rUQTl=nGގْ Dmܹ*p}A ь?ϣXhd6}F sz%IQы3(GlA^'gA2Ά_^c>*$qݛQzhPۑ[[;[C-ݤ(gn'RRֹw^Gn}jjK6T_܄Tb6"+=xS tc!ڼk6Ybg7O{|+8DmhNLZbl;OjeBZ G =E{n.m3FO \X[=+4ri6F)6 y˚,-Qm<-8۠ *8_em)ņVG|*iQm 㬳Aw=ogH2-Qm.oc#!yt2PmW6۰UTx݅a#5"Vkl-֣1Wŀiܦڲlw:hXen& wH~~en#@)ocHn]9]%9lJXX`n"~ 6s {CYM|upzכ>i,:7 h!o,yxGB 9[5Qu*L9[C]UV1`=ƦsKa0h+^kSG+Č_{K(~7=S]&,KB˔ r%c#>Td۞XdG0Q{e*wVz!_d~L'ufQtb'utɒR~N`߯"T}f<_QeOASl=bvLLMlj0ppO"PǍ. 꺴чa0vʜ1ذ-)e3埭E7ac 4ueL"22NlT&YHA*y_ f/p6$aitqkc|RRl-$*40gXgH-ù Cۛ㧨w’I}1<]nLo~,NwV>GGGZt[5nQOKdq/du_MTS=؛qتlh2=^MysK'y樆o#!C{T~V\<+%+œJ3ɪ^Qe9b1X p!2' :FԂ9|΃Q7[.)dlTEm;w>ev骪1VX| vK8#%H%+*}TaƁanZד?"Ml4S&Hңk[ftާk[.>~+x0ɯJ;,#oudDSZӵ6"Km՛#%. {V"/t gk:2>J#T^j4[?Y9'0AԼ_Mx8F:pt 6Ӌ"mk(WrPqȪJTo%5Qu)ѽljgIftqiҧ΅I1$td=/t`m܋g9o#=gibtW± UPHb5X;ȇ9a\;6f}٫c37jzL0𰏧Ey ?JڴOxp' . ̸+޼տ g3Jߤ¬\jZW9j̭VU&_#UZW9s9kVyIXWؕ4?rffjN})2Է\Oo}A*Nפ;BM#XOShv}+!ͱ\է{<(:sjՊ7۱UptV>W2qX5ZᾶФf޺W `iFt<48z o&]0J۴O g~ ɾM cj:,܇ ̓}OˆVl|׋㎤YFou|Ĵ'HXW-VvSF7Գ,]VUfUOx^sgӻD~8/ߪq2 ̿W/[2fU?\4?%eWG\ڿg-oUUUUUUnua~ UQt'P2+=7[S'd#'O,)~4ްWZ |tCࡄwD mlŤo>XM~"=Y\w ;HaxuAC~-Nc6uo݀JNܚ-|Я~CdzIe9d.su~MjaÌuo6ϡeɪaMW|r$`0 ֊jwX\f#mCxXS]=4 FS|4<#vsBlqX顴,˽(K Snk4ozchJ!oFu`]wR1p>-)~[!>^a]^]N 2u@!y`?8!`&$|fА \Jk7[55] tm>pBz&%ikX k}FKun:'PAݚsCA^l;W9wkahü_/b б ϥXyܷV}-ȺJ^ifGܿ)m'|nҳe$S5L24s\m^jvUfrzVVDrim98nV|> cɘ,Z+ ,&|r#'} ba_#u:yϱVMi\;ޝ vSD';1csGذE)] k/yyh(1~Vҝuoxr9ݝC4ĸlK"t=oaH.tln=DiJ]L5PgHs;9ꁈ@ذPLorG\n|KJGO.^/O OA!.V,W9!ZwTV|kzקe9^fUgOU}N_*L;^p:w/38˝g`wz =W2O7+~^W ҅^^o^YzV}e[R7/H߬+~7/LϬ3>7ڽ(^ܲJyzw~̮)Z֒,OCYPTW9=/B=r~w̽4UT_TKY>cdy ˑWM˓N[ߗ5lۗ&yw˗&- y {Qt d&FGk l?t^p9[T`pǡYQou:M^7"8[pNdxd_L}J9Ll\w09cͮt kˏ&=2 w5TLh 7gk݄sg{J>+kn掚~İX y`nG ~+ڼR6ҜAGGx]4QM.XF.o̕k-KS^Yzwu˗^}@Ϩ'Z3/G\q}UQ}U"EWC^//-#Ϩ#~^~/??N_Bj?lVu~Ԭ$vbEjC6_s#tZ\fVڸsY{*-nL;;٢͙.{ՙS[;3-ߊ=HwpV"9j.co^~p$d4mň~&eп8.fnc~*ۗ|cpgοJ8{gܽ zxpSvڐS`keLkjxt3v(~Ŝ 浌H/e< pyK]E%k/)RaFa KŗgR2K҃o^dn}}Goq~w[, 毛Z`209UTGGXCAй>1 tN,D-}Eb 0X\Ց_Ea%NL~XǩlX{\n"gGc {"qn ;5`w1{7zg8TC0p٧Mwx gd#z;^&u=lJG:ؙPH7p7+}ȺM: D:,l7 }7ψcP&=3s[Z*Z.onH96U{*ëa7k-TӰu5}s/<7Aԝ4;2gZЎ,; Gi[A9̒uLUPqc9rגXzµV9r~-V|~ex\E8rXZ=hjߵp˛d nC4s=nφ'_+S<QbٛѷE,S66jqr^lKc,6ZG]duPD-&ڴᦇ[|fk\R';Sd!wjmywvzviA0eeE4p} 8zS@vSS1 iċ[L^շWjY_8Z +gҝrh:7=2&czBwc71?`j" eD³+5VadVB˯Ȯ&K+Xy{UaD+bkXYpϹvtYjFַKD)ղEy&.^=Ouef@oԝët)lV!gvKRٹa 'FK7ɫ_$5 LcG@Xn.eZkdfOF8ӽ[{źb!]BVq?t/jϓJ S=hoODEH4r59G܉05#5wK[w(ݡBA>[ [4w?5VH Si Sn|sֱ*S3o9Ln8YxɊl"&?׹1mDoS ٴs_餉GeS)|F9gGl]j4[u7ަdHY޲F4^"u]6'-Z:zY+5A#:Ϣyڷp6ϟ Lih~2r?KH:Vr+rչ(K]⮵jev0=AZ3QWX{J̺mJEIΐ\E*IaiX&q@J 5p9:4^ͪ~%MLChNGڼ?AԓT@FppISwΒKCe-bWKnޥQ,I#02pM43#Em+q ֌<%^GM7}E\lP>lϚ{Tlk~ʲ|]#cE,JX6ڮF[eD ۃ+:AYi_|\3,i~.S ٓ Wea=߽eWJ{rĝr֔LUB`U@\{ ՚Dzj}n}'Yܹ,U$g&~M֫?&VkkɘYNeoa_tӄvzkw6,'yװWryNu[QSc7לt68cw9nRJÅXruDܡI3я+u33w+w c֞Dž5mõ[z(x䭦cwsBb2W{۱g|ݫ xr '^#;;IN$ZjtSɆFs\h9ΎXGbM9u,ּsVOZC[ṆH8ח.Ky:yyt ?#pzV;4lu;rgzM]t!KKGZmvc:ެfsBߩY\Fݪ眱4d{eزhl/;rg!yjʒFGnVYiocj${/ -r^o G qgd$/mdRg@E|Gf߹{P#/=u`gk:?mw7?{jH ֲՓדOӓ_˗.Nn]94_Gg#~v}eg^rXԋq$.f>%{woL}!YͺOb~n,nue8(x2,q;;bdPT5ƽ>&$uܹF "iavހY2+[SoO͏8kOYVagS $,׭fJ̬DD5Z%!wpX#: Y,2Zkɗ'jظ*õGt\vlW5ջnM%=|\BfEN}d@?Q/9=='ql:kŲG\T#⸠٬+:u;_ƞ࿎Z=EG_eYxIMPe /P, _ _ql7>_ oۅ4 ?il_*__*o^9Կ=;T~ )f^0+*zٵ.Q R~6 GWz lyܸ6<B('\&daWGs[/9 OVmUʴ;JW?dKae_˸+ϰ;yU3O].)1/)-Cїo%/w[H=e*s5. \{D4X6%i!48ᷘpw[G;H&~ǭb r k,gFBsyK9|žn8p'Y3ذ ȫ8892Ye'_(=,P?gVfK>\? 'V\,ּf}ZF-!ލiw1!obyxD3q~&^5D`Mxp2rd4_N؄ {{h^(9(%';a!$\%e+VwKN^Fw궽G"1W8"~klvmېHĜ] s!hkF!pn.G&lӯ%Chm3Ԙ DZd+l($OMM;,S] IT/ݶY,֪ q('B0dXg%7+\8fC).\_f93Yyr_5m%7;"sD;XNgSB#de{wLϑOZܳs=t/^B&+ɵW[ zF'30_G0f{Ku~pZgdj6;@:dp>z- ;q<,cnV*}'ո0w00{! 6vz_248yD3=8 <#Qx5vlZj 摘#y$@v;TL\Q{qURQSq l:k0n稀j%$LA.xq[Ne*QH ;*jko,f4uX8W]iw"g&pnbuP.` %gjbXbW˒F v" L5-}Ju8~Llxj.-wLOWB!?PY@hCw'̨)Rmڅ=Mn=)KY,QLXtivp'l̑;~ֿ*=+DxDG}zohR0u.lpgmWTxL17}7on>)"s4y:xsڏ/7UK0gV⍳Sij$0=g<);Oo ~d je~BHn˄bEZEzbh8-9.WeNw,2B~kZVq+?}1ɚ˓5Fe##VFtK"$ v<2pAiqH_~!skۀx\|pKܸCL": cX{ovRx27,Ђ`YN1mV2;>+a[fb4,t9$FJSj6>|<ëI{>h未qbmoڄ^VI<">vgu{@k!-ǒ|c%h$<UwqwSv ? "@ZB4&[ihf|="9M],.,?a ~c~3{s)~mŬӘåkl6#%n!n=%6٭i)4V=d 5@xo-?*#s !VIl;^)vdM]zv{˺`+>edx2X|M֜k =Ao[Xm:㨬q_.]Ky=-W9n3X%a=D+8g\'tpB;h:qOڼ^n-ju lvst=apV+*|HE_%횳y{M;w恆Ob̫r2*,qf 0ϗU˸WYyaȌ\6 o^qqIu'\Cf.cl;~d}a:k['Zߓ}jqr =a<aEn'|Aunߩ( i!<95[8IQOvzc8/QٻQ kPbLs cj:M_jfɋ(Ǜ2.gf6Pgݑ]&:rP}in[#I m_oj .ޫv |r1p/X-0)zqyThϷ2kM&ӓ32ڮӕq)u.̚VX16>sGi *\ 1u;oGGv2G2L]=DеTcLNw}MіH"dqM))驨d0[9viuDqS:XgџNk/JV۲f]H q=Dcݙ$N'ܮBȣv,zb°Hv[ZLf*dnv:,q_rԙYK!X5I 3xC0z)Jvp, vXnՊ3<ՁM9rbˏ5vkF5hW\<+pJZ,䶊,Y,չ2s'U9-\L:73X F^F7Vp6RU3|xE%Sh -i6:wsX{pZ=h00ӗ^`gFy0y?5$o>Ew>K[#_SDE6>֥}M.,Eg:Nndbشd/VMf9[lz-\1lK޼X*M$%k[lG.)8Xg~JݓŅr9 9pԷ>P.( #{Gv#GoBGKԠeCYQdb8u-Ԡ^U&3;+Z03',u֖R;4?Wڥ~ .'ŧc cd,t1 }K7(M4 Coz񍯴(_0kc,۝v6nzk:Eg7Eu ϓ:O7u8p3xby02gXv#%ʏ^f~k}D ?; jOb=fI0ٞ-X@?ktBݖLkrWVrݸ]bhŇbҮX^6|aḀ̇Եc1rtg"8HsVE,noڰpeXՒJa*]rhquu\+ڸ_PYB]JvZ]jY,Ǖo'?ӓ^M/ɞ *E=xXjz59iYv4?fֲF]wʿ@͡F&b&p?z"Y;ܜa-6^H[$)m8ކ_wX6?HyNŞR n~O= C&խ"!8C2тZhз7w+>>/KI[ߵE_m6zxsDnTpGi ߬RsyDꈡC3N⣩ҡ:iӗreEpdq<6٧l0J VAG $V;cj@G.'l# e܋uۛI]$=1j,Ov}I46gTGi韵K2ܻrO@{~#0F!X57dQSMpB˳?I;jZJChrNǔuA~ȅ*|l$? {Z7G($x]Htŏy ḿju&8uީ|&]˲>/˚[~b.p/ Fvn>-&ͧkYwa+yZfeg7h([n#~̧e<OPG݁מe8noB@>~GM'N[Rlk$XwKiYkymDaSߜOuA{swZl ﻱNmڴȦW~ذs6m. ^'Uc9}5oѨW`V8zu \M˹+6gLY#":;)U)vѠ5<W },M9+9_WV\3Ykf4W*WVZYYֺB\"ppn91q+K4߽pu,\y~%۰u`laYpt)vq*1w^zLc3M%r}k%;[ӵG;h:X;ֿ)IWghF'6Gӿ7ކ;?sKP5QNwo^-VΞhx_ jGdMMRG뵊a#O+-uYU1xo+d2bsAtHl-֝ $X-^̑>~ʈd'q*^qc5q !ϥb> U:V Vnu QLc'(մfߕpB eh>Ř%רۗj}W*5nN.Kgɒ_gɒ5dVrX5d; q5.%\Bݡ].gY{%|8{ZmՇ~?sVK\-ltCvGr $e}+Tꩫ! { I$ >jxu;SoaO;E?8g/Ꞣ:}{ <s2BYQLoqK+(#cȩ ='N?ifSQt& K4wgf&GUI^}`e!co>[n)/TQ4Amz;8>_ΡstL췽?n }u+["~,#{AB:W0;|Vw7֛E5QRzBYX]bcTIwf-p~/Jn)ĒA TWC~~d0RKl<*6T;BNvˆ>2>/Z~˥F#Et(q0;ܟ}40˺-),>q+dφCQśH*y+]0j]hڳÚEumGjasV&%w3څ]}JUr]fmڸļܖrˡ͟QVpXXԴBzIT_G65`?5^&;tzL?<54giçYAJvFѷ,MR-!nc8YGiBm謪aW;J{BZ*e0w-La p#t& *e4LmF5x= S{[/$+w^e37=Go؏RgkN7ڞxI4Q|۝q_j;U&:sRzp" nxzs'ؤ/ӿ՞[WXM5@[SNmCcy7^ x<%M8[&&'lּ:Gd6ۦLc O[硑}K$os:kcD>s7bNLܹz{T;2J6TUdSIv=^l;퉑t;uTmߛ|WxHR}L2; ^\z1?eK6pl{Ng|xY#c,zb>C9w{nRM!gbF՗m bW0FpO00>!la6gzi?SOUUngKEOGWJV-Pfίy[n%me9ڢy=]9ǥobc6 7Tb   |/Y/ eLM.6jǚV UVjRv| czmM7oj} |6|o.V{8ܬmZ}=gɟBcoR+ » fbcnK;ڳX¬xzEgXXUQVxWc\O˓"+踽U[vaHaϝd+jm-3a#Wziv5# tr |C.{Qk#%vnkޏ;x7d)n1ϒټ-߂"GЗ;ϵTmo6dqw[=Dt9|s;{vרD~`P*vlX^,QЂv8nr,S (%ZLv{70XطKD*xg:2N/@kva | t^*T\G eB,4dECMIck,Gv]Gs(]n&,l,&CDhM{9ΩsqiԴڕ*9.'x/MSbMB_{[ݚwj%B$nmJ]=Ul30$?yc|K.ک&y=6|ʟe4lzݥ3MWNax٧zhmmㅥ 6b*ZSpjGLwO:JpB2TOW;'l:iSv'pq֣a. Pm-)(0x̕ ʣmGC+mz14q6axK~#TmZꍤC,~6].̟{lEY|}jaּbDž*SN!}A_E6nSm͐j5Oa GO̶!?(g>w K>k1݉cSe馷nG!a תV},ga^9Fzu5!lDž'"?=C; i#| aI4PF݆)\qA*V=dvhnv余N,[&5OKj4L ڱ5aЫ{V(}K -k߱bUďZ\u'Y$?$zdCv.1XƋ +jY,. %X:AgˑZrf3\<&Tii<8IGjɼc@oɷOpN7c󐟘Ӑ[[I=7 ֠~cAM3U#lغhҺ'h4vbcuG,m.Y(}E,Ln[YTSK cNӦ@*\`y_=YWv(g #A 6,wlό?0}~iK'6ϻ\;/sNXndw'M~+oUFsă$ՕnOIO'o*Pl(cKS@8dt; 2qÈ.{byֶGiIGlmZXj2D{C.jj j64E¦J>I/;+Zj $ЙH.^a_S1ԣ_88vGC??;dݥYAT |zp4+򧅳>;BYC[pl6oݍG;2mEjxKVZ骎-XYl ̆SWl}Ms-TXc24[^X*9&i71ĆW Y׌exD^IF+pw`m7TE4Em@WR[Q ^xK$WeWU|cwM=`|3Gdae-3ϥ~rkgOP ԻhR0S<2R0tvC;j6n4Mwg4v8a|g%| qht0OݺVtNS fwu/O_6MH{!I_ٳhѭ-l,c`ƍ姦FD/#!f4 NX9jfۅ->x=KcvmV&XVK57iu&lþSFk SEVne6Ǵ+JJ,$iи RffՑV~a]]ȇxxX\2Wb{?j񉧥^>Aaci\Mg+͛,+1+ YXu\i~N\y9rf ,W>+Q:\-'01IvPYxbjto@{/ehHmW&1pԴ-\M4R97sK}<"Kj͎Gt9oSQmIi |-4OLf8/nحlX (NscX>N!ЛllXgz3 ZѴ]#Z:uLr 꿚{YY)_@=xFg+?ܱ [$v-`b6`Ⳳ;{X{Vn2Ѽut"''w,aGe5ڑeOtr3׬M9.%?j]q v%app-\a\--b. :Y,ڳY,&K ]K,cڬ˰j>="YZ #5-?iɢh\\z0^/F {5~ZjW8WO?_ߗ!?mC~T?Uٴ),(J/;'gs)nvCO߹~guiݪߑI՟!#?gl Y U__Uw,5g3Dz}; +gڬOi8,J^\%g~d hZ,8i6<%:ԃ]GV3P_.9CxzmZN3бE61lZriɢh\\ ^=7ؽ }~?Yh﮾?\}ew^3S9e!/I\Z/|_bѫjmB=7ڿ6 P3 Bf7ȏAql8bӅ4g?8_dJwb_dw=\5l2o'?V3ueG *ꏬ?cQ1~ -Qeeדꅖw-~kqqqٯAɿQg[/W -E\Z|W45š.b˝Yj?w<~\-V.W9,92 5fsɅޱY_ û,}j½Y/8G"ܗ]-U~`rs5sB\й Z-Jg&U\\n`mnXBҖ&}PuyuV+.bV GUoHhk.³E |y/ԅ̬6XKoqҜ{jsg|yXo&'!1AQaq ?!sY8[S2m̮edǷ.'ƥ\8 ]Eg2J=!gl93xM^=sP9(UMk %=ʹį;%MFkS!RǭJrB FP5M=)ONc 1/x9VFD "̚0lumA{t *3{gg,>,.7|W l/ 0٦P?U4AɈ3%2Jegi7{%@-s]|(6PtT:n&=Cn|W5 /2jWQTe+d^pFA ]}䚞/Fi7ǭgS)Us!ע׉ϥ!/ugI953^ _3P46Ln6+qe@3rI]/ڍoJr&3aK mqګqad6}b_fKC]W X,KǷT̸;P`A+J_o,JF-W?Q'Ϝ@*`9\ b|ΙXBA*+~k0ϮÌq7=\zhf}2{Jy '=>Йa(J*`T6/N5=1ɨN"#iG|'3s!y|܈L"¦fC|0C%j;L肖L/W\EIǢ3˾%t|K/Ro?6We]p LJ :S/3ؕ\̸SĬa/phcϙЕ)e9qANJeBOW0 aWƾf&JĩF|vJf %R ++Qf{B>S0 V.s.q+֦K[˗1zэ J>=nm73ߧ>q= ~z\ߠ'7MohIc4x3 mP}>p3-br`V -bqb 8}J3,@_8X8*g1+u|++3}T}J. k|2T* \,%P)::T9uxADzY .WKB^Ƴ/S)|2{Sq\en7gi*W1('6N<ʖk+̪5r^>}(sS3V=/T"[_pTǭ^~B~̘1~cB-1ZŖSʕ4STDX}LG9̹\66b,n1 XᕍW\JQ/(M3܌+X D(S ʓhSѵ,+ESU-m|r#(< 1}T$+rʩ[ R4F8\öh\"^ubT2=ɶqg2+]K˛ԧ!mCҍצewԪ+@ᖀ&I2Af'^DaGѾhFbk30lF">/so|A]LU0Ofs_ Vw &GRٸ0ĮczA>&=(M>=8=OygQ`,= gUm$.}>! es+"GVy(ugEGNC&t5BnǼvj*|n/줋e;epprATmp'<1KREspu}:>(̗ж1ede^gWWR{@oS5S&+ʞR_K/^ҼTO.\'[~ J&y5p[o˗驎減~"-`^԰6 TN~(n&Lsz\U)Lc\j`Exk%V h:TH>XB,WrQT:Yl\R^<ى^޼\Eb {bZV?ù`kS7qG0TÁ[e1] ̺qt,bA0 ':qk}*`/.u9Xq>=3JɇĿ!n.W]z'St{eʙN]#O̯J'7rgSE:skҌSx7 3rr=++el{c2j;eōOl0g&(8c=E=j\s._-OtC Lw mUXְLR:N1_UYܨ(ZXejvO 312l0 a3#fb}aL3]J9`c07FcfIpfoYƺ7(a2Y}>a2˗.}zfZ^&'3IJ KS.ݿ 6N _%7 ,@M1*:1f` X>%;_M9nQ(_l-<*1̦5AvH U{˔UMs}B9s-tT Ԩ s D7Ot`kE9%bwD&:=TZצ&5C[|zqHa\{BlT2bRp{z R*^!"#}w-Tq.!6\{FDd#w[Z2ة{rt[F -K @*?xs@Dp5zL.U[ Y/㤆jAQ 7M0U* Zf{>CSK^19_53v<3݅کGo  ˰f4b7j]s2-xL1rϣ(Ķ8eMK9e*42`˦;ɸz4Yܲҿ2N;w2n,#n*\* Z'dOq1(a<9cԶRdK]W'2iR]C,r`fN/0զZ/\ىqbט")ˆqK拤Lj`ee,Ģ("((d^4?18D(6SmsJy|AC-;6 :eJ͆fXܧk/OxO@5k{#p,j P\$D9hJGeT& ҍAę_;doI9y) #ک,JPY8)q_hc߈!2Q5(*/+LF ׈'F'?#,ɞ]AFt9 +s얛<7 #HטO?XωVB S>Q<zIk*Q|qtQf-2})s >qj㬪=+T?> ņ]1J'7FsehQ@3LVuiF˵W8m~} 8 `JUΦpFN~!jeC_28j=,+u.Owx YᕙwaeR\TUM+%G$K,/ C?\_CģR/^2fG>'A%ohg18acm?/8ncį7-A6V\k𯼳eYПSL+̙>Xog PéH7Xs8b[Xz aw7.La&L39۶zR|N8͠eFl15 0D-F ³+~gx~z!dP-Ĺ X*`J\Ī+XDŽ4h-߉cbMlP`5F`N\x(+*0(v_OvoԠI-j~qьg߹i‚jhq#g'e{3f=eS\4M<=Cٞ#;} JgM-y*qyfOJP%;I#w3*@je\+R%%WT`m^"^6x'&cR/3~>fukJˋ]0gb b#^gaJ"|"%pTeV_O Jxg'Saotе]Ax= # Rӂ's8ΥƴK`(7̊Ok :Yhac(b&-,a_!_+EefW8! ZyVWteL\J []Q1q#U-9_h APnC0ʧϙF6Eώ58y}J -jX/5S۾1盞q7_Nw1/]L|΁`R3S:Ĺ|%'$? (cH--;hyl24?I{Z1=*JUbSn?jO N?1>Ι4X}76M~;Vv@i=ʗN ϩcs*X9,ox^BE0W!ys)k8idWPUG9p}K,se|%u:V VE/!]?K.̦Z\SbT*9"e+v6CϹߴAgWiree{}^YUmM~ pEvNbu6]&*]r) ?dӣ"<|>'鉞Cq!C|!M 0o,zys=,gy!?<&Q:.DkxܧSM5}Ak]Dh bu~ CQ_oiCn!MDsQ@;Zw;{e8*Rn.L6>j  E6?'_Svx[(̫> s^5Jͫf5mo4~e>^Һ>&+9Z~+Y i@?puS~20͕ۉO/ RK'nיEY7ŭ0rԋlU^ga]1ne̾OEJOdl)2a0eCBܡl审⽠~Π_QyLɌve*6ʶr/ĮT*4F){UqTÃ/[c<\pJMM\˜ߨY\4 nrsG{pJ7?=T1+2SS7@{mWۀq Ɯԣi?˞e?gEcW8oQ (Ot$+/5df!{%Bg 7''L3 fRntyU K/US2s(̷_ROOdBoVl/-UgWL3J4^oh e!$ bO1f k;ͩV }ANOYus;+J^!_]OM^*yHrçޥM06K-~j S LsQ.xNx8)W2W36=f~Ol 1A?sJq Bq~,4V 1:NV}GMLcİj_l?S[_{ye.Oh#]KVF3!Q&ZZ7lm~x{+y7o8\~ YW0n;k1ιĸ׺?L332F0z6G-6̍Yq/kd/:pLUPZ[L˯xw.?kSN$h~j90#XǙD)gj1,}_xhҗqej ;\UKkcgŦns<4R9- q[XL!Q\1o3Z\3:ߢar׉Op kCۓU|WY{eg 5 ~w ?%zz/c_*K){{DA"M6/J)$qu> {/υ쟸IiO8Ī3p˩+UbѮOİT)yb&V[EV{k `mnT‹ ^HK*s#.nJXЫvbd6Ϟ߯|$ij_5KhYmL[V3{q0Z7+u0`(kNWU-Sr(e$mnwjH.USP(&JI@ٝb ^sXOį0-dJ/Xud *Tfd)țjQfa.bwe5`OH9*}7%֢%Z^Wqq'Ye q}3cQ8x/&%$[a )}I17Wɿ{"y$[ufO31ZcQ ^1j&:1`[/{qËڱ(,~&*G12@3 /pD>&X0X&JJ.vaeU YУfӹ4YŎ3(ERcrʺ"̋jW^RYĿ2~!Xիh3g0ێܰua.COL:[X%9/Lt֟7"11o_S2`.(*DW̱cRT&ᨧ}A8+$U0J/q5-S>&fTˏEDns\JjFJ%:.\TjG,4~/rplN&8]YX cVU=Qo$Dp xMW apʯ\G2FcW(ޠ;$PcC)ztUK[wd'SX.6a18xpedCĹ_ľj]B< nW?19Jܼ2?%J4iԽh!;Tw3e1`T 9x-9?;sg$V :0x7ˀc0~[jܷ4:Oih/. U)bnUPazOk^LoPD.цaeB/pE:bcoOb c/W16c. ?yfRJD7㓓,%3qHeoŶv K*q U1*vcO`y&&Ti.L#l'8"䙲ɫVh+SEs 2#2y[s 2T}R8qr8u8*m,x#F)C3o/4ÿĩ&7Ec@#R byq5' %qk0,WF >̱KY[b@g^׼lԻ JB^ п p;:'.QOoEC5,[|sa@ N̫+0b~6 UUWE_9azd]b)G"xSp;/O+$ \ dMMRy^ {(A>/R`Ob \F_̿-_!~Ʊ,bjP? N*_XV5/.y}<\0dѾB la'Hu`b4欳/KM +N~"Q;nS`54 #K)Zf;.AX$t&ٍrвK^V5m@B+"0 T1-凇55(IP,u0m-g4JG5Ŗ9xj Jy} &&IA8=xh.2A/k;Ws>w*5Nd%}MeJZiA՟l^vXg1J?9>(xw vos ͠s۾%-Qsڃ77Fn5g"\osfHO% *vA+>g:OxK[Cm9FS.':}KNh] ?Nl|1J :c],_p.}n7Qaܪ:5Զ1ϜTC̥ߤNJ(~ Zgy%\?BoxLg'5-es&uV^5Ի^ f/1Ƴh,if{XvWut*[oC,Ծ8s2m{n{N3ԣQ(]LlD#,K7L_dc/5N5фUjlRkWuf9EbC}M[B |o\TGf%flw :|df*F*aTP ,6Bo5HD^W)X3k |U6ؘx_ՙfR';-J1zaM \!d+?dxGm`*J|Z.k5FC>nƮ 0f+#C+0' ԺCsx =冩'WCfWF}U),}Z0eckbtt)n&:Qw45Rxfu7җːZ@ 10wup^̴5c"Z?$b.SRg0Y}'UKIk2An,>8L 2SN\>ȎOH^Klzg:,KA-׍R8\>0 i~#`]ފݖ)r0(%E\{$l4TLˠ#B:;5LƿFcb�@O;$H?%T c溷S~;- ] m\ 96(n%rϡy;uK2̜\nh42FnmZ^-/rE 0Uӆ7S#7MO];hILº3+j}{^s<_olZ+vaA[P{ovnz(>'O0أ^hkd~cf9>dYUp7{.!]ħlDq!LFYZŒJ_)s ʼn=f, )!{ǿ/rE.»N l7P2FqbbSu0k,E`$Lybes5a"Mw.;%Al "E7IbfHdX(anrMZbU8.fY)_ A^cEYyͥ8 2b,e9//5i \ؓO28lt*nB5;,Y9j\,uKڪ4z7oTQݘfdTZ[4Kv 7L}}VvLkVዮ@* Afea[ꓛ mp*M[ , %۠!/oW5s1 ߿2)VF-aqMjxg:TKϺ8 8v%x~55S:f=V\0[p| ޥ8Ћ8S6/|9p\ϘuU<^ÎS[su)M+M)b Qo-p j1A*˦au-d4Ħ3H}O@M[Tw36g̿>:~gq dRqj 3'[w2gx?BZ]&feD{(&P36yϕ +T(qnWP#1,`c} !1 $sUPk\A1#d33|+_ֽx֠휑Q+D, KE&ȚWZ-q5D=[LOlo:@PUxy9%OQ M*#(%jkfI?4q4BkBpn-cw,&u hZP݀lP|Fbk*A25?["B可i9WDnf0cSxjy]L]+6K!>]̴ %E(L%ϟooܙgl 0b[#[m\Jts4-Wys-\N2p^ 2Ж9IəK΀1Z8,d~wd N`9BZ Eꊬ>h!E9cfp _i^1$HR4ѷŸZ.d=1nMT^ f̎ecb~)O3Hs߿*{CV]0R*N诘] Q@ X=V6_..yb>Pz%K^)R70Z"x68bڂo-dMK /ƥX!_hZgUQIHQGsT-'ހ:cԤ RB̷Rי}F71j13]u5̾C[ 6/)Z;eoh=5\̩\=6fgv-QFCr.3@*[T YYp&6|~%Z۬ z?) 1VU:|MyuZ++3&sx ΙsA}GRK;m-kRt-&sR w.2KMX@/.5=<sPOZd`{aaܦEIQocl@OE [nOK+1U:qo5y/W;M-/Ȧ`ey#!Qڄj hcGh~D@+w"v$cxABܜV\9;fNM{T,G%뉔ev|U~ g,zBX+`afu?DpriNE+X> UULyW.׉u q*2`D0lB$cw9(NNQy-6\(?of5[G/mc4 fdV9d0ߝAs@+yu/K xE.^X<SV1Z%V1fZc#C572fO71*dNă>q}9X+`qW-R\ H( ,@v')+/$-g)+J_:N`h33b*QK(WpRh1}C\i~$<-*Ǜu5k=yN6!sA+e#UavSx39 륹įXoUQѲ;=T50L9(s1ɆM٦jhٯ#fO"U2-6GgQ-H*6y1P嗣y)&a~ 2(/0OiAMh ]0DYߴ}u4U&xw0a'h :$=s&^%=ºe8Ro/2Y^f8?2fZgdR#x7#[~9DLC?ܲxR* 5Mͦ\ThTNOG(3KZq1%mHi"k^V pO⌱ TQz[/x hgDw/+;n"#^R=8ZQP֠6P% @~`v~&a{c[Y[',!4jn2DMkixUs6?r F匚>9# l<#GPl71m[V2$[>.4\XӹA,7"* ϠVnDo.?&[ s󏩽Cmk98RgSĦ/(7t$+t~x2 8kpoΕ|!z} 1Aa((ز*P;3 e=F.-3Xߘ 3Vx~ c .=%s/;%MΈ=45}Iнe1蓬3]ΑdE\x@C5zle J"Y\xwbGQV7mCRw4#s6{S0#f&RldhgJ rM[cR,R ^& BYC YUh)Os5DnL\Vv[YQM5rڴ]mrj_윧j65 Q 0w/<#G;OV+ N8E .MB`eś_gBVҖA08NxpneBAigޅO [VƦڤ5+Æ*#8Q oij߱8m3rDpiSaB-KNlԼOoImGee.+Q5L\*@x`cuz>>}M,>芘8w;ѕM~ft!8h~#2sL1نc2؊AcM׆#Q]}j8:ZaV1:K%!4 p(E%e_ٞd=H<ϘM EE[jy eaḩ)ܭ@69ᆚNW4?(5M)0͕sB̔l% +$zɢ\<..oIf ͳ W 4ĶT,^b @˷9K -U$4X8r\*2/[f-J2BEl|F{cg6.)mܖlbn`Hl(fry14%m_/kr"#R!\?C*`-{&R>lU MCkg! , n)S#r"mq%:&`Y׬pR-0T T)_r">)JpeL :ớKeiQ@.dK%s/1!3UfL+jKt BA7u !Sbt],mJl aJ7IN"w  C^eqMt1Sh0F8dB%4.K;&ېdTDg{=wA,Abjmܶ˭Ǜ 喝j7iQCYX={Osps{U-[Ӎ_$LA@`a` XĢ݄o\#+R%ԭk0kfc-+{e"K2~e ѩǎ% @@\:Hnƾ&)60wFn  棷i&@J\xf5H2QJA׉Qaa{5aKRxk JḥAs~-yga֍Y|N.oG(PArDf(Q02+hF,e `WdYǢr1we U_Q* C'yOAwM/w]x鿈ąqZYZg2Ґ 9w-"r*R c~X r𘸆q f0``xm3yV?!jap78QX5jG|n-ocUQ|=yE8,4.ȲmCYƘa%(aR `icgr&Jpd%;ԫb+$]kGfD$y#EQ VpfVgE(Zic܇u DT15VY,jc#5} EalchT\RgѮaoA~g,ql8eN#@8bmҺdXxhs#̣YXNQdRϘwqn zB\0Jș]JFivߘ@fJn9 _E "i$d<257?>`bX6sDu˫(j0΄۝އT8ZP[ĻGɵ࡚sԩڦCJ1 \Aygx Ρh(3 7K^w**& "@@BWH fn MϬHr=%؛F._Qr#s=L ͠B>L|Ƕ+3u2x. yuɰCx3%u,ΧWVaۨ[!-ǣ+[sjFF*8I~ 7dQebd1[R楂IyEb2F%^({JU~u-S4j4)ZQZqucG5}l'23 +/0>n1w;*H<K|'xn ~hOsLe8mvw('?S7W 9~x#k`2X~c+L>4BLJi %v#,_7˅TLyYi@DLG:%yyez2ŗS,T/cRw%ip[ҽ8PM`"c NIG͢ <8LGf=Qds 08H, >eWcqF[bg앨ӛ#Y%rY6sA~'#;:B4['FQhLyac4f-o'ʌ~Afbk0Lʫ-2 6, oF/ި1׼c+Vj5pa%՗T-)e+3iG)uOu8O܄bDͭh`ZlMr2+Jڎ gVNaD&gGiEkW1EdX8TX*j>0gXu + :R PMe[ks+(9c麔+{G$@Yպ.:-6w0OhoqJKFѴOt^/IY,=̑3Ex1c.Y2QQ 8'Qko朳BKY6@$gM|\A1Lұ/ US_Ɵ.[jW1KF]LG[ucU2j/kj`S+&Q,g>R՘]eZe)LrG+\RSXmDJzg5is`">czcW3̣p ip)dLRl@Ĺgq|{c],΁>0qa3W/" hVÃ)3[P%w( j'ڠ&/'@kc3{Z\++W\D x--^ hrٴAڎ;K؂_g !Y ]ܰ]%|h. D#xܰ XR/($t_\pۿޖ)-OUuk ]5l >=@ m8L5wcOC<,%۳QJ-ej 3eݗ}Wbޢ[Tr {.XFOWCYև{.^}֎4Dceu~Xd5P“E_uq?b{69{W+# 2EQ+B_ }ۍIڮ9d$i6%eϠ/]j#x|ƂLdSs޽\tI^`?x5p8F:L(otcXZcΌ 8Ժ,An i3J8hs|u,JWw"戚|L3̢Ƣ y+IKi'v/0Fi]q&Xg phɭ *f4vSLyqzb`P~@}CX$â2n-:#Q%5k#uK6 92"=b0h_P}; hTEeF6̳ .;ٕ""fP22pLmԦ_(>#SKiOtKgtUz"iQn61YvgG1oa0m#cn=V6K#Y*`1e[+H@n|n_݊m &/`Y gCֵK]cqW7 W_%r9bؼ5Mb΀ru6%MZZ M@%(=:#Tη5j6]ݓ$+4avEt/2<>){ZJ楷$SL3T^ȱEO]^跹eEVlՃU׉(!5/N쨳#;sPFc+UkQq VԸ8K y&A7l7VH-4FaJtҏQ Yn-~yij:ɬA5gnGJ2a'. <K0[]ሴLx Sd\F 8q6"n{^I{\aݎ]GXVL7 ʀfjɀwܖ^|໳**W,&1/Բps]2L+kK+K 4I1,?ˇ`\LS|KU-?>[-XKV7ot1t0tz#2 lhA/%p6SYE)u A&e1u*>(]P\͞$#l ˺2e3{h,>"MpLDgC n;p /eEIɸ9+=/MO3i h&4̿rX,DHɘGGɌw0WW(6q`t@-;7hc8 K[," ,*h3bk?FEx{حm}W' s[ V*>Du*"QŽ,qzyTUMy^%0tǏJK9Y<MM̻REFN vDeq;ip3{3n4%.Ƨ:JPsSM5 Tm;iL1 ~Ļȗ+KJUwĽ8/⻉Ì'/(;43R*\*\B,Q%e@&PPOb)Oc·+%4Z6M0%JK'l,69t|e(Afv½( :&2u׺ǼS\ Αv]E, 40nG^̡*8ܱsw2tgyJUѿv3`Q5[|MQP! ~,`7*P; qDd־#.ku9Hbr0\<˦㹬~F+d#ǤoWrLLzq#Ԩ 0fKDLY'.0k2&łXH6f2U|Z1.1hO5)J"cP! ѹ`q텛%`_٢)L p4cŪ)tQs,al sa\W #_~ ^ʤ,eD0gd&1i9fys5-%z>K2\WLqq8 i< .v1\zz2¦<i|Ki@*-gb#K7+TG鏟݈'0#Pi_0hxcnaK{BPILx"6'Vޑ.Zh< dsDԛבQ4'2ő3W78 cܛz%wɗ-׈(*s*[Y ט3 s*7R+ ʆG,Ξv9;}LjtL%, )n/?U1s^.d3o6'+{#tv_f+bZ+h*rG_7~^81K|LȶA2lV+p05dywq[Z%x5}5#J%gx(##\ D*_"k &xTlYޢ[v@dČW*vy_SXK}?p=pZO$jq 4--V[,EL5T@ɣBaH}68\Vw\YX /x"6pdy_4)ߖ0މGxN/iO2^8 Ǚg;}"#Qcp]Q۞qlG3u`VU o+ew;F7d[ Xb/s0^ݬQF1h8+"`G|f !Jՙ0P4<&誇<5m3\ =p0Tv| }y5Y]ZWfڹh<N'up-ǝ1-y!dvT).)Cs#2˖wbW791>*rwu^ >~*,[=sn~Zf8[>b^߲5BjfRoŚK33gpCHeF !m|$X0="_ސ[BE(Gܴ=~uE~B!A.K8eZz ~ъh`[rm:|yf/7SPaj}u)ԣpSȲa"Dσ/%u 9mXCvEy/Gx96Wf-)l&:]T_%Μn1h:CܚDa_s11U m0zkt_Զ\T<uH!EثgK0gPS_\M(/ޫHYR|(Q3=n[5Wz.b ez|K0wǠpT)kM#yڥQ¿a !1"Go1.G$ڑڧ(yUR0n 4xlc5f\/U |3KLOgݲu Qɰ u!=]2)}\ /W(WIWc#S19Xu'E%'K>*ڊ֥siv>^жY/Q̥nps/|Gy}[kg1c [HuCwPMiR;.[s u65rJn/UXc1Q/Y(({91:pGeK~oUak 8G StQ 愬?ؚ%>XDˈ_ϑ0Xxyy ij1CR7sn wocs?(=)Spʣ33 u(7-}+7 a]kFxGg' OHk1+WSOp3R$@M2KCD\b,}k(]\w6HwϞ#pxk3w-Ĵ<^f eK G3 b.j3q&E~k6"NM8+2- fЫ!n]+:^ W; b[Br2By)yÉX0#di=V% a:Kģ̧/8a4\]0#Ș cD$sMo˔,Y|Gfi&L^g:1ɊO!AhV0-l\ׯ2ѷ93ۨ72sbo2nfY$}ܦjmqVQkk714-ե~bqBp?HP\ ])1%ʝ38hp[s+w辘|Dw0-㨎!)0Xy.ѭzN7%s/~fby)C4af=](_FW"s$! ~t#Q73Eʸ xؗ2q欹 XPZgOMj`s2.&Ku}-i0`-s-QcQt7tcɕmV`3ʬW<>e^meQ1楇}J hEެF֯I5@Y ^F(e\PwS!]e۟7je_z\&8 z™%f̔6{bXWU/2"(*Q`5|>јDKGhѧ6*Ũo"u/Ŕn%#{H3Pu[)Ơ!XJ^嗬 xvv+eA76bYlŦFN'2HoߊFc;ѱ)hw sUT`6s50r2M[j^zi+U@Db[x=e|8b2L5A -/0{=&EPK3 Tp̖&|y7m*L)tYCwy~chb8d៻g7/+dsx#n |vħ&#>#V zsnzaVUiW@3G0ght;YUURrZ*1ʇ7G]cKi;|-Z{#"݆Uoo\28@Fvc,ӅBčܨ`49))lcU9vzUǀ1\3ZG~t>rk idR)k1#DKA}L[MT=Q|\G MF 10DFZ`#2s\E8)E:^/&t,xaWy[F[&]39"lhK(rjB8ss02sedHhVjPq3a\Q 7ҹĤl WM#LnUan?$JuyY,-[Y"(&\@ÈC39b50hTb!,>L/ǹޢ-tb7sVIa"r1!chv kW17!G1#AsO~nW`6Rfˌx:ܸhs ii]}rgL,Ǽ?x~k@,o3|(,܀U;";Ȥ $J졠%EDjۋ%S ru0Ҝ1feI*"uu 4,ȥId1d 4t-Xr]/d㜠tsoF0voޠ59v  C+2YEV7%}8K ;edȯ _TT/s@1 7Ǚs/!Oe-)q2T0:ɗD;[/f7}3G;f\LvJ#+Z C1rzm658K T2"J\@v19kژč- ˜}uMSUk^L.&*^)VFṉPf>IC68'yPxf>)f!s~"Qjchv:qo m~DF&ުED@=yfRb{YFcFZ;er=_XкDE#H[#qw_RގPMs0C Hfq'Xʨ S&:EJ?i\jn_V% ,pԱW;A{]&0V=ËfbWkr:|(Vi18]0+]+#I A{+d -Q2sc,ܺEXX9rD4vk ~L3n-W4H+"" ^ԩd L-&;nlf۬FuLZ⸏%|BF# s;g;T_WL\MPsq18L1H}n>3LvE1 8lC4"޺&iTiw<4O)H32wvAXL1w8bZe |a_GD4]1Ъ$5<2R~#5YM#cзP{D! zXV[3(~cU"oHnRm JV09n L>ʨˋ>@Oa+/`9yG'r1F)`>̂5m[62yku460/gk@@%tf1Z%l-` 0=<~nz FCsz-) 7n , b׹GIQ VXU@:Mdِ[pm| d"ൺG3%8 7-1\ @8P+p˨,.!jh)upPwNOiNV$W/Qƣ{rM"sQ!:Mgġhߤ!ג~f԰B}|kۊXSss]>0x ңtbF%gB6ɘha~-T^yWdXzв%EcjN ;qSOԫn'mTRĻPs&o(濫~#6Qx*H%2z`qcKwtb# i"o[~HZ[W _BQ &^rko)is.k_%g̋/ܹ\miѺ>XOK[o'EAiGk7=`)2LH/U@!!k ],8#sŗ2 =?⛽t3V41XCa^qe|k0ø驰VJQg 7E2%žA*U88iG2jS8kzn>7-'M(هuT*D.]UGRpuO>@<#Eb<{D:|6 æ0{R.b4yc9k5_$LReퟵF]|ܺkBb-Vk)VOw۩um˙Ԩs蒯4l^b'%^hѻoLĢ%('eטQA*n*._g4 D\_?0~Iu<͞u ~v:,?l#1m~ L\ë{GfUCET Δ5 B71֡Id)uBC lGTV!fm{IR8r~gV`4ÿ%)U4dPM #sx-?0f( mwfsnQ Pͳ)9ُsͻhxGwf{b`_p޶1l\bH%ܹx_&j2]3gbf0]˗8@:2I%OLw4[31ch@AWuZ_#%x5V-qqF\.1:Y^s`lڧeXe{ +yYDfͤ$!cOu 4b DR%-M1+V>&^cȌ1S/V9v˳;kq; VMAXA*UzG03Op1יG,*KS%ETnxA+2~#%ŽcǔOq;HZՉ]>o@/-hu"./Wso71lCZSr,, >gܱ2"(aqUp]?P,+}S3c_zPƖQ5:e0+.ݯ܉gIaA/`%eħ& XdnqaA"3*A"a#7* iC4HV:B/?{BՃԯ'BZ.m8^ ^RlEt#Hi!ytjXځ(IS-ҙ$$F5 եg2rcLJ]>|Eb~~hyTYbhk+~v\X9uW Ki+] <)G ,3 { ^A\{&NeS1=g~d>^3=>q=t(4 -{=>c: mOX&B]v=S'yR%Lh'C}; p 7OqY3{l{hZCB{,. W b?ԙ}cU0 /^w@AWB-2xp6p%T s28,g{hywU1"#9@/eா:]Eh>xH[ޮ+djppS- -cBِSCb)&!;\z"5k0"}JO5\]?7.Cݹf ho rMaeW,HWNP6!93(k̵l'D9BR[_) y-Z&TN7 XSQksd1=F=ॡwlXbv͗\/3U^Zܹ(yktRz5Ǧ#SP7tq7ϣ[Ĭk-)cF:S%\+I^*nxsw,7%)rp>@>iofq3WIe.{En9 aRŖ uP-cU}A-)r^Ч/nWBVQz;V^0_I_bԉ2{spT? bjB^Tw^mn_}L ^*V\x|sCJ}Q~3QOOjlax:KsS0]CQz% ;w-X=(O9f4M5Ծ}%jU5g=2a\Lď/A.-5t2jN- 0hmT1^ ?|CU#u/W%A\$&@~%s0]"5U`8;>5W,GuyfGf<< ݈TGhAN)nsDܾ6{-JT:eh  GYCK Η"pSW| q-vw)tʊ:bp=5z?bŪiAZݖn&.nK1//VKq,L^n Vh̵+5΍}\b89$%ܮisQ% /~'}peQOyizz'?݉{C/'tb^FiOoagw(}g1T?yI/Az'J?}i)z ?iq`p~K'JpCe%ǏQ9{c=ܽN^~&tTϠ׶3Ï1w"&E C+WWZٍ1ŏ#Q6jV& q S}ٔ;7ܔKC5+pBly+^%"w^O(-PP^$+!&dk-ln $ G?jYXC4 3*wcˍWy-b9,p yvk{}ṷq^`@,~! k.xb\]v܌׉,x{u^'WlNn+]|F!zz|~f=R@7_J<33wB57J* 7\q N C4495+p?~o5:Ru|˼=2~D?!sp?}Y0=|_9(|ĝ1a3=aS7I +'9ӿc˘i,?cd -JK+DsK4;{s)+RSb0 x\65} {&+BR)nL{CuyV]=e(NC3^qO&a5 3h@eA0H}P%iKK/&??3"fˁ.jLBQbR L=EU80llTRukJKkb*M|: i'%`8b|ZB"W]ep?m< |DEbF . (k-:iMYPU rB$/}8@ *f%n+1.ydD d 8)[X KH+A :[|p=J3) !u0'PjUNc}6 JB)Wyj0FqpCr%RYǡ%o ,0$tP>#NK ~\h9h33CbbS>(|PQO+DZ;Q5UQ~!J#0bG剪Utn1(JuѰe^͸}R 5g͵'{ HTT>L1go1vY`'`5),C1,c4,L`43#t0yO@>j9ѻ;[n i62\#e`;zCg sP>l a^2Mиc؜/IqnSƽW&JSLr?^r= {E~'1_qܮ#+aK\ _;^BWp_6Pͪy "N_4[L*oLeGSL5Y%L ?E'O7V븩ŜIǿḳ4NԺ2p t|EOщ5 `pD6fRYg!X|w܅h*fGhxG~Fxl=T7ƽY'ERS&dxJϼZR{N*|Jƒey #FIqF1 6qơžDo-W&rv|eVJ'x`ד3h`4Ys.@>2.f _0EPx K cO`^[[/:e^8R 9^q-xWQN1Iؾ3 RO2nSN^AQNKXr?*xn:P[yJݨXOG=PEYqť>;+Y!zG EY= x:? \nP;̰@|G1ͼMqm{ZEgJ00faeoY_kv.d*h|"V߾R*۷O./K,KX0 { vsm>ǿ1(ҙd`.{',)ƌe-AyF>H ^ F'gfK3NOK˖ίn,KoW3V5R2z~,i#ƥ306'$ 63]0(<_0g]A.oV!0$E̯x½-hʮɞ'z5>ɪ1?>Dl]V.PWa h!Ơd<pFyBپ?م;l`p)smmo!e]w81svG]ίB4h[ }.,jڢjs)b@Ǐ3oK..[*P\{9KzVd7y״WTw++lBrJo>zO,V ib|?iǏ8׹pEcߤB_e Go$vy涝^6>u#Sr̭CS弲~RC\̭ZʫL>" о`?37p(]u/zyzԾ+r%FP_}I3S(`078[tnЀsek8%%n2뛞״%Rѯm&K>"”|%h5Dh2j_>"eE|@6SoJų= Zlo2+- 040ğ~c 8cu7KY_KSřa>V+=?QykW3UF_ܬ4@=2]h&`Ta̦Ge{`|$ǻ2 U;?!Ryj5/їR[)ɹoK*;0tSvGp7Y:r[lTRR}Ic/H4J v>Mr,sV!L⢜bdbl~M16.7c1gl?QG,vًgkU{v\1fy{/po+3RUT4KREz䝐zoQ;8;UjoE:2%\M_d ol*XX{D8C&M97W{ T)fܗ?sqy3dYԿ'x"iϴp_W66/*:]y &wڔt~(Nw:3,/HgSMf ,˦1Ő`<#ϧL;t<{T5&y+:uc f4"Tae~n/옖y[' yg`q!Z9qcp3F2N6e??2_2~V?p09'/fGwW:*F[.G҈/pdjgMC4_@OD?K/&X??GqE?DL tK"ʃΣHlY԰UjJ\4AH"=D7LcF5(6n#yM[L~3c x&SpJn,?,D5l Eu4),eؗ 2lg#$Q\O%zܺ;yƈju./"=S@փSH^- ~uw;/[߂0TDT]P1 /`bvz,?(S!o;&%Qi=Q1gr`, ~kp/7rt]<3U1#=-[A7=*^^.Z Ŀss/%ye>?a:z_@uw2atFc<$Hk99',-`ervvVE..{JWՖx`jo|tAhJ$Bo/mSTQxN鱍xmX9RαWSZ iQJ,6l ?X8_.&1o=sg4̡聙3?eǖNQ8[,8{0 @qstI_hT0ܶy'2e?F5\!bT Jz1v%KJ1+ROBL>Vw+ԞmTī9SXI] *۸W;_@WӊIUML<iYژK; aŪP*())E'-ǽz/ QeJe9)/ oq'ԑu ;k9fSTұ&"fqxXlgqˣPNkoAoD;`U5d$@YhxEaNRr=W3L2`\T`2n3BLo?;Z7~㠃K[iw`%87xWѻ{*d.^c6T>RMKpb Q]>X69\+-U ZSEW!)Cو5?ԿM\|KD[25.6Ը11Բ3%ަ iC\x]i x>HrІ;+AsޥydPٸ! bЮ#DBωt=K-N6;ŔdnDL bƭ1]^HL-P1MoiI/M&>bQulюyc-,(We+$%|=d~Itqon}W RG~{K-s,?g~hj_Du8J}N9DXS+!,:3 JUX/_Q ( =_hN]dK5w~0]?]+(621!oQ!8' SjxAk2|-w6ŞCOs W[s 1;w:L-^fjLf8ɨf])er1 L%͒6K/3.F^1YxYlߠe,Թrᢠ0s[ hjSh}N:*JT{8@F z r*fp6_-衸U w!{j7Xe{?x+rAxe:2 赉Onʜs ^n^Jk`PY4 ynY[Fc`e;_8s[O.%^}Qϑ_d}I'&02iR5+&raugCE* 9lkL(6^PFC۟)S%O֥_WU;f,Q! 9Q0tmO Ƚ_t`c'7k981;V0rn-U`嫴QKSA_P38&]dU"{CjN@b7 WcVo7*ػJZ+;y# (Pcn5s%k#1iCBM̪5 y:fk6]l0&0>;#Ct;NG^ a & WqhCwaeAnר¤~-cjURSfP-Xs (`'J#S.>`q7N G)ܠ.W{%̈oAeD( A,KܣDUPzN~t{v *0'ixX-<~>jH&Yc;H Lۄ1\ oc_(qKz UPQ:g+[p\dV/oEx;KŘ'T`IkeWЬ#W8[-uCrhUSm0ϰlwA[РRX,lU.aLs)<_gsrj^!4]i.ޯ1f9 8GdHKon xQ)wJ^Gshxݕ㳙A#{!BK1,3> 0d Qq3J[42b+KJ:Ui$ʿh4P0Q F .Uzt+*]c/9^CZGUS!c5]K qAv x@2IhgnG2}8~Ҏn% 07W-ǴnQ@sk4LP1u-f+Ms85O'ݪn̬ eE1/ qnr<DrU6@v|z.6.J)0ך?-6/-5sӵVllnZ .fi7W#-ZIk|A._Aeq C)yqnƇU̾@! }_Z]_Z5xء@9u f:c >c^u.B2+2 8.?Hr¿%d=:_xM^lxpHo1Ƙq).# ,[6;yb%"yg :%v99:u wV~IU21j~fion`ȉkQGDwoܸ3 1^)T1%=pr`yBqBɉ>!ϖ1ps ,dusi߼ 7K A8D:e0J8>POlCř|员QL?cqqYYNZ|̕ϰږ¶G {j8{e(e \0,ssov@`BvA0^4k0 -}<BQ@ƹEs/=@%#DQ֦]Dۈ1"f1}N:U^T80fFcͰppM&СɤGd68;VSavL ~j؀ rZ/:cܚLqUAdxZru&'xJ{|pQCf0ں#ʣMTc8kwo5 a#WASN\Ug⢔Eԕ);Fjv>9x6e$Em]Jvs+8qo"VVnmmyQz;l][}Cjbx5-bV\,rm dVe8qx06.exPmae,*/a5! /4P:tJ` 5Dd4[;wǴR'f7WnUgRu?Zܯ rd~?k#g͔Su5k+BH W17O_&a[B=NOhx+jG2'dZ[\h~ nAhŏ{ ]Iw,PJ =0 ә$(=/hv{\ޜj6TJ,_{*yăyp)n/5c, h``)i B_`2=Pp ʾy-ݤ2ЦU#M#ޞLARʭ 4hCKΡLE''$)Ѳ}'ՌbPVVBh"j+!Qz;8=qCR҅O(UHŖ 'GV€/ ;\&9<0G1Jt,]cZVбΪ1 kؙh8KU{I Axa*"! ]nD6Mˬw2{A^TQ%Q/HIj&B?(/i#_SE52 b0ҷ):l$|&D'FLk1aۉ* c2->\ʃሼ}=w(8oǖ"VWL(I94-s00UĻ;!BKBA&ҹa-yj!\;&%\p*e]le;O"7{To?4EЅhV7Hx`K^zAU:U.R4֕?\fԸf`6AI_"X4HeGNzpX J!AlRf׉Oj*=@F; U`{PBU'YGP/顺 wd/ysz" qB^d뜷sr HY^; H8B␑U \G ]\bu.Jixy_c d"^=T(+&enf8A&(fB8|D{] &1LBJX`ucRlxxŽ_Uu ָ0SGI2_I/Yĸ9og&rg'' KV\[J)kyU}x3r`!:9k_2~ [Rq;&wV2mOKʀ]1+.|TK RPc=8`6BRѓф*; OadEn2hOđQ\~ {n7of8aEb%_!>Sp>P5 Fՙ, 7NccGqgI\Nӹ%׃ϒ(u'nfrYh4rx= `at*?d*tqTd"TtTv#0@\2CøīfT0m PgˎSk1+%JN'18*n.*VCBlSo+)ylXl_%Ijz@42lLʄ-|`^C5qAK3lwB0ݢmCgVq#6AWq;],3=ZVkMʔQgM؅oǧj<3ŵ{k;qO ظ)qD@A_vT|B7Nis7|TѦCjJږQ+fu k8?pLKf0 w3[4nzc$8 ͭ("Txb(qMFU70Z3F,"S@ڱh >_ z''elU CfCR[FE]u:jp9 =#4)Nμ]ts HL|8NSVGb؇6O*Ѿ84ު@ z_6VrAzK@W,n:Sao=,gm1CpzefO jL3PH1a#, 4U74+6y*Q,v1 K0gt n{sykL߻ǙcFOd$-% Lmr_񹙑%xm1Lu?L.9T]CKᗨ?7s?؍[tl=*A[u>9@u5vgD(:*տ.mR1ρG06ms`NץES9S[ \A!8[I;G00 g(5cXԬ5SE>9жAj&^FKcd(`+G"Qt{_g&Geho\8q/^ ,-KK?SwW5J2G#`rJ PUU+YDy ask?SW1hc_SmU).f̝:F]rNd<^:"~2@F2ّ:㿵haNE'&>H%!7}΅|wyيP [KhX'"gtC^6aR\M bLR_loQlُ?~*]tz/qB&4{[[_*Zث1\]VV 7xe m OL~Z8J?n?$cȽгc TkHyEfzfb)cf,ZZ4tB.г=G!C{N)rHuX&(=;%$6Y}PH27 q/ꏧ1ަ^Ig2Acvɉqql:1:HG  _X7d,ԼzȳoN!Ji˧=WVC<7+m0L]%"b%tn,Al70U83Mҗ )򚏅_1hҰ%Se\~0y< jޠ65|}n 6 xV}+hԹLXrj^QIXOܵ]d#Xf v%n(ȗ@%bnyatX|ԣKΟ>&1&0h^nf K䕇0.Ĝ(.~HWť7\ * N~HK K; aU73>ڇ)*Tb^l&6-hO"h nIpyĶ_pn%>ZlǺ w ZܵߥN;ecR5{p…Ի-plħQ$c7>r!O 2oXhubǒB/YX"GkEdl+8X$u_OoqSw/E_4tGE+U[n.֍D@e>s 1"Nljz1@GTYXe퀗S"md)?K,P)@la3o8t癀d$`qP1w5$=EK{r ޼`CƜ, 夺Ͱ][hS(TUan+Ӎ>J~aN M/ҢlaA[qYe_bqMpnV3y(%'&v8MTCHJk_5@6"^pXs+mͱ(? Ƙ[Mƣ˷ UUY5/F oăɼ#&h* (Ʌ_+S*P$Rn 3wP\c@LUчNEtYFWyVy^;Ѯ8oU;Je_r'_CB'}Չnܛw O}2/o#_4jOwUеW4j :P /׾ڎ={JXE!x R ;4Z#0 )oDLx1TnWGF:}zP3߈y$ܴ\W*m5b?{EI@WpyiS1d;y",#IVKkN]˻/ K>7rl|h)q>3[Mm#1,md[eQܫO/ew0e[[oQ*y%$V`"m NԵ|`븂# -vAgqWYX&u{"sR]Aeˋr6)sf0?Z&xu2UcI6L=4SSJѫ}D{\+ejs{˩}Kwn[ܹ* qx1Sd~%%:2'K +b~6#k?.'~Y EɊ=j$`|O#@e,5L*xD3###/CO9<.Qb'{C/ag01Gr? ҇E_ 85q1ZT72+ CE-GAX_&<' ?0?|ˋ,=.tv{˩h7(TOagd_"w:dVEf̸C4]<[pe]GnjazRLq4(L?i`X>E&/5Z3,s E$1]Yk8^B4z`ZﹹUB`{'sFP[(ZBhIxc7׎Fy%˧AY)]9sM"c;8j.%GqE 0osOMa"`3Ai/Ә\/@ܷs7^F&[--9Q0\Xwvqia6 =8d}<'$qlohU*6MeA̵ E .CRWG0,e c2濇?1+יkΗMۦfWG/o56l~fA" 5u@ԩf8@*ۆI5,v5V+>n Cq\_l L;Uc+.1' 0X%޲$A A$Ҁ ~H[`,`a"jDT $:i 1r7X$ $#A&D!i!![N`cp0 L.S@ P1d @P 6J8Y<A"h!)`hLA$H @ĠfBH4Hx!YXd٤h(A@0XT|[I" K~p$"KdM00 UĔ`IH G%Hyd`HH$ZA@ C ,`c$$60⇳$M`M0 LDH $Ha%6pA !lS RR,hRi e$Y ǂ &`LB$mD_lY/pH.Dd $ A h=%2 ZIAA$#$A DP@ȣ@ pkAP)}$I,2$ahM)X"I :X" Ef J1j #QaM @IA I G ĐI8<Em(  ZdH!ȩhi (4D"  Lg=$l -@O, A t0A6MT H!.@$8 H%\ A ظ! $Yl4P$I jLAKeCAL] vC@83rd@ " I4u! %-&DII%\0aXxw*{$` 0$HR @Y,!&]U$@ɠi('$gEsMI!@$ Ɍ WXId HA,0@4 * 52,8ʆm&LI$Pnĵ$@%l@A$ rP=P8H$@0&>DkBڊn A $@7@`WHADW%F(̛B z=߹`By !p,'ɚA PS% sJmdDL$mBM" >&ceL(/@ {`A) )@&C)$H4 |DLI*S) h@։e0!P > ""@  A 0PLE$J$-ǺB$ G a$@:H II$6l@eQFD$R{J$ A I ("h HHȰG%@$@jD-IS A'gZA$,Q  0Q e!l$Ȇ]~DH,*Bd6@ P2@}&֬fQh)m%e )tD2,"2kЃAA-*0r7V!EUJYiFJ[^ A)4P,I% $, G-p9#iIafld.>x2#nAI"A1`@4H^,(M+JYȐkBlz ~ԋL &Rd-$BXh ƆDaH2a$#\c p mddS0a$H4@!pdGm< i9셜!T )L$- ,` H@$@AY$_v0ttS7עx$- *Q !40'A&M8x0-K |1CMp+N bKi "  4-p<ljX>ʸ"Q ˖$lDMp A,H@J>`>+Ru׺$ ?=254I`hp \Y^ T0BR< z"qP $>>iaPL,AH ;0BXD [6AJoh3NOI6 (X[Ri{ !,6\I m$AMH᫼ī?6<>kR.F|Z"bh,H(T#&x\e{3W۶D->fz c`&JFx"L"H F UFƗ2DO$E=( CDL8h0l\?s±NC)a1u@?슶]QRօ1 Je ƉB%fCTʟ!#AI$9G2lbF@"c4P#G@͝a:ˎ]``R@A .82JM(`2K8mT]=vf$F Ȋ2PV4@*+=kf6iL$gZ@F  d~ A@t ː5,(Dovh.Z@$΀ $@$iVX6Jɴ "y$S`%ž#m (\g|Ӽ@iFC!%a@A"6,xIdqqgDI@dd#H$wy(@&nӷcoL]! DKdx@JQJ$ &k!;͠ )d'CC'L%_PUhN,EdPHP&ҚIx9$nU6!BKD.Jf aBXD)@ ,*ѬH(iZ0aSH#g#m$ 0U@vS@<`(DePjA!@]J:cigQ"Q;VԛWhS2!RQZ9)L8NS],Rdw/`a8=^1VIEU`!c[!<4d ~g3^ׁ &,pjGɡhYBqbB(%2 1HJIn5RA U4nIj,I(hxQ9*eRp)K$W@AA[l Qtd/0\>˶yIR(0J$'$Pit'-A[pwPf  GuEA:$ кs( L`Ҧ60M)C$Yb?ö0/hP 3zdi:$ʺ 1eIFh +CR 5(@F"L ~xER e:! `eET)EaG g#.hBMU!BDP ^7 o y`u\M85}c;FGXiMHc&h_+s @@P Y4Z dPIWoQ7vZ(Ze"P]^%,$:"vYX3]Ĥa" <C(0 TqY !@o&ŀ+h A ?+SWe$$"E)IXz%cC s=3~pi 9Z٠vkvE@eAЁR ڦ̮2BŞ4n\$$bW)Ȗ0HE"v::"ihVڅ|T}&*e&K/[tA S/„PH sxw 5HGXױɡEOMCĂJH3 @,) CC7`"d" Dm >0p4^`C2 (ue702@lE6sqyj2-p!+)" Gp!$ yʞҦh!aդ_ LH$ pH"pSV:!/t- C 0ĶrS@'>t)<of+a*ΰQ( T3z]=Uv1F+ .@1HbEٔ&dzUQ "IM"m( @A`VE }ӗ/dE˱ZWg A1`LZ5@,Q2-&q>MVh$ u6aU8J-t,%x63:;Y7B!EBƒIv`-g>PvH{RӖk0‰Zس%! df~9irA"SabL˞A,2 xgIPLG((b H90& !$ r]ځZdtIi`i- DB $rBi44ؖ-iۄQC$}|N;P$DaDbHW82K -P .ҕ04H AEKD3ݡI A->^g$pޭٞB(*h~eZ[ Ke HM4E gCiQ-J2w  ] ``{vLvc޻YQ =PC BQ^L`&vL $B%AE ݰD j޽!36|EP"A$)4Q)%$ @ KL C$/@m!E# 0 OԐ{4X ӰpF@%6 $HYHaIpV34#0!'x~ 5 M΁M`+A(; 1 l P6՜ %<)IPaɑ0ɪ eIiS@C )PH ?!dDEdqHX6hdN" nl+$ F)"B ۴0A$z(i` ϞƟӳ@fChLYC . 09 Zj.$3 Ge4 劃 }.b魔$U8rM2:;2%:Ic馎[%7 VMי3C(Gyk- d' :QBA /Z_!I;'3a|qN2˳EF7岡Hہ"H5$Sʒρ#Cq3mRnG;CY($l~ܤH8! ,$P%؃2p 2Jt{fUtIH\I-D$ ahD(M"$X$,,Zdik<<ג>I$ I@D KdH ' %~eL "c( $ت 1ހY,M6tBOPI$@@AH(\,ZQЮ8L=F3f; |'Vd&$D+#!l*"BUFʏh;C PQ:ENh =R0ObA eaH2f)nOvCTz1im7pp"X%e J#VYLWPi\ hɞ?$2xkm`!LEp Hd36P2LA2Qt,{GH |j&vیLcBR>=i#J]gL{K@aF#,3Lɴ> Is B5A10;x밒{ ѓLi2Y-Al"A lIJYhW*&ϾbS"@2d$S,!Z*ҕA?@ @I$](I 2 i =tpxPk d A?*!1A Q0aq@P?o sY*;EQh Jpj,eqW[o煩dKRmkn_. sc4x qC{|mA o̲TY,Fɹd*|G _SrUF96,o\<>7,놾P)\|*pwNRshۗ ~&4둩l k-L5FCG/7(L y*V/#%xp-p75q7ao!4#LAC:PFu-KeeJ%f f5ד]|jQo.U|T+f)| ԪT0 d >Pċo18u-.RPp|I_Ե^An^*p) p1|[jZ[A[Ɇ\wH5-,g/[AQ/n@!M>_[CQܤS,KKy5bexKTȫUe\PGp*ijc7 |aLvMW5MFbT_|'rQj-%klop$xG $.e1AwP J!US4sY^F:p^ PG<ymIo|Lr^ SA7b\p͠\p  \ۃ~90(_pj:j9`ZV*%|bJ@`„ j;qk%pV 8LnQ(8JZbW^ E- udn-un!Hd/Ϫ,_O,gd.Y/™Lυ>YLLS)I^ +n6|;s_dY\\AR_G8/ST}P)LO*S\^.GoP^FORjGS>O1N]A& MDL8/.Kz)Irm:e XԵ~7 3pnr9%{T#0X_lZ@kK%XAu---Y,K-. alQPs-.Oh+Ip*!S=yہpw-Ao#N ߚ+%z\%xrO4:-`V&BDDbт-༖ e` 7/ข˸$n @rB<@)JswŲD8DwɅ| yK_*\9rD1_4 w) {8uXf qDLB (qKeYqY]A. }%2dP+p\yée%b9^L[q$\*]EB/<+@]ըFPFCHɖہj%;j1-"D6BQ?DY~T/-/)n"AQ\pԨᒏ#6rW|gsO4: Bg%c)Dp Fa3(E2m-3-nZ)*7P [}rJ‘^%;BEP`g-1seWoq WKW1ߋ芌KS<-EPZl:3 HašVDZ=n~vy(A)igܺ]#xc +دSU5V-duL 7t٘%E:TG_?m*YN#j}ٙOjQ]}YMdgg@P~ EW6҂_KZ4W3HT S.C0*t^q0zJ*..X\6}Ai=ڪbp& FDBR7A;O Xf%<"xV~0OX :%ELMU]S)落u :wnaˏ5 Wi?iVzx /z{=o˗c@ݔά ^R/\`R$6$imsuv+Ze6a e=ݍj d5$N--[e_aI}0 }QƯ A>2Dlkr0pL&uTwQ/«Z|QJ"|:|ȋƢ̢ H P{njK4GYftkS{aln6kK@ ] D!3 g:5D검'xo J 0*[M]S?]e┞!W%3Ri a[&/(yFAjԺ|vA▀꿼A/dF-bb d&)1PGpJ)  J74Qw"xW-ehS h5 wʈUJ<x5#:|uyY?KxVX^ UjsVs]bVg1@)]lb=}Th6pc+GANhIbZ~PWP Pٖ=M+^as0E, X-9tE*=e(Gkc *W\N ZdĘ?e1k\ӃW\J -ixߊ \Sḷ;okHa. >i~'TAX؉=?hI^*Dg57Ǩu;B}0BOQG,-cưq wCQ~r<|W)p)@8KPgJu-[4;P>G;0eY( ʪGd+>w)퉚bF~3[}0T,pRџpGX~άMdz}}P夿~lfqPS޾VG@%M­e >r>Zʪ}Hi:߹ {c(~.RV>ɓ-MIwO܈PS?1]KFla|XG0b+>-rFdD9p 7RXT<:<)Jn%|^, GQ- S}L`Y_Vs EJU %@QZl~ }F e5´=s ؄ԦW$Bt,Zr-8?rmZYM"WyىbWGֻ bc#.#~Y_"ܶYԊP=5=zCuFlVM P%i\ݏT h}*'k6aBR@(WR1VPf9WuUw#z]?-z6Ju]JU}GA:CzN~)Ll?pz Ww;,#I^ vޙgѠ_1b3இuLWuj(#p\ d9qYṯ8V E* K#1.XHQ SP/ E'^2˿ eN H-(zxZua@RСA]G8IF8m1x~GLw8,!Պj66zڲ:g bկ^Flf5͛qZf?%Uh_Rw(X d:X pʼnA AE a-kd:՗(D=t 0C'Zŀ_r@+d+e@c"texq2/P ͛f朕(FQ8մb XךLZqK)/qKWΌeoq Uuhj" ފRr< \8ӔTpo>0o|m|`pjm&jm.kM`⫕\Ta^x;8ikUg|&<'_;7W!_[\J8J ,J1pӄ Rc$b{?};5٫W1Q{MAW9i CDx;:R8hhke~_E : tEU! O4T/KKKDj⏂Y08 a|~W w5~kQ WF>nɻ`"WjM%-̡ܫS|ׅWj3&=ۋhTM0X}2YmKS jTCC~&w5*6/:V}H,)+tu vYF]`}!@Nz1q㭆•,85ø~u/Q*+^ԷPRf fٻ 5>|&/KrCDEm&]IЉVq7A[Yu+QZՑlMJ0(pmfA=o^ q8z L_[~Xc*U6ӥNt^ ( 芛fIqB>yTb8iᇖq5ɾ yGo Oo`VI̲4_sV5ƥč b=Ƙf%)j w˩qk\Y,= A|^n!9blw;j b1 row˨ĨG Ps ѥM8s xc@\F( 6uQfY4{,ٶD~ك?e&:@4ĨL %qWhRP֍Z{b{]VF[JLR4[uuxGy(~WDz%g^˘쪡F6$oa]N)Tq2(AIGoW,[;@ҮSAo^yՔ:}a. pj 8%nZMo- Qxk%͡j ̏ʷ<+~NKLtg0*5QJ2ICEE[l +Q蛉bTS'1V";c%G-̀˰}v½w}AX]!VM% 5jsEZk%΀Yݎc!vU5MWLnmdTt%U*V J5GZRWp7xͻS/i6urP8nVXU]+zkCWyK4c++47|x~!ZiP |*q7:)X\s_[Dzqؔejl?t7;o;xвneHÿ'P*D;MLS R V U(e? XQu𙾪GZ{b?yr, R M4 7WyĬȓk{`^@%TUgao- ,}nS|rSi b/@m0\ɽq-S~]ѿÍ_%u G@WmKEg߹USҙKabT u&cp8.XtD+a +,e\w01oL&S~ }kUBp~ L%m}oW`P)V>.`ܧW~z.*n MPia.rKiwr2 3F= N0}}K.Ԥ;7G0mDmkW"GkUW 4 R>?,{kKz%SagVJʠ]> Yhֵ/y-.-vUX _&m{QʣaL ᆸԯ6˸| > ű\~:J1nK4.߂,2h]Gr7οo{t DyE,L96VA :սw fYg}" v/RWAL f]S~F54~U+]kTۖ5fmԻs!+Hګ`l6b'g}FX}= 06`WHpqľo6G$1cÄ+SZa."\V`71+.\StƸb"SMC5y2 q"7u6ʨgRx$! *̔jU,p%&e~K (1,~1z~#hGlM Fw(u+搾# _ "QDeSW'/ 6E#/ȳqy-1=|+dn$Jy9a3HhQueH5Ƒᬳja0~ ;6n,lK&+ qdYQʖ |]C\$1ƥX*T7pw80[(Ȍ̳h$ bAn{ŸwGoٍzRcߨ.,(U6RX].LZEL Vw>0Z8o,;jz·ܷ<+,) ;/|LN C\:kPnVorj ppu61juɳ~#O4x& `zKgn,D89x K1gG$pile 2qoA*nK٭EQQ>ZmmWH¡eby%0+ UU`]&:4&2="7?w2:FXdEk ~>lf#5nԤRTL C2nRC\&m[|q%qoju<2:QdwEMnԉQ[95,"v}žMN&;u z] `h%T"AXB+,ZUeX %שTM0Ʈ_^u͏4sм^q1Y fSwrC:VV"7~Շ6DRZZ+֪v6~ƀyK=f0=ӌ ֍^Ӳ΍PKYj!u7.S.w\65;+^㓇~1kFeDmZW~ G(< PEAPdtQ1q5F *rU¥tou&oA(Dqkg M\@gU@h_LrwvS)" *aq•(Wy)P7l15gK au7C}fm<Ϸ۳Zzo/ՌyX*XV-ڭd/de PīХ 1% }KV.?yKx^kVPZKS40[:яˈ,zsx p+rcx-cNvR4+˯ϙF^%8S%Pce5P*R pp傹kzcgc];7y4Ї[%U/V[ \;.{A&žOiV1X*?h@B }>&u:>lNGK f̗eUtඬ,\ h]]m&Z6Y[}j YBuPUXkk1!_VLk-Fn՗ޟ7*[M.=o1-Y.؋}JFMN;ec b61syaƕ;1{~nU/W5S6x VԣſQɕG)roƳEZ,,% ƫ*İiMbк1˚VִM/ZҶE~Ql`LpT^^#A˳lffMhEt*)#u&! @nk)c>_qwQiE%̶2ěuUR[jbvjɢ۴SCupJJ}?xq߂3[H*%aj%x_-5/rrY|[>+|F6^]5-`^cFQ Re*3FmVG!$c S  qSV.߶rW Fch54}dD@mHipxxGXjm^{v@5UW;ګ%uTBřǨrrl ՔB\o!d h^D+==Eۊ`T%6B%@LYU \LuH+}ټ6ZP[Y 2ʊ^ꈋ)IЂ!G /*qʾž ܵYk)|_%$1."2 fk,CTq-.طGkî<0KL ],WB!d>Vi~"{$o~ayb*J V _h*%Ht~s-Fƍ+ь`KYF#t]0erͭzBֿ5%QjıuӘ+*S%KfHcU: IH+|fnP̯Xw#H u?@< \ GQ"aTB>ȃ6y,lRu2ɘPBTKJ{rS)xJrLLKJv_We [W VèΞ.Ee >}CL }D: Q>ORGXn[h6AXS\J[p,xĹ&L︌0ɸZQ,>a)DePGekshEJDq6!cpb'Ÿ(J%4DIYOQqlY,ER)Q #ru*J%/A-]^ )_4pG: &g ;LLEF$ j c܅f72 n$]=ܸ̩LbivD1!Mq\ȿu>!)GC>>}R(*5\\fZ[N[\Ih0(QJY=@ Cep-Lpn)׮gezSjPab{b9j$2&M/, 0>a/h`K\hK/-FʯUa0kYJZo\-\|Z~z&8xG_=KPK4žG|ͱP4E2LP{`X1C:BŅdn&B8h63,壐d!E[$x-zK @Ktq.'<B > g]gIl-@c T&E,ai -]:Mݕ |O/2|/1jynDϊ@ "ȕp Kx' xdk7AjFr)AN"2%p3 2Q5,Z" (- 2ȤUfP 'c a,Ke9+JDhK/ 2_REjYjjZ+u@8Z/ \Զ[LׅP.2K*(J@(,)\*d}"F0GLAG40R dM%hkjf)>;HHɝ{m2 E3Q_ BItZ% 8cK+0/W#.-[#(]Fh͢/8%6!q%gUI[q e3Jv!Y)FH JD*A5(0CqD5*J0oGgGpjY4|^"8#w"zfXbX\\CQ+0QEfc;a tE(rƄW)+ ^GN&C{!!cY,+ǘ$YkjYF7*2*HJԹdng|)6 *6x XX7w.ڢ7Ab Q4B2  ib\ :`ps^cl!T d 8(zTgt->BO}|_tx|go5Ϫ}RĸT;)a{dSl1Gpkl c\Jq'|f]O#\\c$J±-nb,K2h%fcF ,`QB%sbSuŦ./Q^AG%\9oq+jZApB/v$hSx&] j !nN[;;MBtpx*laqq X['Qeq q@\_=xzm sG| G\JQ M2C8wh뀸k5%MpJ7Q`R]D:K:rK0/@<_s=[40&偈f0{f+yuW) wn%oq߉DS)늾6;kaDLxik8F2I*-EP1(o p(gf-{QFbe%ę8尖L i qqP^/@e3PmDD\0"fx"rDCk$Db7BQ 4N()˜5 c(PnnR%rKQ|%ĨJD2*i6DrH7߀Wcn-ʸ9LÊsM()qSn`Cp2Qc13 5®.*U(ƌ[MKs8$R_/W+5@`@Q.p7umK5£|$T i|W-Y i0@^+へ8p.Oa}3A:]Dzf= &P1< AFE"JQ*z0JG]@ʹUW%D qW-d)GښFrK8.(/rܦYLr(799\;arHRL}D;+S[-%qU\_RϾedWs{cE\{a힆WS$8l+RD* = dL!>GTSWH=) \q6.Y\Vx[*@2H0 P+ pqO%>,)7ɮ4p*/o |-xK O .R!\ 8f$Zn[3)Kbɑ)H}`NJD%b%z*a.}3>zIqE♸')}#N]]SD>>RƧP |8s%hf<-7f Se(JepQ YXr,<WG0'b'J%JxpRJ DQ)JJË$[---yinYYIXVRYI}Su>7R)}qϯ郸_*'-ԷO}iW/;1 C~\pF 2ˆגC%"W@S.FC+Yln[ \-+D eV\PwA1bA2/)SzX-/u-{bഷ k----/]^oۃ|i M7o߃:n &þUS)CSU d OiJ*u0C^xN*!1 AQ0aq@P?ruSJC >Q N.ZZZZ:EcIKKҢEGRh앩hz/qY< `A vQFY.rYXL=\9_K)T5=\Fd[rv H"߅fϪ#vtQ=,@ GKpy. x5{|+5T[O|%K^M  _U=7q)K^'c9Zw_HpJ"-DQ`Dz3Q;.[ VԺTnZ[p؝K{APYԠ2,;)(T9J%9 qR ǒoKH7IAS+Z1W7RD“r+wxSYqjtw1œ+-NM>8-K{YZJ/u*1ǘX+3+iqUo q\ Zpy%Ĩwl d[|-ETbˈvY-NqYaIIH7]a'8γ TiU5xoa%T;7P6ׁ5b!SQJ&s̬ZmeTw,\>'AC%GAK f{w, 9;P.G9Խ_.ej;Os4Θ-ùH(6~ F px2~w{*0a+w5p˔!c(2Բ#;Vyx]}1*i.Tc)Kej Kel[i-层)wn?h[F4M=5RT\ܩDn&C=JHܪ%MdIA5)=8y)e`ۚ|p%ap{H8{D~ޠW.[<]a.%f@+B y%Fq[˯/qե1+2?@/Y,7nN. 4aK0 De2Sh]MͰ~b'q8nX @&fϢj"QOw͓~!:ʘ^CKdjZ P.r%C v@\%ļ,-5*1*zܢIIQx;n_Q"}@rWDX>hk_KGqe%E܊=Kg! r7'}EE\5uvrOy_@q`㬆-JG y-Ao*hsì*Pb-s^r)x|+iOx[ %k.%H(\ ={~ YOO;;[[-< `e^,"Pj%@Id%\`D+)"5hRBp,J%e\Q^*,i׍.q(\M2nQ6UƼe^A 7<_@j[H2Xx s$"T[-ogL߈\"noV[-j+/R:GTU[zM<SQybw)) ޳D-Fw ~l4yu ^אj;7lmp,.Rױ@#l}G'7szT,z#?5'T~n9E/OJo!OHlܟS_Ogc%[>=&?͟b}RS@}EjE SvR 98Y8ţfKPn:~_x1{=vNB FbnMH&ܰ`2s KEd-c(;D ]P!K'YlY"_|M|"~^Fԫ U0.@n$. \7W1]Gl54A^&Z {ŽPAjZV9RTpR,(%#~,h\Q)XRČ"{9 / jPaudkUu,Ը5.ڝr^4lD=%=Qd+6;. hE/xG[=I F^+-|\t\rف ˍau/x\_[uK׏Pu <8)-P @Pz(X{M!5f(~b*1=.R6TPwZcWuѢ]9wFXB?3=gZۊ]|NֿtGml[LXP*k.%q8! Y7v,Pw6Zc/Slx8-x~%),Re@:{kޘ+AJEWdbnwOpR_bR;GqUW?B_a[c61w6 EYȷ4` Y^d"A|,iw*~,*c}n{ܲܭdA3e ÷P5hlnRb٨K FPgYET}+7 *, U4-fKwq͑Ž͗W??wXM<@Nw~慀J[F6%W'a!Qv s6_vO!.Q-sx+~(TalGDve9i;KVY0u:">!DXˌ[e<Fĸ,NTBsX䥊J†`JFɹp+S~Ø XΡD!{`k JY1Q.UMe9\r-N< 77ܨ Ë d=<ʾvb\J#\`DޕQѹ~7A$FV#FW@Dfbg  DXp$ r, H 2ɩH ¡{!J^0ME~N'%W #xR,#_STH&wCL rzN]MprvVk f.5&TBv`l;A@Y!'[O*:hTA]|eLBbumQWJULe 6֮$CiM*h\LR~ХZ)*]7KPԬpZBTn mD9uaDRd9pYA5Q.r;p ¡`eG *F xtVEAy 1toP*=v{J/&}^ h u7-k2a!Rwv0S[O*kVMU@ob(nJ|MXoxz>v]Enq˸7cpJ>+pCVו/7SLRXx\gLqSfgu)DUv\LoZY @@? |Kw -~%?4@)\ujZSTG@-/> .}?ʚN_x ?X:h/׿! x-S=ě%!i'\QU#Lzɸrj8 j["*<d[f|Ģ0 F:0S;< bj^FUJe`D+7v:5vtq T lnZee+ c 絗X96;(gP%wbTapXԪ|J1Pbk!k Pu8NKUy)}BXӛ"ksgPҚ 1/.&5D+x_ `Q = Y .-Eŷ*n-@N5."Erlœ>%/ic;/|Hc,ıu)}:鿈vBLa[y4O (T^ʀ#*E`\DZ^.1#,XCK=PяSW*We9@X9zTO@Gff )V=ΐ/ÑAYOc+PcP/cdu~(̫MrЖz_BKEKQ'EY! tAv=xrcԥń#ɨ[*;9Y#ppQR9Fi-& e쫚ȹ;D3Wp/rWкmGD9LbXq S5y.D>rr;q*w{k8N) {,qrqe _0(*7.zϓ Gn` Xu:AF 1n?l@.V*'p;.q+Q^/&K#zeજuY;(+1u ^PBc"Fi2Z&vb?ɶ5tza*~t1F׀+[{-\u xVp怋l)JpbT[XS@X[ZG4%\Eɠ|b Y;{x[Utgh-qn3WqG3|F `kM رRu: f WXؐmS@y.͟+:oŒJ"Qkp/YfDv=nGQSRU*$6U~ (%Yk:dưGb"WbH-+w[o(ˎR@KK~ҊmE;xٸ,;-*60!>Qn[Q#Y[ԥ0mG+A9~B4ސܸm\q|V_h)eXil]٪ N`Ԥ `QRM0>Њc í 4 ˨핃c"}5,Ͱl]T b̰w. ?$cx5-*P<3>ax'/ y8#D%"MԾ* E<@o~됤>W>S6vNDmjm[sJżMJe1.05 C j,G#xŭbt~*l k~cܩ[+[4+Y.b~e*&?IZ 0*vz:ñ E%.Rn42"aJpك-"!T_dܦ(,Z.:JJF]yIA|ϱ]`}\_G6K^DX0hQQDJUj(7 6N\_)? fՇh x#Ԯ˪D4PbXTH x=.-WQpTOp J#q\ \twνTZ+q*!8A"dZÄ7U)%V07Cj1zkN;zЏ >QlU6G)@t҉J`EA:~aABH\ (}5S6fX*ö́'}.O;\9 Pr2r%ŏy-dq;5Dh]Qw G{05u* ZbX`ӄ!\Q1W J֣b+w؍T*!exwR+^BUD\[|GiL=!y ]#**.S|#%0*U͐nR1L\Բ<,7sDJ]3x ͝]^+KWVd&p:M2U*=DXqΠcUP`P)JjS)` !̪oJk *Z:q-`WLJp'& $e[`0%JasBiȶww,"pM "ٲ,nSX Op`Ӄ\0>eT3x˜4@DJ.;=arj3[G7,s%T(vDujۨU m?i+1 *CK;.߫ Su]\_+퀁ak_6k?(ǡ!?0=QD%߉9,e\Bv (J ĨjvS)ĬeK!z [f )HUNKE"+rsXS81W"$dvjVcltvՑ^-56F)eQQWܖ}VZ[9{[e$Y+pdu>Z`.Rvn n QQL< 5Sܪa)ó D۸jસR1oSsggC-|[P" w k EaX6jY)!dY ɬvArQDp9vUxPn ˜ Rä4 DwB1Ճߕ@.lG"fjEF6New!oP(,6/eRU)zb$ .PObY?yaUTU@)m񹸚.{>T۳.& #}k]Eo/pav1Z0M)U6@qQ " %GDkPo55KwPJ9,ЄiC5[7wF"\hT.xXn:ŒS{l6@s%TBM`hZIcWKtJ`Jp?x69?擳oZ,S*Vu;wsHn,#1G_f\wlZp e8\8(9[EK`˨`.*nQʤʋ@p%:ker"O Xבt_wi Dz+(h)P.:%Kh5I  s) X˨7-d. 741+H,- ^™bK,@R(%I05Ԕ%@ 9`[u%o hS> @V~˱"nS'Dk>wqEKf? *PK`*P0)±RNE\,_%Cvj { ;'\RR-p'RD_p, EWQ c:`D`B+5'55rUc"!mȯ%Ϙ@{vFy>}ZQMw&9O#%>#_d6`Z♩fhƠAO}ޙ؃z5; ?):"boDڷȁ5-f~gp+w%wY,͒[\!S+^ V[ <(ؒEpx &n "%[Wdp`6' dq`%Sgm**}۹ ߉菼~ 3 Qj+>("Y8=!1pmQQ WSfečNL$QZ`Svkr Eh7%d Tp5ATS9@0m06ktjllgDs`4S?:c055; Kw&h+KV{/ bzQ{L5#jZ]:ph?qu͡maЛ9XU:{/s'S`/RB[9Cpͽ]Cu sE. Y.LV)Rd,A>b p+RN62$Tl˞w%I^7(X VnwTJpv=ɹ^D;3FND\;cQ TeJ%8" Ĭ--[5g`YI|ʈjb.Q7KwKO@y vW@\)Ib* Gnek.R}ee=CpR ˔8tϝ7?(\ GȦ. @dL{7.ra%;P //ܠBjDZJ^؈HL8Z쮌oYvu<۩PD4KaxIpIdX?CDYL pL%I%D 'h.XxYpl4So VAR:\Rʯ+eDQ\+! F*-U %^*[5 V/!D" 0r l]!E$ԣvY/ \ О>W {Hްn-A$cWl}//SIiVďPDmx+BuHG9\BD>R̐H(kQmizeX2ܨPe}0ԽE6 3w{ϯwF#|m?=А|DR(`Zh7=e\>ϿQGC!95=X|Ox V G{NXҥ B,}KDkP1ʙQ`Z]`n=Wp47/lQUek k!e2pJ#)"\հ>#/KQl/ȣ,d-]A/{J'UE8, SR#2 RS8KXe0r6ԤQ1seq+`Ra~bG,m@m4!@g, Ř9+e5({;V|lOG}Q(!퀗bQ?SF~^pвY,g^'qR6.kZdQ^t0ABN+^4p%GlV^`<)|@---)̸P [; ٲ Re,6J402 BK0zE;)U/1]Z&`炝'يJۄ#r|LXEQOQjkKy-Gn}1^[Qsn?dq_SLY  O>fko+>>@Q_SVzTqK^ CQrZ;qF8 ; e1W,䱄 bځtgTjn]8JbgMM xy#PZp&(R\Q$;GPie1E DzlDUʨ ?91w7r5t,ȫvR7L]i"l%R[5:r$EjjPEnmPVZ0n-ej -KgqolɱI^y`lEiih5--EZIr E/e25w6@? qZQ;DXZC~x%(R MK.)͕:D 2\ Nkq"#X-JKx0wN%%wԳ“Jx ˎ,`.:S@-AMAԟ1`xru),T.Υ9Kt_VZ~ej9,NC: ,Kw)ӑ`Pᬕaf?0e\G]eeMey:„[-f~M9^U#U"bWDj\d?0+5Or-aw7fQ7* .ClN.J-$ׅPRD+ԵddUx KKJGmD5+ U7qrJ6bn%"b^"Tn /dJįDpRZ^["T{HTp*%_e JN@X7԰'l ,Nv\["QܵQ.Wlo.Z:\u`k[,c--8+E256 7YرUį4) ~e\!5)|OrPwe^57U%>!6rqSD(Q1Ȩ,^ll&7PeLA2A E=^R{e\ 2y6Ao:d{%f)K%jYq08F0%3yOadY A"9~04s rؽCXA(PNZGYAz]MEKcܱϡ[ f;d첻x Dwv2ũG-dxp|+kkjX;-P %`j]ԴR Ժz]͠nd.l*Ku"^{Jǿ#j 7:xrkxÌ-`Bؗ\Ed 9a;!q+,tb #GRﰊVE@+ApkKLu 5؄!ȳB|b,o QGO sj,yd1eY=bߪT[!RF-\6-%* EDQPD0nᔽC$`\Q`BWb-r!s!仂E^]`nc V߈G -xAd'Qj Pm<6C7>'~cx;):3[p Il܀įJA E2`, ^PD)UKZ|K%E[-!i`ܩD2+.VjPLGd-=ǸԪ~suTׂ]Mhqn#Q~o#X% v&i-*\R+wQԹ%=eIJRuP% !lbhAoqnk(5~+)_x[6KZ8y>7kq2 !k%0J:g"!d9#oLj|8+䣑@d]*,>YzU9FaK<6l`iKqw`N`Ю0&!~>T~?X|o}>LW}y/΢䷇s^-re-C%[xX9ΣYϤ:R^{m.^ m/r5 #RQ|RRS+m%{ 6K@6Om.0l}Yuqf. FT>=,a$_0Aa.;;Qwas~ ۚ+r{Yo b]ЇA"=&?Yɯ_rx+q(\Y`'"-a}E%|,*{Ej^ '&j >k*E^JKA[ˊp1B].YR̼ɴ~3+>WR'*)/xu{yu>;pwb[wC[l%ڊ@]KMS\^;䷢vD@ԿIQ) KBVV/m|N \5-KK}1KCp~`Դŵ]!ؠԶ/*x<;-s)J0- F$*DD'YW ͚]GBtDVx%#*|J̤Ķ=a5D㎾gPlle'gX `9Sg/|s{r ͡.8M'6Ć*= R*zS ~&!1AQaq?>Ȋ%8~0W}ԣڊHċ48Epf}w^0Py{DZMqB<G{4 ׼y ޜ $"`k, -0:aGs/8ޏ,k19b^\&Eq_rģk9[MYxT |eqwayEex\ %7=dg|d匒֟9I87y\ k(i׌X>q) f8Sh:64ӹ_g^6`jch71fp.U%ٚ[N=fDO)910Võj(3/SFفe&ajDa&k/G (e ʔ@:C%Q;yyj er`q'@V>%kʚHkwee)捲>[-`FɄp~5Pdx#A2d:~NKAC1QGox б"3Y !DU_ vXE?ҳI]Fasv̎ZcMm^p#MX >2y]7 B$lr&s_mxWY^sCϜIdOG՘ rxضGv6ʛE9h9^qwO\ lT/X8d_|GF5jO(q3ObdRY5%sf,0.,_&S.mS`-oMDxbVXrs$W/o5i`d` jw:&{KP@Yz p51̜'5\BK;e5=CR&>DnRb^Hp)5oPk Y&Ϩv$`#[,#vrb 8j^Mum|~>n؃h/2M'Pr3H kEִ`@@y GnTxe q "O4h ˞0J\a'N#ui2тI02;SPEC;0:|dAyr{ 1@,0kd+=+ʒ|LI ъo\#V˭9˺ bA0q5S1)\w‰>_']5ocw_#lm.4ÜJAـsɚtq||SK ?c'†B\A I.0,k\ox wd|Л dw˔C%o{  ejoL@uXl"ZnjA'ǜi91% `u`TH?BWcBFx=wnU ?7 L`5VJ tj5)hi:ĪW狉6` $|[Xp?H?+it`TI<6q2@ $P[{yرט|Y@T͜;M; C %@~H-y/8|lon6}1BA^2Ɔ%憜m;@F,׼T k]M _Z뇬#YawA˄x/C&BD-a@iܞG$ӜCofnb$<Ao--m#K$"F^я6skL|)ǗsJcaӉfȗ\߱F9L x f8ʸN&qM h^2 xq:{*sq\wx*s8qg PǥyH9N|,eMj`?bx p)+BL [5ݑZa|o KfÃ:09S]&$J'iI.W灗& Me[aވM\֧5`;gA ׮Nɕ5)w> H@ ]e sS~܄mϖiI0ۍ\I5I2_[缊A&S `TpUXLvx:ָ%$h|I0o[Ҋiz:G(_' >9”Uωs 3 x]}? @:Q+* ~0Y+uHotK 횜’ =$Y:tM, h d1&]WnuBsߠ)8gq&胯9Kt~pC|`;C*SS0i HWrQpSG8/7z!MDZxi(gnB?2;״]w.NUII}r4.&9tia8nE1:c0;tȌLZY//(׌Sy .g!Gݬr`k{f*9;u8o %)#x' \9xJ~08)54#"0}-7vK3uEPܹ98 MNp{p7]u+l^%_CXvFkbGt`B9#~|/UfA Cy?GEW04L5N?#Ud-Oo`8Ƶ>0K^}`2‹Xx`Bb./.z9'pl(W>p޽spf!i:\mLcfl0 M`ow> ~F75:XM&8O\og;tsGsL7 1pZj1#]1p~WG/`SϬRS0V>1޶?]t24pa7P3?*!K5jx7rk jR2^2ϥVnߛ a 8WN,4>ثBp}p)JYp&j%"ۅT6pH:MGVnuѯ0a*HrtyBеw%NjCL@}hEg2$4(4` ޔwڱGD ? Ն%ל4ܼ:[51E<{jbGO3yC9.@Hc$ mnCz:5rY .'F үa:а يJId^NqRî uS/wkwQo8[ц;pc24Tdzٛ<2+u.^\a_)t)XIںtZ͙{ ]xר| M{.-_Is@\!to74;Ɂ˭utm󂈚L6^{@\Dzgu0|ͼtc8 1xqG ]sY; {ƘV78}ʍWw^{qA !;1ݯ94s%B\n _ É#qww|F(my(8كClPi[D+e.=]0iinp X<~5Q81fh}`~bd¡9xm (FϩК!\, >[Ұ36$_~x4\nFEVk uYarKI.l ^> BdeGA`@im4 y/WV^]6$:1DC0Qvç!3@`?G/qapSᬢj/YCRql9 ZqY 8~r'^; ˌ`ifp '|ZnTS'Zq#b`"% L~Ib!}Qxb8Z-nxf7©.@Y9? bXniߌAev>~p8hp{f݇e9Ժyi8jQJM`9 c 8D1 ˩dǁF /| jSI'(ǵHq İG [w|U񹻂ʺs8OFZč'<~1G 3mv)0BS5|T\66ᨯ| 9F΀Ʉp7>=eCM<(^?GƷ%;<'z71yQO20iGat"i(w:!t\T4&a5҆jˁTb *"Z]j5P-BˋFu@w ̑S繈ؑq Ycs0w0v>#ˀlIUK$0Yywq)p?!&L"s<}U^ G]s|Kx (|qs[كCN;ȼWA4Yq N@\w#ww'U3Y x3Qoajldr>&!ǀqXW(uö66h"p_880?y.ĝ\K5t[8Ix`^q.s-ÜGYm ۍ ,iT"I!4 .wKדm b߼U _q^lC笪Mw5f,!j;s%j=\2]5~KL`$yj+ᯜp`7f)sxI@#@\`\ ( ; c#@ ~rTeӄR \4Јoxc9yEbNAO^~1vՅIrK[6r'D}ࣅ91 ;q a6`CSP-"_. |ۜ0'Y8lx/gZPnk!"/x3؝`A怉jϡav  tO"88*HDÜ}q^G$ӔcLwz *5q1#Ngu)B3n'(p&koCY 1 s93{" lOOY¿$._c;`f>w)!"8~@ZK6):\wJ4+ )\NPOǖdSmXx!=gfyǥ5"5DAֹQl;x6Wa%N -IxʴWk0DIƜp'`/=oW(v/I2Q3\g4#ǜí[UMy>0&rbz>8ە\W/&c|z4!>71A#p /Fn/qƐ$8փ~M;#\39+*3|hMWx5c@,(F}`JL{^^sj`˓[erō W$iEsFfv1#>q T(!LTM/޺8)Gs6r7/X.XMuPec} W5v2G q$@NWݫW O;%MĦ4|Jpn(Akٔ4br$CJL /9J8!qېD5h^0_3k1|kKSv^b5rznkH٤!ښe57SS߄V\Mi¦(4.=*i|}U H= G4U k<=Ew׌E4BǜY@Ytl -߬B@rOoi$aS|\/,U~ZaЀЏ툨"#4y&1@kΟxgDJ 9 -%A={b/o!7!:)yg9d!NWXT:`#<̀E<oQ<ΠmMlXCQ`,|8+@eg y0 }hsug9vbv?L;\Qvˆ /]!2Û.i5$r+tې;ΟT4"UǕla 6 [Dbe=e5:/zC޼9f3۬;% (#ã4">pNƼ^WJ6WeMY'tey8ftp 0uKƷxC6na%f3~1iWW!}h|WXme*<\$qY)fGC==z¯8MeX0 !lqǩhn~FpREI?w/>qڙy;ٜbѮxݙ}Ed_nc%\8tJQ/s6/}CABI670o@1R5i&$!1u0^n>p4- -e(pG[0aUA;Ra ,?OFw0Ɂ67:Vaª>upql]#KϜ,{h+:(z1.bmC-axj ;98qlef '- ;吲SfYNU=5*r'X!R&(X\oBDR]`Q{!_XHp$)qHԳuXJfj6 @^7 ?8Fu"1 kH[ 05̀d9=ZitW1yDr 1̩z\ߌb+]Ɏ7Y4 pFRn$_. w}CXi_xrje>a\Ɋa]2!9笨XI/18˱~+ߜL^2E]fX:^Ʊ1u|j&I@m&7 x~H)fiaKz*lG: oԘ%{y?)|__\;e⅑ZGW @'4lOJ?8L! ;1NpJб|eF;tP ǎ7sysvZt^pa@|8 kg_sxjaLvк[w\\]P!ÄAԣIcDT;8UȞ'l2t0^شx.I;pU}ːy>0rd?dr#èр $jaLѸO{Bi!.rEnxhq}ox7]7p 4^*`'Ϝ@ 6ৗYgޜۏ)jkɂ[]a95*:+ /{T;ć.݆QcCj"OGU?2mKᦘ\ ǨqO\e{T="*y>124'Uo*S/U _cuvB1͐:5 bkxX\hU%G%_y4 ̂]wDgY 磜 !~ܨ#\R @Wbf.[*(I5n޼B #GH4wΒ6fUH@PO#=ҺEH/GB6rbjw;{|ܾ99G&A_. X:=fwVy04`聾gyϹe񄯎Yr4lUՈwJho>b?n >;8MqPXCN5Hp<cô! xL91A+|@Q)֜ 2\% qv$X`Q48\ %S,8c!&m4}0k?\~2α'踃c|b L|/x-1;qawv vmqTKC>-h!}0yjX!2-O$v;<`%Rr j*)O[ĒA O? 84kfq(Rم@ :F\ R@(?#MR Vd'T˥ 7[uSe>1Q^0;*468{:lcܡ.s78ž`!Nľp `ߧ4>D\~2˚ʰs%z1L@%<.hy{q>1LZK;}2L*Ƅfi8o&7t.X>{(/M$*G 9k{qt)$'yx8w>FpWC43:e;G pY7)W Oy(U*$[BZX" 3`[hNqΖX@`,PM|$E4žxrilOېUxp4H`H(297.MmߜRQ ?QUM"%>opHJ3ѷ4;ѓq6#%;7/rMlyuXoXOR+] K_ )+M]ůk^٦Ѱw^ O811I]f-K_c|`nu[󛶀jbN H n͞i T r[|`eI9\QH,Z+f_@ud`u2* 7ł-9J\MhkeڏYL98{#oyNG3 ¸u:xwi^Cg}`8)`"?8w<̑l;8 qso&̻Z}bq[%S pbTrX^N^p\nt1N'9cWL*Ͳ[xOyP63}|%s߼H/ #~4>:48 ^]VW*ei[g5eR>pR1iMMclaADvabRE a%9&%?\M+y,ߋ[_l I0p翶 hok㌧z4~1GMJ1ZFԋ h< 4`DL`#oCGa7X,(tQU->xX N>87զJiмˁDG@Jl!cW5>thyVq}vd9aPS4Vހݹbl~uD5iB H-Cl*v񇒏+h 0;ܜƹ\xFޱL1{,&QiYpTnf'n,8|dLrP1y4NZvLޛ Dn21nPo5 }5h|઺qp_ Qwj1 =07%/ciˀkA4{|8v*ZroaݝGY`^ι;LO%ZA| @CG7'>8ſuu*BpMv}G89Q[ !1nI/h.Iߩ<8>?@jqӋQS bNp`v\^5 ç<]lv#xLm5˝fj\m^ lAcjX:ל' . #ˈ#M~̯@zͩ9Ŷt14xcns{P:ȇ#{\,;w !ȳ+̓VqY@ (oF]B:(أ97%bA}?|Dc׉}i; ɧTcٿ\tY?x=ŞpJj霷aܕ(܍]"?o6Ư:!u;z*S]S] s|-lS_8@~k훪 zŊuڟL#9l`,;7KFH Ӿ9-;3^ H̠$GARF$7Fvʐ;/1Qة?Y,sѢ;^4UQ=Qp [8*uG׋{1ZT ^^)3 o׀-5/ ޾|`gi0^V=FI]_|&;_ک4ys[l Q=V^PSތ9cͿ)M:n-Iio]#HZ-=&7? #&Io98z'#Zg4u˅R>6Ww{,%9Z`5FMɡiH?m*XQPa䱑"2"׍@'-\'&w F^]'\< Ъ jlF 3t6n1ɱ vN;mbqVN:E=}pXds6pLtr#S$pSsgOѭSl $#O5inJ"L)nj5N]Zq3hލLg9Vtˈyee  unG9\Rqiu ki BAI n,8h 't6U[Z\. .:/:#L7 p9)6p}f9.y[wz"sUAs <|[y~d|6yO(|TS`JMlM2{?bv/rE 2v<Ɋ i[ 0YbץdaHmGLj}|b^̋`$~r0s 'óF?R)zgH Xo*d1^⠝ qq֯26ӯG|`f N ZjbkTȅ vӼvUn`6zmj7xx!Tw+6"\杲R yo;en 1Ps^πg)4\o;p;+dhLN;pMxA0TыI|Sah~pRY#X3J\z -֞޾2Sk{יִx4GAbHU;pV-!AauJ qVpd|v\WdqM#PA '>,֏aTlfylZ$h8Oi#Tޟv0ni~ @&[C_2@P~? UrĄbjsps:a?v\T)`%A6u~K6Drg }!O*!"Z&ۭ~3tiu[tB(8rԘސsE@zڏ@geeyxɵ7.VS?8M/o '"ګNGc T@4)!y|=˒FU+JU;G>p4E r2hф`܅0e@vCaA t:40>,SƆd_MS<8ȴps1G0[f4a>?>?Ualsn< i9aM^Xs)8>(W ?B4AQ%$"S`I$zߜF4zkQIbQCiFT+k?kaTJ:w%UÁs~cŀoUQN?h1D%-?.\wEmb_8͠C,t_ņsr YӜ-zyŲeBoq^<|a,M떴:/y:?u(,%Tt<>PTdmƽMWΦسu 'UR-rro@4%4?MsTvtV|GBx9sa-<~YbE׍ P^N=}h备?O UVŦEO)>٫Qk,њU98u8wR- ͍طXʦ ~-8oZsm&ꠚa;>ņ]@Xh]/)Ȑ l0*ÆO\eiuy*bO Fk8 Hf#WF& ;]bd,].T@7< o(@؝2$ ,c@BszE> i xM2k/-|`P9+_oy|^ʞG;HV ל*/xK2Re.ևAs!ha.43voESW%Tmk4K5ɣxKTM>BR-쿞 X\k hzqggߧ0#F-;56؃Tj;n!''Usa_\`+d`l~b]7` 'xaM8 '^bפ涵stvq\9ZN}Ay{$8)a? bUZ mҭwrQ߽xlSlίhߍ}3 i]~poX 3O]:x!^ &~2lhkD/󉨫Z۝lCF o2; Q 1nHx pI[=~\aO?-,  lސ^=yܪtcctFFk}25~3l̀.LRBu#5Q'*!cCANFT8(A*:/fF̀sžw88!Bls+YZҒӽ~!a'IEnqؒ+SGu.0Ɉ oDrF@FB(J9^28"BiYPPjqJuyq_ ٚ5>Qyz&(q ot.Mkx'3]P?d9S\!Va*5itV%qK"(N,܆seH'yY/I@m: }MڜR'b 9`x`2b*n汜25@ں9--yA2Jٯ85nb2_0 h謧2 }p/'c95ߙg iZG^~GWф+wB)3`NpqNN!|3ʟ@Q?l]?,Ӳ@HIQ44U]NE0;bXM$3Y`"Cq?DH8^g=@ 0c0#nQu caLAt"Nh?_ٌx゙*^/`J fnt83.(Dsvl'064Ƨc}pUW_Yg21F=8m芟j{lL >o<?|2_+n6h0tXt(o.u^z?h:r$J)767Ϸ R۟@ 'sv5fǽ kJ0$RS}*Ei:߮UnRL>L[(yx#AMr>1ŧ\Tש #ˋ'L@cqx-VU=hp:|SjBXr߿>%y eu187j.XH%}y`ǃܚ1D|{N[6ܘIBq2ISۏ=aZІNv})?Cw){i {x )%@@m)?9γS؎{'T+/ 8[h^.$ _!b?I _G≷K8/_ B~{x?}H+hkᅵy&,>&n3/99X~f*]?^ xg}C/(s |.\M?bod㳼^4wlx(ΝeUqk@ xP d]aԨ6J}GRCdnE(\^s_p'9 ^2'/(xOkfi.ɯ=? Pr}EpGVu!BU]? 'rȁ^q͕E忭:IY)M 3-B7\2ۣ𸬂k4Tf fĮ\uz,s\W0۾5Е;v=U .(RC,vϟ"rQ w!c-As"kܿEKdSO~2TI` >JʏB[='.wt D oMryHpC`D@q=I<"$xlpceKEˈ*,Q oFv,Nne Ͼ"f>n ,t܇чKvQ,5Q*]a.7Ñyv\hr>0w/0]t6%+N5dfj(VH ȳWeQsӂJych6t|a*9/X-'!ljʥ G j \^Еq1eRe8Aq&p))g[k~$_aّ#t?\9D~7I.*s]o0JqyABdA|+Ό.I;T`J:p)I;Lʀz`r\pfȥ q/A=?&(׆cfaN$0HV|LJ 喆>( zCf>4B%UÒ~Go;`'n4/n~og>.Bq'ۑs2$|\Jl> g-'\_L:m9IpsG$Noaߜ1X7˼O{-11:U}k*|Z63W;ON~dSalZz+or?98k1BFS6 "l_#rIR^ DYٞ5it㮱Sf)xP~с:び 854,\Ug5fLLT:Ǜ5 .S>NC.lk@癈ـNMx5=ۣJO{ĕ֎ѿឆ_όh&7u?~DAOJl gNfAyߜTHwcܦhnq8:/rM.Ͽ黺_G2!Hm.Xu^7ړp%p;nh;{ԝ`i69_0&|e丣ZkDrCNsr\㥗2DM\L 5;yL8:pdKAyVeO<l#v'0GVȧ|0SºXqtnj@J?ےӗx[QҶ('֚7zY1FaT[&qPYcEu;El /Xf߹&(+tȟz#zޱP\aRch-~q(qj R]3C }g Mp=&hxo_. u2D8h7L/]?\f|GCHXM@i㌡UfHqZI wXLj  ~m/TysB3s y=Я&cEPxEyn"WgR@Z-6'YT՘W<_S#agrx(NkO^zl8կ'CT]1g@{`[g3'W40“̊A~1*hz06?8ɮ|o*k͌އakwCq*t7$ԯ+0@׼1eJA>߭hwsfRͦ@,00)o%<8RN)z;9 kg"ȊN[ST6 #K ^CU'~#5I+}$)G`$n$G9R4$*mi:N֬AG5Ê9QV2[,X?@=)G4u9rAG K9;cH%%4 #0x$z8Aޯ۽C򷮱|D8<݀tߎc0+D֊' TNwq>v(;X"_YS҈^+06.V-wrIN[U|AkJPۊg^7qxmzȂ> rx-Lt6B{3np'WCJ寭aV t;! lh:u!g xa~8Mw 1j¥wT\97Ń[0$XLqK,dEÉ֨.-h>ʂ6O0>ɼWr_ *(upu43E+X7U~)`D{.e8 mN!|7ÑqjuGwS㬍bW6Z Mqq ȩD:@zHKI EJO!Y܉&}oc4ܬ s_~ƐЪ;n1f\Gz/2{a␣07oCr.auveX'P^"w|qd8dD]Mo(q)9F$(W8%Zwqbv=e97=)7)43a9u7-_]g?yL)'MpRKBv.1l@J9 UW?,UGEq` MXF?9 < ^|`,ш;>#sC(>'&y#f/2u#BLSIУםiX M?S;`5`գpJԝO~r;4kH҆=\;lU"%oan*m#4HmAقKg_ǂ*TS_87tOߦ;CpTKz6bF/]3  =$[uM"b$avG=G'ߎH% |_8Az]^x1=]I5MHM`PON4xifPCb5z~syWQՃ v۶@wg*I4"W!S"&?8fn7^U:Pa(kofG Z@x~2!.<:]bG__*v6¢yQxɅv4l~Ppz. .k+{UviF떍CÊjlRb&sP^u}bc"jpB%=ol@,;8 -GRۧoY$[ Ox56- 9Y8N: ]u}:xtM0bIHGQVi7kFD AJyۚw忦F{8Mv|JY1 Y840,t |c]t:{)?l56}.h:Ma}6Ok.A[ӱjYD]v_ M5  IBF醻6K P8]R | OFO|o m$:t8ƍa9C3LD$<u^I%{<9Ad)Gv GWk|aZ f O k&fbCvZ/{990\a 8f`nk[@ $y:{(Bxҟ'b jƫ_,EGgg:-e6<s?2I "LYo14hG!CtS;^F|&# y4`F<ʼF ~U@|%DŇ@yژwyG\"/3XpH{S wx;3%fZ [/fZ5bCb8Ì,>{xD1,-zvYzPO_T5]< .Tu.뙛}5ꕢ;ns(asFi`q S uZW 1Ҡv8,U"}G ƣ$2%ٌUY!Vppj\w( pPsVz{ 5bshJA 22*4^$C\:Cx CmqJw8e!H1DٷOX4K
\rǕ=!ZvU^r0UŅP`6! =ZL+M,o~q{K%ۜQ`FEGptg[Qb|u)U~"u7*G38嚴#ˋӮ7{ON. *Ѿ:HL{" fHD9ͪ$ 3 z\ e M=AqN;ΐyXXl cZWӡ0-5LrE8GH $4{ ˷:uϣ e6',Nl)>DwX"MlAGްUXvQzFzXSkt’bOPD2mM Qx\q p.% }h%(NZޖXGiAٌSX@N2dy!%NsEG1JC8lFU0QZAHB p*$:K\Ta\+E}#MBbn- &= ;$ߛk&`&l[z00ǛL9s(.-$țY-wy>@,ȈUM%]9>KkjP~0 `2̎4`OCPUVH8yRvoX8&NH(,Ux6ՇrW7ʕAMf;+r Y5WW VCJbڸ}qU%.P8E9#+sXzª #yo?2\9!y.sX8–BÔyn3WSyp_3΢swxO+|k7P5\4)9_߾REv_@N!8o39߮q;,s$UL0Q`Ʋ˗G.r*F)P#mpŏ8(tG0n&Bӊ,o5% =%0;A*0bjj%D9oeŀ-!?3(:<5NG_woYx:%%g3BМD|d4`iDzx-aƾpf6C~5cJbs@Cg2s8 ]5U9« KPA|b1E*<3SM WP1usPf{GT YʻqIp9I,hz!6󀁶o]M0Ri>DL3,;95~0h`@ݹAfƏ9+AS8TN 8aFwT[A!JغpU-$\H.䘚4/Ac\SGdCXwAb Zmi Q#`k Gu$ZL`t;0CB,ӛ Y @Ep٘i$PeT؃4< ")m4L7aܹG*%4ւ|c=h`M&6,>noVa`C՟/㠾):TN7WwC̰,(j)VvtVCVPiNܘ{eSMk),d}:*V_TPT}1ĺ:ƍqp.9B0 "?8yqE-o4lwU7q{0z:ш6q6wp+~08Dd:1Ɋ~buǠk}Q(p[Z>8pq7u` ɂqHFmXTCO\._Ef?B crcdbJy:*(yBk|}d v k`ܿH9>KMYec[O/MLW7@jRD[Z'*d}q60zS4^v' -N ;"w/ ;~Qw\V֬uTP;quMxDoF{[8~1HT+h/Kb\&8AGH_:&-W*19N(+w? qĺ Cn<\E/Eŋc76 8b+X>Mv.GZ?X?!1xxC e퀊 pT M8NY/0l!Ne"*Da4(ʵZ" [>U<_ai =CǬ m|y~y̓KN 8Ye%/hS#H&E^S4q1-V_ѭ#]I9)-yW I+V>K"cWg00Fwȏ%CH"eȊځ4y/o;8ye0 mLc}uh*ub[ 7xd5k Н88t& {L`EGwsWA |)Pz]=;a4 9e}8c7CKaa?#g=jpi>?c$naj 6+[ yĻ>zY0K2ZZ8wSjod/}LHbGs#Zɳ~gȟፁ:/[q]7Q.i КËn0|(s}Xzh=(ra F]yYh$n4B! _"LDAG B! 9PeQ*uS>7-8T=Xŀt*r:oi7\@P,Tf 0g`,eqf ݗw[)\H`^ZC% !F1-c&rygui3qMht\% \_ Mx7wLH͡ztpC'w7/Y{eŚ:4s&2% Z,d H~9 *g13{xUZW +_aQeޟp1s|\&7S'7h1 QyAIVyfC3 >WmeՀ:|r{!跽Y ό E-btq" b8Z>Jb%X[#|s\6zpMw@ZrI;4G)wj;a׏񖭶 J,2oTI`C!LBAWF7(|4Ӝ2;Bh=+)wwtth- Ecj8LKf90Y`tvh %98!I M)aG tLmPouVH$ױ^fp+FcdıZ ZE+n\ڒ,l;zôcfP,dk=\є"l璳O`%VOaNX?7m>}r˝e1) ܟ Lb I0B; pLW)<|`٭ #9!$mciԀ4tρȎ ) Z76: U܋,U]]8X7] b20#@WZPxR +{1`9sܣii8 (i_𻗗XXb!I7d@''UeV#@H^Y<=U*r!S-xٚf&b\#;x Ǚ:ˁJtqZgڊ9_w)ѬO&c!PbPyQtxbҠ2oH)I5 J ChHF"EPBvn̍[X $ytA"Bj[0>v3{vg4/Y2J}xhlvWX\ Jx/9~q zIc73y ьk!8׹i?! г|^٥M8#X%*k;#O$I/8X56BXngxh8o<*=@S4f% FZ"ah<di_G[dH#hq_-@w"^8[=if f7QRM! ej8РI( C'|wɟk "6k=+!]ɬ#6MQ&KA+qZsY`є-VJH|]<& S\ AE:q^qY1GL* QcA˫ WTu$mo"ZJ,#-ҩּXRLGQ <Tv@F(SmU+u Z`ܚl(*ѴPy5TSB!B dl|&4q:8epSMx"'Ik3`VTvK6/ /4ռ{2F;1)E]y0zIƳf=˺f*:rk NAаzc ^˿XP44&9 >>8DONe>yфJo@ֺMU[-xz6zHN-KN4o TU!@a8.xgٱdE8vTBDyBki஘~_Xc`޳M)U )^*u:[rur<&&1stĺ3sHhQRu +gSCj3dG]{ŒLTufKD|οECnp9BB O߶F,}tKy56/Cֲ'8` (%)hw aqt{ǦFܸ'2\`&L-8|ɭy5(SHy & q28<8V"xP=8uuȶ ۏA4Bj~ 6'6XmaIaPwi^ޯaqA'u!޹ . mzMB)ͭ8:qGz#!SdRR0,ob_F^v`wzjj<`%Pqsʧ o;3xE_P Y|scQ-fHi[/F%x*?:"}<ξp/&q 7pɊ(u-N7i뛌E(SD4񉤂89| t x=)ʴg9(z`4?h0L\%Jy_{^88Tfm3SA_?n0%-RYv6tIid"etYR. SSCm::ѣ)߭:t#`__L 5/cqqp#wXcYv]sx% |`8z[#ox$v#z6.; AڌuDls0ٝ k~q~q(x~(=gpfr?h[bgo}phWn3A([8 `xf[$Ӑ7kXٴ#wQ'itBQhPdIWCwMA ;B&FŭѬ-]mb+* _:Dt CC .|vRUϲ )[ {"^L2^@Z'-HjҔ`\H 4 K#O rT+&K_& N%x0Wy`f'<\Fۃ SveR5 vAcR8My\TkNvo/&yXi\dN]qV:,=5/q.o\ xq>3)Ȩz7L@G4|Ȫ|d-8qJaÆ"Aůn/@6_Ԇp00*È\o%ɦ>4f{y05$=Xr٤{[P" HE?l X?C_f6vr 'aA<q(5'`F7u`Aek˰19ԢKA@N 48u.OTE2˲~v֍M[7Tp<@L_CD~Yp'`n62)u ``{ym5/OX K" wްyS5\vӁm,=Pu~YnN Bcfg-!d;qu0We#w#5a0yCK}1w\,' (/gHHΏ(OHO8z@BX4Seu<R)H?0DD1j$Hw*k$5:Qc#rIA:I-Va xM#hkk;P % +b$l,|H `y*BAz[6k_%d^& ezT{#ݭK@ jwf#Hѱִ"_ Ԗ9͗ӜS8:WXɄ7iW;R8ix)v0ݘ8W_@o8H.8lG!jP6}Di s2y{|OvΌtDt0dPu|qZD[G{o;1)5R1JGk4MSlJ90v?j 㗌r7bZ[n]{?{N S''xRMu{EGwE0#[o7sǯ\=8,0 hG +OÀ $061k׏sԱ'xB򆏇x-@(OؙkMbG֌v$JͪB9 h;`H6[B94Rtz«ra7S&W]czBfPN^0wS Nr =bzǫI≰l~8~1hH}ˑS6DOrkWup>KOYVˬ7imk:vb0*MLk"^7O [okXV\YbL|@(׾gBkY]H\T^q2yGi[ݜzMbf1R(묂ryD"mpy#nx U0'RrGTyZeє߇YxB"r^Pₚ(|B{-atNkQ_LoN? n1`0_5ǹSNY8[̡TZz5]Njلk duuM4B&GHNu'^n :Q(yUj}}aׇ+:LC猜E^9>0ܹZ Q~0B M0]Fy#RCWr[ ҏ8(yqp-FoyF *<41ڑD}0{8?'8uA'/ujtሧ۴n79ʚNjCj2r;ʫ:7&ؽ6ć/6pHka׭%JyZ"T(:YTƃ2~ aXK&suYg98(Xo7ESSzqnhN"7`[lPB3gLP($Ѓ ";\@gbF>3Š\un"Ј\* m~ho=g5nBT_!evyp9z͈ #DNsмUzqAp8F`D'= CƵ%: NMM1Թ8x!r7p\5^0n-5D|f sC]1;ɺyXC[ ܠ^ Oi?鄘isM^GTf%N-nTP;_:d ";8QbP?5)N:]n8__2ozL5Ӂ76KQn/Hvm&e 6^b/z&uS84~7dtpu0~As^uhjA,p⻻s{Ă$ع@$(0QvzpxbH;6r{ASC&#BPiIˎu<.70up}rNîm&GbJ+|vo9;kf;ooO Շ ,nuqGnXr/Zէ )Nn1Dqڪ2L$K`ApЅklQI寎n8QQ88’a8 Q5Me8&;r.N ~ӌZ8j'5How~C+}bT%lOgq]`G-ѣ4Of&]-ƖM˯8ilkp?%9xɅQq8Wьu7+*,%a:5z0U77 ir, mCD/'8$%&R\L~%UnЙJq;] \7H` kL D!Z% os[r@ۃl$J$Ņ6\)PihC@qOVn5t1:5)> C&Ln*[qw:N1z9x][MZ^1vjI-W^p4w@kֱ@9h"C&qaB>1k4e ]޸ OwqWs} Nf 7ӌZ3Y[&\؏\1*"{$a1{e:І)q̡!jq|j/폴 }4W)Xju0(oq g iّ@0$Hy MD,맞pzHE;PKK6aΰI_? Mwx lHӜm楂w%!"9|[ҟzP7딂0C)/x/ @4r=4j: a::[YuaAXp].NƸÁ( X7I̢7­ Dm#l@܍KfWI8"v"]Z?]&j4-p b=ޖ OR!G(tZl{rAbF\/vvsxSzH6Fwi6@jY5{`D,aCAqz {K9FTE=$S1S]5e CTi=+C+1)xe8hHUYGbcmY)% eyuW{` _YJ)}.s(~9slv enjK0uq]g'8~{_>fSS}8⼸71!SD{ac bNF`@+fIM Wix8:{)ۅM$oiǂݛog{"txͪWrWZ0w`$lbW)A1Ga84Grb uZV6çp(<}pC=]<MN[QrP<4q 3PJ2+\G!.ܟ!M-!vf! |C_~p}ĸvfv[7&?pET XHQ^w:j6QD$::fKRRfWE/Z>w*T d zVXGhy?{wɈ(&7l;4`BL("i#(*M<#cdh w =mj氀66]_&)U)f+ e@M."h-&E3=St/6wS|i#1.,ҟv(>y /~x@oA`u/b#T͕qǿlv Í]Jq;pVGQ߫?FɊ?vN.*[wH㖴q8 X&UFxnDܳ()0wю @xb Ic$!GhgJLPȌ"Av$'m w emʤѻpt c9|MrPоr`y_A]M\ECOyo47pGBoy* 9.u+#F׼nw=e8{!Cw?\R᷼R[^ub !D>x{ xlZ<ޭvL+'|yű\8`e HEfs2G8E{6=w N*Û,|[D-D7p,x)޴BkxDV4A^" rOR5^ɚ'[n1x}3tSF|?wT6&MPڔ3qEȢq\Di?gז(p(5eu<^'\zQ[h<(*,dYvaP$_:޺`0B燱8g ih@Hxt:!%0 ;rØ Gzb |b zIOy@0dLs *j6QŶNlW&N^ػ6ZGT*Jd/$>}R'Í5\[I1臇SRoJܛCLa6 rNWrR\\;9?{¥Vu y_BbQiZg 9ĠuLi ;5j.j}N!zMc(v;f.&^1%'m, Psaҩ.#^2[QٓK:t8 -01``hc N:8֜cqhPQ2L@51pv)\X  pO8Hq^Qu^sۼΒ>?g,9LջoY]N90t!g:\ѵɱ@74e5Ļ{SUk+yç2:Ŵݼd67 M$#n9}`E&)~q C;1V/X.$8{-X.17~p dߦ SX/mً+;uOJDB6KD"‹?ٛpJNx^ļ??X +~Ȃ6M z!D5#T>O>aǔY@ln!68 gJ0"*l0Y3Dyx㿮6Jd{g5ijb N0j%TѸgP'*:Ħ@$^;(y8EITZ>1ŪrW6PNq砛3J򞆼ս1z,Q.Oy5 xx 1H%%$5ׯ{ŸYMGBd :7]󈆧'CiD9q Fb0wi[k8}gmQB>qpv;ŽСUq钽T 4tT6W aiJJY">vPhh xԀ1áMlGx@{(;zh{5o5}ĭLym#@A9McO ZD6}ٷd@LЈd ܟP {LlЧA7v'fDt"|."sYn"XTo7mU^IO9 ${2 U:rEqXAycSnj' B5r%p-F]ܟ2D us`X׿H$1"Ug ^ 9ӯsTts,#<3{Vƚ]9?O$}M]` K&ywy ֥SX9zA1+$_l\-\EZi*-iN+Jf2"1xHm F#TvxSy|"@|S03 |i 6Og6PC,@Eжz966R];oy.o88VM>s5fP @'L8 ٞI0܎3~ZqB~ w2(x8\6V[4`E/ez`irQH$EG ʾqD%f'Ao 5|(H tfթy! +`psm>JƶGІ" \QWoxVrw;*+ [͍p3SG4֧2p@H"[lJR[a@vn iσy^ x m96<>{ss{d]napߦ\>(B>kx;?ezbF$ #K|ˎU)Mo(RMuN=c 4mGSw L1<7#bXc=c蘼xI\TFFEÓ^1OaI"1(%%:`x |kʧ^@Hs¹qtS]S)zxX]l5Ty\yew[B 4x,qR7ٯLSx4"W9eM^HR7DhмvqxG+r1`OD*VIᤨX2cQtju[e8ք U zbya6HZs;Oه=xîpMi4/m=\MQ; H"Gz2e`W@=<*Aom{0wu2 a?8Cݰ5b^{H?Cط]8N_)\ak kh?Nciy=x.@5}F 6!*&SP\|EVαw,r!Dt '`$ KCjЁa@MÜyEG H;q KDj8!la6Qhuu"pX5!XIc/:PvY^ޘ:V fp,gyz߷/{OL} ;˶?qwq9Uz5^{/b q Ja9-y]|u016k[xK/#4̇._9~Sو6Y{0aqط!pzCX yUH,[:6Ϯ0p.G =7|h2Ma2\o%؟\ҒV S`Xt~a#I$?EV ,ٛ06%q0z̀SN52o9sc4p0#R(/ “H{_|}1r˸JsF=IClƆN 5Ձ+`ۧ$jjbXƈThQ;#=Q.OˈT`칢^h'9@Sl'Ɠx:Ftܔt-`F`w@˺wXk(iaը e6ÍeljYWڴ  $ ه\JA*(f,ҠE)mI=ͣTrAQ.D;SwA | v=kb/:|9KR^vȶ(6:d OA;9{+A jy_ G^bZqQ3S7$G qSwf!r1/\;yrXcH n"/@P1=|\n8kKٌ a0q ! iof% ~o!({+ULHG"r&Eknn k7Z.)ްg[&nxy~rEb̉GgoأFn@LiLW +6b>V+Սy{芮i5~|H &-Xx<`Gi-鋏D+S[i 4_/XrGU)#% >2܁e7ɕ#]h\Zpb?޳p#]X{}X7!.Cq*xW;F!HkUưIBT; lUW Z6 ]MQP(V hAhi#$)v4Bmѳ;V )ٱ]t`)yz6 V!'; &}`~0hT6fKW F lnch߿p^%+eaJ#r %tۓxoZqy"G%q#wZan\)+V\[OA Ɋ40Ve"Ѱ&bT袼H(N3-i `J9P(y2.] YD^" L68Xr3n@Yv"»nAԐNhYȵO ǽ 9_kU#GLoTGZC! 7 t=ڗ ngfkHsl a۞2N5vHB$flB+<G%r$0nEҽ*3BXSyrր!zN}gg.=Xq6cs>z+)8HMspN;rD嘡Fx~VL#_.ZqO$ӗ#C8O̘=O,90ea1xDθ{ް|Vй -Z@1%uǴ.N?:B^?ES(N.QC+]] [ecDZ .PA͋xb<)y#'3X? 7}]bWn-t6chB2P/\U'M(@cv5;_ k jg!N?LBaN`.NZ;'1vq>ؒ>~EIu$2B$;WYm"_i/nJ(JʛD9 n6{ȍ^޵ )jѕ+m6%%7;Gzz l!Y."#(Ѹ4.sTO>PXA 7>{nM5_׋i_Ǟծ?5te57A/XLUy <h."<Yp&hI\ }>q#F"T ڞ6Az'*z 殗ڬYCs$uK/LRg@qEe+^9Yl ǸzR8hncK+i&8-]|a6.QJd|i5N=d-xknACU!N1?r Cq:5lEN AHz\T˺!0<+8A} )TZ[5hZw=KS)1[C8k5D -n` jaF158eR0MO.y[*gZЬei!K.DDzQR=*nR(wN#dmV1= ' +(OXSkMyu"2`K9Z->0M2=( bu'J_N$(>.0^K` ^$*$g>?_\qjw]BǙt$8a8MrWib 0QZo$ɷo̘l&%DxUWg8R &c$<" ME嬜7_!\,V6%o 䣤9L !\4\ 5p`58| mppYr 7@/8(hϮpBx3q򼟎~pZO߾EPu!ЀFǾQ'LRc|cnC#Eֆ0Oy0֔ \ Đ\x2s. w WR+\2@n0Bh[sh{r eHj[.6'|&̳7(%ԒV0nBDe-ayFBG]5̚1y8j {bܑHd0'"Gߩeo/fǼ⫞/uI_!'-! وC g }FIA n%k ohGS6Ţ?HP r*qbmQ` 4gT1Xd^~s0Eߜ ybŤJ`X2Mav;p!Rd֘a59X%t9q_1DR`! N1 u-J"jNogmBH)Au(rXC04 gbZSe0*-G!z˶I>R@tI,',y@ӃbYNA4&G`_"g7R^QN+HCǜ^16}&*2P+f Q)ͬ=C}>v}1R֌`hr|GoxƑa:t_yx|c(ka:|v8<~|du9ř! &d{;v]ua^9WnҐ/rp9J⯸=XN?8RtC@,^^-~D49g C]^~@AKM:b4dZ^2Vu #)z_BE ڸ@@&QvnWu0l|}4Dݟ)Fӹ ;h`<'NDѶιߧ&DҢ}x<'MwhVD88Yg$^03 Y'9>֠[8L퀫Rߌ1yEы$3z+y.$z?BPcESeM:Wn0§F $dd3vH'ÉR M@:Ť W쐦!Xh^Jf޵~J7t~pSX^?u%R_xB1*9'OQ~2ĔGQd|ly uRdhpӽ2`6侍6O2Ԕ`m9:Gr)M_WZ7|| pçTE|;S#o *]({f;b#8 9- iǗTnR}JNj\&00h[ QK-0])9):[E|R x27‚A0 a6vĵٕ8YBM0;Ss P*jӚ]ije7-w?i?no(ty>  ,?4(X[L5 (vfe/KMpM!#p뿌RC5 M]ri*yv {5m; y@;xrE}Mt"L昃`fYޓFDӵow*fD5CqBxq"MA*I+*Iz0Sg8:fn05@1yk,hg}͸FY2-`k``C0 46i*hhHY*hwp4T\Gѐ5잎o~H`಑E|`o;wARWu)f;S8Mq 0P]`rI63eT=֏Rp un|ۉ&TzxΧxZpAX{cVM4;pL9_*ŪVL 3K5"u]cE~2Ĩ{J_') ZzэYT*NxȻAT^wkWkzٌ8{[&Q7ww)`+49ĸ`k!{Lxv=Gf9:3. G\C\⦈q;#>LB5돜U5 2H6S;;I!@߷;N'xX6[ q>T@c N\U}z{]~ܱ#`eu?N\%{M)23O z}`P~>e=h(}aJ C_D 8i2!Nn 0us ypiی+ @GoeS`~!{z"@r +}ߗШv9zӖHkh@WMPjT˃ $M럜!S04nfb_f5>1FW7nHRdkO1MBRDRI[v-Hf \+F{ө˷W4FByVx~2%&v|pO rw3W@ؘ\%"2fݮޙ3J2 0Q˧ dجA*EN^A8r(mcs&/(Y"rXlɫ"඙8iy·C [)wYZClP&! X)n`:ȶ/". 7by&٣уO=)vcsqK#DLEu]D8 @fqXPw?(׃&0ktd:850$u1$_84  HvpȭA|/Z7>m4X;3Hs{Po}=`Ҹ}6gpǔ^:*1pP N`mmINö 6ORh7Vѭ8*{ + h By1h]9ycj3jH:CE4UtuNPr Hl̳x\"x^v3\//}A .”3Bo˨`h)^ \`ܢ1>g Xz2PB蟼qG8M=lYJ󜵧Xoa'L<>p*UtC")kŢ'E- {DU _"V]wfDWKxf=MtrUȄ{Rc[W\p\4GS2)C 3q=Ps'ZѦI]LZn{Ijy8@-Ϫ6?\ь\c2")-MaZvK@J^ Tq?؝u?~J }=EOz=R;)FΡPBͰ;?@? z$d]y/"G ]r 70,%ZC߾FS0`a220r%o}JN09d~tAMpC c]SeݘMVXs[1a*[XNpNPF2i$ K 1AI!]b R+ڭΜM٩8`TId!jW nTfC+9T$<8nryn37H]Y?a|LWP5帠V`ٞ&p;5; PX0%/ Έ}lc V9[cR f NU|ݛmƁu3zE'P`*x%ֶ,ֽs P=`xM(P.VV@.(kKa!#K)8Pwa^'?>||祋pӭ(A]$ѡ-*iε0:'Z#}b}7[55I>F<0ev8A{@CnP8jh*R:C$īsb=ay2]_T>u%D@k081Ċ/|K[a4 =o pS>0l(^2Vp`U޸-G aً}zLxNH$kv)*n#hGÃ==j8Y]K0v`+㟮Ldr\ y?v8uͰ)K^ D%@i%\A n۬X@LTlU"3Z2✝:n@dUY$К€9^ Y`,)@qjQ9[WrGaB1F!s9\+4;r\bMQJSy4(-@. .2QZ* 杞JqMrx0EKg/x] %Gai~\Plxb9{>=8!Y]țɕzq\׎5> Ʋh`įJ&-78잕WuUxOZ|?&v$,IH1G4[?l(n=V! A2g:uk~~rF n]U$z^S7'b|d&8loB:Ɓܰ8TD!:)q.D\@/9 J!)nc"q;XiQåuɻ*ܠ9yB'gW-AҊ:/6r2X4@Ԥ8 v~Z|\LڧUe5T?QqGq)rBX.VX?ÛLp봹hE gHqE&)^͐|v 2VV@-1 Bǣ$3'MgfQxW?3)` ?Lz;>t/gYKj7J9CWٓ?9,k2FnOs(QZ(Ü8 gp<|n@*P!V\ M$besu@J0(J$2R5]o,ozzњ BK.` ֵ='2)w j)<@SGkPE:INɸE .O;xxE7)!#v'B Ri-:&Jb+48Ҟc= CVv8A@CDk\LSV !)zH/혩TvT8)?EdhcT,g;{7$U+VN:Z ys??a?OTr|$&Q?4C&ʙKA62QCF "osMeЛMRByw8ke'`| D ?0E~euRtGx0O4I8w\־p;LD<97 SS |lh׬Dd]y8$`#y oI ŭa}jwBs=nSGrrQoykn, iHp$ FE$qMڐ{ 5. xi6*r=/!rqGqqD:όY?}Ha(o{UqN󈱤;lXSGc,11t;^ xn$LVXVi)-]z֔vڕ>_0|ԟ o4 &WZGfZ?٣@&^C!ۏ6!"J#>ؚ =.B2FkrEӎ?܀a5%\*kš-Q5r,L tRA\fecu~/'8p[Nr)^4cFޔL.UqV.OPSp|\H-^^lyn*/s9O8h0Cxԙ5 |QB1$ŔḚhڀ6xHW*5l"ʙhXCɂt$$ywl*06 z"s `Jma٭fmF@Iό} NCw: +(p&Y9obGE.m-PUiR8(vSeѩU4y@i>x5OߜEm0]Ts-ۯl s(;ڠ6P (,cD*7Cp/Ӱi+p (u %N |.qZw%(SSR{S񃆜[s;hZkslv03  ʞ|<|180FÌnCnBA/qf!udu1:NsdIq !O;Zz{Z{ېCFH5V)`c'Ž 9@`8^ Jl^B1L^vfi8+N=TJow_-O7y|⚃jZa82$-6ܯW>o^ HiqGފGDw8W'2:nbvq(ßH> {8U %BbF\%S6]X m50:s9h!d]r6{ֵ-.}c!+(U86{&X~/ʝ<2e3q֚+* }d~q=|8DBl(226:P5\ Qb<#=z] x k%,(Ztvbr&Ҥp9*o{ u,6NOx |}58ajxpUބ7Ī`TZq2Ez+UHDIGl۶ td~"<O M HJHl PHP"_ s9f;QUg gp]g]rycLZpj8 +q93o`/-ܿf)@h9Al*L@jaIrF96cM *؊xŘI "|rt D*ERa+ to/W,9JI "K\%<fZa[%-hZ%f <[<LwY M&5D}s*8qT#b*Q[9,v=(ѶtGUU(XsەD2CD \G=aFvɭ"}B:R/8/>11o"Glo4An(ّNcF#QS6ヌ ~q$(R B1Xo[s:1g5[W~0ZmFr> .t5wYLl*(^t0 dvPx-Y0tpznUz= T=spApb< ~w hf73:f\Ҟp)Gk o8T$S q1Cq9n=׳M> kU֞ +ОSa 94pJbh0UT2kK=⚃vCGBzϞNc7T >b y0te.!pJzP{;M~- ` e6cX;͊gaN(3 @9vu21=QTblտXbBO5߻teQ:$ IHX?l&x7;ۍ+镨5=jy4%k5uO;B[!lt"qE!fFE04U) =󒞁M9`w'pj(8ό<ô~&bIzLCάZ_Gf' caI?RrɁ<9O p*s7O5 g&TM<5Kx\DH^#@8|6C!n k{ŷ88VƁ/^0E{sG a7(Ӝ(X}`lpFCYenm. ӕgIaDlt=1ҙ"D8_ (>!X~z Sh_B̉KNdLr?@\a֬V_,wMo}P[yp"]t RyO-~97z$_'sN{q|q:rhY#耗M8@XBcUA&Gj {"u6VUZQ8Lu<MIa C`% *l:w-Mhgɝڔ^9Ҵ1wNu=xv]OgGIo  WۜF@g%y%ZWN)3[Xq j|ħKRDwkN.c}Ap1Kw Vb'+l䚨p_15.|hN?& y~\$G$U2c.|hKGd5o-֫ȧ?NrBwwrtQ D@D0E'f y'|қI~s8/n=u5clb5fœv9\Rse7Ӌ_Xya2)X9F4QW8Tׁlڿ,r|SFȯ=hlR]B[/f )& Ƶr~1>ߏ8 L\)hr4G&".I`kc54+j7W8;Ũj[d 'y;`"8þi~>*O([V׌@ƒG;ƽ*s¡gXQ1]c`s>19J5Z]5 #5qF>p>P~J,g? d'5W~`R(Id@)&ӗXߩ q7%P8@` /lPGoN7PSz.ܺq2gi:!N@ae^e\U_!\Aa)} Ip* ɔޅ )?c3pe)>Ml七:\]/P1\IL0 5 Dx~qG터 ? bQSXU}nplp\e0 pf†'Y&Pm)ְ>CO xxt:r8΂f%q%}!fZw|y3XnM/8+_fxsFb=7xP=ߜ3γzm4Tn}c#HcZ(V>{p0X_-Mr9Pdz>fuo y'8wo\ռluZLJqRE`Db]6v(&{w(Y|d#jv7(D(nA%4 ~06p  CfnĞ'Txg㋂*j/*|b!8›(fZŗQ@b wp$-Ucv-NJLD^֔ ħN3Gmw}̖7 h}1CZ߼;y0#|`<"3nϨx‰P/ŵav=eaxpaE o= ՚D3П LKnq c]Z&_@|&GNnq81σ-0L'C}P+h|` \@UĻbSC]l/$ i׬2n)fv%ٖ<0n'Yˎq7yed" M`@b6wP]{x^, B͆pLF4E:^8:V&(@?ʝ<' ͞*Fb 8xH'9jq{FF; ߓ8 5nE‘DLk &:Ù[86+}SYAfkhyBgT j$o,וŘnʵ Ԯq͊[ӓ)` G^p4(ڡqhs9͛V!3iO)jhuА" yߏSoDE":-d:ŒDc tM6#MNLsrs,-} ^,[7?R$B։δԤ|InL.K;8<]w- H rtASsn)Lv)|c\XSTDr#pSC{NW|'JE<8A~q bA'OLCĥkF?&$/Yd{/dO nt^%#oaO0畒Y_qrCO |" 3? ֟D88BndTxn?ym7(S=5?Ǹ 8>#yMl\#ka;5p< un8Q?Y =:|eKs|8[@X1?  K5fygʭOgWKei,%cc(5>3XQkɷ)z ڪ+S'-#HoE'75Kƀ5bm㌻m4007jA`@8ZRq1҂%0  X<8҂d C8JM \T T+I5ze+h?1UU߭G2BFF0f1;e0ơ]^7ܣtwL;Bjy@j^ ٧Bݩ\-A]! ^XTe ]8/+q_4h:5ҟ+`bOלv$9挳1ߜ)(Į=iHÀ!AL4ғp@73Npci PZ) p#2 lC{+V<5`G5[NOgjSn0@-A9Alm:E[DA|pp$o\8 TL3ؔl9`6Z\OtnǕ<VtE12"C" %.˼*ybijze W.ּaV@7?\y %G^=yu锣p9VLi]wY[O qF?J8b%\9n4DXJ?c[kywϔn8Ap*C@OmDN5qT&r>}(ճϮm"RjJ~2s܎f-d_D}#GF؏(Y]4Œm$W֯] 4DIT'k/"ZKq[pgU+jΊ3FМ⍠V7}@bć9E:X&E*xSZ EȵI("[c[{oO4̻L d`=UUp6CAl'd=bjL*+eC"q\YTV3;%\`̡j'{ fQ= KHW{JA=MH78Am.c5x/,p? ,Tݹv#x+}jaybw {']\ &x Qq:-Qfd/O D>rAypGwȤe=9/6yZgyA}kVB7zscgYɏ>sDWn;:GTt{x_ 볌.:u@G{6Bi[᭑.nj㭛g'#d6*]qF땬/zϟrtwMVo.590.>G_8DQƷc_(#.:!ſ  DV]#|\(wXr yM"fEPp[÷3CxM>e_X u5yW5AJ5H(H*|]r]x-*}ചV`SZ9oں}]p`:0Ǜ9!8u9c=(Ya8;,AHJ5v${s\beEsbm>=\:0/# DeT Z5񂈖u0)!9\Coѐ69 &%ҊK1Z#*_X`J,ε3!v_24֐%\0ޣ,OFuX9 Jΰ/ԇN6q<͜4s,x,nzkfxA^ )|= rQxcA3cA6y&-"y+f*nqb]2|4 "!kxdʃ8>w;guƸ*Ep S{ɶ r(f&0,9!ĹѺo5wȏ;d NsI;W 67sya"l|%TbZxH.x`~8sz8?-g Zzھ +~x{4#F/!gp+߳7M"t2.[aBSx2/ fC?sn2^ rT9#ᱼ^Go­Š 8 >:z$0s+ }L= Ӿ,XOFoqoހl\؊j -\Go'|IIi%:J/&w]FLKXPQ8)IփssJȢU4a:C`*UU Pm۬t ޙ1I"Wa!a!^01 Pw񃜼 6yM:5ADymMXI?(ZGe(O+Vh_&JR;Y>gd|+ܮf4 5Һnɹ S;*ň8%Bql#b7hٵ֍FM]~qzFl0mnQīc3"+u~6:w9Txq׼z%=-c 41zv9#șaáxjt;8nCmY{eK-O]+ᆋ;-QئqR97jp;H4,v:qOlLfؼv!_2qgϡ^h1" h)zH I ݦns]~}[-4+a#yf+VW!ߌٜu6.p!|>no؏Tiz3Fmƨ=%L/+l-F @xSe6@X^XUI_$۝? Wp1h=מT`?s=pn fW@ٰ8<xõAMcr-^W 1esQ31xkpE%Z?8 ~c掣`0Rm7-rVe.$BvU'>} bVv6 yPSG  ُ("p\Slçx"j~79ˏsUQ2=GXfIFG=g C/Hl#E*tʶ'TRlf+qjȕG)?v)P`FD{)8Pա.ntՍl;^pAPD&Wu!;#U%y(J4 j(EH*TF' <_8+cz1rmӛc[::ÈC fTZbp/% (4&&%S8X{3~"W7@W2]S^\NR_];\y ;S }602d@1%4]O+V{<}'UDFeRBsI@瀊xh-j)K^J>Nt(LyJrEbxnRRŃȫ!K7pHPL1(uqsUbv@"h,/`b=}}fh48Xi{ɇMi&ysOOGmǏcm2oƼ`Һ `Il[#_&h~r ::+pQ1=0xiN2M~w]>f + ^XkPO%sJxBP =aEowy$ɋ u`tVTw#Js^hJDW`K`dzJay4{ƝR:'O2%FruNN0Q`dpAvy5 x0_+0E o5PXZanي_\Bz o٧E0XGCA{.Y7Lm\4}Kpb]8Ld,:n~5(2]71t=o:ˑ C X{Z1.r3텑%24G?*A~1.MLjCPd u<' pPGogX^:hJd[ &(#}H8x(I zFD:9TrͲoTxV$rR4+P>0mN߯FÈSjOz_a=*ZIWztx{Ȥ0{&;rLZBUɺ{Fwq0|넉۵=X!* V/#6]Ê5upJ@ 0.$yg9x1+3x_r@nw/ȇ'LE ?tNPK: T%0IX1(N0#'dz:8 ieücAQ Z[^9vĠͨ3E^LDyL ȵv}:毩q$>5H Wt7wn&[e} zrU6/ek]wiUmê,lqڋBoB<4)VkPH pbw׺B3  P3oJNhǽHK>]BXkB0GS<áSؒQT dۄ.C6lzp:P[I>t`C8y v(+y[$6I- #,iF~1 ۭM J*4.]=rE}|],ďa|sJ;77[hL CGy]\.+-y OIbEiCxE61tYog^8=\ oG+,|㾕&||Ϧ #s,N0d4?ޱRo, W{hw{M\!P*@Q9. Ygfi4rJjyf+%Oq f>|/u"&JF3_A?QbbeK\|qy c 9՟ΌXb)8{ yFx'P%->J3 ?SaRĢH|'?o`.^ǖo-WI͠X8:OE ZMOXoF2HǁoC!=SĴ{Ss9L _/0|a{w+bۣy/Aow{%_fpż 1_;GŽw0#Y1Bq upP;gc)}mX>dlDz[hf@AM_lM8p¯ĘM|aN2V4 P~&N~؀L ;竀c]Ӧ_. X+4v</O@OV +N fNOqxPlіdq\^z%fl Kʴ7d>/ 7N9jW|bPh ^9@GE:|M+NO8zrzP0y(/*8rtFB=E 4@5Kqk#)"J`G\ l8>*CAC 80E 6[b$ Dtݪ᪡J_x"a ֿuͶ%u&Q2iU3ܙUQ U#Cc.p#pxpӬL8;xa"vSoFs1[m5~2m@ް#inj:>8ON[I28,fk4!wvOG2tg 8⯔w_RS{:0 (Q}ifq鍕Vqz|B)):̳*w;:NZDy(B~q{/CPք_gR<;94o1Sሑ?80ӂ&o=JoZr~ӌhR"?r.Q;wNloRغtvt7,8"9ƛs2 6 Fi9~b$:6N¤SJ0.Bׯ'Rޱ6M]p`x7UfVGE7ZD2jySӏNUy ֢\Vj&n'35sM2MGk77ko ibCe I`B*fz'2OpkG .C=t`Un;B^1 ] #t VnU,[d>!F 9sW.USWRIWU銼A)UW2Š75|` 8"oBH|jBt2(|T/.ƶ75N} .#4s$Z]*dq5 M9Ǎe]K6`wbа8nx9Tֹ~r e!.%,@/kU pCA&.J'~RG'|嚆84KBfw!_*4Dõ)ݾ C`?Ɇk)KO5Hn%h5tP-T!=H0\y#ŸV<34 @0 H`PgNd v&RHz0ICߖ9:"q+K6<~5:!lO߮N9=~0dSlX WH߉މ8@ZIxH~q-ƽuP=ן9D:%8jM}INvƠO0@hKxp9_4ow s -T4idx_pc e? TEPf_rUV|ǝo~oOLk[9̈lEtL*qLA@@159_^Eӧ[+%,}xWSR.AWzFz6nf-x #F^1  ߻j"^ḛ> >STNB8m/l{; ybBX񜪣w=CAUOC{8KaF#kSs@a~revo$lgb:^m6>yqC? \'1:F%>g¡ҤlC6uGΖ<OE./^ؙ9b}f1#:e^@j/Td(k낂_ t5^+q6VHjn䉬|d 7d D]_Xſlfy7O8Ŀ8:z,/b"Ϋ/p߭>ē$$W6m OU 6i(C<~o왔~)=,i:~7FRB)ʛ`-e5-90]3uٓ!7uPy!#r˴p4K^x)$l>^]vه&@C)AN<Ӄ3:yzI7@ZўAx5g{z0E V/1W$ g\ŚJ5 ,km+\ בtEPD[U 5;ǒjC|xâ!onnx`Β8L4yzdaq#᭫)7ĚN5R[!'|- ޷M\x\, v`TϜ ef]@ pĦNj+t?RG7 wŭT$LJ 8RU9]4N0 CP=†GM2z{ő-'~q0g\vb$I^LQ^2,`q4:lj} o&'X1fXks(kMY㭼FߩV;d $mMuh>D4kO/4~Y5xפ|9nz&Ctl^158q0 9:ɿ9i嬹4,B 4ۧxqWXaBI_#8Gό|yxJt N2B_0$^xZγNG>r7LoGAcVM4 >270k!ԥNi5)&K̂4:\ N/Y؟%&\/(]dXj:1-z!r%* pFjas|k4TksiHC\❸Jb&Ϙ> 3Zm7]YD6 8LؖTZDр`ߐj@%AQX7b -ctw2%66j0F!!e6M؃Q&mZhwLٽsW$!q]Do{g)`33qӏSj.S絖a AiWί#y9ؼ9aTCeğ=y=I7e܎9:MpW^W#;!&ɿy-'Zs(Gӗ-E!q * ;ʩ]9(;:,\7k4pfSYbw2ÄCk0G.n1U3pLk690-jV:5*xjW'BDRô?i/R`=e(} }HS & {#f`aW paCh$>C2`2Gy%I ژ4Pm#H1t&s:JDm,HxgQG ژ/(Jq9hl` nxj+DtSSh1* ӳ\IJZr1I5ik*% 7 0Xq$W"CFP)`ƀD^b*u90YȢ.EM&^ v:5&jL v@ Jh M1+Qu +7 Y^_wfWG i7_{}T;UGOYpP0#M<̗xOLp7o^pUdF|3w#,KN|K<9"Txrl1T$6d؊ࢷuG%kUߜ$gadk]&^qU F2$M=yroXb-D7q|nQo1j(~Y3_Qw4pA:!Pxk3cSdػ me0=V/LZKш i0Nh5*vF"ƒ[S.Ȁn.L#vq^3W4p*N/aARX5"G4bD28P osƋLw˧$;XQbKC@ٍu Gg69],9} mTO8L GUhr)ف x^D ' !Dd9sQv8]yQpg0f-4#k ͛Ʊ90\C"E٧=cs]d9ƪ#>qn`ptvk#jZ)R>q޸Ȯ5'dpŇ>LB%ot80O} 1g+V<k`1L}v<<\"p,(NlK2wB^;L TF%1=X ;vo 5wLK5?@s JA=*#2trw#pk._l?!=nlW}x(9 eN9Jc 8@)68L:TZsv;]ifNP"ER{z2 MW"m\"I$E B07ŎMAtWl.Q-Ti[X7xw2[j&, ime/y]8C.5_Cs_alQhQ`M1{ʐi& ay<$BˈY,*&Ű:pHR)t, qF4rbHVR^Ģ-CHZȉ{*@Є+x%\@T"|F8,,Hn.ԅ#2^|Q\QFt@ɔZ|X0j'4˚Jr T,'Vݑ1)4"'hS=jO` :K WKA"tJ'1C ]ys.RՓ6qaEO:"I ^ۤc fO2fhgjْU 9M|fc3&S >q ^Eޘ&:`TLQ*ՁꏼmBh 3mBk>Mfﻼ Nf ގ*B#qNӅow˔&r+9dɘ !"n:|(xDo7d6>qZ^:-Aɀ֑|rw/CLNw>bܩ="SN1GZ^qLB;7bQ%6U?.\Kt=' } B^ycX? FIW7H80ېZ-5 إȌ85#Ǐ*9yHp6n)wZCBqgfL1DHSfHlķ:=0dCQ5 $2/jdxS]cd|&Al\T-IH,#`M8#q!Gx=|w~L }`h|#ܶ[Ƶ5x3@*؎re{[cnǛדp Juy& oX|Wx8t}.lK5Y';ETfH\UqYH,:~>q~D_Uecуz>i6տ=>1jKQ 2.~$@y ?[Љ!Κ c)͛@v Ex^uAweXpm'eq#V)q7WYRfl|'̑+ 2Z}qSg+a3j=*l6}34M"w;§8ISX(' 4g@;ND;I]L+ 0~F;0ګ ЛF|!hCT^,nwɼ0٨rT-5VХqΏT.҅P(S YU$3-F*`5҅ UJIEGxB;?D"7FZ$;dpn L]hqhyB!4>PRli)bA}$]O45uB- Q~2ZNHnưb.N1zqLRNGWkc h-;h|I}ۙfTG+-4 G ;)špʱ{Y<y[G&>iJ:4/0p]""(ԫ|Lr_Q?'4_'i#E.B}ͮ:t'A1k[rDqP<..H{FYҿ8ȩ>sXVK*%YCUu /@'3nN^+:Lw4ܵUk 9O[M:puLqp|SOɈ Ŧbm>0o:tq `Ϝ+/p8Q@gLjAlz 7,|`'4Z;NЯ `'XN$Q÷c~sd xfjkdX+6ipN{1lbhAo {'C{x >=Dg{s8hG2f UnL~ޯ:JV? )̦BCK 8CFό=Wm%xcѧ9ySurū5#ޞ1!)K+$&@p(+*3AoK2Yߌkf) yt;]JTr;ӆU;3ZYLbJۑ6?I/O/h_NԓWW:ɇ]FK%йzGaA0i]?[rxi`pjӼ OQ+=x ߈^L7M0?Ycxؕ^xq>R8_aJvB tu7o&4d% 0JhP%!Kz(FD$.[ \2%bݼoO @dKW6~H< ]27Odi8eϳ «OxbDؔLeh\<͵ E0?xPet}rt6Ѹ; a'+ em/': #8G<oTKPzr\%iNO>pL!/8Q> +fT>hP |<4VR8hޯxqa} @Q9AGeOT\I%q}0"޹1*0zN޷фx.Q<~TEĽ0Lc{cO ZUޱrt7O.&x=mĪJyFu~~8B:P[/XyS$v8:7R:V9Z9Ȧ@j/SJ>fG~S^!4A<n¤V\P}( m>7U9Đ:}VDYWk-a\`5O,\8eazX&o/mRSi3’380x9&*$6!Yrﺉݥ2DKoFB@sȂihc(RLah6ga/m73-_ 'LbHRvAMGvjǢ@A!u@D[-4xy4dDu(2RGn+ ABk j2Z DV m"DbSfMyp-Ba٣ Rf8YeUX)UWAvJ) *^4""u驂e> ": JjeA<  5 &oR9:TE#2#G ܆Yrɣ k}1޵5T #Zp&}7ݔgucRIk+ǤYjN` M m8mG5C='^2E(t&Y+Q,R8 gHQx\&ݍ2jj*G$r占yqke/)I7WJsd8=#Oy%}̫'ӅBV}ԉ9өemiƩ^Cڼ3¾6g%_ 9;虣3}` Y G9|8 z"o;e#;FhR y͇8[S>vH8*G381mt5z`yXU>LwPp1HA|f"s"-W}{p(PEokQAchT@DHL(m:iI!g QBqI EJ%m a( maNR:h݇eN%!c4'P{P%1Z_ E f}:љ>v4OA R `f "8࿨))[ :`y£䊩E5NהWJUAQ؂DP<+](BCi^,CaA6oWn}:`piLhvx$tM=a(wEXWE*i߾ !o4E@EZӫםN@5^7Lg7Äp'-M9'x GC҄?OE\[G7P$+xnB Xě"tЄthyS]Qk~ jBy-qTPɃ\0jK>;(,o  ?-nX>|M~?.@DL UMj4NNi"8@vxd>İ(фȞ8XNFGi8:H8[U>*]s~:#.O.D_9U "T}"]͹G$9Of P\c08&UmԳ,]>yFta/ph3\;NwÄU ;/m v8T)YAq9 n&##P#1D檤fn|Nr4{7``KS?}cpT\ϓy ٬@K|iT,JcvkĔE h+x:)S!Ib) hDPɠ(nefًe|U{`̓dr)CBQt_b'!H3EbѹQTZ5<_ 4n e`FգcA:(h9 16!⩊|9bX;H$,D],2Q͜Dq9u1 XV#,'i<*kj`>*gl\PD‚C@\tNl׸,\<]1`֖7# FyvB"g$ *%T<^"2a]ࡑ9 !pJ Cn!r &ӐUQHu %MfTzR%`vй"sa9CcROaZ. Imte'ƞk'7 •IJjG ',|H9+Iy;o|?ߌnl&IV.)<8 7Y rP}1WD{ÃQLB}#J^51{Ƕ+Iʫ)P#Ӂ alِj i5b[EtacSf\/_]d|ַQ]'o" %,r4B)Z 9 [zLl#\i'0/񯌄Rq>2"HS8l--k }1l'gRڞctty:)sgěT`A/}8֜ܛ" sاT]&hJvMn`"1v?mT& 0ts8Džl^]ӷ`杲5-f)e#Cm("HuARrțڕ *7$Z;UEU2ɠ`Z8W B5jRCŬ.almաqUtJ f-;Bj(@n"g/,Naz%%EWV;\乶 L/0K{"$nِqw)r+* YUYFP,W dlD " ! X1 m[Fm #@[lh"2-#$ʦ!-jTNp<$,o@L38RJ Mɋ~Fx1+G)ѨBl&o|ah5m$9k0y?36@Òo^?8; rDN_8WH6cx?\ %@ӞI+(=8=! ~Ӌx^{Ŝ uZM:F&ف[I +*Ni"xJw4 G歀!ɔYys68)Ӌn3{\-Jd3CM/{98P@n?yS$]4u@Lɶ%1)+j4J.4PBCqd6)ZPO!p Vi50YE\Ph;*铐.QQCۣWaJ 5dPݣ@@5`TrI, 6"5xTͶfkRěa后#`^H~(Qަ-APJpV/h B'T%SC ;Д)[0X(CxW4 ˁ4z\DJ4!MCBE2+6uύpHKC!Q) !a{i"<b5p#R*78w+%Tj BQYyF1Dr H`wC"x Do MƿsI(Vgꀍ`Zq2:[mh iq [1D DPCD>@ $yz b3(O*qe'cQ'NYbl>7+oo{3#>5f0<8'W-ehޱA!_ab8?:Kz/+nRcHPG Mׇ\(h(v-6ehg&Z!:'wƂdh<78Wiq;. p/D*Oxwր0mÌ.qkm}̚4y  |tEpJlgxb)_([wɕ@v1m`v\ 6lfݡh1k=1^p?c>a{'_8q @Zo ^>3N mxp&"ultX!-X&5 ,Y<'lv]H947Re7Ҁ}eYf^(U;X?{ƱSˢ?NO k(Ʊ$kzp|zN1pHY u'8˦֩0 puR 2'i &]9eǼExD(ӱ68=d;mK,Ugs^TQ7LL? ߭YeQ O- *UhGNEL;)MXD+ FbNb2bW hJR iLUqZǀ (98VȽ)WC Tdྣ`kE&lM.3vG.E+ZpѕG6KE ,.8hH1@`M2]PSSD# wdr4׷+91%UcJί!8S@L\/b*H*@w-a=B]֞W%dR +Ěd1 DgE,&2B*2>^x}<47Wsg炕T;T"Yjx<|rgDl4m! җnPi,( "ķb7 G ͔S\ e<B*#A ne/p22]qMQN~1]]cr!:yO׳,NCȣK^XӔퟱ˹n3;>P9E$TD:r"ݶnnj Oƍٿ M;6w3}q5cUYbZR9X;v}p#f b{ aQ\}]/a4S"8{1PvE pGVtc(e|n٭`O#bU lap\D_$BrwbYwD㰘G͙>Φz#ap} {m堢CL ɚۀ(_LeɂVK_|4\l^x6Epag:`#bpy0v}SȎ!Ya ¯ ?fmQ?qo$ڼ8rВ`u·F 6z@Q>1}U(1攋_& <!7`5ycL~9@ qc.v5N>//}""!맓 9ϯ͇Hy',eJa*˗P.%01L0T1B)͈%8*iqӍ ^ݔCGXj|4Z.(Q+Ϝ9&}y,AeQ3Bn9+*_'*;Xʡ9'S.W{8,buOӿ&:N)VWa=s2VHVUޘ33 Oaƻ )5GS'  @\\&ÑvG|}]ӌGz_1(npmE03O=⚟/woX*Ϧ}rp-Og(^+ȋx6 Sh'.*bǟKo?8TqeʯZTNER&\yG50ީ/4•SA ?&|boMB{dDcXp=pOy_.ćJ',]y!T'~2P51ɪ?!ţ{ ~U鱽qP'ݓ/la,T -*wx6OLt )4 )Ohg}朞Ʃ=o>v|4OM~ZuU9gxWOz}#T?! qsV'yxuuxA,dD,󄷣1"fUagLuhQI<cF-v'#/,G4&=ۖ"5q,Np4j=G^~6 f6?޳dFHTSX˲=0F:r_!4I|睫FùS('FߍpQ5J x2F>Vp{!{!qbp*2+L? M Lw'Jf.󍋏|F-6m%11sC*{4^ߜV ;Èt?|SjLR@2?f2INLK|&_9OtST4zߌP~N_lFuiD}3`c g >eBL>U)=6J(~ӌ>KrODW }΀?ni Ddpߛ)_#VA_[%peҳsy8 p$pq@Bٵq7/eÃO/oX;&' (%g֦rgdxJ]]a<m u1_ha>{< E?wclcFoњx%gS|]ooiKee<<{2Oo.LsBM @ƗFEL^D1yA)伟RO4lx>"Ng/ADs$:lCYS?Ā;]˗Jc43. GTg Gd8 Ӡ_tOXĶ7 ތ ܗX,:bW$ل7Z@z F}x l_\Jj w};ް@coduEpI)82fbԎX4a)5yn~؍`KPkUWsUxm;5B4ӱK!NDBMu᪭ p2O<ټxȾs8 =8/֋0lCF\V-Z_\tգB5G*O#1Գ~٪]dyĖ/GL'FoLje"i0* G rn`< gc(|qh q"k (S^X3y/xNc1F.5"dΏq|w2=c{ufZ/z}=`\;_5e+:0ƽfuA^Uůgg'KÉxp@_9-^a]` Y#`'@ˏT8_ WxqN.zHwBьxsC/׶`}" xpX ;ockyΞ&l*&ĤzŸK#7`Ibtk&%XE”"Nq(pǴ 9` z R]/ GC`~0yY_PW𔑑6?|ɠ kіl|.'`| Ao*k`ܗ5=.FdF9Կ rmΧ7ls eoK,`khmMI5Nʂ"}51Ãi#offpunk-v3.0/screenshots/resist.png000066400000000000000000001025501514232770500176020ustar00rootroot00000000000000PNG  IHDR}i̷gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs%%IR$IDATx}uUU9Ν;)051LB5AAgaޙgN /z~t}yΪ70 }3 }3 }CI55( \DBQBTOB1E$m:OqEvdX|}-&=c H:$| )L,;cDZӆ 5mqbS&H|C6qؐ¦;ƄseKLEf=2=V\dsLx xHȓ36k_e}1w<R?~Z@l6S:F˺b!֪n797 (s@hz|g̖K+xS_8=}۷'5Oe}wt軤di c^7F}sRso|軔t )Ia8xkwТNj2w Ij Ko)<ꩍG=D]'90}2p}j+/7& vL6LI3pm,Ƅ軄| \1wO_>uO' zP4w 7;j0Ȼa[nSc=c>ƌ2Ϳ> mY\rrj'MǥLz|~(c_9-;.e a+`D9&@nO׏l2 D_΄5ЛF4U.eI'̺7!M;=G`!Rغ}]~C'MI!+5\ZߚQ23 ۜ ʉwؕhy@&Hu>$vR4w9/Ha>)E WTi oj֯߹*EbOTT%AO=lu6־ Nkor^!{hFReZԈٷJ4>sFd %m[^w/Եp뒣Wu?KJLL`sH\e׃sGQ- XSXg>_WFYc>]VS bau9 YqW$SGlXS-=] U@QP,@K_'tǨj&揼C>X盶^o~6O+>s2[+^=ӵos(WAv誇(Lw޶깱UePV\FݻHȆռ;Sx ?886kF1 @5|frB<)K4ݤngs&[أ"Ak,2I6pI @$O vk%  &XAWX,ӇμvݝO wv9ISrȀ@TubZvEܸ8,`M&eϮҘQL@.E2~UfCs9CHcR})1ߋeK^N;>=K zn}s6O@ he`yõ^n@N&(NONN+kH [%-GpDDUZOnфAR[m \ HgS6wx^ xzr jJB?Qā7JG4狈b((uKTG%()L+)&nPi)ػ8RȤnRx \`2$G.#vB;-sE̠ฌ!`Cﻌ)nT'݄JfaG `Eqn"P}%I`&K`HDtѡ ^F$=\bDK&dXS}Wi:4 1k" UO'k">p2Akc`"&*8YԲƯU}%*m-7:@^]KߡƺKjY( @%'KvZTf1;ҥ p()6oӻGPZǴxH|1bqĸI_Sxf 2A_IRi,Of&4 mN͢ "G|߷V&yyMurWl^Ãe`}Eeo31X}Ҿ3.uǘ1 R WyuҒ88 @xP1ow iv=uolP'$P:Wξ`?{Є(~6Ԃq3O*w |֝l ,&YPzz-[>oat.UXuZ+p]m[VQ0ܨ׾Hgziju\gQ4|Zvi+~ Y _߶oLC Z&xf ?0@, 26傌=^cd?G[ʚ|YIkS/Ȝa^6/n枼>*`.S;$/{uz dQ^ٓYU޴]F>o% /iǡ!zg@ zO[E.5jW!ׇ]ZT^=RQ$R'Ϊ;I`W _-sC \0W*dG  K(2Z#T~yWc[@{$>Mo5+!y/\I;:[wD(ְȳ/ -O-\iƕ]=,,O E+G'̚VRZZ&m<s_H_(y~nU gc@z {٠}{R!|⡡Q r}ܦ(C[}[r)_Z`[`[ \XƎ<.a#7fVd)pZqfawz<%9IB(fY +S(SD` bIyvb[QqAdI$+C`=ҵI = } PƬJm*A`du@ ?kxgM6KFCjﻌЧ: QwƬ) ob)+*4Q,I <((R2BZ|k>{k0` i9hHNT*ae+3B _R`n(Ahm* "wwhD$4 :;~@l FwP!ޞFr }ӇޗPE7kȟIb5wgsݚ0Ĵ1{5V.p+'ROU ;AZw'=.CoڡC'^_A}Ozb[blOꬷ 3i!y/+ܵKISB)J`n֫@5Q_2[jq;qUTr;eX{8<6nT-sCfD2췡77kvqaVW;:v!y/3P@\իq)2i8S ߨA*{!_sM\;޽E3\ ]{>Z@->R}j& jС΢1=)3uߎa><#tS 銼}ϟ&Ϛ!= ?;Rm?:mvgto;z˟UUOA8ݮ1uoǮ& @[<#/F-}TeY8NNͽ:@_"}In mf6?!铒$Mx4lk(n _owW fzF@̶4ve>Y$FBC+藛!ٺMnS96:\o鼰{Bo2@ߥ}X~17b(flO`e~f12* ]bV< O[ݡpmxK}Ae[jj+cj2}.5xP)ingx1:Guo@Ѣ@ߥF#]Q$/A~9aq1wy<\nl $Ot+ kҚ۟5wm^x۞;MdQ7tə1~ x_GX<ĈP:/O_o|8?HFŠO.mZ[bu#>9?g2> ]z~׬[~ @~rL(}"B+ܲ'G>q<.y'8ZrZ 9#C3$EX>eפ(H}#6gmSC(20x߿n@;z鉝߼;՛t 1znE:8\"eϋ9)eQ3?~\J|\ HTSD\L'X<څ4wY½̒. Bj/vTo9V;WiV4l|oe+m['c؈zEshJҶ~v]#wұRߠ \P{vH;hHkO"_[} [d @qK' M3ps2z}݀@O(f,<8Ry=MgϠ#x:Kp- q"ߟ 4 𶹃 cyu,f;I{8; m>r[By-OޟX_ U/H?Lws0U0cXpB$PB 5$?r~N*kJ6]ڙimxVtBk (eMţ{*^~)mB(e+mlrշ$4 2SP& +,/WHh W~NB3uoj&,O`|; k75B_ߗ&g`h`/zgז4T}6TT|:9 ->}m55 _xFbR38>F>@}V!Wof C5Jf|m QzgWk5%i[ CG~A끞q[o\G4E9R P"V0…;H󭣪V"@[|Tq a#+:Bge.3{Nˍ^f@b0yp}'yQv&GFPX@ u_IqnjcOX,3qqͺmWymS- 0Df`R9bP d`s应8&tc I?0p#~5fHIx)_f5@OVcѷt՛F# V, <xVֱS;'5)ܹ nKPԟXvxo 2d>|%@ᬆ/ uCV$G] $nNq g/L[<+t}q*aX4L!yr"}72M**)ir- (_ KݸĿuOXn~#Jm }6+Ss0'r hw*w X}n)啳F4ͷ-eVERnx$ c}{NfDHYEn޽aEInjl580gC@vs\u `oA'߱+]~`ƞ&׳E_cbhZ{B ؖ|wmnW6a#GUQQ}pXM7sn#[z© UWl}$ 'ЎCkA  _Eo'a!uv'w[o}Ny iM#M(tWk&1ȖW]*d:1㫉dtv Aom7z SH)`ڊF(gd&hz"6l2F}ӤX/^ h'm4#% {Tb0UtGI؍B=ЧAvL=3Rul6s{et&\Q0 @lբY mġGPlYwwz *1~zf_z('«S 䘿vP'BCl_\u&T@BA#'aYR0=fpiKٲg=pD }U!!xS W2I{j%;D'`þI'/ nY-Jkʾ{5l ҏ;l~J~Qi im8{]+(LWɢ2ﯬ9N›0rORڷжV!K r軜ȹH.?wlpf?s:@ ,Ychm￉aC(' =Ue7-[H<_\+UGF^a 'T>@{ՌĶ79#D`CM._)_1Hd/y7š"4@ǣ "` 0 O+ЗGܢLuiEIGǏ3뉕;w.龟!6+v'3wS.c E bOq@&ZY;L;"0 W?S8Da,rH-ք2O`fDBfolrGE>>\!Afo22$eM/z/Ax|؍  Ο%@QMXhDddx?wyTwb0$8[ȫ$RۺOQie&_}ή74}N!PvgEkMJHf?qJs$}VO_׵JsdɪѠEA !l~ֳ-ntb oުear<;"1HUUPxXO\ 83U۵[ kЃwN/xdw4#zps׾[S@8L ?<#M%B >7m򯟄~>gĵ,ԏG6Sϒsst8! 2w3Io|:YUmڰ :*zکֺ<Ҿ9`S&7Nx,FUѲ:h?}MQ2M\7$LNPhݣ 7'Ce*S5~6~\o/6zŀKYݡ$ 軴O<-{JS*~^|(3?̼ZA܇ i9:5O_U}-bNc6ۘ (}{2K89o)۩ɡ<(Xi̖.9؁'QsL^w-oVOۼzۜ3Wm {}ͫWs([@¸ɽ kfS oK>C>'ޥ{V.scO sA.KU m.7 Zys~J816@ߥ86>dF#@Āi_96뽌@Ӌ 5˘?B"$/Z; .l ,k3!`\Pb̗"E rP2Ĵӏc&74H) wY} Dv*X'Dp՛W֯&S ynC3WY'-wm͛A<X&=3Nz妙-OHPjSk4gBvj|Q FST`}̙%[-uCB> Յ{3 Sx޽NWoJX6~[]8:h#&ƧAlD LL$ua h\˼la!#'m_ffzWKbAxfӄ  R9ÿ6[|x3vA =[1sS[Y~g[Q`ށW1: M~~" 2:QmR9}w jIkVA`ɡJO$N`[p"dk9`)[3p=#c@_oB^}cBxB{J)7kҤC[_{sVOl|wq`):u{H4jR¸#+ jz;fB1= ̯e)K57P3 0NiJ ѝze]koh3.)+3WUõfn@noa]OGߘiR_ (sJԟ޷jz s{{?ǎD)}ذi*zï>ĩ1:;]$X"-t$A.56X;;AQ/ȡkO~M١ D諅| umo[Y>hR.E06(x*O4}jpA%-S fAS{FT%ehL6]}wm}xazǦN !ĸ4e1bnO 8(O]EV*L[*@͒>}+c]㚌7xj}`/ Rѝ8$.&!Wf$s>?W:)_8<<)  e#ã"$lr~!TI`GV' h_ {1)Ό{Um6W;1VME;LȜSpM٧;q?@܌I$]{]3L̼G7$ q7GP/F@ Վ> e&)Hdߤ"O߰:?v| ܵ㡝rȞd@_]$%A~NJZ+ar^ R8;>8Dӕ4f#"O 欿ISmA&eA =H2Y̬ISoG} +ЈH6 h~jZ,x mDd4:Zd=Dp޷)$ͣGT*_ f4y5?)DgG 1ٞyMz嗳#XXhp?}ۭYnrl@_-xoڹb{~8hlwʯ'5Ҳ=+8Hh!}N2W\N 9Ի΀kZSHhw^ >@!'Մ?pfznf пFT44 5$ȱxjNǛ?F 8NM7 t_/ј]'5h6ȩ@ןӘ_tgPoԃ%dN˃kVČZL /K#@_mrO/h-:ڿ1Baj|n_geĤm*J植"6g-y)&a{i <@[V- ߷P;&6c2j [iCYgT?-h%?0H'}3VϿ 0gv ̫9P Jδ7uiۢ3̱6N}ͯ@< PVǥVdu߇<-]9@rJ{o.;B+:gd Re)VJ#(y# z]xfwu^}u%AWyru-,\*ϘK)H~-BQ<v <{~ M9Q߸U$ FzǗj &/?h?h7HIY2 "r̎ݕ^ B2mF+ņg}u|f`5EAc *dU!hf?漧6À^ϊY2 'бzۡSF) }S=+ oVcݛx4 ""Xz6` 5dmo|2$o·nݚ.8c#6TKX1&mo^0 /no.r3W+/5wX4oq :ؖmuI,"}r{xsljwF[fux)|lbvo*c"(j9))37 wu{O;DSc}W?`:@|gW/G_.ԚO.-5d>^eL8N+&ͯW0DK2 `~ń8о8uG3n' Xi3w駈OՋRmLS @a#@%VAs'>|2'7xR1_{?PSo)A/@ 15c閯w}M9fz1'E@̰Ѻ91_kz@F>?.|>ZL+>dy;dH^}}Wt9_a#M0 OAYk\Θ's5n{=0$KO Mjz4|^3$%";zI_9!Ǣ/>[ kOW']z8徠Zv̹[ov,ڢ@%G_^s o~1b\ ]b@̫n'S>kȯmi軴ŻV4*8tգzk>x+ KK8=͹ȩ27>=`ל{3t{v-(\{"kn)C.yUm l#JSJ0H\5Ԩ3fڋJIceMf~>4\/Fx; Dl@'L2U%_^Lkc$pjy7jvo1*N_ǥ9N9f>V"0¯Z>,jpvB&;'ވѕh_Et&w+qÖ6|f@_/gO.RwھRSڞ8r*9.I`pYyE8" RQq y~O@5UH0ݫ u?K:G@jmGvFU?"?v4!9!Ib0 d }ӃN>;^fnBQߦ,Nʼt `;s мn P6"3Viϡ5!Tkd 8(ڴPZ$"A opDDXSz8Gzr)!5qnDڞI%fg~)Dӏ9jg1TN9v>aBs]7S0>AgngOSyۮ{.n20)k>鏲HZ8L!"v<-}j 'TH_ǭ 閖Vyzӷ[3z_E;_JUeh{'OXdS0^84ͫ*i_¹҅gȻi{rAWX~uZBX![eVMΔi Ǻ`nlEDui3w%+>>q KDgM#svcni\-ztB_a//.*qv:jpR}D`=[GU|~JYRj߳[ / ō.34LQ:VJgULt]F?[S>O;8J,H37aϞ [L%W+xC-zli;7+v6xy:sļ)1JmۮElj/}?nA`)S\}8]X|YBd$#p.P ^": 6KsW*:AݰJvcDcڢOlX@p7813Rjz̬)&!Ju?! KKӡ[z ,KOyjNa\36hrkfRk;- @~np˷b[ݵͲ=a¥?/%`7vNmtH&0(B"^W*So]tZN$x#?#n_k /a~;"klq_I7d5`C&tV&'ī2ې O7EvweQ*΁RZ;>珀qRT08ҟ {XUsplJ!Y ֲX3^IdC-M`?9\KN @*kX })M'ó!H^CP{RH0j PvAr  2tDhycGK[r2tѝuz~\I3PM*##McJ ]5E4os+OAF=zkՙ+R!fμ|8IUV/[=֡ngMNNlvB7I V uKjr3͟4@DϔZ->S[WL%G YK` $5ꟻq=!5%g`B P@s t.pO;,; >M M%2M`Uș|fBWU3aȇF+i)>"¦Ǘ֜pJXX}*mצum[!kz:=Zs@/[ŖʚģuAYF֠v=g! Xl50yg;n'5͙]ZFD85֯hV߈A =ZVnHߞo$Cɲb+E9999.=Pժh]=,8jsϤ P%NsJ4dT'V1l[nAVk㇇,MwED0+y邈11K A5Qa޵kkY9+ Q~%B۟K}8&;9}ȒN;P^c<9>x&ĥIPk'o]@JfH1Z:r5l_&fHqhǁ]|h~Aq_OZ8nujDǟ}%YP:r @~O@@>fp>A/uK$"E&&~~5s.עO5C[&uzqtlxhl7::(GɯB}NII")@n}N uWEl)֗rsTѡE0 J%\$He=JoLkz!]x׫Eg =D~cF̌[4@r)84yƅɇ/o.81#`仿_*)|]mϓ pXw& i9;߀K"RYŷݕd8z$ Umo%;5M^6{R 6 ^9%/C HK>R['Od.Ǫ? N3ҧ'\?29r=]#~|jVwiB$ ym]ήk']־sk!OBy4lШqʓ*XlԠa&N͂]|cjSD"'-vf*c}لu [ R3wk_P>V))8(3Ƒ(`(X MV_ ]#ݵv! R?x:}YoA0d-UAZB֫@KfsxnZ5?gXJiӫ[J`[JY JTᲭdx]}]xTuӰ!4,ޭ#p\O j p,ZsB4X_y3NL ƞQ,uL} ~eoX*ͮԮrj 47عUx>~!k'$sZ 8nM.ʮ:Mw 7y@]:qj@-{ E,Π:יtd"Oin?*tՅ7p9yBZ6>#I-PW-vX2,o"+&gx 5O~gw>&TB"ƶ|3E0nM ^S7vBknqv]52#Yܧ k0JÊ׆OvUkB5PVeh ȮAN%C9_(l֦".@ZzN脈B  kV{[*`g}rODWoMmF /ט@f/*#54ӓ hU2eD_ ˲ub`t`nTvB]qRyVl@6|6ԣqG3UUđ sr߬?>9qǦp1@.]S:=9 Yؾ#8]26L`]H&E7Y1D$bsp:]p%]fjX (`tPvl FOwmՇ$΍=oeFC7 DyRkE~m7<_@Ҳ"Ǝ __'W7"hؕ(?V/F`'Uޘ>cX};;Sp םAX!e:jҚ~$sugxʬRa΀eΐJ+-mp}9xgz8UmNɋʬ*%\c@x*3sNUfվT`B@ EvZ+*jGҙWP\RRb|"^xndYz{ޜ =6f>J߿O eWN!ok` }cHNW)3mOɭO9#UST꺠G\J!ʲWX#YޠܼbOLrztܥh]*lWA}t4aux20v肓buU9D超c^ QIK|ȨG.qEZvo_[^%wD$iǷ&uټ(Д%Aؖi@NJ+PɋUcE;$9$ :B-CM܂"4C_8q84ɀx > C.k+fv SuNTݮcgWvYM/qd%zD@lWkUC%aI)BT P_X+.#u{RU>q|]_9ݴC0|pظZۈ%"Rؑ SץsCԴ@7Y÷U 9uރT(Re9c63y]V>fS>xxPxtuȗ lGĵs *?L_u X9?>`s!,]|?h_OJuߑ22SV LgATb,׌cANJM/E7O6_@Qpo\`>qt]{ǝ&Q6l)533%4E-%╴Gc'*gO'&b@)(*q?$ l`7ÇO2I nܦc-[w~I\_{t6ˌ|Dp;֕) VACN6Ƶj<3j}2NNw?ߖeW I]`L"QM#+·xߚƢUtqݥ[A jG^ ;._ |s4޴8|d5Xȸ_n(͛ulGB(>ZtlQw~qdtiC{(7;N,uo D&1>n5Ԝ|Tߠ`SIAvnNl{zY7\WTBPP !۔j>]4pb] 8z`]%8%QHTb\Zοݹv~SZ@y6ԍrֿ];\n$RT󅿊GvKR9R|~"\gn')Q~EŖz?/;d^pZα_]#_ pfIenӭU5d=y_}wcyE\r1IU^9",tۧ[%V`LW;1R|( `"8ԧ`,}2@G#{]~.spmb.}lBHd{ {>wDuӻt^3wI/N͘ 2ޱ䌎͐C'kH}.)QO wmOaC?@?i4'7w%gId+۵ǻ8F.ZU2$KKu{`{#PON0wy>K[ @B@ҝ:}3w'ӮԫpjťO^t_ !y ]jR&4}5oexA7w>9|ĒW)3Aٶx+@x 'YP@ߟs fh:9p/)'ѭT3}oB^kŴX@؋OAie> H/_?>.}P_-d*t{c9~B|U2?y\~P =ޖp,8KMVlg["DŽ@K›Ӄ~ﱒI$튠g|yG.g~TF:5(Ro7 iqoLyIqih%ZtzOT0wQIr^76jd"ݥŐLΤzgj[ćVMMܽXJb3EfB# TX)z1fP>c67>o/BwV@2$6a+@5O]vewg fT]QAfUڿZꮺ<6)ϴ6wvI.%KjJU}!LX_%ZH~9Gr\v3fqӯx=IXf}Ōf'p8>?xkQd}W0XV5RGsp*6F˻27Sv=o!hp͐ǯ]+OG?7\jHxS- &~5eW3z߶?h{ЀfgUy۵]r3 *lt[p-HоZ?IJ !ջfUqⷳTf vQEm»aiѝC/Ρʈ{XJGWej m|SQ(~bzuV`o%h/yvWAr FoJzo[0~.AU@vR} q[wu= zkYL==5++{mM+ %z^\A~py} %c;6[LJ!Ndikhc_: ì5 x^yLz٘S]˞}"K+*'L2};<o1L/j!?;HWfF+3+:ts:|X@F7-$(}\qT]vUZȶetʀΞ?.SrG%XC[(QB la[ N^|]]Cv29^XVucUPYy֪hQmUY*d׆7[+ažtwސuY2@Yuݳq|CsiQ]:W@IY~ToH{aoAgIMlST.ӰKJ=EmbЁ N9d[j~0]lL/ֹ^cUP?*y'v8#(|*hy386ؚp:KBC -<e. .i@|]]FS8y[U:{vu`RmG ܫ^7ъd:%8T>veDKH8) {UaSmpy +ZQ.Af@$PM =w3BL ]YWƗ7M #F6'+q? ?#TN"Y%OSO$GZ؋^_Q% kNK]LlR= C]"! 4 ^S ]yacjKGO P'My*S{?Wh߲'>8qE4& R6 |MۙKeµ,5<<'M*X)$6>:c1LVƘI0w1kWZ6g`o?)|nȳuLB@oy_1~CUҙTSXW$`7'٬E/],Z%'At8kkԹ{-LlZJ:)mt| Q|'ˉƎ2}) ۜc׽GZ>7wi'b}PaZ Ϯ"bޣȋ9 l9$ `ڡ mvEe6~fH&R% m˪dKWޅУK #?JOޔץ `J_8sUeпZ`@$y^I=b.'A/~"@~bWXK@vB1G\qC$/*.Nϰ U”Nr׮^MzA.[x|\ .:p.yF @@ AIW66{H;ɵU߀dpGjP^G` ¡bSI԰[\P :@Niɹ/_Z O'=it+&)ﵚe̪rU:( bx{'ƾ % }e]_GA+Y@?:5< R*g-|U`ÉGDahlTe!1!Ex\.[fZ Ҫ-ۥ!@_>t[s:r<ty&I^ס4'Ǿ!V!rzwlB? ,Nj/!Տ5D֔ d*}A>iw8ܚ3#t4_/ՕJeU=>$T4W B!/-!Sn&)?11yZHf(v08 }.l;@{A*I^RvU|,1̿D+ZcrDѫN7*L~ZRJfɀC:=_I e-jء 8R[.~ziRp~#$5$$CuEVbxHPN%z}.]&DJ{K"t͋I! B Gf;ܚtmu*mۏ2V0K _֧YS'=*9YQ§>GZ/a~@_iF  Z>髁 G;~rUzJ >M7mgb$["C VAe+uxH(j:Gxa=KӲYt=Ѥxt\ 7G]ƒZ5?5*cyڥcOghq ,r-YV&ŏ%%Nz]]vGs|+-am͏W/x-iE6hУC=K-M*.FT t疌CoON`3o]ȸ35QM #fia%TBN[Q~^nܵu~XZT [ 4OEVW%V`ICh'ԏ4x)U~\slh$w:S'*N̎(gQ¾?3Ȣ~pCTyl.x{l.KY20[g̭(;f7(};t5U3T>j )MIf[K5)&%^˟8@EOUL-l|3ۿ]mQۧ/w/j폺{4o,=K)(n67s~n@~U{$}i@F6?%aƎlc: w^:tpwcOK3uxX͋&c wi9ܯݼ#2W@=Ic ]Zyzl#&%O7 k' ˘M}:t~϶= ڇ&莘Zj1O)X;o:oȯ1R_28#c"g:25vue8p-N2sz|~裌ɖ9zUwD>ON}'7Sz.5V7Go,p<+~3=f7ȹ&k7`5mMkۑ|a%f=JL5j[V?>6էb押}M1sKܑ&b6?E@ޠ'z>٩<>B/qv왉_ݯG^|O0{)>CX ZuHնVm~r =A{ $6D˜z1凕_oT䉵Dw<ηѥ͏9D*P5Y;k}#9 [N wF+=/oeծ j6#;#aQ9uҙ ߬yM`^(#=:%֔9g^Li5.x*8Fdo93@HT86GMJ=kۂ_ ty<-mj5>OCG \|B?8BPBDlۿ6 lXh}"~u^Nmiu]ҧvugoB$uv-j8w^_Ys.MSP?CnRM;}(_wgTϞ"xIArdPZU}Zu2zUҒc+;&%Tn)l۱Ζrw)JX[MiYdG#Lb,",oB0\+vgաS@PS s (x|lSBk 8Q>μĽŊ%'w cf$Ա#Y*~'5JPdQoVuZ]==TdGTc#ȶ5M6h*79RZVL_q{晓GBH0|Lbq$TLJ]e}my!2u~ڃD$ZX}R qh#@neaBdGۧ㎉zV8\Oܹ_VK 7{t} r` e l^2ke.Ϫu-:F mHzc)P4|K3YE zG~BѦ)Л z)jv꣔<]5i;Q!]@T97M(}+iR YF-R~.-&z-pf._V=yB`R%CоL`תވ_j4N" YsW)oӯdL-~%i˘q&_-/;  %#û@ߌ>ShDK\=vlHP:Od컈"->p#SC0܋ޣWO`PapꬾFְt9.^,1~߃I&MB4fϪUkϝ[#w@ |LW|c=D\΋ ySNM褻F U}ژpۜXJE6T`/FLIVeVկqSÚҕɬetvPBTu>b@Wh+:ھ૔ރhf𳺒6#/=Y j pmR=.ɜD/ǷגtIT;_w^cay- <ւþѕW,8f8?umTi;7 bǠC7g}$Q+,15~: rjCﻅx{W=gf ˃EdACrRj0Vd`FJ lHnK(`mJEܬDrsz} Jj'Q*P׺.8Jv*2f(PA@BvijGjs-w)a3?@q)LP- ,YȒOks1BܨycP,hXw &-36Y܂dmAYЗ,U bSer0-o z 2] [ų e~#{@_^ z+[T|vld քwa6iZE7hZtw E|$AE -UUz2T`G8sTڡasr\S)G`@.Z= 2^y@(-,kzz_&;ɱCԛ ¦ȸjmH}{ڟD[PIUt3 9đ++}  =BUŮj5}fxYKO(u.ua75@i ]4j WUq2^t5IOˬi:Gٸ+̀+W+[m7GSb+^@;Hg,!J-]:=eo_s`-Xj/?&=1o?Q Rb H+!W q] `qA;rnSvs.ɦ]5$J^weCӾz@(Xy gb˝dי* .s –gj@sb&?+{+*!-.M' _ɽSgnEVZHϨ>˚o|,p@$!Oާw{ОK_lٙ7x;gƅ B Sb]Pک\$0JH.:($ۺa΀UAv'Tٴ1:$sdߊy`yiXΒ@l8O􂁾\l/R+>2?|JvO.z5 ]w`(*5 PRdSҸg9W{B& 鵆Ezje:T6wDge-pMQ`9g]Փ9pQ'@kwu7`%NeM+W+&YO {\v27oqnؿ쐨 <PC+_.ESFp_j`Fusl[K<`$^$`y/ްgu!oTkF />[29<McgnnaQII[eE([Ti!\sE<RLFV<{fgT)d.zğUK@,)K5L`P}C7!ٺ2h&'[?- zNWN[z$țz*B@yt=he!)j#@$bPkk;2BaMAGs*ip#'/!"[v z wzsUN9=Saf&M7mX?, 2~@e~mZc}99h76+NjA_Vy@/gXXmp9 "{.jP)޾FG_h*i`_6?;@^K޷o,Rzǥ'~J!=vdW7Cͬ|e B;zdY( @fO΍ /?|= @_oud?Fux%«Ro;zVRUʘ P!~ETr2~T[SW;n ٻ9 @^0$@rsv].*,{̬bs8RiBj"*xg3י >EG^K;Z/{Wp[`q l  o NTA@hcLZ~ ] ʍ"G5_(2w+ĸSkr]bf`k[Hev \ScF}OYw~|ct&8p~QzMEktFOŀd5(*Q>UjDHu2[o:K{%"Za}8{`:)+iŐ 2oҮCOh*+ޠquO/{vFU"@/^R@_mu>jϣ鮎-.Bvi n`w--kӕ,6yf@^JIDATy /ϳ<\ǻc;.LjפQtP7[\Uɍ=x_TjlՋ H:Լ 4);z_L%\ICLJ{71b w2Uw6)~lK76 j}h~uztuor / tů{݆>i>Kp%Ɲ[jx`GZKQxl IENDB`offpunk-v3.0/screenshots/xkcdpunk1.png000066400000000000000000002174661514232770500202160ustar00rootroot00000000000000PNG  IHDR\@ IDATx{\gf2\EQD 4=$*Q1evc'5ӲS-ӎf'+RL+L-K)xE eeo3i\ESz?}vά;ϼ/B!B (GW!B; !B!B ڮ׵.*GIoƛ7OnDQ3f̍*_/(( .V 80==j̝;W.{/B!S|1k-sV) 2>Q[Q^%x-(믿ӧϻ{ɐ`i V^}ҥe˖u}B!TC P_;mJ; gڛ~EQ\۠7 EɞE!Bˣpκ՗: 0NiW/ :4--mױ/EQYrh|jjjO:tN03g?~f]tiÆ :t?f۽{wǎرc8?6677kVVEQAAAB!4g`Ky{} 0WmPWyڢ~ɓgN(Kߺu6n'4xƌovjj륅oogϞz'=z v}/'/ڵk㏋dVN:t o߾(N4fpǎ4Mt_B!tۺ- >nPNtߴ"o<[ V嵵W>RT'N8|p{_|ワ\t.4MVTTHK?7d2={|_Aڵ6۷o|AhѾ!BG_} g<>ׯߚ5k.]_p!>>>!!}JeZZځv`̞={ܸqGOU;+++@u'BEE{69{ |'XU*fsE!BwS%S]N6,ǽ׳gӧO߿3g`6mZVV%,[,,,lsYxqȻrM0`0\RZgAdGHs9rw!޽;**JУo߾4MMu/t6bfeSyTBCC>CiI```IIɖ-[|7UUU۷'_ǁ.++۰aChh?g:/h*ݻb|]tV;vLldذaB!nB!P \㏮B!Bw B!j B!j B!j B!j Bc)33~]B7JGW!04mͦ,`Yʎ-&הS2ٯ,,y_. _tZ/.Ȯ͖6\~KXu\ᾭ֪ ^ڰu^8j֪R̒@ZD9V؎/0;xc|G}_Rt?RM2[~ƿU/8 t9yY'O |y 1tPg4BeV@JgI@ 薤77`~5oZ} ڛ ~U[e5/MEgjiM P%b3OϹ/\,P=DM7|!|';%}Wě>*`_j?%E?-QyYJҪZ_e׾_?'VޟzDI]3)) ~j&xOi멀nsJG% :#X EŕAsB9'CM`fM-;:j0FmgF0|=yQ7Fq%lP`T8b&vAc|˹ mO]ߊV GKeZZ7%M肠֯^wZJ_(;JV=qmN?V_ C(9Ya2޺Kzin5ozhn!}q w]~6|! )xe>74.k4]TBQ)S{}^[vXXv^ua2-ګ/*%|`UJbn"VuYאU_Wa{9ubn&`,=CWifm8NU P{Β2< -jv璇0qJ~JP'NHE!K0*r6_u\x\n0PBU'{.lӍU+:?=-Xig?՛-lWxq)]e+ =huCE}#8DPT;^<"bXgcEn# )/Xf=~%2c(˰c"j(ȔEtg/07~մ̟Q7\bK `x - Wv߹wS~:eE=?_!𳵼ɵ*ZcSuQ*ZV;꫼/Zj/֊JیН+tօU )l5vH|w␵2HWocӚhh9IPebiE{):ץy\)v&RHkpɵepZ7/6H,wmJyqh DZ{kg5KzƅfP}\j&Js. E{MԈ)ȆS鮲sB=4x\t (z1q"E#)tvK_pE:?TvKbvͶZ%"`@}_/ƃLP(qԃ<'! @|*!9_t(:=z<?{G!S<_}I|YD>F;aP[|pv״(Lx) AvpfxxQaݥ( VV@\[$􆇒;+4*XQyRihT@W*n$P;d\ͽGe>P7E*5[ѯUNQ0j(S]}k0DNzo7e^JoJfyϸ)aL_=Q2RE YV`lq* A_i=iA|#(:/HڇlEz.f &g f.OXGg8›A]i).6d7;^o׬TF |%2c0F- j g5Mncyb w)KZcp#x)3 (=,j\IP&=LÔGS4ڝ{U; xEF o*DtM^3]h;ӄ ӄ0^^yF)餱kx3ȁvkSU_T}C>?f3ƛ]W:Cd>dz@yJPaﴔ Kм(EnBzkT/S/w)^N$sJ7]ȶոN[q 3T>zZp㿛2e uَ|hn@ˀ@tk̺͙2ga:X×ja~J^^y2a;,Gלg(ꝐxG%OׅjГՑ]l"v\^6>4]騔Q) z(\فw1AzD~99e7@WU#X@ e|i9CQ}\0X9Ē٦׺F}ZxFA2VIk{W/4ob#5^oYBn:7譯m=DH}[")Q,ڥ?U2=Zk=|^BӕnPKWƒ{_8s~kD (Wщ /uWPPBp˶"p}Նf.0%{XT]Ƴa.ʴk}/X] jƚ ;.n&4'$4(ӏQUРSC"eG? 0V>\Q2ַZ`O;n i ^}K`Т'u3Tb\Lkg|> 1NWՂЍQ&t^]l&2|G\G믺-Q kod uEqYb'pRswa'_-xN'IYy 㷛.I}`F_0U`J> 8&X LPFwJ ^tz>npU>( RRYI nPWZL)z3{yS-s_*^QtkӪU-,Ld/):~deBo7܉ゅF/HÙ{#<_}hڦ\hX\EpeTW~*xmhW'WK+_zYWEz^,|PQڰO%oWWvv}hM'&C,%o1څ} bcX?i)]Xv\ 6\X|_5EEݪ3I*}9ҲKuoTfd.UPvP|ȍq蝽觺_?Rk\^8Q*UM{9E0ni Gh ߬F)P&:V8& ftxIYȩ%r,zAQh@Ld5P".47QNK(ǁ|\X*$ڇ0:2sXB6Ǟ;Q&kJ`p"k=`Q!BfW~<'X9.L(pX0* Ud2]Ply{#!ƌ'!mYcTݔy=|K&#Q$o{#ПKe gjݬ`ڢ=6@4145B!״oog]AMykE0F1w B!0!B4B!B-4B!B-ߡuy7,Z"gV2_lAPK=ܷ?k7%G)}NT;)0pvE/$:uQh4Ӻ|qIE6P .u{ؗ7KGbV l{f0űVѵThffk5Z.t'^-?MUB!&at뚤ҳSۣn Ojqf'w&sw:$ J 趽 ioTklx.؇f哺.rf}i*:cxWk\ B`tp̣!oWX%6OT_^yro]نr$U&1N`}V@-É*W;幽w6(R }eTNj( CJoJQ)|S4K1+y>.1Mu:*tT^J iIJ~>\.,n(j_՜onMl{olxn.%HN3FPRoV]mB)De셮/WoNGU.Q]rosjYlF%9qdU{ף v4 ξ緓`&D(t>%mɒmufkdEpkppbM8ZVm CQ|YNhBEE #95{#Tc.(:̯VS۹Cnnՙl[wEYBRwR `bѳ{g/Zn*ܴ^MдSRB!jYcT!Bmy!䭻Yk+B?Lf}կK"BhB!ZX[p IDATǁF!B0F!B0Fm%WFnΎܜwB;ЭViH\Kdن+[~#'Pvj8ò-lI4[_En-!B*?mʏNG7rL*l -JF!VF4YGWOM];JF!Н$.w`J*~QSaQ"f7?kќ{@Fvl_{iO_ _a{ߧ=UE۬2/wU^"^?̒Ct9U} ՕWX2{EX']zAEj2>7%>{Q?HZ>}gҳkIz yҖ%?_N+N[ޯ7-\r`33C*?YVǭdB>0U m\_`5/YVVtC_Qx+A#^xNpmN]Np^*$_+)ꄡgW*#cEQk$rUȂB3_a|i ۥpfl:J ,k}|QrF]TTT^^l!111ǑJZVV}\.'f#l6NY%7]<i0PQQP(ZmDD!Ɔt|0##܌!0H)ׯ䩧*//7 Z$%Ad\\\DDDii\.$"00PRgǑNDR 0$aFsG4Tp@i)&l6y^\\l6z` /HTğa@FI> ?M_Τ'j5{,th !cCvV^M Hd"!FN8a2 F&gflǏh4JCX@aa!qB!`+V=Ԁr۽{E233@tEGGhֳl ֭륅ڶm[rrݡC222Ν8VVVΝ;Iwz~޿p8z-rE.ܹUNfgeeۭ~:999;;;11fz-BBB233#""LcccQ6I7һ-\b˲7o8q"YEQWWq$azȐ!&)77jrO>}Z6###'',=>|x||ŋ 55'߿}8SOw.Yd VdY633sʔ)ǽd)Sl߾߿ 6l(--5kVN tґ#GJ2fܹ۷ooV5!!a͚5M\0L]]݃>Ȳ,1vA҅nߴiS~~~RR?Ȳd !$m:+++55ԁܮ]ӧO{2e󁺺:R͛7={666vΝ!&&ȵ8nҤIk2JKK7yde˖%''K?{v?~ z̙,(=-fzRF###n@>GN388a2ڝݻw;wO˗gdd߿ƍR{) m3l6NjWZ ߺunM'lP26FLIDU7!#yPhƎtwyǽ(SN}?~j>C:駟2=&.+$~ /z,[TTԧOպf'I 6/Kl6ہ8߿ʕ+LLԛlƴ\.Tut[9u]趂Kb8߿BBÇI M8J‹Ș) NHR)$f:99̙3EEE9X"//ɓ$\۸qΝ;kz-JLPo( ,00⋜͛A3dfffee >%%%dѠhviphi@fm۶mԨQ>IȁJ>K6AAIi,ˆ,K3f'1!y` Cm HJl>tի't#Bm7$22 J_y>li@Z-sss fNZB%Q) UgϞ]|9HLL>|O?$M:tl&Ç?`ͶsN6Xm6r_uOH%sxg!4o)HB&{kA'/ Ɔ1t.]hÆ [`AyytWj%ofM8q)))zm)))!3P@fffuuN,Xz꼼0g2رc3cƌ|eW>>E!Va}Cܻ姟~^xf G߹s[z=3KΘ1{ fپ}{LLL߾}dgee͚5kƍd&OB!Ԗ`}CHIzyIt:'LA:hVhl&)888...,,L 2HJbLu'ž$&>|o߾o\>nܸLBy~æq)SڵkԧO ???>>۵k׮]&O.u4YfPQQAӴ4Ia[B y;$mB.ҥKG{}7*"y,ˤIJJJnݪT*׮][PP@zw8[xqvvV%ӪM3<j*/t1䷚nݺov̙V0h Rϟ?ܹ'N\rܸq$ZB=@ߐ GUVK/eeev]t:>jԨ_oIHYYْ%K o>,G"-4C yҋɲ@R{GiiiLLg͚u>Ha;wd9/\FHɚ=J<+h"ixRRƍ?APqٗABGeZòV%/[ y䑬,l6eϾ}~툈/Iyv$9III_}Մ ͛n@l6 Ǎ'ީSH]dFbbHH#B!(7=BVz,>11a)%S=zP* .,,,˗s-E$!!8qd3IIIR{srr ɡnjaHX, XᾶAκ+?}|'%m6uuujzƌ HEEEEՆ,X,YҠ&%%%$%+iݻsrr< LgNF!Va !3'xb2aaDDDAGa2 !gϞ999#JJJ>!CqԩSׯkF^Oݻ~zJKK4i8VZE:_vڕ|wNI9s4C=4o}z^^`P(?#\ABHhnݺS{db 9&Ik֬FERhrALP(-ZvjMJJZz5I ߸qK/q\=fΜ)67nܸr#GJp IDAT éSjMbiӦYVa2r99]cǎ%w^}Ngtt:tгg3g>|cqF)8--AP(6nI.Ď;V\iӦg̘1x`eb֭jG"_yFCdFzq:B;7R+Q~ e⋴8F[QQaۍFh4L<ϓi`OM&qRh2a„$Ţ@`0$$$HSWƒ1֯_?qDRI^}k`ݺuz^*Yӭ\rѢE6hS:m_KvoSʙ䉍LHHx'Y5 ,˒pKWMP$''aQ.w&,>s6mʕӦM{WW! Fo)Bh4BBgϒhFzKfϞ-}mE%\4>{&gunraLWl(СCM HW0aB6p3eYh4yyyf:~ xHo4gرc @!t+a}CF}Sݻ7yPјL&VK%Cj$fu:VdYVsG:55\.t B+V\pF靰&$+)))..lkLBhcƍoϙ3g۶mAAAgϞ=|pQQ $?r cj4i$;J%+j L"r0 q4M[,lJKKKJJNgqqd J~{\.&yf< ={V.-,, Gddn:u\SRjlz4PRr\VVfZIјr͛7Ko &Mlw&8N dNG0*==}Ϟ=Zv̙Rp8vۉGKm6[bbbjj>o߾3fAكwѫWwBy IIIy7¾KZt:vRґ(8ѣ\.#Cy($'#Oh$sG"<ϲ,LAsrrKKKNgII -iZ0q Fr\qB8j:u#֊o+zھ}w)KWd2:uJJRNLMӴY*GRQQAu$ލpgn[YYY! cƌaY۱cɓu:݈#7555--m߾}||8NT92&&Fr)ii w \RRr1T*I`٬V\.'z^%'dz҅\XXHFe!%.Iw/,{###y8.$$D.z^ON:JKK Z!!!'Jzm6[~~l8@Fchh(F#Mv=??jVVVjڨ(2Q"qz^Nj ϥӧXbΜ9YYYC~KC[QQO{g0꫅ ڵ+==i6ˋ^KMM% uuu} fϞݻw﨨ݻ{˖-:t[tVXvZrxl6ۡCXt$4d2}glׯߘ1cڵkĈ֭ 1b'{9aX{%_bjK0QR y8+uWdIxx8ם4 fa~СCd:q܁fsxx8qš:yv;v8Β4ML944o߾$H"]$I@e6kjjJ%Y(PJ3 %RdVbccxLfIMh4XƉ,˺/lƤ|ׯSQV^x/={]6l@u1I|:e 'Ԡw}zܹ~m;tp8(ﯿz:t jR8 fyܸq۷o'_g͚E$LJC%@IIɞ={t:ȑ#'̓|3 O!IFFof͚ &{VZt#Q{"55j?,KʊJ ՛8 cԉulm'PWLJXz;hVBcxB+"v.A.Ã!Қ%r(ȣtpy}~!rfPNNΈ!L&S (J p\FbVH8UVVpWWWX,Pz'ժjBHՎxuHE? J% IPϭV+I$IW RnJ%󧲲28?\T1j!f[[[Bd0@yB?Np{ؑ:SjN @8!,\緵%$$˿SO=+X… ruwwfxpXt:Bl6{ aP|||NNh/944t0j5^߹s爇[,К5kP^^^aa\.W`3F1]vW^y%4$!7jRRRJJJ~~Z(Άc%>]80 ÇNݻ$R3V!TXXXXXh"_zU$N"̙3---)))%66dh;w,//L 8رcc$INNJ$I’? .>G|׏?nZ¤ ǻ{.A{GKB|>uڵǏw݋-r7n(((1f͚GTVV677$$$DW= kjjm6̤?. NS<cIG8wСӧOs8D㊉Y`Aww7,HANwn NٸqceeeMMM\\ܚ5k"##޽{t46؅1prE$s\EEE퐚J{2@zT* X  F ZV eee̍!t :::r9gdP(d K"999$Ij-$''l'pܱKh`ie$HV+k__\. BP*T,X2"8\#Z[[F#B(77>3T0v`x, jARNG;\)ʠ‡GF56؅c[1:D <CCCS3A0nٲ%==}ƍFѣƍoLV֖` P7B詧b4`0Xdcx<,+ A$$$|~ٳBW M}}=zI| OKK+,,)))IMMݵkabef>#@ҹ–۷O*NRGNo7dٰW_}+Sx X bZZZТܵkBN7x#??D>}9X,۷oGA(-[\|Y*>|E[[s=W]] ޽raK,7|},KiiiBB3ƍ/]$Jw޽yfөV y&wޚ5k!%3n N3z,2l P()@X,$].limme\.t999A8@ rv{__.\@F)ɄFZ-XHcZ;::@,j` ~"HR YH:#0l2  F;06..\$"&G[Љ$NLh4_ ![[[fl6L]]],J'dV{BzжuѰ1r\@@/҅6*^W,a60Y Cjj$@@`1nlLsG@CHb!> a- $9XH$vB 觅B!-"M&N.+@ ֒Л(j2KNd Rrl6VZePE$A0!NQTjj*D 륽0A`X 8NV 趫.lL-X@O+sA@P+6|QNS(X,fm *O|[u GH`eIhMQϧaO07\.ɀzLv:f… %%%` o?)t: 3yz[[[תP(@D(t:N03)kLV0P1$INN^R*:tAeitзx<!HYoܠe˼VjjjSSSuu5\l6CoMJA[@( XL4G%ՠ&Aび5A` -  XtH0`=̺耯#t:RaA@fV<r) Kfa@ p\}}}$I€NT$Jy馦&4\ UbXTT4 #D6$tdz*@ Q`% tc\.$ID*KJJ~P5655M~;Ǚ Y+J>5C$EQ{G<VJ& x F;nGj6{aJJJFhj #tEKCЩjRJ`U(:n4uʔ h٬P(Ʊ.+z} C }QQQCC,u 0Z[[黀-A灮qlt:V%%%C:hK 3 i6阥zB?\8LX@O+.a'8CH$AmTx!77W( |a(aJV</++K.KR8 6,CSA{-Ѣ,gefNJMMخ cII IѲXTR)au|POzy<=[4$ ZEUYYFdjr>>8N8r:"hVNSTTd2`zZ 4l2n(\0j`u:sx 4l>qP8Cf~@@RTa%ZmNNNd,rK!\NTjV aEL&DO3Hj].tNDg5S.G 3&IӕLVPO RЫ$^J$,r+#JAٳX,b2L0T8g͘LI2zZE#ANkk+lt`e0 &EDVV^0,f3&IP(U + :… ===Iс"lTCu?,dT*BV e{$z`=Qj! yMMm#S.CvDd2v\Vc!JQ$E"T*ʂ  Q9}}}d2?%bl6Ź\.pNVr94y@ VJ ?` 8,@ dZ;h t*A0IX0iWWWN&rAJg`v; N',H$Vи\C:0 , hZXFvâVi aMFAf3t:d~-''laOphq^oWWDn1X@OАU!(U`x.80iܬ);;h+v;0GXA5.hkw2*Vo(@Vh{r@ H$6M}}}t:~/]afp:`N]TTJ&+}zīzeX`x2I9CdXB; FH(X@zZ- kx<sⴵ(G*DYYYc10jZBOPՂ.):(tLYO@@4[B a3F @OcfÁLÓ)vBv;wl!C.:;;/^E%''=z4''7DFbbb ~O> .h$}?О={vB!hB`DQQQ"5p?44h0\ٳg! @jYf EQׯSiӦ! >CRdϞ=(\.11KQD"9vつ!x=Bʕ+tb%KG?} Rt!O|w?88x40v 20ǎ 65oVzz:2 N_|>Ap8Xlٮ^|gϞE#(Bmذaڵ%~{ؼyZnootM6G-X> 6l߾4 ׯGA8#wIIIh8BT $%%-?8/z=4,[{{{=ORRRzz\.j̝B7oބl6X,|ɧ~:))iǎAW)((J.]&;$أۇ _hQ]]~[رcS IDATo߆_au5' zn)\0iw-//VwF;w`CΜ9]ӧO֒$v 67{Zol6Kͦj?˗/;˗!>Lv766fee ! ***L&4EAQQtJwށޓHJJH$̓DGG`RSS]v!8*Gվ+bccU|EZ+(GTIBWh~~GDD04ti6f"wpIBqYŲxb ׬YOs`?e@qgΜ9p_~{۷o+ %xDCsPtiӦ:u $ |VB]xѣkߺufI҂^f3p8x 8޽{xSN 3 9'''==[,3ONN$r tE]rX,.//X,7nѹsH\jUxx8݂#1 XzL&tREE#""bcc۷o/^B*.[ !ىp bbb.\6-666<<<))ITVTT;Qp=@i||… qbx<ȁBHS3Oss~+F(}t=|nf-]Aç+W;w!;NN7󩩩ٵk׵klvBBBkkkGgggwww[[[www\\\XXXUUՎ;6o<`l޼9ƚL'x"**޸uV$|Mhw(h97ntА`_5&Ah;N ͛Cm")))j؁,V}իW>sBG`B~hh(""vltCwIII*? -y<^[[[ee|>4j`KNHH 29z N_|aXVŋ|=Liii+W )b**h?,+** np4mvڵz fN9Noo/EQnΝ;6 }DC (*x7R>|믿kע H]h"ә;wwwXիW+s:`޽{ = ?~poXv9{AHK/ݹsV |fdFOH$o5MKKK;jnnF%%%mذ믿\x1 c"##-Z__d;we~^zݻAcBEEŵk V]d޾}{aa[oE;ǃʕ+;駟zC~d̘IHHDgNIcX@4ƍ\.b}D"yѰ믿lkkZ>igqALIUXXxQQQɗ/_޵kݻwZ-njnF/i7mTXX_[rV8m0XʆZt)l/--={,P1G;w|EBmvԩ3g `A۷=OГ2q>+V}WϞ= ן9sܹsA[#Vg&CCC>UHJJG9`uvvR5 ^]v VTT̖b|*ɓǎh4gϞQ@~8Bhknn㩫( S ϒžVǐwޕJvZpOۛMrJxp^xx߿?x 22駟w `~,gs`n;::WEw"))ҹ}#GtR ###I\nŋe2sF||g}H{v@A6 |@8˗?!tC o >nf$.^|+V@4P6omFEoD"##U*۴ٿغu+\.))>x\.?rp8(Ac۶m'OI51噗KKK۲e =][WWZ~}DDDDD}dzf͚'O|> zZǓqG~SBss3e˖ F:5 cPK9XQw8`eݺu?K~|~hpG2H]SSrap+8Bp /afLABJH C.:P(d%|~FFT* J[3xKJJfD"S{7bZl63{WWw9N2b&Ḅ@a 3;#NO 4W{0fDGG?ڗG쇇9Hp8GunlٲЅKCٺunwXXy;wle***D"ѪU֬Y#HƿhZq--->Wp`dĵ14 basO>hbtwwYlvBBիW!<@߿(CW`c0 fl`0QEGG3GJssBd7x}Ġ7odXcFF 8N:;wX,x&2P(֭[=_P(/ u-Xի[nݽ{s= `@gyO^^^ZZh'1tVl Yb0Llg}` r8*FCgPc :37n$&& x$Mˀ޹sDc@LX?p8bccap#` ӟz=ИɃSy4o޲eK||O~N:lnnNAgggAAAMM <WTTj9//oenX,t.`Deb@vIqر(x}#~o U9twwƮ_>===%%eŊ fo߾}ʕbI@P(- <[__\+Jh= 3oh4 VgCI& Buuui)d!HTTTjY,Dip\</("T.RBOJ1=V+MAsssBz~BgtP k B999l6["zxtVU3ǽ$M&A#H  <KM0<Tj0L&TZ=VD"|>?he6̏BN\D@ ient0X@,'$4(1G.  Z](*H kCCC  L I$&zBpQZ=Egy#ab#(J*xb%t\MMMVdQeZVlp:?8 tۥVG0g`1eeed45 Smwuuk27/:n 2]`=c(` c(KJJHECCB(0,&\. `fJ( HAMd 8a1k! *3%ZVz| ->C߅傁ӈ㜎P'}6^Pr\n ]f잞6PGjǁjΤUu 1j:'''+,9N0 ͺqgz/"1 R-FggѣGBݻa }M6}!Yoo/N>|v{< 2wa?MQȦMb&%%e  00,Y]UUe^u/lr.*++%Ɉ!aA[78Xo d[P,aoooG3> Pm{4RtYgɒ%wXЀtxhnnF89faf*?c;Fwl<vs85LQΝCky&˅L蘯E=SAu{@W566Bnț͛htx"Blpp(ŒZRuuux+Vpũe >4 4EQ>̼qƥK/^|UVX۴:Ћ/8ڃr BٳgϞwuu|"FCh 6&j\P7K!$Ɋa9h`07o0g0ɀLxӴU0ƤE-hPNDR`ϳl: qJoq$IO._8 ~#om @UU,))!I .۾}K֯_7H:1i!T0ܹs$I&%% vT5xAرb|7o sᅬey !<O(}v}}}qq1EQ:d2Lꦦ&XVHȇOD{{{}}}"ӧO37677_v-55\z5999ț!qf$} QTT$ݻk.oƔ >!L0VB]vm_~+7oޜ?-[ vXs>߃'O?~eӦMhtMh"6}UƖd1K$˗/c`L_L,g_bgyFՒ$y愄+W\rt"4p&>>cLO׬Y|ׯ_'t6L-E m4 c^ h4B163nC^+JPRRhJZ!Zm4vd***xytfUVVFlx.l6wuuA(e0Bc`Abs~@]\.W^pA|6m20YYY!ߍL&FdWW!B*B08:t`8B urH .@;BAQndl6e $|~ 0zY#  c0vl'x$,*n 8eTʂ|~uu5dd-)++]QVV țzAr\/R*pr: TAddd iL&P|>nh4$C@_  G< P744H$zьT*-**\.qIJ؛(x܇9tŅBL&STzl6Sza. ץ €M*i`Kvv#?,4A٧ ,>2>J $D]]]JR(J$Fd~͠}n IDATBrrrr9$tzrCCZ={zze|>6"xtvG@BYYY0 AL4ܔ3ot 0ZVBFZ+v]P uxbҦkRh &_NIsw4X@ ㍳<^`=M%X@,'4Xьӡ\.l63r\jfz^k4z)]"Ҋ4 @vhC\.x/4Пљ ń@v>,ybBaf YdIhp8 ]E$vA0sЩ0Ƀ4?s.f sf[|l}Iyl3GMsssiil3sl3e` !! {{{g ?jΝ;o.f044>o`0,M(.5f~g) &&$I8` x!dZg PYYu;v|oߞ|O=0S7'11]\dʕ[kkBxa?888b#"""";v8~8 ;~RLOOp8ͧNjooG M6޽{25qbN􏚰.fʨ<|_פɟ-""o8y\v{NS$!222n݊:v˗oݺxzv?ۥb~˗kkk{HzꚚ>lƍ/,,CHh4?sb0 |v۷%%%e7n$"66v2Yiv~w ,߿u˗/s\JeX<Ϟ={=O@`5.t:?~ԩmMqe["ׯ}7%%%ׯ_Ք0F,1@eeeKKL&h4f奧#bbb~lPXX؂ fQSSXXX 3rÇ_vݻ,K.+_l6f_tҥKZv߾}xwW^illr b˖-K.'buww֒$w޴W+|>qJzz{{ HOOvt{=.[\\*((wٳgi+P(ܻwʕ+|Jb_~}ͫV s8VÇk׮nSgnrʹs###-[/F`RPPo߾wah8γg{E߿f!T*ݻ}RiCCA111{h4aaav[\+l>_ZZz "jkkwyڵÇlJ:|09N[[ŋ{{{!Iٳgw؁90 s>BiZ^uV~xD;;;ڒ/_>iy~zڵܚC8q"99999Ν{WB"h޽֭ =O<944-..JNN^ti\\\LLG}tu'N`,~(Ȏ߼y3l?y|ڹsgtttdddwwwAA@}j:??J?y,^eM65l6@#ZZZ?vXo_>! կٳrիWM 555կ( EDD^V1?Ç{?hoor  S EQENh\N"ƞrlP(0"I2##nm$ x.r\nnn\ ##!d6HOOBH&M_B!u:$IB,ɤRiYYYOOOWWWnn\.񊊊ΣB<Ţ{`ٹNjr p\ӊD"AHfv*++h4B$I ࡉD .r\|A Fc0F#I1ō}u8,RVVΞf͡t:L&,$IXaۡjFjB*jndD&vM&$]]]fZ RBpcAjj̔@.mmmo7~ӟvuut:Bl6;---""",,޽{aaaE X,DϰlgΜ!IR"DFFn޼/ƍ~0ij`;uuu---jziiiVJOO߾}g vٙG?wܺu֮] AFR]PEi(ӟtСŋ/Zej5566رVMPx㖖FEEDKvvv ]roۏ>z-Prr+f9wB_ZZZ> 99ĉ0@:y>W_oڴW_-//0?br3gμkW666:N6f͚W!t:Ng{{;E>%%(L}vо}=t:Ϝ9x/׭['i@pƍK.T*V^^8|FQW0TTTgdddffFGG]vdɒ\(<<$I~A;v8{,n2#GXn߾-J'Y 3aЅt B˭6L.Ptt\߾> |>fK$,F!bg %IR$Et.Zt: d+))z(Jz5''R< N ryCCC h4̯b!T*۟.EAٴZRL \TT$j58V.6P(j5lFLݝ!fׅޯd MS`YYYq1|{(H\NcX]]]2Lm\.hyB9o.3Fp"۷ooiir`Lt׮][f͋/#C+` =?7nܸy+V.X`<R.^/bǚ*ͶsN4<;R__׭[ٙpI_~}ccN̤OEvBK>x?8??$IL_jˆM6j^<%)]G.^X[[P(^g}Brȑ#3e˖mٲe?|ccuD2 B 7z3Utvv+W <3ќ?(J5gn$Inu:] n[VSPYף5I]]]LK8 Z_I"n㩱dZe2BV BBQ]]= B0Vա?f f&IBN'It|{Օ2Z-UlJt:vgʬ,$^#c@@!1Agg'Bh_~eXX3(p8m|+V@@=pIt'ŋ;]WWwK,AEEE}'nڷoIo6sϖ`/$Ix8?v/.?/]?O>y0d`lH$_~CL!;;v@􊊊Ϗ6lxgl6fz< x<6g1;/M/--}駷okݻ!p8D"૯~zBe={B~~XR YX/"++[0)oݺ5%%e<ŋaz믿cҥKo6NX' ]'==111?00/߽{ĉAcbbvyCChh4<!r\.Ac{w'|1'?8pf=wB:thɒ%iiiG.(7߀3 "¨_c7/ɓ'}>M$Lnw^^^cc#BPB}єt dܾ} 9;+DGG744TVV۷7--㵵ϟ/Z3$INk574xjf` k0rss `0aPՐ^b3 jЦ`0 !X,I6\.7##%"!@1PՂpGZ|^ - \+T}}}K'RH$bf> zLq`rRVuĖd2qX,J3(nl6CATrrrZdjjjͺ\.a9q\iV N$У{64EQ@b]paN't{abT*H$l6bulA7\.1z&BDB@  A?An@VXJ2¨I,3 BrNgVV{!TZhstZ g]@V1,ge @]T5z\Ɯ+**h24kze0~\.wD R) J%X|N-FQTҦTfݣ0!#0tuu5<zz=/))ZmQQQvv6C? .> *p!I__ȆV )1 * >D--xt`xڄt87dAy:::ƹj@33F 2Un*QM_^,H$,P3,Z,A*uqVURk jB+ c(-RMiB$)%D` 'e!q-/ "AWL6;;^׮]Gs}~i?phаp<$h6]A6׿;wb#GCfmdsg ŋOg?^@777CKgt\(**@ wXXƍaK dR?14DL&f9`yϙ;w_$-f0db;|T 6˗/t:|}}Bt:p(J袇jjjR*"믿6T*%7xXJKK-KRRR}}}JJ#~L& ,`Xsoׯ_b<<7 ى~SΤd[֭[aaa?/^,H8??L$C6b޹sgVV֐s{]=<ŋ?#B/бhaad@Q,BES]hT "r2r/^ W'aaactWuX~G+Ha w1.z8Al&E#cL"P(\fMrr2  A4frlGVV*I o*=#TVV"233+++5J~@ (---**ݾ};Bfeffy>`ܹvtڵkD~YMKK 2^'O%bX>q`0 i2ГO> }cgh IDAT B>>>& E>)J`6***L&3|>(==ciN',ΤnNU\\ +cC8}P(lllz&É_hR /pcصk׉'Ξ=;Ν@O7l6[ ( X l6|~CC,y?l$ЇA[,!#X~=BUѤ~3LP⋧O%njjB/4:)))e=[➻w2̮.Fq_pa|s} N; b5Ŕ{n:p,XBeeeYYYSBPQQъ+F kn4z=<{A\.:rvd111EEE.+<v`MLLrJ]]BhI8n2J_$ٳT0=ߟdFEEf~wygd `8Oh4ZFFFzzzRRҐnK$٘@jEÓB0<<wU@XXXMMkkÇ{zz|U Áۺg}688x977:(((ڃ7T|~uuuppZqtR'%%{`V믿vTBTSS3bb9sn߾O?4*tUp8[nQ!HzL&C%%%Dxi5dP(t8IggSSSu:1LC9NLj:4wooܸgO~q2b2BP8   Cl6HOƐ?MIIۺu+xo+..qDq֭[rMZjz jZ Tt8.JǗp\!!!xހ<|pTTT*r BH ?ڵkb^{gǎy7X,@ oZm\\ґl6A H‘ O2=`>>!onohh(hq<>>~…/׮][]] WD3j5555MMMG шxAA ::~6ѳ1gO1@*^zU&-]4//v"Zn?>z;wNVgddlٲM6UVVT*TZ\\ߺukŊg8aaa@ pBbbx饗P3fuTWW$ o; ???պh"6}ƍfmLd &&`#y< ZdyDaDO8z'N fvW*^tbXڢ[Nd:j I gu!B!"F^?{JhG*вeˠrϞ=d 5w/bg۽m6Ϭd@@@mmmVVXrkgtohۀdzrL&p&@cqqX`n]MHHv|? <<|޽XP8!!a0 /LG}l2=h4mmml61NX,~>"4۷ǽgݎM1mF 4㠵kr>썍miih4fscVfʕO?4 ȵWµ`X_>o޼ x,F(8T=Xs VHJ E555AsʰVtuut:*5[sKb?~tedMMXjՈL˗ 'W>z(((HKK##(gfBn;F#Zp %tYZZ<[$]pڵk׮]o@蹆6^)jhhhjjp8`? jFGGSŌC3bJKK}x2%%pLDqM_?A\\\AAAmm-OtttHЕ+WT*@ W VX}#l2n/_qƞbtpic<іe#8˹͛7#.]Z\R[G("NS.4 D[T*JY1o޼b后}T e LA4r9B((fwe>ttt椤$:VtO~:YuP(|'F٬v+~…#Q,6MREDDҥK/KKK N8q\xQTfdd<ðǏ[֋/ /퍤s|;p80X- \R__/H`"4l6B`0͎ AW^0lѢE;"33!rN:{Kt60 {W1w\P2O  bF g0+0LV݅f+//Zjdu?|~ڵ~ԩS:h…O>$8(sbPTfK,w…7on۶X[]Ps֭effBb26Q% Pe2dժU㮪ZvٲePL3@$ݻ711rKLLwC+ }***$ᅬH$wRR;*fy"bRyflb Ç 1555===6m>dҥlChv;,+==͛\h^ 6렾AT!뤤p\(q:j:88baYKKL&H$Ց'--|>_VPI fx}ڵ+Wjn輍JII좢/\pŊ#uŊ{niizQxgృ Á088XӍۯt\.wo @H-_<""BT!jkkd2_ d])Fc=btj 4 pOyyy)))fwO>2lŊJMMKĦrssjjuZZP<>ZZ/+䴴)8Xd}]LLL``ڿnߵkmo߾tܹÑH?^~Xѣ?+**R}gPoo/<07Xz5χFxmnn?H;g&)J׬Yf͚X5HLL16GdOTTT~gΜd2gGlb233f.TFgj4J?QT:R1vz555Flllee錈gje`})>>>00bAoAJRR}7ruYʺ;ILL UySS+RXX}v|bH0+|>#VM&Jb2#z?S|>133sJ>ۄ0`0lxV_>Ln @*M*nL&S@@@VVݻ@}QXX%}6l8uT]]A .j~~~ #66V*└ZՂߺu tEAzHDJA`/LII珅aJ+..̄jmS'D"^נXrrkPRR?g2v,77~3LAӟիW^%`0@0M=g&l6;77w˖-^FOqUZ- SRRbbb]v-_\&!=" FAA3g ~(v`6F4%wRXX Qѽ}tNC}m7nt ={̟?۶m'Nhjj&peeeM6tUV+`,$ n0xEGG;v,??ǎׯ_>:vh2J1!tǎ/fPUJё@,=zh*Srss,YR\\|رz̙˵Z`vPvn۶BZ 8*(99yɒ%,KTBoIII"H,xq_hCm۶ R/0$7v8_~FOB8{ά!(kJo;;;<ŢB~疾޸q'|B*>jjj V |}}.]ظd0<:}4l ѭ14%%eR_U*ϧh~Fp8DbF0l֭5558 |wi䂂%KnQ&ť2p!TzN'}||␼^ahiVTTmۍFѣGp ~; (tTjIgn!f_vW^=Cs>"@9s&**]v!:::F4L[hKA1P4Ak7xJ#bjN(^;0U(III)))L㵶D__NX|ƍcǎedd Y#QT7fzFHaŋ7mڤ!;uwJ3p8\.7<<\nݺuˑY, fT45(3gdffQP, xJ잞ZxI"(VKG9sttt w y(|>{* ꪪ؀`X2LH6P F*a}ɓ'YT 6Cmjjjkk?`0.^ryA897%B>3!dwwwT 6<}0)h˗/kllm&HW\CMA^n`^+Hd 0LЁ9&&lpQR@w^.+p0 \;rHݞ N^^k6i@D?F#2达l7!!A*_W6 bDπR4 m۶*^-PJFAtvvD"r6 *l^XXm\d*++[p׮][WWI;Nf2\RQQk׮k.Y䗿 wjj`Kҗ_~O7S^Ntt޽{;;;(- `J7>}zƍ_|E^^hq^\݌OZZZj5\.VrJJJbccm68l88XR%$$Zp$&&vwwj}X(- !Ҳk׮ÇWUUԀBp\`:&%%!$##jR @ <W;PD"IJJ_luncY`0>쳰~f\.>ԩmT*nǣx TgzRJ*0̃_~frZLRLRnRpOLLlll$o 슊 ZrKJJ<bFfff߿VP 222F@ N7#z{{BtlP(ӚbD"d2}G^X ޺(J"A 0`9rD&s\r(>jb v; 8FKJJiGqqqbb&GGYуҥK|>,:<p,U@[ZZΟ?Ÿ` tZsWUU!N:ݝAD@@@WWB襗^ PPx3T=[)))^ꫯɾIyupL/L,7]]]G`T*ZmRRL@`"..f8~>"/B,CRmݺus-..\Mb2蘱$ F[[tAuvv:]oGGM'[nl6󓒒ΝaX``l6 |>_V!SP*!؈O=Bwvvhlt۷sM6T*J%._|CAN!Ac\  4 X*##oxbT=p8fyǎo`f(ի^n Pj, =3u Xu!JA1obmۆx<fObQQaal.--.]%˗CS |)g\.aX`_y ׯ_ߴiSuuT*% W"@?<|3o"*FR^!{!NgaaaOOO{{ 5cB__͋J @jjkkCxNP!8%n߾h nF!n޼9"BAAA۩C&!)!|`]]]=GFA1^z "33sl6/ YrϞ= yb۷}ٞ;wWTT޽;)))&&&$$4Z򤥥 ZO)V+u9)н3 e2E?J455nB`L<'2!$H&wSNS(˖-rQ@b ӹ\n}}=4ba2L&fc*F*ӄN{饗 P(<}g׉s璎0 ƉWUUVH$R8((ptvv[.88x̺KWSP<i!ÚQ\>ΨT*Rr|nGzJR&dS4m_&C&-=u# , #) I)T*UEE\w!a_PPTy:: &8$pFFj5n'#Lfppb7 c{.BhΜ9L&300P( EN3g}zc#O0J… ?ÔHtԬ\,##>#4 ΝKMM7X,C֩ryWW׼yfмiDDDB R 22RPT*{#@ggBXhg>>Y48Z̢z {{{ѽkXjUIIIGGoS]]]TTtWrAJ!vl6&dDnv].{ c!11r*zq^RYYYG*kMtsr>[\\,JҾ;v+P6^XXhGl5L1Lɲ,ft:l&/X@*8NÁwu'r:""BP΢x~s=dHHZh45c^ރf]tfp |Jud%t s~zzM FZFP(t8A$].0F[@(H$Йr6vl6f9a`0mNxðÇ;wfggC6:44.//O}GNL  f]&,]K.z=yrLLLYY()P(!rd@6Y~ł'##cgɒ%UUU{Q"&&Ctp d; @0㓒-H4I\.7sǁQLT=Ul{O>1HM7|3??7),,to֤#Ca[ogϞ^9=tvv2L5Y9555MAggիWB!d mOZh4BfbDl6w}'HZM0!TWW'ˏ92[VFv4d2i4ڐQUU%v9)#<8h_dۡt:1P@.%vx>>> ||||t:r8P8g6Mܣ?`!fr# @O ---6lP*L&c?顗ʭ[&1 MJJڳgOeee~~~YYBeeee9Fl6 t:r߽{0:9E:n4IC7h5 Ǒ{xÇrnܸ!AqDFFlJ)' zJ)B($$D,+J\Nг\ۻlٲ!CwHHBh+ o߬N Cndq\FMVYLq޽kZq,Áƍj>:y0 SD!!! ,@t:+;WAГܱc,$''%bX@ LbUH$شi[oU__gffwc27zٺukSSjUH@ hvj1 4mǙL'|__HEhyD S2!@qDj^}Jg}6<L=[9e bΝ%CNh4B^Hsp~_d]vM?qĉ'F R  x.bX,Ft:=44f@Z,í :B!uloݺSk X ~TdawwĿ I '/7c|>_TlIJuuu՛6m***ڿ>yشiSvvݻwIiiRfZ= t:а<j28/^paVTl60R !dɒA{?KVr8>e /577S*OedL r>4#δ mfQ鄰_>>>vm6.˳'77h4>3---^ Xp %%%M#&YtoIRO=D"GWWW{{;<P2 NAV188pVf#dVf|>',, i 5 }}}. 1 32D<d2FHx*++!Y3iyXh,jh42 MӇLvEDtt-[xVYYo=00 MH%x#@ Hßz)(?m]GGǬzN`0< ˂q7 pzfeeWTTLd "99h40$nr9`rkiҐA kjj222$1FO,!hp/@|'NTTTLiBFy# \J3@{0^L`0LZbGDDx<oLĈkXNs``@jU՞h-Koo/$ L;Ap\.͞3gχr\RfOC͙/큁5bn&V`09sfL&t /(fg|PXX_XXX^^.Ri=x<L&HfD"BAh4;\lYEEd} `C;8T t:.HyFNw D" j.VN Bp\.F%l6N gAL|P(dX111qqq`VhZNgSSFk.N(ggd2aXн%CX~'ׯ_>FV 4fʱQEyxׇ<`0R)9 n" E{zzFda0TKBhVYzSTup8y]~}Cf&OqWl6AY0ł|XC}Vrl*Z^800- lqH*JP8mOCPPmr8`^Յ|^\53{bZn$< !:!A`w( v{NaaaG1i!r9|a111AН/^ޚ.$$sJҋ/VUU/" 2c2]3gX,RaCBBL&D{ eu:VU(ʋ/>!ȶZur}||#|>p DPkHǠն999ޯ===BҥKMMML&ʕ+" jf d{Q*k֬yg: tZv˖-R B7n裏˻}}}m۶ruMz~EZ6a2nNd2P( J)HI7EA'XM0l.W .Zd8?zЄ۷7nx yR<LWUUO oUh=h, Æw V-CYv X,/:::`,АꫯFKJJR}@JZ[[*rdddܸq񺺺ǓCLr莼_ah4<LSx TuIBs@ 8<3`<88]ZZP(RҥK!ԃ,5)RT* '00p///asQ- EX;CUWWvttLMm8a=d I~~~>>>L&ɂ3 #4 H$2m"ih-10}޼yIII2ӏy 'T2,226fX)))}QGGq,S u B!LHHx6o սf2 ;vlw æB;#t\簰ׯ6@'%%EDD8^SS3#JhY/1_B /Ǚ3g&e?EA__BHLL"ҡ=;MgXXX__n0C\aNp80u1<O"`zuwwCD% 8`p8@> 0-flAGmm-Bhݺu_V5#* ֯_yǖ.%I]]]YYBĉo Ȇ$999ׯ_Áۀ7wQ|Z-FϜ9d2bbbHaG#d~l6?~<..!pVZe2x?#N=(((!!aǎ/^rtv:P O3gLy/`ͬ{|ttǯ^M6Wniiihhx':,O:wzaXcvJ\.5b1&''g_ * :J@'$$\v1/"loow8:nݺuQQQFbY=\NQ . @Bxu:ݶm>{~9s4 iVJ%8n7l۷dDO ^///#]/ϝ;7--ҥK@8˴Z?PvRT*afn0lW֮];DdɒR It0~Uqڵk/f윬CLN.%ð!!ݻ" ].@ BPT^zu&~*ٽ{7Bh߾}566l6(&`0(B(,, !r9TX3s5+B7ԔT*]|9W=u@kgNSѐ-ǽPbr,K`` tI>D"AqGGǤxK?@`QQƍSa6He2Y]]гsW]YL_||<bEDDh4tmOVtR#yp:>Bo~_{u& |GF9R5ec_ט@(-4#a2!pKh $HS&4t,f C`&`!(*cEDWXѕb+ɑ}z$]G֖VWWd9 !Z|@;xCtr7e Ed*ih@VB:;;S:( ubD⢢~_uz\.g0 [---d$tj lr'|g]|9s$NH$;w.!d޽Ss`~ Qv4wx<կ$l^j}3!nBXPh"BȔ[o7^z ". LTJ4r.**D"}}}>"B8Ij_/FH$(P.0#aJVUU{ [, VkQQoP(&+988(Jӵ'Q\`]m]]]/Byy5krssmF1L,._$''Ch^JH6e S曄u ,lW\t>~(G7ŔUtPF+pP(J9L~TCôeYPoCIVCᡡ!@c UIGA={=䓿!PHVսp\.+P N1 ŧg% ".nllD"v}Ϟ=j jkka['OΞ=W^t,˺\.BhHj,fY~xSHvL TB=Xtk:B.eD&tߣ- b6Yw:f'Akd8`0H[7+T dr j g͚  !"۽e˖ݻw/YdϞ=0mrС_|qv=//OѴ_zlovZBӧ6:P(;zƋ3بݻo>V9DqKy%KB?>Շb|] Y,ܮ.f_&Dٳ.\D3( Q0 ^UUU ;?vww ؅MpCCCۉ bVȞJMH a@ҥK ٗKKʍu֝>}Z$" ,[l>7ޘ3gNOOT[[k0|>_]J|(v:FvrիWvͦjGM-B㩩%, MN[9"@fZ`0Eau^^M!CKDy:x## >9}`M4GW,ÇSp$y2;(7n܀ c:HTz`0(60 "TPgϞh47n/bu/N:p>OG";VL&fD'ZJR\f===׮]#~qr@vŠڰR{ݱc`8<>ZRZZZSS ]=Q `Ftn0 s>U^BHkkknbjRp$ m6ԙ]wwwt+ngYv^z>d\e9aZvZ7P(Rv ~[.8pѣkl.hk2RwqGt,L&nwSSSCCn_hQ?qPgɒ%v:|w]~ lM۩ټw^T[[+HN>SOXѓW0]ƒIJOEؿ#`M@ puHT2 ,˦`$P'ADqHR.]oE&\y9Blk{Kۿ[JbNg9[dIOO֭[Rc***^{5p8|O>>H`0m@2"D"P MǨ7L===*JTz<'x%>|f۷oÆ ~'T($1ޞїClP(ǎ˗/k42@755zdU*UFlBl @SOFNjn9rDS*2L*-d2GI ~(@h8 Hc]'x NwE^x]v_~y Bh|GqPqƚT*U1-裏R P!WKpj*РFmm-U*Jh4nذa…FO:000q,SȚnХRTѨT*0LmmC=xQv㩭w^Q* ͖Ɂ@vӶD57r*ݔBF# R4JTe].W07ddBnܸMmqݸn&#G_Finn...|jݷo_YYL&khh}"8cYnܹ=si4Vdn4 #^^+LO @ftpp*͛G P\z5ՍseYgewܡV}>_]]8uԏ~xԓo: ')ӋbLFU^fp-(4E 4 K, Ӈ>% &wP\V*̬|>xA^eŋ JVX,YKEww-[ ř3gfϞmZnϲ,{EBȢE ^h0/[oe0y2rJLr#]A2$ԙsi|~Z׻\.T G{By3gάY8wy縯9 Yu;w,,,ܳg''bWiT̸g2rQy5`M.3{*0`7y hoo[p낰 ~]Bm+@wA~BC= H$߯T*z! Cst:e2٬YΞ=R~T-!˧ I'IIRi2~z&'x S`7&YӾ.RTlq@n709qD2L.Su[\A Qh@j|C555R$ TH1%Y^6 XH$lٲt!#J LqMTQ T^}lm ! 8˗/z5MYY٦Mϟ׷bŊ> z/' QFcPC7lTD L&T*UmmmNN{l6ڵ+v]*r,,' 7B͛}-gB C0LQVBH(ʠ.L͔pZPm\^_YYDB]8Tb5H;)P$ K5f6w܉6^ք ;ZKa ܬϜ9XVVƿtc$pT<YLIp8rɓ' x%K~ӟF?brDy5z%.;6h4=ztN]Ӎ'~L H3B*a4Fa0?X`^RȖ<(P*p4Q&رcd7.F#Jf3;pػw/q'\|9<mmmeee۷o߻w/֭tҨ_Z Ì=d2SqPғ)M!Ч֜ 6tuunlۡS={F-IʉkjZNgRW0`1{kXl6ۺu O].W$(<og^dQYhZ8e瞉ؒErzh.qldʎaXd7& Zm____9;tBظqd<Ν;oa۷?SgΜwr,=ܹ͡sHLH/Sj!L8qr}<{wГd0)5S*jvN(}QBUR4OFH$r q܉jBWXD`R8),,68pж8dlf%rENojX,|,% $S3%Nͦje2ٶm:::9/..~ǬV;VX0̘h"1 \.ψ6vBU鬩9u?>Ml`^: d2B1gBHaaD3kj4`¢de: i7Xz5nHRF[>$z{{FF6T c٩Ŕ%q7$0t:hvʪL&SMMMeeʕ+|ATI⡥D%IjbOoF3eښH$awT~ %j`$ CX< nˡƼy&GKD[o>BvfL> 4д+35+WT(fY+UkF^]; ax)I?)o~5 H5c|ɟB֯q;:˲;fee뿺'O^bڵkq _MA2l TE'dz555`0 ,vV^=nQ(*..b^\\ +òz$!ÅHM*Nh֭cf v͔p8La ҢW˕ !1q <8tC hK ~QB$6Za˗/g}{LRǽk,d[N@f1 3IQ___&NdJ =3UL&AU ^vGu N4eee.0UԔ,E0BGG竪edp8 NWu0r|֬Y,H~ OV<O֯#T/9~8q`nI&ys/SYfD"s&4=c{FKoܸΝ;3!Ag~---$i+W@DIDAT[֛-`0BR4 T'JiD[`Jjkknw~~>Uΰp^*:VKɱAnOnggePGFaV__P(%Kttt>o˖-NG^%%%Dž| q';=mFz4"׮]X,:ZAxT)@2ݲe"(**wʗ_~Ib,XP(<`fl:X9D0A(裏!l -!PXg,Lq HxiP Wk'@Cˠ{njv8ljjڶm۫hlr]!&_(LFkfTm7<ӓ,/**I@ z'W(X,_ ;?SA4=*++?c%"B"@c%IE"zN X,]G5['osjpxhh(Sud@nn.0,C\EEEɧ 3y' )B6Fy7@ccc$yci*l6C P2//ʓFc z{{ _=x<*('oJIX]zl.;6 ɣQ2h . !ҥK EKKKb!|bVS IJ9992%YЅ~Q,0DZ԰Zkkkv69\l2e6®|ujzN|LՄ@ZKwbJt:'$q^axg@mZlWA<=WXdDt: b#H=D/0K=NO'E===AœNgWWWaaa 2 Eg}7hgS,F1&)X&֮]KywO-<Ʋ,˲a w(2\q. &g2yb&E+-[L=zeUV-np|rMMNx<Vqdb5jHxVTT4ѐ akX@,X}[VV6I㚊h|_S hLD&: tvvv^ -Z$JM&S2PP޸ CK5ᔉ)I @N%777'_?яb3:m6XbBdbY2tL&;S /fvy\ zoo^/%5Ja @C$URR`d~tЇax1&#MfNfp= Rq8CRŞ n@ eZHĶnTT6M^0ιe{zж@Dm4 Ǐ'y4D2CCCGBL&ܟ MfqmXT*U\$``Y6;6\JU__XZ@.-񽹹Y^rH$m3W'CAF=RVVf0۳d^^Bv8@ ';3QBF,65e(,eD贃R<[[[0FE $$I8$1t:jD""d+MMM,VUU A RO?Z6'Tn֭#wɃ]PjeJsF"a/~qCdX%cOqs * #9B8v>4fB$qY-ٜp)NJ&Ujp˧_u( ,r0d9XԽ|ᇄM6k֬J9IJ)(ҥKgCwsLA^mj4lDkJ&Zh6J&&*ŢE4M$dN.*=&Df9@w2\.衇@2a!mBH$TBt 4!Pqľ^pl2L,KRH/) ~uuvvt^rey%Vml8nŋȺd2رE-~P/R//((H|R\z}HVՄ^R|>_wq:R4hYf`"U]: ^pab߾h"aI]мUs8 dJ12`0$0^ZZ |Q2pk݉@֖d>W0H˲,P+F:=_ёXhj. B9u\ 憱8 .*8.HR\^RR Uwt:RLf꺺n3 -͟?`0$!7YKXEs(qM[$Ws2 cْ<]u'Nx^(Z8@ %Q>i z%?~sssKKFS$nPU~iii^^"I xR"H8Tj0T*գ>؛Etz@4?MFeeT*u8gώ&[oP#ŹФ"]YuV^bũS._L4za00wքDžaH$l=~(l6狷mvA]2sLhȦJe۷{szj^zu֬Yw\.]2b{m#;;;]&˗,Yi&?~?''\d2H$trssJX,f*--ql.((x饗-[&j>x`MMM\7w450NڪRCT*VkX\9)X\\zY+_|6eY(⧟J%|QdqAAA~~R@pℐp8̯{bBHNN`nnnwXjU\T* J;V1xc``@.'6.{  gϞM 9Náh|+W[ ~?77wy (c "˲.]jjj2La+,,,((BdZaFSTTIJ톡~OO5x eT*-(( jv 胔dBaYS&;V͛Ԕd`֭[n%744ӭ'/q"(zVo1tLҥKuH$UUUqE@=z4 c$JIxUm}̙3 MKGK㸫Wf!ܜKe.H?Z*n;vnw_tRQQQ8v\Nb8AT*?TCX,PqOl__$p8`0Օlƍ UdX*)-ǡyTEfb$I/B-LC 4=a%u(֯_o픒կ._z#Ȃ xx@ח u`WLJUXXXWW' d,!4Boʕge2YFQ*6z&TWWvTA!$< B(%oBNtB!BhB!4B!Bq8X,ә)v;T駟_9SX,V/Jb0X,tt CWWlNX1Zj_9t:ޞCBSC,D"FNx=8bXltB!BEy&hB!4B!Bq!B8`B!P0F!B(@#B! B!!BhB!4B!Bq!B8`B!P0O~7o}_ 7oD|Zh{9cƌ7o۷GywfB!D>,{Wz_y?'q+WDZRPP}è7N!B0m۶>}ԩS]v}W/v?O\ܹs{UƆt>B!:X!>yΝ999ͫXMq%//KJJ>I_E!J)@ 駟noo_~ >#Gzߞ3 (ϟWTٳ_?Gb1vrrrȷ(B!; ]?G ! Bwݛ7on޼9MQhwGy_oMB!D7o@ ?#lk׮ꫯZGۣ555'UV8pG !|+f| :B!H$y#ddm"… 2a(QBH__߅ g QpqB!=0FR'OF{=}4ftB!n pF|o ~QB))bŊ-Klх ~G5LC}OL788xرG}4B`=~_GСC6m?9s&SNݻGI&1cvC!B!BYa 4B!Bq!B8`B!Ph\D%ۦHB!L%O[#B!jXB!PfBrq3o s5ooU3Qg S"B!}f?κ-yif1?E!B(o?ķ+/B!sg o'}}mRhQB!;7=B"Ie>|w'{!BDĨra¨s$)16}{ḏ^~]/b֭w B!b3no&ng}ܜM3l[*/b޼ywy ~]v}ᇿo>lEB!2 BЎ q'9(k8G^x_6nh+**:zB!،27Frd}3z{GklB$I^^^nnX,믓>fB!F篆B+@t[aÓ|O͛oWӟTSSSWW'+#B!3d+W9PB!h0 |df9$_] }[R------ɿB!B&s-B!P@,*B!B< B!ÌtT!B!B!2@#B!7 B!!BhB! ۛBIENDB`offpunk-v3.0/screenshots/xkcdpunk2.png000066400000000000000000003576141514232770500202170ustar00rootroot00000000000000PNG  IHDR2: IDATxy|eOfrLs4izBAK+XDCET*+cDd˪(rH `RJKhH[ҦII&31m(= G-<<)2kqs:@ZY Z7_a:( =K+b=Qua:Z  }pZ1SǪJ74 ﺴ[mG΍!J *aaOkp%W*5Oj@ic$ "}Wjd] W9)$Hԇ;[)<ujA\q ^|V7^l흎mL*|vA*Rc=t4L8A^wkTylհJvx|B8y?usH|U3W~hЇROvF}B`[b'I2SɌFLp57 (*yS6 'wYZH2U;_aՏ0kI˸Sz0 P\ /|~"5| $\p= 5*A/N=)>t*Bqa^omcC|3?@EIv |i7M33tMe9moD2Bm4su|bh,7EsyӃU%? }%gҿEB~#S|/5%EKsmxr{i MG9$@~]ukDJUnƹ5XҸm3<@,MD+Hh M9z%%5;ob¿u׶v >)ygR/o/8_HNk _}Q{"8~i|+ $X+aB@}&hW\ |4b]o}RG^;U=ǝxE\;i.3 6pwK}>O{wYzNy YlJ#oy|Ӳz5zo0~m谛iͻnoJwǽ_Ysq zrvu[^?wGg靚<>3?se;8첾e mׇe7(Rk ]}bITPZd tW>~b;DxՆ:nj@_yOJ$004@rp[,Sr]KRQ>N]Z|{Ho9'xO5*"/"vOx!gc?[wyx7>&\"ux"?@ 03H!Z,:Tlai|q|1YP* $N->gސ̶3>w""n r+~`>DN9x;dACdpJp @/^9'5EIwPދ,V!:"5ז͋HqMI۸|<#{mQQ_5YQ%WN{QS_PdY[99Q *w_z)7T"P) D%кAοP `(-KP֨J[moTR Jkx2 ߡO2%>|@/d %^}ːod[=a7Q}RvMaWxXV\ռJo}HKaSrekCr7Z"l!$U^Wgy)Hދ8T`]M<>I W m 34t~ ,an Np 񣨼ëjh]|g/:^#󈧄J9K3OaKmֶhX+olQkkl>QP8mFr~PRHW~/|əFPIc5W|a?u>\ԱЌ9I2Q'W鸈'G]2U-nzgqT=\tH1qзkPsR wDp%&w'xR@p&^;%!>wY>26pžҚTӟ~zKTf:+_}[J!TS';SG\H 5E0akEܺs {ܟh{I؛B3YOe+*^5`KI$. '5 KxSXML&mhyW @i}Cz6NMR"%Ǿ~2E;Wo Xhay(,@*Vc2uOKN/l+߾w_?/2y&Jߴrk'i? xXq=UދyC&#S: .&+t|?8/MH0NIe~^"5~<>->`Zw+kW*t8'*}\D6?,8JtHZ_(|g|`RkyrUwI#꿼]!P\͵y-/+᭵>oDNmmQു[\&ITC (6ZP oN"}Pj,~c8 hg|E7 s'xm>q !2A\S+#D|/S{Gg U1v_X[uW?:4Y'/Ti^d̴LB HWɻ:s MvI5>Bަct|WwS$q>(iZ/|\ fm5}b𨴳BBU<x(-"8^NOEu=osj<r[PfpǽʙH wJGg2prRy s69S,}{;9DuEf{N:c + Aĥ#A\Z~"oΣjw_* 0naf  Ȅݩ]S9G-(           6]v_3LJvj(iιժBP?3F6޾]W{xTxe?|m?q{ ,a.\eg8v̊#4`QDT:.,ޠFlW{'u)+MK*^b72a/ҘpW?~2KFwk~sʂt  v20NR>{srq#`X_TGPFFJNkf0ZRe䦮C/VLm,v^=M"wm-[x7o ml|mحʨ3?}۴vvn ZAAF D69Mμaʱg"{ə%2ZO. Ve2CjcU{;5?n`%TG D2E'׾fLg3ߋHA-Sz&gn3^K&gNwpS^\=EЇM<4( G+ f`{ə/oAAtt$|TsJxOpx?υ3[)>9nkޯ.Q~w{} ?Xl(-a+W Cp蕊3uVqRN2NyuJK<`FkKLŅP7?0AA /AJO8i|ATsF/L >|lrfg^*J3I*JZsyu9Lv i!7||n`ZnWgLd9&C汋o<CWslǡdrAA\îY.H)M8wѣoi'iH2KWU7 l3Z[6ƛ~fu=ռG,AβreLAnHK~ׯk:Nq:LADCȗn:Lyvq/ѧvm9GܡR+I#QUGTyu!e&v/|\/p˭sw&P@$q:w5 ỰKFx74$AA,e Pt--$|[yoٞƕ{ewD^Ld[[o;HAܺWwv{9nRR%+*ҲmKV~Ts2jE¤X%VQsAg[kWMI%@_;A_#'QAA0RQ3pc񶈃@jqmJB3)< "pxoUm73*|W|S Q&՗&o]0Z  :""_ף 爵T aTÜ,tj ~L/$A*ؚV& hb>Q}ұqǒ5d^DroFJheĭ(4-8 `ZXB'iDml\ `gC˗P8AA:2%2zLh޲=_M%*Sy}>;g\+ۥ>s#nhs,i"q݋>?ñBDR{&;Xi%@ U+pNl+k%r1()sn957b| 3U-P6޳^~Gwef_ ].+#=>~/[1``Z6{F[xzor`Mgn&1AAAəCTAAAȄət!  ?r͝ΑMMAAAAAAAAAAAAuLreB3hczz[sٯTo};&)='YTg^SWpZ%6ꁣ,40˙΋ I>A`c5_g;y% !%^N_$2Tgesp.JһqޫCj  %m@;lbH~l|j۾Gztnt&|5 :gm$LHd:[$\[_xk@4wmP^?)tb\΍:@--{Yl;>д~ң DL{ m'X:k$_sNI7fEvwI6r^AA ) >XL$~o Ti\źE"!Oj3y{U.P O{(۸E)J@`ȻtuT $ UaP.|ǯtcI=vd9AA\!$D |n epnL6ѪwWjZ{Vțȶo>}EVp>["WHãֳMm({IAjQ]  >}_]T  4"_uc_G=lfđKDwg/'7 IDAT-6Ri״:ts 镦7wG6y{B7s|muǯݱ忯AAWET L^;ӊ 3lYLvnEE\;.g#R(>Kǝ=-?@H4Z;3ۺo[\/0I7F<8_M%SZ$awϔ@Q!;[2Ѝ{%0jPWsAK%2XAA|$|RtTUoN^ 8ә?Ugъd흓m۲8~QW+-򑬈i@I$2Yn@s|:QtOK5I4hbrjwmzgHJݘ1SŅbըBMPל$2y-;6`u'WknJL)B@Qު5_} 6bxb*L!f# Hz0=6&X_ -5g} zӛ'ϖ@y8?]z\ܾ{+b{X?{seO(au:l6}Wg_vFu>4_[ֱ󪿹J2AAI{wcKtp81w{w           :YS&Vp$w .yw  Ƞoo_nn{w `xu \1ڻ Jp10FE)/bLJ,U ۟Zv8E]%ќ/j/R`ݔ䔏y7_Z{w#߻[_EG\ďؼjiFr:E-+ı+ڻ/D $D&hÀϨTݑ&um-zۻ#-0h ;B\Y{ 7kΪ5/w/:o۶s˶wGkMxAUĀ!u$D&h1*IȨN!(rz-drp4M:)b MѠ) 4 縋 e21q{/#. Z`:RX_\}6Rm;!~'ˑ噘дTwܼ&KIy~}a.Y3z=:|}lC@QV1G͏+c [f+rlxK\ۯt5A虔r s#Ǘ˨//Z=ڻ#%| Z 2hTpѢڤF\L:gbb(Xu[Νon?xh 0Jee 4 (d9ώ,:rCXWi)@r,@׻LHOhLL6~/7*#5F ͧ-hV+*Yhт#n6kv]%'EA(7eno%R媩a*ѧAh~2Ek4{pUK/<:ѱ &oʊ+?\8D (U_|CꪩcpqhV*!CPL\>p8zhZbc[v;dtcʚdbhT2۷l@=c$DV ݀寎4'|ݑt'O[\#Ɛ5gFCؓGlA񽓖m۶mS)CI;__uDR8 1(w=b TQ2AN#ץCzh &k 6=SܧgRʱfo̡i.IL7A,%s:9#ZA7z~/7s.Ⴃ%%P@;Fq]S;_'% [2zb+ٗ/7wF)/ʚty!Xmݐkջ;X8NϾ3zL8o7k4COO3h8״J%sEf?(tgz\J-U P5?MDGBBd knjI1v҄[rY{wbsӱ EA9y)9Xƾ2-}ŋ/5\8-e6oޠ8eG RS& ɰfoH0F-P|@ٿdjcd2,-8Z@]>1~+Ӳhӆ@fu_nZ^2ohV).\AVrSg/ 2yoR"BIIp HOg=Ņ]hW7&Lbcm]&x {:̚?SP4-|us'e246S-= X Vw\(pbsE#Oo~o Ri ] <FBBd S))N`eŊ}qݑqqqɵv*"CE'TUV.w r;s*~PߤDCZ!Q&$-=w?|5IŁmk7:ledalW99=R'LSAxcG^Yܔ䔃.{얄T9祷Τ&ܵp3pT0&&d8w?0__ԇ<@ (U!Th }L3LЇ5XP9;vΘ8(醂d*j(5j\ٹm']}E drbbVl!: "q!k՚JJK6};oL ))%/7w% ,[UUv3DM&7jH?koq8Ff.(5:laT MG=&S26cX*g>Y4J)ɳz%zcaxs_i JN`[ 8)-\~#<ɷ<`rBe b퇊JXOjh\tFjϲ4(//*a={U%fd͙S yhJ!N(qY:QQFbۼhbbjy-}Y+.`CL䭍AtD$D&2eܸ>{q喁C;(5Eɺ[GE//ZؼG]N2h;1(Bm\piˢ+S$T.k?ޕ`S{y @e2S4%%W}Xl虔??~-L\nؤxlvtQXNp#" )"F4o Z%##"z~WB\`:ic-YJXdcwA-̚1gJĻ ^l0FEK]11iL\tAJYYK,UVϚ ixvNNÜsdŅ<5Խ .V/_7nl}!lp cwB Q-mRSVZ8Հ&|j A\)ƨ_1H<:9}F:P*z`e46%TA0hʽK]?1дX>1 =}bVrT6XW_E3X!|50ǘ۵?l5L& JV=r¥KQQ怵 0k9GGkKZ.Z|@\t4hiS4h4̯ٚ!E cXG7T-&cwTYmCx}xz Tbu'Q<\Vkд̑NQUS-2Fqf"4%7? %Pm,&U9# NJo^6"wT ƨ5} @4s+V,?/QKkSK0 =ȡ}E /~r\1DZ*;A駟&]N qhRbIx[{w}|}{xxx{"EGDKCWeZX\Q+-60 F.BgMwtaLͰVXb4%f%rإƑ$32V0#I))G )+ r_{wU4£|C4yhU+8+"`: )SX\>?Ӈ3e JQY]XbjZrY cT0J(p9-EmO橣GLG֑.)Y92Ʃʧ;Uc˂ `D j``J ]2toG MT_޴mˎN_P`K4Rp8^֜r)S;| /,EޟWRoPҀ^[,CƌXrtµ] k`Y5UzY|k/5cΒEBgD ]`EFj|48:4 ?_"Zoxrk ;*;?'pEݿd~ {Qh K_^GG4K:P ׮JM8Y<3-$}KĒ}!]b~uCZ:LjRU6FThO.|\hi"&ٞI)C32tj倁`/| ڙY< : G  HO›i1h)Rf<Ayh9'p)aMK0Fa\q:1~vK^?6^);+*i!hWICZOE(/z_sιξi3eTWk^x\: 8fiߟI.g=vhz& `ZWדOhu92ltkI =z!1>N;k7=7ؓZރ?1Zdp~N5@|` hXKfH;&3 VXQQQ ɽf [ :pThiԼ5SojVϚ#ArqA6޿/7W|Zm@zY3y ѵ˸aòsr_ys'ݺM̆N7nɃcMf]S$+k_^4&Mz б \.) jld;f9Ke*Κ A$4KĴ5*¸`d?T[{'@Y<5sFy/PzhZ\t>THV|4uYS&S!f􎃵ֈQ[?rEF-k7̚2Y?LV)=dĕ| chC { D9!s`+95oY{eG)Xڗjŋܸظ .EtmC y-)+ߍ[l '1Xj쇊NXm.֩cPgZEe.An4!3JcU2 jHM'p_U5gF]&SScb󮒓 γ2;'_'PF,nr-ޠgS,ףc#1SvQB=qiC۾3O^c6 <^04xFm.I=Ysf4 ,̡i<, <*\55ݞ92ooд`7y9Z e2e94 VwxLE/Ohi  <# s 0`8|Ҍ'BԀt^}_56jKBd#?+i+M IDAT7UC[#dI)8VT*o4J;4rXe_[Qz֜bݗ1*jhFmc&9wkۭvڈrk7Dd͙( +o:qT,KStFp8hC 27k:V$c (q%2tz .a>d_޴9|B>Zyە?E)7khh\%'e 94-sh4opc8劸 ʅŦJ`[Ur, 4->jm50kƼys9@{v_}.Fʚ39ѱryf=<[+\N]M`J헶}cE6:ok>UVȗ!̚21 mnr: &6}.W|vNݽcW[ܸLJ;oN3>pq b'" ZVc`2WTU Xd]@i+]J!z:Ų()h-xh1m-d2Zxl`SUpմXLLX%s[tz1恻4-b"2J8^aFZ , [,etZ-ąq~H|{h7Kyw[\nv Yj7C"?LXG40vfGD~XY>.TU8`mhnxak ܬ/C?nyLr6NvO:Z"(YKϔg*=7k#Z8 :\@is.)(ȯ8VMX(5[[ZZb##/wX*LxX Lm($lۼmkS==qihhX1 \B6YGCcf@ȒH-aJ)˫[b:@:غbH(ʤ t"&T /0_)-XYfk@bwd-|FC}Ù+]"մJE&*&c#HJDZqllD܆0D&z;{۴f4~]c'5^`e9kj|Lxr*c~K'<ŖV@Qtg gtpbkgŖ#FvD[ Ap@Ah S1@ ̏MvX%qQqq# s 9z_O$]'==37II),FEIL,{AVO?;4%x1 s]T㚥aERw3ì0|&7`{HX{3s*z#L}/΢Nְ0d8N3>U+KHOOt/#&~NX)dXj5@4@>P{E`Pz/#%}?JC(/,eXDX**|6zCG)MVoY_-'K$h}ULRŶ/pxVeRxE0c%NQս}߾TkV/*ڰToSxiw!i=,y^G)~Ȫ7'<$qv_Ǿyayk^v A,Xa|4Ĥ`JMn{n?!9hDcρovHX@H+P_Ŧ&f#ڟVԪޟv; sͲ$$c/gAfD~(AAGv]^kcBQzah`*X@,EEU"󟡹ȡ Css҃vw4>>JR_~I `tP ujں k[=EH0tv:CXlhnMHJd)nc=RF-} N PWu4}{ҔVH{yRzztnGs1$84MffO? ͭ_wM/`˾}ʲ\EsB|Lm]םa!aog/?6GNOpaoH{vSX@ҴY7(P\N[2Rg?yohP@-*Y#F>\&h2]jjMqI4m}i`Xvzh2괺`OfDI892EL^Bn0q[Iyqy@P]u, M_6LG֑ex7ECӗKp f_L64K3Q^ aNO*Ti:-9;ug*WD ͭ_m@^Nf\P* %Qj||P~K-^?~wоT>6]TNw74})ÿ,{cǧ}iRJǿG0FQ$vV4dX~0tlB'&K?N!FE8$_M:fj% v48(L$p܈C>u^M\2hj+x(Jez&f'ze =߲߲uq^4'L&1 &>ɉyk ڎ/}V 0X+? 2K85( KFF@?k/5w-5>>U}]>a6<5\Ď[#"H ]ܢ(cvNa #Uym;~qW>yH_>vh,ABT*dfvx$@__`Nk3m-(ͲXOӴ꺺1 1θ7$'Yݻqw\ɉ7ǛqQ3җO;.Q WV`_ny?&H_$%ϯق-ok2U|2b#diaKP,hG|ޟܦ]m#Zm^^<*#fq3u8FHعV 6|U VqV0 ik[o }v8iW W-ܴa 22j6;wm` Ieu=@ܟ~6ua=[mQ՟ %Hh4ъFd-*CL-Sel[w(q=%q->dY~n;S5:ɺ벲US͘F))Py`֦V/]NZ2f,ˮð7`_ީ9O7wƬ'WFκP0An; rE;W8CYbY j ĸD@tA͛^ۻ`sM"O1,; :khA``o򐠠;NX0-&nvюW6S&OBAx2 63jHBrMPJE_jUQ+4440WZ{X;K(SEćk_k6ngԔ_sj~)y6v[}|4YDGf,omG,v@fc0.$\x;n z_jj08vBMqDߥD#M_(}}V[櫫j[pV =yf7 gMVAI⃋xr3 qKd7?&Mg&~`hn90шp/x}E) \(6IО^Je[. c[蹾t?Y.C-J&vwl؞Y^jr6g`x;b( FqOon @f3.K Ώ0(>j:hAa&jx9+ӷ"hÃHKld8$I dGx=麉5t"M.Ť?[{M> c4,j0s$]> ESoh'}W偎9H:ZN7֬hK猼#PIW|TM/_k_Zf *,(eXkb?nj_ =LF6!*HB~ܧ<%R+9thdq7 4(pD0mGO#zOm}U7@|º  ^vp5Q%D֨ pXT*Q{hE ~ʧd|$/Y4)'|27kSApDkxNoD~R v[vOs𼹻Qq| k2[x,EșRzJȃ!64V$Ɨn_j/#E9tǃ ,AtCI474%g)@y1uW[ 0ʃ bj :Nުw9^vrz t,r*^dl8閑%(NR4]~`2OΟӝ)?"l1a1ą.>Rx8k >*IQx?vDQNQ_Aw$n'MatOGܘ(n27w+& %!YLZ\P*ZKm^JN]1!623nDTQtu|`ZZv:*u(/Gm噟pS$Fz(ΟZ@À'b 6.IyHa A25m;?Bʛ:_s5cDF}+.Tt|1^*bk6.3ŶZtDksفԌ9_oi8T3Ao+\f`Ojtq7L_& IIDsILy-N,0=QpA,X$ $Ȉ}`/9^ aN ą2p^3'j:6[ AA̦1)n&)Hc["?vV|@TZv@~a!=vs5rL7f:G.{@?gS"4]Z>ᑔWGEx`oQ!,$޺"9r]{G_rLaOQL@ o=U3p"5= K-\mΜ\ U5,FFVJ[1vA{))<{|/X+RRZ_O)XhSkSafzI<QTe\ jԨ=S[8ֱubrabc6,O(XM( 7°KM﷬S q o#YyÊXr\a Of3j pp9sB$uݝj z}THz 0L>FD}\YQ&Kj+X0L଩( v*~~0G<<@zz.c%Yr?5m=ݭ;<{|oT^liż5IJJFOܺWAjA$%L.p *pH1zdYd , =(v{Ĭʼn̖nF7w" "HN(÷ .=I%3+KLEY/ )vQn.c s6G0 Ttm~1cãL" I:`ݞ0  4o<8rogNx1B_/H86.*>ew[jΟ4P*`ۑ&”xLzzwO\=xB#b4Ǐ8s%T:4M}UkB $&$n[cv(Q @L4v=R qy`؄9I$EȎaI$Er6Ȳ`mÍ["?lڙ>4vzCǡiI_uSKXHsT M+SNfa!aޛP8 .vFϝ3wμQ3o_U(Hq$'|EKbJ gQ=x5BU,˟юtw p__@!z6oC)iݷ5>H(@&,IUpgM扞)"N A X,o2xbbcTprw4qqspKYpHs&Sm]>G%\&-EsvUqo=aXNsM6l4v~~vk86znΑȪ"_|VPP*恁- uc(+U鱟uu{]65y HJ5RvuBQ7|Q# hLK$ME<h3q׍3u78T5 Dq~ JREZM2Z\Ax4vϙ4eᏑD>[8C4>32c`si)5>>) (/Mu]]W-c _C-oo'{R,xҔ-[ Ic#>*^[_݆5mX>+(N3{ykiϧDmW0 &3زI&h5 [aL5s9n*>چυ>Ryn_OKNGLh :B+b##_H_^tt~7voa*omKZ2oA["?x0,7wkjoGSU3shah}ѧA֚EGwNr2ˮ8Lu/8}˹z: <($Y Ŷ*{;DOgL?ypmG~Z34/𱑑AeS{T:tTq.YNKNBl+>VRˁFP90 IsNGҴĻԊyXE:SfDBnsqKiJV Wq'qͭ@Vp'f.KaBëTq/8@܂yvı#o&̉r#GOW{1{)LYKw>y{r]AA!Kqf]'q,OiC44nc{Ȳ,z:: ˲YEpɚZcmVe>wݶ<0rʤ)jhrFb" ͭHV?a{{r2r8^kCtu yQ z~7nE/C(+ΟE(9A8etvIk?''9IMOzu[CY9Msp @t ZJe5#$/Xj/Ɋqy//)nbTp*.;Ng8Hw(8U:;y">;}B2}{_^D7nn["OS;]K WHC3C8m) -fW=Xjs,˥Jr2XBf+v}Hs7nuc78wƍ)'!>&!Z]S}%'8RL8q~hD !{,(znUޢ[vWT LK,؂ ଆN@/@gA&HK\(JCs=KHk4>8R ૫@t8?w8 S( !kr4nxP9mD$It~%n<8Q@ۯaN%'-+㇇׿ Z2aY]j1QSo$VM}_ ӒKϝc/HthUj7nL!}TD~a!U:@48ư`RdǺ4n&@*"F4IyyW{W,6ގ7[q٩an軁63RlhΪ0DRҐ3㛾ю Ϸ5"Ǜ6~txisd7 P^wd(jB_m4n_z%? ٔy == nܸrhGE$$$-MGE@=vyay`׿yhA}qOOIASbK ? !{6};kpf{c##HQp!/߼R Đ/ g!/W?/4fGyWF~֫W`יAng@Q̓-0%ߴ]jCKSEd_??L%*?FFNSqBBM4>> 9 s-zvCs+ O7n-tg9+>qY׉,W$==,Z\켽͛r]_ҠPY<MV:ʱʼn9kmD^o P?Y[]nE>;ƍJ\?I2җcVP]?Ϗ=##˒ZYi+v{U{S߬FtVn`2oe%Y ۆQ`H`R C 첀ūʪҒO~q(mDA q`l IDATKсwoqX8X7N;;24{t["?0!{w͵ٙPrt %w  ͭw,YAaJJ?1!>ƗE&l<LO nyff=v PS h޹}s(TW}?;F%nܸqm% ֦[{sV %\ KR&nw5b8&K|IKN~nhߑQzl6blda7ͽO1 *gV9dwo FF}a`  0ubW&  2b$)X,Fvp4FVVSLg>` # ̓-Ctv;юaRqHEs 9+mEgɢIKSTZ,qW[טy`%-Mч]+=(:C5T(=U >?T*1#62of^,@[\'>`YCܼ?Im]cjz;۷OtS\v38dA߅!kmT5`; 62'M&+PE7#<*Ο1cF`601\g3e}?P`Y0cBuZqc Â& "p1a6w+O~GI˒ #<(062O#D &OIeYTi@v`DG9̬{7T*v;ac"4>>Aq:R (,Bzz CCNG5<&j?N9oW^B1N4}[׿2yC g[9sJ &뛛s0L1є,Y?YAa ?;== (8cgluc *<3;*&aNWVȲ<=v5f֚)С9rt}?fmtqwu\xoFqϸn3Kus#;Faɢg`Z[+kwo/,[OɘeنD uI,wjwΜ$EYdP6aHa qV 6=gD8.q$E!(wqʯv?V$E`Ͽ3m7DL"#:fe('a?f5>>/Έݹ-5{ygӭ]IKN~f 5kBSʴW$(qmsj(vӗa$%1>I丨(Q [@onlͽJ))x##53{rBKG^ cvY$5 'Hq0{lQ7 r2'Q!|gZZ80)=u>9ӛ77Lhxi(ñsl\OxiM%/NOFRbyus>!,/i0D>[y`e`+vW9_6ws*˺ h ǴȆVV<X?w ׿8viR]iSTV1FքFN$$/@ 2:|B徜zq $p1@S8zqWMЯmDp)?|y9w;>N6VK- ɵ8@>\45ZǕQ蚣 (H2yS "Hh8v*c@.:nXW^vtbņ<@륤/jpDaΧr657|og>@Syи%y`={/hV=Ui7?ahdYcEA+յ+şٱ7or8pQI+/U?3KϵU(Α4+#&֯:} F#_=VT9w~hjx<0̙̅[̋^ZV?ur,{>*Xw0(?Z)t(>15=dh? I#@Z `  ۆZrCExAVp.'(QPxRE\dFM(RAaɼF=<U*(ଽ߲uT{~)aGJH.x5"&-87ØF%/B-6%4_ k%+!4.\lin0n5; ͭ۩%ϕkdO"KCDR*`fHX{l{ŤpqPy(ܐT'%aF*@Z^ZoGCC* QJ|;6cA/G XCQF Mrct[B҄m|^;d9:Tc Ճ~<;$sүԪU{rȋ_;vveof;ϧgo6 t`S<=Q$|8ZnުoX["/T+Wx~Uw{vu+r߶KlGѣE9"[-!A;4(Zl&Ka񅒆J0F`y/>\̼< . Ҵa<yIN<犫@@ڄ}|j$:rQ3b=)!.Qp"tFVNIJ>wl>@pW; [6N*{饗 /J&X#/iDz+qw7/ =^υ^^y;ڳ-4vvsX[fi4aXhvrgeCp5CCRv}:vۍv6({7Ax;:{Rz-Q^huxlPvqlx<;aw|Ч>_zgˉDT̛?UrOX3; @9$CiZru<{G7wo=hA+Ir&!O, @hT6鸛6YxqfodV;rjv)'Dd>YZ AӚc_gOoX$580h\|%bo[+fkQ<{=1:+$ {sU[!EQG}ί/JVn:XY[V+1g<\Q %̜cn)ʆ3 ['CA7"&? Nӝ4R3~6Z\d?Gُ6l;=rqt==8JkV-M8qtn c_\fnNXyNCP,СTUiX],ٌVeayCl\>Z]ĹC"@|7ӗP񍹇+|v|vdi980:NVb&;~ 5} ˸8(NRf+G;5?q#M{3̙1r\t?H( 8Ӛ;A|k`@MQs͊JT'&x&ޠBg'Ġ7f;]?>ZUgʂBGnN~&irJr"NLEDg7wf,<yd jZ"$I6ݺ>T,[at\nQ,"t]'j=wp&̜hZZ|e qlxދSM-ҲQa6{T*┤gBFe@hTĥf.X\hs!In"--%|8o7.=**o7Do:/HLK:k<{C y³B4MmCÚwֵ~- [S|D  q6b 3h#)ɉ"F%\dۥju?X5z헯㻎쾾>@SoUxX,4tehQ͉G{i][Zz mg 1[jai]8$&>3cG.BVd3c^o>tG[j5X˻t1Z&΄O*^jnz ϕk-juv܂[%;60?ҬlNC}:t]Jx~Y-);3?NlZƤ퐞^ ޮ6+"N5 "CӷDF(Z'OL|D'uQX? ::O{ZTMxMH,=kB.ۦ룗)IkBO64x{zu07-;RK-,ѡu,XB  EmSP K'*ZyC|rŗJC""g'omab:`nmRihвӥkaiB+vgdLcpvs =zh?~eְGnν!NdH8j\mh i+qlknÝb浲wzӋY;r`Vff $lh32aN0 kGžw#/ M#촴 %|/>VuOԀI3 7h8T/9ڳy<{--kEadT;Ќ3==W۳?r݌u,O9g`^\8` Eg /.y2E6k,-VզB52fFEx˖ ^kYPZRs-4*;\H:oZ跷1;vB"ľ /ҊҲ ?G.wZ  3iFL 2w`[:]PƉq}G{: b"VAJGƳ0hCZ !;;L0`i%kfS7,"t*3TD-wLJ&UP*@@2H(H|+"&>rwWF)<.y/{u]ʚ&J6{_ P{ t$L4TTP57Gț)=;пw-E \(u-`9{2Ogk396ĺEfzħ@E卤4@UAR|^ƾ;jgfAc`+ӂ=nƒEn&@IUHۼ~`[Zh\NQ]OG䐻{z :ΆedHxdHÃ_C"@qkGE{(sžrh5},ԱZ0fyYl{o XetUstCЕ[ӓ?3sZN.{;:ٌ X ʊdyN~|YB'WC:d6;v-/,=z P+tff^}p1 8d*aa6)*ټ$^{. UJ؛{W7"aThD}5TФgeF*8ql:J̷iJ;T'1Nue_eegeu.rIL9R9FyEUFqruWQyCвDT[mi[yދ?#*mjk$9d#'ŵxgo6S"ʮ07 ߋ&I`~E?-qN q@$m;u*:'K/D#[0| 'xYP񅒨Ј^^J|u[$F7J43SV|c*6Lm\QV$'ܢQ1a% *.rqqh4b|nD.mϗZ SǾ]w)nȐ𑑑gϙCdHxjvIl%t%yV$IJ֧krw~gB=WgL02iz{{˝xkA}s###Yc5 ^CCGFFx/tz0A?|!Oo%#nƆ^Oȱ{LxjbxW<'GFFj4rF)?{7͜c`Z#a)n%w c8wX9Y'@3,G6%0;* =}:Ȑ %-"!il/nYe]Hj`ͺ0 YfMc6U)Zh 1^2 bee_U=;"CMN8~J< v1Oc  NЌ*0;h4< *a8ejT1RSSRΆ}+8v*^m݃|"aE߼!ޛ{`wF_л{z*V)c`#9T*'sTTX!c*x: io{sMw֮^]ǶiiZZ&KMcn饿Z/ںL}jt`znyR|PXߧeWKNBk0:l;; Z5N~\V|ؙly.jnBFs N,{8࣋ A7 yl%:$IbgN;I4Z.h0\V^q:3Xh0GnHH%XH=Sb0c3?fy.ߋ R_ZR<>2`,%eӆF^l8TfKpU|y[Uޟg-{wCLV&+C–jym]usƬ$I3'c"w7DF͆ w -,!)U Cv6]ҔcPG.T^*-p8, [s'Ř>10 ț_$~N7lTr3 {WW:$7Uz7mA[iɺwoHݧsgtlޱ Ya 6\k Bɶ5n E;S. DWd%]Pr -irxAwM2%2Lh0^uum\[:k-;x23sWZZz񅒬}*vk:\v`{E]q]JRr~QC/MaϠӒ҆c ST|> k϶3{O`PJrbN~v^q^ .YT,&F|=2E'U .j()xvMm4˕p4$IfFvɲL5n];g'LKLپ>`vvB }}IlC9Eem]5Ɨ>NN[lޱVx64\5]f=lS zt #|yt? C` IɵT/Y,gɗW5'DFx'dac _\BrȵsU}m}TXZ]E]මĭ}|Ҷ3/. 3|/~B"YMcӽy]57ϟ+@ؚuwo㝑 w73z_QN=yCe9Ĝ0kʾv@Q]^9ކo$C>Zh4FB?^cj4VfO~m˘kMQL^ߨT*@"ܿ?ബ?P Lʛ7#dM~l/ عGahQ*V=o~֚z}6o$B\nvRu C`<{ eEK +קny?#}MPCÁ,x } xx!l;X鉣37,dL"1is1UAzd2kTէ}e?Œb (32BOM}!М7kyli;**o@pߋoXW]`f{zf)N8/ߙnڙtP8MR(j@$_`F}5ۦ,4xR Ξ0㔖@J&[&lа `F^߸ί=$"/D#ktx ̖m(!~ ]{ze2kIfLcHKO)thSWx%N|vb ۷kb ;_ݬ^lӀqT/xhc-^k7#\5(z+jNL}D¢2&FOZA CN!p{X,K ~7Ц&j7~ ѡ]]"=VV6?pFnL˵NVf'wgd)ɉgN\7 w9 pXgάwWE㲲"YJRr<\>80PV$ޟӯܨRոD^ߘV2#7Ŀ[iJ։ (*>6tsL1K-RJEQG`7N_iȂ+P vBQTc\]'VyF8:8${^,Xjs*V; 6μ\樘=co@$,+1o.@eADt;_V9yg{EQFWf3$IƆ}PbSp־ô,C9F6ľ}MW*{q#Sw~Q,%)yʢ5 -LWF1ml7~ {3E߼!V(J6c,E<7 F|kuJR2Ndϋ pQIGZVV65\V;1@pB\v\ qR|Z8Dz2280 oDf?EQԕ]Wӆ?WT޸ۦ@U8y}cThOhZ;,>@@Z;Cj]o~jZA7]*O"č8~ ll ZFW $*F:;ʊ >>pxxP?1$&EQlexf ,Xxj̺?{˵wF_vKҌ\\KΗfq|u8hGۋe|LvgdM-zFhxb5J3~99/[Sx?.L>3woJ>路oӷD/:G.72$vf&I6KZQRp㩹O?۹[mXQ[oB_[ ͼeF55|m~CM}O~r % > UK3CdQr@=e0&wuM`WsԩQ@ RjѴ'-$g"alXXAiϐ`nU>1 K܅A7?"N_U+wrvseV7Ǔ?2$fk,kp`@;4Q'oU%nlP3'FPVAi"Z 5#)d&WENxW9h,% %Ss?JoJo7ʳvk:<3A&F "̅ltk5`g$I;8 O+4nzӒ ٛ{`gRCw 1eKNInB/cE)U. ]%yo[uY;*N`LOrHggKV@TZ~1>>/&rY}kG֖oJkTmޱ˕48wLFjoOw> J7nu*2e**o 18ttϡ5ƣ7w @ήf$6(Q5Wc3·V`fjk|]<*oDh.{:Z'7. QyͷV5kYót:gj;)ieoS[ϟ=TQZ '0 ymJsgTmwlYq][pZVye3NM{``УwD܂A-ذ!ƾ䏢+:2$h$@d͕*ލٛ{ Q#g^E [-^\,!-vCjHOٞ& ™+ss+S^Tm]#e E-1-q0CWnC.{qqqryPs4Š qZ~uՉ7bp loH3{xJLBRi*-wc񌤣0$I}gczȱύjI/gr竛h!2jYܾ] R*/,^-Jw&{EX䪷 ÛoNMnV-^mtk -Xf2|k|gOe=48F&oT*'|>U}}u?sX`xM989rWo`*%jqxT\4CZr4g]"!N VTޘV1UEXd-^T,!O:6,6lZk}9UW۾km*/HӍGx*U< s`bZM908!MD5_ xfmòmSDeNbʣlxmo@͍[EQ*14*s\;[_Uv;{zľk'ʫJs N_(mx7}.pY܄a\WT1h4(A@e?GQǸʧ)8WX?~EZmV;=cYdҲ-ڈ5ڬݧN}w]BL*_NojA?QeE2p(]P|Ýo066}Wmh\/=yt]dSU`go`)-3ĺA-񵷻ԴQpРmƚK"9.zru%Dmh~]"P,GÃRykDe|/~ܺĵ+|yvKz4;5)N`LcZK9BZ߀F %\e ny{ӥkk{ZH@J߯z NVgeo[M-u/g]'{+HQ?9[)ʚ&ù?$Qp8~EApyb| <sM=t`ɶ )kVEFtigg>,^%t5[hQ~Ll;zy𢡊<>Ml뾠T4p8~팉uL,+kwVn,9ͧKթTF7IŬil#'{]F+ F04QV,ЎYci[Uů[c@1 |,gf.{s]oyc2k-O&)tJ t\Nʂw ߝQp㋗kSNqȱ)ڈ###wȐǹ͋k|Y{rmlX$6̭j4d;|竛F"Ϟ/^ #)ne;<_ޤX?dqx־/^\'`Ocnp;r|(d}j־:P Y`' 4NfFS2.fxvddđ%ع} L73thO=sM/\yl$I1CMW7 ^צ @[od:r9 goo:^OpInl>;2v3g;"$ϟ=G/zm&xƊ}|F y#7/? n [%Ý?22bt'$bh-/ 9U65|Y=]=wJF4u"}2X`a\̆9xp#;_<VfӰ Gwy׀;-ߋ?ˆA/3Q Gt[pܑcc3Al4ewFp}#}t?1k ӁnQmn{=G}"#1  M7ذ.82 If/ I-O h Ffximyh!]s ]!(DXsƏ|YP#| "J6xKgΉxrɈ;_d&H[_sO"ӗ4BVfK@~ \2A?yKo;c"X(89,ڱlY,VThZ`F{s|˪]w7Z7<ޑc3=cee/_ڝ S0p[ԭO\R\'o(YsȦ/ak|^ X:$q*HK/žz{o׿yc, 'bmSUtZ{ϟ=/;d}jxLԻm?nݸi\~[8=}#T?(zu%^kVxL}C^flG=n @z/IL/ZdDC'C˱<̂[nóQr3J׫kw,Y -eoCš/4-<) Pҟ" \Rmly."`YXid,czgH_Ud=R鑓NW7wgdHb?ΗՓ S?w)gVE22 {sh ώ;#YAS7A(G~+_njn؛{@Ѣj :`(P?\|t7j]{a,E$D|:y5|[pZiʫeșyj4eE2;rZn2pFo[@$LKK/mSh {R+0tvƍs{զ?$=Aޑ„u7-Imvܑu0ȇ|N=_ZZ|xB1GNkjt߳ﳬ}žzx5+.d~/%913sWN~!-|%_8e9:-wSUYѢ|b hVB#ECZcxM,躔BQ_7M:~ܠ<i4ܳZ] Dso}65RTȐ;vT)  鏻@ nJs knTef2K6?p fxx 4%MA'&xS݃Cw>V Fcg *W>l"QTZa]՜:}'"V?$2XE]:@Ýoeei Fk"#`,-Ƕ@8ǭ_Z#KQTaw{؍1`gi؋Gڰ{*=Z$^ }t <o)Pm;vo._֞KW;q8kiwk8rcv! /94iNt^wb?5:a'~|{w;x3Le۾yk-[_ ُ/$9PYSzNFFD}zu3|`#镍 >۽b[aʲ$%&@qcIk}uev'{xP1թAPt9si*h]߃9%|d}@^A7>>>p[/T/ ъHeͅʞ63*$0=pe7nܗ -P9CQ<: ,?d$'gjűG`w|[яfǤb12|[&4EAa*Υf$] IDATr}=興~韚L+?עGYo._AH+vwǟ^f^^hytTC~̞\}興~_}Зtw'81҉?p(h1?2"@?4 ƛcʅUs 0E\h1ת|42U97Hl tq]'&` qlvMtBvdmy@ GӷVq=x 7~͛agZ1g,r2D/A2zL-+cua!#h$_P! ~Xvُ>d7tvjhz17 $DŽnʳ:Ey0$n,NH+I[ Rxٯ܄~GQq(ىτYdD6E/B  ._#ko7+K28#[p1CbeuAF._1ޝ/.._81ꋋAalႾn$rFr2kg0鳊3~A_?E'O'8;?EJ[M3T8:#9yp_cw*P!ŬtnN$9ُ/JB9W[1jϗgfHŨ 0׈ZѲ8 \ޔ>H/А ͂>}&)-eMښ>6rE:}$ɘ%;sbسs<Ķ(V(`n(7[ V7U]*N~YoEGE*B:5T<لxe^/Q6P$-iAP0fOB²%/h+נ17>I{z=–aC/ů\{@#F<~4o`V' G9fR r~PNmjm _m&sc Ntx&MR3m'6luu6]lvz`joTaXt>pD $h#["SB&,YקDFr SOV f%r2W$,! J**9#9!$M{jhWO R[羪x~q(ܫ猰0IhEŞinW48Q 2\"IQoGv[Vگ7\goRPv'/MﯶLN f*~Oꋋ(szrJg;OB<$4|df@,:SOݝSRb\aQkIQ3=ۻ| &*\p{ MV`rا;0 OO\xmSES_:ߙ܇eAiG|0ix<-),r_痯޸~UnvC zxjbʦ$j_LN KD@: ;:**nk_gI߸%GIhJݢB (0BM%Iǣ‚# 4Z͚uH':*$Ioۜ=8w0Wi*;O(B!QEzK zCƺR(h5`!G&/Js3у{}A` [ZxΔøo +3s>hO,8Ql& CӴ@(&6JQ(yNۅX EK h5&Gr9ñ㒩Oyb@36</ʼncb 1 nQW~ M l_c`~J -6 ׯX6EB՘, M[U7JX٥=+?_ݮ6q}BaBůlz}dnԎsݕ*a\|!xx$iKdЙ06*91eB8Vx Z<7n<L6=pڊ ;:R^R2lY'grs2ͺc3]"#qb0L:U ׯ*Fi|wWa1YL ,V}N5Yq -syh7.-)G zFJ}TV~gq5*vq瞝(E٥UN?ZV]#[M#FVff}+*׬L.]Rve/He´g ÐOIO\ F394 j0Y\L0 46/¤7?boαإF2 fLe64d۴wNf /,shM]ݭN O8=QffOa8M++wWvhfbT\@=3; n%@cΎnQ݄NqT/%Na= 3sNԉ{657ܹg{SҒR~J4!̘\rr;Z<%ǐV@N!P" qB<7d6@Ibc nQU\ yAP`HOo:g%0EQK&_~OON4Eݢ^EǩWH߼ƴ*$ ??;%+ Bamݍv*[ƍQ(E 7۠U9EwdmSXa0@l\MV|d nj7SOD9P ]fΝ{PA%1<,̉0L/H5/ۦ6\!B>g7 D&l 蒒.FM$n 3=y030 CL ;kuqaͅ[+\b`m0nד֯?-;r )ں$I نl$I[T%?執}sSkT,F]cb"c |;/."[sdq>t?w(FּuWg֛7Ebʰ}"$89AP?1TBlCǎS.w~xv'z19z*<41>:"-(K~'/'yټ+.U!7MW Ƥրa3={yG7?+cJS{6;f6U?!,14d{޽fo44dkjnSl\@P֙m_4Zxx{g{VH>+z'aRtD}5%Ej*1 kliX]X|`~"興M^͞ {t􌬦+m_#9NkBj"߬m2~;O(%Cё|ŭNMsRnJ2V޷TP(lli9Ha sѯ%j1s3K}tjoqۡYUWQ(\o!js!۵6z۝nQ*EH~>(wEs3ػ`xOU=AH[Cݢ*,>9#SKGP_~ڹnf'stPƝ%ΣS~w*՚_F.EeIsˎH-yg'$a%)[sS_Hٰ.i=9(Oͣ["H%A.lYoթQKW@2XX $c|>F[Y?wlu\ohZ\x!Z)*}[ m˪kЍdʚS q侾M&xM‘ W{z{wdmsU2B&.@yhIOdI/:Sqcɓfst?wPDĉAB/p| b?Xs2H;tObR4YT7p04dXzDG%ZnQTdPqIICSUhpcmi]X3 F\~S{S?jCCkHܽoat`P$diեͤrcpxleTB 5 Thᢣg?0cZ߿}gLyќ'Z-NcK#(c^7G y@ǃۭErjn1 ֮RDK\Z`q7 c!cYّy^0+M ٦/`>nQ,(*u={vM.<m2}XgL mY܎?wLf3D*q=qoQ\eqhJzGíO)C/ #cV~+u+v{bdmZ clu^|8T1ӨDAC|}p2V?쁸 <,EQG>PS9BM厬m0Bɛy>3{MN͢V@l\{z{B6_ 侾90p++໿5(b懋.^aK>P^U/0!a0pB-IX6yYݢh5{];^ŋa4dz` p0 :nbr*l{ vޯCmZBQTV)GbٵoM<>,w`{f61]l*AjZ,zY@A=; |4Jf _󼼜Ռzr8n}F+\]"rp?`E!ξ64>8k^ I_RURU 57)P=~ڷfr,7* ̿@^$ɪð0V0L7t`w>0ma4Z/ 8n+׎/[_5|p8{T1';=O?􉢒DfsGD/JLy@5A? \&" 8.WoxՌf &+O=&zd7i6Mo5,c~a;v/r'0L$ gHQ TG쯹PyqB>Ǡ7Vq`aʟQp}zcKE J@9DqbyS?Q—DGfCq 4-0vr`^D4W 꿩ߴv}m},{`O?tBe憗~z43z2nrMuwBp/*G5 ˖d%|m6qh:oVcrw|FunvNa[>rDzSpp"$Lp$ N.{jT-4M¨gj=0Q` B>9k6uه׮Lz[POРheL`j&` X46q&/zӾǘ;?p IDATNz+vnkw{߾SջF%revdm;\􈝢#h1uݘuC?rC?~[Xq _(;UdcߗgJ>jnvܪ@2<)-ZHKJx^^_5{ӭNͭNͶ}A$/[fi|Ec 0 %`u|?%qc36e2O757(޺&[T'-1!;z3tZ,f4X0 0 S7}RV(¸$1:)-% HlQVo_[X|APeI1ёʅfOF{luT,F>;EgMI)~_l6[m \VǶhPRZ0 . -C^]gA|G*/2@ad:y"IOoT)4]d)MlQqQe9vƭBQTZR!-P~a2yMښcMjsţH4j0,=ilÃv _y%=!Hb:=8)-zCCʬPS! 9%e}ueMX$ ˖$,[9CC[-'KXYwb\ϔ+II$wrL!!(-0!pCLGDp8 w&P<I `o8=ڄ{8Σ,֒2ߒ!:oeBIdQ;=N@gWMPaDRZo^YCmlEujoW~yG36C !ʗ{*wrKV#!Mm&Չ) g[ ʪj`A:GD?eFI[MPy6>K$B[._k^O/,>"Pt|4A#9/:gfL%$%v:S<_͍ku={߾EAaI˖$8VܙkHJ̥*"8kߨ yRߦ{[3:ě:"O q^< a`gh.r@tIʺ]ϿNvf4Hom4ef5->{N# ߵ%:"uϞ]袦nQ%x \q*'$kϗL*cvf0 358L,I1@Znː'F|v8`f0p\ 6ӌ$b,Y/EsRj 7u:n,!'Ooh kli[)rH?TX|TZgOΚ_uwܸk*0,\յ\LTV겪p*;]4o;anv*CðU;| ]CE*:S|[(cW~~OWgsNLgq8E!'eb_zZKeKu;=wT*,c-Q}møu|= vty |-V돷n6>ꛛ*?➘i> Vix} j.TWJA*!q]D~|_ cƋc|poc>NHi ð訸ظXoI0(1\Օ5(D"(O7uo|b6ُ2@-sK O\k5 38m4*uJ{drIBu9Fkntu@nNfU8MMmSgϙ,FHB G)/' mt# lAwyu D<#v#|0Xխڮ \ y4hzTeu_ihDYͺ zI k (m;?ߢ>t2?ע%ߋ_Ppoj'{uJUYړB[Iia'[ "0X) bCuTH;a$1}_w]z$m;F)onف{ bGq$,x{&u?waFp5Y0lGͭNcZT@"v$ƋSM +Nz ;9{r2nY7gGd3<׉nJS^Nd3f% ZsV\ZNύ8Bdos@嗗7K֪w8װ- è=2*_1C7}V|נ"XiOkwѠ7FN)mպC.=i8 ֛܇ߢ8LݥYië 7>9(? ^XVqo"~{r:~玝蚸m!0;IE=n盛]ZES MUNybbR߃0nvf4S xB9DGܶ`p> `4+/_Fb?d-{ PXsa]'`]11uUg[)!o3n.{3se^2Yjʲ!$IhZ֪V"Y0Db\.t#!ki06Ã- Th/jvw]W[zCU20 [\WG]cb6Rf{'޸AƎAF3F ]f0"?|k(:ZXt u*WM{A h5y`^]kmww?- {tfHdwZ[JBOy}Pf;8Nc}.^gWIi)-G.$}} ?Vsl~fyIQ :*_4n \kD/x 0o22/rmv{gjRuw4#Y AHɅB[՚ 8!-L^MqgO)ɭf]AaX;c't,IzO%̓6×>yzq*i6h>ZEqHL-PX|L* wiuWjzz{_uttT\XHkV#Hn02~Ep0O"ha ,h4uã*0D % 8uxN.R)#_͝'a!aG%*£MeGnQw@|'$~J>+ fn*xggygƌo]R+%C+PRTR{D$ K_Z64U54U!Ǩ=;w<6ԅx*m4z8p0p\%aH gg&4w(b/.敕o1\Dzf6p}36h5W~;S7Ǩn9s3;%s腡os[FlAd6߬)reܩRU5 YAe3Aּ#$-u >z?)-ח*)JZ`%-xl٠1X XL[#Bܢ0c(A_}q?U B_D?DT'GxOή+H0;9W]X|,D)47{:`6¨RQ>1:tv- &h1U֣j%ͤ-fqXHP(EFDy|XzHƑL/a AFj?as}CdXɭf9>6P4斦̵\8<Iy}l~Y<aBCuhWrJJHhVEGkn\*칒s;\pyy^^AP5yU Me$XiۃWr:5Qq5F9`psfnhtnä\g4HPŷ^[wS$OJC5hgZ60@QAO?%ofɫFe1ewfAdB ?ᆳTY,'Nʴ1a6]u EpHn2ǮUD,*wT͹[ׯ*:u/?of)Lru) Qb ỹ6m)/_l'{-޺٨WQk;74\/TRL(LYbRZJmݍ,u{Mb=ry$˕fRz,vfhEP`ȘP0P y7. pq6Z"q s `@*wGù\P8b|ҸȄ4d{rd 2tTEzrOooRBK:tOi_]RzzfX]|ܩ.W[ (>]9%fIO<=lɑ<]]q!o󥉉og{XrF$9U ٚ3{qػ0O6Q|t'g&~ǁt[J`(p,T,ijm}% YPKan(f2in};iۮbOD -ё$('/rq>oSKĉEa&.OX gO^)F+(_⋞yӆWg92|ߢ .#,xm  M\ne&Vx OkqL6喝TU^Rnl+P9IwS-->{ *Ԡ鲪>H۟J|Hn'Pf; ZHB@׋–&._ i\"sTp5Ÿ@cY=7Oߢ81 ,}h0`\\}is-Z[knFXtNXHش̎ggw[b#]8;4dթ}F}ZR3x^^99'Oߵe**<׭-R[OА-ce:R _}qLpduј>-:X<2V/+ǞCKf=dpГjSsö}ۛTL^3m?ׯQE`16#=7Do8d濌uf6^@'S"łY6apQ@XK[ eq=P^ӌصXۀR} b?et$٭~*.UW3dJA_(X:@[:#IaIK<%\ZvᏛ~426O?<~TK]=)Jb6hjn@-25J)- :*.镍C1E.t:»B0(0U;N*⣔2HԌZ/%=_}_9AO}$@| ?8j5,hoQpZp4 ,_#G"̼5{S'|rQPXOoLhIBwdm+,>xeLo IDAT+mʼ'>}6juWz⒒޼sGzYnQj':RULdd'T%= Xtv7- ;XFrla E= v? WEA@;<5|IvU:' JF]K#38$MVJ]gg`}%sN_RY^s!JedxPBL߿F)?_"?Oܾvg*eUbq?EuKBo>ZZ_Ǿk4Y]q[ \1v4%Eťw!HғWGGE1'M #Ih,"~@Aoc]3dEi<2!'m S{)J)}|p1E~#4Ytǧנ"aȓ0 h26Jd16HWDf(7Lvcy­NSPx36Q|ܻNjV6\ojm>^s瞊KU'ke̋cVظ~qNB y^^sv8ӥ'Knujvw\ekR榘O#HGKP&T,nZ W*.UH%-6Olzp__P:ISt\{߾/X)Qُ-AfWy0lg햛9."B0*7$I3ů -lO"?4yPHک.$" :d3 JvT7o㢅>)6:0_"N06.ùIi)Ii)u;Z(.HtP0#qôɤSs1!{#nZC/\]*zz{ +MIZnllT٩~1cd5׆Z(=3;=3$-oQIAizQw~4~JȷfhІ5 {uu"ld[1eUyvgwϫ+_/8mɚ2@ǿj"tc3WAcic"?}r\mPEkм<*; _g?k1L![|:b1YޣN/jʛ~~=e$I*pmvK[m IBC˛N|~4{v$ǥ$߰ [zLwn|>7D D,,$,\mMMt (<; V;/Zm}@K:_Pt4Q >EqryQ*N](I1Mw`u;h,+DIEoBHG@&a:sۊ11TҋtiY[8PA 9$T(p[j >ُ}d:qvEE_)ݒyx4˗J(*R&-~o~G% '>U_)ݕ}ϲ\79mUR+t}as>~0n9@*pᙧr7B"çGyH]dQ|Օ][{393xPł/Pq|N*4%P=a=sp.#_%C&_i3zWmݭR)Ҽz}?Pmn2C&HDk_G/iUܷשc4)q)Ùg-<*@Lv8>c'fHFXykîOru,Yӄt\ . &ࣚ`۶bOCC"4IV[Îtq(J!%ktKWf5Vjj:XfXEXGsM9 p‹@ qǂBVͰ ) Z#_ϗ=jnE殳:kŤ/5 teb iq`l9|4xI)񱉅  kq .\klx'P5=Mֲe﷉u?;h2zyC3%Z?|W ^@YLVk21 o؜RY~\o" =i-n^ YLLAL:aXgwϣ1,b ||.].AV=(ȟ H<f(t勪Vԛ^v/v|Æ&8nw^aMxZXXýq'I . Ac3Ph]bhsDva@MR˪?;?q~^!wD_hREPjh9 O ;RĒO@vY+lHv??]~fO l?/ v{kjvIIXw)ʺߟscQ(#6,U9h2x'Oh>'~޼wwwܔPk7}x¢$ELtA6f+&dS?%-Y20mc% }$mO3y@>RW5焄ϮmCb2$|{ڼI1IR8DT,E Ij89O8`$jbZ'' aIYD2*dB&VѩuhP+uN{8Z,MAJ% @M;FCxJ{$Y]U=s y;;^NK*k7Nl};8 %t*V~cswkRW* i0,2*T ouWt #9NN[ Ņp^)T)V5PEM^[P"`pN8Z"i3w!j7NdS NsapsHL"lfj.]n-^-mj3徻g{;tmȽlފm]LZuld6UWU o5~ G>EW^mܰjU\BE94$s^s*@Li) @T Qp"WLlnmѷwpr$48&w 0JT:*a1]>}v;II먻}K¢$m콖 f(4ŷ+לӭCbS PlXm JiZj25*j-R_a62\Y wl]([yE7Hh)߮wJ11 J0v:mU_5DjL]ѠO [1[ˮ4}'ehh:d2h}rV"VX,_t_ \1&h_]Z崔@JJАÇc436mv斱VSUZ,Sq57@^ 2u5"T!ď!qD:b}t(}۠拝0ϒYAkx3ϱ!,H|Gl Je D9LC} "a0rÇ=~踘MIäEKymcŊJ~Κn#!KDoY2 ss;0LM.66f~F#SQ1b19QQYY3`r(XJ W^,ޟPoX#n 9Z :"hx>2ayę.8f(tcw wuV-( @ag?g-p.'3#P]h;mtX41)$3nQS*j<,I;UPGL]Ȓ`onnmG|ҩ#8!p:\BCwϘY*S/RڢoAPILCjxhkdLRϞyaa=Y˖ _ypȤlO3y0>b#˶ݶK|Ȅ]V;\t9FDLs,ֳ4b];,$*M570!f,,i&43<I *^,̒ 8uCid+wZKX8[Nz3C_H~l_v'˲.i%U)2 /9V{G̦N Y[jZ /_*yuk(đܵ7f/I >}?W]|oˏG؝nq\ᚇ7@d'> TųԹ< |A.3xЪ<98O$%pY" a}@M`hr9aBf[]U1wwԞF+k^AJYď݃Πkr8leɘEM#͒X-QMFB*izs'z4%;ğ8o˯66 9lՋ{BQ qIgO0WJrsxD: (B櫢b8fٳg;vͺu[dTBֶ6-DHGq_b= ыRa{ssM&?VqêKKАHU[ j;݈.GE?wcoRLIgt]Ubd7KDo5 ~Q'øy@K$A*b_gv|4ď1l%pz׈5\"XܮD??TE˟B)'koyrFbr6 ||hu(jL~lv{֛+WֲeHg!|Hp[Agl>'+/ZO%ٲ)1<77Bɉ#klޚv#`ۓ$qh(&fQinwW뭶<hwm h,!&ᳳg&># ?/@ڡ5O@-}5Z*yEdxd?K3,95)O[{M<02"ɡYv[ #֟iޑ9 _\fjp <@[\ <[iכ.| $>$Тj>?8!'04X(7!&܃Ťkjo掭T2q**yo,ɏF>, , šW8Djg"8; ˸45yIɧ\ܽep\掑Fo ҷo=;7?{9D"&|S!{} A #pT~х 2_>El1bkaX ̴|@QW_׾r|J40 > f(4 xja9܋ f<7O aiRM\kf2 8q\-6A1$I xdС7#Ho9N|zbezOGDϛw(w# u;CrOL8|pmcⴌpgvOIhFS[_E?" TѼ6crLU%{}okТo9\_Oz!|UKʪjؾnUPnV[w[:1-mD&]8"Q{)oPaٴ-5]y|}E Fx"cy2D P*u<ϿlgHa }ƁA(a þ)9u,8*fO_Gwd0|+ouF EQr?~I;2>vaĤ6WxsN-VJr2Z_yC:5P)]fe3yC(y| .7 :G" -O"Ofe8Zbs Jse c)DE@@auMn(+ٓwbC,J;\ OLH](iٸ|&LZ/6Vrб߳\ty0?LY=#'#9VP|'>6jZ͛mt#3:6Vm3жzW~uCG@MGL=UռA!yJ3%Od;A\+0L>1x b )?;T]U]c-mx0c3 O냼 /Wi=iX?3;.~^TVv6o͵0=g3y@((:xDBC"3.MW0o :]DtvmE#d!Bt$bDlpJ\3˲OHCI<|5VdEN^-}0 ^bݖo6_fEsZ??]PU[ފXO?"f]ϱWVn ˅>y?~[!~̟[!$={x4xߴn˽ĥ%5S[(R4r~pȱu‚_|GCo 8MԷ~jx茷EQ.VD<`"TE 6gn֦+ˣ)z)UzՕZ@XSnf5],.Q[g0O= h A1<"hc z=[K0 'e89c>/@dnS?tvRez_5[m_ +|gSW߿7Jw[{Ҝ5:-t"HPx`h"JuqrjlQBBRNa(D^(W|/ð_D1hu(AfG5LbgFW,{ij,fDŽ1VmZJ?L- ݍ2ذjj8ZАKM{?p}&c0Z]>Pއ TH\"<t|c4EV<45a."غVc\l$L8cw);00=,AiY3,­V@7`{m.&k^,g3 sN$@p\ oGwwc߱i:pR,| :P9nuVL!$2S꯲dQYY_f<&||_VZBivi2??G_x7`"@B~""Хʂd2D䔭2|-HqG\YxQpMT|lbk61 +b`so5 ϰfNW[S;"D$UzLΟX2ob|~,L27OdB9} &O߽'=r|hPٳb"@E˂c燣u$r[XFt PyӤ=uPurnG~/"" J*Nn{1sF +x5pa:Xq9!]0ܛ0&Z%IjRIxC+ѵjnR؏bzVI 8Q1C&$9]nDV h)!V #ò}ݤn5Tɦ?~2wO|D M-ڌN 8 g2w;81 'JRB[ aS#tjcMHje;oaMh็h@ %|$S2Z,N%؛X& 2?ZBBCg|Bn9Jn;L?Pd1z1=o Ic,5>s8xn@E{V}>ߚ9{d 4ofa|mtCI qI7fhv8H)4L&SXA:T X[jw@H:HH񐲐q8@81.['WdA4m=;7>K+==/HD5u7$ɏ~ Vt]9ۥ2ɾ^]ab5w6h QF܉`O8"u_7tItC[}D+z)/Ng9Л駯w476)'||V oU._F$ߟAR+* *hL&_ @n&`Y&%a&NٍoRw}&-%;Br4pnɺe-[6ƮkjJ Ic&|ĉC,*LJR$}%Leh(0yBPD"lx7IYH@IeAZ| o0C P'&+swȝrHd#F#D)$p2!&neW(*J"eb@a&\8rϞ=.{(u@N/nhA1?o^ƒU3Ծ5c9 +$(L|bsR__ IT6c{Q aQ* t+?8p^]ڞ=4U5ml q]kt.嵍{kj$:,,cɪu7hoͺ46Aנ-E/ c$hoý?Y .Hzm]$ER#c4FQ/6xs\dey 5-Af@.%^EN1@YxB@v4xMC劊'NC -2 \6WAG KCaN} 2[ˮ]y`pK")0ZVpbYHJBKIT^{{yգ{q_0v[n^XDCߕ" <0p;^^ZA$wj ?~1C D~ozL~ɛQօ;;fy9`AC1_]'aCJ"wYX7& C0yKRjwO+/LJDfN޶:Q]|A'>=իsN U00 Rh5v:S֭=a*(Hl<ñF)=yQƒU4][S_It&}`,pk2ӗYыRAvf,]|h]A{%@!ACtJ:B'$<܌tzwT ]Z~U=Iqpy{#jjnh07wm+*Xq[2M NB*4hB:F ./){ukzcd!%9-ٵaa*] '׻b*2&MM2 !ew'WY Ety!ɸP*ø% V Aݱ7 N"C5NXxsCw2\ 88^*#V$ZP Ϣp÷ !0`ם0HAG'F8 ΃D/?5aOШ3sO{3 @Dlsia1>!G]Gٹ_ Qzy*re%J.+= fuҳ vr?>y,B`h"x[o/z@|lbMݍ˗JںBy;2PqK77cMC 7Vbُ*^^1GbtFn`w[&Hͻݜ{3Aں=*<&HZ"{y?QXdGWo9@oc,-]@x:|W]5/^.ټ5>6F+xyn紆ܝCBC"oFkY˖b\=ݘ? Kv{QYYQY١G-/OýRwf1/La'FFashNdѠ(Ͽ MN& *oTRp_)$|wOH{8|;p`ښ?OHWo 뚸cu< %I5)4f9yT2*rpᘌ< &3y~x4@Ffϭjdwy3* N[7b]-E,9B1J`l9nBai` :- ,HL VdX̝tL&[H8Kes܌|._)SǠiG]૯?ȱA{䱛ͭsUov;A~)Kx,p> B&xp @Sr<FEFG.DWp}j0 IDATlhc'>:rJ8%jtz]DD4 NZ*h췷=Nߠo?+H]ϿU@8R>NqݜlMPmSw}w[z8q)J8YNRY^rpkOi W|H\@x@χK2%9.%n?{?~4xǿ9rؑSvoQ}jΚKmER`d_QrCNVE@Ң%%+Y(wb߱uab0f(tj4@X\ں^B'ٸ}vsF&H#AIA`AsX2w w>r<0_=G2~F/ nG"> R@A! Z- 11An?Ůk% HH"!fpg8 a/ /|P(Mo7"s&S7"klp/X'AQ AMErhV̚%{Ų7 4 xg8u 8@<~66>FvgQaxLf8/oL7q_DnԽ<-䞚g{-$ 잊8&½4^(YDCypЫ#pͨ|[Vf՜q2>VI.s_FW24$E+7/ %W1C>>?/ѣ`hjW;T/(PG٤T*i)qϛF9"$вfWe&/T', `x 9R99͒} nZ`'c4u7ƄW:j<[c2VQcR^uVߟ_tANw3 yf̶yr@JL5iju5_N `ލ5x[ӓ[pf֟xWR{qMZ;D2|~ð?ujp͟ p/eYZJ2,dE*ʯ~lsM?Z_taMrR\l$7ɄP(nu}@s}}a-H8:] anS~]EiٺP(6ucX!2::$0[;&(֤}ViEee#b6\?o޵vCԺU?|377JgƗ;<]诧 9pd'ɪ=Ĥ{:9Xccy_q 4*C@zĚ<a4$ǥ$e<_e^!ό=dg<ފX6A~bq3~@RE|oR|xgJg?w(?˲"P! A9fWH^G_U4kҲr$ٳ_ZDr80X,ް]ykmc#@#VdTxn7w-A{VkY_w><pO0^`n/)/y>Hox \(x桦K7]FF(%ٱy3V  MϛWظy>"M"v 2,fufUj˙ums‚$nI$1qeeඛ`4)S./ph#2pJVspl-CUWNe&m߳{.]UU"~PivC:typl/Q,^7sˢk,*] FںW^J/C>]TEq\qBOf,c"O;t"_#O {1XN`bG??;3d>sl8)IfqkulYVk(VkkZԶ߻ە)]W+ڏz^ 7_\/s9&dCd>0 I%hq8vLNg^|?_8x3PϏy<ݲu;yωu qI:}R;:\6kBeGPm񃷫OK2YABBYe,6+FP)wys`kocu%$Y m*..YfΟ~E4M?Utw1lE Imd wOz-%C>/H(Z :UoJqCYn=*0[kn4FS ֬V2#H] 0qÆe$S?d{9œێ6eH/|5S&oi.]U*ҩȫ0?v@ٙN{kiyDp[X5hovwyE.*o'/(*yW[A>aM 1--d4n%tp#%;J~} I1ǁm6P$񡻞OXok+o8\*Jű,;j2, HI>+_(8q4 N)} Rk A݁quf8WVa{nF0b#GQ5=V+ y "(nKޮkI{K}n2A-SaiÁ `Rb /dXus ie/VR~+ ~g??VST>vޭnݼQYY}9 E&eΟS&'d:5ʌN Dzdll1Eg33mL Y1|EN m~ckW:0LzZZ}Css_^?Xn+mdl,k{>eMPg>gھUZbpj}_n|u嶅p͓ھ- 䔸TOo^q>e+JNgԼL `SS3(=xIV}swT@qɈ:8!\g0潴X 0N]o_vyfQz<`J.V1޿,0LMQ ]`TϜ>022#D'ܦGFfJ!Hq:&4 D@}STrQ6AE2'twMF#3v[sk,tE lqHѺ`D-tck]MY vn3N)ptqހm 2zQOje/[׻\wq\R;q%jgmk8'pL>-[ ׾r^Vn4LEy|Af}EEbt8X8N 8/!qu!{ `_%EʓZ8kiEl=OgXi &h˪#XKCz?1>?۪ʪ33_yOŁDsjr}CѪd_]>! ඳOPF2Vyຐ+=v%o,zEB`^ Vtv4י T߳};''F\kYqNCa0_PػR>h|AAg0y>C^8޾ShEx^c8*3vx/=v`-`0Mur'Y"xC8 \Z _J8F_`Dϣ}_}'11$R]USspM뾽sc߁^-9otk7G3 T2pɒ%fN4L=O+G`R<̱f/[ٵiU Vtbqiu>+mk8%-=uze~{72Wс @µ2 Uds:\5!Մ~J_x3Դܜ7 {8rsUzU55z_kjj×hЉMu AЮem\dgf8)NMu4M59-HaK3J>sۤ۰,DQueSn]g[g;+$,_j)ܽf̼+*ez.(Sfڒ|hMƦZ׬HK7z/^2Qox/gv\K_6Up"}ies-ZS3Xup[!:RѬPd<^p "T/-[Q4~%$=@9&P)ΐ!IjX\j/ׯx԰aQ[ux<߁R > u?y/Z8?^ׂ =t๼/W;i.>>!>َrOJXt+WWsZm1u8418퍾2.][q* \R^NR!RPh&t)S3;Jݺ- |WyhOrK'Jds5k%mo녳f@87|{wUeUs>SO3Lڌ=رw/,/ڼkoiyy΂y[:|hbn7@AR<e;IuFmo[,zHhܶZ[^)S)0``5o#RNLQݱlםf[ft%Cq'>w3H%<1$5:!ȰJKV߃ҒŹ9acz02Z !c9/!ɲpszZ߽ /E qVߥsPPɽP6Ndgf e7 0 taThA?T*>N4},AA,=ぢ3k6nx 9cj=v`i%s뮍c|@k@ZdqzJx?_D- Hoq,*XjMUeղ3=rw$%bypx^ڸtjeWnz(%PǾe6HRJ l6 T, K^_6/*K3e'Z/3! $Cl{mn'T*ʗk/߮uhab>-Sg9QX8Ϗv 1i5; $60jUF'Y"x)/|Ā/qJ6M]asRT㉾ 閺jV7Sq)&c \u7ADREՕ/7GA qҌX]1=lVRje(<;E318|)b)=.}lA@ =aAU} z)Y?U[g>39gXðD5P]@yY~ 퀺WW׊B\s&)5m'_0lݬ$r ''ӷ͟Nw cc{Tr AQ>oQT4Mߺi5>wG A^v$\DQ;Ū GJCw]\"ceEX2w8ð㜤Iiϧ&^]B3g̩^Ѯ]9KRm]~RƼ43 \1>4g|ʗU*U !uR=gi MO5sq0d@%:C,۝dgfa=Xsզʏ|.0,iibxm)]Dž>jm.A,dT78~فtyyZܝ湳K? !5F\,Ҕ`8p'W>=eڎ̣{69:)K`M~^^FzF>_nik ˰ׅNE%VtۉF߹}ëW Ntv ,w )Mٯ-KI'4?7󕷺~-p:]1 CZ=@"ˌ x%r">J>Xt$!C/c .9^-nw 8H5{c_8&ilVq 7,XVܻ(wZ_+W"51lr30} Y0g4$hv;òƒn-;--5@pe .^l"qnݵٯLP8Tp+wR&‡R9n}z&MIfg-{ʐd]qv MӃO>^}ljӗ])pcwe<AXܢfH؊a!mqP©ώۼ h9u>!}^/O>sgx!m3_yfhnGڻ6s-R:[ ݛikox;!j2%&Bf)&; J?DO0D/j(cBH2X-w@Bw*hNa j#Cʉ`N_n޿ePCwŢrN{ek1OV@B1%`p(8Fs,6wK J}&k'>9)Fq^2#C]uux,u@FzFy9N ccyx.Lq^/A ׀+գƜlwz2 Œn 3fh4q1C. ees-@Ӭ+۝%sV2I)z阣Tٌ$t <8 es-r(#KdAd "w>!4m:fc|g &=iZab[V#}<;{QSserZo}%#ڠ#iIp; B㘎 F`2/_x& mV0M]g1`='O7ܸ/Eə]:'`|ݾmɳW- Cw: 8 /H[}؋qh= 'yUJjy<ާaWW"G1yEg ή4ܪk1]&*fai0Àœ T*dNMY0/KkYЗ. G*; 䠰9%㩪$MɬQE8RpW D2Fd"")d2Wn3 k7AjNj.n^ju5ܾ\aqcezcgN449O @P6{#dgf2O8D};>:ppK5PF3's6֭pXsyDƪy^} 8W?1'@iK+CwҰaҥ(\㥙SSsDgqZ>!d4&?m<ʘ lY4o^E $qM,<`?oּLˌ4e@&nOrcGg@2 4N{0j>!>~9M=Mʐ)hs|c|߼B;jT&Pr]opq~ʳ,Dzlt>3۞=3y^%,cY3qN%txzXORx㠧+s,KRY~IO~JN4ѩ:ƕ|MlHy!g+o]k0lUc4/QWmݻ1 ŶgUz<˲&/[tFCO@`=x!?ʎ$> xݡ:)m\2,eq;{ 1;N +֍PC97idy+Uݾo R݊ Cg-(XfmNf~1<{7@o٢3LyADRuFɱ/֬~IܳMnRy 6رm#c=/<YyY%./FjԀZKz, \O'Gz(O+Cy?Z7K^[﬐)ƭU)̸CǢ8h quAsRR'.(wݻTɳ`5nwQ2dy[qD5eقw^8W~ uA^iM j8A`>_]J@,H5|~ߜ5m/­Q t; Oꪭ ssDo @8j\8 R@Qcob'Bq?4 *pFFS Y"pP/y”G1#td IAAZ!% aHLv||fҞyT\6m&>6(H]ܽ+0dDg' }O ZO0O\zh׮v[?}.ۣsEQ@ a=֥\q0<49NQ @Ncc n=Ϛh9~Ū/U伿"wK| Y",B8 0HW@+m _4 Z2RhlvV RA{uęTSR ꪎkh$uiЉqҤٔ%2x:>NK9~S{M J!4R(M0dgp,W,Zaܥ,ѵrw]0P` HL3~u&KpɗΚgdlg5Vx<ܟ-FNqa S7 .H"Jhn?fQPǣ8ca=zznEEjCPhr: Uhaq;wPitϰjDA u88co->!rUn_q,e'(DbW8L+{XcVH1!*9D6t&`eh4'/ ! h+ @Fw_;?tuې*I{7.Hť";0l =>Ty &6i CǨZ}0!ƧӇٗV&}}xɭ+oYC,]}Wzdzתg'/=AF p}We`xɈUU3:tq6**JFPWg@4HJ#A,| 1\?(8}̕}ui\B=1 ˅QT?oV9Bf%x)`"D0nE dXяS_w"UT# 6O b!+f;;:5ii|S]Q,`[$HmX!JZQrZ:zASC͍*Je8Z?*Z*hI2M B$Q>`QC7W<zͪ;ԝ46Hw66B+^İ07plٲWW џ-q%{<뉌/X_nJL ;N{+j3ajS tjgXtG=(ht;V4N镛͓4s%cF5shFK~TT_GѭCCAkbcM̤hSTc8ôt8JaEK7Xf-Ǭ8UT4 e0b= wY4!+&fqC.ލsWmih Ҍd]Q+CҲD+ c`rm]i ƉO7eڨj8}ly?RZ^U;BX E JmiqبHT81vԝle_oE$8.ő KqMFbW@t~6P)b c}[#W+cx÷%EhwGTug2WZsxI:)cqA]'>%Q'?pYQCP [LVsq =,ن\=$rj/(ՒU{c '%Z@Rw3ЭiN?2`@o`ёe֏y+b+=)!gE?658q"㐄怅ۇstt8\ZF= 2#, :-l&  @Y9Ch@tf9lu^hډ+Aw(R٫58\.p]YW$pP:ކuj|A n>n@~J"A0fx}X9+@R*& "R+ 2 B 8œs—iQ;cH5kv=IrOjjPxH.ts9+t9>&@n"3By#틄ljR aЭIe\A QYf8V 0FÕ_(Xd4:\.4{RCj0ԍa6WuRƼmJJ*5]^/[C_Psm4icqi BXqQAXZ ]HaIX]FZ90FVV 5>pszݛڷ ex!Ǻ=DC_\v_ RcBO41hUP*D/H.,As{_bsF;X ,1K+M!1h ;ܙd縛yڬKs#ۈ;1;{oq/\ CY]SmFO- Umf;:q w`VV'ZYͯܗYyBK~m5fQ-d&pԘQWm]*9b`+5V $H"=Čzæש)p  \9qZф(PI7; nr q^mkt@3סOM3ZcQh7 &T>QaPea"KqEH"KtNEљ뎒(n"h?RE$&mB=A@Y{3a&0xx:4 &<{?tkLlks2Ep믷V[xfuEo0T4uĹKڙon_.EFP朣>h@d&x <xNJ&Nr[;02**qC' S,3]\]u3s/nx!w e#8׻o޻U &A\7AEtJg0"lmlq0B1+3gԧtqT $R\;CtDZV',= v ?2#,A$ ٥AWz#%/@,D#{uVƤe@OG2B (Ѹ(Zn]KP9 Ifۚn+t7Zի צuu+RV;k\mVJJxhbٍǢjx^뱯HrVvwʢe9L+9mLYYaޏp/>?q}st_\8U[_zeս&x@ZwSWF0$Q<e/,1@`}#h}e[p9]|4 c= O猴}Bf-<'/+} >F/! KqhT {!y5议`dq2 {!nztQ WiԪM>ˢ/l]lmh4oT ,z~$ 5 *Jfέkha+fܗ>C>וB 0 ?vtS?_A.}q+${<+WfP!w2Rx tc#0EcX09хLFYf%؃3ڭ}ήІ %jq[ي$D/-Hzжyܻ IDAT5DsV c8hLJN8~z~]vvif!*휇ʴ P x ||3%k\q =uM IXXKj0FԵ}`dA9qtxEk¶o.JfCGrpu2R{*J@ +rl}} yXVVf82#, ^.BDIpeQ*q)HqF <8)}8ioֽ_^9Oťd eK)q7 \m gO5v]w0G͍KIV0q_DbNK&jPEH%/8pȩaHGn}o8+I`PzlsiiGٲ6g؝j녋(P/[2 g._HBZgxmeexjziܻkko#e. ?uxiށ9͜ ~Aٙ}dn5=)AdgWxT_|:l]ٟfZV6X58p8x< _4K6ncmc8c>NC>BH }>XĢ2Nm| >T|4 i(sߜ?}lIdڭ7=W<p<#=uJ:SU* O^Il6Qu>;k3УG8xWg4L Ø[Ώsoˋ|"jW]ûǰӟɜ"PD&*xY"ˌD{(2Jo_2Wxa@wj+Y@AoK[jޫ}8ięs^g'0!.i9Tй_aŪ5SF 5z5 )(@?0{lCќm%ǾѮ.cdg ' rp2`ёM\/|>? ɗ.GCn)(۰'r~'}~3\~n)|:ݷI2;3~w[j[@Tgk]ySȌ DCy[R_C 6-q#'"n5ݳ61s@~ǭg/_ܢNw[/aBe4d1N(`BŜfaƺka.z.Cˈ?.z|jкjCoFC/.޶ͱƓ @9n|=Ζ9\\ݔi&q0&tx<ᄏ*z'J("-~&ldm3ғ_x"S-X.T^.ٿC}B_l8~?O/北߶22A"p ~$LC >0J+>pQ״#8dP?,*]{7P6W[=bDͥ, IN =M%F&8G)HQSru鞒c_{s Ov*nwGG7+g?Z>R1zw3I/TmtW]?w殸#&@UMMFzF]*]|]2 LtWђH`z.oɓR-8Q1\x-u71PԻQaGj)hj0A-jh/BHԿdX-ö)>?&97nA0,T\6ʏڡ:dnFA IA v@3ށ|_Yλ[;L466 Jd23BDhn!C 5/ynQTeUQe:٣,q\K[$605yq,e'Q(8`_^n}#oPzH 5z-&@Ck]Hd#]+ -U"v7-5O/6~:B:#BodXۤ!Mϐx0.zid-H}Kx(-/Zm}D]Q,?8`kj*dK{fLtb09}/Aj}yO3ݨ}_M[[[U)MRusqnD!]d}T 4NVr;$8zF{TxȘ]c;Wc3L&U{ Ee+V,_~ݽ q> ʢ">z֨ovX\6 bݤXWe"*6%wG5{fXoO)/Y8|ibZ>ܹӜ^@,|iǐjZܢ+3}E%UUeWL ZS-ԫ֬cGҝ(yan̢6>1ٛm7/uϣmk3 %H K4"a[\<3 MQ:ieQ"!,Ltf@ Ӂ @1ǁbCF$aVTB{%_R榠vSS-K.|rLmB\NֿPF=G5Sq)I >y?&dggZ餇C6W HtG)=t˟/>_d|عSgP-QֽkB}MA˯u_V{BQ^Rxwc;yԏ:>>A3"EV# I:>Fۜw1$Z.x _|;#Yr2oYuP J^kOHp·K2/]閿@Za,?VN*)P7A9EQ#KqG€=Çi>10ۇiP}h譛DsTG_Wf˩@i ! Y&T`ё%-ۋr?YUSs4q@,':Ydt^AXn#5z2 9X0}諔ٲeir>>`9C,OH0D5JkT澰o.^p٤ի^פa2F*¨Uds\4Ȕy2%8"2~/*!cHc$98^TϢVf4TQMՑ$%R]IOE2z\"= dXEjhًTj2`Q+8ֺ -d%xĉkZM2&i޽7Q}4=R[P( .(辺.+z𼬈*"TQ`W9 ,BoRB4mH;4L9LM|?W2{fL~W*|OqD; |"wiRQroE2(ƨ(ye({]LD:N0aҺW斞zb< ęz"J7z50{Eoƴ1KmZ;r7/\WWWL5Mroaj>|`ھ,.%Fo6b4lշ֭|=rEl,∖ Lu:,?mYxl(fᄁCt4N֐3];_Ɵ߅~hHs k*8X٫իQ77h|bVy[opI-A T$Sɐ|[Y'5TslLZl& Qv"ofiH?:\NIL}o-6Wř}uuuuuVt\3{$:>R+&]!Y ~5v}]z}E t@4 O}!MaQl {bsn. )kEj:+{rN}wRF1fqIf/%sBbhHIj5tՖ_a)'15)6TsBJ"tD$FD:GD}&舨_~I DDzE'5~2.zwTRWk7RS !r5RWo,hz; ArĬ>\VB ?.6'/E- Ҙa(;ҙhD)Nh`|#OdpFJ:8\ɿJp۴'߭qXl\Lbgl)bx^dLȞJ̷֪9WgROx2]MI \dAX]?'(/u5-Vk#BAc<7'+D]Ԉ9~̕sg,v>)`r/(8<+2qNs"w9~ӑ3|Rwd) 'ڗ6%++Gm·i!6uYj?x>G6!Z O7|| lAo0'xE_ľ )Nm^W6 =`J6=]!")9S}bM˦$˵лgB.c\[-gtKs5>G$ٲi'ٝR{ /x|d h?ɼOp ,A UU!'%u [cK]Fp#BkaP´n&0~^T[z{IK2 z2y`b߾zOF&9pgR2bePc#3[[n71lọ.!rW!mR\kok̻\}\hX%u 'ܜg#YM/`CB'qQY %/^OD:s\K^2چYRʬ EX]VӷK`rK2!QIDATtxXvf{^{mYg՘z.S֦@sHG2B.JI@~d~#c8\I .rktZ"b '6507AF"X2ͲPz;ymlI"2IJDG cce}kQcy,VF2(g~s_fU. ͸-e&%^lkfE]`^i[Ő0+g4"S$P"w-v=%&il)HDDdlǠ!fKMA@Y~T,8&Yƨ(~0V4#ۼf)VfX a=,$ƙiR8_;Rx01\kw FcMNwybe Q'\.AO} bj4ȺfcY&cm'&:NXVQxkEuҘ؁f)p ^}P!дƌ/J;X`\()<0m)&Efo!BUq>^QgL>*"%QHrXMz"O2)@Ry,$=!.*E)V1.A`i蘪H_\"ɇҟ@0~ceyen^"F7ǐWCvnzwdnD~&juZ0rfs˰sGDF"y4cZvb%so4xUv SQ'e`Uqe)I}I~lX92 c`_\6vm!r1SC;ʑAN#WQMNZ̳@X-Xq^tZmAh~b"܂\|:o=E-Πzps\sj5"f €x 5UCp7o0ncyA |Apz䉛:kD= 1IY"PЬjZ-kQVnNJ[y Y9÷u٫ؘKuDy@̶/fl@΁#M墀=tdIxt[Vqgh "wn T8YwiЙ5@ʗa1Vm*Ǡns=ni(sG!PNeU.{F#]ǫFsM'EaH-em%B0~1w4uMQcMxvR 1t ]!ӥ.c|ǣf^1Ur`Zc|ZNV%,bBzTfLW"fy8{I復1=q4vl˗.W9oGSz2BЩ"w{GD16D.o@.ufi/ leTl7+ĩկ!݆ԏ^it()3Ç; U]paɒ%)(VWWٓM>}(uuuԩSx≶ /<ݻwݠͶ=!DK, =;ҤIDQ4hPۑ{Z??TUU_>%%E:;$.O>iv{7Y*Bd/eee/^ g̘+qm#P!rILL\|ϟ[oM+''СC k׮ݻwvG-[?~\~CF%#s'LpIGݻw/XM?=zرcyyyIMKhkrrDDQzz(?;SYYq\qqNS.ՠADQ|w80}Ç-ur(77K 駟<_YY駟r-^9^:\sB]W^ܹsiܳgOΤ\f)>&>}p>M;ErѣSO=u ^~[oU T._ꫯ$tHߑ9l~_mܸwɓ'm6ۘ1cMW_g 2O>sΝ9sf޽w!%)Lm6ݞpqmݺUфu͞=[ղ)rW^fAtE 6L3O>9tД)S{ۢ-[l)Sx}3pС,rZfe>"̫j6~ѣGw=zPh|'e˖͜9f-^Xe֕3x o>//W^۷obRSS^|/X`С[lQ_z|AU9͔U999qaaɓ:kɷp&gIɼy~ߩS>T|{o޽SLygnaü6P 6r-۷PsGأ_lٚ5k l2ъ 6Xx6%%tΞ=[\fڵGik^xQl_(Jϋ]ftM7n\뮻DQ4i{!..N~"Θ1C~D[n=s0o3f(sb~n T>2Eꓧk1џ'Dtܹ{^zAKt]e\%K8ydv(J=z(CU_zx뤚ӾjΝM ^gR>4}tA?\>wrC*SWw^|R*ԫgy])SÜ}$&&n۶;EM#t3ke +*׫cǎq=f̘,ΖZ ;hAf͚5v/rŊ#܈_fNSuP֭{7{챁={v߾}*r䩥*(BUU;_kQF\rݺu^ƵRͲPS7"QE8P^7LRoXG}t͚5J PP9xRpgoz@AϤB}VCժl;EM#G>[V"JNNf/*׫Çggg nݺ{@e;ȝiݙgϞծ<*U'N4ф (,u8KJJzbxڦ9#;vl;Jv۷f͚_~YՆ)ի2S~(|BkÒŇޫWcǎ@?W~.,кWѣYYY,ZL*g5.]4dȐLW`2}sϵ~޽{A_pNڳ(׫œټbŊ޽{?ӱAՓDIV';Ǟ={|o޼yڴij2O< KKK2dm5jjjڽ{C=4|zQP+m_Ŧ&8r[<@YYoo=Jg;vɒ%כ|xSS F2 (5g)h}^b ӟ&M4}􂂂'Js>jҤI.$,U]]+&Bw$jQQZZƍΜ9rJOcsHׯZի,;PeT㋊xO{_ɿo~{챲2o>{l!2=㬯]v=S*y&;믿||gW}HݷTyyy<'$$HrxDjSBdR"rSNe۷/--M{7/Zc˖-ozxꤚ]Ea]z5gΜ;΢KCd3) TEq޼yW \f5Wk֬iӱW~-yʟ_z/ovQYYَk;L EZhQ"k*:Wy˥~ .rWF;~;+s#WX1a„G}jɒ%U鬫 k}޶m٥7Ι0xn'뮎M7ݴe?wY鬫  @We6x㍲_~9,#=Ӿ=J[^p4j~H Ñ#GvҥKNp²QF]vR,D;wIΝ[\\,3B/@H؀L4l6GիW{Md!4R\\\mm Mgۛ;w'Mq8Ez7o655UzNNΡC׮]ۻw^@; DIbbb<h!Cgܹ3gݻ;zfffn۶n.\pܸq[nhsQZmv#` ]СCEQ\x1{M&dbS6l`Z۔9{lvڵ6nbosssEQ7n^-Nɔj*Q_zH& a "{b p8>C6%''g˖-68swfee8pݱceggSgϞ|W_}5,Gp#XqBB{bŊ 6Z666v֬YӧOV%%%uLL<5p\X6mڕ+WjjjXs!BUVV^t)++=N'{vv_~b ZzfNW[[GpB(^"O>Yt=s߹w<{{'Fh„ DTTT=#UUU_õM ?nZ-Z4u%K444HwyسgO^^ރ>8͛7O6jjjڽ{C=4|zQip5C(򖞞. bcc~˗/766^x74ܴ7VWW7449sfʕ ??\>戚"t =:-1ѣ #(3@i:]KϜn* %5q͍~>Z`esޭ1QcTq˷c.魳ZP?yeD<z6I󮕺-?4ZSy.@7՜\y2u!nn~QB-@'i*MmyQ3GGν|~ӧO׿իW@t6ǠYFkK)rܹsڴi'O K:XgNܻgϞ={ ^v.H"3" IդwIENDB`offpunk-v3.0/tests/000077500000000000000000000000001514232770500143625ustar00rootroot00000000000000offpunk-v3.0/tests/geminiclient_test.py000066400000000000000000000025131514232770500204430ustar00rootroot00000000000000import offthemes from offpunk import GeminiClient def test_set_prompt(): gc = GeminiClient() prompt_value = "ON" prompt = gc.set_prompt(prompt_value) # Default prompt should be green 32 and go back to default 39 assert prompt == format_prompt("32", "39", prompt_value) # Prompt should still be green if nothing is set def test_set_prompt_without_themes(mocker): mocker.patch("offthemes.offpunk1", {}) mocker.patch("offthemes.colors", {}) gc = GeminiClient() prompt_value = "ON" prompt = gc.set_prompt(prompt_value) # Default prompt should be green 32 and go back to default 39 assert prompt == format_prompt("32", "39", prompt_value) # Prompt should still be green if nothing is set def test_set_prompt_without_themes(mocker): new_theme = offthemes.default.copy() new_theme["prompt_on"] = ["blue"] mocker.patch("offthemes.default", new_theme) gc = GeminiClient() prompt_value = "ON" prompt = gc.set_prompt(prompt_value) # Default prompt should be green 32 and go back to default 39 assert prompt == format_prompt("34", "39", prompt_value) def format_prompt(open_color: str, close_color: str, prompt_value: str) -> str: return ( "\001\x1b[%sm\002" % open_color + prompt_value + "\001\x1b[%sm\002" % close_color + "> " ) offpunk-v3.0/tutorial/000077500000000000000000000000001514232770500150635ustar00rootroot00000000000000offpunk-v3.0/tutorial/bookmarks.gmi000066400000000000000000000023271514232770500175550ustar00rootroot00000000000000# Simple bookmarking Bookmarking the current page is as simple as typing "add". You add the page to your bookmarks list. Try adding this page. > add Now, to see your bookmarks, type "bookmarks" (or "bm" as a shortcut). > bm As you can see, bookmarks are simply a page with links. You can have a direct access to a given link if you know its number. > bm 1 The easiest way to remove a bookmark is to visit it and then type "archive" > archive We will talk a bit more about archiving later but, for now, you can use it. Remember to visit the page to archive first. This might be conterintuitive when starting with offpunk but each command always apply to the current page. When you see your bookmarks, the page you are visiting is your bookmarks list. Alternatively, you can edit your bookmarks by hand with the "list edit" command. > list edit bookmarks Indeed, bookmarks is a simple gemtext file that you can modify by hand. You can even take note between links. The text editor should automatically be the default editor defined in your environment. As you can see, you need to explicitely give the name "bookmarks" when editing. Does it means that we can have other lists? You guessed! => /lists.gmi Multiple lists offpunk-v3.0/tutorial/contribute.gmi000066400000000000000000000140621514232770500177420ustar00rootroot00000000000000# Contributing to Offpunk If you are familiar with Python and Git, contributing to Offpunk is probably a lot easier than it appears. And if you are not a programmer, but still want to help, you can always help with offpunk's translation. Keep reading this page to get an idea of how to clone the repo, and how to send a patch. And then read the translation howto: => ./translation.gmi Learn how to help with translations ## Joining the project The project is currently hosted on Sourcehut. => https://sr.ht/~lioploum/offpunk/ Offpunk on Sourcehut All the technical discussions happen on the Offpunk-devel mailing list. => https://lists.sr.ht/~lioploum/offpunk-devel Offpunk-devel mailing-list archives It is also currently the best place to report a bug or ask a technical question. No need to create an account, simply send an email to the list. => mailto:~lioploum/offpunk-devel@lists.sr.ht Send an email to offpunk-devel IMPORTANT: when replying to a message on the list, be sure to use the "reply all". If you forget, only the original sender will receive your message. Also, emails should be sent in plaintext, not in HTML. => https://useplaintext.email/ Use plaintext email (useplaintext.email) If you plan to interact for more than one issue, its probably a good idea to subscribe to the list. Volume is relatively low and there’s no need to create any account. => mailto:~lioploum/offpunk-devel+subscribe@lists.sr.ht Subscribe to Offpunk-devel ## Making your first contribution The first step is, of course, to clone the git repository on your computer and check if you can run it locally without trouble. ``` git clone https://git.sr.ht/~lioploum/offpunk cd offpunk ./offpunk.py version ``` Now, you can simply explore the source code by yourself or by asking questions on the mailing-list. One good opportunity to learn is to improve this tutorial that you will find in the "tutorial" folder. You can also add your own Offpunk workflow. Once it’s done, commit your change locally. Don’t forget to give a good description to your commit when asked. ``` git add PATH_TO_MODIFIED_FILE git commit ``` Try to keep your modifications in one meaningful commit. If you change something after the commit, simply amend it. ``` git commit -a --amend ``` Once you are satisfied with your commit, it’s time to send it. Offpunk doesn’t use Github. There’s no Pull Requests and web interface. Instead, your commit will be sent as a patch by email. Don’t worry, it is easier than it looks. If it’s the first time you send a git patch by email, your computer may requires some configuration. Follow the git-send-email tutorial. => https://git-send-email.io/ Learn to use email with git! (git-send-email.io) If it is not sufficient, some informations could be found on the Linux kernel development documentation. In most cases, this is not needed. => https://docs.kernel.org/process/email-clients.html How to configure your mail client (kernel.org) Now, in your local offpunk folder, we will set offpunk-devel list as the default destination for patches. This should only be done once. ``` git config sendemail.to "~lioploum/offpunk-devel@lists.sr.ht" ``` Now, we are ready to send the latest commit as a patch. ``` git send-email HEAD^ ``` That’s it! Your first patch to Offpunk is sent! ## Improving the patch In most cases, the patch need to be discussed. This happens on the list itself, commenting the code by emails. You will probably receive suggestions on how to improve the patch. Modify your code accordingly then amend your commit. ``` git commit -a --amend ``` Once ready, send us the v2 of your patch by labelling it as such. ``` git send-email --annotate -v2 HEAD^ ``` The version 2 of your patch should be sent as a new email in its own thread. Don’t reply to the previous thread (which is what the Linux Kernel is recommending because they are way bigger than Offpunk). There can of course be a v3, v4, etc. What is important is to clearly communicate if you plan to work on a new version of the patch, if you consider that it should be merged as is or if you abandon the work on the subject for now. There’s no pressure. We all do this for fun. We simply need to communicate clearly. Also, mistakes happen. We all do mistakes. Your duty is inform the list as soon as you realize you did a mistake. We’ve all sent wrong emails or patches. That’s not a problem. But it may become a problem if people start working on reviewing code that was sent by mistake. ## Rebasing your patches into one If your work takes longer than expected, you will probably makes multiple commits and other commits may happen on TRUNK. We recomment that you work in a local branch. You must give it a name. ``` git branch my_super_feature git checkout my_super_feature_branch ``` You can easily switch between "master" (the official branck) and your own branch through "git checkout". When you are ready to submit, first make sure that "master" is up to date ``` git checkout master git pull ``` Then you will merge your branch into that clean master. We recommend using "git-squash" which will automatically combines everything into one single commit. "git-squash" is often not installed by default, it is part of the "git-extras" package, available on most distributions. It is of course possible to do more traditional "git-merge" or "git-rebase". ``` git squash my_super_feature_branch ``` By default, those changes are not committed. It’s time to write a very informative commit message because that’s the one that wille be used in your email ``` git commit git send-email HEAD^ ``` You may need to solve conflicts in order to conclude the rebase. You may find the following article helpful: => https://www.brethorsting.com/blog/2026/01/git-rebase-for-the-terrified/ Git Rebase for the Terrified (www.brethorsting.com) ## After your patch was accepted Once your patch is accepted, it will be both local and upstream. First do ``` git pull ``` You will be warned that your git branches have diverged. But, in reality, they diverged with the same patch on each side. You can simply resolve this with: ``` git rebase ``` offpunk-v3.0/tutorial/dev-guidelines.gmi000066400000000000000000000073651514232770500205000ustar00rootroot00000000000000# Offpunk’s Development Guidelines ## Minimalism * Offpunk is, first and foremost, a Gemini browser. Gemini shoult stay first-class. Gopher second, Web third. * Offpunk is intended firt and foremost for offline use. * Offpunk goal is not to browse the web but to extract useful informations from web pages. * Each page should always be presented the same. There will never be any CSS or design support in Offpunk. * Offpunk is a reading tool. It doesn’t run any app. There will never be any kind of JS support. If information cannot be extracted without JS on a given page, this page should be considered as broken and it is not Offpunk’s fault. * Offpunk is an open tool. It will only implement free and open protocols. No proprietary protocols. * Offpunk is a CLI tool. Every interaction is done by typing a visible command, not by using a shortcut or a mouse. ## Contributors Culture * Offpunk’s people welcome differences and are respectful of themselves and each others, including outside of the community platforms. * It’s OK to be wrong. It is OK to make a mistake. Be gentle on yourself. * A mistake should be acknowledged and corrected as quick as possible. * Nobody should be blamed for honest mistakes. * It’s OK to change your mind, to disappear for a while or to miss a deadline. * Contributing to Offpunk should be fun. If it becomes an obligation, stop it for a while. * Developers are assumed to work offline, development should be done asynchronously by email. * Each commit should probably comes with an CHANGELOG entry ## Accessibility * Accessibility is critical. Any changes that improve accessibility is high priority. * Any patch that impedes accessibility in any way should be refused. * Feedbacks from users with special-needs or disabilities are highly welcome. We consider those differences as richness. Improving Offpunk for those users improve it for everyone. * Accessibilty should not be confused with implementing every potential workflow. Offpunk is not an universal solution for everyone. Users must have the motivation to adapt to Offpunk’s philosophy and to learn to use it. ## Code structure * Offpunk should run directly as a Python script without any installation, environment settings or anything else. This is non-negotiable core value. python ./offpunk.py * Code should be in as few files as possible in one single folder. * Code should be split in separate files only if it makes sense to use some part of the code as an independent tool. * Everything is a file. The only database is user’s hard disk. No complex datastructure but plain text files. ## Crash and errors * Offpunk should never crash. That’s final. Any crash should be corrected. * Corrolary: Offpunk code should not throw exceptions but handle all possible cases. * Corrolary 2: Any call to an external library should catch any potential exception. ## Dependencies * Offpunk should depend on as few dependencies as possible. Any work toward removing a dependency is good work. * Dependencies should be optional. Code should run without that dependency. * For a depency to be considered, 4 conditions should be met: > 0. The dependency is 100% open and free software, as defined by the FSF. > 1. The dependency is well maintained with a stable and reliable API. There’s no need to track API changes accross versions. > 2. The dependency is popular and widely packaged in most popular Linux distribution (if it is not in Debian, it is probably not popular enough). It is not uncommon to have it already installed. > 3. The feature offered by the dependency cannot be implemented directly in Offpunk in less than 1000LOC. The point is to avoid small dependencies for relatively trivial features. > 4. The dependency doesn’t depend itself on dependencies breaking the previous 3 rules. offpunk-v3.0/tutorial/firststeps.gmi000066400000000000000000000054041514232770500177720ustar00rootroot00000000000000# First steps with Offpunk Welcome to Offpunk. In Offpunk, there’s no way to "clic" or to open tabs. Everything is done by typing commands at the bottom of your screen, next to a green "ON>" prompt. It says "ON" to tell you that Offpunk is currently in "Online" mode. ## Prompt and scrolling in Less If a page is too long to fit the screen, it will be displayed using Less. If you don’t see the "ON>" prompt, it means that you are in Less. In Less, you can scroll with arrows. You can also scroll to the whole next page using the space key. To leave Less and go back to Offpunk, simply quit by pressing "q" (without enter). Trust us, it will quickly become a second-nature to quit with "q". ## Following links Below, you will find a link. But you can’t click on it. That’s why links all have a number next to them. In order to follow a link, you simply need to type this number and hit "enter". If you don’t see the green "ON>" prompt, you are in Less and you must press "q" first. => /myfirstlink.gmi This is a link! Follow it by typing its number now. > 1 ## Commands and help with "help" After you followed the link, you typed "back". Note that some commands have shortcut. Instead of typing "back", you can type "b". As you may have guessed, there’s also a "forward" command (shortcut "f"). You may try a few "back"/"forward" (but don’t forget to hit "q" if you are in Less). > forward > back The list of commands is available with "help". The list of shortcut/abbreviations is available with "abbrevs". You can also use "help" with another command name to learn more about it. For example "help back" will tells you what it is. This is specially useful for more advanced commands. > help back Now, if you type "help", you may not see this page anymore. In order to see the current page, you can always type "view" (or its shortcut "v"). Type "help" then "view" (each followed by enter). > help > view As you may be in Less, you may have to hit "q" to get back to the prompt. ## Exploring the cyberspace with "go" Now that you can follow links and do back/forward, you only need to learn one more command: "go". "go" should always be followed by an URL. So "go gemini://offpunk.net" or "go https://offpunk.net" will brings you to the page you want to explore. > go gemini://offpunk.net/firststeps.gmi While "go/back/forward" are enough to get started, there is obviously a lot more in Offpunk. Let’s give you your first tip, as an appetizer: if you type "go" without any URL, Offpunk will automatically try to find one in your system clipboard. It means that you can simply copy an URL from a document or another browser and hit "go" in Offpunk to access it. But interesting stuff starts with the tour. => /tour.gmi Follow this link to learn advanced browsing with "tour" offpunk-v3.0/tutorial/frozen.gmi000066400000000000000000000022731514232770500170700ustar00rootroot00000000000000# Frozen lists As we have seen when learning about subscriptions, pages in your lists are regularly updated to have the latest content. => /subscriptions.gmi Back to list subscriptions in case you missed it So we currently have two kind of list: normal lists, which are updated, and subscribed lists which are updated and for which every new element is added to the tour. Sometimes, we don’t want to update pages in a list. For those case, we can "freeze" a list. > list create tokeep > list freeze tokeep (as always, list names are available through autocompletion) That’s it. The newly created list is now frozen. If you edit the list, you will see a "#frozen" next to its name. If you remove it, the list will not be frozen anymore. You can, of course, go back to normal through the command line. > list normal tokeep There’s one important point to keep in mind when using frozen list: pages in that list will not be updated except if they also happen to be in a page which is updated. Having a page in a frozen list is thus not a guarantee to freeze all its content. The content might be in another list. It might be refreshed manually with "reload". => /index.gmi Back to the turorial offpunk-v3.0/tutorial/gemini.gmi000066400000000000000000000022531514232770500170330ustar00rootroot00000000000000# The Gemini network You probably know the Web : a website has an url starting with "https://" and is mostly done with pages written in HTML. Your web browser translate the HTML into something you can read, with pictures and features coded using Javascript. Gemini is an alternative network. Instead of website, you have "gemini capsule". Their url starts with "gemini://" and, instead of HTML, the content is written using Gemtext, which is way simpler. This makes Gemini capsules look like simple texts that you can read without all the distraction seen on the web. You can start reading what’s happening on Gemini using Antenna: => gemini://warmedal.se/~antenna/ Antenna or Cosmos => gemini://skyjake.fi/~Cosmos/ Cosmos Once in one of those aggregator, simply add the links that seem interesting to your tour. You can also browse the capsule of Offpunk’s contributors: => gemini://ploum.net/index_en.gmi Ploum.net, the Gemini capsule of Ploum Offpunk is a Gemini browser first. But, sometimes, you need to browse the Web. As the web is more complex, you will sometimes need to use the "view" feature to better read the content. => /view.gmi Different views of the same page offpunk-v3.0/tutorial/help.gmi000066400000000000000000000045511514232770500165160ustar00rootroot00000000000000# Getting help about Offpunk ## In-app help The first thing to remember is that Offpunk has an "help" command. > help It will list the available commands and you can be more specific with "help CMD", for example: > help copy > help go ## Get human help As it may be intimidating, you can get help from a fellow human by sending an email to the user list. => mailto:~lioploum/offpunk-users@lists.sr.ht Send an email the offpunk-users list This can be done from within Offpunk by asking for help twice! > help help Try to write a clear subject, present yourself and your usecase. The list is very open to every kind of discussions. IMPORTANT: when replying to a message on the list, be sure to use the "reply all". If you forget, only the original sender will receive your message. Also, emails should be sent in plaintext, not in HTML. => https://useplaintext.email/ Use plaintext email (useplaintext.email) But don’t worry: we all make mistakes, it is part of the process. Offpunk’s community is small and warmly welcome anyone. The fact that you are interested in using Offpunk is all we need from you. ## Join the community The best way to become part of the community is to join the offpunk-users list permanently. This means you will receive new messages from others. No need to create an account. Simply send an email. => mailto:~lioploum/offpunk-users+subscribe@lists.sr.ht Send an email to subscribe to offpunk-users The volume is fairly low, this should not strain your inbox. Also, remember that you don’t need to read or understand everything. It is fine to just lurk. One day, you may be able to reply to an help request or have a great idea to a problem. But there’s no badge, no statistics. You being there is all that count for us. => https://lists.sr.ht/~lioploum/offpunk-users Offpunk-users mailing-list archives There is also a Matrix room to discuss Offpunk. But as Offpunk users like being offline, it may not be the most active room in history. => https://matrix.to/#/#offpunk:matrix.org Matrix discussion room * TODO: should we create an IRC channel on liberachat? ## Contribute to Offpunk Do you think you’ve found a bug? Do you want to discuss an idea to improve Offpunk? Or do you want to simply fix a mistake in this tutorial? Well, it’s time to contribute! Don’t worry, it is easier than it sounds. => /contribute.gmi Contribute to Offpunk offpunk-v3.0/tutorial/index.gmi000066400000000000000000000027331514232770500166750ustar00rootroot00000000000000# Offpunk, an offline-first command-line browser Offpunk allows you to browse the Web, Gemini, Gopher and subscribe to RSS feeds without leaving your terminal and while being offline. => /screenshots/1.png Screenshot of Offpunk => /whatisoffpunk.gmi What is Offpunk? => https://geminiprotocol.net/ What is Gemini? => /install.gmi Installing Offpunk ## Browse online with Offpunk => /firststeps.gmi First steps in Offpunk => /tour.gmi Efficient browsing with "tour" => /gemini.gmi Start exploring Gemini => /view.gmi Different views of the same page => /open.gmi Open outside of Offpunk ## Work offline => /offline.gmi Working offline and basic sync => /sync.gmi Syncing Offpunk from the command-line ## Organize your bookmarks with Offpunk lists => /bookmarks.gmi Simple bookmarking => /lists.gmi Multiple lists and archives => /subscriptions.gmi Managing RSS/blog/gemlog subscriptions => /frozen.gmi Freezing lists to prevent update of content ## Join the Offpunk Community => /help.gmi Getting help about Offpunk and joining the community Here are some contributed users workflows that could inspire you => /workflow_ploum.gmi Ploum’s workflow => /workflow_jmcs.gmi JMCS’s workflow ## Developing Offpunk => /contribute.gmi How to contribute to Offpunk => /dev-guidelines.gmi Offpunk’s development guidelines => /tasks_pending.gmi Proposal for good first contributions Offpunk contains three tools that you can use independantly in your project. - netcache - ansicat - openk offpunk-v3.0/tutorial/install.gmi000066400000000000000000000021601514232770500172260ustar00rootroot00000000000000# Installing Offpunk => /screenshots/decvt220.jpg Offpunk running on a DECVT220 (picture by Dylan D’Silva) ## Packages Offpunk is already packaged for many distributions. Installing it is probably as simple as "apt install offpunk" or "pacman -S offpunk". => https://repology.org/project/offpunk/versions Offpunk packages listed by Repology Offpunk has optional dependencies. To check if those are installed, use "version" > version ## Helping with packaging We welcome and encourage all efforts to make Offpunk availalbe on new distributions. If you want to give it a try, say hello to the dedicated mailing-list. => https://lists.sr.ht/~lioploum/offpunk-packagers Offpunk-packagers mailing-list ## Git Advanced users and developers may use the latest Git version. ``` git clone https://git.sr.ht/~lioploum/offpunk ``` Once cloned, no build is required. You can launch offpunk.py/openk.py/ansicat.py/netcache.py directly. > python offpunk.py Anyone using the git version should feel comfortable to report any bug on the devel mailing-list. => https://lists.sr.ht/~lioploum/offpunk-devel Offpunk-devel mailing-list offpunk-v3.0/tutorial/lists.gmi000066400000000000000000000037221514232770500167230ustar00rootroot00000000000000# Multiple lists You discovered that your bookmarks list is a simple text file with links. But why not have multiple files so you can handle those differently? That’s the purpose of "list". Let’s see the list we already have: > list We see the bookmarks list and, surprise, we have "system lists". Let’s discuss those later and focus on normal lists first. You can create a list with "list create NAME". Let’s try to make a list with all the link you would like to read later. We will call it "toread" but anything will do as long as it doesn’t exist yet. > list create toread Adding a link to the "toread" list is only a matter of giving the name to "add". > add toread Pro tip: there’s autocompletion on the name of your lists when adding. If a list has a long name, simply press the tab key after the first letters. To display your newly created list, use "list" > list toread Once again, you can use autocomplete. To remove a link from toread, you need to archive it. Just like in bookmarks. But remember that Offpunk has no other context than the current page. If you archive a page, it will be removed from every list (except history and archives) Try the following: > add > add toread > archive If you want to move the current page from a list to another, use "move". For example, we will put the current page in the bookmarks then move it into the "toread" list. > add > move toread You probably guessed that, to edit the list, you can simply: > list edit toread We start to use the "archive" command a lot. But what happens to archived links? Well, they are simply put in a list called "archives". The archives list is special as it contains the last 200 archived URL. You can see it with: > list archives List is probably one of the most powerful command in Offpunk. You can get a taste with: > help list Let’s now explore how you could manage your RSS/blog/gemlogs subscriptions through lists. => /subscriptions.gmi Managing subscriptions with lists offpunk-v3.0/tutorial/make_website.py000066400000000000000000000141361514232770500201010ustar00rootroot00000000000000#!/bin/python import html import os import unicodedata from datetime import datetime baseurl = "offpunk.net" htmldir="../../public_html/" html_page_template = "page_template.html" today = datetime.today().strftime("%Y-%m-%d") #Convert gmi to html # Also convert locals links that ends .gmi to .html def gmi2html(raw,signature=None,relative_links=True,local=False): lines = raw.split("\n") inquote = False inpre = False inul = False inpara = False def sanitize(line): line = unicodedata.normalize('NFC', line) return html.escape(line) content = "" title = "" h2_nbr = 1 for line in lines: if inul and not line.startswith("=>") and not line.startswith("* "): content += "\n" inul = False if inquote and not line.startswith(">"): content += "\n" inquote = False if line.startswith("```"): if inpara: content += "

\n" inpara = False if inpre: content += "
\n" else: content += "
"
            inpre = not inpre
        elif inpre:
            content += sanitize(line) + "\n"
        elif line.startswith("* "):
            if not inul:
                if inpara:
                    content += "

\n" inpara = False content +="
    " inul = True content += "
  • %s
  • \n" %sanitize(line[2:]) elif line.startswith(">"): if not inquote: if inpara: content += "

    \n" inpara = False content += "
    " inquote = True content += sanitize(line[1:]) + "
    " elif line.startswith("##"): if inpara: content += "

    \n" inpara = False content += "

    "%str(h2_nbr) h2_nbr += 1 content += sanitize(line.lstrip("# ")) content += "

    \n" elif line.startswith("# "): #We don’t add directly the first title as it is used in the template if inpara: content += "

    \n" inpara = False if not title: title = sanitize(line[2:]) else: content += "

    " content += sanitize(line[2:]) content += "

    \n" elif line.startswith("=>"): if inpara: content += "

    \n" inpara = False splitted = line[2:].strip().split(maxsplit=1) link = splitted[0] link.removeprefix("https://"+baseurl) link.removeprefix("gemini://"+baseurl) #removing the server part if local #converting local links to html (if gmi) if "://" not in link and link.endswith(".gmi"): link = link[:-4] + ".html" if not relative_links and "://" not in link: link = "https://" + base_url + "/" + link.lstrip("./") elif local: link = local_url + link.lstrip("./") if len(splitted) == 1: description = "" name = link else: name = sanitize(splitted[1]) description = name # Displaying picture if link ends with a picture extension. #Except for commons.wikimedia.org if (link[-4:] in [".jpg",".png",".gif"] or link[-5:] in [".jpeg",".webp"]) and\ not link.startswith("https://commons.wikimedia.org"): if inul: content += "
\n" inul = False #content += "
" if description: content += "
%s
\n"%description content += "\n" else: if not inul: content += "
    \n" inul = True content += "
  • %s
  • "%(link,name) content += "\n" elif line.strip() : if not inpara: content += "

    " inpara = True content += "%s
    \n"%sanitize(line) elif inpara: if content[-5:] == "
    \n": content = content[:-5] content += "

    \n" inpara = False if inul: content += "
\n" inul = False if signature: content += "\n
" + signature + "
" return title, content if __name__=="__main__": files = os.listdir() for f in files: if f.endswith(".gmi"): content = "" #Extracting gmi content from the file with open(f) as fi: content = fi.read() fi.close() #converting content to html title, html_content = gmi2html(content) gemlink = "gemini://" + baseurl + "/" + f f_html = f[:-4] + ".html" httplink = "https://" + baseurl + "/" + f_html image_preview = "screenshots/1.png" #writing html into its template with open(html_page_template) as f: template = f.read() f.close() final_page = template.replace("$CONTENT",html_content).\ replace("$TITLE",title).\ replace("$GEMLINK",gemlink).\ replace("$HTMLLINK",httplink).\ replace("$PUBLISHED_DATE",today).\ replace("$IMAGE_PREVIEW",image_preview) path = htmldir + f_html with open(path, mode="w") as f: f.write(final_page) f.close() offpunk-v3.0/tutorial/myfirstlink.gmi000066400000000000000000000004661514232770500201420ustar00rootroot00000000000000# First link Congratulations! You followed your first link in Offpunk. Now, you can type "back" followed by enter to come back to the previous page. Type "back" then enter. > back Alternatively, you can follow the following link by typing its number: => /firststeps.gmi Back to First steps with Offpunk offpunk-v3.0/tutorial/offline.gmi000066400000000000000000000030101514232770500171750ustar00rootroot00000000000000# Working offline By default, Offpunk is working online as indicated by the green "ON" on your prompt. If you want to disconnect Offpunk, simply type: > offline You are now offline and offpunk will only allows you to view content that has already been cached. The time and date at which the content you visit was cached is written in red, next its title. If you would like to see a fresh version, type: > reload Of course, this is not done immediately as you are offline. But, trust me, you will soon have the newest version of that page. Let’s try to visit this old blog post about releasing Offpunk 2.0. It should not be in your cache. If you can see the content, try to follow links until you don’t have a cached version. => https://ploum.net/2023-11-25-offpunk2.html Announcing Offpunk 2.0 When encountering a link that has never been seen before, offpunk will save it in a list called "to_fetch". Now, let’s come back online > online We will now ask offpunk to synchronize in order to fetch everything you wanted to see but couldn’t. > sync During a sync, offpunk does many things but, firstly, it will fetch everything in your "to_fetch" list and put it in your tour. You can now go back offline. > offline And browse what you wanted to browse previously. > tour or, shorter: > t WARNING: Offpunk has currently no automatic online detection. If in online mode, it will attempts to connect, even if the network is down. If in offline mode, it will never attempt to connect. => /index.gmi Back to tutorial offpunk-v3.0/tutorial/open.gmi000066400000000000000000000011101514232770500165130ustar00rootroot00000000000000# Open outside of Offpunk Sometimes, Offpunk is not enough and you really want to open current file outside of your terminal. Simply try: > open "open" will open the local cached version of the current file using an external handler. By default, xdg-open will be used. If you are connected and want to open the current website in your main browser, use: > open url Open also works with links in the current page, either with the url or not. You can do: > open 2 4 or > open url 2 4 To externally open links 2 and 4 of the current page. => /index.gmi Back to the turorial offpunk-v3.0/tutorial/page_template.html000066400000000000000000000033661514232770500205700ustar00rootroot00000000000000 $TITLE

$TITLE

$CONTENT

Permalinks:
$HTMLLINK
$GEMLINK

offpunk-v3.0/tutorial/subscriptions.gmi000066400000000000000000000054011514232770500204700ustar00rootroot00000000000000# Managing subscriptions Each time you "sync" Offpunk, it will goes through all your lists to refresh the links in your lists. This ensures that the content in your bookmarks is always uptodate. But what if you want more and be notified for every new link appearing in your bookmarks? You can do that by "subscribing" to a given list. Let say you have created a list called "rss" in which you put RSS feeds of blogs you want to follow. Simply type: > list subscribe rss Starting from now one, each time you "sync" offpunk, the following will happen: 1. Each link in the rss list will be refreshed 2. If the refreshed page contains a new link, this link will be added to your tour. Offpunk doesn’t really distinguish between "rss", "gemtext", "atom" or "html". You can subscribe to any page as long as it contains link. Each time a new link is added, it will ends in your tour. WARNING: Offpunk considers a link as "new" if it doesn’t exist yet in its cache. This means that, when you add a new RSS feed, every single post will be added to the tour at the next "sync". This might be what you want but, in some case, this might be too much. A quick solution is to edit your tour manually with "list edit tour" (remember, tour is just another list). Of course, you may want to unsubscribe a list by resetting it to "normal". > list normal rss What the command is doing is simply adding a "#subscribed" tag next to your list title. Another way to mark a list as a subscription is thus to add "#subscribed" next to its title. There’s also a "subscribe" command in Offpunk. When you use the "subscribe" command on a page, it offers you the different RSS feeds of that page. It then puts the selected number in a list called "subscribed" which is, by default, … subscribed. Try it. You will follow the next link to ploum.net the type the following: => https://ploum.net Go to Ploum.net > subscribe > 3 > back You have now successfully added the english-only RSS feed of my personal blog to your "subscribed" list. It is exactly equivalent to doing: > go https://ploum.net > feed > 3 > add subscribed It takes a bit of time to realize that you can subscribe to anything and that RSS are not something magic but simple page written in a different format. In fact, RSS support is so good in Offpunk that you may acquire to reflex of browsing through a blog directly with the RSS feed instead of the homepage. When browsing a feed, the "view full" command allows you to see the content of the articles in a RSS, not only the title. Let’s try it: > go https://ploum.net > feed > 3 > view full > view normal While subsciptions are nice, there are time where we don’t want to update the content of our lists. => /frozen.gmi Let’s learn to freeze lists. => /index.gmi Back to the turorial offpunk-v3.0/tutorial/sync.gmi000066400000000000000000000025311514232770500165360ustar00rootroot00000000000000# Synchronise Offpunk with the external world When launched with the "--sync" option, offpunk will run non-interactively and fetch content from your bookmarks, lists and ressources tentatively accessed while offline. New content found in your subscriptions (see `help subscribe`) will be automatically added to your tour (use `tour ls` to see your current tour, `tour` without argument to access the next item and `tour X` where X is a link number to add the content of a link to your tour). With "--sync", one could specify a "--cache validity" in seconds. This option will not refresh content if a cache exists and is less than the specified amount of seconds old. For example, running > offpunk --sync --cache-validity 43200 will refresh your bookmarks if those are at least 12h old. If cache-validity is not set or set to 0, any cache is considered good and only content never cached before will be fetched. `--assume-yes` will automatically accept SSL certificates with errors instead of refusing them. Sync can be applied to only a subset of list. > offpunk --sync bookmarks tour to_fetch --cache-validity 3600 Offpunk can also be configured as a browser by other tool. If you want to use offpunk directly with a given URL, simply type: > offpunk URL To have offpunk fetch the URL at next sync and close immediately, run: > offpunk --fetch-later URL offpunk-v3.0/tutorial/tasks_pending.gmi000066400000000000000000000036421514232770500204170ustar00rootroot00000000000000# Good first contributions If you want to contribute to offpunk, here are a list of tasks which can probably be done with a little work but do not require to touch the whole codebase. If you are interested, please announce yourself on the offpunk-devel mailing list. ## Cache Triming (Good first-contribution - medium level) We need work on a model that keep the cache within certain limit by removing older, inaccessed elements which are not linked to anything in lists. This is a good first-contribution issue as the cache-triming script should be completely independant and interact little with existing code. You need to like playing with file systems properties. ## HTML rendering (Good first-contribution - advanced level) Better rendering of tables using ASCII tables. This is a good first-contribution for someone interested in HTML parsing/rendering and ready to dig into ansicat. While quite complex, the issue will have little or no interaction with code outside of the HTML rendering engine of Ansicat. You need to like handling HTML/Ascii art/ANSI codes ## UTF-8 in URL: See info on the bugtracker: => https://todo.sr.ht/~lioploum/offpunk/42 bug #42: IDN handling # Heavy refactoring Those tasks are hard and requires touching the code everywhere. ## Refactoring of redirections is needed bug #34: redirections are not kept while offline https://todo.sr.ht/~lioploum/offpunk/34 ## Refactoring of errors is needed bug #30: Netcache: gemini status 4 and 5 should not emit a full traceback https://todo.sr.ht/~lioploum/offpunk/30 bug #3: Error pages are stored in the cache https://todo.sr.ht/~lioploum/offpunk/3 ## Swicth from python-requests to curl Current netcache code is really slow using python-requests. It could be probably optimised greatly by using concurrent http requests and, maybe, switching to libcurl instead of requests (which would also give accesss to all protocols supported by libcurl) offpunk-v3.0/tutorial/tour.gmi000066400000000000000000000014541514232770500165560ustar00rootroot00000000000000# Advanced browsing with "tour" Unlike most browsers you may know, there are no tabs in Offpunk. Instead, Offpunk has the concept of "tour". (shortcut "t"). Technically, the tour is a FIFO list of all the pages you want to visit. Let’s imagine that you are on a page with multiple links that look interesting. => tour1.gmi First link => tour2.gmi Second link => tour3.gmi Third link To add link number 1 to your tour, you can simply write "tour 1". Or, shorter, "t 1". You can also add multiple links: "t 1 2 3". Or add a range: "t 1-3". Try to add a range with "t 1-3" now. > t 1-3 It looks like nothing happened but the links were added to your tour. If you use "tour" or "t" without any number, the next link in you tour will be displayed. Type "t" now. > t => /index.gmi Back to the tutorial offpunk-v3.0/tutorial/tour1.gmi000066400000000000000000000003221514232770500166300ustar00rootroot00000000000000# First link in your tour Congratulations, this was the first link in your tour. Type "t" to get the next link for your tour. > t If you are at the end of tour, you can => /index.gmi return to the tutorial offpunk-v3.0/tutorial/tour2.gmi000066400000000000000000000003021514232770500166270ustar00rootroot00000000000000# Second link in your tour This link is not really interesting. Type "t" to get the next link in your tour. > t If you are at the end of tour, you can => /index.gmi return to the tutorial offpunk-v3.0/tutorial/tour3.gmi000066400000000000000000000011261514232770500166350ustar00rootroot00000000000000# Third link to demonstrate the tour Yay, yet another link from your tour. Funny, isn’t it? Type "t" to get the next link in your tour. If there’s any. Your tour might be empty now. > t By now, you probably understand how to use the tour. You will see, it will quickly becomes second nature to add links to your tour just like you open links in tabs in your graphical browser. You may now go back to the tutorial homepage by typing the number of the related link. => /index.gmi Tutorial homepage Or you may want to explore a bit the Gemini network => /gemini.gmi Start exploring Gemini offpunk-v3.0/tutorial/translation.gmi000066400000000000000000000175011514232770500201230ustar00rootroot00000000000000 # Brief how to to translate and keep offpunk translated and up to date This is a very brief document, showing merely the commands one would need to keep offpunk translatable, and some notes for potential translators on how to add a new language to the available ones As a pre-requirement, make sure you have gettext installed in your system. We recommend you also install poedit, a user-friendly and easy to use editor for po files: ``` # apt install gettext poedit ``` (instructions for other systems are welcome) ## Quick TL;DR If you are interested in the details and reasons for all the commands, keep reading. Here's a quick summary of the steps you need to contribute a translation for offpunk. Have in mind that, since offpunk uses python docstrings to provide user help, we need to do a couple extra steps compared to a typical application using gettext. Mostly, we need to extract all the docstrings we are going to show the user, and write them to a temporary file so the "xgettext" command can find them. In order to make it easier for translators, there's a script in offpunk's repository ("po/create_pot.sh") that would scan the source files and create a "po template" file automatically. Just run these commands: ``` cd cd po/ ./create_pot.sh # this will generate a "messages.pot" file msginit -i messages.pot -o XX.po # XX should be your language code #it will ask you details like your email poedit XX.po ``` poedit will create a XX.mo file that you can test in your system (see below for details) If everything is correct, you can contribute the XX.po file. We'll be happy to accept it! Keep reading now for details :) ## Creating and updating the "po template" (pot file) In the gettext system, all translations start with this file. It's by default called messages.pot To create it, this is the command used (from the root folder of the offpunk source code): ``` $ xgettext --add-comments=TRANSLATORS *py -o po/messages.pot ``` but, because we use docstrings for offpunk's internal help system, there's a previous step: we have a small python script (extract_docstrings.py) to we use to extract all the necessary extra messages and write them to a temporary file. This is all done automatically for you if you use the "create_pot.sh" script. We encourage you to look at those scripts if you are interested. xgettext will "extract" all the translatable strings from all the python files (*py), and use po/messages.pot as the output file (-o) The "--add-comments=TRANSLATORS" part of the command tells xgettext to copy the comments that the devs left for translators. These comments will give valuable tips to translators. See the next page for more detail: => https://www.gnu.org/software/gettext/manual/html_node/Translator-advice.html Advice for translators (gnu.org) in the future, if new "translatable" strings are added (or the strings are modified), this same command can be run again. In fact, if you are going to work in a translation at a given time, generating a fresh messages.pot is always a good idea. Remember you can do this simply by running: ``` ./create_pot.sh ``` from the po/ folder. ## Creating a translation for a new language If you are an offpunk user and want to translate it into your language, you can do it with these steps: first, make sure you have your system configured to use the right locale: ``` $ locale ``` Then, follow the steps above to create the "po template" file (messages.pot) Ideally, your system would be configured to use your native language, and ideally that's the "target" language you'll translate offpunk into (but this is not strictly necessary) . Enter the po/ folder: ``` $ cd po ``` and then run this command to create a po file from the 'po template' file: ``` $ msginit -i messages.pot -o XX.po ``` XX should be the language code of the language you'll translate offpunk into. Examples are fr_FR, fr_CA, es_ES, es_AR, and others If your system does not currently use that same language (locale), you can specify the lang running instead: ``` $ msginit -l LANG_CODE -i messages.pot -o XX.po ``` (you might want to check 'man msginit') ## Translating the messages Po files are technically text files and can be edited with your favorite editor. However, if you are a new translator, I recommend using poedit. ``` $ poedit XX.po ``` XX.po is the file created before Then, you can click on the different messages, and input an appropriate translation under them. When saving, poedit will create a XX.mo file (this is the binary format that your computer will actually use to show offpunk in your language. It also has a menu option to do that. If you were interested, you can manually create this .mo file by: ``` $ msgfmt XX.po -o XX.mo ``` ## Testing your translation After you have translated the whole file (or even some of the strings), and you have a XX.mo file, you can test it by: ``` # cp XX.mo /usr/share/locale/XX/LC_MESSAGES/offpunk.mo ``` (these are the paths in a debian system. Not sure how universal this is, but right now it's more-or-less hardcoded in the .py files) keep in mind 'XX' in that path will match the output you see when you run "locale" in your terminal then you can start offpunk.py from the source code and check if any of the strings have to be changed NOTE: if you current LOCALE doesn't match the one you are translating into, you can test the language anyway, tweaking the environment a bit, only for offpunk. For example: your system is in spanish, but you also speak german, and are now translating offpunk to german. You could test the german translation by: ``` # cp de.mo /usr/share/locale/de/LC_MESSAGES/offpunk.mo #this should require "sudo", or be run as root $ locale LANG=es_ES.UTF-8 [...] $ LANG=de ./offpunk.py ``` ## Keeping your translation up-to-date Every now and then, new messages will make their way into offpunk. New features are added, some messages change... In those cases, you can incorporate the new messages that appear in messages.pot (that you can generate with the 'create_pot.sh script) to your language's po file by running: ``` $ msgmerge -U XX.po messages.pot ``` But, if you don't want to have to remember these commands, poedit also has a menu entry that would let you, while you are translating your po file, "Update from POT file". You can find that menu entry under the "Translation" menu. Then, navigate and choose the updated messages.pot file and you are done, new untranslated strings will apear in the poedit interface for you to translate. Translate, compile the .mo file, test your translation, and you're good! If you get into translating free software into your language, you can explore poedit's capabilities ("pre-translate" from "translation memory" will soon prove its usefulness), and other translation tools and maybe decide you like some other tool better than poedit. Poedit has been used as an example in this guide because it is powerful enough and easy enough to use that we can only recommend it as the perfect starting point. ## A note to devs Ideally, all strings that are shown to users should be translatable, so offpunk users can benefit from it and use the program in their native language. Making the messages translatable is not too difficult. As a general rule, if a message is to be shown, like: ``` print("Welcome to my program") ``` it would be enough to surround the actual string with the "_()" function, like this: ``` print(_("Welcome to my program")) ``` You can also add comments for the future translators that could help them understand tricky messages. Translation software often will show these hints while the translators are working on the messages. ``` #TRANSLATORS: this is a verb. Like in "open the window", not "the window is open" print(_("Open")) ``` Take a look at this link if you are interested in the topic: => https://www.gnu.org/software/gettext/manual/html_node/Translator-advice.html Translator advice offpunk-v3.0/tutorial/view.gmi000066400000000000000000000020531514232770500165330ustar00rootroot00000000000000# Different views of the same page When you want to know where you are, simply type: > url Url returns the current url. It can also easily be shared with your system clipoard thanks to the "copy url" command: > copy url See "help copy" for other use of copy. If "url" is not enough, you can get plenty of information about the current page with "info". > info When you are in a gemini page, the page is straightforward. But, for HTML pages, Offpunk will try to extract the important informations and remove useless cruft. This doesn’t always work. If you want to see the full page, use "view full" (or "v full"). Go back with "v normal". View also allows you to see if there are any embedded RSS feeds with "feed". Feed is not really a view as it opens a new page. Try it now by going to https://ploum.net then trying views. Come back here with "back". > go https://ploum.net > v full > v normal > feed > back > back View also allows you to preview a given link. Try "v 1". > v 1 => /open.gmi Open outside of Offpunk => /index.gmi Back to the turorial offpunk-v3.0/tutorial/whatisoffpunk.gmi000066400000000000000000000026301514232770500204520ustar00rootroot00000000000000# What is Offpunk? => /screenshots/2.png Screenshot of Offpunk Offpunk is a command-line offline-first web browser for your terminal. But what does it mean? ## Command-line No mouse, no shortcut, no hidden key to press. Every action requires you to type a command. Content is displayed in the venerable "less" pager. Offpunk is intented for people who live in their terminal and don’t want to leave it. ## Offline-first Every content you visit is cached and can be visited later while offline. If you try to visit a content not available in your cache, it will be marked to be downloaded later. Offpunk allows you to synchronise you computer once every hour, day or week and work offline without being interrupted. ## Web Browser Yes, it browses the web. But not only. Offpunk transparently browse http/https/gemini/gopher/spartan/finger links. In your terminal, it will nicely display HTML, Gemtext, Gophermap, txt, RSS, Atom and even pictures. You can subscribe to an RSS feed or to any page. Offpunk merges the concept of browsing pages and subscribing to feeds. ## Unix philosophy Offpunk is made of 4 components that can be used separatly: netcache, ansicat, openk and offpunk. While being written in python, dependencies are kept minimal and, when possible, optional. => /screenshots/resist.png RESIST! => /install.gmi Install Offpunk => /firststeps.gmi Start to use Offpunk => /index.gmi Back to the tutorial offpunk-v3.0/tutorial/workflow_jmcs.gmi000066400000000000000000000131021514232770500204440ustar00rootroot00000000000000# How I'm using Offpunk so far (by jmcs) Altough offpunk's great strength seems to be the cache mechanism that allows one to synchronize content and then read offline (using its tour feature), pretty much all of my offpunk usage has been 'online'. Here's what it looks like when I use offpunk: ## Most common workflow Most of the time, I use offpunk via ssh. I connect to my old laptop (that we have on the coffe table to watch videos most of the time), and reconnect to a screen session I have running there. Usually, offpunk is already open in one of the windows. If not, I create a new 'window', and start offpunk: > offpunk Most of the time, it seems I open it without a URL. Then, because I'm still a quite new user, usually take a look at my "lists" usually looking for the bookmarks list (you can type 'bm' to go directly, but for some reason I still don't have it in my "muscle memory" :) ): > ON> list > (6 items) (local file) > > Bookmarks Lists (updated during sync) > [1] bookmarks (10 items) > [2] check_later (5 items) > > Subscriptions (new links in those are added to tour) > [3] subscribed (0 items) > > System Lists > [4] archives (4 items) > [5] history (201 items) > [6] tour (1 items) As you can see, bookmarks is my first list, and I already have saved some pages saved there. You see that I also have a list I created, called "check_later", we'll talk about it later, but the name should be self-explanatory. I press 1: > ON> 1 This will show me my list of bookmarks. A couple of them are gemini aggregators (antenna, Cosmos), another two are the gemini "forums" BBS and Station. I press whatever number I would like to read: > ON> 2 Usually, this is some sort of aggregator, which offpunk shows me as a list of links. Usually at this point, I scan for titles that look interesting, and add them to my tour: > ON> t 2 5 7-11 and then I start reading: > ON> t for each of the pages I read, it may happen that I want to add some of the links in it to my current tour: > ON> t [number of the links] then I go back to read with v, or t to continue with the next page in my tour. or maybe, I want to see what else its author has published. For many pages, I can go check the root of the site: > ON> root But sometimes this doesn't work very well: some people have their gemini capsules (or http sites) in a shared tilde, and their "root" url would take me to the shared tilde root page. That's why every now and then I check what the url is: > ON> url sometimes to go to the "root" of this capsule I only have to navigate one or two levels up: > ON> up but sometimes, selecting the "base url" I saw earlier with my mouse and going directly is faster: > ON> go [middle click, or shift+insert, to paste url] At this point, I might want to add this capsule (or page) to my bookmarks, or to my "check_later" list: > ON> add > [PAGE] added to bookmarks > ON> add check_later > [PAGE] added to check_later Then I usually go back to the page I was reading, with "v" (short for 'view', meaning view current page), or, if I had already finished reading it, continue with my tour: > ON> t I usually like to read my tour in one go, so I try to not add too many links to it. If I don't have too much time, I'll rather add links to my "check_later" list. This feature has been introduced recently to offpunk. I'll have to test it, but it will look like this while reading a page: > ON> add check_later [number of interesting link] When I finish my tour, I might choose to check my bookmarks and maybe visit one of those 'forums', starting the process again, or exit: > ON> q ## Alternative workflow ### starting with a URL Sometimes, someone pastes a link in IRC while I'm hanging there, or any other source. I then would probably start offpunk with the URL as parameter, or if I already have it open, I type 'go [paste the URL]'. Then I proceed as if this was a link from an aggregator or from a page in my bookmarks ## Summary, and to-do I've been using offpunk for just... some days, so my workflow is pretty simple. I'm barely scratching the surface of offpunk's commands capabilities by using it as a mere multi-protocol browser. Just now, while writing this document, I learned that you can tour the content of one of your lists by typing > ON> tour [name of the list] Even when I am using only some of what offpunk offers (mostly because I still haven't learnt and memorized all of its capabilities), it has proven to be a nice distraction-free way to read content from gemini, http and gopher. I have used text-based web browser in the past, but I like specially the way pages are displayed in offpunk (even when sometimes you'll need to "view full" to read all that the page brings) I'm probably not the most "usual" offpunk user, but maybe this document will show you that there are more than one way to use offpunk, and maybe you could try it and see how it can adapt to the way you want to read stuff from the internet :) ## TO-DO There are some offpunk options I want to start exploring and understand better: * I plan to import some feeds I had from years ago from feedly and start using the 'subscriptions' list. * I'm most of the time using "list edit [list]" and deleting items, when maybe I should be using "archive" * I should check exactly how the "offline" mode works and try using it, since it is precisely the "offline" idea what made me read about offpunk in the first place => /index.gmi Back to the tutorial offpunk-v3.0/tutorial/workflow_ploum.gmi000066400000000000000000000117361514232770500206570ustar00rootroot00000000000000# How I use Offpunk (by Ploum) My offpunk is offline by default. The config file contains one line : > offline ## A tour every morning Each morning, while water for my tea is boiling, I launch a full synchronisation of Offpunk. This synchronisation is done through a bash script called "do_the_internet.sh". The offpunk line contains the following: > offpunk --sync --assume-yes --cache-validity 51840 The "--assume-yes" insures I accept new SSL certificates. The cache validity is arbitrarly set around 15 hours which means that it will almost never do a full refresh twice a day but I will not miss a day if, for one reason, I was late yesterday but I’m really early this morning. Once this is done, I can browse through all the news from my subscribed RSS through "tour": > t When I see an article that looks interesting to read and is a bit too long to read right now (remember, I’m sipping tea), I add it to my "toread" list. > add toread At the end of my tour, I go to this list to read whatever I feel reading right now. > list toread > X (where X is a number in the list) After reading, I archive it. > archive If the content is truncated, I ask to see the full version. > v full I may need to see the page in Firefox. > open url If I know that a given link will need to be open in Firefox, I do it directly. > open url X (where X is the number of the link) I may also check first where the link is heading with. > v X (where X is the number of the link) If it looks interesting, I add it to my tour: > t X Then I get back reading the article with > v Sometimes, I follow links and find new stuff to read that I also put in my "toread" list. If the author look interesting, I try to see what else (s)he wrote by immediately going to her/his RSS feed. > feed How, that feed looks interesting. Let’s subscribe by putting it in my RSS subscriptions: > add rss "rss" is a list I created with the following: > list create rss > list subscribe rss Alternatively, you could simply put it in your "subscribed" list. (mine is dedicated to gemlogs only). From time to time, I try to organize my subscription by grouping them into section in my list with > list edit rss I can also remove some older no longer updated feeds. Another alternative is simply to archive them by doing > list rss > X (where X is the number of the feed) > archive ## Other news There are a few pages I want to regularly visit which don’t have a feed. Or feeds for which I don’t want each item to clutter my tour. For those, I’ve created a list called "news". > list create news I didn’t subscribe to that list. It is a simple list. If the news are low and I still have cup in my tea, I browse the whole list with: > tour news > t Some RSS/Atom feeds don’t point to content but contain their own content. To access the whole content, simply ask for the full version of the feed. > v full Once in full mode, I add the feed to my news list. > add news It means I can browse regularly that feed (but no content will be pushed into my tour). ## Remembering Sometimes, I want to remember a page to use it in a project. When this happen, I create a list for that project. > list create offpunk But I don’t want the content of the links there to be updated, ever. I want to preserve the content! So I freeze the list. > list freeze offpunk When something in my "toread" list should be kept for a given project, I move it there. > move offpunk I use "move" and not "add". Else the link would stay in both "toread" and "offpunk", which is not what I want. ## Reading later from outside source Often, I’m forced to use Firefox. But I prefer reading articles in Offpunk. To do that, I simply copy the URL of the page in my clipboard (or the URL received through an email) then, in a terminal, I type: > toread Toread is a zsh alias to the following : > offpunk --fetch-later `wl-paste -p` (I use Wayland. If you use X, replace wl-paste by xclip or xsel) Now, the URL is in my list to_fetch and will be fetched next morning. If Offpunk is already open, I simply type: > go (by default, "go" will use the content of the clipboard) If I don’t want to wait til tomorrow, I run a "short sync" which is the following: > offpunk --sync tour to_fetch --assume-yes --cache-validity 51840 This means that offpunk will only tries to sync the content of lists "tour" and "to_fetch". It will thus ignore "rss", "subscribed" and "news". ## Finding what you’ve read Sometimes, I want to go back to what I’ve just read today or a few days ago. If it’s very recent, I use history: > history If a bit older, I browse the archives. > list archives The archives are limited to 200 (this is configurable). For me, it means between 1 and 2 months of reading. If I really want to find something I’ve read months ago, I use "grep" in the "~/.cache/offpunk" folder. It is rare but… it worked several times. ## Outside of Offpunk Inside my terminal, I now open files with: > openk => /index.gmi Back to the tutorial offpunk-v3.0/ubuntu_dependencies.txt000066400000000000000000000003351514232770500200120ustar00rootroot00000000000000sudo apt install less file xdg-utils xsel chafa timg python3-cryptography python3-requests python3-feedparser python3-bs4 python3-readability python3-setproctitle python3-chardet file python3-lxml-html-clean wl-clipboard offpunk-v3.0/unmerdify.py000066400000000000000000000504771514232770500156110ustar00rootroot00000000000000#!/usr/bin/env python3 # Following code has been originally written by Vincent Jousse - 2025. # All credits to him import argparse import fileinput import glob import logging import logging.config import os import re from copy import deepcopy from dataclasses import dataclass from urllib.parse import urlparse from lxml import etree LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { "format": "[%(asctime)s] [%(levelname)8s] [%(filename)s:%(lineno)s - %(funcName).20s…] %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", } }, "handlers": { "stdout": { "class": "logging.StreamHandler", "stream": "ext://sys.stdout", "formatter": "default", } }, "loggers": {"": {"handlers": ["stdout"], "level": "ERROR"}}, } logging.config.dictConfig(LOGGING) LOGGER = logging.getLogger(__name__) def set_logging_level(level): LOGGING["loggers"][""]["level"] = level logging.config.dictConfig(LOGGING) @dataclass class Command: """Class for keeping track of a command item.""" name: str accept_multiple_values: bool = False is_bool: bool = False xpath_value: bool = False has_capture_group: bool = False special_command: bool = False ignore: bool = False COMMANDS: list[Command] = [ Command("author", accept_multiple_values=True), Command("autodetect_on_failure", is_bool=True), Command("body", accept_multiple_values=True), Command("date", accept_multiple_values=True), Command("find_string", accept_multiple_values=True), Command("http_header", has_capture_group=True, special_command=True), Command("if_page_contains", special_command=True), Command("login_extra_fields", accept_multiple_values=True), Command("login_password_field"), Command("login_uri"), Command("login_username_field"), Command("native_ad_clue", accept_multiple_values=True), Command("next_page_link", accept_multiple_values=True), Command("not_logged_in_xpath"), Command("parser"), Command("prune", is_bool=True), Command("replace_string", has_capture_group=True, accept_multiple_values=True), Command("requires_login", is_bool=True), Command("src_lazy_load_attr"), Command("single_page_link", accept_multiple_values=True), Command("skip_json_ld", is_bool=True), Command("strip", accept_multiple_values=True), Command("strip_attr", accept_multiple_values=True, xpath_value=True), Command("strip_id_or_class", accept_multiple_values=True), Command("strip_image_src", accept_multiple_values=True), Command("test_contains", special_command=True), Command("test_url", accept_multiple_values=True, special_command=True), Command("tidy", is_bool=True), Command("title", accept_multiple_values=True), Command("wrap_in", has_capture_group=True, special_command=True), ] COMMANDS_PER_NAME: dict[str, Command] = { COMMANDS[i].name: COMMANDS[i] for i in range(0, len(COMMANDS)) } def get_config_files( site_config_dir: str, include_config_dir: bool = True ) -> list[str]: """ Read the *.txt files from the site_config directory and returns the file list. Parameters: site_config_dir (str): The path to the directory containing the config files include_config_dir (bool): Should the config_dir be included in the returned list Returns: filenames (list[str]): The list of filenames found with the .txt extension """ filenames: list[str] = [] for file in glob.iglob(f"{site_config_dir}/*.txt", include_hidden=True): if file.endswith("LICENSE.txt"): continue if include_config_dir: filenames.append(file) else: filenames.append(file.removeprefix(f"{site_config_dir}/")) filenames.sort() return filenames def get_host_for_url(url: str) -> str: parsed_uri = urlparse(url) return parsed_uri.netloc def get_possible_config_file_names_for_host( host: str, file_extension: str = ".txt" ) -> list[str]: """ The five filters config files can be of the form - .specific.domain.tld (for *.specific.domain.tld) - specific.domain.tld (for this specific domain) - .domain.tld (for *.domain.tld) - domain.tld (for domain.tld) """ parts = host.split(".") if len(parts) < 2: raise ValueError( f"The host must be of the form `host.com`. It seems that there is no dot in the provided host: {host}" ) tld = parts.pop() domain = parts.pop() first_possible_name = f"{domain}.{tld}{file_extension}" possible_names = [first_possible_name, f".{first_possible_name}"] # While we still have parts in the domain name, prepend the part # and create the 2 new possible names while len(parts) > 0: next_part = parts.pop() possible_name = f"{next_part}.{possible_names[-2]}" possible_names.append(possible_name) possible_names.append(f".{possible_name}") # Put the most specific file names first possible_names.reverse() return possible_names def get_config_file_for_host(config_files: list[str], host: str) -> str | None: possible_config_file_names = get_possible_config_file_names_for_host(host) for config_file in config_files: basename = os.path.basename(config_file) for possible_config_file_name in possible_config_file_names: if basename == possible_config_file_name: return config_file def is_unmerdifiable(url,ftr_site_config): config_files = get_config_files(ftr_site_config) if load_site_config_for_url(config_files,url): return True else: return False def parse_site_config_file(config_file_path: str) -> dict | None: config = {} with open(config_file_path, "r") as file: previous_command = None while line := file.readline(): line = line.strip() # skip comments, empty lines if line == "" or line.startswith("#") or line.startswith("//"): continue command_name = None command_value = None pattern = re.compile(r"^([a-z_]+)(?:\((.*)\))*:[ ]*(.*)$", re.I) result = pattern.search(line) if not result: logging.error( f"-> 🚨 ERROR: unknown line format for line `{line}` in file `{config_file_path}`. Skipping." ) continue command_name = result.group(1).lower() command_arg = result.group(2) command_value = result.group(3) command = COMMANDS_PER_NAME.get(command_name) if command is None: logging.warning( f"-> ⚠️ WARNING: unknown command name for line `{line}` in file `{config_file_path}`. Skipping." ) continue # Check for commands where we accept multiple statements but we don't have args provided # It handles `replace_string: value` and not `replace_string(test): value` if ( command.accept_multiple_values and command_arg is None and not command.special_command ): config.setdefault(command_name, []).append(command_value) # Single value command that should evaluate to a bool elif command.is_bool and not command.special_command: config[command_name] = "yes" == command_value or "true" == command_value # handle replace_string(test): value elif command.name == "replace_string" and command_arg is not None: config.setdefault("find_string", []).append(command_arg) config.setdefault("replace_string", []).append(command_value) # handle http_header(user-agent): Mozilla/5.2 elif command.name == "http_header" and command_arg is not None: config.setdefault("http_header", []).append( {command_arg: command_value} ) # handle if_page_contains: Xpath value elif command.name == "if_page_contains": # Previous command should be applied only if this expression is true previous_command_value = config[previous_command.name] # Move the previous command into the "if_page_contains" command if ( previous_command.accept_multiple_values and len(previous_command_value) > 0 ): config.setdefault("if_page_contains", {})[command_value] = { previous_command.name: previous_command_value.pop() } # Remove the entire key entry if the values are now empty if len(previous_command_value) == 0: config.pop(previous_command.name) # handle if_page_contains: Xpath value elif command.name == "wrap_in": config.setdefault("wrap_in", []).append((command_arg, command_value)) elif command.name == "test_url": config.setdefault("test_url", []).append( {command.name: command_value, "test_contains": []} ) elif command.name == "test_contains": test_url = config.get("test_url") if test_url is None or len(test_url) == 0: logging.error( "-> 🚨 ERROR: No test_url found for given test_contains in file `{config_file_path}`. Skipping." ) continue test_url[-1]["test_contains"].append(command_value) else: config[command_name] = command_value previous_command = command return config if config != {} else None def load_site_config_for_host(config_files: list[str], host: str) -> dict | None: logging.debug(f"-> Loading site config for {host}") config_file = get_config_file_for_host(config_files, host) if config_file: logging.debug(f"-> Found config file, loading {config_file} config.") return parse_site_config_file(config_file) else: logging.debug(f"-> No config file found for host {host}.") def load_site_config_for_url(config_files: list[str], url: str) -> dict | None: return load_site_config_for_host(config_files, get_host_for_url(url)) # Content extractor code def replace_strings(site_config: dict, html: str) -> str: replace_string_cmds = site_config.get("replace_string", []) find_string_cmds = site_config.get("find_string", []) if len(replace_string_cmds) == 0 and len(find_string_cmds) == 0: return html if len(replace_string_cmds) != len(find_string_cmds): logging.error( "🚨 ERROR: `replace_string` and `find_string` counts are not the same but must be, skipping string replacement." ) else: nb_replacement = 0 for replace_string, find_string in zip(replace_string_cmds, find_string_cmds): nb_replacement += html.count(find_string) html = html.replace(find_string, replace_string) logging.debug( f"Replaced {nb_replacement} string{'s'[:nb_replacement ^ 1]} using replace_string/find_string commands." ) logging.debug(f"Html after string replacement: {html}") return html def wrap_in(site_config: dict, lxml_tree): for tag, pattern in site_config.get("wrap_in", []): logging.debug(f"Wrap in `{tag}` => `{pattern}`") elements = lxml_tree.xpath(pattern) for element in elements: parent = element.getparent() newElement = etree.Element(tag) newElement.append(deepcopy(element)) parent.replace(element, newElement) def strip_elements(site_config: dict, lxml_tree): for pattern in site_config.get("strip", []): remove_elements_by_xpath(pattern, lxml_tree) def strip_elements_attributes(site_config: dict, lxml_tree): for pattern in site_config.get("strip_attr", []): remove_attributes_by_xpath(pattern, lxml_tree) def strip_elements_by_id_or_class(site_config: dict, lxml_tree): for pattern in site_config.get("strip_id_or_class", []): # Some entries contain " or ' pattern = pattern.replace("'", "").replace('"', "") remove_elements_by_xpath( f"//*[contains(concat(' ',normalize-space(@class), ' '),' {pattern} ') or contains(concat(' ',normalize-space(@id),' '), ' {pattern} ')]", lxml_tree, ) def strip_image_src(site_config: dict, lxml_tree): for pattern in site_config.get("strip_image_src", []): # Some entries contain " or ' pattern = pattern.replace("'", "").replace('"', "") remove_elements_by_xpath(f"//img[contains(@src,'{pattern}')]", lxml_tree) def get_body_element(site_config: dict, lxml_tree): body_contents = [] for pattern in site_config.get("body", []): elements = lxml_tree.xpath(pattern) for body_element in elements: body_contents.append(body_element) if len(body_contents) == 1: return body_contents[0] if len(body_contents) > 1: body = etree.Element("div") for element in elements: body.append(element) return body def get_body_element_html(site_config: dict, lxml_tree): body = get_body_element(site_config, lxml_tree) if body is not None: return etree.tostring(body, encoding="unicode") def remove_hidden_elements(lxml_tree): remove_elements_by_xpath( "//*[contains(@style,'display:none') or contains(@style,'visibility:hidden')]", lxml_tree, ) def remove_a_empty_elements(lxml_tree): remove_elements_by_xpath( "//a[not(./*) and normalize-space(.)='']", lxml_tree, ) def remove_attributes_by_xpath(xpath_class_expression, lxml_tree): parts = xpath_class_expression.split("/") class_to_remove = parts.pop().replace("@", "") xpath_expression = "/".join(parts) elements = lxml_tree.xpath(xpath_expression) for element in elements: element.attrib.pop(class_to_remove) def remove_elements_by_xpath(xpath_expression, lxml_tree): elements = lxml_tree.xpath(xpath_expression) for element in elements: if isinstance(element, etree._Element): element.getparent().remove(element) else: logging.error( f"🚨 ERROR: remove by xpath, `{xpath_expression}` element is not a Node, got {type(element)}." ) def get_xpath_value_for_command( site_config: dict, command_name: str, lxml_tree ) -> str | None: command_xpaths = site_config.get(command_name, []) for command_xpath in command_xpaths: value = get_xpath_value(site_config, command_xpath, lxml_tree) if value is not None: return value def get_multiple_xpath_values_for_command( site_config: dict, command_name: str, lxml_tree ) -> list[str]: command_xpaths = site_config.get(command_name, []) values = [] for command_xpath in command_xpaths: values = values + get_multiple_xpath_values( site_config, command_xpath, lxml_tree ) return values def get_xpath_value(site_config: dict, xpath: str, lxml_tree): elements = lxml_tree.xpath(xpath) if isinstance(elements, str) or isinstance(elements, etree._ElementUnicodeResult): return str(elements) for element in elements: # Return the first entry found if isinstance(element, str) or isinstance(element, etree._ElementUnicodeResult): return str(element) else: value = etree.tostring(element, method="text", encoding="unicode").strip() return " ".join(value.split()).replace("\n", "") def get_multiple_xpath_values(site_config: dict, xpath: str, lxml_tree): values = [] elements = lxml_tree.xpath(xpath) if isinstance(elements, str) or isinstance(elements, etree._ElementUnicodeResult): return [str(elements)] for element in elements: # Return the first entry found if isinstance(element, str) or isinstance(element, etree._ElementUnicodeResult): values.append(str(element)) else: value = etree.tostring(element, method="text", encoding="unicode").strip() value = " ".join(value.split()).replace("\n", "") values.append(value) return values def get_body(site_config: dict, html: str): html = replace_strings(site_config, html) html_parser = etree.HTMLParser(remove_blank_text=True, remove_comments=True) tree = etree.fromstring(html, html_parser) wrap_in(site_config, tree) strip_elements(site_config, tree) strip_elements_by_id_or_class(site_config, tree) strip_image_src(site_config, tree) remove_hidden_elements(tree) remove_a_empty_elements(tree) return get_body_element_html(site_config, tree) def unmerdify_from_file(content,url=None,ftr_site_config=None,loglevel=logging.ERROR,\ NOCONF_FAIL=True): html = "" # We pass '-' as only file when argparse got no files which will cause fileinput to read from stdin for line in fileinput.input( files=content if len(content) > 0 else ("-",), openhook=fileinput.hook_encoded("utf-8"), ): html += line return unmerdify_html(html,url=url,ftr_site_config=ftr_site_config,loglevel=loglevel,\ NOCONF_FAIL=NOCONF_FAIL) def unmerdify_html(html,url=None,ftr_site_config=None,loglevel=logging.ERROR,\ NOCONF_FAIL=True): set_logging_level(loglevel) if not ftr_site_config: logging.error("Unmerdify requires a path to a local ftr_site_config directory,\ see https://github.com/fivefilters/ftr-site-config directory") return 1 if os.path.isdir(ftr_site_config) and url is None: logging.error( "ERROR: You must provide an URL with --url if you don't provide a specific config file.", ) return 1 if os.path.isdir(ftr_site_config): config_files = get_config_files(ftr_site_config) loaded_site_config = load_site_config_for_url(config_files, url) else: loaded_site_config = parse_site_config_file(ftr_site_config) # If NOCONF_FAIL, we fail if no conf has been found for our content # Else, we simply return the full untouched HTML if loaded_site_config is None: if NOCONF_FAIL: logging.error(f"Unable to load site config for `{ftr_site_config}`.") return 1 else: logging.debug(f"No config for `{url}`, returning full HTML.") return html html_replaced = replace_strings(loaded_site_config, html) html_parser = etree.HTMLParser(remove_blank_text=True, remove_comments=True) tree = etree.fromstring(html_replaced, html_parser) title = get_xpath_value_for_command(loaded_site_config, "title", tree) logging.debug(f"Got title `{title}`.") authors = get_multiple_xpath_values_for_command(loaded_site_config, "author", tree) logging.debug(f"Got authors {authors}.") date = get_xpath_value_for_command(loaded_site_config, "date", tree) logging.debug(f"Got date `{date}`.") body_html = get_body(loaded_site_config, html) return body_html def main() -> int: parser = argparse.ArgumentParser( description="Get the content, only the content: unenshittificator for the web" ) parser.add_argument( "ftr_site_config", type=str, help="The path to the https://github.com/fivefilters/ftr-site-config directory, or a path to a config file.", ) parser.add_argument( "-u", "--url", type=str, help="The url you want to unmerdify.", ) parser.add_argument( "files", metavar="FILE", nargs="*", help="Files to read, if empty, stdin is used.", ) parser.add_argument( "-l", "--loglevel", default=logging.ERROR, choices=logging.getLevelNamesMapping().keys(), help="Set log level", ) # @TODO: extract open graph information if any # https://github.com/j0k3r/graby/blob/master/src/Extractor/ContentExtractor.php#L1241 args = parser.parse_args() bodyhtml = unmerdify_from_file(args.files,url=args.url,ftr_site_config=args.ftr_site_config,loglevel=args.loglevel) print(bodyhtml) if __name__ == "__main__": main() offpunk-v3.0/xkcdpunk.py000077500000000000000000000052321514232770500154260ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import argparse import gettext import random import netcache import openk from offutils import _LOCALE_DIR gettext.bindtextdomain('offpunk', _LOCALE_DIR) gettext.textdomain('offpunk') _ = gettext.gettext def get_latest(name="xkcd",offline=False): if name == "xkcd": rss = "https://xkcd.com/atom.xml" latest_link = 2 #validity is 12h = 43200 seconds if offline: validity = 0 else: validity = 43200 cache = openk.opencache() netcache.fetch(rss,validity=validity,offline=offline) r = cache.get_renderer(rss) if not r: print("xkcdpunk needs to be run at least once online to fetch the rss feed") return None else: link = r.get_link(latest_link) return link def main(): descri = _("xkcdpunk is a tool to display a given XKCD comic in your terminal") parser = argparse.ArgumentParser(prog="xkcd", description=descri) parser.add_argument( "number", nargs="*", help=_("XKCD comic number. Also accept value \"latest\" and \"random\". Default is \"latest\"") ) parser.add_argument( "--offline", action="store_true", help=_("Only access cached comics") ) args = parser.parse_args() cache = openk.opencache() url = "https://xkcd.com/" u = None for n in args.number: if n.isdigit(): u = url + str(n) + "/" elif n == "random": # for the random, we simply take the biggest known xkcd comic # and find a number between 1 and latest last_url = get_latest(offline=args.offline) if last_url: last = last_url.strip("/").split("/")[-1] if last.isdigit(): value = random.randrange(2,int(last)) u = url + str(value) + "/" if args.offline: # If offline, we check for a valid netcache version # We introduce a max counter to not search infinitely max_counter = 0 while not netcache.is_cache_valid(u) and max_counter < 1000: value = random.randrange(2,int(last)) u = url + str(value) + "/" max_counter += 1 elif n == "latest": u = get_latest(offline=args.offline) if not u: #By default, we get the latest u = get_latest(offline=args.offline) if u: cache.openk(u) else: print("No cached XKCD comics were found. Please run xkcdpunk online to build the cache") if __name__ == "__main__": main()