{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-0",
   "metadata": {},
   "source": [
    "# Amazon Connect TEI — Business Case\n",
    "\n",
    "Reproduction of Forrester's *Total Economic Impact™ Of Amazon Connect*\n",
    "(February 2026, commissioned by AWS) — and a live personalization of it.\n",
    "The published composite organization is the **verbatim anchor** (never\n",
    "edited); the verification gate proves this notebook reproduces the\n",
    "published **$78.7M NPV · 342% ROI · <6-month payback**; the client drivers\n",
    "then rescale the composite to your organization.\n",
    "\n",
    "**This notebook is the deliverable** — served interactively with Mercury,\n",
    "exported via nbconvert as the report source (Mercury Notebook Pattern,\n",
    "Variant 4).\n",
    "\n",
    "| Layer | What it is | Confidence |\n",
    "|---|---|---|\n",
    "| Verbatim anchor | Forrester's composite tables, unedited | 🟢 published |\n",
    "| Client overlay | first-order linear rescale by your drivers | 🟡 estimated |\n",
    "| Scenario | adoption × risk stress | 🟡 estimated |\n",
    "\n",
    "Confidence legend: 🟢 confirmed/published · 🟡 estimated (stated assumption) · 🔴 unknown (flagged)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cell-1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:41.740701Z",
     "iopub.status.busy": "2026-07-09T18:15:41.740233Z",
     "iopub.status.idle": "2026-07-09T18:15:42.174061Z",
     "shell.execute_reply": "2026-07-09T18:15:42.173079Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "teicalc loaded — window 2026–2028 · published NPV $78.7M · ROI 342%\n"
     ]
    }
   ],
   "source": [
    "# ── Setup ──────────────────────────────────────────────────────────\n",
    "import sys, pathlib\n",
    "_ROOT = pathlib.Path.cwd()\n",
    "if not (_ROOT / \"teicalc\").exists():             # notebook lives in notebooks/\n",
    "    _ROOT = _ROOT.parent\n",
    "sys.path.insert(0, str(_ROOT))\n",
    "\n",
    "import pandas as pd\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "import mercury as mr\n",
    "\n",
    "# Single source of truth — all math lives in the study package; only\n",
    "# presentation (and Mercury input widgets) lives here.\n",
    "from teicalc import (\n",
    "    ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM, PUBLISHED,\n",
    "    YEARS, X_LABELS,\n",
    "    BENEFIT_DRIVERS, COST_DRIVERS, COMPOSITE, ClientDrivers,\n",
    "    SCENARIOS, apply_scenario, compute_summary, growth_multiplier,\n",
    "    money, html_money, overlay_rows,\n",
    ")\n",
    "from teicalc.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 row colors — color follows the entity across every figure.\n",
    "BENEFIT_COLOR = {\n",
    "    \"ai_contact_resolution\": \"#2a78d6\",       # blue\n",
    "    \"ai_content_sentiment\": \"#1baf7a\",        # aqua\n",
    "    \"ai_forecasting_supervision\": \"#4a3aa7\",  # violet\n",
    "    \"data_driven_profit_lift\": \"#eda100\",     # yellow\n",
    "    \"legacy_solution_savings\": \"#008300\",     # green\n",
    "}\n",
    "COST_COLOR = {\n",
    "    \"amazon_connect_usage\": \"#e34948\",        # red\n",
    "    \"implementation_migration\": \"#eda100\",    # yellow\n",
    "    \"ongoing_management\": \"#4a3aa7\",          # violet\n",
    "}\n",
    "BEN_TOTAL, COST_TOTAL, NPV_COLOR = \"#1baf7a\", \"#e34948\", \"#2a78d6\"\n",
    "\n",
    "\n",
    "def tei_layout(fig, title, subtitle=None, height=460):\n",
    "    t = f\"<b>{title}</b>\"\n",
    "    if subtitle:\n",
    "        t += f\"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>\"\n",
    "    fig.update_layout(\n",
    "        title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor=\"left\"),\n",
    "        paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n",
    "        font=dict(family=FONT_STACK, size=12, color=INK2),\n",
    "        legend=dict(orientation=\"h\", yanchor=\"top\", y=-0.10, x=0,\n",
    "                    font=dict(size=11, color=INK2)),\n",
    "        xaxis=dict(type=\"category\", showgrid=False, linecolor=BASELINE,\n",
    "                   tickfont=dict(color=MUTED)),\n",
    "        yaxis=dict(gridcolor=GRID, zerolinecolor=BASELINE, zerolinewidth=1.5,\n",
    "                   tickformat=\"$~s\", tickfont=dict(color=MUTED)),\n",
    "        hovermode=\"x unified\", bargap=0.45, height=height,\n",
    "        margin=dict(t=70, r=30, b=80, l=70),\n",
    "    )\n",
    "    return fig\n",
    "\n",
    "\n",
    "def bar(x, y, name, color):\n",
    "    return go.Bar(x=x, y=y, name=name,\n",
    "                  marker=dict(color=color, line=dict(width=2, color=SURFACE)),\n",
    "                  hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
    "\n",
    "\n",
    "def cum_line(x, y, name, color=CUMULATIVE, dash=None):\n",
    "    return go.Scatter(x=x, y=y, name=name, mode=\"lines+markers\",\n",
    "                      line=dict(color=color, width=2, dash=dash),\n",
    "                      marker=dict(size=8, line=dict(width=2, color=SURFACE)),\n",
    "                      hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
    "\n",
    "\n",
    "backstage(f\"teicalc loaded — window {YEARS[0]}–{YEARS[-1]} · published \"\n",
    "          f\"NPV {money(PUBLISHED['npv'])} · ROI {PUBLISHED['roi_pct']}%\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "cell-2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.176402Z",
     "iopub.status.busy": "2026-07-09T18:15:42.175865Z",
     "iopub.status.idle": "2026-07-09T18:15:42.192559Z",
     "shell.execute_reply": "2026-07-09T18:15:42.191936Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "0833fb2c5bd3466aba5ff466dec6ff1b",
       "position": "sidebar",
       "widget": "MarkdownWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "0833fb2c5bd3466aba5ff466dec6ff1b",
       "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, \"Composite organization\"),\n",
    "    (2, \"Client inputs\"),\n",
    "    (3, \"Benefits\"),\n",
    "    (4, \"Costs\"),\n",
    "    (5, \"Business case\"),\n",
    "    (6, \"Scenarios\"),\n",
    "    (7, \"Verification & assertions\"),\n",
    "    (8, \"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": "cell-3",
   "metadata": {},
   "source": [
    "<a id=\"section-1\"></a>\n",
    "## 1 · The Forrester composite (verbatim anchor 🟢)\n",
    "\n",
    "Forrester's composite organization: a global B2C company with **$10B\n",
    "year-1 revenue growing 30% YoY**, **2,000 contact-center agents** plus 200\n",
    "supervisors, **20M annual contacts** (75% calls / 25% chat), and a\n",
    "10-minute legacy average handle time.\n",
    "\n",
    "TEI methodology, carried verbatim into the engine: benefits are\n",
    "risk-adjusted **down** (×(1−rf)), costs **up** (×(1+rf)); the initial\n",
    "investment sits at time 0 undiscounted; year flows discount at end-of-year\n",
    "(10%, 3 years). Payback runs on risk-adjusted *undiscounted* flows, per the\n",
    "PDF's Cash Flow Analysis tables.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "cell-4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.194666Z",
     "iopub.status.busy": "2026-07-09T18:15:42.194415Z",
     "iopub.status.idle": "2026-07-09T18:15:42.210841Z",
     "shell.execute_reply": "2026-07-09T18:15:42.210047Z"
    }
   },
   "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>Value 🟢</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Assumption</th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>agents fte</th>\n",
       "      <td>2,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>supervisors fte</th>\n",
       "      <td>200</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>annual contacts y1</th>\n",
       "      <td>20,000,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>growth rate</th>\n",
       "      <td>30%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>call share</th>\n",
       "      <td>75%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>aht legacy minutes</th>\n",
       "      <td>10 min</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>agent salary</th>\n",
       "      <td>$45,760</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>supervisor salary</th>\n",
       "      <td>$55,800</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>discount rate</th>\n",
       "      <td>10%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>analysis years</th>\n",
       "      <td>3 years</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                       Value 🟢\n",
       "Assumption                    \n",
       "agents fte               2,000\n",
       "supervisors fte            200\n",
       "annual contacts y1  20,000,000\n",
       "growth rate                30%\n",
       "call share                 75%\n",
       "aht legacy minutes      10 min\n",
       "agent salary           $45,760\n",
       "supervisor salary      $55,800\n",
       "discount rate              10%\n",
       "analysis years         3 years"
      ]
     },
     "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>Published 🟢</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Metric</th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Benefits PV (risk-adjusted)</th>\n",
       "      <td>$101,696,791</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Costs PV (risk-adjusted)</th>\n",
       "      <td>$22,983,076</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NPV</th>\n",
       "      <td>$78,713,715</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ROI</th>\n",
       "      <td>342%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Payback</th>\n",
       "      <td>&lt;6 months</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                              Published 🟢\n",
       "Metric                                   \n",
       "Benefits PV (risk-adjusted)  $101,696,791\n",
       "Costs PV (risk-adjusted)      $22,983,076\n",
       "NPV                           $78,713,715\n",
       "ROI                                  342%\n",
       "Payback                         <6 months"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "engine reproduction Δ vs PDF (Forrester table rounding): benefits -223.45 · costs -0.22 · npv -223.22\n"
     ]
    }
   ],
   "source": [
    "# ── Composite assumptions & published financial summary (🟢) ─────────\n",
    "composite = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM,\n",
    "                            PUBLISHED[\"discount_rate\"])\n",
    "\n",
    "_fmt = {\n",
    "    \"agents_fte\": \"{:,}\", \"supervisors_fte\": \"{:,}\",\n",
    "    \"annual_contacts_y1\": \"{:,}\", \"growth_rate\": \"{:.0%}\",\n",
    "    \"call_share\": \"{:.0%}\", \"aht_legacy_minutes\": \"{} min\",\n",
    "    \"agent_salary\": \"${:,}\", \"supervisor_salary\": \"${:,}\",\n",
    "    \"discount_rate\": \"{:.0%}\", \"analysis_years\": \"{} years\",\n",
    "}\n",
    "assumptions_df = pd.DataFrame(\n",
    "    [{\"Assumption\": k.replace(\"_\", \" \"), \"Value 🟢\": _fmt[k].format(v)}\n",
    "     for k, v in ASSUMPTIONS.items()])\n",
    "display(assumptions_df.set_index(\"Assumption\"))\n",
    "\n",
    "published_df = pd.DataFrame([\n",
    "    {\"Metric\": \"Benefits PV (risk-adjusted)\", \"Published 🟢\": f\"${PUBLISHED['benefits_pv']:,}\"},\n",
    "    {\"Metric\": \"Costs PV (risk-adjusted)\",    \"Published 🟢\": f\"${PUBLISHED['costs_pv']:,}\"},\n",
    "    {\"Metric\": \"NPV\",                          \"Published 🟢\": f\"${PUBLISHED['npv']:,}\"},\n",
    "    {\"Metric\": \"ROI\",                          \"Published 🟢\": f\"{PUBLISHED['roi_pct']}%\"},\n",
    "    {\"Metric\": \"Payback\",                      \"Published 🟢\": \"<6 months\"},\n",
    "])\n",
    "display(published_df.set_index(\"Metric\"))\n",
    "\n",
    "backstage(f\"engine reproduction Δ vs PDF (Forrester table rounding): \"\n",
    "          f\"benefits {composite['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n",
    "          f\"costs {composite['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n",
    "          f\"npv {composite['npv'] - PUBLISHED['npv']:+,.2f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-5",
   "metadata": {},
   "source": [
    "<a id=\"section-2\"></a>\n",
    "## 2 · Client inputs (overlay 🟡)\n",
    "\n",
    "The overlay is a **first-order linear rescale** of Forrester's composite —\n",
    "it answers *\"what does the composite look like at your size?\"*, not *\"what\n",
    "is your TEI?\"*. Each published row scales with the driver that dominates\n",
    "its derivation in the PDF; project-based costs stay fixed. The client\n",
    "growth rate re-bases the composite's Y1→Y3 trajectory (which embeds 30%\n",
    "YoY).\n",
    "\n",
    "| Published row | Scales with | Confidence |\n",
    "|---|---|---|\n",
    "| AI-driven contact resolution efficiency | contacts | 🟡 |\n",
    "| AI-powered content & sentiment analysis | contacts | 🟡 |\n",
    "| AI-enabled forecasting, scheduling & supervision | agents | 🟡 |\n",
    "| Data-driven profit lift | contacts | 🔴 proxy — revenue-driven in the PDF |\n",
    "| Legacy solution cost savings | agents | 🟡 |\n",
    "| Amazon Connect usage | contacts | 🟡 |\n",
    "| Implementation & migration | fixed | 🟡 project-based |\n",
    "| Ongoing management | fixed | 🟡 |\n",
    "\n",
    "*Change any input in the sidebar — every table, figure and KPI below\n",
    "recomputes. The assertions in §7 hold at any setting.*\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "cell-6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.213716Z",
     "iopub.status.busy": "2026-07-09T18:15:42.213491Z",
     "iopub.status.idle": "2026-07-09T18:15:42.227389Z",
     "shell.execute_reply": "2026-07-09T18:15:42.226903Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "ace9c00dc3a84babb5b29ae61e2be62c",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ace9c00dc3a84babb5b29ae61e2be62c",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f1550b512b0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "01c230d30aed4aa58c5a005c44e65f84",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "01c230d30aed4aa58c5a005c44e65f84",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f1550b4d1d0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "d4ccace17e824e4a85c55ff6ef95afb0",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "d4ccace17e824e4a85c55ff6ef95afb0",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f1550b4cf50>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "eb0056195e9b444dbb8ef3c08e744446",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "eb0056195e9b444dbb8ef3c08e744446",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f1550b516a0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "b1e82327c29a44a8a4a45e8100b3d857",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "b1e82327c29a44a8a4a45e8100b3d857",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f1550b4d810>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Client drivers (Mercury sidebar — widgets only, NO other output) ─\n",
    "# NB: Mercury re-executes only cells BELOW a changed widget's cell, so\n",
    "# this cell constructs widgets ONLY — .value is read downstream.\n",
    "_agents_w = mr.NumberInput(label=\"Contact-center agents (FTE) — composite 2,000\",\n",
    "                           value=2_000, min=50, max=50_000, step=50)\n",
    "_contacts_w = mr.NumberInput(label=\"Annual contacts, year 1 — composite 20M\",\n",
    "                             value=20_000_000, min=100_000, max=500_000_000,\n",
    "                             step=1_000_000)\n",
    "_growth_w = mr.NumberInput(label=\"Contact growth (%/yr) — composite 30\",\n",
    "                           value=30, min=0, max=100, step=5)\n",
    "_discount_w = mr.Select(label=\"Discount rate\", value=\"10% (Forrester)\",\n",
    "                        choices=[\"8%\", \"10% (Forrester)\", \"12%\"])\n",
    "_scenario_w = mr.Select(label=\"Scenario\", value=\"moderate\",\n",
    "                        choices=[\"conservative\", \"moderate\", \"aggressive\"])\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "cell-7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.229494Z",
     "iopub.status.busy": "2026-07-09T18:15:42.229335Z",
     "iopub.status.idle": "2026-07-09T18:15:42.234902Z",
     "shell.execute_reply": "2026-07-09T18:15:42.234410Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Client frame: 2,000 agents · 20M contacts (+30%/yr) · moderate scenario → NPV $78.7M · ROI 342% · payback 0.7 months (~Jan 2026)\n",
      "scale factors — agents 1.00× · contacts 1.00× · growth re-base Y3 1.000×\n"
     ]
    }
   ],
   "source": [
    "# ── Client overlay state (re-runs on any change to the widgets above) ─\n",
    "AGENTS_FTE = int(_agents_w.value)\n",
    "CONTACTS_Y1 = int(_contacts_w.value)\n",
    "GROWTH_RATE = float(_growth_w.value) / 100          # widget holds a % integer\n",
    "DISCOUNT_RATE = {\"8%\": 0.08, \"10% (Forrester)\": 0.10,\n",
    "                 \"12%\": 0.12}[str(_discount_w.value)]\n",
    "SCENARIO = str(_scenario_w.value)\n",
    "\n",
    "DRIVERS = ClientDrivers(agents_fte=AGENTS_FTE, annual_contacts_y1=CONTACTS_Y1,\n",
    "                        growth_rate=GROWTH_RATE, discount_rate=DISCOUNT_RATE)\n",
    "overlay_benefits, overlay_costs = overlay_rows(DRIVERS)\n",
    "client_benefits = apply_scenario(overlay_benefits, SCENARIO)\n",
    "client_costs = apply_scenario(overlay_costs, SCENARIO)\n",
    "client = compute_summary(client_benefits, client_costs, DISCOUNT_RATE)\n",
    "\n",
    "_at_default = (DRIVERS == COMPOSITE and SCENARIO == \"moderate\")\n",
    "\n",
    "print(f\"Client frame: {AGENTS_FTE:,} agents · {CONTACTS_Y1 / 1e6:,.0f}M contacts \"\n",
    "      f\"(+{GROWTH_RATE:.0%}/yr) · {SCENARIO} scenario → NPV {money(client['npv'])} · \"\n",
    "      f\"ROI {client['roi_pct']:.0f}% · payback {client['payback_label']}\")\n",
    "backstage(f\"scale factors — agents {AGENTS_FTE / ASSUMPTIONS['agents_fte']:.2f}× · \"\n",
    "          f\"contacts {CONTACTS_Y1 / ASSUMPTIONS['annual_contacts_y1']:.2f}× · \"\n",
    "          f\"growth re-base Y3 {growth_multiplier(3, GROWTH_RATE):.3f}×\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-8",
   "metadata": {},
   "source": [
    "<a id=\"section-3\"></a>\n",
    "## 3 · Benefits\n",
    "\n",
    "Five benefit streams (Forrester refs At–Et), risk-adjusted down 15–20%.\n",
    "Half the total is AI-driven contact-resolution efficiency; all five phase\n",
    "up with the growth trajectory across the window.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "cell-9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.236799Z",
     "iopub.status.busy": "2026-07-09T18:15:42.236657Z",
     "iopub.status.idle": "2026-07-09T18:15:42.247373Z",
     "shell.execute_reply": "2026-07-09T18:15:42.246802Z"
    }
   },
   "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>Driver</th>\n",
       "      <th>Risk adj</th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>3-yr RA</th>\n",
       "      <th>PV</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Benefit</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>AI-driven contact resolution efficiency</th>\n",
       "      <td>contacts</td>\n",
       "      <td>-15%</td>\n",
       "      <td>11,824,384</td>\n",
       "      <td>20,342,608</td>\n",
       "      <td>32,128,096</td>\n",
       "      <td>64,295,088</td>\n",
       "      <td>51,699,827</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI-powered content and sentiment analysis savings</th>\n",
       "      <td>contacts</td>\n",
       "      <td>-15%</td>\n",
       "      <td>3,898,627</td>\n",
       "      <td>4,554,650</td>\n",
       "      <td>5,347,928</td>\n",
       "      <td>13,801,205</td>\n",
       "      <td>11,326,358</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AI-enabled forecasting, agent scheduling, and supervision</th>\n",
       "      <td>agents</td>\n",
       "      <td>-15%</td>\n",
       "      <td>5,653,928</td>\n",
       "      <td>7,763,696</td>\n",
       "      <td>10,532,955</td>\n",
       "      <td>23,950,579</td>\n",
       "      <td>19,469,777</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Data-driven profit lift with increased conversion</th>\n",
       "      <td>contacts</td>\n",
       "      <td>-20%</td>\n",
       "      <td>960,000</td>\n",
       "      <td>1,248,000</td>\n",
       "      <td>1,622,400</td>\n",
       "      <td>3,830,400</td>\n",
       "      <td>3,123,065</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Legacy solution cost savings</th>\n",
       "      <td>agents</td>\n",
       "      <td>-20%</td>\n",
       "      <td>4,942,080</td>\n",
       "      <td>6,424,704</td>\n",
       "      <td>8,352,115</td>\n",
       "      <td>19,718,899</td>\n",
       "      <td>16,077,540</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td></td>\n",
       "      <td></td>\n",
       "      <td>27,279,019</td>\n",
       "      <td>40,333,658</td>\n",
       "      <td>57,983,494</td>\n",
       "      <td>125,596,172</td>\n",
       "      <td>101,696,568</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                      Driver Risk adj  \\\n",
       "Benefit                                                                 \n",
       "AI-driven contact resolution efficiency             contacts     -15%   \n",
       "AI-powered content and sentiment analysis savings   contacts     -15%   \n",
       "AI-enabled forecasting, agent scheduling, and s...    agents     -15%   \n",
       "Data-driven profit lift with increased conversion   contacts     -20%   \n",
       "Legacy solution cost savings                          agents     -20%   \n",
       "TOTAL                                                                   \n",
       "\n",
       "                                                         2026       2027  \\\n",
       "Benefit                                                                    \n",
       "AI-driven contact resolution efficiency            11,824,384 20,342,608   \n",
       "AI-powered content and sentiment analysis savings   3,898,627  4,554,650   \n",
       "AI-enabled forecasting, agent scheduling, and s...  5,653,928  7,763,696   \n",
       "Data-driven profit lift with increased conversion     960,000  1,248,000   \n",
       "Legacy solution cost savings                        4,942,080  6,424,704   \n",
       "TOTAL                                              27,279,019 40,333,658   \n",
       "\n",
       "                                                         2028     3-yr RA  \\\n",
       "Benefit                                                                     \n",
       "AI-driven contact resolution efficiency            32,128,096  64,295,088   \n",
       "AI-powered content and sentiment analysis savings   5,347,928  13,801,205   \n",
       "AI-enabled forecasting, agent scheduling, and s... 10,532,955  23,950,579   \n",
       "Data-driven profit lift with increased conversion   1,622,400   3,830,400   \n",
       "Legacy solution cost savings                        8,352,115  19,718,899   \n",
       "TOTAL                                              57,983,494 125,596,172   \n",
       "\n",
       "                                                            PV  \n",
       "Benefit                                                         \n",
       "AI-driven contact resolution efficiency             51,699,827  \n",
       "AI-powered content and sentiment analysis savings   11,326,358  \n",
       "AI-enabled forecasting, agent scheduling, and s...  19,469,777  \n",
       "Data-driven profit lift with increased conversion    3,123,065  \n",
       "Legacy solution cost savings                        16,077,540  \n",
       "TOTAL                                              101,696,568  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Benefits table — client overlay, risk-adjusted ───────────────────\n",
    "_ben_rows = client[\"rows\"][\"benefits\"]\n",
    "benefits_df = pd.DataFrame([{\n",
    "    \"Benefit\": r[\"label\"],\n",
    "    \"Driver\": BENEFIT_DRIVERS[r[\"field_key\"]],\n",
    "    \"Risk adj\": f\"-{r['risk_adjustment']:.0%}\",\n",
    "    **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n",
    "    \"3-yr RA\": r[\"three_yr_ra\"],\n",
    "    \"PV\": r[\"pv\"],\n",
    "} for r in _ben_rows]).set_index(\"Benefit\")\n",
    "benefits_df.loc[\"TOTAL\"] = [\"\", \"\"] + [client[\"benefits_by_year\"][y] for y in YEARS] \\\n",
    "    + [sum(r[\"three_yr_ra\"] for r in _ben_rows), client[\"benefits_pv\"]]\n",
    "if not _at_default:      # published composite beside the overlay for reference\n",
    "    benefits_df[\"Composite PV 🟢\"] = \\\n",
    "        [r[\"pv\"] for r in composite[\"rows\"][\"benefits\"]] + [composite[\"benefits_pv\"]]\n",
    "display(benefits_df)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "cell-10",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:42.249508Z",
     "iopub.status.busy": "2026-07-09T18:15:42.249351Z",
     "iopub.status.idle": "2026-07-09T18:15:43.253962Z",
     "shell.execute_reply": "2026-07-09T18:15:43.253381Z"
    }
   },
   "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": "AI-driven contact resolution efficiency",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          11824384.0,
          20342608.0,
          32128096.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI-powered content and sentiment analysis savings",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          3898627.0,
          4554650.2,
          5347928.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#4a3aa7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "AI-enabled forecasting, agent scheduling, and supervision",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          5653928.0,
          7763696.0,
          10532955.2
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#eda100",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Data-driven profit lift with increased conversion",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          960000.0,
          1248000.0,
          1622400.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#008300",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Legacy solution cost savings",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          4942080.0,
          6424704.0,
          8352115.2
         ]
        }
       ],
       "layout": {
        "bargap": 0.45,
        "barmode": "stack",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 480,
        "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 by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>stacked by benefit stream — client overlay</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",
    "_x = [str(y) for y in YEARS]\n",
    "for r in client[\"rows\"][\"benefits\"]:\n",
    "    fig.add_trace(bar(_x, [r[\"ra_by_year\"][y] for y in YEARS],\n",
    "                      r[\"label\"], BENEFIT_COLOR[r[\"field_key\"]]))\n",
    "if not _at_default:      # composite yearly total as de-emphasized context\n",
    "    fig.add_trace(cum_line(_x, [composite[\"benefits_by_year\"][y] for y in YEARS],\n",
    "                           \"Forrester composite total\", CONTEXT, dash=\"dot\"))\n",
    "fig.update_layout(barmode=\"stack\")\n",
    "tei_layout(fig, \"Benefits by year (risk-adjusted)\",\n",
    "           subtitle=\"stacked by benefit stream — client overlay\", height=480)\n",
    "fig.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-11",
   "metadata": {},
   "source": [
    "<a id=\"section-4\"></a>\n",
    "## 4 · Costs\n",
    "\n",
    "Three cost lines, risk-adjusted **up** 5–15%. Consumption-priced Amazon\n",
    "Connect usage is ~90% of costs PV — the cost side scales with contact\n",
    "volume, not seats. Implementation carries the only time-0 outlay\n",
    "($1.09M nominal → $1.20M risk-adjusted, undiscounted in the *Initial*\n",
    "column).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "cell-12",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.257431Z",
     "iopub.status.busy": "2026-07-09T18:15:43.257260Z",
     "iopub.status.idle": "2026-07-09T18:15:43.268554Z",
     "shell.execute_reply": "2026-07-09T18:15:43.267833Z"
    }
   },
   "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>Driver</th>\n",
       "      <th>Risk adj</th>\n",
       "      <th>Initial</th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>PV</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Cost</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>Amazon Connect usage cost</th>\n",
       "      <td>contacts</td>\n",
       "      <td>+5%</td>\n",
       "      <td>0</td>\n",
       "      <td>6,779,270</td>\n",
       "      <td>8,348,722</td>\n",
       "      <td>10,324,609</td>\n",
       "      <td>20,819,775</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Implementation and migration cost</th>\n",
       "      <td>fixed</td>\n",
       "      <td>+10%</td>\n",
       "      <td>1,196,250</td>\n",
       "      <td>207,166</td>\n",
       "      <td>207,166</td>\n",
       "      <td>0</td>\n",
       "      <td>1,555,795</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Ongoing management</th>\n",
       "      <td>fixed</td>\n",
       "      <td>+15%</td>\n",
       "      <td>0</td>\n",
       "      <td>294,630</td>\n",
       "      <td>215,280</td>\n",
       "      <td>215,280</td>\n",
       "      <td>607,506</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TOTAL</th>\n",
       "      <td></td>\n",
       "      <td></td>\n",
       "      <td>1,196,250</td>\n",
       "      <td>7,281,067</td>\n",
       "      <td>8,771,168</td>\n",
       "      <td>10,539,889</td>\n",
       "      <td>22,983,076</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                     Driver Risk adj   Initial      2026  \\\n",
       "Cost                                                                       \n",
       "Amazon Connect usage cost          contacts      +5%         0 6,779,270   \n",
       "Implementation and migration cost     fixed     +10% 1,196,250   207,166   \n",
       "Ongoing management                    fixed     +15%         0   294,630   \n",
       "TOTAL                                                1,196,250 7,281,067   \n",
       "\n",
       "                                       2027       2028         PV  \n",
       "Cost                                                               \n",
       "Amazon Connect usage cost         8,348,722 10,324,609 20,819,775  \n",
       "Implementation and migration cost   207,166          0  1,555,795  \n",
       "Ongoing management                  215,280    215,280    607,506  \n",
       "TOTAL                             8,771,168 10,539,889 22,983,076  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Costs table — client overlay, risk-adjusted ──────────────────────\n",
    "_cost_rows = client[\"rows\"][\"costs\"]\n",
    "costs_df = pd.DataFrame([{\n",
    "    \"Cost\": r[\"label\"],\n",
    "    \"Driver\": COST_DRIVERS[r[\"field_key\"]],\n",
    "    \"Risk adj\": f\"+{r['risk_adjustment']:.0%}\",\n",
    "    \"Initial\": r[\"initial_ra\"],\n",
    "    **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n",
    "    \"PV\": r[\"pv\"],\n",
    "} for r in _cost_rows]).set_index(\"Cost\")\n",
    "costs_df.loc[\"TOTAL\"] = [\"\", \"\", client[\"initial_costs\"]] \\\n",
    "    + [client[\"costs_by_year\"][y] for y in YEARS] + [client[\"costs_pv\"]]\n",
    "if not _at_default:\n",
    "    costs_df[\"Composite PV 🟢\"] = \\\n",
    "        [r[\"pv\"] for r in composite[\"rows\"][\"costs\"]] + [composite[\"costs_pv\"]]\n",
    "display(costs_df)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "cell-13",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.270331Z",
     "iopub.status.busy": "2026-07-09T18:15:43.270194Z",
     "iopub.status.idle": "2026-07-09T18:15:43.287522Z",
     "shell.execute_reply": "2026-07-09T18:15:43.286918Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Amazon Connect usage cost",
         "type": "bar",
         "x": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          6779270.4,
          8348722.2,
          10324609.05
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#eda100",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Implementation and migration cost",
         "type": "bar",
         "x": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          1196250.0,
          207166.30000000002,
          207166.30000000002,
          0.0
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#4a3aa7",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Ongoing management",
         "type": "bar",
         "x": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          294630.0,
          215279.99999999997,
          215279.99999999997
         ]
        }
       ],
       "layout": {
        "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>Costs by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>Initial = undiscounted time-0 outlay (implementation)</span>",
         "x": 0.02,
         "xanchor": "left"
        },
        "xaxis": {
         "linecolor": "#c3c2b7",
         "showgrid": false,
         "tickfont": {
          "color": "#898781"
         },
         "type": "category"
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickfont": {
          "color": "#898781"
         },
         "tickformat": "$~s",
         "zerolinecolor": "#c3c2b7",
         "zerolinewidth": 1.5
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "for r in client[\"rows\"][\"costs\"]:\n",
    "    fig.add_trace(bar(X_LABELS,\n",
    "                      [r[\"initial_ra\"]] + [r[\"ra_by_year\"][y] for y in YEARS],\n",
    "                      r[\"label\"], COST_COLOR[r[\"field_key\"]]))\n",
    "fig.update_layout(barmode=\"stack\")\n",
    "tei_layout(fig, \"Costs by year (risk-adjusted)\",\n",
    "           subtitle=\"Initial = undiscounted time-0 outlay (implementation)\")\n",
    "fig.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-14",
   "metadata": {},
   "source": [
    "<a id=\"section-5\"></a>\n",
    "## 5 · Business case\n",
    "\n",
    "The left column is Forrester's published Financial Summary, verbatim. The\n",
    "right column is the client overlay at the sidebar's drivers. At the\n",
    "defaults the engine reproduces the published totals to within the PDF's\n",
    "own table rounding — the gate in §7 enforces it. Payback lands under a\n",
    "month because the composite's $1.2M initial outlay is small against\n",
    "~$20M of year-1 net benefit; Forrester publishes it simply as\n",
    "\"<6 months\", and sub-month precision is a full-year-aggregation artifact,\n",
    "not a forecast.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "cell-15",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.291216Z",
     "iopub.status.busy": "2026-07-09T18:15:43.291061Z",
     "iopub.status.idle": "2026-07-09T18:15:43.298783Z",
     "shell.execute_reply": "2026-07-09T18:15:43.298192Z"
    }
   },
   "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>Forrester composite (published 🟢)</th>\n",
       "      <th>Client overlay (🟡)</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Benefits PV</th>\n",
       "      <td>$101,696,791</td>\n",
       "      <td>$101,696,568</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Costs PV</th>\n",
       "      <td>$22,983,076</td>\n",
       "      <td>$22,983,076</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NPV</th>\n",
       "      <td>$78,713,715</td>\n",
       "      <td>$78,713,492</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ROI</th>\n",
       "      <td>342%</td>\n",
       "      <td>342%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Payback</th>\n",
       "      <td>&lt;6 months</td>\n",
       "      <td>0.7 months (~Jan 2026)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Discount rate</th>\n",
       "      <td>10%</td>\n",
       "      <td>10%</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "              Forrester composite (published 🟢)      Client overlay (🟡)\n",
       "Benefits PV                        $101,696,791            $101,696,568\n",
       "Costs PV                            $22,983,076             $22,983,076\n",
       "NPV                                 $78,713,715             $78,713,492\n",
       "ROI                                        342%                    342%\n",
       "Payback                               <6 months  0.7 months (~Jan 2026)\n",
       "Discount rate                               10%                     10%"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "engine composite NPV $78,713,491.78 vs published $78,713,715 (Δ -223.22)\n"
     ]
    }
   ],
   "source": [
    "# ── KPIs — published composite beside the client overlay ─────────────\n",
    "kpis_fmt = pd.DataFrame({\n",
    "    \"Forrester composite (published 🟢)\": {\n",
    "        \"Benefits PV\": f\"${PUBLISHED['benefits_pv']:,}\",\n",
    "        \"Costs PV\": f\"${PUBLISHED['costs_pv']:,}\",\n",
    "        \"NPV\": f\"${PUBLISHED['npv']:,}\",\n",
    "        \"ROI\": f\"{PUBLISHED['roi_pct']}%\",\n",
    "        \"Payback\": \"<6 months\",\n",
    "        \"Discount rate\": f\"{PUBLISHED['discount_rate']:.0%}\",\n",
    "    },\n",
    "    \"Client overlay (🟡)\": {\n",
    "        \"Benefits PV\": f\"${client['benefits_pv']:,.0f}\",\n",
    "        \"Costs PV\": f\"${client['costs_pv']:,.0f}\",\n",
    "        \"NPV\": f\"${client['npv']:,.0f}\",\n",
    "        \"ROI\": f\"{client['roi_pct']:.0f}%\",\n",
    "        \"Payback\": client[\"payback_label\"],\n",
    "        \"Discount rate\": f\"{DISCOUNT_RATE:.0%}\",\n",
    "    },\n",
    "})\n",
    "display(kpis_fmt)\n",
    "backstage(f\"engine composite NPV ${composite['npv']:,.2f} vs published \"\n",
    "          f\"${PUBLISHED['npv']:,} (Δ {composite['npv'] - PUBLISHED['npv']:+,.2f})\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "cell-16",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.302934Z",
     "iopub.status.busy": "2026-07-09T18:15:43.302786Z",
     "iopub.status.idle": "2026-07-09T18:15:43.324633Z",
     "shell.execute_reply": "2026-07-09T18:15:43.324079Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Benefits (risk-adjusted)",
         "type": "bar",
         "x": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0,
          27279019.0,
          40333658.2,
          57983494.400000006
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Costs (risk-adjusted)",
         "type": "bar",
         "x": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          -1196250.0,
          -7281066.7,
          -8771168.5,
          -10539889.05
         ]
        },
        {
         "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": [
          "Initial",
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          -1196250.0,
          18801702.3,
          50364192.0,
          97807797.35000001
         ]
        }
       ],
       "layout": {
        "annotations": [
         {
          "align": "left",
          "bgcolor": "#fcfcfb",
          "bordercolor": "#e1e0d9",
          "borderwidth": 1,
          "font": {
           "color": "#0b0b0b",
           "size": 12
          },
          "showarrow": false,
          "text": "NPV &#36;78.7M · ROI 342% · payback 0.7 months (~Jan 2026)",
          "x": 0.02,
          "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": 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>Cash flow — risk-adjusted</b><br><span style='font-size:12px;color:#898781'>benefits up, costs down; initial outlay undiscounted at time 0</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": [
    "# ── Cash flow — mirrors the PDF's Cash Flow Chart (p.25) ─────────────\n",
    "fig = go.Figure()\n",
    "fig.add_trace(bar(X_LABELS,\n",
    "                  [0] + [client[\"benefits_by_year\"][y] for y in YEARS],\n",
    "                  \"Benefits (risk-adjusted)\", BEN_TOTAL))\n",
    "fig.add_trace(bar(X_LABELS,\n",
    "                  [-client[\"initial_costs\"]] + [-client[\"costs_by_year\"][y] for y in YEARS],\n",
    "                  \"Costs (risk-adjusted)\", COST_TOTAL))\n",
    "fig.add_trace(cum_line(X_LABELS,\n",
    "                       [-client[\"initial_costs\"]] + [client[\"cumulative_net_by_year\"][y] for y in YEARS],\n",
    "                       \"Cumulative net\"))\n",
    "fig.update_layout(barmode=\"relative\")\n",
    "fig.add_annotation(\n",
    "    x=0.02, y=0.98, xref=\"paper\", yref=\"paper\", showarrow=False, align=\"left\",\n",
    "    text=(f\"NPV {html_money(client['npv'])} · ROI {client['roi_pct']:.0f}% · \"\n",
    "          f\"payback {client['payback_label']}\"),\n",
    "    font=dict(size=12, color=INK), bgcolor=SURFACE,\n",
    "    bordercolor=GRID, borderwidth=1)\n",
    "tei_layout(fig, \"Cash flow — risk-adjusted\",\n",
    "           subtitle=\"benefits up, costs down; initial outlay undiscounted at time 0\")\n",
    "fig.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "cell-17",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.329401Z",
     "iopub.status.busy": "2026-07-09T18:15:43.329247Z",
     "iopub.status.idle": "2026-07-09T18:15:43.348656Z",
     "shell.execute_reply": "2026-07-09T18:15:43.348131Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "connector": {
          "line": {
           "color": "#e1e0d9"
          }
         },
         "decreasing": {
          "marker": {
           "color": "#e34948"
          }
         },
         "increasing": {
          "marker": {
           "color": "#1baf7a"
          }
         },
         "measure": [
          "relative",
          "relative",
          "total"
         ],
         "text": [
          "&#36;101.7M",
          "-&#36;23.0M",
          "&#36;78.7M"
         ],
         "textposition": "outside",
         "totals": {
          "marker": {
           "color": "#2a78d6"
          }
         },
         "type": "waterfall",
         "x": [
          "Benefits PV",
          "Costs PV",
          "NPV"
         ],
         "y": [
          101696567.55071373,
          -22983075.775356874,
          0
         ]
        }
       ],
       "layout": {
        "bargap": 0.45,
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 420,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "showlegend": false,
        "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>Present value walk — client overlay</b><br><span style='font-size:12px;color:#898781'>discounted at 10%, 3 years</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(go.Waterfall(\n",
    "    x=[\"Benefits PV\", \"Costs PV\", \"NPV\"],\n",
    "    measure=[\"relative\", \"relative\", \"total\"],\n",
    "    y=[client[\"benefits_pv\"], -client[\"costs_pv\"], 0],\n",
    "    text=[html_money(client[\"benefits_pv\"]), html_money(-client[\"costs_pv\"]),\n",
    "          html_money(client[\"npv\"])],\n",
    "    textposition=\"outside\",\n",
    "    connector=dict(line=dict(color=GRID)),\n",
    "    increasing=dict(marker=dict(color=BEN_TOTAL)),\n",
    "    decreasing=dict(marker=dict(color=COST_TOTAL)),\n",
    "    totals=dict(marker=dict(color=NPV_COLOR)),\n",
    "))\n",
    "fig.update_layout(showlegend=False)\n",
    "tei_layout(fig, \"Present value walk — client overlay\",\n",
    "           subtitle=f\"discounted at {'{:.0%}'.format(DISCOUNT_RATE)}, 3 years\",\n",
    "           height=420)\n",
    "fig.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-18",
   "metadata": {},
   "source": [
    "<a id=\"section-6\"></a>\n",
    "## 6 · Scenarios (🟡)\n",
    "\n",
    "Scenarios stress the overlay on two levers: **adoption** scales every\n",
    "nominal value (including the initial outlay), and **risk delta** widens or\n",
    "narrows the TEI risk adjustments — added to benefit risk, subtracted from\n",
    "cost risk, clamped at zero.\n",
    "\n",
    "One counterintuitive consequence, worth stating: the **conservative**\n",
    "scenario *lowers* costs PV as well as benefits — 80% adoption shrinks the\n",
    "consumption-priced usage cost, and the clamp caps how much extra padding\n",
    "the risk delta can add back. The case direction is still conservative:\n",
    "NPV and ROI both fall.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "cell-19",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.353547Z",
     "iopub.status.busy": "2026-07-09T18:15:43.353383Z",
     "iopub.status.idle": "2026-07-09T18:15:43.362921Z",
     "shell.execute_reply": "2026-07-09T18:15:43.362320Z"
    }
   },
   "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>Adoption</th>\n",
       "      <th>Risk Δ</th>\n",
       "      <th>Benefits PV</th>\n",
       "      <th>Costs PV</th>\n",
       "      <th>NPV</th>\n",
       "      <th>ROI %</th>\n",
       "      <th>Payback (months)</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Scenario</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>conservative</th>\n",
       "      <td>80%</td>\n",
       "      <td>+10%</td>\n",
       "      <td>71,672,868</td>\n",
       "      <td>17,437,916</td>\n",
       "      <td>54,234,951</td>\n",
       "      <td>311</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>moderate</th>\n",
       "      <td>100%</td>\n",
       "      <td>+0%</td>\n",
       "      <td>101,696,568</td>\n",
       "      <td>22,983,076</td>\n",
       "      <td>78,713,492</td>\n",
       "      <td>342</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>aggressive</th>\n",
       "      <td>115%</td>\n",
       "      <td>-5%</td>\n",
       "      <td>123,911,705</td>\n",
       "      <td>27,682,369</td>\n",
       "      <td>96,229,337</td>\n",
       "      <td>348</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             Adoption Risk Δ  Benefits PV   Costs PV        NPV  ROI %  \\\n",
       "Scenario                                                                 \n",
       "conservative      80%   +10%   71,672,868 17,437,916 54,234,951    311   \n",
       "moderate         100%    +0%  101,696,568 22,983,076 78,713,492    342   \n",
       "aggressive       115%    -5%  123,911,705 27,682,369 96,229,337    348   \n",
       "\n",
       "              Payback (months)  \n",
       "Scenario                        \n",
       "conservative                 1  \n",
       "moderate                     1  \n",
       "aggressive                   1  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Scenario sweep over the client overlay ───────────────────────────\n",
    "scen_summaries = {\n",
    "    s: compute_summary(apply_scenario(overlay_benefits, s),\n",
    "                       apply_scenario(overlay_costs, s), DISCOUNT_RATE)\n",
    "    for s in SCENARIOS\n",
    "}\n",
    "scen_df = pd.DataFrame([{\n",
    "    \"Scenario\": s,\n",
    "    \"Adoption\": f\"{SCENARIOS[s]['adoption']:.0%}\",\n",
    "    \"Risk Δ\": f\"{SCENARIOS[s]['risk_delta']:+.0%}\",\n",
    "    \"Benefits PV\": r[\"benefits_pv\"],\n",
    "    \"Costs PV\": r[\"costs_pv\"],\n",
    "    \"NPV\": r[\"npv\"],\n",
    "    \"ROI %\": round(r[\"roi_pct\"], 1),\n",
    "    \"Payback (months)\": round(r[\"payback_months\"], 2)\n",
    "        if r[\"payback_months\"] is not None else float(\"nan\"),\n",
    "} for s, r in scen_summaries.items()]).set_index(\"Scenario\")\n",
    "display(scen_df)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cell-20",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.366882Z",
     "iopub.status.busy": "2026-07-09T18:15:43.366731Z",
     "iopub.status.idle": "2026-07-09T18:15:43.383646Z",
     "shell.execute_reply": "2026-07-09T18:15:43.383094Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Benefits PV",
         "type": "bar",
         "x": [
          "conservative",
          "moderate",
          "aggressive"
         ],
         "y": [
          71672867.64838466,
          101696567.55071373,
          123911705.40270472
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Costs PV",
         "type": "bar",
         "x": [
          "conservative",
          "moderate",
          "aggressive"
         ],
         "y": [
          17437916.33959429,
          22983075.775356874,
          27682368.613918103
         ]
        },
        {
         "hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
         "marker": {
          "color": "#2a78d6",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "NPV",
         "type": "bar",
         "x": [
          "conservative",
          "moderate",
          "aggressive"
         ],
         "y": [
          54234951.30879037,
          78713491.77535686,
          96229336.78878662
         ]
        }
       ],
       "layout": {
        "bargap": 0.45,
        "barmode": "group",
        "font": {
         "color": "#52514e",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 420,
        "hovermode": "x unified",
        "legend": {
         "font": {
          "color": "#52514e",
          "size": 11
         },
         "orientation": "h",
         "x": 0,
         "y": -0.1,
         "yanchor": "top"
        },
        "margin": {
         "b": 80,
         "l": 70,
         "r": 30,
         "t": 70
        },
        "paper_bgcolor": "#fcfcfb",
        "plot_bgcolor": "#fcfcfb",
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0.0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1.0,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0.0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1.0,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "font": {
          "color": "#0b0b0b",
          "size": 16
         },
         "text": "<b>Scenario comparison — client overlay</b><br><span style='font-size:12px;color:#898781'>adoption × risk-delta stress on the same drivers</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",
    "_scen = list(scen_summaries)\n",
    "for _name, _key, _color in [(\"Benefits PV\", \"benefits_pv\", BEN_TOTAL),\n",
    "                            (\"Costs PV\", \"costs_pv\", COST_TOTAL),\n",
    "                            (\"NPV\", \"npv\", NPV_COLOR)]:\n",
    "    fig.add_trace(bar(_scen, [scen_summaries[s][_key] for s in _scen],\n",
    "                      _name, _color))\n",
    "fig.update_layout(barmode=\"group\")\n",
    "tei_layout(fig, \"Scenario comparison — client overlay\",\n",
    "           subtitle=\"adoption × risk-delta stress on the same drivers\",\n",
    "           height=420)\n",
    "fig.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-21",
   "metadata": {},
   "source": [
    "<a id=\"section-7\"></a>\n",
    "## 7 · Verification & assertions\n",
    "\n",
    "The gate re-derives the case from the engine and asserts: the verbatim\n",
    "anchor is intact; the engine reproduces Forrester's published totals\n",
    "within table rounding (±$1,000); the overlay is the identity at composite\n",
    "scale; and the structural identities hold at **any** widget setting. It\n",
    "must pass in a headless `nbconvert --execute` run — that is this study's\n",
    "regression check.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "cell-22",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.387984Z",
     "iopub.status.busy": "2026-07-09T18:15:43.387815Z",
     "iopub.status.idle": "2026-07-09T18:15:43.399773Z",
     "shell.execute_reply": "2026-07-09T18:15:43.399196Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All assertions passed.\n",
      "  reproduction Δ vs PDF: benefits -223.45 · costs -0.22 · npv -223.22\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",
    "# ── Anchor integrity — the verbatim record is intact (unconditional) ─\n",
    "_approx(BENEFITS_VERBATIM[0][\"year_values\"][\"1\"], 13_911_040)\n",
    "_approx(COSTS_VERBATIM[1][\"initial\"], 1_087_500)\n",
    "assert ASSUMPTIONS[\"agents_fte\"] == 2_000\n",
    "assert ASSUMPTIONS[\"annual_contacts_y1\"] == 20_000_000\n",
    "assert (COMPOSITE.agents_fte, COMPOSITE.annual_contacts_y1) == (2_000, 20_000_000)\n",
    "\n",
    "# ── Published reproduction — engine defaults, explicit args ──────────\n",
    "_c = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)\n",
    "_approx(_c[\"benefits_pv\"], PUBLISHED[\"benefits_pv\"], tol=1_000)   # Δ −223.45 (PDF rounding)\n",
    "_approx(_c[\"costs_pv\"], PUBLISHED[\"costs_pv\"], tol=1_000)         # Δ −0.22\n",
    "_approx(_c[\"npv\"], PUBLISHED[\"npv\"], tol=1_000)                   # Δ −223.22\n",
    "assert round(_c[\"roi_pct\"]) == PUBLISHED[\"roi_pct\"]               # 342.48 → 342\n",
    "assert _c[\"payback_months\"] < PUBLISHED[\"payback_months_max\"]     # \"<6 months\"\n",
    "_approx(_c[\"payback_months\"], 0.72, tol=0.01)\n",
    "_approx(_c[\"initial_costs\"], 1_196_250)\n",
    "_approx(_c[\"benefits_by_year\"][2026], 27_279_019, tol=1)\n",
    "_approx(_c[\"costs_by_year\"][2028], 10_539_889.05, tol=1)\n",
    "\n",
    "# ── Overlay identity + scaling behaviour (explicit args) ─────────────\n",
    "_ob, _oc = overlay_rows(COMPOSITE)\n",
    "_id = compute_summary(_ob, _oc, 0.10)\n",
    "_approx(_id[\"benefits_pv\"], _c[\"benefits_pv\"], tol=0.01)          # identity at composite\n",
    "_hb, _ = overlay_rows(ClientDrivers(agents_fte=1_000))\n",
    "_approx(next(r for r in _hb if r[\"field_key\"] == \"ai_forecasting_supervision\")\n",
    "        [\"year_values\"][\"1\"], 6_651_680 / 2)                      # agents-driven halves\n",
    "_approx(next(r for r in _hb if r[\"field_key\"] == \"ai_contact_resolution\")\n",
    "        [\"year_values\"][\"1\"], 13_911_040)                         # contacts-driven unmoved\n",
    "\n",
    "# ── Scenario pin (explicit args) ─────────────────────────────────────\n",
    "_s = compute_summary(apply_scenario(BENEFITS_VERBATIM, \"conservative\"),\n",
    "                     apply_scenario(COSTS_VERBATIM, \"conservative\"), 0.10)\n",
    "_approx(_s[\"npv\"], 54_234_951.31, tol=1)\n",
    "\n",
    "# ── Structural ties — hold at ANY widget state (unconditional) ───────\n",
    "_approx(client[\"npv\"], client[\"benefits_pv\"] - client[\"costs_pv\"], tol=0.01)\n",
    "_approx(client[\"roi_pct\"], client[\"npv\"] / client[\"costs_pv\"] * 100, tol=0.01)\n",
    "for _y in YEARS:\n",
    "    _approx(client[\"net_by_year\"][_y],\n",
    "            client[\"benefits_by_year\"][_y] - client[\"costs_by_year\"][_y], tol=0.01)\n",
    "_approx(client[\"cumulative_net_by_year\"][YEARS[-1]],\n",
    "        sum(client[\"net_by_year\"].values()) - client[\"initial_costs\"], tol=0.01)\n",
    "_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"benefits\"]), client[\"benefits_pv\"], tol=0.01)\n",
    "_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"costs\"]), client[\"costs_pv\"], tol=0.01)\n",
    "\n",
    "# ── Live state — only when the sidebar sits at the composite defaults ─\n",
    "if _at_default:\n",
    "    _approx(client[\"benefits_pv\"], 101_696_567.55, tol=1)         # engine-exact\n",
    "    _approx(client[\"npv\"], PUBLISHED[\"npv\"], tol=1_000)\n",
    "    _approx(client[\"payback_months\"], 0.72, tol=0.01)\n",
    "\n",
    "backstage(\"All assertions passed.\")\n",
    "backstage(f\"  reproduction Δ vs PDF: benefits \"\n",
    "          f\"{_c['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n",
    "          f\"costs {_c['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n",
    "          f\"npv {_c['npv'] - PUBLISHED['npv']:+,.2f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23",
   "metadata": {},
   "source": [
    "<a id=\"section-8\"></a>\n",
    "## 8 · Data appendix — for the machines\n",
    "\n",
    "Everything below renders **backstage only** (JupyterLab / nbconvert\n",
    "exports): markdown tables plus one JSON block of model state. Carried in\n",
    "the exports, this is the payload a downstream LLM — or, on the roadmap,\n",
    "Athena as the study repository — consumes directly. On the Mercury stage\n",
    "it stays hidden.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "cell-24",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T18:15:43.404067Z",
     "iopub.status.busy": "2026-07-09T18:15:43.403921Z",
     "iopub.status.idle": "2026-07-09T18:15:43.419260Z",
     "shell.execute_reply": "2026-07-09T18:15:43.418439Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "#### Composite organization (verbatim assumptions 🟢)\n",
      "\n",
      "| Assumption         | Value 🟢   |\n",
      "|:-------------------|:-----------|\n",
      "| agents fte         | 2,000      |\n",
      "| supervisors fte    | 200        |\n",
      "| annual contacts y1 | 20,000,000 |\n",
      "| growth rate        | 30%        |\n",
      "| call share         | 75%        |\n",
      "| aht legacy minutes | 10 min     |\n",
      "| agent salary       | $45,760    |\n",
      "| supervisor salary  | $55,800    |\n",
      "| discount rate      | 10%        |\n",
      "| analysis years     | 3 years    |\n",
      "\n",
      "#### Benefits — client overlay (risk-adjusted $)\n",
      "\n",
      "| Benefit                                                   | Driver   | Risk adj   |       2026 |       2027 |       2028 |     3-yr RA |          PV |\n",
      "|:----------------------------------------------------------|:---------|:-----------|-----------:|-----------:|-----------:|------------:|------------:|\n",
      "| AI-driven contact resolution efficiency                   | contacts | -15%       | 11,824,384 | 20,342,608 | 32,128,096 |  64,295,088 |  51,699,827 |\n",
      "| AI-powered content and sentiment analysis savings         | contacts | -15%       |  3,898,627 |  4,554,650 |  5,347,928 |  13,801,205 |  11,326,358 |\n",
      "| AI-enabled forecasting, agent scheduling, and supervision | agents   | -15%       |  5,653,928 |  7,763,696 | 10,532,955 |  23,950,579 |  19,469,777 |\n",
      "| Data-driven profit lift with increased conversion         | contacts | -20%       |    960,000 |  1,248,000 |  1,622,400 |   3,830,400 |   3,123,065 |\n",
      "| Legacy solution cost savings                              | agents   | -20%       |  4,942,080 |  6,424,704 |  8,352,115 |  19,718,899 |  16,077,540 |\n",
      "| TOTAL                                                     |          |            | 27,279,019 | 40,333,658 | 57,983,494 | 125,596,172 | 101,696,568 |\n",
      "\n",
      "#### Costs — client overlay (risk-adjusted $)\n",
      "\n",
      "| Cost                              | Driver   | Risk adj   |   Initial |      2026 |      2027 |       2028 |         PV |\n",
      "|:----------------------------------|:---------|:-----------|----------:|----------:|----------:|-----------:|-----------:|\n",
      "| Amazon Connect usage cost         | contacts | +5%        |         0 | 6,779,270 | 8,348,722 | 10,324,609 | 20,819,775 |\n",
      "| Implementation and migration cost | fixed    | +10%       | 1,196,250 |   207,166 |   207,166 |          0 |  1,555,795 |\n",
      "| Ongoing management                | fixed    | +15%       |         0 |   294,630 |   215,280 |    215,280 |    607,506 |\n",
      "| TOTAL                             |          |            | 1,196,250 | 7,281,067 | 8,771,168 | 10,539,889 | 22,983,076 |\n",
      "\n",
      "#### KPIs — published composite vs client overlay\n",
      "\n",
      "|               | Forrester composite (published 🟢)   | Client overlay (🟡)    |\n",
      "|:--------------|:-------------------------------------|:-----------------------|\n",
      "| Benefits PV   | $101,696,791                         | $101,696,568           |\n",
      "| Costs PV      | $22,983,076                          | $22,983,076            |\n",
      "| NPV           | $78,713,715                          | $78,713,492            |\n",
      "| ROI           | 342%                                 | 342%                   |\n",
      "| Payback       | <6 months                            | 0.7 months (~Jan 2026) |\n",
      "| Discount rate | 10%                                  | 10%                    |\n",
      "\n",
      "#### Scenarios (client overlay)\n",
      "\n",
      "| Scenario     | Adoption   | Risk Δ   |   Benefits PV |   Costs PV |        NPV |   ROI % |   Payback (months) |\n",
      "|:-------------|:-----------|:---------|--------------:|-----------:|-----------:|--------:|-------------------:|\n",
      "| conservative | 80%        | +10%     |    71,672,868 | 17,437,916 | 54,234,951 |     311 |                  1 |\n",
      "| moderate     | 100%       | +0%      |   101,696,568 | 22,983,076 | 78,713,492 |     342 |                  1 |\n",
      "| aggressive   | 115%       | -5%      |   123,911,705 | 27,682,369 | 96,229,337 |     348 |                  1 |\n",
      "\n",
      "#### Model state (JSON)\n",
      "\n",
      "```json\n",
      "{\n",
      "  \"study\": \"202602_TEI_Amazon_Connect\",\n",
      "  \"source\": \"Forrester TEI of Amazon Connect (Feb 2026, commissioned by AWS)\",\n",
      "  \"published\": {\n",
      "    \"benefits_pv\": 101696791,\n",
      "    \"costs_pv\": 22983076,\n",
      "    \"npv\": 78713715,\n",
      "    \"roi_pct\": 342,\n",
      "    \"payback_months_max\": 6,\n",
      "    \"discount_rate\": 0.1,\n",
      "    \"analysis_years\": 3\n",
      "  },\n",
      "  \"reproduction\": {\n",
      "    \"benefits_pv\": 101696567.55,\n",
      "    \"costs_pv\": 22983075.78,\n",
      "    \"npv\": 78713491.78,\n",
      "    \"roi_pct\": 342.48,\n",
      "    \"payback_months\": 0.72\n",
      "  },\n",
      "  \"client\": {\n",
      "    \"drivers\": {\n",
      "      \"agents_fte\": 2000,\n",
      "      \"annual_contacts_y1\": 20000000,\n",
      "      \"growth_rate\": 0.3,\n",
      "      \"discount_rate\": 0.1,\n",
      "      \"scenario\": \"moderate\"\n",
      "    },\n",
      "    \"benefits_by_year\": {\n",
      "      \"2026\": 27279019,\n",
      "      \"2027\": 40333658,\n",
      "      \"2028\": 57983494\n",
      "    },\n",
      "    \"costs_by_year\": {\n",
      "      \"2026\": 7281067,\n",
      "      \"2027\": 8771168,\n",
      "      \"2028\": 10539889\n",
      "    },\n",
      "    \"net_by_year\": {\n",
      "      \"2026\": 19997952,\n",
      "      \"2027\": 31562490,\n",
      "      \"2028\": 47443605\n",
      "    },\n",
      "    \"cumulative_net_by_year\": {\n",
      "      \"2026\": 18801702,\n",
      "      \"2027\": 50364192,\n",
      "      \"2028\": 97807797\n",
      "    },\n",
      "    \"initial_costs\": 1196250,\n",
      "    \"kpis\": {\n",
      "      \"benefits_pv\": 101696568,\n",
      "      \"costs_pv\": 22983076,\n",
      "      \"npv\": 78713492,\n",
      "      \"roi_pct\": 342.48,\n",
      "      \"payback_months\": 0.72,\n",
      "      \"payback_label\": \"0.7 months (~Jan 2026)\"\n",
      "    },\n",
      "    \"scenarios\": {\n",
      "      \"conservative\": {\n",
      "        \"benefits_pv\": 71672868,\n",
      "        \"costs_pv\": 17437916,\n",
      "        \"npv\": 54234951,\n",
      "        \"roi_pct\": 311.02\n",
      "      },\n",
      "      \"moderate\": {\n",
      "        \"benefits_pv\": 101696568,\n",
      "        \"costs_pv\": 22983076,\n",
      "        \"npv\": 78713492,\n",
      "        \"roi_pct\": 342.48\n",
      "      },\n",
      "      \"aggressive\": {\n",
      "        \"benefits_pv\": 123911705,\n",
      "        \"costs_pv\": 27682369,\n",
      "        \"npv\": 96229337,\n",
      "        \"roi_pct\": 347.62\n",
      "      }\n",
      "    }\n",
      "  },\n",
      "  \"driver_map\": {\n",
      "    \"benefits\": {\n",
      "      \"ai_contact_resolution\": \"contacts\",\n",
      "      \"ai_content_sentiment\": \"contacts\",\n",
      "      \"ai_forecasting_supervision\": \"agents\",\n",
      "      \"data_driven_profit_lift\": \"contacts\",\n",
      "      \"legacy_solution_savings\": \"agents\"\n",
      "    },\n",
      "    \"costs\": {\n",
      "      \"amazon_connect_usage\": \"contacts\",\n",
      "      \"implementation_migration\": \"fixed\",\n",
      "      \"ongoing_management\": \"fixed\"\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(\"Composite organization (verbatim assumptions 🟢)\",\n",
    "         assumptions_df, index=False)\n",
    "_section(\"Benefits — client overlay (risk-adjusted $)\", benefits_df)\n",
    "_section(\"Costs — client overlay (risk-adjusted $)\", costs_df)\n",
    "_section(\"KPIs — published composite vs client overlay\", kpis_fmt)\n",
    "_section(\"Scenarios (client overlay)\", scen_df)\n",
    "\n",
    "backstage(\"\\n#### Model state (JSON)\\n\")\n",
    "backstage(\"```json\")\n",
    "backstage(_json.dumps({\n",
    "    \"study\": \"202602_TEI_Amazon_Connect\",\n",
    "    \"source\": \"Forrester TEI of Amazon Connect (Feb 2026, commissioned by AWS)\",\n",
    "    \"published\": PUBLISHED,\n",
    "    \"reproduction\": {\n",
    "        \"benefits_pv\": round(composite[\"benefits_pv\"], 2),\n",
    "        \"costs_pv\": round(composite[\"costs_pv\"], 2),\n",
    "        \"npv\": round(composite[\"npv\"], 2),\n",
    "        \"roi_pct\": round(composite[\"roi_pct\"], 2),\n",
    "        \"payback_months\": round(composite[\"payback_months\"], 2),\n",
    "    },\n",
    "    \"client\": {\n",
    "        \"drivers\": {\n",
    "            \"agents_fte\": AGENTS_FTE,\n",
    "            \"annual_contacts_y1\": CONTACTS_Y1,\n",
    "            \"growth_rate\": GROWTH_RATE,\n",
    "            \"discount_rate\": DISCOUNT_RATE,\n",
    "            \"scenario\": SCENARIO,\n",
    "        },\n",
    "        \"benefits_by_year\": {str(y): round(client[\"benefits_by_year\"][y]) for y in YEARS},\n",
    "        \"costs_by_year\": {str(y): round(client[\"costs_by_year\"][y]) for y in YEARS},\n",
    "        \"net_by_year\": {str(y): round(client[\"net_by_year\"][y]) for y in YEARS},\n",
    "        \"cumulative_net_by_year\": {str(y): round(client[\"cumulative_net_by_year\"][y]) for y in YEARS},\n",
    "        \"initial_costs\": round(client[\"initial_costs\"]),\n",
    "        \"kpis\": {\n",
    "            \"benefits_pv\": round(client[\"benefits_pv\"]),\n",
    "            \"costs_pv\": round(client[\"costs_pv\"]),\n",
    "            \"npv\": round(client[\"npv\"]),\n",
    "            \"roi_pct\": round(client[\"roi_pct\"], 2),\n",
    "            \"payback_months\": round(client[\"payback_months\"], 2)\n",
    "                if client[\"payback_months\"] is not None else None,\n",
    "            \"payback_label\": client[\"payback_label\"],\n",
    "        },\n",
    "        \"scenarios\": {\n",
    "            s: {\"benefits_pv\": round(r[\"benefits_pv\"]),\n",
    "                \"costs_pv\": round(r[\"costs_pv\"]),\n",
    "                \"npv\": round(r[\"npv\"]),\n",
    "                \"roi_pct\": round(r[\"roi_pct\"], 2)}\n",
    "            for s, r in scen_summaries.items()\n",
    "        },\n",
    "    },\n",
    "    \"driver_map\": {\"benefits\": BENEFIT_DRIVERS, \"costs\": COST_DRIVERS},\n",
    "}, indent=2))\n",
    "backstage(\"```\")\n"
   ]
  }
 ],
 "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": {
     "01c230d30aed4aa58c5a005c44e65f84": {
      "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      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      padding-top: 6px;\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      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": "Annual contacts, year 1 — composite 20M",
       "layout": "IPY_MODEL_cc4716028c0c4335b1d70f4e29e905f6",
       "layout_path": null,
       "max": 500000000.0,
       "min": 100000.0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 1000000.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 20000000.0
      }
     },
     "0833fb2c5bd3466aba5ff466dec6ff1b": {
      "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_ae315fb739f643f7a1fbe0f3678a6002",
       "layout_path": null,
       "placeholder": "​",
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "style": "IPY_MODEL_f9174ef9b9674e9d8c64bd888d70130b",
       "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'});\">Composite organization</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'});\">Client inputs</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'});\">Benefits</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-4');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('4 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">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-5');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('5 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Business case</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-6');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('6 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Scenarios</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-7');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('7 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verification &amp; assertions</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-8');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('8 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
      }
     },
     "48a9c76ed0fb498984234409f404ac3f": {
      "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
      }
     },
     "ace9c00dc3a84babb5b29ae61e2be62c": {
      "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      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      padding-top: 6px;\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      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": "Contact-center agents (FTE) — composite 2,000",
       "layout": "IPY_MODEL_ce0e81f1ea384a79897a9eb663bda565",
       "layout_path": null,
       "max": 50000.0,
       "min": 50.0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 50.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 2000.0
      }
     },
     "ae315fb739f643f7a1fbe0f3678a6002": {
      "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
      }
     },
     "b0de84b09c5743b9a4e7a5dbdf1973d9": {
      "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
      }
     },
     "b1e82327c29a44a8a4a45e8100b3d857": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.select.SelectWidget",
       "_css": "\n    .mljar-select-container {\n      position: relative;\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      padding-left: 4px;\n      padding-right: 4px;\n      overflow: visible;\n    }\n\n    .mljar-select-label {\n      padding-top: 6px;\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      overflow: visible;\n    }\n\n    .mljar-select-container.is-open {\n      z-index: 20;\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      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: auto;\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      position: fixed;\n      z-index: 10000;\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      el.appendChild(container);\n\n      let isOpen = false;\n      let filteredChoices = [];\n      let lastCommittedValue = \"\";\n      let isEditing = false;\n      document.body.appendChild(dropdown);\n\n      const updateDropdownPosition = () => {\n        if (!isOpen) {\n          return;\n        }\n        const rect = control.getBoundingClientRect();\n        dropdown.style.top = `${rect.bottom + 6}px`;\n        dropdown.style.left = `${rect.left}px`;\n        dropdown.style.width = `${rect.width}px`;\n      };\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        if (isOpen) {\n          updateDropdownPosition();\n        }\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      const closeDropdown = () => {\n        isEditing = false;\n        setOpen(false);\n        input.value = lastCommittedValue;\n      };\n\n      control.addEventListener(\"click\", event => {\n        event.stopPropagation();\n        if (isDisabled()) {\n          return;\n        }\n        if (event.target === caret && isOpen) {\n          closeDropdown();\n          input.blur();\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) && !dropdown.contains(event.target)) {\n          closeDropdown();\n        }\n      };\n\n      document.addEventListener(\"click\", handleDocumentClick);\n      window.addEventListener(\"resize\", updateDropdownPosition);\n      document.addEventListener(\"scroll\", updateDropdownPosition, true);\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          closeDropdown();\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        dropdown.remove();\n        document.removeEventListener(\"click\", handleDocumentClick);\n        window.removeEventListener(\"resize\", updateDropdownPosition);\n        document.removeEventListener(\"scroll\", updateDropdownPosition, true);\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": [
        "conservative",
        "moderate",
        "aggressive"
       ],
       "disabled": false,
       "hidden": false,
       "label": "Scenario",
       "layout": "IPY_MODEL_b0de84b09c5743b9a4e7a5dbdf1973d9",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "moderate"
      }
     },
     "cc4716028c0c4335b1d70f4e29e905f6": {
      "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
      }
     },
     "ce0e81f1ea384a79897a9eb663bda565": {
      "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
      }
     },
     "d4ccace17e824e4a85c55ff6ef95afb0": {
      "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      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      padding-top: 6px;\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      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": "Contact growth (%/yr) — composite 30",
       "layout": "IPY_MODEL_48a9c76ed0fb498984234409f404ac3f",
       "layout_path": null,
       "max": 100.0,
       "min": 0.0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 5.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 30.0
      }
     },
     "d67347bf93954b6abc75a466c3259dd3": {
      "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
      }
     },
     "eb0056195e9b444dbb8ef3c08e744446": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.select.SelectWidget",
       "_css": "\n    .mljar-select-container {\n      position: relative;\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      padding-left: 4px;\n      padding-right: 4px;\n      overflow: visible;\n    }\n\n    .mljar-select-label {\n      padding-top: 6px;\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      overflow: visible;\n    }\n\n    .mljar-select-container.is-open {\n      z-index: 20;\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      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: auto;\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      position: fixed;\n      z-index: 10000;\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      el.appendChild(container);\n\n      let isOpen = false;\n      let filteredChoices = [];\n      let lastCommittedValue = \"\";\n      let isEditing = false;\n      document.body.appendChild(dropdown);\n\n      const updateDropdownPosition = () => {\n        if (!isOpen) {\n          return;\n        }\n        const rect = control.getBoundingClientRect();\n        dropdown.style.top = `${rect.bottom + 6}px`;\n        dropdown.style.left = `${rect.left}px`;\n        dropdown.style.width = `${rect.width}px`;\n      };\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        if (isOpen) {\n          updateDropdownPosition();\n        }\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      const closeDropdown = () => {\n        isEditing = false;\n        setOpen(false);\n        input.value = lastCommittedValue;\n      };\n\n      control.addEventListener(\"click\", event => {\n        event.stopPropagation();\n        if (isDisabled()) {\n          return;\n        }\n        if (event.target === caret && isOpen) {\n          closeDropdown();\n          input.blur();\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) && !dropdown.contains(event.target)) {\n          closeDropdown();\n        }\n      };\n\n      document.addEventListener(\"click\", handleDocumentClick);\n      window.addEventListener(\"resize\", updateDropdownPosition);\n      document.addEventListener(\"scroll\", updateDropdownPosition, true);\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          closeDropdown();\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        dropdown.remove();\n        document.removeEventListener(\"click\", handleDocumentClick);\n        window.removeEventListener(\"resize\", updateDropdownPosition);\n        document.removeEventListener(\"scroll\", updateDropdownPosition, true);\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": [
        "8%",
        "10% (Forrester)",
        "12%"
       ],
       "disabled": false,
       "hidden": false,
       "label": "Discount rate",
       "layout": "IPY_MODEL_d67347bf93954b6abc75a466c3259dd3",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "10% (Forrester)"
      }
     },
     "f9174ef9b9674e9d8c64bd888d70130b": {
      "model_module": "@jupyter-widgets/controls",
      "model_module_version": "2.0.0",
      "model_name": "HTMLStyleModel",
      "state": {
       "_model_module": "@jupyter-widgets/controls",
       "_model_module_version": "2.0.0",
       "_model_name": "HTMLStyleModel",
       "_view_count": null,
       "_view_module": "@jupyter-widgets/base",
       "_view_module_version": "2.0.0",
       "_view_name": "StyleView",
       "background": null,
       "description_width": "",
       "font_size": null,
       "text_color": null
      }
     }
    },
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
