From f12bb27bcd1b36148090d6922a85bf1611d327e0 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:39:37 -0500 Subject: [PATCH] feat: make issue monitors visible across task surfaces (#9783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source control plane people use to supervise AI-agent companies. > - Operators need task execution state to be visible where they read, reply to, and manage an issue. > - Scheduled monitor state was easy to miss because it lived in a description-area card and used inconsistent, mostly static time copy. > - The task page, composer, retry card, and properties panel therefore needed one shared monitor-state and countdown language. > - This pull request adds shared live-ticking time utilities, moves monitor status into a top-of-page banner and composer strip, and redesigns the properties row with complete read-only details. > - The benefit is that operators can immediately understand when an agent resumes, why it is waiting, and how to act without hunting across the page. ## Linked Issues or Issue Description Issue monitors can schedule a future agent check, retry, or wake, but the UI did not present that state consistently or prominently. The existing description-area activity card competed with issue content, the composer did not explain that replying wakes the agent early, compact properties copy truncated important details, and relative times did not share a live two-unit formatter. This change makes scheduled monitor state visible and consistent across the issue header, reply composer, properties panel, and scheduled-retry card. Related prior server-side recovery visibility work: #9629 (distinct scope). ## What Changed - Added shared two-unit monitor ETA/offset formatters and a live-ticking countdown hook, including compact absolute-time rules for Today, weekday, and cross-year dates. - Replaced the description-area monitor activity card with a top-of-page status banner and added an inline composer strip that explains replies wake the agent before the scheduled check. - Redesigned the properties Monitor row as readable two-line copy with attempt state, due/overdue/cleared wording, click-to-edit behavior, and a hover/tap details tooltip. - Adopted the shared two-unit formatting in the scheduled-retry card and added focused coverage for monitor formatting, state transitions, visibility, and controls. - Added the approved wireframe package and published reference: https://pages.paperclip.ing/pap-14557-monitor-visibility/ ## Verification - `pnpm vitest run ui/src/lib/issue-monitor.test.tsx ui/src/components/IssueMonitorBanner.test.tsx ui/src/components/IssueProperties.test.tsx` — 3 files, 74 tests passed. - `pnpm -r typecheck` — passed. - `pnpm build` — passed with existing Vite optimization/chunk-size warnings. - UX review approved the rendered real components across desktop/mobile, light/dark, and the scheduled/retrying/due/overdue/cleared/none state matrix. - QA passed 6/6 criteria in Chromium, including a live countdown transition without refresh. Review evidence included the P2 banner state-matrix screenshot and the P3 properties-row and details-tooltip screenshots, plus a dark-mode capture. - `pnpm check:token-gates` currently reports five pre-existing `#9627` comment literals on `origin/master`; this branch introduces none of those literals or any new token violation. ## Risks - Low-to-moderate UI behavior risk: monitor copy and placement change across several issue surfaces, but all derive from one shared state builder and focused tests cover the state matrix. - Countdown rendering wakes once per minute while a visible monitor is scheduled; the hook is limited to active monitor surfaces and stops when hidden. - No database, API contract, migration, telemetry, or authorization behavior changes. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, `gpt-5.6-sol`; context-window metadata was unavailable in this runtime; reasoning, repository tool use, command execution, and test execution enabled. Earlier implementation commits were assisted by Claude Fable 5 and Claude Opus 4.8 (1M context), as credited in their commit trailers. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Fable 5 Co-authored-by: Paperclip --- .../.paperclip-page/state.json | 10 + .../pap-14557-monitor-visibility/index.html | 420 ++++++++++++++++++ .../current-properties-monitor.png | Bin 0 -> 7542 bytes .../screenshots/current-thread-bottom.png | Bin 0 -> 84874 bytes .../wireframes/01-issue-top-mobile.svg | 105 +++++ .../wireframes/01-issue-top.svg | 100 +++++ .../wireframes/02-composer-mobile.svg | 74 +++ .../wireframes/02-composer.svg | 100 +++++ .../wireframes/03-properties-monitor.svg | 121 +++++ .../wireframes/04-states.svg | 103 +++++ .../wireframes/flow.svg | 65 +++ ui/src/components/IssueChatThread.tsx | 9 + .../IssueMonitorActivityCard.test.tsx | 236 ---------- .../components/IssueMonitorActivityCard.tsx | 71 --- ui/src/components/IssueMonitorBanner.test.tsx | 196 ++++++++ ui/src/components/IssueMonitorBanner.tsx | 211 +++++++++ ui/src/components/IssueProperties.test.tsx | 100 ++++- .../issue-properties/IssueProperties.tsx | 131 ++++-- ui/src/lib/issue-monitor.test.tsx | 206 +++++++++ ui/src/lib/issue-monitor.ts | 272 +++++++++++- ui/src/pages/IssueDetail.tsx | 43 +- .../stories/monitor-surfaces.stories.tsx | 167 +++++-- 22 files changed, 2337 insertions(+), 403 deletions(-) create mode 100644 design/pap-14557-monitor-visibility/.paperclip-page/state.json create mode 100644 design/pap-14557-monitor-visibility/index.html create mode 100644 design/pap-14557-monitor-visibility/screenshots/current-properties-monitor.png create mode 100644 design/pap-14557-monitor-visibility/screenshots/current-thread-bottom.png create mode 100644 design/pap-14557-monitor-visibility/wireframes/01-issue-top-mobile.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/01-issue-top.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/02-composer-mobile.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/02-composer.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/03-properties-monitor.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/04-states.svg create mode 100644 design/pap-14557-monitor-visibility/wireframes/flow.svg delete mode 100644 ui/src/components/IssueMonitorActivityCard.test.tsx delete mode 100644 ui/src/components/IssueMonitorActivityCard.tsx create mode 100644 ui/src/components/IssueMonitorBanner.test.tsx create mode 100644 ui/src/components/IssueMonitorBanner.tsx create mode 100644 ui/src/lib/issue-monitor.test.tsx diff --git a/design/pap-14557-monitor-visibility/.paperclip-page/state.json b/design/pap-14557-monitor-visibility/.paperclip-page/state.json new file mode 100644 index 0000000000..1e3344d4d9 --- /dev/null +++ b/design/pap-14557-monitor-visibility/.paperclip-page/state.json @@ -0,0 +1,10 @@ +{ + "bucket": "pages.paperclip.ing", + "prefix": "pap-14557-monitor-visibility/", + "slug": "pap-14557-monitor-visibility", + "url": "https://pages.paperclip.ing/pap-14557-monitor-visibility/", + "baseUrl": "https://pages.paperclip.ing", + "publishedAt": "2026-07-17T15:51:31Z", + "sourceHash": "4d276364eb4e122d782247da6f1f0996763c12175ec0d7efa8ac7a0568fb6f2c", + "version": 1 +} diff --git a/design/pap-14557-monitor-visibility/index.html b/design/pap-14557-monitor-visibility/index.html new file mode 100644 index 0000000000..28963350dd --- /dev/null +++ b/design/pap-14557-monitor-visibility/index.html @@ -0,0 +1,420 @@ + + + + + +PAP-14557 — Monitor visibility wireframes + + + +
+
+ + + PAP-14557 · Monitor visibility
+ Jump to a screen +
+ +
+ +
+ +
+
+
PAP-14557 · Wireframes
+

Make a waiting monitor obvious

+

When an issue has a monitor armed, nothing on the page says so unless you squint at a truncated properties row. These wireframes surface the same monitor state in three places — a banner at the top of the task page, a strip directly above the reply composer, and a readable two-line properties row with a hover tooltip for full details. Every surface shows both the exact time and how far away it is from now.

+
+ 4 screens + 2 mobile + Desktop + mobile + Lo-fi · monochrome + Click any wireframe to zoom +
+
+ +
+
Flow
+

One monitor state, three surfaces

+

The banner, the composer strip, and the properties row are all fed by the same state + copy rules (screen 04), so they can never disagree. Red dashed marks are annotation callouts, not UI.

+
+
flow.svg1280×304
+ Flow diagram of the three monitor surfaces +
+
+ +
+
Step 1 · Task page top
+

1.Monitor banner under the title

+

A full-width banner sits between the issue title and the description whenever a monitor is scheduled, due, or overdue. Headline leads with the relative time ("resumes in 2h 12m"); the second line gives the exact local time, the attempt count, and what is being watched. "Check now" fires the monitor immediately; "Edit" opens the existing monitor editor.

+
+
+
01-issue-top.svg1280×800 · desktop
+ Task page top with monitor banner (desktop) wireframe +
+
+
mobile375×812
+ Task page with monitor banner (mobile) wireframe +
+
+
+

Annotations

+
    +
  • 1 — Monitor banner: bold relative time first, exact time + attempt + watched service second, actions on the right. On mobile the actions drop below the text.
  • +
  • 2 — Properties Monitor row switches to the two-line layout (detail on screen 3). On mobile (callouts 2–3 on the phone frame) the collapsed properties bar carries a compact "in 2h 12m" chip and the strip repeats above the composer.
  • +
+
Why this changes: today the only always-visible signal is the truncated properties row; the existing "Monitor scheduled" card renders in the description area where it scrolls away and is easy to miss. The banner is pinned to the page header area so the very first glance answers "why is nothing happening, and when will it?"
+
+
+
+
+ +
+
Step 2 · Thread bottom
+

2.Strip above the reply composer

+

The bottom of the thread is where you go to ask "what's happening?" — so the answer sits right there. A compact strip directly above the composer repeats the resume time (exact + relative + attempt) with a "Check now" action, and a one-line hint under the composer explains that replying wakes the agent before the scheduled check.

+
+
+
02-composer.svg1280×800 · desktop
+ Monitor strip above composer (desktop) wireframe +
+
+
mobile375×812
+ Monitor strip above composer (mobile) wireframe +
+
+
+
Referencecurrent thread bottom
+ Current thread bottom with no monitor indication +
+
+

Annotations

+
    +
  • 1 — Monitor strip: anchored to the composer (scrolls with it), not part of the comment list. Same copy system as the banner.
  • +
  • 2 — Wake hint under the composer: sets the expectation that sending a message preempts the scheduled check.
  • +
+
Why this changes: in the reference screenshot the last comment says a monitor is armed only in prose ("issue monitor fires 16:08Z"), and once more comments land even that disappears. The strip keeps the resume time visible at the exact spot where a human decides whether to wait or to nudge.
+
+
+
+
+ +
+
Step 3 · Properties pane
+

3.Readable Monitor row + details tooltip

+

The properties row stops cramming label, date, and attempt onto one colliding line. Collapsed: two lines — bold relative time, then muted "absolute time · Attempt N". Hover (tap on mobile) opens a details card with the full timestamp incl. timezone, watched service, notes, last-trigger info, and quick actions. Click still opens the existing editor popover.

+
+
+
03-properties-monitor.svg1280×800 · component sheet
+ Properties monitor row redesign wireframe +
+
+
+
Referencecurrent properties row
+ Current cramped monitor properties row +
+
+

Annotations

+
    +
  • 1 — Current failure: "Next check" truncates to "Ne…", the date shows the year but no time, and "Attempt 1" wraps into a two-line crumb.
  • +
  • 2 — Proposed collapsed row: relative time is the headline; absolute time and attempt live on a second muted line sized for the 352px pane.
  • +
  • 3 — Hover/tap tooltip: exact timestamp with timezone, watched service, notes, last-trigger state, and Check now / Edit / Clear without opening the editor.
  • +
+
Why this changes: the row currently tries to show four facts in one 200px slot, so none of them are readable. Two lines fit the pane; everything else moves to a tooltip you summon only when you care about specifics.
+
+
+
+
+ +
+
Step 4 · System
+

4.State matrix and copy rules

+

All three surfaces share one formatter. Relative time always leads and uses two units under 24h ("in 2h 12m", not "in 2h" — today's single-unit rounding is why the current UI feels vague). Overdue and due-now states get explicit copy instead of silently showing a past date.

+
+
+
04-states.svg1280×800 · component sheet
+ Monitor state matrix wireframe +
+
+
+

States covered

+
    +
  • Scheduled — under 1h / under 24h / beyond today.
  • +
  • Retrying — attempt count > 1 gets visual weight.
  • +
  • Due now / overdue — explicit copy, never a stale-looking past date.
  • +
  • Cleared / none — muted, no clock icon, banner and strip hidden.
  • +
+
Why this changes: a single copy system means the banner, strip, and row can never disagree about when the task resumes, and edge states (overdue, cleared) stop looking like bugs.
+
+
+
+
+ +
+
For reviewer
+

Open questions

+
+
    +
  • Banner vs. existing card — there is already an "IssueMonitorActivityCard" that renders in the description area. Proposal: promote/replace it with the pinned banner (screen 1) rather than showing both. OK?
  • +
  • Strip stickiness — should the composer strip scroll with the thread (inline, as drawn) or stick to the composer even mid-scroll? Drawn as anchored to the composer.
  • +
  • Relative-time precision — adopt the two-unit format ("in 2h 12m") everywhere, including the existing scheduled-retry row which uses the same formatter?
  • +
  • Hover vs. click — the properties row currently opens the editor on click. Proposal keeps click = editor, hover/tap = read-only details tooltip. Fine, or should tap open the editor directly on mobile?
  • +
  • Live countdown — should the relative time tick live (re-render every minute) or is on-load accuracy enough?
  • +
+
+
+ + +
+
+ + + + + + diff --git a/design/pap-14557-monitor-visibility/screenshots/current-properties-monitor.png b/design/pap-14557-monitor-visibility/screenshots/current-properties-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..f3255e33fec965c864c683a9bc8633d1d0fa9e7a GIT binary patch literal 7542 zcmZWu1z1(hx(3|9CZ!wMfPhGBIyTZsmmuBUA>Ab{-637lh%`t^hja)CC@sAaHtk*f z=RfEC_gDXu$9ETKgEF zLeWN2QdLe;5~AwjWNBk>frRud(LbR}LBfP6oMseH;sd0Rc8E5M=itIs;~gedA3?>T z4=>K|>o=eA9dH`48XLmc?obCn(W7z2)+80lY-G!#=N%oj_{X7mJH;Cog_!Ra3AT*R{|N1XRZ$VtoGzI zLM_WTpDb;4+{9cW1-GL{u@WTA!rzo?r%>K~=l-@E^-AL7+kQqt_~JA*zRW8KOGju_ z51F`0Y~5yq?|vS?y?4p;u}(3olC6^Xbw3)kW8Kvu&ff<;-%_X9?)K|*vzj|N3wXcL zgw>!AdOty3L!M=cJJWT4e;qZ}6`^oO&1|b5Sv*9Q4lJ*xg|?ifk`mH$pbSPrj<7)j z0VQPMp#UBvq(@2NNa(-8-(@6~hmPXvl5%puTiwjX!otzj+R3fi zZEp^6HD~i$+f7?Z5oYG(z;0shWNN|g<>35ag7m@*1{57E+)N-|4)%_&FfU=czk0xc z@E zJv}|yJ-OMPT&$p+f`Woj4lXDc7aP!n&DGn{&BTk%(UtyRPX6sj%EHym#m3pq#>o-# z;Mc^|$=ywuj_zTg|NQ(bP75!a|BmG7`uDVe2|^!Qpq%U+(Es=bOkX_I!c=X%EbMip zY#e~)0mcyF7ku$o_y5=M-vR$&sQq6Thxxd+pV#=K~3&SK%I{Mx|iUsX}1m0hjZSz`BirFsIu+jMc zPCQz3Uoe0VGv&!HH#s!=e+``1v69Q=vM|3q`7tG^A99xGGQvDvYb2UZ>;QzvqLj^- z`dp(z_IUMk(d{GQsRBuKohJLt7%D-v_USqkNt?Nf)Q(&DsLO;f;#u0tD5YbIc);Dc z7CEo&>)9>%)~G@{yFsx`{FC}O8`698)!EojUTANB&8f7ACSiO2SoF58nI8g0=xOD#B%krYn?Pc&;=z5Q?6H`$RFb8ngkeLs8e09V2fDyq{`>f z&$Vi*iqGctoQ7N8Z2aan?E`0u26oN@;oDv`%qSD0DgEdTOXVSj8d6bLYJV)8t=7{* zRBBW*`lUgQo=|&6^oC)jRzZe;_PKp#2|=xi#BDnJDx{)WqhDS;7X0!Z ziQH$O^J4v07u~oEy=F)1xl=;@wOIbuXTaJ_2Txq#!ge8DvP&gQD!El4uw&q#JI`5B zjw;jbG#I#ATqSN?6NcEkAvzAu)8FLSgykAc# z2#hT^*wB|Kq|ej0$XSkOZKc*1)1E--@C5KBv0%kge$io1Z}px*MFv*dLgVpcL9w*# zf>)P6Egb%=Nd{q{igF#ryhhDqBUJL^QfL&ev;r4Li-pAFTbW+2ahlgrS{7GvGP;_q z`6F_w^qSd63k48U5JgNrl&bAz`0Y<5yMdYu%wUPABig`;$TzhK4ZYz(vOmg%C zW8TR-lf$x*_^vS`nD&o#DGzZIpj;U2DYh-oYXP&B#SGrfh88d%?#GsLGKS?7a{*yCNJ4Z6Tgu9& zA~_F_n%swgsFEZgB(4~HoVRv;8AKtPLwG~jAY9Q~XMd(ka;x`=lP0S~-kXUs(%z?c zyWJv+RDrjr*IXTbLNCV#$(ktZOYqG)3+!>oQP4$DI!L^yi{*M1iu{6!R0OwDp0RY& zP%lPctmlG^Yzqz=+-rTQg zS6-u^(38yOb9la>?;1RP~!(i(5z&0+KY z&A9i1V6Dp6U*ko?D~;>bA9}Ck(|)FQ278d*FEoR~+e|Y|`ZBPyGzaj8kU+L`6%4?M zAdRe%2*Y$4qKz0zVo=ybtOYK3_MTlYi}+tZ`*B+Hrg`G#hhCF?`4ZJJy-|nPM}6Q>9F{K@%dkrkJ9&XMOVmp%Da`7{j^Ep;)H3-Tvh2UNX&7bd*XRp=uUBauNo8p_ ztq_bO=aycR6fx-q`QO3uWvB{t3)|5m1&p3X%imOo`D}>z`&R_Q6exi&v*%^LY$Dym z1@w^c8*Jy~p$(?}(J0JpMx8GPv7qXuF)8c@x@~TI1gR{B9W6<@qd}_pvLtUdo6H7r zdqO?yJq~r#XAfxtZws>aLr}2@;Ad7$rxw})*$;<_JZ&Id(hdIya-Tidup;Q>m=?GT z-ysDxOoCkL2iWwQK1eSV$vmEWUWBG5o5H5YS3t6~)!}_c0EfFqsE#`7u6|Zuk4g+~ z2PSMCKCGh3)mWXZ43qmRjL8;B%)?K&95`j-=Z)ZkDbdGUFFthJW z#sjUmC7Xn|@g4J-RLu8w&-C;Ch0)5|&MfDg{%GQ4Ao#s}-;G#)g1E!OuS)#ipBnj{ zhCSRN+Jm9;v9&!r96Xk z4pJ3aJ}3Ns^5e@V2HBxhFh}3+Xf!d4TNOT)$%?oi;=5;F9O6=B^JPk_-9n9z@~?aN zC^W!Tt^@!o6^V9rOw+<%C&uS`YF9VMU8K&rF!x_i7P4M%oJt@cbq?&70ZTPT#GsIt zQlU|aDJJlv6hC0%vG-{|DZN9Y_~=`+z6^?b>u5ZkwNC#cJHHX)ZW;&SD{2D-vl>^^u41~^UTmnB4j9AL7LqGpk zdmR?ZA^&*PBH{9W*{Q2q6FlgL(_%G|YhKQw^Wy$;!%-`b-{@m6VzDbQkXfT*_6ZdV zt#Y1-a1RqXZ^}f}3z?FsP3NWSUL+~vixNv?kB_++O7LHmwQ;k~k>^M4$0S234m)3S z+%H_*Tg&mF`x#zhz(N&slMcGtCVPO7MK9OiVz68ov}d;T`#mAIj#g%QLl#s-7+0XA z#z%g2^5yY4@^l`cUR!8|?!v#>jrC*bz-nf!ZogGj6{ zh$aarJOmD)!*ZSM!~TPQWSQt1c|RJ7-*Qu_%gYWP(ZqefQ22e9EgiS8yiwtbF`)<@ zr0DCox_gX3Vv0yYIOGeEA6}uObJN=LhmZ#w1t$n|<9(vA%YV<|dIE9A?#?vvmUXd9 z-e|Z1@XpS!7alwEJ8YW~!znrQ>j-MX?0ONC>%lu7Mj8sFzFn%z`!~~75t0fP!umJH zE`Dd@{9Bn;xrrofx_$4C5dPN~nF2oqOH~Aifqdu+W5uE1Q6)_lMf*ui_UIY0@`tyg zalsR=?cQhGpiwZ+lb=#96?&b1-VBngZ_%H4i4o&04ZMc#_J%m?6W%DHEDJvUasn-F z@SSnXjY>R$;rdzeITKorWhAao7Ngvo@v1z{?Lnx+|eD>A+!) zX6$LEnPmOGvl0XzTB8Y3!R=k531N;SkEBb8$*0l}ygP3bSc=8(bRlIDzZga^RJO&? zvsTechJPUFg;S>B>XS1H(&MR_YBL(;lJvq^K@2`&k3<}8^BemYm_DimRMbP46wh^W zLw)Y0HjSWtU>*PSIjw*uW!+gh&5Xl5pI^*-BpFWDoQlrM+DI`*k->)|QgUZ)rag^$ z%T1CIxN1exu^-4v>$4+U+1K0#c5*6OtlgD6Uv@c+T|Q7M1H~gsCaRrp+b};DkUED) zlDa`iVJA(Tn`3mi-9i&B%9TWnwIxU|a=yRurScaFriMpWPuh)+7PEt8gdC~W1DfRY zG9h>n#NMW{oU$etA2?DK&Ey-d#rIB2#?y$PzJqQ9mp^w1CV};+r$6T*^1noCJK)hB z0OOEr$U@Lv!o9X9izjWrwY?m*wy{>XPQ9c!E7ZZ~5a(QP?@+F@_X}~ovaV|(o7~lo z-oWY`5a>n6D)><(e6^We^vp}yoz|~Gqtg}pRZ-BolcMI^^IH1DnV#DH%b&+vHmz}M z&lE_#E+b(Bs5nVrN^XAsoNNS;WRA{Bdp7nn;6>w?jiqNI@0xYs4RqQ`gz<{)0Y#bb z0dm`RG`_{qzO`E4M9lqlp?A@$6=MnC?tBu05se?^4`8-gn(#lT0D{rv+J1L${qd5O z`4Q)f(>}u9X?#)R!~^#qlRI{fRXz01!A&ELbeCUr@O!R>WC`{oH1Nv7c9a`Sben@& z%6|Kub&=#)ScR5!E)vGkB~F(=uEH{Xk3VZMkssyUG-j>*a_OwBX_MD7&u4?@m|bTk z=NB0I?3F~Lih>d`zX~=QLE=w+V{-2f7)n%6@UEp=Cu;L3*T*tmD-YNYEAD1vdOL8Z z?hB~yx1g>nK0gKXE}6dzBBD()C-FMle?}NmuX1-Bh~ez^XCkoJtS<7pY1#1|3MPp3 z34n@a&EASZo)1M@XdHK(#VNmMEgdi#=Yo(#y5``Kiiffrw4psc^X_fEutYy(X>Uxk zPQ2%C>8AUAMv~`#_D=J&xI}!|)ujY8JmYaihih4>}I14*Y z6*WE#X_*L{uJk9!$BFxN5t=FU55^tRfFQZIm-5EXwG@i8qY{b1oqcjF&Hi1E;z~t( zh2;|0s0No?j;umeFxF3sPX|=Zu3dX)-pjjlMezuwPal`hHxhs2NrJgDS`V=Z;|&_J;@K_V1Y$hZ z(z^08(=&4tvYu?-DOJKvq`BV&5D}^&uvanJaX7vi75*~+13MBkUKk0jf&mnR_<5QY zz;^cHgKT2<0^_SA9*7V>=oS)#Y6K52nd1XZVmS7A=3|F z``}hpJ}@&{ylOLmO}%jJ+x=lK_rO9RAirh7T@Ts#A4dPH-x+7Pj6@TU>2GI%)dCC) z52Xj{braBEw^Pb#IZ0@2zzfVy7!S-NZPY6~L*}o@>HthGGhB=gnibDEbJTUd08p%Y zF$e&p2NlhJPt`R%xS>Lc!s@s?(mUz@n+d)GAK?3avoNIOw460F&Ux_q9el8I#uD6~ zF&+7aUq0=Dy(vmSE(>&x%zn>E$7KG7XdB@G)Q}kzLzWyejQKzt+vwM8S2;};5$z6l z+<-w9KG=RI0sBi=0W6giQh?2&rLtpFmUVs zs4qMOgfd2eE+u!}U$cyS3Z+em0~4|9YXfPW)}H)v7$!PRliwxj0jc0_{@eR(`5nDR zTZTa@*BDmqI!MK8wI09V&q#Xn?Xk>PhJfC19$Kkc9S#2(wGl6B$Y;L@c~EI?ReSHl zNDZGw&;+!w>NT;_iniwf=Vb`cga8$U?Hlp%hui(yE<@c0>jH#50J<^!y8z~70l;H8 z01ufqm*Z#?l)?E(-55e1kyUGTujXsl$ew{H&F|Wb+dbY^;Y{z(*Qne@*pjm8K5ev} zi{@YfSebJ@^+U#gR%<;~D4&utF2~-oj)DP(mC<>y=HZF4bR5)=x;2fJF;T)HMeLVTq_)5}Qb?5-mDQUVyp;8)6j@__ByW({@T^+o(>;5@F3m=nte-0Sx+AVzd zdc({amz+x~josjR^IAZ=SVa!ZJqe#)ddPFlpNZFQJ_XQ8Y`$%crjM@oyluo`Z`b~4 z>hj%vVK`CdN%;d^ck)M>jw=c{z+lbKY~0IBok z7LcrzuK?`5Rh+DxR1^o3A9c>ttTm)gE!S(VdGE1gqt;V*6uKEWVBY%XkHeC=&2wQ3Ey z0&cy^RSQD&fjb!vY`%Q_6Ja+I;=h?7iTfAxj~AcT(ti^c zLoKA~b8%33n(CGZ;A^zUBg~a#n=q+5OG2D~uS8u9Jf&*=DAc8QCSs2fG(w@gu;# zm@n3uB)Sr#y!=Eh?9*Vs+M$CE%fiaSRjV}=CWpCwxE7o;ovN7D$aorXe`O`kGEi(O z%5EJDh@ZqfmW*vtgiOyp@Cu-3I};1OK@(^WdA`3GDI-Y_^DuD7-`|`9oI~zQIL;j- zkco(7LBNN*htuZ;kfi+}<3M0yuzWJ-jE)RdJO+uN2VL)VYyxr!5wn_9Bf< zkLQp#;geDjZl+V79x_nuRD6>}Lk+7#A=}aK0Lw@_2+cS;Tl9K7;kCH$9xERC5tgC> zkGoMfkycbZ>84CKjuOSRl;5Xss0$EjMn)7@>jOnu5}Wt zC!vNiMW<0t2TrTPqtV6gFYigK_E<32bJcsv^N%4B)X*;;18e+=Nw4eAh0~cM0mSMb zKLA{Xq}iB)iJpO86b&-~EYjLFLpPR!H=0Kh7LyivJaI*uW4YT9jfO+}vA=At)z#{pQVm_rJ%mv_C@dcwAcg{=hPkO$ zhrP^@%5^1d(4w1?YMpYNROG0hGC#5Fyg6#0ZgaPbRh%IadD(1v3>=Ob5xm3FuYla2 zKN{mAWCf$La__EgUadF+EzrytxcuSxM60rbxnedwjA>E+R+KD{F*ujfspC~OT8J}a mcz0J^(5Jlb^X>V69(_&el!LT`_aFZ0CihZVsz$;%yr=^IB`xoH=Ljv&+59eP7qL#pr0M5EIf7VqswstEnpLVPV}W z#=^o;1K|O8s)%B}u&}TnIVmXUs3|Bg>v*}_JGsEIuplpkliM_4Hk6S}gJiI`%*8BU zSh9t8{&*Y25#H-2f4}=og!u5xvjyx*Tz99gHiGZ+cAfAc6D2M&czlkoDQ#}#oBFx@ z=IXvIYEdGpD~zP=!{d%Sl$x{Z{ED~Aowu{26AWM#U1lB>GNyw@-X@`Htu}DGK+GlJ_5GpNhYqjTUneaEG2!i*E_{I=?sWWk zHNkheU6Y^PfPxl@{9uasfZrFuczY51D^DWE)HnAmW~ePn1H-`m)I7TM%e@Ssdkx{n zYWA9%SoeWz5Egcn6BaITg$;b@fDd4alt?Um;F}!yC>G-U=jpBD!dw5j#!L_P1r~)&R)ok}iVzaX)sxE(X&f=59=!L`Z(EF@e+p;YCv(d+@m=URuv_kfe zBHa60>VFo;Wq)kDlH{+>cyCZ{SZ3DXX5U3DCTtga+H=^nO5!~%@;$R4^yGdQ*_TaG-tH)RD#eZ0k+V(RZuGM`H6nBp6!z%#OpK<6Y*SJyiT#&$cTa zKKt!HO*i*aGx}Zrv32HfclP@QW>hZ3@HGbYIh|Zf+-bZxf>z{d+7I`(OQQ9NvOGUi z?*Z;NnXV1S;7N1catcyy(#oXn7(!>mr2t-Mp8bZ}9io_D6qLL^oI^^K>I; z6onq(u^;4X5bla170}?KhWfs&^*Y?M=||I^n@ZV4*H}n zQqV)4HgSCQf_o^87Hd6=;nlIYcGrk@Mqi)_JXN$fZ=@=CEkE!#H(YZu5Hl<`%-_6Y zi$}_6Zg@2vl4ew2>c=GW%;z1^a=+iK>*fIE0_n$$hTzL_6(*6xZJVyhEbceC6ViXC z?IN!N$K>&w0uSX7SI=*ecm#_Lz1YUSg_o(7C;94|#l*PwMnP!EvH9H~e}%!zx>7RQeOronu$nwi_h2cM_aK+RjF8 zi728!Uku!n@t$#dlNEA#_NrB8koQw#NX@KUbJKzJJ3hNJ658LNMrAjCmL!RO8Wzpj zZX4RI<$3J$p|bX#nw@3}>S0z;$VVlHe*LX$aHd0kz?^sm0)i9!5F=j*$8bNbPD#8c zF~R*xgSQ%)vroHkWCEx9G^r)UVUK*6wDRn9JwvJkd8`Z3VYb@>hLWe+X14Q(2TEzq z1tFJ5MqHU}74^>FM+m||HRT^Zzo(T8K^1(>TlAUK)GCtRZ@)FacG*QLmkA8r8s7Ok z0;r`lA!PHg*y7wb`0u7o(F_Ye6B9}Pr@BUHd4l2V8V zlA+IHN&>$opLnOCF`CE8fvS7PYf;n?XsjbhGLf!VfKGerN%t~HJYXMvFFY=2%;Z2oKE^WE0- z-y9ri!ks)2LmKxW^lVStXcNuTj&EYmJMW;lP8kE%UM`ozzX$9-PDga!4!|%Jlx07? zZ$~`mCc7jH?|i#L5o^Y%)`U4DQBH3D{dth~xs>0I1L_rH5c0-0TN9>l3c}{7zvtP;h;nr5Yz-lZ!Evo$8AZ)xC5h1N`=tSef^?)I87}oOrT=CNn+Q90N>jZD9cmy+!Ign;1vl%BL(4L>r5ZI+0*enQL zC`9VXN!P&;B>T~UolUe{nJnyJL(%M4hXU9`Hs1x!C??dBC>9&l7iEa;do*5iDXP22@XJsDn%$=6&_qKo>p% zo69GC;>QPlOwG`A|7CX~N7--TZT$it<$CwCt@tIlijmdn)ze%vucRlVHxWBJyn+j- z-9gWxPq3YhF#!>f%(m;RS#49l5#ug_Tsgs5YaJ!pxFVFF`oOJ)WhvR0+JotQ8mj_e zdPNlvZ4g{K$@Yw7^?uOcbnG$pEoKylF1OOgOf9-QO=01vkFF`hH@;u$W1`9Fw$MN3 zl*I9U7yV%QsLI9@!Cf^~TnqL?wzJvjfJz5V;SnD9k%FC3IwV@s-v<8q;V{nwio6h=wtj&(71;l2=Ci6S(uGyn5?DX&Un<@8Y|QnU zP}>-h+7m{++miH|}y zkyZA(mU6M&W_rzwz#jX-o!sK#wc^jVU+>G!r&|$u1eoplX8*Ef)^|G&1;O?7`ayf7 zHor!tG4X>1$#%VToo2vz2>Gs(ijHZbuD5s{G}^Gg#?nnef;#(EPTx8E*=K7do&LE( zUF~Y36kTV2M~-*zyIJ?PmNY&vF3kiDKJ6e4#nt8U;soZYmhG7x<42C*0zs!=FP6Qs z;OU-r5{xR8g;7i);#*XG+VPqY9>rS0?_ZL145B^U&Q6u-UDtX8iJlxXAs)mTmvtP+ z_;sWXdl977(7U6c+jTV#Qa|9FCjYDgmod1ObRggpBIx>@$U6$<&O z#ur1*hbvueH95A&j0M6cUhH#sE1q`XGl{djJkkY`A`JjfDtLr#;K{(6Wt#CUGCAgX zX9GdUFSnLz*&^mu&z`P+Q#xEBB#3gI-ByNMoiddtKLhB|R^joK{5>7Zsh2C*h@~)m z^ZAJ>?r-luATL=3S7_U7;?SB1TV9sKV${{iYEtz_VO9LQh&1pO_=G9+!c!>jDB4Rq zo+J&7oh;H+p>oKFNwKN(eHjEH#W@O3U(`W%-0r#66F0W54obpO3(72mu0WrH(1XZ;=9u1tkAHeRJ?r6_zWQW?L z#g6E9)G+FnWqh1W;2$4v!5_ee@Pf5*Fw=`YK}0Pe;@B2FQf`;|#*zmo_BrcVE}5h5 z#~dm(HtYGmK-L_}{$PofB4&1NSOG{=QRgA|la#k@RIfgY33m&WPAU zSEM6t2NB3lIr!8HE_CRgYLeIF*N4$<=Le+!LCv6nJzCmQ<6QPKvY;YB zQ^V*`mXO}=>C0qK1Op>aO?`%6$3r%H-Ss+sQyCg5==}lnUvFLDQ6(^Y3{B7jFGZ)GtaFc|flez~6Ov$B4kmi4e$#P5WN!nNYEOlG z){ON0PP(6s>Z$>IEG4#*^$Hu={o*=)?4s8@Pe(|Gb{@2!L03V=POp&IO(R%eE3}Yf& zTnhS1hkX=1MW;Kg4R#E|{mM3TLbFW`)b_?PpbX=T!LsZx^_{5W=XJ)M8u8z>@NXTS zGwWnDZ>X;yzP+i@e{TN!jCJggOh>79t1c#8^*>ek+Xer;^t19qs`1ZmmX*2xN&n9) z|Nhb>#WqmPvCMId`A;SOt;@~VTW8$I;L7cdZv))_FBcm2*+|iU)>Z7&ZvCvN%-Z%_ zMSV??S0*JJ%|6`Du5@Cn?7CnyG2U< zM>b~*z8OvH>1IRKeT>duj*}Hgpn#`>8f?o(UHabyBtnRN>ubXz%n$pV;UU1)iCB22 zw4yC{`Sl*)8j5V*csqj!yIzBQ8Ky(UyE@#xm?5Dgp@*vS_lH#$3Ml*V84>L}EyoK9 zXIrL65`-p$sQd2@fne3`0}$$|TUPc7*b#Z@?|hYzN|4qR3n_j^mZ z3Itp9tfa_)nyZ3rbb6CluPbL!E~~=goMpuEiW6;%}(Y_%b zUH)jkOkj4yV#qA8@G{u9o^J32j@*7Bc=sBJ&N{fO0XX34Ef(KC27lqp3M^TlpE_@r zDn5-!j@b42N2FUmbMoGLoguk>2E=I2H^FW{e;&kcmSc z{01EGUJO3Y`feFG9Pl2B1A@OYnpk6y+WV9gfbQoSu3GNWERYRWU+zfr24XZQ;FeUE zi!n+S0UpodT5zhIvrAP6o_*NuyZcr=Qf=SsJAh!%=ySu#N?h}W9}wM6w1|8+pCqCae)~u@ zKO~a&-gV1CFMXu6p;hpa-dPm`qH0{3KFhM@S=RC%%Fgk;ioevvtDj>Sh$USXo^A7O zZfQ{r+%C9*dXA{8&;ddM1*|CLCT4W0wyqVTMgNV~^|f`U6=HYi)2Ju_5&5A)f$nMM z+NTbt)0P2~)%vEeMqn$QtpLD$7PEt)H(d z^)e!U0?w1KZ^%Ip_3)orcdIkuJ;6Wx)!pQYd+kfV%0K#{Ox4poQX+imIG?0)$nEDs za}JW5;y$;#|Fdg6;>EpsL-43VK&y0*`OBzFms^%ggpMSb9GuSIxP&A0^ef~BwEJS` zuh`hM63at`L*WEKUVdDSFD;`TKwTxf=W~6gK^s{P>D+Y2JKz(7AFxLG#`T|piJ4f& zMlnVQk@RkYz_88BLFtGiK}iywk$$XS{}W3<0pCws%{aT`J}UrDWm2@h+XkZUHOrp! zzP0ZqDx_lL`i#GBwVeNM)-qYi4U}<5z0{}Bx6knCWNKA1Db#TJfvds$t~Fe{b&$Wg zLcf-5AuAJ%2A@UL4DdMBB!W^cM#Z}{_qojAxY{g{c!_Bkz~S}#_<{vT9Jd~FUm=pn zGaSq21CoX<=piZ&{Y7dlz%ZIl%CDm7_vdXQ^?WjQ2qEFXeRargpA5^d!DAx}UxTJ^ zODFwY1>#C3j1{gC*sb#bG}Hv2sdIJ+xxVzr0JAOQH-nQ*rQGFVDh~yYkS`US*#$q1 z^JE#=aeR$!;+M07BFOXr2bx@WW7fDQ{TT=N6eLO`C63Po5hy4|Dd!`)b#Y8WgMihU zzBR_H37GK*)8NzM!^u(AI1flL-RqcSp);U{ADrE?$C%bEka}mVnPb#WUG=WB19oqG zY(Zz}K>FFsB74C(1%6BDK##rW*)_N|bDjLmO{VaWne(FgYD zFX;nl3@4H#O*pEYzkk>OkRPwrMHQytCvLUg?csO$GsVS958M_S;1^G%0|15lT0e3p0(E zK+Po`))sO)S4aNL}QmcZsf$h|ja94=Ky>42ZE zYo29YRS&xf+ybd@9{_ul_xj$%>o_SrIY|8wfAuA%n?gIYy6wVte`Y!FtSA4?`2F&V z6-sCtXIfy|Yd^zi+({H#U8;0_I5>@)QkIeu?)C1r6iotRs$z6+9Ty*0G7g-jI$j$QvN;Ei64;dDS_Qh)>N6>x3m0vfyD=og6QQ=4F5t2$25Nw_?pFt#m{i z1%j2)O5B{JCy^!A^r_mqmTcN*0+-0QVF#;y&9(sUK>a}u?b;+ZSn-K2O{=rJh_8OS zvknX(?^K;}eu+J|*vKy_ZCk`~Y7TqETx)L;mLnc?kgliuZjvpQF6#3ODrJ_=n^44i z-YQ-!`dW4*G4b{Nb?=Ih{SeJ2I5qnAz>>a8sG_HQSn_9E)Wg$A?G7S4Lc0Y5uG}RA zo%@|$(&dtAbmT5K{B?`YTxlM3Y{|mTe61NO-D|8RHDIiDstXE+nXtOh@7bt+JH+0g z>m?}=plw*nJaar0Ih4yOQHs)@ug&y^k;C0*twASiQ~DFP__2_fQ%6jH`bUC5yz5 z_xyL^C&Deuq+!aUTx>6nYJbsV3QD5B)e3^mshsW;lpm`)GpML4crHZ=67YQ*df>*| z!_$v@{}6C|8-1U94TF2)bf`IR^+yR>QfmJufSyJ_RF!>U`K&)cJRs2@uZT=Z7u`O2 z2%zcQab&TBsd-G0=W!g?pbRcwnz}BmNu}VoW7oD4 z!8&8n%L(cO>+|539%`^v_KVD`X!5vm`4c!mAB~tL=-qez2xSJNvy|$7wN;;tmFbO^ zdiDHK`Ng9SIV}YCx|whX&>SL;=9u|}bQPP@Y@teKe((?J?s|hzxD1LpvPYV+#D%}J zn|{#gSO=`6rqFSNkFHJX_Mv2xyEClu3JA5=Z8DAeO2ciRzzE>{MOVn!722*U9i(mB zE=KM$@syh5arEYLO$4g`mH)bN?OsoO6m+-ic&iQK4*UAjIZX$2i(nm7f@~a3A!F%v z?Iq00SZlp!SDk)%&u$I>?7mkFqzDzIXO{LoK>Kf$9Hvg@Xwt7Syv%@`vdD#yd5r@7 zLb`$02%06 z24vXD@gVE#8M!+PPNDB8M2A1rsUeH8A`x0atmaY|{&F|N0U69@9(ump!2g7Lln^G$ zxFY*UHM2&5{o1}IYX1oxOSHB4Lu@4{rGub(({bIxs){ZvkSL45N#tF$vYNtMqL_VM zN_8tNqDc>8Mp@2%wlo934Ca&63~dgSa!wKA40-UJ%p~9DBGe`&CVUHw-a zc^Th;42uT~OEm0IURsmH7Yt-I?9A0HUC^dK#$% zmyWg$+oTYnA;e$KT&YarbDz%F8`(csx@FM2e*T6)N+)#xzuJO0E8 zIZ*)Kjn!4M94=~vlEAXDVs~%dSuKQUhI~e0Q0^M+8q*;r%4DbZ(|9(s>XH(7cLSlT zHjmEtgHPTuv+4LFMwi{SA@vMqoSw!a zy|SxFRy0qrzCw>!Hy%WaFmR;lw5zKk83z_9k@4yXV)F|}9FIad^50xAcXClX7c+s( zYTd^ZQp+-1ap%*-)j@msTR~SHvtP>05t{_NT@SHCxgusG7L&Kt&lO&ueDPnWIlrv3 z+Lt}ncqsuK8wY_R4ZqDraW+X%KA7?sFWf^S#r}8b6A?%mUI>#dH{~kvMh}@MU*UWNEXDIz88TaB9G493NTxSl2ZNGm^^; zr_6@eG=B0nKM1GCsY^aE1C^$VuB%v zOa2>8II_e;ufq`>h&)OJuQR{ID_zd@HLC7>5EOw;a4T(*`T(*F4s&$x`~*hFytU@z zUM_ZdvaZi)sC4$C!07eK34B%h&p9&wd;mhoyP#^K(SM?HZF^z!i7r0yklycO6bxjEU=+ z=~LXO<~!3la>dV|QdcRwt2qtZnxQQih?VNp0sFVrZINOTkEnbIJ9|ClZv$G99&aN<~Y@ulR_Z&7I$>UpP~>h^l89 zo#BY6qsNaRF8u8?DSvin-`KOH(mLs?5{!2Utx~{fc+s*i;zK|RCh+#Ty55g~;yNaiuD|J#6I4H(w&b(C} zpbmU;ix8&r1BKs1d&L!G0&BFz5KOA6LY0|1L+##@Tdp#ASQB&h(olj#32Zc(P`S#8 zUfsy)=jv%(3te$}@G$~R*OzwVcFy}WX@X>eWn|9?i;&=a;?TmMtV`qC5%{TR<|o7- z?9LSQEMKiqq_b?CqcBk-71i{RBYO+Z&Ha{r{$RWyvc`@(+R$Fsj*=NPzb&SCQHa0m z4iXz97s#Q<64^KXHr@hBTfWs6Prv;mWSN5A@PNVU%W;9Xr5Y3HG$rIqBz)1d1V5)t z9!hgJ%VEhM*L8b@VzeJJ;_k zF>A5C7d}=yVYDRhV>qG`mDYj~Azf<-aAR3g^Rz4K9@3bMeJGKqX@4ILi|V|;99pB# zIlytO`z&iHxRWh>Oq+&o0Jq4|xI{PH?t3a3x^L!w8fWJ$85y`N3o^YWn6N*+lF%X4 zH+2E|0Qw@Ui-%d69+6R(!q90&gk;^93bdr36{Hc|j zE*^W%r_NDS1#;L)@(33-yUh+-1~)#hT#lIQY0rJ@-I@*g{(u=KIzv94Uh z1$9J%D9nkuJN24P{S@5#q;Im;ef$md-m*pU<<>bO;)EIod0WkX!ctrG2uiXaYVmq+ z@(R*e(N=Of?fPx;oA|p?j$o-WIren}Zdn^z+tyJjf$0OvdxAgryW%>t5hl)YDSc=x zr1M0zf+&hLr}nYgLiz5=a3t>`X}rq5a4&li4WmLpnewn$+iXGc{>=aQOy9WauO*-W69P{OCv*-jA+qD+nYe*$Pa!Y^Q z;|SJ1^Pag`HPlXf=j@z_F^G?rEpu~S{ELc?NbjUIsu#Y|YVjA(!?0;1kE#@xlkw?I zVxX~mlOGRKB76O~a~P@q0`GWXcc+BVR8grnR)lD$GhRQ^qxs8rxFMbTDsfligjS_b{p!b-zcYbr`yFURu(2z&0cl zvtPel*9}P%^)t}7>@0_Z;Jb%eeRf}81*L3chy1b1CaT5br`(c$UrV~rIDL{2xi*8+ zY1f$SXn&9@THjip1D8U8wv&_P8FF;xb;n@YV|bgcZ##Q=QPulcuwrh^+*3pEF3rfvVwOi<7Q;{l9DDe^WX-xWDcK)YNvpQ=RSqk?n^564nH$Dc9`X;@AJ%?EfKz3f;f} zALf>0$W!)z(`EmEq5t&L|99QnQkM7h{ktK6-upG*FK6M=aeElhXcZJxWxdYy8sOH9 zyUSOs-ZE_)2PnvN3}bQ1M<^<52JYM6usV16YIRcIIRTO#@4<<8`dole53^QQZh*5Z zbgfX6;&~kEzFF{Xm4-oRp?5!6_L-)D(xZ82lV8tzs?%wlEdP$A$FUFRpOOMZN>;#) z9&#aYFE5(7bM!g(E%R)E4IFtbv6ieb1Tcl}fS~cJ+rKks?Ev5@ZbX5TGN#YAUR#8$ z4Sr&WExk53vJ8MvtY*&#E=N!g{JLw8+Nl}*y!ir;_t@3qJ`Qb1GXz0hU&gs zI@<$clp5iy9y;d=8-Uo5m@HDjzxF$nAQV8HBl1Gdwh|Jx^{^dsTt$WY?s>DzygzLL z{647*=e&dN3I*do6x!0`H!{8(@u&#IG(ibS72lN@LU~0Z`Ll4sp<&id3IixPasYs? z9d$UN5sBs?s7`w&dvU(kmNSi7XM@a_s3eKL$@Pkz8<2?%=6pxGWG3&0kGBC};9>w}5(O}#u5_haC_wA@ zQiT!96mn8@Lpc(AYgOo0%iDJl$A19&*2=?w#eCyip5zUp{b3~jj#_Yws@f^zD!%xG zQUBj_O<%UN4gaC6nQ_8`jN}zLHKF}OLZdFGrf?4?DefCJ5pirIP+Haxhl2Sb$?Xa} zc{1B@CJC?7Q-B1aE7ksd0D$jV3Zyb$g8;7VH#A+Jk~Y!It?4zzK-V!D8liPcm-=?L z?fTN;4}j8m18KxgzA^^xEDy*_7QHew?LVyV)Jy`X3_w2DiaBKMo(tH_v!8Ws5CMp_ z26jFMV83{pDESK_rkmk*S3E25cT?UHmL(w3^>-f;UyaWia1YzL!=aQ_czL6EhV2=o zAOIn$6Xk1v+bNa`v0&ne-(@*&giPNK=~&DVN)LLC0j4Y?6M&4r&BGj}PnKQ`8yH!v z6Ws?$#0LP&SQ^$K=yOtQ+3dFg07Rm4*B75CmN4jae?aTWKUwE&&TiTv zKWk$7gcn(s|Lj*yICR9;v1z~a z&M?qG2SYPgkvcV=S{4BVB&mMVfnp5F@aq;o{~#K*YJ zP@5Ft9W(M*ErG|P0FjwdFqF4S&`})U_mwMc!fpO@7BKC40$AOfcST<;@gli40X47y zhRzZotvQwzCL;P48W!E^7efN0!mi^VxH#-Z9bE(H)o@vk%L?*XYyr`=WYEJFOO|@K zJ{rfPlYlGp_Fdz`Ljqp3w&_lkNl%S-f$Y2G5#tMdQpTNyz{7DB&FjEDzV!}*-f)1h z8btw8J*^Y)i!gSWcq1^)@{EY$M5gw6VW(06(>f;xnAVr8yD0Nv?a__cT66(ncWjRA zK-EhCl&|@ZiyFVZg_bZ8#%;-?F1@4_Cc>;V8(>KI9ATdWgu0YA-f_gq6d;x7&G_92 zMEnvJO{(PzKkocty@RjT{4eoJn^s0&snZOQt^Fl_g5CE-}S+#}yraB=l$_mou z1!l61Q6ivauaZySb)jAMg>bs}V_Al~10rjsCMw^<^GAkE(Yq?>ahfanm@lga{iHql zS5)My6v47FOSh$;(X=o5?gO;8?-0P9Z_lg%@(1tZpUtr4#+<%_fKd)BiAXzP40f>R ziPT|m zjyTfg%LoU@2Fr#y+ejkXmH23yH#GE@P{~T7Iqg-cnRHdQ-bZIAfDplOfYTY39MalJ z$kTX=s{~%PPw8t&EAl7T_m6Lm&x#k@H)n(=XrSVtkpUS8cGFch{OZxw5fZR}1w@5@ zps#hTu`o5qVM{{@nxKek`52HV{Bf6(pCOfkAy`qQN;`Pl92bDDKR3By4azWhmH%U_llR z9xI2Oy`vB&Q0iS;rpR}aKCaMh1%&%~E0LQcVvhTK9tQ*qIicZT=1P)~XCt3HmI87_ zUGE93SfG}|@8sGl4`9rjhg1_KwehqFVIJFEs%D*6lf9Iknj8F z#|9uB_YKAXi@PZPJ&%VIn{;z*@395`0D=TNK&D7~d@(4{CM`rv>stHgyU=_K>ZWVNfmr_3QhsE*iTC8fQdgUrd!( zJfRwH(u4Ym(IugiC70O``@%(65G~LtjTo}iV^sQTD3JejV4SIh_8dBvz$h}{Q@x;& zM%0@>RcWSy##5m*kxqd4IZOkw9`sDrD!!vtzKlRjhX#r)+>h**H0m`bQjiVX0%S;Q z#XK}0-_-Sro+WD|B8oD7XH9}s4H7%*?tFp z-Vs$fFAR*-dx5|9rXXa1PoT3GIAyVm^yK_p!`B(kcQn|ekB42 ziNZjyL46Pv5Ea>WXy%%3ECB^)8)kPcp)ec^DX49-p9=>&KY1&6q{KqDl6~$(Axa@< zNnCMdt&DKv1)D}zg)b3=33c)^KGZ#A|2QgpJ`_*|3WKQ~D6oR>8k&15eOCtEhL9t5 z)LRkQfM$n&@<*g~Y~c~G<4T5sodd}$+pf+w?rw84%Yj&Y09VRylZLeFEk*VqIM#`N zqFSC*o#28U?&{FSvHiT+gcLvEMz+E3KjpiByagP!@RL@aWqvGA175Re+H9l9=laI- zsLS^*iS(OYu%uEJp7seoM|f3RGTGR{*pf_1K7xk@rqw@aB&ma3dU-=+rx6>UWIAxLjn$aJV!CV2gYYbGHC#*hNxc2DC&cStFTyNjg# zGrXAo8D0_4=s#V_tQYE~?SNCVD{UKMdVQ~l)Lr z&QdDqHXUI`g0)AOpfz~CBeVBjm#LMmDt!2(`x9RQj*I>ksWZ@J;QA{g{)CrLz*3g& zV!ZG$8vW>W8-6w-(Ky>*B-%aXt_(u_r>Edwz_Eqx^v<$Nhkw|D=d3ysIKI2!+v&iG zk8hd_vTnJtwaB{Az>)e`v|YYXc^{_J?LAEDjvaaHHwQSJ zau=%=k_C}Au`#c9Rmhthi0$(N1nkh`Mk)GfDa+_qTq*a>-6cGNaTBp@b334IBi`^im-LZ_%eA}TbGT8gfuKzaM< z_2TzT)P=-j9MyBMbM3lc$ zKf&TM9b9#voivSO@e6VsM}3x(KTa+{U{AtnBHDi|Rgpbfg~GD$XR2b&ONK}}7D^MC zMGw!t7cwlBH1zA6AN9*di%Ka(UK)~2JP&XjqR7pXVrCVesdwGppd`wtSrGiGYkx+C z^i&>TQ6YMuqua{!z*4aF6nWrD>t5+Zr(ErOj2V$kV#r`8-3XkCgetSb*RuYBmUM96 zD-`~w(p{3L?V3d5Cm-n|Hmkn&B|ln7xjU*6FPNAt`HJ{FRZy|hYckDhO6MS6PzI$| zEIcSCQA}hPp|T9aLQ^rxSs)UvUp&ydWPus|;HKX8p|kPr8_@m%p58Zh1Egh6{e!=c zKS}*&wI<_?d-&|~==+-wV7@?TBaN8NByE!n;~ZZ;4Czzt7WcV7$z6VBJ+0PDF@Ot~ zvIE2COxE5N|3)Ovc80UO0(sHlU+`n^49PXbg&Jj3Nj!9K+8626=sI$Q$J}rH(rM#K zk3$j~1*e58(1Jm_@9km^5xP(^fS`G%`2?owwD4BYVnT$Td|X9$CKZ%)VH3r$(W!Q@ z4@Pg(-xpLTo61vE3t;R1#5iKap0ZH+$8tP`a!}3$mHpUW+W8Pr86&<5Vnq-t0f3Xr zRWNkp^mtQ#Ri0G| zbvrt!+X2afeL?qNzjt(X9ti7C$SoZysHufOZ`^?tjM}A*Ds5X*vKT}NYc?lLehBB z6t4^v?8x)^P#Hm_0uaM*?ARpQVCP!|jh$QOhwV0fjW5cm(rHJ%%4m1Xl2@nSm~l0o8t__o^Dt7g|gGq#KhoGsYf%J!5q(?bFvx?8CF)+l(|sV>iPThXpBt~{7_y)sn`mK(ENGq8gS};Qku9zAmBlF1BN8XR z_+hjg4f$4ESCjEt*SXVo$S+VY$QrFcD=)9Dr%rYsM66WJCLH&yn6kP$_WGmGg;#gI zrYUU`sT;_&0vi(p%sP*x<8NFO%*pEFG&qo0ieqF-r{ATds-(om^7HRVt7*$L;@gGM zDICuIOSfh~^0J&276aM--F9j}9U3`lMhTbeuSi-t54RvlP;$TMYKbz*D`Gk3rYi0>yjVFB`+$; zx+5N=^Yw&ft$4qe-115NwMx^5jNnSBa!n%FAzcdB0R1%+l{?=S!6JN>Ax$cc7PNl1 zC$WYtsJAvD5V}dq^6i3Da$vBb1DTK!R4hxwSA{AOEMl)N!rMOR20e`CDe89F$1Ygv zS9*))u6S`BkD<9Y-9noW@1pg?g@58cORP91!aQomllvtvE-@gB1N9wZ>m-JzmvZBy z1jIWFik7j9a+A#k-|Rq`f-(|5?Ej9lP3p_{VS%p)$BNK^fBn4vxmi)Po)7tjut>Rb zeSE_AA@%Gbu!?rcsVycpshr$j#c6)zP|26n&m*A87`h=dI#^&acJREI(x$f9GTzr| zb@Gkh8)r(xbUML>mHDo6t@8)U55HNDB7S31a#w>_=aMX!b1KGV094NWG5&%-Vw#k~ zA(xXd;xsJTCf&G-KyT-|b!b(pm)S!hCJjzpz1pMY_r`z!O=xuyIlOeSTj$Zd9hyaR zsImKAbVII0gepGkv$d$AJ2+-n200%yoAJ+7{sp1v!uplL@HYy$hP-DKJl&Cd`!cQP zU)Yk^MG3c)Ko;Fn$H2L033-7zNaUD8#WH!M`ZQH@Vok?wbz7ns*4%aCUhfEp^dtR> z90dZHjU7yxIgW)=hIeiUcL?thh#n#mBA!)1N>dHSbv7nQP#BA6nTkjeVDUwy1Z337 zft=KK&w*oY61Q{66ed2&li0XHYG7Zf_cB4=P*=7+dxjt2yV#6M(e9tL8k$|*3r?)w zL9_;4#%KQXzW}Tg`V#7u-=*dGsl`nop~WQz50|<8rWHYl|1tjlT}<$mCSEvKIREuD zLs2zkrlgwPJ-^pHL#h)pdB!|i03??Zd z>lHsCTTjiaBxrl2U$smXuMi&!(L|1QGC{&zf^bDR|AYkZqYIoi35j&#b!lQjR1Udh z#iP1fsAAr7>k1q5jxRI{0YOpt6hu&U8xN#yU!2?2^MSSw8=~&%fxzlmN5ICvRI)QY z)%@tna`z*zQ+4cmAw=QT-i9ux!g+t`hg1q2K9{#Ep|1>S!tU6#KneEJ5J))F#cZV7;u7PLe4(~k zbIu{-QMrNK(zzn1>Iv&&$;YC?ngvr)Pqq6h9LFF#<(CewmOm?p2L#@cQtqm(74*rO zz*V0`;)5#oWmhkAjMHu~lurZwGf$;0XuhNn8L91crUs?fjmQd|Iyon_6f}gASd}JN zmDGq;(TUK7pWInYiBt>xo~IC=mCF3 zz*nzaLd?|m3TRBuE@7j``@0xG$P_}!qP^urdNV{uAK;RGHW{3GtSXxPwpJRXW)iNK zDG;m90SMqIsp`>`9Ul6xP3wqH*Mf7-*0_~HBPQyQ0Hp7drD_1YNb|h`?9|>exYNti zf`=mwmC~Bj4yXnugcjaK8V@GmzlkOvI0oR%@eoDXx(W*QTe|p@rX_mW-;>=%@zxI< z7BCeL#zUWA@+*9E*mvk>_ zknZkQX^=*w1!W;1wP++4At;SShX_b_3IbBnDbn5Wj^%&f_ukKb?)TGs9Pc*|CvdDe z$5?ZYd7b0@T`KhR!OD}Ww%svLyAcibm&ZL4Wy)q@RCWQn+5aUl2_b<~c=~TTz?(jL zp+g8)CaB;+zxr?bBi3|av3qu}zc>%XSKW4fGQ^Sd>Q?SA`)`2sMP5$P%Q-ZEE$?qh zbCyYNQqp>Pxr|6I=`>)SLdW(c;zy48pf|-ngdN)JTM5xNJ@;eb=XzzqS z8veb_1e-GHX~P^#`f|>N+`(_XwrHMjaQ<9q^bfy-Mz`2iIlJjw2{rylX12@-l(prB z&vl&tx096vDL@{@;eToNe~VyXEIpB)n5V~2^S_k&za6oEh>!s|DuhARSn}WJ{NLnl zSPb%!d0MRYY3YCe-~aH)?t!BjsIhZ~|AvtNrg1>eqD8_wT61Be|A6_(g2}*9Z4?!N zKmWs@79v3X94SIjn(+^*{9ivIhXanjzmxyS8s zfBnsW?7G*XL7aK7LyOiv|EH^w5CV?gYx(W^A5W-D9?Y_?)<<6dr=|8^*INyFl+5a= z=Ks_1J|Tc;3IBZP@I#rB=iwLzrTMTH&=SJtoZOz%PB^$6FY6{U{egNTu+UM z7Y#~C5lDQydX$zO6ha=DJi}u`>iyI*{ z%`)A-CKFDj6b?`)YRt{5KsN-q?d`^JuHq7xa(660X{<4lpG|H5OChvB{oI)DTv})T zix-%QKOKJB&6Raa2vd7}VFdGcn41{7Bl3j(=UZer3aG2Ol{Q`csU*1t6kz89U@2ja zcisjY)EW@fSgkVvwtP0I6*hqA??Mmi{Z&A*5o4-7D%a7uINf!3YdjuTA%9U6hQrdL zOlh$~hJSDnX4DV@K6skE)6oXFz%SBZSV)|P+-HBdlB|27J|iBEEg+Z!z!iwT5w~9g z%F?NOCeoF(INh8W!lRd}J=y|E3|`UJ$WW0gQapyj2w|Bzz*7c$5?Ka8y@@>KJ{8@| z-??kax-wB3^J65r&>wL}ouDq$q-Bjv2pI$rY7wFFXX3p!%?JJ$8zY@k2R$s`70C|n zY{%t|6&dkW0ios%(mK2MmaDWj?|~XM)xRQU!QS(~%M%CscrB5a7l~tBEIqZJraQ7PGd^x`l;9-{M8%nhucK z$LctrC?+X_0uNhs#v|1FEv8R7DBnn z{5i-|(jV`3PS|E0%%k5FQb+k25AK$7RjPln8eYgSK*Rbc?fEDHpyqFX+8xiZONOg{ z3rK3M_m#ZyH6RLjpvOaeE2azc| zQVwJMsyS4I4cv*TDW4x(*f;MzNJ*2$^O0{{122>*+%$x3cyp3@;R2O*x^m0MQ7mPlvc8)?qk?|KIB z;j>qlxd$b{VID1p2Z&`b(^irerihysBSq=#cab?UQ27d_^Sz!$X&S4>myijhW*AM> z2Hi$pjSPTRn-FZ4b-*hfkKChccf_LePA~zq(Sm;DM%fpqvspEs9y6Z7vgDAPArB_M z=M4-@ARQCkde* z{ROo>xguFQiwj@l(Z~)Btce;;&xXN&Pkw^;h`bB3-)9Ti^9}NXrIhz6mP}lL?=CNw z`#8omUQ;lZ<;xAWs@r>`6e)@$>20DwYFuuTW`kj4b4~v4(mItYRy1&e=FFHdy zw@lZtO;^%w3U&7{#9N48WQ}SQ2o-vFI@L}6o67e0H>brLMD`OF*z5Y`tIrw!2SC!`;UKiO8=nW?gf-4x+oy8zNS# zCvry6`MOeJ4_ku~R#yM#^A0@@!(91r(H6bVnPL-U)|CU=dw};U?=6vn7-mrUwLT^g zo}Eu!+kS7TUK%fEyok8XVWpQ_Q2BU-9ozky#?kc4(^a?R@E3TE-=VBYU4hwN=tji# z`Wn$hSr!QE>06b}s9CT@MEmk&yT4M-lRfYZy)(IqEm~HE6k(;W!#DxQoenESRI_7)LGLCC^`YHGd)H4qc2}}y?$T_+41_Yfj@x2<8 zjN#nQ+~c)nwmH<)H**Trs7PFgb_9yUNTuq$!fm@-COb% zsGx7K(iuKd9y^2FGN~6{s^eFN8tKoN&q(Up8Y1;0>(cjC!Q_o`((KiZi`CwvUS)Vn zu5v*;F%ST~Q84i;DcFxkq@70#ygA2c=jjGFv_akX>wg!SV?3z0e6v5Dj?>8$hwfOi zx3$<{E33iT=d78#zNvo9B3g_qrna>(rC2nP=T}WoQL{#M{tD1oA6J=oS#ELeFmtRN z{QY8ywQELMxy_rD-3Kyof4G(cz_NNU<-NC^6LTOi7vlGBQxq54p;F4x)~WhnN))vY;-XnP(p5jODP%6m!BZI zi~D(l#D<-t5&S!TJ9e8&^x0$&2@eZLJj+YLAzph z5Et0-TW*euR4h;EK*lvDaSfyb943GBnkGTvkCKHwMrg(5l4xKy0PF3JW{>PC@sa+p zK*n&YGK39nh*wJGQc>4*m$UDU`&iJ#24CJ}m0L=s92Co+G7Lh?gon`(PpJuEqGE&O zYg5!DW!VZ}lXXH0pDw=^71^AwSWxa~iG2i9iehB+uZR3tXPSQFX+bsu7T52!&Xc8z zHZtM$z!^UMUaMe-iSFjF`2?)yL|wOPzeSZ~@8Qy$T;SL?Q2kgCpAPj6tw5B!A$$aq z_UXv4h_0@Zv}ZNW>DgruWjy+eHnKClT6G255I(iMn>+ zB`UOe?b!`nv7d*j?uG4AkGA6G6%Tkp-`ni<(%jli3tov#K=YA1(H^MOy?3vmNb@A* z^Bgem_mdIW9qAgN-a~`SW3~%;75(GJ9>=wi9h=LeJFo}ba^M)cG*&cKmg*@C=KS{7z&ZtcZ1OtOla)d_lzY| zRbYA;W^kMXrB8BJEO=b;kcc+sct$wMw@;7A^a;|3ME5TknPp3KSdlMP#lZ9o5B^W|uhFG;>+-(nJ zul7O@JA(1M$j%~#Q}U?P=BPMQt2dn)UjAlUf8Bj2Nd@+Gyj($QdysHQQbD329E(R+ zpF|hpACc4dURepZx<;zomqb7&=S~dAciqrRlt4LJWwoQxJ$g%)>|NhDkEpz$mP@bz zzbsqHT6Qfc2`JWs*(VG4-U+*-OVLHw6yFvqfRY!OU)z`AxZ|lKc@|E}d=Ijja;lc5 z`t#@k45MM9qOUi#=Cy_Fmn>U!w^$1&`i5#jYeXxH%6tcZxDRD0qY7axmdvAHqSW(L z_m#2P-1Bt(^B7{QH@m1$?eIR_Dm1nArSo1Xnf++0&Q3T|mhy3W=LYV0*sH@5@OlS8WbTy~fXmN!y2)CQ- z6d(pFo^cCKDz=k8XvTswMw)7eS!Q#7hHc5Y2&2g^f7#oim{xz8k^~i4uqwA zkLEkIiCcCNE7DzdJ)1|6DiA2E=WD)d%O?S4=LnosdMz>QgD%_9JBn(Qb9zz|aNoto zK^7BS&W$~_g%l+Z9hL?i2q`ReB=X=)fq8={NIwoD*{;B=1m#2}cJ^<((9CFHnI|p0 z&*wUQ$StGk$FUO6oy8pg{hj2u$D89Nbb;*m4yrkv=k)mX`zj$faz8kf?kap*pn=&> zvWds*4Yv*wj@r5|E{}Vxlr0_}$}ikZaiXfD%7)Wo#;zvp zOKSE^8dGkItUEbZ&RRbfRp;pT@b9I0yxLe`D=QiPrN;U_9Dbl!$Nbh<#8&eq$A+cB zn~ps*yH}chnSKNI`BI@p1)*s$c7*6nXLaGqDua_~o|pg9FW7Oe#+vCG?yJ}UCgR1G z+V_9uCpl>ry@}{2U+^O)u-V8Kv6*@~8hBM{J(b=>v<6xpnJbhVPptWM^nCc3uoRdX zMe0tR1(9VKr)A!wV0r^LDeH*qP%w7uHT%PO8bjNjV8MNJ^_fWu|GqXvfSn;03(5ft z+Dppkx6*c`DiC)0EUAjmHj8 z$O}@SU8IzH(L@rAWjvA)Cljtp8&A1O&c)QHIgfYg{F@HNi=RyaB^2NCaRV3Z3uD6r zDq)EI+le?^$NIK)8q3ZNPJd)CC+6!eoeb=cn?yD|y41=ffr{+Xf^4=}(SYDh+ zdx9$UwzyeY|5#D^+WU)Gx8mSk-t3`8N7$q3dn+hS{eEz-L#@a?XN2kgcMWJxb0!-= zPCZGYyKRBcMyO}XiHS$y(B(JDW%842D`SD9n4l*m>% zT56l~VN6qRhO3C)f#@D~D+T*Hy&S^ttguQDl;6L_8|Ock42vA9*O%iURRZ)&-8y&~@4JsprIlf&#);@5h^ z^c3}m=+&Z{H;%0dRUz~4Z+&E`GWexJPN{TW)ucL(Ju7QlogV8UM4r_8`zh-IK?vri zgmHf7g)+Onh_&3Ab)$75T&}lE0p>I5jpdGYI$4ZCsKTZMBi|)`rc4i0AzCmjch0KU zKAR6-(VUv9jd*Tnv#5PRpIZr;UWxHRx2Gk%Ltm+`T+L>0JxVf-S;nD3NaMDE5=sQp zNMk6|B}m#RNp>sdVgrRyE4pyY@V2AnVmw;>t{%&pm_;yuHxN1q@Ih_D9MX!bj8aIrNMp@y9)TSP4+z2SlVMti&7Y;wHL2zrK356OFRN5Zut?ZY ztQOr4P9wnqU&XDfs3pbBg+UeQhimNpPA=Tm`|$WmC#o(AgscdJMRwnRDaBfwd$WT$ zuH7@j=cDMH(FKEb7UUgM>$g3@>y(N+%9v=;w}Gts;V&mWgD;W@a>-e!pPxua7CYwq zLu)EgGW>VRzEydsK|NePKG-lOT}$)Ek2|6fsjzW{;t)*|Qt{`EiTWM?S?x;WCY124 z635rah2Oi+OPmsw^K_l?)?Xd%dA_rH4sZ|kAG$I?X!BTyzU-uE zZmRidTJfsdwDPE~DwNlrr49k;b7b6liX`z`fVeis<)7}bys6*@@_LUm;n>}4}`Bmg<`QN`B8_R1t%&R z8P4Sak4TPuu)KhZhZFc3KL?A7atB2^gj*~P6t%c6Y=IC2e+()y$?>a-uRd;wTv#KS z*dtW<j~ab1)NHnI09VYOM%U5h7d9=U_z|yH1I^+pCwi z@k)VthB%anX&%~hqcir(6v2ci3F(KtA4BVm5YAcJw%<}{m1peE#QpJs*df5?ov?i& zU;cs!M&7QTESXAQ|J~X3t-Bu6I*}s#qG5@bi^Q!AyTZ}Xxu*>+&@!sNjv|_0qEidd z&CCVOY*lMEo(ARf>s2&3Repzs)h~=r$dGUs=7{JcHZm=N9D%0Z_Z}`P;V6XdDkRp$ zRvcD%qR_%>dAvuG&B1(grm;$u0wgq~o;M6r-rTLoQ~tZuD8uweaEns3*meQ#bcbVB0E+8&lf9k)Sg z^8CpLo3kHrW151U1ka=siVKyjTs8WHx8amTyE?=Q!QrP>e^3MUchhB(j$9EQ3YYEt z2L;tf$`>Z7-BB`;Ox#@B26Nhr^x@nWPCV#t({X!7`f@R2@iMP%q6~Ivc-o}e1hR&$ zW6|NiHDK=>Lc0?l8+u0-xN@rU12`YIF~C8k5xs+_9Z>Xc{x{8@c45DKbT!**XDvLK zibZ6%?VE{M=fIbhO;Ss}v#%o+okPygq#dO?9Tb@!U8GE-Rwvs{;4GjAd zH6E^h*0e(SLCK^#k+rllbE$n@%u1NY2$crZzJ=NH6G`8trZJ)4@5JR-UTko=23M2l zUj-j&wjVykE_@61@_ioMY;&n*FdJ1#H1uG^>z01L_W{4a$ZFyKp^L^k)%4R`xB&^qw2$i;rxHGs)XP>(GVKmd%}U}V2pBWQo>(}1{PFf{`0x% zQR`Kz_sf62{f{_~1j!ds$Qld(6Hyz6A^9R~^6B@#f9ijyTw){n;+^QZ|6uz8z9@y{ zi&8Gaj{l-q|4xQPK1|V(eDRL5`+o?r1R^0l>hrQlWUd<0&T^7M|9|--3P>5CPd0Z4 zlwD20E^OT35o18w25!HWJkZjg&>k@!e%eF~;jWpqQ90*Ag=!-y@$)ET?A4R^*{?3@ zi_HuRL6{o-XrrIceXvQ*k!{rR@2P5egYe4DDNy&GZSeznYUHRj$}toUpp+~OUUn*x zj+$nhAE-Xh2%ok+2D|D>ngxEjRJ}pAFTkUd7>lJ6Xaht~+5tpaIw#cFcE_y&BS8ci zBUK0Dk_SM%;DgjP&@;eoPrHwQ=Em!o#;rD&+cgoFeHB2uvL3EM_HD!i9VcXbf2h38 zvk$`K#_dv|X7FAyf`A~J)g=|=!gt$(e6Ug~Ui2(#$F~(nH|Ac0s|Q+3=LUHyV&d0F zqk|xWatb0o!P~z!d79Ms*1PNllziykO6!vN!<#PTCipXHSD8tWl{vW38p?Gm7v>(G zuJ!qqiTXYRYyqVT*$BcYW}$mV5=d>A|Dt_W-tN!D&q1kI`&W&t_6MDJ>}j0N z(qMw+ykP{d9lyc3y3q$5=^p|uKuNS4L||`|wIRdM8m>=%*^^F@8<|_{h%Le^f#q^N z0RE`ktFuL{4_5Tv!}1Q_#n-*(6`{T$L3jWdGS)~-q6(z>zX4co_NZ}_x2lIZ2GQp| zrmrgQ%w|6e%&B*69!Px)h;Yq_H88sTf61E9QWVGCCh4l{E`TmIHlnj!K%W6H+*$W7 zOsmvn!NrucZ3B6nKe_vUrH6qa$!uwG(RS{uPhNr4usGm6T%ShAj5G%ZcCRO);PACs zX$b{2JcwcG+vBbpFo-F>%M<^v3;RPZrble0HPYy5Dm)VHOnUH47)b}t^Bfpr2cR8x z2FLw9OQ~#%xo?4&HQUI1_6}H;Ye2W;Tm_9W{&D5ZeP*8*`X!I{B18G?qCvo=+q2I# z<491RA%^r zMvmS9xvCE?q;_Qi;47R@7qA^b!J@UY3^;z|1&VE{BVioyC1lv9-V3QpBwKa*`8h>1 z+{O<;GI2r*!wV2z#^c}UjZ8Vu0IfF0?ll6?k|e%W-j0`L%hXWkw^b2gUe99r2~g?z zclmD*@uj?A{z!l1!&{g7jrV{3MP$OP?^Z=O?MYHEd!~yGK%j>AUCunbb{kLYPY{j` z5ozhqX8jKb*Gj(5JyWd+Iy9Fegq~e*U#crfWL2f63$27=`GIUvUd<+Cs}y?amtxE4 zPRNUx~B)C%YU_DLH8gRl!znR6dt(V$jAAjxEb_5D{@Fe5k!0xx+;Ce;_R zi~O)TvuwH5yKT69E>C>QKgdF~W!v%MPB+l13-_9(KKM6aWw#d{VG+x)X~SH;olKVw zM&jfEam%BfUw!Hr8ESq(r6C>=F2k$z2GCg9I-wZ8rrM%*9m(y7^5m6t5;&UdORO>;IZubIqTc#qS3z%F#<$A*(Z)7W}C&Wb(WdF`qvKVHV zuYi?+T-BT95nqaIjui2YEW6as%{|8J08rk)kA{iov1|;Ye(=Lvz@2I0_#6Om47ooq5R9!ryYD!JG*(R&Q{ttbdYRa6>8?mFmN;jext z9=?A!pKaOH#XCyYZ5G@@*s_*>`J)js661mK$y(k9S2SCd?dj+1%A{9Hvgo#J9BUNw zcg#L}w340${3|hav-^kvqPRc(VkU@lQT{aVI=S;#QkQI-d#rnG?$Hkby~>LTyQ_WY zU}>WCOtMJV)Byx?9>n}t@m|XS8Ar?3K&0#hTrvAQLsl3|U^Yzif4qbb$AX%|9H7df z$74p-SJls&fLD?6vxG535Npvy$%9(@W%W1qG9(Va0x5Yr8M2U2$RuQj$l0(wi-qYu&I+-Gx=zO^j++6~%yY1JF;lyitc|BfUfE3Lx0n{ z9HS7K5MAzIGZ7$!le?|BMLh7J zh`f`3U>M>LV5Hvj%JM-l_uMdwXQ(XExC%Fa411NM;Bx_|7NJJec}>y}5YG>CE&ZK8 zk`uuqocmfk2of=a(`ppNFdj6>K`0aisF={{tUVQ}P~01q;7?inMMbRl?gYYk@G~oH zyE8!ba~_H53YQp%f5eTYRJOV2CMnv?Q|6dha({XUW36N$6VGkAUCvtW&IR zJlLl+(p%G8iG5VEZ~jjJ-1`u@{`Uq6rfFyx zkx`IEqsZ0^L&=Sj%7Z@4R@xylOJ;Ot1&WGlq&}M@bs9+&b|-A%ohnof9%;ZU6;V7D zlx~$%)^yQFw@QdZ1vh{%Y?3BZ*ek1@Bh%tk?bpYM3$d86Iw@@MIqg*%y2RBa?0L3>zz zEJI-j{0ip^XB|m#LD)YChNgFk;s)QFlE#db%d5AyFrx4N5Uu4{oZlM)O%^!RHB`xj zrXhSXs*4;`w9BD{G9FpdmMukK<0-vLi)w`e)qDqM$Oz#RK7Pk{9CL>Wdfy>z?{95{ z+P;MwMf0DrhGgqyzlT#!A8irJm4&$k3MQZRYPlN#rJSW~L>?K$kMm&@J6Um_mX(hGS7nziwB#Xyr zgLZ|2Rpbv}wbgf0vE^6oUQpv#@0zZbVmbo(;QX5r4*fnhabp$@M*GE%e!gA^sWR8Giy@PoGWo*M*awi6@8nZ_s#NshX8l5-p>taDjkK-Kmp`#Yq!{ zkXvhLtQnPyl67LNB1Kgz&Cu05CZ zSsx;L&CzMd9*k%6Pbn6s{B~)CKyn~e$`BMPzF0mr`$k=3?he+Vu#?*5I4^o;I8L8h z&oHIoJz|o@n01-wV7-Vf1Dmjk{9@&g3r0{UNiRE%Fw0#3$>GSF^l6xgdJIXN2Rl3j z)fx&>C-dG}K3?Qdx>SI;Do09nD)Q_GOKzHkEs(x~vK5^EiWc&vs$j=c=milx9IOqe z)kbU`-@Y(TqYuTxu?-xhF5#t@+vJBOdi)(9nYPhIAkI)@CUs*6SoRk$1a2AGW{QbV z*dlZ9u)LV4@e=-Cl%59SA=w=T908Twpr!pz&D;7%J4B3~q@7~S+msL1UX43XO`rI* z*`McjSo)o5V z!xO4iDhB_z8PA%PJo!fAe+R-sqsSOpX%x|yYZ=D*YT4-CNVa=c_M9309ZEhfulA2t zTl?8wSrco%%N7H$4j=wkxY?d1e{oPtfl5w#hs-BUvxQ3thn5};TS-iw@lg7H@tcnw zUSl9rdp*pm3N1Sw4-%j_sgb}LsP8U0VPk|0XJ#H*jz~9wRThN&nmb;{z*68eJ$*mt zlHL<;dJk(;Uf>RqO_U>U7-m_0^Q8^FN>UobpL_P221NX+no+;g-<8zJO@wut}E4KK|(AcS|0f%Bo<27mZ`kh->!Lr-lJ($w4SneLM7S6^Q5uC zglu%pL>Yv!gvYFWb{HNqFMiPAQ=^K*=eKD;ROr$9FeVTa# zDNzpEZeMxqaki2C9eY-B-)@$bo(2AR zYO7l}+fkk!Avj~7toQH*dWw&Jo?=zYgt4aZuvg)5)M z&&F0kxrPc(Ato%Wl%a`Minms_?}Wl$J67WkZ4kn9m$pwuX_MR&`DDoqvlpCVah6Kh zq~dx|$du8qZ*6zETS`9Gi#9!cs7KUE!B6y!{2Ni{4&i21dFPJK)(-C$>Nm7+xEPyN zlC5n@`?Ad?n^&jrB-aPtdhLewFIo(F?Y_NVbvP@qIqSR~K*U`FSZal@Kt-_uQvP-pw2~>3X-r8AXOVLV}8qa!I#7Sc|Y^|0mUx<5F_0i5d zZ#2=zKk7rvs+L|JVdIUTKXm@#`=dSWn;^$oNM3~X+>>Z4rVjG7F!~cS1qI(gtEm@G zgUds$Wu4`s>R^=Q_hbSYHZ^H0NKjOrgE*R`pM3$ewzMBEUVe=6h(7 zcpiCC*|Hro|7Gm4Cp?IV2zBta+$@QHu@)cuCBOK)Qt_~MAx3Kx3MPV_mXseng3wZ9 z3r7B}=j}Xc>(KsJL>TtAkJ3qxFOD}UPb&9A+CoD7QgNFVqj8!1lJuf0M$Q9EF6#*^LEWUZKZlRmx7*wLhYWcJ!-pO!1X znbA{pREJ3sPzE~aM_^#;d@YF2e=-wR@fC@;`lRc>3JYSN@|(QvE@o|IqY zUM=E4d|wl)=Mu*2kXdsJ?JV`960p9x*~KE)&tkS;Bhx#fxW6NA+#jel*KcgMl}I`1 z(L%Z1I=OrwSJqPHf_{$Lc=-1^_rhE7Mx9$$@zLO6^brtQKJ0$+emoBqpNTp7PRBPVx z?uW1^WuYoG9GAY&H1Fxq(h8c(0-!2$uNpebooVQUXGrN z>{~5$a|)TNuMjSZQk^UGJNB9kzeCu9M-}yZuH~}4C2H>-j1tz$=A}PSG5=V*-p0gB zz0ckCx@_4dQ9`#?@LE*oN=4V6`QE#lpax2 zp!pwXDx;Y*e4?b#M?fOAxxrIG8#AdMfke@c38?2C?DHxwhqn zbhERG&Y89{LgPZHqcL*9fk1sTsrJJmP5J_Xy6PuBpB##SR1xcnpqp0p_bYS^66EXK z*HM-E&<7kBF8-zhsq-A9n!k6WgcC2Aji)?Q)? zIN#NlIb#AVW*}xEi^KTliLH65xEAKY(30YVu=W?mQu%}8v9FuD9MixoxMwT89Eo9Z z0S!p^914l%yYn)@xv}y|zklZ?r_5r*_)#Rd7rejT3vc;rKk$_nMZ<&;{3R&^4zr9WD=hO^pd-Hr)Bc=Bt-uI{M(1{OPx!vRNrAj1BHBvJa zR?0Ph1(ucAG`eZ8TKmLzN=z-F8Ty=i%`skw*(n<%-irLO5TiD9Gc%H;)3 zR*y?xk8V(&XBjzkRWnE=r&tzNbhqIiaz}GJ?L~ZZ5O3A!p(?}h(*>p2Ax^LH#PiKsS1J{NX%b?jw?26GL*KLmQe8BkofB+8_xQ4DQ-NI;vT+>ywNo5<+4uP^qU zpUP`_YdV;<)*y5(KCU-M*Ce-fn)$M%(0!ES2U_LYS7ujByCyDCh6+7}+gQqk)*w$k z9lj@=Dz*|bUVdL9T3>Gna>{~>eV18pLJ9G?az2Jqgn}!INR5Sv^vBWt07_I_n&9-v zxb4c=?W<8GZ1n@W5jv@rJw0*u2nu$9pWE(YWG0PIB7_=leDtao1~8B z<_SS@=p*DnBJb`Ba*%2rbdOp4_~zRVlTGeI4K^^sXOVKVW=h@NDv#d9umqWrlp2`0 zed~Onuf{Yi2pJX?_vkrI`l7|Vtn#^*m)%S~M9|_pozKk2D2;pQa5+sz#+WCXIiH2X zF2-g&tJEJp{U0Cm?=iSs01G3t>HK!lG&b^Q|8)+351P`UGUt_)sy}4>-$(xc z|22L~q90N089iqL-s+0zpe?1%HfRq6tHNj+1g)V(7U9dvM$RZlmC0|xR!|op`g{f3 z)>tLw=w`No83JnU-h+n8Q5;#5AU?0gK~op||2W;TI5~@|&_{+Qoo=8Jj~|E)-1?HM zxD8tTR*0{q=f9}V5O)A+bX%YZUjZu2LSB96y}1ZkP*i^b0L3a+DRale2io=^F6;Td zE)q>{;)5xxP<)M3)8z<|hb2J~o@5cAk79ByrT0T&sbi&%Xa{if%f5$N+L@x8alG~u?<ZZnV04ces0<*B?*RG(nYTI~bjLqcwGa+b=^XQ{~z6+?}RscR!R+oggLHn$SDOU#| z8rL%!yB?xnpuCLc`E)!sLtFqt?fT>WpsZqEz~xD}a;b11$hfmdIwp+xNaHCvImyKoqb~rp>gibjjtE zGPZp!yp?VnR{;>vSA~x@@xOP8%%c9s>2;x_K6fS#bFmSw2W_;r0o5UlzRZoF2i>b{ zOx1~z-E_7==p(k_h$kc5~9p^3gh@($y?d!#Fv ztEm{lVDDqvLDXoFp1h?kAg&gnBHz)eJ&%4%CAjPzB_81(% zDJ_)0YCk>1&Lkt7UPR66Rb61S zh942_S>e?V8@1d3d8-_$@Tp1iBpd@|B73yHS=KCr;G^qm&Ia)71@K%hCq|Ax7nR~% z=XM_ zMsD_9qKA+y2G3TJ<(Iu#I{z^4{`bp2w z!z#{p%N?*^PeGB@FN=}?&H_L@2*2s0h4bGoi0^^ML6cy*IX0k;#IXDvV8It0rTq8Z zG)EmW#zU-JVd?koc?h|X%5UJ`RV=ngM3{kp`09-_r7DuJkz!!4UIQbPE0GIzskG*j z-Vgn&3_|r}9ZAK4`S;#>>j~-O>Azr9j^XGaGT02cq{R`8{sBI^9YerTBAA7~H)4mT z1^!UG1Sv#cyzI?nocM_`4d^|uC&jb~l6~;bbNwHbf$vQ-VglsBGW6|;9{lN*`2{c& z$eEfs(@RJ*a})o@{fScH0t8Vxp5f_0i;Rb$*=7NwPI5a_n~*w>)}-(-$8Xy;xOQlQ zH9&Bkbo;iwCLf~Uze21EzMmc;l45cl@8D^uH<#8$a5%EV_X#MJO#|TI2s|q{#`Ue_ z5@o$Dy2pL3OQ`ck#@1m8=7}#ynn3oe7o#dsJ@DlCpo7M>63_qGsj{sG0Bno){iqBC z`c)}eHUYzR52Te=<2p|bRbs66r`NErgohprkhgBIPAHO;; zqyt@fMLZT-gJyrWQs&leSu8fNE)oi2lHx1(ZZJ8Oe$C zI{FC{3w9Pd6?9(~E2RFGO(5+0ZRxlDh&j$lbe!2XsoHqJx>&9Snd<2=>rypX8BM!m zU#K~6MJv}wG}75API1w$^&(A!a`f%P!LmJZeaDzsr~Hi%2~8gug*vHzZ0FjX?g zeyaJiUO6r4mY9W&fnp)MVic7a7mCczH^_5j(sjRUW{5=?pz;#g7i>$-Sy#QQIF+HD@ zO~8HVspYwXLa%Heo!Q&+kGUTueA{g>>RKv6cR-q#)gxcjq$}F2l{*YDqU6g(7}8e4 zV%EVgGZc#yVBDy=h_Cq>-ZkcWLW8iEVX3AYXHmn#VMTux#_B%*6?;(;TUA~)66;56 z#&0_@DbN<{GO7?3Zzq7#FB}qvaS&;61@3P*G@sAdUh^H>qujxz2*`ON?McnSqbAAv z6m(^T#Bd;Dj>-`>#F)4z=9LMk*m+xV6<_I;GDTF%9c&ug{|qvRY1U`U2?9=dDcCU?7uYi3YW&&LQI%>a;nknF zY^x*3*WD(bl{<+9xN4d1O?B6xBOOx@xKlc)(mj9p#P5$9gE3eyXa)q!E#L*)7qh^v z{S)aAQE+jx~t<946s37VJVg7v_ zwRMvaHBRBM%=4Y}u{IB!V; zJz@+jF_@tBrR9EN%?boexz%*8A*=9{Po2!B1dq*QJsVY`$4Q@|VSAwHHYq9)7oQr} zbJ3|S2OI<4ZO<+wEa`^Awy(W58-e;krrda$6@h^{BT}(u-5Sd_8_4wK5HVF0*$J${ zbMM9b_DA&>*BVA69=T;Bs^zIn3+R>!C7^dL*NiRqvOy*rc)Z~<*Cs2Z`(NG9wtBv7 znM$TE%4K;$A46hAd+O99Gl#Ky0$*B+E8=!5x3rulI#>?~t{rPthn`nP9)TzB`6hv_&o8#7Be=rH!4o+Yu)n zNKnM`b*%3^T~8!3x&8N|KiEM&$Bid1jz9w%SPRxFR`J8PH>b@>s}PKCqfu!B93DSj zVs3NDcv`g;oky!iTXsK?)@WrEUJxPdi^(diX8V#G4t%fD7nVXI+9Hmj6f?n0`a}Ze zGPd^&bP<>F22Nd0Was9*`20QxauTu}6t9h6sqel0{g}H>mcPQrN0ZyCR`URBv1OHD z(p zKP06*)uVX+dnlpTc;q@wp27&oz04GQau$n$TO@t9`~o-1TFQ{cxZWnX;Ku|p zMQC|=J+8>O;tYc6x0I%4q5$@rMLy?vZbNCG!}w9&Bsbco8OLezC2CyPpdy|j#vUh6 zgSgA5rs;W6~D}6QS6!Bmf5XqiNZi-D+ zwH+atJEL1uH*8F!uxiGF!4M)#8mE2b6fg$f5*NJxe)U4Wi-7u|Wo>m{LH$7pYNrg7 z;eg2-A4-*C+~R_Kvbr3#Z(&1aTOTwuP48*S#y2PHyd*Td@S~lRT~8Tm?=<2VFJRzG zGxpFEYt-?iZ8rLp^_YFf-6Bug#q%mJyT4F=s(gfu9MVf1RYb3Zb}C{1N&F0pa(psm zWg|Czl+pvGLjul*F|le|K-=VLI{0WvZ17q3J;KsO;|p77a#lf0?t7H70SqM394KyS zw2^Q#7Pl3hyHfu3w+D2Uq-K@U+(zc?PG}Tpla7(f1Vs*BI24iub#10`enRMw7)G}f zr+b3zOozs}ZzM+yzOQ~&tFI0Uq>1qe9|(Ot>_q?aN6}F)hy!c z(bHWcn;9K#Tf#BemP*o5oe%r3H*`qwH1U&<*gvy$ilTdElu?rFeN<-;S7)jj)N?r4z_^W!5aE(<5gKXG9KsrJdrl!v>HJ3Xl*><y&A%ra)wzv7Sb81$R}n6Da;JDB{DazSXt2M%HknoC%Cj?YjPaQV$w_ArREiB=eF6|hQ4^}cI7C8mCusT3S{dRw?$*zA?T^}!>Fa9r-zB&M1?5{AI< z$4?c6pkJei&Q)V@zEdsS6=?c@*n6v}sNRQvR1u_g5EVp*9F*=-O1fK+X6QQ4e1Gx&-*c|dTIb^2xLhy{!`{z+;*-$v3Qnu}!`TM)uWOx()@^rI3t9|Jn=2HuoiXw1UMDfF?<)^sr)bt7Hq4FF$ zWzl1)9dH`WQxzJV5=3YQ3UCNsH1>>l{U(@^I)PnFJYqp08~ z*iut_g(Y&DB3Wsz^QLeLhr8_4| z;v=j+k!0pMFAOvXAttT!OT&512ob0$I@ZArzZ<5ck-e1kP2Vd~bBa&|L1mtF5qFMJ z5;1w6pS=;~L-`^ZQ4Rteb_OT~uHK`57~TsnHC+_{uOl zxbVq^b|-!-1D=rbG0%%mn*5*~m-mdz1WH=xm^wP6;ups6%CjFR{#Z_xuVv0 z%qZL*a&uLJ@VYs3)0@BE9W4XV=lVqW7se+aVo?3q01A&-WB=C~*NIhNy{t1rkaP@E zt+%ah!nX>SqKA#{Ooa>IG`TK$3-7s7ZDXjI7i;JbPT~NxX&}pH-=)e5BEPdXLHicX zqtpc90TEluc2?q-2Jz?cr98g!Foo&qN)bC|1TruhCiAuSlikZR6d&;j6veZb<|O1NTjKPF1Jl+oFk&sUA?>K19FzLW(FA%x!HY zQa_n0&+%j6b#k|QsaIb-^-0qF*B4qPcA~cvO1k;ka9WK{?-*@SyW(6l@AoIhry{e8 zH*UQ_wYqu0{m*wG4cc4l-u~iIs$CD&W`|mt2ER7%I!XvV6CbilKHKt)&}=RuU-zdT zIQm6S%grd4J7uV#o^u&>_F=o@@JvUWgvJEoS68>z@}sPI)FDPp*gW+C^>l}UnTpAP z>F!Cog05bJ%56K#KgxAfj7mYQf$;XMe9N!YJQMXZMq@sN`Y) zLRn;dR2!6MDM`BzX2MdqNHchBck#Dl(0GhGERV{gX?*yuKiEtl;Bh` z3MG;Ki2#>AHsy_X#q92DS`isO$)V~q0VZ+ooLLqF+nJ{uoM{$qpC84T&8H`YZ2b>T zIt9I77fQyYj`t~qdd{}hs zP-^2 z+P7ZvVc$hc8Nxa`l`NyRM&zF?Yg}6umh5O@ErKpmt|{o|lt+Je(fG3>&D!T&?(Se! z_c=q+gVrjNB$`Gw=ET{GI=K8R+RD zANX#g`%2}%=i`6yeSoR{|3CKs^-CgZH33r7CL*{?gxlYg18F(@>hi*-o2!t~)Lqc^ zDHdP*)B<4YS1$%Z-S-bw;oJ}n8aEK;g!&0=8#*z%8#7o$ALM?0JlL9kvNV7)eFhqN z)^uSFAKAqQJ)sfhd!#RTE+Clm9;Mbw}1&ak=65*b+Row4C_{Gn3`r*SqAGu zcY$1T%q&55MCLQM4t7j89!G*Aqyd1Cm(~GD(gm0x1m8>pm{3Se@6G3eJK{wLH~BDW zuhYA*{V}=;P{tUs&PXwXPeTB@u_1<)`pv#Uai6d__6kThoq-%EJy=e%!ZA?%-?AaK zIspKZ4Hmt=8zc5$PDj;88HNfH2V6x}(@ln#ls2`5li!RafjYlK37yya6C}s+X!%mL z0mPpC7P$AU*=sN*z$_H0DH|i(Bzu}3q#^2XeJSC z!lg`Xo^~EJ$W`f$+?_2QHL8x}{r-u{R{t#aF%7ofi9yV*lnDp}N6IbwkYAO8yOpPZ z0j@^|LD;(R?6fY9xiFZLAqHnJyMT|$8#7R#3m^x~2sj!Hx_bZXLG_;iPkzC^T9zo@ za{%-|nE+))X4rNm!d$M8scd4_YCX~*6KxY*!D?)x-J8nNB%;)P00gR)f-60Z-L1XX zpkNGk$#!2sftZsgoGwEw+W=1ByFXWck2?jLEJ726jR!gd?NSVGx1?~e1l13qXvWz!Wu5>xXcNE^r4rt!W59M~ z3*@UgJRqe`(6XQP47}TI3c&QF=mI~$Cb$b_z|}O!n%O!IL>K?iB=R}{;oJm_QYE(N z#n9)(Ji~cJNA}9BX*uj}Gyy_QbS?j;Ru%}4o1`)FLQVoMOD5C7`MxVwCtD48g;4?m?wSYn9BC|C(Zc_Y;IY~ zm!HQ=(@0?FN!(fs{MQEZWsKCcuAKh@Kolx~X+Z#L;cojF8)%q?Y^Rro1gdO-NH+vt znp5cm3E<#y0b?f#b_?V=)6{r687%2UAD?Iu+tGHMjq0NQH8*Wsek@3$(5{ zx4eVvgJ1FXSQ~@vI?}O#X%H$ z;Uzb3(UDCs71#o}{pi^|5pV1xdpB{^R6$2lO0ejAOVfgn(VY|4-)y`u_j7i}TM**F zA!Chwr2*&<=Bkn8?aM>^1ff_F88KNLn8+UHWq?c_2bqP;-6`2W#J(zN*J`>+3N#Gim$ko&bN@7P@=oHVjVJ$8 zol?MG95G!4)NAEmX*Wyv{j-5te2C+$DyJ!`!8pe41PW+UY%Hi$i|OFhSPw}{r7a*x zNYC)YaS?L(FWASPl>=S{|(^%r8wYAMv|qKR^SP8B5jCvn|{E=?0B5)mvC1 z+zqZWx7_DSc9@R}d3r{2b=o{dmI5_+8dBC-Uc{v>I8ze1?;yO(?cP+TsTjO(=|Oyq{fOb}B1I198S&2Oxn(7|CY04E4So3?_x* z2VsrOovi!U^TS;m&AZP3Jg*?NOMpG!RWt5=XiJ}v-@zDInSP@)dO3B;TsN84p{xorD?9&)Lv&zuj2`}{B^-21-a>a%K*nmn} znut@W&GjoaGT}`s#6;I39w9WQSZT3b%*9c|)os2*;FV-Aep=og{XA-~3Up%K#JkRc zj|dj-dz_L&uSI5P4Amh};*X&1mf+qpq@k+C#u+BI@i0KsitoITNTk?fS4)m1Q9>66 z7KEIB7>moMys@xA5vF{X4qMD9y%w5?KqyA)={L0&DAb9;b~Q|(X4kcjY(=*n5WEg-$N z{~ah;j1alkI6i-xgd}tFW0yGDGPsjQnU3*$FGhB}w0)P*&hjsYFD)7Po7f_#@%;O5 zs}T@%KO(Xb$(=@S%fTPLC)G4mU{c~urY9gD^N@gVmzF@PXFROwWMO3ePkCEi#ZZQE zq}^O2@8EQI(PccfJI(X!eHw<4&v30+ESta71%V3)v^#|23H7uAf8PZ3sHnJ1DG(!` zbKEUM7V(YvB3NP&c<);9Nd5WyL!qSJel9YKhp6bYC*7`(n^S}7UbHV${9R5^yt?Lk z%I=+pOyP+A`G}B%(JatG8VZRsmNahD`;G`^ei&>3^rBPV9N@=HcD9}SRypJfw zl{6r9IKE!y%D9PF+42dEIIHtaXk)&sNt4*}tVCPpO~~IY<0a?z;KYh?;&5sP#UuQ% zVB&pZUl_a8$QlE0Z<00p$wNW%6?>XDe`a-L-07;}BTZCI9$xwZT*Q``j6CmSu>CLp z5G{H?|N2bS=nYaaevE)^JoCsr{EuhUjcwN`0UDW9{PG#oL_CL%W9d&%Ow)sJw$qtr z;c1m@r8BH{tmYjc2FUa+wQ1Nw*ok5AWiC_lT$R?g`1Qzm7@GeIu>P{cy>^CGx)XtD ziCKO-Kf9?0F&l$_fzj`UaVTj>MQuaODjCh`9h%n7lMDI`1a+jJ>4Z?psD=FWfajG z9dNkKv(gfiIO$Io$>?CNdzSL+%T}m>?0dOlj`EDOk4VT*cMEfrl8HDCqvKLoWTeh^ znA&ipYviWBhesCc**0AivQJmI8PwW}b!3qi`~jO+1^mAAONYEA8K3abB!9P9HSIEu zdpfvMIvtr>eeD0zC1O$tJM3J@;rTA5+EQ_vap|x+oQ~)6^ZjL0O2L0tApvH>{c}5Il`|&pn@xC44ekay5^tu znWQghEIegqhh>Jte9yDA#kYYDet_T(vcKK4j_a zD8)<)8=lvspBnog0rBI(n%VB>KVzKgGZwIG$LF2m?Ks4B3Xk_S#Axu_}Yih z;D(-V`u`a?CuE;IBcU3|2zDagEn#%ccau<)u;9*VLd~in3cata4z_iO13{Mr)sCLi zXdGPfX&9S>_&9G+D$7^52Kei7`o>c06GbUQ zS*y?(JTc|d)%~N4(Jsz~hgrR#5wkA}{`A3*mRdnHxrc$m{K-}V{B);QN+2f5RGn6g zHNXntHKaE4OkGW1ut7;*A64oxiTEtW1F7k|H*$&286a)CPV7kzF{oyH9n2|7z(-62 zf73%4A)zFxrCIV;(-_gXihC-&MB1E0x zo9;1vOqbZOp(xa}Vx-_b6y0^hWQ`}F5@npmnS zLm&j3oC|puOb~E$emw*2Yl7EGo#7Mqn1uMgOwABGiR&9$7=PH0k`wQe%KIzGJD2oR zV>#_??@YTM@^{~@&3+nZJtZ+y81{^X!|0n`Ttzy?&Ad?a{yMyc3NOu@6d@TxGS5yPAvv%BE7$!Cs7^}$wndX{``9v&XHy@PE3@%t8UWIS^CtJM@>2X zI6sH>(NrL%4`))n&UJFInXq1ix;-$Th>5r}$8tkH0emvqn zm7v*(>OHXnxcvHb+v{PGcd-=oru(a5eceum}JjS zuJftT%Pv)?in*l#b%aG;1wh&*E}>d_QOCXO@?F!NJo9 zw5GZ5c)Yzn{(|*ZII+C0kLiML--YE!RJWNQ%9E^^cSLj3l@m}Xi;*mz@fhR?^R2{V z2*=#;?DvG1t-IWzyw6@pq_m*8tnQa!AqitI05?PO9>F{{+?ooe)gU)xpG;K%Z;gd? zDLgwHE|$50>Pzf{YiYi==P~VLF~dRRu#b=^(1&UWTiqhPy1*xtE)7BX61z&y=A7Ea z+Dnlu@o{pQSou{fd=Guq%Hx7gf{3l^B-{6sug#r46NZs7{t2X@+}`Yo&|71UV2I#5 zaC#f@yXblPu!JXy$bn>2&ZeNM?I%`<$ROt#}_h(n&)L|MRtCTa0l z*A7~fMrkR=)&LWIIZH>im}Nh50>7(d7%?rTIx;~bq>YOE8gC9fhBXKNnoI$YJ=hJJ zfqr!waH9|T!u*PNs}}k;mVAPem{Q5vk+7r{JpPeQ!9E?#jd&FCeaw@(!Kcz&tl3*c zec9GihA55PF;lLVJ6b*J;lFixNnD!_sO|74Zpvq)@PA=$OyIb_>4dQ3n&8Y>6}^Z% zr;n@Em4j%hJVT{ zm^>GNHq-_;l8d(w_C|nL7&3e{xU~+E`6VePV)@O6LfKbStklPm;Et)k&kC`(S=$1m zbM91Pg8a31o_#^~Vv(Sh1=cDURnn9E&V2|BHx*BXGq_j4P4IX@UHK4EI_E8G)RYj= zS7G6iQ=XD?MCUf}jn2Av0NpU?O15Hq$-1yy9QX4Ti;pc-!hNFWk9Ip#B>!9IT-fgz zN`Frdb5ma1&tNXFTc;+yUi(6L5wh7e_`8DI2n&((OF;My$u+4wr`87pC_>9_BNs)^ zgx%*q7^8eC@X}K55#8lJ28V#4R%3*pr9EkI@GEmQ4plvgPD&Zv8h;ov;C=)S-x6FH zHy{Bv{vrg&zd2|jq^-$dUxOXO0gWSj4})ZZQ;5*U?Ym5g-g_Z`^1I+#=MG1WWO(nA zT=t%U;iKB_lc|KQn?{N6VX>$i%hs@Vgwm&h@)&TL@xVm^TX& z)BN>E8)|v}8z{{%LWx3>8wL8~Uux7PprK2$KV<$ugfcfKs3M_}I515A&C3^Z&|m17 zvQF72$iaugkAio!UT8O)Eazt(i!e;w>>dBmZ9;Y7=NU`d9nuyp5B2frQYS9CCt1tH zYKzf1olH|S5JAu#>Cmn)(9oK2)3~AdW$1-}G7PFQv@NEJ1o}wjyjL^s)Ndsm{(S=( zLoksf(H{Qga#eD3ei`b}_GU}(c0|w-zcB%Qel}|Ee((8j4tfSy^1v+oEhOgSTh6e3 z<9YlI2wAUbQqUg%)pM;Cy~7=opTTA|!Bre*{F?+#p{4sBVt6Mplj6J$DZ%pW3j-wwdTY{8)2$-ffD}kw#Vo74@2O7oDoP^Z{kam${M_-j zjzNgpfoEmy66PVbQd{Y!>b$%uFqfA@J{ZN~GM(kfzcD`qt+dx=k;fy)^pVWI^+za? z9s(48PjTI+xg?5m+Pv4wgkVy~;1~ zCYnv@VZ_EFczLg=g+K5^UD3yTlu#Ycgx7KdJb;vG*TjWeHdeYzEnJ=(6$gY@mR2gM z$mv`BpNbdCfS^o}}Qg&s0~-f6{N9m!vj zUww+3`O-Faf0iXiTRUsIgLB@esK)owfz6k15BsWLQ|Q*ve7)itSpmxC7wi=WLP<){ zfA0QYf=)Kkkqjxkf0GxP#wuT$W637)>&g9h*MCpl5qSRSUtjnSjrq@a zEuZ4b$z_nMZ~&vyfAmq;|MR*Ng0Qd^(|of3aH9XmEBd3r_XO{zDk=RxxIk9iJ@S5p z{0SXb(7&+#D0AF+b5k*uf)DIJ@h8W`Ojz5mBiYW z8s5E>!Mb_QUU_N0X>1rZ?PbvXlS>$iYd;EDHfWS=NdrezQ?2?82lNn(kgaUTc&xoa!#N{+l*?eylqPmAA!X`I!FZeg^PWbSNlB^g3F3 z0j@&T6NuG;hX5{va=P4IAz(oDQJO14(_}RWW#J2C)$zqc7Ro=&cdQE}%i`F)jL9O$4+9jbrlfZNs4v`XKr)BWMc{f^>;ZAJtli zsZ0QcQ3YDP2D1S}^d_%zy%e~!GQ~lL+Sl(<_w{=q{A>9MTJm2s{8$k-6I8BabdM}! zL8H*?0;S*aPq`epkL>|FGzQ+`!BeH@X4%S%${8-}zdw)nbYyExAyGk>_RW!kr$1&L z_gQZt!e#Xbp`5@eRqhgQ;`Qe0qIs5jHRo-P&$<7mdnaMlK=PBvVI$V>4s6DX_$Dw9 zT~IYEdY_se)~n3;fc)2ebtS9YwTBdEfBPgiD?zliHeHNQ%JqWe6BV} z?SD+0C3M6n=g=B?$m=1KDqD`iIn(l;a!4I}A2v+;uKp}I|2(wT!Q9Up<(vENNOHYm z$2o$laBuQmNz$G*mGEji$Rso$)%UbXJ<|!xS2v}u1u(0F$;j*?L^fm$7_F>G#W5O1 zU#I@t z1UcImxC988dk&g{MmHvw@X{+ILd~y5E(2oE5{U5|q|4k;ptmKu0Jgs;`saP%2A0OBJKs!%r`#nbdFm)67j{e0*?;`*cRt;#GpCNUZ z-wl8vD%l5#?HOFA7q0+HX!fC_}f!kETok;(lNJ0}NY(w)TX@{x1h9Z8Nsy3~QOe z?V-1VWh@D_+p+5;F^3+SIj zXD|XW#`8qUtw!caukK78@gMT#dpxr#km-JKCt({WlsZ5-wO{{?ncx&;o;aq^zgCo`oNW>HG;M0Ci z{ZtF=?c{%E04xo+*Flj^(S(8Pw0V?k&O`J^o+9}bkv=}sS>bQkzA;Th_hSzUpJijV z!=UKe!%Z3X-;2^uu}{w@_Sm*U7H`(`+x6QnKlYl3-p)OyoqVcye#NME{%Ul~w0hb; zc-r;sUDbprgZVv?g~@NqR@1&6sxH42Lnj%&)mg#~xw}3|`c(Vq728~pZdly)Jr#He zU2>Yet6u4-F2P1NCETBw^%13b&&FDvq`ojw(%EALg}-QA(D&Ew`@71VyblJ>1M~%+ zYotkg%Aw|4-hi!R;Gx+B$TLC>BDUQ<6kl_W<>$c=6$z4DjCvU5(`{nSVHNGF%d;KE z=4fBwisRg!9!QUmb#FRvP1U^ItaY`R@yzflE!_g7^87{jnK3q2tsv*X|A^Ar%uqTM zTaI_fEq(*|L90~ud_KM-2;AW{mvH_3sM?*b;eqeqid#|a47Qa$rnvJ*J{6n=yH(J` zy>I=(`MFIP1D`AbX#_ne!3KSGIBQGve7ClIK`5HbjQc8 zW?sM_2)0qZ2;!IDYu5cSsKkv&OwxS}T!*^dp(#@daR;#}x91@PSjQH4gbi$3~u}Oh0ssUFWg!N~f%gmxqmW_3K)wc5@=DTH~3|$Rfln(@6U~o8ZEs;KG zQ9c0bircN(r_%3PiRP|pI(+8%1+vmu4l6-lQOZsC#P@w*`QRTQ_ND20K;pZb*em!#xx!W0+*Sw6w2%jpQvY``P%{c~r_6`JV~4cq1^ z2CwIqn(2s7?s?`m20OgZY?l#@{RE^CQ>UEnK1b88E+CRq7id)$je{j#&={eOQGR_u zE^rlb!3<(p!4|}uuA1yYcMsjO2n>=R+X82Ub>qn$0#5%HF)P$}nf+T9Yi2p8v;6o| z%OfzGpv}VHNBQ0}Bg~b6L0Kz006&U=DIk4;WtGq|fYQ5N5mfKJb1@h256*2%KstbR zFXZ@~`$1n44kyd~d1CsQ{q?xm^ZkVSrRTUtq<^*5I&R}rCXG|B&!g4Kq-00Ur<}r- z?+kYbbp_BbJFkgayJomoZ5{48s!zO#nX#zTGMb%W-}fble60lsMU$j@8e! z%e++cWO~C=e^e#zwf%K>bH_a-?R$`QO4^U{dCqK>OOLPhh9B&wQc?T8RhuRFV^-m9 zm_9QAh=9kd1F?l@N^nKMFs3x#WX=3WZOtT;w?hLuRqy5Yie-E>t zO|Z_rlAX3f@G!+437!-KdJJ({2XaK4J<0a4!KUr@g2%mQ@kjR-6W!BGryS>}o5l84 zGVDTDr=IbB$IQA~%}lMV9Vf}_tf&Nu5N8o`?eP*`KTrot+4pDfRi{V!KeG~vD?Q(C zZQcP+*P#2dG{R!5Ko~=k3?pF9!04=9TpamMHEHy&0*f<+9FR{{@su{=K*WoU2wFLP z#%a1D-Ix-n5ilmunsdV_a3_AD?V8n7))=_Qp+cg1*u6a<^9iLT$@dATA*@d_X3Jdo&4z7eb(; zF)jp-O_AkLaUZw>A+iW(xuN)xY5mA)CJZr*H6l`t?_Dg4)JXXr_1q(SD z%Ugv|$yVAB;9s{^_A9-SVeDa09bA`M03J^>+8eNI2OzwU)j>FO!aWk2UA% zN_aS4x;#E`CQUUy*d!9mruiMXyLqsSik46G33~tgw%TD|H>%=0?@yB#L-gh%Y*Av@gA(x?7w5OwGqro(K-C8<-C94c_ zk{-77^1m3h&%uP&?`1lt6=X0alMUKB_FAW8JIVwKgSf5;@mTau{pGrHRp&y_W49Hf zy}f;VAM^Ql?r+{LF|ADUj*)gZQ_hI!G)oI#oE~tKXBP9%^kpn&X>EF`yYH>chO}Xj8>dtaz-%Lc%4QpQN}xOD<0B)pO(Jj{)f02h;pzpuQyB_5-d? zN&SJ8*xDMNEE4Y^k*GG0oF3q_F^E@F##(hgspGa2M?(2JNhyhMCp_lEy<~Oj6x;8m zEMbfaXlA5j(|=$Jxy2iDAGVulbZ!e(TpVqO7E3>f|AT*img3s~kL8bEKAKh@~mclNY3V z$`Jmg+fCb*nhg#5zB6UeRmxuumDOvL@igTyZp(4Gt@S;q^?QGgDH~SPI``l<>QK~= z$3`~C+SXtAN55ogbeZ~XuE7q%XOf)RJ&T0((^sl3+ijz4o2_*Dz9_OA?VRkb74gFD zznhRP>xb?SY`sYCN^mB$Yq_4{(AZDlVMneX-lh4nyyQ-8#KK=4RpEpD`43lek+Z9! zC$AloVei#so*5qXF*1klqqDWndnMIJ>SfC~F~>Wut%|f`N!1dz=*SZem|b?IQFD(HU8TQ4j^C;rOKPWQy#G4?aGaBjxD1Hou_6X8 zON`Kw*NL~+VC%9=!(H|A>#I3$jJ$WPxV{~??5m5L{IIEvwyD`FbLo}-F{YOB)pk_h zp~|N0;Ma)z%GE40Y9u5>Z&b%Zy>l%biMg2*xire!++bv@TzhXsfyRCMw5R!gq(A0w z1^<>#|Ds5w_Hw??tkvV%yZ;)$wIagX(>j>CpVt*6MjtZgsLh;7G?*iL&uFb-+jraV z?-NEk$`^D_C8ub`xhfjo@c@QBsS8$qL%1A@WPvThB!`BRT4e@qWrBD$cE0I;?V8jz zMLFr0{4~uwc}hLNb=**d53NH!Y7ayDW z2X4#}rQdU8uAdrKv^<|O>fEh6cC19YAx!3*x+9CIh)KZA?s#KVeK3ZW?^x{&+ZNfh z@UAVa&Zo7dpe#QBi~+G`heonN56^3qEzl?1l3JBI6;gxCCqfDzl)wMv2Eiq;2xXO* zie$7d_5mQBRC7U6@m(C+u!McyCq23NP%-rLq2~n7Pqy$M(X?xeOGo<+XO-gHM)6gd zP>AbtzQPm#$3!ACxNZWiazfk&-4SLwqNz9ZEr-wsYp!9VHJxIsR?7xG%gbA#^1DBK zLRUAho@f&Yz8TvwCIqvp*Tq$Y+|OG>HqpK`tj{iw18;xddhThmQ+Ig6$r4gZ%=_KU zK}yr&FA75>cyvx2>PZYM%XiN4l=#cvmUuW6bJYNCl2!lvmrtUcSY5K6*?aDnvEuT0 z_T)=nrS+;GOmutb<+ZPa2A(W$9$`&hgXc;kY_LvKdbb$ z*A+fzjJ6$2WqJhL^Mj*oCk2Z;7bUISL0{?J)Jl(=n)OZ%WQA97Pemfl-xBT~G4u@% zW^Pv$pX3iRKGRvK>F%NFYpSfL6ys@Fse2MxU%e=AKlr+~qcGId$H%;h_!~716-YI+ z80FBeVlAL;6ZbmRHM3RjQ1s>7<8=qBzM%n(0rsdY|Grl6)idgLaf+S+utV?rgHPE{EP~GMRTaRJFVGL zQ%*Wr^6m&bHug-9W1=4&O~ZC?3D}*0Y>AXNAI!y0jl`}toOnsyL7sPPQ^?>{k2;?w zn@|D^{MHOaZbj|_)0<5Yj=j1fqFfh}Bb~NR zuaes|KV0evo3X5H3SC?UaNy%IRgDTlyd2LnUVo=A?pv9h{9)ICc$Vd%$fZal5iNYu zEcHO77OL!Xc90U(cp2Nf$<(sQ<1%&^jrR?8`MRB@O#j<(OXbyC%J@6&>>RkhWRZyE z3Zs}^>MUFVA#nfDhk%~rYD^+P8S3agBPO3?_1QH&rEu`EHO#(mb?##=<$~A#Ojgv8 z?#v{TStmzF!EC1KNASh@lZ{eYIIO+sa~M_H6V0XIOPo{-yy3qBel*LD;8XB*ybu>>UJ+;l6&AT-ien4r> z>$%SQg;CINPHR!?Sxie(3-fs^yg6T8t)LgKa88yeT;j|7oOJ5b>n!Jy_futsvEJc{ z_yOTk#VDtC+#4IE@c~cpmrI5Hipq2D5szC)hU?iYbQrui5xduRRH-3!dh4K5H>~qM zWdw2Z{dK&{;ZO*3Wo{9}8z zkw6;1%l`$lx^_mVW4HKq^UGwmWvOLHlRQ)5dg6xFg~i|YXWI>?>YRmtUKc*>ej3Bf zv{J!#;(octy?-a@Bk;5y$~;&d5Hdyh$9eyJ%eO1RD5>si+vTb&`mJ~yoK{}U5F< zw}0u*f|0P+2a_rX{EMZ2l*n9I@T(Pz*%vv{Ek|xswAkSQ1;ar}_6U2J$8Mn5x|sxC>t{T}P(knECaQeTTz0&E?5x4SjHeSJ8P7P~WM#dRp4Gsl39J zH{x*!n}Tn;-kY+q=UWNhPG;9PhB%_z96sGbz~{||X@aS9i^<+nT~E|fz`@7UpfyU1 z>Y2}p{2iEJCgL4)gBjj-m=nCaCoN}cqB@behd)w#&Z+_Tgp@Pm_dIURzM)?#R`$H7 z$M-D(E?F(S^?jdln=+qPA~!7&2c4rOsjd3?#-2DZ>xO10t;Wfny8B5lL6cF#2bdU$sF~E z=@)m^d*1J$uC7#-Pj=%9_06_9l7Ckl`aTY-IXLZMC}^o4 ziji~^n(}r0!>#qG!Qr4qHt-}aIO$2e_y?4fhuf(A)9azH=eZVrcNc6J3pr-3kP^!h zt3R7m-8oy`!LnoRZP($RFIkTEL_RoLXltG7bs-^1TV$fld9=0LPkOu<#NdIv?4DY~jJFFm_wIi)<_ z9~02m!$zaO>NdX+-KzdtN{f(kd*FV&cIHRlkcyMF1fQ6Tgxb!N8tP)=^Zch?L{KyQ zfUcelSyO@*(&vt4OwLlQQe-!#@`RHptJMe|z;`}EW(V?V-8{Wn(z~H!vK!=3RUyT?Cdqgd;~?Fz6l5WZ zN;-%1{Z0`_B*r0|)hk*T)=K?kyhQ|PM=fZa%WwnQ*{+?X_Yis$1gDB8!lxn?;v@{& zvz$_EUBIT0hR_wYvYH_roJN^*Q3!{(8W+n-q$T{3-1z*ucyT0JM#b$=`=MHdQk*+4 z_g8`7?Qch3X1+0u=9@w}J@+!dpp_;5Apt(U66SFoi>2K-+rC3NlglS*F&jfSv!FG= zW`2cPJhHjU5~{?bz!9ArdA7MkTvBe=a58u}X_a7nohSY&AZBgzV#*X+4+G(s8ByNF z_O+g=lnV_Nqd^_kU9Rb?nrrDo_(BI+bMD$6t`r|fj;acCoUsyQo1>-GV*7;Gwy*sY z%327vC zB87bY__1>1ZRjx5|CKkz?CIgk^=LZ%O?H|5MlmPrmdgG zT`XEb4$VZ5Bd_Ujk1sDg8uiw%{9}#uEFAi{S(?7`&VUMT|8+^Xw{NI4=D}s%ZzrEi zrXFXW)TINYdc%bEi%G&B&aqb0WRcDekC8Dh-FBsY{hCMZxb>SJ&Xs`f8@qIK47&Ii zo#T5Loy<+iUa{CD{x;rSbUeo577Fl<&v`8kX6$k{2+e(=yyydVo)fj6=FjOO=n+>L z(b-~G3sIG?+nY|JyRIvw`lpeM&s)7>s+0J_JVb|o*;H_TSE)^U-}$3;pWC6p5H_L+ zCVe_rGCDpI?ud*VMXw8Fr3v)ZnqFTKcs@cFEDD{wh4087!Ar6gU|+1b;%Sj%<=4{= za`Y7oZ=j+`eHA5FpX9Uiscgt(COxh3bU&CgPxi^(4cMLi)T=}rkmf9t8!F#q3rtpf zFC!VWcoYP;@{3tl<21gN8|l=ne2X{!$)JbB#V7Q%Rt3`mtj1%Q><2L{Mi7iUCyD2= zt^yRfe~Khv^7oTHf1^C#JHO#WjQl;Ib9jSSO zGL{lsh<~#8MTk3I!>4b*FW+b+BkFuD)T8M6Cw1lep}lK88G%LKoQXYrf2Y$(CiUIh z)@af)#Krc#(}VVkCu-guF7YL^ZO04ON?={tz64ROfN56kl;D|Kv)69oh#RhpBSf`{ z3trj`yxBL7=c|@1nW)fJKE|vyd@ay7&11~8?Nw~X(9+yzSCze1Z8=rLu}F#|IeT7$ z+_M4rd3m%aFJtG*C&b-eU(UoKq(~|OPQ(*ia^*j9U7yQ)N-%w&QH!MUs^8W!y82o- z($8OimisqnF6DTaf2L+d5Z@18EBu8yMa0K6>XmpnnLVz3R%NAA@# zB-Q?1H+X~}bH)Q3okBL;eigV&3QWzr-+sO_{f8tM2G8l{1a67}QFLX+NRB1_>irTN z>{TKFFO%WtiY2prMbP=U`qkr5qPw_Yc#+}g5J-t#onrcmpMB^2*H-`m9~DRtMJqY` zfTSui0`}jp{>!NG533+2_qWRGl>qja{x6Y%)L%Kk`#V=fVslde*LVJ3{jqG6#gfo_jmgIi5SuQLEL4+}M_IFjMbAli<65~P22Cs%?Nw_G1_4-Ayy-i05Fr!6?OoE{2ac>~6* zswohE5?c)is<;?e|33W4T?*2_#yL{-AABt5$z<8|I2^aozQq!n1|kV%eT7f|eBf^1 z4aNGAv0jkNSK9yQrSuo@lgbc??V#&h!19DQNQB>O)`M8f%nM+)e798{!oc*z1*T&k za*N)?bF0Qe<6}?KZR<5q*=7TsmN)NzFYSW;Om7A?D|nIUuT>M>%)odnwFF}IwvXc^ zXr=tx!U1DlCe?|e4B8WrxUb0g8kjk&dU$_~fuwaaC)>_a?byEx)tk-Olp?B#KxZ*fAzu!u>$%Sg-y1^ zc%@r?Buq{MX-<7upE9=eYa?0D76owiTB;DzhrRrPSzdcxhYOK>6Toh0t;|=h61WPT zCR@WsYi^mI)M5sevCLCBK-ouLXCm7NA6}J$u+B+r$R^n?HqY`C@xpRC);2m!_LtOL zs*~7y5O~57z%!i5dan+qUo&{l`QyL+Q;NX)P{%>k-%8AVLt41;^+`ak za)$ubZ^`#BSe&Km_j6@D71p1;Pqxq>rjZ>?xmy)EDOtk5%?pyW%CIiuY@bsrEaWIt zQR~43y763qGg}u771=rz5u5x16L5J~O}r_>D+XDxczbE6LiZyDByyljsx4sBM$&8X>E+(&YSPO{<-jU)ZPG8w8{8d+dn*c&Xwf^uO&_n zZ>KhWtMPz_el%**u+F341D%Zv_#Bg1GdR{w3;$z9r6Babp5}BsZ0e6r=LX0$aRC^{ zib#w}Q| zPWMef*^zc{0K!b`c|Bo^H^C?f0;QtbdO`7=NXIG7GA~?4)OIt-DUbC-V-66)91c8V z83)O+t4%G=mnW;$;I?$vpYJJy65Sg%z^v;65~Rif>t_mbFkxJ0u`j8vfUi-2>;pFZ z61tpEy1O-0sNUH^xq5~rA`_1SDF>xaAkF&wW?23}j*pMA4{(KhrF=9FVR{czAr)O% zA`Qq2;7}n?X}kK9*BNHtRS$W+%CNve^$uK8){X)ULNvT2c+C6%&2k92)p6xEi=0%1!uX6+` zrUv{~O#NCWq1e=Z&{7UqMHUy7Ko{rQ_=C&%s-KO|1aXSJ{&=P7AK&%cS|^#pKbezr zMyy;=pp;V$`$$D{|8YgIorBD|rxsG*!>g7nIXs(GSHa^0Ls3Ib1lv4!-BpWKU_-5BaI9B2^PO?;?65gXK*ZtqGr2ZL(+g1KWJ=>&0j-u`CIX zRaS6uI;W%JM z`|~PhF784@7I|ew)YZ7Z&c7*tXKcC)toD|b%n!y(rB|5@SuZ1!jkIdkoMk1g;h?cOA|J!jT0Zm|6))RuR}+ z0lIwC*;h-9VKmThG!o#?sgY8hk2Y|h!)%yh`_-(4!@q%A?-f^?@zNk_g8h%p=cnI` zxjraTV;lJ?+LCmo#prFHbHPQ3NT}!t?LW~gvm_5UszSuH)f;N8w0T*DQo$s)*H^07 z6kj}11UlLcBh(8vpfo&bbXJ@j5U)1Aonj#TN+A1f1xyqpeE`b$36H&mgnn5TKKU@Y zO04pvShDu55&u1&?e_+^?_KnP3(PN!Rk^GrkdBUvT>ZBFB(oebT1Xm@97DD!ak$90 zw(8curITv#gaI9XjwRWX`y!dVg<4EAx&7$=*v2$HS4*){a2Lo;7&i7-tE1g1QHD7! zwMrZLbqCmth+5jsnyz1$!2iFUO7Ze8Xb^lMTL z`5PuO~W6yDYT8m3oI-1mT*N)I&sIUE(2eu7xL0%@)T+T2N|4+*vR-y0|o>s(Hp zCRAfV< zM3p}~0Q$Nc;J!{#58u+;qmkq8)&3-@^R}K;K1Xxpe0DETD8Fz}AU{FBCn;rA^bqJ6 zlz?{Bq=qXS!%SKlb}&vBEv?IhT#;lAFQcgjF>fidsO`E@`Zq^A<~9@_C$QQh)49Km zYAaf0X?AU-jhG~pZOOL$5_=r*qgl#umG|T3WJif{)N)3u>|vsqxeC*lxDEx`!2MxY zVWTXwd@dHNNqTu7$7F9ix`wlMo33OT*+=#iSPMoV0o0S5%=#{B3RrvIjqF0DPR{#t zKNfYLuznl;B+fUydFw;Q#mk96yd+D7qO9kpd-(7ch^}ZCFJF_d&WKzISN6Ue5#VsE z_>`vZunWhzGp+tk&fpL;zc@MZXVi=R0c=!m2SB`{87*J{4tty zIjfdPIhTWb{myV2zvuR*p$%Q=%38&}ny+@~^W5=N(@p!OZ+lepbp{3FCAP8mUM){X zwW1Xp>!?588c}-DAx1hcb-`Mz!lZ#tfYWU*wK1d4VGHizWY=vv#zz7J5uYq_xnb_+ zI>6WQ{|RD}oCz;l`IcmKvzyqX*_Wfu-`d@=-F|%dVV%O|tJbaS=HPZoPf^=l<0Sp! z_r#d<+tooULaAWNues_UKB^+;X89honJMXC|A~Kd7?N{uq?aXSdp~j>Hm1)z=^75F zCgplR9kuWG;akTGY=r!fyKa3cT6T=e@=SKL8HPLW!FC(6u#k5uIkH+$JC5d01w;z0i^=%h;U(2PpvdyR zRQ8?{x_Y&U;>kA(1HGq0(x*ADWwfhOP1qTc@6ud8L&WmK-kmk@i_wjy))(r7k7x;Q zHcEJ(dL7_+>+%JryCRSK$@6N<^~idT30*jl&b`YvCJ8xrj*(ABt5Q?T)7Sh z8-!^oj_dA@NQUU-jQm)@8_CWsp{NZy;naW`Z|;|r`#})_mlMb9%|B&SeKdYGTfm~| z+T`A0Fm>H{&tpPZmo5H2Z072NrcOxdzlqD_&BrLK9iAYa;E^qsDE8?r^FNH_-xfLQ zT&6D5%aVZG4_<2B_9ODgv-1tNzd%Nn2r0$9z1jAFg{W@nh15%ewg%?LK(hS%vDeOC z4-LP!an7ERI1p}!_geQkQ3MsK?0Ye)GObp7pzK5FeyA1h)P& zd1~UlCPbPedsL!Y(p*sRsjuUXCQbe;`gs(6iIRw)dPXV+X`HYegaGbJdqd0npXZ+18Yph#?6&pp4$|dI`#2{ z!|=l*QduT0ODP~w*JAHKO&?EmYc?HGdzT@JJBCA3*fVWqy5PNu@4-tEL9ajwy}_a< zUMay<_dR@G#Kb6S40@BTzyr zhj0q5d*$)VBZ=-mwy)$e7!E>f`||(J&EW+6OyIx$@4x$F8~<6e|1s-w!WK1HoOM~F zkQIOj#b8i*p4Pa=B`bhSl$~Z#-?aLukeYH_HP5f`8KUh5rcfg-x}_WtF`?lo1580& zR!f7WC15y7BM_AF1OpUy(fo~I>T_qM^O6Gr0=8PtIuLj5 z+Shq>z3{_%h}k~`sA4DpQQV0qw!7T1YFj}Dq5?-Rxet1p6t`VE}Ak!7cQGIpPf`H-Qq)=(vYk-5DC*4;#Y{NFX8L%#SY&iK0kp{)zsP4+;8OgmBBYY_+} z6$3?`k!aw8w(J5r9@VTzkAO)nXAcYjRU3X%Rbd2R1>z!qPwBww+E#o70$@K+TFQg8 z4WDG<6;rDFDQu!_u;4O0|0+3~b3 z&4rMV@Z2jV_LP2;7uTXM$QetTguLWGAHx6QqKUk5k45CwXw8r&TPCZ;q<&?Uf*|Qv z%wHwl16>YT655ANH%3QlsyxiieP-7Ns;bmFJk6JvMc3X6?FO^)2mvH6SLNNE7NME; zICDd!-PCzu;lEv=>i%LXf4v;MuSk>2Z#-+lAZ=rDg3(r5z&Z9_n5#3+Eg zT??#z?-cJ~vE?K-$J^Kc227eboPy|4e?fz(>8Udp##!td@_57bD;*1g_Ekf=eC+E> zG3_KHyO{Bx{v=!l0AOf>4H3VNf^FbOml{gW=2b1v&!?rgVEXF)PUIr9HEf;E(h4}X z{nP+mBqay?yF*IetK`&1KvZraeWnm7$=PDv2Nhw1cYC2*EXp@2b1 zfw{I)a2*uqwOV=BMcl=J# ze<#=)zuC|iYh&`*-QbEZ9sv>K@4w)h_~;q40&^C{x8DimM%FTo)EoMM{+#Kf>YsP) zZDBUVJW+*f3Woe6axZ+pgEOEgFosl4Uz`C6$NUU3eH|QFhXxFKFP(}X=LKNI*OGAr zsnFpxrGN;qQ&UA|?brxK;T9OC#qTmV2updWDNf#HW8BbIIjhOIJ2ZCCtB~TW?N?4< zh|1?J1v@Q-q>@nhp9;u|I1B7RrwDxhW$?@rgYr z)Ef3Zhxknop@v^N)&zoeL+C_$VSL1S6q#(~TXrehmIXq~eSBtGb>920+w{t8&PwwQG>v23LjKt;zKFLlW18pejMt@AdR1sdMo7vv znc_0c3R>;iTyb8B?5!`;sM=?Pun7&HXR)Sj!MnID{+eTr z3m9a!e?+QRZfMjp(DW;wsgT5! z^{q@I;GHQZ^YmjY6{D=zS^3NRzZ%@;vHmM1A+2B2IiMzix#ZE;$UvGGB*a;!JHO_# z0^Ta*BOHt+B$?Uws53m2tHrMmX{ z?NJS!ilSA^&3p3kk2V$uY{P}m5ubFoO2-IqIi-Km{Ip5@$y1q9FKk(1V z@1LB;QB4+Ad!s#fG;~$QLwHAbvJwF5+tRcBii>a&=v6L{_SZkeEl;mEmgM`lzukXq zc!)0TQflKny3wdD;G1VZH*c`1WsF*>$u)=4p}bQXnJD=mAiKm~&OdIJ_@x`cp_%76 ziQVUOS=Q?YEi=y90@>~#k&h$M6*&SbexsS1qmW>nQ}C2#^4Qt+c$)181EL)F)S8v% zSzD^+wA;0#zNxc4c~mLPu7>f|^2xqA3bg> z2)fQ}ECLMv8+rmGs6E2nU*qATW+q3r&z12K{R#7qcBCrnizh`mJ%Hy0j6Xb|@(QKN zv3e)lW3sG7F7}gME@KRTzk$qqTv(o%Bv=qF^spYlu4eN!{98n$jTd?4N+fqIFY=$& zXqIA0a7|&`%`tGGWhyX~=^$o-;>wz%AkrSWDYlmq-Gpk?btr z&Qo_<$Fpw7N$k%l**aNCVZJ<~2>xz*H|~sUW`L8KnJJA zz_my}k1Q!*Evru#)*efNidc>pUC1f2`4oP6c@xQ9F?AjiCgqD2Te>-@=#-i|CWSP= z-}+>pz}H_dN^1KV@?PCKW?Ze#Z7@mlgv(e47m=Q0mkf^ChqG&~a5)Caau=-_&ymG4 z>PRPuD-FR>eWCql591RWUsry0YdgOt8+Q|L{@cy|b4uuz`I}mGV{A`vn@ zw?dW$kx2}TJ#4pbU-k@W+st>mAz8P8xt>v{i3=kU&pus9KaUdZRvi*BtGtwc`>#WV z?=X9Bhem*yfG9E)Pr;#RSaWT*BmV{wZh|6}&9y}njxlA2zm$SwcA0Q|1>Q>P_{juo zYvgnSY({F&co<|h1Jg;QO7^6-p5b&i$1xF^Xn*DQGUJagbHM+q4GCCX1YiA^-}-Hn zlPqoZHf{V$Zy2XfW^TL1s2@xaw1n_rFMl8L} z%<4#Ftar0|tlS8Twr+2M`1~i~bAMR}#sdOj7E2r^N`&FXVK4S6(TBUf!4{qyrYdF9 z_?g~=PyGK_#GW!l!OF{>Jd%aGcz&M=%kSRgV@a3#do>8*&y#$4Qz{P3JO~8?*)J;# z5D=!A7G)18|9ORs-UPrc+0&hBdeqj>L^#2TV7$1p-`0EikAp6x>{QB~V_EJA5QE_p z-k0-56c=w$m&p8c1tOyB2GzHIgvJmtDzk&HPs3(pvi!412vx*@@_xFIpT+AZ)I^by zczzbLOI++he{P>KiLuyiZAReHs3;E{kt7Wxl9qF9Rzdx_zobjB9=SaK&I*Uw$}v9f zyC+I%{B9Bb5J|D?53)J&=Q7GC3B<`UjG#`NW?JykhKb-O<#SKQO3!Wk?m$uo0ib#? z!28cPGUF%0MhYGTw71+^5^=7GMXeAY$`}2?R2nVP3Kfc zh8ZK8c9H7Vy>Ze{6}mHaE!aDtV$6j!nLpy%pf!QRllq47PICXf5B{t&F*kxy$JM0w z&s+KXGH^H}{H)QB^Z(iu=%=I~;59rIG&}q6&HVleAD^J>fBaWxo~h;!!q0YXHHwzmJ@*{bnbRvVm`P+1*$Y^}tN$m~TJhn`})U4ZxUt~U80bZbn z>Ft3LdsO(5Djg5FGuv!|>dr@BZcUykdug2O0mi^M4AGS8L%AR>TN8?tQ6SbWgSujE zc%S`qf_{I!3_0r@LLL8&f2X6LS>^Qymz|I-lrT@g2ynumNR^Esu4uh@;xh=E2C1%V zjboE5dd)$XUs(W|ct4wP1b!46n@pb9j%<^xHOqzo2v^6F4;{;HPKxFlX7`I{VF#kTTd}C>7 z=+#|%OS$Olc9L8Dg(j%!)~Ix&QY`lCa@C4>oVZt|VX%ko*VGsL-|tFbfnIv~=DRx~ zSi8xliasF1U84I4iHu@{(*sFxXjrN3TWm29qK)9U=&Tpt8b7h@iWTy_a&e;@K_RqEvEQmz@W!^UW>>fP_g4sn+>Q{_l5ZoN0ZUhTH_I#vjGnO`j{=|-YoCYf_L>QQYepOX+6_E=6^Y}S6!x+~==*g>CR zZ;6R#I}#JC;XLMF-ab6(2$Tm(O_<)r>VVAB7^DN~pa}AU-?|t2O^dp%36e6=_BBYD z7i5zoi(Wgw>{f8J1+9Sc@&F<lKuQ^Y|E_PWN!7}+RFb#qzqa;TD6qOjJukAS5R)2~t9+L_*BZtxasSZ8m` z^bkfmc33@rnG*2yp0NBSw?wZEyI7nWk*(7FC{_6LAswG0-NE03_%3Z8HAeZR0 z-lkc=Mh(j%R>lP*V|79_$qg8)drj9M;tWVt9AfYB%Z>uJhYf&idV!ij4)qX-NBeR# zH&ZaNkJ_R+M|dOI#6SUM1Z&=oHU%R4ckKs=52Emq;~3Icj*lLf4yDx#_D5>QDjTXc zZ+{a#672dSyb13M!;wFdb5aTNJRk0?-o98%f$KgM@=a|@pyyGUPwxt5CrO@0E4MqW z4Rw~bV;wY-4uW3I6?8Yd$Tnc}45Y{e4U1uEmGsx({Mk6A&#ZivHUqpakFsX8s$C1k z_C8%{@wkn9@LHH#6>25Pg+MyMW2msn^fwO4_$SZ-?+jr{6zRywKH+&v`{^J`ob`Io zYKdMVCC++GPtZ`jfl+7k8!3bMuqh&--7(6o2Ec>G?wZR0iaY#j_T{Z**{HqTYK>3w zF2V=MOILx5eiqcKm354q^0}hnSM5HzJ`ETOWgvuh$fJ2wKM*GUrIZfY&DaelPxT&iZ@v2a~Q%UoTFb=%1qpJ)CeP_ zVeIEOr%<0tYQ66lC6W*st-)*PX-2XL^^KHU-mAZK&~HE4rahadbz$CMYD9 zE56CUy|%bSpB1`?+ccWNcu{pNhZ~wi0iK_YJw}4FKd^Q z9DQ#1^Gp!sNxPiE9lzoPbj3cgHs_-+1&`nyB_llKduX!G@hj?qD^6CL4{#lS`Vr=sSV6>u6B``hPJjnj+@_h9!f^0WQ@Q7`BIQE=wctf z$Tg@D=5Bj<`R^^MTuE?^vZchj?~6{E?UJJLtys2^fKc=mU&Qw5?@TH5`c=*)GC@=W z{xFhOGC$5ce+3?XsfZTB6sT5mC*)wF)>#3f;hOg}!inFZKJQ=V88&Fxefhe{$K#M8wny zmGw|LvY=@fBkV_zhWOy?KkP9V1u5U}L(;0mxaybbo$?e>yhkcudI6tsf;*|h+4~vC z!EP+)Uw6YO4}Ql&D~;yaG5f;^Ru5!_RV$f7$4-#)Yw+>4ilNM#$3Omm|Lt>wE<0jk z-mQ#MB2O^Ke3m33I|;Vu0v8~9a+iR&P{G;#XiOoN_am6!LZe$@Z#r7K;d_F#V7e`4 z2=E-^%1p70$4|c%fhqNKTQD-T6e6B+Pw6ZmF7s0d;-du+Uf7k|~3jH1deEZp%LNF?*Q+#`hT<{STLq@78 zLSw>*AZ4Z2(pn%7WIGH*trWsA7-0ATK-sqG?N)VY@&S$iI5?^hE?iC62oY z3c^-q_`oni2C$luZ%%Y1a zj^8>OEG8;BVxC^bRCHa9vrDBc&&}7W!qOhdXnzHoO*m5oZm!d%b}jzljJnw7#$0zZ3~S||-B+8r z{R1i$`Rke*;;#1R_bD|Vg{A?*lt^Iu>fCz0_yyd4ihJ(e%P+CfWa3*%bao%kwS+za zqfSO1&fdA0k5Jq6^agS~$w*f~DbEFIwg9Nl^r_|f4?%;S6tZ{aaV=dP-Wz-|$w-Y7 zxfm|nx9DAVU)(KN)?kwn`9x>b4an;Lg~ma~mNf+-ld^68MM7_&!oj?cuq1pP*YWyu zf>>2}HtTn6)LH!!E2PERki15YhuXSw_jpgrH>-47jbZ~>^?XPSgx5(UlhioLzCQe; z1(2b*Ji49P8|MCZc!GF9*vfuI=fZn*4937=L6yQHokXDfY~Rj&5YG=y z*g8Ysg@#`UJZ5AVl|?~NHlba_eDv-?{{95dk2Mhgn633isDLOdsI|TI$b&+UOe8II z@;@KF1ksz0x~;~4j5~s{^~Ud5@ZZ0b)xiy<*cbhSxc%*`TJ4!K7YAA|I$O;MxQ#4M zmpZRb*bjP^QXX69=T8vYgeeM}E4J-nOQ^FV2ZBWFs|z#N%S_39Y&i_x%$)ie{GSIl z^3yt%LfOuqyIwJ*Jtf zuhSp9EP}DuFVnyC*$ZEXg91&|Qq195#6DNwZrY3HR!uXlg&>#5_zQx4A#3lt#S;Ot z+ncUHt6S$?13}tggIfToLsoGtQ{_t4))n!6O#bu8bx1%f;jl<`VIE!6#;oz4pWsY^znNLeQ#Ne3`=25H=^e z(oZ?M#C8@i-n2;dbyQ)(L-}+k^!}=;D4qB?7&iy;Vj%!StIT#}5lk2>10>o(fCNlX z4HcOyQR`Xh0oVl!jw&hvXgl!ZTVt+eclyT2igzEW=nn)mhwSxacg-y@Tn+_P$pqY1 zA6oYpe7h5Y1bDxZil3d8O86e=$4DnttX%;>N4B{=Vq6Bexg4cLaa`$T9yCCa|J_Nx z2t|Uk_ULRvGb?ZTM}3zB`N=P8I4QhKuc{2Y8Mg|WB?bV4*A}=Kr7xv84PCH!UNipP zpa-Jp1C?#J=M<#YIqfMp}QR{6*-Co<#wDOOGX6jS_|3 zDT@&a%$MMskJ8g515N^k$GCxCbKMa|tZ<}qW}ra^2Q+7hQ>&mJ#AngAucsdK&njr@ zl-gJV9Wt+ynT7_)+ug02c*!BJ@G@%TOf(RbY;FpnH3zk}Go;S>{JFif!=)k2#{YS* z0f_5yGdqbEr_F9tI%=T08=}Q|x;){Piw%b_wiUtspd=PRjlAm3^7i6rO%*4AxlIuz zsh|m_D!r(3MNJZuu2!U-sGzRa2j-r%{u-|J@}L#6PtC5U(zSU2qT*dc(^@ZnkBvp0 z6OyK!QC*;*JaT6(d;15V?kx1yL69KCwSo8zaT-p@{oj2lz`>QcIVEN|MlsaGlIOr|iQ zYB&a=zUhx4zpRjtwZgH-@jUS7)UWJm&nFF2r+DJLG>EGqJy1!q|p@hp$rnE?PFkgA2kn{xt6opZ$egP zz(0cHfpyO($u693OE|gk+4`6zO%m|aI+?Oy#_cksKalOTI;%tB$mQ`vf(w3@L~7lE zi{0B`lewJ%7MKWzJ>4nh3^)+G-JQ?q#(2;Gq_v(=T)?|=lm5oXF;Efps6%&s-IhP1sDOtGCfmmw&QzJ zpAb~VF_ue}LT$M%3HqqHvN~G!tNd*Wnqa66I-}IAVs1t=<2JAwSrA~#HJLSclJt@V zD7qEZfR?8@vo_aq?CXX=-~?IEoP5?y?~y~|ZU`twWqLk`m>EF7N+eOuP>i2%nw!<~ zRN))xR+1Q5|DOGT*f_v%F)hZx>TqvX!us=<9n=5V=N@Kw8$zA7W-+-V7w0xqK^o-0 z`SO_`EA8OkbA!^YUb6Rh%x}uaD)H+h0aDlVpbL^9Tt2R6jrJ{q@vb60n-}8->1M8PJMhe&Ot~Ix$P$>uh!HolLRTMkyBZU9`MHGiWZed>XH@<9~>9XOMF{ZFs>}89N`8PBv2K|vqTM-hAm0r`F*8QHn-df zdH7GB6oa_#Q~Pn{z08X<83dm<1eYv173Uruh%1Nm?Z3O2Zil|p-Bg&_&E$lb61nqo zQUI%P^uCmJ2ntc?Y*2gJ)`^+6**Va&24>CL`(eJ7$KA~*T(#3Dbbn41TQ$1<2iZ}+ z1$npx)JKwT`LNc6+|P$ineNT)B_>RDdI`s(0(?R&HER%}r-=2()vaF_kHLu`TD<~a z@Q_{zj4WX~Vq}3i>A4D=dj#p^e|-Kb7lIM(qDuArxW!S90BDPXaPXg+>)*%YQx*7l zR92huKQV>^$n*H;cu4=_Z&$#_ZY-vLM^^~k75{1Y^WN0PTgT2Eo?kTy{P|3LB<%mX zab@l9Ppb4~pbJnJ9H(LC9K*U%W~Brh;8ci5QUR%a0I2<$@R@(f5vT=jpclB*sRAfj z52oRJy<+X5qN4J^Yj>k0O)h2-nCsgtpfhm^HGnnU0Qn4yP%VIrdqL(u<#9LxcHla9 zwwU#x?MUT7x!t45Xd-pF{o^I(fvJ38v^Lomg%uh{}V z(=Dgf45~qP*0OF@#Q^AUv)!Nq9LctDn9G|CWvx)Jr|mAq^TXvq>k$EVp0uO>4{;M_ zO!xd`OB7o*@I-oiNCtH_IrlA~ss+YBF7tXy?0q8(D6;vbqu^oaK6ee0IJN<`jRA{l zhNSfRRxSg(kCb@>xeWB4;(+<1hj3lb_o-1)`w9Sr&$WR1IX;yP;u(5SZb+}cRRa1) z>C=rifyn6NcBdE?f>Fc03F=bFZ374YO4#lD?#CeNDkBY=uTTquPzd@u89?%(I$BH@ z(o9gxdo?1lJa`Ip&^>^-xK%{s%^Zkoa2fq)>owAy*`J+)s5tj5Hg^W4dA6b25JcsO z(f6)sn_*_T=Vk|5Dl3H9O>%k7K3`DVcf64lE<=4TQeSglVt0{m2qdQ?^NxNK$$|9t zG|qIItEAC@-UE{<~77b^A+?^q=Wc6Bd)Ag18%gwqgwv3RHNOLCc5&lfD`~2Xl#Jm#J&h*_Lei1h_=a~v zFU@f+?~NVqC+a>s<8&s20~3>uD@->fr4#N^z%UP5!IGELFyD*FD)M)CUzuM^cA`5VI6=^OW zXiyLC30TvaU)3Qb3}VPPu>u7nfrp>M$1VQlLas9@3;1O^>VlAv_`H)+dMn6~={7~Z z_gtxYpahrSjAZ1)3khO-iqtfYN*`60=DC-=37|pJPv5@fS}CwfAhfEJa7vyqz)7CEH>rvC|c%7rg<|9qH^pUCz_S5!M3vw z({ttV$57$Oj`-~(1;TPn6>m-54laMObH!&&=$a`gMH8%fJF$8Ag}o>wS!%4xqp9Fb!- z%5FRKs0K&T^WEPsm~xoC^DJ$?R~^J6_jfk5&x%MCT+~(=B9qP%l*h4tNvTI%0jn`U=xgOQT?# z^8Qs^g41CcS7>Hh0M~~-Fc?*U^EM}a@d}cR&V`r<=!_p)tv#zbUDPc+2nHXbR%c%L zRsvkd_6^rVOInD#O-*FrHXUV~I7bl4?R|);sM**DHBqFAHL*nEflJR~b!iGpLpeW4 z2ACL(2>ll@jk^0OSJb#GT|N|1jlg)9PW*{KAowF{y<*ikSC@teW+>dyR})(MjxIi! z%_JjY0@D14=?QE1E)=B5H3GueM79iA z28b^?h1u{FK!Z&6M{efw<_cHfiYqtOX{c#-zr=!x=*-mWY}OdY+uB!0x-r2StpEj@ zjn3}_1kd=hqjq;=POt9X)3$(7@RFRwp~ba|MNRu7oRreLurO+h|^#IRMS!GY2t_+|FR`wSMO`buOa<^_4vsfqr$THxe)J zP=d1LW;y@4;Oy_NY7vff46UipDEAFH$mcgh{5RQN%*|5MXXswD&tVg7?UOS14lZl*=}tqLzeH~X9<)pmRS5bQ~I zVLx%edKVy;#(hwiOjI1Fx~jP9n3A0n=l*7$hOF*Hw-e!1vvs@`dj{hbeIcPVlPmm< zJ+}uEp)L#9Ybz=Yxq;95#LDZYJOK&}`>rS#o~pUPU2|C9paCWSJ7mGT(C8kM0&e0B zPZgfri4!U?7+Pr=(7*>*HJfB*;_0#+F2fpI-?BYkuNlTnvYO_4JyY-I*iFsJN!K)& zn|tZ+#BZrlbX#BV7ASTruuL_ikfu8;IMPR15?QW8Odj8uQJq-fRI*0 zytk5ne?m#jm{Kp7|t1)978&ok>qdG%9}h{RyAQ znspj6(`ogdMKzgS#pVZAUEbYKea8zkeOX+CImWvkTPZjubIs~nix+%aH_dY0{AH-G zTUjM!8zgzbG26wSPNeCJU@pQKF)6aA2TASgKfU`RI3N8JlR5~?$ewfh@xB{ycqe#M zz1>YeMAc}hdGk~DdT&xk?A<(Y1ELS|0&yb%iIFGMf2*QWdMl^dcF99%n^C=cp6`83 z!O!_mat(GUPLVxc*M9;S;>+*_gt}kovT4t0tm3Lygr4LMy}jBA*$}mY89saW{2YJ( zYx^I9yiYb!^E=?Yd}FC#+M3Mh*^qmmCs!)e*Y2Bg*X@q1R9;u+8DDEd;-yE}n)W!p z&RR?Y%*tL_GX6rOO+G^g1gq~?f%Isj`d99%xo4uW zPl!)arm}CTj?V@8^NdJjdR(I%*N^-}Z1CDu6x0M-W||w!I^4Y+QYx~sSbj~g9&sL4uuPB8P#-he$32K7W5(&OEx!mlP25EBCeyNLJ~KEG zTT)v#Q81lB%N;ZBmD=FOm!>|H&h4b+#YJF2&hQJpZ}b|x|W{vRFNQ{X2gmni>2j3TMp_C*3_Du zrLXn<_v8r?#jmp4a~X-t{%kPx{7A1kUKGD-Q&(M|Xe-YSxm#T@U@>eii{CUo0H@O? z75k(%HV5scf#g>-6K$N&{;VThR4}I&$o4L0_ae?wTSl_Rd!s2aX~Fy1+dgyGVkO6~IZ0PbGAr_zgLbF6ePTaj*jPJxixYZYbA{iJ=Ze`J z6o&~Dd-`vFo_{!Yin#U6Lb3%#1GKmKk*Slz8FYa`_CPxp6wEcZQ@1of4>O1G1)_jiSI)o0399nAL3BR;xpOox%2 zRlCF2uctmB>NFo|`6_@nwZTIPcd7rB-3|k3Rig07s?#^wx{hpjYj#c$zveZrdudcYH)I-}&dJgkdy)32i{J0haDJXPQ$MJ&KNc9G>F@|P-jG;` zv84MqwZ+2M_nQI8PYw+ZqmKU9WqfT9z!s+l8kD5YLY;d}tu2uo&g?6WLQ zx%fSf+nAgb-gx4hj>fs6&-T2~R#R%^;q@z9E{K?oy%}S zwGwyY&`eBPdYaYi>SeD{vW3&UzEtw!1AhLqah zy>)PE_^;9WdGdQ2IoK6qb;mu+A$|jAd)4~9*TM#|OCV_+g*o%{v*qS0+J1A|dxui3 zPv1473};`<0krrLlkAystMr@{uQZP^$1dKFSgw~?AWem1mTCnP6T;8 z-`tz8t2lxN7Y&L_`9YU3bzH-Q+{JU^x+7zYK{H5mt6J88{IT=t|Ku^-6#ZL^;)!x$ z-?2R=Sp@Wn@uxM^v{g+V9KUKA$|#sb`KdT>09^(bClx-8MD4a>17dqh!c3tlu@{OV zk=i6?LA?5k0^M0`DgFi{cQNa83EHO7o-=X{Z#;1}nk;j~m5#(uaDggCGQBW2{s(`#GPzZ^AtWs;#3Nf@p8FjV&SGb zUTg0wuXA*}MHQ-%4~%93#(QZ6Ct_VCfkuA?BmP8p;@YdPWQGogfFPxO zGd7e2JeYpZMFh1XMG_{fg3E|7K)pRtp{pj>>UG@mR-2i2>ma*GqV<}mX>*NNUfZYC z+MUF&0vWpuov!>f8||a}yk6EPd ztCVq3ec$EgS=CAOd}Q0|o@ix^n#6;Y#bj(nrJ#1~aLjo+HCPY`|SbI zs2v%)4M)_U&WvE>U+>v3od0}xKz=(LHRjOl<{G znIfNpCWba+!J=1x!9+G!m80V+QK|KKe_Yt#wY3z0no)(IDUTDKi0=6!A}9a)m4DE; zq)Y(3$xNG>9z~u2@J5(&>BN6Nl7YDDn6Z4&t;o(!%<&uVW3BcTAKDK2`d*~yWC8C}&R<^|SpYaDUDxEI%Y#|9yUvTZW(otG z5YE(@qs`a-AEL?kd0aLMAh8c++E)4>2AxANHW2`#OaXu?;>0KaAm7T~1h2@XOXL7y z+6aUUH{#2ce=p;GO+>CPsKA-f3fMeOTuQ6D)^q9ar=Mq1KJS+)@c7GHp**!9zyz=K zjZCj}8d&-aaeyc^>rW*SOKIlmSps;8(-x*h`!|#+eLsm;wInNzw>21)=6XQUW{w_I z;&Mz$@BAigN=qVYCNU-{0ATF8BuFgsMEI{ws@wsHL(g09Z<4nsN{ork4}{{55lH+_x*P};^7lkXpUgP>RL zeqK>Z<>HxrTe?3~p5JS<%Au^H@@fo*E$)Z9(5{UK@7uMV{1;r6zQ;JSEr@UR#98$N z=6n$1=7=A|4QtF1TpFQ$IyE7K{7p1&Ip5AiP5`bTmT5hF|IWYP0XU;IB!nH`PTybf za_O;#-A^fE@(G_8S{y$c8pFJDICO{M)TJ$-@ipHLeDPCTm&P4_|B$jHq}=URGcW(5 z3?T!kS`Nz%8Q^sYfLEMC1S9K7$qA48#|MGOMRXDF$wZ{&hEw9Q5wZ`ZPfIPVUwANm zWFaW@KvCD9{H_)36{)ln&(4Y0vxm%icJ-2pK(h(@v&Zm5sD2hN@q{Poho26Jp{X*t z=3Ccl30|SlZTJ_)0zx^DK;20K_~`jwX7$0zE3xOU5pgaByc|68Qk6CFjr}W$?|2-1 z+V_;3$8U}bw_rf@96`VRJc$V>@oSjrKV{fDMpI@XZn}25rGO6i-I!x|J>oM=^@Ngt zm+SJ6PlPECBr`v$k$sR_e&*=JIwh9+r|ThmV}UHfiKh#`E3+my|!%ijF*(n=%~wn(-ppBNzWkz2xb9O|XYejIsgJiP7j za`Rd6M`2^kaBu8~Pvha)L&3Bip(GPQ*U{X~hSy8Zu-UV<8w*6FlTMbzJ-Z!c6ca}5 zA9&>)HdT+7%;uHAvD(g&yc`|du~V4ynQ)%}3VOv?rLAp~m(S!O`%zPNkCOGnw$SWu zR%_aJ?I=N)JXuoD-1|}MHw?>(`^)3|CzZY>{H(v?ig+vv{n=CUJv?>5aNu%tFaJd{ zL(|WOpWnh@Vh*AYeMCQfuG%(#gx(n?%byd9akJ8)pM1%!{7AuJ&F;sP3C%|q^N=HZ zB(e-4j_#W=p_#6so_ASTxON2{hR4g$mTBnT8w$?pAE)C+JT<8&&d|oHv~OYKR703D zU|U%?_$_gN@4zcLVc-4V#1Hi0yd@-d@}|?X=F`33D1!ch|<}1Z}&%2V;SP+!lO!>Lq&$(sO}r$?gUX?*{U=A zn5Ft`l;@1~&PwFAfkEklx%-iIB)&Y(_u5I{88zGHL}%EybHm3G>TsGjT0gwtPUp$p zKU_bRmDpFe z**&#t(6&lecj;K=Daj1qN0DAM=F7CFs1v7mMOGu9n_-L_pSE~6#T~{~nl4XUfm4>^ z{M!FAh!GVeTWbqFM#aPW{Re}v`^@n6U*j8xXCA~Q%<;aTF}?85s)O5m$Xzewx?wiW za~Cib8MNjrKr6^{7hv2^dA*x1i0Hp_86A+*0i z!3kf-H{HSXh^CviMRO4WD!_|eosw2SFVkH+6Z~&e8}MMCh}AVT(C6jBf46V=4qA!r zm&@-Jo|LqC=;CDY@g>#sXL(|cevn&&$Y+4w$@ey(2Sp3wX1?yH_eWn2aIs9+fGf=!ku*K_x!)G3 zG&cD#6g@q1IDnT)1*E7eB_f$;j@>)p$r5yJt(cS^CyPCgM3|;psU@{Ke(!9+U@*g9 zal5@BJhEv=^SkLog-VE$BH*$-T+J2p^tdaxBL@ZCUzzVhu>i>*0=%JIz$RI~;kD!F z0R~HVCP99%{>EGN|7=L)Efk%kD)WaDk2GGam#}c@}_dEw4Cf=m|7w^4K1OR-%1Ih`CS| zc=P&W1;q~}N47RF&u0i*nXX`Vr99T>O@X9I8i}wV7>qjviXe-i5zjuJYVM#5NRvQq zcGjSrY+t3n8Ib@f%6{hS=&y8I7%qSY@|jAmn}JGUFJJ`@fs)BmRz`yTyiCTbC5 zkVZ_{%n`u>utivP{jfyRfF!yE2o~{_@per#TzvC<@x&BJjU264qVQktK*krx`^MW{ z;?*d}r?VWbr5Lt+Y_8R6_O5b;t)wxC_0H`!~~Y7Le# z$$#v0Dccg54rsqxUQrK}s^tT$iqr(huv-&EAQX({x9RUB6`F#BApZ@?oqfqdFypr} zy}Z?b{-6W@#7Rhoc;)V+UpX@sCrU>LVb$&m{|T^pINE|UovX1afI?PF!VGvhCVk|~ zP#L^#YnnS(l8IaxG$nh0>K>}`+1Zv!kn&RI%-41S$5D}Jta686Uxlm!y#dJ{IvGj- z_sg>_2C&nIOu5%(daP|K)3X|6U;!25KOFM|y33SqT63DgHIYbDsP)>uwp)iGXtP=elo6nK^mx&f)LkL>u{lp=(BH$m}CDYR?bQQ+FpT#>dw z_l#ze4PVOK1zh1Pdj2SoG4FLPFYeIdl8~A$s}v`eT>vtCM21ZjI~9G7IKUjh6ZEJ-Z(~% z7Wc2rr1RluOpK`^sH#Wt`*9PH~m&f`Z?e{tBxI|2i5PY&ZLkAIk4>*aIBTE$b z{wq!)C0)Ru9=wye>7}4wgzu6RRuWc>-Mk-zU*s1BYUiK}h<~k-aAX9C%z}I^?wj{f z5Z?a;Xlny{U(COE77IxbuwnJ34*v=TJQ4oj+y0_=H=kbq-(Pm_c8S*i8U;XSQbZ!M z*tNC*LimBc&k2wlnReElUy597`yq`VAQV!}h{y9Xrrby8!2bVWSQi`nPqdIyECFlE z2^daExiqv=DLgd81o3aju6{B&ppLWgP*W9wZR3Sm?%49zM}zQ=0LY0WU<(QWrP-6g z8zh#r*dFKgrMDP!1u&825VbH)vr$t%?M00cD%f_`8he9e4pvm!_J+R}gXBC62x_YU zaoG>VIWFNP;1wHtQv{x-BY~dSWuXsHkOM#@=>xV^E#KS3F>bh04%{F_#&-bp6TK)0 z=GONlv99(VmA@4k{P!8a4kn;~K-13$DEKwt8GM!J1Yp%@m()hbfW6wsIB-(!vJ>oiZ(emQ%C(CcZJP#PN!!BAxoAf#-97C@c0o$oM3!kU<%NvSDsbt<9}>Z61b64H_O}_2rY`N$MUj@Q5RZAq1%^ zF0hWq>8Ir)UAptDMr}8UnpgbrwK+#-n)gM3M|7%?&>^t4p4N1a9~Oi zR1?{em;M9H@t%mZP~S3FZ&q*LDljiUGAn1PhMPGb9N4O$B6x-;LRF;GQ*JmGOItWx z(x<(&tefyEK0`nn@}(FZ>dic})jDYOz!fpKvvY${oAq+3QdDAF#*67i7f@-INq8_a~+U@X!jOab%Z+nQs$|1A0qQ) zUjf_hzO{?sk!T7YYPC4XJA;|={F%NZy|+z-hT-*-i%S8nQ~l9?q2iAa6bt#dx6k547v$8>c=_qa z@;klhcX}IF(~_@2j_m+|%h%BbQ(v`JG%@U!3aJYN>VAXOPNEQfm4k;=z0&~jDa7x$ zOmW*-TUi1*%D~}-Q|^Fti1Q{v*g$14n-ytyb!~YL5-f=Zid86`GCp?N7mp2Df~|#^ z{OXZ+I+yrGe%VISDfd$tMY{LPFwosnMyxRpttqBgWBUNI<1_!csfkmeNwvM{ zxL5G!u6$0V1g!c2Kz8_Y(-%<uL0UbcPjZZ+NuiV#`u@Y)X!fBfRzZ8WJ40A z)sGzd_+t?e7%G!pgPVXdcr1SM@c=?T?o}1+QET+s3$xk3WCj;YE<&<@X7?&+&mW$( zZF_gcYOpon5A+X+RfndgWSg|n3&kZ=> z`wHx_(HQVit66A2IRjSe)rbZ#G4PqoGuZt@XYN+NVngm-P&aB(ESGHB3#=Map4ml! zlbTEu#eo0A5~MRPxzOOFFQov)@0`!4Hts0<;8NEvf*NeoO+%2m zLp1S&n+s;5ulc5OX1U;>t;}`ELk@Is`i3I2Q=-_bX6}Qh3Q&iVrBM_}J)2>*l7%u7S1L(14t+d%q!aoBpx%o9j4 zk!D@Jb9iHtk@v(ohn1!1RiaxziK7Pj)3`TkFE#B78a?wa$8WOh<{2Oln2tUiUjm%) zWb?D9C&0V*gA_%O|Dp1^UpM~f0?vFR(|DIj_Qm^z%&{X5fzvn%xiK!dVMh{XMvnh+ zR+0o_&LJYG(A|~exBZ^inwu{8Xg^cb98GkZh%y!;oLeDJ=idDlXhDRibB^6$k8FZw z4sZ_5%GxbNLmnRltx4nWkY6H1U`hA_aPQvwVvb$~5Szb7AL~gj_}aA!38&0Y&K%b) zrdZD{uml}27WDyxSKsx4X~u*gto;)A@hRZFI4xSYd?)Q@k7JXoCv!{TQVSS}a0v)A zir{q)HTWryGxyTH!Q#w=3{C3aq?#k3xe?Y+BntC^4+isX&aEtxE0FVboXt{KOeSIH zWcM9VddT~8GrGC6lQZ0OzaR%@j(%pQ^xIRTqtx(mNPP&h;Qs4Cz0g~>4kg}BzfP8| zK?gHPSP9Ww?K>BNG2D_LzS%?Wl?_jZ!-32x8IO&JXMyyv7(&+k1|e}n)lg^+ z#JjCbtc`%bcy&y*PdUwGV)dgQquHq8Qr1G!86h3U9qnOB)4D30*Jp zWWFfa75MW`6AebF1=PFwxp<;#0WPvgv97czPt$>3Nxg}GBFUwYu@9+Iy^qoukZ9f3# zSLX=;s2SRtUSz-$G{r&&du_2B>$71s3f?s1@!7Pl<6wAm83?3ZNp*2AJm+~=Bpsk1Xp@U z3fOcBO}?F{v=4O}g%>^in7-`JNPbFxttz(>tG6$X+Z*a4)6#Xkzhfij24&%>7?j8( zufw>mrlPmG(%4;b)RiJwh1jms699wtyCS0+i<8eI^H_?W5HWy&Gv$5O3tb~9J1h#1 zAw}vEe%wYM?78CzHxEX50YoNr9CixsDv!hEJA3X4T;}=n!SYgq)0s(K{T~)fjAW`o zNr9x(09Ddrcve7r5nUu$5vl8Pf+ zMy`@GFru13nlZ79Px~wY)}}N}R#Bo1KpKWsZJ$vK8)WugFZ;MG=%3)#tZU~sdGhf5 zY44-EsZD@zYV|r)N|v*2o+uR3Ur#R7EnmEl^uP5ZWomSy_WEE8cFK}Anet(}FL1)+;FZ^m-cBt@-{e5uV4i6+?$8=P z2)T0pu?m=wbtc0>sl1#-aNFzSy@-`^X2m|ujGYmnM?I$mZ_UZ? z=w;QM;UzCUsN7+ zUtCqxG2=Nj2l~RlPCJu^Z2VKag^9CB8~dEaL##SQ7}I3cO5#<$0$rD%$#jPM;yz{x z$*L<*3v;&>HiPO?@-v$@Cz0qLn>AZCLP{=Q>aB@&P$Fa}hAc@0`neJHxT-dD^%q!W zZ{-}8x+iO0FaKVAo_6ziVR1g}vd;1I6fwzVH=C9wb$(`OdmzC{=s!8)NnW@OSD6u% zZA_aj9pO+?kA6vTYDd)M&RW8GX|M8b88Elo??(`NsTg=gV_wTR?P->s5q+Y9t|wBx zmx?H{Y>`EeFgav8Nz-I7j5rgNTAGoyy9%}`CjVx~F7@FjhFx&0}TCH72NpqNQE5t8OlMEAaog;|EhKffKWS7#-h zcds;qzI-)UBBbz|Y?{gn-)+)IlF16^)ZvF$Kv{wk&D{|l;=vdOkAsz3=E@HJig+KG zg&D?Ln5yDNI5A1TZyITV9%YIgH7hp%WznMh0Nj*ian1}=t~+!f%QW{&d{*#;pb1AT zcMGq&*83z42|iqiL)TVPr9DTBuh{LbS*qXfME=Pwju( zKoK-^`?9o!tXd?nPZ_6dWEW`XZH8|(E6SNrXVtJ8B=fb_yLE!f~`Fon|dUY252dZp#ZZ};8#%M3~b9AGL6tv zXP!LjSX;}`DPe^)dKTF3+)uSl7Lp`Lh|myZy6U2ZiK@K?oDy1=46t)Z+_as$rs zIA@`;ouHX#eAed2(F(Y9o=6Keoq=V1snuN;iQ^6nD4i9ia^+9FzZXk<>|qs{q*Ub8md`^0|LV&!JJ{9=#qoe$K{AdB-<3=a}i?vHNlV{9!9 zp6IUC;i@v2mhfS=WqI9yfPyIp`nN@(dTs-kv}0t%0hurg~J<*oLQ{m*VU98doTph+c{s$eE04 zi)#34FZ50JnSbOu0J?)BDnoPT!Gtm6P|v$Q&Dh(qrA;$GmhzE0vQGp88j1s*m}ertyEzUBC&$Zn?1>_{M+?R2_axgfu( z((jm+wU^i}TxDUbls3k$(tg|$_ki-$SNnRo5Z={ba#D(2m>A32Ky-ItCVI7L z>l*vTdVi&liSojwnjMr55)vg<^5NmsC&uSP-Cw12`5QgZL_^LlkKYopWz!|zB7v+1 zOnt170lVOyM2@1@A=ON}F86z5U@99<&5z~6+CIk=VGN5pa9}3saj>Q%)WB_CC35J2+9=S5dVY0rY0pso(wy!i4T#PW!L5|@WNql5CZxP1ADyPb<^5H z*5D_Q+qML=sWr+@IVXd26u@-}i zXXoU+OD8ULU;e%Bp2pB}$!N2weE#%MV0)IN%{YawRaklc{`8{F0R7Ev-PHt!D*Yj+;Hn3pM$-sVOH?GJIVsznXhG^DBPj_nJaXf z;5K1$ctGg2PbEsi1DMi&3ablIkawA_p!GW>f6ePF&@=Cr5ARKOrHBT6DxdKY1bNj% zAzs2P(@>I_Kr{5-PjkqRzp}?cLD;g#%jr}vmnw>pCu1|2H#pE;s*soSy+ySagNRZF2oZPYJ!$i)-OVrux>K$M7 zy=UlefK6esWgrC<3ft+?0cJ4)30_4!>?Bvt=^S8v(|mhx8RCTHlWpo|jYsw=*$K;U zVVZEHEdoS4Md`FIS^l=6hgv_&eDksyGOxGWp;F+q|@-p%n!U? z{EK^H#kk?-w1VEB<3X06YR?mKKy;`WiRFElS84sCvQY zi}1uz8Gfbp^~Sw_7ylOdqSeV&=W+R#U9hfx`9GH*zylkqgY~-GCTT(u;ei*ktoD&x zD?yR#@=|SIl74O8VeoMXV|fXNtc9dVB!Pe8L-${GF)w0j4Z7>AjT#z$jJ3;^H*Z$* zoSxBRqYfG>AE6Kc+jxDhZY#*Q(bHMhm(eHft zLll61Q|ex%?ldDRE!5srr$1+kbl&M07-OIp&%c3n>ZRd5~SbaUIpP~7MFO@ zi9mCFSZ~zoQji@7!;AK5f9`ofdMAt9jj$ZvIvHA{!(vlH--se`lGMt96)x}CCu`Kj z2Ld>LAMy*8T=>uQeSO=6`|x9F6V-1Q=o`3Z;+RavfN#LedMHTMAhBijBbxzn--|B< zIrsKDpVFHe6Yp9(QTn&=0a5yDZDnETjpJ}aWSu&>k?DrVN@iR%5fZzMn@Mcyn7Z=< zYUW*tx~N`ou=1GQ$pUdrO78xy;6Zb{znxQ$?XfsYeT6iWW+q1l%jQUeRDjJHx$=bC zQk(fi#;g<^%b=q3Jo?Q!#q$Fc-)zZ;_mN#&EQ#YDrOvfSfRm4IKt%KlCjC=*C z0vGkMy>asV>;b#hb!6F~J8nD(jS@0ga3(z6WPkxmc?h>uRcs75v)Za<(c5-^>wg=~|5&?KO#B?wV>e>Hi6SOPrm=)vZ-A$e*~%cNVYN9#S{yj4}96jy9zaf?KZh$AdLis zZ$JI)H<0^23G|x#z1%2USZdL2NoQX7D4#dRp4>(_nJg&o z588Ys&CSj4cRr_VzDo})!rNEy-)2pY%6xCQ5uboOvc+|BiE}M@5TEa)=c1JXBCHD)pB&!5%#ezNnWRNka<|MOK zHuWF&LKwq4w3~XbeJ&})>4iJ0;+DZ!%@>~(K4Y?|@J)R! z1ezCbn#~gkiSP>R81}khGt1{&^oD_=rXHKQ?;S zYX~TGnvC?$=$^X|gOS&GdS%|;*z}1Bs5}f#cU@L5y=%2I8YM}LN&6Nqi!@96w7Key z7d;oWf#yh;-L4h@;GkJm+45E+*#`ejuLIf&H(=6Tm;L5WIPBhZL%}5>GeNmsP^oo( p(|dxNkyL=M+Kr9;`hWV<8xp8W5<$a!={pbjGD4Xf6zRJ~{tx5o{qFz( literal 0 HcmV?d00001 diff --git a/design/pap-14557-monitor-visibility/wireframes/01-issue-top-mobile.svg b/design/pap-14557-monitor-visibility/wireframes/01-issue-top-mobile.svg new file mode 100644 index 0000000000..aeb3f5f851 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/01-issue-top-mobile.svg @@ -0,0 +1,105 @@ + + + + + + + + PAP-14557 + + + + Nightly model comparison sweep + + + In Progress + + + + + + + + + Waiting on monitor — in 2h 12m + Thu Jul 17, 4:08 PM · Attempt 1 + + + Check now + + + + + + + + + + + + + + + Senior Engineer Fable + 15m ago + + + + + + + + + Properties + + + + in 2h 12m + + + + + + + + + + Resumes in 2h 12m + Jul 17, 4:08 PM · Attempt 1 + Check now + + + + + + Reply… + + + + + + + Send + + + + + + + + + 1 + + + + + 2 + + + + + 3 + + + diff --git a/design/pap-14557-monitor-visibility/wireframes/01-issue-top.svg b/design/pap-14557-monitor-visibility/wireframes/01-issue-top.svg new file mode 100644 index 0000000000..cbf09bab1e --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/01-issue-top.svg @@ -0,0 +1,100 @@ + + + + + + + Issues / + PAP-14557 + + + + + + + Nightly model comparison sweep + + + In Progress + + + + + + + + + + Waiting on monitor — resumes in 2h 12m + Thu Jul 17, 4:08 PM (your time) · Attempt 1 · Watching: vercel-deploy + + + Check now + + + + Edit + + + + + + + + + + + + Activity + + + + + + Senior Engineer Fable + 15m ago + + + + + + ⌄ thread continues — see screen 02 for the composer + + + PROPERTIES + Status + In Progress + Priority + Medium + Assignee + + Fable + Approvers + None + + + Monitor + + + + In 2h 12m + Jul 17, 4:08 PM · Attempt 1 + + Watchdog + None + + + + + + + 1 + + + + + 2 + + + diff --git a/design/pap-14557-monitor-visibility/wireframes/02-composer-mobile.svg b/design/pap-14557-monitor-visibility/wireframes/02-composer-mobile.svg new file mode 100644 index 0000000000..895d8be526 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/02-composer-mobile.svg @@ -0,0 +1,74 @@ + + + + + + + + PAP-14557 + + + ⌃ earlier comments + + + + Senior Engineer Fable + 15m ago + + + + Next action (armed): + + + + + + + + Previous · PAP-14544 + Phase 3 — rescore… + + + + + + + + + Resumes in 2h 12m + Thu Jul 17, 4:08 PM · Attempt 1 + + + Check now + + + + + + + Reply… + + + + + + + Agent mode + + + + + Send + + + + + + + + + 1 + + + diff --git a/design/pap-14557-monitor-visibility/wireframes/02-composer.svg b/design/pap-14557-monitor-visibility/wireframes/02-composer.svg new file mode 100644 index 0000000000..cc417d0f53 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/02-composer.svg @@ -0,0 +1,100 @@ + + + + + + + Issues / + PAP-14557 + + + + + PROPERTIES + Status + In Progress + Monitor + + + + In 2h 12m + Jul 17, 4:08 PM · Attempt 1 + + + ⌃ earlier comments + + + + Senior Engineer Fable + 15m ago + + + + Next action (armed): + + + + + + + + + Previous · PAP-14544 + Phase 3 — rescore + gap re-runs… + + + + + + + + + Resumes in 2h 12m + Thu Jul 17, 4:08 PM · Attempt 1 · Watching: vercel-deploy + + + Check now + + + + + + + Reply… + + + + + + + Agent mode + + + + + Senior Engineer Fable + + + + Send + + + + + Sending a reply wakes the agent now — before the scheduled check. + + + + + + + 1 + + + + + 2 + + + diff --git a/design/pap-14557-monitor-visibility/wireframes/03-properties-monitor.svg b/design/pap-14557-monitor-visibility/wireframes/03-properties-monitor.svg new file mode 100644 index 0000000000..bb39e5f302 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/03-properties-monitor.svg @@ -0,0 +1,121 @@ + + + + + Properties pane — Monitor row redesign + Side pane fragment, 352px wide. The same row is reused in the mobile properties sheet. + + + A · Current — cramped, truncated + + + Approvers + None + + Monitor + + + + Ne… + Jul 17, 2026 + Attempt + 1 + + Watchdog + None + + Label truncates to “Ne…”, date is ambiguous (which year matters, + not the time?), attempt badge wraps into two lines. + + + B · Proposed — two-line row + + + Approvers + None + + Monitor + + + + In 2h 12m + Jul 17, 4:08 PM · Attempt 1 + + Watchdog + None + + Relative time leads and is bold; absolute time + attempt get their + own muted line. Nothing truncates at 352px. Row grows to 2 lines. + + + C · Hover / tap — full details + + + + Monitor + + + + In 2h 12m + Jul 17, 4:08 PM · Attempt 1 + + + + + + Monitor + Attempt 1 + + + Next check + Thu, Jul 17, 2026 · 4:08 PM PDT + in 2h 12m + + Watching + vercel-deploy + + Notes + Verify completion, commit ledger / + scores, extend comparison site. + + Last triggered + — not yet triggered + + + + + Check now + + + + Edit + + + + Clear + + + + Hover on desktop, tap on mobile. Exact timestamp with timezone, + service, notes and quick actions — no digging into the edit popover. + + + + + + + 1 + + + + + 2 + + + + + 3 + + + diff --git a/design/pap-14557-monitor-visibility/wireframes/04-states.svg b/design/pap-14557-monitor-visibility/wireframes/04-states.svg new file mode 100644 index 0000000000..105dbc4b84 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/04-states.svg @@ -0,0 +1,103 @@ + + + + + Monitor row — state matrix + One copy system across banner, composer strip and properties row. Relative time always leads. + + + + + SCHEDULED · UNDER 1 HOUR + Monitor + + + + In 24m + Today, 2:20 PM + + + + + + SCHEDULED · UNDER 24 HOURS + Monitor + + + + In 2h 12m + Today, 4:08 PM + + + + + + SCHEDULED · BEYOND TODAY + Monitor + + + + In 3d + Mon Jul 20, 9:00 AM + + + + + + RETRYING · ATTEMPT > 1 + Monitor + + + + In 2h 12m + Today, 4:08 PM · + Attempt 3 + + + + + + DUE NOW + Monitor + + + + Due now + checking momentarily… + + + + + + OVERDUE + Monitor + + + + Overdue by 18m + Jul 17, 1:38 PM · fires on next tick + + + + + + CLEARED + Monitor + Cleared + 2h ago · after attempt 2 + + + + + + NONE + Monitor + None + + + + Copy rules + < 1h → “In 24m” · < 24h → “In 2h 12m” (two units, not “in 2h”) · beyond → “In 3d” + weekday date · past due → “Overdue by …”. + Absolute time says “Today” when it is today; adds the year only when it is not the current year. Banner + strip render only in scheduled / due / overdue states. + diff --git a/design/pap-14557-monitor-visibility/wireframes/flow.svg b/design/pap-14557-monitor-visibility/wireframes/flow.svg new file mode 100644 index 0000000000..3e6d9403c2 --- /dev/null +++ b/design/pap-14557-monitor-visibility/wireframes/flow.svg @@ -0,0 +1,65 @@ + + + + One monitor state, three surfaces — banner (top), strip (composer), row (properties) — all fed by the same copy rules (04). + + + + + + + + + + + + + 01 · Task page top — banner + + + + + + + + + + + + + + + 02 · Above the composer — strip + + + + + + + + + + + + + + + + + + 03 · Properties row + tooltip + + + + + + + + + + + + + 04 · State matrix + copy rules + diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index a23f16d90d..54cdb577dc 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -527,6 +527,8 @@ interface IssueChatThreadProps { verdicts: { id: string; verdict: RequestItemVerdictValue; reason?: string }[], ) => Promise | void; composerRef?: Ref; + /** Optional node rendered inline directly above the sticky composer dock (e.g. the monitor strip). */ + composerAccessory?: ReactNode; issueWorkMode?: IssueWorkMode; /** * Hook for the parent to refetch comments when the user explicitly asks @@ -4249,6 +4251,7 @@ export function IssueChatThread({ onCancelInteraction, onSubmitInteractionVerdicts, composerRef, + composerAccessory, issueWorkMode, onWorkModeChange, onRefreshLatestComments, @@ -5003,6 +5006,12 @@ export function IssueChatThread({ + {showComposer && composerAccessory ? ( +
+ {composerAccessory} +
+ ) : null} + {showComposer ? (
= {}): Issue { - return { - id: "issue-1", - companyId: "company-1", - projectId: null, - projectWorkspaceId: null, - goalId: null, - parentId: null, - title: "Watch deploy", - description: null, - status: "in_progress", - priority: "medium", - assigneeAgentId: "agent-1", - assigneeUserId: null, - responsibleUserId: null, - checkoutRunId: null, - executionRunId: null, - executionAgentNameKey: null, - executionLockedAt: null, - createdByAgentId: null, - createdByUserId: "local-board", - issueNumber: 1, - identifier: "PAP-1", - requestDepth: 0, - billingCode: null, - assigneeAdapterOverrides: null, - executionPolicy: { - mode: "normal", - commentRequired: true, - stages: [], - monitor: { - nextCheckAt: "2026-04-11T12:30:00.000Z", - notes: "Check deployment health", - scheduledBy: "board", - }, - }, - executionState: { - status: "idle", - currentStageId: null, - currentStageIndex: null, - currentStageType: null, - currentParticipant: null, - returnAssignee: null, - reviewRequest: null, - completedStageIds: [], - lastDecisionId: null, - lastDecisionOutcome: null, - monitor: { - status: "scheduled", - nextCheckAt: "2026-04-11T12:30:00.000Z", - lastTriggeredAt: null, - attemptCount: 0, - notes: "Check deployment health", - scheduledBy: "board", - clearedAt: null, - clearReason: null, - }, - }, - monitorNextCheckAt: new Date("2026-04-11T12:30:00.000Z"), - monitorLastTriggeredAt: null, - monitorAttemptCount: 0, - monitorNotes: "Check deployment health", - monitorScheduledBy: "board", - executionWorkspaceId: null, - executionWorkspacePreference: null, - executionWorkspaceSettings: null, - startedAt: null, - completedAt: null, - cancelledAt: null, - hiddenAt: null, - createdAt: new Date("2026-04-11T10:00:00.000Z"), - updatedAt: new Date("2026-04-11T10:00:00.000Z"), - ...overrides, - workMode: overrides.workMode ?? "standard", - }; -} - -describe("IssueMonitorActivityCard", () => { - let container: HTMLDivElement; - - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-04-11T12:00:00.000Z")); - container = document.createElement("div"); - document.body.appendChild(container); - }); - - afterEach(() => { - vi.useRealTimers(); - container.remove(); - }); - - it("renders the scheduled monitor details and check-now action", () => { - const onCheckNow = vi.fn(); - const root = createRoot(container); - - act(() => { - root.render(); - }); - - expect(container.textContent).toContain("Monitor scheduled"); - expect(container.textContent).toContain("Next check"); - expect(container.textContent).toContain("in 30m"); - expect(container.textContent).toContain("Check deployment health"); - - const button = Array.from(container.querySelectorAll("button")).find((candidate) => - candidate.textContent?.includes("Check now"), - ); - expect(button).toBeTruthy(); - - act(() => { - button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - }); - - expect(onCheckNow).toHaveBeenCalledTimes(1); - - act(() => root.unmount()); - }); - - it("does not render external references from monitor metadata", () => { - const root = createRoot(container); - - act(() => { - root.render( - , - ); - }); - - expect(container.textContent).toContain("Deploy provider"); - expect(container.textContent).not.toContain("provider.example"); - expect(container.textContent).not.toContain("token=secret"); - - act(() => root.unmount()); - }); - - it("renders without throwing when monitorNextCheckAt arrives as an ISO string", () => { - const root = createRoot(container); - - act(() => { - root.render( - , - ); - }); - - expect(container.textContent).toContain("Monitor scheduled"); - expect(container.textContent).toContain("Next check"); - expect(container.textContent).toContain("in 30m"); - - act(() => root.unmount()); - }); - - it("renders nothing when the issue has no scheduled monitor", () => { - const root = createRoot(container); - - act(() => { - root.render( - , - ); - }); - - expect(container.textContent).toBe(""); - - act(() => root.unmount()); - }); -}); diff --git a/ui/src/components/IssueMonitorActivityCard.tsx b/ui/src/components/IssueMonitorActivityCard.tsx deleted file mode 100644 index 58764a93ee..0000000000 --- a/ui/src/components/IssueMonitorActivityCard.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { Issue } from "@paperclipai/shared"; -import { Button } from "@/components/ui/button"; -import { formatMonitorOffset } from "@/lib/issue-monitor"; -import { formatDateTime } from "@/lib/utils"; - -function resolveScheduledMonitor(issue: Issue) { - const nextCheckAt = - issue.monitorNextCheckAt ?? - issue.executionPolicy?.monitor?.nextCheckAt ?? - issue.executionState?.monitor?.nextCheckAt ?? - null; - if (!nextCheckAt) return null; - - return { - nextCheckAt, - notes: issue.executionPolicy?.monitor?.notes ?? issue.monitorNotes ?? issue.executionState?.monitor?.notes ?? null, - attemptCount: issue.monitorAttemptCount ?? issue.executionState?.monitor?.attemptCount ?? 0, - serviceName: issue.executionPolicy?.monitor?.serviceName ?? issue.executionState?.monitor?.serviceName ?? null, - }; -} - -interface IssueMonitorActivityCardProps { - issue: Issue; - onCheckNow?: (() => void) | null; - checkingNow?: boolean; -} - -export function IssueMonitorActivityCard({ - issue, - onCheckNow = null, - checkingNow = false, -}: IssueMonitorActivityCardProps) { - const monitor = resolveScheduledMonitor(issue); - if (!monitor) return null; - - return ( -
-
-
-
Monitor scheduled
-
- Next check {formatDateTime(monitor.nextCheckAt)} ({formatMonitorOffset(monitor.nextCheckAt)}) -
- {monitor.notes ? ( -
{monitor.notes}
- ) : null} - {monitor.serviceName ? ( -
- {monitor.serviceName} -
- ) : null} - {monitor.attemptCount > 0 ? ( -
Attempt {monitor.attemptCount}
- ) : null} -
- {onCheckNow ? ( - - ) : null} -
-
- ); -} diff --git a/ui/src/components/IssueMonitorBanner.test.tsx b/ui/src/components/IssueMonitorBanner.test.tsx new file mode 100644 index 0000000000..53c9f0fc7b --- /dev/null +++ b/ui/src/components/IssueMonitorBanner.test.tsx @@ -0,0 +1,196 @@ +// @vitest-environment jsdom + +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; +import type { Issue } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + IssueMonitorBanner, + IssueMonitorComposerStrip, + buildMonitorSurfaceCopy, + hasVisibleMonitorSurface, +} from "./IssueMonitorBanner"; +import type { DerivedMonitorState } from "@/lib/issue-monitor"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +const NOW = new Date("2026-07-17T20:00:00.000Z"); + +function derived(overrides: Partial & { state: DerivedMonitorState["state"] }): DerivedMonitorState { + return { + source: "monitor", + nextCheckAt: null, + attemptCount: 0, + serviceName: null, + ...overrides, + }; +} + +describe("buildMonitorSurfaceCopy", () => { + it("leads with two-unit relative time while scheduled", () => { + const copy = buildMonitorSurfaceCopy( + derived({ + state: "scheduled", + nextCheckAt: new Date(NOW.getTime() + (2 * 60 + 12) * 60_000).toISOString(), + attemptCount: 1, + serviceName: "vercel-deploy", + }), + NOW, + ); + + expect(copy).not.toBeNull(); + expect(copy!.bannerTitle).toBe("Waiting on monitor — resumes in 2h 12m"); + expect(copy!.stripTitle).toBe("Resumes in 2h 12m"); + expect(copy!.tone).toBe("info"); + expect(copy!.bannerMeta).toContain("Attempt 1"); + expect(copy!.bannerMeta).toContain("Watching: vercel-deploy"); + // Absolute time carries the "(your time)" hint on the banner only. + expect(copy!.bannerMeta.some((piece) => piece.includes("(your time)"))).toBe(true); + expect(copy!.stripMeta.some((piece) => piece.includes("(your time)"))).toBe(false); + }); + + it("keeps the retrying attempt count visible", () => { + const copy = buildMonitorSurfaceCopy( + derived({ + state: "retrying", + nextCheckAt: new Date(NOW.getTime() + 90 * 60_000).toISOString(), + attemptCount: 3, + }), + NOW, + ); + expect(copy!.stripTitle).toBe("Resumes in 1h 30m"); + expect(copy!.stripMeta).toContain("Attempt 3"); + }); + + it("uses agent copy for scheduled retries without a monitor", () => { + const copy = buildMonitorSurfaceCopy( + derived({ + state: "retrying", + source: "scheduled-retry", + nextCheckAt: new Date(NOW.getTime() + 90 * 60_000).toISOString(), + attemptCount: 2, + }), + NOW, + ); + + expect(copy!.bannerTitle).toBe("Agent resumes in 1h 30m"); + expect(copy!.stripTitle).toBe("Resumes in 1h 30m"); + }); + + it("switches copy for due-now and overdue states", () => { + const dueNow = buildMonitorSurfaceCopy( + derived({ state: "due-now", nextCheckAt: NOW.toISOString(), attemptCount: 1 }), + NOW, + ); + expect(dueNow!.bannerTitle).toBe("Waiting on monitor — due now"); + expect(dueNow!.stripTitle).toBe("Due now"); + expect(dueNow!.bannerMeta).toContain("Checking momentarily…"); + expect(dueNow!.tone).toBe("info"); + + const overdue = buildMonitorSurfaceCopy( + derived({ + state: "overdue", + nextCheckAt: new Date(NOW.getTime() - 18 * 60_000).toISOString(), + attemptCount: 2, + }), + NOW, + ); + expect(overdue!.bannerTitle).toBe("Waiting on monitor — overdue by 18m"); + expect(overdue!.stripTitle).toBe("Overdue by 18m"); + expect(overdue!.bannerMeta).toContain("Fires on next tick"); + expect(overdue!.tone).toBe("warning"); + }); + + it("hides both surfaces when cleared, none, or without a next check", () => { + expect(buildMonitorSurfaceCopy(derived({ state: "cleared", attemptCount: 2 }), NOW)).toBeNull(); + expect(buildMonitorSurfaceCopy(derived({ state: "none" }), NOW)).toBeNull(); + // A "scheduled" state with no timestamp cannot render an ETA — hide. + expect(buildMonitorSurfaceCopy(derived({ state: "scheduled", nextCheckAt: null }), NOW)).toBeNull(); + }); +}); + +describe("IssueMonitorBanner / IssueMonitorComposerStrip rendering", () => { + let container: HTMLDivElement; + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + vi.useRealTimers(); + container.remove(); + }); + + function issueWithMonitor(nextCheckAt: string | null): Issue { + return { + executionState: nextCheckAt + ? { monitor: { status: "scheduled", nextCheckAt, attemptCount: 1, serviceName: "vercel-deploy" } } + : null, + scheduledRetry: null, + } as unknown as Issue; + } + + it("renders the banner with a working Check now button while waiting", () => { + const onCheckNow = vi.fn(); + expect(hasVisibleMonitorSurface(issueWithMonitor(new Date(NOW.getTime() + 2 * 60 * 60_000).toISOString()))).toBe(true); + const root = createRoot(container); + flushSync(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain("Waiting on monitor — resumes in 2h"); + expect(container.textContent).toContain("Watching: vercel-deploy"); + + const button = Array.from(container.querySelectorAll("button")).find((b) => + b.textContent?.includes("Check now"), + ); + expect(button).toBeTruthy(); + flushSync(() => button?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onCheckNow).toHaveBeenCalledTimes(1); + + flushSync(() => root.unmount()); + }); + + it("hides the banner and strip when there is no monitor", () => { + expect(hasVisibleMonitorSurface(issueWithMonitor(null))).toBe(false); + const root = createRoot(container); + flushSync(() => { + root.render( + <> + + + , + ); + }); + expect(container.textContent).toBe(""); + flushSync(() => root.unmount()); + }); + + it("renders the composer strip with the reply-wakes-agent hint", () => { + const root = createRoot(container); + flushSync(() => { + root.render( + , + ); + }); + + expect(container.querySelector("[data-testid='issue-monitor-composer-strip']")).toBeTruthy(); + expect(container.textContent).toContain("Resumes in 2h"); + expect(container.textContent).toContain("Sending a reply wakes the agent now"); + + flushSync(() => root.unmount()); + }); +}); diff --git a/ui/src/components/IssueMonitorBanner.tsx b/ui/src/components/IssueMonitorBanner.tsx new file mode 100644 index 0000000000..5208a3431e --- /dev/null +++ b/ui/src/components/IssueMonitorBanner.tsx @@ -0,0 +1,211 @@ +import { useMemo } from "react"; +import { Clock } from "lucide-react"; +import type { Issue } from "@paperclipai/shared"; + +import { Button } from "@/components/ui/button"; +import { InlineBanner } from "@/components/InlineBanner"; +import { cn } from "@/lib/utils"; +import { + deriveMonitorState, + formatMonitorAbsolute, + formatMonitorEta, + useMonitorCountdown, + type DerivedMonitorState, + type MonitorDisplayState, +} from "@/lib/issue-monitor"; + +/** Matches the `Date | string` inputs accepted by the issue-monitor helpers. */ +type MonitorDate = Date | string; + +/** + * States in which the waiting-monitor surfaces (top banner + composer strip) + * are shown. `cleared` and `none` hide both surfaces entirely — see + * wireframe 04 (PAP-14557). + */ +const WAITING_STATES: readonly MonitorDisplayState[] = [ + "scheduled", + "retrying", + "due-now", + "overdue", +]; + +export function isWaitingMonitorState(state: MonitorDisplayState): boolean { + return WAITING_STATES.includes(state); +} + +export function hasVisibleMonitorSurface(issue: Issue): boolean { + const derived = deriveMonitorState(issue); + return isWaitingMonitorState(derived.state) && derived.nextCheckAt !== null; +} + +export interface MonitorSurfaceCopy { + /** Prominent lead for the top banner, e.g. "Waiting on monitor — resumes in 2h 12m". */ + bannerTitle: string; + /** Prominent lead for the composer strip, e.g. "Resumes in 2h 12m". */ + stripTitle: string; + /** Muted detail line for the banner (absolute time carries a "(your time)" hint). */ + bannerMeta: string[]; + /** Muted detail line for the composer strip. */ + stripMeta: string[]; + /** `warning` (amber) once overdue, `info` (blue) while still on schedule. */ + tone: "info" | "warning"; +} + +function capitalize(value: string): string { + return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`; +} + +/** + * Pure copy builder shared by the banner and the composer strip so both + * surfaces render one consistent copy system (see wireframe 04). Kept free of + * hooks/`Date.now()` so it is deterministic under test. + */ +export function buildMonitorSurfaceCopy( + derived: DerivedMonitorState, + now: MonitorDate, +): MonitorSurfaceCopy | null { + if (!isWaitingMonitorState(derived.state) || !derived.nextCheckAt) return null; + + const eta = formatMonitorEta(derived.nextCheckAt, now); // "in 2h 12m" | "due now" | "overdue by 18m" + const absolute = formatMonitorAbsolute(derived.nextCheckAt, {}, now); // local time, e.g. "Today, 4:08 PM" + const isScheduledRetryOnly = derived.source === "scheduled-retry"; + + let bannerTitle: string; + let stripTitle: string; + let statusHint: string | null = null; + switch (derived.state) { + case "scheduled": + case "retrying": + bannerTitle = isScheduledRetryOnly ? `Agent resumes ${eta}` : `Waiting on monitor — resumes ${eta}`; + stripTitle = `Resumes ${eta}`; + break; + case "due-now": + bannerTitle = isScheduledRetryOnly ? "Agent retry due now" : "Waiting on monitor — due now"; + stripTitle = "Due now"; + statusHint = "Checking momentarily…"; + break; + case "overdue": + default: + bannerTitle = isScheduledRetryOnly ? `Agent retry ${eta}` : `Waiting on monitor — ${eta}`; + stripTitle = capitalize(eta); + statusHint = "Fires on next tick"; + break; + } + + const attemptLabel = derived.attemptCount >= 1 ? `Attempt ${derived.attemptCount}` : null; + const serviceLabel = derived.serviceName ? `Watching: ${derived.serviceName}` : null; + + const bannerMeta = [statusHint, `${absolute} (your time)`, attemptLabel, serviceLabel].filter( + (piece): piece is string => Boolean(piece), + ); + const stripMeta = [statusHint, absolute, attemptLabel, serviceLabel].filter( + (piece): piece is string => Boolean(piece), + ); + + return { + bannerTitle, + stripTitle, + bannerMeta, + stripMeta, + tone: derived.state === "overdue" ? "warning" : "info", + }; +} + +function useMonitorSurfaceCopy(issue: Issue): MonitorSurfaceCopy | null { + // `nextCheckAt` is stable for a given issue; derive once to seed the ticking + // countdown cadence, then re-derive against the live clock so the surfaces + // roll scheduled → due → overdue on their own. + const nextCheckAt = useMemo(() => deriveMonitorState(issue).nextCheckAt, [issue]); + const now = useMonitorCountdown(nextCheckAt); + return useMemo(() => buildMonitorSurfaceCopy(deriveMonitorState(issue, now), now), [issue, now]); +} + +function CheckNowButton({ + onCheckNow, + checkingNow, +}: { + onCheckNow: () => void; + checkingNow: boolean; +}) { + return ( + + ); +} + +export interface IssueMonitorSurfaceProps { + issue: Issue; + onCheckNow?: (() => void) | null; + checkingNow?: boolean; +} + +/** + * Pinned banner rendered between the issue title and description while a + * monitor is waiting. Replaces the description-area "Monitor scheduled" card + * for the waiting state (PAP-14557 decision 1) — the two never render at once. + */ +export function IssueMonitorBanner({ + issue, + onCheckNow = null, + checkingNow = false, +}: IssueMonitorSurfaceProps) { + const copy = useMonitorSurfaceCopy(issue); + if (!copy) return null; + + return ( + : null} + > + {copy.bannerMeta.join(" · ")} + + ); +} + +/** + * Slim, inline (not sticky) strip anchored directly above the reply composer. + * Mirrors the banner's monitor state and reminds the reader that replying wakes + * the agent early (PAP-14557 decisions 2 + wireframe 02). + */ +export function IssueMonitorComposerStrip({ + issue, + onCheckNow = null, + checkingNow = false, + className, +}: IssueMonitorSurfaceProps & { className?: string }) { + const copy = useMonitorSurfaceCopy(issue); + if (!copy) return null; + + return ( +
+
+
+
+ {onCheckNow ? : null} +
+

+ Sending a reply wakes the agent now — before the scheduled check. +

+
+ ); +} diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index f441a6ecec..4e1f2c46ef 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -1948,6 +1948,7 @@ describe("IssueProperties", () => { }); it("renders monitor controls and clears an existing monitor", async () => { + const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(new Date("2026-04-11T10:00:00.000Z").getTime()); const onUpdate = vi.fn(); const root = renderProperties(container, { issue: createIssue({ @@ -1987,12 +1988,11 @@ describe("IssueProperties", () => { await flush(); expect(container.textContent).toContain("Monitor"); - expect(container.textContent).toContain("Next check"); + expect(container.textContent).toContain("In 2h 30m"); expect(container.querySelector('input[type="datetime-local"]')).toBeNull(); expect(container.querySelector('input[placeholder="What should the agent re-check?"]')).toBeNull(); - const monitorTrigger = Array.from(container.querySelectorAll("button")) - .find((button) => button.textContent?.includes("Next check")); + const monitorTrigger = container.querySelector('[data-testid="monitor-row-trigger"]')?.closest("button"); expect(monitorTrigger).not.toBeUndefined(); await act(async () => { @@ -2025,6 +2025,100 @@ describe("IssueProperties", () => { }); act(() => root.unmount()); + dateNowSpy.mockRestore(); + }); + + it("renders scheduled, retrying, due, overdue, cleared, and empty monitor row states", async () => { + const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(new Date("2026-07-17T13:56:00.000Z").getTime()); + const baseMonitorState = { + status: "scheduled" as const, + nextCheckAt: "2026-07-17T16:08:00.000Z", + lastTriggeredAt: null, + attemptCount: 1, + notes: "Verify deployment", + scheduledBy: "board" as const, + clearedAt: null, + clearReason: null, + }; + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const root = createRoot(container); + const monitorRowText = () => container.querySelector('[data-testid="monitor-row-trigger"]')?.textContent; + const renderMonitor = (issue: Issue) => { + act(() => { + root.render( + + + , + ); + }); + }; + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy({ monitor: { ...baseMonitorState, serviceName: "vercel-deploy" } }), + executionState: createExecutionState({ monitor: baseMonitorState }), + monitorAttemptCount: 1, + })); + await flush(); + expect(monitorRowText()).toContain("In 2h 12m"); + expect(monitorRowText()).toContain("Today, 4:08 PM · Attempt 1"); + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T18:08:00.000Z" } }), + executionState: createExecutionState({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T16:08:00.000Z" } }), + monitorNextCheckAt: new Date("2026-07-17T17:08:00.000Z"), + })); + await flush(); + expect(monitorRowText()).toContain("In 2h 12m"); + expect(monitorRowText()).toContain("Today, 4:08 PM"); + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy({ monitor: { ...baseMonitorState, serviceName: "vercel-deploy" } }), + executionState: createExecutionState({ monitor: { ...baseMonitorState, attemptCount: 3 } }), + monitorAttemptCount: 3, + })); + await flush(); + expect(monitorRowText()).toContain("Attempt 3"); + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T13:56:00.000Z" } }), + executionState: createExecutionState({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T13:56:00.000Z" } }), + })); + await flush(); + expect(monitorRowText()).toContain("Due now"); + expect(monitorRowText()).toContain("checking momentarily…"); + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T13:38:00.000Z" } }), + executionState: createExecutionState({ monitor: { ...baseMonitorState, nextCheckAt: "2026-07-17T13:38:00.000Z" } }), + })); + await flush(); + expect(monitorRowText()).toContain("Overdue by 18m"); + expect(monitorRowText()).toContain("Today, 1:38 PM · fires on next tick"); + + renderMonitor(createIssue({ + executionPolicy: createExecutionPolicy(), + executionState: createExecutionState({ monitor: { + ...baseMonitorState, + status: "cleared", + nextCheckAt: null, + lastTriggeredAt: "2026-07-17T11:56:00.000Z", + attemptCount: 2, + clearedAt: "2026-07-17T12:00:00.000Z", + clearReason: "manual", + } }), + monitorAttemptCount: 2, + monitorLastTriggeredAt: new Date("2026-07-17T11:56:00.000Z"), + })); + await flush(); + expect(monitorRowText()).toContain("Cleared"); + expect(monitorRowText()).toContain("last checked 2h ago · after attempt 2"); + + renderMonitor(createIssue()); + await flush(); + expect(monitorRowText()).toContain("None"); + + act(() => root.unmount()); + dateNowSpy.mockRestore(); }); const watchdogAgent = { diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index 1a524e0648..1da295b16e 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -26,7 +26,14 @@ import { getRecentProjectIds, trackRecentProject } from "../../lib/recent-projec import { orderItemsBySelectedAndRecent } from "../../lib/recent-selections"; import { formatAssigneeUserLabel, formatUserLabel } from "../../lib/assignees"; import { buildExecutionPolicy, stageParticipantValues } from "../../lib/issue-execution-policy"; -import { formatMonitorOffset } from "../../lib/issue-monitor"; +import { + formatMonitorAbsolute, + formatMonitorAbsoluteFull, + formatMonitorEta, + formatMonitorEtaLabel, + formatMonitorOffset, + useMonitorCountdown, +} from "../../lib/issue-monitor"; import { extractProviderIdWithFallback } from "../../lib/model-utils"; import { formatRetryReason } from "../../lib/runRetryState"; import { useRetryNowMutation } from "../../hooks/useRetryNowMutation"; @@ -44,6 +51,7 @@ import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { User, ArrowUpRight, Plus, GitBranch, FolderOpen, HardDrive, Check, Clock, RotateCcw, Loader2, CheckCircle2, ArchiveRestore } from "lucide-react"; import { AgentIcon } from "../AgentIconPicker"; import { InlineEntitySelector, type InlineEntityOption } from "../InlineEntitySelector"; @@ -126,6 +134,8 @@ interface IssuePropertiesProps { externalObjectsLoading?: boolean; externalObjectsError?: boolean; onRetryExternalObjects?: () => void; + onCheckMonitorNow?: () => void; + checkingMonitorNow?: boolean; } const ISSUE_BLOCKER_SEARCH_LIMIT = 50; @@ -142,6 +152,8 @@ export function IssueProperties({ externalObjectsLoading, externalObjectsError, onRetryExternalObjects, + onCheckMonitorNow, + checkingMonitorNow = false, }: IssuePropertiesProps) { const { selectedCompanyId } = useCompany(); const queryClient = useQueryClient(); @@ -176,6 +188,7 @@ export function IssueProperties({ const [approversOpen, setApproversOpen] = useState(false); const [approverSearch, setApproverSearch] = useState(""); const [monitorOpen, setMonitorOpen] = useState(false); + const [monitorDetailsOpen, setMonitorDetailsOpen] = useState(false); const [scheduledRetryOpen, setScheduledRetryOpen] = useState(false); const [labelsOpen, setLabelsOpen] = useState(false); const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false); @@ -990,45 +1003,94 @@ export function IssueProperties({ updateMonitor(null); setMonitorOpen(false); }; - const currentMonitorLabel = (() => { - if (issue.executionPolicy?.monitor?.nextCheckAt) { - return `Next check ${formatDate(new Date(issue.executionPolicy.monitor.nextCheckAt))}`; - } - if (issue.executionState?.monitor?.status === "cleared") { - return "Cleared"; - } - if (issue.monitorLastTriggeredAt) { - return `Last triggered ${timeAgo(issue.monitorLastTriggeredAt)}`; - } - return "None"; - })(); - const monitorNextCheckAt = issue.executionPolicy?.monitor?.nextCheckAt ?? null; + const monitorState = issue.executionState?.monitor ?? null; + const monitorNextCheckAt = monitorState?.nextCheckAt ?? issue.monitorNextCheckAt ?? issue.executionPolicy?.monitor?.nextCheckAt ?? null; + const monitorAttemptCount = issue.monitorAttemptCount ?? monitorState?.attemptCount ?? 0; + const monitorLastTriggeredAt = issue.monitorLastTriggeredAt ?? monitorState?.lastTriggeredAt ?? null; + const monitorServiceName = issue.executionPolicy?.monitor?.serviceName ?? monitorState?.serviceName ?? null; + const monitorNotes = issue.executionPolicy?.monitor?.notes ?? monitorState?.notes ?? null; + const monitorNow = useMonitorCountdown(monitorNextCheckAt); + const monitorRelative = monitorNextCheckAt ? formatMonitorEta(monitorNextCheckAt, monitorNow) : null; + const monitorIsDueNow = monitorRelative === "due now"; + const monitorIsOverdue = Boolean(monitorRelative?.startsWith("overdue by ")); + const monitorPrimary = monitorNextCheckAt + ? formatMonitorEtaLabel(monitorNextCheckAt, monitorNow) + : monitorState?.status === "cleared" + ? "Cleared" + : "None"; + const monitorSecondary = monitorNextCheckAt + ? monitorIsDueNow + ? "checking momentarily…" + : `${formatMonitorAbsolute(monitorNextCheckAt, {}, monitorNow)}${monitorIsOverdue ? " · fires on next tick" : monitorAttemptCount > 0 ? ` · Attempt ${monitorAttemptCount}` : ""}` + : monitorState?.status === "cleared" + ? [ + monitorLastTriggeredAt ? `last checked ${timeAgo(monitorLastTriggeredAt)}` : null, + monitorAttemptCount > 0 ? `after attempt ${monitorAttemptCount}` : null, + ].filter(Boolean).join(" · ") + : null; const monitorTrigger = ( - + + + + setMonitorDetailsOpen(false)} + > {monitorNextCheckAt ? ( - + {monitorNextCheckAt ? ( + event.stopPropagation()} + > +
+ Monitor + {monitorAttemptCount > 0 ? Attempt {monitorAttemptCount} : null} +
+
+
+
Next check
+
{formatMonitorAbsoluteFull(monitorNextCheckAt)}
+
{monitorRelative}
+
+
+
Watching
+
{monitorServiceName ?? "—"}
+
+
+
Notes
+
{monitorNotes ?? "—"}
+
+
+
Last triggered
+
{monitorLastTriggeredAt ? formatMonitorAbsoluteFull(monitorLastTriggeredAt) : "— not yet triggered"}
+
+
+
+ {onCheckMonitorNow ? ( + + ) : null} + + +
+
) : null} -
+ + ); - const monitorAttemptBadge = issue.monitorAttemptCount && issue.monitorAttemptCount > 0 ? ( - - Attempt {issue.monitorAttemptCount} - - ) : null; const scheduledRetry = issue.scheduledRetry ?? null; const retryNow = useRetryNowMutation(issue.id); @@ -2191,7 +2253,6 @@ export function IssueProperties({ triggerContent={monitorTrigger} triggerClassName="min-w-0 max-w-full" popoverClassName={cn("max-w-full", inline ? "w-full" : "w-80 sm:w-(--sz-32rem)")} - extra={monitorAttemptBadge} > {monitorContent} diff --git a/ui/src/lib/issue-monitor.test.tsx b/ui/src/lib/issue-monitor.test.tsx new file mode 100644 index 0000000000..84b7ced040 --- /dev/null +++ b/ui/src/lib/issue-monitor.test.tsx @@ -0,0 +1,206 @@ +// @vitest-environment jsdom + +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + deriveMonitorState, + formatMonitorAbsolute, + formatMonitorAbsoluteFull, + formatMonitorEta, + formatMonitorEtaLabel, + formatMonitorOffset, + useMonitorCountdown, +} from "./issue-monitor"; + +describe("monitor time formatting", () => { + const now = new Date("2026-07-17T19:56:00.000Z"); + + it.each([ + [45_000, "in 45s"], + [5 * 60_000, "in 5m"], + [2 * 60 * 60_000, "in 2h"], + [(2 * 60 + 12) * 60_000, "in 2h 12m"], + [(2 * 60 + 59) * 60_000, "in 2h 59m"], + [(3 * 24 + 4) * 60 * 60_000, "in 3d 4h"], + [3 * 24 * 60 * 60_000, "in 3d"], + ])("formats future offset %i with up to two non-zero units", (offsetMs, expected) => { + expect(formatMonitorEta(new Date(now.getTime() + offsetMs), now)).toBe(expected); + }); + + it("uses due-now grace before switching to overdue copy", () => { + expect(formatMonitorEta(now, now)).toBe("due now"); + expect(formatMonitorEta(new Date(now.getTime() - 59_999), now)).toBe("due now"); + expect(formatMonitorEta(new Date(now.getTime() - 60_000), now)).toBe("overdue by 1m"); + expect(formatMonitorEta(new Date(now.getTime() - 12 * 60_000), now)).toBe("overdue by 12m"); + }); + + it("formats sentence-case ETA labels without slicing prefixes", () => { + expect(formatMonitorEtaLabel(new Date(now.getTime() + (2 * 60 + 12) * 60_000), now)).toBe("In 2h 12m"); + expect(formatMonitorEtaLabel(now, now)).toBe("Due now"); + expect(formatMonitorEtaLabel(new Date(now.getTime() - 18 * 60_000), now)).toBe("Overdue by 18m"); + }); + + it("uses the injectable Date.now clock for scheduled retry offsets", () => { + const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now.getTime()); + expect(formatMonitorOffset(new Date(now.getTime() + 15 * 60_000))).toBe("in 15m"); + expect(formatMonitorOffset(new Date(now.getTime() + 10_000))).toBe("now"); + expect(formatMonitorOffset(now)).toBe("now"); + dateNowSpy.mockRestore(); + }); + + it("formats the full local timestamp with weekday, year and zone", () => { + const timestamp = "2026-07-17T21:08:00.000Z"; + const options = { locale: "en-US", timeZone: "America/Chicago" } as const; + + expect(formatMonitorAbsoluteFull(timestamp, options)).toBe("Friday, July 17, 2026, 4:08:00 PM CDT"); + }); + + describe("formatMonitorAbsolute compact copy (wireframe 04)", () => { + const options = { locale: "en-US", timeZone: "America/Chicago" } as const; + // 2026-07-17T21:08:00Z is 4:08 PM in America/Chicago. + const timestamp = "2026-07-17T21:08:00.000Z"; + + it("says Today when the check lands on the reference day", () => { + const now = "2026-07-17T14:00:00.000Z"; // 9:00 AM Chicago, same day + expect(formatMonitorAbsolute(timestamp, options, now)).toBe("Today, 4:08 PM"); + }); + + it("compares the day in the display time zone, not UTC", () => { + // 2026-07-18T04:08:00Z is still 11:08 PM on Jul 17 in Chicago. + const lateNight = "2026-07-18T04:08:00.000Z"; + const now = "2026-07-17T14:00:00.000Z"; // 9:00 AM Chicago, Jul 17 + expect(formatMonitorAbsolute(lateNight, options, now)).toBe("Today, 11:08 PM"); + }); + + it("prefixes the weekday beyond today within the current year", () => { + const later = "2026-07-20T14:00:00.000Z"; // 9:00 AM Chicago, Mon Jul 20 + const now = "2026-07-17T14:00:00.000Z"; + expect(formatMonitorAbsolute(later, options, now)).toBe("Mon Jul 20, 9:00 AM"); + }); + + it("adds the year only when it differs from the reference year", () => { + const nextYear = "2027-01-04T15:00:00.000Z"; // 9:00 AM Chicago, Mon Jan 4 2027 + const now = "2026-07-17T14:00:00.000Z"; + expect(formatMonitorAbsolute(nextYear, options, now)).toBe("Mon Jan 4, 2027, 9:00 AM"); + }); + }); +}); + +describe("deriveMonitorState", () => { + const now = new Date("2026-07-17T20:00:00.000Z"); + + it("derives scheduled and retrying states with monitor details", () => { + expect( + deriveMonitorState( + { + executionPolicy: { monitor: { nextCheckAt: "2026-07-17T22:12:00.000Z", serviceName: "API" } }, + executionState: { + monitor: { + status: "scheduled", + nextCheckAt: "2026-07-17T22:12:00.000Z", + attemptCount: 1, + serviceName: "API", + }, + }, + }, + now, + ), + ).toEqual({ + state: "scheduled", + source: "monitor", + nextCheckAt: "2026-07-17T22:12:00.000Z", + attemptCount: 1, + serviceName: "API", + }); + + expect( + deriveMonitorState( + { + executionState: { + monitor: { + status: "scheduled", + nextCheckAt: "2026-07-17T22:12:00.000Z", + attemptCount: 3, + serviceName: "deploy health", + }, + }, + }, + now, + ).state, + ).toBe("retrying"); + }); + + it("derives due-now and overdue at the grace boundary", () => { + const issue = (nextCheckAt: string) => ({ + executionState: { monitor: { status: "scheduled" as const, nextCheckAt, attemptCount: 1 } }, + }); + + expect(deriveMonitorState(issue("2026-07-17T19:59:00.001Z"), now).state).toBe("due-now"); + expect(deriveMonitorState(issue("2026-07-17T19:59:00.000Z"), now).state).toBe("overdue"); + }); + + it("derives cleared, none, and scheduled retry states", () => { + expect( + deriveMonitorState({ executionState: { monitor: { status: "cleared", attemptCount: 2 } } }, now), + ).toMatchObject({ state: "cleared", attemptCount: 2 }); + expect(deriveMonitorState({}, now)).toEqual({ + state: "none", + source: "none", + nextCheckAt: null, + attemptCount: 0, + serviceName: null, + }); + expect( + deriveMonitorState( + { + monitorAttemptCount: 0, + scheduledRetry: { + status: "scheduled_retry", + scheduledRetryAt: "2026-07-17T20:05:00.000Z", + scheduledRetryAttempt: 2, + }, + }, + now, + ), + ).toMatchObject({ state: "retrying", source: "scheduled-retry", attemptCount: 2 }); + }); +}); + +describe("useMonitorCountdown", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-17T20:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("ticks every 30 seconds normally, every second near due, and cleans up", () => { + const container = document.createElement("div"); + const root = createRoot(container); + const observed: number[] = []; + + function Probe({ nextCheckAt }: { nextCheckAt: string | null }) { + observed.push(useMonitorCountdown(nextCheckAt).getTime()); + return null; + } + + flushSync(() => root.render()); + expect(vi.getTimerCount()).toBe(1); + + flushSync(() => vi.advanceTimersByTime(30_000)); + expect(observed.at(-1)).toBe(new Date("2026-07-17T20:00:30.000Z").getTime()); + + flushSync(() => root.render()); + flushSync(() => vi.advanceTimersByTime(1_000)); + expect(observed.at(-1)).toBe(new Date("2026-07-17T20:00:31.000Z").getTime()); + + flushSync(() => root.render()); + expect(vi.getTimerCount()).toBe(0); + + flushSync(() => root.unmount()); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/ui/src/lib/issue-monitor.ts b/ui/src/lib/issue-monitor.ts index 3b0f8bde40..adc6f2a476 100644 --- a/ui/src/lib/issue-monitor.ts +++ b/ui/src/lib/issue-monitor.ts @@ -1,12 +1,266 @@ -export function formatMonitorOffset(nextCheckAt: Date | string): string { - const deltaMs = new Date(nextCheckAt).getTime() - Date.now(); - const absMinutes = Math.round(Math.abs(deltaMs) / 60_000); - if (absMinutes <= 0) return "now"; - if (absMinutes < 60) return deltaMs >= 0 ? `in ${absMinutes}m` : `${absMinutes}m ago`; +import { useEffect, useState } from "react"; - const absHours = Math.round(absMinutes / 60); - if (absHours < 24) return deltaMs >= 0 ? `in ${absHours}h` : `${absHours}h ago`; +const SECOND_MS = 1_000; +const MINUTE_MS = 60 * SECOND_MS; +const HOUR_MS = 60 * MINUTE_MS; +const DAY_MS = 24 * HOUR_MS; +const DUE_NOW_GRACE_MS = MINUTE_MS; - const absDays = Math.round(absHours / 24); - return deltaMs >= 0 ? `in ${absDays}d` : `${absDays}d ago`; +type MonitorDate = Date | string; + +type MonitorDetails = { + nextCheckAt?: MonitorDate | null; + attemptCount?: number | null; + serviceName?: string | null; + status?: "scheduled" | "triggered" | "cleared" | null; +}; + +type MonitorPolicy = { + nextCheckAt?: MonitorDate | null; + serviceName?: string | null; +}; + +type ScheduledRetry = { + status?: "scheduled_retry" | "queued" | "running" | "cancelled" | null; + scheduledRetryAt?: MonitorDate | null; + scheduledRetryAttempt?: number | null; +}; + +export interface MonitorIssueLike { + executionState?: { monitor?: MonitorDetails | null } | null; + executionPolicy?: { monitor?: MonitorPolicy | null } | null; + monitorNextCheckAt?: MonitorDate | null; + monitorAttemptCount?: number | null; + scheduledRetry?: ScheduledRetry | null; +} + +export type MonitorDisplayState = + | "scheduled" + | "retrying" + | "due-now" + | "overdue" + | "cleared" + | "none"; + +export interface DerivedMonitorState { + state: MonitorDisplayState; + source: "monitor" | "scheduled-retry" | "none"; + nextCheckAt: MonitorDate | null; + attemptCount: number; + serviceName: string | null; +} + +export interface MonitorDateTimeFormatOptions { + locale?: Intl.LocalesArgument; + timeZone?: string; +} + +function toTimestamp(value: MonitorDate): number { + const timestamp = new Date(value).getTime(); + if (Number.isNaN(timestamp)) throw new RangeError("Invalid monitor date"); + return timestamp; +} + +function formatDuration(durationMs: number): string { + if (durationMs < MINUTE_MS) { + return `${Math.max(1, Math.ceil(durationMs / SECOND_MS))}s`; + } + if (durationMs < HOUR_MS) { + return `${Math.floor(durationMs / MINUTE_MS)}m`; + } + if (durationMs < DAY_MS) { + const hours = Math.floor(durationMs / HOUR_MS); + const minutes = Math.floor((durationMs % HOUR_MS) / MINUTE_MS); + return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; + } + + const days = Math.floor(durationMs / DAY_MS); + const hours = Math.floor((durationMs % DAY_MS) / HOUR_MS); + return hours > 0 ? `${days}d ${hours}h` : `${days}d`; +} + +export function formatMonitorEta(nextCheckAt: MonitorDate, now: MonitorDate = new Date()): string { + const deltaMs = toTimestamp(nextCheckAt) - toTimestamp(now); + if (deltaMs > 0) return `in ${formatDuration(deltaMs)}`; + if (deltaMs > -DUE_NOW_GRACE_MS) return "due now"; + return `overdue by ${formatDuration(Math.abs(deltaMs))}`; +} + +export function formatMonitorEtaLabel(nextCheckAt: MonitorDate, now: MonitorDate = new Date()): string { + const eta = formatMonitorEta(nextCheckAt, now); + return `${eta.charAt(0).toUpperCase()}${eta.slice(1)}`; +} + +function zonedYmd( + date: Date, + locale: Intl.LocalesArgument, + timeZone: string | undefined, +): { year: string; month: string; day: string } { + const parts = new Intl.DateTimeFormat(locale, { + year: "numeric", + month: "numeric", + day: "numeric", + timeZone, + }).formatToParts(date); + const pick = (type: Intl.DateTimeFormatPartTypes) => + parts.find((part) => part.type === type)?.value ?? ""; + return { year: pick("year"), month: pick("month"), day: pick("day") }; +} + +/** + * Compact absolute time for the monitor surfaces (wireframe 04). Renders + * `Today, 8:16 PM` when the check lands on the reference day, otherwise prefixes + * the weekday (`Mon Jul 20, 9:00 AM`) and only adds the year when it differs from + * the reference year (`Mon Jul 20, 2027, 9:00 AM`). Day/year comparisons are made + * in the display time zone so "Today" matches what the user sees. + */ +export function formatMonitorAbsolute( + nextCheckAt: MonitorDate, + options: MonitorDateTimeFormatOptions = {}, + now: MonitorDate = new Date(), +): string { + const target = new Date(toTimestamp(nextCheckAt)); + const reference = new Date(toTimestamp(now)); + const targetYmd = zonedYmd(target, options.locale, options.timeZone); + const referenceYmd = zonedYmd(reference, options.locale, options.timeZone); + + const time = new Intl.DateTimeFormat(options.locale, { + hour: "numeric", + minute: "2-digit", + timeZone: options.timeZone, + }).format(target); + + const isToday = + targetYmd.year === referenceYmd.year && + targetYmd.month === referenceYmd.month && + targetYmd.day === referenceYmd.day; + if (isToday) return `Today, ${time}`; + + const weekday = new Intl.DateTimeFormat(options.locale, { + weekday: "short", + timeZone: options.timeZone, + }).format(target); + const date = new Intl.DateTimeFormat(options.locale, { + month: "short", + day: "numeric", + year: targetYmd.year === referenceYmd.year ? undefined : "numeric", + timeZone: options.timeZone, + }).format(target); + + return `${weekday} ${date}, ${time}`; +} + +export function formatMonitorAbsoluteFull( + nextCheckAt: MonitorDate, + options: MonitorDateTimeFormatOptions = {}, +): string { + const date = new Date(toTimestamp(nextCheckAt)); + const datePart = new Intl.DateTimeFormat(options.locale, { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + timeZone: options.timeZone, + }).format(date); + const timePart = new Intl.DateTimeFormat(options.locale, { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + timeZone: options.timeZone, + }).format(date); + return `${datePart}, ${timePart}`; +} + +export function deriveMonitorState(issue: MonitorIssueLike, now: MonitorDate = new Date()): DerivedMonitorState { + const runtimeMonitor = issue.executionState?.monitor ?? null; + const policyMonitor = issue.executionPolicy?.monitor ?? null; + const scheduledRetry = issue.scheduledRetry ?? null; + const retryIsActive = + scheduledRetry?.status === "scheduled_retry" || + scheduledRetry?.status === "queued" || + scheduledRetry?.status === "running"; + const nextCheckAt = + runtimeMonitor?.nextCheckAt ?? + issue.monitorNextCheckAt ?? + policyMonitor?.nextCheckAt ?? + (retryIsActive ? scheduledRetry?.scheduledRetryAt : null) ?? + null; + const hasMonitor = runtimeMonitor !== null || policyMonitor !== null || issue.monitorNextCheckAt != null; + const source = hasMonitor ? "monitor" : retryIsActive ? "scheduled-retry" : "none"; + const attemptCount = + runtimeMonitor?.attemptCount ?? + (hasMonitor ? issue.monitorAttemptCount : null) ?? + (retryIsActive ? scheduledRetry?.scheduledRetryAttempt : null) ?? + 0; + const serviceName = runtimeMonitor?.serviceName ?? policyMonitor?.serviceName ?? null; + + if (runtimeMonitor?.status === "cleared") { + return { state: "cleared", source, nextCheckAt, attemptCount, serviceName }; + } + + if (!hasMonitor && !retryIsActive) { + return { state: "none", source, nextCheckAt: null, attemptCount: 0, serviceName: null }; + } + if (!nextCheckAt) { + return { state: retryIsActive || attemptCount > 1 ? "retrying" : "scheduled", source, nextCheckAt, attemptCount, serviceName }; + } + + const deltaMs = toTimestamp(nextCheckAt) - toTimestamp(now); + if (deltaMs <= -DUE_NOW_GRACE_MS) { + return { state: "overdue", source, nextCheckAt, attemptCount, serviceName }; + } + if (deltaMs <= 0) { + return { state: "due-now", source, nextCheckAt, attemptCount, serviceName }; + } + return { + state: retryIsActive || attemptCount > 1 ? "retrying" : "scheduled", + source, + nextCheckAt, + attemptCount, + serviceName, + }; +} + +function countdownCadence(nextCheckAt: MonitorDate): number { + const deltaMs = toTimestamp(nextCheckAt) - Date.now(); + return deltaMs > -DUE_NOW_GRACE_MS && deltaMs < DUE_NOW_GRACE_MS ? SECOND_MS : 30 * SECOND_MS; +} + +export function useMonitorCountdown(nextCheckAt: MonitorDate | null | undefined): Date { + const [now, setNow] = useState(() => new Date(Date.now())); + const nextCheckTimestamp = nextCheckAt == null ? null : toTimestamp(nextCheckAt); + + useEffect(() => { + setNow(new Date(Date.now())); + if (nextCheckTimestamp === null) return; + + let timeoutId: ReturnType | null = null; + let cancelled = false; + const scheduleNextTick = () => { + timeoutId = setTimeout(() => { + if (cancelled) return; + setNow(new Date(Date.now())); + scheduleNextTick(); + }, countdownCadence(new Date(nextCheckTimestamp))); + }; + + scheduleNextTick(); + return () => { + cancelled = true; + if (timeoutId !== null) clearTimeout(timeoutId); + }; + }, [nextCheckTimestamp]); + + return now; +} + +export function formatMonitorOffset(nextCheckAt: MonitorDate): string { + const now = new Date(Date.now()); + const deltaMs = toTimestamp(nextCheckAt) - now.getTime(); + if (Math.round(Math.abs(deltaMs) / MINUTE_MS) === 0) return "now"; + const eta = formatMonitorEta(nextCheckAt, now); + if (eta === "due now") return "now"; + if (eta.startsWith("overdue by ")) return `${eta.slice("overdue by ".length)} ago`; + return eta; } diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 897e6a0c59..38d8a1982f 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -102,7 +102,11 @@ import { IssuesList } from "../components/IssuesList"; import { AgentIcon } from "../components/AgentIconPicker"; import { IssueReferenceActivitySummary } from "../components/IssueReferenceActivitySummary"; import { IssueRelatedWorkPanel } from "../components/IssueRelatedWorkPanel"; -import { IssueMonitorActivityCard } from "../components/IssueMonitorActivityCard"; +import { + IssueMonitorBanner, + IssueMonitorComposerStrip, + hasVisibleMonitorSurface, +} from "../components/IssueMonitorBanner"; import { IssueScheduledRetryCard } from "../components/IssueScheduledRetryCard"; import { IssueProperties } from "../components/IssueProperties"; import { PauseAffectsSummaryView } from "../components/interrupt-handoff/InterruptHandoffViews"; @@ -883,6 +887,8 @@ type IssueDetailChatTabProps = { onRefreshLatestComments: () => Promise | void; onWorkModeChange?: (workMode: IssueWorkMode) => Promise | void; composerRef: Ref; + /** Optional node rendered inline directly above the reply composer (e.g. the monitor strip). */ + composerAccessory?: ReactNode; footer?: ReactNode; feedbackVotes?: FeedbackVote[]; feedbackDataSharingPreference: "allowed" | "not_allowed" | "prompt"; @@ -970,6 +976,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ onRefreshLatestComments, onWorkModeChange, composerRef, + composerAccessory, footer, feedbackVotes, feedbackDataSharingPreference, @@ -1162,6 +1169,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ ) : null} ; pendingApprovalAction: { approvalId: string; action: "approve" | "reject" } | null; onApprovalAction: (approvalId: string, action: "approve" | "reject") => void; - onCheckMonitorNow: () => void; - checkingMonitorNow: boolean; handoffFocusSignal?: number; externalReferences?: MarkdownExternalReferenceMap; }; @@ -1275,8 +1281,6 @@ function IssueDetailActivityTab({ userProfileMap, pendingApprovalAction, onApprovalAction, - onCheckMonitorNow, - checkingMonitorNow, handoffFocusSignal = 0, externalReferences, }: IssueDetailActivityTabProps) { @@ -1507,11 +1511,7 @@ function IssueDetailActivityTab({
)} - + {/* Waiting-monitor state now lives in the pinned top banner (IssueMonitorBanner) — PAP-14557 decision 1. */} ); } @@ -3174,6 +3174,8 @@ export function IssueDetail() { externalObjectsLoading={externalObjectsState.isEnabled ? externalObjectsState.isLoading : undefined} externalObjectsError={externalObjectsState.isEnabled ? externalObjectsState.isError : undefined} onRetryExternalObjects={externalObjectsState.isEnabled ? externalObjectsState.refetch : undefined} + onCheckMonitorNow={() => checkIssueMonitorNow.mutate()} + checkingMonitorNow={checkIssueMonitorNow.isPending} /> ); return () => closePanel(); @@ -3186,6 +3188,8 @@ export function IssueDetail() { panelChildIssues, panelIssue, resolvedHasActiveRun, + checkIssueMonitorNow.isPending, + checkIssueMonitorNow.mutate, externalObjectsState.isEnabled, externalObjectsState.groups, externalObjectsState.isLoading, @@ -4478,6 +4482,12 @@ export function IssueDetail() { className="text-xl font-bold" /> + checkIssueMonitorNow.mutate()} + checkingNow={checkIssueMonitorNow.isPending} + /> + updateIssue.mutateAsync({ description })} @@ -4753,6 +4763,15 @@ export function IssueDetail() { onLoadOlderComments={loadOlderComments} onRefreshLatestComments={refetchLatestComments} composerRef={commentComposerRef} + composerAccessory={ + hasVisibleMonitorSurface(issue) ? ( + checkIssueMonitorNow.mutate()} + checkingNow={checkIssueMonitorNow.isPending} + /> + ) : null + } footer={ siblingNavigation ? ( { approvalDecision.mutate({ approvalId, action }); }} - onCheckMonitorNow={() => checkIssueMonitorNow.mutate()} - checkingMonitorNow={checkIssueMonitorNow.isPending} externalReferences={externalObjectsState.isEnabled ? externalObjectsState.markdownReferences : undefined} /> ) : null} @@ -5035,6 +5052,8 @@ export function IssueDetail() { externalObjectsLoading={externalObjectsState.isEnabled ? externalObjectsState.isLoading : undefined} externalObjectsError={externalObjectsState.isEnabled ? externalObjectsState.isError : undefined} onRetryExternalObjects={externalObjectsState.isEnabled ? externalObjectsState.refetch : undefined} + onCheckMonitorNow={() => checkIssueMonitorNow.mutate()} + checkingMonitorNow={checkIssueMonitorNow.isPending} /> diff --git a/ui/storybook/stories/monitor-surfaces.stories.tsx b/ui/storybook/stories/monitor-surfaces.stories.tsx index a8af38f5e8..7fbfc66c98 100644 --- a/ui/storybook/stories/monitor-surfaces.stories.tsx +++ b/ui/storybook/stories/monitor-surfaces.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Issue } from "@paperclipai/shared"; -import { IssueMonitorActivityCard } from "@/components/IssueMonitorActivityCard"; +import { IssueMonitorBanner, IssueMonitorComposerStrip } from "@/components/IssueMonitorBanner"; import { IssueProperties } from "@/components/IssueProperties"; import { storybookExecutionWorkspaces, @@ -38,24 +38,6 @@ const monitoredIssue: Issue = { }, }; -const longerWaitIssue: Issue = { - ...baseIssue, - monitorNextCheckAt: inTwoHours(), - monitorNotes: null, - monitorAttemptCount: 0, - executionPolicy: { - ...(baseIssue.executionPolicy ?? { mode: "normal", commentRequired: true, stages: [] }), - monitor: { - nextCheckAt: inTwoHours().toISOString(), - notes: null, - kind: null, - scheduledBy: "assignee", - serviceName: null, - externalRef: null, - }, - }, -}; - const triggeredIssue: Issue = { ...baseIssue, monitorNextCheckAt: null, @@ -123,39 +105,150 @@ const clearedIssue: Issue = { }, }; +const executionStateStub = baseIssue.executionState ?? { + status: "pending" as const, + currentStageId: null, + currentStageIndex: null, + currentStageType: null, + currentParticipant: null, + returnAssignee: null, + reviewRequest: null, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, +}; + +type StoryMonitor = NonNullable["monitor"]>; + +function withMonitor(monitor: StoryMonitor | null): Issue { + return { + ...baseIssue, + monitorNextCheckAt: monitor?.nextCheckAt ? new Date(monitor.nextCheckAt) : null, + monitorAttemptCount: monitor?.attemptCount ?? 0, + executionState: { ...executionStateStub, monitor }, + scheduledRetry: null, + }; +} + +const scheduledMonitorIssue = withMonitor({ + status: "scheduled", + nextCheckAt: inTwoHours().toISOString(), + lastTriggeredAt: null, + attemptCount: 1, + notes: null, + scheduledBy: "assignee", + kind: "external_service", + serviceName: "vercel-deploy", + externalRef: null, + timeoutAt: null, + maxAttempts: null, + recoveryPolicy: null, + clearedAt: null, + clearReason: null, +}); + +const retryingMonitorIssue = withMonitor({ + status: "scheduled", + nextCheckAt: new Date(Date.now() + 90 * 60_000).toISOString(), + lastTriggeredAt: new Date(Date.now() - 5 * 60_000).toISOString(), + attemptCount: 3, + notes: null, + scheduledBy: "assignee", + kind: "external_service", + serviceName: "deploy health", + externalRef: null, + timeoutAt: null, + maxAttempts: null, + recoveryPolicy: null, + clearedAt: null, + clearReason: null, +}); + +const dueNowMonitorIssue = withMonitor({ + status: "scheduled", + nextCheckAt: new Date(Date.now() - 10_000).toISOString(), + lastTriggeredAt: null, + attemptCount: 1, + notes: null, + scheduledBy: "assignee", + kind: null, + serviceName: "vercel-deploy", + externalRef: null, + timeoutAt: null, + maxAttempts: null, + recoveryPolicy: null, + clearedAt: null, + clearReason: null, +}); + +const overdueMonitorIssue = withMonitor({ + status: "scheduled", + nextCheckAt: new Date(Date.now() - 18 * 60_000).toISOString(), + lastTriggeredAt: null, + attemptCount: 2, + notes: null, + scheduledBy: "assignee", + kind: null, + serviceName: "vercel-deploy", + externalRef: null, + timeoutAt: null, + maxAttempts: null, + recoveryPolicy: null, + clearedAt: null, + clearReason: null, +}); + function MonitorSurfaceStories() { return (
-
+
- IssueMonitorActivityCard - external service (Greptile) + Pinned top banner — scheduled (2h wait, external service)
- undefined} checkingNow={false} /> -
-
- IssueMonitorActivityCard - generic 2h wait, no service metadata + Pinned top banner — retrying (attempt 3)
- undefined} - checkingNow={false} - /> -
+ undefined} /> -
- IssueMonitorActivityCard - returns null when no monitor is set + Pinned top banner — due now +
+ undefined} /> + +
+ Pinned top banner — overdue (warning tone) +
+ undefined} /> + +
+ Pinned top banner — cleared / none (renders nothing)
- (intentionally renders nothing for issues without a scheduled monitor) + (banner + strip hide entirely for cleared / no-monitor issues)
- undefined} /> + undefined} /> + undefined} /> +
+ +
+
+ Composer strip — scheduled (inline, above the reply composer) +
+ undefined} + /> + +
+ Composer strip — overdue +
+ undefined} />
@@ -222,7 +315,7 @@ const meta = { docs: { description: { component: - "Surfaces the IssueMonitorActivityCard and IssueProperties Monitor row in scheduled / not-scheduled / external-service variants for UX review.", + "Surfaces the pinned monitor banner, composer strip, and IssueProperties Monitor row across the wireframe-04 states (scheduled / retrying / due / overdue / cleared) for UX review.", }, }, },