Sidebar mission control for Claude Code: what the main agent and every subagent is doing and why, and how much context arrives from memory

Claude Code mods of the Singularity setup. One so far: mission-control.
A sidebar for Claude Code with three tabs:
▶ marks the agent whose transcript is open beside the tab;run_in_background, ctrl+b, a command moved to the background by its timeout), every foreground command past 5 s, and every Monitor. Background subagents stay on Агенты;epoch/step N/M, [N/M], a bare percent. With no scale in the output it shows the output's size, and flags output silent for two minutes;mb search / mb code), prompt cache, turns, context and limits, vault health. This was the standalone mod mem-meter before.| Command | Does |
|---|---|
/mc | open the Агенты tab |
/mc close | close it |
/mc clear | drop ended agents and the feed of earlier turns |
/jobs | open the Процессы tab |
/jobs close · clear | close it · drop ended jobs |
/mem | open the Память tab |
/mem close · hide · show · quiet · reset | close the tab · hide/show the band above the prompt · soft alerts on/off · reset search and turn counters |
Tool for the model: mcp__mission-control__snapshot, the whole state as JSON (agents, feed, jobs, memory figures, pane status).
Слой для сессии, где лид (Opus) делегирует исполнителям. Живёт в hooks/lead/, версия мода 0.4.0. Правила рубрики (кому что отдавать, как писать бриф) — в скиле team-lead движка Singularity; мод считает, ограждает и сообщает лиду, но не решает за него.
Что считается. Каждый запрос исполнителя пересчитывается в OE (Opus-эквивалент: токены × цена модели / цена входа Opus 5.5; кеш-запись × 1,25). Модель, которой нет в таблице цен (claude-test, новый релиз), считается по цене Fable и помечается ?. Копится по агенту, по прогону workflow и по сессии; запросы лида идут только в сессию. На карточке во вкладке «Агенты» строка «модель: ярус · effort · N OE» (и лимит в enforce), в шапке — бюджет сессии и режим.
Режимы (budgetMode в настройках мода):
observe (по умолчанию) — числа только считаются и попадают в сводку. Ни один запрос и ни один спавн не отклоняется из-за чисел.enforce — исполнитель, у которого исчерпан бюджет агента, прогона или сессии, останавливается до следующего запроса (идущий запрос не обрывается: превысить лимит может только он), новый спавн отклоняется. Запросы лида не блокируются никогда: он должен суметь спросить владельца.Лимиты: agentBudgetOE, runBudgetOE, sessionBudgetOE; пусто или 0 — без лимита. Исключение: fableCap (вызовов Fable за сессию, по умолчанию 3) — 0 запрещает Fable, неверное значение даёт 3.
Что действует в обоих режимах (не зависит от чисел):
advisor и только со строкой ПОЧЕМУ FABLE: (с текстом) в начале строки промпта; сверх fableCap — отказ. Спавн advisor без модели считается спавном Fable (в его определении model: fable). В workflow Fable всегда переписывается на opus. Форк и возобновление не переписываются (движок их модель не читает); форк отмечается в сводке «форк · модель лида».COUNCIL MISSION, PLANNING MISSION или SRE MISSION в начале строки допустим только в ходе, который открыл владелец командой council, planning-council или observation-council (окно переживает конец хода главного и ходы по уведомлениям фоновых членов: оно закрывается следующим собственным промптом владельца — терминал или мост, — который не council-команда, и только если ни один член совета не работает; промпты других источников его не закрывают); внутри окна модель Fable или не указанная становится opus. Маркер в середине строки (цитата в прозе) не считается./mc halt): новые спавны отклоняются, запросы исполнителей не уходят — в любом режиме.Любой сбой ограждения пропускает спавн или запрос как есть и один раз пишет строку в сводку.
Команды (в /mc):
| Команда | Делает |
|---|---|
/mc budget | таблица исполнителей, расход и лимиты |
/mc budget +N | добавить N OE к лимиту сессии (N — целое больше нуля; без заданного sessionBudgetOE добавлять не к чему) |
/mc halt | заморозить исполнителей; лиду уйдёт «владелец остановил исполнителей» (снять — /mc resume, лиду уйдёт «владелец снял заморозку») |
/mc resume | снять заморозку |
/mc solo | в ближайшей сводке лид получит «делай сам, не делегируй» |
Инструмент team (mcp__mission-control__team, только чтение): таблица «агент · ярус · effort · задача · ходы · OE[/лимит] · состояние», строка бюджета сессии и Fable, затем «новости:» — то, что сводка ещё не доставила (инструмент их не сбрасывает), и «флаги:».
Сводка лиду. Только изменения с прошлой сводки: запущен (запущен: задача · ярус · effort; ярус или effort, которых ещё не знаем, — «—», а effort при запуске обычно «—»: его узнают по первому запросу исполнителя; спавн не в фоне получает хвост · не в фоне; агент workflow им не помечается — к нему background движка не относится), закончил (закончил: задача · исход · N OE, исход — готов, ошибка или прерван, N — OE этого прогона, как в журнале недели; по одной строке на прогон, возобновлённый исполнитель закончит ещё раз), напарники (teammates) и конец хода самого лида в сводку не входят, как и в журнал; нет вестей, долгий вызов, 80 % и исчерпание бюджета, отказ, переписанная модель, вызов встроенного советчика, команды владельца. Потолок 600 символов. Однотипные строки сливаются: несколько «запущен» — в одну запущены: N (не в фоне: K) — задача (ярус, effort); …, несколько «закончил» — в одну закончили: N (сумма OE) — задача (исход, OE); … (одна строка остаётся в своём виде; тот же пункт, названный дважды, — «×N»; строка режется как остальные), одинаковые строки вообще сливаются в «×N». Из 20 хранимых строк при переполнении первыми уходят самые старые «запущен»/«закончил», тревоги — в последнюю очередь. Сводка обрезается по границе строки с «…и ещё N». Приходит как контекст после пакета инструментов главного и в контекст следующего промпта владельца; слэш-команда сводку не забирает (она остаётся лиду).
Сторож и пробуждение.
hooks/lead/config.ts), не настройки, до данных недели наблюдения. Короткое молчание (нет вывода модели) движок лечит сам перезапуском агента — мод это не дублирует.$.prompt.submit одной строкой тревоги: не чаще раза на тревогу агента и не чаще раза в 10 минут на сессию. Тревога, пришедшаяся на занятого лида или на паузу между пробуждениями, не теряется: пробуждение придёт позже, как только будет можно.TaskStop, SendMessage с вопросом или ждать.Журнал недели. Конец каждого исполнителя (не лида и не бездействующего тиммейта) добавляет одну строку JSON в lead-journal.jsonl: ts, type, tier, effort, turns, oe, ms, outcome (type — subagentType, tier — haiku/sonnet/opus/fable или ?, turns — шаги модели, oe — собственный расход, ms — время работы; все три — за последний прогон (от спавна или от возобновления), поэтому возобновлённый исполнитель даёт вторую строку только со своим новым расходом, и сумма строк равна итогу строки агента; outcome — done/failed/aborted). Каталог: CLAUDE_PLUGIN_DATA, если задан; иначе <CLAUDE_CONFIG_DIR или ~/.claude>/plugins/data/mission-control/. У $.fs движка нет добавления и есть предел 4 МиБ на чтение и запись, поэтому строка дописывается чтением и записью файла целиком, а ротация — при 3 МиБ: файл становится lead-journal.1.jsonl (хранится один предыдущий). Ошибка записи гасится и ход не ломает; одновременные концы выстраиваются в очередь.
Проверка качества за неделю (спека §7) делается вручную, не кодом мода: из журнала выбрать около 20 реальных делегированных задач, один вызов Opus по рубрике оценивает каждую (выполнено ли по критерию готовности · пришлось ли лиду переделывать · лишние ли вызовы; оценка 0–1 и pass/fail). Вместе с OE по ярусам из того же журнала это основание для чисел enforce.
Настройки движка (~/.claude/settings.json, часть 0 спеки) — мод их не дублирует, потому что движок применяет их раньше хука и надёжнее:
| Ключ | Значение | Зачем |
|---|---|---|
env.CLAUDE_CODE_SUBAGENT_MODEL | sonnet | спавн без модели (ни вызов, ни скрипт, ни frontmatter) идёт на Sonnet, а не на модель лида |
env.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS | 8 | предел одновременных субагентов |
env.CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH | 2 | исполнитель может позвать помощника, помощник — нет |
workflowSizeGuideline | small | workflow меньше 5 агентов, если задача не требует иного |
advisorModel | не задавать | иначе советчик уходит с Fable |
env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE, env.CLAUDE_CODE_EFFORT_LEVEL | не задавать | иначе выбор лида (и ограждение мода) игнорируется |
env читается при старте процесса: проверять в сессии, запущенной после правки. Если сессия принудительно задаёт одну модель всем субагентам, она старше хука.
Встроенный советчик. Наблюдение 2026-10-18: в запросе каждого субагента есть серверный инструмент advisor_20260301 на claude-opus-5-5 (отложенная загрузка) даже при незаданном advisorModel. Мод его не может ни запретить, ни ограничить fableCap — это не спавн, agent.spawn не вызывается. Мод только сообщает: вызов такого инструмента попадает в сводку строкой «советчик вызван». Отключить инструмент можно только настройкой движка, если такая найдётся.
Известные пределы.
advisor могут превысить fableCap на число одновременно идущих: счётчик читается при решении и растёт после.enforce без чисел равен observe.The look is an option, theme, in the plugin's userConfig. Set it in /config → mission-control → «Оформление». A change there reloads the mod, and the open tabs reopen under the new titles.
| Value | Look |
|---|---|
classic (default) | the dark palette and labels above |
warhammer | Warhammer 40k, a cogitator's amber data-slate:<br>• palette: amber phosphor on scorched black glass, brass bezels, scarlet for failure;<br>• each element a panel of its own: the aquila crest (a detailed picture where the terminal draws pictures, Ghostty and kitty; block glyphs elsewhere), an instrument block (a Mechanicus cog that turns while agents work, or on Обряды while a rite runs; the litany; an inverse status strip; the Machine Spirit's or the furnace's state with a lamp; a crawl of binaric cant; the Imperial date, «0 789 026.M3»), a purity seal hanging off Магистр's card, main in a double frame ╔═╡ ✠ МАГИСТР ╞═╗, each subagent in a heavy frame on its branch of the tree, the vox-log of calls, each rite a card with a gauge ▕████▒▒▒▏, each archive section a reliquary with a stamp, the commands as key caps;<br>• the tabs are «✠ ЛЕГИОН», «⌬ ОБРЯДЫ», «✎ АРХИВЫ»; main is «Магистр»; labels «обет», «во имя», «донесение»; jobs «Ковка», «Освящение», «Дозор»;<br>• the transcript too (terminal only): your prompt in an amber frame ┫ ✠ ВОЛЯ МАГИСТРА ┣, each reply block in a brass frame ╡ ✠ ДОНЕСЕНИЕ ╞ with ✠ before headings, ⁜ before list items and ‡ before quotes (code and tables untouched), each tool call a rite in a thin frame with its kind and state, the spinner in the cogitator's words, «☩ Обряд свершён · 12с» with a litany at the end of a turn |
A theme changes only what the mod writes itself: colours, glyphs, frames, headings and labels. What an agent or a command said (task, reason, report, output) is shown as it came; in the transcript a reply's marks are added to the drawing, never to the stored message. A slate's frames line up only in the terminal's one-width cells, so the desktop app draws the plain layout in the theme's palette. The engine's own system lines (a plugin's «reloaded», the «※ recap») have no render site a plugin can hook: the colour theme below colours them, nothing more. Every theme glyph is one narrow cell with no emoji presentation; tests/theme.test.ts holds that.
A new theme is one more entry in hooks/lib/theme.ts (a Theme: palette, titles, banner, rule, tree, icons, words, and a slate for the framed look or null for the plain one) and its name in plugin.json → userConfig.theme.options.
Two more pieces finish the warhammer look, outside the mod's own drawing:
| Piece | Where | Does |
|---|---|---|
| Claude Code colour theme «Когитатор» | mission-control/themes/cogitator.json, shipped with the plugin; its id is custom:mission-control:cogitator | the colours of everything the engine draws itself: text, the prompt's border, permission and question dialogs, toasts, diffs, the background of your messages |
| Ghostty data-slate | terminal/ghostty/: cogitator.conf (palette, padding), cogitator.glsl (the shader), cogitator-live.glsl (the living screen, opt-in), aquila.png (from tools/make-aquila.py) | amber glass, scanlines and a phosphor glow, and around the glass a brass housing drawn in the window's padding: bevel, gilt trim, rivets, a cross medallion in each corner, a Mechanicus hazard band. With --animated, a refresh band rolls down the glass and the phosphor breathes (Ghostty then redraws all the time). Ghostty has no per-window profile, so it applies to every Ghostty window |
| Ranks, heresy, the thought for the day | the mod, warhammer only | agents are called by their Imperial ranks (ℑ Инквизитор, ★ Комиссар, ⌬ Техножрец, ⌖ Скаут…, the type kept after it); a failed rite reads «‡ ЕРЕСЬ ОБНАРУЖЕНА» in a heavy scarlet frame, a refused one «⊘ ОТВЕРГНУТО МАГИСТРОМ», a cut one «⊗ ОБРЯД ПРЕРВАН»; under Легион's instruments, a thought for the day (72 of the mod's own, one a day) |
| The warp's taint | the mod and the status line | from 80% of the context: the Archives' dial turns scarlet under «‡ ВАРП-ПОРЧА · ДУХ ПЕРЕГРУЖЕН», Легион's Machine Spirit reads «ПЕРЕГРУЖЕН» and its binaric cant breaks into noise, the status line's ДУХ МАШИНЫ plate turns scarlet: time to compact |
| The awakening | the band above the prompt | for 4.5 s after the session starts, the cogitator's boot: «+++ ИНИЦИАЦИЯ ДУХА МАШИНЫ +++», a gauge, the log's steps, «✠ ОМНИССИЯ БЛАГОСЛОВЛЯЕТ ✠» |
| The herald | the band above the prompt, while the conversation is empty | the slate's own card under the engine's logo (which has no render site a mod can restyle): the aquila (a picture, or a block mosaic), then «✠ КОГИТАТОР ИМПЕРИУМА ✠» in a double frame: the version, the model, the project and its branch (read from .git/HEAD, no git process), the vault's alarms, the quota, the commands, the Imperial date. The awakening's gauge plays on its strip; once the first prompt is sent the band goes back to Архивы. A short band keeps the card and leaves the aquila out |
| Sounds | mission-control/sounds/*.wav, synthesized by tools/make-sounds.py (this project's own) | the awakening's arpeggio, a vox chirp when a main turn ends, a servo's whir when a long rite begins, a choir's chord when it is done, a klaxon when it fails. The sound option (/config → «Звук когитатора»): off, quiet (default), full. macOS plays them (afplay); Linux and Windows terminals have no player and stay silent |
| Status line | terminal/claude/statusline.py (Python 3.9+, no dependencies) | the two lines under the prompt as an instrument strip: each section opens on a brass plate (the model on the amber one; ДУХ МАШИНЫ, the context; ЛИМИТЫ, the plan's windows with reset times; ДЕСЯТИНА, the cost), its figures follow in amber, gauges are solid two-tone bars; amber at rest, orange to watch, scarlet to act. The band above the prompt follows the same manner (АРХИВЫ, ‡ ТРЕВОГИ). The hint line under it speaks the cogitator's words too («правки дозволены», «shift+tab — сменить», «esc — прервать обряд») |
| Pixel art | terminal/art/aquila-crest.png, embedded in hooks/lib/art.ts by tools/make-art.py | the mod's own aquila on the Agents tab: drawn here in the emblem's manner, not a copy of any artwork |
| A crest you choose | the aquilaImage option: a PNG path | any emblem picture instead of the mod's own. node tools/install.mjs --aquila <url or file> cuts the emblem out of the picture (a gold or amber emblem on a dark ground: its own pixels kept, the ground made transparent, padded to 3 : 1; Python 3 with Pillow) into terminal/art/aquila-custom.png and points the option at it. That file is git-ignored on purpose: someone else's artwork stays on your machine and is never committed; run the same command on each machine. --aquila none goes back to the mod's own |
| A servo-skull you choose | the servoSkull option: a folder of frames | a servo-skull on Обряды in place of the cog, hovering, swaying a little, its optics pulsing, whenever the tab is on screen (pictures only: Ghostty, kitty; elsewhere the cog). node tools/install.mjs --servo-skull <url or file> cuts the picture out (imgqa cutout with its ML venv, or the rembg CLI: python3 -m pip install "rembg[cpu,cli]"), animates it into terminal/art/servo-skull/frame-00.png … (git-ignored, like the crest) and sets the option. --servo-skull none brings the cog back |
| A purity seal you choose | the puritySeal option: a PNG | a purity seal hanging in the top left corner of Архивы, as tall as every block down to the vault (Либрариум) and as wide as its 5 : 24 picture asks, at most a third of the column; the blocks stand beside it, laid out to the room it leaves (pictures only). node tools/install.mjs --purity-seal <url or file> cuts it out as for the servo-skull and pads it to 5 : 24 into terminal/art/purity-seal.png (git-ignored). --purity-seal none takes it down |
| Ranks' portraits you choose | the rankPortraits option: a folder of <rank>.png | a portrait hung on the tree's branch before each agent's card of that rank on Легион, 6 x 3 cells (pictures only); the ranks are inquisitor, techpriest, commissar, arbitrator, chronicler, scout, strategist, augur, guardsman, magos, servitor, and magister for main. node tools/install.mjs --portrait <rank>=<url or file> (repeatable) cuts each out and pads it square into terminal/art/portraits/ (git-ignored); <rank>=none takes one down; a rank with none keeps its plain card |
node tools/install.mjs puts all three in place (see Install).
How Процессы sees a job (probed live 2026-10-14; the engine has no list of background shells for a plugin):
<tmp>/claude-<uid>/<cwd slug>/<session id>/tasks/<id>.output. The folder is found once per session by the session id under the temp roots, or learned from the first background launch's result;[exited with code N] line the engine appends to a background output, the task's notification (prompt.submit, origin task-notification), a TaskStop, a monitor's deadline, or the call's own return for a foreground command;tail -c above 1 MB).On any machine, from wherever the repo is cloned (Node 18+):
node tools/install.mjs # the mod, its warhammer theme, the «Когитатор» colours, the Ghostty slate
node tools/install.mjs --animated # also the living screen: a rolling refresh band, breathing phosphor
node tools/install.mjs --static # back to the still screen (the default; a run with neither keeps the last choice)
node tools/install.mjs --aquila <url or file> # the Agents tab's crest from a picture you choose (see below)
node tools/install.mjs --servo-skull <url or file> # a hovering servo-skull on Обряды in place of the cog (see below)
node tools/install.mjs --purity-seal <url or file> # a purity seal hanging in the top left corner of Архивы (see below)
node tools/install.mjs --portrait inquisitor=<url or file> # a rank's portrait on Легион (repeatable; see below)
node tools/install.mjs --no-ghostty # no Ghostty block (Windows: Ghostty is skipped by itself)
node tools/install.mjs --no-wezterm # no WezTerm block (--no-windows-terminal: no Windows Terminal fragment)
node tools/install.mjs --dry-run # say what would change, write nothing
node tools/install.mjs --uninstall # take the look off; the mod stays loaded, in classic
It backs up each file it touches as <file>.bak-<date> and is safe to run again after a move or a pull. It writes:
~/.claude/settings.json (or $CLAUDE_CONFIG_DIR): env.CLAUDE_CODE_PLUGIN_DIRS gains <repo>/mission-control (other folders kept, an older clone of the mod dropped), pluginConfigs.mission-control.options.theme = "warhammer", theme = "custom:mission-control:cogitator", statusLine = terminal/claude/statusline.py (the theme and the status line before are kept in ~/.claude/singularity-mods.install.json for --uninstall);~/.config/ghostty/config, or $XDG_CONFIG_HOME, or the macOS Application Supporthooks/register.tsx 1096 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Alerts, ClaudeMdFile, Job, JevEntry, JevStatus, TurnRow } from '../types'
5import { JOBS_PANE, registerJobs } from './jobs/hooks'
6import {
7 FULL_READ_MAX,
8 TAIL_BYTES,
9 applyDeadlines,
10 applyOutput,
11 clearJobs,
12 freshOutput,
13 markAnnounced,
14 needsPoll,
15 toAnnounce,
16} from './jobs/model'
17import type { OutputRead } from './jobs/model'
18import { JEV_PANE, registerJev } from './jev/hooks'
19import { FULL_READ_MAX as JEV_FULL_READ_MAX, POLL_MS as JEV_MS, SHOWN as JEV_SHOWN, journalPathOf, newest, parseJournal, parseStatus, previousOf, summarize, vaultOf } from './jev/model'
20import { MEMORY_PANE, registerMemory } from './memory/hooks'
21import { BOOT_MS, markBoot } from './lib/boot'
22import { cogCells } from './lib/cog'
23import { COG, SKULL, cogAngle, cogPalette, skullFrame } from './lib/frame'
24import { halfBlockCells } from './lib/pixmap'
25import { drawsPictures, loadPixmap, refusePictures, saysAlt } from './lib/terminal'
26import { AQUILA_PNG } from './lib/art'
27import { countDoctor } from './lib/parse'
28import { themeOf } from './lib/theme'
29import type { Theme } from './lib/theme'
30import { AGENTS_PANE, registerMission } from './mission/hooks'
31import { leadConfigOf } from './lead/config'
32import { takeDigest } from './lead/digest'
33import { appendJournal, journalLine, runOe } from './lead/journal'
34import { EMPTY_LEAD, pushNews } from './lead/ledger'
35import { endedLine, rowLabel } from './lead/news'
36import { BUDGET_BAD, BUDGET_NO_LIMIT, PHASE_WORDS, TEAM_TOOL, budgetIncrement, haltLead, raiseBudget, resumeLead, sessionLimit, soloLead, teamReport, teamTable } from './lead/team'
37import { watchStep } from './lead/watch'
38import { applyComplete, applyList, clearEnded, isWorking, needsReconcile, needsTick } from './mission/model'
39import type { ListedAgent } from './mission/model'
40import { registerChrome } from './chrome/hooks'
41
42// The module's entry and its lifecycle. The engine takes one unmatched hook per
43// event per module and never follows `$` into an imported function, so everything
44// that spans both features or runs on a timer lives here: session.start,
45// turn.complete, the commands, the snapshot tool, the vault alerts, the panes.
46// memory/ and mission/ register their own observers and drawings.
47
48const SNAPSHOT_TOOL = 'mcp__mission-control__snapshot'
49const MAX_TURNS = 50
50/** Running jobs' output files are read once a second, and only while one runs. */
51const JOBS_MS = 1000
52/** While the tasks folder is not found, it is looked for again at most this often. */
53const JOBS_DIR_RETRY_MS = 30_000
54/** `mb jev status` (enabled, key, model, budget) is asked again at most this often; the journal itself every JEV_MS. */
55const JEV_STATUS_MS = 30_000
56const ALERTS_EVERY_MS = 5 * 60_000
57/** Timers on the Agents tab are redrawn once a second, and only while something works. */
58const TICK_MS = 1000
59/**
60 * The slate's cog turns at four frames a second while an agent works: each frame is a
61 * redraw of the terminal (and of its shader over the whole window), and at ten pixels
62 * across four read as a turn as well as eight (measured 2026-10-16: Ghostty ~8-9% CPU
63 * with two sessions open and the cog turning).
64 */
65const COG_MS = 250
66/** After a blit the engine refused (the tab not on screen), the cog waits this long. */
67const COG_REST_MS = 5000
68/** The engine's own agent list is read every 2 s, and only while a subagent works. */
69const LIST_MS = 2000
70
71// The engine's scan accepts only an atom made in the same file: every file declares
72// the values it reads or writes, with the same key and initial value as the others.
73const hooksA = atom({ plugin: 'mission-control', key: 'hooks' } as const, [])
74const callsA = atom({ plugin: 'mission-control', key: 'calls' } as const, [])
75const readsA = atom({ plugin: 'mission-control', key: 'reads' } as const, [])
76const claudeMdA = atom({ plugin: 'mission-control', key: 'claudeMd' } as const, [])
77const turnsA = atom({ plugin: 'mission-control', key: 'turns' } as const, [])
78const turnA = atom({ plugin: 'mission-control', key: 'turn' } as const, 0)
79const usageA = atom({ plugin: 'mission-control', key: 'usage' } as const, null)
80const alertsA = atom({ plugin: 'mission-control', key: 'alerts' } as const, null)
81const hiddenA = atom({ plugin: 'mission-control', key: 'isHidden' } as const, false)
82const quietA = atom({ plugin: 'mission-control', key: 'isQuiet' } as const, false)
83const paneStatusA = atom({ plugin: 'mission-control', key: 'paneStatus' } as const, null)
84const agentsPaneStatusA = atom({ plugin: 'mission-control', key: 'agentsPaneStatus' } as const, null)
85const actionsA = atom({ plugin: 'mission-control', key: 'actions' } as const, [])
86const lastEndA = atom({ plugin: 'mission-control', key: 'lastTurnEndAt' } as const, null)
87const agentsA = atom({ plugin: 'mission-control', key: 'agents' } as const, [])
88const jobsA = atom({ plugin: 'mission-control', key: 'jobs' } as const, [])
89const jobsDirA = atom({ plugin: 'mission-control', key: 'jobsDir' } as const, null)
90const jobsPaneStatusA = atom({ plugin: 'mission-control', key: 'jobsPaneStatus' } as const, null)
91const jevA = atom({ plugin: 'mission-control', key: 'jev' } as const, null)
92const jevPaneStatusA = atom({ plugin: 'mission-control', key: 'jevPaneStatus' } as const, null)
93const leadA = atom({ plugin: 'mission-control', key: 'lead' } as const, EMPTY_LEAD)
94
95type $ = EngineInterface
96
97/** When the tasks folder was last looked for: a module variable, a reload looks again at once. */
98let jobsDirTriedAt = Number.NEGATIVE_INFINITY
99/** The Jev journal's path once found, how, and the file's size and mtime when it was last read: module variables, a reload looks again. */
100let jevPath: string | null = null
101let jevSource: 'cli' | 'derived' | null = null
102let jevStatus: JevStatus | null = null
103let jevStatusAt = Number.NEGATIVE_INFINITY
104let jevSig = ''
105/** Until when the cog rests after a refused blit: a module variable, a reload tries at once. */
106let cogRestsUntil = Number.NEGATIVE_INFINITY
107/** Until when the servo-skull rests after a refused blit (Обряды closed or behind another tab). */
108let skullRestsUntil = Number.NEGATIVE_INFINITY
109
110/** The slate's sounds: their gain from the `sound` option (0 is silence), set as the module registers. */
111const SOUND_GAIN: Record<string, number> = { off: 0, quiet: 0.35, full: 1 }
112let soundGain = 0
113/** Each job's status at the last look, to sound a rite's end once. */
114const riteStatus = new Map<string, string>()
115
116/**
117 * Where the sounds play: the engine's player (macOS, afplay), or one the mod runs itself
118 * where the engine has none (Windows: PowerShell's MediaPlayer; Linux: paplay or aplay).
119 * Found once as the session starts; null until then, and where nothing can play.
120 */
121let soundHost: 'engine' | 'windows' | 'linux' | null = null
122
123/** Which player this machine has: Windows by its OS variable, macOS or Linux by uname. */
124async function findSoundHost($: $): Promise<'engine' | 'windows' | 'linux' | null> {
125 try {
126 if ((await $.env.get('OS')) === 'Windows_NT') return 'windows'
127 const ran = await $.process.run(['uname', '-s'], { timeoutMs: 3000 })
128 return ran.stdout.trim() === 'Darwin' ? 'engine' : 'linux'
129 } catch {
130 return null
131 }
132}
133
134/** The absolute path of one of the mod's sounds ($.plugin.root may name .claude-plugin itself). */
135function soundFile($: $, name: string): string {
136 const root = $.plugin.root.replace(/[\\/]\.claude-plugin[\\/]?$/, '').replace(/[\\/]+$/, '')
137 return `${root}/sounds/${name}.wav`
138}
139
140/** Plays one of the slate's sounds (mission-control/sounds/<name>.wav); silent where it cannot. */
141async function chime($: $, name: 'awake' | 'vox' | 'servo' | 'rite-done' | 'heresy'): Promise<void> {
142 if (soundGain <= 0 || soundHost === null) return
143 try {
144 if (soundHost === 'engine') {
145 await $.audio.play({ asset: `sounds/${name}.wav` }, { gain: soundGain })
146 return
147 }
148 const file = soundFile($, name)
149 if (soundHost === 'windows') {
150 // MediaPlayer, unlike SoundPlayer, takes a volume; it plays while the script waits.
151 const script = `Add-Type -AssemblyName PresentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${file.replace(/'/g, "''")}'); $p.Volume = ${soundGain}; $p.Play(); Start-Sleep -Milliseconds 1600`
152 await $.process.run(['powershell', '-NoProfile', '-NonInteractive', '-Command', script], { timeoutMs: 5000 })
153 return
154 }
155 try {
156 await $.process.run(['paplay', `--volume=${Math.round(soundGain * 65536)}`, file], { timeoutMs: 5000 })
157 } catch {
158 await $.process.run(['aplay', '-q', file], { timeoutMs: 5000 })
159 }
160 } catch {
161 // no player here, or the clip could not play: the slate stays silent
162 }
163}
164
165/** Once a second: a rite that ended since the last look sounds its end, done or failed. */
166async function listenRites($: $): Promise<void> {
167 try {
168 for (const job of await read($, jobsA)) {
169 const before = riteStatus.get(job.id)
170 riteStatus.set(job.id, job.status)
171 if (before !== 'run' || job.status === 'run') continue
172 if (job.status === 'ok') await chime($, 'rite-done')
173 else if (job.status === 'error') await chime($, 'heresy')
174 }
175 } catch {
176 // a missed look is made up by the next one
177 }
178}
179
180function reason(err: unknown): string {
181 return err instanceof Error ? err.message : String(err)
182}
183
184function sum(list: readonly { tokens: number }[]): number {
185 return list.reduce((acc, x) => acc + x.tokens, 0)
186}
187
188/**
189 * Runs each part of a shared event in turn. One part's failure does not skip the
190 * others; the first failure is thrown once all have run, and the hook's `.catch`
191 * then lets the event go on (fail-open): a broken part never holds up the session,
192 * and the failure stays out of the transcript.
193 */
194async function fanOut(parts: readonly (() => Promise<void>)[]): Promise<void> {
195 const errors: unknown[] = []
196 for (const part of parts) {
197 try {
198 await part()
199 } catch (err) {
200 errors.push(err)
201 }
202 }
203 if (errors.length > 0) throw errors[0]
204}
205
206/** The engine's own token count per instruction file, from the /context breakdown. */
207async function refreshClaudeMd($: $): Promise<void> {
208 let files: readonly { path: string; tokens: number }[] | undefined
209 try {
210 files = (await $.session.usage({ breakdown: 'summary' })).context.breakdown?.memoryFiles
211 } catch {
212 return // no session bound yet (or no breakdown on this host): keep the last figures
213 }
214 if (files === undefined) return
215 const rows: ClaudeMdFile[] = files.map(f => ({ path: f.path, tokens: f.tokens }))
216 await update($, claudeMdA, () => rows)
217}
218
219async function readText($: $, path: string): Promise<string> {
220 const body = await $.fs.read(path)
221 if (typeof body !== 'string') throw new Error(`${path}: not text`)
222 return body
223}
224
225/** Vault location from Singularity's per-machine config, as its own hooks resolve it. */
226async function vaultPath($: $): Promise<string> {
227 const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
228 if (home === undefined) throw new Error('no USERPROFILE/HOME')
229 const cfg = JSON.parse(await readText($, `${home}/.singularity.json`)) as { vault_path?: unknown }
230 if (typeof cfg.vault_path !== 'string') throw new Error('.singularity.json has no vault_path')
231 return cfg.vault_path
232}
233
234/** Reads the same cached files session_start.py reads; never runs `mb` (a cold run takes ~20 s). */
235async function refreshAlerts($: $): Promise<void> {
236 const next: Alerts = { plans: 0, doctorWarn: 0, doctorFail: 0, error: null }
237 try {
238 const vault = await vaultPath($)
239 if (await $.fs.exists(`${vault}/compaction-plans`)) {
240 const entries = await $.fs.list(`${vault}/compaction-plans`)
241 next.plans = entries.filter(f => f.kind === 'file' && f.name.endsWith('.md')).length
242 }
243 if (await $.fs.exists(`${vault}/.mb/doctor-last.json`)) {
244 const doctor = countDoctor(await readText($, `${vault}/.mb/doctor-last.json`))
245 next.doctorWarn = doctor.warn
246 next.doctorFail = doctor.fail
247 }
248 } catch (err) {
249 next.error = reason(err)
250 }
251 await update($, alertsA, () => next)
252}
253
254/** Opens the Memory tab unless it is open; records what the engine answered, so a tab that never shows can be explained. */
255async function openMemoryPane($: $, theme: Theme): Promise<void> {
256 try {
257 const mine = (await $.ui.panes()).find(p => p.id === MEMORY_PANE)
258 if (mine !== undefined && mine.title === theme.titles.memory) {
259 await update($, paneStatusA, () => 'already open')
260 return
261 }
262 // Open under another title: the theme changed and the module reloaded, so the tab is opened again under the new one.
263 if (mine !== undefined) await $.ui.close({ id: MEMORY_PANE })
264 const opened = await $.ui.open({ id: MEMORY_PANE, title: theme.titles.memory })
265 await update($, paneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
266 } catch (err) {
267 await update($, paneStatusA, () => `open failed: ${reason(err)}`)
268 }
269}
270
271/** Opens the Agents tab unless it is open; records what the engine answered, so a tab that never shows can be explained. */
272async function openAgentsPane($: $, theme: Theme): Promise<void> {
273 try {
274 const mine = (await $.ui.panes()).find(p => p.id === AGENTS_PANE)
275 if (mine !== undefined && mine.title === theme.titles.agents) {
276 await update($, agentsPaneStatusA, () => 'already open')
277 return
278 }
279 // Open under another title: the theme changed and the module reloaded, so the tab is opened again under the new one.
280 if (mine !== undefined) await $.ui.close({ id: AGENTS_PANE })
281 const opened = await $.ui.open({ id: AGENTS_PANE, title: theme.titles.agents })
282 await update($, agentsPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
283 } catch (err) {
284 await update($, agentsPaneStatusA, () => `open failed: ${reason(err)}`)
285 }
286}
287
288/** Opens the Processes tab unless it is open; records what the engine answered. */
289async function openJobsPane($: $, theme: Theme): Promise<void> {
290 try {
291 const mine = (await $.ui.panes()).find(p => p.id === JOBS_PANE)
292 if (mine !== undefined && mine.title === theme.titles.jobs) {
293 await update($, jobsPaneStatusA, () => 'already open')
294 return
295 }
296 // Open under another title: the theme changed and the module reloaded, so the tab is opened again under the new one.
297 if (mine !== undefined) await $.ui.close({ id: JOBS_PANE })
298 const opened = await $.ui.open({ id: JOBS_PANE, title: theme.titles.jobs })
299 await update($, jobsPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
300 } catch (err) {
301 await update($, jobsPaneStatusA, () => `open failed: ${reason(err)}`)
302 }
303}
304
305/** The Jev tab is opened by /jev, not unasked; a tab left open under another theme's title is opened again under the new one. */
306async function retitleJevPane($: $, theme: Theme): Promise<void> {
307 try {
308 const mine = (await $.ui.panes()).find(p => p.id === JEV_PANE)
309 if (mine === undefined || mine.title === theme.titles.jev) return
310 await $.ui.close({ id: JEV_PANE })
311 const opened = await $.ui.open({ id: JEV_PANE, title: theme.titles.jev })
312 await update($, jevPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
313 } catch (err) {
314 await update($, jevPaneStatusA, () => `open failed: ${reason(err)}`)
315 }
316}
317
318/**
319 * A long action has begun: bring the Processes tab to the front, so its progress is on
320 * screen without asking. The tab opened last is the one shown, so an open tab behind
321 * another is closed and opened again; never while the person holds a pane's keyboard.
322 */
323async function raiseJobsPane($: $, job: Job, theme: Theme): Promise<void> {
324 try {
325 const what = `${theme.words.kinds[job.kind]} — ${job.description.length > 0 ? job.description : job.gist}`
326 $.ui.toast(theme.slate === null ? `${theme.titles.jobs}: ${what}` : `+++ ${theme.titles.jobs.toUpperCase()} ✠ ${what} +++`)
327 const panes = await $.ui.panes()
328 const mine = panes.find(p => p.id === JOBS_PANE)
329 if (mine !== undefined && (mine.isShown || panes.some(p => p.isFocused))) return
330 if (mine !== undefined) await $.ui.close({ id: JOBS_PANE })
331 const opened = await $.ui.open({ id: JOBS_PANE, title: theme.titles.jobs })
332 await update($, jobsPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
333 } catch (err) {
334 await update($, jobsPaneStatusA, () => `raise failed: ${reason(err)}`)
335 }
336}
337
338/**
339 * The session's background-task folder, `<tmp>/claude-<uid>/<cwd slug>/<session id>/tasks`:
340 * looked for under the temp roots by the session id alone, so neither the uid nor the
341 * slug's rules have to be guessed. Null when it is not there (yet).
342 */
343async function findTasksDir($: $): Promise<string | null> {
344 const sessionId = await $.session.id()
345 const roots = new Set<string>(['/tmp', '/private/tmp'])
346 // The engine lists the variables a module reads: each is named by a literal.
347 const named = [await $.env.get('CLAUDE_CODE_TMPDIR'), await $.env.get('TMPDIR'), await $.env.get('TEMP'), await $.env.get('TMP')]
348 for (const v of named) {
349 if (v !== undefined && v.length > 0) roots.add(v.replace(/[\\/]+$/, ''))
350 }
351 for (const root of roots) {
352 let tops: { name: string; kind: string }[]
353 try {
354 tops = (await $.fs.list(root)).filter(f => f.kind === 'dir' && f.name.startsWith('claude'))
355 } catch {
356 continue
357 }
358 for (const top of tops) {
359 let projects: { name: string; kind: string }[]
360 try {
361 projects = (await $.fs.list(`${root}/${top.name}`)).filter(f => f.kind === 'dir')
362 } catch {
363 continue
364 }
365 for (const project of projects) {
366 const dir = `${root}/${top.name}/${project.name}/${sessionId}/tasks`
367 if (await $.fs.exists(dir)) return dir
368 }
369 }
370 }
371 return null
372}
373
374/** The task output files in the tasks folder; [] when it cannot be read. */
375async function listOutputs($: $, dir: string): Promise<string[]> {
376 try {
377 return (await $.fs.list(dir)).filter(f => f.kind === 'file' && f.name.endsWith('.output')).map(f => f.name)
378 } catch {
379 return []
380 }
381}
382
383/** The last TAIL_BYTES of an output; a large file through `tail -c`, never read whole. Null when unreadable. */
384async function readTail($: $, path: string, size: number): Promise<string | null> {
385 try {
386 if (size <= FULL_READ_MAX) return (await readText($, path)).slice(-TAIL_BYTES)
387 const ran = await $.process.run(['tail', '-c', String(TAIL_BYTES), path], { timeoutMs: 5000 })
388 return ran.exitCode === 0 ? ran.stdout : null
389 } catch {
390 return null
391 }
392}
393
394/** `mb jev status --json`, at most every JEV_STATUS_MS; the last answer stands when the CLI cannot be run. */
395async function askJevStatus($: $, now: number): Promise<void> {
396 if (now - jevStatusAt < JEV_STATUS_MS) return
397 jevStatusAt = now
398 try {
399 const ran = await $.process.run(['mb', 'jev', 'status', '--json'], { timeoutMs: 8000 })
400 const status = ran.exitCode === 0 ? parseStatus(ran.stdout) : null
401 if (status !== null) {
402 jevStatus = status
403 if (jevPath === null && status.journal !== null) {
404 jevPath = status.journal
405 jevSource = 'cli'
406 }
407 }
408 } catch {
409 // no `mb` on the PATH: the path is derived below
410 }
411}
412
413/** The path the engine writes the journal to, derived from the vault in ~/.singularity.json and the host name. */
414async function deriveJevPath($: $): Promise<string | null> {
415 const home = (await $.env.get('HOME')) ?? (await $.env.get('USERPROFILE'))
416 if (home === undefined) return null
417 const vault = vaultOf(await $.fs.read(`${home.replace(/[\\/]+$/, '')}/.singularity.json`))
418 const host = (await $.process.run(['hostname'], { timeoutMs: 5000 })).stdout.trim()
419 return vault === null || host.length === 0 ? null : journalPathOf(vault, host)
420}
421
422/**
423 * Every JEV_MS, while the Jev tab is open: the journal is read again when its size or time changed. The path
424 * is found once (the CLI's status, else derived like the engine does) and kept. A
425 * missing file, a half-written last line and a rotation to log.1.jsonl are all normal.
426 * Never throws; what went wrong is shown on the tab.
427 */
428async function pollJev($: $, force = false): Promise<void> {
429 try {
430 // Nothing is read, and `mb` is never run, for a session that has not opened the tab.
431 if (!force && !(await $.ui.panes()).some(p => p.id === JEV_PANE)) return
432 const now = await $.clock.now()
433 await askJevStatus($, now)
434 if (jevPath === null) {
435 jevPath = await deriveJevPath($)
436 if (jevPath !== null) jevSource = 'derived'
437 }
438 let sessionId: string | null = null
439 try {
440 sessionId = await $.session.id()
441 } catch {
442 // the figure for this session is then zero
443 }
444 const path = jevPath
445 if (path === null) {
446 await update($, jevA, () => ({
447 status: jevStatus,
448 source: null,
449 path: null,
450 sessionId,
451 rows: [],
452 summary: summarize([], sessionId),
453 problem: 'путь журнала не найден: нет `mb jev status` и ~/.singularity.json (vault_path)',
454 }))
455 $.ui.invalidate('ui.render')
456 return
457 }
458 const prev = previousOf(path)
459 const sigOf = async (p: string): Promise<{ sig: string; size: number } | null> => {
460 try {
461 const st = await $.fs.stat(p)
462 return { sig: `${st.size}:${st.mtimeMs}`, size: st.size }
463 } catch {
464 return null
465 }
466 }
467 const cur = await sigOf(path)
468 const old = await sigOf(prev)
469 const sig = `${sessionId ?? ''}|${cur?.sig ?? '-'}|${old?.sig ?? '-'}|${jevStatus === null ? '' : JSON.stringify(jevStatus)}`
470 if (sig === jevSig) return
471 jevSig = sig
472 // No ranged read exists: a file past the read limit is not read, the CLI gives its newest lines.
473 let isBig = false
474 const parts: JevEntry[] = []
475 for (const [file, st] of [[prev, old], [path, cur]] as const) {
476 if (st === null) continue
477 if (st.size > JEV_FULL_READ_MAX) {
478 isBig = true
479 continue
480 }
481 parts.push(...parseJournal(await $.fs.read(file)))
482 }
483 if (isBig) {
484 const ran = await $.process.run(['mb', 'jev', 'log', '--tail', String(JEV_SHOWN)], { timeoutMs: 8000 })
485 parts.push(...(ran.exitCode === 0 ? parseJournal(ran.stdout) : []))
486 }
487 const problem = isBig ? `журнал больше 4 МБ: показаны последние ${JEV_SHOWN} строк из mb jev log --tail ${JEV_SHOWN}, суммы — из mb jev status` : cur === null ? 'журнала пока нет (Jev ещё не вызывали)' : null
488 await update($, jevA, () => ({
489 status: jevStatus,
490 source: jevSource,
491 path,
492 sessionId,
493 rows: newest(parts, JEV_SHOWN),
494 summary: summarize(parts, sessionId),
495 problem,
496 }))
497 $.ui.invalidate('ui.render')
498 } catch (err) {
499 jevSig = ''
500 try {
501 await update($, jevA, jev => ({
502 status: jevStatus,
503 source: jevSource,
504 path: jevPath,
505 sessionId: jev?.sessionId ?? null,
506 rows: jev?.rows ?? [],
507 summary: jev?.summary ?? summarize([], null),
508 problem: `журнал не прочитан: ${reason(err)}`,
509 }))
510 } catch {
511 // the next poll tries again
512 }
513 }
514}
515
516/**
517 * Once a second while a job runs: finds a foreground call's output file, reads each
518 * running job's output when it grew (progress, last line, a background exit), ends a
519 * monitor past its deadline, and raises the tab for a job that just became long.
520 */
521async function pollJobs($: $, theme: Theme): Promise<void> {
522 try {
523 const jobs = await read($, jobsA)
524 if (!needsPoll(jobs)) return
525 const now = await $.clock.now()
526 let dir = await read($, jobsDirA)
527 if (dir === null && now - jobsDirTriedAt >= JOBS_DIR_RETRY_MS) {
528 jobsDirTriedAt = now
529 dir = await findTasksDir($)
530 const found = dir
531 if (found !== null) await update($, jobsDirA, () => found)
532 }
533 const running = jobs.filter(j => j.status === 'run')
534 // A foreground call's file is the one new since it began, when exactly one is.
535 const attach = new Map<string, string>()
536 const unattached = running.filter(j => j.outputPath === null && !j.isBackground && j.before !== null)
537 if (dir !== null && unattached.length === 1) {
538 const job = unattached[0]
539 const claimed = new Set(jobs.map(j => (j.outputPath === null ? '' : (j.outputPath.split(/[\\/]/).at(-1) ?? ''))))
540 const name = job === undefined || job.before === null ? null : freshOutput(job.before, await listOutputs($, dir), claimed)
541 if (job !== undefined && name !== null) attach.set(job.id, `${dir}/${name}`)
542 }
543 const reads: OutputRead[] = []
544 for (const job of running) {
545 const path = job.outputPath ?? attach.get(job.id) ?? null
546 if (path === null) continue
547 try {
548 const stat = await $.fs.stat(path)
549 const grew = stat.size !== job.outputSize || job.outputPath === null
550 reads.push({ id: job.id, path, size: stat.size, tail: grew ? await readTail($, path, stat.size) : null })
551 } catch {
552 // not written yet, or a foreground call's file already gone: its tool.call end settles it
553 }
554 }
555 await update($, jobsA, list => applyDeadlines(applyOutput(list, reads, now), now))
556 const fresh = toAnnounce(await read($, jobsA), now)
557 const first = fresh[0]
558 if (first !== undefined) {
559 await update($, jobsA, list => markAnnounced(list, fresh.map(j => j.id)))
560 void chime($, 'servo')
561 await raiseJobsPane($, first, theme)
562 }
563 // Elapsed times on the tab move every second even when no output did.
564 $.ui.invalidate('ui.render')
565 } catch {
566 // a missed poll is made up by the next one
567 }
568}
569
570/**
571 * Turns the slate's cogs by one frame while work goes on: a blit of each tab's Raster,
572 * no redraw. The angle comes from the clock, so a redraw draws a cog where the blits
573 * left it. When no cog took the frame (the tabs closed or hidden), they rest a while.
574 */
575async function turnCog($: $, theme: Theme, skullOnJobs: boolean): Promise<void> {
576 try {
577 const slate = theme.slate
578 if (slate === null) return
579 const now = await $.clock.now()
580 if (now < cogRestsUntil) return
581 // Each tab's cog turns while its own work goes on: agents on Легион, rites on Обряды.
582 const agentsAwake = (await read($, agentsA)).some(r => isWorking(r.phase))
583 // On Обряды a servo-skull may stand in the cog's place: it has its own timer.
584 const forgeAwake = !skullOnJobs && (await read($, jobsA)).some(j => j.status === 'run')
585 if (!agentsAwake && !forgeAwake) return
586 const cells = cogCells(COG.columns, COG.rows, cogAngle(now), cogPalette(theme, slate))
587 let shown = false
588 for (const [pane, awake] of [
589 [AGENTS_PANE, agentsAwake],
590 [JOBS_PANE, forgeAwake],
591 ] as const) {
592 if (!awake) continue
593 const res = await $.ui.blit({ requestId: pane, key: COG.key, cells })
594 if (res.deny === undefined) shown = true
595 }
596 if (!shown) cogRestsUntil = now + COG_REST_MS
597 } catch {
598 // no surface bound, or the tab drawn without a cog: the next frame tries again
599 }
600}
601
602/**
603 * The servo-skull on Обряды hovers while a rite runs: the next frame's file, swapped by a
604 * blit. At rest it holds still (each frame is a redraw of the window). A refused blit
605 * (the tab hidden, or a terminal without pictures, where the cog is drawn instead) rests it.
606 */
607async function hoverSkull($: $, dir: string, pictures: boolean): Promise<void> {
608 try {
609 const now = await $.clock.now()
610 if (now < skullRestsUntil) return
611 if (!(await read($, jobsA)).some(j => j.status === 'run')) return
612 // A picture's next file where the terminal draws pictures, else its pixmap in half blocks.
613 const frame = skullFrame(dir, now)
614 // Asked each frame: the engine may have refused pictures here since the session began.
615 const live = pictures && (await drawsPictures($))
616 const pix = live ? null : await loadPixmap($, frame)
617 if (!live && pix === null) return
618 const res =
619 pix === null
620 ? await $.ui.blit({ requestId: JOBS_PANE, key: SKULL.key, source: { file: frame, format: 'png' } })
621 : await $.ui.blit({ requestId: JOBS_PANE, key: SKULL.key, cells: halfBlockCells(pix, SKULL.columns, SKULL.rows) })
622 if (saysAlt(res.deny)) {
623 // The engine draws the picture's alt here: the slate goes back to its cells.
624 refusePictures()
625 $.ui.invalidate('ui.render')
626 }
627 if (res.deny !== undefined) skullRestsUntil = now + COG_REST_MS
628 } catch {
629 skullRestsUntil = Number.POSITIVE_INFINITY // no surface here: a reload tries again
630 }
631}
632
633/**
634 * Is the terminal really drawing pictures? Taken for one by its name (Ghostty, kitty),
635 * it is asked once: the crest's picture blitted again. Where the engine answers
636 * that it draws the alt there, the slate goes back to its cells, every tab redrawn.
637 * The blit carries the very picture the tab draws (the chosen one when there is one):
638 * the mod's own in its place would stand on Легион until the tab is drawn again.
639 */
640async function probePictures($: $, crestFile: string | null): Promise<void> {
641 try {
642 if (!(await drawsPictures($))) return
643 const source = crestFile !== null ? { file: crestFile, format: 'png' as const } : { png: AQUILA_PNG }
644 const res = await $.ui.blit({ requestId: AGENTS_PANE, key: 'crest', source })
645 if (saysAlt(res.deny)) {
646 refusePictures()
647 $.ui.invalidate('ui.render')
648 }
649 } catch {
650 // no surface here: the next start asks again
651 }
652}
653
654/** Redraws while something works, so the Agents tab's timers stay true; does nothing when all is quiet. */
655async function tick($: $): Promise<void> {
656 try {
657 if (needsTick(await read($, agentsA), await $.clock.now())) $.ui.invalidate('ui.render')
658 } catch {
659 // a missed redraw is drawn by the next tick
660 }
661 await watch($)
662}
663
664/**
665 * The watchdog (spec 6.5), on the same tick (it runs whether or not anything is drawn): new alarms go to the
666 * lead's news; an idle lead is also woken, at most once per 10 minutes. Fails open: a throw is told once.
667 */
668async function watch($: $): Promise<void> {
669 try {
670 const rows = await read($, agentsA)
671 const now = await $.clock.now()
672 const held = await read($, leadA)
673 if (watchStep(held, rows, now).lead === held) return
674 const out: { wake: string | null } = { wake: null }
675 await update($, leadA, cur => {
676 const step = watchStep(cur, rows, now)
677 out.wake = step.wake
678 return step.lead
679 })
680 if (out.wake !== null) await $.prompt.submit({ text: out.wake })
681 } catch (err) {
682 try {
683 await update($, leadA, cur =>
684 cur.flags.includes('guard-error')
685 ? cur
686 : { ...pushNews(cur, `сторож упал: ${reason(err)}`), flags: [...cur.flags, 'guard-error'] },
687 )
688 } catch {
689 // nothing more to do: the next tick tries again
690 }
691 }
692}
693
694/** Squares the rows with $.agent.list(); `always` reads it even when nothing works (a load, a reload). */
695async function reconcile($: $, always: boolean): Promise<void> {
696 try {
697 if (!always && !needsReconcile(await read($, agentsA), await $.clock.now())) return
698 const listed: ListedAgent[] = (await $.agent.list()).map(a => ({
699 id: a.id,
700 type: a.type,
701 description: a.description,
702 status: a.status,
703 ...(a.parentId === undefined ? {} : { parentId: a.parentId }),
704 ...(a.name === undefined ? {} : { name: a.name }),
705 isTeammate: a.teammateId !== undefined,
706 }))
707 const turn = await read($, turnA)
708 const now = await $.clock.now()
709 await update($, agentsA, rows => applyList(rows, listed, { turn, now }))
710 } catch {
711 // no session bound yet, or a host without agents: the events still keep the rows
712 }
713}
714
715export const register: Register = (on, options) => {
716 // The look comes from the `theme` option (/config → mission-control); a change there
717 // reloads the module, so it is read once here and handed to every drawing.
718 const theme = themeOf((options as Record<string, unknown> | undefined)?.theme)
719 // Sounds belong to a slate; the `sound` option sets how loud (quiet when unset).
720 const soundOpt = (options as Record<string, unknown> | undefined)?.sound
721 soundGain = theme.slate === null ? 0 : (SOUND_GAIN[typeof soundOpt === 'string' ? soundOpt : 'quiet'] ?? SOUND_GAIN.quiet ?? 0)
722 // A crest picture the person chose (tools/install.mjs --aquila): an absolute PNG path.
723 const chosen = (options as Record<string, unknown> | undefined)?.aquilaImage
724 const crestFile = typeof chosen === 'string' && chosen.trim().length > 0 ? chosen.trim() : null
725 // A servo-skull's frames the person chose (tools/install.mjs --servo-skull): a folder.
726 const skull = (options as Record<string, unknown> | undefined)?.servoSkull
727 const skullDir = typeof skull === 'string' && skull.trim().length > 0 ? skull.trim() : null
728 // A purity seal the person chose (tools/install.mjs --purity-seal): a PNG.
729 const sealOpt = (options as Record<string, unknown> | undefined)?.puritySeal
730 const sealFile = typeof sealOpt === 'string' && sealOpt.trim().length > 0 ? sealOpt.trim() : null
731 // The ranks' portraits the person chose (tools/install.mjs --portrait): a folder.
732 const portraitsOpt = (options as Record<string, unknown> | undefined)?.rankPortraits
733 const portraitsDir = typeof portraitsOpt === 'string' && portraitsOpt.trim().length > 0 ? portraitsOpt.trim() : null
734
735 on('session.start', async ($, e, next) => {
736 // The slate's awakening: the band plays the cogitator's boot for a few seconds, redrawn
737 // four times a second, then the timer stops itself.
738 if (theme.slate !== null) {
739 try {
740 const began = await $.clock.now()
741 markBoot(began)
742 if (soundGain > 0) soundHost = await findSoundHost($)
743 void chime($, 'awake')
744 const timer = $.clock.every(250, () => {
745 $.ui.invalidate('ui.render')
746 void $.clock.now().then(now => {
747 if (now - began > BOOT_MS + 250) timer.cancel()
748 })
749 })
750 } catch {
751 // no clock bound: the band draws as always
752 }
753 }
754 // The tabs open first and on their own, so a registration that fails below cannot keep
755 // them shut. Memory first, Agents last: the tab opened last is the one on screen (seen
756 // live in a 170-column terminal), and mission control is what the person opens the
757 // sidebar for; Processes comes forward by itself when a long action begins. After a
758 // reload any may be up with the old module's drawing, so all are redrawn once (re)opened.
759 void openMemoryPane($, theme)
760 .then(() => openJobsPane($, theme))
761 .then(() => openAgentsPane($, theme))
762 .then(() => $.ui.invalidate('ui.render'))
763 .then(() => {
764 if (theme.slate !== null) $.clock.after(2000, () => void probePictures($, crestFile))
765 })
766 .catch(() => undefined)
767 await fanOut([
768 async () => {
769 await $.tool.register({
770 name: 'snapshot',
771 description:
772 'mission-control state as JSON: agents (main and every subagent: phase, task, intent, current action, report), the activity feed, long jobs (background shells, long commands, monitors: kind, gist, who started them, progress parsed from their output), and the memory figures (settings-hook injections, CLAUDE.md tokens, mb search / mb code calls, turns, usage, vault alerts). Read-only.',
773 })
774 },
775 async () => {
776 await $.tool.register({
777 name: 'team',
778 description:
779 'The executors you delegated to, as a table: id, tier, effort, task, turns, OE spent (and the limit), state. Also the session OE budget, the Fable calls used, the news the digest has not delivered yet and the flags (frozen, solo, budget notices). Read-only: it does not clear the news.',
780 })
781 },
782 async () => {
783 await $.command.register({
784 name: 'mem',
785 description: 'Память → контекст: вкладка «Память»; /mem close|hide|show|quiet|reset',
786 argumentHint: '[close|hide|show|quiet|reset]',
787 })
788 },
789 async () => {
790 await refreshAlerts($)
791 $.clock.every(ALERTS_EVERY_MS, () => void refreshAlerts($))
792 // Idle times are drawn from the clock: redraw once a minute so they stay true.
793 $.clock.every(60_000, () => $.ui.invalidate('ui.render'))
794 },
795 async () => {
796 await $.command.register({
797 name: 'mc',
798 description: 'Mission control: кто из агентов чем занят и почему; /mc close|clear|budget [+N]|halt|resume|solo',
799 argumentHint: '[close|clear|budget [+N]|halt|resume|solo]',
800 })
801 },
802 async () => {
803 $.clock.every(TICK_MS, () => void tick($))
804 // On Обряды the servo-skull stands in the cog's place where it can be drawn (a picture,
805 // or the pixmaps beside its frames where the terminal draws none) on its own timer;
806 // elsewhere both tabs' cogs turn on the one.
807 const pictures = await drawsPictures($)
808 const skullOn = theme.slate !== null && skullDir !== null && (pictures || (await loadPixmap($, skullFrame(skullDir, 0))) !== null)
809 if (theme.slate !== null) $.clock.every(COG_MS, () => void turnCog($, theme, skullOn))
810 if (soundGain > 0) $.clock.every(1000, () => void listenRites($))
811 if (skullOn && skullDir !== null) $.clock.every(SKULL.frameMs, () => void hoverSkull($, skullDir, pictures))
812 $.clock.every(LIST_MS, () => void reconcile($, false))
813 // A reload (or the mod set up mid-session) finds agents already at work: learn them now.
814 await reconcile($, true)
815 },
816 async () => {
817 await $.command.register({
818 name: 'jobs',
819 description: 'Процессы: долгие команды (скачивание, обучение, подготовка данных) с прогрессом; /jobs close|clear',
820 argumentHint: '[close|clear]',
821 })
822 },
823 async () => {
824 await $.command.register({
825 name: 'jev',
826 description: 'Jev: журнал вызовов, расход за сессию и всего, остаток; /jev close|status',
827 argumentHint: '[close|status]',
828 })
829 },
830 async () => {
831 $.clock.every(JEV_MS, () => void pollJev($))
832 await retitleJevPane($, theme)
833 await pollJev($)
834 },
835 async () => {
836 $.clock.every(JOBS_MS, () => void pollJobs($, theme))
837 // Found now, so a foreground call's file can be told apart from the first call on.
838 if ((await read($, jobsDirA)) === null) {
839 jobsDirTriedAt = await $.clock.now()
840 const dir = await findTasksDir($)
841 if (dir !== null) await update($, jobsDirA, () => dir)
842 }
843 },
844 ])
845 return next(e)
846 }).catch(($, e, next) => next(e))
847
848 on('turn.complete', async ($, e, next) => {
849 const answer = await next(e)
850 await fanOut([
851 // The council window is not closed here: the councils' background rounds end turns and wake new ones.
852 // The owner's next own prompt closes it (memory/hooks.tsx, prompt.submit).
853 // The vox chirps when a main turn ends (the slate's sound).
854 async () => {
855 if (e.agentId === undefined) await chime($, 'vox')
856 },
857 // Memory: a main turn's row (searches, hook text, steps, edits, cache) and the CLAUDE.md figures.
858 async () => {
859 if (e.agentId !== undefined) return
860 const index = await read($, turnA)
861 const calls = (await read($, callsA)).filter(c => c.turn === index)
862 const hooks = (await read($, hooksA)).filter(inj => inj.turn === index)
863 const acts = (await read($, actionsA)).filter(a => a.turn === index)
864 const u = e.usage
865 const row: TurnRow = {
866 index,
867 searches: calls.length,
868 searchTokens: sum(calls),
869 hookTokens: sum(hooks),
870 durationMs: e.durationMs,
871 actions: acts.length,
872 edits: acts.filter(a => a.kind === 'edit' || a.kind === 'delete').length,
873 input: u?.input_tokens ?? 0,
874 output: u?.output_tokens ?? 0,
875 cacheRead: u?.cache_read_input_tokens ?? 0,
876 cacheWrite: u?.cache_creation_input_tokens ?? 0,
877 }
878 await update($, turnsA, list => [...list.filter(t => t.index !== index), row].slice(-MAX_TURNS))
879 const endedAt = await $.clock.now()
880 await update($, lastEndA, () => endedAt)
881 await refreshClaudeMd($)
882 },
883 // Mission control: the loop's row ends (main waits, a subagent reports, a teammate idles).
884 async () => {
885 const now = await $.clock.now()
886 const u = e.usage
887 // New work only: every step re-reads the cached context, so cache reads would count it
888 // again per step (seen live: 77k summed against the engine's own 27k for one agent).
889 const tokens = u === undefined ? 0 : (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0)
890 await update($, agentsA, rows => applyComplete(rows, { agentId: e.agentId ?? null, reason: e.reason, answer: e.answer, tokens, now }))
891 // The week journal (spec 6.2): an executor's end is one line; main and an idle teammate are not executors' ends.
892 if (e.agentId === undefined) return
893 const ended = (await read($, agentsA)).find(r => r.id === e.agentId)
894 if (ended !== undefined && (ended.phase === 'done' || ended.phase === 'failed' || ended.phase === 'aborted')) {
895 await appendJournal($, journalLine(ended))
896 // The lead hears of the end (spec 6.6): outcome and the OE of this run, as the journal counts it.
897 try {
898 const line = endedLine({ label: rowLabel(ended), outcome: PHASE_WORDS[ended.phase], oe: runOe(ended) })
899 await update($, leadA, cur => pushNews(cur, line))
900 } catch {
901 // the line is lost, the end is not
902 }
903 // What is written is not written again: a resume's next line starts from here.
904 await update($, agentsA, rows => rows.map(r => (r.id === ended.id ? { ...r, journaledOe: ended.oe, journaledSteps: ended.steps } : r)))
905 }
906 },
907 ])
908 return answer
909 }).catch(($, e, next) => next(e))
910
911 on('command.run', { command: 'mc' }, async ($, e) => {
912 const arg = e.args.trim().toLowerCase()
913 if (arg === 'close') {
914 await $.ui.close({ id: AGENTS_PANE })
915 return { text: 'mission-control: вкладка «Агенты» закрыта (/mc — открыть).' }
916 }
917 if (arg === 'clear') {
918 const turn = await read($, turnA)
919 await update($, agentsA, rows => clearEnded(rows))
920 await update($, actionsA, list => list.filter(a => a.turn === turn))
921 return { text: 'mission-control: завершённые агенты и лента прошлых ходов убраны.' }
922 }
923 const words = arg.split(/\s+/)
924 if (words[0] === 'budget') {
925 if (words.length === 1) return { text: teamTable(await read($, agentsA), await read($, leadA), leadConfig, await $.clock.now()) }
926 const n = budgetIncrement(words.slice(1))
927 if (n === null) return { text: BUDGET_BAD }
928 if (leadConfig.sessionBudget === null) return { text: BUDGET_NO_LIMIT }
929 await update($, leadA, cur => raiseBudget(cur, leadConfig, n))
930 return { text: `mission-control: лимит сессии: ${sessionLimit(await read($, leadA), leadConfig)} OE (+${n})` }
931 }
932 if (words.length === 1 && words[0] === 'halt') {
933 await update($, leadA, haltLead)
934 return { text: 'mission-control: исполнители остановлены (/mc resume — снять).' }
935 }
936 if (words.length === 1 && words[0] === 'resume') {
937 if (!(await read($, leadA)).halted) return { text: 'mission-control: исполнители не заморожены' }
938 await update($, leadA, resumeLead)
939 return { text: 'mission-control: заморозка снята.' }
940 }
941 if (words.length === 1 && words[0] === 'solo') {
942 await update($, leadA, soloLead)
943 return { text: 'mission-control: solo — лид получит «делай сам, не делегируй» в ближайшей сводке.' }
944 }
945 if (arg.length > 0) {
946 return { text: `mission-control: не знаю «${arg}». /mc — открыть вкладку, /mc close, /mc clear, /mc budget [+N], /mc halt, /mc resume, /mc solo.` }
947 }
948 const opened = await $.ui.open({ id: AGENTS_PANE, title: theme.titles.agents })
949 await update($, agentsPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
950 return { text: opened.isPlaced ? 'mission-control: вкладка «Агенты» открыта.' : `mission-control: вкладка не встала: ${opened.reason}` }
951 }).catch(($, e, next) => ({ text: `mission-control: /mc упала: ${reason(next.error)}` }))
952
953 on('command.run', { command: 'jobs' }, async ($, e) => {
954 const arg = e.args.trim().toLowerCase()
955 if (arg === 'close') {
956 await $.ui.close({ id: JOBS_PANE })
957 return { text: 'mission-control: вкладка «Процессы» закрыта (/jobs — открыть; новый долгий процесс откроет её сам).' }
958 }
959 if (arg === 'clear') {
960 await update($, jobsA, jobs => clearJobs(jobs))
961 return { text: 'mission-control: завершённые процессы убраны.' }
962 }
963 if (arg.length > 0) return { text: `mission-control: не знаю «${arg}». /jobs — открыть вкладку, /jobs close, /jobs clear.` }
964 const opened = await $.ui.open({ id: JOBS_PANE, title: theme.titles.jobs })
965 await update($, jobsPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
966 return { text: opened.isPlaced ? 'mission-control: вкладка «Процессы» открыта.' : `mission-control: вкладка не встала: ${opened.reason}` }
967 }).catch(($, e, next) => ({ text: `mission-control: /jobs упала: ${reason(next.error)}` }))
968
969 on('command.run', { command: 'jev' }, async ($, e) => {
970 const arg = e.args.trim().toLowerCase()
971 if (arg === 'close') {
972 await $.ui.close({ id: JEV_PANE })
973 return { text: 'mission-control: вкладка «Jev» закрыта (/jev — открыть).' }
974 }
975 if (arg === 'status') {
976 try {
977 const ran = await $.process.run(['mb', 'jev', 'status'], { timeoutMs: 8000 })
978 if (ran.exitCode === 0 && ran.stdout.trim().length > 0) return { text: ran.stdout.trimEnd() }
979 const why = ran.stderr.trim().split('\n')[0] ?? ''
980 return { text: `mb jev status недоступен: ${why.length > 0 ? why.slice(0, 120) : `код ${ran.exitCode}`}` }
981 } catch (err) {
982 return { text: `mb jev status недоступен: ${reason(err).slice(0, 120)}` }
983 }
984 }
985 if (arg.length > 0) return { text: `mission-control: не знаю «${arg}». /jev — открыть вкладку, /jev close, /jev status.` }
986 const opened = await $.ui.open({ id: JEV_PANE, title: theme.titles.jev })
987 await update($, jevPaneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
988 jevSig = ''
989 await pollJev($, true)
990 return { text: opened.isPlaced ? 'mission-control: вкладка «Jev» открыта.' : `mission-control: вкладка не встала: ${opened.reason}` }
991 }).catch(($, e, next) => ({ text: `mission-control: /jev упала: ${reason(next.error)}` }))
992
993 on('command.run', { command: 'mem' }, async ($, e) => {
994 const arg = e.args.trim().toLowerCase()
995 if (arg === 'close') {
996 await $.ui.close({ id: MEMORY_PANE })
997 return { text: 'mission-control: вкладка «Память» закрыта, счётчики снова на полосе над вводом (/mem — открыть).' }
998 }
999 if (arg === 'hide') {
1000 await update($, hiddenA, () => true)
1001 return { text: 'mission-control: полоса памяти скрыта (/mem show — вернуть).' }
1002 }
1003 if (arg === 'show') {
1004 await update($, hiddenA, () => false)
1005 return { text: 'mission-control: полоса памяти показана.' }
1006 }
1007 if (arg === 'quiet') {
1008 const isQuiet = !(await read($, quietA))
1009 await update($, quietA, () => isQuiet)
1010 return { text: `mission-control: мягкие тревоги на полосе (compaction-планы, doctor WARN) ${isQuiet ? 'скрыты' : 'показаны'}.` }
1011 }
1012 if (arg === 'reset') {
1013 await update($, callsA, () => [])
1014 await update($, readsA, () => [])
1015 await update($, turnsA, () => [])
1016 return { text: 'mission-control: счётчики поиска и ходов сброшены (инъекции хуков, CLAUDE.md, агенты и лента оставлены).' }
1017 }
1018 await refreshAlerts($)
1019 await refreshClaudeMd($)
1020 const opened = await $.ui.open({ id: MEMORY_PANE, title: theme.titles.memory })
1021 await update($, paneStatusA, () => (opened.isPlaced ? 'placed' : `not placed: ${opened.reason}`))
1022 return { text: opened.isPlaced ? 'mission-control: вкладка «Память» открыта.' : `mission-control: вкладка не встала: ${opened.reason}` }
1023 }).catch(($, e, next) => ({ text: `mission-control: /mem упала: ${reason(next.error)}` }))
1024
1025 on('tool.call', { tool: SNAPSHOT_TOOL }, async $ => {
1026 let panes: unknown = []
1027 try {
1028 panes = await $.ui.panes()
1029 } catch {
1030 // no surface bound (a test, a headless start): the rest still answers
1031 }
1032 const state = {
1033 turn: await read($, turnA),
1034 agents: await read($, agentsA),
1035 actions: (await read($, actionsA)).slice(-30),
1036 hooks: await read($, hooksA),
1037 claudeMd: await read($, claudeMdA),
1038 calls: await read($, callsA),
1039 reads: await read($, readsA),
1040 turns: await read($, turnsA),
1041 usage: await read($, usageA),
1042 alerts: await read($, alertsA),
1043 paneStatus: await read($, paneStatusA),
1044 agentsPaneStatus: await read($, agentsPaneStatusA),
1045 jobs: await read($, jobsA),
1046 jobsDir: await read($, jobsDirA),
1047 jobsPaneStatus: await read($, jobsPaneStatusA),
1048 jevPaneStatus: await read($, jevPaneStatusA),
1049 panes,
1050 lastTurnEndAt: await read($, lastEndA),
1051 }
1052 return { result: JSON.stringify(state, null, 1) }
1053 })
1054
1055 // The team table for the lead: read-only, the news it lists is still delivered by the digest.
1056 on('tool.call', { tool: TEAM_TOOL }, async $ => ({
1057 result: teamReport(await read($, agentsA), await read($, leadA), leadConfig, await $.clock.now()),
1058 }))
1059
1060 // The digest for the lead (spec 6.6): what changed since the last one, after every tool batch of the
1061 // main loop (a subagent's batch carries agent_id and gets nothing). Fails open: a throw leaves the batch
1062 // untouched and says so once in the lead's news.
1063 const leadConfig = leadConfigOf(options)
1064 on('classic.PostToolBatch', async ($, e, next) => {
1065 const res = await next(e)
1066 if (e.agent_id !== undefined) return res
1067 try {
1068 const out: { text: string | null } = { text: null }
1069 await update($, leadA, cur => {
1070 const taken = takeDigest(cur, leadConfig)
1071 out.text = taken.text
1072 return taken.lead
1073 })
1074 if (out.text === null) return res
1075 return { ...res, additionalContext: [...(res.additionalContext ?? []), out.text] }
1076 } catch (err) {
1077 try {
1078 await update($, leadA, cur =>
1079 cur.flags.includes('guard-error')
1080 ? cur
1081 : { ...pushNews(cur, `сводка упала: ${reason(err)}`), flags: [...cur.flags, 'guard-error'] },
1082 )
1083 } catch {
1084 // nothing more to do: the next batch tries again
1085 }
1086 return res
1087 }
1088 }).catch(($, e, next) => next(e))
1089
1090 registerMission(on, theme, leadConfig, crestFile, portraitsDir)
1091 registerMemory(on, theme, sealFile, crestFile, leadConfig)
1092 registerJobs(on, theme, skullDir)
1093 registerJev(on, theme)
1094 registerChrome(on, theme)
1095}
1096hooks/jobs/hooks.tsx 196 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, On } from 'claude-code'
3
4import { skullFrame } from '../lib/frame'
5import { drawsPictures, loadPixmap } from '../lib/terminal'
6import { forSurface } from '../lib/theme'
7import type { Theme } from '../lib/theme'
8import { MAIN } from '../mission/model'
9import {
10 agentLabelOf,
11 applyCallEnd,
12 applyJobStart,
13 applyTaskEnd,
14 backgroundIdOf,
15 dirOf,
16 outputPathOf,
17 parseNotifications,
18} from './model'
19import { drawJobsPane } from './pane'
20
21// The Processes tab's observers and its drawing. What runs on a timer (polling the
22// output files, raising the tab), /jobs and opening the tab live in register.tsx:
23// the engine takes one unmatched hook per event per module and never follows `$`
24// into an imported function.
25
26export const JOBS_PANE = 'jobs'
27/** Both shells; a pattern, so no build's tool list has to name PowerShell. */
28const SHELLS = /^(Bash|PowerShell)$/
29
30// The engine's scan accepts only an atom made in the same file: every file declares
31// the values it reads or writes, with the same key and initial value as the others.
32const jobsA = atom({ plugin: 'mission-control', key: 'jobs' } as const, [])
33const jobsDirA = atom({ plugin: 'mission-control', key: 'jobsDir' } as const, null)
34const agentsA = atom({ plugin: 'mission-control', key: 'agents' } as const, [])
35
36type $ = EngineInterface
37
38function reason(err: unknown): string {
39 return err instanceof Error ? err.message : String(err)
40}
41
42function str(v: unknown): string {
43 return typeof v === 'string' ? v : ''
44}
45
46/** The task output files in the session's tasks folder; [] when it cannot be read. */
47async function listOutputs($: $, dir: string): Promise<string[]> {
48 try {
49 return (await $.fs.list(dir)).filter(f => f.kind === 'file' && f.name.endsWith('.output')).map(f => f.name)
50 } catch {
51 return []
52 }
53}
54
55type CallFacts = { id: string; tool: string; agentId: string | null; command: string; description: string; isBackground: boolean; timeoutMs: number | null }
56
57/** A long-capable call begins: a row with who started it and why. Never throws. */
58async function begin($: $, f: CallFacts): Promise<void> {
59 try {
60 const agents = await read($, agentsA)
61 const intent = agents.find(r => r.id === (f.agentId ?? MAIN))?.intent ?? null
62 const dir = await read($, jobsDirA)
63 // A foreground call's own output file is the one that appears after this listing.
64 const before = f.isBackground || dir === null ? null : await listOutputs($, dir)
65 const now = await $.clock.now()
66 await update($, jobsA, jobs =>
67 applyJobStart(jobs, { ...f, agentLabel: agentLabelOf(agents, f.agentId), intent, before, now }),
68 )
69 } catch {
70 // the call goes on unobserved
71 }
72}
73
74type Ran = { deny?: string; isError?: true; result?: unknown; text?: string }
75
76/** The call returned: a launch keeps running under its task id, anything else ends. Never throws. */
77async function settle($: $, id: string, ran: Ran): Promise<void> {
78 try {
79 if (ran.deny !== undefined) {
80 await update($, jobsA, jobs => jobs.filter(j => j.id !== id))
81 return
82 }
83 const now = await $.clock.now()
84 const taskId = ran.isError === true ? null : backgroundIdOf(ran.result)
85 let outputPath: string | null = null
86 if (taskId !== null) {
87 const dir = await read($, jobsDirA)
88 outputPath = outputPathOf(ran.text ?? '') ?? (dir === null ? null : `${dir}/${taskId}.output`)
89 const learned = outputPath === null ? null : dirOf(outputPath)
90 if (learned !== null && dir === null) await update($, jobsDirA, () => learned)
91 }
92 const interrupted = (ran.result as { interrupted?: unknown } | undefined)?.interrupted === true
93 const status = ran.isError === true ? 'error' : interrupted ? 'killed' : 'ok'
94 await update($, jobsA, jobs => applyCallEnd(jobs, { id, now, taskId, outputPath, status }))
95 } catch {
96 // a missed end is settled by the poll or the task's notification
97 }
98}
99
100export function registerJobs(on: On, theme: Theme, skullDir: string | null = null): void {
101 const c = theme.colors
102 // Every shell call may turn long: it is followed from its start, and shown once it is.
103 on('tool.call', { tool: SHELLS }, async ($, e, next) => {
104 const input = e as unknown as Record<string, unknown>
105 const command = str(input.command)
106 if (command.length === 0) return next(e)
107 const startedAt = await $.clock.now()
108 const id = str(input.tool_use_id) || `${String(e.tool)}-${startedAt}`
109 await begin($, {
110 id,
111 tool: String(e.tool),
112 agentId: e.agentId ?? null,
113 command,
114 description: str(input.description),
115 isBackground: input.run_in_background === true,
116 timeoutMs: null,
117 })
118 const ran = await next(e)
119 await settle($, id, ran as unknown as Ran)
120 return ran
121 }).catch(($, e, next) => next(e))
122
123 on('tool.call', { tool: 'Monitor' }, async ($, e, next) => {
124 const startedAt = await $.clock.now()
125 const id = e.tool_use_id ?? `Monitor-${startedAt}`
126 await begin($, {
127 id,
128 tool: 'Monitor',
129 agentId: e.agentId ?? null,
130 command: e.command ?? e.ws?.url ?? '',
131 description: e.description,
132 isBackground: true,
133 timeoutMs: typeof e.timeout_ms === 'number' ? e.timeout_ms : null,
134 })
135 const ran = await next(e)
136 await settle($, id, ran as unknown as Ran)
137 return ran
138 }).catch(($, e, next) => next(e))
139
140 // Claude stopped a task: it ended, whatever its output says next.
141 on('tool.call', { tool: 'TaskStop' }, async ($, e, next) => {
142 const ran = await next(e)
143 try {
144 const taskId = e.task_id ?? e.shell_id ?? null
145 if (ran.deny === undefined && ran.isError !== true && taskId !== null) {
146 const now = await $.clock.now()
147 await update($, jobsA, jobs => applyTaskEnd(jobs, { taskId, toolUseId: null, status: 'killed', exitCode: null, now }))
148 }
149 } catch {
150 // the poll settles it
151 }
152 return ran
153 }).catch(($, e, next) => next(e))
154
155 // A background task's notification: how it ended, as the engine tells the model.
156 on('prompt.submit', { origin: { kind: 'task-notification' } }, async ($, e, next) => {
157 try {
158 const ends = parseNotifications(e.text)
159 if (ends.length > 0) {
160 const now = await $.clock.now()
161 await update($, jobsA, jobs => ends.reduce((acc, end) => applyTaskEnd(acc, { ...end, now }), jobs))
162 }
163 } catch {
164 // the poll reads the exit from the output file
165 }
166 return next(e)
167 }).catch(($, e, next) => next(e))
168
169 // The Processes tab: drawn by pane.tsx from the jobs and the agent rows.
170 on('ui.render', { component: 'Pane', requestId: JOBS_PANE }, async ($, e) => {
171 const now = await $.clock.now()
172 const pictures = await drawsPictures($)
173 const data = {
174 jobs: await read($, jobsA),
175 agents: await read($, agentsA),
176 now,
177 // A servo-skull as pictures where the terminal draws them, else its frame in half blocks.
178 skullDir: skullDir !== null && pictures ? skullDir : null,
179 skullPix: skullDir !== null && !pictures ? await loadPixmap($, skullFrame(skullDir, now)) : null,
180 }
181 return drawJobsPane($.ui.resolve(e), data, e.props.bodyColumns, e.props.scroll.bodyRows, forSurface(theme, e.surface))
182 }).catch(($, e, next) => {
183 const { Box, Text } = $.ui.resolve(e)
184 return (
185 <Box flexDirection="column" backgroundColor={c.surface} paddingX={1}>
186 <Text color={c.critical} bold>
187 mission-control: вкладка «{theme.titles.jobs}» упала
188 </Text>
189 <Text color={c.ink2} wrap="wrap">
190 {reason(next.error)}
191 </Text>
192 </Box>
193 )
194 })
195}
196hooks/jobs/model.ts 604 lines1// Long actions, the Processes tab's model: pure functions from engine facts and a
2// task's output text to job rows. No `$` here: the hooks feed facts in, the tests
3// call these directly.
4//
5// Where the facts come from (probed live 2026-10-14, macOS):
6// - every Bash call, foreground included, writes its output as it goes to
7// `<tmp>/claude-<uid>/<cwd slug>/<session id>/tasks/<id>.output`; a foreground
8// call's file is deleted when the call ends;
9// - a background task's file ends with `[exited with code N]` once it exits;
10// - its notification reaches prompt.submit as `<task-notification>` with
11// `<task-id>`, `<tool-use-id>`, `<status>` and a `<summary>` naming the exit code.
12
13import type { AgentRow, Job, JobKind, JobProgress } from '../../types'
14import { basename } from '../lib/actions'
15import { clip } from '../mission/model'
16
17/** A foreground call shows once it has run this long; a background one at once. */
18export const LONG_MS = 5000
19/** An ended job stays folded in the tab this long. */
20export const KEEP_ENDED_MS = 30 * 60_000
21export const MAX_ENDED = 20
22/** A running job whose output has not grown for this long is flagged. */
23export const SILENT_OUTPUT_MS = 2 * 60_000
24/** How much of an output's end is read for its progress. */
25export const TAIL_BYTES = 8192
26/** Above this size the tail is read with `tail -c`, not by reading the whole file. */
27export const FULL_READ_MAX = 1_000_000
28export const COMMAND_MAX = 400
29export const JOB_DESCRIPTION_MAX = 160
30export const LINE_MAX = 200
31
32// ── What a command is ─────────────────────────────────────────────────────────
33
34export const KIND_LABEL: Record<JobKind, string> = {
35 download: 'Скачивание',
36 train: 'Обучение',
37 data: 'Подготовка данных',
38 eval: 'Оценка',
39 install: 'Установка',
40 build: 'Сборка',
41 test: 'Тесты',
42 server: 'Сервер',
43 watch: 'Наблюдение',
44 run: 'Команда',
45}
46
47const GIST: Record<JobKind, string> = {
48 download: 'скачивает',
49 train: 'обучает:',
50 data: 'готовит данные:',
51 eval: 'оценивает:',
52 install: 'ставит',
53 build: 'собирает',
54 test: 'гоняет тесты:',
55 server: 'держит запущенным',
56 watch: 'следит:',
57 run: 'выполняет',
58}
59
60/** When a chain does several things, the one that names it: `pip install x && python train.py` is training. */
61const PRIORITY: readonly JobKind[] = ['train', 'eval', 'data', 'download', 'server', 'test', 'build', 'install', 'watch', 'run']
62
63/** Shell words that only set the stage and say nothing about the action. */
64const STAGE = /^(cd|pushd|popd|echo|printf|date|export|set|unset|source|\.|sleep|true|false|clear|for|while|until|done|do|if|then|else|elif|fi|mkdir|ls|cat|wait|trap|read)(\s|$)/
65/** Prefixes that wrap the real command. */
66const WRAPPER = /^(?:nohup|time|sudo|exec|env|caffeinate(?:\s+-\w+)*|nice(?:\s+-n\s*-?\d+)?|stdbuf(?:\s+-\w+)*)\s+/
67
68/** The meaningful pieces of a command line, each with its wrappers and env assignments taken off. */
69export function commandParts(command: string): string[] {
70 return command
71 // A lone `&` backgrounds what precedes it; `2>&1` and `&>` are redirections, not splits.
72 .split(/&&|\|\||;|\n|\|(?!\|)|(?<![>&<])&(?![&>])/)
73 .map(raw => {
74 let s = raw.trim().replace(/^(do|then|else)\s+/, '')
75 for (let i = 0; i < 6; i += 1) {
76 const before = s
77 s = s.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/, '').replace(WRAPPER, '').trim()
78 if (s === before) break
79 }
80 return s
81 })
82 .filter(s => s.length > 0 && !STAGE.test(s) && !/^[A-Za-z_][A-Za-z0-9_]*=\S*$/.test(s) && !/^[)}]/.test(s))
83}
84
85function argsOf(words: readonly string[]): string[] {
86 return words.filter(a => a.length > 0 && !a.startsWith('-') && !/^\d*[<>]/.test(a) && a !== '>' && a !== '<')
87}
88
89const URL_RE = /\b(?:https?|s3|gs|hf|ftp):\/\/[^\s'"`)]+/
90
91/** `model.bin ← huggingface.co`, or the host alone when the URL has no file. */
92function urlTarget(url: string): string {
93 const m = /^[a-z0-9]+:\/\/([^/?#]+)([^?#]*)/i.exec(url)
94 if (m === null) return url
95 const host = (m[1] ?? '').replace(/^www\./, '')
96 const file = basename(m[2] ?? '')
97 return file.length > 0 && file !== host ? `${file} ← ${host}` : host
98}
99
100/** The script or module a runner runs: `train.py`, `-m pkg.train` → `pkg.train`. */
101function scriptOf(words: readonly string[]): string | null {
102 const m = words.indexOf('-m')
103 if (m >= 0 && words[m + 1] !== undefined) return words[m + 1] ?? null
104 const file = words.slice(1).find(w => /\.(py|sh|bash|zsh|js|mjs|cjs|ts|ipynb|r|jl|rb|pl)$/i.test(w.replace(/["']/g, '')))
105 return file === undefined ? null : basename(file)
106}
107
108const RUNNERS = new Set(['python', 'python3', 'py', 'node', 'bun', 'deno', 'bash', 'sh', 'zsh', 'Rscript', 'julia', 'ruby', 'perl', 'uv', 'poetry', 'pipenv', 'conda', 'tsx', 'ts-node', 'npx'])
109const TRAIN_WORDS = /train|finetun|fine[-_]?tun|\bsft\b|_sft|sft_|lora|\bdpo\b|_dpo|pretrain|distill/i
110const EVAL_WORDS = /eval|bench|score/i
111const DATA_WORDS = /data|prep|preprocess|tokeni[sz]|convert|ingest|embed|index|scrape|crawl|augment|dedup|shard|clean|extract|generat|build_|make_/i
112const DOWNLOAD_WORDS = /download|fetch|pull|mirror|snapshot_download|hf_hub_download|load_dataset|from_pretrained/i
113const TEST_WORDS = /test|verify|check|lint/i
114
115type Classified = { kind: JobKind; target: string }
116
117/** One piece of a command line: what it is and what it acts on. */
118function classify(part: string): Classified {
119 const words = part.split(/\s+/)
120 const prog = basename(words[0] ?? '').replace(/\.exe$/i, '')
121 const args = argsOf(words.slice(1))
122 const sub = args[0] ?? ''
123 const url = URL_RE.exec(part)?.[0] ?? null
124 const low = part.toLowerCase()
125
126 // Downloads first: their target is the URL.
127 if (/^(curl|wget|aria2c|axel|yt-dlp|youtube-dl)$/.test(prog)) return { kind: 'download', target: url === null ? sub : urlTarget(url) }
128 const hf = /\b(?:huggingface-cli|hf)\s+download\s+(\S+)/.exec(part)
129 if (hf !== null) return { kind: 'download', target: `${hf[1] ?? ''} ← Hugging Face` }
130 if (prog === 'git' && sub === 'clone') return { kind: 'download', target: `репозиторий ${basename(args[1] ?? '').replace(/\.git$/, '')}` }
131 if (prog === 'git' && sub === 'lfs') return { kind: 'download', target: 'файлы git LFS' }
132 if ((prog === 'docker' || prog === 'podman') && sub === 'pull') return { kind: 'download', target: args[1] ?? '' }
133 if (prog === 'ollama' && sub === 'pull') return { kind: 'download', target: args[1] ?? '' }
134 if (/^(rsync|scp|rclone|gsutil)$/.test(prog) || /\baws\s+s3\s+(cp|sync)\b|\bgcloud\s+storage\s+(cp|rsync)\b/.test(part)) {
135 return { kind: 'download', target: url === null ? basename(args.at(-2) ?? args[0] ?? '') : urlTarget(url) }
136 }
137
138 // Installs.
139 const pyPip = /(?:^|\s)-m\s+pip\s+install\b/.test(part)
140 if (pyPip || /^(pip3?|pipx|conda|mamba|micromamba|brew|apt|apt-get|npm|pnpm|yarn|bun|cargo|gem|go|poetry)$/.test(prog) || prog === 'uv') {
141 const verb = pyPip ? 'install' : prog === 'uv' && sub === 'pip' ? (args[1] ?? '') : sub
142 if (/^(install|i|add|ci|sync|get)$/.test(verb) || (prog === 'yarn' && args.length === 0)) {
143 const skip = pyPip ? args.indexOf('install') + 1 : prog === 'uv' && sub === 'pip' ? 2 : 1
144 const pkgs = args.slice(Math.max(1, skip)).filter(a => !/^(install|i|add|ci|sync|get|pip)$/.test(a))
145 const shown = pkgs.slice(0, 3).join(', ') + (pkgs.length > 3 ? ', …' : '')
146 return { kind: 'install', target: shown.length > 0 ? shown : `зависимости (${prog === 'uv' ? 'uv' : pyPip ? 'pip' : prog})` }
147 }
148 }
149
150 // Package-manager scripts: build, test, dev servers.
151 if (/^(npm|pnpm|yarn|bun)$/.test(prog)) {
152 const script = sub === 'run' ? (args[1] ?? '') : sub
153 if (/^build/.test(script)) return { kind: 'build', target: `${prog} ${script}` }
154 if (/^test/.test(script)) return { kind: 'test', target: `${prog} ${script}` }
155 if (/^(dev|start|serve|preview)/.test(script)) return { kind: 'server', target: `${prog} ${script}` }
156 }
157 if (/^(make|cmake|ninja|gradle|gradlew|mvn|bazel|tsc|webpack|xcodebuild|msbuild)$/.test(prog)) {
158 if (prog === 'make' && /^(test|check)/.test(sub)) return { kind: 'test', target: `make ${sub}` }
159 return { kind: 'build', target: [prog, sub].filter(Boolean).join(' ') }
160 }
161 if ((prog === 'cargo' || prog === 'go') && sub === 'build') return { kind: 'build', target: `${prog} build` }
162 if ((prog === 'docker' || prog === 'podman') && (sub === 'build' || (sub === 'compose' && args[1] === 'build'))) return { kind: 'build', target: 'образ docker' }
163 if (prog === 'vite' && sub === 'build') return { kind: 'build', target: 'vite build' }
164 if (/^(pytest|jest|vitest|mocha|tox|nox|playwright)$/.test(prog) || /(?:^|\s)-m\s+(pytest|unittest)\b/.test(part)) return { kind: 'test', target: prog }
165 if ((prog === 'cargo' || prog === 'go') && sub === 'test') return { kind: 'test', target: `${prog} test` }
166 if (prog === 'claude' && sub === 'plugin' && args[1] === 'test') return { kind: 'test', target: `плагин ${basename(args[2] ?? '')}`.trim() }
167 if (/^(uvicorn|gunicorn|llama-server|vite)$/.test(prog) || /\bflask\s+run\b|\bjupyter\s+(lab|notebook)\b|\b(ollama|vllm)\s+serve\b|(?:^|\s)-m\s+http\.server\b|\bnext\s+dev\b/.test(part)) {
168 return { kind: 'server', target: prog === 'python' || prog === 'python3' ? 'http.server' : [prog, sub].filter(Boolean).join(' ') }
169 }
170 if ((prog === 'tail' && /\s-[a-zA-Z]*[fF]/.test(part)) || prog === 'watch') return { kind: 'watch', target: basename(args.at(-1) ?? prog) }
171 if (prog === 'mb' && sub === 'ingest') return { kind: 'data', target: 'mb ingest' }
172
173 // Training launchers, then any runner by the name of what it runs.
174 if (/^(torchrun|deepspeed|axolotl)$/.test(prog) || /\baccelerate\s+launch\b|\bmlx_lm\.lora\b|\bllamafactory-cli\s+train\b|\btune\s+run\b/.test(part)) {
175 return { kind: 'train', target: scriptOf(words) ?? prog }
176 }
177 const script = scriptOf(words)
178 const isRunner = RUNNERS.has(prog) || /^\.?\.?\//.test(words[0] ?? '') || script !== null
179 if (isRunner) {
180 // `python -c "…"`: the code itself says what it is.
181 const name = script ?? (/\s-c\s/.test(part) ? low : '')
182 const target = script ?? (words[0] === undefined ? prog : basename(words[0]))
183 if (TRAIN_WORDS.test(name)) return { kind: 'train', target }
184 if (EVAL_WORDS.test(name)) return { kind: 'eval', target }
185 if (DOWNLOAD_WORDS.test(name)) return { kind: 'download', target: script ?? 'с Hugging Face' }
186 if (DATA_WORDS.test(name)) return { kind: 'data', target }
187 if (TEST_WORDS.test(script ?? '')) return { kind: 'test', target }
188 return { kind: 'run', target }
189 }
190 return { kind: 'run', target: [prog, sub].filter(Boolean).join(' ') }
191}
192
193export type JobText = { kind: JobKind; label: string; gist: string }
194
195/** A command in words: its kind, the tab's label for it, and its gist. */
196export function describeJob(tool: string, command: string, description: string): JobText {
197 if (tool === 'Monitor') {
198 const target = description.length > 0 ? description : (commandParts(command)[0] ?? command)
199 return { kind: 'watch', label: KIND_LABEL.watch, gist: clip(`${GIST.watch} ${target}`, LINE_MAX) }
200 }
201 const parts = commandParts(command).map(classify)
202 const rank = (k: JobKind): number => PRIORITY.indexOf(k)
203 const best = parts.reduce<Classified | null>((acc, c) => (acc === null || rank(c.kind) < rank(acc.kind) ? c : acc), null)
204 const chosen = best ?? { kind: 'run' as const, target: clip(command.trim(), 60) }
205 const target = chosen.target.trim().length > 0 ? chosen.target.trim() : (description.length > 0 ? description : clip(command.trim(), 60))
206 return { kind: chosen.kind, label: KIND_LABEL[chosen.kind], gist: clip(`${GIST[chosen.kind]} ${target}`, LINE_MAX) }
207}
208
209// ── What the output says ──────────────────────────────────────────────────────
210
211const ANSI = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-Za-z]|\x1b[=>78]/g
212const CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g
213
214/**
215 * The lines a terminal would show for this output: ANSI codes dropped, and a line
216 * rewritten in place with \r (tqdm, curl, wget) reduced to its last state.
217 */
218export function screenLines(text: string): string[] {
219 return text
220 .replace(ANSI, '')
221 .split('\n')
222 .map(line => {
223 const pieces = line.split('\r').filter(p => p.trim().length > 0)
224 return (pieces.at(-1) ?? '').replace(CONTROL, '').trimEnd()
225 })
226 .filter(line => line.trim().length > 0)
227}
228
229/** The engine's own last line in a background task's output once the command exits. */
230const EXIT_MARK = /^\[exited with code (-?\d+)\]\s*$/
231
232/** The exit code a background task's output ends with; null while it runs. */
233export function exitCodeOf(lines: readonly string[]): number | null {
234 const m = EXIT_MARK.exec(lines.at(-1) ?? '')
235 return m === null ? null : Number(m[1])
236}
237
238type Hit = { fraction: number | null; count: string | null; eta: string | null; rate: string | null }
239
240function num(s: string | undefined): number {
241 return Number((s ?? '').replace(',', '.').replace(/[^\d.eE-]/g, ''))
242}
243
244/** A fraction kept to four places: 1.2/3.0 reads 0.4, not 0.39999999999999997. */
245function round(f: number): number {
246 return Math.round(f * 10_000) / 10_000
247}
248
249function ratio(a: string | undefined, b: string | undefined): number | null {
250 if (a === undefined || b === undefined || a.length === 0 || b.length === 0) return null
251 const x = num(a)
252 const y = num(b)
253 return Number.isFinite(x) && Number.isFinite(y) && y > 0 && x >= 0 && x <= y ? round(x / y) : null
254}
255
256/** "45.2" → 0.452; null for nothing (Number('') is 0, which once read as a 0% bar). */
257function percent(s: string | undefined): number | null {
258 if (s === undefined || s.trim().length === 0) return null
259 const v = num(s)
260 return Number.isFinite(v) && v >= 0 && v <= 100 ? round(v / 100) : null
261}
262
263function eta(s: string | undefined): string | null {
264 const t = (s ?? '').trim()
265 return t.length === 0 || /^[-?:]+$/.test(t) || /^--/.test(t) ? null : t
266}
267
268function rateIn(s: string | undefined): string | null {
269 return /[\d.]+\s*[A-Za-z]*\/s\b/.exec(s ?? '')?.[0]?.replace(/\s+/g, '') ?? null
270}
271
272function hit(fraction: number | null, count: string | null = null, left: string | null = null, rate: string | null = null): Hit {
273 return { fraction, count, eta: left, rate }
274}
275
276const UNIT = String.raw`\s*[kKMGTP]?(?:i?B)?`
277
278/** Each reads one line; the first that matches the latest progress line wins. */
279const MATCHERS: readonly ((line: string) => Hit | null)[] = [
280 // tqdm: " 45%|████▌ | 450/1000 [00:12<00:15, 36.2it/s]"
281 line => {
282 const m = new RegExp(String.raw`(\d{1,3}(?:\.\d+)?)%\s*\|[^|]*\|\s*([\d.]+${UNIT})\s*\/\s*([\d.]+${UNIT})\s*\[([\d:]+)<([\d:?]+)(?:,\s*([^\]]*?))?\s*\]`).exec(line)
283 return m === null ? null : hit(percent(m[1]), `${(m[2] ?? '').trim()}/${(m[3] ?? '').trim()}`, eta(m[5]), rateIn(m[6]))
284 },
285 // tqdm without a total: "1234it [00:05, 246.1it/s]"
286 line => {
287 const m = /(\d[\d.]*\s*(?:it|[kMG]?B))\s*\[([\d:]+)(?:,\s*([^\]]*))?\]/.exec(line)
288 return m === null ? null : hit(null, (m[1] ?? '').replace(/\s+/g, ' '), null, rateIn(m[3]))
289 },
290 // keras: "120/500 [=====>........] - ETA: 12s - loss: 0.31"
291 line => {
292 const m = /^\s*(\d+)\/(\d+)\s*\[[=>.]*\]\s*(?:-\s*ETA:\s*(\S+))?/.exec(line)
293 return m === null ? null : hit(ratio(m[1], m[2]), `${m[1]}/${m[2]}`, eta(m[3]))
294 },
295 // pip / rich: "━━━━━━━╸━━━━ 1.2/3.4 MB 5.6 MB/s eta 0:00:01"
296 line => {
297 const m = /([\d.]+)\/([\d.]+)\s*([kMGT]i?B|it)\s+([\d.]+\s*(?:[kMGT]i?B|it)\/s)(?:\s+eta\s+([\d:]+))?/.exec(line)
298 return m === null ? null : hit(ratio(m[1], m[2]), `${m[1]}/${m[2]} ${m[3]}`, eta(m[5]), rateIn(m[4]))
299 },
300 // git: "Receiving objects: 45% (450/1000), 1.20 MiB | 2.40 MiB/s"
301 line => {
302 const m = /:\s+(\d{1,3})%\s+\((\d+)\/(\d+)\)(?:,\s*[\d.]+\s*[KMG]iB\s*\|\s*([\d.]+\s*[KMG]iB\/s))?/.exec(line)
303 return m === null ? null : hit(percent(m[1]), `${m[2]}/${m[3]}`, null, rateIn(m[4]))
304 },
305 // wget bar: "model.bin 45%[=====> ] 1.20G 3.40MB/s eta 5m 2s"
306 line => {
307 const m = /(\d{1,3})%\s*\[[=> ]*\]\s+([\d.,]+[KMG]?)\s+([\d.,]+\s*[KMG]?B\/s)(?:\s+(?:eta|in)\s+(.+?))?\s*$/.exec(line)
308 return m === null ? null : hit(percent(m[1]), m[2] ?? null, eta(m[4]), rateIn(m[3]))
309 },
310 // wget dots: " 51200K .......... .......... 45% 3.20M 5s"
311 line => {
312 const m = /\.{5,}\s+(\d{1,3})%\s+([\d.,]+[KMG])\s+(\S+)\s*$/.exec(line)
313 return m === null ? null : hit(percent(m[1]), null, eta(m[3]), `${m[2]}/s`)
314 },
315 // curl meter: " 45 123M 45 55.6M 0 0 10.2M 0 0:00:12 0:00:05 0:00:07 10.5M"
316 line => {
317 const m = /^\s*(\d{1,3})\s+([\d.]+[kMGT]?)\s+(\d{1,3})\s+([\d.]+[kMGT]?)\s+\d+\s+\d+\s+[\d.]+[kMGT]?\s+[\d.]+[kMGT]?\s+([\d:-]+)\s+([\d:-]+)\s+([\d:-]+)\s+([\d.]+[kMGT]?)\s*$/.exec(line)
318 return m === null ? null : hit(percent(m[1]), `${m[4]}/${m[2]}`, eta(m[7]), `${m[8]}/s`)
319 },
320 // a training or batch loop: "epoch 3/10", "Step 120 of 500", "эпоха 2 из 5"
321 line => {
322 const m = /\b(epoch|step|iter(?:ation)?|batch|round|эпоха|шаг|итерация)\b\s*[:=#]?\s*\[?\s*(\d+)\s*(?:\/|of|из)\s*(\d+)/i.exec(line)
323 if (m === null) return null
324 const word = (m[1] ?? '').toLowerCase()
325 const said = word === 'epoch' ? 'эпоха' : word === 'step' ? 'шаг' : word
326 return hit(ratio(m[2], m[3]), `${said} ${m[2]}/${m[3]}`)
327 },
328 // a bracketed count: "[12/100]", "(3/7)"
329 line => {
330 const m = /[[(]\s*(\d+)\s*\/\s*(\d+)\s*[\])]/.exec(line)
331 if (m === null || num(m[2]) < 2) return null
332 const f = ratio(m[1], m[2])
333 return f === null ? null : hit(f, `${m[1]}/${m[2]}`)
334 },
335 // a bare percentage, the last on the line: "Downloading… 45.2%"
336 line => {
337 const all = [...line.matchAll(/(?:^|[^\d.])(\d{1,3}(?:\.\d+)?)\s?%/g)]
338 const f = percent(all.at(-1)?.[1])
339 return f === null ? null : hit(f)
340 },
341]
342
343const STAGE_RE = /\b(epoch|эпоха)\b\s*[:=#]?\s*\[?\s*(\d+)\s*(?:\/|of|из)\s*(\d+)/i
344const METRIC_RE = /\b(val_loss|eval_loss|train_loss|loss|val_acc|accuracy|acc|perplexity|ppl)\b['"]?\s*[:=]\s*(-?\d+(?:\.\d+)?(?:e-?\d+)?)/gi
345/** Progress is looked for this many lines back from the end. */
346const LOOK_BACK = 40
347
348/** How far the job got, from the lines its output shows; null when they say nothing. */
349export function parseProgress(lines: readonly string[]): JobProgress | null {
350 const recent = lines.slice(-LOOK_BACK)
351 let primary: Hit | null = null
352 for (let i = recent.length - 1; i >= 0 && primary === null; i -= 1) {
353 const line = recent[i] ?? ''
354 for (const match of MATCHERS) {
355 primary = match(line)
356 if (primary !== null) break
357 }
358 }
359 let stage: string | null = null
360 let metric: string | null = null
361 for (let i = recent.length - 1; i >= 0 && (stage === null || metric === null); i -= 1) {
362 const line = recent[i] ?? ''
363 if (stage === null) {
364 const s = STAGE_RE.exec(line)
365 if (s !== null) stage = `эпоха ${s[2]}/${s[3]}`
366 }
367 if (metric === null) {
368 const found = [...line.matchAll(METRIC_RE)].at(-1)
369 if (found !== undefined) metric = `${(found[1] ?? '').toLowerCase()} ${found[2]}`
370 }
371 }
372 if (primary !== null && stage !== null && primary.count === stage) stage = null
373 if (primary === null && stage === null && metric === null) return null
374 return { fraction: primary?.fraction ?? null, count: primary?.count ?? null, eta: primary?.eta ?? null, rate: primary?.rate ?? null, stage, metric }
375}
376
377// ── Engine texts ──────────────────────────────────────────────────────────────
378
379/** The output file a background launch names in its result text. */
380export function outputPathOf(text: string): string | null {
381 const said = /Output is being written to:\s*(\S+?\.output)\b/.exec(text)?.[1]
382 if (said !== undefined) return said
383 return /(\S*[\\/]tasks[\\/][\w-]+\.output)\b/.exec(text)?.[1] ?? null
384}
385
386/** The folder a path is in. */
387export function dirOf(path: string): string {
388 return path.replace(/[\\/][^\\/]*$/, '')
389}
390
391/** The background task id a Bash or Monitor result carries; null for a call that ran to its end. */
392export function backgroundIdOf(result: unknown): string | null {
393 if (typeof result !== 'object' || result === null) return null
394 const r = result as { backgroundTaskId?: unknown; taskId?: unknown }
395 if (typeof r.backgroundTaskId === 'string' && r.backgroundTaskId.length > 0) return r.backgroundTaskId
396 if (typeof r.taskId === 'string' && r.taskId.length > 0) return r.taskId
397 return null
398}
399
400export type TaskEnd = { taskId: string | null; toolUseId: string | null; status: Job['status']; exitCode: number | null }
401
402function tag(block: string, name: string): string | null {
403 const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(block)
404 return m === null ? null : (m[1] ?? '').trim()
405}
406
407/** Every task end a notification text reports: `<task-notification>` blocks, one per task. */
408export function parseNotifications(text: string): TaskEnd[] {
409 const out: TaskEnd[] = []
410 for (const m of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
411 const block = m[1] ?? ''
412 const word = (tag(block, 'status') ?? '').toLowerCase()
413 const code = /exit code (-?\d+)/i.exec(tag(block, 'summary') ?? '')?.[1]
414 const exitCode = code === undefined ? null : Number(code)
415 let status: Job['status']
416 if (word === 'completed') status = exitCode !== null && exitCode !== 0 ? 'error' : 'ok'
417 else if (word === 'failed') status = 'error'
418 else if (word === 'killed' || word === 'stopped' || word === 'cancelled') status = 'killed'
419 else continue
420 out.push({ taskId: tag(block, 'task-id'), toolUseId: tag(block, 'tool-use-id'), status, exitCode })
421 }
422 return out
423}
424
425/** The one output file that appeared since a foreground call began and nobody claims; null while it is not certain. */
426export function freshOutput(before: readonly string[], names: readonly string[], claimed: ReadonlySet<string>): string | null {
427 const fresh = names.filter(n => n.endsWith('.output') && !before.includes(n) && !claimed.has(n))
428 return fresh.length === 1 ? (fresh[0] ?? null) : null
429}
430
431/** How the person calls the loop that started a job: 'главный', or the agent's name or type. */
432export function agentLabelOf(rows: readonly AgentRow[], agentId: string | null): string {
433 if (agentId === null) return 'главный'
434 const row = rows.find(r => r.id === agentId)
435 if (row === undefined) return 'субагент'
436 return row.name ?? row.type
437}
438
439// ── Rows ──────────────────────────────────────────────────────────────────────
440
441export type JobStart = {
442 id: string
443 tool: string
444 agentId: string | null
445 agentLabel: string
446 command: string
447 description: string
448 intent: string | null
449 isBackground: boolean
450 before: string[] | null
451 timeoutMs: number | null
452 now: number
453}
454
455function oneLine(text: string): string {
456 return text.replace(/\s+/g, ' ').trim()
457}
458
459export function newJob(f: JobStart): Job {
460 const words = describeJob(f.tool, f.command, f.description)
461 return {
462 id: f.id,
463 taskId: null,
464 tool: f.tool,
465 agentId: f.agentId,
466 agentLabel: f.agentLabel,
467 description: clip(oneLine(f.description), JOB_DESCRIPTION_MAX),
468 command: clip(oneLine(f.command), COMMAND_MAX),
469 kind: words.kind,
470 gist: words.gist,
471 intent: f.intent,
472 isBackground: f.isBackground,
473 status: 'run',
474 exitCode: null,
475 startedAt: f.now,
476 endedAt: null,
477 outputPath: null,
478 before: f.before,
479 outputSize: 0,
480 outputAt: null,
481 progress: null,
482 lastLine: null,
483 timeoutMs: f.timeoutMs,
484 isAnnounced: false,
485 }
486}
487
488function isEnded(j: Job): boolean {
489 return j.status !== 'run'
490}
491
492/** Running jobs, and the newest ended ones up to MAX_ENDED. */
493export function trimJobs(jobs: readonly Job[]): Job[] {
494 const ended = jobs.filter(isEnded).sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0))
495 const keep = new Set(ended.slice(0, MAX_ENDED).map(j => j.id))
496 return jobs.filter(j => !isEnded(j) || keep.has(j.id))
497}
498
499/** `/jobs clear`: only what still runs. */
500export function clearJobs(jobs: readonly Job[]): Job[] {
501 return jobs.filter(j => !isEnded(j))
502}
503
504export function applyJobStart(jobs: readonly Job[], f: JobStart): Job[] {
505 return trimJobs([...jobs.filter(j => j.id !== f.id), newJob(f)])
506}
507
508export type CallEnd = {
509 id: string
510 now: number
511 /** The task the call left running in the background (run_in_background, ctrl+b, a timeout); null when it ran to its end. */
512 taskId: string | null
513 outputPath: string | null
514 status: Job['status']
515}
516
517/**
518 * The tool call returned. A launch (or a command moved to the background) keeps
519 * running under its task id; a call that ran to its end ends, and one that never
520 * got long enough to show is dropped.
521 */
522export function applyCallEnd(jobs: readonly Job[], f: CallEnd): Job[] {
523 const job = jobs.find(j => j.id === f.id)
524 if (job === undefined || isEnded(job)) return [...jobs]
525 if (f.taskId !== null) {
526 return jobs.map(j => (j.id === f.id ? { ...j, taskId: f.taskId, isBackground: true, before: null, outputPath: f.outputPath ?? j.outputPath } : j))
527 }
528 if (!job.isAnnounced && f.now - job.startedAt < LONG_MS) return jobs.filter(j => j.id !== f.id)
529 return trimJobs(jobs.map(j => (j.id === f.id ? { ...j, status: f.status, endedAt: f.now, before: null } : j)))
530}
531
532/** A task ended by its notification or a TaskStop: matched by task id, or by the call that started it. */
533export function applyTaskEnd(jobs: readonly Job[], f: TaskEnd & { now: number }): Job[] {
534 return trimJobs(
535 jobs.map(j => {
536 if (isEnded(j)) return j
537 const isIt = (f.taskId !== null && j.taskId === f.taskId) || (f.toolUseId !== null && j.id === f.toolUseId)
538 return isIt ? { ...j, status: f.status, exitCode: f.exitCode ?? j.exitCode, endedAt: f.now } : j
539 }),
540 )
541}
542
543/** What one poll read of a job's output: its size, and its tail when it grew. */
544export type OutputRead = { id: string; path: string; size: number; tail: string | null }
545
546/** The poll's reads folded in: output size and time, progress, last line, and a background exit. */
547export function applyOutput(jobs: readonly Job[], reads: readonly OutputRead[], now: number): Job[] {
548 const byId = new Map(reads.map(r => [r.id, r]))
549 return trimJobs(
550 jobs.map(j => {
551 const r = byId.get(j.id)
552 if (r === undefined || isEnded(j)) return j
553 const grew = r.size !== j.outputSize
554 let next: Job = { ...j, outputPath: r.path, outputSize: r.size, outputAt: grew ? now : j.outputAt }
555 if (r.tail === null) return next
556 const lines = screenLines(r.tail)
557 const code = exitCodeOf(lines)
558 const shown = code === null ? lines : lines.slice(0, -1)
559 next = { ...next, progress: parseProgress(shown) ?? j.progress, lastLine: shown.length > 0 ? clip(shown.at(-1)?.trim() ?? '', LINE_MAX) : j.lastLine }
560 if (code !== null && j.isBackground) next = { ...next, status: code === 0 ? 'ok' : 'error', exitCode: code, endedAt: now }
561 return next
562 }),
563 )
564}
565
566/** A Monitor past its deadline has ended, notified or not. */
567export function applyDeadlines(jobs: readonly Job[], now: number): Job[] {
568 return jobs.map(j =>
569 !isEnded(j) && j.timeoutMs !== null && j.timeoutMs > 0 && now > j.startedAt + j.timeoutMs + 10_000 ? { ...j, status: 'ok', endedAt: now } : j,
570 )
571}
572
573export function markAnnounced(jobs: readonly Job[], ids: readonly string[]): Job[] {
574 return jobs.map(j => (ids.includes(j.id) ? { ...j, isAnnounced: true } : j))
575}
576
577/**
578 * Does the tab show it? A running background job at once, a foreground one once long;
579 * an ended one for a while if it ever showed (a background job did from its start,
580 * even when it ended before a poll could announce it).
581 */
582export function isShown(j: Job, now: number): boolean {
583 if (!isEnded(j)) return j.isBackground || now - j.startedAt >= LONG_MS
584 const didShow = j.isAnnounced || j.isBackground || (j.endedAt ?? 0) - j.startedAt >= LONG_MS
585 return j.endedAt !== null && now - j.endedAt <= KEEP_ENDED_MS && didShow
586}
587
588export function runningShown(jobs: readonly Job[], now: number): Job[] {
589 return jobs.filter(j => !isEnded(j) && isShown(j, now))
590}
591
592export function endedShown(jobs: readonly Job[], now: number): Job[] {
593 return jobs.filter(j => isEnded(j) && isShown(j, now)).sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0))
594}
595
596/** Jobs the tab shows now and has not raised itself for yet. */
597export function toAnnounce(jobs: readonly Job[], now: number): Job[] {
598 return jobs.filter(j => !isEnded(j) && !j.isAnnounced && isShown(j, now))
599}
600
601export function needsPoll(jobs: readonly Job[]): boolean {
602 return jobs.some(j => !isEnded(j))
603}
604hooks/jev/hooks.tsx 41 lines1import { atom, read } from 'claude-code'
2import type { On } from 'claude-code'
3
4import { forSurface } from '../lib/theme'
5import type { Theme } from '../lib/theme'
6import { drawJevPane } from './pane'
7
8// The Jev tab's drawing. What runs on a timer (reading the journal), /jev and opening
9// the tab live in register.tsx: the engine takes one unmatched hook per event per
10// module and never follows `$` into an imported function.
11
12export const JEV_PANE = 'jev'
13
14// The engine's scan accepts only an atom made in the same file: every file declares
15// the values it reads or writes, with the same key and initial value as the others.
16const jevA = atom({ plugin: 'mission-control', key: 'jev' } as const, null)
17
18function reason(err: unknown): string {
19 return err instanceof Error ? err.message : String(err)
20}
21
22export function registerJev(on: On, theme: Theme): void {
23 const c = theme.colors
24 on('ui.render', { component: 'Pane', requestId: JEV_PANE }, async ($, e) => {
25 const jev = await read($, jevA)
26 return drawJevPane($.ui.resolve(e), { jev }, e.props.bodyColumns, e.props.scroll.bodyRows, forSurface(theme, e.surface))
27 }).catch(($, e, next) => {
28 const { Box, Text } = $.ui.resolve(e)
29 return (
30 <Box flexDirection="column" backgroundColor={c.surface} paddingX={1}>
31 <Text color={c.critical} bold>
32 mission-control: вкладка «{theme.titles.jev}» упала
33 </Text>
34 <Text color={c.ink2} wrap="wrap">
35 {reason(next.error)}
36 </Text>
37 </Box>
38 )
39 })
40}
41hooks/jev/model.ts 191 lines1// The Jev tab's model: pure functions from the journal's text and `mb jev status --json`
2// to rows and totals. No `$` here: the poll feeds text in, the tests call these directly.
3//
4// The contract is the engine's journal (jev-plan §4): one JSON object per line, append-only,
5// at `<vault>/.mb/machines/<host>/jev/log.jsonl`, rotated to `log.1.jsonl` past 5 MiB.
6// A line the engine is still writing, or one that is not an object, is skipped, never fatal.
7
8import type { JevEntry, JevStatus, JevSummary } from '../../types'
9import { clip } from '../mission/model'
10
11/** The journal lines shown, newest first. */
12export const SHOWN = 50
13/** `$.fs.read` has no range and stops at 4 MiB: a larger journal is not read, its newest lines come from `mb jev log --tail`. */
14export const FULL_READ_MAX = 4_000_000
15/** The journal is looked at again this often. */
16export const POLL_MS = 3000
17export const HEAD_MAX = 80
18/** The size classes the engine writes, in the order they are counted. */
19export const SIZES = ['tiny', 'small', 'medium', 'large'] as const
20
21function num(v: unknown): number | null {
22 return typeof v === 'number' && Number.isFinite(v) ? v : null
23}
24
25function str(v: unknown): string | null {
26 return typeof v === 'string' ? v : null
27}
28
29/** One journal line as an entry; null when it is not a JSON object (a half-written line). */
30export function parseLine(line: string): JevEntry | null {
31 const s = line.trim()
32 if (s.length === 0 || !s.startsWith('{')) return null
33 let o: unknown
34 try {
35 o = JSON.parse(s)
36 } catch {
37 return null
38 }
39 if (typeof o !== 'object' || o === null || Array.isArray(o)) return null
40 const r = o as Record<string, unknown>
41 const decided = r.decided === 'jev' || r.decided === 'self' ? r.decided : null
42 return {
43 ts: str(r.ts) ?? '',
44 session: str(r.session),
45 kind: str(r.kind) ?? '?',
46 head: clip((str(r.head) ?? '').replace(/\s+/g, ' ').trim(), HEAD_MAX),
47 size: str(r.size),
48 confidence: num(r.confidence),
49 decided,
50 route: str(r.route),
51 // A cost that is not a number is no cost: it never poisons the sum.
52 cost: Math.max(0, num(r.cost) ?? 0),
53 ms: num(r.ms),
54 error: str(r.error),
55 test: r.test === true,
56 }
57}
58
59/**
60 * The entries of a journal's text, oldest first. `isCut` says the text begins mid-file
61 * (a tail), so its first line may be a stump and is dropped. A last line without a
62 * newline is kept only when it parses whole.
63 */
64export function parseJournal(text: string, isCut = false): JevEntry[] {
65 const lines = text.split('\n')
66 if (isCut) lines.shift()
67 const out: JevEntry[] = []
68 for (const line of lines) {
69 const e = parseLine(line)
70 if (e !== null) out.push(e)
71 }
72 return out
73}
74
75/** Totals over entries; `session` limits the session figure to that id (null: no session known). */
76export function summarize(entries: readonly JevEntry[], session: string | null): JevSummary {
77 const bySize: Record<string, number> = {}
78 let total = 0
79 let current = 0
80 let self = 0
81 let errors = 0
82 let sessionCalls = 0
83 for (const e of entries) {
84 total += e.cost
85 if (session !== null && e.session === session) {
86 current += e.cost
87 sessionCalls += 1
88 }
89 if (e.size !== null) bySize[e.size] = (bySize[e.size] ?? 0) + 1
90 if (e.decided === 'self') self += 1
91 if (e.error !== null) errors += 1
92 }
93 return { calls: entries.length, sessionCalls, sessionCost: current, totalCost: total, bySize, self, errors }
94}
95
96/** The newest `limit` entries, newest first. */
97export function newest(entries: readonly JevEntry[], limit = SHOWN): JevEntry[] {
98 return entries.slice(-limit).reverse()
99}
100
101/** Budget minus what was spent; null when no budget is set. */
102export function balanceOf(budget: number | null, totalCost: number): number | null {
103 return budget === null ? null : budget - totalCost
104}
105
106/** A cost in dollars: six decimals; zero is `$0`. */
107export function fmtCost(usd: number): string {
108 return usd === 0 ? '$0' : `$${usd.toFixed(6)}`
109}
110
111/** A balance can be negative; two decimals would hide the small calls, so it has six as well. */
112export function fmtMoney(usd: number): string {
113 return `${usd < 0 ? '-' : ''}$${Math.abs(usd).toFixed(6)}`
114}
115
116/** `2026-10-20T11:02:03.123+03:00` → `10-20 11:02:03`; what is not that stays as it is. */
117export function fmtTime(ts: string): string {
118 const m = /^\d{4}-(\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})/.exec(ts)
119 return m === null ? ts : `${m[1]} ${m[2]}`
120}
121
122/** The first of `keys` present in `o` as a value of the wanted type. */
123function pick<T>(o: Record<string, unknown>, keys: readonly string[], ok: (v: unknown) => v is T): T | null {
124 for (const k of keys) {
125 const v = o[k]
126 if (ok(v)) return v
127 }
128 return null
129}
130
131const isBool = (v: unknown): v is boolean => typeof v === 'boolean'
132const isStr = (v: unknown): v is string => typeof v === 'string' && v.length > 0
133const isNum = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v)
134
135/** `mb jev status --json`, by the engine's names. A key is only ever a yes/no here. */
136export function parseStatus(text: string): JevStatus | null {
137 let o: unknown
138 try {
139 o = JSON.parse(text)
140 } catch {
141 return null
142 }
143 if (typeof o !== 'object' || o === null || Array.isArray(o)) return null
144 const r = o as Record<string, unknown>
145 return {
146 enabled: pick(r, ['enabled'], isBool),
147 keyFound: pick(r, ['key_found'], isBool),
148 model: pick(r, ['model'], isStr),
149 budget: pick(r, ['budget_usd'], isNum),
150 journal: pick(r, ['journal'], isStr),
151 totalCost: pick(r, ['total_cost'], isNum),
152 sessionCost: pick(r, ['session_cost'], isNum),
153 balance: pick(r, ['balance_usd'], isNum),
154 }
155}
156
157/** The three figures: the engine's (`mb jev status --json`) when it gives them, else our own sums. */
158export function figuresOf(summary: JevSummary, status: JevStatus | null): { totalCost: number; sessionCost: number; balance: number | null } {
159 const totalCost = status?.totalCost ?? summary.totalCost
160 return {
161 totalCost,
162 sessionCost: status?.sessionCost ?? summary.sessionCost,
163 balance: status?.balance ?? balanceOf(status?.budget ?? null, totalCost),
164 }
165}
166
167/** The vault from `~/.singularity.json` (key `vault_path`); null when absent. */
168export function vaultOf(configText: string): string | null {
169 try {
170 const o = JSON.parse(configText) as Record<string, unknown>
171 return isStr(o.vault_path) ? o.vault_path : null
172 } catch {
173 return null
174 }
175}
176
177/** The engine's machine id (config.machine_id): the host name, anything outside `A-Za-z0-9_.-` an underscore, 64 at most. */
178export function machineId(host: string): string {
179 return host.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64) || 'unknown'
180}
181
182/** Where the engine writes the journal, derived the way it does. */
183export function journalPathOf(vault: string, host: string): string {
184 return `${vault.replace(/[\\/]+$/, '')}/.mb/machines/${machineId(host)}/jev/log.jsonl`
185}
186
187/** The rotated generation beside a journal. */
188export function previousOf(path: string): string {
189 return path.replace(/log\.jsonl$/, 'log.1.jsonl')
190}
191hooks/memory/hooks.tsx 538 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, On } from 'claude-code'
3
4import type { Alerts, HookInjection, MbCall, Usage } from '../../types'
5import { estimateTokens, findMbInvocations, fmtTokens, hashText, matchesHitPath, parseSearchOutput, shortEvent } from '../lib/parse'
6import type { MbInvocation } from '../lib/parse'
7import { BOOT_BLESS_MS, BOOT_MS, BOOT_STEP_MS, bootElapsed } from '../lib/boot'
8import { branchOf, drawHerald, homeShort } from '../lib/herald'
9import { afterOwnerPrompt } from '../lead/council'
10import { COUNCIL_COMMANDS } from '../lead/guard'
11import { leadConfigOf } from '../lead/config'
12import type { LeadConfig } from '../lead/config'
13import { takeDigest } from '../lead/digest'
14import { EMPTY_LEAD, pushNews } from '../lead/ledger'
15import { drawsPictures, loadPixmap } from '../lib/terminal'
16import { forSurface } from '../lib/theme'
17import type { Theme } from '../lib/theme'
18import { drawMemoryPane } from './pane'
19
20// The memory feature's own observers and drawings. What it shares with mission
21// control (session.start, turn.complete, /mem, the vault alerts) lives in
22// register.tsx: the engine takes one unmatched hook per event per module and
23// never follows `$` into an imported function.
24
25export const MEMORY_PANE = 'mem-meter'
26const MAX_HOOKS = 100
27const MAX_CALLS = 200
28
29/** Hard alerts: a figure past these shows on the band even while the pane is up. */
30const CTX_ALERT = 80
31const FIVE_HOUR_ALERT = 80
32const SEVEN_DAY_ALERT = 90
33
34// The engine's scan accepts only an atom made in the same file: every file declares
35// the values it reads or writes, with the same key and initial value as the others.
36const agentsA = atom({ plugin: 'mission-control', key: 'agents' } as const, [])
37const hooksA = atom({ plugin: 'mission-control', key: 'hooks' } as const, [])
38const callsA = atom({ plugin: 'mission-control', key: 'calls' } as const, [])
39const readsA = atom({ plugin: 'mission-control', key: 'reads' } as const, [])
40const claudeMdA = atom({ plugin: 'mission-control', key: 'claudeMd' } as const, [])
41const turnsA = atom({ plugin: 'mission-control', key: 'turns' } as const, [])
42const turnA = atom({ plugin: 'mission-control', key: 'turn' } as const, 0)
43const usageA = atom({ plugin: 'mission-control', key: 'usage' } as const, null)
44const alertsA = atom({ plugin: 'mission-control', key: 'alerts' } as const, null)
45const hiddenA = atom({ plugin: 'mission-control', key: 'isHidden' } as const, false)
46const quietA = atom({ plugin: 'mission-control', key: 'isQuiet' } as const, false)
47const lastEndA = atom({ plugin: 'mission-control', key: 'lastTurnEndAt' } as const, null)
48const leadA = atom({ plugin: 'mission-control', key: 'lead' } as const, EMPTY_LEAD)
49
50type $ = EngineInterface
51
52function reason(err: unknown): string {
53 return err instanceof Error ? err.message : String(err)
54}
55
56async function recordInjection($: $, event: string, text: string): Promise<void> {
57 const id = hashText(`${event}\n${text}`)
58 const turn = await read($, turnA)
59 const row: HookInjection = {
60 id,
61 event,
62 tokens: estimateTokens(text),
63 chars: text.length,
64 preview: text.split('\n').find(line => line.trim().length > 0)?.trim().slice(0, 120) ?? '',
65 turn,
66 }
67 await update($, hooksA, list => (list.some(inj => inj.id === id) ? list : [...list, row].slice(-MAX_HOOKS)))
68}
69
70async function recordMbCall(
71 $: $,
72 id: string,
73 invocations: MbInvocation[],
74 text: string,
75 flags: { isError: boolean; isAgent: boolean },
76): Promise<void> {
77 const isSearch = invocations.some(i => i.kind === 'search')
78 const parsed = isSearch ? parseSearchOutput(text, invocations.every(i => i.isCodeOnly)) : null
79 const call: MbCall = {
80 id,
81 kind: isSearch ? 'search' : 'code',
82 queries: invocations.map(i => i.query),
83 hits: parsed?.hits ?? null,
84 memHits: parsed?.memHits ?? 0,
85 codeHits: parsed?.codeHits ?? 0,
86 best: parsed?.best ?? null,
87 tokens: estimateTokens(text),
88 paths: parsed?.paths ?? [],
89 turn: await read($, turnA),
90 isAgent: flags.isAgent,
91 isError: flags.isError,
92 }
93 await update($, callsA, list => [...list.filter(c => c.id !== id), call].slice(-MAX_CALLS))
94}
95
96/** Is the Memory tab on screen right now (open, placed and shown)? Unknown counts as no: the band then carries the meter. */
97async function isPaneUp($: $): Promise<boolean> {
98 try {
99 return (await $.ui.panes()).some(p => p.id === MEMORY_PANE && p.isPlaced && p.isShown)
100 } catch {
101 return false
102 }
103}
104
105function sum(list: readonly { tokens: number }[]): number {
106 return list.reduce((acc, x) => acc + x.tokens, 0)
107}
108
109/** Totals and counts per settings-hook event, in first-seen order. */
110function byEvent(hooks: readonly HookInjection[]): { event: string; tokens: number; count: number }[] {
111 const totals = new Map<string, { tokens: number; count: number }>()
112 for (const inj of hooks) {
113 const t = totals.get(inj.event) ?? { tokens: 0, count: 0 }
114 totals.set(inj.event, { tokens: t.tokens + inj.tokens, count: t.count + 1 })
115 }
116 return [...totals.entries()].map(([event, t]) => ({ event, ...t }))
117}
118
119type AlertItem = { text: string; level: 'error' | 'warning' | 'soft' }
120
121function hardAlerts(usage: Usage | null, alerts: Alerts | null): AlertItem[] {
122 const items: AlertItem[] = []
123 if (usage?.percent != null && usage.percent >= CTX_ALERT) {
124 items.push({ text: `контекст ${usage.percent}%`, level: usage.percent >= 90 ? 'error' : 'warning' })
125 }
126 if (usage?.fiveHour != null && usage.fiveHour >= FIVE_HOUR_ALERT) {
127 items.push({ text: `5h ${Math.round(usage.fiveHour)}%`, level: usage.fiveHour >= 95 ? 'error' : 'warning' })
128 }
129 if (usage?.sevenDay != null && usage.sevenDay >= SEVEN_DAY_ALERT) {
130 items.push({ text: `7d ${Math.round(usage.sevenDay)}%`, level: 'warning' })
131 }
132 if (alerts !== null && alerts.doctorFail > 0) items.push({ text: `doctor FAIL×${alerts.doctorFail}`, level: 'error' })
133 return items
134}
135
136function softAlerts(alerts: Alerts | null): AlertItem[] {
137 if (alerts === null) return []
138 const items: AlertItem[] = []
139 if (alerts.plans > 0) items.push({ text: `compaction-планов ${alerts.plans}`, level: 'soft' })
140 if (alerts.doctorWarn > 0) items.push({ text: `doctor WARN×${alerts.doctorWarn}`, level: 'soft' })
141 if (alerts.error !== null) items.push({ text: `vault не прочитан: ${alerts.error}`, level: 'soft' })
142 return items
143}
144
145/** What the herald's card says of the session, beyond the memory figures. */
146type HeraldFacts = { version: string | null; model: string | null; place: string | null; branch: string | null }
147
148/** The engine's version, read once: a module value, a reload reads again. */
149let heraldVersion: string | null = null
150
151async function attempt<T>(read: () => Promise<T>): Promise<T | null> {
152 try {
153 return await read()
154 } catch {
155 return null
156 }
157}
158
159async function readFile($: $, path: string): Promise<string | null> {
160 const body = await attempt(() => $.fs.read(path))
161 return typeof body === 'string' ? body : null
162}
163
164/**
165 * The branch checked out under `root`, from `.git/HEAD` (a worktree's `.git` file names
166 * its own folder), else from the repository's root: a file read, never a git process,
167 * so the band's drawing waits on nothing. Null outside a repository.
168 */
169async function readBranch($: $, root: string): Promise<string | null> {
170 const repoRoot = (await attempt(() => $.session.repo()))?.root ?? null
171 for (const dir of repoRoot === null || repoRoot === root ? [root] : [root, repoRoot]) {
172 let head = await readFile($, `${dir}/.git/HEAD`)
173 if (head === null) {
174 const gitdir = /^gitdir:\s*(.+)$/m.exec((await readFile($, `${dir}/.git`)) ?? '')?.[1]?.trim()
175 if (gitdir !== undefined) head = await readFile($, /^([\\/]|[A-Za-z]:)/.test(gitdir) ? `${gitdir}/HEAD` : `${dir}/${gitdir}/HEAD`)
176 }
177 if (head !== null) return branchOf(head)
178 }
179 return null
180}
181
182/**
183 * The session's facts for the herald's card, or null once the person has sent a prompt
184 * (the card is the empty conversation's) or when the count cannot be read.
185 */
186async function heraldFacts($: $): Promise<HeraldFacts | null> {
187 const turns = await attempt(() => $.session.turns())
188 if (turns === null || turns > 0) return null
189 const root = await attempt(() => $.session.root())
190 heraldVersion ??= (await attempt(() => $.session.version()))?.version ?? null
191 const home = (await attempt(() => $.env.get('HOME'))) ?? (await attempt(() => $.env.get('USERPROFILE'))) ?? null
192 return {
193 version: heraldVersion,
194 model: await attempt(() => $.session.model()),
195 place: root === null ? null : homeShort(root, home),
196 branch: root === null ? null : await readBranch($, root),
197 }
198}
199
200/** Everything the band and the Memory tab read, in one place. */
201async function memoryData($: $) {
202 const hooks = await read($, hooksA)
203 const calls = await read($, callsA)
204 const reads = await read($, readsA)
205 const claudeMd = await read($, claudeMdA)
206 const turns = await read($, turnsA)
207 const usage = await read($, usageA)
208 const alerts = await read($, alertsA)
209 const searches = calls.filter(c => c.kind === 'search')
210 const hits = searches.reduce((acc, c) => acc + (c.hits ?? 0), 0)
211 const best = searches.reduce<number | null>((acc, c) => (c.best !== null && (acc === null || c.best > acc) ? c.best : acc), null)
212 const codeQueries = calls.filter(c => c.kind === 'code').length + searches.filter(c => c.codeHits > 0).length
213 const shown = new Set(calls.flatMap(c => c.paths)).size
214 const turn = await read($, turnA)
215 const lastTurnEndAt = await read($, lastEndA)
216 const now = await $.clock.now()
217 return { hooks, calls, reads, claudeMd, turns, usage, alerts, searches, hits, best, codeQueries, shown, turn, lastTurnEndAt, now }
218}
219
220export function registerMemory(
221 on: On,
222 theme: Theme,
223 sealFile: string | null = null,
224 crestFile: string | null = null,
225 leadConfig: LeadConfig = leadConfigOf({}),
226): void {
227 const pal = theme.colors
228 on('prompt.submit', async ($, e, next) => {
229 await update($, turnA, n => n + 1)
230 // The person's own prompt carries what changed for the lead since the last digest (spec 6.6); a plugin's
231 // wake prompt does not take it. A throw leaves the prompt as it was.
232 let input = e
233 // A slash command is not a message to the lead: it leaves the digest for the next one.
234 if (e.command === undefined && (e.origin.kind === 'composer' || e.origin.kind === 'bridge')) {
235 try {
236 const out: { text: string | null } = { text: null }
237 await update($, leadA, cur => {
238 const taken = takeDigest(cur, leadConfig)
239 out.text = taken.text
240 return taken.lead
241 })
242 if (out.text !== null) input = { ...e, context: [...(e.context ?? []), out.text] }
243 } catch (err) {
244 try {
245 await update($, leadA, cur =>
246 cur.flags.includes('guard-error')
247 ? cur
248 : { ...pushNews(cur, `сводка упала: ${reason(err)}`), flags: [...cur.flags, 'guard-error'] },
249 )
250 } catch {
251 // nothing more to do
252 }
253 }
254 }
255 // The window ends when the owner speaks again (spec 6.7, R25): their own prompt that is not a council command,
256 // with no council member still working. Closed before the prompt enters, so its turn already finds it shut.
257 try {
258 const rows = await read($, agentsA)
259 await update($, leadA, cur => afterOwnerPrompt(cur, rows, { origin: e.origin.kind, command: e.command?.name }))
260 } catch {
261 // the window stays as it was
262 }
263 const res = await next(input)
264 // A council command opens the window once its prompt has entered (a refused one opens nothing).
265 const command = e.command?.name
266 if (res.drop === undefined && command !== undefined && COUNCIL_COMMANDS.includes(command)) {
267 await update($, leadA, l => ({ ...l, councilTurn: command }))
268 }
269 return res
270 }).catch(($, e, next) => next(e))
271
272 // Every text the engine injects for the model, raised on every request that carries it
273 // (seen live: SessionStart's text arrived after a mid-session load); only a settings
274 // hook's (Singularity's) is memory, and the hash keeps each one counted once.
275 on('prompt.attachment', async ($, e, next) => {
276 const answer = await next(e)
277 if (e.origin.kind === 'hook' && answer.text !== null && answer.text.length > 0) {
278 await recordInjection($, e.origin.event, answer.text)
279 }
280 return answer
281 }).catch(($, e, next) => next(e))
282
283 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
284 const invocations = findMbInvocations(e.command)
285 if (invocations.length === 0) return next(e)
286 const ran = await next(e)
287 if (ran.deny === undefined) {
288 await recordMbCall($, e.tool_use_id, invocations, ran.text ?? '', {
289 isError: ran.isError === true,
290 isAgent: e.agentId !== undefined,
291 })
292 }
293 return ran
294 }).catch(($, e, next) => next(e))
295
296 // A pattern, not the name: PowerShell is a built-in on Windows builds only, and this
297 // build's types would refuse a name they do not list.
298 on('tool.call', { tool: /^PowerShell$/ }, async ($, e, next) => {
299 const command = (e as { command?: unknown }).command
300 const invocations = findMbInvocations(typeof command === 'string' ? command : '')
301 if (invocations.length === 0) return next(e)
302 const ran = await next(e)
303 if (ran.deny === undefined) {
304 await recordMbCall($, e.tool_use_id, invocations, ran.text ?? '', {
305 isError: ran.isError === true,
306 isAgent: e.agentId !== undefined,
307 })
308 }
309 return ran
310 }).catch(($, e, next) => next(e))
311
312 // A Read of a file a search returned: the hit was used, not only listed.
313 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
314 const ran = await next(e)
315 if (ran.deny !== undefined || ran.isError === true) return ran
316 const hit = matchesHitPath(e.file_path, (await read($, callsA)).flatMap(c => c.paths))
317 if (hit !== undefined) await update($, readsA, list => (list.includes(hit) ? list : [...list, hit]))
318 return ran
319 }).catch(($, e, next) => next(e))
320
321 on('session.measure', async ($, e, next) => {
322 const answer = await next(e)
323 const window = (kind: string): number | null => e.rateLimits.find(w => w.kind === kind)?.percentUsed ?? null
324 const usage: Usage = {
325 tokens: e.context.tokens ?? null,
326 percent: e.context.percent ?? null,
327 window: e.context.window,
328 fiveHour: window('five_hour'),
329 sevenDay: window('seven_day'),
330 }
331 await update($, usageA, () => usage)
332 return answer
333 }).catch(($, e, next) => next(e))
334
335 // The band: hard alerts while the Memory tab is up; without it, the one-line meter as well.
336 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
337 if (e.props.hasSurvey || (await read($, hiddenA))) return next(e)
338 const isPane = await isPaneUp($)
339 const s = await memoryData($)
340 const items = [...hardAlerts(s.usage, s.alerts), ...(isPane || (await read($, quietA)) ? [] : softAlerts(s.alerts))]
341
342 // The herald: while the conversation is empty, the slate's own card under the engine's
343 // logo (the aquila, then the session), the awakening's gauge on its strip. It gives
344 // way to the usual band once the first prompt is sent.
345 const heraldSlate = e.surface === 'terminal' && !e.props.isWorking && e.props.view.agentId === undefined ? theme.slate : null
346 const facts = heraldSlate === null ? null : await heraldFacts($)
347 if (heraldSlate !== null && facts !== null) {
348 const booting = bootElapsed(s.now)
349 const pictures = await drawsPictures($)
350 return drawHerald(
351 $.ui.resolve(e),
352 {
353 cols: e.props.bodyColumns,
354 maxRows: e.props.maxRows,
355 now: s.now,
356 booting,
357 bootFill: booting === null ? 1 : Math.min(1, booting / (BOOT_BLESS_MS - 300)),
358 ...facts,
359 hookTokens: sum(s.hooks),
360 claudeMdTokens: s.claudeMd.length > 0 ? sum(s.claudeMd) : null,
361 alarms: [...hardAlerts(s.usage, s.alerts), ...softAlerts(s.alerts)].map(item => item.text),
362 fiveHour: s.usage?.fiveHour ?? null,
363 sevenDay: s.usage?.sevenDay ?? null,
364 pictures,
365 crestFile,
366 crestPix: !pictures && crestFile !== null ? await loadPixmap($, crestFile) : null,
367 },
368 theme,
369 heraldSlate,
370 )
371 }
372
373 // With the tab up and nothing alarming the band yields, unless the awakening plays.
374 const awakening = e.surface === 'terminal' && theme.slate !== null && bootElapsed(await $.clock.now()) !== null
375 if (isPane && items.length === 0 && !awakening) return next(e)
376 const { Box, Text } = $.ui.resolve(e)
377 const events = byEvent(s.hooks).map(ev => `${shortEvent(ev.event)} ${fmtTokens(ev.tokens)}`).join(' · ')
378 const last = s.turns.at(-1)
379
380 // The slate's band, in the status line's manner: each part opens on a brass plate,
381 // its figures follow in amber, the alerts on a plate of their own colour.
382 const slate = e.surface === 'terminal' ? theme.slate : null
383 // The awakening plays first, whatever the tab: the boot log of the cogitator.
384 const booting = slate === null ? null : bootElapsed(await $.clock.now())
385 if (slate !== null && booting !== null) {
386 const fill = Math.min(1, booting / (BOOT_BLESS_MS - 300))
387 const width = 24
388 const full = Math.round(fill * width)
389 const alarms = hardAlerts(s.usage, s.alerts).length + softAlerts(s.alerts).length
390 const steps = [
391 'литания пуска прочитана',
392 `архивы вскрыты · хуки ≈${fmtTokens(sum(s.hooks))}`,
393 `CLAUDE.md ${s.claudeMd.length > 0 ? fmtTokens(sum(s.claudeMd)) : '?'}`,
394 'легион построен',
395 alarms === 0 ? 'тревог нет' : `тревог: ${alarms}`,
396 ].slice(0, Math.min(5, Math.floor(booting / BOOT_STEP_MS) + 1))
397 const blessed = booting >= BOOT_BLESS_MS
398 return (
399 <Box flexDirection="column">
400 <Text wrap="truncate-end">
401 <Text backgroundColor={blessed ? pal.accent : slate.frame} color={pal.surface} bold>
402 {blessed ? ' ✠ ОМНИССИЯ БЛАГОСЛОВЛЯЕТ ✠ ' : ' +++ ИНИЦИАЦИЯ ДУХА МАШИНЫ +++ '}
403 </Text>
404 <Text> </Text>
405 <Text color={pal.accent}>{'█'.repeat(full)}</Text>
406 <Text color={pal.track}>{'█'.repeat(width - full)}</Text>
407 <Text color={pal.ink} bold>
408 {' '}
409 {Math.round(fill * 100)}%
410 </Text>
411 <Text color={pal.muted}> · {Math.max(0, Math.ceil((BOOT_MS - booting) / 1000))}с</Text>
412 </Text>
413 <Text wrap="truncate-end">
414 {steps.map((step, i) => (
415 <Text>
416 <Text color={pal.muted}>{i > 0 ? ' · ' : ''}</Text>
417 <Text color={pal.good}>{theme.icons.ok} </Text>
418 <Text color={i === steps.length - 1 && !blessed ? pal.ink : pal.ink2}>{step}</Text>
419 </Text>
420 ))}
421 </Text>
422 </Box>
423 )
424 }
425 if (slate !== null) {
426 const plate = (text: string, ground: string = slate.frame) => (
427 <Text backgroundColor={ground} color={pal.surface} bold>
428 {` ${text} `}
429 </Text>
430 )
431 const dot = <Text color={pal.muted}> · </Text>
432 return (
433 <Box flexDirection="column">
434 {!isPane && (
435 <Text wrap="truncate-end">
436 {plate('АРХИВЫ')}
437 <Text color={pal.muted}> хуки </Text>
438 <Text color={pal.ink}>≈{fmtTokens(sum(s.hooks))}</Text>
439 {events.length > 0 && <Text color={pal.muted}> ({events})</Text>}
440 {dot}
441 <Text color={pal.muted}>CLAUDE.md </Text>
442 <Text color={pal.ink}>{s.claudeMd.length > 0 ? fmtTokens(sum(s.claudeMd)) : '?'}</Text>
443 {dot}
444 <Text color={pal.muted}>поиск </Text>
445 <Text color={s.searches.length > 0 ? pal.search : pal.ink}>×{s.searches.length}</Text>
446 {s.searches.length > 0 && (
447 <Text color={pal.muted}>
448 {' '}({s.hits} найдено · лучший {s.best === null ? '—' : s.best.toFixed(2)} · ≈{fmtTokens(sum(s.searches))} · прочит {s.reads.length}/{s.shown})
449 </Text>
450 )}
451 {dot}
452 <Text color={pal.muted}>код </Text>
453 <Text color={pal.ink}>×{s.codeQueries}</Text>
454 {last !== undefined && last.searches === 0 && <Text color={pal.warning}> {theme.icons.warn} прошлый ход без поиска</Text>}
455 </Text>
456 )}
457 {items.length > 0 && (
458 <Text wrap="truncate-end">
459 {plate(`${theme.icons.warn} ТРЕВОГИ`, items.some(item => item.level === 'error') ? pal.critical : pal.warning)}{' '}
460 {items.map((item, i) => (
461 <Text color={item.level === 'soft' ? pal.muted : item.level === 'error' ? pal.critical : pal.warning}>
462 {i > 0 ? ' · ' : ''}
463 {item.text}
464 </Text>
465 ))}
466 </Text>
467 )}
468 </Box>
469 )
470 }
471
472 return (
473 <Box flexDirection="column">
474 {!isPane && (
475 <Text wrap="truncate-end">
476 <Text dimColor>{theme.words.band}</Text>
477 <Text>хуки ≈{fmtTokens(sum(s.hooks))}</Text>
478 {events.length > 0 && <Text dimColor> ({events})</Text>}
479 <Text dimColor> │ </Text>
480 <Text>CLAUDE.md {s.claudeMd.length > 0 ? fmtTokens(sum(s.claudeMd)) : '?'}</Text>
481 <Text dimColor> │ </Text>
482 <Text color={s.searches.length > 0 ? pal.search : pal.ink2}>поиск ×{s.searches.length}</Text>
483 {s.searches.length > 0 && (
484 <Text dimColor>
485 {' '}· {s.hits} hits · best {s.best === null ? '—' : s.best.toFixed(2)} · ≈{fmtTokens(sum(s.searches))} · прочит {s.reads.length}/{s.shown}
486 </Text>
487 )}
488 <Text dimColor> │ </Text>
489 <Text>код ×{s.codeQueries}</Text>
490 {last !== undefined && last.searches === 0 && <Text color={pal.warning}> │ {theme.icons.warn} прошлый ход без поиска</Text>}
491 </Text>
492 )}
493 {items.length > 0 && (
494 <Text wrap="truncate-end">
495 <Text dimColor>{theme.icons.warn} </Text>
496 {items.map((item, i) => (
497 <Text color={item.level === 'soft' ? pal.muted : item.level === 'error' ? pal.critical : pal.warning}>
498 {i > 0 ? ' · ' : ''}
499 {item.text}
500 </Text>
501 ))}
502 </Text>
503 )}
504 </Box>
505 )
506 }).catch(($, e, next) => {
507 // A drawing that throws must say so where the person looks, not leave the slot silently empty.
508 const { Text } = $.ui.resolve(e)
509 return (
510 <Text color="error" wrap="truncate-end">
511 mission-control: полоса памяти упала: {reason(next.error)}
512 </Text>
513 )
514 })
515
516 // The Memory tab: a dark surface filling the column, drawn by pane.tsx from the session's figures.
517 on('ui.render', { component: 'Pane', requestId: MEMORY_PANE }, async ($, e) => {
518 const s = await memoryData($)
519 // The purity seal as a picture where the terminal draws them, else its pixmap in half blocks.
520 const pictures = sealFile !== null && (await drawsPictures($))
521 const seal = pictures ? sealFile : null
522 const sealPix = sealFile !== null && !pictures ? await loadPixmap($, sealFile) : null
523 return drawMemoryPane($.ui.resolve(e), { ...s, sealFile: seal, sealPix }, e.props.bodyColumns, e.props.scroll.bodyRows, forSurface(theme, e.surface))
524 }).catch(($, e, next) => {
525 const { Box, Text } = $.ui.resolve(e)
526 return (
527 <Box flexDirection="column" backgroundColor={pal.surface} paddingX={1}>
528 <Text color={pal.critical} bold>
529 mission-control: вкладка «{theme.titles.memory}» упала
530 </Text>
531 <Text color={pal.ink2} wrap="wrap">
532 {reason(next.error)}
533 </Text>
534 </Box>
535 )
536 })
537}
538hooks/lib/boot.ts 25 lines1// The slate's awakening: for a few seconds after the session starts (or the mod reloads),
2// the band above the prompt plays the cogitator's boot. When it began is a module value
3// every file of the module shares; a reload starts it over, as a reload is a waking too.
4
5/** How long the band plays the awakening. */
6export const BOOT_MS = 4500
7/** Each step of the boot log shows this long after the one before. */
8export const BOOT_STEP_MS = 600
9/** When the blessing replaces the gauge. */
10export const BOOT_BLESS_MS = 3300
11
12let bootAt: number | null = null
13
14/** The awakening begins now. */
15export function markBoot(now: number): void {
16 bootAt = now
17}
18
19/** Milliseconds into the awakening at `now`, or null when none is playing. */
20export function bootElapsed(now: number): number | null {
21 if (bootAt === null) return null
22 const t = now - bootAt
23 return t >= 0 && t < BOOT_MS ? t : null
24}
25hooks/lib/cog.ts 100 lines1// A Mechanicus cog as a Raster's cells: drawn at any angle, two pixels to a cell (a
2// half block, its top pixel in the foreground and its bottom one in the background),
3// so it turns smoothly when the Agents tab blits a frame after a frame. Pure: numbers
4// and strings, no `$`.
5
6/** The terminal's default colour in a Raster cell (bit 24 alone). */
7const DEFAULT = 0x01000000
8const UPPER = 0x2580 // ▀
9const LOWER = 0x2584 // ▄
10const SPACE = 0x20
11
12/** Teeth on the rim. */
13const TEETH = 8
14
15export type CogPalette = { light: number; mid: number; dark: number; hub: number }
16
17/** '#a8823c' → 0xa8823c. */
18export function hex(color: string): number {
19 return Number.parseInt(color.replace('#', ''), 16) & 0xffffff
20}
21
22/**
23 * The colour of the cog's pixel at (x, y) in a `size` × `size` square, or null where
24 * the cog is not: a rim with teeth, a dark ring, spokes and a hub, turned by `angle`
25 * (radians), lit from the upper left.
26 */
27export function cogPixel(x: number, y: number, size: number, angle: number, p: CogPalette): number | null {
28 const c = (size - 1) / 2
29 const dx = x - c
30 const dy = y - c
31 const r = Math.hypot(dx, dy) / (size / 2)
32 const a = Math.atan2(dy, dx) - angle
33 const tooth = Math.cos(a * TEETH) > 0.25
34 const outer = tooth ? 1.0 : 0.8
35 if (r > outer) return null
36 const lit = (-dx - dy) / (size * 0.7) // upper left is brighter
37 if (r > 0.62) return lit > 0.15 ? p.light : lit < -0.25 ? p.dark : p.mid
38 if (r > 0.5) return p.dark
39 // Four spokes turn with the rim; between them the wheel is open.
40 const spoke = Math.abs(Math.sin(a * 2)) < 0.38
41 if (r > 0.24) return spoke ? p.mid : null
42 return r > 0.13 ? p.hub : p.dark
43}
44
45/** Little-endian u32 words, standard padded base64, as a Raster's `cells` takes them. */
46export function packCells(words: readonly number[]): string {
47 const bytes = new Uint8Array(words.length * 4)
48 words.forEach((w, i) => {
49 bytes[i * 4] = w & 0xff
50 bytes[i * 4 + 1] = (w >>> 8) & 0xff
51 bytes[i * 4 + 2] = (w >>> 16) & 0xff
52 bytes[i * 4 + 3] = (w >>> 24) & 0xff
53 })
54 return toBase64(bytes)
55}
56
57const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
58
59function toBase64(bytes: Uint8Array): string {
60 let out = ''
61 for (let i = 0; i < bytes.length; i += 3) {
62 const a = bytes[i] ?? 0
63 const b = bytes[i + 1] ?? 0
64 const c = bytes[i + 2] ?? 0
65 const n = (a << 16) | (b << 8) | c
66 out += ALPHABET[(n >> 18) & 63] ?? ''
67 out += ALPHABET[(n >> 12) & 63] ?? ''
68 out += i + 1 < bytes.length ? (ALPHABET[(n >> 6) & 63] ?? '') : '='
69 out += i + 2 < bytes.length ? (ALPHABET[n & 63] ?? '') : '='
70 }
71 return out
72}
73
74/**
75 * One frame of the cog in a box of `columns` × `rows` cells (a square of `columns`
76 * pixels across and `rows * 2` down, the cog centred in it), as Raster `cells`.
77 */
78export function cogCells(columns: number, rows: number, angle: number, p: CogPalette): string {
79 const size = Math.min(columns, rows * 2)
80 const ox = Math.floor((columns - size) / 2)
81 const oy = Math.floor((rows * 2 - size) / 2)
82 const at = (x: number, y: number): number | null => {
83 const px = x - ox
84 const py = y - oy
85 if (px < 0 || py < 0 || px >= size || py >= size) return null
86 return cogPixel(px, py, size, angle, p)
87 }
88 const words: number[] = []
89 for (let row = 0; row < rows; row += 1) {
90 for (let col = 0; col < columns; col += 1) {
91 const top = at(col, row * 2)
92 const bottom = at(col, row * 2 + 1)
93 if (top !== null) words.push(UPPER, top, bottom ?? DEFAULT)
94 else if (bottom !== null) words.push(LOWER, bottom, DEFAULT)
95 else words.push(SPACE, DEFAULT, DEFAULT)
96 }
97 }
98 return packCells(words)
99}
100hooks/lib/frame.tsx 479 lines1import type { ElementTable } from 'claude-code'
2
3import { AQUILA_PIXMAP, AQUILA_PNG, AQUILA_PNG_SIZE } from './art'
4import { halfBlockCells, mosaicRuns } from './pixmap'
5import type { Pixmap } from './pixmap'
6import { cogCells, hex } from './cog'
7import type { CogPalette } from './cog'
8import type { Slate, Theme } from './theme'
9
10// The parts of a data-slate: framed panels with name plates, the crest, the status
11// strip, gauges and key caps. Every part is laid out here to an exact number of cells
12// and drawn one element per screen row, so a tab can count its rows and every right
13// edge lands on the same column. Pure: strings and trees, no `$`.
14
15// NB: never name a variable `h` in this module: JSX compiles to the global `h(...)`,
16// and a local `h` turns every element below it into "h is not a function".
17
18export type FrameStyle = 'double' | 'heavy' | 'light'
19
20type Glyphs = { tl: string; tr: string; bl: string; br: string; h: string; v: string; open: string; close: string }
21
22/** Corners, edges and the two halves of a plate (`╡ name ╞`) per frame style. */
23export const FRAME: Record<FrameStyle, Glyphs> = {
24 double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║', open: '╡', close: '╞' },
25 heavy: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃', open: '┫', close: '┣' },
26 light: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│', open: '┤', close: '├' },
27}
28
29/** Cells a string takes: every glyph a slate draws is one cell. */
30export function cells(text: string): number {
31 return [...text].length
32}
33
34/** Cut to `max` cells, an ellipsis in the last when it runs on. */
35export function fit(text: string, max: number): string {
36 const glyphs = [...text]
37 if (glyphs.length <= max) return text
38 if (max <= 0) return ''
39 return `${glyphs.slice(0, max - 1).join('')}…`
40}
41
42/** A run of an edge: `role` says how it is coloured. */
43export type Part = { text: string; role: 'frame' | 'title' | 'meta' }
44
45/**
46 * A top edge of `width` cells: `╔═╡ title ╞════╡ meta ╞═╗`. The meta plate goes first
47 * when the two do not fit, then the title is cut; never wider than `width`.
48 */
49export function edgeTop(style: FrameStyle, width: number, title: string, meta = ''): Part[] {
50 const g = FRAME[style]
51 const w = Math.max(4, width)
52 // ╔═╡ + ' title ' + ╞ … ╡ + ' meta ' + ╞═╗
53 const fixed = 3 + 1 + 1
54 let shownMeta = meta
55 let room = w - fixed - (shownMeta.length > 0 ? cells(shownMeta) + 4 : 0) - 1
56 if (shownMeta.length > 0 && room < Math.min(8, cells(title) + 2)) {
57 shownMeta = ''
58 room = w - fixed - 1
59 }
60 // No room for even « … » between the plate's halves: the edge goes without a title.
61 if (title.length === 0 || room < 3) {
62 const fill = w - 2 - (shownMeta.length > 0 ? cells(shownMeta) + 5 : 0)
63 const parts: Part[] = [{ text: g.tl + g.h.repeat(Math.max(0, fill)), role: 'frame' }]
64 if (shownMeta.length > 0) parts.push({ text: g.open, role: 'frame' }, { text: ` ${shownMeta} `, role: 'meta' }, { text: g.close + g.h, role: 'frame' })
65 parts.push({ text: g.tr, role: 'frame' })
66 return parts
67 }
68 const name = ` ${fit(title, Math.max(1, room - 2))} `
69 const metaCells = shownMeta.length > 0 ? cells(shownMeta) + 4 : 0
70 const fill = Math.max(0, w - 3 - cells(name) - 1 - metaCells - 2)
71 const parts: Part[] = [
72 { text: g.tl + g.h + g.open, role: 'frame' },
73 { text: name, role: 'title' },
74 { text: g.close + g.h.repeat(fill), role: 'frame' },
75 ]
76 if (shownMeta.length > 0) parts.push({ text: g.open, role: 'frame' }, { text: ` ${shownMeta} `, role: 'meta' }, { text: g.close, role: 'frame' })
77 parts.push({ text: g.h + g.tr, role: 'frame' })
78 return parts
79}
80
81/** A bottom edge of `width` cells, with an optional stamp near its right end: `╚════╡ 0x2A7F ╞═╝`. */
82export function edgeBottom(style: FrameStyle, width: number, stamp = ''): Part[] {
83 const g = FRAME[style]
84 const w = Math.max(4, width)
85 const stampCells = stamp.length > 0 ? cells(stamp) + 4 : 0
86 if (stamp.length === 0 || w - 4 - stampCells < 2) return [{ text: g.bl + g.h.repeat(w - 2) + g.br, role: 'frame' }]
87 return [
88 { text: g.bl + g.h.repeat(w - 4 - stampCells + 1), role: 'frame' },
89 { text: g.open, role: 'frame' },
90 { text: ` ${stamp} `, role: 'meta' },
91 { text: g.close + g.h + g.br, role: 'frame' },
92 ]
93}
94
95/** Greedy word wrap into lines of at most `width` cells; a word wider than a line is cut. */
96export function wrapLines(text: string, width: number, max = Number.POSITIVE_INFINITY): string[] {
97 const w = Math.max(4, width)
98 const lines: string[] = []
99 let line = ''
100 for (const word of text.split(/\s+/).filter(x => x.length > 0)) {
101 const candidate = line.length === 0 ? word : `${line} ${word}`
102 if (cells(candidate) <= w) {
103 line = candidate
104 continue
105 }
106 if (line.length > 0) lines.push(line)
107 line = cells(word) <= w ? word : fit(word, w)
108 }
109 if (line.length > 0) lines.push(line)
110 if (lines.length <= max) return lines
111 const kept = lines.slice(0, max)
112 kept[max - 1] = fit(`${kept[max - 1] ?? ''}…`, w)
113 return kept
114}
115
116const MIRROR: Record<string, string> = { '▌': '▐', '▐': '▌', '▖': '▗', '▗': '▖', '▘': '▝', '▝': '▘' }
117
118function mirror(text: string): string {
119 return [...text]
120 .reverse()
121 .map(ch => MIRROR[ch] ?? ch)
122 .join('')
123}
124
125const CREST_LEFT = ['▄▄▄▄▄▄▄▄▄▄▄▄▄ ▗▄▖', ' ▀▀███████████▄▄▐█▀', ' ▀▀▀▀▀██████▄▐█▄', ' ▀▀▀▀▀▀█▀']
126const CREST_MID = [' ', '▄', '✠', '█']
127
128/** The Imperial aquila, two heads and spread wings, built symmetric from its left half: 39 cells across. */
129export const CREST: readonly string[] = CREST_LEFT.map((left, i) => left + (CREST_MID[i] ?? ' ') + mirror(left))
130
131/** The crest centred in `width` cells; null when the column is narrower than it. */
132export function crestLines(width: number): string[] | null {
133 const across = Math.max(...CREST.map(cells))
134 if (width < across) return null
135 const left = Math.floor((width - across) / 2)
136 return CREST.map(row => ' '.repeat(left) + row.padEnd(across) + ' '.repeat(width - left - across))
137}
138
139/** The cog on the Agents tab: its Raster's key and box, turned a little more each frame while agents work. */
140export const COG = { key: 'cog', columns: 10, rows: 5 } as const
141
142/** A servo-skull in place of the cog: its Image's key and box, and the frames it cycles through. */
143export const SKULL = { key: 'skull', columns: 12, rows: 9, frames: 12, frameMs: 250 } as const
144
145/** The frame of the servo-skull at `now` in the folder `dir` (frame-00.png ...), the same on every redraw and blit. */
146export function skullFrame(dir: string, now: number): string {
147 const k = Math.floor(now / SKULL.frameMs) % SKULL.frames
148 return `${dir.replace(/[\\/]+$/, '')}/frame-${String(k).padStart(2, '0')}.png`
149}
150
151/** A rank's portrait on Легион: a square picture in a box of 6 x 3 cells, a cell's gap after it. */
152export const PORTRAIT = { columns: 6, rows: 3 } as const
153
154/** A purity seal's picture is 5 wide to 24 tall; the Archives tab sizes its box to the blocks beside it. */
155export const SEAL_ASPECT = { width: 5, height: 24 } as const
156
157/** Radians the cog has turned at `now`: a slow, steady turn, the same on every redraw and blit. */
158export function cogAngle(now: number): number {
159 return ((now / 1000) * 1.7) % (Math.PI * 2)
160}
161
162/** The cog in the slate's brass, its hub the red eye of the Omnissiah. */
163export function cogPalette(t: Theme, slate: Slate): CogPalette {
164 return { light: hex(t.colors.accent), mid: hex(slate.frame), dark: hex(t.colors.track), hub: hex(t.colors.critical) }
165}
166
167/** The context fill (percent) from which the slate shows the warp's taint. */
168export const WARP_AT = 80
169
170/** Is the Machine Spirit overloaded: the context at or past WARP_AT? */
171export function isWarped(percent: number | null | undefined): boolean {
172 return typeof percent === 'number' && percent >= WARP_AT
173}
174
175const NOISE = ['▓', '░', '▒', '#', '%', '¤', '†', '‡', '?']
176
177/** Binaric cant broken by the warp: a third of its cells turned to noise, the same within a second. */
178export function corruptCant(text: string, seed: number): string {
179 let n = Math.abs(Math.floor(seed)) % 2147483647 || 1
180 return [...text]
181 .map(ch => {
182 n = (n * 48271) % 2147483647
183 return ch !== ' ' && n % 3 === 0 ? (NOISE[n % NOISE.length] ?? ch) : ch
184 })
185 .join('')
186}
187
188const CANT_SOURCE = 'ОМНИССИЯ ВИДИТ ДУХ МАШИНЫ БДИТ ПЛОТЬ СЛАБА'
189
190/**
191 * A strip of binaric cant `width` cells long: bytes of a litany as eight-bit groups,
192 * a cross between every few, shifted by `offset` cells so it crawls as time passes.
193 */
194export function binaryCant(width: number, offset: number): string {
195 const groups: string[] = []
196 for (const ch of CANT_SOURCE) groups.push(((ch.codePointAt(0) ?? 0) & 0xff).toString(2).padStart(8, '0'))
197 const tape = groups.map((g, i) => (i % 4 === 3 ? `${g} ✠ ` : `${g} `)).join('')
198 const loop = [...tape]
199 const start = ((Math.floor(offset) % loop.length) + loop.length) % loop.length
200 let out = ''
201 for (let i = 0; i < width; i += 1) out += loop[(start + i) % loop.length] ?? ' '
202 return out
203}
204
205/**
206 * The date as the Imperium writes it: a check number, the part of the year passed in
207 * thousandths, the year of the millennium, the millennium. 2026-10-16 is «0 789 026.M3».
208 */
209export function imperialDate(now: number): string {
210 const d = new Date(now)
211 const year = d.getUTCFullYear()
212 const start = Date.UTC(year, 0, 1)
213 const length = Date.UTC(year + 1, 0, 1) - start
214 const part = Math.min(999, Math.floor(((now - start) / length) * 1000))
215 const millennium = Math.floor(year / 1000) + 1
216 return `0 ${String(part).padStart(3, '0')} ${String(year % 1000).padStart(3, '0')}.M${millennium}`
217}
218
219/** A reference stamp for a panel's bottom edge: four hex digits from `seed`, as a cogitator numbers its records. */
220export function stamp(prefix: string, seed: number): string {
221 const n = Math.abs(Math.floor(seed * 2654435761) % 0x10000)
222 return `${prefix} 0x${n.toString(16).toUpperCase().padStart(4, '0')}`
223}
224
225/**
226 * The parts drawn with the surface's elements: framed panels, the crest, the strip,
227 * the gauge and the key caps, all in the theme's palette and its slate's brass.
228 */
229export function slateKit(table: ElementTable, t: Theme, slate: Slate) {
230 const { Box, Text } = table
231 const c = t.colors
232
233 const edge = (parts: Part[], titleColor: string, bezel: string = slate.frame) => (
234 <Text wrap="truncate-end">
235 {parts.map(p => (
236 <Text color={p.role === 'frame' ? bezel : p.role === 'title' ? titleColor : c.muted} bold={p.role === 'title'}>
237 {p.text}
238 </Text>
239 ))}
240 </Text>
241 )
242
243 /**
244 * A framed panel, one element per screen row: the top edge with its plates, each row
245 * between the two sides, the bottom edge. `lead` goes before the top edge and `stem`
246 * before every other row (a tree's connectors); `width` is the frame's own.
247 */
248 const panel = (o: {
249 style: FrameStyle
250 width: number
251 title: string
252 meta?: string
253 foot?: string
254 rows: readonly JSX.Element[]
255 titleColor?: string
256 lead?: string
257 stem?: string
258 /** The bezel's colour; the slate's brass by default (scarlet for the warp's taint). */
259 frameColor?: string
260 }): JSX.Element[] => {
261 const g = FRAME[o.style]
262 const bezel = o.frameColor ?? slate.frame
263 const w = Math.max(8, o.width)
264 const lead = o.lead ?? ''
265 const stem = o.stem ?? ''
266 const side = (text: string) => <Text color={bezel}>{text}</Text>
267 const out: JSX.Element[] = []
268 out.push(
269 <Box flexDirection="row">
270 {lead.length > 0 ? <Text color={c.muted}>{lead}</Text> : ''}
271 {edge(edgeTop(o.style, w, o.title, o.meta), o.titleColor ?? c.accent, bezel)}
272 </Box>,
273 )
274 for (const row of o.rows) {
275 out.push(
276 <Box flexDirection="row">
277 {stem.length > 0 ? <Text color={c.muted}>{stem}</Text> : ''}
278 {side(`${g.v} `)}
279 <Box width={w - 4} overflow="hidden">
280 {row}
281 </Box>
282 {side(` ${g.v}`)}
283 </Box>,
284 )
285 }
286 out.push(
287 <Box flexDirection="row">
288 {stem.length > 0 ? <Text color={c.muted}>{stem}</Text> : ''}
289 {edge(edgeBottom(o.style, w, o.foot), c.muted, bezel)}
290 </Box>,
291 )
292 return out
293 }
294
295 /** The crest in the accent, centred; [] when the column is too narrow for it. */
296 const crest = (width: number): JSX.Element[] =>
297 (crestLines(width) ?? []).map(row => (
298 <Text color={c.accent} wrap="truncate-end">
299 {row}
300 </Text>
301 ))
302
303 /** An inverse strip across `width` cells: ` ✠ LEFT ……… RIGHT `, glass-coloured ink on the accent. */
304 const strip = (left: string, right: string, width: number) => {
305 const l = ` ✠ ${left}`
306 const r = right.length > 0 ? `${right} ` : ''
307 const room = width - cells(l) - cells(r)
308 const text = room >= 1 ? l + ' '.repeat(room) + r : fit(l, width).padEnd(width)
309 return (
310 <Text backgroundColor={c.accent} color={c.surface} bold wrap="truncate-end">
311 {text}
312 </Text>
313 )
314 }
315
316 /** A gauge of `width` cells between brass stops: `▕████▒▒▒▒▏`. */
317 const gauge = (fraction: number, width: number, color: string) => {
318 const inner = Math.max(2, width - 2)
319 const f = Math.min(1, Math.max(0, Number.isFinite(fraction) ? fraction : 0))
320 const full = Math.round(f * inner)
321 return (
322 <Text>
323 <Text color={slate.frame}>▕</Text>
324 <Text color={color}>{'█'.repeat(full)}</Text>
325 <Text color={c.track}>{'▒'.repeat(inner - full)}</Text>
326 <Text color={slate.frame}>▏</Text>
327 </Text>
328 )
329 }
330
331 /** Key caps: each command an inverse cap, a space between. */
332 const keys = (list: readonly string[]) => (
333 <Text wrap="truncate-end">
334 {list.map((k, i) => (
335 <Text>
336 {i > 0 ? ' ' : ''}
337 <Text backgroundColor={c.track} color={c.ink2}>
338 {` ${k} `}
339 </Text>
340 </Text>
341 ))}
342 </Text>
343 )
344
345 // A terminal's table has Raster and Image; any other surface never draws a slate.
346 const term = table as Partial<ElementTable<'terminal'>>
347
348 /** The turning cog at `angle`, or null where the surface has no Raster. */
349 const cog = (angle: number): JSX.Element | null => {
350 const { Raster } = term
351 if (Raster === undefined) return null
352 return <Raster key={COG.key} columns={COG.columns} rows={COG.rows} cells={cogCells(COG.columns, COG.rows, angle, cogPalette(t, slate))} />
353 }
354
355 /**
356 * The aquila as a picture `width` cells across (rows to keep its shape), from `file`
357 * when the person chose one,
358 * or null where the surface has no Image. Only where the terminal draws pictures:
359 * elsewhere an Image draws its `alt`, so the caller keeps the block crest there.
360 */
361 const aquila = (width: number, file: string | null = null): { element: JSX.Element; rows: number } | null => {
362 const { Image } = term
363 if (Image === undefined) return null
364 // A cell is twice as tall as wide: the box keeps the picture's shape at width × height / (2 × width px).
365 const rows = Math.max(3, Math.round((width * AQUILA_PNG_SIZE.height) / (2 * AQUILA_PNG_SIZE.width)))
366 // A picture the person chose (a PNG at 3 : 1, as tools/make-art.py --from makes it) is
367 // read by the terminal from its file; else the aquila embedded in the mod.
368 const source = file !== null ? { file, format: 'png' as const } : { png: AQUILA_PNG }
369 return { element: <Image key="crest" source={source} columns={width} rows={rows} alt="✠ аквила Империума ✠" />, rows }
370 }
371
372 /**
373 * The instrument block of a tab's head: the cog (turning while `awake`) beside the
374 * litany, the inverse status strip, a lamp with the machine's state, the crawling
375 * binaric cant and the Imperial date. Null where the column is too narrow or the
376 * surface has no Raster; the caller then draws the litany and the strip alone.
377 * Takes COG.rows screen rows, or SKULL.rows with a servo-skull.
378 */
379 const instruments = (o: {
380 width: number
381 now: number
382 litany: string | null
383 strip: readonly [string, string]
384 state: { label: string; awake: string; asleep: string }
385 awake: boolean
386 /** A servo-skull's frames in place of the cog, where the terminal draws pictures. */
387 skullDir?: string | null
388 /** The servo-skull's current frame as a pixmap, where the terminal draws no pictures. */
389 skullPix?: Pixmap | null
390 /** The warp's taint (the context past WARP_AT): the state overloaded, the cant broken. */
391 warp?: boolean
392 }): JSX.Element | null => {
393 if (o.width < 44) return null
394 const { Image } = term
395 const skull =
396 o.skullDir != null && Image !== undefined ? (
397 <Image key={SKULL.key} source={{ file: skullFrame(o.skullDir, o.now), format: 'png' }} columns={SKULL.columns} rows={SKULL.rows} alt="☠" />
398 ) : o.skullPix != null ? (
399 picture(SKULL.key, o.skullPix, SKULL.columns, SKULL.rows)
400 ) : null
401 const wheel = skull ?? cog(cogAngle(o.now))
402 if (wheel === null) return null
403 const figure = skull !== null ? SKULL.columns : COG.columns
404 const side = o.width - figure - 1
405 const second = Math.floor(o.now / 1000)
406 return (
407 <Box flexDirection="row">
408 <Box width={figure + 1} flexShrink={0}>
409 {wheel}
410 </Box>
411 <Box flexDirection="column" width={side}>
412 <Text color={c.accent} bold wrap="truncate-end">
413 {o.litany ?? ''}
414 </Text>
415 {strip(o.strip[0], o.strip[1], side)}
416 <Text wrap="truncate-end">
417 <Text color={o.awake || o.warp === true ? c.critical : c.muted} bold>
418 {o.warp === true || (o.awake && second % 2 === 0) ? '◉' : '○'}{' '}
419 </Text>
420 <Text color={c.muted}>{o.state.label} ▸ </Text>
421 {o.warp === true ? (
422 <Text color={c.critical} bold>
423 ПЕРЕГРУЖЕН ‡ ВАРП-ПОРЧА
424 </Text>
425 ) : (
426 <Text color={o.awake ? c.accent : c.muted} bold={o.awake}>
427 {o.awake ? o.state.awake : o.state.asleep}
428 </Text>
429 )}
430 </Text>
431 <Text color={o.warp === true ? c.critical : c.rule} wrap="truncate-end">
432 {o.warp === true ? corruptCant(binaryCant(side, second * 2), second) : binaryCant(side, o.awake ? second * 2 : 0)}
433 </Text>
434 <Text wrap="truncate-end">
435 <Text color={c.muted}>ДАТА ИМПЕРИИ </Text>
436 <Text color={c.ink2}>{imperialDate(o.now)}</Text>
437 </Text>
438 </Box>
439 </Box>
440 )
441 }
442
443 /**
444 * The aquila in a block mosaic, for a terminal that draws no pictures: each cell the block
445 * element and two colours nearest the picture, from the chosen picture's pixmap or the
446 * mod's own; rows keep the picture's shape (a cell is twice as tall as wide). Drawn as
447 * text runs, not a Raster: a Raster's palette rounds colours to the terminal's 256, and
448 * the gold went olive and rose. Null off the terminal (no Raster: the desktop app).
449 */
450 const aquilaDots = (width: number, pix: Pixmap | null = null): { element: JSX.Element; rows: number } | null => {
451 if (term.Raster === undefined) return null
452 const source = pix ?? AQUILA_PIXMAP
453 const rows = Math.max(3, Math.round((width * source.h) / (2 * source.w)))
454 const element = (
455 <Box key="crest" flexDirection="column" width={width}>
456 {mosaicRuns(source, width, rows).map((runs, y) => (
457 <Text key={`r${y}`} wrap="truncate-end">
458 {runs.map((run, x) => (
459 <Text key={`c${x}`} {...(run.fg === null ? {} : { color: run.fg })} {...(run.bg === null ? {} : { backgroundColor: run.bg })}>
460 {run.text}
461 </Text>
462 ))}
463 </Text>
464 ))}
465 </Box>
466 )
467 return { element, rows }
468 }
469
470 /** A photograph in half blocks at `columns` × `rows`, for a terminal that draws no pictures. */
471 const picture = (key: string, pix: Pixmap, columns: number, rows: number): JSX.Element | null => {
472 const { Raster } = term
473 if (Raster === undefined) return null
474 return <Raster key={key} columns={columns} rows={rows} cells={halfBlockCells(pix, columns, rows)} />
475 }
476
477 return { panel, crest, strip, gauge, keys, edge, cog, aquila, aquilaDots, picture, instruments }
478}
479hooks/lib/pixmap.ts 344 lines1// Pictures for the terminals that draw none (Windows Terminal, and any without kitty
2// graphics): a small RGBA pixmap, made beside each picture by tools/make-art.py, laid into
3// a Raster's cells at whatever box the tab gives it. An emblem goes in a block mosaic (a
4// cell is the block element and the two colours nearest the picture there: edges to an
5// eighth of a cell); a photograph in half blocks (a cell is two pixels, each its own
6// colour). Pure: numbers and strings, no `$`.
7
8import { packCells } from './cog'
9
10/**
11 * A picture's pixels, w × h RGBA as base64; `shape`, where present, the same picture with
12 * its dark outline thickened (made by tools/make-art.py for an emblem): what a mosaic's
13 * blocks are chosen on, while their colours stay the picture's own.
14 */
15export type Pixmap = { w: number; h: number; rgba: string; shape?: string }
16type Pixels = { w: number; h: number; data: Uint8Array }
17
18const DEFAULT = 0x01000000
19const SPACE = 0x20
20const UPPER = 0x2580 // ▀
21const LOWER = 0x2584 // ▄
22
23const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
24
25function fromBase64(text: string): Uint8Array {
26 const clean = text.replace(/[^A-Za-z0-9+/]/g, '')
27 const out = new Uint8Array(Math.floor((clean.length * 3) / 4))
28 let n = 0
29 let bits = 0
30 let at = 0
31 for (const ch of clean) {
32 n = (n << 6) | ALPHABET.indexOf(ch)
33 bits += 6
34 if (bits >= 8) {
35 bits -= 8
36 out[at] = (n >> bits) & 0xff
37 at += 1
38 }
39 }
40 return out.subarray(0, at)
41}
42
43/** A pixmap's pixels; null when it does not hold w × h × 4 bytes. */
44export function pixelsOf(p: Pixmap): Pixels | null {
45 const data = fromBase64(p.rgba)
46 return data.length === p.w * p.h * 4 ? { w: p.w, h: p.h, data } : null
47}
48
49/** A pixmap from the text of a .pix.json file; null for anything else. */
50export function parsePixmap(text: string): Pixmap | null {
51 try {
52 const v = JSON.parse(text) as Partial<Pixmap>
53 if (typeof v.w !== 'number' || typeof v.h !== 'number' || typeof v.rgba !== 'string') return null
54 return typeof v.shape === 'string' ? { w: v.w, h: v.h, rgba: v.rgba, shape: v.shape } : { w: v.w, h: v.h, rgba: v.rgba }
55 } catch {
56 return null
57 }
58}
59
60/** The average colour and opacity of the pixels in a box [x0, x1) × [y0, y1). */
61function average(px: Pixels, x0: number, y0: number, x1: number, y1: number): { rgb: number; alpha: number } {
62 let r = 0
63 let g = 0
64 let b = 0
65 let a = 0
66 let n = 0
67 const xa = Math.max(0, Math.floor(x0))
68 const ya = Math.max(0, Math.floor(y0))
69 const xb = Math.min(px.w, Math.max(xa + 1, Math.ceil(x1)))
70 const yb = Math.min(px.h, Math.max(ya + 1, Math.ceil(y1)))
71 for (let y = ya; y < yb; y += 1) {
72 for (let x = xa; x < xb; x += 1) {
73 const i = (y * px.w + x) * 4
74 const alpha = px.data[i + 3] ?? 0
75 r += (px.data[i] ?? 0) * alpha
76 g += (px.data[i + 1] ?? 0) * alpha
77 b += (px.data[i + 2] ?? 0) * alpha
78 a += alpha
79 n += 1
80 }
81 }
82 if (a === 0) return { rgb: 0, alpha: 0 }
83 return { rgb: (Math.round(r / a) << 16) | (Math.round(g / a) << 8) | Math.round(b / a), alpha: a / n }
84}
85
86/** A photograph in half blocks: each cell two pixels (top, bottom), each its own colour. */
87export function halfBlockCells(p: Pixmap, columns: number, rows: number): string {
88 const px = pixelsOf(p)
89 const words: number[] = []
90 const sx = px === null ? 0 : px.w / columns
91 const sy = px === null ? 0 : px.h / (rows * 2)
92 for (let row = 0; row < rows; row += 1) {
93 for (let col = 0; col < columns; col += 1) {
94 if (px === null) {
95 words.push(SPACE, DEFAULT, DEFAULT)
96 continue
97 }
98 const top = average(px, col * sx, row * 2 * sy, (col + 1) * sx, (row * 2 + 1) * sy)
99 const bottom = average(px, col * sx, (row * 2 + 1) * sy, (col + 1) * sx, (row * 2 + 2) * sy)
100 const t = top.alpha >= 110
101 const b = bottom.alpha >= 110
102 if (t) words.push(UPPER, top.rgb, b ? bottom.rgb : DEFAULT)
103 else if (b) words.push(LOWER, bottom.rgb, DEFAULT)
104 else words.push(SPACE, DEFAULT, DEFAULT)
105 }
106 }
107 return packCells(words)
108}
109
110/** A glyph's shape: which of a cell's points, (u, v) in [0, 1) from its top left, are its ink. */
111type Shape = (u: number, v: number) => boolean
112
113const lowerEighths = (k: number): Shape => (_u, v) => v >= 1 - k / 8
114const leftEighths = (k: number): Shape => u => u < k / 8
115const quadrants = (tl: boolean, tr: boolean, bl: boolean, br: boolean): Shape => (u, v) => (v < 0.5 ? (u < 0.5 ? tl : tr) : u < 0.5 ? bl : br)
116
117/**
118 * The block elements a cell may take (U+2580–259F: every terminal draws them, most from
119 * their own geometry, no font needed): halves, eighths, the quadrants. With its two colours
120 * a glyph splits a cell between them, so the set draws any edge level or upright to an
121 * eighth of the cell, and a corner to a quarter.
122 */
123const SHAPES: readonly (readonly [number, Shape])[] = [
124 [0x2588, () => true],
125 [0x2580, (_u, v) => v < 0.5],
126 [0x2594, (_u, v) => v < 1 / 8],
127 ...[1, 2, 3, 4, 5, 6, 7].map(k => [0x2580 + k, lowerEighths(k)] as const),
128 ...[1, 2, 3, 4, 5, 6, 7].map(k => [0x2590 - k, leftEighths(k)] as const),
129 [0x2590, u => u >= 0.5],
130 [0x2595, u => u >= 7 / 8],
131 [0x2596, quadrants(false, false, true, false)],
132 [0x2597, quadrants(false, false, false, true)],
133 [0x2598, quadrants(true, false, false, false)],
134 [0x259d, quadrants(false, true, false, false)],
135 [0x2599, quadrants(true, false, true, true)],
136 [0x259a, quadrants(true, false, false, true)],
137 [0x259b, quadrants(true, true, true, false)],
138 [0x259c, quadrants(true, true, false, true)],
139 [0x259e, quadrants(false, true, true, false)],
140 [0x259f, quadrants(false, true, true, true)],
141]
142
143// The points a cell is judged at: SU across, SV down (a cell is twice as tall as wide).
144const SU = 8
145const SV = 16
146/** Each shape's ink at those points, row-major. */
147const INK: readonly (readonly [number, readonly boolean[]])[] = SHAPES.map(([glyph, shape]) => {
148 const ink: boolean[] = []
149 for (let j = 0; j < SV; j += 1) for (let i = 0; i < SU; i += 1) ink.push(shape((i + 0.5) / SU, (j + 0.5) / SV))
150 return [glyph, ink] as const
151})
152
153/** The cogitator's glass: what the picture's transparency shows, unless told otherwise. */
154const GLASS = 0x0e0a05
155
156/** A channel on an 8-step grid, so fewer colour pairs reach the terminal's palette. */
157function step(v: number): number {
158 return Math.min(255, Math.round(v / 8) * 8)
159}
160
161function rgbOf(r: number, g: number, b: number): number {
162 return (step(r) << 16) | (step(g) << 8) | step(b)
163}
164
165/** Is a colour the ground's own (within a step), so the terminal's default can stand for it? */
166function isGround(rgb: number, ground: number): boolean {
167 return (
168 Math.abs(((rgb >> 16) & 0xff) - ((ground >> 16) & 0xff)) <= 8 &&
169 Math.abs(((rgb >> 8) & 0xff) - ((ground >> 8) & 0xff)) <= 8 &&
170 Math.abs((rgb & 0xff) - (ground & 0xff)) <= 8
171 )
172}
173
174/** Mosaics drawn so far, by pixmap, box and ground: a redraw of the tab draws none anew. */
175const mosaics = new WeakMap<Pixmap, Map<string, number[]>>()
176
177/**
178 * A picture in blocks, as close to it as a cell grid allows: each cell the block element and
179 * the two colours (its ink, its ground) that differ least from the picture's pixels there,
180 * the picture laid over `ground` (the terminal's background) where it is transparent. A
181 * colour that is the ground's own is left to the terminal's default, so the cells sit on
182 * whatever is behind them.
183 */
184export function mosaicCells(p: Pixmap, columns: number, rows: number, ground: number = GLASS): string {
185 return packCells(mosaicWords(p, columns, rows, ground))
186}
187
188/** One run of a mosaic's row: its glyphs and their colours (`#rrggbb`, null the terminal's own). */
189export type Run = { text: string; fg: string | null; bg: string | null }
190
191const hexOf = (rgb: number): string | null => (rgb === DEFAULT ? null : `#${rgb.toString(16).padStart(6, '0')}`)
192
193/**
194 * The same mosaic as rows of text runs, each run the cells of one colour pair: what a
195 * Text draws in the colours exactly (a Raster's palette rounds them to the terminal's 256,
196 * and gold goes olive and rose).
197 */
198export function mosaicRuns(p: Pixmap, columns: number, rows: number, ground: number = GLASS): Run[][] {
199 const words = mosaicWords(p, columns, rows, ground)
200 const out: Run[][] = []
201 for (let row = 0; row < rows; row += 1) {
202 const runs: Run[] = []
203 for (let col = 0; col < columns; col += 1) {
204 const at = (row * columns + col) * 3
205 const text = String.fromCharCode(words[at] ?? SPACE)
206 const fg = hexOf(words[at + 1] ?? DEFAULT)
207 const bg = hexOf(words[at + 2] ?? DEFAULT)
208 const last = runs.at(-1)
209 if (last !== undefined && last.fg === fg && last.bg === bg) last.text += text
210 else runs.push({ text, fg, bg })
211 }
212 out.push(runs)
213 }
214 return out
215}
216
217/** An sRGB channel (0..255) in linear light (0..1), where colours mix as light does. */
218function linear(v: number): number {
219 const c = v / 255
220 return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
221}
222
223/** A linear-light channel back to sRGB, 0..255. */
224function srgb(v: number): number {
225 const c = Math.min(1, Math.max(0, v))
226 return 255 * (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055)
227}
228
229type Rgb = readonly [number, number, number]
230
231/**
232 * The mean colour of a cell's points under a glyph's ink (`on`) and around it (`off`); a
233 * side with no points takes the other's, so a full block's ground is its ink.
234 */
235function split(ink: readonly boolean[], r: Float64Array, g: Float64Array, b: Float64Array): { on: Rgb; off: Rgb } {
236 let r1 = 0
237 let g1 = 0
238 let b1 = 0
239 let n1 = 0
240 let r0 = 0
241 let g0 = 0
242 let b0 = 0
243 for (let k = 0; k < r.length; k += 1) {
244 if (ink[k] === true) {
245 r1 += r[k] ?? 0
246 g1 += g[k] ?? 0
247 b1 += b[k] ?? 0
248 n1 += 1
249 } else {
250 r0 += r[k] ?? 0
251 g0 += g[k] ?? 0
252 b0 += b[k] ?? 0
253 }
254 }
255 const n0 = r.length - n1
256 const on: Rgb = n1 > 0 ? [r1 / n1, g1 / n1, b1 / n1] : [r0 / n0, g0 / n0, b0 / n0]
257 const off: Rgb = n0 > 0 ? [r0 / n0, g0 / n0, b0 / n0] : on
258 return { on, off }
259}
260
261/**
262 * The mosaic's cells as (glyph, fg, bg) words, row-major; drawn once per pixmap, box and
263 * ground. The blocks are chosen on the pixmap's `shape` (the picture with its dark outline
264 * thickened, so feathers stay apart) where it has one; their two colours are the picture's
265 * own there, averaged in linear light, so the gold keeps its brightness.
266 */
267function mosaicWords(p: Pixmap, columns: number, rows: number, ground: number): number[] {
268 const key = `${columns}x${rows}@${ground}`
269 const known = mosaics.get(p)?.get(key)
270 if (known !== undefined) return known
271 const px = pixelsOf(p)
272 const shaped = p.shape === undefined || px === null ? null : pixelsOf({ w: p.w, h: p.h, rgba: p.shape })
273 const sx = shaped ?? px
274 const words: number[] = []
275 const gr = (ground >> 16) & 0xff
276 const gg = (ground >> 8) & 0xff
277 const gb = ground & 0xff
278 const n = SU * SV
279 // the shape over the ground, sRGB (what the blocks are judged on) …
280 const sr = new Float64Array(n)
281 const sg = new Float64Array(n)
282 const sb = new Float64Array(n)
283 // … and the picture over the ground, linear light (what the colours are made of)
284 const lr = new Float64Array(n)
285 const lg = new Float64Array(n)
286 const lb = new Float64Array(n)
287 for (let row = 0; row < rows; row += 1) {
288 for (let col = 0; col < columns; col += 1) {
289 if (px === null || sx === null) {
290 words.push(SPACE, DEFAULT, DEFAULT)
291 continue
292 }
293 for (let j = 0; j < SV; j += 1) {
294 const y = Math.min(px.h - 1, Math.floor(((row + (j + 0.5) / SV) / rows) * px.h))
295 for (let i = 0; i < SU; i += 1) {
296 const x = Math.min(px.w - 1, Math.floor(((col + (i + 0.5) / SU) / columns) * px.w))
297 const at = (y * px.w + x) * 4
298 const k = j * SU + i
299 const sa = (sx.data[at + 3] ?? 0) / 255
300 const pa = (px.data[at + 3] ?? 0) / 255
301 sr[k] = (sx.data[at] ?? 0) * sa + gr * (1 - sa)
302 sg[k] = (sx.data[at + 1] ?? 0) * sa + gg * (1 - sa)
303 sb[k] = (sx.data[at + 2] ?? 0) * sa + gb * (1 - sa)
304 lr[k] = linear((px.data[at] ?? 0) * pa + gr * (1 - pa))
305 lg[k] = linear((px.data[at + 1] ?? 0) * pa + gg * (1 - pa))
306 lb[k] = linear((px.data[at + 2] ?? 0) * pa + gb * (1 - pa))
307 }
308 }
309 // The block whose two flat colours fit the shape there best.
310 let best = Number.POSITIVE_INFINITY
311 let chosen: readonly boolean[] = INK[0]?.[1] ?? []
312 let glyph = 0x2588
313 for (const [code, ink] of INK) {
314 const inked = split(ink, sr, sg, sb)
315 let err = 0
316 for (let k = 0; k < n && err < best; k += 1) {
317 const m = ink[k] === true ? inked.on : inked.off
318 const dr = (sr[k] ?? 0) - m[0]
319 const dg = (sg[k] ?? 0) - m[1]
320 const db = (sb[k] ?? 0) - m[2]
321 err += dr * dr + dg * dg + db * db
322 }
323 if (err < best) {
324 best = err
325 glyph = code
326 chosen = ink
327 }
328 }
329 // Its colours: the picture's own under its ink and around it, mixed as light.
330 const mixed = split(chosen, lr, lg, lb)
331 const fg = rgbOf(srgb(mixed.on[0]), srgb(mixed.on[1]), srgb(mixed.on[2]))
332 const bg = rgbOf(srgb(mixed.off[0]), srgb(mixed.off[1]), srgb(mixed.off[2]))
333 const fgGround = isGround(fg, ground)
334 const bgGround = isGround(bg, ground)
335 if (glyph === 0x2588 ? fgGround : fgGround && bgGround) words.push(SPACE, DEFAULT, DEFAULT)
336 else words.push(glyph, fg, glyph === 0x2588 || bgGround ? DEFAULT : bg)
337 }
338 }
339 const byBox = mosaics.get(p) ?? new Map<string, number[]>()
340 byBox.set(key, words)
341 mosaics.set(p, byBox)
342 return words
343}
344hooks/lib/terminal.ts 78 lines1import type { EngineInterface } from 'claude-code'
2
3import { parsePixmap } from './pixmap'
4import type { Pixmap } from './pixmap'
5
6/**
7 * Set once the engine said it draws a picture's alt here (a blit refused for it): the
8 * terminal was taken for one that draws pictures and is not, so the slate goes back to
9 * its cells. A module value: a reload looks again.
10 */
11let picturesRefused = false
12
13/** The engine draws a picture's alt here: no more pictures this load. */
14export function refusePictures(): void {
15 picturesRefused = true
16}
17
18/** Does a blit's refusal say the engine draws the Image's alt here (no kitty graphics)? */
19export function saysAlt(deny: string | undefined): boolean {
20 return deny !== undefined && /\balt\b|placeholder/i.test(deny)
21}
22
23/**
24 * Whether the terminal draws pictures: the engine draws an Image only through kitty graphics
25 * with Unicode placeholders, which Ghostty and kitty have. WezTerm has kitty graphics but no
26 * placeholders, so it is not one: taken for one, its Legion showed the Image's empty alt
27 * whenever the one probe after a start met another tab on screen. Anywhere else an Image
28 * draws its alt, so the slate draws its pictures in cells. False when the environment
29 * cannot be read, or once the engine refused a picture here. Shared by the tabs; `$` is
30 * followed into a function a file of the module's own exports.
31 */
32export async function drawsPictures($: EngineInterface): Promise<boolean> {
33 if (picturesRefused) return false
34 try {
35 return /ghostty|kitty/i.test(`${(await $.env.get('TERM_PROGRAM')) ?? ''} ${(await $.env.get('TERM')) ?? ''}`)
36 } catch {
37 return false
38 }
39}
40
41/**
42 * The ranks' portraits in `dir` by slug (`inquisitor.png` → inquisitor), so a rank with
43 * no picture keeps its plain card. Empty when the folder cannot be read.
44 */
45export async function listPortraits($: EngineInterface, dir: string): Promise<Record<string, string>> {
46 const found: Record<string, string> = {}
47 try {
48 for (const entry of await $.fs.list(dir)) {
49 if (entry.kind !== 'file' || !entry.name.endsWith('.png')) continue
50 found[entry.name.slice(0, -'.png'.length)] = `${dir.replace(/[\\/]+$/, '')}/${entry.name}`
51 }
52 } catch {
53 // no folder (yet): no portraits
54 }
55 return found
56}
57
58/** Pixmaps read so far, by the picture's path: a pixmap does not change while the mod runs. */
59const pixmaps = new Map<string, Pixmap | null>()
60
61/**
62 * The pixmap beside a picture (`x.png` → `x.pix.json`, made by tools/make-art.py), for
63 * the terminals that draw no pictures; null when there is none.
64 */
65export async function loadPixmap($: EngineInterface, png: string): Promise<Pixmap | null> {
66 const known = pixmaps.get(png)
67 if (known !== undefined) return known
68 let found: Pixmap | null = null
69 try {
70 const text = await $.fs.read(png.replace(/\.png$/i, '.pix.json'))
71 found = typeof text === 'string' ? parsePixmap(text) : null
72 } catch {
73 found = null
74 }
75 pixmaps.set(png, found)
76 return found
77}
78hooks/lib/art.ts 1787 lines1// Generated by tools/make-art.py: do not edit by hand, run it again.
2// The detailed aquila the warhammer Agents tab shows where the terminal draws pictures.
3
4/** 480 x 160 pixels, 3 : 1, for a box of 54 x 9 cells. */
5export const AQUILA_PNG_SIZE = { width: 480, height: 160 } as const
6
7/** The PNG, base64. */
8export const AQUILA_PNG =
9 'iVBORw0KGgoAAAANSUhEUgAAAeAAAACgCAMAAADjJU9/AAAAkFBMVEUAAADVqlggEgbryXf02Y5kTSeqjFH+/v2TbjFMNxkiFAUn' +
10 'GAn+/fT49dZqX07697aupoz49NmRek/HkDm3hjjEup5PNSC4t3y2tbRycGb47X77vXz0t7NhWC2leWHuzX3q2qzr2KGupYv//wCb' +
11 'j3VjUzTvz35wZExqX0r/AACzqozDupqglXtVSDYAfwDfumAsKhxPAAAAMHRSTlMA/vr++/39Df39o1taov4H9un9/v72DgUEBwsE' +
12 'BgsHWqdcpAGoWJ5epQFXY2emAkodaOw+AAAgIUlEQVR42u2diXajOhKGZYwAgVeC1056kl7uPjPv/3aj0oJKG4vtOE4PnDlz0zbG' +
13 'Nh+/6q9SIRMybdP2GbZ8bW/P63zMi5+dl+fTGZ22abvb9ichC2/LyXnYq88k918tDjptD7OVm52zpW9kPey1a/KWuq/elNM5fZyt' +
14 'KpPANuYIodeX1XRmHwMv0MjmqbVlSXkefohzmWT26+cZHHVC/ABjM+dAZ6m71UmVv3wfdojvL3mV1N4hZhRkPJ3hDx+bOV4uN2pL' +
15 'mCU/+fNfhhwEdvqZMFvAHG6mEE8y/vCxWQynFqF5kszTty/k3JvP5mfy5U3ub10fcOhppP5ovFRGTmmKMisEU/jPG+lz0vzpN9iT' +
16 'Oi8Xm/ybTog/KPaudOj1Adc8ps65Jl/P3cM01/hrCnvWVhDGgHkwXk2x+APki4IuxTjUIzOODRD/q4vwF/KvVO43k5JP8SVDrZA8' +
17 'ifi+8s0cS2Q/ACF13ku45Tt3gzBImDoPTCK+p3ydlCbLnCyYSnCdhA1fvht1MmHniGKYmET8EfINbTIEa8KROMzjb2r2qgOZsFs7' +
18 'mUT8MfL1Nx2CNeE/A1Ut/mCKdpoNOuwk4geQr/BIc8Nunr6uAxL+sn7F+8xtlzaJ+GO2kHypv5kQLOkFwjAEYGsf/iJ/C4l4ovCe' +
19 '4zNzTvg8oaGZoMyGl669Mfq8Tu19stBxaOJMYvAa1zRKv6eA3UkfqDJ5m6hPW/R+cyX8hfxm7wF159CREm+aapLwewKeeXwjs7s1' +
20 'Fij3Wd8tDfN/2s/XSfRQNuHZBPidR2juh3r4ElWH6pCwI2BVCyM9hMVbT2P0+47QjGZ6nI7xVf0Z1DLJ5xxNLOX52bLZtKMHxBDm' +
21 'b0zZNEa/L2Cep+hMKcqXBJyWbaRtC936K9JJWL/3BPhd539LnQvPO9quKl/Cv50x4PNvvoCrjoatuc6By2l++D0FrM6tIGz45mKT' +
22 'revHw+EAXeuuhPkYTdoxmjfUohFaCRja3/mLj7JtXh7TvHFb46jG9vNN2wjA/BTv85PSET/NJ0ASbdWyJXwmz/rJZ16FdgUcrlDB' +
23 '5XJSlwvf45TvxaEnFrfZpIzkTSkn3hhXHjE/ZzfZr74U22sumeCKNArCVgiGxAeOlr/K16queaxgQbi9Ao4lb+g7yRtcnN2mbSBN' +
24 'EObhq6NN0f2m9lqsWEsz3YitXsEm1MgSJeE6DSZKVpIkU2COLxGNWFQcppbHTFvmbLUwNCtH7vyjgtQn5i7NdZwmZi419ZouX181' +
25 'zRVlHAPDVUVGGf8fzfgG6q6cMTqdf9GnPc+/4BBMVWznMZtvVB7HOjK8FV1p5vApXrHOQzQx8/WvztyjeTj20bS0KTTl0gQQRSa2' +
26 'GXCazXF9S+dKBiRyWZbHEm5cvQAdQh5THr+AC8dnzjdX553Mj4dfgvnFNDcdNJmhKU+9XbB0tlkQMHdZZ32zGZ7pR4Bn7pG8t5nN' +
27 'DXPWwXzzizB3E5SwpQ3Q3ImzkMlzkoygOR+wIcBZyGXZHivrABzaRjBP5PfLxJfdDWNOpCgCydpnpMlPCZNhEwbai2iGNgohtRwD' +
28 'uISgTS99P5+5Gtzl93tE5p6l7aIZCJv1EJqzm9B8AMB9zGdDmNfRgB5lPs645z2Wto8mdQfaO9EMnNxLAb/zx+pmbg3u9BLmOFkb' +
29 'qODhNKmhOb8rTWmU2vMHp00oONeA24/xZlbgeDOfTQHOc1DwrP3k7WHvynxumNOxzHuY5mRwgvKxNFN0auxzIjb1WfW0QIZe+V/z' +
30 'bf+LHs7w/iIBg83/ftbbPwbzjmSNuLwP5K+6J0H5cJp8C9K0I714vOS1Lrje8YddmgU4llYTNey54IoXbAOHdJiLz/EQzKPJWv0X' +
31 'OQQbjkelm+9EU/7pazOJ0KS0VqdebCpPOsk1OzZ4QytweA+n/6gsSR5FHrHue2f7yg9+lfdn7iVrkfbtCnekzj+YZlCbSFEKZygJ' +
32 'ho3ntcFlOzo2SIO7DgmfjHkfJa7zuzOf497eKtLR9jg0S7FV5Pcf3zZpypNoOYvA1YohzIIb5EkccDEbvvFrHrKk2PHM23FVqzmJ' +
33 'DD7Xtx+/86kHsT0O81m0+Wh2W5r6L2u866Aptv1L6/sgVhq0Ra1PdZyTfp5fwtA6O3irBeDhx68LhHkJnkZtL3v5LfqYq6gSPWUP' +
34 'A7iHZtJDk+z3+xeDVKdo4OWxakeghVFBj6OjAKvICmP/YMxYzeBf3VSFfzX+BUkP8+TWzC8GfCXNvaAZStFOFtqdPSAPRlu3o4P6' +
35 'Yzhg9Xatyi55X8C8szCfQqmpYL5/V+ZxwDGaOGxeSxN91ZMpgBq07Bq0fCAXEsyigGlSxwBnYgAozLEu+QzMw8y/5KnnRIxn7gV0' +
36 'i3lcwWnQBN2Cpj0Gw2wFRptuDNrBg6TlbAGtyJPhYfVYNhiwOkamDjDDmM0HGhQgWswyOLeYj1AzHnyKxjB3TVz05ot4po9hjqXZ' +
37 'j1bNSbCRZ9IMqAat3Ar1TDESsHmBi3lMcDaYEygyXYM5yjwOXdV54o2IvgkCm3CLucbnFi0UuZcGLWNFNhuFlukvUzto5Z5dLisI' +
38 'WDe+U+dAM5UYUV/N/Zj59cGYwbxctOVijvn5JrO8io7PvOx6xe2nkZ/XuqtDo9UuSqC9wCALvc9m/otgN7kTrYcDrrXB6jgoQyIZ' +
39 'Y7U1Zm21W8zHw40wvzvBgWhvk/uoF4c4wEOF3q/ms0RsKOCCF/HaTKmIHbu2cvgbZVTvg/kOaJ1Qe0XuQ81sGfwJop93Sheq6XPR' +
40 'ejcUMJWrFoKF7hLyHOSIPkobJa7PqPio/Ukwh9Felfsk5R/f4CCdQ2jBtPxmmailA+BsIGAmGgPg/rKZHgRk8IheR0Do2x9lcuOM' +
41 '6pEx3yz3QWjB6x34NbKBw7COsbONysIypbqvoxgIWHdfpvoISmjxSACfZ8VbJxcHcDcY890zqg9COyb3CaS1fBHfShxsmQp8ND4u' +
42 'K+m2as2uAWyOUtAuLzeXQqZ1uhRkqqpMxiTO75tR3dRF3Tr3KQVaMdfAj7bqsj0ZGk2RJdaAs5CNDgFu18VLM+vJdtzPuizdCsBI' +
43 'EljND5ZRfWjug9CqiQc+DGw3oiAVtVTar3gq1euvRAHPQyZar8TiPUc7fLuwXaIEttnqKSWYT7oE8/0zqjvmPgKteYfFctdjqfQ1' +
44 '4dHlAydLzV2DHi4Wqn/QtvmSL8BBM5//kMx7le2WZtqQIMyfJqO6Xe5joSU4Medwt4MsleIf4peae1LCBSv3ukCAZSIWuGzUW/bZ' +
45 'ri2GzDGTSzHfN6N6l9wH0Cq2p4M68nLXZ6mSJImLFx4uarSAbO3wpZlcEIs6HkvfR5zWRRG5dkyFo8d27ZYKxuGki0sI84NlVDfK' +
46 'feJoAe7XUZaK0g7xNvyPDK8QbHOs27XMnCdMFzx/96ZDxnLoGWy7vmrI12F+r4wqf4/cBw/Ip+ejOnyfpZJfkWXi4oqJN7M8FvSe' +
47 'UcdKtavVMVvZus1QuawsLmN+ksUn7cifWtslT/3x+WQP2rfPqPJrblG6Se7joRV3yhz1uDzIUpXfhMRZePQE8cqq1Fx2hc7oz8oC' +
48 'zNAyiJaE66T8SWeyK3GuyTc0HAeYEOi3crDtgpN/dO8liWG+JqMavR3J4la5z96/cI5mXB5gqcryn+U2i0ZeOxMuKHihAvroEqea' +
49 'YfpKa+yxSnFgXvYuvCw4HI2z7fIfOd722i49Wh99EPtbZVQLcrwI8R/sdrkPlu5xnKUqq+Ni2SXezH2oaNtwLMBpDLByXYV7BWVd' +
50 'Ml4ujkqFg23XMXhT2NUZFfuDXLw05E3RSjuupSvH5brbUsHCK/ki7RLvLIhXwc9sr2zWBM7w2G29pgiYt7CMU9CmhBy3XbUcrVsh' +
51 'x0zR5ZivWDyTA74grd1HA7qS7lJaKhY9KTrqwgcfId4Zvg6oWHiwwEKlaMVYS9qlSZwKr5erR8btj59GIvJMTTKC7VrmsdE6ZLUH' +
52 'Yp6x5JrVe+cX5T6hm489S9U5e1CKMSBfdojXw4sMtsDv3N1gFptm+HH+BnuMsPAup+C7tTJe5kJ/5YC5iQ7bFcXcl1HNr1nfWLaS' +
53 'dk/pSYO87/bi4ywVP+SL3L9DvB4FhhhQ0RtdVU6eJAnzS55Z/TplJVp2KOIZOHiXjAHWi47IV9guF3NlYw7lL/NrFkflCpiPSGvJ' +
54 'bSyVemGneBPWeJ3NLV4ZSQsYepnb/Sw+PfMul8IKv5lX7Jo1LOmVsfmp6ittFxmROM+LK34nRN70HpnS2w+qcA6wVHNsqfa6JB0U' +
55 'ry5VBsZQc+oLZIqTfiFSZLuL8BHxi33GWMay/Nzarvk1tsvDHMyo5vSaxW8TFkprB9ZIhlqqGlkqcWN6TLxyBPBKWWJILfxITEN3' +
56 'NzDvsG2WZEffgnksC3V7C607ZJwTZLvqq21X1Gq3mFly3a/Y9Oc+N7FUQrqHPvEyWKaw8OuHhfWvRoPjMab2pwYDd44qwo118RQs' +
57 '9F6yUtkp4wMS8m1sV1dGlSRXrtA8Cu0oS1VYC3GfTh3ilenoZrdyY6OduRr5ZqL9uwzotfAAi5b/LGChi9DU4mq3Cc9GIBmfTtbC' +
58 '5MVtbFcM8xWAew3yrSzV/rk38v57ma4CgyYmbuQLf8HRvdl9r2WnFmlwe2HYIjYBGn+YVbr8d280ft6/l+3yrPYdG2aHWipmWaqX' +
59 'g4y8uzoq3vLnYuvhbZKIfKXPakK3r4QAJ0mDHJYv4qTxEG8XP8uojDkt8YUOL5btYje1XXfv0brIUsnXiKtiC+KlYfGWp8W2dm9U' +
60 'yFhMvvBnEbTRAcBao0VLOCBid1BhSb1dnMqwjCnIeCtYGVDvZbvuAne9JmMtVbut+8VbEcDrjpTMSXhQ6qrU2IjBdwhgIdHCOgB1' +
61 'EmXmxQaOWGLrlvEaBbxxtous1/nj0B1rqQaLF/BnoczIg1AY9I2UWUm8uxs8wMJjMTXia4yFd/GEcqYMEI6R8Xjb9bGMjaXi4/IY' +
62 'SzVCvHyHTQgvZRH5ZnqkptJG10MAy1fD2JxFRMxoCPFmqeeTBst4qO1CLSHr/IPuQbrMUg0Wr/iJBh8vnHveYBLJZtqBVl1Srsty' +
63 'AWfWvcTUnq6wDBw0tfg5mkCsfj9gsIwf3nZhS9U3e8B86Q4T70nssQrUGlYbKx3GYsN8G1SkigKWiVYTIOyImIq39T/NSoj0NFbG' +
64 '/bbLdOLe13ZdZanGiFfiDdQ1NhscWS2lUVSmbORzrB9wMTOEC/tqwSKW7xyqfMiphpEyflDbtbZ6XEdZqjGRV/RxBOsam60r3yjf' +
65 'xm3LCgAWDVlNB2FbxNtNuPKxMCXoUTIeaLtQJ+76/YdnbamKUZZqlHgBbx2azvljm7rybcsQrX2Gh5uIjXYB81ljaaKbtpaFciK3' +
66 'lML7urZ/hCaz6lTffzZexgNsV2HbrveDK2rFF1iqUeJ9Id8Bb+LjLZfb2pVvgmCYOmUxExGYeNVoFzDsI6KwqXQ0DF00iSvierss' +
67 'fcTQVL/4Tl4ulfEI2+UvBX1TxPQCSzXKNu+hDSsL1TWqv0G+jW2AfXuF6lhkGGDi1rIcq4WvtQZE/HcVqnxk0KC1zy+V8UDbRd85' +
68 'CpfuWkN9lmqIeNsXwk1Ki3DiW+4XtnwLl29h/dXIC8adbnAA16IdomprWYV7NEW4sEW82JfhtHihbkmSrEbKmPQ38NEr2jdGtNIO' +
69 't1SjIu/pGMdLcpBvEStVUo8N078G2wlY9buoWlZhz1ZESpUFiDgnccTHE7lcxt226w6/L2/b0l7pDhIv0XgjdQ2+w9KT74p18W3n' +
70 'wR0b7QDW/S6myydImK08ES8JKaOVD4mYXCrjuJDv8Mu2SXuGzOI2ZbRDa7BtPq2jiS8/Oq+oWNEXaoqrNjJmxkprL1y0Z2IYYGJe' +
71 'Z46E3mCVWSLmT/L6g8AQSYvXJ3KdjLHtaiHXdwCsxjzU49qRNA8Ur7iSJV4/8eUE+AWCIwPIF2VLhcWXzkwAti/IEOD2hKkwrDJo' +
72 'TbhA+ZEtYsYT8oUE6KfFEnFOrpRxu7xka7voPX5dXnTD943LY8QrGtVidQ3eLJRvt9SZWdikBjiqN7XVCrQcZzkMsA7DiLBVyGJJ' +
73 'unHmH+iWR+J9Fa985DimXiRje7Sez+7y2+P8+2hLtb+BeMVolkcSXyhI+/Jl212wnogrytj2xwHjpnGrih2qgu62LCTiUxVJi3MR' +
74 'd24g431ru+4CuOyT7ijximAVmdCHS2C99OS74jeSFoFSJa42eiElDLjGAcauc3qlyoLfNLryRLxcy1MSbghAiK+TsRZyeQfAVdnT' +
75 '9N4l3nYWH63mH8qMFN6AfJNv2xUeOc1Ef9v8audrtsuyAVtppQ7DQBhN/KNYsNp+S0IijiAWORNa1z/cGzBQxtJ23avB7kLxSs9t' +
76 'PuR53ZH4huXLq5W2fUa3E9KZG4D7AVf2iujOoex7ULmZFnXKkIg70uL12c5wWXGxjD98GyNe8V0i/RrySg3Il0C+hOwzxX1UjReA' +
77 '1R10UcDM3ZXag71gbZlpnv9GRFyV0Z4PTO1KGT+2ePEI811kRmloQh92O4F8V65811vbPtP2XxTNJLmeoY4CdvdFM0q0BUhtM71d' +
78 'uyJegYhPJJQWF9BEDTnTdyeafj4ZjxIvSnxnocT3WcrXmRiscv4YRcAzn2/jFUzt6QYLsHfnZYUOYwhnCX7XzTavvE4AEPFzKC2e' +
79 'uWnx55TxOPGKWeto4stXlzpK+VKnXrlw7LPFtwgGYM9GW4Brb+c2DBcxwmCmF06VkkoRH7/GKh8iLfY77z6LjPNR4jWJL/NXGoSb' +
80 'oL6G5UsWaMjmNpulmC+dBQOwNz1iAQ7cWouPZAjzNQFYhobkBQmL+CuRlQ+/T95OiwfK+IEQDxdvV+Irl20Iy5c49pmmjKFzMgsH' +
81 'YM9GO+vrVKE6TvtTO/oTMpZSx0yTsIjlcg4D0uKBMn4IuOcR4lWJrzeh3+jEFyaEeaEKaUCQl9MNyD6vOG6vFt0EZyzdiaDuuRkc' +
82 'htsXirezzXRObI78wR0wPOi0uAk2BLg/d9ct4/MjQH4h34aKN5b4znTiy73FV39ikD/l2ueVkTMqKEYqPVjCGHB49rxEw4EpoliE' +
83 'pZkWu7qTiF+Vm4pMJdppcZ+Mv5GXxxCxF3SccqTG25H4ikoN4HfackC+fNB27PMPm288AHvlaAQ4i/7EH/XKoED4h2Om+YBcuZOI' +
84 'KYh4LSp9HWnxOTTD70oku0vhedgg7RT0g+JVdY0di9Y1ovL17LPDtysAk/PJugARYHEFngnpDMMxwtJMd4o4WPlgO7fyEZMxH18e' +
85 'xmTh7uOweLsSX3Up5Eq+hRN9iW2f+cH/tvjSzgAsfuUVGWkDmL/0dIp9HX1IahH+O8E+YCUIV347z0LPFHY1BOSkR8ZJUpGH2Ux1' +
86 'PiheiTee+MrUOCzf/1j2GW4JPfDdsvafdXcAloux4op1jR1WeK1WFIbrNmBkXJ2HEnfUgpn+T0zEhlosLc5Jl4wfaIRuo1xEvHpC' +
87 'v+7Ay8+z01UHQzE8+dWxzyWUs8wE4cpMEAROyIn8FCul/9MOMS1gmvxbrFz/k5w6wjC/5wxNHvIiVumY6a+Son1dQk+evnKCiGur' +
88 'ISAoY3qXqcExEo6It3tCf99WtiDOUhf+0bPPlcN31RmA5TIyfGllZoxTremolZJjXQ06QXIIV56ZProQqYzPa71YyoCGAE/GjyVg' +
89 'tRxPMGh0TejvjXwDE4OQU7rVZ4dvnfUEYB59iwL/4LsGLNfIh+eqnjCc1R5huzINqW9gEtGIeF8OrXzgttnHAlxFmjxiM74Mt+wE' +
90 'JwbFjJJrn12+aU8GDEp84luDbiutdX9sA88U0dSqHRrSDsLSTJ+8kVhNIuKWHZYNSotbGVfksQiHHoTEt2tCX+0Tli/vk3btMyGG' +
91 '7wyqHQMy4KeGb0/tKsEKMOCQz9DopdEeG2ocM0NYjFe2mYY+6LCIzwhxpCEgPw8+oY83xZT34A3It5HyPUD1mVn2Ch7BDRzbvgAM' +
92 'Ay2XL/9fux6LBCxXXtFPVX1heItbPFQd2lx4DB45SBE3cRGTYQ0Bn2aLJb7UWUJpHZQvPy/8CcteSb5oAokP31l3BixlOts1tF31' +
93 'TgIWC3PQZjeT4u7OhvnbbPHUkkNYPLImaxIU8dpZSokOTYs/A+JwJzuK1Xks+sKUEp9ysOyzx7c3APPJXa7RRhj4TSWrMQIwRPON' +
94 'sLL86ScWfbkJwx5h20zvtnIiKRyJcyu2RvrkP99vgHfXNVr5rgLy/TMX4KmdEtt8X3sDsHRYzSyFTEmvu8RHi7m87xRuc05nTdxn' +
95 'WWH4NUDYMtMLkv8ZEPHKFnG88vHJGB/Iq/v7Yg5eLV9vXl+Qh8QK98YSl+9fvQEY+HB9Fvw6g8lIsRBwJhSsFiVmDT+zfAx/6rpC' +
96 '2jD8l0uYWB21kPTkvoh1J0BOuhDzD/ZKDp9Nw4HFuPFPjMbm9RVf1z47fE/9AVg5LC7gApxUARwZv8EW1kOgIFsIv0LCnYdow/Ap' +
97 'QNgx03lgppB6Ig5UPh4s6R38ewDhukZYvnpmAZ7y7DMgxw0cZNkbgKXD4hGYsidg24j1Nvh4KNf1gK66pydGRRSODgI4DC8JbvEA' +
98 'nAEzrZfbsTsBXBG7lY/iscqS4yVs1TXC8i30HiH7LCWNDNeiPwCDw0ICFpoFwupXcgRzLeGoz7LC8KKyWjwcwshM+/MProidysen' +
99 'FLCRsJ34mtw3JF/XPgt79Sz4Zrge3RuA5QBtBCxHaJiMlvdxPRVGwh2DtBWGUR06E4SfLatlzHRMxGtndKDZZxawknBGnSqmnNcP' +
100 'y5eE7PPB59sbgNsBesM5N6KqweTteUzcHMnUgw2/E7VzkLbCsEf4EDLTJCbir3a62zYEfFIBSwnTQEv0wl7sCsnXts+ZsM9fXb4m' +
101 'ADfRACwH6KenFGQqgBZ6YE7UnyyRz6ViRxY/UnvLBA/DLuGvwkxnvpkOiDj1RCwrH59WwOK7O3jjbTlB+wwlaZIbvkKxy23kNrOg' +
102 'gJ8UzwK0C4Cp+lM+/NQnYXxL2nZpFC0I50QWoUNmurOdByFmn1bA4htWJCRfv7YRsc98EFy3A7JU7Fvrp+M/OCJTYOmwnuD+MUCb' +
103 'SdmCmDOBmYKEpc/qSIbR7D/bvhlFi0F7reYKQ2bar3oERCzm4T4tYOL9WsvBl6/p2QlUnw+iobJAfJ/5AXoMlnFYKyFgXrSCkMuX' +
104 'BZCA+c36sAOUt0DCqx6fhYxWvX1GhAvRWnkgUTPtdwJwER+8X08hv8qWr52mWCTfoH3mNtXhmxO+U9FjsPQArQWcMVAt3RkF7wAt' +
105 'YxmWcPf1oojuttBDahN+JlEz7YpYNtZ+ypmFIdt6vcwi8g3aZ9Gx4/AdEoCVw9ICZjsxQtcbaaXBQG9qMUZDD28r4afOA6Iw7BE+' +
106 'kriZDrTzLNdr8stu+Mti+Tr2OVEtj+74zPeivQGYqAFaCpjBKs9A9sfOAN79kJw3cCnoakfTdUR9VVGIog5h1biZBM20I+LiE1uq' +
107 'kQ22WL62fVbZT47GYyXY9YAArByWqnHwHGRJRZK02KlyJf//3UIkSpQvyFDoakeXz7LC8BpJWo3aOc6nXDONRZz96nzxclQlKkz7' +
108 '9hnOjxmPhWCHBeBKDdBawL9vRU5ULjYG8GZRige3v2MJP7GOg1phmOA1HmDUzkmHmUYiZp/ZMg/+vUtn5tC3z1XLl1l8+wPwKVcO' +
109 '60msF00BrDRX1WIlqx3wr9WikmYLQIs7gYWEYVjIT0PCMHGmlvRMYdhMtyKmv7yA24XUDJ9zyD4H+PYGYPlTIaWkJUKqaD5dyiSJ' +
110 '/xcBXhKZKC1Vw698heo2D/78hROGY4QdM322Ss/F/wNf2fWL+jqeLfvMonxfugKw+h0Ykr/xhe9NRK04sK2cWRCA5RCtAIO13vIT' +
111 'X5mozX+s4E3C9SFbYfglSphZZvo5j33vXzoM47WUvue+fQ7xPcQDsIa7gB9OpNl2U7RpLV/pMhUkS6FkpWBQbim4p7m41VMO68Vm' +
112 'm8EvI+gf8z7m8TB8CBN2zTT/epEv/ktr2Bbf76jrWJ0CcNUJnuDnJ7QzAEu44tckttmGtVktv1CyRLinXA7RZSmH6Fx4MZ6UkrzN' +
113 'nNkm28pfRjCQo2H4gHM+TlhnvshqJcnv+WfrdX6vknXm8bUm+MVNDrEAvG7hMloUvHLNr4RGjeKHNUfGhEVTZlrZZ2GT+B1L2/VB' +
114 'jb+8t3bLq8tFIRbSlZDXHWH4RPwGAEw4S/5fBNu3nZQ2inbW8OhOEJ7iGTC/e6CFyzMe3uBapOhGqfWbWsTTAywff1uj24FS/kQK' +
115 'R9GQnV84sbPhkzt5eGznCQs1zpwmukYb1Ew7HJc1Wlq/giJnZwYsTFJT8G3Gdf6UuvduwSKeHmB7ZU8JmNc8tjM4ThMse1hhGAqO' +
116 'FfoJgHp5NFMM9P8jIxp5d2Jgmr1Q6yxwPOpUhjJgOM8ApWngZjUBGLf3yf9KNy0AUwBsPy0+QgoziVs4Dj9cyPLqMMyf5Ec4G8JO' +
117 '68Hj3R34CISdURvOHDrLqhgUFgbkmHxjKVhm6IR24eTaTfMbHJR7zr2LBLqk+VMpg2NF38guw1X6c9rj8cTXv2HSd15wF3TVCg1y' +
118 'zKbrPm9xuyF3yiHAa8ILWozJ3znj/90t3B8BlICh5gU3E3bdGd7IXN3cwZ8Vvp8qq4npkIJI6S0aFm1RL8sKMupCAi4DgMXLpZ3y' +
119 'AZcScAHZa1WWHW80/FNNW+9N5WRMews04zVcg5tVYD+ohqj+kr1bw9CAeQLNDwDNdSPakEg58b3VsB14BG0LMUDzKBoEPCAZX20g' +
120 'gj+JqkXHjhWZhuOPSKah+VK63x+bEOA8+g8NePNDunHdLjltD7RBvQP8cQMTg+kFJhayJJhEbEQD5vv+huu0XTwRKcoT+eoywCtR' +
121 'v3wqphTncV2Zyl4vA6wS3ckzPXQ9rFKox7+2atOg6Uw+LuFS/zH+pa3Zms7jAw/SLa/qXq+ctg/Zxuam1TR3O23TNm3TNm3TNm23' +
122 '3P4HsGquT9oYe30AAAAASUVORK5CYII='
123
124/** The same aquila as a small RGBA pixmap, for the terminals that draw no pictures (a block mosaic in a Raster). */
125export const AQUILA_PIXMAP = { w: 216, h: 72, rgba:
126 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
127 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
128 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
129 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
130 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAA' +
131 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
132 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAA' +
133 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
134 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
135 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
136 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
137 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
138 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
139 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
140 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
141 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
142 'AAAAAAAAAAIAAAADAAAAAAAAAAAAAAAJu7ivWtTRxlrRzMBa0czAWtHMw1rRzMBa0c7FWfnuzl7/9boaAAAAAP//AAEAAAAAAAAA' +
143 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAEAAAAA//W6Gvnuzl7RzsVZ0czAWtHM' +
144 'w1rRzMBa0czAWtTRxlq7uK9aAAAACQAAAAAAAAAAAAAAAwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
145 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
146 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
147 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
148 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
149 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
150 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
151 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
152 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
153 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAQAAAAAAAAAAAAAAHQYAAHhKNx7SkoBZ/49/XP+Pf1v/' +
154 'j39b/49/W/+Qf1z/jHtZ/19QN/+LeU4q//+qA39/AAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
155 'AAAAAAAAAAAAAAAAAH9/AAL//6oDi3lOKl9QN/+Me1n/kH9c/49/W/+Pf1v/j39b/49/XP+SgFn/Sjce0gYAAHgAAAAdAAAAAAAA' +
156 'AAD/AAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
157 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
158 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
159 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
160 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
161 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
162 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
163 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
164 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAMA' +
165 'AAAAAAAAAAAAADcPAQCXQzEX65d6Rf+Od0j/3cB2+f/jk/n224z5+N2O+fbbjfn/5pT22r53/SgXBcUAAAACAAAAAP+qVQP//wAB' +
166 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAf+qVQMAAAAAAAAAAigXBcXavnf9/+aU' +
167 '9vbbjfn43Y759tuM+f/jk/ndwHb5jndI/5d6Rf9DMRfrDwEAlwAAADcAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAA' +
168 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
169 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
170 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
171 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
172 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
173 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
174 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
175 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
176 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAABDHxEFuVtHJ/qulV3/9duM/3pmPfcHAAD4PSwV++bMg/v0' +
177 '25D78diN++/WjPv/5pf7n4hT+2xYM/b/75zH7diTVQAAAAAAAAAA////AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
178 'AAAAAAAAAAD///8BAAAAAAAAAADt2JNV/++cx2xYM/afiFP7/+aX++/WjPvx2I379NuQ++bMg/s9LBX7BwAA+HpmPff124z/rpVd' +
179 '/1tHJ/ofEQW5AAAAQwAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
180 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
181 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
182 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
183 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
184 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
185 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
186 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
187 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAQAAAAAAAAABwkA' +
188 'AIplUC38y7Bx//vfkPv/5pf4/+SV+mBNK/sEAAD6NCQR++nOhfv125D78tiN++/WjPv/6Jf7c184/KOJUv3/4or/9dWG//XckqLv' +
189 'z38QAAAAAP///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wEAAAAA789/EPXckqL11Yb//+KK/6OJUv1zXzj8' +
190 '/+iX++/WjPvy2I379duQ++nOhfs0JBH7BAAA+mBNK/v/5JX6/+aX+PvfkPvLsHH/ZVAt/AkAAIoAAAAHAAAAAAAAAAQAAAABAAAA' +
191 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
192 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
193 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
194 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
195 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
196 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
197 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
198 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
199 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBLx8O0KiQWv/94pL6/+WW+PLYjfrt04r79NqO++nOg/uUfE37z7R0+vrg' +
200 'kvry2I3689mN+vTaj/ry1ov7VUEj/MuvbPzy0Hz86MV0+v/eiP/02Iy+/9R/Bv///wH//wABAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
201 'AAAA//8AAf///wH/1H8G9NiMvv/eiP/oxXT68tB8/MuvbPxVQSP88taL+/Taj/rz2Y368tiN+vrgkvrPtHT6lHxN++nOg/v02o77' +
202 '7dOK+/LYjfr/5Zb4/eKS+qiQWv8vHw7QAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
203 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
204 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
205 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
206 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
207 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
208 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
209 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
210 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAXCw4BAIxlUS/538R+' +
211 '///pmffy2I768deM+/DWjPv/5Zf5++GS+vnekfz/6Zj8/+OU/Pbbj/z33I/89tuO/P/jlfzWvXr7VUAi/O3OgP3qyHf+6sh3/unG' +
212 'dfj/3of/8NePnAAAAAD//6oDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+qAwAAAADw14+c/96H/+nGdfjqyHf+6sh3/u3OgP1V' +
213 'QCL81r16+//jlfz2247899yP/Pbbj/z/45T8/+mY/Pnekfz74ZL6/+WX+fDWjPvx14z78tiO+v/pmfffxH7/ZVEv+Q4BAIwAABcL' +
214 'AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
215 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
216 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
217 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
218 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
219 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
220 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
221 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
222 'AAAAAAAAAAAAAAAAAAAAAAAAAAI/PwAEGw4EsZqCUP//45T6+N6S+fDWjPvz2Y778NeN+//llfm+pmj/a1cy/1M/IvhWQyT6VkMk' +
223 '+ldFJfpXRCX6VkQl+l1LKfg/Lhj6gWo9+//giv3lw3L+6sh3/urId/7qx3T6+tqJ//PbkVYAAAAA/6pVAwAAAAAAAAAAAAAAAAAA' +
224 'AAD/qlUDAAAAAPPbkVb62on/6sd0+urId/7qyHf+5cNy/v/giv2Baj37Py4Y+l1LKfhWRCX6V0Ql+ldFJfpWQyT6VkMk+lM/Ivhr' +
225 'VzL/vqZo///llfnw143789mO+/DWjPv43pL5/+OU+pqCUP8bDgSxPz8ABAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
226 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
227 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
228 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
229 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
230 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
231 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
232 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
233 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAApSTUa///sl/j/5Zb7' +
234 '992Q+vHXjfvv1oz7/+uZ+KiPVv8UBgDfAAAARAAAAC8AAAAzAAAAMgAAADIAAAAyAAAANQAAAC0PBACpQjAX/LygY/n31oX96MV1' +
235 '/urId/7qyHf88tB8/vHViNv/0HML//8AAQAAAAAAAAAAAAAAAAAAAAD//wAB/9BzC/HViNvy0Hz+6sh3/OrId/7oxXX+99aF/byg' +
236 'Y/lCMBf8DwQAqQAAAC0AAAA1AAAAMgAAADIAAAAyAAAAMwAAAC8AAABEFAYA36iPVv//65n479aM+/HXjfv33ZD6/+WW+//sl/hJ' +
237 'NRr/AAAAKQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
238 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
239 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
240 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
241 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
242 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
243 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
244 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
245 'AAAAAAAAAAAAAAAAAAAAPwAABAAAAAAKBAB3gGk+/52FUPx+Zjv8yK5t/f/llvr/6Zj4qI5W/xQHAeAeDwciAAAAAAAAAAAAAAAA' +
246 'AAAAAAAAAAAAAAAAAAAAAAAAAAAoGQueGw4D/xgMA/WtlFv89dN//ufFdf7qyHf+6cd1+/vZhv/x0oZhAAAAAP//VQMAAAAAAAAA' +
247 'AP//VQMAAAAA8dKGYfvZhv/px3X76sh3/ufFdf7103/+rZRb/BgMA/UbDgP/KBkLngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
248 'AAAAAAAAHg8HIhQHAeCojlb//+mY+P/llvrIrm39fmY7/J2FUPyAaT7/CgQAdwAAAAA/AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
249 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
250 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
251 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
252 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
253 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
254 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
255 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
256 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAH///8B////Af8AAAEAAAABAAAAAX9/fwJ/f38Cfz8/BH9/PwQhFAjPNCQR/g4CAGoA' +
257 'AABPMB8P/dW6df2slFv/EAQA4gAAACMAAAAAAAAABFVVVQM/Pz8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbIxQFrhkMAv88LBX5' +
258 '58l8/evJd/7px3f+6sh2+/fUgP7w0oSzAAAAAP///wIAAAAAAAAAAP///wIAAAAA8NKEs/fUgP7qyHb76cd3/uvJd/7nyXz9PCwV' +
259 '+RkMAv8jFAWuAAAAGwAAAAAAAAAAAAAAAAAAAAAAAAAAPz8/BFVVVQMAAAAEAAAAAAAAACMQBADirJRb/9W6df0wHw/9AAAATw4C' +
260 'AGo0JBH+IRQIz39/PwR/Pz8Ef39/An9/fwIAAAABAAAAAf8AAAH///8B////Af//AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
261 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
262 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
263 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
264 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
265 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
266 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AQAAAAD///8B////' +
267 'Af///wF/f38C////AX9/fwJ/f38CVVVVA1VVVQNVVVUDVVVVA39/fwSZmWYFmZlmBWZmMwUAAAABAAAAAgAAAAAAAAAAAAAAAAAA' +
268 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQbDADNAAAAJgAAAAAAAAAAAAAAOzIjEvMnGg3XS1VVGwAAAAD///8D////Bv///xD/' +
269 '//8M////HdbW1izf398pzsq2P6qqm0Xy7s4/nY5wIiASBeEVCAD9o4hR+vjWgv7nxXX+6sl3/PHOe//u0YLf//+/CP///wEAAAAA' +
270 'AAAAAP///wH//78I7tGC3/HOe//qyXf858V1/vjWgv6jiFH6FQgA/SASBeGdjnAi8u7OP6qqm0XOyrY/39/fKdbW1iz///8d////' +
271 'DP///xD///8G////AwAAAABLVVUbJxoN1zIjEvMAAAA7AAAAAAAAAAAAAAAmGwwAzQAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
272 'AAAAAAAAAAAAAAAAAAAAAAACAAAAAWZmMwWZmWYFmZlmBX9/fwRVVVUDVVVVA1VVVQNVVVUDf39/An9/fwL///8Bf39/Av///wH/' +
273 '//8B////AQAAAAD///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
274 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
275 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
276 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
277 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////Af///wH///8B//9/Av//fwL///8CqqpVA39/fwKqVVUDVVVVA1VVVQNVVVUD' +
278 'VQAAA1UAAAM/Pz8EZmZmBZlmZgVVAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
279 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wT///8L////Cf///xnV3tUf6+vrGry8vDKwrKhBtLS0OoR1VJR5bll6tqiIWIR8' +
280 'Z4dzbWSCjIV7dlFCLedcUD/CiHtmpXVoTsZ0aFS8fGpK2XRmTOZzZk7ie2lI7nhoS/Z2Z0v1fmpG/ol0S/xhTzL/MCwgPycWCHsW' +
281 'CQD/Xkws+PXVhP3oxXX+6sh3/urIdv311YT66sp9PwAAAAD//38C//9/AgAAAADqyn0/9dWE+urIdv3qyHf+6MV1/vXVhP1eTCz4' +
282 'FgkA/ycWCHswLCA/YU8y/4l0S/x+akb+dmdL9XhoS/Z7aUjuc2ZO4nRmTOZ8akrZdGhUvHVoTsaIe2alXFA/wlFCLeeMhXt2c21k' +
283 'goR8Z4e2qIhYeW5ZeoR1VJS0tLQ6sKyoQby8vDLr6+sa1d7VH////xn///8J////C////wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
284 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVQAAA5lmZgVmZmYFPz8/BFUAAANV' +
285 'AAADVVVVA1VVVQNVVVUDqlVVA39/fwKqqlUD////Av//fwL//38C////Af///wH///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
286 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
287 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//' +
288 '/wH///8B////Af///wL///8C/6qqA39/fwR/f38EqqpVA1VVAANVVVUDAAAAAwAAAAMAAAACAAAAA39/fwR/fz8EAAAAAAAAAAAA' +
289 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CP///wX///8R' +
290 '////GP///xbFyr8sr6+vMLWvni2elIlKnJmZU5qXkVSNhnhwj4h4boJ8bn90a12cdW5glnJsXKByZ1i7cGZYuHlrUsd1ZkvVcmVM' +
291 '0XxqSO12Z0n1eGlM8ntqTPt9bEv/fmxL/5F4SP+OeEv/kHlK/6OHT/+pjlb/q5FZ/7ecX/+1nGD/u59f/8mrZ//Iq2j/yapl/9y5' +
292 'av/buWr/3Llq/+bGeP/kxXf/5sd5/f/gh/lnTif+AAAANjEjDkgcDwP/NicS9unLff3synn+6sh3/unHdfv93Yr/8NaJWQAAAAD/' +
293 '/6oD//+qAwAAAADw1olZ/d2K/+nHdfvqyHf+7Mp5/unLff02JxL2HA8D/zEjEUgAAAA2Z04n/v/gh/nmx3n95MV3/+bGeP/cuWr/' +
294 '27lq/9y5av/JqmX/yKto/8mrZ/+7n1//tZxg/7ecX/+rkVn/qY5W/6OHT/+QeUr/jnhL/5F4SP9+bEv/fWxL/3tqTPt4aUzydmdJ' +
295 '9XxqSO1yZUzRdWZL1XlrUsdwZli4cmdYu3JsXKB1bmCWdGtdnIJ8bn+PiHhujYZ4cJqXkVScmZlTnpSJSrWvni2vr68wxcq/LP//' +
296 '/xb///8Y////Ef///wX///8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
297 'AAAAAAAAAAAAAAAAAAAAf38/BH9/fwQAAAADAAAAAgAAAAMAAAADVVVVA1VVAAOqqlUDf39/BH9/fwT/qqoD////Av///wL///8B' +
298 '////Af///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
299 'AH8AAAIAAAADAAAAAwAAAAIAAAABAAAAAgAAfwJ/f38Cf39/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
300 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////D////xP///8W+PjlKOrx6iXl4LIynJmKRpiUkEOOintEi4B5Y4yIg2mI' +
301 'hHpugnpoiYJ6aYV/d2WacmhYtXBoV65yY0i7b2NS0G9iUsx1Z07bdGVI5XRlSeN8akf8fWtK/3xoRP+Ic0r/hHJN/4t3T/+chFL/' +
302 'mYJR/6KHUf+zk1X/tZha/7mdXv/Cp2b/wKVl/8mrZv/Vtm7/0rVu/9W1af/jv23/5MJw/+nIdv7tzHv97Mx6/fHRf/z11IH89NOA' +
303 '+/bSe/r31H3699R9+vPRff300X389NJ9+/HPffzy0Hz78c99/O3LeP3ty3n97ct5/evIdv7syXb96cZ2/vvXgPuGazv/BQAAWjUs' +
304 'EzQfEgb+Lh4O+di7dPzvzHr+6MZ1/urHdPv51oL/9NeKdP///wL//8wF///MBf///wL014p0+daC/+rHdPvoxnX+78x6/ti7dPwu' +
305 'Hg75HxIG/jUsEzQFAABahms7//vXgPvpxnb+7Ml2/evIdv7ty3n97ct5/e3LeP3xz3388tB8+/HPffz00n379NF9/PPRff331H36' +
306 '99R9+vbSe/r004D79dSB/PHRf/zszHr97cx7/enIdv7kwnD/479t/9W1af/StW7/1bZu/8mrZv/ApWX/wqdm/7mdXv+1mFr/s5NV' +
307 '/6KHUf+ZglH/nIRS/4t3T/+Eck3/iHNK/3xoRP99a0r/fGpH/HRlSeN0ZUjldWdO229iUsxvY1LQcmNIu3BoV65yaFi1f3dlmoJ6' +
308 'aYWCemiJiIR6boyIg2mLgHljjop7RJiUkEOcmYpG5eCyMurx6iX4+OUo////Fv///xP///8PAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
309 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/f38Cf39/AgAAfwIAAAACAAAAAQAAAAIAAAAD' +
310 'AAAAA38AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wj///8I////' +
311 'Cff39yD39/8h5evfKMbGtTvCwrk3wLaQSoqEd16CgnxYkol4aoV7cIR/eXF+iHxol3hwXaF+dV2hfW5QxW5kVMhyZlLHemlN3G9j' +
312 'TuB0Zk3jemhG8ndmR+98aET7iXRK/4p3UP+LdEj/mIFS/497UP+ag1T/qo9Y/6aNV/+wk1f/wJ9b/76fXP/FpWD/zrFs/8qtaf/Z' +
313 'u3H/38B0/96/c//mwm/+68dx/enFcf7wz3z889J9+/PSfvv414L699aD+/jWgfr203z69tN9+vbTffrz0Xz789F8+/DPfPzuzHv8' +
314 '7816++7Mevzsy3n868p4/evJeP7ryHb+6sh2/uzKeP7synj+7Mp4/u/Ne/7wzXr+8c57/vfUf/731ID++NR//vnVfv761oD+99N+' +
315 '/vTSf/7z0Hv+8dB+/u/Qgf3w0YL94sBx/u7Jd/qNdET/CQAAag4AACMTBgD+IhMD+tW3b/zwz33+68t6/ezMevr72ob/8dKBbgAA' +
316 'AAAAAAAAAAAAAAAAAADx0oFu+9qG/+zMevrry3r98M99/tW3b/wiEwP7EwYA/gcAACQJAgBrjXRE/+3Jd/riwHH+8NGC/e/Qgf3x' +
317 '0H7+89B7/vTSf/73037++taA/vnVfv741H/+99SA/vfUf/7xznv+8M16/u/Ne/7synj+7Mp4/uzKeP7qyHb+68h2/uvJeP7rynj9' +
318 '7Mt5/O7MevzvzXr77sx7/PDPfPzz0Xz789F8+/bTffr203369tN8+vjWgfr31oP7+NeC+vPSfvvz0n378M98/OnFcf7rx3H95sJv' +
319 '/t6/c//fwHT/2btx/8qtaf/OsWz/xaVg/76fXP/An1v/sJNX/6aNV/+qj1j/moNU/497UP+YgVL/i3RI/4p3UP+JdEr/fGhE+3dm' +
320 'R+96aEbydGZN429jTuB6aU3ccmZSx25kVMh9blDFfnVdoXhwXaGIfGiXf3lxfoV7cISSiXhqgoJ8WIqEd17AtpBKwsK5N8bGtTvl' +
321 '698o9/f/Iff39yD///8J////CP///wgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/f38C' +
322 'AAAAAJ6Qdl+GfG+ceW9mlX9yWbByaVS2e21TunpqTNxwZE7bc2dP23VmTO1zZEzuemhJ8oFsRf19a0j6hW9F/5Z+Tv+VgFL/mX5L' +
323 '/6SMV/+gilf/qI5W/7ebXv+zmV7/vp9c/9CwZv/KqWH/07Jn/9m7cf/XuW//4sJ2/+jIef/mxnj98Mx2+/LPePzvy3b89tR/+vfV' +
324 'gPr21YD6+NeC+vjXgvr11ID79NF7+/TSfPvz0Hz78M57/PHOe/zty3n87Mp5/OvKePzrynn87Mp5/OvKefztynj87ct4/O3KePzw' +
325 'zXn78M15+/DOevv1037689F8+/TSfPv51n/6+daA+vnWgPz61n///NeB//jVgP/yz3z/78t3/+TCc/7kxnj968x9/d26bf7Ys2X+' +
326 '2rhr/seoZP2/oV/9uZpZ/Z+GT/yfiVX8mH9L/IVrOvyLdEP8gGY4/HZfN/x6Yzf8aFQv+19LKfthTSr7WUQj/GJNKflHMxn/Gg0D' +
327 'iKOXglZNQjX+YVI6+ebLg/v02I788teO+/LXjvn63pH+9uKqjv///y3///81////Nf///y324qqO+t6R/vLXjvny14779NiO/ObL' +
328 'g/tgUjr6S0E0/repi1YfEgWLRjMY/2NNKflZRCP8YU0q+19LKftoVC/7emM3/HZfN/yAZjj8i3RD/IVrOvyYf0v8n4lV/J+GT/y5' +
329 'mln9v6Ff/ceoZP3auGv+2LNl/t26bf7rzH395MZ4/eTCc/7vy3f/8s98//jVgP/814H/+tZ///nWgPz51oD6+dZ/+vTSfPvz0Xz7' +
330 '9dN++vDOevvwzXn78M15++3KePzty3j87cp4/OvKefzsynn868p5/OvKePzsynn87ct5/PHOe/zwznv889B8+/TSfPv00Xv79dSA' +
331 '+/jXgvr414L69tWA+vfVgPr21H/678t2/PLPePzwzHb75sZ4/ejIef/iwnb/17lv/9m7cf/Tsmf/yqlh/9CwZv++n1z/s5le/7eb' +
332 'Xv+ojlb/oIpX/6SMV/+Zfkv/lYBS/5Z+Tv+Fb0X/fWtI+oFsRf16aEnyc2RM7nVmTO1zZ0/bcGRO23pqTNx7bVO6cmlUtn9yWbB5' +
333 'b2aVhnxvnJ6Qdl8AAAAAf39/AgAAAAAAAAAAAAAAAAAAAABVAAADAAAAACQWCqiHcEL/tp1i/7qdXf/Ep2X/v6Ni/8yrYf/YtWj/' +
334 '2bdr/+DBdP/hwnX/48N1/+3Ne/3tzX3+7s58+/XRevr20nr79NF6+vfVgPr31YD699WB+vXUgfv11IH79NJ9+/DNevzy0Hz77816' +
335 '/O7MefzuzHn868l3/erId/zryXf87Mp5/OvKePzsy3n88M15+/DNefvwznn79NF7+/PQe/v10nz79tSA+/bTffv31H77+NV++/jW' +
336 'gPv41X/899N9//nVf//yz3z/7Ml4/+3KeP/jwXL/3rxv/+HBdP/SsGb/0rJp/9GwZ/+7nV3/uptb/7KUVv+YfEb/mX5H/4txPv9y' +
337 'WS7zfGU48XJaL+dtVzDUeGI97XFdN/teTS77STgg+ysbC/ocDgH6KRoL+zMhD/pCLhX7XUgn/HJbMvyCZjT8gGY1/JZ5RP2XekT9' +
338 'nYFJ/bSWVv2zlFT9tpZU/cahWP7GoVj+z6hd/ujAavvHoVj/HA4FrHZrWJ9+b1T/hHRW9nxsTvl5akz5eWpM+XlqTPl5akz5fG1R' +
339 '+YFzWPaBcVb3gHFW94FzWPZ8bVH5eWpM+XlqTPl5akz5eWpM+XxsTvmEc1X3gHBV/3xtT8YkFgm+xJ9X/+e/avzPqF3+xqFY/sah' +
340 'WP62llT9s5RU/bSWVv2dgUn9l3pE/ZZ5RP2AZjX8gmY0/HJbMvxdSCf8Qi4V+zMhD/opGgv7HA4B+isbC/pJOCD7Xk0u+3FdN/t4' +
341 'Yj3tbVcw1HJaL+d8ZTjxclku84txPv+Zfkf/mHxG/7KUVv+6m1v/u51d/9GwZ//Ssmn/0rBm/+HBdP/evG//48Fy/+3KeP/syXj/' +
342 '8s98//nVf//3033/+NV//PjWgPv41X7799R++/bTffv21ID79dJ8+/PQe/v00Xv78M55+/DNefvwzXn77Mt5/OvKePzsynn868l3' +
343 '/OrId/zryXf97sx5/O7MefzvzXr88tB8+/DNevz00n379dSB+/XUgfv31YH699WA+vfVgPr00Xr69tJ6+/XRevruznz77c19/u3N' +
344 'e/3jw3X/4cJ1/+DBdP/Zt2v/2LVo/8yrYf+/o2L/xKdl/7qdXf+2nWL/h3BC/yQWCqgAAAAAVQAAAwAAAAAAAAAAAAAAAAAAAAAz' +
345 'AAAFAAAAAAAAAHqZekH7/+SI9vHPffvy0X3789F9+/DOe/zuzHr87sx6/O3LePzty3j868l2/evJePzqyHb868p4/O7NevvtzHn7' +
346 '7816+/PQfPrz0Hz69NJ8+vfUffv3033799V++/fVgPz203389dJ9/fXSff321ID99NJ9//XSff/00Hz/58Z3/+XDdP/jwXP/1bRq' +
347 '/9S0av/Ssmv/waJf/8SmYf+1mFr/o4dQ/6eKUP+XfEn/g2k7/4lvP/96Yzf/ZlAq8WxWMPNeSSbqVkIj3llFJeFFMxrQPy4WzT8u' +
348 'F9EnGAezGw4BnRgJAJwIAAB5CQAAcQ4CAGoGAABMJBgVVUU9O2dWTz9kPzQueVhPSZZ0Z0yxYlVA43JhQfpwXDr7cVs0/YFnOfyb' +
349 'fUX9tJVW/tKwZf7bsmH+3rZj/ue+af7pv2n+571n/uG0X/7htV/+5Lhh/uW4YP7uwWj+8cVs/uvBav7sw23+2rZn/sSgWfyggEX+' +
350 'MCANzAcAAMCKaC//0qVR/cWaTP7InU3+yJ1N/sidTf7InU7+yJxM/sebTP/Ln07/zqJP/8ebTP/InEz+yJ1O/sidTf7InU3+yJ1N' +
351 '/saaTP7Po0/9n35A/wsAANctHQvVoIBF/sSgWfzatmf+7MNt/uvBav7xxWz+7sFo/uW4YP7kuGH+4bVf/uG0X/7nvWf+6b9p/ue+' +
352 'af7etmP+27Jh/tKwZf60lVb+m31F/YFnOfxxWzT9cFw6+3JhQfpiVUDjdGdMsVhPSZY/NC55Vk8/ZEU9O2ckGBVVBgAATA4CAGoJ' +
353 'AABxCAAAeRgJAJwbDgGdJxgHsz8uF9E/LhbNRTMa0FlFJeFWQiPeXkkm6mxWMPNmUCrxemM3/4lvP/+DaTv/l3xJ/6eKUP+jh1D/' +
354 'tZha/8SmYf/Bol//0rJr/9S0av/VtGr/48Fz/+XDdP/nxnf/9NB8//XSff/00n3/9tSA/fXSff310n399tN9/PfVgPz31X7799N9' +
355 '+/fUffv00nz689B8+vPQfPrvzXr77cx5++7Nevvrynj86sh2/OvJePzryXb97ct4/O3LePzuzHr87sx6/PDOe/zz0X378tF9+/HP' +
356 'ffv/5Ij2mXpB+wAAAHoAAAAAMwAABQAAAAAAAAAAAAAAAAAAAAAzAAAFAAAAAAAAAH6cfkX6/+OJ9fTRffr51X/8+dV+/PfUfv31' +
357 '0n/+9dF8/vLPe//xznv/8tB+/+zIdf/wzXr/6cVz/9u7cP/cum7/17Zs/8WmYf/Hp2P/waNh/62RVP+ylVf/n4RN/492RP+UeUb/' +
358 'gGg8/29YL/91XjX/XEkn+lhEJeJXRCTnSDUb10g3G8tGMxvOMyIQuTEiDrMsHQy2EgYAlBMHAIYRBQCHCQAAaQwAAGkFAABdAAAA' +
359 'QwAAAEcAAAA/AAAAKAAAACwAAAAeAAAADgAAABEAAAAAAAAAAAAAAAAAAAAAAAAAAP///wb///8gxcKvQ6qhi2qOh3OPgnNat4Fx' +
360 'UtGDcErwh25B/JJ6Sv+pjFD/uZZR/8OhW/7XsWP+4Ldj/uO5Zf7juWT+2axZ/tSoVv7Tp1b+1KlX/tisWf7esFv+5Llk/uG4ZP7d' +
361 'tGL+1rFj/rucWv6hg0f9j3VB/HFcNPxbRCL8ZE4p/FlDI/xoUCr8NycS/hIIA/98YTL85rhh/tSpWf7XrFv+16xb/tesW/7XrFv+' +
362 '1qta/tuwXP3EnFL9tpFO/duwXP3WrFr+16xb/tesW/7XrFv+16xb/tSqWv7itV/+lXZA/RcMBfw2JRL9aFAq/FlDI/xkTin8W0Qi' +
363 '/HFcNPyPdUH8oYNH/bucWv7WsWP+3bRi/uG4ZP7kuWT+3rBb/tisWf7UqVf+06dW/tSoVv7ZrFn+47lk/uO5Zf7gt2P+17Fj/sOh' +
364 'W/65llH/qYxQ/5J6Sv+HbkH8g3BK8IFxUtGCc1q3jodzj6qhi2rFwq9D////IP///wYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEA' +
365 'AAAOAAAAHgAAACwAAAAoAAAAPwAAAEcAAABDBQAAXQwAAGkJAABpEQUAhxMHAIYSBgCULB0MtjEiDrMzIhC5RjMbzkg3G8tINRvX' +
366 'V0Qk51hEJeJcSSf6dV41/29YL/+AaDz/lHlG/492RP+fhE3/spVX/62RVP/Bo2H/x6dj/8WmYf/Xtmz/3Lpu/9u7cP/pxXP/8M16' +
367 '/+zIdf/y0H7/8c57//LPe//10Xz+9dJ//vfUfv351X78+dV//PTRffr/44n1nH5F+gAAAH4AAAAAMwAABQAAAAAAAAAAAAAAAAAA' +
368 'AAAzAAAFAAAAAAgEAHZ1XDH/wqVj/6eJTv+XfUf/nIFK/4VrPP99ZTv+fWU4/2NOKvdgSij2Yk4r+Es3G9tINhvTRDIX0TknE7k7' +
369 'KhS5NyYTuCQWCJ8lFwibHRAEnAQAAHgEAABvBAAAbQAAAFAAAABTAAAARAAAACwAAAAxAAAAJgAAABQAAAAXAAAADAAAAAMAAAAF' +
370 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////Af///w7b28wyqJ14' +
371 'YXt5dHh+cluqeWpM1nJhR+V1ZEX7hW9G/5F1Qv+ojFD/uJRO/8WfVv/Vr2L/4Ldj/96zX/7it2L83LFd/daqV/3Vqlf91qtZ/tmu' +
372 'XP7cr1z+571n/unAa/7guGX+1a5f/sGhXf2Ze0P9gWc4/HBbNPxbRiT8XEUi/GlRLPxqUCf8iGo4/aSCQ/20j0r+0apc/ui8ZP6H' +
373 'ajj9JhgK+QwCAPlcRSL73LBc/tKnV/7UqVj+1KlY/tSpWP7UqVj+0qdX/uCzXv6Objj9Wkcq/N6yXv7SqFf+1KlY/tSpWP7UqVj+' +
374 '1KlY/tGmVv7itF/+dVot/AwCAPkmGAr6h2o4/ei8ZP7Rqlz+tI9K/qSCQ/2Iajj9alAn/GlRLPxcRSL8W0Yk/HBbNPyBZzj8mXtD' +
375 '/cGhXf3Vrl/+4Lhl/unAa/7nvWf+3K9c/tmuXP7Wq1n+1apX/daqV/3csV394rdi/N6zX/7gt2P/1a9i/8WfVv+4lE7/qIxQ/5F1' +
376 'Qv+Fb0b/dWRF+3JhR+V5akzWfnJbqnt5dHionXhh29vMMv///w7///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
377 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAMAAAAMAAAAFwAAABQAAAAmAAAAMQAAACwAAABEAAAAUwAAAFAEAABt' +
378 'BAAAbwQAAHgdEAScJRcImyQWCJ83JhO4OyoUuTknE7lEMhfRSDYb00s3G9tiTiv4YEoo9mNOKvd9ZTj/fWU7/oVrPP+cgUr/l31H' +
379 '/6eJTv/CpWP/dVwx/wgEAHYAAAAAMwAABQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAACQSBioXCQCAGAsAhREEAHQAAABWAAAAXAAA' +
380 'AEYAAAA6AAAAOgAAABwAAAAbAAAAHQAAAAwAAAAHAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
381 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wH///8BZmYzBUhIJAdtSCQHAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA' +
382 '////DP//0Ry4s6U2rqeKaYNzVoR6blmre21P2HFjSet0YkP5j3ZF/5uATP+pik3/w59Y/8WdUv/Vr1//4bdj++CzXvvhtWD73LFd' +
383 '/NerWPzVqFf816pX+92xXvvgtWD85Llj/urBa//ctWP/zada/sOiWv2sj1H9g2g4/GRPK/tJNx37NyYS+0IwGPtiSib8iGo4/aSD' +
384 'SP26k0v+y6RX/tuxX/7kt1/+671l/u/BZv/pu2P+y6JW/pp6Qv0XCwD6DgMA+kMyG/tOOh380adW/tSpWf7UqVj+1KlY/tSpWP7U' +
385 'qVj+0qdX/uCzXv6Uczr9ZE4u/N6yXv7SqFf+1KlY/tSpWP7UqVj+1KlY/tKnV/7csF3+YEkn/DIjEPsRBgD6FwoA+pp6Qv3Lolb+' +
386 '6btj/u/BZv/rvWX+5Ldf/tuxX/7LpFf+upNL/qSDSP2Iajj9Ykom/EIwGPs3JhL7STcd+2RPK/uDaDj8rI9R/cOiWv3Np1r+3LVj' +
387 '/+rBa//kuWP+4LVg/N2xXvvXqlf71ahX/NerWPzcsV384bVg++CzXvvht2P71a9f/8WdUv/Dn1j/qYpN/5uATP+PdkX/dGJD+XFj' +
388 'Set7bU/Yem5Zq4NzVoSup4ppuLOlNv//0Rz///8MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACbUgkB0hIJAdmZjMF////Af///wEA' +
389 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG' +
390 'AAAABwAAAAwAAAAdAAAAGwAAABwAAAA6AAAAOgAAAEYAAABcAAAAVhEEAHQYCwCFFwkAgCQSBioAAAAAAAAAAgAAAAAAAAAAAAAA' +
391 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//' +
392 '/wH///8B////AT8/AAQ/PwAEPz8ABD8AAAQ/AAAEAAAABFUAAANVAAADAAAAA39/PwR/f1UGVVUqBgAAAAQAAAAFAAAAAwAAAAAA' +
393 'AAAAAAAAAAAAAAAAAAAA////Cv///x7Fvaw+pJ2NY4R8bIt+b1iyem1S0npnRPB3Yj39i3VJ/6CCSv+tjU7/uppY/9GqXP/Xrl3/' +
394 '5Lpm+uW5Yvresl7727Bd/NitWvzVqlf82KtY+92xXPrjuGP747hj/Oi+aP3nv23/1K9j/8KdVv+khkz/gWg5/25VK+5uXDzsW0gt' +
395 '/EE0JvwoHBH7IBIF+ks2GfuDZTT9tpZV/dOqWv7gtGD+4bRe/tywW/7gsl3+5LZg/uCzXv7RqFr+so5J/X9lNvxjSST8Uzwd+11F' +
396 'Ivx6ZDn7oIhS/OHFffxFMhj7xZxS/tisWv7TqFj+1KlY/tSpWP7UqVj+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tSpWP7U' +
397 'qVj+1KlY/tSqWf7TqFf+Ujsb/MOoa/ymjVf8eWM5+11FIvxTPB37Y0kk/H9lNvyyjkn90aha/uCzXv7ktmD+4LJd/tywW/7htF7+' +
398 '4LRg/tOqWv62llX9g2U0/Us2GfsgEgX6KBwR+0E0JvxbSC38blw87G5VK+6BaDn/pIZM/8KdVv/Ur2P/579t/+i+aP3juGP847hj' +
399 '+92xXPrYq1j71apX/NitWvzbsF383rJe++W5Yvrkumb6165d/9GqXP+6mlj/rY1O/6CCSv+LdUn/d2I9/XpnRPB6bVLSfm9YsoR8' +
400 'bIuknY1jxb2sPv///x7///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAABQAAAARVVSoGf39VBn9/PwQAAAADVQAAA1UAAAMA' +
401 'AAAEPwAABD8AAAQ/PwAEPz8ABD8/AAT///8B////Af///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
402 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIzAAAFPz8ABD8AAAQAAAADAAAA' +
403 'BAAAAAMAAAACAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAP///wGqqqoDv39/BAAA' +
404 'AAMAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA////B///3SawqI5EioeHYIt/YZp4bFS4dWdN0nRlSO57ZkH/iHBD/5+DTf+r' +
405 'iUj/vJtW/9CrX//asWD+4LZi++a7Zvresl372q5a/NerWfzVqlj816tY+9uvWvrjt2L75btm/eC1Yf7fuGf/0K1j/7uWUP+tjU//' +
406 'inJC/19JI/9MNxjUOysXtSkbDZRDOjKIZFhEp1xVSsleVEPmcV07/IltPv2nhUb9zKVa/uC1YP7gtF/+2q5a/taqWP7arlr+4bVg' +
407 '/tywXf6+mlL+nHtD/WdPK/xSPB38V0Af/HleMfymg0L90qZV/sypX/312In9+NR+/vLTg/1PPCH7q4VC/dywXP7SqFf+1KlY/tSp' +
408 'WP7UqVj+0qdX/t6xXf6Tcjr9Y00t/NuwXP7RqFf+1KlY/tSpWP7UqVj+06hY/tesWv7HnlL+QzEX++XIfP372IH+9deJ/cypX/3S' +
409 'plX+poNC/XleMfxXQB/8Ujwd/GdPK/yce0P9vppS/tywXf7htWD+2q5a/taqWP7arlr+4LRf/uC1YP7MpVr+p4VG/YltPv1xXTv8' +
410 'XlRD5lxVSslkWESnQzoyiCkbDZQ7Kxe1TDcY1F9JI/+KckL/rY1P/7uWUP/QrWP/37hn/+C1Yf7lu2b947di+9uvWvrXq1j71apY' +
411 '/NerWfzarlr83rJd++a7ZvrgtmL72rFg/tCrX/+8m1b/q4lI/5+DTf+IcEP/e2ZB/3RlSO51Z03SeGxUuIt/YZqKh4dgsKiORP//' +
412 '3Sb///8HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAgAAAAO/f38EqqqqA////wEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEA' +
413 'AAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAADAAAAAgAAAAMAAAAEAAAAAz8AAAQ/PwAEMwAABQAAAAIAAAAAAAAAAAAAAAAAAAAA' +
414 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////' +
415 'Af///wGqqlUDmZlmBT8/PwQAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CPj45SjEt5FPh3pkZoqAaZt+cFHBbWNP03Vk' +
416 'Q/GHcEb/i3RH/6OFS/+2lFD/upZP/9KtX//ZsF393rRf/OS6ZPresl772KxZ/NarWPzWqlj82K1a/NuvWvrlumP66L9p/eK5Zf/f' +
417 't2b/0q9k/7qYVf+ff0T/gms+/2ROKf9JNxrjMSIOwhUGAJcAAABiDAQEP0pGRkGLjotUnJJ3dY6AX7NwYEjYh24/+p5/RP+0j0r/' +
418 'zaRW/t6zX/7csFz+16tZ/tesWf7dsVz+4LVh/tSqWv6vjk7+e18x/FI+HvxPOhz8bVIn/Jd1Pf27k0z+1qta/ua4YP7uv2T+0KZW' +
419 '/qiLUf3szH395cFx/vfWhf1sWDL7lXM4/eC0Xv7Sp1f+1KlY/tSpWP7UqVj+06dX/u6+ZP6Tcjr9YU0u/O3AaP7UqVf+06hY/tSp' +
420 'WP7UqVj+06hY/tyvXP6xjEf9VkQl+/DRgv3mw3L+7Mx9/aiLUf3Qplb+7r9k/ua4YP7Wq1r+u5NM/pd1Pf1tUif8Tzoc/FI+Hvx7' +
421 'XzH8r45O/tSqWv7gtWH+3bFc/tesWf7Xq1n+3LBc/t6zX/7NpFb+tI9K/55/RP+Hbj/6cGBI2I6AX7Ocknd1i46LVEpGRkEMBAQ/' +
422 'AAAAYhUGAJcxIg7CSTca42ROKf+Caz7/n39E/7qYVf/Sr2T/37dm/+K5Zf/ov2n95bpj+tuvWvrYrVr81qpY/NarWPzYrFn83rJe' +
423 '++S6ZPretF/82bBd/dKtX/+6lk//tpRQ/6OFS/+LdEf/h3BG/3VkQ/FtY0/TfnBRwYqAaZuHemRmxLeRT/j45Sj///8IAAAAAAAA' +
424 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAT8/PwSZmWYFqqpVA////wH///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
425 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
426 'AAAAAAAAAAAAAAAA////Af9/fwJVVVUDfz8/BGZmZgUAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////D+7o1y2zsKNOjIN0' +
427 'dod8ZpyCdFnCdGJB33VjQ/OGcEf/lHhE/6CETf+zlFX/yaNX/9GqXP/iuWX74rZg++G1YfvgtGD72a1a/NaqWPzWqlj82q5b+92x' +
428 'XfvjtmD66sFq/OS8af/as2P/0a1h/72dWv+afET/fmU3/2ZRLfRBLhbmNCURvSQYCpMKAAB5AAAASQAAABsAAAAAAAAAAP///xzN' +
429 'yrlYhXxvk4BvUNd8aEX9knVA/6uHRv/GnVD/1qta/96zX/7arlv92a1a/d2xXf7esV3+0qlZ/sOeVv6Wd0D9WkQj/CwdDfsuHw77' +
430 'WEEg/KSAQP3Rplb+37Jd/uCyXf7jtV/+4LRe/sSbUP6GaDX9PisU+5+GT/z21YH+48Fy/vzahv19Zzz6dVos/OW3YP7Rplf+1KlY' +
431 '/tSpWP7Vqlj+1atb/n1fLv09KhH8LiAO+3JXK/3RpVX+1qtZ/tOoWP7UqVj+0qdX/uG0Xv6RcDf9bVcz+vfVg/3kwnL+9tWB/p+G' +
432 'T/w+KxT7hmg1/cSbUP7gtF7+47Vf/uCyXf7fsl3+0aZW/qSAQP1YQSD8Lh8O+ywdDftaRCP8lndA/cOeVv7SqVn+3rFd/t2xXf7Z' +
433 'rVr92q5b/d6zX/7Wq1r/xp1Q/6uHRv+SdUD/fGhF/YBvUNeFfG+Tzcq5WP///xwAAAAAAAAAAAAAABsAAABJCgAAeSQYCpM0JRG9' +
434 'QS4W5mZRLfR+ZTf/mnxE/72dWv/RrWH/2rNj/+S8af/qwWr847Zg+t2xXfvarlv71qpY/NaqWPzZrVr84LRg++G1YfvitmD74rll' +
435 '+9GqXP/Jo1f/s5RV/6CETf+UeET/hnBH/3VjQ/N0YkHfgnRZwod8ZpyMg3R2s7CjTu7o1y3///8PAAAAAAAAAAAAAAAAAAAAAAAA' +
436 'AAAAAAAAAAAAAWZmZgV/Pz8EVVVVA/9/fwL///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
437 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
438 '////GL/FuSyooplQkIVmhH5zX516bFbAc2VM4nZhQPiAbEX/lHlI/6GBRv+0lVX/yqZb/9OrXf/ftmT75rtm+t+zXvvfs1772a1b' +
439 '/NWoV/zVqFf82axZ++C0YPvjtmH75blj/OS9av/XsWP/zahc/7uaWf+Zfkj/eF4x/2RPLuxCMRnfKRkJxSITB44LAABxAAAAUwAA' +
440 'AB8AAAALAAAAAAAAAAAAAAAA////CO/gvUKckXOHa2JTwHRhQfyMcUL/mHlB/7qVTP/WrFr+37Fc+92xXPvarVr73K9b++K1YPzg' +
441 'tF//yaJV/6F/QP+FZzT9W0cn/DUlE/sWCgH6OikT+4JlM/zInlH+4LNf/tyvXP7ZrVr+4LNd/tSqWv6qhUP9Y0ol/EEuFvtuUSb8' +
442 'iWYw/ceravz614L+5sR0/v/hi/6sk1v/Yksn/t+zX/7Sp1f+1KlY/tKnV/7gs13+bVMp/FxML/vYvXj848V8/GZTMPtYQSD8269c' +
443 '/tOoV/7UqVj+0aZX/ua4YP5yVyv9hm9B///ljv3mxHT++teC/seravyJZjD9blEm/EEuFvtjSiX8qoVD/dSqWv7gs13+2a1a/tyv' +
444 'XP7gs1/+yJ5R/oJlM/w6KRP7FgoB+jUlE/tbRyf8hWc0/aF/QP/JolX/4LRf/+K1YPzcr1v72q1a+92xXPvfsVz71qxa/rqVTP+Y' +
445 'eUH/jHFC/3RhQfxrYlPAnJFzh+/gvUL///8IAAAAAAAAAAAAAAAAAAAACwAAAB8AAABTCwAAcSITB44pGQnFQjEZ32RPLux4XjH/' +
446 'mX5I/7uaWf/NqFz/17Fj/+S9av/luWP847Zh++C0YPvZrFn71ahX/NWoV/zZrVv837Ne+9+zXvvmu2b637Zk+9OrXf/Kplv/tJVV' +
447 '/6GBRv+UeUj/gGxF/3ZhQPhzZUziemxWwH5zX52QhWaEqKKZUL/FuSz///8YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
448 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
449 'AAAAAAAAAAAAAAAAAAAA////GOLZqjaWjYFTmY5winxwWaZtYlW/eWhF63lnRv6Ba0P/n4JK/6WER/+zklD/yqZc/9CnV//dtWH9' +
450 '5bpk+t+yXfvdsFz72a5b/NWqWPzVqFf82a1Z+uS4YvrkumT85Lpl/ea+av/ZtGf/xaBX/7CQUP+TeUX/clkt/1ZDI+tCMhrWKRsK' +
451 'xBkMAYwHAABiAAAASwAAAB0AAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAD///8K5t22NZOPgXJ5alKudWZN6n9oPv+WeEL/rYlH/8mg' +
452 'U//fs17637Jd+9quW/zarVr737Jd+uG1X/3ds2L/uZNO/4NnNf9dRiPoVEUu2mFUPtdhUz35WUUq/G5WLv2sh0X9269b/t6yXf7X' +
453 'rFn+269b/t+yXf7DnFL+gmQx/Eo2G/tOOBr7iWgx/L+ZT/3qu2L+xZ1R/tCwZ/3PtnP85sN0/b+fXPxdTTCuTjoa79quW//UqVj9' +
454 '06hX/tmtW/6+l03+RjIY/O/Uifv/5Jb7996S+//mlvtPPSP7oYBE/d6xXP7Sp1f+0qdX/t6yXv9jTCn3RjkkwsGjXvrkwnP+z7Zz' +
455 '/NCwZ/3FnVH+6rti/r+ZT/2JaDH8Tjga+0o2G/uCZDH8w5xS/t+yXf7br1v+16xZ/t6yXf7br1v+rIdF/W5WLv1ZRSr8YVM9+WFU' +
456 'PtdURS7aXUYj6INnNf+5k07/3bNi/+G1X/3fsl362q1a+9quW/zfsl3737Ne+smgU/+tiUf/lnhC/39oPv91Zk3qeWpSrpOPgXLm' +
457 '3bY1////CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAB0AAABLBwAAYhkMAYwpGwrEQjIa1lZDI+tyWS3/k3lF/7CQUP/FoFf/' +
458 '2bRn/+a+av/kumX95Lpk/OS4YvrZrVn61ahX/NWqWPzZrlv83bBc+9+yXfvlumT63bVh/dCnV//Kplz/s5JQ/6WER/+fgkr/gWtD' +
459 '/3lnRv55aEXrbWJVv3xwWaaZjnCKlo2BU+LZqjb///8YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
460 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH9/AAIAAAAAZ1pFlndhO/+HbkD/lXxM/66OUf+8' +
461 'mVP/y6Zb/9uzYP7ds1/95bpk+uK2YPrbrlv716xZ/NaqWPzWq1j82q5Z+uO2YPnovmn847pm/+K6Zv/ctmf/xqRd/6uKS/+TeUX/' +
462 'b1kx/046HO9CMRnSLR0MtRIFAIoHAABkAAAAOwAAAB8AAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//+sorpxy' +
463 'VXx1Z5J6a0/Ye2dC/35nPf+ohkj/xZ5R/9arWfzesVz63bBc+9muWvzesVv64rVf+tyxX//LpFj/p4RG/2dQKP8/KxLOKx8SpEQ6' +
464 'MINzaFaaamNY1XtmQPeKazn9wJdN/duvW/7arlv+1qtZ/t+yXP7YrVv+qodE/WBHIvxFMhn8aU8o/K6KR/3Vqln+4rRe/uW3X/7S' +
465 'p1b+i2o0/VA4GPuDYzL94rVg/qiCP/4PBADrMyQQ99CmV/7Vqlj+0qdX/t+yXf6ng0L9UD0i+//kk/zr0or769GI+//omPtuWzb7' +
466 'im48/eG0Xf7Sp1f+06hY/tqtW/5ROx37DQMA86mCQP7itWD+g2My/VA4GPuLajT90qdW/uW3X/7itF7+1apZ/q6KR/1pTyj8RTIZ' +
467 '/GBHIvyqh0T92K1b/t+yXP7Wq1n+2q5b/tuvW/7Al039ims5/XtmQPdqY1jVc2hWmkQ6MIMrHxKkPysSzmdQKP+nhEb/y6RY/9yx' +
468 'X//itV/63rFb+tmuWvzdsFz73rFc+tarWfzFnlH/qIZI/35nPf97Z0L/emtP2Hx1Z5KunHJV///rKAAAAAAAAAAAAAAAAAAAAAAA' +
469 'AAAAAAAAAAAAAAAAAAAAAAAABQAAAB8AAAA7BwAAZBIFAIotHQy1QjEZ0k46HO9vWTH/k3lF/6uKS//GpF3/3LZn/+K6Zv/jumb/' +
470 '6L5p/OO2YPnarln61qtY/NaqWPzXrFn8265b++K2YPrlumT63bNf/duzYP7Lplv/vJlT/66OUf+VfEz/h25A/3dhO/9nWkWWAAAA' +
471 'AH9/AAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
472 'AAAAAAAAAAAAAD8AAAQAAAAAAAAAbYdnM/32ym/33bJe++K2Yfvcr1z71qpY/NWoV/zXq1n73LBc++CzXvrnvGX8575q/962Zf/a' +
473 's2T/xqVg/6eIS/+NcT7/dl82+k47HvM7KhPRLh8PpA4DAI4AAABcAAAALgAAAB8AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAD//78E' +
474 'AAAAAgAAAAAAAAAAAAAAAP///w/Nzb0+lIdueoN5Y8h1YkDuiW9A/5x9RP+zj0v/zaRV/d6xW/vesVz62q5b+9muWvzitV764bZi' +
475 '/9CnWf+wjk3/i205/1dCIv0tGwa7BQAAgCojJkiton5bkYVtkW9hSNqDZjT/n3w//82hUf/dsVz+269b/tuuW/7fslz+yKBU/otr' +
476 'Nv07KRP7QC8Y+4lrOP3LoVP+4bRe/tuvW/7esVz+1qta/pl4P/1OORv7bVEm/L6WTf3uvmP+t5FK+1E8Hf8bDwS5Lh0Lxr+WTf/a' +
477 'rlv90qdX/tarWf7No1X+QSwS+8Ooafv/8J77/+ma++nNgvxELxX7upRL/dqvW/7Sp1f+1qtY/c+mWP85KBPkGAsCxFE8Hf+3kUr7' +
478 '7r5j/r6WTf1tUSb8Tjkb+5l4P/3Wq1r+3rFc/tuvW/7htF7+y6FT/olrOP1ALxj7OykT+4trNv3IoFT+37Jc/tuuW/7br1v+3bFc' +
479 '/s2hUf+ffD//g2Y0/29hSNqRhW2RraJ+WyojJkgFAACALRsGu1dCIv2LbTn/sI5N/9CnWf/htmL/4rVe+tmuWvzarlv73rFc+t6x' +
480 'W/vNpFX9s49L/5x9RP+Jb0D/dWJA7oN5Y8iUh256zc29Pv///w8AAAAAAAAAAAAAAAAAAAAC//+/BAAAAAAAAAAAAAAAAAAAAAAA' +
481 'AAAAAAAACAAAAB8AAAAuAAAAXA4DAI4uHw+kOyoT0U47HvN2Xzb6jXE+/6eIS//GpWD/2rNk/962Zf/nvmr/57xl/OCzXvrcsFz7' +
482 '16tZ+9WoV/zWqlj83K9c++K2Yfvdsl779spv94dnM/0AAABtAAAAAD8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
483 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAQAAAAAAwMDUWlTLP3swmv5269c++G1' +
484 'YPvkt2H75r5q/922Zv/QqFv/v59b/6KESv+GaTb/cloz91JAI+szIg7UKh0NnA8FAIIAAABcAAAAKQAAABcAAAAIAAAAAAAAAAAA' +
485 'AAAAAAAAAAAAAAAAAAAAmWZmBUhIJAc/Px8IAAAAAAAAAAAAAAAA////B//vwSGcmY5djYBiq3hpSuRrWjz+knZC/6+MSP/FnVH/' +
486 '2a5b+uG0X/vbr1v82q5a/N+yXvvitWD727Be/8OcU/+TdT//a1Mt/DsrFeMUCACfAAAAZAcADyD///8b1MOTa3RpU710YD3/kG82' +
487 '/7iRSf/ZrVn93rFc/dyxXP3esl3+zKNV/q2KSP1pTyX7IRQI+jQjDvuKajX906pa/uCzXf7UqVj+265b/tqvXP6mhET9U0Ah/GBJ' +
488 'JvywjEn96rti/uO2Xv58YC/8PCkS+XdbLP84JQ/DGQ4DxauGRP/fsl380qdX/tKnV/7gs13+nno+/TkpFfuRe037nYZR/Ec0GvyL' +
489 'azX937Jd/tKnV/7TqFf+2q5a/b2WUP8lFwrhMyINzndbLP48KRL6fGAv/OO2Xv7qu2L+sIxJ/WBJJvxTQCH8poRE/dqvXP7brlv+' +
490 '1KlY/uCzXf7Tqlr+imo1/TQjDvshFAj6aU8l+62KSP3Mo1X+3rJd/tyxXP3esVz92a1Z/biRSf+Qbzb/dGA9/3RpU73Uw5Nr////' +
491 'GwcADyAAAABkFAgAnzsrFeNrUy38k3U//8OcU//bsF7/4rVg+9+yXvvarlr8269b/OG0X/vZrlv6xZ1R/6+MSP+SdkL/a1o8/nhp' +
492 'SuSNgGKrnJmOXf/vwSH///8HAAAAAAAAAAAAAAAAPz8fCEhIJAeZZmYFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABcA' +
493 'AAApAAAAXA8FAIIqHQ2cMyIO1FJAI+tyWjP3hmk2/6KESv+/n1v/0Khb/922Zv/mvmr/5Ldh++G1YPvbr1z77MJr+WlTLP0DAwNR' +
494 'AAAAAD8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
495 'AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAE0UzGfPTrmP/vJpW/6OHTv+EaDj/ZU4p9089IeU2JA/IHhAEnhIGAHoAAABRAAAALwAA' +
496 'AA8AAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVVUAAwAAAAQ/Pz8Ef39VBjMzAAUAAAADAAAAAAAAAAAAAAAA////F8G3nkeT' +
497 'jHyTf29OyntmPveBaT//nX5E/7qUTf/Rplb+3rJc+t2yXvzZrVr83bBb++G0X/vcsV7+zKRY/66NTv9+ZDf/Uz0d6S0eDcoIAAB/' +
498 'AAAALAAAAAAAAAAA////NZ6OZ6NiVkPqg2c4/6N/Pv/MoVH937Jd+9ywXPzfsl383bFd/7iSSv+DZTT9VkIh+ysdDfslFgf7h2g0' +
499 '/dWrWv7gs1z+0qdX/titWv7jtV7+pIJE/VM/IPxcQx/8rIlH/d2xXf7qu2H+vphQ/lY/HfxRORj7zaRW/uu8Yf5UPh39BwAA5pNz' +
500 'Pf7gs17+0adX/tSpWP7Sp1f+2q9c/ryWTv1SOxr8OCUO+66IRP3dsVz+06hX/tSpWP7Sp1f+3bFc/q2IRf4UCQHzUjsc/+u8Yv7N' +
501 'pFb+UTkY+1Y/Hfy+mFD+6rth/t2xXf6siUf9XEMf/FM/IPykgkT947Ve/titWv7Sp1f+4LNc/tWrWv6HaDT9JRYH+ysdDftWQiH7' +
502 'g2U0/biSSv/dsV3/37Jd/NywXPzfsl37zKFR/aN/Pv+DZzj/YlZD6p6OZ6P///81AAAAAAAAAAAAAAAsCAAAfy0eDcpTPR3pfmQ3' +
503 '/66NTv/MpFj/3LFe/uG0X/vdsFv72a1a/N2yXvzeslz60aZW/rqUTf+dfkT/gWk//3tmPvd/b07Kk4x8k8G3nkf///8XAAAAAAAA' +
504 'AAAAAAAAAAAAAzMzAAV/f1UGPz8/BAAAAARVVQADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAA8AAAAvAAAAURIGAHoe' +
505 'EASeNiQPyE89IeVlTin3hGg4/6OHTv+8mlb/065j/0UzGfMAAAATAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
506 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAMwAABSIUBr4zIw/DFwsA' +
507 'oQ4EAHgAAABQAAAALgAAABMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVVUAAzMzAAUAAAAEAAAAAn8AAAKqqlUDZmZmBQAA' +
508 'AAIAAAAAAAAAAAAAAAD///8C3d3YLq2ggXp+c1q1a11H5H9oQP+ae0T/r4tI/8ujVv/ftF/83rFc+9qvW/zbrlv737Jd+uC0X/3W' +
509 'rFv/uJNP/5RzO/9YQyL+NSYS0BcKAZUAAABNAAAAGQAAAAAAAAAA////FbGoi3l+bUzQZVQ8/5RyOP+4kUj/2q1Z+t6yXvzcsFz8' +
510 '4bNd+9KpWv+ifT3/WEAc+Ug7KOVkWUX4aVk+/H5iMf3OpFT+37Nd/tKnV/7Vqln+4rVe/riSSf1cRCD7Tzga+6aBQP3ZrVn+4LRe' +
511 '/t6xXP6LbTj9QCsR+4ZnMvznt17+y6FV/Eo2G/4XDALRCQAAbXZbLvzfslz90qdX/tSpWP7UqVj+0KVW/ui6Yf6efEH9bVUs/Om7' +
512 'Yv7Qplb+1KlY/tSpWP7Rp1f+47Vf+5BxPP8MAwCnGA0C0Ek2Gv/LoVX857de/oZnMvxAKxH7i204/d6xXP7gtF7+2a1Z/qaBQP1P' +
513 'OBr7XEQg+7iSSf3itV7+1apZ/tKnV/7fs13+zqRU/n5iMf1pWT78ZFlF+Eg7KOVYQBz5on09/9KpWv/hs1373LBc/N6yXvzarVn6' +
514 'uJFI/5RyOP9lVDz/fm1M0LGoi3n///8VAAAAAAAAAAAAAAAZAAAATRcKAZU1JhLQWEMi/pRzO/+4k0//1qxb/+C0X/3fsl36265b' +
515 '+9qvW/zesVz737Rf/MujVv+vi0j/mntE/39oQP9rXUfkfnNata2ggXrd3dgu////AgAAAAAAAAAAAAAAAAAAAAJmZmYFqqpVA38A' +
516 'AAIAAAACAAAABDMzAAVVVQADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMAAAAuAAAAUA4EAHgXCwChMyMPwyIUBr4z' +
517 'AAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
518 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiQkAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wABfwAAAj8AAAQ/AAAEAAAA' +
519 'AgAAAAEAAAAA////Af///wF/f38EPz8ABAAAAAAAAAAAAAAAAAAAAAD///8ftLGjWZWFY516aETQcF1A+IpwQP+lg0b/vZdN/9et' +
520 'WvzhtV/63bBc+9mtWvzcsFv64bRf+9uxXf/Mp13/pYRI/3BWK/9DMRfuHhEHsAIAAGAAAAAyAAAABwAAAAAAAAAA////BcnFuEyC' +
521 'eWiuZ1c/7nhgOf+ngT7/zKFS/d+yXfrcsV3837Jc+92xXP3AmVL/g2Qw/zwoEO4uJB+qbmhfqYR3XeCUd0X9tJBM/t2wXP7Uqln+' +
522 '1qpZ/t6yXf7Hn1T+Z0wi+yYYCvp5XS382a1a/tyvXP7htF7+xZxR/lZAIPxTOxn8vZRL/u+/ZP6/l03+NiMN+4RlMv2KZzD9DQUB' +
523 '8VpDIPvdsVz+0qdX/tSpWP7UqVj+0qdX/t6yXf6RcTn9YEws/NywXf7SqFf+1KlY/tSpWP7Sp1f+3bJd/nJaMv4NBQD/i2kx+oNl' +
524 'M/42Iw37v5dN/u+/ZP69lEv+UzsZ/FZAIPzFnFH+4bRe/tyvXP7ZrVr+eV0t/CYYCvpnTCL7x59U/t6yXf7Wqln+1KpZ/t2wXP60' +
525 'kEz+lHdF/YR3XeBuaF+pLiQfqjwoEO6DZDD/wJlS/92xXP3fslz73LFd/N+yXfrMoVL9p4E+/3hgOf9nVz/ugnlorsnFuEz///8F' +
526 'AAAAAAAAAAAAAAAHAAAAMgIAAGAeEQewQzEX7nBWK/+lhEj/zKdd/9uxXf/htF/73LBb+tmtWvzdsFz74bVf+tetWvy9l03/pYNG' +
527 '/4pwQP9wXUD4emhE0JWFY520saNZ////HwAAAAAAAAAAAAAAAAAAAAA/PwAEf39/BP///wH///8BAAAAAAAAAAEAAAACPwAABD8A' +
528 'AAR/AAAC//8AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQkAAcAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
529 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/fwAC' +
530 'AAAAAwAAAAMAAAADAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAA////AX9/fwJ/f38EAAAAAAAAAAAAAAAAAAAAAP///w/f0qxKj4dz' +
531 'fHdsWLx3ZUTth29F/5d4P/+8l1D/0qlY/9yvW/vdsVz73K5a+9qtWvvhtF764LVg/NKpWv+4kk3/gGQ0/1M+IPUwIg/CEwkAhAAA' +
532 'AD8AAAAZAAAAAAAAAAAAAAAAAAAAAP//8yycmo18c2dR129cPP+Nbzr/v5VJ/9qtWfvdsl383LBc/OGzXfvSqFj/pYJE/2BHIf8g' +
533 'EgLRFw0JbnZuY2yNfWLDfWU7+a2GQP/brln+16tZ/tWqWf7esVz+wZlO/nxhM/0hFAj7Qy8W+7mSSv3itV7+1qtZ/uCzXf6Zdjz9' +
534 'OCcR+3pdLfzcr1v+57pi/qB+Qv01Igz7lXE3/fTDZf5sUCX+BgAAokMwF+3Wq1v/1KlX/tSpWP7UqVj+0qdX/uCzXv6Tcjr9Yk0t' +
535 '/N6yXv7SqFf+1KlY/tSpWP7Sp1f+3bFc/1pDIPMFAACwbFAm/fTDZf+VcTf9NSIM+6B+Qv3numL+3K9b/npdLfw4JxH7mXY8/eCz' +
536 'Xf7Wq1n+4rVe/rmSSv1DLxb7IRQI+3xhM/3BmU7+3rFc/tWqWf7Xq1n+265Z/q2GQP99ZTv5jX1iw3ZuY2wXDQluIBIC0WBHIf+l' +
537 'gkT/0qhY/+GzXfvcsFz83bJd/NqtWfu/lUn/jW86/29cPP9zZ1HXnJqNfP//8ywAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAD8TCQCE' +
538 'MCIPwlM+IPWAZDT/uJJN/9KpWv/gtWD84bRe+tqtWvvcrlr73bFc+9yvW/vSqVj/vJdQ/5d4P/+Hb0X/d2VE7XdsWLyPh3N839Ks' +
539 'Sv///w8AAAAAAAAAAAAAAAAAAAAAf39/BH9/fwL///8BAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAwAAAAMAAAADf38AAgAA' +
540 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
541 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//9/AqpVVQM/Pz8E' +
542 'AAAAAAAAAAAAAAAA////BeXr6yiypYFqhHRVnnZoUeV/aD7/j3JC/6WDRf/IoVT/3bFb+t+yXfvarlv72a1a+96xXfvitV/73bVk' +
543 '/8OdVf+ZeT//ZU4o/zcnEtwfEQOUAAAAZQAAACEAAAAAAAAAAAAAAAAAAAAAAAAAAP///w/Ry7lUhX9zqG1bQPV6Yjv/pYA//9Gl' +
544 'UvvfsVz727Bc/N6xXfvfslz8v5hQ/4NmNP9ELxP2CQAAmhwZEUe/u6tMhHxlpHxkO/ergz7/1qlW/tuvXP3UqVj94rVg/smeTv6B' +
545 'ZDT9TUEw+y8iFPx7Xy/8265b/titWv7csFz+zaNU/l9HI/xDLxX7s45I/eC0Xf7jtV7+g2Yz/DMkEvuwiUT9779j/nZbLvwlFwj+' +
546 'JxkJoi8gENzBmlP/2a5Z/dOoV/7UqVj+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tOpWP7UqVj+06lZ/jwrE/MgFAiuJRcI' +
547 '+3VaLv3vv2P+sIlE/TMkEvuDZjP847Ve/uC0Xf6zjkj9Qy8V+19HI/zNo1T+3LBc/titWv7brlv+e18v/C8iFPxNQTD7gWQ0/cme' +
548 'Tv7itWD+1KlY/duvXP3WqVb+q4M+/3xkO/eEfGWkv7urTBwZEUcJAACaRC8T9oNmNP+/mFD/37Jc/N6xXfvbsFz837Fc+9GlUvul' +
549 'gD//emI7/21bQPWFf3Oo0cu5VP///w8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEAAABlHxEDlDcnEtxlTij/mXk//8OdVf/dtWT/' +
550 '4rVf+96xXfvZrVr72q5b+9+yXfvdsVv6yKFU/6WDRf+PckL/f2g+/3ZoUeWEdFWesqWBauXr6yj///8FAAAAAAAAAAAAAAAAPz8/' +
551 'BKpVVQP//38CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
552 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
553 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////ILWxnkWEfGyLfnBVyXNjSPx+Zjr/qIdJ/8KbT//RqFf9' +
554 '3rFb+t6xXPvZrVr83bBb+uC0X/vgtF/+0adZ/6aERv95XjH/Tzwg6C4fDbwLAABxAAAARQAAAAkAAAAAAAAAAAAAAAAAAAABAAAA' +
555 'AAAAAAD//+UopZ+PgHRqWdBxWzj/jG49/7yTSP/cr1n63bFd/NuwXPzgs1371KlX/6OARP9gSCP/LBsL2wAAAGgbGyQc///vMpeQ' +
556 'eqh9akr/rohE/9apVf/dsl7806lX/eCzXv3SplX/g2It/15MM/lbVUr4XU42+6mGR/7esl3+1alZ/uGzXv6hfT/9Lh4M+nFVKPza' +
557 'rlv+3bFc/t+yXf5xVyv8QCsR+8KaUP7uv2X+cVgs/EYvEvvSpFD9SjUX/xgMAvS2kEr+3K9b/tKoV/7UqVj+0qdX/uCzXv6Tcjr9' +
558 'Yk0t/N6yXv7SqFf+1KlY/tOoWP7YrFr+xp1Q/ikaCvlGMhb/06RQ/UYvEvtxWCz87r9l/sKaUP5AKxH7cVcr/N+yXf7dsVz+2q5b' +
559 '/nFVKPwuHgz6oX0//eGzXv7VqVn+3rJd/qmGR/5dTjb7W1VK+F5MM/mDYi3/0qZV/+CzXv3TqVf93bJe/NapVf+uiET/fWpK/5eQ' +
560 'eqj//+8yGxskHAAAAGgsGwvbYEgj/6OARP/UqVf/4LNd+9uwXPzdsV383K9Z+ryTSP+Mbj3/cVs4/3RqWdCln4+A///lKAAAAAAA' +
561 'AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAkAAABFCwAAcS4fDbxPPCDoeV4x/6aERv/Rp1n/4LRf/uC0X/vdsFv62a1a/N6xXPvesVv6' +
562 '0ahX/cKbT/+oh0n/fmY6/3NjSPx+cFXJhHxsi7WxnkX///8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
563 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
564 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wEAAAAA//q5N6ebfml+' +
565 'dWK6fmpG7HplQP+Rcj7/tJBM/86jU/7csFz73rFc+tqvXPzarlr84LNd+uG3Y/7WrV3/u5ZO/45wO/9XQiH6PSoSxhUJAZsAAABO' +
566 'AAAAGAAAAAAAAAAAAAAAAP///wEAAAAFAAAAAAAAAAD///8FyMGrT4N+baxrXEXyfWI2/6J/QP/RpFH837Jc+9qvXP3fs1363bFb' +
567 '/cGZT/+AZDX/Qy8U8xgNAq8AAAA6AAAAAP///yCdjWebfWlC/KKARf/NolH93rFc+9OoWP3fslz627Bd/5Z0Ov9LNxrmdG1f2oh6' +
568 'Xvd8ZTn8yZ5P/tuvW/7ar1z+0KZW/mVKI/wSCAP6iGgx/OG0Xf7ZrVv+0aVV/lpDIvxPNxb8zaJT/uy8Y/6CYzD8OiYO+92sVvuV' +
569 'dDr/GAsBnBEGAJaYdj3/4bNe+9KnV/7UqVj+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tKoV/7esVz8tY9J/yEUBrgVCQCn' +
570 'lnQ6/92sVvs6Jg77gmMw/Oy8Y/7NolP+TzcW/FpDIvzRpVX+2a1b/uG0Xf6IaDH8EggD+mVKI/zQplb+2q9c/tuvW/7Jnk/+fGU5' +
571 '/Ih6Xvd0bV/aSzca5pZ0Ov/bsF3/37Jc+tOoWP3esVz7zaJR/aKARf99aUL8nY1nm////yAAAAAAAAAAOhgNAq9DLxTzgGQ1/8GZ' +
572 'T//dsVv937Nd+tqvXP3fslz70aRR/KJ/QP99Yjb/a1xF8oN+bazIwatP////BQAAAAAAAAAAAAAABf///wEAAAAAAAAAAAAAAAAA' +
573 'AAAYAAAAThUJAZs9KhLGV0Ih+o5wO/+7lk7/1q1d/+G3Y/7gs1362q5a/NqvXPzesVz63LBc+86jU/60kEz/kXI+/3plQP9+akbs' +
574 'fnViuqebfmn/+rk3AAAAAP///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
575 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
576 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAH9/fwIAAAAAV0YtynlgNf+tiET/v5lR/9aqV/zgs1773K9b+9muW/zcr1v747Vg+92yXv/E' +
577 'nlX/oYBE/3JbM/9JNhrmJBUGpwYAAHoAAAAsAAAAAAAAAAAAAAAAAAAAAKqqqgNtbUgHAAAAAgAAAAAAAAAA////JJ+Xg4ByaVvU' +
578 'cFs6/45vOf+5kUj/3K9Z+96yXfzcsFz84bRe+tSpV/+ffD//XEUj/y0dCtMMAgB7AAAAGgAAAAD///8ZlI5+jXVfOPyTcjj/x55R' +
579 '/t+xXPvUqVj93LBc+9uvXf+jgUT/QCwS2U1GP42lm4PIg29I9aB6OP7arVn+2a5c/t6yXf6tiET9LiAP+iATBvuqh0b93bFd/tuu' +
580 'W/7KoFL+RzAT+3BVKfzarVn+5rhh/odnMvw7Jw/71KdW/LOKQ/8fEQXdAAAcCQkAAGmDYzH/4bRe+9KnV/7UqVj+0qdX/uCzXv6T' +
581 'cjr9Yk0t/N6yXv7SqFf+1KlY/tKnV/7itV76nXxB/w4DAI0AJCQHHxEF3rOKQ//Up1b8OycP+4dnMvzmuGH+2q1Z/nBVKfxHMBP7' +
582 'yqBS/tuuW/7dsV3+qodG/SATBvsuIA/6rYhE/d6yXf7Zrlz+2q1Z/qB6OP6Db0j1pZuDyE1GP41ALBLZo4FE/9uvXf/csFz71KlY' +
583 '/d+xXPvHnlH+k3I4/3VfOPyUjn6N////GQAAAAAAAAAaDAIAey0dCtNcRSP/n3w//9SpV//htF763LBc/N6yXfzcr1n7uZFI/45v' +
584 'Of9wWzr/cmlb1J+Xg4D///8kAAAAAAAAAAAAAAACbW1IB6qqqgMAAAAAAAAAAAAAAAAAAAAAAAAALAYAAHokFQanSTYa5nJbM/+h' +
585 'gET/xJ5V/92yXv/jtWD73K9b+9muW/zcr1v74LNe+9aqV/y/mVH/rYhE/3lgNf9XRi3KAAAAAH9/fwIAAAAAAAAAAAAAAAAAAAAA' +
586 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
587 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAfIps' +
588 'N/zvwGT316xZ/NyvW/vhtF763rNf/tKpWf+yj03/iWs4/1lDIO8rHAzLEQUAhwAAAD0AAAARAAAAAAAAAAAAAAAAAAAAAD8AAARV' +
589 'VVUGMzMzBQAAAAAAAAAA////DNDNuU2JgGysal1M8H9jNv+jf0D/zqJR/OCzXfvcsFz83rFd+92xXfzBmE3/e18x/0AuFu8TCACa' +
590 'AAAASwAAAAYAAAAA////EJiVho5wYEbxlHQ8/8WaTf/fsl371apZ/tuvXPvgsl3+rYlI/0czFuwRBgJ0vr6zb6CPbNV9YTP/wJVJ' +
591 '/9yxXv7br13+zqNT/ndbLf02KRv8MyIO+7+WTf7csFz+3LBc/ruVTv43JRD7gmMv/OG0Xv7jtl/+i2oy/DsnD/vLoVL916xa/Dwp' +
592 'Ev0DAABDAAAAAAMAAEtmTSf/4rVe/NGnV/7UqVj+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tKnV/7gtF78gGIy/wkCAHEA' +
593 'AAAAAwAAQzwpEv3XrFr8y6FS/TsnD/uLajL847Zf/uG0Xv6CYy/8NyUQ+7uVTv7csFz+3LBc/r+WTf4zIg77Nikb/HdbLf3Oo1P+' +
594 '269d/tyxXv7AlUn/fWEz/6CPbNW+vrNvEQYCdEczFuytiUj/4LJd/tuvXPvVqln+37Jd+8WaTf+UdDz/cGBG8ZiVho7///8QAAAA' +
595 'AAAAAAYAAABLEwgAmkAuFu97XzH/wZhN/92xXfzesV373LBc/OCzXfvOolH8o39A/39jNv9qXUzwiYBsrNDNuU3///8MAAAAAAAA' +
596 'AAAzMzMFVVVVBj8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAD0RBQCHKxwMy1lDIO+Jazj/so9N/9KpWf/es1/+4bRe+tyvW/vX' +
597 'rFn878Bk94psN/wAAAB8AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
598 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
599 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFUAAAMAAAAACQQEN047H/bluWL7165d/8KcU/+XeUP/YEgk/0AtFdkcDwShAAAAWAAA' +
600 'ABoAAAABAAAAAAAAAAAAAAAAVVUAAwAAAARVVVUDf38/BAAAAAEAAAAAAAAAAP///yKdl4R7dWtV1mxaPf+RcDf/uZFJ/9yuWfrf' +
601 's1783LBc/OCyXfvSqFj/o38//1lDIf8pGgrLAgAAbgAAACEAAAAAAAAAAP///w6nn4uAe29X54htP//Blkn/4bNd+9arWf3arVr7' +
602 '37Nd/LaPSf9hRyL/CQAAmoyRlTrg3MeFeWpO/Z12Nf/esVv82K1b/eO3YP2pgj7+VkQo+19QNfthTC38xJ1T/tmtWv7eslz+nXo+' +
603 '/RwPBfqSbTP94bRd/uC0Xv6ZcjX9MiEN+8ObUP7ouV/7XUMd/wYAAHIAAAAA////AQAAAC1INRz92K9d/NOoV/7TqVj+0qdX/uCz' +
604 'Xv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tGnV/7itV78Ykol/wMAAEL///8BAAAAAAYAAHJdQx3/6Llf+8ObUP4yIQ37mXI1/eC0Xv7h' +
605 'tF3+km0z/RwPBfqdej793rJc/tmtWv7EnVP+YUwt/F9QNftWRCj7qYI+/uO3YP3YrVv93rFb/J12Nf95ak794NzHhYyRlToJAACa' +
606 'YUci/7aPSf/fs1382q1a+9arWf3hs137wZZJ/4htP/97b1fnp5+LgP///w4AAAAAAAAAAAAAACECAABuKRoKy1lDIf+jfz//0qhY' +
607 '/+CyXfvcsFz837Ne/NyuWfq5kUn/kXA3/2xaPf91a1XWnZeEe////yIAAAAAAAAAAAAAAAF/fz8EVVVVAwAAAARVVQADAAAAAAAA' +
608 'AAAAAAAAAAAAAQAAABoAAABYHA8EoUAtFdlgSCT/l3lD/8KcU//Xrl3/5bli+047H/YJBAQ3AAAAAFUAAAMAAAAAAAAAAAAAAAAA' +
609 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
610 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA' +
611 'ASoaCOR7XzH/Sjgc7CcZCcAJAABsAAAAOgAAAAQAAAAAAAAAAAAAAAAAAAAAPz8ABAAAAAMAAAABqqqqA1VVVQMAAAAAAAAAAP//' +
612 '/wzOzsBJg3lmrGpdRfV7Yjn/poE+/82hUfzgsl373rJe/OCzXfrarlv9wJlQ/35hL/88KxXwFgkAngAAAEJVVVUDAAAAAAAAAAD/' +
613 '//8RuKyLc3xoRNyBaD3/u5RL/92wWvvXrFr916xZ/OC0Xvy/mVH/blQr/xUHAKkAAAAo////Q6GYgtuGaz3/wJVH/d+zX/zar1v8' +
614 '3LBc/3RVJP9WSTP3jIJq9YJnOP3Kn1D+2q5b/uCzXf6Ucjn9FgkA+olnMv3fsl3+3bBc/ppzNv0yIQ77uJNN/uu7YvuJajX/EggB' +
615 'sP///wE/AAAEAAAAAAAAABk4KRPmyaFV/9isWP3TqFj+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+1KlY/tOoV/7Yrlz9STYc+wAA' +
616 'ACwAAAAAMwAABf///wESCAGwiWo1/+u7Yvu4k03+MiEO+5pzNv3dsFz+37Jd/olnMv0WCQD6lHI5/eCzXf7arlv+yp9Q/oJnOP2M' +
617 'gmr1Vkkz93RVJP/csFz/2q9b/N+zX/zAlUf9hms9/6GYgtv///9DAAAAKBUHAKluVCv/v5lR/+C0XvzXrFn816xa/d2wWvu7lEv/' +
618 'gWg9/3xoRNy4rItz////EQAAAAAAAAAAVVVVAwAAAEIWCQCePCsV8H5hL//AmVD/2q5b/eCzXfresl784LJd+82hUfymgT7/e2I5' +
619 '/2pdRfWDeWaszs7ASf///wwAAAAAAAAAAFVVVQOqqqoDAAAAAQAAAAM/PwAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA6CQAAbCcZ' +
620 'CcBKOBzse18x/yoaCOQAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
621 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
622 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASETBVsAAABKAAAAHwAAAAAAAAAAAAAAAAAAAAB/AAACPwAA' +
623 'BAAAAAEAAAAA////Ab9/fwQAAAACAAAAAAAAAAD///8ro5+SeHRnUNdtW0D/j285/7uTSP/arVn737Ne/N+yXfvgs176zaJT/5V1' +
624 'PP9QOx7/KhsMzQQAAGwAAAAhAAAAAAAAAAAAAAAA////Cre6rF18cl/Ngmc7/66HQf/brln72K1a/darWfzgs177wptS/3FXLf8q' +
625 'Gwq8AAAAO////wzY0ryLfW1Q/5x3N//ZrFb62q9c/eGzXfu8lE3/PCcN72ZfUtKLhHTwhGUy/dKmVP7csFz+1atZ/nhdL/wRBwH6' +
626 'imoz/N6xXP7fsl3+qYdG/TAgDfu4k03+6btj/LCJRP8hEgXeDw8PEQAAAAEAAAACAAAAAgAAAAEgEwbCu5RM/92wXPvSqFf+0qdX' +
627 '/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+06hY/tisWf3IoFT/OCgT4gAAABYAAAAAAAAAAwAAAAEPDw8RIRIF3rCJRP/pu2P8uJNN/jAg' +
628 'Dfuph0b937Jd/t6xXP6KajP8EQcB+nhdL/zVq1n+3LBc/tKmVP6EZTL9i4R08GZfUtI8Jw3vvJRN/+GzXfvar1z92axW+px3N/99' +
629 'bVD/2NK8i////wwAAAA7KhsKvHFXLf/Cm1L/4LNe+9arWfzYrVr9265Z+66HQf+CZzv/fHJfzbe6rF3///8KAAAAAAAAAAAAAAAA' +
630 'AAAAIQQAAGwqGwzNUDse/5V1PP/NolP/4LNe+t+yXfvfs1782q1Z+7uTSP+Pbzn/bVtA/3RnUNejn5J4////KwAAAAAAAAAAAAAA' +
631 'Ar9/fwT///8BAAAAAAAAAAE/AAAEfwAAAgAAAAAAAAAAAAAAAAAAAAAAAAAfAAAASiETBVsAAAABAAAAAAAAAAAAAAAAAAAAAAAA' +
632 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
633 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
634 'AAAAAAAAAAAAAAAAAAAAAH9/AAIAAAADAAAAAwAAAAEAAAAAAAAAAP9/fwI/AAAEAAAAAAAAAAD///8K09C9UoV/cKhuXULyeWE9' +
635 '/6eBP//Po1L737Fc+96yXfzfsl372K1a/bePSf9xWC3/NCQO5BMIAJ0AAAA9AAAABQAAAAAAAAAAAAAAAP///wbd0rBLjYFowX5m' +
636 'Pf+wiEH/3K5Y+tuwXP3Vqln84rRe+9CkVP95XjD/MCAQyQAAAFQAAAAA////PZSOf9qCaDz/wJVH/t2xXf3bsFz82q5a/YRmMv8U' +
637 'CQDGqKGNlJKHa+KCZTP/16xZ/tyxXf7NolP+XkUg/B0SB/t6Xi/837Jc/tuvW/68lU7+MyIO+6J7O/3lt1/9zKBR+j0sE/cEAAA9' +
638 'AAAAAAAAAAMAAAAAAAAAAwAAAAAWCgGqo4JH/9+yXPvSp1f+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+06hX/tywW/u9lk3/JhcH' +
639 'wAAAAABVVQADAAAAAAAAAAMAAAAABAAAPT0sE/fMoFH65bdf/aJ7O/0zIg77vJVO/tuvW/7fslz+el4v/B0SB/teRSD8zaJT/tyx' +
640 'Xf7XrFn+gmUz/5KHa+KooY2UFAkAxoRmMv/arlr927Bc/N2xXf3AlUf+gmg8/5SOf9r///89AAAAAAAAAFQwIBDJeV4w/9CkVP/i' +
641 'tF771apZ/NuwXP3crlj6sIhB/35mPf+NgWjB3dKwS////wYAAAAAAAAAAAAAAAAAAAAFAAAAPRMIAJ00JA7kcVgt/7ePSf/YrVr9' +
642 '37Jd+96yXfzfsVz7z6NS+6eBP/95YT3/bl1C8oV/cKjT0L1S////CgAAAAAAAAAAPwAABP9/fwIAAAAAAAAAAAAAAAEAAAADAAAA' +
643 'A39/AAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
644 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
645 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA' +
646 'AAAAAAAAAAAAAAAA///4J5+bjHtyZ1TTcFs6/4tuOv+9k0f/265Z+t6yXvzfsl3737Je+8qhVP+Ydjv/Tjod/yESBcAGAABuAAAA' +
647 'EgAAAAAAAAAAf38AAgAAAAAAAAAA8+OzQJCCZr2BbUr/rIdF/9uuV/zbsF3806lY/eGzXvvRplf/iGgz/zonEdkHAABoAAAAAP//' +
648 '/wPIxbSXdGlT/4prNf/Yq1f72q9c/d+yXPq9lU3/RTAU/w0FB4jTz7iGindW9Zx3Nv/fs1z93bJe/sCYTv5SPBz8QjEb+3leL/3Y' +
649 'rVr+2q9b/r+XT/4uHw76kW00/eG0Xf7dsFz7ZEgh/wQAAHUAAAAAAAAAAwAAAAAAAAAAAAAAAwAAAAARBQCBj284/uGzXvvSp1f+' +
650 '0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+0qdX/t+yXPuhgEb+FQoBqAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAAQAAHVkSCH/3bBc' +
651 '++G0Xf6RbTT9Lh8O+r+XT/7ar1v+2K1a/nleL/1CMRv7Ujwc/MCYTv7dsl7+37Nc/Zx3Nv+Kd1b108+4hg0FB4hFMBT/vZVN/9+y' +
652 'XPrar1z92KtX+4prNf90aVP/yMW0l////wMAAAAABwAAaDonEdmIaDP/0aZX/+GzXvvTqVj927Bd/NuuV/ysh0X/gW1K/5CCZr3z' +
653 '47NAAAAAAAAAAAB/fwACAAAAAAAAAAAAAAASBgAAbiESBcBOOh3/mHY7/8qhVP/fsl7737Jd+96yXvzbrln6vZNH/4tuOv9wWzr/' +
654 'cmdU05+bjHv///gnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAA' +
655 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
656 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
657 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8B////EtbMs1GCfnKqa1tC9HxjOP+jf0D/0KNR/N+xXfvesl38' +
658 '4bRd+tetW/21jkn/dVks/zMkEOgRBgCVAAAAQQAAAAAAAAAAAAAAAEhIJAcAAAAAAAAAAP//7zCRhm6tc1w1/6F9Pv/WqlX82q9b' +
659 '/NOpWP3gsl371alW/5d3P/8/LRXhBgAAbgAAAAcAAAAA////SpKMfd1yXTn/tIxE/OCzXfzarlv83LFd/I5uOP8YCgDTMy8vRujm' +
660 '0IpxY03/qII8/92yXf3htF79pIFA/0c3Ift9bEn6fmIy/daqWP7ar1z+zKFS/isbCvtkSiP84bVf/t+xW/uKaTP/EQgCrf///wEA' +
661 'AAAFAAAAAAAAAAAAAAAAAAAAAwAAAAACAABabVIo/+W2X/vQplf+0qdX/uCzXv6Tcjr9Yk0t/N6yXv7SqFf+0aZX/uS2X/uLazb/' +
662 'DgQAeAAAAAAAAAADAAAAAAAAAAAAAAAAAAAABf///wERCAKtimkz/9+xW/vhtV/+ZEoj/CsbCvvMoVL+2q9c/taqWP5+YjL9fWxJ' +
663 '+kc3IfukgUD/4bRe/d2yXf2ogjz/cWNN/+jm0IozLy9GGAoA045uOP/csV382q5b/OCzXfy0jET8cl05/5KMfd3///9KAAAAAAAA' +
664 'AAcGAABuPy0V4Zd3P//VqVb/4LJd+9OpWP3ar1v81qpV/KF9Pv9zXDX/kYZurf//7zAAAAAAAAAAAEhIJAcAAAAAAAAAAAAAAAAA' +
665 'AABBEQYAlTMkEOh1WSz/tY5J/9etW/3htF363rJd/N+xXfvQo1H8o39A/3xjOP9rW0L0gn5yqtbMs1H///8S////AQAAAAAAAAAA' +
666 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
667 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
668 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVVVQMA' +
669 'AAAAeHJhg2dVOf+Mbjv/upFI/92vWfresl3837Jd++CzXfvJoFX/l3U6/1E8Hf8eEQa5BwAAaAAAABsAAAAAAAAAAP///wFfPx8I' +
670 'AAAAAAAAAAD///8kjYp9mXRgQPOYdTn/yp9Q/d6xXPvTqVj93rJc+tSqWv+jgEL/TDYW4goAAHgAAAAaAAAAAP///xLUzq2XgXFV' +
671 '/4pqM//Wqlf7269c/t6xXfvDm1H/XkYj/wAAAInd5dQe182rrnNeOv+3jkX83rJe/eK2YP2adjj/PS0a7oyHefNzYED7zKJS/t2x' +
672 'Xv7Jn1D+NiUP+0EtFPvVqFX+4LNe/K6IRf8cDgLgGAwMFQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAVVVAAMYJBgVLR0L2cCYT/7a' +
673 'r1z80KZX/uCzXv6Tcjr9Yk0t/N6yXv7Sp1f+1qtY/c+mWP5ALhb1DAwMKX9/fwIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAEYDAwV' +
674 'HA4C4K6IRf/gs1781ahV/kEtFPs2JQ/7yZ9Q/t2xXv7MolL+c2BA+4yHefM9LRrumnY4/+K2YP3esl79t45F/HNeOv/Xzauu3eXU' +
675 'HgAAAIleRiP/w5tR/96xXfvbr1z+1qpX+4pqM/+BcVX/1M6tl////xIAAAAAAAAAGgoAAHhMNhbio4BC/9SqWv/eslz606lY/d6x' +
676 'XPvKn1D9mHU5/3RgQPONin2Z////JAAAAAAAAAAAXz8fCP///wEAAAAAAAAAAAAAABsHAABoHhEGuVE8Hf+XdTr/yaBV/+CzXfvf' +
677 'sl373rJd/N2vWfq6kUj/jG47/2dVOf94cmGDAAAAAFVVVQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
678 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
679 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
680 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAQEMuE/vkt1/63rFd++CyXfvYrFr+so5L/3VYK/82' +
681 'JRHmDAMAjAAAADsAAAAAAAAAAAAAAABVVVUDbW1IBwAAAAAAAAAA////IpyWiI51ZUjumXc8/8ufT//gs1771apY/dyvXPvfsVz+' +
682 'pYJD/0g2G+wSBwCJAAAAHAAAAAAAAAAA///uT6Safdd6Yjj/s4k//OG0XvzZrVr9269c+pZ1PP8xIQ7fAAAAPv///xmqoIrNfWEz' +
683 '/82hT/rdsV7+3LFc/YBjMf8vIhPVo52L13FePvzFm07+37Nf/sidT/5JMxb7JBUI+7yUS/7fs179zJ9Q+zsoEfgEAAA6AAAAAAAA' +
684 'AAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASFpEIP3gtF780KVW/uCzXv6Tcjr9Yk0t/N6yXv7PpVX+5bdf+3VZK/8A' +
685 'AAB2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAABAAAOjsoEfjMn1D737Ne/byUS/4kFQj7STMW+8idT/7fs1/+' +
686 'xZtO/nFePvyjnYvXLyIT1YBjMf/csVz93bFe/s2hT/p9YTP/qqCKzf///xkAAAA+MSEO35Z1PP/br1z62a1a/eG0XvyziT/8emI4' +
687 '/6Safdf//+5PAAAAAAAAAAAAAAAcEgcAiUg2G+ylgkP/37Fc/tyvXPvVqlj94LNe+8ufT/+Zdzz/dWVI7pyWiI7///8iAAAAAAAA' +
688 'AABtbUgHVVVVAwAAAAAAAAAAAAAAAAAAADsMAwCMNiUR5nVYK/+yjkv/2Kxa/uCyXfvesV375Ldf+kMuE/sAAABAAAAAAAAAAAMA' +
689 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
690 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
691 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
692 'AAAAAAAAVVVVAx4SB862kEz80adY/5JyO/9TPBv9IBQIvQAAAGIAAAAVAAAAAAAAAAAAAAACVQAAA39/PwQAAAAAAAAAAP///xWu' +
693 'n3h7gm9N6ZF1Rf/BmEz/4LJd+9WqWf3arVr737Ne/rGKRv9XPx35GwwDoAAAACgAAAAAAAAAAP///wne1LCPgHFU/4tqMf/SplP6' +
694 '27Bd/t2xXPvKoVP/Y0kj/xAGAJsAAAAD////NY6Iduh4XzX/1KhU+tuwXP3dr1r7ak8j/wwICJ3HxK2mdl86/cSbTP3gtGD+zaBP' +
695 '/kw2GvwxJxz7nno7/t2wW/7gs135XUQg/wQAAHYAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAB/fwACAAAAAX1uVjWTj39+5enaRjAk' +
696 'Gc+gfD3/3bFd/N6xXf6Tcjr9Yk0t/NywXf7arlr9uZJM/jUmFtzEwa5PlpCAfX1uVjUAAAABf38AAgAAAAAAAAAAAAAAAAAAAAAA' +
697 'AAAEAAAAAAQAAHZdRCD/4LNd+d2wW/6eejv+MScc+0w2GvzNoE/+4LRg/sSbTP12Xzr9x8StpgwICJ1qTyP/3a9a+9uwXP3UqFT6' +
698 'eF81/46Iduj///81AAAAAxAGAJtjSSP/yqFT/92xXPvbsF3+0qZT+otqMf+AcVT/3tSwj////wkAAAAAAAAAAAAAACgbDAOgVz8d' +
699 '+bGKRv/fs17+2q1a+9WqWf3gsl37wZhM/5F1Rf+Cb03prp94e////xUAAAAAAAAAAH9/PwRVAAADAAAAAgAAAAAAAAAAAAAAFQAA' +
700 'AGIgFAi9Uzwb/ZJyO//Rp1j/tpBM/B4SB85VVVUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
701 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
702 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
703 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAAAFAAAAABUKBHVEMBf/QC0T5goBAI0AAAA6AAAAAAAA' +
704 'AAAAAAAAAAAABAAAAAKqqqoDAAAAAQAAAAD///8Mr62idHpoReOEZjX/upNL/9+xWvrXq1n92q1a+9+yXf25k03/ZU4o/SITBawA' +
705 'AAAvAAAAAAAAAAAAAAAA////Maqijr6BZzr/r4c//N6yXvzZrlv937Jc+p98P/8yIQ7yBwAAbQAAAAD//+1XjoBi/IloL//cr1r7' +
706 '3bJd/dSpWf5bQyD/DwkHh9vYwph8aEb/v5RG/d6yX/7Mn0/+VToW/F5PNvx8XzP916tY/uG0XvuHZS//EQcBrAAAAAAAAAADAAAA' +
707 'AAAAAAAAAAAAAAAAAAAAAAFVAAADAAAAACUYC8R4Wy7/d2A9/049J/NDMBX916xa/uCzXf6Tcjr9Yk0t/NywXf7cr1v+WUIg/EMx' +
708 'GfZ2XTT+eFou/yUYC8QAAAAAVQAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAARBwGsh2Uv/+G0XvvXq1j+fF8z/V5PNvxV' +
709 'Ohb8zJ9P/t6yX/6/lEb9fGhG/9vYwpgPCQeHW0Mg/9SpWf7dsl393K9a+4loL/+OgGL8///tVwAAAAAHAABtMiEO8p98P//fslz6' +
710 '2a5b/d6yXvyvhz/8gWc6/6qijr7///8xAAAAAAAAAAAAAAAAAAAALyITBaxlTij9uZNN/9+yXf3arVr716tZ/d+xWvq6k0v/hGY1' +
711 '/3poReOvraJ0////DAAAAAAAAAABqqqqAwAAAAIAAAAEAAAAAAAAAAAAAAAAAAAAOgoBAI1ALRPmRDAX/xUKBHUAAAAAMwAABQAA' +
712 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
713 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
714 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
715 'AAAAAAAAAAABAAAAACofChgOAgBZAAAAGAAAAAAAAAAAAAAAAQAAAAQAAAAB////AQAAAAMAAAAA////BLKyo2d2a1fThWk9/7OL' +
716 'RP/dsFr62K1a/dasWvzfs178u5ZP/2pSK/8jFgirAAAANgAAAAAAAAAAAAAAAP///wfb28lyiHxh8o5tN//RpFL7269d/uCyXfvN' +
717 'olL/Zkwl/xYLArUAAAAmAAAAAOTgzH16bFL/nXY1/d+zX/zdsl38upRN/0IwGPkAAABg8vDWi35qSP+/k0T83LBe/t6xXf1QNxT9' +
718 'c2td9n1nPfvJnU3+4rVf/LCKRv4dDwXgCwsAF38AAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAABQAAX1tBHP/IkDn4ypA3' +
719 '/pBmJ/8MBADxiWs4/uy9Y/6RcDn9X0ss/Oe5Yv6ffD/+EAUA+pJoKf3KkTj+yJA5+FtBG/8FAABfAAAAAAAAAAMAAAAAAAAAAAAA' +
720 'AAAAAAAAAAAAAn8AAAILCwAXHQ8F4LCKRv7itV/8yZ1N/n1nPftza132UDcU/d6xXf3csF7+v5NE/H5qSP/y8NaLAAAAYEIwGPm6' +
721 'lE3/3bJd/N+zX/yddjX9emxS/+TgzH0AAAAAAAAAJhYLArVmTCX/zaJS/+CyXfvbr13+0aRS+45tN/+IfGHy29vJcv///wcAAAAA' +
722 'AAAAAAAAAAAAAAA2IxYIq2pSK/+7lk//37Ne/NasWvzYrVr93bBa+rOLRP+FaT3/dmtX07Kyo2f///8EAAAAAAAAAAP///8BAAAA' +
723 'AQAAAAQAAAABAAAAAAAAAAAAAAAYDgIAWSofChgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
724 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
725 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
726 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAAA' +
727 'AAAAAAAAAAABAAAAAAAAAADJxrFVg3pnyHhjP/+yiUL/3q9Z+9muW/3Wq1n84rRe+8yhUv9zWCz/JxgKvQAAAEsAAAAAAAAAAAAA' +
728 'AAQAAAAA////L6ulj7qCbEX/rYY//96yXPzarlv93rJc/J57P/84JQ7vBwAAagAAAAD///8ByMaxnnRjRf+viED93bFe/eG0Xvq1' +
729 'jkf/Lh0L7AAAADz///9pcWNL/7aMQPzfs17937Ne/WJIH/94bVfzi3lW+KaBQf7htWD+yp9Q/DckDfwIAAA6AAAAAAAAAAMAAAAA' +
730 'AAAAAAAAAAAAAAAAAAAAAgAAAAEKAAAZLh4M7rB/Mv7Pnkv8pIZQ+lZBIf48NiqDJRcJ1dirWf+WdTv8YEss/OS4Yv87KBH0MCoh' +
731 'oldCIfujhlD7z55L/LB/Mv4uHgzuCgAAGQAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAACAAAOjckDfzKn1D84bVg/qaB' +
732 'Qf6LeVb4eG1X82JIH//fs17937Ne/baMQPxxY0v/////aQAAADwuHQvstY5H/+G0XvrdsV79r4hA/XRjRf/IxrGe////AQAAAAAH' +
733 'AABqOCUO7557P//eslz82q5b/d6yXPythj//gmxF/6ulj7r///8vAAAAAAAAAAQAAAAAAAAAAAAAAEsnGAq9c1gs/8yhUv/itF77' +
734 '1qtZ/NmuW/3er1n7solC/3hjP/+DemfIycaxVQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAA' +
735 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
736 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
737 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
738 'AAAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAQAAAAAAAAAAAAAAAP///wH//wAB///aHJWCXb16Z0T/qoZF/9eqV/vcsF381KlX' +
739 '/eO1XvrNo1T/fmAu/zMhDcwAAABZAAAAAAAAAACRkW0HAAAAAP///wLm5M5zfXRk8odpOP/TpVD6269d/d6xXfvInlH/Zkwl/xMI' +
740 'ALgSEhIqAAAAAP///xm4sJrEd2E8/7yTR/vesl7+4bRd+qB9Qf8oGQvbAAAAKP//9W1zZUz/rYU9/d2yXv7esl78Zksh/1JIOsme' +
741 'lX7ojmwx/+CzXv7dsFr6YEci/wQAAHQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAMBAGhfVon/8qQN/vGpmP9' +
742 'Tz0j+0w5IPxPPiTtHhEH8HleMPybekD9bVUu/JJ1QPweEQX5Tj0j8Uw5IPxPPSP7xqZj/cqQN/t9Wif/DAQBoQAAAAAAAAADAAAA' +
743 'AAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAHRgRyL/3bBa+uCzXv6ObDH/npV+6FJIOslmSyH/3rJe/N2yXv6thT39c2VM////' +
744 '9W0AAAAoKBkL26B9Qf/htF363rJe/ryTR/t3YTz/uLCaxP///xkAAAAAEhISKhMIALhmTCX/yJ5R/96xXfvbr13906VQ+odpOP99' +
745 'dGTy5uTOc////wIAAAAAkZFtBwAAAAAAAAAAAAAAWTMhDcx+YC7/zaNU/+O1XvrUqVf93LBd/NeqV/uqhkX/emdE/5WCXb3//9oc' +
746 '//8AAf///wEAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
747 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
748 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
749 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
750 'AAAAAAAAAAH/AAAB1L+UDCgaCdu7kUf85bdf+tSpWP3gs1370KZY/45vOv89LBPaAAAAYwAAAAMAAAAA////AioAAAYAAAAA////' +
751 'MKeikLtyYUb/oHs6/92wW/vXrVv94bNd+6qGRv84JhHwAAAAawAAAAAAAAAA////LaGZh891XDX/yp9M+t2yXv3esFr6fF8w/xcL' +
752 'A8UAAAAO////YH1tUv+shDv84LRf/eK1Xvp6XCz/GxMTncu+mLZ9YDD/1atZ/eS3YPqFZTH/DgQAsAAAAAAAAAADAAAAAQAAAAAA' +
753 'AAAAAAAAAAAAAAAAAAADAAAAAAMAAEBDLhP/wYo1+9OhS/2Kdkv7pXYv/lE4FvtwUCD/Zkke/xsQB/qAZzn8bVIq/CUXCPpiRx79' +
754 'cFAg/1E4Fvuldi/+inZL+9OhS/3BijX7Qy4T/wMAAEAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAOBACwhWUx' +
755 '/+S3YPrVq1n9fWAw/8u+mLYbExOdelws/+K1XvrgtF/9rIQ7/H1tUv////9gAAAADhcLA8V8XzD/3rBa+t2yXv3Kn0z6dVw1/6GZ' +
756 'h8////8tAAAAAAAAAAAAAABrOCYR8KqGRv/hs137161b/d2wW/ugezr/cmFG/6eikLv///8wAAAAACoAAAb///8CAAAAAAAAAAMA' +
757 'AABjPSwT2o5vOv/Qplj/4LNd+9SpWP3lt1/6u5FH/CgaCdvUv5QM/wAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
758 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
759 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
760 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
761 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAFNyVir/7L1i+M+mWP+Scz3/' +
762 'Pi0W5wQAAG0AAAAMAAAAAAAAAABtbW0HAAAAAP///wTp5tN0fXVi735kN//JnU363bJe/d2xXPzRplf/dVot/x4QBLcAAAAjAAAA' +
763 'AAAAAAD///hJkIh25YFjMf/ZrFf73LFd/ditWv1rUSf/EwcAqwAAAAT///9cdm1c/qB7OvvitV/937Jc+odmMv8GAACm3te8h3Ff' +
764 'Qf/JnUz847Zf/K+IRP8hFAjbDAAMFAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACIhEADyEUB9SkdzH+z5Q5/LyeYPx+' +
765 'YC38qHkx/iEVCvqleTP9OicR+ikaC/tdRiL7blUq/BgNBfo7KBH7pHgz/SEVCvqoeTH+fmAt/LyeYPzPlDn8pHcx/iEUB9QiEQAP' +
766 'AAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIMAAwUIRQI26+IRP/jtl/8yZ1M/HFfQf/e17yHBgAApodmMv/fslz64rVf' +
767 '/aB7Ovt2bVz+////XAAAAAQTBwCra1En/9itWv3csV392axX+4FjMf+QiHbl///4SQAAAAAAAAAAAAAAIx4QBLd1Wi3/0aZX/92x' +
768 'XPzdsl79yZ1N+n5kN/99dWLv6ebTdP///wQAAAAAbW1tBwAAAAAAAAAAAAAADAQAAG0+LRbnknM9/8+mWP/svWL4clYq/wAAAFMA' +
769 'AAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
770 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
771 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
772 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
773 'AAAAAAAAAAAAAAAAAAABf38AAgAAAAUuHQrRlnQ6/0Y0GeMMAgB+AAAAGAAAAAAAAAAAVVUAAwAAAAIAAAAA////LL+0lbx1Y0P/' +
774 'oHw9/96wW/vXrFv+47Ve+qqGRv9CMBXtAgAAbgAAAAAAAAAAAAAAAP//72KGemL8jmwx/9yvW/vesl38xZ9U/VM+H/0RBwGFAAAA' +
775 'AP//9lZ7cmH/l3Y5/N2yXv3itF36jGoz/wAAAKv//+Zdh3RO/KuEP/7kuWP8xJlM/TIfCvYIAAA7AAAAAAAAAAMAAAAAAAAAAAAA' +
776 'AAAAAAAAAAAAAAAAAAQAAAAACwMAg3BQIv/MkTf616hW/XprS/mtey/+UTgZ+0AsEfujdzL9GA0D+49oLf1KNBb8PSkQ+4tlLP0Z' +
777 'DQP7o3cy/UAsEftROBn7rXsv/nprS/nXqFb9zJE3+nBQIv8LAwCDAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAA' +
778 'CAAAOzIfCvbEmUz95Llj/KuEP/6HdE78///mXQAAAKuMajP/4rRd+t2yXv2Xdjn8e3Jh////9lYAAAAAEQcBhVM+H/3Fn1T93rJd' +
779 '/NyvW/uObDH/hnpi/P//72IAAAAAAAAAAAAAAAACAABuQjAV7aqGRv/jtV7616xb/t6wW/ugfD3/dWND/7+0lbz///8sAAAAAAAA' +
780 'AAJVVQADAAAAAAAAAAAAAAAYDAIAfkY0GeOWdDr/Lh0K0QAAAAV/fwACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
781 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
782 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
783 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
784 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAfDwNQFwkBmgAAABYA' +
785 'AAAAAAAAAAAAAAP///8Bf38AAv///wfz79RzkYNi831gMf/Im0v73bFd/dywXPzVqlf9f2Ex/yESBdcAAAA0AAAAAAAAAAIAAAAA' +
786 '6urVe3dpT/6adjj93rFc/OCzXvu+lUz/OigR+wcAAGcAAAAA///3RYl7YPaVcTX94LRf/eGzXPuWcTX/CgAAwv//4ziajXLolHI1' +
787 '/uK2YPzXqlb6UDsb/wIAAFoAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIFAAAvMiAO+LiHN/vKkzz+r5Zi/INh' +
788 'K/yoeTD8KyET+3xbKf1+WiX8Rzwq+o5oLf1JMxb8STMV/I9oLv1HPCr6flol/HxbKf0rIRP7qHkw/INhK/yvlmL8ypM8/riHN/sy' +
789 'IA74BQAALwAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAIAAFpQOxv/16pW+uK2YPyUcjX+mo1y6P//4zgKAADC' +
790 'lnE1/+GzXPvgtF/9lXE1/Yl7YPb///dFAAAAAAcAAGc6KBH7vpVM/+CzXvvesVz8mnY4/XdpT/7q6tV7AAAAAAAAAAIAAAAAAAAA' +
791 'NCESBdd/YTH/1apX/dywXPzdsV39yJtL+31gMf+Rg2Lz8+/Uc////wd/fwAC////AQAAAAMAAAAAAAAAAAAAABYXCQGaHw8DUAAA' +
792 'AAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
793 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
794 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
795 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
796 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAB/f38C////AcK1kqSDbkb/oXo4/t2wW/vY' +
797 'rVv947Ve+rOMSP9HNBj+CAAAjxcXAAsAAAAAAAAAAf///xDRzLKhc2JH/6+GPvzitV/93rJe+66JR/8wIAzvEwkGUAAAAAD///BG' +
798 'kIdx85NvNP7esV3837Fb+5VvNP8RCADL9/ffIL22ocl7Xi//3bBa/OK0X/t6Wyr/CAAAlQAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA' +
799 'AAAAAAAAAAAAVQAAAwAAAAAVCwK1kmos/8uPNPvQp1n+b14+/LF8Lf1kTCb/ZFY++5BnKv5hRyL9b2VT+ZVrLf1IMhT8SDIU/JVr' +
800 'Lf1vZVP5YUci/ZBnKv5kVj77ZEwm/7F8Lf1vXj780KdZ/suPNPuSaiz/FQsCtQAAAABVAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
801 'AAAAAAAABAAAAAAIAACVelsq/+K0X/vdsFr8e14v/722ocn3998gEQgAy5VvNP/fsVv73rFd/JNvNP6Qh3Hz///wRgAAAAATCQZQ' +
802 'MCAM766JR//esl774rVf/a+GPvxzYkf/0cyyof///xAAAAABAAAAABcXAAsIAACPRzQY/rOMSP/jtV762K1b/d2wW/uhejj+g25G' +
803 '/8K1kqT///8Bf39/AgAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
804 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
805 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
806 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
807 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAA' +
808 'AAEAAAAAAAAAAAAAAAAAAAACAAAAABILA0VELRH737Ng+9+yXfvVqVj+fmAv/yMUB9MAAABBAAAAAP///wEAAAAA////FrSwoLh2' +
809 'YDv/vpNG+92xXv7gs1v6kG43/xsOBM4YEgwqAAAAAP///z+UjHjyj2wx/t2xXPzhtF/7qYNB/xwPA9gVKj8M497Fk31kPP/OolH7' +
810 '4rVf/J97PP8aDwXJFwAXCwAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAP//AAFtbUgHAAAAAAYAAFVTOhn/y5M6+8uVQP6xllz6k20u' +
811 '+pltJ/9SSj3ThXRT8JZqJf9DNCHaioNxzptvK/88KBD5PCgQ+ZtvK/+Kg3HOQzQh2pZqJf+FdFPwUko905ltJ/+TbS76sZZc+suV' +
812 'QP7Lkzr7UzoZ/wYAAFUAAAAAbW1IB///AAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAQXABcLGg8FyZ97PP/itV/8zqJR+31kPP/j' +
813 '3sWTFSo/DBwPA9ipg0H/4bRf+92xXPyPbDH+lIx48v///z8AAAAAGBIMKhsOBM6Qbjf/4LNb+t2xXv6+k0b7dmA7/7SwoLj///8W' +
814 'AAAAAP///wEAAAAAAAAAQSMUB9N+YC//1alY/t+yXfvfs2D7RC0R+xILA0UAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAADAAAA' +
815 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
816 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
817 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
818 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
819 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAKAwCNdVkt/7mR' +
820 'Sv9GMRb7CgEAjwAAAAsAAAAAf38/BAAAAAD///8qoJ2KznpfM//KoFD63bFd/t+yXfqCYzD/FAgAtzAkGBUAAAAA////MZmTgd1/' +
821 'YTD/3LBb/N6yXvyzjUn/LyAQ6QAAABz///9dgXNY/66GPfvjt2H9w5tP/TAeCvMGAAAoAAAAAAAAAAIAAAAAAAAAAKqqqgN/fz8E' +
822 'AAAAAQAAAAAAAAAAAAAnDSoaCeCygzf+xY02/M6cR/5YRSb/X0Mb/0cwD+yKjIeTfmM4/IRbH/8wKyaSiHxmw5huK/8uHQv4Lh0L' +
823 '+JhuK/+IfGbDMCsmkoRbH/9+Yzj8ioyHk0cwD+xfQxv/WEUm/86cR/7FjTb8soM3/ioaCeAAACcNAAAAAAAAAAAAAAABf38/BKqq' +
824 'qgMAAAAAAAAAAAAAAAIAAAAABgAAKDAeCvPDm0/947dh/a6GPfuBc1j/////XQAAABwvIBDps41J/96yXvzcsFv8f2Ew/5mTgd3/' +
825 '//8xAAAAADAkGBUUCAC3gmMw/9+yXfrdsV3+yqBQ+npfM/+gnYrO////KgAAAAB/fz8EAAAAAAAAAAsKAQCPRjEW+7mRSv91WS3/' +
826 'CgMAjQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
827 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
828 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
829 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
830 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
831 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAABIhMGxSYXCNgAAABFAAAAAAAAAABmZjMF////Af/430qhlnvug2Uz/9Wo' +
832 'Vfvesl390qdY+2RMJf8PBACmcVU4CQAAAAD///8vnJWC3H1fLv/brln837Ne/LONSf8tHw7pAAAAKP///yiQhW7zmHQz/OO4YvzX' +
833 'qVb7TzgZ/wUAAFgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///x/U1MdOemM7q7angv7Jo175zJE1/JpxMP4ZDQLL' +
834 'BwAAYiERB2SimX2llGwv/F5AGP1GRD9shXlj1JdrKP8xHw35MR8N+ZdrKP+FeWPURkQ/bF5AGP2UbC/8opl9pSERB2QHAABiGQ0C' +
835 'y5pxMP7MkTX8yaNe+bangv56Yzur1NTHTv///x8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAUAAFhPOBn/16lW++O4' +
836 'YvyYdDP8kIVu8////ygAAAAoLR8O6bONSf/fs178265Z/H1fLv+clYLc////LwAAAABxVTgJDwQApmRMJf/Sp1j73rJd/dWoVfuD' +
837 'ZTP/oZZ77v/430r///8BZmYzBQAAAAAAAAAAAAAARSYXCNgiEwbFAAAAAQAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
838 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
839 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
840 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
841 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
842 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAHxUK' +
843 'GBoNABMAAAAAAAAABAAAAAF/f38CAAAAAJKIcTYuHQrwupNL/Om7Y/vHnU/+RTEW/wQAAHoAAAAAAAAAAP///yGmoZLNd1wu/96w' +
844 'Wvvdsl78wJdM/zYkD/UAAAAw////Abavl9R8YDL/2a1Z/OG0Xvp7Xi7/CwEAmAAAAAAAAAAEAAAAAP//AAEAAAAA4tSqEuXfxlqk' +
845 'mn+EhHxpyHNiR+trVjT/inNJ/7SlgfJ4aEr3qHgt/0s1F/8HAwBCAAAAAP///xt8a0j6d1Qf/zMhCe2urqU8gXNZ6JptKf8vHwzQ' +
846 'Lx8M0JptKf+Bc1norq6lPDMhCe13VB//fGtI+v///xsAAAAABwMAQks1F/+oeC3/eGhK97SlgfKKc0n/a1Y0/3NiR+uEfGnIpJp/' +
847 'hOXfxlri1KoSAAAAAP//AAEAAAAAAAAABAAAAAALAQCYe14u/+G0XvrZrVn8fGAy/7avl9T///8BAAAAMDYkD/XAl0z/3bJe/N6w' +
848 'Wvt3XC7/pqGSzf///yEAAAAAAAAAAAQAAHpFMRb/x51P/um7Y/u6k0v8Lh0K8JKIcTYAAAAAf39/AgAAAAEAAAAEAAAAABoNABMf' +
849 'FQoYAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
850 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
851 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
852 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
853 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
854 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAABFQS8X' +
855 '/q+KR/9DLhT2BQAAXgAAAAAzMzMF////Aa2kjalpUi//3K1Y+OC0X/y/lkz/OCUO9A0DAEsAAAAAxsGrq3phOv/YqlT45Lhh/KB6' +
856 'O/8YCwPIMzMzBQAAAAMAAAABAAAAAP//AAEAAAAB1Lt/Hk9DMednTCb/jWcr/5BnJ/+4iTv6vayC8YxtOPt1YDn4EQYB9SQWCZ8A' +
857 'AAAAMwAABf//zA9IOSdVFgsDhw0AAID///8kfm5R+Kp4LPwvIAzOLyAMzqp4LPx+blH4////JA0AAIAWCwOHSDknVf//zA8zAAAF' +
858 'AAAAACQWCZ8RBgH1dWA5+IxtOPu9rILxuIk7+pBnJ/+NZyv/Z0wm/09DMefUu38eAAAAAf//AAEAAAAAAAAAAQAAAAMzMzMFGAsD' +
859 'yKB6O//kuGH82KpU+HphOv/GwaurAAAAAA0DAEs4JQ70v5ZM/+C0X/zcrVj4aVIv/62kjan///8BMzMzBQAAAAAFAABeQy4U9q+K' +
860 'R/9BLxf+AAAARQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
861 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
862 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
863 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
864 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
865 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
866 'AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAD8/PwQAAAAAFw0CYBoOA+QIAAA7AAAAAAAAAAQAAAAC////AQ0AACciEgLSlnM4' +
867 '/8acUPw3JA71AwAASQAAAAD///8Fa2RUbS4cB/Ovikf/xZtO/DAgDfIFAAAsAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAA' +
868 'AE1FMBP/jWkv/31ZJP++qXX3b1cx+rmMPfuMeE/+DwMAzP//VQMAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAD//+Q6fGlG/4RdI/8p' +
869 'GwuxKRsLsYRdI/98aUb////kOgAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAv//VQMPAwDMjHhP/rmMPftvVzH6vql1931ZJP+NaS//' +
870 'RTAT/wAAAE0AAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAABQAALDAgDfLFm078r4pH/y4cB/NrZFRt////BQAAAAADAABJNyQO' +
871 '9cacUPyWczj/IhIC0g0AACf///8BAAAAAgAAAAQAAAAACAAAOxoOA+QXDQJgAAAAAD8/PwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
872 'AAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
873 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
874 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
875 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
876 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
877 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC' +
878 'AAAAACocDhIAAAAAAAAAAwAAAAAAAAAAAAAAAgAAAAAAAAAYIhUGwDQjD/8OAgBYAAAAAAAAAAQAAAACAAAAAAAAACgpGAi6OCQO' +
879 '/xEIAFYAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAf39/AgAAAAAcEAh+BAAAnn13YpiDdlv5fVUc/bKKRvyDdFf/FwsA5j0o' +
880 'ChkAAAAAAAAAAgAAAABVVQADVVVVBgAAAAH69uE8Wk06tBoMAbUhFAdkIRQHZBoMAbVaTTq0+vbhPAAAAAFVVVUGVVUAAwAAAAAA' +
881 'AAACAAAAAD0oChkXCwDmg3RX/7KKRvx9VRz9g3Zb+X13YpgEAACeHBAIfgAAAAB/f38CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE' +
882 'AAAAABEIAFY4JA7/KRgIugAAACgAAAAAAAAAAgAAAAQAAAAADgIAWDQjD/8iFQbAAAAAGAAAAAAAAAACAAAAAAAAAAAAAAADAAAA' +
883 'ACocDhIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
884 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
885 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
886 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
887 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
888 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
889 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAA' +
890 'IRYLFxMJAE8AAAAAAAAAAwAAAAAAAAAAPz8/BAAAAAAAAAAMEgkAVAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
891 'AH9/fwIAAAAA////Aa+okMh3VSL+sYI2/FxFJPl4b130IBIE7SAYCB8AAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
892 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAACAYCB8gEgTteG9d9FxFJPmxgjb8d1Ui/q+okMj/' +
893 '//8BAAAAAH9/fwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAASCQBUAAAADAAAAAA/Pz8EAAAAAAAAAAAAAAAD' +
894 'AAAAABMJAE8hFgsXAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
895 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
896 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
897 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
898 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
899 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
900 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
901 'AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAA' +
902 'AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAEzAAAF6syZGT0vHNaOZyv9clMk/w8JB4Ogj22VJxgJ' +
903 '/xYQAC4AAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAD///8Bf38/BFUAAAMAAAACAAAAAlUAAAN/fz8E////AQAAAAAAAAAAAAAAAAAA' +
904 'AAAAAAADAAAAABYQAC4nGAn/oI9tlQ8JB4NyUyT/jmcr/T0vHNbqzJkZMwAABf8AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
905 'AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA' +
906 'AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
907 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
908 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
909 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
910 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
911 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
912 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
913 'AAAAAAAAAQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
914 'AAAAAAAAAAAAAAADAAAAAAAAADMsHAz/JhgJzQAAAAIAAAAOIBIG6h8RBDkAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
915 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAB8RBDkgEgbqAAAADgAAAAImGAnNLBwM/wAA' +
916 'ADMAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAA' +
917 'AAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
918 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
919 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
920 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
921 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
922 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
923 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
924 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
925 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVVVVAwAAAAAbDwRxGhEEOQAAAAAAAAAA' +
926 'HhEFZCEWCFwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
927 'AAAAAAAAAAADAAAAACEWCFweEQVkAAAAAAAAAAAaEQQ5Gw8EcQAAAABVVVUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
928 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
929 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
930 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
931 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
932 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
933 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
934 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
935 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
936 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
937 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABVQAAAyocABIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
938 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACocABJVAAADAAAAAQAAAAEAAAAAAAAA' +
939 'AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
940 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
941 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
942 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
943 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
944 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
945 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
946 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
947 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
948 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAgAAAAAA' +
949 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
950 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
951 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
952 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
953 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
954 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
955 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
956 shape:
957 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
958 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
959 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
960 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
961 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAA' +
962 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
963 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAA' +
964 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
965 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
966 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
967 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
968 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
969 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
970 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
971 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
972 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
973 'AAAAAAAAAAIAAAADAAAAAAAAAAAAAAAJBQIAWhEIAloTCwVaEwsFWhMLBVoTCwVaFAgCWQoFAl4AAAAaAAAAAAAAAAEAAAAAAAAA' +
974 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGgoFAl4UCAJZEwsFWhML' +
975 'BVoTCwVaEwsFWhEIAloFAgBaAAAACQAAAAAAAAAAAAAAAwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
976 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
977 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
978 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
979 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
980 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
981 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
982 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
983 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
984 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAACAgIHQYEAHgMBwHSHxMH/yITB/8bDgP/' +
985 'HA8E/xwPBP8cDwT/IhMG/xYNBP8YEgwqqlUAAwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
986 'AAAAAAAAAAAAAAAAAAAAAAKqVQADGBIMKhYNBP8iEwb/HA8E/xwPBP8cDwT/Gw4D/yITB/8fEwf/DAcB0gYEAHgICAgdAAAAAAAA' +
987 'AAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
988 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
989 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
990 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
991 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
992 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
993 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
994 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
995 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAMA' +
996 'AAAAAAAAAAkJCTcAAACXBQAA6yATBv8eEAX/NSQP+buhZfnXvnz50Ll3+c+4d/nexH72XUko/QsCAMUAAAACAAAAAKqqVQMAAAAB' +
997 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaqqVQMAAAAAAAAAAgsCAMVdSSj93sR+' +
998 '9s+4d/nQuXf51758+buhZfk1JA/5HhAF/yATBv8FAADrAAAAlwkJCTcAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAA' +
999 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1000 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1001 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1002 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1003 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1004 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1005 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1006 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1007 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAHBwdDAAAAuQoBAPo6KRX/ZlIv/x0QBfclFwn4EgYA+5F4Sfv/' +
1008 '7Jv7+N2R+/zilPv43ZD7SDUc+xMJAfaCbELHFQ8JVQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1009 'AAAAAAAAAAAAAAABAAAAAAAAAAAVDwlVgmxCxxMJAfZINRz7+N2Q+/zilPv43ZH7/+yb+5F4SfsSBgD7JRcJ+B0QBfdmUi//OikV' +
1010 '/woBAPoAAAC5BwcHQwAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1011 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1012 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1013 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1014 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1015 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1016 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1017 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1018 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAQAAAAAJEgkBwAA' +
1019 'AIoIAAD8STgh/6OJU/v22434x7Bx+gkAAPsbDwT6BQAA+4VuQvv84ZP76tGI+/bckfvQtHH7Gw0E/EEvGP35037/waVi/2BRMKIA' +
1020 'AAAQAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAEGBRMKLBpWL/+dN+/0EvGP0bDQT8' +
1021 '0LRx+/bckfvq0Yj7/OGT+4VuQvsFAAD7Gw8E+gkAAPvHsHH69tuN+KOJU/tJOCH/CAAA/AAAAIokSCQHAAAAAAAAAAQAAAABAAAA' +
1022 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1023 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1024 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1025 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1026 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1027 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1028 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1029 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1030 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHBwdBAAAA0CIVCP+ZgE76+t6O+P3jlPr435P79tuN+3plOvtBLhn7V0Uo+t/F' +
1031 'gPr/6pr6+d+S+v/wn/qpkVv7BwAA/G1XL/z10Xz899R++u3KeP9VSCq+AAAABv///wEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1032 'AAAAAAAAAf///wEAAAAGVUgqvu3KeP/31H769dF8/G1XL/wHAAD8qZFb+//wn/r535L6/+qa+t/FgPpXRSj6QS4Z+3plOvv22437' +
1033 '+N+T+/3jlPr63o74mYBO+iIVCP8AAADQBwcHQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1034 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1035 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1036 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1037 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1038 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1039 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1040 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1041 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAACwAAAIwDAAD5VEIk' +
1042 '/9e8ePf/6Jn68NaN+/nfkvvt0oj5xKts+sasbPy2m1/8xatq/MeubvzApmn8v6Vo/MyzcfxnVTH7CgIA/KSIT/300Xz+48Jz/vXQ' +
1043 'fPjpynj/iVs1nAAAAABVqlUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVapVAwAAAACJWzWc6cp4//XQfPjjwnP+9NF8/qSIT/0K' +
1044 'AgD8Z1Ux+8yzcfy/pWj8wKZp/MeubvzFq2r8tptf/MasbPzEq2z67dKI+fnfkvvw1o37/+iZ+te8ePdUQiT/AwAA+QAAAIwAAAAL' +
1045 'AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1046 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1047 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1048 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1049 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1050 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1051 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1052 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1053 'AAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAEAgAAsSQYC/+giFX6/eKU+fnfkvvr0on7/+eX+7qfYvkwIRH/Gw4C/xsNAvgeEQX6Gw4C' +
1054 '+hoNAfocDgL6HA4C+hwPAvgcDwT6HhAE+8OiW/3614D+58Z2/ufFdv7y0Xz67cJz/0EyHVYAAAAAVVVVAwAAAAAAAAAAAAAAAAAA' +
1055 'AABVVVUDAAAAAEEyHVbtwnP/8tF8+ufFdv7nxnb++teA/sOiW/0eEAT7HA8E+hwPAvgcDgL6HA4C+hoNAfobDgL6HhEF+hsNAvgb' +
1056 'DgL/MCER/7qfYvn/55f769KJ+/nfkvv94pT5oIhV+iQYC/8CAACxAAAABAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1057 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1058 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1059 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1060 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1061 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1062 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1063 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1064 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAABgYpCgIA/41zQfjrzYD7' +
1065 '58yB+vXbkPv/5pf7vaJi+BsNBP8AAADfCwcDRAoFBS8FBQAzCgUFMgoKBTIKBQUyCQQENQsLBS0KBwOpIBIG/DQjEfm1mFj98tB7' +
1066 '/ujGdv7oxXb899Z//ox3SNsAAAAL////AQAAAAAAAAAAAAAAAAAAAAD///8BAAAAC4x3SNv31n/+6MV2/OjGdv7y0Hv+tZhY/TQj' +
1067 'EfkgEgb8CgcDqQsLBS0JBAQ1CgUFMgoKBTIKBQUyBQUAMwoFBS8LBwNEAAAA3xsNBP+9omL4/+aX+/XbkPvnzIH6682A+41zQfgK' +
1068 'AgD/AAYGKQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1069 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1070 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1071 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1072 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1073 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1074 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1075 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1076 'AAAAAAAAAAAAAAAAAAAAAAAABAAAAAAEAgJ3HhEG/yweDfwUCAD8PCsV/eTHf/rApWX4GQwE/wMBAOAPDwciAAAAAAAAAAAAAAAA' +
1077 'AAAAAAAAAAAAAAAAAAAAAAAAAAAGAwGeIBMG/xcLAvUmFwj80bFo/vDOe/7nxnX+8c56+969cf8sJxdhAAAAAKqqVQMAAAAAAAAA' +
1078 'AKqqVQMAAAAALCcXYd69cf/xznr758Z1/vDOe/7RsWj+JhcI/BcLAvUgEwb/BgMBngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1079 'AAAAAAAADw8HIgMBAOAZDAT/wKVl+OTHf/o8KxX9FAgA/CweDfweEQb/BAICdwAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1080 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1081 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1082 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1083 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1084 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1085 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1086 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1087 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAIAAAACAAAABAAAAAQMBwLPFQwE/gAAAGoA' +
1088 'AwNPBwEA/TkoEv0mGQr/AgAA4gcHByMAAAAAPwAABAAAAAMAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbBwQBrh8UB/8UBwD5' +
1089 'dV40/fbSff7oxnb+6ch2+/vWf/6zmVuzAAAAAP//fwIAAAAAAAAAAP//fwIAAAAAs5lbs/vWf/7pyHb76MZ2/vbSff51XjT9FAcA' +
1090 '+R8UB/8HBAGuAAAAGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAM/AAAEAAAAAAcHByMCAADiJhkK/zkoEv0HAQD9AAMDTwAA' +
1091 'AGoVDAT+DAcCzwAAAAQAAAAEAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1092 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1093 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1094 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1095 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1096 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1097 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAA' +
1098 'AQAAAAEAAAACAAAAAQAAAAIAAAACVVUAAwAAAAMAAAADAAAAAwAAAAQzAAAFMwAABTMAAAX//wABf39/AgAAAAAAAAAAAAAAAAAA' +
1099 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQLBwLNAAAAJgAAAAAAAAAABAgEOxEJAfMFAgDXAAAAGwAAAAAAVVUDACoqBj8vHxBV' +
1100 'VSoMIxoRHRwXCywfGAwpDAgEPwAAAEUEAAA/AAAAIhEKA+EdEAT9MCAO+tu5bf7ty3n+6ch2/PLPe//jwnPfAAAACP//AAEAAAAA' +
1101 'AAAAAP//AAEAAAAI48Jz3/LPe//pyHb87ct5/tu5bf4wIA76HRAE/REKA+EAAAAiBAAAPwAAAEUMCAQ/HxgMKRwXCywjGhEdVVUq' +
1102 'DD8vHxAAKioGAFVVAwAAAAAAAAAbBQIA1xEJAfMECAQ7AAAAAAAAAAAAAAAmCwcCzQAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1103 'AAAAAAAAAAAAAAAAAAB/f38C//8AATMAAAUzAAAFMwAABQAAAAQAAAADAAAAAwAAAANVVQADAAAAAgAAAAIAAAABAAAAAgAAAAEA' +
1104 'AAABAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1105 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1106 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1107 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1108 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAwAAAAIAAAADVVUAA1VVAANVAAAD' +
1109 'AAAAAwAAAAMAAAAEAAAABQAAAAUAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1110 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQXFxcLABwcCRQUChkxIBAfMScTGh4ZCjIjGw9BJx4ROgMBAZQAAAB6AAAAWA8F' +
1111 'AIcRBQCCDAAAdiYYCOc0JA/CIBQGpQEAAMYEAAC8AwAA2QwCAOYNAgDiFAgA7jooD/Y7KA/1HA8D/iMWCfwZDwX/AAAAPwoGAnsg' +
1112 'FAf/FwoC+LGWV/32037+5sR0/vPQe/3Iq2f6GBQMPwAAAAAAAAACAAAAAgAAAAAYFAw/yKtn+vPQe/3mxHT+9tN+/rGWV/0XCgL4' +
1113 'IBQH/woGAnsAAAA/GQ8F/yMWCfwcDwP+OygP9TooD/YUCADuDQIA4gwCAOYDAADZBAAAvAEAAMYgFAalNCQPwiYYCOcMAAB2EQUA' +
1114 'gg8FAIcAAABYAAAAegMBAZQnHhE6IxsPQR4ZCjIxJxMaMSAQHxQUChkAHBwJFxcXCwAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1115 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAUAAAAFAAAABAAAAAMA' +
1116 'AAADVQAAA1VVAANVVQADAAAAAwAAAAIAAAADAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1117 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1118 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1119 'AAEAAAABAAAAAQAAAAIAAAACAAAAAwAAAAQ/AAAEAAAAA1VVAANVVQADVQAAAwAAAAMAAAACAAAAAwAAAAQAAAAEAAAAAAAAAAAA' +
1120 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAUADw8R' +
1121 'AAoKGAALCxYRCwUsJRoKMCIWCy0fFApKLiEPUxsSCVQAAABwAAAAbgAAAH8NAwCcDQMAlgkAAKAnGQm7OCYQuBwPA8cFAADVCAAA' +
1122 '0QsCAO0TBgD1DwMA8hsNA/tBLhP/PCoR/ycXCP8xIRD/MCEP/zclEP9CLxX/QS8Y/1tGI/9vViz/Yksl/3VbMf+BaDv/fGM3/5B1' +
1123 'Qf+WekT/kXM9/6qOUf+pjFD/spVV/beWU/kdEAX+AAAANgAAAEgeEgf/EgYA9pB2RP331H/+58V1/urJdvv20n3/v6BhWQAAAAD/' +
1124 '/6oD//+qAwAAAAC/oGFZ9tJ9/+rJdvvnxXX+99R//pB2RP0SBgD2HhIH/wAAAEgAAAA2HRAF/reWU/mylVX9qYxQ/6qOUf+Rcz3/' +
1125 'lnpE/5B1Qf98Yzf/gWg7/3VbMf9iSyX/b1Ys/1tGI/9BLxj/Qi8V/zclEP8wIQ//MSEQ/ycXCP88KhH/QS4T/xsNA/sPAwDyEwYA' +
1126 '9QsCAO0IAADRBQAA1RwPA8c4JhC4JxkJuwkAAKANAwCWDQMAnAAAAH8AAABuAAAAcBsSCVQuIQ9THxQKSiIWCy0lGgowEQsFLAAL' +
1127 'CxYACgoYAA8PEQAAAAUAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1128 'AAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAADAAAAAgAAAANVAAADVVUAA1VVAAMAAAADPwAABAAAAAQAAAADAAAAAgAAAAIAAAAB' +
1129 'AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1130 'AAAAAAJVAAADVVUAA38AAAL/AAABfwAAAgAAAAIAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1131 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAERERDwAADRMACwsWBgYGKAAAACUAAAAyDgcDRh4PB0MaDwdEKx4MYzAkEWkX' +
1132 'DQRuAAAAiQAAAIUAAACaDAEAtQ4CAK4MAQC7LR4L0DMhDMwMAwDbDgQA5Q0EAOMRBwD8GQsB/xQIAP8lFgf/TDcZ/zQjDv81JBH/' +
1133 'QjAY/0AuF/9MNhn/Uj0f/1JAIv9vVi7/e2E0/3FYLv+Lbz3/knhF/5B1Q/+ggUf/rI9T/66SVf66nFr9t5pZ/calX/zauW382Ldt' +
1134 '+9+8bvrsx3T68M97+u3LeP3sy3n88c97+/jVgPz41oD7+9iB/P/agv3/24P9/92E/f/bhP7/3IT9/+OI/urEb/spGgr/AAAAWgQA' +
1135 'ADQeEgb+EwcA+YVsPfz00n3+5sV2/ujHdfv31H7/l4NPdAAAAAKZmWYFmZlmBQAAAAKXg09099R+/+jHdfvmxXb+9NJ9/oVsPfwT' +
1136 'BwD5HhIG/gQAADQAAABaKRoK/+rEb/v/44j+/9yE/f/bhP7/3YT9/9uD/f/agv372IH8+NaA+/jVgPzxz3v77Mt5/O3LeP3wz3v6' +
1137 '7Md0+t+8bvrYt2372rlt/MalX/y3mln9upxa/a6SVf6sj1P/oIFH/5B1Q/+SeEX/i289/3FYLv97YTT/b1Yu/1JAIv9SPR//TDYZ' +
1138 '/0AuF/9CMBj/NSQR/zQjDv9MNxn/JRYH/xQIAP8ZCwH/EQcA/A0EAOMOBADlDAMA2zMhDMwtHgvQDAEAuw4CAK4MAQC1AAAAmgAA' +
1139 'AIUAAACJFw0EbjAkEWkrHgxjGg8HRB4PB0MOBwNGAAAAMgAAACUGBgYoAAsLFgAADRMREREPAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
1140 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAgAAAAJ/AAAC/wAAAX8AAAJVVQAD' +
1141 'VQAAAwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8/Pwh/Xz8IcVU4' +
1142 'CRcXDyAXFw8hExMMKAAAADsAAAA3AAAASg0FAF4XCwJYFQwCaiwcDYQ0JBB+EgoDlwAAAKEAAAChAQAAxQwBAMgIAADHHBAD3Dso' +
1143 'D+AuHQvjDwUA8hYKAu8UCQL7Gw4D/yITBv8eDwT/RzIX/1Q9HP89KRL/RzQb/1I+If9RPSD/X0cj/19HIv9hSSX/gmk5/4RqOf+F' +
1144 'azz/pIhO/6GFTf+rjE/+t5VR/bORT/6/n138yqpk+8mpY/vdvHD65MNz++XCdPrvynX69NB6+vnWgPr203379NJ9+/jVgPz82YL8' +
1145 '/NiC+//bg/z/24P8/9uE/f/dhf7/24P+/92F/v/bg/7713/++9d//vfUf/710n7+8c57/u/Mef7xznv+6cV0/uG8bP7pxnX+17Vq' +
1146 '/tKyaf7SsWn+vJ5c/ryeW/26nFr9n4BF/pl6Qfo4JhH/DAQAagAAACMeEwf+EwcA+opwP/z92H/+7st3/fHOefr9133/n4ZMbgAA' +
1147 'AAAAAAAAAAAAAAAAAACfhkxu/dd9//HOefruy3f9/dh//opwP/wTBwD7HhMH/gAAACQLBABrOCYR/5l6QfqfgEX+upxa/byeW/28' +
1148 'nlz+0rFp/tKyaf7XtWr+6cZ1/uG8bP7pxXT+8c57/u/Mef7xznv+9dJ+/vfUf/7713/++9d//v/bg/7/3YX+/9uD/v/dhf7/24T9' +
1149 '/9uD/P/bg/z82IL7/NmC/PjVgPz00n379tN9+/nWgPr00Hr678p1+uXCdPrkw3P73bxw+smpY/vKqmT7v59d/LORT/63lVH9q4xP' +
1150 '/qGFTf+kiE7/hWs8/4RqOf+CaTn/YUkl/19HIv9fRyP/UT0g/1I+If9HNBv/PSkS/1Q9HP9HMhf/Hg8E/yITBv8bDgP/FAkC+xYK' +
1151 'Au8PBQDyLh0L4zsoD+AcEAPcCAAAxwwBAMgBAADFAAAAoQAAAKESCgOXNCQQfiwcDYQVDAJqFwsCWA0FAF4AAABKAAAANwAAADsT' +
1152 'EwwoFxcPIRcXDyBxVTgJf18/CD8/PwgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAC' +
1153 'AAAAABIKAl8dEQacMyUPlQ0FALAAAAC2AQAAugoBANwPAwDbCQAA2yITBe0/KxLuLRwK8hgMAv0fEgf6HREG/ycYCf8tHQz/KxoJ' +
1154 '/1U/Hv9cRSL/SDQZ/1tHJv9kTiv/Y0wp/3lhNf9zWS//f2M1/5h9Rv+QdUH/mn9I/7aYWf+ylFb9wqFc+8+vZvzFolr81LNp+te2' +
1155 'bPrYt2z66MV1+uzJePruzHj79tJ6+/fSefv71n37+tiB/PrXgPz/24P8/9uD/P3agvz/3IT8/9yD/P/dhPz/3oX8/9yE/P/dhPz9' +
1156 '2ID7/9qC+/rWgPvz0X3689B8++vJd/vqxnX67Ml4+t66a/zVsWX/3Lpt/8amYP/FpWH/wKFe/6eLT/6qjlD9noNK/YRnN/6Iajj+' +
1157 'gmc3/nRcNP15YTb9ZE4q/VE9H/xSPyD8PywV/CwbCvwwIQ/8JhcJ/CATB/wfEQb8EgYA+w8DAPsMAQD7AgAA/AcAAPkbDQP/EgkB' +
1158 'iAIAAFYfEwf+GAsC+YBoPfvVvXj8ybFw+8qzc/nTunL+bV0yjhwRCy0hHA41IRwONRwRCy1tXTKO07py/sqzc/nJsXD71b14/IBo' +
1159 'PfsYCwL6HxMH/gIAAFYSCwGLGw0D/wcAAPkCAAD8DAEA+w8DAPsSBgD7HxEG/CATB/wmFwn8MCEP/CwbCvw/LBX8Uj8g/FE9H/xk' +
1160 'Tir9eWE2/XRcNP2CZzf+iGo4/oRnN/6eg0r9qo5Q/aeLT/7AoV7/xaVh/8amYP/cum3/1bFl/966a/zsyXj66sZ1+uvJd/vz0Hz7' +
1161 '89F9+vrWgPv/2oL7/diA+//dhPz/3IT8/96F/P/dhPz/3IP8/9yE/P3agvz/24P8/9uD/PrXgPz62IH8+9Z9+/fSefv20nr77sx4' +
1162 '++zJeProxXX62Lds+te2bPrUs2n6xaJa/M+vZvzCoVz7spRW/baYWf+af0j/kHVB/5h9Rv9/YzX/c1kv/3lhNf9jTCn/ZE4r/1tH' +
1163 'Jv9INBn/XEUi/1U/Hv8rGgn/LR0M/ycYCf8dEQb/HxIH+hgMAv0tHAryPysS7iITBe0JAADbDwMA2woBANwBAAC6AAAAtg0FALAz' +
1164 'JQ+VHREGnBIKAl8AAAAAfwAAAgAAAAAAAAAAAAAAAAAAAABVAAADAAAAAAwGAaguHAv/ZEwm/11GIv91XDL/dF00/3deM/+Kbjv/' +
1165 'jnVE/5l+SP+liU7/oIRL/7iYVv3FpmL+xKRg+9SxZ/rdu2371rFk+uC/cfrhwHL65cN0+vDNefvyz3v79dJ8+/3Zgfz92ID7/9mB' +
1166 '/P3ag/z/24P8/92E/f/bgvz/3IP8/9yD/P3agvz/3IT8/9yE+//bg/v/3IT7/NmC+/vYgfvwzXr77Mp4++zKefvgwHH73rtu++C+' +
1167 'cPvOq2H8yKVe/82sZP+1llb/tZdY/66RVP+Xe0X/mn5H/5B1Qf9yVyv/e2I1/3BXL/9hSij/Zk8s/1I9IP9CMBj/RDMa/zAgDv8k' +
1168 'FgfzKxwL8R4RBecUDQbUIxQH7RsNAvsbDgP7FgkA+wwBAPoJAAD6BwAA+wcAAPoFAAD7EQYA/CATB/wrHAz8LBwL/D4sFf1NORv9' +
1169 'Szcb/WFLKf1uVi79cFgw/YNmNf6Nbjj+j285/raQS/tYQB7/AAAArBAJA58hEgX/EQcA9hYLA/kVDAf5FQwG+RUMBvkWDQb5EggB' +
1170 '+RAGAPYPBAD3DgQA9xAFAPYSCAH5Fg0G+RUMBvkVDAb5FQwH+RYLA/kRBgD3HxAF/wwHA8YAAAC+WEAe/7aPSvyPbzn+jW44/oNm' +
1171 'Nf5wWDD9blYu/WFLKf1LNxv9TTkb/T4sFf0sHAv8KxwM/CATB/wRBgD8BQAA+wcAAPoHAAD7CQAA+gwBAPoWCQD7Gw4D+xsNAvsj' +
1172 'FAftFA0G1B4RBecrHAvxJBYH8zAgDv9EMxr/QjAY/1I9IP9mTyz/YUoo/3BXL/97YjX/clcr/5B1Qf+afkf/l3tF/66RVP+1l1j/' +
1173 'tZZW/82sZP/IpV7/zqth/OC+cPveu2774MBx++zKefvsynj78M16+/vYgfv82YL7/9yE+//bg/v/3IT7/9yE/P3agvz/3IP8/9yD' +
1174 '/P/bgvz/3YT9/9uD/P3ag/z/2YH8/diA+/3Zgfz10nz78s97+/DNefvlw3T64cBy+uC/cfrWsWT63btt+9SxZ/rEpGD7xaZi/riY' +
1175 'Vv2ghEv/pYlO/5l+SP+OdUT/im47/3deM/90XTT/dVwy/11GIv9kTCb/LhwL/wwGAagAAAAAVQAAAwAAAAAAAAAAAAAAAAAAAAAA' +
1176 'AAAFAAAAAAAAAHo6JxH768Vw9vLQffv31H7799R++/vXgPz/2YH8/92E/P/ehfz/3IT8/92F/f/ehfz/24P8/9yD/P3ag/v82YL7' +
1177 '/dqD+/vYgfr72IH6+teA+vfUfvv10n375sR0++LBcvzhwHP80bJp/dCwZv3NrmX9v6Be/8KkYP+zlVf/oIRM/6aJUP+Ve0j/g2k6' +
1178 '/4huPf9yWzP/ZVAs/2hSLf9TPyL/UDwg/1A9If89KxX/OScR/zkoEv8fEgb/GAwC8RoOBPMVCgHqGAsC3hIHAeEBAADQEAQAzQ8E' +
1179 'ANEAAACzAAAAnQAAAJwAAAB5AAAAcQIAAGoAAABMAAAAVQAAAGcAAABkBAIAeRIIAZYAAACxEAUA4xcLAvoWCgL7HRAF/TAhDvxA' +
1180 'Lhf9WEEf/nphNv6WdT3+p4NE/r2XUf7TqVn+16tZ/t6xXP7luGD+5bdg/ui6Yv7qvGP+27Fg/sqhVP68lk7+n39E/n1gL/w9KhP+' +
1181 'DQgDzA4HAsA1JA//lXM7/ZV0O/6Uczr+lXM6/pVzOv6Uczr+lnQ7/pZ1O/+dez//oH1A/5d2PP+VdDv+lHM6/pVzOv6Vczr+lHM6' +
1182 '/pRzOv6ZeD79QzEa/xIJAtcXDgXVPCkT/n1gL/yff0T+vJZO/sqhVP7bsWD+6rxj/ui6Yv7lt2D+5bhg/t6xXP7Xq1n+06lZ/r2X' +
1183 'Uf6ng0T+lnU9/nphNv5YQR/+QC4X/TAhDvwdEAX9FgoC+xcLAvoQBQDjAAAAsRIIAZYEAgB5AAAAZAAAAGcAAABVAAAATAIAAGoA' +
1184 'AABxAAAAeQAAAJwAAACdAAAAsw8EANEQBADNAQAA0BIHAeEYCwLeFQoB6hoOBPMYDALxHxIG/zkoEv85JxH/PSsV/1A9If9QPCD/' +
1185 'Uz8i/2hSLf9lUCz/clsz/4huPf+DaTr/lXtI/6aJUP+ghEz/s5VX/8KkYP+/oF7/za5l/dCwZv3Rsmn94cBz/OLBcvzmxHT79dJ9' +
1186 '+/fUfvv614D6+9iB+vvYgfr92oP7/NmC+/3ag/v/3IP8/9uD/P/ehfz/3YX9/9yE/P/ehfz/3YT8/9mB/PvXgPz31H7799R++/LQ' +
1187 'ffvrxXD2OicR+wAAAHoAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAH49KRH6+NF49e3HdfrqxnT86cZ0/Ne2a/3Y' +
1188 't2z+zq5m/sGhXf/DpGD/uZpY/6KBRf+vkVP/mn1G/491Q/+UeUX/gWg8/3FZL/91XTP/XEcn/1E+If9SPyH/QS8Y/0AuF/8+LRf/' +
1189 'Lh4N/y0dC/8nGQn/DgUA+g8GAOIOBQDnDgQA1w8FAMsNAwDODQIAuQ8EALMAAAC2AAAAlAAAAIYAAACHAAAAaQAAAGkFAABdCwAA' +
1190 'QwcAAEcAAAA/AAAAKAAAACwAAAAeAAAADgAAABEAAAAAAAAAAAAAAAAAAAAAAAAAAKqqfwYPDw8gEw8DQxMMBGoFAQCPDQQAtw8E' +
1191 'ANESCALwFQsD/CsbC/9FMxv/V0Af/3JZLv6Qc0H+qoVE/r2WTv7Uqlr+37Jd/uS2X/7qvGL+6bti/uG0Xv7arlr+zaVY/riSTP6n' +
1192 'g0P+kHI//mlRKv5LNRn9NSYU/BYKAvwFAAD8CAAA/A4DAPwaDQP8IRQG/h0QBf8qGwz80qhY/uS2X/7gs17+4bRe/uG0Xv7htF7+' +
1193 '4LNe/ua4YP1+YjH9WUQj/dGpXP3ktl7+4LNe/uG0Xv7htF7+4bRe/uG0Xv7eslz+QC4W/RoNA/wjFAf9Gg0D/A4DAPwIAAD8BQAA' +
1194 '/BYKAvw1JhT8SzUZ/WlRKv6Qcj/+p4ND/riSTP7NpVj+2q5a/uG0Xv7pu2L+6rxi/uS2X/7fsl3+1Kpa/r2WTv6qhUT+kHNB/nJZ' +
1195 'Lv5XQB//RTMb/ysbC/8VCwP8EggC8A8EANENBAC3BQEAjxMMBGoTDwNDDw8PIKqqfwYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEA' +
1196 'AAAOAAAAHgAAACwAAAAoAAAAPwcAAEcLAABDBQAAXQAAAGkAAABpAAAAhwAAAIYAAACUAAAAtg8EALMNAgC5DQMAzg8FAMsOBADX' +
1197 'DgUA5w8GAOIOBQD6JxkJ/y0dC/8uHg3/Pi0X/0AuF/9BLxj/Uj8h/1E+If9cRyf/dV0z/3FZL/+BaDz/lHlF/491Q/+afUb/r5FT' +
1198 '/6KBRf+5mlj/w6Rg/8GhXf/Ormb+2Lds/te2a/3pxnT86sZ0/O3Hdfr40Xj1PSkR+gAAAH4AAAAAAAAABQAAAAAAAAAAAAAAAAAA' +
1199 'AAAAAAAFAAAAAAQCAHYlFwj/X0gl/0MvFf88KhP/PSwV/zAgDv8zIxH+JhkL/xQLA/ckFQf2Gg4E+AUAANsHAADTBwAA0QgAALkI' +
1200 'AAC5CQAAuAsAAJ8NAQCbAAAAnAAAAHgAAABvAAAAbQAAAFAAAABTBwAARAsFACwKBQAxAAAGJgAAABQAAAsXABUVDABVVQMAMzMF' +