{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5f5ffd6a",
   "metadata": {},
   "source": [
    "# CTM × Genesys CCaaS — Business Case (Current Spend Excluded)\n",
    "\n",
    "**Thesis:** the Genesys/Broadreach business case presents **$13.6M annual / $15.0M 3-year benefits**\n",
    "against a cost case of **$4.3M/yr CCaaS licences + $2.4M professional services + $167K training** —\n",
    "but every claimed benefit is produced by AI capabilities whose **token consumption** and\n",
    "**implementation effort** are absent from that cost case. Per CTM's direction this notebook\n",
    "**excludes any consideration of current spend** — no do-nothing baseline, no double-billing,\n",
    "no cost-avoidance credit. It keeps Genesys's benefits **verbatim** and judges the programme on\n",
    "its **absolute** cost: net = Genesys benefits − full programme cost.\n",
    "\n",
    "| 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) + 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 | — | **Excluded** — current spend carries no weight (CTM directive) |\n",
    "\n",
    "Confidence legend: 🟢 confirmed (published/contractual) · 🟡 estimated (working assumption) · 🔴 unknown.\n",
    "\n",
    "*Frame note: the standalone Genesys programme is thin without a current-spend tailwind — the **virtual-agents alternative** (`ctm_business_case_virtual_agents.ipynb`) is the companion case that carries the ROI. AI implementation uses the V2 hours-range × blended-rate model; the activity-level\n",
    "`ImplementationEffort` engine sketched in the labour doc is deferred (see section 11). Timeline = 2026–2028,\n",
    "contract start Jan 2026; schedule per the current PM delivery timeline (NA UAT Oct 2026 … last go-live Nov 2027).*\n",
    "\n",
    "*This notebook is the deliverable: serve it interactively with `mercury --working-dir notebooks/`, tune the 🟡 inputs live for the client, then export an LLM-readable report source with `python scripts/export_report.py`.*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "26632e8d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:09.753197Z",
     "iopub.status.busy": "2026-07-13T22:00:09.752920Z",
     "iopub.status.idle": "2026-07-13T22:00:10.296464Z",
     "shell.execute_reply": "2026-07-13T22:00:10.295587Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tokencalc loaded — 9 sites, 2,088 named users (contracted: 2,088)\n"
     ]
    }
   ],
   "source": [
    "# ── Setup ──────────────────────────────────────────────────────────\n",
    "import sys, pathlib\n",
    "_ROOT = pathlib.Path.cwd()\n",
    "if not (_ROOT / \"tokencalc\").exists():           # notebook lives in notebooks/\n",
    "    _ROOT = _ROOT.parent\n",
    "sys.path.insert(0, str(_ROOT))\n",
    "\n",
    "import dataclasses\n",
    "import datetime as dt\n",
    "import math\n",
    "\n",
    "import pandas as pd\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "import mercury as mr\n",
    "\n",
    "from tokencalc import *\n",
    "# Single source of truth for the corrected case — all math lives in the\n",
    "# library; only presentation (and Mercury input widgets) lives here.\n",
    "from tokencalc.appendix4 import (\n",
    "    YEARS, YEAR_INDEX, REGIONS, CAPABILITIES,\n",
    "    VERBATIM_BENEFITS, SLIDE_TOTALS, TCO_VERBATIM,\n",
    "    IMPL_MONTH, REALIZE_MONTH, BENEFIT_LAG_MONTHS, NA_EMAIL_IMPL_MONTH,\n",
    "    AI_IMPL_HOURS, KB_READINESS_HOURS, STEADY_STATE_HOURS,\n",
    "    DEFAULT_RAMP_MONTHS, SMELL_TEST_FLOOR,\n",
    "    autorespond_meter, benefits_by_year, build_impl_costs, build_rollouts,\n",
    "    build_scopes, case_flows, case_kpis, claim_scenario,\n",
    "    crossfoot_tolerance, current_costs_by_year, current_months_in_year,\n",
    "    current_state_inputs, hours_pick, impl_feature_regions,\n",
    "    MANAGED_SERVICES_ANNUAL, MCX_GO_LIVE, PS_CONTRACTED_TOTAL, PS_MILESTONES,\n",
    "    licence_costs_by_year, managed_services_by_year, money, html_money,\n",
    "    month_label, ps_costs_by_year, ps_milestones_dataframe, region_agents,\n",
    "    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",
    "# ── Chart chrome (dataviz reference palette, light surface) ─────────\n",
    "INK, INK2, MUTED = \"#0b0b0b\", \"#52514e\", \"#898781\"\n",
    "SURFACE, GRID, BASELINE = \"#fcfcfb\", \"#e1e0d9\", \"#c3c2b7\"\n",
    "CUMULATIVE, CONTEXT = \"#52514e\", \"#c3c2b7\"   # neutral line; de-emphasized context series\n",
    "FONT_STACK = 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n",
    "\n",
    "# Fixed capability colors — color follows the entity across every figure.\n",
    "CAP_COLOR = {\n",
    "    \"Agent Copilot\": \"#2a78d6\",       # blue\n",
    "    \"WFM\": \"#1baf7a\",                 # aqua\n",
    "    \"Email\": \"#eda100\",               # yellow (sub-3:1 → relief via total labels + tables)\n",
    "    \"STA\": \"#008300\",                 # green\n",
    "    \"Predictive Routing\": \"#4a3aa7\",  # violet\n",
    "    \"Supervisor Copilot\": \"#e34948\",  # red\n",
    "}\n",
    "COST_COLOR = {\n",
    "    \"CCaaS platform licences (contracted, ramp-adjusted)\": \"#2a78d6\",\n",
    "    \"Base professional services + training\": \"#1baf7a\",\n",
    "    \"NTT PS + training (SOW billing milestones)\": \"#1baf7a\",\n",
    "    \"NTT managed services (from MCX go-live)\": \"#0b6b4a\",\n",
    "    \"Existing platform (term-contract run-off)\": \"#eda100\",\n",
    "    \"AI token consumption\": \"#008300\",\n",
    "    \"AI implementation + KB readiness\": \"#4a3aa7\",\n",
    "    \"AI steady-state tuning\": \"#e34948\",\n",
    "}\n",
    "SEQ_BLUES = [\"#cde2fb\", \"#b7d3f6\", \"#9ec5f4\", \"#86b6ef\", \"#6da7ec\", \"#5598e7\",\n",
    "             \"#3987e5\", \"#2a78d6\", \"#256abf\", \"#1c5cab\", \"#184f95\", \"#104281\", \"#0d366b\"]\n",
    "\n",
    "\n",
    "def tei_layout(fig, title, subtitle=None, height=460):\n",
    "    t = f\"<b>{title}</b>\"\n",
    "    if subtitle:\n",
    "        t += f\"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>\"\n",
    "    fig.update_layout(\n",
    "        title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor=\"left\"),\n",
    "        paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n",
    "        font=dict(family=FONT_STACK, size=12, color=INK2),\n",
    "        legend=dict(orientation=\"h\", yanchor=\"top\", y=-0.10, x=0,\n",
    "                    font=dict(size=11, color=INK2)),\n",
    "        xaxis=dict(type=\"category\", showgrid=False, linecolor=BASELINE,\n",
    "                   tickfont=dict(color=MUTED)),\n",
    "        yaxis=dict(gridcolor=GRID, zerolinecolor=BASELINE, zerolinewidth=1.5,\n",
    "                   tickformat=\"$~s\", tickfont=dict(color=MUTED)),\n",
    "        hovermode=\"x unified\", bargap=0.45, height=height,\n",
    "        margin=dict(t=70, r=30, b=80, l=70),\n",
    "    )\n",
    "    return fig\n",
    "\n",
    "\n",
    "def bar(x, y, name, color):\n",
    "    return go.Bar(x=x, y=y, name=name,\n",
    "                  marker=dict(color=color, line=dict(width=2, color=SURFACE)),\n",
    "                  hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
    "\n",
    "\n",
    "def cum_line(x, y, name, color=CUMULATIVE, dash=None):\n",
    "    return go.Scatter(x=x, y=y, name=name, mode=\"lines+markers\",\n",
    "                      line=dict(color=color, width=2, dash=dash),\n",
    "                      marker=dict(size=8, line=dict(width=2, color=SURFACE)),\n",
    "                      hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
    "\n",
    "\n",
    "backstage(f\"tokencalc loaded — {len(CTM_DEFAULT_SITES)} sites, \"\n",
    "      f\"{sum(s.named_users for s in CTM_DEFAULT_SITES):,} named users \"\n",
    "      f\"(contracted: {CONTRACTED_NAMED_USERS:,})\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "7b33a037",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.299016Z",
     "iopub.status.busy": "2026-07-13T22:00:10.298717Z",
     "iopub.status.idle": "2026-07-13T22:00:10.315415Z",
     "shell.execute_reply": "2026-07-13T22:00:10.314637Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "fbc4f6246d0d4b36852ad6ad5f252d84",
       "position": "sidebar",
       "widget": "MarkdownWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "fbc4f6246d0d4b36852ad6ad5f252d84",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "MarkdownWidget(value='<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, …"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Jump-to-section ToC (Mercury sidebar — widgets only, no output) ──\n",
    "# Fragment links don't scroll inside Mercury's app shell (SPA base URL),\n",
    "# so each entry scrolls via JS: anchor id first, heading-text fallback.\n",
    "_TOC = [\n",
    "    (1, \"Current state & contracts\"),\n",
    "    (2, \"Verbatim benefits & schedule\"),\n",
    "    (3, \"Token consumption\"),\n",
    "    (4, \"AI implementation effort\"),\n",
    "    (5, \"Corrected cost stack\"),\n",
    "    (6, \"Fig 1 — Benefits\"),\n",
    "    (7, \"Fig 2 — Costs\"),\n",
    "    (8, \"Fig 3 — Business case\"),\n",
    "    (9, \"Sensitivity\"),\n",
    "    (10, \"Verification & assertions\"),\n",
    "    (11, \"Risks & next steps\"),\n",
    "    (12, \"Data appendix\"),\n",
    "]\n",
    "# NB: no raw \"<\" allowed inside the handler — python-markdown escapes the\n",
    "# whole tag if the attribute text looks like malformed HTML.\n",
    "_JS = (\"var el=document.getElementById('section-{n}');\"\n",
    "       \"if(!el){{document.querySelectorAll('h1,h2').forEach(function(h){{\"\n",
    "       \"if(!el&&h.textContent.trim().indexOf('{n} ')===0){{el=h;}}}});}}\"\n",
    "       \"if(el)el.scrollIntoView({{behavior:'smooth',block:'start'}});\")\n",
    "_items = \"\".join(\n",
    "    f'<li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline\"'\n",
    "    f' onclick=\"{_JS.format(n=n)}\">{label}</a></li>'\n",
    "    for n, label in _TOC)\n",
    "_toc = mr.Markdown(\n",
    "    text=(f'<b>Jump to section</b>'\n",
    "          f'<ol style=\"padding-left:1.2em;margin:6px 0\">{_items}</ol>'),\n",
    "    position=\"sidebar\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fda63800",
   "metadata": {},
   "source": [
    "<a id=\"section-1\"></a>\n",
    "\n",
    "## 1 · Contract inputs — data collection (current spend excluded)\n",
    "\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**. 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. The signed NTT\n",
    "SOW also fixes **PS at $2.03M over four billing milestones (2026–27)** and adds **managed\n",
    "services at $411K/yr from MCX go-live (30 Sep 2026)** — the corrected stack bills both as\n",
    "contracted; the as-pitched stack keeps the deck's year-1 PS lump.\n",
    "\n",
    "**CTM directive:** current spend is *excluded* from this case. The per-region current-platform table below is retained **for reference only** and is forced to **$0** downstream — there is no double-billing line and no cost-avoidance credit. Two contract facts the deck's cost case ignores are still collected here as editable inputs:\n",
    "\n",
    "1. **Genesys ramp programme** — `RAMP_MONTHS = 6` (🟢 order form): licences are free for\n",
    "   the first 6 months, so the licence commit bills from July 2026.\n",
    "2. **NTT SOW milestones + managed services** — PS billed on the SOW milestones (PM timeline:\n",
    "   Mar 2026 / Oct 2026 / Oct 2027 / Nov 2027) and managed services ($411K/yr) from MCX go-live\n",
    "   (16 Oct 2026).\n",
    "\n",
    "*(The current-platform table stays visible so real regional figures can be dropped in later if\n",
    "CTM ever wants the cost-avoidance frame back — but it does not affect any number in this case.)*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "7e4e12ce",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.317817Z",
     "iopub.status.busy": "2026-07-13T22:00:10.317550Z",
     "iopub.status.idle": "2026-07-13T22:00:10.360840Z",
     "shell.execute_reply": "2026-07-13T22:00:10.359993Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "4f18a5dd5d4245a9b7558564825d4a09",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4f18a5dd5d4245a9b7558564825d4a09",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3b0c27fe00>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "4f1b8f8777ef43deb835fe53c4dd2994",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4f1b8f8777ef43deb835fe53c4dd2994",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3b0c0487d0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "27e26a695a9d48c9bae29e3402ae1132",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "27e26a695a9d48c9bae29e3402ae1132",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaede3b10>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "6ada443fa3844d198bc69221d8867bbc",
       "position": "inline",
       "widget": "DateInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6ada443fa3844d198bc69221d8867bbc",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.date.DateInputWidget object at 0x7f3b0c27fcb0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "bb1cf20444e2490597fd26df35490de9",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "bb1cf20444e2490597fd26df35490de9",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaec37820>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "7971c319317c4e90bdfe69607c9e0de7",
       "position": "inline",
       "widget": "DateInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "7971c319317c4e90bdfe69607c9e0de7",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.date.DateInputWidget object at 0x7f3b0c049a90>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "7013bd8d97d040cbb8172345f8a2829b",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "7013bd8d97d040cbb8172345f8a2829b",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaec36780>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "fd23a1e2c3f84fd4ba4803b26fdec9ff",
       "position": "inline",
       "widget": "DateInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "fd23a1e2c3f84fd4ba4803b26fdec9ff",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.date.DateInputWidget object at 0x7f3b0c04a350>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "1ebe071b962145cdbed25a1ae3dc8ccb",
       "position": "inline",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "1ebe071b962145cdbed25a1ae3dc8ccb",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaec796d0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "336a0118e33249788ad6521ecbba63d9",
       "position": "inline",
       "widget": "DateInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "336a0118e33249788ad6521ecbba63d9",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.date.DateInputWidget object at 0x7f3aaec37950>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Contract inputs (model: tokencalc.appendix4) ─────────────────────\n",
    "# ⚙ Interactive when served with Mercury; headless runs (nbconvert /\n",
    "#   regression gate) use the widget defaults below.\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
    "# so this cell constructs widgets ONLY — .value is read downstream.\n",
    "_ramp_w = mr.NumberInput(\n",
    "    label=\"Genesys ramp — licence-free months\", value=DEFAULT_RAMP_MONTHS,\n",
    "    min=0, max=24, step=1, position=\"inline\")\n",
    "_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",
    "ASIA_SITES = [n for n in ALL_SITES if n.startswith(\"APAC\")]\n",
    "REGION_SITES = region_site_names(sites)\n",
    "agents_by_region = region_agents(sites)\n",
    "\n",
    "# ── Per-region current-state inputs (EDIT as real data arrives) ──────\n",
    "# Seeded as $7.3M × agent share; annual_cost and contract_termination\n",
    "# are the editable inputs.\n",
    "_seed = current_state_inputs(sites)\n",
    "assert abs(_seed[\"annual_cost\"].sum() - TCO_VERBATIM[\"current_annual\"]) < 1\n",
    "\n",
    "_cost_w, _term_w = {}, {}\n",
    "for _r in REGIONS:\n",
    "    _cost_w[_r] = mr.NumberInput(\n",
    "        label=f\"{_r} — current platform cost ($/yr)\",\n",
    "        value=round(float(_seed.loc[_r, \"annual_cost\"])),\n",
    "        min=0, max=8_000_000, step=10_000, position=\"inline\")\n",
    "    _term_w[_r] = mr.DateInput(\n",
    "        label=f\"{_r} — contract termination\",\n",
    "        value=_seed.loc[_r, \"contract_termination\"].isoformat(),\n",
    "        position=\"inline\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "52d1e89f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.363655Z",
     "iopub.status.busy": "2026-07-13T22:00:10.363368Z",
     "iopub.status.idle": "2026-07-13T22:00:10.385562Z",
     "shell.execute_reply": "2026-07-13T22:00:10.384873Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "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",
      "licence run-rate: contracted $3.2M/yr (deck pitched $4.3M/yr — as-pitched stack keeps it)\n",
      "ramp-adjusted licences by year: {2026: '$3.2M', 2027: '$3.2M', 2028: '$3.2M'}\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>agents</th>\n",
       "      <th>share</th>\n",
       "      <th>annual_cost</th>\n",
       "      <th>contract_termination</th>\n",
       "      <th>confidence</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>region</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>NA</th>\n",
       "      <td>890</td>\n",
       "      <td>0</td>\n",
       "      <td>3,348,969</td>\n",
       "      <td>2027-12-31</td>\n",
       "      <td>🟡 agent-share allocation of the verbatim total</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ANZ</th>\n",
       "      <td>180</td>\n",
       "      <td>0</td>\n",
       "      <td>677,320</td>\n",
       "      <td>2027-12-31</td>\n",
       "      <td>🟡 agent-share allocation of the verbatim total</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>EMEA</th>\n",
       "      <td>320</td>\n",
       "      <td>0</td>\n",
       "      <td>1,204,124</td>\n",
       "      <td>2027-12-31</td>\n",
       "      <td>🟡 agent-share allocation of the verbatim total</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ASIA</th>\n",
       "      <td>550</td>\n",
       "      <td>0</td>\n",
       "      <td>2,069,588</td>\n",
       "      <td>2027-12-31</td>\n",
       "      <td>🟡 agent-share allocation of the verbatim total</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        agents  share  annual_cost contract_termination  \\\n",
       "region                                                    \n",
       "NA         890      0    3,348,969           2027-12-31   \n",
       "ANZ        180      0      677,320           2027-12-31   \n",
       "EMEA       320      0    1,204,124           2027-12-31   \n",
       "ASIA       550      0    2,069,588           2027-12-31   \n",
       "\n",
       "                                            confidence  \n",
       "region                                                  \n",
       "NA      🟡 agent-share allocation of the verbatim total  \n",
       "ANZ     🟡 agent-share allocation of the verbatim total  \n",
       "EMEA    🟡 agent-share allocation of the verbatim total  \n",
       "ASIA    🟡 agent-share allocation of the verbatim total  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>data item</th>\n",
       "      <th>status</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Per-region current-platform contract values</td>\n",
       "      <td>— reference only; excluded from the case</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Actual termination / renewal dates per region</td>\n",
       "      <td>— excluded from this case (current spend zeroed)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Genesys ramp period in the actual order form</td>\n",
       "      <td>🟢 6 months per order form</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Contracted licence run-rate</td>\n",
       "      <td>🟢 $3.2M/yr signed (deck pitched $4.3M/yr)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>NTT PS billing milestones (SOW)</td>\n",
       "      <td>🟢 $2.03M over four milestones (PM timeline)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>NTT managed services</td>\n",
       "      <td>🟢 $410,918/yr from MCX go-live (16 Oct 2026)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Contracted token rate (vs $1.00 US list)</td>\n",
       "      <td>🔴 not sourced — list rate used</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Non-NAM site volumes &amp; AHTs</td>\n",
       "      <td>🟡 tokencalc placeholders (defaults.py warning)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Early-termination / co-term options on NICE IE...</td>\n",
       "      <td>🔴 unknown</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                           data item  \\\n",
       "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 licence run-rate   \n",
       "4                    NTT PS billing milestones (SOW)   \n",
       "5                               NTT managed services   \n",
       "6           Contracted token rate (vs $1.00 US list)   \n",
       "7                        Non-NAM site volumes & AHTs   \n",
       "8  Early-termination / co-term options on NICE IE...   \n",
       "\n",
       "                                             status  \n",
       "0          — reference only; excluded from the case  \n",
       "1  — excluded from this case (current spend zeroed)  \n",
       "2                         🟢 6 months per order form  \n",
       "3         🟢 $3.2M/yr signed (deck pitched $4.3M/yr)  \n",
       "4       🟢 $2.03M over four milestones (PM timeline)  \n",
       "5      🟢 $410,918/yr from MCX go-live (16 Oct 2026)  \n",
       "6                    🔴 not sourced — list rate used  \n",
       "7    🟡 tokencalc placeholders (defaults.py warning)  \n",
       "8                                         🔴 unknown  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Contract values → model (re-runs on any change to the widgets above) ─\n",
    "RAMP_MONTHS = int(_ramp_w.value)\n",
    "LICENCE_ANNUAL = float(_licence_w.value)\n",
    "\n",
    "current_state = _seed.copy()\n",
    "for _r in REGIONS:\n",
    "    current_state.loc[_r, \"annual_cost\"] = float(_cost_w[_r].value)\n",
    "    current_state.loc[_r, \"contract_termination\"] = dt.date.fromisoformat(\n",
    "        _term_w[_r].value)\n",
    "\n",
    "current_by_year = current_costs_by_year(current_state)\n",
    "# ── CTM directive: EXCLUDE current spend from this business case ──────\n",
    "# The programme is judged on its own absolute cost vs Genesys benefits —\n",
    "# no do-nothing baseline, no double-billing, no cost-avoidance credit.\n",
    "# The current-state table above is retained for reference only; it is\n",
    "# forced to $0 here so it carries NO weight downstream.\n",
    "current_by_year = {y: 0.0 for y in YEARS}\n",
    "# Licences are INVOICED in full from year 1 ($3.2M), even though the\n",
    "# order-form ramp gives free months operationally — CTM's invoice does\n",
    "# not net the ramp down. Bill the full contracted annual every year.\n",
    "licence_by_year = {y: LICENCE_ANNUAL for y in YEARS}\n",
    "\n",
    "backstage(f\"current-state total {money(current_state['annual_cost'].sum())}/yr \"\n",
    "      f\"(seed: {money(TCO_VERBATIM['current_annual'])}; deck 3-yr \"\n",
    "      f\"{money(TCO_VERBATIM['current_3yr'])} — deck rounding)\")\n",
    "backstage(f\"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\"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",
    "# ── Remaining data-collection checklist ──────────────────────────────\n",
    "display(pd.DataFrame([\n",
    "    (\"Per-region current-platform contract values\", \"— reference only; excluded from the case\"),\n",
    "    (\"Actual termination / renewal dates per region\", \"— excluded from this case (current spend zeroed)\"),\n",
    "    (\"Genesys ramp period in the actual order form\", \"🟢 6 months per order form\"),\n",
    "    (\"Contracted licence run-rate\", \"🟢 $3.2M/yr signed (deck pitched $4.3M/yr)\"),\n",
    "    (\"NTT PS billing milestones (SOW)\", \"🟢 $2.03M over four milestones (PM timeline)\"),\n",
    "    (\"NTT managed services\", \"🟢 $410,918/yr from MCX go-live (16 Oct 2026)\"),\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",
    "], columns=[\"data item\", \"status\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c74e449",
   "metadata": {},
   "source": [
    "<a id=\"section-2\"></a>\n",
    "\n",
    "## 2 · Verbatim Genesys benefits & deployment schedule\n",
    "\n",
    "Benefits are taken **verbatim** from Appendix 4 slides 12–15 (per-region × capability, annual and\n",
    "3-yr values). The deck gives no per-year split, so each cell's 3-yr value is **phased by Genesys's\n",
    "own deployment schedule** (slides 17–21): implementation months NA=18, ANZ=21, EMEA=24, ASIA=27,\n",
    "and *\"business benefits start 3 months after implementation\"* — realization Sep 2027 / Dec 2027 /\n",
    "Mar 2028 / Jun 2028. Exception: the NA Gantt shows **Email realizing Apr 2027** (implemented month 13).\n",
    "\n",
    "**Month convention:** `RolloutPlan.go_live_month = m` means active from calendar month *m+1*; the\n",
    "deck's labels are inclusive (NA \"realizes Sep 2027\" ⇒ September counts), so rollout keys are set to\n",
    "*label − 1*. Deck-implied realization windows differ from its own schedule by ±1 month in places —\n",
    "the per-cell scaling below absorbs that; don't \"fix\" it.\n",
    "\n",
    "The result: **benefits in 2026 are $0 — by Genesys's own schedule.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c7e86e80",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.387332Z",
     "iopub.status.busy": "2026-07-13T22:00:10.387171Z",
     "iopub.status.idle": "2026-07-13T22:00:10.435879Z",
     "shell.execute_reply": "2026-07-13T22:00:10.435044Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cross-foot OK — cell grain sums to $15.0M 3-yr / $13.7M annual (deck headline: $15.0M / $13.6M — deck rounding)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th>region</th>\n",
       "      <th>ANZ</th>\n",
       "      <th>ASIA</th>\n",
       "      <th>EMEA</th>\n",
       "      <th>NA</th>\n",
       "      <th>TOTAL</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>capability</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Agent Copilot</th>\n",
       "      <td>3900000</td>\n",
       "      <td>0</td>\n",
       "      <td>64000</td>\n",
       "      <td>3400000</td>\n",
       "      <td>7364000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>WFM</th>\n",
       "      <td>1400000</td>\n",
       "      <td>914000</td>\n",
       "      <td>687000</td>\n",
       "      <td>0</td>\n",
       "      <td>3001000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Email</th>\n",
       "      <td>143000</td>\n",
       "      <td>93000</td>\n",
       "      <td>235000</td>\n",
       "      <td>2500000</td>\n",
       "      <td>2971000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>STA</th>\n",
       "      <td>105000</td>\n",
       "      <td>72000</td>\n",
       "      <td>131000</td>\n",
       "      <td>506000</td>\n",
       "      <td>814000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Predictive Routing</th>\n",
       "      <td>302000</td>\n",
       "      <td>51000</td>\n",
       "      <td>6000</td>\n",
       "      <td>167000</td>\n",
       "      <td>526000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Supervisor Copilot</th>\n",
       "      <td>27000</td>\n",
       "      <td>0</td>\n",
       "      <td>49000</td>\n",
       "      <td>291000</td>\n",
       "      <td>367000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td>5877000</td>\n",
       "      <td>1130000</td>\n",
       "      <td>1172000</td>\n",
       "      <td>6864000</td>\n",
       "      <td>15043000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "region                  ANZ     ASIA     EMEA       NA     TOTAL\n",
       "capability                                                      \n",
       "Agent Copilot       3900000        0    64000  3400000   7364000\n",
       "WFM                 1400000   914000   687000        0   3001000\n",
       "Email                143000    93000   235000  2500000   2971000\n",
       "STA                  105000    72000   131000   506000    814000\n",
       "Predictive Routing   302000    51000     6000   167000    526000\n",
       "Supervisor Copilot    27000        0    49000   291000    367000\n",
       "TOTAL               5877000  1130000  1172000  6864000  15043000"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── VERBATIM Appendix 4 slides 12-15 (data: tokencalc.appendix4) ─────\n",
    "verbatim = verbatim_dataframe()\n",
    "\n",
    "# Deck rounds to $0.1M — its own tables cross-foot ±$50-120K.\n",
    "_tol = crossfoot_tolerance\n",
    "\n",
    "for r, expect in SLIDE_TOTALS[\"regional_3yr\"].items():\n",
    "    got = verbatim.loc[verbatim.region == r, \"three_yr\"].sum()\n",
    "    assert abs(got - expect) <= _tol(expect), (r, got, expect)\n",
    "for c, expect in SLIDE_TOTALS[\"capability_3yr\"].items():\n",
    "    got = verbatim.loc[verbatim.capability == c, \"three_yr\"].sum()\n",
    "    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",
    "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",
    "                             aggfunc=\"sum\", margins=True, margins_name=\"TOTAL\")\n",
    "        .reindex([*CAPABILITIES, \"TOTAL\"]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "5ace8c50",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.437620Z",
     "iopub.status.busy": "2026-07-13T22:00:10.437432Z",
     "iopub.status.idle": "2026-07-13T22:00:10.444270Z",
     "shell.execute_reply": "2026-07-13T22:00:10.443168Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "4c89a258ad0048e6a2797233bd2c04d2",
       "position": "sidebar",
       "widget": "CheckboxWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4c89a258ad0048e6a2797233bd2c04d2",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.checkbox.CheckboxWidget object at 0x7f3aaec5a660>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Schedule option (Mercury sidebar — widgets only, no output) ──────\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
    "# so this cell constructs widgets ONLY — .value is read downstream.\n",
    "_na_email_w = mr.CheckBox(label=\"NA Email implements early (Jan 2027)\", value=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "fe3572d2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.446194Z",
     "iopub.status.busy": "2026-07-13T22:00:10.445998Z",
     "iopub.status.idle": "2026-07-13T22:00:10.457166Z",
     "shell.execute_reply": "2026-07-13T22:00:10.456462Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NA Email exception: implemented Oct 2026, realizes Jan 2027\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>implemented</th>\n",
       "      <th>benefits realize</th>\n",
       "      <th>token months 2026</th>\n",
       "      <th>token months 2027</th>\n",
       "      <th>token months 2028</th>\n",
       "      <th>benefit months 2026</th>\n",
       "      <th>benefit months 2027</th>\n",
       "      <th>benefit months 2028</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>region</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>NA</th>\n",
       "      <td>Oct 2026 (m10)</td>\n",
       "      <td>Jan 2027 (m13)</td>\n",
       "      <td>3</td>\n",
       "      <td>12</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>12</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ANZ</th>\n",
       "      <td>Jun 2027 (m18)</td>\n",
       "      <td>Sep 2027 (m21)</td>\n",
       "      <td>0</td>\n",
       "      <td>7</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>4</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>EMEA</th>\n",
       "      <td>Feb 2027 (m14)</td>\n",
       "      <td>May 2027 (m17)</td>\n",
       "      <td>0</td>\n",
       "      <td>11</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>8</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ASIA</th>\n",
       "      <td>Oct 2027 (m22)</td>\n",
       "      <td>Jan 2028 (m25)</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           implemented benefits realize  token months 2026  token months 2027  \\\n",
       "region                                                                          \n",
       "NA      Oct 2026 (m10)   Jan 2027 (m13)                  3                 12   \n",
       "ANZ     Jun 2027 (m18)   Sep 2027 (m21)                  0                  7   \n",
       "EMEA    Feb 2027 (m14)   May 2027 (m17)                  0                 11   \n",
       "ASIA    Oct 2027 (m22)   Jan 2028 (m25)                  0                  3   \n",
       "\n",
       "        token months 2028  benefit months 2026  benefit months 2027  \\\n",
       "region                                                                \n",
       "NA                     12                    0                   12   \n",
       "ANZ                    12                    0                    4   \n",
       "EMEA                   12                    0                    8   \n",
       "ASIA                   12                    0                    0   \n",
       "\n",
       "        benefit months 2028  \n",
       "region                       \n",
       "NA                       12  \n",
       "ANZ                      12  \n",
       "EMEA                     12  \n",
       "ASIA                     12  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Genesys/Broadreach deployment schedule (slides 17-21) ────────────\n",
    "NA_EMAIL_EARLY = bool(_na_email_w.value)\n",
    "# IMPL_MONTH = {NA: 18, ANZ: 21, EMEA: 24, ASIA: 27}; benefits realize\n",
    "# +3 months; NA Email implements early (Jan 2027, realizes Apr 2027).\n",
    "# go_live_month = label − 1 so the labelled month is included (section 2 note).\n",
    "\n",
    "TOKEN_ROLLOUT, EMAIL_TOKEN_ROLLOUT, BENEFIT_ROLLOUT = build_rollouts(\n",
    "    sites, NA_EMAIL_EARLY, RAMP_MONTHS)\n",
    "\n",
    "schedule = pd.DataFrame([\n",
    "    {\n",
    "        \"region\": r,\n",
    "        \"implemented\": f\"{month_label(IMPL_MONTH[r])} (m{IMPL_MONTH[r]})\",\n",
    "        \"benefits realize\": f\"{month_label(REALIZE_MONTH[r])} (m{REALIZE_MONTH[r]})\",\n",
    "        **{f\"token months {y}\": TOKEN_ROLLOUT.live_months_in_year(REGION_SITES[r][0], YEAR_INDEX[y])\n",
    "           for y in YEARS},\n",
    "        **{f\"benefit months {y}\": BENEFIT_ROLLOUT.live_months_in_year(r, YEAR_INDEX[y])\n",
    "           for y in YEARS},\n",
    "    }\n",
    "    for r in REGIONS\n",
    "]).set_index(\"region\")\n",
    "if NA_EMAIL_EARLY:\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)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "a06e90b9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.459077Z",
     "iopub.status.busy": "2026-07-13T22:00:10.458910Z",
     "iopub.status.idle": "2026-07-13T22:00:10.542268Z",
     "shell.execute_reply": "2026-07-13T22:00:10.541659Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "benefits by year: {2026: '$0K', 2027: '$5.4M', 2028: '$9.7M'}\n",
      "⚠ 2026 benefits are $0 — the honest consequence of the deck's own deployment schedule.\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th>year</th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>TOTAL</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>capability</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Agent Copilot</th>\n",
       "      <td>0</td>\n",
       "      <td>2,700,600</td>\n",
       "      <td>4,663,400</td>\n",
       "      <td>7,364,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>WFM</th>\n",
       "      <td>0</td>\n",
       "      <td>624,800</td>\n",
       "      <td>2,376,200</td>\n",
       "      <td>3,001,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Email</th>\n",
       "      <td>0</td>\n",
       "      <td>1,379,750</td>\n",
       "      <td>1,591,250</td>\n",
       "      <td>2,971,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>STA</th>\n",
       "      <td>0</td>\n",
       "      <td>331,650</td>\n",
       "      <td>482,350</td>\n",
       "      <td>814,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Predictive Routing</th>\n",
       "      <td>0</td>\n",
       "      <td>161,400</td>\n",
       "      <td>364,600</td>\n",
       "      <td>526,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Supervisor Copilot</th>\n",
       "      <td>0</td>\n",
       "      <td>171,850</td>\n",
       "      <td>195,150</td>\n",
       "      <td>367,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td>0</td>\n",
       "      <td>5,370,050</td>\n",
       "      <td>9,672,950</td>\n",
       "      <td>15,043,000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "year                2026      2027      2028      TOTAL\n",
       "capability                                             \n",
       "Agent Copilot          0 2,700,600 4,663,400  7,364,000\n",
       "WFM                    0   624,800 2,376,200  3,001,000\n",
       "Email                  0 1,379,750 1,591,250  2,971,000\n",
       "STA                    0   331,650   482,350    814,000\n",
       "Predictive Routing     0   161,400   364,600    526,000\n",
       "Supervisor Copilot     0   171,850   195,150    367,000\n",
       "TOTAL                  0 5,370,050 9,672,950 15,043,000"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Phase each verbatim 3-yr value by its region's realization window ─\n",
    "benefits_long = benefits_by_year(BENEFIT_ROLLOUT, NA_EMAIL_EARLY)\n",
    "benefit_total_by_year = benefits_long.groupby(\"year\")[\"benefit\"].sum().to_dict()\n",
    "\n",
    "# Scaling is at the finest grain, so every verbatim total is reproduced exactly.\n",
    "assert benefit_total_by_year[2026] == 0.0, \"2026 must be $0 under Genesys's own schedule\"\n",
    "for (region, cap), (_, three_yr) in VERBATIM_BENEFITS.items():\n",
    "    got = benefits_long.query(\"region == @region and capability == @cap\")[\"benefit\"].sum()\n",
    "    assert abs(got - three_yr) < 1e-6, (region, cap)\n",
    "\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",
    "        .reindex([*CAPABILITIES, \"TOTAL\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b9174ab",
   "metadata": {},
   "source": [
    "<a id=\"section-3\"></a>\n",
    "\n",
    "## 3 · Feature enablement → token consumption (missing cost #1)\n",
    "\n",
    "Every deck capability maps to Genesys AI Experience token meters (all 🟢 published rates unless\n",
    "noted), consumed from each region's **implementation month** — three months *before* benefits:\n",
    "\n",
    "| Deck capability | Token meter(s) | Treatment |\n",
    "|---|---|---|\n",
    "| Agent Copilot | Agent Copilot **[named]** — 40 tokens/user/mo | Per V2 correction #1, this **includes email/chat Auto-Suggest** and interaction summarization. Scoped to NA/ANZ/EMEA by default (ASIA claims $0 Copilot benefit — toggle below). |\n",
    "| Email | Email AI (**Auto-Respond**) — per message | 🔴→🟡 rate not published; working assumption below (anchored to Genesys Cloud Copilot's 20 AI actions/token), deck's 25.5% auto-respond rate. Sensitivity in section 9. |\n",
    "| STA | Speech & Text Analytics **[named]** — 30 tokens/user/mo | All sites. |\n",
    "| Predictive Routing | Predictive Routing — 17 routed interactions/token | All sites; `PR_ELIGIBILITY` scopes to PR-enabled queue share. |\n",
    "| Supervisor Copilot | AI Summary & Insights (**$0 by Rule 1** — Copilot's rate already covers summarization) + AI Translate as small-volume proxy | Kept visible to show the coverage rule. |\n",
    "| WFM | **No token meter** — WEM is licence-included | $0 row; impl effort assumed inside the base $2.4M PS (no V2 line). |\n",
    "\n",
    "Consumption uses a **claim-level scenario** (no maturity haircut — Genesys claims none) and bills\n",
    "with Genesys's monthly `ceil` token rounding, per-region pricing at $1.00/token US list (🔴 contracted\n",
    "rates not sourced)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "977418e1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.544997Z",
     "iopub.status.busy": "2026-07-13T22:00:10.544753Z",
     "iopub.status.idle": "2026-07-13T22:00:10.562447Z",
     "shell.execute_reply": "2026-07-13T22:00:10.561099Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "5985ba7b869548d4a55b0490fb0fa302",
       "position": "sidebar",
       "widget": "CheckboxWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "5985ba7b869548d4a55b0490fb0fa302",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.checkbox.CheckboxWidget object at 0x7f3b0c04ac10>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "84926ebcbf1b4909870e0162052155da",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "84926ebcbf1b4909870e0162052155da",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaec73460>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "26ad610c103f4511853bbdcfd637a108",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "26ad610c103f4511853bbdcfd637a108",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaec72be0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "3946415fb4ac441fb7912a77a2f143d4",
       "position": "sidebar",
       "widget": "SliderWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "3946415fb4ac441fb7912a77a2f143d4",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.slider.SliderWidget object at 0x7f3aaec59a90>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "6190da45d1d444118fbd3272d96d09c4",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6190da45d1d444118fbd3272d96d09c4",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f3aaecb2250>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── CONFIG — token model knobs (Mercury sidebar; defaults when headless) ─\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
    "# so this cell constructs widgets ONLY — .value is read downstream.\n",
    "_copilot_asia_w = mr.CheckBox(\n",
    "    label=\"Copilot includes ASIA sites\", value=False)\n",
    "# deck claims $0 Copilot benefit in ASIA → excluded by default\n",
    "_email_rate_w = mr.NumberInput(\n",
    "    label=\"Email auto-respond rate\", value=0.255,\n",
    "    min=0.0, max=0.6, step=0.005)\n",
    "# deck: \"Reduced Email Interactions with Auto-Respond 25.5%\"\n",
    "_email_tokens_w = mr.NumberInput(\n",
    "    label=\"Auto-respond tokens per message 🟡\", value=0.05,\n",
    "    min=0.0, max=0.5, step=0.005)\n",
    "# 🟡 ≈ one AI action per generated response (20 actions/token)\n",
    "_pr_elig_w = mr.Slider(\n",
    "    label=\"Predictive Routing eligibility (%)\", value=100,\n",
    "    min=0, max=100)\n",
    "# share of voice volume on PR-enabled queues\n",
    "_ai_translate_w = mr.NumberInput(\n",
    "    label=\"AI Translate eligibility 🟡\", value=0.01,\n",
    "    min=0.0, max=1.0, step=0.01)\n",
    "# 🟡 supervisor-evaluation slice of interactions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "9767177f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.564909Z",
     "iopub.status.busy": "2026-07-13T22:00:10.564722Z",
     "iopub.status.idle": "2026-07-13T22:00:10.575688Z",
     "shell.execute_reply": "2026-07-13T22:00:10.574705Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>feature</th>\n",
       "      <th>meter_type</th>\n",
       "      <th>units_per_token</th>\n",
       "      <th>tokens_per_unit</th>\n",
       "      <th>confidence</th>\n",
       "      <th>notes</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Agent Copilot [named]</td>\n",
       "      <td>per_user_per_month</td>\n",
       "      <td>NaN</td>\n",
       "      <td>40</td>\n",
       "      <td>🟢 confirmed</td>\n",
       "      <td>40 tokens per named user per month. Includes i...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Speech &amp; Text Analytics [named]</td>\n",
       "      <td>per_user_per_month</td>\n",
       "      <td>NaN</td>\n",
       "      <td>30</td>\n",
       "      <td>🟢 confirmed</td>\n",
       "      <td>STA named licence; 30 tokens per named user pe...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Predictive Routing</td>\n",
       "      <td>per_interaction</td>\n",
       "      <td>17</td>\n",
       "      <td>0</td>\n",
       "      <td>🟢 confirmed</td>\n",
       "      <td>Predictive routing; 17 routes per token.</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>AI Summary &amp; Insights</td>\n",
       "      <td>per_summary</td>\n",
       "      <td>50</td>\n",
       "      <td>0</td>\n",
       "      <td>🟢 confirmed</td>\n",
       "      <td>Supervisor standalone summarization; 50 summar...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>AI Translate</td>\n",
       "      <td>per_interaction</td>\n",
       "      <td>2</td>\n",
       "      <td>0</td>\n",
       "      <td>🟢 confirmed</td>\n",
       "      <td>AI translation; 2 translations per token.</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Email AI (Auto-Respond)</td>\n",
       "      <td>per_message</td>\n",
       "      <td>20</td>\n",
       "      <td>0</td>\n",
       "      <td>🟡 estimated</td>\n",
       "      <td>WORKING ASSUMPTION — rate unpublished; ≈1 AI a...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                           feature          meter_type  units_per_token  \\\n",
       "0            Agent Copilot [named]  per_user_per_month              NaN   \n",
       "1  Speech & Text Analytics [named]  per_user_per_month              NaN   \n",
       "2               Predictive Routing     per_interaction               17   \n",
       "3            AI Summary & Insights         per_summary               50   \n",
       "4                     AI Translate     per_interaction                2   \n",
       "5          Email AI (Auto-Respond)         per_message               20   \n",
       "\n",
       "   tokens_per_unit   confidence  \\\n",
       "0               40  🟢 confirmed   \n",
       "1               30  🟢 confirmed   \n",
       "2                0  🟢 confirmed   \n",
       "3                0  🟢 confirmed   \n",
       "4                0  🟢 confirmed   \n",
       "5                0  🟡 estimated   \n",
       "\n",
       "                                               notes  \n",
       "0  40 tokens per named user per month. Includes i...  \n",
       "1  STA named licence; 30 tokens per named user pe...  \n",
       "2           Predictive routing; 17 routes per token.  \n",
       "3  Supervisor standalone summarization; 50 summar...  \n",
       "4          AI translation; 2 translations per token.  \n",
       "5  WORKING ASSUMPTION — rate unpublished; ≈1 AI a...  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Claim-level scenario, meter override & scopes (tokencalc.appendix4) ─\n",
    "# No adoption_curve on any scope — a curve would silently override the\n",
    "# claim scenario's flat consumption realization.\n",
    "COPILOT_INCLUDES_ASIA = bool(_copilot_asia_w.value)\n",
    "EMAIL_AUTO_RESPOND_RATE = float(_email_rate_w.value)\n",
    "EMAIL_AUTORESPOND_TOKENS_PER_MSG = float(_email_tokens_w.value)\n",
    "PR_ELIGIBILITY = _pr_elig_w.value / 100\n",
    "AI_TRANSLATE_ELIGIBILITY = float(_ai_translate_w.value)\n",
    "USE_CONTRACTED_RATES = False         # no contracted token rate sourced yet\n",
    "\n",
    "GENESYS_CLAIM = claim_scenario(EMAIL_AUTO_RESPOND_RATE)\n",
    "AUTORESPOND_METER = autorespond_meter(EMAIL_AUTORESPOND_TOKENS_PER_MSG)\n",
    "METERS = {**DEFAULT_METERS, \"Email AI (Auto-Respond)\": AUTORESPOND_METER}\n",
    "CORE_SCOPES, EMAIL_SCOPES = build_scopes(\n",
    "    sites, COPILOT_INCLUDES_ASIA, PR_ELIGIBILITY, AI_TRANSLATE_ELIGIBILITY)\n",
    "\n",
    "USED_FEATURES = [sc.feature for sc in CORE_SCOPES + EMAIL_SCOPES]\n",
    "display(meters_dataframe({f: METERS[f] for f in USED_FEATURES})\n",
    "        .drop(columns=[\"source\"]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "3f3102f4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.578640Z",
     "iopub.status.busy": "2026-07-13T22:00:10.578465Z",
     "iopub.status.idle": "2026-07-13T22:00:10.628687Z",
     "shell.execute_reply": "2026-07-13T22:00:10.627130Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AI token consumption (missing cost #1): {2026: '$424K', 2027: '$2.7M', 2028: '$3.5M'} → 3-yr $6.6M\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th>year</th>\n",
       "      <th>conf</th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>3-yr</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>cost_line</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Predictive Routing</th>\n",
       "      <td>🟢</td>\n",
       "      <td>214,299</td>\n",
       "      <td>1,362,323</td>\n",
       "      <td>1,851,816</td>\n",
       "      <td>3,428,438</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Agent Copilot [named]</th>\n",
       "      <td>🟢</td>\n",
       "      <td>114,000</td>\n",
       "      <td>662,400</td>\n",
       "      <td>715,200</td>\n",
       "      <td>1,491,600</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Speech &amp; Text Analytics [named]</th>\n",
       "      <td>🟢</td>\n",
       "      <td>85,500</td>\n",
       "      <td>550,620</td>\n",
       "      <td>751,680</td>\n",
       "      <td>1,387,800</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Email AI (Auto-Respond)</th>\n",
       "      <td>🟡</td>\n",
       "      <td>10,551</td>\n",
       "      <td>67,080</td>\n",
       "      <td>91,500</td>\n",
       "      <td>169,131</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI Translate</th>\n",
       "      <td>🟢</td>\n",
       "      <td>0</td>\n",
       "      <td>34,185</td>\n",
       "      <td>69,540</td>\n",
       "      <td>103,725</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI Summary &amp; Insights</th>\n",
       "      <td>🟢</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>WFM (no token meter — licence-included)</th>\n",
       "      <td>🟢</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td></td>\n",
       "      <td>424,350</td>\n",
       "      <td>2,676,608</td>\n",
       "      <td>3,479,736</td>\n",
       "      <td>6,580,694</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "year                                    conf    2026      2027      2028  \\\n",
       "cost_line                                                                  \n",
       "Predictive Routing                         🟢 214,299 1,362,323 1,851,816   \n",
       "Agent Copilot [named]                      🟢 114,000   662,400   715,200   \n",
       "Speech & Text Analytics [named]            🟢  85,500   550,620   751,680   \n",
       "Email AI (Auto-Respond)                    🟡  10,551    67,080    91,500   \n",
       "AI Translate                               🟢       0    34,185    69,540   \n",
       "AI Summary & Insights                      🟢       0         0         0   \n",
       "WFM (no token meter — licence-included)    🟢       0         0         0   \n",
       "TOTAL                                        424,350 2,676,608 3,479,736   \n",
       "\n",
       "year                                         3-yr  \n",
       "cost_line                                          \n",
       "Predictive Routing                      3,428,438  \n",
       "Agent Copilot [named]                   1,491,600  \n",
       "Speech & Text Analytics [named]         1,387,800  \n",
       "Email AI (Auto-Respond)                   169,131  \n",
       "AI Translate                              103,725  \n",
       "AI Summary & Insights                           0  \n",
       "WFM (no token meter — licence-included)         0  \n",
       "TOTAL                                   6,580,694  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Token cost by year — engine call, rollout-gated ──────────────────\n",
    "tokens_long = token_costs_by_year(\n",
    "    sites, METERS, DEFAULT_PRICING, GENESYS_CLAIM, CORE_SCOPES, EMAIL_SCOPES,\n",
    "    TOKEN_ROLLOUT, EMAIL_TOKEN_ROLLOUT, use_contracted=USE_CONTRACTED_RATES)\n",
    "\n",
    "tokens_pivot = tokens_long.pivot_table(index=\"cost_line\", columns=\"year\",\n",
    "                                       values=\"annual_cost\", aggfunc=\"sum\")\n",
    "tokens_pivot.loc[\"WFM (no token meter — licence-included)\"] = [0.0, 0.0, 0.0]\n",
    "tokens_pivot[\"3-yr\"] = tokens_pivot.sum(axis=1)\n",
    "tokens_pivot = tokens_pivot.sort_values(\"3-yr\", ascending=False)\n",
    "tokens_pivot.loc[\"TOTAL\"] = tokens_pivot.sum()\n",
    "token_total_by_year = {y: float(tokens_pivot.loc[\"TOTAL\", y]) for y in YEARS}\n",
    "\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",
    "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)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "e62ecfa7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:10.636419Z",
     "iopub.status.busy": "2026-07-13T22:00:10.636215Z",
     "iopub.status.idle": "2026-07-13T22:00:12.215681Z",
     "shell.execute_reply": "2026-07-13T22:00:12.213933Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Agent Copilot [named]",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          114000.0,
          662400.0,
          715200.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#008300",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Speech & Text Analytics [named]",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          85500.0,
          550620.0,
          751680.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#4a3aa7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Predictive Routing",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          214299.0,
          1362323.0,
          1851816.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#eda100",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Email AI (Auto-Respond)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          10551.0,
          67080.0,
          91500.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI Translate",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          34185.0,
          69540.0
         ]
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$424K</b>",
          "x": 0,
          "y": 424350.0,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$2.7M</b>",
          "x": 1,
          "y": 2676608.0,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$3.5M</b>",
          "x": 2,
          "y": 3479736.0,
          "yshift": 12
         }
        ],
        "bargap": 0.45,
        "barmode": "stack",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 460,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>AI Experience token consumption — absent from the Genesys cost case</b><br><span style='font-size:12px;color:#898781'>Claim-level usage · published meter rates · $1.00/token US list · consumption starts at each region's implementation month</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Chart: token cost by meter per year (stacked) ────────────────────\n",
    "TOKEN_LINE_COLOR = {   # entity colors consistent with the capability palette\n",
    "    \"Agent Copilot [named]\": CAP_COLOR[\"Agent Copilot\"],\n",
    "    \"Speech & Text Analytics [named]\": CAP_COLOR[\"STA\"],\n",
    "    \"Predictive Routing\": CAP_COLOR[\"Predictive Routing\"],\n",
    "    \"Email AI (Auto-Respond)\": CAP_COLOR[\"Email\"],\n",
    "    \"AI Translate\": CAP_COLOR[\"Supervisor Copilot\"],\n",
    "    \"AI Summary & Insights\": \"#e87ba4\",\n",
    "}\n",
    "X = [str(y) for y in YEARS]\n",
    "fig = go.Figure()\n",
    "for line, color in TOKEN_LINE_COLOR.items():\n",
    "    vals = [tokens_long.query(\"cost_line == @line and year == @y\")[\"annual_cost\"].sum()\n",
    "            for y in YEARS]\n",
    "    if sum(vals) > 0:\n",
    "        fig.add_trace(bar(X, vals, line, color))\n",
    "for i, y in enumerate(YEARS):   # annotations anchor by category index, not label\n",
    "    total = token_total_by_year[y]\n",
    "    fig.add_annotation(x=i, y=total, text=f\"<b>{money(total)}</b>\", showarrow=False,\n",
    "                       yshift=12, font=dict(size=12, color=INK))\n",
    "fig.update_layout(barmode=\"stack\")\n",
    "tei_layout(fig, \"AI Experience token consumption — absent from the Genesys cost case\",\n",
    "           \"Claim-level usage · published meter rates · $1.00/token US list · \"\n",
    "           \"consumption starts at each region's implementation month\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf0108e5",
   "metadata": {},
   "source": [
    "<a id=\"section-4\"></a>\n",
    "\n",
    "## 4 · AI implementation effort (missing cost #2)\n",
    "\n",
    "The deck's $2.4M professional services covers **base platform implementation only** — none of the\n",
    "five AI capabilities are turn-key. Hours below are the **V2 corrected LoE**\n",
    "(`docs/ctm_ai_labour_estimate_V2.md`; V1 was vendor-inflated ~4×): configuration, tuning cycles and\n",
    "enablement at CTM scale, not custom development. Per V2 correction #1, Agent Copilot's line\n",
    "includes email/chat Auto-Suggest — one implementation.\n",
    "\n",
    "**Model (deliberately simple — the activity-level engine is deferred):**\n",
    "hours(feature) × blended rate, allocated to each feature's scoped regions by agent share, spread\n",
    "uniformly from contract start to that region's implementation month; **KB readiness** is a\n",
    "separately-flagged prerequisite project; **steady-state** is an absolute 500–900 h/yr\n",
    "(tuning, KB refresh, drift correction) booked program-level in 2027–2028."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "3b395fbd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.219421Z",
     "iopub.status.busy": "2026-07-13T22:00:12.219130Z",
     "iopub.status.idle": "2026-07-13T22:00:12.236263Z",
     "shell.execute_reply": "2026-07-13T22:00:12.235002Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "ffa89c9fcbcd417896eaba9fec38d96c",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ffa89c9fcbcd417896eaba9fec38d96c",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f3a6c989010>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "f8456441f0ae4695ae97815b0b0aac84",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f8456441f0ae4695ae97815b0b0aac84",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f3a6ca9d310>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "f27fe4f9506d44b5948d86125bdf587b",
       "position": "sidebar",
       "widget": "CheckboxWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f27fe4f9506d44b5948d86125bdf587b",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.checkbox.CheckboxWidget object at 0x7f3a6ca9d590>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── CONFIG — AI implementation (V2 LoE; Mercury sidebar, widgets only) ─\n",
    "# Hours ranges live in tokencalc.appendix4 (AI_IMPL_HOURS / KB_READINESS_HOURS\n",
    "# / STEADY_STATE_HOURS), sourced from docs/ctm_ai_labour_estimate_V2.md.\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
    "# so this cell constructs widgets ONLY — .value is read downstream.\n",
    "_hours_mode_w = mr.Select(label=\"Impl hours (V2 range)\", value=\"mid\",\n",
    "                          choices=[\"low\", \"mid\", \"high\"])\n",
    "_blended_rate_w = mr.Select(  # 175 offshore-heavy | 225 typical | 275 onshore\n",
    "    label=\"Blended rate ($/h)\", value=\"225\",\n",
    "    choices=[\"175\", \"225\", \"275\"])\n",
    "_kb_w = mr.CheckBox(label=\"Include KB readiness (500–1,500 h)\", value=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "23756537",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.239635Z",
     "iopub.status.busy": "2026-07-13T22:00:12.239284Z",
     "iopub.status.idle": "2026-07-13T22:00:12.282614Z",
     "shell.execute_reply": "2026-07-13T22:00:12.281166Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>workstream</th>\n",
       "      <th>regions</th>\n",
       "      <th>low h</th>\n",
       "      <th>high h</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Agent Copilot</td>\n",
       "      <td>NA, ANZ, EMEA</td>\n",
       "      <td>1200</td>\n",
       "      <td>1800</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Email Auto-Respond</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>800</td>\n",
       "      <td>1400</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>STA</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>800</td>\n",
       "      <td>1200</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Supervisor Copilot</td>\n",
       "      <td>NA, ANZ, EMEA</td>\n",
       "      <td>200</td>\n",
       "      <td>400</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Predictive Routing</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>400</td>\n",
       "      <td>700</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Cross-cutting (PM, governance, testing, integr...</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>1000</td>\n",
       "      <td>1800</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>KB readiness (prerequisite)</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>500</td>\n",
       "      <td>1500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Steady-state (h/yr, 2027-28)</td>\n",
       "      <td>NA, ANZ, EMEA, ASIA</td>\n",
       "      <td>500</td>\n",
       "      <td>900</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                          workstream              regions  \\\n",
       "0                                      Agent Copilot        NA, ANZ, EMEA   \n",
       "1                                 Email Auto-Respond  NA, ANZ, EMEA, ASIA   \n",
       "2                                                STA  NA, ANZ, EMEA, ASIA   \n",
       "3                                 Supervisor Copilot        NA, ANZ, EMEA   \n",
       "4                                 Predictive Routing  NA, ANZ, EMEA, ASIA   \n",
       "5  Cross-cutting (PM, governance, testing, integr...  NA, ANZ, EMEA, ASIA   \n",
       "6                        KB readiness (prerequisite)  NA, ANZ, EMEA, ASIA   \n",
       "7                       Steady-state (h/yr, 2027-28)  NA, ANZ, EMEA, ASIA   \n",
       "\n",
       "   low h  high h  \n",
       "0   1200    1800  \n",
       "1    800    1400  \n",
       "2    800    1200  \n",
       "3    200     400  \n",
       "4    400     700  \n",
       "5   1000    1800  \n",
       "6    500    1500  \n",
       "7    500     900  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AI implementation (mid, $225/h): Y1 LoE $1.3M + KB readiness $225K + steady-state $315K (2027-28)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>hours</th>\n",
       "      <th>cost</th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>workstream</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Agent Copilot</th>\n",
       "      <td>1,500</td>\n",
       "      <td>337,500</td>\n",
       "      <td>311,832</td>\n",
       "      <td>25,668</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Cross-cutting (PM, governance, testing, integration)</th>\n",
       "      <td>1,400</td>\n",
       "      <td>315,000</td>\n",
       "      <td>257,242</td>\n",
       "      <td>57,758</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Email Auto-Respond</th>\n",
       "      <td>1,100</td>\n",
       "      <td>247,500</td>\n",
       "      <td>202,119</td>\n",
       "      <td>45,381</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>KB readiness (prerequisite)</th>\n",
       "      <td>1,000</td>\n",
       "      <td>225,000</td>\n",
       "      <td>183,744</td>\n",
       "      <td>41,256</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Predictive Routing</th>\n",
       "      <td>550</td>\n",
       "      <td>123,750</td>\n",
       "      <td>101,059</td>\n",
       "      <td>22,691</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>STA</th>\n",
       "      <td>1,000</td>\n",
       "      <td>225,000</td>\n",
       "      <td>183,744</td>\n",
       "      <td>41,256</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Supervisor Copilot</th>\n",
       "      <td>300</td>\n",
       "      <td>67,500</td>\n",
       "      <td>62,366</td>\n",
       "      <td>5,134</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Steady-state tuning (2027-28)</th>\n",
       "      <td>1,400</td>\n",
       "      <td>315,000</td>\n",
       "      <td>0</td>\n",
       "      <td>157,500</td>\n",
       "      <td>157,500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td>8,250</td>\n",
       "      <td>1,856,250</td>\n",
       "      <td>1,302,108</td>\n",
       "      <td>396,642</td>\n",
       "      <td>157,500</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                    hours      cost      2026  \\\n",
       "workstream                                                                      \n",
       "Agent Copilot                                       1,500   337,500   311,832   \n",
       "Cross-cutting (PM, governance, testing, integra...  1,400   315,000   257,242   \n",
       "Email Auto-Respond                                  1,100   247,500   202,119   \n",
       "KB readiness (prerequisite)                         1,000   225,000   183,744   \n",
       "Predictive Routing                                    550   123,750   101,059   \n",
       "STA                                                 1,000   225,000   183,744   \n",
       "Supervisor Copilot                                    300    67,500    62,366   \n",
       "Steady-state tuning (2027-28)                       1,400   315,000         0   \n",
       "TOTAL                                               8,250 1,856,250 1,302,108   \n",
       "\n",
       "                                                      2027    2028  \n",
       "workstream                                                          \n",
       "Agent Copilot                                       25,668       0  \n",
       "Cross-cutting (PM, governance, testing, integra...  57,758       0  \n",
       "Email Auto-Respond                                  45,381       0  \n",
       "KB readiness (prerequisite)                         41,256       0  \n",
       "Predictive Routing                                  22,691       0  \n",
       "STA                                                 41,256       0  \n",
       "Supervisor Copilot                                   5,134       0  \n",
       "Steady-state tuning (2027-28)                      157,500 157,500  \n",
       "TOTAL                                              396,642 157,500  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── V2 hours-range × rate model (tokencalc.appendix4.build_impl_costs) ─\n",
    "# Swap point for the future activity-level LoE engine.\n",
    "HOURS_MODE = str(_hours_mode_w.value)\n",
    "BLENDED_RATE = int(_blended_rate_w.value)\n",
    "INCLUDE_KB_READINESS = bool(_kb_w.value)\n",
    "\n",
    "_regions = impl_feature_regions(COPILOT_INCLUDES_ASIA)\n",
    "display(pd.DataFrame(\n",
    "    [{\"workstream\": f, \"regions\": \", \".join(_regions.get(f, REGIONS)),\n",
    "      \"low h\": lo, \"high h\": hi}\n",
    "     for f, (lo, hi) in {**AI_IMPL_HOURS,\n",
    "                         \"KB readiness (prerequisite)\": KB_READINESS_HOURS,\n",
    "                         \"Steady-state (h/yr, 2027-28)\": STEADY_STATE_HOURS}.items()]))\n",
    "\n",
    "impl_detail, impl_by_year, kb_by_year, steady_by_year = build_impl_costs(\n",
    "    sites, HOURS_MODE, BLENDED_RATE, INCLUDE_KB_READINESS,\n",
    "    COPILOT_INCLUDES_ASIA, NA_EMAIL_EARLY)\n",
    "\n",
    "impl_summary = impl_detail.groupby(\"workstream\")[[\"hours\", \"cost\", *YEARS]].sum()\n",
    "impl_summary.loc[\"Steady-state tuning (2027-28)\"] = [\n",
    "    hours_pick(STEADY_STATE_HOURS, HOURS_MODE) * 2,\n",
    "    sum(steady_by_year.values()), *[steady_by_year[y] for y in YEARS]]\n",
    "impl_summary.loc[\"TOTAL\"] = impl_summary.sum()\n",
    "print(f\"AI implementation ({HOURS_MODE}, ${BLENDED_RATE}/h): \"\n",
    "      f\"Y1 LoE {money(sum(impl_by_year.values()))}\"\n",
    "      + (f\" + KB readiness {money(sum(kb_by_year.values()))}\" if INCLUDE_KB_READINESS else \"\")\n",
    "      + f\" + steady-state {money(sum(steady_by_year.values()))} (2027-28)\")\n",
    "display(impl_summary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "1ffaa74a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.284671Z",
     "iopub.status.busy": "2026-07-13T22:00:12.284479Z",
     "iopub.status.idle": "2026-07-13T22:00:12.291405Z",
     "shell.execute_reply": "2026-07-13T22:00:12.287871Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AI implementation $1.3M ÷ benefit claim $15.0M = 8.8%  →  FLAG — under-modelled by the industry benchmark\n",
      "Industry benchmark: 20-40% of the Y1 benefit claim goes to implementation.\n",
      "Reading: V2 deliberately strips vendor-services inflation from V1 (which sat at ~31%).\n",
      "If the corrected case still looks good at V2 hours, it is robust to the higher estimate;\n",
      "Section 9 sweeps hours × rate to show exactly how much the conclusion depends on this line.\n"
     ]
    }
   ],
   "source": [
    "# ── Smell test (V1 doc rule): AI impl should be ≥15% of the AI benefit claim ──\n",
    "_impl_total = sum(impl_by_year.values())\n",
    "_ratio = _impl_total / SLIDE_TOTALS[\"total_3yr\"]\n",
    "_verdict = (\"PASS\" if _ratio >= SMELL_TEST_FLOOR\n",
    "            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",
    "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.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "execution_count": null,
   "id": "b958f93d",
   "metadata": {},
   "outputs": [],
   "source": [
    "<a id=\"section-5\"></a>\n",
    "\n",
    "## 5 · Corrected cost stack\n",
    "\n",
    "The pitched case vs the same case with the three missing costs added — plus the signed\n",
    "contract mechanics: licences are **invoiced in full from year 1** ($3.2M — the order-form\n",
    "ramp gives free months operationally but the invoice is not netted down); PS bills on the **SOW milestones** (50/50 across 2026–27, $2.03M total vs the deck's\n",
    "$2.4M year-1 lump); and **managed services** ($411K/yr from MCX go-live) join the stack —\n",
    "an ongoing cost the deck's TCO never carried. **No existing-platform line appears** — current\n",
    "spend is excluded from this case per CTM's direction."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "ad880bda",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.293736Z",
     "iopub.status.busy": "2026-07-13T22:00:12.293540Z",
     "iopub.status.idle": "2026-07-13T22:00:12.481314Z",
     "shell.execute_reply": "2026-07-13T22:00:12.480554Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Corrected programme cost: {2026: '$6.2M', 2027: '$7.7M', 2028: '$7.2M'} → 3-yr $21.1M\n",
      "As pitched (deck): {2026: '$6.9M', 2027: '$4.3M', 2028: '$4.3M'} → 3-yr $15.5M (deck says $15.4M — rounding)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>3-yr</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>CCaaS platform licences (contracted, ramp-adjusted)</th>\n",
       "      <td>3,200,000</td>\n",
       "      <td>3,200,000</td>\n",
       "      <td>3,200,000</td>\n",
       "      <td>9,600,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NTT PS + training (SOW billing milestones)</th>\n",
       "      <td>1,179,723</td>\n",
       "      <td>1,012,723</td>\n",
       "      <td>0</td>\n",
       "      <td>2,192,446</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NTT managed services (from MCX go-live)</th>\n",
       "      <td>68,486</td>\n",
       "      <td>410,918</td>\n",
       "      <td>410,918</td>\n",
       "      <td>890,323</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI token consumption</th>\n",
       "      <td>424,350</td>\n",
       "      <td>2,676,608</td>\n",
       "      <td>3,479,736</td>\n",
       "      <td>6,580,694</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI implementation + KB readiness</th>\n",
       "      <td>1,302,108</td>\n",
       "      <td>239,142</td>\n",
       "      <td>0</td>\n",
       "      <td>1,541,250</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI steady-state tuning</th>\n",
       "      <td>0</td>\n",
       "      <td>157,500</td>\n",
       "      <td>157,500</td>\n",
       "      <td>315,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL — corrected</th>\n",
       "      <td>6,174,668</td>\n",
       "      <td>7,696,892</td>\n",
       "      <td>7,248,154</td>\n",
       "      <td>21,119,714</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                        2026      2027  \\\n",
       "CCaaS platform licences (contracted, ramp-adjus... 3,200,000 3,200,000   \n",
       "NTT PS + training (SOW billing milestones)         1,179,723 1,012,723   \n",
       "NTT managed services (from MCX go-live)               68,486   410,918   \n",
       "AI token consumption                                 424,350 2,676,608   \n",
       "AI implementation + KB readiness                   1,302,108   239,142   \n",
       "AI steady-state tuning                                     0   157,500   \n",
       "TOTAL — corrected                                  6,174,668 7,696,892   \n",
       "\n",
       "                                                        2028       3-yr  \n",
       "CCaaS platform licences (contracted, ramp-adjus... 3,200,000  9,600,000  \n",
       "NTT PS + training (SOW billing milestones)                 0  2,192,446  \n",
       "NTT managed services (from MCX go-live)              410,918    890,323  \n",
       "AI token consumption                               3,479,736  6,580,694  \n",
       "AI implementation + KB readiness                           0  1,541,250  \n",
       "AI steady-state tuning                               157,500    315,000  \n",
       "TOTAL — corrected                                  7,248,154 21,119,714  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>3-yr</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>CCaaS platform licences (as pitched, no ramp)</th>\n",
       "      <td>4,300,000</td>\n",
       "      <td>4,300,000</td>\n",
       "      <td>4,300,000</td>\n",
       "      <td>12,900,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Base professional services + training</th>\n",
       "      <td>2,567,000</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>2,567,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL — as pitched</th>\n",
       "      <td>6,867,000</td>\n",
       "      <td>4,300,000</td>\n",
       "      <td>4,300,000</td>\n",
       "      <td>15,467,000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                   2026      2027      2028  \\\n",
       "CCaaS platform licences (as pitched, no ramp) 4,300,000 4,300,000 4,300,000   \n",
       "Base professional services + training         2,567,000         0         0   \n",
       "TOTAL — as pitched                            6,867,000 4,300,000 4,300,000   \n",
       "\n",
       "                                                    3-yr  \n",
       "CCaaS platform licences (as pitched, no ramp) 12,900,000  \n",
       "Base professional services + training          2,567,000  \n",
       "TOTAL — as pitched                            15,467,000  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>share</th>\n",
       "      <th>amount</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>SOW billing milestone</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>SOW Effective Date</th>\n",
       "      <td>2026-03-15</td>\n",
       "      <td>30%</td>\n",
       "      <td>$607,633.94</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Start of client UAT (first region, NA)</th>\n",
       "      <td>2026-10-16</td>\n",
       "      <td>20%</td>\n",
       "      <td>$405,089.30</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Start of client UAT (last region, APAC)</th>\n",
       "      <td>2027-10-01</td>\n",
       "      <td>30%</td>\n",
       "      <td>$607,633.94</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Completion of last go-live migration</th>\n",
       "      <td>2027-11-01</td>\n",
       "      <td>20%</td>\n",
       "      <td>$405,089.30</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                               date share       amount\n",
       "SOW billing milestone                                                 \n",
       "SOW Effective Date                       2026-03-15   30%  $607,633.94\n",
       "Start of client UAT (first region, NA)   2026-10-16   20%  $405,089.30\n",
       "Start of client UAT (last region, APAC)  2027-10-01   30%  $607,633.94\n",
       "Completion of last go-live migration     2027-11-01   20%  $405,089.30"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "ps_by_year = ps_costs_by_year(contracted=True)   # SOW milestones + verbatim training\n",
    "ps_pitched_by_year = ps_costs_by_year()          # deck's verbatim year-1 lump\n",
    "managed_by_year = managed_services_by_year()     # 🟢 bills from MCX go-live (16 Oct 2026)\n",
    "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 (contracted, ramp-adjusted)\": licence_by_year,\n",
    "    \"NTT PS + training (SOW billing milestones)\": ps_by_year,\n",
    "    \"NTT managed services (from MCX go-live)\": managed_by_year,\n",
    "    \"AI token consumption\": token_total_by_year,\n",
    "    \"AI implementation + KB readiness\": impl_kb_by_year,\n",
    "    \"AI steady-state tuning\": steady_by_year,\n",
    "}).T[YEARS]\n",
    "corrected_costs[\"3-yr\"] = corrected_costs.sum(axis=1)\n",
    "corrected_total_by_year = {y: float(corrected_costs[y].sum()) for y in YEARS}\n",
    "\n",
    "pitched_costs = pd.DataFrame({\n",
    "    \"CCaaS platform licences (as pitched, no ramp)\": {y: TCO_VERBATIM[\"ccaas_annual\"] for y in YEARS},\n",
    "    \"Base professional services + training\": ps_pitched_by_year,\n",
    "}).T[YEARS]\n",
    "pitched_costs[\"3-yr\"] = pitched_costs.sum(axis=1)\n",
    "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",
    "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",
    "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",
    "display(pd.concat([pitched_costs,\n",
    "                   pd.DataFrame(pitched_costs.sum()).T.set_axis([\"TOTAL — as pitched\"])]))\n",
    "\n",
    "_ms = ps_milestones_dataframe()\n",
    "_ms[\"amount\"] = _ms[\"amount\"].map(\"${:,.2f}\".format)\n",
    "_ms[\"share\"] = _ms[\"share\"].map(\"{:.0%}\".format)\n",
    "display(_ms.set_index(\"name\").rename_axis(\"SOW billing milestone\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be744ec4",
   "metadata": {},
   "source": [
    "<a id=\"section-6\"></a>\n",
    "\n",
    "## 6 · Figure 1 — Benefits over 3 years (verbatim Genesys)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "2e2b207c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.483199Z",
     "iopub.status.busy": "2026-07-13T22:00:12.482947Z",
     "iopub.status.idle": "2026-07-13T22:00:12.604318Z",
     "shell.execute_reply": "2026-07-13T22:00:12.603153Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Agent Copilot",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          2700600.0,
          4663400.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "WFM",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          624800.0,
          2376200.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#eda100",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Email",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          1379750.0,
          1591250.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#008300",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "STA",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          331650.0,
          482350.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#4a3aa7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Predictive Routing",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          161400.0,
          364600.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Supervisor Copilot",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          171850.0,
          195150.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "line": {
          "color": "#52514e",
          "width": 2
         },
         "marker": {
          "line": {
           "color": "#fcfcfb",
           "width": 2
          },
          "size": 8
         },
         "mode": "lines+markers",
         "name": "Cumulative benefits",
         "type": "scatter",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": {
          "bdata": "AAAAAAAAAAAAAACAMHxUQQAAAAA3sWxB",
          "dtype": "f8"
         }
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$0K</b>",
          "x": 0,
          "y": 0.0,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$5.4M</b>",
          "x": 1,
          "y": 5370050.0,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$9.7M</b>",
          "x": 2,
          "y": 9672950.0,
          "yshift": 12
         },
         {
          "font": {
           "color": "#52514e",
           "size": 12
          },
          "showarrow": false,
          "text": "3-yr total $15.0M (verbatim)",
          "x": 2,
          "xshift": -40,
          "y": 15043000.0,
          "yshift": 16
         }
        ],
        "bargap": 0.45,
        "barmode": "stack",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 460,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Benefits over 3 years — verbatim Genesys (Appendix 4)</b><br><span style='font-size:12px;color:#898781'>Phased by Genesys's own deployment schedule: benefits realize +3 months after regional go-live — $0 in 2026 · WFM = Workforce Forecast & Scheduling</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "for cap in CAPABILITIES:\n",
    "    vals = [benefits_long.query(\"capability == @cap and year == @y\")[\"benefit\"].sum()\n",
    "            for y in YEARS]\n",
    "    fig.add_trace(bar(X, vals, cap, CAP_COLOR[cap]))\n",
    "cum = pd.Series([benefit_total_by_year[y] for y in YEARS]).cumsum()\n",
    "fig.add_trace(cum_line(X, cum, \"Cumulative benefits\"))\n",
    "for i, y in enumerate(YEARS):\n",
    "    fig.add_annotation(x=i, y=benefit_total_by_year[y],\n",
    "                       text=f\"<b>{money(benefit_total_by_year[y])}</b>\",\n",
    "                       showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
    "fig.add_annotation(x=len(YEARS) - 1, y=float(cum.iloc[-1]),\n",
    "                   text=f\"3-yr total {money(float(cum.iloc[-1]))} (verbatim)\",\n",
    "                   showarrow=False, yshift=16, xshift=-40, font=dict(size=12, color=INK2))\n",
    "fig.update_layout(barmode=\"stack\")\n",
    "tei_layout(fig, \"Benefits over 3 years — verbatim Genesys (Appendix 4)\",\n",
    "           \"Phased by Genesys's own deployment schedule: benefits realize +3 months after \"\n",
    "           \"regional go-live — $0 in 2026 · WFM = Workforce Forecast & Scheduling\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "execution_count": null,
   "id": "95e3aa5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "<a id=\"section-7\"></a>\n",
    "\n",
    "## 7 · Figure 2 — Costs over 3 years, corrected\n",
    "\n",
    "The stack is the full programme cost — **excluding current spend** (no existing-platform line).\n",
    "The dashed grey line is the deck's cumulative cost case ($15.4M) for reference — the gap between\n",
    "the lines is what the pitch left out (tokens + AI implementation), net of the ramp credit and\n",
    "contracted-rate savings it also left out. Licences are invoiced in full from year 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c2c9418f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.606835Z",
     "iopub.status.busy": "2026-07-13T22:00:12.606508Z",
     "iopub.status.idle": "2026-07-13T22:00:12.637762Z",
     "shell.execute_reply": "2026-07-13T22:00:12.636921Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "CCaaS platform licences (contracted, ramp-adjusted)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          3200000.0,
          3200000.0,
          3200000.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "NTT PS + training (SOW billing milestones)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          1179723.24,
          1012723.24,
          0.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#0b6b4a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "NTT managed services (from MCX go-live)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          68486.40000000001,
          410918.4000000001,
          410918.4000000001
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#008300",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI token consumption",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          424350.0,
          2676608.0,
          3479736.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#4a3aa7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI implementation + KB readiness",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          1302107.96532141,
          239142.03467859002,
          0.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI steady-state tuning",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          157500.0,
          157500.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "line": {
          "color": "#52514e",
          "width": 2
         },
         "marker": {
          "line": {
           "color": "#fcfcfb",
           "width": 2
          },
          "size": 8
         },
         "mode": "lines+markers",
         "name": "Cumulative — corrected",
         "type": "scatter",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": {
          "bdata": "BJa95vKNV0GQwvXoOHVqQa5H4RouJHRB",
          "dtype": "f8"
         }
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "line": {
          "color": "#c3c2b7",
          "dash": "dash",
          "width": 2
         },
         "marker": {
          "line": {
           "color": "#fcfcfb",
           "width": 2
          },
          "size": 8
         },
         "mode": "lines+markers",
         "name": "Cumulative — as pitched",
         "type": "scatter",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": {
          "bdata": "AAAAAA4yWkEAAAAAo0xlQQAAAAA/gG1B",
          "dtype": "f8"
         }
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$6.2M</b>",
          "x": 0,
          "y": 6174667.605321411,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$7.7M</b>",
          "x": 1,
          "y": 7696891.67467859,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$7.2M</b>",
          "x": 2,
          "y": 7248154.4,
          "yshift": 12
         },
         {
          "font": {
           "color": "#52514e",
           "size": 12
          },
          "showarrow": false,
          "text": "3-yr <b>&#36;21.1M</b> — &#36;5.7M above the pitch",
          "x": 2,
          "xshift": -70,
          "y": 21119713.68,
          "yshift": 18
         }
        ],
        "bargap": 0.45,
        "barmode": "stack",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 500,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Programme cost over 3 years — with the missed costs</b><br><span style='font-size:12px;color:#898781'>Existing platforms bill until 31 Dec 2027 term-contract end (double-billing) · licences at the contracted run-rate, ramp-free for 6 months · SOW-billed PS + managed services · tokens + AI implementation added</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "for line in corrected_costs.index:\n",
    "    fig.add_trace(bar(X, [corrected_costs.loc[line, y] for y in YEARS], line, COST_COLOR[line]))\n",
    "cum_corrected = pd.Series([corrected_total_by_year[y] for y in YEARS]).cumsum()\n",
    "cum_pitched = pd.Series([pitched_total_by_year[y] for y in YEARS]).cumsum()\n",
    "fig.add_trace(cum_line(X, cum_corrected, \"Cumulative — corrected\"))\n",
    "fig.add_trace(cum_line(X, cum_pitched, \"Cumulative — as pitched\", color=CONTEXT, dash=\"dash\"))\n",
    "for i, y in enumerate(YEARS):\n",
    "    fig.add_annotation(x=i, y=corrected_total_by_year[y],\n",
    "                       text=f\"<b>{money(corrected_total_by_year[y])}</b>\",\n",
    "                       showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
    "delta = float(cum_corrected.iloc[-1] - cum_pitched.iloc[-1])\n",
    "fig.add_annotation(x=len(YEARS) - 1, y=float(cum_corrected.iloc[-1]),\n",
    "                   text=f\"3-yr <b>{html_money(float(cum_corrected.iloc[-1]))}</b> — \"\n",
    "                        f\"{html_money(delta)} above the pitch\",\n",
    "                   showarrow=False, yshift=18, xshift=-70, font=dict(size=12, color=INK2))\n",
    "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 at the contracted run-rate, ramp-free for 6 months · SOW-billed PS \"\n",
    "           \"+ managed services · tokens + AI implementation added\", height=500)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "82335f50",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.640436Z",
     "iopub.status.busy": "2026-07-13T22:00:12.640176Z",
     "iopub.status.idle": "2026-07-13T22:00:12.666239Z",
     "shell.execute_reply": "2026-07-13T22:00:12.665328Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#c3c2b7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "As pitched (deck)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          6867000.0,
          4300000.0,
          4300000.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Corrected",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          6174667.605321411,
          7696891.67467859,
          7248154.4
         ]
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>−$692K</b>",
          "x": 0,
          "xshift": 16,
          "y": 6174667.605321411,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>+$3.4M</b>",
          "x": 1,
          "xshift": 16,
          "y": 7696891.67467859,
          "yshift": 12
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>+$2.9M</b>",
          "x": 2,
          "xshift": 16,
          "y": 7248154.4,
          "yshift": 12
         }
        ],
        "bargap": 0.45,
        "bargroupgap": 0.15,
        "barmode": "group",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 400,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Cost case: as pitched vs corrected, by year</b><br><span style='font-size:12px;color:#898781'>Delta labels = what each year's pitch understates</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "fig.add_trace(bar(X, [pitched_total_by_year[y] for y in YEARS], \"As pitched (deck)\", CONTEXT))\n",
    "fig.add_trace(bar(X, [corrected_total_by_year[y] for y in YEARS], \"Corrected\", \"#2a78d6\"))\n",
    "for i, y in enumerate(YEARS):\n",
    "    d = corrected_total_by_year[y] - pitched_total_by_year[y]\n",
    "    fig.add_annotation(x=i, y=corrected_total_by_year[y], xshift=16,\n",
    "                       text=f\"<b>{'+' if d >= 0 else '−'}{money(abs(d))}</b>\",\n",
    "                       showarrow=False, yshift=12, font=dict(size=12, color=INK))\n",
    "fig.update_layout(barmode=\"group\", bargap=0.35, bargroupgap=0.15)\n",
    "tei_layout(fig, \"Cost case: as pitched vs corrected, by year\",\n",
    "           \"Delta labels = what each year's pitch understates\", height=400)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bde5fbf2",
   "metadata": {},
   "source": [
    "<a id=\"section-8\"></a>\n",
    "\n",
    "## 8 · Figure 3 — Overall business case / ROI (absolute)\n",
    "\n",
    "**Frame:** absolute — **current spend excluded** per CTM. Incremental cost = **full programme\n",
    "cost** (no $7.3M baseline); net = verbatim Genesys benefits − programme cost. There is no\n",
    "double-billing penalty and no cost-avoidance credit; the programme stands on the benefits it\n",
    "generates alone. This is a deliberately harsher frame than the deck's — and it is why the\n",
    "**virtual-agents alternative** matters (companion notebook).\n",
    "\n",
    "*Not modelled: any early-termination fees, and migration costs beyond the PS/impl lines (section 11).*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "de7679d8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.669513Z",
     "iopub.status.busy": "2026-07-13T22:00:12.669023Z",
     "iopub.status.idle": "2026-07-13T22:00:12.677791Z",
     "shell.execute_reply": "2026-07-13T22:00:12.676762Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "7d6b41e9a308492588bab31ad47834e7",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "7d6b41e9a308492588bab31ad47834e7",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f3a6c826d50>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── NPV rate (Mercury sidebar — widgets only, no output) ────────────\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell,\n",
    "# so this cell constructs widgets ONLY — .value is read downstream.\n",
    "_npv_rate_w = mr.Select(label=\"NPV discount rate\", value=\"13.5% (deck)\",\n",
    "                        choices=[\"13.5% (deck)\", \"8.0% (CTM treasury)\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "08433116",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.680244Z",
     "iopub.status.busy": "2026-07-13T22:00:12.680025Z",
     "iopub.status.idle": "2026-07-13T22:00:12.685685Z",
     "shell.execute_reply": "2026-07-13T22:00:12.684194Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "corrected: net by year {2026: '-$6.2M', 2027: '-$2.3M', 2028: '$2.4M'}\n",
      "pitched:   net by year {2026: '-$6.9M', 2027: '$1.1M', 2028: '$5.4M'}\n"
     ]
    }
   ],
   "source": [
    "# ABSOLUTE frame (CTM directive — current spend excluded):\n",
    "# incremental cost = full programme cost; net = Genesys benefits − cost.\n",
    "# No $7.3M do-nothing baseline (baseline_annual = 0).\n",
    "DISCOUNT_RATE = (TCO_VERBATIM[\"npv_discount_rate\"]\n",
    "                 if _npv_rate_w.value.startswith(\"13.5\") else 0.08)\n",
    "BASELINE_ANNUAL = 0.0\n",
    "\n",
    "inc_corrected, net_corrected = case_flows(\n",
    "    corrected_total_by_year, benefit_total_by_year, BASELINE_ANNUAL)\n",
    "inc_pitched, net_pitched = case_flows(\n",
    "    pitched_total_by_year, benefit_total_by_year, BASELINE_ANNUAL)\n",
    "kpi_corrected = case_kpis(inc_corrected, net_corrected, DISCOUNT_RATE)\n",
    "kpi_pitched = case_kpis(inc_pitched, net_pitched, DISCOUNT_RATE)\n",
    "\n",
    "\n",
    "def fmt_roi(k):\n",
    "    return f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net cost saving\"\n",
    "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()} }\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "04c43658",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.687540Z",
     "iopub.status.busy": "2026-07-13T22:00:12.687366Z",
     "iopub.status.idle": "2026-07-13T22:00:12.714912Z",
     "shell.execute_reply": "2026-07-13T22:00:12.713701Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Benefits (verbatim Genesys)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          5370050.0,
          9672950.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Programme cost (current spend excluded)",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          -6174667.605321411,
          -7696891.67467859,
          -7248154.4
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "line": {
          "color": "#52514e",
          "width": 2
         },
         "marker": {
          "line": {
           "color": "#fcfcfb",
           "width": 2
          },
          "size": 8
         },
         "mode": "lines+markers",
         "name": "Cumulative net",
         "type": "scatter",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": {
          "bdata": "BJa95vKNV8GQwvWoIDdgwboehWtKLlfB",
          "dtype": "f8"
         }
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>-$6.2M</b>",
          "x": 0,
          "y": -6174667.605321411,
          "yshift": -14
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>-$8.5M</b>",
          "x": 1,
          "y": -8501509.280000001,
          "yshift": -14
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>-$6.1M</b>",
          "x": 2,
          "y": -6076713.680000002,
          "yshift": -14
         },
         {
          "align": "left",
          "bgcolor": "#fcfcfb",
          "bordercolor": "#e1e0d9",
          "borderwidth": 1,
          "font": {
           "color": "#52514e",
           "size": 12
          },
          "showarrow": false,
          "text": "3-yr net <b>-&#36;6.1M</b> · ROI <b>-29%</b><br>NPV@13.5% <b>-&#36;5.6M</b> · payback <b>beyond 2028</b>",
          "x": 0.01,
          "xref": "paper",
          "y": 0.98,
          "yref": "paper"
         }
        ],
        "bargap": 0.45,
        "barmode": "relative",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 500,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Corrected business case — benefits vs incremental cost</b><br><span style='font-size:12px;color:#898781'>No do-nothing baseline · benefits realize from 2027 on the PM timeline</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "fig.add_trace(bar(X, [benefit_total_by_year[y] for y in YEARS],\n",
    "                  \"Benefits (verbatim Genesys)\", \"#2a78d6\"))\n",
    "fig.add_trace(bar(X, [-inc_corrected[y] for y in YEARS],\n",
    "                  \"Programme cost (current spend excluded)\", \"#e34948\"))\n",
    "cum_net = pd.Series([net_corrected[y] for y in YEARS]).cumsum()\n",
    "fig.add_trace(cum_line(X, cum_net, \"Cumulative net\"))\n",
    "for i, c in enumerate(cum_net):\n",
    "    fig.add_annotation(x=i, y=float(c), text=f\"<b>{money(float(c))}</b>\", showarrow=False,\n",
    "                       yshift=14 if c >= 0 else -14, font=dict(size=12, color=INK))\n",
    "fig.update_layout(barmode=\"relative\")\n",
    "fig.add_annotation(\n",
    "    xref=\"paper\", yref=\"paper\", x=0.01, y=0.98, align=\"left\", showarrow=False,\n",
    "    font=dict(size=12, color=INK2), bgcolor=SURFACE, bordercolor=GRID, borderwidth=1,\n",
    "    text=(f\"3-yr net <b>{html_money(kpi_corrected['net_3yr'])}</b> · \"\n",
    "          f\"ROI <b>{fmt_roi(kpi_corrected)}</b><br>\"\n",
    "          f\"NPV@{DISCOUNT_RATE:.1%} <b>{html_money(kpi_corrected['npv'])}</b> · \"\n",
    "          f\"payback <b>{kpi_corrected['payback']}</b>\"))\n",
    "tei_layout(fig, \"Corrected business case — benefits vs incremental cost\",\n",
    "           \"No do-nothing baseline · benefits realize from 2027 on the PM timeline\", height=500)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "a8c9584e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.717839Z",
     "iopub.status.busy": "2026-07-13T22:00:12.717626Z",
     "iopub.status.idle": "2026-07-13T22:00:12.732320Z",
     "shell.execute_reply": "2026-07-13T22:00:12.731299Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The missed costs move the 3-yr case by $5.7M (-$424K pitched → -$6.1M corrected).\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>As pitched (deck)</th>\n",
       "      <th>Corrected</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>3-yr benefits</th>\n",
       "      <td>$15.0M</td>\n",
       "      <td>$15.0M</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3-yr incremental cost</th>\n",
       "      <td>$15.5M</td>\n",
       "      <td>$21.1M</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3-yr net</th>\n",
       "      <td>-$424K</td>\n",
       "      <td>-$6.1M</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ROI (net ÷ incremental cost)</th>\n",
       "      <td>-3%</td>\n",
       "      <td>-29%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NPV @ 13.5% (deck rate)</th>\n",
       "      <td>-$1.5M</td>\n",
       "      <td>-$5.6M</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NPV @ 8.0% (CTM treasury)</th>\n",
       "      <td>-$1.2M</td>\n",
       "      <td>-$5.8M</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Payback</th>\n",
       "      <td>beyond 2028</td>\n",
       "      <td>beyond 2028</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                             As pitched (deck)    Corrected\n",
       "3-yr benefits                           $15.0M       $15.0M\n",
       "3-yr incremental cost                   $15.5M       $21.1M\n",
       "3-yr net                                -$424K       -$6.1M\n",
       "ROI (net ÷ incremental cost)               -3%         -29%\n",
       "NPV @ 13.5% (deck rate)                 -$1.5M       -$5.6M\n",
       "NPV @ 8.0% (CTM treasury)               -$1.2M       -$5.8M\n",
       "Payback                            beyond 2028  beyond 2028"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def kpi_display(k, net):\n",
    "    return {\n",
    "        \"3-yr benefits\": 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\": k[\"payback\"],\n",
    "    }\n",
    "\n",
    "\n",
    "kpis_fmt = pd.DataFrame({\"As pitched (deck)\": kpi_display(kpi_pitched, net_pitched),\n",
    "                         \"Corrected\": kpi_display(kpi_corrected, net_corrected)})\n",
    "delta_net = kpi_pitched[\"net_3yr\"] - kpi_corrected[\"net_3yr\"]\n",
    "print(f\"The missed costs move the 3-yr case by {money(delta_net)} \"\n",
    "      f\"({money(kpi_pitched['net_3yr'])} pitched → {money(kpi_corrected['net_3yr'])} corrected).\")\n",
    "display(kpis_fmt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7d54d0d",
   "metadata": {},
   "source": [
    "<a id=\"section-9\"></a>\n",
    "\n",
    "## 9 · Sensitivity\n",
    "\n",
    "The two weakest inputs, swept: the **Email Auto-Respond token rate** (🔴 unpublished — the working\n",
    "assumption is the only estimated meter in the token stack) and the **AI implementation LoE**\n",
    "(V1 vs V2 disagreed by ~4×)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "6671014d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.735125Z",
     "iopub.status.busy": "2026-07-13T22:00:12.734908Z",
     "iopub.status.idle": "2026-07-13T22:00:12.812825Z",
     "shell.execute_reply": "2026-07-13T22:00:12.811222Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "colorscale": [
          [
           0.0,
           "#cde2fb"
          ],
          [
           0.08333333333333333,
           "#b7d3f6"
          ],
          [
           0.16666666666666666,
           "#9ec5f4"
          ],
          [
           0.25,
           "#86b6ef"
          ],
          [
           0.3333333333333333,
           "#6da7ec"
          ],
          [
           0.4166666666666667,
           "#5598e7"
          ],
          [
           0.5,
           "#3987e5"
          ],
          [
           0.5833333333333334,
           "#2a78d6"
          ],
          [
           0.6666666666666666,
           "#256abf"
          ],
          [
           0.75,
           "#1c5cab"
          ],
          [
           0.8333333333333334,
           "#184f95"
          ],
          [
           0.9166666666666666,
           "#104281"
          ],
          [
           1.0,
           "#0d366b"
          ]
         ],
         "hovertemplate": "%{y} × %{x}: %{z:$,.0f}<extra></extra>",
         "showscale": false,
         "type": "heatmap",
         "x": [
          "0.02 tokens/msg",
          "0.05 tokens/msg",
          "0.1 tokens/msg"
         ],
         "xgap": 2,
         "y": [
          "10.0% auto-respond",
          "25.5% auto-respond",
          "50.0% auto-respond"
         ],
         "ygap": 2,
         "z": [
          [
           26532.0,
           66303.0,
           132606.0
          ],
          [
           67688.0,
           169131.0,
           338167.0
          ],
          [
           132606.0,
           331515.0,
           663030.0
          ]
         ]
        }
       ],
       "layout": {
        "annotations": [
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$27K</b>",
          "x": 0,
          "y": 0
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$66K</b>",
          "x": 1,
          "y": 0
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$133K</b>",
          "x": 2,
          "y": 0
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$68K</b>",
          "x": 0,
          "y": 1
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$169K</b>",
          "x": 1,
          "y": 1
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$338K</b>",
          "x": 2,
          "y": 1
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$133K</b>",
          "x": 0,
          "y": 2
         },
         {
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$332K</b>",
          "x": 1,
          "y": 2
         },
         {
          "font": {
           "color": "#ffffff",
           "size": 12
          },
          "showarrow": false,
          "text": "<b>$663K</b>",
          "x": 2,
          "y": 2
         }
        ],
        "bargap": 0.45,
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 380,
        "hovermode": "closest",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Email Auto-Respond tokens — 3-yr cost under the unpublished rate</b><br><span style='font-size:12px;color:#898781'>Deck claims a $2.9M 3-yr Email benefit; even the worst corner stays well below it</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── 3-yr Email Auto-Respond token cost: tokens/msg × auto-respond rate ─\n",
    "TOK_GRID = [0.02, 0.05, 0.10]\n",
    "RATE_GRID = [0.10, 0.255, 0.50]\n",
    "\n",
    "\n",
    "def email_token_cost_3yr(tokens_per_msg, respond_rate):\n",
    "    m = {**METERS, \"Email AI (Auto-Respond)\": autorespond_meter(tokens_per_msg)}\n",
    "    sc = claim_scenario(respond_rate)\n",
    "    return sum(\n",
    "        calculate_total_cost(sites, EMAIL_SCOPES, m, DEFAULT_PRICING, sc, YEAR_INDEX[y],\n",
    "                             include_platform=False, rollout=EMAIL_TOKEN_ROLLOUT)\n",
    "        [\"annual_cost\"].sum()\n",
    "        for y in YEARS)\n",
    "\n",
    "\n",
    "grid = [[email_token_cost_3yr(t, r) for t in TOK_GRID] for r in RATE_GRID]\n",
    "zmax = max(max(row) for row in grid)\n",
    "fig = go.Figure(go.Heatmap(\n",
    "    z=grid, x=[f\"{t} tokens/msg\" for t in TOK_GRID],\n",
    "    y=[f\"{r:.1%} auto-respond\" for r in RATE_GRID],\n",
    "    colorscale=[[i / (len(SEQ_BLUES) - 1), c] for i, c in enumerate(SEQ_BLUES)],\n",
    "    showscale=False, xgap=2, ygap=2,\n",
    "    hovertemplate=\"%{y} × %{x}: %{z:$,.0f}<extra></extra>\"))\n",
    "for i, r in enumerate(RATE_GRID):\n",
    "    for j, t in enumerate(TOK_GRID):\n",
    "        v = grid[i][j]\n",
    "        fig.add_annotation(x=j, y=i, text=f\"<b>{money(v)}</b>\", showarrow=False,\n",
    "                           font=dict(size=12, color=\"#ffffff\" if v > 0.55 * zmax else INK))\n",
    "tei_layout(fig, \"Email Auto-Respond tokens — 3-yr cost under the unpublished rate\",\n",
    "           \"Deck claims a $2.9M 3-yr Email benefit; even the worst corner stays well below it\",\n",
    "           height=380)\n",
    "fig.update_layout(xaxis=dict(showgrid=False), yaxis=dict(showgrid=False, tickformat=None),\n",
    "                  hovermode=\"closest\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "faa1e95f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.815150Z",
     "iopub.status.busy": "2026-07-13T22:00:12.814916Z",
     "iopub.status.idle": "2026-07-13T22:00:12.844312Z",
     "shell.execute_reply": "2026-07-13T22:00:12.843515Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3-yr corrected net under implementation-cost assumptions (V2 LoE range × blended rate):\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>$175/h</th>\n",
       "      <th>$225/h</th>\n",
       "      <th>$275/h</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>low hours</th>\n",
       "      <td>16,647,036</td>\n",
       "      <td>16,352,036</td>\n",
       "      <td>16,057,036</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>mid hours</th>\n",
       "      <td>16,235,786</td>\n",
       "      <td>15,823,286</td>\n",
       "      <td>15,410,786</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>high hours</th>\n",
       "      <td>15,824,536</td>\n",
       "      <td>15,294,536</td>\n",
       "      <td>14,764,536</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "               $175/h     $225/h     $275/h\n",
       "low hours  16,647,036 16,352,036 16,057,036\n",
       "mid hours  16,235,786 15,823,286 15,410,786\n",
       "high hours 15,824,536 15,294,536 14,764,536"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Spread across the whole grid: $1.9M — the conclusion is not hostage to the implementation estimate; tokens and double-billing dominate.\n"
     ]
    }
   ],
   "source": [
    "# ── 3-yr corrected net across impl hours mode × blended rate ─────────\n",
    "def corrected_net_3yr(mode, rate):\n",
    "    _, iby, kby, ssby = build_impl_costs(sites, mode, rate, INCLUDE_KB_READINESS,\n",
    "                                         COPILOT_INCLUDES_ASIA, NA_EMAIL_EARLY)\n",
    "    total = {y: licence_by_year[y] + ps_by_year[y] + managed_by_year[y]\n",
    "                + current_by_year[y] + token_total_by_year[y]\n",
    "                + iby[y] + kby[y] + ssby[y] for y in YEARS}\n",
    "    _, net = case_flows(total, benefit_total_by_year)\n",
    "    return sum(net.values())\n",
    "\n",
    "\n",
    "impl_sens = pd.DataFrame(\n",
    "    {f\"${r}/h\": {m: corrected_net_3yr(m, r) for m in (\"low\", \"mid\", \"high\")}\n",
    "     for r in (175, 225, 275)})\n",
    "impl_sens.index = [f\"{m} hours\" for m in impl_sens.index]\n",
    "print(\"3-yr corrected net under implementation-cost assumptions \"\n",
    "      \"(V2 LoE range × blended rate):\")\n",
    "display(impl_sens)\n",
    "spread = float(impl_sens.max().max() - impl_sens.min().min())\n",
    "print(f\"Spread across the whole grid: {money(spread)} — the conclusion is not \"\n",
    "      f\"hostage to the implementation estimate; tokens and double-billing dominate.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46cd8894",
   "metadata": {},
   "source": [
    "<a id=\"section-10\"></a>\n",
    "\n",
    "## 10 · Verification & assertions\n",
    "\n",
    "The next cell re-derives key numbers independently and **raises on any failure**, so\n",
    "`jupyter nbconvert --execute` acts as a regression gate for this notebook. Config-dependent\n",
    "checks are guarded so the cell stays valid after you edit the CONFIG cells."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "6516ea96",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.846063Z",
     "iopub.status.busy": "2026-07-13T22:00:12.845859Z",
     "iopub.status.idle": "2026-07-13T22:00:12.880547Z",
     "shell.execute_reply": "2026-07-13T22:00:12.879217Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All assertions passed.\n",
      "  benefits 3-yr $15.0M · tokens 3-yr $6.6M · corrected net 3-yr -$6.1M\n"
     ]
    }
   ],
   "source": [
    "def _approx(got, want, tol=0.5):\n",
    "    assert abs(got - want) <= tol, f\"got {got:,.2f}, want {want:,.2f}\"\n",
    "\n",
    "\n",
    "# Section 1 — contract mechanics. Engine pins use explicit default arguments so the\n",
    "# gate tests tokencalc.appendix4, not the current widget state; live-state\n",
    "# checks only run when the inputs sit at their defaults.\n",
    "_seed_check = current_state_inputs(sites)\n",
    "_approx(_seed_check[\"annual_cost\"].sum(), 7_300_000)\n",
    "_l12 = licence_costs_by_year(12)\n",
    "_approx(_l12[2026], 0)\n",
    "_approx(_l12[2027], 4_300_000)                       # deck rate stays verbatim\n",
    "assert DEFAULT_RAMP_MONTHS == 6                      # 🟢 order form\n",
    "_approx(licence_costs_by_year()[2026], 2_150_000)    # 6-mo ramp: half-year bills in 2026\n",
    "# NTT SOW — billing milestones + managed services (🟢 contractual)\n",
    "_approx(PS_CONTRACTED_TOTAL, 2_025_446.48, tol=0.01)\n",
    "_ps_c = ps_costs_by_year(contracted=True)\n",
    "_approx(_ps_c[2026], 607_633.94 + 405_089.30 + 167_000, tol=0.01)  # 30% + 20% + training\n",
    "_approx(_ps_c[2027], 607_633.94 + 405_089.30, tol=0.01)            # 30% + 20%\n",
    "_approx(managed_services_by_year()[2026],\n",
    "        MANAGED_SERVICES_ANNUAL * 2 / 12, tol=0.01)                # Nov–Dec after go-live\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",
    "# This notebook invoices the full contracted annual in year 1 (no ramp net-down):\n",
    "for _y in YEARS:\n",
    "    _approx(licence_by_year[_y], LICENCE_ANNUAL)\n",
    "_contracts_at_default = (\n",
    "    (current_state[\"annual_cost\"] - _seed_check[\"annual_cost\"]).abs().max() < 1\n",
    "    and all(t == dt.date(2027, 12, 31) for t in current_state[\"contract_termination\"])\n",
    ")\n",
    "# Current spend is excluded from this case → the run-off line is $0 in\n",
    "# every year regardless of the (reference-only) widget state.\n",
    "for _y in YEARS:\n",
    "    _approx(current_by_year[_y], 0)\n",
    "\n",
    "# Section 2 — verbatim benefits reproduce the deck\n",
    "_approx(benefit_total_by_year[2026], 0)\n",
    "_approx(sum(benefit_total_by_year.values()), verbatim[\"three_yr\"].sum())\n",
    "assert abs(sum(benefit_total_by_year.values()) - SLIDE_TOTALS[\"total_3yr\"]) <= _tol(15e6)\n",
    "for r in REGIONS:\n",
    "    _approx(benefits_long.query(\"region == @r\")[\"benefit\"].sum(),\n",
    "            verbatim.query(\"region == @r\")[\"three_yr\"].sum())\n",
    "\n",
    "# Section 3 — token hand-checks (independent derivations)\n",
    "sta = tokens_long.query(\"cost_line == 'Speech & Text Analytics [named]'\")\n",
    "_named = {s.site_name: s.named_users for s in sites}\n",
    "_sta_2028 = sum(_named[n] * 30 * TOKEN_ROLLOUT.live_months_in_year(n, 3) for n in ALL_SITES)\n",
    "_approx(sta.query(\"year == 2028\")[\"annual_cost\"].sum(), _sta_2028)\n",
    "if not COPILOT_INCLUDES_ASIA:\n",
    "    _approx(_sta_2028, 751_680)          # PM timeline: every site live all 12 months in 2028\n",
    "    cp = tokens_long.query(\"cost_line == 'Agent Copilot [named]' and year == 2028\")\n",
    "    _approx(cp[\"annual_cost\"].sum(), 1_490 * 40 * 12)   # = $715,200\n",
    "assert (tokens_long.query(\"cost_line == 'AI Summary & Insights'\")[\"annual_cost\"] == 0).all(), \\\n",
    "    \"Rule 1: Copilot must cover AI Summary at Copilot sites\"\n",
    "assert math.ceil(1_214_358 * DEFAULT_METERS[\"Predictive Routing\"].tokens_per_unit) == 71_433\n",
    "# PM timeline: NA live Oct 2026 → tokens DO accrue in 2026 (NA-only).\n",
    "_approx(tokens_long.query(\"year == 2026\")[\"annual_cost\"].sum(), 424_350)\n",
    "\n",
    "# Section 4 — impl model reconciles with the V2 doc (explicit default args)\n",
    "_, _impl_p, _kb_p, _steady_p = build_impl_costs(sites, \"mid\", 225, True, False, True)\n",
    "_approx(sum(_impl_p.values()), 5_850 * 225)        # $1,316,250 — V2's \"$1.3M\"\n",
    "_approx(sum(_kb_p.values()), 1_000 * 225)\n",
    "_approx(sum(_steady_p.values()), 700 * 225 * 2)\n",
    "\n",
    "# Section 5 — cost stacks\n",
    "_approx(sum(pitched_total_by_year.values()),\n",
    "        3 * TCO_VERBATIM[\"ccaas_annual\"] + ps_pitched_by_year[2026])  # ≈ deck's $15.4M\n",
    "_approx(sum(corrected_total_by_year.values()),\n",
    "        sum(licence_by_year.values()) + sum(ps_by_year.values())\n",
    "        + sum(managed_by_year.values()) + sum(current_by_year.values())\n",
    "        + sum(token_total_by_year.values()) + sum(impl_kb_by_year.values())\n",
    "        + sum(steady_by_year.values()))\n",
    "\n",
    "# Section 8 — flows tie out\n",
    "for y in YEARS:\n",
    "    _approx(net_corrected[y],\n",
    "            benefit_total_by_year[y] - (corrected_total_by_year[y] - BASELINE_ANNUAL))\n",
    "\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'])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8275984f",
   "metadata": {},
   "source": [
    "<a id=\"section-11\"></a>\n",
    "\n",
    "## 11 · Risks, gaps & next steps\n",
    "\n",
    "**Findings to lead with**\n",
    "- **Predictive Routing is net-negative standalone at claimed scope:** ~$1.8M/yr in tokens at 100%\n",
    "  eligibility vs a $470K/yr claimed benefit. `PR_ELIGIBILITY` is the honest knob — Genesys likely\n",
    "  intends PR on a queue subset; ask which queues.\n",
    "- **2026 is a pure cost year** — $0 benefits (earliest region, NA, realizes Jan 2027 on the PM\n",
    "  timeline), while CTM already funds PS, licences (post-ramp) and early AI implementation.\n",
    "- **Current spend is excluded** (CTM directive): no double-billing penalty and no\n",
    "  cost-avoidance credit. This is a harsher frame than the deck's — the standalone Genesys\n",
    "  programme is thin on its own, which is exactly why the **virtual-agents alternative**\n",
    "  (`ctm_business_case_virtual_agents.ipynb`) is the case to lead with.\n",
    "- The cost corrections are not one-directional: the ramp credit (6 licence-free months, 🟢\n",
    "  order form) and the contracted licence rate ($3.2M/yr vs the pitched $4.3M) improve the\n",
    "  case, while the SOW's managed services ($411K/yr from MCX go-live) add a run-rate cost\n",
    "  the deck's TCO never carried.\n",
    "\n",
    "**Known gaps / data wanted**\n",
    "- Non-NAM site volumes & AHTs are `tokencalc` placeholders (🟡) — all non-NA token figures inherit\n",
    "  that. Regional current-cost split is an agent-share allocation pending real contract data.\n",
    "- Email Auto-Respond token rate is unpublished (🔴) — section 9 bounds it; even the worst corner is small\n",
    "  relative to the Email benefit claim.\n",
    "- Early-termination fees, co-term options, and migration costs beyond PS/impl are not modelled.\n",
    "- The smell test (section 4) flags V2 implementation hours as below the 20–40% industry band — V2\n",
    "  deliberately strips vendor inflation; section 9 shows the conclusion is robust across the whole\n",
    "  V2 range × rate grid.\n",
    "- Deck internal rounding: cell-level benefits cross-foot to $15.04M vs the $15.0M headline\n",
    "  (and $13.72M vs $13.6M annual); tolerated, not \"fixed\".\n",
    "\n",
    "**Next steps**\n",
    "- Replace section 4's hours-range model with the activity-level `ImplementationEffort` engine sketched in\n",
    "  `docs/ctm_ai_labour_estimate.md` (dataclasses + `tokencalc/implementation.py` + tests).\n",
    "- Confirm contracted token rates and regional pricing (EU/AU/APAC flagged TBD at $1.00 list).\n",
    "- Feed real per-region current-platform contract values and termination dates into section 1."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ce1bad2",
   "metadata": {},
   "source": [
    "<a id=\"section-12\"></a>\n",
    "\n",
    "## 12 · Machine-readable data appendix\n",
    "\n",
    "Every number behind the figures above, emitted as markdown tables plus a JSON block, so an\n",
    "LLM can draft the client report or presentation directly from the HTML/markdown export\n",
    "(`python scripts/export_report.py`). Plotly figures export as JavaScript an LLM cannot\n",
    "read — this section carries the data. The dump renders **backstage** (JupyterLab and the exports) and stays hidden in the Mercury app. The tables reflect the **current** input state, so a\n",
    "client-customized session exports a client-customized appendix.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "c4c025b5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-13T22:00:12.882594Z",
     "iopub.status.busy": "2026-07-13T22:00:12.882419Z",
     "iopub.status.idle": "2026-07-13T22:00:12.931226Z",
     "shell.execute_reply": "2026-07-13T22:00:12.930400Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "#### Current-state contracts by region (inputs)\n",
      "\n",
      "| region   |   agents |   annual_cost | contract_termination   | confidence                                      |\n",
      "|:---------|---------:|--------------:|:-----------------------|:------------------------------------------------|\n",
      "| NA       |      890 |     3,348,969 | 2027-12-31             | 🟡 agent-share allocation of the verbatim total |\n",
      "| ANZ      |      180 |       677,320 | 2027-12-31             | 🟡 agent-share allocation of the verbatim total |\n",
      "| EMEA     |      320 |     1,204,124 | 2027-12-31             | 🟡 agent-share allocation of the verbatim total |\n",
      "| ASIA     |      550 |     2,069,588 | 2027-12-31             | 🟡 agent-share allocation of the verbatim total |\n",
      "\n",
      "#### Verbatim Genesys benefits — capability × region (3-yr $)\n",
      "\n",
      "| capability         |     ANZ |   ASIA |   EMEA |      NA |\n",
      "|:-------------------|--------:|-------:|-------:|--------:|\n",
      "| Agent Copilot      | 3900000 |      0 |  64000 | 3400000 |\n",
      "| WFM                | 1400000 | 914000 | 687000 |       0 |\n",
      "| Email              |  143000 |  93000 | 235000 | 2500000 |\n",
      "| STA                |  105000 |  72000 | 131000 |  506000 |\n",
      "| Predictive Routing |  302000 |  51000 |   6000 |  167000 |\n",
      "| Supervisor Copilot |   27000 |      0 |  49000 |  291000 |\n",
      "\n",
      "#### Benefits by capability × year (schedule-phased, $)\n",
      "\n",
      "| capability         |   2026 |      2027 |      2028 |\n",
      "|:-------------------|-------:|----------:|----------:|\n",
      "| Agent Copilot      |      0 | 2,700,600 | 4,663,400 |\n",
      "| WFM                |      0 |   624,800 | 2,376,200 |\n",
      "| Email              |      0 | 1,379,750 | 1,591,250 |\n",
      "| STA                |      0 |   331,650 |   482,350 |\n",
      "| Predictive Routing |      0 |   161,400 |   364,600 |\n",
      "| Supervisor Copilot |      0 |   171,850 |   195,150 |\n",
      "\n",
      "#### AI token cost by meter × year ($)\n",
      "\n",
      "| cost_line                               |    2026 |      2027 |      2028 |      3-yr |\n",
      "|:----------------------------------------|--------:|----------:|----------:|----------:|\n",
      "| Predictive Routing                      | 214,299 | 1,362,323 | 1,851,816 | 3,428,438 |\n",
      "| Agent Copilot [named]                   | 114,000 |   662,400 |   715,200 | 1,491,600 |\n",
      "| Speech & Text Analytics [named]         |  85,500 |   550,620 |   751,680 | 1,387,800 |\n",
      "| Email AI (Auto-Respond)                 |  10,551 |    67,080 |    91,500 |   169,131 |\n",
      "| AI Translate                            |       0 |    34,185 |    69,540 |   103,725 |\n",
      "| AI Summary & Insights                   |       0 |         0 |         0 |         0 |\n",
      "| WFM (no token meter — licence-included) |       0 |         0 |         0 |         0 |\n",
      "| TOTAL                                   | 424,350 | 2,676,608 | 3,479,736 | 6,580,694 |\n",
      "\n",
      "#### AI implementation by workstream ($, phased)\n",
      "\n",
      "| workstream                                           |   hours |      cost |      2026 |    2027 |    2028 |\n",
      "|:-----------------------------------------------------|--------:|----------:|----------:|--------:|--------:|\n",
      "| Agent Copilot                                        |   1,500 |   337,500 |   311,832 |  25,668 |       0 |\n",
      "| Cross-cutting (PM, governance, testing, integration) |   1,400 |   315,000 |   257,242 |  57,758 |       0 |\n",
      "| Email Auto-Respond                                   |   1,100 |   247,500 |   202,119 |  45,381 |       0 |\n",
      "| KB readiness (prerequisite)                          |   1,000 |   225,000 |   183,744 |  41,256 |       0 |\n",
      "| Predictive Routing                                   |     550 |   123,750 |   101,059 |  22,691 |       0 |\n",
      "| STA                                                  |   1,000 |   225,000 |   183,744 |  41,256 |       0 |\n",
      "| Supervisor Copilot                                   |     300 |    67,500 |    62,366 |   5,134 |       0 |\n",
      "| Steady-state tuning (2027-28)                        |   1,400 |   315,000 |         0 | 157,500 | 157,500 |\n",
      "| TOTAL                                                |   8,250 | 1,856,250 | 1,302,108 | 396,642 | 157,500 |\n",
      "\n",
      "#### Corrected programme cost stack ($)\n",
      "\n",
      "|                                                     |      2026 |      2027 |      2028 |      3-yr |\n",
      "|:----------------------------------------------------|----------:|----------:|----------:|----------:|\n",
      "| CCaaS platform licences (contracted, ramp-adjusted) | 3,200,000 | 3,200,000 | 3,200,000 | 9,600,000 |\n",
      "| NTT PS + training (SOW billing milestones)          | 1,179,723 | 1,012,723 |         0 | 2,192,446 |\n",
      "| NTT managed services (from MCX go-live)             |    68,486 |   410,918 |   410,918 |   890,323 |\n",
      "| AI token consumption                                |   424,350 | 2,676,608 | 3,479,736 | 6,580,694 |\n",
      "| AI implementation + KB readiness                    | 1,302,108 |   239,142 |         0 | 1,541,250 |\n",
      "| AI steady-state tuning                              |         0 |   157,500 |   157,500 |   315,000 |\n",
      "\n",
      "#### As-pitched cost stack ($)\n",
      "\n",
      "|                                               |      2026 |      2027 |      2028 |       3-yr |\n",
      "|:----------------------------------------------|----------:|----------:|----------:|-----------:|\n",
      "| CCaaS platform licences (as pitched, no ramp) | 4,300,000 | 4,300,000 | 4,300,000 | 12,900,000 |\n",
      "| Base professional services + training         | 2,567,000 |         0 |         0 |  2,567,000 |\n",
      "\n",
      "#### KPIs — as pitched vs corrected\n",
      "\n",
      "|                              | As pitched (deck)   | Corrected   |\n",
      "|:-----------------------------|:--------------------|:------------|\n",
      "| 3-yr benefits                | $15.0M              | $15.0M      |\n",
      "| 3-yr incremental cost        | $15.5M              | $21.1M      |\n",
      "| 3-yr net                     | -$424K              | -$6.1M      |\n",
      "| ROI (net ÷ incremental cost) | -3%                 | -29%        |\n",
      "| NPV @ 13.5% (deck rate)      | -$1.5M              | -$5.6M      |\n",
      "| NPV @ 8.0% (CTM treasury)    | -$1.2M              | -$5.8M      |\n",
      "| Payback                      | beyond 2028         | beyond 2028 |\n",
      "\n",
      "#### Model state (JSON)\n",
      "\n",
      "```json\n",
      "{\n",
      "  \"benefits_by_year\": {\n",
      "    \"2026\": 0,\n",
      "    \"2027\": 5370050,\n",
      "    \"2028\": 9672950\n",
      "  },\n",
      "  \"corrected_cost_by_year\": {\n",
      "    \"2026\": 6174668,\n",
      "    \"2027\": 7696892,\n",
      "    \"2028\": 7248154\n",
      "  },\n",
      "  \"pitched_cost_by_year\": {\n",
      "    \"2026\": 6867000,\n",
      "    \"2027\": 4300000,\n",
      "    \"2028\": 4300000\n",
      "  },\n",
      "  \"net_by_year_corrected\": {\n",
      "    \"2026\": -6174668,\n",
      "    \"2027\": -2326842,\n",
      "    \"2028\": 2424796\n",
      "  },\n",
      "  \"kpis_corrected\": {\n",
      "    \"benefits_3yr\": 15043000,\n",
      "    \"incremental_cost_3yr\": 21119714,\n",
      "    \"net_3yr\": -6076714,\n",
      "    \"roi\": -0.2877,\n",
      "    \"npv\": -5588081,\n",
      "    \"discount_rate\": 0.135,\n",
      "    \"payback\": \"beyond 2028\"\n",
      "  },\n",
      "  \"kpis_pitched\": {\n",
      "    \"benefits_3yr\": 15043000,\n",
      "    \"incremental_cost_3yr\": 15467000,\n",
      "    \"net_3yr\": -424000,\n",
      "    \"roi\": -0.0274,\n",
      "    \"npv\": -1544853,\n",
      "    \"discount_rate\": 0.135,\n",
      "    \"payback\": \"beyond 2028\"\n",
      "  },\n",
      "  \"assumptions\": {\n",
      "    \"ramp_months\": 6,\n",
      "    \"licence_annual\": 3200000,\n",
      "    \"deck_ccaas_annual\": 4300000,\n",
      "    \"ps_contracted_total\": 2025446.48,\n",
      "    \"ps_milestones\": [\n",
      "      {\n",
      "        \"name\": \"SOW Effective Date\",\n",
      "        \"date\": \"2026-03-15\",\n",
      "        \"amount\": 607633.94\n",
      "      },\n",
      "      {\n",
      "        \"name\": \"Start of client UAT (first region, NA)\",\n",
      "        \"date\": \"2026-10-16\",\n",
      "        \"amount\": 405089.3\n",
      "      },\n",
      "      {\n",
      "        \"name\": \"Start of client UAT (last region, APAC)\",\n",
      "        \"date\": \"2027-10-01\",\n",
      "        \"amount\": 607633.94\n",
      "      },\n",
      "      {\n",
      "        \"name\": \"Completion of last go-live migration\",\n",
      "        \"date\": \"2027-11-01\",\n",
      "        \"amount\": 405089.3\n",
      "      }\n",
      "    ],\n",
      "    \"managed_services_annual\": 410918.4,\n",
      "    \"mcx_go_live\": \"2026-10-16\",\n",
      "    \"hours_mode\": \"mid\",\n",
      "    \"blended_rate\": 225,\n",
      "    \"include_kb_readiness\": true,\n",
      "    \"copilot_includes_asia\": false,\n",
      "    \"na_email_early\": true,\n",
      "    \"pr_eligibility\": 1.0,\n",
      "    \"email_auto_respond_rate\": 0.255,\n",
      "    \"email_tokens_per_msg\": 0.05,\n",
      "    \"use_contracted_rates\": false,\n",
      "    \"discount_rate\": 0.135,\n",
      "    \"current_state_by_region\": {\n",
      "      \"NA\": {\n",
      "        \"annual_cost\": 3348969,\n",
      "        \"contract_termination\": \"2027-12-31\"\n",
      "      },\n",
      "      \"ANZ\": {\n",
      "        \"annual_cost\": 677320,\n",
      "        \"contract_termination\": \"2027-12-31\"\n",
      "      },\n",
      "      \"EMEA\": {\n",
      "        \"annual_cost\": 1204124,\n",
      "        \"contract_termination\": \"2027-12-31\"\n",
      "      },\n",
      "      \"ASIA\": {\n",
      "        \"annual_cost\": 2069588,\n",
      "        \"contract_termination\": \"2027-12-31\"\n",
      "      }\n",
      "    }\n",
      "  }\n",
      "}\n",
      "```\n"
     ]
    }
   ],
   "source": [
    "# ── Data appendix — LLM-readable dump of every model output ──────────\n",
    "# Renders backstage only (JupyterLab / nbconvert exports) — hidden on\n",
    "# the Mercury stage, where the narrative and figures carry the story.\n",
    "import json as _json\n",
    "\n",
    "\n",
    "def _section(title, df, **kw):\n",
    "    backstage(f\"\\n#### {title}\\n\")\n",
    "    backstage(df.to_markdown(floatfmt=\",.0f\", **kw))\n",
    "\n",
    "\n",
    "_section(\"Current-state contracts by region (inputs)\",\n",
    "         current_state.reset_index().drop(columns=[\"share\"]), index=False)\n",
    "_section(\"Verbatim Genesys benefits — capability × region (3-yr $)\",\n",
    "         verbatim.pivot_table(index=\"capability\", columns=\"region\",\n",
    "                              values=\"three_yr\", aggfunc=\"sum\").reindex(CAPABILITIES))\n",
    "_section(\"Benefits by capability × year (schedule-phased, $)\",\n",
    "         benefits_long.pivot_table(index=\"capability\", columns=\"year\",\n",
    "                                   values=\"benefit\", aggfunc=\"sum\").reindex(CAPABILITIES))\n",
    "_section(\"AI token cost by meter × year ($)\", tokens_pivot.drop(columns=[\"conf\"]))\n",
    "_section(\"AI implementation by workstream ($, phased)\", impl_summary)\n",
    "_section(\"Corrected programme cost stack ($)\", corrected_costs)\n",
    "_section(\"As-pitched cost stack ($)\", pitched_costs)\n",
    "_section(\"KPIs — as pitched vs corrected\", kpis_fmt)\n",
    "\n",
    "\n",
    "def _jval(v):\n",
    "    if isinstance(v, float):\n",
    "        return round(v, 4) if abs(v) < 10 else round(v)\n",
    "    return v\n",
    "\n",
    "\n",
    "backstage(\"\\n#### Model state (JSON)\\n\")\n",
    "backstage(\"```json\")\n",
    "backstage(_json.dumps({\n",
    "    \"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",
    "    \"net_by_year_corrected\": {str(y): round(net_corrected[y]) for y in YEARS},\n",
    "    \"kpis_corrected\": {k: _jval(v) for k, v in kpi_corrected.items()},\n",
    "    \"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",
    "        \"ps_contracted_total\": round(PS_CONTRACTED_TOTAL, 2),\n",
    "        \"ps_milestones\": [{\"name\": m[\"name\"], \"date\": m[\"date\"].isoformat(),\n",
    "                           \"amount\": m[\"amount\"]} for m in PS_MILESTONES],\n",
    "        \"managed_services_annual\": MANAGED_SERVICES_ANNUAL,\n",
    "        \"mcx_go_live\": MCX_GO_LIVE.isoformat(),\n",
    "        \"hours_mode\": HOURS_MODE,\n",
    "        \"blended_rate\": BLENDED_RATE,\n",
    "        \"include_kb_readiness\": INCLUDE_KB_READINESS,\n",
    "        \"copilot_includes_asia\": COPILOT_INCLUDES_ASIA,\n",
    "        \"na_email_early\": NA_EMAIL_EARLY,\n",
    "        \"pr_eligibility\": PR_ELIGIBILITY,\n",
    "        \"email_auto_respond_rate\": EMAIL_AUTO_RESPOND_RATE,\n",
    "        \"email_tokens_per_msg\": EMAIL_AUTORESPOND_TOKENS_PER_MSG,\n",
    "        \"use_contracted_rates\": USE_CONTRACTED_RATES,\n",
    "        \"discount_rate\": DISCOUNT_RATE,\n",
    "        \"current_state_by_region\": {\n",
    "            r: {\"annual_cost\": round(float(current_state.loc[r, \"annual_cost\"])),\n",
    "                \"contract_termination\": current_state.loc[r, \"contract_termination\"].isoformat()}\n",
    "            for r in REGIONS},\n",
    "    },\n",
    "}, indent=2))\n",
    "backstage(\"```\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "61677b64-31c4-4b42-bba7-2fe4031c6e5d",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {
     "062c84e8b0e24ee4a9b24c732665c7f8": {
      "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
      }
     },
     "10b3118fbe3b46c89d22e101c9a280a8": {
      "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
      }
     },
     "1b74ac40a60341a9998d45d63a4275a4": {
      "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
      }
     },
     "1e34f12588214862ad5402e3843bca43": {
      "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
      }
     },
     "1ebe071b962145cdbed25a1ae3dc8ccb": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.number.NumberInputWidget",
       "_css": "\n    .mljar-number-container {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n      font-size: 14px;\n      color: #0f172a;\n      margin-bottom: 8px;\n      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      margin-bottom: 4px;\n      font-weight: 600;\n    }\n\n    .mljar-number-field-row {\n      display: flex;\n      align-items: stretch;\n      width: 100%;\n      min-height: 40px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-sizing: border-box;\n      overflow: hidden;\n    }\n\n    .mljar-number-input {\n      flex: 1 1 auto;\n      min-width: 0;\n      min-height: 100%;\n      padding: 7px 10px;\n      border: 0;\n      border-radius: 0;\n      background: #ffffff;\n      box-sizing: border-box;\n      background-color: #ffffff !important;\n      color: #0f172a !important;\n      font: inherit;\n      line-height: 1.2;\n      -moz-appearance: textfield;\n    }\n\n    .mljar-number-input::-webkit-outer-spin-button,\n    .mljar-number-input::-webkit-inner-spin-button {\n      -webkit-appearance: none;\n      margin: 0;\n    }\n\n    .mljar-number-input:disabled {\n      background: #f5f5f5;\n      color: #888;\n      cursor: not-allowed;\n    }\n\n    .mljar-number-input:focus {\n      outline: none;\n    }\n\n    .mljar-number-field-row:focus-within {\n      border-color: #007bff;\n      border-width: 2px;\n      box-shadow: none;\n    }\n\n    .mljar-number-field-row:focus-within .mljar-number-controls {\n      border-left-color: #007bff;\n    }\n\n    .mljar-number-controls {\n      display: flex;\n      align-items: stretch;\n      flex: 0 0 auto;\n      border-left: 1px solid #cfd1d5;\n      background: #f1f1f2;\n    }\n\n    .mljar-number-step-btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      width: 38px;\n      min-width: 38px;\n      min-height: 100%;\n      border: 0;\n      border-radius: 0;\n      background: transparent;\n      color: #0f172a;\n      font: inherit;\n      font-size: 18px;\n      font-weight: 700;\n      line-height: 1;\n      cursor: pointer;\n      padding: 0;\n      user-select: none;\n      -webkit-user-select: none;\n      touch-action: manipulation;\n      transition: background-color 0.14s ease, color 0.14s ease;\n    }\n\n    .mljar-number-step-up {\n      border-left: 1px solid #cfd1d5;\n    }\n\n    .mljar-number-step-btn:hover {\n      background: #f3f3f4;\n    }\n\n    .mljar-number-step-btn:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:focus-visible {\n      outline: none;\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:disabled {\n      background: #f5f5f5;\n      color: #aaa;\n      cursor: not-allowed;\n    }\n\n    @media (max-width: 768px) {\n      .mljar-number-field-row {\n        min-height: 44px;\n      }\n\n      .mljar-number-input {\n        min-height: 44px;\n        padding: 8px 12px;\n      }\n\n      .mljar-number-step-btn {\n        font-size: 19px;\n        width: 44px;\n        min-width: 44px;\n      }\n    }\n    ",
       "_dom_classes": [],
       "_esm": "\n    function render({ model, el }) {\n      const container = document.createElement(\"div\");\n      container.classList.add(\"mljar-number-container\");\n\n      const topLabel = document.createElement(\"div\");\n      topLabel.classList.add(\"mljar-number-label\");\n\n      const fieldRow = document.createElement(\"div\");\n      fieldRow.classList.add(\"mljar-number-field-row\");\n\n      const input = document.createElement(\"input\");\n      input.type = \"number\";\n      input.classList.add(\"mljar-number-input\");\n\n      const decrementBtn = document.createElement(\"button\");\n      decrementBtn.type = \"button\";\n      decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n      decrementBtn.textContent = \"-\";\n      decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n      const incrementBtn = document.createElement(\"button\");\n      incrementBtn.type = \"button\";\n      incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n      incrementBtn.textContent = \"+\";\n      incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n      const controls = document.createElement(\"div\");\n      controls.classList.add(\"mljar-number-controls\");\n\n      controls.appendChild(decrementBtn);\n      controls.appendChild(incrementBtn);\n      fieldRow.appendChild(input);\n      fieldRow.appendChild(controls);\n\n      container.appendChild(topLabel);\n      container.appendChild(fieldRow);\n      el.appendChild(container);\n\n      function clamp(val, min, max) {\n        if (Number.isFinite(min) && val < min) return min;\n        if (Number.isFinite(max) && val > max) return max;\n        return val;\n      }\n\n      function normalizeStep(step) {\n        return Number.isFinite(step) && step > 0 ? step : 1;\n      }\n\n      function snapToStep(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = Math.round((value - base) / safeStep);\n        const snapped = base + steps * safeStep;\n        const precision = Math.max(\n          0,\n          (String(safeStep).split(\".\")[1] || \"\").length\n        );\n\n        return Number(snapped.toFixed(precision + 2));\n      }\n\n      function isOnStepGrid(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = (value - base) / safeStep;\n        const nearest = Math.round(steps);\n        const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n        return Math.abs(steps - nearest) <= epsilon;\n      }\n\n      function getCurrentBounds() {\n        return {\n          min: Number(model.get(\"min\")),\n          max: Number(model.get(\"max\")),\n        };\n      }\n\n      function isTransientDraft(raw) {\n        return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n      }\n\n      let isEditing = false;\n      const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n      function clearPendingDraftCommit() {\n        if (debounceTimer) clearTimeout(debounceTimer);\n        pendingDraftValue = null;\n      }\n\n      function parseDraftValue(rawValue) {\n        const raw = String(rawValue).trim();\n        if (isTransientDraft(raw)) {\n          return { kind: \"transient\" };\n        }\n\n        const value = Number(raw);\n        if (!Number.isFinite(value)) {\n          return { kind: \"invalid\" };\n        }\n\n        return { kind: \"number\", value };\n      }\n\n      function commitValue(nextValue, saveNow = true) {\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        const parsed = parseDraftValue(nextValue);\n        if (parsed.kind !== \"number\") {\n          syncFromModel();\n          return;\n        }\n\n        let v = parsed.value;\n        v = clamp(v, min, max);\n        v = snapToStep(v, min, step);\n        v = clamp(v, min, max);\n        input.value = String(v);\n        model.set(\"value\", v);\n\n        if (saveNow) {\n          model.save_changes();\n        }\n      }\n\n      function syncFromModel() {\n        topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n        const min = Number(model.get(\"min\"));\n        const max = Number(model.get(\"max\"));\n        const step = Number(model.get(\"step\"));\n\n        if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n        if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n        if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n        const v = Number(model.get(\"value\"));\n        if (!isEditing) {\n          input.value = Number.isFinite(v) ? String(v) : \"\";\n        }\n\n        const disabled = !!model.get(\"disabled\");\n        input.disabled = disabled;\n        incrementBtn.disabled = disabled;\n        decrementBtn.disabled = disabled;\n\n        const hidden = !!model.get(\"hidden\");\n        container.style.display = hidden ? \"none\" : \"flex\";\n      }\n\n      let debounceTimer = null;\n      let pendingDraftValue = null;\n      input.addEventListener(\"focus\", () => {\n        isEditing = true;\n      });\n\n      input.addEventListener(\"input\", () => {\n        if (model.get(\"disabled\")) return;\n\n        const parsed = parseDraftValue(input.value);\n        if (parsed.kind !== \"number\") {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        const v = parsed.value;\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        if (Number.isFinite(min) && v < min) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (Number.isFinite(max) && v > max) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (!isOnStepGrid(v, min, step)) {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        pendingDraftValue = v;\n        if (debounceTimer) clearTimeout(debounceTimer);\n        debounceTimer = setTimeout(() => {\n          if (pendingDraftValue === null) return;\n          model.set(\"value\", pendingDraftValue);\n          model.save_changes();\n          pendingDraftValue = null;\n        }, INPUT_COMMIT_DEBOUNCE_MS);\n      });\n\n      input.addEventListener(\"blur\", () => {\n        isEditing = false;\n        clearPendingDraftCommit();\n        commitValue(input.value, true);\n      });\n\n      input.addEventListener(\"keydown\", event => {\n        if (event.key === \"Enter\") {\n          event.preventDefault();\n          input.blur();\n        }\n      });\n\n      incrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base + step, min, step));\n      });\n\n      decrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base - step, min, step));\n      });\n\n      model.on(\"change:value\", syncFromModel);\n      model.on(\"change:min\", syncFromModel);\n      model.on(\"change:max\", syncFromModel);\n      model.on(\"change:step\", syncFromModel);\n      model.on(\"change:label\", syncFromModel);\n      model.on(\"change:disabled\", syncFromModel);\n      model.on(\"change:hidden\", syncFromModel);\n\n      syncFromModel();\n\n      // ---- read cell id (no DOM modifications) ----\n      /*\n      const ID_ATTR = \"data-cell-id\";\n      const hostWithId = el.closest(`[${ID_ATTR}]`);\n      const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n      if (cellId) {\n        model.set(\"cell_id\", cellId);\n        model.save_changes();\n        model.send({ type: \"cell_id_detected\", value: cellId });\n      } else {\n        const mo = new MutationObserver(() => {\n          const host = el.closest(`[${ID_ATTR}]`);\n          const newId = host?.getAttribute(ID_ATTR);\n          if (newId) {\n            model.set(\"cell_id\", newId);\n            model.save_changes();\n            model.send({ type: \"cell_id_detected\", value: newId });\n            mo.disconnect();\n          }\n        });\n        mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n      }*/\n    }\n    export default { render };\n    ",
       "_model_module": "anywidget",
       "_model_module_version": "~0.11.*",
       "_model_name": "AnyModel",
       "_view_count": null,
       "_view_module": "anywidget",
       "_view_module_version": "~0.11.*",
       "_view_name": "AnyView",
       "cell_id": "",
       "disabled": false,
       "hidden": false,
       "label": "ASIA — current platform cost ($/yr)",
       "layout": "IPY_MODEL_4122ab35287242e2809394da2226977c",
       "layout_path": null,
       "max": 8000000.0,
       "min": 0.0,
       "position": "inline",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 10000.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 2069588.0
      }
     },
     "1ebe6cf8ed324e68963a737c2f382822": {
      "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
      }
     },
     "26ad610c103f4511853bbdcfd637a108": {
      "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": "Auto-respond tokens per message 🟡",
       "layout": "IPY_MODEL_5d76f9f81bdd44bfb6c72776727c26e9",
       "layout_path": null,
       "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
      }
     },
     "27e26a695a9d48c9bae29e3402ae1132": {
      "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_65308a5920c44024a346f04a04539929",
       "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
      }
     },
     "336a0118e33249788ad6521ecbba63d9": {
      "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_3a09e832a71d4539bc6b6ea49bee0371",
       "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"
      }
     },
     "345414b0120d421d8234208704d27582": {
      "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
      }
     },
     "3946415fb4ac441fb7912a77a2f143d4": {
      "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_89ecf935020b4804acd749e521c25259",
       "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
      }
     },
     "3a09e832a71d4539bc6b6ea49bee0371": {
      "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
      }
     },
     "4122ab35287242e2809394da2226977c": {
      "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
      }
     },
     "4c89a258ad0048e6a2797233bd2c04d2": {
      "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_345414b0120d421d8234208704d27582",
       "layout_path": null,
       "n_toggles": 0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": true
      }
     },
     "4f18a5dd5d4245a9b7558564825d4a09": {
      "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_e89c1e81df274bbfbacecb23781ef45b",
       "layout_path": null,
       "max": 24.0,
       "min": 0.0,
       "position": "inline",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 1.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 6.0
      }
     },
     "4f1b8f8777ef43deb835fe53c4dd2994": {
      "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_619bb17b8bcc485bb479af80cbe2fd4c",
       "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
      }
     },
     "50400e98530e4b12ad374e31fe989863": {
      "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
      }
     },
     "5985ba7b869548d4a55b0490fb0fa302": {
      "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_5f3ae07d38ec488da4e4e00d05eed819",
       "layout_path": null,
       "n_toggles": 0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": false
      }
     },
     "5d76f9f81bdd44bfb6c72776727c26e9": {
      "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
      }
     },
     "5f3ae07d38ec488da4e4e00d05eed819": {
      "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
      }
     },
     "6190da45d1d444118fbd3272d96d09c4": {
      "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_1ebe6cf8ed324e68963a737c2f382822",
       "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
      }
     },
     "619bb17b8bcc485bb479af80cbe2fd4c": {
      "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
      }
     },
     "649ff563533144869b6a626a820a7d90": {
      "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
      }
     },
     "652b98f4c90c4007b1e4536218f9361a": {
      "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
      }
     },
     "65308a5920c44024a346f04a04539929": {
      "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
      }
     },
     "6ada443fa3844d198bc69221d8867bbc": {
      "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_50400e98530e4b12ad374e31fe989863",
       "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"
      }
     },
     "7013bd8d97d040cbb8172345f8a2829b": {
      "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_062c84e8b0e24ee4a9b24c732665c7f8",
       "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
      }
     },
     "7971c319317c4e90bdfe69607c9e0de7": {
      "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_1b74ac40a60341a9998d45d63a4275a4",
       "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"
      }
     },
     "7a38470c3656494495096af0caafd68e": {
      "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
      }
     },
     "7d6b41e9a308492588bab31ad47834e7": {
      "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_649ff563533144869b6a626a820a7d90",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "13.5% (deck)"
      }
     },
     "84926ebcbf1b4909870e0162052155da": {
      "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_90536af7df2044feaba9073e0e70d44f",
       "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
      }
     },
     "89ecf935020b4804acd749e521c25259": {
      "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
      }
     },
     "90536af7df2044feaba9073e0e70d44f": {
      "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
      }
     },
     "bb1cf20444e2490597fd26df35490de9": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.number.NumberInputWidget",
       "_css": "\n    .mljar-number-container {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n      font-size: 14px;\n      color: #0f172a;\n      margin-bottom: 8px;\n      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      margin-bottom: 4px;\n      font-weight: 600;\n    }\n\n    .mljar-number-field-row {\n      display: flex;\n      align-items: stretch;\n      width: 100%;\n      min-height: 40px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-sizing: border-box;\n      overflow: hidden;\n    }\n\n    .mljar-number-input {\n      flex: 1 1 auto;\n      min-width: 0;\n      min-height: 100%;\n      padding: 7px 10px;\n      border: 0;\n      border-radius: 0;\n      background: #ffffff;\n      box-sizing: border-box;\n      background-color: #ffffff !important;\n      color: #0f172a !important;\n      font: inherit;\n      line-height: 1.2;\n      -moz-appearance: textfield;\n    }\n\n    .mljar-number-input::-webkit-outer-spin-button,\n    .mljar-number-input::-webkit-inner-spin-button {\n      -webkit-appearance: none;\n      margin: 0;\n    }\n\n    .mljar-number-input:disabled {\n      background: #f5f5f5;\n      color: #888;\n      cursor: not-allowed;\n    }\n\n    .mljar-number-input:focus {\n      outline: none;\n    }\n\n    .mljar-number-field-row:focus-within {\n      border-color: #007bff;\n      border-width: 2px;\n      box-shadow: none;\n    }\n\n    .mljar-number-field-row:focus-within .mljar-number-controls {\n      border-left-color: #007bff;\n    }\n\n    .mljar-number-controls {\n      display: flex;\n      align-items: stretch;\n      flex: 0 0 auto;\n      border-left: 1px solid #cfd1d5;\n      background: #f1f1f2;\n    }\n\n    .mljar-number-step-btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      width: 38px;\n      min-width: 38px;\n      min-height: 100%;\n      border: 0;\n      border-radius: 0;\n      background: transparent;\n      color: #0f172a;\n      font: inherit;\n      font-size: 18px;\n      font-weight: 700;\n      line-height: 1;\n      cursor: pointer;\n      padding: 0;\n      user-select: none;\n      -webkit-user-select: none;\n      touch-action: manipulation;\n      transition: background-color 0.14s ease, color 0.14s ease;\n    }\n\n    .mljar-number-step-up {\n      border-left: 1px solid #cfd1d5;\n    }\n\n    .mljar-number-step-btn:hover {\n      background: #f3f3f4;\n    }\n\n    .mljar-number-step-btn:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:focus-visible {\n      outline: none;\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:disabled {\n      background: #f5f5f5;\n      color: #aaa;\n      cursor: not-allowed;\n    }\n\n    @media (max-width: 768px) {\n      .mljar-number-field-row {\n        min-height: 44px;\n      }\n\n      .mljar-number-input {\n        min-height: 44px;\n        padding: 8px 12px;\n      }\n\n      .mljar-number-step-btn {\n        font-size: 19px;\n        width: 44px;\n        min-width: 44px;\n      }\n    }\n    ",
       "_dom_classes": [],
       "_esm": "\n    function render({ model, el }) {\n      const container = document.createElement(\"div\");\n      container.classList.add(\"mljar-number-container\");\n\n      const topLabel = document.createElement(\"div\");\n      topLabel.classList.add(\"mljar-number-label\");\n\n      const fieldRow = document.createElement(\"div\");\n      fieldRow.classList.add(\"mljar-number-field-row\");\n\n      const input = document.createElement(\"input\");\n      input.type = \"number\";\n      input.classList.add(\"mljar-number-input\");\n\n      const decrementBtn = document.createElement(\"button\");\n      decrementBtn.type = \"button\";\n      decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n      decrementBtn.textContent = \"-\";\n      decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n      const incrementBtn = document.createElement(\"button\");\n      incrementBtn.type = \"button\";\n      incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n      incrementBtn.textContent = \"+\";\n      incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n      const controls = document.createElement(\"div\");\n      controls.classList.add(\"mljar-number-controls\");\n\n      controls.appendChild(decrementBtn);\n      controls.appendChild(incrementBtn);\n      fieldRow.appendChild(input);\n      fieldRow.appendChild(controls);\n\n      container.appendChild(topLabel);\n      container.appendChild(fieldRow);\n      el.appendChild(container);\n\n      function clamp(val, min, max) {\n        if (Number.isFinite(min) && val < min) return min;\n        if (Number.isFinite(max) && val > max) return max;\n        return val;\n      }\n\n      function normalizeStep(step) {\n        return Number.isFinite(step) && step > 0 ? step : 1;\n      }\n\n      function snapToStep(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = Math.round((value - base) / safeStep);\n        const snapped = base + steps * safeStep;\n        const precision = Math.max(\n          0,\n          (String(safeStep).split(\".\")[1] || \"\").length\n        );\n\n        return Number(snapped.toFixed(precision + 2));\n      }\n\n      function isOnStepGrid(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = (value - base) / safeStep;\n        const nearest = Math.round(steps);\n        const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n        return Math.abs(steps - nearest) <= epsilon;\n      }\n\n      function getCurrentBounds() {\n        return {\n          min: Number(model.get(\"min\")),\n          max: Number(model.get(\"max\")),\n        };\n      }\n\n      function isTransientDraft(raw) {\n        return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n      }\n\n      let isEditing = false;\n      const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n      function clearPendingDraftCommit() {\n        if (debounceTimer) clearTimeout(debounceTimer);\n        pendingDraftValue = null;\n      }\n\n      function parseDraftValue(rawValue) {\n        const raw = String(rawValue).trim();\n        if (isTransientDraft(raw)) {\n          return { kind: \"transient\" };\n        }\n\n        const value = Number(raw);\n        if (!Number.isFinite(value)) {\n          return { kind: \"invalid\" };\n        }\n\n        return { kind: \"number\", value };\n      }\n\n      function commitValue(nextValue, saveNow = true) {\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        const parsed = parseDraftValue(nextValue);\n        if (parsed.kind !== \"number\") {\n          syncFromModel();\n          return;\n        }\n\n        let v = parsed.value;\n        v = clamp(v, min, max);\n        v = snapToStep(v, min, step);\n        v = clamp(v, min, max);\n        input.value = String(v);\n        model.set(\"value\", v);\n\n        if (saveNow) {\n          model.save_changes();\n        }\n      }\n\n      function syncFromModel() {\n        topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n        const min = Number(model.get(\"min\"));\n        const max = Number(model.get(\"max\"));\n        const step = Number(model.get(\"step\"));\n\n        if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n        if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n        if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n        const v = Number(model.get(\"value\"));\n        if (!isEditing) {\n          input.value = Number.isFinite(v) ? String(v) : \"\";\n        }\n\n        const disabled = !!model.get(\"disabled\");\n        input.disabled = disabled;\n        incrementBtn.disabled = disabled;\n        decrementBtn.disabled = disabled;\n\n        const hidden = !!model.get(\"hidden\");\n        container.style.display = hidden ? \"none\" : \"flex\";\n      }\n\n      let debounceTimer = null;\n      let pendingDraftValue = null;\n      input.addEventListener(\"focus\", () => {\n        isEditing = true;\n      });\n\n      input.addEventListener(\"input\", () => {\n        if (model.get(\"disabled\")) return;\n\n        const parsed = parseDraftValue(input.value);\n        if (parsed.kind !== \"number\") {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        const v = parsed.value;\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        if (Number.isFinite(min) && v < min) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (Number.isFinite(max) && v > max) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (!isOnStepGrid(v, min, step)) {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        pendingDraftValue = v;\n        if (debounceTimer) clearTimeout(debounceTimer);\n        debounceTimer = setTimeout(() => {\n          if (pendingDraftValue === null) return;\n          model.set(\"value\", pendingDraftValue);\n          model.save_changes();\n          pendingDraftValue = null;\n        }, INPUT_COMMIT_DEBOUNCE_MS);\n      });\n\n      input.addEventListener(\"blur\", () => {\n        isEditing = false;\n        clearPendingDraftCommit();\n        commitValue(input.value, true);\n      });\n\n      input.addEventListener(\"keydown\", event => {\n        if (event.key === \"Enter\") {\n          event.preventDefault();\n          input.blur();\n        }\n      });\n\n      incrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base + step, min, step));\n      });\n\n      decrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base - step, min, step));\n      });\n\n      model.on(\"change:value\", syncFromModel);\n      model.on(\"change:min\", syncFromModel);\n      model.on(\"change:max\", syncFromModel);\n      model.on(\"change:step\", syncFromModel);\n      model.on(\"change:label\", syncFromModel);\n      model.on(\"change:disabled\", syncFromModel);\n      model.on(\"change:hidden\", syncFromModel);\n\n      syncFromModel();\n\n      // ---- read cell id (no DOM modifications) ----\n      /*\n      const ID_ATTR = \"data-cell-id\";\n      const hostWithId = el.closest(`[${ID_ATTR}]`);\n      const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n      if (cellId) {\n        model.set(\"cell_id\", cellId);\n        model.save_changes();\n        model.send({ type: \"cell_id_detected\", value: cellId });\n      } else {\n        const mo = new MutationObserver(() => {\n          const host = el.closest(`[${ID_ATTR}]`);\n          const newId = host?.getAttribute(ID_ATTR);\n          if (newId) {\n            model.set(\"cell_id\", newId);\n            model.save_changes();\n            model.send({ type: \"cell_id_detected\", value: newId });\n            mo.disconnect();\n          }\n        });\n        mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n      }*/\n    }\n    export default { render };\n    ",
       "_model_module": "anywidget",
       "_model_module_version": "~0.11.*",
       "_model_name": "AnyModel",
       "_view_count": null,
       "_view_module": "anywidget",
       "_view_module_version": "~0.11.*",
       "_view_name": "AnyView",
       "cell_id": "",
       "disabled": false,
       "hidden": false,
       "label": "ANZ — current platform cost ($/yr)",
       "layout": "IPY_MODEL_dbdeb8e991de4af1bd01c0587db78993",
       "layout_path": null,
       "max": 8000000.0,
       "min": 0.0,
       "position": "inline",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 10000.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 677320.0
      }
     },
     "bd7adf2ad6a245559678679f2dafcd3e": {
      "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
      }
     },
     "dbdeb8e991de4af1bd01c0587db78993": {
      "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
      }
     },
     "e89c1e81df274bbfbacecb23781ef45b": {
      "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
      }
     },
     "ed113f0cc8384a3d9c1acec6f8147c37": {
      "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
      }
     },
     "f27fe4f9506d44b5948d86125bdf587b": {
      "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_1e34f12588214862ad5402e3843bca43",
       "layout_path": null,
       "n_toggles": 0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": true
      }
     },
     "f8456441f0ae4695ae97815b0b0aac84": {
      "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": [
        "175",
        "225",
        "275"
       ],
       "disabled": false,
       "hidden": false,
       "label": "Blended rate ($/h)",
       "layout": "IPY_MODEL_652b98f4c90c4007b1e4536218f9361a",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "225"
      }
     },
     "fbc4f6246d0d4b36852ad6ad5f252d84": {
      "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_10b3118fbe3b46c89d22e101c9a280a8",
       "layout_path": null,
       "placeholder": "​",
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "style": "IPY_MODEL_bd7adf2ad6a245559678679f2dafcd3e",
       "tabbable": null,
       "tooltip": null,
       "value": "<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: normal; line-height: 1.65; color: #0f172a; word-break: break-word;\"><p style=\"margin: 0 0 1em;\"><b>Jump to section</b><ol style=\"padding-left:1.2em;margin:6px 0; margin: 0 0 1em; padding-left: 1.4em;\"><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-1');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('1 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Current state &amp; contracts</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-2');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('2 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verbatim benefits &amp; schedule</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-3');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('3 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Token consumption</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-4');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('4 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">AI implementation effort</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-5');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('5 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Corrected cost stack</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-6');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('6 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Fig 1 — Benefits</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-7');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('7 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Fig 2 — Costs</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-8');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('8 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Fig 3 — Business case</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-9');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('9 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Sensitivity</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-10');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('10 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verification &amp; assertions</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-11');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('11 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Risks &amp; next steps</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-12');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('12 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
      }
     },
     "fd23a1e2c3f84fd4ba4803b26fdec9ff": {
      "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_7a38470c3656494495096af0caafd68e",
       "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"
      }
     },
     "ffa89c9fcbcd417896eaba9fec38d96c": {
      "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": [
        "low",
        "mid",
        "high"
       ],
       "disabled": false,
       "hidden": false,
       "label": "Impl hours (V2 range)",
       "layout": "IPY_MODEL_ed113f0cc8384a3d9c1acec6f8147c37",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "mid"
      }
     }
    },
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
