From 79d4a0f0110cbb2941306674275d5dd7f1eaa1ae Mon Sep 17 00:00:00 2001 From: veenm Date: Mon, 26 Oct 2026 21:08:42 +0100 Subject: [PATCH] Dashboard Studio 1.14.0: new cards, kiosk announcements, guest mode, templates, sharing and phone app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Seven new cards: parcels, air quality, water and gas usage, solar forecast, garage and gate, sports, Formula One - Kiosk: announcements from automations, tab rotation, guest mode, click sound - Editor: templates, widget presets, undo history, alignment guides, Lovelace import, share link and QR code, theme variants, suggestions, touch and hold actions - Phones: installable app with shortcuts, offline states, tab swipe, pull to refresh, bottom sheets, haptics, phone/large visibility, landscape layout, editing on the phone, away tab - Fixed: the weather forecast and to-do list could stay on Loading… Co-Authored-By: Claude Opus 5.5 --- dashboard_studio/CHANGELOG.md | 17 + dashboard_studio/DOCS.md | 81 +- dashboard_studio/config.yaml | 2 +- dashboard_studio/server/main.py | 40 +- dashboard_studio/server/mock_ha.py | 107 ++- dashboard_studio/web/app-icon.svg | 18 + dashboard_studio/web/app.js | 994 +++++++++++++++++++++- dashboard_studio/web/apple-touch-icon.png | Bin 0 -> 28901 bytes dashboard_studio/web/i18n.js | 216 +++++ dashboard_studio/web/icon-192.png | Bin 0 -> 31751 bytes dashboard_studio/web/icon-512.png | Bin 0 -> 144320 bytes dashboard_studio/web/icons.js | 14 + dashboard_studio/web/index.html | 20 + dashboard_studio/web/qr.js | 174 ++++ dashboard_studio/web/style.css | 246 +++++- dashboard_studio/web/sw.js | 31 + dashboard_studio/web/widgets.js | 354 +++++++- 17 files changed, 2256 insertions(+), 58 deletions(-) create mode 100644 dashboard_studio/web/app-icon.svg create mode 100644 dashboard_studio/web/apple-touch-icon.png create mode 100644 dashboard_studio/web/icon-192.png create mode 100644 dashboard_studio/web/icon-512.png create mode 100644 dashboard_studio/web/qr.js create mode 100644 dashboard_studio/web/sw.js diff --git a/dashboard_studio/CHANGELOG.md b/dashboard_studio/CHANGELOG.md index d30966f..84ef097 100644 --- a/dashboard_studio/CHANGELOG.md +++ b/dashboard_studio/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.14.0 + +- **Seven new cards:** parcels on the way (PostNL, DHL), air quality per room with a hint to open a window, water and gas use per day, the solar forecast with the best time to run the washer, a garage and gate button that asks first, sports scores (TeamTracker, optionally only on match days) and Formula One (next race, sessions, standings and results). +- **Announcements:** an automation can put a full-screen message on the screens, such as "Dinner is ready". +- **Kiosk:** tabs can take turns, pausing when someone touches the screen. +- **Guest mode:** a simple screen with only lights and thermostats, turned on with a button, an entity or a schedule. +- **New dashboards from templates:** wall tablet, phone, energy and family, filled with your own devices. +- **Import from Home Assistant:** turn an existing Home Assistant dashboard into a Dashboard Studio dashboard. +- **Share** a dashboard as a link or QR code, optionally in kiosk mode. +- **Editor:** save widgets as presets, see and jump back through the undo history, and alignment guides while dragging. +- **Devices not on a dashboard yet** are pointed out in the widget list, ready to add. +- **Touch and hold** a widget for its details, or give it a second action of its own. +- **Kiosk:** an optional soft click when something is pressed, for wall tablets that can't vibrate. +- **Themes** can switch to a matching dark or light version with the device. +- **Phones:** install Dashboard Studio as an app with its own icon and home screen shortcuts, keep showing the last known states when offline, swipe between tabs, pull down to refresh, popups as bottom sheets, a short vibration on presses, widgets only on phones or only on larger screens, a landscape layout, editing on the phone itself, and a tab that opens when you're away from home. +- Fixed: the weather forecast and to-do list could stay on "Loading…" when the page had just connected. + ## 1.13.1 - Car card: the battery bar is always shown now, along the bottom of the card, with the charge limit marked. It turns green with the charging details while the car charges. On the wide card it runs the full width. diff --git a/dashboard_studio/DOCS.md b/dashboard_studio/DOCS.md index f15421f..5c91510 100644 --- a/dashboard_studio/DOCS.md +++ b/dashboard_studio/DOCS.md @@ -19,9 +19,15 @@ More editing tools: - **Several widgets at once:** Shift-click or ⌘-click (Ctrl-click) widgets to select them together. Drag one to move them all, or use the arrow keys. The right panel can line up their left, right, top or bottom edges, give them the same width or height (that of the widget you clicked last), duplicate them, copy them elsewhere or delete them. - **Copy to…** (in the right panel) copies the selected widgets to another tab or another dashboard. - **Into a scroll or swipe card:** drag a widget onto a horizontal scroll or swipe card; it lights up, and when you let go the widget becomes one of its cards. To take a card out again, open it in the container's settings and choose **Take out of this card**. +- **Alignment guides:** while you drag or resize a widget, pink lines show where its edges line up with other widgets. +- **Presets:** **Save as preset** (in the right panel) keeps a set-up widget under a name. Presets appear at the top of the widget list, on every dashboard; hover one to delete it. +- **Devices not on a dashboard:** the top of the widget list says how many of your devices (lights, thermostats, covers, players, cameras, locks, switches and more) aren't on any dashboard yet. Open it to add one to the current tab with the widget that suits it. Also in the **⋯** menu. +- **Touch and hold:** holding a widget for half a second shows the details of its entity. Under **Touch and hold** in a widget's settings, you can make it switch the entity on or off, run an action of its own, or do nothing. +- **Undo history…** in the **⋯** menu lists the changes made since you opened the editor, such as *Added Light* or *Moved 3 widgets*. Pick one to go back to just after it; redo brings the later ones back. ## Look and feel +- **Light and dark versions:** turn on **Switch to a dark or light version with the device** under a theme to use a matching dark version of a light theme when the device is in dark mode, and the other way round. *System* always does this. - **Theme:** *System* follows your device's light or dark appearance. The other 14 themes are fixed: Light, Dark, Nord, Dusk, Forest, Graphite, Indigo, Rose, Ocean, Sunrise, Lavender, Mint, Sand, and High contrast for maximum readability. Text meets accessibility contrast levels in every theme. Changing any color switches the theme to *Custom*. - **Tile style:** *Cards* puts every widget on a tile. *Outlined* draws a thin border instead. *Plain* removes the cards completely, so widgets sit straight on the background. You can also turn off the background for a single widget under **Style → No background**. - **Lights that are on** glow: the tile turns warm, or takes on the bulb's color for color lights. In Plain style, the light's icon and brightness bar glow instead. @@ -103,6 +109,34 @@ An alarm panel (`alarm_control_panel.*`) with its state, a keypad for the code ( Home Assistant's notifications, with a button to dismiss each one or all, plus available updates (`update.*`) with a button to install them (after confirmation). +### Parcels + +Shows the parcels on their way from parcel integrations such as PostNL or DHL, whose sensors keep a list of parcels in their attributes. Each parcel shows its sender or description, its status and when it's expected; parcels coming today are highlighted. Tap a parcel to open its tracking page, when the integration provides one. Parcels delivered in the last two days can be shown too, dimmed. + +### Air quality + +Groups CO₂, fine dust (PM2.5, PM10), VOC and humidity sensors per room, with a green, orange or red dot for each room and each value. A hint says what to do: *Open a window* above 1000 ppm CO₂, *Too humid* above 70% humidity, or *The air is dry* below 30%. Leave the list empty to find every such sensor that is in a room (area) in Home Assistant, or pick sensors yourself. + +### Water and gas + +Pick a water, gas or energy meter (a sensor with a state class *total* or *total_increasing*, the same kind the energy dashboard uses). The card shows today's use so far, yesterday's total, and a bar for each of the last days (7 by default) with the average as a dashed line. Water in m³ is shown in litres. + +### Solar forecast + +For the Forecast.Solar integration. Pick the *production today* sensor and the card fills in tomorrow's and the current power by itself. It shows today's expected production hour by hour (the current hour marked) and the best time to run the washer or dishwasher: the hours in a row with the most sun from now on (3 by default). + +### Garage and gate + +One big button for a garage door, gate or lock, which opens or closes it (or stops it while moving). It asks first, unless you turn that off. Underneath: whether it's open, for how long, and who last opened or closed it, when Home Assistant knows (a person with a user account). + +### Sports + +For the TeamTracker integration: the next match with both teams, their logos, the kick-off time and the venue and TV channel; the live score with the match clock during the match; the final score afterwards. Turn on **Only show on match days** to hide the card on other days. + +### Formula One + +For the F1 Sensor integration. Pick the *next race* sensor and the card finds the standings and results sensors itself. It shows the next race with a countdown and the times of every session in your own time zone, and tabs for the driver standings, the constructor standings and the last race's results. + ### Vacuum The card grows with its size. On a small card it's a compact row with the status, battery and buttons. On a bigger card, the robot sits in the middle with a battery ring around it, a pulse while it cleans and a bolt while it charges. Below it are tiles for battery, cleaned area, cleaning time and any extra sensors you add (such as filter or brush life), the suction power levels, and labelled buttons to start or pause, send it back to the dock and locate it. Optionally add a map: pick the camera or image entity that your vacuum integration provides (for example Roborock or Valetudo), and it refreshes every few seconds. If the battery level is a separate sensor in your integration, pick it under **Battery sensor**. @@ -180,7 +214,20 @@ In edit mode, hidden widgets stay visible but faded, with a **Hidden now** label Turn on **Designed for phones** under **Grid** in the dashboard settings. The dashboard switches to a 4-column layout that you design at phone width, so what you see while editing is what the phone shows. Existing widgets are converted: small tiles become half width, larger cards full width, and nothing overlaps. Turning it off converts back to 12 columns. -On a phone, such a dashboard fills the whole screen, without the top bar with the dashboard picker and **Edit**. To bring the bar back, touch and hold an empty spot for a moment; it hides again after 10 seconds. On a computer, a phone dashboard shows as a phone-width column. Dashboards that aren't designed for phones keep reflowing into two columns on phones, with the top bar. +On a phone, such a dashboard fills the whole screen, without the top bar with the dashboard picker and **Edit**. To bring the bar back, touch and hold an empty spot for a moment; it hides again after 10 seconds. On a computer, a phone dashboard shows as a phone-width column. Dashboards that aren't designed for phones keep reflowing into two columns on phones, with the top bar. With the phone on its side, a phone dashboard shows two columns of its layout next to each other. + +### On phones + +- **Install as an app:** **⋯ → Install as an app…** explains how. Home Assistant's own page can't be installed, so open Dashboard Studio in its own tab first (the dialog has a button for it). Installed, it opens full screen with its own icon and the dashboard's colors in the status bar. Dashboard Studio keeps Home Assistant's sign-in fresh in the background while it's open, using the login Home Assistant saved in that browser (with *Keep me logged in*). If it was closed for a long time and shows an error, open Home Assistant once. +- **Shortcuts:** tick **Shortcut on the app icon** in the settings of a button, tile, light, thermostat or garage card. On Android, long-press the installed app's icon to run it straight away (up to four shortcuts; iPhones don't support this). +- **Offline:** each device keeps the last known states of its dashboard. When the connection drops, the dashboard keeps showing them with a small note saying from when, and catches up when it reconnects. +- **Swipe** left or right over the dashboard to go to the next or previous tab (not over cards that swipe or scroll themselves). +- **Pull down** at the top of the dashboard to reload every state. +- **Popups** open as sheets from the bottom; drag one down to close it. +- **Vibration:** buttons, switches and sliders give a short tick on phones that support it (not iPhones). Turn this off under **This device**. Wall tablets can play a soft click instead: turn on **A soft click when something is pressed** in the **Kiosk** settings. +- **Show on:** under **Visibility**, a widget can be shown on all screens, only on phones, or only on larger screens. +- **Editing on the phone:** **Edit** works on a phone too. The bar at the bottom opens the widget list (**Add**) and the settings as sheets. Tap a widget to select it, then drag it or its corner; tap it again for its settings. +- **Away from home:** in the **Tabs** settings, **When you're away from home, open** picks a tab, such as cameras, the garage or the alarm, that opens while you're away, and the dashboard goes back when you're home. It follows the person signed in on the device, or a person you choose. Not used in kiosk mode. ## Tabs and side menu @@ -207,6 +254,26 @@ Press **Kiosk** in the toolbar, or open the dashboard with `#kiosk` at the end o **Doorbell:** in the **Kiosk** settings, pick the doorbell (a binary sensor that turns on, an `event.*` doorbell entity, or a switch or helper) and the camera to show. When it rings while kiosk mode is on, the camera fills the screen with a live picture, the screen saver goes away and a short chime plays (you can turn that off). It closes by itself after a minute, or after the number of seconds you set, or when you tap **Close**. +**Tabs that take turns:** with several tabs, set **Go to the next tab every (seconds)** to show them in turn. Touching the screen pauses this for a minute. + +**Announcements:** an automation can put a message on the screens, for example *Dinner is ready*. Fire the event `dashboard_studio_announce`: + +```yaml +action: + - event: dashboard_studio_announce + event_data: + message: Dinner is ready! + title: Kitchen # optional + icon: dining # optional, any icon name from the icon picker + seconds: 60 # optional, closes by itself after this + device: Kitchen tablet # optional, only this named device (see Which dashboard opens where) + sound: true # optional, a short chime +``` + +The message fills the screen, wakes it from the screen saver, and closes after the seconds you give (a minute by default) or when someone taps **OK**. It shows on open dashboards that aren't being edited; turn it off per dashboard in the **Kiosk** settings. + +**Guest mode** shows a simple screen with only lights and thermostats, for guests or a babysitter. Set it up under **Guest mode** in the dashboard settings: it turns on with the button there (on that device only), with an entity such as an `input_boolean` (on every screen), or between two times. You can pick which lights and thermostats it shows and the welcome text. A **Guest mode** button in the top bar ends it; in kiosk mode, use the exit corner, where the prompt has a **Guest mode** button too. Editing always shows the real dashboard. + On a wall tablet, use **New tab** in the kiosk settings. It opens the dashboard on its own, without the Home Assistant sidebar, and full screen works there. ## Language and night mode @@ -217,7 +284,13 @@ Dashboard Studio is available in English and Dutch (Nederlands). By default it f ## Several dashboards -Use **+** for a new dashboard. The **⋯** menu (in edit mode) can rename, duplicate, export, import or delete a dashboard. Exported JSON files can be imported on another Home Assistant install. +Use **+** for a new dashboard. You can start empty, from your devices, or from a template that fills itself with your devices: *Wall tablet* (clock, weather, calendar, rooms and cameras), *Phone* (people, lights, climate and media in the phone layout), *Energy* (power flow, solar forecast, water and gas, graphs) or *Family* (who's home, calendar, shopping list, parcels and weather). + +The **⋯** menu (in edit mode) can rename, duplicate, export, import or delete a dashboard. Exported JSON files can be imported on another Home Assistant install. + +**Import from Home Assistant…** turns one of your existing Home Assistant dashboards into a new dashboard here, with a tab for each view. Entities, tile, button, light, thermostat, sensor, gauge, graph, weather, media, camera, calendar, to-do, alarm, area and markdown cards become matching widgets, and stacks and grids are taken apart; other cards are skipped and counted. The original stays as it is. Dashboards that Home Assistant generates itself (without cards of their own) can't be imported. + +**Share…** shows a link and a QR code for the dashboard, optionally opening in kiosk mode. It opens on any device that is signed in to Home Assistant. **Version history…** in the **⋯** menu lists the earlier saves of the dashboard, up to 30, with the time and the number of widgets. Changes made within five minutes of each other count as one version. **Restore** brings a version back; ⌘Z undoes that. @@ -229,7 +302,7 @@ Use **⋯ → Who sees which dashboard…** in edit mode to choose the dashboard - **Other devices** you have named appear in the list too, so you can change them from anywhere. - **People:** pick a dashboard per Home Assistant user. People appear once they've opened Dashboard Studio, or when they have a person entity. -A device's choice comes before a person's. Without either, the last dashboard used on that device opens. A link with `#view=…` always wins. You can still switch dashboards at any time. +A device's choice comes before a person's. Without either, the last dashboard used on that device opens. A link with `#view=…` or a shared link always wins. You can still switch dashboards at any time. ## Version and updates @@ -237,4 +310,4 @@ The version you're running is shown at the bottom of the dashboard settings, und ## Data -Dashboards are stored in the app's `/data/dashboards.json`, and their version history in `/data/history/`, so both are included in Home Assistant backups. +Dashboards and presets are stored in the app's `/data/dashboards.json`, and their version history in `/data/history/`, so both are included in Home Assistant backups. Night mode, guest mode started with the button, vibration and the last known states are kept on each device. diff --git a/dashboard_studio/config.yaml b/dashboard_studio/config.yaml index b8486eb..243b6cd 100644 --- a/dashboard_studio/config.yaml +++ b/dashboard_studio/config.yaml @@ -1,5 +1,5 @@ name: Dashboard Studio -version: "1.13.1" +version: "1.14.0" slug: dashboard_studio description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes. url: https://git.melvanveen.nl/veenm/ha-dashboard-studio diff --git a/dashboard_studio/server/main.py b/dashboard_studio/server/main.py index 4d0ed37..6f425b0 100644 --- a/dashboard_studio/server/main.py +++ b/dashboard_studio/server/main.py @@ -147,7 +147,9 @@ def record_history(old, new): async def index(request): # Stamp the version on every asset URL, so an update can never be served from a stale cache. html = (WEB_DIR / "index.html").read_text() - html = re.sub(r'(static/[\w.-]+\.(?:js|css|png))"', rf'\1?v={VERSION}"', html) + # Locally, files change without a new version number: stamp them with their last change instead. + stamp = VERSION if not MOCK else f"{VERSION}-{int(max(f.stat().st_mtime for f in WEB_DIR.iterdir()))}" + html = re.sub(r'(static/[\w.-]+\.(?:js|css|png))"', rf'\1?v={stamp}"', html) html = html.replace("__VERSION__", VERSION) return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store"}) @@ -198,6 +200,40 @@ async def version(request): return web.json_response({"version": VERSION}, headers={"Cache-Control": "no-store"}) +def all_widgets(widgets): + for w in widgets or []: + if isinstance(w, dict): + yield w + yield from all_widgets((w.get("cfg") or {}).get("children")) + + +async def manifest(request): + """The web app manifest, so the dashboard can be installed on a phone's home screen. + Widgets marked as a shortcut appear when you long-press the app icon (Android).""" + shortcuts = [] + for d in load_store().get("dashboards", []): + for t in d.get("tabs", []): + for w in all_widgets(t.get("widgets")): + if w.get("shortcut") and len(shortcuts) < 4: + shortcuts.append({"name": str(w["shortcut"])[:40], "url": f"./?ds_action={w.get('id')}", + "icons": [{"src": "static/icon-192.png", "sizes": "192x192", "type": "image/png"}]}) + body = { + "name": "Dashboard Studio", "short_name": "Dashboards", "id": "./", "start_url": "./", "scope": "./", + "display": "standalone", "background_color": "#0f1526", "theme_color": "#0f1526", + "icons": [{"src": "static/icon-192.png", "sizes": "192x192", "type": "image/png"}, + {"src": "static/icon-512.png", "sizes": "512x512", "type": "image/png"}, + {"src": "static/app-icon.svg", "sizes": "any", "type": "image/svg+xml"}], + "shortcuts": shortcuts, + } + return web.json_response(body, content_type="application/manifest+json", headers={"Cache-Control": "no-store"}) + + +async def service_worker(request): + """Network first; the last good copy of the page, its files and the dashboards when the network fails.""" + js = (WEB_DIR / "sw.js").read_text().replace("__VERSION__", VERSION) + return web.Response(text=js, content_type="application/javascript", headers={"Cache-Control": "no-store"}) + + async def get_dashboards(request): return web.json_response(load_store()) @@ -334,6 +370,8 @@ def make_app(): app = web.Application(middlewares=[ingress_only], client_max_size=8 * 1024 * 1024) app.router.add_get("/", index) app.router.add_get("/api/version", version) + app.router.add_get("/manifest.webmanifest", manifest) + app.router.add_get("/sw.js", service_worker) app.router.add_get("/api/changelog", changelog) app.router.add_get("/api/whoami", whoami) app.router.add_get("/api/users", users) diff --git a/dashboard_studio/server/mock_ha.py b/dashboard_studio/server/mock_ha.py index 8f67334..d0f2846 100644 --- a/dashboard_studio/server/mock_ha.py +++ b/dashboard_studio/server/mock_ha.py @@ -133,6 +133,8 @@ class MockHA: entity("sensor.model_y_odometer", "24816", friendly_name="Model Y Odometer", unit_of_measurement="km", device_class="distance"), ] } + self.states.update({e["entity_id"]: e for e in self._extra_entities()}) + self.states["cover.garage_door"]["context"] = {"id": "mock", "user_id": "mock-user-alex"} self.clients = set() self.todo = [{"uid": "1", "summary": "Milk", "status": "needs_action"}, {"uid": "2", "summary": "Bread", "status": "needs_action"}, {"uid": "3", "summary": "Coffee beans", "status": "needs_action"}, {"uid": "4", "summary": "Batteries", "status": "completed"}] @@ -143,6 +145,94 @@ class MockHA: } self.note_subs = set() + def _extra_entities(self): + """Parcels, air quality, meters, solar forecast, sports and Formula One.""" + now = datetime.now().astimezone() + day = now.replace(hour=0, minute=0, second=0, microsecond=0) + at = lambda d, h, m=0: (day + timedelta(days=d, hours=h, minutes=m)).isoformat() + drivers = [("Max", "Verstappen", "VER", "Red Bull", 395), ("Lando", "Norris", "NOR", "McLaren", 372), ("Oscar", "Piastri", "PIA", "McLaren", 341), + ("Charles", "Leclerc", "LEC", "Ferrari", 290), ("George", "Russell", "RUS", "Mercedes", 245), ("Lewis", "Hamilton", "HAM", "Ferrari", 198)] + standings = [{"position": str(i + 1), "points": str(p), "wins": str(max(0, 6 - i * 2)), "Driver": {"givenName": g, "familyName": f, "code": c}, "Constructors": [{"name": team}]} + for i, (g, f, c, team, p) in enumerate(drivers)] + teams = [{"position": str(i + 1), "points": str(p), "wins": str(w), "Constructor": {"name": n}} for i, (n, p, w) in enumerate([("McLaren", 713, 9), ("Ferrari", 488, 3), ("Red Bull", 470, 7), ("Mercedes", 402, 2)])] + results = [{"position": str(i + 1), "points": str([25, 18, 15, 12, 10, 8][i]), "Driver": {"givenName": g, "familyName": f, "code": c}, "Constructor": {"name": team}, + "status": "Finished", "Time": {"time": ["1:32:10.553", "+2.411", "+8.902", "+15.300", "+21.774", "+30.105"][i]}} + for i, (g, f, c, team, _) in enumerate(drivers)] + return [ + entity("sensor.postnl_delivery", "2", friendly_name="PostNL Delivery", + enroute=[{"key": "3SABCD123", "name": "Bol.com", "status_message": "De bezorger is onderweg", "planned_date": at(0, 14), "planned_to": at(0, 16), "url": "https://jouw.postnl.nl/"}, + {"key": "3SXYZ987", "name": "Coolblue", "status_message": "Wordt gesorteerd", "planned_date": at(1, 0)}], + delivered=[{"key": "3SQQQ555", "name": "Zalando", "status_message": "Bezorgd", "delivery_date": at(-1, 11, 20)}]), + entity("sensor.living_room_co2", "940", friendly_name="Living Room CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"), + entity("sensor.living_room_pm25", "8", friendly_name="Living Room PM2.5", unit_of_measurement="µg/m³", device_class="pm25"), + entity("sensor.bedroom_co2", "1340", friendly_name="Bedroom CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"), + entity("sensor.bedroom_humidity", "72", friendly_name="Bedroom Humidity", unit_of_measurement="%", device_class="humidity"), + entity("sensor.office_co2", "610", friendly_name="Office CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"), + entity("sensor.office_humidity", "46", friendly_name="Office Humidity", unit_of_measurement="%", device_class="humidity"), + entity("sensor.water_meter", "1234.567", friendly_name="Water Meter", unit_of_measurement="m³", device_class="water", state_class="total_increasing"), + entity("sensor.gas_meter", "4321.12", friendly_name="Gas Meter", unit_of_measurement="m³", device_class="gas", state_class="total_increasing"), + entity("sensor.energy_production_today", "18.4", friendly_name="Estimated energy production - today", unit_of_measurement="kWh", device_class="energy"), + entity("sensor.energy_production_tomorrow", "12.1", friendly_name="Estimated energy production - tomorrow", unit_of_measurement="kWh", device_class="energy"), + entity("sensor.power_production_now", "1240", friendly_name="Estimated power production - now", unit_of_measurement="W", device_class="power"), + entity("sensor.team_tracker_ajax", "PRE", friendly_name="Ajax", sport="soccer", league="Eredivisie", team_name="Ajax", team_abbr="AJA", team_homeaway="home", + opponent_name="PSV", opponent_abbr="PSV", date=at(0, 20), kickoff_in="in a few hours", venue="Johan Cruijff ArenA", tv_network="ESPN"), + entity("sensor.f1_next_race", at(5, 18), friendly_name="F1 Next Race", season="2026", round="21", race_name="São Paulo Grand Prix", + circuit_name="Autódromo José Carlos Pace", circuit_locality="São Paulo", circuit_country="Brazil", + first_practice_start=at(3, 15, 30), sprint_qualifying_start=at(3, 19, 30), sprint_start=at(4, 15), qualifying_start=at(4, 19), race_start=at(5, 18)), + entity("sensor.f1_driver_standings", "Max Verstappen", friendly_name="F1 Driver Standings", driver_standings=standings), + entity("sensor.f1_constructor_standings", "McLaren", friendly_name="F1 Constructor Standings", constructor_standings=teams), + entity("sensor.f1_last_race_results", "Mexico City Grand Prix", friendly_name="F1 Last Race Results", race_name="Mexico City Grand Prix", results=results), + ] + + def statistics(self, ids, start): + start_dt = datetime.fromisoformat(start.replace("Z", "+00:00")).astimezone() + now = datetime.now().astimezone() + out = {} + for eid in ids: + base = {"sensor.gas_meter": 3.2}.get(eid, 0.18) + rows, d = [], start_dt + while d <= now: + seed = (d.toordinal() * 7 + len(eid)) % 10 + change = base * (0.6 + seed / 12) + if d.date() == now.date(): + change *= now.hour / 24 + rows.append({"start": int(d.timestamp() * 1000), "end": int((d + timedelta(days=1)).timestamp() * 1000), "change": round(change, 3)}) + d += timedelta(days=1) + out[eid] = rows + return out + + def solar_forecast(self): + day = datetime.now().astimezone().replace(hour=0, minute=0, second=0, microsecond=0) + hours = {} + for d, peak in ((0, 2600), (1, 1700)): + for h in range(6, 21): + wh = max(0, peak * math.sin(math.pi * (h - 6) / 14)) + hours[(day + timedelta(days=d, hours=h)).isoformat()] = round(wh) + return {"mock_entry": {"wh_hours": hours}} + + def broadcast_event(self, event_type, data): + event = {"event_type": event_type, "data": data or {}, "time_fired": now_iso()} + for ws, sub_ids in list(self.clients): + for sid in sub_ids: + asyncio.ensure_future(self._safe_send(ws, {"id": sid, "type": "event", "event": event})) + + LOVELACE = {"views": [ + {"title": "Home", "path": "home", "icon": "mdi:home", "cards": [ + {"type": "entities", "title": "Living room", "entities": ["light.living_room", "light.reading_lamp", {"entity": "sensor.living_room_temperature", "name": "Temperature"}]}, + {"type": "thermostat", "entity": "climate.thermostat"}, + {"type": "weather-forecast", "entity": "weather.home"}, + {"type": "horizontal-stack", "cards": [{"type": "button", "entity": "switch.coffee_machine"}, {"type": "light", "entity": "light.kitchen"}]}, + {"type": "media-control", "entity": "media_player.living_room_speaker"}, + {"type": "markdown", "content": "## Welcome home"}, + ]}, + {"title": "Energy", "path": "energy", "cards": [ + {"type": "gauge", "entity": "sensor.power_usage", "min": 0, "max": 5000}, + {"type": "history-graph", "entities": ["sensor.power_usage", "sensor.solar_production"], "hours_to_show": 24}, + {"type": "tile", "entity": "sensor.solar_production"}, + {"type": "picture-entity", "entity": "camera.front_yard"}, + ]}, + ]} + # -- state handling --------------------------------------------------- def set_state(self, entity_id, state=None, **attrs): @@ -454,10 +544,10 @@ class MockHA: ]}) elif typ == "config/entity_registry/list": areas = { - "living_room": ["light.living_room", "light.reading_lamp", "light.downstairs", "media_player.living_room_speaker", "media_player.tv", "sensor.living_room_temperature", "sensor.living_room_humidity", "binary_sensor.hallway_motion", "climate.thermostat"], + "living_room": ["sensor.living_room_co2", "sensor.living_room_pm25", "light.living_room", "light.reading_lamp", "light.downstairs", "media_player.living_room_speaker", "media_player.tv", "sensor.living_room_temperature", "sensor.living_room_humidity", "binary_sensor.hallway_motion", "climate.thermostat"], "kitchen": ["light.kitchen", "switch.coffee_machine", "media_player.kitchen_speaker", "sensor.washer_status"], - "bedroom": ["light.bedroom", "binary_sensor.front_door"], - "office": ["fan.office", "input_boolean.guest_mode"], + "bedroom": ["light.bedroom", "binary_sensor.front_door", "sensor.bedroom_co2", "sensor.bedroom_humidity"], + "office": ["fan.office", "input_boolean.guest_mode", "sensor.office_co2", "sensor.office_humidity"], "garage": ["cover.garage_door", "lock.front_door"], } rows = [{"entity_id": e, "area_id": a, "device_id": None, "disabled_by": None, "hidden_by": None, "entity_category": None} for a, es in areas.items() for e in es] @@ -466,6 +556,17 @@ class MockHA: await ws.send_json({"id": mid, "type": "result", "success": True, "result": rows}) elif typ == "config/device_registry/list": await ws.send_json({"id": mid, "type": "result", "success": True, "result": []}) + elif typ == "recorder/statistics_during_period": + await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.statistics(req.get("statistic_ids") or [], req.get("start_time"))}) + elif typ == "energy/solar_forecast": + await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.solar_forecast()}) + elif typ == "fire_event": + self.broadcast_event(req.get("event_type"), req.get("event_data")) + await ws.send_json({"id": mid, "type": "result", "success": True, "result": {"context": {"id": "mock"}}}) + elif typ == "lovelace/dashboards/list": + await ws.send_json({"id": mid, "type": "result", "success": True, "result": [{"url_path": "wall-tablet", "title": "Wall tablet", "mode": "storage", "icon": "mdi:tablet"}]}) + elif typ == "lovelace/config": + await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.LOVELACE}) elif typ == "ping": await ws.send_json({"id": mid, "type": "pong"}) else: diff --git a/dashboard_studio/web/app-icon.svg b/dashboard_studio/web/app-icon.svg new file mode 100644 index 0000000..7c776b7 --- /dev/null +++ b/dashboard_studio/web/app-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/dashboard_studio/web/app.js b/dashboard_studio/web/app.js index a5e99f0..1336b55 100644 --- a/dashboard_studio/web/app.js +++ b/dashboard_studio/web/app.js @@ -60,6 +60,7 @@ const states = await this.send({ type: "get_states" }); this.states = Object.fromEntries(states.map((s) => [s.entity_id, s])); this.send({ type: "subscribe_events", event_type: "state_changed" }, (ev) => { + if (ev.event_type && ev.event_type !== "state_changed") return; const { entity_id, new_state } = ev.data; if (new_state) this.states[entity_id] = new_state; else delete this.states[entity_id]; @@ -75,6 +76,10 @@ else this.notifications = { ...(this.notifications || {}), ...ev.notifications }; this.onChange("__notifications"); }).catch(() => {}); + // Announcements: an automation fires this event to put a message on the screens. + this.send({ type: "subscribe_events", event_type: "dashboard_studio_announce" }, (ev) => { + if (ev.event_type === "dashboard_studio_announce") this.onAnnounce?.(ev.data || {}); + }).catch(() => {}); this.onStatus(true); this.onChange(null); return; @@ -142,6 +147,18 @@ }); return res?.response?.[entityId]?.forecast || []; } + /** Daily totals of a meter from the long-term statistics, oldest first, today last. */ + async statistics(entityId, days) { + await this.ready; + const start = new Date(); start.setHours(0, 0, 0, 0); start.setDate(start.getDate() - (days - 1)); + const res = await this.send({ type: "recorder/statistics_during_period", start_time: start.toISOString(), statistic_ids: [entityId], period: "day", types: ["change"] }); + return (res?.[entityId] || []).map((r) => ({ start: typeof r.start === "number" ? r.start : new Date(r.start).getTime(), change: r.change })); + } + /** Forecast.Solar's expected production per hour, per configured plane. */ + async solarForecast() { + await this.ready; + return this.send({ type: "energy/solar_forecast" }); + } async history(entityIds, hours) { await this.ready; const res = await this.send({ @@ -186,10 +203,11 @@ const DEFAULT_THEME = { preset: "System", surface: "cards", radius: 20, bgImage: "", blur: 0, shadow: true }; const DEFAULT_GRID = { cols: 12, rowHeight: 72, gap: 14, maxWidth: 1440 }; - const DEFAULT_KIOSK = { saver: 0, saverStyle: "clock", saverSource: "links", saverUrls: "", saverAlbums: [], saverInterval: 30, exitCorner: "top-right", hideCursor: true, doorbell: "", doorbellCamera: "", doorbellSecs: 60, doorbellSound: true }; + const DEFAULT_KIOSK = { saver: 0, saverStyle: "clock", saverSource: "links", saverUrls: "", saverAlbums: [], saverInterval: 30, exitCorner: "top-right", hideCursor: true, doorbell: "", doorbellCamera: "", doorbellSecs: 60, doorbellSound: true, rotate: 0, announce: true }; + const DEFAULT_GUEST = { when: "manual", entity: "", from: "18:00", to: "23:00", entities: [], title: "" }; const newTab = (name, icon = "home") => ({ id: uid(), name, icon, widgets: [] }); - const newDashboard = (name) => ({ id: uid(), name, theme: { ...DEFAULT_THEME }, grid: { ...DEFAULT_GRID }, kiosk: { ...DEFAULT_KIOSK }, nav: { show: false, style: "icons" }, tabs: [newTab(tr("Overview"))] }); + const newDashboard = (name) => ({ id: uid(), name, theme: { ...DEFAULT_THEME }, grid: { ...DEFAULT_GRID }, kiosk: { ...DEFAULT_KIOSK }, guest: { ...DEFAULT_GUEST }, nav: { show: false, style: "icons" }, tabs: [newTab(tr("Overview"))] }); // Bring dashboards saved by older versions up to the current shape. function migrate(d) { @@ -202,6 +220,7 @@ d.theme = { ...DEFAULT_THEME, ...t }; d.grid = { ...DEFAULT_GRID, ...(d.grid || {}) }; d.kiosk = { ...DEFAULT_KIOSK, ...(d.kiosk || {}) }; + d.guest = { ...DEFAULT_GUEST, ...(d.guest || {}) }; d.nav = { show: false, style: "icons", ...(d.nav || {}) }; if (!Array.isArray(d.tabs) || !d.tabs.length) d.tabs = [{ ...newTab(tr("Overview")), widgets: d.widgets || [] }]; delete d.widgets; @@ -223,6 +242,318 @@ return { id: uid(), type, x, y, w: def.size[0], h: def.size[1], cfg, style: { transparent: !!def.transparent, bg: "", color: "", accent: "" } }; } + // ------------------------------------------------------------ app on the home screen + let installPrompt = null; + window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); installPrompt = e; }); + const standalone = () => matchMedia("(display-mode: standalone)").matches || navigator.standalone === true; + async function installDialog() { + let inFrame = false; + try { inFrame = window.top !== window; } catch { inFrame = true; } + const ios = /iphone|ipad|ipod/i.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); + const how = inFrame + ? tr("Home Assistant’s page can’t be installed itself. Open Dashboard Studio in its own tab first, then choose Install as an app again there.") + : ios ? tr("In Safari, tap the Share button, then Add to Home Screen.") + : installPrompt ? tr("Your browser can install Dashboard Studio as an app with its own icon. It opens full screen, without the browser’s bars.") + : tr("In your browser’s menu, choose Install app or Add to Home screen."); + const choice = await openModal(`

${tr("Install as an app")}

${esc(how)}

+

${tr("It opens on the dashboard chosen for that device. Mark buttons as a shortcut to use them from the app icon.")}

+ `, + (e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x); }); + if (choice === "tab") window.open(new URL(`?ds=${current.id}`, BASE).href, "_blank", "noopener"); + if (choice === "install" && installPrompt) { installPrompt.prompt(); installPrompt = null; } + } + + /** Opened on its own (as an app, or in a tab), nobody renews Home Assistant's ingress session, so + * after a while requests would fail. Renew it the way Home Assistant's own page does, with the + * login that page saved in this browser. Inside Home Assistant this isn't needed. */ + async function keepIngressSession() { + let top = true; + try { top = window.top === window; } catch { top = false; } + if (!top || !location.pathname.startsWith("/api/hassio_ingress/")) return; + const renew = async () => { + let tokens; + try { tokens = JSON.parse(localStorage.getItem("hassTokens") || "null"); } catch { tokens = null; } + if (!tokens?.refresh_token) return; + if (!tokens.access_token || Date.now() > (tokens.expires || 0) - 60000) { + const r = await fetch("/auth/token", { method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: tokens.refresh_token, client_id: tokens.clientId || `${location.origin}/` }) }); + if (!r.ok) return; + const n = await r.json(); + tokens = { ...tokens, access_token: n.access_token, expires_in: n.expires_in, expires: Date.now() + n.expires_in * 1000 }; + localStorage.setItem("hassTokens", JSON.stringify(tokens)); + } + const r = await fetch("/api/hassio/ingress/session", { method: "POST", headers: { Authorization: `Bearer ${tokens.access_token}` } }); + if (!r.ok) return; + const session = (await r.json())?.data?.session; + if (session) document.cookie = `ingress_session=${session};path=/api/hassio_ingress/;SameSite=Strict${location.protocol === "https:" ? ";Secure" : ""}`; + }; + try { await renew(); } catch { /* offline: the cached page carries on */ } + setInterval(() => renew().catch(() => {}), 5 * 60 * 1000); + } + + // ------------------------------------------------------------ last known states + // The states the open dashboard uses are kept on this device, so it can show them while offline. + let stateCacheTimer = 0, lastLive = 0; + function cacheStates() { + if (stateCacheTimer) return; // at most every few seconds, however busy the states are + stateCacheTimer = setTimeout(() => { + stateCacheTimer = 0; + if (!current || !ha.connected) return; + const ids = new Set(["sun.sun"]); + for (const t of current.tabs) for (const w of t.widgets) entitiesOf(w).forEach((e) => ids.add(e)); + const states = {}; + for (const id of ids) if (ha.states[id]) states[id] = ha.states[id]; + const json = JSON.stringify({ at: Date.now(), states }); + if (json.length < 1.5e6) lsSet("ds.states", json); + }, 5000); + } + function loadCachedStates() { + try { + const c = JSON.parse(lsGet("ds.states") || "null"); + if (c?.states && !Object.keys(ha.states).length) { ha.states = c.states; lastLive = c.at; } + } catch { /* nothing kept */ } + } + const offlineText = () => (lastLive ? tr("Offline · {0}", new Date(lastLive).toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })) : tr("Offline")); + let offlineTimer = 0; + function showOffline(on) { + const el = $("#offline-note"); + if (!on) { clearTimeout(offlineTimer); offlineTimer = 0; el.hidden = true; return; } + if (offlineTimer || !el.hidden) return; // already counting, or showing (each reconnect attempt reports again) + // a short blip (an update, a reconnect) shouldn't flash a note + offlineTimer = setTimeout(() => { + offlineTimer = 0; + el.textContent = lastLive ? tr("Offline. Showing the states from {0}.", new Date(lastLive).toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })) : tr("Offline. Reconnecting…"); + el.hidden = false; + }, 4000); + } + + // ------------------------------------------------------------ sharing + // A link to Home Assistant's own page for this app, with the dashboard in it, so it opens + // through the sidebar on any device that is signed in to Home Assistant. + function sharedParams() { + const out = new URLSearchParams(location.search); + try { if (window.top !== window) for (const [k, v] of new URLSearchParams(window.top.location.search)) out.set(k, v); } catch { /* another origin */ } + return out; + } + function shareURL(id, inKiosk) { + let u; + try { u = new URL(window.top.location.href); } catch { u = new URL(location.href); } + u.hash = ""; u.search = ""; + u.searchParams.set("ds", id); + if (inKiosk) u.searchParams.set("ds_kiosk", "1"); + return u.href; + } + async function shareDialog() { + const draw = (inKiosk) => { + const url = shareURL(current.id, inKiosk); + $("#share-url").value = url; + $("#share-qr").innerHTML = window.QR.svg(url, { label: tr("QR code for {0}", current.name) }); + }; + const done = openModal(`

${tr("Share “{0}”", esc(current.name))}

+

${tr("Open this link or scan the code on another device that is signed in to Home Assistant.")}

+ + + + `, + async (e, close) => { + const x = e.target.closest("[data-x]")?.dataset.x; + if (x === "done") close(null); + if (x === "copy") { + try { await navigator.clipboard.writeText($("#share-url").value); toast(tr("Link copied.")); } + catch { $("#share-url").select(); document.execCommand?.("copy"); toast(tr("Link copied.")); } + } + }); + draw(false); + $("#share-kiosk").onchange = (e) => draw(e.target.checked); + await done; + } + + // ------------------------------------------------------------ import from Home Assistant + // Turns the cards of an existing Home Assistant dashboard into widgets, one tab per view. + const ent = (x) => (typeof x === "string" ? x : x?.entity) || ""; + function convertCard(card, out) { + const t = String(card.type || "").replace(/^custom:/, ""); + const e = card.entity || ""; + const ents = (card.entities || []).map(ent).filter(Boolean); + const add = (type, cfg, w, h) => out.push({ type, cfg, w, h }); + switch (t) { + case "vertical-stack": case "horizontal-stack": case "grid": (card.cards || []).forEach((c) => convertCard(c, out)); return; + case "entities": case "glance": if (ents.length) add("list", { title: card.title || "", entities: ents }, 4, Math.min(6, 1 + ents.length)); return; + case "light": add("light", { entity: e, name: card.name || "" }, 3, 2); return; + case "thermostat": case "climate": add("climate", { entity: e, name: card.name || "" }, 3, 3); return; + case "weather-forecast": add("forecast", { entity: e }, 6, 4); return; + case "media-control": add("media", { entities: [e] }, 4, 3); return; + case "markdown": add("text", { text: String(card.content || "").replace(/^#+\s*/gm, "").replace(/\*\*/g, "") }, 4, 2); return; + case "gauge": add("gauge", { entity: e, name: card.name || "", min: card.min ?? 0, max: card.max ?? 100 }, 3, 3); return; + case "history-graph": case "statistics-graph": case "sensor-graph": if (ents.length || e) add("graph", { title: card.title || "", entities: ents.length ? ents : [e], hours: card.hours_to_show || 24 }, 6, 3); return; + case "sensor": add("value", { entity: e, name: card.name || "" }, 3, 2); return; + case "picture-entity": case "picture-glance": { const cam = card.camera_image || (domainOf(e) === "camera" ? e : ""); if (cam) add("camera", { entity: cam }, 4, 3); return; } + case "calendar": if (ents.length) add("calendar", { entities: ents }, 4, 5); return; + case "todo-list": case "shopping-list": add("todo", { entity: e || "todo.shopping_list" }, 4, 4); return; + case "alarm-panel": add("alarm", { entity: e }, 4, 5); return; + case "area": add("room", { area: card.area || "" }, 4, 3); return; + case "tile": case "button": case "entity": case "mushroom-entity-card": case "mushroom-light-card": + if (domainOf(e) === "light") add("light", { entity: e, name: card.name || "" }, 3, 2); + else if (domainOf(e) === "climate") add("climate", { entity: e, name: card.name || "" }, 3, 3); + else if (e) add("entity", { entity: e, name: card.name || "" }, 3, 2); + return; + default: + if (e) add("entity", { entity: e, name: card.name || "" }, 3, 2); + else if (ents.length) add("list", { title: card.title || "", entities: ents }, 4, Math.min(6, 1 + ents.length)); + else out.skipped = (out.skipped || 0) + 1; + } + } + function convertLovelace(config, name) { + const d = newDashboard(name); + d.tabs = []; + let skipped = 0; + for (const view of config.views || []) { + const tabW = { ...newTab(view.title || tr("Tab {0}", d.tabs.length + 1), String(view.icon || "").replace(/^mdi:/, "")), widgets: [] }; + if (!Icons.has(tabW.icon)) tabW.icon = "tile"; + const cards = [...(view.cards || []), ...(view.sections || []).flatMap((sec) => sec.cards || [])]; + const out = []; + cards.forEach((c) => convertCard(c, out)); + skipped += out.skipped || 0; + for (const o of out) { + if (!Widgets[o.type]) continue; + const wd = makeWidget(o.type); + Object.assign(wd.cfg, o.cfg); + wd.w = o.w; wd.h = o.h; + Object.assign(wd, findSpot(wd.w, wd.h, tabW.widgets, d.grid.cols)); + tabW.widgets.push(wd); + } + d.tabs.push(tabW); + } + if (!d.tabs.length) d.tabs.push(newTab(tr("Overview"))); + d.nav.show = d.tabs.length > 1; + return { d, skipped }; + } + async function lovelaceDialog() { + if (!ha.connected) { toast(tr("Not connected to Home Assistant. Retrying…"), true); return; } + const list = await ha.send({ type: "lovelace/dashboards/list" }).catch(() => []); + const opts = [{ url_path: "", title: tr("Overview") }, ...(list || [])]; + const pick = await openModal(`

${tr("Import from Home Assistant")}

+

${tr("Choose one of your Home Assistant dashboards. Its cards become widgets in a new dashboard here; the original stays as it is.")}

+ + `, + (e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x === "ok" ? { path: $("#ll-pick").value, title: $("#ll-pick").selectedOptions[0].text } : null); }); + if (!pick) return; + let config; + try { config = await ha.send({ type: "lovelace/config", url_path: pick.path || null }); } + catch (e) { toast(tr("Couldn’t load that dashboard: {0}", e.message), true); return; } + if (config?.strategy && !config.views) { toast(tr("That dashboard is generated by Home Assistant and has no cards of its own to import."), true); return; } + const { d, skipped } = convertLovelace(config || {}, pick.title); + store.dashboards.push(d); + scheduleSave(); + setCurrent(d.id); + setEdit(true); + const n = d.tabs.reduce((a, t) => a + t.widgets.length, 0); + toast(skipped ? tr("Imported {0} widgets. {1} cards couldn’t be converted.", n, skipped) : tr("Imported {0} widgets.", n)); + } + + // ------------------------------------------------------------ templates + // Ready-made layouts, filled with whatever matching devices this Home Assistant has. + function templateBuilder(name, { mobile = false } = {}) { + const d = newDashboard(name); + if (mobile) Object.assign(d, { mobile: true, grid: { ...d.grid, cols: 4, maxWidth: 440 } }); + const cols = d.grid.cols, ws = d.tabs[0].widgets; + const ids = Object.keys(ha.states); + const of = (dom, test = () => true) => ids.filter((e) => domainOf(e) === dom && test(ha.states[e])); + const add = (type, w, h, cfg = {}) => { + const wd = makeWidget(type); + Object.assign(wd.cfg, cfg); + wd.w = Math.min(w, cols); wd.h = h; + Object.assign(wd, findSpot(wd.w, wd.h, ws, cols)); + ws.push(wd); + return wd; + }; + const dc = (s, ...c) => c.includes(s.attributes.device_class); + return { d, add, of, dc, ids }; + } + const TEMPLATES = { + wall: { icon: "kiosk", name: "Wall tablet", desc: "Clock, weather, calendar, rooms and cameras for a screen on the wall.", build() { + const { d, add, of } = templateBuilder(tr("Wall tablet")); + Object.assign(d.theme, PRESETS.Dark, { preset: "Dark" }); + d.kiosk.saver = 10; + const weather = of("weather")[0]; + add("clock", 4, 3, { weather: weather || "", date: true }); + if (weather) add("forecast", 8, 3, { entity: weather, show: "both" }); + const cals = of("calendar"); + if (cals.length) add("calendar", 4, 6, { entities: cals }); + for (const a of (ha.reg?.areas || []).slice(0, 4)) add("room", 4, 3, { area: a.area_id }); + const cam = of("camera")[0]; + if (cam) add("camera", 4, 3, { entity: cam }); + const players = of("media_player").slice(0, 4); + if (players.length) add("media", 4, 3, { entities: players }); + return d; + } }, + phone: { icon: "phone", name: "Phone", desc: "A 4-column layout for your phone: people, lights, climate and media.", build() { + const { d, add, of } = templateBuilder(tr("Phone"), { mobile: true }); + add("heading", 4, 1, { text: tr("Home"), subtitle: "", size: 26 }); + const people = of("person"); + if (people.length) add("people", 4, 2, { entities: people.slice(0, 4) }); + for (const e of of("light", (s) => !s.attributes.entity_id).slice(0, 4)) add("entity", 2, 2, { entity: e }); + const clim = of("climate")[0]; + if (clim) add("climate", 4, 3, { entity: clim }); + const weather = of("weather")[0]; + if (weather) add("weather", 4, 2, { entity: weather }); + const garage = of("cover", (s) => /garage|gate|poort|hek/i.test(s.entity_id + friendly(s)))[0]; + if (garage) add("gate", 2, 3, { entity: garage }); + const players = of("media_player").slice(0, 3); + if (players.length) add("media", 4, 3, { entities: players }); + return d; + } }, + energy: { icon: "bolt", name: "Energy", desc: "Power flow, solar forecast, water and gas use and history graphs.", build() { + const { d, add, of, dc } = templateBuilder(tr("Energy")); + add("heading", 12, 1, { text: tr("Energy"), subtitle: "" }); + const power = of("sensor", (s) => dc(s, "power")); + const find = (re) => power.find((e) => re.test(e)); + const solar = find(/solar|pv|zon/i), grid = find(/grid|net|p1/i), battery = find(/battery|accu/i); + if (solar || grid) add("energy", 6, 5, { solar: solar || "", grid: grid || "", battery: battery || "" }); + const today = of("sensor").find((e) => /energy_production_today/.test(e)); + if (today) add("solar", 6, 5, { today, ...W_AUTOFILL("solar", today) }); + for (const e of of("sensor", (s) => dc(s, "water", "gas") && /total/.test(s.attributes.state_class || "")).slice(0, 2)) + add("usage", 4, 3, { entity: e, kind: ha.states[e].attributes.device_class === "gas" ? "gas" : "water" }); + if (power.length) add("graph", 12, 3, { entities: power.slice(0, 3), title: tr("Power, last 24 hours") }); + return d; + } }, + family: { icon: "people", name: "Family", desc: "Who’s home, the family calendar, shopping list, parcels and the weather.", build() { + const { d, add, of } = templateBuilder(tr("Family")); + add("clock", 4, 2, { date: true, weather: of("weather")[0] || "" }); + const people = of("person"); + if (people.length) add("people", 8, 2, { entities: people }); + const cals = of("calendar"); + if (cals.length) add("calendar", 4, 6, { entities: cals }); + const todo = of("todo")[0]; + if (todo) add("todo", 4, 5, { entity: todo }); + const weather = of("weather")[0]; + if (weather) add("forecast", 4, 4, { entity: weather, show: "daily" }); + const parcels = of("sensor", (s) => Object.values(s.attributes).some((v) => Array.isArray(v) && typeof v[0] === "object" && /parcel|delivery|postnl|dhl|pakket/i.test(s.entity_id))); + if (parcels.length) add("parcels", 4, 4, { entities: parcels }); + const waste = of("sensor", (s) => /afvalwijzer/i.test(s.entity_id)); + if (waste.length) add("waste", 4, 3, {}); + add("notifications", 4, 3, {}); + return d; + } }, + }; + const W_AUTOFILL = (type, eid) => Object.fromEntries(Object.entries(Widgets[type].autofill?.fill(eid, ha) || {}).filter(([, v]) => v)); + + /** New dashboard: a name and where to start from. */ + async function templateDialog() { + const opts = [["empty", "tile", "Empty", "Start with a blank grid."], ...Object.entries(TEMPLATES).map(([k, t]) => [k, t.icon, t.name, t.desc]), ["devices", "sparkles", "From my devices", "A mix of your lights, sensors and devices."]]; + return openModal(`

${tr("New dashboard")}

+
+
${opts.map(([k, ic, n, desc], i) => ``).join("")}
+
`, + (e, close) => { if (e.target.closest("[data-x='cancel']")) close(null); }); + } + document.addEventListener("submit", (e) => { + if (e.target.id !== "tpl-form") return; + e.preventDefault(); + const name = $("#tpl-name").value.trim(), template = e.target.querySelector("input[name=tpl]:checked")?.value || "empty"; + if (name && template !== "devices" && template !== "empty" && !ha.connected) { toast(tr("Not connected to Home Assistant. Retrying…"), true); return; } + if (name) modalClose?.({ name, template }); + }); + function starterDashboard() { const d = newDashboard(tr("Home")); const ids = Object.keys(ha.states); @@ -295,7 +626,7 @@ const isSel = (id) => id === selectedId || multi.has(id); let activeTabId = null; /** The tab whose widgets are on screen. */ - const tab = () => current.tabs.find((t) => t.id === activeTabId) || current.tabs[0]; + const tab = () => (guestOn && !editMode ? guestTab() : current.tabs.find((t) => t.id === activeTabId) || current.tabs[0]); /** Cards inside a container card (scroll, swipe) live in its cfg.children. */ let childIndex = null; function findWidget(id, list = tab().widgets) { @@ -311,6 +642,7 @@ /** What the inspector edits: a card inside the selected container, or the selected widget. */ const editTarget = () => { const w = selectedWidget(); return (childIndex != null && w?.cfg.children?.[childIndex]) || w; }; const undoStack = [], redoStack = []; + const undoAt = [], redoAt = []; // when each step was made, for the undo history list let lastMutKey = null, lastMutAt = 0; const caches = {}; // per-widget runtime cache (graph history) @@ -343,8 +675,9 @@ const now = Date.now(); if (!(key && key === lastMutKey && now - lastMutAt < 1200)) { undoStack.push(JSON.stringify(current)); - if (undoStack.length > 100) undoStack.shift(); - redoStack.length = 0; + undoAt.push(now); + if (undoStack.length > 100) { undoStack.shift(); undoAt.shift(); } + redoStack.length = redoAt.length = 0; } lastMutKey = key; lastMutAt = now; fn(); @@ -361,8 +694,51 @@ lastMutKey = null; scheduleSave(); updateUndoButtons(); renderAll(); } - function undo() { if (undoStack.length) { redoStack.push(JSON.stringify(current)); restore(undoStack.pop()); } } - function redo() { if (redoStack.length) { undoStack.push(JSON.stringify(current)); restore(redoStack.pop()); } } + function undo() { if (undoStack.length) { redoStack.push(JSON.stringify(current)); redoAt.push(undoAt.pop()); restore(undoStack.pop()); } } + function redo() { if (redoStack.length) { undoStack.push(JSON.stringify(current)); undoAt.push(redoAt.pop()); restore(redoStack.pop()); } } + + /** What changed between two versions of a dashboard, in a few words. */ + function describeChange(a, b) { + const all = (d) => { const m = new Map(); const walk = (ws) => ws.forEach((w) => { m.set(w.id, w); walk(w.cfg?.children || []); }); d.tabs.forEach((t) => walk(t.widgets)); return m; }; + const wa = all(a), wb = all(b), name = (w) => tr(Widgets[w.type]?.label || w.type); + const added = [...wb.values()].filter((w) => !wa.has(w.id)), gone = [...wa.values()].filter((w) => !wb.has(w.id)); + const several = (list, one, many) => (list.length === 1 ? tr(one, name(list[0])) : tr(many, list.length)); + if (added.length && gone.length) return tr("Moved widgets around"); + if (added.length) return several(added, "Added {0}", "Added {0} widgets"); + if (gone.length) return several(gone, "Deleted {0}", "Deleted {0} widgets"); + const same = (x, y) => JSON.stringify(x) === JSON.stringify(y); + const changed = [...wb.values()].filter((w) => !same(w, wa.get(w.id))); + if (changed.length) { + const kind = (w) => { const o = wa.get(w.id); return o.x !== w.x || o.y !== w.y ? (o.w !== w.w || o.h !== w.h ? "size" : "move") : o.w !== w.w || o.h !== w.h ? "size" : "edit"; }; + const k = kind(changed[0]); + if (k === "move") return several(changed, "Moved {0}", "Moved {0} widgets"); + if (k === "size") return several(changed, "Resized {0}", "Resized {0} widgets"); + return several(changed, "Edited {0}", "Edited {0} widgets"); + } + if (!same(a.tabs.map((t) => [t.id, t.name, t.icon]), b.tabs.map((t) => [t.id, t.name, t.icon])) || !same(a.nav, b.nav)) return tr("Changed the tabs"); + if (!same(a.theme, b.theme)) return tr("Changed the look"); + if (!same(a.grid, b.grid) || a.mobile !== b.mobile) return tr("Changed the grid"); + if (!same(a.kiosk, b.kiosk)) return tr("Changed the kiosk settings"); + if (!same(a.guest, b.guest)) return tr("Changed guest mode"); + if (a.name !== b.name) return tr("Renamed the dashboard"); + return tr("Changed the dashboard"); + } + + /** The changes made in this session, newest first; picking one goes back to just after it. */ + async function undoHistoryDialog() { + if (!undoStack.length) { toast(tr("No changes yet in this session.")); return; } + const states = [...undoStack, JSON.stringify(current)]; + const rows = undoStack.map((snap, i) => ({ i, label: describeChange(JSON.parse(snap), JSON.parse(states[i + 1])), at: undoAt[i] })).reverse(); + const pick = await openModal(`

${tr("Undo history")}

${tr("Pick a change to go back to just after it. Redo brings the later ones back.")}

+
${rows.map((r, n) => ``).join("")} +
+ `, + (e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x === "cancel" ? null : x); }); + if (pick == null) return; + const steps = undoStack.length - (Number(pick) + 1); + for (let i = 0; i < steps; i++) undo(); + } function updateUndoButtons() { $("#btn-undo").disabled = !undoStack.length; $("#btn-redo").disabled = !redoStack.length; @@ -371,8 +747,20 @@ // ------------------------------------------------------------ rendering const VAR_FOR = { bg: "--d-bg", tile: "--d-tile", text: "--d-text", text2: "--d-text-2", accent: "--d-accent", onAccent: "--d-on-accent", separator: "--d-separator", fill: "--d-fill", lit: "--d-lit" }; + /** A theme's light or dark twin, built from its accent, for themes set to follow the device. */ + function themeVariant(t) { + if (!t.follow || t.preset === "System" || t.preset === "Custom" || !t.bg || !/^#[0-9a-f]{6}$/i.test(t.bg)) return t; + const lum = (hex) => { const n = parseInt(hex.slice(1), 16); return (0.299 * (n >> 16) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) / 255; }; + const isDark = lum(t.bg) < 0.45, wantDark = prefersDark(); + if (isDark === wantDark) return t; + const a = t.accent || "#0a84ff"; + return wantDark + ? { ...t, bg: `color-mix(in srgb, ${a} 7%, #0f0f11)`, tile: `color-mix(in srgb, ${a} 9%, #1c1c1e)`, text: "#f5f5f7", text2: "#a1a1a6", separator: "rgba(84,84,88,0.55)", fill: "rgba(118,118,128,0.24)", lit: "#ffe2a8", onAccent: "#ffffff", shadow: false } + : { ...t, bg: `color-mix(in srgb, ${a} 7%, #f2f2f7)`, tile: `color-mix(in srgb, ${a} 3%, #ffffff)`, text: "#1d1d1f", text2: "#6e6e73", separator: "rgba(60,60,67,0.16)", fill: "rgba(120,120,128,0.14)", lit: "#ffedc7", onAccent: "#ffffff", shadow: true }; + } + function applyTheme() { - const t = nightDark ? { ...current.theme, ...PRESETS.Dark, preset: "Dark" } : current.theme, s = stageEl.style; + const t = themeVariant(nightDark ? { ...current.theme, ...PRESETS.Dark, preset: "Dark" } : current.theme), s = stageEl.style; stageEl.classList.remove("surface-cards", "surface-outline", "surface-plain"); stageEl.classList.add(`surface-${t.surface || "cards"}`); for (const k of COLOR_KEYS) { @@ -382,6 +770,7 @@ const bg = t.preset !== "System" && t.bg ? t.bg : "var(--canvas)"; if (t.bgImage) s.setProperty("--d-bg", `center / cover no-repeat url("${t.bgImage.replace(/"/g, "")}"), ${bg}`); s.setProperty("--d-radius", `${t.radius ?? 20}px`); + document.querySelector('meta[name="theme-color"]')?.setAttribute("content", /^#[0-9a-f]{3,8}$/i.test(t.bg || "") && t.preset !== "System" ? t.bg : prefersDark() ? "#000000" : "#f2f2f7"); s.setProperty("--d-gap", `${Number(current.grid.gap) || 14}px`); stageEl.classList.toggle("theme-outline", !!t.outline && t.preset !== "System"); s.setProperty("--d-blur", `${t.blur || 0}px`); @@ -393,13 +782,14 @@ const g = current.grid; const pad = window.innerWidth < 560 ? 12 : 20; const avail = canvasEl.clientWidth - pad * 2; - const width = g.maxWidth ? Math.min(avail, Number(g.maxWidth)) : avail; + const landscape = !editMode && !!current.mobile && phoneLandscape.matches; + const width = landscape ? Math.min(avail, 900) : g.maxWidth ? Math.min(avail, Number(g.maxWidth)) : avail; // Phone dashboards are drawn exactly as designed; others reflow into two columns on narrow screens. const compact = !editMode && width < 560 && !current.mobile; - const cols = compact ? 2 : Math.max(1, Number(g.cols) || 12); + const cols = compact ? 2 : landscape ? (Number(g.cols) || 4) * 2 : Math.max(1, Number(g.cols) || 12); const gap = compact ? Math.min(Number(g.gap) || 0, 12) : Number(g.gap) || 0; const rh = Number(g.rowHeight) || 72; - metrics = { width, cols, gap, rh, colW: (width - gap * (cols - 1)) / cols, compact }; + metrics = { width, cols, gap, rh, colW: (width - gap * (cols - 1)) / cols, compact, landscape }; gridEl.style.width = `${width}px`; gridEl.style.margin = `${editMode ? 4 : 8}px auto ${pad}px`; } @@ -419,6 +809,8 @@ } } function isVisible(w) { + const scr = w.visibility?.screen, phone = matchMedia("(max-width: 600px)").matches; + if ((scr === "phone" && !phone) || (scr === "large" && phone)) return false; if (Widgets[w.type]?.autoHide?.(w.cfg, ctxFor(w))) return false; const rules = (w.visibility?.rules || []).filter((r) => r.entity); if (!rules.length) return true; @@ -430,6 +822,15 @@ function positions() { const map = {}; const shown = editMode ? tab().widgets : tab().widgets.filter(isVisible); + if (metrics.landscape) { // reading order, packed into the double-width grid + const placed = []; + for (const w of [...shown].sort((a, b) => a.y - b.y || a.x - b.x)) { + const p = { ...findSpot(w.w, w.h, placed, metrics.cols), w: w.w, h: w.h }; + map[w.id] = p; + placed.push(p); + } + return map; + } if (!metrics.compact) { if (editMode) { for (const w of shown) map[w.id] = w; return map; } // Rows that only hidden widgets used close up, so nothing leaves a hole. @@ -476,7 +877,7 @@ /** Side menu with the dashboard's tabs (a bottom tab bar on phones). */ function renderNav() { - const show = current.nav.show || (editMode && current.tabs.length > 1); + const show = !(guestOn && !editMode) && (current.nav.show || (editMode && current.tabs.length > 1)); document.body.classList.toggle("has-tabs", show); navEl.hidden = !show; if (!show) return; @@ -521,9 +922,11 @@ } const phoneScreen = matchMedia("(max-width: 600px)"); + // A phone on its side: wide but low. Phone dashboards then use two columns of their layout side by side. + const phoneLandscape = matchMedia("(orientation: landscape) and (max-height: 500px)"); function renderGrid() { if (!current) return; - document.body.classList.toggle("phone-ui", !!current.mobile && !editMode && phoneScreen.matches); + document.body.classList.toggle("phone-ui", !!current.mobile && !editMode && (phoneScreen.matches || phoneLandscape.matches)); renderNav(); applyTheme(); computeMetrics(); @@ -595,10 +998,13 @@ let interacting = false; function onStateChange(entityId) { + if (ha.connected) { lastLive = Date.now(); cacheStates(); } if (livePopup && livePopup.eid === entityId) livePopup.draw(); // an open climate popup follows the device if (!current) return; if (entityId === current.kiosk.doorbell || entityId == null) checkDoorbell(); if (entityId === "sun.sun" || entityId == null) applyNight(); + if (entityId === current.guest?.entity || entityId == null) checkGuest(); + if (entityId == null || entityId === awayEntity()) checkAway(); if (entityId == null) { for (const c of Object.values(caches)) c.key = null; renderGrid(); if (editMode) renderInspector(); return; } const affected = tab().widgets.filter((w) => entitiesOf(w).includes(entityId) || ruleEntities(w).includes(entityId)); if (affected.some((w) => isVisible(w) !== lastVisible[w.id])) { renderGrid(); return; } @@ -617,14 +1023,76 @@ $("#dash-select").innerHTML = store.dashboards.map((d) => ``).join(""); } + // ------------------------------------------------------------ suggestions + // Devices that aren't on any dashboard yet, each with the widget that suits it. + const SUGGEST = { light: "light", climate: "climate", cover: "covers", media_player: "media", camera: "camera", lock: "entity", switch: "entity", fan: "entity", + vacuum: "vacuum", alarm_control_panel: "alarm", todo: "todo", input_boolean: "entity", scene: "entity", weather: "forecast" }; + function suggestions() { + const used = new Set(); + for (const d of store.dashboards) for (const t of d.tabs) for (const w of t.widgets) { entitiesOf(w).forEach((e) => used.add(e)); ruleEntities(w).forEach((e) => used.add(e)); } + const hidden = new Set(); + // entities Home Assistant hides, and lights that are part of a group already on a dashboard + for (const st0 of Object.values(ha.states)) if (Array.isArray(st0.attributes.entity_id) && used.has(st0.entity_id)) st0.attributes.entity_id.forEach((e) => hidden.add(e)); + return Object.values(ha.states).filter((x) => SUGGEST[domainOf(x.entity_id)] && !used.has(x.entity_id) && !hidden.has(x.entity_id) && x.state !== "unavailable") + .sort((a, b) => domainOf(a.entity_id).localeCompare(domainOf(b.entity_id)) || friendly(a).localeCompare(friendly(b))); + } + async function suggestionsDialog() { + const list = suggestions(); + if (!list.length) { toast(tr("Every device is on a dashboard already.")); return; } + const draw = () => suggestions().map((x) => `
${window.HAFmt.icon(x)}${esc(friendly(x))}${esc(x.entity_id)} · ${esc(tr(Widgets[SUGGEST[domainOf(x.entity_id)]].label))}
`).join("") || `
${tr("Every device is on a dashboard already.")}
`; + await openModal(`

${tr("Not on a dashboard yet")}

${tr("These devices aren’t on any of your dashboards. Add one to this tab with the widget that suits it.")}

+
${draw()}
+ `, + (e, close) => { + const b = e.target.closest("[data-x]"); + if (!b) return; + if (b.dataset.x === "done") return close(null); + const eid = b.dataset.e, type = SUGGEST[domainOf(eid)]; + const wd = makeWidget(type); + if (Widgets[type].fields.some((f) => f.k === "entities")) wd.cfg.entities = [eid]; else wd.cfg.entity = eid; + wd.w = current.mobile ? (wd.w >= 3 ? 4 : 2) : Math.min(wd.w, current.grid.cols); + Object.assign(wd, findSpot(wd.w, wd.h)); + mutate(() => tab().widgets.push(wd)); + $(".sg-list").innerHTML = draw(); + renderPalette(); + }); + } + function renderPalette() { const search = $("#palette-search"), q = search.value.trim().toLowerCase(); search.placeholder = tr("Search widgets"); const items = Object.entries(Widgets).filter(([type, d]) => !q || [type, d.label, tr(d.label)].some((x) => x.toLowerCase().includes(q))); - $("#palette-list").innerHTML = items.length - ? items.map(([type, d]) => ``).join("") + const presets = (store.presets || []).filter((p) => !q || p.name.toLowerCase().includes(q)); + const presetHTML = presets.length ? `
${tr("My presets")}
${presets.map((p) => `
`).join("")}
${tr("Widgets")}
` : ""; + const nSug = ha.connected && store.dashboards?.length ? suggestions().length : 0; + const sugHTML = nSug && !q ? `` : ""; + $("#palette-list").innerHTML = items.length || presets.length + ? sugHTML + presetHTML + items.map(([type, d]) => ``).join("") : `

${tr("No widgets match “{0}”.", esc(search.value.trim()))}

`; } + + /** A set-up widget saved under a name, to add again on any dashboard. Kept with the dashboards. */ + async function savePreset(id) { + const w = findWidget(id); + if (!w) return; + const name = await askText(tr("Save as preset"), w.cfg.name || w.cfg.title || tr(Widgets[w.type].label), tr("Save")); + if (!name) return; + const { x, y, ...rest } = clone(w); + (store.presets ||= []).push({ id: uid(), name, widget: rest }); + scheduleSave(); + renderPalette(); + toast(tr("Saved “{0}”. You’ll find it at the top of the widget list.", name)); + } + function addPreset(pid) { + const p = (store.presets || []).find((x) => x.id === pid); + if (!p) return; + const wd = { ...clone(p.widget), id: uid() }; + (wd.cfg.children || []).forEach((ch) => (ch.id = uid())); + wd.w = Math.min(wd.w, current.grid.cols); + Object.assign(wd, findSpot(wd.w, wd.h)); + mutate(() => tab().widgets.push(wd)); + select(wd.id); + } $("#palette-search").addEventListener("input", renderPalette); $("#palette-search").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#palette-list .pal-item")?.click(); }); @@ -720,6 +1188,7 @@ function renderInspector() { const el = $("#inspector"); + updateEditBar(); if (multi.size > 1) { const btn = (k, l) => ``; el.innerHTML = `
${tr("{0} widgets selected", multi.size)}
@@ -752,7 +1221,11 @@ ${current.tabs.length > 1 ? fieldHTML({ k: "tab", t: "select", l: "Tab", o: current.tabs.map((t) => t.id), labels: Object.fromEntries(current.tabs.map((t) => [t.id, t.name])) }, tab().id, "move") : ""}
${tr("Style")}
${STYLE_FIELDS.map((f) => fieldHTML(f, w.style?.[f.k], "style")).join("")}
${visibilityHTML(w)} -
+ ${holdHTML(w)} + ${["button", "entity", "light", "gate", "climate"].includes(w.type) ? `
${tr("Home screen")}
+
+

${tr("When Dashboard Studio is installed as an app on Android, long-press its icon to run this widget straight away. Up to four shortcuts.")}

` : ""} +

${tr("Drag a widget onto a scroll or swipe card to put it inside.")}

`; fillAlbums(el); return; @@ -764,6 +1237,9 @@
${tr("Tabs")}
${fieldHTML({ k: "show", t: "check", l: "Show tabs in a side menu" }, current.nav.show, "nav")} ${fieldHTML({ k: "style", t: "seg", l: "Menu style", o: ["icons", "labels"] }, current.nav.style || "icons", "nav")} + ${current.tabs.length > 1 ? `${fieldHTML({ k: "awayTab", t: "select", l: "When you’re away from home, open", o: ["", ...current.tabs.map((t) => t.id)], labels: { "": tr("Nothing special"), ...Object.fromEntries(current.tabs.map((t) => [t.id, t.name])) } }, current.nav.awayTab || "", "nav")} + ${current.nav.awayTab ? fieldHTML({ k: "awayPerson", t: "entity", l: "Away means this person is away", domains: ["person", "device_tracker"], ph: "person." }, current.nav.awayPerson || "", "nav") : ""} + ${current.nav.awayTab ? `

${tr("Leave the person empty to follow whoever is signed in on the device. Not used in kiosk mode.")}

` : ""}` : ""}
${current.tabs.map((t, i) => `
@@ -782,6 +1258,7 @@ }).join("")}

${tr("System follows your device’s light or dark appearance.")}

+ ${t.preset !== "System" && t.preset !== "Custom" ? fieldHTML({ k: "follow", t: "check", l: "Switch to a dark or light version with the device" }, t.follow, "theme") : ""} ${fieldHTML({ k: "surface", t: "seg", l: "Tile style", o: ["cards", "outline", "plain"] }, t.surface, "theme")} ${fieldHTML({ k: "radius", t: "number", l: "Corner radius (px)" }, t.radius, "theme")}
@@ -803,6 +1280,9 @@ ${fieldHTML({ k: "saverInterval", t: "number", l: "Change photo every (seconds)" }, k.saverInterval, "kiosk")}` : ""} ${fieldHTML({ k: "exitCorner", t: "select", l: "Exit corner (tap it 5 times to leave)", o: ["top-right", "top-left", "bottom-right", "bottom-left"] }, k.exitCorner, "kiosk")} ${fieldHTML({ k: "hideCursor", t: "check", l: "Hide the pointer when idle" }, k.hideCursor, "kiosk")} + ${current.tabs.length > 1 ? fieldHTML({ k: "rotate", t: "number", l: "Go to the next tab every (seconds)", ph: "0 = off" }, k.rotate, "kiosk") : ""} + ${fieldHTML({ k: "announce", t: "check", l: "Show announcements from Home Assistant" }, k.announce, "kiosk")} + ${fieldHTML({ k: "clickSound", t: "check", l: "A soft click when something is pressed" }, k.clickSound, "kiosk")} ${fieldHTML({ k: "doorbell", t: "entity", l: "Doorbell", domains: ["binary_sensor", "event", "input_boolean", "switch"] }, k.doorbell, "kiosk")} ${k.doorbell ? `${fieldHTML({ k: "doorbellCamera", t: "entity", l: "Camera to show", domains: ["camera"] }, k.doorbellCamera, "kiosk")}
${fieldHTML({ k: "doorbellSecs", t: "number", l: "Close after (seconds)" }, k.doorbellSecs, "kiosk")}
@@ -811,6 +1291,7 @@

${tr("On a wall tablet, open kiosk in a new tab to hide the Home Assistant sidebar too.")}

+ ${guestHTML()}
${tr("This device")}
${fieldHTML({ k: "lang", t: "select", l: "Language on this device", o: Object.keys(window.I18n.languages), labels: Object.fromEntries(Object.entries(window.I18n.languages).map(([c, n]) => [c, c === "auto" ? tr(n) : n])) }, window.I18n.preference(), "device")} ${nightHTML()} @@ -825,12 +1306,25 @@ fillAlbums(el); } + function guestHTML() { + const g = (current.guest ||= { ...DEFAULT_GUEST }); + return `
${tr("Guest mode")}
+

${tr("A simple screen with only lights and thermostats, for guests or a babysitter.")}

+ ${fieldHTML({ k: "when", t: "seg", l: "Turns on", o: ["manual", "entity", "times"], labels: { manual: tr("With the button"), entity: tr("With an entity"), times: tr("Between times") } }, g.when, "guest")} + ${g.when === "entity" ? fieldHTML({ k: "entity", t: "entity", l: "On while this is on", domains: ["input_boolean", "switch", "schedule", "binary_sensor"] }, g.entity, "guest") : ""} + ${g.when === "times" ? `
${fieldHTML({ k: "from", t: "time", l: "From" }, g.from, "guest")}${fieldHTML({ k: "to", t: "time", l: "Until" }, g.to, "guest")}
` : ""} + ${fieldHTML({ k: "title", t: "text", l: "Welcome text", ph: tr("Welcome") }, g.title, "guest")} + ${fieldHTML({ k: "entities", t: "entities", l: "Lights and thermostats (leave empty for all)", domains: ["light", "climate"] }, g.entities, "guest")} +
`; + } + function nightHTML() { const n = nightCfg(); return `${fieldHTML({ k: "mode", t: "seg", l: "Night mode", o: ["off", "sunset", "times"], labels: { off: tr("Off"), sunset: tr("After sunset"), times: tr("Between times") } }, n.mode, "night")} ${n.mode === "times" ? `
${fieldHTML({ k: "from", t: "time", l: "From" }, n.from, "night")}${fieldHTML({ k: "to", t: "time", l: "Until" }, n.to, "night")}
` : ""} ${n.mode !== "off" ? `${fieldHTML({ k: "dim", t: "number", l: "Dim the screen (%)" }, n.dim, "night")}${fieldHTML({ k: "dark", t: "check", l: "Switch to the dark theme" }, n.dark, "night")}` : ""} -

${tr("Night mode is saved on this device only, and is off while editing.")}

`; +

${tr("Night mode is saved on this device only, and is off while editing.")}

+
`; } /** Album pickers load their list from the server after the inspector is drawn. */ @@ -892,6 +1386,15 @@ renderInspector(); } + /** What touching and holding a widget does: its details by default, or a second action. */ + function holdHTML(w) { + if (Widgets[w.type]?.container) return ""; + const h = w.hold || {}; + return `
${tr("Touch and hold")}
+ ${fieldHTML({ k: "action", t: "select", l: "Touch and hold to", o: ["details", "toggle", "service", "none"], labels: { details: tr("Show the details"), toggle: tr("Switch on or off"), service: tr("Run an action"), none: tr("Do nothing") } }, h.action || "details", "hold")} + ${h.action === "service" ? `${fieldHTML({ k: "service", t: "text", l: "Action (e.g. scene.turn_on)", ph: "script.good_night" }, h.service, "hold")}${fieldHTML({ k: "entity", t: "entity", l: "Target entity (optional)" }, h.entity, "hold")}${fieldHTML({ k: "data", t: "json", l: "Action data (JSON)" }, h.data, "hold")}` : ""}
`; + } + function visibilityHTML(w) { const vis = w.visibility || { mode: "all", rules: [] }; const common = ["on", "off", "running", "idle", "playing", "paused", "home", "not_home", "unavailable"]; @@ -913,6 +1416,7 @@ ? `

${isVisible(w) ? Icons.svg("eye") + tr("Visible right now") : Icons.svg("eye-off") + tr("Hidden right now")}

` : ""; return `
${tr("Visibility")}

${tr("Show this widget only when the conditions below are met. Leave empty to always show it.")}

+ ${fieldHTML({ k: "screen", t: "seg", l: "Show on", o: ["all", "phone", "large"], labels: { all: tr("All screens"), phone: tr("Phones"), large: tr("Larger screens") } }, vis.screen || "all", "vis")} ${vis.rules.length > 1 ? fieldHTML({ k: "mode", t: "seg", l: "Show when", o: ["all", "any"] }, vis.mode || "all", "vis") : ""} ${rows}${status}
`; @@ -927,6 +1431,13 @@ if (isChange && k === "icon") renderInspector(); return; } + if (path === "hold") { + const w = selectedWidget(); + if (t.dataset.json && v) { try { JSON.parse(v); t.classList.remove("invalid"); } catch { t.classList.add("invalid"); return; } } + mutate(() => { (w.hold ||= {})[k] = v; }, `${w.id}|hold|${k}`, { rerender: false }); + if (isChange && k === "action") renderInspector(); + return; + } if (path === "vis" || path === "rule") { const w = tab().widgets.find((x) => x.id === selectedId); mutate(() => { @@ -940,6 +1451,7 @@ if (path === "layout") { convertLayout(!!v); renderInspector(); return; } if (path === "nav") { mutate(() => { current.nav[k] = v; }); + if (k === "awayTab" || k === "awayPerson") { awayState = null; if (isChange) renderInspector(); } return; } if (path === "move") { @@ -954,6 +1466,14 @@ toast(tr("Moved to “{0}”.", target.name)); return; } + if (path === "guest") { + const g = current.guest; + if (idx === "new") { if (!isChange || !v) return; mutate(() => { (g[k] ||= []).push(v); }, null, { rerender: false }); } + else mutate(() => { if (idx != null) g[k][Number(idx)] = v; else g[k] = v; }, `guest|${k}|${idx ?? ""}`, { rerender: false }); + checkGuest(true); + if (isChange || t.tagName === "BUTTON") renderInspector(); + return; + } if (path === "night") { const n = nightCfg(); n[k] = v; @@ -1028,6 +1548,13 @@ function onInspectorInput(e) { const t = e.target; + if (t.matches?.("[data-haptics]")) { if (e.type === "change") { lsSet("ds.haptics", t.checked ? "1" : "0"); haptic(); } return; } + if (t.matches?.("[data-shortcut]")) { + if (e.type !== "change") return; + const w = selectedWidget(), s0 = ha.states[w.cfg.entity]; + mutate(() => { if (t.checked) w.shortcut = w.cfg.name || w.cfg.label || (s0 ? friendly(s0) : tr(Widgets[w.type].label)); else delete w.shortcut; }, null, { rerender: false }); + return; + } const albums = t.closest?.(".albums"); if (albums) { if (e.type !== "change") return; @@ -1094,12 +1621,28 @@ return { x: 0, y: 0 }; } + // On a phone, the widget list and the settings are sheets that slide up from the bottom. + const phoneEditing = () => editMode && matchMedia("(max-width: 640px)").matches; + function openSheet(name) { + document.body.classList.toggle("sheet-palette", name === "palette"); + document.body.classList.toggle("sheet-inspector", name === "inspector"); + $("#sheet-backdrop").hidden = !name; + } + function updateEditBar() { + $("#eb-insp").innerHTML = `${Icons.svg(selectedId ? "edit" : "sliders")}${esc(selectedId ? tr("Edit") : tr("Settings"))}`; + } + $("#eb-add").onclick = () => openSheet("palette"); + $("#eb-insp").onclick = () => openSheet("inspector"); + $("#sheet-backdrop").onclick = () => openSheet(null); + document.querySelectorAll(".sheet-close").forEach((b) => (b.onclick = () => openSheet(null))); + function addWidget(type) { const wd = makeWidget(type); wd.w = current.mobile ? (wd.w >= 3 ? 4 : 2) : Math.min(wd.w, current.grid.cols); Object.assign(wd, findSpot(wd.w, wd.h)); mutate(() => tab().widgets.push(wd)); select(wd.id); + if (phoneEditing()) openSheet(null); gridEl.querySelector(`.w[data-id="${wd.id}"]`)?.scrollIntoView({ block: "nearest", behavior: "smooth" }); } @@ -1283,6 +1826,8 @@ if (e.target.closest("[data-tool]")) return; const w = tab().widgets.find((x) => x.id === el.dataset.id); if (e.shiftKey || e.metaKey || e.ctrlKey) { select(w.id, true); e.preventDefault(); return; } + // By touch, the first tap selects (so the page can still scroll); a selected widget drags. + if (e.pointerType === "touch" && w.id !== selectedId && !multi.has(w.id)) { select(w.id); return; } const mode = e.target.closest(".w-resize") ? "resize" : "move"; if (multi.has(w.id) && mode === "move") { startGroupDrag(e, el); return; } if (selectedId !== w.id) select(w.id); @@ -1290,7 +1835,7 @@ ph.className = "placeholder"; gridEl.appendChild(ph); placeEl(ph, w); - drag = { w, el, ph, mode, sx: e.clientX, sy: e.clientY, orig: { x: w.x, y: w.y, w: w.w, h: w.h }, next: { x: w.x, y: w.y, w: w.w, h: w.h }, r0: rectFor(w) }; + drag = { w, el, ph, mode, touch: e.pointerType === "touch", sx: e.clientX, sy: e.clientY, orig: { x: w.x, y: w.y, w: w.w, h: w.h }, next: { x: w.x, y: w.y, w: w.w, h: w.h }, r0: rectFor(w) }; el.classList.add("dragging"); el.setPointerCapture(e.pointerId); e.preventDefault(); @@ -1355,6 +1900,7 @@ n.h = Math.max(1, o.h + Math.round(dy / (rh + gap))); } placeEl(drag.ph, n); + showGuides(n, drag.w); if (drag.mode === "move" && !Widgets[drag.w.type]?.container) { const into = containerAt(e.clientX, e.clientY); if (into !== drag.into) { drag.into?.classList.remove("drop-into"); into?.classList.add("drop-into"); drag.into = into; } @@ -1364,7 +1910,19 @@ updateGridHeight(pos); }); + /** Lines where the dragged widget's edges line up with other widgets, like in design apps. */ + function showGuides(n, w) { + gridEl.querySelectorAll(".guide").forEach((g) => g.remove()); + const { colW, gap, rh } = metrics; + const others = tab().widgets.filter((o) => o !== w); + const xs = new Set(others.flatMap((o) => [o.x, o.x + o.w])), ys = new Set(others.flatMap((o) => [o.y, o.y + o.h])); + const line = (cls, px) => { const g = document.createElement("div"); g.className = `guide ${cls}`; g.style[cls === "v" ? "left" : "top"] = `${Math.max(0, px)}px`; gridEl.appendChild(g); }; + for (const x of new Set([n.x, n.x + n.w])) if (xs.has(x)) line("v", x * (colW + gap) - gap / 2); + for (const y of new Set([n.y, n.y + n.h])) if (ys.has(y)) line("h", y * (rh + gap) - gap / 2); + } + function endDrag() { + gridEl.querySelectorAll(".guide").forEach((g) => g.remove()); if (!drag) return; if (drag.group) { const { group, cx, cy } = drag; @@ -1377,7 +1935,7 @@ }); return; } - const { w, ph, el, orig, next, into } = drag; + const { w, ph, el, orig, next, into, touch: touchEnd } = drag; ph.remove(); el.classList.remove("dragging"); into?.classList.remove("drop-into"); @@ -1392,7 +1950,11 @@ toast(tr("Moved into “{0}”.", tr(Widgets[box.type].label))); return; } - if (orig.x === next.x && orig.y === next.y && orig.w === next.w && orig.h === next.h) { placeEl(el, w); return; } + if (orig.x === next.x && orig.y === next.y && orig.w === next.w && orig.h === next.h) { + placeEl(el, w); + if (touchEnd && phoneEditing()) openSheet("inspector"); // tapping the selected widget opens its settings + return; + } mutate(() => { Object.assign(w, next); resolveCollisions(w); }); if (!multi.has(w.id)) select(w.id); } @@ -1414,6 +1976,7 @@ } gridEl.addEventListener("click", (e) => { + if (!editMode && e.target.closest("[data-act]")) haptic(); if (editMode) { const tool = e.target.closest("[data-tool]"); if (tool) { @@ -1432,6 +1995,13 @@ const t = Math.round(((s?.attributes.temperature ?? 20) + Number(a.dataset.v)) * 10) / 10; ha.callService("climate", "set_temperature", { temperature: t }, eid); } else if (act === "hvac") ha.callService("climate", "set_hvac_mode", { hvac_mode: a.dataset.v }, eid); + else if (act === "gate") gateAction(eid, a.dataset.v === "1"); + else if (act === "card-tab") { // a card's own tabs (Formula One): kept in its cache, not saved + const id = a.closest(".w[data-id]")?.dataset.id; + (caches[id] ||= {}).tab = a.dataset.v; + const top = tab().widgets.find((w) => w.id === id || findWidget(id, w.cfg.children || [])); + if (top) renderWidget(top); + } else if (act === "light-color") { const [kind, val] = a.dataset.v.split(":"); ha.callService("light", "turn_on", kind === "k" ? { color_temp_kelvin: Number(val) } : { rgb_color: val.split(",").map(Number) }, eid); @@ -1495,13 +2065,7 @@ else if (act === "sw-go") caches[a.closest(".w:not(.child)").dataset.id]?.go?.(Number(a.dataset.v)); else if (act === "svc") { const [d, s] = a.dataset.v.split("."); ha.callService(d, s, {}, eid); } else if (act === "button") { - const c = findWidget(a.closest(".w").dataset.id).cfg; - let data = {}; - if (c.data) { try { data = JSON.parse(c.data); } catch { toast(tr("The button’s action data isn’t valid JSON. Fix it in the inspector."), true); return; } } - if (c.service && c.service.includes(".")) { const [d, s] = c.service.split("."); ha.callService(d, s, data, c.entity || undefined); } - else if (c.entity) defaultAction(c.entity); - else { toast(tr("This button has no action yet. Set one in the inspector."), true); return; } - a.animate?.([{ transform: "scale(.96)" }, { transform: "scale(1)" }], 160); + if (runButton(findWidget(a.closest(".w").dataset.id).cfg)) a.animate?.([{ transform: "scale(.96)" }, { transform: "scale(1)" }], 160); } }); @@ -1519,6 +2083,7 @@ }); gridEl.addEventListener("change", (e) => { const t = e.target, eid = t.dataset.e; + if (!editMode) haptic(); interacting = false; if (t.dataset.act === "brightness") ha.callService("light", "turn_on", { brightness_pct: Number(t.value) }, eid); if (t.dataset.act === "volume") ha.callService("media_player", "volume_set", { volume_level: Number(t.value) / 100 }, eid); @@ -1561,6 +2126,43 @@ }); /** Like confirmDestructive, for actions that aren't destructive. */ + /** A button widget's action: its own action with data, or the default for its entity. */ + function runButton(c) { + let data = {}; + if (c.data) { try { data = JSON.parse(c.data); } catch { toast(tr("The button’s action data isn’t valid JSON. Fix it in the inspector."), true); return false; } } + if (c.service && c.service.includes(".")) { const [d, sv] = c.service.split("."); ha.callService(d, sv, data, c.entity || undefined); } + else if (c.entity) defaultAction(c.entity); + else { toast(tr("This button has no action yet. Set one in the inspector."), true); return false; } + return true; + } + + /** A home screen shortcut (long-press the app icon): runs a widget's main action, wherever it is. */ + async function runShortcut(id) { + let w = null; + for (const d of store.dashboards) for (const t of d.tabs) w ||= findWidget(id, t.widgets); + if (!w) { toast(tr("That shortcut’s widget no longer exists."), true); return; } + await ha.ready; + const eid = w.cfg.entity || (w.cfg.entities || [])[0]; + if (w.type === "button") { if (!runButton(w.cfg)) return; } + else if (w.type === "gate") await gateAction(eid, w.cfg.confirm !== false); + else if (eid) defaultAction(eid); + else return; + toast(tr("Done: {0}", w.shortcut || w.cfg.name || w.cfg.label || friendly(ha.states[eid] || { entity_id: eid }))); + } + + /** The garage and gate card: open, close or stop, after a check when the card asks for one. */ + async function gateAction(eid, ask) { + const s = ha.states[eid]; + if (!s) return; + const d = domainOf(eid), open = ["open", "opening", "unlocked", "unlocking", "on"].includes(s.state), moving = ["opening", "closing"].includes(s.state); + const verb = d === "button" ? "Press" : moving ? "Stop" : open ? (d === "lock" ? "Lock" : "Close") : d === "lock" ? "Unlock" : "Open"; + if (ask && !(await confirmAction(tr(`${verb} {0}?`, friendly(s)), "", tr(verb)))) return; + if (d === "cover") ha.callService("cover", moving ? "stop_cover" : open ? "close_cover" : "open_cover", {}, eid); + else if (d === "lock") ha.callService("lock", open ? "lock" : "unlock", {}, eid); + else if (d === "button") ha.callService("button", "press", {}, eid); + else ha.callService(d, "toggle", {}, eid); + } + function confirmAction(title, message, label) { return openModal(`

${esc(title)}

${esc(message)}

`, @@ -1890,12 +2492,13 @@ promptOpen = true; const timer = setTimeout(() => modalClose?.(null), PROMPT_MS); const choice = await openModal(`

${tr("Leave kiosk mode?")}

${tr("This closes by itself in a few seconds.")}

- `, + `, (e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x); }); clearTimeout(timer); promptOpen = false; if (choice === "exit") exitKiosk(); else if (choice === "full") toggleFullscreen(false); + else if (choice === "guest") guestOn ? endGuest() : setGuestManual(true); } function inExitCorner(e) { @@ -1988,6 +2591,129 @@ } $("#doorbell").addEventListener("click", (e) => { if (e.target.closest(".db-close")) closeDoorbell(); }); + // ------------------------------------------------------------ announcements + // An automation fires the event dashboard_studio_announce with a message; open screens show it full screen. + let announceTimer = 0; + ha.onAnnounce = (d) => { + if (!current || editMode || current.kiosk.announce === false || !d.message) return; + const me2 = (store.assign?.devices?.[DEVICE_ID]?.name || "").toLowerCase(); + const targets = [].concat(d.device || d.devices || []).map((x) => String(x).toLowerCase()); + if (targets.length && !targets.includes(me2) && !targets.includes(DEVICE_ID.toLowerCase())) return; + const el = $("#announce"); + hideSaver(); + if (modalClose) modalClose(null); + el.querySelector(".an-title").textContent = d.title || tr("Announcement"); + el.querySelector(".an-msg").textContent = d.message; + el.querySelector(".an-ico").innerHTML = Icons.svg(Icons.has(d.icon) ? d.icon : "megaphone"); + el.querySelector(".an-ok").textContent = tr("OK"); + el.hidden = false; + el.querySelector(".an-ok").focus(); + if (d.sound !== false) { window.HAFmt.beep?.(); setTimeout(() => window.HAFmt.beep?.(), 350); } + clearTimeout(announceTimer); + announceTimer = setTimeout(closeAnnounce, Math.max(5, Number(d.seconds) || 60) * 1000); + }; + function closeAnnounce() { + clearTimeout(announceTimer); + $("#announce").hidden = true; + poke(); + } + $("#announce").addEventListener("click", (e) => { if (e.target.closest(".an-ok") || e.target.id === "announce") closeAnnounce(); }); + + // ------------------------------------------------------------ tab rotation + // In kiosk mode the tabs can take turns; touching the screen pauses that for a minute. + let lastRotate = Date.now(), lastTouch = 0; + document.addEventListener("pointerdown", () => { lastTouch = Date.now(); }, { capture: true, passive: true }); + function rotateTick() { + const secs = Number(current?.kiosk.rotate) || 0; + if (!kiosk || secs <= 0 || current.tabs.length < 2 || (guestOn && !editMode) || modalClose || !$("#saver").hidden) return; + const now = Date.now(); + if (now - lastTouch < 60000 || now - lastRotate < Math.max(5, secs) * 1000) return; + lastRotate = now; + const i = current.tabs.findIndex((t) => t.id === tab().id); + setTab(current.tabs[(i + 1) % current.tabs.length].id); + } + + // ------------------------------------------------------------ away from home + // A dashboard can switch to another tab (garage, alarm, cameras) while you are away, and back when you're home. + let awayState = null, awayFrom = null; + function awayEntity() { + if (!current?.nav?.awayTab) return null; + if (current.nav.awayPerson) return current.nav.awayPerson; + return Object.values(ha.states).find((x) => x.entity_id.startsWith("person.") && me?.id && x.attributes.user_id === me.id)?.entity_id || null; + } + function checkAway() { + const eid = awayEntity(), s = eid && ha.states[eid]; + if (!s || editMode || kiosk || !current.tabs.some((t) => t.id === current.nav.awayTab)) return; + const away = s.state !== "home"; + if (away === awayState) return; + const first = awayState === null; + awayState = away; + if (away && tab().id !== current.nav.awayTab) { awayFrom = tab().id; setTab(current.nav.awayTab); if (!first) toast(tr("You’re away: showing “{0}”.", tab().name)); } + else if (!away && awayFrom && tab().id === current.nav.awayTab) { setTab(awayFrom); awayFrom = null; } + } + + // ------------------------------------------------------------ guest mode + // A plain screen with lights and thermostats. Turned on per device with a button, or for + // everyone by an entity or a time of day. Editing always shows the real dashboard. + let guestOn = false, guestCache = null; + const guestManual = () => !!current && lsGet(`ds.guest.${current.id}`) === "1"; + function setGuestManual(on) { + lsSet(`ds.guest.${current.id}`, on ? "1" : "0"); + checkGuest(true); + toast(on ? tr("Guest mode is on.") : tr("Guest mode is off.")); + } + function checkGuest(force) { + if (!current) return; + const g = current.guest || DEFAULT_GUEST; + const skipped = Date.now() < Number(lsGet(`ds.guestskip.${current.id}`) || 0); // ended early on this device + const on = guestManual() || (g.when === "entity" && !!g.entity && ["on", "home", "true"].includes(ha.states[g.entity]?.state)) + || (g.when === "times" && !skipped && betweenTimes(g.from, g.to)); + if (on === guestOn && !force) return; + guestOn = on; + guestCache = null; + document.body.classList.toggle("guest", on && !editMode); + $("#btn-guest").hidden = !(on && !editMode); + if (!editMode) renderGrid(); + } + function guestTab() { + const g = current.guest || DEFAULT_GUEST, cols = Number(current.grid.cols) || 12; + const pickIds = g.entities?.length ? g.entities : Object.keys(ha.states).filter((e) => (e.startsWith("light.") || e.startsWith("climate.")) && !ha.states[e].attributes.entity_id); + const key = `${pickIds.join(",")}|${cols}|${g.title}`; + if (guestCache?.key === key) return guestCache.tab; + const lights = pickIds.filter((e) => e.startsWith("light.")).sort((a, b) => friendly(ha.states[a] || { entity_id: a }).localeCompare(friendly(ha.states[b] || { entity_id: b }))); + const climates = pickIds.filter((e) => e.startsWith("climate.")); + const widgets = []; + const add = (type, cfg, w, h) => { + const wd = makeWidget(type); + Object.assign(wd.cfg, cfg); + wd.id = `guest-${type}-${cfg.entity || "title"}`; + wd.w = Math.min(w, cols); wd.h = h; + Object.assign(wd, findSpot(wd.w, wd.h, widgets, cols)); + widgets.push(wd); + }; + add("heading", { text: g.title || tr("Welcome"), subtitle: tr("Lights and temperature"), size: 30 }, cols, 1); + const tw = cols >= 12 ? 4 : Math.max(2, Math.floor(cols / 2)); + for (const e of climates) add("climate", { entity: e }, tw, 3); + const lw = cols >= 12 ? 3 : Math.max(2, Math.floor(cols / 2)); + for (const e of lights) add("light", { entity: e, colors: false }, lw, 2); + guestCache = { key, tab: { id: "guest", name: tr("Guest mode"), icon: "person", widgets } }; + return guestCache.tab; + } + /** Ending guest mode undoes whatever turned it on: the button, the entity, or the rest of today's time window. */ + async function endGuest() { + if (!(await confirmAction(tr("End guest mode?"), "", tr("End guest mode")))) return; + const g = current.guest; + if (g.when === "entity" && g.entity && ["input_boolean", "switch"].includes(domainOf(g.entity))) ha.callService(domainOf(g.entity), "turn_off", {}, g.entity); + if (g.when === "times" && betweenTimes(g.from, g.to)) { + const [h, m] = String(g.to || "0:0").split(":").map(Number), end = new Date(); + end.setHours(h || 0, m || 0, 0, 0); + if (end < Date.now()) end.setDate(end.getDate() + 1); + lsSet(`ds.guestskip.${current.id}`, String(end.getTime())); + } + setGuestManual(false); + } + $("#btn-guest").onclick = endGuest; + // ------------------------------------------------------------ night mode // Per device: dim the screen and use the dark theme after sunset or between two times. const DEFAULT_NIGHT = { mode: "off", from: "22:00", to: "07:00", dim: 40, dark: true }; @@ -1995,12 +2721,15 @@ function nightCfg() { try { return { ...DEFAULT_NIGHT, ...JSON.parse(lsGet("ds.night") || "{}") }; } catch { return { ...DEFAULT_NIGHT }; } } + /** Whether the time of day is between two "HH:MM" times, also across midnight. */ + function betweenTimes(fromV, toV) { + const mins = (v) => { const [h, m] = String(v || "0:0").split(":").map(Number); return (h || 0) * 60 + (m || 0); }; + const d = new Date(), now = d.getHours() * 60 + d.getMinutes(), from = mins(fromV), to = mins(toV); + return from <= to ? now >= from && now < to : now >= from || now < to; + } function isNight(n) { if (n.mode === "sunset") return ha.states["sun.sun"]?.state === "below_horizon"; - if (n.mode !== "times") return false; - const mins = (v) => { const [h, m] = String(v || "0:0").split(":").map(Number); return (h || 0) * 60 + (m || 0); }; - const d = new Date(), now = d.getHours() * 60 + d.getMinutes(), from = mins(n.from), to = mins(n.to); - return from <= to ? now >= from && now < to : now >= from || now < to; + return n.mode === "times" && betweenTimes(n.from, n.to); } function applyNight() { if (!current) return; @@ -2011,6 +2740,140 @@ if (dark !== nightDark) { nightDark = dark; applyTheme(); } } + // ------------------------------------------------------------ touch: haptics, swiping tabs, pull to refresh, sheets + const coarse = matchMedia("(pointer: coarse)"); + function haptic() { + if (coarse.matches && lsGet("ds.haptics") !== "0") navigator.vibrate?.(8); + if (kiosk && current?.kiosk.clickSound) clickSound(); + } + /** A short, quiet tick, made on the spot (no sound file), for wall tablets that can't vibrate. */ + let audioCtx = null; + function clickSound() { + try { + audioCtx ||= new (window.AudioContext || window.webkitAudioContext)(); + const t0 = audioCtx.currentTime, o = audioCtx.createOscillator(), g = audioCtx.createGain(); + o.type = "triangle"; o.frequency.setValueAtTime(1800, t0); o.frequency.exponentialRampToValueAtTime(600, t0 + 0.04); + g.gain.setValueAtTime(0.12, t0); g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.05); + o.connect(g).connect(audioCtx.destination); + o.start(t0); o.stop(t0 + 0.06); + } catch { /* no audio */ } + } + + // ------------------------------------------------------------ touch and hold + // Holding a widget for half a second runs its second action; the tap that follows is swallowed. + let holdTimer = 0, holdStart = null; + gridEl.addEventListener("pointerdown", (e) => { + clearTimeout(holdTimer); + if (editMode || e.button !== 0) return; + const el = e.target.closest(".w[data-id]"); + if (!el || e.target.closest("input, textarea, select, .hs-nav, .sw-dot")) return; + holdStart = { x: e.clientX, y: e.clientY }; + holdTimer = setTimeout(() => { + const w = findWidget(el.dataset.id); + if (!w || !runHold(w)) return; + swallowClick = true; + haptic(); + el.animate?.([{ transform: "scale(.97)" }, { transform: "scale(1)" }], 180); + }, 500); + }); + gridEl.addEventListener("pointermove", (e) => { if (holdStart && Math.hypot(e.clientX - holdStart.x, e.clientY - holdStart.y) > 10) { clearTimeout(holdTimer); holdStart = null; } }); + for (const ev of ["pointerup", "pointercancel", "pointerleave"]) gridEl.addEventListener(ev, () => { clearTimeout(holdTimer); holdStart = null; }); + gridEl.addEventListener("contextmenu", (e) => { if (!editMode && coarse.matches && e.target.closest(".w")) e.preventDefault(); }); + + function runHold(w) { + const h = w.hold || {}, eid = w.cfg.entity || (w.cfg.entities || [])[0] || h.entity; + switch (h.action || "details") { + case "none": return false; + case "toggle": if (!eid) return false; defaultAction(eid); return true; + case "service": { + if (!h.service?.includes(".")) return false; + let data = {}; + if (h.data) { try { data = JSON.parse(h.data); } catch { return false; } } + const [d, sv] = h.service.split("."); + ha.callService(d, sv, data, h.entity || undefined); + return true; + } + default: if (!eid || !ha.states[eid]) return false; moreInfo(eid); return true; + } + } + + // Swipe left or right over the dashboard to go to the next or previous tab (not over cards that swipe themselves). + let swipeStart = null; + const SWIPE_OWN = ".hs, .sw, input, textarea, select, .swatches, .todo-list, .f1-tabs, .fc-hours, .sf-bars, .us-bars"; + canvasEl.addEventListener("touchstart", (e) => { + const tch = e.touches[0]; + swipeStart = !editMode && e.touches.length === 1 && current.tabs.length > 1 && !(guestOn) && !e.target.closest(SWIPE_OWN) + ? { x: tch.clientX, y: tch.clientY, t: Date.now() } : null; + }, { passive: true }); + canvasEl.addEventListener("touchend", (e) => { + if (!swipeStart) return; + const tch = e.changedTouches[0], dx = tch.clientX - swipeStart.x, dy = tch.clientY - swipeStart.y, dt = Date.now() - swipeStart.t; + swipeStart = null; + if (Math.abs(dx) < 70 || Math.abs(dy) > Math.abs(dx) * 0.6 || dt > 700) return; + const i = current.tabs.findIndex((t) => t.id === tab().id), j = i + (dx < 0 ? 1 : -1); + if (j < 0 || j >= current.tabs.length) return; + haptic(); + setTab(current.tabs[j].id); + if (!reducedMotion()) gridEl.animate([{ transform: `translateX(${dx < 0 ? 40 : -40}px)`, opacity: 0.4 }, { transform: "none", opacity: 1 }], { duration: 220, easing: "cubic-bezier(.2,.8,.2,1)" }); + }, { passive: true }); + + // Pull down at the top to fetch every state again. + let pull = null; + const ptr = $("#ptr"); + stageEl.addEventListener("touchstart", (e) => { + pull = !editMode && stageEl.scrollTop <= 0 && e.touches.length === 1 && !e.target.closest("input, textarea, select, .hs, .sw") ? { y: e.touches[0].clientY, d: 0 } : null; + }, { passive: true }); + stageEl.addEventListener("touchmove", (e) => { + if (!pull) return; + pull.d = Math.max(0, e.touches[0].clientY - pull.y); + if (stageEl.scrollTop > 0) { pull = null; ptr.hidden = true; return; } + if (pull.d > 10) { + ptr.hidden = false; + const k = Math.min(1, pull.d / 90); + ptr.style.transform = `translate(-50%, ${Math.min(70, pull.d * 0.5)}px) rotate(${k * 270}deg)`; + ptr.style.opacity = String(k); + ptr.classList.toggle("ready", k >= 1); + } + }, { passive: true }); + stageEl.addEventListener("touchend", async () => { + if (!pull) return; + const go = pull.d >= 90; + pull = null; + if (!go) { ptr.hidden = true; return; } + haptic(); + ptr.classList.add("spin"); + try { + if (ha.connected) { + const states = await ha.send({ type: "get_states" }); + ha.states = Object.fromEntries(states.map((x) => [x.entity_id, x])); + onStateChange(null); + } else toast(tr("Not connected to Home Assistant. Retrying…"), true); + } finally { + setTimeout(() => { ptr.classList.remove("spin", "ready"); ptr.hidden = true; }, 400); + } + }); + + // On phones, popups are sheets from the bottom; drag one down to close it. + let sheetDrag = null; + const sheet = $("#modal .modal-card"); + sheet.addEventListener("touchstart", (e) => { + sheetDrag = matchMedia("(max-width: 600px)").matches && sheet.scrollTop <= 0 && !e.target.closest("input, textarea, select, .lp-slider, input[type=range]") ? { y: e.touches[0].clientY, d: 0 } : null; + }, { passive: true }); + sheet.addEventListener("touchmove", (e) => { + if (!sheetDrag) return; + sheetDrag.d = Math.max(0, e.touches[0].clientY - sheetDrag.y); + sheet.style.transition = "none"; + sheet.style.transform = `translateY(${sheetDrag.d}px)`; + }, { passive: true }); + sheet.addEventListener("touchend", () => { + if (!sheetDrag) return; + const close = sheetDrag.d > 110; + sheetDrag = null; + sheet.style.transition = ""; + sheet.style.transform = ""; + if (close) modalClose?.(null); + }); + // ------------------------------------------------------------ phone dashboards // The top bar is hidden; touching and holding an empty spot brings it back for a while. let peekTimer = 0, holdTimer2 = 0; @@ -2035,17 +2898,24 @@ current = store.dashboards.find((d) => d.id === id) || store.dashboards[0]; lsSet("ds.current", current.id); activeTabId = lsGet(`ds.tab.${current.id}`); + guestOn = false; guestCache = null; awayState = null; awayFrom = null; undoStack.length = redoStack.length = 0; { selectedId = null; multi.clear(); } updateUndoButtons(); renderAll(); + checkGuest(true); + checkAway(); } function setEdit(on) { editMode = on; + if (!on) openSheet(null); + else renderPalette(); // the suggestions count needs the states lsSet("ds.edit", on ? "1" : "0"); if (!on) { selectedId = null; multi.clear(); } renderAll(); + checkGuest(true); + checkAway(); } /** Earlier saves of this dashboard, from the server. Restoring one can be undone. */ @@ -2088,9 +2958,10 @@ $("#btn-redo").onclick = redo; $("#dash-select").onchange = (e) => setCurrent(e.target.value); $("#btn-new").onclick = async () => { - const name = await askText(tr("New dashboard"), tr("New dashboard"), tr("Create")); - if (!name) return; - const d = newDashboard(name); + const choice = await templateDialog(); + if (!choice) return; + const d = choice.template === "devices" ? starterDashboard() : TEMPLATES[choice.template]?.build() || newDashboard(choice.name); + d.name = choice.name; store.dashboards.push(d); scheduleSave(); setCurrent(d.id); @@ -2110,6 +2981,16 @@ assignDialog(); } else if (act === "history") { historyDialog(); + } else if (act === "undo-history") { + undoHistoryDialog(); + } else if (act === "share") { + shareDialog(); + } else if (act === "suggest") { + suggestionsDialog(); + } else if (act === "install") { + installDialog(); + } else if (act === "lovelace") { + lovelaceDialog(); } else if (act === "starter") { if (!ha.connected) { toast(tr("Not connected to Home Assistant. Retrying…"), true); return; } const d = starterDashboard(); @@ -2159,9 +3040,19 @@ if (b) setTab(b.dataset.tab); }); - $("#palette-list").addEventListener("click", (e) => { + $("#palette-list").addEventListener("click", async (e) => { const b = e.target.closest("[data-add]"); - if (b) addWidget(b.dataset.add); + if (b) { addWidget(b.dataset.add); return; } + if (e.target.closest("[data-suggest]")) { suggestionsDialog(); return; } + const pa = e.target.closest("[data-preset-add]"); + if (pa) { addPreset(pa.dataset.presetAdd); return; } + const pd = e.target.closest("[data-preset-del]"); + if (pd) { + const p = store.presets.find((x) => x.id === pd.dataset.presetDel); + if (!(await confirmDestructive(tr("Delete preset “{0}”", p.name), tr("Widgets you already added from it stay."), tr("Delete")))) return; + store.presets = store.presets.filter((x) => x !== p); + scheduleSave(); renderPalette(); + } }); const insp = $("#inspector"); @@ -2180,6 +3071,11 @@ renderInspector(); return; } const del = e.target.closest("[data-ent-del]"); + if (del && del.dataset.path === "guest") { + mutate(() => current.guest[del.dataset.k].splice(Number(del.dataset.entDel), 1), null, { rerender: false }); + checkGuest(true); renderInspector(); return; + } + if (e.target.closest("[data-guest-toggle]")) { setGuestManual(!guestManual()); lsSet(`ds.guestskip.${current.id}`, "0"); renderInspector(); return; } if (del) { const w = editTarget(); // may be a card inside a scroll or swipe card mutate(() => (w.cfg[del.dataset.k] || []).splice(Number(del.dataset.entDel), 1), null, { rerender: false }); @@ -2219,7 +3115,8 @@ }); function toolAction(name, ids) { - if (name === "copyto") copyTo(ids); + if (name === "preset") savePreset(ids[0]); + else if (name === "copyto") copyTo(ids); else if (ids.length > 1) name === "dup" ? duplicateMany(ids) : removeMany(ids); else name === "dup" ? duplicate(ids[0]) : remove(ids[0]); } @@ -2270,11 +3167,14 @@ let resizeRaf = 0; window.addEventListener("resize", () => { cancelAnimationFrame(resizeRaf); resizeRaf = requestAnimationFrame(renderGrid); }); matchMedia("(prefers-color-scheme: dark)").addEventListener?.("change", () => renderGrid()); + phoneLandscape.addEventListener?.("change", () => renderGrid()); + phoneScreen.addEventListener?.("change", () => renderGrid()); // widgets shown only on phones or only on larger screens // clocks, screen saver and camera refresh setInterval(() => { gridEl.querySelectorAll("[data-clock]").forEach((el) => { el.innerHTML = clockHTML(el.dataset.h24 === "1", el.dataset.sec === "1", el.dataset.date === "1"); }); if (!$("#saver").hidden) updateSaver(); + rotateTick(); gridEl.querySelectorAll("[data-countdown]").forEach((el) => { const left = Math.max(0, (new Date(el.dataset.countdown) - Date.now()) / 1000), dur = Number(el.dataset.dur) || 1; el.textContent = window.HAFmt.hms(left); @@ -2289,12 +3189,13 @@ }); }, 1000); // keep "x min ago" labels and graph time windows fresh - setInterval(() => applyNight(), 60000); + setInterval(() => { applyNight(); checkGuest(); }, 60000); setInterval(() => { if (!editMode && current) tab().widgets.filter((w) => w.type === "graph" || w.type === "calendar" || w.cfg.showChanged).forEach((w) => renderWidget(w)); }, 60000); // ------------------------------------------------------------ boot function paintChromeIcons() { $("#btn-new").innerHTML = Icons.svg("plus"); + $("#eb-add").insertAdjacentHTML("afterbegin", Icons.svg("plus")); $("#btn-undo").innerHTML = Icons.svg("undo"); $("#btn-redo").innerHTML = Icons.svg("redo"); $("#btn-menu").innerHTML = Icons.svg("more"); @@ -2305,7 +3206,8 @@ ha.onStatus = (ok) => { const c = $("#conn"); c.classList.toggle("ok", ok); - c.textContent = ok ? tr("Connected") : tr("Offline"); + c.textContent = ok ? tr("Connected") : offlineText(); + showOffline(!ok); // An update restarts the app, which drops the connection: check right after reconnecting. if (ok) checkVersion(); }; @@ -2328,7 +3230,7 @@ } setInterval(checkVersion, 5 * 60 * 1000); ha.onChange = onStateChange; - ha.onRegistry = () => { renderGrid(); if (editMode) renderInspector(); }; + ha.onRegistry = () => { renderGrid(); if (editMode) { renderInspector(); renderPalette(); } }; /** Translate the static page: elements marked data-i18n, plus every aria-label and title. */ function translateStatic() { @@ -2340,6 +3242,9 @@ translateStatic(); paintChromeIcons(); renderPalette(); + await keepIngressSession(); + loadCachedStates(); + if ("serviceWorker" in navigator && APP_VERSION !== "dev") navigator.serviceWorker.register(new URL("sw.js", BASE), { scope: BASE.pathname }).catch(() => {}); try { const r = await fetch(new URL("api/dashboards", BASE)); store = await r.json(); @@ -2360,13 +3265,16 @@ save(); } me = await api("api/whoami").catch(() => null); - const hashId = (location.hash.match(/view=([\w-]+)/) || [])[1]; + const shared = sharedParams(); + const hashId = (location.hash.match(/view=([\w-]+)/) || [])[1] || shared.get("ds"); // Which dashboard to open: a link wins, then this device's choice, then this person's, then the last one used here. const dev = store.assign.devices[DEVICE_ID], forUser = me?.id && store.assign.users[me.id]; const exists = (id) => id && store.dashboards.some((d) => d.id === id); setCurrent([hashId, dev?.dashboard, forUser, lsGet("ds.current")].find(exists)); - if (location.hash.includes("kiosk") || (dev?.kiosk && !hashId)) enterKiosk({ fullscreen: false }); + if (location.hash.includes("kiosk") || shared.get("ds_kiosk") === "1" || (dev?.kiosk && !hashId)) enterKiosk({ fullscreen: false }); setTimeout(phoneHint, 1500); + const action = shared.get("ds_action"); + if (action) { history.replaceState(null, "", location.pathname + location.hash); runShortcut(action); } // After an update, point at what changed (once per device). const seen = lsGet("ds.seenVersion"); if (APP_VERSION !== "dev" && seen && seen !== APP_VERSION) setTimeout(() => toast(tr("Updated to {0}. See what’s new under About in the settings.", APP_VERSION)), 2000); diff --git a/dashboard_studio/web/apple-touch-icon.png b/dashboard_studio/web/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ef5d5ad23de84cd62eafaf471cac24d59881a786 GIT binary patch literal 28901 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 X$BxdI9x6uM00000NkvXXu0mjf<|D}E literal 0 HcmV?d00001 diff --git a/dashboard_studio/web/icon-512.png b/dashboard_studio/web/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..ff08f018f8792c787bcb3ddf15f96a7a0bdf2e7a GIT binary patch literal 144320 zcmd43XH-*97(PfxM2aFHH7Y71Qj}gJAYy~4H0cN;O$b$5NGM8Enh2^w0w$ zL_i3=Ludi%1PCM{W%K)&-LqeI&)HA=;hsBp?wvc6duHZ+p67kv#6K}H;yEpPnu&>t z=fVBE&zP847)2JQQyh%bVelxKabos=W~9$lF(AIqxKMX_{@|6dG1Co3`4kg#tUD9i ze~&N@QO3b&`iO?z&qlDXU}6tlGQgBMq*iYU4Iu1wEv6y;KYW$c}iXL0+ZpD zhxfjv-qTmhSx&5J^W1hAt1w@qY{F=Z-P6Yw<>sD5dySIq{G$l(_93~=p{(YOV@j%eK8*%**PL;h0x zNZwhb(>41=e@ap3KchL%ziW7)Bmw2QHc;(~*{KfN&E?&ht0~I%Kl*};;vSG&g9n*3 zE>Qfp)r)L{J3??3aQ_F5SrYBxu3I`fy@zZ`erO1*&(^)Q4bKW$00o_#ehk~|-KX){ zSyfObrTfr8{LlRs`>eOEDs03|IJ`KRghzEPrYS5qpPIv1x(8Pgd-~?(_ zd(0PaC`?Rj58nxy*pE2DmTK=ok8d6$mzT8c?i|l1>2a)zMR+~j??=vB&(j9@J|Wwd z!*ScYH10se18}6rbZ})Vt z?!tEW<6?EbX>aUvrWVWx)5PTpO{C3D7ohC?rkPtBt<7SuuX(s+pUY zJVNoD5%bUzvh+MrlE4h2-D!2(JarQ9OY5j#l3f2SE7PZsVy2V2#-?eH(MpF;DYKn( z^tR)}tVp!Z>I)n`xp@}rbiof}Db~{tVXaIp`*Nm>)W!#?rQpQot!W$J`5nX&A3#?- z&ug3O_9gA9axT__b>Eq4qpg{K;G5ti6zajj+3GaE+XLezpSC42=7eObW!Y1BxO{`} z9K>b}wY>^3QbuEwHKwK}xijpV-;hirVZL2beZso5RGc1P0}>Iq48eT*dFHPglc|UY z*-CMqw8NKSiQ3`8sn8FMai;LL!kL#Z`owk)S(I$~Cy^6SCwGB+k6(eUVawXZFC&#j zSgmc$vFGV#D68LGH=Utd_S;;$TTW@TB%asua8tZ70fWadrT~3)jeb?w;d$E;MyDZImKs;8tuRC5b;j7cdsh_En_jOop4ArFL^l#?* z{7vgP0(9!a8h#fY(IP>|)3dc4%-WXv;Ermn4o@WazhSmH{kw%;IkquB|D5qG7)0lD zpbF#CQO8BJVnF>N%lQO}8=*q66iHz1s}6qIl#KI9xLEF$VyxMH+~x3a@P$tb?S^uV zBP4m+ED#M$hB-Kol5Odsr~gny`-P1y`cc0X2e8`obb);t@%v%}x~J~t@wshB9`#2R zLFO_u5u{#)uRx^*5fM0J1$+|Ft)us1sitAS>D|AM4>*mx%(;D>bhG|d-Zf&^E}J+D zLp6AGT?kfM0&D{0FV3wL)7H4s>XIwgh$n{KAC(yWFOEL6hUF<+Syx722w39>`N&9-&rk0JgrfHU z7KM9-EM?j~C7#V6Y0Vi|V?xe{hLHfUJy%>NY&n@COuQ%CzQ++(5~iGgP1-G#>x;Zs zRG}8}@TzXGLd5GI7CKX1st4XQA z!sn`~!o0Sl_sB-4pE?=6RYHRs2QPdCC7K(9uvVPvYyUODcu7e9XtEzz2FE|+?FllO zEVdOuy<8((8X5{|9C*IP1W!+V0u#R~$hzh>B6ijLD@XP@7V2lb6;R{^%gjt#?{2!7 zcgvLUn{FM2C(B&jh36o9+?*9fI38)xpv=kCJ^Kh>1LfeFwwrJ&hEjUuN+pFR!5Y3{ zmQmBP-A{Ey^i)7_A7ivF#tJ${9w1jk#^H!HG7!H(ryOA9v9xjr%UoK&gVoV4km}85 zQYywYKSJ-vLXAma6s4IKc2WvjfQ}sZ=ve!HxM7`34X}a>rLxc20kq#+XJpFblTTs= zDuU!Q{`^J#dG)C8sOhT7suK%@P$#zcAl z)_ww5Mzm$=;?H!|-uZwwBa*S^ey|J8mrh;-!ipBGFhugc9G18;vH?DyEu_ZOf~F^6 zL+nC&6xzN+R=;j~rdKZ(usp?glP86P<_7&D31e+{FA7$!K12eq{d!J+qUW@`YVA6A zp@8+lV!PYL;oFQQMYp9#ixn&${wq3d+C4oA5#@UOJZSuH>BhY+XKGHdlofQP;?{jm zmS2%WG3WfL$(!`0_X^iBq@H;cnWI@-8y%WrL7>h?|$#X4dk=_X{DSNyI&mY-~10 zbt0*V`9bDfxyT#0eCX!j_Z@2Y#VlJ8i4!z)g}cCAiS3@4hq%4*iDmlXMfOkf(p12S z78;SAvhowZ&Usm}XJ8pXa@&N%C86_tkgyPnGaz^5%|knQwHg!Pgectyq8#@1!s%T$ zuiJ$b+(NFP1^%esJqfDq_4H>No{r+ObLOGezIC2NZLpmYqlVlB+VTtE9J*HKf}D_g z{ZAtMe_BK!hW;D~LKAlT+WuAHVo`JvTy7BReMXPz>0CcGCQe@Qn+hT8;7o^FKB9zX zuv^1iVo(DVRR5x1%pM!R&|qA=X`e#~n(X8Wy$PK4siF4o7o8;2?7z(e`tgVyx-IiE z{-4pA`JZC7Rg~!?wLV}nisxY%T^3RSQ`ionDdfV!j<#gzM>A~|kmDeT^Hg(C^4f-( zM$R)eCK&zbIAe`UeAF8Y3^kX4D|Z%Iw2#7t_q_4Xn;ciFFDOj8zgJ`?skgp3Y*~!3 z&tY*NHk-4#GnEaTY7g%4QTsZwy0z{bbbMD+LlF%?46NbdoA3u}82aLd=D?rT@%)q7 zO>f*gAbAcql-y8ewQS;j_^E-pfs|p>2P02qW@4-P$oOmP`#e7C&zE_wV3~PIXiHCF zvvD{ME%xsTb}zwClAK8ZDKHuEb?+Nc4R#DIo%v>DJ`-GdG;-~QT1g&Bz%|*mAW3@L ztMB98B3z-_Xj0augNFM-tvHan3Is<(4%D=NQkr+eLp4sylroD}udHjVgPCYYa`Y#; zCsM#=*cXL0+paSOIQCix`ElgpOQ`aZvRbN%Cn&{Ubqj2d9i)iEpzpGUiHpQDh{8>m zgJqTa$%)@SHM2h7k|N*0avcYgH8SepN6-z0(2nDD4D7Fgr##5S zXLz1TTr;47^7j$6wdD|pa!8t-ptRtq>XreL`44CX7lCOeb0gf|ZybYl^P5k*!Q_eb-IJZ7jJf6@+=ix11*lgnBH5M{zaI{yeBYNmQEHtlxH=~){tel3 z*}{)KpMBz5gYNpqO?O%{(Tn}8oY}R=TF3ipDXL9Un4N2ZFY+~#iN<$SbL5nit7CeD zE_xl7$Gn972BNXJL4kiEW&=8T;l|TAZQ4;n%fPLDl?j+N&`k4=^;ekQ>j5cR2n~=- z*sbQAE28|RAJ6*1!{+KlveQ^~*1ef%3+4y~I))qCHv8s8B4bgT_1Uoh@#tI8{)V!P z>kfq5rCEp4Vt-hyUMykethB3=EwjrUuT+X&`meqe^MBT3@M~;1wxo0Qj5~d+*Xmp% z%M@k){MQ1r3>Ww$p#E^u<87IIR@P_+d-bn^i$SjV-)&#;YYM!ySOBSFzs`ZsB~>nc zd$FLAc_Ed~*rqaj5vcvKs@=3Pz>%rON29$IYchks|a z^tM@d5zI7CPC8CsgU~@4rHDrF!vxqN@14-gioHi4nGdA76xV2Fj->?G@X~1v#meCb zmH<3^Nbfvgre)Mf}z8J6SR@rEtjZeD-o+9Fi$m-FmF{sdj* zH0IatbA^%(JwsCMMy+#&9DX-PjCap2aX*h9oo2$sLOA^f**gCJ^%}8|qp~9C$-(Kc z$@p25NogiZ&%7{o7GVvQ>s09>>6BA`daOC~D_{Smq8M`JFrE~epMo;HGxUe!`>)8_ zrzrCXNI(qaA??G8F~Uv$q4>vJW=%TM0mgel)&7*+fgyKESVEkB(&#BpYg0KE_s02q zRDWua|ENF)=xbk~E`yidg0GQ)l<~V2Mh?g6&I-@9+LCVmduxy7u-!ytEMVl5)(wTrY<=rRT(r_@M zbdoncd~@k#&@6j;+aWi=6TnO95ZK^5oukMA(sOUZNCUg!FG5DJZBjy0>|DF5N~(2% z1hJ7%EO_=r- zZr^n-JQtKt%Ghx{kd4ajC{s9I<5AvB1K*i027HzHO8-@-dvT<`e^uvY&>7{vvH1{He$O-e#VE(Z;L(4|Qk$E~lL^5V0zOUfc&Tvp@O>kX_3T zA9p*Iq_Wt6WwM1)jd3%%RA=1WF4bs8dqfJ>7bA2QbVh8~ntPOuZsupiZ9kqVcm<~E zTDQC=B8?41XVtH0Fl;Kp`PwIId3X=I=7eVj#ul|6T0uX&ud}9vlTAiz)8+JTSo>WS zzn?V$+#zf8@ilI>;VhvvbRTIAF3_l9vbG$`>NC=h{8ThOL78{XLLplme?$%S-$9{P zL34Z>F_zpLLmx`+RVmmKN;>@h@$!N~I>e8X2*xC(YBeDGkTjN;+O)bl>y|y zVdxXD7p@*3gp0K=?MI~nB5@M>@AD6vQ&$TtZSG8;he>T*fA)2>QQ^YI>X@-x{=+{m zYNUjT!yD4xpnuvfV_Qf$(LBoi$)A<`H9#qU;V07Ii;s~3 z4PTTO_a%at6Y@W@%X>`0{{qM0fhPDhng+Bl&fd`Kwh{MrWvxG1$PfxAX~WM_P5j4$ zCyfkbB_i@m8{pqoNRFK*PwvT6==^STzTexNK4_$ zf3{>zR@WvA4XZO+(SZFAU^DjH@fx~ol+u4Wq|KxU#2D+q1kYc(0_hh<7D!j3(>~Kp z+HawlgXfb`4K4dY%SO;b4#zQOai06nQnp=aKWPQtwan!u3NwA=140S822vl?LU4gq zqe$f7lPqcv4YA>nf%*Sq=)$jc?A)l<3IN-_dDWZ6pD8Iq93lR1N9k zT`NEaPIxN1@S3e+-+PeaW*>uhK0-K-OOr!iWMF9DjH>Dz+^lqG`pLKHB~&Yh6+J1c zsDT`DhICCYeRmkQ0Mb=zN*2aI94akQVC46dtZ|Gzrv`+x4_;1_WxsM$YcT&e9+)^z=*^AS3N zn14!vjG@%i_X~9$vmT#Y@2N$yuo3hwbl|K{B=IcNc3yR#jt;5&|7h2TeZWz^b*qgJ z1Dak4d8K|MM~Hhd%XoLFW*9aJ4OxGY-_f9XLp}v{Fc37kNNAM=wi;}bZg7iypp21N zFXf#39=TSii-#v|VnH*KAvfs}gSSGs7rc(7!{KBJjBL=XZ~OeUc#t=FVUs#X&V8V@ zaA%){ox4wJn=`t;+3SR-y$I1(59wD62RD*M zPtw}lJ4augD!_4V$-*r(HUW4gU<>p5`yhLIi@n8jx?2hBfz}uMd5k>_iU@1kk1pIF z*$`*fZTU6oqyKnDYy+!I7l%z>^!oG33D8Q>Vxq>~Zd$&!7($U!>r4RH_Y)WlHH11M zIsiG#TYww8Vk+r^I=h#pa1vh~c6=0W2DSmR?nS}LKfDi);|}7rE{hKOKKk(OYCPB7 z>4+p7xFZ@Vgqr)Q1q|AH6TBpQ*r8~p2UEdS+`pd#lZ5Ik!gc#s&qMnE)2Ce`8vn;&w5G;k_UbjU-mWwCViCaLvCTe2 z>(W?#qPtdIVwtyhd;8RhQ@-u_mw&3|x`DxWZGK~Hk2zk>`#-&;{!-6Jo=pNR?6!|s zl7HDiJr+>h+vxwu&QY;&Dn3qS|62q5)0`|P9y#x-jlOKcbo4Fp3wZ(j?th{NJcOeH z8WaTU8@NU0XeV43f(brJ*mLLduUz=@v-WHIh0PF3kHO0Kaq+;qZ(|$PTQbR`IhOM* z-cljAEZrLXEl!yrztv2?yC~t+k8I$!0#;{7L|TGZ$;h{(w@MUmo4Ttw-&*yjrp5j- zEZ5lZ(31+=%G>%eGggn2WAL&Nb5bUVgzED`&Ye~2a{cnIy^Q&IinY?I687|Fz?kI< zM{7JXO-&D5P|C7+M^xaca>ct+fc4C6)Qn+}Yvt0Tt-7Z6tJYIRqy7KQ?yp6Vx2&zq zW~Tgpt*hkRji4X>G6x{R*Yy*{er9b01i%-OM0l#9Qa~>KfHIL?IG1b zFzNr%_DV1JnSE)dmR)))@bJ6e%r>V}hZDPV-r_ed(dl{ix}ex@HmTWhwy?5ImEU1n zQT_7LihZIjn}Fg?<)FF5IFOl;wk3O6PSiP%OQMZO0>Rta%ZSE{D!CUwDklT$%nlSM zfQ@mMvF+-AOdJ$`HQ5_vkDuKTPkdmbCEG8;xuMzL_d9%GIRn9Jj?tz|As$+D_gtTV zvLJE}$d>+k9@=_W+~+p2UP$cIwqs6MNhQ)daAsdD0!~vv3Bl$-X|I@cJl>1DarW>B zWIy2O|90)`x3u-5zMa%>Gw&P&F7$|;8O-5p${Q1^PHgLrnNWn>+su-WP#iJMo2fSC z>^OL_Wx{&d+9fi}5B@XLhR6C%|Ut z@Gi+TDN$$lcWlKv%PA!Z!6>O2y*)^+cAUjjBh6F(=6{GQ(BR8hCl>5Tkr2 zXP2OC9lijM;Z6jUe^L0g1%~rLmwt<{FV&kE+UL#W-9{SU{|zJ9{pX~3^;0RN#XRbF%m>_>whi6}}8dV-pe#XKZd%27Kw0 zV?1En=1;Zh{&eM|w0J`?j?G(xu1yEkl3;6(6mB3+sC4TA-IKqDf>pP@`sP`P$-tAo zDw1b;yWrv-Er`U=C!Pu4!YcGR>$_}mIh+tHQ|xV;Ibrhw`yel>Y|ytxNn)&_-l6EXUFCqfSvmcb7QqN`kgeikKem0lx`g_VgM)Ibc+~c!!-~ z(;Um{2vXv1Xp6seQ26TCU-r%BUXOB_DAXCOP22hmbnx`ssak!hTVEsMxDXz%`|4V{ zqz%`Fcx^4S27$v>H|X~}65eu!NrzHM!b9VtFu*M!!GGo7b*PlOoOgZU5_nsoR%aNz zEH(W-?-?K^W8+qwO>l0dnjpPET7Yb{U%H=szU=59}v6 zkdptCU$eq8ef}{BIzq>qPGZOa6=eGeQ(-?CXC|jmqGd zkF{lkFI2MT7ubJh4&vAEx1NJ19&xF%?zvs!%whQgD zeDe4vQBkAsgUyy5Ej8#cF^Vqc`!lrR7HAVY;}7lZ;u~^z@|jaK@|TEVkpT8Axf<-@ z*O!ofRsGPlfgI?TW2cMhtD6#&fg0Z*<%@W08zpwXP^-9Sa4qTnjfT|i!Ot@T;14{o zVzpNDBr#sC!PlQ>I68!jL=JCNSL9r`A9mW(uShrKq%Xnl_^crmU*b8b_YPSN7KUtN z^Kx{Z14S?UJ>qX`r0MuF0P_uZkB+oo4}O^U^?UaCj9J=rKFzFqrNwSgfOB{G!53D| zyh9O;*|MegcKY~S#^h=N>i=B#pLe%Wqys6brM>TAc)?EQx%RQbTqejCT-O~y345av z>=TX!T?uaOZMR%(8!AJGoO5NLl*|1U$vs`T9oYTyqE==5KhXl?leB*DQ_8f|=GW!3 zjyLSxwS=BFwqEGW?TINUGzzsHE!ELHj9|J4I4Bw3VTI_g;CG3gCC!Urx)aw%rhS!W z6`n^!M=vz;eDj?-Ync(U(y~@Ocm8)cLdeS1K3Jb`Af`{tE7`xgGL1ifosKWBLt zoee1#TE$C?mDV@XP6%huj#k&Ew2r(c2LW|OpYud z>tn`RJR|$q@qH;O-5p{iR6@}2ai0H6?BMOLxyh7@P zj@COYYdAe_Fko)VSRPI;!Z$0CT(fe{yoqnrm^yf0 z5AO}Ag)jSt9BiMWjlu4oAJ#b|I=sQW<`Kee;?8X{lv46S{k5t09JQ%x(B^5=e$S5~LmgIb5zYForecYDT zEBt0v+jZ{ASU36P0YJNSJ#Ai+7Z+<;weTm_z7RdiSs+vGECGAH(aI^R(6yX2WG5_6 zK;Q54R1KPOHNO76DtL?clGEMK7E&X>TrxZH$&U}osd{2s4JtDQc1Gp`WKD%~>`JpP zF~6;0{aaxD4FO+(PJ(yH8A~vT`{rnualHXFcx*si~K+Qd=k6+tayzoxr6)RB9qtuzAP>>7EU z+V9o&yN3uiZu4Hqp(T7EZpJ9k;*7tZlG7fHJ>87Qi*f&2VU4~v*1gUvHF2^o{N^;( zH|>%&w-z-Al_1mb>3tluOra0i)KSBbMGq41Zg!l99f5nHI}YY_Gi@vSdAg!?LF^}U zYF0J;#BvNM2pR`=0Y3<>Uve?07jCUFj25UXjR3;)F z5H{?SC4Jz1pZv+|@e;2SK{lwUyVlF!&9v+VIF1*E3G-mKrxqY-N?@-8XZlg} zmFJm~i>7BTxjO$@^>{2ks4)p@X z_P+&!Qf~AR^vCYJ--w?Vf^X7)XVY8wT|x8rZAe`IBYek!B8a&OZzNlfTRSvF&KGAC zH$J?;@=!M6`ol!0L!KWu9HmlBuKakoq4XjFwl$D)*}>^n^009KHi+b!)Wew^P(1%& z+r4=iEn+)3u`tcQ3ptzUVh?Z017)e$z?su z@*MT^lB>~HeCCXlb`i-X_ED=Cx>ie6dpo2V?NAu0Jj>tD75|->o|;Zo!s)K`@x)Ap z+FPM|WH>DI97}YjIEJ4ENzd@b*0Q@K0XG<4i0j4angX}M^2Rd;HyEc-*KO+IC%awycgpU^G2IQ z#bmqT0l2V()y8B4bsw6JI;=vn! z@7T09@Po9PqLhwUZfX|v2?K5xd&ai}FuPm9&cR28tux>?t!LMK6Mz)QeXG@*ZrJ<} z#YB~hsSXX!Oi{p!pMwUJja9dvMFbdyj=un3SNsny9*|LZ60C?DBMT!<)9nXlQD9}2 z%(EMtMJ{s&j%9pR=a`-MIs09*8z)41 zpI%(&*ku#7?yR>r~(w~IL;}Aq%gf7NhBv!>?y&4!{6=M!(Egfb#52Pe} zY+!c8xuJ1ADK}F?0N*)n;l06<%jC<@*HbUXmJz=aC962}pZW?cDYT4n*1~8vnudK& zY19S#_Dcn))kPQa;7hW&8?M9&zrb!=_z}-*+Gu{1487W9d`KGNviU2B6yr_?aJa2> zP6j&T-gr&37ofk6424x zb~^5s8sNgN=Bk9U;fG67q^hn=CD) zIBN7Sm$W`T{sXu$!GGW6y}1^cE z{+r<<;+(O`Vp6F;Ec+#7Os!QPX(N)4Pl5(kUX!l>dJQzjHy7rgufH76j5l{j$uP&> z({jQ_f6#ur{jGv7Ufs$-wG$OEK#(6CjY={C(nMJqL*}=6w^X+AHS}~0KDcZ0MUa$N z88``6P5?6&(``YS;>D7X4L(ZL0Y(N!ZE}!jMDYNIUoO8JZ<>~c(LsHIYO^c6Pj+?d zeA<1)7uz*DgdE0qV^VEg{pY%6!>b(6sdHC`I5n|#>hgAA^dH8>CVPP;=Wc#l zMNf2G2-|Q{)V^qC50g|HOGH3XZsI* ze3sjRZqOQ>nnjgcGgvZgvb(KRS&#Jo+)#Tf*LYvuoxrLQVP|LP7sL)?{oEOw3%{>v zbRHzI3X4}@{`DP(WEk`R3;C%IZ3Oe2X@2q23Pp&KN_q^R<+wbSbn>3hpEB2w#?9lc z<|_B%fC!f)z~%^QLPr~f`am7#g)q9qz981Vg1iReI}9x+v7MjFWN&7&b}mea_guUK z9tS*nU~60z)N%>*6n!}&mwh8=Iy+{yo23W)cizn};JnE`Saun$f=e(ORPF-LWP?Im zoyuF&u_7;mf;5FMjWQjc{*uKu^sg|lbf>hfLcPifGINoVKsm|hav(+bVI~K=|5jL` z|DIH?2}NNMvyQf#HXN^8(w*9U0*1xwvV`7*ctZXR#y(M)V)XpP`8zp-g=}z#T(Wbj zcY9FakrB=}up_f893SW~l_EBm*cRE^yBS#)+bY(wXSmE)@M2^cY)8lqEx|WW8M2(K zAm0uv^+;`yM!xS+|JYU)ZNQzj0Jf44D%NVg%9Ol(@^IUU2=Q3aOOE@cxt>Q=;U+vK2EM`&U~1pCGYVrntBFlsEYGfaZsj=Utf6habOzh6GIKHnpKUtlIgZS3MB* zQ?GR}^1ZVD_9W{v&4sJ4!Ya2B)_EtgE+_LXfiFP)+bR=>=#KnA+H(+&-7y(va@(zC zF^S};9*Y8bQ}O3nBo8_aJAuwsuUz@Hv@MpgF1uEUOe&RiKa7W-`g zys05^c-LIQ`*j8EUV3Emkju^Q=Oc3E_wTc0Dia8l72(yA)DG5Ty|PBcGD9IbqZ<4I zsCxjw;v14(lYrf6wnl1pR1iZl5gKQ=QA`sNoUD@5xW6pB&;~$nU&HL|O zo@qBU4j29Mzg$p7vR1;chCXGV|?5tH1QRH z{cLy5{@0`Oi5$->7nLqU&x`CF@#t(YFS8}AdqAR}7{>S6^uMd0|6{*5q+q(=38d^Q zoY;l_aeO?V45*vgy;D*-Qmvmn7V4ob6+JAa-UTOn2HTOVLw4J%?pwBrH!J;ol4S9j zA|jK$0ou?8osQC7ykkAAw(jk8KU0b@!gX4$$$#Iqz^4RdYL+TP8%23&Gh~{jsREQu zIe0AC0*Cd0A)jG8CH?68oG6k~$lZGr%|@T@25&M%qj`d)7cH=A_^tCjb(4*I2biWM z`R196jvFrjKDZ%YPb&QktqpnAf*H;iLu-u&oZz@yhr#f{CW zT_y9d55EIcAO-FQPnsN);mej(qp@cUn#|-`yfZNDDTxHpWjP}TTHCU4Zq+opdoBK zRpXPt1$NP~x^Ay&xq{6X#bym}iHkzCsFK4?)_Uk0DKZ zMxVQ#>bVjp=ty^teu=~ti_X^DF>5%U)_Q#Y)OS4uO|P0;;kCd%QfBMx=RZofg6|Ms zaoHrGKfbWWD;(mHY%8qtT{ax%z@{9`O_7y<9^>+j((P=iXpODAZ7G3~yiO+prZ^nflzW!*HmVrGWJH*(D;FI^dbpHr_LS7g?FI)}7@o1E*gF$sw^& z^e@_Tl25E)$~AGfa(O@A#_msUwhXu1MLE2{`JEe27CqnchX&*hVoCMC+eQ!hfr3hA zi=|zF)yKkgIbCd-ZQu&GXn8Wch^au@FON7Sw4QPz|C`Gcs{IN(pLhn|?LMR8ZObyN@!w%Q*e0eGZ0_0{}g(~3q2LRE6y1n=Hy-b+ZA{)akHu?eMd>b5? zm%U;p-;(=hSDO}Ck#m`V!LXo63ebE;mWCB~AMjS)LYE?Z-%M>?D9reworhb+QmCbK9i@e z|4jJyQ$$8L_&R&sA>N}b_MiSj=LZNL7-MutRBn*Vrz}aQv+xa)E`Wx+=6g_D;Z`^7LTE zMG_`hcs0NLr;sJ{|7`P&=d~}sPSe_!I%7+x%;f?rbs**St32(p&kQE%LtX>Rg;}-SKQulj( zs;$4~i%+oppcM4e{&lOl@l;WPd1qBCNiRzQ3E^|qe77j=-Zw?(cf~|6Af7MA>k{4P z>YUhnEw9+d-`bgVX&?=%ig3Q!?~b^HQOF!dSX+5+ggqqMvU0xP>RhV$ItlBM5a2=sVHfOMRXay3g+^{8LES zGIwFnPyoa|#`zkb;VRfNHnh|iP^&H1_5qdH*wuyIP@GjWA&A_)>tqsx)=!??yt}ul zE3x-L4xwvo@XyDt|DM-G_XA{SFhQ*MB55&vXt(9rmd_UW)PoDp1xN3Qk7iw{2ib3H zuE-dMKkKNQ`Hm{tsL+CvE0f`Ju)!|y(ojJ$nfa_)JPZ5ofkIA)QN!74{Do=(;8st2 z3gD`-f7YAm7tY@>BG;j{VLlbLy%C**gPK8k>k&H3=3Tva>c>mL=1@>|$Qb)GUyY`c zHgl4TtB9>RwuItONhL92uz6E>#@L7@gxVWG)p3u)WS#6*h?NalE&>SccaTq8UoY?c zo=~{ie@Dp5!t|oCx{vJiD}tfE`B83?It~*(5b(q)zDLx6d9kq-(By~Rvd4hBQ zL-{TiK;@uT@P=5S#fD8E0*AoPa4w)QuOLxzGFpx&$HAV zl7_UBRVt$@oLhe>f@%dR&XAzpDbqK*(_W;N?#BQ#^ZjeD`{%@Znf#M53s07fcqBC; z#p0U5gB8ecycQSeJQ^q~YQj@~X={jqiHd#}0a4~;RQxZK@2`BHd?0ID3a zlxBG{ouBuYcIcrR)8*HDTTJLVi=j=y)9_84LxHoPsJ+aqmA9;xURCtxZ>NdI%&7ZC zE`abntbHVTKv*cn`E3d}Y=yn{k1&d@xc^20xu$R8(d&0@U%~Q1bq@#dC#?@U74T(O zYmKyd!a&7mL@^m?_h+;Cc&Ys^qknF{Yo5f4|LbDeG%dh=K@_aXeGH&-WY+qEdOEn})ItA@gH+h@|7jdwb+T>^=<*lAck@@r3; zoyHe{ryv=#C6tdd*Uie|aRks~Q_{0pxPRvR?Oj$%!mjXlF8OQ~Fl^$a4>-{e=zjAH z+3@N0`EyLhip%p;*}tXLyRdQIr_9TuUVsD5Lka6Mu_e8uL=f>>ApVk8U%UveVM|x| z6X*QeFF$y5uIWxV(!Xy`UZ*rWd^Oi^9GW!eg4v-F{GX|`y;O4#5?pNOQDLvKGks}b zA}6fLfd^;@G|c*mp*#Ez$~hVNec_K9_e;e3j4GObw$Sq@(vKlT>#(l#qzd?xf4OdV zRezo<^>OYwUM&gJWAVb{BEe-w=9exUsTO*seu4;}p_bR>jnb&D=`{nIXjLi|Ps(_4 z3<+d$i4eP;6s~HaoO{kk$)`_FEqw;uJmxMMq!%a0APNt{qf9A@|6-n)e|MamNhx`! zpJ1SPh#Ze`S`x@!%2W;`X>A2DOX4dz6Gl7UF)|7En@~F!#`T|nqnNvef4OWcHg86^ zZd%W3TF%tV1r5*qn*BgRbp= z=5(+f&6t1*8?{M7befLOo-+vcEM8CSS}E;X>G#>=3JFJ0b!QqzyUocz}{oL`U;I`Fcq z=v?9R$l7$Z*{>H?Mwf0WHbpss!k53h%A6eS+yJ#K?ch97hxWLHSgeM(Xu-F4#CP8} z9AY1Z^ldhXDcxMUwV0sRZS$oJwauEjJzzhKI{Fbm!|_U$Ot}(iP8B1iNz50}%Di7H ztUH|eod;sgf=9G~=1@8YaT5{N2~5}G?MCV~aP8fC6ZXOWgT{v1NRl}f+-=lTjfeZs zliKjdgzxjuf`3E)Eii}ug|w@tM$=B$?B-N0=P;{M>{Me~bZWwSY?RB`p;A7b1YAnu z;u~bzPa0BFIH+yKgpv1oRN6l+CUj4YX*UEL+h++|J`P3*ZMujpZE$T*)OpNDSQwM9 zu2nq|C z+HFNp#yxdyy&DO4UUn}fHX7x66>qq55H*~qHel4<(QMgce zlm)%@p46c{^ieOPy6xxR#LXZTSogkU?#cQ3*v+-iFcrq+KyAQU8nlUeaII5eheGWR zZHv2^yi6QR-YpIh*%s2;UsXsF(kFvA&dfgOmkAdA^~|h^z3AMvnyBmE;XN^hhvkB( znA@tbgOsl6Gsg&GV&{!>)AA<=P!K)ySQZ2AHQ0KJmuxw0>DQ&7Xl4WpdKxeJyxecC zTs?GrXf_)kKiAtY$J83~7APGOsCvALwU z{W37bhlD^tfsWZM?4oN-RA` z*?dL$4v757eR9m$ik{K1uD}vvSf?Man=v_qtn@f`Gyj6qOi~vz5y1^@C3hy$vbS8q z=N8=W+%`zSi1)Lei>|S!e{6Bm5nQIXr>lKhL#@c+H?B%h%A0wwD-BND_Gp&d>%FOqOd9JAYU$vXvrV$IF2AI{cegUc=8L(Eu0aR`{bUO z#k0ZgWK7G3^edhHV})SLs}O5??6%;%DQd$%5!?!&#jvKFzG9c8x{^IuCf}7NouV;~ z(G0Gke4?h(ZYnz6f6EnZH5VLHwhgORJ-w%n#srnZw$16!q5Ceo*E#tlWQ?}He%10* z_YRpRi@kGt>ug1T0ds2km_`hW(GhCf`~HYxp4Y@T6Z<>=;<45L{x1OU8qj-ycEc7p zCC)#lNT1_qOCx^^Sp?j+VTD{F~>@Q)^f9HZv$64;j>Kp^)83X+#O~(b_v)cGx^?=WUN=%4_ zC&EpOmgj!KGdR3k2RK*mWL{Ly)cKu1DDZjl2;+V>a4)*qcCTK&JlV_1%JE8SsD(`(t?GTt7FzLc z|B2#fC%xBqw+|_<)8AqEA8@5fO2}QvGGXFx7K#I>95MUv*6N2=QLw-hv6Ex?+*qau z&*V}Gu(ZtDxhf<3A0ZibEB15R!f!zz^~}o^3~J{9rnK;MN@DB1Pojv0YN-52Zyt~P zC|a!h{LRd1nvW5RjbliFcjWc)yN=`5Ybp@Q}&-o71 z)dyEN)E7OGhrmzI_#bSw^M@~fa=GH6_6-qF5xF*`rtA3Cf0T>tuQLDnYTt!-y4cqY zhMw<<-+beB{>a};VLqL98Q|?6YCwP8lX~Lm&^hOx*RA{1n$|nvsy~)4a9|v)>EN-p zBdIm0#nqo7c7h+Hr^f!r9^baX4Zz>O{k`3U45nCiEp2u=-k?b?gA_?dA`&is=%Tf} z+TdGS=ok*zkk;#3ul^_uTPkh*R2YJ-ZTW)*@V+gMUw=Oy9s0ySPyhLpHZ=M7z}Gsd1$`}$nmPZ1i|i)g0!v9>Q!SqBOZ9o8 z*1!B~=S0L1@%1+%%p1|Dh|S0U)_1&jP+Hv$z|A+`T0ZpQE8R#W4&zMPfQuZAK5h(p zzLsBj-yggRF#3CW@d@H7c;9pZY;kdz!`^^>vcmVFkD)hg!^_dqz?a7#zkSVPShLc_ zj~f4~pZWHbC*807?7zFB>|NuR`q~IuJYpC|xSoD^+5-F^^!WK4$Z08QNpda;DbLZOE{M1P&6wchvh&H38vgGnk9*jq<)@$f`Syv{jy(`N zBS(`R5U2Ho$^>Ko{*&TC7JPZD2E6!Ne6fU70b9K1mMx%-op%HT_Z(Nak$lhbZ*2Uj z#h?Cx$Jwt&Ug{&F@?i}5>1qG{0K0&$aq&7cZb1Zv1J`r3GT?2^fTKe&>pSC&GyMHr z-K6m9?v|UbN6p;k3@pt6HKHd0)Xs%>x}@ywem@|@RBNhrV2i6CZg4f>>YswhNd|nC z`VYAiWB=o=el=}t<*jyCKtO991VR6@M5%9?jy7rFN~ZK>d9zlNY(|@#hvod?AG#Me z^w2^d_GDw3&(Q7jN&X3c|C*l>NE$u}^4Op2yWjb&@{;F0q5A_p6ro!%7)6PH5tDaS z+=Hw<;K4Iqz?lFMJ1_=AP;!Fk2$p~?zDz%|@Bu?l;PujijxOC_zzPm@yn%FKM=)## ztp`5wG;9a`SdaL%;!^#hmC@D)ZABC zhd@KW5XN|5XYqj!@wEDQL~-Fom-_hO5%2HLn|~gg`FdFfsFn6J0e8CiQv1coonE7D zy&+HCEpO^Pw)L;MfM2bD_)gP&qHh>aKS!Cb!QZaunSZE1w72Jo|84JFa0BrE4}8Qq z=%7yw?hmwjn1S#S*X@W6vWds|oQ4u) zs;+v;RnQ#k@n!v$ZQTI3Xw^OQrQ+^u*z++q9@%VR>$BsvbB^EgMJ_Clf{VugoU``W zX9DbobB`^C#_znwNX_~mp)wV~jn==34;o`hQ+;QMeIM`6m+g zHo;|^K5#Yc(|{c0O)%zLtOf6T|3`z-&i(}8>T9kmpS||_j#&y!h0}m>AlF#z@aPXC z@#-yb<8D1fe9)w8v3`jH10UWIg(~R~Pr%@hWLY|l)spA}0m1Q2&~dvEBnP~-3K;(J ze!p?V;Rg7|Kkcaf$j{~WRloDAjmqOX3D zF?0)vFACg^7^T6xeqT;lisByzz|_Y287KuJt{zGf*y24$#-u9})GlJ+Pz75~X^}62 zET~7V@H`q5jPLf_Uvamr#bf`S{@H*>Uv_c%*I#MB@Mr&gg9-_PbIeDr!ChbUh|?gx z(NkfIL&HgPT5!Sm!!c-~4Vm~yTwuT!SAT}a>iodKdk86F4XC&U(KW|^Y{vg-Uv=N| z=*#$U9q(0P(+;~^)xl>5RQG~Y^&fK}YQ+cNv?A*jVX(!!e#RU={WHVo*;lyK?zsKd z?)_ZRW;kbHy&2%M0PT+j@pmJ3bRW(_OIy{{f6#y&^a{3|aM7aX18znAqtn+x5Nl)o zJuNtfXLsL7z!o2JwO4IaYsdeA4}H`H>kYt_b|Et@gh!@(X9*;N>7n)dKS8d91LHs! zc-V+XA}lO%&=<6!Y-kR-&?_IMEBHgNX#L`~%-_r}zXIZ>`NjMDpr)3a0Tx&+;46Bm zKmWu(m1Ql?jy6}}uY3nC`WLz}6o=cX_P|3%&~jz}paH|nz~K;=VDQCNc+=yHe1SIh zr>8B_N7QrtWI;{Y*xvEqX?LX0|E|Z_uTnCdZDhO_%_eokh&aS2>vKHnBSx$8_{fEy zK?kP^q6Xf8(j(})o4TVu&bO~_>E~Vj=Y8#mUgkDuU^5wrzYP(836k16-@jF|xSpdn zV;@kR3R17zUH|QzSY+>b?tkz5z(;);{086yAN**Y=4>D|B{V~Nl{hU7g2)L6E;?Es z(==L8`b~SN3%MbswG_RppIaS)+lFq4f7s9B+_8gK|1^skgoV`!Y`nJR8vgXJzJIyj zz0UKju;@j`0wX^E2eboGL05xHJC-%|0b9K1#~LlH_IP09bE_q88PMG6uVG4^YqyiM z!v`*Y;p&fQ;i>=T_#+d?VEi9&uXD@OzvlinIE<(LvqAB3vJ>N@v!nGyKTYEaPtf`i z4LM!Y3*Jivjuu)-19$J<>wnTW=*55AU-%5?3~VL?{GA9j{%o{9Un1IiVPCDP+^wts z(5QJLh-b2DGs)Hkn(q^iAG`hU{kG@R{0877AG^vIDx72K?wyCFcbKcSTLd&*G@vaz z(B0Y-E`F?mM+@Je#J{n>Ys*WI;BtNTnBQMTHP#s*7FTl_ukB-3<*YMzmuG+DgZx@q z62wMT5i-`{-c_im1m#l;bAh0{y@7Cj&~&iTR37mOwzAMTXoBF`-RwzAS^tfy4`&;Eu7mNWPB7ryODmE8gG z!9X$`;}E}Lp}LNqS_8%tm9us8|J?KJ^Dwml_^W>NANC>RbK8mxP)pPljT$@mybHnX z@D--Q=@lkw9;vOb|M10ErN*Cp-TK_(;xkc?2^f9~E}pN*Ne?(pFs^j|sO|YQzXAC8 zRi6%e-4tw9G`T7$=p0f8n{-!w?wjFD{Z0JtTb1qn3?2%J z<&S0`owD`Q7EZgA1`ze*Bb?v-r2Cfh&fe=d*YFBDjt*!BwGjzI&xd4z95lhW8lI7- zNe?T%(^z;pyt6{bgCqfb0hfP14e+)8LlPY5c#6mUbpG%&_l&TY=XR%W2BDSjMCZ#8vw>&E(;j9OLQ3&JEGL))5f zao5F(TBogD|G~pqx|i4+=|e^~z;jFb+()AU7y6s{-JkH~<5zvgmG8X$jsxYI&&DSL z$Nn0#>jqjuvHZxFJpxAw%>)tNL8M{oL<4!&m=@4<8D>^Lfs|7G;2% zYM%m}bME<}+3QtCyddqO*5?SqCmus3t1{nU{dZSXBxvk2A}` zTe86^PlBb5y(;!Y;in zF20u5;>-HsX%FJ&dj0Lxi~o`f_mwAJao5O0O%5sXv{<;*ueUbzWeMBh{BK|Vx1ZWx zW@e_(8Q4e$+S=-Cu2}%u8VAn@M#ZzpfeqHbPd2$Bh<@Qf4r~j$A9fP#9CLy(R$f8Z zWhcRba_8=2L&buFz29yC>f`?^GXSpKC_rpAPz$Y~%gI+6gD6^ys~>&fRWB@E9EPU+ zd$(wV3yA6vgaN%Y>CaGo&J>%mzbDejIcl$Qy7|xHYeC?Oq+c&c?J={DasLDS` zfE!wRlIx)}*hE_-q(1TEE4ae+&DLr1kAC^JNaK(uR_Px@^oGvRD?8Yb9l@dQMOWD# zJff|Ke~v%$^{>1(tq#0+Nn*`jz2;$7_w6b1EK0EYfOf2B{_o#^R`)oTS84hMw?*n_ z+MI#SWPsY@+a)yK0-*K=3r4EnZegtT?1JL)i*!<7S1u54AIx^Y@p>+i(86U#=tA{n0U^^`GlUS8d9z z{G$gP=!cy;2x8R{aWf{d*$UzpeS(go2by!+se$0GvO;j#?)-6mVKeN8K4fWG9v)~8 z9k<8@D|esMu!o+`Ud3I9^aw(n>)Qwoa`-1z;XnajK35JEjIv z-}?DOc&++(4)Y<;vyKk3tlydX6LXp7YiffBm5z#4eIAeD>y5{60It3E1~b6I9SebA zn;aP&Ib;ig(^xaKZU{oEtHgR88u6Phr>#@Jd{;d94c;~RR`q$0{Zbm=_2(DVc(m^g zzo8pLd?hgA)T^c^f5F{NrY7116rYt{;6VF_enD{eOAveTROk|Bwu9BznnIH@c|Qkq z-PS1>xie`Ohya5>c$^P=ro(O68BO4wMz?7kA?}=xSAusNu5OPTF0&bU1C2|zfPPr4 z9h~F$Om4^c@3f~?Pky}pwLnX@*MCnDatCVZKdY{+u*44kyts1Q6 z&a(NGb28#L-<($c9mHT4nV)!|0Ts94K(^|;)_xIi_qEsG*!dx5!I zxjgQnm&CQ`1{by1DoIf_P=PMd>^{1-;B#7K5FQ?k`?1;SHoWJzuY&Eh55?@cpzPRv zwml!SM`Jtfx7c^^6)|f^?L%2d4wfB>>Nf-*`N$T+VRLz*c8~8l%>B z?b_|NcgDW`MK=Y@*8i@)amMREyb@iIIkQUryOuTdP5C3cf>ZXzC+OiYw`-fS$KJo2 z-vzc^sEQ0{v@aI6LTC}hkd(?un?Tot1JWnFI;F)}5FITp-9Yb4_D20C?LNwhq5Iw9 zrv!EK_KSX4;K?WpAY#T`Hzt1npHKaB85@FQCcs~y+G%$Sk9zP$U0=^*T=nj&_a9oQ z#)R-1Uj1~o(RBv`kNsQP=)zgC2nRymYd;6O@2=(O85f)X^^@Sy<>$&~eS0k!J9eK{ zcATLbx(7G=;TAz%+uXdz|Gk(?jA1&~{iI@eTYahav20rr7v;lj{q^>%N|nK>b@GF2a~339t~BI3<63a(labJclIonH za<=2IiwoaVrb)LP&-O-GUVnK9139s?F1B>uV18hnBZfJax?pj_Eiy4(m z?!H|prHs31Xd_m52Qkx8o5uP?jQ>2V(aY|0Vflv-U28E##6BO)cdvg7iW^VwsX<1Z z!ApqUd{M@XL35D)JfC^>rKZ2(uIm}j8Q882P-|z@x1OlMqc>dZrk1Y%s%ITQg_PD?QgDcQC-S(VY%6X5lCj!@$qc_>#jy`;=tIXqh+~YT| zJVgqa0aMPP8Sa~N&7W|G-W~m_&n!;1V~dPYJK5rYpw;TTKV)z3M41_kxxwGGh&tl; zEDb*r_2t!72UR&zY9pP)d}*Eu?%88cm1;X|pU|G$oPlGJfwtDDLDeI+TU>hw(EF^` zokq=D%l%GlPI>-Iw;*#yJctPgE^LrvB6@e04`MHg9r*|~^Lsx&*gv%vmn@KW-F5aB zz>PQBuXI&@-3>W}LyKv%fGQ~Owo~K32h1kc1uWd4^H+V#`kkrrieEu`pPf2c5n4Er zP!9wBhInE;Zm#2zzx&QxnDE&D_^d!R5W?l&cWqu}S6$AJSY;I?ulTS0cFF1(sz7Ig z0q$e;0W-TAFjR$3gJ;+)^>ZI$PS*GJtB$P%JI=7TI`&*q%8l=~n}G$t7jZn}Cs1-D z9~K0j%3+lLF0bAb3dTb-rgWxc+Xvec<4sj>sfd(!?wp*TR8|`c2sBg1$f)Ex4nrJ!|pM@USfT zrdK807iiMk3L8GsFI$!0?N*w*%tr4Ye>O{Iw2zfyQGESp0Ah8+e*94&^xexA)z+UD zPws_()ld3zyl&JxA_H*_KFBm}QsBhkex7~5QqHoUd_4wj2R9h!UtW&hY;OtN@*%g_ z#@yoB9UmVf`ZyTeviPe)Y)4L%IgIqUd=nI}*Ft4gPVF*UFD^<4rALsC7Vj73 zfSS_{D1RJ?UkUVOf*O`}P4>C~Fn3kcun1pN-|OjT4tcB4=YMNIdb=~+&QmGdx&OI| z6#pIk65zoD{i8Z9X22HjJWhxb3-rSbbg(8_$wai(+2Wnrq|jxJ29zE_m#H?a6DHqa z1brwf&HrXJo4;@5#>)WN9YCuqRMoEmh9*0c2W|+$G2q?rY@ev_S{-obZ}DKLb>QY& zyc17IHA4t~&@CFc>)?_9G1~mGskY;6yWud~tv}{b?FP1ZklUDBTskw9?lt4bl9^hM zzO28kxfT~+wl6;f;S*2Dv@y4M?X9Hs`YYYb?6onscvu^s?{?`~J}T6|&jEx?dHkJ= zz?rc138*QE#ZtP_s636eo2p6#?yj%?Ass$4|MYK7%?yrF2HKkAM!=_@)w-Zh+*<$3 z77*1su*H{!(AGaV`$Gqr#FgfVom!nOuKvM ze*#d|bb334Ae1ze8jd4!-udWP6D|n7!Lb8(XKy|F(NUYrn?A|AKh`IIxL3A|!Rz^_yw!z%y3>?c0v^BSL=PqB%wf@JvuHLYo*XH$Z zcvQEj4P2rAT}rwIAvXBP-g@-6?zpqWNeCmFH%#vu3r)u1~0HS{mi*U?abtp>(7l7qyGh8s?U&2aanuF~ORFezca`P1+J z<5B8a`}umEBCLvB$H=_PxpmlRX2f+XAJw)P(JGCw$L&Hu-t+fY;dC6wh7U&%**8c0 zGXe580!-;wSUucK{2p_%SMw0oxzNp(#-}n=FwSp`WG_GD?;RJ%+8R!@_^DN|VL9d* z@;AXeP=Bw~H*1>}m4^tA_^ooyjmFGnVOu=^{Rx9>4W5e5V9vlX$$-{ujPb)()HAi* zK~Ot1gX!w_j=COuZ8!C(re;bpLDz31^V5W>KomfOY(h6fA-h?YV^rZLHA<~ ze-9iySau(@)A}7~qV@>vq^N6`$Ur&L!IoyYMBP!?8yc)MExnFgFfEMOxv6lnn`!e= znf-%Mu<+`;ZGP8ir0&=LOg*oCc2$M)C*CPFJGM@qwV$s5$}9AV3(dl7zsw2J?|AJ; zq|*f!bU`lt@$63fbu~WlccL_Wv0soZhxr*!d{%(MBaV)&Zg@8me^uwQ)Ig^%B}9F- zPBOppDu}dHgX`&x>I6Q~=G185h14Fs_!7+0&EtBT&=#)sB7Y{X&9w{6JM2H05^ho_~Ejy z-ZLGe^5 zvY?R_3b4iPGFnS)@nzk{<7}MlpjJWo?4Q>-pL;RKyV^Ip&jq7KeFJM~sBqj)wY76> zH2-1lS`%bPn~^yK+nIsB=CoF;B7uI`se>(E`_Vu@P}&5Ibkgj9_(~%dn#T-mV>bNA z;Ui_YCbe_gsSOxGu7Qi1`dXO*pE?9hTl=0i%Mlg=h8^idk8p`+nEmVc4T;6e5c|*9 zoZ(er*ysA3;5-P`rhdPKUK#w6l9-GRnHG0nwxlYFQYePX*Co(bO(Rx*edk}V(4dRv z2GFzDesry=*KlnI)#W3vBM4`f!bi?t)zhj-ysfY953#3z^+P&Q00g6PTe z(k1=9rJw{qi&nG@vwYTH#X0~#i!_gId92UBo%RxkFFtwm1<WX;cZiq}czzpaWM zllO63f-;WE?Tc|!GsN#*&VYj8Z&z3P)zBp#XdV!>%HaKm18MN`mA?%(ejww$Z+U%r z@87+?{L}m2QEs~aYS(nm`4^XaJn-^z+2?(6x$H4tY+s*=Up8FXjQMu-+RqIARg8<2 zsjrLAn4DIbg?IfapWa_-E>iOp1JviX%Rl`jSA5mZGN7@Me6oioR5^6~7>vJssP+c+ z)5TTe%l=zGpIG5NeZR>zUHX6@YLVhGKN~#F9tK)7Lha*94Ho8y>#6z4^IyC(oHNj7 z;N1H>w%q+ozpL!pYj0&XZU4phE*uv<{42}H|NO<}#*h46)4Ms;>(#53IjR{ORILZY ztA3>rcZS{ETT=hfbcFb7)?p)#6x#qv=AID-D`d3)(OI?ci7WHqhSl=07j5{k5Md*M9n{ zk@WS~e6n2shEJAvz2P5n%kNy3D`nLm|e=Flh;ad9W zqZT_J;p9}Zq6Mkvt=DS-=@ndySADDSdX?g99dM1iAUfC5N8d#JsPdY(v$GrYSM;u$ z8(vJ)MHx8vKA%_a`K0f+GO2TN0n@I%`;F(h zq06WGmrmJhp*)aA=B4jX2E`{I z;yOHtnFuBLfy?@P8!_)ggZO(|$j4iQCSz)|+r(q^&;`-1?In>%X~b^x4@aw~ z#8F$->AG87{TZ5mZ@?B8U#kOr|J~KIZpddk*!X|*Mc==0^UsR7L4X&T4gGo>KH_WZ zdp4r$Hs%&rf2*U#)!)j1!z1YYmFA;ca~&fs4MRAo&)DV1P%^I62W;_0ywI|}mImJH zXmR>zSstE=_?ZXNtK~zPG3jWqI3G6b@Q#=ly7Yd#@TUdXY<;O}W-w=<&A^^>F0xnt z&$06BDy{7v;PCiU0(+}q(~Y9F`UD6%&LuRpwuTq*<2C$1kHB~Y7uCP_wfvGZ7;VTA zzWB`Q>_N_$S(Y6ReAg57rU|^tpou#V5EZ zAp@G}^k6D<++Y)o)%xm-^SeH z;%{}dxcW0}d0KqHCqpqlT*WM|Z;RPHp!U7CeeM_oy8J5uHZKgVBWrx%%oV%IW-b_? zV8=ADJzt#88MxaQeWyM4@Bcv5R4#anfX@kRx>;#ztriTOs$^iT-3nF5g4DKiR|rm@ zp!ftAu7AuU8qf}(i4{6-U4p?D{2ST-Tz@ks-7oYH_N?lUM-lPcl#Zahs|ILNNQUEr z=D4S%LdTuoBrAVQYw@n%WWoCYF(IeiTk_E9>u^Vm+(5^16B;KwJjE6fFH`vEKLA9( zH|#(oa9hAw&7klZ0hjd)2+js8y#72C8IQj472#Ez#DOioNY9=N%oX0jU4P@T|3)@{ zSwI#Yt2Fn~d|g*-V{Y+v32#4>*Q}WVw)mD=ZewooEi$Rmf0bqeBe#d88#*gR*|*Z2XSFyop{4PSRc7tsG28lQOLiGb_uVf^&3 zb%nxEQhn_2rfNK{96Xu^GyA80vn)=7(y;{Yy|5Qs2gV2fc#OZs_* zALJ^$SXF=U$7E||`j3aS?iNBTQb21F)bPQL0zC)Hbbvd z)~R1H^MgN8_omD0+AV|1mgaz6@7TdlU(@Lf4q&tY@uVQ`n{i`qa|RY>VAuY0%Uz!E z^o4y>M7|-g70qWa>psdjaZu-iwdS3sc`$+qQ~B#`Vg)*GCqWQ*rb5S!PY@ICQ-zLO zr=T&$d=99&Er_^O0UfuKpd;vl0Cc^)1u)>?qB+(MSiJr2%YE}*zxD~l<9>hS&pRNf@DD&C z&!bIbq!h^FX|(#ppgL@Hng>xKi)YX}_w6!h&=<9#o#3dn-OXPeTqK%keA<7}2mTV< zW+rNc9yZ5q6YU!SHvO&*-!9GqU&uoqsrdp+dBEF^Cy>qQ-B&xqH+x zd9eEL@)1bfKzrx^X79~|ZrjtkzIE>0w{MU1bh~d442`V_f(&7X20@~zr4fxvf<-LH z1WYQ%m_L$ADiz9Tl_o`%U`%15ro@oJ2v)$@jUXT`0hEq(Gcs?!U)cCIx;A&6CKYvY=xn1ok-bVja z^X-rO7I@QF{0Gx*cO7s2N~?_$d={xJ_t6RfR62zX7SMvK*#ATe>w5hPusIo{ByU!FAorieZn}bL>Q=#s|}6IUm(7M8{Rhl z=+I$~CjY75;ZjGF;Liq+RSt@^5~@ZM?rOtgEjh3AH-8us?mQ0ePxqukq%**Q*jixW zqLw4^7u@|1U8?5C9CpBGdxaw!Gl6gpEfH=5uIkWLsh@xTOw0LdUdd7Q8!#0FF9mj8 zW1I0$V45BN3_;dye~*7jJ9xBYwu~1nlIi=yi?;<{{X{jpz{9K;u{HJpi*5xBbvCjMe~S9H`>It`lkqfz1`mUKkK)yyYDXss!i) z9|U5`fMX{%%tjuCk};}kZuzrx!P7NP*+rbo9WQPOimlDuPxo8YhpLN7^u7YpZBS(4 zRwuVV?1`4b<^eyOOj*ExO94oA>Q)X5kAYyYHVPBE8jI=(u%NjF8LJ=s2!=~B|k?JK1Ar)wiIjf4#dTr zi!jP@@<$%6>T_x;?jQQ>D)!u$#iw6w#c*5S{MZybzJL_RzhSTF7KCrzhN>T$AGV}( z;P{Z?v>BexCVyq)li)Jwja(#e+azipek=c_O)5^}z5D`#6}#^l3xFzH^I2r86uTx2 z_&oN51lkCux|{p#9l$nTg{A-SK^9CyQC(e4kH8R^#x90}B@rdbVS7;#0ESvRLp3 zcg|E*o_O{`4-KO*yKHr!n5IR@Z6}4~+dUS0xHH}<1fThFbOXTT9q5oxn?L&9W+!R_ z>q---vfW=zGdF$FK0`}ic6|if_RhHf&nSQ21ykU}%K-M*T%8v!1HE2HP43AMS<&b7 zVKKzK#eW&lLd+-qnSkH4IqOA#4_{0z@X~wVF}?b;zV1{hfA}Z=-Wk6Vm_b=%z5)vl z+)+F~L~rOJEqoc;-W4R!6z@I44R_dEldy~Gf%a$1r5VRpaTk%btHnTah7x?qU%v$~ zEO=|agOy&qtP&`3Hq|Oy;;p*DS0(1EF-H5F3T3~-#)H>$5$gdy5fl&mJs0{g+0-2j z&nrRFF?R=`@brJZ+^1u1gYyf2(COQw*!I)Y7=lhGlRuv>&I7rDkTFrDa2leUBqV?( zn{EBzwsGZoF^~Q=0I&Du4S#1*uyw4WSe>B`XO_PLsP-Agm)b8h;>*dwe8NOujO|Nv zV+SRb!iWB9nwcCT7&tR_BhDG0VGFr%`SVPl^7kg+vCb{3w(>6mau+~_lV^XO$MZ1| zpx4c`Y8u)_30~5VYhXiSl4}9v_vJ;|0+(*ReY)>!{!Goi6Slw;550eSt|7Vpsl};*mz63edO?fks9L!mZhG z#La*Xv0;J^v?m*HXW+;OqK8mJ<1JD`)tT5MhvJtov|iJ&;7(>R_(^8V)>F-XxW5d@ zh#Z#@>jZZ=3?$!KD9R+coxzj?tmk1{^e&Qb|lEV{m-f#+g8VVrui$G zu}#3;7Y*IgYVSb$*CkMuwLSyVH1~OP!Mn5t+XP>K5Q|K!Y=DcIKlxSEJ~>hq$a6BJ zek&`0+RWeOn^)3o>lyQ?W5tJ_2jI*xY;X_2d}Q|JMb`qa|Dr#nANYCWiSxev!gJI8 z@BSOozuN3NvH4kX*(Jbg#0DZ1??B@^1R6mF0#F}YXgK0CA0qtzsAe4KV&lSqu^WgE zLJf^q73zsfa;@ykBG(6V046B_AfNG!p_f|IP@xbj1!!D{KoBp84addNN*pSN*`Z}b z1MZZUEgF-B3E-=H?EA6{q-^?`KUW8u6u53d$Tq-O<%%ST#j0~hfPcx~ViK-}v&42C z+d{=A0mnE%bRq+eYf3^0ZsPM#7##J$5Br=|Ikv5iHMys65GQUO$*%hnzv1_{oLPks(c^*m)}1<^XLPo zvazAeVDz~4L1_I$re{NmXVA91hqQ1YA7lfg7l?tCdi!jKnBK!0?%dDpQ1b22GQtm9 zpSB!UaI?Poq2U)o!^9m({P6g4d@f-E`L}F>wM3u*1}uDmF%yVUUvV@Xad^zhIA!;y4mc*>Qqvf$8L)vOG3F^X z`Y+!8=ir7ztyRoHMLg^^5jjf!ehjz?8aD84Tf&`fL-2JzbW+L73QA}+9C2t@o_WOlqaOH4Uw+l^v;`Z-{dr(w ztu)As2soTFe;cB0u|>b}310S#S8eGw6Mu%5MFZI?B`=z6@=0F+tFIP_ztSxTZuy3# zdyf5oR{1M^PL!Rm+fe!Qqnr2F%)}Vrg*ZN{7?FT&iTk(qLxDir0O8$C&t`Z-O#Q_HVuNROR z?+`{EaN1^pj={9Tmf%W;#x7d7kJ|y<3?q#r*Ne1(5-SmQdwaEG8qR`-;9UL84T({m zG*HNcBf4O$Z^-hy=_WMalK7)8h)w-hX?+O76B}`XJ3irVzZq71&}_$s3SaD%ZS$Y| znDmb~G5AT}ulT*+f{_yybdUS9z{Ggzxku-`&n$nnPui;5Q4OgAp5o?LCGAP~8V9ym zAUbP#+P!E9tCwxNpnXH7ztGjk@(oKDDmH4Ip?=Zw?;Br^zs9{nrfrGY(KGhB#@G1w z9RL#Scl{$;iCePtIo3b4^37RULw8?Z3@vcSE8Z}@{)@ipRH=US=YC*%>fv8Lm5t*l z*Z5_|*1uZ+ncwvfYG{_wgr4DuAE3`vDNCMW4sD)3&4al3E4)@8F@%8oV5D(qyGo3g z$3-|kS3`=dZ?{?WC8qaKLyvi@Sh()x2m6JN}bDPMI0E`j@+KR+8+#yf|9mzIXoV>6Y8>JaMwm zKK7C6!|(aN6YC#EaT)hD-fN74GqS6ML+x<+=$6**{c6)TwfI=e*L?vysOxtcrmmNu$lvYN>Rp?OLw%t3olw^<8KU@HOn8lLK;tA zm_)RH5&7=2z5-Cii<}}xudB#a4nokfJXb!-!A6Qac+euLKOOWkCmy#7RAyOlQ|geo z1L>DK35bvg)&Pcw@5Rc1PFJ4%&~)jTCjk6ZE4T6A{F$HQ9|t<@_W}6MANk{6`8xp) zPkm@^gybD~+WeP(sP?MMP2qjn>9$gJZnNMm?l_>OmuTiXGj#aAx|cGLvg?G+j6UF} za$2ZM&nc}Syi4$`Xs8`&kq3l@BX{Jq_CV^joN$s zW^RF3|CZk|-Sei;I#r7Izx%tVE6*SAFP)C+kTsrv>oSk~W@IneC5s62xMXUU2ao*P zUS?K7EU-706tya2T){J+N;c~U`isrdu1GfV&?-K(Vz=aSEbGKn1-aYGMUl!tB^{9w zCYc1dmM;fG`ZAtH6Fch#cCB9tBe^8tHL59ztoLE9XGDS%bl9Jl4ugchT)o(&i1j@u zOI~#jn?E$JUOC)Cz-8F$(2xqgtY?gJO2M+ZtQTwfnmNq0 zDw!6Uo?GjY>?VfRh-O1TV(<)ht!7_wDuWV_t%`Fn(dc%(m!|#-;Ajah!9*SkNmL0A zq_8HK)&jw2zGO_Z^!CA4x)qnVmq|nRYNKYMq6zvM2D;4D_nvuCNqIxA;?a@^PK;U* ztFbCxP6PNk)LQflU~7>oIXjUbpBFfm?bxBz0$q?gv0(v7v2W#9F~BRlA};=wPyXU` z>D6CY3dcOMv48fLe7!%%R6UxwVk4P8KibkcllOav#$Upi!;vBl=^(^w{hik|{DpHbKeN`WRQ%{uAjWZPdU zxG8<`Jo8Ba9)>ov;r-#JYysZ&f9svc`=PS2A)b5U(dnU|_}-Cp4mjhpMsKDpP6}rZ zr3A0>obi9qdW_8~kuNW?S92_$V%qlAZRPIRRlxZr37zGXW4uebTfUGpC_0l>|2zyv}204 z7Ar4sQqtK%e#tkv`YCA3S43*)A0g;>~d$9`+q@Zt_YYTo5zciv* zN=p=N0(C)mN;dEJ7{sTguBOXvKDy7XMD-@ickBZQ)TtQkN&Ocg=d~P zm5sxdnPr)2fsETC8)76ZpbG+&h62}f|67OW^+!a{UBV1L$G@dfLY>d9{QYA*u5mMmgX2^^aA563S!5meC;rtm z-H})U3jhE>07*naRPohXXE%SkVW-kHAYRc!e^);7uV+u^p8wFo%f>UipOLg{AaS|< zW{I;!l6N51M||xqaRsjV>-yaPgVkf_FK1pbG#~P6>sr)mq+izG0oBOw)^c$2;zVlv zF*oWCK+Ox^fs2(hK`pwY-T=l>4E57wq$$a7i@0lLc=e!GZgnta>Kz!D? zElAyb9X#YAtV+dd#U;V-!YDdr1r z@mbb>n|~X!Ay}`pY+!xZN-&lit`qDCHd=U2W)5&+kNVk=-B>8ehc|2m%N05WUfm@q z8;-A7KBhi)2v_pF!BN^MUt{QYSIQUzm=r4<_xSfX~!VWOT z;Wyr~c;OX(>4hh!>Ersj-&cPDTGziQ7RPi*BxjcY0qwnR&I8-8_3I2;8`rvh!&g2G zW$d~f)E@`L6Scq#k-vf=C&pT*T?r~l&wCv%`}SJ$4kKCAUkmUp8)+<;eYa%$07v?k9S7BSx=ZQZ~nT4YGOqx6iqi zfUwRMd{=?92K^s?Em-!OCb2vu;!6^~ zgH+wkfq%m6&c`J>0C9Mb-*W10UVJkD?K4{XU*>wD^VxzF?F9VYcedo`HWiM65*278L-Xy?8@~lE-FC-x-#h=*sSyY~@45sW@nh(HQS5eulgwb%Te?&j;{;#0MoJYL4Ig+J zFMs`s6T1BTGy0`Jz3%r~7sj$CK+X9Yn~ZrdU=>C&rTMPZm@jhUy?|I(cu6xC3iqbm zR+u+4ADSxh=e5N%y08cfLTad%B>+BsABsKsUzi16IK$Yc!lw8b0?&$_P(O`?M$v{ zP)0|%W29;yqG|D~cx8+KGVJA0jWnN4;Y{-Onb>l>UespGj&6<;(on`aavgC=*r3pc z$YOrciFAyvEht8>xWx~C<+1na{Q#ewF1_-%Dd*dVXwDwJa{0OG%E$E(;#0eSt!v4p z(o9B<p8zu2frjoJqk82lGHUbL9mkYg#AGyh{FP?#s>90xx;pXH2j8oZoe-q(1yp-#78r zziU|--{V|AXupzBEyf=I+Yw@ir3Ogx-ohNopoeBo9W*;kDF}^~r)ZW~`3q;ZFQJ-) zmc}83f{@}H8fC>Fb1rtx*Ms84o*yfp1CYTOu>lS$bCO#Dh>3CpjO)+_**an6oX9vf zM58#c*n}Xwj1?r7(v^n(!!BK#RsY~Ov0nI7PyYL>0r4rdR}^l~krxS+Qa*t7@kUS0 zCjl1ewnn1b21=q(j&EI~iKkCFN?N<=r#8GX!j-={o=fHJ7 zYpokGf3-l(0X0VYEgi%i0R5%k+!<o5qPQdT?}L8FO1QPA}IT1x_9~_0B`J_}Q16 zs0H*3e{cSpKcoJw%d3-Q_T5i+GIH1R-@37d z0qn&Fbp&FBx^W$Vu9pAoFBVTs;>VVu6;}444JEGHyxf;%Af@8%oFT-Qpi5#0sz9w* z0XJbgK?$NfC^!kLYTx*_69Y>xr@wqhA7w|Q3?;mV(6tKqa*<<_KVaO>(u z(Xd8w+2Yd8g8*^~6^H}TOST(M%G^mXL;gy;jwZL7K*v&$I&sbML2y(!!d|r(_}pHm zndQTLh2K}WXe>6sD)-7(+!yrsw2%Dcbos6~OqX8%juX8xaPhuie#0Nn8$9)3<AP%{>;7EZFL`M)e@73seH8&j-OM-QBz_wrW!s8enP6}cO(S5n$Ti|t{{|BbK zKIJVZ&f|)H;qU%;{S6HZ51td-Ik93U>)OCT&pHsOiR?8T=z=VG>p&HyS95Uog=PJj zrvO4K4Q4pfED+lV__HVb?)9Vb#CzC!WgqK9=i*p|-)18bf|;O9OY*w|*lsW>rIkQi zeaUaVLB{bmv_~ms@v`02F=*QwTsE$_6*~(QB@40$(wB@+Dukn>&#mr~$XdbXu~C~p zR_MO!UOR(A`?wTLH4ux)lw~Xw?uD{7%WhSU)`9U^_Rx&IicUVB{;;0R|L}DA&eu+t z?tZI&Ku9M9myY)v-Tvjlr#`ITANb%j=@rB&OU~uF=oc*iZt?Ymbblz|r9TQ^S~OX9 zR^5?f&Lh6`-Hnq!Z_Hf2qpTL{Ct^Gwyzwi2GP>l4`j)TEJieSNShXf&w*E>$woKxL znI~)0%A0MvuibbpaObP;n_l<%zyDN$Km0TQ=k)X=A3T+fQ!9JH*8uLZStGdC0Bn2A zH?%M2e{@WchS!7|+qD+-NdkPyn%4H$24iYwvo}P?_K$h6!Sah{e2;6?h|jrn0K4+n zn*cq-mq>=X{VnaM{N`n#+|e1ub|+JzZXFx-vjxbqzAeHfh9;u3O4maNh6kjq z}3=ov+b7o|ox!k7)YkuDnQy zU+=s8%rrg!xIUlMhX2uNdiD|7(NFs(3>>UVctFZlJVj3Uv#o>V&pdxQa$B^+oG1lW z%-y)EAeJ18x6r7s%VF}d9C(=gwaVXF_8cQ8+Ng();T*39T8$-9^xxx8NZkhJuLi1N zInlNbcG8N2{dK$T>o-yhT$=9t8vWAWttWrB=;=ovm>&L@-+%p+&X{bNaUMv3Bkq8s zql(AB1b~$D-v728w)4c4U<0=TT+6rMRr0S_%gq^X8py^HSdHMeSPltpp)A-sz=cy7 zx-H`n{QaUxEw`Ky)OU@nR=z^FPjt02H_q*Ie&L$pv7&6`l-R{a~ZvWuUQGQ##tJXD* zpAzOOo)ZGOTH@P`zoHn9%5i=$5 zzt3LlJ26t$33EoLDmt^hWdK@mkzm%icT?;2sfctB76^S03IT52KGz|_cOaPKPN0V&9Z=4$E9>n}9q#&VEtWMV7! zM?LVBzd6p>Tj2jE*bQ9ssgFDb?uUv*c9;0d9bC8Z-SRr* zuD@h&@%Kx0R{{Y{^|eVgK6eE+7C#y0ZY6$IYI8H93U?7q)`2*Rj$6y?P$UOp!E6?} z#Y&96NQ#IRVxaYxYWuHE)&_CvBv9NgD|0*bJy;!lp^EE)_Bk1`jn#Sb(d^0LH{;fZ9s^A}a?+;B+JfwGpT~|L15+O%V>mLQhh~Li9@c(#sYm8&yisNs zKGH@zi9K@@xzLbPy&eK=rCc4<6ZK6TjMHo^9tZ4ii}8aTw#kD48!%fC|Eg%W=aD1D zz=?qup4b+Lg~tm}=jG&_U}^jNP1X6^=b79|*n^0so8JtnVlzp)EjsCWp-0Jb62pdT z3qH%O8XOOlKoo1kF1p?5FxwN^v|>xIGy}bXiw2~4DHa}n8In#6&emGKx`e*o^%zQ7zWm?>_&z{jW#=^Ta-<(H@u#Xpafz z4Lwz3&kxVikGkUb9XKn%X1)mKu0ZS79|@q~({rB$mY`(!<%Vp5SN^82oL=_U&pTD@ z5B$jAoGw52^r>u|l`=z>5?Hm(WA-{jSX!$>+mx)D=Yqz(q2(LTqL+2Gp=X%zu+f7x z>eqZM`#Gu3en9M@mBiJL$u$t^5npSQ>*iMgNF<51w0TWTMk8&9e4Gb5dCA2E^lA_m z8a76iCYFwlZpCi2<}Fv9BAAv$sepG!wmB$sqeD}rBul!2Fk_}yipXILIfcS4QI+U+ zXmbafIUY*9au7X3ArM2^#I3eAh@;RD2p+qk0ab{z+Oz5whgQ|CE~B=5j|{`8j`rNSSr!{Qb!+g|WM-Oo)c zu$(+A4qr{>!~i4l>+ig5K!<&~5nJHayIwxM@yq}CsX~AB7k_N}WATlGq(#f zX7MBg9PtQ@S1R7*XF&>{4 zSrrpynmEOq35}Qdw!Exc-d-wKd&j9h$rU)_5tl>fpLHb1m0#j?!o z;*m=vz!9JMwK#k2_4D%wnw?J~Fed zUZAqE6At?>klbnkLu;|>4P89@_o{@oQX-muuvy=4Cbx>vdXn@H0O3cnktm~Op#J`!Nc^ykgL5B2Vm7Gn7=6|;g&6{8k>{&4 zSHA@;L_1dg;_ z{v#K-miPW6tF^n7{m^#V3-2KLto(uHZ?Tj(0=MEBa*l4=VU!*9^G5kBh>( zJ=g8M+vcKQu_G1QapT>TU)tiN_I5|SNsVnGUxcimUr~KC6IlcT-zhJua z75BPHp83Q>(__E-)6*kA|9?-Td-^AgtmXBWGVfF`8JJGfNT4?QmEx+cYe^~zhIp8=-B7va?9_BE5f#~hR zq19C;o4SP@sAUx~OBMfb^-f%i+-!@CdSzlm&Pz(*dr4w*9~LC;+SlJLW3R4SxRs9? z%6#Pyj-CiMuS3->=0kIYcK`qkbalpUGeZD)_RWg8W}wD zYlv28Bfg7Pe2(a(fO(M(VZ+``JODXL{=T>83Es09JSBMe1P%@y@$hLN<3I;a_=~;R z1zL`$i>p$3LidAqBwh>D0ws%YZSeeU|7Gs;Bh&Z$>D*hiK>1}r_F8^9vhdaS_iXS= zSywP*;Z)tmWt(U?S+I8w6`lPed`n$Q!Kh*xp;i#Ivxen9CQ5#!jd*b-+eF}qhrfoS z%!o&Bq>Xs^YdFe`_*yRYeg4%F7r6g9p=HSDq&MS#nO8N`82g#WK0#1s)OGY7GjM)B z8#xbnhG@@0WM16m()5Ne`R3^@U;ph}oB!A@oEW_MYyaHz=CAv<>6Y8?8d=vIf9bvN zm|p!^Uw10MAO08r%kZI&kqIn(fgb-J-|Pc5_DQ40d?ge-{Bh44@$lz4 z}K*PD(##MLFGLXdmie1?!cJ@}kflT-J@qrfU&_}i}qF)H4Md%XW0jM|@4s@U( zCkDA0iM07N)O^K{Md;l*oc?e!z#^%CsElRaS#c7wDhYHUA&FEOlaf^~jO7KAF z8+cb7cq@%~>>L%wcHm+0##bPIMttq}FniV?loCha7QY#z&k-L?9SMz}z{BE=uRzBa zl5UVe-}p?lS5e#LaReroqaKm6CHr+(uD*Sz5_={3Li z`Ir7q-fvGl^nQJ%@k7@<_9HoDEHl29Uf@o>q~Fl1Jvu!TtS=mxvWXYtr28M!&wJru zAKFq`o3BcpQj2+vJ;tGSS+_X(p@lcP_e88qQ90y7Z+_~ExuwCVEaEnVaxs*%R3L%J#*Yi4512YHb=V(Mjk}+R0^h0+9 z{Ulw>ij=v4J?MZJW4S}$@xSeN|HbL9SH0=j>EHRvd#6wTcm7+yIOkgbWbW&~@DEOR zzVeO7jyVdKUwCf1|K0kfzl+-Q8n@o36kniYXP{;g0hJtk4Y0~eK*EgmQc|`pdlvg& z`C~FI_G!=BfuP}!7zka_cRn`s(s7k>Kh6Zc$7FZ<|xxfx+z5-WW z7va^g^h|H&vwaX(XbJr%z@gOUZ-yHOA;h!k$B*-{YVl*j=K@`+LC^!nekxCylX|@} zwL&2lQwhBAIj+Q4kRy_zqE?-TQI)m8EM4~FCJ=v)Gyc}F99$*7ayuV84ZGbH&#~<% z(}L^t_W56j{&&A>+yHgRl9sOZ`lEU0O})EM!@b9B_ai3^Qeb*=65yiFTx>2wu9!Ff z@t(JQ?(|k&`LDn4KThWS;0}L(f6slNGyVEM{Z2heuOH?*=W>^xMR@J!{k~IqdHARQ zx9J)E;Lk-b9-j&_kdX`CBX}!(hIW?FVn(vy*7~(&LDt6&J&ptAIr|K%*$Z+)vEIOE zTejKGW?QNU4@6|Zg9efXc(Ov=cgzW342DGcPy)MFz-BmmvV7*7Kom zC-Q?mPCnoN&k>dlJGK3aCpIfMp6IW|D1X}ztmgzOmP%U*`D`PJHIH07C?E?QS53F- z(;+U4Az!ApteGev1hkO>4(QBoQ5#H-fs!wW4Y9la9O7?Ff3!K`v)%Xt>SOy2cjU8W z|B!e_6UOBua2|#G{MF!H=oiY|KdO)TA?>}@KQ@I;6!q!J>>B_Y%FH9ko==jX_-iyh zEz9_)_$ax$X+TYLP4l2kp|5$Od5ftROL@)b{+{Vmzws}fYV#l4>g8|y?bB!eyMJ|h z>6<=lB%eL5e!K6Te_D4<-wRzBum1Gd*4Xs;^jKHqHH@kD4zLhuomm>)@t0C zKTM~c zjnS2r*XqRO?$>?#ihA@a{eJ(uzDs?sUmLy1g|VoQ*X)Kh{xc7}`g_b8vy8^N20P>U zJomrERBIm&X-H-nA4*`eL_x$K9cCzG+=Ibk27yie~MO&kh?WFc)l=2da)yRYM5LE%Lss zl0aQvNmKUlX&d+}99Si`fi`DO%-K8sO1SMV_C;7|k}12i(X6q7Ti(3#SNZJoKd?XN zwyO5L5dF_iu5cJ5jA_o9Da*lf7k_SLn7y-2na z8?pbn(UFmYHe~}H3^CMkfZB=bY@D}Bwr)?#u5o->%5h1|g_t=9z%4j3b+r#ep9U;` z{Rao_6I-ohBaJ%)ujki2oj{hT!w~k9^az6rA<` zhd=$d-<|RGr0ZR<;uFn!D7J24t~F|_mczqA^1Je93Mb!ocyNG@K7QrX1{;pXJE~4+ zvCqXwC5IA$ioixJg)~mx1*o7{WPG+oc#@`CrQ?R^W&) zmS}kjaKugA(AllO>A!OAzG~J!|7y#l9XH#vd*LdoSC_ATSI??zd%Co(ntRq3(@+Or zfk+m4J{03dsHksELtg^SVH-9Tshh6wD|w&(`~T~STlCkm+;-PJzT@!5ulQEA@y4^- z#}4DwzvXLvSK-)-gTnpq{_g3@^Un^H3yf+wvhH|@HXO%uB4^Z6fP5_9aF_hX63Otl zQo*cqfihF`s-b=8Fhe0*@ii?0*%mtesr`ol>*F=qan$-=a?AR=Wgj(kyU47+7N0$o z!>bsOfDOzM$%2*2VvGYzZh#UAt)%7Nw;Pv6VdLdiBH!h+8T%hJ)k)T8E6$$Kk>zR` zyy}%3T1^A^%7lOlccTd>aQFgCO_sd;wv+Ab(9(LS>O=luy{6dzDe`1xxg^4 z0ZBW~?GkGGU&A23p-$5>=G`nA^WxWYHZ>Qp>X!j|_~>v`m43O;@A!{&<$T9=%Z#@N ze#SR_$BF-J@3y;NHQoEA|K6$M|JX0QYkK_tpFfq2Yp>j6moe(`SrK?VCzw=Pz*QZ( z>UjdreDR!7C2(WyXnXU#n&qP>flQA;NR1!pPcpHIi%dh{h{LNAi;+OfBLl~Qg!{`zmsE>IKBNvDU@dbjz z3yfS~Ngi^B)z`H#1Vb6X8W3yrW1|qi4pH`@LNWTZB zodQepkfYk#1$aRF)958uW6kq|hh;RtJ<^5Wsh5~`|18%?yMSv*9h|!#H-%%&SRV0^=jhd7c&l_>ZzQ@1CSu0i32bsWK@|!-W*+9dZf6$my^SokWb;;ZZ z7QVn;@r<;xi!tmoZ_99Cxmd?@iH88QKuAPNBsfP`*~(@qHb@o04z!#Vf3P>(4!?qP zFg7XK1Psv&ixKkhm49hkL)HjIHhgygnieh*Me!9ohswLOuuhCVX)wtaRA&LJQYRGH zMYJ?v&hH$+U?<+sQ?=$cuy5I^7{n)5yTtf*apzd3qY2*r4&&Qm%YtvociYlJ=KDx% zEoH>rwhFigpX^D@v_*VFcOVAd30I0d7 zu?$0NXn>;xCDc_N%73-M&|oc~AanCxONVT;=~EQGAqQJk3v#=kO&t~w_T`G-W+7t> zIsKbywv>8paJ4vLSw(`irooe8o)a(`M-_|3$lF&lNEO~^7O)i;G~9RmjYFnDc1&#& zvT5d)C)2uaCV1=_t@ukMu@&UFw4Ot$JW6=B6MH-5MeK05+Wp8e z-nJy?6JO?NjABG~0o*0v?i@_&Rb;oOcL+BuJd2n6oOkkmjj;X_;X^<4*RJ{+VO~)5 z#;^R=Q-$-154>l3>;pe_DjU~DS!1{6p~j~0+60M&QOm$yMlaO4c+iVC4}NhrM57B- zaaD(|dOnW}!0ammrFQUf9iJVvBZcDmUGq0KGUch~gxnW`s;^HFDw)_U3q5rq>H`QI zaWf!{0n$*^8+LF-L$(^fjIwb^PGoK)*5BT%;uQ%piynOG2zm*0C-6d_;>)0T*wrn5 zGqwtfEHR73mF zP;Rqsu;5*FUmMpU{T@iyjD0@h@OBnE5GM|H3N_n~G!r!k%V*CARrHJ8OW@22z9R66 zH`o5|3VW^{0y`EnM|K z@X!9%N!72fp4THCRMscDn}Mw+jH@~TM|>t=69&*u7U&5oUZCX0c~!WyZC7e872|Ag zwvABy1dhJL--P1{Lh=r5hby3q?V=o%(2%5)PB7)zo2C;b+2RF~R5>r0K!E&9L!nmQ z+qMXLy-9@1GzoJo1oX%PL{uhV?|1UV=Eor;pu}@nW2ZE>sg0KA{0?j($ZR;TWkv%Zp zwjtosAoimP?@#`;N*;|l#AUX9D0oEOXdb6fS zQjaG@>w!S#6lV!mzTpn%^>QED^T8kg-s$~+_uHl?9(v!Iw@bZ7_*?W_1bQRkT`$|P zf6slNJ-zxjzjK3j+{!~g{&%M5pM3PVYB!iXW1LcBT?z!4AMgB%$Kd-lA7+4lmm zS?gcHaV?Z`mrRhj1afn4V zm5utm!&mS$PSdy&@jtvk(-B>vG^!i6H+e5&$U{$KIkZu)?AHBBEnw)XQ~Bp-2yC~{ zfACk1I)cj8ejKq5Es|QgvQu9RsCt7Ho{<0aKk(n^;kvIn)5y7c{?#A&j_IR6_mBK@xxey% z{8!UQ^#1(iBQ-{J#%qmPrshu`Rpv0$5WeEWU~aA}knFGD270#V7yyvXDX z++DVVV$g5M$B2T(R=t9^?6RfdDDTfml$;mfh*BqK2(ijB(KPd;;(6H1$rCT1-Kjx;50npa5e+K>K7Z?!w2#H1p@^YWI{jR@o=6~P+w0n zgG*beVLzgm=&-TAfS0f$To4B&P!OC=72aZCFG5zTNMPMYjb4ApDp9EV$^FnjM`IbTRFMD2Plw5qs z%YE+q>OXbnf8YPf5B}`*!5{wX)8%KM+}Q|EJn-J>7ytf$s;Bw?+{BgpSr*nNUL$I{(RM~H7G&*_CJe9uSRUU zwF>c_V+GIRL-Aq?^i?}NXp*)=ph>3AS0p0Nl0x_alPq9M3>6O>pGblA z7hF=XVfW=^rWt|XNMK~6FYT5pgZQfrxio?Ut3Z#=wtsUZ66ra(*59_j*k}hEV4dGO z{xu(EXemJh*ZNB?74dd_OulN9vWcaA{ux>8_TSn6Za8t&bX?)ne)-ysNmD~3vkN|g z-5f)`%y^T*ER@B4q_XaBG1p`ZA>%Kz$(@xs%OO~3Yo z-!bvf;k{q{N2g1->L%<3DNjE9E7NcM+z(ukXwD*x^hLp1e=_E4tt#44eH~`6Y-fW$ z+~4MUR%0hLreU3YWc{!CUNlEcIE(m#LvuMSUY!8UvQkK604eJQrl`viJ|%FC`_K6j zVeioSQ#y3G$~Lv1qn9FArV zhc@t!47pJ{Ovz_c#%}O*(Cq5~B%2wK;=2pBS@lv|@W=&j`$Lw-DxGKsh6CJHI*hsh z*7h?F0=MH|Z_vBEEVONX`lH%mkebot9k`CK@=lg%g$0K5V$Ywi$tW2YK2cXqy{9?B zd;Q_nNdWy(KMn*9jj{DqXXEby;X4D^l~TqMelB9+pNrr}-I>v6ed z{_*$!=&|hbe83YA{D0G1e&>HY@l^lyE?4vmfA_!ZZ>YO{mUz9xIT!!)_#do?2FXjk z2+Ml6&<2$m&6p2_u?yctDv?J3B@>&&)6Vo zlt63-2ab6A^TM()WK)~1(?ee4j3tM+iVzT+a~2l#6Qr*?kh5ZO%yYe@tfv6lIzlBc zn#HWHMM_sI-gLQYK^3M?OvFRqNeGJ1=3g65Er?zub2%h6N;ali04tWz%xTWJZWwWJWwPVwMm5=;PQM8Xs~bpLH2!Mm%!lOxfpu z{rW$BVb~|s`3;PWIr`51*K>js?{PkQu4;%#ta=l9VKqASjDW|HJzU5GFZcP3KltCB z`R4!J6Cay+!0))5e;Vnj-}u1vOW*ewrbmDA$3nVp@Zq2Nhtt!KeDJ#YKM%(&YZRby z4cfq7npm9XQscYEIXJR`)@xf)7-=JZAU|;s7avL>Xd9W==YfXkEPH^yWro8Tlkpw- zZ`^;%W|2Tx6he#LG!pgTfoobVENskG*p9t)h)_BpgCV#bDLIQX{TPDK z`jD}9&HG=S!YHPXJ(d&%ainlg>$ohy)gLeWsS&~Xf*+^lgxM#*%qQ8>*)S5;6PEMo z*t|&ayk73}#2heOTRW&O=sob`3BAG?7^;t&3ec8o<%CD0ZZjgT827H{GTw6)l1 zt3Cn9zETO(V2jjmyi>MM+Y2EM60?NU$>gxspAF6W$Rvo07ri!Tbi_M6H(fC|kB)6n zPwWTfGu!SEEGPURc~181V~$xh>-r$Qeg0?C|8%RG)a~gzytF%QE4nxIYIf!$xBo3C z3HZKi*W=w%Z3mP7wN4P1Stx|-4*fm*mwvPI(ZoN)^62~inQ!}FIm4IyJo>(OPfvdM z{rXDaKbco-=*g{&i;i(SND7kxE|l~1Ej1LJdbbDJP%-dy(wt< zrUhTawSGB@lh`}>0&Ts%Wt|Mf--u@|$Qr-aFZp2}9wC1{1h@oPi-gQ{gVi#jg(3tz z3}rijI4{U?zG_#+e~t%DS&x-gWe);|*OW431OK@)1E% zcw%%?@zeSF1cWPmFAx&WNa(AbNq;S%cc95oDO7vR)ry8V@TIcVUVSoCCp9JKK~v(k zif)G^JLG~~S#;k(>$uDp9)6pdLFq2`X2ubbb^I02;4is$z>YFB`B>}cWNx2-^{4xK zqrG}ebpKR)pKbqh#m?7P^o<8s_Y)Q7p1<1S&YH#*C+nVgK1p!2HGixqUKhqAL$dT+ zhY5K>3NQD$@11}8%>TZhJB~c)$M^T=QTWrrU;bbJ^Xc9%|CZ@BpZyKz;bP0?$KLzH z=klVWEl1aZu)Q925It10H_ZD?59M*5WDVRE{+52P+cm38W45ho@IbHcnuGKD-gx+H zpTt55T;paAhoj`LhXBi{^@27GFL1DJbe+J+$HWyjR=z;&4LkB)iD+ysJfYc#Ruwkm zG>BIj>3X+!=ajrRY?1vHK;K2+ML*cclm2Wvs8W2AA=p*bq0Ti7xk|$?thR^UjvsrD zEvlFqi_Fvuhnk3-XtS$P)_9meL`$KdNg0b(Ssm9M?EGz|0!-V^?I|*;F7d1bEWHR+ zuFC-J@WNh2qpEE0^QRHIY1YlV)ne(4QaF$P$0cDfhTZo*pB05L1=zRgXGg+`fMl=e zT^nYq<}n1#gTIVfN{XH~qI1_^&ROX7EWF%@zwf_W@5?{S!pnX56Tinl^mAuvcFWe4 z%g;{_>IZoEO5nb)`7_gPcipokb5?|BAAe+e=qLZ)S(&?VcCwKM)*x4y4E8R);#ZBq zPiYulU@@1Y=Z5FiU~pmgoU8o<@>;&;y`H>H|g8d@WPKg0s6*rSAsTYP;Q^IIyyRruy_0!yyA~~A9bwI9Uik0@O z-0<45w}1OyNu>N$D%8;8}5Hq3Keo>mU0F#l`zv|j@(!aQVuHSdO73Mtm#<`HHt`F<=;8;*pGVYS=2>Dz3Hr&Mx`| zTRg2;^7h|xi;`I8JUhPGAj#nb7?7Btb^!bQt9fpO{;x4uMS;{qyOQAN-lKF?PYMFs|oKbcn6izh%AM+OIS}LL1a(G&TbdwWoEISh9x4 z_CflyqY)Xlme>MPYJWZ0{%FSXtNe)LwgiJf@W6F~YlFx|W^`b=!I_@}(CUG#|F&O{ zTZ(l$pp6>e{9|E3j|80~wDKBAjg= zd1={Zn@e8;t5ly=mYu5@9ggz`-{;@WwE`ER|Eq`TB*u5qIICZLf!$ z&9t_+mOqp)bJmI3)dIe@7qJ7O7aSY4$Wik59e@K3+7@O`P`n_Nt}o8r9kSz&5uN}3 zGG2%l66%*V*!>KE#89v8x=s+X%b-%X9mQ9JRU>f)j~y}H2!h}&PQHu@i;W!-(Y2!0 zU4wF#wj&T^ijS~#Ri)s&^hH!z{FlVK!w%z@6}zxKJYi$;*yI3s6+lsfJ7C#q)iD=k zBiVQo9wDer4#ak`E1%Mww+Qk2#HbYx%kMCLq6*ySfA;-Po4Ox0KN$CJYj4`~plU?E z_4UBq7xb|eMXP}zIp)kVZmcMRW}ii4@mfJ%?(k6Ia0;`rT*p1?c z`{b+Orgw)Vxy#s*(XxTxsuK@kvgr^goLg??#G>LcUkCGnlBfv4n=XWB1|ow&FK_rO zvu=afN<#)hAMwa7aP){zhi6#26^BA_DO=eTMq1%ms}`vGMndr19tDxu_E)(kX8MGd zOW&;Zv60vcuRxM!49id0X)wW{)`QJpeJs@o+AN3i!HoihkfB)K zzYbUgn}?w)TUmXsB^{?b1j8gHP%BvFL=H$_xUS&xzD5L75bUj{pi2? zf$50{|IM*qBmBa%PwD*yfAdUYxFL*WG6fR!fM;b5oOy%H=KL%5WvplR*JNOxWDRip z#7>~y&)(*>BU8!xD;a1h_@x^E!3U8!1?z+n2ywu%%$Dk)2|T!w9f#-w>yyo~76~l) zJH{*svUy8(c)Xz}fGZxx^S6g4_|Oef8?vM%(lAbiw{%C7m_vOChwwbm7MZaKxh-kZ~jU8xUUhWKY#uI`Ob+4 z3vc$~VXA{iaKntxK-ys3^G5uj_VJVuv*t(5k-~@lPXzmKBj72)!*_gQ0uxstI%7=6 z#SGk~z+5jq;{$`4*?3x!Wz8oR0G0CTE+Z^)S-t)9j`10a5j08;(zVurKA;w zMTOPK&^q8ecvpfv6M#h?G^)ms5jC)F>p*gswJP=3{92%pMx?yMr_2rMAWYFgGM2M| zb)xG9QnIj3Lq+ms-I}Hpbw({SY9LRvquyd~)GIu#RuEe0m(9g;r#0IigzuX@K!Qm|LJ#5{AuBfp)gh%o0LHCo(A(+%~;PoFu%vYbqAfD`YV3B z^jtxn5}GU90Wi^qAU6dPS6~_3u()AAxdz(*AiilGxBMBPt=1uUpbZTC?G_X>RxPY$ zMD^N0Nxn8eXvs4WMSh8_GQddbLHBq3S%9!1+!B&6NeN@K4rRj(@d+L${MU!Z;8B38 z6qrW^0u+RdqfF7V21`{aWljW6R*I}6Nb3P9aoBc?T1#XJl)d0WZTOO zN7~xfuDS=3Vw(q4@Q!!wzif0HsZy~maqRO~i_mr#xc}XIiV^6CXMx?9HRgwol@uee zW|^QL3he$TPtXhkp~aEL`0$|UXI*aem;0P`d#vs88sW!&?f;!#^Euxzz5G+Z(7!Io z9Dnu`A2$7?|N0;6p}$YA)o%KFrY4ZGUan0urw)g%udDGt&xKYzggq}4SuU7Cg=Z!) zy=txwVa;f!UgA8=-%;|{GXOC=`m00O7Ir%Fb99+NDG(8;0M{C zH>=DD9CZ7(I{?%!9Z(Wf-t|VfmM|L|CGM&YU5$|$9v0s|e>Ihw^DO(HzF@iwY`!PPekbFQO@(_KEL*Ze_5O4L+26pHMhwFf**O$ zKk(<8JHKEK8H*mL3|-+h&IyunZ3-2Y;K1#&=6Whf>>zuY9R;yhD;}`Qrp3)#NrhJO z=#7+`p+zg};CP|150poA#6Lp*8-E25dHP6K^p@(mL7(~SH6|?Me=f`qkxF`|o)z+n&-k+G&0qUx zr(15jb2d~yILf!rJUUlGNxb6vHYvCI5kuB7VnVt4)&Y1I`xYJ;aUjJxHKe}g zLmSH0f-d>cEOw~9IBsZp;T>N#UqcLv9-6DvA;$UrnFq_Z{-Ky23sLGa7Rh5 z^M)q5&@6tay%=t2dEp&ra(yx7Pp)$_Fn*awZk`(JZk)8MIty04Arn#kfuH6|wO7qI z?j|rkuu*ds94#AlH=V`d6`%gadJXQkZO^$=8ov-P_c>(_d-bEVfW{+3avs~@I4k|^9|cQdbCzt%&y%Vq=#F?Rp4dAyp3U%g*wYC>LU4GQ z)dvQER2pf-3B@eI!nyjNufXRW1tH>)wEtIx#YVpP-k*`PaKTcEOgDt$v@OZeD8A!> z-8RH}7n^OZE|%DjP%IX&*;eu@LIIy49fwSho_zF4e>s zd^V3w%pXv!FWy!4570F^v9v>mcDLlLwHZt2IulF25|_%VIwv>E8%MC>rh3)*_xZc6 zb{&q=|BUN2dAg^*7UTVyDr#RhF--67&>%u(E(?n169G}^toZ~AQIk;Sb2jC+yI<*F z^V>S%K85+q&p+#*@csDvfAkbq_Nq5d3uruMNHzrIL2>e^5nj-FWij&D7sV5d@m~gz z$7sP3kE5b-r1b1Dq`u`3;vD3F?l!k2;CVRC$KW0BY`X0M*7^5CfSr;Rg|~rS3Z}_W zl_4c`+<*Y#`YBy`(?{zHSJZ5>C}~&rp9&(+1}&YkB0ldB&_Axe%Uph{wOf@2WoGX~;B3U(=XbC`xkPB7?zT40G!g?!O5 z6zU`z3RL^xwB+%Gd_fB|-u9{y%Fa%Gh*(%kn=ZmS9wb9UVr}1AmAg@lMaJT_`0w+t zHo9T@pT5&4lb`e3j1!L?R~XBhkQyV<=wqe{FWw3@Uxi_Z$upHNMSN^xno04TVGjN=WCGjG%VR{vS|E?Gz>99%r zcK&!zp1kY=o<0~BWCI5J_Pg*a`oR(rL~0nDQucDBlM7?8k>h9^+RRO}In#-u{2*I} zg^l6`4?RvqROgmwS*xaUtd75pR`8OW4HO=yjzO~Fx0uz+WAoacvY)(Ws5ty{z^yM3Qw>)Ri%&?A6D}jkhg*pLo}Nn# ziuC72bJaD4z6L||u9==YJfx{fLfmWf3Ph0MM)PzmCfDf zEdG+QjB!_h;zh@Db@fgij_*3kf~6F2L9H%EWZ8?3pm_7!{V+(GJ4m=<^KHw-02>?9 z$A`_STFEO)YE4t;Owy;5b*uv6qHg)VnS}Jz%5ZVr7TkS3R&~}I)n}jV&;2Jmc zEDy`SdkulW~naPQP>io&9G|?>!0-8*wXv~yoS*uFf zxZcMb`Q<*>yX|fY{|re!tlfcgVi3A`Eft>iuZH=sS*!?_7i~jc_7J1ghSLxNyx4#r z8jup*jw!N+&F{HqaSrh{;-O(?aCemanavd>PJ;vaa0mn$103YNN8%ZAR*u$5JF(ev z8`0PPURj8;#Ol@}H8y4R`AA+LZ_seq#GzrhR!wJ2)^ehypf_ZAj)x64SLK`q%y`M7 zZ$aCTJqmW^E377T0tBV=mN`s3Lz|__94nZ~#hV3Gou5@ehr0v#BW{Ei!*#OZI|SyG zpF)w`E;fXN2n8Qv*w8`_-XL$h<&R`DbsM+W)jKC(tyyn_G2%%~pNOsQ3IyztSBlrg5?j-6!28U*@o>PxFv%HB?Vs z9{aVQo?i0WPd&DsryqS_x|v??bL7#*Cf9h|PJ0f?k zpX0Bu2P}jk3HwsMyu?QqI=|76=M?mxC!5^{TIA+_Ar6~~W7UP~LaA+|tB?IO6kZ!= z!EB;|XjbDjL=77=1Zi4fTgSv_wVmQkETbKdvX+A5pjl9!(`vls$&rB4UI-Sg6&!$# zRP6iwmv*?Y{mSs_M`S~B3xRO6k;pINR{QdvMP4i~Iv4(1qY!BAcD4!SrUpa67eZO^LURtZs6*=IqrW#PAQ7opu)wp^I++m;;| zCnesq@gXN|LAp}#$F9nj7fB1&!iRmYaEb#X$P-c>K!lo5&@mRxsuq|%s)kR94I?zd zW&>OfwcjQqzk+rrT3o;m&7vLt-2QM)iv|^N;3#bE${Un8_xV@bb~{u%U;qBEA~}~*y;#_c#SF+mk$UJF8rLCkCPL_s$2v0oVZfVa`|UzFF6>5f zfn|7bN=W)pTHT0gA>bDrIO5HZ?>QdHm^RBx(0YQ&JFra(%;rIK{*JJy(P@ZYooNJ$ zAO)Xsw8J>}mAV!Pbl59PSI}1(WgGjwWQf3nfMriog;Ur6@*&vL*kT#rmjNl3h6o>^ zve1NwSPb)*0}B|zV~Z048DNEMCuJ2rBqwVru8#axG}F-$Qeq24X{3Q;H&D1^YgjlU z_(yiZp_y(IFuQEo7v1&+cj=2~JID<9Y=5B5aU$!OcEr1um#FoC6m%)T*a=Kd4IS4I z^UnBXChdUPZTn55i3VE-p2wZ-!+63Q8{XT$Ip=hrfo=1sazry$Mhr9f$z7sm?x{5dqOHKl4>b z#8C>-l>&j(6?VyT>ygd0He1PH!w($^F)kjf33m8+Wf2t9Cl;nl#yoVfrHcKiF8Qd` z%SpuLECi9ZK}^VS1;k=jFZ@ANp=ZiaBhk0E16u;}8JCkeX*El(g zBPRh*JoLVa7X$Hbfc$wN4+-*|0ACeww_soPEwIr7j9G@H#NKX8kq*}K9TRy}T4k}yM+lhq!pTl<^FdT59T_x!MW&FzEc`+7bjXB%;d(V=Y5-%a~# zdzNSaYliI$Pdt#F8u_c3o^~k0R{nYupk;kXqJdiptQwHU^k#_-Ry!UD>KF0k%my3& zCw46;)v%5SogKxSxorB19_{gMe6?fC-)7>}-7ww3mfXE&l7 zSpIzbAN$DlBIxFfVmDgJ%SG4i&bB%!`F4icI3bUCiv`e5SStuMIDt^saHLt_s)rhk zgiv;>c{)aEZu^He7W67csQdh@?JjcvyN@*XJ(t+88B;xsGI#xjoMe3G3y~gYj0YYT zobLqC^_t(FS3)_Nz|>WSw7C1SZ-Hyw0*rc%bH=>}XRYyyVRQMapn5J)$+{TG__uym zL5=_M1R}U02C_5)DY9+f?IX5g-y&C=JUi^oKI~xg;GBusiI#8sWbdQw$X~w&z#^82 zOKv=lyj=<8GTLr?fnkMM9McMA5x~{NtsM|x-cv194;=a7AM1DtQsA;DLDzf>A0{5_ z)xGnHLd&sWVVB%$@se(L0`Xq)6@Ilwa>6~gM*SBH(sR(nmk-w5Sutb@Cq^_WBOcm8 zu)WwDGH%0$)T<2(QeO|~_zJWPhc4UpX_F;x#75oXLS1sCwpkWlbOVta@lCz$T4IUC zeg2GC+OS&WZ2F%bX?uq8z?{$q@8@B-+=t$`_lZGOv1|UQj_PI2Jnb0xPqzw=ruu26?tg__y!y3rGy9ERh+AM8*L-O_(x|0Z$FWDe8k|0kS!dztArZ3w!hXnZv3a!Ph5fP z_>y|5Z%v_RIyz>ML{pLL*z@#i@*ueUWvJPB;9C&bKOd#HI9s@Ge|X1}m~_J>)2Wq)mc z#g5&;*a_T~pHBeNAPzt?`f3U>Zb(3#K*iE4ji|Mm0NM2AK^vDJ2y}A)KYMT9CCPDI zdnVD1eI-bVqMqK-y62%1|_O*ezzkBUQct&M)HHgMSGQ29o%x!mf zbN7hI$jo^3)j9s)p`$-&u3%b)y8+n;?QAyB`0>L`hj^`TOVRK0hV^bFLhXYGqU_f$ zCkFBO*i+kO9acaGK^tQC*0J^r@5prX$DYcBVMralZo=k|{FWdGzJ$}J=VY&O7v7C< zx`E(slp6!2l{OKe1Y9J@mu207KlPylg%+HWD2j{tf++pK{A@hxK)e{6V zmPwHOWrpqZ{?3@6gtG^J^d4X*abn-`ywZyovIBr;Ct&ppUjETo@;|F&c=cI$)VQuc zi@()(_klv|v!-tI@#*p6{mPzd`4qs!b!@QL4R=BW!rzVf>Ol_xNomK6sev1>vsRIV|}Y9bd8AQo9Hr`m>Dh z4@`Ve)j9Jkq@rHX{D?E_%e#7&G6_Ch-?VQ@s-l{%rr&zth+4|BU+&J^nLz z3?jLy!ih75xzbDl=E`3@ofuPyk2&TzrOO|&KjCdO_?j^0`o1`nz1^@w4OC+VHL^ckRUol9Xp@zwQ9+ z;h=O~yLL#80yL+K13~*g!L!czD$t7txEij-!V3wD4t#iAX;jm-?T z2;JG>@QbdGl_Q3G=Zd+5U_{`spVgfAS&!&S7uu%6%eCl-c;5NlvC@6FldFg_tLkX_^C` zw#-lcWuC0o$dt33J@C`_K-a~>WbnSmHOHOe6K-Wwz1Hx25Tg)T=Eb$nwYAt8nt-2I zYjw=Czfo!{bB+0Vv|rnSQTNQXKGZQe^~g28^2VL4>w5sIghuhGC4kU}vIv0s+r$u{ zcoUn@XT$RXC~r`43fY)s8-ztiKDXo%;>Ftm@*><-H1e;F3>rMHR)_?k>l%L?DQ_%D z)WtF6g2wuoY=?6lgmzCLbYYXYM%iFuam2(8aeNjx95VQ@$GU*M$hLW7W5}^VCH!f! z>)HIVmo~=rq3?(p1_dz-x#mMzREihc7mS#{qu#dc_RscbykTi-sE?0v7ZaCc(r{gBu2YSyG2{uKhFR}AP!^J26QOR67jPOKs0T)noiCl z=YU}mvLnGc?;}DR2#cCG^zu;R3S%0B(djg#{josVv1{ylW6m(4g%66~%9kkwLlFe0 zGBU{J4#0jwQ(2-U4q|nDRQKIoZfAgr&n5EKQ$5jJd-%}y(&R|w;yqx`F?W}dnQRb> zVLL$xK0On@JeF_omQ56N!D235ELywT_OJADy2t-eyl49-KbBGdvB%#7<}vO3Ac92U z{1eJJGpx*)Zc{uw-r;D`r<&!5jv+u z2U09Q(1bDuT3>kWRL8Q`3n#P71Q{1?6prgJWK0j+a}9!*Y|%G-^B>90J~ErO#3#0= zwLhN%7?0k^gm*t{Fyk+iAETSRHa9*OJUeZ!z0-(nLTv1#T_Xw*^=|lKizdV$4-}IV z1o+7LcH`*Ujp#MS!e#!%W1qdOrz=>fbhX0gom)oK;~ z@E{;>VO4hAG*i0y$Ez1l(c6F6=_YIOgS~VV3g+&LW596;-{O&A@F*1D`Ph+@DfJDc zAXnOmTmkbpA67Wq|Fa)|jhco$b9-4TG~^jijU&Ua9Pm8$-7<7U%)jRZBG$kb&j7gH zg4_+TY$PnUM@u}Xmk0A@$+nx46tuITc;uC<`*H{Ct8hEEeWZXUk*$ll| z*x@MF1AHVmduXwlV?djfHe{=bmJK&`K4Y(q*ob)1rMl|>Pw6tu8w^M)pvxMA{zw{FUj zga&-Q05uZE~yqz?Nz zj6XT+hx-&4@lf`7l@ZV(>%gsjP-vF1xMxiANOf%S=~(8J<*gu@ygz_>nX;ns^KkaS zkI@5b&UqhE3NjT*Mc}PcUelGFH*FN2w*O)8D4)qAM$`WwuaD3Rom01QpZ z{!xR+cu*`2@y5Qjr!?ETdqD2`Er98)%|PODLs73YU^Z1Dvs*b-DT?A-gxU7;0U;WW z%{DM2(|~9`T7W?Nnagoo6d6vsLvItbV@)5ba9bK1R;X#@DGow_I|3M73KIUfq8JuK zgsUNBN8j&}E;=<)xUh^KH)!M<5(3fdrn~bd+N&nm;HJ~sHKeb)$%m6sh~>)Gdh0uE zx3;?o!1z54EV*#E6zpt&#+H7d&`&@1_}iZbG2^HRKy+fwt$osari23{|M_`rR|-UP z7D>&I4oZnH`^+5xBl1({o@eB`-wt;2D>5fL`A>2?%dcSP!mu+{ zoa+|5+W$;}2NH7|Zkp+=$MDCW=bmUwJlmeQ$Wg)ze9ri{Rbr%mjQ6kkER_ad!TL@IgGn9GE;pU%*w0|oYoZqb__BIgt=P~QAU z&>U#;NvH9nKCEE=o&f0*3VQI2A*U^iQ_7sg=U%2sTbSea8G&Az8XQ3X_5crZ-a|(WFogiVsPIMY+E;o zT?tuF5S&sNJjAp8>h)Xwz0iKF@z0=XhHA1pe;7dJzPEpdivjdJn*67(LC=Cp_DDfA zV@(DI+Z@U2pZRYirc1pG!_-f3_P`I_18dH)H;|FeJo# z2%49*5ENhbC!2+~3ll$EF;}%sWAG8EtYaI!(j?;fJJ7g93JEm67wgPpOncr6!xnh$ ztExKyB3LTm`V1omd=`ZvDQul8!7M2KIFO9Y#Q&Bn4DH5V7@%2?&fyh*#E~*V*>s4$ zV<1`VcP;UPG&=jQ^kE_P0F*w_3?1E&9e~?zad%9ZJa%Y*ir2CenEOc-V&>!_y!c3k zeoX9!u%uMlNSanWiz=iCf}qT6ZL5e^$V`D{`1 zg>hofcmZ_q$85pSd-Bybe2-qpqeUCxM9v=gfqP)hxv#$Z(m6@4c8!XAXS2|OfMvrw zRDH1++Q`qGApa?uSQO&B3O*W&~pnZe_>YRzvuQ zc(!&g?bl4*1Gr;AHa6#Dkd?vj#dEoBS_1YH;o@<%?e_!4_V{46BV5F=gYX9CP0@bt9=$Hi|dd#C*5!husG?p*(vTeaOJ`s33-k?94nPeT%EIDIc4^ZjA-wNca1$F<#1X;9Vft)Iz_O-2^}Qw3A)bf$^;&q8 z-`aY%zg^U!as6j8{@nR;KIq_+Tz^hx(nxbdW9MfC$avieU`)LZptDa56kQfEOM;a{JH2*#kdT53D)I34jwD=Op-6lC?>Gi111y#huK7qXTy<*cusV z*~|l}ALqO0i{!TpXk-w)r+iocTP&TYhxqK~f%ZTBRQ+gh8c(-*{}-R{%x9BLKk^`v zYQE*3rh49cE`E#-Vpd4!}@tQKs1VkK3ija@;;Wu7n?0g_~#pK4s zUTv-IEG^Qz1?mr%*u@Vet(<;P9cm+je=NuL)K1~qe)WVp$wR2lJNDO)G5-EEjpn3B zSO+lsoxx0e%nRlcQj!>9pUiviAS}KZoP4wEGhaA5Kd>t~?gG5<=tbu&zoX$F4;nuw zID6oS>H%^s>*VF~_19l1e>sy?IZn9J%zTE!ae^M~k^eKJVodU$JLpjk@|k#=EE``k z_F?H9Jolz<-XagVx+C&baP*I`1KhP9Z$UvjIy@Q}zRzgfc6GEmKM&;*Ig5w@13=YR z=QP0oBBXc&i{Rdc)b-Do0r}&A)VaK<4I4yX3y)ZS63H+k_AN4`T$WJLfM{Xb-X9XP ziQ+pyYx~4=3+mJU)4n`sfMr`(A0lb<#u25g=0bV;am*}Z61$0{x~0`$AlzI2YBT)k zey4bMcMf2!)=nYq20z>HuDUrM;oLrUtP1yWzv}f(IsN_J#@}mU2T0?^9#4v=D8Fk| z1jvrX@(k|6hXH+*eF`92u5MJr?vpQl0)XvDk6*le^W_(yXE}S|2kilJt>vpPKc6HP zrMc~VuEYYH(@y#u^52!;c~yw3+Fsli?I|>n!rqkiRP&5-^dIW=akXE(@EO`aZvl+W zDi%Us@oU!(I9CEJ2dpj;V8iW33k4iG+k!VXJ1(f1CIa8>FNK)Ws)DF(Sy+|rgrIh1 zHY!5&FFrT+TdZb#He_GIWi0W(jChI*$t>uXyF|gPlj+M2>RUs*HHdO6(hs?27N3(NGWi zo2~jg^2g>5?zaDNbiSJdqo%fNk-&6NcEuo-Ihve$xR`hN?m=QnL(iPvC+- z=(?NWFc5HYi%?1pRdoQOE{K7T_;^;e`VrfXbRIlIsFEx#9Eh7XF?mxThsD!>7QH&q zj>Zr>>C48Gw#oh$UV~p6w8TT>q8mP8obCUajeiEkNt3(z8C>U;#;bP&lKtW_{~#j9 ztyL>Ykgdp&{oDYBBKDe>gXPcE0&|G_!;cv+OMlJKmYJ@A}+V9mA9KmFJ_Y8NbI z&3_R{>8@DtkL#xwzD0i1g+{q$kNgkU^TIxmLtHO3H$J#{V{MP^9{Uo%TgJ8-#S0tz zJ@yjM60hX+Yk+&X6R-@MRtJcJyH&H$36Blc`^!CobS{71LG3~~sstAWjOjHM+eHSN z3bJg#(->d1435rbX0s*g8k-E5RdYU(z_6n-Z&AgOG|&O^F%tbrm&Gld@CYmxz$zan zTj=9c?397WggfdJ52AZq#R8A=-RKJ)yMnkczKLc7l`A~jPc(?90k%JF4)tt*`^5b* z&>IgCFsc(YefJr{0P5$^WAS0Z|U-$olm5==BVXtcmxCr?Ea9~(@TCr>^Ti#uj7 zTpm4otn-IQI%j?&=2^}j_+fg093$5#Jah87?*NdHQzlobO$KjprGYz*2bw3{r1mq~ zV>;A`3}jv1*JI|5_p`=ygg7k?{8%~{ffQs*Ykh?W{8syOw;V%ySzsV|+XCceXE4~A zx!M-hH{8(|uDNe$VXY>92nbs3OV@UEFhpD!ZE!oF4(`=T-0cZvc4F8ljy=oTckE>& zl-yYF6$)|`_uJseveCz;Vo=>1z8A1A>!A~a-Piys`KuI=Kl`Y&R7^8TY_!0u57V?r zZFu>KQ8oKlEXWh5ATLTGIK09YHxa)6%r~}q*=i59=cxaX{1)5E0&4V6%W7pF4H7VY{k}=3Jg}LZ5lWn+N`s4c2mIE;-Sn z95>Sa`b%AbO?m0%SBjqS?13Mo2i6?>?9-2~7R1SX=lhCpfpl&Fo&VwtB(2dYw0`Ff zbIJ1w0Z#xSDytKl-0#=t~^4I&yl z4H?6{D1}1nYm;ASh#m#3D&d+#4}8-X0$xJ#jXx|zTo@1v@J4&gS_6JS9zSRMe_rG7 z=W)11cM>q(%sGLXSCcUi;JBJO1e%NoI{!8P9%Ih*JNE>wG&aBf`~%ff*2THB6P`Wr zJbPfxv5!7{U&|Qzn9>vnCg*qPL`ICfol?!rlCUPbBR2Wp_0npKzuGCh<6n%@qC1jx zNVP!>>uI2Ez3&90zJSIT*mn1KZtSOh0W^*RoQ;qL9&6p0cg1dxT0YbWP7k=6aR0P{ zwkJ6)APF8FWc1=n#P=hz>=1DJ7G*1%;2QzNBN`yw;HE+_RYxyQB50Ei%yR){Tv6Tmlox1v?ZVvV+^b83|=j zi9~)&0M$(!eRH8ga{?dkyXQ6$JePnOSi+;9Wqci=}z6J97IgEeuVFL1;Aztc? z-Q<|65INCxo~CM=bB|kNVmQx4@R)Cc2v1(h=GR|*qD$@6&j!1L(= za;vxhpFH{c^6AGPWzDR4@|@y)zX8hws*vjx=lme1ZG>UMyHSqr@N3%m_eyLUm^mB7 zT&EOp#p@~>dvgd2+P&c&6{xQ-RUg-Y@H>Td%}x+H<%{D-Z;G6z)Qqqg$(}Kl-!r^KkaS^Xvg~t0g(+B%Is?D3_g+WF_T#MZB&>Vm&6Oo%%zT9d87& zSKFB-v5p{iTdwicDvy=eh&1!iw3rWZ!E37!N3-?qvM@*IS=xW~5MZqA$2ux>(`1}Q z#es{#-NkH<(aV+?-0O}bM3Zp#fYHHjXtFjf$?yN6AFc<%vJ57^1pVnViX3>6Vn}wVBef(|?>BHD5Ev{qtN`GzZ;Ai`9_dk8OmY-z&3C-b=C}eQ*hdIKy zlK0H1=9N#9C1{m7Fn1{2paXp&x9a?C0JJBX%(+_@J70YIr^}14{?ha8#g|_8RT@`l zXE}S|dG!ES{^S;gJpS|he;hnHPKDfFp({fRzRR}0!f3BD*A(W0=g+FN;7CU%L}pdw zGogw%&dP?1&NbzZyx8_J3dQ#*gVlbbC^t3VbSE(WD1~8PAK`h!v6STfucv+mG3%EO z81<$i9OvQb$UZO-Y4y@rfAYybpnk)(r zG&yeBa9d@Jj|1-Q_R=V!#u|AmbtS}xO5zAy@o?`RyO9D{yppSp;OTGsCPH^!AUHCG zt8VQ*1lJhAV=wSo?5#2@exKi?KjcaSUu^_G+uwd52mX@r_r@$E$!6JbpHD_jhnr@O z5=uGF^x|bHF~g4qpCA&8W)dgJyvqhBYrx#S)7Juj_4$Xkz~R-`-q`RHIeXxF^uU@^ zT>U@c`~S&983*RCcybuO6=p-_<|YC5{zc*>$nMoSf)OORbAM zvGs(7MNJI;Rpj3bCl>HWOG7t6qOl7G-(junP;reXUH+O@dZAl9J_J-6IdH|p zy?ewG1!!Ec6e=+DHdvWeexXJ9Xz^_SK88Qd_fA#e@lOpHw?1Asu18XjQ^uc@g ze&b~FF>5|}GCuIu`nb|oS#)pxtuiZK+3rD}W8VKV-!{Jb0i0FSFV9KgK)oo1u6)M> zwkf;6%u!1_6UmwL--Wm0F;BXYigD9$ zVn;d@{*|_0jhnfthIm}K>!q!nkMTm{Pl>+%rOoNo^eC#iOThf$Z|Cxs+(O&N``gRs zAOE-f#r|=`on+StKU3Lc4qwo@A~bWpEY95 z|KiLYaQ=DO-oVxNy#ahjhJD#pB58-5zMhxP?6@MM%6+ei7_o_m5a6nM|3Zzbd5}wBof@9s=x3%?b zKfN=3x%ZFzP6v0J*v;b~clRkYrR+NpKCScJe)o7>^;DD{;yB||LKqar5t;r zR3s^tcFMgi2URZ)bQy4G9+VKN{dkb~dN^~zLdtWi0zoJLH-FGp+Xwmmf;R@To3Z9j zdze%D>-tMG5`*2@`fPqC{3Y#w%w&xbI*>*L*2N0bKyC1|ArhxUMTw%7fRE;}P!M*5 zFB^SsMdF91Wa~}iLt*n5hgD}*Nw{0pANvy}PGv{_WYckD55AYNv;&)w9VCLXZL1dA zzIZ4d;r4?P!{%3nUIE3~w0FSnWA(SYNqdRG!)Hh>!A&f#7YC})agI5TI939-aYU$4 zDqDvK{fGEg8<%5Hw(C`tqdi!Z!UO)Gz0GzFv~|;>gTF>{C9TTYeviWK@mK#ekAI-K4rawibtSF% zeU{Jmr)O@BJv92T(EX`H;g2-_h*J!#@>FojLvboA1OngtMGI@IVicPvng6=t`EmNPqa< zzmL|e!KyVgf&8&f7IytF6vG-D1X*cRsSgCVy@BBVE1db_c_3k8CbGg6_xvzFb8IXT zkZl^#0Xa(L;f98_w!4r}N8Z@8wLkkBjaWJhL;Q)fL2y-uR-Ayu7`X(M_ZZ#L61&mCm=NM0>LmUy@l$yJT?d|4yo@Bp-!e;KnY z;^5E5k+1C1eqs|n3*=NxkfFjEEC(Zz+Xds4)tPW?Jj=_SB3U%a zf;{5#6}})R50Y!jxHc6+@|$W*K9}fK2OG@6kKh0I%^%<3#)5v9vj?7~2gsFvqa^wC zyZ`(*{qU5&ZBi-c6ejh+$aEUO%Lf6jvNc8Q( zU%eI;781gbP3-mz2a6QT0fkr#8EHT-(H#5HW599LB&E4|TU)d(c;Zl`9-`o4@+L zPk;3ZetcoKGT`-npS>fWc%T8-*sw*Z?fPA5;75Ba4g6SEAFJ>C;ywCr9EI4g4ERIw z9QkVuYi!t})b?NMFIM)W{Z0PQi7y>brt)$K1`|`!-pSte+6_#v7 zU~+mAJbQ>Um291y-}4_Mh_*C0fyQOH&`vnm>3A&O>H=q4ZBrT_ckC}T_%=6iOqo`E zm09tP{};61&j2)7Is;?v6yrJoDcnByb9Gc$-25LcZ>Dq{ays&W5q5o5Zh3IgwVvoR zNIDE(vin*gnieYG89-JBaFdgX3>*Yxyij3Fr!e@!4<8@=Mo2zJDBkE|!$rJruuF^h zCtmrmyoEzYHui#u>bB%7P-_gSLqQNWkW#!VM_3sPBR>KI=}m>JW9YVUcbOteBggO7oq%iG^`IN*Jot$HyK;pzf5YZ>n zZw4ad1r1&-c-|W|FM8Oj-7i1;Ky&xe<@L9JYq6K~O>Vz<>z9{L^hGXR9L+cnfA>AW z+P`l8bjY9n@Sm5DKGa7`lW@*c@Z^}tg99n#RA^4Z&@vN@-;oy;)k+)JVaz>fuX=?z zS@F&I7_|NIX;}$3ChB*J6x`O)FaCYnzwNztR!cy=Tpeh<0Z2G!{X!HmgXQg6)`-m~ z1K_Hd4sE(3x6#uAY>fKS!J zPct8LX~&9NkwcYh@Ca9#O=(BY_NOm?M&nNkdsI8L+h~#h$^B;8HvYu#yLBilV^#GR zb4lK~EN1$N&k7O)1txIP&c#Ac);s^d_~gCIr}|_-3s-k<=p>-;@0NbTzuz7p7s!cT z`O_2c{qc8~5C5F6`z75bl63+gDj`x}2 znT8LQgS!Xqj|sPV#bfQd&Cl9?rf=KpNYlWHnr?K&nB(ETxPrv{epJDEk5ApDMS^KQ z9jB`WKO7`gHemj|q2a(UEb7l2tBhbre_VFh?!?8Ll6XglMaP1Yz{Ga)V7mAaRl4E( z{eMX2N1irh5V%`_F!B-bNex5^wO!`VwT&l} zH{br%)q+2+PVjfu18n-_!up>7AAkQJm-qjqZ~L3n0`kfl9q8N|np4oS2-1NZj4UuV z%tZ{gj1<~9IyEdj`M>W^*euQV?E+(QlYI!=vp2!Q!SJmNjuBq)3LVJM1Sl)XqLM9a zn1%Mi0sEW7;=t%WM!_yu&O!stQaboM0~DcY5Q(;D z5tffVF4|FY%_~j)hpyw!7pm^GO**p;&H}~xjA+Q~n7app*tQdCs)7_zMu!HNu*YZj z&|_(qJ50NH5K$4k0}u}K2t_aPkyKHP{KRa9Yx!Mw99iZTUfQ*O+DLoLUz%WB*N67s zb$myi-PXhM*YQODZ2xut{@CNstlW#|u!hO`@3}I3CWDh%JWl96IW$vP-dqy^06+jq zL_t(IOPG0JJHq=c4_4ha(U{}gT?4z*JR5b-dD*rpbR_z@?gV_J9|(H$-G8xeKOMmP zyPtjfu`Qg3zpozPOMceR`IB3}|F3`3uStH8j!@n#%j=eVonK^ia0FGzNs<-_(2Lz+ zxSOeMemMWpDW!NcLdJ*9*{<=izxf*ewm%LZ+0tyGb_yY`__)~sVZcu~&!zp3)0ld_ zZm|1LvvqHu9`K_=06;%7%q*u{W5Y*8gyi}`2s3_EA|{$i9MA}#hxJ^mL)9`|kzsI= zv~&XB`32llkMEGzb(6=FRCE1=RQ0qBMzMnwy6QAQD=JA6~ z+nWtIa`Mymh-rM}-AVWx`RZ+dlS=`Ahg=>4{ zJ^M-5M5ORk>1}bgA2BE!GrADGCk-=rg=!5b+j4}deSO_&QAf`~xw_-g4{^btq5Y2& zah1lww4&`Qbb#;(lGAx#OHTvYT=j%suCK9^;Ms89Yj&Xn&?f}Quz+SY@q*&c)`B6s zh;apv)b<>}*x+^}v7j0O!9gDRd}RC}6Nd<>FyMs1kx~Q1F0Nv=y!9f9R5!YD4+XMy zz|nOP*?7%weujWt`vV2Yrcv2-YtN$M1IsV1^%Y|8+P~GdL&Gez?#2~zh-Zs!Q@{ZV z2`F)%?eBQq?f>*s`z!s~KD^uC9lP4=Sbv}K&!R#c3|#Ubm|Bn8sD1J>SN=1;QkW8X z^mxdYgmi4Of<+d!#<;Z+gtroSTM#!C#08e)s=e-ul(QTwZ=PUlznh z@Wxy3_&eUY8*u)VaJuhr?Z{91a@F4-_G$b0lAquI*S{;*^ijX2F3jRU73PWaaPq78 zmWvTvj-zNpwxC0eQZF94+9|9AFl7R8(nGn{H@PIU$$7*k;DdZm4z?49hs+QEzV!l^ z*Cp*roN#_|(Fwp(+@sJZz*{{k8)o}}K{ihoh+IW{%SnJ6-RLEP9U3j3H+N9G;4q7h z2qyKULLBWQ`Oiths3oZAz}BBXkznsw$^zMzRcnWi(we5ZeFUGjpbc(lf#TtB+20JE znA;aUaRE}$0v+vA1_~TX+L|`Qvi`t`UQ^EY|NO?EF(EUZ6CV6zV^fB4vZtJG(5+D?WqL~pIW~1ep`7u9dm~NT>CD~*h}a0DN{0hyVWDaY zI#jsJphe04aDrVeI2}h2TZ61W*hGgB+XS7($tNf{vrQ|EFB2UF7?voKm>AO9-L4sk zOhGbbYey{0l$K1lBQ_nkM1d6KyU7kgOb73(xFzuj3hem7Oj`V8(Ln+02gDPa*M~?N|TI0D5?4nCLqC>4zSFh7v#A_D^b0GI^ee zqiV7Wxr$U~Vkajh#W6zR$7ZyhLq0LjoFzy1Ttc2YptfU-_(IW-2kF-XKmY8*%NuY1 z)8*ASewo~nSWg7F6Y%91pZoRAH@Z4J%U?|oyr7SNzVz}ddcAsetDgptCm-r-ec#h} z`+wsv_;IEsSDYYg&OEKjl^b}dTJtLZ4g!a&lil(Yk24j#f>>Pe#tJk`ywXC^agpC> zgIHGhQ}vHZYrVDy9;%i+v_B^R_c}aU$Srdgl>LW9gtvb*_qWdvOD1@>UKWH$x|;Tx zzd9keIdnzNisH}m>t2A|t>+G0kFdOO3a~o~n!66uSz5)gGfNZ}Vfbi{d}HMWf9;Xz z)Wgn($W^|{l2!m%auG3`ANAT3cGZayQLF zthBiL&{p}h=_&p!9vjtlq~kNO(03_b)P?sK3u* zi<$B7TLAl5(}?5`89|Hm+D=Y0UuI?}$0fY^k*wbqunzvH%O3MU$?c4@N$v=`mkj-h zpv#lX2k-rVmyh*Hfj8d%N4+<^e{FDY8FSm1lY#!Q&=cJm@NX1Mn@;f4_W;jRJ<^pv zPxU|6i?KTv^5x?X-@m;7-hcZp4=Len1KIFpeG2(0$L1>VR@pp+d3S+q(vR{4LLR#u z?H}w_Y@oS1l)Ptp98bhdTl^!% z^9ee=h%mQ0t4M>U{kdAj9Pv?Ljt;dBMwnrPBc@VvA&lT7FyKnQHe+J5aogVR<0ikc zGyrUy7RM$(#UD~NQTwG)hv{y$A=S%_`7QRc&-VZH*nA@et;g(XlZc$ z(jx!pg~uAFN3P%JjFUGu{y)F}2cN`Mj&Ubr0-+Ya&aKMdg_SEjO2oc%8kTc*&B5Zx zQLs~;)>n8R9{;hD{6{kF33SzmJ=UM1{f|3A6HtQ*q<@&m?k5TG9(g5#sEOkHD}o^~ z=|~32ZgS```}Gga8;CID(2cR+fQpK!ZNgK*zlinRI}h{jfGzpGd+ck4a=eyDS@u}9 zY@XYo`X)p9>}QraDZw^S{}>qm#DHAwg{PBRmn3!+-&K~)08%?y>m4fGxKli`znNB2Q=2CF#kds68TbP*e z@63~dCtrVg`QT6gdHJ(mujnDc*WUc4;G6lx4koAIup9GC*K+ASU%odLzMYoZtL%JX#eA_@9gb5piV@f8!s7PmH{?D9>^2a;QC{8xp45F z%N*v!KtCYrF`*CbE4!{LP}KdF^T82ZA4K;i4AqWp!*=kiKgT5_&>Wi$M$alRL_;DE z`Hh@4I60B51qFr%W~z98>%b06-KApHt$-=*iD|I|t)+Nj=sTbKT8sVIw5*uWu7p;F z7<}gA(f+OYS7UWmc(y-z=P^-7-Ri$}tcw?>Z`+%9Ysc|n-0f>sS@CtS28Ydpkw`=I zH0}JssD(K2%1MAHv2jJqTW6dkFwfANIr+p70gQdp(mZC4ie+=BK`2=ZNka18hYM+q zXkKTSyzn^`Ql%mm)AaLCKhSps=riH+|I#b3>*`;x`Pg8;IL0dH;V16_9@yl0jn6;( zSY!5y;HMhv*)N<38TW1cw}?IXTWv*15lZ0^pTrS)wXx%)?kq=_QrccaKGb#p3;){p zcReAAJo6_pVgILYzrP1y@6dKuV1onl;yrXe)5{$8`}=ehqB7vguS)I`u!wM@QJ*;$ zmhal93)|foL?;WAI|P=<#Ih|`EIRmtt^>At6T(04BWOC%O4!4!plB^%P}14CQt1^9 z-)X>NX2&B_JzEiv*jeXTfZT35rLC0sx%L-MtQ-eC)cxpqu{%m`k8mI*eEg-3$Hp28 zJylxtY7>4Jl9=MG=}mex!FfZhBZduRL8L2JHkL#3`e_BUg)mX1}&_lFq&)n9P# zgfVyaJO;=2T6oK>bHlgy-BjsnLMLu=WwOzYrWc?w>z^DlX=EkG44xYcHe@aPMmvQm ziSJT8UkMO;mX>TRpM0!XxQ7Ajh0BY2$n4Rhm%L8!n*+QL-A@dS=@UHX9$-`F>Yj_h zCwegR$yZ-pzR*WIz1ho`)8@)&NBN5AN@X{R1L4``i__|^aZ#K?&aJSI_7W|;L=gFX zZXD&at}gRPdR|jUzZneN%Zl$*utRh25RU#IZqLEK9MlFJaDNlvwb))mfKn*To1kIa zVVP9eB$UZ3!#> zcE8vw4T;(UQjYd(tFST_9{Fn;_MwbDP<$GO3Lfqd7jU+}W6(}%Pu(7$O0DpEtkT=f zJCB70V(%{*e+JW=<_s2d!6)f#u%h|)zkD!@e4;Dd7xv=g+Ve&M`#pQf=LDaruxAFQ ziB-augU9?LlX!ra4H}thX!c#cBX3|j0;MblN7#1@Y`=c z-Tb7#(oQ0zUrVF@*Iv{UTU%*M`xi)QTiws9|CsG>428tQc&_jc9=6*N!6RX}aUXwI zezx{Ertx$I$}5Dq;_@)^foa~pep-GcjQc z#?EB)Nq~&9n6P(JY?g!g>85-NfLi(zLo86yAS~;ktin=)Ki+F(!QzR1?g;Sd0t$l@ zdF-WP8y>YRXrPW2L1fU75EzKt&shs(+9w6a$CZzM#cU9_`)b6Mx!)d~qPVIXlo) zkB)6d+%_GFWX3%~dm*u$eB_P<3dE@OLVx({^GAt`aHh=ziK8^|L&#hB6~|vU&FYpG z{cHZw0NxWN(sxU3xm{w{*Baxh^(^vfakOfu5bBDb?WaGI2kDzVC#XNv-$(vtta*nG zz1A_oZp%@hu_>)GD=ZI4xh22)>R1<6$I3cj#wP%K!XGtMSkNo!1Yu@D$#1Er9mD9SwNSFeEGwzK>XO1srJD> zPpSD~*O2JBvhZ6DImft`S35cgwu2XF_@&68F$}BN542&2`jn#s5`XlOyjDF5z%6a} zQ-607NY<|6y;jHFRjjpB_@F(7;&>;1>p{D2g#Fw+v?-(Sc&-GG0@n?wpTY;@y;irU z>e~0jUD|{8Xe){nzxAMv9Xoj1;n+OMUaKAj;FcCYY>;f+{s^Z}c;Qn#dt-1&vmYxb zJX&#Gy{Z{g9x^GN6Cve@+yLn-_)!jix>ngzY0T zvu;>qHh(GQghD2;YWu{OISTby9F~ZM(UCSzO;7voTR=-M93_Ju2(hKH08zpqv(y4wWk9gb^aKP7>QUPqZFklVE(J z)7TVuLYg^(00qNh7!pt9JAcrj6dv)1=GWrXcD6>-c$K5S2#wQJu6!T;9rYjP*XI~N z+s`QIb$dKI?uGQ)iXZzM{#8M^i4LRq&v^W=zMbKz4OE}8(Yx<_%bm%o=_(S5si`!$ zt~Q|gZ#ufYWVu!f(`ig_o1ab-)poJ(j;uJIaHSK7K5I~jsDoHQB2lbnX7jgLWjI-} zlNqmBfgE{HVpxG>AkE)5{4P*koF=c*N1Kx!aW5%&AmxreWE_XRolNv&T6g3pvD!ag z$grQriX0s1@*VBBU473a91awK_@ z`L*SY41goQZ$K8$iM$wO1pj?|o+*`Q3q+HiPclTyNxI+^(y})3&m5~S9HNq$WcE3bH=lvdrNzh|<)IIgWiSl4)j z>;7y%{gd2Xx^Y8~{=^dVv2{J>yZyQ?yfyxM2r!eb z1{)Oh;y@;yH!e*;X;2BD8c%`ZSBDq)!W}%$M6L5|V(3{>7MP&XePJHT^VPv@z{p`T zR}4bp)tf%?n36MwiH(IuCj~k=@wEt#8aoVm8e4sU2{r7ueG1%*f^TW6Ey$%`xWbS< z+Kf*t;(7do3dbA;DtNd%zVjoS-b4f0N>dqppzY~UiP*m1-%;Q4vbFgieCxk)S$=vP z`A~=re2=k_1Xim0P{AuzeCH>Y-QL;$wJ(n2=l;~;x<8lkJoNWb-~F#EV{4F8oVd@) zEW9V=gXXMb5Sy{~T{X!e_N}h4Jx_=LzLW&wRdb@Ykb}OpMQZr;x?x7NauiPpFJj;> zg6En}753jAsHOd`L)s#%3PLJlV9!n7wy-U?p`IkSl+5YafZX}N&4;Ts;VyqMW%Q_j zRI(W-|07_cSfWV@9qjqme=6RzJ^xjAek&iiy^jyi zIYBmO^2BeUO;#BY7X8mpGe`~>3uR=1hhCgkgJKS!0jrRQuf(3U4 zYv5}lw(nXgop{QhRj#X-%*mtdC9ZucZ!M@VU<_vAa|aE4o|z}E`x;^=LjaS?=*>TJjM5%%(JFIRIKYW z2bi`U){2RU$owe)VnE*O5fDkrHN2BgGFn>X39mY(?67%Xi0n~+m%fdy_KO$R*0b7w zSiWrud|3W$f5+n1=yhBQk!_#elD|b;?H4bst!K6Wu>8a0fAtXHu|W-Nr>jY9?6Ug{ zpK)s;@Fdfe)g)wl=v6q4&xW5)Vv%QmVD4tYr;^)$*x-b~cNVaZvI@2+=%EAMx9JIB zIv>8{6Ce3#C>BCfNoU4l>_;A+ukdMyE*a2sdmSW-c9ED7mOmD*dVth{D%|^NT419H zA|2f_7K2 zsW2+ZVU6z)evI+=9kM5rkIWkt;%uzUiCp39Edg(!3K#yw*qA-}Jojf~mMP{Un{D#4 ztzw?$Y=ZeHlWo2n>t8xZaLjz9W`<4#bRyyyL$iI=b5}7Z9JGfxQE~ojQu^)$7Y5u7 z6n&<{*2dUOyW4)?aXz(A?vqDnEf47*`KvDYL;jBXNBN^YA z6|dgW{!#xZU%kSk{!#vDuk9{8%GYk;L-s3Ic*`HW`}`g4AN8wS`$u@xKgu8NHIBlg z{4M)=2+)&m66c-=MRp5jnFwe|45pf`5uGI4E)&7Y(R zG>u7|93GR*X8+qZtSnfO!}{#=PClA}{wygnId>;S zaM2upVs9r}5hIw0F_Y3_qYodd3sj+xuxA*Os>7W63XTIz+e3>~@ipcvd&l_Oj)B%2 zo2%aqKkAEdWq%XA2cc=Tf$rIUI`FxUzwe@X4C!E}_6$ae%5>(gs6Kh;>e=}(@xc?5 z2S~D#V|Fl~y;gX7*m%YsOH>611(%DdLXSRklBwq;VC+?LQeo6{9W(N<>F($cz{Uv_80SywW?VwZjD6i6Ukn}w}E+p-Fjx=fw zCg<*4^}|Qpayo!>!p#P4v)Bz987~qeZ)tGmYi*FvI$_rtL7wg>rraU4O>9Y6;-ih8 zBTpQ53UileUKfgM9Uz66DTQJVAuzFJj#y6QE#64(x~f08;Yw!n1%B=BVf{7@NI}0e z6Rre8jhy4gsC*Pk+%56uRecZTz455a1yna)ltGH(F&L)7~3(`bRaNiaRHV zl!Q~qkz{!fUI>c;`nCnCi7k@P?%mn&)7d+cMP|$WISJ{Ycxe!mMTLz!gC;v3F;Z*%_V&1SyE=A#yFql}w@WQI{5&aHS~u`{Fi&!sq_ri_xb9m7ns%~oQluyRrM z?3oh`{n_^hryU5mLBxh#0wXpUrlbj&4Jq($^+tbY{&#-ZBzg`?*3|%bYd@BTyUmdu zhqBYqkFBjAWVbYHkE8luX~AQ&@vJoP+N%At{d9_6Gk%A3@HjqgJloHO9qYfP@%OI* zHsO*$Bsl4`Cu=)^2}*~u&2vTUjwo|W?kF=cbJd*0glbWG-#VwS)zxU$bbKJzmSlPHn(%GVbFM-96!?DJ67L=ns5_5Z=B(LALHDzlmD?A^!+9 z7Vx|BI}N^S0cZQ&4?lG~R>P6}JAE0mzX5&G8JRs2x(|MNFH>yRfW@ zvP*Lj2M{BNq}KwG@QX9+IhQwOyGl(`Lbe|b8tZ8J?qd^Cbf3fct%^sN7hih$^7t`7 z2E+-9o@~$$0{L#jrhg*Oy9c;K`i*`Bh?7U)*ZTS9FTec4Cv*7P*Qy=AMMHMU9;&3t zMx)+g2U47aO|_|)4s_YmYqK@#(swo*A{+an-D`icb7{c3K9*dF&0F{_n*AfM*B0O4 zZNd-H{>Pq(t%Qhgp%VXg=O7>1ebQZr!c)G$KK!2i0Ho3Xe-mc8T;1V{o6`K$7FgTmHH}#y5e`@ zm;JN-d;k7bj6ZY1R+zft^g)k=yNd{`-3c5cjMC)~1j| zv2n?>@vy;qX8kZOndSTzBZ>i=-(FsR`L)X{uf3_C1Aapr|0~+~Uo`(b{5*P~KL`BT zr~Dl7NB%kBWW*ldI{r|%@!v%zlS0Vaw9h|ssx%~$8-diFw$2x7*pG+ir}a0lk+&RT zE|R=T!0U*wYq#UCrCab>^tVuCpluDr;`eXA-U1+ul)C}3+|w{YA?dzs3psL4pv_l1 zr#F3sHzT>yU%h}nwS z*^Qbc58=U|B1U-FSc)3W#@`a3sdXR8I zb+Wi^k%z=vb`jD17t;1X^gKpJm~GjpWUQDwiRr3jJx;l{@ZCil)s^MwI5Od{y!zJV zwKsoh*dGjg#;!iae&inDgpAi~ufHkJzx5r0Pe1wa^6`i7T|WNk1JP#OH|SATEs$`F zI~^c6Ggo+xC!fe&=wnYfX+$9>lTxKX-SO~t%0u$W{(XH#vou?lLC=eE!8{=UGw%`) zNBlIBwwZeIX^tpkXoJ-P#@7mwwtt8!x7wo}kq0JZw{Mh49 zTw4Yx-?*Y7pEIP24qB$eCTRJ+5y>H9R|`8=yqKBOxi7wq<|Ke^JiY>nLdBgSdOF1f zq9GO?Rp%v&;}oj$=*5>VufO$cZ6v?Eyzt16EQN76;`^H?`aSw@zE*p%nOAHcgwN`8 z?179wYXWb#JbK}=#z}9zbnzZfoUpy|<~x2pdGht;qYvM|{Q14#U%vk8i!GMqfHPvD zZK#kY&XSxP4Y_MR*+eN!mQ@B$@z|?e;Y0FS4`rqM0r@YS+@tjFL7+*_^BS2ZulF&?7xcV|h?UmLK=!;`uWe$Y^Ddkjp_go3&c6`g}O@Yn-T4t5s@!iHwTlK={*qGo3&-|nQ zaH+O~lK8bnYCffA8m3^pN}|x3&Ts?<&j!cTWnybf_7ym?5s97^R%_`x$50=v4h;%- z8VvdDgFA5j10%A6>%7&#RlDkXeaqVk$oPaJzOz`!KC21EfWy=UeTKGG(`HvuvoHC8 zaI5|3>(50&8Fz4Z<$E^R1q{EXMIRl#CKlX*8(JXjK{ws|?TzxplXgyfF-^P^12&6W zu1;2n9fGO73HEINU(xtGRE?wlUD$jzrhY*)Uv+RzIDZnyISw!F^mvQc6CCWof;%GB z7<1T13YkGt)loU<&0i=xeRHg0T1?)4_qUf<-+0&gx~U0a)Bo~|&;8+v*S8d?Vb|Q}k zYeeoS#10}Zvq$&oVylUfD;kW~n^rC@D!Az28J|cJJDzY&d=?DEh8ocg7Y_VN)t=$T zOxvh>W0iuT`Si)!j^cR7e-g0%+5#ZON9?(l!;0^mp}JFqPv_M49?!&$8Pz z7Fg!0rkp{C4-KK66q2pYQT+5oKQ+C_|JpAvZ~pSPcW(8wiG2RqCwjvCD-53HudoL= zVPgz=@oc~=ufFb2ZO|0fySH^mfVT+#_=o?zeDuM4w_7;Nj+3hrrMbRk>u{a9!mos? zc>^{R2U<<|s=neIZ^cvVU#Gl0VXl; z9Wwd7Q3~dBzhaol5?~44wU@r-&{xUK-|@Z$(AW&KXhdj#sOT5D1t1?0#%Heh>mT>= zM+C%>uoLH0WET0x61m>Vu!>hH1hmqLfUMstknKE{7+o)kh_x(j8>^C^c2CS*Y1s(U zRaXXT39rTrsjN7)lt7;_*k=62KJC1g+w>5FH{scwX|)=?&1ZRw{H*;G$Jm?N*?zJw zS?DhLS&Tm?zdVZ9tNWzD44F@`!QnS@k3R%XoaX^w%P4m^!M3Ep6W%KjG!35G+w|hZ zeJe3;JsdettT*z%czk*DSO20DfVVdl5H^0EBInBgEPp#az=_<)AN|>H3%vRopW=Ag zjpHqY-~9GpFR#Dx_T~5g^>2QrgZy%GA~E}nA$2F46N%bgz7=-^cg3?-_JS*pc6lfF zW&;N#^3K0&Zx0FSgZPd3EVUf2?LVmh9Rs*4t{(%UIa@jS&sBi9EM(?oYvb`h2y~n~ zPexe!7#JS3^M(m+F&_1WfLT|wZP=bWcgOP9LVRs8N+}JAe^hJ-xC<>6QN4(jgcR)& zC1{V-89;-a`Aa=oobD!fgu$y%v4Q9VFBa>^p?|DU3e- zg7$6;o0Pg5w!6Tzjj#xzFnSxjW8hl8aTMM$puo)A0=(8w{P%Xx+5SU9w4V#pf7j#h zVVrU7Oe$oia`NoW8kw=rLxI6phYWt?$tUOO_K6;|L}WtsB7qeFLH_l3m1vTm@}fS@ z_tv}rOYaxIRFXFQCzsDY`AF~Y?+=J=%oF*W=>bmcI1za9#g{Ly>eB*!Yk)feFTe8I z<#+$_|6acM{4?bgc{rtO71iQ}D{aJAP_e>0;<$lXciX3Wo57>|ZLPr3Mwz z;v#%xeVSaR%W3!kGD)8O`1f z2XjEVKs6I&>JTRXMUjl^nhTyM;Arp<1KG}hKSbcv&_Q<{m_MR<-rGpTEg{~^ufB76 z=O6yBtIeNV#JnGTZvOQ1-$!Ar8FR+Ig@*_KYjB$@!$@DMQq@TmrweF19?#jSA63OF@FdNKs4w#UTe(4y%rA)j+zbHwQbn%*gEKk zuYkvwRdtx^&G!3K0Mf7n1X=K{C`$2t6F?x+p8QnMorm#QAfWK#kXz?tR5d^R18WmP z7vacQPjZ}K1Rq&MtOEx$TrV*8Evi;E|Cot_&wGp9r;u^~`jF)HpY}!91jXys&?f=m z`-})OeiUf?gb+gAVf(2Nn;k@pPqe!pzHQm;-F{+1H#B5gR=l*pXVbsdxvtfAVB1mn zPesOv;FjS#{1dEuVZvQa@3wlf1hmXdfAd|KXH!%*O`E#YyM;$!9()AX5**o zd1_@jB>G;gcP&=`8@><|V&>^Uwds<XjA>RqpwNk?euxa`QBxD&FEdU5{<}V)NY|UOkRIvt!7q|)#@FvZbKo$i*X)Hb_tdCjw**=aZ zhHd!BmOM2UBP}-6@MwKi_>BRA=m1(m^lufX!?(OdBoy!(%ZfV(otlxqz1&US%7ArM-EEMs=+;4Tpvu#$)@2&R zMy3RK*K3C+GqrBVQLuSCXi&Hlk80QW;(NSn>{f{J`y47Zz6w7=Yc$aGvZF%V%ZA4H zaeL9&JgZT{sWM=g%1c+}n+!b6ve8G*uAw2Ug18~gq3xz>AFe)YNH zwX@Pzy!>Z?Gyl+Lnt1a^4Z`F&!uzD$GyY(B|)j4Zi>2#tw7F ztAOnKjv6c#=r6zitEgR9A}aKP^? z$=E%elix{Yr?Zp`ot!9O5UAI>|7C9q&2~n1)psm90F5tDsQ14@rkqU-1cy=>ZfIud zu-aVl@{cgKVhP-F7ebH`_Ts&?0@TaZLTNMMSquDYdvhhu;=xvobsTtc0Jqg0LfC&s zG>e8i0VSB25W)74^-KC(Ww0?LMiIdamQY`~U_{FBdHJ~mA&BF}ML`=A9dYz~VuH3b zt!q;RvK7&h$FK@*-vcq}`Y>`^FmOw|mV7?7X`UOm;)lribO(VY$pd%cG+{WAjhn z)C1%M zxj~M!{Q4jNMb8BMGC8Nz^OPQF+mNmvz*D>9)Cn4K2#2NMjgUj;hL8cUfW4F*gh6Z_9UJGp0hGnGCkt2oS(Xeq( z{K&B}`iFJKD~keW{Jzz0T^1CnYf-?6W2_G-j!-goFbfk4j&!&kZ?_yMjYA%6MVwgI0JCZ2;3;;0av`D(@*xh z;^iOVDzoC1b6lzbSA63S@t$}*){+ITc-61kaH~C=;2^8aidSxJ!-|)GgsaSoS8i?d zif{b0{p7Jx{%1LR;AuTTj`U6dxx+I7d_sWI^aS!Ej|-F2&F!Qo zI;3gKy5<=duh4VA#w0xs%o7geRbRtS?jtUO(>zl`3L@5TVS`xUOpQ&Sd1G5Zdm|LQ zr4+0GHKxSaSb@3zrSKM zpWi3cbQ~C0>+u>DPA0z5_mL)eEN)zFzG$R?QO(tf5G_vLoJHpj1aU@2Hmv8?7UQO02LtPlJ^O-g=>1nH8_B zTqg!Wa7rO~bt}KNdBw{=!qo=&*?xK<{qQ4=zfWF0rp%FS%$=dk5_0|p?dojW+)+zu zZcDK=LCkQ4&jVWC;8jP@v#UNVpZUg-Dzp4`ebmqEhw6RLU+7uR9(b?^I0@*NK#@cL z^gsSj<=~^qF7hjXUfanw$>haU1J9Hr7r~R0;e{4?tki}xSIk0`;1yP`&~mb0NTZ4a z`HOm>aody}e*BWu2GM{%5Q8evTEc;rzs3U0D>36MzITWnWO66mo7PnOupmF?LQN+p>q6Ciu)XTO8)^6b@x5!`7Vqzm<65T@Z#~ zt1fWG%f}gbi`%LS&^SI33FIw2Qkc~DOcTyrZ~=D=s8~QyyS-4R74wwe`QQXU>i~0y zIf5mGWCa@-1BYt&ews#W5fFpxvM|turX|)GSInB{yWpj_6@fj^vVCto_!;S4bQR- z5{Bgp9bjl8-BZw_SbLjy3eh+|z&C$1&oN*(GI|KmP1+l_G!WC+Xz_c0?qCH!bcdit zBnD;6gt2MoJc$nyk4Eun!xm1wLQS*b+&LHxyJt8g;g04q1P?w9(W0blm-v@%Y(x?c zQeoNx9G{;!dM-w&b8Jn0`kI8e%vbxZ>rjo4ttz~1_@!OhveB^vz9}p; z=!bH)p9E8PeV6fRem`m#a;Mkuz7A@5np7{0r*PNK)76;ps7Xka| znrV(KY8t=v>f4u>^xi)OJABly3%QBs;p~BD=mBzu+-c#jTPC^(6 zj%*=#bAB-K$zc5fF{x4rPKHx}#%+5gjBn-vFWVSsJY*eH4hG#8(Hxr`!%occSG;^2 zfX9)b?F|(EEbaHDHiG^erA+7;Fg|>){_RC_>8M#o*ii9{F!3O-O_Mse7jyd_1~h=Z zNi-Whz3hgFPJ7Vf1s}=1fV0`_79{p(WA>c@>7mOaMY8aggU}GMk1q(&46vL#4PYii zmkhWM4mk@P?O@5|e=Eg-dRcFvaghr*ofRn)xZ;(eK@^}~j%?sW8Cs;SZOEQY4!mW4 zZw!7W@In+I7lKm?R~(vN_A&6bKa`fT3axlW&i1D#zR&n$L4C^%a1l=K^v^Ztyxs=z zgkj!AmLuom%oERJ(LLupR?PXFJaBg)3sIlXk#*_uv#_te_0RFE;}@TQy1k#f5l`go zfoJRio&{LHEa=z&_&@yf!_8%K5FD8OM0wBLAzz)d3p5&H^|L*wCh`?-tC<(J?->#w z%$549@7lvBIRAbLY*46U8wl<_Z(#Pnp^d-#uKgpl4LN8$__B|C{G*>8kib+dJkUnk zxT^rPv9kGMUA^Q50;m_Ak@p1ID%J4}grUu+@ghUl!8V8u5sR756$&B{W7AfsLz=?g zZ`-cRAAzXsG*@$gWrf&?Dg3KoM+A$+L~J00P3eKM*dc$@K6Y*u6e&QxtUGYU@8wVX z({b|NfPuyrXgBfa_KffJqnIuePg*MqMxc#B>j(!BN0xA>@^!uZMf zZ}GX!I!w5BZ(0IrThI2Z_uBW2ul_SVGGEiT5Yv_IuRhR6ukT|9n$6yGffH%5Iq7E3 zZ-2sJ065uZ1$g<5-)ukZ8hI(1FpVxy)Ye;#yLP52VWX~ z=!iQ5)>ebZLH$8q?Pw(t%BX$-Dk?HNxG3_{rS{-x+Z0P|+WH zQuND4W&qq~EW)GRX=^AZkKM6@t|@`LGOLhnrKS1MDp{2xT ztzFY9^IhOW_R_CJl)qxYywZk76nkg;>4&v1_WlVcqYwT(#y|PYyu%)|pd(-k`D3>_ zWD-nzjqjZ-{!Zgg9EG{-(Vsljxq|24aG2Xy)HPH^4fEA#h$`|ad!FJWCVvK+L(*gEX zq1q1c@JR-mA3E}FeFU_WaYX`0{9Kuz5^Ta?0 zN#?{lV{5jl7SSOx=<$4y*MlU3r`HqFUV8o4wn4=Fs^9tD{`ACIo<|RmOXL)VJbm}q z|76^`E)2NhW+LmADzN)rp>tXMLeocwY){7TaMFD&#>47!#l!vXN6$-qYd-l!tH9?E zi{LHA@8jQUzdi=|9SrDdIt?FFvza;o1;*C|6JUNuce+TMryl~8Y=TeF_KQe+azQa` z4>HNt3trEw!X_uajAIyV79RYHBq8F_dlTYhv1$jAJXR2g5_;}Z80W0OMuCW8eJ`|! z;&##Q!5u?5qcL`dW^m&`I24Dq;Gus1V^4;FD_+ar)&JT4pa1xiqU0?zsGqpGmH(5y z&b{pD%4;PAI+Mg?=HSV2&j-nH7ohWxDf8&@%a<4RLq9Ftwmr+)1JAPu)|`6tt#|c{ zg84PcSvKve>vi&T5UhNzw%rWvtZs~EyRWp}hd2TSf|r0UMGR54Qc=Q@dA0 z==Lu`PGSU}xc$`O%ZCL5jbjtqeFZ(r=z9Ydo_2+2TBJ;mcw~qvI@m~jg>_q4y+ZVe zt*~(xB8M#(;EE4f%EK2Ndxf=ASd}Yy&`(>=_R|mU3w2f@{Z@SAyW2Uye75l?8)e;p z*vy@?=u_83l>qsT(aBv%`eW`ep8WaYTI!6U#bimkyS-Sz-b=6jYMTdKsqwR~XE}S| zdG-J~wcZ7I=iT2pEt|#7`$FT zb@An`0!o`;I!e5-TPVDKfVW=AuKKnt8qoLx@tM8~bU)E5Y|J12`2+fe68MzW=8Bhp zgtjGO?Yj-srai~_?dxp+*8d*c>8tkH3XlEgG5Hb3zj1Bj&6qJyA>>U!Aq!20P zv7M^gMV0dfpEDqHoKGU@8(OZe{@16rVV~vff#=l&Yi_;CM*=5@Cw=V(d@D_H;EGqE z!ehWy9j0n2(<;B(Sn=Vnq!q9FalM6JT4h$a+F0?*G`8;&uYL_M4YUziwUJt<0zQ^` z4$oiw;l)Elt{kkRt{>TaF^HPs)k3VQg_$uu#-k{6~G`y&9Nt1@>?8?3o6Q+3D|)tRT=m$0E&`5dG~vD z4&Y_QSN{D5Hy2+Hmlk*jTY#~}r%N8|I46{=xOzEz8A8smBB>>AqmB8`V=bR-2 zO~kxxFjTy^5>XZoHcFO(s%6*E205!Brrt=UN2SteSvDxM?dCYaSe`<3;i^&U6A|YYKS0W6i#dvlIVrI#=l{^9aI`3n#ikGJrikyT zLRY>3Q@q8Bsb#>zlNo9ZWj8t?#UNWA8q>7J7N4*j-{2bpeo|w9+xo&O29Rr=SAN`G ziPv?daN@I#c-L7Ka=l6i1X)vg>q$r~@Nc_5^6mOeJoW=Wsde4(C9e#XwazO)=I>1X zRl&9oC+zksWc$-Wd%Y4Twp1p;Gf(`Ch}1o2Fq1Ck*fIAz_w>!(5lTjS^*#ErmPudu z{O8JE=%oc-N(+49^Pk;t-}hA?$ao!z$GCW?U*)0I8EP`)-}3+-rBd;p9YZryMz&%s z{N6uBmBFtVD^J;NQZM8(UxQE?_VwkuD{iV|9*TmJe+$5oU{7hOcqo8}tw z!0{fr#Cgb!MI`)or9obJe)`y#7xCxvX=tuI2!ogXaY8|U{QC|Ax+Ijqv4Qf4QB<;T z382qZ?uOaQbOIx}b#yrXn9wYQCztRT5ffN?>Cg2CbNEPcP6(m!(}Jc!ru|}PJz_u~ zlZC$y56m^WP7*k-iLEg-W!cI^e$G?A=QA<6J3_*NYyc-_bis z%0GGcS8k%q3BcAA7v?3kz&gfWd;NVHtTTq?ufr31?uF1gE$og5%2sCnU(o~_Qz z7tb5Cc(E)fTwbs+d2vxWCjwP-P_nhT4)MTI zi8$;#3YJL>wH$m_Hw0efM4N~oTj_9hLs!L4M~oF8KYZd6WjP7(u)s#Cys7fBrv{^7 zt>Xc`>O?wtqaU2guHx$kMn*ZYrG>e#gMd;7gV!nChT{oQbtCs)OddPaT-pfNNA>_xRnfOP%?m2WIt<*0X2% z!WIiIhf52*gce|oG1hnsfVKKP?|J?3+0V|WP8gfp7(L^>WzeRDy>|cGbZ7k6eoxV> zY~?H4#pk!pnQJYV4YO-Yl@Il-6W@@JGjS|-_!~|c**-Z5umz<%hluUOb9^fAO>}8! zS3vcKvXRfWZYTm$B4A%ZxCX+c65=fi8^J{nBp6Q~o>%HxZ(07t})>c~}Fvi}_r^eAptdyw-y@43xN zc)Dpf+G;yi^0z3Xrl}DF; zJ1;l2*;xri>C`+D)dO3_+ki}}qeKXv=N(dk)Dt5-LdFgq6iW8Z0mn_Fq{To}K8RlJ z^zaXqto8$a?C7XUPZW@$ow;yN9QO4(Gax9MD*M5BLo?LxLY@_r%VTf0p#W*e_k%h2 z{!2i>zLrkS!$FqBpRGt~+*GuA@Y9&|)bzx=@2G0)nA3xgokrLj)LBddCcezv?lsWc z{@Ms-cClf6^Jix3u5B|I{3ObE6&V`o+|I1p=gXAK-YN$$4nQ!ybFZ>vwKXVuak;hC zeV$#sfIoEw<8@a;efpxk+|@W)eB}y-7iw>q@SCGly#hWY)T}+5Wm6%{aU$BY`-B8Q zop8%v9YH1*n1nMx`>Ae?#|4jDP=UW>*-386*Q#Y|QUf#W_?Vt>|hp#wkr zF?@@Q4;{s7;BA51mt*)E^#jZsXIIw_29A#pzJQlkbO;{t zFn#Z4?zQq3jP-#0BSP?4Y9Uo-EsrwKPaZBjILFkxoK`6olMstJkm=vh-YsmpbtV>9 z#BLrQzK3k+29u!7FE>vqa9f?oG!)?lu_VIDL)|Duj*`QJH1=XZZb~YX8XNWJ(+O?S zeg{PLzrv}SYwC#cqYYar&?bduu@y1|uujs(@F^>m?nND;l*P2t9_@4F8TO9OrL5L- zquN@lK50g$qW*V*V2F7I+iHgBI4@X-)!*Cd5RqzYPiZ&CE0?ZYKc4bOQe13|*;o3KqC!6mvKw8DcB7Wr>8y?3Ts>U4R@Ja$fdp>M+`uEvs zofQskA`fISrg=^li+ZxsW%?vWYrJ-EC~| z*eOPBzGpVDR`=C(UFR2ezIO@5{`Jxs5BU+klzNjje|WL0jzS6GY8S=FiBMyyvyGFV)z>exBmGH zj&@#p9IsEn7at-xfZrs)u_kp`U=vSZY_2+{R?h1mnsIS$IYHmm{twS70h689c!~-o zpHGI4{>mpi)()J^(j3vnNA8>DV|7U7#cEa2=Ze>2zHv8=_A_O0ZqO9n!NE*^*Tv@* z2Qq#(^bR+LsnLw24qz@Z0--D+QIDoYSzbQ;)`rH`SIwox8+ud>Pn}ADE6UHUU{N_JX=+(y_mxyzrhj?7iu}B6u~6jFRim9@ zeDWzs{TR1$S>3uqOsJC;5_wLZCblMJ{lEtH{q0iY5v) ztgtD;;_+o~S5Ef?qUGhC4}CEpTmx+M=_?vd)Rf8#^e!J1w@V~kDw-C*%nEvZ0EENa%UW;gAR z-?tZmWGag1O8@wuNB1nbW8*C{um2a5o4N%K$%IdJ&iPOe_}b&CetgnTH60oK)n=Dy zS-CKt0ndHb&5@)ds)Dz#AtbnQ_}j%W25|~!vTgu)AFX~2e)M5}t6F0Jnp3RD;w4d@ z44xtlWT$NL$#@D}g`$_~t{M>~Zu$yKpF!C`PbHg{1V2`DC;TGhCQC?*TzwxPim6dw zDdAs@T^MO=Z}KD(0=4-dmUoUIpDfMj3_dkVyEmAjPM}{ss6q|;$NA#`y7A&P{>zQIpXe> z2A)fzRfr;LujF~78mSe&%7<_-W~dl3eMXsru)jynzMVQ{ur7>LBd8y@6pcN1u}e)v z*C9Ab_hgy1?c3!9ABVM*urfRGGEWP`I>z5GblX9Z-AN{L=Cp_XmMzg9m~24df5z1(juToV!_b`0xGYm0c4!JQ_xoYx_ObEV96TcD2#FORbq`$dE z@GbHEwX4gbEPM(97cn*zuC#-XhR0Py{49MaB?l*cnJH~o)AJv+<*TZ(*Ansijy_p+ zY&pZZG$4*o%2B9O-mO4 zfGbWHkG%_?w|M2Cg z_omPSsm~aUQ-r%<#Q=v%pn)x|t%a8ScQC>BN3dVd*kR*wQKlUZb{byXYMQ2x-1^JVa^+pGf>7`cmUx9wTI0Sd2;;ix0p` zJz$BNK!qKw3i6Yg>bld*)?5IRF>+1_G-VdYYQ?h@m?99j7*NP74|MeDY+f^DTwK=iw~9Y}=-v87L8m33 zcShUUWpRwgLPlL*rW;3#T9K)fdi6JL8rdT1rUOGoC#QoMw_<&G{$zE?5m=o3r4Zby z?Z19MFiX~SxPtpT{=jc_lfIv7={Ncn67ZZF?}hDSgz0~4Iy_vI5l<8xVmf-ec@juS z;jEU!>WD)jHu)r?XJwc|>1hNswUNHLpV{S($z`oJZx-D44nn7#7a@F1v1BoLRxqkhfxco$dnP?AblPKOA5CZ%A>{WQywJ6K-iJ=S>Ew37W1!dcwGW%C9zmhR|zb&d# zSpDM;DI3-D*^87og7A+BVOW7Oog0zfP|fP(mN{QDaUfQr&7X31D2=Y0=FOs}M&P#L zY3_($+FS17RJzmE&$>b&hxYBJFb4?aM*TKqoEzQ~M>cNF7ncxFf+&J8arqyE6Z(aB z+@|$sA@+M2_FtjfnCHD?2VL2KA;?d)wb?oEX?``L{(`C{6{Pk6b#=lvSrs!gc+x-F z(`}GSJVwz|jrKY43#FKo>2xGn(h=`B9#i8KSZ2&B1hH$+D@jmz*4t#n=2Od0)!E=a ziD6iufGLE;pYfaLyq3X1ReXe}`Y;tG5^GfbQH(EEvp088y`>@|PgIz|W)&fvq_;9U zS_Hlaz@;k1qcFnKH$v}>5{05eDfewdX0_e=b0YZM5xIOTw?LCQu0~IPrZs4z4Y4_d z0Y**wo_SiYgK{9eATC?ieclYl7D2Rr6wzyGJ4>ozfztrcF4`V&^7cW6(dYOSLb@v7 zOTv$7xw-C`n(sGk8L%cq`$yu(X5aq&8dr)b9}+s1vuX(0vx4LKm241~hSvhbGwJt` zA}_dpN?Csi>L={Xw=NP&79wb*yozkl-7pAPkW}?ydSK7d0BDbLhcjnNf6NvaRsa}GnjN$KcjoTt?0Cu6(_ow_i!|!tL|=+;kiKPD^Z?{xZe7y?>o*B4I`+m9iWNIuiVpe#ed7>~Tsh~c$Pa}jAPrjsdJ)H-qQDFpKz zuL?pO{R+X+exunU_{&Ckg2bbtJ%(zTlYl@9JDzJbDFk?6(3?BJeO?hlS@T99^rB!E}t|JKso8e_2<*H?4;H6x!fC zYiq)iHx3=G>?ig;T%gcUbNB^3sHaRqw`9y#SOD;P`HnIh5ukz{B}wkWOF@h+P&d?I zLH8^=@A=uht+PZ~^@gEyS&xHMhfE&3VM9W0?)J%#J+&V zfI3Sn1u6`UA_PlMAKaaHS4EiTO@Cf=EBm;nbPbNr?P?lVtELj_JNgt1XY4U)>hBI8 zIN@ltOdmaLfFIY0_U2}fZj6ii4ij#=&Z<+UNbE@Qc{|Dp8*iZwu!MCK=0G4qK%l)* zyVv>l(6)5_7BLm>$9iEv;1@e9X(X(&&#-=L3Bk9pKNb6v=?Ps*;dx0M%zy(hEaa;+ zmULyW%zMth46CZJsJTS*+9w~0yBe|<{!y;H2k?YpeJf$_mW$EHx{N0sx>}b@8s|lo z5`O2P|5e~jnbSHJdH0{rv(o^z$M35_Uz_srzi!ydR{|>lWOpIHq?_)`i%UW$9hAmo zr!F=40Q9+}$pPLoS$n}v8lx>7t1mBK?{Q1 zVN?o-)*my<1Y6(FmE@bo-L2s%Uxg;~j&c@A*4aYqAn#XbWuY;Hw03#&F&$&0(S=mD zr3rs(O789=;;8Yp#MExA;mAL;cXh};zP*1{=SL;nx-lhmo%{M?kid6aU1e^-i9VCr zDu(FlB5X#cCp=|Ee^jXMH4eB9H28oAG6@tfv5!%``TRp%efl3B7Z*N?8Q!+F=#~>x z0jaG&GKPE+{MfrQ7q;9Qxmkj&iBee%&VwWykZ4ri#e`aCmsAz6v=S#M}^Vi50#^V^=7dY*czR6qw+_ut;N zdo-7FA@Hv}SQMo$5dvSgG-_3|U}Yh?2woQ5n>Le`s))Hxfs1X%aJh;&q3}HTsV#gl z>*YOZk9XAqN`AX*XEP*`)Omtp``LhBG;BppDD&R^rk$I55)W|Bg_KP=S{I2q)>p5B z)1#_QqPo;s6CQn-dpZp1Hwlxfgf1(}PZSOWlk@nRC+dJf_0^z(M=U?W-Qlmixr_{)nD5xeS^rni9`ER)^5EVsJ% zt3_k&a*hwT{T`G5Q|6QZA8o&aloth*Qv33Hublu4y_y^ z>!YE2>ZBARVU-I7z{8K4b4RBRlt|AZwI2>zgO#MQP5#T6R{fGe_j{SKA_V;j;aHtp z`X48Tee%wyJ&B>VIkD-E5O0!KKaDL%EwTsIC^V(nowsE;5O=W61w@lhW{#${&3>kd zD#P;mRD<9kafl@SdGo@c(Gjo&WqaL2EODz4_&*m+LF{ZbMl@j zJqpu8t1pl45O5MaONr4UCKF2{eod2`vYMN1^?22PacTLu_t*@zw*srx=T|CWvd$UD z`JRCj15*8Ru7~uMeX_Y#jBGsgC$a^Ll<)zqSMACxp`#tt>I!Wtw@7)+68p6tC;HeF zc<%`h!qbueu40`)qhh{l3zgVByV%1D-pgIwZ=!thsG=;!{F??OP#tv$tOddRLWSQe z?8atc>1a;M+_i5`?&i?-naMZMn=3tOI5f*${;J`>nyBiaPeks{#!uH+C4%MLH^8GU z)#{6xW)>)n_K9G`L~?|xI>yiYnE5y2FSm_=*)5E3_{#toYo_8s$JoIg^y{eiE|GQu z$N8jD{i`3X!{1%1pXYjTZc3lRpi_av49-3ubi}o8 z*b~*oi_k__shvyV<{l4`*@hcOA7W(C=LkdxpPIZJD8l&8@KEcFl*S~5RFY6woQGH@ z@ZyuHVES1BULD%%tX(>hXIfB+?Fv@Om&Hpf>6;2?Nr6%=W#28uIj1@4i5&zQ2W*eD zn`U;C+tj|FMroCY?jvRSF>H_gPAzoO{yB4!4`QjDvBqsBQ|XkmsdkFW2@(lulTHIy z$8Km5u7$bpwYC^-}kd%}}qYHOCwXZPCgO!<-6 zr|_n`&DX6kkiL?{-{hmS&QY8tz!Bc*KWM#>^W`A0DMky(dGzIZdC+Sdb+X_v=2(b4 zsAB7+=mz@rG~4leRS{ERZ4|$QKX+r*_GF?v<|>78w`nX z1}a*pdL~8{y5n#8U{1WMF#6z5KRi~nFy+NTRN@PaBwJ-v72*}lWw(RisC-> zhGoJrpevvY{4uYnG$wn^zs}*Z_V{aOplVTBzxPI|O-#=K%~&_p5LqHk*u%)=>JybR zIA0{~^X1#m1EkzdQcV*ke9l8$gp7Lh{qh#3<+TZ4W0Cpb~-7%SM3L?VA{c$WfI09-50ZTR%B98Q8M$j_C04> z4?+B3-=}Lilw5sqsjTSfc!w?aPc4R2jxq4gb{do#;CCLP`sk)=6O<j?@SjWKz8{ii{#)7>J!IbmA;H&o-SSw=74%tc)JUKNW8)`lO+|L;TH|NkNA{WSLq z>NnuuF&h`{>(Mr|Iaxcr=H-!@-|YB$q9zVCkRXDAZ78KvOXTgL+MjaRy?D^|XWxP%=MKp!nQ@E8J;lvTflS83 zfi$!+tdZ}ou#5#tOFxtI_fW%=SEO{=WdA8T*~w*S`UeMAx9&~jAoZ!Ihr^{Z1XCVi z;?@s4f0@IcA?TMfkaC6&){Q9sG?+}R*q*R9g=YwN_#B!0U16mA`?z)7*SbdFFG3q(3yYdWzF|jB$)fW; zPG3sDV_Q2&VRbx{w4uwW+c^a~Vfivcym1U|51|xiwD=I>??hf8-(C(cNpvNibg3Jb29bi$z zx9m}|i6gT|#qdx`!S8Cks|+evpw={DQjqR@CEhn9;|vAEPRRqVj_T z_SKz)7uia(9eb#fzM1mbBw>z<46LdDX^9@W-8K=)DUN==KGEQ#W~eGOlcOC)s4mll zP9TrwLFcXKdFMg3SVBE?ac%IK{&`hs^~nO+%uUsW$M4NwCI;1fPuub`$XOOesApt7B~S;_^)uV-=Jlh zpGgDHm{*G49jYsJOF+lb?Uw*!5Mx5+S)>JzlX+|V?{9ZqBFKMAu%Dx=R~Lw2y`fa$ zwc=xU;NTYj4{H-!1jyKgTYAOrA*_h&%SS%{G$CZnOztdxI<^s7bf%|Z1pYuwh^Uv8 zp})+*aa;;uWiT15s&igQaP~)6!GM+*67O_^3f?ypf9nG9sO&8;UUl-)A^s|`dw|>j zby-;B`kT$8QeX3LEq9U%+q5=r!-?HX2|PB~0oDcS{_5wfOqT^^dt!kDrma&{aCk9S z+}`!}H^HI85GDa6IK*1}g(17MOtWm&{s$sh69l4%^fF*A>-Of4m?~jC()r%jL#cWs zbuT3^n=<@qd}T>RRUAmsyGsQj_n2+;SbusX8t@);O{1tKoH1O`YdD9`PAekFMEunN zd%ym>!WDL7)YQp^NuEBGaVvrW1YghPf-PEftCqPoZ;Szr?WT?z4+Yomu>Atg*3@Bh zHSFTvXY>JJxdHK2blOZ2F@&-4=L0gyP@+iNyuzheI@YGzyt^nCHCqn0Wf-_u7_aJ~ zpJnqtx;1Zcu!*e!mGBlm(i^=ONJri}m`Uu>c(F=O_L720vwZIB&YWNm8&njMf)*sB zMjMnu9jG-!Eb1puc*6=NPe$v|hEnhE4rS7xj|lvl|9tHa2xdLe0&sb}S1lf?L&7&9 z&v*)+%`d6!+f^0@vjcyJ^Cr)(qr!0XNGEe>e;*OU&3w^S%xnnFEQ|70)7rJ7@<&Xjm7fLx6t0GW{Sg6jt51MsVA)>xEuC9TmT^Ohv|||A>1Xb;lX1sj`!TW5!2oj+-4o7GvEV}$x%@yH z**QWr>Z_JWTh-oQ?GLH+stdBfOSp9MYyI%5x4>yVmk9pvy9E#xIX7vw*MDriu)N+J zo^|80s>$?v;q1Q*jW`1nGwj~FinzdW+}h9Ath~gToce?8cEf$g8yO(pD-(Q02BSzJJa|!Sno|8a+d<5M77vvGYfJIh!r*xC-uD6UuYZGT zguAi}Lt2o^2tg?QXg@b^-gBiX%fKM~Q+HdU&v$|8ccw>GiN+d!y<@|*3~OxHIkIR7 zTRc50Y~Q7V?&FPY=NOWfv@$a zduDtn!s&`wvvq7B(Z5st-UTKz@m3P<^gWXQiQ1yd%N-VT^qv?P&OU!weXqp zuMoV^(P(WJZ__{pL|&%<38A^2Z7c-qI-LnNmNE&H<1$MjlU^hy?u7=NjFt#lGtJF> zX2)=En>CqjV8W|a>Fm8a=WSR!X|A&IHC5D4zxe@jDFf&oN1p?Lg30^|i3wPJ&VBf1 zIfsu=_}nT+kULYdgmlOEU`i$A0P&XFIGD z?V!LL)N)$k$E{5$Vzk!~d_@5_#x8%7f!EsI z4>-HKw{eQxi4Z1kPo9nz@);tTU5%+$(*@D0hhwPd+T6zxlq|zGQOVl3?8kA9mP2l* zQ$%~KX15!SY)97qlOdR81sk>$=DLP>fK}mf_FFkf!VkV|tHyj1y&V}Re6EV$Vq81< zkdAOTyp5n%jwhvv>LwUP(7Hn!vQ#leZSK;}54>!Gd~Klz_XEbU6^H1OwU=B=y^$0I zwdHF|D&~Et7|ATIm4@C}?@+xh7IbaK@&aPtK~J@pe)i|n+>2mqy8+3gedfSUtf=9i zI(h1=eFC38>5UG@ZBjLBTELw$zPH{sT{cBH39{3Jlh_lB-$@ZE>dy(v`M0W)EnqMw zBn~?ilQ~83rCxKOe#%4zslC>h?FPrg`#k&sM{;+#SCQ#y=@pj$Nz|-%t2XvNu^)Zx znq0|A`Z1I@cCjUX&d=iS&jx%998mi3?Un)>;N-|1X{y!_%H@KzN6Or=xU!jjAN zOE)J^jKda|DrM(I`;XMfM}uCDkpt77{Mc_KHiE?*8I7c0pMQU=e)Ve{U|n#PGOm6C z1Ra7A@4?8S?+ynlT7#^Sr+#YUU7%uto4eD|sS^-VY$W;8+0|L_C8Aq6XwUfbeJ3HO zrtYTIu*2)e$;CSNbuFV)_P950?(Rhag=cyr7ei4Tl?QP}BK^1T)m-n*q}Dgs57wSc z|4>KlPa*v{J{47(*5P6%hSX4 zXC(eMV*m?|%_rM2UJPqSQbQUb&ibz{FUiOwzF|~A5kwt$N;|nIO3CHn!ut%^abW}f zT~$s4i#Cfp{M;&FuapN+X0Xc{wTo)dcy4I&?e)y#NvBJPw|g89vzXcJrw5m4gO!Ka zLT;T#7>!F*$o`u2KRT2B(8qMDsQ_6y4;YJr}S0?{Jw7T;CTN;Ys>)(~29O)|JP3 zDqPN-%E=7hkjA>;2QHpv*8$1O7TbuNnGKE>ql3gvqu)NLrpdwW3AA0!_zMMN4l)Ul z&d6^l&?m(G{s2LmLnJ~lHpURUuK2d7Y(p!Wz0{+Y?HaEFo<rL}xoxED9<`bDwyGY6upD3f4)2B_JL14CJ$% zbB8*PoC=%xfSp-H#E^8)C)+fNj3j*&#sP)?_9@SANov}Zr&T3i-E$=&SVgM1X_?XL+} zy6uTH3kcYuJN0VHM3d)#?_(eRH14`F=;1YTiuY>pQom;fm5_eB^t{1ng%CEc^;MG5 zA}>$vS)a4rQjfU`svgP?Vr6eS_mWI*n~EstqjjfW8?Z}KDHgO?kWKESIC>piR{_+B zA-qOB@1B3P`GR~v#sD|3heWGz-uGmgaSTo4VOwc|JGeSK+U&Sr@7TI4sPGce9QumaWx5!U45O~52`tG z;l9b}@*XGSF!}zIzyB#&Gu0**+F7uTU~ZvK=!oUD(}Su=+U<iZEmmfU?qbN-?&9Bh6U!LYN+Z;DbU{!ImK^xG=Jgj!+G{Ibs zV+q7&{vuU0_FSfDgYzhXt@lppk9rd@hV3x$DA+DNZJg?|%Nct+odasX^uBx6%y8pX zYvUAxrpL~aNUjrTyt?Bg0Xn^!30VV3d&0fP4E70$yeXEu8DWZnQz_Qnotx6XL+N^fL5iyvt?=Q!*s7G%3_7Z^L)jp#4WAcf-4 z>RrkX72R;nLH|tAB~ai|!p`Uea+g2*mhCXQIpIOya{wioRy0XsQ#m9d4q<#g~65W$`aeN=dx_UM%n6IaJ zRP=7SG6e38>?Bsbvz{!&s2F+sJ$=7=o8HmyI?co&_qo6)7kR=p_Vj|ZZ+d#Y-S}4A zpOoYZ``yOoium2Tin=^qTLm0nXN!V5!-1fBa*>X*Ek*t`zFMDKVsdh_x8JfK@%xmC zari@A=CO^ZY7bC3^e^PK7<5zYGR3@;9K6zq|1N*J3iZ7W2@=(LSa6f}7TBtv>)X*O z>_h7b^;0LRXY>lXDFu*WWgzF2(%mG-kMcK~w#H%9#OAfIQEEx1J{POzNi}h}-)uh& z#Y2Vq?_ZbmVLO^zV=`6|&cw;%)^RV{_0s%u9Q_x>q)%?!$0<}=c2$qj=C9UHPk$j8a8@@FNXx498_-f zeU*K;tROGG%uP7j1FbRm7@VXXC+*hARvi+2&q@abh&~q=geo20Y-ZG>kDh)9#K!y` z;C3={BkzkKI5&V&t@pb{wg2_?TGt^jX}}m_z{U<^?)8V!|9&C9>-Ku#3Ow4!`ys7_ z18$G}&vvfSAiX;!Tdym9#;^Tbe6J_uFACvTka5{5BA34(vF`2i)JyK~<8?+g2E||g z<`im(uMp{WP%X7d$#XyYx z&|)kq0bWfT&%hlM58@+A`ua-Nee*9wI?;GFq^_ogW7j`8j}hf+ZrRsDoR%AO;fKd{ z&|Ho?G>9}{kTM=Tge$MYH*LjyR;g^_|A!-6f*s8Y)%?(VOBE<8mPNg(p{h&F$1@Wv z(aQuF&qoqzGlvCF9~FM6S|l zJkTe=&8+PDF>d(faTY@KG){#|0|h{U#S@T)nC6$Lz5J&)S)&)}aKH=xn?krTj4!AZ0++KT%-qkXCWjS?%9Y0Ef8+#RusdweYTt<0MY7;c`@tn2lMNBQyec!jt zQF~6?o6%}Uo?C8`Ms$K(-K7*TY`i5)Pu0g&rV6ok(q`!GmlPE-xxaoi3SkmHZr?o#-FccMtAXtw)cSV=ZAX8>0$E$ z6Dm{67i2$&c3m@JFWAoGP|AjW9sZpPA}zr3`%88$)2_LVzM|cAQ}^85YV3bt7!EkY zhdlH-LRJAXQ~h1Jk<~@Z{Tq zdEJ=<+$;PhdJ00Il!Ku~MdIfFenQFzzZGrIVV<6?m4QT%A<>^zqb){xZHF_(WjpWnd}xYv$_ z0t6fr;+%E^g`L^uK_~|Y$6>um@=1ci9wrTd>m6{!A7eVcMaH7dK1uB^FYGl0`PHHJ z(TWNlT_m7+M6MfEvfx@+ z=V>4RaP)HzSI6)2VQ>r*KBltkdL_zF(DW@|8PG|)7pKs798JjlKrsHidA6tOWhbxA zR3rD|wIukdtENdwM{ zN1k*Rbx6^A^{r~B0Z)TZ(Q#UMj&CKwb>Zy3a=b}GlypaGtFi|2mL}f6v@?D#kWHt~ zCGBQ@dWCyLfO&t*pvXsCM?2=U!fn91Q$as8<-^A|SoE&_BI@c$B-j5o%6CJcwuUy* zO(@I_)}OLu=}{Q(aRf&G_~WFmJc?AG?h#hx=`}s~E_ChbJq2-kI$Q!o@*G0;uk)8g zpJoN2$e((TL}u{sN5uVIbITNR!&?TejwI-UH=eA%amM8nZpubk4|Xhxc%A5lp|Psd zpTpHW>zkLNXPt~{c<|u*zAilC#m6ua?|le&qWvXN1{yo%P@1Pty^{ihF9`iOV7p@` zY{L(iEAd2a)XqwJ{IXuaYnnj4$;XPUKea{Y7g`(|491ho1{1PKez_(sRT3UTey98> z8IDlaJ+88pF&W_BeDzqXKxMKl?ZIq?3>3C*%(Z+|L!QrFO{zwPvm@yQk_Q7}IrXf{ z^d5t-zSq1&R#O9M_%G~55B+TOuj=94JTH8EQv$bMhObB`%SQSYIFM7=fLQ1NNG}=T zeD!}`$^+lm+-OjVun<~`8w0MKYh`V>M#iElY<_i_%-}nj|j_rff>p)6~%Q zy7ldBl%3mxm2^fZeJs2K4^?0Nu+GrW3OxrXJeokI`7egu%oMVOO>;U9&6aHTRC>H9 zl(Y=Ej_lhGFyCHfI7EW_rzVIsO-Fj$Zb>OVTy@oqvR3}|ibwg)3i?V#BE&taepT$< zB~|&Ao+tJ^$HJSZW(WEu-w?Z& zMFtk*OoO5azn{#zFt&-Nw(~aZcNGGjvLI=`yIQa0PY008oyMWpgq{r=zulMhHRyNr zFxH)TQ=gV{eqnqecO9w`&q`E)VYDanpA(D5(aDwbH=BPe8J9w*) zWQQ|z+7n9+J+Dn(fql(HE5G6IO+Kqlm5i7cW&*tMeDJNUMd!d(o)GW-GOSwVaZGYU zlC?Gebqp!!BJ>Bjb!_Z%Kf+%~4-uKmf&2Y;KoJ5IW}ZGxc+9|4Peq+8CUbe$nnaV0 zwB6j|bU*G2e}!wEaKIbM#{`#6bkkMhzoHU3g{j8tGP|wKWG#<>83tq6l3>z+3jnNj zRc)-`Z#=y(ujqfE&;F`!WvI+e^%saLBYQpO7Len8lDt&L*}8r_c>gLYbp2@TckH=! zk^<>UYQ+&l?XRN&hl#XR&)1?FJ~xxcBesp_GXlt*MMZ^ug^>8ub&m;S%Aq*QHxi{a zo$0Orvj~H@I$g&Cm4h89K8%kN9&DfvCO)L}uaP;4yT_W1Q|Q<1;$t21(#+8+el8r_ z@IM8EpqvF{qK#bYuXS@6NgTt_m5IW3*+4b?wO9bVdT(WZv^D&lKFNtAxDWy&*|PZ< zZDg9JBH}pn0VN) z7T38~D#*U51M~zUo4(p#2n!Jfv|hwr$XykY&la!)X5M81hkss$=JX9wk!6|y_+oYY zyVXGfuquNQAZ05kru{D9TbZLJSPkfBZXR@N2iGg9 zu<_Aj81|f+pn?Crw}~BSnFh1z<<#ioWw&$>mI~aL?HjmXKbxL|7*ZVY)R=aGO_7)= zT2w`?9gn-s*%ao^r107^Sa%H6%gpdeQ}$tAu@Sks350JI$E z3r>;Zu-@n{dgbKp%x(%?49Im{Uz7w9viiMbC2t)VmcEft>IEBvyIS(Z`iu2V(fb_U z&%1^Ji3NGv%=XZ$A_y&VJc>-b4_os-m-MmiA&S6or04;eZ2ws|{RTnpILajy8#Z-3 zzr1Ja(~n%%$Nr<&=h7`nxqb=77dKJ=1#w+@O{4L!c;-cu|GNuda`}Sbfx^h5+`(3p ziLP4XO|f>8(ymwbiiAvHXrI5t?vN6!t5Vf$%)X>UM7|tL`2J5MZ7W%k!8j22a{xr5 zTA;jAwbMnBT_lU#ux>fSqYrP^;KT~+k5hrAA@`8n9$dXh4gM9FKxeg!D{|Oby{aOr zHS~mw7J22gb1!LAMZo5&4 zxZSg)k*>Q|+afgC`0U+Nt&CNpA-i_KK%i%*Ldj+t7`Xnf?aDZj?r}7nE4o)gW^fq& z(%_o9Sv2lA6^S~9?`Kbok)uCr)(2^2^_)(7eH~*xC%} z>F|s2{H0wMZ4QMt6$Z~I#>+o%8JUfTZ2PkSL=(sxL}EMuhmS@%Mq!JEs*di{%9JZ8 zBC>N8Xn^-JviHJ#V!HQff5;QYZAg8KdD{w!4up#tu(GbZJ!o`U$YPeFqlCEmJK>Z4 zdpC0FyK7!Vmg4eh;So7QhQU0iNEPN97ojN?r@!wp@|T5UC?ey;C%L&b#6I6)=$iD zGOlLhlf|~#2Rr(DhtxU%6K4 zK&ri+*l#Vw`O9F{R>e|Y1GPS=ZC{Q6Q5!{%chpMt}zMMV$he9qBJ&p z^+_Hc`eT`HtS&6yNsnyIm!A9LKEKR=f7PP&@1?zom%t4_WH3_rqZM+^gnB&qBbyvn z-idE~H^?j>?(@vEhvoDbeI+XOC-Fzd|bz7ZVXQ3a(78j zp!)7kvcZW$9%LPvG}qM%szVc`16bn36e)g>91+st=^vp>{mqQAFt(C^h>ts8z0Jxo zs!ja#W(%slT_}i*5np5W8^h{TscpHo+%B24CLeO1%^qTJc<_@Q9pahPI zN34eb*!ln+FVb|nI`;OAUHPFu)+mwx6u@8Gcw9j>sWzHZ=bYsBBJAc*cId^k+<2^* z4*Z~`qq~Xh)W-^N~WD6G=0hb>M_{%i4cJ{MV;E&_Kv zX&qK_u3e42OU2{I43rL0T2s+QSe4({M%8hXKmKX}>p)J^2AxMxnqn9jL-SDlD)43; z<*vtb-jsRaDeDtAZ>~+3xr1drBB7#K{in$Gr2gjj&fE9)TMydrK0jz*|B;KH->J?y zzIRs(WA6Pq<^*4g%qd3N``BFn`6#r#E9=mGj@=whUrBk5A38pG)zP*3B)#>UPLcSpJLwC8eCA zqyyUF+Ewighg6b$Ns6}9a~`mI#QQj+Zapk!(x9TGHAbl+If&YS@9wU9b&;P={D$1l zW_MGp_1|`J1ipIDaaw5h>s|k0x6y2swD33T=uS6M4Z04E6BrGiknr?L47$C!KYOt4 z!%X%mcufcf>jJ5|F`m zq28U?&HkQiO#M@1&fbo>!JZf`Jc|ZIk`uq*SDAyM_)5??b8F!wdr|~k#7X}@0P{c$ zzu#%#FCjdB()W44>UZB&;Rl3>ai8wC?~a>w*GtARe8xP{W&C?WoLM2!vzF%wTx_A| zUl>pZ9pjBkGJw$V;MHd4(=v~4)VB-SZSKTa7D4ugv_tD%nozQ<9v3WG*9auFl`8@b zf25SGGV?l=!hqF{*SLe{GR0_JRT!$Ku)fzRyedL${K6N1z9QF^h_6^;Z``Rjm5Lp2 z0kF^oG(AQc=PVdOK*veTb6kjaJh zBtT|-Z}8wNZw2Y`o2eaF#K|oj?^Chc-q=-!ZgEOi*&gd$KK)t&p0r~DB~w%H6w{t zJ;`I9(pglp9%ZkHM#g_Izz^Qd``>~uXT?Xl^3i(x$J80jVBDtuPh9C)aoZ53z;rQ& zkM2I`@rEusLq)Fg0^@3;x7MS9CMjDi4pWgMk{2I=ga1^8k0&U;&zxKNKB4d;B*ei- zWs6s`{0yo(69s@s#PgJRKXFtuy7vIOCH+f{s#)MxE1&)Ux(b z>oJQgXL?6~%A&LLx458u=kAQ;)E=8-W60hkhxTlId;P|CzWh>u+VElZKYikjoi5YZ zX9SYc#nac*%^B~E(vBpi$n-ThJUA0`*AtAKRA{CA!2$SS46*f(wa$+kXHQp`*Zay2Pmjl^<_ zS?tnNf6=YGy*={EurFVXtMN9HQ0lEa01Y<{)kaA`l%X7*y@Rc&xKf*wBAXYP1;zX< zj^=gnUd(%noBi-sAf8q#3hFKkS~0HRNtT;~Pr`y?He7;2&e3lHC=QofXDmzgUi=V`a^!W%KsHK70=<(Hx?cHAcA;W=TFqi`eCK zzs>=$^IQPJC*X)EeEI2>yZ^fTmtUzD^zi;W{K20W3~f_C7vo*`U6oqA2#A7K#BJm{g9D@0D@bT$b74wO4Et9v$fa9k%$gu^no-3FCD&k9hUw+7JVibr<|B*RtP zeFU6 zOdOS2VbiXa9oX_%UfCkv;+^<59IT2Jf02w#q!P`NJ-3^WhQ;v73+u4C#Xrgu8??sppSOAqh$$M1RYL7$EB zuKLbnk|q-4wG0Z^^*;;JmFeRBA>$l-_QW39Ac_Efef{-A+EKyab^*|cXdj)m0+FuA)QpNGGF#~&fD~=YWLfOlg4UEx zx5_H@0)&Q^90l@9-k?;@UZ{jQ$D|Vm)U9LCU^iZ);?pni;E6y?G9kVbV3$Ur*a>j> zEheNb92O@Pm4_Q<;Clr*+w?cN2lQ<*UromGF-j-FY)xf%Q@q=rTLanb>;HrX>LO2}K|7t*GF(Y7S%-UFgYD~Lyz~Ik8YeQM z&*i$eTUgw1I}vs^R4z~&yXCgAsy91d(Q}93G1u|aAk+a~f3J9<;x_(#fyu~?i-D__ zD;=_r@sCgO+%fRU0|f~50++)UlazhJfDwrT17JWtWTPz`{l48`imK$5*>aDE8!*RR z`pqq9bqXts{pdE1o&PKGVpLuDoxdyL2=7vV#>~U*f5wBygkwB@#FfsCoY|p!VXA`X zrY48(g|*bf!C&^L^461az%bz%oQ{cU0Y=TsO`jLM-njg7PM?1I*~3>q`pO601jKkH z(RX*0JRUvfHBx7Oc0YoW82^=#H!t0mqxMehe;c0sdEu`M2~u`ClL7Yf170WC%^2> zm|>>_@7MdS0;ptt55u?Q)hgXMj|&svbl`vpF7NrvR)6zN1LmWWZz<$k1;O_d&%K(b z)$y51z)W0bF}J`GJ7DE)gOR0HIXR>f1BAX_mW@{V>+tsWsVN`-XpOKRAG8^20J0lGyxt%Pa5oXG3Qm zN~#d51ruJDDqy~4;bH#F+4~^AZ1UFURFl_VwwH&E|H(IgT0j5$uulMpai8eBJ4qV5 z>r%rsB6ZxCUfsXe{m2HkJ5GdTd%S9Y^lKfa@w)%bJl%Fb1y&5}qwC2A4Hnt!mIEs- zhw7E?myg!F6yG_Qo(4h?hY6r(7uyCqi^xd@khO&s%)+MfONIO~q_sihChxDuFYV(7 z(X!;^wMF5IBIhsf+govq^(0OOz^2Vz1m97ptxO1Io-9bKc3=>+{1HnWBtQmt zkly-)0{k-nB)~^M_aldIdhKCMHSkxUKtrWx1rbFxq1m$6P7+VO9S zSjO-j|BUo@=#6;NZ5)f)#h;ncJzxhP{>%?O;OG+Cxft)R?-(N)$E6v^9nXo8u}<=sAAR#PKe`7&JMK$Y+n})= zWsY69|J7IZJCYM^?BQ#VUNLPGcnK-6C^|6wu5=q*(U(kIb16Vk6vj5 zD?Q`iA{lI{U+qgHvdQ=9;`q^G;xFibIsxYiWV*-o#ka6rnS{NaV7YtT_-IP0ynU+_ z>P;L?{0IeP$ zZD4QM?z`U+cW`jps%2Zllj!;7-~MEpGR($s0X7o{uVN zTnuR|N+3V{dH#!s<|l>G$Dl?wc&UfC{{Bma|B?Xz&Y$|;2OsCT81Jm_JU&_L8K*;M zjFM~W-j(qjd1dHi%@5ti|ICBxl^S#Am{dA>j#Q%RLc?b+MM)k!pF_J*m%bf**ZhY1 z>tldyhMnr++eof7<*t#!9KBM4I^nA~)Y#&S$F0W7u%*fELqTFvJ@4$2*{?Qh7e)ay zDN31z2ws~v?-5LBNF+BHN9)y7ednx4N7;GfS6n;+&V2_Gdj9C3bIK#ejaSkUz8&I# zWLOHu%D)}VchJUtp^CcQh1>Rmz9Xr1Ed_Mt-w`KoALQp^{e2ssvFrJw+xCq4=})fr z{fI1+#}i0>Z{EY*_XEt){hEi|WlQ;Vn&<6|mOJXlXFtH%%;^u!y!{^k(zN_rKl|as zPrv!I2flOvAtquY#(hfP?WHdvuQ9ysOUt<2uV&SFxpk@XJj8@TZh&${ z@e-lMLnC4y(KQVlH*WbfBgzCuxsNQ0*m8bh83*urHI2E1OAM-wvS;Ht4Ik&H)!XWt`dw8x1&EL5#TSI*@igAZD^u=L z|1=5hF#TS7wm-MAbL@XNp}TMTYKRw!>4=$W8TXR;qff-52VUg{i#w%(P5|}+p;TE7R{Ek9EM5atO;cqS z4}#X8lNXlomTEI^k!`Is-!I_ji)(j+vz*{#iiI=?#>WuD5ZCio_@#W1mOEW^U=d!`{Z}uMRUK9kH7ZG?>Z3M zKHW#3dAKSN=*sV7Yo2jW+Q$Fz7M?kD++Vk0zbM{|>YoYeM6}j<-e`DFjo)usHl9NrYr%nIFbCmr4|`yrF*O4BT~)4S%k2CjoH-=1$^11GGl=6F>JM=#V!T2{B`9l#7_3H@R4qHSmT6mx)HyM zLu-2-p6e=IXzN|-Ho)*w|9kC!9+C05S3kI)82=gw$2p~}%C@c&lRk{FCkV2d@j15c zS0A>M33Tovy`@i>J^R6JBHx9}2kGMve(ZnLkE1=Db|98N__05ErLE{I`s^+fUC&jjl*TiKv%xyH_^Ad|J{V!@-?QFZ~30CI_7Utf1dy_jd_tulbPH#8_uSDEr3zB zLj%+b!z!cASc4ay+x5nnjl>qYI-lY!GCYLDA9OCsO~->z8ML@j#I)GHD2Q@7$3-bP zMq6~fKq`+|<_92tRL(y#R2TkOj!ze$m{D3veTLuk zfkD<(9=h_^l-Jkwcw>kQ!r_c!XzN{isXvWG^Msn8KG@nC=lTJKQ zcbVc~JiCktJ$8vfGK3^ZL$|(Wi{th824;SFl4b7Jg>CQP@aA85I2+#`RQIflzx$bw z9lrfX|J>QK`w$WfG40cR^p%HhmZz+n9iXKOavR<|ev6+0Ps+eQ^5Jd)P{q)?hME^{ zsePbUpRnYWpypLpy6HVnU*Oi?916$|K~357zf_aef;5yIi$yWGVvo#PIka#QmGxrB zVlzSW4SWs*+2D#Cll8_4hu(Miy087} z!~5R*yq|ZY-O_UjygZcVOZ&E8-Tots{^{|mzPq5kCFN86Q|nz0RlTUwW4+$}74JU0 z=PO<{PaIBn(Eq!rPO}=cTp;=@vvr2e-tM2h&TyP4JI*~^$6oPEA6weYC(_ITmuAkw zKjR>*IV~UCyCKiMPxbl8v%{lr^fk=gIDLR#{?HG7`{5sd->*D;==BfYYjhv}^hXcG zvQP98eRbzaE?Ki#dcAfl0HT;7d%JLIIMlY{p`(4XDf>iOCAaR_(3zjAZb9~9xr^7CJK z)32ZW)MpO=-$(OE@GtoJ?>+pAU-}CV-}s9@kl$iZ)NM7_U$x)rKYaUdMvC;07L`%? z*e~sC+0tK&FZ7R^^%&{dBPx2mp*r#U+-E+2_$kHl>Z?x zVRuLAjX(A5@apf8_qiarBexIA%@^K!C>aK8*rg7EhKi4G!tr1(!Ys=own<@=b-KbA;Nsp0s$NY|@#jgk>s?Cy z;h`k=*-#67Xz983O+Oz?P7zOYN5pdV-FPZ5Wi$N37v4C0;^Ut>yz5<$4`2QMuk@XP zmfK!~toD0~US%s^**?CW5cY(?ei+1)VwW+@xU=VDv3#`!8JEZQOaqT%@yIeKEDn|z z(?+~axO#v2^sOI$>+tliJUcx3jbGm13-Gbe{pjJ}`8&Vw@JGJ+_a0t*{JcAA?|h$p z=IQ6``-eaOV^_fG8~W%=CV3Jt#;=Ll<6mXZ8|!OX`BEBU<@f$5t8CR>ZO-xMWVdW| zKENBNbzEA|(sO|5lAv$nW$U?O#|=M5g3ODR@JuB`h`bBkdL?d|b-5s^jRu~d6A*P= z39oCF?-a232^)COO~yBO7-$#Ux_N?&lCsyPmO;YgH=Z6o{^OrIeDV{zL!&Xcmep)uKjf8#PmWmm1Ecgj-l@JZmA!dQH@^2jJ8(6AR|#ME#P>eYC-lv|r5W=cm*v@H;GrvjT{*#{cU{7cRQngDYYgJE z--Mbs^!TWs($zOI(keR_%g!JAc>|4ta{(-}TQ%6=SobWiAT5Uyv0{d*jx0*Po+lLg zNaCo{09MGtq#Ok-QY>H$u`WFZyKYa!u;%_2wx+DB*< zW9Ws^jjaW8=n=HwlDI5%<#9u@>!B;}!lnFYe&#cW|K`8@%ZLB>`|tQs+6Hom^uvGe z#}9w@FMiMAv!8q8#`f8VwYK5PS8tWU(^IAGwbI(Gv}MqhUv*XNp^?k7t#wJbJ4xW#>{xF}nG)4tLWSpCy&N(W=pJ)0l@gwQZbccO|* z9Wt1t!Cqrr`IcXywKYg^asn_FGQRl6zSnm_g($W1WA7luL(OZsDV4I-OEEzJ^=|@b zL1B^V`{3E&RqhoBr{d>oX2Vwu@H}AF@A9`Mn9|ij|M;+A!NR8?6A5tQ#+@Uv zb=)S%m7eKr#SuxEg_6D?26WNDS17y}&Y{tZY>OX@7+|#V$MRPWKlRg}KK!x&;jbQk z;uAk>oiB&OC-gAmpZ!ZY32<{>Q|XpL&%H3K&dOJp=>!j*skPDD=-0M*s||cH-+1HA z!%ygL04D(*2krM#+s;^6`#ZN9^L84+V*~H{ktYmd^a-{Va#D^ArF%+z!`v7CsD4A_3-=hrJ=J~hmRs?^^Lzfo z!=LyE|JzIXh#(*J0~;UbZu`>*?u)zn*1vrNH19FVxLjV8k#8zLI$rC=L(NOC)F#bT z0Xz7y_Z$=NIf#szHZ(ja$YKJM;t>?p2hc-%6eh-WB#Y(tGH zm5oMSQ-agljC=NYp4>McHIuUOY$W-1LSPfj^wQiwRb|WwzBZT>3fBW{#bdct?=!vi z5nXkD+vOyHKHy=z-~ZA7{_wB8%{jwmwcnD0e_YeHxm(AaOpuV{G#MmZ?p&rT} zs|?w!$EV|;i4lF%7(~XLtIg2Pn^Le?2TILLQmO(KkqhmuOR1A|^kYCItHuz%K%+$ENfs}~$RitjltveJ!yWt`oNSK^5I8hfeXy*6 zsvj$$z=<+nsn4eFs~HKYs#S2He{M}~SS)H_V>|&^*NFLH6z)KPkY7rRz85uEu67(r ze5}>OuT*5oktAw1`POgkWY;gs4QFezB}8ldumAgZeOX=kPqS==SxTjCU!1_IGsk`G=n#Uj1E<53juT5F)zE+F$j>XYzmd_y3oNU-sGu55Mhee$(N% zeBjp|zVgZIR~4Tl`I$FAefU2<@z)Q0z3;hi_^T9^JG+<)M4`ohDhh)c(qbMqCam=w=;6^1I?RT$NBM4^={+^~TivEF4qL6?`hFCDXOl;^nsmT4`P3 z0|1=L+ZfQe60*Uq<&y`iahUN08mLo`~DCA$d|=?{?jnmlJD0u0enR8>%R7@XdS)UhR3!)?SmuKzCTiO z)XoI!s89b@c0@UHr{4Eyu}50Ji8Ny|+HV~`{prsgUeiYe->vTqcwYZkKefzw;cDFV zH&w}GJ7#E_%CIL6?t=MYJ(Y1PkeCJrC1GWJT3b2-l>iyK<~iZEY;-uOR>D1Z->Q%7 zz7ux>zVKhYd3e|Fcyf634MccJY~26(4}RO>_kHBQJ^ZToe(3NmU-iw0554w_4ZJ!@|$Q-RZ*wB+Bi1`Nc)!#=UX1l-LRO5)a^%aiqj1%NkaV|8O3eQ8_Y$FUum z@#LpI$U7K(<4^xQ(w8lMa2!{#QF)|*$RVa7SbOvy21tnsDD#o|C>d$AY|C8)no{Q$ zj>k{^ODES#X=3z`cOQsbKDe6-(k#G4lP(E5rE9vnz)vu;BDF2H_LdyXXHVbqNBvf; zFK7O5eCH1y{`r6QSNnSc)^@nD{ii+YBhp^Keb;hyR^EMW+x9Ow@aOTcU(;3m*nR3# zKXdq+ul>Na{eKceU9Z5yd;BPtjvtE`{cWBSvt1{mZrV%+*T(bU6@3aP_s+nvTt>_Z zCYoas1~&-!0x#wRZv%Kj?HxORe+NGk@cZ99Jo&AU4v&9>RutdGyT5oIif1Sgf$MeL>u>+y&&iMa zrPeQg!B9t@w(BMzFKyeS4y}A$s_k*gPTyZ+aO}tP3c)*K z{QvB|Y0z%jb=P<9)O~wYx4NaatqIvyTP@qz@+8b)z!2FEgDEhT3K*L)F<>B2fm9_$ zC`jM~A5wfsfJ&UIP!JNQDse_ECl16ESjM)%*dWQ4v8I+bi42E{D1$o{^#uH zJkN9A_q{{+ZS}L?_nfu%+H2nH>@z%5)zUQa5y5Bm#X+~;s?TiinE%*6X*}x%&9$s> z$%1Q9h3a!Q7qZ;>`_VlR|HWkoxv;Jr`9`0+e~sglA;*e`iyys^mZ}E70h&%oFaF4- z>Eh44G@bjN)35n&47}De!Z-Z!B|kUp=lr`&bIcJx^-Zn1&LOCin$P%5tKnIO@hryiLyMO$qkkG;(8t%HGyE$EUu-gj*eJd${60xSTMLr3o7Yw=FELX5ixc#>TD%j)WZhlB1wj-k8~v*QYvIBzCrqo9zv^@jRbI9} zgyHj7`$na-^L+#AiVq?f)+;(5fu6BqhTxMujiL)Z=5D8g18JqDApJ!%z3uUZ(-xZN zC4a-8JBQtzfe(JQPF71>@T2%^d^sQ&9fhGcJWzrIX(yTgC!c)U z-xjz?23!0s%CbLFzv|LHb$udPK66a)4VOKxCrpKjTS{$FV~CZFiik@Ajt@iWqW-y7 z@C7Zr9k`#2HIB_2sd0k3@HZ|_rytN42EA7o0&n+Yyc@~ISCudPun*tu|B8IQ?@2PH zdd#^g=QHY?G6l|Uz~Q4Ce)s0{1F{?o962fc#RmGUUqjJvTj*nOS6eacl$S%1uL}tI z5Ce=DFAh#y(Sf<+#P9b2!~pstR0<$KD-I_|1fW}tw?j^l@O=rIy=(sY*);VS zEqoR|r$4?1YX-wz5jsAJQ|o1|aT7&2C_p&PVu!n*LGi_@tuyEvWs zX1z4<^?H7Ix07*$dVJ%!_0pqy)csr)p>8Yi*t{B);aU({ zqce=s>X)%Paf|9?T6-IW7URZOyLk;^rIE@kPdpkx#c3@z*spi%qe_dH1}78G>eAQ{qNlmSpSA-quol_3~);Kh6nVz2XnXu+Z z$K~3}ozgQHc=ovq)9tq@o?-rHr=)o6k^aiqwti9KWXtD`KotweNj(XmZ?l5bv^Qk1 zA)e}Y<@WK1j46V&EJ$?$-8Ec8H|(Ah*o*(0?qao_x>sw)J9HuNCdKkbMRSif%IjwJ z`}0vZY#`Uzv6o)dq&~0teNNjWn&r=G`;^wgk7(_^LEhYV+>oins^bNgECtL+7w~+v zSM75C@o0u!@C!fnPW{hy?_&*PgS>F`&^RZ70_Qg3GqI49+K(?l+LnJJ+q2oGkX8J; z04V3nh0T2_EX9nVCp2?LaYLs{cr+IOm~Ild=tIYxOe<7h!VU~a`*X(g0wZDnf*?ww z+}7xn919nZ@I47qfj+)pK){P1v%$ie#T7ZfL|}#V)TE9(oHM=yQXl>@;oL>=#9{Jcb@C`_dFK>dZIDAgECs6(Roy=tCsd1@Vn#J3Eyw- zlVf7wqk;a;O{w)sUSup(9pir8=@W)8s6l9m3%I7KU;fd@m@c3V!Cys<6Whhl`?d)^yVGHlH+pyl~o9pHx!9^ip);=5?l7o?|BsU#QKhy|BL#DK&F12|Hb$=Rto~Pk7E2O9O!0$h1amh z=hyvxb&vr|m%NysJXO2F!++B8907rNP{+n%bMd2&``CmYe6}w|NapZ%R2;{St|UhNi6Zt(U+IOr z+Tshceh)x~N?AD;=FvcC;V<{-mD0Gjm{`WZ(3K$R(_*uFwa)@aBAof&xgnF~AV`-slB29xij=8I`b)yz7_xeDI!D1E5~!}cRG z=;ud|;@fO*K*AgO@(u>POdIjg3OarX*9pDr&k8#}En{B^`cXZf?(s{Xi- zWMCoz(x8V%e_Q{W1Fp@cPXW{d0XfPx4di-chXFB;7bL;wHD*g8i-(B4>aE>N0KW5( zKVG;Ty)Og&sPWP%y`xuuUWjygaEdn}`DsBXM7Td5C^I}A81L|P`(l4Fe@ZHOlVE){ zs|fh&Hv@`C0WeLD9A13Uc1c2B?1ZO{eRvYG$05)fCiF!n+(6o?=>@$p?9g^+pmJ05 z1BcIFN?n&zotTvC4w`UBF|^O0EyG}95g*n$nMWwC{N-uKPFzp2m0VsWh@ zvDP?siG_j{cVfU|@c2aBu4D-O8}wbhI?0E%d#C4Tg1ktOoDr2Tw4wWnfb`%78=of7 zF}AD_cyVZl47{=#pdMZKseg)}2FM`>x?T@9hnsb&?)i8c^z|Vr$v!S*!!#4L8|<-A z9q0VF9D68P+87hmwLeFV?~4Dd9gGcD2o^^m2HN>KATgJ745;bgcZ7yu8Xp&gW{VAT zlLQM_07xa^bpVL_j^BRNf@S{hmp=4}YFi$tetA=t6pusFa29`j3e8OPdDCE4J7ntG zVxG_}z|oP5Cr_%BSl(#nm+V)}(I<>vj4>cHhaE=WMGc{&?VmD8ZmliXs*7Zi){yAn| z@!}C2FOgY~{NebvJqV4*o^>B~hYZ}P8K}c#IjrzAH1HfeK)QWkyi(`CI%=2mqn)~% zU1ANCF24kj!<$vXc#h~*LLi5PAHh3cUL05{SR~xMNWfTKl5O60V12^hT2Lj1M{Y76 zzq0`H`n;3G42jKm{9>*cwAbRr#|V+ccQ4%Hs|iIf9RU;@*6W^m$-qwx`rCsTK68c@ zQLJ>->SWY<)nEHRC^omr0tX*>vKXc zdpbbWY7Z0x2Ti~E_7WEYI&dJ-eEb&)XNpxsUM3skDDK1lkbzew1Jt7HQDyFP(NO;` z{aOy@nw@KxT8Du`r?HT@=u1J+J;SrDYjytfF=ijy9Xl_#8hSktzHdE%?Wp+mCct44 zim|MqRp&2r)Ou;)ZsEtQ{4Ur2iI&4H#{@r8?t?cXZeGfG~>43u@ z7wiiF%ZiArzCOV>#a5pHfT+Ho00?*d&FuKCe0~E|oH1M2QDGbpsyTLOv-8mLtXiEw zhWtq3?YG^0xBy6kR`9l4Z!#tMOzrMF06ZU(SmvYWXr(vZTo?pGAMs`GY7Vb<{<)x` z&BuonrY1b#-BK@!JMcL1J7OikywZ=hyu05$U2L$Rfpc7N?ctvX#xE{V`ZKYy$x{K- zEFm19?Pu+c9(IQe+yEKyu|!={;AsQunpgGCwao#|IFJ^+Wy~jh<`3TEvy2gmb50rT z2ao(3A{JV!*Z`QpvzYU-hYy3l#+zYn-^wCvUA#n&O0-6EpcuWdHUvj@fhA*okzc{+ z29Ee_Pb!Q9)iP1Q;5AZv)*|#=(keyFUaYWbC)9_oE*m>85O~)zki z#)8K8@7Lr1nzUNhxZ{UICZxNxm$^0>)8;A8)`^TZXuw_k4nkuUj5ziG`nzw1a%ej; zaMzu;%z0$EX0+glM|7l(c*e&$8S#v-;m9-M;XCR4pUIa1xtMGIs}`hDUE655(;jlL zQriP=BJ|D>ckn(2dVufz=dPUth&u$12_IV+;h@sLWkttA9rK*|VWd|y4>xGFb!}cEHUi zoR3p8(xNjCttDm%7kq(*m#oPiDJ=RBe#@Kg$?yTLGH~A;?&zG>K;^u0KjIM^X(OKT zaZW}&wD#L>b~^U!0=snZZJTp4Y@nHEV+Z7rfg3CXDN!IB$4}sh z_jnyEV(H`0rvu~AN!Ya za*SKtrP*V3Fkb=(y^fMSc!6P8(yK^sGC{sUoD3bAOnZ5F5vRIgW z_s&F_U(mBeFHPx^oImn}4S2P&2$oC87Joen5Z*qP9Sapl?4TNQ!xnEs*+m7dRbkjT z(3{5Y|B5%7=djz(z?=P(z)SwfbRt%|McwL_@wBY4Edmk=Vq@4&G ztdnb^@IyOf;0DRScu_p!d);%LYZQ2#jhgpyAXo>@2J|7JU4}qHGZNX@VlcD_jWA=) z@8eszk6+dh+-L|s#ysMTrTKQclW+mtEvxv%=Xy zOyMkW#ODCuCm5i%_*k|WD;rUOjyK$U$HblPp>1a1jrZK{Z_R_sEU=NUKyX@M^)A0h zmUgB3VhcFpR=Q(1+B2qS&)xUN(8qG6`42Dr-Z@-?Fd1XIs^mX?>-_@mAb?T~>RAp@>o)#Lz;cj?Xt z7(|z~ple#r&#(hQN1BzWW1c}49ol>dG&By2^&`_t0=qkc974bmA9*f|ul#dYySSS} zcZ|PZ0*J*UJRZFQM{^*C@UCIS*))&9Yled0Fa-*)6J$Z-R#%#k+-QA5M*+*A-GBj~ zpg^#_S|nNUe5w}B?`p=LIJG{SzZCeHg8jh07gmPFgBJ79wES<)bDmUPWH~GY1)G^}w6pcJ1zK`GKs6pTz zcj&zy652KY)PZY3e1W@kj>kyVpZHyS7PJV*0lxVg{6M4Q*Ww;g|j~2 zQ9y&eYTv!0@hO4mF_e^#1rHx~hYY;38Q=h`!X2P!uK$L{#TW=boj?zWG(({tYY6T& zzM*hbK?54sKF}0QKmoEb7$`i)F3>nE*x1`L2Gkbcf!;)(VX=(%7^^1$F#t2oLc%c) z4)mt688#obXa0c}dSLVfqK6g;&RF1x7gelE3=3BME0%f%15IAXtg>}*fgqyQvjexF zPV9)yroxX}@LDjfle-%am!4h|o2#5^>FTWa_D4jZHcw_l;}QWz1u5`h11I7>?sOb* zOysx{Pkm2E$yxZYJ7nOM&H(l5O}iNip~xv_7kfJio|c0K$Y$P#;Il3XfSae`h?{TM zxCWxv2Nil??#37mIO0X!v2XqaWL5YpR&UzPPzdafNw7ur1OTRN&0wLDML~Bp*fe9i zc5u>;sbJ;*>|Fe@s_3+0V?{Z=xD`_oMBvg7)?Kk^n zL6ffH={;w8o%P4Xk-I14+^mMa2#wn~4xlNpwVGCvHEtYb+N`u;3<#dlLj2y}^sZUz zwT}FnZ}?Tp35mV@37V|ZI=jY-woq!+Y#TFFd`rE|M@UVm1*j+*aKugQk-~S|i|xDa zx~dy^jqpj76`SuIKyoP4kI8O3Ed_SrGU1 zhowQLn+T3ku ze5C-lXziYDgU2_zza(=2ypV}&fi}G#I3v+Gy{FfiTu;a)1PDJ z1dkd4ma6$AZq?eMy>9P?v9L+bwqQV}#(|XwWUNfETPCNIX=-TdSY5-yxe$Si-L5TT z)AUvE_|oaC@85px4aaM#-T&opoWAT!?o{TP31*<+waGzr0hg0&W(FIcuD!-K<%=z} zMRw75YPwB7|9kU!{od>m^N+6W1Ad{Etz+y}kCij|m0(nabsTfNqN6-?ZE`$`;!PCI zng9Sm07*naR6|2#aCqy>u{i!ASV$M)t$Dm~(D0!hGVqFKzy+)wwMv0kd6!Gzb4#Zg z+|@2!ik*MgKg3t|{8tz9tW)*J!DyQOlz)!3Vd4(NeOh35&XTK{1~WZF!mTkNjYP00 z)#9k^u3P5Q> z21ns*jeH>r*9oVsG_i`e`wh(FWwGx;Y+Qpfg$vc;&3dWjm}PTn7G*@BF&y zv!8!*`rH?uz6{qF@6bJW-!{GH8{RqxNU<|_E_zXeXFg}f4P;Ds@W9o4xh~2^RD`qo zoBS@OdCuY39525XeK(yuJ8}14a;5R;{o=uu#GiUY3mYEJ?*~xN+Qn3S#XtP5a826yRdn*!e@?#B^Dk;T=!l7?z2;a z$1P*lBg%ZjnK?>nvu}Z2_C-q=%lWS*;SVv&Vw-*h8v@qx`;_Jg;$tAp9U9QMlm|Ll z7#p`Z17yR4K;f2cIO64LdpUcJXgLEkZgrw5vHoHn#_htg$XW~Z{+eLylR{4P$azug zc$gpk*M}jE(63iX!^d#>Dh4ZuVrAVI-d!W+nF-B{%*oq1y-WP}eb0L)UITxvwL2B_ zd%x}e>1==fGV$k70FL{neMsw z&N>{{YLD3;V;=GS;)+>EI4^Sfq^~@198k|bo`#gn?~4%)p0WOYyphB4_~$y?eV{C` z4jkGc12;kjsOw($136wG2ZBQgETm`*(Q4fV3WWe8d5UVl+1nwEd#___M*~ zoM>K*n%9U_*gM0`^WVUR>a^yMeXkLs0TXwiHEXA5=yg)81GsQ#rrViC!AL6{%Z#_% zN{IbQVi*Qbt9TIc<`LnuyvPIhBHQ|`T{tfT`1{8j$Ag-nQVkz31K2aIW?c2ONyE70 zH#BK1Z))P@|BD_Jw-Q&s{>P%pdkt9R5=cXo(xRMkaA0@?McSZ+9_aDzV5p7VdHc=N zAN+TJ?evD%bLaTlXx#b#w%`1~bpE{FC^zP39e&1uBObm?*pScUH7px&)G^{4Ohf0G z^Ut0C8{YUjzqiBE*UaB`a$kI+^SZyw(~CnFr^FE9eKh3FIK1leSii#3(Zhgx98X+C zFk*-dI5Nh~HYDbN9y0I>W`MF)VY-eRrltd3*Ay@HEjZVD(^B_=RDW<_;R48O&xXxF zaJN7@Wjx}=E7~kCmX@;Uh7Rl2Xv4N7{y`$PfZA-XVVPboY3#SgcoDtB0V^Ch;s{%T zc7Y(lfyT8D3}u1%RL(95UuYF^NtaBtE-<=7fsa3bq)Do4ks^+4H97MYk?hy&{DMx5 zq)RD|Xv_(z=ov?+KI?3IV)VDAm7}#6j~kE;#=z(egfTd9#2KT-8Q_3(H~dGw_t(EB z-V<>Dm%m~9w||RH{=7@xIVo^O`!UDhOjlq}ooP|N=7%0yct$wt7V{zsE1!eysQR0z6Vn;a9=|E1-3Fx1B-Kd}_7n|o+=vCSA8tA+mO z*f{n|uV~2yC zK-Hs;QNCmtF8NHH7Ao(xQI@@V(S?B>&H6-d9V0s*9sAwC{oT_8A9-l{@t^qc z^w<;6?NIRQCGj%Hula_zO<#83>*o09$=Q=G=ZUf8JowASznU*+zkT6{et5nA=JWM+ zzaASWbk|g^^sU)f!-`v!FrhCvaBeLnl3xbUn2#XIay*MNrw;l zENbCDz(WRZgbcWDRiADW$a zcmtE{z4={bDtv)See|v2G4$W0Z}_|Y_Vd#%{^ddmj0c>%S2zFT99?BxQx6xWJ4HZR zMMR~fn}L*yf=!3CAT1rELy&<;BLnH~6c{~_&e5YgHb!hPws-&c>+Ww~&b=p}=RAkJ z)-f^R9q=}d4~>?b^w5i-?Y#vFu4aZ?2%3+9pSi_&Q+Z=EJ~;$-b#_TVFZTGa(%6?& ztq@CY=h5vCzFv@HQ({VI9`mJ6s~MlF$)DHX$x!? zBZo3qKI_rcMc(5IFdM7i{RPqKZVT=z)7<-$1E#Wni(>y({d^BD^;7V()ESKL>U?xc zIQY-bGvX0UjD?<-;yqC*`wta@!=_cgH2XhIl){%1YsCt?QX50O1+RKoqO1EycYnCS z(dRBGS&E=zT0LFJ*%0`*3Z_K6NKEPC>}-09y^!6kZFlaz7k}rJ-^(6TalqbC)zz~1 z$m^6F*1&9G4#?(p@3-3No~l zVi*O%Ss(ke#>Eunn%$AXq8;r;`Lz-K2esUs`?JJVtj?KA8Iv`vRth0+D3$DU0$E5C z&IbgYA$ujd7uHEdM0K3EUUBx(E_=VjYGxMIeLL61gtZ$w(?STu~>;G))n|LG= zXBi*lnzPu(@@|vduYV}~LL}t7_xR_I!)J`Y=4IP(@q6?qdvJ!}c%hzVWOEZINQ{-^ zj}YFnX-#?uROOE~PKO`!3IKc2(jZF4B>z0miDp@dHoA$`^&2JcJ$$G`s#P~M%G&)I zROo|2)-&&(p^Maw7hF(n>(%&IDtJH!7!o|x`ls$AU*|nim@=u($kvalzpq41KH?GK zDH)U*D^T6vf++>9TGwcfO_6WireBSPf2&55@=75d0liyp8TO;YXZAdHMlqciU9UUm zZoe82q0+eX|L#Ffsz=xIXUS&}r87dJ&}mwJeRlpdFDSs{*4Y*S_X_B^=H%bT9vt!S z&A-}&dyJW>iq}K;Q;`BeZ8axHR66|c^Z?#&19~Dqx7wvs-MU0eQrk7$Vd2j97L zv7(oXIcjT_uUYO%rm=B14>y1W_*`gzAdA3TT#^U6eAEFJv!+7#(MJ_OH|A8cTB1Oo z+LqtH@X;`}<|r$27_aHQw(`M!s<3>0Ld3|?jN`cD7;lvS5XNPuszb}$QHm+O*`?KZ zD8&ENlh2-qTbt839@?E)fN6HL&t6q^n(#Eaxo84GCoX&IA|+~=@rJ*z$5!%}J;s#B z<|0`%6KHr2&3InTE&X74x-JWo&XP|&NwKD|6@OuAYb?St*co`0^y+|B5{vo``Gyn+ z7#)Rle}j!$eNmyI(uj^F9~x2NpV0Xo(L&TLwd`pu^&&cXip`4nK$*8u6)oChE@+-& z|7|>UjOG<8!3(;c%J_3f`6Ktp*K}IuovN-P3J1SMs#*pHHp*8j4k^DwC=ie1t+K zKU^7E#d&(i0$J}Op%gO6@~gOC;EQFr-@BU5s8}-M`SWs;_e0U!PH_=TZD>%6SjU`F zuU@=M7N+Qv|32;LRK*jQC8*DS50r}i_l$ejsu`=^M;-QrGEF(ex({(D`T2hxjfAZQ zHu|EVQgf8OUKLh$W!T_19&B1FgP8Hc8H4#)yjK-)9)4q!dzqfBozG5QzexKm_~6-t zKEZIr*&kvNk;ve$DNgpHkExgbHU2_xE{@CjS==sXja5_HV+-I-_39u_@TvEkPw`S+ z8649&Pi^kw3$PmosTvD-%bJmh^Q*9_0a#Uw8Yuz5T=}N16h%vF`s2-iIC_<@j!*u* zz`*=0{dGZio=Lh~uLi{V2+^FFVUoM@(0ZI>FEIMY!!^g}z&PJt$n$mlCO>=WcCn39 z_`qAWz_2kf!utPsiRaXs2(x;Tmg3!=3)A!K?g+o{&-X{OR?x1@`Ts@QR$Ghm7TPg@ zzVyqA_f0oZq^n;J&La}n;pKRh040i!GTa2_B^b{QV*88UX%=Gv=;8IiivF|li@xHN zFLeq^bJ@v$dFQOrhQmb?bX{{~z{6ajR`*CCy|Z; ze`7v1PqosxhGJibyq?k|4XUQnqV&t}mBIEss@T`(4N_BD-UM^GBBWV5(>5N7mXT^Y zl{|#^v2=8@wJ6Eu-t1Cvx-9Cxho=7e>=O&jnXh445d5@o50`3(16&};w=(?nCrwKO z+e#YKjUCK1U9zXJ(QK5|?Ee%PO!(uB)2u$KFUV<(r*Ehrx;@71>leNR9wGnx6x{za z1-qyc!4whi%Wkwq!u}R^8Afx)`}7|qOw{D4n&7#0x+^>q=ph^D$jL-XO3NZAD4$dq z>5X7EM`IjpwI4JMKubYIjBr|foi4Le_&V=p&z-Xxy7HYZyd8RN=3C6i=l zd7y)L<_omPvm@l_kR07d0k`WB4m)k>MK|;-{qdYlt=r`Ab?>EJ(VZb0&8B*0^PRVBqch^M`8o zGb9|w4%?Gw-L&k59y`vpbTZhFJt7`NQFJC%bZ;EZuC0SPv&`cyovhz-&Q47Wbu`bG zta%Ne3oEsD)8lJkc(QQ#@|7 zQn+X?W}KF>D|cUjHdt+^FX__Wmeg;lB`cp@|sc6M2IQyQ=*!3%TWu;wb@` z@*%aIz|`=a*B*xOU44%Mo=0V!qt(o_dgj{o*y%ibiS zR<(}%zZ|VZI&H)$opx%?LwV`Pkl(C1a1}B91}E8}X3P-%>0$02)vo9%KBG_js*zet z9WiXI=;T<6vWf0@D(u<>xpGQ;<5`S_QH)C;5HxyIbkcEMxgMIHT>g1Yfjq<3(k51g zW71ZeGsWXIOYHfy?)4S_qw0Dx5^EiV>Y`Hy#oMpHCF^w#&*0dJHJD`JW%o7l%lhz| zWeK@^zuf*#`_aI@?=|7S4BX$RU29zn1N#K|?Wea5)oWOh&Nfr86n>|^A*JVDmOQhV zu*jQlisV1;GW|RI_GRY1dLC@JE-5FwOOEHbS_en@OS$pQenzB_rn=})zMo{-;pAiJ z^DMmJIYlQUaGe}eV5I|yJ^DDV`Jj#Gv(a*~(@WkclYPW=67lt}-e7bj5m}DRH_A`| z>Wt+RG+vPSzt(IhF{EZT`y+kp_D4{bW9PwPUh7A$F~I%@QU{cL(dV^5-cz-l5dM8W z9+ih=6;)mfFus1R>upx23w^J{s@0h+ylr5uZJ__5$1xyq?7)+_dM7xgo|R}`tEdyk z<@Y_R7=1Z9-aPoHAn}orAe~(o3z2$ZNU?ygD(=>jN)UW6)qYP6u!5LWy^e$gY0!)q zFK&(mTkcqf%2I5*Jwy}%-c@;5E;SX5@Ah7z?aJO|K-|@|{)tSfhf$h_th&!g^~jKA zH#S0f!VBBOvt|rixrNQ_*tlZOBr_U)c3jLzI?$&d&etD1hhRl1> z3mq7|9Rz)L_Gg8DgwY21EjG2~$;h2j-vQ*NY{x!rWhu`0-K>Y83Jl16HCO3r5qLNS z+#ZDfdwev;D@wF&!kuuC`JeH49jc#n#FsIH+t-3~Wre z_eup*GR#L134v!ud_9J$PVc3v66ZchR$x5ZX6H|KQSdvc=%&^f4+xlO)55lsINXYS z;}y4wI*B-ANDZ_4YV-*gbR`XJw!F@#7JQ;(AHWDo3W+kvOa)dTrD(BH zmaZ=T<|gfAK3NkFx$PA)?+i^GkHhKjSSo{2(Mb)Wod>@My}SYBBTMrgi4c>?zWw_MZiRS+ET%RQE5~IRbszqU3z#c=INAW;e8{pq3UYey%B-KtB^AGbKetKUDvn z$6)2K&PAc)41NzcLR%V$V!R3D^Lgf3@z=M~TK`ADsha`dUf%X#4cezi7)q;0Cy|I4 z9Z4!%QCj;ErKXVWw?1)JnCZ+LwDY-;H$k1E(v+f~)8puTLG$$#$+F>06;#9HrU#B* zxiTAc*{v6UpF-}OPYrn4R$#|OIOnkW=S%pfH4&ysCRFqojXuMY|H(nol#4Gab!3C< zrS~0!L1WSS9}K;8WZ$Q89P!OI^AWPAj&i4QK4>k@eIvec{WpP^l2G~K`lLfQYMOPv zSUwo3C#T3|4iJZoox1jnB9Es=g_c=m93{7XnXS)?OPp`H2Mb!&|X7|py?!{E~|`?c|DeSOZFclm78s5&1cr?FD$lw~q2Wa4GG~uN2 zWXS3a$zLIa9)RF^^H8(fEkKpe2pj|He@sZytljNVK{5JM6H2Nu+=8=lSe&FZt0dcH+KI`A<@ zfC7gLzA3gxrfSAZEhJDar({T0LL!MCZ8q|CdM&xgMIsrc-_$Bygg+sr#4?-1RnoU~PP#<{2h{}>JaqR%!QXJe%^T3=d(pi&-5=MLo2DVsf7dJY zv(Od_NqHRKmGwR)D!C!lguood7*anLpVJua!dS6cZySXD#UX;h=>Y=)8g)!2b`S#L zOIifM*k{QE4I@v^-33!v8Bea)r4%0{r(&f_*(8J9I_*7DnY4ym_9hY`b>s3He<&!z@44WM71GY{dr%@C8QS8N7f07(dz!H8Gl|Zf=AoY^ zY{PjLKB?aj3oOpnk`k(u`cf8aS-rW;OHVr0+?!;?%ZG@RN^cYyBbNej#cLvMhBoVVqGLr#)Gssy4&3ZY{ilGfL6f>!CLmV2pgJXulwzS6KV z-i#*UiB326=~aGY`kFM}htc1cm4O?{yF!r{!^~(GK=`R&Nba0TMcR#!`f%8hDX!!8 zB62B@r(62W*NNaWGeX#?eAAePT))&$se&M-;rOknwTF% z7hEdYw|oRD(7m`NTCvyoBM@L+b%JX-gsz&3(oZd=L-qP@MTia0@g~vV2-5j+0>6U8 zyeHAFw@1*%hE%Zvz!Y?y-Ro-30Y*1{#r`t`WPgln}iiVNYCpV_U5N`*)DGJf3lAhS!>$ zo^j1VS~&X4S6SDATv(ZGlH!_2WNf3g#1;G()vl}2c1x3^KWl!JWoJDniQNdl-W3I| zb5$b*BE53X#jvB90f--Z&m2Xg9%RXCJMAY0z#k{liv6_2@!=-&rt&gBwijf>8QrJCev!)H5BKd1|_daRIU8*Hdl71svsyOsBa;x6VZ=*~%<6QA_^`@Ct z5$41F1tL$~C`a9fC_3uqKdBQ~3f^=Y)}0a|QE0u_AZ1Utt7`zVc&GZ-matG)K%qHi zYb0=5e6uEouEZ8%vG%j3adzV|GXuAqj`m(jOqC@MoYr-yq6sw1fWAkn|2w3nu(*Mi zeP5^QU5|}L$qW7FzYE0B+>3{`o!wRCxSdr4tbHf5*gor_b`d+A4VY@x{LAH3e;6(C z+Qr}TH!6@A!D^2CgsjuJ`^L4+fAOS5pLl5cdqQ$XM;GP zejTs?e;pM21*(|ca(-#547&7`SZgPqh2D?hBgwLRS@>cgI;ADx3KKg8znbu)xf>tR zv^5OFt|pLny=9Z*%KY(TR;YdJRuY zVB2Uswg+78EbLz7b!@U+F#)J2Bs&7lZqnxxkBI`zHw^jB&?eCfOP_>wn;r%8uY@}q z;{P6E?$rBMyWg+oF5LhuQ&i{72+_mTKI;uGC z-*gw_9=PoU?nyH+dPqqKYtV+i=-`y+o!=?QHb$uoR(!gK9SaM^1V58IyhflTLLhfF zPk}p70N`e_=xl>UnbV&3R(h^0NLsi<;J_XltGZFk@jP3n<1X5dWl(B0x&EK#wXohl zXdmyWT3r$;FCYOl%-v+4A3tJ6A6xaM;*<%WJcbujMlYRt0bEmrNv8I{A+}lerY(f2jQ7G0xPNui@ zeU=Riwt*9%SB3lIfaqg?ktH!zZ?S)!m{-E}C*2$M>03V*{Lr}|+C@{~Mh0M=ZKXx0f>gULP0tAL z$Z~K_!=)LkyiQc*(=YuOVe{Gy&4#Ou`r07A&1`pO%DCiG*h=KHqbQ>r>1HuCU>RU$ zk^hIgPV}fhzptj#Ec!P0uXPi98_Bl5w+BB=^qXMW9R2H58$;{nB9S^uuKJvT!>_K+Y*pt zm``=rFwI(p;L<)kC4@l}4G=;7g<1;Yq>ufJ{c_>{N5fd2yDDwed#Jt{K2}&E z1@)4LXN%Sc*{6H=;IS2X)j8(GRAFZ-fEdv)yP}A!P#p@rn~G4^rBopD&s8OJTpJ*{ zg(EjwQ274I&p_#p;k!3-Qs|d=S)Px{F;QT@*f0G)WuBN2bTzu`Gss5Q(RLvQ4=CJO z9N*vM z&2ibZYMu@q)`{|KmNTfLdp^TOUp#Ll#Sl#lce!qbtd;;_NL}2(J!ABSxsQ+EI&v#e zqG_el{O;6P6E6WU7aSy#RzJ#DF3BLcYShe;v`6vsv|!uEI)Sa9jb!hn*{I;>#Xw;( z===Eun{vr~HY#&pxvE@FX@>A*11?VQolPV>&jr7grI!int@4H4lyf+6?>^8@qa#zG z;%1_kMWYPV5d$RJSv8YW67M7^Vjom|yIuck>W$O+0_C&ExX_4H+#FQj_4<|Y8D|PF zvS(cuCmu|c__0jW-B1a0Wp0mi)SSsIq`eYP2Hcnf{rvXjO*L~qa0!R33c%hd2Sjcg zBr=0GRGm8|8CtBB%2!CbL>8Y0gbYlQbcw$pU1-aqTapKCo1s5P!W9!{ya*GS^%nj0 zs&gy=b6BuPiN8>vqGiXVJJLHgE%$zOMPVzwzP--Zt99 z#OY(*Zct4@SHEu>1pI|flS0(8L?4d`3IC)Np^o5d+hp5yL2t0VaO&Xy^XSzxyLpSX zZ@qOSu|C>~LD;pOA^^@@S*{)N=aV6FE~tkYL>domoiM*3Wgj7(RZ4799H(gNyqEs* zZ|Af8q3}tL+x8~96zgq)&Q$R(A^jD)Qfe~cq(7#ntr5#2A?gdF$0Rh8geKAy-`5xdX>LWn}Mz68? zP4fVYsMFy4)if4_r#fH7l=6mF{o+5nDW6x@t5B7Ex3(!U@_nu)gYo|;j~mv(MuW01 z->HMKyHQxDq>oKHJnz##hJDO6^Uxrz6=yT3)ufFV6M!rV)la^>4rOl~;MF(ZB(d@b z2h3_&IL#^fV&!30-W^0fW3rINpbwM(3RYDGbvIR0EEPHB9>`+ti2=^ECn}h8A2i7e z3ZGlyriehNX=r)3(P{U+V$!2}-b?kW%hy6{k8XGAky@+U;7t|xMRSv@#vM(&t^ zR<3w7@4>M@AVxIjHF6?%0^|CR>BI*NxRDM$#k=)!nnAIUxr<}iX|ma`B-_K<;F*{d zTm~?;eO{j|-o6gLp6Pa+79jhvuTT9G-u+2X4kJvpeqNRy7Y6pVY(>pZO#v_3FJIf8 zEzB5%PdHA{I5tX8c9uyeDJ&xOMYV1c38+&jC9gJo*JVTlDnd%F%$xne{2S#uDq5dz zA}AV+HJcv15yQ1deYCpTGc73G&X?HYobI!q@Owb1S!cqKa6MFGN4MJ;>WGmBn8W_L zGyw-QJG$3p_pXU`mwhjLU7Cv?8l#BY`U#*qNRY5-g(w2{nLq8jb5!gXW>@)9LA$=8 zL;AWkJ20nf8ro$P6br0yoiQJ((w{pz_eA+D+qm!m#PVD#^)kUkOK#AkDN^(Etk!q#I~_|-bTY#?7vMvGNVt(Vx1`qe~d6m(+(oQs)P=Skn9=Z#a0BK`a1%j1-Y@$K_E;Gyw{nY#-mw4>G}VL-|&Ef z%E6`Omz8hwalJ1=eN~1CiGegp*Z7T2?kWD^JFdMwq(_NkK@R`vYN;4+9Ups{l5L(Z z`>qDJ*-EO4ykTiU2{KO)v!8iFGUvG}|L zDlHYD8Fe#YU{Oi{R(HOBpbV)AY%$z{=fCObI;{AHZA{xflQ2h?t_?9S-ufp44%rt}^Mfb%xt&NBKy|j`4m04qfd=jNl4dZi*g#krZ}^G=kJoy*$ONn!E~WM3+Jy ziEGX!1by5H(aW7$vA|dcWA0%Wkc;T^f|6Se_B=I@gz&Of{jyGN>IrEL#j_5SA7Fxt zeWh=OeLPkivWT(%_$Y98$29;ycSg$x9E}t^pqOvfNltBxEe8jt`8QtQq71D=O6Qkm z%^TT3-Lx9tw_dB%2Ox|N6GY&_^?BIkZFoH&-iO6v@9ziD6GMADV;dMm#VxGA3WX((kX1y& zaB&%(FjRw0P{BztvY|ci9MZb3pZ*%CZ~pXLABe8IDmG_*Vfg?Ylm`}l)gn{jt9(@S zu>|i21M;vSsuth%JTiD`mA&$@HC`cm_NwXH>%(R(6zPUbQ7mj~yCDv2y2hm6toOxE z)#(9BJa;kaUaB2FlAYeysPhg?B;bB~4(Li5J;As?lINQz^w9%z?)*1f4A`--o%*BF zH-IYJ6YDn(O!;s~K8A#TH-x_Axz;D>cGcMxg0N*kiDloN(_5YWTtcJr=S-Hi-#Oxe_67oHRQ@&cV{|IF6Wg(d*gs(C zW3+sexQ!)#``RTP_wVGd3qe3D2o&j+K@Oi2)xZA45kS*C81ydp{>bR1n(KonC zZ(P2blpx%M4b2my(Xt&%R)tIVZiZrDHJ?7X639i!2?Dr=# z!0sOfNz*?!j@?AoIh;(NN%Hx6UGez6SiOea9PjA`z}lxwn|5l|giYQc0800$=mS4R z13n4g;ofD-zZXC@j*X|+&zT6lM6VV`i8%0jH+hS&-x%Yb#+M+-T~Lk@G3O23cq=`# z4BR#-NSAQYsJKJ#k3{baU^eaZeT26Wcp>)!-C}-f;yIwH`f*(WGQsAze>WlKNx)@z zi;6Yn^DtSc#rf;TDuU2x48KUNm}%?o-*(vYP5)kpY+#ESzI{*_$Ep_iE|@lCCp*vx zeuI0a!HD>nMQjGYggtX~hF+}G5-#s)%LZvUl#GP>?ag|87Hy5JR|FP zcMdefN0B}ZqIftv^iPmElweu>Rj&T-EK4vLuTN;KQ2xLZwhhtl3s&eh&(;J(_930y zul5Fz0b z!7rVvGI4YMC)vk=%Z6fEnYi+RXvd@e(!U~*zG?;+o)aSr8$PJ)Y2CWHC&>L#&BMS& zR81{p60*;0GwKU>#TWa0+HPVkS27?xwvWSmfXuUkcA>oKxDFNN)9JeNTPE{E5d!I# zHao9YrGWp$h^NU(uvQFJo;Ouf%s>+q=jNXWQa-Q?4`FL!V~3dDiEnQ`wwJ!aQ*TQ5 zj{Q5n$vZo^uD|Gv*a4%wUa-?gUF;8b?oHVduoji7qm&=oHyWzSN~PxKWhWPQW`=f+ z#tx3=?G*yE=S{qPpkz+tDi_C?5!_o``4L|cF4f9%img5GknsEhKLNVYn}P+6D9X+? ziW0e=Izo_5x{QI_b;gv{I|8v;)8&k4`VswcL&dh=)k1IlSnoRmZ{gnsx9z*7l3-!1$C?%zrZ@Bo?ocX3&SlFG<<$HfU)VPSJ(1RbNs zf^i3Sg?zqvmZ@tM#O9e&6$;gCE^3*vA%vSO8j)f?{inL02*tx{D>itRYSIsUu^xY# zFFTral~_ALo*S0Iiv8^M$l^J@XS1)Oa6&1>*(uo*zg155cmDGEEJ2*h`9Odx!@%AX zmeykbepHq;`O0di{-5@r=fH{${G^!g1|eUfkrh^74oHZ4@Pp=82UBpI3-0*iPFBR@ z*l^N!V)Yx37+u(!cS|)BQ2NTX!z6B}p@V~WX@_f9dAGm<)^Nx7vCK{ZYAbp~+^9bl zmV2SjbsJX~{eMSw;pacRUqLQ@H@$|R#v2zZIxF+91yXi0bS4`1wPX%M} zl(;#*G~B*9j@H=L%ZvznNEsA#H3~Ry?|(Xhn7TwQ2SIvoSyJUSdAOsK~D zV?(mSYCc~izd{;+UDU-Pun913zCSM~S}T3q*IHbx}41uKPmX zj!_Rpr2Q)pb#N5*2vMYSsrL3Ak`Qbaf$@;tWA$?^VL!Fh16e zZuGdB2*;{GW>L1!H@{a92Fx@I=&&5-$TRff0F#e}Ex;WsT;g^D@lbeX#}_n3=iGuX zai`_!IsBAoDh%xRC;%For$(mhN9@*c3Uxo_l*IIo(v&x3D&krJQ)3kLV5VZF1jf1< zW3mUkTiK?B7&$!PukmfWOa6;#%j(nRNF8walI}T_)h-YfERtJ&v)|G0)bTg1B$ka^ z;_v6oTf8ltCsayfPe=8Xuq{%9I-imCDvrkWWJA<0iDFtoffGo@9CTp~VJLy9qpr*Q zJa@(}qxDQv%}oYyy8SrbP%L0{`MdMVWau%P55M2UzyH`^?>GJZ4;7vXq$}^=No=u$ z!#>@Te#iR3AmCSy(K9idszVhI=~+#CC{nmhd+>c;(~@5iL^c>4-HyRl_)WKAB5OD1 z6cHv6It26R4%pw;Z@UC~^KooU1=Z`Gbu8|`gn{$pz9&`9;>(iZsll?4J)I)(^&a11 z^F8{L3tCCeHKJ=%2uiUKf9i7My{5=E{ET;Cf73(l(_^t}qvl}640e;*rgR8$`A|_R zD=yq9WulnTOy>akZOviO(YF!4zt^jO_OJJ`$mVQ?c4KmqfhgsEe%U=po}zdece%mO zIi~Eka6ZNf9WLnWYPFXRzry%5x5uf4aPw~xlddfjS9&$GL=56fWQ}FLR{|N9Qwm}+ zRbuad2CO&fH+S`X4G_CdJ*#UB^Vdv`;l3jTd(d)f*Y`&7Rr7a7WdoxtgCb9?joA%C zmW|K$CNJfuV0bTMfx*xuR$K*Q3<+#ZUftqgS8674%|tEX`o=i34su)c=$?GnFt3U9 zva}(vIY`T9IdBaf(5RR2$t$ zxY((wWqSs?<@r_t`!NE8qu|{$_rV!n?zD9Dqkt8KTSmQrtDmimZD!|y1to^j7A42S z4B!q6Q5tS*4Cs1waf0=p#YW+5BoZDFJ~Zk&P^H4T_0ggd^UN5Xj$3^1T=acL8)nlv zOR>YwTaPtkTaCV@1|b{zMd})uwvPiRbUzEm_suqUxGem@85xOuwn18PfPNHO-;G07 zv;XvMhSqgx3~`FG=ZX*|{4``-&OIU3uTNuyKayM+$UG_kG(4=f=Sc5JY5dug%~CM+ zi9nPcTTS+C2V?65Jqve4-#})*K?}76uNTtq5pUDb?A~)&o*2n?T9~##XPWtPSn*dQ!MRq zpwGLuZT-U-phw$bYX3jRjot6~vms_5+H;QjN^5I!qxCmMC=)46N>g_aEHl{6Ip0Il zNglZnX;$RDH0PLMvk-qIWOu=A`e!kItnWxC;MOMDXu_MOPQq>{=aZjpLxYe2XNZxV zZKvAZDDYc_-jm#MW4w);PY8d9_9Gb~7^}9uzec5yd}Gxdm@p22Z=8QzT+F+CIjDd1 z-=yHgZMXF3Xu`i>v+Tl-!0fz~`!;iNvD^OcG#QPOKDjDw7X)JG{E0hPFY&k&VteIs za+T(o!~O^H=;7)ZlNUsJkxNLk8i0HWVI@^cVpXsPIykdK2~OC5!0el%JCIM+)~Jv! zD46yes%k%4qJYvQ6MWlJ3A>tbTB<%|1z7H*+@F1G7R#_oNmcrO0!5Chb7hZME_By) z`E!P=*D)}r@Z@@iRL|7~W*wy9ZV6dQJ98?)`~>tyvqm31d-*MZCk->L846Mm+)OK^jqu!vH`1* zeU83+a(ho2Zq&$1j#qe$RA?4sqY|#7&g4k>=T!Oc7GQ_XfE(hoKN7m{@zW7(S!F2< zb?@wb{VD?@#;^_BRL;re)qnfz`BW6OD+9{5XZwl#gKc!no!wZi3A25clM;ppl#m-M zC9m;Z)!`@w$2HOHq`QKjUkM|P8L@K(o}#N2K_R|(G?aWD)|I}e$bGHxyvxyo=DO*O zORqY@wDS%>#%O+#{|aBz2}-%7k-Piu;}xR7k>bWiUCK2ElCcj*K1@~R3<=i)ptIee zad+ArqR{5b2j+Tn5<5%`D9E%Drg`)n_#bs3D(D!$h&~1m$BXP_B+6_M5Mpoq=!vkl zwq5)E^Us9!V^R)#6RQyJ{;nN2>Wo<$zuyQy2i8N2y`-4=p4)lXpV^!(ehU|?VO8G_ zkx%{*L%Pt52oc}q5@8DX)kN$gXYac-`X$bVVl%&@-Vs$&WJ8y4lz0_RDJ%Em@7SsW zea3byxmANu>iAqxOcq*xO`ehyHOR=SAfKMugex-yUU%>L2N~rsBlcsP^c>#V%UjGh z-g=;@Vl5TPgQC(VxOUo6UZkARr$~_~^G4d@B!E&2TJ4F*1*x3^J2zG+Xh)S4x1@Bj z$PPTJZnb>RTYs`?eX7Ut@+shk>Uj2*eAS_Ivc%k;)4e-lR>!3_4L(EG`5DyQ0kMZb~nU+`_X+ zHuK(YSdsodybC1*8*km2^tQ~V%rgprSXsu3_^ zAO9D4?Jwqk3Y{M^BkDxlz^|3niDxXcHFLk0+V{V=zpY8b&ty|(%P|og(G;XFIG7(y z+Vg%K`|_KG0C?RD?N~EjD{Qnzt8rpqhd32*j<27i?j;blk*wj{EY2|U+|B7Vk0h@Y z)&?^cFhLZ_^YSZRLky=9@*I2;75J>I#?Q& zn}~dyekq7(#*MG-NS+wM4+{J$0h8)`pfmHbWh+{@tv`m*Dz`om};70y&KH)cBkY4F9 zxRVtU**`B;Q6@S0@7*bAZ8I7<+$=eHpj^UQ{!2k4N4oDNN1RK}>k*Qu zFg1t?f5why-j^;iUk8sumDNA${4Gpi$~GsPawj88K>ECSOuoJmp$2ZTKT zxI|8X{!E>wKeuf(-M(abll|`)VG>t`WQB^v|tMxLfsGT)Py9n9_$`OJdC==B^}3@@L-Y#a&I1 zmm^ZKd`Dy?$fUvm8==ZsSBa4ORC6Gl2HQ;ynEjo9qk7MFD?yA60loQ7WqvEk!w$c% zkQf^>zS^(SR;V`4!9{a<{D{0shNza+GstTd=%^g$^!BreZt3Nr!m^a@Z@POCpZ^jQ_xbuyr zsvA-wbvjq>{2Atq?N5G{e)n|Mq6w|%sE#=~~k*kjVh|1r4ijTUgQdsrj0O6la>8TMB zNzA^`Q2W4#4FOQ?4S3DV5^G)kVZ*sx&7NeaAXkg_eNHw!{o8lLWLY`MC5J61b-GIr zJReF~hss{pPBxMX-~LE!L+oz-y|xB!nfjGY`c9Z#$l|-EhrP2Zc$xCWP_Mvb`*LrQ zG3q}ly~6Gi19*m;=4a(9^OvFWJ9>g?!riuEdL%LMm*UD$Mtx~}l^B{UC31Nj#>+U6 z%SAzsUBY2t;W>DfF#5&_YwBQ8CR^cfiUX-ojy?7B0E&j%jxy0U5*A(lw`il|Io7F$ zo+wZ3)7dQcR57N37;K0vGEbDsk|M~EOsMkOkr$TA6O4h~}z36TGg7;66i%cfV zw{)lmT@IQ;#+knO_UzuVSO=L8VWH;xmF~6gmyPgmarBL}rqt!F#yFQBLm^}#-K!p| z(>4|N2|r7_%ts)`yHNvt6_AVIk)g zE*U`=g1`@e&TpHfK^0n1`$tzHz?19VlCU1^ul`VVNmCAn&DC%Bq{(2AFXV5#x;_yU zZ`{s|a+x+}9-DU88Bibp1ZAtTOIc3#MV_i|jEwJ>nrQYo7@ApX^*omp4Gh^`WChr^ z&|3FY>0n7rxXv6qkACyvyYoEy(iAs656lGV*C}NaS!9FQ=#{vnzA1k45sw3QObIUq zzVJBRVc<#?UC^Fb?V}MBOXD%{nUGqz8W!T&z3`^rT^C2Orh8(8JaXuM#=gE8e7~2f z$jaAfacIyrm-Sv9Q)$l1bJRHm)@!xVXijo8-9?=ZQCjZ2|2daO9OIbnaq4Uuw_m`1 z?sE~MT=<^G(JUYCn{xs%X5#bsi18TSo$UXnQ8kkMB(C}6aPbKmNvg#yBib050L!fC zVc_EIW3q4KF!~)(qoD4ZPomA8u{X&ct#UnE9T0!AWgM`)17H(Skv)(+Vgha}yABJ; zFE*T?@ot+-|7vA?@Oe7$u3Q1Tfem`U7|(~cf_s-ILAs|#bmE`*rGC)^+zg+2!p~e@ zgH$fi-CN-$2f{Q8R`hG`7aHGeb7dLwD>gxQIvj8!twu;C#(0b`>ZBU zBO++vAyjJtA4*EGXy;cOLHY4Yg0}RY7df|q$DK2FafRuwBw}h`g2UnT_EblCF z_4W-BWhsLmr~6%X21gCWAwKEHYdjY2TKp}arY2^eS=Vt|eTJG;$khn=1S4CzgF(h- zi88QigD<4+9KTzQ%^$e?&}qB)^V?E2=`T)idS8F~C0(U4lzKs|CGgLqlA6B=8dJpW z0LZr*hAr@n#U9-TFegW;8c~FJou6I}OCO3>IlEQvTow~CBE=r_4;`FUQVWj28vA>& zV)B^vV*KA@zvVL&;2Qs`Y1!+*HRo6xM9`CQyNz1~W7R+AekhcodCHV^OcyZ~@~M|x z)_fr3zO6W^?Fdy|hU#HCX2OwEVZJ`|Gyk|PkRs#!{7sad&FZJ`mW*U68A3nc-gUK% zo4T2QwGX;^qYvlx&kr9~I~&+3WGzWANXNLc@59oKt1tz!?vISJg$HcmeFqPh{mNZr z9-3X>ea2-$!Exb#=i6kdqVq&~s)0MZ@FSIMU8WT9yH(2WLbWKE>46%%?Qj_Xa!$dF zX&~}8pTbLqj#eF)WlLAHt(v0&HbKLh#a*4&k9yegEN%K+!pT3lNKQM|RqE+NZxmqP zLp+>rf1t$kt`*l&ShNzeYS9jnXIyMHI?1Ug-kv|Ng zDE(Bzy;JRnRQ&35g~z||S#(z4F!N&OQ|3B3sJPuwGl)pwg=v^f2)#5y>TulBWn`@S z3nE;sntFDECUop7qp7D(A`_&}J6_2lET(kIB~#!?XbVT!Atah6&@dYNFO5aIhZX-> zR%wm)Yp7>^=xSZZ_7S3dc>C@CYdT3`z<}na?APy}|NRTrXv=Z$qWzwn2-6ZyhRht5 zHzeiL%|1tVU>_Cc4V$8g|B{(nt15I6>=k(gLJYXf{fZ|vsj}u+nqKDv5(4M4>&Qj@ zx@BGa1UxB!)l7gyc(196hR2jplfeVVF|Sfq&eWtPcd()xym6nv6a1iToe zxMGnVwacHr4W`a~zW4L0gYN7_LY5IrcON__!f|wOnjRy8zZ2Hf7F$JbJQti~(eT~l znC9~Xh@YI0Bjsw3o$jGj_P21`XNqmRVmO7ju&-V0{xJYO8F%rwtrl4|TK8OQcTNkj zn;K?s*%d2iBckO36T``@1Y%7H*V^g;+d@HTBiDytkuH5MCB9ADU`kHIb+PWZSe@|X zw12x^*cXdNgvs}U+13|^OD6o2d&U@rVf;QCUS93lA*21#tPIQ32h4-v+b@@tZ(@NL zK)t3FzafR`_8Thzu`20zd>G(dyM5J0o_<)3&pvjKsKIxQAb)k`EH~>{mkDq7@j7*^ z{+6RCLt_b|W+2p5Id4ECh&x!rT|zy@gsSiS!Jy%Ez23yMaSGFXOd^=r8g{Jc^!qG2 zG3kTR)8}!y1Z2Gy89o2%kcOaJk4n>OPMX=KWu#ZoUPu-el+wL&Sn%Z~+lgg`sCLO= z1ML>ay>Y8cPIY^)Or*|os=kC?G@)M7t@Hxa!F{pdKj(c>*!#Te-VF5~L5L{=m8|l9 ze^03#HIs#a5zqfh`|7BuzUbefq(hNz5J8cWmKaJ&0YzyT5b5q_C4ewoiR#LL& zjL_I09H;`m7qY)Z#Qe$?gEENKSQmj6FCtRD@3jvHl3WtK|)IsMn8$Jfxbs+v!hbQLf z4c1F}XN)VFKc}wswq2yeGA{VuG^Ff`gjyyf;nqFqnv`?{N~6Rg8+5c`(2+c z@HqxU&mq`4@N#?+?JqJSm?4&^D@#w0@G}$h#HR`i&f|_r%F9x|3d#yCYjRv}xl@bN z=Wv7?>3POm%kF=AV4v$~;AYqTI|KKcEk%)8IU==%{cp_@1nqP`W&7iyr~XGeb7LR; z-`G=7qniQ0sNU3E>;)%YS-ln-t!&kYn&xf7wwyQj0$NUMkG6-}D=&4Yl4!Jc-& z%Hs<$2dlkVXT>wS>|0{uSq^r%-MGZ$1xpY0ZY5{44NJOCLhn6=9)0)Jm&9NMh^CE{ z=q3L}#CkPhgzRZ&(j5&{Y)aMkyPbk>~kNb(O#A_irJp&y6GCXKB|B!jz+K(7!XtO~XylWm18 zH=$Ry&5oAp8*hBSV~3V}6{PHf>-Dx;0^he{_4DX~Qj!L-$MS2Oqrg9*4oNrRoAg(v??O_JFq_bqCI4){?x{~he) z!j$G_A~JE%kmC?rh2DX>zvKfa{T65eD4t7&E;Z?_-LgS7jD}L;e*OKEUT1S*C0a`4 z5YFwwHldzJX*BuCE?nTCmcT-eXhBVq-Kx$$e&r?{{o2t%pk3l^X8hZNEKJ_rP6C8B zlVd@^KsN_D;XjfnaMKfI@h~Ovnj$tq&t;va_tfhLLNUTEj;67GGECk|p-YUL_r=mK zPJL}19>>Oe_pUm-}y`sMO_Tytwt%lCmsv*ZWp98;Rx4GW4d~c!F zW+&*^_M5yjG8WI1m0*<(cmL@Wo{*i{aU|QZvbVV}_hcGbOm|AQqy|LB9n9hK)N+N| z&LbpxZ#BEAimWLp>OOy9-*5b2VyIhIYjVqD7ww z+o9yfcIDJfOFRZ019m#REXFCHVo410>e>o8&GY%xW`#}jbFQRdmiJ{Ai+OcojaUc% z($7jl#xG{xQU>t_8yH^6G<}HYSm?q$$HH;yaG_pFp$$4^ilD#M(-HR$pQ;o-KYq9& z$Do5>$mKCEQ`5mqFcWgShpk2IlFlcs;cbX)Q1yI~H0cX-Jld-IHH=C7g;hbZO2{h> zzY}lFwB#5!pKvdaZtA6mL$_nS5sN7ZyPT?wR?IlAI$k@L4|9ER;s!oiOtZe0a*It8 zG)Q2?=!0R@t$_Y@8UEV>B7+k9XWgs(X6_8`pQWoGxUy|}!2U9P&!-k&vz+pkF;*<2 zW3?kAcEqKfZ#@>97v$H=a*-eeVSf@ZyEkTd`r=mKK)OWUuLllsPpA*kDgIa{AK5PL z(|1kH^D^cp1QuiJe`#f%E@}Xy3t8Gb&6V1$`3coMGz*rt?f*-~a(vY0wl=L49c`W~ zvi)JWrP+NV9eAW)C0;OSLRqF?|C}%>SjD}mvzC-}DNeQ;SKM36wMJseFtiJ5bOdSs zM(p0dh}+Kff>=jFMoRyJJf=T5lIrJQ26bk65Rup(k18=5jT9;0RVDzAV1Hs-seTbP zDGn7L+N@n*OR(D(lmCOxlovT7SjO6MV{bF<37b7KL)8k@4vq(-{SreQ1&<`?BPdX(ou@m!84U}sHRo{ zr~ds)6u4m)%Z0HXK?tSypxjuoRmDhfzhOrRvLAa6{^8njP;4$1GAY1kPna1uRaYG>}d~fq(3jXtax3HsOq1) zDDR(xIbqn{{PyAnboT$ys8jT&ljk5vMPWrcK~Xc=x*ulg!Vo!}IAy%v_@@uclvqrK z03Z1F4kU$TE=)rWL?BV4@%3c%A~ZPyUi&B7$1cN%X05NWMW-(bqiur}pvbpFh*^;J+au zwZCq%>%c-5;1Jns=-#rO?K&!Gd?fTtKG|z*wEZRkU=e&y#Xcq7^X~)Jhu=&@WY1@q zA4C28VkTx*XPvk*G~Aw>@}nab@L`N7geh&O>C)C>i$7^%Jik2}S|c&90{YREFLxA{ z=C)S>^j(FS28w~u)C`N&VV`fGUzFWu;D6L6%n?LT%PJt^v{pLqjvWlu&oFu*JiVn3 z>!6kbkYDo|x21D09+SGVahpj7p655dclU`fs*iUbd^QW&W69fmt~P(XFpc*~xr|bz zucsPDMx`zjl>_5vp%ET<^f0&-Rq_JG&9?rb97|$`ym9ef{HP41&j6XL1tKZPGR`Rp6YodubP3ht zUA@QhNH;|J(MV9s9=rROR(OeD zfox55A|4B+6xn8&eNlj}#D$$kZcfwJiV)upx3$AMm=>INLM=t}DeO{iZp!@C#|LnY zc*dKtH@?BT5l0^seKBcj4INd|H6)pRM$kujhQ(L$(%avtH=^Z!q^{J z>$rotNFUtKaF3gCSV;7tRU)QrbA(()gZ1dyYH^)}2=Wis!3{bZ#>cKtOhXJp9YRYH z#<#NyvTIEsSd}<%23d|e#3oc>`2@lFcZ&^%e|WXnUFyVsV+Mo(6QL`Hv(1szvIc8U z0-VU`oXN>pq$)d?JLBLcW;AooV|i1yjL~Ts3j&(b^y0;x`);vaj}qH{mW58e%E=(@ zBSc3uJFikWhl3CTFh=$^asw8WiaXO6ETRct86qPhK}nh1M9e@Yuc|aoe@C!TN>VC< zhki666e`{2b-3h(Rbsi9Dg*=l-cCu4nIE{LaHf9xE*E=hvHgaPrtQcMc%v3TDZH21 zw%O*G$U%fRq4J>mpCNOvNghEVk?QbGfheyCMbD7Hp?=Z7p8ckKcCe^iNoBH&Luj%C zSN3bcaGfkGM#)^`4pq|rh`xu6XiCxVg(*_rPH3&R?>vdggwa6}mlt%~bY-hYS3#?c zwrZ2FU~~Y%a4SB@Wgx`p5#5+LjXERT2x~{PVZ$v+@;yd$>^47?XPnq{Bx0GfH9Rlz zNTyG8?)Iw^8!;h%3m~Lw?OS;m>~l!c#VkG7TN6dBE4^(&7X@q7;`J2G{?6tXXDUt4 z8;A3KL-Oax9wcJI{ECqeMmD(qmRZqdZ>#n`S88`naHx7;CYgWKpZ2!@+z<~T!q9aH zwEtM15>HNzGC06jQVUWnYq8AaWTeIfsYJ3S9kfl*baQTo89HKhr&X`mup!0i- zdu@lzy3mx28#6TZpLA6x3f}h~W7gSlrc^vO0z&S@{~jGlvPiop*Czv9gCzqIvO_(f zDXwspr)sm3E`jrGHWBVs89_H%d=<-eNW&q zyOYgI%w-H4PCAu;NgaxxXSIgyIyntvU^`@AuI3s8mY9Bb;XH^Kl;qTM69!a#{OjJd zp^I!n8vFmAxE_o>eNW3B_p&k|Sq<3YVUAJ6wWAC!Np6oVc z&NN~>C)*c}UMB#b|NHFZEf(4brel(4#`Z8emBs15;*4UA-xjwo9yhXYnaaWTjfG9n zDb^oiQ5IN*;gh;ukfTMn=n4Wl+GyG;48|0k9>~g9yzYywe)0#<**I)yzM~_zBN$po z&Eo9KIVjyf%~8i?OT@RaCpdWr(SIZ>R7VtRyKRT7Ams=#2$rR-#>Yby>ulsz$>qT) z-)*N5U&8Xq^I~m6L;X=ze}sB)^XzxfZ3FY&FoSl0rgu=yIXwaYGQU1bvQ2 zjgL~!Zo|@iS_@zMd>y?}b+b`K8+Q0k#27J5lNn4}WgM*m>;9w;TwfmLza4mP4(hT?<7u8G+`mvv zPNGuPd#OmH`X2kkPk9tCq8PD9AnlCiA$@#O_jUu4-a+<3gmU{N1WNMX@9UNw#O_XX&Urw3F=Lm^56{cDimrwgC0;?k=qm_{Z zcdhP9Ut{$A>a4EQ?2gMYT9S;kkT2VVW_bAvH;9S9kP8oZ(3g?}_f$_($FJYS&L+v2jr8 zCv|} z#WK}(KiW*NKIl2LZhG3<245{U@^X`W+?N=N;yw6~mA{$_##e0Jm{?j3ECtc9ihnut z#Y2OoIWHhUekH2xMCrXPO8zK0*2+Qv-yi;>PZqR3Z-sxB#Q(W9=gDI~R#yJy{T3A! zct4EfSu9>fRj4L@Zy9o}@8>Qq#S?e|dlOei7befo>OjmjBp69AC5FY9K1sG+^Y(X`LbX30B=~ zRReDQz~6=7*IDiblZHP-MU!9`Uh96$YZ@0kRe=Njd=K{7+*O#B8UnftjzHKSjiwiy zgVHTnI(Nf@1B1gfvRs3Uifx&o(*7L6&UxSC;4z%F2kG0(PTyWtkyeL!y zq|t!`7exV1kfJ7dqZ?x3_ARuq>d_j^6gk<}Fh02yu+)S+4*m_AH2mszD;VOix@dQB z(uRvVeLJRw4+I9AQ0X1wZYOn@zN=Ri3bLTXjLWob(@-r?X492%psd@S0VGElL3|)< z2@P4<;b_qd)eH5|=pERz+r|e7v-__r`3;mM`OQ_`7PC(?4&%RTxfqsPON)fu- z%6ll*|IcVm#+tllh%y$becRmU-8C?@^&?E-!XC@PvNRQf0t$I9xwcaB7q^ej69IBp z!Pvm-&nn3tQlUrmK5}dB+QFYb$2?+;^@yzBidFiaY84vFv;#Jifa5GnZ4$)h2+OfVM4@0IoU7Y`=aRy!t$M%#@%@_z3HkJP4*KfvS*oIqxyQI# zXDmU!fN-VarPf_q5%IWx#iObjg=HiMi|K{}tc%?4G6%RNA|M11xrdX5&Md%8n|rOy zd0TfGHClcT;lP$FI!9zWGO0r7vY#T-9{*Zdx71&*jfmq5Um_bhW?)Ww@@GYF4|f{3O0TQLhpQi&!dZu+44?y^>Lk1_UhOTo)n>Ul8e`Ya;cusxhg^K z*4(VWM@?b}#S9`^kP6Tny*X%=a-xJ2ZFPaoMb;*o8asW4W`KzC08iF7I;ciV$pO#0 z&q$?Ds>ZxCywSCs27X4I3vq6{TI9cs1(hs7NNs3hZ4F;R6S4YF%Zm0ta-v~DU-ClG z2U=_UhZdj{S8);0b0x@q+08KX3P#<5FNPCybC8b9#7ex~bohI;n`ct+97^jXI#W^3 zxeNg*KXXR^xVbbR4>=G+Z*3p(-|_s*OT!UasZ{=^$NqECMcZsWOKJH zF+TE4T9f78r~8@s2j3Lwx_Umbbfg$Q<6Kml&wwJ=*dN4Njbd+z?;RyfOq*7KNhz$m z8UKFq3bXe?ms~u*_PA9fE7a>EKqI=5UCc zd%}k5ET03;Db*HVNU!#6_oVybEYMrQqKb@;2AutiP0Q8v-9qND2J!T*@HMaWpdE3v z733m)EbotT2wvE~vK#+3537kvQht{^Q&Qk)hstmh&MTwfq< zn{WM=Mb(NwbSo6zeO=Go2jGz2kaq67uV)@qdL98Zfl;)%xu1v_w?ZY)@6o66s76`p z=IV8y=jb+M2=D1({^Kj$@kFo~0(hLS*9h@h}_12eWSStXZFiA$a zl&f%r(UVHgqGnvQ($jj;=vBi1xxwOoz;x&Vpig>FEI_%?I$SAW`+my{Vpefc;T>bf z|1s%XTFI+lRB|6|x@d6LEGAne;UybC%+OG#dFt|?IgD^+1H<_LYmM(h6EP?q#grS9QGx zU}qS|HIfHWo=F}9T+XG106O*lLsAE;HyPj~_F(tOap@|wX4rOh#K3)(F;Or1gWu%U zWyTjwiM)(&fu-4!zbkWL_1_)zZObX#sq~nA-eW`Lj;ErAb?h!!B{34Xk>ptIgl>{Z zHw4Kt%)+Y?cJH10a2QU~wABl9q3zdeMZEB9pNq2fw#INZmH*{lw8 zLd~?mu{m%EHR}A`^5l#Od>oK{Aaywdh6nLH+Cc4aGt1IeeF&}#)4fmx-BE;$f=7Yi z3Cc8+Yv^xWTW&~6rt)N zG~89fokBs11(bRgGuTY#GD5IcN1)_MZVF^`|Y_bA>PWP>U+dv--Mnia%?LK z=2e0@;sa{28^>KtQFZo0>=^aFY!qRqVG7vdI&J2Z@U|qvZ#5u;!gAwhOZSIH4&$tY zgw^^9KYFKa`jqRWAqek^us*m0(h=OFev6C0BCJlH3_Cr+!P;ju ze)Sy)gCRGERTDkEbbMG$qTdrJE-LG)zDnI`cnF`t3N{+8Cs_nE%fc_G zmcQ~QM`wApjK-4##N69a5?%Tjl(*lKOH<6x`t2!;4lcX3A43}K&~WFb!34x9 zCu!bc$7Ij8$AadTa3r#voxA4V{7731fb;4mQrQj*CprbKdetm7d+4`5wiyfMwitTv z(KT?d)6eINO$JbyI&6^wF97eKCei#Hdn@og_P#ZUnEAZBz^(0dAd9&KmTTg%Vgu%m ztuy4PrV{*Hc9vDIi?0sDAZYSv4Z*Re6Eqc9cSwEvu(68t^>`~epdGWebmk8B>H{{9 z2cO2P%g0b^XFQCLWbs{#5bch%IP84p(bqnz%GPx^n?)#9CjkTH)EZ8Y0!Q+vKCs{=$&-qd!GN2E|Pmy0#Tb7Zb`BrXZfz^FruXh) z{PF;3^9kXykTn|55|L)dR4x<3kdS7FqmXC7ag5*WBV(TY*eXtPU;)<7qWlP{FTRF} zM(1%-&CkGYu4F~Zwadnb#veEmrrtPVmL#l{OI8-XV|R3eBktm*a<-E~S2kQXx<8rQ zj+fPpZsK<)xXf2~wO*&Ln;};V!Wexk#Ck{ z9Rn^StGB5SNVF~s8vUkW`@|dmm}eoVD12vNyZw#Ptzb$2Gq9t1P-`(_ z!L&3N^A0;jvV;G7$any2x(FaAMy`LFs!l1Q}HudnEA(Bs}ovD%f zVfKx=rdx7KaiI#+*sJ8Aa@pHL>t)jZDsLS6NTeRIyZ193Ppsq9;{BM_#;5VUmEP2`$*mnl2j6v;_*FK!4b69 z$>81;(UgdOEK2VwUZakJy!d z4Tqsv!=}l@NJkJ(9lxP(&S?!pW?kmb^F}-k@whxXud2!S{4TmJ-I{$>hUm>gBN{bl zA}&*g>$b0Gqf^(Ab|($+KLNX8QWv!_&mc;0W#@}j=`XnE8LVWC#=bINg1&u_JcLv8-Z6fGV&Ca}OHQfAww!vB9G!3?O*A(Q$X` zSEg_UIsHmq>ACi2CrC4m?$?flta4+{kSQH~2niG*^tqC}AhKlre-gL?AGs_HhQ zA{@7-%nI!R{y7IrT_eG>uH)VUprG%vs4p3R`sHk8ISP^5+&Hr|q+4XAR)XX2=IJ1w zkmf_NQ#D|RKJ|r=W-GwvmdrS)v#A-1Cso#J-nCVEvl-Ede2(x{8zkwqNZMgHL8s>< zAr^{@M%=~r<)1Ralxm?7Ge|paam9n4O6X?b)?3#hH%t|_M9zlT?_5dPBZd{#@0z^g z_nN>c$e8euh?9J99q(B)ONU1tj!@Dawpp(`EdjRU6o-WLgibX$ZhXPo)T#NHUXy{R zZ7R+}1fx{k51Pvut-e1QEW>QMWa@HvtFcxW>|9)y3kHmW&5{G!jz2x4^!$b;^{^V2UA8#+eX5$0_x)iKxk>sO{M-`#*S&y{p&L9NfE)%zms;_g{EV= zqfkQ16y?jp`;d8hlccJQ7dE=P%rq`ZyWn9#hL-kglMRc@ZuBw-rVB&Gf#7LuKqM=+ z11^(|`TEhanL7G6rYT7LL&lzPqTwhwkdqpe?C>{zJ8f6r!Mt4@f}^p}!e-5# z8;0=Hx@)o~Z0NX;u=M)%itm!(0huM+5mR^|-ab=x+}A1)R;y|ENb1jwQ^&^avimm- zCs*j5<#P0z$OnhcDau}UlJu5kj)t~()(Ge&oi=lor`z-`b3@y7WF{`AvT%IE)XM=y zZ!NpFD)c0tw8t#j9m}{nSIPeuLg>VoT)ldL>xxl>k?-;LX~WU7U{mP$O5hQ;`D_`v zc1jGq0bcjWh91vioL8aI97vU*!Y|EL;FPoLfCD@6%t>oa2jh zX<8D3?)fh4_^nj7%Js;5EKHKlsPHl?;3M!wf+=Fs!*HIDdt^Q|kp`OLzS0u3EPsva zUV+C4(P7Na?@mq6{-d;97MllMmg@)QFTbqgUr4C9NZ|i>irvRUb!ESdr~gyYXmU(X zZdPw}{x^OHvLfi=&j*#afF1ebWeBaTZb+SPAg0+MZN}&^^pAm2Rc{P1w_{3~zk}8S PfIBo^>Zn#KTSojZ{8Pgt literal 0 HcmV?d00001 diff --git a/dashboard_studio/web/icons.js b/dashboard_studio/web/icons.js index 7fdca5c..477cb16 100644 --- a/dashboard_studio/web/icons.js +++ b/dashboard_studio/web/icons.js @@ -151,6 +151,18 @@ "arrow-up": "M12 19V5M5 12l7-7 7 7", "arrow-down": "M12 5v14M19 12l-7 7-7-7", "copy-to": "M9 9h11v11H9zM5 15H4V4h11v1M13 14.5h4M15 12.5l2 2-2 2", + package: "M21 8l-9-5-9 5v8l9 5 9-5zM3 8l9 5 9-5M12 13v8M7.5 5.5l9 5", + trophy: "M8 4h8v5a4 4 0 0 1-8 0zM8 6H5a3 3 0 0 0 3 4M16 6h3a3 3 0 0 1-3 4M12 13v4M8 21h8M10 17h4v4h-4z", + flag: "M5 21V4M5 4h11l-2 4 2 4H5", + share: "M12 3v12M8 7l4-4 4 4M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7", + qr: "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h2v2h-2zM18 14h2M14 18h2M18 18h2v2M17 17h1", + layout: "M4 4h16v6H4zM4 14h7v6H4zM15 14h5v6h-5z", + bookmark: "M6 3h12v18l-6-4-6 4z", + megaphone: "M3 10v4h4l7 5V5L7 10zM17 9a4 4 0 0 1 0 6M19.5 6.5a8 8 0 0 1 0 11", + import: "M12 3v12M8 11l4 4 4-4M5 19h14", + refresh: "M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7", + air: "M3 8h10a3 3 0 1 0-3-3M3 12h15a3 3 0 1 1-3 3M3 16h7", + gate: "M3 21V5M21 21V5M3 9h18M3 15h18M8 9v6M13 9v6M18 9v6", }; const FILLED = new Set(["play", "pause", "next", "prev"]); // Dutch (and some English) words for the icon search, so "keuken" or "slaapkamer" find something. @@ -167,6 +179,8 @@ paw: "huisdier hond kat", bike: "fiets", bulb: "lamp licht", plug: "stopcontact stekker", fan: "ventilator", thermometer: "temperatuur thermostaat", droplet: "water vocht", washer: "wasmachine", dryer: "droger", dishwasher: "vaatwasser", vacuum: "stofzuiger robot", car: "auto", lock: "slot", camera: "camera", speaker: "speaker luidspreker", music: "muziek", door: "deur", window: "raam", timer: "timer kookwekker", people: "personen familie", + package: "pakket pakje post bezorging doos", trophy: "sport wedstrijd beker voetbal", flag: "vlag formule 1 race", air: "lucht luchtkwaliteit co2", + gate: "hek poort oprit", megaphone: "omroep mededeling bericht", share: "delen", qr: "qr code", bookmark: "favoriet bewaren", }; /** Icon names matching a search, by name or by the words above. */ diff --git a/dashboard_studio/web/index.html b/dashboard_studio/web/index.html index 7932635..1cb60fb 100644 --- a/dashboard_studio/web/index.html +++ b/dashboard_studio/web/index.html @@ -9,6 +9,13 @@ Dashboard Studio + + + + + + +
@@ -29,6 +36,7 @@ +
@@ -36,11 +44,16 @@ + + + +
+
@@ -48,6 +61,7 @@
+
+ + + +
@@ -73,6 +92,7 @@ + diff --git a/dashboard_studio/web/qr.js b/dashboard_studio/web/qr.js new file mode 100644 index 0000000..53839b7 --- /dev/null +++ b/dashboard_studio/web/qr.js @@ -0,0 +1,174 @@ +/* A small QR code encoder for share links: byte mode, error correction level M. + * Follows the structure of Project Nayuki's QR Code generator (MIT licence). + * QR.svg(text) returns an SVG string; QR.matrix(text, mask) the modules (true = dark). */ +(function () { + // Per version (index 1 to 40), for level M: error correction codewords per block, and the number of blocks. + const ECC = [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28]; + const BLOCKS = [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49]; + + const rawModules = (v) => { + let r = (16 * v + 128) * v + 64; + if (v >= 2) { const n = Math.floor(v / 7) + 2; r -= (25 * n - 10) * n - 55; if (v >= 7) r -= 36; } + return r; + }; + const dataCodewords = (v) => Math.floor(rawModules(v) / 8) - ECC[v] * BLOCKS[v]; + const bit = (x, i) => ((x >>> i) & 1) !== 0; + + // Reed-Solomon over GF(2^8) with the polynomial 0x11D. + function gfMul(x, y) { + let z = 0; + for (let i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11d); z ^= ((y >>> i) & 1) * x; } + return z; + } + function rsDivisor(degree) { + const r = new Array(degree).fill(0); + r[degree - 1] = 1; + let root = 1; + for (let i = 0; i < degree; i++) { + for (let j = 0; j < r.length; j++) { r[j] = gfMul(r[j], root); if (j + 1 < r.length) r[j] ^= r[j + 1]; } + root = gfMul(root, 0x02); + } + return r; + } + function rsRemainder(data, divisor) { + const r = divisor.map(() => 0); + for (const b of data) { + const f = b ^ r.shift(); + r.push(0); + divisor.forEach((c, i) => (r[i] ^= gfMul(c, f))); + } + return r; + } + + function encodeData(bytes) { + let ver = 1; + for (; ver <= 40; ver++) if (4 + (ver < 10 ? 8 : 16) + bytes.length * 8 <= dataCodewords(ver) * 8) break; + if (ver > 40) throw new Error("Text too long for a QR code"); + const bits = []; + const put = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); }; + put(4, 4); // byte mode + put(bytes.length, ver < 10 ? 8 : 16); + for (const b of bytes) put(b, 8); + const cap = dataCodewords(ver) * 8; + put(0, Math.min(4, cap - bits.length)); + put(0, (8 - (bits.length % 8)) % 8); + for (let pad = 0xec; bits.length < cap; pad ^= 0xec ^ 0x11) put(pad, 8); + const data = []; + for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b2) => (a << 1) | b2, 0)); + // split into blocks, add error correction, interleave + const nBlocks = BLOCKS[ver], eccLen = ECC[ver], raw = Math.floor(rawModules(ver) / 8); + const nShort = nBlocks - (raw % nBlocks), shortLen = Math.floor(raw / nBlocks), div = rsDivisor(eccLen); + const blocks = []; + for (let i = 0, k = 0; i < nBlocks; i++) { + const dat = data.slice(k, k + shortLen - eccLen + (i < nShort ? 0 : 1)); + k += dat.length; + const ecc = rsRemainder(dat, div); + if (i < nShort) dat.push(0); + blocks.push(dat.concat(ecc)); + } + const out = []; + for (let i = 0; i < blocks[0].length; i++) blocks.forEach((b, j) => { if (i !== shortLen - eccLen || j >= nShort) out.push(b[i]); }); + return { ver, codewords: out }; + } + + function build(ver, codewords, mask) { + const size = ver * 4 + 17; + const mod = Array.from({ length: size }, () => new Array(size).fill(false)); + const fn = Array.from({ length: size }, () => new Array(size).fill(false)); + const set = (x, y, dark) => { mod[y][x] = dark; fn[y][x] = true; }; + for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); } + const finder = (x, y) => { + for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) { + const d = Math.max(Math.abs(dx), Math.abs(dy)), xx = x + dx, yy = y + dy; + if (xx >= 0 && xx < size && yy >= 0 && yy < size) set(xx, yy, d !== 2 && d !== 4); + } + }; + finder(3, 3); finder(size - 4, 3); finder(3, size - 4); + const align = []; + if (ver > 1) { + const n = Math.floor(ver / 7) + 2, step = ver === 32 ? 26 : Math.ceil((ver * 4 + 4) / (n * 2 - 2)) * 2; + align.push(6); + for (let pos = size - 7; align.length < n; pos -= step) align.splice(1, 0, pos); + } + align.forEach((ax, i) => align.forEach((ay, j) => { + if ((i === 0 && j === 0) || (i === 0 && j === align.length - 1) || (i === align.length - 1 && j === 0)) return; + for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1); + })); + const format = (m) => { + const d = (0 << 3) | m; // level M has format bits 00 + let rem = d; + for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537); + const b = ((d << 10) | rem) ^ 0x5412; + for (let i = 0; i <= 5; i++) set(8, i, bit(b, i)); + set(8, 7, bit(b, 6)); set(8, 8, bit(b, 7)); set(7, 8, bit(b, 8)); + for (let i = 9; i < 15; i++) set(14 - i, 8, bit(b, i)); + for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(b, i)); + for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(b, i)); + set(8, size - 8, true); + }; + format(0); + if (ver >= 7) { + let rem = ver; + for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25); + const b = (ver << 12) | rem; + for (let i = 0; i < 18; i++) { const a = size - 11 + (i % 3), c = Math.floor(i / 3); set(a, c, bit(b, i)); set(c, a, bit(b, i)); } + } + // the data, in the zigzag order + let i = 0; + for (let right = size - 1; right >= 1; right -= 2) { + if (right === 6) right = 5; + for (let vert = 0; vert < size; vert++) for (let j = 0; j < 2; j++) { + const x = right - j, up = ((right + 1) & 2) === 0, y = up ? size - 1 - vert : vert; + if (!fn[y][x] && i < codewords.length * 8) { mod[y][x] = bit(codewords[i >>> 3], 7 - (i & 7)); i++; } + } + } + const masks = [(x, y) => (x + y) % 2 === 0, (x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0, + (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0, + (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0]; + for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!fn[y][x] && masks[mask](x, y)) mod[y][x] = !mod[y][x]; + format(mask); + return mod; + } + + /** How hard a pattern is to scan (lower is better): runs, blocks, finder look-alikes and balance. */ + function penalty(m) { + const n = m.length; + let p = 0, dark = 0; + const lines = [...m, ...m[0].map((_, x) => m.map((row) => row[x]))]; + for (const line of lines) { + let run = 1; + for (let i = 1; i <= n; i++) { + if (i < n && line[i] === line[i - 1]) run++; + else { if (run >= 5) p += run - 2; run = 1; } + } + const s = line.map((v) => (v ? 1 : 0)).join(""); + for (const pat of ["10111010000", "00001011101"]) for (let k = s.indexOf(pat); k !== -1; k = s.indexOf(pat, k + 1)) p += 40; + } + for (let y = 0; y < n - 1; y++) for (let x = 0; x < n - 1; x++) { + const c = m[y][x]; + if (c === m[y][x + 1] && c === m[y + 1][x] && c === m[y + 1][x + 1]) p += 3; + } + for (const row of m) for (const v of row) if (v) dark++; + p += Math.floor(Math.abs(dark * 20 - n * n * 10) / (n * n)) * 10; + return p; + } + + function matrix(text, mask) { + const { ver, codewords } = encodeData([...new TextEncoder().encode(text)]); + if (mask != null) return build(ver, codewords, mask); + let best = null, bestScore = Infinity; + for (let m = 0; m < 8; m++) { const mm = build(ver, codewords, m), sc = penalty(mm); if (sc < bestScore) { best = mm; bestScore = sc; } } + return best; + } + + function svg(text, { border = 3, label = "QR code" } = {}) { + const m = matrix(text), n = m.length + border * 2; + let d = ""; + m.forEach((row, y) => row.forEach((v, x) => { if (v) d += `M${x + border} ${y + border}h1v1h-1z`; })); + return ``; + } + + const api = { matrix, svg }; + if (typeof window !== "undefined") window.QR = api; + if (typeof module !== "undefined") module.exports = api; +})(); diff --git a/dashboard_studio/web/style.css b/dashboard_studio/web/style.css index 57eb5f1..440a48e 100644 --- a/dashboard_studio/web/style.css +++ b/dashboard_studio/web/style.css @@ -1093,6 +1093,238 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } } @container (max-height: 120px) { .tz.tz .tz-ctrls { display: none; } } +/* parcels */ +.pc { display: flex; flex-direction: column; gap: 10px; min-height: 0; flex: 1; } +.pc-sum { font-size: 14px; color: var(--d-text-2); } +.pc-sum b { font-size: 22px; color: var(--d-text); margin-right: 2px; } +.pc-today { color: var(--d-accent); font-weight: 600; } +.pc-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; min-height: 0; } +.pc-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--d-fill); color: inherit; text-decoration: none; min-width: 0; } +.pc-row.today { background: color-mix(in srgb, var(--d-accent) 14%, transparent); } +.pc-row.done { opacity: .6; } +.pc-ico { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--d-tile); font-size: 17px; flex: none; } +.pc-row.today .pc-ico { color: var(--d-accent); } +.pc-row.done .pc-ico { color: var(--ok); } +.pc-text { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.pc-name { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pc-status { font-size: 12px; color: var(--d-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pc-when { font-size: 12px; font-weight: 600; color: var(--d-text-2); white-space: nowrap; text-align: right; } +.pc-row.today .pc-when { color: var(--d-accent); } + +/* air quality */ +.aq { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; min-height: 0; } +.aq-row { --aq: var(--ok); display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--d-fill); } +.aq-row.lvl1 { --aq: var(--warn); } .aq-row.lvl2 { --aq: var(--crit); } +.aq-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--aq); box-shadow: 0 0 0 4px color-mix(in srgb, var(--aq) 22%, transparent); } +.aq-room { display: flex; flex-direction: column; min-width: 0; } +.aq-room b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.aq-room span { font-size: 12px; color: var(--d-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.aq-row.lvl2 .aq-room span { color: var(--crit); font-weight: 600; } +.aq-vals { display: flex; gap: 4px; } +.aq-val { display: flex; flex-direction: column; align-items: center; min-width: 44px; padding: 3px 6px; border-radius: 9px; background: var(--d-tile); } +.aq-val small { font-size: 10px; color: var(--d-text-2); } +.aq-val span { font-size: 13px; font-weight: 600; } +.aq-val.lvl1 span { color: var(--warn); } .aq-val.lvl2 span { color: var(--crit); } +@container (max-width: 300px) { .aq-val small { display: none; } .aq-val { min-width: 36px; } } + +/* water and gas */ +.us-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; min-width: 0; } +.us-head .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.us-ico { width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; font-size: 16px; color: #0a84ff; background: color-mix(in srgb, #0a84ff 16%, transparent); flex: none; } +.us-ico.gas { color: #ff9f0a; background: color-mix(in srgb, #ff9f0a 16%, transparent); } +.us-ico.electricity { color: var(--ok); background: color-mix(in srgb, var(--ok) 16%, transparent); } +.us { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; } +.us-now { display: flex; flex-direction: column; gap: 2px; } +.us-big { font-size: clamp(26px, 9cqw, 38px); font-weight: 700; letter-spacing: -0.02em; line-height: 1; } +.us-big small { font-size: .45em; font-weight: 600; color: var(--d-text-2); margin-left: 3px; } +.us-cmp { font-size: 12px; color: var(--d-text-2); } +.us-cmp .up { color: var(--warn); } .us-cmp .down { color: var(--ok); } +.us-bars { position: relative; flex: 1; min-height: 40px; display: flex; align-items: stretch; gap: 4px; } +.us-day { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 4px; min-width: 0; } +.us-bar { width: 100%; max-width: 26px; border-radius: 5px 5px 2px 2px; background: color-mix(in srgb, var(--d-text) 18%, transparent); } +.us-day.today .us-bar { background: #0a84ff; } +.us.gas .us-day.today .us-bar { background: #ff9f0a; } +.us.electricity .us-day.today .us-bar { background: var(--ok); } +.us-lbl { font-size: 10px; color: var(--d-text-2); } +.us-avg { position: absolute; left: 0; right: 0; height: 0; border-top: 1px dashed var(--d-text-2); opacity: .6; margin-bottom: 18px; pointer-events: none; } +.us-foot { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--d-text-2); } +@container (max-height: 200px) { .us-bars, .us-foot { display: none; } } + +/* solar forecast */ +.sf { display: flex; flex-direction: column; gap: 12px; flex: 1; min-height: 0; } +.sf-top { display: flex; align-items: flex-end; gap: 18px; flex-wrap: wrap; } +.sf-top > div { display: flex; flex-direction: column; } +.sf-lbl { font-size: 12px; color: var(--d-text-2); } +.sf-big { font-size: clamp(28px, 10cqw, 42px); font-weight: 700; letter-spacing: -0.02em; line-height: 1; } +.sf-mid { font-size: 18px; font-weight: 600; } +.sf-big small, .sf-mid small { font-size: .5em; color: var(--d-text-2); margin-left: 3px; font-weight: 600; } +.sf-bars { flex: 1; min-height: 50px; display: flex; align-items: stretch; gap: 3px; padding-bottom: 14px; } +.sf-h { position: relative; flex: 1; display: flex; align-items: flex-end; } +.sf-h i { width: 100%; border-radius: 4px 4px 1px 1px; background: color-mix(in srgb, #ffcc00 55%, transparent); } +.sf-h.past i { opacity: .45; } +.sf-h.best i { background: #ff9f0a; } +.sf-h.now i { background: #ffcc00; box-shadow: 0 0 0 2px var(--d-tile), 0 0 0 3px #ffcc00; } +.sf-h span { position: absolute; bottom: -15px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--d-text-2); } +.sf-best { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; padding: 8px 10px; border-radius: 12px; background: color-mix(in srgb, #ff9f0a 14%, transparent); } +.sf-best .i { color: #ff9f0a; font-size: 18px; flex: none; } +@container (max-height: 220px) { .sf-bars { display: none; } } + +/* garage and gate */ +.gt { display: grid; grid-template-rows: auto 1fr auto; gap: 8px; height: 100%; min-height: 0; } +.gt-head { display: flex; align-items: center; gap: 8px; min-width: 0; } +.gt-head .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.gt-state { font-size: 12px; font-weight: 600; color: var(--d-text-2); } +.gt.open .gt-state { color: var(--warn); } +.gt-btn { justify-self: center; align-self: center; display: flex; flex-direction: column; align-items: center; gap: 6px; min-height: 0; } +.gt-ring { width: clamp(64px, 34cqmin, 116px); aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; font-size: clamp(28px, 14cqmin, 50px); + background: var(--d-fill); box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--d-text) 10%, transparent); transition: background-color .25s, color .25s, transform .15s; } +.gt-btn:active .gt-ring { transform: scale(.96); } +.gt.open .gt-ring { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn); box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--warn) 55%, transparent); } +.gt.moving .gt-ring { animation: gt-pulse 1.2s ease-in-out infinite; } +@keyframes gt-pulse { 50% { opacity: .55; } } +.gt-lbl { font-size: 14px; font-weight: 600; } +.gt-since { font-size: 12px; color: var(--d-text-2); text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +@media (prefers-reduced-motion: reduce) { .gt.moving .gt-ring { animation: none; } } + +/* sports */ +.sp { display: flex; flex-direction: column; gap: 8px; height: 100%; justify-content: center; min-height: 0; } +.sp-league { font-size: 12px; font-weight: 600; color: var(--d-text-2); text-align: center; text-transform: uppercase; letter-spacing: .04em; } +.sp-match { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; } +.sp-team { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; text-align: center; } +.sp-team img { width: clamp(36px, 16cqmin, 64px); aspect-ratio: 1; object-fit: contain; } +.sp-badge { width: clamp(36px, 16cqmin, 64px); aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; background: var(--d-fill); font-weight: 700; font-size: 13px; } +.sp-team > span:last-child { font-size: 13px; font-weight: 600; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sp-team.win > span:last-child { color: var(--ok); } +.sp-mid { display: flex; flex-direction: column; align-items: center; gap: 2px; } +.sp-score { font-size: clamp(26px, 11cqmin, 40px); font-weight: 700; letter-spacing: -0.02em; } +.sp-score i { font-style: normal; color: var(--d-text-2); margin: 0 6px; } +.sp-time { font-size: clamp(22px, 9cqmin, 32px); font-weight: 700; } +.sp-sub { font-size: 12px; color: var(--d-text-2); white-space: nowrap; } +.sp-live { color: var(--crit); } +.sp-live::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--crit); margin-right: 4px; vertical-align: 1px; animation: tz-pulse 1.4s infinite; } +.sp-extra { font-size: 12px; color: var(--d-text-2); text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* Formula One */ +.f1 { display: flex; flex-direction: column; gap: 10px; height: 100%; min-height: 0; } +.f1-next { padding: 12px; border-radius: 14px; background: linear-gradient(135deg, color-mix(in srgb, #e10600 22%, var(--d-fill)), var(--d-fill)); } +.f1-top { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--d-text-2); } +.f1-top .i { color: #e10600; font-size: 15px; } +.f1-top b { margin-left: auto; color: var(--d-text); font-weight: 600; } +.f1-race { font-size: 18px; font-weight: 700; margin-top: 4px; letter-spacing: -0.01em; } +.f1-place { font-size: 12px; color: var(--d-text-2); } +.f1-sessions { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; font-size: 12px; } +.f1-sessions > div { display: flex; justify-content: space-between; gap: 8px; } +.f1-sessions .past { opacity: .5; } +.f1-sessions .next { font-weight: 700; color: var(--d-text); } +.f1-tabs { display: flex; gap: 4px; padding: 3px; border-radius: 10px; background: var(--d-fill); } +.f1-tabs button { flex: 1; padding: 5px 4px; border-radius: 8px; font-size: 12px; font-weight: 600; color: var(--d-text-2); } +.f1-tabs button[aria-selected="true"] { background: var(--d-tile); color: var(--d-text); box-shadow: 0 1px 3px rgba(0,0,0,.12); } +.f1-list { display: flex; flex-direction: column; overflow-y: auto; min-height: 0; } +.f1-row { display: grid; grid-template-columns: 26px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--d-separator); } +.f1-row:last-child { border-bottom: 0; } +.f1-pos { font-weight: 700; color: var(--d-text-2); text-align: center; } +.f1-row:first-child .f1-pos { color: #e10600; } +.f1-name { display: flex; flex-direction: column; min-width: 0; } +.f1-name b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.f1-name span { font-size: 11px; color: var(--d-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.f1-val { font-size: 13px; font-weight: 600; } +@container (max-height: 380px) { .f1-sessions { display: none; } } + +/* announcements */ +#announce { position: fixed; inset: 0; z-index: 95; display: grid; place-items: center; padding: 24px; background: rgba(0, 0, 0, .72); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); } +#announce[hidden] { display: none; } +.an-card { width: min(640px, 100%); display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; color: #fff; } +.an-ico { width: 84px; height: 84px; border-radius: 50%; display: grid; place-items: center; font-size: 40px; background: var(--accent); color: #fff; animation: an-pop .5s cubic-bezier(.2, 1.4, .4, 1); } +@keyframes an-pop { from { transform: scale(.4); opacity: 0; } } +.an-title { font-size: 18px; font-weight: 600; opacity: .8; } +.an-msg { font-size: clamp(30px, 6vw, 56px); font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; white-space: pre-line; } +.an-ok { min-height: 52px; min-width: 160px; font-size: 18px; margin-top: 10px; } +@media (prefers-reduced-motion: reduce) { .an-ico { animation: none; } } +#btn-guest[hidden] { display: none; } +#btn-guest { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); } + +/* editor: alignment guides, presets, templates, undo history */ +.guide { position: absolute; z-index: 6; pointer-events: none; background: #ff2d92; } +.guide.v { top: 0; bottom: 0; width: 1px; } +.guide.h { left: 0; right: 0; height: 1px; } +.pal-sub { grid-column: 1 / -1; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-2); margin: 6px 2px 0; } +.pal-preset { position: relative; display: grid; } +.pal-del { position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; border-radius: 50%; display: none; place-items: center; font-size: 12px; background: var(--surface); color: var(--text-2); } +.pal-preset:hover .pal-del, .pal-del:focus-visible { display: grid; } +.modal-card:has(.tpl-grid) { width: 640px; } +.tpl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; margin-top: 14px; max-height: 52vh; overflow-y: auto; } +.tpl { display: flex; gap: 12px; align-items: flex-start; padding: 12px; border-radius: 14px; border: 1px solid var(--separator); cursor: pointer; } +.tpl input { position: absolute; opacity: 0; pointer-events: none; } +.tpl:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); } +.tpl:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; } +.tpl-ico { width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; font-size: 20px; background: var(--fill); flex: none; } +.tpl > span:last-child { display: flex; flex-direction: column; gap: 2px; font-size: 13px; color: var(--text-2); } +.tpl b { color: var(--text); font-size: 14px; } +.undo-row { width: 100%; text-align: left; border-radius: 10px; } +.undo-row:hover { background: var(--fill); } + +/* share */ +.share-qr { width: 220px; max-width: 70%; margin: 14px auto 12px; border-radius: 12px; overflow: hidden; box-shadow: 0 0 0 1px var(--separator); } +.share-qr svg { display: block; width: 100%; height: auto; } +.share-row { display: flex; gap: 8px; } +.share-row .input { flex: 1; min-width: 0; font-size: 13px; } +.share-row .btn { flex: none; } + +/* offline note: visible without the top bar too (phones, kiosk) */ +#offline-note { position: fixed; z-index: 85; left: 50%; top: calc(10px + env(safe-area-inset-top)); transform: translateX(-50%); padding: 6px 14px; border-radius: 99px; + font-size: 13px; font-weight: 600; color: #fff; background: rgba(28, 28, 30, .86); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 16px rgba(0, 0, 0, .25); max-width: calc(100% - 32px); text-align: center; } +#offline-note[hidden] { display: none; } +body.editing #offline-note { display: none; } + +/* pull to refresh */ +#ptr { position: fixed; z-index: 86; left: 50%; top: calc(8px + env(safe-area-inset-top)); width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; + background: var(--surface); color: var(--text-2); box-shadow: 0 4px 16px rgba(0, 0, 0, .2); transform: translate(-50%, 0); pointer-events: none; } +#ptr[hidden] { display: none; } +#ptr svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +#ptr.ready { color: var(--accent); } +#ptr.spin svg { animation: ptr-spin .8s linear infinite; } +@keyframes ptr-spin { to { transform: rotate(360deg); } } + +/* popups as bottom sheets on phones */ +@media (max-width: 600px) { + .modal { align-items: flex-end; padding: 0; } + .modal-card, .modal-card:has(.tpl-grid) { width: 100%; max-height: 90vh; border-radius: 20px 20px 0 0; padding: 26px 18px calc(18px + env(safe-area-inset-bottom)); transition: transform .25s ease; animation: sheet-up .3s cubic-bezier(.2, .9, .3, 1); } + .modal-card::before { content: ""; position: sticky; top: -18px; display: block; width: 38px; height: 5px; border-radius: 3px; background: var(--separator); margin: -14px auto 12px; } + .modal-actions { flex-wrap: wrap; } + .modal-actions .btn { flex: 1 1 auto; min-height: 46px; } +} +@keyframes sheet-up { from { transform: translateY(100%); } } +@media (prefers-reduced-motion: reduce) { .modal-card { animation: none !important; } } + +/* phone editing: the bar with "Add widget" and the settings */ +#edit-bar { display: none; position: fixed; z-index: 63; left: 50%; bottom: calc(14px + env(safe-area-inset-bottom)); transform: translateX(-50%); gap: 8px; padding: 6px; + border-radius: 99px; background: var(--glass); border: 1px solid var(--glass-edge); box-shadow: 0 8px 30px rgba(0, 0, 0, .25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); } +#edit-bar { max-width: calc(100% - 24px); } +#edit-bar .btn { min-height: 44px; border-radius: 99px; padding: 0 18px; white-space: nowrap; } +#sheet-backdrop { position: fixed; inset: 0; z-index: 61; background: rgba(0, 0, 0, .35); } +#sheet-backdrop[hidden] { display: none; } +.sheet-close { display: none; position: sticky; top: 0; float: right; width: 34px; height: 34px; border-radius: 50%; place-items: center; font-size: 20px; z-index: 2; } +body:not(.editing) #edit-bar { display: none !important; } +@media (pointer: coarse) { + body.editing .w.selected { touch-action: none; } + body.editing .w.selected .w-resize { width: 34px; height: 34px; } + body.editing .w-tools button { width: 38px; height: 38px; } +} +@media (prefers-reduced-motion: reduce) { body.editing #palette, body.editing #inspector { transition: none; } } + +/* suggestions */ +.pal-sug { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; padding: 9px 10px; border-radius: 12px; text-align: left; font-size: 13px; font-weight: 500; + color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); } +.pal-sug .i { font-size: 17px; flex: none; } +.sg-row { display: flex; align-items: center; gap: 10px; padding: 8px 2px; border-bottom: 1px solid var(--separator); } +.sg-row:last-child { border-bottom: 0; } +.sg-row .grow { display: flex; flex-direction: column; min-width: 0; } +.sg-row b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sg-row .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sg-ico { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--fill); font-size: 17px; flex: none; } +.sg-row .btn { flex: none; } +body:not(.editing) .w { -webkit-touch-callout: none; } + /* ============================================================ phone dashboards */ body.phone-ui:not(.peek) #topbar { display: none; } body.phone-ui #stage { padding-top: calc(env(safe-area-inset-top) + 6px); } @@ -1160,10 +1392,16 @@ body.kiosk.idle, body.kiosk.idle * { cursor: none !important; } body.editing #stage { flex-direction: column; align-items: stretch; } #workspace { flex-direction: column; } body.editing #stage { order: 1; flex: 1; } - body.editing #palette { order: 2; width: auto; max-height: 120px; padding: 10px; border: 0; border-top: 1px solid var(--separator); } - body.editing #palette .panel-title, body.editing #palette .hint { display: none; } - body.editing #palette-list { grid-template-columns: repeat(4, 1fr); } - body.editing #inspector { order: 3; width: auto; max-height: 40vh; padding: 14px; border: 0; border-top: 1px solid var(--separator); } + /* editing on a phone: the widget list and the settings slide up as sheets */ + body.editing #palette, body.editing #inspector { position: fixed; left: 0; right: 0; bottom: 0; z-index: 62; width: auto; height: 72vh; max-height: none; overflow-y: auto; + padding: 16px 16px calc(84px + env(safe-area-inset-bottom)); border: 0; border-radius: 20px 20px 0 0; background: var(--surface); box-shadow: 0 -10px 40px rgba(0, 0, 0, .3); + transform: translateY(105%); transition: transform .3s cubic-bezier(.2, .9, .3, 1); } + body.editing.sheet-palette #palette, body.editing.sheet-inspector #inspector { transform: none; } + body.editing #palette .hint { display: none; } + body.editing #palette-list { grid-template-columns: repeat(3, 1fr); } + body.editing #edit-bar { display: flex; } + body.editing .sheet-close { display: grid; } + body.editing #canvas { padding-bottom: 90px; } .btn.label-wide span { display: none; } } diff --git a/dashboard_studio/web/sw.js b/dashboard_studio/web/sw.js new file mode 100644 index 0000000..7705392 --- /dev/null +++ b/dashboard_studio/web/sw.js @@ -0,0 +1,31 @@ +/* Dashboard Studio service worker: always the network first, so updates show straight away. + * When the network fails (or Home Assistant's ingress session has run out), it answers with + * the last good copy, so the app can start, show the last known states and reconnect. */ +const CACHE = "ds-__VERSION__"; +const SKIP = /\/(ws|api\/camera|api\/immich|api\/history)/; + +self.addEventListener("install", () => self.skipWaiting()); +self.addEventListener("activate", (e) => { + e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k.startsWith("ds-") && k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())); +}); +self.addEventListener("fetch", (e) => { + const req = e.request, url = new URL(req.url); + if (req.method !== "GET" || url.origin !== location.origin || SKIP.test(url.pathname)) return; + const page = req.mode === "navigate"; + e.respondWith((async () => { + const cache = await caches.open(CACHE); + try { + const res = await fetch(req); + if (res.ok) { + cache.put(page ? new Request(new URL("./", self.registration.scope)) : req, res.clone()); + return res; + } + if (!page) return res; + throw new Error(String(res.status)); + } catch (err) { + const hit = page ? await cache.match(new URL("./", self.registration.scope).href) : await cache.match(req); + if (hit) return hit; + throw err; + } + })()); +}); diff --git a/dashboard_studio/web/widgets.js b/dashboard_studio/web/widgets.js index 2f504df..2f7fb58 100644 --- a/dashboard_studio/web/widgets.js +++ b/dashboard_studio/web/widgets.js @@ -549,7 +549,7 @@ if (ctx.cache.key !== key) { ctx.cache.key = key; try { ctx.cache.items = (await ctx.ha.send({ type: "todo/item/list", entity_id: c.entity }))?.items || []; } catch { ctx.cache.items = ctx.cache.items || []; } - if (el.isConnected) ctx.rerender(); + ctx.rerender(); return; } // Swipe a row to the left to delete it (touch); the ✕ does the same with a mouse. @@ -630,7 +630,7 @@ cache.key = c.entity; cache.at = Date.now(); const [daily, hourly] = await Promise.all([ctx.ha.forecast(c.entity, "daily").catch(() => null), ctx.ha.forecast(c.entity, "hourly").catch(() => null)]); cache.daily = daily; cache.hourly = hourly; - if (el.isConnected) ctx.rerender(); + ctx.rerender(); }, }; @@ -1266,6 +1266,356 @@ }, }; + // ---------------------------------------------------------------- parcels + // Parcel integrations (PostNL, DHL, …) keep their parcels as lists in a sensor's attributes; + // field names differ, so each parcel is read from whichever names it has. + const pick = (o, ...keys) => { for (const k of keys) if (o?.[k] != null && o[k] !== "") return o[k]; return null; }; + const dayLabel = (d) => { + const today = new Date(); today.setHours(0, 0, 0, 0); + const diff = Math.round((new Date(d).setHours(0, 0, 0, 0) - today) / 86400000); + return diff === 0 ? t("Today") : diff === 1 ? t("Tomorrow") : diff === -1 ? t("Yesterday") : new Date(d).toLocaleDateString(LOCALE, { weekday: "short", day: "numeric", month: "short" }); + }; + function parcelsOf(s) { + const out = []; + for (const [k, v] of Object.entries(s.attributes)) { + if (!Array.isArray(v) || !v.length || typeof v[0] !== "object") continue; + const listDelivered = /deliver|bezorgd|afgeleverd/i.test(k) && !/route|onderweg|transit/i.test(k); + for (const p of v) { + const status = String(pick(p, "status_message", "status", "state", "statusText") || ""); + const when = pick(p, "expected_datetime", "planned_date", "delivery_date", "expected_delivery", "eta", "date", "planned_from"); + const date = when && !isNaN(new Date(when)) ? new Date(when) : null; + out.push({ + name: String(pick(p, "name", "title", "description", "sender", "shop", "product", "key", "tracking_number", "barcode") || t("Parcel")), + status, date, until: pick(p, "planned_to", "expected_to"), + done: listDelivered || p.delivered === true || /delivered|bezorgd|afgeleverd/i.test(status), + url: pick(p, "url", "track_url", "tracking_url"), + }); + } + } + return out; + } + + W.parcels = { + label: "Parcels", icon: "package", size: [4, 4], + fields: [ + { k: "title", t: "text", l: "Title", d: "Parcels" }, + { k: "entities", t: "entities", l: "Parcel sensors (PostNL, DHL, …)", domains: ["sensor"] }, + { k: "showDelivered", t: "check", l: "Show parcels delivered in the last two days", d: true }, + ], + entities: (c) => c.entities || [], + render(c, { ha }) { + const list = (c.entities || []).map((e) => ha.states[e]).filter(Boolean); + if (!list.length) return `${title(c.title)}${missing((c.entities || [])[0], "a parcel sensor")}`; + let items = list.flatMap((s) => { + const ps = parcelsOf(s); + return ps.length || !isFinite(Number(s.state)) ? ps : []; // a count with no list means nothing on the way + }); + const recent = Date.now() - 2 * 86400000; + if (c.showDelivered === false) items = items.filter((p) => !p.done); + else items = items.filter((p) => !p.done || !p.date || p.date.getTime() > recent); + items.sort((a, b) => a.done - b.done || (a.date?.getTime() ?? 9e15) - (b.date?.getTime() ?? 9e15)); + const coming = items.filter((p) => !p.done), today = coming.filter((p) => p.date && dayLabel(p.date) === t("Today")).length; + const hm2 = (d) => new Date(d).toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" }); + const when = (p) => { + if (!p.date) return ""; + const hasTime = p.date.getHours() || p.date.getMinutes(); + return `${dayLabel(p.date)}${hasTime ? ` ${hm2(p.date)}${p.until && !isNaN(new Date(p.until)) ? `–${hm2(p.until)}` : ""}` : ""}`; + }; + const rows = items.map((p) => `<${p.url ? `a href="${esc(p.url)}" target="_blank" rel="noopener"` : "div"} class="pc-row ${p.done ? "done" : ""} ${p.date && dayLabel(p.date) === t("Today") && !p.done ? "today" : ""}"> + ${Icons.svg(p.done ? "check" : "package")} + ${esc(p.name)}${esc(p.done ? t("Delivered") : p.status || t("On the way"))} + ${esc(when(p))}`).join(""); + return `${title(c.title)}
+
${coming.length} ${esc(coming.length === 1 ? t("parcel on the way") : t("parcels on the way"))}${today ? ` · ${esc(t("{0} today", today))}` : ""}
+ ${rows ? `
${rows}
` : `
${t("No parcels on the way.")}
`}
`; + }, + }; + + // ---------------------------------------------------------------- air quality + const AIR = { + carbon_dioxide: { k: "co2", l: "CO₂", lv: (v) => (v <= 800 ? 0 : v <= 1200 ? 1 : 2) }, + pm25: { k: "pm25", l: "PM2.5", lv: (v) => (v <= 12 ? 0 : v <= 35 ? 1 : 2) }, + pm10: { k: "pm10", l: "PM10", lv: (v) => (v <= 20 ? 0 : v <= 50 ? 1 : 2) }, + volatile_organic_compounds: { k: "voc", l: "VOC", lv: (v) => (v <= 300 ? 0 : v <= 1000 ? 1 : 2) }, + volatile_organic_compounds_parts: { k: "voc", l: "VOC", lv: (v) => (v <= 300 ? 0 : v <= 1000 ? 1 : 2) }, + humidity: { k: "hum", l: "Humidity", lv: (v) => (v >= 40 && v <= 60 ? 0 : v >= 30 && v <= 70 ? 1 : 2) }, + }; + const AIR_WORDS = /\b(co2|carbon dioxide|pm2[.,]?5|pm10|voc|humidity|luchtvochtigheid|vochtigheid|air quality|luchtkwaliteit)\b/gi; + + W.air = { + label: "Air quality", icon: "air", size: [4, 4], + fields: [ + { k: "title", t: "text", l: "Title", d: "Air quality" }, + { k: "entities", t: "entities", l: "Sensors (leave empty to find CO₂, fine dust and humidity per room)", domains: ["sensor"] }, + ], + entities: (c, ha) => (c.entities?.length ? c.entities : Object.values(ha?.states || {}).filter((s) => AIR[s.attributes.device_class]).map((s) => s.entity_id)), + render(c, { ha }) { + const reg = ha.reg; + const areaOf = {}; + if (reg) for (const [a, ids] of Object.entries(reg.byArea)) for (const id of ids) areaOf[id] = reg.areas.find((x) => x.area_id === a)?.name; + const sensors = (c.entities?.length ? c.entities : W.air.entities(c, ha)).map((e) => ha.states[e]).filter((s) => s && AIR[s.attributes.device_class] && isFinite(Number(s.state))); + // Without chosen sensors, only rooms count; with chosen ones, a sensor's name is its room when it has none. + const rooms = new Map(); + for (const s of sensors) { + const room = areaOf[s.entity_id] || (c.entities?.length ? friendly(s).replace(AIR_WORDS, "").replace(/\s+/g, " ").trim() || friendly(s) : null); + if (!room) continue; + (rooms.get(room) || rooms.set(room, []).get(room)).push(s); + } + if (!rooms.size) return `${title(c.title)}
${t("No air quality sensors found. Add CO₂, fine dust or humidity sensors in the inspector.")}
`; + const names = ["Good", "Fair", "Poor"]; + const rows = [...rooms.entries()].map(([room, ss]) => { + const m = ss.map((s) => { const d = AIR[s.attributes.device_class], v = Number(s.state); return { ...d, v, lvl: d.lv(v), s }; }); + const worst = Math.max(...m.map((x) => x.lvl)); + const co2 = m.find((x) => x.k === "co2")?.v, hum = m.find((x) => x.k === "hum")?.v, pm = m.find((x) => (x.k === "pm25" || x.k === "pm10") && x.lvl === 2); + const hint = co2 >= 1000 ? t("Open a window") : pm ? t("Air the room or turn on the purifier") : hum > 70 ? t("Too humid, air the room") : hum < 30 ? t("The air is dry") : ""; + return `
+ ${esc(room)}${esc(hint || t(names[worst]))} + ${m.map((x) => ``).join("")}
`; + }).join(""); + return `${title(c.title)}
${rows}
`; + }, + }; + + // ---------------------------------------------------------------- water and gas + // Daily use from Home Assistant's long-term statistics of a meter (the same numbers the energy dashboard uses). + W.usage = { + label: "Water and gas", icon: "droplet", size: [4, 3], + fields: [ + { k: "entity", t: "entity", l: "Meter (water, gas or energy)", domains: ["sensor"] }, + { k: "title", t: "text", l: "Title", ph: "Entity name" }, + { k: "kind", t: "seg", l: "Type", o: ["water", "gas", "electricity"], d: "water" }, + { k: "days", t: "number", l: "Days to show", d: 7 }, + ], + render(c, { ha, cache }) { + const s = ha.states[c.entity]; + if (!s) return missing(c.entity, "a meter"); + let unit = s.attributes.unit_of_measurement || ""; + // Water reads better in litres than in thousandths of a cubic metre. + const liters = (c.kind || "water") === "water" && unit === "m³"; + if (liters) unit = "L"; + const ico = { water: "droplet", gas: "flame", electricity: "bolt" }[c.kind] || "droplet"; + const head = `
${Icons.svg(ico)}${esc(c.title || friendly(s))}
`; + if (cache.error) return `${head}
${t("No statistics for this meter. It needs a state class (total or total_increasing).")}
`; + if (!cache.days) return `${head}
${t("Loading…")}
`; + const days = liters ? cache.days.map((d) => ({ ...d, v: Math.round(d.v * 1000) })) : cache.days; + const today = days[days.length - 1]?.v ?? 0, yest = days[days.length - 2]?.v; + const max = Math.max(...days.map((d) => d.v), 0.0001), avg = days.slice(0, -1).reduce((a, d) => a + d.v, 0) / Math.max(1, days.length - 1); + return `${head}
+
${esc(fmtNum(today))}${esc(unit)} + ${t("Today so far")}${yest != null ? ` · ${t("yesterday {0}", `${fmtNum(yest)} ${unit}`)}` : ""}
+ +
${t("{0} days", days.length)}: ${esc(fmtNum(days.reduce((a, d) => a + d.v, 0)))} ${esc(unit)}${t("Average {0}", `${fmtNum(avg)} ${unit}`)}
+
`; + }, + async after(el, c, ctx) { + if (!ctx.ha.states[c.entity]) return; + const cache = ctx.cache, n = Math.max(3, Math.min(31, Number(c.days) || 7)); + const key = `${c.entity}|${n}`; + if (cache.key === key && Date.now() - cache.at < 10 * 60 * 1000) return; + cache.key = key; cache.at = Date.now(); + try { + const rows = await ctx.ha.statistics(c.entity, n); + cache.days = rows.map((r) => ({ v: Math.max(0, Number(r.change) || 0), label: new Date(r.start).toLocaleDateString(LOCALE, { weekday: "short" }) })); + cache.error = !rows.length; + } catch { cache.error = true; } + ctx.rerender(); // the card may have been redrawn while loading: draw whichever one is on screen now + }, + }; + + // ---------------------------------------------------------------- solar forecast + // Forecast.Solar: the day totals come from its sensors, the hour by hour forecast from the energy API. + W.solar = { + label: "Solar forecast", icon: "solar", size: [4, 4], + fields: [ + { k: "title", t: "text", l: "Title", d: "Solar forecast" }, + { k: "today", t: "entity", l: "Production today (kWh)", domains: ["sensor"] }, + { k: "tomorrow", t: "entity", l: "Production tomorrow (kWh)", domains: ["sensor"] }, + { k: "now", t: "entity", l: "Power now (W)", domains: ["sensor"] }, + { k: "hours", t: "number", l: "Hours for the washer or dishwasher", d: 3 }, + ], + autofill: { + from: "today", + fill: (eid, ha) => { + const has = (e) => (ha.states[e] ? e : ""); + return { tomorrow: has(eid.replace("today", "tomorrow")), now: has(eid.replace("energy_production_today", "power_production_now")) }; + }, + }, + entities: (c) => [c.today, c.tomorrow, c.now].filter(Boolean), + render(c, { ha, cache }) { + const today = ha.states[c.today], tomorrow = ha.states[c.tomorrow], now = ha.states[c.now]; + if (!today && !tomorrow) return `${title(c.title)}${missing(c.today, "the production today sensor")}`; + const kwh = (s) => (s ? `${fmtNum(s.state)}${esc(s.attributes.unit_of_measurement || "kWh")}` : "–"); + const hours = cache.hours || []; + const day0 = new Date(); day0.setHours(0, 0, 0, 0); + const todays = hours.filter((h) => h.t >= day0.getTime() && h.t < day0.getTime() + 86400000); + const shown = todays.filter((h) => h.wh > 0 || (new Date(h.t).getHours() >= 7 && new Date(h.t).getHours() <= 20)); + const max = Math.max(...shown.map((h) => h.wh), 1), nowH = new Date().getHours(); + // the best window to run an appliance: the most sun in N hours in a row, from now on + const n = Math.max(1, Math.min(6, Number(c.hours) || 3)); + const ahead = hours.filter((h) => h.t >= Date.now() - 3600000); + let best = null; + for (let i = 0; i + n <= ahead.length; i++) { + const sum = ahead.slice(i, i + n).reduce((a, h) => a + h.wh, 0); + if (ahead[i + n - 1].t - ahead[i].t === (n - 1) * 3600000 && (!best || sum > best.sum)) best = { sum, from: ahead[i].t, to: ahead[i].t + n * 3600000 }; + } + const hmT = (ms) => new Date(ms).toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" }); + return `${title(c.title)}
+
${t("Today")}${kwh(today)}
+ ${tomorrow ? `
${t("Tomorrow")}${kwh(tomorrow)}
` : ""} + ${now ? `
${t("Now")}${esc(fmtState(now))}
` : ""}
+ ${shown.length ? `
${shown.map((h) => { const hr = new Date(h.t).getHours(); return `
= best.from && h.t < best.to ? "best" : ""}" title="${esc(`${hmT(h.t)}: ${fmtNum(h.wh / 1000)} kWh`)}">${hr % 3 === 0 ? `${hr}` : ""}
`; }).join("")}
` + : cache.loaded ? `
${t("The hour by hour forecast isn’t available.")}
` : ""} + ${best ? `
${Icons.svg("washer")}${esc(t("Best time for the washer: {0}", `${dayLabel(best.from) === t("Today") ? "" : dayLabel(best.from) + " "}${hmT(best.from)}–${hmT(best.to)}`))}
` : ""} +
`; + }, + async after(el, c, ctx) { + const cache = ctx.cache; + if (cache.at && Date.now() - cache.at < 30 * 60 * 1000) return; + cache.at = Date.now(); + try { + const res = await ctx.ha.solarForecast(); + const sum = {}; + for (const entry of Object.values(res || {})) for (const [iso, wh] of Object.entries(entry.wh_hours || {})) { const tm = new Date(iso).getTime(); sum[tm] = (sum[tm] || 0) + Number(wh || 0); } + cache.hours = Object.entries(sum).map(([tm, wh]) => ({ t: Number(tm), wh })).sort((a, b) => a.t - b.t); + } catch { cache.hours = []; } + cache.loaded = true; + ctx.rerender(); + }, + }; + + // ---------------------------------------------------------------- garage and gate + /** Who changed an entity last, from the user on its context (matched to a person). */ + function changedBy(s, ha) { + const uid = s?.context?.user_id; + if (!uid) return ""; + const p = Object.values(ha.states).find((x) => x.entity_id.startsWith("person.") && x.attributes.user_id === uid); + return p ? friendly(p) : ""; + } + + W.gate = { + label: "Garage and gate", icon: "garage", size: [3, 3], + fields: [ + { k: "entity", t: "entity", l: "Garage door, gate or lock", domains: ["cover", "lock", "switch", "button", "input_boolean"] }, + { k: "name", t: "text", l: "Name", ph: "Entity name" }, + { k: "kind", t: "seg", l: "Icon", o: ["garage", "gate", "door"], d: "garage" }, + { k: "confirm", t: "check", l: "Ask before opening or closing", d: true }, + ], + render(c, { ha }) { + const s = ha.states[c.entity]; + if (!s) return missing(c.entity, "a garage door or gate"); + const d = domainOf(c.entity), st0 = s.state; + const open = ["open", "opening", "unlocked", "unlocking", "on"].includes(st0), moving = ["opening", "closing", "locking", "unlocking"].includes(st0); + const name = c.name || friendly(s); + const label = d === "button" ? t("Press") : moving ? st(st0) : open ? (d === "lock" ? t("Lock") : t("Close")) : d === "lock" ? t("Unlock") : t("Open"); + const by = changedBy(s, ha); + const since = `${open ? t("Open") : st(st0)} · ${relTime(s.last_changed)}${by ? ` · ${t("by {0}", by)}` : ""}`; + const ico = { garage: "garage", gate: "gate", door: "door" }[c.kind] || "garage"; + return `
+
${esc(name)}${esc(open ? t("Open") : d === "button" ? "" : st(st0))}
+ +
${esc(since)}
`; + }, + }; + + // ---------------------------------------------------------------- sports (TeamTracker) + const isMatchDay = (s) => s && (s.state === "IN" || (s.attributes.date && dayLabel(s.attributes.date) === t("Today"))); + + W.sports = { + label: "Sports", icon: "trophy", size: [4, 3], + fields: [ + { k: "entity", t: "entity", l: "Team (TeamTracker sensor)", domains: ["sensor"] }, + { k: "matchDay", t: "check", l: "Only show on match days", d: false }, + { k: "venue", t: "check", l: "Show the venue and TV channel", d: true }, + ], + autoHide: (c, { ha }) => !!c.matchDay && !isMatchDay(ha.states[c.entity]), + render(c, { ha }) { + const s = ha.states[c.entity]; + if (!s) return missing(c.entity, "a TeamTracker sensor"); + const a = s.attributes, state = String(s.state).toUpperCase(); + if (state === "NOT_FOUND" || state === "BYE" || !a.opponent_name) return `
${esc(a.league || friendly(s))}
${state === "BYE" ? t("No match this round.") : t("No match planned.")}
`; + const team = { name: a.team_name, short: a.team_abbr, logo: a.team_logo, score: a.team_score, win: a.team_winner }, + opp = { name: a.opponent_name, short: a.opponent_abbr, logo: a.opponent_logo, score: a.opponent_score, win: a.opponent_winner }; + const [home, away] = a.team_homeaway === "away" ? [opp, team] : [team, opp]; + const side = (x) => `
${x.logo ? `` : `${esc((x.short || x.name || "?").slice(0, 3))}`}${esc(x.name || x.short || "")}
`; + const kick = a.date ? new Date(a.date) : null; + let mid; + if (state === "PRE") mid = `${kick ? esc(kick.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })) : "–"}${esc(kick ? dayLabel(kick) : "")}${a.kickoff_in ? ` · ${esc(a.kickoff_in)}` : ""}`; + else mid = `${esc(home.score ?? 0)}–${esc(away.score ?? 0)}${state === "IN" ? `${t("Live")} ${esc(a.clock || "")}` : t("Full time")}`; + const extra = c.venue !== false ? [a.venue, a.tv_network].filter(Boolean).join(" · ") : ""; + return `
${esc(a.league || a.sport || "")}${a.event_name && state !== "PRE" ? "" : ""}
+
${side(home)}
${mid}
${side(away)}
+ ${extra ? `
${esc(extra)}
` : ""}${state === "IN" && a.last_play ? `
${esc(a.last_play)}
` : ""}
`; + }, + }; + + // ---------------------------------------------------------------- Formula One (F1 Sensor integration) + // Its sensors carry the season data from the Jolpica (Ergast) API in their attributes. + const F1_SESSIONS = [["first_practice", "Practice 1"], ["second_practice", "Practice 2"], ["third_practice", "Practice 3"], ["sprint_qualifying", "Sprint qualifying"], ["sprint", "Sprint"], ["qualifying", "Qualifying"], ["race", "Race"]]; + const f1List = (s, ...keys) => { const a = s?.attributes || {}; for (const k of keys) if (Array.isArray(a[k])) return a[k]; return []; }; + const f1Driver = (r) => { const d = r.Driver || r.driver || {}; return typeof d === "string" ? d : [d.givenName, d.familyName].filter(Boolean).join(" ") || d.code || ""; }; + const f1Team = (r) => { const c = r.Constructor || (r.Constructors || [])[0] || r.constructor_name || r.team; return typeof c === "string" ? c : c?.name || ""; }; + + W.f1 = { + label: "Formula One", icon: "flag", size: [4, 5], + fields: [ + { k: "next", t: "entity", l: "Next race", domains: ["sensor"] }, + { k: "drivers", t: "entity", l: "Driver standings", domains: ["sensor"] }, + { k: "teams", t: "entity", l: "Constructor standings", domains: ["sensor"] }, + { k: "results", t: "entity", l: "Last race results", domains: ["sensor"] }, + { k: "show", t: "seg", l: "Show first", o: ["drivers", "teams", "results"], d: "drivers" }, + { k: "top", t: "number", l: "Positions to show", d: 5 }, + ], + autofill: { + from: "next", + fill: (eid, ha) => { + const pre = eid.replace(/next_race.*$/, ""), has = (e) => (ha.states[e] ? e : ""); + return { drivers: has(`${pre}driver_standings`), teams: has(`${pre}constructor_standings`), results: has(`${pre}last_race_results`) }; + }, + }, + entities: (c) => [c.next, c.drivers, c.teams, c.results].filter(Boolean), + render(c, { ha, cache }) { + const next = ha.states[c.next]; + if (!next && !c.drivers) return missing(c.next, "the next race sensor"); + const a = next?.attributes || {}; + const iso = (k) => a[`${k}_start`] || a[`${k}_date`] || a[`${k}_start_local`]; + const raceAt = iso("race") ? new Date(iso("race")) : null; + let countdown = ""; + if (raceAt && raceAt > Date.now()) { + const m = Math.round((raceAt - Date.now()) / 60000), dd = Math.floor(m / 1440), hh = Math.floor((m % 1440) / 60); + countdown = dd ? t("in {0} d {1} h", dd, hh) : t("in {0} h {1} min", hh, m % 60); + } + const when = (d) => `${d.toLocaleDateString(LOCALE, { weekday: "short", day: "numeric", month: "short" })} ${d.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })}`; + const sessions = F1_SESSIONS.map(([k, l]) => [l, iso(k)]).filter(([, v]) => v && !isNaN(new Date(v))); + const nextSession = sessions.find(([, v]) => new Date(v) > Date.now()); + const place = [a.circuit_name || a.Circuit?.circuitName, a.circuit_locality || a.Circuit?.Location?.locality, a.circuit_country || a.Circuit?.Location?.country].filter(Boolean).join(" · "); + const tab = cache.tab || c.show || "drivers", top = Math.max(1, Math.min(20, Number(c.top) || 5)); + const tabs = [["drivers", "Drivers", c.drivers], ["teams", "Teams", c.teams], ["results", "Last race", c.results]].filter(([, , e]) => e && ha.states[e]); + const cur = tabs.find(([k]) => k === tab) || tabs[0]; + let rows = ""; + if (cur) { + const s = ha.states[cur[2]]; + const list = cur[0] === "drivers" ? f1List(s, "driver_standings", "standings", "DriverStandings") + : cur[0] === "teams" ? f1List(s, "constructor_standings", "standings", "ConstructorStandings") : f1List(s, "results", "Results", "race_results"); + rows = list.slice(0, top).map((r) => { + const name = cur[0] === "teams" ? f1Team(r) : f1Driver(r); + const sub = cur[0] === "teams" ? (r.wins != null ? t("{0} wins", r.wins) : "") : f1Team(r); + const val = cur[0] === "results" ? (r.Time?.time || r.time || r.status || "") : `${r.points ?? ""} ${t("pts")}`; + return `
${esc(r.position ?? r.positionText ?? "")}${esc(name)}${esc(sub)}${esc(val)}
`; + }).join(""); + } + const raceName = a.race_name || a.raceName || (next && !/^\d{4}-/.test(next.state) ? next.state : ""); + return `
+ ${next ? `
${Icons.svg("flag")}${esc(a.season ? `${a.season} · ${t("Round {0}", a.round)}` : t("Next race"))}${countdown ? `${esc(countdown)}` : ""}
+
${esc(raceName || t("Next race"))}
${place ? `
${esc(place)}
` : ""} + ${sessions.length ? `
${sessions.map(([l, v]) => `
${esc(t(l))}${esc(when(new Date(v)))}
`).join("")}
` : ""}
` : ""} + ${tabs.length ? `
${tabs.map(([k, l]) => ``).join("")}
+
${rows || `
${t("No standings yet.")}
`}
` : ""}
`; + }, + }; + W.text = { label: "Text", icon: "text", size: [4, 2], fields: [