Files
palladium/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_migration_wfm.ipynb

7392 lines
328 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** — the verbatim $2.4M professional services +\n",
"$167K training. The only 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 · 12-month ramp credit |\n",
"| Base PS + training | Same deck | Verbatim $2.4M + $167K, year 1 — **includes migration + WFM** |\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-08T12:20:32.301747Z",
"iopub.status.busy": "2026-07-08T12:20:32.301520Z",
"iopub.status.idle": "2026-07-08T12:20:32.712911Z",
"shell.execute_reply": "2026-07-08T12:20:32.712211Z"
}
},
"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",
" money, html_money, month_label, ps_costs_by_year,\n",
" 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",
" \"Base PS + training (incl. migration + WFM)\": \"#1baf7a\",\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-08T12:20:32.715665Z",
"iopub.status.busy": "2026-07-08T12:20:32.715313Z",
"iopub.status.idle": "2026-07-08T12:20:32.728544Z",
"shell.execute_reply": "2026-07-08T12:20:32.727683Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
"position": "sidebar",
"widget": "MarkdownWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
"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 = 12`: licences are free for the first 12 months,\n",
" so the licence run-rate bills from month 13."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "f8863db7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-08T12:20:32.730401Z",
"iopub.status.busy": "2026-07-08T12:20:32.730193Z",
"iopub.status.idle": "2026-07-08T12:20:32.768973Z",
"shell.execute_reply": "2026-07-08T12:20:32.768081Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x783236e37e00>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "27e2171a593043338205c5b352f20f3d",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "27e2171a593043338205c5b352f20f3d",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7831ee7ffc50>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "60a9596904a34374bc605974ac3a3235",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "60a9596904a34374bc605974ac3a3235",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x783236e37cb0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x783236c016d0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x783236c01e50>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "bae439b7a2d04ab29390996726534753",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "bae439b7a2d04ab29390996726534753",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7831ee84afd0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x783236c02210>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "f8196ba7d89c487eb23ad614f78881fe",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "f8196ba7d89c487eb23ad614f78881fe",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7831ee84b490>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.date.DateInputWidget object at 0x7831ee84b5c0>"
]
},
"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-08T12:20:32.771129Z",
"iopub.status.busy": "2026-07-08T12:20:32.770911Z",
"iopub.status.idle": "2026-07-08T12:20:32.786705Z",
"shell.execute_reply": "2026-07-08T12:20:32.786022Z"
}
},
"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**: the verbatim $2.4M PS + $167K\n",
" training in year 1. No extra cost lines for migration or WFM.\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-08T12:20:32.788467Z",
"iopub.status.busy": "2026-07-08T12:20:32.788266Z",
"iopub.status.idle": "2026-07-08T12:20:32.804322Z",
"shell.execute_reply": "2026-07-08T12:20:32.803665Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7831ee6969f0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7831ee876ba0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "caae1a34df0945249b1a3d39320540b1",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "caae1a34df0945249b1a3d39320540b1",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x783236c02850>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x783236c02710>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.checkbox.CheckboxWidget object at 0x7831ee84be10>"
]
},
"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-08T12:20:32.806129Z",
"iopub.status.busy": "2026-07-08T12:20:32.805883Z",
"iopub.status.idle": "2026-07-08T12:20:32.821726Z",
"shell.execute_reply": "2026-07-08T12:20:32.820984Z"
}
},
"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 12 months\n",
"ramp-adjusted licences by year: {2026: '$0K', 2027: '$3.2M', 2028: '$3.2M'}\n",
"base PS + training (incl. migration + WFM): $2.6M, year 1 only\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() # verbatim $2.4M PS + $167K training, year 1\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\"base PS + training (incl. migration + WFM): {money(ps_by_year[2026])}, year 1 only\")\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-08T12:20:32.823759Z",
"iopub.status.busy": "2026-07-08T12:20:32.823547Z",
"iopub.status.idle": "2026-07-08T12:20:32.868665Z",
"shell.execute_reply": "2026-07-08T12:20:32.867804Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"WFM benefits by year: {2026: '$0K', 2027: '$108K', 2028: '$2.2M'} → 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>107,692</td>\n",
" <td>1,292,308</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>107,692</td>\n",
" <td>2,206,308</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 107,692 1,292,308 1,400,000\n",
"ASIA 0 0 914,000 914,000\n",
"TOTAL 0 107,692 2,206,308 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-08T12:20:32.871525Z",
"iopub.status.busy": "2026-07-08T12:20:32.871295Z",
"iopub.status.idle": "2026-07-08T12:20:34.302766Z",
"shell.execute_reply": "2026-07-08T12:20:34.302073Z"
}
},
"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,
107692.30769230769,
1292307.6923076923
]
},
{
"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": "AAAAAAAAAADsxE7sxEr6QP////+Hp0FB",
"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>$108K</b>",
"x": 1,
"y": 107692.30769230769,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.2M</b>",
"x": 2,
"y": 2206307.692307692,
"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, base PS, double-billing\n",
"\n",
"Three lines only. 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",
"two contract mechanics it ignored: the **ramp credit** (licences bill from month 13)\n",
"and the **term-contract run-off** (double-billing until 31 Dec 2027)."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "ccde902f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-08T12:20:34.305733Z",
"iopub.status.busy": "2026-07-08T12:20:34.305440Z",
"iopub.status.idle": "2026-07-08T12:20:34.317837Z",
"shell.execute_reply": "2026-07-08T12:20:34.317271Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Migration programme cost: {2026: '$9.9M', 2027: '$10.5M', 2028: '$3.2M'} → 3-yr $23.6M\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>0</td>\n",
" <td>3,200,000</td>\n",
" <td>3,200,000</td>\n",
" <td>6,400,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Base PS + training (incl. migration + WFM)</th>\n",
" <td>2,567,000</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>2,567,000</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>9,867,001</td>\n",
" <td>10,500,001</td>\n",
" <td>3,200,000</td>\n",
" <td>23,567,002</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" 2026 2027 \\\n",
"CCaaS platform licences (contracted, ramp-adjus... 0 3,200,000 \n",
"Base PS + training (incl. migration + WFM) 2,567,000 0 \n",
"Existing platform (term-contract run-off) 7,300,001 7,300,001 \n",
"TOTAL 9,867,001 10,500,001 \n",
"\n",
" 2028 3-yr \n",
"CCaaS platform licences (contracted, ramp-adjus... 3,200,000 6,400,000 \n",
"Base PS + training (incl. migration + WFM) 0 2,567,000 \n",
"Existing platform (term-contract run-off) 0 14,600,002 \n",
"TOTAL 3,200,000 23,567,002 "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"migration_costs = pd.DataFrame({\n",
" \"CCaaS platform licences (contracted, ramp-adjusted)\": licence_by_year,\n",
" \"Base PS + training (incl. migration + WFM)\": ps_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\"])]))"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c058e795",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-08T12:20:34.320002Z",
"iopub.status.busy": "2026-07-08T12:20:34.319861Z",
"iopub.status.idle": "2026-07-08T12:20:34.345827Z",
"shell.execute_reply": "2026-07-08T12:20:34.345186Z"
}
},
"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": [
0.0,
3200000.0,
3200000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Base PS + training (incl. migration + WFM)",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
2567000.0,
0.0,
0.0
]
},
{
"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": "AAAAIN/RYkEAAACgaWxzQQAAAKCpeXZB",
"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>$9.9M</b>",
"x": 0,
"y": 9867001.0,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$10.5M</b>",
"x": 1,
"y": 10500001.0,
"yshift": 12
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$3.2M</b>",
"x": 2,
"y": 3200000.0,
"yshift": 12
},
{
"font": {
"color": "#52514e",
"size": 12
},
"showarrow": false,
"text": "3-yr <b>&#36;23.6M</b> — &#36;1.7M above doing nothing",
"x": 2,
"xshift": -70,
"y": 23567002.0,
"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 12 months, then bill at the run-rate</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 12 months, then bill at the run-rate\", 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",
"At the **contracted licence rate ($3.2M/yr)** the case turns positive inside the deck's\n",
"own 20262028 window, with payback in late 2028. At the **deck's pitched $4.3M/yr** the\n",
"same case stays net-negative through 2028 — the KPI table shows both columns, so the walk\n",
"from the Genesys report to the contracted reality is explicit. Where a case doesn't pay\n",
"back in-window, the **run-rate breakeven** KPI extrapolates: steady state =\n",
"(baseline licence run-rate) + scoped WFM annual values.\n",
"\n",
"*Not modelled: early-termination fees, and migration costs beyond the base PS line.*"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "d8293152",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-08T12:20:34.347730Z",
"iopub.status.busy": "2026-07-08T12:20:34.347609Z",
"iopub.status.idle": "2026-07-08T12:20:34.352044Z",
"shell.execute_reply": "2026-07-08T12:20:34.351451Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "bb915be640904627a7e37f0597028fab",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "bb915be640904627a7e37f0597028fab",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.select.SelectWidget object at 0x7831aba912b0>"
]
},
"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-08T12:20:34.353609Z",
"iopub.status.busy": "2026-07-08T12:20:34.353505Z",
"iopub.status.idle": "2026-07-08T12:20:34.357689Z",
"shell.execute_reply": "2026-07-08T12:20:34.357172Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"net by year: {2026: '-$2.6M', 2027: '-$3.1M', 2028: '$6.3M'} → 3-yr $647K\n",
"steady-state run-rate saving $7.0M/yr ($4.1M licences + $2.9M WFM annuals) → breakeven 35 months (~Nov 2028)\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",
"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(wfm_annual_runrate(WFM_REGIONS))} WFM annuals) → breakeven {BREAKEVEN}\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "9d135a19",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-08T12:20:34.359533Z",
"iopub.status.busy": "2026-07-08T12:20:34.359414Z",
"iopub.status.idle": "2026-07-08T12:20:34.381700Z",
"shell.execute_reply": "2026-07-08T12:20:34.381025Z"
}
},
"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,
107692.30769230769,
2206307.692307692
]
},
{
"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": [
-2567001.0,
-3200001.0,
4100000.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 net",
"type": "scatter",
"x": [
"2026",
"2027",
"2028"
],
"y": {
"bdata": "AAAAgKyVQ8HsxE5sq5ZVwQAAAACsviNB",
"dtype": "f8"
}
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$2.6M</b>",
"x": 0,
"y": -2567001.0,
"yshift": -14
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$5.7M</b>",
"x": 1,
"y": -5659309.692307692,
"yshift": -14
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$647K</b>",
"x": 2,
"y": 646998.0,
"yshift": 14
},
{
"align": "left",
"bgcolor": "#fcfcfb",
"bordercolor": "#e1e0d9",
"borderwidth": 1,
"font": {
"color": "#52514e",
"size": 12
},
"showarrow": false,
"text": "3-yr net <b>&#36;647K</b> · ROI <b>39%</b><br>NPV@13.5% <b>-&#36;349K</b> · payback <b>35 months (~Nov 2028)</b><br>run-rate <b>&#36;7.0M/yr</b> post-window → breakeven <b>35 months (~Nov 2028)</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-08T12:20:34.384035Z",
"iopub.status.busy": "2026-07-08T12:20:34.383904Z",
"iopub.status.idle": "2026-07-08T12:20:34.393837Z",
"shell.execute_reply": "2026-07-08T12:20:34.393300Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The licence correction moves the 3-yr case by $2.2M vs the deck's pitched rate. 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 rate ($4.3M/yr)</th>\n",
" <th>Contracted ($3.2M/yr)</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>$3.9M</td>\n",
" <td>$1.7M</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3-yr net</th>\n",
" <td>-$1.6M</td>\n",
" <td>$647K</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ROI (net ÷ incremental cost)</th>\n",
" <td>-40%</td>\n",
" <td>39%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV @ 13.5% (deck rate)</th>\n",
" <td>-$2.0M</td>\n",
" <td>-$349K</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV @ 8.0% (CTM treasury)</th>\n",
" <td>-$1.8M</td>\n",
" <td>-$22K</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Payback (202628 window)</th>\n",
" <td>beyond 2028</td>\n",
" <td>35 months (~Nov 2028)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Steady-state run-rate saving</th>\n",
" <td>$5.9M/yr</td>\n",
" <td>$7.0M/yr</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Breakeven</th>\n",
" <td>40 months (~Apr 2029, extrapolated)</td>\n",
" <td>35 months (~Nov 2028)</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Deck rate ($4.3M/yr) \\\n",
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
"3-yr incremental cost $3.9M \n",
"3-yr net -$1.6M \n",
"ROI (net ÷ incremental cost) -40% \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 $5.9M/yr \n",
"Breakeven 40 months (~Apr 2029, extrapolated) \n",
"\n",
" Contracted ($3.2M/yr) \n",
"3-yr WFM benefits (verbatim, scoped) $2.3M \n",
"3-yr incremental cost $1.7M \n",
"3-yr net $647K \n",
"ROI (net ÷ incremental cost) 39% \n",
"NPV @ 13.5% (deck rate) -$349K \n",
"NPV @ 8.0% (CTM treasury) -$22K \n",
"Payback (202628 window) 35 months (~Nov 2028) \n",
"Steady-state run-rate saving $7.0M/yr \n",
"Breakeven 35 months (~Nov 2028) "
]
},
"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 same case at the deck's pitched rate.\n",
"_lic_deck = licence_costs_by_year(RAMP_MONTHS, TCO_VERBATIM[\"ccaas_annual\"])\n",
"total_deck = {y: current_by_year[y] + _lic_deck[y] + ps_by_year[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 rate ({money(TCO_VERBATIM['ccaas_annual'])}/yr)\":\n",
" kpi_display(kpi_deck, net_deck, RUNRATE_DECK, BREAKEVEN_DECK),\n",
" f\"Contracted ({money(LICENCE_ANNUAL)}/yr)\":\n",
" kpi_display(kpi, net_by, RUNRATE_SAVING, BREAKEVEN),\n",
"})\n",
"print(f\"The licence correction moves the 3-yr case by \"\n",
" f\"{money(kpi['net_3yr'] - kpi_deck['net_3yr'])} vs the deck's pitched rate. \"\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-08T12:20:34.395983Z",
"iopub.status.busy": "2026-07-08T12:20:34.395834Z",
"iopub.status.idle": "2026-07-08T12:20:34.432996Z",
"shell.execute_reply": "2026-07-08T12:20:34.432388Z"
}
},
"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": [
[
-2553002.0000000005,
-4353002.0,
-5853002.0
],
[
-953002.0000000005,
-2453002.0000000005,
-3703002.0000000005
],
[
646997.9999999995,
-553002.0000000005,
-1553002.0000000005
],
[
2246998.0,
1346997.9999999998,
596997.9999999998
],
[
3846997.9999999995,
3246997.9999999995,
2746997.9999999995
]
],
"zmid": 0
}
],
"layout": {
"annotations": [
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$2.6M</b>",
"x": 0,
"y": 0
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>-$4.4M</b>",
"x": 1,
"y": 0
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>-$5.9M</b>",
"x": 2,
"y": 0
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$953K</b>",
"x": 0,
"y": 1
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$2.5M</b>",
"x": 1,
"y": 1
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$3.7M</b>",
"x": 2,
"y": 1
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$647K</b>",
"x": 0,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$553K</b>",
"x": 1,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>-$1.6M</b>",
"x": 2,
"y": 2
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.2M</b>",
"x": 0,
"y": 3
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$1.3M</b>",
"x": 1,
"y": 3
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$597K</b>",
"x": 2,
"y": 3
},
{
"font": {
"color": "#ffffff",
"size": 12
},
"showarrow": false,
"text": "<b>$3.8M</b>",
"x": 0,
"y": 4
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$3.2M</b>",
"x": 1,
"y": 4
},
{
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "<b>$2.7M</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 · 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 (12-mo ramp × $3.2M/yr): $647K · deck corner (12-mo × $4.3M/yr): -$1.6M — the licence correction is what pulls the window positive.\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] 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 · WFM scope and contract dates held at \"\n",
" \"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 corner \"\n",
" f\"({DEFAULT_RAMP_MONTHS}-mo × {money(TCO_VERBATIM['ccaas_annual'])}/yr): \"\n",
" f\"{money(net_3yr(DEFAULT_RAMP_MONTHS, TCO_VERBATIM['ccaas_annual']))} — \"\n",
" f\"the licence correction is what pulls the window positive.\")"
]
},
{
"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-08T12:20:34.435111Z",
"iopub.status.busy": "2026-07-08T12:20:34.434996Z",
"iopub.status.idle": "2026-07-08T12:20:34.448521Z",
"shell.execute_reply": "2026-07-08T12:20:34.447923Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All assertions passed.\n",
" WFM benefits 3-yr $2.3M · programme cost 3-yr $23.6M · net 3-yr $647K · breakeven 35 months (~Nov 2028)\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",
"_l12 = licence_costs_by_year(12)\n",
"_approx(_l12[2026], 0)\n",
"_approx(_l12[2027], 4_300_000)\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",
"# 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 / 13, tol=1) # ANZ: 1 of 13 live months in 2027\n",
"_approx(wfm_annual_runrate(), 2_900_000)\n",
"_approx(runrate_saving_annual(), 5_900_000)\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(12, tco(\"ccaas_annual\"))\n",
"_approx(_lc[2027], 3_200_000)\n",
"_approx(runrate_saving_annual(tco(\"ccaas_annual\")), 7_000_000)\n",
"\n",
"# Section 5 — default-flow pins (pure engine defaults, no widget state)\n",
"_cur_d = current_costs_by_year(_seed_check)\n",
"_ps_d = ps_costs_by_year()\n",
"_tot_d = {y: _cur_d[y] + _l12[y] + _ps_d[y] for y in YEARS}\n",
"_ben_d = {y: float(_by_d.get(y, 0.0)) for y in YEARS}\n",
"_inc_d, _net_d = case_flows(_tot_d, _ben_d)\n",
"_approx(sum(_net_d.values()), -1_553_000, tol=1) # $2.314M WFM $3.867M incremental\n",
"assert runrate_breakeven_label(_net_d, runrate_saving_annual()) == \\\n",
" \"40 months (~Apr 2029, extrapolated)\"\n",
"_tot_c = {y: _cur_d[y] + _lc[y] + _ps_d[y] for y in YEARS}\n",
"_inc_c, _net_c = case_flows(_tot_c, _ben_d)\n",
"_approx(sum(_net_c.values()), 647_000, tol=1) # contracted rate → positive in-window\n",
"assert runrate_breakeven_label(_net_c, runrate_saving_annual(tco(\"ccaas_annual\"))) == \\\n",
" \"35 months (~Nov 2028)\"\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()) + ps_by_year[2026] + 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}\")"
]
},
{
"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 contracted licence rate is what makes the migration stand on its own**: at the\n",
" signed $3.2M/yr the case is positive in-window (payback ~Nov 2028); at the deck's\n",
" pitched $4.3M/yr it would run net $1.6M through 2028, breaking even ~April 2029 on\n",
" run-rate alone. Keep both KPI columns in front of CTM — the walk from the deck's own\n",
" numbers is the credibility story. (With-AI case: the Corrected 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",
"- Not modelled: early-termination fees, co-term options, migration costs beyond the base\n",
" PS line, 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 $2.4M PS really include the NA WFM migration\n",
" 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-08T12:20:34.450522Z",
"iopub.status.busy": "2026-07-08T12:20:34.450397Z",
"iopub.status.idle": "2026-07-08T12:20:34.473012Z",
"shell.execute_reply": "2026-07-08T12:20:34.472367Z"
}
},
"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 | 107,692 | 1,292,308 |\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) | 0 | 3,200,000 | 3,200,000 | 6,400,000 |\n",
"| Base PS + training (incl. migration + WFM) | 2,567,000 | 0 | 0 | 2,567,000 |\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 | 9,867,001 | 10,500,001 | 3,200,000 |\n",
"| incremental cost | 2,567,001 | 3,200,001 | -4,100,000 |\n",
"| net | -2,567,001 | -3,092,309 | 6,306,308 |\n",
"\n",
"#### KPIs — migration + WFM (no AI)\n",
"\n",
"| | Deck rate ($4.3M/yr) | Contracted ($3.2M/yr) |\n",
"|:-------------------------------------|:------------------------------------|:------------------------|\n",
"| 3-yr WFM benefits (verbatim, scoped) | $2.3M | $2.3M |\n",
"| 3-yr incremental cost | $3.9M | $1.7M |\n",
"| 3-yr net | -$1.6M | $647K |\n",
"| ROI (net ÷ incremental cost) | -40% | 39% |\n",
"| NPV @ 13.5% (deck rate) | -$2.0M | -$349K |\n",
"| NPV @ 8.0% (CTM treasury) | -$1.8M | -$22K |\n",
"| Payback (202628 window) | beyond 2028 | 35 months (~Nov 2028) |\n",
"| Steady-state run-rate saving | $5.9M/yr | $7.0M/yr |\n",
"| Breakeven | 40 months (~Apr 2029, extrapolated) | 35 months (~Nov 2028) |\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\": 107692,\n",
" \"2028\": 2206308\n",
" },\n",
" \"cost_by_year\": {\n",
" \"2026\": 9867001,\n",
" \"2027\": 10500001,\n",
" \"2028\": 3200000\n",
" },\n",
" \"net_by_year\": {\n",
" \"2026\": -2567001,\n",
" \"2027\": -3092309,\n",
" \"2028\": 6306308\n",
" },\n",
" \"kpis\": {\n",
" \"benefits_3yr\": 2314000,\n",
" \"incremental_cost_3yr\": 1667002,\n",
" \"net_3yr\": 646998,\n",
" \"roi\": 0.3881,\n",
" \"npv\": -349036,\n",
" \"discount_rate\": 0.135,\n",
" \"payback\": \"35 months (~Nov 2028)\"\n",
" },\n",
" \"kpis_at_deck_rate\": {\n",
" \"benefits_3yr\": 2314000,\n",
" \"incremental_cost_3yr\": 3867002,\n",
" \"net_3yr\": -1553002,\n",
" \"roi\": -0.4016,\n",
" \"npv\": -1955248,\n",
" \"discount_rate\": 0.135,\n",
" \"payback\": \"beyond 2028\"\n",
" },\n",
" \"runrate_saving_annual\": 7000000,\n",
" \"breakeven\": \"35 months (~Nov 2028)\",\n",
" \"assumptions\": {\n",
" \"ramp_months\": 12,\n",
" \"licence_annual\": 3200000,\n",
" \"deck_ccaas_annual\": 4300000,\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",
" \"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": {
"12974d31f88c40d99967fc5d44b9e2a4": {
"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
}
},
"1beaba7af87e4528abd06da3151e882f": {
"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
}
},
"1c591bdaa1e44a55be8d24db8956b3d1": {
"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_9d4f1c2f10084d87bbfb422c3247f7e1",
"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
}
},
"27e2171a593043338205c5b352f20f3d": {
"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_7c01b290a6594aa69680bdd3a4e74995",
"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
}
},
"2a43f7bc543d446ead3eeaaadbbf3cea": {
"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_45fdc31c02434d799c78a20846c791dc",
"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"
}
},
"2bf25ab141c646aeae3b8e9b80b6762d": {
"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_92aaf404ceee451f84aeb8f3252558e0",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"2f3ac4164f5a4454ba631d0df39e5d87": {
"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_9a295ac0af8348ebb661f2a0a9be8d17",
"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"
}
},
"3208166b83d6414c93131c6d9a6b1dac": {
"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
}
},
"3446e8dd427b4abe8d361ec75bb0a9fa": {
"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_f9673c814e8148b1be991bcf12f12fff",
"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": 12.0
}
},
"3b92df6e04544844a57103b779776678": {
"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
}
},
"4275d017a33a4f7d87a8ba63fca41b03": {
"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
}
},
"45fdc31c02434d799c78a20846c791dc": {
"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
}
},
"60a9596904a34374bc605974ac3a3235": {
"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_ed3779c8295a44339d7441a254909d33",
"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"
}
},
"6b1f795e977941baa1ca60f5f93bb458": {
"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
}
},
"6bf6331dbeea4a2e932d9ba01fd28557": {
"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_3b92df6e04544844a57103b779776678",
"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
}
},
"78688c06ca0b4ee19c1a0b81f18d628a": {
"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_82f7314d49cb445481ab1f5a82fd31d6",
"layout_path": null,
"placeholder": "",
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"style": "IPY_MODEL_4275d017a33a4f7d87a8ba63fca41b03",
"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>"
}
},
"7c01b290a6594aa69680bdd3a4e74995": {
"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
}
},
"82f7314d49cb445481ab1f5a82fd31d6": {
"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
}
},
"8b8b8d01f0ca46f39c16bd16b93a1475": {
"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_6b1f795e977941baa1ca60f5f93bb458",
"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"
}
},
"92aaf404ceee451f84aeb8f3252558e0": {
"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
}
},
"9a295ac0af8348ebb661f2a0a9be8d17": {
"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
}
},
"9d4f1c2f10084d87bbfb422c3247f7e1": {
"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
}
},
"9ee437efc7884fe9a1f870a0aead3e90": {
"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
}
},
"9f85cf3f71b842ba83ed70bd17ea748f": {
"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_3208166b83d6414c93131c6d9a6b1dac",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": false
}
},
"a3797b4763c8482f82c23bea6b27de8f": {
"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
}
},
"bae439b7a2d04ab29390996726534753": {
"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_12974d31f88c40d99967fc5d44b9e2a4",
"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
}
},
"bb915be640904627a7e37f0597028fab": {
"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_9ee437efc7884fe9a1f870a0aead3e90",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "13.5% (deck)"
}
},
"c76324ffcf3e4df5a078d6b3fb6ac13c": {
"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
}
},
"caae1a34df0945249b1a3d39320540b1": {
"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_c76324ffcf3e4df5a078d6b3fb6ac13c",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"ed3779c8295a44339d7441a254909d33": {
"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
}
},
"f7b01a3662ce4ee4a8396fa5df4f048f": {
"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_a3797b4763c8482f82c23bea6b27de8f",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": true
}
},
"f8196ba7d89c487eb23ad614f78881fe": {
"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_1beaba7af87e4528abd06da3151e882f",
"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
}
},
"f9673c814e8148b1be991bcf12f12fff": {
"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
}
}
},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}