Files
palladium/studies/202607_CTM_GenesysCX/notebooks/ctm_migration_wfm.ipynb

7579 lines
337 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "a79bef3a",
"metadata": {},
"source": [
"# CTM × Genesys CCaaS — Migration + WFM (No-AI) Case\n",
"\n",
"**Thesis:** strip every AI capability out of the business case and price the platform move\n",
"alone: migrate all four regions to Genesys Cloud CX, move NA off its existing WFM-like tool\n",
"onto Genesys WFM, and implement WFM for APAC (**ANZ + ASIA**). Migration and WFM are\n",
"**included in the base implementation price** — contracted as the signed NTT SOW: **$2.03M\n",
"professional services** billed over four milestones (202627), plus the verbatim $167K\n",
"training and **$411K/yr managed services** from MCX go-live (30 Sep 2026). The only\n",
"benefits kept are the deck's verbatim **WFM** lines for the regions\n",
"in WFM scope; every other benefit (Agent Copilot, Email, STA, Predictive Routing, Supervisor\n",
"Copilot) is AI-driven and out.\n",
"\n",
"| Layer | Source | Treatment |\n",
"|---|---|---|\n",
"| WFM benefits | *Appendix 4 — CCaaS Platform Benefit Calculations (Consolidated).pptx*, slides 1215 | **Verbatim**, scoped to NA/ANZ/ASIA, phased on Genesys's own deployment schedule |\n",
"| CCaaS licences | Deck TCO (slides 56) + signed contract | Deck pitched $4.3M/yr; **contracted $3.2M/yr** drives the case · 6-month ramp credit (🟢 order form) |\n",
"| NTT PS + training | Signed SOW + deck training line | **$2.03M over four billing milestones (202627)** + $167K training — **includes migration + WFM** (deck pitched $2.4M, year 1) |\n",
"| Managed services | Signed SOW | **$411K/yr from MCX go-live (30 Sep 2026)** — ongoing run-rate cost the deck's TCO never carried |\n",
"| Existing platform run-off | Term contracts to 31 Dec 2027, per-region | Double-billing until termination |\n",
"| AI tokens / AI implementation | — | **Removed** — see `ctm_business_case_corrected.ipynb` |\n",
"\n",
"Companion to the Corrected Business Case notebook — same engine (`tokencalc`), same\n",
"baseline-relative frame, same 20262028 window, so the two cases are directly comparable.\n",
"Confidence legend: 🟢 confirmed (published/contractual) · 🟡 estimated (working assumption) · 🔴 unknown.\n",
"\n",
"*This notebook is the deliverable: serve it interactively with `mercury --working-dir notebooks/`, tune the 🟡 inputs live for the client, then export an LLM-readable report source with `python scripts/export_report.py`.*"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "38d0b7d9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:22.503625Z",
"iopub.status.busy": "2026-07-13T19:34:22.503379Z",
"iopub.status.idle": "2026-07-13T19:34:22.942301Z",
"shell.execute_reply": "2026-07-13T19:34:22.941521Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tokencalc loaded — 9 sites, 2,088 named users (contracted: 2,088)\n"
]
}
],
"source": [
"# ── Setup ──────────────────────────────────────────────────────────\n",
"import sys, pathlib\n",
"_ROOT = pathlib.Path.cwd()\n",
"if not (_ROOT / \"tokencalc\").exists(): # notebook lives in notebooks/\n",
" _ROOT = _ROOT.parent\n",
"sys.path.insert(0, str(_ROOT))\n",
"\n",
"import datetime as dt\n",
"\n",
"import pandas as pd\n",
"import plotly.graph_objects as go\n",
"\n",
"import mercury as mr\n",
"\n",
"from tokencalc import *\n",
"# Single source of truth — all math lives in the library; only\n",
"# presentation (and Mercury input widgets) lives here.\n",
"from tokencalc.appendix4 import (\n",
" YEARS, YEAR_INDEX, REGIONS,\n",
" VERBATIM_BENEFITS, TCO_VERBATIM, IMPL_MONTH, REALIZE_MONTH,\n",
" DEFAULT_RAMP_MONTHS,\n",
" build_rollouts, case_flows, case_kpis, crossfoot_tolerance,\n",
" current_costs_by_year, current_state_inputs, licence_costs_by_year,\n",
" MANAGED_SERVICES_ANNUAL, MCX_GO_LIVE, PS_CONTRACTED_TOTAL, PS_MILESTONES,\n",
" managed_services_by_year, money, html_money, month_label, ps_costs_by_year,\n",
" ps_milestones_dataframe, region_agents, region_site_names, tco,\n",
")\n",
"from tokencalc.migration_wfm import (\n",
" DEFAULT_WFM_REGIONS, runrate_breakeven_label, runrate_saving_annual,\n",
" wfm_annual_runrate, wfm_benefits_by_year,\n",
")\n",
"from tokencalc.business_case import npv\n",
"from tokencalc.staging import backstage\n",
"\n",
"pd.options.display.float_format = \"{:,.0f}\".format\n",
"\n",
"# ── Chart chrome (dataviz reference palette, light surface) ─────────\n",
"INK, INK2, MUTED = \"#0b0b0b\", \"#52514e\", \"#898781\"\n",
"SURFACE, GRID, BASELINE = \"#fcfcfb\", \"#e1e0d9\", \"#c3c2b7\"\n",
"CUMULATIVE, CONTEXT = \"#52514e\", \"#c3c2b7\" # neutral line; de-emphasized context series\n",
"FONT_STACK = 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n",
"\n",
"# Fixed region colors — color follows the entity across every figure.\n",
"REGION_COLOR = {\"NA\": \"#2a78d6\", \"ANZ\": \"#1baf7a\", \"EMEA\": \"#4a3aa7\", \"ASIA\": \"#eda100\"}\n",
"COST_COLOR = {\n",
" \"CCaaS platform licences (contracted, ramp-adjusted)\": \"#2a78d6\",\n",
" \"NTT PS + training (SOW billing milestones)\": \"#1baf7a\",\n",
" \"NTT managed services (from MCX go-live)\": \"#0b6b4a\",\n",
" \"Existing platform (term-contract run-off)\": \"#eda100\",\n",
"}\n",
"DIVERGING = [[0.0, \"#e34948\"], [0.5, \"#f0efe9\"], [1.0, \"#2a78d6\"]]\n",
"\n",
"\n",
"def tei_layout(fig, title, subtitle=None, height=460):\n",
" t = f\"<b>{title}</b>\"\n",
" if subtitle:\n",
" t += f\"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>\"\n",
" fig.update_layout(\n",
" title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor=\"left\"),\n",
" paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n",
" font=dict(family=FONT_STACK, size=12, color=INK2),\n",
" legend=dict(orientation=\"h\", yanchor=\"top\", y=-0.10, x=0,\n",
" font=dict(size=11, color=INK2)),\n",
" xaxis=dict(type=\"category\", showgrid=False, linecolor=BASELINE,\n",
" tickfont=dict(color=MUTED)),\n",
" yaxis=dict(gridcolor=GRID, zerolinecolor=BASELINE, zerolinewidth=1.5,\n",
" tickformat=\"$~s\", tickfont=dict(color=MUTED)),\n",
" hovermode=\"x unified\", bargap=0.45, height=height,\n",
" margin=dict(t=70, r=30, b=80, l=70),\n",
" )\n",
" return fig\n",
"\n",
"\n",
"def bar(x, y, name, color):\n",
" return go.Bar(x=x, y=y, name=name,\n",
" marker=dict(color=color, line=dict(width=2, color=SURFACE)),\n",
" hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
"\n",
"\n",
"def cum_line(x, y, name, color=CUMULATIVE, dash=None):\n",
" return go.Scatter(x=x, y=y, name=name, mode=\"lines+markers\",\n",
" line=dict(color=color, width=2, dash=dash),\n",
" marker=dict(size=8, line=dict(width=2, color=SURFACE)),\n",
" hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
"\n",
"\n",
"X = [str(y) for y in YEARS]\n",
"\n",
"backstage(f\"tokencalc loaded — {len(CTM_DEFAULT_SITES)} sites, \"\n",
" f\"{sum(s.named_users for s in CTM_DEFAULT_SITES):,} named users \"\n",
" f\"(contracted: {CONTRACTED_NAMED_USERS:,})\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "104afa86",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:22.944759Z",
"iopub.status.busy": "2026-07-13T19:34:22.944509Z",
"iopub.status.idle": "2026-07-13T19:34:22.956133Z",
"shell.execute_reply": "2026-07-13T19:34:22.955587Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "6b6471d39e2544f58844cffb1d57afe9",
"position": "sidebar",
"widget": "MarkdownWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "6b6471d39e2544f58844cffb1d57afe9",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"MarkdownWidget(value='<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, …"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Jump-to-section ToC (Mercury sidebar — widgets only, no output) ──\n",
"# Fragment links don't scroll inside Mercury's app shell (SPA base URL),\n",
"# so each entry scrolls via JS: anchor id first, heading-text fallback.\n",
"_TOC = [\n",
" (1, \"Current state & contracts\"),\n",
" (2, \"Scenario scope & licences\"),\n",
" (3, \"WFM benefits\"),\n",
" (4, \"Cost stack\"),\n",
" (5, \"Business case\"),\n",
" (6, \"Sensitivity\"),\n",
" (7, \"Verification & assertions\"),\n",
" (8, \"Risks & notes\"),\n",
" (9, \"Data appendix\"),\n",
"]\n",
"# NB: no raw \"<\" allowed inside the handler — python-markdown escapes the\n",
"# whole tag if the attribute text looks like malformed HTML.\n",
"_JS = (\"var el=document.getElementById('section-{n}');\"\n",
" \"if(!el){{document.querySelectorAll('h1,h2').forEach(function(h){{\"\n",
" \"if(!el&&h.textContent.trim().indexOf('{n} ')===0){{el=h;}}}});}}\"\n",
" \"if(el)el.scrollIntoView({{behavior:'smooth',block:'start'}});\")\n",
"_items = \"\".join(\n",
" f'<li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline\"'\n",
" f' onclick=\"{_JS.format(n=n)}\">{label}</a></li>'\n",
" for n, label in _TOC)\n",
"_toc = mr.Markdown(\n",
" text=(f'<b>Jump to section</b>'\n",
" f'<ol style=\"padding-left:1.2em;margin:6px 0\">{_items}</ol>'),\n",
" position=\"sidebar\")"
]
},
{
"cell_type": "markdown",
"id": "cecdb4b9",
"metadata": {},
"source": [
"<a id=\"section-1\"></a>\n",
"\n",
"## 1 · Current state & contract inputs — data collection\n",
"\n",
"Verbatim anchors (Appendix 4, slides 56): current solution costs **$7.3M/yr globally**\n",
"(ANZ, APAC, EMEA, NA — $22M over 3 years); CCaaS is **$4.3M/yr** with **$2.4M professional\n",
"services + $167K training in year 1**.\n",
"\n",
"The same three contract facts as the corrected notebook, collected as editable inputs:\n",
"\n",
"1. **Per-region current cost** — seeded as $7.3M × (region agents ÷ total agents). 🟡 Allocation\n",
" estimate; replace with real regional contract values as they arrive. If NA's existing\n",
" WFM-like tool bills separately from the platform, fold it into NA's number here — migrating\n",
" off it is part of this scenario.\n",
"2. **Contract termination dates** — the current platforms are **term contracts**; CTM pays until\n",
" termination (default **31 Dec 2027**) regardless of when Genesys goes live → double-billing.\n",
"3. **Genesys ramp programme** — `RAMP_MONTHS = 6` (🟢 order form): licences are free for\n",
" the first 6 months, so the licence run-rate bills from July 2026."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "f8863db7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:22.958354Z",
"iopub.status.busy": "2026-07-13T19:34:22.958178Z",
"iopub.status.idle": "2026-07-13T19:34:22.994994Z",
"shell.execute_reply": "2026-07-13T19:34:22.994203Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "f755a69ae50540b3aac94ceb249b1cc2",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "f755a69ae50540b3aac94ceb249b1cc2",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd639503e00>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "a4de65fac18f44e086d105b7f75693fe",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "a4de65fac18f44e086d105b7f75693fe",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd5e4f13b10>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "ff3441542e0b43db946e184e4b461b1e",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "ff3441542e0b43db946e184e4b461b1e",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x7bd639503cb0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "401b35065476433d96aeed7fcad746e8",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "401b35065476433d96aeed7fcad746e8",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd638ac9bd0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "0fff3f6c07fe4316a91344082245882c",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "0fff3f6c07fe4316a91344082245882c",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x7bd638ac9d10>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "bab7c81ec7ae47deabd7fc74f205c38e",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "bab7c81ec7ae47deabd7fc74f205c38e",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd5e4f628b0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "7e22e4c6e3dd44fc8eeb96c4f501a342",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "7e22e4c6e3dd44fc8eeb96c4f501a342",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x7bd638aca0d0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "c2a141345720426b933108c66be94b8b",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "c2a141345720426b933108c66be94b8b",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd5e4f63820>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "ec33300bf1e14da98f0dd7bfc01a2f44",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "ec33300bf1e14da98f0dd7bfc01a2f44",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x7bd5e4f63950>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Contract inputs (model: tokencalc.appendix4) ─────────────────────\n",
"# ⚙ Interactive when served with Mercury; headless runs (nbconvert /\n",
"# regression gate) use the widget defaults below.\n",
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
"_ramp_w = mr.NumberInput(\n",
" label=\"Genesys ramp — licence-free months\", value=DEFAULT_RAMP_MONTHS,\n",
" min=0, max=24, step=1, position=\"inline\")\n",
"\n",
"sites = list(CTM_DEFAULT_SITES)\n",
"REGION_SITES = region_site_names(sites)\n",
"agents_by_region = region_agents(sites)\n",
"\n",
"# ── Per-region current-state inputs (EDIT as real data arrives) ──────\n",
"# Seeded as $7.3M × agent share; annual_cost and contract_termination\n",
"# are the editable inputs.\n",
"_seed = current_state_inputs(sites)\n",
"assert abs(_seed[\"annual_cost\"].sum() - TCO_VERBATIM[\"current_annual\"]) < 1\n",
"\n",
"_cost_w, _term_w = {}, {}\n",
"for _r in REGIONS:\n",
" _cost_w[_r] = mr.NumberInput(\n",
" label=f\"{_r} — current platform cost ($/yr)\",\n",
" value=round(float(_seed.loc[_r, \"annual_cost\"])),\n",
" min=0, max=8_000_000, step=10_000, position=\"inline\")\n",
" _term_w[_r] = mr.DateInput(\n",
" label=f\"{_r} — contract termination\",\n",
" value=_seed.loc[_r, \"contract_termination\"].isoformat(),\n",
" position=\"inline\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "0bf61187",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:22.996863Z",
"iopub.status.busy": "2026-07-13T19:34:22.996642Z",
"iopub.status.idle": "2026-07-13T19:34:23.012452Z",
"shell.execute_reply": "2026-07-13T19:34:23.011782Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"current-state total $7.3M/yr (seed: $7.3M; deck 3-yr $22.0M — deck rounding)\n",
"existing-platform run-off by year: {2026: '$7.3M', 2027: '$7.3M', 2028: '$0K'}\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>agents</th>\n",
" <th>share</th>\n",
" <th>annual_cost</th>\n",
" <th>contract_termination</th>\n",
" <th>confidence</th>\n",
" </tr>\n",
" <tr>\n",
" <th>region</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>NA</th>\n",
" <td>890</td>\n",
" <td>0</td>\n",
" <td>3,348,969</td>\n",
" <td>2027-12-31</td>\n",
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ANZ</th>\n",
" <td>180</td>\n",
" <td>0</td>\n",
" <td>677,320</td>\n",
" <td>2027-12-31</td>\n",
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
" </tr>\n",
" <tr>\n",
" <th>EMEA</th>\n",
" <td>320</td>\n",
" <td>0</td>\n",
" <td>1,204,124</td>\n",
" <td>2027-12-31</td>\n",
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ASIA</th>\n",
" <td>550</td>\n",
" <td>0</td>\n",
" <td>2,069,588</td>\n",
" <td>2027-12-31</td>\n",
" <td>🟡 agent-share allocation of the verbatim total</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" agents share annual_cost contract_termination \\\n",
"region \n",
"NA 890 0 3,348,969 2027-12-31 \n",
"ANZ 180 0 677,320 2027-12-31 \n",
"EMEA 320 0 1,204,124 2027-12-31 \n",
"ASIA 550 0 2,069,588 2027-12-31 \n",
"\n",
" confidence \n",
"region \n",
"NA 🟡 agent-share allocation of the verbatim total \n",
"ANZ 🟡 agent-share allocation of the verbatim total \n",
"EMEA 🟡 agent-share allocation of the verbatim total \n",
"ASIA 🟡 agent-share allocation of the verbatim total "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Contract values → model (re-runs on any change to the widgets above) ─\n",
"RAMP_MONTHS = int(_ramp_w.value)\n",
"\n",
"current_state = _seed.copy()\n",
"for _r in REGIONS:\n",
" current_state.loc[_r, \"annual_cost\"] = float(_cost_w[_r].value)\n",
" current_state.loc[_r, \"contract_termination\"] = dt.date.fromisoformat(\n",
" _term_w[_r].value)\n",
"\n",
"current_by_year = current_costs_by_year(current_state)\n",
"\n",
"backstage(f\"current-state total {money(current_state['annual_cost'].sum())}/yr \"\n",
" f\"(seed: {money(TCO_VERBATIM['current_annual'])}; deck 3-yr \"\n",
" f\"{money(TCO_VERBATIM['current_3yr'])} — deck rounding)\")\n",
"backstage(f\"existing-platform run-off by year: \"\n",
" f\"{ {y: money(v) for y, v in current_by_year.items()} }\")\n",
"display(current_state)"
]
},
{
"cell_type": "markdown",
"id": "2caa14c2",
"metadata": {},
"source": [
"<a id=\"section-2\"></a>\n",
"\n",
"## 2 · Scenario scope — what the base price includes\n",
"\n",
"- **Genesys Cloud CX migration** for all four regions.\n",
"- **NA — WFM migration**: users move off the existing WFM-like tool onto Genesys WFM. The deck\n",
" claims **$0** WFM benefit for NA (\"has similar feature\") — kept verbatim.\n",
"- **APAC (ANZ + ASIA) — WFM implementation**: net-new WFM capability.\n",
"- **EMEA — no WFM** in this scenario; the deck's $687K 3-yr EMEA WFM line is dropped\n",
" (upside option — toggle it in the sidebar to see the effect).\n",
"- **Base implementation price covers all of the above** — contracted as the signed NTT SOW:\n",
" **$2.03M professional services** billed over four milestones (202627; section 4 shows the\n",
" schedule) plus the verbatim $167K training. No extra cost lines for migration or WFM.\n",
"- 🟢 **Managed services — contracted**: **$410,918/yr** commences billing at MCX go-live\n",
" (30 Sep 2026) — an ongoing run-rate cost the deck's TCO never carried; it stays in the\n",
" post-window steady-state run-rate.\n",
"- 🟢 **Licence run-rate — contracted**: the signed rate is **$3.2M/yr**, below the deck's\n",
" pitched $4.3M/yr. The case runs on the contracted rate; the KPI table in section 5 keeps\n",
" a deck-rate column beside it so the walk from the Genesys report stays visible.\n",
" 🟡 Still assumed to include the WFM/WEM SKUs (incl. new APAC seats) — if WEM is an\n",
" add-on, raise the sidebar rate."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "8bafeb91",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:23.014180Z",
"iopub.status.busy": "2026-07-13T19:34:23.014015Z",
"iopub.status.idle": "2026-07-13T19:34:23.029326Z",
"shell.execute_reply": "2026-07-13T19:34:23.028514Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "655850770ff34fb8bf1076de30bf2ddc",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "655850770ff34fb8bf1076de30bf2ddc",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7bd5e4faa9f0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "8ffdd60ef54a4c829429b46b438c36db",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "8ffdd60ef54a4c829429b46b438c36db",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7bd5e4f8aa50>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "d4d0cebd7a9345e2a88c5fa842d24337",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "d4d0cebd7a9345e2a88c5fa842d24337",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7bd638aca710>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "abd40a60e1974670886b2bd43aab35c2",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "abd40a60e1974670886b2bd43aab35c2",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7bd638aca5d0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "1138fba4608d4de484f84b11e1ae2862",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "1138fba4608d4de484f84b11e1ae2862",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7bd5e4dec2b0>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── CONFIG — scenario scope (Mercury sidebar — widgets only, no output) ─\n",
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
"_licence_w = mr.NumberInput(\n",
" label=\"Genesys licence run-rate ($/yr)\",\n",
" value=round(tco(\"ccaas_annual\")), # contracted; deck pitched $4.3M\n",
" min=0, max=10_000_000, step=100_000)\n",
"_wfm_w = {\n",
" \"NA\": mr.CheckBox(label=\"WFM — NA (migrate existing users)\", value=True),\n",
" \"ANZ\": mr.CheckBox(label=\"WFM — ANZ (implement)\", value=True),\n",
" \"ASIA\": mr.CheckBox(label=\"WFM — ASIA (implement)\", value=True),\n",
" \"EMEA\": mr.CheckBox(label=\"WFM — EMEA (upside option)\", value=False),\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "6d971716",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:23.031153Z",
"iopub.status.busy": "2026-07-13T19:34:23.030906Z",
"iopub.status.idle": "2026-07-13T19:34:23.045403Z",
"shell.execute_reply": "2026-07-13T19:34:23.044707Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"WFM scope: NA, ANZ, ASIA · licence run-rate $3.2M/yr contracted (deck pitched $4.3M/yr) · ramp 6 months\n",
"ramp-adjusted licences by year: {2026: '$1.6M', 2027: '$3.2M', 2028: '$3.2M'}\n",
"NTT PS + training (SOW milestones, incl. migration + WFM): $2.2M across 2026-27 · managed services $411K/yr from MCX go-live\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>WFM treatment</th>\n",
" <th>in scope</th>\n",
" <th>verbatim annual</th>\n",
" <th>verbatim 3-yr</th>\n",
" </tr>\n",
" <tr>\n",
" <th>region</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>NA</th>\n",
" <td>migrate existing users</td>\n",
" <td>✓</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ANZ</th>\n",
" <td>implement (APAC)</td>\n",
" <td>✓</td>\n",
" <td>1300000</td>\n",
" <td>1400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>EMEA</th>\n",
" <td>implement (upside option)</td>\n",
" <td>—</td>\n",
" <td>824000</td>\n",
" <td>687000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ASIA</th>\n",
" <td>implement (APAC)</td>\n",
" <td>✓</td>\n",
" <td>1600000</td>\n",
" <td>914000</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" WFM treatment in scope verbatim annual verbatim 3-yr\n",
"region \n",
"NA migrate existing users ✓ 0 0\n",
"ANZ implement (APAC) ✓ 1300000 1400000\n",
"EMEA implement (upside option) — 824000 687000\n",
"ASIA implement (APAC) ✓ 1600000 914000"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Scope derivation + licence schedule ──────────────────────────────\n",
"LICENCE_ANNUAL = float(_licence_w.value)\n",
"WFM_REGIONS = [r for r in REGIONS if bool(_wfm_w[r].value)]\n",
"\n",
"licence_by_year = licence_costs_by_year(RAMP_MONTHS, LICENCE_ANNUAL)\n",
"ps_by_year = ps_costs_by_year(contracted=True) # SOW milestones + verbatim training\n",
"managed_by_year = managed_services_by_year() # 🟢 bills from MCX go-live (30 Sep 2026)\n",
"\n",
"_treat = {\"NA\": \"migrate existing users\", \"ANZ\": \"implement (APAC)\",\n",
" \"ASIA\": \"implement (APAC)\", \"EMEA\": \"implement (upside option)\"}\n",
"wfm_scope_table = pd.DataFrame([\n",
" {\"region\": r, \"WFM treatment\": _treat[r],\n",
" \"in scope\": \"✓\" if r in WFM_REGIONS else \"—\",\n",
" \"verbatim annual\": VERBATIM_BENEFITS[(r, \"WFM\")][0],\n",
" \"verbatim 3-yr\": VERBATIM_BENEFITS[(r, \"WFM\")][1]}\n",
" for r in REGIONS]).set_index(\"region\")\n",
"\n",
"print(f\"WFM scope: {', '.join(WFM_REGIONS) or '(none)'} · \"\n",
" f\"licence run-rate {money(LICENCE_ANNUAL)}/yr contracted \"\n",
" f\"(deck pitched {money(TCO_VERBATIM['ccaas_annual'])}/yr) · ramp {RAMP_MONTHS} months\")\n",
"backstage(f\"ramp-adjusted licences by year: \"\n",
" f\"{ {y: money(v) for y, v in licence_by_year.items()} }\")\n",
"print(f\"NTT PS + training (SOW milestones, incl. migration + WFM): \"\n",
" f\"{money(sum(ps_by_year.values()))} across 2026-27 · managed services \"\n",
" f\"{money(MANAGED_SERVICES_ANNUAL)}/yr from MCX go-live\")\n",
"display(wfm_scope_table)"
]
},
{
"cell_type": "markdown",
"id": "95b71218",
"metadata": {},
"source": [
"<a id=\"section-3\"></a>\n",
"\n",
"## 3 · WFM benefits — verbatim, scoped, schedule-phased\n",
"\n",
"Only the deck's WFM lines survive the no-AI cut, and only for the regions in WFM scope.\n",
"They phase on the same Genesys deployment schedule as the corrected notebook (benefits\n",
"realize **implementation + 3 months**, inclusive): ANZ realizes Dec 2027, ASIA Jun 2028 —\n",
"so most of the WFM value lands at the very edge of the 3-year window.\n",
"\n",
"*Deck quirk, kept verbatim: ASIA's WFM **annual** value ($1.6M) exceeds its **3-yr** value\n",
"($914K) — the deck's own late-realization arithmetic. The 3-yr figure is what phases here;\n",
"the annual figure feeds the post-window run-rate in section 5.*"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "12673034",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:23.047481Z",
"iopub.status.busy": "2026-07-13T19:34:23.047226Z",
"iopub.status.idle": "2026-07-13T19:34:23.082155Z",
"shell.execute_reply": "2026-07-13T19:34:23.081476Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"WFM benefits by year: {2026: '$0K', 2027: '$350K', 2028: '$2.0M'} → 3-yr $2.3M\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th>year</th>\n",
" <th>2026</th>\n",
" <th>2027</th>\n",
" <th>2028</th>\n",
" <th>TOTAL</th>\n",
" </tr>\n",
" <tr>\n",
" <th>region</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>NA</th>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ANZ</th>\n",
" <td>0</td>\n",
" <td>350,000</td>\n",
" <td>1,050,000</td>\n",
" <td>1,400,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ASIA</th>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>914,000</td>\n",
" <td>914,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>TOTAL</th>\n",
" <td>0</td>\n",
" <td>350,000</td>\n",
" <td>1,964,000</td>\n",
" <td>2,314,000</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
"year 2026 2027 2028 TOTAL\n",
"region \n",
"NA 0 0 0 0\n",
"ANZ 0 350,000 1,050,000 1,400,000\n",
"ASIA 0 0 914,000 914,000\n",
"TOTAL 0 350,000 1,964,000 2,314,000"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── WFM slice of the verbatim benefits (tokencalc.migration_wfm) ─────\n",
"_, _, BENEFIT_ROLLOUT = build_rollouts(sites, ramp_months=RAMP_MONTHS)\n",
"wfm_long = wfm_benefits_by_year(BENEFIT_ROLLOUT, WFM_REGIONS)\n",
"ben_by_year = {y: float(wfm_long.loc[wfm_long.year == y, \"benefit\"].sum())\n",
" for y in YEARS}\n",
"\n",
"backstage(f\"WFM benefits by year: { {y: money(v) for y, v in ben_by_year.items()} } \"\n",
" f\"→ 3-yr {money(sum(ben_by_year.values()))}\")\n",
"if wfm_long.empty:\n",
" print(\"No WFM regions in scope — pure licence-swap case.\")\n",
"else:\n",
" display(wfm_long.pivot_table(index=\"region\", columns=\"year\", values=\"benefit\",\n",
" aggfunc=\"sum\", margins=True, margins_name=\"TOTAL\")\n",
" .reindex([*[r for r in REGIONS if r in WFM_REGIONS], \"TOTAL\"]))"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "3cbefa13",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:23.084146Z",
"iopub.status.busy": "2026-07-13T19:34:23.083884Z",
"iopub.status.idle": "2026-07-13T19:34:24.380344Z",
"shell.execute_reply": "2026-07-13T19:34:24.379394Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#2a78d6",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "NA WFM",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
0.0,
0.0,
0.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "ANZ WFM",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
0.0,
350000.0,
1050000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#eda100",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "ASIA WFM",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
0.0,
0.0,
914000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"line": {
"color": "#52514e",
"width": 2
},
"marker": {
"line": {
"color": "#fcfcfb",
"width": 2
},
"size": 8
},
"mode": "lines+markers",
"name": "Cumulative WFM benefits",
"type": "scatter",
"x": [
"2026",
"2027",
"2028"
],
"y": {
"bdata": "AAAAAAAAAAAAAAAAwFwVQQAAAACIp0FB",
"dtype": "f8"
}
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$0K</b>",
"x": 0,
"y": 0.0,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$350K</b>",
"x": 1,
"y": 350000.0,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.0M</b>",
"x": 2,
"y": 1964000.0,
"yshift": 12
}
],
"bargap": 0.45,
"barmode": "stack",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 420,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>WFM benefits — verbatim, scoped, schedule-phased</b><br><span style='font-size:12px;color:#898781'>NA claims $0 (migration — deck: 'has similar feature') · ANZ realizes Dec 2027, ASIA Jun 2028 — value lands at the edge of the window</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"for _r in [r for r in REGIONS if r in WFM_REGIONS]:\n",
" _v = [float(wfm_long.query(\"region == @_r and year == @y\")[\"benefit\"].sum())\n",
" for y in YEARS]\n",
" fig.add_trace(bar(X, _v, f\"{_r} WFM\", REGION_COLOR[_r]))\n",
"cum_ben = pd.Series([ben_by_year[y] for y in YEARS]).cumsum()\n",
"fig.add_trace(cum_line(X, cum_ben, \"Cumulative WFM benefits\"))\n",
"for i, y in enumerate(YEARS):\n",
" fig.add_annotation(x=i, y=ben_by_year[y], text=f\"<b>{money(ben_by_year[y])}</b>\",\n",
" showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
"fig.update_layout(barmode=\"stack\")\n",
"tei_layout(fig, \"WFM benefits — verbatim, scoped, schedule-phased\",\n",
" \"NA claims $0 (migration — deck: 'has similar feature') · ANZ realizes \"\n",
" \"Dec 2027, ASIA Jun 2028 — value lands at the edge of the window\", height=420)\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"id": "059bd0f9",
"metadata": {},
"source": [
"<a id=\"section-4\"></a>\n",
"\n",
"## 4 · Cost stack — licences, NTT SOW, double-billing\n",
"\n",
"Four lines. The AI token-consumption and AI implementation lines are gone —\n",
"that is the point of this scenario. What remains is the deck's own cost case plus the\n",
"contract facts it ignored: the **ramp credit** (licences bill from month 7 — 🟢 6-month\n",
"ramp per the order form), the **term-contract run-off** (double-billing until\n",
"31 Dec 2027), and the signed NTT SOW — **PS billed over four milestones** (50/50 across\n",
"202627) and **managed services** from MCX go-live.\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "ccde902f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.387579Z",
"iopub.status.busy": "2026-07-13T19:34:24.387133Z",
"iopub.status.idle": "2026-07-13T19:34:24.416055Z",
"shell.execute_reply": "2026-07-13T19:34:24.415071Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Migration programme cost: {2026: '$10.1M', 2027: '$11.9M', 2028: '$3.6M'} → 3-yr $25.7M\n",
"Do nothing: $7.3M/yr → 3-yr $21.9M\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>2026</th>\n",
" <th>2027</th>\n",
" <th>2028</th>\n",
" <th>3-yr</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>CCaaS platform licences (contracted, ramp-adjusted)</th>\n",
" <td>1,600,000</td>\n",
" <td>3,200,000</td>\n",
" <td>3,200,000</td>\n",
" <td>8,000,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NTT PS + training (SOW billing milestones)</th>\n",
" <td>1,179,723</td>\n",
" <td>1,012,723</td>\n",
" <td>0</td>\n",
" <td>2,192,446</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NTT managed services (from MCX go-live)</th>\n",
" <td>68,486</td>\n",
" <td>410,918</td>\n",
" <td>410,918</td>\n",
" <td>890,323</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Existing platform (term-contract run-off)</th>\n",
" <td>7,300,001</td>\n",
" <td>7,300,001</td>\n",
" <td>0</td>\n",
" <td>14,600,002</td>\n",
" </tr>\n",
" <tr>\n",
" <th>TOTAL</th>\n",
" <td>10,148,211</td>\n",
" <td>11,923,643</td>\n",
" <td>3,610,918</td>\n",
" <td>25,682,772</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" 2026 2027 \\\n",
"CCaaS platform licences (contracted, ramp-adjus... 1,600,000 3,200,000 \n",
"NTT PS + training (SOW billing milestones) 1,179,723 1,012,723 \n",
"NTT managed services (from MCX go-live) 68,486 410,918 \n",
"Existing platform (term-contract run-off) 7,300,001 7,300,001 \n",
"TOTAL 10,148,211 11,923,643 \n",
"\n",
" 2028 3-yr \n",
"CCaaS platform licences (contracted, ramp-adjus... 3,200,000 8,000,000 \n",
"NTT PS + training (SOW billing milestones) 0 2,192,446 \n",
"NTT managed services (from MCX go-live) 410,918 890,323 \n",
"Existing platform (term-contract run-off) 0 14,600,002 \n",
"TOTAL 3,610,918 25,682,772 "
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>date</th>\n",
" <th>share</th>\n",
" <th>amount</th>\n",
" </tr>\n",
" <tr>\n",
" <th>SOW billing milestone</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>SOW Effective Date</th>\n",
" <td>2026-03-15</td>\n",
" <td>30%</td>\n",
" <td>$607,633.94</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Start of client UAT (first region, NA)</th>\n",
" <td>2026-10-16</td>\n",
" <td>20%</td>\n",
" <td>$405,089.30</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Start of client UAT (last region, APAC)</th>\n",
" <td>2027-10-01</td>\n",
" <td>30%</td>\n",
" <td>$607,633.94</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Completion of last go-live migration</th>\n",
" <td>2027-11-01</td>\n",
" <td>20%</td>\n",
" <td>$405,089.30</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" date share amount\n",
"SOW billing milestone \n",
"SOW Effective Date 2026-03-15 30% $607,633.94\n",
"Start of client UAT (first region, NA) 2026-10-16 20% $405,089.30\n",
"Start of client UAT (last region, APAC) 2027-10-01 30% $607,633.94\n",
"Completion of last go-live migration 2027-11-01 20% $405,089.30"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"migration_costs = pd.DataFrame({\n",
" \"CCaaS platform licences (contracted, ramp-adjusted)\": licence_by_year,\n",
" \"NTT PS + training (SOW billing milestones)\": ps_by_year,\n",
" \"NTT managed services (from MCX go-live)\": managed_by_year,\n",
" \"Existing platform (term-contract run-off)\": current_by_year,\n",
"}).T[YEARS]\n",
"migration_costs[\"3-yr\"] = migration_costs.sum(axis=1)\n",
"total_by_year = {y: float(migration_costs[y].sum()) for y in YEARS}\n",
"\n",
"BASELINE_ANNUAL = TCO_VERBATIM[\"current_annual\"]\n",
"donothing_by_year = {y: BASELINE_ANNUAL for y in YEARS}\n",
"\n",
"backstage(f\"Migration programme cost: { {y: money(v) for y, v in total_by_year.items()} } \"\n",
" f\"→ 3-yr {money(sum(total_by_year.values()))}\")\n",
"backstage(f\"Do nothing: {money(BASELINE_ANNUAL)}/yr → 3-yr {money(3 * BASELINE_ANNUAL)}\")\n",
"display(pd.concat([migration_costs,\n",
" pd.DataFrame(migration_costs.sum()).T.set_axis([\"TOTAL\"])]))\n",
"\n",
"_ms = ps_milestones_dataframe()\n",
"_ms[\"amount\"] = _ms[\"amount\"].map(\"${:,.2f}\".format)\n",
"_ms[\"share\"] = _ms[\"share\"].map(\"{:.0%}\".format)\n",
"display(_ms.set_index(\"name\").rename_axis(\"SOW billing milestone\"))\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c058e795",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.418363Z",
"iopub.status.busy": "2026-07-13T19:34:24.418119Z",
"iopub.status.idle": "2026-07-13T19:34:24.457096Z",
"shell.execute_reply": "2026-07-13T19:34:24.456122Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#2a78d6",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "CCaaS platform licences (contracted, ramp-adjusted)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
1600000.0,
3200000.0,
3200000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "NTT PS + training (SOW billing milestones)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
1179723.24,
1012723.24,
0.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#0b6b4a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "NTT managed services (from MCX go-live)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
68486.40000000001,
410918.4000000001,
410918.4000000001
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#eda100",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Existing platform (term-contract run-off)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
7300001.0,
7300001.0,
0.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"line": {
"color": "#52514e",
"width": 2
},
"marker": {
"line": {
"color": "#fcfcfb",
"width": 2
},
"size": 8
},
"mode": "lines+markers",
"name": "Cumulative — migration programme",
"type": "scatter",
"x": [
"2026",
"2027",
"2028"
],
"y": {
"bdata": "SOF6VC5bY0FI4XrUogx1Qa5H4To1fnhB",
"dtype": "f8"
}
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"line": {
"color": "#c3c2b7",
"dash": "dash",
"width": 2
},
"marker": {
"line": {
"color": "#fcfcfb",
"width": 2
},
"size": 8
},
"mode": "lines+markers",
"name": "Cumulative — do nothing",
"type": "scatter",
"x": [
"2026",
"2027",
"2028"
],
"y": {
"bdata": "oGNvAEDH3gDgKk4B",
"dtype": "i4"
}
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$10.1M</b>",
"x": 0,
"y": 10148210.64,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$11.9M</b>",
"x": 1,
"y": 11923642.64,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$3.6M</b>",
"x": 2,
"y": 3610918.4,
"yshift": 12
},
{
"font": {
"color": "#52514e",
"size": 12
},
"showarrow": false,
"text": "3-yr <b>&#36;25.7M</b> — &#36;3.8M above doing nothing",
"x": 2,
"xshift": -70,
"y": 25682771.68,
"yshift": 18
}
],
"bargap": 0.45,
"barmode": "stack",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 500,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Migration programme cost vs doing nothing</b><br><span style='font-size:12px;color:#898781'>Double-billing hits 2026-27 while existing term contracts run off · licences ramp-free for 6 months (order form), then bill at the run-rate · SOW-billed PS + managed services</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"for line in migration_costs.index:\n",
" fig.add_trace(bar(X, [migration_costs.loc[line, y] for y in YEARS],\n",
" line, COST_COLOR[line]))\n",
"cum_mig = pd.Series([total_by_year[y] for y in YEARS]).cumsum()\n",
"cum_nothing = pd.Series([donothing_by_year[y] for y in YEARS]).cumsum()\n",
"fig.add_trace(cum_line(X, cum_mig, \"Cumulative — migration programme\"))\n",
"fig.add_trace(cum_line(X, cum_nothing, \"Cumulative — do nothing\", color=CONTEXT, dash=\"dash\"))\n",
"for i, y in enumerate(YEARS):\n",
" fig.add_annotation(x=i, y=total_by_year[y], text=f\"<b>{money(total_by_year[y])}</b>\",\n",
" showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
"_delta = float(cum_mig.iloc[-1] - cum_nothing.iloc[-1])\n",
"fig.add_annotation(x=len(YEARS) - 1, y=float(cum_mig.iloc[-1]),\n",
" text=f\"3-yr <b>{html_money(float(cum_mig.iloc[-1]))}</b> — \"\n",
" f\"{html_money(_delta)} above doing nothing\",\n",
" showarrow=False, yshift=18, xshift=-70, font=dict(size=12, color=INK2))\n",
"fig.update_layout(barmode=\"stack\")\n",
"tei_layout(fig, \"Migration programme cost vs doing nothing\",\n",
" \"Double-billing hits 2026-27 while existing term contracts run off · \"\n",
" \"licences ramp-free for 6 months (order form), then bill at the run-rate · \"\n",
" \"SOW-billed PS + managed services\", height=500)\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"id": "f24b1255",
"metadata": {},
"source": [
"<a id=\"section-5\"></a>\n",
"\n",
"## 5 · Business case vs do nothing\n",
"\n",
"**Frame:** identical to the corrected notebook — baseline-relative, against *do nothing*\n",
"(keep paying $7.3M/yr). Incremental cost = programme cost baseline; net = scoped WFM\n",
"benefits incremental cost. The 202627 double-billing penalty and the 2028 cost-avoidance\n",
"credit both fall out of this one frame.\n",
"\n",
"With the signed contracts in — **$3.2M/yr licences, 6-month ramp, SOW-billed PS, managed\n",
"services** — the platform move alone still does not pay back inside the deck's own\n",
"20262028 window (net ≈ $1.5M). The **run-rate breakeven** KPI extrapolates past the\n",
"window: steady state = (baseline licence run-rate managed services) + scoped WFM\n",
"annual values, which carries the case to breakeven in early 2029. The deck frame (pitched\n",
"$4.3M/yr, deck's year-1 PS lump, no managed services) is worse on both counts — the KPI\n",
"table shows the columns side by side, so the walk from the Genesys report to the\n",
"contracted reality stays explicit.\n",
"\n",
"*Not modelled: early-termination fees, and migration costs beyond the SOW.*\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "d8293152",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.460722Z",
"iopub.status.busy": "2026-07-13T19:34:24.460346Z",
"iopub.status.idle": "2026-07-13T19:34:24.469686Z",
"shell.execute_reply": "2026-07-13T19:34:24.468764Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "fa2b443aef4f4d2ab8e12371c8da9a52",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "fa2b443aef4f4d2ab8e12371c8da9a52",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.select.SelectWidget object at 0x7bd59f9d41a0>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── NPV rate (Mercury sidebar — widgets only, no output) ────────────\n",
"# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
"# so this cell constructs widgets ONLY — .value is read downstream.\n",
"_npv_rate_w = mr.Select(label=\"NPV discount rate\", value=\"13.5% (deck)\",\n",
" choices=[\"13.5% (deck)\", \"8.0% (CTM treasury)\"])"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "c89973af",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.472312Z",
"iopub.status.busy": "2026-07-13T19:34:24.471959Z",
"iopub.status.idle": "2026-07-13T19:34:24.480220Z",
"shell.execute_reply": "2026-07-13T19:34:24.479435Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"net by year: {2026: '-$2.8M', 2027: '-$4.3M', 2028: '$5.7M'} → 3-yr -$1.5M\n",
"steady-state run-rate saving $6.6M/yr ($4.1M licences $411K managed services + $2.9M WFM annuals) → breakeven 39 months (~Mar 2029, extrapolated)\n"
]
}
],
"source": [
"# Baseline-relative frame (tokencalc.appendix4.case_flows / case_kpis):\n",
"# baseline = do nothing = keep paying $7.3M/yr.\n",
"DISCOUNT_RATE = (TCO_VERBATIM[\"npv_discount_rate\"]\n",
" if _npv_rate_w.value.startswith(\"13.5\") else 0.08)\n",
"inc, net_by = case_flows(total_by_year, ben_by_year)\n",
"kpi = case_kpis(inc, net_by, DISCOUNT_RATE)\n",
"\n",
"# Steady-state run-rate once contracts end and the ramp is over\n",
"# (tokencalc.migration_wfm) — fills the end-2028 deficit.\n",
"RUNRATE_SAVING = runrate_saving_annual(LICENCE_ANNUAL, WFM_REGIONS,\n",
" managed_annual=MANAGED_SERVICES_ANNUAL)\n",
"BREAKEVEN = runrate_breakeven_label(net_by, RUNRATE_SAVING)\n",
"\n",
"\n",
"def fmt_roi(k):\n",
" return f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net cost saving\"\n",
"\n",
"\n",
"backstage(f\"net by year: { {y: money(v) for y, v in net_by.items()} } \"\n",
" f\"→ 3-yr {money(kpi['net_3yr'])}\")\n",
"backstage(f\"steady-state run-rate saving {money(RUNRATE_SAVING)}/yr \"\n",
" f\"({money(BASELINE_ANNUAL - LICENCE_ANNUAL)} licences \"\n",
" f\"{money(MANAGED_SERVICES_ANNUAL)} managed services + \"\n",
" f\"{money(wfm_annual_runrate(WFM_REGIONS))} WFM annuals) → breakeven {BREAKEVEN}\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "9d135a19",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.482509Z",
"iopub.status.busy": "2026-07-13T19:34:24.482260Z",
"iopub.status.idle": "2026-07-13T19:34:24.515278Z",
"shell.execute_reply": "2026-07-13T19:34:24.514283Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "WFM benefits (verbatim, scoped)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
0.0,
350000.0,
1964000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#e34948",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Incremental cost vs $7.3M/yr baseline",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
-2848210.6400000006,
-4623642.640000001,
3689081.6
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"line": {
"color": "#52514e",
"width": 2
},
"marker": {
"line": {
"color": "#fcfcfb",
"width": 2
},
"size": 8
},
"mode": "lines+markers",
"name": "Cumulative net",
"type": "scatter",
"x": [
"2026",
"2027",
"2028"
],
"y": {
"bdata": "IIXrUem6RcEghetR7ypbweh6FK5jaTbB",
"dtype": "f8"
}
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$2.8M</b>",
"x": 0,
"y": -2848210.6400000006,
"yshift": -14
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$7.1M</b>",
"x": 1,
"y": -7121853.280000001,
"yshift": -14
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$1.5M</b>",
"x": 2,
"y": -1468771.6800000016,
"yshift": -14
},
{
"align": "left",
"bgcolor": "#fcfcfb",
"bordercolor": "#e1e0d9",
"borderwidth": 1,
"font": {
"color": "#52514e",
"size": 12
},
"showarrow": false,
"text": "3-yr net <b>-&#36;1.5M</b> · ROI <b>-39%</b><br>NPV@13.5% <b>-&#36;2.0M</b> · payback <b>beyond 2028</b><br>run-rate <b>&#36;6.6M/yr</b> post-window → breakeven <b>39 months (~Mar 2029, extrapolated)</b>",
"x": 0.01,
"xref": "paper",
"y": 0.98,
"yref": "paper"
}
],
"bargap": 0.45,
"barmode": "relative",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 500,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Migration + WFM — benefits vs incremental cost</b><br><span style='font-size:12px;color:#898781'>Baseline = keep paying $7.3M/yr · the platform swap alone does not pay back inside the deck's own 3-year window</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"fig.add_trace(bar(X, [ben_by_year[y] for y in YEARS],\n",
" \"WFM benefits (verbatim, scoped)\", \"#1baf7a\"))\n",
"fig.add_trace(bar(X, [-inc[y] for y in YEARS],\n",
" \"Incremental cost vs $7.3M/yr baseline\", \"#e34948\"))\n",
"cum_net = pd.Series([net_by[y] for y in YEARS]).cumsum()\n",
"fig.add_trace(cum_line(X, cum_net, \"Cumulative net\"))\n",
"for i, c in enumerate(cum_net):\n",
" fig.add_annotation(x=i, y=float(c), text=f\"<b>{money(float(c))}</b>\", showarrow=False,\n",
" yshift=14 if c >= 0 else -14, font=dict(size=12, color=INK))\n",
"fig.update_layout(barmode=\"relative\")\n",
"fig.add_annotation(\n",
" xref=\"paper\", yref=\"paper\", x=0.01, y=0.98, align=\"left\", showarrow=False,\n",
" font=dict(size=12, color=INK2), bgcolor=SURFACE, bordercolor=GRID, borderwidth=1,\n",
" text=(f\"3-yr net <b>{html_money(kpi['net_3yr'])}</b> · \"\n",
" f\"ROI <b>{fmt_roi(kpi)}</b><br>\"\n",
" f\"NPV@{DISCOUNT_RATE:.1%} <b>{html_money(kpi['npv'])}</b> · \"\n",
" f\"payback <b>{kpi['payback']}</b><br>\"\n",
" f\"run-rate <b>{html_money(RUNRATE_SAVING)}/yr</b> post-window → \"\n",
" f\"breakeven <b>{BREAKEVEN}</b>\"))\n",
"tei_layout(fig, \"Migration + WFM — benefits vs incremental cost\",\n",
" \"Baseline = keep paying $7.3M/yr · the platform swap alone does not pay \"\n",
" \"back inside the deck's own 3-year window\", height=500)\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "7ca576f1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.517991Z",
"iopub.status.busy": "2026-07-13T19:34:24.517696Z",
"iopub.status.idle": "2026-07-13T19:34:24.533900Z",
"shell.execute_reply": "2026-07-13T19:34:24.532905Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The signed contracts move the 3-yr case by $2.2M vs the deck frame — the licence correction helps; SOW managed services cost against it. For the with-AI case in the same frame, open the Corrected Business Case notebook.\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Deck frame ($4.3M/yr, deck PS)</th>\n",
" <th>Contracted ($3.2M/yr + SOW)</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>3-yr WFM benefits (verbatim, scoped)</th>\n",
" <td>$2.3M</td>\n",
" <td>$2.3M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3-yr incremental cost</th>\n",
" <td>$6.0M</td>\n",
" <td>$3.8M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3-yr net</th>\n",
" <td>-$3.7M</td>\n",
" <td>-$1.5M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ROI (net ÷ incremental cost)</th>\n",
" <td>-62%</td>\n",
" <td>-39%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV @ 13.5% (deck rate)</th>\n",
" <td>-$3.8M</td>\n",
" <td>-$2.0M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV @ 8.0% (CTM treasury)</th>\n",
" <td>-$3.8M</td>\n",
" <td>-$1.8M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Payback (202628 window)</th>\n",
" <td>beyond 2028</td>\n",
" <td>beyond 2028</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Steady-state run-rate saving</th>\n",
" <td>$5.9M/yr</td>\n",
" <td>$6.6M/yr</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Breakeven</th>\n",
" <td>44 months (~Aug 2029, extrapolated)</td>\n",
" <td>39 months (~Mar 2029, extrapolated)</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Deck frame ($4.3M/yr, deck PS) \\\n",
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
"3-yr incremental cost $6.0M \n",
"3-yr net -$3.7M \n",
"ROI (net ÷ incremental cost) -62% \n",
"NPV @ 13.5% (deck rate) -$3.8M \n",
"NPV @ 8.0% (CTM treasury) -$3.8M \n",
"Payback (202628 window) beyond 2028 \n",
"Steady-state run-rate saving $5.9M/yr \n",
"Breakeven 44 months (~Aug 2029, extrapolated) \n",
"\n",
" Contracted ($3.2M/yr + SOW) \n",
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
"3-yr incremental cost $3.8M \n",
"3-yr net -$1.5M \n",
"ROI (net ÷ incremental cost) -39% \n",
"NPV @ 13.5% (deck rate) -$2.0M \n",
"NPV @ 8.0% (CTM treasury) -$1.8M \n",
"Payback (202628 window) beyond 2028 \n",
"Steady-state run-rate saving $6.6M/yr \n",
"Breakeven 39 months (~Mar 2029, extrapolated) "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def kpi_display(k, net, runrate, breakeven):\n",
" return {\n",
" \"3-yr WFM benefits (verbatim, scoped)\": money(k[\"benefits_3yr\"]),\n",
" \"3-yr incremental cost\": money(k[\"incremental_cost_3yr\"]),\n",
" \"3-yr net\": money(k[\"net_3yr\"]),\n",
" \"ROI (net ÷ incremental cost)\": fmt_roi(k),\n",
" f\"NPV @ {k['discount_rate']:.1%} (deck rate)\": money(k[\"npv\"]),\n",
" \"NPV @ 8.0% (CTM treasury)\": money(npv([net[y] for y in YEARS], 0.08)),\n",
" \"Payback (202628 window)\": k[\"payback\"],\n",
" \"Steady-state run-rate saving\": f\"{money(runrate)}/yr\",\n",
" \"Breakeven\": breakeven,\n",
" }\n",
"\n",
"\n",
"# Deck-anchored comparison: the deck's own frame — pitched licence rate,\n",
"# verbatim year-1 PS lump, no managed services (the SOW lines are contracted\n",
"# facts the deck never carried).\n",
"_lic_deck = licence_costs_by_year(RAMP_MONTHS, TCO_VERBATIM[\"ccaas_annual\"])\n",
"_ps_deck = ps_costs_by_year()\n",
"total_deck = {y: current_by_year[y] + _lic_deck[y] + _ps_deck[y] for y in YEARS}\n",
"inc_deck, net_deck = case_flows(total_deck, ben_by_year)\n",
"kpi_deck = case_kpis(inc_deck, net_deck, DISCOUNT_RATE)\n",
"RUNRATE_DECK = runrate_saving_annual(TCO_VERBATIM[\"ccaas_annual\"], WFM_REGIONS)\n",
"BREAKEVEN_DECK = runrate_breakeven_label(net_deck, RUNRATE_DECK)\n",
"\n",
"kpis_fmt = pd.DataFrame({\n",
" f\"Deck frame ({money(TCO_VERBATIM['ccaas_annual'])}/yr, deck PS)\":\n",
" kpi_display(kpi_deck, net_deck, RUNRATE_DECK, BREAKEVEN_DECK),\n",
" f\"Contracted ({money(LICENCE_ANNUAL)}/yr + SOW)\":\n",
" kpi_display(kpi, net_by, RUNRATE_SAVING, BREAKEVEN),\n",
"})\n",
"print(f\"The signed contracts move the 3-yr case by \"\n",
" f\"{money(kpi['net_3yr'] - kpi_deck['net_3yr'])} vs the deck frame — the licence \"\n",
" f\"correction helps; SOW managed services cost against it. \"\n",
" f\"For the with-AI case in the same frame, open the Corrected Business Case notebook.\")\n",
"display(kpis_fmt)"
]
},
{
"cell_type": "markdown",
"id": "a7329ce6",
"metadata": {},
"source": [
"<a id=\"section-6\"></a>\n",
"\n",
"## 6 · Sensitivity — ramp × licence run-rate\n",
"\n",
"The two commercial levers Genesys controls directly, swept against the 3-yr net:\n",
"the **ramp programme length** (licence-free months) and the **licence run-rate**\n",
"(contracted $3.2M/yr vs the deck's pitched $4.3M/yr; 🟡 WEM-SKU inclusion either way).\n",
"Contract **termination dates**\n",
"are the other big lever — edit the per-region dates in section 1 and the whole\n",
"notebook recomputes."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "4ab17e24",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.536467Z",
"iopub.status.busy": "2026-07-13T19:34:24.536203Z",
"iopub.status.idle": "2026-07-13T19:34:24.584567Z",
"shell.execute_reply": "2026-07-13T19:34:24.583618Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"colorscale": [
[
0.0,
"#e34948"
],
[
0.5,
"#f0efe9"
],
[
1.0,
"#2a78d6"
]
],
"hovertemplate": "%{y} × %{x}: %{z:$,.0f}<extra></extra>",
"showscale": false,
"type": "heatmap",
"x": [
"$3.2M/yr (contracted)",
"$3.8M/yr",
"$4.3M/yr (deck)"
],
"xgap": 2,
"y": [
"0-mo ramp",
"6-mo ramp",
"12-mo ramp",
"18-mo ramp",
"24-mo ramp"
],
"ygap": 2,
"z": [
[
-3068771.6800000016,
-4868771.680000002,
-6368771.680000002
],
[
-1468771.6800000016,
-2968771.6800000016,
-4218771.680000002
],
[
131228.31999999844,
-1068771.6800000016,
-2068771.6800000016
],
[
1731228.3199999984,
831228.3199999984,
81228.31999999844
],
[
3331228.3199999984,
2731228.3199999984,
2231228.3199999984
]
],
"zmid": 0
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$3.1M</b>",
"x": 0,
"y": 0
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>-$4.9M</b>",
"x": 1,
"y": 0
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>-$6.4M</b>",
"x": 2,
"y": 0
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$1.5M</b>",
"x": 0,
"y": 1
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$3.0M</b>",
"x": 1,
"y": 1
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>-$4.2M</b>",
"x": 2,
"y": 1
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$131K</b>",
"x": 0,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$1.1M</b>",
"x": 1,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$2.1M</b>",
"x": 2,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$1.7M</b>",
"x": 0,
"y": 3
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$831K</b>",
"x": 1,
"y": 3
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$81K</b>",
"x": 2,
"y": 3
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$3.3M</b>",
"x": 0,
"y": 4
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.7M</b>",
"x": 1,
"y": 4
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.2M</b>",
"x": 2,
"y": 4
}
],
"bargap": 0.45,
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 400,
"hovermode": "closest",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>3-yr net across ramp length × licence run-rate</b><br><span style='font-size:12px;color:#898781'>Diverging scale centered at $0 · SOW PS + managed services included · WFM scope and contract dates held at current inputs</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Contracted corner (6-mo ramp × $3.2M/yr): -$1.5M · deck-rate corner (6-mo × $4.3M/yr): -$4.2M — the licence correction recovers $2.8M but the 3-yr window stays negative; breakeven rides the post-2028 run-rate.\n"
]
}
],
"source": [
"# ── 3-yr net across ramp months × licence run-rate ───────────────────\n",
"RAMP_GRID = [0, 6, 12, 18, 24]\n",
"LIC_GRID = [3_200_000, 3_800_000, 4_300_000]\n",
"_LIC_TAG = {3_200_000: \" (contracted)\", 4_300_000: \" (deck)\"}\n",
"\n",
"\n",
"def net_3yr(ramp_months, licence_annual):\n",
" lby = licence_costs_by_year(ramp_months, licence_annual)\n",
" t = {y: current_by_year[y] + lby[y] + ps_by_year[y] + managed_by_year[y]\n",
" for y in YEARS}\n",
" _, n = case_flows(t, ben_by_year)\n",
" return sum(n.values())\n",
"\n",
"\n",
"z = [[net_3yr(rm, lc) for lc in LIC_GRID] for rm in RAMP_GRID]\n",
"_zabs = max(abs(v) for row in z for v in row)\n",
"fig = go.Figure(go.Heatmap(\n",
" z=z, x=[f\"{money(lc)}/yr{_LIC_TAG.get(lc, '')}\" for lc in LIC_GRID],\n",
" y=[f\"{rm}-mo ramp\" for rm in RAMP_GRID],\n",
" zmid=0, colorscale=DIVERGING, showscale=False, xgap=2, ygap=2,\n",
" hovertemplate=\"%{y} × %{x}: %{z:$,.0f}<extra></extra>\"))\n",
"for i, rm in enumerate(RAMP_GRID):\n",
" for j, lc in enumerate(LIC_GRID):\n",
" v = z[i][j]\n",
" fig.add_annotation(x=j, y=i, text=f\"<b>{money(v)}</b>\", showarrow=False,\n",
" font=dict(size=12,\n",
" color=\"#ffffff\" if abs(v) > 0.65 * _zabs else INK))\n",
"tei_layout(fig, \"3-yr net across ramp length × licence run-rate\",\n",
" \"Diverging scale centered at $0 · SOW PS + managed services included · \"\n",
" \"WFM scope and contract dates held at current inputs\", height=400)\n",
"fig.update_layout(xaxis=dict(showgrid=False), yaxis=dict(showgrid=False, tickformat=None),\n",
" hovermode=\"closest\")\n",
"fig.show()\n",
"print(f\"Contracted corner ({RAMP_MONTHS}-mo ramp × {money(LICENCE_ANNUAL)}/yr): \"\n",
" f\"{money(net_3yr(RAMP_MONTHS, LICENCE_ANNUAL))} · deck-rate corner \"\n",
" f\"({RAMP_MONTHS}-mo × {money(TCO_VERBATIM['ccaas_annual'])}/yr): \"\n",
" f\"{money(net_3yr(RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} — the licence \"\n",
" f\"correction recovers \"\n",
" f\"{money(net_3yr(RAMP_MONTHS, LICENCE_ANNUAL) - net_3yr(RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} \"\n",
" f\"but the 3-yr window stays negative; breakeven rides the post-2028 run-rate.\")"
]
},
{
"cell_type": "markdown",
"id": "13d4932f",
"metadata": {},
"source": [
"<a id=\"section-7\"></a>\n",
"\n",
"## 7 · Verification & assertions\n",
"\n",
"The next cell re-derives key numbers independently and **raises on any failure**, so\n",
"`jupyter nbconvert --execute` acts as a regression gate for this notebook. Engine pins\n",
"use explicit default arguments so the gate tests `tokencalc`, not the current widget\n",
"state; live-state checks are guarded so the cell stays valid after you edit the inputs."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "939417e4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.587372Z",
"iopub.status.busy": "2026-07-13T19:34:24.587093Z",
"iopub.status.idle": "2026-07-13T19:34:24.611911Z",
"shell.execute_reply": "2026-07-13T19:34:24.610957Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All assertions passed.\n",
" WFM benefits 3-yr $2.3M · programme cost 3-yr $25.7M · net 3-yr -$1.5M · breakeven 39 months (~Mar 2029, extrapolated)\n"
]
}
],
"source": [
"def _approx(got, want, tol=0.5):\n",
" assert abs(got - want) <= tol, f\"got {got:,.2f}, want {want:,.2f}\"\n",
"\n",
"\n",
"# Section 1 — contract mechanics (engine pins, explicit defaults)\n",
"_seed_check = current_state_inputs(sites)\n",
"_approx(_seed_check[\"annual_cost\"].sum(), 7_300_000)\n",
"assert DEFAULT_RAMP_MONTHS == 6 # 🟢 order form\n",
"_l_deck = licence_costs_by_year() # deck rate at the real ramp\n",
"_approx(_l_deck[2026], 2_150_000)\n",
"_approx(_l_deck[2027], 4_300_000)\n",
"_approx(licence_costs_by_year(12)[2026], 0) # mechanics: 12-mo ramp zeroes 2026\n",
"_contracts_at_default = (\n",
" (current_state[\"annual_cost\"] - _seed_check[\"annual_cost\"]).abs().max() < 1\n",
" and all(t == dt.date(2027, 12, 31) for t in current_state[\"contract_termination\"])\n",
")\n",
"if _contracts_at_default:\n",
" # widget seeds are whole dollars → per-region rounding of ≤ $0.50\n",
" _approx(current_by_year[2026], 7_300_000, tol=len(REGIONS))\n",
" _approx(current_by_year[2028], 0)\n",
"\n",
"# NTT SOW — billing milestones + managed services (🟢 contractual)\n",
"_approx(PS_CONTRACTED_TOTAL, 2_025_446.48, tol=0.01)\n",
"_ps_c = ps_costs_by_year(contracted=True)\n",
"_approx(_ps_c[2026], 607_633.94 + 405_089.30 + 167_000, tol=0.01) # 30% + 20% + training\n",
"_approx(_ps_c[2027], 607_633.94 + 405_089.30, tol=0.01) # 30% + 20%\n",
"_ms_c = managed_services_by_year()\n",
"_approx(_ms_c[2026], MANAGED_SERVICES_ANNUAL * 2 / 12, tol=0.01) # NovDec after go-live\n",
"_approx(_ms_c[2027], MANAGED_SERVICES_ANNUAL, tol=0.01)\n",
"\n",
"# Sections 2-3 — WFM scope & phasing at engine defaults\n",
"_, _, _rollout_d = build_rollouts(sites)\n",
"_wfm_d = wfm_benefits_by_year(_rollout_d) # NA + ANZ + ASIA\n",
"assert set(_wfm_d[\"region\"]) == set(DEFAULT_WFM_REGIONS)\n",
"_approx(_wfm_d[\"benefit\"].sum(), 2_314_000) # $0 NA + $1.4M ANZ + $914K ASIA\n",
"_by_d = _wfm_d.groupby(\"year\")[\"benefit\"].sum()\n",
"_approx(_by_d[2026], 0)\n",
"_approx(_by_d[2027], 1_400_000 * 4 / 16, tol=1) # ANZ: 4 of 16 live months in 2027 (PM timeline)\n",
"_approx(wfm_annual_runrate(), 2_900_000)\n",
"_approx(runrate_saving_annual(), 5_900_000) # deck frame: no managed services\n",
"\n",
"# Contracted licence overlay (tokencalc.appendix4.TCO_CONTRACTED)\n",
"_approx(tco(\"ccaas_annual\"), 3_200_000) # signed correction\n",
"_approx(TCO_VERBATIM[\"ccaas_annual\"], 4_300_000) # deck record stays verbatim\n",
"_lc = licence_costs_by_year(annual=tco(\"ccaas_annual\"))\n",
"_approx(_lc[2026], 1_600_000) # 6-mo ramp: half-year bills in 2026\n",
"_approx(_lc[2027], 3_200_000)\n",
"_approx(runrate_saving_annual(tco(\"ccaas_annual\"),\n",
" managed_annual=MANAGED_SERVICES_ANNUAL),\n",
" 6_589_081.60, tol=0.01)\n",
"\n",
"# Section 5 — default-flow pins (pure engine defaults, no widget state)\n",
"_cur_d = current_costs_by_year(_seed_check)\n",
"_ben_d = {y: float(_by_d.get(y, 0.0)) for y in YEARS}\n",
"_tot_d = {y: _cur_d[y] + _l_deck[y] + ps_costs_by_year()[y] for y in YEARS}\n",
"_inc_d, _net_d = case_flows(_tot_d, _ben_d)\n",
"_approx(sum(_net_d.values()), -3_703_000, tol=1) # deck frame\n",
"assert runrate_breakeven_label(_net_d, runrate_saving_annual()) == \\\n",
" \"44 months (~Aug 2029, extrapolated)\"\n",
"_tot_c = {y: _cur_d[y] + _lc[y] + _ps_c[y] + _ms_c[y] for y in YEARS}\n",
"_inc_c, _net_c = case_flows(_tot_c, _ben_d)\n",
"_approx(sum(_net_c.values()), -1_468_770, tol=1) # contracted frame — still negative\n",
"assert runrate_breakeven_label(\n",
" _net_c, runrate_saving_annual(tco(\"ccaas_annual\"),\n",
" managed_annual=MANAGED_SERVICES_ANNUAL)) == \\\n",
" \"39 months (~Mar 2029, extrapolated)\"\n",
"\n",
"# Live state — stack and flows tie out at whatever the widgets say\n",
"_approx(sum(total_by_year.values()),\n",
" sum(licence_by_year.values()) + sum(ps_by_year.values())\n",
" + sum(managed_by_year.values()) + sum(current_by_year.values()))\n",
"for y in YEARS:\n",
" _approx(net_by[y], ben_by_year[y] - (total_by_year[y] - BASELINE_ANNUAL))\n",
"\n",
"backstage(\"All assertions passed.\")\n",
"backstage(f\" WFM benefits 3-yr {money(sum(ben_by_year.values()))} · \"\n",
" f\"programme cost 3-yr {money(sum(total_by_year.values()))} · \"\n",
" f\"net 3-yr {money(kpi['net_3yr'])} · breakeven {BREAKEVEN}\")\n"
]
},
{
"cell_type": "markdown",
"id": "67ac8561",
"metadata": {},
"source": [
"<a id=\"section-8\"></a>\n",
"\n",
"## 8 · Risks, notes & next steps\n",
"\n",
"**Findings to lead with**\n",
"- **The migration alone does not stand up inside the window, even at the signed rate**:\n",
" with the full contracts in ($3.2M/yr licences, 6-month ramp, SOW milestones, managed\n",
" services) the 3-yr net is ≈ $1.5M, breaking even ~Mar 2029 on run-rate alone; the deck\n",
" frame ($4.3M/yr, deck's PS lump) runs ≈ $3.7M / ~Aug 2029. Keep both KPI columns in\n",
" front of CTM — the walk from the deck's own numbers is the credibility story, and the\n",
" post-termination run-rate is what carries the case. (With-AI case: the Corrected\n",
" Business Case notebook.)\n",
"- **2026 is a pure cost year** here too — PS outlay plus double-billing, zero benefits.\n",
"- The ramp credit and the term-contract run-off cut in opposite directions; both are\n",
" contract facts the deck's cost case omitted.\n",
"\n",
"**Known gaps / assumptions**\n",
"- 🟡 Licence run-rate assumes WFM/WEM SKUs (incl. new APAC seats) are inside $4.3M/yr —\n",
" if WEM is an add-on, raise the sidebar run-rate; section 6 shows the sensitivity.\n",
"- 🟡 Regional current-cost split is an agent-share allocation pending real contract data;\n",
" if NA's existing WFM tool bills separately, fold it into NA's section-1 input.\n",
"- Deck quirk kept verbatim: ASIA WFM annual ($1.6M) exceeds its 3-yr value ($914K); the\n",
" annual value feeds the run-rate extrapolation, the 3-yr value phases in-window.\n",
"- EMEA WFM ($687K 3-yr) is an upside option — sidebar toggle.\n",
"- 🟡 Training ($167K) is kept as the deck's verbatim year-1 line — the SOW milestones\n",
" don't itemize it. Managed services are modelled with no end date inside the window.\n",
"- Not modelled: early-termination fees, co-term options, migration costs beyond the SOW,\n",
" and any benefit realization lag beyond Genesys's own schedule.\n",
"\n",
"**Next steps**\n",
"- Feed real per-region contract values and termination dates into section 1.\n",
"- Confirm in the order form that WFM/WEM SKUs and the APAC seats are inside the run-rate.\n",
"- Pressure-test the base-price claim: does the $2.03M SOW really include the NA WFM\n",
" migration and two net-new WFM implementations?"
]
},
{
"cell_type": "markdown",
"id": "b8ee762f",
"metadata": {},
"source": [
"<a id=\"section-9\"></a>\n",
"\n",
"## 9 · Machine-readable data appendix\n",
"\n",
"Every number behind the figures above, emitted as markdown tables plus a JSON block, so an\n",
"LLM can draft the client report or presentation directly from the HTML/markdown export\n",
"(`python scripts/export_report.py`). Plotly figures export as JavaScript an LLM cannot\n",
"read — this section carries the data. The dump renders **backstage** (JupyterLab and the exports) and stays hidden in the Mercury app. The tables reflect the **current** input state, so a\n",
"client-customized session exports a client-customized appendix."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "32ebed57",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-13T19:34:24.615086Z",
"iopub.status.busy": "2026-07-13T19:34:24.614717Z",
"iopub.status.idle": "2026-07-13T19:34:24.652647Z",
"shell.execute_reply": "2026-07-13T19:34:24.651541Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"#### Current-state contracts by region (inputs)\n",
"\n",
"| region | agents | annual_cost | contract_termination | confidence |\n",
"|:---------|---------:|--------------:|:-----------------------|:------------------------------------------------|\n",
"| NA | 890 | 3,348,969 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
"| ANZ | 180 | 677,320 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
"| EMEA | 320 | 1,204,124 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
"| ASIA | 550 | 2,069,588 | 2027-12-31 | 🟡 agent-share allocation of the verbatim total |\n",
"\n",
"#### WFM scope — treatment and verbatim values ($)\n",
"\n",
"| region | WFM treatment | in scope | verbatim annual | verbatim 3-yr |\n",
"|:---------|:--------------------------|:-----------|------------------:|----------------:|\n",
"| NA | migrate existing users | ✓ | 0 | 0 |\n",
"| ANZ | implement (APAC) | ✓ | 1300000 | 1400000 |\n",
"| EMEA | implement (upside option) | — | 824000 | 687000 |\n",
"| ASIA | implement (APAC) | ✓ | 1600000 | 914000 |\n",
"\n",
"#### WFM benefits by region × year (schedule-phased, $)\n",
"\n",
"| region | 2026 | 2027 | 2028 |\n",
"|:---------|-------:|--------:|----------:|\n",
"| ANZ | 0 | 350,000 | 1,050,000 |\n",
"| ASIA | 0 | 0 | 914,000 |\n",
"| NA | 0 | 0 | 0 |\n",
"\n",
"#### Migration programme cost stack ($)\n",
"\n",
"| | 2026 | 2027 | 2028 | 3-yr |\n",
"|:----------------------------------------------------|----------:|----------:|----------:|-----------:|\n",
"| CCaaS platform licences (contracted, ramp-adjusted) | 1,600,000 | 3,200,000 | 3,200,000 | 8,000,000 |\n",
"| NTT PS + training (SOW billing milestones) | 1,179,723 | 1,012,723 | 0 | 2,192,446 |\n",
"| NTT managed services (from MCX go-live) | 68,486 | 410,918 | 410,918 | 890,323 |\n",
"| Existing platform (term-contract run-off) | 7,300,001 | 7,300,001 | 0 | 14,600,002 |\n",
"\n",
"#### Business-case flows vs do-nothing baseline ($)\n",
"\n",
"| | 2026 | 2027 | 2028 |\n",
"|:-----------------|-----------:|-----------:|-----------:|\n",
"| programme cost | 10,148,211 | 11,923,643 | 3,610,918 |\n",
"| incremental cost | 2,848,211 | 4,623,643 | -3,689,082 |\n",
"| net | -2,848,211 | -4,273,643 | 5,653,082 |\n",
"\n",
"#### KPIs — migration + WFM (no AI)\n",
"\n",
"| | Deck frame ($4.3M/yr, deck PS) | Contracted ($3.2M/yr + SOW) |\n",
"|:-------------------------------------|:------------------------------------|:------------------------------------|\n",
"| 3-yr WFM benefits (verbatim, scoped) | $2.3M | $2.3M |\n",
"| 3-yr incremental cost | $6.0M | $3.8M |\n",
"| 3-yr net | -$3.7M | -$1.5M |\n",
"| ROI (net ÷ incremental cost) | -62% | -39% |\n",
"| NPV @ 13.5% (deck rate) | -$3.8M | -$2.0M |\n",
"| NPV @ 8.0% (CTM treasury) | -$3.8M | -$1.8M |\n",
"| Payback (202628 window) | beyond 2028 | beyond 2028 |\n",
"| Steady-state run-rate saving | $5.9M/yr | $6.6M/yr |\n",
"| Breakeven | 44 months (~Aug 2029, extrapolated) | 39 months (~Mar 2029, extrapolated) |\n",
"\n",
"#### Model state (JSON)\n",
"\n",
"```json\n",
"{\n",
" \"scenario\": \"Genesys Cloud CX migration + WFM (NA migrate, APAC implement) \\u2014 no AI\",\n",
" \"wfm_benefits_by_year\": {\n",
" \"2026\": 0,\n",
" \"2027\": 350000,\n",
" \"2028\": 1964000\n",
" },\n",
" \"cost_by_year\": {\n",
" \"2026\": 10148211,\n",
" \"2027\": 11923643,\n",
" \"2028\": 3610918\n",
" },\n",
" \"net_by_year\": {\n",
" \"2026\": -2848211,\n",
" \"2027\": -4273643,\n",
" \"2028\": 5653082\n",
" },\n",
" \"kpis\": {\n",
" \"benefits_3yr\": 2314000,\n",
" \"incremental_cost_3yr\": 3782772,\n",
" \"net_3yr\": -1468772,\n",
" \"roi\": -0.3883,\n",
" \"npv\": -1960584,\n",
" \"discount_rate\": 0.135,\n",
" \"payback\": \"beyond 2028\"\n",
" },\n",
" \"kpis_at_deck_rate\": {\n",
" \"benefits_3yr\": 2314000,\n",
" \"incremental_cost_3yr\": 6017002,\n",
" \"net_3yr\": -3703002,\n",
" \"roi\": -0.6154,\n",
" \"npv\": -3827149,\n",
" \"discount_rate\": 0.135,\n",
" \"payback\": \"beyond 2028\"\n",
" },\n",
" \"runrate_saving_annual\": 6589082,\n",
" \"breakeven\": \"39 months (~Mar 2029, extrapolated)\",\n",
" \"assumptions\": {\n",
" \"ramp_months\": 6,\n",
" \"licence_annual\": 3200000,\n",
" \"deck_ccaas_annual\": 4300000,\n",
" \"ps_contracted_total\": 2025446.48,\n",
" \"ps_milestones\": [\n",
" {\n",
" \"name\": \"SOW Effective Date\",\n",
" \"date\": \"2026-03-15\",\n",
" \"amount\": 607633.94\n",
" },\n",
" {\n",
" \"name\": \"Start of client UAT (first region, NA)\",\n",
" \"date\": \"2026-10-16\",\n",
" \"amount\": 405089.3\n",
" },\n",
" {\n",
" \"name\": \"Start of client UAT (last region, APAC)\",\n",
" \"date\": \"2027-10-01\",\n",
" \"amount\": 607633.94\n",
" },\n",
" {\n",
" \"name\": \"Completion of last go-live migration\",\n",
" \"date\": \"2027-11-01\",\n",
" \"amount\": 405089.3\n",
" }\n",
" ],\n",
" \"managed_services_annual\": 410918.4,\n",
" \"mcx_go_live\": \"2026-10-16\",\n",
" \"licence_includes_wfm_skus\": true,\n",
" \"wfm_regions\": [\n",
" \"NA\",\n",
" \"ANZ\",\n",
" \"ASIA\"\n",
" ],\n",
" \"migration_and_wfm_in_base_price\": true,\n",
" \"discount_rate\": 0.135,\n",
" \"baseline_annual\": 7300000,\n",
" \"current_state_by_region\": {\n",
" \"NA\": {\n",
" \"annual_cost\": 3348969,\n",
" \"contract_termination\": \"2027-12-31\"\n",
" },\n",
" \"ANZ\": {\n",
" \"annual_cost\": 677320,\n",
" \"contract_termination\": \"2027-12-31\"\n",
" },\n",
" \"EMEA\": {\n",
" \"annual_cost\": 1204124,\n",
" \"contract_termination\": \"2027-12-31\"\n",
" },\n",
" \"ASIA\": {\n",
" \"annual_cost\": 2069588,\n",
" \"contract_termination\": \"2027-12-31\"\n",
" }\n",
" }\n",
" }\n",
"}\n",
"```\n"
]
}
],
"source": [
"# ── Data appendix — LLM-readable dump of every model output ──────────\n",
"# Renders backstage only (JupyterLab / nbconvert exports) — hidden on\n",
"# the Mercury stage, where the narrative and figures carry the story.\n",
"import json as _json\n",
"\n",
"\n",
"def _section(title, df, **kw):\n",
" backstage(f\"\\n#### {title}\\n\")\n",
" backstage(df.to_markdown(floatfmt=\",.0f\", **kw))\n",
"\n",
"\n",
"_section(\"Current-state contracts by region (inputs)\",\n",
" current_state.reset_index().drop(columns=[\"share\"]), index=False)\n",
"_section(\"WFM scope — treatment and verbatim values ($)\", wfm_scope_table)\n",
"if not wfm_long.empty:\n",
" _section(\"WFM benefits by region × year (schedule-phased, $)\",\n",
" wfm_long.pivot_table(index=\"region\", columns=\"year\",\n",
" values=\"benefit\", aggfunc=\"sum\"))\n",
"_section(\"Migration programme cost stack ($)\", migration_costs)\n",
"_section(\"Business-case flows vs do-nothing baseline ($)\",\n",
" pd.DataFrame({\"programme cost\": total_by_year,\n",
" \"incremental cost\": inc, \"net\": net_by}).T[YEARS])\n",
"_section(\"KPIs — migration + WFM (no AI)\", kpis_fmt)\n",
"\n",
"\n",
"def _jval(v):\n",
" if isinstance(v, float):\n",
" return round(v, 4) if abs(v) < 10 else round(v)\n",
" return v\n",
"\n",
"\n",
"backstage(\"\\n#### Model state (JSON)\\n\")\n",
"backstage(\"```json\")\n",
"backstage(_json.dumps({\n",
" \"scenario\": \"Genesys Cloud CX migration + WFM (NA migrate, APAC implement) — no AI\",\n",
" \"wfm_benefits_by_year\": {str(y): round(ben_by_year[y]) for y in YEARS},\n",
" \"cost_by_year\": {str(y): round(total_by_year[y]) for y in YEARS},\n",
" \"net_by_year\": {str(y): round(net_by[y]) for y in YEARS},\n",
" \"kpis\": {k: _jval(v) for k, v in kpi.items()},\n",
" \"kpis_at_deck_rate\": {k: _jval(v) for k, v in kpi_deck.items()},\n",
" \"runrate_saving_annual\": round(RUNRATE_SAVING),\n",
" \"breakeven\": BREAKEVEN,\n",
" \"assumptions\": {\n",
" \"ramp_months\": RAMP_MONTHS,\n",
" \"licence_annual\": round(LICENCE_ANNUAL),\n",
" \"deck_ccaas_annual\": round(TCO_VERBATIM[\"ccaas_annual\"]),\n",
" \"ps_contracted_total\": round(PS_CONTRACTED_TOTAL, 2),\n",
" \"ps_milestones\": [{\"name\": m[\"name\"], \"date\": m[\"date\"].isoformat(),\n",
" \"amount\": m[\"amount\"]} for m in PS_MILESTONES],\n",
" \"managed_services_annual\": MANAGED_SERVICES_ANNUAL,\n",
" \"mcx_go_live\": MCX_GO_LIVE.isoformat(),\n",
" \"licence_includes_wfm_skus\": True,\n",
" \"wfm_regions\": WFM_REGIONS,\n",
" \"migration_and_wfm_in_base_price\": True,\n",
" \"discount_rate\": DISCOUNT_RATE,\n",
" \"baseline_annual\": round(BASELINE_ANNUAL),\n",
" \"current_state_by_region\": {\n",
" r: {\"annual_cost\": round(float(current_state.loc[r, \"annual_cost\"])),\n",
" \"contract_termination\": current_state.loc[r, \"contract_termination\"].isoformat()}\n",
" for r in REGIONS},\n",
" },\n",
"}, indent=2))\n",
"backstage(\"```\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
"038fdeed63e244b7b3ee5a36c876039e": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"0fff3f6c07fe4316a91344082245882c": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.date.DateInputWidget",
"_css": "\n .mljar-date-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "ANZ — contract termination",
"layout": "IPY_MODEL_9de21a8805534b84a0d44b60c998113a",
"layout_path": null,
"max": "",
"min": "",
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "2027-12-31"
}
},
"1138fba4608d4de484f84b11e1ae2862": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"appearance": "toggle",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "WFM — EMEA (upside option)",
"last_changed_at": "",
"layout": "IPY_MODEL_d263a846169b4fcf8493e58a0a277820",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": false
}
},
"362dfb796cfc4881a89dd7e566db84d6": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"401b35065476433d96aeed7fcad746e8": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "ANZ — current platform cost ($/yr)",
"layout": "IPY_MODEL_ba02c7051fbc4fd0a304e49019f0e69b",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 677320.0
}
},
"4b5a5306392949a9a6c9f1414e28e166": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"655850770ff34fb8bf1076de30bf2ddc": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "Genesys licence run-rate ($/yr)",
"layout": "IPY_MODEL_9871a3fecaec4886b2da0dfdfa495ad9",
"layout_path": null,
"max": 10000000.0,
"min": 0.0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"step": 100000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 3200000.0
}
},
"6b6471d39e2544f58844cffb1d57afe9": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "HTMLView",
"cell_id": "",
"description": "",
"description_allow_html": false,
"layout": "IPY_MODEL_aa069fb9941e456191f05ffe96e143be",
"layout_path": null,
"placeholder": "",
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"style": "IPY_MODEL_d7fc62778a6f48fa8e03760f8e67f869",
"tabbable": null,
"tooltip": null,
"value": "<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: normal; line-height: 1.65; color: #0f172a; word-break: break-word;\"><p style=\"margin: 0 0 1em;\"><b>Jump to section</b><ol style=\"padding-left:1.2em;margin:6px 0; margin: 0 0 1em; padding-left: 1.4em;\"><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-1');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('1 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Current state &amp; contracts</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-2');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('2 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Scenario scope &amp; licences</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-3');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('3 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">WFM benefits</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-4');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('4 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Cost stack</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-5');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('5 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Business case</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-6');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('6 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Sensitivity</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-7');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('7 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verification &amp; assertions</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-8');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('8 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Risks &amp; notes</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-9');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('9 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
}
},
"7e22e4c6e3dd44fc8eeb96c4f501a342": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.date.DateInputWidget",
"_css": "\n .mljar-date-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "EMEA — contract termination",
"layout": "IPY_MODEL_4b5a5306392949a9a6c9f1414e28e166",
"layout_path": null,
"max": "",
"min": "",
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "2027-12-31"
}
},
"837fa7d9dfef4562b952d2a7a9ef2b99": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"8ffdd60ef54a4c829429b46b438c36db": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"appearance": "toggle",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "WFM — NA (migrate existing users)",
"last_changed_at": "",
"layout": "IPY_MODEL_f69fbf92b5684033896094b83d45cfd6",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"9045ff71291a455abb6f1d9e3c2cc471": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"9871a3fecaec4886b2da0dfdfa495ad9": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"9d2e70e1ffab471692880e7ce645219d": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"9de21a8805534b84a0d44b60c998113a": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"9eeb15eeffa249929fca65f62246b45b": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"a4de65fac18f44e086d105b7f75693fe": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "NA — current platform cost ($/yr)",
"layout": "IPY_MODEL_b1b5ab2787cd409bb2552372f9ace4b4",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 3348969.0
}
},
"aa069fb9941e456191f05ffe96e143be": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"abd40a60e1974670886b2bd43aab35c2": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"appearance": "toggle",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "WFM — ASIA (implement)",
"last_changed_at": "",
"layout": "IPY_MODEL_9d2e70e1ffab471692880e7ce645219d",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"b1b5ab2787cd409bb2552372f9ace4b4": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"b26784cff5864afb8f4760fd7bb8aa1a": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"b549f3626d8e46f2bf7e849cf91e381f": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"ba02c7051fbc4fd0a304e49019f0e69b": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"bab7c81ec7ae47deabd7fc74f205c38e": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "EMEA — current platform cost ($/yr)",
"layout": "IPY_MODEL_038fdeed63e244b7b3ee5a36c876039e",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 1204124.0
}
},
"c2a141345720426b933108c66be94b8b": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "ASIA — current platform cost ($/yr)",
"layout": "IPY_MODEL_b26784cff5864afb8f4760fd7bb8aa1a",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 2069588.0
}
},
"d263a846169b4fcf8493e58a0a277820": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"d4d0cebd7a9345e2a88c5fa842d24337": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.checkbox.CheckboxWidget",
"_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"appearance": "toggle",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "WFM — ANZ (implement)",
"last_changed_at": "",
"layout": "IPY_MODEL_9eeb15eeffa249929fca65f62246b45b",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"d7fc62778a6f48fa8e03760f8e67f869": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLStyleModel",
"state": {
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLStyleModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "StyleView",
"background": null,
"description_width": "",
"font_size": null,
"text_color": null
}
},
"ec33300bf1e14da98f0dd7bfc01a2f44": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.date.DateInputWidget",
"_css": "\n .mljar-date-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "ASIA — contract termination",
"layout": "IPY_MODEL_362dfb796cfc4881a89dd7e566db84d6",
"layout_path": null,
"max": "",
"min": "",
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "2027-12-31"
}
},
"f69fbf92b5684033896094b83d45cfd6": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"f755a69ae50540b3aac94ceb249b1cc2": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "Genesys ramp — licence-free months",
"layout": "IPY_MODEL_9045ff71291a455abb6f1d9e3c2cc471",
"layout_path": null,
"max": 24.0,
"min": 0.0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 1.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 6.0
}
},
"fa2b443aef4f4d2ab8e12371c8da9a52": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.select.SelectWidget",
"_css": "\n .mljar-select-container {\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n }\n\n .mljar-select-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: none;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n margin-top: 6px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n container.appendChild(dropdown);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target)) {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n isEditing = false;\n setOpen(false);\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n document.removeEventListener(\"click\", handleDocumentClick);\n };\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"choices": [
"13.5% (deck)",
"8.0% (CTM treasury)"
],
"disabled": false,
"hidden": false,
"label": "NPV discount rate",
"layout": "IPY_MODEL_837fa7d9dfef4562b952d2a7a9ef2b99",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "13.5% (deck)"
}
},
"ff3441542e0b43db946e184e4b461b1e": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.date.DateInputWidget",
"_css": "\n .mljar-date-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-date-label {\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-date-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n box-sizing: border-box;\n line-height: 1.4;\n }\n\n .mljar-date-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-date-input:focus {\n outline: none;\n border-color: #007bff;\n border-width: 2px;\n box-shadow: none;\n }\n\n @media (max-width: 768px) {\n .mljar-date-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-date-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-date-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"date\";\n input.classList.add(\"mljar-date-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Date\";\n input.value = model.get(\"value\") || \"\";\n\n const min = model.get(\"min\") || \"\";\n const max = model.get(\"max\") || \"\";\n if (min) input.min = min; else input.removeAttribute(\"min\");\n if (max) input.max = max; else input.removeAttribute(\"max\");\n\n input.disabled = !!model.get(\"disabled\");\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n model.save_changes();\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "NA — contract termination",
"layout": "IPY_MODEL_b549f3626d8e46f2bf7e849cf91e381f",
"layout_path": null,
"max": "",
"min": "",
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "2027-12-31"
}
}
},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}