diff --git a/lith/Caddyfile b/lith/Caddyfile index fee24bff12..3e4639bc05 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -1042,7 +1042,7 @@ oskiewar.com, midi.oskiewar.com { # answers 304 against its ETag and loads from disk. Without the header the # browser's heuristic kept whole modules stale across deploys — a returning # visitor once ran a months-old round-room.mjs against the current relay. - @oskiewarmodules path /oskiewar.js /oskiewar-sfx.mjs /oskiewar-voice.mjs /oskiewar-midi.mjs + @oskiewarmodules path /oskiewar.js /oskiewar-sfx.mjs /oskiewar-voice.mjs /oskiewar-midi.mjs /oskiewar-wizard.mjs handle @oskiewarmodules { root * /opt/ac/xbox/live header Cache-Control no-cache diff --git a/system/netlify/functions/oskiewar-consent.mjs b/system/netlify/functions/oskiewar-consent.mjs new file mode 100644 index 0000000000..c1cb864e49 --- /dev/null +++ b/system/netlify/functions/oskiewar-consent.mjs @@ -0,0 +1,223 @@ +// The consent wall behind oskiewar's "add yourself" door. +// +// A player who wants to become a fighter hands over material — their face, +// how they move, their voice — and the thing that makes that a transaction +// rather than a surrender is that they say in advance which parts the game +// may use, for what, and for how long. This endpoint is where that answer is +// asked and recorded. It is the only path to a generation capability: no +// receipt, no capability, no job. +// +// REGARDE itself is not in this repository and must not be. The prover is +// client code under NDA, so what lives here is a bridge: it shapes the +// player's answer into the DATA_OPERATION × GRANT_CONSENT request the desk +// expects, forwards it, and returns the outcome. Every substantive decision +// is made on the other side of REGARDE_GATEWAY_URL. +// +// Three properties this file exists to hold: +// +// 1. Fail closed. An unset gateway URL, a timeout, a malformed answer, a +// non-allow outcome — every one of them returns no capability. There is +// no branch where a generation token is issued on a guess. +// 2. No raw identity crosses the wire. The desk keys grants by pseudonymous +// sub, and its gateway refuses payloads carrying names, handles, or +// emails outright. The player's Auth0 subject is hashed with a +// purpose-scoped salt before it leaves this process, so REGARDE learns a +// stable oskiewar pseudonym and never the person. +// 3. Nothing defaults on. Voice, movement, biography, marketing, +// merchandise, and model training are each their own line in the scope. +// A player who ticks "appearance" has not thereby agreed to be heard, +// advertised, or trained on. +// +// See the working proposal (vault: regarde/proposals/oskiewar-regarde) for +// the pipeline this gates and the fighter bundle it eventually produces. + +import { createHash } from "node:crypto"; +import { authorize } from "../../backend/authorization.mjs"; + +const GATEWAY_TIMEOUT_MS = 6000; +const SCHEMA_VERSION = 1; +const PURPOSE = "oskiewar_fighter_generation"; + +// The scope vocabulary, split the way the player experiences it. These are +// closed sets on purpose: an unknown output or distribution is a refusal, not +// a pass-through, because a term REGARDE has not registered is a term nobody +// has decided the meaning of. +export const SOURCES = ["appearance", "movement", "voice", "biography"]; +export const OUTPUTS = ["portrait", "fighter_mesh", "fighter_animation", "match_audio"]; +export const DISTRIBUTION = ["private_preview", "local_gameplay", "online_play", + "tournament_display"]; +// Kept out of the lists above because they are not degrees of the same thing. +// A player can want to be playable online and still never want to be an +// advertisement, and the UI has to be able to say so in those words. +export const SEPARATE = ["marketing", "merchandise", "model_training"]; + +// Retention is chosen, never inherited. The proposal is explicit that neither +// source nor derived media may default to indefinite, so the absence of a +// choice here is an error rather than a fallback. +const RETENTION = ["bound_to_purpose_scope", "pilot_deadline"]; + +function fail(statusCode, message) { + return { + statusCode, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ outcome: "refuse", capability: null, message }), + }; +} + +// A subset check that treats "not a list" and "contains something unregistered" +// as the same failure, because both mean the caller and the schema disagree. +function subsetOf(values, vocabulary) { + if (!Array.isArray(values)) return null; + const picked = [...new Set(values.map((value) => String(value)))]; + return picked.every((value) => vocabulary.includes(value)) ? picked : null; +} + +// The pseudonym. Salted so the same person is a different subject to a +// different purpose — an oskiewar grant cannot be correlated against any other +// REGARDE operation AC might one day run — and hashed so this side of the wire +// is the last place the Auth0 subject exists. +function pseudonym(sub, salt) { + return "sub_" + createHash("sha256").update(`${salt}:${PURPOSE}:${sub}`) + .digest("hex").slice(0, 32); +} + +// The frozen fields are the grant. Once this object is hashed into a receipt +// it is what the player actually agreed to, so it is built in one place and +// never edited downstream — a generation worker reads it, it does not amend it. +export function frozenFields(scope) { + return { + data_class: "digital_replica_source", + operation_kind: "GRANT_CONSENT", + purpose_scope: { + purpose: PURPOSE, + source: scope.source, + outputs: scope.outputs, + distribution: scope.distribution, + transform: "stylized", + ...Object.fromEntries(SEPARATE.map((key) => [key, scope[key] === true])), + }, + retention_constraint: scope.retention, + schema_version: SCHEMA_VERSION, + }; +} + +// Reading the player's answer. Everything is required and nothing is inferred: +// a missing field is a malformed ask, not an empty permission. +export function readScope(body) { + const source = subsetOf(body?.source, SOURCES); + if (!source?.length) return { error: "pick at least one kind of material" }; + const outputs = subsetOf(body?.outputs, OUTPUTS); + if (!outputs?.length) return { error: "pick at least one thing to make" }; + const distribution = subsetOf(body?.distribution, DISTRIBUTION); + if (!distribution?.length) return { error: "pick where it may appear" }; + if (!RETENTION.includes(body?.retention)) + return { error: "choose how long the material may be kept" }; + // The two couplings the proposal names by hand, because they are the ones a + // player would reasonably assume and be wrong about. Appearance does not + // imply voice; a voice output without the voice material is an ask for + // something nobody offered. + if (outputs.includes("match_audio") && !source.includes("voice")) + return { error: "match audio needs a voice sample you have offered" }; + if ((outputs.includes("fighter_mesh") || outputs.includes("fighter_animation")) && + !source.includes("appearance")) + return { error: "a body needs appearance references" }; + const scope = { source, outputs, distribution, retention: body.retention }; + for (const key of SEPARATE) scope[key] = body?.[key] === true; + return { scope }; +} + +export async function handler(event) { + if (event.httpMethod === "OPTIONS") + return { statusCode: 204, headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Authorization, Content-Type", + "Access-Control-Allow-Methods": "POST, OPTIONS" } }; + if (event.httpMethod !== "POST") return fail(405, "POST only."); + + // Anonymous play is the front door of this game and always will be, but it + // is not a door into a grant: a receipt has to belong to somebody who can + // later come back and withdraw it. + const user = await authorize(event.headers); + if (!user?.sub) return fail(401, "Sign in before you give anything away."); + + let body; + try { body = JSON.parse(event.body || "{}"); } + catch { return fail(400, "Unreadable answer."); } + + const { scope, error } = readScope(body); + if (error) return fail(400, error); + + const gateway = process.env.REGARDE_GATEWAY_URL; + const salt = process.env.REGARDE_SUBJECT_SALT; + // Unconfigured is refused rather than waved through. This is the branch that + // would otherwise quietly become "generation works, the wall is decorative." + if (!gateway || !salt) + return fail(503, "The consent desk is not reachable right now."); + + const request = { + venue: "oskiewar", + subject: pseudonym(user.sub, salt), + operation_type: "DATA_OPERATION", + frozen_fields: frozenFields(scope), + // Idempotent per subject per exact scope: asking the same question twice + // returns the same receipt instead of littering the chain with duplicates, + // while any change to the scope is a genuinely new ask. + idempotency_key: createHash("sha256") + .update(JSON.stringify([pseudonym(user.sub, salt), frozenFields(scope)])) + .digest("hex").slice(0, 32), + operation_descriptor: { + purpose: PURPOSE, + description: "Generate an oskiewar fighter from player-supplied material.", + }, + }; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS); + let answer; + try { + const upstream = await fetch(gateway, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + signal: controller.signal, + }); + if (!upstream.ok) { + console.log(`🚪 regarde gateway ${upstream.status}`); + return fail(502, "The consent desk refused the ask."); + } + answer = await upstream.json(); + } catch (err) { + console.log(`🚪 regarde gateway unreachable: ${err.message}`); + return fail(504, "The consent desk did not answer."); + } finally { clearTimeout(timer); } + + // "allow" is the only outcome that opens anything. "edit" means the desk + // countered with narrower terms the player would have to accept as a fresh + // ask, "deny" and "refuse" mean no. None of the three produces a capability + // here, and the client is expected to show the counter rather than retry. + const outcome = String(answer?.outcome || "refuse"); + const receipt = answer?.receipt + ? { + decision: answer.receipt.decision, + hash: answer.receipt.hash ?? answer.receipt.jti ?? null, + issued_at: answer.receipt.iat ?? null, + expires_at: answer.receipt.exp ?? null, + } + : null; + + return { + statusCode: 200, + headers: { "Content-Type": "application/json", + "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*" }, + body: JSON.stringify({ + outcome, + receipt, + scope: request.frozen_fields.purpose_scope, + counter: answer?.counter?.frozen_fields?.purpose_scope ?? null, + // Slice 1 proves the wall. The generation capability is the next thing + // to land behind it, and it stays null until there is a worker whose + // reach this grant can actually bound. + capability: null, + }), + }; +} diff --git a/system/tests/oskiewar-consent.test.mjs b/system/tests/oskiewar-consent.test.mjs new file mode 100644 index 0000000000..38b31e37bf --- /dev/null +++ b/system/tests/oskiewar-consent.test.mjs @@ -0,0 +1,97 @@ +// The consent wall's reading of a player's answer. +// +// The wall is the only door to a generation capability, so the interesting +// tests are the refusals: what a player did NOT tick must never survive into +// the frozen fields, and an ask the desk has no registered term for must fail +// rather than pass through. See the working proposal (vault: +// regarde/proposals/oskiewar-regarde) for the grant these fields become. +import assert from "node:assert/strict"; +import test from "node:test"; +import { readScope, frozenFields, SOURCES, OUTPUTS, DISTRIBUTION, SEPARATE } + from "../netlify/functions/oskiewar-consent.mjs"; + +// The narrowest answer the wall can send: look at me, make a picture, show +// nobody. Every test below is this with one thing changed. +const minimal = { + source: ["appearance"], + outputs: ["portrait"], + distribution: ["private_preview"], + retention: "bound_to_purpose_scope", +}; + +test("the minimal answer is a grant", () => { + const { scope, error } = readScope(minimal); + assert.equal(error, undefined); + assert.deepEqual(scope.source, ["appearance"]); +}); + +test("nothing defaults on", () => { + const { scope } = readScope(minimal); + // Marketing, merchandise and model training are the three a player is most + // likely to agree to by momentum. Silence is not agreement. + for (const key of SEPARATE) assert.equal(scope[key], false, key); + const frozen = frozenFields(scope).purpose_scope; + for (const key of SEPARATE) assert.equal(frozen[key], false, key); +}); + +test("an empty category is refused rather than read as permission", () => { + for (const key of ["source", "outputs", "distribution"]) + assert.ok(readScope({ ...minimal, [key]: [] }).error, key); +}); + +test("a term the schema has not registered fails the ask", () => { + // Not merely dropped: a word nobody has decided the meaning of must not + // travel to the desk under a grant that appears to cover it. + assert.ok(readScope({ ...minimal, outputs: ["portrait", "hologram"] }).error); + assert.ok(readScope({ ...minimal, source: ["dna"] }).error); + assert.ok(readScope({ ...minimal, retention: "forever" }).error); +}); + +test("retention is chosen, never inherited", () => { + assert.ok(readScope({ ...minimal, retention: undefined }).error); +}); + +test("appearance does not imply voice", () => { + // The coupling the proposal names by hand. A player who offered a face has + // not offered a throat, so match audio without a voice sample is an ask for + // something nobody put on the table. + const { error } = readScope({ ...minimal, outputs: ["portrait", "match_audio"] }); + assert.match(error, /voice/); + const allowed = readScope({ ...minimal, source: ["appearance", "voice"], + outputs: ["portrait", "match_audio"] }); + assert.equal(allowed.error, undefined); +}); + +test("a body needs appearance references", () => { + assert.ok(readScope({ ...minimal, source: ["biography"], + outputs: ["fighter_mesh"] }).error); +}); + +test("the frozen fields are the grant the proposal describes", () => { + const { scope } = readScope({ ...minimal, source: ["appearance", "movement"], + outputs: ["portrait", "fighter_animation"], + distribution: ["private_preview", "local_gameplay"] }); + const frozen = frozenFields(scope); + assert.equal(frozen.data_class, "digital_replica_source"); + assert.equal(frozen.operation_kind, "GRANT_CONSENT"); + assert.equal(frozen.purpose_scope.purpose, "oskiewar_fighter_generation"); + assert.equal(frozen.purpose_scope.transform, "stylized"); + assert.equal(frozen.retention_constraint, "bound_to_purpose_scope"); + assert.equal(frozen.schema_version, 1); +}); + +test("duplicate ticks cannot inflate a category", () => { + const { scope } = readScope({ ...minimal, source: ["appearance", "appearance"] }); + assert.deepEqual(scope.source, ["appearance"]); +}); + +test("the wall and the endpoint agree on the vocabulary", async () => { + // The card the player reads is built from its own copy of these lists. If + // the two drift, a box a player can tick becomes an unregistered term and + // the ask fails at the desk with nothing on screen to explain why. + const { readFile } = await import("node:fs/promises"); + const card = await readFile( + new URL("../../xbox/live/oskiewar-wizard.mjs", import.meta.url), "utf8"); + for (const value of [...SOURCES, ...OUTPUTS, ...DISTRIBUTION, ...SEPARATE]) + assert.ok(card.includes(`"${value}"`), `wall is missing ${value}`); +}); diff --git a/xbox/live/account.mjs b/xbox/live/account.mjs index 851f2d7f06..2b0f7fa1d9 100644 --- a/xbox/live/account.mjs +++ b/xbox/live/account.mjs @@ -290,6 +290,10 @@ export default function mountAccount({ sfx = () => {}, probe = true } = {}) { if (source === "otp") return await otp.token(); try { return await (await spaClient())?.getTokenSilently(); } catch { return null; } } + // Published on the account record because the consent wall needs it too: a + // REGARDE grant has to be keyed to somebody who can come back and withdraw + // it, and the wall is a separate module with no other way to ask who is here. + account.bearer = bearer; async function submit() { if (busy) return; diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index 93f20f4598..6e8a55c4a6 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -234,6 +234,9 @@ // that nobody is signed in. globalThis.__oskiewarAccount = { ready: false, signedIn: false, handle: "", colors: [], + // Replaced by `account.mjs` with the real token accessor. Answering null + // until then is the honest state: nobody is signed in yet. + bearer: async () => null, }; // The game's way of asking for a panel: "login", "handle", or "" to shut // it. `account.mjs` replaces this with an accessor so a press lands on the @@ -245,6 +248,14 @@ // below need the fact, not the request. globalThis.__oskiewarAccountOpen = false; + // 🚪 The "add yourself" wall. Declared here, empty, because the game reads + // it every frame to decide whether to draw the invitation on the fighter's + // head — a ring nobody can open would be worse than no ring. `null` until + // `oskiewar-wizard.mjs` mounts, which is exactly what the Xbox and the reel + // renderer leave it as. + globalThis.__oskiewarWizard = null; + globalThis.__oskiewarWizardOpen = false; + // Keep a stable 1080-unit vertical coordinate system while allowing the // horizontal game surface to follow the browser's actual aspect ratio. // 16:9 therefore remains exactly 1920x1080, while 9:16 becomes 608x1080 @@ -379,6 +390,16 @@ oscillator.stop(now + .14); } + // Is a DOM panel standing in front of the canvas? Sign-in and the consent + // wall both take the whole keyboard and the whole pointer while they are + // up — they own text fields, checkboxes, Enter and Escape — so the game + // beneath them must not also receive the press. One predicate rather than + // a growing list at each guard. + function panelOpen() { + return globalThis.__oskiewarAccountOpen === true || + globalThis.__oskiewarWizardOpen === true; + } + function dismissTitle() { if (!titleOpen) return false; titleOpen = false; @@ -386,6 +407,24 @@ return true; } + // The head is carved out of the anywhere-button above. `tapTitle` makes the + // whole title frame START, which is right for a game somebody wants to + // play and wrong for the one place a player who wants to BE the fighter is + // already pointing. The circle comes from the game, which is the only + // thing that knows where the face went this frame; if no wizard is mounted + // the game publishes nothing and the face starts a fight like the rest of + // the screen. + function tapTitleHead(point) { + if (!titleOpen || !point) return false; + const head = globalThis.__oskiewarTouch?.titleHead; + const wizard = globalThis.__oskiewarWizard; + if (!head || !wizard) return false; + if (Math.hypot(point.x - head.x, point.y - head.y) > head.radius) return false; + unlockAudio(); + wizard.open(); + return true; + } + function tapTitle(point = null) { if (!titleOpen) return false; // Anywhere is the button. The bounded start target left the rest of @@ -1145,11 +1184,11 @@ addEventListener("keydown", (event) => { if (event.target instanceof HTMLButtonElement) return; - // Typing an email address must not also throw punches and start the - // game. Every unmapped key is "start" down here, so an open account - // panel has to take the whole keyboard, not just the mapped half — the - // panel handles its own Enter and Escape. - if (globalThis.__oskiewarAccountOpen) return; + // Typing an email address — or ticking a scope box — must not also throw + // punches and start the game. Every unmapped key is "start" down here, so + // an open panel has to take the whole keyboard, not just the mapped half; + // each panel handles its own Enter and Escape. + if (panelOpen()) return; const mapped = keyMap.get(event.code); if (!mapped) { if (tapTitle()) event.preventDefault(); @@ -1206,7 +1245,7 @@ }); globalThis.__oskiewarWheel = { yaw: 0, pitch: 0, zoom: 0 }; canvas.addEventListener("wheel", (event) => { - if (globalThis.__oskiewarAccountOpen) return; + if (panelOpen()) return; event.preventDefault(); const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? canvas.clientHeight : 1; const wheel = globalThis.__oskiewarWheel; @@ -1219,18 +1258,19 @@ }, { passive: false }); let pinchScale = 1; canvas.addEventListener("gesturestart", (event) => { - if (globalThis.__oskiewarAccountOpen) return; + if (panelOpen()) return; event.preventDefault(); pinchScale = event.scale || 1; }, { passive: false }); canvas.addEventListener("gesturechange", (event) => { - if (globalThis.__oskiewarAccountOpen) return; + if (panelOpen()) return; event.preventDefault(); const scale = event.scale || 1; globalThis.__oskiewarWheel.zoom -= Math.log(scale / pinchScale) * 300; pinchScale = scale; }, { passive: false }); canvas.addEventListener("pointerdown", (event) => { + if (globalThis.__oskiewarWizardOpen) return; const point = canvasPoint(event); const touchKey = touchKeyAt(point); if (touchKey) { @@ -1242,7 +1282,7 @@ unlockAudio(); return; } - if (tapTitle(point)) { + if (tapTitleHead(point) || tapTitle(point)) { event.preventDefault(); return; } @@ -1287,6 +1327,7 @@ import createOskiewarMidi from "/oskiewar-midi.mjs"; import createFrameDriver from "/frame-driver.mjs"; import mountAccount from "/account.mjs"; + import mountWizard from "/oskiewar-wizard.mjs"; import { qrcode } from "/aesthetic.computer/dep/@akamfoad/qr/qr.mjs"; import { captureOskiewarAction, @@ -1309,6 +1350,16 @@ probe: !socialPreview, }); + // 🚪 The consent wall, mounted after the door it depends on. Kept out of a + // social-preview render for the same reason the account probe is: that pass + // draws a picture of the title and must not grow furniture a viewer of the + // picture could never press. + if (!socialPreview) + globalThis.__oskiewarWizard = mountWizard({ + sfx: (name, amount, pan) => oskiewarSfx.drum(name, amount, pan), + bearer: () => globalThis.__oskiewarAccount.bearer(), + }); + // The fighters' throats and the announcer. Same context, same signal bus, // separate bank — the drums are the room and the voice is the person in // it, and they want different limiters. `?voice=off` for a silent cast, diff --git a/xbox/live/oskiewar-wizard.mjs b/xbox/live/oskiewar-wizard.mjs new file mode 100644 index 0000000000..24c8e82d1f --- /dev/null +++ b/xbox/live/oskiewar-wizard.mjs @@ -0,0 +1,269 @@ +// The "add yourself" wall — the popover behind the head on the title screen. +// +// Every other door in this shell starts a fight. This one asks a question +// first, and the question is the product: which parts of yourself the game may +// use, to make what, to be seen where. The player answers before any material +// is collected and long before any model is invoked, because a scope offered +// after the upload is a formality and a scope offered before it is a decision. +// +// DOM rather than canvas, for the same reason the sign-in door is DOM: these +// are checkboxes and they have to be reachable by a screen reader, a thumb, a +// gamepad's on-screen cursor, and a keyboard. A hand-rolled canvas checkbox +// would be none of those. +// +// This module holds no policy. It renders the ask, collects the answer, and +// sends it to /api/oskiewar-consent, which is the only thing here that can +// reach REGARDE. An outcome other than "allow" ends the flow — there is no +// retry loop, because asking the same desk the same question until it says yes +// is how a consent wall becomes a nag screen. + +const ENDPOINT = "/api/oskiewar-consent"; + +// The scope card, in the order a person thinks about it: what of mine, to +// make what, seen where, kept how long. `note` is the sentence that stops a +// reasonable wrong assumption before it is made. +const SECTIONS = [ + { key: "source", legend: "what of yours", kind: "check", rows: [ + { value: "appearance", label: "how you look", + note: "photos or a description you write", on: true }, + { value: "movement", label: "how you move" }, + { value: "voice", label: "your voice", + note: "a recording, used only for what you tick below" }, + { value: "biography", label: "something about you" }, + ] }, + { key: "outputs", legend: "to make", kind: "check", rows: [ + { value: "portrait", label: "a portrait", on: true }, + { value: "fighter_mesh", label: "a body to fight in" }, + { value: "fighter_animation", label: "the way that body moves" }, + { value: "match_audio", label: "a voice in the match", + note: "needs the voice sample above" }, + ] }, + { key: "distribution", legend: "seen where", kind: "check", rows: [ + { value: "private_preview", label: "only you, as a preview", on: true }, + { value: "local_gameplay", label: "fights on your own machine" }, + { value: "online_play", label: "fights against other people" }, + { value: "tournament_display", label: "shown at a tournament" }, + ] }, + { key: "retention", legend: "kept how long", kind: "radio", rows: [ + { value: "bound_to_purpose_scope", label: "only while this grant stands", + on: true }, + { value: "pilot_deadline", label: "until the pilot ends, then purged" }, + ] }, +]; + +// Held apart from the card above, and worded as refusals, because these are +// the three a player is most likely to agree to by momentum. Nothing here is +// a degree of playing the game; each is a different thing being done with a +// person. +const SEPARATE = [ + { value: "marketing", label: "may advertise the game" }, + { value: "merchandise", label: "may be printed on something sold" }, + { value: "model_training", label: "may train a model" }, +]; + +const STYLE = ` +#wizard-panel { position: fixed; inset: 0; z-index: 21; + display: grid; place-items: center; padding: 20px; + background: rgba(4, 7, 18, .86); backdrop-filter: blur(3px); } +#wizard-panel[hidden] { display: none; } +#wizard-card { width: 100%; max-width: 31rem; max-height: 88vh; overflow: auto; + display: flex; flex-direction: column; gap: 14px; + padding: 22px; border: 3px solid #6e768d; border-radius: 14px; + background: #171b28; box-shadow: 5px 7px 0 rgba(4, 7, 18, .55); + font: 400 16px/1.4 "Comic Relief", "Comic Sans MS", Arial, sans-serif; + color: #f3f6ff; } +#wizard-card h2 { margin: 0; font-size: 26px; font-weight: 700; } +#wizard-bargain { margin: 0; font-size: 15px; color: #b0b8ca; } +#wizard-card fieldset { margin: 0; padding: 12px 13px; border: 2px solid #3b4256; + border-radius: 9px; display: flex; flex-direction: column; gap: 9px; } +#wizard-card legend { padding: 0 6px; font-size: 14px; color: #b0b8ca; + text-transform: lowercase; } +#wizard-card fieldset.separate { border-color: #7a5560; } +#wizard-card label.row { display: flex; gap: 10px; align-items: flex-start; + cursor: pointer; } +#wizard-card label.row input { margin: 3px 0 0; width: 20px; height: 20px; + accent-color: #e6cd5c; flex: 0 0 auto; } +#wizard-card .row span { display: block; } +#wizard-card .row .note { font-size: 13px; color: #b0b8ca; } +#wizard-note { margin: 0; min-height: 1.4em; font-size: 15px; color: #b0b8ca; } +#wizard-note.trouble { color: #ff9a8a; } +#wizard-note.settled { color: #9ce6a8; } +#wizard-receipt { margin: 0; font-size: 13px; color: #b0b8ca; + word-break: break-all; } +#wizard-actions { display: flex; flex-wrap: wrap; gap: 10px; } +#wizard-panel button { appearance: none; flex: 1 1 auto; + padding: 11px 14px; border: 2px solid #6e768d; border-radius: 9px; + background: #b0b8ca; color: #171b28; + font: 700 17px/1 "Comic Relief", "Comic Sans MS", Arial, sans-serif; + box-shadow: 2px 3px 0 rgba(4, 7, 18, .45); cursor: pointer; } +#wizard-panel button:hover { background: #d9dfee; } +#wizard-panel button:focus-visible { outline: 3px solid #e6cd5c; + outline-offset: 2px; } +#wizard-panel button[disabled] { opacity: .55; cursor: default; } +#wizard-panel #wizard-back { flex: 0 1 auto; background: #3b4256; + color: #e8ecf8; } +#wizard-panel #wizard-back:hover { background: #4d566f; } +#wizard-panel.working #wizard-card { opacity: .7; } +`; + +export default function mountWizard({ sfx = () => {}, bearer = async () => null } = {}) { + const style = document.createElement("style"); + style.textContent = STYLE; + document.head.append(style); + + const panel = document.createElement("div"); + panel.id = "wizard-panel"; + panel.hidden = true; + panel.innerHTML = ` +
`; + document.body.append(panel); + + const card = panel.querySelector("#wizard-card"); + const sections = panel.querySelector("#wizard-sections"); + const note = panel.querySelector("#wizard-note"); + const receiptLine = panel.querySelector("#wizard-receipt"); + const go = panel.querySelector("#wizard-go"); + const back = panel.querySelector("#wizard-back"); + let busy = false; + + const row = (name, kind, { value, label, note: hint, on }) => { + const wrap = document.createElement("label"); + wrap.className = "row"; + const input = document.createElement("input"); + input.type = kind === "radio" ? "radio" : "checkbox"; + input.name = name; + input.value = value; + input.checked = on === true; + const text = document.createElement("span"); + text.innerHTML = `${label}` + + (hint ? `${hint}` : ""); + wrap.append(input, text); + return wrap; + }; + + for (const section of SECTIONS) { + const set = document.createElement("fieldset"); + const legend = document.createElement("legend"); + legend.textContent = section.legend; + set.append(legend, ...section.rows.map((r) => row(section.key, section.kind, r))); + sections.append(set); + } + const separate = document.createElement("fieldset"); + separate.className = "separate"; + const separateLegend = document.createElement("legend"); + separateLegend.textContent = "asked separately, off unless you say so"; + separate.append(separateLegend, + ...SEPARATE.map((r) => row("separate", "check", r))); + sections.append(separate); + + const picked = (name) => [...card.querySelectorAll(`input[name="${name}"]`)] + .filter((input) => input.checked).map((input) => input.value); + + function working(state) { + busy = state; + panel.classList.toggle("working", state); + go.disabled = state; + for (const input of card.querySelectorAll("input")) input.disabled = state; + } + + function say(message, tone = "") { + note.textContent = message; + note.className = tone; + } + + function open() { + if (!panel.hidden) return; + panel.hidden = false; + globalThis.__oskiewarWizardOpen = true; + say(""); + receiptLine.textContent = ""; + sfx("block", .9, 0); + card.querySelector("input")?.focus(); + } + + function close() { + if (panel.hidden) return; + panel.hidden = true; + globalThis.__oskiewarWizardOpen = false; + working(false); + } + + back.addEventListener("click", close); + // Escape is the only way out that does not touch the answer, which matters + // on a panel whose accidental submit would be a grant. + addEventListener("keydown", (event) => { + if (event.key === "Escape" && !panel.hidden) { event.preventDefault(); close(); } + }); + + card.addEventListener("submit", async (event) => { + event.preventDefault(); + if (busy) return; + const token = await bearer(); + // Said plainly rather than by bouncing them to the sign-in door: a grant + // has to belong to somebody who can come back and withdraw it, and that is + // a reason worth reading. + if (!token) { + say("Sign in first — a grant has to belong to someone who can withdraw it.", + "trouble"); + return; + } + const answer = { + source: picked("source"), + outputs: picked("outputs"), + distribution: picked("distribution"), + retention: picked("retention")[0], + }; + for (const { value } of SEPARATE) answer[value] = picked("separate").includes(value); + + working(true); + say("Asking the consent desk…"); + let result; + try { + const response = await fetch(ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json", + authorization: "Bearer " + token }, + body: JSON.stringify(answer), + }); + result = await response.json(); + } catch { + working(false); + say("The consent desk did not answer. Nothing was collected.", "trouble"); + return; + } + working(false); + + if (result?.outcome === "allow") { + sfx("hit", .9, 0); + say("Allowed, and recorded. Nothing has been generated yet.", "settled"); + receiptLine.textContent = result.receipt?.hash + ? `receipt ${result.receipt.hash}` : ""; + return; + } + if (result?.outcome === "edit") { + // The desk countered. Shown, never auto-accepted: narrower terms are + // still terms, and they are the player's to take or leave. + say("The desk offered narrower terms instead. Adjust and ask again.", + "trouble"); + receiptLine.textContent = result.counter + ? "offered: " + JSON.stringify(result.counter) : ""; + return; + } + say(result?.message || "Not granted. No material was taken and nothing was made.", + "trouble"); + receiptLine.textContent = ""; + }); + + return { open, close, get isOpen() { return !panel.hidden; } }; +} diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index f9bd5ab4c0..bf8c48c828 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -14904,6 +14904,66 @@ function drawSafeZones() { drawCornerCrops(actionSafeRect(), 34, 2, [105, 255, 118]); } +// The head is the second door on the title screen. START is still anywhere — +// `tapTitle` in the shell makes the whole frame the button — so the one place +// that must NOT start a fight is the face, because that is where a player who +// wants to BE the fighter is already looking. The game publishes the head's +// screen circle and the shell hit-tests it ahead of the anywhere-button; the +// ring is drawn here rather than in the shell because only the game knows +// where the head went this frame. +// +// Nothing is drawn unless a wizard is actually mounted (`__oskiewarWizard`), +// so the Xbox and the reel renderer — which have no DOM to open — never show +// an affordance that would dud. +function drawTitleHeadDoor(t, ink, suppressed) { + const touch = globalThis.__oskiewarTouch; + if (!touch) return; + const onTitle = shellMode === "MENU" && titleTransitionAt === null; + if (!onTitle || suppressed || !globalThis.__oskiewarWizard) { + if (touch.titleHead) touch.titleHead = null; + return; + } + // Only over a seat that is actually yours. The attract loop's "action" + // variant dresses BOTH fighters as bots (`beginTraining`), and a ring that + // says "add yourself" on a bot's head invites a player to replace somebody + // who was never there. No local body on screen, no door. + const seat = players[0]; + if (!seat || !seat.alive || seat.npc || seat.bot) { touch.titleHead = null; return; } + const head = runnerWorldGeometry(seat, t).head; + if (!head) { touch.titleHead = null; return; } + // Radius in screen units is measured, not guessed: the title camera pushes + // in and out across the attract loop, and a fixed pixel radius would drift + // off the face. Same idiom the killcam tears use. + const point = projectPoint(head.x, head.y, head.z); + const edge = projectPoint(head.x + head.radius, head.y, head.z); + const radius = Math.max(9, Math.hypot(edge.x - point.x, edge.y - point.y)); + const reach = radius * 1.22; + touch.titleHead = { x: point.x, y: point.y, radius: reach }; + // On the screen-UI lane, so a fighter standing in front of their own face + // cannot draw through the invitation. + triangleDepth = -1.42; + const pulse = (Math.sin(t * 2.4) + 1) / 2; + // Gold on its own rather than mixed toward the title ink: ink is near-white + // on the night sky and near-black at noon, so mixing gave a different — and + // on the light theme, muddy — colour every hour. The dark under-ring is what + // carries it across both, the same trick the type beside it uses. + const glow = mixColor([230, 205, 92], [255, 250, 226], pulse * .55); + const stroke = Math.max(1.5, radius * .085); + circle(point.x, point.y, reach, stroke * 2.1, [16, 20, 34]); + circle(point.x, point.y, reach, stroke, glow); + // The words ride a plate. A meditating fighter fills the whole column under + // their own chin, so unplated type landed on shoulders and arms and read as + // part of the body; the plate is dark on both themes because the gold is. + const label = "add yourself"; + const size = Math.max(11, Math.round(radius * .44)); + const width = handleWidth(label, size); + const padX = Math.round(size * .55), padY = Math.round(size * .34); + const left = point.x - width / 2; + const top = point.y + reach + size * .45; + box(left - padX, top - padY, width + padX * 2, size + padY * 2, 16, 20, 34); + typeWrite(label, left, top, size, ...glow); +} + function gamePaint() { syncGameView(); const run = runtime(); @@ -15204,6 +15264,7 @@ function gamePaint() { // piece of world could have drawn straight through it. drawFrameMeter(); drawImpacts(); + drawTitleHeadDoor(t, titleInk, reelMinimal); const counting = !roundResult && introAge < roundIntroDurationUs(); // The matchup card announces two names in the middle of the screen, which // is exactly where the wordmark sits. On the entry fight the word wins. diff --git a/xbox/live/render-social-preview.mjs b/xbox/live/render-social-preview.mjs index f2bb8d1906..748740f633 100644 --- a/xbox/live/render-social-preview.mjs +++ b/xbox/live/render-social-preview.mjs @@ -44,6 +44,7 @@ function fileFor(pathname) { // and the capture comes out empty rather than loudly wrong. if (["/oskiewar.js", "/oskiewar-sfx.mjs", "/oskiewar-voice.mjs", "/oskiewar-midi.mjs", "/frame-driver.mjs", "/account.mjs", + "/oskiewar-wizard.mjs", "/round-room.mjs"].includes(pathname)) return join(live, pathname.slice(1)); if (pathname === "/aesthetic.computer/lib/auth0-otp.mjs") return join(repo, "system/public/aesthetic.computer/lib/auth0-otp.mjs"); diff --git a/xbox/live/social/manifest.json b/xbox/live/social/manifest.json index 009f4fad34..03f7310868 100644 --- a/xbox/live/social/manifest.json +++ b/xbox/live/social/manifest.json @@ -1,7 +1,7 @@ { "format": "ac.oskiewar.social-preview", "version": 2, - "build": "d62634d4df311083", + "build": "badeed4bb0a14667", "theme": "light", "imageWidth": 1200, "imageHeight": 630, diff --git a/xbox/live/social/oskiewar-title.jpg b/xbox/live/social/oskiewar-title.jpg index 9b6a71d9996981613d3e9893c74ff1687658fe59..953cad67a30d9dbb090b13df7d5377714b9898b3 100644 GIT binary patch delta 56233 zcmX?cgSr19^M-{?@*JFO>@2KY%#2L`k1%8lFfcMRGchx>ak8=TaBy<5Pd>mTQh$j{ z)F?D8v2gMfF=eNq#);y_!AT1bs+fltl}wsCZSls7mnEf~lbaSDx?*BxQCzlU>E;ir zYU-8|&C@q++3J##THbQx;r}BHvW$!jjI4}|j0|wV%EZjV#?HaW`Tq!mhadwJBNHz-}Ko;_Q>4pC2NY~?=HD}zhdGl`>9nWt~!sj^H%Pfws-$}hyM(94!0j%uhF^mWJ{H$ z+nI&e(?fMbgI5H-ds2IG3b*yOsax}0L!w$*BhS8eeevE?Cdc`$)a+A*bJvzu8!)h5 znAoURFBrX*^`@M3f0E@o+ul2$p5IuuYP)~dwxH9&kxHQ|UN@zVnbwL`T7A}fyWQBM z@_F;h_&dy@_9sN+9-8lXb5j3`Wglnu<|otdl+An0dM;~v)GHxQwTL|vCHg{L?+Wj! z_g{1Q{-1aMUf$~e7n=Wjv)Q!0Tu1q?n~2ZjTUuYh8}h%NK_Rrd`8<2j{vB+q>L+OJ zJ>(Hr7<21*+SYL2RZ=Y*V;|LQSe$n@+*Imo(&xJTTeH91U(~Tt=hdQF*HxnCt}t5r zDBRIkT0e00)fZ3l&gHAUy&E$-|55nc@6L_Ve?F9I|8B@y{*!6-9k~jtseccc2LEXc zeJ3<)wb$l~;#)Cgy7BdAR=s{Df7kXx+|I?ztF#tveYIhE$jx#WR|B6<&u1lD)V|ur z>vzRpWBbMPCN7$FLDnhnbuYEUNJMl%w^HoN!PhadduiIO;(KxPP;i{=pHxF;qoP0HSt*NWhrT$9g z$Z4r}`%9` v2RZIRNdRbO13=2g4y&U`Nub?5!8 s{2FJ=k1+fd*$MOp8bncrb_MUjen!^V%<%f;I~eTKjfNh$XvU1 z$~0|X*==fu603#nExx#hewnv>=ikt(o#98`^VU_ntbcR);p;#BE{XP=YwoIithe~G z{HojBOJDLb_ebkH$9-6uSGvBvCrsST&HBX8tLuH|bUojB|5}a$Z_!%cCr|C(ZoYIg zj4`1gq#&?7 U=sqb!l6ipYQF9Cpz}L-t;9z$4I`#|KOF+^@j5gy}9^#r}SAp#oFH9``4aZbxK|N zwrZ79)v0+b&*W|I2KCyCZ`yAEq~Pq54|0;*(&LS$SsuMQdFuA^t-s=<_U;v|mtsAd zH}hj)|E`zIC7)@|x_I Y$$ZFoASz_PCJ7PLE1%@8|l-A`J;yd^1I=7voqAQmE{2+PE&GPts z?udO$9#1-UWcTIo;a+^H@7G+H_?GJ{vo?0lb$7Mj#Y)A75nt}E&sy`3U6#kk)V(?8 z;`81;H)P6AKYHvebmgIGg|D`HUe;ZySL=Q*FD_`{x$FWiq$W>e)~#oZ+Y?>(IpF27 zU2$6;yM5q)SoE=b=a%cQFR#zpcH_<0+DFPS!}2_3ZF;h-=PJ2ADLeaRs$B0XgQBvo zogcSMy_x9h@a;sT!sc(glg-% trR?xrAn~T5rAmRc!rs|2r9fORx1NK9=sRakjnE9XB&lSa{R5o3qX& zE!+8ib>G5|C+?M9F%7hgms NPo%h_nRbd z>^VT=DMpt{uTUW7dYm<<9EbowNJxgMHjv&ggganykF0D}2&$mZfIm zn%(u6bPVj4*S*}Nw0PFKo1%f{F0VRP&9l}t4l{3#b4gXtT=sijGo$~veK+pu=}tRU zm48xCZk5eEwWIgWM^@Hs`m{*hc>1-cw@c%rX2#9?_T$yNxp$)8hF_HC{dC}J@~U+^ zSYLUnFYKRLd2foTuc!LWRr9)4T<>;2>ibgqymi&oJDwJ;QL`V+%vx~ZN9Ln!_DBC2 z+;uJA&AtE9$R_F2)rhRbyX$)2zWA{;d7`XLZ(LvUL{Zz>TAK4Do?e&?TC#+)^wj@O z-0iZZ58o@T*bw>T&%$YayB6J#`o*E0u(GDWdUxZCw^uthnF^Zco!VXTLzX}DU9J1# zuQ$XZE4o&0(_*`mu}W)kcBSI9eAS3q&n?epX;0EL^Zgzd`c1G-@6ueWrkNi$y^cDO z`ZFizf^z(~<+Yn;ZV0bmHuY5Smi5UA1skVFzPojF$+S7;N+*hb-F~`nna->G)swPj zMX5&YoEUX8a;r_x#Epxq*Y2Dsbv4BGZc3S^bz#MqOHme?)~{PjeNA2Z8v9GuMD4iM zy=&Qv6`dD@S5 zZ`HeBzx??w|HkEcOK)ABa`)2quP-{+x1>+quBUQW`ljPa-H 2>027)aC6z#E1y*Y&D6fMo|r4u(KbCl |}5@uWDWJ5cXb*IZHF2kv&M84UcB z8<)w|vxm-256d+?oHtX #8 zGdH&dr)-v8cx;VWy!5`J$$vJ+KKa)@`$f;@E!kU@KHjR8jgxNnE &rcdZ?nr(UVdk~J5&76C7$ V7f%@$p?RulF R!&}|q?Bc6U7@nR{>`~Z)8>n{ZF*gPX|Gf7)fu(t9tW!WeEYTI z)|Hjp{Ehzw|7Uob9DBKJXK-pr+2znrQBz*L58k`u#-o}1y*u-g=N#9VEAAgMC*;m# zzwk{lQ@0ky 6oolDYpgD$`LxBj}TdtGSO&S}!8Cn_)7QE#MKlwX}#{w(9Qi$aMwZznDu*lbpHl^rjzL`YQv=LgIrKNm%eiAHEhUuD7r4 z^Y+=w?iruDW^hdle$H9Xb6x$#PkXgr^Clf+_|3lK+q&y#C0AeK`S#ZKaLAW=b0_Ru zx3qk^!e;HA8n1W$Ntw9zX p&R*Iv2T{_ FX{TWZ{_V(!AH7Q^)7Fe*?8p0i)*HW zX1)`RHr2DY-F16ETdC0W>ux;{W|1p@bB~!6KYDT^&u{UxH{XxD^Mq^;^$%K;Z~Qa4 zwZCd{U~uW}l)L6}E0U+!B)xol=zH#Vv$tVIUF+UQin1za>4bBYowezEnk@LfQL1b8 z+Yo=z56-Grn#yuxHmp3mddb;24B8qDS0)yoT_$wVY-JJGjTd*m^q)%I6n@ S#zv$TK&~(9SS7 x^>u; z*HX7X%q>jOI3d-2GUKe^zWyoEPh)4_(>t+lS-nojx;VF;f_EmbX`K=iJ5yy=)(ZwM z{{`Sw#3*m^^6c{M*ODY|CC{*P_pFXObu;PYTkpc `_D1|@5de?FSEMuTgV-%+b_9d_2#9o7OU^rH}m|&^Y30HC0|@M+q5p~ z-MYIqf~(&I8_h{xXx3G+Ei3fks!4Yp!~fI^7reg{t6F1n?oUVl^SG;7mS*p_`>pdA zepqo#K5^HTi=mm}d>2D5dAwMf{=~|9>e<+-=APT)_8qP`x8=sIgbRP-SwrNd?_@r1 zD=k_Qf75W|I^U_HF?aV?&j{OgFKPFy*!#N{E?XRxwR1t$(X)G Rwk%<3~m^i8+;;uP86g6l#%Mc0+9ozFiN`~Cja zP3x|Hw!P&4E_%8Bv#a92H~W0EseiJ$ (Hf zmR-ub?55j2uPQICGDjBPyWweu9lcq3tM&?CD{g>xizl}&ldQiwC%$m@@;N-2yndN0 zuHSeoc5P?EqGid|w{GwCSDte!Dq`}Q&Gu6Rr7sE}J#}r-PK!GQA7^)d5Iwx~d0^4^ z5N~hC)4uuJFPp}PR;`;Ts<60qMfaXbmS*>JXLUT+JDauA`PSx@mDeS2-Fq8)_12TM z`@a5YIw;Z?ohI$`#r0EFnO6OqO<(dW^=|Jy>w15YM8%z48xvFi`L1{FZj(+d{O#NO z$4>m_a_Q{ZPes4yFM56L)Y}xB_>ZzP%O<_f)yrKm;dJIvouCst`uH}fOu5#k@3Lyu z?TfQEzPuW~I-;xBpLsFECBukSGdf?d_|Gu$-QMXpV|FVX)ZvTe)?528)9zDk{K@(v zPla+Bn-4cv%&C91HI8+ux9m3QsMSR=r`FB9blY&+o9?^ntE!H_>$nlMd)cdXPqSm5 zeT$JR2%PtAae7eHhf^m%_Ns5bUMUtnd!wMd-ig(!qMK)$O)K@Ccxl<2?mgMlYj0h@ zSI+%M|Dlb{i@DSNxBNQI+Ap>7=t&WuFH2WUtc$7tY=6u%Zg0%jSF7K+xOkq~tiE@3 zlK7&(_0Fvo%O7hkUax*ay|QR-$j|bC?Ddr;{~7e |oGx+v=(^4YP;?)+-ib?)bmrC7BlY`5MW z?|Nb@-_*dr!DaRTvg_YZ+@BS}8F`;`)~}l;*^Y0xR;*ip f>?c`r_*(WsixMjC3)w5dVSd%g_o O`}+(7S6wKYp!{d8)lzeyM4eRzY#ir2PlBy>H%{^U3c-tnQjx`^oj^tLA+znrgXo z=kMERvo_w->fBR%-{r&CuXparE~pWY`0?vuvhwBCM^-w9MoWc7>UCT#ely);Vyt)A z6<60dPqSxPTE!dB=e<0;v#CrydH-bD$-nr0PcmEI37WS)BRK6}ZLHbri?*uo&C5Se zty @5joUb>H$SwVLzhK{^oxNO9#%JW7>h>!yxV!q!)k9CS z_RiK?ylA5E%>KN$Mf!_Y&Gq{-_wN2V+@}-Pw0FKSYV*|A5m~c6OIAc{)pl;@GLO}_ z GdzFcOR <#x3J_{~6lqKkL=M@;~wD;EJE&yuTLqWW8hR_;c8QRoKcc zU0+kmPl>Bt4G~&hwOXaW)L(YawN+}rwW8BjmOakAX1iEIEVMm(>DlGy*Ic~!U}@LR zDIcSgiswo{nirD&ZSG`uoxEPDRehZ=R^6L@Rq*1?!unY;ezUy1)O14D>w=>3-p| z$u_0R)JAaCcgx%A{+_;%57*f0)R{imsP(ZYW?yFX^Ww?>88UJwFLp285@O+%8$9dX zn>(qmGq(IyeimKN{darCwar Bx%TKhI^1O5D&OVb|U+I+iNjImS=}mV1X>)H< z-pc)2c`Gdg%bm~G^HrYy$zS=*CG029??t~Z?=CF(&%nMuOuuUPP5 $Od(@$OmfmoH!SI3czAwVUfnsj6?iy!*31^_a13wLh$_seb6~r>{0AeJveNPL5&7 zV!r_HQ%k&C`RbbaqKH!0{|wrD0&lz(In`V1>O562X7`(8Tg{8_ 1Q3u~cKn{Yx(y z7%L8d2bvaNIqP%$OE$yqr;(xe+79>K$c&FH*b(M6@u0VGaB1<>`XZ&$&=42bP#2BW zHrM_%Z;|hqp1mzMWwP8mjk_6#41dP0EA6{_=6U(2MPEMcyym|8&ODwlQ!BNT+pI$> z4)<> YMNXaNhc8a`^j`Ppzbvi(U%8@wPPN{>(G$3)N0XJlnh5)7@u%zS0k? zis@I5Z#jP?JY?a@&xRU-^`)wLEfd}ZzxloA 7qq5gKFf? zJt@BJ|FSeS zvDbx#`^#|G3lE9ObuvCfELF*u)<7xKHnwS9Gapb@1%zQ#0S0Y~}c0&meMt z`tE19ub 6<3q?u>b~WY6u~@HdkVhr5UR-AtG!9;-F)M@shW)5>x;r &mm@l~>P} 7BZ6dZI6T^0n(qTGf0XX4Ed;+f#Zo zEjWBtjCuWL`6Hiit&9(y_~@(llWjJaFNSOXjOsd-_1x0R=Sp%)@zm6!yyt7g4w*Vl z*4cOWjp&rE+jtI3&dk)`@%5Bx+rEodxo3`VtP3uWn`B&ODQXs!&D-4@n&;EKzOCfP zI*s31deb9K{VvL?KI&B~HR-gK4atjG_4c!AV6=(Q_VlC5^`U}+XG7xVmix6VdLX{d zUHyXep@cbs@h`PE?p*rL@T^Y>Z|mw^zeKjD9lMefl)K4n@l=U-@#m~(hQ5~flXL73 zcv==UJNI1QtbI)zyG6cy6 w^vkZ7EZciq^L555 zg*A)UbZ$OqBcw9XUwWcU{q?OUFP;qv2?=?zV)>mV>$rdXIsaQf`|XcAU&3{rAKsPS zGDYUyihT(=;qTTjE?RHvtE4 zDQGrgC`ME^{T@KG*DtGF_d_VLdBP`*cQx^jv+En!t21f`MW3WM(b! z9O$O(JoOd7azjgZ1l-n~HtpS!S?kIUOYN-bPI*?eDF0$rt=q *rsQ9!fyMAow~M`*M0KYbJunX z?s?I*;_9=zfo6B3?x!7V_|MR=-Kl=^+4^&w^;|bn4s8ky%vg5EXYv7e`@DJo84g4* zv_JW5|8ti8Z{$CSz3~53x&G%t{oh>w83g4Hdt1*t9x1KQwO!rx#IxP1t3yI!F9z>w zUa8$%epsHz;ykBS-k)Hh==n$a)=b(dv}^gP)a-lLlqbrq=vw(g>FfR6>+5bS BXvLx$C93hCiRK<>tSoRCw=8XJ_S`r}uq) z8+~fUd+)t {m-keg_pWeB8@0wjV%ahYS*#ud~$-6!OtYn$_GTPSN^~G#|>xqK4 z9({k6qi2PchsS9i-{k%NyY0ybWnmj;Yx}JZD_yF&zi;ZN-oMY!ty@>GU8}PByiS25 zvzVvK#1o(~kgKOwUlbHx_bOM9+1hxs-<8>aP2atE-6wwdsLt=R<-OIb`-)yXx%uTv z<}NF(*o%Q*xn^4iMx9H2{^ZKEzjp00rvDk-O`a|6o!4%CUf1mR9IkKb)%htWZG&>Y z?OgZgir1WVuCC!qT3RLX?$>t5)|*bc>F?_5&nhb` MkzEpL|$U<&XGx@8`dAp{D2~w{+|J{|xCH_?-& Wrwck}1H zM;8n2ixIvQZd|!aQz_p4;Od{t-o1}^`Ks96H+92R`N_Ye+uOgG-}Y{3{}W-~GWoZl zz|pUVwm*6+etP-cj*XG4W$xv(#y$P5{x-J0XTkM7VON)m^W99o`s(b9X||W^_MFQu zd-?W${;iCPj8|(}pY2<|?_1Zl=bA^u{z( 92YDf|J*7i+Qn9&+K*&|I*b dF$ENs-+@J9M>Ab(or>>uSS^4d${|x7D-+8XJGj(b4&ZhhXby;@nKZ?YWo*Y|l@wS1XcZ qx-g4gl&ycdBJ~z6)e5s}J (hURzZ;|O|5p3F{r$Hu;gf&Ovs(On`77TCjOP!4npR9pc4wJpeV$qv^(U;V^JenV z6`6mgO)vGBt8;P5Cg1IAPxWV;e^sfjF5h-Z@Y{|L!r^Ow-2N)hzi{=Ar`x35j>hKH zFUzuvUaKs6?w(r3u8N;i16N*s>le0Yr^I!qTsEn#HOj4gdM|J9`nO}Q@S!*N8K*tp zuzrTgqDPOG{B-lXmcN~Q^_Tr}tfA91Z>+!NTXros=%k6f{#5O}8B-@oU$ZTYy%yEQ zwsWGlztIovM =rKXB(RTo>jG;;aT zg2$zY?_b!xfN>9#T<*k1*?LCS-DMUV_rH{DN&Xet67KFeBRe+bT2$9Hk0UEKM@n+f zEm+;TYR^e^<(b0rr#Hu%y?C})a?eZ6>MiF3?A!KTU$a;Gr0#AHL+vts{ in#?^>;UX5E(=$834lyB+)8o6^a{^{;Ey zwVjnyrMg!2hTJ|HoIE+8_r8kLQA@AWv#sZvUC^1l#lB vL;JMnPQ6`3a1 zr;|=+*WKB&L2GMZ)~mo;{@|$VmVv>w>z#e-znqu9xQ6Ls*Q0G+(tOs5OH*a{-FFvq zXVvlbow(NJ@2k~U!Npd{71=p+xFUBjKRk4|D|p|m*HiC^KbG07kQ1}{iMXYC$!6Qr zFN;h=Bc;QiY+NxrZ}q<4re|Es!>VSQdQDm`t0Bd`HCTGO&YSbsUujeyoq4OidYaiL z(}|a?>a8S~xG4v(T0M2n{_O{Acz-3_@lX8Ea7dgtsy*TPyL~pt^1}Yr<;D4Zn-=l? ze(LSK{G`8K5qe7fCV8)3f18jr^?Y?oZR)&9<+iJL*~T) IGChfCG?-j0{v-Mx0b=*}a1PW5ITm#yK8 zi`^Dl?h>x_!{y5BS6h{%kE{-z^jU4C^}#9ySGkLFr*!mtPsC5VRlP<%O4p=Ybh6=+ zq{Y{BcV@m$a(%LDdD)5jtDaKN4Mjt{?p~GEZp<{Bd$I7FhS08>%g&{azjD{y_#@%B z$oyLGGbu6M!akWZ_l1AVPUWll=((mTCd_Eg8Wy?eQ;ABnqsmEZh4PNUwy*0b~a z8*`NvrE22y4$AA4r<=|>yLere))Lb_EA|~beQMQ_*f}eWTaIg1i&{?H(WAR}h0Ns6 zL(}%fRO|nkyDcq@YnOH2#PsR2yz3T6X$ju*bh^3JV`gRd`e(6BC--z*xp^i$uWQBC zbe*GL*E8m=%oOwP*9r9cy?0kuMUd;wwVqQW_TJupv+QZT_QbN-se(SIzD)J7*eul> z&bh3Lfvxe)`s>#Vt#4jmd~?aAT~)vPOntozC)UKrdUbn-tbX(FL{nV%RU6(v*(x_C zPh<|Px0wF=shVN(+MO}#ebbumGr!78t3TOhW*YNd|4OfG{Eea$H*a&UEiU+dvv+Nz zuFm?l{h#u(eMNeb?|cinzH^I#cHoNDXC__QJbl^ixBE`+YJTOrTdBLGS!aGo?)5+K zwu a%axqh5Io4F6=inS>VZabxs zSu1*1d}7Dd*l)hyZ`Xc%RiypiXK83kMt@bsZ-I-M53?VC4C<4!nw|Sg-|qPSjkDb+ zy)io*uyS5_Nam%M )y_c zt {a?kta(_P)I@@x0;^)>f@87XGSuzj2U$R;;n-;EoweQ8?)zHc?_eCob! z;mS`-m(}}y?0oI&Qe6J3Yu= Q$)G59 J854OV8W4WY?iC>&QIM`nMfd zS)a{3op|))@ABs|yUq8sMfJ{@y{hO}_4@WDt1qv6%{c4S4bT1hyR4VDl$h_1ZR-EE z HMmMK}bD<*{>-1jDa%50@yc3bY;Fw552 zKC3URzDg(mbk%aV{|x^bd=^a>U$``<`tnheiMA@)PqQq~7FAf9e~91k_~yQw4?b00 z42pWCHgD?T^W85$ BJ-};}yHRTcaX6b0d8A{Wtjz1O^x3X9i;(j{V^>j3I^zQIgr~Qujb?mLu z4o_E&NwAtdF*Ek%cA5IT? SQX2)SFrr%ZM}2(pJR9FujTxFaC%m$-STS( z+h6sR{fc_^Y^ka3U5OhnwWppuefNrCt(fKgHM0%NZwBt$dq2%4;y{{da_ceY;-DDe zy^ro^f7tpfHu_A{Su=OR*6y{Aue-jRzI*-dhp2o*%Gxs@qD}9v>Y8pPkyXD-@3;1i zzK{?8tg(B9-#AYV{!_j0^0Lcwq+;cEq*+x5Y}!?8c3SVo?zzgAqHmY2o~T@Q=iG~p zJG -`ldK&iSV9qBFPivO`_wKMoJE{&jYJ{`?=ay#MOIs;rv+E`I%uX$J)3 zx43WFcKQ(Ov}o4O%>1bKNU;x>v_3m$Wlz31YjNtwo8>vRqIX5hvKFnI-gGeR=(P|H zw@>v~?@z4%xMjy?+eh#FYuKBkLp^Np=t)4ylQrVGj`Yi90#d(TodaO3fP|0Z8G zzj Sof@)!VtCr7@FK(HaDA89sk`6q zTHYB_`L4F|s?4#66H?5zR_}fGTI+mhc)+{#Q&0c3xVX5uEjv+v#RUWm=PXuQeB|S+ z-BS;RIlH?{oz$MQPxFm`)~Yuh^R|XKFAuyihu20sqV3MxW8s_Px~EQ`e`52V+z+ abr#&e&tG$dAZ>-1(Vo_pr) zt!s;Ce~MYPukUn!*OAMIFO{y^t>W?Ir=7OR(ZkG5eJf8-dV0D$_lu{rvgw0hm#^wp zW&RUhip`2QQa>@-?rCh##Ves+Up^E+Dam@i)7I;0sDI#Q$b>$ZmQ+?%y_QwPC5?n8 zlRk}z&P}Fk!L#}TYx`ua3-wp85V^8zVgB(2M_ !a>9s576%eVf0;`VD7 zm$KWdw6{HtRzAK-if7wl*OIMamtxjVmyith4EHupYg(qhRpz9`w^duu7HMf|>7D9| zJdkE$IH7pa&tjFb>8ZsseOofp>IJtgExEj__RXJNi)Szx_vSD#FuXK=>wUar)sffB zd~3HPe|xrGf65)Lo4!)JbAJZLt>}9CRjD+{JiT^vm2}&c)1tc9eoow{`ZRRwZTVBH zU*Ei}ld v#X@+1%w}{~4~VDEiOfxZR|AOS9|OZR@za-3oJ;?zp05IHlZdUsJekjl|WdQrCCZ z_7rb7(ta}Im*Lf*o=IQ+a&I-d6KYy{x$KL12Sc~PuaPm&XR=d2BlomdlE9(1=l-SpSucDXZmBlblv zODR9Sbor;EOZU>2^snA=;*^?B#`BG$Z#?g+cYRvoS6{9@an;pTcP$Ptx_s7d oLlZnxQb6MuhO3_v6B|9F8va-s$<1$rM=N>HlGb`=UbPnQt~=H_*7q2@h3Br zc)s0wufNu&eShP6y mY;J9@?$rz5lfQi5>{r{PZ%>zVJ=Xj@JY1C3 zWb%2Pg2Te0=O?) yv)VZvHF! zyUg^d*iR*E{-z^IdMkpbti5Ue#jNr3s-<1KW_hIbSuUKp>ZEdY;MwX>vyhOuuC95D zB@UZDXS-6>@#y$wo1nis`xU3X-fJ5?|C_Shs%2qP*)#Lja!pyfXiZFf+M3D>4<_CC zQ{T2*c2!);ywkz)xie+%xi5aWZl3A0`mC>6>-GMuez<@Akz3Eimwbtz{Bzox8#VJ! z)~l83t8V=sbLY>T>6&v-pWXF2_x!fsUvFQ3v~%I+fb!+yPqM#Wf2;Z`_ubLgUAJ~e z8+WnHJe&I3^U>OOQ&;SHw|w^|(~#Khej#)Bn!i5(`u5VjB1dnwmMuT}JLSW5yYi!U z{>!)T_Liu>{%Of+QCHm{)8LY4zB9h(bgbSjSR1|c+=Pg$S(j7`-hH{iAhGm;)4BV9 zRVN2l8-F_eRn(^V-j{8=x-GY9&sqEK$)c&&PhFp<-M(}(=<2K0o2Lq{dg_!pS8n+O zlc@E}W{T++<=gI7-4=3d)$HAqE0%X&w6zt@-se62U|?<6JoPK}Z{9V!hgGi;zwN7A zYJ6MQG(~5g`qsb`-z--=TcwrPy-TY?^Q7r@?tqxWx9Vw{)ty(q|Fzo@mSpZB`)R?7 z1#`c=x_a}R@6U*%A+8~=p|_VEJ8Cw=J0xWH1mnJ$7L)Ejc>26|^UH7BUK-EV4)^~Y z-=`6MFP}&3_ZH4T9>eW3Ls$6ROkDNBCGXwW-E+MaHRo@-9+YsA@7ix$S(i;mo!8t* zP3@GJw4`9|q5Im}iW6U~b19z29^!W8X?Whs(--4n4z5c|T(oAQsaw^884P0FlN*=$ z*E4P1C-koNKf?vN2`@h@=k%TZd_r3*urf5>Gk^KCMQ)43*GI~-z4K>r^}pnF$}8^e ziz~;n^fUXUyEnO?bX~f`WIJcoPHEoKbIYfmaa(QnpW)!kq@tbcV}IV7Ybn#+-#xok z(zAFwfB3 >Mv!83>zr3Ql z`b94a`Xy?-j=jBi>AcwN5~VvD&Rg_T-u?-Gs9Cz(>s!}$t(WCXHeTGdXxp-Rlh}To zH0hKU{ +WsdmcK?#|)&{#ZH}~Dhb=;9^sd{U(@TJwf z{*$l7DE7*$`t0kT*4Ml4OXbC`uAeboz0dOA7OmGTu`<08duU%*q~emLmu%`4X4!2D zONsfO`Mm6Qv87tLq1`dVwZFPei)UtSwfOK^@4+6U!^b97{MvNw=ZahPr%%1L{?p~V z_3q025}%ceS2nGi=eKs6q~M$K8!Ioo-Pp6}X={o}c(rY9=(^W=Gq;LtExDvq9DL&H z-Syd%4+eUtJT~4K?y0?{S#e)xp1fyEn%OP$Yr&Uq&Uulsu3Jj`8vEt^yUKGqR$Sk> zw#YJi*XrOYD=r1Icl9aFE_-}cO6+3&6H}v_sFu` *7RR1ipTTYM?xNTC zIHjcv&TrT?ExJg{Lp 2vuQVioJMd$Zx=-yO5NmSq2DxYT>ESLR4@V9-S6 w}kw{*&=f`p=O5=K4p;sWR8R-ifR_`RkaT{-b}(%QLr| zU3WMUz3s*9o%NBmSp~*-eqCNxvDf#}*GO4WR--Ea^7_!KU$d@!ES`R)qnvHpRXy4D zcQOjMF5czp8*IAk{Ish(H-&E9y-g%*=hq-5eRn&njH_p}e%;;fmufS4v!wI+&odG? z3UAx?RQ>8rv$xOw#r%?fRz2(La=&F}%eR`n`y2i2+J7KMTd_1^-V@*LH9=}0&r4}7Dn9buy!2*S>_t1Sjwc}@uC5=QK9(MLGVD{@AT_60 zd)x7X1v4ujRq3mI33{{Q=E8|q$CZ>;<;~Mrb=5U{s@VLByKnj*9rt+`I_ yxy8HEY$lI@`eQPFuPY-ca-Wht?NPXAbxaUfF z-;0cR@2y((G_+{d&d~5|e^B4y@haJ=KewlaZg%;U^T;Dy<;tnmcQ^g?_F1>0?eu9@ zlP@Lp=jL{8+!wAB%2%}L?W=q%!?^j8{~7wqZ&htRY@@tz=Cz>m>Kz~7AJzW-w%9uJ z^VHrOKBB7s8A{`p)cIvUSf2VKvBt^sZpJ!~eYecj--kGTvlh*?>E5*2MRMDn8;@1~ zGe||s{!JAq)Ko8DxKrqNLRQ^Rsh01T_g&dplD}Eo{Y(92e_30zP%%|+)@N_N6|G$w zI(tn(by`U5)qgy$zI$zd@133X>DG@o=caOTz4sTtsJ&0;Ny)8{yx)I2i*E(rn(lhI zR5eY!=}K@+@ym#fSFS3lY={sHK6>)1y#2=Ri~IDy8eUbe+aI24zH9f?bDNeuELcDF z@|7!BJT`gT^u^x!ZdqToYSp?|t7^1XEnT;2Qst9LnahgydCEO4p7%8NYH0YXRqNLr zFBK2cjf(F(S-9)`-DC550?pTK<^LO|zP5g$O?YzlsZVS4g4=3po~mE}5 #tnvFIx0#B9C{=K7msAZO$>b`yXxj6j5&=t#YRJ z*5^Q}U6Za#{hRzP ;1`P zhQg^)Q7%Qfropqf8(+0dc6GP@qFlddo%E0FLpwY#ar<#MM7+$Acsh5^Mn|E%pYwJX z`)XgmG%0*l-t(QI;cr}==f0J@;xBbizMlWk^mB3hVy7QJZ6y#AdS9}ZZ-0xQNFg8R zkNORn#{@l9O(z`|T3&6u^Og4UqR-xe#=D9=m8Zt+R#GZ)TVyIIoXU{-bldIngq3l3 zxSL;F9NvE9xz?uXQcHU~-?+|Odit-P?ur$8w^r`HXPT1#sm#jkKZ8&A-?Z&p{ch-2 z{hpg%f3V_y#`;@x_8oEBv2j!M^X$uIR_4l+Z_O^%`TgK(mg-^KH!F^=d?#ugs%Kul z +MI! zs`Gjibf%c_g}%$Uv1-NSD>*Cn%wF_qqgnXsQ1h(3CpZ5@ew5C681DFf=G5c6r+G>2 zUd0t|JvDpP{avnHRo}u(i 8tI&Hk*PT{fP3}@Ta!0D)9FAde@MU zoORhx=bnAl_fF!8tKW*NH+g>>-Qr=3%rbhm|Fp=CS|PJHeu?deborONq#T$crr#X8 z?pwCZWOacVx8KdvT{`QusjKnz8=Ll4&YGW+w)@(=v-=b(A{XTPrFPyfs#ca-t>@Tw zUbj})JYK4+{Mf0E= so zZ*^(sAGmLo*Ls-t+B@aMvbCoSEah(8_cU3p>br8TV7$+B{U-Iem*G>(UaScX*Ij=l z`)J$S1>(Fe$Fo-!af{k6TK3l0)c9&-(2eBkJLh?CJqgo}x$06J({ &kV&$q?`xl)Q6_PZ6`O@j_?Gx`Z{;t0H zi_gA)skFt`m3piG#IO1}akl#u&A@5ud}~d1{QUWA24D8^eH+swzwDItTlBP~^!)U_ zeOla8u58(Ia=)89w@gpA*~@LG_ph(L8dh5t{B6%2iw)PfmrZ `giuP-sZedIohXW zs|-9ZZw~V;$h-FNa_`^cbISDVjn{p)JsY`gmHOGQUN3)r*uLjW6JvLF-HM #L)&yRW+)EsmHUJ*VT!uRHC0%l$&tRvwJqp8D$C`k=y=n5PHzRI|#q ze(O&ywYnU?JVN!~)Bec9@};)t*4++xRn^^^cKgJ+uU-8fkMB>6>YBY*e&37U+W`gj z&u=Hr|4?(v_3SHc<2UT57v)~fem<#s?K<|I%8@IV6|LGUu`YX>`l3aP%d(DN4KsNn z{%X&%zp=&Z=YBQ)F4d{EDy{z1H20k`-}SGp3yr^(^lN^aPrG0KmY6-~RxF&k#{YSd z*7je4{LV>c(VRT9A|wu4YV0$6yi%|H`T3`ZCm!8Uf7`iZTDDBglP^29SAV!%7xwh@ z^W!sL&X(VQF||xed+D}SQm;!-&9hn+)e(DE*2O=|vOjBicg$5+*KeAqmCO^()_bn` zJ7t!Ayv^LFq4Tb*Hig_@le=)mv}r2w|DI_~e-om5@_F)hFSA_1%Udq@%-mt|@a=VO zrz1Plob8WZE19^W{@a0fp(RtfuD*EEyN~m#<-M~PgNv3G#9h!$Jz)IelUA G#?pI#lc;#zUcm3+U8!wfVPde69zqGsLrdifnn|}W+&wanO1b1%}-d46dYTb8P z!%Nv~UD?DvzF&^hpR!4(=n0?itrv@mN^|wQw3bKK@A+Bd%-=S(@?|^g7pr$_g-eTK z3a{S2xa^O9% bg{?RoLc+iwqUkaa%;@&t@HHDZ**R{ zq4{7%VDFQ?Z@Q*tZB5PiBe*^Fad@QK`cK nQ^IxTKQ7-r^PTFgt4G?z zd(USrzIta>R@5|YxnJLp#s_u&x%oW#Y0}@^D*bCNDc9 ~GK8+-lD zv-`jJKFghc`_ox8 z_pU*fL(7Qg!Shq$thsr%B%`RUGrzFCtph2FbaXIOMy zJhN!t-cqLhnu~k?T)w^jZEmS;aBXh5-lffNlW* LS^W!`AMl0;S zHA$n0Tcz*c$xT=G?x{a 4%B<@0QwCGiyavop0x_ zJh*DwHr?x!T>2KZPrO}GZ2In|_?5n*sw)T1W(4<|TCBF7DZ%MgG%0`5PGdVkw#EMf z?pM@Zicn3y^mB4ZTDZq-vs1USL+vh_g)Qk=Ei?J5Y1Gx^S7tWIfEC_*z=~<&)*%i}Kby%UW)vzSaEx zH_5!^*0XJP);|}tf3@(!%(bzR^Zwk-su!4c{=tj0u~sIR Zw?6(75OC@8)sVW}vu$P@H+fuJWab$vy?aTK_MKI$RwcAYPDwM{qjmpjc+?Ax zh4GU%S+Bl+ ^&j7A|uZK>}t>ZaaH)B_{{ldzb-L|4VZV7Cn!~7 zTXg$Tv%7a)?_Jdqd4GRVuG02=`KUMZBd1&rIUebEGI8RhqxDM4Grn2MTvz&aG59F! z%doHqr^;u_U3|FI@56e&)wAP&FZylAzj24bo=10A*f?dceRkpGqpeS7evR|=IJwwk zNlNL3!&Y0*xQ4iTin)X N>c}Lu)7tUh zww~(w9rr|a#o-+ fddjHDG3RS*@@{=3>S^Y@aeR*Tw kGpRo-d2AL|y}nyvlw&*D=L-)S$+dbj%Y( xx(339WbEWZU&FBdvb%{-xWsz7)-CTA8(I z-Lk%nl)j9#EIUhyC&h7BeJ=(EFIu#G&4RNbhb33+O0VB=@>9Op&zFL=UEj0=Yl07k ziXXMRbhBEaqOo2$cGfQYDObb%<4bd!v|X=Nwce}LUJ-gFlz;N2vUepLEPua$seJLa zl65k_aqF_BJa ?Q;8(eLp^>Ps}dvYfY!F z$=}1XRvg %=m65U+; 0sG-0j@;U)$Ek@-{K9mOZ+A_r^z+Hy)L 9MUB-i@iKD?Urjo)d;6!hrQ*Mzl%4xG_vD(X|1No-cvh!+*`M33He}Y1 z^-o;y)}7Qc&{l0$U*uB%=}**O_tIst-~K%Df0iE~9@{&2$;_nGU%m5F*2n)zJ)i$w zwRqkp+v-<8ckMjYd1imqk6DYmR$Tn0Sri(#W*V#i+Uje|zs@Y@zV`C%F5v{PHY>|j z(x%a~R;>I}^2uwS-P(DxYokl!g+o8`^Gp56Ok8>@>-@bX{Z92MKKnnO7xt@P{xsy( zvA JG-8}>wa{6@9I}!J5TIh zv`XsG>s4Q8 zJ6GpST)1n@(?!*>57yoa$$PzjbMXm@z0=Os7yX)jB6xCOjXL+%*QLkoejBfOelzCN z8RvtKCxt()3yQt^bKT#aMx_sHH_Y1DXWFyVy*EY2cjEh13nNmCYQ4E KUZ<_jiZ5Aj_iIl+y3Y4<*-pXWYwcBm_c~vn3qE`=RrIsyuJd`Tl9tzs zy?Rx3O#RQCjX!Mn+1Umk6%`c>y1v8WvAfKp_X5+r#NG2x=K9np-CfIH_bIqIuXpOa z?T4PaD_P&V>iVj^;zqx~hM#XgTut7+ZtB}h-mANREk9ablY9R2-8m~8{;rW*mA^jp z^{>AiuRnBeDT{S@^J>2C;>D9TJzP?$y4^5YsW)DE<)f*Yul87dGMl$7Qufs?-9J&S zAy#guo)(A7TrYKXy(`M^S~b6Zxmj=0WuI=r{g*3KyEV6k+|mpAbL!r|S&P(Nwaxu` zZ+ut#&k*^1zQyG%U&*ui61Dr5t=iTsvUb*a-@u?1o6n2d^tJX^Z56bfy2dqjqHt+k z`+{#wlm3K8&420r%<0OR^dI~O>`$-tt+dtCe|-2sNx65Z>8s8wR|{N&{k~?;m8tK2 zmOXWyyV}dwuR8W`c*4wdUAAg_#?$3fE4B4adGF1dDEsiw_urC#wI9se>NmCP$i>6K zQ>)Ip>gE6XsiY?`<=%{$FOOaNCs+`t_T%W~U9lb~)`jV4ZCk6M*Uj{$YTav=npgX0 zq- {a0OWN(<~XBTgz!U3Glp$=xwsv$eFUE*w1ea7Ee S}mtXzbNf zt3aUqiSnDW`p0*FeQ#JdJ9X>3+2%(w1Fx6{pISfdcW%~VX{A+POFpdHI!Wu@I(K)q zSI_rKoAb%{911;jZ7SEPOZ{Iz>)-q1z;KLV`M#Sof8Vpv%l+qd)#d(|>i-Op1+C`a z4*TCPygkYMr Dl>9BiwJW{ N7Y0-jggZe|6Dk|BE|O?FZL1?|d`m zknEqem)^YpTb1%}F?aHR2G#h`?`HXO^4nYDg8wtz@^@g|Ij>Ud{Py%Xm6N5_vX^f~ zhH7k_<=?Y9s_(_Gm6tELtAAF?(yG`0^Sgt!V%Nc>Emy-s_U~S{xb^d?_g9kxV^;s3 zaQE)QnJqWZOIOW~ww`#t>9_4(QTw3jS!v6EY*buqWK-6!yYb5HJVC1}t8Jfrr958z zyuI&t-c{Gz%UAk+oSYaZue;o9Qq-3F{n}gW?7o%q>}q??$>BbCUw;3Uch(ZW&F9}b ze5C&GXZtG>T9>^(tbCmFLcg~d;n?9RgloeH-?YFTi z%d6t;Hf`NaVwW!;I+AK?s^jZB>)4{4Yg+{c1%*>woX; *V>W^T-?-Ad|BidXI%S=YDuM{PCN`gOY1dr#`hxtDG0 zv!{k!u79JawB>G$Z|Ta9WxgN3Z*jbO^VZg7&%>6V`p>YiW@f$AFV`m1{|u6^{3Jyt z*6dic>#)t)-^sn_%D+ghH+b8Bp(f6}QqQ$yzsGrrx8J&2LgQDa<>bcS-{_Mtaq)%< zjennt>RG?l2Yt1jUVnJ~d9{DsZ*uIkjeDGfEgDZ2$q3zyPdk=xsu}L@8Yxof$ s`>xQED)H)Rma;roUi+H_nci8~dv--{VAMX1{ZW!@ zuPZFr=zcyn*4^#w_Gk0bj?KTcBBM{WblUvjl`l5L*s7k;3tx33^Uke$F`at>U#nCC zTzXw2<$hY4J-vTW&^+{|;oL_BKYX?=l|22YPt*M9hfSaB^6z;ky$W93R*=7RdR>@G z+``yZn|iuznzr_It=C_$=;YGg&do`s$0kX=bNMy CGf(Qi>e-QW zeU|r9>;DXaZ|WEAPdD2BUD$YwacljneIl>FOqW}eY_o2@pUxK1PsjG0nYOKuUuAyk z*X7S _s5e*c$t#pSn4z0}WK`L?Ehj{nOnVbA{z6*+f*?hN|RaAxtZ#V0my zW6ucBxcbwwuIJ0$_NO8{_o-&@4YZ!u_1;}H*LwT?S(Xz0S^u6Z@>-XcZvQ0Q`Z`U~ z->BYbyTwk^O`B_{UYT-d(~136TV8(Cnr*y^J7jXiZo@5ZU+;Z?pQi1dJ Zd@Z zi+e+B_k6IfUthG>I`iw+QvaS)r%i26{VwBo6 t_>{ eRy0K?b zNSp0{2H)5F&ib!kG{5-5DbH;akIab**NJ|x@wwmh7F~~n%csWM3D3E*Bq#Kr*1QVE zr~Z4}O8>5kOiP`5F`=UM+Rvh?3fm)BJX^PFV*Qgxon7D8zsU|+_P#lM&UU`X+v7Hr zPELEW%}S n{m*dmtMKt{#nJUmb=>R1c2$O1_vMC*&e9A0GD-Aa z H k^D6W|gT{_^UvKgrs_uDtW}VxvNSUu$`Www}+AVQ*dwsR(^_pL;O_}w1Hdm&7 zpLI|3rDFfOJD*P7NPKqtOX&O1(62>#xz1UC cc zkw$jYW}5xF{WMtV*IcQ(a|gHI`0?P={KLTyUPWpDUETdtW$$y78#%&9p5+S%cP*PU z|Mz^>&*k5RySG_)WYwLwV>VbTbU-WYetdoH*3EKFZhDu4qBIjzg6&H_Op3iJZ}c^! zW>xo@*-O=ges8yrTz2)|#XZO7YKc|X$%~pc_3nN*Q6*6MS^gu{8SPuA<}6q;tulO? zMa24|--5!WmSs#`OC2jern??Fdv3AeS(o$sRTEQ9wS%Hty{dR`m-N~QhT3NDZ>s;W zt?;I1{fB(%RT6*2mK7@dmMw34w070D#FfS?m(NpuH7R`WZcll0SC5FbRcd$Mx!yCm z9?y1p^S1fjdJ6A$J)e0{HqUsuIGbzc;;D1atld+zEzI)XI wnRkR;mb^XGT z{o_o?YQMi$!X+=)*qFF~F0D`Iyf{zV $BIE zp7zi0)SjKa 9^Hj`@Za@tJ8gVFU#LE`(4MJ-9HwV^9M;Ux%{L@>%+8 OPYYXL z=>@BaHs5)-(dFdQMXf2xa;tiaeyIJf+8(S>zvIq2@vfKIdfR5b7Cp4~?w4z?Zd|jP zC0f2@qSsZIuoG88+)V>TMMXsg0}k$JsV`p~{`5b?%&fYfQmv;Z{hdAOP4#pBuw~+D z%0aWYEOa#aZR-D1D1TG)27jf;ulnw<-u19}XXFO%>AJp)uKIjQt#jo2z4+%I&6VK? zMTO_Ngob^pSK8(td!JuMIdeMajxA-i76(7xaqH}Hv%F%`o9pU!yL4LVy~x;$wwf|e zV<)d{`4(}hQao*2)8@monKpa=GZpdsx4CxK;oRMuk4Bt{&2fzkS*xygU+nJfukQ-q zZCe%nc3YLztE{S1R?BpP9c?mIf(?UHvuEvIv}pNC5B6j058kQ2RHu@)Z0F0bk6te_ z-!=EoBAY6`YSW l^0)xEOq {f1`IB|dORFN;tOQja3T)S&{e6vi}wtDM6 z`zg^2c15p>d}t! ^nZUp(U|n9) zw>;F^z*KJ2?Yl-_8;<+Vj$T&d@pXCarm|44XTS8qc4pqR4m`Q-&foCqv#V|#%fIk6 z;_kkeKD#g9S-E&sXVur_TYdf_t{*}^y67is)E~?Jxz4n3bL@%-JEGQI*%TNSbnafp zs>F$1CzXq;Zav!>7<^;-{H`}fA8&qNb+t-z<;P9OgVSm*C57*N@??9e*u0;24?mcA zdXuNL%#FLkbJpi;l@`5QGA%aL&5wU&d(X0`qN*3pdscra|J=EvFt+@1Rri$8+{2$s z_FlN0uVmLYss63ksc9S6hCKC{R`F`8WoUVQ?JiTx^5)8Qo020$H)(bK-YRu6tYb>Z z1dp4m-uai^nRxlg)$mi_i_NF5alJp&)^+ALje_#&1$!4n=Kg00SfRW%ZP)6F`&H)M zy>I9F=8C32k4Q=IyQk^3(#qBIPhJ#@^S|KY{d3CVh3+e}nI&WQv9+9G5IE8?c_Ooa zeaqdw7H6)n xw^XE46F$_SK)Z| zzFC}c$?M?Y`6oBNI({)uIXm>U?WUF8yzVZ(cV66jGPBoT+@ )QA=D%1-UdZr$~mSt~PZ !Ydw=7$#bJwc%D+T$@Z_S-~ zEz4|4{3q^K_o>(8-kFMCKD%AZ>t{;w)f4lUgvRp+=G3mgyWrbe=}WiQ&3bJ5AuMgD zuJwv{oZLV6sV$zkYLfO<8`X)cPhI=5$TDtsmCaR#oSC+gZbEaWTSwR5DLgM&996pd z^1(~x7t8g-^W;{qU%C3no^`ou6Ly)#PFuA} R 38wzSA(;wraY@( zb3L?P{ix|#yFY z{l_5v7u#HKikX#Fe_X7e&M)z$eD%~AlV6^zw$__`i#a&?%*NmwuSF|0^ZwSdZ56HF zIcIkj_nB3zI=8-mE_rvhZ{e)fXa7BQ&)j;^Fy7SFr{kEZp!rSPtfya1%|qilx|ZFY z{p4m`d1KMBgHh*iJ(we3syFTV&2=^Wyl>f}XXR`wwb^=duinSE7W+@VeYV58s66JP zi~8H;o!s}?cGl0G9{Ta$uc#iYrTsfE9)DQ2)mvFQ+UxCA;Y+ `R-cMpb^WAOE{>7U^t*Z9$Iv#m?N;;f>X3yrG633&K zwTl0|WIl1_@0ch4w<^8USBD%*FHvoudMC?OZP(hcAD#tY(mwaKWY;G=m+Crs^uyaj z{~44oToasm+f ;*3%Pt&I;GF;uV3=S;I}q^vY)z7%U+kQ z_xt J-`b@9>>@+x6ZoylA!SXISQ=jr%&+#fOA^HFgxPPbu7JpBP}ZJn-qP zxf4(SS$k>ee};YGn!ijR{`$K#{b{}Y&Vw7d&K=u)-RIYGUz1~x%NO-&YCU|?r}uev z)bqX4XI8$xUHk1 vq~$Z_U~o z>HlEmb}Qw2vklojl9nb`JMH5ZvHWN7=>F@zG)^$nrn!~Zp!uha*H!sIr>NBBOYElA0*hm2ORqm%r1jhN(xfcux$72rMeWLrKCL3O@znK=C*Qid`roLs z(%!snabthq+w<3C;(Eh2b@q0vEpMH@=E5T7$jD8l^>;fr%Rhcmw0n`^sjWGAl{;4c zkUD>J`qNLfd*!cR*%)O0sO!w#dCZ;@AcUrr%cA=zq`2LCaboV~5R9&{SH|sR>^psG4&-KYu zh3A=Xysd6_(s9{``UmX2QvWzhuSq}YTe <0O z8ou$;=G=SN%~NmPY5zE-UeLAflDz5dtHtq5vx2v?#T~GSc;@Oj{qYs^?9f!xV>e5d z_pO_>YM#p^sTnC*mXi!mUOZbIDk^HFrFqukXN}0=TMvWpfBZRn`%j7bw{QM4T+zJy zH}$;8`dfSTYma|hV145k+rQVfM)iTK-)8@3V3+@{zu4Nm@#4Pf(EkjFrvEKDv42JX zyD$HKy}!EsivBNivBMt i~Xo04BS z-Yv7qRNg w?$~Ln;3Rh)MxK$X U(EP2qic4pG6`hVZn%wL_x>T_P${;XN(zIlIy&vI-yE%Q??d*x1@x3Tw5yjrP| z`Sf?x4O#Vf6FFlK |J)wRUidd`iQ6|1^l z%sLtp>KfZAJ0ZnHVs78M=Ot^FZjWA}RPxm{)M!b-LFwRf+jFrK1% CSo&_CdFr-k^_(vg*WRrQUhKB0G}Pr)aNB{TLav~N>?%8N ziUcj~TsM2!Z11_fx*wXh{66)8*Xq=atNz{ZM8jsrbbi|X Y7-FfxN`R}K!?s{eM z&LlYa)a;|yveK7tb {kL8|)%i9gXm9bFW4i;5vyV9zPgLD;UpsC6s;UaB$hh@Y{~3Co z+E E`#X3nmbSsw6xNlv(B;M%Ob zRjW5CN&J(4@}Hq)dB +SuxeWldng5{CAJ3`fu zhNYjl*BKhtbuLZ+wb3;5ttbC8eAawxpr`xgXUXa=UWenC>|SZ7{@P?Az3u+Y$e0(? zza4wF{;k$cna>|@&NJ_4yWGEMPhHlT+^~QR5j-ln$23<~?e>0p w%w-Bcs1rS=^hy<;JeBtFBr- zU2EwVAAV0|r|7w=E6LYO+syZdS^PU@Jay$)Q?8;j({2ZL9yNV)RBdnC{(t@TJLhHB z&OIF9-?MK166ya8A5FtHr`1oeN-x`K`e*WSuH$~Y-|T(;S4^w6Yxm!dr91yf<%v#j zzjw~N&NtR?W4vk5KA)!4>OhrT<$7a}vww5L3@@wy@cP^7Yc;ju&X4Jj*nLatjy+dB zZn)?2f$ye!PBM8bxlGz=AKDuk_ddzI_)5JSWAkm3<1CvVhef4by!r7Jhwc^w^FtDf zy&I1m`7L$D|3;>rQd)e+ng0y@;%_d6ecz}5X791v8!h+`Kb?J3KWp{=odxmb?`@73 zPOOM&?>-x{K3x0HcKzM{yVm{w9Cl{^wana*RiAvscBkG ^r(VCB@E(|XUon$-2? z{GDH}{~5MK-wcuGk~*{LmPyou&KCzwpUpk}b?+9v{QnFSkCjFJXE@@1|4W*l_IIBZ zk8ap;`pVrBT`L-0I$L9De|^5L_4I!?gP(4z5f)!uk#qIOwwoWH39kBGb!nshN}oT^ zr`Y~yNb1@B$^TB2+OkYD?fSJATc+upmOTFHw#qs8llgmYFI>O0dZMUcm}b wCTIj*jD+fwx8 z)pMZ}7@CaE+x*G9vNm3F)~2t&lFPDcJ0rE!zbMM?m{ecBX!_BAIiA0t_458adbQqt z>c{w%pMHGbnS1p=!@f nj{Y-r+)0t;wLq5M`oBjD_fk(8}zP|5aH&s((pYM*p6#D0i-#_1u zm7Cu0eI*j>xnj}Ps4l^xG}&b{p5Eis-)R;a{;{uge$3Moo3i$PPhP$J=J_stxd^3y zdS*&*u5C5DU0B7~xb?;P)m!-XWUS8z6>&>-A?#@D2qg{V> z>g;S24|!d-F!SxfnQW$E=enO7mPVDoS+TCEEMH{es!w~@-Mqahr#|`7i~PjYv*H`B zmiYIpRk5 u$u+h*z0&U*FT5sciLQabS%o z@4D O(hSRer 1^rQlKrJC-s@gm8+z%$d2zF4Y|BkHY(J{jDO11V?(bvUr_cS($86j; z)o=6WSU=aT*8K6cI&rbGfz!oB&)TbAtt^hUD7t#b?&ITCe+;58HS?W*y&_lRx&5P8 z9gkNp&e~ZXnp3p(#L>`KtImanel+&=dYf~7t?%ZOj&`}KizcP5PVshCF$vl!YW7lF z`rh_`-ItGaY&)BEWZj$Wh4m+LJ RS-rDKq zxf>g2Jg=xRHP`j}&v5 XV_MaO!cGs?|pSL9_{ifE;$n3^5At7$cq4uGHRtwjD zJX6#zxbbpy?v;<10)vbvY0Q`_HUHYGy`U`(t7h{z`AHU?v#Z#+=AdEp+c4FngKyXF zek#dhQh06CtjL?n8ZTtI?_3G@TJbWprcyC>>guaDf45ok)-IpaE15Yb@9NL!wZG%{ z%c;MZS6RO)GVNqeu9m6#(o2(S#GlWZ #WqY&rzQ)$TYIWtM|{zuC%)HbEt*-sNz=@C`!n~=r8iXy7rvEAeYy0R zN=c=!j@CZi6-^PL_Q#Uy9gl3BEj|C~SKBov?{*!v)bGhFuD6k^Zk@X-V9n&On|~YI z>`t84{dHHC)$3H=R8{9n)usw*l_K*SYePapjJT(H7Ve2&q48QzbZg196_VbrJePNQ zym*)#xTeo+)th6cYqON2Hz_HpuekQ|$h&!KZ2mJ$6O)TipK4xz_fd(Zy7aB=KBupL z7W_CAw(O;-tjVU;|CW^3YZ-sv$x}Mz`Hl5A-evor_POolZ$q5 zPc6%xWcYFBO3`0?r$%mR=2$lG_k{R7gJ6GO&zJkE+HIwIm8zzm`kFoW*Q#sp_G{;b zPLJGL-1}#4!CUSFo3vYfgT4n{4R*fg&t-k^{PdExsk`%E?)_`)mo@G3jpH}!opT>( zZPWF<($yt1KRhd`DA-K@+{MSb6IV@A(Tm%3#d2NPzTAm|qG!`xi|=2rvHx&hu=U7c z >Z*G_{BZ93vwx@Gom_tKIRD3j)rXeNdwC}4 zXuun*!`?abDrTQLHS?xa`K3iw6F+_4o&Q{ {zF?n;a2higx-m%aL`%0lD& z*5fTNe{h`3GWxY&bM@sr>z-P_SiN#(%2eGV+dD7412^d_{qmZ)Gdz8<_`$${uXRVB zS>)(?Z9L-Z9u$_}^ssK<=b2Y}mi;JJl$R{}UZt&-|LL`2{+Ct%83JE!Z_kQXeE6HS zJ$Lt 7rSL%a76#bMzd1S7ugXed7AE^qto}kq&QP-4=N4tXtvp zy4CyFmqeDN++EQ5`-c8;mDej~vhz!_`fRS0WqR_XWs>#mT}w;4Ztc?j>zlIr-m$r5 zIg?DyvL#vQ7m-I01_p5l%#OCRw)%vrNJ!D6XsuBvVH#fz8ks!ik6jP~Yxxo1WF zxv!sWPrlgtagpAs6|LXqoHkzVwN7uT7uUtyEvX@G( s5$LmA~HVYtpmzX<_gk4fUsSo`D ((w?TYq8C z4*%x+zE9@%@3J_%?Vj}N{|v`=U$_2sD(+FS!SW*}LBC!u%~stJY}Lk=|I+Ja@Wepj zS6QppU+IeDPrRmYrg+nSQOQ)^u#=_}zpm(-T_q|S?057(gII`ePLI?=OFQ4cf~t>~ zDG41 &-6R zCYk$l@3O0r%7O8YRa3XUnlWep!Yy6BRm=a}o;N?zb;{HUcQj|32pu;uQhQUA=WcrP zt*c*GE$?E}n=xWm(NEu;{2DWV)$i5y!WD1&`8%h5{&ezdZ0+%dtM+Ce&d{7~@-xqn zN8c~)r0ONMt--gt*4?-kwfdI#qFL^aCR=yDo%T9VNMBcXR>)3Y<^7@7>(aK??=KF% zwIXnWQpOuTS+B2J+xAR~Un%dOnyLQw?EcdIK>^ ?*D~)yzw#@9Ani z@2^q6W3QdhRIRIFUq0sKhGvJaT0K=TNY7h4vG+XWYQ&;-CpzYBwYa|et(bXUZS0|S z^SY+#Pu2OPEHQtpuXg;5v^kltc YRlcE8TFO&H%RCZ%FUG2L zUAg^KJGdnBPMA64q=%svH>0P7oBd~4m;0YVDXG5H;0BkK@ *LK>Qs^&~QcC=45#5H1ZN=|ZU*|Vp`-?H{bO%*>n zZNA>rLd*G&Pu9zqimv+`DZ+a@|IC_mCEmZ2!`8o5wlO%CHBYH!*Ddd7^KX34?-U7H z>MbnywpQx*tW7t2f}_@7x9-XOJGr#P+j!rKo2Rb)XNZ4q7^kw?{={0PohOce3JKqS zBH?$+UD@9=`s26qyg5>!D>S+H-i2I^izlLf=3c$7@qEu>(ZEQrEA^4Drk8vOyL9Ah z)|XdbzRg~C_3`5Asix`v%~LOZom^hGC)zZ2`ay5`SwX#iT4wo|I3ueHJa(@;SSMO@ zWw-GME3Z}g>ieE;+^xIo-O=Q0UPrPQW>2hIJk#q|nU;R@toQs(BHymR{FE3|a_9A} za~_|%LuD_TeF$~aHQv8$ZfM=sU-hSFEm_jlG)+UHcfw(AA&bY!foqck&0JjkSzNw~ zZvN10^0oYO%{q-=^=VOA@!S99MFjt6s4L3n-xxjTk- u#`v+X>g?EWM&riD29X|Etm0-_6Z(;LUkMqjTO 1uIKmRdK(OxxQoT{>{#ei>F2GvpS$y zrdr&%{#k^q$DS>(_U)bTwdKA4`>VDtM{Q1i$UZ)A@dwoz*J3wFueC9_8+9yZ?bZ#I zuDnMjR;_t2xl7A(&$*j#isL$-D_N&&>0DXz!!Po;&fAn _+FBof_wnAQJ$^4uV=rc!O`3J@ zy6fFF{1e6Ve$P$~Gp~yaefBhVV)A^U+MU0C9*OPRbnf)o%^aJ NWsoz}jiD&Mw_nn?3yYtj@tyQa5)qf3*kDT6gI@K?0sr%6qog_oAz{|oPw(M_e zo?2-#$0_NOrPQnFsmtDSi&pL2ylL&a5dW3R{q^rBtagh1##6}adw6+f>8!53@4GIp z`nI#sGj`v_qp{~szPR^3YV}; |6O{Q?Rlzi>4o3Z&s{Efw{MQW(*ouw6@SY) zo>wZ<>jf*n?0$dmNxAs@Z&y8*wLDtp=PR|&|N1Nu@Z^fy`|GMzuhSAXnMNz!dtG_4 zCb0VZySC0pmr}YtmSn9`IvFk;{5o#+U$^7&t?VzV%QNov&RYMU;bQiG2Hk%DKPG{H zVh{Jtzx;N4Vejq5w{LFM$w)h9yJSkecxITf|F*kFm(@?RzsoIfv|`_**-{UVx97|c zEb4shCCw@`>2lzbl``L}_JzLFSaAQTuSv%>LqYM?+v7S`&3a_x_H)Ah?Z&;oZoAI9 ze|Xuvd#Vf0ZWWv4)idd1-s-2T4zIbm%iQvu+t-|}9qXEGubO3@csH+W!e*&sD-2$< zs;;~4$;!H`Nu_PxJhA#uo7B{sl P(w^&ZX> 2E7cFp7ZVS03j%)_F?OLx3FU%d5} z(#)$ |oPBN!wcyYzu*(vh6>&chM zb;kL-Z>QzDvYwkeHRO}}%Z+c{P3wz($kcycbTQ^J+ZCP5VFfYv%#(eyOKv@lELgUA z+k4YPQnTdsBeay0Q~b~D<5cZDaZF#m?4_0YuFyKi+0V>k*)QmH2OD`w>U}ZGIu-4^ z>C#lbW!_g0EuQ 2BrT#sl~B-vu6* z&%8U?cdf 4lwEf<*QmaXudVh-Vos#nHjk@%XxV*1hw%rb0HOuSNsa-RLEHV>& zSA9S6UEgi%TG!Cf7ajGj&adtt61)0Otp CBn6d#Rai$)!KdyeC)u*)M HbIz8uKV8QxT(@$xy )z(^JdV&6;=DpLtd4 zs%c@HBo<{?PII>nPsuVB4Gk^3u_V7$y#9!o_L^CTmqn#_opzm&CNXE~>e5`{u*?@r zS4E{cyYH^j>5I7Y-2LjwEw#dR+iu1mE;U>EA?&QisaM*Im+yXL_;|YKOEtF;^Qw?G z_l>u;it~4Ub Z(VIOhuH3x# 3@%%BX_ZH2ro^kxC2LQ LiuggWH^Q?Y(PSWb)PPH=1Zdnsj zZl!f>P5Vr#C-p8XVy~`R6ExG)O)uz4&lyo3H?=(mJF0h{U9t4;_LP09Az7@O{Y>q< zb Ww~ZJ0)xHdV>)hVXgo?@GHKf?cV)%awfRe)7T>vYbH(ay zueUqB`>kfdna{9C$1!8q_fo0G8KHWvGeft9ow Z35r7BbVjSsT}CvIP*b}r=A z*U)wAj^+s_Ki2Q5Ubp!E%jEhsm)CNwZ<2Ni+IU*_xQ2^o^|PJp{I>W+T#b4X;;G&n zTpxOKc9?JTor~A?&+0FCKDp2I?fUP>^8em%oBpQY?eSB=Pc rABg%|Bgk+9KAi_KiVus7^?dg!xBt5baM276z9@ors7%-hhA?0_x7&+9|I zMN3U4oRukdHV&QS_HFU3Z>w@vtk^3mYU{FP(ULjw!4ncTPBqy2uygUxMVG_FW2~1S zT=()r`i#|i%`fkTXD(a6c*Ct#8LyRdE}Z5!j#ieLy=d8~d5bqJc=(xf+ljt@$t^ph zAKp7OY5BXdXOAB@H}9-A+i_w?!j+P(8(zj$z09w-st?p!GvT8C>D604|9H