"
]
},
"metadata": {},
@@ -437,6 +459,10 @@
"_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",
+ "_licence_w = mr.NumberInput(\n",
+ " label=\"CCaaS licence run-rate ($/yr) — contracted\",\n",
+ " value=round(tco(\"ccaas_annual\")), # signed; deck pitched $4.3M/yr\n",
+ " min=0, max=10_000_000, step=100_000, position=\"inline\")\n",
"\n",
"sites = list(CTM_DEFAULT_SITES)\n",
"ALL_SITES = [s.site_name for s in sites]\n",
@@ -468,10 +494,10 @@
"id": "52d1e89f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.182019Z",
- "iopub.status.busy": "2026-07-08T09:51:40.181871Z",
- "iopub.status.idle": "2026-07-08T09:51:40.201335Z",
- "shell.execute_reply": "2026-07-08T09:51:40.200480Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.368197Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.367738Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.398478Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.397723Z"
}
},
"outputs": [
@@ -481,7 +507,8 @@
"text": [
"current-state total $7.3M/yr (seed: $7.3M; deck 3-yr $22.0M — deck rounding)\n",
"deck CCaaS 3-yr (no ramp) = $15.5M (deck: $15.4M)\n",
- "ramp-adjusted licences by year: {2026: '$0K', 2027: '$4.3M', 2028: '$4.3M'}\n"
+ "licence run-rate: contracted $3.2M/yr (deck pitched $4.3M/yr — as-pitched stack keeps it)\n",
+ "ramp-adjusted licences by year: {2026: '$0K', 2027: '$3.2M', 2028: '$3.2M'}\n"
]
},
{
@@ -619,16 +646,21 @@
" \n",
" \n",
" | 3 | \n",
+ " Contracted licence run-rate | \n",
+ " 🟢 $3.2M/yr signed (deck pitched $4.3M/yr) | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
" Contracted token rate (vs $1.00 US list) | \n",
" 🔴 not sourced — list rate used | \n",
"
\n",
" \n",
- " | 4 | \n",
+ " 5 | \n",
" Non-NAM site volumes & AHTs | \n",
" 🟡 tokencalc placeholders (defaults.py warning) | \n",
"
\n",
" \n",
- " | 5 | \n",
+ " 6 | \n",
" Early-termination / co-term options on NICE IE... | \n",
" 🔴 unknown | \n",
"
\n",
@@ -641,17 +673,19 @@
"0 Per-region current-platform contract values \n",
"1 Actual termination / renewal dates per region \n",
"2 Genesys ramp period in the actual order form \n",
- "3 Contracted token rate (vs $1.00 US list) \n",
- "4 Non-NAM site volumes & AHTs \n",
- "5 Early-termination / co-term options on NICE IE... \n",
+ "3 Contracted licence run-rate \n",
+ "4 Contracted token rate (vs $1.00 US list) \n",
+ "5 Non-NAM site volumes & AHTs \n",
+ "6 Early-termination / co-term options on NICE IE... \n",
"\n",
" status \n",
"0 🟡 seeded by agent share above \n",
"1 🟡 defaulted to 31 Dec 2027 \n",
"2 🟡 12 months assumed \n",
- "3 🔴 not sourced — list rate used \n",
- "4 🟡 tokencalc placeholders (defaults.py warning) \n",
- "5 🔴 unknown "
+ "3 🟢 $3.2M/yr signed (deck pitched $4.3M/yr) \n",
+ "4 🔴 not sourced — list rate used \n",
+ "5 🟡 tokencalc placeholders (defaults.py warning) \n",
+ "6 🔴 unknown "
]
},
"metadata": {},
@@ -661,6 +695,7 @@
"source": [
"# ── Contract values → model (re-runs on any change to the widgets above) ─\n",
"RAMP_MONTHS = int(_ramp_w.value)\n",
+ "LICENCE_ANNUAL = float(_licence_w.value)\n",
"\n",
"current_state = _seed.copy()\n",
"for _r in REGIONS:\n",
@@ -669,15 +704,17 @@
" _term_w[_r].value)\n",
"\n",
"current_by_year = current_costs_by_year(current_state)\n",
- "licence_by_year = licence_costs_by_year(RAMP_MONTHS)\n",
+ "licence_by_year = licence_costs_by_year(RAMP_MONTHS, LICENCE_ANNUAL)\n",
"\n",
- "print(f\"current-state total {money(current_state['annual_cost'].sum())}/yr \"\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",
- "print(f\"deck CCaaS 3-yr (no ramp) = \"\n",
+ "backstage(f\"deck CCaaS 3-yr (no ramp) = \"\n",
" f\"{money(3 * TCO_VERBATIM['ccaas_annual'] + TCO_VERBATIM['prof_services_y1'] + TCO_VERBATIM['training_y1'])} \"\n",
" f\"(deck: {money(TCO_VERBATIM['ccaas_3yr'])})\")\n",
- "print(f\"ramp-adjusted licences by year: \"\n",
+ "print(f\"licence run-rate: contracted {money(LICENCE_ANNUAL)}/yr \"\n",
+ " f\"(deck pitched {money(TCO_VERBATIM['ccaas_annual'])}/yr — as-pitched stack keeps it)\")\n",
+ "backstage(f\"ramp-adjusted licences by year: \"\n",
" f\"{ {y: money(v) for y, v in licence_by_year.items()} }\")\n",
"display(current_state)\n",
"\n",
@@ -686,6 +723,7 @@
" (\"Per-region current-platform contract values\", \"🟡 seeded by agent share above\"),\n",
" (\"Actual termination / renewal dates per region\", \"🟡 defaulted to 31 Dec 2027\"),\n",
" (\"Genesys ramp period in the actual order form\", \"🟡 12 months assumed\"),\n",
+ " (\"Contracted licence run-rate\", \"🟢 $3.2M/yr signed (deck pitched $4.3M/yr)\"),\n",
" (\"Contracted token rate (vs $1.00 US list)\", \"🔴 not sourced — list rate used\"),\n",
" (\"Non-NAM site volumes & AHTs\", \"🟡 tokencalc placeholders (defaults.py warning)\"),\n",
" (\"Early-termination / co-term options on NICE IEX et al.\", \"🔴 unknown\"),\n",
@@ -721,10 +759,10 @@
"id": "c7e86e80",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.203381Z",
- "iopub.status.busy": "2026-07-08T09:51:40.203220Z",
- "iopub.status.idle": "2026-07-08T09:51:40.242509Z",
- "shell.execute_reply": "2026-07-08T09:51:40.241683Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.400716Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.400542Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.444021Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.437975Z"
}
},
"outputs": [
@@ -863,7 +901,7 @@
" assert abs(got - expect) <= _tol(expect), (c, got, expect)\n",
"assert abs(verbatim[\"three_yr\"].sum() - SLIDE_TOTALS[\"total_3yr\"]) <= _tol(15e6)\n",
"assert abs(verbatim[\"annual\"].sum() - SLIDE_TOTALS[\"total_annual\"]) <= _tol(13.6e6)\n",
- "print(f\"cross-foot OK — cell grain sums to {money(verbatim['three_yr'].sum())} 3-yr / \"\n",
+ "backstage(f\"cross-foot OK — cell grain sums to {money(verbatim['three_yr'].sum())} 3-yr / \"\n",
" f\"{money(verbatim['annual'].sum())} annual (deck headline: $15.0M / $13.6M — deck rounding)\")\n",
"\n",
"display(verbatim.pivot_table(index=\"capability\", columns=\"region\", values=\"three_yr\",\n",
@@ -877,27 +915,27 @@
"id": "5ace8c50",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.244670Z",
- "iopub.status.busy": "2026-07-08T09:51:40.244518Z",
- "iopub.status.idle": "2026-07-08T09:51:40.249368Z",
- "shell.execute_reply": "2026-07-08T09:51:40.248700Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.448792Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.448556Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.461582Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.459562Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
- "model_id": "98b5a8a698284978906b4a1d6229f869",
+ "model_id": "1ee1d47e07a841a8908db4d0ea1d3952",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "98b5a8a698284978906b4a1d6229f869",
+ "model_id": "1ee1d47e07a841a8908db4d0ea1d3952",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -917,10 +955,10 @@
"id": "fe3572d2",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.251808Z",
- "iopub.status.busy": "2026-07-08T09:51:40.251620Z",
- "iopub.status.idle": "2026-07-08T09:51:40.266075Z",
- "shell.execute_reply": "2026-07-08T09:51:40.265409Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.463946Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.463734Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.479705Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.478527Z"
}
},
"outputs": [
@@ -1072,7 +1110,7 @@
" for r in REGIONS\n",
"]).set_index(\"region\")\n",
"if NA_EMAIL_EARLY:\n",
- " print(f\"NA Email exception: implemented {month_label(NA_EMAIL_IMPL_MONTH)}, \"\n",
+ " backstage(f\"NA Email exception: implemented {month_label(NA_EMAIL_IMPL_MONTH)}, \"\n",
" f\"realizes {month_label(NA_EMAIL_IMPL_MONTH + BENEFIT_LAG_MONTHS)}\")\n",
"display(schedule)"
]
@@ -1083,10 +1121,10 @@
"id": "a06e90b9",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.268237Z",
- "iopub.status.busy": "2026-07-08T09:51:40.268094Z",
- "iopub.status.idle": "2026-07-08T09:51:40.353581Z",
- "shell.execute_reply": "2026-07-08T09:51:40.353106Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.482124Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.481856Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.584120Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.581742Z"
}
},
"outputs": [
@@ -1213,7 +1251,7 @@
" got = benefits_long.query(\"region == @region and capability == @cap\")[\"benefit\"].sum()\n",
" assert abs(got - three_yr) < 1e-6, (region, cap)\n",
"\n",
- "print(f\"benefits by year: { {y: money(v) for y, v in benefit_total_by_year.items()} }\")\n",
+ "backstage(f\"benefits by year: { {y: money(v) for y, v in benefit_total_by_year.items()} }\")\n",
"print(\"⚠ 2026 benefits are $0 — the honest consequence of the deck's own deployment schedule.\")\n",
"display(benefits_long.pivot_table(index=\"capability\", columns=\"year\", values=\"benefit\",\n",
" aggfunc=\"sum\", margins=True, margins_name=\"TOTAL\")\n",
@@ -1252,27 +1290,27 @@
"id": "977418e1",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.355687Z",
- "iopub.status.busy": "2026-07-08T09:51:40.355555Z",
- "iopub.status.idle": "2026-07-08T09:51:40.374165Z",
- "shell.execute_reply": "2026-07-08T09:51:40.373340Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.588145Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.587638Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.630228Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.628706Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
- "model_id": "745d27870c3f49bab0b544c35bcfd939",
+ "model_id": "2058a28b452f4885954eed2c21208812",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "745d27870c3f49bab0b544c35bcfd939",
+ "model_id": "2058a28b452f4885954eed2c21208812",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1281,17 +1319,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "45476efc0f5f4bd5a21da5df43beffac",
+ "model_id": "d3d9c76f75b34f688de66cca01913f38",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "45476efc0f5f4bd5a21da5df43beffac",
+ "model_id": "d3d9c76f75b34f688de66cca01913f38",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1300,17 +1338,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "382c80a20e064400997f826af1ea07b1",
+ "model_id": "536a9842fe6a41078505ef6772a81435",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "382c80a20e064400997f826af1ea07b1",
+ "model_id": "536a9842fe6a41078505ef6772a81435",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1319,17 +1357,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "bf31d1fd5233456b8920c5afa3f70dcf",
+ "model_id": "544d7fde83c442349a54af52de538da2",
"position": "sidebar",
"widget": "SliderWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "bf31d1fd5233456b8920c5afa3f70dcf",
+ "model_id": "544d7fde83c442349a54af52de538da2",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1338,17 +1376,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "befa1244f84249babfdc2a5dca694656",
+ "model_id": "43858d8fa5774cca84507ed5cf72138e",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "befa1244f84249babfdc2a5dca694656",
+ "model_id": "43858d8fa5774cca84507ed5cf72138e",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1386,10 +1424,10 @@
"id": "9767177f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.376460Z",
- "iopub.status.busy": "2026-07-08T09:51:40.376277Z",
- "iopub.status.idle": "2026-07-08T09:51:40.391170Z",
- "shell.execute_reply": "2026-07-08T09:51:40.390461Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.633813Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.633401Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.653551Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.652404Z"
}
},
"outputs": [
@@ -1539,10 +1577,10 @@
"id": "3f3102f4",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.393244Z",
- "iopub.status.busy": "2026-07-08T09:51:40.393021Z",
- "iopub.status.idle": "2026-07-08T09:51:40.446493Z",
- "shell.execute_reply": "2026-07-08T09:51:40.445600Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.658238Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.657989Z",
+ "iopub.status.idle": "2026-07-08T12:20:39.709180Z",
+ "shell.execute_reply": "2026-07-08T12:20:39.708683Z"
}
},
"outputs": [
@@ -1702,7 +1740,7 @@
"\n",
"conf = {f: METERS[f].confidence.icon for f in USED_FEATURES}\n",
"tokens_pivot.insert(0, \"conf\", [conf.get(i, \"🟢\") for i in tokens_pivot.index[:-1]] + [\"\"])\n",
- "print(f\"AI token consumption (missing cost #1): \"\n",
+ "backstage(f\"AI token consumption (missing cost #1): \"\n",
" f\"{ {y: money(v) for y, v in token_total_by_year.items()} } \"\n",
" f\"→ 3-yr {money(sum(token_total_by_year.values()))}\")\n",
"display(tokens_pivot)"
@@ -1714,10 +1752,10 @@
"id": "e62ecfa7",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:40.448420Z",
- "iopub.status.busy": "2026-07-08T09:51:40.448204Z",
- "iopub.status.idle": "2026-07-08T09:51:41.774729Z",
- "shell.execute_reply": "2026-07-08T09:51:41.774078Z"
+ "iopub.execute_input": "2026-07-08T12:20:39.710735Z",
+ "iopub.status.busy": "2026-07-08T12:20:39.710622Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.026234Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.025568Z"
}
},
"outputs": [
@@ -2767,27 +2805,27 @@
"id": "3b395fbd",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.777077Z",
- "iopub.status.busy": "2026-07-08T09:51:41.776864Z",
- "iopub.status.idle": "2026-07-08T09:51:41.787040Z",
- "shell.execute_reply": "2026-07-08T09:51:41.786303Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.030988Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.030695Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.043978Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.042581Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
- "model_id": "76bcbbb0475747a190b161ee64d1f295",
+ "model_id": "c3066f4048b84495a1956bd5af19302f",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "76bcbbb0475747a190b161ee64d1f295",
+ "model_id": "c3066f4048b84495a1956bd5af19302f",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -2796,17 +2834,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "385dfb4c6e4e4bcfb29506e057a6d048",
+ "model_id": "a2ae0d29f75c46d195430ab5c30cdfd6",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "385dfb4c6e4e4bcfb29506e057a6d048",
+ "model_id": "a2ae0d29f75c46d195430ab5c30cdfd6",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -2815,17 +2853,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "80daf3a6ecc340e98ccf5c5a1c1daa0c",
+ "model_id": "58a2284801644c5c994a6779ba7b84b8",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "80daf3a6ecc340e98ccf5c5a1c1daa0c",
+ "model_id": "58a2284801644c5c994a6779ba7b84b8",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -2852,10 +2890,10 @@
"id": "23756537",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.789254Z",
- "iopub.status.busy": "2026-07-08T09:51:41.789059Z",
- "iopub.status.idle": "2026-07-08T09:51:41.814283Z",
- "shell.execute_reply": "2026-07-08T09:51:41.813619Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.046343Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.046114Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.075431Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.074560Z"
}
},
"outputs": [
@@ -3159,10 +3197,10 @@
"id": "1ffaa74a",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.818477Z",
- "iopub.status.busy": "2026-07-08T09:51:41.818253Z",
- "iopub.status.idle": "2026-07-08T09:51:41.823221Z",
- "shell.execute_reply": "2026-07-08T09:51:41.822490Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.077838Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.077644Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.082296Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.081756Z"
}
},
"outputs": [
@@ -3186,10 +3224,10 @@
" else \"FLAG — under-modelled by the industry benchmark\")\n",
"print(f\"AI implementation {money(_impl_total)} ÷ benefit claim \"\n",
" f\"{money(SLIDE_TOTALS['total_3yr'])} = {_ratio:.1%} → {_verdict}\")\n",
- "print(\"Industry benchmark: 20-40% of the Y1 benefit claim goes to implementation.\")\n",
- "print(\"Reading: V2 deliberately strips vendor-services inflation from V1 (which sat at ~31%).\")\n",
- "print(\"If the corrected case still looks good at V2 hours, it is robust to the higher estimate;\")\n",
- "print(\"Section 9 sweeps hours × rate to show exactly how much the conclusion depends on this line.\")"
+ "backstage(\"Industry benchmark: 20-40% of the Y1 benefit claim goes to implementation.\")\n",
+ "backstage(\"Reading: V2 deliberately strips vendor-services inflation from V1 (which sat at ~31%).\")\n",
+ "backstage(\"If the corrected case still looks good at V2 hours, it is robust to the higher estimate;\")\n",
+ "backstage(\"Section 9 sweeps hours × rate to show exactly how much the conclusion depends on this line.\")"
]
},
{
@@ -3212,10 +3250,10 @@
"id": "ad880bda",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.825105Z",
- "iopub.status.busy": "2026-07-08T09:51:41.824928Z",
- "iopub.status.idle": "2026-07-08T09:51:41.847391Z",
- "shell.execute_reply": "2026-07-08T09:51:41.846766Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.084449Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.084277Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.108089Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.107342Z"
}
},
"outputs": [
@@ -3223,7 +3261,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Corrected programme cost: {2026: '$10.8M', 2027: '$13.5M', 2028: '$7.8M'} → 3-yr $32.1M\n",
+ "Corrected programme cost: {2026: '$10.8M', 2027: '$12.4M', 2028: '$6.7M'} → 3-yr $29.9M\n",
"As pitched (deck): {2026: '$6.9M', 2027: '$4.3M', 2028: '$4.3M'} → 3-yr $15.5M (deck says $15.4M — rounding)\n"
]
},
@@ -3256,11 +3294,11 @@
" \n",
" \n",
" \n",
- " | CCaaS platform licences (ramp-adjusted) | \n",
+ " CCaaS platform licences (contracted, ramp-adjusted) | \n",
" 0 | \n",
- " 4,300,000 | \n",
- " 4,300,000 | \n",
- " 8,600,000 | \n",
+ " 3,200,000 | \n",
+ " 3,200,000 | \n",
+ " 6,400,000 | \n",
"
\n",
" \n",
" | Base professional services + training | \n",
@@ -3300,32 +3338,32 @@
"
\n",
" | TOTAL — corrected | \n",
" 10,790,218 | \n",
- " 13,515,925 | \n",
- " 7,838,449 | \n",
- " 32,144,591 | \n",
+ " 12,415,925 | \n",
+ " 6,738,449 | \n",
+ " 29,944,591 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " 2026 2027 2028 \\\n",
- "CCaaS platform licences (ramp-adjusted) 0 4,300,000 4,300,000 \n",
- "Base professional services + training 2,567,000 0 0 \n",
- "Existing platform (term-contract run-off) 7,300,001 7,300,001 0 \n",
- "AI token consumption 0 1,176,183 3,345,156 \n",
- "AI implementation + KB readiness 923,217 582,241 35,793 \n",
- "AI steady-state tuning 0 157,500 157,500 \n",
- "TOTAL — corrected 10,790,218 13,515,925 7,838,449 \n",
+ " 2026 2027 \\\n",
+ "CCaaS platform licences (contracted, ramp-adjus... 0 3,200,000 \n",
+ "Base professional services + training 2,567,000 0 \n",
+ "Existing platform (term-contract run-off) 7,300,001 7,300,001 \n",
+ "AI token consumption 0 1,176,183 \n",
+ "AI implementation + KB readiness 923,217 582,241 \n",
+ "AI steady-state tuning 0 157,500 \n",
+ "TOTAL — corrected 10,790,218 12,415,925 \n",
"\n",
- " 3-yr \n",
- "CCaaS platform licences (ramp-adjusted) 8,600,000 \n",
- "Base professional services + training 2,567,000 \n",
- "Existing platform (term-contract run-off) 14,600,002 \n",
- "AI token consumption 4,521,339 \n",
- "AI implementation + KB readiness 1,541,250 \n",
- "AI steady-state tuning 315,000 \n",
- "TOTAL — corrected 32,144,591 "
+ " 2028 3-yr \n",
+ "CCaaS platform licences (contracted, ramp-adjus... 3,200,000 6,400,000 \n",
+ "Base professional services + training 0 2,567,000 \n",
+ "Existing platform (term-contract run-off) 0 14,600,002 \n",
+ "AI token consumption 3,345,156 4,521,339 \n",
+ "AI implementation + KB readiness 35,793 1,541,250 \n",
+ "AI steady-state tuning 157,500 315,000 \n",
+ "TOTAL — corrected 6,738,449 29,944,591 "
]
},
"metadata": {},
@@ -3405,7 +3443,7 @@
"impl_kb_by_year = {y: impl_by_year[y] + kb_by_year[y] for y in YEARS}\n",
"\n",
"corrected_costs = pd.DataFrame({\n",
- " \"CCaaS platform licences (ramp-adjusted)\": licence_by_year,\n",
+ " \"CCaaS platform licences (contracted, ramp-adjusted)\": licence_by_year,\n",
" \"Base professional services + training\": ps_by_year,\n",
" \"Existing platform (term-contract run-off)\": current_by_year,\n",
" \"AI token consumption\": token_total_by_year,\n",
@@ -3423,10 +3461,10 @@
"pitched_total_by_year = {y: float(pitched_costs[y].sum()) for y in YEARS}\n",
"\n",
"kb_note = \"\" if INCLUDE_KB_READINESS else \" (KB readiness excluded)\"\n",
- "print(f\"Corrected programme cost{kb_note}: \"\n",
+ "backstage(f\"Corrected programme cost{kb_note}: \"\n",
" f\"{ {y: money(v) for y, v in corrected_total_by_year.items()} } \"\n",
" f\"→ 3-yr {money(sum(corrected_total_by_year.values()))}\")\n",
- "print(f\"As pitched (deck): { {y: money(v) for y, v in pitched_total_by_year.items()} } \"\n",
+ "backstage(f\"As pitched (deck): { {y: money(v) for y, v in pitched_total_by_year.items()} } \"\n",
" f\"→ 3-yr {money(sum(pitched_total_by_year.values()))} (deck says $15.4M — rounding)\")\n",
"display(pd.concat([corrected_costs,\n",
" pd.DataFrame(corrected_costs.sum()).T.set_axis([\"TOTAL — corrected\"])]))\n",
@@ -3450,10 +3488,10 @@
"id": "2e2b207c",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.851171Z",
- "iopub.status.busy": "2026-07-08T09:51:41.851015Z",
- "iopub.status.idle": "2026-07-08T09:51:41.929381Z",
- "shell.execute_reply": "2026-07-08T09:51:41.928512Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.110284Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.110060Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.215614Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.214675Z"
}
},
"outputs": [
@@ -4549,10 +4587,10 @@
"id": "c2c9418f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.931256Z",
- "iopub.status.busy": "2026-07-08T09:51:41.931080Z",
- "iopub.status.idle": "2026-07-08T09:51:41.959011Z",
- "shell.execute_reply": "2026-07-08T09:51:41.958194Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.218474Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.218230Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.257013Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.256102Z"
}
},
"outputs": [
@@ -4572,7 +4610,7 @@
"width": 2
}
},
- "name": "CCaaS platform licences (ramp-adjusted)",
+ "name": "CCaaS platform licences (contracted, ramp-adjusted)",
"type": "bar",
"x": [
"2026",
@@ -4581,8 +4619,8 @@
],
"y": [
0.0,
- 4300000.0,
- 4300000.0
+ 3200000.0,
+ 3200000.0
]
},
{
@@ -4717,7 +4755,7 @@
"2028"
],
"y": {
- "bdata": "/1aBNqmUZEHYbpbnHS53QQAAAPDMp35B",
+ "bdata": "/1aBNqmUZEHYbpbnjyF2QQAAAPCwjnxB",
"dtype": "f8"
}
},
@@ -4768,9 +4806,9 @@
"size": 12
},
"showarrow": false,
- "text": "$13.5M",
+ "text": "$12.4M",
"x": 1,
- "y": 13515924.770938251,
+ "y": 12415924.770938251,
"yshift": 12
},
{
@@ -4779,9 +4817,9 @@
"size": 12
},
"showarrow": false,
- "text": "$7.8M",
+ "text": "$6.7M",
"x": 2,
- "y": 7838448.525773196,
+ "y": 6738448.525773196,
"yshift": 12
},
{
@@ -4790,10 +4828,10 @@
"size": 12
},
"showarrow": false,
- "text": "3-yr $32.1M — $16.7M above the pitch",
+ "text": "3-yr $29.9M — $14.5M above the pitch",
"x": 2,
"xshift": -70,
- "y": 32144591.0,
+ "y": 29944591.0,
"yshift": 18
}
],
@@ -5605,7 +5643,7 @@
"color": "#0b0b0b",
"size": 16
},
- "text": "Programme cost over 3 years — with the missed costs
Existing platforms bill until 31 Dec 2027 term-contract end (double-billing) · licences ramp-free for 12 months · tokens + AI implementation added",
+ "text": "Programme cost over 3 years — with the missed costs
Existing platforms bill until 31 Dec 2027 term-contract end (double-billing) · licences at the contracted run-rate, ramp-free for 12 months · tokens + AI implementation added",
"x": 0.02,
"xanchor": "left"
},
@@ -5653,7 +5691,7 @@
"fig.update_layout(barmode=\"stack\")\n",
"tei_layout(fig, \"Programme cost over 3 years — with the missed costs\",\n",
" \"Existing platforms bill until 31 Dec 2027 term-contract end (double-billing) · \"\n",
- " \"licences ramp-free for 12 months · tokens + AI implementation added\", height=500)\n",
+ " \"licences at the contracted run-rate, ramp-free for 12 months · tokens + AI implementation added\", height=500)\n",
"fig.show()"
]
},
@@ -5663,10 +5701,10 @@
"id": "82335f50",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.961536Z",
- "iopub.status.busy": "2026-07-08T09:51:41.961313Z",
- "iopub.status.idle": "2026-07-08T09:51:41.987870Z",
- "shell.execute_reply": "2026-07-08T09:51:41.986961Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.259920Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.259677Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.287540Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.286646Z"
}
},
"outputs": [
@@ -5717,8 +5755,8 @@
],
"y": [
10790217.703288553,
- 13515924.770938251,
- 7838448.525773196
+ 12415924.770938251,
+ 6738448.525773196
]
}
],
@@ -5742,10 +5780,10 @@
"size": 12
},
"showarrow": false,
- "text": "+$9.2M",
+ "text": "+$8.1M",
"x": 1,
"xshift": 16,
- "y": 13515924.770938251,
+ "y": 12415924.770938251,
"yshift": 12
},
{
@@ -5754,10 +5792,10 @@
"size": 12
},
"showarrow": false,
- "text": "+$3.5M",
+ "text": "+$2.4M",
"x": 2,
"xshift": 16,
- "y": 7838448.525773196,
+ "y": 6738448.525773196,
"yshift": 12
}
],
@@ -6637,27 +6675,27 @@
"id": "de7679d8",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:41.990670Z",
- "iopub.status.busy": "2026-07-08T09:51:41.990415Z",
- "iopub.status.idle": "2026-07-08T09:51:41.997557Z",
- "shell.execute_reply": "2026-07-08T09:51:41.996938Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.289983Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.289757Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.296966Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.296103Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
- "model_id": "d26e504d6ea9473494f410c336b26643",
+ "model_id": "0a76099b436240728b539f8867340625",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "d26e504d6ea9473494f410c336b26643",
+ "model_id": "0a76099b436240728b539f8867340625",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -6678,10 +6716,10 @@
"id": "08433116",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.000177Z",
- "iopub.status.busy": "2026-07-08T09:51:41.999927Z",
- "iopub.status.idle": "2026-07-08T09:51:42.006870Z",
- "shell.execute_reply": "2026-07-08T09:51:42.005929Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.298855Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.298655Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.304752Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.303928Z"
}
},
"outputs": [
@@ -6689,7 +6727,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "corrected: net by year {2026: '-$3.5M', 2027: '-$3.6M', 2028: '$11.9M'}\n",
+ "corrected: net by year {2026: '-$3.5M', 2027: '-$2.5M', 2028: '$13.0M'}\n",
"pitched: net by year {2026: '$433K', 2027: '$5.6M', 2028: '$15.4M'}\n"
]
}
@@ -6709,8 +6747,8 @@
"\n",
"def fmt_roi(k):\n",
" return f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net cost saving\"\n",
- "print(f\"corrected: net by year { {y: money(v) for y, v in net_corrected.items()} }\")\n",
- "print(f\"pitched: net by year { {y: money(v) for y, v in net_pitched.items()} }\")"
+ "backstage(f\"corrected: net by year { {y: money(v) for y, v in net_corrected.items()} }\")\n",
+ "backstage(f\"pitched: net by year { {y: money(v) for y, v in net_pitched.items()} }\")"
]
},
{
@@ -6719,10 +6757,10 @@
"id": "04c43658",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.008680Z",
- "iopub.status.busy": "2026-07-08T09:51:42.008535Z",
- "iopub.status.idle": "2026-07-08T09:51:42.033033Z",
- "shell.execute_reply": "2026-07-08T09:51:42.032368Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.306676Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.306474Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.337304Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.336449Z"
}
},
"outputs": [
@@ -6773,8 +6811,8 @@
],
"y": [
-3490217.7032885533,
- -6215924.770938251,
- -538448.5257731955
+ -5115924.770938251,
+ 561551.4742268045
]
},
{
@@ -6799,7 +6837,7 @@
"2028"
],
"y": {
- "bdata": "/FsF2tSgSsECwbM+bQ1bwQAAAEDyTVJB",
+ "bdata": "/FsF2tSgSsECwbM+NdtWwQAAAEBislpB",
"dtype": "f8"
}
}
@@ -6823,9 +6861,9 @@
"size": 12
},
"showarrow": false,
- "text": "-$7.1M",
+ "text": "-$6.0M",
"x": 1,
- "y": -7091636.97972131,
+ "y": -5991636.97972131,
"yshift": -14
},
{
@@ -6834,9 +6872,9 @@
"size": 12
},
"showarrow": false,
- "text": "$4.8M",
+ "text": "$7.0M",
"x": 2,
- "y": 4798409.0,
+ "y": 6998409.0,
"yshift": 14
},
{
@@ -6849,7 +6887,7 @@
"size": 12
},
"showarrow": false,
- "text": "3-yr net $4.8M · ROI 47%
NPV@13.5% $2.3M · payback 32 months (~Aug 2028)",
+ "text": "3-yr net $7.0M · ROI 87%
NPV@13.5% $3.9M · payback 30 months (~Jun 2028)",
"x": 0.01,
"xref": "paper",
"y": 0.98,
@@ -7723,10 +7761,10 @@
"id": "a8c9584e",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.035071Z",
- "iopub.status.busy": "2026-07-08T09:51:42.034912Z",
- "iopub.status.idle": "2026-07-08T09:51:42.045982Z",
- "shell.execute_reply": "2026-07-08T09:51:42.045162Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.340060Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.339841Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.354230Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.353592Z"
}
},
"outputs": [
@@ -7734,7 +7772,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "The missed costs move the 3-yr case by $16.7M ($21.5M pitched → $4.8M corrected).\n"
+ "The missed costs move the 3-yr case by $14.5M ($21.5M pitched → $7.0M corrected).\n"
]
},
{
@@ -7771,32 +7809,32 @@
" \n",
" | 3-yr incremental cost | \n",
" -$6.4M | \n",
- " $10.2M | \n",
+ " $8.0M | \n",
"
\n",
" \n",
" | 3-yr net | \n",
" $21.5M | \n",
- " $4.8M | \n",
+ " $7.0M | \n",
"
\n",
" \n",
" | ROI (net ÷ incremental cost) | \n",
" n/a — net cost saving | \n",
- " 47% | \n",
+ " 87% | \n",
"
\n",
" \n",
" | NPV @ 13.5% (deck rate) | \n",
" $15.3M | \n",
- " $2.3M | \n",
+ " $3.9M | \n",
"
\n",
" \n",
" | NPV @ 8.0% (CTM treasury) | \n",
" $17.5M | \n",
- " $3.1M | \n",
+ " $4.9M | \n",
"
\n",
" \n",
" | Payback | \n",
" immediate | \n",
- " 32 months (~Aug 2028) | \n",
+ " 30 months (~Jun 2028) | \n",
"
\n",
" \n",
"\n",
@@ -7805,12 +7843,12 @@
"text/plain": [
" As pitched (deck) Corrected\n",
"3-yr benefits $15.0M $15.0M\n",
- "3-yr incremental cost -$6.4M $10.2M\n",
- "3-yr net $21.5M $4.8M\n",
- "ROI (net ÷ incremental cost) n/a — net cost saving 47%\n",
- "NPV @ 13.5% (deck rate) $15.3M $2.3M\n",
- "NPV @ 8.0% (CTM treasury) $17.5M $3.1M\n",
- "Payback immediate 32 months (~Aug 2028)"
+ "3-yr incremental cost -$6.4M $8.0M\n",
+ "3-yr net $21.5M $7.0M\n",
+ "ROI (net ÷ incremental cost) n/a — net cost saving 87%\n",
+ "NPV @ 13.5% (deck rate) $15.3M $3.9M\n",
+ "NPV @ 8.0% (CTM treasury) $17.5M $4.9M\n",
+ "Payback immediate 30 months (~Jun 2028)"
]
},
"metadata": {},
@@ -7858,10 +7896,10 @@
"id": "6671014d",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.048036Z",
- "iopub.status.busy": "2026-07-08T09:51:42.047821Z",
- "iopub.status.idle": "2026-07-08T09:51:42.120089Z",
- "shell.execute_reply": "2026-07-08T09:51:42.119404Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.356523Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.356378Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.423548Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.422667Z"
}
},
"outputs": [
@@ -8932,10 +8970,10 @@
"id": "faa1e95f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.122576Z",
- "iopub.status.busy": "2026-07-08T09:51:42.122340Z",
- "iopub.status.idle": "2026-07-08T09:51:42.161638Z",
- "shell.execute_reply": "2026-07-08T09:51:42.160914Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.426278Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.426076Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.463565Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.462879Z"
}
},
"outputs": [
@@ -8975,21 +9013,21 @@
" \n",
" \n",
" | low hours | \n",
- " 5,622,159 | \n",
- " 5,327,159 | \n",
- " 5,032,159 | \n",
+ " 7,822,159 | \n",
+ " 7,527,159 | \n",
+ " 7,232,159 | \n",
"
\n",
" \n",
" | mid hours | \n",
- " 5,210,909 | \n",
- " 4,798,409 | \n",
- " 4,385,909 | \n",
+ " 7,410,909 | \n",
+ " 6,998,409 | \n",
+ " 6,585,909 | \n",
"
\n",
" \n",
" | high hours | \n",
- " 4,799,659 | \n",
- " 4,269,659 | \n",
- " 3,739,659 | \n",
+ " 6,999,659 | \n",
+ " 6,469,659 | \n",
+ " 5,939,659 | \n",
"
\n",
" \n",
"\n",
@@ -8997,9 +9035,9 @@
],
"text/plain": [
" $175/h $225/h $275/h\n",
- "low hours 5,622,159 5,327,159 5,032,159\n",
- "mid hours 5,210,909 4,798,409 4,385,909\n",
- "high hours 4,799,659 4,269,659 3,739,659"
+ "low hours 7,822,159 7,527,159 7,232,159\n",
+ "mid hours 7,410,909 6,998,409 6,585,909\n",
+ "high hours 6,999,659 6,469,659 5,939,659"
]
},
"metadata": {},
@@ -9056,10 +9094,10 @@
"id": "6516ea96",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.163714Z",
- "iopub.status.busy": "2026-07-08T09:51:42.163521Z",
- "iopub.status.idle": "2026-07-08T09:51:42.198239Z",
- "shell.execute_reply": "2026-07-08T09:51:42.197461Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.465149Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.465023Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.503951Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.502996Z"
}
},
"outputs": [
@@ -9068,7 +9106,7 @@
"output_type": "stream",
"text": [
"All assertions passed.\n",
- " benefits 3-yr $15.0M · tokens 3-yr $4.5M · corrected net 3-yr $4.8M\n"
+ " benefits 3-yr $15.0M · tokens 3-yr $4.5M · corrected net 3-yr $7.0M\n"
]
}
],
@@ -9084,7 +9122,9 @@
"_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",
+ "_approx(_l12[2027], 4_300_000) # deck rate stays verbatim\n",
+ "_approx(tco(\"ccaas_annual\"), 3_200_000) # signed contract overlay\n",
+ "_approx(licence_costs_by_year(12, tco(\"ccaas_annual\"))[2027], 3_200_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",
@@ -9135,8 +9175,8 @@
" _approx(net_corrected[y],\n",
" benefit_total_by_year[y] - (corrected_total_by_year[y] - BASELINE_ANNUAL))\n",
"\n",
- "print(\"All assertions passed.\")\n",
- "print(f\" benefits 3-yr {money(sum(benefit_total_by_year.values()))} · \"\n",
+ "backstage(\"All assertions passed.\")\n",
+ "backstage(f\" benefits 3-yr {money(sum(benefit_total_by_year.values()))} · \"\n",
" f\"tokens 3-yr {money(sum(token_total_by_year.values()))} · \"\n",
" f\"corrected net 3-yr {money(kpi_corrected['net_3yr'])}\")"
]
@@ -9156,8 +9196,8 @@
" intends PR on a queue subset; ask which queues.\n",
"- **2026 is a pure cost year** — $0 benefits under Genesys's own schedule, while CTM double-pays\n",
" existing platforms plus PS and early AI implementation.\n",
- "- The ramp programme (12 licence-free months) is a real credit the deck also failed to model —\n",
- " the correction is not one-directional.\n",
+ "- The corrections are not one-directional: the ramp programme (12 licence-free months) and\n",
+ " the contracted licence rate ($3.2M/yr vs the pitched $4.3M) both cut in Genesys's favour.\n",
"\n",
"**Known gaps / data wanted**\n",
"- Non-NAM site volumes & AHTs are `tokencalc` placeholders (🟡) — all non-NA token figures inherit\n",
@@ -9192,7 +9232,7 @@
"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 tables reflect the **current** input state, so a\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.\n"
]
},
@@ -9202,10 +9242,10 @@
"id": "c4c025b5",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:42.200663Z",
- "iopub.status.busy": "2026-07-08T09:51:42.200497Z",
- "iopub.status.idle": "2026-07-08T09:51:42.242257Z",
- "shell.execute_reply": "2026-07-08T09:51:42.241359Z"
+ "iopub.execute_input": "2026-07-08T12:20:41.506628Z",
+ "iopub.status.busy": "2026-07-08T12:20:41.506424Z",
+ "iopub.status.idle": "2026-07-08T12:20:41.551405Z",
+ "shell.execute_reply": "2026-07-08T12:20:41.550687Z"
}
},
"outputs": [
@@ -9274,14 +9314,14 @@
"\n",
"#### Corrected programme cost stack ($)\n",
"\n",
- "| | 2026 | 2027 | 2028 | 3-yr |\n",
- "|:------------------------------------------|----------:|----------:|----------:|-----------:|\n",
- "| CCaaS platform licences (ramp-adjusted) | 0 | 4,300,000 | 4,300,000 | 8,600,000 |\n",
- "| Base professional services + training | 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",
- "| AI token consumption | 0 | 1,176,183 | 3,345,156 | 4,521,339 |\n",
- "| AI implementation + KB readiness | 923,217 | 582,241 | 35,793 | 1,541,250 |\n",
- "| AI steady-state tuning | 0 | 157,500 | 157,500 | 315,000 |\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 professional services + training | 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",
+ "| AI token consumption | 0 | 1,176,183 | 3,345,156 | 4,521,339 |\n",
+ "| AI implementation + KB readiness | 923,217 | 582,241 | 35,793 | 1,541,250 |\n",
+ "| AI steady-state tuning | 0 | 157,500 | 157,500 | 315,000 |\n",
"\n",
"#### As-pitched cost stack ($)\n",
"\n",
@@ -9295,12 +9335,12 @@
"| | As pitched (deck) | Corrected |\n",
"|:-----------------------------|:----------------------|:----------------------|\n",
"| 3-yr benefits | $15.0M | $15.0M |\n",
- "| 3-yr incremental cost | -$6.4M | $10.2M |\n",
- "| 3-yr net | $21.5M | $4.8M |\n",
- "| ROI (net ÷ incremental cost) | n/a — net cost saving | 47% |\n",
- "| NPV @ 13.5% (deck rate) | $15.3M | $2.3M |\n",
- "| NPV @ 8.0% (CTM treasury) | $17.5M | $3.1M |\n",
- "| Payback | immediate | 32 months (~Aug 2028) |\n",
+ "| 3-yr incremental cost | -$6.4M | $8.0M |\n",
+ "| 3-yr net | $21.5M | $7.0M |\n",
+ "| ROI (net ÷ incremental cost) | n/a — net cost saving | 87% |\n",
+ "| NPV @ 13.5% (deck rate) | $15.3M | $3.9M |\n",
+ "| NPV @ 8.0% (CTM treasury) | $17.5M | $4.9M |\n",
+ "| Payback | immediate | 30 months (~Jun 2028) |\n",
"\n",
"#### Model state (JSON)\n",
"\n",
@@ -9313,8 +9353,8 @@
" },\n",
" \"corrected_cost_by_year\": {\n",
" \"2026\": 10790218,\n",
- " \"2027\": 13515925,\n",
- " \"2028\": 7838449\n",
+ " \"2027\": 12415925,\n",
+ " \"2028\": 6738449\n",
" },\n",
" \"pitched_cost_by_year\": {\n",
" \"2026\": 6867000,\n",
@@ -9323,17 +9363,17 @@
" },\n",
" \"net_by_year_corrected\": {\n",
" \"2026\": -3490218,\n",
- " \"2027\": -3601419,\n",
- " \"2028\": 11890046\n",
+ " \"2027\": -2501419,\n",
+ " \"2028\": 12990046\n",
" },\n",
" \"kpis_corrected\": {\n",
" \"benefits_3yr\": 15043000,\n",
- " \"incremental_cost_3yr\": 10244591,\n",
- " \"net_3yr\": 4798409,\n",
- " \"roi\": 0.4684,\n",
- " \"npv\": 2261247,\n",
+ " \"incremental_cost_3yr\": 8044591,\n",
+ " \"net_3yr\": 6998409,\n",
+ " \"roi\": 0.87,\n",
+ " \"npv\": 3867460,\n",
" \"discount_rate\": 0.135,\n",
- " \"payback\": \"32 months (~Aug 2028)\"\n",
+ " \"payback\": \"30 months (~Jun 2028)\"\n",
" },\n",
" \"kpis_pitched\": {\n",
" \"benefits_3yr\": 15043000,\n",
@@ -9346,6 +9386,8 @@
" },\n",
" \"assumptions\": {\n",
" \"ramp_months\": 12,\n",
+ " \"licence_annual\": 3200000,\n",
+ " \"deck_ccaas_annual\": 4300000,\n",
" \"hours_mode\": \"mid\",\n",
" \"blended_rate\": 225,\n",
" \"include_kb_readiness\": true,\n",
@@ -9382,12 +9424,14 @@
],
"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",
- " print(f\"\\n#### {title}\\n\")\n",
- " print(df.to_markdown(floatfmt=\",.0f\", **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",
@@ -9411,9 +9455,9 @@
" return v\n",
"\n",
"\n",
- "print(\"\\n#### Model state (JSON)\\n\")\n",
- "print(\"```json\")\n",
- "print(_json.dumps({\n",
+ "backstage(\"\\n#### Model state (JSON)\\n\")\n",
+ "backstage(\"```json\")\n",
+ "backstage(_json.dumps({\n",
" \"benefits_by_year\": {str(y): round(benefit_total_by_year[y]) for y in YEARS},\n",
" \"corrected_cost_by_year\": {str(y): round(corrected_total_by_year[y]) for y in YEARS},\n",
" \"pitched_cost_by_year\": {str(y): round(pitched_total_by_year[y]) for y in YEARS},\n",
@@ -9422,6 +9466,8 @@
" \"kpis_pitched\": {k: _jval(v) for k, v in kpi_pitched.items()},\n",
" \"assumptions\": {\n",
" \"ramp_months\": RAMP_MONTHS,\n",
+ " \"licence_annual\": round(LICENCE_ANNUAL),\n",
+ " \"deck_ccaas_annual\": round(TCO_VERBATIM[\"ccaas_annual\"]),\n",
" \"hours_mode\": HOURS_MODE,\n",
" \"blended_rate\": BLENDED_RATE,\n",
" \"include_kb_readiness\": INCLUDE_KB_READINESS,\n",
@@ -9438,7 +9484,7 @@
" for r in REGIONS},\n",
" },\n",
"}, indent=2))\n",
- "print(\"```\")\n"
+ "backstage(\"```\")\n"
]
}
],
@@ -9463,166 +9509,42 @@
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
- "0954b55479024ff5a2ac0c36ad9f3ef6": {
- "model_module": "@jupyter-widgets/base",
- "model_module_version": "2.0.0",
- "model_name": "LayoutModel",
+ "0a76099b436240728b539f8867340625": {
+ "model_module": "anywidget",
+ "model_module_version": "~0.11.*",
+ "model_name": "AnyModel",
"state": {
- "_model_module": "@jupyter-widgets/base",
- "_model_module_version": "2.0.0",
- "_model_name": "LayoutModel",
+ "_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": "@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
+ "_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_3c3d7da84fb348c1a43bc1820e982eca",
+ "layout_path": null,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": "13.5% (deck)"
}
},
- "11500f028a0048e084c56f478424ad68": {
- "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
- }
- },
- "126434365d3e4febac4341acf10839fb": {
- "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
- }
- },
- "2302d16d822548e291cc7fbf6657c65f": {
+ "0bfe75e3708140a49973b3af4ddda41a": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -9641,8 +9563,8 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "ASIA — contract termination",
- "layout": "IPY_MODEL_126434365d3e4febac4341acf10839fb",
+ "label": "EMEA — contract termination",
+ "layout": "IPY_MODEL_d94f1210f1cd418c9efd2a5229ed8f02",
"layout_path": null,
"max": "",
"min": "",
@@ -9655,7 +9577,7 @@
"value": "2027-12-31"
}
},
- "26b216b0e04a48d680b5e10c6a5ada58": {
+ "0c28dc240962449d95c3c0961fcae03c": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -9708,7 +9630,523 @@
"width": null
}
},
- "2a33e86c1bbb4747a29fdcc44844f5a0": {
+ "119e218090684e5192891ae301b29a8b": {
+ "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
+ }
+ },
+ "12c27b75c74a4785aac38abca3d50a0a": {
+ "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_16a2df5aca114aa28e9d7207ca9f3112",
+ "layout_path": null,
+ "placeholder": "",
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "style": "IPY_MODEL_f5efc754df6641c9a87d314245c1ce85",
+ "tabbable": null,
+ "tooltip": null,
+ "value": ""
+ }
+ },
+ "16a2df5aca114aa28e9d7207ca9f3112": {
+ "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
+ }
+ },
+ "1a52eaf6b29447c1b8aa1db54a4e56b8": {
+ "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_9ed2e7cc399b4d1aacab8aaf9470d535",
+ "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
+ }
+ },
+ "1ee1d47e07a841a8908db4d0ea1d3952": {
+ "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": "NA Email implements early (Jan 2027)",
+ "last_changed_at": "",
+ "layout": "IPY_MODEL_119e218090684e5192891ae301b29a8b",
+ "layout_path": null,
+ "n_toggles": 0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": true
+ }
+ },
+ "2058a28b452f4885954eed2c21208812": {
+ "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": "Copilot includes ASIA sites",
+ "last_changed_at": "",
+ "layout": "IPY_MODEL_ac02fa42bd4f486791baff281b65c380",
+ "layout_path": null,
+ "n_toggles": 0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": false
+ }
+ },
+ "2912bf24d4e645d3bd78bc5c1b4953b5": {
+ "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": "CCaaS licence run-rate ($/yr) — contracted",
+ "layout": "IPY_MODEL_ba177efc36ad40ad9b4b68b67f31818a",
+ "layout_path": null,
+ "max": 10000000.0,
+ "min": 0.0,
+ "position": "inline",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "step": 100000.0,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": 3200000.0
+ }
+ },
+ "368cc08eb20b428b8f2fb62dcb989e46": {
+ "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
+ }
+ },
+ "384c2bacd884439d9885d07729c8f480": {
+ "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
+ }
+ },
+ "3c3d7da84fb348c1a43bc1820e982eca": {
+ "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
+ }
+ },
+ "417b05a4d31e4c96b1971794d6692f14": {
+ "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
+ }
+ },
+ "43858d8fa5774cca84507ed5cf72138e": {
+ "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": "AI Translate eligibility 🟡",
+ "layout": "IPY_MODEL_cffb228855c24bd39947ad7a0dfa6396",
+ "layout_path": null,
+ "max": 1.0,
+ "min": 0.0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "step": 0.01,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": 0.01
+ }
+ },
+ "4666a3204ec84afba77fcbed27edae43": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -9728,7 +10166,7 @@
"disabled": false,
"hidden": false,
"label": "ANZ — current platform cost ($/yr)",
- "layout": "IPY_MODEL_11500f028a0048e084c56f478424ad68",
+ "layout": "IPY_MODEL_9226e080c9134e24bedd33c6fbbc1a9a",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
@@ -9742,15 +10180,15 @@
"value": 677320.0
}
},
- "2e45dcf72b104082a58763592b715a55": {
+ "536a9842fe6a41078505ef6772a81435": {
"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 ",
+ "_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-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 ",
+ "_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",
@@ -9761,21 +10199,89 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "ANZ — contract termination",
- "layout": "IPY_MODEL_26b216b0e04a48d680b5e10c6a5ada58",
+ "label": "Auto-respond tokens per message 🟡",
+ "layout": "IPY_MODEL_417b05a4d31e4c96b1971794d6692f14",
"layout_path": null,
- "max": "",
- "min": "",
- "position": "inline",
+ "max": 0.5,
+ "min": 0.0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "step": 0.005,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": 0.05
+ }
+ },
+ "544d7fde83c442349a54af52de538da2": {
+ "model_module": "anywidget",
+ "model_module_version": "~0.11.*",
+ "model_name": "AnyModel",
+ "state": {
+ "_anywidget_id": "mercury.slider.SliderWidget",
+ "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ",
+ "_dom_classes": [],
+ "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\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": "Predictive Routing eligibility (%)",
+ "layout": "IPY_MODEL_df38600cdf104a408fad11492a02da34",
+ "layout_path": null,
+ "max": 100,
+ "min": 0,
+ "position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": "2027-12-31"
+ "value": 100
}
},
- "35f2f48a2784461c82476fbea9858d53": {
+ "58a2284801644c5c994a6779ba7b84b8": {
+ "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": "Include KB readiness (500–1,500 h)",
+ "last_changed_at": "",
+ "layout": "IPY_MODEL_cbeec3c96a7f4ca88ec32660ebe6dfd2",
+ "layout_path": null,
+ "n_toggles": 0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": true
+ }
+ },
+ "65e5f9944d224c329037ac9c608080e7": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -9828,15 +10334,68 @@
"width": null
}
},
- "382c80a20e064400997f826af1ea07b1": {
+ "69fea1412b934758bb191b199d12410b": {
+ "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
+ }
+ },
+ "70b68a62010b4fef96d9117d387ed629": {
"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 ",
+ "_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-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 ",
+ "_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",
@@ -9847,22 +10406,213 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "Auto-respond tokens per message 🟡",
- "layout": "IPY_MODEL_94a54469924a41409caba5c42a970880",
+ "label": "ANZ — contract termination",
+ "layout": "IPY_MODEL_c346f16e28a144e0a389f6cbffdba564",
"layout_path": null,
- "max": 0.5,
- "min": 0.0,
- "position": "sidebar",
+ "max": "",
+ "min": "",
+ "position": "inline",
"render_slot_id": null,
"source_cell_id": null,
- "step": 0.005,
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": 0.05
+ "value": "2027-12-31"
}
},
- "385dfb4c6e4e4bcfb29506e057a6d048": {
+ "79098a2b88e441bca21c67fc17de3376": {
+ "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
+ }
+ },
+ "87d58807331b444b831a9611af9b9b97": {
+ "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_69fea1412b934758bb191b199d12410b",
+ "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"
+ }
+ },
+ "9226e080c9134e24bedd33c6fbbc1a9a": {
+ "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
+ }
+ },
+ "9ed2e7cc399b4d1aacab8aaf9470d535": {
+ "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
+ }
+ },
+ "a2ae0d29f75c46d195430ab5c30cdfd6": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -9887,7 +10637,7 @@
"disabled": false,
"hidden": false,
"label": "Blended rate ($/h)",
- "layout": "IPY_MODEL_35f2f48a2784461c82476fbea9858d53",
+ "layout": "IPY_MODEL_65e5f9944d224c329037ac9c608080e7",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
@@ -9898,7 +10648,7 @@
"value": "225"
}
},
- "3b1c6efd42814d679c5107d99346d1ef": {
+ "ac02fa42bd4f486791baff281b65c380": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -9951,40 +10701,7 @@
"width": null
}
},
- "3b809f57362449818455b7a7df410ba1": {
- "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_5d904f0514f84f5195bb3cc725e843ec",
- "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"
- }
- },
- "41fd82f604bd4389b29a48c6975e5008": {
+ "ba177efc36ad40ad9b4b68b67f31818a": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -10037,320 +10754,7 @@
"width": null
}
},
- "45476efc0f5f4bd5a21da5df43beffac": {
- "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": "Email auto-respond rate",
- "layout": "IPY_MODEL_5dc57ed79012411cba84d03f093f4ad5",
- "layout_path": null,
- "max": 0.6,
- "min": 0.0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "step": 0.005,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": 0.255
- }
- },
- "5d904f0514f84f5195bb3cc725e843ec": {
- "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
- }
- },
- "5dc57ed79012411cba84d03f093f4ad5": {
- "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
- }
- },
- "6225392622974e3f9d9a1748875fb09c": {
- "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
- }
- },
- "6cdbf07eb87c4e09b0de8ee9c2bd2afe": {
- "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_6225392622974e3f9d9a1748875fb09c",
- "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"
- }
- },
- "724914abd50e4279ad8c872dbac9a721": {
- "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
- }
- },
- "745d27870c3f49bab0b544c35bcfd939": {
- "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": "Copilot includes ASIA sites",
- "last_changed_at": "",
- "layout": "IPY_MODEL_a150edcb6d264ffe8817ec078552a812",
- "layout_path": null,
- "n_toggles": 0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": false
- }
- },
- "76bcbbb0475747a190b161ee64d1f295": {
+ "c3066f4048b84495a1956bd5af19302f": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -10375,7 +10779,7 @@
"disabled": false,
"hidden": false,
"label": "Impl hours (V2 range)",
- "layout": "IPY_MODEL_c369da299b29427ea1074a46bacbae39",
+ "layout": "IPY_MODEL_d8bd31bfb7ad486da6f2a61a52d977b2",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
@@ -10386,87 +10790,7 @@
"value": "mid"
}
},
- "80daf3a6ecc340e98ccf5c5a1c1daa0c": {
- "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": "Include KB readiness (500–1,500 h)",
- "last_changed_at": "",
- "layout": "IPY_MODEL_c090f5b0970a40be81c9645ef58bb678",
- "layout_path": null,
- "n_toggles": 0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": true
- }
- },
- "8850876c02774870bf5987f929e17598": {
- "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
- }
- },
- "8bfe6fed4f5a4807a9fe4297af1c998a": {
- "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_d8cc57ec346b46b68d8ef45b8f06640e",
- "layout_path": null,
- "placeholder": "",
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "style": "IPY_MODEL_8850876c02774870bf5987f929e17598",
- "tabbable": null,
- "tooltip": null,
- "value": ""
- }
- },
- "94a54469924a41409caba5c42a970880": {
+ "c346f16e28a144e0a389f6cbffdba564": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -10519,669 +10843,7 @@
"width": null
}
},
- "98b5a8a698284978906b4a1d6229f869": {
- "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": "NA Email implements early (Jan 2027)",
- "last_changed_at": "",
- "layout": "IPY_MODEL_41fd82f604bd4389b29a48c6975e5008",
- "layout_path": null,
- "n_toggles": 0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": true
- }
- },
- "a150edcb6d264ffe8817ec078552a812": {
- "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
- }
- },
- "ac3e662f8eb046debbeef1783e5e0a03": {
- "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_cb61e24f6a36442480c15926a6c3c00d",
- "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
- }
- },
- "ae3eef3c182b46d5ac570e574c0fa782": {
- "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_eeac123ecebc4b57be6f5621d202c436",
- "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
- }
- },
- "b58b9680d08849c18e4a290d490d4f56": {
- "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
- }
- },
- "befa1244f84249babfdc2a5dca694656": {
- "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": "AI Translate eligibility 🟡",
- "layout": "IPY_MODEL_f232e71a8a90478e812a4473af06082a",
- "layout_path": null,
- "max": 1.0,
- "min": 0.0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "step": 0.01,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": 0.01
- }
- },
- "bf31d1fd5233456b8920c5afa3f70dcf": {
- "model_module": "anywidget",
- "model_module_version": "~0.11.*",
- "model_name": "AnyModel",
- "state": {
- "_anywidget_id": "mercury.slider.SliderWidget",
- "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n margin-bottom: 8px;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ",
- "_dom_classes": [],
- "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\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": "Predictive Routing eligibility (%)",
- "layout": "IPY_MODEL_0954b55479024ff5a2ac0c36ad9f3ef6",
- "layout_path": null,
- "max": 100,
- "min": 0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": 100
- }
- },
- "c090f5b0970a40be81c9645ef58bb678": {
- "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
- }
- },
- "c369da299b29427ea1074a46bacbae39": {
- "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
- }
- },
- "cb61e24f6a36442480c15926a6c3c00d": {
- "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
- }
- },
- "cbfcf1a55a2346bfbfc433fdaebdfd76": {
- "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_3b1c6efd42814d679c5107d99346d1ef",
- "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
- }
- },
- "d26e504d6ea9473494f410c336b26643": {
- "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_b58b9680d08849c18e4a290d490d4f56",
- "layout_path": null,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": "13.5% (deck)"
- }
- },
- "d8cc57ec346b46b68d8ef45b8f06640e": {
- "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
- }
- },
- "eeac123ecebc4b57be6f5621d202c436": {
- "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
- }
- },
- "f232e71a8a90478e812a4473af06082a": {
- "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
- }
- },
- "f56d7388533948518dca55aeb6efb4bb": {
+ "c4d6af10d9554e709089a8fb228f0f1d": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -11201,7 +10863,7 @@
"disabled": false,
"hidden": false,
"label": "ASIA — current platform cost ($/yr)",
- "layout": "IPY_MODEL_724914abd50e4279ad8c872dbac9a721",
+ "layout": "IPY_MODEL_384c2bacd884439d9885d07729c8f480",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
@@ -11214,6 +10876,477 @@
"url_key": "",
"value": 2069588.0
}
+ },
+ "c87c3137444e4dd69a6f46b74170270d": {
+ "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_368cc08eb20b428b8f2fb62dcb989e46",
+ "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
+ }
+ },
+ "cbeec3c96a7f4ca88ec32660ebe6dfd2": {
+ "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
+ }
+ },
+ "cffb228855c24bd39947ad7a0dfa6396": {
+ "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
+ }
+ },
+ "d3d9c76f75b34f688de66cca01913f38": {
+ "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": "Email auto-respond rate",
+ "layout": "IPY_MODEL_0c28dc240962449d95c3c0961fcae03c",
+ "layout_path": null,
+ "max": 0.6,
+ "min": 0.0,
+ "position": "sidebar",
+ "render_slot_id": null,
+ "source_cell_id": null,
+ "step": 0.005,
+ "tabbable": null,
+ "tooltip": null,
+ "url_key": "",
+ "value": 0.255
+ }
+ },
+ "d8bd31bfb7ad486da6f2a61a52d977b2": {
+ "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
+ }
+ },
+ "d94f1210f1cd418c9efd2a5229ed8f02": {
+ "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
+ }
+ },
+ "da0acf9493a74a58a1b4d1b67806902f": {
+ "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
+ }
+ },
+ "dcabb5e69fb94e829297689c7a6ae40a": {
+ "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_79098a2b88e441bca21c67fc17de3376",
+ "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
+ }
+ },
+ "ddf1294225004414bf666eb493358f85": {
+ "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_da0acf9493a74a58a1b4d1b67806902f",
+ "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"
+ }
+ },
+ "df38600cdf104a408fad11492a02da34": {
+ "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
+ }
+ },
+ "f5efc754df6641c9a87d314245c1ce85": {
+ "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
+ }
}
},
"version_major": 2,
diff --git a/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_migration_wfm.ipynb b/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_migration_wfm.ipynb
index c6fc838..067e786 100644
--- a/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_migration_wfm.ipynb
+++ b/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_migration_wfm.ipynb
@@ -18,7 +18,7 @@
"| Layer | Source | Treatment |\n",
"|---|---|---|\n",
"| WFM benefits | *Appendix 4 — CCaaS Platform Benefit Calculations (Consolidated).pptx*, slides 12–15 | **Verbatim**, scoped to NA/ANZ/ASIA, phased on Genesys's own deployment schedule |\n",
- "| CCaaS licences | Same deck (TCO slides 5–6) | $4.3M/yr run-rate, 12-month ramp credit |\n",
+ "| CCaaS licences | Deck TCO (slides 5–6) + 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",
@@ -36,10 +36,10 @@
"id": "38d0b7d9",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:32.737375Z",
- "iopub.status.busy": "2026-07-08T09:51:32.737238Z",
- "iopub.status.idle": "2026-07-08T09:51:33.255260Z",
- "shell.execute_reply": "2026-07-08T09:51:33.254449Z"
+ "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": [
@@ -76,13 +76,14 @@
" 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,\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",
@@ -95,7 +96,7 @@
"# 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 (ramp-adjusted)\": \"#2a78d6\",\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",
@@ -137,7 +138,7 @@
"\n",
"X = [str(y) for y in YEARS]\n",
"\n",
- "print(f\"tokencalc loaded — {len(CTM_DEFAULT_SITES)} sites, \"\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:,})\")"
]
@@ -148,22 +149,22 @@
"id": "104afa86",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.257950Z",
- "iopub.status.busy": "2026-07-08T09:51:33.257573Z",
- "iopub.status.idle": "2026-07-08T09:51:33.273846Z",
- "shell.execute_reply": "2026-07-08T09:51:33.273063Z"
+ "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": "2063160f4cf04fa0adb8c33b318c1d1e",
+ "model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
"position": "sidebar",
"widget": "MarkdownWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "2063160f4cf04fa0adb8c33b318c1d1e",
+ "model_id": "78688c06ca0b4ee19c1a0b81f18d628a",
"version_major": 2,
"version_minor": 0
},
@@ -237,27 +238,27 @@
"id": "f8863db7",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.276402Z",
- "iopub.status.busy": "2026-07-08T09:51:33.276199Z",
- "iopub.status.idle": "2026-07-08T09:51:33.318744Z",
- "shell.execute_reply": "2026-07-08T09:51:33.318199Z"
+ "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": "dcb8281a29e34d3287e37ff6484dec79",
+ "model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "dcb8281a29e34d3287e37ff6484dec79",
+ "model_id": "3446e8dd427b4abe8d361ec75bb0a9fa",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -266,17 +267,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "6f1d59be19ad40a4b16ad8a9e08cf2b7",
+ "model_id": "27e2171a593043338205c5b352f20f3d",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "6f1d59be19ad40a4b16ad8a9e08cf2b7",
+ "model_id": "27e2171a593043338205c5b352f20f3d",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -285,17 +286,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "96c9d66e7f8a4da7b876af1a331c10fc",
+ "model_id": "60a9596904a34374bc605974ac3a3235",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "96c9d66e7f8a4da7b876af1a331c10fc",
+ "model_id": "60a9596904a34374bc605974ac3a3235",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -304,17 +305,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "4f94ada4d3654c0d88b0f2de6f245827",
+ "model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "4f94ada4d3654c0d88b0f2de6f245827",
+ "model_id": "1c591bdaa1e44a55be8d24db8956b3d1",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -323,17 +324,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "b71ef9d82870445b88c103eff878a1b5",
+ "model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "b71ef9d82870445b88c103eff878a1b5",
+ "model_id": "8b8b8d01f0ca46f39c16bd16b93a1475",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -342,17 +343,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "d41e8f15cfb54b9fa8732e5abd70cdaa",
+ "model_id": "bae439b7a2d04ab29390996726534753",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "d41e8f15cfb54b9fa8732e5abd70cdaa",
+ "model_id": "bae439b7a2d04ab29390996726534753",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -361,17 +362,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "0162044e4540407da3e89e5ea81d7e89",
+ "model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "0162044e4540407da3e89e5ea81d7e89",
+ "model_id": "2f3ac4164f5a4454ba631d0df39e5d87",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -380,17 +381,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "3392e2ae1897473d93976897652e5812",
+ "model_id": "f8196ba7d89c487eb23ad614f78881fe",
"position": "inline",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "3392e2ae1897473d93976897652e5812",
+ "model_id": "f8196ba7d89c487eb23ad614f78881fe",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -399,17 +400,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "8234a1340dc541e698d035ab4dd7a4dc",
+ "model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
"position": "inline",
"widget": "DateInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "8234a1340dc541e698d035ab4dd7a4dc",
+ "model_id": "2a43f7bc543d446ead3eeaaadbbf3cea",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -454,10 +455,10 @@
"id": "0bf61187",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.320979Z",
- "iopub.status.busy": "2026-07-08T09:51:33.320791Z",
- "iopub.status.idle": "2026-07-08T09:51:33.336843Z",
- "shell.execute_reply": "2026-07-08T09:51:33.336277Z"
+ "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": [
@@ -574,10 +575,10 @@
"\n",
"current_by_year = current_costs_by_year(current_state)\n",
"\n",
- "print(f\"current-state total {money(current_state['annual_cost'].sum())}/yr \"\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",
- "print(f\"existing-platform run-off by year: \"\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)"
]
@@ -599,9 +600,11 @@
" (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 assumption**: the $4.3M/yr CCaaS quote includes the WFM/WEM SKUs,\n",
- " including the new APAC seats. If WEM turns out to be an add-on, raise the run-rate in\n",
- " the sidebar."
+ "- 🟢 **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."
]
},
{
@@ -610,27 +613,27 @@
"id": "8bafeb91",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.339178Z",
- "iopub.status.busy": "2026-07-08T09:51:33.339013Z",
- "iopub.status.idle": "2026-07-08T09:51:33.351562Z",
- "shell.execute_reply": "2026-07-08T09:51:33.350984Z"
+ "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": "695a72ce5689408388b3d68ce751c9ba",
+ "model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "695a72ce5689408388b3d68ce751c9ba",
+ "model_id": "6bf6331dbeea4a2e932d9ba01fd28557",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -639,17 +642,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "9987d9a9e53a4b679f82bf7f93944b8a",
+ "model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "9987d9a9e53a4b679f82bf7f93944b8a",
+ "model_id": "f7b01a3662ce4ee4a8396fa5df4f048f",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -658,17 +661,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "9d56a55a60aa4ddebd1ad3f5860f8658",
+ "model_id": "caae1a34df0945249b1a3d39320540b1",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "9d56a55a60aa4ddebd1ad3f5860f8658",
+ "model_id": "caae1a34df0945249b1a3d39320540b1",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -677,17 +680,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "41e766bc8d664e87b3e287f4fef5029a",
+ "model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "41e766bc8d664e87b3e287f4fef5029a",
+ "model_id": "2bf25ab141c646aeae3b8e9b80b6762d",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -696,17 +699,17 @@
{
"data": {
"application/mercury+json": {
- "model_id": "a3b57e4e572245bdb6f26b4d55011a3d",
+ "model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
"position": "sidebar",
"widget": "CheckboxWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "a3b57e4e572245bdb6f26b4d55011a3d",
+ "model_id": "9f85cf3f71b842ba83ed70bd17ea748f",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -719,7 +722,7 @@
"# 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_VERBATIM[\"ccaas_annual\"]),\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",
@@ -735,10 +738,10 @@
"id": "6d971716",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.353691Z",
- "iopub.status.busy": "2026-07-08T09:51:33.353510Z",
- "iopub.status.idle": "2026-07-08T09:51:33.366166Z",
- "shell.execute_reply": "2026-07-08T09:51:33.365438Z"
+ "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": [
@@ -746,8 +749,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "WFM scope: NA, ANZ, ASIA · licence run-rate $4.3M/yr · ramp 12 months\n",
- "ramp-adjusted licences by year: {2026: '$0K', 2027: '$4.3M', 2028: '$4.3M'}\n",
+ "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"
]
},
@@ -849,8 +852,9 @@
" 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 · ramp {RAMP_MONTHS} months\")\n",
- "print(f\"ramp-adjusted licences by year: \"\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)"
@@ -881,10 +885,10 @@
"id": "12673034",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.367994Z",
- "iopub.status.busy": "2026-07-08T09:51:33.367788Z",
- "iopub.status.idle": "2026-07-08T09:51:33.400877Z",
- "shell.execute_reply": "2026-07-08T09:51:33.400070Z"
+ "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": [
@@ -982,7 +986,7 @@
"ben_by_year = {y: float(wfm_long.loc[wfm_long.year == y, \"benefit\"].sum())\n",
" for y in YEARS}\n",
"\n",
- "print(f\"WFM benefits by year: { {y: money(v) for y, v in ben_by_year.items()} } \"\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",
@@ -998,10 +1002,10 @@
"id": "3cbefa13",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:33.402727Z",
- "iopub.status.busy": "2026-07-08T09:51:33.402557Z",
- "iopub.status.idle": "2026-07-08T09:51:34.792313Z",
- "shell.execute_reply": "2026-07-08T09:51:34.791340Z"
+ "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": [
@@ -2016,10 +2020,10 @@
"id": "ccde902f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.795246Z",
- "iopub.status.busy": "2026-07-08T09:51:34.795014Z",
- "iopub.status.idle": "2026-07-08T09:51:34.809042Z",
- "shell.execute_reply": "2026-07-08T09:51:34.808477Z"
+ "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": [
@@ -2027,7 +2031,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Migration programme cost: {2026: '$9.9M', 2027: '$11.6M', 2028: '$4.3M'} → 3-yr $25.8M\n",
+ "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"
]
},
@@ -2060,11 +2064,11 @@
" \n",
" \n",
" \n",
- " | CCaaS platform licences (ramp-adjusted) | \n",
+ " CCaaS platform licences (contracted, ramp-adjusted) | \n",
" 0 | \n",
- " 4,300,000 | \n",
- " 4,300,000 | \n",
- " 8,600,000 | \n",
+ " 3,200,000 | \n",
+ " 3,200,000 | \n",
+ " 6,400,000 | \n",
"
\n",
" \n",
" | Base PS + training (incl. migration + WFM) | \n",
@@ -2083,26 +2087,26 @@
"
\n",
" | TOTAL | \n",
" 9,867,001 | \n",
- " 11,600,001 | \n",
- " 4,300,000 | \n",
- " 25,767,002 | \n",
+ " 10,500,001 | \n",
+ " 3,200,000 | \n",
+ " 23,567,002 | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " 2026 2027 2028 \\\n",
- "CCaaS platform licences (ramp-adjusted) 0 4,300,000 4,300,000 \n",
- "Base PS + training (incl. migration + WFM) 2,567,000 0 0 \n",
- "Existing platform (term-contract run-off) 7,300,001 7,300,001 0 \n",
- "TOTAL 9,867,001 11,600,001 4,300,000 \n",
+ " 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",
- " 3-yr \n",
- "CCaaS platform licences (ramp-adjusted) 8,600,000 \n",
- "Base PS + training (incl. migration + WFM) 2,567,000 \n",
- "Existing platform (term-contract run-off) 14,600,002 \n",
- "TOTAL 25,767,002 "
+ " 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": {},
@@ -2111,7 +2115,7 @@
],
"source": [
"migration_costs = pd.DataFrame({\n",
- " \"CCaaS platform licences (ramp-adjusted)\": licence_by_year,\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",
@@ -2121,9 +2125,9 @@
"BASELINE_ANNUAL = TCO_VERBATIM[\"current_annual\"]\n",
"donothing_by_year = {y: BASELINE_ANNUAL for y in YEARS}\n",
"\n",
- "print(f\"Migration programme cost: { {y: money(v) for y, v in total_by_year.items()} } \"\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",
- "print(f\"Do nothing: {money(BASELINE_ANNUAL)}/yr → 3-yr {money(3 * BASELINE_ANNUAL)}\")\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\"])]))"
]
@@ -2134,10 +2138,10 @@
"id": "c058e795",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.812963Z",
- "iopub.status.busy": "2026-07-08T09:51:34.812768Z",
- "iopub.status.idle": "2026-07-08T09:51:34.844402Z",
- "shell.execute_reply": "2026-07-08T09:51:34.839354Z"
+ "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": [
@@ -2157,7 +2161,7 @@
"width": 2
}
},
- "name": "CCaaS platform licences (ramp-adjusted)",
+ "name": "CCaaS platform licences (contracted, ramp-adjusted)",
"type": "bar",
"x": [
"2026",
@@ -2166,8 +2170,8 @@
],
"y": [
0.0,
- 4300000.0,
- 4300000.0
+ 3200000.0,
+ 3200000.0
]
},
{
@@ -2236,7 +2240,7 @@
"2028"
],
"y": {
- "bdata": "AAAAIN/RYkEAAACg93h0QQAAAKDFknhB",
+ "bdata": "AAAAIN/RYkEAAACgaWxzQQAAAKCpeXZB",
"dtype": "f8"
}
},
@@ -2287,9 +2291,9 @@
"size": 12
},
"showarrow": false,
- "text": "$11.6M",
+ "text": "$10.5M",
"x": 1,
- "y": 11600001.0,
+ "y": 10500001.0,
"yshift": 12
},
{
@@ -2298,9 +2302,9 @@
"size": 12
},
"showarrow": false,
- "text": "$4.3M",
+ "text": "$3.2M",
"x": 2,
- "y": 4300000.0,
+ "y": 3200000.0,
"yshift": 12
},
{
@@ -2309,10 +2313,10 @@
"size": 12
},
"showarrow": false,
- "text": "3-yr $25.8M — $3.9M above doing nothing",
+ "text": "3-yr $23.6M — $1.7M above doing nothing",
"x": 2,
"xshift": -70,
- "y": 25767002.0,
+ "y": 23567002.0,
"yshift": 18
}
],
@@ -3190,11 +3194,12 @@
"benefits − incremental cost. The 2026–27 double-billing penalty and the 2028 cost-avoidance\n",
"credit both fall out of this one frame.\n",
"\n",
- "Inside the deck's own 2026–2028 window the migration-only case is **net-negative** — the\n",
- "licence saving and WFM value arrive too late to cover the PS outlay and double-billing.\n",
- "The **run-rate breakeven** KPI extrapolates past the window: once contracts are terminated\n",
- "and the ramp is over, the steady-state saving is (baseline − licence run-rate) + scoped\n",
- "WFM annual values.\n",
+ "At the **contracted licence rate ($3.2M/yr)** the case turns positive inside the deck's\n",
+ "own 2026–2028 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.*"
]
@@ -3205,27 +3210,27 @@
"id": "d8293152",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.846655Z",
- "iopub.status.busy": "2026-07-08T09:51:34.846470Z",
- "iopub.status.idle": "2026-07-08T09:51:34.855106Z",
- "shell.execute_reply": "2026-07-08T09:51:34.854285Z"
+ "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": "8686e81af3af4348b1963ec0223daf8c",
+ "model_id": "bb915be640904627a7e37f0597028fab",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
- "model_id": "8686e81af3af4348b1963ec0223daf8c",
+ "model_id": "bb915be640904627a7e37f0597028fab",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -3246,10 +3251,10 @@
"id": "c89973af",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.857641Z",
- "iopub.status.busy": "2026-07-08T09:51:34.857408Z",
- "iopub.status.idle": "2026-07-08T09:51:34.864408Z",
- "shell.execute_reply": "2026-07-08T09:51:34.863578Z"
+ "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": [
@@ -3257,8 +3262,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "net by year: {2026: '-$2.6M', 2027: '-$4.2M', 2028: '$5.2M'} → 3-yr -$1.6M\n",
- "steady-state run-rate saving $5.9M/yr ($3.0M licences + $2.9M WFM annuals) → breakeven 40 months (~Apr 2029, extrapolated)\n"
+ "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"
]
}
],
@@ -3280,9 +3285,9 @@
" return f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net cost saving\"\n",
"\n",
"\n",
- "print(f\"net by year: { {y: money(v) for y, v in net_by.items()} } \"\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",
- "print(f\"steady-state run-rate saving {money(RUNRATE_SAVING)}/yr \"\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}\")"
]
@@ -3293,10 +3298,10 @@
"id": "9d135a19",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.868353Z",
- "iopub.status.busy": "2026-07-08T09:51:34.868186Z",
- "iopub.status.idle": "2026-07-08T09:51:34.908914Z",
- "shell.execute_reply": "2026-07-08T09:51:34.908362Z"
+ "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": [
@@ -3347,8 +3352,8 @@
],
"y": [
-2567001.0,
- -4300001.0,
- 3000000.0
+ -3200001.0,
+ 4100000.0
]
},
{
@@ -3373,7 +3378,7 @@
"2028"
],
"y": {
- "bdata": "AAAAgKyVQ8HsxE5s48hZwQAAAABqsjfB",
+ "bdata": "AAAAgKyVQ8HsxE5sq5ZVwQAAAACsviNB",
"dtype": "f8"
}
}
@@ -3397,9 +3402,9 @@
"size": 12
},
"showarrow": false,
- "text": "-$6.8M",
+ "text": "-$5.7M",
"x": 1,
- "y": -6759309.692307692,
+ "y": -5659309.692307692,
"yshift": -14
},
{
@@ -3408,10 +3413,10 @@
"size": 12
},
"showarrow": false,
- "text": "-$1.6M",
+ "text": "$647K",
"x": 2,
- "y": -1553002.0,
- "yshift": -14
+ "y": 646998.0,
+ "yshift": 14
},
{
"align": "left",
@@ -3423,7 +3428,7 @@
"size": 12
},
"showarrow": false,
- "text": "3-yr net -$1.6M · ROI -40%
NPV@13.5% -$2.0M · payback beyond 2028
run-rate $5.9M/yr post-window → breakeven 40 months (~Apr 2029, extrapolated)",
+ "text": "3-yr net $647K · ROI 39%
NPV@13.5% -$349K · payback 35 months (~Nov 2028)
run-rate $7.0M/yr post-window → breakeven 35 months (~Nov 2028)",
"x": 0.01,
"xref": "paper",
"y": 0.98,
@@ -4299,10 +4304,10 @@
"id": "7ca576f1",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.911493Z",
- "iopub.status.busy": "2026-07-08T09:51:34.911272Z",
- "iopub.status.idle": "2026-07-08T09:51:34.922969Z",
- "shell.execute_reply": "2026-07-08T09:51:34.922144Z"
+ "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": [
@@ -4310,7 +4315,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "The migration stands on cost avoidance alone — for the with-AI case in the same frame, open the Corrected Business Case notebook.\n"
+ "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"
]
},
{
@@ -4334,61 +4339,82 @@
" \n",
" \n",
" | \n",
- " Migration + WFM (no AI) | \n",
+ " Deck rate ($4.3M/yr) | \n",
+ " Contracted ($3.2M/yr) | \n",
"
\n",
" \n",
" \n",
" \n",
" | 3-yr WFM benefits (verbatim, scoped) | \n",
" $2.3M | \n",
+ " $2.3M | \n",
"
\n",
" \n",
" | 3-yr incremental cost | \n",
" $3.9M | \n",
+ " $1.7M | \n",
"
\n",
" \n",
" | 3-yr net | \n",
" -$1.6M | \n",
+ " $647K | \n",
"
\n",
" \n",
" | ROI (net ÷ incremental cost) | \n",
" -40% | \n",
+ " 39% | \n",
"
\n",
" \n",
" | NPV @ 13.5% (deck rate) | \n",
" -$2.0M | \n",
+ " -$349K | \n",
"
\n",
" \n",
" | NPV @ 8.0% (CTM treasury) | \n",
" -$1.8M | \n",
+ " -$22K | \n",
"
\n",
" \n",
" | Payback (2026–28 window) | \n",
" beyond 2028 | \n",
+ " 35 months (~Nov 2028) | \n",
"
\n",
" \n",
" | Steady-state run-rate saving | \n",
" $5.9M/yr | \n",
+ " $7.0M/yr | \n",
"
\n",
" \n",
- " | Breakeven (run-rate extrapolation) | \n",
+ " Breakeven | \n",
" 40 months (~Apr 2029, extrapolated) | \n",
+ " 35 months (~Nov 2028) | \n",
"
\n",
" \n",
"\n",
""
],
"text/plain": [
- " Migration + WFM (no AI)\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 (2026–28 window) beyond 2028\n",
- "Steady-state run-rate saving $5.9M/yr\n",
- "Breakeven (run-rate extrapolation) 40 months (~Apr 2029, extrapolated)"
+ " 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 (2026–28 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 (2026–28 window) 35 months (~Nov 2028) \n",
+ "Steady-state run-rate saving $7.0M/yr \n",
+ "Breakeven 35 months (~Nov 2028) "
]
},
"metadata": {},
@@ -4396,19 +4422,37 @@
}
],
"source": [
- "kpis_fmt = pd.DataFrame({\"Migration + WFM (no AI)\": {\n",
- " \"3-yr WFM benefits (verbatim, scoped)\": money(kpi[\"benefits_3yr\"]),\n",
- " \"3-yr incremental cost\": money(kpi[\"incremental_cost_3yr\"]),\n",
- " \"3-yr net\": money(kpi[\"net_3yr\"]),\n",
- " \"ROI (net ÷ incremental cost)\": fmt_roi(kpi),\n",
- " f\"NPV @ {kpi['discount_rate']:.1%} (deck rate)\": money(kpi[\"npv\"]),\n",
- " \"NPV @ 8.0% (CTM treasury)\": money(npv([net_by[y] for y in YEARS], 0.08)),\n",
- " \"Payback (2026–28 window)\": kpi[\"payback\"],\n",
- " \"Steady-state run-rate saving\": f\"{money(RUNRATE_SAVING)}/yr\",\n",
- " \"Breakeven (run-rate extrapolation)\": BREAKEVEN,\n",
- "}})\n",
- "print(\"The migration stands on cost avoidance alone — for the with-AI case in the \"\n",
- " \"same frame, open the Corrected Business Case notebook.\")\n",
+ "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 (2026–28 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)"
]
},
@@ -4423,7 +4467,8 @@
"\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",
- "(🟡 whether WFM/WEM SKUs are really inside $4.3M/yr). Contract **termination dates**\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."
]
@@ -4434,10 +4479,10 @@
"id": "4ab17e24",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.924840Z",
- "iopub.status.busy": "2026-07-08T09:51:34.924675Z",
- "iopub.status.idle": "2026-07-08T09:51:34.967070Z",
- "shell.execute_reply": "2026-07-08T09:51:34.966339Z"
+ "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": [
@@ -4467,9 +4512,9 @@
"showscale": false,
"type": "heatmap",
"x": [
+ "$3.2M/yr (contracted)",
"$3.8M/yr",
- "$4.3M/yr",
- "$4.8M/yr"
+ "$4.3M/yr (deck)"
],
"xgap": 2,
"y": [
@@ -4482,29 +4527,29 @@
"ygap": 2,
"z": [
[
+ -2553002.0000000005,
-4353002.0,
- -5853002.0,
- -7353002.0
+ -5853002.0
],
[
+ -953002.0000000005,
-2453002.0000000005,
- -3703002.0000000005,
- -4953002.0
+ -3703002.0000000005
],
[
+ 646997.9999999995,
-553002.0000000005,
- -1553002.0000000005,
- -2553002.0
+ -1553002.0000000005
],
[
+ 2246998.0,
1346997.9999999998,
- 596997.9999999998,
- -153002.00000000047
+ 596997.9999999998
],
[
+ 3846997.9999999995,
3246997.9999999995,
- 2746997.9999999995,
- 2246997.9999999995
+ 2746997.9999999995
]
],
"zmid": 0
@@ -4518,10 +4563,20 @@
"size": 12
},
"showarrow": false,
- "text": "-$4.4M",
+ "text": "-$2.6M",
"x": 0,
"y": 0
},
+ {
+ "font": {
+ "color": "#ffffff",
+ "size": 12
+ },
+ "showarrow": false,
+ "text": "-$4.4M",
+ "x": 1,
+ "y": 0
+ },
{
"font": {
"color": "#ffffff",
@@ -4529,16 +4584,6 @@
},
"showarrow": false,
"text": "-$5.9M",
- "x": 1,
- "y": 0
- },
- {
- "font": {
- "color": "#ffffff",
- "size": 12
- },
- "showarrow": false,
- "text": "-$7.4M",
"x": 2,
"y": 0
},
@@ -4548,10 +4593,20 @@
"size": 12
},
"showarrow": false,
- "text": "-$2.5M",
+ "text": "-$953K",
"x": 0,
"y": 1
},
+ {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 12
+ },
+ "showarrow": false,
+ "text": "-$2.5M",
+ "x": 1,
+ "y": 1
+ },
{
"font": {
"color": "#0b0b0b",
@@ -4559,16 +4614,6 @@
},
"showarrow": false,
"text": "-$3.7M",
- "x": 1,
- "y": 1
- },
- {
- "font": {
- "color": "#ffffff",
- "size": 12
- },
- "showarrow": false,
- "text": "-$5.0M",
"x": 2,
"y": 1
},
@@ -4578,10 +4623,20 @@
"size": 12
},
"showarrow": false,
- "text": "-$553K",
+ "text": "$647K",
"x": 0,
"y": 2
},
+ {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 12
+ },
+ "showarrow": false,
+ "text": "-$553K",
+ "x": 1,
+ "y": 2
+ },
{
"font": {
"color": "#0b0b0b",
@@ -4589,16 +4644,6 @@
},
"showarrow": false,
"text": "-$1.6M",
- "x": 1,
- "y": 2
- },
- {
- "font": {
- "color": "#0b0b0b",
- "size": 12
- },
- "showarrow": false,
- "text": "-$2.6M",
"x": 2,
"y": 2
},
@@ -4608,10 +4653,20 @@
"size": 12
},
"showarrow": false,
- "text": "$1.3M",
+ "text": "$2.2M",
"x": 0,
"y": 3
},
+ {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 12
+ },
+ "showarrow": false,
+ "text": "$1.3M",
+ "x": 1,
+ "y": 3
+ },
{
"font": {
"color": "#0b0b0b",
@@ -4619,26 +4674,16 @@
},
"showarrow": false,
"text": "$597K",
- "x": 1,
- "y": 3
- },
- {
- "font": {
- "color": "#0b0b0b",
- "size": 12
- },
- "showarrow": false,
- "text": "-$153K",
"x": 2,
"y": 3
},
{
"font": {
- "color": "#0b0b0b",
+ "color": "#ffffff",
"size": 12
},
"showarrow": false,
- "text": "$3.2M",
+ "text": "$3.8M",
"x": 0,
"y": 4
},
@@ -4648,7 +4693,7 @@
"size": 12
},
"showarrow": false,
- "text": "$2.7M",
+ "text": "$3.2M",
"x": 1,
"y": 4
},
@@ -4658,7 +4703,7 @@
"size": 12
},
"showarrow": false,
- "text": "$2.2M",
+ "text": "$2.7M",
"x": 2,
"y": 4
}
@@ -5501,14 +5546,15 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Current corner (12-mo ramp × $4.3M/yr): -$1.6M — no corner of the grid turns the 3-yr window positive on its own.\n"
+ "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_800_000, 4_300_000, 4_800_000]\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",
@@ -5521,7 +5567,7 @@
"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\" for lc in LIC_GRID],\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}\"))\n",
@@ -5537,9 +5583,11 @@
"fig.update_layout(xaxis=dict(showgrid=False), yaxis=dict(showgrid=False, tickformat=None),\n",
" hovermode=\"closest\")\n",
"fig.show()\n",
- "print(f\"Current corner ({RAMP_MONTHS}-mo ramp × {money(LICENCE_ANNUAL)}/yr): \"\n",
- " f\"{money(net_3yr(RAMP_MONTHS, LICENCE_ANNUAL))} — no corner of the grid turns \"\n",
- " f\"the 3-yr window positive on its own.\")"
+ "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.\")"
]
},
{
@@ -5563,10 +5611,10 @@
"id": "939417e4",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.969853Z",
- "iopub.status.busy": "2026-07-08T09:51:34.969667Z",
- "iopub.status.idle": "2026-07-08T09:51:34.989093Z",
- "shell.execute_reply": "2026-07-08T09:51:34.988234Z"
+ "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": [
@@ -5575,7 +5623,7 @@
"output_type": "stream",
"text": [
"All assertions passed.\n",
- " WFM benefits 3-yr $2.3M · programme cost 3-yr $25.8M · net 3-yr -$1.6M · breakeven 40 months (~Apr 2029, extrapolated)\n"
+ " WFM benefits 3-yr $2.3M · programme cost 3-yr $23.6M · net 3-yr $647K · breakeven 35 months (~Nov 2028)\n"
]
}
],
@@ -5610,6 +5658,13 @@
"_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",
@@ -5619,6 +5674,11 @@
"_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",
@@ -5626,8 +5686,8 @@
"for y in YEARS:\n",
" _approx(net_by[y], ben_by_year[y] - (total_by_year[y] - BASELINE_ANNUAL))\n",
"\n",
- "print(\"All assertions passed.\")\n",
- "print(f\" WFM benefits 3-yr {money(sum(ben_by_year.values()))} · \"\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}\")"
]
@@ -5642,10 +5702,11 @@
"## 8 · Risks, notes & next steps\n",
"\n",
"**Findings to lead with**\n",
- "- **The platform swap alone does not carry the case in the deck's own window**: net −$1.6M\n",
- " by end-2028 at defaults. At the $5.9M/yr steady-state run-rate it turns ~April 2029 —\n",
- " a real but slow cost-avoidance story. The AI capabilities are what pull payback inside\n",
- " the window (see the Corrected Business Case notebook).\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",
@@ -5680,7 +5741,7 @@
"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 tables reflect the **current** input state, so a\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."
]
},
@@ -5690,10 +5751,10 @@
"id": "32ebed57",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-07-08T09:51:34.991564Z",
- "iopub.status.busy": "2026-07-08T09:51:34.991336Z",
- "iopub.status.idle": "2026-07-08T09:51:35.026654Z",
- "shell.execute_reply": "2026-07-08T09:51:35.025674Z"
+ "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": [
@@ -5712,13 +5773,7 @@
"| 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"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "\n",
"| region | WFM treatment | in scope | verbatim annual | verbatim 3-yr |\n",
"|:---------|:--------------------------|:-----------|------------------:|----------------:|\n",
"| NA | migrate existing users | ✓ | 0 | 0 |\n",
@@ -5736,33 +5791,33 @@
"\n",
"#### Migration programme cost stack ($)\n",
"\n",
- "| | 2026 | 2027 | 2028 | 3-yr |\n",
- "|:-------------------------------------------|----------:|----------:|----------:|-----------:|\n",
- "| CCaaS platform licences (ramp-adjusted) | 0 | 4,300,000 | 4,300,000 | 8,600,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",
+ "| | 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 | 11,600,001 | 4,300,000 |\n",
- "| incremental cost | 2,567,001 | 4,300,001 | -3,000,000 |\n",
- "| net | -2,567,001 | -4,192,309 | 5,206,308 |\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",
- "| | Migration + WFM (no AI) |\n",
- "|:-------------------------------------|:------------------------------------|\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 (2026–28 window) | beyond 2028 |\n",
- "| Steady-state run-rate saving | $5.9M/yr |\n",
- "| Breakeven (run-rate extrapolation) | 40 months (~Apr 2029, extrapolated) |\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 (2026–28 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",
@@ -5776,16 +5831,25 @@
" },\n",
" \"cost_by_year\": {\n",
" \"2026\": 9867001,\n",
- " \"2027\": 11600001,\n",
- " \"2028\": 4300000\n",
+ " \"2027\": 10500001,\n",
+ " \"2028\": 3200000\n",
" },\n",
" \"net_by_year\": {\n",
" \"2026\": -2567001,\n",
- " \"2027\": -4192309,\n",
- " \"2028\": 5206308\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",
@@ -5793,11 +5857,12 @@
" \"discount_rate\": 0.135,\n",
" \"payback\": \"beyond 2028\"\n",
" },\n",
- " \"runrate_saving_annual\": 5900000,\n",
- " \"breakeven\": \"40 months (~Apr 2029, extrapolated)\",\n",
+ " \"runrate_saving_annual\": 7000000,\n",
+ " \"breakeven\": \"35 months (~Nov 2028)\",\n",
" \"assumptions\": {\n",
" \"ramp_months\": 12,\n",
- " \"licence_annual\": 4300000,\n",
+ " \"licence_annual\": 3200000,\n",
+ " \"deck_ccaas_annual\": 4300000,\n",
" \"licence_includes_wfm_skus\": true,\n",
" \"wfm_regions\": [\n",
" \"NA\",\n",
@@ -5833,12 +5898,14 @@
],
"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",
- " print(f\"\\n#### {title}\\n\")\n",
- " print(df.to_markdown(floatfmt=\",.0f\", **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",
@@ -5861,19 +5928,21 @@
" return v\n",
"\n",
"\n",
- "print(\"\\n#### Model state (JSON)\\n\")\n",
- "print(\"```json\")\n",
- "print(_json.dumps({\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",
@@ -5885,7 +5954,7 @@
" for r in REGIONS},\n",
" },\n",
"}, indent=2))\n",
- "print(\"```\")"
+ "backstage(\"```\")"
]
}
],
@@ -5910,40 +5979,7 @@
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
- "0162044e4540407da3e89e5ea81d7e89": {
- "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_199388df2fbf4ad0b864d40ca6c68473",
- "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"
- }
- },
- "0def362b5cd2438fbd5cb52352defbd2": {
+ "12974d31f88c40d99967fc5d44b9e2a4": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -5996,7 +6032,7 @@
"width": null
}
},
- "199388df2fbf4ad0b864d40ca6c68473": {
+ "1beaba7af87e4528abd06da3151e882f": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -6049,141 +6085,7 @@
"width": null
}
},
- "2063160f4cf04fa0adb8c33b318c1d1e": {
- "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_aff1d81cd3cd4168b6a133eed16f673c",
- "layout_path": null,
- "placeholder": "",
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "style": "IPY_MODEL_65dd9ec7dc60475093da3ae2c9deb3c9",
- "tabbable": null,
- "tooltip": null,
- "value": ""
- }
- },
- "210a51183af8466cb6514a862e2333b3": {
- "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
- }
- },
- "293f226456dc405082c5a8c2acc54214": {
- "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
- }
- },
- "3392e2ae1897473d93976897652e5812": {
+ "1c591bdaa1e44a55be8d24db8956b3d1": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -6202,8 +6104,8 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "ASIA — current platform cost ($/yr)",
- "layout": "IPY_MODEL_d731a700aec848699c2adaf537190046",
+ "label": "ANZ — current platform cost ($/yr)",
+ "layout": "IPY_MODEL_9d4f1c2f10084d87bbfb422c3247f7e1",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
@@ -6214,116 +6116,77 @@
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": 2069588.0
+ "value": 677320.0
}
},
- "39bb3041c34a44398c4933f6f1a004f5": {
- "model_module": "@jupyter-widgets/base",
- "model_module_version": "2.0.0",
- "model_name": "LayoutModel",
+ "27e2171a593043338205c5b352f20f3d": {
+ "model_module": "anywidget",
+ "model_module_version": "~0.11.*",
+ "model_name": "AnyModel",
"state": {
- "_model_module": "@jupyter-widgets/base",
- "_model_module_version": "2.0.0",
- "_model_name": "LayoutModel",
+ "_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": "@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
+ "_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
}
},
- "3e86dbe19c264dceabdca70642bd9e22": {
- "model_module": "@jupyter-widgets/base",
- "model_module_version": "2.0.0",
- "model_name": "LayoutModel",
+ "2a43f7bc543d446ead3eeaaadbbf3cea": {
+ "model_module": "anywidget",
+ "model_module_version": "~0.11.*",
+ "model_name": "AnyModel",
"state": {
- "_model_module": "@jupyter-widgets/base",
- "_model_module_version": "2.0.0",
- "_model_name": "LayoutModel",
+ "_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": "@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
+ "_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"
}
},
- "41e766bc8d664e87b3e287f4fef5029a": {
+ "2bf25ab141c646aeae3b8e9b80b6762d": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -6345,7 +6208,7 @@
"hidden": false,
"label": "WFM — ASIA (implement)",
"last_changed_at": "",
- "layout": "IPY_MODEL_293f226456dc405082c5a8c2acc54214",
+ "layout": "IPY_MODEL_92aaf404ceee451f84aeb8f3252558e0",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
@@ -6357,15 +6220,15 @@
"value": true
}
},
- "4f94ada4d3654c0d88b0f2de6f245827": {
+ "2f3ac4164f5a4454ba631d0df39e5d87": {
"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 ",
+ "_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-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 ",
+ "_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",
@@ -6376,22 +6239,21 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "ANZ — current platform cost ($/yr)",
- "layout": "IPY_MODEL_72ecb74f5c3c41ff840ae316192edd30",
+ "label": "EMEA — contract termination",
+ "layout": "IPY_MODEL_9a295ac0af8348ebb661f2a0a9be8d17",
"layout_path": null,
- "max": 8000000.0,
- "min": 0.0,
+ "max": "",
+ "min": "",
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
- "step": 10000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": 677320.0
+ "value": "2027-12-31"
}
},
- "5911858701ee466d939d30e0483d43fa": {
+ "3208166b83d6414c93131c6d9a6b1dac": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -6444,7 +6306,94 @@
"width": null
}
},
- "65dd9ec7dc60475093da3ae2c9deb3c9": {
+ "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",
@@ -6462,7 +6411,146 @@
"text_color": null
}
},
- "695a72ce5689408388b3d68ce751c9ba": {
+ "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",
@@ -6482,7 +6570,7 @@
"disabled": false,
"hidden": false,
"label": "Genesys licence run-rate ($/yr)",
- "layout": "IPY_MODEL_5911858701ee466d939d30e0483d43fa",
+ "layout": "IPY_MODEL_3b92df6e04544844a57103b779776678",
"layout_path": null,
"max": 10000000.0,
"min": 0.0,
@@ -6493,44 +6581,38 @@
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": 4300000.0
+ "value": 3200000.0
}
},
- "6f1d59be19ad40a4b16ad8a9e08cf2b7": {
- "model_module": "anywidget",
- "model_module_version": "~0.11.*",
- "model_name": "AnyModel",
+ "78688c06ca0b4ee19c1a0b81f18d628a": {
+ "model_module": "@jupyter-widgets/controls",
+ "model_module_version": "2.0.0",
+ "model_name": "HTMLModel",
"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",
+ "_model_module": "@jupyter-widgets/controls",
+ "_model_module_version": "2.0.0",
+ "_model_name": "HTMLModel",
"_view_count": null,
- "_view_module": "anywidget",
- "_view_module_version": "~0.11.*",
- "_view_name": "AnyView",
+ "_view_module": "@jupyter-widgets/controls",
+ "_view_module_version": "2.0.0",
+ "_view_name": "HTMLView",
"cell_id": "",
- "disabled": false,
- "hidden": false,
- "label": "NA — current platform cost ($/yr)",
- "layout": "IPY_MODEL_b0780d60c5bd499b8d556a0bb73058f3",
+ "description": "",
+ "description_allow_html": false,
+ "layout": "IPY_MODEL_82f7314d49cb445481ab1f5a82fd31d6",
"layout_path": null,
- "max": 8000000.0,
- "min": 0.0,
- "position": "inline",
+ "placeholder": "",
+ "position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
- "step": 10000.0,
+ "style": "IPY_MODEL_4275d017a33a4f7d87a8ba63fca41b03",
"tabbable": null,
"tooltip": null,
- "url_key": "",
- "value": 3348969.0
+ "value": ""
}
},
- "72ecb74f5c3c41ff840ae316192edd30": {
+ "7c01b290a6594aa69680bdd3a4e74995": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -6583,7 +6665,7 @@
"width": null
}
},
- "8053b5723e0148d68de3b68e3487708e": {
+ "82f7314d49cb445481ab1f5a82fd31d6": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
@@ -6636,7 +6718,7 @@
"width": null
}
},
- "8234a1340dc541e698d035ab4dd7a4dc": {
+ "8b8b8d01f0ca46f39c16bd16b93a1475": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -6655,8 +6737,8 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "ASIA — contract termination",
- "layout": "IPY_MODEL_b94e124e2e6c48a9be808e9a5c3f51ce",
+ "label": "ANZ — contract termination",
+ "layout": "IPY_MODEL_6b1f795e977941baa1ca60f5f93bb458",
"layout_path": null,
"max": "",
"min": "",
@@ -6669,7 +6751,340 @@
"value": "2027-12-31"
}
},
- "8686e81af3af4348b1963ec0223daf8c": {
+ "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",
@@ -6693,7 +7108,7 @@
"disabled": false,
"hidden": false,
"label": "NPV discount rate",
- "layout": "IPY_MODEL_0def362b5cd2438fbd5cb52352defbd2",
+ "layout": "IPY_MODEL_9ee437efc7884fe9a1f870a0aead3e90",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
@@ -6704,74 +7119,60 @@
"value": "13.5% (deck)"
}
},
- "96c9d66e7f8a4da7b876af1a331c10fc": {
- "model_module": "anywidget",
- "model_module_version": "~0.11.*",
- "model_name": "AnyModel",
+ "c76324ffcf3e4df5a078d6b3fb6ac13c": {
+ "model_module": "@jupyter-widgets/base",
+ "model_module_version": "2.0.0",
+ "model_name": "LayoutModel",
"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",
+ "_model_module": "@jupyter-widgets/base",
+ "_model_module_version": "2.0.0",
+ "_model_name": "LayoutModel",
"_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_3e86dbe19c264dceabdca70642bd9e22",
- "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"
+ "_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
}
},
- "9987d9a9e53a4b679f82bf7f93944b8a": {
- "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_210a51183af8466cb6514a862e2333b3",
- "layout_path": null,
- "n_toggles": 0,
- "position": "sidebar",
- "render_slot_id": null,
- "source_cell_id": null,
- "tabbable": null,
- "tooltip": null,
- "url_key": "",
- "value": true
- }
- },
- "9d56a55a60aa4ddebd1ad3f5860f8658": {
+ "caae1a34df0945249b1a3d39320540b1": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -6793,7 +7194,7 @@
"hidden": false,
"label": "WFM — ANZ (implement)",
"last_changed_at": "",
- "layout": "IPY_MODEL_39bb3041c34a44398c4933f6f1a004f5",
+ "layout": "IPY_MODEL_c76324ffcf3e4df5a078d6b3fb6ac13c",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
@@ -6805,7 +7206,60 @@
"value": true
}
},
- "a3b57e4e572245bdb6f26b4d55011a3d": {
+ "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",
@@ -6825,9 +7279,9 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "WFM — EMEA (upside option)",
+ "label": "WFM — NA (migrate existing users)",
"last_changed_at": "",
- "layout": "IPY_MODEL_f481dfe5ada34986856ee9ab29a5ced2",
+ "layout": "IPY_MODEL_a3797b4763c8482f82c23bea6b27de8f",
"layout_path": null,
"n_toggles": 0,
"position": "sidebar",
@@ -6836,255 +7290,10 @@
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": false
+ "value": true
}
},
- "aff1d81cd3cd4168b6a133eed16f673c": {
- "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
- }
- },
- "b0780d60c5bd499b8d556a0bb73058f3": {
- "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
- }
- },
- "b0b406419cd3439da91ebcabfcb93a94": {
- "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
- }
- },
- "b71ef9d82870445b88c103eff878a1b5": {
- "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_dffad10ba9794be3a2497f87f445f0b7",
- "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"
- }
- },
- "b94e124e2e6c48a9be808e9a5c3f51ce": {
- "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
- }
- },
- "d41e8f15cfb54b9fa8732e5abd70cdaa": {
+ "f8196ba7d89c487eb23ad614f78881fe": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
@@ -7103,8 +7312,8 @@
"cell_id": "",
"disabled": false,
"hidden": false,
- "label": "EMEA — current platform cost ($/yr)",
- "layout": "IPY_MODEL_8053b5723e0148d68de3b68e3487708e",
+ "label": "ASIA — current platform cost ($/yr)",
+ "layout": "IPY_MODEL_1beaba7af87e4528abd06da3151e882f",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
@@ -7115,150 +7324,10 @@
"tabbable": null,
"tooltip": null,
"url_key": "",
- "value": 1204124.0
+ "value": 2069588.0
}
},
- "d731a700aec848699c2adaf537190046": {
- "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
- }
- },
- "dcb8281a29e34d3287e37ff6484dec79": {
- "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_b0b406419cd3439da91ebcabfcb93a94",
- "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
- }
- },
- "dffad10ba9794be3a2497f87f445f0b7": {
- "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
- }
- },
- "f481dfe5ada34986856ee9ab29a5ced2": {
+ "f9673c814e8148b1be991bcf12f12fff": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
diff --git a/studies/202512_GenesysCX/ctm-token-calculator/tests/test_appendix4.py b/studies/202512_GenesysCX/ctm-token-calculator/tests/test_appendix4.py
index f1ca39f..a07e98b 100644
--- a/studies/202512_GenesysCX/ctm-token-calculator/tests/test_appendix4.py
+++ b/studies/202512_GenesysCX/ctm-token-calculator/tests/test_appendix4.py
@@ -106,3 +106,10 @@ def test_case_flows_and_kpis():
inc2 = {y: -1.0 for y in a4.YEARS}
net2 = {y: benefits[y] + 1.0 for y in a4.YEARS}
assert a4.case_kpis(inc2, net2)["roi"] is None
+
+
+def test_contracted_overlays_verbatim():
+ assert a4.tco("ccaas_annual") == 3_200_000 # signed contract
+ assert a4.TCO_VERBATIM["ccaas_annual"] == 4_300_000 # deck record intact
+ assert a4.tco("current_annual") == a4.TCO_VERBATIM["current_annual"]
+ assert a4.licence_costs_by_year(12, a4.tco("ccaas_annual"))[2027] == 3_200_000
diff --git a/studies/202512_GenesysCX/ctm-token-calculator/tests/test_staging.py b/studies/202512_GenesysCX/ctm-token-calculator/tests/test_staging.py
new file mode 100644
index 0000000..209feb8
--- /dev/null
+++ b/studies/202512_GenesysCX/ctm-token-calculator/tests/test_staging.py
@@ -0,0 +1,15 @@
+"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
+
+from tokencalc import staging
+
+
+def test_backstage_prints_only_off_stage(monkeypatch, capsys):
+ monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
+ assert not staging.on_stage()
+ staging.backstage("visible")
+ assert capsys.readouterr().out == "visible\n"
+
+ monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
+ assert staging.on_stage()
+ staging.backstage("hidden")
+ assert capsys.readouterr().out == ""
diff --git a/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/appendix4.py b/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/appendix4.py
index 06e74ad..565f7d5 100644
--- a/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/appendix4.py
+++ b/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/appendix4.py
@@ -99,6 +99,19 @@ TCO_VERBATIM: dict[str, float] = {
"npv_discount_rate": 0.135, # deck's benefit-NPV rate
}
+#: Actual contracted values where they differ from the deck — 🟢
+#: contractual. Layered over TCO_VERBATIM, which stays the untouched
+#: record of what Genesys pitched (the anchor CTM can follow);
+#: presentation reads through :func:`tco`.
+TCO_CONTRACTED: dict[str, float] = {
+ "ccaas_annual": 3_200_000, # signed licence run-rate (deck pitched $4.3M/yr)
+}
+
+
+def tco(key: str) -> float:
+ """Contracted value where one exists, else the deck's verbatim anchor."""
+ return TCO_CONTRACTED.get(key, TCO_VERBATIM[key])
+
#: Genesys/Broadreach deployment schedule (slides 17-21), months from
#: Jan 2026 inclusive. Benefits realize IMPL + 3 months.
IMPL_MONTH = {"NA": 18, "ANZ": 21, "EMEA": 24, "ASIA": 27}
diff --git a/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/staging.py b/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/staging.py
new file mode 100644
index 0000000..2ec7bec
--- /dev/null
+++ b/studies/202512_GenesysCX/ctm-token-calculator/tokencalc/staging.py
@@ -0,0 +1,29 @@
+"""
+Stage vs backstage — is this notebook render stakeholder-facing?
+
+The Mercury CLI (``mercury --working-dir …``) exports ``MERCURY_CONFIG_DIR``
+into the server process so the widget library can locate ``config.toml``
+(see ``mercury/config.py``); every kernel that server spawns inherits it.
+JupyterLab and nbconvert kernels don't have it. That makes the variable a
+reliable signal for "the audience is looking" (the stage) versus an
+analyst session or a headless export run (backstage).
+
+Diagnostics routed through :func:`backstage` stay visible in JupyterLab
+and land in the nbconvert exports (where the machine-readable appendix
+must appear for LLM consumption) but never render in the Mercury app.
+"""
+
+from __future__ import annotations
+
+import os
+
+
+def on_stage() -> bool:
+ """True when running under the Mercury app (stakeholder-facing)."""
+ return os.getenv("MERCURY_CONFIG_DIR") is not None
+
+
+def backstage(*args, **kwargs) -> None:
+ """``print`` that renders only backstage (JupyterLab, nbconvert)."""
+ if not on_stage():
+ print(*args, **kwargs)