diff --git a/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_business_case_corrected.ipynb b/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_business_case_corrected.ipynb index 2a34e4c..cb1b654 100644 --- a/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_business_case_corrected.ipynb +++ b/studies/202512_GenesysCX/ctm-token-calculator/notebooks/ctm_business_case_corrected.ipynb @@ -16,7 +16,7 @@ "| Layer | Source | Treatment |\n", "|---|---|---|\n", "| Benefits | *Appendix 4 — CCaaS Platform Benefit Calculations (Consolidated).pptx*, slides 5–21 | **Verbatim** — phased by Genesys's own deployment schedule, scaled to the deck's 3-yr totals |\n", - "| Base costs | Same deck (TCO slides 5–6) | Verbatim, plus the Genesys ramp programme (12 months licence-free) |\n", + "| Base costs | Same deck (TCO slides 5–6) + signed contract | Deck verbatim, plus the ramp programme; licences at the **contracted $3.2M/yr** (deck pitched $4.3M) |\n", "| AI token consumption | `tokencalc` engine, published Genesys meter rates | **Added** — missing cost #1 |\n", "| AI implementation effort | `docs/ctm_ai_labour_estimate_V2.md` | **Added** — missing cost #2 |\n", "| Existing platform run-off | Term contracts to 31 Dec 2027, per-region | **Added** — missing cost #3 (double-billing) |\n", @@ -36,10 +36,10 @@ "id": "26632e8d", "metadata": { "execution": { - "iopub.execute_input": "2026-07-08T09:51:39.653726Z", - "iopub.status.busy": "2026-07-08T09:51:39.653487Z", - "iopub.status.idle": "2026-07-08T09:51:40.129224Z", - "shell.execute_reply": "2026-07-08T09:51:40.128247Z" + "iopub.execute_input": "2026-07-08T12:20:38.848850Z", + "iopub.status.busy": "2026-07-08T12:20:38.848661Z", + "iopub.status.idle": "2026-07-08T12:20:39.301104Z", + "shell.execute_reply": "2026-07-08T12:20:39.300310Z" } }, "outputs": [ @@ -82,9 +82,10 @@ " crossfoot_tolerance, current_costs_by_year, current_months_in_year,\n", " current_state_inputs, hours_pick, impl_feature_regions,\n", " licence_costs_by_year, money, html_money, month_label,\n", - " ps_costs_by_year, region_agents, region_site_names, token_costs_by_year,\n", + " ps_costs_by_year, region_agents, region_site_names, tco, token_costs_by_year,\n", " verbatim_dataframe,\n", ")\n", + "from tokencalc.staging import backstage\n", "\n", "pd.options.display.float_format = \"{:,.0f}\".format\n", "\n", @@ -104,7 +105,7 @@ " \"Supervisor Copilot\": \"#e34948\", # red\n", "}\n", "COST_COLOR = {\n", - " \"CCaaS platform licences (ramp-adjusted)\": \"#2a78d6\",\n", + " \"CCaaS platform licences (contracted, ramp-adjusted)\": \"#2a78d6\",\n", " \"Base professional services + training\": \"#1baf7a\",\n", " \"Existing platform (term-contract run-off)\": \"#eda100\",\n", " \"AI token consumption\": \"#008300\",\n", @@ -148,7 +149,7 @@ " hovertemplate=\"%{fullData.name}: %{y:$,.0f}\")\n", "\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:,})\")" ] @@ -159,22 +160,22 @@ "id": "7b33a037", "metadata": { "execution": { - "iopub.execute_input": "2026-07-08T09:51:40.131312Z", - "iopub.status.busy": "2026-07-08T09:51:40.131093Z", - "iopub.status.idle": "2026-07-08T09:51:40.144098Z", - "shell.execute_reply": "2026-07-08T09:51:40.143408Z" + "iopub.execute_input": "2026-07-08T12:20:39.304845Z", + "iopub.status.busy": "2026-07-08T12:20:39.304602Z", + "iopub.status.idle": "2026-07-08T12:20:39.316583Z", + "shell.execute_reply": "2026-07-08T12:20:39.315968Z" } }, "outputs": [ { "data": { "application/mercury+json": { - "model_id": "8bfe6fed4f5a4807a9fe4297af1c998a", + "model_id": "12c27b75c74a4785aac38abca3d50a0a", "position": "sidebar", "widget": "MarkdownWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "8bfe6fed4f5a4807a9fe4297af1c998a", + "model_id": "12c27b75c74a4785aac38abca3d50a0a", "version_major": 2, "version_minor": 0 }, @@ -231,7 +232,9 @@ "\n", "Verbatim anchors (Appendix 4, slide 5–6): current solution costs **$7.3M/yr globally**\n", "(ANZ, APAC, EMEA, NA — $22M over 3 years); CCaaS is **$4.3M/yr** with **$2.4M professional\n", - "services + $167K training in year 1**.\n", + "services + $167K training in year 1**. The **contracted** licence run-rate is\n", + "**$3.2M/yr** (🟢 signed) — the corrected stack runs on it; the as-pitched stack keeps\n", + "the deck's $4.3M so the comparison stays anchored to the Genesys report.\n", "\n", "Three contract facts the deck's cost case ignores, collected here as editable inputs:\n", "\n", @@ -240,7 +243,7 @@ "2. **Contract termination dates** — the current platforms are **term contracts**; CTM pays until\n", " termination (default **31 Dec 2027**) regardless of when Genesys goes live → double-billing.\n", "3. **Genesys ramp programme** — `RAMP_MONTHS = 12`: licences are free for the first 12 months,\n", - " so the $4.3M/yr commit bills from month 13." + " so the licence commit bills from month 13." ] }, { @@ -249,27 +252,27 @@ "id": "7e4e12ce", "metadata": { "execution": { - "iopub.execute_input": "2026-07-08T09:51:40.146666Z", - "iopub.status.busy": "2026-07-08T09:51:40.146467Z", - "iopub.status.idle": "2026-07-08T09:51:40.180219Z", - "shell.execute_reply": "2026-07-08T09:51:40.179090Z" + "iopub.execute_input": "2026-07-08T12:20:39.322355Z", + "iopub.status.busy": "2026-07-08T12:20:39.322221Z", + "iopub.status.idle": "2026-07-08T12:20:39.365143Z", + "shell.execute_reply": "2026-07-08T12:20:39.364274Z" } }, "outputs": [ { "data": { "application/mercury+json": { - "model_id": "ae3eef3c182b46d5ac570e574c0fa782", + "model_id": "c87c3137444e4dd69a6f46b74170270d", "position": "inline", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "ae3eef3c182b46d5ac570e574c0fa782", + "model_id": "c87c3137444e4dd69a6f46b74170270d", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -278,17 +281,17 @@ { "data": { "application/mercury+json": { - "model_id": "ac3e662f8eb046debbeef1783e5e0a03", + "model_id": "2912bf24d4e645d3bd78bc5c1b4953b5", "position": "inline", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "ac3e662f8eb046debbeef1783e5e0a03", + "model_id": "2912bf24d4e645d3bd78bc5c1b4953b5", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -297,17 +300,36 @@ { "data": { "application/mercury+json": { - "model_id": "6cdbf07eb87c4e09b0de8ee9c2bd2afe", + "model_id": "dcabb5e69fb94e829297689c7a6ae40a", + "position": "inline", + "widget": "NumberInputWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "dcabb5e69fb94e829297689c7a6ae40a", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "87d58807331b444b831a9611af9b9b97", "position": "inline", "widget": "DateInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "6cdbf07eb87c4e09b0de8ee9c2bd2afe", + "model_id": "87d58807331b444b831a9611af9b9b97", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -316,17 +338,17 @@ { "data": { "application/mercury+json": { - "model_id": "2a33e86c1bbb4747a29fdcc44844f5a0", + "model_id": "4666a3204ec84afba77fcbed27edae43", "position": "inline", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "2a33e86c1bbb4747a29fdcc44844f5a0", + "model_id": "4666a3204ec84afba77fcbed27edae43", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -335,17 +357,17 @@ { "data": { "application/mercury+json": { - "model_id": "2e45dcf72b104082a58763592b715a55", + "model_id": "70b68a62010b4fef96d9117d387ed629", "position": "inline", "widget": "DateInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "2e45dcf72b104082a58763592b715a55", + "model_id": "70b68a62010b4fef96d9117d387ed629", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -354,17 +376,17 @@ { "data": { "application/mercury+json": { - "model_id": "cbfcf1a55a2346bfbfc433fdaebdfd76", + "model_id": "1a52eaf6b29447c1b8aa1db54a4e56b8", "position": "inline", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "cbfcf1a55a2346bfbfc433fdaebdfd76", + "model_id": "1a52eaf6b29447c1b8aa1db54a4e56b8", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -373,17 +395,17 @@ { "data": { "application/mercury+json": { - "model_id": "3b809f57362449818455b7a7df410ba1", + "model_id": "0bfe75e3708140a49973b3af4ddda41a", "position": "inline", "widget": "DateInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "3b809f57362449818455b7a7df410ba1", + "model_id": "0bfe75e3708140a49973b3af4ddda41a", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -392,17 +414,17 @@ { "data": { "application/mercury+json": { - "model_id": "f56d7388533948518dca55aeb6efb4bb", + "model_id": "c4d6af10d9554e709089a8fb228f0f1d", "position": "inline", "widget": "NumberInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "f56d7388533948518dca55aeb6efb4bb", + "model_id": "c4d6af10d9554e709089a8fb228f0f1d", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -411,17 +433,17 @@ { "data": { "application/mercury+json": { - "model_id": "2302d16d822548e291cc7fbf6657c65f", + "model_id": "ddf1294225004414bf666eb493358f85", "position": "inline", "widget": "DateInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "2302d16d822548e291cc7fbf6657c65f", + "model_id": "ddf1294225004414bf666eb493358f85", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "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)