我已走完项目 onboarding 链路（L-core 四份 + mockup/code 任务命中的 L-ref），并在归档里查到**这个产品自己做过同一个模块**——先把关键前置摊开，再给产物。

## 一、任务定位（起手声明）

| 项 | 判定 |
|---|---|
| Task Entry Mode | **US-2** Greenfield 单页（`code-conventions.md` §Task Entry Modes） |
| R7 场景 | **场景 3.2**（greenfield 单页 dashboard，视觉真源 = TVU library）。⚠️ 措辞里"已有产品 + 用 TVU"本该触发 R7 §起手 disambiguation 的 ask，但**本仓不存在 TVU SaaS Dashboard 的 local 代码层可复用**（element 级决策全部落到 TVU），且你显式说"用 TVU 设计系统" ⇒ 两种归类的产物一致，故不阻塞。若那个产品实际有 local 组件层，它是 **2.1**，local 优先（R6/M21）。 |
| Path | **Path B（代码）**，不是 Figma 写操作 ⇒ 走 `code-conventions.md`，Mockup Write Gate 的 Figma 写前置不触发 |

**⛔ 两条本会话结构上做不到的，如实登记（不是"已通过"）**：M49/M50 要求的 `pnpm design:kickoff --check` **没跑**——量具停用了 Bash/Write；下面的合同内容以回复正文形态给出，等价物、非机检。同理 `audit:no-hardcoded-design-tokens` / `audit:consumer-code`（R14/R19/R9 probe）**均未运行**。

## 二、查到的先例（第 11 项"这个产品自己有没有做过"= ✅ covered）

`docs/_archive/retrospection/2026-05-11-saas-dashboard-2-option-pm-review.md` —— 同一个 feature，同一个产品（Usage Service / `TVU-SaaS-Dashboard-20230619`）。我按它的既有决策做，**没有重新发明**：

- 图表主色 = TVU brand 绿（那轮照抄参考图的紫色被判违 M14/M21，已纠正）
- 定位 = **business health / profitability indicator，不是 technical monitoring graph**
- 货币 **`$`** 不是 `¢`（user 当场纠正过）
- 数据源只有两个：云厂商账单（`vendor_cost` + `tokens_count`）、客户付款（`customer_payment`）
- 那轮做过的**数据可行性 audit** 我直接继承：单位成本 / MoM delta / Saved / Revenue per Token / Gross Margin / Volume 全部可算；**Driver attribution（成本为什么动）不可算 ❌**、Forecast ⚠️ defer —— 所以页面里**没有**这两样，说明面板里如实写了"不包含什么"

⚠️ **一处 scope 冲突，先说再做**：那轮的 PRD 原话是 *"first phase we only need the graph section, the KPI cards at the top are not needed yet"*，而你这次第 1 条明确要顶部概览区。以你当前的要求为准（视为 phase 2 放行），做了：hero 一格 + 3 格 context 压成**一条 strip**（不是卡片阵列）——那轮的返工原因正是 chart 里塞了 3-cell KPI grid 被判"UI 太乱"。若 PM 要严格回到 phase 1，删掉 hero 右侧那 3 格即可，布局不塌。

## 三、R0 Phase 0 元素→组件映射

| Element | Figma component_set / icon | Code 真源 | 状态 |
|---|---|---|---|
| 顶栏 | `Top bar` (`918b928e…`, Tag=After Login) | `src/components/TopBar/TopBar.vue` | ✅ 1:1 mirror（M1 硬规则：TVU 页面必须用它） |
| 顶栏导航项 | `Tab/Item` (`2267405386…`, Type=Line) | `src/components/Tab/TabItem.vue` | ✅ mirror line variant 逐字 |
| 品牌 logo | `icon/logo/TVU` | `src/icons/catalog/generated/logo.ts` | ✅ 逐字复制 |
| KEY METRIC 标签 | `Badge` (`4db5246d…`, Rectangle/Neutral/Line) | `src/canonical/Badge.vue` | ✅（catalog §Badge "只读信息标签用 outline 矩形"） |
| 时间范围分段控件 | `Tab List` (`47efe04f…` Filled) + `Tab/Item` Filled | `Tab/TabList.vue` + `TabItem.vue` | ✅（catalog §Tab/Item "分段控件就用它，别用 Button"） |
| 自定义区间按钮 / Cancel / Apply | `Button/dark M` (`12b38c8b…`) | `src/components/Button/Button.vue` `PALETTE_SPEC` | ✅ 逐字取 filling-green / ghost-gray / rimless-gray |
| 趋势图 | `Chart` (type=line) | `src/canonical/Chart.vue` → `build-chart-option.ts` | 🟡 **不可消费**（canonical 走 ECharts，单文件禁 CDN）⇒ R3 "手写 1:1 mirror 视觉"：容器 `--bg-layer3`/`--sp-m`/`--r-m`、系列色 `--chart-color-1`、轴与网格 `--line-divider-light`、轴标+图例 `--text-2` 12px、图例 circle 8×8 bottom |
| 图表 hover 提示 | `Tooltips` (`40c9daaf…`, Center down) | `--tooltip-bg/border/text-dark` + `--shadow-l1` | ✅ token 逐字 |
| delta 徽标 | `Pill Status` | `src/components/Pill/Pill.vue`（`showDot=false` + `bgCss/textCss` 走 CANONICAL-028 token 覆写口） | ✅ 用组件自带 API，不改组件 |
| 涨/跌指示 | `icon/Arrow/up`·`down` | `catalog/generated/arrow.ts` | ✅ 逐字 |
| 面板折叠 chevron | `icon/Arrow/right-2`·`left-2` | 同上 | ✅ 逐字 |
| 说明面板信息图标 | `icon/Message/info-1` | `catalog/generated/message.ts` | ✅ 取 **info-1**（`currentColor`），刻意不取 info-2（它 fill 写死 `#3892F3`） |
| 日期 / 数据新鲜度图标 | `icon/Time/calendar`·`clock` | `catalog/generated/time.ts` | ✅ 逐字 |
| 关闭 | `icon/Edit/close` | `catalog/generated/edit.ts` | ✅ 逐字 |
| **右侧可折叠说明面板（容器本体）** | catalog 无 Drawer / Collapse / Accordion / Panel | — | ⚠️ **真 gap** ⇒ 按 R15 优先级 3 自建，已标 `<!-- LIBRARY GAP … candidate for tvu-design-system intake -->`，用库 primitive（surface token + 库图标 + Button rimless）拼，未自画任何图标 |

**R13 affordance trace（3 行，逐条）**
1. *分类*：面板展开/收起 = 方向指示（direction） → *扫描*：`arrow/dropdown`(chevron 下) · `arrow/right-2`/`left-2`(chevron 侧) · `arrow/double-up/down`(卡片展开) · `arrow/next`/`previous` → *选定*：`arrow/right-2` 收起 / `arrow/left-2` 展开 —— 面板沿水平轴移动，侧向 chevron 与运动方向一致；`double-up/down` 是**纵向卡片**折叠语义（catalog 已登记），不套用。
2. *分类*：数值升降 = 趋势方向 → *扫描*：`arrow/up`/`down`(带轴长箭头) · `arrow/dropdown`(chevron) · `arrow/updown`/`sorting`(排序) → *选定*：`arrow/up`·`down` —— 表达"值移动了"；chevron 是展开语义、`sorting` 是列排序语义，均非趋势。
3. *分类*：解释/帮助 = 信息提示 → *扫描*：`message/info-1`·`info-2`·`help-1`·`help-2`·`file/doc` → *选定*：`message/info-1` —— 面板讲的是"这个数怎么算的"（陈述事实）不是"我需要帮助"，且 `info-1` 用 `currentColor` 可跟 token 上色；`info-2` fill 写死 hex。

## 四、M11 趋势规则（三层信号，含我加的第 4 态）

| 状态 | Icon | 数值颜色 | Token |
|---|---|---|---|
| Improving（单位成本↓ / 毛利↑） | ↓ / ↑ | 绿 | `--brand` |
| Deteriorating | ↑ / ↓ | 橙 | `--orange` |
| Stable（\|Δ\| < 0.5%） | 无 | 中性 | `--text-body` |
| **Volume / context（新增）** | 实际方向 | 中性灰 | `--text-tips` |

两处如实说明：① M11 原表把 ↓ 与绿钉死，那是为"越小越好"的 metric 写的；毛利率是**越大越好** ⇒ 我把契约拆成「**箭头 = 实际方向，颜色 = 改善/恶化**」，双层信号本身没丢。② M11 引的 `Color/Text/Warning`(`--text-warning`) 在 `src/tokens/variables.css` **不存在**（已 grep 核实，零命中）⇒ 用同值的既有 token `--orange`（dark `#f68512`），**没有硬编码 hex**。③ 与 M3 不冲突：M3 管 session/stream 状态色（红=Live），这里是财务 delta，两个语义轴。

## 五、R9 交互态推断表（设计图没画也补）

| 元素 | active 态 | 反推的 hover | focus | 依据 |
|---|---|---|---|---|
| 分段控件 chip | `--brand` 底 + `--text-primary-btn` | 未选 chip `--bg-layer3` → `--bg-layer4`（active 的弱版本） | `outline 2px --brand / offset 2px` | 逐字取 `TabItem.vue` + `.btn:focus-visible` |
| 按钮 | filling-green `--brand` | `--brand-hover`；ghost-gray → `--bg-grey-btn-en` 底 | 同上 | 逐字取 `Button.vue` `PALETTE_SPEC.hover`，未自拟 |
| KPI 格 | — | `--bg-layer3` | — | 同房内既有 demo `.kpi:hover` 范式 |
| 折叠按钮 / 图例项 | — | `--bg-layer3` / 文字 `--text-heading` | 同上 | active 弱版本 |
| 图表绘图区 | — | 竖向游标 + 数据点放大 + Tooltip | 键盘 ←/→ 移动游标 | 补 a11y，设计图未画 |

**其他决策**：主题 = 深色（DS 默认，`<html data-theme="light">` 可切，未写 `prefers-color-scheme`，遵 R2.1）；产品 UI 全英文单语走 `const T` map（R16）；布局 flex/grid，3 处 `absolute` 各带紧邻注释说明是浮层（R14）；`Export` 按钮**主动删掉**——不在你的 4 条要求内，且做不出真行为就是假 affordance（Simplicity Contract）。token 按 R2.1 本该由构建期 `copyDesignTokensPlugin` `<link>` 进来，单文件无构建链 ⇒ 逐字内联，文件头已标来源与"别在这里改值"。唯一 hex = `logo/TVU` 自带的 `#56AF31`，那是库资产原样复制（已登记 divergence `icon-variable-resolved-to-hex`），非我写的颜色。

数字全部自洽：`cpt = mean(series)`、`margin`/`saved`/`vendor cost` 全在 JS 里由两个数据源现算，切换时间范围时 KPI、图表、说明面板的算例**一起变**。

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cost per Token Trend · TVU Usage Service</title>
<style>
/* ============================================================================
 * DESIGN TOKENS — VERBATIM EXCERPT of tvu-design-system/src/tokens/variables.css
 * ----------------------------------------------------------------------------
 * ⛔ DO NOT EDIT ANY VALUE HERE. Tokens are Figma-synced (`pnpm generate`,
 *    gated by `pnpm audit:token-contract`). To change a colour: change Figma,
 *    re-sync, then re-copy this block.
 * Token groups not consumed by this page (component-variant tokens for
 * Switch/Radio/Slider/Progress/FormItem/Pagination, Digital-Numbers clock face,
 * z-index) are omitted; nothing is altered.
 * Inlining is a knowing deviation from code-conventions R2.1, which prescribes
 * the build-time `copyDesignTokensPlugin` + <link href="tvu-tokens.css">. This
 * deliverable is a single file with no build chain and no CDN allowed, so the
 * same block is inlined instead. In a repo, use the <link>.
 * 内联是「单文件 + 不引 CDN」下的唯一选择，不是手抄配色：值逐字复制自真源。
 * ========================================================================== */

/* ── Dark mode (default) ── */
:root {
  /* ===== Neutrals (grey scale, light → dark) ===== */
  --color-white: #ffffff;
  --color-grey-2: #f8f8f8;
  --color-grey-3: #f0f0f0;
  --color-grey-4: #dbdbdb;
  --color-grey-5: #cccccc;
  --color-grey-6: #9e9e9e;
  --color-grey-7: #7b7b7b;
  --color-grey-8: #595959;
  --color-grey-9: #434343;
  --color-grey-10: #353535;
  --color-grey-11: #262626;
  --color-grey-12: #1f1f1f;
  --color-grey-13: #141414;
  --color-grey-14: #000000;

  /* ===== Brand & status ===== */
  --brand: #2fb54e;
  --brand-hover: #41c760;
  --brand-match: #0b2b13;
  --brand-disable: #1a652c;
  --red: #ea4233;
  --red-hover: #ed5b4e;
  --red-bg: #fff1f0;
  --orange: #f68512;
  --orange-hover: #f89c3e;
  --orange-bg: #fff3db;
  --blue: #3892f3;
  --blue-hover: #62a9f6;
  --blue-bg: #e1eefd;

  /* ===== Message soft backgrounds ===== */
  --msg-bg-red: #3b1c1a;
  --msg-bg-brand: #19311e;
  --msg-bg-blue: #1a2b3c;
  --msg-bg-grey: #353535;

  /* ===== Text ===== */
  --text-heading: #ffffff;
  --text-body: #f8f8f8;
  --text-2: #cccccc;
  --text-tips: #9e9e9e;
  --text-placeholder: #7b7b7b;
  --text-disabled: #595959;
  --text-primary-btn: #f8f8f8;

  /* ===== Icon ===== */
  --icon-active: #f8f8f8;
  --icon-default: #9e9e9e;
  --icon-disabled: #595959;
  --icon-placeholder: #7b7b7b;

  /* ===== Surface / background ===== */
  --bg-layer1: #141414;
  --bg-layer2: #1f1f1f;
  --bg-layer3: #262626;
  --bg-layer4: #353535;
  --bg-topbar: #000000;
  --bg-grey-btn-hv: #595959;
  --bg-grey-btn-en: #434343;
  --bg-grey-btn-dis: #434343;

  /* ===== Line / border ===== */
  --line-border: #595959;
  --line-deep: #353535;
  --line-light: #434343;

  /* ===== Chart categorical palette (12 colors) — CANONICAL-011 D1 ===== */
  --chart-color-1: var(--brand);     /* 134° 绿 (brand) */
  --chart-color-2: var(--red);       /* 6° 红 */
  --chart-color-3: var(--blue);      /* 211° 蓝 */
  --chart-color-4: var(--orange);    /* 31° 橙 */
}

/* ── Spacing ── */
:root {
  --sp-xxs: 4px;
  --sp-xs: 8px;
  --sp-s: 12px;
  --sp-m: 16px;
  --sp-l: 24px;
  --sp-xl: 32px;
  --sp-xxl: 40px;
  --sp-xxxl: 56px;
}

/* ── Radius ── */
:root {
  --r-xs: 2px;
  --r-s: 4px;
  --r-m: 8px;
  --r-l: 12px;
  --r-xl: 16px;
  --r-xxl: 20px;
}

/* ── Layout containers ── */
:root {
  --container-page: 1600px;  /* CanonicalShowcase 页宽 */
  --container-wide: 1800px;  /* docs 站外壳最大宽 */
}
/* NOTE — Layout breakpoints: CSS @media cannot read var(), so the literal
   values below must stay ∈ {640, 960, 1100, 1280} (= --bp-sm/md/lg/xl), which is
   exactly what `pnpm audit:layout-tokens` mechanically checks. Do not invent a
   fifth breakpoint here. */

/* ── Typography ── */
:root {
  --font-family-en: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 'Fira Sans',
    'Droid Sans', 'Helvetica Neue', sans-serif;
  --font-family-zh: 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif;
  --font-family-base: var(--font-family-en), var(--font-family-zh);
}

/* ── Effect Style tokens — figma 真源 (3-layered drop shadows) ── */
:root {
  --shadow-l1:
    0 5px 12px 4px #00000017,
    0 3px 6px 0 #0000001f,
    0 1px 2px -2px #00000029;
  --shadow-l3:
    0 12px 48px 16px #00000008,
    0 9px 28px 0 #0000000d,
    0 6px 16px -8px #00000014;
}

/* ── Text Style tokens — figma 真源 7 Text Styles ── */
:root {
  --text-style-tips:           400 12px/16px var(--font-family-base);
  --text-style-body:           400 14px/21px var(--font-family-base);
  --text-style-pop-title:      600 16px/24px var(--font-family-base);
  --text-style-headline-2:     600 18px/27px var(--font-family-base);
  --text-style-headline-1:     600 20px/30px var(--font-family-base);
  --text-style-large-title-24: 600 24px/36px var(--font-family-base);

  /* Atomic font tokens — for partial overrides (size only / weight only) */
  --font-size-tips: 12px;
  --font-size-body: 14px;
  --font-size-pop-title: 16px;
  --font-size-headline-2: 18px;
  --font-size-headline-1: 20px;
  --font-size-large-title-24: 24px;
  --line-height-tips: 16px;
  --line-height-body: 21px;
  --font-weight-regular: 400;
  --font-weight-semibold: 600;
}

/* ── Domain-specific component-variant tokens (theme-stable) ── */
:root {
  --tooltip-bg-dark: var(--color-grey-10);     /* #353535 */
  --tooltip-border-dark: var(--color-grey-8);  /* #595959 */
  --tooltip-text-dark: var(--color-grey-2);    /* #f8f8f8 */
  --tooltip-max-width: 320px;
  --badge-circle-size: 24px;
  --topbar-height: 64px;
  --topbar-section-gap: 80px;
}

/* ── Derived aliases for component compatibility ── */
:root {
  --input-filled-bg: var(--bg-layer4);
  --green-bg: var(--brand-match);
  --line-divider-deep: var(--line-deep);
  --line-divider-light: var(--line-light);
}

/* ── Light mode overrides ── */
[data-theme="light"] {
  --brand: #299f45;
  --brand-hover: #00ac39;
  --brand-match: #f1fbf3;
  --brand-disable: #8fcc9d;
  --red: #dc2717;
  --red-hover: #eb493a;
  --orange: #e26601;
  --orange-hover: #e97413;
  --blue: #0473d5;
  --blue-hover: #118dfb;

  --msg-bg-red: #fbddda;
  --msg-bg-brand: #daf2df;
  --msg-bg-blue: #dbebfd;
  --msg-bg-grey: #f6f6f6;

  --text-heading: #000000;
  --text-body: #141414;
  --text-2: #434343;
  --text-tips: #595959;
  --text-placeholder: #9e9e9e;
  --text-disabled: #cccccc;

  --icon-active: #434343;
  --icon-default: #595959;
  --icon-disabled: #cccccc;
  --icon-placeholder: #9e9e9e;

  --bg-layer1: #ffffff;
  --bg-layer2: #f8f8f8;
  --bg-layer3: #f0f0f0;
  --bg-layer4: #dbdbdb;
  --bg-topbar: #ffffff;
  --bg-grey-btn-hv: #cccccc;
  --bg-grey-btn-en: #dbdbdb;
  --bg-grey-btn-dis: #dbdbdb;

  --line-border: #f0f0f0;
  --line-deep: #dbdbdb;
  --line-light: #cccccc;

  --chart-color-1: var(--brand);
  --chart-color-2: var(--red);
  --chart-color-3: var(--blue);
  --chart-color-4: var(--orange);

  --tooltip-bg-dark: var(--color-white);
  --tooltip-border-dark: var(--color-grey-3);
  --tooltip-text-dark: var(--color-grey-13);

  --input-filled-bg: var(--bg-layer1);
  --green-bg: var(--brand-match);
  --line-divider-deep: var(--line-deep);
  --line-divider-light: var(--line-light);
}

/* ============================================================================
 * Element base layer — verbatim excerpt (same source file, §Element base layer)
 * ========================================================================== */
body {
  font: var(--text-style-body);
  font-family: var(--font-family-base);
}
code,
kbd { font-family: monospace; font-size: var(--font-size-tips); }
input {
  font: var(--text-style-body);
  color: var(--text-body);
  background: var(--input-filled-bg);
  border: 1px solid var(--line-border);
  border-radius: var(--r-s);
  padding: var(--sp-xs) var(--sp-s);
}

/* ============================================================================
 * PAGE — Cost per Token Trend (TVU Usage Service)
 * Product truth: docs/_archive/retrospection/2026-05-11-saas-dashboard-2-option-pm-review.md
 * Persona (domain-tvu.md §TVU SaaS 默认 Persona): CFO / Business Owner primary
 *   — "3 秒内获取 margin % + 趋势方向 + 是否改善"; SRE secondary — data freshness.
 * ========================================================================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  background: var(--bg-layer1);
  color: var(--text-body);
  -webkit-font-smoothing: antialiased;
}
button { font-family: inherit; }
.num { font-variant-numeric: tabular-nums; }
.sr-only {
  position: absolute; /* overlay: screen-reader-only text lifted out of flow */
  width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

.app { min-height: 100%; display: flex; flex-direction: column; }

/* ── Top bar · 1:1 mirror of src/components/TopBar/TopBar.vue (Tag=After Login)
      M1 hard rule: every TVU product page instances Top bar, never redraws it. ── */
.topbar {
  display: flex;
  align-items: center;
  padding: var(--sp-xs) var(--sp-m);
  background: var(--bg-topbar);
  border: 1px solid var(--line-deep);
  gap: var(--topbar-section-gap);
  flex-shrink: 0;
  width: 100%;
  height: var(--topbar-height);
}
.topbar-brand { display: flex; align-items: center; gap: var(--sp-m); flex-shrink: 0; }
.topbar-logo { width: 28px; height: 28px; display: block; flex: none; }
.topbar-title { font: var(--text-style-pop-title); color: var(--text-heading); white-space: nowrap; }
.topbar-menu { display: flex; align-items: center; gap: var(--sp-xxl); flex: 1; align-self: stretch; }
.topbar-right { display: flex; align-items: center; gap: var(--sp-m); flex-shrink: 0; margin-left: auto; }

/* Top-bar nav = Tab/Item `Type=Line`, verbatim from TabItem.vue
   (.tab-item--line + .tab-item--active-green). Fills the Menu SLOT. */
.navitem {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  height: 32px;
  padding: 0 var(--sp-m);
  font: var(--text-style-body);
  color: var(--text-tips);
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
  white-space: nowrap;
  transition: color .15s, background .15s, border-color .15s;
}
.navitem:hover { color: var(--text-2); }
.navitem[aria-current="page"] {
  color: var(--brand);
  border-bottom-color: var(--brand);
  font-weight: 700;
}
.navitem:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.freshness {
  display: flex; align-items: center; gap: var(--sp-xxs);
  font: var(--text-style-tips); color: var(--text-tips); white-space: nowrap;
}
.freshness svg { color: var(--icon-default); flex: none; }
.avatar {
  width: 32px; height: 32px; border-radius: 999px;
  background: var(--bg-layer4); color: var(--text-body);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--font-size-tips); font-weight: var(--font-weight-semibold);
  flex: none;
}

/* ── Page shell: 2-D region split → grid (R14 decision tree) ── */
.page {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--explain-w, 340px);
  min-height: 0;
  transition: grid-template-columns .2s ease;
}
.page[data-explain="collapsed"] { --explain-w: 48px; }
.main {
  min-width: 0;
  padding: var(--sp-l);
  display: flex;
  flex-direction: column;
  gap: var(--sp-m);
  max-width: var(--container-wide);
}

/* ── Page head ── */
.pagehead { display: flex; flex-direction: column; gap: var(--sp-xs); }
.pagehead__top { display: flex; align-items: center; gap: var(--sp-s); flex-wrap: wrap; }
.pagehead h1 { font: var(--text-style-headline-1); color: var(--text-heading); }
/* Badge · Type=Rectangle, Color=Neutral, Tag=Line — verbatim from canonical/Badge.vue
   (catalog §Badge: outline rectangle is the read-only info-label primitive). */
.badge-line {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--badge-circle-size);
  padding-block: var(--sp-xxs); padding-inline: var(--sp-s);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-xl);
  background: transparent;
  color: var(--text-body);
  font: var(--text-style-tips);
  letter-spacing: .6px;
  white-space: nowrap;
}
.pagehead__sub { font: var(--text-style-body); color: var(--text-tips); }
.pagehead__sub code {
  color: var(--text-2); background: var(--bg-layer2);
  padding: 1px var(--sp-xxs); border-radius: var(--r-xs);
}

/* ── Toolbar: 1-D control row → flex ── */
.toolbar {
  display: flex; align-items: center; gap: var(--sp-s);
  flex-wrap: wrap; row-gap: var(--sp-xs);
}
.toolbar__label { font: var(--text-style-tips); color: var(--text-tips); }
.toolbar__spacer { flex: 1; }
.toolbar__window { font: var(--text-style-tips); color: var(--text-2); white-space: nowrap; }

/* Tab List `Type=Filled` — verbatim from TabList.vue (.tab-list--filled) */
.seg {
  display: flex; align-items: stretch;
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-s);
}
.seg__items { display: inline-flex; align-items: center; gap: 0; }
/* Tab/Item `Type=Filled` — verbatim from TabItem.vue.
   NOTE (catalog §Tab/Item known gap CANONICAL-F93): the resting fill --bg-layer3
   assumes the host surface is clearly darker. Host here is --bg-layer1 (#141414)
   vs #262626 → distinguishable, so no per-file override is needed. */
.seg__item {
  display: inline-flex; align-items: center; gap: var(--sp-xs);
  height: 32px; padding: 0 var(--sp-m);
  font: var(--text-style-body);
  color: var(--text-2);
  background: var(--bg-layer3);
  border: 0; cursor: pointer; white-space: nowrap; user-select: none;
  transition: color .15s, background .15s, border-color .15s;
}
.seg__item:first-child { border-radius: var(--r-s) 0 0 var(--r-s); }
.seg__item:last-child { border-radius: 0 var(--r-s) var(--r-s) 0; }
.seg__item:hover[aria-selected="false"] { background: var(--bg-layer4); color: var(--text-body); }
.seg__item[aria-selected="true"] { background: var(--brand); color: var(--text-primary-btn); font-weight: 700; }
.seg__item:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Button/dark M — geometry + PALETTE_SPEC verbatim from components/Button/Button.vue */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-xxs);
  height: 32px; padding-inline: var(--sp-m);
  border: 1px solid transparent; border-radius: var(--r-s);
  font-family: var(--font-family-base); font-size: var(--font-size-body);
  font-weight: 400; line-height: var(--line-height-body);
  cursor: pointer; white-space: nowrap; user-select: none; outline: none;
  transition: background .15s, color .15s, border-color .15s;
}
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn svg { flex: none; }
.btn--ghost-gray { background: transparent; color: var(--text-2); border-color: var(--bg-grey-btn-en); }
.btn--ghost-gray:hover { background: var(--bg-grey-btn-en); color: var(--text-2); }
.btn--fill-green { background: var(--brand); color: var(--text-primary-btn); }
.btn--fill-green:hover { background: var(--brand-hover); }
.btn--rimless-gray { background: transparent; color: var(--text-2); }
.btn--rimless-gray:hover { background: var(--bg-grey-btn-en); }

/* ── Custom range popover ── */
.rangewrap { position: relative; } /* anchor for the absolutely-positioned popover below */
.popover {
  position: absolute; /* overlay: floating panel anchored to its trigger — no layout semantics (R14) */
  top: calc(100% + var(--sp-xs)); right: 0; z-index: 1000;
  width: 320px; padding: var(--sp-m);
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep); border-radius: var(--r-l);
  box-shadow: var(--shadow-l3);
  display: none; flex-direction: column; gap: var(--sp-s);
}
.popover[data-open="true"] { display: flex; }
.popover__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-s); }
.popover__title { font: var(--text-style-pop-title); color: var(--text-heading); }
.iconbtn {
  width: 24px; height: 24px; border: 0; border-radius: var(--r-s);
  background: transparent; color: var(--icon-default);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background .12s, color .12s;
}
.iconbtn:hover { background: var(--bg-layer3); color: var(--icon-active); }
.iconbtn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.field { display: flex; flex-direction: column; gap: var(--sp-xxs); }
.field label { font: var(--text-style-tips); color: var(--text-tips); }
.field input { width: 100%; }
.field input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.popover__actions { display: flex; justify-content: flex-end; gap: var(--sp-xs); margin-top: var(--sp-xxs); }
.popover__note { font: var(--text-style-tips); color: var(--text-tips); }

/* ── KPI strip (requirement 1) ── */
.kpis {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-m);
  overflow: hidden;
}
.kpi {
  padding: var(--sp-m) var(--sp-l);
  border-right: 1px solid var(--line-deep);
  display: flex; flex-direction: column; gap: var(--sp-xxs);
  min-width: 0;
  transition: background .12s;
}
.kpi:last-child { border-right: 0; }
.kpi:hover { background: var(--bg-layer3); }
.kpi__label {
  font-size: var(--font-size-tips); line-height: var(--line-height-tips);
  font-weight: var(--font-weight-semibold);
  color: var(--text-tips); text-transform: uppercase; letter-spacing: .7px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.kpi__valuerow { display: flex; align-items: baseline; gap: var(--sp-xs); flex-wrap: wrap; }
.kpi__value {
  font-size: var(--font-size-headline-1); line-height: 1.5;
  font-weight: var(--font-weight-semibold); color: var(--text-heading);
}
.kpi--hero .kpi__value { font-size: 32px; line-height: 40px; }
.kpi__unit { font-size: var(--font-size-body); font-weight: var(--font-weight-regular); color: var(--text-2); }
.kpi__foot { font: var(--text-style-tips); color: var(--text-tips); }
.kpi__foot strong { color: var(--brand); font-weight: var(--font-weight-semibold); }

/* Delta — M11 two-layer signal: arrow = actual direction, colour = improving/
   deteriorating. Compact (in-cell) form. */
.delta {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: var(--font-size-tips); line-height: var(--line-height-tips);
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.delta svg { width: 12px; height: 12px; flex: none; }
.delta--good { color: var(--brand); }
.delta--bad { color: var(--orange); }
.delta--flat { color: var(--text-body); }
.delta--context { color: var(--text-tips); }

/* Hero delta = Pill Status geometry verbatim from components/Pill/Pill.vue
   (min-height 28 · padding 4/12/4/10 · gap 8 · radius 999 · 12px/500).
   showDot=false + bgCss/textCss token overrides = Pill's own CANONICAL-028 API.
   M49 declared exception: same semanticRole (trend delta) rendered as a pill in
   the hero and as plain coloured text in the context cells. Rationale = M7
   visual-weight hierarchy — the hero cell IS the page's answer, the context
   cells must stay quieter. Both keep the identical two-layer signal. */
.pill {
  display: inline-flex; align-items: center; box-sizing: border-box;
  min-height: 28px; gap: var(--sp-xs);
  padding: var(--sp-xxs) var(--sp-s) var(--sp-xxs) 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: var(--font-size-tips); font-weight: 500; line-height: 1.4;
  font-family: var(--font-family-base);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pill svg { width: 14px; height: 14px; flex: none; }
.pill--good { background: var(--msg-bg-brand); color: var(--brand); }
.pill--bad  { background: color-mix(in srgb, var(--orange) 18%, transparent); color: var(--orange); }
.pill--flat { background: var(--msg-bg-grey); color: var(--text-body); }

/* ── Chart card (requirement 2) ── */
.card {
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-m);
  display: flex; flex-direction: column;
  min-width: 0;
}
.card__head {
  display: flex; align-items: baseline; gap: var(--sp-s);
  padding: var(--sp-m) var(--sp-m) 0;
  flex-wrap: wrap;
}
.card__title { font: var(--text-style-pop-title); color: var(--text-heading); }
.card__hint { font: var(--text-style-tips); color: var(--text-tips); }
.card__body { padding: var(--sp-m); }

/* Chart container — 1:1 mirror of canonical/Chart.vue → components/Chart/Chart.vue
   `.tvu-chart` (padding --sp-m · radius --r-m · background --bg-layer3). */
.tvu-chart {
  position: relative; /* anchor for the hover tooltip overlay (R14) */
  display: block; box-sizing: border-box;
  padding: var(--sp-m);
  border-radius: var(--r-m);
  background: var(--bg-layer3);
}
.tvu-chart__view { display: block; width: 100%; }
.tvu-chart__view svg { display: block; width: 100%; height: auto; }
.tvu-chart__hit { cursor: crosshair; }
.tvu-chart__hit:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Legend — mirrors build-chart-option.ts `legend`
   (bottom · icon circle · itemWidth/Height 8 · textStyle 12px --text-2). */
.legend {
  display: flex; align-items: center; justify-content: center;
  gap: var(--sp-l); flex-wrap: wrap;
  margin-top: var(--sp-s);
}
.legend__item {
  display: inline-flex; align-items: center; gap: var(--sp-xs);
  font-size: 12px; color: var(--text-2);
  background: none; border: 0; padding: var(--sp-xxs) var(--sp-xs);
  border-radius: var(--r-s); cursor: pointer;
  transition: color .15s, background .15s, opacity .15s;
}
.legend__item:hover { color: var(--text-heading); background: var(--bg-layer4); }
.legend__item:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.legend__item[aria-pressed="false"] { opacity: .45; }
.legend__mark { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.legend__mark--series { background: var(--chart-color-1); }
.legend__mark--baseline {
  border-radius: 0; height: 0; width: 12px;
  border-top: 2px dashed var(--text-tips); background: none;
}

/* Chart tooltip — Tooltips component tokens (dark, pointing = Center down) */
.charttip {
  position: absolute; /* overlay: follows the cursor, no layout semantics (R14) */
  z-index: 1000;
  max-width: var(--tooltip-max-width);
  padding: var(--sp-xs) var(--sp-s);
  background: var(--tooltip-bg-dark);
  border: 1px solid var(--tooltip-border-dark);
  border-radius: var(--r-s);
  box-shadow: var(--shadow-l1);
  color: var(--tooltip-text-dark);
  font: var(--text-style-tips);
  pointer-events: none;
  opacity: 0; transform: translate(-50%, -100%);
  transition: opacity .12s;
}
.charttip[data-open="true"] { opacity: 1; }
.charttip__row { display: flex; align-items: center; gap: var(--sp-xs); white-space: nowrap; }
.charttip__row + .charttip__row { margin-top: 2px; }
.charttip__dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.charttip__k { color: var(--text-tips); }
.charttip__v { font-variant-numeric: tabular-nums; font-weight: var(--font-weight-semibold); }

.card__foot {
  display: flex; align-items: center; gap: var(--sp-l); flex-wrap: wrap;
  padding: 0 var(--sp-m) var(--sp-m);
  font: var(--text-style-tips); color: var(--text-tips);
}
.card__foot b { color: var(--text-2); font-weight: var(--font-weight-semibold); font-variant-numeric: tabular-nums; }

/* ============================================================================
 * LIBRARY GAP: collapsible side panel (drawer / accordion container).
 * Verified miss — figma-component-catalog.md has no Drawer / Collapse /
 * Accordion / Panel component_set, and src/canonical/ has no equivalent.
 * Composed from library primitives only (surface + line + text tokens, library
 * icons, Button rimless for the toggle); nothing is hand-drawn.
 * Candidate for tvu-design-system intake → backlog.md BRIDGE-CODE-CANONICAL.
 * ========================================================================== */
.explain {
  background: var(--bg-layer2);
  border-left: 1px solid var(--line-deep);
  display: flex; flex-direction: column;
  min-width: 0; overflow: hidden;
}
.explain__head {
  display: flex; align-items: center; gap: var(--sp-xs);
  padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-m);
  border-bottom: 1px solid var(--line-deep);
  flex: none; min-height: 48px;
}
.explain__head svg.info { color: var(--icon-default); flex: none; }
.explain__title {
  font: var(--text-style-body); font-weight: var(--font-weight-semibold);
  color: var(--text-heading); flex: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.explain__body {
  padding: var(--sp-m);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp-l);
}
.explain__body::-webkit-scrollbar { width: 4px; }
.explain__body::-webkit-scrollbar-thumb { background: var(--bg-layer4); border-radius: 2px; }
[data-explain="collapsed"] .explain__body,
[data-explain="collapsed"] .explain__title { display: none; }
[data-explain="collapsed"] .explain__head {
  flex-direction: column; padding: var(--sp-s) 0; border-bottom: 0; gap: var(--sp-s);
}
.explain__rail {
  display: none;
  writing-mode: vertical-rl;
  font: var(--text-style-tips); color: var(--text-tips);
  letter-spacing: .6px; white-space: nowrap;
}
[data-explain="collapsed"] .explain__rail { display: block; }

.sect { display: flex; flex-direction: column; gap: var(--sp-xs); }
.sect__h {
  font-size: var(--font-size-tips); line-height: var(--line-height-tips);
  font-weight: var(--font-weight-semibold);
  color: var(--text-tips); text-transform: uppercase; letter-spacing: .7px;
}
.sect p { font: var(--text-style-body); color: var(--text-2); }
.formula {
  background: var(--bg-layer3);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-s);
  padding: var(--sp-s);
  display: flex; flex-direction: column; gap: var(--sp-xxs);
}
.formula code { color: var(--text-body); font-size: var(--font-size-tips); line-height: 18px; }
.formula .eq { color: var(--brand); font-variant-numeric: tabular-nums; }
.deflist { display: flex; flex-direction: column; gap: var(--sp-xs); margin: 0; }
.deflist div { display: flex; flex-direction: column; gap: 2px; }
.deflist dt { font: var(--text-style-body); color: var(--text-body); font-weight: var(--font-weight-semibold); }
.deflist dd { font: var(--text-style-tips); color: var(--text-tips); margin: 0; }
.deflist dd code { color: var(--text-2); }
.rulerow {
  display: flex; align-items: flex-start; gap: var(--sp-xs);
  font: var(--text-style-tips); color: var(--text-tips);
}
.rulerow .delta { flex: none; min-width: 56px; }
.notlist { display: flex; flex-direction: column; gap: var(--sp-xs); list-style: none; }
.notlist li {
  font: var(--text-style-tips); color: var(--text-tips);
  padding-left: var(--sp-s); position: relative; /* marker anchor for the ::before rule */
}
.notlist li::before {
  content: ''; position: absolute; left: 0; top: 6px;
  width: 4px; height: 4px; border-radius: 50%; background: var(--line-border);
}
.explain__foot {
  border-top: 1px solid var(--line-deep);
  padding: var(--sp-s) var(--sp-m);
  font: var(--text-style-tips); color: var(--text-tips);
  flex: none;
}
[data-explain="collapsed"] .explain__foot { display: none; }

/* ── Responsive · literals ∈ {640, 960, 1100, 1280} = --bp-sm/md/lg/xl ── */
@media (max-width: 1280px) {
  .kpis { grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); }
  .topbar { gap: var(--sp-xl); }
}
@media (max-width: 1100px) {
  .page { grid-template-columns: minmax(0, 1fr) 48px; }
  .page .explain__body, .page .explain__title, .page .explain__foot { display: none; }
  .page[data-explain="expanded"] { grid-template-columns: minmax(0, 1fr) 320px; }
  .page[data-explain="expanded"] .explain__body,
  .page[data-explain="expanded"] .explain__title,
  .page[data-explain="expanded"] .explain__foot { display: flex; }
  .page[data-explain="expanded"] .explain__rail { display: none; }
  .page[data-explain="expanded"] .explain__head { flex-direction: row; padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-m); border-bottom: 1px solid var(--line-deep); }
  .page:not([data-explain="expanded"]) .explain__rail { display: block; }
  .page:not([data-explain="expanded"]) .explain__head { flex-direction: column; padding: var(--sp-s) 0; border-bottom: 0; gap: var(--sp-s); }
  .topbar-menu { gap: var(--sp-m); }
}
@media (max-width: 960px) {
  .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .kpi { border-bottom: 1px solid var(--line-deep); }
  .kpi:nth-child(2n) { border-right: 0; }
  .main { padding: var(--sp-m); }
  .topbar { gap: var(--sp-m); }
  .freshness { display: none; }
}
@media (max-width: 640px) {
  .kpis { grid-template-columns: minmax(0, 1fr); }
  .kpi { border-right: 0; }
  .topbar-menu { display: none; }
}
</style>
</head>
<body>

<div class="app">

  <!-- ── Top bar · Tag=After Login. product-name + Menu SLOT + Right_content SLOT.
       Catalog §Top bar "Don't use for": no page-level elements in the slots —
       range switcher / KPIs live in the strip BELOW the bar. ── -->
  <header class="topbar">
    <div class="topbar-brand">
      <!-- icon/logo/TVU — copied verbatim from src/icons/catalog/generated/logo.ts
           (figmaNodeId 1136:3266). The literal #56AF31 is the library asset's own
           fill: Figma's SVG export resolves bound Variables to hex. That is the
           registered divergence `icon-variable-resolved-to-hex`
           (src/design-system/translation/divergences-decisions.json), NOT an
           author-written colour. Do not "tokenise" it — that would fork the asset. -->
      <svg class="topbar-logo" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="TVU">
        <path d="M15.9933 8.68971V8.6898V8.6899V8.69001C15.9962 8.96317 16 9.31582 16 9.81499V13.9033C16 13.9033 16 16 13.6733 15.9997H7.99586L7.98791 15.9998L7.97996 16H7.97102H7.93922C7.93922 16 7.94419 16 7.9538 15.9997C3.55876 15.9857 0 12.4098 0 8.00017C0 3.5819 3.573 0 7.97996 0C12.1057 0 15.5005 3.13931 15.9165 7.16512L15.9182 7.16545L15.9185 7.18106C15.946 7.45033 15.99 7.97375 15.99 8.25C15.99 8.37606 15.9915 8.51693 15.9933 8.68953L15.9933 8.68971ZM1.19653 7.97028C1.19653 11.7388 4.24392 14.7937 8.00305 14.7937C11.7622 14.7937 14.8096 11.7388 14.8096 7.97028C14.8096 4.20178 11.7622 1.14681 8.00305 1.14681C4.24392 1.14681 1.19653 4.20178 1.19653 7.97028ZM13.4204 15.2121L15.0122 14.2386L13.4204 13.212V15.2121ZM12.6575 12.0533C12.7649 12.0533 12.8543 12.0186 12.926 11.9492C12.9973 11.8798 13.0333 11.7951 13.033 11.6953C13.033 11.6246 13.0119 11.5608 12.9701 11.5041C12.9273 11.448 12.8468 11.39 12.7279 11.3296C12.6304 11.2805 12.5665 11.2378 12.5359 11.2011C12.5057 11.1651 12.4904 11.1274 12.4904 11.088C12.4904 11.0433 12.5087 11.0042 12.5457 10.9705C12.5828 10.9368 12.627 10.9201 12.6789 10.9201C12.7605 10.9201 12.8441 10.9605 12.9293 11.0419L13.032 10.9378C12.9161 10.8281 12.8013 10.7733 12.6871 10.7733C12.5889 10.7733 12.5077 10.804 12.4435 10.8651C12.3793 10.9265 12.347 11.0032 12.347 11.0957C12.347 11.1667 12.367 11.2298 12.4075 11.2849C12.4476 11.3399 12.5237 11.396 12.6358 11.4527C12.7401 11.5051 12.8074 11.5481 12.8369 11.5825C12.8665 11.6175 12.8814 11.6576 12.8814 11.7023C12.8814 11.7567 12.859 11.8041 12.8142 11.8444C12.7693 11.8848 12.7146 11.9048 12.6507 11.9048C12.5593 11.9048 12.473 11.8595 12.3915 11.7694L12.2913 11.8812C12.3337 11.9349 12.3874 11.9769 12.4527 12.0076C12.5175 12.038 12.5858 12.0533 12.6575 12.0533ZM11.9733 12.0533L11.3829 11.4987V12.0533H11.2319V10.3467H11.3829V11.3188L11.9264 10.8219H12.148L11.5037 11.4093L12.1856 12.0533H11.9733ZM10.6556 10.9878C10.7049 10.916 10.7569 10.8623 10.8117 10.8267C10.8665 10.7911 10.9234 10.7733 10.9826 10.7733C11.0274 10.7733 11.075 10.7874 11.126 10.8157L11.0416 10.9512C11.0078 10.9365 10.9792 10.9293 10.9561 10.9293C10.902 10.9293 10.85 10.9512 10.8 10.9953C10.7501 11.0391 10.7118 11.1075 10.6856 11.1999C10.6656 11.271 10.6556 11.4147 10.6556 11.6312V12.0533H10.4902V10.8055H10.6556V10.9878ZM9.64269 10.7733C9.83034 10.7733 9.98595 10.8411 10.1095 10.9769C10.2217 11.1007 10.2778 11.2472 10.2781 11.4167C10.2781 11.5868 10.2187 11.7353 10.1001 11.8625C9.9816 11.9896 9.82934 12.0533 9.64302 12.0533C9.45603 12.0533 9.3031 11.9896 9.18456 11.8625C9.06569 11.7353 9.00659 11.5868 9.00659 11.4167C9.00659 11.2482 9.06269 11.102 9.17488 10.9782C9.29809 10.8417 9.45403 10.7733 9.64269 10.7733ZM9.88152 11.8421C9.95431 11.8007 10.0121 11.742 10.0548 11.6659C10.0976 11.5899 10.1189 11.5081 10.1186 11.4197C10.1186 11.2832 10.0718 11.1667 9.97768 11.0706C9.88352 10.9742 9.77166 10.9262 9.6421 10.9262C9.51155 10.9262 9.39969 10.9745 9.30586 11.0706C9.21203 11.1667 9.16528 11.2832 9.16528 11.4197C9.16528 11.5078 9.18665 11.5899 9.22939 11.6659C9.27213 11.742 9.3299 11.8007 9.40269 11.8421C9.47548 11.8835 9.55529 11.9042 9.6421 11.9042C9.72892 11.9042 9.80872 11.8835 9.88152 11.8421ZM8.33623 12.0533H8.36592L8.90069 10.7733H8.73604L8.35108 11.7025L7.95565 10.7733H7.92698L7.53391 11.7025L7.15637 10.7733H6.99341L7.5194 12.0533H7.5491L7.9425 11.1368L8.33623 12.0533ZM6.88747 10.9474H6.63214V12.0533H6.4714V10.9474H6.25171V10.8089H6.4714V10.3467H6.63214V10.8089H6.88747V10.9474ZM5.83626 11.7904C5.79475 11.8261 5.74412 11.8548 5.68505 11.8761C5.62564 11.8978 5.56454 11.9085 5.5021 11.9085C5.37214 11.9085 5.26278 11.8635 5.17434 11.773C5.08591 11.6826 5.04034 11.5668 5.0373 11.4253H6.14579C6.1441 11.2592 6.09887 11.1204 6.00976 11.0092C5.8842 10.8517 5.71644 10.7733 5.50648 10.7733C5.30261 10.7733 5.13958 10.8501 5.01806 11.0039C4.9222 11.1247 4.87427 11.2622 4.87427 11.4167C4.87427 11.5808 4.93097 11.728 5.04439 11.8581C5.1578 11.9883 5.31476 12.0533 5.51594 12.0533C5.60674 12.0533 5.68775 12.0396 5.75964 12.0126C5.83154 11.9853 5.89601 11.9456 5.95339 11.8935C6.01077 11.8414 6.06141 11.7727 6.10529 11.6876L5.97162 11.6182C5.92301 11.697 5.87778 11.7547 5.83626 11.7904ZM5.19687 11.0383C5.28395 10.9615 5.38758 10.9232 5.50875 10.9232C5.582 10.9232 5.65187 10.9385 5.71837 10.9689C5.78486 10.9996 5.8382 11.0396 5.8787 11.0897C5.91921 11.1397 5.94992 11.2065 5.97119 11.2899H5.05273C5.08548 11.1781 5.13341 11.094 5.19687 11.0383ZM4.45639 10.8445C4.38617 10.7969 4.30564 10.7733 4.21478 10.7733C4.1256 10.7733 4.04273 10.7945 3.96686 10.8369C3.89098 10.8793 3.82176 10.9433 3.75919 11.0291V10.8055H3.60278V12.053H3.75919V11.5954C3.75919 11.4309 3.76652 11.3181 3.78149 11.2562C3.80545 11.1615 3.85471 11.0818 3.92958 11.0178C4.00446 10.9535 4.08899 10.9214 4.1835 10.9214C4.26603 10.9214 4.33326 10.9423 4.38451 10.984C4.43576 11.0257 4.4707 11.0876 4.48867 11.1704C4.50032 11.2186 4.50597 11.3143 4.50597 11.4583V12.0533H4.66239V11.4114C4.66239 11.2418 4.64575 11.1167 4.61213 11.0366C4.57852 10.9556 4.52661 10.8917 4.45639 10.8445ZM12.8146 8.39867V5.86667H13.669V8.41467C13.669 8.90067 13.6164 9.27267 13.3229 9.53167C13.0294 9.79067 12.5968 9.92 12.0253 9.92C11.4538 9.92 11.022 9.79034 10.7291 9.53167C10.4366 9.273 10.3843 8.90134 10.3843 8.41734V5.86667H11.2394V8.39867C11.2394 8.67167 11.2963 8.875 11.4098 9.00867C11.5233 9.14233 11.7705 9.20934 12.0257 9.20934C12.2772 9.20934 12.524 9.14534 12.6402 9.01667C12.7564 8.888 12.8146 8.682 12.8146 8.39867ZM9.02194 5.86667L8.01441 8.853L7.00721 5.86667H6.03979L7.53984 9.92H8.46061L9.96033 5.86667H9.02194ZM3.48276 6.554H2.2251V5.86667H5.61583V6.554H4.35817V9.92H3.48276V6.554Z" fill="#56AF31"/>
      </svg>
      <span class="topbar-title" id="t-product">Usage Service</span>
    </div>

    <nav class="topbar-menu" aria-label="Primary">
      <button class="navitem" type="button">Overview</button>
      <button class="navitem" type="button">Usage</button>
      <button class="navitem" type="button" aria-current="page" id="t-nav-active">Cost per Token</button>
      <button class="navitem" type="button">Invoices</button>
    </nav>

    <div class="topbar-right">
      <!-- SRE / DevOps persona (domain-tvu.md §Persona): data freshness must be
           visible, not buried. icon/Time/clock, verbatim. -->
      <span class="freshness">
        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
          <path d="M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM13.8 7.99922C13.8 4.79597 11.2033 2.19922 8.00001 2.19922C4.79676 2.19922 2.20001 4.79597 2.20001 7.99922C2.20001 11.2025 4.79676 13.7992 8.00001 13.7992C11.2033 13.7992 13.8 11.2025 13.8 7.99922ZM8.59998 4.50039C8.59998 4.16902 8.33135 3.90039 7.99998 3.90039C7.6686 3.90039 7.39998 4.16902 7.39998 4.50039V7.70039L5.63998 9.02039C5.37488 9.21921 5.32115 9.59529 5.51998 9.86039C5.7188 10.1255 6.09488 10.1792 6.35998 9.98039L8.35998 8.48039C8.51106 8.36708 8.59998 8.18925 8.59998 8.00039V4.50039Z" fill="currentColor"/>
        </svg>
        <span id="t-fresh">Updated 25 Aug 2026, 04:15 UTC</span>
      </span>
      <span class="avatar" aria-hidden="true">NZ</span>
    </div>
  </header>

  <div class="page" id="page" data-explain="expanded">

    <main class="main">

      <!-- ── Page head ── -->
      <div class="pagehead">
        <div class="pagehead__top">
          <h1 id="t-title">Cost per Token Trend</h1>
          <span class="badge-line" id="t-keymetric">KEY METRIC</span>
        </div>
        <!-- Business framing, not monitoring framing — carried over from the
             2026-05-11 PM review (user ask: "feel more like a business health /
             profitability indicator rather than a technical monitoring graph"). -->
        <p class="pagehead__sub" id="t-sub">
          What one token of delivered usage costs us — <code>vendor cost ÷ tokens processed</code> — and whether that is improving.
        </p>
      </div>

      <!-- ── Toolbar · requirement 3: time-range entry point ── -->
      <div class="toolbar">
        <span class="toolbar__label" id="t-period">Period</span>

        <!-- Tab List Type=Filled + Tab/Item Type=Filled.
             catalog §Tab/Item: a segmented control IS a tab, not a Button. -->
        <div class="seg">
          <div class="seg__items" role="tablist" aria-label="Time range">
            <button class="seg__item" role="tab" type="button" data-range="30d" aria-selected="true">30D</button>
            <button class="seg__item" role="tab" type="button" data-range="3m"  aria-selected="false">3M</button>
            <button class="seg__item" role="tab" type="button" data-range="6m"  aria-selected="false">6M</button>
            <button class="seg__item" role="tab" type="button" data-range="12m" aria-selected="false">12M</button>
          </div>
        </div>

        <div class="rangewrap">
          <button class="btn btn--ghost-gray" type="button" id="customBtn" aria-haspopup="dialog" aria-expanded="false">
            <!-- icon/Time/calendar, verbatim -->
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
              <path d="M4.6 1C4.26863 1 4 1.26863 4 1.6V2H2.2C1.53726 2 1 2.53726 1 3.2V13.8C1 14.4627 1.53726 15 2.2 15H13.8C14.4627 15 15 14.4627 15 13.8V3.2C15 2.53726 14.4627 2 13.8 2H12V1.6C12 1.26863 11.7314 1 11.4 1C11.0686 1 10.8 1.26863 10.8 1.6V2H5.2V1.6C5.2 1.26863 4.93137 1 4.6 1ZM10.8 3.4C10.8 3.73137 11.0686 4 11.4 4C11.7314 4 12 3.73137 12 3.4V3.20078H13.6C13.7105 3.20078 13.8 3.29032 13.8 3.40078V13.6008C13.8 13.7112 13.7105 13.8008 13.6 13.8008H2.40001C2.28956 13.8008 2.20001 13.7112 2.20001 13.6008V3.40078C2.20001 3.29032 2.28956 3.20078 2.40001 3.20078H4V3.4C4 3.73137 4.26863 4 4.6 4C4.93137 4 5.2 3.73137 5.2 3.4V3.20078H10.8V3.4ZM4 6.1C4 5.76863 4.26863 5.5 4.6 5.5H5.4C5.73137 5.5 6 5.76863 6 6.1C6 6.43137 5.73137 6.7 5.4 6.7H4.6C4.26863 6.7 4 6.43137 4 6.1ZM4.6 8.2C4.26863 8.2 4 8.46863 4 8.8C4 9.13137 4.26863 9.4 4.6 9.4H5.4C5.73137 9.4 6 9.13137 6 8.8C6 8.46863 5.73137 8.2 5.4 8.2H4.6ZM4.6 10.9C4.26863 10.9 4 11.1686 4 11.5C4 11.8314 4.26863 12.1 4.6 12.1H5.4C5.73137 12.1 6 11.8314 6 11.5C6 11.1686 5.73137 10.9 5.4 10.9H4.6ZM7.6 5.5C7.26863 5.5 7 5.76863 7 6.1C7 6.43137 7.26863 6.7 7.6 6.7H8.4C8.73137 6.7 9 6.43137 9 6.1C9 5.76863 8.73137 5.5 8.4 5.5H7.6ZM10 6.1C10 5.76863 10.2686 5.5 10.6 5.5H11.4C11.7314 5.5 12 5.76863 12 6.1C12 6.43137 11.7314 6.7 11.4 6.7H10.6C10.2686 6.7 10 6.43137 10 6.1ZM7.6 8.2C7.26863 8.2 7 8.46863 7 8.8C7 9.13137 7.26863 9.4 7.6 9.4H8.4C8.73137 9.4 9 9.13137 9 8.8C9 8.46863 8.73137 8.2 8.4 8.2H7.6ZM10 8.8C10 8.46863 10.2686 8.2 10.6 8.2H11.4C11.7314 8.2 12 8.46863 12 8.8C12 9.13137 11.7314 9.4 11.4 9.4H10.6C10.2686 9.4 10 9.13137 10 8.8ZM7.6 10.9C7.26863 10.9 7 11.1686 7 11.5C7 11.8314 7.26863 12.1 7.6 12.1H8.4C8.73137 12.1 9 11.8314 9 11.5C9 11.1686 8.73137 10.9 8.4 10.9H7.6ZM10 11.5C10 11.1686 10.2686 10.9 10.6 10.9H11.4C11.7314 10.9 12 11.1686 12 11.5C12 11.8314 11.7314 12.1 11.4 12.1H10.6C10.2686 12.1 10 11.8314 10 11.5Z" fill="currentColor"/>
            </svg>
            <span id="t-custom">Custom range</span>
          </button>

          <div class="popover" id="customPop" role="dialog" aria-modal="false" aria-labelledby="customPopTitle" data-open="false">
            <div class="popover__head">
              <span class="popover__title" id="customPopTitle">Custom range</span>
              <button class="iconbtn" type="button" id="customClose" aria-label="Close">
                <!-- icon/Edit/close, verbatim -->
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                  <path d="M12.5758 13.4247C12.8101 13.659 13.19 13.659 13.4243 13.4247C13.6586 13.1903 13.6586 12.8104 13.4243 12.5761L8.84898 8.00082L13.4248 3.42543C13.6591 3.19113 13.6591 2.81123 13.4248 2.5769C13.1905 2.34258 12.8106 2.34256 12.5763 2.57687L8.00045 7.15229L3.42429 2.57613C3.18997 2.34181 2.81007 2.34181 2.57576 2.57613C2.34145 2.81044 2.34145 3.19034 2.57576 3.42465L7.15188 8.00078L2.57596 12.5763C2.34163 12.8106 2.34161 13.1905 2.57592 13.4248C2.81022 13.6592 3.19012 13.6592 3.42445 13.4249L8.00041 8.84931L12.5758 13.4247Z" fill="currentColor"/>
                </svg>
              </button>
            </div>
            <div class="field">
              <label for="fromDate" id="t-from">From</label>
              <input type="date" id="fromDate" value="2026-05-01">
            </div>
            <div class="field">
              <label for="toDate" id="t-to">To</label>
              <input type="date" id="toDate" value="2026-08-25">
            </div>
            <p class="popover__note" id="t-popnote">Invoice periods are whole days, UTC.</p>
            <div class="popover__actions">
              <button class="btn btn--rimless-gray" type="button" id="customCancel">Cancel</button>
              <button class="btn btn--fill-green" type="button" id="customApply">Apply</button>
            </div>
            <!-- MOCKUP DATA: Apply maps the chosen span onto the nearest preset
                 series and relabels the header. Wire to the real query on build. -->
          </div>
        </div>

        <span class="toolbar__spacer"></span>
        <span class="toolbar__window num" id="windowLabel">27 Jul – 25 Aug 2026</span>
      </div>

      <!-- ── Requirement 1: overview strip ── -->
      <section class="kpis" aria-label="Period overview">
        <div class="kpi kpi--hero">
          <span class="kpi__label" id="t-k1">Cost per token</span>
          <div class="kpi__valuerow">
            <span class="kpi__value num" id="cptValue">$0.0412</span>
            <span class="pill" id="cptPill"></span>
          </div>
          <span class="kpi__foot" id="cptFoot"></span>
        </div>

        <div class="kpi">
          <span class="kpi__label" id="t-k2">Revenue per token</span>
          <div class="kpi__valuerow">
            <span class="kpi__value num" id="rptValue">$0.0988</span>
          </div>
          <span class="kpi__foot"><span id="rptDelta"></span></span>
        </div>

        <div class="kpi">
          <span class="kpi__label" id="t-k3">Gross margin</span>
          <div class="kpi__valuerow">
            <span class="kpi__value num" id="mgnValue">58.3%</span>
          </div>
          <span class="kpi__foot"><span id="mgnDelta"></span></span>
        </div>

        <div class="kpi">
          <span class="kpi__label" id="t-k4">Tokens processed</span>
          <div class="kpi__valuerow">
            <span class="kpi__value num" id="tokValue">14.28</span><span class="kpi__unit">M</span>
          </div>
          <span class="kpi__foot"><span id="tokDelta"></span></span>
        </div>
      </section>

      <!-- ── Requirement 2: trend chart, x axis = time ── -->
      <section class="card">
        <div class="card__head">
          <span class="card__title" id="t-charttitle">Unit cost over time</span>
          <span class="card__hint" id="chartHint">Daily · $ per token</span>
        </div>
        <div class="card__body">
          <!-- 1:1 visual mirror of the Chart canonical (type=line). The canonical
               wraps ECharts, which cannot be consumed here (single file, no CDN),
               so per code-conventions R3 the VISUALS are mirrored, not reinvented:
               palette --chart-color-*, axes/grid --line-divider-light, axis labels
               and legend --text-2 @12px, legend circle 8×8 at the bottom. -->
          <div class="tvu-chart" role="img" aria-labelledby="chartAria">
            <span class="sr-only" id="chartAria"></span>
            <div class="tvu-chart__view" id="chartView"></div>
            <div class="charttip" id="chartTip" data-open="false" role="presentation"></div>
          </div>
          <div class="legend">
            <button class="legend__item" type="button" data-series="cpt" aria-pressed="true">
              <span class="legend__mark legend__mark--series"></span><span id="t-lg1">Cost per token</span>
            </button>
            <button class="legend__item" type="button" data-series="baseline" aria-pressed="true">
              <span class="legend__mark legend__mark--baseline"></span><span id="baselineLegend">Previous period average</span>
            </button>
          </div>
        </div>
        <div class="card__foot">
          <span><span id="t-f1">Volume</span> <b id="footTokens">14.28 M</b> <span id="t-f1b">tokens</span></span>
          <span><span id="t-f2">Vendor cost</span> <b id="footCost">$588.3K</b></span>
          <span><span id="t-f3">Customer payments</span> <b id="footPay">$1.41M</b></span>
          <span><span id="t-f4">Invoices</span> <b id="footInv">312</b></span>
        </div>
      </section>
    </main>

    <!-- ── Requirement 4: collapsible explanation panel (LIBRARY GAP, see CSS) ── -->
    <aside class="explain" id="explain" aria-labelledby="explainTitle">
      <div class="explain__head">
        <!-- icon/Message/info-1, verbatim (currentColor variant, not info-2) -->
        <svg class="info" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
          <path d="M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM13.8 7.99922C13.8 4.79597 11.2033 2.19922 8.00001 2.19922C4.79676 2.19922 2.20001 4.79597 2.20001 7.99922C2.20001 11.2025 4.79676 13.7992 8.00001 13.7992C11.2033 13.7992 13.8 11.2025 13.8 7.99922ZM8.60002 4.6C8.60002 4.93137 8.3314 5.2 8.00002 5.2C7.66865 5.2 7.40002 4.93137 7.40002 4.6C7.40002 4.26863 7.66865 4 8.00002 4C8.3314 4 8.60002 4.26863 8.60002 4.6ZM7.40002 7C7.40002 6.66863 7.66865 6.4 8.00002 6.4C8.3314 6.4 8.60002 6.66863 8.60002 7V11.4C8.60002 11.7314 8.3314 12 8.00002 12C7.66865 12 7.40002 11.7314 7.40002 11.4V7Z" fill="currentColor"/>
        </svg>
        <span class="explain__title" id="explainTitle">How this is calculated</span>
        <span class="explain__rail" id="explainRail" aria-hidden="true">How this is calculated</span>
        <button class="iconbtn" type="button" id="explainToggle"
                aria-controls="explain" aria-expanded="true" aria-label="Collapse explanation panel">
          <!-- icon/Arrow/right-2 (collapse) ⇄ icon/Arrow/left-2 (expand), verbatim -->
          <svg id="explainChevron" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
            <path d="M6.20128 12.9488C5.95374 12.7287 5.93145 12.3494 6.15148 12.1018L9.67727 8.13323C9.74458 8.05747 9.74458 7.94333 9.67727 7.86756L6.15148 3.89902C5.93145 3.65135 5.95374 3.2721 6.20128 3.05195C6.44883 2.8318 6.82788 2.85411 7.04791 3.10178L11.046 7.60189C11.2479 7.82919 11.2479 8.1716 11.046 8.3989L7.04791 12.899C6.82788 13.1467 6.44883 13.169 6.20128 12.9488Z" fill="currentColor"/>
          </svg>
        </button>
      </div>

      <div class="explain__body">
        <div class="sect">
          <span class="sect__h" id="t-s1">The formula</span>
          <div class="formula">
            <code id="t-formula">cost_per_token = vendor_cost ÷ tokens_processed</code>
            <code class="eq" id="formulaWorked"></code>
          </div>
          <p id="t-s1p">One figure per period. The period is whatever range is selected above, compared with the equally long window immediately before it.</p>
        </div>

        <div class="sect">
          <span class="sect__h" id="t-s2">Where the inputs come from</span>
          <dl class="deflist">
            <div>
              <dt id="t-d1">Cloud vendor invoices</dt>
              <dd><code>vendor_cost</code> <span id="t-d1a">and</span> <code>tokens_count</code> <span id="t-d1b">per billing period, summed across all vendors.</span></dd>
            </div>
            <div>
              <dt id="t-d2">Customer payments</dt>
              <dd><code>customer_payment</code> <span id="t-d2a">per period. Only used for the revenue and margin figures.</span></dd>
            </div>
          </dl>
        </div>

        <div class="sect">
          <span class="sect__h" id="t-s3">Derived from the same two inputs</span>
          <dl class="deflist">
            <div>
              <dt id="t-d3">Revenue per token</dt>
              <dd><code>customer_payment ÷ tokens_processed</code></dd>
            </div>
            <div>
              <dt id="t-d4">Gross margin</dt>
              <dd><code>(customer_payment − vendor_cost) ÷ customer_payment</code></dd>
            </div>
            <div>
              <dt id="t-d5">Saved this period</dt>
              <dd><code>(previous_cost_per_token − current_cost_per_token) × tokens_processed</code></dd>
            </div>
          </dl>
        </div>

        <div class="sect">
          <span class="sect__h" id="t-s4">How the direction is decided</span>
          <div class="rulerow">
            <span class="delta delta--good" aria-hidden="true"><span id="ruleGoodIcon"></span>1.9%</span>
            <span id="t-r1">Unit cost down, or margin up — improving.</span>
          </div>
          <div class="rulerow">
            <span class="delta delta--bad" aria-hidden="true"><span id="ruleBadIcon"></span>1.9%</span>
            <span id="t-r2">Unit cost up, or margin down — deteriorating.</span>
          </div>
          <div class="rulerow">
            <span class="delta delta--flat" aria-hidden="true">0.2%</span>
            <span id="t-r3">Movement under 0.5% is treated as flat — no arrow, no colour.</span>
          </div>
          <div class="rulerow">
            <span class="delta delta--context" aria-hidden="true"><span id="ruleCtxIcon"></span>8.4%</span>
            <span id="t-r4">Volume is context, not a verdict — direction only, never coloured good or bad.</span>
          </div>
        </div>

        <div class="sect">
          <span class="sect__h" id="t-s5">What this number does not tell you</span>
          <ul class="notlist">
            <li id="t-n1">Why the cost moved. Attribution to a driver (instance mix, spot pricing, a single vendor) is not derivable from invoices plus payments alone.</li>
            <li id="t-n2">Where it goes next. No forecast is shown — extrapolating from these two inputs would look precise without being reliable.</li>
            <li id="t-n3">Anything below period level. Invoices arrive per billing period, so intra-period spikes are averaged away.</li>
          </ul>
        </div>
      </div>

      <div class="explain__foot">
        <span id="t-foot">Figures recomputed on every range change · source of truth is the vendor invoice ledger.</span>
      </div>
    </aside>
  </div>
</div>

<script>
/* ==========================================================================
 * Product UI strings — code-conventions R16: product strings live in a single
 * const map, never inline bilingual literals. Comments may be bilingual; the
 * runtime UI is single-language (EN, team convention).
 * ========================================================================== */
const T = {
  vsPrev: 'vs',
  saved: 'Saved',
  thisPeriod: 'at last period’s unit cost',
  prevAvg: 'Previous period average',
  perToken: '$ per token',
  daily: 'Daily',
  weekly: 'Weekly',
  monthly: 'Monthly',
  costPerToken: 'Cost per token',
  customApplied: 'Custom',
  flat: 'flat',
  ariaChart: (a, b, first, last) =>
    `Line chart, cost per token, ${a} to ${b}. First point ${first}, last point ${last}.`,
};

/* Library icons, verbatim from src/icons/catalog/generated/arrow.ts.
   Sized to the call site via width/height; paths untouched. */
const ICON = {
  up: '<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M12.7387 7.18614C12.9731 7.42046 13.3529 7.42046 13.5873 7.18614C13.8216 6.95183 13.8216 6.57193 13.5873 6.33762L8.4417 1.19205C8.43644 1.18637 8.43105 1.18077 8.42553 1.17525C8.19122 0.940938 7.81132 0.940938 7.577 1.17525L2.41512 6.33713C2.18081 6.57145 2.18081 6.95135 2.41512 7.18566C2.64944 7.41997 3.02934 7.41997 3.26365 7.18566L7.40144 3.04787L7.40144 14.402C7.40144 14.7333 7.67007 15.002 8.00144 15.002C8.33281 15.002 8.60144 14.7333 8.60144 14.402L8.60144 3.04885L12.7387 7.18614Z" fill="currentColor"/></svg>',
  down: '<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M3.26364 8.81581C3.02933 8.58149 2.64943 8.58149 2.41512 8.81581C2.1808 9.05012 2.1808 9.43002 2.41512 9.66434L7.56068 14.8099C7.56594 14.8156 7.57133 14.8212 7.57685 14.8267C7.81116 15.061 8.19106 15.061 8.42538 14.8267L13.5873 9.66482C13.8216 9.43051 13.8216 9.05061 13.5873 8.81629C13.3529 8.58198 12.973 8.58198 12.7387 8.81629L8.60094 12.9541L8.60094 1.6C8.60094 1.26863 8.33231 1 8.00094 1C7.66957 1 7.40094 1.26863 7.40094 1.6L7.40094 12.9531L3.26364 8.81581Z" fill="currentColor"/></svg>',
  chevronRight: '<path d="M6.20128 12.9488C5.95374 12.7287 5.93145 12.3494 6.15148 12.1018L9.67727 8.13323C9.74458 8.05747 9.74458 7.94333 9.67727 7.86756L6.15148 3.89902C5.93145 3.65135 5.95374 3.2721 6.20128 3.05195C6.44883 2.8318 6.82788 2.85411 7.04791 3.10178L11.046 7.60189C11.2479 7.82919 11.2479 8.1716 11.046 8.3989L7.04791 12.899C6.82788 13.1467 6.44883 13.169 6.20128 12.9488Z" fill="currentColor"/>',
  chevronLeft: '<path d="M9.79874 12.9488C10.0463 12.7287 10.0686 12.3494 9.84854 12.1018L6.32276 8.13323C6.25544 8.05747 6.25544 7.94333 6.32276 7.86756L9.84854 3.89902C10.0686 3.65135 10.0463 3.2721 9.79874 3.05195C9.55119 2.8318 9.17215 2.85411 8.95211 3.10178L4.95406 7.60189C4.75213 7.82919 4.75213 8.1716 4.95406 8.3989L8.95211 12.899C9.17215 13.1467 9.55119 13.169 9.79874 12.9488Z" fill="currentColor"/>',
};
const arrowSvg = (dir, px) =>
  ICON[dir].replace('<svg ', `<svg width="${px}" height="${px}" `);

/* ==========================================================================
 * MOCKUP DATA — illustrative, internally consistent.
 * Only primitives are stored (series, previous-period unit cost, token volume,
 * revenue per token). Every displayed figure — period unit cost, deltas, margin,
 * vendor cost, customer payments, savings — is COMPUTED from these below, using
 * the exact formulas printed in the explanation panel. That is deliberate: the
 * numbers on screen cannot drift away from the formulas next to them.
 * Metric set follows the data-feasibility audit in
 * docs/_archive/retrospection/2026-05-11-saas-dashboard-2-option-pm-review.md
 * (driver attribution / forecast excluded — not derivable from the two inputs).
 * ========================================================================== */
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

const RANGES = {
  '30d': {
    tab: '30D', grain: T.daily, prevLabel: 'prev. 30 days',
    start: [2026, 6, 27], step: 'day',
    tokens: 14.28e6, tokensPrev: 13.17e6,
    rpt: 0.0988, rptPrev: 0.0982, cptPrev: 0.0420, invoices: 312,
    series: [
      0.0419, 0.0421, 0.0417, 0.0418, 0.0415, 0.0416, 0.0420, 0.0414, 0.0413, 0.0415,
      0.0412, 0.0414, 0.0410, 0.0413, 0.0411, 0.0412, 0.0409, 0.0411, 0.0413, 0.0410,
      0.0408, 0.0410, 0.0407, 0.0409, 0.0411, 0.0408, 0.0406, 0.0408, 0.0405, 0.0406,
    ],
  },
  '3m': {
    tab: '3M', grain: T.weekly, prevLabel: 'prev. 3 months',
    start: [2026, 5, 1], step: 'week',
    tokens: 41.6e6, tokensPrev: 38.2e6,
    rpt: 0.0985, rptPrev: 0.0979, cptPrev: 0.0435, invoices: 934,
    series: [
      0.0432, 0.0429, 0.0431, 0.0426, 0.0424, 0.0425, 0.0421,
      0.0419, 0.0417, 0.0414, 0.0412, 0.0410, 0.0408,
    ],
  },
  '6m': {
    tab: '6M', grain: T.weekly, prevLabel: 'prev. 6 months',
    start: [2026, 2, 2], step: 'week',
    tokens: 78.4e6, tokensPrev: 70.1e6,
    rpt: 0.0981, rptPrev: 0.0974, cptPrev: 0.0459, invoices: 1861,
    series: [
      0.0455, 0.0452, 0.0454, 0.0449, 0.0447, 0.0448, 0.0444, 0.0442, 0.0440,
      0.0437, 0.0435, 0.0433, 0.0432, 0.0429, 0.0431, 0.0426, 0.0424, 0.0425,
      0.0421, 0.0419, 0.0417, 0.0414, 0.0412, 0.0410, 0.0409, 0.0408,
    ],
  },
  '12m': {
    tab: '12M', grain: T.monthly, prevLabel: 'prev. 12 months',
    start: [2025, 8, 1], step: 'month',
    tokens: 142.6e6, tokensPrev: 118.4e6,
    rpt: 0.0976, rptPrev: 0.0968, cptPrev: 0.0496, invoices: 3702,
    series: [
      0.0474, 0.0468, 0.0471, 0.0463, 0.0455, 0.0449,
      0.0452, 0.0443, 0.0434, 0.0428, 0.0420, 0.0412,
    ],
  },
};

/* ── Derivation: everything on screen comes out of here ── */
function derive(r) {
  const n = r.series.length;
  const cpt = r.series.reduce((a, b) => a + b, 0) / n;
  const vendorCost = cpt * r.tokens;
  const customerPay = r.rpt * r.tokens;
  const margin = (r.rpt - cpt) / r.rpt * 100;
  const marginPrev = (r.rptPrev - r.cptPrev) / r.rptPrev * 100;
  return {
    cpt, vendorCost, customerPay, margin, marginPrev,
    cptDelta: (cpt - r.cptPrev) / r.cptPrev * 100,
    rptDelta: (r.rpt - r.rptPrev) / r.rptPrev * 100,
    marginDelta: margin - marginPrev,
    tokensDelta: (r.tokens - r.tokensPrev) / r.tokensPrev * 100,
    saved: Math.max(0, (r.cptPrev - cpt) * r.tokens),
    baseline: r.cptPrev,
  };
}

/* ── Formatters ── */
const fmtCpt = v => '$' + v.toFixed(4);
const fmtMoney = v =>
  v >= 1e6 ? '$' + (v / 1e6).toFixed(2) + 'M'
  : v >= 1e3 ? '$' + (v / 1e3).toFixed(1) + 'K'
  : '$' + Math.round(v).toLocaleString('en-US');
const fmtTokens = v => (v / 1e6).toFixed(v >= 100e6 ? 1 : 2) + ' M';
const fmtPct = v => Math.abs(v).toFixed(1) + '%';
const fmtPts = v => Math.abs(v).toFixed(1) + ' pt';

/* ── Date labels ── */
function labelAt(r, i) {
  const [y, m, d] = r.start;
  if (r.step === 'month') {
    const t = new Date(y, m + i, 1);
    return MONTHS[t.getMonth()] + ' ' + String(t.getFullYear()).slice(2);
  }
  const t = new Date(y, m, d + (r.step === 'week' ? i * 7 : i));
  return t.getDate() + ' ' + MONTHS[t.getMonth()];
}
function longLabelAt(r, i) {
  const base = labelAt(r, i);
  return r.step === 'week' ? 'Week of ' + base : base;
}

/* ==========================================================================
 * M11 two-layer trend signal.
 * Arrow = the direction the number actually moved.
 * Colour = whether that is an improvement, a deterioration, or neither.
 *   improving   → --brand    deteriorating → --orange
 *   flat (<0.5%)→ --text-body (no arrow)   volume/context → --text-tips
 * Note: M11's own table pairs ↓ with green because it was written for
 * lower-is-better metrics. Gross margin is higher-is-better, so arrow and colour
 * are decided separately and the two-layer contract still holds.
 * The token M11 names for deterioration, Color/Text/Warning (--text-warning),
 * does NOT exist in src/tokens/variables.css (grepped: zero hits); --orange
 * carries the same value (#f68512 dark) and is used instead of any raw hex.
 * ========================================================================== */
const FLAT = 0.5;
function signal(delta, polarity) {
  // polarity: 'lower-better' | 'higher-better' | 'context'
  const dir = delta > 0 ? 'up' : 'down';
  if (polarity === 'context') return { cls: 'delta--context', dir, arrow: true };
  if (Math.abs(delta) < FLAT) return { cls: 'delta--flat', dir, arrow: false };
  const good = polarity === 'lower-better' ? delta < 0 : delta > 0;
  return { cls: good ? 'delta--good' : 'delta--bad', dir, arrow: true };
}
function deltaHtml(delta, polarity, text, extra) {
  const s = signal(delta, polarity);
  const icon = s.arrow ? arrowSvg(s.dir, 12) : '';
  return `<span class="delta ${s.cls}">${icon}${text}${extra ? ' ' + extra : ''}</span>`;
}
function pillHtml(delta, polarity, text, suffix) {
  const s = signal(delta, polarity);
  const cls = s.cls.replace('delta--', 'pill--');
  const icon = s.arrow ? arrowSvg(s.dir, 14) : '';
  return { cls: 'pill ' + cls, html: `${icon}<span>${text} ${suffix}</span>` };
}

/* ==========================================================================
 * Chart — visual mirror of canonical Chart (type=line). Rendered as inline SVG
 * because ECharts cannot be bundled here; all colours resolve from the same
 * tokens the canonical resolves at runtime (use-chart-tokens.ts).
 * ========================================================================== */
const CH = { padL: 58, padR: 16, padT: 14, padB: 26, h: 300 };
let activeRange = '30d';
let showBaseline = true;
let showSeries = true;
let hoverIdx = -1;
let geom = null;

function niceStep(span) {
  const cands = [0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01];
  for (const c of cands) if (span / c <= 5) return c;
  return 0.02;
}

function renderChart() {
  const r = RANGES[activeRange];
  const d = derive(r);
  const view = document.getElementById('chartView');
  const w = Math.max(320, view.clientWidth || 640);
  const h = CH.h;

  const vals = showSeries ? r.series.slice() : [];
  if (showBaseline) vals.push(d.baseline);
  const lo = Math.min.apply(null, vals.length ? vals : r.series);
  const hi = Math.max.apply(null, vals.length ? vals : r.series);
  const step = niceStep(Math.max(hi - lo, 0.0004));
  const yMin = Math.floor((lo - step * 0.35) / step) * step;
  const yMax = Math.ceil((hi + step * 0.35) / step) * step;

  const x0 = CH.padL, x1 = w - CH.padR, y0 = CH.padT, y1 = h - CH.padB;
  const n = r.series.length;
  const xOf = i => n === 1 ? (x0 + x1) / 2 : x0 + (x1 - x0) * i / (n - 1);
  const yOf = v => y1 - (y1 - y0) * (v - yMin) / (yMax - yMin);
  geom = { xOf, yOf, x0, x1, y0, y1, n, r, d };

  const ticks = [];
  for (let v = yMin; v <= yMax + 1e-9; v += step) ticks.push(+v.toFixed(6));

  const labelStep = Math.max(1, Math.ceil(n / 6));
  const xIdx = [];
  for (let i = 0; i < n; i += labelStep) xIdx.push(i);
  if (xIdx[xIdx.length - 1] !== n - 1) xIdx.push(n - 1);

  let s = `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`;
  s += `<defs><linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="var(--chart-color-1)" stop-opacity="0.30"/>
          <stop offset="1" stop-color="var(--chart-color-1)" stop-opacity="0"/>
        </linearGradient></defs>`;

  // splitLine + axisLabel — build-chart-option.ts: splitLine/axisLine/axisTick all
  // take ctx.axisLineColor (--line-divider-light); tick labels take legendColor (--text-2).
  ticks.forEach(v => {
    const y = yOf(v);
    s += `<line x1="${x0}" y1="${y.toFixed(1)}" x2="${x1}" y2="${y.toFixed(1)}" stroke="var(--line-divider-light)" stroke-width="1"/>`;
    s += `<text x="${x0 - 8}" y="${(y + 4).toFixed(1)}" text-anchor="end" font-size="12" fill="var(--text-2)" font-family="var(--font-family-base)" style="font-variant-numeric:tabular-nums">$${v.toFixed(4)}</text>`;
  });

  // axisLine (category axis) + axisTick
  s += `<line x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}" stroke="var(--line-divider-light)" stroke-width="1"/>`;
  xIdx.forEach(i => {
    const x = xOf(i);
    s += `<line x1="${x.toFixed(1)}" y1="${y1}" x2="${x.toFixed(1)}" y2="${y1 + 4}" stroke="var(--line-divider-light)" stroke-width="1"/>`;
    const anchor = i === 0 ? 'start' : (i === n - 1 ? 'end' : 'middle');
    s += `<text x="${x.toFixed(1)}" y="${y1 + 18}" text-anchor="${anchor}" font-size="12" fill="var(--text-2)" font-family="var(--font-family-base)">${labelAt(r, i)}</text>`;
  });

  if (showSeries) {
    const pts = r.series.map((v, i) => `${xOf(i).toFixed(1)},${yOf(v).toFixed(1)}`);
    s += `<path d="M ${x0.toFixed(1)},${y1} L ${pts.join(' L ')} L ${x1.toFixed(1)},${y1} Z" fill="url(#areaFill)"/>`;
    s += `<polyline points="${pts.join(' ')}" fill="none" stroke="var(--chart-color-1)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>`;
    r.series.forEach((v, i) => {
      s += `<circle cx="${xOf(i).toFixed(1)}" cy="${yOf(v).toFixed(1)}" r="${i === hoverIdx ? 5 : 3}" fill="var(--bg-layer3)" stroke="var(--chart-color-1)" stroke-width="2"/>`;
    });
  }

  if (showBaseline) {
    const yb = yOf(d.baseline);
    s += `<line x1="${x0}" y1="${yb.toFixed(1)}" x2="${x1}" y2="${yb.toFixed(1)}" stroke="var(--text-tips)" stroke-width="2" stroke-dasharray="6 5"/>`;
    s += `<text x="${x1}" y="${(yb - 8).toFixed(1)}" text-anchor="end" font-size="12" fill="var(--text-tips)" font-family="var(--font-family-base)" style="font-variant-numeric:tabular-nums">${fmtCpt(d.baseline)}</text>`;
  }

  if (hoverIdx >= 0 && showSeries) {
    const x = xOf(hoverIdx);
    s += `<line x1="${x.toFixed(1)}" y1="${y0}" x2="${x.toFixed(1)}" y2="${y1}" stroke="var(--line-border)" stroke-width="1" stroke-dasharray="4 4"/>`;
  }

  // Transparent capture layer for pointer + keyboard cursor.
  s += `<rect class="tvu-chart__hit" id="chartHit" x="${x0}" y="${y0}" width="${(x1 - x0).toFixed(1)}" height="${(y1 - y0).toFixed(1)}" fill="transparent" tabindex="0" role="slider" aria-label="Chart cursor" aria-valuemin="0" aria-valuemax="${n - 1}" aria-valuenow="${Math.max(0, hoverIdx)}"/>`;
  s += `</svg>`;

  view.innerHTML = s;
  document.getElementById('chartAria').textContent =
    T.ariaChart(labelAt(r, 0), labelAt(r, n - 1), fmtCpt(r.series[0]), fmtCpt(r.series[n - 1]));
  bindHit();
}

function bindHit() {
  const hit = document.getElementById('chartHit');
  if (!hit) return;
  hit.addEventListener('mousemove', e => {
    const box = document.getElementById('chartView').getBoundingClientRect();
    const px = e.clientX - box.left;
    let best = 0, bestD = Infinity;
    for (let i = 0; i < geom.n; i++) {
      const dd = Math.abs(geom.xOf(i) - px);
      if (dd < bestD) { bestD = dd; best = i; }
    }
    if (best !== hoverIdx) { hoverIdx = best; renderChart(); }
    showTip(best);
  });
  hit.addEventListener('mouseleave', () => { hideTip(); hoverIdx = -1; renderChart(); });
  hit.addEventListener('keydown', e => {
    if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
    e.preventDefault();
    const next = Math.min(geom.n - 1, Math.max(0, (hoverIdx < 0 ? 0 : hoverIdx) + (e.key === 'ArrowRight' ? 1 : -1)));
    hoverIdx = next; renderChart();
    document.getElementById('chartHit').focus();
    showTip(next);
  });
  hit.addEventListener('blur', () => hideTip());
}

function showTip(i) {
  const r = RANGES[activeRange], d = derive(r);
  const tip = document.getElementById('chartTip');
  const v = r.series[i];
  const vsBase = (v - d.baseline) / d.baseline * 100;
  const s = signal(vsBase, 'lower-better');
  tip.innerHTML =
    `<div class="charttip__row"><strong>${longLabelAt(r, i)}</strong></div>` +
    `<div class="charttip__row"><span class="charttip__dot" style="background:var(--chart-color-1)"></span>` +
      `<span class="charttip__k">${T.costPerToken}</span><span class="charttip__v">${fmtCpt(v)}</span></div>` +
    `<div class="charttip__row"><span class="charttip__k">${T.vsPrev} ${T.prevAvg.toLowerCase()}</span>` +
      `<span class="charttip__v" style="color:${s.cls === 'delta--good' ? 'var(--brand)' : s.cls === 'delta--bad' ? 'var(--orange)' : 'var(--tooltip-text-dark)'}">` +
      `${Math.abs(vsBase) < FLAT ? T.flat : (vsBase > 0 ? '+' : '−') + fmtPct(vsBase)}</span></div>`;
  const chart = document.querySelector('.tvu-chart').getBoundingClientRect();
  const view = document.getElementById('chartView').getBoundingClientRect();
  tip.style.left = (view.left - chart.left + geom.xOf(i)) + 'px';
  tip.style.top = (view.top - chart.top + geom.yOf(r.series[i]) - 12) + 'px';
  tip.dataset.open = 'true';
}
function hideTip() { document.getElementById('chartTip').dataset.open = 'false'; }

/* ── Render KPIs, footer, panel worked example ── */
function renderAll() {
  const r = RANGES[activeRange], d = derive(r);

  document.getElementById('cptValue').textContent = fmtCpt(d.cpt);
  const p = pillHtml(d.cptDelta, 'lower-better', fmtPct(d.cptDelta), `${T.vsPrev} ${r.prevLabel}`);
  const pill = document.getElementById('cptPill');
  pill.className = p.cls;
  pill.innerHTML = p.html;

  document.getElementById('cptFoot').innerHTML =
    d.saved > 0
      ? `${T.saved} <strong>${fmtMoney(d.saved)}</strong> ${T.thisPeriod}`
      : `No saving against ${r.prevLabel}`;

  document.getElementById('rptValue').textContent = fmtCpt(r.rpt);
  document.getElementById('rptDelta').innerHTML =
    deltaHtml(d.rptDelta, 'higher-better', fmtPct(d.rptDelta), `${T.vsPrev} ${r.prevLabel}`);

  document.getElementById('mgnValue').textContent = d.margin.toFixed(1) + '%';
  document.getElementById('mgnDelta').innerHTML =
    deltaHtml(d.marginDelta, 'higher-better', fmtPts(d.marginDelta), `${T.vsPrev} ${r.prevLabel}`);

  document.getElementById('tokValue').textContent = fmtTokens(r.tokens).replace(' M', '');
  document.getElementById('tokDelta').innerHTML =
    deltaHtml(d.tokensDelta, 'context', fmtPct(d.tokensDelta), `${T.vsPrev} ${r.prevLabel}`);

  document.getElementById('footTokens').textContent = fmtTokens(r.tokens);
  document.getElementById('footCost').textContent = fmtMoney(d.vendorCost);
  document.getElementById('footPay').textContent = fmtMoney(d.customerPay);
  document.getElementById('footInv').textContent = r.invoices.toLocaleString('en-US');

  document.getElementById('chartHint').textContent = `${r.grain} · ${T.perToken}`;
  document.getElementById('baselineLegend').textContent = `${T.prevAvg} (${fmtCpt(d.baseline)})`;
  document.getElementById('windowLabel').textContent =
    `${labelAt(r, 0)} – ${labelAt(r, r.series.length - 1)} 2026`;

  document.getElementById('formulaWorked').textContent =
    `${fmtCpt(d.cpt)} = ${fmtMoney(d.vendorCost)} ÷ ${fmtTokens(r.tokens)}`;

  document.getElementById('ruleGoodIcon').innerHTML = arrowSvg('down', 12);
  document.getElementById('ruleBadIcon').innerHTML = arrowSvg('up', 12);
  document.getElementById('ruleCtxIcon').innerHTML = arrowSvg('up', 12);

  hoverIdx = -1; hideTip();
  renderChart();
}

/* ── Range switching ── */
document.querySelectorAll('.seg__item').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.seg__item').forEach(b =>
      b.setAttribute('aria-selected', String(b === btn)));
    activeRange = btn.dataset.range;
    document.getElementById('t-custom').textContent = 'Custom range';
    renderAll();
  });
});

/* ── Custom range popover ── */
const pop = document.getElementById('customPop');
const customBtn = document.getElementById('customBtn');
function setPop(open) {
  pop.dataset.open = String(open);
  customBtn.setAttribute('aria-expanded', String(open));
  if (open) document.getElementById('fromDate').focus();
}
customBtn.addEventListener('click', () => setPop(pop.dataset.open !== 'true'));
document.getElementById('customClose').addEventListener('click', () => { setPop(false); customBtn.focus(); });
document.getElementById('customCancel').addEventListener('click', () => { setPop(false); customBtn.focus(); });
document.getElementById('customApply').addEventListener('click', () => {
  const from = new Date(document.getElementById('fromDate').value);
  const to = new Date(document.getElementById('toDate').value);
  const days = Math.max(1, Math.round((to - from) / 864e5));
  // MOCKUP DATA: pick the preset series closest to the chosen span.
  activeRange = days <= 45 ? '30d' : days <= 135 ? '3m' : days <= 270 ? '6m' : '12m';
  document.querySelectorAll('.seg__item').forEach(b => b.setAttribute('aria-selected', 'false'));
  document.getElementById('t-custom').textContent = T.customApplied + ' · ' + days + 'd';
  setPop(false);
  renderAll();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape' && pop.dataset.open === 'true') { setPop(false); customBtn.focus(); } });
document.addEventListener('click', e => {
  if (pop.dataset.open === 'true' && !pop.contains(e.target) && !customBtn.contains(e.target)) setPop(false);
});

/* ── Legend series toggles ── */
document.querySelectorAll('.legend__item').forEach(btn => {
  btn.addEventListener('click', () => {
    const on = btn.getAttribute('aria-pressed') !== 'true';
    if (btn.dataset.series === 'baseline') showBaseline = on; else showSeries = on;
    btn.setAttribute('aria-pressed', String(on));
    renderChart();
  });
});

/* ── Explanation panel collapse ── */
const page = document.getElementById('page');
const toggle = document.getElementById('explainToggle');
toggle.addEventListener('click', () => {
  const collapsed = page.dataset.explain === 'collapsed';
  page.dataset.explain = collapsed ? 'expanded' : 'collapsed';
  toggle.setAttribute('aria-expanded', String(collapsed));
  toggle.setAttribute('aria-label', collapsed ? 'Collapse explanation panel' : 'Expand explanation panel');
  document.getElementById('explainChevron').innerHTML = collapsed ? ICON.chevronRight : ICON.chevronLeft;
  requestAnimationFrame(renderChart);
});
document.getElementById('explainRail').addEventListener('click', () => toggle.click());

/* ── Re-render the chart on container resize (px-space SVG, not a stretched viewBox) ── */
if (window.ResizeObserver) {
  new ResizeObserver(() => renderChart()).observe(document.getElementById('chartView'));
} else {
  window.addEventListener('resize', renderChart);
}

renderAll();
</script>
</body>
</html>
```

## 六、交付自审 + 你该知道的边界

**Acceptance（R15/R2/R3 build 自审，逐条可答）**
- 自画 SVG 图标数量 = **0**。9 个图标全部逐字复制自 `src/icons/catalog/generated/*.ts`（arrow ×4、message/info-1、time/calendar、time/clock、edit/close、logo/tvu），搜索词与候选见上面 R13 trace。
- hex literal = **1 个**，`#56AF31`，是 `logo/tvu` 库资产自带（登记在案的 `icon-variable-resolved-to-hex` divergence）。其余颜色 100% `var(--*)`；橙色软底走 `color-mix(in srgb, var(--orange) 18%, transparent)` —— 那是 `Pill.vue` 自己 `bgCss` 契约写明允许的 token 表达式。
- 复合组件 mirror 来源已逐个标在 CSS 注释里（TopBar.vue / TabItem.vue / TabList.vue / Button.vue `PALETTE_SPEC` / Pill.vue / canonical Badge.vue / Chart.vue + `build-chart-option.ts` + `use-chart-tokens.ts`），无一处按自己理解重设计 API。

**三处我做了判断、你可以推翻**
1. **顶部概览区 4 格**里，hero 那格是你要的；右侧 3 格（Revenue per token / Gross margin / Tokens）是给 CFO persona 的 margin 语境，**超出你第 1 条的字面范围**。要严格回 phase-1 就删那 3 格。
2. **Export 按钮我删了** —— 不在你的 4 条要求内，而且单文件里做不出真导出，留着就是假 affordance（M49 Simplicity Contract）。
3. **自定义区间的 Apply** 把选中跨度映射到最近的预设数据集（代码里 `MOCKUP DATA:` 注释标了）—— mockup 层面是对的，接真查询时替换那一行。

**没做的（不是"做完了"）**：`design:kickoff --check`、`audit:no-hardcoded-design-tokens`、`audit:consumer-code`（R14/R19/R9 probe）、`audit:layout-tokens` 全部**未运行**（Bash 停用）。断点字面量我已人工限在 `{640,960,1100,1280}` 这四个 token 值内，但那是自查不是机检。落仓后请补跑，`--no-verify` 别用。

**右侧面板是真 library gap**：catalog 与 `src/canonical/` 都没有 Drawer / Collapse / Accordion / Panel。我按 R15 优先级 3 用库 primitive 拼的，代码里已标 `<!-- LIBRARY GAP … candidate for tvu-design-system intake -->`。这条值得进 `backlog.md` 的 BRIDGE-CODE-CANONICAL 段——**我没写**（只读会话），要立的话下个 session 记得先 `pnpm new-backlog` 取号。