{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-0",
   "metadata": {},
   "source": [
    "# [Study Title] — Business Case\n",
    "\n",
    "**Thesis:** one paragraph stating what this notebook demonstrates and the frame it uses\n",
    "(here: a platform migration priced against *doing nothing*, with the vendor's pitched\n",
    "numbers kept verbatim as the anchor and the signed contract layered over them).\n",
    "\n",
    "This notebook **is** the deliverable: serve it interactively with\n",
    "`mercury --working-dir notebooks/`, tune the 🟡 inputs live for the client, then export\n",
    "an LLM-readable report source with `python scripts/export_report.py`. All math lives in\n",
    "`studylib/` — the notebook renders it.\n",
    "\n",
    "Confidence legend: 🟢 confirmed (published/contractual) · 🟡 estimated (working\n",
    "assumption) · 🔴 unknown."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cell-1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:50.535111Z",
     "iopub.status.busy": "2026-07-08T16:36:50.534789Z",
     "iopub.status.idle": "2026-07-08T16:36:50.959725Z",
     "shell.execute_reply": "2026-07-08T16:36:50.956659Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "studylib loaded — window 2026–2028\n"
     ]
    }
   ],
   "source": [
    "# ── Setup ──────────────────────────────────────────────────────────\n",
    "import sys, pathlib\n",
    "_ROOT = pathlib.Path.cwd()\n",
    "if not (_ROOT / \"studylib\").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 library; only\n",
    "# presentation (and Mercury input widgets) lives here.\n",
    "from studylib.model import (\n",
    "    YEARS, ANCHOR_VERBATIM, DEFAULT_RAMP_MONTHS,\n",
    "    anchor, benefits_by_year, case_flows, case_kpis, current_costs_by_year,\n",
    "    money, html_money, payback_label, platform_costs_by_year, services_by_year,\n",
    ")\n",
    "from studylib.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 = \"#fcfcfb\", \"#e1e0d9\"\n",
    "CUMULATIVE, CONTEXT = \"#52514e\", \"#c3c2b7\"\n",
    "FONT_STACK = 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n",
    "\n",
    "\n",
    "def tei_layout(fig, title, subtitle=None, height=420):\n",
    "    \"\"\"House chart chrome — recessive grid, ink text, title top-left.\"\"\"\n",
    "    t = f\"<b>{title}</b>\"\n",
    "    if subtitle:\n",
    "        t += f\"<br><span style='font-size:12px;color:{INK2}'>{subtitle}</span>\"\n",
    "    fig.update_layout(\n",
    "        title=dict(text=t, font=dict(size=16, color=INK), x=0, xanchor=\"left\"),\n",
    "        font=dict(family=FONT_STACK, size=12, color=INK),\n",
    "        paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n",
    "        margin=dict(l=60, r=30, t=80, b=45), height=height,\n",
    "        legend=dict(orientation=\"h\", yanchor=\"bottom\", y=1.0, x=0,\n",
    "                    bgcolor=\"rgba(0,0,0,0)\"),\n",
    "        xaxis=dict(showgrid=False, zeroline=False),\n",
    "        yaxis=dict(gridcolor=GRID, zeroline=False, tickformat=\"$,.0f\"),\n",
    "        hovermode=\"x unified\",\n",
    "    )\n",
    "    return fig\n",
    "\n",
    "\n",
    "def bar(x, y, name, color):\n",
    "    return go.Bar(x=x, y=y, name=name, marker_color=color,\n",
    "                  marker_line=dict(color=SURFACE, width=2),\n",
    "                  hovertemplate=\"%{y:$,.0f}<extra>\" + name + \"</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),\n",
    "                      hovertemplate=\"%{y:$,.0f}<extra>\" + name + \"</extra>\")\n",
    "\n",
    "\n",
    "X = [str(y) for y in YEARS]\n",
    "backstage(f\"studylib loaded — window {YEARS[0]}–{YEARS[-1]}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-2",
   "metadata": {},
   "source": [
    "<a id=\"section-1\"></a>\n",
    "\n",
    "## 1 · Inputs\n",
    "\n",
    "Contract inputs collected as live widgets. Sidebar widgets are scenario knobs; use\n",
    "`position=\"inline\"` for data-collection tables that belong in the page flow.\n",
    "\n",
    "> **Reactivity contract:** Mercury re-executes only the cells *below* a changed\n",
    "> widget's cell — never the defining cell itself. So the next cell constructs\n",
    "> widgets ONLY (no other output), and `.value` is read one cell further down."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "cell-3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:50.962324Z",
     "iopub.status.busy": "2026-07-08T16:36:50.961887Z",
     "iopub.status.idle": "2026-07-08T16:36:50.977751Z",
     "shell.execute_reply": "2026-07-08T16:36:50.976977Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/mercury+json": {
       "model_id": "924aaf1b9a7244388d4fdc29e0f544e2",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "924aaf1b9a7244388d4fdc29e0f544e2",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f00bfe02cf0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "60e56739936e4f2ab8ce7a25fe31c092",
       "position": "sidebar",
       "widget": "NumberInputWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "60e56739936e4f2ab8ce7a25fe31c092",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.number.NumberInputWidget object at 0x7f00bfd15950>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/mercury+json": {
       "model_id": "56c2f21d17cf40009b6d4bcee7bcdd9a",
       "position": "sidebar",
       "widget": "SelectWidget"
      },
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "56c2f21d17cf40009b6d4bcee7bcdd9a",
       "version_major": 2,
       "version_minor": 1
      },
      "text/plain": [
       "<mercury.select.SelectWidget object at 0x7f01201d9160>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ── Inputs (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",
    "_platform_w = mr.NumberInput(label=\"Platform run-rate ($/yr) — contracted\",\n",
    "                             value=round(anchor(\"platform_annual\")),\n",
    "                             min=0, max=10_000_000, step=25_000)\n",
    "_ramp_w = mr.NumberInput(label=\"Ramp — billing-free months\",\n",
    "                         value=DEFAULT_RAMP_MONTHS, min=0, max=24, step=3)\n",
    "_npv_w = mr.Select(label=\"NPV discount rate\", value=\"10% (vendor)\",\n",
    "                   choices=[\"10% (vendor)\", \"8% (treasury)\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "cell-4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:50.980200Z",
     "iopub.status.busy": "2026-07-08T16:36:50.979922Z",
     "iopub.status.idle": "2026-07-08T16:36:50.985441Z",
     "shell.execute_reply": "2026-07-08T16:36:50.984712Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "platform by year: {2026: '$250K', 2027: '$500K', 2028: '$500K'}\n",
      "platform run-rate: contracted $500K/yr (vendor pitched $600K/yr) · ramp 6 months\n"
     ]
    }
   ],
   "source": [
    "# ── Model state (re-runs on any change to the widgets above) ────────\n",
    "PLATFORM_ANNUAL = float(_platform_w.value)\n",
    "RAMP_MONTHS = int(_ramp_w.value)\n",
    "DISCOUNT_RATE = 0.10 if _npv_w.value.startswith(\"10\") else 0.08\n",
    "\n",
    "platform_by_year = platform_costs_by_year(RAMP_MONTHS, PLATFORM_ANNUAL)\n",
    "services_by = services_by_year()\n",
    "current_by_year = current_costs_by_year()\n",
    "ben_by_year = benefits_by_year()\n",
    "BASELINE_ANNUAL = anchor(\"baseline_annual\")\n",
    "\n",
    "backstage(f\"platform by year: { {y: money(v) for y, v in platform_by_year.items()} }\")\n",
    "print(f\"platform run-rate: contracted {money(PLATFORM_ANNUAL)}/yr \"\n",
    "      f\"(vendor pitched {money(ANCHOR_VERBATIM['platform_annual'])}/yr) · \"\n",
    "      f\"ramp {RAMP_MONTHS} months\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-5",
   "metadata": {},
   "source": [
    "<a id=\"section-2\"></a>\n",
    "\n",
    "## 2 · Business case vs doing nothing\n",
    "\n",
    "**Frame:** baseline-relative. Incremental cost = programme cost − baseline\n",
    "(the do-nothing run-rate); net = benefits − incremental cost. Double-billing\n",
    "while the old platform runs off and the post-termination cost-avoidance credit\n",
    "both fall out of this one frame.\n",
    "\n",
    "The KPI table keeps a **vendor-frame column** (pitched rate, verbatim anchors)\n",
    "beside the **contracted column**, so the client can walk from their own numbers\n",
    "to the corrected reality."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "cell-6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:50.987674Z",
     "iopub.status.busy": "2026-07-08T16:36:50.987427Z",
     "iopub.status.idle": "2026-07-08T16:36:51.023013Z",
     "shell.execute_reply": "2026-07-08T16:36:51.022122Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "net by year: {2026: '-$500K', 2027: '-$200K', 2028: '$1.1M'}\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>2026</th>\n",
       "      <th>2027</th>\n",
       "      <th>2028</th>\n",
       "      <th>3-yr</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Platform (contracted, ramp-adjusted)</th>\n",
       "      <td>250,000</td>\n",
       "      <td>500,000</td>\n",
       "      <td>500,000</td>\n",
       "      <td>1,250,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Services (year 1)</th>\n",
       "      <td>250,000</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>250,000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Existing platform (term-contract run-off)</th>\n",
       "      <td>1,000,000</td>\n",
       "      <td>1,000,000</td>\n",
       "      <td>0</td>\n",
       "      <td>2,000,000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                               2026      2027    2028  \\\n",
       "Platform (contracted, ramp-adjusted)        250,000   500,000 500,000   \n",
       "Services (year 1)                           250,000         0       0   \n",
       "Existing platform (term-contract run-off) 1,000,000 1,000,000       0   \n",
       "\n",
       "                                               3-yr  \n",
       "Platform (contracted, ramp-adjusted)      1,250,000  \n",
       "Services (year 1)                           250,000  \n",
       "Existing platform (term-contract run-off) 2,000,000  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Vendor frame ($600K/yr)</th>\n",
       "      <th>Contracted ($500K/yr)</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>3-yr benefits</th>\n",
       "      <td>$900K</td>\n",
       "      <td>$900K</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3-yr incremental cost</th>\n",
       "      <td>$750K</td>\n",
       "      <td>$500K</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3-yr net</th>\n",
       "      <td>$150K</td>\n",
       "      <td>$400K</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>ROI</th>\n",
       "      <td>20%</td>\n",
       "      <td>80%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>NPV @ 10%</th>\n",
       "      <td>$3K</td>\n",
       "      <td>$207K</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Payback</th>\n",
       "      <td>35 months (~Nov 2028)</td>\n",
       "      <td>32 months (~Aug 2028)</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                      Vendor frame ($600K/yr)  Contracted ($500K/yr)\n",
       "3-yr benefits                           $900K                  $900K\n",
       "3-yr incremental cost                   $750K                  $500K\n",
       "3-yr net                                $150K                  $400K\n",
       "ROI                                       20%                    80%\n",
       "NPV @ 10%                                 $3K                  $207K\n",
       "Payback                 35 months (~Nov 2028)  32 months (~Aug 2028)"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "study_costs = pd.DataFrame({\n",
    "    \"Platform (contracted, ramp-adjusted)\": platform_by_year,\n",
    "    \"Services (year 1)\": services_by,\n",
    "    \"Existing platform (term-contract run-off)\": current_by_year,\n",
    "}).T[YEARS]\n",
    "study_costs[\"3-yr\"] = study_costs.sum(axis=1)\n",
    "total_by_year = {y: float(study_costs[y].sum()) for y in YEARS}\n",
    "\n",
    "inc, net_by = case_flows(total_by_year, ben_by_year)\n",
    "kpi = case_kpis(inc, net_by, DISCOUNT_RATE)\n",
    "\n",
    "# Vendor-anchored comparison: same frame at the pitched (verbatim) rate.\n",
    "_plat_vendor = platform_costs_by_year(RAMP_MONTHS, ANCHOR_VERBATIM[\"platform_annual\"])\n",
    "total_vendor = {y: current_by_year[y] + _plat_vendor[y] + services_by[y] for y in YEARS}\n",
    "inc_v, net_v = case_flows(total_vendor, ben_by_year)\n",
    "kpi_v = case_kpis(inc_v, net_v, DISCOUNT_RATE)\n",
    "\n",
    "\n",
    "def kpi_col(k):\n",
    "    return {\n",
    "        \"3-yr benefits\": money(k[\"benefits_3yr\"]),\n",
    "        \"3-yr incremental cost\": money(k[\"incremental_cost_3yr\"]),\n",
    "        \"3-yr net\": money(k[\"net_3yr\"]),\n",
    "        \"ROI\": f\"{k['roi']:.0%}\" if k[\"roi\"] is not None else \"n/a — net saving\",\n",
    "        f\"NPV @ {k['discount_rate']:.0%}\": money(k[\"npv\"]),\n",
    "        \"Payback\": k[\"payback\"],\n",
    "    }\n",
    "\n",
    "\n",
    "kpis_fmt = pd.DataFrame({\n",
    "    f\"Vendor frame ({money(ANCHOR_VERBATIM['platform_annual'])}/yr)\": kpi_col(kpi_v),\n",
    "    f\"Contracted ({money(PLATFORM_ANNUAL)}/yr)\": kpi_col(kpi),\n",
    "})\n",
    "backstage(f\"net by year: { {y: money(v) for y, v in net_by.items()} }\")\n",
    "display(study_costs)\n",
    "display(kpis_fmt)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "cell-7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:51.025383Z",
     "iopub.status.busy": "2026-07-08T16:36:51.025145Z",
     "iopub.status.idle": "2026-07-08T16:36:52.252524Z",
     "shell.execute_reply": "2026-07-08T16:36:52.251601Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "hovertemplate": "%{y:$,.0f}<extra>Benefits</extra>",
         "marker": {
          "color": "#1baf7a",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Benefits",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          0.0,
          300000.0,
          600000.0
         ]
        },
        {
         "hovertemplate": "%{y:$,.0f}<extra>Incremental cost vs $1.0M/yr baseline</extra>",
         "marker": {
          "color": "#e34948",
          "line": {
           "color": "#fcfcfb",
           "width": 2
          }
         },
         "name": "Incremental cost vs $1.0M/yr baseline",
         "type": "bar",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": [
          -500000.0,
          -500000.0,
          500000.0
         ]
        },
        {
         "hovertemplate": "%{y:$,.0f}<extra>Cumulative net</extra>",
         "line": {
          "color": "#52514e",
          "width": 2
         },
         "marker": {
          "size": 8
         },
         "mode": "lines+markers",
         "name": "Cumulative net",
         "type": "scatter",
         "x": [
          "2026",
          "2027",
          "2028"
         ],
         "y": {
          "bdata": "AAAAAICEHsEAAAAAwFwlwQAAAAAAahhB",
          "dtype": "f8"
         }
        }
       ],
       "layout": {
        "annotations": [
         {
          "align": "left",
          "bgcolor": "#fcfcfb",
          "bordercolor": "#e1e0d9",
          "borderwidth": 1,
          "font": {
           "color": "#52514e",
           "size": 12
          },
          "showarrow": false,
          "text": "3-yr net <b>&#36;400K</b> · NPV@10% <b>&#36;207K</b> · payback <b>32 months (~Aug 2028)</b>",
          "x": 0.01,
          "xref": "paper",
          "y": 0.98,
          "yref": "paper"
         }
        ],
        "barmode": "relative",
        "font": {
         "color": "#0b0b0b",
         "family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
         "size": 12
        },
        "height": 420,
        "hovermode": "x unified",
        "legend": {
         "bgcolor": "rgba(0,0,0,0)",
         "orientation": "h",
         "x": 0,
         "y": 1.0,
         "yanchor": "bottom"
        },
        "margin": {
         "b": 45,
         "l": 60,
         "r": 30,
         "t": 80
        },
        "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>Business case vs doing nothing</b><br><span style='font-size:12px;color:#52514e'>Baseline-relative: net = benefits − (programme cost − baseline)</span>",
         "x": 0,
         "xanchor": "left"
        },
        "xaxis": {
         "showgrid": false,
         "zeroline": false
        },
        "yaxis": {
         "gridcolor": "#e1e0d9",
         "tickformat": "$,.0f",
         "zeroline": false
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = go.Figure()\n",
    "fig.add_trace(bar(X, [ben_by_year[y] for y in YEARS], \"Benefits\", \"#1baf7a\"))\n",
    "fig.add_trace(bar(X, [-inc[y] for y in YEARS],\n",
    "                  f\"Incremental cost vs {money(BASELINE_ANNUAL)}/yr baseline\",\n",
    "                  \"#e34948\"))\n",
    "cum_net = pd.Series([net_by[y] for y in YEARS]).cumsum()\n",
    "fig.add_trace(cum_line(X, cum_net, \"Cumulative net\"))\n",
    "fig.update_layout(barmode=\"relative\")\n",
    "# Several amounts in one annotation → html_money, or MathJax eats the text.\n",
    "fig.add_annotation(\n",
    "    xref=\"paper\", yref=\"paper\", x=0.01, y=0.98, align=\"left\", showarrow=False,\n",
    "    font=dict(size=12, color=INK2), bgcolor=SURFACE, bordercolor=GRID, borderwidth=1,\n",
    "    text=(f\"3-yr net <b>{html_money(kpi['net_3yr'])}</b> · \"\n",
    "          f\"NPV@{DISCOUNT_RATE:.0%} <b>{html_money(kpi['npv'])}</b> · \"\n",
    "          f\"payback <b>{kpi['payback']}</b>\"))\n",
    "tei_layout(fig, \"Business case vs doing nothing\",\n",
    "           \"Baseline-relative: net = benefits − (programme cost − baseline)\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-8",
   "metadata": {},
   "source": [
    "<a id=\"section-3\"></a>\n",
    "\n",
    "## 3 · Verification & assertions\n",
    "\n",
    "Engine pins use **explicit default arguments**, so the gate tests `studylib`, not the\n",
    "current widget state; live-state checks only run when the inputs sit at their defaults.\n",
    "This cell must pass under headless `nbconvert --execute` — it is the study's smoke test.\n",
    "Output renders backstage only."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "cell-9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:52.254794Z",
     "iopub.status.busy": "2026-07-08T16:36:52.254464Z",
     "iopub.status.idle": "2026-07-08T16:36:52.262674Z",
     "shell.execute_reply": "2026-07-08T16:36:52.261927Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All assertions passed.\n",
      "  net 3-yr $400K · payback 32 months (~Aug 2028)\n"
     ]
    }
   ],
   "source": [
    "def _approx(got, want, tol=0.5):\n",
    "    assert abs(got - want) <= tol, f\"got {got:,.2f}, want {want:,.2f}\"\n",
    "\n",
    "\n",
    "# Engine pins — explicit defaults, independent of widget state\n",
    "_approx(anchor(\"platform_annual\"), 500_000)               # signed overlay\n",
    "_approx(ANCHOR_VERBATIM[\"platform_annual\"], 600_000)      # vendor record intact\n",
    "_p = platform_costs_by_year()                             # verbatim rate, 6-mo ramp\n",
    "_approx(_p[2026], 300_000)\n",
    "_approx(_p[2027], 600_000)\n",
    "_b = benefits_by_year()\n",
    "_approx(sum(_b.values()), anchor(\"benefit_3yr\"))\n",
    "_approx(_b[2026], 0)                                      # nothing lands in year 1\n",
    "\n",
    "# Default-flow pins (contracted frame at engine defaults)\n",
    "_pc = platform_costs_by_year(annual=anchor(\"platform_annual\"))\n",
    "_tot = {y: current_costs_by_year()[y] + _pc[y] + services_by_year()[y] for y in YEARS}\n",
    "_, _net = case_flows(_tot, _b)\n",
    "_approx(sum(_net.values()), 400_000)\n",
    "assert payback_label(_net) == \"32 months (~Aug 2028)\"\n",
    "\n",
    "# Live state — only checked when the widgets sit at their defaults\n",
    "_at_default = (PLATFORM_ANNUAL == round(anchor(\"platform_annual\"))\n",
    "               and RAMP_MONTHS == DEFAULT_RAMP_MONTHS)\n",
    "if _at_default:\n",
    "    _approx(kpi[\"net_3yr\"], 400_000)\n",
    "for y in YEARS:\n",
    "    _approx(net_by[y], ben_by_year[y] - (total_by_year[y] - BASELINE_ANNUAL))\n",
    "\n",
    "backstage(\"All assertions passed.\")\n",
    "backstage(f\"  net 3-yr {money(kpi['net_3yr'])} · payback {kpi['payback']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-10",
   "metadata": {},
   "source": [
    "<a id=\"section-4\"></a>\n",
    "\n",
    "## 4 · Data appendix — for the machines\n",
    "\n",
    "Everything above, dumped as markdown tables plus one JSON block of model state, so the\n",
    "exported report is complete LLM input without re-running anything. The dump renders\n",
    "**backstage** (JupyterLab and the exports) and stays hidden in the Mercury app."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "cell-11",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-08T16:36:52.264962Z",
     "iopub.status.busy": "2026-07-08T16:36:52.264712Z",
     "iopub.status.idle": "2026-07-08T16:36:52.284958Z",
     "shell.execute_reply": "2026-07-08T16:36:52.284097Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "#### Cost stack ($)\n",
      "\n",
      "|                                           |      2026 |      2027 |    2028 |      3-yr |\n",
      "|:------------------------------------------|----------:|----------:|--------:|----------:|\n",
      "| Platform (contracted, ramp-adjusted)      |   250,000 |   500,000 | 500,000 | 1,250,000 |\n",
      "| Services (year 1)                         |   250,000 |         0 |       0 |   250,000 |\n",
      "| Existing platform (term-contract run-off) | 1,000,000 | 1,000,000 |       0 | 2,000,000 |\n",
      "\n",
      "#### Business-case flows vs do-nothing baseline ($)\n",
      "\n",
      "|                  |      2026 |      2027 |      2028 |\n",
      "|:-----------------|----------:|----------:|----------:|\n",
      "| programme cost   | 1,500,000 | 1,500,000 |   500,000 |\n",
      "| incremental cost |   500,000 |   500,000 |  -500,000 |\n",
      "| net              |  -500,000 |  -200,000 | 1,100,000 |\n",
      "\n",
      "#### KPIs — vendor frame vs contracted\n",
      "\n",
      "|                       | Vendor frame ($600K/yr)   | Contracted ($500K/yr)   |\n",
      "|:----------------------|:--------------------------|:------------------------|\n",
      "| 3-yr benefits         | $900K                     | $900K                   |\n",
      "| 3-yr incremental cost | $750K                     | $500K                   |\n",
      "| 3-yr net              | $150K                     | $400K                   |\n",
      "| ROI                   | 20%                       | 80%                     |\n",
      "| NPV @ 10%             | $3K                       | $207K                   |\n",
      "| Payback               | 35 months (~Nov 2028)     | 32 months (~Aug 2028)   |\n",
      "\n",
      "#### Model state (JSON)\n",
      "\n",
      "```json\n",
      "{\n",
      "  \"scenario\": \"TEMPLATE \\u2014 replace with the study's one-line scenario\",\n",
      "  \"benefit_by_year\": {\n",
      "    \"2026\": 0,\n",
      "    \"2027\": 300000,\n",
      "    \"2028\": 600000\n",
      "  },\n",
      "  \"cost_by_year\": {\n",
      "    \"2026\": 1500000,\n",
      "    \"2027\": 1500000,\n",
      "    \"2028\": 500000\n",
      "  },\n",
      "  \"net_by_year\": {\n",
      "    \"2026\": -500000,\n",
      "    \"2027\": -200000,\n",
      "    \"2028\": 1100000\n",
      "  },\n",
      "  \"kpis\": {\n",
      "    \"benefits_3yr\": 900000.0,\n",
      "    \"incremental_cost_3yr\": 500000.0,\n",
      "    \"net_3yr\": 400000.0,\n",
      "    \"roi\": 0.8,\n",
      "    \"npv\": 206611.5702,\n",
      "    \"discount_rate\": 0.1,\n",
      "    \"payback\": \"32 months (~Aug 2028)\"\n",
      "  },\n",
      "  \"kpis_vendor_frame\": {\n",
      "    \"benefits_3yr\": 900000.0,\n",
      "    \"incremental_cost_3yr\": 750000.0,\n",
      "    \"net_3yr\": 150000.0,\n",
      "    \"roi\": 0.2,\n",
      "    \"npv\": 3380.9166,\n",
      "    \"discount_rate\": 0.1,\n",
      "    \"payback\": \"35 months (~Nov 2028)\"\n",
      "  },\n",
      "  \"assumptions\": {\n",
      "    \"platform_annual\": 500000,\n",
      "    \"vendor_platform_annual\": 600000,\n",
      "    \"ramp_months\": 6,\n",
      "    \"discount_rate\": 0.1,\n",
      "    \"baseline_annual\": 1000000\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(\"Cost stack ($)\", study_costs)\n",
    "_section(\"Business-case flows vs do-nothing baseline ($)\",\n",
    "         pd.DataFrame({\"programme cost\": total_by_year,\n",
    "                       \"incremental cost\": inc, \"net\": net_by}).T[YEARS])\n",
    "_section(\"KPIs — vendor frame vs contracted\", kpis_fmt)\n",
    "\n",
    "backstage(\"\\n#### Model state (JSON)\\n\")\n",
    "backstage(\"```json\")\n",
    "backstage(_json.dumps({\n",
    "    \"scenario\": \"TEMPLATE — replace with the study's one-line scenario\",\n",
    "    \"benefit_by_year\": {str(y): round(ben_by_year[y]) for y in YEARS},\n",
    "    \"cost_by_year\": {str(y): round(total_by_year[y]) for y in YEARS},\n",
    "    \"net_by_year\": {str(y): round(net_by[y]) for y in YEARS},\n",
    "    \"kpis\": {k: (round(v, 4) if isinstance(v, float) else v)\n",
    "             for k, v in kpi.items()},\n",
    "    \"kpis_vendor_frame\": {k: (round(v, 4) if isinstance(v, float) else v)\n",
    "                          for k, v in kpi_v.items()},\n",
    "    \"assumptions\": {\n",
    "        \"platform_annual\": round(PLATFORM_ANNUAL),\n",
    "        \"vendor_platform_annual\": ANCHOR_VERBATIM[\"platform_annual\"],\n",
    "        \"ramp_months\": RAMP_MONTHS,\n",
    "        \"discount_rate\": DISCOUNT_RATE,\n",
    "        \"baseline_annual\": round(BASELINE_ANNUAL),\n",
    "    },\n",
    "}, indent=2))\n",
    "backstage(\"```\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {
     "2cd79c44365840a4af77e279bf99a67f": {
      "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
      }
     },
     "56c2f21d17cf40009b6d4bcee7bcdd9a": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.select.SelectWidget",
       "_css": "\n    .mljar-select-container {\n      display: flex;\n      flex-direction: column;\n      font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n      font-size: 14px;\n      color: #0f172a;\n      margin-bottom: 8px;\n      padding-left: 4px;\n      padding-right: 4px;\n    }\n\n    .mljar-select-label {\n      margin-bottom: 4px;\n      font-weight: 600;\n    }\n\n    .mljar-select-control {\n      position: relative;\n      display: flex;\n      align-items: center;\n      cursor: default;\n    }\n\n    .mljar-select-widget-input {\n      width: 100%;\n      min-height: 40px;\n      padding: 9px 36px 9px 10px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-sizing: border-box;\n      line-height: 1.4;\n      transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n      appearance: none !important;\n      background-color: #ffffff !important;\n      color: #0f172a !important;\n      cursor: default;\n    }\n\n    .mljar-select-widget-input:focus {\n      outline: none;\n      border-color: #007bff;\n      border-width: 2px;\n      box-shadow: none;\n      cursor: text;\n    }\n\n    .mljar-select-caret {\n      position: absolute;\n      right: 12px;\n      top: 50%;\n      width: 8px;\n      height: 8px;\n      border-right: 1.5px solid #0f172a;\n      border-bottom: 1.5px solid #0f172a;\n      transform: translateY(-65%) rotate(45deg);\n      pointer-events: none;\n      opacity: 0.5;\n      transition: transform 0.18s ease, opacity 0.18s ease;\n    }\n\n    .mljar-select-container.is-open .mljar-select-caret {\n      opacity: 1;\n      transform: translateY(-35%) rotate(225deg);\n    }\n\n    .mljar-select-dropdown {\n      display: none;\n      margin-top: 6px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n      overflow: hidden;\n    }\n\n    .mljar-select-list {\n      max-height: 260px;\n      overflow-y: auto;\n    }\n\n    .mljar-select-option {\n      display: block;\n      width: 100%;\n      padding: 9px 10px;\n      border: 0;\n      background: transparent;\n      color: #0f172a;\n      text-align: left;\n      cursor: pointer;\n      font: inherit;\n      transition: background-color 0.14s ease, color 0.14s ease;\n    }\n\n    .mljar-select-option:hover {\n      background: #f3f3f4;\n    }\n\n    .mljar-select-option:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-select-option.is-selected {\n      background: #e6f2ff;\n      color: #007bff;\n      font-weight: 600;\n    }\n\n    .mljar-select-option.is-selected:hover,\n    .mljar-select-option.is-selected:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-select-empty {\n      display: none;\n      padding: 10px;\n      color: #616673;\n      font-size: 0.95em;\n    }\n\n    .mljar-select-widget-input:disabled {\n      background: #f5f5f5;\n      color: #888;\n      cursor: not-allowed;\n    }\n\n    .mljar-select-control.is-disabled .mljar-select-caret {\n      opacity: 0.45;\n    }\n    ",
       "_dom_classes": [],
       "_esm": "\n    function render({ model, el }) {\n      const normalize = value => String(value ?? \"\").toLowerCase().trim();\n      const getChoices = () =>\n        Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n      const isDisabled = () => !!model.get(\"disabled\");\n      const isHidden = () => !!model.get(\"hidden\");\n\n      const container = document.createElement(\"div\");\n      container.classList.add(\"mljar-select-container\");\n\n      if (model.get(\"label\")) {\n        const topLabel = document.createElement(\"div\");\n        topLabel.classList.add(\"mljar-select-label\");\n        topLabel.innerHTML = model.get(\"label\");\n        container.appendChild(topLabel);\n      }\n\n      const control = document.createElement(\"div\");\n      control.classList.add(\"mljar-select-control\");\n\n      const input = document.createElement(\"input\");\n      input.type = \"text\";\n      input.classList.add(\"mljar-select-widget-input\");\n      input.autocomplete = \"off\";\n      input.spellcheck = false;\n\n      const caret = document.createElement(\"div\");\n      caret.classList.add(\"mljar-select-caret\");\n\n      control.appendChild(input);\n      control.appendChild(caret);\n\n      const dropdown = document.createElement(\"div\");\n      dropdown.classList.add(\"mljar-select-dropdown\");\n\n      const list = document.createElement(\"div\");\n      list.classList.add(\"mljar-select-list\");\n\n      const emptyState = document.createElement(\"div\");\n      emptyState.classList.add(\"mljar-select-empty\");\n      emptyState.textContent = \"No matches\";\n\n      dropdown.appendChild(list);\n      dropdown.appendChild(emptyState);\n\n      container.appendChild(control);\n      container.appendChild(dropdown);\n      el.appendChild(container);\n\n      let isOpen = false;\n      let filteredChoices = [];\n      let lastCommittedValue = \"\";\n      let isEditing = false;\n\n      const setOpen = next => {\n        if (isDisabled()) {\n          isOpen = false;\n        } else {\n          isOpen = !!next;\n        }\n        container.classList.toggle(\"is-open\", isOpen);\n        dropdown.style.display = isOpen ? \"block\" : \"none\";\n      };\n\n      const updateDisabledState = () => {\n        const disabled = isDisabled();\n        input.disabled = disabled;\n        control.classList.toggle(\"is-disabled\", disabled);\n      };\n\n      const updateHiddenState = () => {\n        container.style.display = isHidden() ? \"none\" : \"\";\n      };\n\n      const syncInputWithValue = () => {\n        const value = model.get(\"value\") || \"\";\n        lastCommittedValue = value;\n        if (!isEditing) {\n          input.value = value;\n        }\n      };\n\n      const filterChoices = query => {\n        const normalizedQuery = normalize(query);\n        const allChoices = getChoices();\n        if (!normalizedQuery) {\n          return allChoices;\n        }\n        return allChoices.filter(choice =>\n          normalize(choice).includes(normalizedQuery)\n        );\n      };\n\n      const renderList = () => {\n        list.innerHTML = \"\";\n        filteredChoices.forEach(choice => {\n          const option = document.createElement(\"button\");\n          option.type = \"button\";\n          option.classList.add(\"mljar-select-option\");\n          if (choice === model.get(\"value\")) {\n            option.classList.add(\"is-selected\");\n          }\n          option.textContent = choice;\n          option.addEventListener(\"mousedown\", event => {\n            event.preventDefault();\n            event.stopPropagation();\n            model.set(\"value\", choice);\n            model.save_changes();\n            isEditing = false;\n            syncInputWithValue();\n            renderList();\n            setOpen(false);\n          });\n          list.appendChild(option);\n        });\n\n        const hasMatches = filteredChoices.length > 0;\n        list.style.display = hasMatches ? \"block\" : \"none\";\n        emptyState.style.display = hasMatches ? \"none\" : \"block\";\n      };\n\n      const refreshList = () => {\n        filteredChoices = filterChoices(input.value);\n        renderList();\n      };\n\n      const openWithCurrentQuery = () => {\n        isEditing = true;\n        input.value = \"\";\n        refreshList();\n        setOpen(true);\n      };\n\n      control.addEventListener(\"click\", event => {\n        event.stopPropagation();\n        if (isDisabled()) {\n          return;\n        }\n        openWithCurrentQuery();\n        input.focus();\n      });\n\n      input.addEventListener(\"input\", () => {\n        if (isDisabled()) {\n          return;\n        }\n        refreshList();\n        setOpen(true);\n      });\n\n      input.addEventListener(\"focus\", () => {\n        if (isDisabled()) {\n          return;\n        }\n        openWithCurrentQuery();\n      });\n\n      input.addEventListener(\"blur\", () => {\n        isEditing = false;\n        input.value = lastCommittedValue;\n      });\n\n      const handleDocumentClick = event => {\n        if (!container.contains(event.target)) {\n          isEditing = false;\n          setOpen(false);\n          input.value = lastCommittedValue;\n        }\n      };\n\n      document.addEventListener(\"click\", handleDocumentClick);\n\n      model.on(\"change:value\", () => {\n        syncInputWithValue();\n        refreshList();\n      });\n\n      model.on(\"change:choices\", () => {\n        const choices = getChoices();\n        if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n          model.set(\"value\", choices[0]);\n          model.save_changes();\n          return;\n        }\n        syncInputWithValue();\n        refreshList();\n      });\n\n      model.on(\"change:disabled\", () => {\n        updateDisabledState();\n        if (isDisabled()) {\n          isEditing = false;\n          setOpen(false);\n        }\n      });\n\n      model.on(\"change:hidden\", () => {\n        updateHiddenState();\n      });\n\n      updateDisabledState();\n      updateHiddenState();\n      syncInputWithValue();\n      refreshList();\n      setOpen(false);\n\n      return () => {\n        document.removeEventListener(\"click\", handleDocumentClick);\n      };\n    }\n    export default { render };\n    ",
       "_model_module": "anywidget",
       "_model_module_version": "~0.11.*",
       "_model_name": "AnyModel",
       "_view_count": null,
       "_view_module": "anywidget",
       "_view_module_version": "~0.11.*",
       "_view_name": "AnyView",
       "cell_id": "",
       "choices": [
        "10% (vendor)",
        "8% (treasury)"
       ],
       "disabled": false,
       "hidden": false,
       "label": "NPV discount rate",
       "layout": "IPY_MODEL_2cd79c44365840a4af77e279bf99a67f",
       "layout_path": null,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": "10% (vendor)"
      }
     },
     "60e56739936e4f2ab8ce7a25fe31c092": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.number.NumberInputWidget",
       "_css": "\n    .mljar-number-container {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n      font-size: 14px;\n      color: #0f172a;\n      margin-bottom: 8px;\n      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      margin-bottom: 4px;\n      font-weight: 600;\n    }\n\n    .mljar-number-field-row {\n      display: flex;\n      align-items: stretch;\n      width: 100%;\n      min-height: 40px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-sizing: border-box;\n      overflow: hidden;\n    }\n\n    .mljar-number-input {\n      flex: 1 1 auto;\n      min-width: 0;\n      min-height: 100%;\n      padding: 7px 10px;\n      border: 0;\n      border-radius: 0;\n      background: #ffffff;\n      box-sizing: border-box;\n      background-color: #ffffff !important;\n      color: #0f172a !important;\n      font: inherit;\n      line-height: 1.2;\n      -moz-appearance: textfield;\n    }\n\n    .mljar-number-input::-webkit-outer-spin-button,\n    .mljar-number-input::-webkit-inner-spin-button {\n      -webkit-appearance: none;\n      margin: 0;\n    }\n\n    .mljar-number-input:disabled {\n      background: #f5f5f5;\n      color: #888;\n      cursor: not-allowed;\n    }\n\n    .mljar-number-input:focus {\n      outline: none;\n    }\n\n    .mljar-number-field-row:focus-within {\n      border-color: #007bff;\n      border-width: 2px;\n      box-shadow: none;\n    }\n\n    .mljar-number-field-row:focus-within .mljar-number-controls {\n      border-left-color: #007bff;\n    }\n\n    .mljar-number-controls {\n      display: flex;\n      align-items: stretch;\n      flex: 0 0 auto;\n      border-left: 1px solid #cfd1d5;\n      background: #f1f1f2;\n    }\n\n    .mljar-number-step-btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      width: 38px;\n      min-width: 38px;\n      min-height: 100%;\n      border: 0;\n      border-radius: 0;\n      background: transparent;\n      color: #0f172a;\n      font: inherit;\n      font-size: 18px;\n      font-weight: 700;\n      line-height: 1;\n      cursor: pointer;\n      padding: 0;\n      user-select: none;\n      -webkit-user-select: none;\n      touch-action: manipulation;\n      transition: background-color 0.14s ease, color 0.14s ease;\n    }\n\n    .mljar-number-step-up {\n      border-left: 1px solid #cfd1d5;\n    }\n\n    .mljar-number-step-btn:hover {\n      background: #f3f3f4;\n    }\n\n    .mljar-number-step-btn:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:focus-visible {\n      outline: none;\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:disabled {\n      background: #f5f5f5;\n      color: #aaa;\n      cursor: not-allowed;\n    }\n\n    @media (max-width: 768px) {\n      .mljar-number-field-row {\n        min-height: 44px;\n      }\n\n      .mljar-number-input {\n        min-height: 44px;\n        padding: 8px 12px;\n      }\n\n      .mljar-number-step-btn {\n        font-size: 19px;\n        width: 44px;\n        min-width: 44px;\n      }\n    }\n    ",
       "_dom_classes": [],
       "_esm": "\n    function render({ model, el }) {\n      const container = document.createElement(\"div\");\n      container.classList.add(\"mljar-number-container\");\n\n      const topLabel = document.createElement(\"div\");\n      topLabel.classList.add(\"mljar-number-label\");\n\n      const fieldRow = document.createElement(\"div\");\n      fieldRow.classList.add(\"mljar-number-field-row\");\n\n      const input = document.createElement(\"input\");\n      input.type = \"number\";\n      input.classList.add(\"mljar-number-input\");\n\n      const decrementBtn = document.createElement(\"button\");\n      decrementBtn.type = \"button\";\n      decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n      decrementBtn.textContent = \"-\";\n      decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n      const incrementBtn = document.createElement(\"button\");\n      incrementBtn.type = \"button\";\n      incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n      incrementBtn.textContent = \"+\";\n      incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n      const controls = document.createElement(\"div\");\n      controls.classList.add(\"mljar-number-controls\");\n\n      controls.appendChild(decrementBtn);\n      controls.appendChild(incrementBtn);\n      fieldRow.appendChild(input);\n      fieldRow.appendChild(controls);\n\n      container.appendChild(topLabel);\n      container.appendChild(fieldRow);\n      el.appendChild(container);\n\n      function clamp(val, min, max) {\n        if (Number.isFinite(min) && val < min) return min;\n        if (Number.isFinite(max) && val > max) return max;\n        return val;\n      }\n\n      function normalizeStep(step) {\n        return Number.isFinite(step) && step > 0 ? step : 1;\n      }\n\n      function snapToStep(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = Math.round((value - base) / safeStep);\n        const snapped = base + steps * safeStep;\n        const precision = Math.max(\n          0,\n          (String(safeStep).split(\".\")[1] || \"\").length\n        );\n\n        return Number(snapped.toFixed(precision + 2));\n      }\n\n      function isOnStepGrid(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = (value - base) / safeStep;\n        const nearest = Math.round(steps);\n        const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n        return Math.abs(steps - nearest) <= epsilon;\n      }\n\n      function getCurrentBounds() {\n        return {\n          min: Number(model.get(\"min\")),\n          max: Number(model.get(\"max\")),\n        };\n      }\n\n      function isTransientDraft(raw) {\n        return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n      }\n\n      let isEditing = false;\n      const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n      function clearPendingDraftCommit() {\n        if (debounceTimer) clearTimeout(debounceTimer);\n        pendingDraftValue = null;\n      }\n\n      function parseDraftValue(rawValue) {\n        const raw = String(rawValue).trim();\n        if (isTransientDraft(raw)) {\n          return { kind: \"transient\" };\n        }\n\n        const value = Number(raw);\n        if (!Number.isFinite(value)) {\n          return { kind: \"invalid\" };\n        }\n\n        return { kind: \"number\", value };\n      }\n\n      function commitValue(nextValue, saveNow = true) {\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        const parsed = parseDraftValue(nextValue);\n        if (parsed.kind !== \"number\") {\n          syncFromModel();\n          return;\n        }\n\n        let v = parsed.value;\n        v = clamp(v, min, max);\n        v = snapToStep(v, min, step);\n        v = clamp(v, min, max);\n        input.value = String(v);\n        model.set(\"value\", v);\n\n        if (saveNow) {\n          model.save_changes();\n        }\n      }\n\n      function syncFromModel() {\n        topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n        const min = Number(model.get(\"min\"));\n        const max = Number(model.get(\"max\"));\n        const step = Number(model.get(\"step\"));\n\n        if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n        if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n        if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n        const v = Number(model.get(\"value\"));\n        if (!isEditing) {\n          input.value = Number.isFinite(v) ? String(v) : \"\";\n        }\n\n        const disabled = !!model.get(\"disabled\");\n        input.disabled = disabled;\n        incrementBtn.disabled = disabled;\n        decrementBtn.disabled = disabled;\n\n        const hidden = !!model.get(\"hidden\");\n        container.style.display = hidden ? \"none\" : \"flex\";\n      }\n\n      let debounceTimer = null;\n      let pendingDraftValue = null;\n      input.addEventListener(\"focus\", () => {\n        isEditing = true;\n      });\n\n      input.addEventListener(\"input\", () => {\n        if (model.get(\"disabled\")) return;\n\n        const parsed = parseDraftValue(input.value);\n        if (parsed.kind !== \"number\") {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        const v = parsed.value;\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        if (Number.isFinite(min) && v < min) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (Number.isFinite(max) && v > max) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (!isOnStepGrid(v, min, step)) {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        pendingDraftValue = v;\n        if (debounceTimer) clearTimeout(debounceTimer);\n        debounceTimer = setTimeout(() => {\n          if (pendingDraftValue === null) return;\n          model.set(\"value\", pendingDraftValue);\n          model.save_changes();\n          pendingDraftValue = null;\n        }, INPUT_COMMIT_DEBOUNCE_MS);\n      });\n\n      input.addEventListener(\"blur\", () => {\n        isEditing = false;\n        clearPendingDraftCommit();\n        commitValue(input.value, true);\n      });\n\n      input.addEventListener(\"keydown\", event => {\n        if (event.key === \"Enter\") {\n          event.preventDefault();\n          input.blur();\n        }\n      });\n\n      incrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base + step, min, step));\n      });\n\n      decrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base - step, min, step));\n      });\n\n      model.on(\"change:value\", syncFromModel);\n      model.on(\"change:min\", syncFromModel);\n      model.on(\"change:max\", syncFromModel);\n      model.on(\"change:step\", syncFromModel);\n      model.on(\"change:label\", syncFromModel);\n      model.on(\"change:disabled\", syncFromModel);\n      model.on(\"change:hidden\", syncFromModel);\n\n      syncFromModel();\n\n      // ---- read cell id (no DOM modifications) ----\n      /*\n      const ID_ATTR = \"data-cell-id\";\n      const hostWithId = el.closest(`[${ID_ATTR}]`);\n      const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n      if (cellId) {\n        model.set(\"cell_id\", cellId);\n        model.save_changes();\n        model.send({ type: \"cell_id_detected\", value: cellId });\n      } else {\n        const mo = new MutationObserver(() => {\n          const host = el.closest(`[${ID_ATTR}]`);\n          const newId = host?.getAttribute(ID_ATTR);\n          if (newId) {\n            model.set(\"cell_id\", newId);\n            model.save_changes();\n            model.send({ type: \"cell_id_detected\", value: newId });\n            mo.disconnect();\n          }\n        });\n        mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n      }*/\n    }\n    export default { render };\n    ",
       "_model_module": "anywidget",
       "_model_module_version": "~0.11.*",
       "_model_name": "AnyModel",
       "_view_count": null,
       "_view_module": "anywidget",
       "_view_module_version": "~0.11.*",
       "_view_name": "AnyView",
       "cell_id": "",
       "disabled": false,
       "hidden": false,
       "label": "Ramp — billing-free months",
       "layout": "IPY_MODEL_e41731dbca994eeeb59b0b6edcfc9276",
       "layout_path": null,
       "max": 24.0,
       "min": 0.0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 3.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 6.0
      }
     },
     "924aaf1b9a7244388d4fdc29e0f544e2": {
      "model_module": "anywidget",
      "model_module_version": "~0.11.*",
      "model_name": "AnyModel",
      "state": {
       "_anywidget_id": "mercury.number.NumberInputWidget",
       "_css": "\n    .mljar-number-container {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n      font-size: 14px;\n      color: #0f172a;\n      margin-bottom: 8px;\n      padding-left: 4px;\n      padding-right: 4px;\n      box-sizing: border-box;\n    }\n\n    .mljar-number-label {\n      margin-bottom: 4px;\n      font-weight: 600;\n    }\n\n    .mljar-number-field-row {\n      display: flex;\n      align-items: stretch;\n      width: 100%;\n      min-height: 40px;\n      border: 1px solid #cfd1d5;\n      border-radius: 6px;\n      background: #ffffff;\n      box-sizing: border-box;\n      overflow: hidden;\n    }\n\n    .mljar-number-input {\n      flex: 1 1 auto;\n      min-width: 0;\n      min-height: 100%;\n      padding: 7px 10px;\n      border: 0;\n      border-radius: 0;\n      background: #ffffff;\n      box-sizing: border-box;\n      background-color: #ffffff !important;\n      color: #0f172a !important;\n      font: inherit;\n      line-height: 1.2;\n      -moz-appearance: textfield;\n    }\n\n    .mljar-number-input::-webkit-outer-spin-button,\n    .mljar-number-input::-webkit-inner-spin-button {\n      -webkit-appearance: none;\n      margin: 0;\n    }\n\n    .mljar-number-input:disabled {\n      background: #f5f5f5;\n      color: #888;\n      cursor: not-allowed;\n    }\n\n    .mljar-number-input:focus {\n      outline: none;\n    }\n\n    .mljar-number-field-row:focus-within {\n      border-color: #007bff;\n      border-width: 2px;\n      box-shadow: none;\n    }\n\n    .mljar-number-field-row:focus-within .mljar-number-controls {\n      border-left-color: #007bff;\n    }\n\n    .mljar-number-controls {\n      display: flex;\n      align-items: stretch;\n      flex: 0 0 auto;\n      border-left: 1px solid #cfd1d5;\n      background: #f1f1f2;\n    }\n\n    .mljar-number-step-btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      width: 38px;\n      min-width: 38px;\n      min-height: 100%;\n      border: 0;\n      border-radius: 0;\n      background: transparent;\n      color: #0f172a;\n      font: inherit;\n      font-size: 18px;\n      font-weight: 700;\n      line-height: 1;\n      cursor: pointer;\n      padding: 0;\n      user-select: none;\n      -webkit-user-select: none;\n      touch-action: manipulation;\n      transition: background-color 0.14s ease, color 0.14s ease;\n    }\n\n    .mljar-number-step-up {\n      border-left: 1px solid #cfd1d5;\n    }\n\n    .mljar-number-step-btn:hover {\n      background: #f3f3f4;\n    }\n\n    .mljar-number-step-btn:active {\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:focus-visible {\n      outline: none;\n      background: #e6f2ff;\n      color: #007bff;\n    }\n\n    .mljar-number-step-btn:disabled {\n      background: #f5f5f5;\n      color: #aaa;\n      cursor: not-allowed;\n    }\n\n    @media (max-width: 768px) {\n      .mljar-number-field-row {\n        min-height: 44px;\n      }\n\n      .mljar-number-input {\n        min-height: 44px;\n        padding: 8px 12px;\n      }\n\n      .mljar-number-step-btn {\n        font-size: 19px;\n        width: 44px;\n        min-width: 44px;\n      }\n    }\n    ",
       "_dom_classes": [],
       "_esm": "\n    function render({ model, el }) {\n      const container = document.createElement(\"div\");\n      container.classList.add(\"mljar-number-container\");\n\n      const topLabel = document.createElement(\"div\");\n      topLabel.classList.add(\"mljar-number-label\");\n\n      const fieldRow = document.createElement(\"div\");\n      fieldRow.classList.add(\"mljar-number-field-row\");\n\n      const input = document.createElement(\"input\");\n      input.type = \"number\";\n      input.classList.add(\"mljar-number-input\");\n\n      const decrementBtn = document.createElement(\"button\");\n      decrementBtn.type = \"button\";\n      decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n      decrementBtn.textContent = \"-\";\n      decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n      const incrementBtn = document.createElement(\"button\");\n      incrementBtn.type = \"button\";\n      incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n      incrementBtn.textContent = \"+\";\n      incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n      const controls = document.createElement(\"div\");\n      controls.classList.add(\"mljar-number-controls\");\n\n      controls.appendChild(decrementBtn);\n      controls.appendChild(incrementBtn);\n      fieldRow.appendChild(input);\n      fieldRow.appendChild(controls);\n\n      container.appendChild(topLabel);\n      container.appendChild(fieldRow);\n      el.appendChild(container);\n\n      function clamp(val, min, max) {\n        if (Number.isFinite(min) && val < min) return min;\n        if (Number.isFinite(max) && val > max) return max;\n        return val;\n      }\n\n      function normalizeStep(step) {\n        return Number.isFinite(step) && step > 0 ? step : 1;\n      }\n\n      function snapToStep(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = Math.round((value - base) / safeStep);\n        const snapped = base + steps * safeStep;\n        const precision = Math.max(\n          0,\n          (String(safeStep).split(\".\")[1] || \"\").length\n        );\n\n        return Number(snapped.toFixed(precision + 2));\n      }\n\n      function isOnStepGrid(value, min, step) {\n        const safeStep = normalizeStep(step);\n        const base = Number.isFinite(min) ? min : 0;\n        const steps = (value - base) / safeStep;\n        const nearest = Math.round(steps);\n        const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n        return Math.abs(steps - nearest) <= epsilon;\n      }\n\n      function getCurrentBounds() {\n        return {\n          min: Number(model.get(\"min\")),\n          max: Number(model.get(\"max\")),\n        };\n      }\n\n      function isTransientDraft(raw) {\n        return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n      }\n\n      let isEditing = false;\n      const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n      function clearPendingDraftCommit() {\n        if (debounceTimer) clearTimeout(debounceTimer);\n        pendingDraftValue = null;\n      }\n\n      function parseDraftValue(rawValue) {\n        const raw = String(rawValue).trim();\n        if (isTransientDraft(raw)) {\n          return { kind: \"transient\" };\n        }\n\n        const value = Number(raw);\n        if (!Number.isFinite(value)) {\n          return { kind: \"invalid\" };\n        }\n\n        return { kind: \"number\", value };\n      }\n\n      function commitValue(nextValue, saveNow = true) {\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        const parsed = parseDraftValue(nextValue);\n        if (parsed.kind !== \"number\") {\n          syncFromModel();\n          return;\n        }\n\n        let v = parsed.value;\n        v = clamp(v, min, max);\n        v = snapToStep(v, min, step);\n        v = clamp(v, min, max);\n        input.value = String(v);\n        model.set(\"value\", v);\n\n        if (saveNow) {\n          model.save_changes();\n        }\n      }\n\n      function syncFromModel() {\n        topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n        const min = Number(model.get(\"min\"));\n        const max = Number(model.get(\"max\"));\n        const step = Number(model.get(\"step\"));\n\n        if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n        if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n        if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n        const v = Number(model.get(\"value\"));\n        if (!isEditing) {\n          input.value = Number.isFinite(v) ? String(v) : \"\";\n        }\n\n        const disabled = !!model.get(\"disabled\");\n        input.disabled = disabled;\n        incrementBtn.disabled = disabled;\n        decrementBtn.disabled = disabled;\n\n        const hidden = !!model.get(\"hidden\");\n        container.style.display = hidden ? \"none\" : \"flex\";\n      }\n\n      let debounceTimer = null;\n      let pendingDraftValue = null;\n      input.addEventListener(\"focus\", () => {\n        isEditing = true;\n      });\n\n      input.addEventListener(\"input\", () => {\n        if (model.get(\"disabled\")) return;\n\n        const parsed = parseDraftValue(input.value);\n        if (parsed.kind !== \"number\") {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        const v = parsed.value;\n        const { min, max } = getCurrentBounds();\n        const step = Number(model.get(\"step\"));\n        if (Number.isFinite(min) && v < min) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (Number.isFinite(max) && v > max) {\n          clearPendingDraftCommit();\n          return;\n        }\n        if (!isOnStepGrid(v, min, step)) {\n          clearPendingDraftCommit();\n          return;\n        }\n\n        pendingDraftValue = v;\n        if (debounceTimer) clearTimeout(debounceTimer);\n        debounceTimer = setTimeout(() => {\n          if (pendingDraftValue === null) return;\n          model.set(\"value\", pendingDraftValue);\n          model.save_changes();\n          pendingDraftValue = null;\n        }, INPUT_COMMIT_DEBOUNCE_MS);\n      });\n\n      input.addEventListener(\"blur\", () => {\n        isEditing = false;\n        clearPendingDraftCommit();\n        commitValue(input.value, true);\n      });\n\n      input.addEventListener(\"keydown\", event => {\n        if (event.key === \"Enter\") {\n          event.preventDefault();\n          input.blur();\n        }\n      });\n\n      incrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base + step, min, step));\n      });\n\n      decrementBtn.addEventListener(\"click\", () => {\n        if (model.get(\"disabled\")) return;\n        const current = Number(model.get(\"value\"));\n        const step = normalizeStep(Number(model.get(\"step\")));\n        const min = Number(model.get(\"min\"));\n        const base = Number.isFinite(current) ? current : 0;\n        commitValue(snapToStep(base - step, min, step));\n      });\n\n      model.on(\"change:value\", syncFromModel);\n      model.on(\"change:min\", syncFromModel);\n      model.on(\"change:max\", syncFromModel);\n      model.on(\"change:step\", syncFromModel);\n      model.on(\"change:label\", syncFromModel);\n      model.on(\"change:disabled\", syncFromModel);\n      model.on(\"change:hidden\", syncFromModel);\n\n      syncFromModel();\n\n      // ---- read cell id (no DOM modifications) ----\n      /*\n      const ID_ATTR = \"data-cell-id\";\n      const hostWithId = el.closest(`[${ID_ATTR}]`);\n      const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n      if (cellId) {\n        model.set(\"cell_id\", cellId);\n        model.save_changes();\n        model.send({ type: \"cell_id_detected\", value: cellId });\n      } else {\n        const mo = new MutationObserver(() => {\n          const host = el.closest(`[${ID_ATTR}]`);\n          const newId = host?.getAttribute(ID_ATTR);\n          if (newId) {\n            model.set(\"cell_id\", newId);\n            model.save_changes();\n            model.send({ type: \"cell_id_detected\", value: newId });\n            mo.disconnect();\n          }\n        });\n        mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n      }*/\n    }\n    export default { render };\n    ",
       "_model_module": "anywidget",
       "_model_module_version": "~0.11.*",
       "_model_name": "AnyModel",
       "_view_count": null,
       "_view_module": "anywidget",
       "_view_module_version": "~0.11.*",
       "_view_name": "AnyView",
       "cell_id": "",
       "disabled": false,
       "hidden": false,
       "label": "Platform run-rate ($/yr) — contracted",
       "layout": "IPY_MODEL_dab8383f122d4e85b850290ec384a030",
       "layout_path": null,
       "max": 10000000.0,
       "min": 0.0,
       "position": "sidebar",
       "render_slot_id": null,
       "source_cell_id": null,
       "step": 25000.0,
       "tabbable": null,
       "tooltip": null,
       "url_key": "",
       "value": 500000.0
      }
     },
     "dab8383f122d4e85b850290ec384a030": {
      "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
      }
     },
     "e41731dbca994eeeb59b0b6edcfc9276": {
      "model_module": "@jupyter-widgets/base",
      "model_module_version": "2.0.0",
      "model_name": "LayoutModel",
      "state": {
       "_model_module": "@jupyter-widgets/base",
       "_model_module_version": "2.0.0",
       "_model_name": "LayoutModel",
       "_view_count": null,
       "_view_module": "@jupyter-widgets/base",
       "_view_module_version": "2.0.0",
       "_view_name": "LayoutView",
       "align_content": null,
       "align_items": null,
       "align_self": null,
       "border_bottom": null,
       "border_left": null,
       "border_right": null,
       "border_top": null,
       "bottom": null,
       "display": null,
       "flex": null,
       "flex_flow": null,
       "grid_area": null,
       "grid_auto_columns": null,
       "grid_auto_flow": null,
       "grid_auto_rows": null,
       "grid_column": null,
       "grid_gap": null,
       "grid_row": null,
       "grid_template_areas": null,
       "grid_template_columns": null,
       "grid_template_rows": null,
       "height": null,
       "justify_content": null,
       "justify_items": null,
       "left": null,
       "margin": null,
       "max_height": null,
       "max_width": null,
       "min_height": null,
       "min_width": null,
       "object_fit": null,
       "object_position": null,
       "order": null,
       "overflow": null,
       "padding": null,
       "right": null,
       "top": null,
       "visibility": null,
       "width": null
      }
     }
    },
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
